wendkeep 0.10.0 → 0.12.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 +24 -0
- package/README.md +2 -1
- package/bin/wendkeep.mjs +6 -0
- package/hooks/subagent-usage.mjs +59 -8
- package/package.json +1 -1
- package/src/cost.mjs +107 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@ All notable changes to **wendkeep** are documented here. Format based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this project follows
|
|
5
5
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.12.0] — 2026-07-06
|
|
8
|
+
|
|
9
|
+
Deeper subagent/workflow telemetry.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **Workflow run metadata** in the `## Subagents & Workflows` section: each run now shows its
|
|
13
|
+
**status** (completed / killed / …), phase titles, duration and agent count — read from the
|
|
14
|
+
authoritative `workflows/wf_*.json`. On a real session this surfaced a **killed** run that
|
|
15
|
+
still cost $2.50 next to the completed $5.76 one — wasted spend you couldn't see before.
|
|
16
|
+
- **Subagent tools rollup:** the distinct tools the subagents used, shown in the section and a
|
|
17
|
+
new `subagents_tools` frontmatter field.
|
|
18
|
+
|
|
19
|
+
## [0.11.0] — 2026-07-06
|
|
20
|
+
|
|
21
|
+
Vault-wide cost.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- **`wendkeep cost`** — aggregate AI-coding spend across every session note in the vault:
|
|
25
|
+
total (main + subagents), by model, by day. `--since <YYYY-MM-DD>` to window; `--json` for
|
|
26
|
+
scripting. Builds on the per-session cost the capture hooks already record — on a real
|
|
27
|
+
project vault it surfaced **~$4.7k across 140 sessions** in one command.
|
|
28
|
+
|
|
7
29
|
## [0.10.0] — 2026-07-06
|
|
8
30
|
|
|
9
31
|
Subagent & workflow telemetry — closing the biggest observability gap.
|
|
@@ -239,6 +261,8 @@ Initial release — the capture engine, extracted from a system in daily product
|
|
|
239
261
|
- `wendkeep init` (cross-platform installer) + optional `@bitbonsai/mcpvault` MCP server.
|
|
240
262
|
|
|
241
263
|
<!-- Only v0.4.0+ is tagged in git (history starts here); older versions link to npm. -->
|
|
264
|
+
[0.12.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.12.0
|
|
265
|
+
[0.11.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.11.0
|
|
242
266
|
[0.10.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.10.0
|
|
243
267
|
[0.9.1]: https://github.com/rogersialves/wendkeep/releases/tag/v0.9.1
|
|
244
268
|
[0.9.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.9.0
|
package/README.md
CHANGED
|
@@ -73,7 +73,8 @@ No re‑copying, no snapshot to re‑sync — the package is the single source o
|
|
|
73
73
|
| `wendkeep change <sub>` | Change lifecycle: `new [--simple]` / `list` / `show` / `status` / `done <id>` / `undone <id>` / `diff` / `archive [--force]`. |
|
|
74
74
|
| `wendkeep verify [--deep]` | Run the change's task sensors; `--deep` assembles the independent-verification package. |
|
|
75
75
|
| `wendkeep spec <sub>` | Living specs: `list` / `show <capability>`. |
|
|
76
|
-
| `wendkeep sensors list`
|
|
76
|
+
| `wendkeep sensors <sub>` | `list` / `add <id> "<command>"` — view/edit `wendkeep.sensors.json` (JSON Schema shipped). |
|
|
77
|
+
| `wendkeep cost [--since d]` | Aggregate AI-coding spend across the vault's sessions — total, by model, by day (`--json`). |
|
|
77
78
|
| `wendkeep lesson add "t" "l"` | Record a project-local lesson (injected at the next SessionStart). |
|
|
78
79
|
| `wendkeep sync-defs` | Copy `.brain/agents\|skills` into the project (`.codex/agents`, `.claude/skills`). |
|
|
79
80
|
| `wendkeep validate-memory [path]` | Validate `.brain/CORE.md` (cap 25, 3 sections, no secrets/PII). |
|
package/bin/wendkeep.mjs
CHANGED
|
@@ -47,6 +47,7 @@ Usage:
|
|
|
47
47
|
done <id> | undone <id> | diff | archive [--force].
|
|
48
48
|
wendkeep spec <sub> Living specs: list | show <capability>.
|
|
49
49
|
wendkeep sensors <sub> list | add <id> "<command>" [--severity --type --report].
|
|
50
|
+
wendkeep cost [--since d] Aggregate AI-coding spend across the vault's sessions (--json).
|
|
50
51
|
wendkeep verify [--change s] Run a change's task sensors + record evidence (the gate).
|
|
51
52
|
wendkeep lesson add "t" "l" Record a project-local lesson (injected at SessionStart).
|
|
52
53
|
wendkeep validate-memory [path] Validate .brain/CORE.md against the compaction
|
|
@@ -130,6 +131,11 @@ async function main() {
|
|
|
130
131
|
runSensors(rest);
|
|
131
132
|
break;
|
|
132
133
|
}
|
|
134
|
+
case 'cost': {
|
|
135
|
+
const { runCost } = await import('../src/cost.mjs');
|
|
136
|
+
runCost(rest);
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
133
139
|
case '--version':
|
|
134
140
|
case '-v':
|
|
135
141
|
process.stdout.write(`${version()}\n`);
|
package/hooks/subagent-usage.mjs
CHANGED
|
@@ -45,6 +45,29 @@ function runIdOfPath(file) {
|
|
|
45
45
|
return m ? m[1] : null;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// workflows/wf_<rid>.json carries authoritative run metadata (name/status/phases/duration).
|
|
49
|
+
function readWorkflowRuns(sessionDir) {
|
|
50
|
+
const runs = {};
|
|
51
|
+
let names;
|
|
52
|
+
try { names = readdirSync(join(sessionDir, 'workflows')); } catch { return runs; }
|
|
53
|
+
for (const n of names) {
|
|
54
|
+
if (!/^wf_.*\.json$/i.test(n)) continue;
|
|
55
|
+
try {
|
|
56
|
+
const o = JSON.parse(readFileSync(join(sessionDir, 'workflows', n), 'utf8'));
|
|
57
|
+
const runId = o.runId || n.replace(/\.json$/i, '');
|
|
58
|
+
runs[runId] = {
|
|
59
|
+
name: o.workflowName || runId,
|
|
60
|
+
status: o.status || '',
|
|
61
|
+
agentCount: o.agentCount || 0,
|
|
62
|
+
totalTokens: o.totalTokens || 0,
|
|
63
|
+
durationMs: o.durationMs || 0,
|
|
64
|
+
phases: (o.phases || []).map((p) => p && p.title).filter(Boolean),
|
|
65
|
+
};
|
|
66
|
+
} catch { /* skip bad run json */ }
|
|
67
|
+
}
|
|
68
|
+
return runs;
|
|
69
|
+
}
|
|
70
|
+
|
|
48
71
|
function tokensTotal(t = {}) {
|
|
49
72
|
return (t.input || 0) + (t.cached || 0) + (t.cacheWrite || 0) + (t.output || 0);
|
|
50
73
|
}
|
|
@@ -60,8 +83,10 @@ export function collectSubagentUsage(sessionDir) {
|
|
|
60
83
|
if (!files.length) return null;
|
|
61
84
|
|
|
62
85
|
const nameMap = workflowNameMap(sessionDir);
|
|
86
|
+
const runs = readWorkflowRuns(sessionDir);
|
|
63
87
|
const subagents = [];
|
|
64
88
|
const wf = {};
|
|
89
|
+
const allTools = new Set();
|
|
65
90
|
const usageAgg = { input: 0, cached: 0, cacheWrite: 0, output: 0 };
|
|
66
91
|
let count = 0;
|
|
67
92
|
let calls = 0;
|
|
@@ -71,10 +96,11 @@ export function collectSubagentUsage(sessionDir) {
|
|
|
71
96
|
const summary = summarizeTokenUsage(parseTokenUsageFromTranscript(f));
|
|
72
97
|
if (!summary.calls) continue;
|
|
73
98
|
const runId = runIdOfPath(f);
|
|
74
|
-
const workflow = runId ? nameMap[runId] || runId : null;
|
|
99
|
+
const workflow = runId ? (runs[runId] && runs[runId].name) || nameMap[runId] || runId : null;
|
|
75
100
|
let agentType = '';
|
|
76
101
|
try { agentType = JSON.parse(readFileSync(f.replace(/\.jsonl$/i, '.meta.json'), 'utf8')).agentType || ''; } catch { /* no meta */ }
|
|
77
102
|
const tokens = tokensTotal(summary.totals);
|
|
103
|
+
for (const t of summary.tools) allTools.add(t);
|
|
78
104
|
|
|
79
105
|
subagents.push({
|
|
80
106
|
id: basename(f).replace(/^agent-/, '').replace(/\.jsonl$/i, '').slice(0, 12),
|
|
@@ -82,6 +108,7 @@ export function collectSubagentUsage(sessionDir) {
|
|
|
82
108
|
workflow,
|
|
83
109
|
model: summary.models[0] || '?',
|
|
84
110
|
tools: summary.tools.length,
|
|
111
|
+
toolNames: summary.tools,
|
|
85
112
|
calls: summary.calls,
|
|
86
113
|
tokens,
|
|
87
114
|
cost: round4(summary.costs.model),
|
|
@@ -99,22 +126,44 @@ export function collectSubagentUsage(sessionDir) {
|
|
|
99
126
|
}
|
|
100
127
|
if (!count) return null;
|
|
101
128
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
129
|
+
// Merge transcript-derived cost with the authoritative run metadata (status/phases/duration).
|
|
130
|
+
const runIds = new Set([...Object.keys(wf), ...Object.keys(runs)]);
|
|
131
|
+
const workflows = [...runIds].map((runId) => {
|
|
132
|
+
const t = wf[runId] || { agents: 0, cost: 0 };
|
|
133
|
+
const r = runs[runId] || {};
|
|
134
|
+
return {
|
|
135
|
+
runId,
|
|
136
|
+
name: r.name || nameMap[runId] || runId,
|
|
137
|
+
status: r.status || '',
|
|
138
|
+
agents: r.agentCount || t.agents,
|
|
139
|
+
cost: round4(t.cost),
|
|
140
|
+
totalTokens: r.totalTokens || 0,
|
|
141
|
+
durationMs: r.durationMs || 0,
|
|
142
|
+
phases: r.phases || [],
|
|
143
|
+
};
|
|
144
|
+
});
|
|
105
145
|
|
|
106
146
|
return {
|
|
107
147
|
subagents,
|
|
108
148
|
workflows,
|
|
109
|
-
aggregate: { count, calls, tokens: tokensTotal(usageAgg), cost: round4(cost), usage: usageAgg },
|
|
149
|
+
aggregate: { count, calls, tokens: tokensTotal(usageAgg), cost: round4(cost), usage: usageAgg, tools: [...allTools] },
|
|
110
150
|
};
|
|
111
151
|
}
|
|
112
152
|
|
|
153
|
+
function workflowLine(w) {
|
|
154
|
+
const parts = [w.runId];
|
|
155
|
+
if (w.status) parts.push(w.status);
|
|
156
|
+
parts.push(`${w.agents} agentes`);
|
|
157
|
+
if (w.phases && w.phases.length) parts.push(`fases: ${w.phases.join(', ')}`);
|
|
158
|
+
if (w.durationMs) parts.push(`${Math.round(w.durationMs / 1000)}s`);
|
|
159
|
+
parts.push(usd(w.cost));
|
|
160
|
+
return `${w.name} (${parts.join(' · ')})`;
|
|
161
|
+
}
|
|
162
|
+
|
|
113
163
|
export function renderSubagentSection(c) {
|
|
114
164
|
const a = c.aggregate;
|
|
115
|
-
const wf = c.workflows.length
|
|
116
|
-
|
|
117
|
-
: '(nenhum)';
|
|
165
|
+
const wf = c.workflows.length ? c.workflows.map(workflowLine).join('; ') : '(nenhum)';
|
|
166
|
+
const tools = a.tools && a.tools.length ? a.tools.join(', ') : '(nenhuma)';
|
|
118
167
|
const rows = c.subagents
|
|
119
168
|
.map((s) => `| ${s.id} | ${s.agentType || '-'} | ${s.workflow || '-'} | ${s.model} | ${s.tools} | ${fmt(s.tokens)} | ${usd(s.cost)} |`)
|
|
120
169
|
.join('\n');
|
|
@@ -124,6 +173,7 @@ export function renderSubagentSection(c) {
|
|
|
124
173
|
|
|
125
174
|
- **Subagents:** ${a.count} · ${a.calls} chamadas · ${fmt(a.tokens)} tokens · ${usd(a.cost)}
|
|
126
175
|
- **Workflows:** ${wf}
|
|
176
|
+
- **Tools (subagents):** ${tools}
|
|
127
177
|
|
|
128
178
|
<details><summary>Por subagent (${a.count})</summary>
|
|
129
179
|
|
|
@@ -169,6 +219,7 @@ export function upsertSubagentUsage(sessionPath, transcriptPath) {
|
|
|
169
219
|
content = setFrontmatterField(content, 'subagents_count', a.count);
|
|
170
220
|
content = setFrontmatterField(content, 'subagents_tokens_total', a.tokens);
|
|
171
221
|
content = setFrontmatterField(content, 'subagents_custo_usd', a.cost);
|
|
222
|
+
content = setFrontmatterField(content, 'subagents_tools', `"${(a.tools || []).join(', ')}"`);
|
|
172
223
|
content = setFrontmatterField(content, 'tokens_total_incl_subagents', frontmatterNumber(content, 'tokens_total') + a.tokens);
|
|
173
224
|
content = upsertSection(content, '## Subagents & Workflows', renderSubagentSection(collected));
|
|
174
225
|
writeFileSync(sessionPath, content, 'utf8');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// `wendkeep cost` — aggregate AI-coding spend across every session note in the vault.
|
|
2
|
+
// Each session note carries cost in its frontmatter (main + subagents since 0.10.0); this
|
|
3
|
+
// rolls the whole vault up: total, by day, by model. Pure aggregation + a thin CLI.
|
|
4
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
5
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
6
|
+
import { getLocale } from '../hooks/locale.mjs';
|
|
7
|
+
|
|
8
|
+
const round4 = (n) => Math.round((Number(n) || 0) * 10000) / 10000;
|
|
9
|
+
const usd = (n) => `$${(Number(n) || 0).toFixed(4)}`;
|
|
10
|
+
|
|
11
|
+
function fmValue(content, key) {
|
|
12
|
+
const m = content.match(new RegExp(`^${key}:\\s*(.+)$`, 'm'));
|
|
13
|
+
return m ? m[1].trim().replace(/^["']|["']$/g, '') : '';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Parse the cost-relevant frontmatter of one note; null if it is not a session note.
|
|
17
|
+
export function parseSessionCost(content) {
|
|
18
|
+
if (!/^type:\s*session\s*$/m.test(content)) return null;
|
|
19
|
+
return {
|
|
20
|
+
date: (fmValue(content, 'date') || '').slice(0, 10),
|
|
21
|
+
model: fmValue(content, 'custo_modelo_label') || fmValue(content, 'modelo') || '?',
|
|
22
|
+
mainCost: Number(fmValue(content, 'custo_modelo_usd')) || 0,
|
|
23
|
+
subCost: Number(fmValue(content, 'subagents_custo_usd')) || 0,
|
|
24
|
+
tokens: Number(fmValue(content, 'tokens_total')) || 0,
|
|
25
|
+
subTokens: Number(fmValue(content, 'subagents_tokens_total')) || 0,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function aggregateCosts(entries) {
|
|
30
|
+
const byDay = {};
|
|
31
|
+
const byModel = {};
|
|
32
|
+
let main = 0;
|
|
33
|
+
let sub = 0;
|
|
34
|
+
let tokens = 0;
|
|
35
|
+
let subTokens = 0;
|
|
36
|
+
for (const e of entries) {
|
|
37
|
+
main += e.mainCost; sub += e.subCost; tokens += e.tokens; subTokens += e.subTokens;
|
|
38
|
+
const d = e.date || '?';
|
|
39
|
+
(byDay[d] = byDay[d] || { cost: 0, count: 0 }).cost += e.mainCost + e.subCost;
|
|
40
|
+
byDay[d].count += 1;
|
|
41
|
+
(byModel[e.model] = byModel[e.model] || { cost: 0, count: 0 }).cost += e.mainCost + e.subCost;
|
|
42
|
+
byModel[e.model].count += 1;
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
count: entries.length,
|
|
46
|
+
main: round4(main), sub: round4(sub), total: round4(main + sub),
|
|
47
|
+
tokens, subTokens,
|
|
48
|
+
byDay: Object.entries(byDay).sort().map(([date, v]) => ({ date, cost: round4(v.cost), count: v.count })),
|
|
49
|
+
byModel: Object.entries(byModel).sort((a, b) => b[1].cost - a[1].cost).map(([model, v]) => ({ model, cost: round4(v.cost), count: v.count })),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function walkNotes(dir) {
|
|
54
|
+
const out = [];
|
|
55
|
+
let names;
|
|
56
|
+
try { names = readdirSync(dir); } catch { return out; }
|
|
57
|
+
for (const n of names) {
|
|
58
|
+
const p = join(dir, n);
|
|
59
|
+
let st;
|
|
60
|
+
try { st = statSync(p); } catch { continue; }
|
|
61
|
+
if (st.isDirectory()) out.push(...walkNotes(p));
|
|
62
|
+
else if (n.endsWith('.md')) out.push(p);
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function collectVaultCost(vaultBase, { since } = {}) {
|
|
68
|
+
const sessionsDir = join(vaultBase, getLocale(vaultBase).folders.sessions);
|
|
69
|
+
const entries = [];
|
|
70
|
+
for (const f of walkNotes(sessionsDir)) {
|
|
71
|
+
let e;
|
|
72
|
+
try { e = parseSessionCost(readFileSync(f, 'utf8')); } catch { continue; }
|
|
73
|
+
if (!e) continue;
|
|
74
|
+
if (since && e.date && e.date < since) continue;
|
|
75
|
+
entries.push(e);
|
|
76
|
+
}
|
|
77
|
+
return aggregateCosts(entries);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function opt(argv, name) {
|
|
81
|
+
const i = argv.indexOf(name);
|
|
82
|
+
if (i >= 0) return argv[i + 1];
|
|
83
|
+
const eq = argv.find((a) => a.startsWith(`${name}=`));
|
|
84
|
+
return eq ? eq.slice(name.length + 1) : undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function runCost(argv) {
|
|
88
|
+
const vaultRaw = opt(argv, '--vault') || process.env.OBSIDIAN_VAULT_PATH;
|
|
89
|
+
if (!vaultRaw) { process.stderr.write('wendkeep cost: no vault (--vault or OBSIDIAN_VAULT_PATH).\n'); process.exit(2); }
|
|
90
|
+
const vaultBase = isAbsolute(vaultRaw) ? vaultRaw : resolve(process.cwd(), vaultRaw);
|
|
91
|
+
if (!existsSync(vaultBase)) { process.stderr.write(`wendkeep cost: vault not found: ${vaultBase}\n`); process.exit(2); }
|
|
92
|
+
const agg = collectVaultCost(vaultBase, { since: opt(argv, '--since') });
|
|
93
|
+
|
|
94
|
+
if (argv.includes('--json')) { process.stdout.write(`${JSON.stringify(agg, null, 2)}\n`); process.exit(0); }
|
|
95
|
+
|
|
96
|
+
process.stdout.write(`Custo total (vault): ${usd(agg.total)} — ${agg.count} sessão(ões)\n`);
|
|
97
|
+
process.stdout.write(` main: ${usd(agg.main)} · subagents: ${usd(agg.sub)}\n`);
|
|
98
|
+
if (agg.byModel.length) {
|
|
99
|
+
process.stdout.write('\nPor modelo:\n');
|
|
100
|
+
for (const m of agg.byModel) process.stdout.write(` ${m.model.padEnd(20)} ${usd(m.cost)} (${m.count})\n`);
|
|
101
|
+
}
|
|
102
|
+
if (agg.byDay.length) {
|
|
103
|
+
process.stdout.write('\nPor dia:\n');
|
|
104
|
+
for (const d of agg.byDay) process.stdout.write(` ${d.date} ${usd(d.cost)} (${d.count})\n`);
|
|
105
|
+
}
|
|
106
|
+
process.exit(0);
|
|
107
|
+
}
|