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/src/hooks.js ADDED
@@ -0,0 +1,77 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const os = require('node:os');
6
+
7
+ const SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
8
+
9
+ const HOOK_DEFINITIONS = {
10
+ SessionStart: [
11
+ {
12
+ hooks: [{ type: 'command', command: 'claude-code-popup start --dir "$PWD"' }],
13
+ },
14
+ ],
15
+ Notification: [
16
+ {
17
+ matcher: 'permission_prompt',
18
+ hooks: [{ type: 'command', command: 'claude-code-popup send --dir "$PWD"' }],
19
+ },
20
+ ],
21
+ SessionEnd: [
22
+ {
23
+ hooks: [{ type: 'command', command: 'claude-code-popup end --dir "$PWD"' }],
24
+ },
25
+ ],
26
+ };
27
+
28
+ function readSettings() {
29
+ if (!fs.existsSync(SETTINGS_PATH)) return {};
30
+ try {
31
+ return JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf8'));
32
+ } catch {
33
+ return {};
34
+ }
35
+ }
36
+
37
+ function isClaudeNotifyHook(entry) {
38
+ if (!entry || !Array.isArray(entry.hooks)) return false;
39
+ return entry.hooks.some(
40
+ (h) => typeof h?.command === 'string' && h.command.includes('claude-code-popup'),
41
+ );
42
+ }
43
+
44
+ function installHooks() {
45
+ const dir = path.dirname(SETTINGS_PATH);
46
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
47
+
48
+ const settings = readSettings();
49
+ settings.hooks = settings.hooks || {};
50
+
51
+ for (const [event, entries] of Object.entries(HOOK_DEFINITIONS)) {
52
+ const existing = Array.isArray(settings.hooks[event]) ? settings.hooks[event] : [];
53
+ const filtered = existing.filter((e) => !isClaudeNotifyHook(e));
54
+ settings.hooks[event] = [...filtered, ...entries];
55
+ }
56
+
57
+ fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2), 'utf8');
58
+ return SETTINGS_PATH;
59
+ }
60
+
61
+ function uninstallHooks() {
62
+ if (!fs.existsSync(SETTINGS_PATH)) return null;
63
+ const settings = readSettings();
64
+ if (!settings.hooks) return SETTINGS_PATH;
65
+
66
+ for (const event of Object.keys(HOOK_DEFINITIONS)) {
67
+ if (!Array.isArray(settings.hooks[event])) continue;
68
+ settings.hooks[event] = settings.hooks[event].filter((e) => !isClaudeNotifyHook(e));
69
+ if (settings.hooks[event].length === 0) delete settings.hooks[event];
70
+ }
71
+ if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
72
+
73
+ fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2), 'utf8');
74
+ return SETTINGS_PATH;
75
+ }
76
+
77
+ module.exports = { SETTINGS_PATH, installHooks, uninstallHooks };
package/src/i18n.js ADDED
@@ -0,0 +1,88 @@
1
+ 'use strict';
2
+
3
+ const STRINGS = {
4
+ ja: {
5
+ subtitle: 'Claude Code の通知ポップアップをカスタマイズします。',
6
+ section_design: '通知デザイン',
7
+ section_message: '通知メッセージ',
8
+ field_title: 'タイトル',
9
+ section_position: '表示位置',
10
+ field_position: '位置',
11
+ position_br: '右下',
12
+ position_bl: '左下',
13
+ position_tr: '右上',
14
+ position_tl: '左上',
15
+ section_duration: '表示時間',
16
+ duration_seconds: '{n} 秒',
17
+ duration_never: '消えない',
18
+ duration_hint: '0 にすると自動で消えません。',
19
+ section_sound: 'サウンド',
20
+ sound_enable: '通知音を鳴らす',
21
+ save: '設定を保存',
22
+ saved: '保存しました',
23
+ design_1_title: 'ミニマル',
24
+ design_1_desc: '白ベース、シャドウ控えめ。汎用',
25
+ design_2_title: 'アクセントバー・ダーク',
26
+ design_2_desc: 'ダークカード+左サイドにカラーバー',
27
+ design_3_title: 'Claude カラー',
28
+ design_3_desc: 'オレンジのヘッダー帯、ブランド寄り',
29
+ design_4_title: 'ライト+アクセントバー',
30
+ design_4_desc: 'ライトカード+左サイドにカラーバー',
31
+ design_5_title: 'ダーク',
32
+ design_5_desc: 'ダークカード、シンプル',
33
+ notify_default_title: '入力待ちです',
34
+ notify_card_tooltip: 'クリックでターミナルを前面に',
35
+ notify_dismiss: '閉じる',
36
+ notify_clear_all: 'すべて消去',
37
+ notify_settings: '設定',
38
+ },
39
+ en: {
40
+ subtitle: 'Customize the Claude Code notification popup.',
41
+ section_design: 'Notification design',
42
+ section_message: 'Notification message',
43
+ field_title: 'Title',
44
+ section_position: 'Position',
45
+ field_position: 'Position',
46
+ position_br: 'Bottom right',
47
+ position_bl: 'Bottom left',
48
+ position_tr: 'Top right',
49
+ position_tl: 'Top left',
50
+ section_duration: 'Duration',
51
+ duration_seconds: '{n} s',
52
+ duration_never: 'Never',
53
+ duration_hint: 'Set to 0 to keep cards visible until dismissed.',
54
+ section_sound: 'Sound',
55
+ sound_enable: 'Play notification sound',
56
+ save: 'Save settings',
57
+ saved: 'Saved',
58
+ design_1_title: 'Minimal',
59
+ design_1_desc: 'White base, subtle shadow. Versatile.',
60
+ design_2_title: 'Accent bar — dark',
61
+ design_2_desc: 'Dark card with a left accent bar',
62
+ design_3_title: 'Claude colour',
63
+ design_3_desc: 'Orange header band, brand-forward',
64
+ design_4_title: 'Accent bar — light',
65
+ design_4_desc: 'Light card with a left accent bar',
66
+ design_5_title: 'Dark',
67
+ design_5_desc: 'Plain dark card',
68
+ notify_default_title: 'Input required',
69
+ notify_card_tooltip: 'Click to bring the terminal to front',
70
+ notify_dismiss: 'Dismiss',
71
+ notify_clear_all: 'Clear all',
72
+ notify_settings: 'Settings',
73
+ },
74
+ };
75
+
76
+ function pickLocale(raw) {
77
+ if (!raw) return 'en';
78
+ const tag = String(raw).toLowerCase();
79
+ if (tag.startsWith('ja')) return 'ja';
80
+ return 'en';
81
+ }
82
+
83
+ function getDict(localeTag) {
84
+ const key = pickLocale(localeTag);
85
+ return { locale: key, strings: STRINGS[key] };
86
+ }
87
+
88
+ module.exports = { STRINGS, pickLocale, getDict };
package/src/ipc.js ADDED
@@ -0,0 +1,85 @@
1
+ 'use strict';
2
+
3
+ const net = require('node:net');
4
+
5
+ const HOST = '127.0.0.1';
6
+
7
+ function createServer(handler) {
8
+ const server = net.createServer((socket) => {
9
+ let buffer = '';
10
+ socket.setEncoding('utf8');
11
+ socket.on('data', (chunk) => {
12
+ buffer += chunk;
13
+ let idx;
14
+ while ((idx = buffer.indexOf('\n')) >= 0) {
15
+ const line = buffer.slice(0, idx).trim();
16
+ buffer = buffer.slice(idx + 1);
17
+ if (!line) continue;
18
+ let message;
19
+ try {
20
+ message = JSON.parse(line);
21
+ } catch {
22
+ socket.write(JSON.stringify({ ok: false, error: 'invalid_json' }) + '\n');
23
+ continue;
24
+ }
25
+ Promise.resolve()
26
+ .then(() => handler(message))
27
+ .then((reply) => {
28
+ socket.write(JSON.stringify({ ok: true, reply: reply ?? null }) + '\n');
29
+ })
30
+ .catch((err) => {
31
+ socket.write(JSON.stringify({ ok: false, error: String(err?.message || err) }) + '\n');
32
+ });
33
+ }
34
+ });
35
+ socket.on('error', () => {
36
+ /* ignore */
37
+ });
38
+ });
39
+ return server;
40
+ }
41
+
42
+ function listen(server) {
43
+ return new Promise((resolve, reject) => {
44
+ server.once('error', reject);
45
+ server.listen(0, HOST, () => {
46
+ const addr = server.address();
47
+ resolve(addr.port);
48
+ });
49
+ });
50
+ }
51
+
52
+ function send(port, message, { host = HOST, timeoutMs = 2000 } = {}) {
53
+ return new Promise((resolve, reject) => {
54
+ const socket = new net.Socket();
55
+ let buffer = '';
56
+ let done = false;
57
+ const finish = (err, value) => {
58
+ if (done) return;
59
+ done = true;
60
+ socket.destroy();
61
+ err ? reject(err) : resolve(value);
62
+ };
63
+ socket.setEncoding('utf8');
64
+ socket.setTimeout(timeoutMs);
65
+ socket.once('timeout', () => finish(new Error('ipc_timeout')));
66
+ socket.once('error', (err) => finish(err));
67
+ socket.on('data', (chunk) => {
68
+ buffer += chunk;
69
+ const idx = buffer.indexOf('\n');
70
+ if (idx >= 0) {
71
+ const line = buffer.slice(0, idx);
72
+ try {
73
+ finish(null, JSON.parse(line));
74
+ } catch (err) {
75
+ finish(err);
76
+ }
77
+ }
78
+ });
79
+ socket.connect(port, host, () => {
80
+ socket.write(JSON.stringify(message) + '\n');
81
+ });
82
+ });
83
+ }
84
+
85
+ module.exports = { createServer, listen, send, HOST };
package/src/runtime.js ADDED
@@ -0,0 +1,80 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const net = require('node:net');
5
+ const { RUNTIME_PATH, ensureDir } = require('./config');
6
+
7
+ function readRuntime() {
8
+ if (!fs.existsSync(RUNTIME_PATH)) return null;
9
+ try {
10
+ return JSON.parse(fs.readFileSync(RUNTIME_PATH, 'utf8'));
11
+ } catch {
12
+ return null;
13
+ }
14
+ }
15
+
16
+ function writeRuntime(info) {
17
+ ensureDir();
18
+ fs.writeFileSync(RUNTIME_PATH, JSON.stringify(info, null, 2), 'utf8');
19
+ }
20
+
21
+ function clearRuntime() {
22
+ if (fs.existsSync(RUNTIME_PATH)) {
23
+ try {
24
+ fs.unlinkSync(RUNTIME_PATH);
25
+ } catch {
26
+ /* ignore */
27
+ }
28
+ }
29
+ }
30
+
31
+ function isProcessAlive(pid) {
32
+ if (!pid) return false;
33
+ try {
34
+ process.kill(pid, 0);
35
+ return true;
36
+ } catch (err) {
37
+ return err.code === 'EPERM';
38
+ }
39
+ }
40
+
41
+ function probePort(port, host = '127.0.0.1', timeoutMs = 300) {
42
+ return new Promise((resolve) => {
43
+ const socket = new net.Socket();
44
+ let done = false;
45
+ const finish = (ok) => {
46
+ if (done) return;
47
+ done = true;
48
+ socket.destroy();
49
+ resolve(ok);
50
+ };
51
+ socket.setTimeout(timeoutMs);
52
+ socket.once('connect', () => finish(true));
53
+ socket.once('timeout', () => finish(false));
54
+ socket.once('error', () => finish(false));
55
+ socket.connect(port, host);
56
+ });
57
+ }
58
+
59
+ async function getRunningInstance() {
60
+ const info = readRuntime();
61
+ if (!info) return null;
62
+ if (!isProcessAlive(info.pid)) {
63
+ clearRuntime();
64
+ return null;
65
+ }
66
+ if (!(await probePort(info.port))) {
67
+ clearRuntime();
68
+ return null;
69
+ }
70
+ return info;
71
+ }
72
+
73
+ module.exports = {
74
+ readRuntime,
75
+ writeRuntime,
76
+ clearRuntime,
77
+ isProcessAlive,
78
+ probePort,
79
+ getRunningInstance,
80
+ };