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:
parent
d13e8411f0
commit
b643dd4f19
@ -56,18 +56,14 @@
|
|||||||
"@clack/prompts": "^0.11.0",
|
"@clack/prompts": "^0.11.0",
|
||||||
"@code-yeongyu/comment-checker": "^0.6.1",
|
"@code-yeongyu/comment-checker": "^0.6.1",
|
||||||
"@modelcontextprotocol/sdk": "^1.25.1",
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
||||||
"@openauthjs/openauth": "^0.4.3",
|
|
||||||
"@opencode-ai/plugin": "^1.1.19",
|
"@opencode-ai/plugin": "^1.1.19",
|
||||||
"@opencode-ai/sdk": "^1.1.19",
|
"@opencode-ai/sdk": "^1.1.19",
|
||||||
"commander": "^14.0.2",
|
"commander": "^14.0.2",
|
||||||
"detect-libc": "^2.0.0",
|
"detect-libc": "^2.0.0",
|
||||||
"hono": "^4.10.4",
|
|
||||||
"js-yaml": "^4.1.1",
|
"js-yaml": "^4.1.1",
|
||||||
"jsonc-parser": "^3.3.1",
|
"jsonc-parser": "^3.3.1",
|
||||||
"open": "^11.0.0",
|
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"picomatch": "^4.0.2",
|
"picomatch": "^4.0.2",
|
||||||
"xdg-basedir": "^5.1.0",
|
|
||||||
"zod": "^4.1.8"
|
"zod": "^4.1.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@ -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,
|
|
||||||
}
|
|
||||||
@ -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,
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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:
|
|
||||||
`
|
|
||||||
@ -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 }
|
|
||||||
}
|
|
||||||
@ -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 }
|
|
||||||
}
|
|
||||||
@ -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 }
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -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 }
|
|
||||||
}
|
|
||||||
@ -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`
|
|
||||||
}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export { buildCommentsXml } from "./xml-builder"
|
|
||||||
export { formatHookMessage } from "./formatter"
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
import type { FileComments } from "../types"
|
|
||||||
|
|
||||||
function escapeXml(text: string): string {
|
|
||||||
return text
|
|
||||||
.replace(/&/g, "&")
|
|
||||||
.replace(/</g, "<")
|
|
||||||
.replace(/>/g, ">")
|
|
||||||
.replace(/"/g, """)
|
|
||||||
.replace(/'/g, "'")
|
|
||||||
}
|
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
@ -14,7 +14,6 @@ export { createThinkModeHook } from "./think-mode";
|
|||||||
export { createClaudeCodeHooksHook } from "./claude-code-hooks";
|
export { createClaudeCodeHooksHook } from "./claude-code-hooks";
|
||||||
export { createRulesInjectorHook } from "./rules-injector";
|
export { createRulesInjectorHook } from "./rules-injector";
|
||||||
export { createBackgroundNotificationHook } from "./background-notification"
|
export { createBackgroundNotificationHook } from "./background-notification"
|
||||||
export { createBackgroundCompactionHook } from "./background-compaction"
|
|
||||||
export { createAutoUpdateCheckerHook } from "./auto-update-checker";
|
export { createAutoUpdateCheckerHook } from "./auto-update-checker";
|
||||||
|
|
||||||
export { createAgentUsageReminderHook } from "./agent-usage-reminder";
|
export { createAgentUsageReminderHook } from "./agent-usage-reminder";
|
||||||
|
|||||||
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
export interface InteractiveBashArgs {
|
|
||||||
tmux_command: string
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user