wendkeep 0.74.0 → 0.75.0
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 +35 -0
- package/README.en.md +3 -3
- package/README.md +3 -3
- package/docs/en/commands/observer.md +22 -11
- package/docs/pt-BR/commands/observer.md +22 -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 +372 -12
- package/src/observer-sql-store.mjs +108 -28
- package/src/observer-store.mjs +15 -3
- package/src/observer.mjs +104 -14
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 });
|