wendkeep 0.2.2 → 0.2.3
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 +1 -0
- package/bin/wendkeep.mjs +1 -0
- package/hooks/linked-notes.mjs +5 -5
- package/hooks/obsidian-common.mjs +8 -1
- package/package.json +1 -1
- package/src/init.mjs +51 -7
- package/src/vault-theme.mjs +75 -0
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
|
|
package/hooks/linked-notes.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'fs';
|
|
3
3
|
import { basename, join } from 'path';
|
|
4
4
|
import {
|
|
5
|
-
|
|
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,
|
|
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,
|
|
481
|
-
const decisionsDir = join(vaultBase,
|
|
482
|
-
const learningsDir = join(vaultBase,
|
|
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' (
|
|
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.
|
|
3
|
+
"version": "0.2.3",
|
|
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'}
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
291
|
+
log(` [3/4] .mcp.json ${hadFile ? 'merged' : 'created'} (${names}${hadFile ? ', .bak saved' : ''})`);
|
|
255
292
|
}
|
|
256
293
|
} else {
|
|
257
|
-
log(' [3/
|
|
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,75 @@
|
|
|
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 c = `var(--wk-${k})`;
|
|
24
|
+
return [
|
|
25
|
+
`/* ${k} */`,
|
|
26
|
+
`.${v.cssClass} .inline-title {`,
|
|
27
|
+
` color: ${c};`,
|
|
28
|
+
` border-left: 4px solid ${c};`,
|
|
29
|
+
` padding-left: 0.5em;`,
|
|
30
|
+
`}`,
|
|
31
|
+
`.${v.cssClass} .view-header-title { color: ${c}; }`,
|
|
32
|
+
`.${v.cssClass} .markdown-preview-view h1 { border-bottom: 2px solid ${c}; }`,
|
|
33
|
+
].join('\n');
|
|
34
|
+
}).join('\n\n');
|
|
35
|
+
|
|
36
|
+
return `/* ${SNIPPET_NAME} — generated by wendkeep. Accents notes by type (cssclasses). */
|
|
37
|
+
:root {
|
|
38
|
+
${vars}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
${blocks}
|
|
42
|
+
`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Add the snippet to appearance.json's enabled list without touching other prefs.
|
|
46
|
+
export function mergeAppearance(existing, snippetName = SNIPPET_NAME) {
|
|
47
|
+
const a = existing && typeof existing === 'object' ? { ...existing } : {};
|
|
48
|
+
const set = new Set(Array.isArray(a.enabledCssSnippets) ? a.enabledCssSnippets : []);
|
|
49
|
+
set.add(snippetName);
|
|
50
|
+
a.enabledCssSnippets = [...set];
|
|
51
|
+
return a;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Graph color groups: one per note folder, colored from the palette.
|
|
55
|
+
export function graphColorGroups() {
|
|
56
|
+
return ENTRIES.map(([, v]) => ({
|
|
57
|
+
query: `path:"${v.folder}"`,
|
|
58
|
+
color: { a: 1, rgb: parseInt(v.hex.slice(1), 16) },
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Merge color groups into graph.json, deduping by query and keeping other keys.
|
|
63
|
+
export function mergeGraphColorGroups(existing, groups) {
|
|
64
|
+
const g = existing && typeof existing === 'object' ? { ...existing } : {};
|
|
65
|
+
const current = Array.isArray(g.colorGroups) ? [...g.colorGroups] : [];
|
|
66
|
+
const seen = new Set(current.map((x) => x.query));
|
|
67
|
+
for (const group of groups) {
|
|
68
|
+
if (!seen.has(group.query)) {
|
|
69
|
+
current.push(group);
|
|
70
|
+
seen.add(group.query);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
g.colorGroups = current;
|
|
74
|
+
return g;
|
|
75
|
+
}
|