wendkeep 0.68.0 → 0.68.5

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.
@@ -1,159 +1,159 @@
1
- // .agent/hooks/brain-core.mjs
2
- // Camada fria do brain: indexa o frontmatter das notas de sessão (0 token LLM).
3
- import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
4
- import { basename, join } from 'node:path';
5
- import { ensureDir, stripYamlQuotes, toVaultRelative } from './obsidian-common.mjs';
6
- import { getLocale } from './locale.mjs';
7
-
8
- export function brainDir(vaultBase) {
9
- return join(vaultBase, '.brain');
10
- }
11
-
12
- // Frontmatter YAML simples: escalares `k: v` + listas `k:` seguido de ` - item`.
13
- export function parseFrontmatter(content) {
14
- const m = content.match(/^---\n([\s\S]*?)\n---/);
15
- if (!m) return {};
16
- const data = {};
17
- const lines = m[1].split('\n');
18
- for (let i = 0; i < lines.length; i++) {
19
- const kv = lines[i].match(/^([\w-]+):\s*(.*)$/);
20
- if (!kv) continue;
21
- const key = kv[1];
22
- const val = kv[2];
23
- if (val === '') {
24
- const list = [];
25
- while (i + 1 < lines.length && /^\s+-\s+/.test(lines[i + 1])) {
26
- list.push(stripYamlQuotes(lines[++i].replace(/^\s+-\s+/, '').trim()));
27
- }
28
- data[key] = list.length ? list : '';
29
- } else {
30
- data[key] = stripYamlQuotes(val.trim());
31
- }
32
- }
33
- return data;
34
- }
35
-
36
- function walkMd(dir) {
37
- const out = [];
38
- let entries;
39
- try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return out; }
40
- for (const e of entries) {
41
- const fp = join(dir, e.name);
42
- if (e.isDirectory()) out.push(...walkMd(fp));
43
- else if (e.name.endsWith('.md')) out.push(fp);
44
- }
45
- return out;
46
- }
47
-
48
- const DERIVED_RE = /\[\[(0[456]-[^\]|]+?)(?:\|[^\]]*)?\]\]/g;
49
- function derivedLinks(content) {
50
- const dec = new Set(), bug = new Set(), lea = new Set();
51
- let m;
52
- while ((m = DERIVED_RE.exec(content))) {
53
- const t = m[1];
54
- if (t.startsWith('04-')) dec.add(t);
55
- else if (t.startsWith('05-')) bug.add(t);
56
- else if (t.startsWith('06-')) lea.add(t);
57
- }
58
- return { decisions: [...dec], bugs: [...bug], learnings: [...lea] };
59
- }
60
-
61
- // Varre 02-Sessões/** e regrava .brain/index.jsonl inteiro. Provider-agnóstico.
62
- export function buildBrainIndex(vaultBase) {
63
- const rows = [];
64
- for (const fp of walkMd(join(vaultBase, getLocale(vaultBase).folders.sessions))) {
65
- let content;
66
- try { content = readFileSync(fp, 'utf8'); } catch { continue; }
67
- const fm = parseFrontmatter(content);
68
- if (fm.type && fm.type !== 'session') continue;
69
- const der = derivedLinks(content);
70
- rows.push({
71
- session_id: fm.session_id || '',
72
- date: fm.date || '',
73
- provider: fm.provider || '',
74
- status: fm.status || '',
75
- summary: fm.summary || '',
76
- file: toVaultRelative(vaultBase, fp),
77
- tags: Array.isArray(fm.tags) ? fm.tags : (fm.tags ? [fm.tags] : []),
78
- decisions: der.decisions,
79
- bugs: der.bugs,
80
- learnings: der.learnings,
81
- });
82
- }
83
- rows.sort((a, b) => (a.date + a.file).localeCompare(b.date + b.file));
84
- ensureDir(brainDir(vaultBase));
85
- const out = rows.map((r) => JSON.stringify(r)).join('\n') + (rows.length ? '\n' : '');
86
- writeFileSync(join(brainDir(vaultBase), 'index.jsonl'), out, 'utf8');
87
- return rows;
88
- }
89
-
90
- // Lê o índice gravado (linhas JSONL). Usado pelo recall e pelo digest.
91
- export function loadIndex(vaultBase) {
92
- try {
93
- return readFileSync(join(brainDir(vaultBase), 'index.jsonl'), 'utf8')
94
- .split('\n').filter(Boolean).map((l) => JSON.parse(l));
95
- } catch {
96
- return [];
97
- }
98
- }
99
-
100
- const DIGEST_CAPS = { decisions: 5, sessions: 4, bugs: 2, learnings: 2 };
101
-
102
- function adrNumber(path) {
103
- const m = path.match(/ADR-(\d+)/);
104
- return m ? Number(m[1]) : -1;
105
- }
106
-
107
- // Destila index.jsonl em .brain/DIGEST.md (camada quente, determinístico, 0 token LLM).
108
- // Cap por construção: 1 header + 13 itens (5/4/2/2) + 1 pointer = máx 15 linhas.
109
- export function buildBrainDigest(vaultBase, rows = null) {
110
- const data = rows ?? loadIndex(vaultBase);
111
- const byDateDesc = [...data].sort((a, b) =>
112
- String(b.date || '').localeCompare(String(a.date || '')) || String(b.file || '').localeCompare(String(a.file || '')));
113
-
114
- const seen = new Set();
115
- const pick = (kind, max) => {
116
- const out = [];
117
- for (const r of byDateDesc) {
118
- for (const p of r[kind] || []) {
119
- if (out.length >= max) return out;
120
- if (!seen.has(p)) { seen.add(p); out.push(p); }
121
- }
122
- }
123
- return out;
124
- };
125
-
126
- // The digest is INJECTED into every session, so a dead wikilink is dead weight in the model's
127
- // context. Keep only targets that resolve to a real note (by vault-relative path or basename)
128
- // and drop placeholder paths (a truncated `…` from a summary line). `pick` collects extra so
129
- // caps still fill after filtering.
130
- const known = new Set();
131
- for (const r of data) {
132
- const rel = String(r.file || '').replace(/\.md$/i, '');
133
- if (rel) { known.add(rel); known.add(basename(rel)); }
134
- }
135
- const resolves = (p) => {
136
- const t = String(p || '').replace(/\.md$/i, '').trim();
137
- if (!t || t.includes('...') || t.includes('…')) return false;
138
- return known.has(t) || known.has(basename(t)) || existsSync(join(vaultBase, `${t}.md`));
139
- };
140
- const pickLive = (kind, max) => pick(kind, max * 4).filter(resolves).slice(0, max);
141
-
142
- const decisions = pickLive('decisions', DIGEST_CAPS.decisions).sort((a, b) => adrNumber(b) - adrNumber(a));
143
- const sessions = byDateDesc.slice(0, DIGEST_CAPS.sessions);
144
- const bugs = pickLive('bugs', DIGEST_CAPS.bugs);
145
- const learnings = pickLive('learnings', DIGEST_CAPS.learnings);
146
-
147
- const lines = ['<!-- AUTO-GERADO por brain-core.mjs (0 token LLM). NÃO editar. Rebuild: node .agent/hooks/brain-reindex.mjs -->'];
148
- for (const d of decisions) lines.push(`- Decisão: [[${d}]]`);
149
- for (const s of sessions) lines.push(`- Sessão ${s.date} (${s.provider || '?'}): ${s.summary || s.file} → [[${String(s.file || '').replace(/\.md$/, '')}]]`);
150
- for (const b of bugs) lines.push(`- Bug: [[${b}]]`);
151
- for (const l of learnings) lines.push(`- Aprendizado: [[${l}]]`);
152
-
153
- const shown = sessions.length;
154
- if (data.length > shown) lines.push(`- +${data.length - shown} mais no índice — use /brain-recall <tópico>`);
155
-
156
- ensureDir(brainDir(vaultBase));
157
- writeFileSync(join(brainDir(vaultBase), 'DIGEST.md'), lines.join('\n') + '\n', 'utf8');
158
- return lines;
159
- }
1
+ // .agent/hooks/brain-core.mjs
2
+ // Camada fria do brain: indexa o frontmatter das notas de sessão (0 token LLM).
3
+ import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import { basename, join } from 'node:path';
5
+ import { ensureDir, stripYamlQuotes, toVaultRelative } from './obsidian-common.mjs';
6
+ import { getLocale } from './locale.mjs';
7
+
8
+ export function brainDir(vaultBase) {
9
+ return join(vaultBase, '.brain');
10
+ }
11
+
12
+ // Frontmatter YAML simples: escalares `k: v` + listas `k:` seguido de ` - item`.
13
+ export function parseFrontmatter(content) {
14
+ const m = content.match(/^---\n([\s\S]*?)\n---/);
15
+ if (!m) return {};
16
+ const data = {};
17
+ const lines = m[1].split('\n');
18
+ for (let i = 0; i < lines.length; i++) {
19
+ const kv = lines[i].match(/^([\w-]+):\s*(.*)$/);
20
+ if (!kv) continue;
21
+ const key = kv[1];
22
+ const val = kv[2];
23
+ if (val === '') {
24
+ const list = [];
25
+ while (i + 1 < lines.length && /^\s+-\s+/.test(lines[i + 1])) {
26
+ list.push(stripYamlQuotes(lines[++i].replace(/^\s+-\s+/, '').trim()));
27
+ }
28
+ data[key] = list.length ? list : '';
29
+ } else {
30
+ data[key] = stripYamlQuotes(val.trim());
31
+ }
32
+ }
33
+ return data;
34
+ }
35
+
36
+ function walkMd(dir) {
37
+ const out = [];
38
+ let entries;
39
+ try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return out; }
40
+ for (const e of entries) {
41
+ const fp = join(dir, e.name);
42
+ if (e.isDirectory()) out.push(...walkMd(fp));
43
+ else if (e.name.endsWith('.md')) out.push(fp);
44
+ }
45
+ return out;
46
+ }
47
+
48
+ const DERIVED_RE = /\[\[(0[456]-[^\]|]+?)(?:\|[^\]]*)?\]\]/g;
49
+ function derivedLinks(content) {
50
+ const dec = new Set(), bug = new Set(), lea = new Set();
51
+ let m;
52
+ while ((m = DERIVED_RE.exec(content))) {
53
+ const t = m[1];
54
+ if (t.startsWith('04-')) dec.add(t);
55
+ else if (t.startsWith('05-')) bug.add(t);
56
+ else if (t.startsWith('06-')) lea.add(t);
57
+ }
58
+ return { decisions: [...dec], bugs: [...bug], learnings: [...lea] };
59
+ }
60
+
61
+ // Varre 02-Sessões/** e regrava .brain/index.jsonl inteiro. Provider-agnóstico.
62
+ export function buildBrainIndex(vaultBase) {
63
+ const rows = [];
64
+ for (const fp of walkMd(join(vaultBase, getLocale(vaultBase).folders.sessions))) {
65
+ let content;
66
+ try { content = readFileSync(fp, 'utf8'); } catch { continue; }
67
+ const fm = parseFrontmatter(content);
68
+ if (fm.type && fm.type !== 'session') continue;
69
+ const der = derivedLinks(content);
70
+ rows.push({
71
+ session_id: fm.session_id || '',
72
+ date: fm.date || '',
73
+ provider: fm.provider || '',
74
+ status: fm.status || '',
75
+ summary: fm.summary || '',
76
+ file: toVaultRelative(vaultBase, fp),
77
+ tags: Array.isArray(fm.tags) ? fm.tags : (fm.tags ? [fm.tags] : []),
78
+ decisions: der.decisions,
79
+ bugs: der.bugs,
80
+ learnings: der.learnings,
81
+ });
82
+ }
83
+ rows.sort((a, b) => (a.date + a.file).localeCompare(b.date + b.file));
84
+ ensureDir(brainDir(vaultBase));
85
+ const out = rows.map((r) => JSON.stringify(r)).join('\n') + (rows.length ? '\n' : '');
86
+ writeFileSync(join(brainDir(vaultBase), 'index.jsonl'), out, 'utf8');
87
+ return rows;
88
+ }
89
+
90
+ // Lê o índice gravado (linhas JSONL). Usado pelo recall e pelo digest.
91
+ export function loadIndex(vaultBase) {
92
+ try {
93
+ return readFileSync(join(brainDir(vaultBase), 'index.jsonl'), 'utf8')
94
+ .split('\n').filter(Boolean).map((l) => JSON.parse(l));
95
+ } catch {
96
+ return [];
97
+ }
98
+ }
99
+
100
+ const DIGEST_CAPS = { decisions: 5, sessions: 4, bugs: 2, learnings: 2 };
101
+
102
+ function adrNumber(path) {
103
+ const m = path.match(/ADR-(\d+)/);
104
+ return m ? Number(m[1]) : -1;
105
+ }
106
+
107
+ // Destila index.jsonl em .brain/DIGEST.md (camada quente, determinístico, 0 token LLM).
108
+ // Cap por construção: 1 header + 13 itens (5/4/2/2) + 1 pointer = máx 15 linhas.
109
+ export function buildBrainDigest(vaultBase, rows = null) {
110
+ const data = rows ?? loadIndex(vaultBase);
111
+ const byDateDesc = [...data].sort((a, b) =>
112
+ String(b.date || '').localeCompare(String(a.date || '')) || String(b.file || '').localeCompare(String(a.file || '')));
113
+
114
+ const seen = new Set();
115
+ const pick = (kind, max) => {
116
+ const out = [];
117
+ for (const r of byDateDesc) {
118
+ for (const p of r[kind] || []) {
119
+ if (out.length >= max) return out;
120
+ if (!seen.has(p)) { seen.add(p); out.push(p); }
121
+ }
122
+ }
123
+ return out;
124
+ };
125
+
126
+ // The digest is INJECTED into every session, so a dead wikilink is dead weight in the model's
127
+ // context. Keep only targets that resolve to a real note (by vault-relative path or basename)
128
+ // and drop placeholder paths (a truncated `…` from a summary line). `pick` collects extra so
129
+ // caps still fill after filtering.
130
+ const known = new Set();
131
+ for (const r of data) {
132
+ const rel = String(r.file || '').replace(/\.md$/i, '');
133
+ if (rel) { known.add(rel); known.add(basename(rel)); }
134
+ }
135
+ const resolves = (p) => {
136
+ const t = String(p || '').replace(/\.md$/i, '').trim();
137
+ if (!t || t.includes('...') || t.includes('…')) return false;
138
+ return known.has(t) || known.has(basename(t)) || existsSync(join(vaultBase, `${t}.md`));
139
+ };
140
+ const pickLive = (kind, max) => pick(kind, max * 4).filter(resolves).slice(0, max);
141
+
142
+ const decisions = pickLive('decisions', DIGEST_CAPS.decisions).sort((a, b) => adrNumber(b) - adrNumber(a));
143
+ const sessions = byDateDesc.slice(0, DIGEST_CAPS.sessions);
144
+ const bugs = pickLive('bugs', DIGEST_CAPS.bugs);
145
+ const learnings = pickLive('learnings', DIGEST_CAPS.learnings);
146
+
147
+ const lines = ['<!-- AUTO-GERADO por brain-core.mjs (0 token LLM). NÃO editar. Rebuild: node .agent/hooks/brain-reindex.mjs -->'];
148
+ for (const d of decisions) lines.push(`- Decisão: [[${d}]]`);
149
+ for (const s of sessions) lines.push(`- Sessão ${s.date} (${s.provider || '?'}): ${s.summary || s.file} → [[${String(s.file || '').replace(/\.md$/, '')}]]`);
150
+ for (const b of bugs) lines.push(`- Bug: [[${b}]]`);
151
+ for (const l of learnings) lines.push(`- Aprendizado: [[${l}]]`);
152
+
153
+ const shown = sessions.length;
154
+ if (data.length > shown) lines.push(`- +${data.length - shown} mais no índice — use /brain-recall <tópico>`);
155
+
156
+ ensureDir(brainDir(vaultBase));
157
+ writeFileSync(join(brainDir(vaultBase), 'DIGEST.md'), lines.join('\n') + '\n', 'utf8');
158
+ return lines;
159
+ }
@@ -1,32 +1,32 @@
1
- // .agent/hooks/brain-recall.mjs
2
- // Query engine read-only: pontua o índice por tópico. Token só no resultado.
3
- // Uso: node .agent/hooks/brain-recall.mjs <termos da busca>
4
- import { pathToFileURL } from 'node:url';
5
- import { getVaultBase } from './obsidian-common.mjs';
6
- import { loadIndex } from './brain-core.mjs';
7
-
8
- export { loadIndex };
9
-
10
- export function scoreRows(rows, query, topK = 5) {
11
- const terms = String(query).toLowerCase().split(/\s+/).filter(Boolean);
12
- if (!terms.length) return [];
13
- return rows
14
- .map((r) => {
15
- // Inclui slug do file + paths das derivadas (ADR/bug/aprendizado) no haystack:
16
- // os títulos das sessões e tags são genéricos; o sinal tópico vem dos slugs.
17
- const hay = `${r.summary || ''} ${(r.tags || []).join(' ')} ${r.file || ''} ${(r.decisions || []).join(' ')} ${(r.bugs || []).join(' ')} ${(r.learnings || []).join(' ')}`.toLowerCase();
18
- let score = 0;
19
- for (const t of terms) if (hay.includes(t)) score++;
20
- return { row: r, score };
21
- })
22
- .filter((s) => s.score > 0)
23
- .sort((a, b) => b.score - a.score || String(b.row.date || '').localeCompare(String(a.row.date || '')))
24
- .slice(0, topK)
25
- .map((s) => s.row);
26
- }
27
-
28
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
29
- const vaultBase = getVaultBase();
30
- const hits = scoreRows(loadIndex(vaultBase), process.argv.slice(2).join(' '));
31
- process.stdout.write(JSON.stringify(hits, null, 2) + '\n');
32
- }
1
+ // .agent/hooks/brain-recall.mjs
2
+ // Query engine read-only: pontua o índice por tópico. Token só no resultado.
3
+ // Uso: node .agent/hooks/brain-recall.mjs <termos da busca>
4
+ import { pathToFileURL } from 'node:url';
5
+ import { getVaultBase } from './obsidian-common.mjs';
6
+ import { loadIndex } from './brain-core.mjs';
7
+
8
+ export { loadIndex };
9
+
10
+ export function scoreRows(rows, query, topK = 5) {
11
+ const terms = String(query).toLowerCase().split(/\s+/).filter(Boolean);
12
+ if (!terms.length) return [];
13
+ return rows
14
+ .map((r) => {
15
+ // Inclui slug do file + paths das derivadas (ADR/bug/aprendizado) no haystack:
16
+ // os títulos das sessões e tags são genéricos; o sinal tópico vem dos slugs.
17
+ const hay = `${r.summary || ''} ${(r.tags || []).join(' ')} ${r.file || ''} ${(r.decisions || []).join(' ')} ${(r.bugs || []).join(' ')} ${(r.learnings || []).join(' ')}`.toLowerCase();
18
+ let score = 0;
19
+ for (const t of terms) if (hay.includes(t)) score++;
20
+ return { row: r, score };
21
+ })
22
+ .filter((s) => s.score > 0)
23
+ .sort((a, b) => b.score - a.score || String(b.row.date || '').localeCompare(String(a.row.date || '')))
24
+ .slice(0, topK)
25
+ .map((s) => s.row);
26
+ }
27
+
28
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
29
+ const vaultBase = getVaultBase();
30
+ const hits = scoreRows(loadIndex(vaultBase), process.argv.slice(2).join(' '));
31
+ process.stdout.write(JSON.stringify(hits, null, 2) + '\n');
32
+ }
@@ -1,13 +1,13 @@
1
- // .agent/hooks/brain-reindex.mjs
2
- // Backfill manual: reconstrói .brain/index.jsonl + .brain/DIGEST.md varrendo todo 02-Sessões.
3
- // Uso: node .agent/hooks/brain-reindex.mjs [caminho-do-vault]
4
- import { pathToFileURL } from 'node:url';
5
- import { getVaultBase } from './obsidian-common.mjs';
6
- import { buildBrainDigest, buildBrainIndex, brainDir } from './brain-core.mjs';
7
-
8
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
9
- const vaultBase = getVaultBase({ obsidian_vault_path: process.argv[2] });
10
- const rows = buildBrainIndex(vaultBase);
11
- const digest = buildBrainDigest(vaultBase, rows);
12
- process.stdout.write(`[brain] index: ${rows.length} sessões; digest: ${digest.length} linhas → ${brainDir(vaultBase)}\n`);
13
- }
1
+ // .agent/hooks/brain-reindex.mjs
2
+ // Backfill manual: reconstrói .brain/index.jsonl + .brain/DIGEST.md varrendo todo 02-Sessões.
3
+ // Uso: node .agent/hooks/brain-reindex.mjs [caminho-do-vault]
4
+ import { pathToFileURL } from 'node:url';
5
+ import { getVaultBase } from './obsidian-common.mjs';
6
+ import { buildBrainDigest, buildBrainIndex, brainDir } from './brain-core.mjs';
7
+
8
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
9
+ const vaultBase = getVaultBase({ obsidian_vault_path: process.argv[2] });
10
+ const rows = buildBrainIndex(vaultBase);
11
+ const digest = buildBrainDigest(vaultBase, rows);
12
+ process.stdout.write(`[brain] index: ${rows.length} sessões; digest: ${digest.length} linhas → ${brainDir(vaultBase)}\n`);
13
+ }