wendkeep 0.14.0 → 0.16.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 +33 -0
- package/README.md +15 -0
- package/bin/wendkeep.mjs +8 -1
- package/hooks/import-sessions.mjs +165 -0
- package/hooks/session-start.mjs +12 -9
- package/hooks/session-stop.mjs +3 -3
- package/package.json +1 -1
- package/src/cost.mjs +15 -1
- package/src/import.mjs +53 -0
- package/src/taxonomy.mjs +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,38 @@ 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.16.0] — 2026-07-06
|
|
8
|
+
|
|
9
|
+
Retroactive memory.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **`wendkeep import`** — backfill the vault with this project's *past* Claude Code sessions.
|
|
13
|
+
It scans `.claude/projects/<slug>/*.jsonl`, and for every session not already in the vault
|
|
14
|
+
(deduped by `session_id` against the `SESSION_REGISTRY`) reconstructs a full, dated session
|
|
15
|
+
note — frontmatter, one iteration block per turn, cost + subagent telemetry, derived
|
|
16
|
+
decision/bug/learning notes, and a finalized closing — placed in its **real** date folder
|
|
17
|
+
(`02-Sessões/<year>/<MM-MMM>/DIA <dd>/`), not today's. One command turns your whole history
|
|
18
|
+
into memory that `wendkeep cost` immediately aggregates.
|
|
19
|
+
- Offline replay of the live capture flow (same `buildSessionContent` / `insertIteration` /
|
|
20
|
+
`finalizeSessionFile` / usage + subagent code) so an imported note is indistinguishable
|
|
21
|
+
from a captured one.
|
|
22
|
+
- Options: `--from <dir>` (point at the `.claude/projects` folder explicitly), `--project`,
|
|
23
|
+
`--since <date>`, `--limit <n>`, `--dry-run` (report without writing), `--json`.
|
|
24
|
+
- Idempotent: re-running skips everything already imported. Never overwrites an existing note.
|
|
25
|
+
- v1 covers Claude Code transcripts; Codex is a follow-up.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
- `session-start.mjs` now guards its `main()` behind the standard `import.meta.url` check (like
|
|
29
|
+
`session-stop.mjs`) so its note-building helpers can be imported by `import`/tests without
|
|
30
|
+
running the hook. No behavioral change when invoked as a hook.
|
|
31
|
+
|
|
32
|
+
## [0.15.0] — 2026-07-06
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
- **`wendkeep cost --top [N]`** — the N priciest sessions (cost incl. subagents · date · file),
|
|
36
|
+
most expensive first (default 10). Spot where the money went. `cost --json` now also carries
|
|
37
|
+
the per-session `sessions` list.
|
|
38
|
+
|
|
7
39
|
## [0.14.0] — 2026-07-06
|
|
8
40
|
|
|
9
41
|
### Changed
|
|
@@ -283,6 +315,7 @@ Initial release — the capture engine, extracted from a system in daily product
|
|
|
283
315
|
- `wendkeep init` (cross-platform installer) + optional `@bitbonsai/mcpvault` MCP server.
|
|
284
316
|
|
|
285
317
|
<!-- Only v0.4.0+ is tagged in git (history starts here); older versions link to npm. -->
|
|
318
|
+
[0.15.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.15.0
|
|
286
319
|
[0.14.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.14.0
|
|
287
320
|
[0.13.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.13.0
|
|
288
321
|
[0.12.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.12.0
|
package/README.md
CHANGED
|
@@ -95,12 +95,27 @@ No re‑copying, no snapshot to re‑sync — the package is the single source o
|
|
|
95
95
|
| `wendkeep spec <sub>` | Living specs: `list` / `show <capability>`. |
|
|
96
96
|
| `wendkeep sensors <sub>` | `list` / `add <id> "<command>"` — view/edit `wendkeep.sensors.json` (JSON Schema shipped). |
|
|
97
97
|
| `wendkeep cost [--since d]` | Aggregate AI-coding spend across the vault's sessions — total, by model, by day (`--json`). |
|
|
98
|
+
| `wendkeep import [opts]` | **Retroactive memory** — backfill past Claude sessions into the vault (deduped by `session_id`). `--from <dir>` / `--since d` / `--limit n` / `--dry-run` / `--json`. |
|
|
98
99
|
| `wendkeep lesson add "t" "l"` | Record a project-local lesson (injected at the next SessionStart). |
|
|
99
100
|
| `wendkeep sync-defs` | Copy `.brain/agents\|skills` into the project (`.codex/agents`, `.claude/skills`). |
|
|
100
101
|
| `wendkeep validate-memory [path]` | Validate `.brain/CORE.md` (cap 25, 3 sections, no secrets/PII). |
|
|
101
102
|
| `wendkeep doctor [--vault P]` | Run a vault health check (integrity of sessions, registry, links). |
|
|
102
103
|
| `wendkeep --version` / `--help` | Version / usage. |
|
|
103
104
|
|
|
105
|
+
## Retroactive memory (`import`)
|
|
106
|
+
|
|
107
|
+
Install wendkeep into an existing project and it only remembers sessions **from now on**. `wendkeep import` fixes that: it reads the project's past Claude Code transcripts (`~/.claude/projects/<slug>/*.jsonl`) and rebuilds each one as a full session note in its **real** date folder — frontmatter, one iteration block per turn, cost + subagent telemetry, derived decision/bug/learning notes, finalized closing. It is an offline replay of the live capture flow, so an imported note is indistinguishable from a captured one.
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
wendkeep import --vault .myproject-vault --dry-run # preview what would be imported
|
|
111
|
+
wendkeep import --vault .myproject-vault # write the notes
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
- **Deduped** by `session_id` against the vault's `SESSION_REGISTRY` — only sessions not already present are imported, and it never overwrites an existing note. Re-running is a no-op.
|
|
115
|
+
- **`--from <dir>`** points at the `.claude/projects/<slug>/` folder explicitly (use it if the auto-derived path misses). Also: `--since <date>`, `--limit <n>`, `--json`.
|
|
116
|
+
- Once imported, `wendkeep cost` aggregates your entire history — retroactively.
|
|
117
|
+
- v1 covers Claude Code; Codex import is a follow-up.
|
|
118
|
+
|
|
104
119
|
## Change lifecycle — the a2 loop (spec‑driven, native)
|
|
105
120
|
|
|
106
121
|
Beyond capturing sessions, wendkeep is a **harness**: a native, zero‑dependency loop that keeps *intent* (specs), *work* (changes) and *proof* (sensors) together in the vault, wikilinked into the Obsidian graph.
|
package/bin/wendkeep.mjs
CHANGED
|
@@ -47,7 +47,9 @@ 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 [
|
|
50
|
+
wendkeep cost [opts] Aggregate AI-coding spend across the vault's sessions.
|
|
51
|
+
wendkeep import [opts] Backfill: import this project's past Claude sessions into the vault.
|
|
52
|
+
--since <date> · --top [N] (priciest sessions) · --json.
|
|
51
53
|
wendkeep verify [--change s] Run a change's task sensors + record evidence (the gate).
|
|
52
54
|
wendkeep lesson add "t" "l" Record a project-local lesson (injected at SessionStart).
|
|
53
55
|
wendkeep validate-memory [path] Validate .brain/CORE.md against the compaction
|
|
@@ -136,6 +138,11 @@ async function main() {
|
|
|
136
138
|
runCost(rest);
|
|
137
139
|
break;
|
|
138
140
|
}
|
|
141
|
+
case 'import': {
|
|
142
|
+
const { runImportCli } = await import('../src/import.mjs');
|
|
143
|
+
runImportCli(rest);
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
139
146
|
case '--version':
|
|
140
147
|
case '-v':
|
|
141
148
|
process.stdout.write(`${version()}\n`);
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Retroactive memory: import Claude Code transcripts that predate wendkeep into the vault.
|
|
3
|
+
// Each `.claude/projects/<slug>/<session_id>.jsonl` becomes a full session note in its real
|
|
4
|
+
// dated folder, deduped by session_id against the vault's SESSION_REGISTRY. This is an offline
|
|
5
|
+
// replay of the live capture flow — same skeleton, same iteration blocks, same cost/subagent
|
|
6
|
+
// telemetry, same finalize — so an imported note is indistinguishable from a captured one.
|
|
7
|
+
import { existsSync, readdirSync, writeFileSync } from 'fs';
|
|
8
|
+
import { basename, join } from 'path';
|
|
9
|
+
import {
|
|
10
|
+
parseTranscript,
|
|
11
|
+
buildIterationBlock,
|
|
12
|
+
insertIteration,
|
|
13
|
+
finalizeSessionFile,
|
|
14
|
+
mergeCreatedNotes,
|
|
15
|
+
findLinkedDerivedNotes,
|
|
16
|
+
} from './session-stop.mjs';
|
|
17
|
+
import { buildSessionContent, allocateSessionPath } from './session-start.mjs';
|
|
18
|
+
import { createLinkedNotes } from './linked-notes.mjs';
|
|
19
|
+
import { updateSessionUsage } from './token-usage.mjs';
|
|
20
|
+
import { upsertSubagentUsage } from './subagent-usage.mjs';
|
|
21
|
+
import { readSessionRegistry, upsertSessionRegistry, formatLocalIso, formatDate } from './obsidian-common.mjs';
|
|
22
|
+
|
|
23
|
+
// Claude encodes a project's absolute path as its `.claude/projects` dir name by replacing each
|
|
24
|
+
// path separator and the drive colon with '-'. `C:\GitHub\WendKeep` -> `C--GitHub-WendKeep`.
|
|
25
|
+
// Note: each char maps 1:1 (no collapsing), so `C:\` yields `C--`.
|
|
26
|
+
export function claudeProjectSlug(projectPath) {
|
|
27
|
+
return String(projectPath || '').replace(/[\\/:]/g, '-');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function homeDir() {
|
|
31
|
+
return process.env.USERPROFILE || process.env.HOME || '';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function defaultClaudeProjectsDir(projectPath) {
|
|
35
|
+
const home = homeDir();
|
|
36
|
+
if (!home) return '';
|
|
37
|
+
return join(home, '.claude', 'projects', claudeProjectSlug(projectPath));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// List candidate transcripts. The Claude session_id IS the filename, so we get it without
|
|
41
|
+
// parsing — cheap dedup for the (usually large) set of already-captured sessions.
|
|
42
|
+
export function discoverTranscripts(projectPath, fromDir) {
|
|
43
|
+
const dir = fromDir || defaultClaudeProjectsDir(projectPath);
|
|
44
|
+
if (!dir || !existsSync(dir)) return { dir, transcripts: [] };
|
|
45
|
+
const transcripts = readdirSync(dir)
|
|
46
|
+
.filter((name) => /\.jsonl$/i.test(name))
|
|
47
|
+
.map((name) => ({ path: join(dir, name), sessionId: basename(name, '.jsonl') }));
|
|
48
|
+
return { dir, transcripts };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Session objective for the note title/frontmatter: first real user prompt, one line.
|
|
52
|
+
function deriveSummary(tx) {
|
|
53
|
+
for (const turn of tx.turns || []) {
|
|
54
|
+
const prompt = (turn.userPrompts || []).find(Boolean);
|
|
55
|
+
if (prompt) {
|
|
56
|
+
return prompt.replace(/[\r\n#]+/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 80) || 'session';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return 'session';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function toDate(ts, fallback) {
|
|
63
|
+
const d = ts ? new Date(ts) : null;
|
|
64
|
+
return d && Number.isFinite(d.getTime()) ? d : fallback;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Replay one transcript into a fresh, finalized note. Returns {sessionId, relPath, turns} or
|
|
68
|
+
// null when the transcript has no memorializable turns. Assumes the caller already deduped.
|
|
69
|
+
export function importSession(vaultBase, txPath, preParsed) {
|
|
70
|
+
const tx = preParsed || parseTranscript(txPath);
|
|
71
|
+
const turns = tx.turns || [];
|
|
72
|
+
if (!turns.length) return null;
|
|
73
|
+
|
|
74
|
+
const sessionId = tx.sessionId || basename(String(txPath), '.jsonl');
|
|
75
|
+
const startDate = toDate(turns[0].timestamp, new Date());
|
|
76
|
+
const endDate = toDate(turns.at(-1).timestamp, startDate);
|
|
77
|
+
const summary = deriveSummary(tx);
|
|
78
|
+
|
|
79
|
+
// Skeleton in the real dated folder.
|
|
80
|
+
const { absPath, relPath } = allocateSessionPath(vaultBase, startDate, summary);
|
|
81
|
+
writeFileSync(absPath, buildSessionContent({ relPath, now: startDate, summary }), 'utf-8');
|
|
82
|
+
|
|
83
|
+
// One iteration block per turn (insertIteration dedups by turn marker -> re-import safe).
|
|
84
|
+
for (const turn of turns) {
|
|
85
|
+
const block = buildIterationBlock(tx, { turn_id: turn.turnId, now: turn.timestamp });
|
|
86
|
+
insertIteration(absPath, block, turn.turnId, tx);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Cost + subagent telemetry, exactly like the live Stop hook. Fail-open.
|
|
90
|
+
try {
|
|
91
|
+
updateSessionUsage({ vaultBase, sessionRel: relPath, sessionPath: absPath, transcriptPath: txPath });
|
|
92
|
+
} catch { /* usage is best-effort */ }
|
|
93
|
+
try {
|
|
94
|
+
upsertSubagentUsage(absPath, txPath);
|
|
95
|
+
} catch { /* subagent telemetry is best-effort */ }
|
|
96
|
+
|
|
97
|
+
// Finalize: derived notes + closing section + ended_at from the last turn.
|
|
98
|
+
const endedAt = formatLocalIso(endDate);
|
|
99
|
+
const created = mergeCreatedNotes(
|
|
100
|
+
createLinkedNotes(vaultBase, formatDate(endDate), relPath, tx),
|
|
101
|
+
findLinkedDerivedNotes(vaultBase, relPath),
|
|
102
|
+
);
|
|
103
|
+
finalizeSessionFile(absPath, tx, created, endedAt);
|
|
104
|
+
|
|
105
|
+
upsertSessionRegistry(vaultBase, sessionId, {
|
|
106
|
+
session_file: relPath,
|
|
107
|
+
status: 'done',
|
|
108
|
+
started_at: formatLocalIso(startDate),
|
|
109
|
+
ended_at: endedAt,
|
|
110
|
+
last_turn_id: turns.at(-1).turnId,
|
|
111
|
+
transcript_path: txPath,
|
|
112
|
+
imported: true,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return { sessionId, relPath, turns: turns.length };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Import every not-yet-captured transcript. Deduped by session_id against the registry.
|
|
119
|
+
// Options: { projectPath, from, since (ISO/date), limit, dryRun }.
|
|
120
|
+
export function runImport(vaultBase, opts = {}) {
|
|
121
|
+
const { projectPath = process.cwd(), from = '', since = '', limit = 0, dryRun = false } = opts;
|
|
122
|
+
const { dir, transcripts } = discoverTranscripts(projectPath, from);
|
|
123
|
+
const captured = new Set(Object.keys(readSessionRegistry(vaultBase).sessions || {}));
|
|
124
|
+
const sinceMs = since ? Date.parse(since) : 0;
|
|
125
|
+
const report = { dir, scanned: transcripts.length, imported: 0, skipped: 0, errors: [], sessions: [] };
|
|
126
|
+
|
|
127
|
+
let done = 0;
|
|
128
|
+
for (const t of transcripts) {
|
|
129
|
+
if (captured.has(t.sessionId)) { report.skipped++; continue; }
|
|
130
|
+
if (limit && done >= limit) break;
|
|
131
|
+
|
|
132
|
+
let tx;
|
|
133
|
+
try {
|
|
134
|
+
tx = parseTranscript(t.path);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
report.errors.push({ sessionId: t.sessionId, error: error.message });
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const turns = tx.turns || [];
|
|
140
|
+
if (!turns.length) { report.skipped++; continue; }
|
|
141
|
+
|
|
142
|
+
const startTs = turns[0].timestamp || '';
|
|
143
|
+
if (sinceMs && startTs && Number.isFinite(Date.parse(startTs)) && Date.parse(startTs) < sinceMs) {
|
|
144
|
+
report.skipped++;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (dryRun) {
|
|
149
|
+
report.sessions.push({ sessionId: t.sessionId, turns: turns.length, startTs, dryRun: true });
|
|
150
|
+
report.imported++;
|
|
151
|
+
done++;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const r = importSession(vaultBase, t.path, tx);
|
|
157
|
+
if (r) { report.sessions.push(r); report.imported++; done++; }
|
|
158
|
+
else { report.skipped++; }
|
|
159
|
+
} catch (error) {
|
|
160
|
+
report.errors.push({ sessionId: t.sessionId, error: error.message });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return report;
|
|
165
|
+
}
|
package/hooks/session-start.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync, writeFileSync } from 'fs';
|
|
3
3
|
import { basename, join } from 'path';
|
|
4
|
+
import { pathToFileURL } from 'url';
|
|
4
5
|
import {
|
|
5
6
|
controlPath,
|
|
6
7
|
ensureDir,
|
|
@@ -30,7 +31,7 @@ import {
|
|
|
30
31
|
yamlQuote,
|
|
31
32
|
} from './obsidian-common.mjs';
|
|
32
33
|
|
|
33
|
-
function buildSessionContent({ relPath, now, summary = 'session' }) {
|
|
34
|
+
export function buildSessionContent({ relPath, now, summary = 'session' }) {
|
|
34
35
|
const date = formatDate(now);
|
|
35
36
|
const startedAt = formatLocalIso(now);
|
|
36
37
|
const titleTime = formatTime(now).slice(0, 5);
|
|
@@ -112,7 +113,7 @@ Sessão ainda em andamento.
|
|
|
112
113
|
`;
|
|
113
114
|
}
|
|
114
115
|
|
|
115
|
-
function allocateSessionPath(vaultBase, now, summary = 'session') {
|
|
116
|
+
export function allocateSessionPath(vaultBase, now, summary = 'session') {
|
|
116
117
|
const folderRel = sessionFolderRel(now, vaultBase);
|
|
117
118
|
const folderAbs = join(vaultBase, folderRel);
|
|
118
119
|
ensureDir(folderAbs);
|
|
@@ -301,11 +302,13 @@ function main() {
|
|
|
301
302
|
});
|
|
302
303
|
}
|
|
303
304
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
305
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
306
|
+
try {
|
|
307
|
+
main();
|
|
308
|
+
} catch (error) {
|
|
309
|
+
process.stderr.write(`[codex-obsidian] SessionStart falhou: ${error.message}\n`);
|
|
310
|
+
writeHookOutput({
|
|
311
|
+
systemMessage: `[codex-obsidian] Não foi possível criar a sessão Obsidian: ${error.message}`,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
311
314
|
}
|
package/hooks/session-stop.mjs
CHANGED
|
@@ -781,7 +781,7 @@ function noteReferencesSession(content, sessionRel) {
|
|
|
781
781
|
return content.includes(sessionRel) || content.includes(sessionLink);
|
|
782
782
|
}
|
|
783
783
|
|
|
784
|
-
function findLinkedDerivedNotes(vaultBase, sessionRel) {
|
|
784
|
+
export function findLinkedDerivedNotes(vaultBase, sessionRel) {
|
|
785
785
|
const linked = { decisions: [], bugs: [], learnings: [] };
|
|
786
786
|
const locF = getLocale(vaultBase).folders;
|
|
787
787
|
const folders = {
|
|
@@ -808,7 +808,7 @@ function findLinkedDerivedNotes(vaultBase, sessionRel) {
|
|
|
808
808
|
return linked;
|
|
809
809
|
}
|
|
810
810
|
|
|
811
|
-
function mergeCreatedNotes(created, linked) {
|
|
811
|
+
export function mergeCreatedNotes(created, linked) {
|
|
812
812
|
const merged = { decisions: [], bugs: [], learnings: [] };
|
|
813
813
|
for (const key of Object.keys(merged)) {
|
|
814
814
|
merged[key] = [...new Set([...(created[key] || []), ...(linked[key] || [])])];
|
|
@@ -861,7 +861,7 @@ function replaceClosingSection(content, closing) {
|
|
|
861
861
|
return `${content.slice(0, index).trimEnd()}\n\n${closing}\n`;
|
|
862
862
|
}
|
|
863
863
|
|
|
864
|
-
function finalizeSessionFile(sessionPath, tx, created, endedAt) {
|
|
864
|
+
export function finalizeSessionFile(sessionPath, tx, created, endedAt) {
|
|
865
865
|
const pending = extractPending(tx.rawTextForDetection);
|
|
866
866
|
const links = (items) => items.length ? items.map((rel) => ` - ${wikilinkFromRel(rel)}`).join('\n') : ' - Nenhuma';
|
|
867
867
|
const summary = tx.latestAssistantMessage
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.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
|
@@ -76,9 +76,15 @@ export function collectVaultCost(vaultBase, { since } = {}) {
|
|
|
76
76
|
try { e = parseSessionCost(readFileSync(f, 'utf8')); } catch { continue; }
|
|
77
77
|
if (!e) continue;
|
|
78
78
|
if (since && e.date && e.date < since) continue;
|
|
79
|
+
e.file = f.slice(vaultBase.length + 1).replace(/\\/g, '/');
|
|
79
80
|
entries.push(e);
|
|
80
81
|
}
|
|
81
|
-
|
|
82
|
+
const agg = aggregateCosts(entries);
|
|
83
|
+
// Per-session list (main+sub), most expensive first — powers `cost --top`.
|
|
84
|
+
agg.sessions = entries
|
|
85
|
+
.map((e) => ({ file: e.file, date: e.date, cost: round4(e.mainCost + e.subCost) }))
|
|
86
|
+
.sort((a, b) => b.cost - a.cost);
|
|
87
|
+
return agg;
|
|
82
88
|
}
|
|
83
89
|
|
|
84
90
|
function opt(argv, name) {
|
|
@@ -97,6 +103,14 @@ export function runCost(argv) {
|
|
|
97
103
|
|
|
98
104
|
if (argv.includes('--json')) { process.stdout.write(`${JSON.stringify(agg, null, 2)}\n`); process.exit(0); }
|
|
99
105
|
|
|
106
|
+
const topIdx = argv.indexOf('--top');
|
|
107
|
+
if (topIdx >= 0) {
|
|
108
|
+
const n = Number(argv[topIdx + 1]) || 10;
|
|
109
|
+
process.stdout.write(`Sessões mais caras (top ${n} de ${agg.count}):\n`);
|
|
110
|
+
for (const s of agg.sessions.slice(0, n)) process.stdout.write(` ${usd(s.cost).padStart(11)} ${s.date || '?'} ${s.file}\n`);
|
|
111
|
+
process.exit(0);
|
|
112
|
+
}
|
|
113
|
+
|
|
100
114
|
process.stdout.write(`Custo total (vault): ${usd(agg.total)} — ${agg.count} sessão(ões) · ${usd(agg.avg)}/sessão\n`);
|
|
101
115
|
process.stdout.write(` main: ${usd(agg.main)} · subagents: ${usd(agg.sub)}\n`);
|
|
102
116
|
if (agg.wasted) process.stdout.write(` desperdiçado (runs killed/failed): ${usd(agg.wasted)}\n`);
|
package/src/import.mjs
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// `wendkeep import` — retroactive memory. Scans this project's Claude Code transcripts and
|
|
2
|
+
// turns every session that isn't already in the vault into a full, dated session note (deduped
|
|
3
|
+
// by session_id). One command backfills your whole history: cost, subagents, iterations.
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
6
|
+
import { runImport, defaultClaudeProjectsDir } from '../hooks/import-sessions.mjs';
|
|
7
|
+
|
|
8
|
+
function opt(argv, name) {
|
|
9
|
+
const i = argv.indexOf(name);
|
|
10
|
+
if (i >= 0) return argv[i + 1];
|
|
11
|
+
const eq = argv.find((a) => a.startsWith(`${name}=`));
|
|
12
|
+
return eq ? eq.slice(name.length + 1) : undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function runImportCli(argv) {
|
|
16
|
+
const vaultRaw = opt(argv, '--vault') || process.env.OBSIDIAN_VAULT_PATH;
|
|
17
|
+
if (!vaultRaw) { process.stderr.write('wendkeep import: no vault (--vault or OBSIDIAN_VAULT_PATH).\n'); process.exit(2); }
|
|
18
|
+
const vaultBase = isAbsolute(vaultRaw) ? vaultRaw : resolve(process.cwd(), vaultRaw);
|
|
19
|
+
if (!existsSync(vaultBase)) { process.stderr.write(`wendkeep import: vault not found: ${vaultBase}\n`); process.exit(2); }
|
|
20
|
+
|
|
21
|
+
const projectRaw = opt(argv, '--project') || process.cwd();
|
|
22
|
+
const projectPath = isAbsolute(projectRaw) ? projectRaw : resolve(process.cwd(), projectRaw);
|
|
23
|
+
const from = opt(argv, '--from') || '';
|
|
24
|
+
const since = opt(argv, '--since') || '';
|
|
25
|
+
const limit = Number(opt(argv, '--limit')) || 0;
|
|
26
|
+
const dryRun = argv.includes('--dry-run');
|
|
27
|
+
|
|
28
|
+
const sourceDir = from || defaultClaudeProjectsDir(projectPath);
|
|
29
|
+
if (!from && (!sourceDir || !existsSync(sourceDir))) {
|
|
30
|
+
process.stderr.write(`wendkeep import: no Claude transcripts for this project.\n`);
|
|
31
|
+
process.stderr.write(` Looked in: ${sourceDir || '(home not resolved)'}\n`);
|
|
32
|
+
process.stderr.write(` Pass --from <dir> pointing at .claude/projects/<slug>/ explicitly.\n`);
|
|
33
|
+
process.exit(2);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const report = runImport(vaultBase, { projectPath, from, since, limit, dryRun });
|
|
37
|
+
|
|
38
|
+
if (argv.includes('--json')) { process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); process.exit(0); }
|
|
39
|
+
|
|
40
|
+
const verb = dryRun ? 'importaria' : 'importadas';
|
|
41
|
+
process.stdout.write(`Fonte: ${report.dir}\n`);
|
|
42
|
+
process.stdout.write(`${report.scanned} transcript(s) · ${report.imported} ${verb} · ${report.skipped} já no vault\n`);
|
|
43
|
+
for (const s of report.sessions) {
|
|
44
|
+
const tag = s.dryRun ? '(dry-run)' : '→';
|
|
45
|
+
process.stdout.write(` ${tag} ${s.sessionId} ${s.turns} turno(s)${s.relPath ? ` ${s.relPath}` : ''}\n`);
|
|
46
|
+
}
|
|
47
|
+
if (report.errors.length) {
|
|
48
|
+
process.stdout.write(`\n${report.errors.length} com erro (pulados):\n`);
|
|
49
|
+
for (const e of report.errors) process.stdout.write(` ${e.sessionId}: ${e.error}\n`);
|
|
50
|
+
}
|
|
51
|
+
if (dryRun) process.stdout.write('\nNada foi escrito (--dry-run). Rode sem --dry-run para criar as notas.\n');
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|