wendkeep 0.24.0 → 0.25.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 +14 -0
- package/README.md +12 -0
- package/bin/wendkeep.mjs +8 -1
- package/package.json +1 -1
- package/src/cost.mjs +108 -3
- package/src/stats.mjs +45 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@ 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.25.0] — 2026-07-08
|
|
8
|
+
|
|
9
|
+
Cost trend/projection + shareable stats + launch assets.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **`wendkeep cost --trend [day|week|month]`** — cost bucketed over time plus a run-rate
|
|
13
|
+
**projection** (recent-window daily average × horizon). `wendkeep cost --write` generates a
|
|
14
|
+
`00-Custo.md` trend note in the vault (by-month table + projection + top models). `src/cost.mjs`.
|
|
15
|
+
- **`wendkeep stats`** — one shareable line: sessions · prompts · spend · date span · models
|
|
16
|
+
(`--json` too). For the npm page, a README badge line, or a post. `src/stats.mjs`.
|
|
17
|
+
- **Launch assets** (`docs/`): README hero (tagline, badges, quickstart, screenshot slot),
|
|
18
|
+
Show HN / r/ObsidianMD / X post drafts (`docs/20-launch-posts.md`), and a repeatable
|
|
19
|
+
graph-screenshot guide (`docs/21-graph-screenshot.md`).
|
|
20
|
+
|
|
7
21
|
## [0.24.0] — 2026-07-08
|
|
8
22
|
|
|
9
23
|
### Changed
|
package/README.md
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
# wendkeep
|
|
2
2
|
|
|
3
|
+
> **Your AI coding agent forgets every session. wendkeep makes it remember — in the Obsidian vault you already use.**
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/wendkeep)
|
|
3
6
|

|
|
7
|
+

|
|
8
|
+

|
|
4
9
|
|
|
5
10
|
**A persistent‑memory harness for AI coding agents, built on your Obsidian vault.** Every Claude Code / Codex session is captured turn‑by‑turn into local Markdown — with token/cost tracking, auto‑extracted decisions, bugs and learnings, and a curated memory layer injected back at the start of the next session. On top of that memory core sits a native, zero‑dependency **change lifecycle** (spec → change → TDD → sensor‑gated archive) that keeps intent, work and proof wikilinked in one graph. 100% local, open‑core.
|
|
6
11
|
|
|
12
|
+
```bash
|
|
13
|
+
npm i -D wendkeep && npx wendkeep init # captures from the next session on
|
|
14
|
+
npx wendkeep import # backfill past Claude + Codex sessions
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
<!-- SCREENSHOT: Obsidian graph of a wendkeep vault (sessions ↔ decisions ↔ bugs ↔ changes). Drop a PNG/GIF at docs/assets/graph.png and reference it here. -->
|
|
18
|
+
|
|
7
19
|
> Extracted from a system in daily production use: the capture engine, cost tracking and graph wiring are battle‑tested; the cross‑platform installer (`wendkeep init`) and the native change loop are the newer parts. See [`docs/`](https://github.com/rogersialves/wendkeep/tree/main/docs) for the project's strategy and decision log.
|
|
8
20
|
|
|
9
21
|
---
|
package/bin/wendkeep.mjs
CHANGED
|
@@ -48,7 +48,9 @@ Usage:
|
|
|
48
48
|
wendkeep spec <sub> Living specs: list | show <capability>.
|
|
49
49
|
wendkeep sensors <sub> list | add <id> "<command>" [--severity --type --report].
|
|
50
50
|
wendkeep cost [opts] Aggregate AI-coding spend across the vault's sessions.
|
|
51
|
-
--since <date> · --top [N] (priciest
|
|
51
|
+
--since <date> · --top [N] (priciest) · --trend [day|week|month]
|
|
52
|
+
(+ run-rate projection) · --write (generate 00-Custo.md) · --json.
|
|
53
|
+
wendkeep stats [--vault P] One shareable line: sessions · prompts · spend · span · models (--json).
|
|
52
54
|
wendkeep import [opts] Backfill: import this project's past Claude + Codex sessions into
|
|
53
55
|
the vault (deduped by session_id). --source all|claude|codex (default
|
|
54
56
|
all) · --stamp-ids (backfill session_id in existing notes) ·
|
|
@@ -144,6 +146,11 @@ async function main() {
|
|
|
144
146
|
runCost(rest);
|
|
145
147
|
break;
|
|
146
148
|
}
|
|
149
|
+
case 'stats': {
|
|
150
|
+
const { runStats } = await import('../src/stats.mjs');
|
|
151
|
+
runStats(rest);
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
147
154
|
case 'import': {
|
|
148
155
|
const { runImportCli } = await import('../src/import.mjs');
|
|
149
156
|
runImportCli(rest);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.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
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
// `wendkeep cost` — aggregate AI-coding spend across every session note in the vault.
|
|
2
2
|
// Each session note carries cost in its frontmatter (main + subagents since 0.10.0); this
|
|
3
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';
|
|
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
7
|
|
|
8
8
|
const round4 = (n) => Math.round((Number(n) || 0) * 10000) / 10000;
|
|
9
9
|
const usd = (n) => `$${(Number(n) || 0).toFixed(4)}`;
|
|
10
|
+
const today = () => {
|
|
11
|
+
const d = new Date();
|
|
12
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
13
|
+
};
|
|
10
14
|
|
|
11
15
|
function fmValue(content, key) {
|
|
12
16
|
const m = content.match(new RegExp(`^${key}:\\s*(.+)$`, 'm'));
|
|
@@ -24,6 +28,7 @@ export function parseSessionCost(content) {
|
|
|
24
28
|
wasted: Number(fmValue(content, 'subagents_wasted_usd')) || 0,
|
|
25
29
|
tokens: Number(fmValue(content, 'tokens_total')) || 0,
|
|
26
30
|
subTokens: Number(fmValue(content, 'subagents_tokens_total')) || 0,
|
|
31
|
+
prompts: Number(fmValue(content, 'prompts')) || 0,
|
|
27
32
|
};
|
|
28
33
|
}
|
|
29
34
|
|
|
@@ -35,8 +40,9 @@ export function aggregateCosts(entries) {
|
|
|
35
40
|
let wasted = 0;
|
|
36
41
|
let tokens = 0;
|
|
37
42
|
let subTokens = 0;
|
|
43
|
+
let prompts = 0;
|
|
38
44
|
for (const e of entries) {
|
|
39
|
-
main += e.mainCost; sub += e.subCost; wasted += e.wasted || 0; tokens += e.tokens; subTokens += e.subTokens;
|
|
45
|
+
main += e.mainCost; sub += e.subCost; wasted += e.wasted || 0; tokens += e.tokens; subTokens += e.subTokens; prompts += e.prompts || 0;
|
|
40
46
|
const d = e.date || '?';
|
|
41
47
|
(byDay[d] = byDay[d] || { cost: 0, count: 0 }).cost += e.mainCost + e.subCost;
|
|
42
48
|
byDay[d].count += 1;
|
|
@@ -48,12 +54,93 @@ export function aggregateCosts(entries) {
|
|
|
48
54
|
count: entries.length,
|
|
49
55
|
main: round4(main), sub: round4(sub), total: round4(total), wasted: round4(wasted),
|
|
50
56
|
avg: round4(entries.length ? total / entries.length : 0),
|
|
51
|
-
tokens, subTokens,
|
|
57
|
+
tokens, subTokens, prompts,
|
|
52
58
|
byDay: Object.entries(byDay).sort().map(([date, v]) => ({ date, cost: round4(v.cost), count: v.count })),
|
|
53
59
|
byModel: Object.entries(byModel).sort((a, b) => b[1].cost - a[1].cost).map(([model, v]) => ({ model, cost: round4(v.cost), count: v.count })),
|
|
54
60
|
};
|
|
55
61
|
}
|
|
56
62
|
|
|
63
|
+
// Shift a 'YYYY-MM-DD' string by `delta` days (UTC, pure).
|
|
64
|
+
function shiftDate(dateStr, delta) {
|
|
65
|
+
const d = new Date(`${dateStr}T00:00:00Z`);
|
|
66
|
+
d.setUTCDate(d.getUTCDate() + delta);
|
|
67
|
+
return d.toISOString().slice(0, 10);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ISO-ish week key 'YYYY-Www' for a 'YYYY-MM-DD' string (pure).
|
|
71
|
+
function isoWeek(dateStr) {
|
|
72
|
+
const d = new Date(`${dateStr}T00:00:00Z`);
|
|
73
|
+
const day = (d.getUTCDay() + 6) % 7; // Mon=0
|
|
74
|
+
d.setUTCDate(d.getUTCDate() - day + 3); // nearest Thursday
|
|
75
|
+
const firstThu = new Date(Date.UTC(d.getUTCFullYear(), 0, 4));
|
|
76
|
+
const week = 1 + Math.round(((d - firstThu) / 86400000 - 3 + ((firstThu.getUTCDay() + 6) % 7)) / 7);
|
|
77
|
+
return `${d.getUTCFullYear()}-W${String(week).padStart(2, '0')}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Group the per-day cost series (agg.byDay) into day|week|month buckets. Pure.
|
|
81
|
+
export function trendBuckets(byDay, bucket = 'month') {
|
|
82
|
+
const keyOf = (date) => (bucket === 'day' ? date : bucket === 'week' ? isoWeek(date) : String(date).slice(0, 7));
|
|
83
|
+
const map = {};
|
|
84
|
+
for (const d of byDay || []) {
|
|
85
|
+
if (!d.date || d.date === '?') continue;
|
|
86
|
+
const k = keyOf(d.date);
|
|
87
|
+
(map[k] = map[k] || { period: k, cost: 0, count: 0 }).cost += d.cost;
|
|
88
|
+
map[k].count += d.count;
|
|
89
|
+
}
|
|
90
|
+
return Object.values(map).sort((a, b) => a.period.localeCompare(b.period)).map((b) => ({ ...b, cost: round4(b.cost) }));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Run-rate projection from the last `windowDays` of activity. Pure; nowStr = 'YYYY-MM-DD'
|
|
94
|
+
// (defaults to the latest day seen). Honest: a flat run-rate, not a fitted forecast.
|
|
95
|
+
export function projectSpend(byDay, { nowStr, windowDays = 30, horizonDays = 30 } = {}) {
|
|
96
|
+
const days = (byDay || []).filter((d) => d.date && d.date !== '?');
|
|
97
|
+
if (!days.length) return { dailyRate: 0, projected: 0, windowDays, horizonDays, basisDays: 0, basisTotal: 0 };
|
|
98
|
+
const now = nowStr || days[days.length - 1].date;
|
|
99
|
+
const cutoff = shiftDate(now, -windowDays);
|
|
100
|
+
const recent = days.filter((d) => d.date > cutoff);
|
|
101
|
+
const basisTotal = recent.reduce((s, d) => s + d.cost, 0);
|
|
102
|
+
const dailyRate = basisTotal / windowDays;
|
|
103
|
+
return { dailyRate: round4(dailyRate), projected: round4(dailyRate * horizonDays), windowDays, horizonDays, basisDays: recent.length, basisTotal: round4(basisTotal) };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// A generated vault note: cost by month + projection + top models. Overwrites (it is generated).
|
|
107
|
+
export function renderTrendNote(agg, proj, dateStr) {
|
|
108
|
+
const rows = trendBuckets(agg.byDay, 'month').map((b) => `| ${b.period} | ${usd(b.cost)} | ${b.count} |`).join('\n');
|
|
109
|
+
const models = agg.byModel.slice(0, 8).map((m) => `| ${m.model} | ${usd(m.cost)} | ${m.count} |`).join('\n');
|
|
110
|
+
return `---
|
|
111
|
+
type: cost-trend
|
|
112
|
+
date: ${dateStr}
|
|
113
|
+
cssclasses:
|
|
114
|
+
- topic-dashboard
|
|
115
|
+
tags:
|
|
116
|
+
- custo
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
# Custo — tendência
|
|
120
|
+
|
|
121
|
+
> Gerado por \`wendkeep cost --write\`. ${agg.count} sessão(ões) · total ${usd(agg.total)} · ${usd(agg.avg)}/sessão.
|
|
122
|
+
|
|
123
|
+
## Por mês
|
|
124
|
+
|
|
125
|
+
| Mês | Custo | Sessões |
|
|
126
|
+
|---|---|---|
|
|
127
|
+
${rows || '| — | — | — |'}
|
|
128
|
+
|
|
129
|
+
## Projeção (run-rate ${proj.windowDays}d)
|
|
130
|
+
|
|
131
|
+
Base: ${usd(proj.basisTotal)} nos últimos ${proj.windowDays} dias (${proj.basisDays} dia(s) com atividade) → **${usd(proj.dailyRate)}/dia**.
|
|
132
|
+
Projeção próximos ${proj.horizonDays} dias: **${usd(proj.projected)}**.
|
|
133
|
+
|
|
134
|
+
> Run-rate simples (média diária × horizonte), não previsão ajustada.
|
|
135
|
+
|
|
136
|
+
## Por modelo
|
|
137
|
+
|
|
138
|
+
| Modelo | Custo | Sessões |
|
|
139
|
+
|---|---|---|
|
|
140
|
+
${models || '| — | — | — |'}
|
|
141
|
+
`;
|
|
142
|
+
}
|
|
143
|
+
|
|
57
144
|
function walkNotes(dir) {
|
|
58
145
|
const out = [];
|
|
59
146
|
let names;
|
|
@@ -111,6 +198,24 @@ export function runCost(argv) {
|
|
|
111
198
|
process.exit(0);
|
|
112
199
|
}
|
|
113
200
|
|
|
201
|
+
const trendIdx = argv.indexOf('--trend');
|
|
202
|
+
if (trendIdx >= 0) {
|
|
203
|
+
const bucket = ['day', 'week', 'month'].includes(argv[trendIdx + 1]) ? argv[trendIdx + 1] : 'month';
|
|
204
|
+
const proj = projectSpend(agg.byDay);
|
|
205
|
+
process.stdout.write(`Tendência de custo (por ${bucket}):\n`);
|
|
206
|
+
for (const b of trendBuckets(agg.byDay, bucket)) process.stdout.write(` ${b.period} ${usd(b.cost).padStart(11)} (${b.count})\n`);
|
|
207
|
+
process.stdout.write(`\nRun-rate ${proj.windowDays}d: ${usd(proj.dailyRate)}/dia · projeção ${proj.horizonDays}d: ${usd(proj.projected)} (base ${usd(proj.basisTotal)} em ${proj.basisDays} dia(s))\n`);
|
|
208
|
+
process.exit(0);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (argv.includes('--write')) {
|
|
212
|
+
const proj = projectSpend(agg.byDay);
|
|
213
|
+
const rel = '00-Custo.md';
|
|
214
|
+
writeFileSync(join(vaultBase, rel), renderTrendNote(agg, proj, today()), 'utf8');
|
|
215
|
+
process.stdout.write(`cost --write: ${rel} (nota de tendência gerada)\n`);
|
|
216
|
+
process.exit(0);
|
|
217
|
+
}
|
|
218
|
+
|
|
114
219
|
process.stdout.write(`Custo total (vault): ${usd(agg.total)} — ${agg.count} sessão(ões) · ${usd(agg.avg)}/sessão\n`);
|
|
115
220
|
process.stdout.write(` main: ${usd(agg.main)} · subagents: ${usd(agg.sub)}\n`);
|
|
116
221
|
if (agg.wasted) process.stdout.write(` desperdiçado (runs killed/failed): ${usd(agg.wasted)}\n`);
|
package/src/stats.mjs
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// `wendkeep stats` — one shareable line about what the vault has captured. For the npm page,
|
|
2
|
+
// a README badge line, or a tweet. Read-only; reuses the cost aggregation.
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
5
|
+
import { collectVaultCost } from './cost.mjs';
|
|
6
|
+
|
|
7
|
+
const usd = (n) => `$${(Number(n) || 0).toFixed(2)}`;
|
|
8
|
+
const num = (n) => String(Math.round(Number(n) || 0)).replace(/\B(?=(\d{3})+(?!\d))/g, '.');
|
|
9
|
+
|
|
10
|
+
// Pure: derive the shareable stats from a collectVaultCost aggregate.
|
|
11
|
+
export function statsFrom(agg) {
|
|
12
|
+
const days = (agg.byDay || []).map((d) => d.date).filter((d) => d && d !== '?').sort();
|
|
13
|
+
return {
|
|
14
|
+
sessions: agg.count,
|
|
15
|
+
prompts: agg.prompts || 0,
|
|
16
|
+
cost: agg.total,
|
|
17
|
+
models: (agg.byModel || []).length,
|
|
18
|
+
firstDay: days[0] || '',
|
|
19
|
+
lastDay: days[days.length - 1] || '',
|
|
20
|
+
spanDays: days.length,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function statsLine(s) {
|
|
25
|
+
const span = s.firstDay && s.lastDay ? ` · ${s.spanDays} dia(s) (${s.firstDay}→${s.lastDay})` : '';
|
|
26
|
+
return `wendkeep: ${num(s.sessions)} sessão(ões) · ${num(s.prompts)} prompts · ${usd(s.cost)} capturado${span} · ${s.models} modelo(s)`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function opt(argv, name) {
|
|
30
|
+
const i = argv.indexOf(name);
|
|
31
|
+
if (i >= 0) return argv[i + 1];
|
|
32
|
+
const eq = argv.find((a) => a.startsWith(`${name}=`));
|
|
33
|
+
return eq ? eq.slice(name.length + 1) : undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function runStats(argv) {
|
|
37
|
+
const vaultRaw = opt(argv, '--vault') || process.env.OBSIDIAN_VAULT_PATH;
|
|
38
|
+
if (!vaultRaw) { process.stderr.write('wendkeep stats: no vault (--vault or OBSIDIAN_VAULT_PATH).\n'); process.exit(2); }
|
|
39
|
+
const vaultBase = isAbsolute(vaultRaw) ? vaultRaw : resolve(process.cwd(), vaultRaw);
|
|
40
|
+
if (!existsSync(vaultBase)) { process.stderr.write(`wendkeep stats: vault not found: ${vaultBase}\n`); process.exit(2); }
|
|
41
|
+
const s = statsFrom(collectVaultCost(vaultBase));
|
|
42
|
+
if (argv.includes('--json')) { process.stdout.write(`${JSON.stringify(s, null, 2)}\n`); process.exit(0); }
|
|
43
|
+
process.stdout.write(`${statsLine(s)}\n`);
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|