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)
83 lines
2.3 KiB
TypeScript
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,
|
|
})
|
|
}
|
|
},
|
|
}
|
|
}
|