wendkeep 0.74.0 → 0.75.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/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/ .brain/observer-sql-state.json .brain/observer-sql-outbox/',
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/ .brain/observer-sql-publisher.lock',
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/ .brain/observer-sql-state.json .brain/observer-sql-outbox/',
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/ .brain/observer-sql-publisher.lock',
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}`,
package/src/note.mjs CHANGED
@@ -17,6 +17,8 @@ import { getLocale } from '../hooks/locale.mjs';
17
17
  import { buildManualBugNote, buildManualLearningNote, relinkDerivedNotes } from '../hooks/linked-notes.mjs';
18
18
  import { repairStackedFrontmatter } from '../hooks/frontmatter-repair.mjs';
19
19
  import { repairDerivedSections } from '../hooks/derived-sections.mjs';
20
+ import { enqueueObserverDocumentChange } from './observer-sql-publish.mjs';
21
+ import { readProjectForValidation } from '../packages/vault/src/validate-memory.mjs';
20
22
 
21
23
  const TYPES = {
22
24
  bug: { folderKey: 'bugs', prefix: 'BUG', build: buildManualBugNote },
@@ -120,6 +122,11 @@ export function runNote(argv) {
120
122
  } catch { /* sem sessão ativa — nota nasce sem backlink */ }
121
123
 
122
124
  writeFileSync(filePath, kind.build(title.trim(), { num, dateStr, sessionRel, localeId: loc.id }), 'utf8');
123
- process.stdout.write(`${toVaultRelative(vaultBase, filePath)}\n`);
125
+ const logicalPath = toVaultRelative(vaultBase, filePath);
126
+ try {
127
+ const project = readProjectForValidation(vaultBase);
128
+ if (project.ok && project.projectId) enqueueObserverDocumentChange({ vaultBase, projectId: project.projectId, logicalPath });
129
+ } catch { /* Observer é fail-open; reconcile recupera qualquer enqueue perdido. */ }
130
+ process.stdout.write(`${logicalPath}\n`);
124
131
  process.exit(0);
125
132
  }
@@ -1,8 +1,8 @@
1
1
  import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { buildProjectSnapshot } from './observer-snapshot.mjs';
4
- import { publishObserverSql } from './observer-sql-publish.mjs';
3
+ import { publishObserverSqlIncremental } from './observer-sql-publish.mjs';
5
4
  import { observerAuthHeaders, resolveObserverToken } from './observer-auth.mjs';
5
+ import { readProjectForValidation } from '../packages/vault/src/validate-memory.mjs';
6
6
 
7
7
  const OUTBOX_REL = join('.brain', 'observer-outbox');
8
8
  const REQUEST_TIMEOUT_MS = 500;
@@ -88,51 +88,26 @@ export async function retryObserverOutbox({ vaultBase, url, token = process.env.
88
88
 
89
89
  export async function publishObserverSnapshot({
90
90
  vaultBase,
91
- projectRoot,
92
91
  url = process.env.WENDKEEP_OBSERVER_URL || '',
93
92
  now = new Date(),
94
93
  input = {},
95
94
  token = process.env.WENDKEEP_OBSERVER_TOKEN || '',
96
95
  } = {}) {
97
96
  try {
98
- const event = buildProjectSnapshot({ vaultBase, projectRoot, now });
99
- const sql = await publishObserverSql({
97
+ const project = readProjectForValidation(vaultBase);
98
+ if (!project.ok || !project.projectId) throw new Error(project.errors?.join(' ') || 'PROJECT.json inválido.');
99
+ const sql = await publishObserverSqlIncremental({
100
100
  vaultBase,
101
- projectId: event.project_id,
101
+ projectId: project.projectId,
102
102
  url,
103
103
  input,
104
104
  now,
105
105
  token,
106
106
  });
107
- if (!url) return { ok: sql.ok, skipped: true, queued: sql.queued, hookExitCode: 0, event_id: event.event_id, sql };
108
-
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.
107
+ // SQLite is the only live authority. The legacy snapshot store remains
108
+ // readable solely as a migration source for pre-SQL installations.
112
109
  const memory = { ok: sql.ok, queued: sql.queued, changed: sql.changed, pending: sql.pending, authority: 'sqlite' };
113
- try {
114
- const response = await postSnapshot(url, event, resolveObserverToken(token));
115
- return {
116
- ok: true,
117
- queued: false,
118
- hookExitCode: 0,
119
- event_id: event.event_id,
120
- duplicate: response.duplicate === true,
121
- memory,
122
- sql,
123
- };
124
- } catch (error) {
125
- queueOutbox(vaultBase, event);
126
- return {
127
- ok: false,
128
- queued: true,
129
- hookExitCode: 0,
130
- event_id: event.event_id,
131
- error: error.message,
132
- memory,
133
- sql,
134
- };
135
- }
110
+ return { ok: sql.ok, queued: sql.queued, hookExitCode: 0, memory, sql };
136
111
  } catch (error) {
137
112
  return { ok: false, queued: false, hookExitCode: 0, error: error.message };
138
113
  }
@@ -4,17 +4,11 @@ import { readFileSync } from 'node:fs';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { gunzipSync } from 'node:zlib';
6
6
  import {
7
- appendObserverEvent,
8
- getObserverProject,
9
7
  listRegisteredObserverProjects,
10
- readObserverIndex,
11
- registerObserverProject,
8
+ readObserverIndexSource,
12
9
  } from './observer-store.mjs';
13
10
  import { MAX_SNAPSHOT_BYTES, validateObserverSnapshot } from './observer-snapshot.mjs';
14
- import {
15
- setMemoryMode,
16
- validateMemoryEvent,
17
- } from './observer-memory.mjs';
11
+ import { validateMemoryEvent } from './observer-memory.mjs';
18
12
  import {
19
13
  OBSERVER_SQL_FILE,
20
14
  OBSERVER_SQL_SCHEMA_VERSION,
@@ -22,6 +16,8 @@ import {
22
16
  ingestObserverEvents,
23
17
  migrateObserverDatabase,
24
18
  readSqlProject,
19
+ readSqlProjectOverview,
20
+ readSqlProjectSnapshot,
25
21
  listSqlProjects,
26
22
  readSqlDocument,
27
23
  readSqlSync,
@@ -33,6 +29,7 @@ import {
33
29
  readUsageCalls,
34
30
  readUsageSummary,
35
31
  registerSqlProject,
32
+ upsertSqlProjectSnapshot,
36
33
  } from './observer-sql-store.mjs';
37
34
  import { migrateObserverContainerData } from './observer-sql-migrate.mjs';
38
35
 
@@ -182,8 +179,8 @@ function ensureSqlProjectRegistration(dataDir, sqlDb, projectId) {
182
179
  } catch (error) {
183
180
  if (error?.code !== 'project_not_registered') throw error;
184
181
  }
185
- const legacy = getObserverProject(dataDir, projectId)
186
- || listRegisteredObserverProjects(dataDir).find((item) => item.projectId === projectId);
182
+ const legacy = listRegisteredObserverProjects(dataDir).find((item) => item.projectId === projectId)
183
+ || readObserverIndexSource(dataDir).projects.find((item) => item.projectId === projectId);
187
184
  if (!legacy) return false;
188
185
  registerSqlProject(sqlDb, {
189
186
  projectId: legacy.projectId,
@@ -234,13 +231,16 @@ export async function startObserverServer({
234
231
  const legacyMigration = migrateObserverContainerData(dataDir, { database: sqlDb });
235
232
  const registered = [
236
233
  ...listRegisteredObserverProjects(dataDir),
237
- ...readObserverIndex(dataDir).projects.map((item) => ({
234
+ ...readObserverIndexSource(dataDir).projects.map((item) => ({
238
235
  projectId: item.projectId,
239
236
  projectName: item.projectName,
240
237
  wendkeepVersion: item.snapshot?.wendkeep_version || '',
241
238
  })),
242
239
  ];
243
240
  for (const project of registered) registerSqlProject(sqlDb, project);
241
+ for (const project of readObserverIndexSource(dataDir).projects) {
242
+ if (project.snapshot) upsertSqlProjectSnapshot(sqlDb, project.snapshot);
243
+ }
244
244
  const server = createServer(async (req, res) => {
245
245
  try {
246
246
  const pathname = new URL(req.url || '/', 'http://127.0.0.1').pathname;
@@ -286,21 +286,11 @@ export async function startObserverServer({
286
286
  }
287
287
 
288
288
  if (parts.length === 2 && req.method === 'GET') {
289
- const index = readObserverIndex(dataDir);
290
- const legacy = new Map(index.projects.map(({ snapshot, ...summary }) => [summary.projectId, summary]));
291
- for (const project of listSqlProjects(sqlDb)) {
292
- const current = legacy.get(project.project_id) || {};
293
- legacy.set(project.project_id, {
294
- ...current,
295
- projectId: project.project_id,
296
- projectName: current.projectName || project.project_name,
297
- wendkeepVersion: current.wendkeepVersion || project.wendkeep_version,
298
- registeredAt: current.registeredAt || project.registered_at,
299
- });
300
- }
301
289
  json(res, 200, {
302
- schema_version: index.schema_version,
303
- projects: [...legacy.values()].sort((a, b) => a.projectId.localeCompare(b.projectId)),
290
+ schema_version: 1,
291
+ projects: listSqlProjects(sqlDb)
292
+ .map((project) => readSqlProjectOverview(sqlDb, project.project_id))
293
+ .sort((a, b) => a.projectId.localeCompare(b.projectId)),
304
294
  });
305
295
  return;
306
296
  }
@@ -386,11 +376,11 @@ export async function startObserverServer({
386
376
  return;
387
377
  }
388
378
  const body = parseJson(await readBody(req));
389
- try {
390
- json(res, 200, setMemoryMode(dataDir, projectId, body.mode));
391
- } catch (error) {
392
- errorResponse(res, 400, error?.code || 'invalid_memory_mode', error?.message || 'modo inválido.');
379
+ if (body.mode && body.mode !== 'container-authority') {
380
+ errorResponse(res, 409, 'sql_authority_fixed', 'O Observer SQL é a autoridade única; modos legados são somente fonte de migração.');
381
+ return;
393
382
  }
383
+ json(res, 200, { ...readSqlSync(sqlDb, projectId), mode: 'container-authority', compatibility_noop: true });
394
384
  return;
395
385
  }
396
386
 
@@ -449,19 +439,8 @@ export async function startObserverServer({
449
439
  }
450
440
 
451
441
  if (parts.length === 3 && req.method === 'GET') {
452
- let project = getObserverProject(dataDir, projectId);
453
- if (!project) {
454
- try {
455
- const sqlProject = readSqlProject(sqlDb, projectId);
456
- project = {
457
- projectId: sqlProject.project_id,
458
- projectName: sqlProject.project_name,
459
- wendkeepVersion: sqlProject.wendkeep_version,
460
- registeredAt: sqlProject.registered_at,
461
- eventCount: 0,
462
- };
463
- } catch { /* handled as not found below */ }
464
- }
442
+ let project;
443
+ try { project = readSqlProjectOverview(sqlDb, projectId); } catch { /* handled below */ }
465
444
  if (!project) {
466
445
  errorResponse(res, 404, 'project_not_found', `projeto não encontrado: ${projectId}`);
467
446
  return;
@@ -476,7 +455,7 @@ export async function startObserverServer({
476
455
  errorResponse(res, 400, 'project_mismatch', 'project_id do corpo não corresponde à rota.');
477
456
  return;
478
457
  }
479
- const result = registerObserverProject(dataDir, {
458
+ const result = registerSqlProject(sqlDb, {
480
459
  projectId,
481
460
  projectName: body.project_name,
482
461
  wendkeepVersion: body.wendkeep_version,
@@ -485,30 +464,23 @@ export async function startObserverServer({
485
464
  errorResponse(res, 400, 'invalid_project', result.errors.join(' '));
486
465
  return;
487
466
  }
488
- registerSqlProject(sqlDb, {
489
- projectId,
490
- projectName: body.project_name,
491
- wendkeepVersion: body.wendkeep_version,
467
+ json(res, 201, {
468
+ projectId: result.project.project_id,
469
+ projectName: result.project.project_name,
470
+ wendkeepVersion: result.project.wendkeep_version,
471
+ registeredAt: result.project.registered_at,
492
472
  });
493
- json(res, 201, result.project);
494
473
  return;
495
474
  }
496
475
 
497
476
  if (parts.length === 4 && parts[3] === 'changes' && req.method === 'GET') {
498
- const project = getObserverProject(dataDir, projectId);
499
- if (!project) {
500
- let sqlProject;
501
- try { sqlProject = readSqlProject(sqlDb, projectId); } catch (error) {
502
- if (error?.code !== 'project_not_registered') throw error;
503
- }
504
- if (!sqlProject) {
505
- errorResponse(res, 404, 'project_not_found', `projeto não encontrado: ${projectId}`);
506
- return;
507
- }
508
- json(res, 200, { project_id: projectId, changes: [] });
477
+ let snapshot;
478
+ try { snapshot = readSqlProjectSnapshot(sqlDb, projectId); } catch (error) {
479
+ if (error?.code !== 'project_not_registered') throw error;
480
+ errorResponse(res, 404, 'project_not_found', `projeto não encontrado: ${projectId}`);
509
481
  return;
510
482
  }
511
- json(res, 200, { project_id: projectId, changes: project.snapshot?.changes || [] });
483
+ json(res, 200, { project_id: projectId, changes: snapshot?.changes || [] });
512
484
  return;
513
485
  }
514
486
 
@@ -519,14 +491,17 @@ export async function startObserverServer({
519
491
  errorResponse(res, 400, 'invalid_snapshot', validation.errors.join(' '));
520
492
  return;
521
493
  }
522
- const result = appendObserverEvent(dataDir, body);
494
+ const result = upsertSqlProjectSnapshot(sqlDb, body);
523
495
  if (!result.accepted && result.duplicate) {
524
496
  json(res, 200, { accepted: false, duplicate: true, event_id: body.event_id });
525
497
  return;
526
498
  }
527
499
  if (!result.accepted) {
528
- const unregistered = result.errors.some((item) => /não registrado/.test(item));
529
- errorResponse(res, unregistered ? 409 : 400, unregistered ? 'project_not_registered' : 'invalid_event', result.errors.join(' '));
500
+ if (result.stale) {
501
+ json(res, 200, { accepted: false, stale: true, event_id: body.event_id });
502
+ return;
503
+ }
504
+ errorResponse(res, 400, 'invalid_event', 'snapshot não aceito.');
530
505
  return;
531
506
  }
532
507
  json(res, 201, { accepted: true, duplicate: false, event_id: body.event_id });
@@ -41,7 +41,7 @@ function walk(root, relativeRoot, out) {
41
41
  for (const entry of readdirSync(root, { withFileTypes: true })) {
42
42
  const logicalPath = relativeRoot ? `${relativeRoot}/${entry.name}` : entry.name;
43
43
  if (entry.name.endsWith('.tmp') || entry.name.endsWith('.lock')
44
- || ['observer-memory-outbox', 'observer-outbox', 'observer-sql-outbox', 'observer-sql-state.json', 'observer-memory-state.json'].includes(entry.name)) continue;
44
+ || ['observer-memory-outbox', 'observer-outbox', 'observer-sql-outbox', 'observer-sql-state.json', 'observer-sql-publisher.lock', 'observer-memory-state.json'].includes(entry.name)) continue;
45
45
  const absolute = join(root, entry.name);
46
46
  if (entry.isDirectory()) walk(absolute, logicalPath, out);
47
47
  else if (entry.isFile()) out.push({ absolute, logicalPath });