diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 9d79d33d6..05bfa6114 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed Bash internal URLs remaining unresolved when used as unquoted arguments inside command substitutions ([#5535](https://github.com/can1357/oh-my-pi/issues/5535)). + ## [16.5.2] - 2026-07-14 ### Breaking Changes diff --git a/packages/coding-agent/src/tools/bash-skill-urls.ts b/packages/coding-agent/src/tools/bash-skill-urls.ts index 135990b21..b4de3bd4b 100644 --- a/packages/coding-agent/src/tools/bash-skill-urls.ts +++ b/packages/coding-agent/src/tools/bash-skill-urls.ts @@ -142,7 +142,14 @@ function unquoteToken(token: string): string { } function isInsideShellQuote(command: string, index: number): boolean { - let quote: "'" | '"' | undefined; + type ShellQuote = "'" | '"' | undefined; + interface CommandSubstitution { + outerQuote: ShellQuote; + depth: number; + } + + let quote: ShellQuote; + const substitutions: CommandSubstitution[] = []; for (let i = 0; i < index; i++) { const char = command[i]; if (char === "\\" && quote !== "'") { @@ -155,6 +162,25 @@ function isInsideShellQuote(command: string, index: number): boolean { } if (char === '"' && quote !== "'") { quote = quote === '"' ? undefined : '"'; + continue; + } + if (char === "$" && command[i + 1] === "(" && quote !== "'") { + substitutions.push({ outerQuote: quote, depth: 1 }); + quote = undefined; + i++; + continue; + } + if (quote !== undefined) continue; + + const substitution = substitutions.at(-1); + if (!substitution) continue; + if (char === "(") { + substitution.depth++; + } else if (char === ")") { + substitution.depth--; + if (substitution.depth === 0) { + quote = substitutions.pop()?.outerQuote; + } } } return quote !== undefined; diff --git a/packages/coding-agent/test/tools/bash-skill-urls.test.ts b/packages/coding-agent/test/tools/bash-skill-urls.test.ts index 9ea65e242..d84a41409 100644 --- a/packages/coding-agent/test/tools/bash-skill-urls.test.ts +++ b/packages/coding-agent/test/tools/bash-skill-urls.test.ts @@ -206,6 +206,16 @@ describe("expandInternalUrls", () => { ); }); + it("expands an unquoted URL inside a double-quoted command substitution", async () => { + const skills = [createSkill("valid-skill", "/tmp/skills/valid-skill")]; + const command = 'echo "$(realpath skill://valid-skill/SKILL.md 2>&1)"'; + const expectedPath = path.join(skills[0].baseDir, "SKILL.md"); + + await expect(expandInternalUrls(command, { skills })).resolves.toBe( + `echo "$(realpath ${shellEscape(expectedPath)} 2>&1)"`, + ); + }); + it("leaves literal internal URLs embedded in quoted text unchanged", async () => { const router = createInternalRouter({ "memory://root/summary.md": { sourcePath: "/tmp/memories/summary.md" },