@qwen-code/arena-agent 0.0.10-dev.6 → 0.0.10-dev.8
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/.env.example +20 -9
- package/CHANGELOG.md +188 -0
- package/README.md +21 -19
- package/dist/agent.d.ts +8 -0
- package/dist/agent.js +32 -12
- package/dist/agent.js.map +1 -1
- package/dist/cli.js +87 -20
- package/dist/cli.js.map +1 -1
- package/dist/code-cli.js +0 -0
- package/dist/code-tools.d.ts +5 -0
- package/dist/code-tools.js +254 -64
- package/dist/code-tools.js.map +1 -1
- package/dist/compaction.d.ts +5 -0
- package/dist/compaction.js +57 -6
- package/dist/compaction.js.map +1 -1
- package/dist/config.d.ts +0 -2
- package/dist/config.js +1 -3
- package/dist/config.js.map +1 -1
- package/dist/file-edit.d.ts +2 -0
- package/dist/file-edit.js +9 -4
- package/dist/file-edit.js.map +1 -1
- package/dist/history.js +17 -0
- package/dist/history.js.map +1 -1
- package/dist/midturn-checkpoint-hook.d.ts +16 -0
- package/dist/midturn-checkpoint-hook.js +30 -0
- package/dist/midturn-checkpoint-hook.js.map +1 -0
- package/dist/provider.d.ts +0 -1
- package/dist/provider.js +13 -42
- package/dist/provider.js.map +1 -1
- package/dist/replay-responses.d.ts +17 -0
- package/dist/replay-responses.js +100 -0
- package/dist/replay-responses.js.map +1 -0
- package/dist/routify-web-fetch.js +18 -4
- package/dist/routify-web-fetch.js.map +1 -1
- package/dist/snapshot-client.d.ts +10 -0
- package/dist/snapshot-client.js +39 -0
- package/dist/snapshot-client.js.map +1 -0
- package/dist/tools.d.ts +2 -0
- package/dist/tools.js +134 -23
- package/dist/tools.js.map +1 -1
- package/dist/tui.js +1 -1
- package/dist/tui.js.map +1 -1
- package/dist/web-fetch-output.d.ts +9 -0
- package/dist/web-fetch-output.js +38 -0
- package/dist/web-fetch-output.js.map +1 -0
- package/docs/BINARY_DISTRIBUTION.md +25 -16
- package/docs/MODES_AND_RELEASES.md +28 -5
- package/package.json +5 -1
- package/scripts/README.md +62 -0
- package/scripts/check-changelog.mjs +66 -0
- package/dist/web/app.d.ts +0 -1
package/dist/code-tools.js
CHANGED
|
@@ -12,11 +12,13 @@ import iconv from "iconv-lite";
|
|
|
12
12
|
import mime from "mime/lite";
|
|
13
13
|
import { executeBraveWebSearch } from "./brave-search.js";
|
|
14
14
|
import { FetchPageReader } from "./fetch-page.js";
|
|
15
|
-
import { replaceUniqueText } from "./file-edit.js";
|
|
15
|
+
import { replaceFirstText, replaceUniqueText } from "./file-edit.js";
|
|
16
16
|
import { generatedImageBytesForExtension } from "./image.js";
|
|
17
17
|
import { generateImageWithModelRouter, } from "./model-router-media.js";
|
|
18
18
|
import { executeRoutifyWebFetch, routifyWebFetchEnabled, } from "./routify-web-fetch.js";
|
|
19
|
+
import { replayResponseFor } from "./replay-responses.js";
|
|
19
20
|
import { toolDefinitions } from "./spec.js";
|
|
21
|
+
import { boundedWebFetchResult, truncateCodeArenaText } from "./web-fetch-output.js";
|
|
20
22
|
const knownTextApplicationMimeTypes = new Set([
|
|
21
23
|
"application/ecmascript",
|
|
22
24
|
"application/javascript",
|
|
@@ -230,6 +232,7 @@ const visualIntroText = "<code-harness-system-message>\n" +
|
|
|
230
232
|
"</code-harness-system-message>";
|
|
231
233
|
const bashOutputLimit = 20_000;
|
|
232
234
|
const lintOutputLimit = 10_000;
|
|
235
|
+
const grepLineCharacterLimit = 2_000;
|
|
233
236
|
// 中段截断,逐字反推自 3 个 truncated:true 参考样本:头尾各 9987 字符 +
|
|
234
237
|
// 26 字符标记,总长恰 20000(由 T1(b) 执行回放发现,非首段截断)。
|
|
235
238
|
const bashTruncationMarker = "\n... output truncated ...\n";
|
|
@@ -380,6 +383,14 @@ function workspaceFileKind(filePath, sample) {
|
|
|
380
383
|
function json(value) {
|
|
381
384
|
return JSON.stringify(value);
|
|
382
385
|
}
|
|
386
|
+
function capGrepLine(value) {
|
|
387
|
+
// Production captures cap every returned grep line independently. This is
|
|
388
|
+
// intentionally a prefix without a marker, including for the matching line.
|
|
389
|
+
return value.slice(0, grepLineCharacterLimit);
|
|
390
|
+
}
|
|
391
|
+
function capGrepLines(values) {
|
|
392
|
+
return values.map(capGrepLine);
|
|
393
|
+
}
|
|
383
394
|
function errorMessage(error) {
|
|
384
395
|
return error instanceof Error ? error.message : String(error);
|
|
385
396
|
}
|
|
@@ -451,9 +462,14 @@ function matchesGlob(filePath, pattern) {
|
|
|
451
462
|
matcher.test(path.posix.basename(normalized))));
|
|
452
463
|
});
|
|
453
464
|
}
|
|
454
|
-
function packageNameIsSafe(value) {
|
|
465
|
+
function packageNameIsSafe(value, mode) {
|
|
455
466
|
if (!value || value.startsWith("-") || /\s/.test(value))
|
|
456
467
|
return false;
|
|
468
|
+
if (mode === "react") {
|
|
469
|
+
// React captures reject version ranges before invoking npm, while accepting
|
|
470
|
+
// exact versions and tags such as latest. Keep other modes' validator intact.
|
|
471
|
+
return /^(?:@[\w.-]+\/)?[\w.-]+(?:@[\w.+-]+)?$/.test(value);
|
|
472
|
+
}
|
|
457
473
|
return /^(?:@[\w.-]+\/)?[\w.-]+(?:@(?:[\w*^<>=|.+-]+))?$/.test(value);
|
|
458
474
|
}
|
|
459
475
|
function valueAtPath(value, instancePath) {
|
|
@@ -757,6 +773,125 @@ export class CodeToolExecutor {
|
|
|
757
773
|
const names = this.definitions.map((tool) => tool.function.name);
|
|
758
774
|
return `Model tried to call unavailable tool '${name}'. Available tools: ${names.join(", ")}.`;
|
|
759
775
|
}
|
|
776
|
+
rawArguments(call) {
|
|
777
|
+
return typeof call.function?.arguments === "string"
|
|
778
|
+
? call.function.arguments
|
|
779
|
+
: "{}";
|
|
780
|
+
}
|
|
781
|
+
normalizeArguments(name, parsedArguments) {
|
|
782
|
+
// Keep the parsed value available to checkpoint hooks while applying the
|
|
783
|
+
// same captured runtime defaults to the independent dispatch value.
|
|
784
|
+
const argumentsValue = (this.config.mode === "react" && typeof parsedArguments === "string"
|
|
785
|
+
? {}
|
|
786
|
+
: structuredClone(parsedArguments));
|
|
787
|
+
// The captured provider advertises these fields as required, but its
|
|
788
|
+
// runtime still applies the documented defaults when a model omits
|
|
789
|
+
// them. Preserve the byte-identical outgoing schema while matching the
|
|
790
|
+
// observed execution behavior.
|
|
791
|
+
if (name === "read_file") {
|
|
792
|
+
argumentsValue.offset ??= 1;
|
|
793
|
+
argumentsValue.limit ??= 2000;
|
|
794
|
+
}
|
|
795
|
+
else if (name === "list_files" || name === "glob_files") {
|
|
796
|
+
argumentsValue.path ??= "";
|
|
797
|
+
}
|
|
798
|
+
else if (this.config.mode === "full-stack" && name === "bash" &&
|
|
799
|
+
argumentsValue && typeof argumentsValue === "object" &&
|
|
800
|
+
!Array.isArray(argumentsValue) && argumentsValue.workdir === undefined) {
|
|
801
|
+
argumentsValue.workdir = "";
|
|
802
|
+
}
|
|
803
|
+
else if (name === "grep_files") {
|
|
804
|
+
argumentsValue.path ??= "";
|
|
805
|
+
argumentsValue.glob ??= "";
|
|
806
|
+
argumentsValue.output_mode ??= "content";
|
|
807
|
+
argumentsValue["-i"] ??= false;
|
|
808
|
+
argumentsValue["-B"] ??= 0;
|
|
809
|
+
argumentsValue["-A"] ??= 0;
|
|
810
|
+
argumentsValue["-C"] ??= 0;
|
|
811
|
+
argumentsValue.context ??= 0;
|
|
812
|
+
}
|
|
813
|
+
return argumentsValue;
|
|
814
|
+
}
|
|
815
|
+
validationMessage(name, value, validate) {
|
|
816
|
+
if (validate(value))
|
|
817
|
+
return undefined;
|
|
818
|
+
const issues = validationIssues(name, value, validate.errors || [], this.definitions);
|
|
819
|
+
return (`Invalid input for tool ${name}: Type validation failed: ` +
|
|
820
|
+
`Value: ${JSON.stringify(value)}.\n` +
|
|
821
|
+
`Error message: ${JSON.stringify(issues, null, 2)}`);
|
|
822
|
+
}
|
|
823
|
+
inspect(call) {
|
|
824
|
+
const id = String(call.id || "");
|
|
825
|
+
const name = String(call.function?.name || "");
|
|
826
|
+
const rawArguments = this.rawArguments(call);
|
|
827
|
+
const validate = this.validators.get(name);
|
|
828
|
+
if (!validate) {
|
|
829
|
+
let parsedArguments = undefined;
|
|
830
|
+
let parsed = false;
|
|
831
|
+
try {
|
|
832
|
+
parsedArguments = JSON.parse(rawArguments);
|
|
833
|
+
parsed = true;
|
|
834
|
+
}
|
|
835
|
+
catch {
|
|
836
|
+
// Tool-name validation takes precedence. Parsing is best-effort here
|
|
837
|
+
// only so a valid argument payload remains available to the hook.
|
|
838
|
+
}
|
|
839
|
+
return {
|
|
840
|
+
id,
|
|
841
|
+
name,
|
|
842
|
+
rawArguments,
|
|
843
|
+
...(parsed ? { arguments: parsedArguments } : {}),
|
|
844
|
+
validation: "unknown_tool",
|
|
845
|
+
validationMessage: this.unavailableToolMessage(name),
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
let parsedArguments;
|
|
849
|
+
try {
|
|
850
|
+
parsedArguments = JSON.parse(rawArguments);
|
|
851
|
+
}
|
|
852
|
+
catch (error) {
|
|
853
|
+
return {
|
|
854
|
+
id,
|
|
855
|
+
name,
|
|
856
|
+
rawArguments,
|
|
857
|
+
validation: "invalid_json",
|
|
858
|
+
validationMessage: `Invalid input for tool ${name}: JSON parsing failed: Text: ` +
|
|
859
|
+
`${rawArguments}.\nError message: ${errorMessage(error)}`,
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
let argumentsValue;
|
|
863
|
+
try {
|
|
864
|
+
argumentsValue = this.normalizeArguments(name, parsedArguments);
|
|
865
|
+
}
|
|
866
|
+
catch (error) {
|
|
867
|
+
return {
|
|
868
|
+
id,
|
|
869
|
+
name,
|
|
870
|
+
rawArguments,
|
|
871
|
+
arguments: parsedArguments,
|
|
872
|
+
validation: "invalid_arguments",
|
|
873
|
+
validationMessage: errorMessage(error),
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
const validationMessage = this.validationMessage(name, argumentsValue, validate);
|
|
877
|
+
if (validationMessage !== undefined) {
|
|
878
|
+
return {
|
|
879
|
+
id,
|
|
880
|
+
name,
|
|
881
|
+
rawArguments,
|
|
882
|
+
arguments: parsedArguments,
|
|
883
|
+
validation: "invalid_arguments",
|
|
884
|
+
validationMessage,
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
return {
|
|
888
|
+
id,
|
|
889
|
+
name,
|
|
890
|
+
rawArguments,
|
|
891
|
+
arguments: parsedArguments,
|
|
892
|
+
validation: "valid",
|
|
893
|
+
};
|
|
894
|
+
}
|
|
760
895
|
async execute(call, _context) {
|
|
761
896
|
const callId = String(call.id || "");
|
|
762
897
|
const name = String(call.function?.name || "");
|
|
@@ -765,30 +900,8 @@ export class CodeToolExecutor {
|
|
|
765
900
|
// 监听器不会补发已发生的 abort:已取消的 signal 必须在产生任何副作用
|
|
766
901
|
// 之前短路,否则命令会照常 spawn 直到自然结束。
|
|
767
902
|
this.signal?.throwIfAborted();
|
|
768
|
-
const
|
|
769
|
-
|
|
770
|
-
: "{}");
|
|
771
|
-
// The captured provider advertises these fields as required, but its
|
|
772
|
-
// runtime still applies the documented defaults when a model omits
|
|
773
|
-
// them. Preserve the byte-identical outgoing schema while matching the
|
|
774
|
-
// observed execution behavior.
|
|
775
|
-
if (name === "read_file") {
|
|
776
|
-
argumentsValue.offset ??= 1;
|
|
777
|
-
argumentsValue.limit ??= 2000;
|
|
778
|
-
}
|
|
779
|
-
else if (name === "list_files" || name === "glob_files") {
|
|
780
|
-
argumentsValue.path ??= "";
|
|
781
|
-
}
|
|
782
|
-
else if (name === "grep_files") {
|
|
783
|
-
argumentsValue.path ??= "";
|
|
784
|
-
argumentsValue.glob ??= "";
|
|
785
|
-
argumentsValue.output_mode ??= "content";
|
|
786
|
-
argumentsValue["-i"] ??= false;
|
|
787
|
-
argumentsValue["-B"] ??= 0;
|
|
788
|
-
argumentsValue["-A"] ??= 0;
|
|
789
|
-
argumentsValue["-C"] ??= 0;
|
|
790
|
-
argumentsValue.context ??= 0;
|
|
791
|
-
}
|
|
903
|
+
const parsedArguments = JSON.parse(this.rawArguments(call));
|
|
904
|
+
const argumentsValue = this.normalizeArguments(name, parsedArguments);
|
|
792
905
|
const validate = this.validators.get(name);
|
|
793
906
|
if (!validate) {
|
|
794
907
|
return {
|
|
@@ -797,14 +910,12 @@ export class CodeToolExecutor {
|
|
|
797
910
|
content: this.unavailableToolMessage(name),
|
|
798
911
|
};
|
|
799
912
|
}
|
|
800
|
-
|
|
801
|
-
|
|
913
|
+
const validationMessage = this.validationMessage(name, argumentsValue, validate);
|
|
914
|
+
if (validationMessage !== undefined) {
|
|
802
915
|
return {
|
|
803
916
|
role: "tool",
|
|
804
917
|
tool_call_id: callId,
|
|
805
|
-
content:
|
|
806
|
-
`Value: ${JSON.stringify(argumentsValue)}.\n` +
|
|
807
|
-
`Error message: ${JSON.stringify(issues, null, 2)}`,
|
|
918
|
+
content: validationMessage,
|
|
808
919
|
};
|
|
809
920
|
}
|
|
810
921
|
const result = await this.dispatch(name, argumentsValue, callId);
|
|
@@ -843,6 +954,13 @@ export class CodeToolExecutor {
|
|
|
843
954
|
};
|
|
844
955
|
}
|
|
845
956
|
async dispatch(name, values, _toolCallId) {
|
|
957
|
+
// Replay mode (CODE_HARNESS_REPLAY_RESPONSES set): serve recorded outputs
|
|
958
|
+
// verbatim for nondeterministic tools (web_search/web_fetch/fetch_media/…)
|
|
959
|
+
// so replayed sessions stay offline and deterministic. Unlisted calls fall
|
|
960
|
+
// through to real execution.
|
|
961
|
+
const replayed = replayResponseFor(name, values);
|
|
962
|
+
if (replayed)
|
|
963
|
+
return replayed.output;
|
|
846
964
|
switch (name) {
|
|
847
965
|
case "read_file":
|
|
848
966
|
return this.readCodeFile(String(values.path), Number(values.offset), Number(values.limit));
|
|
@@ -1064,7 +1182,9 @@ export class CodeToolExecutor {
|
|
|
1064
1182
|
status: "success",
|
|
1065
1183
|
file: {
|
|
1066
1184
|
path: this.relativePath(target),
|
|
1067
|
-
content:
|
|
1185
|
+
content: this.config.mode === "react"
|
|
1186
|
+
? truncateCodeArenaText(`${numbered}\n\n${footer}`)
|
|
1187
|
+
: `${numbered}\n\n${footer}`,
|
|
1068
1188
|
contentType: contentTypeForFile(target),
|
|
1069
1189
|
},
|
|
1070
1190
|
totalLines: lines.length,
|
|
@@ -1147,15 +1267,24 @@ export class CodeToolExecutor {
|
|
|
1147
1267
|
message: `File "${rawPath}" does not exist`,
|
|
1148
1268
|
};
|
|
1149
1269
|
}
|
|
1150
|
-
|
|
1270
|
+
// React replaces the first exact occurrence, including when an indented
|
|
1271
|
+
// context also occurs on a later, more deeply indented line. Keep the
|
|
1272
|
+
// existing uniqueness check for fuzzy fallback and the other modes.
|
|
1273
|
+
const replace = this.config.mode === "react" && context.length > 0 && source.includes(context)
|
|
1274
|
+
? replaceFirstText : replaceUniqueText;
|
|
1275
|
+
const edited = replace(source, context, replacement, {
|
|
1151
1276
|
whitespaceInsensitiveMultiline: true,
|
|
1277
|
+
whitespaceInsensitiveSingleLine: this.config.mode === "react",
|
|
1278
|
+
preserveNewlines: this.config.mode === "react",
|
|
1152
1279
|
});
|
|
1153
1280
|
if (edited === undefined) {
|
|
1154
1281
|
// Captured sessions re-issue an edit after compaction or a retry. The
|
|
1155
1282
|
// provider reports success without touching the file when the context
|
|
1156
1283
|
// is already gone and the replacement is in place (f39 re-issues four
|
|
1157
1284
|
// dashboard edits this way).
|
|
1158
|
-
|
|
1285
|
+
// That retry behavior comes from Full Stack captures. React still
|
|
1286
|
+
// reports Context not found even when the replacement is present.
|
|
1287
|
+
if (this.config.mode !== "react" && replacement && source.includes(replacement)) {
|
|
1159
1288
|
return this.mutationResult("Edited", rawPath);
|
|
1160
1289
|
}
|
|
1161
1290
|
return {
|
|
@@ -1198,6 +1327,19 @@ export class CodeToolExecutor {
|
|
|
1198
1327
|
}
|
|
1199
1328
|
current?.lines.push(line);
|
|
1200
1329
|
}
|
|
1330
|
+
if (this.config.mode === "react") {
|
|
1331
|
+
// React parses duplicate operation paths before applying any writes.
|
|
1332
|
+
// In particular, Delete + Add of the same file is an Add duplicate.
|
|
1333
|
+
const seen = new Set();
|
|
1334
|
+
for (const operation of operations) {
|
|
1335
|
+
const target = await this.confinedPath(operation.path);
|
|
1336
|
+
if (seen.has(target)) {
|
|
1337
|
+
const label = operation.kind[0].toUpperCase() + operation.kind.slice(1);
|
|
1338
|
+
return { status: "error", message: `${label} File Error: Duplicate Path: ${operation.path}` };
|
|
1339
|
+
}
|
|
1340
|
+
seen.add(target);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1201
1343
|
for (const operation of operations) {
|
|
1202
1344
|
const target = await this.confinedPath(operation.path);
|
|
1203
1345
|
if (operation.kind === "add") {
|
|
@@ -1313,6 +1455,11 @@ export class CodeToolExecutor {
|
|
|
1313
1455
|
return { status: "success" };
|
|
1314
1456
|
}
|
|
1315
1457
|
async listCodeFiles(rawPath) {
|
|
1458
|
+
if (this.config.mode === "react" && rawPath === "/")
|
|
1459
|
+
rawPath = ".";
|
|
1460
|
+
if (this.config.mode === "react" && (path.isAbsolute(rawPath) || rawPath.includes(".."))) {
|
|
1461
|
+
return { status: "error", message: "Path must be relative to project root (no absolute paths or '..' traversal)" };
|
|
1462
|
+
}
|
|
1316
1463
|
const target = await this.confinedPath(rawPath || ".");
|
|
1317
1464
|
if (!(await this.existsWithExactCase(rawPath || ".")) ||
|
|
1318
1465
|
!existsSync(target)) {
|
|
@@ -1321,7 +1468,8 @@ export class CodeToolExecutor {
|
|
|
1321
1468
|
const files = this.orderListedFiles(await this.walkFiles(target));
|
|
1322
1469
|
return {
|
|
1323
1470
|
status: "success",
|
|
1324
|
-
files: files.
|
|
1471
|
+
files: (this.config.mode === "react" ? files.slice(0, 500) : files)
|
|
1472
|
+
.map((filePath) => ({ path: filePath })),
|
|
1325
1473
|
};
|
|
1326
1474
|
}
|
|
1327
1475
|
async grepCodeFiles(values) {
|
|
@@ -1408,8 +1556,10 @@ export class CodeToolExecutor {
|
|
|
1408
1556
|
continue;
|
|
1409
1557
|
}
|
|
1410
1558
|
const lines = content.split("\n");
|
|
1559
|
+
const searchLineCount = this.config.mode === "react" && lines.at(-1) === ""
|
|
1560
|
+
? lines.length - 1 : lines.length;
|
|
1411
1561
|
const matchingLines = [];
|
|
1412
|
-
for (let index = 0; index <
|
|
1562
|
+
for (let index = 0; index < searchLineCount; index += 1) {
|
|
1413
1563
|
if (expression.test(lines[index]))
|
|
1414
1564
|
matchingLines.push(index);
|
|
1415
1565
|
}
|
|
@@ -1422,14 +1572,15 @@ export class CodeToolExecutor {
|
|
|
1422
1572
|
const match = {
|
|
1423
1573
|
path: relative,
|
|
1424
1574
|
lineNumber: index + 1,
|
|
1425
|
-
lineContent: lines[index],
|
|
1575
|
+
lineContent: capGrepLine(lines[index]),
|
|
1426
1576
|
};
|
|
1427
1577
|
if (groupedContext) {
|
|
1428
1578
|
const contextBefore = position === 0
|
|
1429
1579
|
? lines.slice(Math.max(0, index - before), index)
|
|
1430
1580
|
: previousAfter;
|
|
1431
|
-
if (contextBefore.length > 0)
|
|
1432
|
-
match.contextBefore = contextBefore;
|
|
1581
|
+
if (contextBefore.length > 0) {
|
|
1582
|
+
match.contextBefore = capGrepLines(contextBefore);
|
|
1583
|
+
}
|
|
1433
1584
|
const nextMatch = matchingLines[position + 1] ?? lines.length;
|
|
1434
1585
|
previousAfter = lines.slice(index + 1, Math.min(index + 1 + contextSpan, nextMatch));
|
|
1435
1586
|
if (nextMatch === lines.length &&
|
|
@@ -1437,15 +1588,36 @@ export class CodeToolExecutor {
|
|
|
1437
1588
|
index + 1 + contextSpan >= lines.length) {
|
|
1438
1589
|
previousAfter = previousAfter.slice(0, -1);
|
|
1439
1590
|
}
|
|
1440
|
-
if (previousAfter.length > 0)
|
|
1441
|
-
match.contextAfter = previousAfter;
|
|
1591
|
+
if (previousAfter.length > 0) {
|
|
1592
|
+
match.contextAfter = capGrepLines(previousAfter);
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
else if (this.config.mode === "react") {
|
|
1596
|
+
// The captured adapter attaches context lines to the preceding
|
|
1597
|
+
// match, even across ripgrep group separators. Only a file's first
|
|
1598
|
+
// match receives contextBefore. Do not duplicate overlapping groups.
|
|
1599
|
+
if (position === 0 && before > 0) {
|
|
1600
|
+
const contextBefore = lines.slice(Math.max(0, index - before), index);
|
|
1601
|
+
if (contextBefore.length) {
|
|
1602
|
+
match.contextBefore = capGrepLines(contextBefore);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
const nextMatch = matchingLines[position + 1];
|
|
1606
|
+
const end = Math.min(index + 1 + after, nextMatch ?? searchLineCount);
|
|
1607
|
+
const contextAfter = lines.slice(index + 1, end);
|
|
1608
|
+
if (nextMatch !== undefined && before > 0) {
|
|
1609
|
+
contextAfter.push(...lines.slice(Math.max(end, index + 1, nextMatch - before), nextMatch));
|
|
1610
|
+
}
|
|
1611
|
+
if (contextAfter.length) {
|
|
1612
|
+
match.contextAfter = capGrepLines(contextAfter);
|
|
1613
|
+
}
|
|
1442
1614
|
}
|
|
1443
1615
|
else {
|
|
1444
1616
|
if (before > 0) {
|
|
1445
|
-
match.contextBefore = lines.slice(Math.max(0, index - before), index);
|
|
1617
|
+
match.contextBefore = capGrepLines(lines.slice(Math.max(0, index - before), index));
|
|
1446
1618
|
}
|
|
1447
1619
|
if (after > 0) {
|
|
1448
|
-
match.contextAfter = lines.slice(index + 1, index + 1 + after);
|
|
1620
|
+
match.contextAfter = capGrepLines(lines.slice(index + 1, index + 1 + after));
|
|
1449
1621
|
}
|
|
1450
1622
|
}
|
|
1451
1623
|
matches.push(match);
|
|
@@ -1495,7 +1667,7 @@ export class CodeToolExecutor {
|
|
|
1495
1667
|
return { status: "success", paths: [], truncated: false };
|
|
1496
1668
|
}
|
|
1497
1669
|
const allPaths = files
|
|
1498
|
-
.filter((filePath) => matchesGlob(path.relative(target, filePath).split(path.sep).join("/"), pattern))
|
|
1670
|
+
.filter((filePath) => matchesGlob(path.relative(target, filePath).split(path.sep).join("/"), pattern) || (this.config.mode === "react" && matchesGlob(this.relativePath(filePath), pattern)))
|
|
1499
1671
|
.map((filePath) => this.relativePath(filePath));
|
|
1500
1672
|
return {
|
|
1501
1673
|
status: "success",
|
|
@@ -1568,11 +1740,11 @@ export class CodeToolExecutor {
|
|
|
1568
1740
|
});
|
|
1569
1741
|
}
|
|
1570
1742
|
async installNpmPackages(packages) {
|
|
1571
|
-
const invalid = packages.find((packageName) => !packageNameIsSafe(packageName));
|
|
1743
|
+
const invalid = packages.find((packageName) => !packageNameIsSafe(packageName, this.config.mode));
|
|
1572
1744
|
if (invalid) {
|
|
1573
1745
|
return {
|
|
1574
1746
|
status: "error",
|
|
1575
|
-
message: `Invalid package name: ${invalid}`,
|
|
1747
|
+
message: `Invalid package name: ${this.config.mode === "react" ? JSON.stringify(invalid) : invalid}`,
|
|
1576
1748
|
};
|
|
1577
1749
|
}
|
|
1578
1750
|
const args = ["install", "-S"];
|
|
@@ -1611,11 +1783,15 @@ export class CodeToolExecutor {
|
|
|
1611
1783
|
};
|
|
1612
1784
|
}
|
|
1613
1785
|
async fullStackBash(command, workdir) {
|
|
1786
|
+
const cwd = workdir.trim() || ".";
|
|
1787
|
+
if (path.isAbsolute(cwd) || cwd.includes("..")) {
|
|
1788
|
+
throw new Error(`Invalid workdir: ${cwd}. Commands run from the project root by default. Only use workdir for relative subdirectories, never absolute paths.`);
|
|
1789
|
+
}
|
|
1614
1790
|
const configuredTimeout = Number(process.env.CODE_HARNESS_BASH_TIMEOUT_SECONDS || 300);
|
|
1615
1791
|
const timeoutSeconds = Number.isFinite(configuredTimeout) && configuredTimeout > 0
|
|
1616
1792
|
? configuredTimeout
|
|
1617
1793
|
: 300;
|
|
1618
|
-
const result = await this.bash(command,
|
|
1794
|
+
const result = await this.bash(command, cwd, timeoutSeconds);
|
|
1619
1795
|
const exitCode = result.exit_code;
|
|
1620
1796
|
const stdout = String(result.stdout || "");
|
|
1621
1797
|
const stderr = String(result.stderr || "");
|
|
@@ -1886,11 +2062,7 @@ export class CodeToolExecutor {
|
|
|
1886
2062
|
const result = routifyWebFetchEnabled()
|
|
1887
2063
|
? await executeRoutifyWebFetch(url, format, { signal: this.signal })
|
|
1888
2064
|
: await this.pageReader.readFormatted(url, format);
|
|
1889
|
-
return
|
|
1890
|
-
status: "success",
|
|
1891
|
-
title: String(result.title || "No Title Found"),
|
|
1892
|
-
content: String(result.content || ""),
|
|
1893
|
-
};
|
|
2065
|
+
return boundedWebFetchResult(result);
|
|
1894
2066
|
}
|
|
1895
2067
|
async pexelsRequest(pathname, parameters) {
|
|
1896
2068
|
const apiKey = process.env.PEXELS_API_KEY;
|
|
@@ -1929,7 +2101,7 @@ export class CodeToolExecutor {
|
|
|
1929
2101
|
},
|
|
1930
2102
|
});
|
|
1931
2103
|
}
|
|
1932
|
-
mapPexelsPhotos(payload) {
|
|
2104
|
+
mapPexelsPhotos(payload, orientation) {
|
|
1933
2105
|
const photos = Array.isArray(payload.photos) ? payload.photos : [];
|
|
1934
2106
|
return photos.map((value) => {
|
|
1935
2107
|
const photo = record(value);
|
|
@@ -1938,12 +2110,14 @@ export class CodeToolExecutor {
|
|
|
1938
2110
|
type: "image",
|
|
1939
2111
|
id: Number(photo.id),
|
|
1940
2112
|
pexelsUrl: stringValue(photo.url),
|
|
1941
|
-
recommendedUrl:
|
|
2113
|
+
recommendedUrl: (orientation === "landscape" || orientation === "portrait"
|
|
2114
|
+
? stringValue(source[orientation]) : "") ||
|
|
2115
|
+
stringValue(source.large2x) ||
|
|
1942
2116
|
stringValue(source.large) ||
|
|
1943
2117
|
stringValue(source.original),
|
|
1944
|
-
thumbnailUrl: stringValue(source.
|
|
1945
|
-
stringValue(source.
|
|
1946
|
-
stringValue(source.
|
|
2118
|
+
thumbnailUrl: stringValue(source.tiny) ||
|
|
2119
|
+
stringValue(source.medium) ||
|
|
2120
|
+
stringValue(source.small),
|
|
1947
2121
|
width: Number(photo.width),
|
|
1948
2122
|
height: Number(photo.height),
|
|
1949
2123
|
creatorName: stringValue(photo.photographer),
|
|
@@ -1963,20 +2137,29 @@ export class CodeToolExecutor {
|
|
|
1963
2137
|
const files = Array.isArray(video.video_files)
|
|
1964
2138
|
? video.video_files.map(record)
|
|
1965
2139
|
: [];
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
2140
|
+
// Captures contain full-resolution HD/UHD files. A quality label alone
|
|
2141
|
+
// does not order renditions; compare their actual pixel dimensions.
|
|
2142
|
+
const playable = files.filter((item) => stringValue(item.file_type) === "video/mp4" && stringValue(item.link));
|
|
2143
|
+
const preferred = playable.sort((left, right) => (Number(right.width) || 0) * (Number(right.height) || 0) -
|
|
2144
|
+
(Number(left.width) || 0) * (Number(left.height) || 0))[0] || files[0] || {};
|
|
1969
2145
|
return {
|
|
1970
2146
|
type: "video",
|
|
1971
2147
|
id: Number(video.id),
|
|
1972
2148
|
pexelsUrl: stringValue(video.url),
|
|
1973
2149
|
recommendedUrl: stringValue(preferred.link),
|
|
1974
|
-
thumbnailUrl: stringValue(pictures[0]?.picture),
|
|
2150
|
+
thumbnailUrl: stringValue(video.image) || stringValue(pictures[0]?.picture),
|
|
1975
2151
|
width: Number(video.width),
|
|
1976
2152
|
height: Number(video.height),
|
|
1977
2153
|
duration: Number(video.duration),
|
|
1978
2154
|
creatorName: stringValue(user.name),
|
|
1979
2155
|
creatorUrl: stringValue(user.url),
|
|
2156
|
+
videoFile: {
|
|
2157
|
+
url: stringValue(preferred.link),
|
|
2158
|
+
width: Number(preferred.width),
|
|
2159
|
+
height: Number(preferred.height),
|
|
2160
|
+
...(preferred.quality !== undefined ? { quality: preferred.quality } : {}),
|
|
2161
|
+
fileType: stringValue(preferred.file_type),
|
|
2162
|
+
},
|
|
1980
2163
|
};
|
|
1981
2164
|
});
|
|
1982
2165
|
}
|
|
@@ -1994,7 +2177,7 @@ export class CodeToolExecutor {
|
|
|
1994
2177
|
if (mediaType === "image" || mediaType === "both") {
|
|
1995
2178
|
const photos = await this.pexelsRequest("/v1/search", common);
|
|
1996
2179
|
totalResults += Number(photos.total_results || 0);
|
|
1997
|
-
photoResults = this.mapPexelsPhotos(photos);
|
|
2180
|
+
photoResults = this.mapPexelsPhotos(photos, orientation);
|
|
1998
2181
|
}
|
|
1999
2182
|
if (mediaType === "video" || mediaType === "both") {
|
|
2000
2183
|
const videos = await this.pexelsRequest("/v1/videos/search", common);
|
|
@@ -2016,13 +2199,20 @@ export class CodeToolExecutor {
|
|
|
2016
2199
|
}
|
|
2017
2200
|
async fetchMedia(query, mediaType, count, orientation, size, locale) {
|
|
2018
2201
|
const requestedCount = Math.min(10, Math.max(1, Math.trunc(count)));
|
|
2019
|
-
|
|
2202
|
+
try {
|
|
2203
|
+
return await this.fetchMediaFromPexels(query, mediaType, requestedCount, orientation, size, locale);
|
|
2204
|
+
}
|
|
2205
|
+
catch (error) {
|
|
2206
|
+
throw new Error(`Media fetch failed: ${errorMessage(error)}`);
|
|
2207
|
+
}
|
|
2020
2208
|
}
|
|
2021
2209
|
async generateImage(rawPath, prompt, images) {
|
|
2022
2210
|
if (this.generatedImageCount >= 10) {
|
|
2023
2211
|
return {
|
|
2024
2212
|
status: "error",
|
|
2025
|
-
message:
|
|
2213
|
+
message: this.config.mode === "react"
|
|
2214
|
+
? "Failed to generate image: Image generation limit of 10 reached for this session. Use CSS gradients, SVG, or placeholder images for remaining visuals."
|
|
2215
|
+
: "generate_image can be used at most 10 times per session",
|
|
2026
2216
|
};
|
|
2027
2217
|
}
|
|
2028
2218
|
this.generatedImageCount += 1;
|