feat(hooks): add comment-checker hook for detecting unnecessary comments

- Port Go comment-checker to TypeScript using web-tree-sitter
- Support 38 programming languages via tree-sitter-wasms
- Filter out valid comments: BDD patterns, lint directives, docstrings, shebangs
- Integrate with OpenCode's tool.execute.before/after hooks
- Attach feedback to Write/Edit/MultiEdit tool output
This commit is contained in:
YeonGyu-Kim
2025-12-05 02:49:47 +09:00
parent 22acb0def1
commit 02a9402472
15 changed files with 655 additions and 4 deletions

View File

@@ -5,8 +5,12 @@
"": {
"name": "oh-my-opencode",
"dependencies": {
"@ast-grep/cli": "^0.40.0",
"@ast-grep/napi": "^0.40.0",
"@opencode-ai/plugin": "^1.0.7",
"tree-sitter-wasms": "^0.1.12",
"web-tree-sitter": "^0.24.7",
"zod": "^4.1.8",
},
"devDependencies": {
"bun-types": "latest",
@@ -17,7 +21,26 @@
},
},
},
"trustedDependencies": [
"@ast-grep/cli",
],
"packages": {
"@ast-grep/cli": ["@ast-grep/cli@0.40.0", "", { "dependencies": { "detect-libc": "2.1.2" }, "optionalDependencies": { "@ast-grep/cli-darwin-arm64": "0.40.0", "@ast-grep/cli-darwin-x64": "0.40.0", "@ast-grep/cli-linux-arm64-gnu": "0.40.0", "@ast-grep/cli-linux-x64-gnu": "0.40.0", "@ast-grep/cli-win32-arm64-msvc": "0.40.0", "@ast-grep/cli-win32-ia32-msvc": "0.40.0", "@ast-grep/cli-win32-x64-msvc": "0.40.0" }, "bin": { "sg": "sg", "ast-grep": "ast-grep" } }, "sha512-L8AkflsfI2ZP70yIdrwqvjR02ScCuRmM/qNGnJWUkOFck+e6gafNVJ4e4jjGQlEul+dNdBpx36+O2Op629t47A=="],
"@ast-grep/cli-darwin-arm64": ["@ast-grep/cli-darwin-arm64@0.40.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UehY2MMUkdJbsriP7NKc6+uojrqPn7d1Cl0em+WAkee7Eij81VdyIjRsRxtZSLh440ZWQBHI3PALZ9RkOO8pKQ=="],
"@ast-grep/cli-darwin-x64": ["@ast-grep/cli-darwin-x64@0.40.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-RFDJ2ZxUbT0+grntNlOLJx7wa9/ciVCeaVtQpQy8WJJTvXvkY0etl8Qlh2TmO2x2yr+i0Z6aMJi4IG/Yx5ghTQ=="],
"@ast-grep/cli-linux-arm64-gnu": ["@ast-grep/cli-linux-arm64-gnu@0.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-4p55gnTQ1mMFCyqjtM7bH9SB9r16mkwXtUcJQGX1YgFG4WD+QG8rC4GwSuNNZcdlYaOQuTWrgUEQ9z5K06UXfg=="],
"@ast-grep/cli-linux-x64-gnu": ["@ast-grep/cli-linux-x64-gnu@0.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-u2MXFceuwvrO+OQ6zFGoJ6wbATXn46HWwW79j4UPrXYJzVl97jRyjJOIQTJOzTflsk02fjP98DQkfvbXt2dl3Q=="],
"@ast-grep/cli-win32-arm64-msvc": ["@ast-grep/cli-win32-arm64-msvc@0.40.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-E/I1xpF/RQL2fo1CQsQfTxyDLnChsbZ+ERrQHKuF1FI4WrkaPOBibpqda60QgVmUcgOGZyZ/GRb3iKEVWPsQNQ=="],
"@ast-grep/cli-win32-ia32-msvc": ["@ast-grep/cli-win32-ia32-msvc@0.40.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-9h12OQu1BR0GxHEtT+Z4QkJk3LLWLiKwjBkjXUGlASHYDPTyLcs85KwDLeFHs4BwarF8TDdF+KySvB9WPGl/nQ=="],
"@ast-grep/cli-win32-x64-msvc": ["@ast-grep/cli-win32-x64-msvc@0.40.0", "", { "os": "win32", "cpu": "x64" }, "sha512-n2+3WynEWFHhXg6KDgjwWQ0UEtIvqUITFbKEk5cDkUYrzYhg/A6kj0qauPwRbVMoJms49vtsNpLkzzqyunio5g=="],
"@ast-grep/napi": ["@ast-grep/napi@0.40.0", "", { "optionalDependencies": { "@ast-grep/napi-darwin-arm64": "0.40.0", "@ast-grep/napi-darwin-x64": "0.40.0", "@ast-grep/napi-linux-arm64-gnu": "0.40.0", "@ast-grep/napi-linux-arm64-musl": "0.40.0", "@ast-grep/napi-linux-x64-gnu": "0.40.0", "@ast-grep/napi-linux-x64-musl": "0.40.0", "@ast-grep/napi-win32-arm64-msvc": "0.40.0", "@ast-grep/napi-win32-ia32-msvc": "0.40.0", "@ast-grep/napi-win32-x64-msvc": "0.40.0" } }, "sha512-tq6nO/8KwUF/mHuk1ECaAOSOlz2OB/PmygnvprJzyAHGRVzdcffblaOOWe90M9sGz5MAasXoF+PTcayQj9TKKA=="],
"@ast-grep/napi-darwin-arm64": ["@ast-grep/napi-darwin-arm64@0.40.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZMjl5yLhKjxdwbqEEdMizgQdWH2NrWsM6Px+JuGErgCDe6Aedq9yurEPV7veybGdLVJQhOah6htlSflXxjHnYA=="],
@@ -70,10 +93,16 @@
"bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"tree-sitter-wasms": ["tree-sitter-wasms@0.1.13", "", { "dependencies": { "tree-sitter-wasms": "^0.1.11" } }, "sha512-wT+cR6DwaIz80/vho3AvSF0N4txuNx/5bcRKoXouOfClpxh/qqrF4URNLQXbbt8MaAxeksZcZd1j8gcGjc+QxQ=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"web-tree-sitter": ["web-tree-sitter@0.24.7", "", {}, "sha512-CdC/TqVFbXqR+C51v38hv6wOPatKEUGxa39scAeFSm98wIhZxAYonhRQPSMmfZ2w7JDI0zQDdzdmgtNk06/krQ=="],
"zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="],
}
}

View File

@@ -12,10 +12,12 @@
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"./schema.json": "./dist/oh-my-opencode.schema.json"
},
"scripts": {
"build": "bun build src/index.ts --outdir dist --target bun --format esm --external @ast-grep/napi && tsc --emitDeclarationOnly",
"build": "bun build src/index.ts --outdir dist --target bun --format esm --external @ast-grep/napi && tsc --emitDeclarationOnly && bun run build:schema",
"build:schema": "bun run script/build-schema.ts",
"clean": "rm -rf dist",
"prepublishOnly": "bun run clean && bun run build",
"typecheck": "tsc --noEmit"
@@ -40,8 +42,12 @@
},
"homepage": "https://github.com/code-yeongyu/oh-my-opencode#readme",
"dependencies": {
"@ast-grep/cli": "^0.40.0",
"@ast-grep/napi": "^0.40.0",
"@opencode-ai/plugin": "^1.0.7"
"@opencode-ai/plugin": "^1.0.7",
"tree-sitter-wasms": "^0.1.12",
"web-tree-sitter": "^0.24.7",
"zod": "^4.1.8"
},
"devDependencies": {
"bun-types": "latest",
@@ -49,5 +55,8 @@
},
"peerDependencies": {
"bun": ">=1.0.0"
}
},
"trustedDependencies": [
"@ast-grep/cli"
]
}

View File

@@ -0,0 +1,204 @@
import type { LanguageConfig } from "./types"
export const EXTENSION_TO_LANGUAGE: Record<string, string> = {
py: "python",
js: "javascript",
jsx: "javascript",
ts: "typescript",
tsx: "tsx",
go: "golang",
java: "java",
kt: "kotlin",
scala: "scala",
c: "c",
h: "c",
cpp: "cpp",
cc: "cpp",
cxx: "cpp",
hpp: "cpp",
rs: "rust",
rb: "ruby",
sh: "bash",
bash: "bash",
cs: "csharp",
swift: "swift",
ex: "elixir",
exs: "elixir",
lua: "lua",
php: "php",
ml: "ocaml",
mli: "ocaml",
sql: "sql",
html: "html",
htm: "html",
css: "css",
yaml: "yaml",
yml: "yaml",
toml: "toml",
hcl: "hcl",
tf: "hcl",
dockerfile: "dockerfile",
proto: "protobuf",
svelte: "svelte",
elm: "elm",
groovy: "groovy",
cue: "cue",
}
export const QUERY_TEMPLATES: Record<string, string> = {
python: "(comment) @comment",
javascript: "(comment) @comment",
typescript: "(comment) @comment",
tsx: "(comment) @comment",
golang: "(comment) @comment",
rust: `
(line_comment) @comment
(block_comment) @comment
`,
kotlin: `
(line_comment) @comment
(multiline_comment) @comment
`,
java: `
(line_comment) @comment
(block_comment) @comment
`,
c: "(comment) @comment",
cpp: "(comment) @comment",
csharp: "(comment) @comment",
ruby: "(comment) @comment",
bash: "(comment) @comment",
swift: "(comment) @comment",
elixir: "(comment) @comment",
lua: "(comment) @comment",
php: "(comment) @comment",
ocaml: "(comment) @comment",
sql: "(comment) @comment",
html: "(comment) @comment",
css: "(comment) @comment",
yaml: "(comment) @comment",
toml: "(comment) @comment",
hcl: "(comment) @comment",
dockerfile: "(comment) @comment",
protobuf: "(comment) @comment",
svelte: "(comment) @comment",
elm: "(comment) @comment",
groovy: "(comment) @comment",
cue: "(comment) @comment",
scala: "(comment) @comment",
}
export const DOCSTRING_QUERIES: Record<string, string> = {
python: `
(module . (expression_statement (string) @docstring))
(class_definition body: (block . (expression_statement (string) @docstring)))
(function_definition body: (block . (expression_statement (string) @docstring)))
`,
javascript: `
(comment) @jsdoc
(#match? @jsdoc "^/\\\\*\\\\*")
`,
typescript: `
(comment) @jsdoc
(#match? @jsdoc "^/\\\\*\\\\*")
`,
tsx: `
(comment) @jsdoc
(#match? @jsdoc "^/\\\\*\\\\*")
`,
java: `
(comment) @javadoc
(#match? @javadoc "^/\\\\*\\\\*")
`,
}
export const BDD_KEYWORDS = new Set([
"given",
"when",
"then",
"arrange",
"act",
"assert",
"when & then",
"when&then",
])
export const TYPE_CHECKER_PREFIXES = [
"type:",
"noqa",
"pyright:",
"ruff:",
"mypy:",
"pylint:",
"flake8:",
"pyre:",
"pytype:",
"eslint-disable",
"eslint-enable",
"eslint-ignore",
"prettier-ignore",
"ts-ignore",
"ts-expect-error",
"ts-nocheck",
"clippy::",
"allow(",
"deny(",
"warn(",
"forbid(",
"nolint",
"go:generate",
"go:build",
"go:embed",
"coverage:",
"c8 ignore",
"biome-ignore",
"region",
"endregion",
]
export const HOOK_MESSAGE_HEADER = `COMMENT/DOCSTRING DETECTED - IMMEDIATE ACTION REQUIRED
Your recent changes contain comments or docstrings, which triggered this hook.
You need to take immediate action. You must follow the conditions below.
(Listed in priority order - you must always act according to this priority order)
CRITICAL WARNING: This hook message MUST NEVER be ignored, even if you receive it multiple times.
You MUST provide corresponding explanation or action for EACH occurrence of this message.
Ignoring this message or failing to respond appropriately is strictly prohibited.
PRIORITY-BASED ACTION GUIDELINES:
1. This is a comment/docstring that already existed before
\t-> Explain to the user that this is an existing comment/docstring and proceed (justify it)
2. This is a newly written comment: but it's in given, when, then format
\t-> Tell the user it's a BDD comment and proceed (justify it)
\t-> Note: This applies to comments only, not docstrings
3. This is a newly written comment/docstring: but it's a necessary comment/docstring
\t-> Tell the user why this comment/docstring is absolutely necessary and proceed (justify it)
\t-> Examples of necessary comments: complex algorithms, security-related, performance optimization, regex, mathematical formulas
\t-> Examples of necessary docstrings: public API documentation, complex module/class interfaces
\t-> IMPORTANT: Most docstrings are unnecessary if the code is self-explanatory. Only keep truly essential ones.
4. This is a newly written comment/docstring: but it's an unnecessary comment/docstring
\t-> Apologize to the user and remove the comment/docstring.
\t-> Make the code itself clearer so it can be understood without comments/docstrings.
\t-> For verbose docstrings: refactor code to be self-documenting instead of adding lengthy explanations.
MANDATORY REQUIREMENT: You must acknowledge this hook message and take one of the above actions.
Review in the above priority order and take the corresponding action EVERY TIME this appears.
Detected comments/docstrings:
`
export function getLanguageByExtension(filePath: string): string | null {
const lastDot = filePath.lastIndexOf(".")
if (lastDot === -1) {
const baseName = filePath.split("/").pop()?.toLowerCase()
if (baseName === "dockerfile") return "dockerfile"
return null
}
const ext = filePath.slice(lastDot + 1).toLowerCase()
return EXTENSION_TO_LANGUAGE[ext] ?? null
}

View File

@@ -0,0 +1,142 @@
import type { CommentInfo, CommentType } from "./types"
import { getLanguageByExtension, QUERY_TEMPLATES, DOCSTRING_QUERIES } from "./constants"
export function isSupportedFile(filePath: string): boolean {
return getLanguageByExtension(filePath) !== null
}
function determineCommentType(text: string, nodeType: string): CommentType {
const stripped = text.trim()
if (nodeType === "line_comment") {
return "line"
}
if (nodeType === "block_comment" || nodeType === "multiline_comment") {
return "block"
}
if (stripped.startsWith('"""') || stripped.startsWith("'''")) {
return "docstring"
}
if (stripped.startsWith("//") || stripped.startsWith("#")) {
return "line"
}
if (stripped.startsWith("/*") || stripped.startsWith("<!--") || stripped.startsWith("--")) {
return "block"
}
return "line"
}
export async function detectComments(
filePath: string,
content: string,
includeDocstrings = true
): Promise<CommentInfo[]> {
const langName = getLanguageByExtension(filePath)
if (!langName) {
return []
}
const queryPattern = QUERY_TEMPLATES[langName]
if (!queryPattern) {
return []
}
try {
const Parser = (await import("web-tree-sitter")).default
await Parser.init()
const parser = new Parser()
let wasmPath: string
try {
const wasmModule = await import(`tree-sitter-wasms/out/tree-sitter-${langName}.wasm`)
wasmPath = wasmModule.default
} catch {
const languageMap: Record<string, string> = {
golang: "go",
csharp: "c_sharp",
cpp: "cpp",
}
const mappedLang = languageMap[langName] || langName
try {
const wasmModule = await import(`tree-sitter-wasms/out/tree-sitter-${mappedLang}.wasm`)
wasmPath = wasmModule.default
} catch {
return []
}
}
const language = await Parser.Language.load(wasmPath)
parser.setLanguage(language)
const tree = parser.parse(content)
const comments: CommentInfo[] = []
const query = language.query(queryPattern)
const matches = query.matches(tree.rootNode)
for (const match of matches) {
for (const capture of match.captures) {
const node = capture.node
const text = node.text
const lineNumber = node.startPosition.row + 1
const commentType = determineCommentType(text, node.type)
const isDocstring = commentType === "docstring"
if (isDocstring && !includeDocstrings) {
continue
}
comments.push({
text,
lineNumber,
filePath,
commentType,
isDocstring,
})
}
}
if (includeDocstrings) {
const docQuery = DOCSTRING_QUERIES[langName]
if (docQuery) {
try {
const docQueryObj = language.query(docQuery)
const docMatches = docQueryObj.matches(tree.rootNode)
for (const match of docMatches) {
for (const capture of match.captures) {
const node = capture.node
const text = node.text
const lineNumber = node.startPosition.row + 1
const alreadyAdded = comments.some(
(c) => c.lineNumber === lineNumber && c.text === text
)
if (!alreadyAdded) {
comments.push({
text,
lineNumber,
filePath,
commentType: "docstring",
isDocstring: true,
})
}
}
}
} catch {}
}
}
comments.sort((a, b) => a.lineNumber - b.lineNumber)
return comments
} catch {
return []
}
}

View File

@@ -0,0 +1,21 @@
import type { CommentInfo, FilterResult } from "../types"
import { BDD_KEYWORDS } from "../constants"
function stripCommentPrefix(text: string): string {
let stripped = text.trim().toLowerCase()
const prefixes = ["#", "//", "--", "/*", "*/"]
for (const prefix of prefixes) {
if (stripped.startsWith(prefix)) {
stripped = stripped.slice(prefix.length).trim()
}
}
return stripped
}
export function filterBddComments(comment: CommentInfo): FilterResult {
const normalized = stripCommentPrefix(comment.text)
if (BDD_KEYWORDS.has(normalized)) {
return { shouldSkip: true, reason: `BDD keyword: ${normalized}` }
}
return { shouldSkip: false }
}

View File

@@ -0,0 +1,24 @@
import type { CommentInfo, FilterResult } from "../types"
import { TYPE_CHECKER_PREFIXES } from "../constants"
function stripCommentPrefix(text: string): string {
let stripped = text.trim().toLowerCase()
const prefixes = ["#", "//", "/*", "--"]
for (const prefix of prefixes) {
if (stripped.startsWith(prefix)) {
stripped = stripped.slice(prefix.length).trim()
}
}
stripped = stripped.replace(/^@/, "")
return stripped
}
export function filterDirectiveComments(comment: CommentInfo): FilterResult {
const normalized = stripCommentPrefix(comment.text)
for (const prefix of TYPE_CHECKER_PREFIXES) {
if (normalized.startsWith(prefix.toLowerCase())) {
return { shouldSkip: true, reason: `Directive: ${prefix}` }
}
}
return { shouldSkip: false }
}

View File

@@ -0,0 +1,12 @@
import type { CommentInfo, FilterResult } from "../types"
export function filterDocstringComments(comment: CommentInfo): FilterResult {
if (comment.isDocstring) {
return { shouldSkip: true, reason: "Docstring" }
}
const trimmed = comment.text.trimStart()
if (trimmed.startsWith("/**")) {
return { shouldSkip: true, reason: "JSDoc/PHPDoc" }
}
return { shouldSkip: false }
}

View File

@@ -0,0 +1,26 @@
import type { CommentInfo, CommentFilter } from "../types"
import { filterBddComments } from "./bdd"
import { filterDirectiveComments } from "./directive"
import { filterDocstringComments } from "./docstring"
import { filterShebangComments } from "./shebang"
export { filterBddComments, filterDirectiveComments, filterDocstringComments, filterShebangComments }
const ALL_FILTERS: CommentFilter[] = [
filterShebangComments,
filterBddComments,
filterDirectiveComments,
filterDocstringComments,
]
export function applyFilters(comments: CommentInfo[]): CommentInfo[] {
return comments.filter((comment) => {
for (const filter of ALL_FILTERS) {
const result = filter(comment)
if (result.shouldSkip) {
return false
}
}
return true
})
}

View File

@@ -0,0 +1,9 @@
import type { CommentInfo, FilterResult } from "../types"
export function filterShebangComments(comment: CommentInfo): FilterResult {
const trimmed = comment.text.trimStart()
if (trimmed.startsWith("#!")) {
return { shouldSkip: true, reason: "Shebang" }
}
return { shouldSkip: false }
}

View File

@@ -0,0 +1,101 @@
import type { PendingCall, FileComments } from "./types"
import { detectComments, isSupportedFile } from "./detector"
import { applyFilters } from "./filters"
import { formatHookMessage } from "./output"
const pendingCalls = new Map<string, PendingCall>()
const PENDING_CALL_TTL = 60_000
function cleanupOldPendingCalls(): void {
const now = Date.now()
for (const [callID, call] of pendingCalls) {
if (now - call.timestamp > PENDING_CALL_TTL) {
pendingCalls.delete(callID)
}
}
}
setInterval(cleanupOldPendingCalls, 10_000)
export function createCommentCheckerHooks() {
return {
"tool.execute.before": async (
input: { tool: string; sessionID: string; callID: string },
output: { args: Record<string, unknown> }
): Promise<void> => {
const toolLower = input.tool.toLowerCase()
if (toolLower !== "write" && toolLower !== "edit" && toolLower !== "multiedit") {
return
}
const filePath = (output.args.filePath ?? output.args.file_path) as string | undefined
const content = output.args.content as string | undefined
if (!filePath) {
return
}
if (!isSupportedFile(filePath)) {
return
}
pendingCalls.set(input.callID, {
filePath,
content,
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> => {
const pendingCall = pendingCalls.get(input.callID)
if (!pendingCall) {
return
}
pendingCalls.delete(input.callID)
if (output.output.toLowerCase().includes("error")) {
return
}
try {
let content: string
if (pendingCall.content) {
content = pendingCall.content
} else {
const file = Bun.file(pendingCall.filePath)
content = await file.text()
}
const rawComments = await detectComments(pendingCall.filePath, content)
const filteredComments = applyFilters(rawComments)
if (filteredComments.length === 0) {
return
}
const fileComments: FileComments[] = [
{
filePath: pendingCall.filePath,
comments: filteredComments,
},
]
const message = formatHookMessage(fileComments)
output.output += `\n\n${message}`
} catch {}
},
}
}
export * from "./types"
export * from "./constants"
export * from "./detector"
export * from "./filters"
export * from "./output"

View File

@@ -0,0 +1,11 @@
import type { FileComments } from "../types"
import { HOOK_MESSAGE_HEADER } from "../constants"
import { buildCommentsXml } from "./xml-builder"
export function formatHookMessage(fileCommentsList: FileComments[]): string {
if (fileCommentsList.length === 0) {
return ""
}
const xml = buildCommentsXml(fileCommentsList)
return `${HOOK_MESSAGE_HEADER}${xml}\n`
}

View File

@@ -0,0 +1,2 @@
export { buildCommentsXml } from "./xml-builder"
export { formatHookMessage } from "./formatter"

View File

@@ -0,0 +1,24 @@
import type { FileComments } from "../types"
function escapeXml(text: string): string {
return text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;")
}
export function buildCommentsXml(fileCommentsList: FileComments[]): string {
const lines: string[] = []
for (const fc of fileCommentsList) {
lines.push(`<comments file="${escapeXml(fc.filePath)}">`)
for (const comment of fc.comments) {
lines.push(`\t<comment line-number="${comment.lineNumber}">${escapeXml(comment.text)}</comment>`)
}
lines.push(`</comments>`)
}
return lines.join("\n")
}

View File

@@ -0,0 +1,36 @@
export type CommentType = "line" | "block" | "docstring"
export interface CommentInfo {
text: string
lineNumber: number
filePath: string
commentType: CommentType
isDocstring: boolean
metadata?: Record<string, string>
}
export interface LanguageConfig {
extensions: string[]
commentQuery: string
docstringQuery?: string
}
export interface PendingCall {
filePath: string
content?: string
tool: "write" | "edit" | "multiedit"
sessionID: string
timestamp: number
}
export interface FileComments {
filePath: string
comments: CommentInfo[]
}
export interface FilterResult {
shouldSkip: boolean
reason?: string
}
export type CommentFilter = (comment: CommentInfo) => FilterResult

View File

@@ -2,3 +2,4 @@ export * from "./todo-continuation-enforcer"
export * from "./context-window-monitor"
export * from "./session-notification"
export * from "./session-recovery"
export * from "./comment-checker"