ccc-notifier 0.4.0 → 0.5.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/dist/cli.js CHANGED
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  fmtMuteUntil
4
- } from "./chunk-TUZVISLD.js";
4
+ } from "./chunk-DDUK4EWQ.js";
5
5
  import {
6
- codexHooksFile,
7
- matchesMarker
8
- } from "./chunk-5LHZOZZO.js";
6
+ CODEX_HOOK_EVENTS,
7
+ CODEX_HOOK_TIMEOUT_SECONDS,
8
+ matchesMarker,
9
+ parseConfiguredOwnedCodexHookCommand
10
+ } from "./chunk-S3XKO7MY.js";
9
11
  import {
10
12
  notifyOS,
11
13
  notifySlack,
@@ -14,6 +16,9 @@ import {
14
16
  import {
15
17
  isWSL
16
18
  } from "./chunk-DGXUSPS4.js";
19
+ import {
20
+ findLatestCodexRollout
21
+ } from "./chunk-HLJAJS2W.js";
17
22
  import {
18
23
  codexHome,
19
24
  detectCodex
@@ -22,8 +27,9 @@ import {
22
27
  aggregateNewTurn,
23
28
  computeCost,
24
29
  getUsdJpy,
25
- loadPriceTable
26
- } from "./chunk-LHKBGA5K.js";
30
+ loadPriceTable,
31
+ splitIntoCodexTurnDrafts
32
+ } from "./chunk-LVMKY6JB.js";
27
33
  import {
28
34
  formatJPY,
29
35
  formatTokens,
@@ -35,18 +41,191 @@ import {
35
41
  readConfig,
36
42
  readMuteState,
37
43
  readTurns
38
- } from "./chunk-26CISNOE.js";
44
+ } from "./chunk-5PH7PPD6.js";
39
45
 
40
46
  // src/cli.ts
41
- import { realpathSync } from "fs";
47
+ import { realpathSync as realpathSync2 } from "fs";
42
48
  import { createRequire } from "module";
43
49
  import { fileURLToPath, pathToFileURL } from "url";
44
50
 
45
51
  // src/doctor.ts
46
- import { existsSync, readdirSync, statSync } from "fs";
52
+ import { existsSync as existsSync2, readdirSync, statSync as statSync2 } from "fs";
47
53
  import { readFile } from "fs/promises";
48
54
  import { homedir } from "os";
49
- import { join } from "path";
55
+ import { join as join2 } from "path";
56
+
57
+ // src/codex/hook-diagnostics.ts
58
+ import { existsSync, realpathSync, statSync, readFileSync } from "fs";
59
+ import { delimiter, dirname, extname, isAbsolute, join, normalize, resolve } from "path";
60
+ var MAX_JSON_BYTES = 1024 * 1024;
61
+ function isObject(value) {
62
+ return typeof value === "object" && value !== null && !Array.isArray(value);
63
+ }
64
+ function normalizedAbsolute(path) {
65
+ return normalize(isAbsolute(path) ? path : resolve(path));
66
+ }
67
+ function normalizedCommandPath(path) {
68
+ return path.replace(/\\/g, "/");
69
+ }
70
+ function identityPath(path) {
71
+ const absolute = normalizedAbsolute(path);
72
+ try {
73
+ return realpathSync(absolute);
74
+ } catch {
75
+ return absolute;
76
+ }
77
+ }
78
+ function existing(path) {
79
+ try {
80
+ statSync(path);
81
+ return true;
82
+ } catch {
83
+ return false;
84
+ }
85
+ }
86
+ function findRepoRootCandidate(cwd) {
87
+ const fallback = normalizedAbsolute(cwd);
88
+ let current = fallback;
89
+ for (; ; ) {
90
+ if (existsSync(join(current, ".git"))) return current;
91
+ const parent = dirname(current);
92
+ if (parent === current) return fallback;
93
+ current = parent;
94
+ }
95
+ }
96
+ function formatFor(path) {
97
+ const extension = extname(path).toLowerCase();
98
+ if (extension === ".json") return "json";
99
+ if (extension === ".toml") return "toml";
100
+ return "opaque";
101
+ }
102
+ function discoverCodexHookSources(options) {
103
+ const repoRoot = findRepoRootCandidate(options.cwd);
104
+ const candidates = [
105
+ { path: join(options.codexHome, "hooks.json"), scope: "user", format: "json", discovery: "standard", activeState: "unknown" },
106
+ { path: join(options.codexHome, "config.toml"), scope: "user", format: "toml", discovery: "standard", activeState: "unknown" },
107
+ { path: join(repoRoot, ".codex", "hooks.json"), scope: "project", format: "json", discovery: "standard", activeState: "unknown" },
108
+ { path: join(repoRoot, ".codex", "config.toml"), scope: "project", format: "toml", discovery: "standard", activeState: "unknown" }
109
+ ];
110
+ for (const path of (options.envSources ?? "").split(delimiter).filter(Boolean)) {
111
+ candidates.push({
112
+ path: normalizedAbsolute(path),
113
+ scope: "env-extra",
114
+ format: formatFor(path),
115
+ discovery: "supplemental",
116
+ activeState: "unknown"
117
+ });
118
+ }
119
+ const seen = /* @__PURE__ */ new Set();
120
+ const result = [];
121
+ for (const candidate of candidates) {
122
+ const path = normalizedAbsolute(candidate.path);
123
+ if (!existing(path)) continue;
124
+ const identity = identityPath(path);
125
+ if (seen.has(identity)) continue;
126
+ seen.add(identity);
127
+ result.push({ ...candidate, path });
128
+ }
129
+ return result;
130
+ }
131
+ function inspectJson(source, expectedNodePath, expectedCliPath) {
132
+ let stat;
133
+ try {
134
+ stat = statSync(source.path);
135
+ } catch {
136
+ return { handlers: [], warning: { sourcePath: source.path, kind: "read-failed" }, nonstandardFeature: false };
137
+ }
138
+ if (!stat.isFile()) return { handlers: [], warning: { sourcePath: source.path, kind: "not-regular" }, nonstandardFeature: false };
139
+ if (stat.size > MAX_JSON_BYTES) return { handlers: [], warning: { sourcePath: source.path, kind: "too-large" }, nonstandardFeature: false };
140
+ let parsed;
141
+ try {
142
+ parsed = JSON.parse(readFileSync(source.path, "utf8"));
143
+ } catch {
144
+ return { handlers: [], warning: { sourcePath: source.path, kind: "invalid-json" }, nonstandardFeature: false };
145
+ }
146
+ if (!isObject(parsed)) return { handlers: [], warning: { sourcePath: source.path, kind: "invalid-shape" }, nonstandardFeature: false };
147
+ if (parsed.hooks !== void 0 && !isObject(parsed.hooks)) {
148
+ return { handlers: [], warning: { sourcePath: source.path, kind: "invalid-shape" }, nonstandardFeature: false };
149
+ }
150
+ const hooks = isObject(parsed.hooks) ? parsed.hooks : {};
151
+ for (const event of CODEX_HOOK_EVENTS) {
152
+ const groups = hooks[event];
153
+ if (groups === void 0) continue;
154
+ if (!Array.isArray(groups) || groups.some((group) => !isObject(group) || !Array.isArray(group.hooks))) {
155
+ return { handlers: [], warning: { sourcePath: source.path, kind: "invalid-shape" }, nonstandardFeature: false };
156
+ }
157
+ }
158
+ const handlers = [];
159
+ for (const event of CODEX_HOOK_EVENTS) {
160
+ const groups = hooks[event];
161
+ if (!Array.isArray(groups)) continue;
162
+ for (const group of groups) {
163
+ if (!isObject(group) || !Array.isArray(group.hooks)) continue;
164
+ for (const handler of group.hooks) {
165
+ if (!isObject(handler) || handler.type !== "command") continue;
166
+ const command = parseConfiguredOwnedCodexHookCommand(handler.command, event);
167
+ if (command === null) continue;
168
+ handlers.push({
169
+ sourcePath: source.path,
170
+ scope: source.scope,
171
+ event,
172
+ nodePath: command.nodePath,
173
+ cliPath: command.cliPath,
174
+ timeout: handler.timeout,
175
+ pathMatches: command.nodePath === normalizedCommandPath(expectedNodePath) && command.cliPath === normalizedCommandPath(expectedCliPath),
176
+ timeoutMatches: handler.timeout === CODEX_HOOK_TIMEOUT_SECONDS
177
+ });
178
+ }
179
+ }
180
+ }
181
+ return {
182
+ handlers,
183
+ nonstandardFeature: isObject(parsed.features) && "hooks" in parsed.features
184
+ };
185
+ }
186
+ function diagnoseCodexHookSources(options) {
187
+ const candidates = discoverCodexHookSources(options);
188
+ const handlers = [];
189
+ const inspectedJsonSources = [];
190
+ const opaqueSources = [];
191
+ const warnings = [];
192
+ for (const source of candidates) {
193
+ if (source.format !== "json") {
194
+ opaqueSources.push(source.path);
195
+ continue;
196
+ }
197
+ const inspected = inspectJson(source, options.expectedNodePath, options.expectedCliPath);
198
+ if (inspected.warning) warnings.push(inspected.warning);
199
+ else inspectedJsonSources.push(source.path);
200
+ if (inspected.nonstandardFeature) warnings.push({ sourcePath: source.path, kind: "nonstandard-feature-field" });
201
+ handlers.push(...inspected.handlers);
202
+ }
203
+ const exactDuplicates = [];
204
+ for (const event of CODEX_HOOK_EVENTS) {
205
+ const matches = handlers.filter((handler) => handler.event === event);
206
+ if (matches.length > 1) {
207
+ exactDuplicates.push({ event, count: matches.length, sources: [...new Set(matches.map((handler) => handler.sourcePath))] });
208
+ }
209
+ }
210
+ const sameLayerMixedRepresentation = [];
211
+ for (const scope of ["user", "project"]) {
212
+ const json = candidates.find((source) => source.scope === scope && source.format === "json");
213
+ const toml = candidates.find((source) => source.scope === scope && source.format === "toml");
214
+ if (json && toml) sameLayerMixedRepresentation.push({ scope, json: json.path, toml: toml.path });
215
+ }
216
+ return {
217
+ candidates,
218
+ inspectedJsonSources,
219
+ opaqueSources,
220
+ handlers,
221
+ exactDuplicates,
222
+ sameLayerMixedRepresentation,
223
+ effectiveState: "unknown",
224
+ warnings
225
+ };
226
+ }
227
+
228
+ // src/doctor.ts
50
229
  function icon(status) {
51
230
  if (status === "ok") return "\u2705";
52
231
  if (status === "warn") return "\u26A0\uFE0F";
@@ -62,10 +241,10 @@ function isRecord(v) {
62
241
  return typeof v === "object" && v !== null && !Array.isArray(v);
63
242
  }
64
243
  function settingsPath() {
65
- return process.env.CCCN_CLAUDE_SETTINGS || join(homedir(), ".claude", "settings.json");
244
+ return process.env.CCCN_CLAUDE_SETTINGS || join2(homedir(), ".claude", "settings.json");
66
245
  }
67
246
  function projectsDir() {
68
- return process.env.CCCN_CLAUDE_PROJECTS || join(homedir(), ".claude", "projects");
247
+ return process.env.CCCN_CLAUDE_PROJECTS || join2(homedir(), ".claude", "projects");
69
248
  }
70
249
  function tokenizeCommand(command) {
71
250
  const tokens = [];
@@ -95,7 +274,7 @@ async function safeRun(name, fn) {
95
274
  }
96
275
  async function checkHookRegistration() {
97
276
  const file = settingsPath();
98
- if (!existsSync(file)) {
277
+ if (!existsSync2(file)) {
99
278
  log("fail", `settings.json \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${file}(init \u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044)`);
100
279
  return false;
101
280
  }
@@ -137,7 +316,7 @@ async function checkHookRegistration() {
137
316
  let allScriptsExist = true;
138
317
  for (const command of matchedCommands) {
139
318
  const scriptPath = extractScriptPath(command);
140
- if (scriptPath === null || !existsSync(scriptPath)) {
319
+ if (scriptPath === null || !existsSync2(scriptPath)) {
141
320
  allScriptsExist = false;
142
321
  }
143
322
  }
@@ -152,7 +331,7 @@ async function checkHookRegistration() {
152
331
  if (first === void 0) continue;
153
332
  const looksAbsolute = first.includes("/") || first.includes(":\\");
154
333
  if (!looksAbsolute) continue;
155
- if (!existsSync(first)) {
334
+ if (!existsSync2(first)) {
156
335
  log(
157
336
  "warn",
158
337
  `hook \u306E Node \u5B9F\u884C\u30D1\u30B9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093(mise \u7B49\u3067\u306E\u66F4\u65B0\u304C\u539F\u56E0\u306E\u53EF\u80FD\u6027)\u3002init \u3092\u518D\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044: ${first}`
@@ -162,48 +341,64 @@ async function checkHookRegistration() {
162
341
  return true;
163
342
  }
164
343
  async function checkCodex() {
165
- if (!detectCodex()) {
344
+ const expectedCli = process.env.CCCN_CLI_PATH ?? process.argv[1] ?? "";
345
+ const diagnostics = diagnoseCodexHookSources({
346
+ codexHome: codexHome(),
347
+ cwd: process.cwd(),
348
+ expectedNodePath: process.execPath,
349
+ expectedCliPath: expectedCli,
350
+ envSources: process.env.CCCN_CODEX_HOOK_SOURCES
351
+ });
352
+ if (!detectCodex() && diagnostics.candidates.length === 0) {
166
353
  log("ok", "Codex CLI \u306F\u672A\u691C\u51FA\u3067\u3059(\u672A\u4F7F\u7528\u306A\u3089\u554F\u984C\u3042\u308A\u307E\u305B\u3093)");
167
- return true;
354
+ return { ok: true, stopConfigured: false };
168
355
  }
169
- const hooksFile = codexHooksFile();
170
- const matchedCommands = [];
171
- if (existsSync(hooksFile)) {
172
- try {
173
- const raw = await readFile(hooksFile, "utf8");
174
- const parsed = JSON.parse(raw);
175
- if (isRecord(parsed)) {
176
- const hooks = parsed.hooks;
177
- const stopEntries = isRecord(hooks) ? hooks.Stop : void 0;
178
- if (Array.isArray(stopEntries)) {
179
- for (const entry of stopEntries) {
180
- if (!isRecord(entry)) continue;
181
- const innerHooks = entry.hooks;
182
- if (!Array.isArray(innerHooks)) continue;
183
- for (const h of innerHooks) {
184
- if (isRecord(h) && typeof h.command === "string" && matchesMarker(h.command)) {
185
- matchedCommands.push(h.command);
186
- }
187
- }
188
- }
189
- }
190
- }
191
- } catch {
356
+ for (const source of diagnostics.candidates) {
357
+ if (source.format === "toml") {
358
+ log("warn", `Codex inline hook\u5019\u88DC\u3092\u691C\u51FA\u3057\u307E\u3057\u305F(${source.scope}, ${source.discovery}): ${source.path}`);
359
+ log("warn", "config.toml\u306F\u89E3\u91C8\u3057\u306A\u3044\u305F\u3081handler\u30FBfeatures.hooks\u30FBtrust\u306E\u5B9F\u52B9\u72B6\u614B\u306F\u672A\u78BA\u8A8D\u3067\u3059\u3002Codex\u3067 /hooks \u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044");
360
+ } else if (source.format === "opaque") {
361
+ log("warn", `Codex opaque env-extra source\u3092\u691C\u51FA\u3057\u307E\u3057\u305F(\u5185\u5BB9\u672A\u78BA\u8A8D): ${source.path}`);
192
362
  }
193
363
  }
194
- if (matchedCommands.length > 0) {
195
- log("ok", `Codex \u306E Stop hook \u304C\u767B\u9332\u3055\u308C\u3066\u3044\u307E\u3059: ${matchedCommands.join(" / ")}`);
196
- log("ok", "codex \u5074\u3067 hook \u3092\u627F\u8A8D\u6E08\u307F\u304B\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044(\u672A\u627F\u8A8D\u3060\u3068\u901A\u77E5\u3055\u308C\u307E\u305B\u3093)");
197
- } else {
198
- log("warn", "Codex \u3092\u691C\u51FA\u3057\u307E\u3057\u305F\u304C hook \u304C\u672A\u767B\u9332\u3067\u3059\u3002init --codex \u3067\u767B\u9332\u3067\u304D\u307E\u3059");
364
+ for (const warning of diagnostics.warnings) {
365
+ if (warning.kind === "nonstandard-feature-field") {
366
+ log("warn", `Codex JSON source\u306B\u975E\u6A19\u6E96features.hooks field\u304C\u3042\u308A\u307E\u3059\u3002global disabled\u306E\u6839\u62E0\u306B\u306F\u3057\u307E\u305B\u3093: ${warning.sourcePath}`);
367
+ } else {
368
+ log("warn", `Codex JSON source\u3092\u5B89\u5168\u306B\u691C\u67FB\u3067\u304D\u307E\u305B\u3093(${warning.kind}): ${warning.sourcePath}`);
369
+ }
199
370
  }
200
- const sessionsDir = join(codexHome(), "sessions");
201
- if (existsSync(sessionsDir)) {
371
+ for (const handler of diagnostics.handlers) {
372
+ log(
373
+ handler.pathMatches && handler.timeoutMatches ? "ok" : "warn",
374
+ `Codex ${handler.event} hook\u3092\u8A2D\u5B9A\u30D5\u30A1\u30A4\u30EB\u4E0A\u3067\u78BA\u8A8D(${handler.scope}): ${handler.sourcePath}; actual nodePath=${handler.nodePath}, actual cliPath=${handler.cliPath}, expected nodePath=${process.execPath.replace(/\\/g, "/")}, expected cliPath=${expectedCli.replace(/\\/g, "/")}, \u5B9F\u4F53path=${handler.pathMatches ? "\u4E00\u81F4" : "\u4E0D\u4E00\u81F4(stale/wrong)"}, timeout=${String(handler.timeout)}`
375
+ );
376
+ }
377
+ for (const event of CODEX_HOOK_EVENTS) {
378
+ if (!diagnostics.handlers.some((handler) => handler.event === event)) {
379
+ log("warn", `Codex ${event} hook\u306F\u691C\u67FB\u3067\u304D\u305FJSON source\u3067\u306F\u78BA\u8A8D\u3067\u304D\u307E\u305B\u3093\u3002inline/plugin/managed source\u306F\u672A\u78BA\u8A8D\u3067\u3059\u3002\u5FC5\u8981\u306A\u3089 init --codex\u3001\u5B9F\u52B9\u72B6\u614B\u306FCodex\u306E /hooks\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044`);
380
+ }
381
+ }
382
+ for (const duplicate of diagnostics.exactDuplicates) {
383
+ log("warn", `Codex ${duplicate.event} hook\u306Eexact duplicate\u3092\u691C\u67FB\u6E08\u307FJSON\u3067\u78BA\u8A8D(${duplicate.count}\u4EF6): ${duplicate.sources.join(" / ")}\u3002matching hooks\u306F\u8907\u6570source\u304B\u3089\u3059\u3079\u3066\u5B9F\u884C\u3055\u308C\u5F97\u307E\u3059`);
384
+ }
385
+ for (const mixed of diagnostics.sameLayerMixedRepresentation) {
386
+ log("warn", `Codex ${mixed.scope} layer\u306Bhooks.json\u3068config.toml\u304C\u4F75\u5B58\u3057\u3066\u3044\u307E\u3059(potential duplicate\u3001TOML\u5185\u5BB9\u672A\u78BA\u8A8D): ${mixed.json} / ${mixed.toml}`);
387
+ }
388
+ log("warn", "Codex hook\u306Eglobal/individual disabled\u3001project/hook trust\u306F\u9759\u7684\u8A3A\u65AD\u3067\u306F\u672A\u78BA\u8A8D\u3067\u3059\u3002Codex\u3067 /hooks\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044");
389
+ log("warn", "plugin/managed/session source\u3092\u542B\u3080\u5B9F\u52B9\u72B6\u614B\u306F\u9759\u7684\u8A3A\u65AD\u3060\u3051\u3067\u306F\u5B8C\u5168\u5217\u6319\u3067\u304D\u307E\u305B\u3093\u3002Codex\u3067 /hooks\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044");
390
+ const sessionsDir = join2(codexHome(), "sessions");
391
+ if (existsSync2(sessionsDir)) {
202
392
  log("ok", `Codex \u306E\u30BB\u30C3\u30B7\u30E7\u30F3\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u3092\u78BA\u8A8D\u3057\u307E\u3057\u305F: ${sessionsDir}`);
203
393
  } else {
204
394
  log("ok", `Codex \u306E\u30BB\u30C3\u30B7\u30E7\u30F3\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u306F\u307E\u3060\u3042\u308A\u307E\u305B\u3093: ${sessionsDir}(\u30BB\u30C3\u30B7\u30E7\u30F3\u672A\u4F5C\u6210\u306E\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059)`);
205
395
  }
206
- return true;
396
+ return {
397
+ ok: true,
398
+ stopConfigured: diagnostics.handlers.some(
399
+ (handler) => handler.event === "Stop" && handler.scope !== "env-extra"
400
+ )
401
+ };
207
402
  }
208
403
  function readDirSafe(dir) {
209
404
  try {
@@ -219,12 +414,12 @@ function findLatestTranscript(dir) {
219
414
  const entries = readDirSafe(current);
220
415
  if (entries === null) return;
221
416
  for (const entry of entries) {
222
- const full = join(current, entry.name);
417
+ const full = join2(current, entry.name);
223
418
  if (entry.isDirectory()) {
224
419
  walk(full);
225
420
  } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
226
421
  try {
227
- const mtime = statSync(full).mtimeMs;
422
+ const mtime = statSync2(full).mtimeMs;
228
423
  if (mtime > latestMtime) {
229
424
  latestMtime = mtime;
230
425
  latestPath = full;
@@ -362,33 +557,99 @@ async function checkNotification(cfg) {
362
557
  return true;
363
558
  }
364
559
  }
365
- async function checkRecentSessionTotal(latestTranscript) {
560
+ async function checkClaudeRecentSessionTotal(latestTranscript) {
366
561
  if (latestTranscript === null) {
367
- log("warn", "\u76F4\u8FD1\u30BB\u30C3\u30B7\u30E7\u30F3\u5408\u8A08: transcript \u304C\u898B\u3064\u304B\u3089\u306A\u3044\u305F\u3081\u8A08\u7B97\u3092\u30B9\u30AD\u30C3\u30D7\u3057\u307E\u3057\u305F");
562
+ log("warn", "Claude Code \u76F4\u8FD1\u30BB\u30C3\u30B7\u30E7\u30F3\u5408\u8A08: transcript \u304C\u898B\u3064\u304B\u3089\u306A\u3044\u305F\u3081\u8A08\u7B97\u3092\u30B9\u30AD\u30C3\u30D7\u3057\u307E\u3057\u305F");
368
563
  return true;
369
564
  }
370
565
  try {
371
566
  const aggregate2 = await aggregateNewTurn(latestTranscript, null);
372
567
  if (aggregate2 === null) {
373
- log("warn", "\u76F4\u8FD1\u30BB\u30C3\u30B7\u30E7\u30F3\u5408\u8A08: \u65B0\u898F usage \u304C\u7121\u3044\u305F\u3081\u8A08\u7B97\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F");
568
+ log("warn", "Claude Code \u76F4\u8FD1\u30BB\u30C3\u30B7\u30E7\u30F3\u5408\u8A08: \u65B0\u898F usage \u304C\u7121\u3044\u305F\u3081\u8A08\u7B97\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F");
374
569
  return true;
375
570
  }
376
571
  const table = await loadPriceTable(paths().cacheDir, { offline: true });
377
572
  const breakdown = computeCost(aggregate2.main, aggregate2.sidechain, table);
378
573
  log(
379
574
  "ok",
380
- `\u76F4\u8FD1\u30BB\u30C3\u30B7\u30E7\u30F3\u5408\u8A08: ${formatUSD(breakdown.usd)}(Claude Code \u306E /cost \u306E Total cost \u3068\u898B\u6BD4\u3079\u3066\u304F\u3060\u3055\u3044)`
575
+ `Claude Code \u76F4\u8FD1\u30BB\u30C3\u30B7\u30E7\u30F3\u5408\u8A08: ${formatUSD(breakdown.usd)}(Claude Code \u306E /cost \u306E Total cost \u3068\u898B\u6BD4\u3079\u3066\u304F\u3060\u3055\u3044)`
381
576
  );
382
577
  return true;
383
578
  } catch (err) {
384
- log("warn", `\u76F4\u8FD1\u30BB\u30C3\u30B7\u30E7\u30F3\u5408\u8A08\u306E\u8A08\u7B97\u4E2D\u306B\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F: ${errMessage(err)}`);
579
+ log("warn", `Claude Code \u76F4\u8FD1\u30BB\u30C3\u30B7\u30E7\u30F3\u5408\u8A08\u306E\u8A08\u7B97\u4E2D\u306B\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F: ${errMessage(err)}`);
580
+ return true;
581
+ }
582
+ }
583
+ function emptyBuckets() {
584
+ return { input: 0, output: 0, cacheWrite5m: 0, cacheWrite1h: 0, cacheRead: 0 };
585
+ }
586
+ function mergeUsage(target, incoming) {
587
+ for (const [model, bucket] of Object.entries(incoming)) {
588
+ const merged = Object.hasOwn(target, model) ? target[model] : emptyBuckets();
589
+ merged.input += bucket.input;
590
+ merged.output += bucket.output;
591
+ merged.cacheWrite5m += bucket.cacheWrite5m;
592
+ merged.cacheWrite1h += bucket.cacheWrite1h;
593
+ merged.cacheRead += bucket.cacheRead;
594
+ target[model] = merged;
595
+ }
596
+ }
597
+ function safeUnknownModels(models) {
598
+ const safe = [...new Set(models.map(
599
+ (model) => model.replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, "").trim().slice(0, 64) || "unknown"
600
+ ))].sort();
601
+ const shown = safe.slice(0, 5);
602
+ return `${shown.join(", ")}${safe.length > shown.length ? `, ...(+${safe.length - shown.length})` : ""}`;
603
+ }
604
+ async function checkCodexRecentSessionTotal(configured) {
605
+ if (!configured) return true;
606
+ try {
607
+ const sessionsRoot = join2(codexHome(), "sessions");
608
+ if (!existsSync2(sessionsRoot)) {
609
+ log("warn", "Codex \u6700\u65B0rollout\u5408\u8A08: \u30BB\u30C3\u30B7\u30E7\u30F3\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u304C\u306A\u3044\u305F\u3081\u30B9\u30AD\u30C3\u30D7");
610
+ return true;
611
+ }
612
+ const discovery = await findLatestCodexRollout(sessionsRoot);
613
+ if (discovery.unreadableDirs > 0 || discovery.unreadableFiles > 0) {
614
+ log("warn", "Codex \u6700\u65B0rollout\u5408\u8A08: rollout\u63A2\u7D22\u3092\u5B8C\u5168\u306B\u691C\u8A3C\u3067\u304D\u305A\u6700\u65B0\u3092\u78BA\u5B9A\u3067\u304D\u306A\u3044\u305F\u3081\u30B9\u30AD\u30C3\u30D7");
615
+ return true;
616
+ }
617
+ if (discovery.latest === null) {
618
+ log("warn", "Codex \u6700\u65B0rollout\u5408\u8A08: rollout\u304C\u898B\u3064\u304B\u3089\u306A\u3044\u305F\u3081\u30B9\u30AD\u30C3\u30D7");
619
+ return true;
620
+ }
621
+ const drafts = await splitIntoCodexTurnDrafts(discovery.latest, null);
622
+ if (drafts === null || drafts.length === 0) {
623
+ log("warn", "Codex \u6700\u65B0rollout\u5408\u8A08: usage\u304C\u306A\u3044\u3001\u307E\u305F\u306F\u6709\u52B9\u306Atoken_count\u3092\u89E3\u6790\u3067\u304D\u307E\u305B\u3093");
624
+ return true;
625
+ }
626
+ const usage = /* @__PURE__ */ Object.create(null);
627
+ for (const draft of drafts) mergeUsage(usage, draft.agg.main);
628
+ const table = await loadPriceTable(paths().cacheDir, { offline: true });
629
+ const breakdown = computeCost(usage, {}, table);
630
+ if (breakdown.unknownModels.length > 0) {
631
+ log(
632
+ "warn",
633
+ `Codex \u6700\u65B0rollout\u5408\u8A08: ${formatUSD(breakdown.usd)}(API\u63DB\u7B97\u30FB\u5358\u4E00rollout\u306E\u307F\u30FB\u89AA/\u5B50\u672A\u5206\u985E/\u975E\u5408\u7B97\u30FBClaude Code\u5206\u3068\u306F\u5225\u96C6\u8A08\u3002\u305F\u3060\u3057\u5358\u4FA1\u4E0D\u660E\u30E2\u30C7\u30EB\u3092\u542B\u3080\u305F\u3081\u904E\u5C11\u8A08\u4E0A\u306E\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059: ${safeUnknownModels(breakdown.unknownModels)})`
634
+ );
635
+ } else {
636
+ log("ok", `Codex \u6700\u65B0rollout\u5408\u8A08: ${formatUSD(breakdown.usd)}(API\u63DB\u7B97\u30FB\u5358\u4E00rollout\u306E\u307F\u30FB\u89AA/\u5B50\u672A\u5206\u985E/\u975E\u5408\u7B97\u30FBClaude Code\u5206\u3068\u306F\u5225\u96C6\u8A08)`);
637
+ }
638
+ return true;
639
+ } catch {
640
+ log("warn", "Codex \u6700\u65B0rollout\u5408\u8A08\u306E\u8A08\u7B97\u4E2D\u306B\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u305F\u305F\u3081\u30B9\u30AD\u30C3\u30D7");
385
641
  return true;
386
642
  }
387
643
  }
388
644
  async function runDoctor() {
389
645
  const results = [];
390
646
  results.push(await safeRun("settings.json", () => checkHookRegistration()));
391
- results.push(await safeRun("codex", () => checkCodex()));
647
+ let codexStopConfigured = false;
648
+ results.push(await safeRun("codex", async () => {
649
+ const result = await checkCodex();
650
+ codexStopConfigured = result.stopConfigured;
651
+ return result.ok;
652
+ }));
392
653
  let latestTranscript = null;
393
654
  results.push(
394
655
  await safeRun("projects", async () => {
@@ -402,7 +663,8 @@ async function runDoctor() {
402
663
  results.push(await safeRun("pricing", () => checkPricing()));
403
664
  results.push(await safeRun("fx", () => checkFx(cfg)));
404
665
  results.push(await safeRun("notify", () => checkNotification(cfg)));
405
- results.push(await safeRun("recent-session", () => checkRecentSessionTotal(latestTranscript)));
666
+ results.push(await safeRun("claude-recent-session", () => checkClaudeRecentSessionTotal(latestTranscript)));
667
+ results.push(await safeRun("codex-recent-session", () => checkCodexRecentSessionTotal(codexStopConfigured)));
406
668
  const hasFailure = results.some((ok) => ok === false);
407
669
  return hasFailure ? 1 : 0;
408
670
  }
@@ -519,6 +781,18 @@ function aggregate(turns) {
519
781
  total.costUSD += totalUsd;
520
782
  total.costJPY += totalJpy;
521
783
  total.subagentsUSD += saUsd;
784
+ if (rec.source === "codex" && rec.subagentActivity) {
785
+ const activity = total.codexSubagentActivity ?? {
786
+ turns: 0,
787
+ started: 0,
788
+ stopped: 0,
789
+ usageStatus: "unavailable"
790
+ };
791
+ activity.turns += 1;
792
+ activity.started += rec.subagentActivity.started;
793
+ activity.stopped += rec.subagentActivity.stopped;
794
+ total.codexSubagentActivity = activity;
795
+ }
522
796
  }
523
797
  const daily = [...dailyMap.values()].sort((a, b) => a.date.localeCompare(b.date));
524
798
  const byModel = {};
@@ -543,6 +817,12 @@ function printTable(result, days) {
543
817
  if (result.total.subagentsUSD > 0) {
544
818
  console.log(`(\u3046\u3061\u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8 ${formatUSD(result.total.subagentsUSD)})`);
545
819
  }
820
+ if (result.total.codexSubagentActivity) {
821
+ const activity = result.total.codexSubagentActivity;
822
+ console.log(
823
+ `(Codex\u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8\u5229\u7528\u3042\u308A\u30FB\u6599\u91D1\u672A\u96C6\u8A08: ${activity.turns}\u30BF\u30FC\u30F3 / \u958B\u59CB${activity.started}\u30FB\u7D42\u4E86${activity.stopped})`
824
+ );
825
+ }
546
826
  console.log("");
547
827
  console.log("\u30E2\u30C7\u30EB\u5225 (By model):");
548
828
  console.log("\u30E2\u30C7\u30EB".padEnd(28) + "\u30BF\u30FC\u30F3".padStart(8) + "USD".padStart(10) + "JPY".padStart(12));
@@ -619,6 +899,26 @@ var COMMANDS = [
619
899
  { cmd: "--version, -v", ja: "\u30D0\u30FC\u30B8\u30E7\u30F3\u3092\u8868\u793A", en: "Show the version" },
620
900
  { cmd: "--help, -h", ja: "\u3053\u306E\u30D8\u30EB\u30D7\u3092\u8868\u793A", en: "Show this help" }
621
901
  ];
902
+ var CODEX_PASSIVE_EVENTS = ["Stop", "UserPromptSubmit", "SubagentStart", "SubagentStop"];
903
+ function isCodexPassiveEvent(value) {
904
+ return CODEX_PASSIVE_EVENTS.includes(value);
905
+ }
906
+ async function runCodexPassiveHook(event, text) {
907
+ try {
908
+ if (event === "Stop") {
909
+ const trackMod = await import("./track-KEZON6KI.js");
910
+ await trackMod.runTrack(text, { codex: true });
911
+ } else if (event === "UserPromptSubmit") {
912
+ const activity = await import("./subagent-store-A6MIN22X.js");
913
+ activity.handleCodexUserPromptSubmitHook(text);
914
+ } else {
915
+ const activity = await import("./subagent-store-A6MIN22X.js");
916
+ activity.handleCodexSubagentHook(text, event === "SubagentStart" ? "start" : "stop");
917
+ }
918
+ } catch {
919
+ }
920
+ return event === "SubagentStart" || event === "UserPromptSubmit" ? Buffer.alloc(0) : Buffer.from("{}\n", "utf8");
921
+ }
622
922
  var CMD_COLUMN_WIDTH = Math.max(...COMMANDS.map((c) => c.cmd.length)) + 2;
623
923
  var HELP_TEXT = [
624
924
  "ccc-notifier(Claude Code Cost notifier)\u2014 Claude Code \u306E\u30D7\u30ED\u30F3\u30D7\u30C8\u3054\u3068\u306E\u30B3\u30B9\u30C8\u901A\u77E5 / per-prompt cost notifier for Claude Code",
@@ -640,9 +940,9 @@ function readVersion() {
640
940
  }
641
941
  }
642
942
  function readStdin(timeoutMs = 500) {
643
- return new Promise((resolve) => {
943
+ return new Promise((resolve2) => {
644
944
  if (process.stdin.isTTY) {
645
- resolve("");
945
+ resolve2("");
646
946
  return;
647
947
  }
648
948
  let data = "";
@@ -662,7 +962,7 @@ function readStdin(timeoutMs = 500) {
662
962
  process.stdin.unref();
663
963
  } catch {
664
964
  }
665
- resolve(result);
965
+ resolve2(result);
666
966
  };
667
967
  const onData = (chunk) => {
668
968
  data += typeof chunk === "string" ? chunk : chunk.toString("utf8");
@@ -679,22 +979,30 @@ async function main(argv) {
679
979
  const [cmd, ...rest] = argv;
680
980
  try {
681
981
  switch (cmd) {
982
+ case "__ccc-notifier-codex-hook": {
983
+ const event = rest[0];
984
+ if (!isCodexPassiveEvent(event) || rest.length !== 1) return 0;
985
+ const text = await readStdin();
986
+ const response = await runCodexPassiveHook(event, text);
987
+ if (response.length > 0) process.stdout.write(response);
988
+ return 0;
989
+ }
682
990
  case "track": {
683
991
  const text = await readStdin();
684
992
  const codex = rest.includes("--codex");
685
993
  try {
686
- const trackMod = await import("./track-QT2U3E2R.js");
994
+ const trackMod = await import("./track-KEZON6KI.js");
687
995
  await trackMod.runTrack(text, { codex });
688
996
  } catch {
689
997
  }
690
998
  return 0;
691
999
  }
692
1000
  case "init": {
693
- const { runInit } = await import("./setup-W3CSTHJC.js");
1001
+ const { runInit } = await import("./setup-IXPMVWOE.js");
694
1002
  return await runInit(rest);
695
1003
  }
696
1004
  case "uninstall": {
697
- const { runUninstall } = await import("./setup-W3CSTHJC.js");
1005
+ const { runUninstall } = await import("./setup-IXPMVWOE.js");
698
1006
  return await runUninstall(rest);
699
1007
  }
700
1008
  case "doctor":
@@ -702,27 +1010,27 @@ async function main(argv) {
702
1010
  case "report":
703
1011
  return await runReport(rest);
704
1012
  case "dashboard": {
705
- const { runDashboard } = await import("./dashboard-GYKABTTN.js");
1013
+ const { runDashboard } = await import("./dashboard-6F2QNUJT.js");
706
1014
  return await runDashboard(rest);
707
1015
  }
708
1016
  case "sweep": {
709
- const { runSweep } = await import("./sweep-IBSV4LRD.js");
1017
+ const { runSweep } = await import("./sweep-UIONM4UA.js");
710
1018
  return await runSweep(rest);
711
1019
  }
712
1020
  case "history": {
713
- const { runHistory } = await import("./history-6OUJLXJT.js");
1021
+ const { runHistory } = await import("./history-7KN26LJH.js");
714
1022
  return await runHistory(rest);
715
1023
  }
716
1024
  case "budget": {
717
- const { runBudget } = await import("./budget-EHWPEYXY.js");
1025
+ const { runBudget } = await import("./budget-TBWVHUKA.js");
718
1026
  return runBudget(rest);
719
1027
  }
720
1028
  case "mute": {
721
- const { runMute } = await import("./mute-GBDNN5PB.js");
1029
+ const { runMute } = await import("./mute-MPX3K7QN.js");
722
1030
  return runMute(rest);
723
1031
  }
724
1032
  case "unmute": {
725
- const { runUnmute } = await import("./mute-GBDNN5PB.js");
1033
+ const { runUnmute } = await import("./mute-MPX3K7QN.js");
726
1034
  return runUnmute();
727
1035
  }
728
1036
  case "--version":
@@ -749,8 +1057,8 @@ function isEntryPoint() {
749
1057
  const invoked = process.argv[1];
750
1058
  if (!invoked) return false;
751
1059
  try {
752
- const invokedUrl = pathToFileURL(realpathSync(invoked)).href;
753
- const selfUrl = pathToFileURL(realpathSync(fileURLToPath(import.meta.url))).href;
1060
+ const invokedUrl = pathToFileURL(realpathSync2(invoked)).href;
1061
+ const selfUrl = pathToFileURL(realpathSync2(fileURLToPath(import.meta.url))).href;
754
1062
  return invokedUrl === selfUrl;
755
1063
  } catch {
756
1064
  try {
@@ -766,5 +1074,6 @@ if (isEntryPoint()) {
766
1074
  });
767
1075
  }
768
1076
  export {
769
- main
1077
+ main,
1078
+ runCodexPassiveHook
770
1079
  };