wendkeep 0.13.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 CHANGED
@@ -4,6 +4,49 @@ 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
+
39
+ ## [0.14.0] — 2026-07-06
40
+
41
+ ### Changed
42
+ - **dotcontext is no longer a default companion.** wendkeep's native a2 loop (`change` /
43
+ `verify` / gate) recreates dotcontext's execution/gate role, so pinning it duplicates the
44
+ harness. The interactive / `--yes` default is now **`context-mode` only**; dotcontext stays
45
+ selectable via `--companions dotcontext` for anyone already invested.
46
+ - **README:** rewrote "Install & set up" with a clear **`init` options table** and a
47
+ per-companion breakdown; clarified that `--no-mcp` skips **only wendkeep's own** vault MCP
48
+ (companion MCPs still follow `--companions`).
49
+
7
50
  ## [0.13.0] — 2026-07-06
8
51
 
9
52
  Cost intelligence: waste + average.
@@ -272,6 +315,8 @@ Initial release — the capture engine, extracted from a system in daily product
272
315
  - `wendkeep init` (cross-platform installer) + optional `@bitbonsai/mcpvault` MCP server.
273
316
 
274
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
319
+ [0.14.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.14.0
275
320
  [0.13.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.13.0
276
321
  [0.12.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.12.0
277
322
  [0.11.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.11.0
package/README.md CHANGED
@@ -39,20 +39,40 @@ npx wendkeep init
39
39
 
40
40
  1. Create the vault folder taxonomy and a templated `README.md` (default vault: `<project>/.<project-name>-vault`, e.g. `.MyApp-vault`; override with `--vault`).
41
41
  2. **Merge** the three session hooks and `OBSIDIAN_VAULT_PATH` into `.claude/settings.json` — without clobbering your existing settings (a `.bak` is saved; an unparseable file is left untouched and a `.new` is written for you to merge).
42
- 3. Add the `wendkeep-vault` MCP server to `.mcp.json` (skip with `--no-mcp`).
43
- 4. Offer to pin **companion** plugins/MCP (multi-choice; `context-mode` + `dotcontext` pre-checked). Each is wired through the most agent-agnostic path it supports — `context-mode` and `dotcontext` as `.mcp.json` MCP servers (any agent), `understand-anything` via a `understand-inject` SessionStart hook that injects its domain graph when generated, and the Claude Code plugin layer (`extraKnownMarketplaces` + `enabledPlugins`) as a bonus. **`dotcontext`** (execution/PREVC harness) also gets its lifecycle hooks wired (SessionStart-last + Stop + PostToolUse, pinned CLI) and a starter `.context/config/sensors.json` seeded with a `wendkeep validate-memory` sensor plus one per detected `package.json` script (test/typecheck/lint/build) — add the `.context/runtime|cache|logs|plans|agents|skills/` paths to `.gitignore` (wendkeep doesn't touch git). If dotcontext is already configured globally (`~/.claude.json`), the project MCP entry is **skipped automatically** to avoid a duplicate; tune with `--dotcontext-mcp <auto|project|none>` and `--dotcontext-hooks <full|light|none>` (light drops PostToolUse). Control selection with `--companions <csv>` or `--no-companions`. (`caveman` additionally runs its own cross-agent installer on non-Claude agents.)
42
+ 3. Add the **`wendkeep-vault`** MCP server to `.mcp.json` so the agent can read/write the vault. Skip with `--no-mcp` — e.g. when the agent already has a vault MCP. (`--no-mcp` skips *only wendkeep's own* MCP; companion MCPs still follow `--companions`.)
43
+ 4. Offer to pin **companion** plugins/MCP (multi-choice; only `context-mode` pre-checked). Each is wired the most agent-agnostic way it supports:
44
+ - **`context-mode`** — context optimizer + FTS5 memory, as a `.mcp.json` MCP server (any agent). The recommended default.
45
+ - **`understand-anything`** — project domain graph, via a `understand-inject` SessionStart hook that injects the graph when generated.
46
+ - **`caveman`** — token-compression mode; runs its own cross-agent installer on non-Claude agents.
47
+ - **`dotcontext`** — *legacy, not recommended.* wendkeep's native a2 loop (`change` / `verify` / gate) already does its job, so installing it **duplicates the harness**. Still selectable via `--companions dotcontext` for anyone already invested (tune with `--dotcontext-mcp` / `--dotcontext-hooks`), but off by default.
48
+
49
+ Control with `--companions <csv>` or `--no-companions`. The Claude Code plugin layer (`extraKnownMarketplaces` + `enabledPlugins`) is wired as a bonus where the companion has one.
44
50
  5. Install a **color system** into the vault's `.obsidian/`: a CSS snippet that accents notes by type (session/decision/bug/learning, via the `cssclasses` the hooks emit) plus graph color groups by folder. Non-destructive merge into `appearance.json`/`graph.json`; skip with `--no-colors`.
45
51
  6. Seed the **curated memory layer**: `.brain/CORE.md` (the hand-curated hot layer, with the 3 required sections) and `.brain/COMPACTION_PROTOCOL.md` (the protocol guide). The auto layers (`DIGEST.md`, `index.jsonl`) are generated by the hooks. Validate the curated layer with `wendkeep validate-memory` (cap 25 lines, 3 sections, no secrets/PII).
46
52
  7. Seed the **definitions + skills layer**: `.brain/agents/` + `.brain/skills/` (versioned source of truth), including the native process skills `wk-workflow` / `wk-tdd` / `wk-debugging` / `wk-brainstorming` / `wk-planning`. `init` runs `wendkeep sync-defs` for you, delivering them to `.codex/agents/` + `.claude/skills/` (re‑run `sync-defs` after editing `.brain`).
47
53
  8. Seed the **change/spec lifecycle**: the `07-Specs/` + `08-Mudanças/` folders and a native `wendkeep.sensors.json` (a `validate-memory` sensor plus one per detected `package.json` script). Drives `wendkeep change` / `wendkeep verify` — see **Change lifecycle** below.
48
54
 
49
55
  ```bash
50
- npx wendkeep init --vault "~/vaults/work" --project . --yes # non-interactive (companions: context-mode,dotcontext)
51
- npx wendkeep init --companions "context-mode,caveman" --yes
52
- npx wendkeep init --no-companions --yes
56
+ npx wendkeep init --vault "~/vaults/work" --project . --yes # non-interactive (default companion: context-mode)
57
+ npx wendkeep init --companions "context-mode,understand-anything" --yes
58
+ npx wendkeep init --no-companions --no-mcp --yes # zero companions, no wendkeep MCP
53
59
  ```
54
60
 
55
- Then open the vault in Obsidian, send a test prompt in your agent, and confirm a note appears under `02-Sessões/…`.
61
+ ### `init` options
62
+
63
+ | Flag | What it does |
64
+ |---|---|
65
+ | `--vault <path>` | Vault folder. Default `<project>/.<project-name>-vault`; interactive init asks. Point it at an existing vault to install into it. |
66
+ | `--project <path>` | Project root to wire (default: current directory). |
67
+ | `--locale <pt-BR\|en>` | Vault language — folder names, scaffold, skills. Interactive init asks; locked at init. |
68
+ | `--companions <csv>` | Companions to pin: `context-mode,caveman,understand-anything` (default: `context-mode`; `dotcontext` is legacy). |
69
+ | `--no-companions` | Pin no companions. |
70
+ | `--no-mcp` | Skip **wendkeep's own** vault MCP (`wendkeep-vault`). Companion MCPs still follow `--companions`. |
71
+ | `--no-colors` | Skip the Obsidian color system (`.obsidian` snippet + graph groups). |
72
+ | `--yes`, `-y` | Non-interactive; accept defaults (skips the language / vault / companion prompts). |
73
+ | `--force` | Overwrite existing wendkeep config blocks. |
74
+
75
+ Then open the vault in Obsidian, send a test prompt in your agent, and confirm a note appears under `02-Sessões/…` (or `02-Sessions/…` for an `en` vault).
56
76
 
57
77
  ## Updating
58
78
 
@@ -75,12 +95,27 @@ No re‑copying, no snapshot to re‑sync — the package is the single source o
75
95
  | `wendkeep spec <sub>` | Living specs: `list` / `show <capability>`. |
76
96
  | `wendkeep sensors <sub>` | `list` / `add <id> "<command>"` — view/edit `wendkeep.sensors.json` (JSON Schema shipped). |
77
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`. |
78
99
  | `wendkeep lesson add "t" "l"` | Record a project-local lesson (injected at the next SessionStart). |
79
100
  | `wendkeep sync-defs` | Copy `.brain/agents\|skills` into the project (`.codex/agents`, `.claude/skills`). |
80
101
  | `wendkeep validate-memory [path]` | Validate `.brain/CORE.md` (cap 25, 3 sections, no secrets/PII). |
81
102
  | `wendkeep doctor [--vault P]` | Run a vault health check (integrity of sessions, registry, links). |
82
103
  | `wendkeep --version` / `--help` | Version / usage. |
83
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
+
84
119
  ## Change lifecycle — the a2 loop (spec‑driven, native)
85
120
 
86
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
@@ -29,8 +29,8 @@ Usage:
29
29
  --vault <path> Obsidian vault folder (default: <project>/.<project-name>-vault).
30
30
  --project <path> Project root to wire (default: current directory).
31
31
  --no-mcp Do not add the mcpvault MCP server to .mcp.json.
32
- --companions <csv> Companion plugins/MCP to pin: context-mode,dotcontext,caveman,understand-anything
33
- (default when interactive/--yes: context-mode,dotcontext).
32
+ --companions <csv> Companion plugins/MCP to pin: context-mode,caveman,understand-anything
33
+ (default: context-mode). dotcontext is legacy — the native a2 loop replaces it.
34
34
  --no-companions Skip companion plugins/MCP entirely.
35
35
  --no-colors Skip the Obsidian color system (.obsidian snippet + graph groups).
36
36
  --dotcontext-mcp <v> dotcontext MCP placement: auto (default; skip project entry
@@ -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 [--since d] Aggregate AI-coding spend across the vault's sessions (--json).
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
+ }
@@ -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
- try {
305
- main();
306
- } catch (error) {
307
- process.stderr.write(`[codex-obsidian] SessionStart falhou: ${error.message}\n`);
308
- writeHookOutput({
309
- systemMessage: `[codex-obsidian] Não foi possível criar a sessão Obsidian: ${error.message}`,
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
  }
@@ -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.13.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
- return aggregateCosts(entries);
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
+ }
package/src/taxonomy.mjs CHANGED
@@ -43,6 +43,7 @@ export const HOOK_FILES = [
43
43
  'brain-recall.mjs',
44
44
  'brain-reindex.mjs',
45
45
  'session-backfill.mjs',
46
+ 'import-sessions.mjs',
46
47
  'vault-health.mjs',
47
48
  'understand-inject.mjs',
48
49
  ];
@@ -124,8 +125,11 @@ export const COMPANIONS = [
124
125
  },
125
126
  {
126
127
  id: 'dotcontext',
127
- label: 'dotcontext orquestração de execução (PREVC + sensores + gate E→V)',
128
- default: true,
128
+ // Legacy: wendkeep's native a2 loop (change/verify/gate) recreates dotcontext's role,
129
+ // so it is no longer recommended — installing it would duplicate the native harness.
130
+ // Kept selectable for anyone already invested in it; not a default.
131
+ label: 'dotcontext — legado (o loop a2 nativo do wendkeep substitui; não recomendado)',
132
+ default: false,
129
133
  // MCP-only (no Claude Code plugin). Agent-agnostic server, @latest surface.
130
134
  mcp: { key: 'dotcontext', entry: { type: 'stdio', command: 'npx', args: ['-y', '@dotcontext/mcp@latest'], env: {} } },
131
135
  // Lifecycle hooks via the pinned CLI; wired by wendkeep (single writer).