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 +36 -3
- package/docs/en/commands/observer.md +18 -12
- package/docs/en/commands/sessions-and-import.md +4 -4
- package/docs/pt-BR/commands/observer.md +18 -12
- package/docs/pt-BR/commands/sessions-and-import.md +4 -4
- package/hooks/understand-inject.mjs +1 -1
- package/package.json +5 -4
- package/packages/integrations/src/host-hooks.mjs +1 -1
- package/packages/vault/src/memory-store.mjs +17 -4
- package/src/init.mjs +2 -2
- package/src/observer-auth.mjs +10 -0
- package/src/observer-memory-publish.mjs +13 -8
- package/src/observer-privacy.mjs +23 -0
- package/src/observer-publish.mjs +10 -7
- package/src/observer-server.mjs +51 -0
- package/src/observer-sql-publish.mjs +72 -31
- package/src/observer-sql-store.mjs +33 -2
- package/src/observer.mjs +10 -3
- package/src/release-changelog.mjs +1 -1
- package/src/release-provenance.mjs +68 -0
- package/src/taxonomy.mjs +1 -1
- package/src/vault-readme.mjs +2 -2
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
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
28
|
-
|
|
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
|
-
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
91
|
+
sessões de agentes, rollups de custo e chamadas. Mensagens e transcripts só 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.
|
|
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.
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
+
}
|
package/src/observer-publish.mjs
CHANGED
|
@@ -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,
|
package/src/observer-server.mjs
CHANGED
|
@@ -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
|
|
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(
|
|
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: {
|
|
231
|
+
metadata: { source_label: basename(source.path), source_hash: hash(normalizePath(source.path)) },
|
|
206
232
|
},
|
|
207
|
-
};
|
|
208
|
-
const calls = transcriptCalls({
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
|
303
|
-
const
|
|
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
|
|
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] [--
|
|
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/
|
|
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 = [
|
package/src/vault-readme.mjs
CHANGED
|
@@ -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
|