wendkeep 0.35.0 → 0.36.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/CHANGELOG.md CHANGED
@@ -870,3 +870,9 @@ Initial release — the capture engine, extracted from a system in daily product
870
870
  [0.2.7]: https://www.npmjs.com/package/wendkeep/v/0.2.7
871
871
  [0.2.0]: https://www.npmjs.com/package/wendkeep/v/0.2.0
872
872
  [0.1.0]: https://www.npmjs.com/package/wendkeep/v/0.1.0
873
+ # 0.36.0 — 2026-07-11
874
+
875
+ - Custos de subagents agora são atribuídos ao modelo que realmente os executou por meio de um ledger por sessão; campos legados continuam compatíveis.
876
+ - Novo `wendkeep cost rebuild`, dry-run por padrão, reconstrói sessões antigas via `SESSION_REGISTRY` e, com `--apply`, grava um manifesto auditável em `.brain/COST_REBUILD.json`.
877
+ - Totais combinados de tokens/custo passam a ser persistidos explicitamente nas notas.
878
+ - Preços API-equivalentes de GPT-5.6 Sol, Terra e Luna adicionados à tabela editável e ao fallback interno.
package/README.md CHANGED
@@ -118,6 +118,7 @@ No re‑copying, no snapshot to re‑sync — the package is the single source o
118
118
  | `wendkeep sensors <sub>` | `list` / `add <id> "<command>"` — view/edit `wendkeep.sensors.json` (JSON Schema shipped). |
119
119
  | `wendkeep cost [--since d]` | Aggregate AI-coding spend across the vault's sessions — total, by model, by day (`--json`). |
120
120
  | `wendkeep import [opts]` | **Retroactive memory** — backfill past **Claude + Codex** sessions into the vault (deduped by `session_id`). `--source all\|claude\|codex` / `--from <dir>` / `--codex-from <dir>` / `--since d` / `--limit n` / `--dry-run` / `--json`. |
121
+ | `wendkeep cost rebuild [opts]` | Recalcula custos históricos do transcript principal e subagents usando `SESSION_REGISTRY`. Dry-run por padrão; `--apply` atualiza notas e grava `.brain/COST_REBUILD.json`. Aceita `--session`, `--limit` e `--json`. |
121
122
  | `wendkeep lesson add "t" "l"` | Record a project-local lesson (injected at the next SessionStart). |
122
123
  | `wendkeep sync-defs` | Copy `.brain/agents\|skills` into `.codex/agents`, `.claude/skills`, `.agents/skills`; `--check` detects drift. |
123
124
  | `wendkeep validate-memory [path]` | Validate `.brain/CORE.md` (cap 25, 3 sections, no secrets/PII). |
package/README.pt-BR.md CHANGED
@@ -115,6 +115,7 @@ Sem recopiar, sem snapshot pra re‑sincronizar — o pacote é a única fonte d
115
115
  | `wendkeep spec <sub>` | Specs vivos: `list` / `show <capability>`. |
116
116
  | `wendkeep sensors <sub>` | `list` / `add <id> "<comando>"` — vê/edita `wendkeep.sensors.json` (JSON Schema incluso). |
117
117
  | `wendkeep cost [opts]` | Agrega o gasto de IA nas sessões do cofre — total, por modelo, por dia · `--top [N]` · `--trend [day\|week\|month]` (+ projeção) · `--write` (gera `00-Custo.md`) · `--json`. |
118
+ | `wendkeep cost rebuild [opts]` | Reconstrói custos históricos do transcript principal e subagents via `SESSION_REGISTRY`. Dry-run por padrão; `--apply` grava notas e `.brain/COST_REBUILD.json`. |
118
119
  | `wendkeep stats [--vault P]` | Uma linha compartilhável: sessões · prompts · gasto · período · modelos (`--json`). |
119
120
  | `wendkeep import [opts]` | **Memória retroativa** — importa sessões passadas de **Claude + Codex** pro cofre (dedup por `session_id`). `--source all\|claude\|codex` / `--from <dir>` / `--codex-from <dir>` / `--stamp-ids` / `--since d` / `--limit n` / `--dry-run` / `--json`. |
120
121
  | `wendkeep dashboard [--force]` | (Re)gera os Bases filtrados por pasta + o MOC `00-Dashboard`. |
package/bin/wendkeep.mjs CHANGED
@@ -51,6 +51,9 @@ Usage:
51
51
  wendkeep cost [opts] Aggregate AI-coding spend across the vault's sessions.
52
52
  --since <date> · --top [N] (priciest) · --trend [day|week|month]
53
53
  (+ run-rate projection) · --write (generate 00-Custo.md) · --json.
54
+ wendkeep cost rebuild Recalculate historical parent + subagent costs from SESSION_REGISTRY.
55
+ Dry-run by default · --apply writes notes + .brain/COST_REBUILD.json
56
+ · --session <id|file> · --limit N · --json.
54
57
  wendkeep stats [--vault P] One shareable line: sessions · prompts · spend · span · models (--json).
55
58
  wendkeep import [opts] Backfill: import this project's past Claude + Codex sessions into
56
59
  the vault (deduped by session_id). --source all|claude|codex (default
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "_nota": "Preços API por milhão de tokens. cachedInput = cache read. Cache write aplica multiplicador no código: 5m = 1.25x input, 1h = 2x input. Editar aqui quando o provedor mudar preços (sem mexer no .mjs). Se o arquivo sumir ou ficar inválido, o hook usa a tabela embutida em token-usage.mjs.",
3
- "_fonte": "Anthropic https://www.anthropic.com/pricing — conferido 2026-06-13 (Opus 4.8: $5 input / $25 output; cache read 0.1x; cache write 1.25x 5m / 2x 1h)",
4
- "models": {
3
+ "_fonte": "OpenAI https://openai.com/index/gpt-5-6/ e https://help.openai.com/en/articles/20001325-a-preview-of-gpt-56-sol-terra-and-luna; Anthropic https://www.anthropic.com/pricing conferido 2026-07-11",
4
+ "models": {
5
+ "gpt-5.6-sol": { "label": "GPT-5.6 Sol API", "provider": "openai", "input": 5, "cachedInput": 0.5, "output": 30 },
6
+ "gpt-5.6-terra": { "label": "GPT-5.6 Terra API", "provider": "openai", "input": 2.5, "cachedInput": 0.25, "output": 15 },
7
+ "gpt-5.6-luna": { "label": "GPT-5.6 Luna API", "provider": "openai", "input": 1, "cachedInput": 0.1, "output": 6 },
5
8
  "gpt-5.5": {
6
9
  "label": "GPT-5.5 API",
7
10
  "provider": "openai",
@@ -91,6 +91,7 @@ export function collectSubagentUsage(sessionDir) {
91
91
  let count = 0;
92
92
  let calls = 0;
93
93
  let cost = 0;
94
+ const modelMap = new Map();
94
95
 
95
96
  for (const f of files) {
96
97
  const summary = summarizeTokenUsage(parseTokenUsageFromTranscript(f));
@@ -112,8 +113,18 @@ export function collectSubagentUsage(sessionDir) {
112
113
  calls: summary.calls,
113
114
  tokens,
114
115
  cost: round4(summary.costs.model),
116
+ modelRows: summary.modelRows,
115
117
  });
116
118
 
119
+ for (const row of summary.modelRows || []) {
120
+ const key = `${row.provider || '?'}\u0000${row.model || '?'}`;
121
+ const current = modelMap.get(key) || { provider: row.provider || '?', model: row.model || '?', calls: 0, tokens: 0, cost: 0 };
122
+ current.calls += row.calls || 0;
123
+ current.tokens += tokensTotal(row.usage);
124
+ current.cost += row.costs?.model || 0;
125
+ modelMap.set(key, current);
126
+ }
127
+
117
128
  count += 1;
118
129
  calls += summary.calls;
119
130
  cost += summary.costs.model;
@@ -151,7 +162,9 @@ export function collectSubagentUsage(sessionDir) {
151
162
  return {
152
163
  subagents,
153
164
  workflows,
154
- aggregate: { count, calls, tokens: tokensTotal(usageAgg), cost: round4(cost), wasted, usage: usageAgg, tools: [...allTools] },
165
+ aggregate: { count, calls, tokens: tokensTotal(usageAgg), cost: round4(cost), wasted, usage: usageAgg, tools: [...allTools],
166
+ modelRows: [...modelMap.values()].map((r) => ({ ...r, cost: round4(r.cost), source: 'subagent' })),
167
+ },
155
168
  };
156
169
  }
157
170
 
@@ -173,19 +186,22 @@ export function renderSubagentSection(c) {
173
186
  .map((s) => `| ${s.id} | ${s.agentType || '-'} | ${s.workflow || '-'} | ${s.model} | ${s.tools} | ${fmt(s.tokens)} | ${usd(s.cost)} |`)
174
187
  .join('\n');
175
188
  const wasteLine = a.wasted ? `\n- **Desperdiçado (runs killed/failed):** ${usd(a.wasted)}` : '';
189
+ const combinedLine = c.combined ? `\n- **Sessão completa (main + subagents):** ${fmt(c.combined.tokens)} tokens · ${usd(c.combined.cost)}` : '';
190
+ const modelRows = c.combined?.models?.map((m) => `| ${m.model} | ${m.source} | ${fmt(m.tokens)} | ${usd(m.cost)} |`).join('\n') || '';
191
+ const models = modelRows ? `\n\n### Por modelo (sessão completa)\n\n| Modelo | Origem | Tokens | Custo |\n|---|---|---:|---:|\n${modelRows}` : '';
176
192
  return `## Subagents & Workflows
177
193
 
178
- > Custo de subagents/workflows desta sessão NÃO incluído no total principal acima.
194
+ > Custo de subagents/workflows desta sessão, seguido do total combinado e da atribuição por modelo.
179
195
 
180
196
  - **Subagents:** ${a.count} · ${a.calls} chamadas · ${fmt(a.tokens)} tokens · ${usd(a.cost)}
181
197
  - **Workflows:** ${wf}
182
- - **Tools (subagents):** ${tools}${wasteLine}
198
+ - **Tools (subagents):** ${tools}${wasteLine}${combinedLine}
183
199
 
184
200
  ### Por subagent (${a.count})
185
201
 
186
202
  | Agent | Tipo | Workflow | Modelo | Tools | Tokens | Custo |
187
203
  |---|---|---|---|---:|---:|---:|
188
- ${rows}`;
204
+ ${rows}${models}`;
189
205
  }
190
206
 
191
207
  function setFrontmatterField(content, key, value) {
@@ -228,6 +244,26 @@ export function upsertSubagentUsage(sessionPath, transcriptPath) {
228
244
  content = setFrontmatterField(content, 'subagents_tools', `"${(a.tools || []).join(', ')}"`);
229
245
  content = setFrontmatterField(content, 'subagents_wasted_usd', a.wasted || 0);
230
246
  content = setFrontmatterField(content, 'tokens_total_incl_subagents', frontmatterNumber(content, 'tokens_total') + a.tokens);
247
+ content = setFrontmatterField(content, 'custo_total_incl_subagents_usd', round4(frontmatterNumber(content, 'custo_modelo_usd') + a.cost));
248
+ let mainRows = [];
249
+ try {
250
+ const main = summarizeTokenUsage(parseTokenUsageFromTranscript(transcriptPath));
251
+ mainRows = (main.modelRows || []).map((r) => ({
252
+ provider: r.provider || '?', model: r.model || '?', source: 'main', calls: r.calls || 0,
253
+ tokens: tokensTotal(r.usage), cost: round4(r.costs?.model || 0),
254
+ }));
255
+ } catch { /* preserve legacy aggregate fallback */ }
256
+ if (!mainRows.length) {
257
+ const mainModel = (content.match(/^custo_modelo_label:\s*["']?([^"'\r\n]+)["']?\s*$/m) || [])[1] || '?';
258
+ mainRows = [{ model: mainModel, source: 'main', cost: round4(frontmatterNumber(content, 'custo_modelo_usd')), tokens: frontmatterNumber(content, 'tokens_total') }];
259
+ }
260
+ const ledger = [...mainRows, ...(a.modelRows || [])];
261
+ collected.combined = {
262
+ tokens: frontmatterNumber(content, 'tokens_total') + a.tokens,
263
+ cost: round4(frontmatterNumber(content, 'custo_modelo_usd') + a.cost),
264
+ models: ledger,
265
+ };
266
+ content = setFrontmatterField(content, 'custo_por_modelo_json', `'${JSON.stringify(ledger).replaceAll("'", "''")}'`);
231
267
  content = upsertSection(content, '## Subagents & Workflows', renderSubagentSection(collected));
232
268
  writeFileSync(sessionPath, content, 'utf8');
233
269
  return true;
@@ -12,7 +12,10 @@ import {
12
12
  // Cache write: 5m = 1.25x input, 1h = 2x input (multiplicadores em calculateCost).
13
13
  // Tabela editável em pricing.json (mesma pasta); esta é o fallback embutido
14
14
  // usado quando o JSON some ou fica inválido — o hook nunca deve quebrar por isso.
15
- const DEFAULT_PRICE_REFERENCE = {
15
+ const DEFAULT_PRICE_REFERENCE = {
16
+ 'gpt-5.6-sol': { label: 'GPT-5.6 Sol API', provider: 'openai', input: 5, cachedInput: 0.5, output: 30 },
17
+ 'gpt-5.6-terra': { label: 'GPT-5.6 Terra API', provider: 'openai', input: 2.5, cachedInput: 0.25, output: 15 },
18
+ 'gpt-5.6-luna': { label: 'GPT-5.6 Luna API', provider: 'openai', input: 1, cachedInput: 0.1, output: 6 },
16
19
  'gpt-5.5': {
17
20
  label: 'GPT-5.5 API',
18
21
  provider: 'openai',
@@ -82,7 +85,18 @@ export function loadPriceReference(file = PRICING_FILE) {
82
85
 
83
86
  const PRICE_REFERENCE = loadPriceReference();
84
87
 
85
- const MODEL_ALIASES = {
88
+ const MODEL_ALIASES = {
89
+ 'gpt-5.6': 'gpt-5.6-sol',
90
+ 'gpt-5.6-sol': 'gpt-5.6-sol',
91
+ 'gpt-5-6-sol': 'gpt-5.6-sol',
92
+ 'openai/gpt-5.6': 'gpt-5.6-sol',
93
+ 'openai/gpt-5.6-sol': 'gpt-5.6-sol',
94
+ 'gpt-5.6-terra': 'gpt-5.6-terra',
95
+ 'gpt-5-6-terra': 'gpt-5.6-terra',
96
+ 'openai/gpt-5.6-terra': 'gpt-5.6-terra',
97
+ 'gpt-5.6-luna': 'gpt-5.6-luna',
98
+ 'gpt-5-6-luna': 'gpt-5.6-luna',
99
+ 'openai/gpt-5.6-luna': 'gpt-5.6-luna',
86
100
  'gpt-5.5': 'gpt-5.5',
87
101
  'gpt-5_5': 'gpt-5.5',
88
102
  'openai/gpt-5.5': 'gpt-5.5',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.35.0",
3
+ "version": "0.36.0",
4
4
  "description": "A persistent-memory harness for AI coding agents on your Obsidian vault: turn-by-turn session capture plus a native, zero-dependency spec→change→verify→archive loop (sensor-gated, independent verdict, mutation discrimination). Local-first, agent-agnostic (Claude Code, Codex, Cursor…).",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cost.mjs CHANGED
@@ -4,6 +4,7 @@
4
4
  import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
5
5
  import { isAbsolute, join, resolve } from 'node:path';
6
6
  import { getLocale } from '../hooks/locale.mjs';
7
+ import { rebuildSessionCosts } from './rebuild-costs.mjs';
7
8
 
8
9
  const round4 = (n) => Math.round((Number(n) || 0) * 10000) / 10000;
9
10
  const usd = (n) => `$${(Number(n) || 0).toFixed(4)}`;
@@ -20,6 +21,11 @@ function fmValue(content, key) {
20
21
  // Parse the cost-relevant frontmatter of one note; null if it is not a session note.
21
22
  export function parseSessionCost(content) {
22
23
  if (!/^type:\s*session\s*$/m.test(content)) return null;
24
+ let ledger = [];
25
+ try {
26
+ const raw = fmValue(content, 'custo_por_modelo_json').replaceAll("''", "'");
27
+ if (raw) ledger = JSON.parse(raw);
28
+ } catch { /* legacy/malformed note: use safe fallback below */ }
23
29
  return {
24
30
  date: (fmValue(content, 'date') || '').slice(0, 10),
25
31
  model: fmValue(content, 'custo_modelo_label') || fmValue(content, 'modelo') || '?',
@@ -29,6 +35,7 @@ export function parseSessionCost(content) {
29
35
  tokens: Number(fmValue(content, 'tokens_total')) || 0,
30
36
  subTokens: Number(fmValue(content, 'subagents_tokens_total')) || 0,
31
37
  prompts: Number(fmValue(content, 'prompts')) || 0,
38
+ ledger,
32
39
  };
33
40
  }
34
41
 
@@ -46,8 +53,16 @@ export function aggregateCosts(entries) {
46
53
  const d = e.date || '?';
47
54
  (byDay[d] = byDay[d] || { cost: 0, count: 0 }).cost += e.mainCost + e.subCost;
48
55
  byDay[d].count += 1;
49
- (byModel[e.model] = byModel[e.model] || { cost: 0, count: 0 }).cost += e.mainCost + e.subCost;
50
- byModel[e.model].count += 1;
56
+ const rows = e.ledger?.length ? e.ledger : [
57
+ { model: e.model, cost: e.mainCost },
58
+ ...(e.subCost ? [{ model: 'subagents (legado, modelo desconhecido)', cost: e.subCost }] : []),
59
+ ];
60
+ const seen = new Set();
61
+ for (const row of rows) {
62
+ const model = row.model || '?';
63
+ (byModel[model] = byModel[model] || { cost: 0, count: 0 }).cost += Number(row.cost) || 0;
64
+ if (!seen.has(model)) { byModel[model].count += 1; seen.add(model); }
65
+ }
51
66
  }
52
67
  const total = main + sub;
53
68
  return {
@@ -186,6 +201,16 @@ export function runCost(argv) {
186
201
  if (!vaultRaw) { process.stderr.write('wendkeep cost: no vault (--vault or OBSIDIAN_VAULT_PATH).\n'); process.exit(2); }
187
202
  const vaultBase = isAbsolute(vaultRaw) ? vaultRaw : resolve(process.cwd(), vaultRaw);
188
203
  if (!existsSync(vaultBase)) { process.stderr.write(`wendkeep cost: vault not found: ${vaultBase}\n`); process.exit(2); }
204
+ if (argv[0] === 'rebuild') {
205
+ const report = rebuildSessionCosts(vaultBase, {
206
+ apply: argv.includes('--apply'),
207
+ session: opt(argv, '--session') || '',
208
+ limit: Number(opt(argv, '--limit')) || 0,
209
+ });
210
+ if (argv.includes('--json')) process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
211
+ else process.stdout.write(`cost rebuild (${report.mode}): ${report.scanned} lidas · ${report.changed} alteradas · ${report.unchanged} iguais · ${report.missing.length} sem fonte · ${report.errors.length} erros\n${report.mode === 'apply' ? 'Relatório: .brain/COST_REBUILD.json\n' : 'Nenhum arquivo foi alterado; use --apply para gravar.\n'}`);
212
+ process.exit(report.ok ? 0 : 1);
213
+ }
189
214
  const agg = collectVaultCost(vaultBase, { since: opt(argv, '--since') });
190
215
 
191
216
  if (argv.includes('--json')) { process.stdout.write(`${JSON.stringify(agg, null, 2)}\n`); process.exit(0); }
@@ -0,0 +1,40 @@
1
+ // Deterministic cost reconstruction for historical sessions.
2
+ // Registry is authoritative: session_file <-> transcript_path. Dry-run restores every note.
3
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { readSessionRegistry } from '../hooks/obsidian-common.mjs';
6
+ import { updateSessionUsage } from '../hooks/token-usage.mjs';
7
+ import { upsertSubagentUsage } from '../hooks/subagent-usage.mjs';
8
+
9
+ export function rebuildSessionCosts(vaultBase, { apply = false, session = '', limit = 0 } = {}) {
10
+ const registry = readSessionRegistry(vaultBase);
11
+ const report = { version: 1, generatedAt: new Date().toISOString(), mode: apply ? 'apply' : 'dry-run', scanned: 0, changed: 0, unchanged: 0, missing: [], errors: [], sessions: [] };
12
+ const entries = Object.entries(registry.sessions || {}).map(([sessionId, value]) => ({ sessionId, ...value }))
13
+ .filter((e) => e.session_file && e.transcript_path)
14
+ .filter((e) => !session || e.sessionId === session || e.session_file === session);
15
+ for (const entry of entries) {
16
+ if (limit && report.scanned >= limit) break;
17
+ report.scanned += 1;
18
+ const note = join(vaultBase, entry.session_file);
19
+ if (!existsSync(note) || !existsSync(entry.transcript_path)) {
20
+ report.missing.push({ sessionId: entry.sessionId, session: entry.session_file, note: existsSync(note), transcript: existsSync(entry.transcript_path) });
21
+ continue;
22
+ }
23
+ const before = readFileSync(note, 'utf8');
24
+ try {
25
+ updateSessionUsage({ vaultBase, sessionRel: entry.session_file, sessionPath: note, transcriptPath: entry.transcript_path });
26
+ upsertSubagentUsage(note, entry.transcript_path);
27
+ const after = readFileSync(note, 'utf8');
28
+ const changed = before !== after;
29
+ if (changed) report.changed += 1; else report.unchanged += 1;
30
+ report.sessions.push({ sessionId: entry.sessionId, session: entry.session_file, transcript: entry.transcript_path, changed });
31
+ if (!apply && changed) writeFileSync(note, before, 'utf8');
32
+ } catch (error) {
33
+ if (!apply) writeFileSync(note, before, 'utf8');
34
+ report.errors.push({ sessionId: entry.sessionId, session: entry.session_file, error: error.message });
35
+ }
36
+ }
37
+ report.ok = report.errors.length === 0;
38
+ if (apply) writeFileSync(join(vaultBase, '.brain', 'COST_REBUILD.json'), `${JSON.stringify(report, null, 2)}\n`, 'utf8');
39
+ return report;
40
+ }
package/src/stats.mjs CHANGED
@@ -14,7 +14,7 @@ export function statsFrom(agg) {
14
14
  sessions: agg.count,
15
15
  prompts: agg.prompts || 0,
16
16
  cost: agg.total,
17
- models: (agg.byModel || []).length,
17
+ models: (agg.byModel || []).filter((m) => m.model !== 'subagents (legado, modelo desconhecido)').length,
18
18
  firstDay: days[0] || '',
19
19
  lastDay: days[days.length - 1] || '',
20
20
  spanDays: days.length,