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