YeonGyu-Kim 490c0b626f Add auto-slash-command hook for intercepting and replacing slash commands
This hook intercepts user messages starting with '/' and REPLACES them with the actual command template output instead of injecting instructions. The implementation includes:

- Slash command detection (detector.ts) - identifies messages starting with '/'
- Command discovery and execution (executor.ts) - loads templates from ~/.claude/commands/ or similar
- Hook integration (index.ts) - registers with chat.message event to replace output.parts
- Comprehensive test coverage - 37 tests covering detection, replacement, error handling, and command exclusions
- Configuration support in HookNameSchema

Key features:
- Supports excluded commands to skip processing
- Loads command templates from user's command directory
- Replaces user input before reaching the LLM
- Tests all edge cases including missing files, malformed templates, and special commands

🤖 Generated with assistance of OhMyOpenCode (https://github.com/code-yeongyu/oh-my-opencode)
2026-01-01 21:03:27 +09:00

83 lines
2.3 KiB
TypeScript

import {
detectSlashCommand,
extractPromptText,
} from "./detector"
import { executeSlashCommand } from "./executor"
import { log } from "../../shared"
import {
AUTO_SLASH_COMMAND_TAG_OPEN,
AUTO_SLASH_COMMAND_TAG_CLOSE,
} from "./constants"
import type {
AutoSlashCommandHookInput,
AutoSlashCommandHookOutput,
} from "./types"
export * from "./detector"
export * from "./executor"
export * from "./constants"
export * from "./types"
const sessionProcessedCommands = new Set<string>()
export function createAutoSlashCommandHook() {
return {
"chat.message": async (
input: AutoSlashCommandHookInput,
output: AutoSlashCommandHookOutput
): Promise<void> => {
const promptText = extractPromptText(output.parts)
if (
promptText.includes(AUTO_SLASH_COMMAND_TAG_OPEN) ||
promptText.includes(AUTO_SLASH_COMMAND_TAG_CLOSE)
) {
return
}
const parsed = detectSlashCommand(promptText)
if (!parsed) {
return
}
const commandKey = `${input.sessionID}:${input.messageID}:${parsed.command}`
if (sessionProcessedCommands.has(commandKey)) {
return
}
sessionProcessedCommands.add(commandKey)
log(`[auto-slash-command] Detected: /${parsed.command}`, {
sessionID: input.sessionID,
args: parsed.args,
})
const result = await executeSlashCommand(parsed)
const idx = output.parts.findIndex((p) => p.type === "text" && p.text)
if (idx < 0) {
return
}
if (result.success && result.replacementText) {
const taggedContent = `${AUTO_SLASH_COMMAND_TAG_OPEN}\n${result.replacementText}\n${AUTO_SLASH_COMMAND_TAG_CLOSE}`
output.parts[idx].text = taggedContent
log(`[auto-slash-command] Replaced message with command template`, {
sessionID: input.sessionID,
command: parsed.command,
})
} else {
const errorMessage = `${AUTO_SLASH_COMMAND_TAG_OPEN}\n[AUTO-SLASH-COMMAND ERROR]\n${result.error}\n\nOriginal input: ${parsed.raw}\n${AUTO_SLASH_COMMAND_TAG_CLOSE}`
output.parts[idx].text = errorMessage
log(`[auto-slash-command] Command not found, showing error`, {
sessionID: input.sessionID,
command: parsed.command,
error: result.error,
})
}
},
}
}