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/CHANGELOG.md +47 -0
- package/README.en.md +3 -3
- package/README.md +3 -3
- package/docs/en/commands/observer.md +29 -13
- package/docs/pt-BR/commands/observer.md +27 -11
- package/package.json +1 -1
- package/packages/cli/src/index.mjs +1 -1
- package/schema/observer/005-project-scoped-identities.sql +217 -0
- package/src/change.mjs +41 -1
- package/src/doctor.mjs +5 -0
- package/src/init.mjs +2 -2
- package/src/note.mjs +8 -1
- package/src/observer-publish.mjs +9 -34
- package/src/observer-server.mjs +38 -63
- package/src/observer-sql-migrate.mjs +1 -1
- package/src/observer-sql-publish.mjs +392 -18
- package/src/observer-sql-store.mjs +108 -28
- package/src/observer-store.mjs +15 -3
- package/src/observer.mjs +106 -14
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
|
-
|
|
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
|
}
|
package/src/observer-publish.mjs
CHANGED
|
@@ -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 {
|
|
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
|
|
99
|
-
|
|
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:
|
|
101
|
+
projectId: project.projectId,
|
|
102
102
|
url,
|
|
103
103
|
input,
|
|
104
104
|
now,
|
|
105
105
|
token,
|
|
106
106
|
});
|
|
107
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/observer-server.mjs
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
186
|
-
||
|
|
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
|
-
...
|
|
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:
|
|
303
|
-
projects:
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
|
453
|
-
|
|
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 =
|
|
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
|
-
|
|
489
|
-
projectId,
|
|
490
|
-
projectName:
|
|
491
|
-
wendkeepVersion:
|
|
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
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
529
|
-
|
|
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 });
|