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.
- package/LICENSE +21 -0
- package/README.md +171 -0
- package/dist/budget-5H2GQRXH.js +71 -0
- package/dist/chunk-4JNZ7BHO.js +276 -0
- package/dist/chunk-64N5SGTT.js +1024 -0
- package/dist/chunk-DGXUSPS4.js +21 -0
- package/dist/chunk-IIYMGLV4.js +309 -0
- package/dist/chunk-KUJZYZSG.js +66 -0
- package/dist/chunk-L5MLVTA2.js +393 -0
- package/dist/chunk-OEG3AVU6.js +436 -0
- package/dist/chunk-TB5A7U7G.js +82 -0
- package/dist/chunk-TBFKGFZX.js +87 -0
- package/dist/cli.js +717 -0
- package/dist/dashboard-LKK6NOBN.js +14 -0
- package/dist/history-MWFHO5VR.js +114 -0
- package/dist/mute-2JW3NQXN.js +12 -0
- package/dist/setup-RT62VO37.js +15 -0
- package/dist/sweep-XKKODGMA.js +460 -0
- package/dist/track-PBHTVZ3L.js +168 -0
- package/package.json +53 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
browserOpenPlan,
|
|
4
|
+
runDashboard,
|
|
5
|
+
writeDashboardHtml
|
|
6
|
+
} from "./chunk-64N5SGTT.js";
|
|
7
|
+
import "./chunk-DGXUSPS4.js";
|
|
8
|
+
import "./chunk-TBFKGFZX.js";
|
|
9
|
+
import "./chunk-IIYMGLV4.js";
|
|
10
|
+
export {
|
|
11
|
+
browserOpenPlan,
|
|
12
|
+
runDashboard,
|
|
13
|
+
writeDashboardHtml
|
|
14
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
paths
|
|
4
|
+
} from "./chunk-IIYMGLV4.js";
|
|
5
|
+
|
|
6
|
+
// src/history.ts
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync, renameSync, rmSync } from "fs";
|
|
8
|
+
import * as p from "@clack/prompts";
|
|
9
|
+
function parseFlags(argv) {
|
|
10
|
+
let days = null;
|
|
11
|
+
let yes = false;
|
|
12
|
+
for (let i = 0; i < argv.length; i++) {
|
|
13
|
+
const a = argv[i];
|
|
14
|
+
if (a === "--yes" || a === "-y") {
|
|
15
|
+
yes = true;
|
|
16
|
+
} else if (a === "--days") {
|
|
17
|
+
const n = Number.parseInt(argv[i + 1] ?? "", 10);
|
|
18
|
+
if (Number.isFinite(n) && n > 0) days = n;
|
|
19
|
+
i++;
|
|
20
|
+
} else if (a.startsWith("--days=")) {
|
|
21
|
+
const n = Number.parseInt(a.slice("--days=".length), 10);
|
|
22
|
+
if (Number.isFinite(n) && n > 0) days = n;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return { days, yes };
|
|
26
|
+
}
|
|
27
|
+
function readLines(file) {
|
|
28
|
+
const raw = readFileSync(file, "utf8");
|
|
29
|
+
const lines = [];
|
|
30
|
+
for (const line of raw.split("\n")) {
|
|
31
|
+
if (!line.trim()) continue;
|
|
32
|
+
let rec = null;
|
|
33
|
+
try {
|
|
34
|
+
rec = JSON.parse(line);
|
|
35
|
+
} catch {
|
|
36
|
+
rec = null;
|
|
37
|
+
}
|
|
38
|
+
lines.push({ raw: line, rec });
|
|
39
|
+
}
|
|
40
|
+
return lines;
|
|
41
|
+
}
|
|
42
|
+
function isTargeted(rec, cutoffMs) {
|
|
43
|
+
if (cutoffMs === null) return true;
|
|
44
|
+
const ts = Date.parse(rec.ts);
|
|
45
|
+
if (!Number.isFinite(ts)) return false;
|
|
46
|
+
return ts < cutoffMs;
|
|
47
|
+
}
|
|
48
|
+
function atomicWrite(file, content) {
|
|
49
|
+
const tmp = `${file}.tmp`;
|
|
50
|
+
writeFileSync(tmp, content, "utf8");
|
|
51
|
+
renameSync(tmp, file);
|
|
52
|
+
}
|
|
53
|
+
async function runHistory(argv) {
|
|
54
|
+
const [sub, ...rest] = argv;
|
|
55
|
+
if (sub !== "clear" && sub !== "redact") {
|
|
56
|
+
console.error(
|
|
57
|
+
"\u4F7F\u3044\u65B9 / Usage: ccc-notifier history <clear|redact> [--days N] [--yes]\n clear \u2026 \u30EC\u30B3\u30FC\u30C9\u3054\u3068\u524A\u9664(\u30C1\u30E3\u30FC\u30C8\u30FB\u96C6\u8A08\u304B\u3089\u3082\u6D88\u3048\u308B) / delete records\n redact \u2026 \u30D7\u30ED\u30F3\u30D7\u30C8\u5168\u6587\u3060\u3051\u6D88\u3059(\u30B3\u30B9\u30C8\u30FB\u30C1\u30E3\u30FC\u30C8\u306F\u6B8B\u3059) / strip prompts only"
|
|
58
|
+
);
|
|
59
|
+
return 1;
|
|
60
|
+
}
|
|
61
|
+
const flags = parseFlags(rest);
|
|
62
|
+
const file = paths().historyFile;
|
|
63
|
+
if (!existsSync(file)) {
|
|
64
|
+
console.log("\u5C65\u6B74\u304C\u3042\u308A\u307E\u305B\u3093(history.jsonl \u306F\u672A\u4F5C\u6210\u3067\u3059)\u3002");
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
const lines = readLines(file);
|
|
68
|
+
const cutoff = flags.days !== null ? Date.now() - flags.days * 864e5 : null;
|
|
69
|
+
const scope = flags.days !== null ? `${flags.days}\u65E5\u3088\u308A\u524D` : "\u5168\u671F\u9593";
|
|
70
|
+
const targetSet = /* @__PURE__ */ new Set();
|
|
71
|
+
for (let i = 0; i < lines.length; i++) {
|
|
72
|
+
const rec = lines[i].rec;
|
|
73
|
+
if (!rec || !isTargeted(rec, cutoff)) continue;
|
|
74
|
+
if (sub === "redact" && !(typeof rec.prompt === "string" && rec.prompt.length > 0)) continue;
|
|
75
|
+
targetSet.add(i);
|
|
76
|
+
}
|
|
77
|
+
if (targetSet.size === 0) {
|
|
78
|
+
console.log(`\u5BFE\u8C61\u304C\u3042\u308A\u307E\u305B\u3093(${scope})\u3002`);
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
const action = sub === "clear" ? "\u30EC\u30B3\u30FC\u30C9\u3054\u3068\u524A\u9664" : "\u30D7\u30ED\u30F3\u30D7\u30C8\u5168\u6587\u3092\u6D88\u53BB";
|
|
82
|
+
if (!flags.yes) {
|
|
83
|
+
const confirmed = await p.confirm({
|
|
84
|
+
message: `${scope}\u306E\u5C65\u6B74 ${targetSet.size} \u4EF6\u3092${action}\u3057\u307E\u3059\u3002\u5143\u306B\u623B\u305B\u307E\u305B\u3093\u3002\u3088\u308D\u3057\u3044\u3067\u3059\u304B?`,
|
|
85
|
+
initialValue: false
|
|
86
|
+
});
|
|
87
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
88
|
+
p.cancel("\u30AD\u30E3\u30F3\u30BB\u30EB\u3057\u307E\u3057\u305F");
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (sub === "clear") {
|
|
93
|
+
const kept = lines.filter((_, i) => !targetSet.has(i)).map((l) => l.raw);
|
|
94
|
+
if (kept.length === 0) {
|
|
95
|
+
rmSync(file, { force: true });
|
|
96
|
+
} else {
|
|
97
|
+
atomicWrite(file, kept.join("\n") + "\n");
|
|
98
|
+
}
|
|
99
|
+
console.log(`\u5C65\u6B74 ${targetSet.size} \u4EF6\u3092\u524A\u9664\u3057\u307E\u3057\u305F(${scope})\u3002`);
|
|
100
|
+
} else {
|
|
101
|
+
const out = lines.map((l, i) => {
|
|
102
|
+
if (!targetSet.has(i) || l.rec === null) return l.raw;
|
|
103
|
+
return JSON.stringify({ ...l.rec, prompt: "" });
|
|
104
|
+
});
|
|
105
|
+
atomicWrite(file, out.join("\n") + "\n");
|
|
106
|
+
console.log(
|
|
107
|
+
`\u30D7\u30ED\u30F3\u30D7\u30C8\u5168\u6587\u3092 ${targetSet.size} \u4EF6\u6D88\u53BB\u3057\u307E\u3057\u305F(${scope}\u3002\u30B3\u30B9\u30C8\u96C6\u8A08\u30FB\u30C1\u30E3\u30FC\u30C8\u306F\u4FDD\u6301\u3055\u308C\u307E\u3059)\u3002`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
export {
|
|
113
|
+
runHistory
|
|
114
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
matchesMarker,
|
|
4
|
+
runInit,
|
|
5
|
+
runUninstall
|
|
6
|
+
} from "./chunk-L5MLVTA2.js";
|
|
7
|
+
import "./chunk-4JNZ7BHO.js";
|
|
8
|
+
import "./chunk-DGXUSPS4.js";
|
|
9
|
+
import "./chunk-TBFKGFZX.js";
|
|
10
|
+
import "./chunk-IIYMGLV4.js";
|
|
11
|
+
export {
|
|
12
|
+
matchesMarker,
|
|
13
|
+
runInit,
|
|
14
|
+
runUninstall
|
|
15
|
+
};
|
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
collectSubagentUsage
|
|
4
|
+
} from "./chunk-TB5A7U7G.js";
|
|
5
|
+
import {
|
|
6
|
+
computeCost,
|
|
7
|
+
extractBucket,
|
|
8
|
+
getUsdJpy,
|
|
9
|
+
loadPriceTable,
|
|
10
|
+
promptCandidate
|
|
11
|
+
} from "./chunk-OEG3AVU6.js";
|
|
12
|
+
import {
|
|
13
|
+
formatJPY,
|
|
14
|
+
formatUSD,
|
|
15
|
+
modelDisplayName
|
|
16
|
+
} from "./chunk-TBFKGFZX.js";
|
|
17
|
+
import {
|
|
18
|
+
appendTurn,
|
|
19
|
+
loadCursor,
|
|
20
|
+
logError,
|
|
21
|
+
paths,
|
|
22
|
+
readConfig,
|
|
23
|
+
sanitizeCursor,
|
|
24
|
+
saveCursor
|
|
25
|
+
} from "./chunk-IIYMGLV4.js";
|
|
26
|
+
|
|
27
|
+
// src/sweep.ts
|
|
28
|
+
import { readFile } from "fs/promises";
|
|
29
|
+
import { promises as fsp } from "fs";
|
|
30
|
+
import { join } from "path";
|
|
31
|
+
import { homedir } from "os";
|
|
32
|
+
var NEWLINE = 10;
|
|
33
|
+
var MAX_SEEN_KEYS = 500;
|
|
34
|
+
var SYNTHETIC_MODEL = "<synthetic>";
|
|
35
|
+
var DAY_MS = 864e5;
|
|
36
|
+
var ACTIVE_GUARD_MIN = 5;
|
|
37
|
+
var ACTIVE_GUARD_MS = ACTIVE_GUARD_MIN * 6e4;
|
|
38
|
+
function isRecord(v) {
|
|
39
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
40
|
+
}
|
|
41
|
+
function strOrNull(v) {
|
|
42
|
+
return typeof v === "string" ? v : null;
|
|
43
|
+
}
|
|
44
|
+
function emptyBuckets() {
|
|
45
|
+
return { input: 0, output: 0, cacheWrite5m: 0, cacheWrite1h: 0, cacheRead: 0 };
|
|
46
|
+
}
|
|
47
|
+
function addToModel(target, model, b) {
|
|
48
|
+
const cur = target[model] ?? emptyBuckets();
|
|
49
|
+
cur.input += b.input;
|
|
50
|
+
cur.output += b.output;
|
|
51
|
+
cur.cacheWrite5m += b.cacheWrite5m;
|
|
52
|
+
cur.cacheWrite1h += b.cacheWrite1h;
|
|
53
|
+
cur.cacheRead += b.cacheRead;
|
|
54
|
+
target[model] = cur;
|
|
55
|
+
}
|
|
56
|
+
function sumBuckets(usage) {
|
|
57
|
+
const total = emptyBuckets();
|
|
58
|
+
for (const b of Object.values(usage)) {
|
|
59
|
+
total.input += b.input;
|
|
60
|
+
total.output += b.output;
|
|
61
|
+
total.cacheWrite5m += b.cacheWrite5m;
|
|
62
|
+
total.cacheWrite1h += b.cacheWrite1h;
|
|
63
|
+
total.cacheRead += b.cacheRead;
|
|
64
|
+
}
|
|
65
|
+
return total;
|
|
66
|
+
}
|
|
67
|
+
function collectModels(main, sidechain) {
|
|
68
|
+
const models = [];
|
|
69
|
+
for (const m of Object.keys(main)) if (!models.includes(m)) models.push(m);
|
|
70
|
+
for (const m of Object.keys(sidechain)) if (!models.includes(m)) models.push(m);
|
|
71
|
+
return models;
|
|
72
|
+
}
|
|
73
|
+
function newBuffer() {
|
|
74
|
+
return {
|
|
75
|
+
prompt: null,
|
|
76
|
+
pending: /* @__PURE__ */ new Map(),
|
|
77
|
+
sessionId: "",
|
|
78
|
+
cwd: null,
|
|
79
|
+
gitBranch: null,
|
|
80
|
+
firstTs: null,
|
|
81
|
+
lastTs: null
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
async function readAll(path) {
|
|
85
|
+
try {
|
|
86
|
+
return await readFile(path);
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function splitIntoTurnDrafts(transcriptPath, cursor) {
|
|
92
|
+
const buffer = await readAll(transcriptPath);
|
|
93
|
+
if (buffer === null) {
|
|
94
|
+
const nc = cursor ?? { offset: 0, lastUuid: null, lastTs: null, seenMessageKeys: [] };
|
|
95
|
+
return { drafts: [], newCursor: nc };
|
|
96
|
+
}
|
|
97
|
+
const fileSize = buffer.length;
|
|
98
|
+
let startOffset;
|
|
99
|
+
let rescan;
|
|
100
|
+
if (cursor !== null && cursor.offset > 0 && cursor.offset <= fileSize && buffer[cursor.offset - 1] === NEWLINE) {
|
|
101
|
+
startOffset = cursor.offset;
|
|
102
|
+
rescan = false;
|
|
103
|
+
} else {
|
|
104
|
+
startOffset = 0;
|
|
105
|
+
rescan = cursor !== null;
|
|
106
|
+
}
|
|
107
|
+
const priorSeen = new Set(cursor?.seenMessageKeys ?? []);
|
|
108
|
+
const tsFloor = cursor?.lastTs ?? null;
|
|
109
|
+
const drafts = [];
|
|
110
|
+
const runSeen = /* @__PURE__ */ new Set();
|
|
111
|
+
const newKeys = [];
|
|
112
|
+
let winLastUuid = null;
|
|
113
|
+
let winLastTs = null;
|
|
114
|
+
let cur = newBuffer();
|
|
115
|
+
const flush = () => {
|
|
116
|
+
if (cur.pending.size === 0) return;
|
|
117
|
+
const mainPerModel = {};
|
|
118
|
+
const sidechainPerModel = {};
|
|
119
|
+
for (const [key, pm] of cur.pending) {
|
|
120
|
+
runSeen.add(key);
|
|
121
|
+
newKeys.push(key);
|
|
122
|
+
if (pm.isSidechain) addToModel(sidechainPerModel, pm.model, pm.bucket);
|
|
123
|
+
else addToModel(mainPerModel, pm.model, pm.bucket);
|
|
124
|
+
}
|
|
125
|
+
drafts.push({
|
|
126
|
+
prompt: cur.prompt ?? "",
|
|
127
|
+
mainPerModel,
|
|
128
|
+
sidechainPerModel,
|
|
129
|
+
apiCalls: cur.pending.size,
|
|
130
|
+
firstTs: cur.firstTs,
|
|
131
|
+
lastTs: cur.lastTs,
|
|
132
|
+
cwd: cur.cwd,
|
|
133
|
+
gitBranch: cur.gitBranch,
|
|
134
|
+
sessionId: cur.sessionId
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
const handleLine = (raw) => {
|
|
138
|
+
if (raw.trim().length === 0) return;
|
|
139
|
+
let obj;
|
|
140
|
+
try {
|
|
141
|
+
obj = JSON.parse(raw);
|
|
142
|
+
} catch {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (!isRecord(obj)) return;
|
|
146
|
+
const ts = strOrNull(obj.timestamp);
|
|
147
|
+
if (rescan && tsFloor !== null && ts !== null && ts <= tsFloor) return;
|
|
148
|
+
const isSide = obj.isSidechain === true;
|
|
149
|
+
const type = obj.type;
|
|
150
|
+
const message = isRecord(obj.message) ? obj.message : null;
|
|
151
|
+
let isBoundary = false;
|
|
152
|
+
let boundaryPrompt = "";
|
|
153
|
+
if (type === "user" && !isSide && message !== null) {
|
|
154
|
+
const cand = promptCandidate(message.content);
|
|
155
|
+
if (cand !== null) {
|
|
156
|
+
const t = cand.trim();
|
|
157
|
+
if (t.length > 0 && !t.startsWith("<")) {
|
|
158
|
+
isBoundary = true;
|
|
159
|
+
boundaryPrompt = t;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (isBoundary) {
|
|
164
|
+
flush();
|
|
165
|
+
cur = newBuffer();
|
|
166
|
+
cur.prompt = boundaryPrompt;
|
|
167
|
+
}
|
|
168
|
+
const sid = strOrNull(obj.sessionId);
|
|
169
|
+
if (sid !== null) cur.sessionId = sid;
|
|
170
|
+
if (!isSide) {
|
|
171
|
+
const c = strOrNull(obj.cwd);
|
|
172
|
+
if (c !== null) cur.cwd = c;
|
|
173
|
+
const gb = strOrNull(obj.gitBranch);
|
|
174
|
+
if (gb !== null) cur.gitBranch = gb;
|
|
175
|
+
}
|
|
176
|
+
if (ts !== null) {
|
|
177
|
+
if (cur.firstTs === null || ts < cur.firstTs) cur.firstTs = ts;
|
|
178
|
+
if (cur.lastTs === null || ts > cur.lastTs) cur.lastTs = ts;
|
|
179
|
+
if (winLastTs === null || ts > winLastTs) winLastTs = ts;
|
|
180
|
+
}
|
|
181
|
+
const uuid = strOrNull(obj.uuid);
|
|
182
|
+
if (uuid !== null) winLastUuid = uuid;
|
|
183
|
+
if (type === "assistant" && message !== null) {
|
|
184
|
+
const usage = message.usage;
|
|
185
|
+
if (isRecord(usage)) {
|
|
186
|
+
const rawModel = message.model;
|
|
187
|
+
if (rawModel !== SYNTHETIC_MODEL) {
|
|
188
|
+
const id = strOrNull(message.id) ?? "";
|
|
189
|
+
const reqId = strOrNull(obj.requestId) ?? "";
|
|
190
|
+
const key = `${id}:${reqId}`;
|
|
191
|
+
if (!priorSeen.has(key) && !runSeen.has(key)) {
|
|
192
|
+
const model = strOrNull(rawModel) ?? "unknown";
|
|
193
|
+
cur.pending.set(key, { model, isSidechain: isSide, bucket: extractBucket(usage) });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
let lineStart = startOffset;
|
|
200
|
+
for (let pos = startOffset; pos < fileSize; pos++) {
|
|
201
|
+
if (buffer[pos] !== NEWLINE) continue;
|
|
202
|
+
handleLine(buffer.toString("utf8", lineStart, pos));
|
|
203
|
+
lineStart = pos + 1;
|
|
204
|
+
}
|
|
205
|
+
const newOffset = lineStart;
|
|
206
|
+
flush();
|
|
207
|
+
const combined = [...cursor?.seenMessageKeys ?? [], ...newKeys];
|
|
208
|
+
const seenMessageKeys = combined.length > MAX_SEEN_KEYS ? combined.slice(combined.length - MAX_SEEN_KEYS) : combined;
|
|
209
|
+
const newCursor = {
|
|
210
|
+
offset: newOffset,
|
|
211
|
+
lastUuid: winLastUuid,
|
|
212
|
+
lastTs: winLastTs,
|
|
213
|
+
seenMessageKeys
|
|
214
|
+
};
|
|
215
|
+
return { drafts, newCursor };
|
|
216
|
+
}
|
|
217
|
+
function draftToRecord(draft, ts, table, fx) {
|
|
218
|
+
const breakdown = computeCost(draft.mainPerModel, draft.sidechainPerModel, table);
|
|
219
|
+
const sidechainHasModels = Object.keys(draft.sidechainPerModel).length > 0;
|
|
220
|
+
const rec = {
|
|
221
|
+
schemaVersion: 1,
|
|
222
|
+
ts,
|
|
223
|
+
sessionId: draft.sessionId,
|
|
224
|
+
project: draft.cwd ?? "",
|
|
225
|
+
gitBranch: draft.gitBranch,
|
|
226
|
+
models: collectModels(draft.mainPerModel, draft.sidechainPerModel),
|
|
227
|
+
tokens: sumBuckets(draft.mainPerModel),
|
|
228
|
+
sidechainTokens: sidechainHasModels ? sumBuckets(draft.sidechainPerModel) : null,
|
|
229
|
+
apiCalls: draft.apiCalls,
|
|
230
|
+
costUSD: breakdown.usd,
|
|
231
|
+
costByModel: breakdown.byModel,
|
|
232
|
+
costJPY: breakdown.usd * fx.rate,
|
|
233
|
+
// 円換算は sweep 実行時レート
|
|
234
|
+
fxRate: fx.rate,
|
|
235
|
+
fxSource: fx.source,
|
|
236
|
+
prompt: draft.prompt,
|
|
237
|
+
ingest: "sweep"
|
|
238
|
+
};
|
|
239
|
+
if (breakdown.unknownModels.length > 0) rec.unknownModels = breakdown.unknownModels;
|
|
240
|
+
return rec;
|
|
241
|
+
}
|
|
242
|
+
function maxCursorTs(newCursors) {
|
|
243
|
+
let max = null;
|
|
244
|
+
for (const nc of newCursors) {
|
|
245
|
+
const t = nc.cursor.lastTs;
|
|
246
|
+
if (t !== null && (max === null || t > max)) max = t;
|
|
247
|
+
}
|
|
248
|
+
return max;
|
|
249
|
+
}
|
|
250
|
+
function mergeUnknownModels(rec, extra) {
|
|
251
|
+
if (extra.length === 0) return;
|
|
252
|
+
const merged = rec.unknownModels ? [...rec.unknownModels] : [];
|
|
253
|
+
for (const m of extra) if (!merged.includes(m)) merged.push(m);
|
|
254
|
+
rec.unknownModels = merged;
|
|
255
|
+
}
|
|
256
|
+
async function processTranscript(mainPath, table, fx, daysCutoff, dryRun, summary) {
|
|
257
|
+
const cursor = sanitizeCursor(loadCursor(mainPath));
|
|
258
|
+
const { drafts, newCursor } = await splitIntoTurnDrafts(mainPath, cursor);
|
|
259
|
+
const records = [];
|
|
260
|
+
for (const draft of drafts) {
|
|
261
|
+
const ts = draft.lastTs ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
262
|
+
if (daysCutoff !== null) {
|
|
263
|
+
const tsMs = Date.parse(ts);
|
|
264
|
+
if (!Number.isFinite(tsMs) || tsMs < daysCutoff) continue;
|
|
265
|
+
}
|
|
266
|
+
records.push(draftToRecord(draft, ts, table, fx));
|
|
267
|
+
}
|
|
268
|
+
let sa = null;
|
|
269
|
+
try {
|
|
270
|
+
sa = await collectSubagentUsage(mainPath);
|
|
271
|
+
} catch (err) {
|
|
272
|
+
logError("sweep:subagents", err);
|
|
273
|
+
sa = null;
|
|
274
|
+
}
|
|
275
|
+
const saHasUsage = sa !== null && sa.apiCalls > 0;
|
|
276
|
+
if (saHasUsage) {
|
|
277
|
+
const saBreakdown = computeCost(sa.perModel, {}, table);
|
|
278
|
+
summary.subagentsUSD += saBreakdown.usd;
|
|
279
|
+
const saBlock = {
|
|
280
|
+
costUSD: saBreakdown.usd,
|
|
281
|
+
costByModel: saBreakdown.byModel,
|
|
282
|
+
tokens: sumBuckets(sa.perModel),
|
|
283
|
+
apiCalls: sa.apiCalls,
|
|
284
|
+
agentFiles: sa.agentFiles
|
|
285
|
+
};
|
|
286
|
+
if (records.length > 0) {
|
|
287
|
+
const last = records[records.length - 1];
|
|
288
|
+
last.subagents = saBlock;
|
|
289
|
+
mergeUnknownModels(last, saBreakdown.unknownModels);
|
|
290
|
+
} else {
|
|
291
|
+
const rec = {
|
|
292
|
+
schemaVersion: 1,
|
|
293
|
+
ts: maxCursorTs(sa.newCursors) ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
294
|
+
sessionId: "",
|
|
295
|
+
project: "",
|
|
296
|
+
gitBranch: null,
|
|
297
|
+
models: collectModels(sa.perModel, {}),
|
|
298
|
+
tokens: emptyBuckets(),
|
|
299
|
+
sidechainTokens: null,
|
|
300
|
+
apiCalls: 0,
|
|
301
|
+
costUSD: 0,
|
|
302
|
+
costByModel: {},
|
|
303
|
+
costJPY: 0,
|
|
304
|
+
fxRate: fx.rate,
|
|
305
|
+
fxSource: fx.source,
|
|
306
|
+
prompt: "",
|
|
307
|
+
subagents: saBlock,
|
|
308
|
+
ingest: "sweep"
|
|
309
|
+
};
|
|
310
|
+
mergeUnknownModels(rec, saBreakdown.unknownModels);
|
|
311
|
+
records.push(rec);
|
|
312
|
+
}
|
|
313
|
+
summary.agentFiles += sa.agentFiles;
|
|
314
|
+
}
|
|
315
|
+
for (const rec of records) {
|
|
316
|
+
summary.newRecords += 1;
|
|
317
|
+
summary.totalUSD += rec.costUSD;
|
|
318
|
+
if (rec.costByModel) {
|
|
319
|
+
for (const [m, c] of Object.entries(rec.costByModel)) {
|
|
320
|
+
summary.byModel[m] = (summary.byModel[m] ?? 0) + c;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (!dryRun) {
|
|
325
|
+
for (const rec of records) appendTurn(rec);
|
|
326
|
+
if (drafts.length > 0) saveCursor(mainPath, newCursor);
|
|
327
|
+
if (saHasUsage) {
|
|
328
|
+
for (const nc of sa.newCursors) saveCursor(nc.path, nc.cursor);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function projectsRoot(override) {
|
|
333
|
+
if (override) return override;
|
|
334
|
+
return process.env.CCCN_CLAUDE_PROJECTS || join(homedir(), ".claude", "projects");
|
|
335
|
+
}
|
|
336
|
+
function parseSweepFlags(argv) {
|
|
337
|
+
const flags = { dryRun: false, days: null, projects: null, includeActive: false };
|
|
338
|
+
for (let i = 0; i < argv.length; i++) {
|
|
339
|
+
const a = argv[i];
|
|
340
|
+
if (a === "--dry-run") {
|
|
341
|
+
flags.dryRun = true;
|
|
342
|
+
} else if (a === "--include-active") {
|
|
343
|
+
flags.includeActive = true;
|
|
344
|
+
} else if (a === "--days" || a.startsWith("--days=")) {
|
|
345
|
+
const v = a.includes("=") ? a.slice("--days=".length) : argv[++i];
|
|
346
|
+
const n = Number(v);
|
|
347
|
+
if (Number.isFinite(n) && n >= 0) flags.days = n;
|
|
348
|
+
} else if (a === "--projects" || a.startsWith("--projects=")) {
|
|
349
|
+
const v = a.includes("=") ? a.slice("--projects=".length) : argv[++i];
|
|
350
|
+
if (v) flags.projects = v;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return flags;
|
|
354
|
+
}
|
|
355
|
+
async function listProjectDirs(root) {
|
|
356
|
+
try {
|
|
357
|
+
const entries = await fsp.readdir(root, { withFileTypes: true });
|
|
358
|
+
return entries.filter((e) => e.isDirectory()).map((e) => join(root, e.name));
|
|
359
|
+
} catch {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
async function isRecentlyModified(path) {
|
|
364
|
+
try {
|
|
365
|
+
const st = await fsp.stat(path);
|
|
366
|
+
return Date.now() - st.mtimeMs < ACTIVE_GUARD_MS;
|
|
367
|
+
} catch {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
async function listTranscripts(projectDir) {
|
|
372
|
+
try {
|
|
373
|
+
const entries = await fsp.readdir(projectDir, { withFileTypes: true });
|
|
374
|
+
return entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl")).map((e) => join(projectDir, e.name));
|
|
375
|
+
} catch {
|
|
376
|
+
return [];
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
function printSweepSummary(summary, fx) {
|
|
380
|
+
if (summary.dryRun) {
|
|
381
|
+
console.log("(dry-run: \u66F8\u304D\u8FBC\u307F\u306F\u884C\u3063\u3066\u3044\u307E\u305B\u3093)");
|
|
382
|
+
}
|
|
383
|
+
console.log(
|
|
384
|
+
`\u8D70\u67FB: \u30D7\u30ED\u30B8\u30A7\u30AF\u30C8 ${summary.projects} / transcript ${summary.transcripts} / \u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8\u30D5\u30A1\u30A4\u30EB ${summary.agentFiles}`
|
|
385
|
+
);
|
|
386
|
+
if (summary.skippedActive > 0) {
|
|
387
|
+
console.log(
|
|
388
|
+
`\u30B9\u30AD\u30C3\u30D7: ${summary.skippedActive} transcript(\u76F4\u8FD1${ACTIVE_GUARD_MIN}\u5206\u4EE5\u5185\u306B\u66F4\u65B0 = \u9032\u884C\u4E2D\u30BB\u30C3\u30B7\u30E7\u30F3\u306E\u53EF\u80FD\u6027)\u3002\u30BB\u30C3\u30B7\u30E7\u30F3\u5B8C\u4E86\u5F8C\u306B\u518D\u5B9F\u884C\u3059\u308B\u304B\u3001\u5B8C\u4E86\u6E08\u307F\u3068\u5206\u304B\u3063\u3066\u3044\u308B\u5834\u5408\u306F --include-active \u3067\u53D6\u308A\u8FBC\u3081\u307E\u3059`
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
if (summary.newRecords === 0) {
|
|
392
|
+
console.log("\u65B0\u898F\u306F\u3042\u308A\u307E\u305B\u3093\u3067\u3057\u305F");
|
|
393
|
+
} else {
|
|
394
|
+
console.log(
|
|
395
|
+
`\u65B0\u898F\u53D6\u308A\u8FBC\u307F: ${summary.newRecords} \u30BF\u30FC\u30F3\u3001\u5408\u8A08 ${formatUSD(summary.totalUSD)}(${formatJPY(summary.totalJPY)})`
|
|
396
|
+
);
|
|
397
|
+
if (summary.subagentsUSD > 0) {
|
|
398
|
+
console.log(
|
|
399
|
+
` \u3046\u3061\u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8: ${formatUSD(summary.subagentsUSD)}(${formatJPY(summary.subagentsUSD * fx.rate)})`
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
const top = Object.entries(summary.byModel).filter(([, c]) => c > 0).sort((a, b) => b[1] - a[1]).slice(0, 5);
|
|
403
|
+
if (top.length > 0) {
|
|
404
|
+
console.log(" \u30E2\u30C7\u30EB\u5225(\u4E0A\u4F4D):");
|
|
405
|
+
for (const [m, c] of top) {
|
|
406
|
+
console.log(` ${modelDisplayName(m)}: ${formatUSD(c)}`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
console.log("\u65E2\u306B\u8A08\u4E0A\u6E08\u307F\u306E\u5206\u306F\u30B9\u30AD\u30C3\u30D7\u3055\u308C\u307E\u3057\u305F(\u4E8C\u91CD\u8A08\u4E0A\u306A\u3057)");
|
|
411
|
+
console.log(`\u5186\u63DB\u7B97\u30EC\u30FC\u30C8: 1USD = ${fx.rate}JPY(source=${fx.source})`);
|
|
412
|
+
}
|
|
413
|
+
async function runSweep(argv) {
|
|
414
|
+
const flags = parseSweepFlags(argv);
|
|
415
|
+
const root = projectsRoot(flags.projects);
|
|
416
|
+
const projectDirs = await listProjectDirs(root);
|
|
417
|
+
if (projectDirs === null) {
|
|
418
|
+
console.log(`\u8D70\u67FB\u30EB\u30FC\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${root}`);
|
|
419
|
+
return 1;
|
|
420
|
+
}
|
|
421
|
+
const cfg = readConfig();
|
|
422
|
+
const cacheDir = paths().cacheDir;
|
|
423
|
+
const table = await loadPriceTable(cacheDir, { offline: false });
|
|
424
|
+
const fx = await getUsdJpy(cfg, cacheDir);
|
|
425
|
+
const summary = {
|
|
426
|
+
projects: projectDirs.length,
|
|
427
|
+
transcripts: 0,
|
|
428
|
+
agentFiles: 0,
|
|
429
|
+
newRecords: 0,
|
|
430
|
+
totalUSD: 0,
|
|
431
|
+
totalJPY: 0,
|
|
432
|
+
subagentsUSD: 0,
|
|
433
|
+
byModel: {},
|
|
434
|
+
skippedActive: 0,
|
|
435
|
+
dryRun: flags.dryRun
|
|
436
|
+
};
|
|
437
|
+
const daysCutoff = flags.days !== null ? Date.now() - flags.days * DAY_MS : null;
|
|
438
|
+
for (const projectDir of projectDirs) {
|
|
439
|
+
const transcripts = await listTranscripts(projectDir);
|
|
440
|
+
for (const mainPath of transcripts) {
|
|
441
|
+
summary.transcripts += 1;
|
|
442
|
+
if (!flags.includeActive && await isRecentlyModified(mainPath)) {
|
|
443
|
+
summary.skippedActive += 1;
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
try {
|
|
447
|
+
await processTranscript(mainPath, table, fx, daysCutoff, flags.dryRun, summary);
|
|
448
|
+
} catch (err) {
|
|
449
|
+
logError("sweep:transcript", err);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
summary.totalJPY = summary.totalUSD * fx.rate;
|
|
454
|
+
printSweepSummary(summary, fx);
|
|
455
|
+
return 0;
|
|
456
|
+
}
|
|
457
|
+
export {
|
|
458
|
+
runSweep,
|
|
459
|
+
splitIntoTurnDrafts
|
|
460
|
+
};
|