wendkeep 0.22.0 → 0.24.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 +38 -0
- package/README.md +1 -1
- package/bin/wendkeep.mjs +7 -1
- package/hooks/change-core.mjs +4 -3
- package/hooks/linked-notes.mjs +29 -3
- package/hooks/obsidian-common.mjs +38 -7
- package/package.json +1 -1
- package/src/init.mjs +10 -3
- package/src/taxonomy.mjs +4 -2
- package/src/vault-views.mjs +99 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,44 @@ 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.24.0] — 2026-07-08
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- **No companion is pre-selected anymore.** `context-mode` was pre-checked (and the
|
|
11
|
+
non-interactive default); wendkeep is a neutral harness and should not presume a third-party
|
|
12
|
+
plugin. The interactive picker now starts with **nothing checked**, `init --yes` (and any
|
|
13
|
+
non-interactive run) installs **no** companions, and `resolveCompanions({})` returns `[]`.
|
|
14
|
+
Opt in explicitly — interactively (Space) or `--companions context-mode`. `src/taxonomy.mjs`.
|
|
15
|
+
- Prompt/help/README text updated to reflect the empty default.
|
|
16
|
+
|
|
17
|
+
## [0.23.0] — 2026-07-08
|
|
18
|
+
|
|
19
|
+
Vault structure — generated views + housekeeping (audit wave 2).
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- **Generated Bases + Dashboard MOC**: `wendkeep init` now writes one folder-filtered `.base`
|
|
23
|
+
per taxonomy area (sessions/decisions/bugs/learnings/specs/changes) and a `00-Dashboard.md`
|
|
24
|
+
that embeds them — the vault's structural index. Filters are **by folder**
|
|
25
|
+
(`file.inFolder("05-Bugs")`), fixing the tag-filter that hid ~1/3 of bugs. New
|
|
26
|
+
`wendkeep dashboard [--force]` (re)generates them; non-destructive (never clobbers your own
|
|
27
|
+
bases). Locale-aware. `src/vault-views.mjs`.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
- **Archive ADRs land in the dated month folder** (`04-Decisões/<year>/<MM-MMM>/`) alongside
|
|
31
|
+
session-derived decisions, instead of the year root. `hooks/change-core.mjs`.
|
|
32
|
+
- **`SESSION_REGISTRY` is pruned** on the idle sweep: `done` entries older than 90 days, then a
|
|
33
|
+
cap of 200 most-recent — active entries are never touched. Bounds the per-hook read/serialize
|
|
34
|
+
cost that had grown to 330 entries / ~170 KB in production. `hooks/obsidian-common.mjs`.
|
|
35
|
+
- **Generated note names truncate on a word boundary** instead of mid-word (`slugify` gained a
|
|
36
|
+
boundary-aware `maxLen`). `hooks/obsidian-common.mjs`, `hooks/linked-notes.mjs`.
|
|
37
|
+
- **Learnings dedup vault-wide**: a learning already recorded anywhere in `06-Aprendizados`
|
|
38
|
+
(by `content_key`) is not re-emitted on a later day/session. `hooks/linked-notes.mjs`.
|
|
39
|
+
|
|
40
|
+
### Deferred
|
|
41
|
+
- Unifying the two `buildSessionContent` skeletons (session-start / session-ensure) stays as
|
|
42
|
+
tracked tech-debt — pure refactor, high regression risk in the capture layer, and the
|
|
43
|
+
user-facing drift (`session_id`) was already closed in 0.18/0.21.
|
|
44
|
+
|
|
7
45
|
## [0.22.0] — 2026-07-08
|
|
8
46
|
|
|
9
47
|
Hardening — 10 audit-confirmed bugs fixed (each survived an adversarial refuter).
|
package/README.md
CHANGED
|
@@ -65,7 +65,7 @@ npx wendkeep init --no-companions --no-mcp --yes # zero companions,
|
|
|
65
65
|
| `--vault <path>` | Vault folder. Default `<project>/.<project-name>-vault`; interactive init asks. Point it at an existing vault to install into it. |
|
|
66
66
|
| `--project <path>` | Project root to wire (default: current directory). |
|
|
67
67
|
| `--locale <pt-BR\|en>` | Vault language — folder names, scaffold, skills. Interactive init asks; locked at init. |
|
|
68
|
-
| `--companions <csv>` | Companions to pin: `context-mode,caveman,understand-anything` (default:
|
|
68
|
+
| `--companions <csv>` | Companions to pin: `context-mode,caveman,understand-anything` (default: **none** — opt in explicitly; `dotcontext` is legacy). |
|
|
69
69
|
| `--no-companions` | Pin no companions. |
|
|
70
70
|
| `--no-mcp` | Skip **wendkeep's own** vault MCP (`wendkeep-vault`). Companion MCPs still follow `--companions`. |
|
|
71
71
|
| `--no-colors` | Skip the Obsidian color system (`.obsidian` snippet + graph groups). |
|
package/bin/wendkeep.mjs
CHANGED
|
@@ -30,7 +30,7 @@ Usage:
|
|
|
30
30
|
--project <path> Project root to wire (default: current directory).
|
|
31
31
|
--no-mcp Do not add the mcpvault MCP server to .mcp.json.
|
|
32
32
|
--companions <csv> Companion plugins/MCP to pin: context-mode,caveman,understand-anything
|
|
33
|
-
(default:
|
|
33
|
+
(default: none — opt in explicitly). dotcontext is legacy — the native a2 loop replaces it.
|
|
34
34
|
--no-companions Skip companion plugins/MCP entirely.
|
|
35
35
|
--no-colors Skip the Obsidian color system (.obsidian snippet + graph groups).
|
|
36
36
|
--dotcontext-mcp <v> dotcontext MCP placement: auto (default; skip project entry
|
|
@@ -56,6 +56,7 @@ Usage:
|
|
|
56
56
|
--dry-run · --json.
|
|
57
57
|
wendkeep verify [--deep] [--change s] Run a change's task sensors + record evidence (the gate);
|
|
58
58
|
--deep assembles the verification package for the wk-verify pass.
|
|
59
|
+
wendkeep dashboard [--force] (Re)generate the vault's folder-filtered Bases + 00-Dashboard MOC.
|
|
59
60
|
wendkeep lesson add "t" "l" Record a project-local lesson (injected at SessionStart).
|
|
60
61
|
wendkeep validate-memory [path] Validate .brain/CORE.md against the compaction
|
|
61
62
|
protocol (cap 25, 3 sections, no secrets/PII). Uses
|
|
@@ -148,6 +149,11 @@ async function main() {
|
|
|
148
149
|
runImportCli(rest);
|
|
149
150
|
break;
|
|
150
151
|
}
|
|
152
|
+
case 'dashboard': {
|
|
153
|
+
const { runDashboard } = await import('../src/vault-views.mjs');
|
|
154
|
+
runDashboard(rest);
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
151
157
|
case '--version':
|
|
152
158
|
case '-v':
|
|
153
159
|
process.stdout.write(`${version()}\n`);
|
package/hooks/change-core.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// the `wendkeep change` CLI (src/change.mjs) and the brain-inject hook. No external deps.
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { dirname, join } from 'node:path';
|
|
6
|
-
import { ensureDir, wikilinkFromRel } from './obsidian-common.mjs';
|
|
6
|
+
import { ensureDir, wikilinkFromRel, monthFolderRelFromDateStr } from './obsidian-common.mjs';
|
|
7
7
|
import { parseSpecsList, promoteSpecs } from './spec-core.mjs';
|
|
8
8
|
import { getLocale } from './locale.mjs';
|
|
9
9
|
|
|
@@ -261,8 +261,9 @@ export function archiveChange(vaultBase, slug, { gate = gateGreen, dateStr, adrN
|
|
|
261
261
|
writeFileSync(pp, c, 'utf8');
|
|
262
262
|
} catch { /* proposta ilegível — segue */ }
|
|
263
263
|
|
|
264
|
-
|
|
265
|
-
|
|
264
|
+
// ADR goes in the same dated month folder as session-derived decisions (04-Decisões/ano/MM-MMM/)
|
|
265
|
+
// — not the year root — so all ADRs sit together in the vault's convention.
|
|
266
|
+
const adrDirRel = monthFolderRelFromDateStr(loc.folders.decisions, dateStr, vaultBase);
|
|
266
267
|
ensureDir(join(vaultBase, adrDirRel));
|
|
267
268
|
const num = String(adrNum).padStart(3, '0');
|
|
268
269
|
const adrRel = join(adrDirRel, `ADR-${num}-${slug}.md`);
|
package/hooks/linked-notes.mjs
CHANGED
|
@@ -477,6 +477,30 @@ function listMd(dir) {
|
|
|
477
477
|
}
|
|
478
478
|
|
|
479
479
|
// Chaves content_key das derivadas já existentes que linkam esta sessão.
|
|
480
|
+
// Vault-wide learning content_keys (recursive over the learnings folder). existingKeysForSession
|
|
481
|
+
// only looks at the current session + month, so the same lesson re-extracted on a later day/
|
|
482
|
+
// session was duplicated. This dedups a learning against everything already learned in the vault.
|
|
483
|
+
function collectLearningKeys(vaultBase) {
|
|
484
|
+
const keys = new Set();
|
|
485
|
+
const root = join(vaultBase, getLocale(vaultBase).folders.learnings);
|
|
486
|
+
const walk = (d) => {
|
|
487
|
+
let entries;
|
|
488
|
+
try { entries = readdirSync(d, { withFileTypes: true }); } catch { return; }
|
|
489
|
+
for (const e of entries) {
|
|
490
|
+
const p = join(d, e.name);
|
|
491
|
+
if (e.isDirectory()) walk(p);
|
|
492
|
+
else if (e.name.endsWith('.md')) {
|
|
493
|
+
try {
|
|
494
|
+
const m = readFileSync(p, 'utf-8').match(/^content_key:\s*"?(.*?)"?\s*$/m);
|
|
495
|
+
if (m && m[1]) keys.add(m[1]);
|
|
496
|
+
} catch { /* nota ilegível */ }
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
walk(root);
|
|
501
|
+
return keys;
|
|
502
|
+
}
|
|
503
|
+
|
|
480
504
|
function existingKeysForSession(vaultBase, sessionRel, dateStr) {
|
|
481
505
|
const wikilink = wikilinkFromRel(sessionRel);
|
|
482
506
|
const out = { bugs: [], decisions: [], learnings: [] };
|
|
@@ -518,7 +542,7 @@ export function createLinkedNotes(vaultBase, dateStr, sessionRel, tx, options =
|
|
|
518
542
|
const issueRef = issueRefs[0] || '';
|
|
519
543
|
const bugKey = derivedContentKey(bugDetails.rootCause);
|
|
520
544
|
if (!alreadyHasKey(existingKeys.bugs, bugKey)) {
|
|
521
|
-
const causeSlug = slugify(bugDetails.rootCause
|
|
545
|
+
const causeSlug = slugify(bugDetails.rootCause, 'bug', 40);
|
|
522
546
|
const fileName = issueRef ? `${issueRef}-${causeSlug}.md` : `${dateStr}-bug-${causeSlug}.md`;
|
|
523
547
|
const filePath = join(bugsDir, fileName);
|
|
524
548
|
if (!existsSync(filePath)) writeFileSync(filePath, buildBugNoteContent(bugDetails, issueRef, dateStr, sessionRel, provider, bugKey, loc.id), 'utf-8');
|
|
@@ -531,7 +555,7 @@ export function createLinkedNotes(vaultBase, dateStr, sessionRel, tx, options =
|
|
|
531
555
|
if (decisionDetails) {
|
|
532
556
|
const decisionKey = derivedContentKey(decisionDetails.title);
|
|
533
557
|
if (!alreadyHasKey(existingKeys.decisions, decisionKey)) {
|
|
534
|
-
const titleSlug = slugify(decisionDetails.title
|
|
558
|
+
const titleSlug = slugify(decisionDetails.title, 'decisao', 40);
|
|
535
559
|
const existing = adrFileExistsBySlug(decisionsDir, titleSlug);
|
|
536
560
|
const fileName = existing || `ADR-${String(getNextAdrNumber(vaultBase)).padStart(3, '0')}-${titleSlug}.md`;
|
|
537
561
|
const filePath = join(decisionsDir, fileName);
|
|
@@ -546,10 +570,12 @@ export function createLinkedNotes(vaultBase, dateStr, sessionRel, tx, options =
|
|
|
546
570
|
|
|
547
571
|
const learnings = extractLearningDetails(tx, bugDetails);
|
|
548
572
|
if (learnings) {
|
|
573
|
+
const vaultLearningKeys = collectLearningKeys(vaultBase); // vault-wide dedup
|
|
549
574
|
for (const learning of learnings) {
|
|
550
575
|
const learningKey = derivedContentKey(learning.title);
|
|
551
576
|
if (alreadyHasKey(existingKeys.learnings, learningKey)) continue;
|
|
552
|
-
|
|
577
|
+
if (vaultLearningKeys.has(learningKey)) continue; // already learned elsewhere in the vault
|
|
578
|
+
const learningSlug = slugify(learning.title, 'aprendizado', 40);
|
|
553
579
|
const fileName = `${dateStr}-${learningSlug}.md`;
|
|
554
580
|
const filePath = join(learningsDir, fileName);
|
|
555
581
|
if (!existsSync(filePath)) writeFileSync(filePath, buildLearningNoteContent(learning, dateStr, sessionRel, provider, learningKey, loc.id), 'utf-8');
|
|
@@ -289,12 +289,38 @@ export function sweepStaleSessions(registry, nowMs, maxIdleMs, excludeTranscript
|
|
|
289
289
|
return closed;
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
-
//
|
|
292
|
+
// Registry retention. The registry is read/serialized in full on every hook and scanned O(N)
|
|
293
|
+
// for routing — it only needs active + recent sessions (historical audit lives in the notes).
|
|
294
|
+
// Left unbounded it grew to 330 entries / ~170 KB in production.
|
|
295
|
+
export const REGISTRY_KEEP_DONE = 200;
|
|
296
|
+
export const REGISTRY_DONE_MAX_AGE_MS = 90 * 24 * 60 * 60 * 1000;
|
|
297
|
+
|
|
298
|
+
// Pure: drop 'done' entries older than maxAgeMs, then cap the remaining 'done' at keepDone
|
|
299
|
+
// (newest by ended_at/updated_at/started_at kept). Never touches active entries. Mutates the
|
|
300
|
+
// registry and returns how many were pruned.
|
|
301
|
+
export function pruneRegistry(registry, nowMs, { keepDone = REGISTRY_KEEP_DONE, maxAgeMs = REGISTRY_DONE_MAX_AGE_MS } = {}) {
|
|
302
|
+
const sessions = registry?.sessions || {};
|
|
303
|
+
const stamp = (v) => Date.parse((v && (v.ended_at || v.updated_at || v.started_at)) || '') || 0;
|
|
304
|
+
let pruned = 0;
|
|
305
|
+
for (const [id, v] of Object.entries(sessions)) {
|
|
306
|
+
if (!v || v.status !== 'done') continue;
|
|
307
|
+
const t = stamp(v);
|
|
308
|
+
if (t && nowMs - t > maxAgeMs) { delete sessions[id]; pruned += 1; }
|
|
309
|
+
}
|
|
310
|
+
const done = Object.entries(sessions)
|
|
311
|
+
.filter(([, v]) => v && v.status === 'done')
|
|
312
|
+
.sort((a, b) => stamp(b[1]) - stamp(a[1]));
|
|
313
|
+
for (const [id] of done.slice(keepDone)) { delete sessions[id]; pruned += 1; }
|
|
314
|
+
return pruned;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Wrapper de IO: varre as ociosas, poda o registry, grava e fecha a NOTA `.md` de cada
|
|
293
318
|
// sessão encerrada (mantém vault e registry alinhados). Devolve quantas fechou.
|
|
294
319
|
export function sweepStaleSessionsFile(vaultBase, now = new Date(), maxIdleMs = SESSION_IDLE_CLOSE_MS, excludeTranscriptPath = '') {
|
|
295
320
|
const registry = readSessionRegistry(vaultBase);
|
|
296
321
|
const closed = sweepStaleSessions(registry, now.getTime(), maxIdleMs, excludeTranscriptPath);
|
|
297
|
-
|
|
322
|
+
const pruned = pruneRegistry(registry, now.getTime());
|
|
323
|
+
if (closed.length || pruned) writeSessionRegistry(vaultBase, registry);
|
|
298
324
|
for (const { session_file, ended_at } of closed) {
|
|
299
325
|
try { closeSessionNoteFile(vaultBase, session_file, ended_at); } catch { /* nunca derruba o sweep */ }
|
|
300
326
|
}
|
|
@@ -335,15 +361,20 @@ export function closeSessionNote(content, endedAt) {
|
|
|
335
361
|
return next;
|
|
336
362
|
}
|
|
337
363
|
|
|
338
|
-
export function slugify(text, fallback = 'nota') {
|
|
339
|
-
|
|
364
|
+
export function slugify(text, fallback = 'nota', maxLen = 60) {
|
|
365
|
+
let slug = String(text || '')
|
|
340
366
|
.normalize('NFD')
|
|
341
367
|
.replace(/[\u0300-\u036f]/g, '')
|
|
342
368
|
.toLowerCase()
|
|
343
369
|
.replace(/[^a-z0-9]+/g, '-')
|
|
344
|
-
.replace(/^-+|-+$/g, '')
|
|
345
|
-
|
|
346
|
-
|
|
370
|
+
.replace(/^-+|-+$/g, '');
|
|
371
|
+
if (slug.length > maxLen) {
|
|
372
|
+
// Truncate on a word boundary (last '-' before maxLen) when a reasonable one exists,
|
|
373
|
+
// instead of cutting mid-word \u2014 keeps generated note names readable.
|
|
374
|
+
const cut = slug.slice(0, maxLen);
|
|
375
|
+
const lastDash = cut.lastIndexOf('-');
|
|
376
|
+
slug = (lastDash > maxLen * 0.5 ? cut.slice(0, lastDash) : cut).replace(/-+$/g, '');
|
|
377
|
+
}
|
|
347
378
|
return slug || fallback;
|
|
348
379
|
}
|
|
349
380
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.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/init.mjs
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
DOTCONTEXT_GITIGNORE,
|
|
23
23
|
} from './taxonomy.mjs';
|
|
24
24
|
import { renderVaultReadme } from './vault-readme.mjs';
|
|
25
|
+
import { seedVaultViews } from './vault-views.mjs';
|
|
25
26
|
import { canInteractiveSelect, selectCompanionsInteractive } from './companion-select.mjs';
|
|
26
27
|
import {
|
|
27
28
|
SNIPPET_NAME,
|
|
@@ -206,13 +207,13 @@ function installVaultColors(vaultPath) {
|
|
|
206
207
|
const PROMPTS = {
|
|
207
208
|
'pt-BR': {
|
|
208
209
|
vault: (f) => `Caminho do vault Obsidian (Enter aceita o padrão, ou digite outro)\n [${f}]\n> `,
|
|
209
|
-
companionsHeader: '\nCompanions (plugins/MCP opcionais —
|
|
210
|
+
companionsHeader: '\nCompanions (plugins/MCP opcionais — nenhum vem pré-marcado):',
|
|
210
211
|
companionsAsk: (def) => `Digite os ids separados por vírgula (Enter aceita [${def}], "none" p/ nenhum): `,
|
|
211
212
|
menu: { hint: 'Espaço marca/desmarca · ↑/↓ move · a=todos · n=nenhum · Enter confirma', header: 'Companions' },
|
|
212
213
|
},
|
|
213
214
|
en: {
|
|
214
215
|
vault: (f) => `Obsidian vault path (Enter for the default, or type another)\n [${f}]\n> `,
|
|
215
|
-
companionsHeader: '\nCompanions (optional plugins/MCP —
|
|
216
|
+
companionsHeader: '\nCompanions (optional plugins/MCP — none pre-selected):',
|
|
216
217
|
companionsAsk: (def) => `Enter ids comma-separated (Enter for [${def}], "none" for none): `,
|
|
217
218
|
menu: { hint: 'Space toggles · ↑/↓ move · a=all · n=none · Enter confirms', header: 'Companions' },
|
|
218
219
|
},
|
|
@@ -364,7 +365,13 @@ export async function runInit(argv) {
|
|
|
364
365
|
try { scripts = JSON.parse(readFileSync(join(projectPath, 'package.json'), 'utf8')).scripts || {}; } catch { /* no package.json */ }
|
|
365
366
|
writeFileSync(sensorsFile, renderSensorsJson(scripts), 'utf8');
|
|
366
367
|
}
|
|
367
|
-
|
|
368
|
+
// Generated Bases + Dashboard MOC (folder-filtered views over the taxonomy) — non-destructive.
|
|
369
|
+
let viewsNote = '';
|
|
370
|
+
try {
|
|
371
|
+
const views = seedVaultViews(vaultPath);
|
|
372
|
+
if (views.length) viewsNote = `, ${views.length} view(s) + dashboard`;
|
|
373
|
+
} catch { /* views são bônus — nunca derrubam o init */ }
|
|
374
|
+
log(` [1/4] vault taxonomy: ${folders.length} folders (${created} created, locale ${loc.id})${readmeNote}, .brain + change/spec + sensors seeded${viewsNote}`);
|
|
368
375
|
// Deliver the seeded defs (agents + wk process skills) to the project so they're
|
|
369
376
|
// usable immediately — no separate `wendkeep sync-defs` step needed.
|
|
370
377
|
const synced = syncDefs(vaultPath, projectPath);
|
package/src/taxonomy.mjs
CHANGED
|
@@ -102,8 +102,10 @@ export function hookCommand(name) {
|
|
|
102
102
|
export const COMPANIONS = [
|
|
103
103
|
{
|
|
104
104
|
id: 'context-mode',
|
|
105
|
-
label: 'context-mode — otimizador de contexto + memória FTS5 (
|
|
106
|
-
default:
|
|
105
|
+
label: 'context-mode — otimizador de contexto + memória FTS5 (opcional)',
|
|
106
|
+
// Not a default: wendkeep is a neutral harness and does not presume a third-party plugin.
|
|
107
|
+
// Opt in interactively or with `--companions context-mode`.
|
|
108
|
+
default: false,
|
|
107
109
|
marketplace: { source: 'git', url: 'https://github.com/mksglu/context-mode.git' },
|
|
108
110
|
plugin: 'context-mode',
|
|
109
111
|
// Agent-agnostic: MCP server, self-updating via unpinned npx.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Generated Obsidian Bases + a Dashboard MOC — the vault's structural index (0.23.0).
|
|
2
|
+
// Each taxonomy folder gets a folder-filtered `.base` (NOT tag-filtered: a tag filter hid ~1/3
|
|
3
|
+
// of the corpus in production) and 00-Dashboard.md embeds them. Locale-aware; non-destructive
|
|
4
|
+
// (never overwrites a user's own base) unless force is passed. Zero runtime deps.
|
|
5
|
+
import { existsSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
7
|
+
import { getLocale } from '../hooks/locale.mjs';
|
|
8
|
+
|
|
9
|
+
// Per-area view: which folder, which frontmatter columns, and their display labels.
|
|
10
|
+
function areaSpecs(loc) {
|
|
11
|
+
const f = loc.folders;
|
|
12
|
+
const en = loc.id === 'en';
|
|
13
|
+
const date = en ? 'Date' : 'Data';
|
|
14
|
+
return [
|
|
15
|
+
{ key: 'sessoes', folder: f.sessions, title: en ? 'Sessions' : 'Sessões',
|
|
16
|
+
cols: ['date', 'provider', 'custo_modelo_usd', 'prompts', 'tool_calls'],
|
|
17
|
+
labels: { date, provider: en ? 'Agent' : 'Agente', custo_modelo_usd: 'US$', prompts: 'Prompts', tool_calls: 'Tools' } },
|
|
18
|
+
{ key: 'decisoes', folder: f.decisions, title: en ? 'Decisions' : 'Decisões',
|
|
19
|
+
cols: ['date', 'status'], labels: { date, status: 'Status' } },
|
|
20
|
+
{ key: 'bugs', folder: f.bugs, title: 'Bugs',
|
|
21
|
+
cols: ['date', 'status', 'severity'], labels: { date, status: 'Status', severity: en ? 'Severity' : 'Severidade' } },
|
|
22
|
+
{ key: 'aprendizados', folder: f.learnings, title: en ? 'Learnings' : 'Aprendizados',
|
|
23
|
+
cols: ['date'], labels: { date } },
|
|
24
|
+
{ key: 'specs', folder: f.specs, title: 'Specs', cols: ['date'], labels: { date } },
|
|
25
|
+
{ key: 'mudancas', folder: f.changes, title: en ? 'Changes' : 'Mudanças',
|
|
26
|
+
cols: ['date', 'status'], labels: { date, status: 'Status' } },
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// A minimal, valid Base: folder filter + one table view. No formulas (that's where Base syntax
|
|
31
|
+
// errors hide) — the folder filter is the whole fix.
|
|
32
|
+
export function renderBase(spec) {
|
|
33
|
+
const props = spec.cols.map((c) => ` ${c}:\n displayName: ${spec.labels[c] || c}`).join('\n');
|
|
34
|
+
const order = ['file.name', ...spec.cols].map((c) => ` - ${c}`).join('\n');
|
|
35
|
+
return `filters:
|
|
36
|
+
and:
|
|
37
|
+
- file.inFolder("${spec.folder}")
|
|
38
|
+
- file.ext == "md"
|
|
39
|
+
properties:
|
|
40
|
+
${props}
|
|
41
|
+
views:
|
|
42
|
+
- type: table
|
|
43
|
+
name: ${spec.title}
|
|
44
|
+
order:
|
|
45
|
+
${order}
|
|
46
|
+
`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function renderDashboard(specs, en) {
|
|
50
|
+
const blocks = specs.map((s) => `## ${s.title}\n\n![[${s.key}.base]]`).join('\n\n---\n\n');
|
|
51
|
+
return `---
|
|
52
|
+
tags:
|
|
53
|
+
- dashboard
|
|
54
|
+
cssclasses:
|
|
55
|
+
- topic-dashboard
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
# ${en ? 'Vault — Overview' : 'Vault — Visão Geral'}
|
|
59
|
+
|
|
60
|
+
> ${en ? 'Generated by wendkeep. Each section embeds a folder-filtered Base view (needs the Obsidian Bases core plugin).' : 'Gerado pelo wendkeep. Cada seção embeda uma view Base filtrada por pasta (requer o core plugin Bases do Obsidian).'}
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
${blocks}
|
|
65
|
+
`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Write one `<key>.base` per area + 00-Dashboard.md. Non-destructive unless force. Returns the
|
|
69
|
+
// list of files written.
|
|
70
|
+
export function seedVaultViews(vaultBase, { force = false } = {}) {
|
|
71
|
+
const loc = getLocale(vaultBase);
|
|
72
|
+
const specs = areaSpecs(loc);
|
|
73
|
+
const written = [];
|
|
74
|
+
for (const s of specs) {
|
|
75
|
+
const p = join(vaultBase, `${s.key}.base`);
|
|
76
|
+
if (force || !existsSync(p)) { writeFileSync(p, renderBase(s), 'utf8'); written.push(`${s.key}.base`); }
|
|
77
|
+
}
|
|
78
|
+
const dash = join(vaultBase, '00-Dashboard.md');
|
|
79
|
+
if (force || !existsSync(dash)) { writeFileSync(dash, renderDashboard(specs, loc.id === 'en'), 'utf8'); written.push('00-Dashboard.md'); }
|
|
80
|
+
return written;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function opt(argv, name) {
|
|
84
|
+
const i = argv.indexOf(name);
|
|
85
|
+
if (i >= 0) return argv[i + 1];
|
|
86
|
+
const eq = argv.find((a) => a.startsWith(`${name}=`));
|
|
87
|
+
return eq ? eq.slice(name.length + 1) : undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// `wendkeep dashboard [--vault P] [--force]` — (re)generate the Bases + Dashboard.
|
|
91
|
+
export function runDashboard(argv) {
|
|
92
|
+
const vaultRaw = opt(argv, '--vault') || process.env.OBSIDIAN_VAULT_PATH;
|
|
93
|
+
if (!vaultRaw) { process.stderr.write('wendkeep dashboard: no vault (--vault or OBSIDIAN_VAULT_PATH).\n'); process.exit(2); }
|
|
94
|
+
const vaultBase = isAbsolute(vaultRaw) ? vaultRaw : resolve(process.cwd(), vaultRaw);
|
|
95
|
+
if (!existsSync(vaultBase)) { process.stderr.write(`wendkeep dashboard: vault not found: ${vaultBase}\n`); process.exit(2); }
|
|
96
|
+
const written = seedVaultViews(vaultBase, { force: argv.includes('--force') });
|
|
97
|
+
process.stdout.write(written.length ? `dashboard: ${written.length} arquivo(s) — ${written.join(', ')}\n` : 'dashboard: nada a fazer (use --force para regenerar)\n');
|
|
98
|
+
process.exit(0);
|
|
99
|
+
}
|