chore: remove 1,152 lines of verified dead code (#874)

* chore(deps): remove unused dependencies

Removed @openauthjs/openauth, hono, open, and xdg-basedir - none are imported in src/

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* chore(cleanup): remove unused agent prompts and tool files

Deleted:
- src/agents/build-prompt.ts (exports never imported)
- src/agents/plan-prompt.ts (exports never imported)
- src/tools/ast-grep/napi.ts (never imported)
- src/tools/interactive-bash/types.ts (never imported)

Verified by: LSP FindReferences + explore agents

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* chore(hooks): remove unused comment-checker filters

Deleted entire filters/ directory:
- filters/bdd.ts
- filters/directive.ts
- filters/docstring.ts
- filters/shebang.ts
- filters/index.ts

Not used by main hook (cli.ts uses external binary instead)

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* chore(hooks): remove unused comment-checker output and constants

Deleted:
- output/formatter.ts
- output/xml-builder.ts
- output/index.ts
- constants.ts

All 0 external imports - migrated to external binary

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* chore(hooks): remove unused pruning subsystem

Deleted pruning subsystem (dependency order):
- pruning-purge-errors.ts
- pruning-storage.ts
- pruning-supersede.ts
- pruning-executor.ts

Not imported by main recovery hook

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* chore(hooks): remove unused createBackgroundCompactionHook export

Removed export from index.ts - never imported in src/index.ts

Verified by: LSP FindReferences (only 2 refs: definition + barrel export)

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

---------

Co-authored-by: justsisyphus <sisyphus-dev-ai@users.noreply.github.com>
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
justsisyphus 2026-01-17 21:12:55 +09:00 committed by GitHub
parent d13e8411f0
commit b643dd4f19
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 0 additions and 1156 deletions

View File

@ -56,18 +56,14 @@
"@clack/prompts": "^0.11.0",
"@code-yeongyu/comment-checker": "^0.6.1",
"@modelcontextprotocol/sdk": "^1.25.1",
"@openauthjs/openauth": "^0.4.3",
"@opencode-ai/plugin": "^1.1.19",
"@opencode-ai/sdk": "^1.1.19",
"commander": "^14.0.2",
"detect-libc": "^2.0.0",
"hono": "^4.10.4",
"js-yaml": "^4.1.1",
"jsonc-parser": "^3.3.1",
"open": "^11.0.0",
"picocolors": "^1.1.1",
"picomatch": "^4.0.2",
"xdg-basedir": "^5.1.0",
"zod": "^4.1.8"
},
"devDependencies": {

View File

@ -1,68 +0,0 @@
/**
* OpenCode's default build agent system prompt.
*
* This prompt enables FULL EXECUTION mode for the build agent, allowing file
* modifications, command execution, and system changes while focusing on
* implementation and execution.
*
* Inspired by OpenCode's build agent behavior.
*
* @see https://github.com/sst/opencode/blob/6f9bea4e1f3d139feefd0f88de260b04f78caaef/packages/opencode/src/session/prompt/build-switch.txt
* @see https://github.com/sst/opencode/blob/6f9bea4e1f3d139feefd0f88de260b04f78caaef/packages/opencode/src/agent/agent.ts#L118-L125
*/
export const BUILD_SYSTEM_PROMPT = `<system-reminder>
# Build Mode - System Reminder
BUILD MODE ACTIVE - you are in EXECUTION phase. Your responsibility is to:
- Implement features and make code changes
- Execute commands and run tests
- Fix bugs and refactor code
- Deploy and build systems
- Make all necessary file modifications
You have FULL permissions to edit files, run commands, and make system changes.
This is the implementation phase - execute decisively and thoroughly.
---
## Responsibility
Your current responsibility is to implement, build, and execute. You should:
- Write and modify code to accomplish the user's goals
- Run tests and builds to verify your changes
- Fix errors and issues that arise
- Use all available tools to complete the task efficiently
- Delegate to specialized agents when appropriate for better results
**NOTE:** You should ask the user for clarification when requirements are ambiguous,
but once the path is clear, execute confidently. The goal is to deliver working,
tested, production-ready solutions.
---
## Important
The user wants you to execute and implement. You SHOULD make edits, run necessary
tools, and make changes to accomplish the task. Use your full capabilities to
deliver excellent results.
</system-reminder>
`
/**
* OpenCode's default build agent permission configuration.
*
* Allows the build agent full execution permissions:
* - edit: "ask" - Can modify files with confirmation
* - bash: "ask" - Can execute commands with confirmation
* - webfetch: "allow" - Can fetch web content
*
* This provides balanced permissions - powerful but with safety checks.
*
* @see https://github.com/sst/opencode/blob/6f9bea4e1f3d139feefd0f88de260b04f78caaef/packages/opencode/src/agent/agent.ts#L57-L68
* @see https://github.com/sst/opencode/blob/6f9bea4e1f3d139feefd0f88de260b04f78caaef/packages/opencode/src/agent/agent.ts#L118-L125
*/
export const BUILD_PERMISSION = {
edit: "ask" as const,
bash: "ask" as const,
webfetch: "allow" as const,
}

View File

@ -1,162 +0,0 @@
/**
* OhMyOpenCode Plan Agent System Prompt
*
* A streamlined planner that:
* - SKIPS user dialogue/Q&A (no user questioning)
* - KEEPS context gathering via explore/librarian agents
* - Uses Metis ONLY for AI slop guardrails
* - Outputs plan directly to user (no file creation)
*
* For the full Prometheus experience with user dialogue, use "Prometheus (Planner)" agent.
*/
export const PLAN_SYSTEM_PROMPT = `<system-reminder>
# Plan Mode - System Reminder
## ABSOLUTE CONSTRAINTS (NON-NEGOTIABLE)
### 1. NO IMPLEMENTATION - PLANNING ONLY
You are a PLANNER, NOT an executor. You must NEVER:
- Start implementing ANY task
- Write production code
- Execute the work yourself
- "Get started" on any implementation
- Begin coding even if user asks
Your ONLY job is to CREATE THE PLAN. Implementation is done by OTHER agents AFTER you deliver the plan.
If user says "implement this" or "start working", you respond: "I am the plan agent. I will create a detailed work plan for execution by other agents."
### 2. READ-ONLY FILE ACCESS
You may NOT create or edit any files. You can only READ files for context gathering.
- Reading files for analysis: ALLOWED
- ANY file creation or edits: STRICTLY FORBIDDEN
### 3. PLAN OUTPUT
Your deliverable is a structured work plan delivered directly in your response.
You do NOT deliver code. You do NOT deliver implementations. You deliver PLANS.
ZERO EXCEPTIONS to these constraints.
</system-reminder>
You are a strategic planner. You bring foresight and structure to complex work.
## Your Mission
Create structured work plans that enable efficient execution by AI agents.
## Workflow (Execute Phases Sequentially)
### Phase 1: Context Gathering (Parallel)
Launch **in parallel**:
**Explore agents** (3-5 parallel):
\`\`\`
Task(subagent_type="explore", prompt="Find [specific aspect] in codebase...")
\`\`\`
- Similar implementations
- Project patterns and conventions
- Related test files
- Architecture/structure
**Librarian agents** (2-3 parallel):
\`\`\`
Task(subagent_type="librarian", prompt="Find documentation for [library/pattern]...")
\`\`\`
- Framework docs for relevant features
- Best practices for the task type
### Phase 2: AI Slop Guardrails
Call \`Metis (Plan Consultant)\` with gathered context to identify guardrails:
\`\`\`
Task(
subagent_type="Metis (Plan Consultant)",
prompt="Based on this context, identify AI slop guardrails:
User Request: {user's original request}
Codebase Context: {findings from Phase 1}
Generate:
1. AI slop patterns to avoid (over-engineering, unnecessary abstractions, verbose comments)
2. Common AI mistakes for this type of task
3. Project-specific conventions that must be followed
4. Explicit 'MUST NOT DO' guardrails"
)
\`\`\`
### Phase 3: Plan Generation
Generate a structured plan with:
1. **Core Objective** - What we're achieving (1-2 sentences)
2. **Concrete Deliverables** - Exact files/endpoints/features
3. **Definition of Done** - Acceptance criteria
4. **Must Have** - Required elements
5. **Must NOT Have** - Forbidden patterns (from Metis guardrails)
6. **Task Breakdown** - Sequential/parallel task flow
7. **References** - Existing code to follow
## Key Principles
1. **Infer intent from context** - Use codebase patterns and common practices
2. **Define concrete deliverables** - Exact outputs, not vague goals
3. **Clarify what NOT to do** - Most important for preventing AI mistakes
4. **References over instructions** - Point to existing code
5. **Verifiable acceptance criteria** - Commands with expected outputs
6. **Implementation + Test = ONE task** - NEVER separate
7. **Parallelizability is MANDATORY** - Enable multi-agent execution
`
/**
* OpenCode's default plan agent permission configuration.
*
* Restricts the plan agent to read-only operations:
* - edit: "deny" - No file modifications allowed
* - bash: Only read-only commands (ls, grep, git log, etc.)
* - webfetch: "allow" - Can fetch web content for research
*
* @see https://github.com/sst/opencode/blob/db2abc1b2c144f63a205f668bd7267e00829d84a/packages/opencode/src/agent/agent.ts#L63-L107
*/
export const PLAN_PERMISSION = {
edit: "deny" as const,
bash: {
"cut*": "allow" as const,
"diff*": "allow" as const,
"du*": "allow" as const,
"file *": "allow" as const,
"find * -delete*": "ask" as const,
"find * -exec*": "ask" as const,
"find * -fprint*": "ask" as const,
"find * -fls*": "ask" as const,
"find * -fprintf*": "ask" as const,
"find * -ok*": "ask" as const,
"find *": "allow" as const,
"git diff*": "allow" as const,
"git log*": "allow" as const,
"git show*": "allow" as const,
"git status*": "allow" as const,
"git branch": "allow" as const,
"git branch -v": "allow" as const,
"grep*": "allow" as const,
"head*": "allow" as const,
"less*": "allow" as const,
"ls*": "allow" as const,
"more*": "allow" as const,
"pwd*": "allow" as const,
"rg*": "allow" as const,
"sort --output=*": "ask" as const,
"sort -o *": "ask" as const,
"sort*": "allow" as const,
"stat*": "allow" as const,
"tail*": "allow" as const,
"tree -o *": "ask" as const,
"tree*": "allow" as const,
"uniq*": "allow" as const,
"wc*": "allow" as const,
"whereis*": "allow" as const,
"which*": "allow" as const,
"*": "ask" as const,
},
webfetch: "allow" as const,
}

View File

@ -1,125 +0,0 @@
import type { DynamicContextPruningConfig } from "../../config"
import type { PruningState, PruningResult } from "./pruning-types"
import { executeDeduplication } from "./pruning-deduplication"
import { executeSupersedeWrites } from "./pruning-supersede"
import { executePurgeErrors } from "./pruning-purge-errors"
import { applyPruning } from "./pruning-storage"
import { log } from "../../shared/logger"
const DEFAULT_PROTECTED_TOOLS = new Set([
"task",
"todowrite",
"todoread",
"lsp_rename",
"session_read",
"session_write",
"session_search",
])
function createPruningState(): PruningState {
return {
toolIdsToPrune: new Set<string>(),
currentTurn: 0,
fileOperations: new Map(),
toolSignatures: new Map(),
erroredTools: new Map(),
}
}
export async function executeDynamicContextPruning(
sessionID: string,
config: DynamicContextPruningConfig,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
client: any
): Promise<PruningResult> {
const state = createPruningState()
const protectedTools = new Set([
...DEFAULT_PROTECTED_TOOLS,
...(config.protected_tools || []),
])
log("[pruning-executor] starting DCP", {
sessionID,
notification: config.notification,
turnProtection: config.turn_protection,
})
let dedupCount = 0
let supersedeCount = 0
let purgeCount = 0
if (config.strategies?.deduplication?.enabled !== false) {
dedupCount = executeDeduplication(
sessionID,
state,
{ enabled: true },
protectedTools
)
}
if (config.strategies?.supersede_writes?.enabled !== false) {
supersedeCount = executeSupersedeWrites(
sessionID,
state,
{
enabled: true,
aggressive: config.strategies?.supersede_writes?.aggressive || false,
},
protectedTools
)
}
if (config.strategies?.purge_errors?.enabled !== false) {
purgeCount = executePurgeErrors(
sessionID,
state,
{
enabled: true,
turns: config.strategies?.purge_errors?.turns || 5,
},
protectedTools
)
}
const totalPruned = state.toolIdsToPrune.size
const tokensSaved = await applyPruning(sessionID, state)
log("[pruning-executor] DCP complete", {
totalPruned,
tokensSaved,
deduplication: dedupCount,
supersede: supersedeCount,
purge: purgeCount,
})
const result: PruningResult = {
itemsPruned: totalPruned,
totalTokensSaved: tokensSaved,
strategies: {
deduplication: dedupCount,
supersedeWrites: supersedeCount,
purgeErrors: purgeCount,
},
}
if (config.notification !== "off" && totalPruned > 0) {
const message =
config.notification === "detailed"
? `Pruned ${totalPruned} tool outputs (~${Math.round(tokensSaved / 1000)}k tokens). Dedup: ${dedupCount}, Supersede: ${supersedeCount}, Purge: ${purgeCount}`
: `Pruned ${totalPruned} tool outputs (~${Math.round(tokensSaved / 1000)}k tokens)`
await client.tui
.showToast({
body: {
title: "Dynamic Context Pruning",
message,
variant: "success",
duration: 3000,
},
})
.catch(() => {})
}
return result
}

View File

@ -1,152 +0,0 @@
import { existsSync, readdirSync, readFileSync } from "node:fs"
import { join } from "node:path"
import type { PruningState, ErroredToolCall } from "./pruning-types"
import { estimateTokens } from "./pruning-types"
import { log } from "../../shared/logger"
import { MESSAGE_STORAGE } from "../../features/hook-message-injector"
export interface PurgeErrorsConfig {
enabled: boolean
turns: number
protectedTools?: string[]
}
interface ToolPart {
type: string
callID?: string
tool?: string
state?: {
input?: unknown
output?: string
status?: string
}
}
interface MessagePart {
type: string
parts?: ToolPart[]
}
function getMessageDir(sessionID: string): string | null {
if (!existsSync(MESSAGE_STORAGE)) return null
const directPath = join(MESSAGE_STORAGE, sessionID)
if (existsSync(directPath)) return directPath
for (const dir of readdirSync(MESSAGE_STORAGE)) {
const sessionPath = join(MESSAGE_STORAGE, dir, sessionID)
if (existsSync(sessionPath)) return sessionPath
}
return null
}
function readMessages(sessionID: string): MessagePart[] {
const messageDir = getMessageDir(sessionID)
if (!messageDir) return []
const messages: MessagePart[] = []
try {
const files = readdirSync(messageDir).filter(f => f.endsWith(".json"))
for (const file of files) {
const content = readFileSync(join(messageDir, file), "utf-8")
const data = JSON.parse(content)
if (data.parts) {
messages.push(data)
}
}
} catch {
return []
}
return messages
}
export function executePurgeErrors(
sessionID: string,
state: PruningState,
config: PurgeErrorsConfig,
protectedTools: Set<string>
): number {
if (!config.enabled) return 0
const messages = readMessages(sessionID)
let currentTurn = 0
for (const msg of messages) {
if (!msg.parts) continue
for (const part of msg.parts) {
if (part.type === "step-start") {
currentTurn++
}
}
}
state.currentTurn = currentTurn
let turnCounter = 0
let prunedCount = 0
let tokensSaved = 0
for (const msg of messages) {
if (!msg.parts) continue
for (const part of msg.parts) {
if (part.type === "step-start") {
turnCounter++
continue
}
if (part.type !== "tool" || !part.callID || !part.tool) continue
if (protectedTools.has(part.tool)) continue
if (config.protectedTools?.includes(part.tool)) continue
if (state.toolIdsToPrune.has(part.callID)) continue
if (part.state?.status !== "error") continue
const turnAge = currentTurn - turnCounter
if (turnAge >= config.turns) {
state.toolIdsToPrune.add(part.callID)
prunedCount++
const input = part.state.input
if (input) {
tokensSaved += estimateTokens(JSON.stringify(input))
}
const errorInfo: ErroredToolCall = {
callID: part.callID,
toolName: part.tool,
turn: turnCounter,
errorAge: turnAge,
}
state.erroredTools.set(part.callID, errorInfo)
log("[pruning-purge-errors] pruned old error", {
tool: part.tool,
callID: part.callID,
turn: turnCounter,
errorAge: turnAge,
threshold: config.turns,
})
}
}
}
log("[pruning-purge-errors] complete", {
prunedCount,
tokensSaved,
currentTurn,
threshold: config.turns,
})
return prunedCount
}

View File

@ -1,101 +0,0 @@
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs"
import { join } from "node:path"
import type { PruningState } from "./pruning-types"
import { estimateTokens } from "./pruning-types"
import { log } from "../../shared/logger"
import { MESSAGE_STORAGE } from "../../features/hook-message-injector"
function getMessageDir(sessionID: string): string | null {
if (!existsSync(MESSAGE_STORAGE)) return null
const directPath = join(MESSAGE_STORAGE, sessionID)
if (existsSync(directPath)) return directPath
for (const dir of readdirSync(MESSAGE_STORAGE)) {
const sessionPath = join(MESSAGE_STORAGE, dir, sessionID)
if (existsSync(sessionPath)) return sessionPath
}
return null
}
interface ToolPart {
type: string
callID?: string
tool?: string
state?: {
input?: unknown
output?: string
status?: string
}
}
interface MessageData {
parts?: ToolPart[]
[key: string]: unknown
}
export async function applyPruning(
sessionID: string,
state: PruningState
): Promise<number> {
const messageDir = getMessageDir(sessionID)
if (!messageDir) {
log("[pruning-storage] message dir not found", { sessionID })
return 0
}
let totalTokensSaved = 0
let filesModified = 0
try {
const files = readdirSync(messageDir).filter(f => f.endsWith(".json"))
for (const file of files) {
const filePath = join(messageDir, file)
const content = readFileSync(filePath, "utf-8")
const data: MessageData = JSON.parse(content)
if (!data.parts) continue
let modified = false
for (const part of data.parts) {
if (part.type !== "tool" || !part.callID) continue
if (!state.toolIdsToPrune.has(part.callID)) continue
if (part.state?.input) {
const inputStr = JSON.stringify(part.state.input)
totalTokensSaved += estimateTokens(inputStr)
part.state.input = { __pruned: true, reason: "DCP" }
modified = true
}
if (part.state?.output) {
totalTokensSaved += estimateTokens(part.state.output)
part.state.output = "[Content pruned by Dynamic Context Pruning]"
modified = true
}
}
if (modified) {
writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8")
filesModified++
}
}
} catch (error) {
log("[pruning-storage] error applying pruning", {
sessionID,
error: String(error),
})
}
log("[pruning-storage] applied pruning", {
sessionID,
filesModified,
totalTokensSaved,
})
return totalTokensSaved
}

View File

@ -1,212 +0,0 @@
import { existsSync, readdirSync, readFileSync } from "node:fs"
import { join } from "node:path"
import type { PruningState, FileOperation } from "./pruning-types"
import { estimateTokens } from "./pruning-types"
import { log } from "../../shared/logger"
import { MESSAGE_STORAGE } from "../../features/hook-message-injector"
export interface SupersedeWritesConfig {
enabled: boolean
aggressive: boolean
}
interface ToolPart {
type: string
callID?: string
tool?: string
state?: {
input?: unknown
output?: string
}
}
interface MessagePart {
type: string
parts?: ToolPart[]
}
function getMessageDir(sessionID: string): string | null {
if (!existsSync(MESSAGE_STORAGE)) return null
const directPath = join(MESSAGE_STORAGE, sessionID)
if (existsSync(directPath)) return directPath
for (const dir of readdirSync(MESSAGE_STORAGE)) {
const sessionPath = join(MESSAGE_STORAGE, dir, sessionID)
if (existsSync(sessionPath)) return sessionPath
}
return null
}
function readMessages(sessionID: string): MessagePart[] {
const messageDir = getMessageDir(sessionID)
if (!messageDir) return []
const messages: MessagePart[] = []
try {
const files = readdirSync(messageDir).filter(f => f.endsWith(".json"))
for (const file of files) {
const content = readFileSync(join(messageDir, file), "utf-8")
const data = JSON.parse(content)
if (data.parts) {
messages.push(data)
}
}
} catch {
return []
}
return messages
}
function extractFilePath(toolName: string, input: unknown): string | null {
if (!input || typeof input !== "object") return null
const inputObj = input as Record<string, unknown>
if (toolName === "write" || toolName === "edit" || toolName === "read") {
if (typeof inputObj.filePath === "string") {
return inputObj.filePath
}
}
return null
}
export function executeSupersedeWrites(
sessionID: string,
state: PruningState,
config: SupersedeWritesConfig,
protectedTools: Set<string>
): number {
if (!config.enabled) return 0
const messages = readMessages(sessionID)
const writesByFile = new Map<string, FileOperation[]>()
const readsByFile = new Map<string, number[]>()
let currentTurn = 0
for (const msg of messages) {
if (!msg.parts) continue
for (const part of msg.parts) {
if (part.type === "step-start") {
currentTurn++
continue
}
if (part.type !== "tool" || !part.callID || !part.tool) continue
if (protectedTools.has(part.tool)) continue
if (state.toolIdsToPrune.has(part.callID)) continue
const filePath = extractFilePath(part.tool, part.state?.input)
if (!filePath) continue
if (part.tool === "write" || part.tool === "edit") {
if (!writesByFile.has(filePath)) {
writesByFile.set(filePath, [])
}
writesByFile.get(filePath)!.push({
callID: part.callID,
tool: part.tool,
filePath,
turn: currentTurn,
})
if (!state.fileOperations.has(filePath)) {
state.fileOperations.set(filePath, [])
}
state.fileOperations.get(filePath)!.push({
callID: part.callID,
tool: part.tool,
filePath,
turn: currentTurn,
})
} else if (part.tool === "read") {
if (!readsByFile.has(filePath)) {
readsByFile.set(filePath, [])
}
readsByFile.get(filePath)!.push(currentTurn)
}
}
}
let prunedCount = 0
let tokensSaved = 0
for (const [filePath, writes] of writesByFile) {
const reads = readsByFile.get(filePath) || []
if (config.aggressive) {
for (const write of writes) {
const superseded = reads.some(readTurn => readTurn > write.turn)
if (superseded) {
state.toolIdsToPrune.add(write.callID)
prunedCount++
const input = findToolInput(messages, write.callID)
if (input) {
tokensSaved += estimateTokens(JSON.stringify(input))
}
log("[pruning-supersede] pruned superseded write", {
tool: write.tool,
callID: write.callID,
turn: write.turn,
filePath,
})
}
}
} else {
if (writes.length > 1) {
for (const write of writes.slice(0, -1)) {
const superseded = reads.some(readTurn => readTurn > write.turn)
if (superseded) {
state.toolIdsToPrune.add(write.callID)
prunedCount++
const input = findToolInput(messages, write.callID)
if (input) {
tokensSaved += estimateTokens(JSON.stringify(input))
}
log("[pruning-supersede] pruned superseded write (conservative)", {
tool: write.tool,
callID: write.callID,
turn: write.turn,
filePath,
})
}
}
}
}
}
log("[pruning-supersede] complete", {
prunedCount,
tokensSaved,
filesTracked: writesByFile.size,
mode: config.aggressive ? "aggressive" : "conservative",
})
return prunedCount
}
function findToolInput(messages: MessagePart[], callID: string): unknown | null {
for (const msg of messages) {
if (!msg.parts) continue
for (const part of msg.parts) {
if (part.type === "tool" && part.callID === callID && part.state?.input) {
return part.state.input
}
}
}
return null
}

View File

@ -1,83 +0,0 @@
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.
CODE SMELL WARNING: Using comments as visual separators (e.g., "// =========", "# ---", "// *** Section ***")
is a code smell. If you need separators, your file is too long or poorly organized.
Refactor into smaller modules or use proper code organization instead of comment-based section dividers.
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:
`

View File

@ -1,21 +0,0 @@
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

@ -1,24 +0,0 @@
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

@ -1,12 +0,0 @@
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

@ -1,26 +0,0 @@
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

@ -1,9 +0,0 @@
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

@ -1,11 +0,0 @@
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

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

View File

@ -1,24 +0,0 @@
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

@ -14,7 +14,6 @@ export { createThinkModeHook } from "./think-mode";
export { createClaudeCodeHooksHook } from "./claude-code-hooks";
export { createRulesInjectorHook } from "./rules-injector";
export { createBackgroundNotificationHook } from "./background-notification"
export { createBackgroundCompactionHook } from "./background-compaction"
export { createAutoUpdateCheckerHook } from "./auto-update-checker";
export { createAgentUsageReminderHook } from "./agent-usage-reminder";

View File

@ -1,116 +0,0 @@
import { parse, Lang } from "@ast-grep/napi"
import { NAPI_LANGUAGES } from "./constants"
import type { NapiLanguage, AnalyzeResult, MetaVariable, Range } from "./types"
const LANG_MAP: Record<NapiLanguage, Lang> = {
html: Lang.Html,
javascript: Lang.JavaScript,
tsx: Lang.Tsx,
css: Lang.Css,
typescript: Lang.TypeScript,
}
export function parseCode(code: string, lang: NapiLanguage) {
const parseLang = LANG_MAP[lang]
if (!parseLang) {
const supportedLangs = NAPI_LANGUAGES.join(", ")
throw new Error(
`Unsupported language for NAPI: "${lang}"\n` +
`Supported languages: ${supportedLangs}\n\n` +
`Use ast_grep_search for other languages (25 supported via CLI).`
)
}
return parse(parseLang, code)
}
export function findPattern(root: ReturnType<typeof parseCode>, pattern: string) {
return root.root().findAll(pattern)
}
function nodeToRange(node: ReturnType<ReturnType<typeof parseCode>["root"]>): Range {
const range = node.range()
return {
start: { line: range.start.line, column: range.start.column },
end: { line: range.end.line, column: range.end.column },
}
}
function extractMetaVariablesFromPattern(pattern: string): string[] {
const matches = pattern.match(/\$[A-Z_][A-Z0-9_]*/g) || []
return [...new Set(matches.map((m) => m.slice(1)))]
}
export function extractMetaVariables(
node: ReturnType<ReturnType<typeof parseCode>["root"]>,
pattern: string
): MetaVariable[] {
const varNames = extractMetaVariablesFromPattern(pattern)
const result: MetaVariable[] = []
for (const name of varNames) {
const match = node.getMatch(name)
if (match) {
result.push({
name,
text: match.text(),
kind: String(match.kind()),
})
}
}
return result
}
export function analyzeCode(
code: string,
lang: NapiLanguage,
pattern: string,
shouldExtractMetaVars: boolean
): AnalyzeResult[] {
const root = parseCode(code, lang)
const matches = findPattern(root, pattern)
return matches.map((node) => ({
text: node.text(),
range: nodeToRange(node),
kind: String(node.kind()),
metaVariables: shouldExtractMetaVars ? extractMetaVariables(node, pattern) : [],
}))
}
export function transformCode(
code: string,
lang: NapiLanguage,
pattern: string,
rewrite: string
): { transformed: string; editCount: number } {
const root = parseCode(code, lang)
const matches = findPattern(root, pattern)
if (matches.length === 0) {
return { transformed: code, editCount: 0 }
}
const edits = matches.map((node) => {
const metaVars = extractMetaVariables(node, pattern)
let replacement = rewrite
for (const mv of metaVars) {
replacement = replacement.replace(new RegExp(`\\$${mv.name}`, "g"), mv.text)
}
return node.replace(replacement)
})
const transformed = root.root().commitEdits(edits)
return { transformed, editCount: edits.length }
}
export function getRootInfo(code: string, lang: NapiLanguage): { kind: string; childCount: number } {
const root = parseCode(code, lang)
const rootNode = root.root()
return {
kind: String(rootNode.kind()),
childCount: rootNode.children().length,
}
}

View File

@ -1,3 +0,0 @@
export interface InteractiveBashArgs {
tmux_command: string
}