Show the active skill with the model/effort its turn actually runs on, and flag when the next turn falls back to another model

개인 환경 백업 및 복구 자동화 스크립트~ :smile:
# 설정 및 프로그램 목록등 백업
bash ./backupmysetting.sh
# 전체 설치
bash ./installall.sh
# 필요한 환경/프로그램은 installxxx.sh 사용
#!/usr/bin/env bash shebang 을 추가하거나, 아래처럼 전역 sh 링크를 bash로 변경한다.sudo unlink /bin/sh
sudo ln -s /bin/bash /bin/shhooks/register.ts 85 lines1import type { EngineInterface, Register, TurnCompleteReason } from 'claude-code'
2import { atom, read, update } from 'claude-code'
3
4import type { SkillRun } from '../types'
5
6const runs = atom({ plugin: 'skill-badge', key: 'runs' } as const, [])
7const model = atom({ plugin: 'skill-badge', key: 'model' } as const, '')
8
9const KEEP = 8
10
11const RESULT: Record<TurnCompleteReason, SkillRun['result']> = {
12 answer: 'done',
13 aborted: 'aborted',
14 refusal: 'failed',
15 error: 'failed',
16}
17
18const short = (m: string) => m.replace(/^.*claude-/, '')
19
20const isOpen = (r: SkillRun | undefined) => r !== undefined && r.result === undefined
21
22const patchOpen = async ($: EngineInterface, fn: (r: SkillRun) => SkillRun) => {
23 const list = await read($, runs)
24 const last = list.at(-1)
25 if (last !== undefined && isOpen(last) && JSON.stringify(fn(last)) !== JSON.stringify(last)) {
26 await update($, runs, l => [...l.slice(0, -1), fn(last)])
27 }
28}
29
30export const register: Register = on => {
31 let loaded: string[] = []
32
33 on('prompt.submit', async ($, e, next) => {
34 if (e.turnId === undefined) {
35 const name = /^\/([\w:.-]+)(?=\s|$)/.exec(e.text)?.[1]
36 const isCommand = name !== undefined && (await $.command.list()).some(c => c.name === name)
37 loaded = isCommand ? [name] : []
38 }
39 return next(e)
40 })
41
42 on('tool.call', { tool: 'Skill' }, async (_, e, next) => {
43 const ran = await next(e)
44 if (e.agentId === undefined && ran.deny === undefined && ran.isError !== true) {
45 loaded = [...new Set([...loaded, e.skill])]
46 }
47 return ran
48 })
49
50 on('turn.step', async function* ($, e, next) {
51 if (e.agentId !== undefined) {
52 return yield* next(e)
53 }
54 const effort = e.effort === undefined ? {} : { effort: String(e.effort) }
55 if (loaded.length > 0) {
56 const skills = loaded
57 if (isOpen((await read($, runs)).at(-1))) {
58 await patchOpen($, r => ({ ...r, skills }))
59 } else {
60 const run = { skills, model: short(e.model), ...effort, startedAt: await $.clock.now() }
61 await update($, runs, l => [...l, run].slice(-KEEP))
62 }
63 }
64 const step = yield* next(e)
65 const answered = short(step.usage?.model ?? e.model)
66 if (loaded.length > 0) {
67 await patchOpen($, r => ({ ...r, model: answered }))
68 }
69 const tag = [answered, effort.effort].filter(v => v !== undefined).join(' · ')
70 if ((await read($, model)) !== tag) {
71 await update($, model, () => tag)
72 }
73 return step
74 })
75
76 on('turn.complete', async ($, e, next) => {
77 if (e.agentId === undefined && loaded.length > 0) {
78 const endedAt = await $.clock.now()
79 await patchOpen($, r => ({ ...r, endedAt, result: RESULT[e.reason] }))
80 loaded = []
81 }
82 return next(e)
83 })
84}
85types/index.d.ts 15 lines1export type SkillRun = {
2 skills: string[]
3 model: string
4 effort?: string
5 startedAt: number
6 endedAt?: number
7 result?: 'done' | 'failed' | 'aborted'
8}
9
10declare module 'claude-code' {
11 interface PluginState {
12 'skill-badge': { runs: SkillRun[]; model: string }
13 }
14}
15