From 22a85240584f5fa421a01958bb96b2b116e5b2fb Mon Sep 17 00:00:00 2001 From: roboomp Date: Fri, 17 Jul 2026 08:04:10 +0000 Subject: [PATCH] fix(tools): recognized zip-family archives and pruned unconvertible extensions - Treated ZIP-based .jar/.war/.ear/.apk as zip archives in archiveFormatFromPath and parseArchivePathCandidates so read/write member access works. - Shared one archive-extension alternation between format detection and path splitting to stop them drifting. - Derived the markit convertible-extension set from a single source of truth (utils/markit) matching the registered converters (pdf/docx/pptx/xlsx/epub), dropping legacy .doc/.ppt/.xls/.rtf that had no converter and only produced Unsupported format errors. - Updated read/write tool prompts to document the zip-family extensions. Fixes #5808 --- packages/coding-agent/CHANGELOG.md | 5 ++++ .../coding-agent/src/cli/file-processor.ts | 3 +- .../coding-agent/src/prompts/tools/read.md | 2 +- .../coding-agent/src/prompts/tools/write.md | 2 +- packages/coding-agent/src/tools/fetch.ts | 11 +++----- packages/coding-agent/src/tools/read.ts | 5 +--- packages/coding-agent/src/utils/markit.ts | 12 ++++++++ packages/coding-agent/src/utils/zip.ts | 17 ++++++++++- packages/coding-agent/test/tools.test.ts | 28 +++++++++++++++++++ 9 files changed, 69 insertions(+), 16 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 493bd5332..cd92e1524 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,11 @@ ## [Unreleased] +### Fixed + +- Fixed `read`/`write` not recognizing ZIP-based `.jar`/`.war`/`.ear`/`.apk` files as archives, so `read lib.jar:META-INF/MANIFEST.MF` failed with path-not-found ([#5808](https://github.com/can1357/oh-my-pi/issues/5808)). +- Fixed legacy binary `.doc`/`.ppt`/`.xls`/`.rtf` being advertised as convertible in `read`, `fetch`, and CLI `@file` handling despite having no markit converter, which surfaced an `Unsupported format` error instead of falling through to normal file handling ([#5808](https://github.com/can1357/oh-my-pi/issues/5808)). + ## [17.0.2] - 2026-07-17 ### Added diff --git a/packages/coding-agent/src/cli/file-processor.ts b/packages/coding-agent/src/cli/file-processor.ts index 355337f8f..2ebf570c2 100644 --- a/packages/coding-agent/src/cli/file-processor.ts +++ b/packages/coding-agent/src/cli/file-processor.ts @@ -9,13 +9,12 @@ import chalk from "chalk"; import { resolveReadPath } from "../tools/path-utils"; import { formatBytes } from "../tools/render-utils"; import { formatDimensionNote, resizeImage } from "../utils/image-resize"; -import { convertFileWithMarkit } from "../utils/markit"; +import { CONVERTIBLE_EXTENSIONS, convertFileWithMarkit } from "../utils/markit"; // Keep CLI startup responsive and avoid OOM when users pass huge files. // If a file exceeds these limits, we include it as a path-only block. const MAX_CLI_TEXT_BYTES = 5 * 1024 * 1024; // 5MB const MAX_CLI_IMAGE_BYTES = 25 * 1024 * 1024; // 25MB -const CONVERTIBLE_EXTENSIONS = new Set([".pdf", ".doc", ".docx", ".ppt", ".pptx", ".xls", ".xlsx", ".rtf", ".epub"]); export interface ProcessedFiles { text: string; diff --git a/packages/coding-agent/src/prompts/tools/read.md b/packages/coding-agent/src/prompts/tools/read.md index b2bcd639c..47610ccdb 100644 --- a/packages/coding-agent/src/prompts/tools/read.md +++ b/packages/coding-agent/src/prompts/tools/read.md @@ -15,7 +15,7 @@ Read files, directories, archives, SQLite, images, documents, internal resources - {{#if IS_HL_MODE}}File + selector → `[foo.ts#1A2B]` snapshot header + numbered lines. Copy `[FILENAME#TAG]` for anchored edits; NEVER fabricate the tag.{{/if}} - Directory → depth-limited dirent listing. - SQLite (`.sqlite`, `.sqlite3`, `.db`, `.db3`): `file.db` (tables), `file.db:table` (schema+rows), `file.db:table:key` (by PK), `?limit=`/`?where=`/`?q=SELECT`. -- Archives (`.tar`, `.tar.gz`, `.tgz`, `.zip`): `archive.ext:path/inside/archive` reads a member. +- Archives (`.tar`, `.tar.gz`, `.tgz`, `.zip`, plus ZIP-based `.jar`/`.war`/`.ear`/`.apk`): `archive.ext:path/inside/archive` reads a member. - Documents → extracted text. Notebooks → editable cells. Images → {{#if INSPECT_IMAGE_ENABLED}}metadata; call `inspect_image`{{else}}decoded inline{{/if}}. `:raw` bypasses converters. - URLs → reader-mode clean text/markdown; `:raw` → untouched HTML. Bare `host:port` needs trailing slash. - Internal URIs — all schemes take selectors. `artifact://` recovers spilled output; page with `:N-M`/`:raw:N-M`. diff --git a/packages/coding-agent/src/prompts/tools/write.md b/packages/coding-agent/src/prompts/tools/write.md index 00af5c3a6..d03765c43 100644 --- a/packages/coding-agent/src/prompts/tools/write.md +++ b/packages/coding-agent/src/prompts/tools/write.md @@ -3,7 +3,7 @@ Creates or overwrites file at specified path. - Creating new files explicitly required by task - Replacing entire file contents when editing would be more complex -- Supports `.tar`, `.tar.gz`, `.tgz`, and `.zip` archive entries via `archive.ext:path/inside/archive` +- Supports `.tar`, `.tar.gz`, `.tgz`, `.zip`, and ZIP-based `.jar`/`.war`/`.ear`/`.apk` archive entries via `archive.ext:path/inside/archive` - Supports SQLite row operations via `db.sqlite:table` (insert), `db.sqlite:table:key` (update with JSON content, delete with empty content) diff --git a/packages/coding-agent/src/tools/fetch.ts b/packages/coding-agent/src/tools/fetch.ts index 174afdb77..e813772b2 100644 --- a/packages/coding-agent/src/tools/fetch.ts +++ b/packages/coding-agent/src/tools/fetch.ts @@ -19,6 +19,7 @@ import { renderStatusLine, urlHyperlink } from "../tui"; import { CachedOutputBlock, markFramedBlockComponent } from "../tui/output-block"; import { webpExclusionForModel } from "../utils/image-loading"; import { formatDimensionNote, resizeImage } from "../utils/image-resize"; +import { CONVERTIBLE_EXTENSIONS } from "../utils/markit"; import { ensureTool } from "../utils/tools-manager"; import { type ArchiveFormat, listArchiveRoot, sniffArchiveFormat } from "../utils/zip"; import { extractWithParallel, findParallelApiKey, getParallelExtractContent } from "../web/parallel"; @@ -39,21 +40,17 @@ import { clampTimeout } from "./tool-timeouts"; // ============================================================================= const FETCH_DEFAULT_MAX_LINES = 300; -// Convertible document types handled by markit. +// MIME types markit can convert — one per registered converter (pdf, docx, +// pptx, xlsx, epub). Legacy `application/msword`, `application/vnd.ms-*`, and +// `application/rtf` are intentionally absent: markit has no converter for them. const CONVERTIBLE_MIMES = new Set([ "application/pdf", - "application/msword", - "application/vnd.ms-powerpoint", - "application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.openxmlformats-officedocument.presentationml.presentation", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "application/rtf", "application/epub+zip", ]); -const CONVERTIBLE_EXTENSIONS = new Set([".pdf", ".doc", ".docx", ".ppt", ".pptx", ".xls", ".xlsx", ".rtf", ".epub"]); - const NOTEBOOK_MIMES = new Set(["application/x-ipynb+json"]); const NOTEBOOK_EXTENSIONS = new Set([".ipynb"]); diff --git a/packages/coding-agent/src/tools/read.ts b/packages/coding-agent/src/tools/read.ts index 172cc110e..105a7f1d8 100644 --- a/packages/coding-agent/src/tools/read.ts +++ b/packages/coding-agent/src/tools/read.ts @@ -62,7 +62,7 @@ import { MAX_IMAGE_INPUT_BYTES, webpExclusionForModel, } from "../utils/image-loading"; -import { convertFileWithMarkit } from "../utils/markit"; +import { CONVERTIBLE_EXTENSIONS, convertFileWithMarkit } from "../utils/markit"; import { type ArchiveReader, formatArchiveEntryLines, openArchive, parseArchivePathCandidates } from "../utils/zip"; import { buildDirectoryTree, type DirectoryTree } from "../workspace-tree"; import { @@ -151,9 +151,6 @@ function getSummaryParseCache(session: object): LRUCache = new Set([".pdf", ".docx", ".pptx", ".xlsx", ".epub"]); + export interface MarkitConversionResult { content: string; ok: boolean; diff --git a/packages/coding-agent/src/utils/zip.ts b/packages/coding-agent/src/utils/zip.ts index 962c4e18c..947ad189f 100644 --- a/packages/coding-agent/src/utils/zip.ts +++ b/packages/coding-agent/src/utils/zip.ts @@ -233,12 +233,27 @@ function ensureParentDirectories(map: Map): void { } } +/** + * Extensions that are ZIP containers under a different name — JVM (`.jar`, + * `.war`, `.ear`) and Android (`.apk`) packages are all ZIP archives. Treated + * as `zip` for member read/list and whole-archive rewrite. + */ +const ZIP_ALIAS_EXTENSIONS = ["jar", "war", "ear", "apk"] as const; + +/** + * Regex alternation of every recognized archive extension, longest first so + * `.tar.gz` wins over `.tar`. Shared with `parseArchivePathCandidates` as its + * split pattern so extension recognition and path splitting never drift. + */ +const ARCHIVE_EXTENSION_ALTERNATION = ["tar\\.gz", "tgz", "zip", "tar", ...ZIP_ALIAS_EXTENSIONS].join("|"); + /** Infer an archive format from a filesystem path's extension. */ export function archiveFormatFromPath(filePath: string): ArchiveFormat | undefined { const normalized = filePath.toLowerCase(); if (normalized.endsWith(".tar.gz") || normalized.endsWith(".tgz")) return "tar.gz"; if (normalized.endsWith(".tar")) return "tar"; if (normalized.endsWith(".zip")) return "zip"; + if (ZIP_ALIAS_EXTENSIONS.some(ext => normalized.endsWith(`.${ext}`))) return "zip"; return undefined; } @@ -635,7 +650,7 @@ async function readZipEntries(source: ByteSource): Promise */ export function parseArchivePathCandidates(filePath: string): ArchivePathCandidate[] { const normalized = filePath.replace(/\\/g, "/"); - const pattern = /\.(?:tar\.gz|tgz|zip|tar)(?=(?::|$))/gi; + const pattern = new RegExp(`\\.(?:${ARCHIVE_EXTENSION_ALTERNATION})(?=(?::|$))`, "gi"); const seen = new Set(); const candidates: ArchivePathCandidate[] = []; diff --git a/packages/coding-agent/test/tools.test.ts b/packages/coding-agent/test/tools.test.ts index 788887eb2..e6f48e0d7 100644 --- a/packages/coding-agent/test/tools.test.ts +++ b/packages/coding-agent/test/tools.test.ts @@ -601,6 +601,20 @@ describe("Coding Agent Tools", () => { expect(output).not.toContain("Cannot read binary file"); }); + it("does not route a legacy .xls through markit's unsupported-format error", async () => { + // `.xls` was advertised as convertible but markit only registers the + // OOXML `.xlsx` converter, so reading a legacy `.xls` surfaced + // "Unsupported format: .xls" instead of falling through to normal + // file handling (issue #5808). An OLE2 header (`D0 CF 11 E0`) is a + // binary blob, so the read tool now refuses it as binary. + const xlsFile = path.join(testDir, "legacy.xls"); + fs.writeFileSync(xlsFile, Buffer.from([0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1])); + + const output = getTextOutput(await readTool.execute("test-call-legacy-xls", { path: xlsFile })); + expect(output).not.toContain("Unsupported format"); + expect(output).toContain("Cannot read binary file"); + }); + it("should reject malformed internal-URL selectors instead of dumping the whole resource", async () => { await expect(readTool.execute("test-call-bad-internal-sel", { path: "artifact://3:-100" })).rejects.toThrow( /Invalid selector ':-100'/, @@ -736,6 +750,20 @@ describe("Coding Agent Tools", () => { path: "fixture-subpath.zip", create: (entries: ArchiveFixtureEntry[]) => createZipArchive(entries), }, + { + // `.jar`/`.war` are ZIP containers under a different extension. + // Regression: archiveFormatFromPath / parseArchivePathCandidates + // previously excluded them, so `read lib.jar:member` failed with + // path-not-found (issue #5808). + label: ".jar", + path: "fixture-subpath.jar", + create: (entries: ArchiveFixtureEntry[]) => createZipArchive(entries), + }, + { + label: ".war", + path: "fixture-subpath.war", + create: (entries: ArchiveFixtureEntry[]) => createZipArchive(entries), + }, ]) { it(`should read ${archiveCase.label} subpaths`, async () => { const archivePath = path.join(testDir, archiveCase.path);