@cydm/pie 1.0.24 → 1.0.26

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 (31) hide show
  1. package/README.md +23 -5
  2. package/dist/builtin/extensions/ask-user/index.js +3 -4
  3. package/dist/builtin/extensions/ask-user/package.json +16 -0
  4. package/dist/builtin/extensions/changelog/index.js +65 -65
  5. package/dist/builtin/extensions/changelog/package.json +11 -0
  6. package/dist/builtin/extensions/deploy/package.json +11 -0
  7. package/dist/builtin/extensions/document-attachments/package.json +11 -0
  8. package/dist/builtin/extensions/files/package.json +14 -0
  9. package/dist/builtin/extensions/init/index.js +33 -33
  10. package/dist/builtin/extensions/init/package.json +11 -0
  11. package/dist/builtin/extensions/kimi-attachments/index.js +1 -1
  12. package/dist/builtin/extensions/kimi-attachments/package.json +11 -0
  13. package/dist/builtin/extensions/plan-mode/index.js +5 -34
  14. package/dist/builtin/extensions/plan-mode/package.json +11 -0
  15. package/dist/builtin/extensions/subagent/index.js +4 -5
  16. package/dist/builtin/extensions/subagent/package.json +14 -0
  17. package/dist/builtin/extensions/todo/index.js +3 -4
  18. package/dist/builtin/extensions/todo/package.json +11 -0
  19. package/dist/chunks/{chunk-X44YTB6R.js → chunk-4B42ZU4B.js} +1 -1
  20. package/dist/chunks/{chunk-HAAYC2N5.js → chunk-64XJIKVK.js} +334 -99
  21. package/dist/chunks/{chunk-QYYRJ7JW.js → chunk-GARJAWH6.js} +375 -252
  22. package/dist/chunks/{chunk-XWNIIJ3X.js → chunk-L63BJMTT.js} +7537 -6629
  23. package/dist/chunks/{src-PENSDHBM.js → src-27UD2ZBZ.js} +2 -2
  24. package/dist/chunks/{test-stream-PRUYMLXQ.js → test-stream-ZNGAUUVH.js} +6 -2
  25. package/dist/cli.js +2229 -1686
  26. package/dist/{chunks/typescript-GSKWJIO4.js → code-intel-worker.cjs} +2507 -2031
  27. package/models.schema.json +38 -13
  28. package/package.json +10 -6
  29. package/dist/chunks/chunk-J4P2LP7K.js +0 -3846
  30. package/dist/chunks/chunk-QMIIW75M.js +0 -95
  31. package/dist/session-preview-worker.js +0 -159
@@ -2,18 +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
- interpretShellExit
10
- } from "./chunk-XWNIIJ3X.js";
11
- import {
8
+ interpretShellExit,
12
9
  requestInteraction
13
- } from "./chunk-J4P2LP7K.js";
10
+ } from "./chunk-L63BJMTT.js";
14
11
  import {
15
12
  Type
16
- } from "./chunk-HAAYC2N5.js";
13
+ } from "./chunk-64XJIKVK.js";
17
14
 
18
15
  // src/config.ts
19
16
  import { existsSync, mkdirSync, readFileSync, renameSync } from "fs";
@@ -130,14 +127,54 @@ function migrateConfigFromAgentDir() {
130
127
 
131
128
  // src/capabilities/bash/index.ts
132
129
  import { spawn } from "child_process";
133
- import { createWriteStream, existsSync as existsSync2, statSync, writeFileSync } from "fs";
134
- import { tmpdir, homedir as homedir2 } from "os";
130
+ import { closeSync, createWriteStream, existsSync as existsSync2, openSync, statSync, unlinkSync, writeFileSync } from "fs";
131
+ import { tmpdir } from "os";
135
132
  import { join as join2, resolve as resolve2 } from "path";
136
- import { randomBytes } from "crypto";
133
+ import { createHash, randomBytes } from "crypto";
137
134
 
138
135
  // src/capabilities/bash/truncate.ts
139
- var DEFAULT_MAX_LINES = 2e3;
140
- 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
+ }
141
178
  function formatSize(bytes) {
142
179
  if (bytes < 1024) {
143
180
  return `${bytes}B`;
@@ -222,7 +259,7 @@ var bashSchema = Type.Object({
222
259
  command: Type.String({ description: "The bash command to execute" }),
223
260
  timeout: Type.Optional(
224
261
  Type.Number({
225
- 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."
226
263
  })
227
264
  ),
228
265
  cwd: Type.Optional(Type.String({ description: "Working directory for the command" })),
@@ -234,6 +271,23 @@ var bashSchema = Type.Object({
234
271
  ], { description: "Shell to use. Defaults to bash on macOS/Linux and PowerShell on Windows. Use this only for platform-specific commands." }))
235
272
  });
236
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
+ }
237
291
  function validateBashCommand(command) {
238
292
  const value = typeof command === "string" ? command : "";
239
293
  const trimmed = value.trim();
@@ -307,18 +361,95 @@ function getTempFilePath() {
307
361
  const id = randomBytes(8).toString("hex");
308
362
  return join2(tmpdir(), `pie-bash-${id}.log`);
309
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);
310
422
  function finishTempFileStream(stream) {
311
- if (!stream) return Promise.resolve();
312
- return new Promise((resolve4, reject) => {
313
- const onError = (error) => {
423
+ if (!stream || stream.closed) return Promise.resolve();
424
+ return new Promise((resolve3, reject) => {
425
+ let done = false;
426
+ const cleanup = () => {
427
+ stream.off("close", onClose);
314
428
  stream.off("error", onError);
315
- reject(error);
316
429
  };
430
+ const onClose = () => {
431
+ if (done) return;
432
+ done = true;
433
+ cleanup();
434
+ resolve3();
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);
317
443
  stream.once("error", onError);
318
- stream.end(() => {
319
- stream.off("error", onError);
320
- resolve4();
321
- });
444
+ try {
445
+ if (!stream.writableEnded) {
446
+ stream.end();
447
+ } else if (stream.closed) {
448
+ onClose();
449
+ }
450
+ } catch (error) {
451
+ onError(error);
452
+ }
322
453
  });
323
454
  }
324
455
  function buildBashRecoveryHint(command, output) {
@@ -351,10 +482,22 @@ function killProcessTree(pid) {
351
482
  }
352
483
  }
353
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
+ };
354
497
  return {
355
498
  name: "bash",
356
499
  label: "bash",
357
- 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.`,
358
501
  parameters: bashSchema,
359
502
  execute: async (_toolCallId, { command, timeout, cwd, shell }, signal, onUpdate) => {
360
503
  validateBashCommand(command);
@@ -363,34 +506,9 @@ function createBashTool(defaultCwd, options) {
363
506
  const workingDir = normalizeWorkingDirectory(cwd, defaultCwd);
364
507
  validateWorkingDirectory(workingDir);
365
508
  const risk = classifyShellCommand(effectiveCommand, resolvedShell, {
366
- homeDir: homedir2(),
367
509
  cwd: workingDir,
368
- env: process.env,
369
- pathStyle: process.platform === "win32" ? "windows" : "posix"
510
+ sandboxRoot: defaultCwd ?? workingDir
370
511
  });
371
- if (risk.destructive && risk.catastrophic) {
372
- const targetExplanation = risk.catastrophicReason === "indeterminate_target" ? "The agent shell tool cannot statically prove every deletion target is scoped" : "The command targets a filesystem root, home directory, or system directory";
373
- return {
374
- content: [{
375
- type: "text",
376
- text: `Blocked catastrophic shell command (${risk.commandClass}): ${risk.riskWarnings.join("; ")}. ${targetExplanation} and is never executed, even in YOLO mode, by the agent shell tool. Narrow the target to a statically resolvable subdirectory and retry.`
377
- }],
378
- details: {
379
- exitCode: null,
380
- signal: null,
381
- timedOut: false,
382
- shell: resolvedShell,
383
- commandClass: risk.commandClass,
384
- riskWarnings: risk.riskWarnings,
385
- permissionDecision: {
386
- allowed: false,
387
- reason: "catastrophic command blocked by shell policy",
388
- requiresPermission: true
389
- }
390
- },
391
- isError: true
392
- };
393
- }
394
512
  if (risk.destructive && !options?.isYoloMode?.()) {
395
513
  const approved = await (options?.confirm ?? requestHighRiskConfirmation)({
396
514
  command: effectiveCommand,
@@ -421,18 +539,31 @@ function createBashTool(defaultCwd, options) {
421
539
  };
422
540
  }
423
541
  }
424
- const timeoutMs = (timeout ?? DEFAULT_TIMEOUT_SECONDS) * 1e3;
425
- return new Promise((resolve4, reject) => {
542
+ const timeoutPolicy = resolveTimeoutPolicy(timeout, risk.commandClass);
543
+ const timeoutMs = timeoutPolicy.effectiveSeconds * 1e3;
544
+ return new Promise((resolve3, reject) => {
426
545
  if (signal?.aborted) {
427
546
  reject(new Error("Operation aborted"));
428
547
  return;
429
548
  }
430
549
  let tempFilePath;
431
550
  let tempFileStream;
551
+ let settled = false;
432
552
  let totalBytes = 0;
433
553
  const chunks = [];
434
554
  let chunksBytes = 0;
435
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
+ };
436
567
  let timedOut = false;
437
568
  const spawnCommand = buildSpawnCommand(resolvedShell, effectiveCommand);
438
569
  const child = (options?.spawnProcess ?? spawn)(spawnCommand.file, spawnCommand.args, {
@@ -447,19 +578,33 @@ function createBashTool(defaultCwd, options) {
447
578
  killProcessTree(child.pid);
448
579
  }
449
580
  }, timeoutMs);
450
- const onAbort = () => {
581
+ const failExecution = (error) => {
582
+ if (settled) return;
583
+ settled = true;
451
584
  clearTimeout(timeoutId);
452
- if (child.pid) {
453
- killProcessTree(child.pid);
454
- }
455
- 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"));
456
592
  };
457
593
  signal?.addEventListener("abort", onAbort, { once: true });
458
594
  child.stdout?.on("data", (data) => {
595
+ if (settled) return;
459
596
  totalBytes += data.length;
597
+ captureHead(data);
460
598
  if (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {
461
- tempFilePath = getTempFilePath();
462
- 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
+ }
463
608
  for (const chunk of chunks) {
464
609
  tempFileStream.write(chunk);
465
610
  }
@@ -476,7 +621,7 @@ function createBashTool(defaultCwd, options) {
476
621
  if (onUpdate) {
477
622
  const fullBuffer = Buffer.concat(chunks);
478
623
  const fullText = fullBuffer.toString("utf-8");
479
- const truncation = truncateTail(fullText);
624
+ const truncation = truncateHeadTail(fullText);
480
625
  onUpdate({
481
626
  content: [{ type: "text", text: truncation.content || "" }],
482
627
  details: {
@@ -493,6 +638,7 @@ function createBashTool(defaultCwd, options) {
493
638
  shell: resolvedShell,
494
639
  commandClass: risk.commandClass,
495
640
  riskWarnings: risk.riskWarnings,
641
+ timeoutPolicy,
496
642
  permissionDecision: {
497
643
  allowed: true,
498
644
  requiresPermission: risk.requiresPermission
@@ -502,10 +648,19 @@ function createBashTool(defaultCwd, options) {
502
648
  }
503
649
  });
504
650
  child.stderr?.on("data", (data) => {
651
+ if (settled) return;
505
652
  totalBytes += data.length;
653
+ captureHead(data);
506
654
  if (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {
507
- tempFilePath = getTempFilePath();
508
- 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
+ }
509
664
  for (const chunk of chunks) {
510
665
  tempFileStream.write(chunk);
511
666
  }
@@ -524,13 +679,32 @@ function createBashTool(defaultCwd, options) {
524
679
  try {
525
680
  clearTimeout(timeoutId);
526
681
  signal?.removeEventListener("abort", onAbort);
682
+ if (settled) return;
527
683
  const fullBuffer = Buffer.concat(chunks);
528
684
  let fullOutput = fullBuffer.toString("utf-8");
529
- 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
+ }
530
697
  let outputText = truncation.content || "(no output)";
531
698
  if (truncation.truncated && !tempFilePath) {
532
- tempFilePath = getTempFilePath();
533
- 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
+ }
534
708
  }
535
709
  await finishTempFileStream(tempFileStream);
536
710
  const details = {
@@ -540,6 +714,7 @@ function createBashTool(defaultCwd, options) {
540
714
  shell: resolvedShell,
541
715
  commandClass: risk.commandClass,
542
716
  riskWarnings: risk.riskWarnings,
717
+ timeoutPolicy,
543
718
  permissionDecision: {
544
719
  allowed: true,
545
720
  requiresPermission: risk.requiresPermission
@@ -558,29 +733,26 @@ function createBashTool(defaultCwd, options) {
558
733
  outputLines: truncation.outputLines
559
734
  };
560
735
  details.fullOutputPath = tempFilePath;
561
- const startLine = truncation.totalLines - truncation.outputLines + 1;
562
- const endLine = truncation.totalLines;
563
736
  if (truncation.lastLinePartial) {
564
- const lastLineSize = formatSize(
565
- Buffer.byteLength(fullOutput.split("\n").pop() || "", "utf-8")
566
- );
567
- outputText += `
568
-
569
- [Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${tempFilePath}]`;
570
- } else if (truncation.truncatedBy === "lines") {
571
737
  outputText += `
572
738
 
573
- [Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${tempFilePath}]`;
739
+ [Head/tail preview limited to ${formatSize(truncation.outputBytes)}. Full output: ${tempFilePath}]`;
574
740
  } else {
575
741
  outputText += `
576
742
 
577
- [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}]`;
578
744
  }
579
745
  }
580
746
  if (timedOut) {
581
747
  outputText += `
582
748
 
583
- [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}.]`;
584
756
  }
585
757
  const recoveryHint = buildBashRecoveryHint(command, outputText);
586
758
  if (recoveryHint) {
@@ -588,19 +760,25 @@ function createBashTool(defaultCwd, options) {
588
760
 
589
761
  [Recovery hint: ${recoveryHint}]`;
590
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
+ }
591
771
  const content = [{ type: "text", text: outputText }];
592
- resolve4({ content, details, isError: timedOut || semantic.isError });
772
+ settled = true;
773
+ resolve3({ content, details, isError: timedOut || semantic.isError });
593
774
  } catch (error) {
775
+ discardOutputArtifact(tempFilePath, tempFileStream);
776
+ settled = true;
594
777
  reject(error);
595
778
  }
596
779
  });
597
780
  child.on("error", (err) => {
598
- clearTimeout(timeoutId);
599
- signal?.removeEventListener("abort", onAbort);
600
- if (tempFileStream) {
601
- tempFileStream.end();
602
- }
603
- 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.`));
604
782
  });
605
783
  });
606
784
  }
@@ -610,185 +788,129 @@ function createBashTool(defaultCwd, options) {
610
788
  // src/capabilities/code-intel.ts
611
789
  import * as fs from "node:fs";
612
790
  import * as path from "node:path";
613
- var projectCache = /* @__PURE__ */ new Map();
614
- var typescriptModulePromise;
615
- function loadTypeScript() {
616
- typescriptModulePromise ??= import("./typescript-GSKWJIO4.js");
617
- return typescriptModulePromise;
618
- }
619
- function findTsconfig(startPath, projectRoot) {
620
- let dir = fs.existsSync(startPath) && fs.statSync(startPath).isDirectory() ? startPath : path.dirname(startPath);
621
- const root = path.resolve(projectRoot);
622
- while (dir.startsWith(root)) {
623
- const candidate = path.join(dir, "tsconfig.json");
624
- if (fs.existsSync(candidate)) return candidate;
625
- const parent = path.dirname(dir);
626
- if (parent === dir) break;
627
- dir = parent;
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();
628
807
  }
629
- const rootCandidate = path.join(root, "tsconfig.json");
630
- return fs.existsSync(rootCandidate) ? rootCandidate : void 0;
631
808
  }
632
- function readConfig(ts, configPath) {
633
- const config = ts.readConfigFile(configPath, ts.sys.readFile);
634
- if (config.error) {
635
- throw new Error(ts.flattenDiagnosticMessageText(config.error.messageText, "\n"));
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);
636
815
  }
637
- const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, path.dirname(configPath));
638
- return { fileNames: parsed.fileNames, options: parsed.options };
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
+ });
639
837
  }
640
- function walkJsTs(root, limit = 1e3) {
641
- const skip = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "Library", "Temp", "obj"]);
642
- const out = [];
643
- const visit = (dir) => {
644
- if (out.length >= limit) return;
645
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
646
- if (skip.has(entry.name)) continue;
647
- const full = path.join(dir, entry.name);
648
- if (entry.isDirectory()) visit(full);
649
- else if (/\.[cm]?[jt]sx?$/.test(entry.name)) out.push(full);
650
- if (out.length >= limit) return;
651
- }
652
- };
653
- if (fs.existsSync(root)) visit(root);
654
- return out;
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;
655
848
  }
656
- function createProject(ts, projectRoot, focusPath) {
657
- const configPath = findTsconfig(focusPath ?? projectRoot, projectRoot);
658
- const key = configPath ?? `loose:${projectRoot}`;
659
- const cached = projectCache.get(key);
660
- if (cached) return cached;
661
- const config = configPath ? readConfig(ts, configPath) : {
662
- fileNames: walkJsTs(projectRoot),
663
- options: {
664
- allowJs: true,
665
- checkJs: true,
666
- moduleResolution: ts.ModuleResolutionKind.NodeJs,
667
- target: ts.ScriptTarget.ES2022,
668
- module: ts.ModuleKind.ESNext,
669
- jsx: ts.JsxEmit.ReactJSX
849
+ function runWorker(request, timeoutMs, signal) {
850
+ return new Promise((resolve3, reject) => {
851
+ if (signal?.aborted) {
852
+ reject(new Error("Operation aborted"));
853
+ return;
670
854
  }
671
- };
672
- const versions = new Map(config.fileNames.map((file) => [path.resolve(file), "0"]));
673
- const host = {
674
- getScriptFileNames: () => [...versions.keys()],
675
- getScriptVersion: (fileName) => versions.get(path.resolve(fileName)) ?? "0",
676
- getScriptSnapshot: (fileName) => {
677
- if (!fs.existsSync(fileName)) return void 0;
678
- return ts.ScriptSnapshot.fromString(fs.readFileSync(fileName, "utf8"));
679
- },
680
- getCurrentDirectory: () => configPath ? path.dirname(configPath) : projectRoot,
681
- getCompilationSettings: () => config.options,
682
- getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options),
683
- fileExists: ts.sys.fileExists,
684
- readFile: ts.sys.readFile,
685
- readDirectory: ts.sys.readDirectory,
686
- directoryExists: ts.sys.directoryExists,
687
- getDirectories: ts.sys.getDirectories
688
- };
689
- const project = {
690
- configPath,
691
- service: ts.createLanguageService(host),
692
- fileNames: [...versions.keys()],
693
- options: config.options
694
- };
695
- projectCache.set(key, project);
696
- return project;
697
- }
698
- function offsetOf(ts, sourceFile, line, character) {
699
- if (!line || !character) throw new Error("operation requires path, line, and character");
700
- return ts.getPositionOfLineAndCharacter(sourceFile, Math.max(0, line - 1), Math.max(0, character - 1));
701
- }
702
- function loc(fileName, start = 0) {
703
- const text = fs.existsSync(fileName) ? fs.readFileSync(fileName, "utf8").slice(0, start) : "";
704
- const lines = text.split(/\r?\n/);
705
- return `${fileName}:${lines.length}:${lines[lines.length - 1].length + 1}`;
706
- }
707
- function diagnosticText(ts, d) {
708
- const where = d.file && typeof d.start === "number" ? `${loc(d.file.fileName, d.start)} ` : "";
709
- return `${where}${ts.flattenDiagnosticMessageText(d.messageText, "\n")}`;
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
+ });
710
896
  }
711
- function navItems(items, fileName) {
712
- const out = [];
713
- const visit = (item, prefix = "") => {
714
- const span = item.spans[0];
715
- out.push(`${prefix}${item.text} (${item.kind}) ${span ? loc(fileName, span.start) : fileName}`);
716
- for (const child of item.childItems ?? []) visit(child, `${prefix}${item.text}.`);
717
- };
718
- for (const item of items) visit(item);
719
- return out;
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
+ }
720
907
  }
721
908
  function createTypeScriptCodeIntelProvider() {
722
- const lightweight = createLightweightCodeIntelProvider();
723
909
  return {
724
910
  async run(input, context) {
725
- try {
726
- const ts = await loadTypeScript();
727
- const filePath = context.resolvePath(input.path);
728
- const project = createProject(ts, context.projectRoot, filePath);
729
- const files = filePath ? [filePath] : project.fileNames;
730
- if (input.operation === "diagnostics") {
731
- const diagnostics = files.flatMap((file) => [
732
- ...project.service.getSyntacticDiagnostics(file),
733
- ...project.service.getSemanticDiagnostics(file)
734
- ]);
735
- return {
736
- text: diagnostics.length ? diagnostics.map((diagnostic) => diagnosticText(ts, diagnostic)).join("\n") : "No TypeScript diagnostics found.",
737
- details: {
738
- operation: input.operation,
739
- projectRoot: context.projectRoot,
740
- path: filePath,
741
- provider: "typescript_service",
742
- resultCount: diagnostics.length,
743
- diagnosticCount: diagnostics.length,
744
- tsconfigPath: project.configPath
745
- }
746
- };
747
- }
748
- if (input.operation === "symbols") {
749
- const query = input.query?.toLowerCase();
750
- const symbols = files.flatMap((file) => navItems(project.service.getNavigationBarItems(file), file)).filter((line) => !query || line.toLowerCase().includes(query));
751
- return {
752
- text: symbols.length ? symbols.join("\n") : "No symbols found.",
753
- details: { operation: input.operation, projectRoot: context.projectRoot, path: filePath, provider: "typescript_service", resultCount: symbols.length, tsconfigPath: project.configPath }
754
- };
755
- }
756
- if (!filePath) throw new Error(`${input.operation} requires path`);
757
- const source = project.service.getProgram()?.getSourceFile(filePath);
758
- if (!source) throw new Error(`File is not part of the TypeScript project: ${input.path}`);
759
- const offset = offsetOf(ts, source, input.line, input.character);
760
- if (input.operation === "definition") {
761
- const defs = project.service.getDefinitionAtPosition(filePath, offset) ?? [];
762
- return {
763
- text: defs.length ? defs.map((def) => `${def.name} ${loc(def.fileName, def.textSpan.start)}`).join("\n") : "No definition found.",
764
- details: { operation: input.operation, projectRoot: context.projectRoot, path: filePath, provider: "typescript_service", resultCount: defs.length, tsconfigPath: project.configPath }
765
- };
766
- }
767
- if (input.operation === "references") {
768
- const refs = project.service.getReferencesAtPosition(filePath, offset) ?? [];
769
- return {
770
- text: refs.length ? refs.map((ref) => loc(ref.fileName, ref.textSpan.start)).join("\n") : "No references found.",
771
- details: { operation: input.operation, projectRoot: context.projectRoot, path: filePath, provider: "typescript_service", resultCount: refs.length, tsconfigPath: project.configPath }
772
- };
773
- }
774
- const quickInfo = project.service.getQuickInfoAtPosition(filePath, offset);
775
- const text = quickInfo ? ts.displayPartsToString(quickInfo.displayParts ?? []) + (quickInfo.documentation?.length ? `
776
- ${ts.displayPartsToString(quickInfo.documentation)}` : "") : "No hover information found.";
777
- return {
778
- text,
779
- details: { operation: input.operation, projectRoot: context.projectRoot, path: filePath, provider: "typescript_service", resultCount: quickInfo ? 1 : 0, tsconfigPath: project.configPath }
780
- };
781
- } catch (error) {
782
- const fallback = await lightweight.run(input, context);
783
- return {
784
- ...fallback,
785
- details: {
786
- ...fallback.details,
787
- provider: "lightweight_fallback",
788
- fallbackReason: error instanceof Error ? error.message : String(error)
789
- }
790
- };
791
- }
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);
792
914
  }
793
915
  };
794
916
  }
@@ -807,7 +929,7 @@ function resolvePlaywrightCli() {
807
929
  return require2.resolve("@playwright/cli/playwright-cli.js");
808
930
  }
809
931
  function runPlaywright(args, timeoutMs = 6e4) {
810
- return new Promise((resolve4, reject) => {
932
+ return new Promise((resolve3, reject) => {
811
933
  const child = spawn2(process.execPath, [resolvePlaywrightCli(), ...args], {
812
934
  stdio: ["ignore", "pipe", "pipe"],
813
935
  shell: false
@@ -832,7 +954,7 @@ function runPlaywright(args, timeoutMs = 6e4) {
832
954
  });
833
955
  child.on("close", (code) => {
834
956
  clearTimeout(timer);
835
- if (code === 0) resolve4(stdout.trim());
957
+ if (code === 0) resolve3(stdout.trim());
836
958
  else reject(new Error((stderr || stdout || `Playwright CLI exited with ${code}`).trim()));
837
959
  });
838
960
  });
@@ -941,7 +1063,7 @@ function createCliWebResearchHealthStore(configDir) {
941
1063
 
942
1064
  // src/capabilities/index.ts
943
1065
  function createCliHostCapabilities(cwd, options = {}) {
944
- const bashTool = createBashTool(cwd, { isYoloMode: options.isYoloMode });
1066
+ const bashTool = createBashTool(cwd);
945
1067
  const healthStore = createCliWebResearchHealthStore(options.configDir ?? getConfigDir());
946
1068
  const webSearchTool = createSharedWebSearchTool({
947
1069
  getModel: options.getModel ?? (() => void 0),
@@ -1022,5 +1144,6 @@ export {
1022
1144
  getRuntimeLogPath,
1023
1145
  migrateConfigFromAgentDir,
1024
1146
  createCliWebResearchHealthStore,
1147
+ cleanupBashOutputArtifacts,
1025
1148
  createCliHostCapabilities
1026
1149
  };