wendkeep 0.72.0 → 0.72.1

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,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.72.1] — 2026-08-20
8
+
9
+ ### Added
10
+
11
+ - **Proveniência verificável de release.** A publicação gera um receipt com commit, versão, tag,
12
+ integridade npm, execução do workflow e GitHub Release, e recusa divergências entre o SHA testado,
13
+ a tag e o tarball publicado.
14
+ - **Contrato seguro do Observer.** O Keep Core permanece em Node.js 18+, enquanto comandos SQL
15
+ diagnosticam `WENDKEEP_OBSERVER_NODE_UNSUPPORTED` abaixo do Node.js 22.13. Mutações exigem Bearer,
16
+ non-loopback exige token e requisições validam Host e Origin.
17
+ - **Níveis de captura.** `metadata` é o padrão sem mensagens; `messages` e `full-transcript` são
18
+ opt-in. Caminhos absolutos não são publicados e init recomenda ignorar state e outbox SQL.
19
+
20
+ ### Changed
21
+
22
+ - **Release somente após CI verde.** O workflow publica o SHA aprovado pela matriz Core (Node 18/20)
23
+ e Observer (Node 22.13/24), cria a tag no mesmo commit e pode reconciliar execuções repetidas.
24
+ - **Dogfooding pelo working tree.** O repositório não depende mais de `wendkeep` em devDependencies;
25
+ seus hooks chamam `node ./bin/wendkeep.mjs`, enquanto projetos consumidores usam
26
+ `npx --no-install wendkeep` e o tarball continua testado isoladamente.
27
+
28
+ ### Fixed
29
+
30
+ - **Identidade de arquivos de memória no Windows.** A revalidação compara o índice do arquivo como
31
+ inteiro exato e tolera a inconsistência conhecida do serial de volume do libuv antigo, evitando
32
+ falsos `VAULT_PATH_UNSAFE` no Node.js 22.13 sem relaxar a rejeição de hardlinks ou reparses.
33
+ - **Ingestão SQL grande em runners lentos.** O timeout HTTP cresce com o tamanho bruto do lote até
34
+ 120 segundos, preservando 15 segundos para payloads vazios/pequenos e evitando outbox falsa para
35
+ lotes gzip válidos acima de 64 MB.
36
+ - **Trusted Publisher preservado após o gate de CI.** `auto-tag.yml`, o workflow já autorizado no
37
+ npm, passa a executar a matriz da `main` e mantém o publish em um job com `needs: test`;
38
+ `test.yml` fica exclusivo para pull requests.
39
+
7
40
  ## [0.72.0] — 2026-08-17
8
41
 
9
42
  ### Added
@@ -1029,7 +1062,7 @@ All notable changes to **wendkeep** are documented here. Format based on
1029
1062
  enum de eventos de hook do Codex.
1030
1063
  - A projeção Codex tem três diferenças em relação ao formato do `settings.json`, todas
1031
1064
  **silenciosas quando erradas** — daí valerem registro. (1) A chave de timeout é `timeoutSec`,
1032
- não `timeout`. (2) O comando é sempre `npx wendkeep hook <nome>`, nunca a forma node-direta:
1065
+ não `timeout`. (2) O comando é sempre `npx --no-install wendkeep hook <nome>`, nunca a forma node-direta:
1033
1066
  aquela emite `${CLAUDE_PROJECT_DIR}`, que não existe no Codex, então a flag `preferLocal` é
1034
1067
  ignorada de propósito na projeção. (3) As chaves de evento são PascalCase — o snake_case que
1035
1068
  se vê em `[hooks.state]` no `~/.codex/config.toml` é o rótulo interno do evento, não a chave
@@ -1613,7 +1646,7 @@ they only read + append.
1613
1646
 
1614
1647
  ### Note
1615
1648
  - You do **not** need `wendkeep init` for a routine update: the hooks live in the package
1616
- (`settings.json` calls `npx wendkeep hook …`), so `npm i -D wendkeep@latest` updates them.
1649
+ (`settings.json` calls `npx --no-install wendkeep hook …`), so `npm i -D wendkeep@latest` updates them.
1617
1650
  Re-run `init` only when a release adds new wiring (the CHANGELOG says so); it's idempotent.
1618
1651
 
1619
1652
  ## [0.26.0] — 2026-07-08
@@ -1793,7 +1826,7 @@ Fix: memory + active-change injection wired by default.
1793
1826
 
1794
1827
  ### Upgrade
1795
1828
  - Existing installs pick it up by re-running `wendkeep init --force` (idempotent — it only adds the
1796
- missing hook), or by adding `npx wendkeep hook brain-inject` to the SessionStart hooks manually.
1829
+ missing hook), or by adding `npx --no-install wendkeep hook brain-inject` to the SessionStart hooks manually.
1797
1830
 
1798
1831
  ## [0.18.0] — 2026-07-06
1799
1832
 
@@ -6,7 +6,7 @@
6
6
 
7
7
  The Observer consolidates observability for multiple WendKeep projects in a local service. The
8
8
  Docker volume keeps `/data/observer.sqlite` as the single authority for documents, sessions,
9
- agents, usage, calls, and complete transcripts. The content can be browsed and searched in the
9
+ agents, usage, and calls. Complete transcripts are optional and require explicit capture. The content can be browsed and searched in the
10
10
  container without depending on Obsidian for queries.
11
11
 
12
12
  ## When to use
@@ -24,8 +24,9 @@ WendKeep hooks.
24
24
 
25
25
  ## Prerequisites
26
26
 
27
- Initialize projects with WendKeep and explicitly register each project before starting the HTTP
28
- server. The default local mode has no token to configure.
27
+ Use Node.js 22.13 or newer for the SQL Observer. Keep Core and the remaining commands continue to
28
+ support Node.js 18 or newer. Explicitly register each project and set `WENDKEEP_OBSERVER_TOKEN`;
29
+ loopback reads remain open, while every mutation requires a Bearer token.
29
30
 
30
31
  ## Syntax
31
32
 
@@ -33,8 +34,8 @@ server. The default local mode has no token to configure.
33
34
  npx wendkeep observer status --data-dir <directory> --json
34
35
  npx wendkeep observer register --project <project> --vault <vault> --data-dir <directory>
35
36
  npx wendkeep observer publish --project <project> --vault <vault> --data-dir <directory>
36
- npx wendkeep observer memory import --project <project> --vault <vault> --url http://127.0.0.1:8787 --json
37
- npx wendkeep observer serve --host 127.0.0.1 --port 8787 --data-dir <directory>
37
+ npx wendkeep observer memory import --project <project> --vault <vault> --url http://127.0.0.1:8787 --token <token> --json
38
+ npx wendkeep observer serve --host 127.0.0.1 --port 8787 --data-dir <directory> --token <token>
38
39
  ```
39
40
 
40
41
  ## Options and exit codes
@@ -44,8 +45,9 @@ npx wendkeep observer serve --host 127.0.0.1 --port 8787 --data-dir <directory>
44
45
  - `--project` and `--vault` identify a project for `register`, `publish`, and `memory import`.
45
46
  - `--host` accepts only `127.0.0.1`, `localhost`, or `::1`; other hosts are rejected before
46
47
  listening.
47
- - `/v1` is open in the default local mode; keep `--host 127.0.0.1` and do not publish the port on
48
- a network address.
48
+ - `--token` or `WENDKEEP_OBSERVER_TOKEN` authenticates mutations; `--allow-non-loopback` fails without one.
49
+ - `WENDKEEP_OBSERVER_CAPTURE_LEVEL` accepts `metadata` (default, no messages), `messages`, or
50
+ `full-transcript`. Absolute local paths are never published.
49
51
  - Exit `0` means success; exit `1` means configuration or operation failure; the publisher hook
50
52
  also returns `0` when the Observer is unavailable.
51
53
 
@@ -53,7 +55,8 @@ npx wendkeep observer serve --host 127.0.0.1 --port 8787 --data-dir <directory>
53
55
 
54
56
  ```powershell
55
57
  npx wendkeep observer register --project C:\GitHub\WendKeep --vault C:\GitHub\WendKeep\.WendKeep-vault --data-dir C:\WendKeepObserver
56
- npx wendkeep observer serve --host 127.0.0.1 --port 8787 --data-dir C:\WendKeepObserver
58
+ $env:WENDKEEP_OBSERVER_TOKEN = '<strong-local-token>'
59
+ npx wendkeep observer serve --host 127.0.0.1 --port 8787 --data-dir C:\WendKeepObserver --token $env:WENDKEEP_OBSERVER_TOKEN
57
60
  $env:WENDKEEP_OBSERVER_URL = 'http://127.0.0.1:8787'
58
61
  ```
59
62
 
@@ -66,14 +69,14 @@ docker compose -f docker/wendkeep-observer/compose.yaml up -d --build
66
69
  ## Local web dashboard
67
70
 
68
71
  With the server running, open [http://127.0.0.1:8787/](http://127.0.0.1:8787/) in a browser. The
69
- dashboard is served by the same process and opens directly, without a form or token. Keep the port
72
+ dashboard is served by the same process and opens directly for reads, without a form or token. Keep the port
70
73
  bound to the computer loopback; do not expose this address on a network interface.
71
74
 
72
75
  The dashboard shows the multi-project list, version, health, latest session, active change, change
73
76
  count, and last capture time. Opening a project exposes Overview, Consumption, Sessions, Memory,
74
77
  Changes, and Sync screens. Consumption shows total cost, token categories, primary agents,
75
78
  subagents, providers, models, daily trend, historical coverage, and calls with prompt, response,
76
- and complete transcript. Loading, empty, unavailable-server, conflict, no-pricing, and stale-data
79
+ and transcript content according to the selected capture level. Loading, empty, unavailable-server, conflict, no-pricing, and stale-data
77
80
  states are visible, with manual refresh and an automatic 15-second refresh.
78
81
 
79
82
  If the browser shows the shell but the list fails, check the service health at
@@ -84,7 +87,8 @@ If the browser shows the shell but the list fails, check the service health at
84
87
  `register` stores `project_id`, name, version, and registration time. `publish` reads the local
85
88
  vault, produces the snapshot, and sends idempotent events to SQLite containing the complete content
86
89
  of sessions, decisions, bugs, learnings, specs, changes, CORE, DIGEST, SHARED_MEMORY, brain state,
87
- agent sessions, cost rollups, calls, and transcripts. The container stores everything in
90
+ agent sessions, cost rollups, and calls. Messages and transcripts are only sent by capture levels
91
+ that explicitly enable them. The container stores everything in
88
92
  `/data/observer.sqlite`; it does not mount `C:\GitHub` or any `.WendKeep-vault`. Markdown is only
89
93
  the text held in SQL and is recreated as files only by an explicit read-only export.
90
94
  `memory import` performs the initial load and returns file/hash parity. During migration, the
@@ -104,7 +108,9 @@ outbox is temporary transport, not authority.
104
108
  - `project_not_registered`: run `observer register` before publishing.
105
109
  - `host loopback`: replace `0.0.0.0` or a LAN address with `127.0.0.1`.
106
110
  - Pending outbox: the service was unavailable; preserve `.brain/observer-outbox/` and
107
- `.brain/observer-sql-outbox/`, then rerun the publisher. Do not delete events manually.
111
+ `.brain/observer-sql-outbox/`, then rerun the publisher. Also ignore
112
+ `.brain/observer-sql-state.json` and `.brain/observer-sql-outbox/` in a versioned vault. Do not delete events manually.
113
+ - `WENDKEEP_OBSERVER_NODE_UNSUPPORTED`: run the Observer on Node.js 22.13 or newer.
108
114
  - If memory or usage is incomplete, check the Sync screen, preserve the outbox, and run
109
115
  `observer memory import` to rebuild the load from the vault.
110
116
 
@@ -26,11 +26,11 @@ and a vault bound to the correct project.
26
26
  ## Syntax
27
27
 
28
28
  ```bash
29
- npx wendkeep hook <name>
29
+ npx --no-install wendkeep hook <name>
30
30
  npx wendkeep session list
31
31
  npx wendkeep session show <id>
32
32
  npx wendkeep session use <id>
33
- npx wendkeep hook session-backfill --session <id> [--write]
33
+ npx --no-install wendkeep hook session-backfill --session <id> [--write]
34
34
  npx wendkeep import [options]
35
35
  ```
36
36
 
@@ -96,8 +96,8 @@ npx wendkeep import [options]
96
96
  ```bash
97
97
  npx wendkeep session list
98
98
  npx wendkeep session show 019abc-session-id
99
- npx wendkeep hook session-backfill --session 019abc-session-id
100
- npx wendkeep hook session-backfill --session 019abc-session-id --write
99
+ npx --no-install wendkeep hook session-backfill --session 019abc-session-id
100
+ npx --no-install wendkeep hook session-backfill --session 019abc-session-id --write
101
101
  npx wendkeep import --source codex --since 2026-07-01 --dry-run --json
102
102
  ```
103
103
 
@@ -6,7 +6,7 @@
6
6
 
7
7
  O Observer consolida a observabilidade de vários projetos WendKeep em um serviço local. O volume
8
8
  Docker mantém o SQLite `/data/observer.sqlite` como autoridade única para documentos, sessões,
9
- agentes, uso, chamadas e transcripts completos. O conteúdo fica disponível para navegação e busca
9
+ agentes, uso e chamadas. Transcripts completos são opcionais e exigem captura explícita. O conteúdo fica disponível para navegação e busca
10
10
  no próprio container, sem depender do Obsidian para consulta.
11
11
 
12
12
  ## Quando usar
@@ -24,8 +24,9 @@ pelos hooks e pelo WendKeep local.
24
24
 
25
25
  ## Pré-requisitos
26
26
 
27
- Tenha os projetos inicializados com WendKeep e registre explicitamente cada projeto antes de
28
- iniciar o servidor HTTP. No modo local padrão não token para configurar.
27
+ Tenha Node.js 22.13 ou mais recente para executar o Observer SQL. O Keep Core e os demais comandos
28
+ continuam compatíveis com Node.js 18 ou mais recente. Registre explicitamente cada projeto e defina
29
+ `WENDKEEP_OBSERVER_TOKEN`; leituras no loopback permanecem abertas, mas toda mutação exige Bearer.
29
30
 
30
31
  ## Sintaxe
31
32
 
@@ -33,8 +34,8 @@ iniciar o servidor HTTP. No modo local padrão não há token para configurar.
33
34
  npx wendkeep observer status --data-dir <diretório> --json
34
35
  npx wendkeep observer register --project <projeto> --vault <vault> --data-dir <diretório>
35
36
  npx wendkeep observer publish --project <projeto> --vault <vault> --data-dir <diretório>
36
- npx wendkeep observer memory import --project <projeto> --vault <vault> --url http://127.0.0.1:8787 --json
37
- npx wendkeep observer serve --host 127.0.0.1 --port 8787 --data-dir <diretório>
37
+ npx wendkeep observer memory import --project <projeto> --vault <vault> --url http://127.0.0.1:8787 --token <token> --json
38
+ npx wendkeep observer serve --host 127.0.0.1 --port 8787 --data-dir <diretório> --token <token>
38
39
  ```
39
40
 
40
41
  ## Opções e códigos de saída
@@ -44,8 +45,9 @@ npx wendkeep observer serve --host 127.0.0.1 --port 8787 --data-dir <diretório>
44
45
  - `--project` e `--vault` identificam o projeto nos comandos `register`, `publish` e `memory import`.
45
46
  - `--host` aceita somente `127.0.0.1`, `localhost` ou `::1`; outros hosts são recusados antes do
46
47
  listen.
47
- - as rotas `/v1` ficam abertas no modo local padrão; mantenha `--host 127.0.0.1` e não publique a
48
- porta em um endereço de rede.
48
+ - `--token` ou `WENDKEEP_OBSERVER_TOKEN` autentica mutações; `--allow-non-loopback` falha sem token.
49
+ - `WENDKEEP_OBSERVER_CAPTURE_LEVEL` aceita `metadata` (padrão, sem mensagens), `messages` ou
50
+ `full-transcript`. Caminhos locais absolutos nunca são publicados.
49
51
  - Exit `0` indica sucesso; exit `1` indica falha de configuração ou operação; o hook publisher
50
52
  também retorna `0` quando o Observer está indisponível.
51
53
 
@@ -53,7 +55,8 @@ npx wendkeep observer serve --host 127.0.0.1 --port 8787 --data-dir <diretório>
53
55
 
54
56
  ```powershell
55
57
  npx wendkeep observer register --project C:\GitHub\WendKeep --vault C:\GitHub\WendKeep\.WendKeep-vault --data-dir C:\WendKeepObserver
56
- npx wendkeep observer serve --host 127.0.0.1 --port 8787 --data-dir C:\WendKeepObserver
58
+ $env:WENDKEEP_OBSERVER_TOKEN = '<token-local-forte>'
59
+ npx wendkeep observer serve --host 127.0.0.1 --port 8787 --data-dir C:\WendKeepObserver --token $env:WENDKEEP_OBSERVER_TOKEN
57
60
  $env:WENDKEEP_OBSERVER_URL = 'http://127.0.0.1:8787'
58
61
  ```
59
62
 
@@ -66,14 +69,14 @@ docker compose -f docker/wendkeep-observer/compose.yaml up -d --build
66
69
  ## Painel web local
67
70
 
68
71
  Com o servidor em execução, abra [http://127.0.0.1:8787/](http://127.0.0.1:8787/) no navegador.
69
- O painel é servido pelo mesmo processo e abre diretamente, sem formulário ou token. A porta fica
72
+ O painel é servido pelo mesmo processo e abre diretamente para consultas, sem formulário ou token. A porta fica
70
73
  presa ao loopback do computador; não coloque o endereço em uma interface de rede.
71
74
 
72
75
  O painel mostra a lista multi-projeto, versão, saúde, sessão mais recente, change ativa, contagem
73
76
  de changes e data da última captura. Ao abrir um projeto, o workspace oferece Overview, Consumo,
74
77
  Sessões, Memória, Changes e Sincronização. A aba Consumo mostra custo total, tokens por categoria,
75
78
  agentes principais, subagentes, provedores, modelos, tendência diária, cobertura histórica e
76
- chamadas com prompt, resposta e transcript completo. Os estados de carregamento, vazio, servidor
79
+ chamadas conforme o nível de captura escolhido. Os estados de carregamento, vazio, servidor
77
80
  indisponível, conflito, modelo sem tarifa e dados desatualizados ficam visíveis, e a atualização
78
81
  pode ser manual ou automática a cada 15 segundos.
79
82
 
@@ -85,7 +88,8 @@ Se o navegador mostrar a tela mas a lista falhar, confirme a saúde em
85
88
  `register` grava `project_id`, nome, versão e data de registro. `publish` lê o vault local,
86
89
  produz o snapshot e envia eventos idempotentes para o SQLite com o conteúdo integral das sessões,
87
90
  decisões, bugs, aprendizados, specs, changes, CORE, DIGEST, SHARED_MEMORY, estado do brain,
88
- sessões de agentes, rollups de custo, chamadas e transcripts. O container grava tudo em
91
+ sessões de agentes, rollups de custo e chamadas. Mensagens e transcripts são enviados nos
92
+ níveis de captura que os habilitam. O container grava tudo em
89
93
  `/data/observer.sqlite`; não monta `C:\GitHub` nem qualquer `.WendKeep-vault`. Markdown é aceito
90
94
  somente como conteúdo de uma coluna SQL e volta a existir como arquivo apenas pela exportação
91
95
  read-only sob demanda. `memory import` faz a carga inicial e retorna a paridade por arquivo e hash.
@@ -106,7 +110,9 @@ corpo antes de ingerir. O outbox é transporte temporário, não autoridade.
106
110
  - `project_not_registered`: rode `observer register` antes de publicar.
107
111
  - `host loopback`: troque `0.0.0.0` ou endereço LAN por `127.0.0.1`.
108
112
  - Outbox pendente: o serviço estava indisponível; preserve `.brain/observer-outbox/` e
109
- `.brain/observer-sql-outbox/` e repita o publisher. Não apague eventos manualmente.
113
+ `.brain/observer-sql-outbox/` e repita o publisher. Adicione também
114
+ `.brain/observer-sql-state.json` e `.brain/observer-sql-outbox/` ao ignore do Vault. Não apague eventos manualmente.
115
+ - `WENDKEEP_OBSERVER_NODE_UNSUPPORTED`: execute o Observer em Node.js 22.13 ou mais recente.
110
116
  - Se a memória ou o consumo ficarem incompletos, verifique a tela Sincronização, preserve o
111
117
  outbox e rode `observer memory import` para reconstruir a carga a partir do vault.
112
118
 
@@ -26,11 +26,11 @@ Claude/Codex e um vault vinculado ao projeto correto.
26
26
  ## Sintaxe
27
27
 
28
28
  ```bash
29
- npx wendkeep hook <nome>
29
+ npx --no-install wendkeep hook <nome>
30
30
  npx wendkeep session list
31
31
  npx wendkeep session show <id>
32
32
  npx wendkeep session use <id>
33
- npx wendkeep hook session-backfill --session <id> [--write]
33
+ npx --no-install wendkeep hook session-backfill --session <id> [--write]
34
34
  npx wendkeep import [opções]
35
35
  ```
36
36
 
@@ -95,8 +95,8 @@ npx wendkeep import [opções]
95
95
  ```bash
96
96
  npx wendkeep session list
97
97
  npx wendkeep session show 019abc-session-id
98
- npx wendkeep hook session-backfill --session 019abc-session-id
99
- npx wendkeep hook session-backfill --session 019abc-session-id --write
98
+ npx --no-install wendkeep hook session-backfill --session 019abc-session-id
99
+ npx --no-install wendkeep hook session-backfill --session 019abc-session-id --write
100
100
  npx wendkeep import --source codex --since 2026-07-01 --dry-run --json
101
101
  ```
102
102
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // understand-inject — SessionStart hook (agent-agnostic, run via `npx wendkeep hook
2
+ // understand-inject — SessionStart hook (agent-agnostic, run via `npx --no-install wendkeep hook
3
3
  // understand-inject`). If the Understand-Anything domain graph has been generated
4
4
  // (`.understand-anything/knowledge-graph.json` at the project root), inject a cheap
5
5
  // slice of it into the session; otherwise stay silent. Never breaks the session.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.72.0",
3
+ "version": "0.72.1",
4
4
  "description": "Vault-first persistent memory for AI coding agents, with an optional profile-aware governance runtime: OFF, FLOW, GUIDE, GOVERN, or ASSURE. Local-first and agent-agnostic (Claude Code, Codex, Cursor…).",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -41,10 +41,12 @@
41
41
  "node": ">=18"
42
42
  },
43
43
  "scripts": {
44
- "check": "node --check scripts/release.mjs && node --check scripts/release-plan.mjs && node --check bin/wendkeep.mjs && node --check packages/cli/src/index.mjs && node --check src/init.mjs && node --check src/doctor.mjs && node --check src/project-vault.mjs && node --check src/observer-snapshot.mjs && node --check src/observer-store.mjs && node --check src/observer-memory.mjs && node --check src/observer-memory-publish.mjs && node --check src/observer-sql-store.mjs && node --check src/observer-sql-migrate.mjs && node --check src/observer-sql-publish.mjs && node --check src/observer-transcript-store.mjs && node --check src/observer-server.mjs && node --check src/observer.mjs && node --check src/observer-publish.mjs && node --check src/operating-profile.mjs && node --check src/profile.mjs && node --check src/flow.mjs && node --check web/observer/app.mjs && node --check hooks/observer-publish.mjs && node --check hooks/operating-profile-runtime.mjs && node --check hooks/operating-profile-task-store.mjs && node --check hooks/flow-core.mjs && node --check hooks/flow-protected-policy.mjs && node --check hooks/git-snapshot.mjs && node --check hooks/vault-path-safety.mjs && node --check hooks/vault-runtime-store.mjs && node --check packages/harness/src/index.mjs && node --check packages/harness/src/flow-store.mjs && node --check packages/harness/src/operating-profile.mjs && node --check packages/harness/src/sensors-core.mjs && node --check packages/integrations/src/host-hooks.mjs && node --check packages/integrations/src/hook-envelope.mjs && node --check packages/integrations/src/prompt-content.mjs && node --check packages/integrations/src/transcript-usage.mjs && node --check packages/integrations/src/transcripts.mjs && node --check packages/integrations/src/session-identity.mjs && node --check packages/integrations/src/index.mjs && node --check packages/mcp/src/config.mjs && node --check packages/mcp/src/index.mjs && node --check packages/vault/src/index.mjs && node --check packages/vault/src/project-vault.mjs && node --check packages/vault/src/vault-path-safety.mjs && node --check packages/vault/src/locale.mjs && node --check packages/vault/src/memory-schema.mjs && node --check packages/vault/src/memory-mode.mjs && node --check packages/vault/src/memory-handoff.mjs && node --check packages/vault/src/memory-store.mjs && node --check packages/vault/src/validate-core.mjs && node --check packages/vault/src/validate-memory.mjs",
44
+ "check": "node --check scripts/release.mjs && node --check scripts/release-plan.mjs && node --check scripts/release-provenance.mjs && node --check scripts/run-scope.mjs && node --check src/release-provenance.mjs && node --check bin/wendkeep.mjs && node --check packages/cli/src/index.mjs && node --check src/init.mjs && node --check src/doctor.mjs && node --check src/project-vault.mjs && node --check src/observer-auth.mjs && node --check src/observer-privacy.mjs && node --check src/observer-snapshot.mjs && node --check src/observer-store.mjs && node --check src/observer-memory.mjs && node --check src/observer-memory-publish.mjs && node --check src/observer-sql-store.mjs && node --check src/observer-sql-migrate.mjs && node --check src/observer-sql-publish.mjs && node --check src/observer-transcript-store.mjs && node --check src/observer-server.mjs && node --check src/observer.mjs && node --check src/observer-publish.mjs && node --check src/operating-profile.mjs && node --check src/profile.mjs && node --check src/flow.mjs && node --check web/observer/app.mjs && node --check hooks/observer-publish.mjs && node --check hooks/operating-profile-runtime.mjs && node --check hooks/operating-profile-task-store.mjs && node --check hooks/flow-core.mjs && node --check hooks/flow-protected-policy.mjs && node --check hooks/git-snapshot.mjs && node --check hooks/vault-path-safety.mjs && node --check hooks/vault-runtime-store.mjs && node --check packages/harness/src/index.mjs && node --check packages/harness/src/flow-store.mjs && node --check packages/harness/src/operating-profile.mjs && node --check packages/harness/src/sensors-core.mjs && node --check packages/integrations/src/host-hooks.mjs && node --check packages/integrations/src/hook-envelope.mjs && node --check packages/integrations/src/prompt-content.mjs && node --check packages/integrations/src/transcript-usage.mjs && node --check packages/integrations/src/transcripts.mjs && node --check packages/integrations/src/session-identity.mjs && node --check packages/integrations/src/index.mjs && node --check packages/mcp/src/config.mjs && node --check packages/mcp/src/index.mjs && node --check packages/vault/src/index.mjs && node --check packages/vault/src/project-vault.mjs && node --check packages/vault/src/vault-path-safety.mjs && node --check packages/vault/src/locale.mjs && node --check packages/vault/src/memory-schema.mjs && node --check packages/vault/src/memory-mode.mjs && node --check packages/vault/src/memory-handoff.mjs && node --check packages/vault/src/memory-store.mjs && node --check packages/vault/src/validate-core.mjs && node --check packages/vault/src/validate-memory.mjs",
45
45
  "test": "node --test --test-concurrency=2",
46
+ "test:core": "node scripts/run-scope.mjs core",
46
47
  "release": "node scripts/release.mjs",
47
48
  "release:dry": "node scripts/release.mjs --dry-run",
49
+ "release:provenance": "node scripts/release-provenance.mjs",
48
50
  "prepack": "node scripts/readme-pack.mjs pre",
49
51
  "postpack": "node scripts/readme-pack.mjs post"
50
52
  },
@@ -70,7 +72,6 @@
70
72
  "url": "https://github.com/rogersialves/wendkeep/issues"
71
73
  },
72
74
  "devDependencies": {
73
- "acorn": "^8.18.0",
74
- "wendkeep": "^0.69.0"
75
+ "acorn": "^8.18.0"
75
76
  }
76
77
  }
@@ -32,7 +32,7 @@ export const SESSION_HOOKS = [
32
32
  ];
33
33
 
34
34
  export function hookCommand(name) {
35
- return `npx wendkeep hook ${name}`;
35
+ return `npx --no-install wendkeep hook ${name}`;
36
36
  }
37
37
 
38
38
  // Forma node-direta do comando de hook: 1 processo (~100-250ms) em vez dos 3 do npx (cold-start
@@ -112,12 +112,25 @@ function readCheckedMemoryFile(vaultBase, path, encoding, label, { allowMissing
112
112
  return readFileSync(checked.target, encoding);
113
113
  }
114
114
 
115
+ export function memoryFileIdentityMatches(descriptor, target, {
116
+ platform = process.platform,
117
+ } = {}) {
118
+ if (descriptor.ino !== target.ino) return false;
119
+ // libuv before 1.51 can report an inconsistent Windows volume serial number
120
+ // between stat(path) and fstat(fd). The inode is still the file index; path
121
+ // containment/reparse checks and nlink validation remain independent guards.
122
+ return platform === 'win32' || descriptor.dev === target.dev;
123
+ }
124
+
115
125
  function assertOpenedMemoryFile(vaultBase, path, fd, label) {
116
126
  const checked = checkedMemoryFile(vaultBase, path, label, { allowMissing: false });
117
- const descriptor = fstatSync(fd);
118
- const target = statSync(checked.target);
119
- if (!descriptor.isFile() || descriptor.nlink > 1 || target.nlink > 1
120
- || descriptor.dev !== target.dev || descriptor.ino !== target.ino) {
127
+ // Windows file identities can exceed Number's safe integer range. Node 22.13 may
128
+ // round stat(path) and fstat(fd) differently for the same file, so compare the
129
+ // exact bigint values and keep nlink as the independent hardlink guard.
130
+ const descriptor = fstatSync(fd, { bigint: true });
131
+ const target = statSync(checked.target, { bigint: true });
132
+ if (!descriptor.isFile() || descriptor.nlink > 1n || target.nlink > 1n
133
+ || !memoryFileIdentityMatches(descriptor, target)) {
121
134
  throw unsafeMemoryPath(`${label} mudou de inode ou possui hardlink antes da mutação: ${checked.target}`);
122
135
  }
123
136
  return checked.target;
package/src/init.mjs CHANGED
@@ -323,7 +323,7 @@ const MESSAGES = {
323
323
  mcpSkipped: ' [4/5] .mcp.json ignorado (--no-mcp, sem companions MCP)',
324
324
  colorsSkipped: ' [5/5] cores ignoradas (--no-colors)',
325
325
  colors: (r) => ` [5/5] cores: ${r}`,
326
- runtimeIgnore: ' [!] ignore runtimes locais do wendkeep no Git quando o vault for versionado: .brain/.change-* .brain/runtime/flows/',
326
+ runtimeIgnore: ' [!] ignore runtimes locais do wendkeep no Git quando o vault for versionado: .brain/.change-* .brain/runtime/flows/ .brain/observer-sql-state.json .brain/observer-sql-outbox/',
327
327
  merged: 'mesclado', created: 'criado', bakSaved: ', .bak salvo',
328
328
  nextSteps: '\nPróximos passos:',
329
329
  step1: (v) => ` 1. Abra o vault no Obsidian: "Abrir pasta como cofre" -> ${v}`,
@@ -352,7 +352,7 @@ const MESSAGES = {
352
352
  mcpSkipped: ' [4/5] .mcp.json skipped (--no-mcp, no MCP companions)',
353
353
  colorsSkipped: ' [5/5] colors skipped (--no-colors)',
354
354
  colors: (r) => ` [5/5] colors: ${r}`,
355
- runtimeIgnore: ' [!] keep local wendkeep runtimes out of Git when the vault is versioned: .brain/.change-* .brain/runtime/flows/',
355
+ runtimeIgnore: ' [!] keep local wendkeep runtimes out of Git when the vault is versioned: .brain/.change-* .brain/runtime/flows/ .brain/observer-sql-state.json .brain/observer-sql-outbox/',
356
356
  merged: 'merged', created: 'created', bakSaved: ', .bak saved',
357
357
  nextSteps: '\nNext steps:',
358
358
  step1: (v) => ` 1. Open the vault in Obsidian: "Open folder as vault" -> ${v}`,
@@ -0,0 +1,10 @@
1
+ export function resolveObserverToken(value = '') {
2
+ return String(value || process.env.WENDKEEP_OBSERVER_TOKEN || '').trim();
3
+ }
4
+
5
+ export function observerAuthHeaders(token, headers = {}) {
6
+ const resolved = resolveObserverToken(token);
7
+ return resolved
8
+ ? { ...headers, authorization: `Bearer ${resolved}` }
9
+ : { ...headers };
10
+ }
@@ -12,6 +12,8 @@ import {
12
12
  } from 'node:fs';
13
13
  import { join } from 'node:path';
14
14
  import { MAX_MEMORY_CONTENT_BYTES } from './observer-memory.mjs';
15
+ import { observerAuthHeaders } from './observer-auth.mjs';
16
+ import { sanitizeObserverContent } from './observer-privacy.mjs';
15
17
 
16
18
  export const MEMORY_OUTBOX_REL = '.brain/observer-memory-outbox';
17
19
  export const MEMORY_STATE_FILE = '.brain/observer-memory-state.json';
@@ -103,7 +105,7 @@ function memoryFiles(vaultBase) {
103
105
 
104
106
  export function localMemoryManifest(vaultBase) {
105
107
  return Object.fromEntries(memoryFiles(vaultBase).map((file) => {
106
- const content = readFileSync(file.absolute, 'utf8');
108
+ const content = sanitizeObserverContent(readFileSync(file.absolute, 'utf8'));
107
109
  return [file.logicalPath, {
108
110
  logical_path: file.logicalPath,
109
111
  content_hash: hash(content),
@@ -159,7 +161,7 @@ export function buildMemoryEventBatch({
159
161
  const events = [];
160
162
 
161
163
  for (const file of currentFiles) {
162
- const content = readFileSync(file.absolute, 'utf8');
164
+ const content = sanitizeObserverContent(readFileSync(file.absolute, 'utf8'));
163
165
  if (Buffer.byteLength(content, 'utf8') > MAX_MEMORY_CONTENT_BYTES) {
164
166
  throw new Error('arquivo excede o limite de memória: ' + file.logicalPath);
165
167
  }
@@ -231,12 +233,12 @@ export function listMemoryOutbox(vaultBase) {
231
233
  .map((name) => join(dir, name));
232
234
  }
233
235
 
234
- async function postBatch(url, projectId, events, fetchImpl = globalThis.fetch) {
236
+ async function postBatch(url, projectId, events, fetchImpl = globalThis.fetch, token = '') {
235
237
  const response = await fetchImpl(
236
238
  String(url).replace(/\/$/, '') + '/v1/projects/' + encodeURIComponent(projectId) + '/memory/events',
237
239
  {
238
240
  method: 'POST',
239
- headers: { 'content-type': 'application/json', accept: 'application/json' },
241
+ headers: observerAuthHeaders(token, { 'content-type': 'application/json', accept: 'application/json' }),
240
242
  body: JSON.stringify({ events }),
241
243
  },
242
244
  );
@@ -249,6 +251,7 @@ export async function retryObserverMemoryOutbox({
249
251
  projectId = projectIdFromVault(vaultBase),
250
252
  url,
251
253
  fetchImpl = globalThis.fetch,
254
+ token = process.env.WENDKEEP_OBSERVER_TOKEN || '',
252
255
  } = {}) {
253
256
  const files = listMemoryOutbox(vaultBase);
254
257
  if (!url) return { attempted: 0, confirmed: 0, pending: files.length };
@@ -262,7 +265,7 @@ export async function retryObserverMemoryOutbox({
262
265
  unlinkSync(path);
263
266
  continue;
264
267
  }
265
- await postBatch(url, projectId, batch.events, fetchImpl);
268
+ await postBatch(url, projectId, batch.events, fetchImpl, token);
266
269
  unlinkSync(path);
267
270
  confirmed += 1;
268
271
  } catch {
@@ -277,10 +280,11 @@ export async function compareMemoryParity({
277
280
  projectId = projectIdFromVault(vaultBase),
278
281
  url,
279
282
  fetchImpl = globalThis.fetch,
283
+ token = process.env.WENDKEEP_OBSERVER_TOKEN || '',
280
284
  } = {}) {
281
285
  const response = await fetchImpl(
282
286
  String(url).replace(/\/$/, '') + '/v1/projects/' + encodeURIComponent(projectId) + '/memory/tree',
283
- { headers: { accept: 'application/json' } },
287
+ { headers: observerAuthHeaders(token, { accept: 'application/json' }) },
284
288
  );
285
289
  if (!response.ok) throw new Error('Observer respondeu HTTP ' + response.status + '.');
286
290
  const body = await response.json();
@@ -309,9 +313,10 @@ export async function publishObserverMemory({
309
313
  sourceTurnId = '',
310
314
  now = new Date(),
311
315
  fetchImpl = globalThis.fetch,
316
+ token = process.env.WENDKEEP_OBSERVER_TOKEN || '',
312
317
  } = {}) {
313
318
  if (!vaultBase || !projectId) throw new Error('vaultBase e projectId são obrigatórios.');
314
- await retryObserverMemoryOutbox({ vaultBase, projectId, url, fetchImpl });
319
+ await retryObserverMemoryOutbox({ vaultBase, projectId, url, fetchImpl, token });
315
320
  const state = readState(vaultBase);
316
321
  const batch = buildMemoryEventBatch({ vaultBase, projectId, sourceSessionId, sourceTurnId, now, state });
317
322
  if (batch.events.length === 0) {
@@ -324,7 +329,7 @@ export async function publishObserverMemory({
324
329
  return { ok: false, queued: true, scanned: batch.scanned, changed: batch.changed, pending: listMemoryOutbox(vaultBase).length, hookExitCode: 0 };
325
330
  }
326
331
  try {
327
- await postBatch(url, projectId, batch.events, fetchImpl);
332
+ await postBatch(url, projectId, batch.events, fetchImpl, token);
328
333
  commitMemoryPublishState(vaultBase, batch.nextState);
329
334
  return { ok: true, queued: false, scanned: batch.scanned, changed: batch.changed, pending: listMemoryOutbox(vaultBase).length };
330
335
  } catch (error) {
@@ -0,0 +1,23 @@
1
+ import { basename } from 'node:path';
2
+
3
+ const TRANSCRIPT_PATH_KEY = /^(?:transcript_path|agent_transcript_path|transcriptPath|agentTranscriptPath)$/i;
4
+ const TRANSCRIPT_PATH_LINE = /^(\s*["']?(?:transcript_path|agent_transcript_path|transcriptPath|agentTranscriptPath)["']?\s*:\s*)(["']?)(.*?)(\2)(\s*,?\s*)$/gmi;
5
+
6
+ function sourceLabel(value) {
7
+ return basename(String(value || '').replaceAll('\\', '/'));
8
+ }
9
+
10
+ export function sanitizeObserverContent(content) {
11
+ return String(content || '').replace(TRANSCRIPT_PATH_LINE, (_line, prefix, quote, value, _closing, suffix) => (
12
+ `${prefix}${quote}${sourceLabel(value)}${quote}${suffix}`
13
+ ));
14
+ }
15
+
16
+ export function sanitizeObserverMetadata(value) {
17
+ if (Array.isArray(value)) return value.map(sanitizeObserverMetadata);
18
+ if (!value || typeof value !== 'object') return value;
19
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
20
+ key,
21
+ TRANSCRIPT_PATH_KEY.test(key) ? sourceLabel(item) : sanitizeObserverMetadata(item),
22
+ ]));
23
+ }
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSyn
2
2
  import { join } from 'node:path';
3
3
  import { buildProjectSnapshot } from './observer-snapshot.mjs';
4
4
  import { publishObserverSql } from './observer-sql-publish.mjs';
5
+ import { observerAuthHeaders, resolveObserverToken } from './observer-auth.mjs';
5
6
 
6
7
  const OUTBOX_REL = join('.brain', 'observer-outbox');
7
8
  const REQUEST_TIMEOUT_MS = 500;
@@ -46,15 +47,15 @@ function removeOutbox(vaultBase, eventId) {
46
47
  if (existsSync(path)) unlinkSync(path);
47
48
  }
48
49
 
49
- async function postSnapshot(url, event) {
50
+ async function postSnapshot(url, event, token) {
50
51
  const controller = new AbortController();
51
52
  const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
52
53
  try {
53
54
  const response = await fetch(`${String(url).replace(/\/$/, '')}/v1/projects/${encodeURIComponent(event.project_id)}/snapshot`, {
54
55
  method: 'POST',
55
- headers: {
56
+ headers: observerAuthHeaders(token, {
56
57
  'content-type': 'application/json',
57
- },
58
+ }),
58
59
  body: JSON.stringify(event),
59
60
  signal: controller.signal,
60
61
  });
@@ -70,14 +71,14 @@ async function postSnapshot(url, event) {
70
71
  }
71
72
  }
72
73
 
73
- export async function retryObserverOutbox({ vaultBase, url } = {}) {
74
+ export async function retryObserverOutbox({ vaultBase, url, token = process.env.WENDKEEP_OBSERVER_TOKEN || '' } = {}) {
74
75
  if (!url) return { attempted: 0, confirmed: 0, pending: listOutbox(vaultBase).length };
75
76
  let attempted = 0;
76
77
  let confirmed = 0;
77
78
  for (const event of listOutbox(vaultBase)) {
78
79
  attempted += 1;
79
80
  try {
80
- await postSnapshot(url, event);
81
+ await postSnapshot(url, event, token);
81
82
  removeOutbox(vaultBase, event.event_id);
82
83
  confirmed += 1;
83
84
  } catch { /* preserve the event for a later retry */ }
@@ -91,6 +92,7 @@ export async function publishObserverSnapshot({
91
92
  url = process.env.WENDKEEP_OBSERVER_URL || '',
92
93
  now = new Date(),
93
94
  input = {},
95
+ token = process.env.WENDKEEP_OBSERVER_TOKEN || '',
94
96
  } = {}) {
95
97
  try {
96
98
  const event = buildProjectSnapshot({ vaultBase, projectRoot, now });
@@ -100,15 +102,16 @@ export async function publishObserverSnapshot({
100
102
  url,
101
103
  input,
102
104
  now,
105
+ token,
103
106
  });
104
107
  if (!url) return { ok: sql.ok, skipped: true, queued: sql.queued, hookExitCode: 0, event_id: event.event_id, sql };
105
108
 
106
- await retryObserverOutbox({ vaultBase, url });
109
+ await retryObserverOutbox({ vaultBase, url, token });
107
110
  // SQL is the live authority. Keep the legacy-shaped `memory` field for
108
111
  // older integrations while reporting the real SQL publication separately.
109
112
  const memory = { ok: sql.ok, queued: sql.queued, changed: sql.changed, pending: sql.pending, authority: 'sqlite' };
110
113
  try {
111
- const response = await postSnapshot(url, event);
114
+ const response = await postSnapshot(url, event, resolveObserverToken(token));
112
115
  return {
113
116
  ok: true,
114
117
  queued: false,
@@ -1,4 +1,5 @@
1
1
  import { createServer } from 'node:http';
2
+ import { createHash, timingSafeEqual } from 'node:crypto';
2
3
  import { readFileSync } from 'node:fs';
3
4
  import { fileURLToPath } from 'node:url';
4
5
  import { gunzipSync } from 'node:zlib';
@@ -52,6 +53,39 @@ function loopbackOnly(host) {
52
53
  return LOOPBACK_HOSTS.has(String(host || '').toLowerCase());
53
54
  }
54
55
 
56
+ function safeTokenEqual(actual, expected) {
57
+ if (!actual || !expected) return false;
58
+ const left = createHash('sha256').update(String(actual)).digest();
59
+ const right = createHash('sha256').update(String(expected)).digest();
60
+ return timingSafeEqual(left, right);
61
+ }
62
+
63
+ function bearerToken(req) {
64
+ const match = String(req.headers.authorization || '').match(/^Bearer\s+(.+)$/i);
65
+ return match?.[1] || '';
66
+ }
67
+
68
+ function requestHostname(value) {
69
+ try { return new URL(`http://${String(value || '')}`).hostname.toLowerCase(); }
70
+ catch { return ''; }
71
+ }
72
+
73
+ function validateAuthority(req, { loopback }) {
74
+ const hostname = requestHostname(req.headers.host);
75
+ if (!hostname || (loopback && !LOOPBACK_HOSTS.has(hostname))) {
76
+ return { ok: false, status: 421, code: 'invalid_host', message: 'Host não corresponde ao binding do Observer.' };
77
+ }
78
+ const origin = String(req.headers.origin || '');
79
+ if (origin) {
80
+ let originHostname = '';
81
+ try { originHostname = new URL(origin).hostname.toLowerCase(); } catch { /* invalid below */ }
82
+ if (!originHostname || originHostname !== hostname) {
83
+ return { ok: false, status: 403, code: 'invalid_origin', message: 'Origin não corresponde ao Host do Observer.' };
84
+ }
85
+ }
86
+ return { ok: true };
87
+ }
88
+
55
89
  function json(res, status, body) {
56
90
  const content = JSON.stringify(body);
57
91
  res.writeHead(status, {
@@ -184,10 +218,16 @@ export async function startObserverServer({
184
218
  port = 8787,
185
219
  dataDir,
186
220
  allowNonLoopback = false,
221
+ token = process.env.WENDKEEP_OBSERVER_TOKEN || '',
187
222
  } = {}) {
188
223
  if (!loopbackOnly(host) && !allowNonLoopback) {
189
224
  throw new Error(`Observer HTTP aceita somente host loopback; recebido: ${host}`);
190
225
  }
226
+ if (!loopbackOnly(host) && !token) {
227
+ const error = new Error('Observer non-loopback exige --token ou WENDKEEP_OBSERVER_TOKEN.');
228
+ error.code = 'WENDKEEP_OBSERVER_TOKEN_REQUIRED';
229
+ throw error;
230
+ }
191
231
  if (!dataDir) throw new Error('dataDir é obrigatório.');
192
232
  const sqlDb = ensureObserverDatabase(dataDir);
193
233
  const databaseMigration = migrateObserverDatabase(sqlDb);
@@ -204,6 +244,17 @@ export async function startObserverServer({
204
244
  const server = createServer(async (req, res) => {
205
245
  try {
206
246
  const pathname = new URL(req.url || '/', 'http://127.0.0.1').pathname;
247
+ const authority = validateAuthority(req, { loopback: loopbackOnly(host) });
248
+ if (!authority.ok) {
249
+ errorResponse(res, authority.status, authority.code, authority.message);
250
+ return;
251
+ }
252
+ const authenticated = safeTokenEqual(bearerToken(req), token);
253
+ const mutating = !['GET', 'HEAD', 'OPTIONS'].includes(String(req.method || '').toUpperCase());
254
+ if ((mutating || !loopbackOnly(host)) && !authenticated) {
255
+ errorResponse(res, 401, 'observer_auth_required', 'Bearer token válido é obrigatório para esta operação.');
256
+ return;
257
+ }
207
258
  if (req.method === 'GET' && pathname === '/healthz') {
208
259
  json(res, 200, {
209
260
  ok: true,
@@ -13,6 +13,8 @@ import { gzipSync } from 'node:zlib';
13
13
  import { parseTranscriptContent } from '../packages/integrations/src/transcripts.mjs';
14
14
  import { parseSessionCost, } from './cost.mjs';
15
15
  import { buildSessionIdentityMap, listMigrationDocuments, parseFrontmatter, sessionEvents } from './observer-sql-migrate.mjs';
16
+ import { observerAuthHeaders } from './observer-auth.mjs';
17
+ import { sanitizeObserverContent, sanitizeObserverMetadata } from './observer-privacy.mjs';
16
18
 
17
19
  export const SQL_OUTBOX_REL = '.brain/observer-sql-outbox';
18
20
  export const SQL_STATE_REL = '.brain/observer-sql-state.json';
@@ -20,6 +22,28 @@ const SQL_SCHEMA_VERSION = 1;
20
22
  export const SQL_EVENT_BATCH_SIZE = 64;
21
23
  export const SQL_EVENT_BATCH_BYTES = 8 * 1024 * 1024;
22
24
  const REQUEST_TIMEOUT_MS = 15000;
25
+ const MAX_REQUEST_TIMEOUT_MS = 120000;
26
+ const REQUEST_TIMEOUT_BYTES_STEP = 1024 * 1024;
27
+ const CAPTURE_LEVELS = new Set(['metadata', 'messages', 'full-transcript']);
28
+
29
+ export function observerSqlRequestTimeoutMs(rawBytes) {
30
+ const size = Math.max(0, Number(rawBytes) || 0);
31
+ const oversizedBytes = Math.max(0, size - SQL_EVENT_BATCH_BYTES);
32
+ return Math.min(
33
+ MAX_REQUEST_TIMEOUT_MS,
34
+ REQUEST_TIMEOUT_MS + Math.ceil(oversizedBytes / REQUEST_TIMEOUT_BYTES_STEP) * 1000,
35
+ );
36
+ }
37
+
38
+ export function normalizeObserverCaptureLevel(value = process.env.WENDKEEP_OBSERVER_CAPTURE_LEVEL || 'metadata') {
39
+ const level = String(value || 'metadata').trim().toLowerCase();
40
+ if (!CAPTURE_LEVELS.has(level)) {
41
+ const error = new Error(`Nível de captura inválido: ${level}. Use metadata, messages ou full-transcript.`);
42
+ error.code = 'WENDKEEP_OBSERVER_CAPTURE_LEVEL_INVALID';
43
+ throw error;
44
+ }
45
+ return level;
46
+ }
23
47
 
24
48
  function text(value, fallback = '') { return String(value ?? fallback); }
25
49
  function hash(value) { return createHash('sha256').update(typeof value === 'string' ? value : JSON.stringify(value)).digest('hex'); }
@@ -73,7 +97,9 @@ function tokenPayload(usage = {}) {
73
97
  }
74
98
 
75
99
  function documentEvent({ projectId, logicalPath, content, metadata, revision, occurredAt }) {
76
- const contentHash = hash(content);
100
+ const safeContent = sanitizeObserverContent(content);
101
+ const safeMetadata = sanitizeObserverMetadata(metadata);
102
+ const contentHash = hash(safeContent);
77
103
  return {
78
104
  schema_version: 1,
79
105
  event_id: eventId('document', projectId, `${logicalPath}:${revision}:${contentHash}`),
@@ -89,11 +115,11 @@ function documentEvent({ projectId, logicalPath, content, metadata, revision, oc
89
115
  : logicalPath.startsWith('07-Specs/') ? 'spec'
90
116
  : logicalPath.startsWith('08-Mudanças/') ? 'change' : 'memory',
91
117
  title: basename(logicalPath).replace(/\.md$/i, ''),
92
- content,
118
+ content: safeContent,
93
119
  content_hash: contentHash,
94
120
  revision,
95
- metadata,
96
- source_session_id: text(metadata?.session_id),
121
+ metadata: safeMetadata,
122
+ source_session_id: text(safeMetadata?.session_id),
97
123
  },
98
124
  };
99
125
  }
@@ -123,7 +149,7 @@ function agentEvent({ projectId, sessionId, agentId, parentAgentId = '', role =
123
149
  };
124
150
  }
125
151
 
126
- function transcriptCalls({ projectId, sessionId, agentId, role, provider, modelFallback, transcriptId, content, occurredAt }) {
152
+ function transcriptCalls({ projectId, sessionId, agentId, role, provider, modelFallback, transcriptId, content, occurredAt, includeMessages }) {
127
153
  let parsed;
128
154
  try { parsed = parseTranscriptContent(content); } catch { return []; }
129
155
  return (parsed.turns || []).flatMap((turn, index) => {
@@ -154,8 +180,8 @@ function transcriptCalls({ projectId, sessionId, agentId, role, provider, modelF
154
180
  cost_usd: 0,
155
181
  cost_status: 'unknown',
156
182
  transcript_id: transcriptId,
157
- prompt_text: prompt,
158
- response_text: response,
183
+ prompt_text: includeMessages ? prompt : '',
184
+ response_text: includeMessages ? response : '',
159
185
  status: turn.status === 'aborted' ? 'aborted' : 'complete',
160
186
  metadata: { tools: turn.tools || [], source: 'transcript-parser' },
161
187
  },
@@ -180,7 +206,7 @@ function sourceCandidates({ vaultBase, logicalPath, fm, input }) {
180
206
  return candidates;
181
207
  }
182
208
 
183
- function completeTranscriptEvents({ projectId, sessionId, mainAgentId, provider, model, source, now }) {
209
+ function completeTranscriptEvents({ projectId, sessionId, mainAgentId, provider, model, source, now, captureLevel }) {
184
210
  const content = readFileSync(source.path, 'utf8');
185
211
  const agentId = source.role === 'subagent'
186
212
  ? `${projectId}:${sessionId}:subagent:${hash(source.path).slice(0, 16)}`
@@ -189,7 +215,7 @@ function completeTranscriptEvents({ projectId, sessionId, mainAgentId, provider,
189
215
  ? agentEvent({ projectId, sessionId, agentId, parentAgentId: mainAgentId, role: 'subagent', provider, model, input: source.agentInput, occurredAt: now })
190
216
  : null;
191
217
  const fingerprint = hash(content);
192
- const transcript = {
218
+ const transcript = captureLevel === 'full-transcript' ? {
193
219
  schema_version: 1,
194
220
  event_id: eventId('transcript', projectId, `${source.transcriptId}:${fingerprint}`),
195
221
  kind: 'transcript.upsert',
@@ -202,10 +228,21 @@ function completeTranscriptEvents({ projectId, sessionId, mainAgentId, provider,
202
228
  coverage: 'complete',
203
229
  content,
204
230
  source: 'hook-transcript',
205
- metadata: { original_path: source.path.replaceAll('\\', '/') },
231
+ metadata: { source_label: basename(source.path), source_hash: hash(normalizePath(source.path)) },
206
232
  },
207
- };
208
- const calls = transcriptCalls({ projectId, sessionId, agentId, role: source.role, provider, modelFallback: model, transcriptId: source.transcriptId, content, occurredAt: now });
233
+ } : null;
234
+ const calls = transcriptCalls({
235
+ projectId,
236
+ sessionId,
237
+ agentId,
238
+ role: source.role,
239
+ provider,
240
+ modelFallback: model,
241
+ transcriptId: source.transcriptId,
242
+ content,
243
+ occurredAt: now,
244
+ includeMessages: captureLevel !== 'metadata',
245
+ });
209
246
  return { events: [agent, transcript, ...calls].filter(Boolean), fingerprint, transcriptId: source.transcriptId };
210
247
  }
211
248
 
@@ -218,9 +255,10 @@ function dedupeEvents(events) {
218
255
  });
219
256
  }
220
257
 
221
- export function buildObserverSqlEventBatch({ vaultBase, projectId, input = {}, now = new Date(), state = readState(vaultBase), remoteDocuments = {} } = {}) {
258
+ export function buildObserverSqlEventBatch({ vaultBase, projectId, input = {}, now = new Date(), state = readState(vaultBase), remoteDocuments = {}, captureLevel = process.env.WENDKEEP_OBSERVER_CAPTURE_LEVEL || 'metadata' } = {}) {
222
259
  if (!vaultBase || !projectId) throw new Error('vaultBase e projectId são obrigatórios.');
223
260
  const occurredAt = isoNow(now);
261
+ const resolvedCaptureLevel = normalizeObserverCaptureLevel(captureLevel);
224
262
  const nextState = { schema_version: SQL_SCHEMA_VERSION, files: { ...(state.files || {}) }, transcripts: { ...(state.transcripts || {}) } };
225
263
  const events = [];
226
264
  const sessionContexts = [];
@@ -235,7 +273,7 @@ export function buildObserverSqlEventBatch({ vaultBase, projectId, input = {}, n
235
273
  let changed = 0;
236
274
  for (const file of files) {
237
275
  const content = readFileSync(file.absolute, 'utf8');
238
- const contentHash = hash(content);
276
+ const contentHash = hash(sanitizeObserverContent(content));
239
277
  const previous = state.files?.[file.logicalPath];
240
278
  const remote = remoteDocuments?.[file.logicalPath];
241
279
  const baseRevision = Math.max(Number(previous?.revision || 0), Number(remote?.revision || 0));
@@ -263,9 +301,10 @@ export function buildObserverSqlEventBatch({ vaultBase, projectId, input = {}, n
263
301
  for (const source of sources) {
264
302
  const content = readFileSync(source.path, 'utf8');
265
303
  const fingerprint = hash(content);
266
- if (state.transcripts?.[source.transcriptId]?.content_hash === fingerprint) continue;
267
- const complete = completeTranscriptEvents({ projectId, sessionId, mainAgentId, provider, model, source, now: occurredAt });
268
- nextState.transcripts[source.transcriptId] = { content_hash: complete.fingerprint, coverage: 'complete' };
304
+ const previousTranscript = state.transcripts?.[source.transcriptId];
305
+ if (previousTranscript?.content_hash === fingerprint && previousTranscript?.coverage === resolvedCaptureLevel) continue;
306
+ const complete = completeTranscriptEvents({ projectId, sessionId, mainAgentId, provider, model, source, now: occurredAt, captureLevel: resolvedCaptureLevel });
307
+ nextState.transcripts[source.transcriptId] = { content_hash: complete.fingerprint, coverage: resolvedCaptureLevel };
269
308
  const summaryId = complete.transcriptId;
270
309
  for (const event of complete.events) {
271
310
  if (event.kind === 'agent.upsert' && event.payload.agent_id !== mainAgentId) events.push(event);
@@ -297,14 +336,15 @@ export function listSqlOutbox(vaultBase) {
297
336
  });
298
337
  }
299
338
 
300
- async function postSqlChunk({ url, projectId, events, fetchImpl = globalThis.fetch }) {
339
+ async function postSqlChunk({ url, projectId, events, fetchImpl = globalThis.fetch, token = '' }) {
301
340
  const controller = new AbortController();
302
- const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
303
- const wireBody = gzipSync(Buffer.from(JSON.stringify({ events }), 'utf8'));
341
+ const rawBody = Buffer.from(JSON.stringify({ events }), 'utf8');
342
+ const timer = setTimeout(() => controller.abort(), observerSqlRequestTimeoutMs(rawBody.byteLength));
343
+ const wireBody = gzipSync(rawBody);
304
344
  try {
305
345
  const response = await fetchImpl(`${String(url).replace(/\/$/, '')}/v1/projects/${encodeURIComponent(projectId)}/ingest`, {
306
346
  method: 'POST',
307
- headers: { 'content-type': 'application/json', 'content-encoding': 'gzip', accept: 'application/json' },
347
+ headers: observerAuthHeaders(token, { 'content-type': 'application/json', 'content-encoding': 'gzip', accept: 'application/json' }),
308
348
  body: wireBody,
309
349
  signal: controller.signal,
310
350
  });
@@ -314,11 +354,11 @@ async function postSqlChunk({ url, projectId, events, fetchImpl = globalThis.fet
314
354
  } finally { clearTimeout(timer); }
315
355
  }
316
356
 
317
- async function readRemoteDocuments({ url, projectId, fetchImpl = globalThis.fetch }) {
357
+ async function readRemoteDocuments({ url, projectId, fetchImpl = globalThis.fetch, token = '' }) {
318
358
  if (!url) return {};
319
359
  try {
320
360
  const response = await fetchImpl(`${String(url).replace(/\/$/, '')}/v1/projects/${encodeURIComponent(projectId)}/memory/tree`, {
321
- headers: { accept: 'application/json' },
361
+ headers: observerAuthHeaders(token, { accept: 'application/json' }),
322
362
  });
323
363
  if (!response.ok) return {};
324
364
  const body = await response.json().catch(() => ({}));
@@ -328,7 +368,7 @@ async function readRemoteDocuments({ url, projectId, fetchImpl = globalThis.fetc
328
368
  }
329
369
  }
330
370
 
331
- async function postSqlBatch({ url, projectId, events, fetchImpl = globalThis.fetch }) {
371
+ async function postSqlBatch({ url, projectId, events, fetchImpl = globalThis.fetch, token = '' }) {
332
372
  const aggregate = { accepted: 0, rejected: 0, conflicts: 0, stale: 0, duplicates: 0 };
333
373
  let chunk = [];
334
374
  let chunkBytes = 0;
@@ -339,6 +379,7 @@ async function postSqlBatch({ url, projectId, events, fetchImpl = globalThis.fet
339
379
  projectId,
340
380
  events: items,
341
381
  fetchImpl,
382
+ token,
342
383
  });
343
384
  for (const key of Object.keys(aggregate)) aggregate[key] += Number(response?.[key]) || 0;
344
385
  };
@@ -358,7 +399,7 @@ async function postSqlBatch({ url, projectId, events, fetchImpl = globalThis.fet
358
399
  return aggregate;
359
400
  }
360
401
 
361
- export async function retryObserverSqlOutbox({ vaultBase, projectId, url, fetchImpl = globalThis.fetch } = {}) {
402
+ export async function retryObserverSqlOutbox({ vaultBase, projectId, url, fetchImpl = globalThis.fetch, token = process.env.WENDKEEP_OBSERVER_TOKEN || '' } = {}) {
362
403
  const pending = listSqlOutbox(vaultBase);
363
404
  if (!url) return { attempted: 0, confirmed: 0, pending: pending.length };
364
405
  let attempted = 0;
@@ -366,7 +407,7 @@ export async function retryObserverSqlOutbox({ vaultBase, projectId, url, fetchI
366
407
  for (const batch of pending) {
367
408
  attempted += 1;
368
409
  try {
369
- await postSqlBatch({ url, projectId, events: batch.events, fetchImpl });
410
+ await postSqlBatch({ url, projectId, events: batch.events, fetchImpl, token });
370
411
  unlinkSync(batch.path);
371
412
  confirmed += 1;
372
413
  } catch { break; }
@@ -374,14 +415,14 @@ export async function retryObserverSqlOutbox({ vaultBase, projectId, url, fetchI
374
415
  return { attempted, confirmed, pending: listSqlOutbox(vaultBase).length };
375
416
  }
376
417
 
377
- export async function publishObserverSql({ vaultBase, projectId, url = process.env.WENDKEEP_OBSERVER_URL || '', input = {}, now = new Date(), fetchImpl = globalThis.fetch } = {}) {
418
+ export async function publishObserverSql({ vaultBase, projectId, url = process.env.WENDKEEP_OBSERVER_URL || '', input = {}, now = new Date(), fetchImpl = globalThis.fetch, token = process.env.WENDKEEP_OBSERVER_TOKEN || '', captureLevel = process.env.WENDKEEP_OBSERVER_CAPTURE_LEVEL || 'metadata' } = {}) {
378
419
  if (!vaultBase || !projectId) throw new Error('vaultBase e projectId são obrigatórios.');
379
- const replay = await retryObserverSqlOutbox({ vaultBase, projectId, url, fetchImpl });
420
+ const replay = await retryObserverSqlOutbox({ vaultBase, projectId, url, fetchImpl, token });
380
421
  const state = readState(vaultBase);
381
422
  const remoteDocuments = Object.keys(state.files || {}).length === 0
382
- ? await readRemoteDocuments({ url, projectId, fetchImpl })
423
+ ? await readRemoteDocuments({ url, projectId, fetchImpl, token })
383
424
  : {};
384
- const batch = buildObserverSqlEventBatch({ vaultBase, projectId, input, now, state, remoteDocuments });
425
+ const batch = buildObserverSqlEventBatch({ vaultBase, projectId, input, now, state, remoteDocuments, captureLevel });
385
426
  atomicJson(statePath(vaultBase), batch.nextState);
386
427
  if (!batch.events.length) return { ok: true, queued: false, scanned: batch.scanned, changed: batch.changed, pending: listSqlOutbox(vaultBase).length, replay };
387
428
  if (!url) {
@@ -389,7 +430,7 @@ export async function publishObserverSql({ vaultBase, projectId, url = process.e
389
430
  return { ok: false, queued: true, scanned: batch.scanned, changed: batch.changed, pending: listSqlOutbox(vaultBase).length, replay, hookExitCode: 0 };
390
431
  }
391
432
  try {
392
- const response = await postSqlBatch({ url, projectId, events: batch.events, fetchImpl });
433
+ const response = await postSqlBatch({ url, projectId, events: batch.events, fetchImpl, token });
393
434
  return { ok: true, queued: false, scanned: batch.scanned, changed: batch.changed, pending: listSqlOutbox(vaultBase).length, replay, response };
394
435
  } catch (error) {
395
436
  queueOutbox(vaultBase, { schema_version: SQL_SCHEMA_VERSION, project_id: projectId, events: batch.events });
@@ -1,8 +1,8 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';
3
+ import { createRequire } from 'node:module';
3
4
  import { join, basename, dirname } from 'node:path';
4
5
  import { fileURLToPath } from 'node:url';
5
- import { DatabaseSync } from 'node:sqlite';
6
6
  import { decodeTranscript, encodeTranscript } from './observer-transcript-store.mjs';
7
7
 
8
8
  export const OBSERVER_SQL_FILE = 'observer.sqlite';
@@ -16,6 +16,36 @@ const EVENT_KINDS = new Set([
16
16
  'usage.rollup', 'llm_call', 'transcript.upsert',
17
17
  ]);
18
18
 
19
+ const OBSERVER_SQL_MINIMUM_NODE = '22.13.0';
20
+ const require = createRequire(import.meta.url);
21
+ let DatabaseSync;
22
+
23
+ export function observerSqlRuntimeSupport(version = process.versions.node) {
24
+ const current = String(version || '0.0.0');
25
+ const [major = 0, minor = 0] = current.split('.').map((part) => Number(part) || 0);
26
+ return {
27
+ supported: major > 22 || (major === 22 && minor >= 13),
28
+ minimum: OBSERVER_SQL_MINIMUM_NODE,
29
+ current,
30
+ };
31
+ }
32
+
33
+ function observerSqlRuntimeError(support = observerSqlRuntimeSupport()) {
34
+ const error = new Error(`Observer SQL requer Node.js >= ${support.minimum}; atual: ${support.current}. O Keep Core continua compatível com Node.js >= 18.`);
35
+ error.code = 'WENDKEEP_OBSERVER_NODE_UNSUPPORTED';
36
+ return error;
37
+ }
38
+
39
+ function observerDatabaseSync() {
40
+ const support = observerSqlRuntimeSupport();
41
+ if (!support.supported) throw observerSqlRuntimeError(support);
42
+ if (!DatabaseSync) {
43
+ try { ({ DatabaseSync } = require('node:sqlite')); }
44
+ catch { throw observerSqlRuntimeError(support); }
45
+ }
46
+ return DatabaseSync;
47
+ }
48
+
19
49
  function now() { return new Date().toISOString(); }
20
50
 
21
51
  function text(value, fallback = '') { return String(value ?? fallback); }
@@ -63,7 +93,8 @@ function migrationFiles() {
63
93
  export function openObserverDatabase(dataDir) {
64
94
  if (!dataDir) throw new Error('dataDir é obrigatório.');
65
95
  mkdirSync(dataDir, { recursive: true });
66
- const db = new DatabaseSync(join(dataDir, OBSERVER_SQL_FILE));
96
+ const SqliteDatabase = observerDatabaseSync();
97
+ const db = new SqliteDatabase(join(dataDir, OBSERVER_SQL_FILE));
67
98
  db.exec('PRAGMA foreign_keys = ON; PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000;');
68
99
  return db;
69
100
  }
package/src/observer.mjs CHANGED
@@ -7,15 +7,17 @@ import { publishObserverSql } from './observer-sql-publish.mjs';
7
7
  import { startObserverServer } from './observer-server.mjs';
8
8
  import { ensureObserverDatabase, migrateObserverDatabase, listSqlProjects, OBSERVER_SQL_FILE, OBSERVER_SQL_SCHEMA_VERSION } from './observer-sql-store.mjs';
9
9
  import { resolveProjectVault } from '../packages/vault/src/project-vault.mjs';
10
+ import { observerAuthHeaders, resolveObserverToken } from './observer-auth.mjs';
10
11
 
11
12
  export const OBSERVER_HELP = `wendkeep observer — Observer local multi-projeto
12
13
 
13
14
  Uso:
14
15
  wendkeep observer serve [--data-dir P] [--host 127.0.0.1] [--port 8787]
15
- [--allow-non-loopback]
16
+ [--allow-non-loopback] [--token TOKEN]
16
17
  wendkeep observer register --project P [--vault V] [--data-dir D] [--json]
17
18
  wendkeep observer publish --project P [--vault V] [--data-dir D] [--json]
18
- wendkeep observer memory import --project P [--vault V] [--url U] [--json]
19
+ wendkeep observer memory import --project P [--vault V] [--url U] [--token TOKEN]
20
+ [--capture-level metadata|messages|full-transcript] [--json]
19
21
  wendkeep observer status [--data-dir D] [--json]
20
22
 
21
23
  O Observer local pode manter snapshots operacionais e uma cópia completa da memória em volume
@@ -79,6 +81,7 @@ export async function runObserver(argv = [], { write = (chunk) => process.stdout
79
81
  return 0;
80
82
  }
81
83
  const dir = dataDir(argv);
84
+ const token = resolveObserverToken(optionValue(argv, '--token'));
82
85
 
83
86
  if (sub === 'status') {
84
87
  print({ ...summary(readObserverIndex(dir)), database: databaseSummary(dir) }, asJson, write);
@@ -92,6 +95,7 @@ export async function runObserver(argv = [], { write = (chunk) => process.stdout
92
95
  host,
93
96
  port: Number(optionValue(argv, '--port') || 8787),
94
97
  allowNonLoopback: argv.includes('--allow-non-loopback'),
98
+ token,
95
99
  });
96
100
  const address = server.address();
97
101
  process.stdout.write(`wendkeep observer listening: http://${address.address}:${address.port}\n`);
@@ -106,7 +110,7 @@ export async function runObserver(argv = [], { write = (chunk) => process.stdout
106
110
  const snapshot = buildProjectSnapshot({ vaultBase: vault, projectRoot: root });
107
111
  const url = optionValue(argv, '--url') || process.env.WENDKEEP_OBSERVER_URL || '';
108
112
  if (!url) throw new Error('observer memory import: --url ou WENDKEEP_OBSERVER_URL é obrigatório.');
109
- const headers = { 'content-type': 'application/json', accept: 'application/json' };
113
+ const headers = observerAuthHeaders(token, { 'content-type': 'application/json', accept: 'application/json' });
110
114
  const registration = await fetch(
111
115
  String(url).replace(/\/$/, '') + '/v1/projects/' + encodeURIComponent(snapshot.project_id),
112
116
  {
@@ -124,11 +128,14 @@ export async function runObserver(argv = [], { write = (chunk) => process.stdout
124
128
  vaultBase: vault,
125
129
  projectId: snapshot.project_id,
126
130
  url,
131
+ token,
132
+ captureLevel: optionValue(argv, '--capture-level') || process.env.WENDKEEP_OBSERVER_CAPTURE_LEVEL || 'metadata',
127
133
  });
128
134
  const parity = await compareMemoryParity({
129
135
  vaultBase: vault,
130
136
  projectId: snapshot.project_id,
131
137
  url,
138
+ token,
132
139
  });
133
140
  const result = {
134
141
  ok: sql.ok && parity.missing === 0 && parity.mismatched === 0,
@@ -1,5 +1,5 @@
1
1
  // Pure helper: extract a single version's release notes from a Keep-a-Changelog
2
- // file. Reused by scripts/release.mjs and .github/workflows/release.yml so the
2
+ // file. Reused by scripts/release.mjs and .github/workflows/auto-tag.yml so the
3
3
  // GitHub Release body always matches the committed CHANGELOG.
4
4
 
5
5
  const HEADER_RE = /^##\s*\[([^\]]+)\]\s*[—–-]\s*(.+?)\s*$/;
@@ -0,0 +1,68 @@
1
+ const DEPENDENCY_FIELDS = Object.freeze([
2
+ 'dependencies',
3
+ 'devDependencies',
4
+ 'optionalDependencies',
5
+ 'peerDependencies',
6
+ ]);
7
+
8
+ export function packageHasSelfDependency(pkg = {}) {
9
+ const name = String(pkg.name || '');
10
+ if (!name) return false;
11
+ return DEPENDENCY_FIELDS.some((field) => Object.hasOwn(pkg[field] || {}, name));
12
+ }
13
+
14
+ export function evaluateReleaseProvenance({
15
+ name,
16
+ version,
17
+ headCommit,
18
+ tagCommit = '',
19
+ publishedIntegrity = '',
20
+ localIntegrity = '',
21
+ requirePublished = false,
22
+ } = {}) {
23
+ const tag = `v${version}`;
24
+ if (tagCommit && tagCommit !== headCommit) {
25
+ return {
26
+ ok: false,
27
+ code: 'tag_commit_mismatch',
28
+ message: `${tag} aponta para ${tagCommit.slice(0, 7)}, não para ${headCommit.slice(0, 7)}. Bump a versão antes de alterar a árvore publicada.`,
29
+ };
30
+ }
31
+ if (publishedIntegrity && !tagCommit) {
32
+ return {
33
+ ok: false,
34
+ code: 'published_tag_missing',
35
+ message: `${name}@${version} está publicado, mas ${tag} não comprova o commit correspondente.`,
36
+ };
37
+ }
38
+ if (requirePublished && !publishedIntegrity) {
39
+ return {
40
+ ok: false,
41
+ code: 'published_artifact_missing',
42
+ message: `${name}@${version} ainda não possui integridade consultável no npm.`,
43
+ };
44
+ }
45
+ if (publishedIntegrity && localIntegrity && publishedIntegrity !== localIntegrity) {
46
+ return {
47
+ ok: false,
48
+ code: 'tarball_integrity_mismatch',
49
+ message: `o tarball de ${tag} diverge do artefato publicado no npm.`,
50
+ };
51
+ }
52
+ if (publishedIntegrity && !localIntegrity) {
53
+ return {
54
+ ok: false,
55
+ code: 'local_integrity_missing',
56
+ message: `não foi possível calcular a integridade do tarball de ${tag}.`,
57
+ };
58
+ }
59
+ return {
60
+ ok: true,
61
+ code: publishedIntegrity ? 'verified' : 'release_candidate',
62
+ name,
63
+ version,
64
+ tag,
65
+ commit: headCommit,
66
+ integrity: publishedIntegrity || localIntegrity || '',
67
+ };
68
+ }
package/src/taxonomy.mjs CHANGED
@@ -131,7 +131,7 @@ export const RUNNABLE_HOOKS = [
131
131
  // the MOST agent-agnostic mechanism it supports; the Claude Code plugin entry
132
132
  // (extraKnownMarketplaces + enabledPlugins) is an additive bonus, never the base.
133
133
  // - mcp: .mcp.json server entry (works on any MCP-capable agent)
134
- // - wendkeepHook: a wendkeep-authored hook (runs via `npx wendkeep hook`, any agent)
134
+ // - wendkeepHook: a wendkeep-authored hook (runs via `npx --no-install wendkeep hook`, any agent)
135
135
  // - installer: cross-agent install script (used on non-Claude agents)
136
136
  // marketplace/plugin shapes are verified against a real ~/.claude/settings.json.
137
137
  export const COMPANIONS = [
@@ -52,7 +52,7 @@ export function renderVaultReadme({ projectName, vaultPath, withMcp = true, loca
52
52
  const mcpIntro = withMcp ? ', and read/written by the **MCPVault** MCP server' : '';
53
53
  const access = [`- **Obsidian:** open this folder with "Open folder as vault" → \`${vaultPath}\``];
54
54
  if (withMcp) access.push('- **Agent (MCP):** the `wendkeep-vault` server (MCPVault) points at this vault (set in `.mcp.json`), giving the agent read/write on the notes.');
55
- access.push('- **Hooks:** Codex and Claude Code call `npx wendkeep hook <name>`; the vault is discovered from the project-local `.wendkeep.json` binding and checked against `.brain/PROJECT.json`.');
55
+ access.push('- **Hooks:** Codex and Claude Code call `npx --no-install wendkeep hook <name>`; the vault is discovered from the project-local `.wendkeep.json` binding and checked against `.brain/PROJECT.json`.');
56
56
  return `# Obsidian vault — ${name}
57
57
 
58
58
  > Knowledge base of **${name}**, captured automatically by wendkeep from AI coding-agent
@@ -84,7 +84,7 @@ ${access.join('\n')}
84
84
  const mcpIntro = withMcp ? ', e lida/escrita pelo MCP server **MCPVault**' : '';
85
85
  const access = [`- **Obsidian:** abra esta pasta com "Open folder as vault" → \`${vaultPath}\``];
86
86
  if (withMcp) access.push('- **Agente (MCP):** o servidor `wendkeep-vault` (MCPVault) é apontado para este vault pelo `wendkeep init` (em `.mcp.json`), dando ao agente leitura/escrita das notas.');
87
- access.push('- **Hooks:** Codex e Claude Code chamam `npx wendkeep hook <name>`; o vault é descoberto pelo vínculo local `.wendkeep.json` e validado contra `.brain/PROJECT.json`.');
87
+ access.push('- **Hooks:** Codex e Claude Code chamam `npx --no-install wendkeep hook <name>`; o vault é descoberto pelo vínculo local `.wendkeep.json` e validado contra `.brain/PROJECT.json`.');
88
88
  return `# Vault Obsidian — ${name}
89
89
 
90
90
  > Base de conhecimento de **${name}**, capturada automaticamente pelo wendkeep a