ccc-notifier 0.1.0 → 0.3.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/README.md +17 -10
- package/dist/{budget-5H2GQRXH.js → budget-KKTFYAXH.js} +2 -2
- package/dist/{chunk-KUJZYZSG.js → chunk-4RZ6OGTD.js} +1 -1
- package/dist/{chunk-L5MLVTA2.js → chunk-7KVOQ4UZ.js} +258 -42
- package/dist/chunk-DSV75EF7.js +357 -0
- package/dist/{chunk-IIYMGLV4.js → chunk-ECADO26T.js} +9 -2
- package/dist/chunk-HTYUYKFW.js +21 -0
- package/dist/{chunk-TBFKGFZX.js → chunk-J5QAYTFE.js} +3 -0
- package/dist/{chunk-OEG3AVU6.js → chunk-LHKBGA5K.js} +27 -1
- package/dist/{chunk-4JNZ7BHO.js → chunk-NV5UOHJA.js} +1 -1
- package/dist/{chunk-64N5SGTT.js → chunk-QX5KIRSU.js} +109 -13
- package/dist/cli.js +72 -19
- package/dist/{dashboard-LKK6NOBN.js → dashboard-TH3NOVT3.js} +3 -3
- package/dist/{history-MWFHO5VR.js → history-DLFYQJFM.js} +1 -1
- package/dist/{mute-2JW3NQXN.js → mute-EPLYH67P.js} +2 -2
- package/dist/{setup-RT62VO37.js → setup-PTO5N46B.js} +5 -4
- package/dist/{sweep-XKKODGMA.js → sweep-25EZAOGC.js} +121 -9
- package/dist/{track-PBHTVZ3L.js → track-PXFSGMGL.js} +30 -14
- package/package.json +5 -2
- package/dist/chunk-TB5A7U7G.js +0 -82
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ccc-notifier
|
|
2
2
|
|
|
3
|
-
**ccc = Claude Code Cost
|
|
3
|
+
**ccc = Claude Code Cost**(現在は [Codex CLI](docs/codex.md) のコストも同じ仕組みで扱えます)。Claude Code や Codex CLI で **プロンプトを実行するたび**、そのターンにかかったコストを **$(USD)と¥(JPY)の両方** で自動通知するツールです。**5分で導入できます。**
|
|
4
4
|
|
|
5
5
|
```
|
|
6
6
|
💰 API換算 $0.267(¥40)| Fable 5
|
|
@@ -18,18 +18,20 @@ in 1.2k(cache 40%)/ out 480 · 📁 my-app · 今日: $1.85
|
|
|
18
18
|
|
|
19
19
|
## 特徴 / Features
|
|
20
20
|
|
|
21
|
-
- **ターン毎に自動通知** — Claude Code の応答が完了するたび(Stop hook)に、そのターンのコストを自動でプッシュ通知します。自分から `/cost` を見に行く必要はありません
|
|
21
|
+
- **ターン毎に自動通知** — Claude Code / Codex CLI の応答が完了するたび(Stop hook)に、そのターンのコストを自動でプッシュ通知します。自分から `/cost` を見に行く必要はありません
|
|
22
22
|
- **$ と ¥ を併記** — USD と JPY の両方を毎回表示します(為替レートは自動取得 + キャッシュ + 固定フォールバックの三段構え)
|
|
23
23
|
- **プロンプト全文をローカルに履歴保存** — `~/.ccc-notifier/history.jsonl` にそのターンのプロンプト全文を保存します(外部には送信されません)
|
|
24
|
-
- **HTMLダッシュボード** — `dashboard` コマンドで、サマリー・コスト推移(**日 / 週 / 月**で切替、横スクロールで過去まで)・モデル別/プロジェクト別内訳・検索できるターン履歴を1枚の HTML(完全自己完結・ライト/ダーク対応)に書き出してブラウザで開きます。棒をクリックするとその期間が選択され、内訳・履歴が連動(「通算」で全期間)
|
|
25
|
-
- **月予算(monthly budget)** — 月に使える金額(USD)を設定すると、ダッシュボードに**当月の使用額 / 予算・使用率(%)
|
|
24
|
+
- **HTMLダッシュボード** — `dashboard` コマンドで、サマリー・コスト推移(**日 / 週 / 月**で切替、横スクロールで過去まで)・モデル別/プロジェクト別内訳・検索できるターン履歴を1枚の HTML(完全自己完結・ライト/ダーク対応)に書き出してブラウザで開きます。棒をクリックするとその期間が選択され、内訳・履歴が連動(「通算」で全期間)。Codex CLI を併用していれば **Claude / Codex** のソースフィルタも使えます
|
|
25
|
+
- **月予算(monthly budget)** — 月に使える金額(USD)を設定すると、ダッシュボードに**当月の使用額 / 予算・使用率(%)** をプログレスバーで表示します(`init` の対話、または `ccc-notifier budget <金額>` で設定。Claude Code と Codex CLI の合算です)
|
|
26
|
+
- **Codex CLI もそのまま検出** — `init` を実行すると `~/.codex` の有無を見て、あれば Codex にも導入するか聞かれます。通知・履歴・ダッシュボード・`sweep` は Claude Code と全く同じ仕組みで扱います([詳細](docs/codex.md))
|
|
26
27
|
- **OS 標準の通知機構のみ使用・追加依存ゼロ** — 通知は macOS では `osascript`、Windows では PowerShell 標準のトースト通知機能のみで送信します(node-notifier 等の外部通知ライブラリには一切依存しません)
|
|
27
|
-
- **全処理ローカル・フェイルセーフ設計** — 通知や集計の処理が失敗しても、Claude Code 本体の応答は絶対にブロックしません
|
|
28
|
+
- **全処理ローカル・フェイルセーフ設計** — 通知や集計の処理が失敗しても、Claude Code / Codex CLI 本体の応答は絶対にブロックしません
|
|
28
29
|
|
|
29
30
|
## 必要環境 / Requirements
|
|
30
31
|
|
|
31
32
|
- Node.js 20 以上(未導入の場合は [Node.js の用意](docs/installing-node.md) を参照してください)
|
|
32
|
-
- Claude Code(
|
|
33
|
+
- Claude Code(インストール・利用中であること。`init` は Claude Code の Stop hook を必ず設定します)
|
|
34
|
+
- (任意)[Codex CLI](docs/codex.md) を併用している場合は、`init` が自動検出してそのコストも同じ仕組みで通知できます
|
|
33
35
|
|
|
34
36
|
Windows / WSL2 で使う場合は、環境ごとの手順を [Windows / WSL2 での導入](docs/windows-wsl2.md) にまとめています。
|
|
35
37
|
|
|
@@ -70,16 +72,17 @@ node dist/cli.js init
|
|
|
70
72
|
|
|
71
73
|
2. **質問に答える**
|
|
72
74
|
|
|
73
|
-
対話形式で次の4
|
|
75
|
+
対話形式で次の4点(Codex CLI を検出した場合はもう1点)を聞かれます。
|
|
74
76
|
|
|
75
|
-
- 通知チャネル(OS通知のみ / Slackのみ / OS通知+Slack)
|
|
77
|
+
- 通知チャネル(OS通知のみ / Slackのみ / OS通知+Slack / 通知なし(記録・ダッシュボードのみ))
|
|
76
78
|
- コスト表示ラベル(API換算 / 実額)
|
|
77
79
|
- USD/JPY のフォールバック為替レート(既定 150円)
|
|
78
80
|
- 月の予算(USD、既定 $400。`0` で無効。ダッシュボードに当月の使用率を表示。詳細は [月予算](docs/monthly-budget.md))
|
|
81
|
+
- (Codex CLI 検出時)Codex にもコスト通知を入れるか(既定 Yes。詳細は [Codex CLI 対応](docs/codex.md))
|
|
79
82
|
|
|
80
83
|
完了すると Claude Code の `~/.claude/settings.json` に Stop hook が自動で追記されます。**既存の設定内容(他の hook や設定)は一切変更されず**、書き込み前に必ず `settings.json.bak-<タイムスタンプ>` としてバックアップが作成されます。settings.json が壊れている(JSONとして解析できない)場合は自動編集を諦め、手動で追記する内容を画面に表示するだけで、ファイルには一切書き込みません。
|
|
81
84
|
|
|
82
|
-
3. **Claude Code で何か実行してみる**
|
|
85
|
+
3. **Claude Code(や Codex CLI)で何か実行してみる**
|
|
83
86
|
|
|
84
87
|
ひとこと実行して応答が完了すると、通知が届きます。
|
|
85
88
|
|
|
@@ -95,9 +98,12 @@ CI などから非対話で `init` したい場合は次のフラグが使えま
|
|
|
95
98
|
| `--os-only` | Slack を無効化し OS通知のみにする |
|
|
96
99
|
| `--slack-webhook <url>` | Slack Incoming Webhook URL を指定して有効化 |
|
|
97
100
|
| `--slack-only` | Slack のみにする(OS 通知を無効化)。`--slack-webhook` と併用が必須 |
|
|
101
|
+
| `--no-notify` | 通知なし(記録・ダッシュボードのみ)。`--os-only` / `--slack-only` / `--slack-webhook` とは併用不可(例: `npx ccc-notifier init --yes --no-notify`。詳細は [設定](docs/configuration.md#通知なしモード記録ダッシュボードのみ--dashboard-only-mode)) |
|
|
98
102
|
| `--label <api_equivalent\|actual>` | コスト表示ラベルを指定 |
|
|
99
103
|
| `--rate <number>` | USD/JPY フォールバックレートを指定 |
|
|
100
104
|
| `--budget <USD>` | 月予算(USD)を指定(0 で無効)。未指定なら既定 **$400**(既存設定があれば維持) |
|
|
105
|
+
| `--codex` | Codex CLI にも Stop hook を導入する(`~/.codex` 未検出でも強制導入。詳細は [Codex CLI 対応](docs/codex.md)) |
|
|
106
|
+
| `--no-codex` | Codex hook を導入しない(検出しても触らない)。`--codex` とは併用不可 |
|
|
101
107
|
|
|
102
108
|
## コマンド一覧 / Commands
|
|
103
109
|
|
|
@@ -138,7 +144,7 @@ CI などから非対話で `init` したい場合は次のフラグが使えま
|
|
|
138
144
|
npx ccc-notifier dashboard # 生成してブラウザで開く
|
|
139
145
|
```
|
|
140
146
|
|
|
141
|
-
グラフの棒をクリックするとその期間に内訳・履歴が連動し、「通算」で全期間に戻せます。Claude Code の応答完了ごとに自動再生成され、開きっぱなしのタブは約30
|
|
147
|
+
グラフの棒をクリックするとその期間に内訳・履歴が連動し、「通算」で全期間に戻せます。Claude Code / Codex CLI の応答完了ごとに自動再生成され、開きっぱなしのタブは約30秒ごとに最新化されます。Codex CLI のレコードがあれば **Claude / Codex** を絞り込むソースフィルタも表示されます(詳細は [Codex CLI 対応](docs/codex.md))。検索・行クリックでプロンプト全文を確認できます:
|
|
142
148
|
|
|
143
149
|

|
|
144
150
|
|
|
@@ -157,6 +163,7 @@ npx ccc-notifier dashboard # 生成してブラウザで開く
|
|
|
157
163
|
|
|
158
164
|
- [Node.js の用意](docs/installing-node.md) — Node.js 20 が未導入の方へ(mise / 公式インストーラ)
|
|
159
165
|
- [Windows / WSL2 での導入](docs/windows-wsl2.md) — ネイティブ Windows と WSL2 での手順
|
|
166
|
+
- [Codex CLI 対応](docs/codex.md) — 導入・hook の信頼承認・仕組み・制限
|
|
160
167
|
- [ダッシュボード](docs/dashboard.md) — 期間の連動・自動更新・履歴の削除
|
|
161
168
|
- [月予算 / Monthly budget](docs/monthly-budget.md) — 当月の使用率表示
|
|
162
169
|
- [過去分の取り込み / sweep](docs/sweep.md) — hook 導入前・後から完了した分の回収
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
import {
|
|
3
3
|
formatJPY,
|
|
4
4
|
formatUSD
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-J5QAYTFE.js";
|
|
6
6
|
import {
|
|
7
7
|
currentMonthTotals,
|
|
8
8
|
paths,
|
|
9
9
|
readConfig
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-ECADO26T.js";
|
|
11
11
|
|
|
12
12
|
// src/budget.ts
|
|
13
13
|
import { writeFileSync } from "fs";
|
|
@@ -2,42 +2,177 @@
|
|
|
2
2
|
import {
|
|
3
3
|
notifyOS,
|
|
4
4
|
notifySlack
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-NV5UOHJA.js";
|
|
6
|
+
import {
|
|
7
|
+
codexHome,
|
|
8
|
+
detectCodex
|
|
9
|
+
} from "./chunk-HTYUYKFW.js";
|
|
6
10
|
import {
|
|
7
11
|
paths,
|
|
8
12
|
readConfig
|
|
9
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-ECADO26T.js";
|
|
10
14
|
|
|
11
15
|
// src/setup.ts
|
|
16
|
+
import {
|
|
17
|
+
copyFileSync as copyFileSync2,
|
|
18
|
+
existsSync as existsSync2,
|
|
19
|
+
mkdirSync as mkdirSync2,
|
|
20
|
+
readFileSync as readFileSync2,
|
|
21
|
+
rmSync,
|
|
22
|
+
writeFileSync as writeFileSync2
|
|
23
|
+
} from "fs";
|
|
24
|
+
import { basename, dirname as dirname2, join as join2 } from "path";
|
|
25
|
+
import { homedir } from "os";
|
|
26
|
+
import { fileURLToPath } from "url";
|
|
27
|
+
import * as p from "@clack/prompts";
|
|
28
|
+
|
|
29
|
+
// src/codex/setup.ts
|
|
12
30
|
import {
|
|
13
31
|
copyFileSync,
|
|
14
32
|
existsSync,
|
|
15
33
|
mkdirSync,
|
|
16
34
|
readFileSync,
|
|
17
|
-
rmSync,
|
|
18
35
|
writeFileSync
|
|
19
36
|
} from "fs";
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
37
|
+
import { dirname, join } from "path";
|
|
38
|
+
function isPlainObject(v) {
|
|
39
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
40
|
+
}
|
|
41
|
+
function codexHooksFile() {
|
|
42
|
+
return join(codexHome(), "hooks.json");
|
|
43
|
+
}
|
|
44
|
+
function codexHookCommand(nodePath, cliPath) {
|
|
45
|
+
let node = nodePath;
|
|
46
|
+
let cli = cliPath;
|
|
47
|
+
if (process.platform === "win32") {
|
|
48
|
+
node = node.replace(/\\/g, "/");
|
|
49
|
+
cli = cli.replace(/\\/g, "/");
|
|
50
|
+
}
|
|
51
|
+
return `"${node}" "${cli}" track --codex`;
|
|
52
|
+
}
|
|
53
|
+
function ourCodexStopEntry(command) {
|
|
54
|
+
return { hooks: [{ type: "command", command }] };
|
|
55
|
+
}
|
|
56
|
+
function isOurStopEntry(entry) {
|
|
57
|
+
if (!isPlainObject(entry)) return false;
|
|
58
|
+
const hooks = entry.hooks;
|
|
59
|
+
if (!Array.isArray(hooks)) return false;
|
|
60
|
+
return hooks.some(
|
|
61
|
+
(h) => isPlainObject(h) && typeof h.command === "string" && matchesMarker(h.command)
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
function buildManualSnippet(command) {
|
|
65
|
+
return JSON.stringify(ourCodexStopEntry(command), null, 2);
|
|
66
|
+
}
|
|
67
|
+
function backupHooks(path) {
|
|
68
|
+
const backupPath = `${path}.bak-${Date.now()}`;
|
|
69
|
+
copyFileSync(path, backupPath);
|
|
70
|
+
return backupPath;
|
|
71
|
+
}
|
|
72
|
+
function writeHooks(path, obj) {
|
|
73
|
+
writeFileSync(path, JSON.stringify(obj, null, 2) + "\n", "utf8");
|
|
74
|
+
}
|
|
75
|
+
function registerCodexHook(nodePath, cliPath) {
|
|
76
|
+
const hooksFile = codexHooksFile();
|
|
77
|
+
const command = codexHookCommand(nodePath, cliPath);
|
|
78
|
+
if (!existsSync(hooksFile)) {
|
|
79
|
+
mkdirSync(dirname(hooksFile), { recursive: true });
|
|
80
|
+
writeHooks(hooksFile, { hooks: { Stop: [ourCodexStopEntry(command)] } });
|
|
81
|
+
return { status: "written", backupPath: null };
|
|
82
|
+
}
|
|
83
|
+
const raw = readFileSync(hooksFile, "utf8");
|
|
84
|
+
let parsed;
|
|
85
|
+
try {
|
|
86
|
+
parsed = JSON.parse(raw);
|
|
87
|
+
} catch {
|
|
88
|
+
return { status: "manual", backupPath: null, manualSnippet: buildManualSnippet(command) };
|
|
89
|
+
}
|
|
90
|
+
if (!isPlainObject(parsed)) {
|
|
91
|
+
return { status: "manual", backupPath: null, manualSnippet: buildManualSnippet(command) };
|
|
92
|
+
}
|
|
93
|
+
const obj = parsed;
|
|
94
|
+
const hooksVal = obj.hooks;
|
|
95
|
+
if (hooksVal !== void 0 && !isPlainObject(hooksVal)) {
|
|
96
|
+
return { status: "manual", backupPath: null, manualSnippet: buildManualSnippet(command) };
|
|
97
|
+
}
|
|
98
|
+
const hooks = hooksVal ?? {};
|
|
99
|
+
const stopVal = hooks.Stop;
|
|
100
|
+
if (stopVal !== void 0 && !Array.isArray(stopVal)) {
|
|
101
|
+
return { status: "manual", backupPath: null, manualSnippet: buildManualSnippet(command) };
|
|
102
|
+
}
|
|
103
|
+
const stop = stopVal ?? [];
|
|
104
|
+
const idx = stop.findIndex(isOurStopEntry);
|
|
105
|
+
if (idx >= 0) {
|
|
106
|
+
const entry = stop[idx];
|
|
107
|
+
const entryHooks = entry.hooks;
|
|
108
|
+
const needsUpdate = entryHooks.some(
|
|
109
|
+
(h) => isPlainObject(h) && typeof h.command === "string" && matchesMarker(h.command) && h.command !== command
|
|
110
|
+
);
|
|
111
|
+
if (!needsUpdate) return { status: "unchanged", backupPath: null };
|
|
112
|
+
const backupPath2 = backupHooks(hooksFile);
|
|
113
|
+
for (const h of entryHooks) {
|
|
114
|
+
if (isPlainObject(h) && typeof h.command === "string" && matchesMarker(h.command)) {
|
|
115
|
+
h.command = command;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
obj.hooks = hooks;
|
|
119
|
+
hooks.Stop = stop;
|
|
120
|
+
writeHooks(hooksFile, obj);
|
|
121
|
+
return { status: "written", backupPath: backupPath2 };
|
|
122
|
+
}
|
|
123
|
+
const backupPath = backupHooks(hooksFile);
|
|
124
|
+
stop.push(ourCodexStopEntry(command));
|
|
125
|
+
obj.hooks = hooks;
|
|
126
|
+
hooks.Stop = stop;
|
|
127
|
+
writeHooks(hooksFile, obj);
|
|
128
|
+
return { status: "written", backupPath };
|
|
129
|
+
}
|
|
130
|
+
function removeCodexHook() {
|
|
131
|
+
const hooksFile = codexHooksFile();
|
|
132
|
+
if (!existsSync(hooksFile)) return { status: "unchanged", backupPath: null };
|
|
133
|
+
const raw = readFileSync(hooksFile, "utf8");
|
|
134
|
+
let parsed;
|
|
135
|
+
try {
|
|
136
|
+
parsed = JSON.parse(raw);
|
|
137
|
+
} catch {
|
|
138
|
+
return { status: "unchanged", backupPath: null };
|
|
139
|
+
}
|
|
140
|
+
if (!isPlainObject(parsed)) return { status: "unchanged", backupPath: null };
|
|
141
|
+
const obj = parsed;
|
|
142
|
+
const hooks = isPlainObject(obj.hooks) ? obj.hooks : null;
|
|
143
|
+
const stop = hooks && Array.isArray(hooks.Stop) ? hooks.Stop : null;
|
|
144
|
+
if (!hooks || !stop || !stop.some(isOurStopEntry)) {
|
|
145
|
+
return { status: "unchanged", backupPath: null };
|
|
146
|
+
}
|
|
147
|
+
const backupPath = backupHooks(hooksFile);
|
|
148
|
+
const filtered = stop.filter((e) => !isOurStopEntry(e));
|
|
149
|
+
if (filtered.length === 0) {
|
|
150
|
+
delete hooks.Stop;
|
|
151
|
+
} else {
|
|
152
|
+
hooks.Stop = filtered;
|
|
153
|
+
}
|
|
154
|
+
writeHooks(hooksFile, obj);
|
|
155
|
+
return { status: "written", backupPath };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/setup.ts
|
|
24
159
|
var HOOK_MARKER = "ccc-notifier";
|
|
25
160
|
var HOOK_TIMEOUT = 15;
|
|
26
161
|
var SLACK_PROMPT_CHARS = 100;
|
|
27
162
|
var DEFAULT_BUDGET_USD = 400;
|
|
28
|
-
function
|
|
163
|
+
function isPlainObject2(v) {
|
|
29
164
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
30
165
|
}
|
|
31
166
|
function settingsPath() {
|
|
32
|
-
return process.env.CCCN_CLAUDE_SETTINGS ||
|
|
167
|
+
return process.env.CCCN_CLAUDE_SETTINGS || join2(homedir(), ".claude", "settings.json");
|
|
33
168
|
}
|
|
34
169
|
function resolveCliPath() {
|
|
35
170
|
const override = process.env.CCCN_CLI_PATH;
|
|
36
171
|
if (override) return override;
|
|
37
172
|
const here = fileURLToPath(import.meta.url);
|
|
38
|
-
const dir =
|
|
39
|
-
if (basename(dir) === "dist") return
|
|
40
|
-
return
|
|
173
|
+
const dir = dirname2(here);
|
|
174
|
+
if (basename(dir) === "dist") return join2(dir, "cli.js");
|
|
175
|
+
return join2(dir, "..", "dist", "cli.js");
|
|
41
176
|
}
|
|
42
177
|
function buildHookCommand() {
|
|
43
178
|
let nodePath = process.execPath;
|
|
@@ -51,12 +186,12 @@ function buildHookCommand() {
|
|
|
51
186
|
function matchesMarker(command) {
|
|
52
187
|
return command.includes(HOOK_MARKER);
|
|
53
188
|
}
|
|
54
|
-
function
|
|
55
|
-
if (!
|
|
189
|
+
function isOurStopEntry2(entry) {
|
|
190
|
+
if (!isPlainObject2(entry)) return false;
|
|
56
191
|
const hooks = entry.hooks;
|
|
57
192
|
if (!Array.isArray(hooks)) return false;
|
|
58
193
|
return hooks.some(
|
|
59
|
-
(h) =>
|
|
194
|
+
(h) => isPlainObject2(h) && typeof h.command === "string" && matchesMarker(h.command)
|
|
60
195
|
);
|
|
61
196
|
}
|
|
62
197
|
function ourStopEntry(command) {
|
|
@@ -64,7 +199,7 @@ function ourStopEntry(command) {
|
|
|
64
199
|
}
|
|
65
200
|
function backupSettings(path) {
|
|
66
201
|
const backupPath = `${path}.bak-${Date.now()}`;
|
|
67
|
-
|
|
202
|
+
copyFileSync2(path, backupPath);
|
|
68
203
|
return backupPath;
|
|
69
204
|
}
|
|
70
205
|
function printManualSnippet(path, command, reason) {
|
|
@@ -100,12 +235,22 @@ function takeValue(argv, i, prefix) {
|
|
|
100
235
|
return argv[i + 1];
|
|
101
236
|
}
|
|
102
237
|
function parseInitFlags(argv) {
|
|
103
|
-
const flags = {
|
|
238
|
+
const flags = {
|
|
239
|
+
yes: false,
|
|
240
|
+
osOnly: false,
|
|
241
|
+
slackOnly: false,
|
|
242
|
+
noNotify: false,
|
|
243
|
+
codex: false,
|
|
244
|
+
noCodex: false
|
|
245
|
+
};
|
|
104
246
|
for (let i = 0; i < argv.length; i++) {
|
|
105
247
|
const a = argv[i];
|
|
106
248
|
if (a === "--yes" || a === "-y") flags.yes = true;
|
|
107
249
|
else if (a === "--os-only") flags.osOnly = true;
|
|
108
250
|
else if (a === "--slack-only") flags.slackOnly = true;
|
|
251
|
+
else if (a === "--no-notify") flags.noNotify = true;
|
|
252
|
+
else if (a === "--codex") flags.codex = true;
|
|
253
|
+
else if (a === "--no-codex") flags.noCodex = true;
|
|
109
254
|
else if (a === "--slack-webhook" || a.startsWith("--slack-webhook=")) {
|
|
110
255
|
flags.slackWebhook = takeValue(argv, i, "--slack-webhook");
|
|
111
256
|
if (!a.includes("=")) i++;
|
|
@@ -131,13 +276,13 @@ function parseUninstallFlags(argv) {
|
|
|
131
276
|
return flags;
|
|
132
277
|
}
|
|
133
278
|
function mergeSettings(sPath, command) {
|
|
134
|
-
if (!
|
|
135
|
-
|
|
279
|
+
if (!existsSync2(sPath)) {
|
|
280
|
+
mkdirSync2(dirname2(sPath), { recursive: true });
|
|
136
281
|
const fresh = { hooks: { Stop: [ourStopEntry(command)] } };
|
|
137
|
-
|
|
282
|
+
writeFileSync2(sPath, JSON.stringify(fresh, null, 2) + "\n", "utf8");
|
|
138
283
|
return { ok: true, backupPath: null };
|
|
139
284
|
}
|
|
140
|
-
const raw =
|
|
285
|
+
const raw = readFileSync2(sPath, "utf8");
|
|
141
286
|
let parsed;
|
|
142
287
|
try {
|
|
143
288
|
parsed = JSON.parse(raw);
|
|
@@ -145,22 +290,22 @@ function mergeSettings(sPath, command) {
|
|
|
145
290
|
printManualSnippet(sPath, command, "settings.json \u306E JSON \u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F");
|
|
146
291
|
return { ok: false, backupPath: null };
|
|
147
292
|
}
|
|
148
|
-
if (!
|
|
293
|
+
if (!isPlainObject2(parsed)) {
|
|
149
294
|
printManualSnippet(sPath, command, "settings.json \u306E\u30EB\u30FC\u30C8\u304C\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u306F\u3042\u308A\u307E\u305B\u3093");
|
|
150
295
|
return { ok: false, backupPath: null };
|
|
151
296
|
}
|
|
152
297
|
const obj = parsed;
|
|
153
298
|
const backupPath = backupSettings(sPath);
|
|
154
|
-
if (!
|
|
299
|
+
if (!isPlainObject2(obj.hooks)) obj.hooks = {};
|
|
155
300
|
const hooks = obj.hooks;
|
|
156
301
|
if (!Array.isArray(hooks.Stop)) hooks.Stop = [];
|
|
157
302
|
const stop = hooks.Stop;
|
|
158
|
-
const idx = stop.findIndex(
|
|
303
|
+
const idx = stop.findIndex(isOurStopEntry2);
|
|
159
304
|
if (idx >= 0) {
|
|
160
305
|
const entry = stop[idx];
|
|
161
306
|
const entryHooks = entry.hooks;
|
|
162
307
|
for (const h of entryHooks) {
|
|
163
|
-
if (
|
|
308
|
+
if (isPlainObject2(h) && typeof h.command === "string" && matchesMarker(h.command)) {
|
|
164
309
|
h.command = command;
|
|
165
310
|
h.timeout = HOOK_TIMEOUT;
|
|
166
311
|
}
|
|
@@ -168,16 +313,29 @@ function mergeSettings(sPath, command) {
|
|
|
168
313
|
} else {
|
|
169
314
|
stop.push(ourStopEntry(command));
|
|
170
315
|
}
|
|
171
|
-
|
|
316
|
+
writeFileSync2(sPath, JSON.stringify(obj, null, 2) + "\n", "utf8");
|
|
172
317
|
return { ok: true, backupPath };
|
|
173
318
|
}
|
|
174
319
|
async function runInit(argv) {
|
|
175
320
|
const flags = parseInitFlags(argv);
|
|
321
|
+
if (flags.codex && flags.noCodex) {
|
|
322
|
+
console.error("--codex \u3068 --no-codex \u306F\u540C\u6642\u306B\u6307\u5B9A\u3067\u304D\u307E\u305B\u3093");
|
|
323
|
+
return 1;
|
|
324
|
+
}
|
|
325
|
+
let installCodex = false;
|
|
176
326
|
const cfg = readConfig();
|
|
327
|
+
const initialChannel = !cfg.notify.os && !cfg.notify.slack ? "none" : cfg.notify.slack ? cfg.notify.os ? "both" : "slack" : "os";
|
|
177
328
|
cfg.notify.os = true;
|
|
178
329
|
const budgetDefault = cfg.monthlyBudgetUSD > 0 ? cfg.monthlyBudgetUSD : DEFAULT_BUDGET_USD;
|
|
179
330
|
if (flags.yes) {
|
|
180
|
-
if (flags.
|
|
331
|
+
if (flags.noNotify) {
|
|
332
|
+
if (flags.osOnly || flags.slackOnly || flags.slackWebhook !== void 0) {
|
|
333
|
+
console.error("--no-notify \u3068 --os-only / --slack-only / --slack-webhook \u306F\u540C\u6642\u306B\u6307\u5B9A\u3067\u304D\u307E\u305B\u3093");
|
|
334
|
+
return 1;
|
|
335
|
+
}
|
|
336
|
+
cfg.notify.os = false;
|
|
337
|
+
cfg.notify.slack = null;
|
|
338
|
+
} else if (flags.slackOnly) {
|
|
181
339
|
if (flags.osOnly) {
|
|
182
340
|
console.error("--slack-only \u3068 --os-only \u306F\u540C\u6642\u306B\u6307\u5B9A\u3067\u304D\u307E\u305B\u3093");
|
|
183
341
|
return 1;
|
|
@@ -226,6 +384,7 @@ async function runInit(argv) {
|
|
|
226
384
|
} else {
|
|
227
385
|
cfg.monthlyBudgetUSD = budgetDefault;
|
|
228
386
|
}
|
|
387
|
+
installCodex = flags.codex;
|
|
229
388
|
} else {
|
|
230
389
|
p.intro("ccc-notifier \u30BB\u30C3\u30C8\u30A2\u30C3\u30D7");
|
|
231
390
|
const channel = await p.select({
|
|
@@ -233,9 +392,10 @@ async function runInit(argv) {
|
|
|
233
392
|
options: [
|
|
234
393
|
{ value: "os", label: "OS \u901A\u77E5\u306E\u307F" },
|
|
235
394
|
{ value: "slack", label: "Slack \u306E\u307F(OS \u901A\u77E5\u306A\u3057)" },
|
|
236
|
-
{ value: "both", label: "OS \u901A\u77E5 + Slack" }
|
|
395
|
+
{ value: "both", label: "OS \u901A\u77E5 + Slack" },
|
|
396
|
+
{ value: "none", label: "\u901A\u77E5\u306A\u3057(\u8A18\u9332\u30FB\u30C0\u30C3\u30B7\u30E5\u30DC\u30FC\u30C9\u306E\u307F)" }
|
|
237
397
|
],
|
|
238
|
-
initialValue:
|
|
398
|
+
initialValue: initialChannel
|
|
239
399
|
});
|
|
240
400
|
if (p.isCancel(channel)) {
|
|
241
401
|
p.cancel("\u30AD\u30E3\u30F3\u30BB\u30EB\u3057\u307E\u3057\u305F");
|
|
@@ -259,6 +419,7 @@ async function runInit(argv) {
|
|
|
259
419
|
cfg.notify.os = channel === "both";
|
|
260
420
|
} else {
|
|
261
421
|
cfg.notify.slack = null;
|
|
422
|
+
cfg.notify.os = channel === "os";
|
|
262
423
|
}
|
|
263
424
|
const labelChoice = await p.select({
|
|
264
425
|
message: "\u30B3\u30B9\u30C8\u8868\u793A\u30E9\u30D9\u30EB",
|
|
@@ -305,18 +466,42 @@ async function runInit(argv) {
|
|
|
305
466
|
}
|
|
306
467
|
const parsedBudget = Number(budgetStr);
|
|
307
468
|
if (Number.isFinite(parsedBudget) && parsedBudget >= 0) cfg.monthlyBudgetUSD = parsedBudget;
|
|
469
|
+
if (flags.codex) {
|
|
470
|
+
installCodex = true;
|
|
471
|
+
} else if (flags.noCodex) {
|
|
472
|
+
installCodex = false;
|
|
473
|
+
} else if (detectCodex()) {
|
|
474
|
+
const codexConfirm = await p.confirm({
|
|
475
|
+
message: "Codex CLI \u3092\u691C\u51FA\u3057\u307E\u3057\u305F\u3002Codex \u306B\u3082\u30B3\u30B9\u30C8\u901A\u77E5\u3092\u5165\u308C\u307E\u3059\u304B?",
|
|
476
|
+
initialValue: true
|
|
477
|
+
});
|
|
478
|
+
if (p.isCancel(codexConfirm)) {
|
|
479
|
+
p.cancel("\u30AD\u30E3\u30F3\u30BB\u30EB\u3057\u307E\u3057\u305F");
|
|
480
|
+
return 1;
|
|
481
|
+
}
|
|
482
|
+
installCodex = codexConfirm;
|
|
483
|
+
}
|
|
308
484
|
p.outro("\u8A2D\u5B9A\u3092\u9069\u7528\u3057\u307E\u3059");
|
|
309
485
|
}
|
|
310
486
|
const cccn = paths();
|
|
311
|
-
|
|
487
|
+
writeFileSync2(cccn.configFile, JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
312
488
|
const command = buildHookCommand();
|
|
313
489
|
const sPath = settingsPath();
|
|
314
490
|
const result = mergeSettings(sPath, command);
|
|
315
491
|
if (!result.ok) return 1;
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
492
|
+
let codexResult = null;
|
|
493
|
+
if (installCodex) {
|
|
494
|
+
codexResult = registerCodexHook(process.execPath, resolveCliPath());
|
|
495
|
+
}
|
|
496
|
+
const notifyDisabled = !cfg.notify.os && !cfg.notify.slack;
|
|
497
|
+
if (notifyDisabled) {
|
|
498
|
+
console.log("\u30C6\u30B9\u30C8\u901A\u77E5: \u901A\u77E5\u306A\u3057\u30E2\u30FC\u30C9\u306E\u305F\u3081\u30B9\u30AD\u30C3\u30D7\u3057\u307E\u3057\u305F");
|
|
499
|
+
} else {
|
|
500
|
+
const testRecord = makeTestRecord();
|
|
501
|
+
await notifyOS(testRecord, cfg);
|
|
502
|
+
if (cfg.notify.slack) {
|
|
503
|
+
await notifySlack(testRecord, cfg);
|
|
504
|
+
}
|
|
320
505
|
}
|
|
321
506
|
console.log("");
|
|
322
507
|
console.log(`settings \u3092\u66F4\u65B0\u3057\u307E\u3057\u305F: ${sPath}`);
|
|
@@ -324,16 +509,42 @@ async function runInit(argv) {
|
|
|
324
509
|
result.backupPath ? ` \u30D0\u30C3\u30AF\u30A2\u30C3\u30D7: ${result.backupPath}` : " (settings.json \u3092\u65B0\u898F\u4F5C\u6210\u3057\u305F\u305F\u3081\u30D0\u30C3\u30AF\u30A2\u30C3\u30D7\u306F\u3042\u308A\u307E\u305B\u3093)"
|
|
325
510
|
);
|
|
326
511
|
console.log(` \u8A2D\u5B9A\u30D5\u30A1\u30A4\u30EB: ${cccn.configFile}`);
|
|
327
|
-
|
|
512
|
+
if (notifyDisabled) {
|
|
513
|
+
console.log(
|
|
514
|
+
"\u901A\u77E5\u306A\u3057\u30E2\u30FC\u30C9\u3067\u3059\u3002Claude Code \u306E\u5229\u7528\u306F\u81EA\u52D5\u3067\u8A18\u9332\u3055\u308C\u307E\u3059\u3002\u30C0\u30C3\u30B7\u30E5\u30DC\u30FC\u30C9: npx ccc-notifier dashboard"
|
|
515
|
+
);
|
|
516
|
+
} else {
|
|
517
|
+
console.log("Claude Code \u3067\u4F55\u304B\u5B9F\u884C\u3059\u308B\u3068\u901A\u77E5\u304C\u5C4A\u304D\u307E\u3059\u3002\u78BA\u8A8D: npx ccc-notifier doctor");
|
|
518
|
+
}
|
|
519
|
+
if (codexResult) {
|
|
520
|
+
if (codexResult.status === "written") {
|
|
521
|
+
console.log(`Codex \u306B\u3082 Stop hook \u3092\u767B\u9332\u3057\u307E\u3057\u305F: ${codexHooksFile()}`);
|
|
522
|
+
if (codexResult.backupPath) {
|
|
523
|
+
console.log(` \u30D0\u30C3\u30AF\u30A2\u30C3\u30D7: ${codexResult.backupPath}`);
|
|
524
|
+
}
|
|
525
|
+
console.log(
|
|
526
|
+
"\u6B21\u56DE codex \u8D77\u52D5\u6642\u306B\u300EHooks need review\u300F\u304C\u8868\u793A\u3055\u308C\u307E\u3059\u3002\u300ETrust all and continue\u300F\u3092\u9078\u3076\u3068\u6709\u52B9\u306B\u306A\u308A\u307E\u3059(\u627F\u8A8D\u307E\u3067\u306F\u52D5\u304D\u307E\u305B\u3093)"
|
|
527
|
+
);
|
|
528
|
+
} else if (codexResult.status === "unchanged") {
|
|
529
|
+
console.log("Codex \u306E Stop hook \u306F\u767B\u9332\u6E08\u307F\u3067\u3059");
|
|
530
|
+
} else {
|
|
531
|
+
console.error(`Codex \u306E hooks.json \u3092\u81EA\u52D5\u7DE8\u96C6\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F: ${codexHooksFile()}`);
|
|
532
|
+
console.error("\u5B89\u5168\u306E\u305F\u3081 hooks.json \u306E\u81EA\u52D5\u7DE8\u96C6\u3092\u4E2D\u6B62\u3057\u307E\u3057\u305F\u3002");
|
|
533
|
+
console.error("\u4EE5\u4E0B\u306E JSON \u3092 hooks.json \u306E hooks.Stop \u914D\u5217\u306B\u624B\u52D5\u3067\u8FFD\u52A0\u3057\u3066\u304F\u3060\u3055\u3044:");
|
|
534
|
+
console.error("");
|
|
535
|
+
console.error(codexResult.manualSnippet ?? "");
|
|
536
|
+
console.error("");
|
|
537
|
+
}
|
|
538
|
+
}
|
|
328
539
|
return 0;
|
|
329
540
|
}
|
|
330
541
|
async function runUninstall(argv) {
|
|
331
542
|
const flags = parseUninstallFlags(argv);
|
|
332
543
|
const sPath = settingsPath();
|
|
333
|
-
if (!
|
|
544
|
+
if (!existsSync2(sPath)) {
|
|
334
545
|
console.log("\u767B\u9332\u306A\u3057: settings.json \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3067\u3057\u305F\u3002");
|
|
335
546
|
} else {
|
|
336
|
-
const raw =
|
|
547
|
+
const raw = readFileSync2(sPath, "utf8");
|
|
337
548
|
let parsed;
|
|
338
549
|
let parseOk = true;
|
|
339
550
|
try {
|
|
@@ -341,29 +552,33 @@ async function runUninstall(argv) {
|
|
|
341
552
|
} catch {
|
|
342
553
|
parseOk = false;
|
|
343
554
|
}
|
|
344
|
-
if (!parseOk || !
|
|
555
|
+
if (!parseOk || !isPlainObject2(parsed)) {
|
|
345
556
|
console.error(`settings.json \u3092\u89E3\u6790\u3067\u304D\u306A\u3044\u305F\u3081\u7DE8\u96C6\u3092\u30B9\u30AD\u30C3\u30D7\u3057\u307E\u3057\u305F: ${sPath}`);
|
|
346
557
|
} else {
|
|
347
558
|
const obj = parsed;
|
|
348
|
-
const hooks =
|
|
559
|
+
const hooks = isPlainObject2(obj.hooks) ? obj.hooks : null;
|
|
349
560
|
const stop = hooks && Array.isArray(hooks.Stop) ? hooks.Stop : null;
|
|
350
|
-
const hasMarker = stop ? stop.some(
|
|
561
|
+
const hasMarker = stop ? stop.some(isOurStopEntry2) : false;
|
|
351
562
|
if (!hasMarker || !hooks || !stop) {
|
|
352
563
|
console.log("\u767B\u9332\u306A\u3057: \u672C\u30C4\u30FC\u30EB\u306E Stop \u30D5\u30C3\u30AF\u306F\u767B\u9332\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002");
|
|
353
564
|
} else {
|
|
354
565
|
const backupPath = backupSettings(sPath);
|
|
355
|
-
const filtered = stop.filter((e) => !
|
|
566
|
+
const filtered = stop.filter((e) => !isOurStopEntry2(e));
|
|
356
567
|
if (filtered.length === 0) {
|
|
357
568
|
delete hooks.Stop;
|
|
358
569
|
} else {
|
|
359
570
|
hooks.Stop = filtered;
|
|
360
571
|
}
|
|
361
|
-
|
|
572
|
+
writeFileSync2(sPath, JSON.stringify(obj, null, 2) + "\n", "utf8");
|
|
362
573
|
console.log(`\u672C\u30C4\u30FC\u30EB\u306E Stop \u30D5\u30C3\u30AF\u3092\u9664\u53BB\u3057\u307E\u3057\u305F: ${sPath}`);
|
|
363
574
|
console.log(` \u30D0\u30C3\u30AF\u30A2\u30C3\u30D7: ${backupPath}`);
|
|
364
575
|
}
|
|
365
576
|
}
|
|
366
577
|
}
|
|
578
|
+
const codexRemoval = removeCodexHook();
|
|
579
|
+
if (codexRemoval.status === "written") {
|
|
580
|
+
console.log("Codex \u306E Stop hook \u3092\u524A\u9664\u3057\u307E\u3057\u305F");
|
|
581
|
+
}
|
|
367
582
|
if (flags.purge) {
|
|
368
583
|
const home = paths().home;
|
|
369
584
|
let doPurge = flags.yes;
|
|
@@ -387,6 +602,7 @@ async function runUninstall(argv) {
|
|
|
387
602
|
}
|
|
388
603
|
|
|
389
604
|
export {
|
|
605
|
+
codexHooksFile,
|
|
390
606
|
matchesMarker,
|
|
391
607
|
runInit,
|
|
392
608
|
runUninstall
|