@oh-my-pi/pi-coding-agent 6.2.0 → 6.7.0

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 (93) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/docs/sdk.md +1 -1
  3. package/package.json +5 -5
  4. package/scripts/generate-template.ts +6 -6
  5. package/src/cli/args.ts +3 -0
  6. package/src/core/agent-session.ts +39 -0
  7. package/src/core/bash-executor.ts +3 -3
  8. package/src/core/cursor/exec-bridge.ts +95 -88
  9. package/src/core/custom-commands/bundled/review/index.ts +142 -145
  10. package/src/core/custom-commands/bundled/wt/index.ts +68 -66
  11. package/src/core/custom-commands/loader.ts +4 -6
  12. package/src/core/custom-tools/index.ts +2 -2
  13. package/src/core/custom-tools/loader.ts +66 -61
  14. package/src/core/custom-tools/types.ts +4 -4
  15. package/src/core/custom-tools/wrapper.ts +61 -25
  16. package/src/core/event-bus.ts +19 -47
  17. package/src/core/extensions/index.ts +8 -4
  18. package/src/core/extensions/loader.ts +160 -120
  19. package/src/core/extensions/types.ts +4 -4
  20. package/src/core/extensions/wrapper.ts +149 -100
  21. package/src/core/hooks/index.ts +1 -1
  22. package/src/core/hooks/tool-wrapper.ts +96 -70
  23. package/src/core/hooks/types.ts +1 -2
  24. package/src/core/index.ts +1 -0
  25. package/src/core/mcp/index.ts +6 -2
  26. package/src/core/mcp/json-rpc.ts +88 -0
  27. package/src/core/mcp/loader.ts +22 -4
  28. package/src/core/mcp/manager.ts +202 -48
  29. package/src/core/mcp/tool-bridge.ts +143 -55
  30. package/src/core/mcp/tool-cache.ts +122 -0
  31. package/src/core/python-executor.ts +3 -9
  32. package/src/core/sdk.ts +33 -32
  33. package/src/core/session-manager.ts +30 -0
  34. package/src/core/settings-manager.ts +34 -1
  35. package/src/core/ssh/ssh-executor.ts +6 -84
  36. package/src/core/streaming-output.ts +107 -53
  37. package/src/core/tools/ask.ts +92 -93
  38. package/src/core/tools/bash.ts +103 -94
  39. package/src/core/tools/calculator.ts +41 -26
  40. package/src/core/tools/complete.ts +76 -66
  41. package/src/core/tools/context.ts +22 -24
  42. package/src/core/tools/exa/index.ts +1 -1
  43. package/src/core/tools/exa/mcp-client.ts +56 -101
  44. package/src/core/tools/find.ts +250 -253
  45. package/src/core/tools/git.ts +39 -33
  46. package/src/core/tools/grep.ts +440 -427
  47. package/src/core/tools/index.ts +62 -61
  48. package/src/core/tools/ls.ts +119 -114
  49. package/src/core/tools/lsp/clients/biome-client.ts +5 -7
  50. package/src/core/tools/lsp/clients/index.ts +4 -4
  51. package/src/core/tools/lsp/clients/lsp-linter-client.ts +5 -7
  52. package/src/core/tools/lsp/config.ts +2 -2
  53. package/src/core/tools/lsp/index.ts +604 -578
  54. package/src/core/tools/notebook.ts +121 -119
  55. package/src/core/tools/output.ts +163 -147
  56. package/src/core/tools/patch/applicator.ts +1100 -0
  57. package/src/core/tools/patch/diff.ts +362 -0
  58. package/src/core/tools/patch/fuzzy.ts +647 -0
  59. package/src/core/tools/patch/index.ts +430 -0
  60. package/src/core/tools/patch/normalize.ts +220 -0
  61. package/src/core/tools/patch/normative.ts +49 -0
  62. package/src/core/tools/patch/parser.ts +528 -0
  63. package/src/core/tools/patch/shared.ts +228 -0
  64. package/src/core/tools/patch/types.ts +244 -0
  65. package/src/core/tools/python.ts +139 -136
  66. package/src/core/tools/read.ts +237 -216
  67. package/src/core/tools/render-utils.ts +196 -77
  68. package/src/core/tools/renderers.ts +1 -1
  69. package/src/core/tools/ssh.ts +99 -80
  70. package/src/core/tools/task/executor.ts +11 -7
  71. package/src/core/tools/task/index.ts +352 -343
  72. package/src/core/tools/task/worker.ts +13 -23
  73. package/src/core/tools/todo-write.ts +74 -59
  74. package/src/core/tools/web-fetch.ts +54 -47
  75. package/src/core/tools/web-search/index.ts +27 -16
  76. package/src/core/tools/write.ts +73 -44
  77. package/src/core/ttsr.ts +106 -152
  78. package/src/core/voice.ts +49 -39
  79. package/src/index.ts +16 -12
  80. package/src/lib/worktree/index.ts +1 -9
  81. package/src/modes/interactive/components/diff.ts +15 -8
  82. package/src/modes/interactive/components/settings-defs.ts +24 -0
  83. package/src/modes/interactive/components/tool-execution.ts +34 -6
  84. package/src/modes/interactive/controllers/event-controller.ts +6 -19
  85. package/src/modes/interactive/controllers/input-controller.ts +1 -1
  86. package/src/modes/interactive/utils/ui-helpers.ts +5 -1
  87. package/src/modes/rpc/rpc-mode.ts +99 -81
  88. package/src/prompts/tools/patch.md +76 -0
  89. package/src/prompts/tools/read.md +1 -1
  90. package/src/prompts/tools/{edit.md → replace.md} +1 -0
  91. package/src/utils/shell.ts +0 -40
  92. package/src/core/tools/edit-diff.ts +0 -574
  93. package/src/core/tools/edit.ts +0 -345
@@ -1,6 +1,6 @@
1
1
  import { homedir } from "node:os";
2
2
  import path from "node:path";
3
- import type { AgentTool } from "@oh-my-pi/pi-agent-core";
3
+ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
4
4
  import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
5
5
  import type { Component } from "@oh-my-pi/pi-tui";
6
6
  import { Text } from "@oh-my-pi/pi-tui";
@@ -15,7 +15,7 @@ import type { RenderResultOptions } from "../custom-tools/types";
15
15
  import { renderPromptTemplate } from "../prompt-templates";
16
16
  import type { ToolSession } from "../sdk";
17
17
  import { ScopeSignal, untilAborted } from "../utils";
18
- import { createLsTool } from "./ls";
18
+ import { LsTool } from "./ls";
19
19
  import { resolveReadPath, resolveToCwd } from "./path-utils";
20
20
  import { shortenPath, wrapBrackets } from "./render-utils";
21
21
  import {
@@ -417,7 +417,7 @@ const readSchema = Type.Object({
417
417
  path: Type.String({ description: "Path to the file to read (relative or absolute)" }),
418
418
  offset: Type.Optional(Type.Number({ description: "Line number to start reading from (1-indexed)" })),
419
419
  limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" })),
420
- lines: Type.Optional(Type.Boolean({ description: "Prepend line numbers to output (default: true)" })),
420
+ lines: Type.Optional(Type.Boolean({ description: "Prepend line numbers to output (default: false)" })),
421
421
  });
422
422
 
423
423
  export interface ReadToolDetails {
@@ -425,74 +425,109 @@ export interface ReadToolDetails {
425
425
  redirectedTo?: "ls";
426
426
  }
427
427
 
428
- export function createReadTool(session: ToolSession): AgentTool<typeof readSchema> {
429
- const autoResizeImages = session.settings?.getImageAutoResize() ?? true;
430
- const lsTool = createLsTool(session);
431
- return {
432
- name: "read",
433
- label: "Read",
434
- description: renderPromptTemplate(readDescription, {
428
+ type ReadParams = { path: string; offset?: number; limit?: number; lines?: boolean };
429
+
430
+ /**
431
+ * Read tool implementation.
432
+ *
433
+ * Reads files with support for images, documents (via markitdown), and text.
434
+ * Directories redirect to the ls tool.
435
+ */
436
+ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
437
+ public readonly name = "read";
438
+ public readonly label = "Read";
439
+ public readonly description: string;
440
+ public readonly parameters = readSchema;
441
+
442
+ private readonly session: ToolSession;
443
+ private readonly autoResizeImages: boolean;
444
+ private readonly lsTool: LsTool;
445
+
446
+ constructor(session: ToolSession) {
447
+ this.session = session;
448
+ this.autoResizeImages = session.settings?.getImageAutoResize() ?? true;
449
+ this.lsTool = new LsTool(session);
450
+ this.description = renderPromptTemplate(readDescription, {
435
451
  DEFAULT_MAX_LINES: String(DEFAULT_MAX_LINES),
436
- }),
437
- parameters: readSchema,
438
- execute: async (
439
- toolCallId: string,
440
- { path: readPath, offset, limit, lines }: { path: string; offset?: number; limit?: number; lines?: boolean },
441
- signal?: AbortSignal,
442
- ) => {
443
- const absolutePath = resolveReadPath(readPath, session.cwd);
444
-
445
- return untilAborted(signal, async () => {
446
- let isDirectory = false;
447
- let fileSize = 0;
448
- try {
449
- const stat = await Bun.file(absolutePath).stat();
450
- fileSize = stat.size;
451
- isDirectory = stat.isDirectory();
452
- } catch (error) {
453
- if (isNotFoundError(error)) {
454
- let message = `File not found: ${readPath}`;
455
-
456
- // Skip fuzzy matching for remote mounts (sshfs) to avoid hangs
457
- if (!isRemoteMountPath(absolutePath)) {
458
- const suggestions = await findReadPathSuggestions(readPath, session.cwd, signal);
459
-
460
- if (suggestions?.suggestions.length) {
461
- const scopeLabel = suggestions.scopeLabel ? ` in ${suggestions.scopeLabel}` : "";
462
- message += `\n\nClosest matches${scopeLabel}:\n${suggestions.suggestions.map((match) => `- ${match}`).join("\n")}`;
463
- if (suggestions.truncated) {
464
- message += `\n[Search truncated to first ${MAX_FUZZY_CANDIDATES} paths. Refine the path if the match isn't listed.]`;
465
- }
466
- } else if (suggestions?.error) {
467
- message += `\n\nFuzzy match failed: ${suggestions.error}`;
468
- } else if (suggestions?.scopeLabel) {
469
- message += `\n\nNo similar paths found in ${suggestions.scopeLabel}.`;
452
+ });
453
+ }
454
+
455
+ public async execute(
456
+ toolCallId: string,
457
+ params: ReadParams,
458
+ signal?: AbortSignal,
459
+ _onUpdate?: AgentToolUpdateCallback<ReadToolDetails>,
460
+ _context?: AgentToolContext,
461
+ ): Promise<AgentToolResult<ReadToolDetails>> {
462
+ const { path: readPath, offset, limit, lines } = params;
463
+ const absolutePath = resolveReadPath(readPath, this.session.cwd);
464
+
465
+ return untilAborted(signal, async () => {
466
+ let isDirectory = false;
467
+ let fileSize = 0;
468
+ try {
469
+ const stat = await Bun.file(absolutePath).stat();
470
+ fileSize = stat.size;
471
+ isDirectory = stat.isDirectory();
472
+ } catch (error) {
473
+ if (isNotFoundError(error)) {
474
+ let message = `File not found: ${readPath}`;
475
+
476
+ // Skip fuzzy matching for remote mounts (sshfs) to avoid hangs
477
+ if (!isRemoteMountPath(absolutePath)) {
478
+ const suggestions = await findReadPathSuggestions(readPath, this.session.cwd, signal);
479
+
480
+ if (suggestions?.suggestions.length) {
481
+ const scopeLabel = suggestions.scopeLabel ? ` in ${suggestions.scopeLabel}` : "";
482
+ message += `\n\nClosest matches${scopeLabel}:\n${suggestions.suggestions.map((match) => `- ${match}`).join("\n")}`;
483
+ if (suggestions.truncated) {
484
+ message += `\n[Search truncated to first ${MAX_FUZZY_CANDIDATES} paths. Refine the path if the match isn't listed.]`;
470
485
  }
486
+ } else if (suggestions?.error) {
487
+ message += `\n\nFuzzy match failed: ${suggestions.error}`;
488
+ } else if (suggestions?.scopeLabel) {
489
+ message += `\n\nNo similar paths found in ${suggestions.scopeLabel}.`;
471
490
  }
472
-
473
- throw new Error(message);
474
491
  }
475
- throw error;
476
- }
477
492
 
478
- if (isDirectory) {
479
- const lsResult = await lsTool.execute(toolCallId, { path: readPath, limit }, signal);
480
- return {
481
- content: lsResult.content,
482
- details: { redirectedTo: "ls", truncation: lsResult.details?.truncation },
483
- };
493
+ throw new Error(message);
484
494
  }
495
+ throw error;
496
+ }
485
497
 
486
- const mimeType = await detectSupportedImageMimeTypeFromFile(absolutePath);
487
- const ext = path.extname(absolutePath).toLowerCase();
498
+ if (isDirectory) {
499
+ const lsResult = await this.lsTool.execute(toolCallId, { path: readPath, limit }, signal);
500
+ return {
501
+ content: lsResult.content,
502
+ details: { redirectedTo: "ls", truncation: lsResult.details?.truncation },
503
+ };
504
+ }
488
505
 
489
- // Read the file based on type
490
- let content: (TextContent | ImageContent)[];
491
- let details: ReadToolDetails | undefined;
506
+ const mimeType = await detectSupportedImageMimeTypeFromFile(absolutePath);
507
+ const ext = path.extname(absolutePath).toLowerCase();
508
+
509
+ // Read the file based on type
510
+ let content: (TextContent | ImageContent)[];
511
+ let details: ReadToolDetails | undefined;
512
+
513
+ if (mimeType) {
514
+ if (fileSize > MAX_IMAGE_SIZE) {
515
+ const sizeStr = formatSize(fileSize);
516
+ const maxStr = formatSize(MAX_IMAGE_SIZE);
517
+ content = [
518
+ {
519
+ type: "text",
520
+ text: `[Image file too large: ${sizeStr} exceeds ${maxStr} limit. Use an image viewer or resize the image.]`,
521
+ },
522
+ ];
523
+ } else {
524
+ // Read as image (binary)
525
+ const file = Bun.file(absolutePath);
526
+ const buffer = await file.arrayBuffer();
492
527
 
493
- if (mimeType) {
494
- if (fileSize > MAX_IMAGE_SIZE) {
495
- const sizeStr = formatSize(fileSize);
528
+ // Check actual buffer size after reading to prevent OOM during serialization
529
+ if (buffer.byteLength > MAX_IMAGE_SIZE) {
530
+ const sizeStr = formatSize(buffer.byteLength);
496
531
  const maxStr = formatSize(MAX_IMAGE_SIZE);
497
532
  content = [
498
533
  {
@@ -501,178 +536,164 @@ export function createReadTool(session: ToolSession): AgentTool<typeof readSchem
501
536
  },
502
537
  ];
503
538
  } else {
504
- // Read as image (binary)
505
- const file = Bun.file(absolutePath);
506
- const buffer = await file.arrayBuffer();
507
-
508
- // Check actual buffer size after reading to prevent OOM during serialization
509
- if (buffer.byteLength > MAX_IMAGE_SIZE) {
510
- const sizeStr = formatSize(buffer.byteLength);
511
- const maxStr = formatSize(MAX_IMAGE_SIZE);
512
- content = [
513
- {
514
- type: "text",
515
- text: `[Image file too large: ${sizeStr} exceeds ${maxStr} limit. Use an image viewer or resize the image.]`,
516
- },
517
- ];
518
- } else {
519
- const base64 = Buffer.from(buffer).toString("base64");
520
-
521
- if (autoResizeImages) {
522
- // Resize image if needed - catch errors from WASM
523
- try {
524
- const resized = await resizeImage({ type: "image", data: base64, mimeType });
525
- const dimensionNote = formatDimensionNote(resized);
526
-
527
- let textNote = `Read image file [${resized.mimeType}]`;
528
- if (dimensionNote) {
529
- textNote += `\n${dimensionNote}`;
530
- }
531
-
532
- content = [
533
- { type: "text", text: textNote },
534
- { type: "image", data: resized.data, mimeType: resized.mimeType },
535
- ];
536
- } catch {
537
- // Fall back to original image on resize failure
538
- content = [
539
- { type: "text", text: `Read image file [${mimeType}]` },
540
- { type: "image", data: base64, mimeType },
541
- ];
539
+ const base64 = Buffer.from(buffer).toString("base64");
540
+
541
+ if (this.autoResizeImages) {
542
+ // Resize image if needed - catch errors from WASM
543
+ try {
544
+ const resized = await resizeImage({ type: "image", data: base64, mimeType });
545
+ const dimensionNote = formatDimensionNote(resized);
546
+
547
+ let textNote = `Read image file [${resized.mimeType}]`;
548
+ if (dimensionNote) {
549
+ textNote += `\n${dimensionNote}`;
542
550
  }
543
- } else {
551
+
552
+ content = [
553
+ { type: "text", text: textNote },
554
+ { type: "image", data: resized.data, mimeType: resized.mimeType },
555
+ ];
556
+ } catch {
557
+ // Fall back to original image on resize failure
544
558
  content = [
545
559
  { type: "text", text: `Read image file [${mimeType}]` },
546
560
  { type: "image", data: base64, mimeType },
547
561
  ];
548
562
  }
563
+ } else {
564
+ content = [
565
+ { type: "text", text: `Read image file [${mimeType}]` },
566
+ { type: "image", data: base64, mimeType },
567
+ ];
549
568
  }
550
569
  }
551
- } else if (CONVERTIBLE_EXTENSIONS.has(ext)) {
552
- // Convert document via markitdown
553
- const result = await convertWithMarkitdown(absolutePath, signal);
554
- if (result.ok) {
555
- // Apply truncation to converted content
556
- const truncation = truncateHead(result.content);
557
- let outputText = truncation.content;
558
-
559
- if (truncation.truncated) {
560
- outputText += `\n\n[Document converted via markitdown. Output truncated to ${formatSize(DEFAULT_MAX_BYTES)}]`;
561
- details = { truncation };
562
- }
563
-
564
- content = [{ type: "text", text: outputText }];
565
- } else {
566
- // markitdown not available or failed
567
- const errorMsg =
568
- result.error === "markitdown not found"
569
- ? `markitdown not installed. Install with: pip install markitdown`
570
- : result.error || "conversion failed";
571
- content = [{ type: "text", text: `[Cannot read ${ext} file: ${errorMsg}]` }];
570
+ }
571
+ } else if (CONVERTIBLE_EXTENSIONS.has(ext)) {
572
+ // Convert document via markitdown
573
+ const result = await convertWithMarkitdown(absolutePath, signal);
574
+ if (result.ok) {
575
+ // Apply truncation to converted content
576
+ const truncation = truncateHead(result.content);
577
+ let outputText = truncation.content;
578
+
579
+ if (truncation.truncated) {
580
+ outputText += `\n\n[Document converted via markitdown. Output truncated to ${formatSize(DEFAULT_MAX_BYTES)}]`;
581
+ details = { truncation };
572
582
  }
573
- } else {
574
- // Read as text
575
- const file = Bun.file(absolutePath);
576
- const textContent = await file.text();
577
- const allLines = textContent.split("\n");
578
- const totalFileLines = allLines.length;
579
583
 
580
- // Apply offset if specified (1-indexed to 0-indexed)
581
- const startLine = offset ? Math.max(0, offset - 1) : 0;
582
- const startLineDisplay = startLine + 1; // For display (1-indexed)
584
+ content = [{ type: "text", text: outputText }];
585
+ } else if (result.error) {
586
+ // markitdown not available or failed
587
+ const errorMsg =
588
+ result.error === "markitdown not found"
589
+ ? `markitdown not installed. Install with: pip install markitdown`
590
+ : result.error || "conversion failed";
591
+ content = [{ type: "text", text: `[Cannot read ${ext} file: ${errorMsg}]` }];
592
+ } else {
593
+ content = [{ type: "text", text: `[Cannot read ${ext} file: conversion failed]` }];
594
+ }
595
+ } else {
596
+ // Read as text
597
+ const file = Bun.file(absolutePath);
598
+ const textContent = await file.text();
599
+ const allLines = textContent.split("\n");
600
+ const totalFileLines = allLines.length;
601
+
602
+ // Apply offset if specified (1-indexed to 0-indexed)
603
+ const startLine = offset ? Math.max(0, offset - 1) : 0;
604
+ const startLineDisplay = startLine + 1; // For display (1-indexed)
605
+
606
+ // Check if offset is out of bounds
607
+ if (startLine >= allLines.length) {
608
+ throw new Error(`Offset ${offset} is beyond end of file (${allLines.length} lines total)`);
609
+ }
583
610
 
584
- // Check if offset is out of bounds
585
- if (startLine >= allLines.length) {
586
- throw new Error(`Offset ${offset} is beyond end of file (${allLines.length} lines total)`);
587
- }
611
+ // If limit is specified by user, use it; otherwise we'll let truncateHead decide
612
+ let selectedContent: string;
613
+ let userLimitedLines: number | undefined;
614
+ if (limit !== undefined) {
615
+ const endLine = Math.min(startLine + limit, allLines.length);
616
+ selectedContent = allLines.slice(startLine, endLine).join("\n");
617
+ userLimitedLines = endLine - startLine;
618
+ } else {
619
+ selectedContent = allLines.slice(startLine).join("\n");
620
+ }
588
621
 
589
- // If limit is specified by user, use it; otherwise we'll let truncateHead decide
590
- let selectedContent: string;
591
- let userLimitedLines: number | undefined;
592
- if (limit !== undefined) {
593
- const endLine = Math.min(startLine + limit, allLines.length);
594
- selectedContent = allLines.slice(startLine, endLine).join("\n");
595
- userLimitedLines = endLine - startLine;
622
+ // Apply truncation (respects both line and byte limits)
623
+ const truncation = truncateHead(selectedContent);
624
+
625
+ // Add line numbers if requested (default: false)
626
+ const shouldAddLineNumbers = lines === true;
627
+ const prependLineNumbers = (text: string, startNum: number): string => {
628
+ const textLines = text.split("\n");
629
+ const lastLineNum = startNum + textLines.length - 1;
630
+ const padWidth = String(lastLineNum).length;
631
+ return textLines
632
+ .map((line, i) => {
633
+ const lineNum = String(startNum + i).padStart(padWidth, " ");
634
+ return `${lineNum}\t${line}`;
635
+ })
636
+ .join("\n");
637
+ };
638
+
639
+ let outputText: string;
640
+
641
+ if (truncation.firstLineExceedsLimit) {
642
+ const firstLine = allLines[startLine] ?? "";
643
+ const firstLineBytes = Buffer.byteLength(firstLine, "utf-8");
644
+ const snippet = truncateStringToBytesFromStart(firstLine, DEFAULT_MAX_BYTES);
645
+ const shownSize = formatSize(snippet.bytes);
646
+
647
+ outputText = shouldAddLineNumbers ? prependLineNumbers(snippet.text, startLineDisplay) : snippet.text;
648
+ if (snippet.text.length > 0) {
649
+ outputText += `\n\n[Line ${startLineDisplay} is ${formatSize(
650
+ firstLineBytes,
651
+ )}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Showing first ${shownSize} of the line.]`;
596
652
  } else {
597
- selectedContent = allLines.slice(startLine).join("\n");
653
+ outputText = `[Line ${startLineDisplay} is ${formatSize(
654
+ firstLineBytes,
655
+ )}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Unable to display a valid UTF-8 snippet.]`;
598
656
  }
599
-
600
- // Apply truncation (respects both line and byte limits)
601
- const truncation = truncateHead(selectedContent);
602
-
603
- // Add line numbers if requested (default: true)
604
- const shouldAddLineNumbers = lines !== false;
605
- const prependLineNumbers = (text: string, startNum: number): string => {
606
- const lines = text.split("\n");
607
- const lastLineNum = startNum + lines.length - 1;
608
- const padWidth = String(lastLineNum).length;
609
- return lines
610
- .map((line, i) => {
611
- const lineNum = String(startNum + i).padStart(padWidth, " ");
612
- return `${lineNum}\t${line}`;
613
- })
614
- .join("\n");
615
- };
616
-
617
- let outputText: string;
618
-
619
- if (truncation.firstLineExceedsLimit) {
620
- const firstLine = allLines[startLine] ?? "";
621
- const firstLineBytes = Buffer.byteLength(firstLine, "utf-8");
622
- const snippet = truncateStringToBytesFromStart(firstLine, DEFAULT_MAX_BYTES);
623
- const shownSize = formatSize(snippet.bytes);
624
-
625
- outputText = shouldAddLineNumbers ? prependLineNumbers(snippet.text, startLineDisplay) : snippet.text;
626
- if (snippet.text.length > 0) {
627
- outputText += `\n\n[Line ${startLineDisplay} is ${formatSize(
628
- firstLineBytes,
629
- )}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Showing first ${shownSize} of the line.]`;
630
- } else {
631
- outputText = `[Line ${startLineDisplay} is ${formatSize(
632
- firstLineBytes,
633
- )}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Unable to display a valid UTF-8 snippet.]`;
634
- }
635
- details = { truncation };
636
- } else if (truncation.truncated) {
637
- // Truncation occurred - build actionable notice
638
- const endLineDisplay = startLineDisplay + truncation.outputLines - 1;
639
- const nextOffset = endLineDisplay + 1;
640
-
641
- outputText = shouldAddLineNumbers
642
- ? prependLineNumbers(truncation.content, startLineDisplay)
643
- : truncation.content;
644
-
645
- if (truncation.truncatedBy === "lines") {
646
- outputText += `\n\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue]`;
647
- } else {
648
- outputText += `\n\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(
649
- DEFAULT_MAX_BYTES,
650
- )} limit). Use offset=${nextOffset} to continue]`;
651
- }
652
- details = { truncation };
653
- } else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) {
654
- // User specified limit, there's more content, but no truncation
655
- const remaining = allLines.length - (startLine + userLimitedLines);
656
- const nextOffset = startLine + userLimitedLines + 1;
657
-
658
- outputText = shouldAddLineNumbers
659
- ? prependLineNumbers(truncation.content, startLineDisplay)
660
- : truncation.content;
661
- outputText += `\n\n[${remaining} more lines in file. Use offset=${nextOffset} to continue]`;
657
+ details = { truncation };
658
+ } else if (truncation.truncated) {
659
+ // Truncation occurred - build actionable notice
660
+ const endLineDisplay = startLineDisplay + truncation.outputLines - 1;
661
+ const nextOffset = endLineDisplay + 1;
662
+
663
+ outputText = shouldAddLineNumbers
664
+ ? prependLineNumbers(truncation.content, startLineDisplay)
665
+ : truncation.content;
666
+
667
+ if (truncation.truncatedBy === "lines") {
668
+ outputText += `\n\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue]`;
662
669
  } else {
663
- // No truncation, no user limit exceeded
664
- outputText = shouldAddLineNumbers
665
- ? prependLineNumbers(truncation.content, startLineDisplay)
666
- : truncation.content;
670
+ outputText += `\n\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(
671
+ DEFAULT_MAX_BYTES,
672
+ )} limit). Use offset=${nextOffset} to continue]`;
667
673
  }
668
-
669
- content = [{ type: "text", text: outputText }];
674
+ details = { truncation };
675
+ } else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) {
676
+ // User specified limit, there's more content, but no truncation
677
+ const remaining = allLines.length - (startLine + userLimitedLines);
678
+ const nextOffset = startLine + userLimitedLines + 1;
679
+
680
+ outputText = shouldAddLineNumbers
681
+ ? prependLineNumbers(truncation.content, startLineDisplay)
682
+ : truncation.content;
683
+ outputText += `\n\n[${remaining} more lines in file. Use offset=${nextOffset} to continue]`;
684
+ } else {
685
+ // No truncation, no user limit exceeded
686
+ outputText = shouldAddLineNumbers
687
+ ? prependLineNumbers(truncation.content, startLineDisplay)
688
+ : truncation.content;
670
689
  }
671
690
 
672
- return { content, details };
673
- });
674
- },
675
- };
691
+ content = [{ type: "text", text: outputText }];
692
+ }
693
+
694
+ return { content, details };
695
+ });
696
+ }
676
697
  }
677
698
 
678
699
  // =============================================================================