wendkeep 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/README.md +85 -0
- package/bin/wendkeep.mjs +98 -0
- package/hooks/brain-core.mjs +142 -0
- package/hooks/brain-inject.mjs +42 -0
- package/hooks/brain-recall.mjs +32 -0
- package/hooks/brain-reindex.mjs +13 -0
- package/hooks/linked-notes.mjs +536 -0
- package/hooks/obsidian-common.mjs +517 -0
- package/hooks/pricing.json +48 -0
- package/hooks/session-backfill.mjs +141 -0
- package/hooks/session-ensure.mjs +386 -0
- package/hooks/session-start.mjs +309 -0
- package/hooks/session-stop.mjs +1110 -0
- package/hooks/token-usage.mjs +950 -0
- package/hooks/vault-health.mjs +201 -0
- package/package.json +35 -0
- package/src/doctor.mjs +36 -0
- package/src/init.mjs +171 -0
- package/src/taxonomy.mjs +76 -0
|
@@ -0,0 +1,950 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
3
|
+
import { basename, dirname, join } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import {
|
|
6
|
+
getVaultBase,
|
|
7
|
+
readControl,
|
|
8
|
+
truncate,
|
|
9
|
+
} from './obsidian-common.mjs';
|
|
10
|
+
|
|
11
|
+
// Preços API por milhão de tokens. cachedInput = cache read.
|
|
12
|
+
// Cache write: 5m = 1.25x input, 1h = 2x input (multiplicadores em calculateCost).
|
|
13
|
+
// Tabela editável em pricing.json (mesma pasta); esta é o fallback embutido
|
|
14
|
+
// usado quando o JSON some ou fica inválido — o hook nunca deve quebrar por isso.
|
|
15
|
+
const DEFAULT_PRICE_REFERENCE = {
|
|
16
|
+
'gpt-5.5': {
|
|
17
|
+
label: 'GPT-5.5 API',
|
|
18
|
+
provider: 'openai',
|
|
19
|
+
input: 5,
|
|
20
|
+
cachedInput: 0.5,
|
|
21
|
+
output: 30,
|
|
22
|
+
},
|
|
23
|
+
'claude-opus-4.7': {
|
|
24
|
+
label: 'Claude Opus 4.7 API',
|
|
25
|
+
provider: 'anthropic',
|
|
26
|
+
input: 5,
|
|
27
|
+
cachedInput: 0.5,
|
|
28
|
+
output: 25,
|
|
29
|
+
},
|
|
30
|
+
'claude-opus-4.8': {
|
|
31
|
+
label: 'Claude Opus 4.8 API',
|
|
32
|
+
provider: 'anthropic',
|
|
33
|
+
input: 5,
|
|
34
|
+
cachedInput: 0.5,
|
|
35
|
+
output: 25,
|
|
36
|
+
},
|
|
37
|
+
'claude-sonnet-4.6': {
|
|
38
|
+
label: 'Claude Sonnet 4.6 API',
|
|
39
|
+
provider: 'anthropic',
|
|
40
|
+
input: 3,
|
|
41
|
+
cachedInput: 0.3,
|
|
42
|
+
output: 15,
|
|
43
|
+
},
|
|
44
|
+
'claude-haiku-4.5': {
|
|
45
|
+
label: 'Claude Haiku 4.5 API',
|
|
46
|
+
provider: 'anthropic',
|
|
47
|
+
input: 1,
|
|
48
|
+
cachedInput: 0.1,
|
|
49
|
+
output: 5,
|
|
50
|
+
},
|
|
51
|
+
'claude-fable-5': {
|
|
52
|
+
label: 'Claude Fable 5 API',
|
|
53
|
+
provider: 'anthropic',
|
|
54
|
+
input: 10,
|
|
55
|
+
cachedInput: 1,
|
|
56
|
+
output: 50,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const PRICING_FILE = join(dirname(fileURLToPath(import.meta.url)), 'pricing.json');
|
|
61
|
+
|
|
62
|
+
// Carrega a tabela de preços do JSON editável; cai no fallback embutido se o
|
|
63
|
+
// arquivo sumir, não for JSON válido ou não tiver `models` com entradas.
|
|
64
|
+
export function loadPriceReference(file = PRICING_FILE) {
|
|
65
|
+
try {
|
|
66
|
+
const models = JSON.parse(readFileSync(file, 'utf-8'))?.models;
|
|
67
|
+
if (models && typeof models === 'object' && Object.keys(models).length) {
|
|
68
|
+
return models;
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
// arquivo ausente/corrompido — usa fallback.
|
|
72
|
+
}
|
|
73
|
+
return DEFAULT_PRICE_REFERENCE;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const PRICE_REFERENCE = loadPriceReference();
|
|
77
|
+
|
|
78
|
+
const MODEL_ALIASES = {
|
|
79
|
+
'gpt-5.5': 'gpt-5.5',
|
|
80
|
+
'gpt-5_5': 'gpt-5.5',
|
|
81
|
+
'openai/gpt-5.5': 'gpt-5.5',
|
|
82
|
+
'claude-opus-4.7': 'claude-opus-4.7',
|
|
83
|
+
'claude-opus-4-7': 'claude-opus-4.7',
|
|
84
|
+
'anthropic/claude-opus-4.7': 'claude-opus-4.7',
|
|
85
|
+
'anthropic/claude-opus-4-7': 'claude-opus-4.7',
|
|
86
|
+
'claude-opus-4.8': 'claude-opus-4.8',
|
|
87
|
+
'claude-opus-4-8': 'claude-opus-4.8',
|
|
88
|
+
'anthropic/claude-opus-4.8': 'claude-opus-4.8',
|
|
89
|
+
'anthropic/claude-opus-4-8': 'claude-opus-4.8',
|
|
90
|
+
'claude-sonnet-4.6': 'claude-sonnet-4.6',
|
|
91
|
+
'claude-sonnet-4-6': 'claude-sonnet-4.6',
|
|
92
|
+
'anthropic/claude-sonnet-4.6': 'claude-sonnet-4.6',
|
|
93
|
+
'anthropic/claude-sonnet-4-6': 'claude-sonnet-4.6',
|
|
94
|
+
'claude-haiku-4.5': 'claude-haiku-4.5',
|
|
95
|
+
'claude-haiku-4-5': 'claude-haiku-4.5',
|
|
96
|
+
'claude-haiku-4-5-20251001': 'claude-haiku-4.5',
|
|
97
|
+
'anthropic/claude-haiku-4.5': 'claude-haiku-4.5',
|
|
98
|
+
'anthropic/claude-haiku-4-5': 'claude-haiku-4.5',
|
|
99
|
+
'claude-fable-5': 'claude-fable-5',
|
|
100
|
+
'claude-fable-5[1m]': 'claude-fable-5',
|
|
101
|
+
'anthropic/claude-fable-5': 'claude-fable-5',
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const MANAGED_FRONTMATTER_KEYS = new Set([
|
|
105
|
+
'modelo',
|
|
106
|
+
'modelos',
|
|
107
|
+
'provedor_modelo',
|
|
108
|
+
'provedores_modelo',
|
|
109
|
+
'nivel_pensamento',
|
|
110
|
+
'prompts',
|
|
111
|
+
'tool_calls',
|
|
112
|
+
'tools_distinct',
|
|
113
|
+
'tools',
|
|
114
|
+
'chamadas_llm',
|
|
115
|
+
'tokens_input',
|
|
116
|
+
'tokens_cache_write',
|
|
117
|
+
'tokens_cached_input',
|
|
118
|
+
'tokens_output',
|
|
119
|
+
'tokens_reasoning',
|
|
120
|
+
'tokens_total',
|
|
121
|
+
'custo_modelo_label',
|
|
122
|
+
'custo_modelo_usd',
|
|
123
|
+
'custo_por_modelo',
|
|
124
|
+
'usage_por_transcript',
|
|
125
|
+
// Legado: chaves antigas removidas ao reprocessar a sessão.
|
|
126
|
+
'custo_estimado_gpt55_usd',
|
|
127
|
+
'custo_estimado_opus47_usd',
|
|
128
|
+
'custo_delta_opus47_usd',
|
|
129
|
+
'usage_report',
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
// Convenção interna: campos disjuntos.
|
|
133
|
+
// input = tokens de entrada NÃO cacheados; cached = cache read; cacheWrite = cache write
|
|
134
|
+
// (cacheWrite1h = subparcela 1h, para custo 2x); thinking = tokens de raciocínio
|
|
135
|
+
// (Claude: estimado de chars/3.5; Codex: reasoning_output_tokens, já contidos em output).
|
|
136
|
+
export function emptyTokenUsage() {
|
|
137
|
+
return {
|
|
138
|
+
input: 0,
|
|
139
|
+
cached: 0,
|
|
140
|
+
cacheWrite: 0,
|
|
141
|
+
cacheWrite1h: 0,
|
|
142
|
+
output: 0,
|
|
143
|
+
reasoning: 0,
|
|
144
|
+
total: 0,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Formato Codex: cached_input_tokens é SUBCONJUNTO de input_tokens — separa aqui.
|
|
149
|
+
export function normalizeCodexUsage(raw = {}) {
|
|
150
|
+
const inputAll = Number(raw.input_tokens || 0);
|
|
151
|
+
const cached = Math.min(Number(raw.cached_input_tokens || 0), inputAll);
|
|
152
|
+
const output = Number(raw.output_tokens || 0);
|
|
153
|
+
return {
|
|
154
|
+
input: inputAll - cached,
|
|
155
|
+
cached,
|
|
156
|
+
cacheWrite: 0,
|
|
157
|
+
cacheWrite1h: 0,
|
|
158
|
+
output,
|
|
159
|
+
reasoning: Number(raw.reasoning_output_tokens || 0),
|
|
160
|
+
total: Number(raw.total_tokens || 0) || inputAll + output,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Formato Claude Code: campos já disjuntos.
|
|
165
|
+
export function normalizeClaudeUsage(raw = {}) {
|
|
166
|
+
const input = Number(raw.input_tokens || 0);
|
|
167
|
+
const cached = Number(raw.cache_read_input_tokens || 0);
|
|
168
|
+
const cacheWrite = Number(raw.cache_creation_input_tokens || 0);
|
|
169
|
+
const cacheWrite1h = Number(raw.cache_creation?.ephemeral_1h_input_tokens || 0);
|
|
170
|
+
const output = Number(raw.output_tokens || 0);
|
|
171
|
+
return {
|
|
172
|
+
input,
|
|
173
|
+
cached,
|
|
174
|
+
cacheWrite,
|
|
175
|
+
cacheWrite1h: Math.min(cacheWrite1h, cacheWrite),
|
|
176
|
+
output,
|
|
177
|
+
reasoning: 0,
|
|
178
|
+
total: input + cached + cacheWrite + output,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function addUsage(target, usage) {
|
|
183
|
+
target.input += usage.input;
|
|
184
|
+
target.cached += usage.cached;
|
|
185
|
+
target.cacheWrite += usage.cacheWrite;
|
|
186
|
+
target.cacheWrite1h += usage.cacheWrite1h;
|
|
187
|
+
target.output += usage.output;
|
|
188
|
+
target.reasoning += usage.reasoning;
|
|
189
|
+
target.total += usage.total;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function normalizeModelName(model) {
|
|
193
|
+
const clean = String(model || 'unknown').trim() || 'unknown';
|
|
194
|
+
const lower = clean.toLowerCase();
|
|
195
|
+
if (MODEL_ALIASES[lower]) return MODEL_ALIASES[lower];
|
|
196
|
+
// Fallback: remove sufixo de data (ex.: claude-opus-4-8-20260528) e tenta de novo.
|
|
197
|
+
const noDate = lower.replace(/-\d{8}$/, '');
|
|
198
|
+
return MODEL_ALIASES[noDate] || clean;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function normalizeProvider(provider) {
|
|
202
|
+
return String(provider || 'unknown').trim() || 'unknown';
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function calculateCost(usage, price) {
|
|
206
|
+
const inputCost = (usage.input / 1_000_000) * price.input;
|
|
207
|
+
const cachedCost = (usage.cached / 1_000_000) * price.cachedInput;
|
|
208
|
+
const write1h = usage.cacheWrite1h;
|
|
209
|
+
const write5m = Math.max(usage.cacheWrite - write1h, 0);
|
|
210
|
+
const writeCost = ((write5m * 1.25 + write1h * 2) / 1_000_000) * price.input;
|
|
211
|
+
const outputCost = (usage.output / 1_000_000) * price.output;
|
|
212
|
+
return roundUsd(inputCost + cachedCost + writeCost + outputCost);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function roundUsd(value) {
|
|
216
|
+
return Math.round(Number(value || 0) * 10000) / 10000;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Resolve o nome do modelo (com aliases/sufixo de data) para a tabela de preços.
|
|
220
|
+
// Devolve null quando o modelo não está tabelado.
|
|
221
|
+
export function priceForModel(model) {
|
|
222
|
+
return PRICE_REFERENCE[normalizeModelName(model)] || null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Custo USD por tipo de uso (mesmos multiplicadores de cache do calculateCost).
|
|
226
|
+
// null quando o preço do modelo é desconhecido.
|
|
227
|
+
export function costBreakdown(usage, price) {
|
|
228
|
+
if (!price) return null;
|
|
229
|
+
const u = usage || {};
|
|
230
|
+
const write1h = u.cacheWrite1h || 0;
|
|
231
|
+
const write5m = Math.max((u.cacheWrite || 0) - write1h, 0);
|
|
232
|
+
const input = (u.input / 1_000_000) * price.input;
|
|
233
|
+
const cached = (u.cached / 1_000_000) * price.cachedInput;
|
|
234
|
+
const cacheWrite = ((write5m * 1.25 + write1h * 2) / 1_000_000) * price.input;
|
|
235
|
+
const output = (u.output / 1_000_000) * price.output;
|
|
236
|
+
return { input, cached, cacheWrite, output, total: input + cached + cacheWrite + output };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function escapeTableCell(value) {
|
|
240
|
+
return String(value ?? '')
|
|
241
|
+
.replace(/\r?\n/g, ' ')
|
|
242
|
+
.replace(/\|/g, '\\|')
|
|
243
|
+
.trim();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Formata inteiros com separador de milhar pt-BR (3656657 -> 3.656.657).
|
|
247
|
+
function fmtNum(value) {
|
|
248
|
+
const n = Math.trunc(Number(value) || 0);
|
|
249
|
+
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, '.');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function addUnique(list, value) {
|
|
253
|
+
const clean = String(value || '').trim();
|
|
254
|
+
if (clean && !list.includes(clean)) list.push(clean);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function parseJsonLine(line) {
|
|
258
|
+
try {
|
|
259
|
+
return JSON.parse(line);
|
|
260
|
+
} catch {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function extractTextContent(content) {
|
|
266
|
+
if (typeof content === 'string') return content;
|
|
267
|
+
if (!Array.isArray(content)) return '';
|
|
268
|
+
return content
|
|
269
|
+
.map((item) => item?.text || item?.input_text || item?.output_text || '')
|
|
270
|
+
.filter(Boolean)
|
|
271
|
+
.join('\n');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function shouldIgnoreUserText(text) {
|
|
275
|
+
return /^# AGENTS\.md instructions/.test(text)
|
|
276
|
+
|| text.startsWith('<environment_context>')
|
|
277
|
+
|| text.startsWith('<permissions instructions>')
|
|
278
|
+
|| text.startsWith('<system-reminder>')
|
|
279
|
+
|| text.startsWith('<local-command-caveat>')
|
|
280
|
+
|| text.startsWith('<command-name>')
|
|
281
|
+
|| text.startsWith('<ide_')
|
|
282
|
+
|| text.startsWith('## Memory')
|
|
283
|
+
|| text.includes('You are Codex, a coding agent');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function emptyParseResult(transcriptPath) {
|
|
287
|
+
return {
|
|
288
|
+
transcriptPath,
|
|
289
|
+
sessionId: '',
|
|
290
|
+
provider: 'unknown',
|
|
291
|
+
model: 'unknown',
|
|
292
|
+
pensamento: '',
|
|
293
|
+
userPrompts: [],
|
|
294
|
+
tools: [],
|
|
295
|
+
toolCalls: 0,
|
|
296
|
+
calls: [],
|
|
297
|
+
byModel: new Map(),
|
|
298
|
+
totals: emptyTokenUsage(),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function trackByModel(result, provider, model, usage) {
|
|
303
|
+
const key = `${provider}:${model}`;
|
|
304
|
+
if (!result.byModel.has(key)) {
|
|
305
|
+
result.byModel.set(key, { model, provider, calls: 0, usage: emptyTokenUsage() });
|
|
306
|
+
}
|
|
307
|
+
const entry = result.byModel.get(key);
|
|
308
|
+
entry.calls += 1;
|
|
309
|
+
addUsage(entry.usage, usage);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function parseCodexLines(lines, result) {
|
|
313
|
+
let currentProvider = 'unknown';
|
|
314
|
+
let currentModel = 'unknown';
|
|
315
|
+
let latestPrompt = '';
|
|
316
|
+
|
|
317
|
+
for (const line of lines) {
|
|
318
|
+
const event = parseJsonLine(line);
|
|
319
|
+
if (!event) continue;
|
|
320
|
+
|
|
321
|
+
const payload = event.payload || {};
|
|
322
|
+
|
|
323
|
+
if (event.type === 'session_meta') {
|
|
324
|
+
result.sessionId = payload.id || result.sessionId;
|
|
325
|
+
currentProvider = normalizeProvider(payload.model_provider || currentProvider);
|
|
326
|
+
currentModel = normalizeModelName(payload.model || currentModel);
|
|
327
|
+
result.provider = currentProvider;
|
|
328
|
+
result.model = currentModel;
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (event.type === 'turn_context') {
|
|
333
|
+
currentProvider = normalizeProvider(payload.model_provider || currentProvider);
|
|
334
|
+
currentModel = normalizeModelName(payload.model || currentModel);
|
|
335
|
+
result.provider = currentProvider;
|
|
336
|
+
result.model = currentModel;
|
|
337
|
+
const effort = payload.effort || payload.reasoning_effort
|
|
338
|
+
|| payload.collaboration_mode?.settings?.reasoning_effort || '';
|
|
339
|
+
if (effort) result.pensamento = String(effort);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (event.type === 'event_msg' && payload.type === 'user_message') {
|
|
344
|
+
const text = String(payload.message || '').trim();
|
|
345
|
+
if (text && !shouldIgnoreUserText(text)) {
|
|
346
|
+
latestPrompt = text;
|
|
347
|
+
addUnique(result.userPrompts, text);
|
|
348
|
+
}
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (event.type === 'response_item' && payload.type === 'message' && payload.role === 'user') {
|
|
353
|
+
const text = extractTextContent(payload.content).trim();
|
|
354
|
+
if (text && !shouldIgnoreUserText(text)) {
|
|
355
|
+
latestPrompt = text;
|
|
356
|
+
addUnique(result.userPrompts, text);
|
|
357
|
+
}
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (event.type === 'response_item' && payload.type === 'function_call') {
|
|
362
|
+
result.toolCalls += 1;
|
|
363
|
+
addUnique(result.tools, payload.name || 'function_call');
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (event.type === 'response_item' && payload.type === 'tool_search_call') {
|
|
368
|
+
result.toolCalls += 1;
|
|
369
|
+
addUnique(result.tools, 'tool_search');
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (event.type === 'response_item' && payload.type === 'web_search_call') {
|
|
374
|
+
result.toolCalls += 1;
|
|
375
|
+
addUnique(result.tools, 'web_search');
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (event.type !== 'event_msg' || payload.type !== 'token_count') continue;
|
|
380
|
+
|
|
381
|
+
const info = payload.info || {};
|
|
382
|
+
const rawUsage = info.last_token_usage;
|
|
383
|
+
if (!rawUsage) continue;
|
|
384
|
+
|
|
385
|
+
const usage = normalizeCodexUsage(rawUsage);
|
|
386
|
+
const model = normalizeModelName(info.model || payload.model || currentModel);
|
|
387
|
+
const provider = normalizeProvider(info.model_provider || payload.model_provider || currentProvider);
|
|
388
|
+
|
|
389
|
+
result.calls.push({
|
|
390
|
+
index: result.calls.length + 1,
|
|
391
|
+
model,
|
|
392
|
+
provider,
|
|
393
|
+
usage,
|
|
394
|
+
prompt: truncate(latestPrompt, 110),
|
|
395
|
+
});
|
|
396
|
+
addUsage(result.totals, usage);
|
|
397
|
+
trackByModel(result, provider, model, usage);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return result;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Transcript Claude Code: uma linha "assistant" por content block, repetindo o MESMO
|
|
404
|
+
// requestId/message.id e a MESMA usage — dedupe obrigatório para não multiplicar tokens.
|
|
405
|
+
function parseClaudeLines(lines, result) {
|
|
406
|
+
result.provider = 'anthropic';
|
|
407
|
+
const seenUsage = new Set();
|
|
408
|
+
const seenTools = new Set();
|
|
409
|
+
const seenThinking = new Set();
|
|
410
|
+
let thinkingChars = 0;
|
|
411
|
+
let latestPrompt = '';
|
|
412
|
+
|
|
413
|
+
for (const line of lines) {
|
|
414
|
+
const event = parseJsonLine(line);
|
|
415
|
+
if (!event) continue;
|
|
416
|
+
|
|
417
|
+
if (event.sessionId && !result.sessionId) result.sessionId = event.sessionId;
|
|
418
|
+
|
|
419
|
+
if (event.type === 'user' && !event.toolUseResult && event.message) {
|
|
420
|
+
const text = extractTextContent(event.message.content).trim();
|
|
421
|
+
if (text && !shouldIgnoreUserText(text)) {
|
|
422
|
+
latestPrompt = text;
|
|
423
|
+
addUnique(result.userPrompts, text);
|
|
424
|
+
}
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (event.type !== 'assistant' || !event.message) continue;
|
|
429
|
+
|
|
430
|
+
const msg = event.message;
|
|
431
|
+
const model = normalizeModelName(msg.model);
|
|
432
|
+
if (model === '<synthetic>' || msg.model === '<synthetic>') continue;
|
|
433
|
+
|
|
434
|
+
for (const block of msg.content || []) {
|
|
435
|
+
if (block?.type === 'tool_use' && block.id && !seenTools.has(block.id)) {
|
|
436
|
+
seenTools.add(block.id);
|
|
437
|
+
result.toolCalls += 1;
|
|
438
|
+
addUnique(result.tools, block.name || 'tool_use');
|
|
439
|
+
}
|
|
440
|
+
if (block?.type === 'thinking' && block.thinking) {
|
|
441
|
+
const thinkKey = `${msg.id || ''}:${block.thinking.slice(0, 60)}`;
|
|
442
|
+
if (!seenThinking.has(thinkKey)) {
|
|
443
|
+
seenThinking.add(thinkKey);
|
|
444
|
+
thinkingChars += block.thinking.length;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const usageKey = event.requestId || msg.id || '';
|
|
450
|
+
if (!msg.usage || !usageKey || seenUsage.has(usageKey)) continue;
|
|
451
|
+
seenUsage.add(usageKey);
|
|
452
|
+
|
|
453
|
+
const usage = normalizeClaudeUsage(msg.usage);
|
|
454
|
+
result.calls.push({
|
|
455
|
+
index: result.calls.length + 1,
|
|
456
|
+
model,
|
|
457
|
+
provider: 'anthropic',
|
|
458
|
+
usage,
|
|
459
|
+
prompt: truncate(latestPrompt, 110),
|
|
460
|
+
});
|
|
461
|
+
addUsage(result.totals, usage);
|
|
462
|
+
trackByModel(result, 'anthropic', model, usage);
|
|
463
|
+
result.model = model;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Thinking estimado: ~3,5 chars por token. Distribuído no total como informação à parte
|
|
467
|
+
// (já contido em output_tokens — não somar de novo).
|
|
468
|
+
const thinkingTokens = Math.round(thinkingChars / 3.5);
|
|
469
|
+
if (thinkingTokens > 0) {
|
|
470
|
+
result.totals.reasoning = thinkingTokens;
|
|
471
|
+
result.pensamento = `thinking ~${formatTokensShort(thinkingTokens)}`;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return result;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function formatTokensShort(n) {
|
|
478
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
479
|
+
if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
|
|
480
|
+
return String(n);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function detectTranscriptFormat(lines) {
|
|
484
|
+
for (const line of lines.slice(0, 20)) {
|
|
485
|
+
const event = parseJsonLine(line);
|
|
486
|
+
if (!event) continue;
|
|
487
|
+
if (event.type === 'session_meta' || event.type === 'turn_context'
|
|
488
|
+
|| event.type === 'event_msg' || event.type === 'response_item') return 'codex';
|
|
489
|
+
if (event.type === 'assistant' || event.type === 'queue-operation'
|
|
490
|
+
|| event.type === 'file-history-snapshot' || event.type === 'last-prompt') return 'claude';
|
|
491
|
+
}
|
|
492
|
+
return 'codex';
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
export function parseTokenUsageFromTranscript(transcriptPath) {
|
|
496
|
+
const result = emptyParseResult(transcriptPath);
|
|
497
|
+
if (!transcriptPath || !existsSync(transcriptPath)) return result;
|
|
498
|
+
|
|
499
|
+
const lines = readFileSync(transcriptPath, 'utf-8').split('\n').filter(Boolean);
|
|
500
|
+
return detectTranscriptFormat(lines) === 'claude'
|
|
501
|
+
? parseClaudeLines(lines, result)
|
|
502
|
+
: parseCodexLines(lines, result);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function modelCost(usage, model) {
|
|
506
|
+
const normalized = normalizeModelName(model);
|
|
507
|
+
const price = PRICE_REFERENCE[normalized];
|
|
508
|
+
return price ? calculateCost(usage, price) : 0;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export function summarizeTokenUsage(parsed) {
|
|
512
|
+
const totals = parsed.totals;
|
|
513
|
+
const modelRows = [...parsed.byModel.values()].map((entry) => ({
|
|
514
|
+
...entry,
|
|
515
|
+
costs: {
|
|
516
|
+
model: modelCost(entry.usage, entry.model),
|
|
517
|
+
},
|
|
518
|
+
}));
|
|
519
|
+
const modelCostTotal = roundUsd(modelRows.reduce((sum, row) => sum + row.costs.model, 0));
|
|
520
|
+
const modelLabel = modelRows.length === 1 ? modelRows[0].model : parsed.model;
|
|
521
|
+
|
|
522
|
+
return {
|
|
523
|
+
sessionId: parsed.sessionId,
|
|
524
|
+
transcriptPath: parsed.transcriptPath,
|
|
525
|
+
pensamento: parsed.pensamento || '',
|
|
526
|
+
prompts: parsed.userPrompts.length,
|
|
527
|
+
toolCalls: parsed.toolCalls,
|
|
528
|
+
tools: parsed.tools,
|
|
529
|
+
calls: parsed.calls.length,
|
|
530
|
+
models: [...new Set(modelRows.map((row) => row.model))],
|
|
531
|
+
providers: [...new Set(modelRows.map((row) => row.provider))],
|
|
532
|
+
totals,
|
|
533
|
+
costs: {
|
|
534
|
+
model: modelCostTotal,
|
|
535
|
+
modelLabel,
|
|
536
|
+
},
|
|
537
|
+
modelRows,
|
|
538
|
+
callsTable: parsed.calls,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// ---------------------------------------------------------------------------
|
|
543
|
+
// Histórico por transcript (reaberturas): cada conversa tem transcript próprio.
|
|
544
|
+
// O frontmatter guarda uma entrada por transcript; os campos planos são a soma.
|
|
545
|
+
// ---------------------------------------------------------------------------
|
|
546
|
+
|
|
547
|
+
function transcriptIdFromPath(transcriptPath) {
|
|
548
|
+
return basename(String(transcriptPath || '')).replace(/\.jsonl?$/i, '') || 'desconhecido';
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function entryFromSummary(summary, transcriptId) {
|
|
552
|
+
return {
|
|
553
|
+
transcript_id: transcriptId,
|
|
554
|
+
provider: summary.providers.join(' + ') || 'unknown',
|
|
555
|
+
modelos: summary.models,
|
|
556
|
+
pensamento: summary.pensamento || '',
|
|
557
|
+
input: summary.totals.input,
|
|
558
|
+
cache_write: summary.totals.cacheWrite,
|
|
559
|
+
cache_read: summary.totals.cached,
|
|
560
|
+
output: summary.totals.output,
|
|
561
|
+
reasoning: summary.totals.reasoning,
|
|
562
|
+
total: summary.totals.total,
|
|
563
|
+
custo_usd: summary.costs.model,
|
|
564
|
+
prompts: summary.prompts,
|
|
565
|
+
tool_calls: summary.toolCalls,
|
|
566
|
+
chamadas_llm: summary.calls,
|
|
567
|
+
tools: summary.tools,
|
|
568
|
+
atualizado_em: new Date().toISOString().slice(0, 19),
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Parser do bloco YAML `usage_por_transcript` gerado por este próprio script (formato fixo).
|
|
573
|
+
function parseUsageHistory(frontmatter) {
|
|
574
|
+
const match = frontmatter.match(/^usage_por_transcript:\n((?:[ ]{2,}.*\n?)*)/m);
|
|
575
|
+
if (!match) return [];
|
|
576
|
+
|
|
577
|
+
const entries = [];
|
|
578
|
+
let current = null;
|
|
579
|
+
for (const rawLine of match[1].split('\n')) {
|
|
580
|
+
const line = rawLine.trimEnd();
|
|
581
|
+
if (!line.trim()) continue;
|
|
582
|
+
const itemStart = line.match(/^[ ]{2}- transcript_id:\s*(.*)$/);
|
|
583
|
+
if (itemStart) {
|
|
584
|
+
current = { transcript_id: stripYamlScalar(itemStart[1]), modelos: [], tools: [] };
|
|
585
|
+
entries.push(current);
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
if (!current) continue;
|
|
589
|
+
const listItem = line.match(/^[ ]{6}- (.*)$/);
|
|
590
|
+
if (listItem && current._listKey) {
|
|
591
|
+
current[current._listKey].push(stripYamlScalar(listItem[1]));
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
const kv = line.match(/^[ ]{4}([a-z_]+):\s*(.*)$/);
|
|
595
|
+
if (!kv) continue;
|
|
596
|
+
const [, key, value] = kv;
|
|
597
|
+
if (key === 'modelos' || key === 'tools') {
|
|
598
|
+
current._listKey = key;
|
|
599
|
+
current[key] = [];
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
current._listKey = null;
|
|
603
|
+
current[key] = /^-?\d+(\.\d+)?$/.test(value) ? Number(value) : stripYamlScalar(value);
|
|
604
|
+
}
|
|
605
|
+
return entries.map(({ _listKey, ...entry }) => entry);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function stripYamlScalar(value) {
|
|
609
|
+
return String(value || '').trim().replace(/^["']|["']$/g, '');
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function aggregateEntries(entries) {
|
|
613
|
+
const agg = {
|
|
614
|
+
input: 0, cacheWrite: 0, cached: 0, output: 0, reasoning: 0, total: 0,
|
|
615
|
+
custo: 0, prompts: 0, toolCalls: 0, calls: 0,
|
|
616
|
+
models: [], providers: [], pensamentos: [], tools: [],
|
|
617
|
+
};
|
|
618
|
+
for (const e of entries) {
|
|
619
|
+
agg.input += Number(e.input || 0);
|
|
620
|
+
agg.cacheWrite += Number(e.cache_write || 0);
|
|
621
|
+
agg.cached += Number(e.cache_read || 0);
|
|
622
|
+
agg.output += Number(e.output || 0);
|
|
623
|
+
agg.reasoning += Number(e.reasoning || 0);
|
|
624
|
+
agg.total += Number(e.total || 0);
|
|
625
|
+
agg.custo = roundUsd(agg.custo + Number(e.custo_usd || 0));
|
|
626
|
+
agg.prompts += Number(e.prompts || 0);
|
|
627
|
+
agg.toolCalls += Number(e.tool_calls || 0);
|
|
628
|
+
agg.calls += Number(e.chamadas_llm || 0);
|
|
629
|
+
for (const m of e.modelos || []) addUnique(agg.models, m);
|
|
630
|
+
addUnique(agg.providers, e.provider);
|
|
631
|
+
if (e.pensamento) addUnique(agg.pensamentos, e.pensamento);
|
|
632
|
+
for (const t of e.tools || []) addUnique(agg.tools, t);
|
|
633
|
+
}
|
|
634
|
+
return agg;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function extractFrontmatterValue(content, key) {
|
|
638
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
639
|
+
if (!match) return '';
|
|
640
|
+
const line = match[1].match(new RegExp(`^${key}:\\s*(.*)$`, 'm'));
|
|
641
|
+
return line ? line[1].trim().replace(/^["']|["']$/g, '') : '';
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function stripManagedFrontmatter(frontmatter) {
|
|
645
|
+
const lines = frontmatter.split('\n');
|
|
646
|
+
const kept = [];
|
|
647
|
+
let skipping = false;
|
|
648
|
+
|
|
649
|
+
for (const line of lines) {
|
|
650
|
+
const root = line.match(/^([A-Za-z0-9_]+):/);
|
|
651
|
+
if (root) {
|
|
652
|
+
skipping = MANAGED_FRONTMATTER_KEYS.has(root[1]);
|
|
653
|
+
if (!skipping) kept.push(line);
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (!skipping) kept.push(line);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
return kept.join('\n').trimEnd();
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function yamlList(key, values, indent = '') {
|
|
664
|
+
if (!values.length) return `${indent}${key}: []`;
|
|
665
|
+
return `${indent}${key}:\n${values.map((value) => `${indent} - "${String(value).replace(/"/g, '\\"')}"`).join('\n')}`;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function buildUsageFrontmatter(agg, entries) {
|
|
669
|
+
const model = agg.models.length === 1 ? agg.models[0] : agg.models.join(' + ');
|
|
670
|
+
const provider = agg.providers.length === 1 ? agg.providers[0] : agg.providers.join(' + ');
|
|
671
|
+
|
|
672
|
+
const historyYaml = entries.length
|
|
673
|
+
? [
|
|
674
|
+
'usage_por_transcript:',
|
|
675
|
+
...entries.flatMap((e) => [
|
|
676
|
+
` - transcript_id: "${e.transcript_id}"`,
|
|
677
|
+
` provider: "${e.provider || 'unknown'}"`,
|
|
678
|
+
yamlList('modelos', e.modelos || [], ' '),
|
|
679
|
+
` pensamento: "${e.pensamento || ''}"`,
|
|
680
|
+
` input: ${e.input || 0}`,
|
|
681
|
+
` cache_write: ${e.cache_write || 0}`,
|
|
682
|
+
` cache_read: ${e.cache_read || 0}`,
|
|
683
|
+
` output: ${e.output || 0}`,
|
|
684
|
+
` reasoning: ${e.reasoning || 0}`,
|
|
685
|
+
` total: ${e.total || 0}`,
|
|
686
|
+
` custo_usd: ${e.custo_usd || 0}`,
|
|
687
|
+
` prompts: ${e.prompts || 0}`,
|
|
688
|
+
` tool_calls: ${e.tool_calls || 0}`,
|
|
689
|
+
` chamadas_llm: ${e.chamadas_llm || 0}`,
|
|
690
|
+
yamlList('tools', e.tools || [], ' '),
|
|
691
|
+
` atualizado_em: "${e.atualizado_em || ''}"`,
|
|
692
|
+
]),
|
|
693
|
+
].join('\n')
|
|
694
|
+
: 'usage_por_transcript: []';
|
|
695
|
+
|
|
696
|
+
return [
|
|
697
|
+
`modelo: "${model || 'unknown'}"`,
|
|
698
|
+
yamlList('modelos', agg.models),
|
|
699
|
+
`provedor_modelo: "${provider || 'unknown'}"`,
|
|
700
|
+
yamlList('provedores_modelo', agg.providers),
|
|
701
|
+
`nivel_pensamento: "${agg.pensamentos.join(' + ')}"`,
|
|
702
|
+
`prompts: ${agg.prompts}`,
|
|
703
|
+
`tool_calls: ${agg.toolCalls}`,
|
|
704
|
+
`tools_distinct: ${agg.tools.length}`,
|
|
705
|
+
yamlList('tools', agg.tools),
|
|
706
|
+
`chamadas_llm: ${agg.calls}`,
|
|
707
|
+
`tokens_input: ${agg.input}`,
|
|
708
|
+
`tokens_cache_write: ${agg.cacheWrite}`,
|
|
709
|
+
`tokens_cached_input: ${agg.cached}`,
|
|
710
|
+
`tokens_output: ${agg.output}`,
|
|
711
|
+
`tokens_reasoning: ${agg.reasoning}`,
|
|
712
|
+
`tokens_total: ${agg.total}`,
|
|
713
|
+
`custo_modelo_label: "${model || 'unknown'}"`,
|
|
714
|
+
`custo_modelo_usd: ${agg.custo}`,
|
|
715
|
+
historyYaml,
|
|
716
|
+
].join('\n');
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function upsertSessionFrontmatter(content, agg, entries) {
|
|
720
|
+
const managedYaml = buildUsageFrontmatter(agg, entries);
|
|
721
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
722
|
+
if (!match) return `---\n${managedYaml}\n---\n\n${content}`;
|
|
723
|
+
|
|
724
|
+
const clean = stripManagedFrontmatter(match[1]);
|
|
725
|
+
const nextFrontmatter = [clean, managedYaml].filter(Boolean).join('\n');
|
|
726
|
+
return `---\n${nextFrontmatter}\n---${content.slice(match[0].length)}`;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function buildModelTable(summary) {
|
|
730
|
+
if (!summary.modelRows.length) return 'Nenhum modelo registrado.';
|
|
731
|
+
|
|
732
|
+
return [
|
|
733
|
+
'| Modelo | Provider | Chamadas | Input | Cache W | Cache R | Output | Reasoning | Total | Custo |',
|
|
734
|
+
'|---|---|---:|---:|---:|---:|---:|---:|---:|---:|',
|
|
735
|
+
...summary.modelRows.map((row) => [
|
|
736
|
+
`| ${escapeTableCell(row.model)}`,
|
|
737
|
+
escapeTableCell(row.provider),
|
|
738
|
+
fmtNum(row.calls),
|
|
739
|
+
fmtNum(row.usage.input),
|
|
740
|
+
fmtNum(row.usage.cacheWrite),
|
|
741
|
+
fmtNum(row.usage.cached),
|
|
742
|
+
fmtNum(row.usage.output),
|
|
743
|
+
fmtNum(row.usage.reasoning),
|
|
744
|
+
fmtNum(row.usage.total),
|
|
745
|
+
`$${row.costs.model.toFixed(4)} |`,
|
|
746
|
+
].join(' | ')),
|
|
747
|
+
].join('\n');
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function buildHistoryTable(entries) {
|
|
751
|
+
if (!entries.length) return 'Nenhuma reabertura registrada.';
|
|
752
|
+
|
|
753
|
+
return [
|
|
754
|
+
'| Transcript | Modelo(s) | Pensamento | Input | Cache W | Cache R | Output | Total | Custo | Atualizado |',
|
|
755
|
+
'|---|---|---|---:|---:|---:|---:|---:|---:|---|',
|
|
756
|
+
...entries.map((e) => [
|
|
757
|
+
`| ${escapeTableCell(String(e.transcript_id).slice(0, 12))}…`,
|
|
758
|
+
escapeTableCell((e.modelos || []).join(' + ')),
|
|
759
|
+
escapeTableCell(e.pensamento || '-'),
|
|
760
|
+
fmtNum(e.input),
|
|
761
|
+
fmtNum(e.cache_write),
|
|
762
|
+
fmtNum(e.cache_read),
|
|
763
|
+
fmtNum(e.output),
|
|
764
|
+
fmtNum(e.total),
|
|
765
|
+
`$${Number(e.custo_usd || 0).toFixed(4)}`,
|
|
766
|
+
`${escapeTableCell(e.atualizado_em || '')} |`,
|
|
767
|
+
].join(' | ')),
|
|
768
|
+
].join('\n');
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function buildUsageSection(agg, entries, summary) {
|
|
772
|
+
return `## Uso de tokens e custos
|
|
773
|
+
|
|
774
|
+
> Estimativa API-equivalente baseada nos transcripts locais (Codex/Claude Code). Não representa cobrança real do plano.
|
|
775
|
+
|
|
776
|
+
| Métrica | Valor |
|
|
777
|
+
|---|---:|
|
|
778
|
+
| Prompts | ${agg.prompts} |
|
|
779
|
+
| Ferramentas | ${agg.tools.length} tools / ${agg.toolCalls} calls |
|
|
780
|
+
| Chamadas com uso | ${fmtNum(agg.calls)} |
|
|
781
|
+
| Input tokens (não cacheados) | ${fmtNum(agg.input)} |
|
|
782
|
+
| Cache write tokens | ${fmtNum(agg.cacheWrite)} |
|
|
783
|
+
| Cache read tokens | ${fmtNum(agg.cached)} |
|
|
784
|
+
| Output tokens | ${fmtNum(agg.output)} |
|
|
785
|
+
| Thinking/reasoning tokens | ${fmtNum(agg.reasoning)} |
|
|
786
|
+
| Total tokens | ${fmtNum(agg.total)} |
|
|
787
|
+
| Modelo(s) | ${agg.models.join(' + ') || 'unknown'} |
|
|
788
|
+
| Nível de pensamento | ${agg.pensamentos.join(' + ') || '-'} |
|
|
789
|
+
| Custo estimado | $${agg.custo.toFixed(4)} |
|
|
790
|
+
|
|
791
|
+
### Por reabertura
|
|
792
|
+
|
|
793
|
+
${buildHistoryTable(entries)}
|
|
794
|
+
|
|
795
|
+
### Por modelo (transcript atual)
|
|
796
|
+
|
|
797
|
+
${buildModelTable(summary)}
|
|
798
|
+
`;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
export function upsertUsageSection(content, section) {
|
|
802
|
+
// Normaliza espaçamento nos dois caminhos (inserção e substituição) para manter idempotência.
|
|
803
|
+
const assemble = (head, rest) => (rest
|
|
804
|
+
? `${head.trimEnd()}\n\n${section.trimEnd()}\n\n${rest.trimStart()}`
|
|
805
|
+
: `${head.trimEnd()}\n\n${section.trimEnd()}\n`);
|
|
806
|
+
|
|
807
|
+
const marker = '\n## Uso de tokens e custos';
|
|
808
|
+
const existing = content.indexOf(marker);
|
|
809
|
+
let base = content;
|
|
810
|
+
|
|
811
|
+
if (existing !== -1) {
|
|
812
|
+
const next = content.indexOf('\n## ', existing + marker.length);
|
|
813
|
+
const usageTail = next === -1 ? content.slice(existing) : content.slice(existing, next);
|
|
814
|
+
const orphanIteration = usageTail.search(/\n### \d{2}:\d{2} - /);
|
|
815
|
+
const preservedTail = orphanIteration === -1 ? '' : usageTail.slice(orphanIteration).trim();
|
|
816
|
+
const rest = next === -1 ? '' : content.slice(next).trimStart();
|
|
817
|
+
base = [
|
|
818
|
+
content.slice(0, existing).trimEnd(),
|
|
819
|
+
preservedTail,
|
|
820
|
+
rest,
|
|
821
|
+
].filter(Boolean).join('\n\n');
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const anchor = base.includes('\n## Pendências')
|
|
825
|
+
? '\n## Pendências'
|
|
826
|
+
: '\n## Encerramento';
|
|
827
|
+
const anchorIndex = base.indexOf(anchor);
|
|
828
|
+
if (anchorIndex === -1) return assemble(base, '');
|
|
829
|
+
return assemble(base.slice(0, anchorIndex), base.slice(anchorIndex));
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// Migração: nota antiga com totais planos mas sem usage_por_transcript.
|
|
833
|
+
// Preserva como entrada "legado", exceto quando o transcript atual parece ser a
|
|
834
|
+
// mesma conversa que gerou os totais (mesmos modelos) — aí descarta para não duplicar.
|
|
835
|
+
function legacyEntryFromNote(content, summary) {
|
|
836
|
+
const total = Number(extractFrontmatterValue(content, 'tokens_total') || 0);
|
|
837
|
+
if (!total) return null;
|
|
838
|
+
|
|
839
|
+
const legacyModel = extractFrontmatterValue(content, 'modelo');
|
|
840
|
+
const currentModels = summary.models.join(' + ');
|
|
841
|
+
if (legacyModel && legacyModel === currentModels) return null;
|
|
842
|
+
|
|
843
|
+
return {
|
|
844
|
+
transcript_id: 'legado',
|
|
845
|
+
provider: extractFrontmatterValue(content, 'provedor_modelo') || 'unknown',
|
|
846
|
+
modelos: legacyModel ? [legacyModel] : [],
|
|
847
|
+
pensamento: '',
|
|
848
|
+
input: Number(extractFrontmatterValue(content, 'tokens_input') || 0),
|
|
849
|
+
cache_write: 0,
|
|
850
|
+
cache_read: Number(extractFrontmatterValue(content, 'tokens_cached_input') || 0),
|
|
851
|
+
output: Number(extractFrontmatterValue(content, 'tokens_output') || 0),
|
|
852
|
+
reasoning: Number(extractFrontmatterValue(content, 'tokens_reasoning') || 0),
|
|
853
|
+
total,
|
|
854
|
+
custo_usd: Number(extractFrontmatterValue(content, 'custo_modelo_usd') || 0),
|
|
855
|
+
prompts: Number(extractFrontmatterValue(content, 'prompts') || 0),
|
|
856
|
+
tool_calls: Number(extractFrontmatterValue(content, 'tool_calls') || 0),
|
|
857
|
+
chamadas_llm: Number(extractFrontmatterValue(content, 'chamadas_llm') || 0),
|
|
858
|
+
tools: [],
|
|
859
|
+
atualizado_em: '',
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
export function updateSessionUsage({ vaultBase, sessionRel, sessionPath, transcriptPath }) {
|
|
864
|
+
if (!sessionPath || !existsSync(sessionPath) || !transcriptPath || !existsSync(transcriptPath)) {
|
|
865
|
+
return null;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const parsed = parseTokenUsageFromTranscript(transcriptPath);
|
|
869
|
+
const summary = summarizeTokenUsage(parsed);
|
|
870
|
+
if (!summary.calls) return null;
|
|
871
|
+
|
|
872
|
+
const sessionContent = readFileSync(sessionPath, 'utf-8');
|
|
873
|
+
const fmMatch = sessionContent.match(/^---\n([\s\S]*?)\n---/);
|
|
874
|
+
const existingEntries = fmMatch ? parseUsageHistory(fmMatch[1]) : [];
|
|
875
|
+
|
|
876
|
+
const transcriptId = transcriptIdFromPath(transcriptPath);
|
|
877
|
+
let entries = existingEntries.filter((e) => e.transcript_id !== transcriptId);
|
|
878
|
+
|
|
879
|
+
if (!existingEntries.length) {
|
|
880
|
+
const legacy = legacyEntryFromNote(sessionContent, summary);
|
|
881
|
+
if (legacy) entries.push(legacy);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
entries.push(entryFromSummary(summary, transcriptId));
|
|
885
|
+
|
|
886
|
+
const agg = aggregateEntries(entries);
|
|
887
|
+
const withFrontmatter = upsertSessionFrontmatter(sessionContent, agg, entries);
|
|
888
|
+
const withSection = upsertUsageSection(withFrontmatter, buildUsageSection(agg, entries, summary));
|
|
889
|
+
writeFileSync(sessionPath, withSection, 'utf-8');
|
|
890
|
+
|
|
891
|
+
return {
|
|
892
|
+
summary,
|
|
893
|
+
aggregate: agg,
|
|
894
|
+
entries,
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function parseCliArgs(argv) {
|
|
899
|
+
const args = {};
|
|
900
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
901
|
+
const item = argv[i];
|
|
902
|
+
if (!item.startsWith('--')) continue;
|
|
903
|
+
const key = item.slice(2);
|
|
904
|
+
const next = argv[i + 1];
|
|
905
|
+
if (!next || next.startsWith('--')) {
|
|
906
|
+
args[key] = true;
|
|
907
|
+
} else {
|
|
908
|
+
args[key] = next;
|
|
909
|
+
i += 1;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return args;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function runCli() {
|
|
916
|
+
const args = parseCliArgs(process.argv.slice(2));
|
|
917
|
+
const vaultBase = getVaultBase({ obsidian_vault_path: args.vault });
|
|
918
|
+
const control = readControl(vaultBase);
|
|
919
|
+
const sessionRel = args.session || control.session_file || control.last_session_file || '';
|
|
920
|
+
const sessionPath = sessionRel ? join(vaultBase, sessionRel) : '';
|
|
921
|
+
const transcriptPath = args.transcript || '';
|
|
922
|
+
|
|
923
|
+
const result = updateSessionUsage({ vaultBase, sessionRel, sessionPath, transcriptPath });
|
|
924
|
+
if (!result) {
|
|
925
|
+
console.log(JSON.stringify({ ok: false, reason: 'usage-not-available' }, null, 2));
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
console.log(JSON.stringify({
|
|
930
|
+
ok: true,
|
|
931
|
+
session: sessionRel,
|
|
932
|
+
calls: result.summary.calls,
|
|
933
|
+
prompts: result.summary.prompts,
|
|
934
|
+
toolCalls: result.summary.toolCalls,
|
|
935
|
+
models: result.summary.models,
|
|
936
|
+
pensamento: result.summary.pensamento,
|
|
937
|
+
totals: result.summary.totals,
|
|
938
|
+
costs: result.summary.costs,
|
|
939
|
+
reaberturas: result.entries.length,
|
|
940
|
+
}, null, 2));
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
944
|
+
try {
|
|
945
|
+
runCli();
|
|
946
|
+
} catch (error) {
|
|
947
|
+
process.stderr.write(`[codex-obsidian] Token usage falhou: ${error.message}\n`);
|
|
948
|
+
process.exitCode = 1;
|
|
949
|
+
}
|
|
950
|
+
}
|