wendkeep 0.50.0 → 0.52.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 +40 -0
- package/bin/wendkeep.mjs +4 -0
- package/hooks/frontmatter-repair.mjs +135 -0
- package/hooks/harness-doctor.mjs +60 -1
- package/hooks/pricing.json +14 -0
- package/hooks/token-usage.mjs +5 -0
- package/package.json +1 -1
- package/src/doctor.mjs +4 -1
- package/src/note.mjs +16 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,46 @@ 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.52.0] — 2026-07-25
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **`claude-opus-5` e `claude-mythos-5` deixam de custar $0.** Modelo ausente de
|
|
12
|
+
`hooks/pricing.json` faz `priceForModel` devolver `null` e a parcela dele do custo virar
|
|
13
|
+
zero — sem erro, sem aviso, sem nada no `doctor`. Num vault real:
|
|
14
|
+
`claude-opus-5 $0.0000` no `wendkeep cost`, enquanto Opus 4.8 e Fable 5 somavam $462.
|
|
15
|
+
Adicionados com os preços de API: Opus 5 a $5 input / $0,50 cache read / $25 output
|
|
16
|
+
(mesmo tier do Opus 4.8) e Mythos 5 a $10 / $1 / $50 (mesmo tier do Fable 5), com os
|
|
17
|
+
aliases das variantes de id. Notas já fechadas com custo zerado se corrigem com
|
|
18
|
+
`wendkeep cost rebuild`.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- **`wendkeep doctor` surfaça modelos sem preço.** Nova seção `[preços]`: lista os modelos
|
|
23
|
+
que aparecem nas notas de sessão com uso registrado mas sem entrada na tabela, e aponta
|
|
24
|
+
`hooks/pricing.json`. Cada modelo citado é consultado direto em `priceForModel` — **não**
|
|
25
|
+
se infere pelo sintoma "custo zerado", que não funciona: na nota que motivou a correção,
|
|
26
|
+
`modelo: "claude-opus-4.8 + claude-fable-5 + claude-opus-5"` fecha com **$415** porque os
|
|
27
|
+
dois primeiros têm preço, e só a fatia do Opus 5 está zerada. Um detector por custo zero
|
|
28
|
+
passaria batido justamente no caso real. Capability `session-observability` (OBS-9, OBS-10).
|
|
29
|
+
|
|
30
|
+
## [0.51.0] — 2026-07-25
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- **`wendkeep note repair-frontmatter [--apply]` — conserta as notas de sessão empilhadas.**
|
|
35
|
+
A 0.50.0 fechou a causa (lock + escrita atômica) e ensinou o `doctor` a **apontar** as
|
|
36
|
+
notas danificadas, mas não havia comando para consertá-las — a única checagem do doctor
|
|
37
|
+
sem conserto ao lado. O reparo funde os blocos num só sem perder nada: as chaves-base vêm
|
|
38
|
+
do bloco original (o de baixo, o único que as tem) e os valores gerenciados do bloco mais
|
|
39
|
+
recente (o do topo, último prepend). O merge opera sobre o texto bruto de cada chave, sem
|
|
40
|
+
reserializar YAML, então listas aninhadas atravessam byte-a-byte. Dry-run por padrão como
|
|
41
|
+
o `note relink`; `--apply` grava pelo mesmo lock dos hooks. Antes de gravar valida que o
|
|
42
|
+
resultado tem um bloco só, que nenhuma chave sumiu e que o corpo sobreviveu — falhando
|
|
43
|
+
qualquer uma, pula e reporta. O `doctor` agora imprime
|
|
44
|
+
`→ wendkeep note repair-frontmatter --apply` quando a contagem é > 0. Capability
|
|
45
|
+
`session-observability` (OBS-7, OBS-8) e `vault-doctor` (DIAG-5).
|
|
46
|
+
|
|
7
47
|
## [0.50.0] — 2026-07-23
|
|
8
48
|
|
|
9
49
|
### Fixed
|
package/bin/wendkeep.mjs
CHANGED
|
@@ -80,6 +80,10 @@ Usage:
|
|
|
80
80
|
wendkeep note relink [--apply] Backfill orphan derived notes (BUG/APR without a source session),
|
|
81
81
|
linking each to the modal source session of its type/month cohort. Dry-run
|
|
82
82
|
by default; --apply writes; skips notes with no sibling to infer from.
|
|
83
|
+
wendkeep note repair-frontmatter [--apply] Merge stacked frontmatter blocks in session notes
|
|
84
|
+
(damage from pre-lock concurrent writes) into a single block: base keys
|
|
85
|
+
from the original block, values from the newest. Dry-run by default;
|
|
86
|
+
--apply writes under the same lock as the hooks · --json.
|
|
83
87
|
wendkeep lesson add "t" "l" Record a project-local lesson (injected at SessionStart).
|
|
84
88
|
wendkeep validate-memory [path] Validate .brain/CORE.md against the compaction
|
|
85
89
|
protocol (cap 25, 3 sections, no secrets/PII). Uses
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Reparo das notas de sessão que ficaram com frontmatter empilhado.
|
|
2
|
+
//
|
|
3
|
+
// O dano vem das versões anteriores à session-note-atomic-write: sem lock, um hook lia a
|
|
4
|
+
// nota já truncada por outro e prependava um frontmatter novo. Como o prepend entra pelo
|
|
5
|
+
// TOPO, o bloco de baixo é o original (o único com type/date/provider/source) e o de cima
|
|
6
|
+
// é a gravação mais recente. Ficar com um só perde metade da informação; a fusão é que é a
|
|
7
|
+
// resposta.
|
|
8
|
+
//
|
|
9
|
+
// A causa já está fechada — isto aqui limpa o que ficou para trás.
|
|
10
|
+
import { readFileSync } from 'node:fs';
|
|
11
|
+
import { relative } from 'node:path';
|
|
12
|
+
import { checkStackedFrontmatter } from './harness-doctor.mjs';
|
|
13
|
+
import { mutateSessionNote } from './session-note-io.mjs';
|
|
14
|
+
|
|
15
|
+
// Separa os blocos de frontmatter empilhados no topo do corpo da nota.
|
|
16
|
+
//
|
|
17
|
+
// A regra de "empilhado" é a MESMA de `checkStackedFrontmatter`: só conta como bloco o que
|
|
18
|
+
// reabre com `---` logo após o fechamento do anterior. Detector e reparador precisam
|
|
19
|
+
// concordar — se divergissem, o doctor acusaria uma nota que o reparo não conserta (ou
|
|
20
|
+
// pior, o reparo comeria corpo que o doctor considera são). Um `---` no meio do texto
|
|
21
|
+
// (regra horizontal, tabela) fica no corpo, onde deve ficar.
|
|
22
|
+
export function splitStackedFrontmatter(content) {
|
|
23
|
+
const blocks = [];
|
|
24
|
+
let rest = typeof content === 'string' ? content : '';
|
|
25
|
+
|
|
26
|
+
while (/^---\n/.test(rest)) {
|
|
27
|
+
const close = rest.indexOf('\n---', 4);
|
|
28
|
+
if (close < 0) break;
|
|
29
|
+
blocks.push(rest.slice(0, close + 4));
|
|
30
|
+
rest = rest.slice(close + 4).replace(/^[\r\n]+/, '');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return { blocks, body: rest };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Quebra o miolo de um frontmatter em entradas top-level, preservando as linhas literais.
|
|
37
|
+
// Uma linha `^chave:` abre a entrada; o que vier indentado/em branco pertence a ela. Nada é
|
|
38
|
+
// reinterpretado — listas YAML aninhadas atravessam byte-a-byte, sem reserializar aspas,
|
|
39
|
+
// recuo ou ordem (o que geraria um diff gigante numa nota de 228 KB).
|
|
40
|
+
function parseEntries(block) {
|
|
41
|
+
const inner = block.replace(/^---\n/, '').replace(/\n---$/, '');
|
|
42
|
+
const entries = new Map();
|
|
43
|
+
let current = null;
|
|
44
|
+
|
|
45
|
+
for (const line of inner.split('\n')) {
|
|
46
|
+
const root = line.match(/^([A-Za-z0-9_-]+):/);
|
|
47
|
+
if (root) {
|
|
48
|
+
current = root[1];
|
|
49
|
+
entries.set(current, [line]);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (current) entries.get(current).push(line);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Linha em branco no fim de uma entrada é layout, não valor.
|
|
56
|
+
for (const lines of entries.values()) {
|
|
57
|
+
while (lines.length > 1 && lines[lines.length - 1].trim() === '') lines.pop();
|
|
58
|
+
}
|
|
59
|
+
return entries;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Funde os blocos empilhados num só. Devolve `null` quando não há nada a fundir.
|
|
63
|
+
export function mergeStackedFrontmatter(content) {
|
|
64
|
+
const { blocks, body } = splitStackedFrontmatter(content);
|
|
65
|
+
if (blocks.length < 2) return null;
|
|
66
|
+
|
|
67
|
+
// A base é o bloco de BAIXO: é o original, o único com as chaves-base, e define a ordem
|
|
68
|
+
// das chaves. Depois aplica de baixo para cima, então o bloco do topo — a gravação mais
|
|
69
|
+
// recente — é o último a escrever e vence. Chave que só existe num bloco de cima entra no
|
|
70
|
+
// fim, nunca é descartada.
|
|
71
|
+
const merged = parseEntries(blocks[blocks.length - 1]);
|
|
72
|
+
for (let i = blocks.length - 2; i >= 0; i -= 1) {
|
|
73
|
+
for (const [key, lines] of parseEntries(blocks[i])) merged.set(key, lines);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return `---\n${[...merged.values()].flat().join('\n')}\n---\n\n${body}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Um reparo que perde dado é pior que o dano que ele conserta: só grava o que passar aqui.
|
|
80
|
+
function validateMerge(original, merged) {
|
|
81
|
+
const after = splitStackedFrontmatter(merged);
|
|
82
|
+
if (after.blocks.length !== 1) return 'resultado não ficou com um bloco só';
|
|
83
|
+
|
|
84
|
+
const kept = new Set(parseEntries(after.blocks[0]).keys());
|
|
85
|
+
const before = splitStackedFrontmatter(original);
|
|
86
|
+
for (const block of before.blocks) {
|
|
87
|
+
for (const key of parseEntries(block).keys()) {
|
|
88
|
+
if (!kept.has(key)) return `chave perdida no merge: ${key}`;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (before.body && !merged.endsWith(before.body)) return 'corpo da nota não sobreviveu ao merge';
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Varre as notas de sessão empilhadas e as funde. Dry-run por padrão.
|
|
96
|
+
export function repairStackedFrontmatter(vaultBase, { apply = false, lockTimeoutMs } = {}) {
|
|
97
|
+
const repaired = [];
|
|
98
|
+
const skipped = [];
|
|
99
|
+
|
|
100
|
+
for (const abs of checkStackedFrontmatter(vaultBase).notes) {
|
|
101
|
+
const rel = relative(vaultBase, abs).replaceAll('\\', '/');
|
|
102
|
+
let original;
|
|
103
|
+
try {
|
|
104
|
+
original = readFileSync(abs, 'utf-8');
|
|
105
|
+
} catch {
|
|
106
|
+
skipped.push({ file: rel, reason: 'leitura falhou' });
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const merged = mergeStackedFrontmatter(original);
|
|
111
|
+
if (merged === null) continue; // detector e merge concordam: nada a fundir
|
|
112
|
+
|
|
113
|
+
const problem = validateMerge(original, merged);
|
|
114
|
+
if (problem) {
|
|
115
|
+
skipped.push({ file: rel, reason: problem });
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const blocks = splitStackedFrontmatter(original).blocks.length;
|
|
120
|
+
if (!apply) {
|
|
121
|
+
repaired.push({ file: rel, blocks });
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Sob o mesmo lock dos hooks: reparar enquanto um subagente escreve seria repetir o bug.
|
|
126
|
+
const outcome = mutateSessionNote(abs, () => merged, lockTimeoutMs ? { timeoutMs: lockTimeoutMs } : {});
|
|
127
|
+
if (!outcome.written) {
|
|
128
|
+
skipped.push({ file: rel, reason: `gravação não ocorreu (${outcome.reason})` });
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
repaired.push({ file: rel, blocks });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { applied: apply, repaired, skipped };
|
|
135
|
+
}
|
package/hooks/harness-doctor.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { activeChange, parseTasks, backfillArtifactLinks } from './change-core.m
|
|
|
6
6
|
import { relinkDerivedNotes } from './linked-notes.mjs';
|
|
7
7
|
import { buildEffectiveRequirementPackage, checkSpecsState, evaluateVerdict, tasksHashOf, validateSpecImpact } from './spec-core.mjs';
|
|
8
8
|
import { getLocale } from './locale.mjs';
|
|
9
|
+
import { priceForModel } from './token-usage.mjs';
|
|
9
10
|
import { readControl } from './obsidian-common.mjs';
|
|
10
11
|
|
|
11
12
|
export function checkHarness(vaultBase, projectRoot) {
|
|
@@ -122,11 +123,69 @@ export function checkStackedFrontmatter(vaultBase) {
|
|
|
122
123
|
return { count: notes.length, notes };
|
|
123
124
|
}
|
|
124
125
|
|
|
126
|
+
// Um modelo fora de `pricing.json` faz `priceForModel` devolver null e a parcela dele do custo
|
|
127
|
+
// virar zero — sem erro, sem aviso. Modelo novo (claude-opus-5, claude-mythos-5) cai nisso por
|
|
128
|
+
// default. A checagem é sobre o vault, não sobre o caminho de cálculo: o cálculo roda em hook a
|
|
129
|
+
// cada turno, onde avisar viraria ruído e lançar derrubaria a captura da sessão.
|
|
130
|
+
//
|
|
131
|
+
// Cada modelo citado na nota é consultado direto em `priceForModel` — NÃO se infere pelo
|
|
132
|
+
// sintoma "custo zerado". Numa sessão multi-modelo (`modelo: "claude-opus-4.8 + claude-opus-5"`)
|
|
133
|
+
// os modelos precificados mantêm o total acima de zero e escondem o que falta: no vault que
|
|
134
|
+
// motivou esta change, a nota fecha com $415 e a fatia do Opus 5 é a única zerada.
|
|
135
|
+
export function checkUnpricedModels(vaultBase) {
|
|
136
|
+
const counts = new Map();
|
|
137
|
+
|
|
138
|
+
const modelsOf = (frontmatter) => {
|
|
139
|
+
// `modelos:` é a lista canônica; `modelo:` é o rótulo agregado (junta com " + ").
|
|
140
|
+
const list = frontmatter.match(/^modelos:\n((?:\s+- .*\n?)+)/m);
|
|
141
|
+
if (list) return list[1].split('\n').map((l) => l.replace(/^\s*-\s*/, '')).filter(Boolean);
|
|
142
|
+
const label = (frontmatter.match(/^modelo:\s*(.+)$/m) || [])[1] || '';
|
|
143
|
+
return label.split('+');
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const walk = (dir) => {
|
|
147
|
+
let entries = [];
|
|
148
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
149
|
+
for (const entry of entries) {
|
|
150
|
+
const abs = join(dir, entry.name);
|
|
151
|
+
if (entry.isDirectory()) { walk(abs); continue; }
|
|
152
|
+
if (!entry.name.endsWith('.md')) continue;
|
|
153
|
+
let content;
|
|
154
|
+
try { content = readFileSync(abs, 'utf-8'); } catch { continue; }
|
|
155
|
+
const fm = content.match(/^---\n([\s\S]*?)\n---/);
|
|
156
|
+
if (!fm) continue;
|
|
157
|
+
// Sessão sem uso registrado não é sintoma de nada — custo zero ali é correto.
|
|
158
|
+
if (!(Number((fm[1].match(/^tokens_total:\s*(.+)$/m) || [])[1]) > 0)) continue;
|
|
159
|
+
for (const raw of modelsOf(fm[1])) {
|
|
160
|
+
const model = raw.trim().replace(/^["']|["']$/g, '');
|
|
161
|
+
if (!model || model === 'unknown') continue;
|
|
162
|
+
if (priceForModel(model)) continue;
|
|
163
|
+
counts.set(model, (counts.get(model) || 0) + 1);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
walk(join(vaultBase, '02-Sessões'));
|
|
169
|
+
return { models: [...counts].map(([model, notes]) => ({ model, notes })) };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function renderUnpricedModelLines(unpriced) {
|
|
173
|
+
const lines = [`[preços] ${unpriced.models.length} modelo(s) sem preço na tabela`];
|
|
174
|
+
for (const { model, notes } of unpriced.models) {
|
|
175
|
+
lines.push(` ✗ ${model} (${notes} nota(s) com custo zerado)`);
|
|
176
|
+
}
|
|
177
|
+
if (unpriced.models.length) lines.push(' → adicione o modelo em hooks/pricing.json');
|
|
178
|
+
else lines.push(' tabela de preços completa ✓');
|
|
179
|
+
return lines;
|
|
180
|
+
}
|
|
181
|
+
|
|
125
182
|
// Formatador puro pra que a saída do doctor seja testável sem process.exit.
|
|
126
183
|
export function renderStackedFrontmatterLines(vaultBase, stacked) {
|
|
127
184
|
const lines = [`[notas] ${stacked.count} sessão(ões) com frontmatter empilhado`];
|
|
128
185
|
for (const abs of stacked.notes) lines.push(` ✗ ${relative(vaultBase, abs)}`);
|
|
129
|
-
|
|
186
|
+
// Como as demais checagens do doctor: nunca apontar um problema sem oferecer o conserto.
|
|
187
|
+
if (stacked.count) lines.push(' → wendkeep note repair-frontmatter --apply');
|
|
188
|
+
else lines.push(' frontmatter íntegro ✓');
|
|
130
189
|
return lines;
|
|
131
190
|
}
|
|
132
191
|
|
package/hooks/pricing.json
CHANGED
|
@@ -26,6 +26,13 @@
|
|
|
26
26
|
"cachedInput": 0.5,
|
|
27
27
|
"output": 25
|
|
28
28
|
},
|
|
29
|
+
"claude-opus-5": {
|
|
30
|
+
"label": "Claude Opus 5 API",
|
|
31
|
+
"provider": "anthropic",
|
|
32
|
+
"input": 5,
|
|
33
|
+
"cachedInput": 0.5,
|
|
34
|
+
"output": 25
|
|
35
|
+
},
|
|
29
36
|
"claude-sonnet-4.6": {
|
|
30
37
|
"label": "Claude Sonnet 4.6 API",
|
|
31
38
|
"provider": "anthropic",
|
|
@@ -53,6 +60,13 @@
|
|
|
53
60
|
"input": 10,
|
|
54
61
|
"cachedInput": 1,
|
|
55
62
|
"output": 50
|
|
63
|
+
},
|
|
64
|
+
"claude-mythos-5": {
|
|
65
|
+
"label": "Claude Mythos 5 API",
|
|
66
|
+
"provider": "anthropic",
|
|
67
|
+
"input": 10,
|
|
68
|
+
"cachedInput": 1,
|
|
69
|
+
"output": 50
|
|
56
70
|
}
|
|
57
71
|
}
|
|
58
72
|
}
|
package/hooks/token-usage.mjs
CHANGED
|
@@ -117,6 +117,9 @@ const MODEL_ALIASES = {
|
|
|
117
117
|
'claude-opus-4-8': 'claude-opus-4.8',
|
|
118
118
|
'anthropic/claude-opus-4.8': 'claude-opus-4.8',
|
|
119
119
|
'anthropic/claude-opus-4-8': 'claude-opus-4.8',
|
|
120
|
+
'claude-opus-5': 'claude-opus-5',
|
|
121
|
+
'claude-opus-5-0': 'claude-opus-5',
|
|
122
|
+
'anthropic/claude-opus-5': 'claude-opus-5',
|
|
120
123
|
'claude-sonnet-4.6': 'claude-sonnet-4.6',
|
|
121
124
|
'claude-sonnet-4-6': 'claude-sonnet-4.6',
|
|
122
125
|
'anthropic/claude-sonnet-4.6': 'claude-sonnet-4.6',
|
|
@@ -132,6 +135,8 @@ const MODEL_ALIASES = {
|
|
|
132
135
|
'claude-fable-5': 'claude-fable-5',
|
|
133
136
|
'claude-fable-5[1m]': 'claude-fable-5',
|
|
134
137
|
'anthropic/claude-fable-5': 'claude-fable-5',
|
|
138
|
+
'claude-mythos-5': 'claude-mythos-5',
|
|
139
|
+
'anthropic/claude-mythos-5': 'claude-mythos-5',
|
|
135
140
|
};
|
|
136
141
|
|
|
137
142
|
const MANAGED_FRONTMATTER_KEYS = new Set([
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.52.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/doctor.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
4
4
|
import { existsSync } from 'node:fs';
|
|
5
5
|
import { dirname, join, resolve } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
-
import { checkHarness, checkVaultLinks, checkSessionActivity, checkStackedFrontmatter, renderStackedFrontmatterLines } from '../hooks/harness-doctor.mjs';
|
|
7
|
+
import { checkHarness, checkVaultLinks, checkSessionActivity, checkStackedFrontmatter, renderStackedFrontmatterLines, checkUnpricedModels, renderUnpricedModelLines } from '../hooks/harness-doctor.mjs';
|
|
8
8
|
import { checkSyncDefs } from './sync-defs.mjs';
|
|
9
9
|
import { resolveProjectVault } from './project-vault.mjs';
|
|
10
10
|
|
|
@@ -73,6 +73,9 @@ export function runDoctor(argv) {
|
|
|
73
73
|
const stacked = checkStackedFrontmatter(vaultBase);
|
|
74
74
|
process.stdout.write(`\n${renderStackedFrontmatterLines(vaultBase, stacked).join('\n')}\n`);
|
|
75
75
|
|
|
76
|
+
// 3c. Modelo fora de pricing.json fecha a sessão com custo zero, sem erro — só aparece aqui.
|
|
77
|
+
process.stdout.write(`\n${renderUnpricedModelLines(checkUnpricedModels(vaultBase)).join('\n')}\n`);
|
|
78
|
+
|
|
76
79
|
// 4. Sessão: não mente "inativa" quando há atividade recente (workflow/subagente em background).
|
|
77
80
|
const act = checkSessionActivity(vaultBase);
|
|
78
81
|
if (act.lastSession) {
|
package/src/note.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from '../hooks/obsidian-common.mjs';
|
|
16
16
|
import { getLocale } from '../hooks/locale.mjs';
|
|
17
17
|
import { buildManualBugNote, buildManualLearningNote, relinkDerivedNotes } from '../hooks/linked-notes.mjs';
|
|
18
|
+
import { repairStackedFrontmatter } from '../hooks/frontmatter-repair.mjs';
|
|
18
19
|
|
|
19
20
|
const TYPES = {
|
|
20
21
|
bug: { folderKey: 'bugs', prefix: 'BUG', build: buildManualBugNote },
|
|
@@ -45,8 +46,22 @@ export function runNote(argv) {
|
|
|
45
46
|
process.exit(0);
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
if (sub === 'repair-frontmatter') {
|
|
50
|
+
const vaultRaw = opt(rest, '--vault') || process.env.OBSIDIAN_VAULT_PATH;
|
|
51
|
+
if (!vaultRaw) { process.stderr.write('wendkeep note repair-frontmatter: no vault (--vault or OBSIDIAN_VAULT_PATH).\n'); process.exit(2); }
|
|
52
|
+
const vaultBase = isAbsolute(vaultRaw) ? vaultRaw : resolve(process.cwd(), vaultRaw);
|
|
53
|
+
if (!existsSync(vaultBase)) { process.stderr.write(`wendkeep note repair-frontmatter: vault not found: ${vaultBase}\n`); process.exit(2); }
|
|
54
|
+
const r = repairStackedFrontmatter(vaultBase, { apply: rest.includes('--apply') });
|
|
55
|
+
if (rest.includes('--json')) { process.stdout.write(`${JSON.stringify(r, null, 2)}\n`); process.exit(0); }
|
|
56
|
+
process.stdout.write(`${r.repaired.length} nota(s) com frontmatter empilhado${r.applied ? ' reparada(s)' : ' seriam reparada(s)'}\n`);
|
|
57
|
+
for (const n of r.repaired) process.stdout.write(` ${n.file} (${n.blocks} blocos -> 1)\n`);
|
|
58
|
+
for (const s of r.skipped) process.stdout.write(` pulado: ${s.file} (${s.reason})\n`);
|
|
59
|
+
if (!r.applied && r.repaired.length) process.stdout.write('\ndry-run — nada escrito. Rode com --apply para fundir os blocos.\n');
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
|
|
48
63
|
if (sub !== 'new') {
|
|
49
|
-
process.stderr.write('wendkeep note: subcomando desconhecido (use `note new --type bug|learning "<título>"` ou `note
|
|
64
|
+
process.stderr.write('wendkeep note: subcomando desconhecido (use `note new --type bug|learning "<título>"`, `note relink [--apply]` ou `note repair-frontmatter [--apply]`).\n');
|
|
50
65
|
process.exit(2);
|
|
51
66
|
}
|
|
52
67
|
|