feat(airi-plugin-claude-code): now works with Claude Code hooks
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "@proj-airi/airi-plugin-claude-code",
|
||||
"type": "module",
|
||||
"version": "0.7.2-beta.3",
|
||||
"private": true,
|
||||
"description": "manifest.json description",
|
||||
"bin": {
|
||||
"tsdown": "./dist/run.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsdown",
|
||||
"dev": "tsx ./src/run.ts",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@guiiai/logg": "catalog:",
|
||||
"@proj-airi/server-sdk": "workspace:*",
|
||||
"cac": "^6.7.14",
|
||||
"debug": "^4.4.3",
|
||||
"destr": "^2.0.5",
|
||||
"vue": "^3.5.22"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/claude-code": "^2.0.11",
|
||||
"tsx": "^4.20.6",
|
||||
"vue-tsc": "^3.1.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import type { Buffer } from 'node:buffer'
|
||||
|
||||
import type { HookInput } from '@anthropic-ai/claude-code'
|
||||
|
||||
import { argv, exit, stdin } from 'node:process'
|
||||
|
||||
import debug from 'debug'
|
||||
|
||||
import { Format, LogLevel, LogLevelString, useLogg } from '@guiiai/logg'
|
||||
import { Client } from '@proj-airi/server-sdk'
|
||||
import { cac } from 'cac'
|
||||
|
||||
import { name, version } from '../package.json'
|
||||
import { resolveComma, toArray } from './utils/general'
|
||||
|
||||
interface Options {
|
||||
config?: string
|
||||
configLoader?: 'auto' | 'native' | 'unconfig'
|
||||
noConfig?: boolean
|
||||
debug?: boolean | string | string[]
|
||||
logLevel?: LogLevelString.Log | LogLevelString.Warning | LogLevelString.Error
|
||||
failOnWarn?: boolean
|
||||
env?: Record<string, string>
|
||||
quiet?: boolean
|
||||
}
|
||||
|
||||
let logger = useLogg(name).withLogLevel(LogLevel.Log).withFormat(Format.Pretty)
|
||||
|
||||
const cli = cac('airi-plugin-claude-code-cli')
|
||||
cli.help().version(version)
|
||||
|
||||
cli
|
||||
.command('send', 'Pass Claude Code hook event to Channel Server', { ignoreOptionDefaultValue: true, allowUnknownOptions: true })
|
||||
.option('-c, --config <filename>', 'Use a custom config file')
|
||||
.option('--config-loader <loader>', 'Config loader to use: auto, native, unconfig', { default: 'auto' })
|
||||
.option('--no-config', 'Disable config file')
|
||||
.option('--debug [feat]', 'Show debug logs')
|
||||
.option('-l, --logLevel <level>', 'Set log level: info, warn, error, silent')
|
||||
.option('--fail-on-warn', 'Fail on warnings', { default: true })
|
||||
.option('--env.* <value>', 'Define env variables')
|
||||
.option('--quiet', 'Suppress all logs')
|
||||
.action(async (_, flags: Options) => {
|
||||
if (flags?.quiet) {
|
||||
logger = logger.withLogLevel(-1 as LogLevel)
|
||||
}
|
||||
else {
|
||||
logger = logger.withLogLevelString(flags?.logLevel ?? LogLevelString.Log)
|
||||
}
|
||||
|
||||
async function readStdin(): Promise<string> {
|
||||
const chunks: string[] = []
|
||||
for await (const chunk of stdin) {
|
||||
chunks.push((chunk as Buffer).toString('utf-8'))
|
||||
}
|
||||
|
||||
return chunks.join('')
|
||||
}
|
||||
|
||||
if (stdin.isTTY) {
|
||||
throw new Error('`send` doesn\'t work without stdin input, Claude Code hooks events are expected to be piped to this command.')
|
||||
}
|
||||
|
||||
const stdinInput = await readStdin()
|
||||
if (!stdinInput.trim()) {
|
||||
throw new Error('`send` received empty stdin input, Claude Code hooks events are expected to be piped to this command.')
|
||||
}
|
||||
|
||||
const hookEvent = JSON.parse(stdinInput) as HookInput
|
||||
|
||||
if (hookEvent.hook_event_name === 'UserPromptSubmit') {
|
||||
const channelServer = new Client({ name: 'proj-airi:plugin-claude-code', autoConnect: false })
|
||||
await channelServer.connect()
|
||||
|
||||
channelServer.send({ type: 'input:text', data: { text: hookEvent.prompt } })
|
||||
}
|
||||
})
|
||||
|
||||
export async function runCLI(): Promise<void> {
|
||||
cli.parse(argv, { run: false })
|
||||
|
||||
if (cli.options.debug) {
|
||||
let namespace: string
|
||||
if (cli.options.debug === true) {
|
||||
namespace = `${name}:*`
|
||||
}
|
||||
else {
|
||||
// support debugging multiple flags with comma-separated list
|
||||
namespace = resolveComma(toArray(cli.options.debug))
|
||||
.map(v => `${name}:${v}`)
|
||||
.join(',')
|
||||
}
|
||||
|
||||
const enabled = debug.disable()
|
||||
if (enabled)
|
||||
namespace += `,${enabled}`
|
||||
|
||||
debug.enable(namespace)
|
||||
debug(`${name}:debug`)('Debugging enabled', namespace)
|
||||
}
|
||||
|
||||
try {
|
||||
await cli.runMatchedCommand()
|
||||
}
|
||||
catch (error) {
|
||||
logger.withError(error).error('running failed')
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env node
|
||||
import module from 'node:module'
|
||||
|
||||
import { runCLI } from './cli'
|
||||
|
||||
try {
|
||||
module.enableCompileCache?.()
|
||||
}
|
||||
catch {}
|
||||
runCLI()
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* https://github.com/rolldown/tsdown/blob/a7e267ab7f4e836e836dab5cecf029fc35fd1939/src/utils/general.ts
|
||||
*/
|
||||
export function toArray<T>(
|
||||
val: T | T[] | null | undefined,
|
||||
defaultValue?: T,
|
||||
): T[] {
|
||||
if (Array.isArray(val)) {
|
||||
return val
|
||||
}
|
||||
else if (val == null) {
|
||||
if (defaultValue)
|
||||
return [defaultValue]
|
||||
return []
|
||||
}
|
||||
else {
|
||||
return [val]
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveComma<T extends string>(arr: T[]): T[] {
|
||||
return arr.flatMap(format => format.split(',') as T[])
|
||||
}
|
||||
|
||||
export function resolveRegex<T>(str: T): T | RegExp {
|
||||
if (
|
||||
typeof str === 'string'
|
||||
&& str.length > 2
|
||||
&& str[0] === '/'
|
||||
&& str.at(-1) === '/'
|
||||
) {
|
||||
return new RegExp(str.slice(1, -1))
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
export function debounce<T extends (...args: any[]) => any>(
|
||||
fn: T,
|
||||
wait: number,
|
||||
): T {
|
||||
let timeout: ReturnType<typeof setTimeout> | undefined
|
||||
return function (this: any, ...args: any[]) {
|
||||
if (timeout)
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(() => {
|
||||
timeout = undefined
|
||||
fn.apply(this, args)
|
||||
}, wait)
|
||||
} as T
|
||||
}
|
||||
|
||||
export function slash(string: string): string {
|
||||
return string.replaceAll('\\', '/')
|
||||
}
|
||||
|
||||
export const noop = <T>(v: T): T => v
|
||||
|
||||
export function matchPattern(
|
||||
id: string,
|
||||
patterns: (string | RegExp)[],
|
||||
): boolean {
|
||||
return patterns.some((pattern) => {
|
||||
if (pattern instanceof RegExp) {
|
||||
pattern.lastIndex = 0
|
||||
return pattern.test(id)
|
||||
}
|
||||
return id === pattern
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { defineConfig } from 'tsdown'
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
entry: ['./src/run.ts'],
|
||||
inlineOnly: [],
|
||||
platform: 'node',
|
||||
dts: true,
|
||||
fixedExtension: true,
|
||||
unused: true,
|
||||
publint: true,
|
||||
},
|
||||
])
|
||||
Reference in New Issue
Block a user