wendkeep 0.2.1 → 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 +83 -21
- package/src/taxonomy.mjs +12 -6
- 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
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// OBSIDIAN_VAULT_PATH into .claude/settings.json, and adds the mcpvault server to
|
|
4
4
|
// .mcp.json. Idempotent: re-running only adds what is missing.
|
|
5
5
|
import { spawnSync } from 'node:child_process';
|
|
6
|
-
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
7
8
|
import { basename, isAbsolute, join, resolve } from 'node:path';
|
|
8
9
|
import { createInterface } from 'node:readline/promises';
|
|
9
10
|
import {
|
|
@@ -19,9 +20,17 @@ import {
|
|
|
19
20
|
companionMcpPatch,
|
|
20
21
|
companionHookSpecs,
|
|
21
22
|
cavemanInstallerCommand,
|
|
23
|
+
cavemanInstallerUrl,
|
|
22
24
|
} from './taxonomy.mjs';
|
|
23
25
|
import { renderVaultReadme } from './vault-readme.mjs';
|
|
24
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';
|
|
25
34
|
|
|
26
35
|
function parseArgs(argv) {
|
|
27
36
|
const args = { mcp: true, yes: false, force: false };
|
|
@@ -33,6 +42,7 @@ function parseArgs(argv) {
|
|
|
33
42
|
else if (a === '--yes' || a === '-y') args.yes = true;
|
|
34
43
|
else if (a === '--force') args.force = true;
|
|
35
44
|
else if (a === '--no-companions') args.noCompanions = true;
|
|
45
|
+
else if (a === '--no-colors') args.noColors = true;
|
|
36
46
|
else if (a === '--companions') args.companions = argv[++i];
|
|
37
47
|
else if (a.startsWith('--companions=')) args.companions = a.slice(13);
|
|
38
48
|
else if (a.startsWith('--vault=')) args.vault = a.slice(8);
|
|
@@ -118,6 +128,60 @@ export function mergeMcp(existing, { vaultPath, withVault = true, companions = [
|
|
|
118
128
|
return m;
|
|
119
129
|
}
|
|
120
130
|
|
|
131
|
+
// Run caveman's cross-agent installer (non-Claude skill coverage). Downloads the
|
|
132
|
+
// script to a temp file and runs it as a FILE — piping to iex/bash leaves the
|
|
133
|
+
// script's self-path null and the installer aborts. Best-effort, fail-soft: the
|
|
134
|
+
// Claude Code plugin entry is already wired regardless.
|
|
135
|
+
async function runCavemanInstaller(log) {
|
|
136
|
+
const url = cavemanInstallerUrl();
|
|
137
|
+
const ext = process.platform === 'win32' ? 'ps1' : 'sh';
|
|
138
|
+
const tmp = join(tmpdir(), `wendkeep-caveman-install-${process.pid}.${ext}`);
|
|
139
|
+
log(`\n caveman: fetching cross-agent installer (best-effort): ${url}`);
|
|
140
|
+
try {
|
|
141
|
+
const res = await fetch(url);
|
|
142
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
143
|
+
writeFileSync(tmp, await res.text(), 'utf8');
|
|
144
|
+
const cmd = cavemanInstallerCommand(process.platform, tmp);
|
|
145
|
+
log(` caveman: running ${cmd.command} ${cmd.args.join(' ')}`);
|
|
146
|
+
const r = spawnSync(cmd.command, cmd.args, { stdio: 'inherit' });
|
|
147
|
+
if (r.status !== 0) {
|
|
148
|
+
log(` [!] caveman installer exited ${r.status ?? '?'} — Claude Code plugin entry still wired; rerun manually if needed.`);
|
|
149
|
+
}
|
|
150
|
+
} catch (e) {
|
|
151
|
+
log(` [!] caveman installer skipped (${e.message}) — Claude Code plugin entry still wired.`);
|
|
152
|
+
} finally {
|
|
153
|
+
try { rmSync(tmp, { force: true }); } catch { /* temp cleanup best-effort */ }
|
|
154
|
+
}
|
|
155
|
+
}
|
|
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
|
+
|
|
121
185
|
// --- main -------------------------------------------------------------------
|
|
122
186
|
|
|
123
187
|
export async function runInit(argv) {
|
|
@@ -148,11 +212,12 @@ export async function runInit(argv) {
|
|
|
148
212
|
} else if (args.companions !== undefined) {
|
|
149
213
|
companions = resolveCompanions({ companionsFlag: args.companions });
|
|
150
214
|
} else if (process.stdin.isTTY && !args.yes) {
|
|
151
|
-
log('\nCompanions (plugins/MCP opcionais — context-mode é a principal):');
|
|
152
215
|
if (canInteractiveSelect()) {
|
|
216
|
+
log(''); // the checkbox menu renders its own header
|
|
153
217
|
companions = await selectCompanionsInteractive(COMPANIONS);
|
|
154
218
|
} else {
|
|
155
219
|
// Text fallback (no raw-mode TTY): list + comma input with clear instructions.
|
|
220
|
+
log('\nCompanions (plugins/MCP opcionais — context-mode é a principal):');
|
|
156
221
|
for (const c of COMPANIONS) log(` ${c.default ? '[x]' : '[ ]'} ${c.label}`);
|
|
157
222
|
const def = resolveCompanions({}).join(',');
|
|
158
223
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -170,7 +235,8 @@ export async function runInit(argv) {
|
|
|
170
235
|
log(` project : ${projectPath}`);
|
|
171
236
|
log(` vault : ${vaultPath}`);
|
|
172
237
|
log(` mcp : ${args.mcp ? 'mcpvault (wendkeep-vault)' : 'skipped'}`);
|
|
173
|
-
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`);
|
|
174
240
|
|
|
175
241
|
// 1. Vault taxonomy ---------------------------------------------------------
|
|
176
242
|
if (!existsSync(vaultPath)) mkdirSync(vaultPath, { recursive: true });
|
|
@@ -189,7 +255,7 @@ export async function runInit(argv) {
|
|
|
189
255
|
writeFileSync(readmePath, renderVaultReadme({ projectName: basename(projectPath), vaultPath, withMcp: args.mcp }), 'utf8');
|
|
190
256
|
readmeNote = ', README.md created';
|
|
191
257
|
}
|
|
192
|
-
log(` [1/
|
|
258
|
+
log(` [1/4] vault taxonomy: ${VAULT_FOLDERS.length} folders (${created} created)${readmeNote}`);
|
|
193
259
|
|
|
194
260
|
// 2. .claude/settings.json --------------------------------------------------
|
|
195
261
|
const settingsPath = join(projectPath, '.claude', 'settings.json');
|
|
@@ -198,13 +264,13 @@ export async function runInit(argv) {
|
|
|
198
264
|
// Unparseable existing file — never clobber. Drop a .new for manual merge.
|
|
199
265
|
const fresh = mergeSettings(null, { vaultPath, withMcp: args.mcp, force: true, companions }).settings;
|
|
200
266
|
writeJson(`${settingsPath}.new`, fresh);
|
|
201
|
-
log(` [2/
|
|
267
|
+
log(` [2/4] settings.json exists but is not valid JSON -> wrote ${settingsPath}.new (merge by hand)`);
|
|
202
268
|
} else {
|
|
203
269
|
const hadFile = settingsRead.data !== null;
|
|
204
270
|
const { settings, added } = mergeSettings(settingsRead.data, { vaultPath, withMcp: args.mcp, force: args.force, companions });
|
|
205
271
|
if (hadFile) backup(settingsPath);
|
|
206
272
|
writeJson(settingsPath, settings);
|
|
207
|
-
log(` [2/
|
|
273
|
+
log(` [2/4] settings.json ${hadFile ? 'merged' : 'created'} (${added} hook(s) wired, OBSIDIAN_VAULT_PATH set${hadFile ? ', .bak saved' : ''})`);
|
|
208
274
|
}
|
|
209
275
|
|
|
210
276
|
// 3. .mcp.json --------------------------------------------------------------
|
|
@@ -217,32 +283,28 @@ export async function runInit(argv) {
|
|
|
217
283
|
const names = [args.mcp && MCP_SERVER_KEY, ...Object.keys(companionMcp)].filter(Boolean).join(', ');
|
|
218
284
|
if (!mcpRead.ok) {
|
|
219
285
|
writeJson(`${mcpPath}.new`, mergeMcp(null, mcpOpts));
|
|
220
|
-
log(` [3/
|
|
286
|
+
log(` [3/4] .mcp.json exists but is not valid JSON -> wrote ${mcpPath}.new (merge by hand)`);
|
|
221
287
|
} else {
|
|
222
288
|
const hadFile = mcpRead.data !== null;
|
|
223
289
|
if (hadFile) backup(mcpPath);
|
|
224
290
|
writeJson(mcpPath, mergeMcp(mcpRead.data, mcpOpts));
|
|
225
|
-
log(` [3/
|
|
291
|
+
log(` [3/4] .mcp.json ${hadFile ? 'merged' : 'created'} (${names}${hadFile ? ', .bak saved' : ''})`);
|
|
226
292
|
}
|
|
227
293
|
} else {
|
|
228
|
-
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)}`);
|
|
229
302
|
}
|
|
230
303
|
|
|
231
304
|
// caveman has no MCP / agnostic-hook path: on non-Claude agents its skills come
|
|
232
305
|
// from its own cross-agent installer. Best-effort, fail-soft — the Claude Code
|
|
233
306
|
// plugin entry is already wired in settings.json regardless.
|
|
234
|
-
if (companions.includes('caveman'))
|
|
235
|
-
const cmd = cavemanInstallerCommand();
|
|
236
|
-
log(`\n caveman: running cross-agent installer (best-effort): ${cmd.command} ${cmd.args.join(' ')}`);
|
|
237
|
-
try {
|
|
238
|
-
const r = spawnSync(cmd.command, cmd.args, { stdio: 'inherit' });
|
|
239
|
-
if (r.status !== 0) {
|
|
240
|
-
log(` [!] caveman installer exited ${r.status ?? '?'} — Claude Code plugin entry still wired; rerun manually if needed.`);
|
|
241
|
-
}
|
|
242
|
-
} catch (e) {
|
|
243
|
-
log(` [!] caveman installer could not run (${e.code || e.message}) — Claude Code plugin entry still wired.`);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
307
|
+
if (companions.includes('caveman')) await runCavemanInstaller(log);
|
|
246
308
|
|
|
247
309
|
log('\nNext steps:');
|
|
248
310
|
log(` 1. Open the vault in Obsidian: "Open folder as vault" -> ${vaultPath}`);
|
package/src/taxonomy.mjs
CHANGED
|
@@ -178,15 +178,21 @@ export function companionHookSpecs(ids) {
|
|
|
178
178
|
return specs;
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
// {command, args}; the caller decides whether to spawn it.
|
|
184
|
-
export function cavemanInstallerCommand(platform = process.platform) {
|
|
181
|
+
// The caveman installer URL for this platform (.ps1 on Windows, .sh elsewhere).
|
|
182
|
+
export function cavemanInstallerUrl(platform = process.platform) {
|
|
185
183
|
const { sh, ps1 } = COMPANION_BY_ID.caveman.installer;
|
|
184
|
+
return platform === 'win32' ? ps1 : sh;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Command to run caveman's installer FROM A LOCAL FILE. We download the script to a
|
|
188
|
+
// temp file and run it as a file (not `irm|iex` / `curl|bash`) so the script gets a
|
|
189
|
+
// real $PSCommandPath/$0 — piping to iex leaves that null and the installer aborts
|
|
190
|
+
// ("cannot bind argument to parameter 'Path'"). Pure: returns {command, args}.
|
|
191
|
+
export function cavemanInstallerCommand(platform = process.platform, scriptPath) {
|
|
186
192
|
if (platform === 'win32') {
|
|
187
|
-
return { command: 'powershell', args: ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-
|
|
193
|
+
return { command: 'powershell', args: ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', scriptPath] };
|
|
188
194
|
}
|
|
189
|
-
return { command: 'bash', args: [
|
|
195
|
+
return { command: 'bash', args: [scriptPath] };
|
|
190
196
|
}
|
|
191
197
|
|
|
192
198
|
// Derive the default vault folder name from the project folder: `.<project>-vault`.
|
|
@@ -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
|
+
}
|