wendkeep 0.25.0 → 0.27.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,41 @@ 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.27.0] — 2026-07-08
8
+
9
+ ### Fixed
10
+ - **Re-running `wendkeep init` no longer re-asks for the vault (or language) — and can't split your
11
+ data.** On a project already set up, init now reads the registered vault from
12
+ `.claude/settings.json` (`OBSIDIAN_VAULT_PATH`) and the locked locale from the vault's
13
+ `.brain/config.json`, reuses both, and skips the prompts. Previously a re-run (e.g. after
14
+ `npm i -D wendkeep@latest`) offered the *derived* default (`.<project>-vault`); accepting it — or
15
+ mistyping the name — created a **second, divergent vault**. `--vault` / `--locale` still override.
16
+ New exported `detectRegisteredVault()` / `readVaultLocale()`. `src/init.mjs`.
17
+
18
+ ### Note
19
+ - You do **not** need `wendkeep init` for a routine update: the hooks live in the package
20
+ (`settings.json` calls `npx wendkeep hook …`), so `npm i -D wendkeep@latest` updates them.
21
+ Re-run `init` only when a release adds new wiring (the CHANGELOG says so); it's idempotent.
22
+
23
+ ## [0.26.0] — 2026-07-08
24
+
25
+ ### Fixed
26
+ - **`wendkeep init` output now follows the chosen vault language.** Picking Português left the
27
+ whole summary + `[n/4]` steps + "Next steps" block in English; only the interactive prompts were
28
+ localized. All init output is now driven by a locale message set (pt-BR / en) resolved from the
29
+ language answer — "Próximos passos", "taxonomia do vault", "sensores semeados", etc. `src/init.mjs`.
30
+
31
+ ## [0.25.1] — 2026-07-08
32
+
33
+ ### Added
34
+ - **Landing page in the repo**: a static SVG hero (`docs/assets/wendkeep-hero.svg`, the knowledge
35
+ graph) embedded at the top of the README, plus the self-contained interactive landing at
36
+ `docs/index.html` (live Canvas graph; serve `docs/` via GitHub Pages for a public URL).
37
+
38
+ ### Changed
39
+ - `wendkeep stats` now says **"N dias ativos (first→last)"** — the count is distinct days *with
40
+ activity*, not the calendar span; the old "N dia(s)" read as calendar days.
41
+
7
42
  ## [0.25.0] — 2026-07-08
8
43
 
9
44
  Cost trend/projection + shareable stats + launch assets.
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # wendkeep
2
2
 
3
+ [Português](README.pt-BR.md) · **English**
4
+
3
5
  > **Your AI coding agent forgets every session. wendkeep makes it remember — in the Obsidian vault you already use.**
4
6
 
5
7
  [![npm](https://img.shields.io/npm/v/wendkeep.svg)](https://www.npmjs.com/package/wendkeep)
@@ -7,6 +9,10 @@
7
9
  ![zero deps](https://img.shields.io/badge/runtime%20deps-0-brightgreen)
8
10
  ![node](https://img.shields.io/badge/node-%E2%89%A518-blue)
9
11
 
12
+ [![wendkeep — persistent memory for AI coding agents, shown as a knowledge graph of sessions, decisions, bugs, learnings and changes](docs/assets/wendkeep-hero.svg)](docs/index.html)
13
+
14
+ **In the graph:** 🔵 session · 🟣 decision · 🔴 bug · 🟢 learning · 🟡 change — every note, backlinked.
15
+
10
16
  **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.
11
17
 
12
18
  ```bash
@@ -14,24 +20,28 @@ npm i -D wendkeep && npx wendkeep init # captures from the next session on
14
20
  npx wendkeep import # backfill past Claude + Codex sessions
15
21
  ```
16
22
 
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. -->
23
+ **▶ Interactive demo:** [`docs/index.html`](docs/index.html) a self-contained page with the live knowledge‑graph hero. Open it locally (or serve `docs/` on any static host). The image above is a static render of it.
24
+
25
+ > **From one real production vault** (`npx wendkeep stats`): **308** sessions · **1,696** prompts · **$4,836** captured across **46 active days** (Jan–Jul 2026) · **15** models — every one a note in the graph.
26
+
27
+ <!-- Optional: drop a real Obsidian graph screenshot at docs/assets/graph.png and add it here (see docs/21-graph-screenshot.md). -->
18
28
 
19
29
  > 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.
20
30
 
21
31
  ---
22
32
 
23
- ## Why
24
-
25
- The pieces to give a coding agent durable memory exist, but fragmented (qmd‑sessions, memsearch, Nexus, hand‑written hooks). wendkeep ships them as one turnkey package that lands the memory **inside the Obsidian graph you already use** — no manual setup, no snapshot to keep in sync.
33
+ ## The problem: the context dies when the window closes
26
34
 
27
- ## What you get
35
+ Decisions, dead ends, the reason you chose X over Y — gone next session. The pieces to fix that exist but are scattered (qmd‑sessions, memsearch, Nexus, hand‑written hooks). wendkeep ships them as one turnkey package that writes into a knowledge graph **inside the Obsidian vault you already use** — no manual setup, no snapshot to keep in sync.
28
36
 
29
- - **Automatic session capture** — `SessionStart` / `UserPromptSubmit` / `Stop` hooks write each session to `02-Sessões/<year>/<month>/DIA <dd>/` as Markdown with YAML frontmatter, turn‑by‑turn iterations, and wikilinks.
30
- - **Multi‑agent** — detects the real provider (Claude Code, Codex, Copilot) at runtime; one install covers all.
31
- - **Token & cost tracking** — per‑model, cache‑aware pricing (`pricing.json`) with a per‑session usage table.
32
- - **Auto‑extracted derived notes** — decisions (ADR‑style), bugs and learnings pulled from the transcript into `04-Decisões/`, `05-Bugs/`, `06-Aprendizados/`, backlinked to the session.
33
- - **Curated memory** — a cold frontmatter index (`.brain/`) plus a budget‑capped `CORE` + `DIGEST` injected at the next `SessionStart`.
34
- - **Local‑first** — everything is plain Markdown on your disk. The optional MCP server (`@bitbonsai/mcpvault`) lets the agent read/write the vault; no cloud, no account.
37
+ | | |
38
+ |---|---|
39
+ | **Capture** — every turn, on disk | `SessionStart` / `Stop` hooks write each session to a dated Markdown note: prompts, iterations, files touched, wikilinks. |
40
+ | **Derive** — decisions, bugs, learnings | Pulled from the transcript into their own notes, backlinked to the session. Your history becomes navigable, not archival. |
41
+ | **Recall** — injected back | A budget‑capped `CORE` + `DIGEST` and the active change are fed to the agent at the next `SessionStart`. It resumes where it left off. |
42
+ | **Cost** — what it all cost | Per‑model, cache‑aware token pricing per session — plus `cost --trend` with a run‑rate projection across the whole vault. |
43
+ | **Multi‑agent** — one install, all agents | Detects the real provider (Claude Code, Codex, Copilot) at runtime. |
44
+ | **Local‑first** — no cloud, no account | Everything is plain Markdown on your disk. An optional MCP server (`@bitbonsai/mcpvault`) lets the agent read/write the vault. |
35
45
 
36
46
  ## Requirements
37
47
 
@@ -114,9 +124,9 @@ No re‑copying, no snapshot to re‑sync — the package is the single source o
114
124
  | `wendkeep doctor [--vault P]` | Run a vault health check (integrity of sessions, registry, links). |
115
125
  | `wendkeep --version` / `--help` | Version / usage. |
116
126
 
117
- ## Retroactive memory (`import`)
127
+ ## Retroactive memory (`import`) — install today, remember yesterday
118
128
 
119
- 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 and Codex** transcripts and rebuilds each one as a full session note in its **real** date folder — frontmatter (tagged with the transcript's real provider), 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.
129
+ Install wendkeep into an existing project and it only remembers sessions **from now on**. `wendkeep import` fixes that: one command backfills your project's past **Claude & Codex** sessions into the vault — deduped, dated, with cost — so the graph starts full, not empty. It rebuilds each transcript as a full session note in its **real** date folder — frontmatter (tagged with the transcript's real provider), one iteration block per turn, cost + subagent telemetry, derived decision/bug/learning notes, finalized closing. An offline replay of the live capture flow, so an imported note is indistinguishable from a captured one.
120
130
 
121
131
  ```bash
122
132
  wendkeep import --vault .myproject-vault --dry-run # preview what would be imported (both agents)
@@ -143,6 +153,8 @@ explore → propose → apply (TDD) → verify → archive
143
153
  - **Verify** — `wendkeep verify` runs the sensors your tasks declared (from `wendkeep.sensors.json` at the project root) and records `evidencia.json`. A critical red fails the gate; a `warning` red is advisory.
144
154
  - **Archive** — `wendkeep change archive <slug>` **gates** on the evidence (blocks unless every declared critical sensor is green), promotes each capability's spec delta (`ADDED`/`MODIFIED`/`REMOVED`) into the living `07-Specs/<capability>.md`, moves the change to `_arquivo/`, and mints an ADR in `04-Decisões/`.
145
155
 
156
+ > The gate blocks unless the scaffold is filled, no task is open, evidence is fresh, and every declared requirement is covered. **`--force` is the human's call — never the agent's.**
157
+
146
158
  `wendkeep init` also seeds **native process skills** (`wk-workflow`, `wk-tdd`, `wk-debugging`, `wk-brainstorming`, `wk-planning`, `wk-verify`) into `.brain/skills` and delivers them to `.claude/skills` — the *how* layer, zero‑dep. Optional companions (`context-mode`, `dotcontext`, `understand-anything`, `caveman`) remain an opt‑in extra layer.
147
159
 
148
160
  ### The loop in five minutes
@@ -193,6 +205,16 @@ The archive **gate** blocks unless: the change scaffold is filled (G0), no task
193
205
  - **Transcript formats are agent‑internal** and can change between agent versions; parsing is isolated but may need updates.
194
206
  - Installer wires **Claude Code** settings + `.mcp.json`. Codex hooks run on the same scripts but are not auto‑wired yet (import already covers past Codex sessions via `--source codex`).
195
207
 
208
+ ---
209
+
210
+ ## Stop re‑explaining your codebase every morning
211
+
212
+ ```bash
213
+ npm i -D wendkeep && npx wendkeep init
214
+ ```
215
+
216
+ **[Install from npm](https://www.npmjs.com/package/wendkeep)** · **[Star on GitHub](https://github.com/rogersialves/wendkeep)** — MIT · open‑core · your data never leaves your disk.
217
+
196
218
  ## License
197
219
 
198
220
  MIT
@@ -0,0 +1,189 @@
1
+ # wendkeep
2
+
3
+ **Português** · [English](README.md)
4
+
5
+ > **Seu agente de código esquece cada sessão. O wendkeep faz ele lembrar — no cofre Obsidian que você já usa.**
6
+
7
+ [![npm](https://img.shields.io/npm/v/wendkeep.svg)](https://www.npmjs.com/package/wendkeep)
8
+ ![test](https://github.com/rogersialves/wendkeep/actions/workflows/test.yml/badge.svg)
9
+ ![zero deps](https://img.shields.io/badge/deps%20de%20runtime-0-brightgreen)
10
+ ![node](https://img.shields.io/badge/node-%E2%89%A518-blue)
11
+
12
+ [![wendkeep — memória persistente para agentes de código, mostrada como um grafo de sessões, decisões, bugs, aprendizados e mudanças](docs/assets/wendkeep-hero.pt.svg)](docs/index.pt.html)
13
+
14
+ **No grafo:** 🔵 sessão · 🟣 decisão · 🔴 bug · 🟢 aprendizado · 🟡 mudança — cada nota, com backlink.
15
+
16
+ **Um harness de memória persistente para agentes de código, construído sobre o seu cofre Obsidian.** Cada sessão do Claude Code / Codex é capturada turno a turno em Markdown local — com rastreio de tokens/custo, decisões, bugs e aprendizados extraídos automaticamente, e uma camada de memória curada injetada de volta no início da próxima sessão. Sobre esse núcleo de memória fica um **ciclo de mudança** nativo e sem dependências (spec → change → TDD → archive com gate por sensor) que mantém intenção, trabalho e prova wikilinkados num só grafo. 100% local, open‑core.
17
+
18
+ ```bash
19
+ npm i -D wendkeep && npx wendkeep init # captura a partir da próxima sessão
20
+ npx wendkeep import # importa sessões passadas do Claude + Codex
21
+ ```
22
+
23
+ **▶ Demo interativo:** [`docs/index.pt.html`](docs/index.pt.html) — uma página autocontida com o herói de grafo vivo. Abra local (ou sirva `docs/` em qualquer host estático). A imagem acima é um render estático dele.
24
+
25
+ > **De um cofre de produção real** (`npx wendkeep stats`): **308** sessões · **1.696** prompts · **US$ 4.836** capturados em **46 dias ativos** (jan–jul 2026) · **15** modelos — cada uma delas uma nota no grafo.
26
+
27
+ > Extraído de um sistema em uso diário de produção: o motor de captura, o rastreio de custo e a fiação do grafo são testados em batalha; o instalador multiplataforma (`wendkeep init`) e o ciclo de mudança nativo são as partes mais novas. Veja [`docs/`](https://github.com/rogersialves/wendkeep/tree/main/docs) para a estratégia e o log de decisões do projeto.
28
+
29
+ ---
30
+
31
+ ## O problema: o contexto morre quando a janela fecha
32
+
33
+ Decisões, becos sem saída, o motivo de você ter escolhido X em vez de Y — some na próxima sessão. As peças pra resolver existem, mas espalhadas (qmd‑sessions, memsearch, Nexus, hooks feitos à mão). O wendkeep entrega tudo num pacote turnkey que escreve num grafo de conhecimento **dentro do cofre Obsidian que você já usa** — sem setup manual, sem snapshot pra manter sincronizado.
34
+
35
+ | | |
36
+ |---|---|
37
+ | **Captura** — cada turno, no disco | Os hooks `SessionStart` / `Stop` escrevem cada sessão numa nota Markdown datada: prompts, iterações, arquivos tocados, wikilinks. |
38
+ | **Deriva** — decisões, bugs, aprendizados | Puxados do transcript pra notas próprias, com backlink pra sessão. Seu histórico fica navegável, não arquivístico. |
39
+ | **Recall** — injetado de volta | Um `CORE` + `DIGEST` com budget capado e a change ativa são injetados no agente no próximo `SessionStart`. Ele retoma de onde parou. |
40
+ | **Custo** — quanto tudo custou | Preço por modelo, ciente de cache, por sessão — mais `cost --trend` com projeção run‑rate no cofre inteiro. |
41
+ | **Multi‑agente** — uma instalação, todos os agentes | Detecta o provedor real (Claude Code, Codex, Copilot) em runtime. |
42
+ | **Local‑first** — sem nuvem, sem conta | Tudo é Markdown puro no seu disco. Um MCP opcional (`@bitbonsai/mcpvault`) deixa o agente ler/escrever o cofre. |
43
+
44
+ ## Requisitos
45
+
46
+ - Node.js ≥ 18
47
+ - Um agente de código com hooks (Claude Code hoje; Codex atendido pelos mesmos hooks)
48
+ - Obsidian (pra ver o grafo) — opcional, mas é o ponto
49
+
50
+ ## Instalar & configurar
51
+
52
+ ```bash
53
+ # no seu projeto
54
+ npm install --save-dev wendkeep # ou: npm install -g wendkeep
55
+ npx wendkeep init
56
+ ```
57
+
58
+ O `wendkeep init` é interativo e **idempotente**. Ele:
59
+
60
+ 1. Cria a taxonomia de pastas do cofre e um `README.md` templado (cofre padrão: `<projeto>/.<nome-do-projeto>-vault`, ex.: `.MeuApp-vault`; sobrescreva com `--vault`).
61
+ 2. **Mescla** os três hooks de sessão e o `OBSIDIAN_VAULT_PATH` no `.claude/settings.json` — sem atropelar suas configs (salva um `.bak`; arquivo ilegível fica intocado e um `.new` é escrito pra você mesclar).
62
+ 3. Adiciona o servidor MCP **`wendkeep-vault`** ao `.mcp.json` pro agente ler/escrever o cofre. Pule com `--no-mcp` — ex.: quando o agente já tem um MCP de cofre. (`--no-mcp` pula *só o MCP do próprio wendkeep*; os MCPs de companion seguem `--companions`.)
63
+ 4. Oferece fixar plugins/MCP **companion** (múltipla escolha; **nenhum** pré-marcado). Cada um é wirado do jeito mais agnóstico que suporta:
64
+ - **`context-mode`** — otimizador de contexto + memória FTS5, como servidor MCP no `.mcp.json` (qualquer agente).
65
+ - **`understand-anything`** — grafo de domínio do projeto, via um hook `understand-inject` no SessionStart que injeta o grafo quando gerado.
66
+ - **`caveman`** — modo de compressão de tokens; roda seu próprio instalador cross‑agent em agentes não‑Claude.
67
+ - **`dotcontext`** — *legado, não recomendado.* O loop a2 nativo do wendkeep (`change` / `verify` / gate) já faz o trabalho dele, então instalar **duplica o harness**. Ainda selecionável via `--companions dotcontext` pra quem já usa (ajuste com `--dotcontext-mcp` / `--dotcontext-hooks`), mas off por padrão.
68
+
69
+ Controle com `--companions <csv>` ou `--no-companions`. A camada de plugin do Claude Code (`extraKnownMarketplaces` + `enabledPlugins`) é wirada como bônus onde o companion tiver uma.
70
+ 5. Instala um **sistema de cores** no `.obsidian/` do cofre: um snippet CSS que colore notas por tipo (sessão/decisão/bug/aprendizado, via as `cssclasses` que os hooks emitem) mais grupos de cor do grafo por pasta. Merge não‑destrutivo em `appearance.json`/`graph.json`; pule com `--no-colors`.
71
+ 6. Semeia a **camada de memória curada**: `.brain/CORE.md` (a camada quente curada à mão, com as 3 seções obrigatórias) e `.brain/COMPACTION_PROTOCOL.md` (o guia do protocolo). As camadas automáticas (`DIGEST.md`, `index.jsonl`) são geradas pelos hooks. Valide a camada curada com `wendkeep validate-memory` (cap 25 linhas, 3 seções, sem segredos/PII).
72
+ 7. Semeia a **camada de definições + skills**: `.brain/agents/` + `.brain/skills/` (fonte da verdade versionada), incluindo as skills de processo nativas `wk-workflow` / `wk-tdd` / `wk-debugging` / `wk-brainstorming` / `wk-planning` / `wk-verify` (algumas trazem templates, ex.: o `verdict-template.json` + prompt de revisor da `wk-verify`). O `init` roda o `wendkeep sync-defs` pra você, entregando em `.codex/agents/` + `.claude/skills/` (rode `sync-defs` de novo após editar o `.brain`).
73
+ 8. Semeia o **ciclo change/spec**: as pastas `07-Specs/` + `08-Mudanças/` e um `wendkeep.sensors.json` nativo (um sensor `validate-memory` mais um por script detectado no `package.json`). Move o `wendkeep change` / `wendkeep verify` — veja **Ciclo de mudança** abaixo.
74
+
75
+ ```bash
76
+ npx wendkeep init --vault "~/vaults/work" --project . --yes # não-interativo
77
+ npx wendkeep init --companions "context-mode,understand-anything" --yes
78
+ npx wendkeep init --no-companions --no-mcp --yes # zero companions, sem MCP do wendkeep
79
+ ```
80
+
81
+ ### Opções do `init`
82
+
83
+ | Flag | O que faz |
84
+ |---|---|
85
+ | `--vault <path>` | Pasta do cofre. Padrão `<projeto>/.<nome-do-projeto>-vault`; o init interativo pergunta. Aponte pra um cofre existente pra instalar nele. |
86
+ | `--project <path>` | Raiz do projeto a wirar (padrão: diretório atual). |
87
+ | `--locale <pt-BR\|en>` | Idioma do cofre — nomes das pastas, scaffold, skills. O init interativo pergunta; travado no init. |
88
+ | `--companions <csv>` | Companions a fixar: `context-mode,caveman,understand-anything` (padrão: **nenhum** — opte explicitamente; `dotcontext` é legado). |
89
+ | `--no-companions` | Não fixa nenhum companion. |
90
+ | `--no-mcp` | Pula o MCP de cofre **do próprio wendkeep** (`wendkeep-vault`). Os MCPs de companion seguem `--companions`. |
91
+ | `--no-colors` | Pula o sistema de cores do Obsidian (snippet `.obsidian` + grupos do grafo). |
92
+ | `--yes`, `-y` | Não-interativo; aceita os padrões (pula os prompts de idioma / cofre / companion). |
93
+ | `--force` | Sobrescreve os blocos de config do wendkeep existentes. |
94
+
95
+ Depois abra o cofre no Obsidian, mande um prompt de teste no seu agente e confirme que uma nota aparece em `02-Sessões/…` (ou `02-Sessions/…` num cofre `en`).
96
+
97
+ ## Atualizar
98
+
99
+ Como os hooks vivem dentro do pacote instalado (o settings.json chama `npx wendkeep hook <name>`), atualizar é só:
100
+
101
+ ```bash
102
+ npm update wendkeep
103
+ ```
104
+
105
+ Sem recopiar, sem snapshot pra re‑sincronizar — o pacote é a única fonte da verdade.
106
+
107
+ ## Comandos
108
+
109
+ | Comando | O que faz |
110
+ |---|---|
111
+ | `wendkeep init` | Configura o wendkeep num projeto (taxonomia do cofre + settings + MCP + skills). |
112
+ | `wendkeep hook <name>` | Roda um hook de sessão; invocado pelo `settings.json` (lê o JSON do agente no stdin). |
113
+ | `wendkeep change <sub>` | Ciclo de mudança: `new [--simple]` / `list` / `show` / `status` / `done <id>` / `undone <id>` / `diff` / `archive [--force]`. |
114
+ | `wendkeep verify [--deep]` | Roda os sensores das tarefas da change; `--deep` monta o pacote de verificação independente. |
115
+ | `wendkeep spec <sub>` | Specs vivos: `list` / `show <capability>`. |
116
+ | `wendkeep sensors <sub>` | `list` / `add <id> "<comando>"` — vê/edita `wendkeep.sensors.json` (JSON Schema incluso). |
117
+ | `wendkeep cost [opts]` | Agrega o gasto de IA nas sessões do cofre — total, por modelo, por dia · `--top [N]` · `--trend [day\|week\|month]` (+ projeção) · `--write` (gera `00-Custo.md`) · `--json`. |
118
+ | `wendkeep stats [--vault P]` | Uma linha compartilhável: sessões · prompts · gasto · período · modelos (`--json`). |
119
+ | `wendkeep import [opts]` | **Memória retroativa** — importa sessões passadas de **Claude + Codex** pro cofre (dedup por `session_id`). `--source all\|claude\|codex` / `--from <dir>` / `--codex-from <dir>` / `--stamp-ids` / `--since d` / `--limit n` / `--dry-run` / `--json`. |
120
+ | `wendkeep dashboard [--force]` | (Re)gera os Bases filtrados por pasta + o MOC `00-Dashboard`. |
121
+ | `wendkeep lesson add "t" "l"` | Registra uma lição local do projeto (injetada no próximo SessionStart). |
122
+ | `wendkeep sync-defs` | Copia `.brain/agents\|skills` pro projeto (`.codex/agents`, `.claude/skills`). |
123
+ | `wendkeep validate-memory [path]` | Valida `.brain/CORE.md` (cap 25, 3 seções, sem segredos/PII). |
124
+ | `wendkeep doctor [--vault P]` | Roda um check de saúde do cofre (integridade de sessões, registry, links). |
125
+ | `wendkeep --version` / `--help` | Versão / uso. |
126
+
127
+ ## Memória retroativa (`import`) — instale hoje, lembre de ontem
128
+
129
+ Instale o wendkeep num projeto existente e ele só lembra sessões **a partir de agora**. O `wendkeep import` conserta isso: um comando importa as sessões passadas de **Claude & Codex** do projeto pro cofre — dedup, datadas, com custo — então o grafo começa cheio, não vazio. Reconstrói cada transcript como uma nota de sessão completa na pasta datada **real** — frontmatter (taggeado com o provedor real), um bloco de iteração por turno, custo + telemetria de subagents, notas derivadas de decisão/bug/aprendizado, encerramento finalizado. Um replay offline do fluxo de captura vivo, então uma nota importada é indistinguível de uma capturada.
130
+
131
+ ```bash
132
+ wendkeep import --vault .meuprojeto-vault --dry-run # prévia do que seria importado (os dois agentes)
133
+ wendkeep import --vault .meuprojeto-vault # escreve as notas
134
+ wendkeep import --vault .meuprojeto-vault --source codex # só Codex
135
+ ```
136
+
137
+ - **Os dois agentes por padrão** (`--source all`). As sessões do Claude vêm de `~/.claude/projects/<slug>/`; os rollouts do Codex de `~/.codex/sessions/**`, escopados pro projeto pelo `cwd` gravado em cada sessão (insensível a case e separador, subpastas inclusas). Estreite com `--source claude` / `--source codex`.
138
+ - Toda nota grava o **`session_id`** e o **`provider`** no frontmatter (captura live e import iguais). Carimbe notas antigas com `wendkeep import --stamp-ids` (preenche o id a partir do registry; idempotente).
139
+ - **Dedup** por `session_id` contra o `SESSION_REGISTRY` do cofre **e** o frontmatter das notas existentes — só importa sessões ausentes e nunca sobrescreve. Rodar de novo é no‑op.
140
+ - Depois de importar, o `wendkeep cost` agrega seu histórico inteiro — retroativamente, nos dois agentes.
141
+
142
+ ## Ciclo de mudança — o loop a2 (spec‑driven, nativo)
143
+
144
+ Além de capturar sessões, o wendkeep é um **harness**: um loop nativo e sem dependências que mantém *intenção* (specs), *trabalho* (changes) e *prova* (sensores) juntos no cofre, wikilinkados no grafo Obsidian.
145
+
146
+ ```
147
+ explore → propose → apply (TDD) → verify → archive
148
+ ```
149
+
150
+ - **Propose** — `wendkeep change new <slug>` faz o scaffold de `08-Mudanças/<slug>/` (`proposta.md`, `design.md`, `tarefas.md` e um delta `specs/`). A change vira *ativa* e é injetada no próximo `SessionStart`, então o agente retoma o trabalho em curso.
151
+ - **Apply** — implemente cada tarefa de `tarefas.md`. Taggeie a tarefa que precisa de prova de máquina com `[sensor:<id>]`.
152
+ - **Verify** — `wendkeep verify` roda os sensores que suas tarefas declararam (do `wendkeep.sensors.json` na raiz do projeto) e grava `evidencia.json`. Um vermelho crítico falha o gate; um vermelho `warning` é aviso.
153
+ - **Archive** — `wendkeep change archive <slug>` faz **gate** na evidência (bloqueia a não ser que todo sensor crítico declarado esteja verde), promove o delta de cada capability (`ADDED`/`MODIFIED`/`REMOVED`) pro `07-Specs/<capability>.md` vivo, move a change pro `_arquivo/` e cunha um ADR em `04-Decisões/`.
154
+
155
+ > O gate bloqueia a não ser que o scaffold esteja preenchido, nenhuma tarefa aberta, evidência fresca e todo requisito declarado coberto. **`--force` é decisão do humano — nunca do agente.**
156
+
157
+ O `wendkeep init` também semeia **skills de processo nativas** (`wk-workflow`, `wk-tdd`, `wk-debugging`, `wk-brainstorming`, `wk-planning`, `wk-verify`) em `.brain/skills` e as entrega em `.claude/skills` — a camada do *como*, zero‑dep. Companions opcionais (`context-mode`, `dotcontext`, `understand-anything`, `caveman`) ficam como camada extra opt‑in.
158
+
159
+ ## Como funciona
160
+
161
+ ```
162
+ sessão do agente ──hooks──▶ wendkeep ──▶ Markdown no cofre ──▶ índice .brain + grafo Obsidian
163
+ (Claude/Codex) (Node) (02-Sessões/…) (CORE+DIGEST, backlinks)
164
+ ```
165
+
166
+ O settings.json do agente aponta cada hook pra `npx wendkeep hook …`. No `Stop`, o wendkeep parseia o transcript, anexa o turno, atualiza a tabela de tokens/custo e (idempotentemente) emite qualquer nota de decisão/bug/aprendizado. Em todo `SessionStart`, o `brain-inject` injeta de volta a memória curada (CORE + DIGEST), a change ativa, as lições do projeto e um roteador `<wk_process>` que roteia qualquer tarefa não‑trivial pelo loop a2 (planejar → `change new` + preencher o scaffold → TDD → verify → archive).
167
+
168
+ O **gate** do archive bloqueia a não ser que: o scaffold da change esteja preenchido (G0), nenhuma tarefa esteja aberta (G1), todo sensor crítico declarado esteja verde (com evidência fresca) e — quando a change declara `[req:]` — um `verdict.json` independente cubra eles. O `--force` é a saída de emergência humana; o agente é instruído a nunca usar por conta própria.
169
+
170
+ ## Notas & roadmap
171
+
172
+ - **Nomes das pastas do cofre são em Português por padrão** (`02-Sessões`, `04-Decisões`, …). Passe `wendkeep init --locale en` pra um cofre em inglês (`02-Sessions`, `04-Decisions`, scaffold/skills em inglês). O locale é uma propriedade do cofre, travada no init; os parsers são bilíngues, então conteúdo misto nunca quebra.
173
+ - **Busca é scoring por keyword/frontmatter**, não embeddings on‑device (isso está no roadmap).
174
+ - **Formatos de transcript são internos ao agente** e podem mudar entre versões; o parsing é isolado mas pode precisar de atualizações.
175
+ - O instalador wira settings do **Claude Code** + `.mcp.json`. Os hooks do Codex rodam nos mesmos scripts mas ainda não são auto‑wirados (o import já cobre sessões Codex passadas via `--source codex`).
176
+
177
+ ---
178
+
179
+ ## Pare de reexplicar seu código toda manhã
180
+
181
+ ```bash
182
+ npm i -D wendkeep && npx wendkeep init
183
+ ```
184
+
185
+ **[Instalar do npm](https://www.npmjs.com/package/wendkeep)** · **[Deixar uma star no GitHub](https://github.com/rogersialves/wendkeep)** — MIT · open‑core · seus dados nunca saem do seu disco.
186
+
187
+ ## Licença
188
+
189
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.25.0",
3
+ "version": "0.27.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/init.mjs CHANGED
@@ -223,6 +223,65 @@ export function promptStrings(localeId) {
223
223
  return PROMPTS[localeId] || PROMPTS['pt-BR'];
224
224
  }
225
225
 
226
+ // Output messages by locale — the summary, the [n/4] steps and the next-steps block follow the
227
+ // chosen vault language (before this, a pt-BR install still printed English output).
228
+ const MESSAGES = {
229
+ 'pt-BR': {
230
+ header: '\nwendkeep init',
231
+ lProject: ' projeto ', lVault: ' vault ', lMcp: ' mcp ',
232
+ lCompanions: ' companions ', lColors: ' cores ',
233
+ skipped: 'ignorado', none: 'nenhum',
234
+ colorsOn: 'wendkeep-colors (snippet + grupos do grafo)',
235
+ taxonomy: (n, c, loc, readme, views) => ` [1/4] taxonomia do vault: ${n} pastas (${c} criadas, locale ${loc})${readme}, .brain + change/spec + sensores semeados${views}`,
236
+ readmeCreated: ', README.md criado', viewsNote: (n) => `, ${n} view(s) + dashboard`,
237
+ defs: (s, a) => ` defs entregues: ${s} skill(s) -> .claude/skills, ${a} agent(s) -> .codex/agents`,
238
+ settingsBadJson: (p) => ` [2/4] settings.json existe mas não é JSON válido -> escrevi ${p}.new (mescle à mão)`,
239
+ settings: (verb, added, bak) => ` [2/4] settings.json ${verb} (${added} hook(s) wirados, OBSIDIAN_VAULT_PATH setado${bak})`,
240
+ mcpBadJson: (p) => ` [3/4] .mcp.json existe mas não é JSON válido -> escrevi ${p}.new (mescle à mão)`,
241
+ mcp: (verb, names, bak) => ` [3/4] .mcp.json ${verb} (${names}${bak})`,
242
+ mcpSkipped: ' [3/4] .mcp.json ignorado (--no-mcp, sem companions MCP)',
243
+ colorsSkipped: ' [4/4] cores ignoradas (--no-colors)',
244
+ colors: (r) => ` [4/4] cores: ${r}`,
245
+ merged: 'mesclado', created: 'criado', bakSaved: ', .bak salvo',
246
+ nextSteps: '\nPróximos passos:',
247
+ step1: (v) => ` 1. Abra o vault no Obsidian: "Abrir pasta como cofre" -> ${v}`,
248
+ step2: ' 2. Garanta que o wendkeep está instalado onde o agente roda (npm i -D wendkeep, ou -g).',
249
+ step3: ' 3. Abra o Claude Code neste projeto e envie um prompt de teste.',
250
+ step4: ' 4. Confirme que uma nota aparece em 02-Sessões/<ano>/<mês>/DIA <dd>/ no vault.',
251
+ updateLater: ' Atualize depois com: npm update wendkeep (sem recopiar — os hooks vivem no pacote).\n',
252
+ vaultRegistered: (v, loc) => `\n cofre já registrado neste projeto: ${v} (locale ${loc}) — pergunta pulada. Use --vault para mudar.`,
253
+ },
254
+ en: {
255
+ header: '\nwendkeep init',
256
+ lProject: ' project ', lVault: ' vault ', lMcp: ' mcp ',
257
+ lCompanions: ' companions ', lColors: ' colors ',
258
+ skipped: 'skipped', none: 'none',
259
+ colorsOn: 'wendkeep-colors (snippet + graph groups)',
260
+ taxonomy: (n, c, loc, readme, views) => ` [1/4] vault taxonomy: ${n} folders (${c} created, locale ${loc})${readme}, .brain + change/spec + sensors seeded${views}`,
261
+ readmeCreated: ', README.md created', viewsNote: (n) => `, ${n} view(s) + dashboard`,
262
+ defs: (s, a) => ` defs delivered: ${s} skill(s) -> .claude/skills, ${a} agent(s) -> .codex/agents`,
263
+ settingsBadJson: (p) => ` [2/4] settings.json exists but is not valid JSON -> wrote ${p}.new (merge by hand)`,
264
+ settings: (verb, added, bak) => ` [2/4] settings.json ${verb} (${added} hook(s) wired, OBSIDIAN_VAULT_PATH set${bak})`,
265
+ mcpBadJson: (p) => ` [3/4] .mcp.json exists but is not valid JSON -> wrote ${p}.new (merge by hand)`,
266
+ mcp: (verb, names, bak) => ` [3/4] .mcp.json ${verb} (${names}${bak})`,
267
+ mcpSkipped: ' [3/4] .mcp.json skipped (--no-mcp, no MCP companions)',
268
+ colorsSkipped: ' [4/4] colors skipped (--no-colors)',
269
+ colors: (r) => ` [4/4] colors: ${r}`,
270
+ merged: 'merged', created: 'created', bakSaved: ', .bak saved',
271
+ nextSteps: '\nNext steps:',
272
+ step1: (v) => ` 1. Open the vault in Obsidian: "Open folder as vault" -> ${v}`,
273
+ step2: ' 2. Make sure wendkeep is installed where the agent runs (npm i -D wendkeep, or -g).',
274
+ step3: ' 3. Open Claude Code in this project and send a test prompt.',
275
+ step4: ' 4. Confirm a note appears under 02-Sessões/<year>/<month>/DIA <dd>/ in the vault.',
276
+ updateLater: ' Update later with: npm update wendkeep (no re-copying — hooks live in the package).\n',
277
+ vaultRegistered: (v, loc) => `\n vault already registered for this project: ${v} (locale ${loc}) — prompt skipped. Use --vault to change.`,
278
+ },
279
+ };
280
+
281
+ export function initMessages(localeId) {
282
+ return MESSAGES[localeId] || MESSAGES['pt-BR'];
283
+ }
284
+
226
285
  // Map the language answer to a locale id. 2/en → en; 1/pt/empty/unknown → pt-BR.
227
286
  export function parseLocaleAnswer(ans) {
228
287
  const a = String(ans || '').trim().toLowerCase();
@@ -230,23 +289,53 @@ export function parseLocaleAnswer(ans) {
230
289
  return 'pt-BR';
231
290
  }
232
291
 
292
+ // The vault path this project was set up with — read from .claude/settings.json's
293
+ // OBSIDIAN_VAULT_PATH (written by a prior init). Empty when the project isn't configured yet.
294
+ export function detectRegisteredVault(projectPath) {
295
+ try {
296
+ const s = JSON.parse(readFileSync(join(projectPath, '.claude', 'settings.json'), 'utf8'));
297
+ const v = s && s.env && s.env.OBSIDIAN_VAULT_PATH;
298
+ return typeof v === 'string' && v ? v : '';
299
+ } catch { return ''; }
300
+ }
301
+
302
+ // The locale locked in a vault's .brain/config.json (empty if absent/invalid).
303
+ export function readVaultLocale(vaultPath) {
304
+ try {
305
+ const c = JSON.parse(readFileSync(join(vaultPath, '.brain', 'config.json'), 'utf8'));
306
+ return c && LOCALES[c.locale] ? c.locale : '';
307
+ } catch { return ''; }
308
+ }
309
+
233
310
  export async function runInit(argv) {
234
311
  const args = parseArgs(argv);
235
312
  const projectPath = resolve(args.project || process.cwd());
236
313
  const log = (s) => process.stdout.write(`${s}\n`);
237
314
 
238
- // Language first (i18n): an interactive TTY without --locale is asked the vault language;
239
- // folders, prompts and scaffold all follow the answer.
315
+ // Recognize an already-configured project: the vault is registered in the project's
316
+ // settings.json (OBSIDIAN_VAULT_PATH) and its locale is locked in the vault's config.json.
317
+ // On re-run (e.g. after `npm i -D wendkeep@latest`) we reuse both and SKIP the language + vault
318
+ // prompts — asking again risks a divergent vault from a mistyped name. `--vault` / `--locale`
319
+ // override; the vault question is a once-per-project thing.
320
+ const registeredVault = args.vault ? '' : detectRegisteredVault(projectPath);
321
+ if (registeredVault && !args.locale) args.locale = readVaultLocale(registeredVault) || args.locale;
322
+
323
+ // Language first (i18n): an interactive TTY without --locale (and not already registered) is
324
+ // asked the vault language; folders, prompts and scaffold all follow the answer.
240
325
  if (!args.locale && process.stdin.isTTY && !args.yes) {
241
326
  const rl = createInterface({ input: process.stdin, output: process.stdout });
242
327
  const ans = await rl.question('Idioma do vault / Vault language:\n [1] Português (pt-BR) [2] English (en)\n> ');
243
328
  rl.close();
244
329
  args.locale = parseLocaleAnswer(ans);
245
330
  }
246
- const P = promptStrings(args.locale && LOCALES[args.locale] ? args.locale : DEFAULT_LOCALE);
331
+ const resolvedLocale = args.locale && LOCALES[args.locale] ? args.locale : DEFAULT_LOCALE;
332
+ const P = promptStrings(resolvedLocale);
333
+ const M = initMessages(resolvedLocale);
247
334
 
248
- let vaultPath = args.vault;
249
- if (!vaultPath) {
335
+ let vaultPath = args.vault || registeredVault;
336
+ if (registeredVault) {
337
+ log(M.vaultRegistered(registeredVault, resolvedLocale));
338
+ } else if (!vaultPath) {
250
339
  const fallback = join(projectPath, deriveVaultDirName(projectPath));
251
340
  if (process.stdin.isTTY && !args.yes) {
252
341
  const rl = createInterface({ input: process.stdin, output: process.stdout });
@@ -285,12 +374,12 @@ export async function runInit(argv) {
285
374
  companions = resolveCompanions({});
286
375
  }
287
376
 
288
- log('\nwendkeep init');
289
- log(` project : ${projectPath}`);
290
- log(` vault : ${vaultPath}`);
291
- log(` mcp : ${args.mcp ? 'mcpvault (wendkeep-vault)' : 'skipped'}`);
292
- log(` companions : ${companions.length ? companions.join(', ') : 'none'}`);
293
- log(` colors : ${args.noColors ? 'skipped' : 'wendkeep-colors (snippet + graph groups)'}\n`);
377
+ log(M.header);
378
+ log(`${M.lProject}: ${projectPath}`);
379
+ log(`${M.lVault}: ${vaultPath}`);
380
+ log(`${M.lMcp}: ${args.mcp ? 'mcpvault (wendkeep-vault)' : M.skipped}`);
381
+ log(`${M.lCompanions}: ${companions.length ? companions.join(', ') : M.none}`);
382
+ log(`${M.lColors}: ${args.noColors ? M.skipped : M.colorsOn}\n`);
294
383
 
295
384
  // dotcontext controls (only relevant when selected): hook level + MCP placement.
296
385
  // Skip the project MCP entry when dotcontext is already global (avoids a duplicate
@@ -329,7 +418,7 @@ export async function runInit(argv) {
329
418
  let readmeNote = '';
330
419
  if (!existsSync(readmePath)) {
331
420
  writeFileSync(readmePath, renderVaultReadme({ projectName: basename(projectPath), vaultPath, withMcp: args.mcp, locale: loc.id }), 'utf8');
332
- readmeNote = ', README.md created';
421
+ readmeNote = M.readmeCreated;
333
422
  }
334
423
  // Seed the curated memory layer (CORE.md) + the compaction-protocol doc. The
335
424
  // DIGEST/index are auto-generated by the hooks; CORE is hand-curated, so we
@@ -369,14 +458,14 @@ export async function runInit(argv) {
369
458
  let viewsNote = '';
370
459
  try {
371
460
  const views = seedVaultViews(vaultPath);
372
- if (views.length) viewsNote = `, ${views.length} view(s) + dashboard`;
461
+ if (views.length) viewsNote = M.viewsNote(views.length);
373
462
  } catch { /* views são bônus — nunca derrubam o init */ }
374
- log(` [1/4] vault taxonomy: ${folders.length} folders (${created} created, locale ${loc.id})${readmeNote}, .brain + change/spec + sensors seeded${viewsNote}`);
463
+ log(M.taxonomy(folders.length, created, loc.id, readmeNote, viewsNote));
375
464
  // Deliver the seeded defs (agents + wk process skills) to the project so they're
376
465
  // usable immediately — no separate `wendkeep sync-defs` step needed.
377
466
  const synced = syncDefs(vaultPath, projectPath);
378
467
  if (synced.skills.length || synced.agents.length) {
379
- log(` defs delivered: ${synced.skills.length} skill(s) -> .claude/skills, ${synced.agents.length} agent(s) -> .codex/agents`);
468
+ log(M.defs(synced.skills.length, synced.agents.length));
380
469
  }
381
470
 
382
471
  // 2. .claude/settings.json --------------------------------------------------
@@ -386,13 +475,13 @@ export async function runInit(argv) {
386
475
  // Unparseable existing file — never clobber. Drop a .new for manual merge.
387
476
  const fresh = mergeSettings(null, { vaultPath, withMcp: args.mcp, force: true, companions, skipMcp, dotcontextHookLevel }).settings;
388
477
  writeJson(`${settingsPath}.new`, fresh);
389
- log(` [2/4] settings.json exists but is not valid JSON -> wrote ${settingsPath}.new (merge by hand)`);
478
+ log(M.settingsBadJson(settingsPath));
390
479
  } else {
391
480
  const hadFile = settingsRead.data !== null;
392
481
  const { settings, added } = mergeSettings(settingsRead.data, { vaultPath, withMcp: args.mcp, force: args.force, companions, skipMcp, dotcontextHookLevel });
393
482
  if (hadFile) backup(settingsPath);
394
483
  writeJson(settingsPath, settings);
395
- log(` [2/4] settings.json ${hadFile ? 'merged' : 'created'} (${added} hook(s) wired, OBSIDIAN_VAULT_PATH set${hadFile ? ', .bak saved' : ''})`);
484
+ log(M.settings(hadFile ? M.merged : M.created, added, hadFile ? M.bakSaved : ''));
396
485
  }
397
486
 
398
487
  // 3. .mcp.json --------------------------------------------------------------
@@ -405,22 +494,22 @@ export async function runInit(argv) {
405
494
  const names = [args.mcp && MCP_SERVER_KEY, ...Object.keys(companionMcp)].filter(Boolean).join(', ');
406
495
  if (!mcpRead.ok) {
407
496
  writeJson(`${mcpPath}.new`, mergeMcp(null, mcpOpts));
408
- log(` [3/4] .mcp.json exists but is not valid JSON -> wrote ${mcpPath}.new (merge by hand)`);
497
+ log(M.mcpBadJson(mcpPath));
409
498
  } else {
410
499
  const hadFile = mcpRead.data !== null;
411
500
  if (hadFile) backup(mcpPath);
412
501
  writeJson(mcpPath, mergeMcp(mcpRead.data, mcpOpts));
413
- log(` [3/4] .mcp.json ${hadFile ? 'merged' : 'created'} (${names}${hadFile ? ', .bak saved' : ''})`);
502
+ log(M.mcp(hadFile ? M.merged : M.created, names, hadFile ? M.bakSaved : ''));
414
503
  }
415
504
  } else {
416
- log(' [3/4] .mcp.json skipped (--no-mcp, no MCP companions)');
505
+ log(M.mcpSkipped);
417
506
  }
418
507
 
419
508
  // 4. Vault color system (.obsidian) -----------------------------------------
420
509
  if (args.noColors) {
421
- log(' [4/4] colors skipped (--no-colors)');
510
+ log(M.colorsSkipped);
422
511
  } else {
423
- log(` [4/4] colors: ${installVaultColors(vaultPath)}`);
512
+ log(M.colors(installVaultColors(vaultPath)));
424
513
  }
425
514
 
426
515
  // caveman has no MCP / agnostic-hook path: on non-Claude agents its skills come
@@ -437,10 +526,10 @@ export async function runInit(argv) {
437
526
  log(` [!] add to .gitignore (wendkeep não toca git): ${DOTCONTEXT_GITIGNORE.join(' ')}`);
438
527
  }
439
528
 
440
- log('\nNext steps:');
441
- log(` 1. Open the vault in Obsidian: "Open folder as vault" -> ${vaultPath}`);
442
- log(' 2. Make sure wendkeep is installed where the agent runs (npm i -D wendkeep, or -g).');
443
- log(' 3. Open Claude Code in this project and send a test prompt.');
444
- log(' 4. Confirm a note appears under 02-Sessões/<year>/<month>/DIA <dd>/ in the vault.');
445
- log(' Update later with: npm update wendkeep (no re-copying — hooks live in the package).\n');
529
+ log(M.nextSteps);
530
+ log(M.step1(vaultPath));
531
+ log(M.step2);
532
+ log(M.step3);
533
+ log(M.step4);
534
+ log(M.updateLater);
446
535
  }
package/src/stats.mjs CHANGED
@@ -22,7 +22,8 @@ export function statsFrom(agg) {
22
22
  }
23
23
 
24
24
  export function statsLine(s) {
25
- const span = s.firstDay && s.lastDay ? ` · ${s.spanDays} dia(s) (${s.firstDay}→${s.lastDay})` : '';
25
+ // "dias ativos" = distinct days WITH activity, not the calendar span (shown in parens).
26
+ const span = s.firstDay && s.lastDay ? ` · ${s.spanDays} dias ativos (${s.firstDay}→${s.lastDay})` : '';
26
27
  return `wendkeep: ${num(s.sessions)} sessão(ões) · ${num(s.prompts)} prompts · ${usd(s.cost)} capturado${span} · ${s.models} modelo(s)`;
27
28
  }
28
29