refactor: wave 1 - extract leaf modules, rename catch-all files, split index.ts hooks

- Split 25+ index.ts files into hook.ts + extracted modules
- Rename all catch-all utils.ts/helpers.ts to domain-specific names
- Split src/tools/lsp/ into ~15 focused modules
- Split src/tools/delegate-task/ into ~18 focused modules
- Separate shared types from implementation
- 155 files changed, 60+ new files created
- All typecheck clean, 61 tests pass
This commit is contained in:
YeonGyu-Kim
2026-02-08 13:57:26 +09:00
parent 7a15c1f3f9
commit df03300211
156 changed files with 7280 additions and 6771 deletions
+63
View File
@@ -0,0 +1,63 @@
import type { PendingCall } from "./types"
import { existsSync } from "fs"
import { runCommentChecker, getCommentCheckerPath, startBackgroundInit, type HookInput } from "./cli"
let cliPathPromise: Promise<string | null> | null = null
export function initializeCommentCheckerCli(debugLog: (...args: unknown[]) => void): void {
// Start background CLI initialization (may trigger lazy download)
startBackgroundInit()
cliPathPromise = getCommentCheckerPath()
cliPathPromise
.then((path) => {
debugLog("CLI path resolved:", path || "disabled (no binary)")
})
.catch((err) => {
debugLog("CLI path resolution error:", err)
})
}
export function getCommentCheckerCliPathPromise(): Promise<string | null> | null {
return cliPathPromise
}
export async function processWithCli(
input: { tool: string; sessionID: string; callID: string },
pendingCall: PendingCall,
output: { output: string },
cliPath: string,
customPrompt: string | undefined,
debugLog: (...args: unknown[]) => void,
): Promise<void> {
void input
debugLog("using CLI mode with path:", cliPath)
const hookInput: HookInput = {
session_id: pendingCall.sessionID,
tool_name: pendingCall.tool.charAt(0).toUpperCase() + pendingCall.tool.slice(1),
transcript_path: "",
cwd: process.cwd(),
hook_event_name: "PostToolUse",
tool_input: {
file_path: pendingCall.filePath,
content: pendingCall.content,
old_string: pendingCall.oldString,
new_string: pendingCall.newString,
edits: pendingCall.edits,
},
}
const result = await runCommentChecker(hookInput, cliPath, customPrompt)
if (result.hasComments && result.message) {
debugLog("CLI detected comments, appending message")
output.output += `\n\n${result.message}`
} else {
debugLog("CLI: no comments detected")
}
}
export function isCliPathUsable(cliPath: string | null): cliPath is string {
return Boolean(cliPath && existsSync(cliPath))
}
+123
View File
@@ -0,0 +1,123 @@
import type { PendingCall } from "./types"
import type { CommentCheckerConfig } from "../../config/schema"
import { initializeCommentCheckerCli, getCommentCheckerCliPathPromise, isCliPathUsable, processWithCli } from "./cli-runner"
import { registerPendingCall, startPendingCallCleanup, takePendingCall } from "./pending-calls"
import * as fs from "fs"
import { tmpdir } from "os"
import { join } from "path"
const DEBUG = process.env.COMMENT_CHECKER_DEBUG === "1"
const DEBUG_FILE = join(tmpdir(), "comment-checker-debug.log")
function debugLog(...args: unknown[]) {
if (DEBUG) {
const msg = `[${new Date().toISOString()}] [comment-checker:hook] ${args
.map((a) => (typeof a === "object" ? JSON.stringify(a, null, 2) : String(a)))
.join(" ")}\n`
fs.appendFileSync(DEBUG_FILE, msg)
}
}
export function createCommentCheckerHooks(config?: CommentCheckerConfig) {
debugLog("createCommentCheckerHooks called", { config })
startPendingCallCleanup()
initializeCommentCheckerCli(debugLog)
return {
"tool.execute.before": async (
input: { tool: string; sessionID: string; callID: string },
output: { args: Record<string, unknown> },
): Promise<void> => {
debugLog("tool.execute.before:", {
tool: input.tool,
callID: input.callID,
args: output.args,
})
const toolLower = input.tool.toLowerCase()
if (toolLower !== "write" && toolLower !== "edit" && toolLower !== "multiedit") {
debugLog("skipping non-write/edit tool:", toolLower)
return
}
const filePath = (output.args.filePath ??
output.args.file_path ??
output.args.path) as string | undefined
const content = output.args.content as string | undefined
const oldString = (output.args.oldString ?? output.args.old_string) as string | undefined
const newString = (output.args.newString ?? output.args.new_string) as string | undefined
const edits = output.args.edits as Array<{ old_string: string; new_string: string }> | undefined
debugLog("extracted filePath:", filePath)
if (!filePath) {
debugLog("no filePath found")
return
}
debugLog("registering pendingCall:", {
callID: input.callID,
filePath,
tool: toolLower,
})
registerPendingCall(input.callID, {
filePath,
content,
oldString: oldString as string | undefined,
newString: newString as string | undefined,
edits,
tool: toolLower as PendingCall["tool"],
sessionID: input.sessionID,
timestamp: Date.now(),
})
},
"tool.execute.after": async (
input: { tool: string; sessionID: string; callID: string },
output: { title: string; output: string; metadata: unknown },
): Promise<void> => {
debugLog("tool.execute.after:", { tool: input.tool, callID: input.callID })
const pendingCall = takePendingCall(input.callID)
if (!pendingCall) {
debugLog("no pendingCall found for:", input.callID)
return
}
debugLog("processing pendingCall:", pendingCall)
// Only skip if the output indicates a tool execution failure
const outputLower = output.output.toLowerCase()
const isToolFailure =
outputLower.includes("error:") ||
outputLower.includes("failed to") ||
outputLower.includes("could not") ||
outputLower.startsWith("error")
if (isToolFailure) {
debugLog("skipping due to tool failure in output")
return
}
try {
// Wait for CLI path resolution
const cliPath = await getCommentCheckerCliPathPromise()
if (!isCliPathUsable(cliPath)) {
// CLI not available - silently skip comment checking
debugLog("CLI not available, skipping comment check")
return
}
// CLI mode only
debugLog("using CLI:", cliPath)
await processWithCli(input, pendingCall, output, cliPath, config?.custom_prompt, debugLog)
} catch (err) {
debugLog("tool.execute.after failed:", err)
}
},
}
}
+1 -171
View File
@@ -1,171 +1 @@
import type { PendingCall } from "./types"
import { runCommentChecker, getCommentCheckerPath, startBackgroundInit, type HookInput } from "./cli"
import type { CommentCheckerConfig } from "../../config/schema"
import * as fs from "fs"
import { existsSync } from "fs"
import { tmpdir } from "os"
import { join } from "path"
const DEBUG = process.env.COMMENT_CHECKER_DEBUG === "1"
const DEBUG_FILE = join(tmpdir(), "comment-checker-debug.log")
function debugLog(...args: unknown[]) {
if (DEBUG) {
const msg = `[${new Date().toISOString()}] [comment-checker:hook] ${args.map(a => typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a)).join(' ')}\n`
fs.appendFileSync(DEBUG_FILE, msg)
}
}
const pendingCalls = new Map<string, PendingCall>()
const PENDING_CALL_TTL = 60_000
let cliPathPromise: Promise<string | null> | null = null
let cleanupIntervalStarted = false
function cleanupOldPendingCalls(): void {
const now = Date.now()
for (const [callID, call] of pendingCalls) {
if (now - call.timestamp > PENDING_CALL_TTL) {
pendingCalls.delete(callID)
}
}
}
export function createCommentCheckerHooks(config?: CommentCheckerConfig) {
debugLog("createCommentCheckerHooks called", { config })
if (!cleanupIntervalStarted) {
cleanupIntervalStarted = true
setInterval(cleanupOldPendingCalls, 10_000)
}
// Start background CLI initialization (may trigger lazy download)
startBackgroundInit()
cliPathPromise = getCommentCheckerPath()
cliPathPromise.then(path => {
debugLog("CLI path resolved:", path || "disabled (no binary)")
}).catch(err => {
debugLog("CLI path resolution error:", err)
})
return {
"tool.execute.before": async (
input: { tool: string; sessionID: string; callID: string },
output: { args: Record<string, unknown> }
): Promise<void> => {
debugLog("tool.execute.before:", { tool: input.tool, callID: input.callID, args: output.args })
const toolLower = input.tool.toLowerCase()
if (toolLower !== "write" && toolLower !== "edit" && toolLower !== "multiedit") {
debugLog("skipping non-write/edit tool:", toolLower)
return
}
const filePath = (output.args.filePath ?? output.args.file_path ?? output.args.path) as string | undefined
const content = output.args.content as string | undefined
const oldString = output.args.oldString ?? output.args.old_string as string | undefined
const newString = output.args.newString ?? output.args.new_string as string | undefined
const edits = output.args.edits as Array<{ old_string: string; new_string: string }> | undefined
debugLog("extracted filePath:", filePath)
if (!filePath) {
debugLog("no filePath found")
return
}
debugLog("registering pendingCall:", { callID: input.callID, filePath, tool: toolLower })
pendingCalls.set(input.callID, {
filePath,
content,
oldString: oldString as string | undefined,
newString: newString as string | undefined,
edits,
tool: toolLower as "write" | "edit" | "multiedit",
sessionID: input.sessionID,
timestamp: Date.now(),
})
},
"tool.execute.after": async (
input: { tool: string; sessionID: string; callID: string },
output: { title: string; output: string; metadata: unknown }
): Promise<void> => {
debugLog("tool.execute.after:", { tool: input.tool, callID: input.callID })
const pendingCall = pendingCalls.get(input.callID)
if (!pendingCall) {
debugLog("no pendingCall found for:", input.callID)
return
}
pendingCalls.delete(input.callID)
debugLog("processing pendingCall:", pendingCall)
// Only skip if the output indicates a tool execution failure
const outputLower = output.output.toLowerCase()
const isToolFailure =
outputLower.includes("error:") ||
outputLower.includes("failed to") ||
outputLower.includes("could not") ||
outputLower.startsWith("error")
if (isToolFailure) {
debugLog("skipping due to tool failure in output")
return
}
try {
// Wait for CLI path resolution
const cliPath = await cliPathPromise
if (!cliPath || !existsSync(cliPath)) {
// CLI not available - silently skip comment checking
debugLog("CLI not available, skipping comment check")
return
}
// CLI mode only
debugLog("using CLI:", cliPath)
await processWithCli(input, pendingCall, output, cliPath, config?.custom_prompt)
} catch (err) {
debugLog("tool.execute.after failed:", err)
}
},
}
}
async function processWithCli(
input: { tool: string; sessionID: string; callID: string },
pendingCall: PendingCall,
output: { output: string },
cliPath: string,
customPrompt?: string
): Promise<void> {
debugLog("using CLI mode with path:", cliPath)
const hookInput: HookInput = {
session_id: pendingCall.sessionID,
tool_name: pendingCall.tool.charAt(0).toUpperCase() + pendingCall.tool.slice(1),
transcript_path: "",
cwd: process.cwd(),
hook_event_name: "PostToolUse",
tool_input: {
file_path: pendingCall.filePath,
content: pendingCall.content,
old_string: pendingCall.oldString,
new_string: pendingCall.newString,
edits: pendingCall.edits,
},
}
const result = await runCommentChecker(hookInput, cliPath, customPrompt)
if (result.hasComments && result.message) {
debugLog("CLI detected comments, appending message")
output.output += `\n\n${result.message}`
} else {
debugLog("CLI: no comments detected")
}
}
export { createCommentCheckerHooks } from "./hook"
@@ -0,0 +1,32 @@
import type { PendingCall } from "./types"
const pendingCalls = new Map<string, PendingCall>()
const PENDING_CALL_TTL = 60_000
let cleanupIntervalStarted = false
function cleanupOldPendingCalls(): void {
const now = Date.now()
for (const [callID, call] of pendingCalls) {
if (now - call.timestamp > PENDING_CALL_TTL) {
pendingCalls.delete(callID)
}
}
}
export function startPendingCallCleanup(): void {
if (cleanupIntervalStarted) return
cleanupIntervalStarted = true
setInterval(cleanupOldPendingCalls, 10_000)
}
export function registerPendingCall(callID: string, pendingCall: PendingCall): void {
pendingCalls.set(callID, pendingCall)
}
export function takePendingCall(callID: string): PendingCall | undefined {
const pendingCall = pendingCalls.get(callID)
if (!pendingCall) return undefined
pendingCalls.delete(callID)
return pendingCall
}