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