diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 91f250df5..286b4d493 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -86,6 +86,10 @@ - Fixed seen-line guard retries forcing agents to resend entire unchanged patches. Complete inline reveals now issue one-shot `RETRY ` continuations that rerun validation against live files, while numbered lines in successful edit output join the returned snapshot's seen-line provenance only when the written content exactly matches that output. +### Fixed + +- Fixed Claude Code custom tool discovery attempting to import non-module files from `.claude/tools` ([#8471](https://github.com/can1357/oh-my-pi/pull/8471) by [@Kigbnajd](https://github.com/Kigbnajd)). + ## [17.3.1] - 2026-08-13 ### Fixed diff --git a/packages/coding-agent/src/discovery/claude.ts b/packages/coding-agent/src/discovery/claude.ts index 6786c77b7..6f3b7c624 100644 --- a/packages/coding-agent/src/discovery/claude.ts +++ b/packages/coding-agent/src/discovery/claude.ts @@ -406,8 +406,9 @@ async function loadTools(ctx: LoadContext): Promise> { const userToolsDir = path.join(userBase, "tools"); const userResult = await loadFilesFromDir(ctx, userToolsDir, PROVIDER_ID, "user", { + extensions: ["ts", "js"], transform: (name, _content, path, source) => { - const toolName = name.replace(/\.(ts|js|sh|bash|py)$/, ""); + const toolName = name.replace(/\.(ts|js)$/, ""); return { name: toolName, path, @@ -425,8 +426,9 @@ async function loadTools(ctx: LoadContext): Promise> { const projectToolsDir = path.join(projectBase, "tools"); const projectResult = await loadFilesFromDir(ctx, projectToolsDir, PROVIDER_ID, "project", { + extensions: ["ts", "js"], transform: (name, _content, path, source) => { - const toolName = name.replace(/\.(ts|js|sh|bash|py)$/, ""); + const toolName = name.replace(/\.(ts|js)$/, ""); return { name: toolName, path, diff --git a/packages/coding-agent/test/discovery/claude-tools.test.ts b/packages/coding-agent/test/discovery/claude-tools.test.ts new file mode 100644 index 000000000..2124505bb --- /dev/null +++ b/packages/coding-agent/test/discovery/claude-tools.test.ts @@ -0,0 +1,70 @@ +import { afterEach, beforeEach, describe, expect, test, vi } from "bun:test"; +import * as fs from "node:fs/promises"; +import * as os from "node:os"; +import * as path from "node:path"; +import { type CustomTool, toolCapability } from "@oh-my-pi/pi-coding-agent/capability/tool"; +import { resetSettingsForTest, Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; +import { initializeWithSettings, loadCapability } from "@oh-my-pi/pi-coding-agent/discovery"; +import { removeWithRetries } from "@oh-my-pi/pi-utils"; + +describe("Claude Code custom tool discovery", () => { + let root = ""; + let home = ""; + let project = ""; + let originalHome: string | undefined; + let originalClaudeConfigDir: string | undefined; + + beforeEach(async () => { + resetSettingsForTest(); + originalHome = process.env.HOME; + originalClaudeConfigDir = process.env.CLAUDE_CONFIG_DIR; + delete process.env.CLAUDE_CONFIG_DIR; + root = await fs.mkdtemp(path.join(os.tmpdir(), "omp-claude-tools-")); + home = path.join(root, "home"); + project = path.join(root, "project"); + process.env.HOME = home; + vi.spyOn(os, "homedir").mockReturnValue(home); + await fs.mkdir(path.join(project, ".git"), { recursive: true }); + const settings = await Settings.init({ inMemory: true, cwd: project }); + initializeWithSettings(settings); + }); + + afterEach(async () => { + resetSettingsForTest(); + vi.restoreAllMocks(); + if (originalHome === undefined) delete process.env.HOME; + else process.env.HOME = originalHome; + if (originalClaudeConfigDir === undefined) delete process.env.CLAUDE_CONFIG_DIR; + else process.env.CLAUDE_CONFIG_DIR = originalClaudeConfigDir; + await removeWithRetries(root); + }); + + test("discovers only JavaScript and TypeScript modules", async () => { + const userTools = path.join(home, ".claude", "tools"); + const projectTools = path.join(project, ".claude", "tools"); + await fs.mkdir(userTools, { recursive: true }); + await fs.mkdir(projectTools, { recursive: true }); + await Promise.all([ + fs.writeFile(path.join(userTools, "user-tool.ts"), "export default () => ({});\n"), + fs.writeFile(path.join(projectTools, "project-tool.js"), "export default () => ({});\n"), + fs.writeFile(path.join(userTools, "helper.sh"), "#!/bin/sh\n"), + fs.writeFile(path.join(projectTools, "helper.bash"), "#!/bin/bash\n"), + fs.writeFile(path.join(projectTools, "helper.py"), "print('helper')\n"), + fs.writeFile(path.join(projectTools, "notes.md"), "# Notes\n"), + ]); + + const result = await loadCapability(toolCapability.id, { + cwd: project, + providers: ["claude"], + }); + const tools = result.items + .map(tool => ({ name: tool.name, level: tool.level })) + .sort((a, b) => a.name.localeCompare(b.name)); + + expect(result.warnings).toEqual([]); + expect(tools).toEqual([ + { name: "project-tool", level: "project" }, + { name: "user-tool", level: "user" }, + ]); + }); +});