@oh-my-pi/pi-coding-agent 13.7.6 → 13.8.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.
- package/CHANGELOG.md +15 -0
- package/package.json +7 -7
- package/scripts/generate-docs-index.ts +3 -3
- package/src/capability/context-file.ts +6 -3
- package/src/capability/fs.ts +18 -0
- package/src/capability/index.ts +3 -2
- package/src/capability/types.ts +2 -0
- package/src/config/model-resolver.ts +14 -2
- package/src/discovery/agents-md.ts +3 -4
- package/src/discovery/agents.ts +104 -84
- package/src/discovery/builtin.ts +28 -15
- package/src/discovery/claude.ts +27 -9
- package/src/extensibility/skills.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/main.ts +4 -0
- package/src/patch/hashline.ts +113 -0
- package/src/patch/index.ts +13 -2
- package/src/prompts/tools/hashline.md +9 -10
- package/src/sdk.ts +4 -0
- package/src/session/agent-session.ts +17 -29
- package/src/tools/fetch.ts +152 -4
package/src/tools/fetch.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
3
|
+
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
|
3
4
|
import { htmlToMarkdown } from "@oh-my-pi/pi-natives";
|
|
4
5
|
import { type Component, Text } from "@oh-my-pi/pi-tui";
|
|
5
6
|
import { ptree, truncate } from "@oh-my-pi/pi-utils";
|
|
@@ -12,6 +13,7 @@ import fetchDescription from "../prompts/tools/fetch.md" with { type: "text" };
|
|
|
12
13
|
import { DEFAULT_MAX_BYTES, truncateHead } from "../session/streaming-output";
|
|
13
14
|
import { renderStatusLine } from "../tui";
|
|
14
15
|
import { CachedOutputBlock } from "../tui/output-block";
|
|
16
|
+
import { formatDimensionNote, resizeImage } from "../utils/image-resize";
|
|
15
17
|
import { ensureTool } from "../utils/tools-manager";
|
|
16
18
|
import { summarizeUrlWithKagi } from "../web/kagi";
|
|
17
19
|
import { specialHandlers } from "../web/scrapers";
|
|
@@ -72,6 +74,10 @@ const CONVERTIBLE_EXTENSIONS = new Set([
|
|
|
72
74
|
".ogg",
|
|
73
75
|
]);
|
|
74
76
|
|
|
77
|
+
const SUPPORTED_INLINE_IMAGE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
|
|
78
|
+
const MAX_INLINE_IMAGE_SOURCE_BYTES = 20 * 1024 * 1024;
|
|
79
|
+
const MAX_INLINE_IMAGE_OUTPUT_BYTES = 0.75 * 1024 * 1024;
|
|
80
|
+
|
|
75
81
|
// =============================================================================
|
|
76
82
|
// Utilities
|
|
77
83
|
// =============================================================================
|
|
@@ -145,6 +151,14 @@ function isConvertible(mime: string, extensionHint: string): boolean {
|
|
|
145
151
|
return false;
|
|
146
152
|
}
|
|
147
153
|
|
|
154
|
+
function resolveImageMimeType(mime: string): string | null {
|
|
155
|
+
return mime.startsWith("image/") ? mime : null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function isInlineImageMimeTypeSupported(mimeType: string): boolean {
|
|
159
|
+
return SUPPORTED_INLINE_IMAGE_MIME_TYPES.has(mimeType);
|
|
160
|
+
}
|
|
161
|
+
|
|
148
162
|
/**
|
|
149
163
|
* Check if content looks like HTML
|
|
150
164
|
*/
|
|
@@ -542,6 +556,15 @@ function formatJson(content: string): string {
|
|
|
542
556
|
}
|
|
543
557
|
}
|
|
544
558
|
|
|
559
|
+
interface FetchImagePayload {
|
|
560
|
+
data: string;
|
|
561
|
+
mimeType: string;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
type FetchRenderResult = RenderResult & {
|
|
565
|
+
image?: FetchImagePayload;
|
|
566
|
+
};
|
|
567
|
+
|
|
545
568
|
// =============================================================================
|
|
546
569
|
// Unified Special Handler Dispatch
|
|
547
570
|
// =============================================================================
|
|
@@ -549,7 +572,11 @@ function formatJson(content: string): string {
|
|
|
549
572
|
/**
|
|
550
573
|
* Try all special handlers
|
|
551
574
|
*/
|
|
552
|
-
async function handleSpecialUrls(
|
|
575
|
+
async function handleSpecialUrls(
|
|
576
|
+
url: string,
|
|
577
|
+
timeout: number,
|
|
578
|
+
signal?: AbortSignal,
|
|
579
|
+
): Promise<FetchRenderResult | null> {
|
|
553
580
|
for (const handler of specialHandlers) {
|
|
554
581
|
if (signal?.aborted) {
|
|
555
582
|
throw new ToolAbortError();
|
|
@@ -573,7 +600,7 @@ async function renderUrl(
|
|
|
573
600
|
raw: boolean,
|
|
574
601
|
useKagiSummarizer: boolean,
|
|
575
602
|
signal?: AbortSignal,
|
|
576
|
-
): Promise<
|
|
603
|
+
): Promise<FetchRenderResult> {
|
|
577
604
|
const notes: string[] = [];
|
|
578
605
|
const fetchedAt = new Date().toISOString();
|
|
579
606
|
if (signal?.aborted) {
|
|
@@ -626,8 +653,124 @@ async function renderUrl(
|
|
|
626
653
|
const mime = normalizeMime(response.contentType);
|
|
627
654
|
const extHint = getExtensionHint(finalUrl);
|
|
628
655
|
|
|
656
|
+
const imageMimeType = resolveImageMimeType(mime);
|
|
657
|
+
const canInlineImage = Boolean(imageMimeType && isInlineImageMimeTypeSupported(imageMimeType));
|
|
658
|
+
let skipConvertibleBinaryRetry = false;
|
|
659
|
+
if (imageMimeType && !canInlineImage) {
|
|
660
|
+
notes.push(
|
|
661
|
+
`Image MIME type ${imageMimeType} is unsupported for inline model serialization; falling back to textual rendering`,
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
if (canInlineImage && imageMimeType) {
|
|
665
|
+
const binary = await fetchBinary(finalUrl, timeout, signal);
|
|
666
|
+
if (binary.ok) {
|
|
667
|
+
notes.push("Fetched image binary");
|
|
668
|
+
const conversionExtension = getExtensionHint(finalUrl, binary.contentDisposition) || extHint;
|
|
669
|
+
let convertedText: string | null = null;
|
|
670
|
+
const converted = await convertWithMarkitdown(binary.buffer, conversionExtension, timeout, signal);
|
|
671
|
+
if (converted.ok) {
|
|
672
|
+
if (converted.content.trim().length > 50) {
|
|
673
|
+
notes.push("Converted with markitdown");
|
|
674
|
+
convertedText = converted.content;
|
|
675
|
+
} else {
|
|
676
|
+
notes.push("markitdown conversion produced no usable output");
|
|
677
|
+
}
|
|
678
|
+
} else if (converted.error) {
|
|
679
|
+
notes.push(`markitdown conversion failed: ${converted.error}`);
|
|
680
|
+
} else {
|
|
681
|
+
notes.push("markitdown conversion failed");
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (binary.buffer.byteLength > MAX_INLINE_IMAGE_SOURCE_BYTES) {
|
|
685
|
+
notes.push(
|
|
686
|
+
`Image exceeds inline source limit (${binary.buffer.byteLength} bytes > ${MAX_INLINE_IMAGE_SOURCE_BYTES} bytes)`,
|
|
687
|
+
);
|
|
688
|
+
const output = finalizeOutput(
|
|
689
|
+
convertedText ?? `Fetched image content (${imageMimeType}), but it is too large to inline render.`,
|
|
690
|
+
);
|
|
691
|
+
return {
|
|
692
|
+
url,
|
|
693
|
+
finalUrl,
|
|
694
|
+
contentType: imageMimeType,
|
|
695
|
+
method: convertedText ? "markitdown" : "image-too-large",
|
|
696
|
+
content: output.content,
|
|
697
|
+
fetchedAt,
|
|
698
|
+
truncated: output.truncated,
|
|
699
|
+
notes,
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const resized = await resizeImage(
|
|
704
|
+
{ type: "image", data: binary.buffer.toBase64(), mimeType: imageMimeType },
|
|
705
|
+
{ maxBytes: MAX_INLINE_IMAGE_OUTPUT_BYTES },
|
|
706
|
+
);
|
|
707
|
+
const isDecodedImage =
|
|
708
|
+
resized.originalWidth > 0 && resized.originalHeight > 0 && resized.width > 0 && resized.height > 0;
|
|
709
|
+
if (!isDecodedImage) {
|
|
710
|
+
notes.push(`Fetched payload could not be decoded as ${imageMimeType}; returning text metadata only`);
|
|
711
|
+
const output = finalizeOutput(
|
|
712
|
+
convertedText ??
|
|
713
|
+
rawContent ??
|
|
714
|
+
`Fetched payload was labeled ${imageMimeType}, but bytes were not a valid image.`,
|
|
715
|
+
);
|
|
716
|
+
return {
|
|
717
|
+
url,
|
|
718
|
+
finalUrl,
|
|
719
|
+
contentType: imageMimeType,
|
|
720
|
+
method: convertedText ? "markitdown" : "image-invalid",
|
|
721
|
+
content: output.content,
|
|
722
|
+
fetchedAt,
|
|
723
|
+
truncated: output.truncated,
|
|
724
|
+
notes,
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
if (resized.buffer.length > MAX_INLINE_IMAGE_OUTPUT_BYTES) {
|
|
728
|
+
notes.push(
|
|
729
|
+
`Image exceeds inline output limit after resize (${resized.buffer.length} bytes > ${MAX_INLINE_IMAGE_OUTPUT_BYTES} bytes)`,
|
|
730
|
+
);
|
|
731
|
+
const output = finalizeOutput(
|
|
732
|
+
convertedText ?? `Fetched image content (${imageMimeType}), but it is too large to inline render.`,
|
|
733
|
+
);
|
|
734
|
+
return {
|
|
735
|
+
url,
|
|
736
|
+
finalUrl,
|
|
737
|
+
contentType: imageMimeType,
|
|
738
|
+
method: convertedText ? "markitdown" : "image-too-large",
|
|
739
|
+
content: output.content,
|
|
740
|
+
fetchedAt,
|
|
741
|
+
truncated: output.truncated,
|
|
742
|
+
notes,
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const dimensionNote = formatDimensionNote(resized);
|
|
747
|
+
let imageSummary = convertedText ?? `Fetched image content (${resized.mimeType}).`;
|
|
748
|
+
if (dimensionNote) {
|
|
749
|
+
imageSummary += `\n${dimensionNote}`;
|
|
750
|
+
}
|
|
751
|
+
const output = finalizeOutput(imageSummary);
|
|
752
|
+
return {
|
|
753
|
+
url,
|
|
754
|
+
finalUrl,
|
|
755
|
+
contentType: resized.mimeType,
|
|
756
|
+
method: "image",
|
|
757
|
+
content: output.content,
|
|
758
|
+
fetchedAt,
|
|
759
|
+
truncated: output.truncated,
|
|
760
|
+
notes,
|
|
761
|
+
image: {
|
|
762
|
+
data: resized.data,
|
|
763
|
+
mimeType: resized.mimeType,
|
|
764
|
+
},
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
notes.push(binary.error ? `Binary fetch failed: ${binary.error}` : "Binary fetch failed");
|
|
768
|
+
notes.push("Falling back to textual rendering from initial response");
|
|
769
|
+
skipConvertibleBinaryRetry = true;
|
|
770
|
+
}
|
|
771
|
+
|
|
629
772
|
// Step 3: Handle convertible binary files (PDF, DOCX, etc.)
|
|
630
|
-
if (isConvertible(mime, extHint)) {
|
|
773
|
+
if (!skipConvertibleBinaryRetry && isConvertible(mime, extHint)) {
|
|
631
774
|
const binary = await fetchBinary(finalUrl, timeout, signal);
|
|
632
775
|
if (binary.ok) {
|
|
633
776
|
const ext = getExtensionHint(finalUrl, binary.contentDisposition) || extHint;
|
|
@@ -976,7 +1119,12 @@ export class FetchTool implements AgentTool<typeof fetchSchema, FetchToolDetails
|
|
|
976
1119
|
notes: result.notes,
|
|
977
1120
|
};
|
|
978
1121
|
|
|
979
|
-
const
|
|
1122
|
+
const contentBlocks: Array<TextContent | ImageContent> = [{ type: "text", text: output }];
|
|
1123
|
+
if (result.image) {
|
|
1124
|
+
contentBlocks.push({ type: "image", data: result.image.data, mimeType: result.image.mimeType });
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
const resultBuilder = toolResult(details).content(contentBlocks).sourceUrl(result.finalUrl);
|
|
980
1128
|
if (needsArtifact) {
|
|
981
1129
|
resultBuilder.truncation(truncation, { direction: "head", artifactId });
|
|
982
1130
|
} else if (result.truncated) {
|