wendkeep 0.9.1 → 0.11.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 +28 -0
- package/README.md +2 -1
- package/bin/wendkeep.mjs +6 -0
- package/hooks/session-stop.mjs +9 -0
- package/hooks/subagent-usage.mjs +176 -0
- package/package.json +1 -1
- package/src/cost.mjs +107 -0
- package/src/taxonomy.mjs +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,32 @@ 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.11.0] — 2026-07-06
|
|
8
|
+
|
|
9
|
+
Vault-wide cost.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **`wendkeep cost`** — aggregate AI-coding spend across every session note in the vault:
|
|
13
|
+
total (main + subagents), by model, by day. `--since <YYYY-MM-DD>` to window; `--json` for
|
|
14
|
+
scripting. Builds on the per-session cost the capture hooks already record — on a real
|
|
15
|
+
project vault it surfaced **~$4.7k across 140 sessions** in one command.
|
|
16
|
+
|
|
17
|
+
## [0.10.0] — 2026-07-06
|
|
18
|
+
|
|
19
|
+
Subagent & workflow telemetry — closing the biggest observability gap.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- **Subagent + workflow capture:** the Stop hook now scans the session's sibling subagent
|
|
23
|
+
transcripts (`<session>/subagents/**`) and workflow runs, and folds them into the session
|
|
24
|
+
note — a new `## Subagents & Workflows` section (aggregate + a collapsible per-subagent
|
|
25
|
+
table) plus frontmatter fields (`subagents_count`, `subagents_tokens_total`,
|
|
26
|
+
`subagents_custo_usd`, `tokens_total_incl_subagents`). Reuses the token-usage parser
|
|
27
|
+
(deduped per request). Previously a session that spawned a Workflow recorded ONLY the main
|
|
28
|
+
transcript — on a real audit session that hid **12 subagents / 4.6M tokens / $7.59** (2× the
|
|
29
|
+
main). The main `tokens_total` stays the main agent's (comparable to Claude Code's own
|
|
30
|
+
display); subagents are a separate axis.
|
|
31
|
+
- Provider-gated by structure (Claude Code's `subagents/` layout); fail-open — never blocks Stop.
|
|
32
|
+
|
|
7
33
|
## [0.9.1] — 2026-07-06
|
|
8
34
|
|
|
9
35
|
Interactive install UX: language first.
|
|
@@ -223,6 +249,8 @@ Initial release — the capture engine, extracted from a system in daily product
|
|
|
223
249
|
- `wendkeep init` (cross-platform installer) + optional `@bitbonsai/mcpvault` MCP server.
|
|
224
250
|
|
|
225
251
|
<!-- Only v0.4.0+ is tagged in git (history starts here); older versions link to npm. -->
|
|
252
|
+
[0.11.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.11.0
|
|
253
|
+
[0.10.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.10.0
|
|
226
254
|
[0.9.1]: https://github.com/rogersialves/wendkeep/releases/tag/v0.9.1
|
|
227
255
|
[0.9.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.9.0
|
|
228
256
|
[0.8.1]: https://github.com/rogersialves/wendkeep/releases/tag/v0.8.1
|
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/session-stop.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { addUsage, costBreakdown, emptyTokenUsage, normalizeClaudeUsage, normali
|
|
|
8
8
|
import { buildBrainDigest, buildBrainIndex } from './brain-core.mjs';
|
|
9
9
|
import { activeChangeLink } from './change-core.mjs';
|
|
10
10
|
import { getLocale } from './locale.mjs';
|
|
11
|
+
import { upsertSubagentUsage } from './subagent-usage.mjs';
|
|
11
12
|
import {
|
|
12
13
|
ensureDir,
|
|
13
14
|
findActiveSessionByTranscript,
|
|
@@ -1044,6 +1045,14 @@ function main() {
|
|
|
1044
1045
|
process.stderr.write(`[codex-obsidian] Token usage falhou: ${error.message}\n`);
|
|
1045
1046
|
}
|
|
1046
1047
|
|
|
1048
|
+
// Subagent/workflow telemetry (0.10.0): fold sibling subagent transcripts into the note.
|
|
1049
|
+
// Provider-gated by structure + fail-open — never derruba o Stop.
|
|
1050
|
+
try {
|
|
1051
|
+
upsertSubagentUsage(sessionPath, transcriptPath);
|
|
1052
|
+
} catch (error) {
|
|
1053
|
+
process.stderr.write(`[codex-obsidian] Subagent usage falhou: ${error.message}\n`);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1047
1056
|
if (!shouldFinalizeSession()) {
|
|
1048
1057
|
writeControl(vaultBase, {
|
|
1049
1058
|
...control,
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// hooks/subagent-usage.mjs — capture subagent + workflow telemetry (0.10.0). The Stop hook
|
|
2
|
+
// only reads the MAIN transcript; a session that spawns subagents/workflows (e.g. a Workflow
|
|
3
|
+
// run) burns tokens in sibling transcripts the note never recorded. This scans them.
|
|
4
|
+
// Reuses token-usage.mjs's parser. Provider-gated by structure (Claude Code layout).
|
|
5
|
+
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { basename, join } from 'node:path';
|
|
7
|
+
import { parseTokenUsageFromTranscript, summarizeTokenUsage } from './token-usage.mjs';
|
|
8
|
+
|
|
9
|
+
const fmt = (n) => Math.trunc(Number(n) || 0).toString().replace(/\B(?=(\d{3})+(?!\d))/g, '.');
|
|
10
|
+
const usd = (n) => `$${(Number(n) || 0).toFixed(4)}`;
|
|
11
|
+
|
|
12
|
+
// <slug>/<id>.jsonl (main transcript) -> <slug>/<id>/ (sibling dir with subagents/workflows).
|
|
13
|
+
export function sessionDirFromTranscript(transcriptPath) {
|
|
14
|
+
return String(transcriptPath || '').replace(/\.jsonl?$/i, '');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function walkAgentJsonl(dir) {
|
|
18
|
+
const out = [];
|
|
19
|
+
let names;
|
|
20
|
+
try { names = readdirSync(dir); } catch { return out; }
|
|
21
|
+
for (const n of names) {
|
|
22
|
+
const p = join(dir, n);
|
|
23
|
+
let st;
|
|
24
|
+
try { st = statSync(p); } catch { continue; }
|
|
25
|
+
if (st.isDirectory()) out.push(...walkAgentJsonl(p));
|
|
26
|
+
else if (n.startsWith('agent-') && n.endsWith('.jsonl')) out.push(p);
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// workflows/scripts/<name>-wf_<rid>.js -> { wf_<rid>: <name> }
|
|
32
|
+
function workflowNameMap(sessionDir) {
|
|
33
|
+
const map = {};
|
|
34
|
+
let names;
|
|
35
|
+
try { names = readdirSync(join(sessionDir, 'workflows', 'scripts')); } catch { return map; }
|
|
36
|
+
for (const n of names) {
|
|
37
|
+
const m = n.match(/^(.*)-(wf_[a-z0-9-]+)\.js$/i);
|
|
38
|
+
if (m) map[m[2]] = m[1];
|
|
39
|
+
}
|
|
40
|
+
return map;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function runIdOfPath(file) {
|
|
44
|
+
const m = file.replace(/\\/g, '/').match(/\/(wf_[a-z0-9-]+)\//i);
|
|
45
|
+
return m ? m[1] : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function tokensTotal(t = {}) {
|
|
49
|
+
return (t.input || 0) + (t.cached || 0) + (t.cacheWrite || 0) + (t.output || 0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const round4 = (n) => Math.round((Number(n) || 0) * 10000) / 10000;
|
|
53
|
+
|
|
54
|
+
// Aggregate every subagent transcript under <sessionDir>/subagents. null when the dir is
|
|
55
|
+
// absent (Codex / a session with no subagents) or nothing parseable.
|
|
56
|
+
export function collectSubagentUsage(sessionDir) {
|
|
57
|
+
const subDir = join(sessionDir, 'subagents');
|
|
58
|
+
if (!existsSync(subDir)) return null;
|
|
59
|
+
const files = walkAgentJsonl(subDir);
|
|
60
|
+
if (!files.length) return null;
|
|
61
|
+
|
|
62
|
+
const nameMap = workflowNameMap(sessionDir);
|
|
63
|
+
const subagents = [];
|
|
64
|
+
const wf = {};
|
|
65
|
+
const usageAgg = { input: 0, cached: 0, cacheWrite: 0, output: 0 };
|
|
66
|
+
let count = 0;
|
|
67
|
+
let calls = 0;
|
|
68
|
+
let cost = 0;
|
|
69
|
+
|
|
70
|
+
for (const f of files) {
|
|
71
|
+
const summary = summarizeTokenUsage(parseTokenUsageFromTranscript(f));
|
|
72
|
+
if (!summary.calls) continue;
|
|
73
|
+
const runId = runIdOfPath(f);
|
|
74
|
+
const workflow = runId ? nameMap[runId] || runId : null;
|
|
75
|
+
let agentType = '';
|
|
76
|
+
try { agentType = JSON.parse(readFileSync(f.replace(/\.jsonl$/i, '.meta.json'), 'utf8')).agentType || ''; } catch { /* no meta */ }
|
|
77
|
+
const tokens = tokensTotal(summary.totals);
|
|
78
|
+
|
|
79
|
+
subagents.push({
|
|
80
|
+
id: basename(f).replace(/^agent-/, '').replace(/\.jsonl$/i, '').slice(0, 12),
|
|
81
|
+
agentType,
|
|
82
|
+
workflow,
|
|
83
|
+
model: summary.models[0] || '?',
|
|
84
|
+
tools: summary.tools.length,
|
|
85
|
+
calls: summary.calls,
|
|
86
|
+
tokens,
|
|
87
|
+
cost: round4(summary.costs.model),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
count += 1;
|
|
91
|
+
calls += summary.calls;
|
|
92
|
+
cost += summary.costs.model;
|
|
93
|
+
for (const k of Object.keys(usageAgg)) usageAgg[k] += summary.totals[k] || 0;
|
|
94
|
+
if (runId) {
|
|
95
|
+
wf[runId] = wf[runId] || { agents: 0, cost: 0 };
|
|
96
|
+
wf[runId].agents += 1;
|
|
97
|
+
wf[runId].cost += summary.costs.model;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (!count) return null;
|
|
101
|
+
|
|
102
|
+
const workflows = Object.entries(wf).map(([runId, v]) => ({
|
|
103
|
+
runId, name: nameMap[runId] || runId, agents: v.agents, cost: round4(v.cost),
|
|
104
|
+
}));
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
subagents,
|
|
108
|
+
workflows,
|
|
109
|
+
aggregate: { count, calls, tokens: tokensTotal(usageAgg), cost: round4(cost), usage: usageAgg },
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function renderSubagentSection(c) {
|
|
114
|
+
const a = c.aggregate;
|
|
115
|
+
const wf = c.workflows.length
|
|
116
|
+
? c.workflows.map((w) => `${w.name} (${w.runId} · ${w.agents} agentes · ${usd(w.cost)})`).join('; ')
|
|
117
|
+
: '(nenhum)';
|
|
118
|
+
const rows = c.subagents
|
|
119
|
+
.map((s) => `| ${s.id} | ${s.agentType || '-'} | ${s.workflow || '-'} | ${s.model} | ${s.tools} | ${fmt(s.tokens)} | ${usd(s.cost)} |`)
|
|
120
|
+
.join('\n');
|
|
121
|
+
return `## Subagents & Workflows
|
|
122
|
+
|
|
123
|
+
> Custo de subagents/workflows desta sessão — NÃO incluído no total principal acima.
|
|
124
|
+
|
|
125
|
+
- **Subagents:** ${a.count} · ${a.calls} chamadas · ${fmt(a.tokens)} tokens · ${usd(a.cost)}
|
|
126
|
+
- **Workflows:** ${wf}
|
|
127
|
+
|
|
128
|
+
<details><summary>Por subagent (${a.count})</summary>
|
|
129
|
+
|
|
130
|
+
| Agent | Tipo | Workflow | Modelo | Tools | Tokens | Custo |
|
|
131
|
+
|---|---|---|---|---:|---:|---:|
|
|
132
|
+
${rows}
|
|
133
|
+
|
|
134
|
+
</details>`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function setFrontmatterField(content, key, value) {
|
|
138
|
+
const m = content.match(/^---\n([\s\S]*?)\n---/);
|
|
139
|
+
if (!m) return content;
|
|
140
|
+
const re = new RegExp(`^${key}:.*$`, 'm');
|
|
141
|
+
const line = `${key}: ${value}`;
|
|
142
|
+
const fm = re.test(m[1]) ? m[1].replace(re, line) : `${m[1]}\n${line}`;
|
|
143
|
+
return content.replace(m[0], `---\n${fm}\n---`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function frontmatterNumber(content, key) {
|
|
147
|
+
const m = content.match(new RegExp(`^${key}:\\s*(.+)$`, 'm'));
|
|
148
|
+
return m ? Number(String(m[1]).trim().replace(/^["']|["']$/g, '')) || 0 : 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function upsertSection(content, heading, body) {
|
|
152
|
+
const esc = heading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
153
|
+
const re = new RegExp(`${esc}\\n[\\s\\S]*?(?=\\n## |$)`);
|
|
154
|
+
if (re.test(content)) return content.replace(re, `${body}\n`);
|
|
155
|
+
for (const anchor of ['\n## Encerramento', '\n## Issues Linear', '\n## Pendências']) {
|
|
156
|
+
const i = content.indexOf(anchor);
|
|
157
|
+
if (i >= 0) return `${content.slice(0, i)}\n\n${body}\n${content.slice(i)}`;
|
|
158
|
+
}
|
|
159
|
+
return `${content.trimEnd()}\n\n${body}\n`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Stop-hook entry: scan the session's subagents/workflows, fold into the note. Fail-open.
|
|
163
|
+
export function upsertSubagentUsage(sessionPath, transcriptPath) {
|
|
164
|
+
if (!sessionPath || !existsSync(sessionPath)) return false;
|
|
165
|
+
const collected = collectSubagentUsage(sessionDirFromTranscript(transcriptPath));
|
|
166
|
+
if (!collected) return false;
|
|
167
|
+
const a = collected.aggregate;
|
|
168
|
+
let content = readFileSync(sessionPath, 'utf8');
|
|
169
|
+
content = setFrontmatterField(content, 'subagents_count', a.count);
|
|
170
|
+
content = setFrontmatterField(content, 'subagents_tokens_total', a.tokens);
|
|
171
|
+
content = setFrontmatterField(content, 'subagents_custo_usd', a.cost);
|
|
172
|
+
content = setFrontmatterField(content, 'tokens_total_incl_subagents', frontmatterNumber(content, 'tokens_total') + a.tokens);
|
|
173
|
+
content = upsertSection(content, '## Subagents & Workflows', renderSubagentSection(collected));
|
|
174
|
+
writeFileSync(sessionPath, content, 'utf8');
|
|
175
|
+
return true;
|
|
176
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.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
|
+
}
|