Reports the account, input-wait notices and rate limits of this Claude Code session to the Pike terminal it runs in.

「AI エージェント × ターミナル」に特化した、軽量な開発環境です。 ターミナル中心で作業する開発者が VS Code の代わりに使えることを目指しています。
Tauri v2(Rust + Vue / TypeScript)製。Windows を主対象としています。macOS(Apple Silicon)版も署名・公証して配布しており、自動更新も同じように働きます。macOS ではホストのログインシェルで開発でき、Windows 固有の機能(WSL、ジャンプリスト、管理者ターミナル、ウィンドウ背景の透過)だけが対象外です。
<img alt="エディタとファイルツリー" src="docs/manual/img/screenshot-editor.png">
<img alt="Git パネルとターミナルのエージェント" src="docs/manual/img/screenshot-git.png">
CLAUDE_CONFIG_DIR)にも追従し、Claude Code の hook から実際のアカウントを受け取れるpike file.rs:42 でファイルを開く、pike <dir> でプロジェクト切替、--wait で GIT_EDITOR 連携詳しい使い方は ユーザーマニュアル を参照してください。
GitHub Releases から最新のインストーラをダウンロードして実行します。
| ファイル | 種類 |
|---|---|
Pike_x.x.x_x64-setup.exe | Windows インストーラ(NSIS、推奨) |
Pike_x.x.x_x64_en-US.msi | Windows インストーラ(MSI) |
Pike_x.x.x_aarch64.dmg | macOS(Apple Silicon) |
インストール後に自動でアップデートを確認します。手動で確認するときは歯車メニュー →「更新を確認」から行えます。Windows 版と macOS 版のどちらも対象です。
配布しているのは Apple Silicon(arm64)版だけです。Intel Mac では動きません。
配布物は Apple Developer ID で署名して公証(notarization)しているため、ダウンロードしてそのまま開けます。
winget install Casey.Just)開発タスクは justfile にまとめてあります。just だけで一覧が出ます。 レシピは Git Bash で走るため、Windows では Git for Windows が必要です。
# 依存をインストール
npm install
# 同梱用の ripgrep バイナリをダウンロード
just fetch-rg
# 開発版(インストール版と共存して起動できます)
just dev
# 本番ビルド
just build
# コミット前チェック一式(Biome / vue-tsc / ドキュメント整合 / ショートカット照合 / clippy / cargo test)
just check
アーキテクチャ、実装ルール、コミット規約などの開発者向け情報は CLAUDE.md にまとまっています。
hooks/register.ts 229 lines1// Pike が同梱する Claude Code の mod(#437)。Pike のターミナルで動く claude から、
2// アカウントの申告・入力待ちの知らせ・レート制限・ターミナルごとの状態を Pike へ渡す。
3//
4// **渡し方は `pike agent-hook` を起こすだけ**(`settings.json` に登録する hook と同じ受け口。
5// 受け側の正本は `src-tauri/src/agent_hook.rs`)。この mod が持つのは「いつ、何を付けて
6// 起こすか」だけで、申告の置き場も配送も Rust の側にある。
7//
8// **Pike のターミナルの外では何もしない**(`PIKE_PTY_ID` と `PIKE_HOOK_EXE` が無い)。
9// 利用者がこのフォルダを別の場所から読み込ませても、起こす相手が居ない。
10//
11// **セッションを待たせない。** `pike` の起動は待たずに `next(e)` へ進む(失敗も握り潰す)。
12// 例外は 2 つ。目印の環境変数を立てるところ(同じイベントの settings hook より先に
13// 立っていなければ意味が無い)と、セッションの終わり(待たないと、起こす前にプロセスが
14// 終わる)。
15import type { EngineInterface, Register } from 'claude-code'
16
17/** レートの報告の最短間隔。ターンのたびにプロセスを起こさない。 */
18const USAGE_INTERVAL_MS = 60_000
19/** 枠が動いたときの最短間隔。動くたびに送ると、応答のたびにプロセスが立つ。 */
20const USAGE_CHANGED_MS = 15_000
21
22/** `Notification` の種類 → Pike の契機(`agent_hook.rs` の `NoticeKind`)。載っていない種類は知らせない。 */
23const NOTICE_EVENTS: Record<string, string> = {
24 permission_prompt: 'waiting',
25 agent_needs_input: 'waiting',
26 elicitation_dialog: 'waiting',
27 idle_prompt: 'idle',
28}
29
30type Pike = {
31 exe: string
32 installKey: string
33 /** 走っている Pike へ届ける配送があるか(Pike が `PIKE_HOOK_LIVE` で知らせる。無い OS がある)。 */
34 live: boolean
35}
36
37/**
38 * `session.measure` は記録の場所(どのアカウントかの手がかり)を持たないので、classic の
39 * イベントで見たものを覚えておく。
40 *
41 * **`SessionStart` だけで覚えない。** mod は読み直されることがあり(Pike の更新が
42 * このファイルを書き換える)、そのときモジュールの変数は初期化される。`Stop` は
43 * ターンのたびに `session.measure` の直前に来るので、そこで拾い直せば報告は止まらない。
44 */
45let transcriptPath: string | undefined
46let lastUsageAt = 0
47
48/** 読めた結果は覚える。どれも Pike が PTY の環境に置いた値で、セッションの途中で変わらない。 */
49let pikeCache: Pike | undefined
50
51async function pikeOf($: EngineInterface): Promise<Pike | undefined> {
52 if (pikeCache) return pikeCache
53 const exe = await $.env.get('PIKE_HOOK_EXE')
54 const pty = await $.env.get('PIKE_PTY_ID')
55 if (!exe || !pty) return undefined
56 pikeCache = {
57 exe,
58 installKey: (await $.env.get('PIKE_INSTALL_KEY')) ?? '',
59 live: (await $.env.get('PIKE_HOOK_LIVE')) === '1',
60 }
61 return pikeCache
62}
63
64/**
65 * 知らせと状態の報告の宛先。**走っている Pike へ届ける配送が無い環境では `undefined`**
66 * (`Pike.live`)で、そこではプロセスを起こさない。申告とレートの報告は `pikeOf` を使う
67 * (ファイルに書くものなので、配送に依らない)。
68 */
69async function liveOf($: EngineInterface): Promise<Pike | undefined> {
70 const pike = await pikeOf($)
71 return pike?.live ? pike : undefined
72}
73
74/** `pike agent-hook` を起こす。**待たない**(呼び出し側は `void` で捨てる)。 */
75async function report($: EngineInterface, pike: Pike, flags: string[], payload: unknown): Promise<void> {
76 const argv = [pike.exe, 'agent-hook', '--via=mod', `--install-key=${pike.installKey}`, '--agent=claude', ...flags]
77 try {
78 await $.process.run(argv, { stdin: JSON.stringify(payload), timeoutMs: 10_000 })
79 } catch {
80 // Pike が居ない・起こせない。セッションには関係が無い。
81 }
82}
83
84/**
85 * 状態の報告(`--state=`)を、起きた順に 1 本ずつ送るための列。
86 *
87 * **報告は 1 件ごとに別のプロセス**なので、並べて起こすと Pike に届く順が入れ替わる
88 * (終わったセッションに「待機中」が後から届いて一覧に残る、中断したターンの「実行中」が
89 * 「待機中」の後に届いて印が回り続ける)。前の 1 本が終わってから次を起こす。
90 */
91let stateQueue: Promise<void> = Promise.resolve()
92
93function enqueue(task: () => Promise<void>): Promise<void> {
94 stateQueue = stateQueue.then(task)
95 return stateQueue
96}
97
98/** 許可の確認を出したまま答えを待っているか。答えのあとに「実行中」へ戻すために覚える。 */
99let awaitingPermission = false
100
101/**
102 * 許可の確認に答えが出たら「実行中」へ戻す。**待っていなければ何もしない**(ツールの
103 * たびに呼ばれるので、プロセスを起こすのは確認を出していたときだけ)。
104 */
105async function reportResumed($: EngineInterface): Promise<void> {
106 if (awaitingPermission) await reportRunning($)
107}
108
109/** ターンが走っていることを知らせる(始まったときと、確認に答えが出たとき)。 */
110async function reportRunning($: EngineInterface): Promise<void> {
111 awaitingPermission = false
112 const pike = await liveOf($)
113 if (pike) void enqueue(() => report($, pike, ['--state=running'], {}))
114}
115
116/**
117 * ターンが終わったことを、コンテキストの埋まり具合を添えて知らせる。
118 *
119 * **`session.measure` を待たずにここで読む。** あちらは値が動いたときにしか来ないので、
120 * 応答の前に中断したターンでは来ず、タブが「実行中」のまま残る。
121 */
122async function reportIdle($: EngineInterface, pike: Pike): Promise<void> {
123 const flags = ['--state=idle']
124 try {
125 const { context } = await $.session.usage()
126 // 最初の応答より前(始めたばかり・圧縮の直後)は `tokens` が無い。
127 if (context.tokens !== undefined) flags.push(`--context=${context.tokens}/${context.window}`)
128 } catch {
129 // 読めなくても、終わったことは知らせる。
130 }
131 try {
132 // セッションを再開できる場所(`agent_hook.rs` の `AgentState::root`)。worktree へ
133 // 移ると動くので、ターンのたびに読む。
134 flags.push(`--root=${await $.session.root()}`)
135 } catch {
136 // 無ければ、Pike はタブの場所で再開する。
137 }
138 await report($, pike, flags, {})
139}
140
141export const register: Register = (on) => {
142 on('classic.SessionStart', async ($, e, next) => {
143 const pike = await pikeOf($)
144 if (!pike) return next(e)
145 // **この mod が動いている目印。** `settings.json` に登録済みの同じ hook は、これを見て
146 // 何もせずに終わる(二重の通知を防ぐ)。`next(e)` がその hook を走らせるので、先に立てる。
147 await $.env.set('PIKE_AGENT_MOD', '1')
148 transcriptPath = e.transcript_path
149 // 申告(stdin)と、このターミナルでセッションが始まったことを 1 回で渡す。
150 // 配送が無い環境では申告だけ(フラグ無しが申告。`agent_hook.rs` の `run_hook`)。
151 const flags = pike.live ? ['--state=session', `--session=${e.session_id}`] : []
152 void enqueue(() => report($, pike, flags, { transcript_path: e.transcript_path, cwd: e.cwd }))
153 return next(e)
154 })
155
156 on('classic.Notification', async ($, e, next) => {
157 const event = NOTICE_EVENTS[e.notification_type]
158 // 許可の確認は `PermissionRequest` で知らせ済み。答えを待っているあいだに来る
159 // 同じ確認の `permission_prompt` では、プロセスを起こさない。
160 const known = awaitingPermission && e.notification_type === 'permission_prompt'
161 const pike = event && !known ? await liveOf($) : undefined
162 if (pike) void report($, pike, [`--event=${event}`], {})
163 return next(e)
164 })
165
166 // 許可の確認が出た時点で知らせる。`Notification` の `permission_prompt` は同じ確認の
167 // 約 6 秒後に来る(実測)ので、そちらだけだと知らせが遅れる。
168 //
169 // 状態(`--state=waiting`)も同じ 1 回で渡す。知らせは「まだ見ていない」あいだだけの
170 // 印で、状態は答えるまで続く(`agent_hook.rs` の `StateKind::Waiting`)。
171 on('classic.PermissionRequest', async ($, e, next) => {
172 const pike = await liveOf($)
173 if (pike) {
174 awaitingPermission = true
175 void enqueue(() => report($, pike, ['--state=waiting', '--event=waiting'], {}))
176 }
177 return next(e)
178 })
179
180 // 確認に答えが出た(ツールが走った・失敗した・断られた)。ターンはまだ続いている。
181 on('classic.PostToolUse', async ($, e, next) => (await reportResumed($), next(e)))
182 on('classic.PostToolUseFailure', async ($, e, next) => (await reportResumed($), next(e)))
183 on('classic.PermissionDenied', async ($, e, next) => (await reportResumed($), next(e)))
184
185 on('classic.Stop', async ($, e, next) => {
186 transcriptPath = e.transcript_path
187 const pike = await liveOf($)
188 if (pike) void report($, pike, ['--event=done'], {})
189 return next(e)
190 })
191
192 // 実行中かどうか。サブエージェントは `turn.start` を出さないので、来るのは本線だけ。
193 on('turn.start', async ($, e, next) => (await reportRunning($), next(e)))
194
195 on('turn.complete', async ($, e, next) => {
196 // サブエージェントの終わりは本線の終わりではない。
197 if (e.agentId !== undefined) return next(e)
198 const pike = await liveOf($)
199 awaitingPermission = false
200 if (pike) void enqueue(() => reportIdle($, pike))
201 return next(e)
202 })
203
204 on('session.end', async ($, e, next) => {
205 const pike = await liveOf($)
206 // 列の後ろに並べて待つ。先に送ると、まだ列にいるターンの終わりが後から届く。
207 // 利用者が自分で抜けたときは印を付ける(`agent_hook.rs` の `AgentState::left`)。
208 // `clear` と `resume` は付けない: 同じプロセスが次のセッションを始めて名乗り直す。
209 const left = e.reason === 'prompt_input_exit' || e.reason === 'logout'
210 const flags = left ? ['--state=ended', '--left'] : ['--state=ended']
211 if (pike) await enqueue(() => report($, pike, flags, {}))
212 return next(e)
213 })
214
215 on('session.measure', async ($, e, next) => {
216 // 枠が動いたときは早めに、そうでなければ間隔を空けて送る(「まだこの値」という鮮度のため)。
217 const since = Date.now() - lastUsageAt
218 const due = since >= (e.changed.includes('rateLimits') ? USAGE_CHANGED_MS : USAGE_INTERVAL_MS)
219 if (due && transcriptPath && e.rateLimits.length > 0) {
220 const pike = await pikeOf($)
221 if (pike) {
222 lastUsageAt = Date.now()
223 void report($, pike, ['--usage'], { transcript_path: transcriptPath, rate_limits: e.rateLimits })
224 }
225 }
226 return next(e)
227 })
228}
229