ccc-notifier 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.
@@ -0,0 +1,393 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ notifyOS,
4
+ notifySlack
5
+ } from "./chunk-4JNZ7BHO.js";
6
+ import {
7
+ paths,
8
+ readConfig
9
+ } from "./chunk-IIYMGLV4.js";
10
+
11
+ // src/setup.ts
12
+ import {
13
+ copyFileSync,
14
+ existsSync,
15
+ mkdirSync,
16
+ readFileSync,
17
+ rmSync,
18
+ writeFileSync
19
+ } from "fs";
20
+ import { basename, dirname, join } from "path";
21
+ import { homedir } from "os";
22
+ import { fileURLToPath } from "url";
23
+ import * as p from "@clack/prompts";
24
+ var HOOK_MARKER = "ccc-notifier";
25
+ var HOOK_TIMEOUT = 15;
26
+ var SLACK_PROMPT_CHARS = 100;
27
+ var DEFAULT_BUDGET_USD = 400;
28
+ function isPlainObject(v) {
29
+ return typeof v === "object" && v !== null && !Array.isArray(v);
30
+ }
31
+ function settingsPath() {
32
+ return process.env.CCCN_CLAUDE_SETTINGS || join(homedir(), ".claude", "settings.json");
33
+ }
34
+ function resolveCliPath() {
35
+ const override = process.env.CCCN_CLI_PATH;
36
+ if (override) return override;
37
+ const here = fileURLToPath(import.meta.url);
38
+ const dir = dirname(here);
39
+ if (basename(dir) === "dist") return join(dir, "cli.js");
40
+ return join(dir, "..", "dist", "cli.js");
41
+ }
42
+ function buildHookCommand() {
43
+ let nodePath = process.execPath;
44
+ let cliPath = resolveCliPath();
45
+ if (process.platform === "win32") {
46
+ nodePath = nodePath.replace(/\\/g, "/");
47
+ cliPath = cliPath.replace(/\\/g, "/");
48
+ }
49
+ return `"${nodePath}" "${cliPath}" track`;
50
+ }
51
+ function matchesMarker(command) {
52
+ return command.includes(HOOK_MARKER);
53
+ }
54
+ function isOurStopEntry(entry) {
55
+ if (!isPlainObject(entry)) return false;
56
+ const hooks = entry.hooks;
57
+ if (!Array.isArray(hooks)) return false;
58
+ return hooks.some(
59
+ (h) => isPlainObject(h) && typeof h.command === "string" && matchesMarker(h.command)
60
+ );
61
+ }
62
+ function ourStopEntry(command) {
63
+ return { hooks: [{ type: "command", command, timeout: HOOK_TIMEOUT }] };
64
+ }
65
+ function backupSettings(path) {
66
+ const backupPath = `${path}.bak-${Date.now()}`;
67
+ copyFileSync(path, backupPath);
68
+ return backupPath;
69
+ }
70
+ function printManualSnippet(path, command, reason) {
71
+ const snippet = JSON.stringify(ourStopEntry(command), null, 2);
72
+ console.error(`ERROR: ${reason}: ${path}`);
73
+ console.error("\u5B89\u5168\u306E\u305F\u3081 settings.json \u306E\u81EA\u52D5\u7DE8\u96C6\u3092\u4E2D\u6B62\u3057\u307E\u3057\u305F\u3002");
74
+ console.error("\u4EE5\u4E0B\u306E JSON \u3092 settings.json \u306E hooks.Stop \u914D\u5217\u306B\u624B\u52D5\u3067\u8FFD\u52A0\u3057\u3066\u304F\u3060\u3055\u3044:");
75
+ console.error("");
76
+ console.error(snippet);
77
+ console.error("");
78
+ }
79
+ function makeTestRecord() {
80
+ return {
81
+ schemaVersion: 1,
82
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
83
+ sessionId: "cccn-setup-test",
84
+ project: process.cwd(),
85
+ gitBranch: null,
86
+ models: ["claude-fable-5"],
87
+ tokens: { input: 100, output: 50, cacheWrite5m: 0, cacheWrite1h: 0, cacheRead: 0 },
88
+ sidechainTokens: null,
89
+ apiCalls: 1,
90
+ costUSD: 0.01,
91
+ costJPY: 1.5,
92
+ fxRate: 150,
93
+ fxSource: "fixed",
94
+ prompt: "\u30BB\u30C3\u30C8\u30A2\u30C3\u30D7\u5B8C\u4E86\u30C6\u30B9\u30C8"
95
+ };
96
+ }
97
+ function takeValue(argv, i, prefix) {
98
+ const a = argv[i];
99
+ if (a.startsWith(prefix + "=")) return a.slice(prefix.length + 1);
100
+ return argv[i + 1];
101
+ }
102
+ function parseInitFlags(argv) {
103
+ const flags = { yes: false, osOnly: false, slackOnly: false };
104
+ for (let i = 0; i < argv.length; i++) {
105
+ const a = argv[i];
106
+ if (a === "--yes" || a === "-y") flags.yes = true;
107
+ else if (a === "--os-only") flags.osOnly = true;
108
+ else if (a === "--slack-only") flags.slackOnly = true;
109
+ else if (a === "--slack-webhook" || a.startsWith("--slack-webhook=")) {
110
+ flags.slackWebhook = takeValue(argv, i, "--slack-webhook");
111
+ if (!a.includes("=")) i++;
112
+ } else if (a === "--label" || a.startsWith("--label=")) {
113
+ flags.label = takeValue(argv, i, "--label");
114
+ if (!a.includes("=")) i++;
115
+ } else if (a === "--rate" || a.startsWith("--rate=")) {
116
+ flags.rate = takeValue(argv, i, "--rate");
117
+ if (!a.includes("=")) i++;
118
+ } else if (a === "--budget" || a.startsWith("--budget=")) {
119
+ flags.budget = takeValue(argv, i, "--budget");
120
+ if (!a.includes("=")) i++;
121
+ }
122
+ }
123
+ return flags;
124
+ }
125
+ function parseUninstallFlags(argv) {
126
+ const flags = { yes: false, purge: false };
127
+ for (const a of argv) {
128
+ if (a === "--yes" || a === "-y") flags.yes = true;
129
+ else if (a === "--purge") flags.purge = true;
130
+ }
131
+ return flags;
132
+ }
133
+ function mergeSettings(sPath, command) {
134
+ if (!existsSync(sPath)) {
135
+ mkdirSync(dirname(sPath), { recursive: true });
136
+ const fresh = { hooks: { Stop: [ourStopEntry(command)] } };
137
+ writeFileSync(sPath, JSON.stringify(fresh, null, 2) + "\n", "utf8");
138
+ return { ok: true, backupPath: null };
139
+ }
140
+ const raw = readFileSync(sPath, "utf8");
141
+ let parsed;
142
+ try {
143
+ parsed = JSON.parse(raw);
144
+ } catch {
145
+ printManualSnippet(sPath, command, "settings.json \u306E JSON \u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F");
146
+ return { ok: false, backupPath: null };
147
+ }
148
+ if (!isPlainObject(parsed)) {
149
+ printManualSnippet(sPath, command, "settings.json \u306E\u30EB\u30FC\u30C8\u304C\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u306F\u3042\u308A\u307E\u305B\u3093");
150
+ return { ok: false, backupPath: null };
151
+ }
152
+ const obj = parsed;
153
+ const backupPath = backupSettings(sPath);
154
+ if (!isPlainObject(obj.hooks)) obj.hooks = {};
155
+ const hooks = obj.hooks;
156
+ if (!Array.isArray(hooks.Stop)) hooks.Stop = [];
157
+ const stop = hooks.Stop;
158
+ const idx = stop.findIndex(isOurStopEntry);
159
+ if (idx >= 0) {
160
+ const entry = stop[idx];
161
+ const entryHooks = entry.hooks;
162
+ for (const h of entryHooks) {
163
+ if (isPlainObject(h) && typeof h.command === "string" && matchesMarker(h.command)) {
164
+ h.command = command;
165
+ h.timeout = HOOK_TIMEOUT;
166
+ }
167
+ }
168
+ } else {
169
+ stop.push(ourStopEntry(command));
170
+ }
171
+ writeFileSync(sPath, JSON.stringify(obj, null, 2) + "\n", "utf8");
172
+ return { ok: true, backupPath };
173
+ }
174
+ async function runInit(argv) {
175
+ const flags = parseInitFlags(argv);
176
+ const cfg = readConfig();
177
+ cfg.notify.os = true;
178
+ const budgetDefault = cfg.monthlyBudgetUSD > 0 ? cfg.monthlyBudgetUSD : DEFAULT_BUDGET_USD;
179
+ if (flags.yes) {
180
+ if (flags.slackOnly) {
181
+ if (flags.osOnly) {
182
+ console.error("--slack-only \u3068 --os-only \u306F\u540C\u6642\u306B\u6307\u5B9A\u3067\u304D\u307E\u305B\u3093");
183
+ return 1;
184
+ }
185
+ if (flags.slackWebhook === void 0) {
186
+ console.error("--slack-only \u306F --slack-webhook \u3067 Webhook URL \u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044");
187
+ return 1;
188
+ }
189
+ cfg.notify.os = false;
190
+ cfg.notify.slack = {
191
+ webhookUrl: flags.slackWebhook,
192
+ promptChars: SLACK_PROMPT_CHARS,
193
+ sendFullPrompt: false
194
+ };
195
+ } else if (flags.osOnly) {
196
+ cfg.notify.slack = null;
197
+ } else if (flags.slackWebhook !== void 0) {
198
+ cfg.notify.slack = {
199
+ webhookUrl: flags.slackWebhook,
200
+ promptChars: SLACK_PROMPT_CHARS,
201
+ sendFullPrompt: false
202
+ };
203
+ }
204
+ if (flags.label !== void 0) {
205
+ if (flags.label !== "api_equivalent" && flags.label !== "actual") {
206
+ console.error(`--label \u306F api_equivalent \u304B actual \u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044(\u53D7\u9818: ${flags.label})`);
207
+ return 1;
208
+ }
209
+ cfg.costLabel = flags.label;
210
+ }
211
+ if (flags.rate !== void 0) {
212
+ const r = Number(flags.rate);
213
+ if (!Number.isFinite(r) || r <= 0) {
214
+ console.error(`--rate \u306F\u6B63\u306E\u6570\u5024\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044(\u53D7\u9818: ${flags.rate})`);
215
+ return 1;
216
+ }
217
+ cfg.fx.fallbackRate = r;
218
+ }
219
+ if (flags.budget !== void 0) {
220
+ const b = Number(flags.budget);
221
+ if (!Number.isFinite(b) || b < 0) {
222
+ console.error(`--budget \u306F 0 \u4EE5\u4E0A\u306E\u6570\u5024\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044(\u53D7\u9818: ${flags.budget})`);
223
+ return 1;
224
+ }
225
+ cfg.monthlyBudgetUSD = b;
226
+ } else {
227
+ cfg.monthlyBudgetUSD = budgetDefault;
228
+ }
229
+ } else {
230
+ p.intro("ccc-notifier \u30BB\u30C3\u30C8\u30A2\u30C3\u30D7");
231
+ const channel = await p.select({
232
+ message: "\u901A\u77E5\u30C1\u30E3\u30CD\u30EB\u3092\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044",
233
+ options: [
234
+ { value: "os", label: "OS \u901A\u77E5\u306E\u307F" },
235
+ { value: "slack", label: "Slack \u306E\u307F(OS \u901A\u77E5\u306A\u3057)" },
236
+ { value: "both", label: "OS \u901A\u77E5 + Slack" }
237
+ ],
238
+ initialValue: "os"
239
+ });
240
+ if (p.isCancel(channel)) {
241
+ p.cancel("\u30AD\u30E3\u30F3\u30BB\u30EB\u3057\u307E\u3057\u305F");
242
+ return 1;
243
+ }
244
+ if (channel === "both" || channel === "slack") {
245
+ const webhook = await p.text({
246
+ message: "Slack Incoming Webhook URL",
247
+ placeholder: "https://hooks.slack.com/services/...",
248
+ validate: (v) => !v || !v.startsWith("https://") ? "https:// \u3067\u59CB\u307E\u308B URL \u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044" : void 0
249
+ });
250
+ if (p.isCancel(webhook)) {
251
+ p.cancel("\u30AD\u30E3\u30F3\u30BB\u30EB\u3057\u307E\u3057\u305F");
252
+ return 1;
253
+ }
254
+ cfg.notify.slack = {
255
+ webhookUrl: webhook,
256
+ promptChars: SLACK_PROMPT_CHARS,
257
+ sendFullPrompt: false
258
+ };
259
+ cfg.notify.os = channel === "both";
260
+ } else {
261
+ cfg.notify.slack = null;
262
+ }
263
+ const labelChoice = await p.select({
264
+ message: "\u30B3\u30B9\u30C8\u8868\u793A\u30E9\u30D9\u30EB",
265
+ options: [
266
+ { value: "api_equivalent", label: "API \u63DB\u7B97(\u5B9A\u984D\u30D7\u30E9\u30F3\u3067\u3082\u5F93\u91CF\u63DB\u7B97\u3067\u8868\u793A)" },
267
+ { value: "actual", label: "\u5B9F\u984D" }
268
+ ],
269
+ initialValue: cfg.costLabel
270
+ });
271
+ if (p.isCancel(labelChoice)) {
272
+ p.cancel("\u30AD\u30E3\u30F3\u30BB\u30EB\u3057\u307E\u3057\u305F");
273
+ return 1;
274
+ }
275
+ cfg.costLabel = labelChoice === "actual" ? "actual" : "api_equivalent";
276
+ const rateStr = await p.text({
277
+ message: "USD/JPY \u30D5\u30A9\u30FC\u30EB\u30D0\u30C3\u30AF\u70BA\u66FF\u30EC\u30FC\u30C8",
278
+ placeholder: String(cfg.fx.fallbackRate),
279
+ defaultValue: String(cfg.fx.fallbackRate),
280
+ validate: (v) => {
281
+ if (v === void 0 || v === "") return void 0;
282
+ const n = Number(v);
283
+ return !Number.isFinite(n) || n <= 0 ? "\u6B63\u306E\u6570\u5024\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044" : void 0;
284
+ }
285
+ });
286
+ if (p.isCancel(rateStr)) {
287
+ p.cancel("\u30AD\u30E3\u30F3\u30BB\u30EB\u3057\u307E\u3057\u305F");
288
+ return 1;
289
+ }
290
+ const parsedRate = Number(rateStr);
291
+ if (Number.isFinite(parsedRate) && parsedRate > 0) cfg.fx.fallbackRate = parsedRate;
292
+ const budgetStr = await p.text({
293
+ message: "\u6708\u306E\u4E88\u7B97(USD\u3001\u65E2\u5B9A $400\u30020 \u3067\u7121\u52B9\u30FB\u30C0\u30C3\u30B7\u30E5\u30DC\u30FC\u30C9\u306B\u5F53\u6708\u306E\u4F7F\u7528\u7387\u3092\u8868\u793A)",
294
+ placeholder: String(budgetDefault),
295
+ defaultValue: String(budgetDefault),
296
+ validate: (v) => {
297
+ if (v === void 0 || v === "") return void 0;
298
+ const n = Number(v);
299
+ return !Number.isFinite(n) || n < 0 ? "0 \u4EE5\u4E0A\u306E\u6570\u5024\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044" : void 0;
300
+ }
301
+ });
302
+ if (p.isCancel(budgetStr)) {
303
+ p.cancel("\u30AD\u30E3\u30F3\u30BB\u30EB\u3057\u307E\u3057\u305F");
304
+ return 1;
305
+ }
306
+ const parsedBudget = Number(budgetStr);
307
+ if (Number.isFinite(parsedBudget) && parsedBudget >= 0) cfg.monthlyBudgetUSD = parsedBudget;
308
+ p.outro("\u8A2D\u5B9A\u3092\u9069\u7528\u3057\u307E\u3059");
309
+ }
310
+ const cccn = paths();
311
+ writeFileSync(cccn.configFile, JSON.stringify(cfg, null, 2) + "\n", "utf8");
312
+ const command = buildHookCommand();
313
+ const sPath = settingsPath();
314
+ const result = mergeSettings(sPath, command);
315
+ if (!result.ok) return 1;
316
+ const testRecord = makeTestRecord();
317
+ await notifyOS(testRecord, cfg);
318
+ if (cfg.notify.slack) {
319
+ await notifySlack(testRecord, cfg);
320
+ }
321
+ console.log("");
322
+ console.log(`settings \u3092\u66F4\u65B0\u3057\u307E\u3057\u305F: ${sPath}`);
323
+ console.log(
324
+ 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
+ );
326
+ console.log(` \u8A2D\u5B9A\u30D5\u30A1\u30A4\u30EB: ${cccn.configFile}`);
327
+ 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");
328
+ return 0;
329
+ }
330
+ async function runUninstall(argv) {
331
+ const flags = parseUninstallFlags(argv);
332
+ const sPath = settingsPath();
333
+ if (!existsSync(sPath)) {
334
+ console.log("\u767B\u9332\u306A\u3057: settings.json \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3067\u3057\u305F\u3002");
335
+ } else {
336
+ const raw = readFileSync(sPath, "utf8");
337
+ let parsed;
338
+ let parseOk = true;
339
+ try {
340
+ parsed = JSON.parse(raw);
341
+ } catch {
342
+ parseOk = false;
343
+ }
344
+ if (!parseOk || !isPlainObject(parsed)) {
345
+ 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
+ } else {
347
+ const obj = parsed;
348
+ const hooks = isPlainObject(obj.hooks) ? obj.hooks : null;
349
+ const stop = hooks && Array.isArray(hooks.Stop) ? hooks.Stop : null;
350
+ const hasMarker = stop ? stop.some(isOurStopEntry) : false;
351
+ if (!hasMarker || !hooks || !stop) {
352
+ 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
+ } else {
354
+ const backupPath = backupSettings(sPath);
355
+ const filtered = stop.filter((e) => !isOurStopEntry(e));
356
+ if (filtered.length === 0) {
357
+ delete hooks.Stop;
358
+ } else {
359
+ hooks.Stop = filtered;
360
+ }
361
+ writeFileSync(sPath, JSON.stringify(obj, null, 2) + "\n", "utf8");
362
+ console.log(`\u672C\u30C4\u30FC\u30EB\u306E Stop \u30D5\u30C3\u30AF\u3092\u9664\u53BB\u3057\u307E\u3057\u305F: ${sPath}`);
363
+ console.log(` \u30D0\u30C3\u30AF\u30A2\u30C3\u30D7: ${backupPath}`);
364
+ }
365
+ }
366
+ }
367
+ if (flags.purge) {
368
+ const home = paths().home;
369
+ let doPurge = flags.yes;
370
+ if (!flags.yes) {
371
+ const confirmed = await p.confirm({
372
+ message: `\u30C7\u30FC\u30BF\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u3092\u5B8C\u5168\u306B\u524A\u9664\u3057\u307E\u3059\u304B?(${home})`,
373
+ initialValue: false
374
+ });
375
+ if (p.isCancel(confirmed)) {
376
+ p.cancel("\u30AD\u30E3\u30F3\u30BB\u30EB\u3057\u307E\u3057\u305F");
377
+ return 0;
378
+ }
379
+ doPurge = confirmed;
380
+ }
381
+ if (doPurge) {
382
+ rmSync(home, { recursive: true, force: true });
383
+ console.log(`\u30C7\u30FC\u30BF\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u3092\u524A\u9664\u3057\u307E\u3057\u305F: ${home}`);
384
+ }
385
+ }
386
+ return 0;
387
+ }
388
+
389
+ export {
390
+ matchesMarker,
391
+ runInit,
392
+ runUninstall
393
+ };