|
| 1 | +import type { Buffer } from 'node:buffer' |
| 2 | + |
| 3 | +import type { HookInput } from '@anthropic-ai/claude-code' |
| 4 | + |
| 5 | +import { argv, exit, stdin } from 'node:process' |
| 6 | + |
| 7 | +import debug from 'debug' |
| 8 | + |
| 9 | +import { Format, LogLevel, LogLevelString, useLogg } from '@guiiai/logg' |
| 10 | +import { Client } from '@proj-airi/server-sdk' |
| 11 | +import { cac } from 'cac' |
| 12 | + |
| 13 | +import { name, version } from '../package.json' |
| 14 | +import { resolveComma, toArray } from './utils/general' |
| 15 | + |
| 16 | +interface Options { |
| 17 | + config?: string |
| 18 | + configLoader?: 'auto' | 'native' | 'unconfig' |
| 19 | + noConfig?: boolean |
| 20 | + debug?: boolean | string | string[] |
| 21 | + logLevel?: LogLevelString.Log | LogLevelString.Warning | LogLevelString.Error |
| 22 | + failOnWarn?: boolean |
| 23 | + env?: Record<string, string> |
| 24 | + quiet?: boolean |
| 25 | +} |
| 26 | + |
| 27 | +let logger = useLogg(name).withLogLevel(LogLevel.Log).withFormat(Format.Pretty) |
| 28 | + |
| 29 | +const cli = cac('airi-plugin-claude-code-cli') |
| 30 | +cli.help().version(version) |
| 31 | + |
| 32 | +cli |
| 33 | + .command('send', 'Pass Claude Code hook event to Channel Server', { ignoreOptionDefaultValue: true, allowUnknownOptions: true }) |
| 34 | + .option('-c, --config <filename>', 'Use a custom config file') |
| 35 | + .option('--config-loader <loader>', 'Config loader to use: auto, native, unconfig', { default: 'auto' }) |
| 36 | + .option('--no-config', 'Disable config file') |
| 37 | + .option('--debug [feat]', 'Show debug logs') |
| 38 | + .option('-l, --logLevel <level>', 'Set log level: info, warn, error, silent') |
| 39 | + .option('--fail-on-warn', 'Fail on warnings', { default: true }) |
| 40 | + .option('--env.* <value>', 'Define env variables') |
| 41 | + .option('--quiet', 'Suppress all logs') |
| 42 | + .action(async (_, flags: Options) => { |
| 43 | + if (flags?.quiet) { |
| 44 | + logger = logger.withLogLevel(-1 as LogLevel) |
| 45 | + } |
| 46 | + else { |
| 47 | + logger = logger.withLogLevelString(flags?.logLevel ?? LogLevelString.Log) |
| 48 | + } |
| 49 | + |
| 50 | + async function readStdin(): Promise<string> { |
| 51 | + const chunks: string[] = [] |
| 52 | + for await (const chunk of stdin) { |
| 53 | + chunks.push((chunk as Buffer).toString('utf-8')) |
| 54 | + } |
| 55 | + |
| 56 | + return chunks.join('') |
| 57 | + } |
| 58 | + |
| 59 | + if (stdin.isTTY) { |
| 60 | + throw new Error('`send` doesn\'t work without stdin input, Claude Code hooks events are expected to be piped to this command.') |
| 61 | + } |
| 62 | + |
| 63 | + const stdinInput = await readStdin() |
| 64 | + if (!stdinInput.trim()) { |
| 65 | + throw new Error('`send` received empty stdin input, Claude Code hooks events are expected to be piped to this command.') |
| 66 | + } |
| 67 | + |
| 68 | + const hookEvent = JSON.parse(stdinInput) as HookInput |
| 69 | + |
| 70 | + if (hookEvent.hook_event_name === 'UserPromptSubmit') { |
| 71 | + const channelServer = new Client({ name: 'proj-airi:plugin-claude-code', autoConnect: false }) |
| 72 | + await channelServer.connect() |
| 73 | + |
| 74 | + channelServer.send({ type: 'input:text', data: { text: hookEvent.prompt } }) |
| 75 | + } |
| 76 | + }) |
| 77 | + |
| 78 | +export async function runCLI(): Promise<void> { |
| 79 | + cli.parse(argv, { run: false }) |
| 80 | + |
| 81 | + if (cli.options.debug) { |
| 82 | + let namespace: string |
| 83 | + if (cli.options.debug === true) { |
| 84 | + namespace = `${name}:*` |
| 85 | + } |
| 86 | + else { |
| 87 | + // support debugging multiple flags with comma-separated list |
| 88 | + namespace = resolveComma(toArray(cli.options.debug)) |
| 89 | + .map(v => `${name}:${v}`) |
| 90 | + .join(',') |
| 91 | + } |
| 92 | + |
| 93 | + const enabled = debug.disable() |
| 94 | + if (enabled) |
| 95 | + namespace += `,${enabled}` |
| 96 | + |
| 97 | + debug.enable(namespace) |
| 98 | + debug(`${name}:debug`)('Debugging enabled', namespace) |
| 99 | + } |
| 100 | + |
| 101 | + try { |
| 102 | + await cli.runMatchedCommand() |
| 103 | + } |
| 104 | + catch (error) { |
| 105 | + logger.withError(error).error('running failed') |
| 106 | + exit(1) |
| 107 | + } |
| 108 | +} |
0 commit comments