读取本机 CodeBuddy 登录信息,在底部状态区域显示剩余积分、总积分和今日用量

个人 Claude Code 插件(mods)集合。每个子目录都是一个独立的 Claude Code 插件,通过 hooks 实现界面定制与行为增强,在本会话中热加载,无需重启。
| 插件 | 版本 | 说明 |
|---|---|---|
| show-mode | 0.3.0 | hidden 模式仅实时展示当前思考,隐藏工具调用与过程痕迹,保存文件 diff 与思考过程供回看 |
| shell-mode | 0.3.0 | 用 /shell-mode 切换 Bash 控制台,显示输出颜色,退出后将命令和结果保留在 CLI 中 |
| codebuddy | 0.1.0 | 在底部状态区域展示 CodeBuddy 剩余积分、总积分和今日用量 |
将本仓库克隆到 Claude Code 的 mods 目录 ~/.claude/mods/ 下,插件会在会话中自动热加载:
# Windows(PowerShell)
git clone <本仓库地址> "$env:USERPROFILE\.claude\mods"
# macOS / Linux
git clone <本仓库地址> ~/.claude/mods
注意:如果
~/.claude/mods目录已存在,可以克隆到临时目录后把需要的插件子目录(如show-mode/)复制进去。
克隆后的目录结构:
~/.claude/mods/
└── show-mode/
├── .claude-plugin/
│ ├── plugin.json # 插件清单(名称、版本、用户配置项)
│ └── types/ # claude-code API 类型定义
├── hooks/
│ ├── hooks.json # 模块入口声明
│ ├── register.tsx # 插件主逻辑(hooks 注册)
│ └── register.test.tsx # 测试
├── types/
│ └── index.d.ts # 插件状态与数据结构类型
└── tsconfig.json
如果只想启用其中某个插件,也可以在 Claude Code 会话内通过 /plugin 命令管理,或将插件目录添加到本地 marketplace 后安装。
show-mode 在插件清单(show-mode/.claude-plugin/plugin.json)中声明了一个用户配置项:
| 配置项 | 类型 | 可选值 | 默认值 | 说明 |
|---|---|---|---|---|
mode | string | off / hidden | hidden | off=正常显示且不新增记录;hidden=仅展示当前思考,并保存文件 diff 与思考过程 |
运行时模式优先于清单配置:通过 /show-mode 命令切换后,以切换后的值为准;未切换时回落到清单配置的默认值。
安装后在会话中使用 /show-mode 命令:
/show-mode off # 切换到正常显示模式
/show-mode hidden # 切换到仅展示当前思考模式
/show-mode status # 查看当前模式与保存记录统计
/show-mode view # 回看最近的记录(文件 diff + 思考过程,每类最近 10 条)
/show-mode view code # 只回看文件更新 diff(diff 语法着色,最近 10 条)
/show-mode view think # 只回看思考过程(含工具/技能调用记录,最近 10 条)
/show-mode view all # 回看更多记录(每类最近 30 条)
/show-mode view clear # 清空全部保存的文件 diff 和思考过程记录
说明:
off 模式下也可以查看之前 hidden 模式保存的记录。view all 可查看每类最近 30 条。在项目目录启动 Claude Code 时加载插件:
claude --plugin-dir "$env:USERPROFILE\.claude\mods\shell-mode"
先在 Claude Code 输入框中输入 /shell-mode,然后在打开的控制台 $ 后输入命令:
/shell-mode # 未开启时进入 Bash 模式
pwd
ls
cd internal
git status
/shell-mode # 已开启时退出,返回正常 Claude 对话
进入后,控制台取得键盘焦点,在 $ 后输入命令并回车。再次输入 /shell-mode、输入 exit 或按 Esc 关闭控制台,返回 Claude 对话。在 Claude 输入框和 Shell 控制台中输入 /shell-mode 都可以退出已开启的模式。/shell-mode on 和 /shell-mode off 仍可用于明确开启或关闭,无须强制指定参数。控制台位置由 Claude Code 决定:通常位于输入框上方,全屏且窗口足够宽时位于侧边。
控制台直接显示命令和原始输出,不使用带插件名前缀的日志,也不触发 Prompt dropped by a hook 提示,不显示成功提示或退出码:
$ ls
config docs go.mod main.go
$ git status
On branch test
nothing to commit, working tree clean
$
每条命令由 Git Bash 直接执行,不主动启动模型回合。实际命令与输出保存在本地,CLI 的原生命令记录仅保存短引用,模型后续只能读取该引用。支持管道、重定向和连续 cd,后续命令沿用上次结束时的目录。退出后再次进入,从 Claude 会话目录开始。
每条命令完成后增加一个空行,分隔相邻命令块。退出控制台时,把当前保留的命令和结果保存到 %LOCALAPPDATA%/Claude/shell-mode/history/,再通过 CLI 命令记录中的引用读取并渲染,继续显示在对话屏幕上,保留换行和颜色。通过 /shell-mode、exit、Esc 或窗格关闭按钮退出均会保留输出。重绘和插件重载时也从本地存档恢复;清理该目录会使对应的历史输出无法再次显示。
保留 Claude 原生窗格的自动输入焦点。窗格的 × 和输入框的回车图标由 Claude 绘制,当前公开接口没有隐藏开关,因此这两个图标保留。
直接显示的 ls 按控制台宽度多列排版;ll 等别名仍使用 Bash 配置。普通文件、目录、链接等颜色由 ls 和 LS_COLORS 确定。git status、git diff 等直接显示的 Git 输出启用 Git 自带的颜色;其他程序明确输出的 ANSI 颜色也会保留。不根据文件后缀或输出文字猜测颜色,没有颜色信息的内容保持默认颜色。显式 ls -1、ls -l、ls --color=never 等选项优先。管道、重定向、变量展开和复合命令不注入列格式或颜色,以免改变命令处理的数据。
Windows 优先使用 CLAUDE_CODE_GIT_BASH_PATH 指定的 Bash,否则查找 Git for Windows 的常见安装目录,避免误用 WSL 的 bash.exe。
每条命令使用登录 Bash 加载 /etc/profile 和个人登录配置,再开启 alias 展开,因此可以使用配置中的 ll 等别名。通常由 ~/.bash_profile 加载 ~/.bashrc。加载配置后恢复本次工作目录,保持连续 cd 的行为。
每条命令使用独立 Bash 进程,执行结束后显示输出,最多运行十分钟。只保持工作目录,变量、函数等 Shell 状态不跨命令保持;交互式终端程序不适用。插件重载或会话切换后回到正常对话。
命令输出保留原始换行,制表符转换为空格,ANSI 颜色转换为原生 Text 样式;光标移动等控制序列会移除。每次输出最多显示 50,000 字符、200 行,超过限制会显示截断提示。控制台历史最多保留最近 50,000 字符、200 行,并限制颜色片段数量,超出时删除最旧的内容;关闭时保存这部分输出,再释放控制台的临时历史。
加载插件:
claude --plugin-dir "$env:USERPROFILE\.claude\mods\codebuddy"
插件读取 %USERPROFILE%\AppData\Local\CodeBuddyExtension\Data\Public\auth\Tencent-Cloud.coding-copilot.info 的 account.enterpriseId 和 auth.accessToken,请求 CodeBuddy 企业用户积分及每日用量接口,将展示文字写入 %USERPROFILE%/.claude/codebuddy/usage.json 缓存,由现有 statusLine 脚本追加第二行 token usage · today 90 · remain 3322/10000,位于原状态行下方、权限提示上方,不带警告图标和插件名前缀。每日查询范围按 Asia/Taipei 时区动态生成当天的 00:00:00 至 23:59:59,分页参数为 pageNum: 1、pageSize: 1,today 和 remain 均去掉小数部分(不四舍五入),没有用量记录时显示 0。启动时自动查询,此后每分钟刷新,也可输入 /codebuddy 手动刷新。插件自身的重载成功提示在 InfoNotice 渲染时隐藏。
每次刷新重新读取登录文件,登录信息更新后无需修改插件。文件缺失、登录失效或接口异常时,界面显示错误提示;不会保存令牌或输出原始响应。
在现有 ~/.claude/statusline.sh 的主状态行输出之后追加:
bash "${USERPROFILE:-$HOME}/.claude/mods/codebuddy/statusline.sh"
缓存只含展示文字和更新时间,缺失或超过三分钟未更新时显示 --。积分缓存每分钟刷新,界面在宿主下一次执行 statusLine 时更新。本机原脚本已备份为 ~/.claude/statusline.before-codebuddy.sh。
cd show-mode
npx tsc --noEmit # 类型检查
show-mode/hooks/register.tsx,通过 register: Register 导出,使用 on() 订阅生命周期与渲染事件。claude-code 提供的 atom / read / update,按 plugin + key 隔离。show-mode/types/index.d.ts 扩展了 claude-code 的 PluginState 接口。show-mode/hooks/register.test.tsx,基于 claude-code/testing。.
├── README.md
└── show-mode/ # show-mode 插件
├── .claude-plugin/ # 插件清单与官方类型
├── hooks/ # hooks 逻辑与测试
├── types/ # 插件类型声明
└── tsconfig.json # TypeScript 配置hooks/register.tsx 166 lines1import type { EngineInterface, Register, Timer } from 'claude-code'
2
3const USAGE_URL = 'https://www.codebuddy.cn/billing/meter/get-enterprise-user-usage'
4const DAILY_USAGE_URL = 'https://www.codebuddy.cn/billing/meter/get-user-daily-usage'
5const AUTH_PATH = '/AppData/Local/CodeBuddyExtension/Data/Public/auth/Tencent-Cloud.coding-copilot.info'
6
7const object = (value: unknown): Record<string, unknown> =>
8 value !== null && typeof value === 'object' && !Array.isArray(value)
9 ? value as Record<string, unknown>
10 : {}
11
12const points = (value: unknown): number | undefined => {
13 if (typeof value !== 'number' && typeof value !== 'string') return undefined
14 if (typeof value === 'string' && value.trim() === '') return undefined
15 const number = Number(value)
16 return Number.isFinite(number) ? number : undefined
17}
18
19const today = () => {
20 const parts = new Intl.DateTimeFormat('en-US', {
21 timeZone: 'Asia/Taipei', year: 'numeric', month: '2-digit', day: '2-digit',
22 }).formatToParts(new Date())
23 const part = (type: string) => parts.find((value) => value.type === type)?.value
24 return `${part('year')}-${part('month')}-${part('day')}`
25}
26
27const loadDailyUsage = async ($: EngineInterface, headers: Record<string, string>): Promise<string> => {
28 try {
29 const date = today()
30 const response = await $.http.fetch(DAILY_USAGE_URL, {
31 method: 'POST', headers,
32 body: JSON.stringify({
33 startTime: `${date} 00:00:00`, endTime: `${date} 23:59:59`, pageNum: 1, pageSize: 1,
34 }),
35 })
36 if (!response.ok) return '查询失败'
37 const result = object(JSON.parse(response.text))
38 if (result.code !== 0) return '查询失败'
39 const data = object(result.data)
40 if (!Array.isArray(data.data)) return '数据异常'
41 if (data.data.length === 0 && points(data.total) === 0) return '0'
42 const credit = points(object(data.data[0]).credit)
43 return credit === undefined ? '数据异常' : String(Math.trunc(credit))
44 } catch {
45 return '查询失败'
46 }
47}
48
49const loadUsage = async ($: EngineInterface): Promise<string> => {
50 let account: Record<string, unknown>
51 let auth: Record<string, unknown>
52 try {
53 const home = await $.env.get('USERPROFILE')
54 if (!home) return '无法确定用户目录'
55 const info = object(JSON.parse((await $.fs.read(
56 home.replace(/\\/g, '/').replace(/\/$/, '') + AUTH_PATH,
57 )).replace(/^\uFEFF/, '')))
58 account = object(info.account)
59 auth = object(info.auth)
60 } catch {
61 return '无法读取登录信息,请先登录 CodeBuddy'
62 }
63
64 const enterpriseId = account.enterpriseId
65 const accessToken = auth.accessToken
66 if (typeof enterpriseId !== 'string' || enterpriseId.trim() === ''
67 || typeof accessToken !== 'string' || accessToken.trim() === '') {
68 return '登录信息缺少 enterpriseId 或 accessToken'
69 }
70
71 try {
72 const headers = {
73 Authorization: `Bearer ${accessToken}`,
74 'x-enterprise-id': enterpriseId,
75 'Content-Type': 'application/json',
76 }
77 const response = await $.http.fetch(USAGE_URL, {
78 method: 'POST',
79 headers,
80 body: '{}',
81 })
82 if (response.status === 401 || response.status === 403) return '登录失效或无权限,请重新登录 CodeBuddy'
83 if (!response.ok) return `积分查询失败(HTTP ${response.status})`
84 const result = object(JSON.parse(response.text))
85 if (result.code !== 0) return '积分接口返回业务错误,请稍后重试'
86 const data = object(result.data)
87 const credit = points(data.credit)
88 const limitNum = points(data.limitNum)
89 if (credit === undefined || limitNum === undefined) return '积分接口返回的数据格式异常'
90 const dailyUsage = await loadDailyUsage($, headers)
91 return `today ${dailyUsage} · remain ${Math.trunc(credit)}/${Math.trunc(limitNum)}`
92 } catch {
93 // 不输出原始异常、响应或认证信息,避免泄露令牌。
94 return '积分查询失败,请检查网络或登录状态'
95 }
96}
97
98let label = '正在查询积分…'
99let timer: Timer | undefined
100let pending: Promise<void> | undefined
101let generation = 0
102
103const publishUsage = async ($: EngineInterface) => {
104 try {
105 const home = await $.env.get('USERPROFILE')
106 if (!home) return
107 await $.fs.write(`${home.replace(/\\/g, '/')}/.claude/codebuddy/usage.json`, JSON.stringify({
108 text: `token usage · ${label}`, updatedAt: Date.now(),
109 }))
110 } catch {
111 $.ui.log('无法更新 CodeBuddy 状态栏缓存', { to: 'debug' })
112 }
113}
114
115const refresh = ($: EngineInterface): Promise<void> => {
116 if (pending) return pending
117 const current = generation
118 pending = (async () => {
119 try {
120 const nextLabel = await loadUsage($)
121 if (current !== generation) return
122 label = nextLabel
123 await publishUsage($)
124 } finally {
125 if (current === generation) pending = undefined
126 }
127 })()
128 return pending
129}
130
131export const register: Register = (on) => {
132 on('session.start', async ($, e, next) => {
133 timer?.cancel()
134 generation += 1
135 pending = undefined
136 label = '正在查询积分…'
137 $.ui.status(undefined)
138 await $.command.register({ name: 'codebuddy', description: '刷新 CodeBuddy 剩余积分、总积分和今日使用', immediate: true })
139 timer = $.clock.every(60_000, () => { void refresh($) })
140 void refresh($)
141 return next(e)
142 })
143
144 on('command.run', { command: 'codebuddy' }, async ($) => {
145 await refresh($)
146 return { text: `token usage · ${label}` }
147 })
148
149 on('ui.render', { component: 'InfoNotice' }, ($, e, next) => {
150 if (/^(?:●\s*)?codebuddy:\s*reloaded\s*\(\d+ hooks?:[^\r\n]*\)\s*$/.test(e.props.text.trim())) {
151 const { Box } = $.ui.resolve(e)
152 return <Box />
153 }
154 return next(e)
155 })
156
157 on('session.end', ($, e, next) => {
158 timer?.cancel()
159 timer = undefined
160 generation += 1
161 pending = undefined
162 $.ui.status(undefined)
163 return next(e)
164 })
165}
166