wendkeep 0.71.1 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.71.1",
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-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
  }
@@ -24,13 +24,15 @@ export const SESSION_HOOKS = [
24
24
  { event: 'PostToolUse', matcher: 'AskUserQuestion', name: 'decision-capture', timeout: 15, statusMessage: 'wendkeep: recording decision' },
25
25
  // Refresh subagent/workflow telemetry as each subagent finishes (resilient to a missed Stop).
26
26
  { event: 'SubagentStop', matcher: null, name: 'subagent-stop', timeout: 20, codex: true, statusMessage: 'wendkeep: subagent telemetry' },
27
+ // Publish the SQL observer projection after the subagent telemetry is settled.
28
+ { event: 'SubagentStop', matcher: null, name: 'observer-publish', timeout: 5, order: 20, codex: true, statusMessage: 'wendkeep: publishing local observer usage' },
27
29
  // Log plan/task progress into the active session note when a task is marked complete.
28
30
  // codex: TaskCompleted is not in Codex's hook event enum.
29
31
  { event: 'TaskCompleted', matcher: null, name: 'task-log', timeout: 10, statusMessage: 'wendkeep: plan progress' },
30
32
  ];
31
33
 
32
34
  export function hookCommand(name) {
33
- return `npx wendkeep hook ${name}`;
35
+ return `npx --no-install wendkeep hook ${name}`;
34
36
  }
35
37
 
36
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;
@@ -0,0 +1,107 @@
1
+ PRAGMA foreign_keys = ON;
2
+
3
+ CREATE TABLE IF NOT EXISTS schema_migrations (
4
+ version INTEGER PRIMARY KEY,
5
+ name TEXT NOT NULL,
6
+ applied_at TEXT NOT NULL
7
+ );
8
+
9
+ CREATE TABLE IF NOT EXISTS projects (
10
+ project_id TEXT PRIMARY KEY,
11
+ project_name TEXT NOT NULL,
12
+ wendkeep_version TEXT NOT NULL DEFAULT '',
13
+ registered_at TEXT NOT NULL,
14
+ updated_at TEXT NOT NULL
15
+ );
16
+
17
+ CREATE TABLE IF NOT EXISTS ingest_events (
18
+ event_id TEXT PRIMARY KEY,
19
+ project_id TEXT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
20
+ kind TEXT NOT NULL,
21
+ payload_hash TEXT NOT NULL,
22
+ payload_json TEXT NOT NULL,
23
+ occurred_at TEXT NOT NULL,
24
+ ingested_at TEXT NOT NULL,
25
+ status TEXT NOT NULL DEFAULT 'accepted'
26
+ );
27
+
28
+ CREATE INDEX IF NOT EXISTS idx_ingest_events_project_time
29
+ ON ingest_events(project_id, occurred_at);
30
+
31
+ CREATE TABLE IF NOT EXISTS memory_events (
32
+ event_id TEXT PRIMARY KEY REFERENCES ingest_events(event_id) ON DELETE CASCADE,
33
+ project_id TEXT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
34
+ entity_type TEXT NOT NULL,
35
+ logical_path TEXT NOT NULL,
36
+ operation TEXT NOT NULL,
37
+ revision INTEGER NOT NULL DEFAULT 1,
38
+ content_hash TEXT NOT NULL DEFAULT '',
39
+ source_session_id TEXT NOT NULL DEFAULT '',
40
+ source_turn_id TEXT NOT NULL DEFAULT '',
41
+ occurred_at TEXT NOT NULL,
42
+ payload_json TEXT NOT NULL,
43
+ UNIQUE(project_id, logical_path, revision)
44
+ );
45
+
46
+ CREATE INDEX IF NOT EXISTS idx_memory_events_project_path
47
+ ON memory_events(project_id, logical_path, revision);
48
+
49
+ CREATE TABLE IF NOT EXISTS documents (
50
+ document_id TEXT PRIMARY KEY,
51
+ project_id TEXT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
52
+ logical_path TEXT NOT NULL,
53
+ entity_type TEXT NOT NULL,
54
+ title TEXT NOT NULL DEFAULT '',
55
+ content TEXT NOT NULL DEFAULT '',
56
+ metadata_json TEXT NOT NULL DEFAULT '{}',
57
+ content_hash TEXT NOT NULL DEFAULT '',
58
+ revision INTEGER NOT NULL DEFAULT 1,
59
+ source_session_id TEXT NOT NULL DEFAULT '',
60
+ source_turn_id TEXT NOT NULL DEFAULT '',
61
+ captured_at TEXT NOT NULL,
62
+ deleted_at TEXT,
63
+ UNIQUE(project_id, logical_path)
64
+ );
65
+
66
+ CREATE INDEX IF NOT EXISTS idx_documents_project_type
67
+ ON documents(project_id, entity_type, deleted_at);
68
+
69
+ CREATE INDEX IF NOT EXISTS idx_documents_project_revision
70
+ ON documents(project_id, revision);
71
+
72
+ CREATE TABLE IF NOT EXISTS sessions (
73
+ session_id TEXT PRIMARY KEY,
74
+ project_id TEXT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
75
+ provider TEXT NOT NULL DEFAULT '',
76
+ status TEXT NOT NULL DEFAULT 'unknown',
77
+ summary TEXT NOT NULL DEFAULT '',
78
+ change_slug TEXT NOT NULL DEFAULT '',
79
+ started_at TEXT,
80
+ ended_at TEXT,
81
+ updated_at TEXT NOT NULL,
82
+ metadata_json TEXT NOT NULL DEFAULT '{}'
83
+ );
84
+
85
+ CREATE INDEX IF NOT EXISTS idx_sessions_project_time
86
+ ON sessions(project_id, started_at, ended_at);
87
+
88
+ CREATE TABLE IF NOT EXISTS agent_runs (
89
+ agent_id TEXT PRIMARY KEY,
90
+ project_id TEXT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
91
+ session_id TEXT NOT NULL REFERENCES sessions(session_id) ON DELETE CASCADE,
92
+ parent_agent_id TEXT,
93
+ role TEXT NOT NULL DEFAULT 'main',
94
+ agent_name TEXT NOT NULL DEFAULT '',
95
+ agent_type TEXT NOT NULL DEFAULT '',
96
+ workflow TEXT NOT NULL DEFAULT '',
97
+ status TEXT NOT NULL DEFAULT 'unknown',
98
+ model TEXT NOT NULL DEFAULT '',
99
+ effort TEXT NOT NULL DEFAULT '',
100
+ started_at TEXT,
101
+ ended_at TEXT,
102
+ metadata_json TEXT NOT NULL DEFAULT '{}',
103
+ FOREIGN KEY(parent_agent_id) REFERENCES agent_runs(agent_id) ON DELETE SET NULL
104
+ );
105
+
106
+ CREATE INDEX IF NOT EXISTS idx_agent_runs_project_session
107
+ ON agent_runs(project_id, session_id, role);
@@ -0,0 +1,72 @@
1
+ CREATE TABLE IF NOT EXISTS usage_rollups (
2
+ rollup_key TEXT PRIMARY KEY,
3
+ project_id TEXT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
4
+ session_id TEXT NOT NULL REFERENCES sessions(session_id) ON DELETE CASCADE,
5
+ agent_id TEXT NOT NULL REFERENCES agent_runs(agent_id) ON DELETE CASCADE,
6
+ role TEXT NOT NULL DEFAULT 'main',
7
+ provider TEXT NOT NULL DEFAULT '',
8
+ model_provider TEXT NOT NULL DEFAULT '',
9
+ model TEXT NOT NULL DEFAULT '',
10
+ effort TEXT NOT NULL DEFAULT '',
11
+ calls INTEGER NOT NULL DEFAULT 0,
12
+ tokens_input INTEGER NOT NULL DEFAULT 0,
13
+ tokens_cache_write INTEGER NOT NULL DEFAULT 0,
14
+ tokens_cache_read INTEGER NOT NULL DEFAULT 0,
15
+ tokens_output INTEGER NOT NULL DEFAULT 0,
16
+ tokens_reasoning INTEGER NOT NULL DEFAULT 0,
17
+ tokens_total INTEGER NOT NULL DEFAULT 0,
18
+ cost_usd REAL NOT NULL DEFAULT 0,
19
+ cost_status TEXT NOT NULL DEFAULT 'unknown',
20
+ pricing_source TEXT NOT NULL DEFAULT '',
21
+ pricing_version TEXT NOT NULL DEFAULT '',
22
+ wasted_usd REAL NOT NULL DEFAULT 0,
23
+ revision INTEGER NOT NULL DEFAULT 1,
24
+ occurred_at TEXT NOT NULL,
25
+ source_event_id TEXT NOT NULL REFERENCES ingest_events(event_id) ON DELETE CASCADE,
26
+ metadata_json TEXT NOT NULL DEFAULT '{}'
27
+ );
28
+
29
+ CREATE INDEX IF NOT EXISTS idx_usage_rollups_project_time
30
+ ON usage_rollups(project_id, occurred_at);
31
+
32
+ CREATE INDEX IF NOT EXISTS idx_usage_rollups_project_agent
33
+ ON usage_rollups(project_id, agent_id, role);
34
+
35
+ CREATE INDEX IF NOT EXISTS idx_usage_rollups_project_model
36
+ ON usage_rollups(project_id, model_provider, model);
37
+
38
+ CREATE TABLE IF NOT EXISTS llm_calls (
39
+ call_id TEXT PRIMARY KEY,
40
+ project_id TEXT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
41
+ session_id TEXT NOT NULL REFERENCES sessions(session_id) ON DELETE CASCADE,
42
+ agent_id TEXT NOT NULL REFERENCES agent_runs(agent_id) ON DELETE CASCADE,
43
+ role TEXT NOT NULL DEFAULT 'main',
44
+ provider TEXT NOT NULL DEFAULT '',
45
+ model_provider TEXT NOT NULL DEFAULT '',
46
+ model TEXT NOT NULL DEFAULT '',
47
+ effort TEXT NOT NULL DEFAULT '',
48
+ sequence INTEGER NOT NULL DEFAULT 0,
49
+ occurred_at TEXT NOT NULL,
50
+ tokens_input INTEGER NOT NULL DEFAULT 0,
51
+ tokens_cache_write INTEGER NOT NULL DEFAULT 0,
52
+ tokens_cache_read INTEGER NOT NULL DEFAULT 0,
53
+ tokens_output INTEGER NOT NULL DEFAULT 0,
54
+ tokens_reasoning INTEGER NOT NULL DEFAULT 0,
55
+ tokens_total INTEGER NOT NULL DEFAULT 0,
56
+ cost_usd REAL NOT NULL DEFAULT 0,
57
+ cost_status TEXT NOT NULL DEFAULT 'unknown',
58
+ transcript_id TEXT,
59
+ prompt_text TEXT NOT NULL DEFAULT '',
60
+ response_text TEXT NOT NULL DEFAULT '',
61
+ status TEXT NOT NULL DEFAULT 'complete',
62
+ metadata_json TEXT NOT NULL DEFAULT '{}'
63
+ );
64
+
65
+ CREATE INDEX IF NOT EXISTS idx_llm_calls_project_time
66
+ ON llm_calls(project_id, occurred_at);
67
+
68
+ CREATE INDEX IF NOT EXISTS idx_llm_calls_project_agent
69
+ ON llm_calls(project_id, agent_id, sequence);
70
+
71
+ CREATE INDEX IF NOT EXISTS idx_llm_calls_project_model
72
+ ON llm_calls(project_id, model_provider, model);
@@ -0,0 +1,21 @@
1
+ CREATE TABLE IF NOT EXISTS transcripts (
2
+ transcript_id TEXT PRIMARY KEY,
3
+ project_id TEXT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
4
+ session_id TEXT NOT NULL REFERENCES sessions(session_id) ON DELETE CASCADE,
5
+ agent_id TEXT NOT NULL REFERENCES agent_runs(agent_id) ON DELETE CASCADE,
6
+ coverage TEXT NOT NULL DEFAULT 'summary_only',
7
+ codec TEXT NOT NULL DEFAULT 'gzip',
8
+ content_gzip BLOB NOT NULL,
9
+ content_sha256 TEXT NOT NULL,
10
+ original_bytes INTEGER NOT NULL,
11
+ compressed_bytes INTEGER NOT NULL,
12
+ source TEXT NOT NULL DEFAULT '',
13
+ occurred_at TEXT NOT NULL,
14
+ metadata_json TEXT NOT NULL DEFAULT '{}'
15
+ );
16
+
17
+ CREATE INDEX IF NOT EXISTS idx_transcripts_project_session
18
+ ON transcripts(project_id, session_id, occurred_at);
19
+
20
+ CREATE INDEX IF NOT EXISTS idx_transcripts_project_coverage
21
+ ON transcripts(project_id, coverage);
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';
@@ -68,7 +70,8 @@ function entityType(logicalPath) {
68
70
  }
69
71
 
70
72
  function shouldSkip(name, relativePath) {
71
- if (TRANSIENT_NAMES.has(name) || name.endsWith('.tmp') || name.endsWith('.lock')) return true;
73
+ if (TRANSIENT_NAMES.has(name) || name === 'observer-sql-state.json' || name === 'observer-sql-outbox'
74
+ || name.endsWith('.tmp') || name.endsWith('.lock')) return true;
72
75
  if (relativePath === MEMORY_STATE_FILE) return true;
73
76
  return false;
74
77
  }
@@ -102,7 +105,7 @@ function memoryFiles(vaultBase) {
102
105
 
103
106
  export function localMemoryManifest(vaultBase) {
104
107
  return Object.fromEntries(memoryFiles(vaultBase).map((file) => {
105
- const content = readFileSync(file.absolute, 'utf8');
108
+ const content = sanitizeObserverContent(readFileSync(file.absolute, 'utf8'));
106
109
  return [file.logicalPath, {
107
110
  logical_path: file.logicalPath,
108
111
  content_hash: hash(content),
@@ -158,7 +161,7 @@ export function buildMemoryEventBatch({
158
161
  const events = [];
159
162
 
160
163
  for (const file of currentFiles) {
161
- const content = readFileSync(file.absolute, 'utf8');
164
+ const content = sanitizeObserverContent(readFileSync(file.absolute, 'utf8'));
162
165
  if (Buffer.byteLength(content, 'utf8') > MAX_MEMORY_CONTENT_BYTES) {
163
166
  throw new Error('arquivo excede o limite de memória: ' + file.logicalPath);
164
167
  }
@@ -230,12 +233,12 @@ export function listMemoryOutbox(vaultBase) {
230
233
  .map((name) => join(dir, name));
231
234
  }
232
235
 
233
- async function postBatch(url, projectId, events, fetchImpl = globalThis.fetch) {
236
+ async function postBatch(url, projectId, events, fetchImpl = globalThis.fetch, token = '') {
234
237
  const response = await fetchImpl(
235
238
  String(url).replace(/\/$/, '') + '/v1/projects/' + encodeURIComponent(projectId) + '/memory/events',
236
239
  {
237
240
  method: 'POST',
238
- headers: { 'content-type': 'application/json', accept: 'application/json' },
241
+ headers: observerAuthHeaders(token, { 'content-type': 'application/json', accept: 'application/json' }),
239
242
  body: JSON.stringify({ events }),
240
243
  },
241
244
  );
@@ -248,6 +251,7 @@ export async function retryObserverMemoryOutbox({
248
251
  projectId = projectIdFromVault(vaultBase),
249
252
  url,
250
253
  fetchImpl = globalThis.fetch,
254
+ token = process.env.WENDKEEP_OBSERVER_TOKEN || '',
251
255
  } = {}) {
252
256
  const files = listMemoryOutbox(vaultBase);
253
257
  if (!url) return { attempted: 0, confirmed: 0, pending: files.length };
@@ -261,7 +265,7 @@ export async function retryObserverMemoryOutbox({
261
265
  unlinkSync(path);
262
266
  continue;
263
267
  }
264
- await postBatch(url, projectId, batch.events, fetchImpl);
268
+ await postBatch(url, projectId, batch.events, fetchImpl, token);
265
269
  unlinkSync(path);
266
270
  confirmed += 1;
267
271
  } catch {
@@ -276,10 +280,11 @@ export async function compareMemoryParity({
276
280
  projectId = projectIdFromVault(vaultBase),
277
281
  url,
278
282
  fetchImpl = globalThis.fetch,
283
+ token = process.env.WENDKEEP_OBSERVER_TOKEN || '',
279
284
  } = {}) {
280
285
  const response = await fetchImpl(
281
286
  String(url).replace(/\/$/, '') + '/v1/projects/' + encodeURIComponent(projectId) + '/memory/tree',
282
- { headers: { accept: 'application/json' } },
287
+ { headers: observerAuthHeaders(token, { accept: 'application/json' }) },
283
288
  );
284
289
  if (!response.ok) throw new Error('Observer respondeu HTTP ' + response.status + '.');
285
290
  const body = await response.json();
@@ -308,9 +313,10 @@ export async function publishObserverMemory({
308
313
  sourceTurnId = '',
309
314
  now = new Date(),
310
315
  fetchImpl = globalThis.fetch,
316
+ token = process.env.WENDKEEP_OBSERVER_TOKEN || '',
311
317
  } = {}) {
312
318
  if (!vaultBase || !projectId) throw new Error('vaultBase e projectId são obrigatórios.');
313
- await retryObserverMemoryOutbox({ vaultBase, projectId, url, fetchImpl });
319
+ await retryObserverMemoryOutbox({ vaultBase, projectId, url, fetchImpl, token });
314
320
  const state = readState(vaultBase);
315
321
  const batch = buildMemoryEventBatch({ vaultBase, projectId, sourceSessionId, sourceTurnId, now, state });
316
322
  if (batch.events.length === 0) {
@@ -323,7 +329,7 @@ export async function publishObserverMemory({
323
329
  return { ok: false, queued: true, scanned: batch.scanned, changed: batch.changed, pending: listMemoryOutbox(vaultBase).length, hookExitCode: 0 };
324
330
  }
325
331
  try {
326
- await postBatch(url, projectId, batch.events, fetchImpl);
332
+ await postBatch(url, projectId, batch.events, fetchImpl, token);
327
333
  commitMemoryPublishState(vaultBase, batch.nextState);
328
334
  return { ok: true, queued: false, scanned: batch.scanned, changed: batch.changed, pending: listMemoryOutbox(vaultBase).length };
329
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
+ }
@@ -1,7 +1,8 @@
1
1
  import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { buildProjectSnapshot } from './observer-snapshot.mjs';
4
- import { publishObserverMemory } from './observer-memory-publish.mjs';
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 */ }
@@ -90,25 +91,27 @@ export async function publishObserverSnapshot({
90
91
  projectRoot,
91
92
  url = process.env.WENDKEEP_OBSERVER_URL || '',
92
93
  now = new Date(),
94
+ input = {},
95
+ token = process.env.WENDKEEP_OBSERVER_TOKEN || '',
93
96
  } = {}) {
94
97
  try {
95
98
  const event = buildProjectSnapshot({ vaultBase, projectRoot, now });
96
- if (!url) return { ok: true, skipped: true, queued: false, hookExitCode: 0, event_id: event.event_id };
99
+ const sql = await publishObserverSql({
100
+ vaultBase,
101
+ projectId: event.project_id,
102
+ url,
103
+ input,
104
+ now,
105
+ token,
106
+ });
107
+ if (!url) return { ok: sql.ok, skipped: true, queued: sql.queued, hookExitCode: 0, event_id: event.event_id, sql };
97
108
 
98
- await retryObserverOutbox({ vaultBase, url });
99
- let memory;
100
- try {
101
- memory = await publishObserverMemory({
102
- vaultBase,
103
- projectId: event.project_id,
104
- url,
105
- now,
106
- });
107
- } catch (error) {
108
- memory = { ok: false, queued: false, error: error.message };
109
- }
109
+ await retryObserverOutbox({ vaultBase, url, token });
110
+ // SQL is the live authority. Keep the legacy-shaped `memory` field for
111
+ // older integrations while reporting the real SQL publication separately.
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,
@@ -116,6 +119,7 @@ export async function publishObserverSnapshot({
116
119
  event_id: event.event_id,
117
120
  duplicate: response.duplicate === true,
118
121
  memory,
122
+ sql,
119
123
  };
120
124
  } catch (error) {
121
125
  queueOutbox(vaultBase, event);
@@ -126,6 +130,7 @@ export async function publishObserverSnapshot({
126
130
  event_id: event.event_id,
127
131
  error: error.message,
128
132
  memory,
133
+ sql,
129
134
  };
130
135
  }
131
136
  } catch (error) {