wendkeep 0.59.0 → 0.61.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 +45 -0
- package/README.en.md +13 -3
- package/README.md +13 -3
- package/docs/en/commands/memory.md +6 -2
- package/docs/pt-BR/commands/memory.md +6 -2
- package/hooks/memory-handoff.mjs +1 -199
- package/hooks/memory-mode.mjs +1 -89
- package/hooks/memory-schema.mjs +1 -310
- package/hooks/memory-store.mjs +1 -900
- package/hooks/vault-path-safety.mjs +2 -558
- package/package.json +10 -2
- package/packages/cli/package.json +5 -0
- package/packages/harness/package.json +5 -0
- package/packages/integrations/package.json +5 -0
- package/packages/mcp/package.json +5 -0
- package/packages/pi/package.json +5 -0
- package/packages/vault/package.json +6 -0
- package/packages/vault/src/index.mjs +8 -0
- package/packages/vault/src/memory-handoff.mjs +199 -0
- package/packages/vault/src/memory-mode.mjs +89 -0
- package/packages/vault/src/memory-schema.mjs +310 -0
- package/packages/vault/src/memory-store.mjs +900 -0
- package/packages/vault/src/project-vault.mjs +327 -0
- package/packages/vault/src/validate-core.mjs +181 -0
- package/packages/vault/src/validate-memory.mjs +128 -0
- package/packages/vault/src/vault-path-safety.mjs +558 -0
- package/src/memory.mjs +77 -11
- package/src/project-vault.mjs +2 -326
- package/src/validate-core.mjs +1 -181
- package/src/validate-memory.mjs +1 -128
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
// Keep Core: project-to-Vault binding and resolution, independent from Harness policy.
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
renameSync,
|
|
8
|
+
statSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from 'node:fs';
|
|
11
|
+
import { basename, dirname, isAbsolute, join, parse, relative, resolve } from 'node:path';
|
|
12
|
+
|
|
13
|
+
export const PROJECT_CONFIG_FILE = '.wendkeep.json';
|
|
14
|
+
export const PROJECT_MARKER_REL = '.brain/PROJECT.json';
|
|
15
|
+
export const PROJECT_CONFIG_SCHEMA = 1;
|
|
16
|
+
|
|
17
|
+
const PROJECT_VAULT_INTEGRITY_CODES = new Set([
|
|
18
|
+
'WENDKEEP_VAULT_CONFIG_INVALID',
|
|
19
|
+
'WENDKEEP_VAULT_MARKER_MISSING',
|
|
20
|
+
'WENDKEEP_VAULT_PROJECT_MISMATCH',
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
export function isProjectVaultIntegrityError(error) {
|
|
24
|
+
return PROJECT_VAULT_INTEGRITY_CODES.has(error?.code);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function json(path) {
|
|
28
|
+
try { return JSON.parse(readFileSync(path, 'utf8')); }
|
|
29
|
+
catch (error) {
|
|
30
|
+
const wrapped = new Error(`Configuração WendKeep inválida em "${path}": ${error.message}`);
|
|
31
|
+
wrapped.code = 'WENDKEEP_VAULT_CONFIG_INVALID';
|
|
32
|
+
throw wrapped;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function atomicJson(path, value) {
|
|
37
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
38
|
+
const content = `${JSON.stringify(value, null, 2)}\n`;
|
|
39
|
+
if (existsSync(path) && readFileSync(path, 'utf8') === content) return false;
|
|
40
|
+
const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
41
|
+
writeFileSync(temp, content, 'utf8');
|
|
42
|
+
renameSync(temp, path);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function startDirectory(value) {
|
|
47
|
+
const candidate = resolve(String(value || process.cwd()));
|
|
48
|
+
try { return statSync(candidate).isFile() ? dirname(candidate) : candidate; }
|
|
49
|
+
catch { return candidate; }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function walkParents(start) {
|
|
53
|
+
const result = [];
|
|
54
|
+
let current = startDirectory(start);
|
|
55
|
+
const root = parse(current).root;
|
|
56
|
+
while (true) {
|
|
57
|
+
result.push(current);
|
|
58
|
+
if (current === root) break;
|
|
59
|
+
const parent = dirname(current);
|
|
60
|
+
if (parent === current) break;
|
|
61
|
+
current = parent;
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function inputStart(input = {}, fallback = '') {
|
|
67
|
+
return input.cwd
|
|
68
|
+
|| input.project_dir
|
|
69
|
+
|| input.projectDir
|
|
70
|
+
|| input.workspace?.cwd
|
|
71
|
+
|| process.env.CLAUDE_PROJECT_DIR
|
|
72
|
+
|| fallback
|
|
73
|
+
|| process.cwd();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function bindingDiagnostic(error) {
|
|
77
|
+
return {
|
|
78
|
+
code: error?.code || 'WENDKEEP_VAULT_CONFIG_INVALID',
|
|
79
|
+
message: error?.message || 'Configuração WendKeep inválida.',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function vaultFromConfig(projectRoot, config) {
|
|
84
|
+
const valid = config
|
|
85
|
+
&& typeof config === 'object'
|
|
86
|
+
&& !Array.isArray(config)
|
|
87
|
+
&& config.schemaVersion === PROJECT_CONFIG_SCHEMA
|
|
88
|
+
&& typeof config.projectId === 'string'
|
|
89
|
+
&& config.projectId.trim()
|
|
90
|
+
&& typeof config.vault === 'string'
|
|
91
|
+
&& config.vault.trim();
|
|
92
|
+
if (!valid) {
|
|
93
|
+
const error = new Error(
|
|
94
|
+
`Configuração incompleta em "${join(projectRoot, PROJECT_CONFIG_FILE)}". `
|
|
95
|
+
+ 'Rode `wendkeep init --project <path> --vault <path>`.',
|
|
96
|
+
);
|
|
97
|
+
error.code = 'WENDKEEP_VAULT_CONFIG_INVALID';
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
return isAbsolute(config.vault) ? resolve(config.vault) : resolve(projectRoot, config.vault);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function readProjectBinding(projectRoot) {
|
|
104
|
+
const root = resolve(projectRoot);
|
|
105
|
+
const path = join(root, PROJECT_CONFIG_FILE);
|
|
106
|
+
if (!existsSync(path)) return null;
|
|
107
|
+
const config = json(path);
|
|
108
|
+
return { config, configPath: path, projectRoot: root, base: vaultFromConfig(root, config) };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function findProjectBinding(start) {
|
|
112
|
+
for (const projectRoot of walkParents(start)) {
|
|
113
|
+
const found = readProjectBinding(projectRoot);
|
|
114
|
+
if (found) return found;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function findLegacyProjectVault(start) {
|
|
120
|
+
for (const projectRoot of walkParents(start)) {
|
|
121
|
+
const settingsPath = join(projectRoot, '.claude', 'settings.json');
|
|
122
|
+
if (!existsSync(settingsPath)) continue;
|
|
123
|
+
try {
|
|
124
|
+
const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
125
|
+
const raw = settings?.env?.OBSIDIAN_VAULT_PATH;
|
|
126
|
+
if (typeof raw === 'string' && raw.trim()) {
|
|
127
|
+
return {
|
|
128
|
+
base: isAbsolute(raw) ? resolve(raw) : resolve(projectRoot, raw),
|
|
129
|
+
projectRoot,
|
|
130
|
+
source: 'legacy-project-settings',
|
|
131
|
+
configPath: settingsPath,
|
|
132
|
+
projectId: '',
|
|
133
|
+
config: null,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
} catch (error) {
|
|
137
|
+
const wrapped = new Error(`Configuração WendKeep legada inválida em "${settingsPath}": ${error.message}`);
|
|
138
|
+
wrapped.code = 'WENDKEEP_VAULT_CONFIG_INVALID';
|
|
139
|
+
throw wrapped;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function readVaultMarker(vaultPath) {
|
|
146
|
+
const markerPath = join(resolve(vaultPath), ...PROJECT_MARKER_REL.split('/'));
|
|
147
|
+
if (!existsSync(markerPath)) return null;
|
|
148
|
+
return { marker: json(markerPath), markerPath };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function validateMarker(result) {
|
|
152
|
+
const found = readVaultMarker(result.base);
|
|
153
|
+
if (!found) {
|
|
154
|
+
const error = new Error(
|
|
155
|
+
`O vault "${result.base}" ainda não possui ${PROJECT_MARKER_REL}. `
|
|
156
|
+
+ `Rode \`wendkeep init --project "${result.projectRoot}" --vault "${result.base}" --yes\`.`,
|
|
157
|
+
);
|
|
158
|
+
error.code = 'WENDKEEP_VAULT_MARKER_MISSING';
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
if (found.marker?.projectId !== result.projectId) {
|
|
162
|
+
const error = new Error(
|
|
163
|
+
`Vault de outro projeto: configuração "${result.projectId}" aponta para marcador `
|
|
164
|
+
+ `"${found.marker?.projectId || 'ausente'}" em "${found.markerPath}".`,
|
|
165
|
+
);
|
|
166
|
+
error.code = 'WENDKEEP_VAULT_PROJECT_MISMATCH';
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function resolveProjectVault({
|
|
172
|
+
input = {},
|
|
173
|
+
startDir = '',
|
|
174
|
+
explicitVault = '',
|
|
175
|
+
allowLegacySettings = true,
|
|
176
|
+
validateIdentity = true,
|
|
177
|
+
} = {}) {
|
|
178
|
+
const start = inputStart(input, startDir);
|
|
179
|
+
const explicit = explicitVault || input?.obsidian_vault_path;
|
|
180
|
+
if (explicit) {
|
|
181
|
+
let bindingError = null;
|
|
182
|
+
try { findProjectBinding(start); }
|
|
183
|
+
catch (error) {
|
|
184
|
+
if (error?.code !== 'WENDKEEP_VAULT_CONFIG_INVALID') throw error;
|
|
185
|
+
bindingError = bindingDiagnostic(error);
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
base: isAbsolute(explicit) ? resolve(explicit) : resolve(startDirectory(start), explicit),
|
|
189
|
+
source: explicitVault ? 'explicit' : 'payload',
|
|
190
|
+
projectRoot: startDirectory(start),
|
|
191
|
+
projectId: '',
|
|
192
|
+
configPath: '',
|
|
193
|
+
config: null,
|
|
194
|
+
...(bindingError ? { bindingError } : {}),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let binding = null;
|
|
199
|
+
let bindingFailure = null;
|
|
200
|
+
try { binding = findProjectBinding(start); }
|
|
201
|
+
catch (error) {
|
|
202
|
+
if (error?.code !== 'WENDKEEP_VAULT_CONFIG_INVALID') throw error;
|
|
203
|
+
bindingFailure = error;
|
|
204
|
+
}
|
|
205
|
+
if (binding) {
|
|
206
|
+
const result = {
|
|
207
|
+
base: binding.base,
|
|
208
|
+
source: 'project-config',
|
|
209
|
+
projectRoot: binding.projectRoot,
|
|
210
|
+
projectId: binding.config.projectId,
|
|
211
|
+
configPath: binding.configPath,
|
|
212
|
+
config: binding.config,
|
|
213
|
+
};
|
|
214
|
+
if (validateIdentity) validateMarker(result);
|
|
215
|
+
return result;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (allowLegacySettings) {
|
|
219
|
+
const legacy = findLegacyProjectVault(start);
|
|
220
|
+
if (legacy) {
|
|
221
|
+
return {
|
|
222
|
+
...legacy,
|
|
223
|
+
...(bindingFailure ? { bindingError: bindingDiagnostic(bindingFailure) } : {}),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (bindingFailure) throw bindingFailure;
|
|
229
|
+
|
|
230
|
+
const error = new Error(
|
|
231
|
+
`Nenhum vault WendKeep vinculado ao projeto em "${startDirectory(start)}". `
|
|
232
|
+
+ `Crie ${PROJECT_CONFIG_FILE} com \`wendkeep init --project "${startDirectory(start)}" --vault <path> --yes\`.`,
|
|
233
|
+
);
|
|
234
|
+
error.code = 'WENDKEEP_VAULT_UNCONFIGURED';
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function portableVaultPath(projectRoot, vaultPath) {
|
|
239
|
+
const rel = relative(projectRoot, vaultPath);
|
|
240
|
+
if (rel && !rel.startsWith('..') && !isAbsolute(rel)) return rel.replaceAll('\\', '/');
|
|
241
|
+
return vaultPath;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function objectRecord(value) {
|
|
245
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function bindProjectVault({ projectRoot, vaultPath, configPatch = {} }) {
|
|
249
|
+
const root = resolve(projectRoot);
|
|
250
|
+
const base = isAbsolute(vaultPath) ? resolve(vaultPath) : resolve(root, vaultPath);
|
|
251
|
+
const existing = readProjectBinding(root);
|
|
252
|
+
const existingMarker = readVaultMarker(base);
|
|
253
|
+
const projectId = existing?.config?.projectId || existingMarker?.marker?.projectId || randomUUID();
|
|
254
|
+
|
|
255
|
+
if (existingMarker?.marker?.projectId && existingMarker.marker.projectId !== projectId) {
|
|
256
|
+
const error = new Error(
|
|
257
|
+
`Não é seguro vincular "${root}" ao vault de outro projeto: `
|
|
258
|
+
+ `esperado "${projectId}", encontrado "${existingMarker.marker.projectId}".`,
|
|
259
|
+
);
|
|
260
|
+
error.code = 'WENDKEEP_VAULT_PROJECT_MISMATCH';
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
mkdirSync(join(base, '.brain'), { recursive: true });
|
|
265
|
+
const previousConfig = objectRecord(existing?.config);
|
|
266
|
+
const patch = objectRecord(configPatch);
|
|
267
|
+
const config = {
|
|
268
|
+
...previousConfig,
|
|
269
|
+
...patch,
|
|
270
|
+
schemaVersion: PROJECT_CONFIG_SCHEMA,
|
|
271
|
+
projectId,
|
|
272
|
+
vault: portableVaultPath(root, base),
|
|
273
|
+
};
|
|
274
|
+
if (previousConfig.harness || patch.harness) {
|
|
275
|
+
config.harness = {
|
|
276
|
+
...objectRecord(previousConfig.harness),
|
|
277
|
+
...objectRecord(patch.harness),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
const marker = {
|
|
281
|
+
...objectRecord(existingMarker?.marker),
|
|
282
|
+
schemaVersion: PROJECT_CONFIG_SCHEMA,
|
|
283
|
+
projectId,
|
|
284
|
+
projectName: basename(root),
|
|
285
|
+
};
|
|
286
|
+
atomicJson(join(base, ...PROJECT_MARKER_REL.split('/')), marker);
|
|
287
|
+
atomicJson(join(root, PROJECT_CONFIG_FILE), config);
|
|
288
|
+
return { base, projectRoot: root, projectId, config, marker };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function updateProjectBinding(projectRoot, updater) {
|
|
292
|
+
const binding = readProjectBinding(projectRoot);
|
|
293
|
+
if (!binding) {
|
|
294
|
+
const error = new Error(
|
|
295
|
+
`Nenhum binding WendKeep em "${resolve(projectRoot)}". Rode \`wendkeep init\` primeiro.`,
|
|
296
|
+
);
|
|
297
|
+
error.code = 'WENDKEEP_VAULT_UNCONFIGURED';
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
if (typeof updater !== 'function') {
|
|
301
|
+
throw new TypeError('updateProjectBinding exige uma função updater.');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const current = {
|
|
305
|
+
...binding.config,
|
|
306
|
+
...(binding.config.harness ? { harness: { ...objectRecord(binding.config.harness) } } : {}),
|
|
307
|
+
};
|
|
308
|
+
const candidate = updater(current);
|
|
309
|
+
if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
|
|
310
|
+
const error = new Error('Updater do binding WendKeep deve retornar um objeto de configuração.');
|
|
311
|
+
error.code = 'WENDKEEP_VAULT_CONFIG_INVALID';
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const config = {
|
|
316
|
+
...candidate,
|
|
317
|
+
schemaVersion: PROJECT_CONFIG_SCHEMA,
|
|
318
|
+
projectId: binding.config.projectId,
|
|
319
|
+
vault: binding.config.vault,
|
|
320
|
+
};
|
|
321
|
+
atomicJson(binding.configPath, config);
|
|
322
|
+
return {
|
|
323
|
+
...binding,
|
|
324
|
+
config,
|
|
325
|
+
base: vaultFromConfig(binding.projectRoot, config),
|
|
326
|
+
};
|
|
327
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// Memory-compaction protocol for the curated .brain/CORE.md layer.
|
|
2
|
+
// Ported from NutriGym-Vision's scripts/validate-brain-core.js to ESM:
|
|
3
|
+
// - cap 25 lines (hard), 22 (soft warning) — 1 durable item per line
|
|
4
|
+
// - 3 required sections
|
|
5
|
+
// - no secrets / no real-provider PII emails
|
|
6
|
+
// Plus the seeded skeleton and the protocol reference doc.
|
|
7
|
+
|
|
8
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
9
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
10
|
+
|
|
11
|
+
const HARD_LIMIT = 25;
|
|
12
|
+
const SOFT_LIMIT = 22;
|
|
13
|
+
|
|
14
|
+
// Bilingual (0.8.0): a CORE is valid when it carries the COMPLETE section set of either
|
|
15
|
+
// locale — pt-BR or en. Mixed/partial sets fail (the 3 sections are one contract).
|
|
16
|
+
const SECTION_SETS = {
|
|
17
|
+
'pt-BR': [
|
|
18
|
+
{ label: 'Preferências do Usuário', regex: /^##\s+Prefer[êe]ncias\s+do\s+Usu[áa]rio\s*$/im },
|
|
19
|
+
{ label: 'Padrões Ativos', regex: /^##\s+Padr[õo]es\s+Ativos\s*$/im },
|
|
20
|
+
{ label: 'Pendências Abertas', regex: /^##\s+Pend[êe]ncias\s+Abertas\s*$/im },
|
|
21
|
+
],
|
|
22
|
+
en: [
|
|
23
|
+
{ label: 'User Preferences', regex: /^##\s+User\s+Preferences\s*$/im },
|
|
24
|
+
{ label: 'Active Patterns', regex: /^##\s+Active\s+Patterns\s*$/im },
|
|
25
|
+
{ label: 'Open Items', regex: /^##\s+Open\s+Items\s*$/im },
|
|
26
|
+
],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Secret patterns reject only "real" values (length floor); abstract mentions like
|
|
30
|
+
// `sk_*` / `whsec_*` (trailing asterisk) are allowed.
|
|
31
|
+
const SECRET_PATTERNS = [
|
|
32
|
+
{ name: 'Stripe secret key', regex: /\bsk_(?:live|test)_[A-Za-z0-9]{20,}\b/ },
|
|
33
|
+
{ name: 'Stripe webhook secret', regex: /\bwhsec_[A-Za-z0-9]{20,}\b/ },
|
|
34
|
+
{ name: 'JWT token', regex: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/ },
|
|
35
|
+
{ name: 'Bearer token', regex: /\bBearer\s+[A-Za-z0-9._-]{20,}\b/i },
|
|
36
|
+
{ name: 'OpenAI API key', regex: /\bsk-[A-Za-z0-9]{40,}\b/ },
|
|
37
|
+
{ name: 'Anthropic API key', regex: /\bsk-ant-[A-Za-z0-9_-]{40,}\b/ },
|
|
38
|
+
{ name: 'Google API key', regex: /\bAIza[0-9A-Za-z_-]{35}\b/ },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const PII_EMAIL_REGEX = /\b[A-Za-z0-9._%+-]+@(?!example\.(?:com|org|net)\b)(?:gmail|hotmail|yahoo|outlook|live|icloud|protonmail)\.[A-Za-z]{2,}\b/i;
|
|
42
|
+
|
|
43
|
+
// Validate CORE.md content. Returns { ok, errors, warnings, lineCount }.
|
|
44
|
+
export function validateCore(content) {
|
|
45
|
+
const text = String(content ?? '');
|
|
46
|
+
const lines = text.split('\n');
|
|
47
|
+
const lineCount = text.endsWith('\n') ? lines.length - 1 : lines.length;
|
|
48
|
+
const errors = [];
|
|
49
|
+
|
|
50
|
+
if (lineCount > HARD_LIMIT) {
|
|
51
|
+
errors.push(`Tamanho ${lineCount} > ${HARD_LIMIT} linhas (hard limit). Curar: remover itens resolvidos (detalhe vive no vault/git).`);
|
|
52
|
+
}
|
|
53
|
+
// Pick the locale set that matches best; require it to be complete.
|
|
54
|
+
const missingBySet = Object.values(SECTION_SETS).map((set) => set.filter(({ regex }) => !regex.test(text)));
|
|
55
|
+
const best = missingBySet.reduce((a, b) => (b.length < a.length ? b : a));
|
|
56
|
+
for (const { label } of best) errors.push(`Seção obrigatória ausente: ## ${label}`);
|
|
57
|
+
for (const { name, regex } of SECRET_PATTERNS) {
|
|
58
|
+
const m = text.match(regex);
|
|
59
|
+
if (m) errors.push(`Possível ${name} detectado: "${m[0].slice(0, 30)}..." — substituir por [REDACTED_SECRET].`);
|
|
60
|
+
}
|
|
61
|
+
const em = text.match(PII_EMAIL_REGEX);
|
|
62
|
+
if (em) errors.push(`Email real detectado: "${em[0]}" — usar user@example.com.`);
|
|
63
|
+
|
|
64
|
+
const warnings = [];
|
|
65
|
+
if (lineCount >= SOFT_LIMIT && lineCount <= HARD_LIMIT) {
|
|
66
|
+
warnings.push(`Tamanho ${lineCount}/${HARD_LIMIT} linhas — perto do limite; remover itens resolvidos (≥${SOFT_LIMIT}).`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { ok: errors.length === 0, errors, warnings, lineCount };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// The seeded CORE.md (must pass validateCore). Bootstraps the 3 sections so the
|
|
73
|
+
// curated hot layer exists with the right shape from day one.
|
|
74
|
+
export function renderCoreSkeleton(localeId = 'pt-BR') {
|
|
75
|
+
if (localeId === 'en') {
|
|
76
|
+
return `# CORE — curated memory core (.brain)
|
|
77
|
+
|
|
78
|
+
> RULE #1 — the project's canonical memory. Hand-curated, 25-line cap (validate: \`wendkeep validate-memory\`). Volatile facts live in DIGEST.md (auto). Depth: /brain-recall <topic>.
|
|
79
|
+
|
|
80
|
+
## User Preferences
|
|
81
|
+
- (durable preferences: language, style, conventions)
|
|
82
|
+
|
|
83
|
+
## Active Patterns
|
|
84
|
+
- (active patterns/architecture another agent must know)
|
|
85
|
+
|
|
86
|
+
## Open Items
|
|
87
|
+
- (open items/decisions — remove when resolved)
|
|
88
|
+
`;
|
|
89
|
+
}
|
|
90
|
+
return `# CORE — núcleo curado da memória (.brain)
|
|
91
|
+
|
|
92
|
+
> REGRA #1 — memória canônica do projeto. Curado à mão, cap 25 linhas (valide: \`wendkeep validate-memory\`). Volátil vive no DIGEST.md (auto). Profundidade: /brain-recall <tópico>.
|
|
93
|
+
|
|
94
|
+
## Preferências do Usuário
|
|
95
|
+
- (preferências duráveis: idioma, estilo, convenções)
|
|
96
|
+
|
|
97
|
+
## Padrões Ativos
|
|
98
|
+
- (padrões/arquitetura ativos que outro agente precise saber)
|
|
99
|
+
|
|
100
|
+
## Pendências Abertas
|
|
101
|
+
- (pendências/decisões em aberto — remova quando resolvidas)
|
|
102
|
+
`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// The compaction-protocol reference doc dropped into the vault.
|
|
106
|
+
export function renderCompactionProtocol() {
|
|
107
|
+
return `# Protocolo de Memória — núcleo curado + digest automático (.brain)
|
|
108
|
+
|
|
109
|
+
> Como cada agente recebe, consulta e persiste memória entre sessões no seu vault.
|
|
110
|
+
|
|
111
|
+
## 1. Duas camadas
|
|
112
|
+
|
|
113
|
+
- **QUENTE** (auto-injetada por sessão, budget ~45 linhas):
|
|
114
|
+
- \`.brain/CORE.md\` — curado à mão, **≤25 linhas** (1 item/linha): preferências, padrões, pendências.
|
|
115
|
+
- \`.brain/DIGEST.md\` — auto-gerado (0 token LLM, ≤15 linhas): decisões/sessões/bugs/aprendizados recentes.
|
|
116
|
+
- **FRIA** (sob demanda):
|
|
117
|
+
- \`.brain/index.jsonl\` — índice de todas as sessões (1/linha, frontmatter).
|
|
118
|
+
- Vault: \`02-Sessões/**\`, \`04-Decisões/**\`, \`05-Bugs/**\`, \`06-Aprendizados/**\`. Desce via \`/brain-recall <tópico>\`.
|
|
119
|
+
|
|
120
|
+
## 2. Compactação = regra de geração (sem trabalho manual)
|
|
121
|
+
|
|
122
|
+
- **DIGEST se auto-compacta**: caps determinísticos (5 decisões, 4 sessões, 2 bugs, 2 aprendizados + \`+N mais\`). O velho cai do quente sozinho e permanece no índice/vault. **NUNCA editar** \`DIGEST.md\`/\`index.jsonl\`.
|
|
123
|
+
- **CORE**: quando ≥22 linhas (soft warning), remover itens resolvidos/obsoletos — o detalhe já vive no vault e no histórico do git.
|
|
124
|
+
|
|
125
|
+
## 3. O que escrever no CORE
|
|
126
|
+
|
|
127
|
+
Só estado **durável** que outro agente precise saber — preferência, padrão ativo, pendência aberta. 1 linha por item. Nunca log de sessão (isso é automático no vault).
|
|
128
|
+
|
|
129
|
+
3 seções fixas (obrigatórias): \`## Preferências do Usuário\`, \`## Padrões Ativos\`, \`## Pendências Abertas\`.
|
|
130
|
+
|
|
131
|
+
## 4. Sem segredos / PII
|
|
132
|
+
|
|
133
|
+
\`CORE.md\` nunca contém tokens (\`sk_*\`, \`whsec_*\`, JWT, Bearer), API keys, senhas ou email/telefone real. Use \`[REDACTED_SECRET]\` / \`user@example.com\`.
|
|
134
|
+
|
|
135
|
+
## 5. Validação
|
|
136
|
+
|
|
137
|
+
\`\`\`bash
|
|
138
|
+
wendkeep validate-memory # valida <vault>/.brain/CORE.md
|
|
139
|
+
wendkeep validate-memory <path> # valida outro arquivo
|
|
140
|
+
\`\`\`
|
|
141
|
+
|
|
142
|
+
Checa: cap 25 (soft 22), 3 seções, sem segredos/PII. Exit 0 = OK, 1 = falha.
|
|
143
|
+
`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// CLI entry for `wendkeep validate-memory [path]`. Resolves the target from an
|
|
147
|
+
// explicit path, else <vault>/.brain/CORE.md (--vault or OBSIDIAN_VAULT_PATH).
|
|
148
|
+
export function runValidateMemory(argv) {
|
|
149
|
+
let target;
|
|
150
|
+
let vault;
|
|
151
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
152
|
+
const a = argv[i];
|
|
153
|
+
if (a === '--vault') vault = argv[++i];
|
|
154
|
+
else if (a.startsWith('--vault=')) vault = a.slice(8);
|
|
155
|
+
else if (!a.startsWith('-')) target = a;
|
|
156
|
+
}
|
|
157
|
+
if (!target) {
|
|
158
|
+
const base = vault || process.env.OBSIDIAN_VAULT_PATH;
|
|
159
|
+
if (!base) {
|
|
160
|
+
process.stderr.write('wendkeep validate-memory: no target. Pass a path, --vault <path>, or set OBSIDIAN_VAULT_PATH.\n');
|
|
161
|
+
process.exit(2);
|
|
162
|
+
}
|
|
163
|
+
target = join(base, '.brain', 'CORE.md');
|
|
164
|
+
}
|
|
165
|
+
const abs = isAbsolute(target) ? target : resolve(process.cwd(), target);
|
|
166
|
+
if (!existsSync(abs)) {
|
|
167
|
+
process.stderr.write(`wendkeep validate-memory: not found: ${abs}\n`);
|
|
168
|
+
process.exit(2);
|
|
169
|
+
}
|
|
170
|
+
const res = validateCore(readFileSync(abs, 'utf8'));
|
|
171
|
+
if (!res.ok) {
|
|
172
|
+
process.stderr.write(`❌ CORE.md viola protocolo (${res.errors.length} erro${res.errors.length > 1 ? 's' : ''}):\n`);
|
|
173
|
+
for (const e of res.errors) process.stderr.write(` - ${e}\n`);
|
|
174
|
+
process.stderr.write('\nProtocolo: .brain/COMPACTION_PROTOCOL.md\n');
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
let msg = `✅ CORE.md OK (${res.lineCount} linhas, 3/3 seções, sem segredos).`;
|
|
178
|
+
for (const w of res.warnings) msg += `\n ⚠ ${w}`;
|
|
179
|
+
process.stdout.write(`${msg}\n`);
|
|
180
|
+
process.exit(0);
|
|
181
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { validateMemoryEvent, validateSharedMemory } from './memory-schema.mjs';
|
|
4
|
+
import { assertVaultPathSafe } from './vault-path-safety.mjs';
|
|
5
|
+
import { validateCore } from './validate-core.mjs';
|
|
6
|
+
|
|
7
|
+
function failedComponent(errors, extra = {}) {
|
|
8
|
+
return { ok: false, errors: Array.isArray(errors) ? errors : [errors], warnings: [], ...extra };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function readRequired(vaultBase, path, label) {
|
|
12
|
+
let checked;
|
|
13
|
+
try {
|
|
14
|
+
checked = assertVaultPathSafe(vaultBase, path, {
|
|
15
|
+
expectedType: 'file', label: `artefato ${label}`,
|
|
16
|
+
});
|
|
17
|
+
} catch (error) {
|
|
18
|
+
return { ok: false, error: `${label} inseguro: ${error?.message || error}` };
|
|
19
|
+
}
|
|
20
|
+
if (!checked.exists) return { ok: false, error: `${label} ausente: ${path}` };
|
|
21
|
+
try {
|
|
22
|
+
// Deliberately adjacent to the open performed by readFileSync.
|
|
23
|
+
checked = assertVaultPathSafe(vaultBase, checked.target, {
|
|
24
|
+
allowMissing: false, expectedType: 'file', label: `artefato ${label}`,
|
|
25
|
+
});
|
|
26
|
+
return { ok: true, content: readFileSync(checked.target, 'utf8') };
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return { ok: false, error: `${label} ilegível: ${error?.message || error}` };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function readProjectForValidation(vaultBase) {
|
|
33
|
+
const path = join(vaultBase, '.brain', 'PROJECT.json');
|
|
34
|
+
const read = readRequired(vaultBase, path, 'PROJECT.json');
|
|
35
|
+
if (!read.ok) return failedComponent(read.error, { projectId: '', path });
|
|
36
|
+
try {
|
|
37
|
+
const marker = JSON.parse(read.content);
|
|
38
|
+
if (!marker || typeof marker.projectId !== 'string' || !marker.projectId) {
|
|
39
|
+
return failedComponent('PROJECT.json inválido: projectId ausente.', { projectId: '', path });
|
|
40
|
+
}
|
|
41
|
+
return { ok: true, errors: [], warnings: [], projectId: marker.projectId, marker, path };
|
|
42
|
+
} catch (error) {
|
|
43
|
+
return failedComponent(`PROJECT.json contém JSON inválido: ${error?.message || error}`, { projectId: '', path });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Read and validate the append-only JSONL authority without repairing or mutating it. */
|
|
48
|
+
export function readLedgerForValidation(vaultBase, { projectId } = {}) {
|
|
49
|
+
const path = join(vaultBase, '.brain', 'MEMORY_EVENTS.jsonl');
|
|
50
|
+
const read = readRequired(vaultBase, path, 'MEMORY_EVENTS.jsonl');
|
|
51
|
+
if (!read.ok) return failedComponent(read.error, { events: [], eventIds: new Set(), path });
|
|
52
|
+
|
|
53
|
+
const errors = [];
|
|
54
|
+
const warnings = [];
|
|
55
|
+
const events = [];
|
|
56
|
+
const eventIds = new Set();
|
|
57
|
+
const normalized = read.content.replace(/\r\n/g, '\n');
|
|
58
|
+
const lines = normalized.split('\n');
|
|
59
|
+
const logicalLines = normalized === '' ? [] : (normalized.endsWith('\n') ? lines.slice(0, -1) : lines);
|
|
60
|
+
logicalLines.forEach((line, index) => {
|
|
61
|
+
if (!line.trim()) {
|
|
62
|
+
errors.push(`MEMORY_EVENTS.jsonl linha ${index + 1} está vazia no meio do ledger.`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
let event;
|
|
66
|
+
try {
|
|
67
|
+
event = JSON.parse(line);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
errors.push(`MEMORY_EVENTS.jsonl linha ${index + 1} contém JSON inválido/parcial: ${error?.message || error}`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const validation = validateMemoryEvent(event, projectId ? { projectId } : {});
|
|
73
|
+
for (const error of validation.errors) errors.push(`MEMORY_EVENTS.jsonl linha ${index + 1}: ${error}`);
|
|
74
|
+
for (const warning of validation.warnings) warnings.push(`MEMORY_EVENTS.jsonl linha ${index + 1}: ${warning}`);
|
|
75
|
+
if (typeof event?.event_id === 'string' && event.event_id) {
|
|
76
|
+
if (eventIds.has(event.event_id)) errors.push(`MEMORY_EVENTS.jsonl event_id duplicado: ${event.event_id}.`);
|
|
77
|
+
eventIds.add(event.event_id);
|
|
78
|
+
}
|
|
79
|
+
events.push(event);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
ok: errors.length === 0,
|
|
84
|
+
errors,
|
|
85
|
+
warnings,
|
|
86
|
+
events,
|
|
87
|
+
eventIds,
|
|
88
|
+
lineCount: logicalLines.length,
|
|
89
|
+
path,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function validateCoreArtifact(vaultBase) {
|
|
94
|
+
const path = join(vaultBase, '.brain', 'CORE.md');
|
|
95
|
+
const read = readRequired(vaultBase, path, 'CORE.md');
|
|
96
|
+
if (!read.ok) return failedComponent(read.error, { lineCount: 0, path });
|
|
97
|
+
return { ...validateCore(read.content), path, content: read.content };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function validateSharedArtifact(vaultBase, eventIds) {
|
|
101
|
+
const path = join(vaultBase, '.brain', 'SHARED_MEMORY.md');
|
|
102
|
+
const read = readRequired(vaultBase, path, 'SHARED_MEMORY.md');
|
|
103
|
+
if (!read.ok) return failedComponent(read.error, { path });
|
|
104
|
+
return { ...validateSharedMemory(read.content, { eventIds }), path, content: read.content };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function combineMemoryResults({ project, core, ledger, shared }) {
|
|
108
|
+
const components = { project, core, ledger, shared };
|
|
109
|
+
const errors = [];
|
|
110
|
+
const warnings = [];
|
|
111
|
+
for (const [name, result] of Object.entries(components)) {
|
|
112
|
+
for (const error of result?.errors || []) errors.push(`${name}: ${error}`);
|
|
113
|
+
for (const warning of result?.warnings || []) warnings.push(`${name}: ${warning}`);
|
|
114
|
+
}
|
|
115
|
+
return { ok: errors.length === 0, errors, warnings, ...components };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Validate the v2 local memory bundle as a read-only composition. Missing/corrupt
|
|
120
|
+
* artifacts remain explicit failures; they are never silently treated as empty.
|
|
121
|
+
*/
|
|
122
|
+
export function validateMemoryBundle(vaultBase) {
|
|
123
|
+
const project = readProjectForValidation(vaultBase);
|
|
124
|
+
const core = validateCoreArtifact(vaultBase);
|
|
125
|
+
const ledger = readLedgerForValidation(vaultBase, { projectId: project.projectId });
|
|
126
|
+
const shared = validateSharedArtifact(vaultBase, ledger.eventIds);
|
|
127
|
+
return combineMemoryResults({ project, core, ledger, shared });
|
|
128
|
+
}
|