wendkeep 0.2.2 → 0.2.4

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/README.md CHANGED
@@ -39,6 +39,7 @@ npx wendkeep init
39
39
  2. **Merge** the three session hooks and `OBSIDIAN_VAULT_PATH` into `.claude/settings.json` — without clobbering your existing settings (a `.bak` is saved; an unparseable file is left untouched and a `.new` is written for you to merge).
40
40
  3. Add the `wendkeep-vault` MCP server to `.mcp.json` (skip with `--no-mcp`).
41
41
  4. Offer to pin **companion** plugins/MCP (multi-choice; `context-mode` pre-checked). Each is wired through the most agent-agnostic path it supports — `context-mode` as an `.mcp.json` MCP server (any agent), `understand-anything` via a `understand-inject` SessionStart hook that injects its domain graph when generated, and the Claude Code plugin layer (`extraKnownMarketplaces` + `enabledPlugins`) as a bonus. Control with `--companions <csv>` or `--no-companions`. (`caveman` additionally runs its own cross-agent installer on non-Claude agents.)
42
+ 5. Install a **color system** into the vault's `.obsidian/`: a CSS snippet that accents notes by type (session/decision/bug/learning, via the `cssclasses` the hooks emit) plus graph color groups by folder. Non-destructive merge into `appearance.json`/`graph.json`; skip with `--no-colors`.
42
43
 
43
44
  ```bash
44
45
  npx wendkeep init --vault "~/vaults/work" --project . --yes # non-interactive (companions: context-mode)
package/bin/wendkeep.mjs CHANGED
@@ -32,6 +32,7 @@ Usage:
32
32
  --companions <csv> Companion plugins/MCP to pin: context-mode,caveman,understand-anything
33
33
  (default when interactive/--yes: context-mode).
34
34
  --no-companions Skip companion plugins/MCP entirely.
35
+ --no-colors Skip the Obsidian color system (.obsidian snippet + graph groups).
35
36
  --yes, -y Non-interactive; accept defaults.
36
37
  --force Overwrite existing wendkeep config blocks.
37
38
 
@@ -2,7 +2,7 @@
2
2
  import { existsSync, readFileSync, readdirSync, writeFileSync } from 'fs';
3
3
  import { basename, join } from 'path';
4
4
  import {
5
- datedFolderRelFromDateStr,
5
+ monthFolderRelFromDateStr,
6
6
  derivedContentKey,
7
7
  ensureDir,
8
8
  getNextAdrNumber,
@@ -457,7 +457,7 @@ function existingKeysForSession(vaultBase, sessionRel, dateStr) {
457
457
  const wikilink = wikilinkFromRel(sessionRel);
458
458
  const out = { bugs: [], decisions: [], learnings: [] };
459
459
  for (const [type, folder] of Object.entries(DERIVED_FOLDERS)) {
460
- const dir = join(vaultBase, datedFolderRelFromDateStr(folder, dateStr));
460
+ const dir = join(vaultBase, monthFolderRelFromDateStr(folder, dateStr));
461
461
  for (const fileName of listMd(dir)) {
462
462
  try {
463
463
  const c = readFileSync(join(dir, fileName), 'utf-8');
@@ -477,9 +477,9 @@ function alreadyHasKey(keys, candidate) {
477
477
  export function createLinkedNotes(vaultBase, dateStr, sessionRel, tx, options = {}) {
478
478
  const linked = { decisions: [], bugs: [], learnings: [] };
479
479
  const provider = providerMeta(options.provider);
480
- const bugsDir = join(vaultBase, datedFolderRelFromDateStr('05-Bugs', dateStr));
481
- const decisionsDir = join(vaultBase, datedFolderRelFromDateStr('04-Decisões', dateStr));
482
- const learningsDir = join(vaultBase, datedFolderRelFromDateStr('06-Aprendizados', dateStr));
480
+ const bugsDir = join(vaultBase, monthFolderRelFromDateStr('05-Bugs', dateStr));
481
+ const decisionsDir = join(vaultBase, monthFolderRelFromDateStr('04-Decisões', dateStr));
482
+ const learningsDir = join(vaultBase, monthFolderRelFromDateStr('06-Aprendizados', dateStr));
483
483
  ensureDir(bugsDir);
484
484
  ensureDir(decisionsDir);
485
485
  ensureDir(learningsDir);
@@ -133,12 +133,19 @@ export function datedFolderRel(rootFolder, date = new Date()) {
133
133
  return join(rootFolder, String(p.year), MONTH_FOLDERS[p.month - 1], `DIA ${pad2(p.day)}`);
134
134
  }
135
135
 
136
- // Mesma estrutura datada a partir de uma string 'YYYY-MM-DD' (usada pelas notas derivadas).
136
+ // Mesma estrutura datada a partir de uma string 'YYYY-MM-DD' (sessões: até o DIA).
137
137
  export function datedFolderRelFromDateStr(rootFolder, dateStr) {
138
138
  const [year, month, day] = String(dateStr).split('-');
139
139
  return join(rootFolder, year, MONTH_FOLDERS[Number(month) - 1], `DIA ${pad2(day)}`);
140
140
  }
141
141
 
142
+ // Estrutura até o MÊS (sem DIA) — usada pelas notas derivadas (decisões/bugs/
143
+ // aprendizados): tudo do mês fica junto em <pasta>/<ano>/<MM-MMM>/.
144
+ export function monthFolderRelFromDateStr(rootFolder, dateStr) {
145
+ const [year, month] = String(dateStr).split('-');
146
+ return join(rootFolder, year, MONTH_FOLDERS[Number(month) - 1]);
147
+ }
148
+
142
149
  export function sessionFolderRel(date = new Date()) {
143
150
  return datedFolderRel('02-Sessões', date);
144
151
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Automatically capture AI coding agent sessions (Claude Code, Codex) as local Markdown in your Obsidian vault — turn-by-turn history, cost/token tracking, auto-extracted decisions/bugs/learnings, rendered in the graph. Local-first.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/init.mjs CHANGED
@@ -24,6 +24,13 @@ import {
24
24
  } from './taxonomy.mjs';
25
25
  import { renderVaultReadme } from './vault-readme.mjs';
26
26
  import { canInteractiveSelect, selectCompanionsInteractive } from './companion-select.mjs';
27
+ import {
28
+ SNIPPET_NAME,
29
+ renderColorSnippetCss,
30
+ mergeAppearance,
31
+ graphColorGroups,
32
+ mergeGraphColorGroups,
33
+ } from './vault-theme.mjs';
27
34
 
28
35
  function parseArgs(argv) {
29
36
  const args = { mcp: true, yes: false, force: false };
@@ -35,6 +42,7 @@ function parseArgs(argv) {
35
42
  else if (a === '--yes' || a === '-y') args.yes = true;
36
43
  else if (a === '--force') args.force = true;
37
44
  else if (a === '--no-companions') args.noCompanions = true;
45
+ else if (a === '--no-colors') args.noColors = true;
38
46
  else if (a === '--companions') args.companions = argv[++i];
39
47
  else if (a.startsWith('--companions=')) args.companions = a.slice(13);
40
48
  else if (a.startsWith('--vault=')) args.vault = a.slice(8);
@@ -146,6 +154,34 @@ async function runCavemanInstaller(log) {
146
154
  }
147
155
  }
148
156
 
157
+ // Install the vault color system into .obsidian: write wendkeep's CSS snippet,
158
+ // enable it (non-destructive), and add graph color groups. Returns a short note.
159
+ // Unparseable user JSON is left untouched (we only merge into valid/absent files).
160
+ function installVaultColors(vaultPath) {
161
+ const obsidianDir = join(vaultPath, '.obsidian');
162
+ const snippetsDir = join(obsidianDir, 'snippets');
163
+ mkdirSync(snippetsDir, { recursive: true });
164
+ writeFileSync(join(snippetsDir, `${SNIPPET_NAME}.css`), renderColorSnippetCss(), 'utf8');
165
+
166
+ let enabled = false;
167
+ const appPath = join(obsidianDir, 'appearance.json');
168
+ const appRead = readJsonSafe(appPath);
169
+ if (appRead.ok) {
170
+ writeJson(appPath, mergeAppearance(appRead.data || {}, SNIPPET_NAME));
171
+ enabled = true;
172
+ }
173
+
174
+ let groups = 0;
175
+ const graphPath = join(obsidianDir, 'graph.json');
176
+ const graphRead = readJsonSafe(graphPath);
177
+ if (graphRead.ok) {
178
+ const merged = mergeGraphColorGroups(graphRead.data || {}, graphColorGroups());
179
+ writeJson(graphPath, merged);
180
+ groups = graphColorGroups().length;
181
+ }
182
+ return `snippet ${SNIPPET_NAME}.css${enabled ? ' (enabled)' : ' (enable by hand: appearance.json unreadable)'} + ${groups} graph group(s)`;
183
+ }
184
+
149
185
  // --- main -------------------------------------------------------------------
150
186
 
151
187
  export async function runInit(argv) {
@@ -199,7 +235,8 @@ export async function runInit(argv) {
199
235
  log(` project : ${projectPath}`);
200
236
  log(` vault : ${vaultPath}`);
201
237
  log(` mcp : ${args.mcp ? 'mcpvault (wendkeep-vault)' : 'skipped'}`);
202
- log(` companions : ${companions.length ? companions.join(', ') : 'none'}\n`);
238
+ log(` companions : ${companions.length ? companions.join(', ') : 'none'}`);
239
+ log(` colors : ${args.noColors ? 'skipped' : 'wendkeep-colors (snippet + graph groups)'}\n`);
203
240
 
204
241
  // 1. Vault taxonomy ---------------------------------------------------------
205
242
  if (!existsSync(vaultPath)) mkdirSync(vaultPath, { recursive: true });
@@ -218,7 +255,7 @@ export async function runInit(argv) {
218
255
  writeFileSync(readmePath, renderVaultReadme({ projectName: basename(projectPath), vaultPath, withMcp: args.mcp }), 'utf8');
219
256
  readmeNote = ', README.md created';
220
257
  }
221
- log(` [1/3] vault taxonomy: ${VAULT_FOLDERS.length} folders (${created} created)${readmeNote}`);
258
+ log(` [1/4] vault taxonomy: ${VAULT_FOLDERS.length} folders (${created} created)${readmeNote}`);
222
259
 
223
260
  // 2. .claude/settings.json --------------------------------------------------
224
261
  const settingsPath = join(projectPath, '.claude', 'settings.json');
@@ -227,13 +264,13 @@ export async function runInit(argv) {
227
264
  // Unparseable existing file — never clobber. Drop a .new for manual merge.
228
265
  const fresh = mergeSettings(null, { vaultPath, withMcp: args.mcp, force: true, companions }).settings;
229
266
  writeJson(`${settingsPath}.new`, fresh);
230
- log(` [2/3] settings.json exists but is not valid JSON -> wrote ${settingsPath}.new (merge by hand)`);
267
+ log(` [2/4] settings.json exists but is not valid JSON -> wrote ${settingsPath}.new (merge by hand)`);
231
268
  } else {
232
269
  const hadFile = settingsRead.data !== null;
233
270
  const { settings, added } = mergeSettings(settingsRead.data, { vaultPath, withMcp: args.mcp, force: args.force, companions });
234
271
  if (hadFile) backup(settingsPath);
235
272
  writeJson(settingsPath, settings);
236
- log(` [2/3] settings.json ${hadFile ? 'merged' : 'created'} (${added} hook(s) wired, OBSIDIAN_VAULT_PATH set${hadFile ? ', .bak saved' : ''})`);
273
+ log(` [2/4] settings.json ${hadFile ? 'merged' : 'created'} (${added} hook(s) wired, OBSIDIAN_VAULT_PATH set${hadFile ? ', .bak saved' : ''})`);
237
274
  }
238
275
 
239
276
  // 3. .mcp.json --------------------------------------------------------------
@@ -246,15 +283,22 @@ export async function runInit(argv) {
246
283
  const names = [args.mcp && MCP_SERVER_KEY, ...Object.keys(companionMcp)].filter(Boolean).join(', ');
247
284
  if (!mcpRead.ok) {
248
285
  writeJson(`${mcpPath}.new`, mergeMcp(null, mcpOpts));
249
- log(` [3/3] .mcp.json exists but is not valid JSON -> wrote ${mcpPath}.new (merge by hand)`);
286
+ log(` [3/4] .mcp.json exists but is not valid JSON -> wrote ${mcpPath}.new (merge by hand)`);
250
287
  } else {
251
288
  const hadFile = mcpRead.data !== null;
252
289
  if (hadFile) backup(mcpPath);
253
290
  writeJson(mcpPath, mergeMcp(mcpRead.data, mcpOpts));
254
- log(` [3/3] .mcp.json ${hadFile ? 'merged' : 'created'} (${names}${hadFile ? ', .bak saved' : ''})`);
291
+ log(` [3/4] .mcp.json ${hadFile ? 'merged' : 'created'} (${names}${hadFile ? ', .bak saved' : ''})`);
255
292
  }
256
293
  } else {
257
- log(' [3/3] .mcp.json skipped (--no-mcp, no MCP companions)');
294
+ log(' [3/4] .mcp.json skipped (--no-mcp, no MCP companions)');
295
+ }
296
+
297
+ // 4. Vault color system (.obsidian) -----------------------------------------
298
+ if (args.noColors) {
299
+ log(' [4/4] colors skipped (--no-colors)');
300
+ } else {
301
+ log(` [4/4] colors: ${installVaultColors(vaultPath)}`);
258
302
  }
259
303
 
260
304
  // caveman has no MCP / agnostic-hook path: on non-Claude agents its skills come
@@ -0,0 +1,83 @@
1
+ // Vault color system for `wendkeep init`. Two layers, both keyed to the note
2
+ // taxonomy the hooks already emit:
3
+ // - a CSS snippet that accents an open note by its `cssclasses`
4
+ // (topic-session/decision/bug/learning), enabled via appearance.json
5
+ // - Obsidian graph color groups that tint nodes by their folder
6
+ // Pure + side-effect free; init does the file I/O and non-destructive merges.
7
+
8
+ export const SNIPPET_NAME = 'wendkeep-colors';
9
+
10
+ // Soft, semantic palette — readable in light and dark themes.
11
+ export const NOTE_COLORS = {
12
+ session: { cssClass: 'topic-session', folder: '02-Sessões', hex: '#4f86c6' }, // blue
13
+ decision: { cssClass: 'topic-decision', folder: '04-Decisões', hex: '#9b72cf' }, // purple
14
+ bug: { cssClass: 'topic-bug', folder: '05-Bugs', hex: '#d9645c' }, // red
15
+ learning: { cssClass: 'topic-learning', folder: '06-Aprendizados', hex: '#5ca862' }, // green
16
+ };
17
+
18
+ const ENTRIES = Object.entries(NOTE_COLORS);
19
+
20
+ export function renderColorSnippetCss() {
21
+ const vars = ENTRIES.map(([k, v]) => ` --wk-${k}: ${v.hex};`).join('\n');
22
+ const blocks = ENTRIES.map(([k, v]) => {
23
+ const cls = `.${v.cssClass}`;
24
+ const c = `var(--wk-${k})`;
25
+ return [
26
+ `/* ${k} */`,
27
+ `/* left accent strip down the whole note — Reading + Live Preview */`,
28
+ `${cls} .markdown-preview-sizer,`,
29
+ `${cls} .cm-sizer {`,
30
+ ` border-left: 5px solid ${c};`,
31
+ ` padding-left: 1.2em;`,
32
+ `}`,
33
+ `/* title + every heading in the note's color (both modes) */`,
34
+ `${cls} .inline-title,`,
35
+ `${cls} .cm-header,`,
36
+ `${cls} :is(h1, h2, h3, h4, h5, h6) {`,
37
+ ` color: ${c};`,
38
+ `}`,
39
+ `${cls} .inline-title { border-bottom: 2px solid ${c}; }`,
40
+ ].join('\n');
41
+ }).join('\n\n');
42
+
43
+ return `/* ${SNIPPET_NAME} — generated by wendkeep. Accents notes by type (cssclasses).
44
+ Works in both Reading and Live Preview. */
45
+ :root {
46
+ ${vars}
47
+ }
48
+
49
+ ${blocks}
50
+ `;
51
+ }
52
+
53
+ // Add the snippet to appearance.json's enabled list without touching other prefs.
54
+ export function mergeAppearance(existing, snippetName = SNIPPET_NAME) {
55
+ const a = existing && typeof existing === 'object' ? { ...existing } : {};
56
+ const set = new Set(Array.isArray(a.enabledCssSnippets) ? a.enabledCssSnippets : []);
57
+ set.add(snippetName);
58
+ a.enabledCssSnippets = [...set];
59
+ return a;
60
+ }
61
+
62
+ // Graph color groups: one per note folder, colored from the palette.
63
+ export function graphColorGroups() {
64
+ return ENTRIES.map(([, v]) => ({
65
+ query: `path:"${v.folder}"`,
66
+ color: { a: 1, rgb: parseInt(v.hex.slice(1), 16) },
67
+ }));
68
+ }
69
+
70
+ // Merge color groups into graph.json, deduping by query and keeping other keys.
71
+ export function mergeGraphColorGroups(existing, groups) {
72
+ const g = existing && typeof existing === 'object' ? { ...existing } : {};
73
+ const current = Array.isArray(g.colorGroups) ? [...g.colorGroups] : [];
74
+ const seen = new Set(current.map((x) => x.query));
75
+ for (const group of groups) {
76
+ if (!seen.has(group.query)) {
77
+ current.push(group);
78
+ seen.add(group.query);
79
+ }
80
+ }
81
+ g.colorGroups = current;
82
+ return g;
83
+ }