wendkeep 0.47.0 → 0.48.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 +13 -0
- package/README.md +2 -0
- package/bin/wendkeep.mjs +3 -0
- package/hooks/linked-notes.mjs +72 -1
- package/package.json +1 -1
- package/src/note.mjs +17 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ All notable changes to **wendkeep** are documented here. Format based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this project follows
|
|
5
5
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.48.0] — 2026-07-23
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`wendkeep note relink [--apply]` — backfill de proveniência das notas derivadas
|
|
12
|
+
órfãs.** BUG/APR legadas (criadas por versão antiga, sem `source:` de sessão) ficam ilhas
|
|
13
|
+
no grafo — num vault real: 13 de 15 BUG e 3 de 8 APR sem nenhum link de entrada nem de
|
|
14
|
+
saída. A origem não está registrada no órfão, mas os irmãos não-órfãos do mesmo tipo
|
|
15
|
+
carregam a sessão-fonte real. O comando liga cada órfão herdando a sessão **modal** (mais
|
|
16
|
+
comum) dos irmãos do mesmo tipo e mês, injetando `source:` + `related:` no frontmatter.
|
|
17
|
+
Dry-run por default; `--apply` escreve; idempotente; pula e reporta o órfão sem nenhum
|
|
18
|
+
irmão-fonte pra inferir (nunca chuta). Documentado em `derived-notes` (DRV-9).
|
|
19
|
+
|
|
7
20
|
## [0.47.0] — 2026-07-23
|
|
8
21
|
|
|
9
22
|
### Added
|
package/README.md
CHANGED
|
@@ -184,6 +184,8 @@ wendkeep note new --type learning "a regex without /g only ever returns the firs
|
|
|
184
184
|
|
|
185
185
|
It prints the created path, numbers from the current max (recursive scan), files it in the month folder for today (`--date YYYY-MM-DD` to override), and links the active session in `source:` so the graph stays connected. Agents get this rule injected at SessionStart — they call the command instead of guessing a filename.
|
|
186
186
|
|
|
187
|
+
**Reconnecting legacy notes.** Derived notes created by older versions carry no `source:` session and sit as islands in the graph. `wendkeep note relink` backfills them: each orphan inherits the modal source session of its type/month cohort (the session its non-orphan siblings already point to). Dry-run by default; `--apply` writes; notes with no sibling to infer from are skipped and reported.
|
|
188
|
+
|
|
187
189
|
**Migrating an existing vault.** Notes created before `0.41.0` have date-prefixed names (`2026-07-16-bug-<slug>.md`) and may sit in legacy `DIA N` subfolders. One command per tree renumbers them chronologically, moves them up into the month folder, and rewrites every wikilink across the vault:
|
|
188
190
|
|
|
189
191
|
```bash
|
package/bin/wendkeep.mjs
CHANGED
|
@@ -77,6 +77,9 @@ Usage:
|
|
|
77
77
|
wendkeep renumber-learnings Same for 06-Aprendizados/06-Learnings with APR-<NNNN>-<slug>.
|
|
78
78
|
wendkeep note new --type bug|learning "<título>" Create a numbered derived note (BUG-/APR-NNNN)
|
|
79
79
|
in the month folder and print its vault path. --date YYYY-MM-DD · --vault P.
|
|
80
|
+
wendkeep note relink [--apply] Backfill orphan derived notes (BUG/APR without a source session),
|
|
81
|
+
linking each to the modal source session of its type/month cohort. Dry-run
|
|
82
|
+
by default; --apply writes; skips notes with no sibling to infer from.
|
|
80
83
|
wendkeep lesson add "t" "l" Record a project-local lesson (injected at SessionStart).
|
|
81
84
|
wendkeep validate-memory [path] Validate .brain/CORE.md against the compaction
|
|
82
85
|
protocol (cap 25, 3 sections, no secrets/PII). Uses
|
package/hooks/linked-notes.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'fs';
|
|
3
|
-
import { basename, join } from 'path';
|
|
3
|
+
import { basename, join, relative } from 'path';
|
|
4
4
|
import {
|
|
5
5
|
monthFolderRelFromDateStr,
|
|
6
6
|
derivedContentKey,
|
|
@@ -63,6 +63,77 @@ function sessionYamlLinks(sessionRel) {
|
|
|
63
63
|
].join('\n');
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
// --- note relink: backfill de proveniência das notas derivadas órfãs (DRV-9) -----
|
|
67
|
+
// Nota derivada legada (BUG/APR criada por wendkeep antigo) nasce sem `source:` de sessão —
|
|
68
|
+
// ilha no grafo. A origem não está registrada nela, mas os irmãos NÃO-órfãos do mesmo tipo
|
|
69
|
+
// carregam a sessão-fonte real: o órfão herda a sessão MODAL (mais comum) do seu (tipo, mês).
|
|
70
|
+
|
|
71
|
+
// Extrai a sessão do primeiro `source: - [[...]]` do frontmatter (vazio se não houver).
|
|
72
|
+
function sourceSessionOf(content) {
|
|
73
|
+
const m = content.match(/^source:\s*\n\s*-\s*"?\[\[([^\]"|]+)/m);
|
|
74
|
+
return m ? m[1].trim() : '';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Injeta source+related antes do `---` de fechamento do frontmatter. Sem frontmatter, no-op.
|
|
78
|
+
function insertSourceLinks(content, sessionRel) {
|
|
79
|
+
const m = content.match(/^(---\n[\s\S]*?\n)(---\n)/);
|
|
80
|
+
if (!m) return content;
|
|
81
|
+
return `${m[1]}${sessionYamlLinks(sessionRel)}\n${m[2]}${content.slice(m[0].length)}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function modalKey(counts) {
|
|
85
|
+
const entries = Object.entries(counts || {});
|
|
86
|
+
if (!entries.length) return '';
|
|
87
|
+
entries.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
88
|
+
return entries[0][0];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function relinkDerivedNotes(vaultBase, { apply = false } = {}) {
|
|
92
|
+
const loc = getLocale(vaultBase);
|
|
93
|
+
const monthOf = (abs) => relative(vaultBase, abs).replaceAll('\\', '/').split('/').slice(0, 3).join('/');
|
|
94
|
+
const linked = [];
|
|
95
|
+
const skipped = [];
|
|
96
|
+
for (const [folderKey, prefix] of [['bugs', 'BUG'], ['learnings', 'APR']]) {
|
|
97
|
+
const root = join(vaultBase, loc.folders[folderKey]);
|
|
98
|
+
const files = [];
|
|
99
|
+
const walk = (dir) => {
|
|
100
|
+
let entries;
|
|
101
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
102
|
+
for (const e of entries) {
|
|
103
|
+
const p = join(dir, e.name);
|
|
104
|
+
if (e.isDirectory()) walk(p);
|
|
105
|
+
else if (e.name.endsWith('.md') && e.name.startsWith(`${prefix}-`)) files.push(p);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
walk(root);
|
|
109
|
+
const byMonth = {};
|
|
110
|
+
const typeWide = {};
|
|
111
|
+
const orphans = [];
|
|
112
|
+
for (const p of files) {
|
|
113
|
+
let c;
|
|
114
|
+
try { c = readFileSync(p, 'utf8'); } catch { continue; }
|
|
115
|
+
const src = sourceSessionOf(c);
|
|
116
|
+
if (src) {
|
|
117
|
+
(byMonth[monthOf(p)] ??= {})[src] = ((byMonth[monthOf(p)] || {})[src] || 0) + 1;
|
|
118
|
+
typeWide[src] = (typeWide[src] || 0) + 1;
|
|
119
|
+
} else {
|
|
120
|
+
orphans.push({ p, c });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
for (const o of orphans) {
|
|
124
|
+
const rel = relative(vaultBase, o.p).replaceAll('\\', '/');
|
|
125
|
+
const session = modalKey(byMonth[monthOf(o.p)]) || modalKey(typeWide);
|
|
126
|
+
if (!session) { skipped.push({ file: rel, reason: 'sem irmão com source para inferir' }); continue; }
|
|
127
|
+
if (apply) {
|
|
128
|
+
try { writeFileSync(o.p, insertSourceLinks(o.c, session), 'utf8'); }
|
|
129
|
+
catch { skipped.push({ file: rel, reason: 'escrita falhou' }); continue; }
|
|
130
|
+
}
|
|
131
|
+
linked.push({ file: rel, session: basename(session) });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return { applied: apply, linked, skipped };
|
|
135
|
+
}
|
|
136
|
+
|
|
66
137
|
function extractIssueRefs(tx) {
|
|
67
138
|
const text = [
|
|
68
139
|
assistantText(tx),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.48.0",
|
|
4
4
|
"description": "A persistent-memory harness for AI coding agents on your Obsidian vault: turn-by-turn session capture plus a native, zero-dependency spec→change→verify→archive loop (sensor-gated, independent verdict, mutation discrimination). Local-first, agent-agnostic (Claude Code, Codex, Cursor…).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/note.mjs
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
toVaultRelative,
|
|
15
15
|
} from '../hooks/obsidian-common.mjs';
|
|
16
16
|
import { getLocale } from '../hooks/locale.mjs';
|
|
17
|
-
import { buildManualBugNote, buildManualLearningNote } from '../hooks/linked-notes.mjs';
|
|
17
|
+
import { buildManualBugNote, buildManualLearningNote, relinkDerivedNotes } from '../hooks/linked-notes.mjs';
|
|
18
18
|
|
|
19
19
|
const TYPES = {
|
|
20
20
|
bug: { folderKey: 'bugs', prefix: 'BUG', build: buildManualBugNote },
|
|
@@ -30,8 +30,23 @@ function opt(argv, name) {
|
|
|
30
30
|
|
|
31
31
|
export function runNote(argv) {
|
|
32
32
|
const [sub, ...rest] = argv;
|
|
33
|
+
|
|
34
|
+
if (sub === 'relink') {
|
|
35
|
+
const vaultRaw = opt(rest, '--vault') || process.env.OBSIDIAN_VAULT_PATH;
|
|
36
|
+
if (!vaultRaw) { process.stderr.write('wendkeep note relink: no vault (--vault or OBSIDIAN_VAULT_PATH).\n'); process.exit(2); }
|
|
37
|
+
const vaultBase = isAbsolute(vaultRaw) ? vaultRaw : resolve(process.cwd(), vaultRaw);
|
|
38
|
+
if (!existsSync(vaultBase)) { process.stderr.write(`wendkeep note relink: vault not found: ${vaultBase}\n`); process.exit(2); }
|
|
39
|
+
const r = relinkDerivedNotes(vaultBase, { apply: rest.includes('--apply') });
|
|
40
|
+
if (rest.includes('--json')) { process.stdout.write(`${JSON.stringify(r, null, 2)}\n`); process.exit(0); }
|
|
41
|
+
process.stdout.write(`${r.linked.length} nota(s) derivada(s) órfã(s)${r.applied ? ' linkadas' : ' seriam linkadas'}\n`);
|
|
42
|
+
for (const l of r.linked) process.stdout.write(` ${l.file} -> ${l.session}\n`);
|
|
43
|
+
for (const s of r.skipped) process.stdout.write(` pulado: ${s.file} (${s.reason})\n`);
|
|
44
|
+
if (!r.applied && r.linked.length) process.stdout.write('\ndry-run — nada escrito. Rode com --apply para injetar os backlinks.\n');
|
|
45
|
+
process.exit(0);
|
|
46
|
+
}
|
|
47
|
+
|
|
33
48
|
if (sub !== 'new') {
|
|
34
|
-
process.stderr.write('wendkeep note: subcomando desconhecido (use `note new --type bug|learning "<título>"`).\n');
|
|
49
|
+
process.stderr.write('wendkeep note: subcomando desconhecido (use `note new --type bug|learning "<título>"` ou `note relink [--apply]`).\n');
|
|
35
50
|
process.exit(2);
|
|
36
51
|
}
|
|
37
52
|
|