claude-code-popup 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tom-Canon-Rock
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,240 @@
1
+ # claude-code-popup
2
+
3
+ Claude Code (CLI) の Notification フックが発火したときに、デスクトップへカスタムデザインのポップアップを表示するツールです。
4
+
5
+ ## 特長
6
+
7
+ - 複数の Claude Code セッションをカード形式で同時表示(3 件まで自動伸縮、それ以上スクロール)
8
+ - カード本体クリックで起動元ターミナルを前面に呼び戻し
9
+ - ヘッダーから設定 GUI 起動(⚙)/カード一括消去(×)
10
+ - 5 種類のデザインテーマ
11
+ - マルチモニター対応(カーソルがあるディスプレイへ表示)
12
+ - `SessionEnd` フックと連動して Electron プロセスを自動停止
13
+
14
+ ## インストール
15
+
16
+ ### npm(公開後)
17
+
18
+ ```sh
19
+ npm install -g claude-code-popup
20
+ ```
21
+
22
+ `postinstall` で `~/.claude/settings.json` にフックが自動登録されます。
23
+
24
+ ### 開発インストール
25
+
26
+ ```sh
27
+ git clone <repo>
28
+ cd claude-code-popup
29
+ npm install
30
+ npm link # PATH に claude-code-popup を登録
31
+ claude-code-popup setup # 設定 GUI とフック登録を一括実行
32
+ ```
33
+
34
+ 解除:
35
+
36
+ ```sh
37
+ npm unlink -g claude-code-popup
38
+ ```
39
+
40
+ ---
41
+
42
+ ## 使用手順
43
+
44
+ ### 1. 初回セットアップ
45
+
46
+ グローバルインストールした場合、フックは自動登録されています。`claude-code-popup setup` を一度だけ実行して、デザイン・位置・表示時間などをお好みに調整してください。
47
+
48
+ ```sh
49
+ claude-code-popup setup
50
+ ```
51
+
52
+ ### 2. 通常運用
53
+
54
+ 以降は意識する必要はありません。Claude Code の各セッションで以下が自動実行されます。
55
+
56
+ | Claude Code フック | claude-code-popup 動作 |
57
+ |--------------------|---------------------|
58
+ | `SessionStart` | Electron をバックグラウンド起動(既起動ならスキップ) |
59
+ | `Notification`(`permission_prompt`) | 通知ウィンドウにカードを追加 |
60
+ | `SessionEnd` | 該当セッションのカードを削除。最後のセッションだったら Electron も終了 |
61
+
62
+ ### 3. 通知が来たとき
63
+
64
+ | 操作 | 結果 |
65
+ |------|------|
66
+ | カード本体をクリック | 起動元ターミナルを前面に |
67
+ | カード右上の `×` | 該当カードのみ削除 |
68
+ | ヘッダーの `×` | 全カード一括削除 |
69
+ | ヘッダーの `⚙` | 設定 GUI を起動 |
70
+
71
+ ---
72
+
73
+ ## 設定 GUI の表示
74
+
75
+ `claude-code-popup setup` で表示される 1 ページ UI です。各項目は変更後「設定を保存」を押すまで適用されません。
76
+
77
+ 設定 GUI の表示言語は **OS のロケール** に追従します(現在 `ja` / `en` 対応、それ以外は `en`)。
78
+
79
+ | セクション | 内容 | 備考 |
80
+ |------------|------|------|
81
+ | 通知デザイン | 5 種類のグリッドから選択。下にプレビュー | クリック即反映 |
82
+ | 通知メッセージ | カードに表示するタイトル文 | 空のままなら OS ロケールに応じた既定文を使用 |
83
+ | 表示位置 | 右下/左下/右上/左上のセレクト | カーソルがあるディスプレイ基準 |
84
+ | 表示時間 | 0〜60 秒のスライダー | `0` で自動消去なし/タイマーバーも非表示 |
85
+ | サウンド | 通知音オン/オフ | OS の通知音 API を利用 |
86
+
87
+ ### 5 つのデザイン
88
+
89
+ | # | 名前 | 特徴 |
90
+ |---|------|------|
91
+ | 1 | ミニマル | 白ベース、シャドウ控えめ |
92
+ | 2 | アクセントバー・ダーク | ダークカード+左サイドにカラーバー |
93
+ | 3 | Claude カラー | オレンジヘッダー帯 |
94
+ | 4 | ライト+アクセントバー | ライトカード+左サイドにカラーバー |
95
+ | 5 | ダーク | ダークカード、シンプル |
96
+
97
+ タイマーバーはすべてのデザインで `duration > 0` のとき自動表示されます。
98
+
99
+ ---
100
+
101
+ ## CLI コマンド
102
+
103
+ | コマンド | 用途 |
104
+ |----------|------|
105
+ | `claude-code-popup setup` | 設定 GUI を起動。フックも併せて確認・登録 |
106
+ | `claude-code-popup start --dir <path>` | 通知デーモンを起動(`SessionStart` フック用) |
107
+ | `claude-code-popup send --dir <path>` | 通知ウィンドウへカード追加(`Notification` フック用) |
108
+ | `claude-code-popup end --dir <path>` | 該当セッションのカードを削除し、最後のセッションだった場合はデーモンを終了(`SessionEnd` フック用) |
109
+ | `claude-code-popup help` | ヘルプ |
110
+
111
+ `send` は Electron が起動していなければ自動起動します(フック以外から手動でも使えます)。
112
+
113
+ ---
114
+
115
+ ## 設定ファイル
116
+
117
+ `~/.claude-code-popup/config.json`
118
+
119
+ ```json
120
+ {
121
+ "design": 1,
122
+ "message": {
123
+ "title": ""
124
+ },
125
+ "position": "bottom-right",
126
+ "duration": 5,
127
+ "sound": false
128
+ }
129
+ ```
130
+
131
+ | キー | 型 | 説明 |
132
+ |------|----|------|
133
+ | `design` | `1〜5` | デザインプリセット番号 |
134
+ | `message.title` | string | カードに表示するタイトル文。空文字なら OS ロケールに応じた既定文を使用 |
135
+ | `position` | `"top-left" \| "top-right" \| "bottom-left" \| "bottom-right"` | 表示位置 |
136
+ | `duration` | number (秒) | 自動消去までの秒数。`0` で消えない(タイマーバーも非表示) |
137
+ | `sound` | boolean | 通知音の有無 |
138
+
139
+ ランタイム情報(PID/IPC ポート)は `~/.claude-code-popup/runtime.json` に保存され、Electron 起動の重複防止に使われます。Electron 終了時に自動削除されます。
140
+
141
+ ---
142
+
143
+ ## 使用上の注意
144
+
145
+ ### 起動時の遅延
146
+
147
+ Electron のコールドスタートに **1〜3 秒** ほどかかります。`SessionStart` フックで先回り起動する設計のため、`Notification` フック発火時の通知表示はほぼ即座になります。手動で `send` だけ叩いた場合は、初回のみウィンドウ表示まで待ち時間があります。
148
+
149
+ ### プロセスのライフサイクル
150
+
151
+ - **通常時**: Electron プロセスは存在しません
152
+ - **Claude Code 起動中**: 1 つだけバックグラウンドで動作(複数セッションでも 1 プロセス)
153
+ - **全セッション終了時**: 自動的に終了
154
+
155
+ 手動で `claude-code-popup start` を叩いた場合、対応する `end` を発行するか手動で kill するまでデーモンが残り続けます。テスト用途では注意してください。
156
+
157
+ ```powershell
158
+ # 残ってしまった場合の手動終了(Windows)
159
+ Get-Process electron -ErrorAction SilentlyContinue | Stop-Process -Force
160
+ ```
161
+
162
+ ### マルチモニター環境
163
+
164
+ 通知ウィンドウは `claude-code-popup send` 実行時点での **マウスカーソルがあるディスプレイ** に表示されます。期待と違うモニターに出る場合はカーソルの位置を確認してください。
165
+
166
+ ### フックの取り扱い
167
+
168
+ `~/.claude/settings.json` を `installHooks` / `uninstallHooks` で編集します。
169
+
170
+ - 既存の他のフックは保持されます(`claude-code-popup` 関連だけマージ)
171
+ - ファイルが存在しない場合は新規作成されます
172
+ - グローバル install / uninstall 時に自動実行されます(`postinstall` / `preuninstall`)
173
+ - `npm link` での開発インストールでは `postinstall` は走らないため、`claude-code-popup setup` で手動登録してください
174
+
175
+ ### `claude-code-popup` コマンドが PATH に通っていない場合
176
+
177
+ フックは裸の `claude-code-popup` を呼ぶため、PATH に通っていないと **Claude Code 側ではフックが silent fail** します。`Get-Command claude-code-popup`(PowerShell)/`which claude-code-popup`(bash)で確認できます。
178
+
179
+ ### ターミナル前面化の制約
180
+
181
+ | OS | 動作 |
182
+ |----|------|
183
+ | Windows | 親プロセス(cmd / PowerShell / Windows Terminal / VS Code 統合ターミナルなど)を遡り、最初に MainWindow を持つプロセスを `SetForegroundWindow` で前面化 |
184
+ | macOS | `Terminal.app` を `osascript` で activate(best effort) |
185
+ | Linux | `wmctrl` 依存(インストールが必要な場合あり) |
186
+
187
+ VS Code や JetBrains の統合ターミナルから Claude Code を起動している場合、前面化されるのは IDE 全体のウィンドウになります。
188
+
189
+ ### 透明ウィンドウのレンダリング
190
+
191
+ 通知ウィンドウは `transparent: true` の Electron BrowserWindow です。ごく稀に GPU ドライバ起因で描画されない場合があります。その場合:
192
+
193
+ ```powershell
194
+ # Electron をリセットして再起動
195
+ Get-Process electron -ErrorAction SilentlyContinue | Stop-Process -Force
196
+ Remove-Item ~/.claude-code-popup/runtime.json -ErrorAction SilentlyContinue
197
+ ```
198
+
199
+ その後、新しい Claude Code セッションを開いて再現するか確認してください。
200
+
201
+ ---
202
+
203
+ ## ディレクトリ構成
204
+
205
+ ```
206
+ claude-code-popup/
207
+ ├── bin/
208
+ │ ├── cli.js CLI エントリポイント
209
+ │ ├── postinstall.js グローバル install 時にフック登録
210
+ │ └── preuninstall.js グローバル uninstall 時にフック解除
211
+ ├── src/
212
+ │ ├── config.js ~/.claude-code-popup/config.json 読み書き
213
+ │ ├── hooks.js ~/.claude/settings.json のフック登録/解除
214
+ │ ├── ipc.js CLI ↔ Electron 間の TCP/JSON プロトコル
215
+ │ └── runtime.js ロックファイル / プロセス存在確認
216
+ └── electron/
217
+ ├── main.js IPC サーバ + ウィンドウ管理
218
+ ├── preload.js contextBridge 経由の IPC ブリッジ
219
+ ├── notify.html / .js / .css 通知ウィンドウ
220
+ └── setup.html / .js / .css 設定 GUI
221
+ ```
222
+
223
+ ## 対応 OS
224
+
225
+ | OS | 状態 |
226
+ |----|------|
227
+ | Windows | 動作確認済み |
228
+ | macOS | 通知 UI 動作。ターミナル前面化は Terminal.app 限定 |
229
+ | Linux | 通知 UI 動作。ターミナル前面化は `wmctrl` 依存 |
230
+
231
+ ## 今後の予定
232
+
233
+ - npm レジストリへの公開
234
+ - ユーザー独自デザインの追加機能
235
+ - 通知履歴ログ
236
+ - macOS / Linux のターミナル前面化精度向上
237
+
238
+ ## ライセンス
239
+
240
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const path = require('node:path');
5
+ const { spawn, spawnSync } = require('node:child_process');
6
+
7
+ const ipc = require('../src/ipc');
8
+ const runtime = require('../src/runtime');
9
+ const { installHooks } = require('../src/hooks');
10
+ const { CONFIG_PATH } = require('../src/config');
11
+
12
+ function getForegroundHwnd() {
13
+ if (process.platform !== 'win32') return null;
14
+ try {
15
+ const ps =
16
+ "Add-Type -MemberDefinition '[DllImport(\"user32.dll\")] public static extern IntPtr GetForegroundWindow();' -Name F -Namespace Win -ErrorAction SilentlyContinue; [Win.F]::GetForegroundWindow().ToInt64()";
17
+ const r = spawnSync('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', ps], {
18
+ encoding: 'utf8',
19
+ windowsHide: true,
20
+ timeout: 4000,
21
+ });
22
+ const n = parseInt((r.stdout || '').trim(), 10);
23
+ return Number.isFinite(n) && n > 0 ? n : null;
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+
29
+ const ROOT = path.resolve(__dirname, '..');
30
+ const ELECTRON_ENTRY = path.join(ROOT, 'electron', 'main.js');
31
+
32
+ function parseArgs(argv) {
33
+ const args = { _: [], flags: {} };
34
+ for (let i = 0; i < argv.length; i++) {
35
+ const a = argv[i];
36
+ if (a.startsWith('--')) {
37
+ const key = a.slice(2);
38
+ const next = argv[i + 1];
39
+ if (next === undefined || next.startsWith('--')) {
40
+ args.flags[key] = true;
41
+ } else {
42
+ args.flags[key] = next;
43
+ i++;
44
+ }
45
+ } else {
46
+ args._.push(a);
47
+ }
48
+ }
49
+ return args;
50
+ }
51
+
52
+ function resolveElectronBinary() {
53
+ try {
54
+ return require('electron');
55
+ } catch (err) {
56
+ console.error('[claude-code-popup] electron is not installed. Run: npm install');
57
+ throw err;
58
+ }
59
+ }
60
+
61
+ function spawnElectron({ mode } = {}) {
62
+ const electronPath = resolveElectronBinary();
63
+ const env = { ...process.env, CLAUDE_NOTIFY_MODE: mode || 'notify' };
64
+ const child = spawn(electronPath, [ELECTRON_ENTRY], {
65
+ detached: true,
66
+ stdio: 'ignore',
67
+ env,
68
+ windowsHide: true,
69
+ });
70
+ child.unref();
71
+ return child;
72
+ }
73
+
74
+ async function waitForServer({ tries = 40, intervalMs = 150 } = {}) {
75
+ for (let i = 0; i < tries; i++) {
76
+ const info = await runtime.getRunningInstance();
77
+ if (info) return info;
78
+ await new Promise((r) => setTimeout(r, intervalMs));
79
+ }
80
+ return null;
81
+ }
82
+
83
+ async function ensureRunning(mode) {
84
+ const existing = await runtime.getRunningInstance();
85
+ if (existing) return existing;
86
+ spawnElectron({ mode });
87
+ const ready = await waitForServer();
88
+ if (!ready) throw new Error('failed_to_start_electron');
89
+ return ready;
90
+ }
91
+
92
+ async function cmdSetup() {
93
+ // Hooks are normally installed automatically by the npm postinstall script.
94
+ // For dev (npm link) flows, run it here too so the UI is fully wired up.
95
+ const settingsPath = installHooks();
96
+ console.log(`[claude-code-popup] config -> ${CONFIG_PATH}`);
97
+ console.log(`[claude-code-popup] hooks ensured -> ${settingsPath}`);
98
+ spawnElectron({ mode: 'setup' });
99
+ }
100
+
101
+ async function cmdStart(args) {
102
+ const dir = args.flags.dir && args.flags.dir !== true ? String(args.flags.dir) : '';
103
+ const hwnd = getForegroundHwnd();
104
+ const info = await ensureRunning('notify');
105
+ if (dir) {
106
+ try {
107
+ await ipc.send(info.port, { type: 'start', dir, ppid: process.ppid, hwnd });
108
+ } catch {
109
+ /* ignore */
110
+ }
111
+ }
112
+ console.log(`[claude-code-popup] running on port ${info.port} (pid ${info.pid})`);
113
+ }
114
+
115
+ async function cmdSend(args) {
116
+ const dir = args.flags.dir;
117
+ if (!dir || dir === true) {
118
+ console.error('[claude-code-popup] --dir <path> is required');
119
+ process.exit(1);
120
+ }
121
+ const info = await ensureRunning('notify');
122
+ // Also send hwnd on `send` so manual `send` calls (without prior `start`) still work.
123
+ // For real Claude Code flow, start has already populated sessionHwnd, but we update
124
+ // here too in case the user moved between terminals.
125
+ const hwnd = getForegroundHwnd();
126
+ await ipc.send(info.port, { type: 'send', dir: String(dir), ppid: process.ppid, hwnd });
127
+ }
128
+
129
+ async function cmdEnd(args) {
130
+ const dir = args.flags.dir;
131
+ if (!dir || dir === true) {
132
+ console.error('[claude-code-popup] --dir <path> is required');
133
+ process.exit(1);
134
+ }
135
+ const info = await runtime.getRunningInstance();
136
+ if (!info) return;
137
+ try {
138
+ await ipc.send(info.port, { type: 'end', dir: String(dir) });
139
+ } catch {
140
+ /* electron may have already exited */
141
+ }
142
+ }
143
+
144
+ function printHelp() {
145
+ console.log(`claude-code-popup <command> [options]
146
+
147
+ Commands:
148
+ setup Launch settings GUI and install Claude Code hooks
149
+ start Start the notification daemon (idempotent)
150
+ send --dir <path> Add a notification card for the given directory
151
+ end --dir <path> Remove the notification card for the given directory
152
+ help Show this message
153
+ `);
154
+ }
155
+
156
+ async function main() {
157
+ const args = parseArgs(process.argv.slice(2));
158
+ const cmd = args._[0];
159
+ try {
160
+ switch (cmd) {
161
+ case 'setup':
162
+ await cmdSetup();
163
+ break;
164
+ case 'start':
165
+ await cmdStart(args);
166
+ break;
167
+ case 'send':
168
+ await cmdSend(args);
169
+ break;
170
+ case 'end':
171
+ await cmdEnd(args);
172
+ break;
173
+ case 'help':
174
+ case undefined:
175
+ printHelp();
176
+ break;
177
+ default:
178
+ console.error(`[claude-code-popup] unknown command: ${cmd}`);
179
+ printHelp();
180
+ process.exit(1);
181
+ }
182
+ } catch (err) {
183
+ console.error(`[claude-code-popup] ${err?.message || err}`);
184
+ process.exit(1);
185
+ }
186
+ }
187
+
188
+ main();
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // Only run when this package is installed globally — local/dev installs
5
+ // (npm install <pkg> in a project, or npm install inside this repo) should
6
+ // NOT modify the user's ~/.claude/settings.json.
7
+ if (!process.env.npm_config_global) {
8
+ process.exit(0);
9
+ }
10
+
11
+ try {
12
+ const { installHooks, SETTINGS_PATH } = require('../src/hooks');
13
+ installHooks();
14
+ console.log(`[claude-code-popup] Claude Code hooks installed -> ${SETTINGS_PATH}`);
15
+ console.log('[claude-code-popup] Run "claude-code-popup setup" to configure the notification UI.');
16
+ } catch (err) {
17
+ console.error('[claude-code-popup] hook install skipped:', err?.message || err);
18
+ }
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ if (!process.env.npm_config_global) {
5
+ process.exit(0);
6
+ }
7
+
8
+ try {
9
+ const { uninstallHooks, SETTINGS_PATH } = require('../src/hooks');
10
+ uninstallHooks();
11
+ console.log(`[claude-code-popup] Claude Code hooks removed from ${SETTINGS_PATH}`);
12
+ } catch (err) {
13
+ console.error('[claude-code-popup] hook uninstall skipped:', err?.message || err);
14
+ }