@cydm/pie 1.0.23 → 1.0.25

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 (28) hide show
  1. package/README.md +51 -5
  2. package/dist/builtin/extensions/ask-user/index.js +3 -3
  3. package/dist/builtin/extensions/ask-user/package.json +16 -0
  4. package/dist/builtin/extensions/changelog/package.json +11 -0
  5. package/dist/builtin/extensions/deploy/package.json +11 -0
  6. package/dist/builtin/extensions/document-attachments/package.json +11 -0
  7. package/dist/builtin/extensions/files/package.json +14 -0
  8. package/dist/builtin/extensions/init/package.json +11 -0
  9. package/dist/builtin/extensions/kimi-attachments/package.json +11 -0
  10. package/dist/builtin/extensions/plan-mode/index.js +3 -3
  11. package/dist/builtin/extensions/plan-mode/package.json +11 -0
  12. package/dist/builtin/extensions/subagent/index.js +4 -4
  13. package/dist/builtin/extensions/subagent/package.json +14 -0
  14. package/dist/builtin/extensions/todo/index.js +3 -3
  15. package/dist/builtin/extensions/todo/package.json +11 -0
  16. package/dist/builtin/skills/browser-tools/README.md +1 -1
  17. package/dist/builtin/skills/browser-tools/SKILL.md +3 -3
  18. package/dist/builtin/skills/browser-tools/playwright-cli.js +22 -1
  19. package/dist/chunks/{chunk-PUZP2Q3F.js → chunk-4B42ZU4B.js} +1 -1
  20. package/dist/chunks/{chunk-7ABSJC2D.js → chunk-64XJIKVK.js} +371 -120
  21. package/dist/chunks/{chunk-4WU5VAX4.js → chunk-BEWVRLCP.js} +387 -230
  22. package/dist/chunks/{chunk-AFTBFF6J.js → chunk-O4DPFYYP.js} +3425 -416
  23. package/dist/chunks/{src-UCXJ4C6P.js → src-27UD2ZBZ.js} +2 -2
  24. package/dist/chunks/{test-stream-UKULOPZB.js → test-stream-ZNGAUUVH.js} +6 -2
  25. package/dist/cli.js +3574 -2050
  26. package/dist/{chunks/typescript-GSKWJIO4.js → code-intel-worker.cjs} +2507 -2031
  27. package/models.schema.json +38 -13
  28. package/package.json +11 -7
@@ -2,16 +2,15 @@ import { createRequire as __createRequire } from "node:module"; const require =
2
2
  import {
3
3
  classifyShellCommand,
4
4
  createCodeIntelTool,
5
- createLightweightCodeIntelProvider,
6
5
  createSharedWebFetchTool,
7
6
  createSharedWebResearchTool,
8
7
  createSharedWebSearchTool,
9
8
  interpretShellExit,
10
9
  requestInteraction
11
- } from "./chunk-AFTBFF6J.js";
10
+ } from "./chunk-O4DPFYYP.js";
12
11
  import {
13
12
  Type
14
- } from "./chunk-7ABSJC2D.js";
13
+ } from "./chunk-64XJIKVK.js";
15
14
 
16
15
  // src/config.ts
17
16
  import { existsSync, mkdirSync, readFileSync, renameSync } from "fs";
@@ -75,6 +74,9 @@ function getCustomThemesDir() {
75
74
  function getSettingsPath() {
76
75
  return join(getConfigDir(), "settings.json");
77
76
  }
77
+ function getProjectSettingsPath(cwd) {
78
+ return join(cwd, CONFIG_DIR_NAME, "settings.json");
79
+ }
78
80
  function getSessionsDir() {
79
81
  return join(getConfigDir(), "sessions");
80
82
  }
@@ -125,14 +127,54 @@ function migrateConfigFromAgentDir() {
125
127
 
126
128
  // src/capabilities/bash/index.ts
127
129
  import { spawn } from "child_process";
128
- import { createWriteStream, existsSync as existsSync2, statSync, writeFileSync } from "fs";
130
+ import { closeSync, createWriteStream, existsSync as existsSync2, openSync, statSync, unlinkSync, writeFileSync } from "fs";
129
131
  import { tmpdir } from "os";
130
- import { join as join2 } from "path";
131
- import { randomBytes } from "crypto";
132
+ import { join as join2, resolve as resolve2 } from "path";
133
+ import { createHash, randomBytes } from "crypto";
132
134
 
133
135
  // src/capabilities/bash/truncate.ts
134
- var DEFAULT_MAX_LINES = 2e3;
135
- var DEFAULT_MAX_BYTES = 50 * 1024;
136
+ var DEFAULT_MAX_LINES = 400;
137
+ var DEFAULT_MAX_BYTES = 16 * 1024;
138
+ function truncateHeadTail(content, options = {}) {
139
+ const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
140
+ const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
141
+ const totalBytes = Buffer.byteLength(content, "utf-8");
142
+ const lines = content.split("\n");
143
+ if (lines.length <= maxLines && totalBytes <= maxBytes) return truncateTail(content, options);
144
+ const headCount = Math.max(1, Math.floor(maxLines / 2));
145
+ const tailCount = Math.max(1, maxLines - headCount);
146
+ let head = lines.slice(0, headCount);
147
+ let tail = lines.slice(-tailCount);
148
+ const omittedNotice = () => `... [truncated: ${Math.max(0, lines.length - head.length - tail.length)} middle lines omitted] ...`;
149
+ let combined = () => [...head, omittedNotice(), ...tail].join("\n");
150
+ while (Buffer.byteLength(combined(), "utf-8") > maxBytes && (head.length > 1 || tail.length > 1)) {
151
+ if (head.length >= tail.length && head.length > 1) head.pop();
152
+ else if (tail.length > 1) tail.shift();
153
+ }
154
+ let output = combined();
155
+ let lastLinePartial = false;
156
+ if (Buffer.byteLength(output, "utf-8") > maxBytes) {
157
+ const marker = "\n... [truncated middle] ...\n";
158
+ const available = Math.max(2, maxBytes - Buffer.byteLength(marker, "utf-8"));
159
+ const bytes = Buffer.from(content, "utf-8");
160
+ const prefix = bytes.subarray(0, Math.floor(available / 2)).toString("utf-8").replace(/\uFFFD+$/g, "");
161
+ const suffix = bytes.subarray(Math.max(0, bytes.length - Math.ceil(available / 2))).toString("utf-8").replace(/^\uFFFD+/g, "");
162
+ output = `${prefix}${marker}${suffix}`;
163
+ lastLinePartial = true;
164
+ }
165
+ return {
166
+ content: output,
167
+ truncated: true,
168
+ truncatedBy: lines.length > maxLines ? "lines" : "bytes",
169
+ totalLines: lines.length,
170
+ totalBytes,
171
+ outputLines: head.length + tail.length,
172
+ outputBytes: Buffer.byteLength(output, "utf-8"),
173
+ lastLinePartial,
174
+ maxLines,
175
+ maxBytes
176
+ };
177
+ }
136
178
  function formatSize(bytes) {
137
179
  if (bytes < 1024) {
138
180
  return `${bytes}B`;
@@ -217,7 +259,7 @@ var bashSchema = Type.Object({
217
259
  command: Type.String({ description: "The bash command to execute" }),
218
260
  timeout: Type.Optional(
219
261
  Type.Number({
220
- description: "Timeout in seconds. Default: 120s (2 minutes). For longer tasks, explicitly set a higher value: { timeout: 600 } for npm install, { timeout: 600 } for cargo build, etc."
262
+ description: "Timeout in seconds. Default: 120s (2 minutes). Longer timeouts are honored for installs, builds, and tests; generic custom scripts and polling commands are capped to protect the turn budget."
221
263
  })
222
264
  ),
223
265
  cwd: Type.Optional(Type.String({ description: "Working directory for the command" })),
@@ -229,6 +271,23 @@ var bashSchema = Type.Object({
229
271
  ], { description: "Shell to use. Defaults to bash on macOS/Linux and PowerShell on Windows. Use this only for platform-specific commands." }))
230
272
  });
231
273
  var DEFAULT_TIMEOUT_SECONDS = 120;
274
+ var MAX_GENERIC_TIMEOUT_SECONDS = 120;
275
+ function resolveTimeoutPolicy(requestedTimeout, commandClass) {
276
+ const requestedSeconds = requestedTimeout ?? DEFAULT_TIMEOUT_SECONDS;
277
+ if (requestedSeconds > MAX_GENERIC_TIMEOUT_SECONDS && commandClass !== "package_manager" && commandClass !== "test_or_build") {
278
+ return {
279
+ requestedSeconds,
280
+ effectiveSeconds: MAX_GENERIC_TIMEOUT_SECONDS,
281
+ capped: true,
282
+ reason: `long timeouts are capped at ${MAX_GENERIC_TIMEOUT_SECONDS}s for non-build commands`
283
+ };
284
+ }
285
+ return {
286
+ requestedSeconds,
287
+ effectiveSeconds: requestedSeconds,
288
+ capped: false
289
+ };
290
+ }
232
291
  function validateBashCommand(command) {
233
292
  const value = typeof command === "string" ? command : "";
234
293
  const trimmed = value.trim();
@@ -263,7 +322,7 @@ function buildSpawnCommand(shell, command) {
263
322
  if (shell === "cmd") {
264
323
  return { file: "cmd.exe", args: ["/d", "/s", "/c", command], shell: false };
265
324
  }
266
- return { file: command, args: [], shell: true };
325
+ return { file: process.platform === "win32" ? "bash.exe" : "bash", args: ["-c", command], shell: false };
267
326
  }
268
327
  async function requestHighRiskConfirmation(request) {
269
328
  const response = await requestInteraction({
@@ -296,24 +355,101 @@ function validateWorkingDirectory(cwd) {
296
355
  }
297
356
  function normalizeWorkingDirectory(cwd, defaultCwd) {
298
357
  const requestedCwd = typeof cwd === "string" && cwd.trim().length > 0 ? cwd.trim() : void 0;
299
- return requestedCwd ?? defaultCwd ?? process.cwd();
358
+ return resolve2(requestedCwd ?? defaultCwd ?? process.cwd());
300
359
  }
301
360
  function getTempFilePath() {
302
361
  const id = randomBytes(8).toString("hex");
303
362
  return join2(tmpdir(), `pie-bash-${id}.log`);
304
363
  }
364
+ var retainedOutputArtifacts = /* @__PURE__ */ new Set();
365
+ function getErrorCode(error) {
366
+ if (!error || typeof error !== "object" || !("code" in error)) return void 0;
367
+ const code = error.code;
368
+ return typeof code === "string" ? code : void 0;
369
+ }
370
+ function createPrivateOutputArtifact() {
371
+ for (let attempt = 0; attempt < 10; attempt++) {
372
+ const path3 = getTempFilePath();
373
+ try {
374
+ const fd = openSync(path3, "wx", 384);
375
+ retainedOutputArtifacts.add(path3);
376
+ try {
377
+ return { path: path3, stream: createWriteStream(path3, { fd, autoClose: true }) };
378
+ } catch (error) {
379
+ closeSync(fd);
380
+ removeOutputArtifact(path3);
381
+ throw error;
382
+ }
383
+ } catch (error) {
384
+ if (getErrorCode(error) !== "EEXIST") throw error;
385
+ }
386
+ }
387
+ throw new Error("Unable to allocate a private bash output artifact.");
388
+ }
389
+ function removeOutputArtifact(path3) {
390
+ if (!path3) return;
391
+ try {
392
+ unlinkSync(path3);
393
+ } catch (error) {
394
+ if (getErrorCode(error) !== "ENOENT") throw error;
395
+ }
396
+ retainedOutputArtifacts.delete(path3);
397
+ }
398
+ function discardOutputArtifact(path3, stream) {
399
+ if (stream && !stream.closed) {
400
+ stream.once("close", () => {
401
+ try {
402
+ removeOutputArtifact(path3);
403
+ } catch {
404
+ }
405
+ });
406
+ stream.destroy();
407
+ }
408
+ try {
409
+ removeOutputArtifact(path3);
410
+ } catch {
411
+ }
412
+ }
413
+ function cleanupBashOutputArtifacts() {
414
+ for (const path3 of [...retainedOutputArtifacts]) {
415
+ try {
416
+ removeOutputArtifact(path3);
417
+ } catch {
418
+ }
419
+ }
420
+ }
421
+ process.once("exit", cleanupBashOutputArtifacts);
305
422
  function finishTempFileStream(stream) {
306
- if (!stream) return Promise.resolve();
423
+ if (!stream || stream.closed) return Promise.resolve();
307
424
  return new Promise((resolve3, reject) => {
308
- const onError = (error) => {
425
+ let done = false;
426
+ const cleanup = () => {
427
+ stream.off("close", onClose);
309
428
  stream.off("error", onError);
310
- reject(error);
311
429
  };
312
- stream.once("error", onError);
313
- stream.end(() => {
314
- stream.off("error", onError);
430
+ const onClose = () => {
431
+ if (done) return;
432
+ done = true;
433
+ cleanup();
315
434
  resolve3();
316
- });
435
+ };
436
+ const onError = (error) => {
437
+ if (done) return;
438
+ done = true;
439
+ cleanup();
440
+ reject(error instanceof Error ? error : new Error(String(error)));
441
+ };
442
+ stream.once("close", onClose);
443
+ stream.once("error", onError);
444
+ try {
445
+ if (!stream.writableEnded) {
446
+ stream.end();
447
+ } else if (stream.closed) {
448
+ onClose();
449
+ }
450
+ } catch (error) {
451
+ onError(error);
452
+ }
317
453
  });
318
454
  }
319
455
  function buildBashRecoveryHint(command, output) {
@@ -346,10 +482,22 @@ function killProcessTree(pid) {
346
482
  }
347
483
  }
348
484
  function createBashTool(defaultCwd, options) {
485
+ const recentCommands = /* @__PURE__ */ new Map();
486
+ const rememberCommandResult = (key, fingerprint) => {
487
+ const previous = recentCommands.get(key);
488
+ const count = (previous?.count ?? 0) + 1;
489
+ const sameResult = previous?.fingerprint === fingerprint;
490
+ recentCommands.set(key, { count, fingerprint });
491
+ if (recentCommands.size > 100) {
492
+ const oldest = recentCommands.keys().next().value;
493
+ if (oldest) recentCommands.delete(oldest);
494
+ }
495
+ return { count, sameResult };
496
+ };
349
497
  return {
350
498
  name: "bash",
351
499
  label: "bash",
352
- description: `Execute a shell command in the current working directory. The public tool name remains bash for compatibility; on Windows shell:auto uses PowerShell, otherwise bash. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Default timeout: 120s. Use timeout parameter for longer tasks (e.g., npm install -> 600).`,
500
+ description: `Execute a shell command in the current working directory. The public tool name remains bash for compatibility; on Windows shell:auto uses PowerShell, otherwise bash. Returns stdout and stderr as a bounded head/tail preview of at most ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB. If truncated, the result includes a private full-output artifact path. Default timeout: 120s. Long timeouts are reserved for installs, builds, and tests; custom scripts, searches, and polling commands are capped at ${MAX_GENERIC_TIMEOUT_SECONDS}s and should be split into bounded steps with progress output.`,
353
501
  parameters: bashSchema,
354
502
  execute: async (_toolCallId, { command, timeout, cwd, shell }, signal, onUpdate) => {
355
503
  validateBashCommand(command);
@@ -357,7 +505,10 @@ function createBashTool(defaultCwd, options) {
357
505
  const effectiveCommand = buildEffectiveCommand(command, options);
358
506
  const workingDir = normalizeWorkingDirectory(cwd, defaultCwd);
359
507
  validateWorkingDirectory(workingDir);
360
- const risk = classifyShellCommand(effectiveCommand, resolvedShell);
508
+ const risk = classifyShellCommand(effectiveCommand, resolvedShell, {
509
+ cwd: workingDir,
510
+ sandboxRoot: defaultCwd ?? workingDir
511
+ });
361
512
  if (risk.destructive && !options?.isYoloMode?.()) {
362
513
  const approved = await (options?.confirm ?? requestHighRiskConfirmation)({
363
514
  command: effectiveCommand,
@@ -388,7 +539,8 @@ function createBashTool(defaultCwd, options) {
388
539
  };
389
540
  }
390
541
  }
391
- const timeoutMs = (timeout ?? DEFAULT_TIMEOUT_SECONDS) * 1e3;
542
+ const timeoutPolicy = resolveTimeoutPolicy(timeout, risk.commandClass);
543
+ const timeoutMs = timeoutPolicy.effectiveSeconds * 1e3;
392
544
  return new Promise((resolve3, reject) => {
393
545
  if (signal?.aborted) {
394
546
  reject(new Error("Operation aborted"));
@@ -396,13 +548,25 @@ function createBashTool(defaultCwd, options) {
396
548
  }
397
549
  let tempFilePath;
398
550
  let tempFileStream;
551
+ let settled = false;
399
552
  let totalBytes = 0;
400
553
  const chunks = [];
401
554
  let chunksBytes = 0;
402
555
  const maxChunksBytes = DEFAULT_MAX_BYTES * 2;
556
+ const headChunks = [];
557
+ let headBytes = 0;
558
+ let totalNewlines = 0;
559
+ const maxHeadBytes = Math.floor(DEFAULT_MAX_BYTES / 2);
560
+ const captureHead = (data) => {
561
+ totalNewlines += data.reduce((count, byte) => count + (byte === 10 ? 1 : 0), 0);
562
+ if (headBytes >= maxHeadBytes) return;
563
+ const prefix = data.subarray(0, maxHeadBytes - headBytes);
564
+ headChunks.push(prefix);
565
+ headBytes += prefix.length;
566
+ };
403
567
  let timedOut = false;
404
568
  const spawnCommand = buildSpawnCommand(resolvedShell, effectiveCommand);
405
- const child = spawn(spawnCommand.file, spawnCommand.args, {
569
+ const child = (options?.spawnProcess ?? spawn)(spawnCommand.file, spawnCommand.args, {
406
570
  shell: spawnCommand.shell,
407
571
  cwd: workingDir,
408
572
  env: process.env,
@@ -414,19 +578,33 @@ function createBashTool(defaultCwd, options) {
414
578
  killProcessTree(child.pid);
415
579
  }
416
580
  }, timeoutMs);
417
- const onAbort = () => {
581
+ const failExecution = (error) => {
582
+ if (settled) return;
583
+ settled = true;
418
584
  clearTimeout(timeoutId);
419
- if (child.pid) {
420
- killProcessTree(child.pid);
421
- }
422
- reject(new Error("Operation aborted"));
585
+ signal?.removeEventListener("abort", onAbort);
586
+ if (child.pid) killProcessTree(child.pid);
587
+ discardOutputArtifact(tempFilePath, tempFileStream);
588
+ reject(error);
589
+ };
590
+ const onAbort = () => {
591
+ failExecution(new Error("Operation aborted"));
423
592
  };
424
593
  signal?.addEventListener("abort", onAbort, { once: true });
425
594
  child.stdout?.on("data", (data) => {
595
+ if (settled) return;
426
596
  totalBytes += data.length;
597
+ captureHead(data);
427
598
  if (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {
428
- tempFilePath = getTempFilePath();
429
- tempFileStream = createWriteStream(tempFilePath);
599
+ try {
600
+ const artifact = createPrivateOutputArtifact();
601
+ tempFilePath = artifact.path;
602
+ tempFileStream = artifact.stream;
603
+ tempFileStream.once("error", failExecution);
604
+ } catch (error) {
605
+ failExecution(error instanceof Error ? error : new Error(String(error)));
606
+ return;
607
+ }
430
608
  for (const chunk of chunks) {
431
609
  tempFileStream.write(chunk);
432
610
  }
@@ -443,7 +621,7 @@ function createBashTool(defaultCwd, options) {
443
621
  if (onUpdate) {
444
622
  const fullBuffer = Buffer.concat(chunks);
445
623
  const fullText = fullBuffer.toString("utf-8");
446
- const truncation = truncateTail(fullText);
624
+ const truncation = truncateHeadTail(fullText);
447
625
  onUpdate({
448
626
  content: [{ type: "text", text: truncation.content || "" }],
449
627
  details: {
@@ -460,6 +638,7 @@ function createBashTool(defaultCwd, options) {
460
638
  shell: resolvedShell,
461
639
  commandClass: risk.commandClass,
462
640
  riskWarnings: risk.riskWarnings,
641
+ timeoutPolicy,
463
642
  permissionDecision: {
464
643
  allowed: true,
465
644
  requiresPermission: risk.requiresPermission
@@ -469,10 +648,19 @@ function createBashTool(defaultCwd, options) {
469
648
  }
470
649
  });
471
650
  child.stderr?.on("data", (data) => {
651
+ if (settled) return;
472
652
  totalBytes += data.length;
653
+ captureHead(data);
473
654
  if (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {
474
- tempFilePath = getTempFilePath();
475
- tempFileStream = createWriteStream(tempFilePath);
655
+ try {
656
+ const artifact = createPrivateOutputArtifact();
657
+ tempFilePath = artifact.path;
658
+ tempFileStream = artifact.stream;
659
+ tempFileStream.once("error", failExecution);
660
+ } catch (error) {
661
+ failExecution(error instanceof Error ? error : new Error(String(error)));
662
+ return;
663
+ }
476
664
  for (const chunk of chunks) {
477
665
  tempFileStream.write(chunk);
478
666
  }
@@ -491,13 +679,32 @@ function createBashTool(defaultCwd, options) {
491
679
  try {
492
680
  clearTimeout(timeoutId);
493
681
  signal?.removeEventListener("abort", onAbort);
682
+ if (settled) return;
494
683
  const fullBuffer = Buffer.concat(chunks);
495
684
  let fullOutput = fullBuffer.toString("utf-8");
496
- const truncation = truncateTail(fullOutput);
685
+ if (totalBytes > fullBuffer.length) {
686
+ fullOutput = `${Buffer.concat(headChunks).toString("utf-8")}
687
+ ... [stream middle omitted] ...
688
+ ${fullOutput}`;
689
+ }
690
+ const truncation = truncateHeadTail(fullOutput);
691
+ if (totalBytes > truncation.totalBytes) {
692
+ truncation.truncated = true;
693
+ truncation.totalBytes = totalBytes;
694
+ truncation.totalLines = totalNewlines + 1;
695
+ truncation.truncatedBy = totalNewlines + 1 > DEFAULT_MAX_LINES ? "lines" : "bytes";
696
+ }
497
697
  let outputText = truncation.content || "(no output)";
498
698
  if (truncation.truncated && !tempFilePath) {
499
- tempFilePath = getTempFilePath();
500
- writeFileSync(tempFilePath, fullOutput, "utf-8");
699
+ const path3 = getTempFilePath();
700
+ const fd = openSync(path3, "wx", 384);
701
+ tempFilePath = path3;
702
+ retainedOutputArtifacts.add(path3);
703
+ try {
704
+ writeFileSync(fd, fullOutput, "utf-8");
705
+ } finally {
706
+ closeSync(fd);
707
+ }
501
708
  }
502
709
  await finishTempFileStream(tempFileStream);
503
710
  const details = {
@@ -507,6 +714,7 @@ function createBashTool(defaultCwd, options) {
507
714
  shell: resolvedShell,
508
715
  commandClass: risk.commandClass,
509
716
  riskWarnings: risk.riskWarnings,
717
+ timeoutPolicy,
510
718
  permissionDecision: {
511
719
  allowed: true,
512
720
  requiresPermission: risk.requiresPermission
@@ -525,29 +733,26 @@ function createBashTool(defaultCwd, options) {
525
733
  outputLines: truncation.outputLines
526
734
  };
527
735
  details.fullOutputPath = tempFilePath;
528
- const startLine = truncation.totalLines - truncation.outputLines + 1;
529
- const endLine = truncation.totalLines;
530
736
  if (truncation.lastLinePartial) {
531
- const lastLineSize = formatSize(
532
- Buffer.byteLength(fullOutput.split("\n").pop() || "", "utf-8")
533
- );
534
737
  outputText += `
535
738
 
536
- [Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${tempFilePath}]`;
537
- } else if (truncation.truncatedBy === "lines") {
538
- outputText += `
539
-
540
- [Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${tempFilePath}]`;
739
+ [Head/tail preview limited to ${formatSize(truncation.outputBytes)}. Full output: ${tempFilePath}]`;
541
740
  } else {
542
741
  outputText += `
543
742
 
544
- [Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${tempFilePath}]`;
743
+ [Showing a bounded head/tail preview of ${truncation.totalLines} lines (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${tempFilePath}]`;
545
744
  }
546
745
  }
547
746
  if (timedOut) {
548
747
  outputText += `
549
748
 
550
- [Command timed out after ${timeout ?? DEFAULT_TIMEOUT_SECONDS} seconds]`;
749
+ [Command timed out after ${timeoutPolicy.effectiveSeconds} seconds]`;
750
+ outputText += "\n[Timeout hint: split custom searches or service polls into smaller bounded steps with progress output, inspect partial results/logs, then make a decision. Avoid repeating long waits. Reserve long timeouts for installs, builds, and tests.]";
751
+ }
752
+ if (timeoutPolicy.capped) {
753
+ outputText += `
754
+
755
+ [Timeout policy: requested ${timeoutPolicy.requestedSeconds} seconds, capped to ${timeoutPolicy.effectiveSeconds} seconds because ${timeoutPolicy.reason}.]`;
551
756
  }
552
757
  const recoveryHint = buildBashRecoveryHint(command, outputText);
553
758
  if (recoveryHint) {
@@ -555,19 +760,25 @@ function createBashTool(defaultCwd, options) {
555
760
 
556
761
  [Recovery hint: ${recoveryHint}]`;
557
762
  }
763
+ const repeatedCommand = rememberCommandResult(
764
+ `${resolvedShell}\0${workingDir}\0${effectiveCommand}`,
765
+ createHash("sha256").update(JSON.stringify({ code, sig, timedOut, outputText })).digest("hex")
766
+ );
767
+ details.repeatedCommand = repeatedCommand;
768
+ if (repeatedCommand.count > 1 && repeatedCommand.sameResult) {
769
+ outputText += "\n\n[Repeat hint: this exact command already returned the same result in this session. If it proves the task, stop; otherwise change the command or inspect a different file/range.]";
770
+ }
558
771
  const content = [{ type: "text", text: outputText }];
772
+ settled = true;
559
773
  resolve3({ content, details, isError: timedOut || semantic.isError });
560
774
  } catch (error) {
775
+ discardOutputArtifact(tempFilePath, tempFileStream);
776
+ settled = true;
561
777
  reject(error);
562
778
  }
563
779
  });
564
780
  child.on("error", (err) => {
565
- clearTimeout(timeoutId);
566
- signal?.removeEventListener("abort", onAbort);
567
- if (tempFileStream) {
568
- tempFileStream.end();
569
- }
570
- reject(new Error(`Failed to start bash command in ${workingDir}: ${err.message}. Check that the command is executable and cwd exists.`));
781
+ failExecution(new Error(`Failed to start bash command in ${workingDir}: ${err.message}. Check that the command is executable and cwd exists.`));
571
782
  });
572
783
  });
573
784
  }
@@ -577,185 +788,129 @@ function createBashTool(defaultCwd, options) {
577
788
  // src/capabilities/code-intel.ts
578
789
  import * as fs from "node:fs";
579
790
  import * as path from "node:path";
580
- var projectCache = /* @__PURE__ */ new Map();
581
- var typescriptModulePromise;
582
- function loadTypeScript() {
583
- typescriptModulePromise ??= import("./typescript-GSKWJIO4.js");
584
- return typescriptModulePromise;
585
- }
586
- function findTsconfig(startPath, projectRoot) {
587
- let dir = fs.existsSync(startPath) && fs.statSync(startPath).isDirectory() ? startPath : path.dirname(startPath);
588
- const root = path.resolve(projectRoot);
589
- while (dir.startsWith(root)) {
590
- const candidate = path.join(dir, "tsconfig.json");
591
- if (fs.existsSync(candidate)) return candidate;
592
- const parent = path.dirname(dir);
593
- if (parent === dir) break;
594
- dir = parent;
595
- }
596
- const rootCandidate = path.join(root, "tsconfig.json");
597
- return fs.existsSync(rootCandidate) ? rootCandidate : void 0;
598
- }
599
- function readConfig(ts, configPath) {
600
- const config = ts.readConfigFile(configPath, ts.sys.readFile);
601
- if (config.error) {
602
- throw new Error(ts.flattenDiagnosticMessageText(config.error.messageText, "\n"));
603
- }
604
- const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, path.dirname(configPath));
605
- return { fileNames: parsed.fileNames, options: parsed.options };
606
- }
607
- function walkJsTs(root, limit = 1e3) {
608
- const skip = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "Library", "Temp", "obj"]);
609
- const out = [];
610
- const visit = (dir) => {
611
- if (out.length >= limit) return;
612
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
613
- if (skip.has(entry.name)) continue;
614
- const full = path.join(dir, entry.name);
615
- if (entry.isDirectory()) visit(full);
616
- else if (/\.[cm]?[jt]sx?$/.test(entry.name)) out.push(full);
617
- if (out.length >= limit) return;
618
- }
619
- };
620
- if (fs.existsSync(root)) visit(root);
621
- return out;
622
- }
623
- function createProject(ts, projectRoot, focusPath) {
624
- const configPath = findTsconfig(focusPath ?? projectRoot, projectRoot);
625
- const key = configPath ?? `loose:${projectRoot}`;
626
- const cached = projectCache.get(key);
627
- if (cached) return cached;
628
- const config = configPath ? readConfig(ts, configPath) : {
629
- fileNames: walkJsTs(projectRoot),
630
- options: {
631
- allowJs: true,
632
- checkJs: true,
633
- moduleResolution: ts.ModuleResolutionKind.NodeJs,
634
- target: ts.ScriptTarget.ES2022,
635
- module: ts.ModuleKind.ESNext,
636
- jsx: ts.JsxEmit.ReactJSX
791
+ import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
792
+ import { Worker } from "node:worker_threads";
793
+ var WORKER_TIMEOUT_MS = 3e4;
794
+ var MAX_ACTIVE_CODE_INTEL_WORKERS = 1;
795
+ var MAX_QUEUED_CODE_INTEL_WORKERS = 16;
796
+ var activeCodeIntelWorkers = 0;
797
+ var codeIntelWorkerWaiters = [];
798
+ function releaseWorkerSlot() {
799
+ activeCodeIntelWorkers = Math.max(0, activeCodeIntelWorkers - 1);
800
+ while (activeCodeIntelWorkers < MAX_ACTIVE_CODE_INTEL_WORKERS && codeIntelWorkerWaiters.length > 0) {
801
+ const waiter = codeIntelWorkerWaiters.shift();
802
+ waiter.signal?.removeEventListener("abort", waiter.onAbort);
803
+ if (waiter.timeout) clearTimeout(waiter.timeout);
804
+ if (waiter.signal?.aborted) continue;
805
+ activeCodeIntelWorkers++;
806
+ waiter.resolve();
807
+ }
808
+ }
809
+ function acquireWorkerSlot(deadline, signal) {
810
+ if (signal?.aborted) return Promise.reject(new Error("Operation aborted"));
811
+ if (Date.now() >= deadline) return Promise.reject(new Error(`TypeScript code intelligence exceeded ${WORKER_TIMEOUT_MS}ms including queue wait`));
812
+ if (activeCodeIntelWorkers < MAX_ACTIVE_CODE_INTEL_WORKERS) {
813
+ activeCodeIntelWorkers++;
814
+ return Promise.resolve(releaseWorkerSlot);
815
+ }
816
+ if (codeIntelWorkerWaiters.length >= MAX_QUEUED_CODE_INTEL_WORKERS) {
817
+ return Promise.reject(new Error("TypeScript code intelligence queue is full"));
818
+ }
819
+ return new Promise((resolve3, reject) => {
820
+ const waiter = { resolve: () => resolve3(releaseWorkerSlot), reject, signal };
821
+ waiter.onAbort = () => {
822
+ const index = codeIntelWorkerWaiters.indexOf(waiter);
823
+ if (index >= 0) codeIntelWorkerWaiters.splice(index, 1);
824
+ if (waiter.timeout) clearTimeout(waiter.timeout);
825
+ reject(new Error("Operation aborted"));
826
+ };
827
+ waiter.timeout = setTimeout(() => {
828
+ const index = codeIntelWorkerWaiters.indexOf(waiter);
829
+ if (index >= 0) codeIntelWorkerWaiters.splice(index, 1);
830
+ signal?.removeEventListener("abort", waiter.onAbort);
831
+ reject(new Error(`TypeScript code intelligence exceeded ${WORKER_TIMEOUT_MS}ms including queue wait`));
832
+ }, Math.max(1, deadline - Date.now()));
833
+ waiter.timeout.unref?.();
834
+ signal?.addEventListener("abort", waiter.onAbort, { once: true });
835
+ codeIntelWorkerWaiters.push(waiter);
836
+ });
837
+ }
838
+ function resolveWorkerPath() {
839
+ const moduleDirectory = path.dirname(fileURLToPath2(import.meta.url));
840
+ const candidates = [
841
+ path.join(moduleDirectory, "code-intel-worker.cjs"),
842
+ path.join(moduleDirectory, "..", "code-intel-worker.cjs"),
843
+ path.join(moduleDirectory, "..", "..", "dist", "code-intel-worker.cjs")
844
+ ];
845
+ const workerPath = candidates.find((candidate) => fs.existsSync(candidate));
846
+ if (!workerPath) throw new Error("compiled code-intel worker is unavailable; rebuild the CLI");
847
+ return workerPath;
848
+ }
849
+ function runWorker(request, timeoutMs, signal) {
850
+ return new Promise((resolve3, reject) => {
851
+ if (signal?.aborted) {
852
+ reject(new Error("Operation aborted"));
853
+ return;
637
854
  }
638
- };
639
- const versions = new Map(config.fileNames.map((file) => [path.resolve(file), "0"]));
640
- const host = {
641
- getScriptFileNames: () => [...versions.keys()],
642
- getScriptVersion: (fileName) => versions.get(path.resolve(fileName)) ?? "0",
643
- getScriptSnapshot: (fileName) => {
644
- if (!fs.existsSync(fileName)) return void 0;
645
- return ts.ScriptSnapshot.fromString(fs.readFileSync(fileName, "utf8"));
646
- },
647
- getCurrentDirectory: () => configPath ? path.dirname(configPath) : projectRoot,
648
- getCompilationSettings: () => config.options,
649
- getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options),
650
- fileExists: ts.sys.fileExists,
651
- readFile: ts.sys.readFile,
652
- readDirectory: ts.sys.readDirectory,
653
- directoryExists: ts.sys.directoryExists,
654
- getDirectories: ts.sys.getDirectories
655
- };
656
- const project = {
657
- configPath,
658
- service: ts.createLanguageService(host),
659
- fileNames: [...versions.keys()],
660
- options: config.options
661
- };
662
- projectCache.set(key, project);
663
- return project;
664
- }
665
- function offsetOf(ts, sourceFile, line, character) {
666
- if (!line || !character) throw new Error("operation requires path, line, and character");
667
- return ts.getPositionOfLineAndCharacter(sourceFile, Math.max(0, line - 1), Math.max(0, character - 1));
668
- }
669
- function loc(fileName, start = 0) {
670
- const text = fs.existsSync(fileName) ? fs.readFileSync(fileName, "utf8").slice(0, start) : "";
671
- const lines = text.split(/\r?\n/);
672
- return `${fileName}:${lines.length}:${lines[lines.length - 1].length + 1}`;
673
- }
674
- function diagnosticText(ts, d) {
675
- const where = d.file && typeof d.start === "number" ? `${loc(d.file.fileName, d.start)} ` : "";
676
- return `${where}${ts.flattenDiagnosticMessageText(d.messageText, "\n")}`;
677
- }
678
- function navItems(items, fileName) {
679
- const out = [];
680
- const visit = (item, prefix = "") => {
681
- const span = item.spans[0];
682
- out.push(`${prefix}${item.text} (${item.kind}) ${span ? loc(fileName, span.start) : fileName}`);
683
- for (const child of item.childItems ?? []) visit(child, `${prefix}${item.text}.`);
684
- };
685
- for (const item of items) visit(item);
686
- return out;
855
+ const worker = new Worker(pathToFileURL(resolveWorkerPath()), {
856
+ workerData: request,
857
+ execArgv: [],
858
+ resourceLimits: { maxOldGenerationSizeMb: 128, maxYoungGenerationSizeMb: 32, stackSizeMb: 4 }
859
+ });
860
+ let finishing = false;
861
+ let exited = false;
862
+ const finish = (error, result) => {
863
+ if (finishing) return;
864
+ finishing = true;
865
+ clearTimeout(timeout);
866
+ signal?.removeEventListener("abort", onAbort);
867
+ void (async () => {
868
+ if (!exited) {
869
+ try {
870
+ await worker.terminate();
871
+ } catch {
872
+ }
873
+ }
874
+ if (error) reject(error);
875
+ else resolve3(result);
876
+ })();
877
+ };
878
+ const onAbort = () => finish(new Error("Operation aborted"));
879
+ const timeout = setTimeout(() => finish(new Error(`TypeScript code intelligence exceeded ${WORKER_TIMEOUT_MS}ms including queue wait`)), Math.max(1, timeoutMs));
880
+ timeout.unref?.();
881
+ signal?.addEventListener("abort", onAbort, { once: true });
882
+ worker.once("message", (message) => {
883
+ if (!message || typeof message !== "object" || typeof message.ok !== "boolean") {
884
+ finish(new Error("code-intel worker returned an invalid response"));
885
+ return;
886
+ }
887
+ if ("error" in message) finish(new Error(message.error));
888
+ else finish(void 0, message.result);
889
+ });
890
+ worker.once("error", (error) => finish(error));
891
+ worker.once("exit", (code) => {
892
+ exited = true;
893
+ if (!finishing) finish(new Error(`code-intel worker exited before responding (${code})`));
894
+ });
895
+ });
896
+ }
897
+ async function runWorkerBounded(request, signal) {
898
+ const deadline = Date.now() + WORKER_TIMEOUT_MS;
899
+ const release = await acquireWorkerSlot(deadline, signal);
900
+ try {
901
+ const remaining = deadline - Date.now();
902
+ if (remaining <= 0) throw new Error(`TypeScript code intelligence exceeded ${WORKER_TIMEOUT_MS}ms including queue wait`);
903
+ return await runWorker(request, remaining, signal);
904
+ } finally {
905
+ release();
906
+ }
687
907
  }
688
908
  function createTypeScriptCodeIntelProvider() {
689
- const lightweight = createLightweightCodeIntelProvider();
690
909
  return {
691
910
  async run(input, context) {
692
- try {
693
- const ts = await loadTypeScript();
694
- const filePath = context.resolvePath(input.path);
695
- const project = createProject(ts, context.projectRoot, filePath);
696
- const files = filePath ? [filePath] : project.fileNames;
697
- if (input.operation === "diagnostics") {
698
- const diagnostics = files.flatMap((file) => [
699
- ...project.service.getSyntacticDiagnostics(file),
700
- ...project.service.getSemanticDiagnostics(file)
701
- ]);
702
- return {
703
- text: diagnostics.length ? diagnostics.map((diagnostic) => diagnosticText(ts, diagnostic)).join("\n") : "No TypeScript diagnostics found.",
704
- details: {
705
- operation: input.operation,
706
- projectRoot: context.projectRoot,
707
- path: filePath,
708
- provider: "typescript_service",
709
- resultCount: diagnostics.length,
710
- diagnosticCount: diagnostics.length,
711
- tsconfigPath: project.configPath
712
- }
713
- };
714
- }
715
- if (input.operation === "symbols") {
716
- const query = input.query?.toLowerCase();
717
- const symbols = files.flatMap((file) => navItems(project.service.getNavigationBarItems(file), file)).filter((line) => !query || line.toLowerCase().includes(query));
718
- return {
719
- text: symbols.length ? symbols.join("\n") : "No symbols found.",
720
- details: { operation: input.operation, projectRoot: context.projectRoot, path: filePath, provider: "typescript_service", resultCount: symbols.length, tsconfigPath: project.configPath }
721
- };
722
- }
723
- if (!filePath) throw new Error(`${input.operation} requires path`);
724
- const source = project.service.getProgram()?.getSourceFile(filePath);
725
- if (!source) throw new Error(`File is not part of the TypeScript project: ${input.path}`);
726
- const offset = offsetOf(ts, source, input.line, input.character);
727
- if (input.operation === "definition") {
728
- const defs = project.service.getDefinitionAtPosition(filePath, offset) ?? [];
729
- return {
730
- text: defs.length ? defs.map((def) => `${def.name} ${loc(def.fileName, def.textSpan.start)}`).join("\n") : "No definition found.",
731
- details: { operation: input.operation, projectRoot: context.projectRoot, path: filePath, provider: "typescript_service", resultCount: defs.length, tsconfigPath: project.configPath }
732
- };
733
- }
734
- if (input.operation === "references") {
735
- const refs = project.service.getReferencesAtPosition(filePath, offset) ?? [];
736
- return {
737
- text: refs.length ? refs.map((ref) => loc(ref.fileName, ref.textSpan.start)).join("\n") : "No references found.",
738
- details: { operation: input.operation, projectRoot: context.projectRoot, path: filePath, provider: "typescript_service", resultCount: refs.length, tsconfigPath: project.configPath }
739
- };
740
- }
741
- const quickInfo = project.service.getQuickInfoAtPosition(filePath, offset);
742
- const text = quickInfo ? ts.displayPartsToString(quickInfo.displayParts ?? []) + (quickInfo.documentation?.length ? `
743
- ${ts.displayPartsToString(quickInfo.documentation)}` : "") : "No hover information found.";
744
- return {
745
- text,
746
- details: { operation: input.operation, projectRoot: context.projectRoot, path: filePath, provider: "typescript_service", resultCount: quickInfo ? 1 : 0, tsconfigPath: project.configPath }
747
- };
748
- } catch (error) {
749
- const fallback = await lightweight.run(input, context);
750
- return {
751
- ...fallback,
752
- details: {
753
- ...fallback.details,
754
- provider: "lightweight_fallback",
755
- fallbackReason: error instanceof Error ? error.message : String(error)
756
- }
757
- };
758
- }
911
+ const filePath = context.resolvePath(input.path);
912
+ if (filePath && !context.isPathAllowed(filePath)) throw new Error(`Path escapes the project root: ${input.path}`);
913
+ return await runWorkerBounded({ input, projectRoot: context.projectRoot, filePath }, context.signal);
759
914
  }
760
915
  };
761
916
  }
@@ -833,12 +988,12 @@ function createCliRenderedPageReader() {
833
988
  try {
834
989
  await runPlaywright([`-s=${session}`, "open", "about:blank"]);
835
990
  await runPlaywright([`-s=${session}`, "goto", url]);
836
- const [snapshot, pageInfoRaw, screenshotOutput, consoleOutput, networkOutput] = await Promise.all([
991
+ const [snapshot, pageInfoRaw, screenshotOutput, consoleOutput, requestsOutput] = await Promise.all([
837
992
  runPlaywright([`-s=${session}`, "snapshot"]),
838
993
  runPlaywright([`-s=${session}`, "--raw", "eval", "() => JSON.stringify({ title: document.title, href: location.href, text: document.body ? document.body.innerText : '' })"]),
839
994
  runPlaywright([`-s=${session}`, "--raw", "screenshot"]).catch((error) => `screenshot_failed: ${error instanceof Error ? error.message : String(error)}`),
840
995
  runPlaywright([`-s=${session}`, "--raw", "console"]).catch((error) => `console_failed: ${error instanceof Error ? error.message : String(error)}`),
841
- runPlaywright([`-s=${session}`, "--raw", "network"]).catch((error) => `network_failed: ${error instanceof Error ? error.message : String(error)}`)
996
+ runPlaywright([`-s=${session}`, "--raw", "requests"]).catch((error) => `requests_failed: ${error instanceof Error ? error.message : String(error)}`)
842
997
  ]);
843
998
  const pageInfo = parsePageInfo(pageInfoRaw);
844
999
  const text = pageInfo.text?.trim() || snapshot.trim();
@@ -853,7 +1008,7 @@ function createCliRenderedPageReader() {
853
1008
  citationAnchors: [{ url: pageInfo.finalUrl || url, title: pageInfo.title, startOffset: 0, endOffset: Math.min(120, text.length) }],
854
1009
  screenshotPath: parseScreenshotPath(screenshotOutput),
855
1010
  consoleMessages: linesOf(consoleOutput),
856
- networkRequests: linesOf(networkOutput)
1011
+ networkRequests: linesOf(requestsOutput)
857
1012
  };
858
1013
  } finally {
859
1014
  runPlaywright([`-s=${session}`, "close"], 1e4).catch(() => void 0);
@@ -983,10 +1138,12 @@ export {
983
1138
  getAgentDir,
984
1139
  getCustomThemesDir,
985
1140
  getSettingsPath,
1141
+ getProjectSettingsPath,
986
1142
  getSessionsDir,
987
1143
  getLogsDir,
988
1144
  getRuntimeLogPath,
989
1145
  migrateConfigFromAgentDir,
990
1146
  createCliWebResearchHealthStore,
1147
+ cleanupBashOutputArtifacts,
991
1148
  createCliHostCapabilities
992
1149
  };