wendkeep 0.24.0 → 0.26.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 +46 -12
- package/README.pt-BR.md +189 -0
- package/bin/wendkeep.mjs +8 -1
- package/package.json +1 -1
- package/src/cost.mjs +108 -3
- package/src/init.mjs +83 -24
- package/src/stats.mjs +46 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,39 @@ 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.26.0] — 2026-07-08
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **`wendkeep init` output now follows the chosen vault language.** Picking Português left the
|
|
11
|
+
whole summary + `[n/4]` steps + "Next steps" block in English; only the interactive prompts were
|
|
12
|
+
localized. All init output is now driven by a locale message set (pt-BR / en) resolved from the
|
|
13
|
+
language answer — "Próximos passos", "taxonomia do vault", "sensores semeados", etc. `src/init.mjs`.
|
|
14
|
+
|
|
15
|
+
## [0.25.1] — 2026-07-08
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **Landing page in the repo**: a static SVG hero (`docs/assets/wendkeep-hero.svg`, the knowledge
|
|
19
|
+
graph) embedded at the top of the README, plus the self-contained interactive landing at
|
|
20
|
+
`docs/index.html` (live Canvas graph; serve `docs/` via GitHub Pages for a public URL).
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- `wendkeep stats` now says **"N dias ativos (first→last)"** — the count is distinct days *with
|
|
24
|
+
activity*, not the calendar span; the old "N dia(s)" read as calendar days.
|
|
25
|
+
|
|
26
|
+
## [0.25.0] — 2026-07-08
|
|
27
|
+
|
|
28
|
+
Cost trend/projection + shareable stats + launch assets.
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
- **`wendkeep cost --trend [day|week|month]`** — cost bucketed over time plus a run-rate
|
|
32
|
+
**projection** (recent-window daily average × horizon). `wendkeep cost --write` generates a
|
|
33
|
+
`00-Custo.md` trend note in the vault (by-month table + projection + top models). `src/cost.mjs`.
|
|
34
|
+
- **`wendkeep stats`** — one shareable line: sessions · prompts · spend · date span · models
|
|
35
|
+
(`--json` too). For the npm page, a README badge line, or a post. `src/stats.mjs`.
|
|
36
|
+
- **Launch assets** (`docs/`): README hero (tagline, badges, quickstart, screenshot slot),
|
|
37
|
+
Show HN / r/ObsidianMD / X post drafts (`docs/20-launch-posts.md`), and a repeatable
|
|
38
|
+
graph-screenshot guide (`docs/21-graph-screenshot.md`).
|
|
39
|
+
|
|
7
40
|
## [0.24.0] — 2026-07-08
|
|
8
41
|
|
|
9
42
|
### Changed
|
package/README.md
CHANGED
|
@@ -1,25 +1,47 @@
|
|
|
1
1
|
# wendkeep
|
|
2
2
|
|
|
3
|
+
[Português](README.pt-BR.md) · **English**
|
|
4
|
+
|
|
5
|
+
> **Your AI coding agent forgets every session. wendkeep makes it remember — in the Obsidian vault you already use.**
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/wendkeep)
|
|
3
8
|

|
|
9
|
+

|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
[](docs/index.html)
|
|
13
|
+
|
|
14
|
+
**In the graph:** 🔵 session · 🟣 decision · 🔴 bug · 🟢 learning · 🟡 change — every note, backlinked.
|
|
4
15
|
|
|
5
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.
|
|
6
17
|
|
|
18
|
+
```bash
|
|
19
|
+
npm i -D wendkeep && npx wendkeep init # captures from the next session on
|
|
20
|
+
npx wendkeep import # backfill past Claude + Codex sessions
|
|
21
|
+
```
|
|
22
|
+
|
|
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). -->
|
|
28
|
+
|
|
7
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.
|
|
8
30
|
|
|
9
31
|
---
|
|
10
32
|
|
|
11
|
-
##
|
|
33
|
+
## The problem: the context dies when the window closes
|
|
12
34
|
|
|
13
|
-
|
|
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.
|
|
14
36
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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. |
|
|
23
45
|
|
|
24
46
|
## Requirements
|
|
25
47
|
|
|
@@ -102,9 +124,9 @@ No re‑copying, no snapshot to re‑sync — the package is the single source o
|
|
|
102
124
|
| `wendkeep doctor [--vault P]` | Run a vault health check (integrity of sessions, registry, links). |
|
|
103
125
|
| `wendkeep --version` / `--help` | Version / usage. |
|
|
104
126
|
|
|
105
|
-
## Retroactive memory (`import`)
|
|
127
|
+
## Retroactive memory (`import`) — install today, remember yesterday
|
|
106
128
|
|
|
107
|
-
Install wendkeep into an existing project and it only remembers sessions **from now on**. `wendkeep import` fixes that:
|
|
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.
|
|
108
130
|
|
|
109
131
|
```bash
|
|
110
132
|
wendkeep import --vault .myproject-vault --dry-run # preview what would be imported (both agents)
|
|
@@ -131,6 +153,8 @@ explore → propose → apply (TDD) → verify → archive
|
|
|
131
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.
|
|
132
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/`.
|
|
133
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
|
+
|
|
134
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.
|
|
135
159
|
|
|
136
160
|
### The loop in five minutes
|
|
@@ -181,6 +205,16 @@ The archive **gate** blocks unless: the change scaffold is filled (G0), no task
|
|
|
181
205
|
- **Transcript formats are agent‑internal** and can change between agent versions; parsing is isolated but may need updates.
|
|
182
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`).
|
|
183
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
|
+
|
|
184
218
|
## License
|
|
185
219
|
|
|
186
220
|
MIT
|
package/README.pt-BR.md
ADDED
|
@@ -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
|
+
[](https://www.npmjs.com/package/wendkeep)
|
|
8
|
+

|
|
9
|
+

|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
[](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/bin/wendkeep.mjs
CHANGED
|
@@ -48,7 +48,9 @@ Usage:
|
|
|
48
48
|
wendkeep spec <sub> Living specs: list | show <capability>.
|
|
49
49
|
wendkeep sensors <sub> list | add <id> "<command>" [--severity --type --report].
|
|
50
50
|
wendkeep cost [opts] Aggregate AI-coding spend across the vault's sessions.
|
|
51
|
-
--since <date> · --top [N] (priciest
|
|
51
|
+
--since <date> · --top [N] (priciest) · --trend [day|week|month]
|
|
52
|
+
(+ run-rate projection) · --write (generate 00-Custo.md) · --json.
|
|
53
|
+
wendkeep stats [--vault P] One shareable line: sessions · prompts · spend · span · models (--json).
|
|
52
54
|
wendkeep import [opts] Backfill: import this project's past Claude + Codex sessions into
|
|
53
55
|
the vault (deduped by session_id). --source all|claude|codex (default
|
|
54
56
|
all) · --stamp-ids (backfill session_id in existing notes) ·
|
|
@@ -144,6 +146,11 @@ async function main() {
|
|
|
144
146
|
runCost(rest);
|
|
145
147
|
break;
|
|
146
148
|
}
|
|
149
|
+
case 'stats': {
|
|
150
|
+
const { runStats } = await import('../src/stats.mjs');
|
|
151
|
+
runStats(rest);
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
147
154
|
case 'import': {
|
|
148
155
|
const { runImportCli } = await import('../src/import.mjs');
|
|
149
156
|
runImportCli(rest);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "A persistent-memory harness for AI coding agents on your Obsidian vault: turn-by-turn session capture plus a native, zero-dependency spec→change→verify→archive loop (sensor-gated, independent verdict, mutation discrimination). Local-first, agent-agnostic (Claude Code, Codex, Cursor…).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/cost.mjs
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
// `wendkeep cost` — aggregate AI-coding spend across every session note in the vault.
|
|
2
2
|
// Each session note carries cost in its frontmatter (main + subagents since 0.10.0); this
|
|
3
3
|
// rolls the whole vault up: total, by day, by model. Pure aggregation + a thin CLI.
|
|
4
|
-
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
4
|
+
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
6
6
|
import { getLocale } from '../hooks/locale.mjs';
|
|
7
7
|
|
|
8
8
|
const round4 = (n) => Math.round((Number(n) || 0) * 10000) / 10000;
|
|
9
9
|
const usd = (n) => `$${(Number(n) || 0).toFixed(4)}`;
|
|
10
|
+
const today = () => {
|
|
11
|
+
const d = new Date();
|
|
12
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
13
|
+
};
|
|
10
14
|
|
|
11
15
|
function fmValue(content, key) {
|
|
12
16
|
const m = content.match(new RegExp(`^${key}:\\s*(.+)$`, 'm'));
|
|
@@ -24,6 +28,7 @@ export function parseSessionCost(content) {
|
|
|
24
28
|
wasted: Number(fmValue(content, 'subagents_wasted_usd')) || 0,
|
|
25
29
|
tokens: Number(fmValue(content, 'tokens_total')) || 0,
|
|
26
30
|
subTokens: Number(fmValue(content, 'subagents_tokens_total')) || 0,
|
|
31
|
+
prompts: Number(fmValue(content, 'prompts')) || 0,
|
|
27
32
|
};
|
|
28
33
|
}
|
|
29
34
|
|
|
@@ -35,8 +40,9 @@ export function aggregateCosts(entries) {
|
|
|
35
40
|
let wasted = 0;
|
|
36
41
|
let tokens = 0;
|
|
37
42
|
let subTokens = 0;
|
|
43
|
+
let prompts = 0;
|
|
38
44
|
for (const e of entries) {
|
|
39
|
-
main += e.mainCost; sub += e.subCost; wasted += e.wasted || 0; tokens += e.tokens; subTokens += e.subTokens;
|
|
45
|
+
main += e.mainCost; sub += e.subCost; wasted += e.wasted || 0; tokens += e.tokens; subTokens += e.subTokens; prompts += e.prompts || 0;
|
|
40
46
|
const d = e.date || '?';
|
|
41
47
|
(byDay[d] = byDay[d] || { cost: 0, count: 0 }).cost += e.mainCost + e.subCost;
|
|
42
48
|
byDay[d].count += 1;
|
|
@@ -48,12 +54,93 @@ export function aggregateCosts(entries) {
|
|
|
48
54
|
count: entries.length,
|
|
49
55
|
main: round4(main), sub: round4(sub), total: round4(total), wasted: round4(wasted),
|
|
50
56
|
avg: round4(entries.length ? total / entries.length : 0),
|
|
51
|
-
tokens, subTokens,
|
|
57
|
+
tokens, subTokens, prompts,
|
|
52
58
|
byDay: Object.entries(byDay).sort().map(([date, v]) => ({ date, cost: round4(v.cost), count: v.count })),
|
|
53
59
|
byModel: Object.entries(byModel).sort((a, b) => b[1].cost - a[1].cost).map(([model, v]) => ({ model, cost: round4(v.cost), count: v.count })),
|
|
54
60
|
};
|
|
55
61
|
}
|
|
56
62
|
|
|
63
|
+
// Shift a 'YYYY-MM-DD' string by `delta` days (UTC, pure).
|
|
64
|
+
function shiftDate(dateStr, delta) {
|
|
65
|
+
const d = new Date(`${dateStr}T00:00:00Z`);
|
|
66
|
+
d.setUTCDate(d.getUTCDate() + delta);
|
|
67
|
+
return d.toISOString().slice(0, 10);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ISO-ish week key 'YYYY-Www' for a 'YYYY-MM-DD' string (pure).
|
|
71
|
+
function isoWeek(dateStr) {
|
|
72
|
+
const d = new Date(`${dateStr}T00:00:00Z`);
|
|
73
|
+
const day = (d.getUTCDay() + 6) % 7; // Mon=0
|
|
74
|
+
d.setUTCDate(d.getUTCDate() - day + 3); // nearest Thursday
|
|
75
|
+
const firstThu = new Date(Date.UTC(d.getUTCFullYear(), 0, 4));
|
|
76
|
+
const week = 1 + Math.round(((d - firstThu) / 86400000 - 3 + ((firstThu.getUTCDay() + 6) % 7)) / 7);
|
|
77
|
+
return `${d.getUTCFullYear()}-W${String(week).padStart(2, '0')}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Group the per-day cost series (agg.byDay) into day|week|month buckets. Pure.
|
|
81
|
+
export function trendBuckets(byDay, bucket = 'month') {
|
|
82
|
+
const keyOf = (date) => (bucket === 'day' ? date : bucket === 'week' ? isoWeek(date) : String(date).slice(0, 7));
|
|
83
|
+
const map = {};
|
|
84
|
+
for (const d of byDay || []) {
|
|
85
|
+
if (!d.date || d.date === '?') continue;
|
|
86
|
+
const k = keyOf(d.date);
|
|
87
|
+
(map[k] = map[k] || { period: k, cost: 0, count: 0 }).cost += d.cost;
|
|
88
|
+
map[k].count += d.count;
|
|
89
|
+
}
|
|
90
|
+
return Object.values(map).sort((a, b) => a.period.localeCompare(b.period)).map((b) => ({ ...b, cost: round4(b.cost) }));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Run-rate projection from the last `windowDays` of activity. Pure; nowStr = 'YYYY-MM-DD'
|
|
94
|
+
// (defaults to the latest day seen). Honest: a flat run-rate, not a fitted forecast.
|
|
95
|
+
export function projectSpend(byDay, { nowStr, windowDays = 30, horizonDays = 30 } = {}) {
|
|
96
|
+
const days = (byDay || []).filter((d) => d.date && d.date !== '?');
|
|
97
|
+
if (!days.length) return { dailyRate: 0, projected: 0, windowDays, horizonDays, basisDays: 0, basisTotal: 0 };
|
|
98
|
+
const now = nowStr || days[days.length - 1].date;
|
|
99
|
+
const cutoff = shiftDate(now, -windowDays);
|
|
100
|
+
const recent = days.filter((d) => d.date > cutoff);
|
|
101
|
+
const basisTotal = recent.reduce((s, d) => s + d.cost, 0);
|
|
102
|
+
const dailyRate = basisTotal / windowDays;
|
|
103
|
+
return { dailyRate: round4(dailyRate), projected: round4(dailyRate * horizonDays), windowDays, horizonDays, basisDays: recent.length, basisTotal: round4(basisTotal) };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// A generated vault note: cost by month + projection + top models. Overwrites (it is generated).
|
|
107
|
+
export function renderTrendNote(agg, proj, dateStr) {
|
|
108
|
+
const rows = trendBuckets(agg.byDay, 'month').map((b) => `| ${b.period} | ${usd(b.cost)} | ${b.count} |`).join('\n');
|
|
109
|
+
const models = agg.byModel.slice(0, 8).map((m) => `| ${m.model} | ${usd(m.cost)} | ${m.count} |`).join('\n');
|
|
110
|
+
return `---
|
|
111
|
+
type: cost-trend
|
|
112
|
+
date: ${dateStr}
|
|
113
|
+
cssclasses:
|
|
114
|
+
- topic-dashboard
|
|
115
|
+
tags:
|
|
116
|
+
- custo
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
# Custo — tendência
|
|
120
|
+
|
|
121
|
+
> Gerado por \`wendkeep cost --write\`. ${agg.count} sessão(ões) · total ${usd(agg.total)} · ${usd(agg.avg)}/sessão.
|
|
122
|
+
|
|
123
|
+
## Por mês
|
|
124
|
+
|
|
125
|
+
| Mês | Custo | Sessões |
|
|
126
|
+
|---|---|---|
|
|
127
|
+
${rows || '| — | — | — |'}
|
|
128
|
+
|
|
129
|
+
## Projeção (run-rate ${proj.windowDays}d)
|
|
130
|
+
|
|
131
|
+
Base: ${usd(proj.basisTotal)} nos últimos ${proj.windowDays} dias (${proj.basisDays} dia(s) com atividade) → **${usd(proj.dailyRate)}/dia**.
|
|
132
|
+
Projeção próximos ${proj.horizonDays} dias: **${usd(proj.projected)}**.
|
|
133
|
+
|
|
134
|
+
> Run-rate simples (média diária × horizonte), não previsão ajustada.
|
|
135
|
+
|
|
136
|
+
## Por modelo
|
|
137
|
+
|
|
138
|
+
| Modelo | Custo | Sessões |
|
|
139
|
+
|---|---|---|
|
|
140
|
+
${models || '| — | — | — |'}
|
|
141
|
+
`;
|
|
142
|
+
}
|
|
143
|
+
|
|
57
144
|
function walkNotes(dir) {
|
|
58
145
|
const out = [];
|
|
59
146
|
let names;
|
|
@@ -111,6 +198,24 @@ export function runCost(argv) {
|
|
|
111
198
|
process.exit(0);
|
|
112
199
|
}
|
|
113
200
|
|
|
201
|
+
const trendIdx = argv.indexOf('--trend');
|
|
202
|
+
if (trendIdx >= 0) {
|
|
203
|
+
const bucket = ['day', 'week', 'month'].includes(argv[trendIdx + 1]) ? argv[trendIdx + 1] : 'month';
|
|
204
|
+
const proj = projectSpend(agg.byDay);
|
|
205
|
+
process.stdout.write(`Tendência de custo (por ${bucket}):\n`);
|
|
206
|
+
for (const b of trendBuckets(agg.byDay, bucket)) process.stdout.write(` ${b.period} ${usd(b.cost).padStart(11)} (${b.count})\n`);
|
|
207
|
+
process.stdout.write(`\nRun-rate ${proj.windowDays}d: ${usd(proj.dailyRate)}/dia · projeção ${proj.horizonDays}d: ${usd(proj.projected)} (base ${usd(proj.basisTotal)} em ${proj.basisDays} dia(s))\n`);
|
|
208
|
+
process.exit(0);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (argv.includes('--write')) {
|
|
212
|
+
const proj = projectSpend(agg.byDay);
|
|
213
|
+
const rel = '00-Custo.md';
|
|
214
|
+
writeFileSync(join(vaultBase, rel), renderTrendNote(agg, proj, today()), 'utf8');
|
|
215
|
+
process.stdout.write(`cost --write: ${rel} (nota de tendência gerada)\n`);
|
|
216
|
+
process.exit(0);
|
|
217
|
+
}
|
|
218
|
+
|
|
114
219
|
process.stdout.write(`Custo total (vault): ${usd(agg.total)} — ${agg.count} sessão(ões) · ${usd(agg.avg)}/sessão\n`);
|
|
115
220
|
process.stdout.write(` main: ${usd(agg.main)} · subagents: ${usd(agg.sub)}\n`);
|
|
116
221
|
if (agg.wasted) process.stdout.write(` desperdiçado (runs killed/failed): ${usd(agg.wasted)}\n`);
|
package/src/init.mjs
CHANGED
|
@@ -223,6 +223,63 @@ 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
|
+
},
|
|
253
|
+
en: {
|
|
254
|
+
header: '\nwendkeep init',
|
|
255
|
+
lProject: ' project ', lVault: ' vault ', lMcp: ' mcp ',
|
|
256
|
+
lCompanions: ' companions ', lColors: ' colors ',
|
|
257
|
+
skipped: 'skipped', none: 'none',
|
|
258
|
+
colorsOn: 'wendkeep-colors (snippet + graph groups)',
|
|
259
|
+
taxonomy: (n, c, loc, readme, views) => ` [1/4] vault taxonomy: ${n} folders (${c} created, locale ${loc})${readme}, .brain + change/spec + sensors seeded${views}`,
|
|
260
|
+
readmeCreated: ', README.md created', viewsNote: (n) => `, ${n} view(s) + dashboard`,
|
|
261
|
+
defs: (s, a) => ` defs delivered: ${s} skill(s) -> .claude/skills, ${a} agent(s) -> .codex/agents`,
|
|
262
|
+
settingsBadJson: (p) => ` [2/4] settings.json exists but is not valid JSON -> wrote ${p}.new (merge by hand)`,
|
|
263
|
+
settings: (verb, added, bak) => ` [2/4] settings.json ${verb} (${added} hook(s) wired, OBSIDIAN_VAULT_PATH set${bak})`,
|
|
264
|
+
mcpBadJson: (p) => ` [3/4] .mcp.json exists but is not valid JSON -> wrote ${p}.new (merge by hand)`,
|
|
265
|
+
mcp: (verb, names, bak) => ` [3/4] .mcp.json ${verb} (${names}${bak})`,
|
|
266
|
+
mcpSkipped: ' [3/4] .mcp.json skipped (--no-mcp, no MCP companions)',
|
|
267
|
+
colorsSkipped: ' [4/4] colors skipped (--no-colors)',
|
|
268
|
+
colors: (r) => ` [4/4] colors: ${r}`,
|
|
269
|
+
merged: 'merged', created: 'created', bakSaved: ', .bak saved',
|
|
270
|
+
nextSteps: '\nNext steps:',
|
|
271
|
+
step1: (v) => ` 1. Open the vault in Obsidian: "Open folder as vault" -> ${v}`,
|
|
272
|
+
step2: ' 2. Make sure wendkeep is installed where the agent runs (npm i -D wendkeep, or -g).',
|
|
273
|
+
step3: ' 3. Open Claude Code in this project and send a test prompt.',
|
|
274
|
+
step4: ' 4. Confirm a note appears under 02-Sessões/<year>/<month>/DIA <dd>/ in the vault.',
|
|
275
|
+
updateLater: ' Update later with: npm update wendkeep (no re-copying — hooks live in the package).\n',
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
export function initMessages(localeId) {
|
|
280
|
+
return MESSAGES[localeId] || MESSAGES['pt-BR'];
|
|
281
|
+
}
|
|
282
|
+
|
|
226
283
|
// Map the language answer to a locale id. 2/en → en; 1/pt/empty/unknown → pt-BR.
|
|
227
284
|
export function parseLocaleAnswer(ans) {
|
|
228
285
|
const a = String(ans || '').trim().toLowerCase();
|
|
@@ -243,7 +300,9 @@ export async function runInit(argv) {
|
|
|
243
300
|
rl.close();
|
|
244
301
|
args.locale = parseLocaleAnswer(ans);
|
|
245
302
|
}
|
|
246
|
-
const
|
|
303
|
+
const resolvedLocale = args.locale && LOCALES[args.locale] ? args.locale : DEFAULT_LOCALE;
|
|
304
|
+
const P = promptStrings(resolvedLocale);
|
|
305
|
+
const M = initMessages(resolvedLocale);
|
|
247
306
|
|
|
248
307
|
let vaultPath = args.vault;
|
|
249
308
|
if (!vaultPath) {
|
|
@@ -285,12 +344,12 @@ export async function runInit(argv) {
|
|
|
285
344
|
companions = resolveCompanions({});
|
|
286
345
|
}
|
|
287
346
|
|
|
288
|
-
log(
|
|
289
|
-
log(
|
|
290
|
-
log(
|
|
291
|
-
log(
|
|
292
|
-
log(
|
|
293
|
-
log(
|
|
347
|
+
log(M.header);
|
|
348
|
+
log(`${M.lProject}: ${projectPath}`);
|
|
349
|
+
log(`${M.lVault}: ${vaultPath}`);
|
|
350
|
+
log(`${M.lMcp}: ${args.mcp ? 'mcpvault (wendkeep-vault)' : M.skipped}`);
|
|
351
|
+
log(`${M.lCompanions}: ${companions.length ? companions.join(', ') : M.none}`);
|
|
352
|
+
log(`${M.lColors}: ${args.noColors ? M.skipped : M.colorsOn}\n`);
|
|
294
353
|
|
|
295
354
|
// dotcontext controls (only relevant when selected): hook level + MCP placement.
|
|
296
355
|
// Skip the project MCP entry when dotcontext is already global (avoids a duplicate
|
|
@@ -329,7 +388,7 @@ export async function runInit(argv) {
|
|
|
329
388
|
let readmeNote = '';
|
|
330
389
|
if (!existsSync(readmePath)) {
|
|
331
390
|
writeFileSync(readmePath, renderVaultReadme({ projectName: basename(projectPath), vaultPath, withMcp: args.mcp, locale: loc.id }), 'utf8');
|
|
332
|
-
readmeNote =
|
|
391
|
+
readmeNote = M.readmeCreated;
|
|
333
392
|
}
|
|
334
393
|
// Seed the curated memory layer (CORE.md) + the compaction-protocol doc. The
|
|
335
394
|
// DIGEST/index are auto-generated by the hooks; CORE is hand-curated, so we
|
|
@@ -369,14 +428,14 @@ export async function runInit(argv) {
|
|
|
369
428
|
let viewsNote = '';
|
|
370
429
|
try {
|
|
371
430
|
const views = seedVaultViews(vaultPath);
|
|
372
|
-
if (views.length) viewsNote =
|
|
431
|
+
if (views.length) viewsNote = M.viewsNote(views.length);
|
|
373
432
|
} catch { /* views são bônus — nunca derrubam o init */ }
|
|
374
|
-
log(
|
|
433
|
+
log(M.taxonomy(folders.length, created, loc.id, readmeNote, viewsNote));
|
|
375
434
|
// Deliver the seeded defs (agents + wk process skills) to the project so they're
|
|
376
435
|
// usable immediately — no separate `wendkeep sync-defs` step needed.
|
|
377
436
|
const synced = syncDefs(vaultPath, projectPath);
|
|
378
437
|
if (synced.skills.length || synced.agents.length) {
|
|
379
|
-
log(
|
|
438
|
+
log(M.defs(synced.skills.length, synced.agents.length));
|
|
380
439
|
}
|
|
381
440
|
|
|
382
441
|
// 2. .claude/settings.json --------------------------------------------------
|
|
@@ -386,13 +445,13 @@ export async function runInit(argv) {
|
|
|
386
445
|
// Unparseable existing file — never clobber. Drop a .new for manual merge.
|
|
387
446
|
const fresh = mergeSettings(null, { vaultPath, withMcp: args.mcp, force: true, companions, skipMcp, dotcontextHookLevel }).settings;
|
|
388
447
|
writeJson(`${settingsPath}.new`, fresh);
|
|
389
|
-
log(
|
|
448
|
+
log(M.settingsBadJson(settingsPath));
|
|
390
449
|
} else {
|
|
391
450
|
const hadFile = settingsRead.data !== null;
|
|
392
451
|
const { settings, added } = mergeSettings(settingsRead.data, { vaultPath, withMcp: args.mcp, force: args.force, companions, skipMcp, dotcontextHookLevel });
|
|
393
452
|
if (hadFile) backup(settingsPath);
|
|
394
453
|
writeJson(settingsPath, settings);
|
|
395
|
-
log(
|
|
454
|
+
log(M.settings(hadFile ? M.merged : M.created, added, hadFile ? M.bakSaved : ''));
|
|
396
455
|
}
|
|
397
456
|
|
|
398
457
|
// 3. .mcp.json --------------------------------------------------------------
|
|
@@ -405,22 +464,22 @@ export async function runInit(argv) {
|
|
|
405
464
|
const names = [args.mcp && MCP_SERVER_KEY, ...Object.keys(companionMcp)].filter(Boolean).join(', ');
|
|
406
465
|
if (!mcpRead.ok) {
|
|
407
466
|
writeJson(`${mcpPath}.new`, mergeMcp(null, mcpOpts));
|
|
408
|
-
log(
|
|
467
|
+
log(M.mcpBadJson(mcpPath));
|
|
409
468
|
} else {
|
|
410
469
|
const hadFile = mcpRead.data !== null;
|
|
411
470
|
if (hadFile) backup(mcpPath);
|
|
412
471
|
writeJson(mcpPath, mergeMcp(mcpRead.data, mcpOpts));
|
|
413
|
-
log(
|
|
472
|
+
log(M.mcp(hadFile ? M.merged : M.created, names, hadFile ? M.bakSaved : ''));
|
|
414
473
|
}
|
|
415
474
|
} else {
|
|
416
|
-
log(
|
|
475
|
+
log(M.mcpSkipped);
|
|
417
476
|
}
|
|
418
477
|
|
|
419
478
|
// 4. Vault color system (.obsidian) -----------------------------------------
|
|
420
479
|
if (args.noColors) {
|
|
421
|
-
log(
|
|
480
|
+
log(M.colorsSkipped);
|
|
422
481
|
} else {
|
|
423
|
-
log(
|
|
482
|
+
log(M.colors(installVaultColors(vaultPath)));
|
|
424
483
|
}
|
|
425
484
|
|
|
426
485
|
// caveman has no MCP / agnostic-hook path: on non-Claude agents its skills come
|
|
@@ -437,10 +496,10 @@ export async function runInit(argv) {
|
|
|
437
496
|
log(` [!] add to .gitignore (wendkeep não toca git): ${DOTCONTEXT_GITIGNORE.join(' ')}`);
|
|
438
497
|
}
|
|
439
498
|
|
|
440
|
-
log(
|
|
441
|
-
log(
|
|
442
|
-
log(
|
|
443
|
-
log(
|
|
444
|
-
log(
|
|
445
|
-
log(
|
|
499
|
+
log(M.nextSteps);
|
|
500
|
+
log(M.step1(vaultPath));
|
|
501
|
+
log(M.step2);
|
|
502
|
+
log(M.step3);
|
|
503
|
+
log(M.step4);
|
|
504
|
+
log(M.updateLater);
|
|
446
505
|
}
|
package/src/stats.mjs
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// `wendkeep stats` — one shareable line about what the vault has captured. For the npm page,
|
|
2
|
+
// a README badge line, or a tweet. Read-only; reuses the cost aggregation.
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
5
|
+
import { collectVaultCost } from './cost.mjs';
|
|
6
|
+
|
|
7
|
+
const usd = (n) => `$${(Number(n) || 0).toFixed(2)}`;
|
|
8
|
+
const num = (n) => String(Math.round(Number(n) || 0)).replace(/\B(?=(\d{3})+(?!\d))/g, '.');
|
|
9
|
+
|
|
10
|
+
// Pure: derive the shareable stats from a collectVaultCost aggregate.
|
|
11
|
+
export function statsFrom(agg) {
|
|
12
|
+
const days = (agg.byDay || []).map((d) => d.date).filter((d) => d && d !== '?').sort();
|
|
13
|
+
return {
|
|
14
|
+
sessions: agg.count,
|
|
15
|
+
prompts: agg.prompts || 0,
|
|
16
|
+
cost: agg.total,
|
|
17
|
+
models: (agg.byModel || []).length,
|
|
18
|
+
firstDay: days[0] || '',
|
|
19
|
+
lastDay: days[days.length - 1] || '',
|
|
20
|
+
spanDays: days.length,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function statsLine(s) {
|
|
25
|
+
// "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})` : '';
|
|
27
|
+
return `wendkeep: ${num(s.sessions)} sessão(ões) · ${num(s.prompts)} prompts · ${usd(s.cost)} capturado${span} · ${s.models} modelo(s)`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function opt(argv, name) {
|
|
31
|
+
const i = argv.indexOf(name);
|
|
32
|
+
if (i >= 0) return argv[i + 1];
|
|
33
|
+
const eq = argv.find((a) => a.startsWith(`${name}=`));
|
|
34
|
+
return eq ? eq.slice(name.length + 1) : undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function runStats(argv) {
|
|
38
|
+
const vaultRaw = opt(argv, '--vault') || process.env.OBSIDIAN_VAULT_PATH;
|
|
39
|
+
if (!vaultRaw) { process.stderr.write('wendkeep stats: no vault (--vault or OBSIDIAN_VAULT_PATH).\n'); process.exit(2); }
|
|
40
|
+
const vaultBase = isAbsolute(vaultRaw) ? vaultRaw : resolve(process.cwd(), vaultRaw);
|
|
41
|
+
if (!existsSync(vaultBase)) { process.stderr.write(`wendkeep stats: vault not found: ${vaultBase}\n`); process.exit(2); }
|
|
42
|
+
const s = statsFrom(collectVaultCost(vaultBase));
|
|
43
|
+
if (argv.includes('--json')) { process.stdout.write(`${JSON.stringify(s, null, 2)}\n`); process.exit(0); }
|
|
44
|
+
process.stdout.write(`${statsLine(s)}\n`);
|
|
45
|
+
process.exit(0);
|
|
46
|
+
}
|