From ba5b50ad1d23f0d9a13ca31ab18dba6e536f8f42 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 12 Oct 2026 13:57:24 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=8B=86=E5=88=86=20cockpit=20?= =?UTF-8?q?=E7=82=BA=E5=8F=AF=E5=80=8B=E5=88=A5=E5=AE=89=E8=A3=9D=E7=9A=84?= =?UTF-8?q?=20statusbar=20=E8=88=87=20companion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - statusbar:輸入框上方狀態列,指令 /statusbar - companion:小說角色陪伴,指令 /companion,自行渲染角色行 - companion 資料目錄改為 ~/.companion - 整併 M4 檔案至 companion;M2 實作移至 hooks/companion/persona - README 移除本機資料夾安裝,補上更新與移除指令 Co-Authored-By: Claude Sonnet 5.5 --- .claude-plugin/marketplace.json | 11 +++- README.md | 39 +++++++---- plugins/cockpit/.claude-plugin/plugin.json | 10 --- plugins/cockpit/hooks/companion/index.tsx | 4 -- plugins/cockpit/hooks/register.tsx | 9 --- plugins/companion/.claude-plugin/plugin.json | 9 +++ .../hooks/companion/affinity.ts | 0 .../hooks/companion/band.test.ts | 29 +------- .../hooks/companion/band.ts | 4 +- .../hooks/companion/care.test.ts | 2 +- .../hooks/companion/care.ts | 2 +- .../hooks/companion/companion.test.ts | 6 +- .../hooks/companion/core.ts | 8 +-- .../hooks/companion/core/apply.ts | 0 .../hooks/companion/core/core.test.ts | 0 .../hooks/companion/core/personality.ts | 0 .../hooks/companion/core/speech.ts | 0 .../hooks/companion/core/store.ts | 0 .../hooks/companion/core/types.ts | 0 .../hooks/companion/defaults.ts | 0 .../hooks/companion/fs.ts | 0 .../{hud => companion}/hooks/companion/io.ts | 0 .../hooks/companion/memory.test.ts | 0 .../hooks/companion/memory.ts | 0 .../hooks/companion/mood.ts | 0 .../hooks/companion/node-fs.ts | 0 .../hooks/companion/persona}/affinity.ts | 0 .../companion/persona}/companion.test.ts | 0 .../hooks/companion/persona}/core.ts | 4 +- .../hooks/companion/persona}/inject.ts | 0 .../hooks/companion/persona}/interaction.ts | 0 .../hooks/companion/persona}/mood.ts | 0 .../hooks/companion/persona}/secrets.ts | 0 .../hooks/companion/persona}/status.ts | 0 .../hooks/companion/persona}/tone.ts | 0 .../hooks/companion/persona}/types.ts | 0 plugins/companion/hooks/companion/seg.ts | 17 +++++ .../hooks/companion/todo.ts | 0 .../hooks/companion/types.ts | 0 .../{cockpit => companion}/hooks/hooks.json | 0 plugins/companion/hooks/register.tsx | 49 ++++++++++++++ .../skills/ingest/SKILL.md | 4 +- plugins/{cockpit => companion}/tsconfig.json | 0 plugins/statusbar/.claude-plugin/plugin.json | 10 +++ .../hooks/hud => statusbar/hooks}/format.ts | 6 -- plugins/statusbar/hooks/hooks.json | 1 + .../hud => statusbar/hooks}/hud.test.tsx | 0 .../hooks/register.tsx} | 66 +++++++------------ plugins/statusbar/tsconfig.json | 3 + .../{cockpit => statusbar}/types/index.d.ts | 2 +- 50 files changed, 162 insertions(+), 133 deletions(-) delete mode 100644 plugins/cockpit/.claude-plugin/plugin.json delete mode 100644 plugins/cockpit/hooks/companion/index.tsx delete mode 100644 plugins/cockpit/hooks/register.tsx create mode 100644 plugins/companion/.claude-plugin/plugin.json rename plugins/{cockpit => companion}/hooks/companion/affinity.ts (100%) rename plugins/{cockpit => companion}/hooks/companion/band.test.ts (66%) rename plugins/{cockpit => companion}/hooks/companion/band.ts (93%) rename plugins/{hud => companion}/hooks/companion/care.test.ts (99%) rename plugins/{hud => companion}/hooks/companion/care.ts (99%) rename plugins/{cockpit => companion}/hooks/companion/companion.test.ts (97%) rename plugins/{cockpit => companion}/hooks/companion/core.ts (95%) rename plugins/{cockpit => companion}/hooks/companion/core/apply.ts (100%) rename plugins/{cockpit => companion}/hooks/companion/core/core.test.ts (100%) rename plugins/{cockpit => companion}/hooks/companion/core/personality.ts (100%) rename plugins/{cockpit => companion}/hooks/companion/core/speech.ts (100%) rename plugins/{cockpit => companion}/hooks/companion/core/store.ts (100%) rename plugins/{cockpit => companion}/hooks/companion/core/types.ts (100%) rename plugins/{cockpit => companion}/hooks/companion/defaults.ts (100%) rename plugins/{cockpit => companion}/hooks/companion/fs.ts (100%) rename plugins/{hud => companion}/hooks/companion/io.ts (100%) rename plugins/{hud => companion}/hooks/companion/memory.test.ts (100%) rename plugins/{hud => companion}/hooks/companion/memory.ts (100%) rename plugins/{cockpit => companion}/hooks/companion/mood.ts (100%) rename plugins/{cockpit => companion}/hooks/companion/node-fs.ts (100%) rename plugins/{cockpit/companion => companion/hooks/companion/persona}/affinity.ts (100%) rename plugins/{cockpit/companion => companion/hooks/companion/persona}/companion.test.ts (100%) rename plugins/{cockpit/companion => companion/hooks/companion/persona}/core.ts (98%) rename plugins/{cockpit/companion => companion/hooks/companion/persona}/inject.ts (100%) rename plugins/{cockpit/companion => companion/hooks/companion/persona}/interaction.ts (100%) rename plugins/{cockpit/companion => companion/hooks/companion/persona}/mood.ts (100%) rename plugins/{cockpit/companion => companion/hooks/companion/persona}/secrets.ts (100%) rename plugins/{cockpit/companion => companion/hooks/companion/persona}/status.ts (100%) rename plugins/{cockpit/companion => companion/hooks/companion/persona}/tone.ts (100%) rename plugins/{cockpit/companion => companion/hooks/companion/persona}/types.ts (100%) create mode 100644 plugins/companion/hooks/companion/seg.ts rename plugins/{hud => companion}/hooks/companion/todo.ts (100%) rename plugins/{cockpit => companion}/hooks/companion/types.ts (100%) rename plugins/{cockpit => companion}/hooks/hooks.json (100%) create mode 100644 plugins/companion/hooks/register.tsx rename plugins/{cockpit => companion}/skills/ingest/SKILL.md (93%) rename plugins/{cockpit => companion}/tsconfig.json (100%) create mode 100644 plugins/statusbar/.claude-plugin/plugin.json rename plugins/{cockpit/hooks/hud => statusbar/hooks}/format.ts (96%) create mode 100644 plugins/statusbar/hooks/hooks.json rename plugins/{cockpit/hooks/hud => statusbar/hooks}/hud.test.tsx (100%) rename plugins/{cockpit/hooks/hud/index.tsx => statusbar/hooks/register.tsx} (67%) create mode 100644 plugins/statusbar/tsconfig.json rename plugins/{cockpit => statusbar}/types/index.d.ts (97%) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 1c15cee..d36a764 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -6,9 +6,14 @@ }, "plugins": [ { - "name": "cockpit", - "source": "./plugins/cockpit", - "description": "Cockpit 外掛,內含 hud 模組(輸入框上方單行 HUD):模型、目錄、分支、5h/7d 額度與重置、context、CPU/記憶體/磁碟" + "name": "statusbar", + "source": "./plugins/statusbar", + "description": "輸入框上方單行狀態列:模型、目錄、分支、5h/7d 額度與重置、context、CPU/記憶體/磁碟" + }, + { + "name": "companion", + "source": "./plugins/companion", + "description": "小說角色陪伴:人設注入、語氣、心情與好感、互動記憶、主動關心與待辦,並可用 ingest skill 解析小說建立角色" } ] } diff --git a/README.md b/README.md index 8b653cb..2cb57b2 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,44 @@ # jsc-marketplace -JSC 自用 Claude Code mods。 +JSC 自用 Claude Code mods。每個 mod 皆可單獨安裝、更新與移除。 | Mod | 說明 | | --- | --- | -| [cockpit](plugins/cockpit) | Cockpit 外掛,內含 hud 模組(輸入框上方單行 HUD):模型、目錄、分支、5h/7d 額度與重置、context、CPU/記憶體/磁碟 | +| [statusbar](plugins/statusbar) | 輸入框上方單行狀態列:模型、目錄、分支、5h/7d 額度與重置、context、CPU/記憶體/磁碟 | +| [companion](plugins/companion) | 小說角色陪伴:人設注入、語氣、心情與好感、互動記憶、主動關心與待辦,並可用 ingest skill 解析小說建立角色 | ## 安裝 -本機資料夾: - -```bash -claude plugin marketplace add /root/jsc-marketplace -claude plugin install cockpit@jsc-marketplace -``` - -或透過 Gitea 遠端: +先加入 marketplace,再依需要安裝個別 mod: ```bash claude plugin marketplace add https://gitea.jsc.idv.tw/jiantw83/jsc-marketplace.git -claude plugin install cockpit@jsc-marketplace +claude plugin install statusbar@jsc-marketplace +claude plugin install companion@jsc-marketplace ``` ## 指令 -- `/cockpit hud on | off | compact`:切換狀態列 -- `/hud on | off | compact`:上述指令的別名 +- `/statusbar on | off | compact`:切換狀態列 +- `/companion status | switch <角色> | off | setup`:角色陪伴控制 + +## 更新 + +```bash +claude plugin marketplace update jsc-marketplace +claude plugin update statusbar@jsc-marketplace +claude plugin update companion@jsc-marketplace +``` + +更新後需重新啟動 Claude Code 才會生效。 + +## 移除 + +```bash +claude plugin uninstall statusbar@jsc-marketplace +claude plugin uninstall companion@jsc-marketplace +claude plugin marketplace remove jsc-marketplace +``` ## 授權 diff --git a/plugins/cockpit/.claude-plugin/plugin.json b/plugins/cockpit/.claude-plugin/plugin.json deleted file mode 100644 index 4477598..0000000 --- a/plugins/cockpit/.claude-plugin/plugin.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "cockpit", - "version": "0.1.0", - "description": "Cockpit 外掛,內含 hud 模組(輸入框上方單行 HUD):模型、目錄、分支、5h/7d 額度與重置、context、CPU/記憶體/磁碟", - "license": "MIT", - "types": "./types/index.d.ts", - "author": { - "name": "JSC" - } -} diff --git a/plugins/cockpit/hooks/companion/index.tsx b/plugins/cockpit/hooks/companion/index.tsx deleted file mode 100644 index a11d86c..0000000 --- a/plugins/cockpit/hooks/companion/index.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import type { Register } from 'claude-code' - -// companion 模組:目前為空殼,之後的階段再補內容 -export const register: Register = () => {} diff --git a/plugins/cockpit/hooks/register.tsx b/plugins/cockpit/hooks/register.tsx deleted file mode 100644 index bd93a2b..0000000 --- a/plugins/cockpit/hooks/register.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import type { Register } from 'claude-code' - -import { register as companion } from './companion' -import { register as hud } from './hud' - -export const register: Register = on => { - hud(on) - companion(on) -} diff --git a/plugins/companion/.claude-plugin/plugin.json b/plugins/companion/.claude-plugin/plugin.json new file mode 100644 index 0000000..9f378db --- /dev/null +++ b/plugins/companion/.claude-plugin/plugin.json @@ -0,0 +1,9 @@ +{ + "name": "companion", + "version": "0.1.0", + "description": "小說角色陪伴:人設注入、語氣、心情與好感、互動記憶、主動關心與待辦,並可用 ingest skill 解析小說建立角色", + "license": "MIT", + "author": { + "name": "JSC" + } +} diff --git a/plugins/cockpit/hooks/companion/affinity.ts b/plugins/companion/hooks/companion/affinity.ts similarity index 100% rename from plugins/cockpit/hooks/companion/affinity.ts rename to plugins/companion/hooks/companion/affinity.ts diff --git a/plugins/cockpit/hooks/companion/band.test.ts b/plugins/companion/hooks/companion/band.test.ts similarity index 66% rename from plugins/cockpit/hooks/companion/band.test.ts rename to plugins/companion/hooks/companion/band.test.ts index 6d7f89a..cb89699 100644 --- a/plugins/cockpit/hooks/companion/band.test.ts +++ b/plugins/companion/hooks/companion/band.test.ts @@ -1,19 +1,8 @@ import { describe, expect, test } from 'claude-code/testing' -import { bandLines, fit, lineWidth } from '../hud/format' -import type { HudData } from '../hud/format' import { fitCompanion } from './band' import type { View } from './core' - -const DATA: HudData = { - model: 'claude-opus-5-5[1m]', - cwd: '/home/dev/work/luopan', - home: '/home/dev', - git: { branch: 'issue-3', dirty: 1, ahead: 0, behind: 0 }, - usage: { fiveHour: { percent: 42 }, sevenDay: { percent: 71 }, ctxPercent: 31 }, - sys: { cpu: 23 }, - tzOffsetMin: 480, -} +import { lineWidth } from './seg' const VIEW: View = { core: { id: 'miko', name: '月見ミコ', facts: {} as never, neuroticism: 50, baseline: { p: 0, a: 0, d: 0 } }, @@ -56,19 +45,3 @@ describe('角色行', () => { expect(text({ ...VIEW, todos: 0 }, 200)).not.toContain('☐') }) }) - -describe('band', () => { - test('沒有角色時只有第一行,且與現況逐字相同', () => { - for (const cols of [200, 100, 60]) { - const lines = bandLines(DATA, cols, false, null) - expect(lines.length).toBe(1) - expect(lines[0]).toEqual(fit(DATA, cols, false)) - } - }) - - test('有角色時第一行不變,第二行是角色行', () => { - const lines = bandLines(DATA, 120, false, fitCompanion(VIEW, 120)) - expect(lines.length).toBe(2) - expect(lines[0]).toEqual(fit(DATA, 120, false)) - }) -}) diff --git a/plugins/cockpit/hooks/companion/band.ts b/plugins/companion/hooks/companion/band.ts similarity index 93% rename from plugins/cockpit/hooks/companion/band.ts rename to plugins/companion/hooks/companion/band.ts index 07a5efa..97cca84 100644 --- a/plugins/cockpit/hooks/companion/band.ts +++ b/plugins/companion/hooks/companion/band.ts @@ -1,5 +1,5 @@ -import { bar, lineWidth } from '../hud/format' -import type { Seg } from '../hud/format' +import { bar, lineWidth } from './seg' +import type { Seg } from './seg' import type { View } from './core' const axis = (v: number) => bar(((v + 1) / 2) * 100, 3) diff --git a/plugins/hud/hooks/companion/care.test.ts b/plugins/companion/hooks/companion/care.test.ts similarity index 99% rename from plugins/hud/hooks/companion/care.test.ts rename to plugins/companion/hooks/companion/care.test.ts index a153a43..6b90e7b 100644 --- a/plugins/hud/hooks/companion/care.test.ts +++ b/plugins/companion/hooks/companion/care.test.ts @@ -140,7 +140,7 @@ describe('關心:靜音與不打斷', () => { assert.deepEqual((await s.care.tick()).map(c => c.trigger), ['session_start']) }) - test('/cockpit quiet 在期間內靜音,到期後恢復', async () => { + test('/companion quiet 在期間內靜音,到期後恢復', async () => { const s = setup() await s.care.quiet(30) await s.care.onUserTurn() diff --git a/plugins/hud/hooks/companion/care.ts b/plugins/companion/hooks/companion/care.ts similarity index 99% rename from plugins/hud/hooks/companion/care.ts rename to plugins/companion/hooks/companion/care.ts index 3c61f6e..7591ed9 100644 --- a/plugins/hud/hooks/companion/care.ts +++ b/plugins/companion/hooks/companion/care.ts @@ -291,7 +291,7 @@ export interface CareEngine { onToolResult(ok: boolean): Promise /** 定時/事件檢查:回傳要立即顯示的 toast 或氣泡 */ tick(signals?: Signals): Promise - /** /cockpit quiet [分鐘],0 取消靜音 */ + /** /companion quiet [分鐘],0 取消靜音 */ quiet(minutes: number): Promise state(): Promise } diff --git a/plugins/cockpit/hooks/companion/companion.test.ts b/plugins/companion/hooks/companion/companion.test.ts similarity index 97% rename from plugins/cockpit/hooks/companion/companion.test.ts rename to plugins/companion/hooks/companion/companion.test.ts index 274b4f3..a0e00ec 100644 --- a/plugins/cockpit/hooks/companion/companion.test.ts +++ b/plugins/companion/hooks/companion/companion.test.ts @@ -23,7 +23,7 @@ const MIKO: CompanionCore = { } const H = 3_600_000 const DAY = 24 * H -const ROOT = '/h/.cockpit' +const ROOT = '/h/.companion' function setup(builtins = [MIKO]) { const fs = memoryFs() @@ -39,10 +39,10 @@ describe('啟用', () => { expect((await c.ensureActivated()).first).toBe(false) }) - test('companion off 後沒有角色,且重新啟動不會自動再啟用', async () => { + test('off 後沒有角色,且重新啟動不會自動再啟用', async () => { const { c } = setup() await c.ensureActivated() - await c.command('companion off') + await c.command('off') expect(await c.view()).toBe(null) await c.ensureActivated() expect(await c.view()).toBe(null) diff --git a/plugins/cockpit/hooks/companion/core.ts b/plugins/companion/hooks/companion/core.ts similarity index 95% rename from plugins/cockpit/hooks/companion/core.ts rename to plugins/companion/hooks/companion/core.ts index 6a59a87..284f9ea 100644 --- a/plugins/cockpit/hooks/companion/core.ts +++ b/plugins/companion/hooks/companion/core.ts @@ -138,7 +138,7 @@ export function createCompanion({ fs, clock, root, builtins = [FUBUKI] }: Compan else if (k === 'quietEnd') u.quiet.end = val else (u as Record)[k] = val } - if (bad.length) return `無法解析:${bad.join(' ')}\n用法:/cockpit setup nickname=… wake=HH:MM sleep=HH:MM tz=+0800 careLimit=N quietStart=HH:MM quietEnd=HH:MM` + if (bad.length) return `無法解析:${bad.join(' ')}\n用法:/companion setup nickname=… wake=HH:MM sleep=HH:MM tz=+0800 careLimit=N quietStart=HH:MM quietEnd=HH:MM` if (args.length) await writeJson(fs, userPath, u) return [ args.length ? '設定已儲存:' : '目前設定(以 key=value 修改):', @@ -151,14 +151,14 @@ export function createCompanion({ fs, clock, root, builtins = [FUBUKI] }: Compan async function command(raw: string): Promise { const [sub = '', ...rest] = raw.trim().split(/\s+/).filter(Boolean) if (sub === 'status') return status() - if (sub === 'switch') return rest[0] ? switchTo(rest[0]) : `用法:/cockpit switch <角色>。可用:${(await available()).join('、')}` - if (sub === 'companion' && rest[0] === 'off') { + if (sub === 'switch') return rest[0] ? switchTo(rest[0]) : `用法:/companion switch <角色>。可用:${(await available()).join('、')}` + if (sub === 'off') { const g = await getGlobal() await writeJson(fs, globalPath, { ...g, active: null, activatedOnce: true }) return '助理已關閉,所有資料保留。' } if (sub === 'setup') return setup(rest) - return '用法:/cockpit status | switch <角色> | companion off | setup [key=value…]' + return '用法:/companion status | switch <角色> | off | setup [key=value…]' } return { ensureActivated, view, moodEvent, affinityEvent, command, getUser } diff --git a/plugins/cockpit/hooks/companion/core/apply.ts b/plugins/companion/hooks/companion/core/apply.ts similarity index 100% rename from plugins/cockpit/hooks/companion/core/apply.ts rename to plugins/companion/hooks/companion/core/apply.ts diff --git a/plugins/cockpit/hooks/companion/core/core.test.ts b/plugins/companion/hooks/companion/core/core.test.ts similarity index 100% rename from plugins/cockpit/hooks/companion/core/core.test.ts rename to plugins/companion/hooks/companion/core/core.test.ts diff --git a/plugins/cockpit/hooks/companion/core/personality.ts b/plugins/companion/hooks/companion/core/personality.ts similarity index 100% rename from plugins/cockpit/hooks/companion/core/personality.ts rename to plugins/companion/hooks/companion/core/personality.ts diff --git a/plugins/cockpit/hooks/companion/core/speech.ts b/plugins/companion/hooks/companion/core/speech.ts similarity index 100% rename from plugins/cockpit/hooks/companion/core/speech.ts rename to plugins/companion/hooks/companion/core/speech.ts diff --git a/plugins/cockpit/hooks/companion/core/store.ts b/plugins/companion/hooks/companion/core/store.ts similarity index 100% rename from plugins/cockpit/hooks/companion/core/store.ts rename to plugins/companion/hooks/companion/core/store.ts diff --git a/plugins/cockpit/hooks/companion/core/types.ts b/plugins/companion/hooks/companion/core/types.ts similarity index 100% rename from plugins/cockpit/hooks/companion/core/types.ts rename to plugins/companion/hooks/companion/core/types.ts diff --git a/plugins/cockpit/hooks/companion/defaults.ts b/plugins/companion/hooks/companion/defaults.ts similarity index 100% rename from plugins/cockpit/hooks/companion/defaults.ts rename to plugins/companion/hooks/companion/defaults.ts diff --git a/plugins/cockpit/hooks/companion/fs.ts b/plugins/companion/hooks/companion/fs.ts similarity index 100% rename from plugins/cockpit/hooks/companion/fs.ts rename to plugins/companion/hooks/companion/fs.ts diff --git a/plugins/hud/hooks/companion/io.ts b/plugins/companion/hooks/companion/io.ts similarity index 100% rename from plugins/hud/hooks/companion/io.ts rename to plugins/companion/hooks/companion/io.ts diff --git a/plugins/hud/hooks/companion/memory.test.ts b/plugins/companion/hooks/companion/memory.test.ts similarity index 100% rename from plugins/hud/hooks/companion/memory.test.ts rename to plugins/companion/hooks/companion/memory.test.ts diff --git a/plugins/hud/hooks/companion/memory.ts b/plugins/companion/hooks/companion/memory.ts similarity index 100% rename from plugins/hud/hooks/companion/memory.ts rename to plugins/companion/hooks/companion/memory.ts diff --git a/plugins/cockpit/hooks/companion/mood.ts b/plugins/companion/hooks/companion/mood.ts similarity index 100% rename from plugins/cockpit/hooks/companion/mood.ts rename to plugins/companion/hooks/companion/mood.ts diff --git a/plugins/cockpit/hooks/companion/node-fs.ts b/plugins/companion/hooks/companion/node-fs.ts similarity index 100% rename from plugins/cockpit/hooks/companion/node-fs.ts rename to plugins/companion/hooks/companion/node-fs.ts diff --git a/plugins/cockpit/companion/affinity.ts b/plugins/companion/hooks/companion/persona/affinity.ts similarity index 100% rename from plugins/cockpit/companion/affinity.ts rename to plugins/companion/hooks/companion/persona/affinity.ts diff --git a/plugins/cockpit/companion/companion.test.ts b/plugins/companion/hooks/companion/persona/companion.test.ts similarity index 100% rename from plugins/cockpit/companion/companion.test.ts rename to plugins/companion/hooks/companion/persona/companion.test.ts diff --git a/plugins/cockpit/companion/core.ts b/plugins/companion/hooks/companion/persona/core.ts similarity index 98% rename from plugins/cockpit/companion/core.ts rename to plugins/companion/hooks/companion/persona/core.ts index 8beef7e..eb16d5a 100644 --- a/plugins/cockpit/companion/core.ts +++ b/plugins/companion/hooks/companion/persona/core.ts @@ -24,7 +24,7 @@ type State = { mood: Pad; moodAt: number; affinity: aff.Affinity } export type Options = { fs: Fs clock: Clock - /** 資料根目錄,例如 ~/cockpit */ + /** 資料根目錄,例如 ~/.companion */ root: string /** 當前角色資料夾名稱 */ id: string @@ -160,7 +160,7 @@ export function createCompanion({ fs, clock, root, id }: Options) { await fs.write(paths.user, JSON.stringify({ ...(await loadUser()), tone: on })) }, - /** `/cockpit tone on|off` */ + /** `/companion tone on|off` */ async command(line: string): Promise { const m = /^tone\s+(on|off)$/.exec(line.trim()) if (!m) return null diff --git a/plugins/cockpit/companion/inject.ts b/plugins/companion/hooks/companion/persona/inject.ts similarity index 100% rename from plugins/cockpit/companion/inject.ts rename to plugins/companion/hooks/companion/persona/inject.ts diff --git a/plugins/cockpit/companion/interaction.ts b/plugins/companion/hooks/companion/persona/interaction.ts similarity index 100% rename from plugins/cockpit/companion/interaction.ts rename to plugins/companion/hooks/companion/persona/interaction.ts diff --git a/plugins/cockpit/companion/mood.ts b/plugins/companion/hooks/companion/persona/mood.ts similarity index 100% rename from plugins/cockpit/companion/mood.ts rename to plugins/companion/hooks/companion/persona/mood.ts diff --git a/plugins/cockpit/companion/secrets.ts b/plugins/companion/hooks/companion/persona/secrets.ts similarity index 100% rename from plugins/cockpit/companion/secrets.ts rename to plugins/companion/hooks/companion/persona/secrets.ts diff --git a/plugins/cockpit/companion/status.ts b/plugins/companion/hooks/companion/persona/status.ts similarity index 100% rename from plugins/cockpit/companion/status.ts rename to plugins/companion/hooks/companion/persona/status.ts diff --git a/plugins/cockpit/companion/tone.ts b/plugins/companion/hooks/companion/persona/tone.ts similarity index 100% rename from plugins/cockpit/companion/tone.ts rename to plugins/companion/hooks/companion/persona/tone.ts diff --git a/plugins/cockpit/companion/types.ts b/plugins/companion/hooks/companion/persona/types.ts similarity index 100% rename from plugins/cockpit/companion/types.ts rename to plugins/companion/hooks/companion/persona/types.ts diff --git a/plugins/companion/hooks/companion/seg.ts b/plugins/companion/hooks/companion/seg.ts new file mode 100644 index 0000000..5744ffe --- /dev/null +++ b/plugins/companion/hooks/companion/seg.ts @@ -0,0 +1,17 @@ +// 角色行用的字串片段與寬度計算(與 statusbar 外掛各自獨立,不互相依賴) +export type Seg = { text: string; color?: string; dim?: boolean; bold?: boolean } + +export function bar(p: number, cells = 5): string { + const n = Math.max(0, Math.min(cells, Math.round((p / 100) * cells))) + return '▰'.repeat(n) + '▱'.repeat(cells - n) +} + +const WIDE = /[ᄀ-ᅟ⺀-꓏가-힣豈-﫿︰-﹏＀-⦆¢-₩]/ + +export function width(s: string): number { + let w = 0 + for (const ch of s) w += WIDE.test(ch) ? 2 : 1 + return w +} + +export const lineWidth = (segs: Seg[]) => segs.reduce((w, s) => w + width(s.text), 0) diff --git a/plugins/hud/hooks/companion/todo.ts b/plugins/companion/hooks/companion/todo.ts similarity index 100% rename from plugins/hud/hooks/companion/todo.ts rename to plugins/companion/hooks/companion/todo.ts diff --git a/plugins/cockpit/hooks/companion/types.ts b/plugins/companion/hooks/companion/types.ts similarity index 100% rename from plugins/cockpit/hooks/companion/types.ts rename to plugins/companion/hooks/companion/types.ts diff --git a/plugins/cockpit/hooks/hooks.json b/plugins/companion/hooks/hooks.json similarity index 100% rename from plugins/cockpit/hooks/hooks.json rename to plugins/companion/hooks/hooks.json diff --git a/plugins/companion/hooks/register.tsx b/plugins/companion/hooks/register.tsx new file mode 100644 index 0000000..67b421e --- /dev/null +++ b/plugins/companion/hooks/register.tsx @@ -0,0 +1,49 @@ +import type { Register } from 'claude-code' + +import { fitCompanion } from './companion/band' +import { createCompanion } from './companion/core' +import { nodeFs } from './companion/node-fs' + +let companion: ReturnType | undefined + +export const register: Register = on => { + on('session.start', async ($, e, next) => { + await $.command.register({ + name: 'companion', + description: '角色陪伴控制:狀態、切換角色、關閉、使用者設定', + argumentHint: 'status | switch <角色> | off | setup', + }) + const home = await $.env.get('HOME') + companion = createCompanion({ fs: nodeFs, clock: { now: Date.now }, root: `${home ?? ''}/.companion` }) + const first = await companion.ensureActivated() + if (first.first) $.ui.toast(`已啟用 ${first.name}。用 /companion off 可關閉`) + + return next(e) + }) + + on('command.run', { command: 'companion' }, async ($, e) => ({ + text: companion ? await companion.command(e.args) : '尚未就緒。', + })) + + on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => { + const rest = await next(e) + const view = await companion?.view() + if (e.props.hasSurvey || !view) return rest + + const line = fitCompanion(view, e.props.bodyColumns) + const { Box, Text } = $.ui.resolve(e) + + return ( + + {rest} + + {line.map(s => ( + + {s.text} + + ))} + + + ) + }) +} diff --git a/plugins/cockpit/skills/ingest/SKILL.md b/plugins/companion/skills/ingest/SKILL.md similarity index 93% rename from plugins/cockpit/skills/ingest/SKILL.md rename to plugins/companion/skills/ingest/SKILL.md index 7492b76..39d22a6 100644 --- a/plugins/cockpit/skills/ingest/SKILL.md +++ b/plugins/companion/skills/ingest/SKILL.md @@ -1,6 +1,6 @@ --- name: ingest -description: 解析一章輕小說(txt 或 md),抽出世界觀與人物,逐一詢問合併、新建或略過,並寫入 cockpit 角色資料。使用者提供小說章節檔或要求建立/更新角色設定時使用。 +description: 解析一章輕小說(txt 或 md),抽出世界觀與人物,逐一詢問合併、新建或略過,並寫入 companion 角色資料。使用者提供小說章節檔或要求建立/更新角色設定時使用。 --- # 小說章節解析 @@ -23,5 +23,5 @@ description: 解析一章輕小說(txt 或 md),抽出世界觀與人物, ## 注意 - 模型從對白推得的個性可信度偏低,只當先驗。 -- 不要在 repo 內保存受版權保護的長篇設定或台詞;資料寫在使用者家目錄的 cockpit 資料夾。 +- 不要在 repo 內保存受版權保護的長篇設定或台詞;資料寫在使用者家目錄的 `~/.companion` 資料夾。 - 範例完整流程見 `docs/ingest-walkthrough.md`。 diff --git a/plugins/cockpit/tsconfig.json b/plugins/companion/tsconfig.json similarity index 100% rename from plugins/cockpit/tsconfig.json rename to plugins/companion/tsconfig.json diff --git a/plugins/statusbar/.claude-plugin/plugin.json b/plugins/statusbar/.claude-plugin/plugin.json new file mode 100644 index 0000000..994b6fb --- /dev/null +++ b/plugins/statusbar/.claude-plugin/plugin.json @@ -0,0 +1,10 @@ +{ + "name": "statusbar", + "version": "0.1.0", + "description": "輸入框上方的單行狀態列:模型、目錄、分支、5h/7d 額度與重置、context、CPU/記憶體/磁碟", + "license": "MIT", + "types": "./types/index.d.ts", + "author": { + "name": "JSC" + } +} diff --git a/plugins/cockpit/hooks/hud/format.ts b/plugins/statusbar/hooks/format.ts similarity index 96% rename from plugins/cockpit/hooks/hud/format.ts rename to plugins/statusbar/hooks/format.ts index 7628f4c..0d93cdd 100644 --- a/plugins/cockpit/hooks/hud/format.ts +++ b/plugins/statusbar/hooks/format.ts @@ -232,9 +232,3 @@ export function parseDf(out: string): number | undefined { const m = row && /(\d+)%/.exec(row) return m ? Number(m[1]) : undefined } - -// band 的所有行:第一行恆為 HUD;有角色時才多第二行角色行 -export function bandLines(d: HudData, columns: number, compact: boolean, companion: Seg[] | null): Seg[][] { - const first = fit(d, columns, compact) - return companion ? [first, companion] : [first] -} diff --git a/plugins/statusbar/hooks/hooks.json b/plugins/statusbar/hooks/hooks.json new file mode 100644 index 0000000..5a63f64 --- /dev/null +++ b/plugins/statusbar/hooks/hooks.json @@ -0,0 +1 @@ +{ "modules": ["./register.tsx"] } diff --git a/plugins/cockpit/hooks/hud/hud.test.tsx b/plugins/statusbar/hooks/hud.test.tsx similarity index 100% rename from plugins/cockpit/hooks/hud/hud.test.tsx rename to plugins/statusbar/hooks/hud.test.tsx diff --git a/plugins/cockpit/hooks/hud/index.tsx b/plugins/statusbar/hooks/register.tsx similarity index 67% rename from plugins/cockpit/hooks/hud/index.tsx rename to plugins/statusbar/hooks/register.tsx index f5fe744..8948508 100644 --- a/plugins/cockpit/hooks/hud/index.tsx +++ b/plugins/statusbar/hooks/register.tsx @@ -1,19 +1,16 @@ import { atom, read, update } from 'claude-code' import type { EngineInterface, Register, SessionContextUsage, SessionRateLimit } from 'claude-code' -import type { Mode, Sys, Usage } from '../../types' -import { fitCompanion } from '../companion/band' -import { createCompanion } from '../companion/core' -import { nodeFs } from '../companion/node-fs' -import { bandLines, cpuPercent, parseCpu, parseDf, parseGit, parseMem, parseOffset } from './format' +import type { Mode, Sys, Usage } from '../types' +import { fit, cpuPercent, parseCpu, parseDf, parseGit, parseMem, parseOffset } from './format' import type { CpuSample } from './format' -const usageAtom = atom({ plugin: 'cockpit', key: 'usage' } as const, {}) -const gitAtom = atom({ plugin: 'cockpit', key: 'git' } as const, null) -const sysAtom = atom({ plugin: 'cockpit', key: 'sys' } as const, {}) -const modeAtom = atom({ plugin: 'cockpit', key: 'mode' } as const, 'auto') -const alertedAtom = atom({ plugin: 'cockpit', key: 'alerted' } as const, []) -const effortAtom = atom({ plugin: 'cockpit', key: 'effort' } as const, null) +const usageAtom = atom({ plugin: 'statusbar', key: 'usage' } as const, {}) +const gitAtom = atom({ plugin: 'statusbar', key: 'git' } as const, null) +const sysAtom = atom({ plugin: 'statusbar', key: 'sys' } as const, {}) +const modeAtom = atom({ plugin: 'statusbar', key: 'mode' } as const, 'auto') +const alertedAtom = atom({ plugin: 'statusbar', key: 'alerted' } as const, []) +const effortAtom = atom({ plugin: 'statusbar', key: 'effort' } as const, null) const SYS_MS = 3000 const GIT_MS = 15_000 @@ -37,7 +34,6 @@ function toUsage(context: SessionContextUsage, rateLimits: SessionRateLimit[]): } let home: string | undefined -let companion: ReturnType | undefined let tzOffsetMin = 0 let prevCpu: CpuSample | null = null let ticks = 0 @@ -98,20 +94,11 @@ async function runHud($: EngineInterface, args: string) { export const register: Register = on => { on('session.start', async ($, e, next) => { await $.command.register({ - name: 'cockpit', - description: 'Cockpit 控制:助理(status、switch、companion off、setup)與 hud 狀態列', - argumentHint: 'status | switch <角色> | companion off | setup | hud on | off | compact', - }) - // 別名:/hud 等同 /cockpit hud - await $.command.register({ - name: 'hud', - description: '切換輸入框上方的狀態列(/cockpit hud 的別名)', + name: 'statusbar', + description: '切換輸入框上方的狀態列', argumentHint: 'on | off | compact', }) home = await $.env.get('HOME') - companion = createCompanion({ fs: nodeFs, clock: { now: Date.now }, root: `${home ?? ''}/.cockpit` }) - const first = await companion.ensureActivated() - if (first.first) $.ui.toast(`已啟用 ${first.name}。用 /cockpit companion off 可關閉`) try { const r = await $.process.run(['date', '+%z'], { timeoutMs: 3000 }) tzOffsetMin = parseOffset(r.stdout) @@ -150,18 +137,12 @@ export const register: Register = on => { return ran }) - on('command.run', { command: 'hud' }, ($, e) => runHud($, e.args)) - - on('command.run', { command: 'cockpit' }, async ($, e, next) => { - const [sub, ...rest] = e.args.trim().split(/\s+/) - if (sub === 'hud') return runHud($, rest.join(' ')) - - return { text: companion ? await companion.command(e.args) : '尚未就緒。' } - }) + on('command.run', { command: 'statusbar' }, ($, e) => runHud($, e.args)) on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => { const mode = await read($, modeAtom) - if (e.props.hasSurvey || mode === 'off') return next(e) + const rest = await next(e) + if (e.props.hasSurvey || mode === 'off') return rest const data = { model: await $.session.model(), @@ -173,23 +154,20 @@ export const register: Register = on => { tzOffsetMin, effort: await read($, effortAtom), } - const view = await companion?.view() - const cols = e.props.bodyColumns - const lines = bandLines(data, cols, mode === 'compact', view ? fitCompanion(view, cols, mode === 'compact') : null) + const line = fit(data, e.props.bodyColumns, mode === 'compact') const { Box, Text } = $.ui.resolve(e) // 引擎在 band 與輸入框之間已留一行空白,上方補一行讓上下間距相同 return ( - {lines.map(line => ( - - {line.map(s => ( - - {s.text} - - ))} - - ))} + + {line.map(s => ( + + {s.text} + + ))} + + {rest} ) }) diff --git a/plugins/statusbar/tsconfig.json b/plugins/statusbar/tsconfig.json new file mode 100644 index 0000000..8681ec0 --- /dev/null +++ b/plugins/statusbar/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./.claude-plugin/types/tsconfig.json" +} diff --git a/plugins/cockpit/types/index.d.ts b/plugins/statusbar/types/index.d.ts similarity index 97% rename from plugins/cockpit/types/index.d.ts rename to plugins/statusbar/types/index.d.ts index c155965..f34c434 100644 --- a/plugins/cockpit/types/index.d.ts +++ b/plugins/statusbar/types/index.d.ts @@ -16,7 +16,7 @@ export type Mode = 'auto' | 'compact' | 'off' declare module 'claude-code' { interface PluginState { - cockpit: { + statusbar: { usage: Usage git: Git | null sys: Sys