From cf73f3a11f1797c538cfbf2fcc81dd224487f2bb Mon Sep 17 00:00:00 2001 From: Charles Packer Date: Tue, 4 Nov 2025 11:50:07 -0800 Subject: [PATCH] ci: add typechecking, fail fast in CI, and patch typechecking errors (#63) --- .github/workflows/ci.yml | 26 +++++++++++-- .github/workflows/release.yml | 3 ++ .husky/pre-commit | 1 + bun.lock | 4 ++ package.json | 4 ++ scripts/check.js | 43 +++++++++++++++++++++ src/agent/check-approval.ts | 8 ++-- src/agent/model.ts | 8 +++- src/cli/components/AdvancedDiffRenderer.tsx | 12 ++++-- src/cli/components/MarkdownDisplay.tsx | 11 ++++-- src/cli/components/PasteAwareTextInput.tsx | 2 + src/cli/helpers/backfill.ts | 3 ++ src/cli/helpers/formatArgsDisplay.ts | 6 ++- src/cli/helpers/stream.ts | 2 +- src/permissions/analyzer.ts | 12 +++--- src/permissions/matcher.ts | 8 ++-- src/tests/clipboard.test.ts | 2 +- src/tests/tools/bash-background.test.ts | 8 ++-- src/tests/tools/bash.test.ts | 18 ++++----- src/tests/tools/edit.test.ts | 2 +- src/tests/tools/ls.test.ts | 18 ++++----- src/tests/tools/multiedit.test.ts | 2 +- src/tests/tools/tool-truncation.test.ts | 1 + src/tools/impl/Grep.ts | 4 +- src/tools/impl/LS.ts | 6 ++- src/tools/impl/MultiEdit.ts | 12 ++++-- src/tools/toolDefinitions.ts | 26 ++++++------- 27 files changed, 183 insertions(+), 69 deletions(-) create mode 100755 scripts/check.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 691b607..e65ee34 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,30 @@ on: - main jobs: + check: + name: Lint & Type Check + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: 1.3.0 + + - name: Install dependencies + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: bun install + + - name: Lint & Type Check + run: bun run check + build: + needs: check name: ${{ matrix.name }} runs-on: ${{ matrix.runner }} strategy: @@ -43,9 +66,6 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: bun install - - name: Lint - run: bun run lint - - name: Run tests run: bun test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e5292f6..97fa829 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -78,6 +78,9 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: bun install + - name: Lint & Type Check + run: bun run check + - name: Build bundle run: bun run build diff --git a/.husky/pre-commit b/.husky/pre-commit index 82e9155..32177c4 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,2 +1,3 @@ #!/usr/bin/env sh bun lint-staged +bun run typecheck diff --git a/bun.lock b/bun.lock index 25b6f61..775a07c 100644 --- a/bun.lock +++ b/bun.lock @@ -11,6 +11,7 @@ "devDependencies": { "@types/bun": "latest", "@types/diff": "^8.0.0", + "@types/picomatch": "^4.0.2", "diff": "^8.0.2", "husky": "9.1.7", "ink": "^5.0.0", @@ -18,6 +19,7 @@ "ink-text-input": "^5.0.0", "lint-staged": "16.2.4", "minimatch": "^10.0.3", + "picomatch": "^2.3.1", "react": "18.2.0", "typescript": "^5.0.0", }, @@ -41,6 +43,8 @@ "@types/node": ["@types/node@24.9.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg=="], + "@types/picomatch": ["@types/picomatch@4.0.2", "", {}, "sha512-qHHxQ+P9PysNEGbALT8f8YOSHW0KJu6l2xU8DYY0fu/EmGxXdVnuTLvFUvBgPJMSqXq29SYHveejeAha+4AYgA=="], + "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], "@vscode/ripgrep": ["@vscode/ripgrep@1.17.0", "", { "dependencies": { "https-proxy-agent": "^7.0.2", "proxy-from-env": "^1.1.0", "yauzl": "^2.9.2" } }, "sha512-mBRKm+ASPkUcw4o9aAgfbusIu6H4Sdhw09bjeP1YOBFTJEZAnrnk6WZwzv8NEjgC82f7ILvhmb1WIElSugea6g=="], diff --git a/package.json b/package.json index 1c8708d..74d32ba 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "devDependencies": { "@types/bun": "latest", "@types/diff": "^8.0.0", + "@types/picomatch": "^4.0.2", "diff": "^8.0.2", "husky": "9.1.7", "ink": "^5.0.0", @@ -39,12 +40,15 @@ "ink-text-input": "^5.0.0", "lint-staged": "16.2.4", "minimatch": "^10.0.3", + "picomatch": "^2.3.1", "react": "18.2.0", "typescript": "^5.0.0" }, "scripts": { "lint": "bunx --bun @biomejs/biome@2.2.5 check src", "fix": "bunx --bun @biomejs/biome@2.2.5 check --write src", + "typecheck": "tsc --noEmit", + "check": "bun run scripts/check.js", "dev": "bun --loader:.md=text --loader:.mdx=text --loader:.txt=text run src/index.ts", "build": "bun run build.js", "prepare": "bun run build", diff --git a/scripts/check.js b/scripts/check.js new file mode 100755 index 0000000..5b3966b --- /dev/null +++ b/scripts/check.js @@ -0,0 +1,43 @@ +#!/usr/bin/env bun +// Script to run linting and type checking with helpful error messages + +import { $ } from "bun"; + +console.log("🔍 Running lint and type checks...\n"); + +let failed = false; + +// Run lint +console.log("📝 Running Biome linter..."); +try { + await $`bun run lint`; + console.log("✅ Linting passed\n"); +} catch (error) { + console.error("❌ Linting failed\n"); + console.error("To fix automatically, run:"); + console.error(" bun run fix\n"); + failed = true; +} + +// Run typecheck +console.log("🔎 Running TypeScript type checker..."); +try { + await $`bun run typecheck`; + console.log("✅ Type checking passed\n"); +} catch (error) { + console.error("❌ Type checking failed\n"); + console.error("Fix the type errors shown above, then run:"); + console.error(" bun run typecheck\n"); + failed = true; +} + +if (failed) { + console.error("❌ Checks failed. Please fix the errors above."); + console.error("\nQuick commands:"); + console.error(" bun run fix # Auto-fix linting issues"); + console.error(" bun run typecheck # Check types only"); + console.error(" bun run check # Run both checks"); + process.exit(1); +} + +console.log("✅ All checks passed!"); diff --git a/src/agent/check-approval.ts b/src/agent/check-approval.ts index 2c72b36..e00d622 100644 --- a/src/agent/check-approval.ts +++ b/src/agent/check-approval.ts @@ -69,10 +69,12 @@ export async function getResumeData( const approvalMessage = matchingMessages.find( (msg) => msg.message_type === "approval_request_message", ); - const inContextMessage = - approvalMessage ?? matchingMessages[matchingMessages.length - 1]!; + const lastMessage = matchingMessages[matchingMessages.length - 1]; + const inContextMessage = approvalMessage ?? lastMessage; - messageToCheck = inContextMessage; + if (inContextMessage) { + messageToCheck = inContextMessage; + } } else { console.warn( `[check-approval] In-context message ${inContextLastMessageId} not found in cursor fetch.\n` + diff --git a/src/agent/model.ts b/src/agent/model.ts index 062b623..dbfb667 100644 --- a/src/agent/model.ts +++ b/src/agent/model.ts @@ -25,7 +25,13 @@ export function resolveModel(modelIdentifier: string): string | null { */ export function getDefaultModel(): string { const defaultModel = models.find((m) => m.isDefault); - return defaultModel?.handle || models[0].handle; + if (defaultModel) return defaultModel.handle; + + const firstModel = models[0]; + if (!firstModel) { + throw new Error("No models available in models.json"); + } + return firstModel.handle; } /** diff --git a/src/cli/components/AdvancedDiffRenderer.tsx b/src/cli/components/AdvancedDiffRenderer.tsx index aada554..3d5df27 100644 --- a/src/cli/components/AdvancedDiffRenderer.tsx +++ b/src/cli/components/AdvancedDiffRenderer.tsx @@ -281,7 +281,9 @@ export function AdvancedDiffRenderer( let newNo = h.newStart; let lastRemovalNo: number | null = null; for (let i = 0; i < h.lines.length; i++) { - const raw = h.lines[i].raw || ""; + const line = h.lines[i]; + if (!line) continue; + const raw = line.raw || ""; const ch = raw.charAt(0); const body = raw.slice(1); // Skip meta lines (e.g., "\ No newline at end of file"): do not display, do not advance counters, @@ -291,7 +293,9 @@ export function AdvancedDiffRenderer( // Helper to find next non-meta '+' index const findNextPlus = (start: number): string | undefined => { for (let j = start + 1; j < h.lines.length; j++) { - const r = h.lines[j].raw || ""; + const nextLine = h.lines[j]; + if (!nextLine) continue; + const r = nextLine.raw || ""; if (r.charAt(0) === "\\") continue; // skip meta if (r.startsWith("+")) return r.slice(1); break; // stop at first non-meta non-plus @@ -301,7 +305,9 @@ export function AdvancedDiffRenderer( // Helper to find previous non-meta '-' index const findPrevMinus = (start: number): string | undefined => { for (let k = start - 1; k >= 0; k--) { - const r = h.lines[k].raw || ""; + const prevLine = h.lines[k]; + if (!prevLine) continue; + const r = prevLine.raw || ""; if (r.charAt(0) === "\\") continue; // skip meta if (r.startsWith("-")) return r.slice(1); break; // stop at first non-meta non-minus diff --git a/src/cli/components/MarkdownDisplay.tsx b/src/cli/components/MarkdownDisplay.tsx index 28c63ef..64a6734 100644 --- a/src/cli/components/MarkdownDisplay.tsx +++ b/src/cli/components/MarkdownDisplay.tsx @@ -77,7 +77,7 @@ export const MarkdownDisplay: React.FC = ({ // Check for headers const headerMatch = line.match(headerRegex); - if (headerMatch) { + if (headerMatch?.[1] && headerMatch[2] !== undefined) { const level = headerMatch[1].length; const content = headerMatch[2]; @@ -119,7 +119,12 @@ export const MarkdownDisplay: React.FC = ({ // Check for list items const listMatch = line.match(listItemRegex); - if (listMatch) { + if ( + listMatch && + listMatch[1] !== undefined && + listMatch[2] && + listMatch[3] !== undefined + ) { const indent = listMatch[1].length; const marker = listMatch[2]; const content = listMatch[3]; @@ -146,7 +151,7 @@ export const MarkdownDisplay: React.FC = ({ // Check for blockquotes const blockquoteMatch = line.match(blockquoteRegex); - if (blockquoteMatch) { + if (blockquoteMatch && blockquoteMatch[1] !== undefined) { contentBlocks.push( diff --git a/src/cli/components/PasteAwareTextInput.tsx b/src/cli/components/PasteAwareTextInput.tsx index ac9f979..444a49b 100644 --- a/src/cli/components/PasteAwareTextInput.tsx +++ b/src/cli/components/PasteAwareTextInput.tsx @@ -60,6 +60,8 @@ export function PasteAwareTextInput({ onSubmit?: (value: string) => void; placeholder?: string; focus?: boolean; + externalCursorOffset?: number; + onCursorOffsetChange?: (n: number) => void; }>; // Sync external value changes (treat incoming value as DISPLAY value) diff --git a/src/cli/helpers/backfill.ts b/src/cli/helpers/backfill.ts index e0fe4f0..ab26485 100644 --- a/src/cli/helpers/backfill.ts +++ b/src/cli/helpers/backfill.ts @@ -123,6 +123,9 @@ export function backfillBuffers( if (toolCalls.length > 0 && toolCalls[0]?.tool_call_id) { const toolCall = toolCalls[0]; const toolCallId = toolCall.tool_call_id; + // Skip if any required fields are missing + if (!toolCallId || !toolCall.name || !toolCall.arguments) break; + const exists = buffers.byId.has(lineId); buffers.byId.set(lineId, { diff --git a/src/cli/helpers/formatArgsDisplay.ts b/src/cli/helpers/formatArgsDisplay.ts index a6e819c..f0e8972 100644 --- a/src/cli/helpers/formatArgsDisplay.ts +++ b/src/cli/helpers/formatArgsDisplay.ts @@ -23,11 +23,13 @@ export function formatArgsDisplay(argsJson: string): { if ("request_heartbeat" in clone) delete clone.request_heartbeat; parsed = clone; const keys = Object.keys(parsed); + const firstKey = keys[0]; if ( keys.length === 1 && - ["query", "path", "file_path", "command", "label"].includes(keys[0]) + firstKey && + ["query", "path", "file_path", "command", "label"].includes(firstKey) ) { - const v = parsed[keys[0]]; + const v = parsed[firstKey]; display = typeof v === "string" ? v : String(v); } else { display = Object.entries(parsed) diff --git a/src/cli/helpers/stream.ts b/src/cli/helpers/stream.ts index 0e9e0d8..70d2e90 100644 --- a/src/cli/helpers/stream.ts +++ b/src/cli/helpers/stream.ts @@ -191,7 +191,7 @@ export async function drainStreamWithResume( // Use the resume result (should have proper stop_reason now) result = resumeResult; - } catch (e) { + } catch (_e) { // Resume failed - stick with the error stop_reason // The original error result will be returned } diff --git a/src/permissions/analyzer.ts b/src/permissions/analyzer.ts index 8cc5511..647d0f6 100644 --- a/src/permissions/analyzer.ts +++ b/src/permissions/analyzer.ts @@ -161,7 +161,7 @@ function analyzeBashApproval( _workingDir: string, ): ApprovalContext { const parts = command.trim().split(/\s+/); - const baseCommand = parts[0]; + const baseCommand = parts[0] || ""; const firstArg = parts[1] || ""; // Dangerous commands - no persistence @@ -178,7 +178,7 @@ function analyzeBashApproval( "killall", ]; - if (dangerousCommands.includes(baseCommand)) { + if (baseCommand && dangerousCommands.includes(baseCommand)) { return { recommendedRule: "", ruleDescription: "", @@ -248,7 +248,7 @@ function analyzeBashApproval( } // Package manager commands - if (["npm", "bun", "yarn", "pnpm"].includes(baseCommand)) { + if (baseCommand && ["npm", "bun", "yarn", "pnpm"].includes(baseCommand)) { const subcommand = firstArg; const thirdPart = parts[2]; @@ -295,7 +295,7 @@ function analyzeBashApproval( "tail", ]; - if (safeCommands.includes(baseCommand)) { + if (baseCommand && safeCommands.includes(baseCommand)) { return { recommendedRule: `Bash(${baseCommand}:*)`, ruleDescription: `'${baseCommand}' commands`, @@ -318,7 +318,7 @@ function analyzeBashApproval( for (const segment of segments) { const segmentParts = segment.trim().split(/\s+/); - const segmentBase = segmentParts[0]; + const segmentBase = segmentParts[0] || ""; const segmentArg = segmentParts[1] || ""; // Check if this segment is git command @@ -350,7 +350,7 @@ function analyzeBashApproval( } // Check if this segment is npm/bun/yarn/pnpm - if (["npm", "bun", "yarn", "pnpm"].includes(segmentBase)) { + if (segmentBase && ["npm", "bun", "yarn", "pnpm"].includes(segmentBase)) { const subcommand = segmentArg; const thirdPart = segmentParts[2]; diff --git a/src/permissions/matcher.ts b/src/permissions/matcher.ts index c498534..d58ecdd 100644 --- a/src/permissions/matcher.ts +++ b/src/permissions/matcher.ts @@ -26,7 +26,7 @@ export function matchesFilePattern( // Extract tool name and file path from query // Format: "ToolName(filePath)" const queryMatch = query.match(/^([^(]+)\((.+)\)$/); - if (!queryMatch) { + if (!queryMatch || !queryMatch[1] || !queryMatch[2]) { return false; } const queryTool = queryMatch[1]; @@ -35,7 +35,7 @@ export function matchesFilePattern( // Extract tool name and glob pattern from permission rule // Format: "ToolName(pattern)" const patternMatch = pattern.match(/^([^(]+)\((.+)\)$/); - if (!patternMatch) { + if (!patternMatch || !patternMatch[1] || !patternMatch[2]) { return false; } const patternTool = patternMatch[1]; @@ -98,7 +98,7 @@ export function matchesBashPattern(query: string, pattern: string): boolean { // Extract the command from query // Format: "Bash(actual command)" or "Bash()" const queryMatch = query.match(/^Bash\((.*)\)$/); - if (!queryMatch) { + if (!queryMatch || queryMatch[1] === undefined) { return false; } const command = queryMatch[1]; @@ -106,7 +106,7 @@ export function matchesBashPattern(query: string, pattern: string): boolean { // Extract the command pattern from permission rule // Format: "Bash(command pattern)" or "Bash()" const patternMatch = pattern.match(/^Bash\((.*)\)$/); - if (!patternMatch) { + if (!patternMatch || patternMatch[1] === undefined) { return false; } const commandPattern = patternMatch[1]; diff --git a/src/tests/clipboard.test.ts b/src/tests/clipboard.test.ts index 051ed77..6884b67 100644 --- a/src/tests/clipboard.test.ts +++ b/src/tests/clipboard.test.ts @@ -76,7 +76,7 @@ test("buildMessageContentFromDisplay handles mixed content", () => { type: "text", text: "Start Pasted content middle ", }); - expect(content[1].type).toBe("image"); + expect(content[1]?.type).toBe("image"); expect(content[2]).toEqual({ type: "text", text: " end" }); }); diff --git a/src/tests/tools/bash-background.test.ts b/src/tests/tools/bash-background.test.ts index 9c14820..dff5140 100644 --- a/src/tests/tools/bash-background.test.ts +++ b/src/tests/tools/bash-background.test.ts @@ -11,8 +11,8 @@ describe("Bash background tools", () => { run_in_background: true, }); - expect(result.content[0].text).toContain("background with ID:"); - expect(result.content[0].text).toMatch(/bash_\d+/); + expect(result.content[0]?.text).toContain("background with ID:"); + expect(result.content[0]?.text).toMatch(/bash_\d+/); }); test("BashOutput retrieves output from background shell", async () => { @@ -24,7 +24,7 @@ describe("Bash background tools", () => { }); // Extract bash_id from the response text - const match = startResult.content[0].text.match(/bash_(\d+)/); + const match = startResult.content[0]?.text.match(/bash_(\d+)/); expect(match).toBeDefined(); const bashId = `bash_${match?.[1]}`; @@ -51,7 +51,7 @@ describe("Bash background tools", () => { run_in_background: true, }); - const match = startResult.content[0].text.match(/bash_(\d+)/); + const match = startResult.content[0]?.text.match(/bash_(\d+)/); const bashId = `bash_${match?.[1]}`; // Kill it (KillBash uses shell_id parameter) diff --git a/src/tests/tools/bash.test.ts b/src/tests/tools/bash.test.ts index 49ff39c..9812ad1 100644 --- a/src/tests/tools/bash.test.ts +++ b/src/tests/tools/bash.test.ts @@ -9,7 +9,7 @@ describe("Bash tool", () => { }); expect(result.content).toBeDefined(); - expect(result.content[0].text).toContain("Hello, World!"); + expect(result.content[0]?.text).toContain("Hello, World!"); expect(result.isError).toBeUndefined(); }); @@ -19,7 +19,7 @@ describe("Bash tool", () => { description: "Test stderr", }); - expect(result.content[0].text).toContain("error message"); + expect(result.content[0]?.text).toContain("error message"); }); test("returns error for failed command", async () => { @@ -29,7 +29,7 @@ describe("Bash tool", () => { }); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain("Exit code"); + expect(result.content[0]?.text).toContain("Exit code"); }); test("times out long-running command", async () => { @@ -40,7 +40,7 @@ describe("Bash tool", () => { }); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain("timed out"); + expect(result.content[0]?.text).toContain("timed out"); }, 2000); test("runs command in background mode", async () => { @@ -50,8 +50,8 @@ describe("Bash tool", () => { run_in_background: true, }); - expect(result.content[0].text).toContain("background with ID:"); - expect(result.content[0].text).toMatch(/bash_\d+/); + expect(result.content[0]?.text).toContain("background with ID:"); + expect(result.content[0]?.text).toMatch(/bash_\d+/); }); test("handles complex commands with pipes", async () => { @@ -65,8 +65,8 @@ describe("Bash tool", () => { description: "Test pipe", }); - expect(result.content[0].text).toContain("bar"); - expect(result.content[0].text).not.toContain("foo"); + expect(result.content[0]?.text).toContain("bar"); + expect(result.content[0]?.text).not.toContain("foo"); }); test("lists background processes with /bashes command", async () => { @@ -76,7 +76,7 @@ describe("Bash tool", () => { }); expect(result.content).toBeDefined(); - expect(result.content[0].text).toBeDefined(); + expect(result.content[0]?.text).toBeDefined(); }); test("throws error when command is missing", async () => { diff --git a/src/tests/tools/edit.test.ts b/src/tests/tools/edit.test.ts index 9141dcf..5934521 100644 --- a/src/tests/tools/edit.test.ts +++ b/src/tests/tools/edit.test.ts @@ -108,7 +108,7 @@ describe("Edit tool", () => { file_path: file, old_string: "World", new_str: "Bun", - } as Parameters[0]), + } as unknown as Parameters[0]), ).rejects.toThrow(/missing required parameter.*new_string/); }); }); diff --git a/src/tests/tools/ls.test.ts b/src/tests/tools/ls.test.ts index c192ec5..99b9740 100644 --- a/src/tests/tools/ls.test.ts +++ b/src/tests/tools/ls.test.ts @@ -17,9 +17,9 @@ describe("LS tool", () => { const result = await ls({ path: testDir.path }); - expect(result.content[0].text).toContain("file1.txt"); - expect(result.content[0].text).toContain("file2.txt"); - expect(result.content[0].text).toContain("subdir/"); + expect(result.content[0]?.text).toContain("file1.txt"); + expect(result.content[0]?.text).toContain("file2.txt"); + expect(result.content[0]?.text).toContain("subdir/"); }); test("shows directories with trailing slash", async () => { @@ -29,8 +29,8 @@ describe("LS tool", () => { const result = await ls({ path: testDir.path }); - expect(result.content[0].text).toContain("folder/"); - expect(result.content[0].text).toContain("file.txt"); + expect(result.content[0]?.text).toContain("folder/"); + expect(result.content[0]?.text).toContain("file.txt"); }); test("throws error for non-existent directory", async () => { @@ -64,10 +64,10 @@ describe("LS tool", () => { ignore: ["*.log", "node_modules"], }); - expect(result.content[0].text).toContain("file1.txt"); - expect(result.content[0].text).toContain("important.txt"); - expect(result.content[0].text).not.toContain("file2.log"); - expect(result.content[0].text).not.toContain("node_modules"); + expect(result.content[0]?.text).toContain("file1.txt"); + expect(result.content[0]?.text).toContain("important.txt"); + expect(result.content[0]?.text).not.toContain("file2.log"); + expect(result.content[0]?.text).not.toContain("node_modules"); }); test("throws error when ignore is a string instead of array", async () => { diff --git a/src/tests/tools/multiedit.test.ts b/src/tests/tools/multiedit.test.ts index 26a9529..c484a82 100644 --- a/src/tests/tools/multiedit.test.ts +++ b/src/tests/tools/multiedit.test.ts @@ -96,7 +96,7 @@ describe("MultiEdit tool", () => { multi_edit({ file_path: file, edits: [ - { old_string: "foo", new_str: "baz" } as Parameters< + { old_string: "foo", new_str: "baz" } as unknown as Parameters< typeof multi_edit >[0]["edits"][0], ], diff --git a/src/tests/tools/tool-truncation.test.ts b/src/tests/tools/tool-truncation.test.ts index 7c8504b..5d50e8b 100644 --- a/src/tests/tools/tool-truncation.test.ts +++ b/src/tests/tools/tool-truncation.test.ts @@ -267,6 +267,7 @@ describe("tool truncation integration tests", () => { const bashIdMatch = message.match(/with ID: (.+)/); expect(bashIdMatch).toBeTruthy(); const bashId = bashIdMatch?.[1]; + if (!bashId) throw new Error("bashId not found"); // Wait a bit for output to accumulate await new Promise((resolve) => setTimeout(resolve, 100)); diff --git a/src/tools/impl/Grep.ts b/src/tools/impl/Grep.ts index 4c87da5..d645940 100644 --- a/src/tools/impl/Grep.ts +++ b/src/tools/impl/Grep.ts @@ -113,7 +113,9 @@ export async function grep(args: GrepArgs): Promise { for (const line of lines) { const parts = line.split(":"); if (parts.length >= 2) { - const count = parseInt(parts[parts.length - 1], 10); + const lastPart = parts[parts.length - 1]; + if (!lastPart) continue; + const count = parseInt(lastPart, 10); if (!Number.isNaN(count) && count > 0) { totalMatches += count; filesWithMatches++; diff --git a/src/tools/impl/LS.ts b/src/tools/impl/LS.ts index c4b29fc..2b7bcc0 100644 --- a/src/tools/impl/LS.ts +++ b/src/tools/impl/LS.ts @@ -19,7 +19,11 @@ export async function ls( args: LSArgs, ): Promise<{ content: Array<{ type: string; text: string }> }> { validateRequiredParams(args, ["path"], "LS"); - validateParamTypes(args, LSSchema, "LS"); + validateParamTypes( + args as unknown as Record, + LSSchema, + "LS", + ); const { path: inputPath, ignore = [] } = args; const dirPath = resolve(inputPath); try { diff --git a/src/tools/impl/MultiEdit.ts b/src/tools/impl/MultiEdit.ts index 27a6eb6..a53084c 100644 --- a/src/tools/impl/MultiEdit.ts +++ b/src/tools/impl/MultiEdit.ts @@ -25,12 +25,16 @@ export async function multi_edit( throw new Error(`File path must be absolute, got: ${file_path}`); if (!edits || edits.length === 0) throw new Error("No edits provided"); for (let i = 0; i < edits.length; i++) { + const edit = edits[i]; + if (!edit) { + throw new Error(`Edit ${i + 1} is undefined`); + } validateRequiredParams( - edits[i] as Record, + edit as unknown as Record, ["old_string", "new_string"], `MultiEdit (edit ${i + 1})`, ); - if (edits[i].old_string === edits[i].new_string) + if (edit.old_string === edit.new_string) throw new Error( `Edit ${i + 1}: No changes to make: old_string and new_string are exactly the same.`, ); @@ -39,7 +43,9 @@ export async function multi_edit( let content = await fs.readFile(file_path, "utf-8"); const appliedEdits: string[] = []; for (let i = 0; i < edits.length; i++) { - const { old_string, new_string, replace_all = false } = edits[i]; + const edit = edits[i]; + if (!edit) continue; + const { old_string, new_string, replace_all = false } = edit; const occurrences = content.split(old_string).length - 1; if (occurrences === 0) { throw new Error( diff --git a/src/tools/toolDefinitions.ts b/src/tools/toolDefinitions.ts index fd241b2..8d2e0d1 100644 --- a/src/tools/toolDefinitions.ts +++ b/src/tools/toolDefinitions.ts @@ -35,7 +35,7 @@ import ReadSchema from "./schemas/Read.json"; import TodoWriteSchema from "./schemas/TodoWrite.json"; import WriteSchema from "./schemas/Write.json"; -type ToolImplementation = (args: Record) => Promise; +type ToolImplementation = (args: Record) => Promise; interface ToolAssets { schema: Record; @@ -47,62 +47,62 @@ const toolDefinitions = { Bash: { schema: BashSchema, description: BashDescription.trim(), - impl: bash as ToolImplementation, + impl: bash as unknown as ToolImplementation, }, BashOutput: { schema: BashOutputSchema, description: BashOutputDescription.trim(), - impl: bash_output as ToolImplementation, + impl: bash_output as unknown as ToolImplementation, }, Edit: { schema: EditSchema, description: EditDescription.trim(), - impl: edit as ToolImplementation, + impl: edit as unknown as ToolImplementation, }, ExitPlanMode: { schema: ExitPlanModeSchema, description: ExitPlanModeDescription.trim(), - impl: exit_plan_mode as ToolImplementation, + impl: exit_plan_mode as unknown as ToolImplementation, }, Glob: { schema: GlobSchema, description: GlobDescription.trim(), - impl: glob as ToolImplementation, + impl: glob as unknown as ToolImplementation, }, Grep: { schema: GrepSchema, description: GrepDescription.trim(), - impl: grep as ToolImplementation, + impl: grep as unknown as ToolImplementation, }, KillBash: { schema: KillBashSchema, description: KillBashDescription.trim(), - impl: kill_bash as ToolImplementation, + impl: kill_bash as unknown as ToolImplementation, }, LS: { schema: LSSchema, description: LSDescription.trim(), - impl: ls as ToolImplementation, + impl: ls as unknown as ToolImplementation, }, MultiEdit: { schema: MultiEditSchema, description: MultiEditDescription.trim(), - impl: multi_edit as ToolImplementation, + impl: multi_edit as unknown as ToolImplementation, }, Read: { schema: ReadSchema, description: ReadDescription.trim(), - impl: read as ToolImplementation, + impl: read as unknown as ToolImplementation, }, TodoWrite: { schema: TodoWriteSchema, description: TodoWriteDescription.trim(), - impl: todo_write as ToolImplementation, + impl: todo_write as unknown as ToolImplementation, }, Write: { schema: WriteSchema, description: WriteDescription.trim(), - impl: write as ToolImplementation, + impl: write as unknown as ToolImplementation, }, } as const satisfies Record;