wendkeep 0.2.4 → 0.2.6
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 +2 -0
- package/bin/wendkeep.mjs +16 -0
- package/package.json +1 -1
- package/src/init.mjs +22 -19
- package/src/sync-defs.mjs +123 -0
- package/src/taxonomy.mjs +13 -19
- package/src/validate-core.mjs +156 -0
- package/src/vault-theme.mjs +157 -40
package/README.md
CHANGED
|
@@ -40,6 +40,8 @@ npx wendkeep init
|
|
|
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
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`.
|
|
43
|
+
6. Seed the **curated memory layer**: `.brain/CORE.md` (the hand-curated hot layer, with the 3 required sections) and `.brain/COMPACTION_PROTOCOL.md` (the protocol guide). The auto layers (`DIGEST.md`, `index.jsonl`) are generated by the hooks. Validate the curated layer with `wendkeep validate-memory` (cap 25 lines, 3 sections, no secrets/PII).
|
|
44
|
+
7. Seed the **definitions layer**: `.brain/agents/` + `.brain/skills/` (versioned source of truth for custom agents/skills, with a README + one example each). Run `wendkeep sync-defs` to copy them into the project's agent dirs (`.brain/agents/*.toml` → `.codex/agents/`, `.brain/skills/<name>/` → `.claude/skills/`).
|
|
43
45
|
|
|
44
46
|
```bash
|
|
45
47
|
npx wendkeep init --vault "~/vaults/work" --project . --yes # non-interactive (companions: context-mode)
|
package/bin/wendkeep.mjs
CHANGED
|
@@ -40,6 +40,12 @@ Usage:
|
|
|
40
40
|
agent's JSON on stdin. Names: ${RUNNABLE_HOOKS.join(', ')}.
|
|
41
41
|
|
|
42
42
|
wendkeep doctor [--vault P] Run a vault health check.
|
|
43
|
+
wendkeep validate-memory [path] Validate .brain/CORE.md against the compaction
|
|
44
|
+
protocol (cap 25, 3 sections, no secrets/PII). Uses
|
|
45
|
+
--vault <path> or OBSIDIAN_VAULT_PATH if no path given.
|
|
46
|
+
wendkeep sync-defs [opts] Copy versioned defs from the vault's .brain into the
|
|
47
|
+
project: .brain/agents/*.toml -> .codex/agents,
|
|
48
|
+
.brain/skills/<name> -> .claude/skills. --vault P --project P.
|
|
43
49
|
wendkeep --version Print version.
|
|
44
50
|
wendkeep --help Show this help.
|
|
45
51
|
`;
|
|
@@ -80,6 +86,16 @@ async function main() {
|
|
|
80
86
|
runDoctor(rest);
|
|
81
87
|
break;
|
|
82
88
|
}
|
|
89
|
+
case 'validate-memory': {
|
|
90
|
+
const { runValidateMemory } = await import('../src/validate-core.mjs');
|
|
91
|
+
runValidateMemory(rest);
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
case 'sync-defs': {
|
|
95
|
+
const { runSyncDefs } = await import('../src/sync-defs.mjs');
|
|
96
|
+
runSyncDefs(rest);
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
83
99
|
case '--version':
|
|
84
100
|
case '-v':
|
|
85
101
|
process.stdout.write(`${version()}\n`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
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,8 +3,7 @@
|
|
|
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,
|
|
7
|
-
import { tmpdir } from 'node:os';
|
|
6
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
8
7
|
import { basename, isAbsolute, join, resolve } from 'node:path';
|
|
9
8
|
import { createInterface } from 'node:readline/promises';
|
|
10
9
|
import {
|
|
@@ -19,8 +18,7 @@ import {
|
|
|
19
18
|
companionSettingsPatch,
|
|
20
19
|
companionMcpPatch,
|
|
21
20
|
companionHookSpecs,
|
|
22
|
-
|
|
23
|
-
cavemanInstallerUrl,
|
|
21
|
+
cavemanInstallCommand,
|
|
24
22
|
} from './taxonomy.mjs';
|
|
25
23
|
import { renderVaultReadme } from './vault-readme.mjs';
|
|
26
24
|
import { canInteractiveSelect, selectCompanionsInteractive } from './companion-select.mjs';
|
|
@@ -31,6 +29,8 @@ import {
|
|
|
31
29
|
graphColorGroups,
|
|
32
30
|
mergeGraphColorGroups,
|
|
33
31
|
} from './vault-theme.mjs';
|
|
32
|
+
import { renderCoreSkeleton, renderCompactionProtocol } from './validate-core.mjs';
|
|
33
|
+
import { seedDefinitions } from './sync-defs.mjs';
|
|
34
34
|
|
|
35
35
|
function parseArgs(argv) {
|
|
36
36
|
const args = { mcp: true, yes: false, force: false };
|
|
@@ -132,25 +132,16 @@ export function mergeMcp(existing, { vaultPath, withVault = true, companions = [
|
|
|
132
132
|
// script to a temp file and runs it as a FILE — piping to iex/bash leaves the
|
|
133
133
|
// script's self-path null and the installer aborts. Best-effort, fail-soft: the
|
|
134
134
|
// Claude Code plugin entry is already wired regardless.
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
const tmp = join(tmpdir(), `wendkeep-caveman-install-${process.pid}.${ext}`);
|
|
139
|
-
log(`\n caveman: fetching cross-agent installer (best-effort): ${url}`);
|
|
135
|
+
function runCavemanInstaller(log) {
|
|
136
|
+
const cmd = cavemanInstallCommand();
|
|
137
|
+
log(`\n caveman: running cross-agent installer (best-effort, Gemini skipped):\n ${cmd}`);
|
|
140
138
|
try {
|
|
141
|
-
const
|
|
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' });
|
|
139
|
+
const r = spawnSync(cmd, { stdio: 'inherit', shell: true });
|
|
147
140
|
if (r.status !== 0) {
|
|
148
141
|
log(` [!] caveman installer exited ${r.status ?? '?'} — Claude Code plugin entry still wired; rerun manually if needed.`);
|
|
149
142
|
}
|
|
150
143
|
} catch (e) {
|
|
151
144
|
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
145
|
}
|
|
155
146
|
}
|
|
156
147
|
|
|
@@ -255,7 +246,19 @@ export async function runInit(argv) {
|
|
|
255
246
|
writeFileSync(readmePath, renderVaultReadme({ projectName: basename(projectPath), vaultPath, withMcp: args.mcp }), 'utf8');
|
|
256
247
|
readmeNote = ', README.md created';
|
|
257
248
|
}
|
|
258
|
-
|
|
249
|
+
// Seed the curated memory layer (CORE.md) + the compaction-protocol doc. The
|
|
250
|
+
// DIGEST/index are auto-generated by the hooks; CORE is hand-curated, so we
|
|
251
|
+
// bootstrap it with the 3 required sections. Non-destructive.
|
|
252
|
+
const brainDir = join(vaultPath, '.brain');
|
|
253
|
+
mkdirSync(brainDir, { recursive: true });
|
|
254
|
+
const corePath = join(brainDir, 'CORE.md');
|
|
255
|
+
if (!existsSync(corePath)) writeFileSync(corePath, renderCoreSkeleton(), 'utf8');
|
|
256
|
+
const protoPath = join(brainDir, 'COMPACTION_PROTOCOL.md');
|
|
257
|
+
if (!existsSync(protoPath)) writeFileSync(protoPath, renderCompactionProtocol(), 'utf8');
|
|
258
|
+
// Seed the definitions layer (.brain/agents + .brain/skills): versioned source of
|
|
259
|
+
// truth for custom agents/skills. `wendkeep sync-defs` copies them to the agent dirs.
|
|
260
|
+
seedDefinitions(brainDir);
|
|
261
|
+
log(` [1/4] vault taxonomy: ${VAULT_FOLDERS.length} folders (${created} created)${readmeNote}, .brain seeded (CORE + agents/skills)`);
|
|
259
262
|
|
|
260
263
|
// 2. .claude/settings.json --------------------------------------------------
|
|
261
264
|
const settingsPath = join(projectPath, '.claude', 'settings.json');
|
|
@@ -304,7 +307,7 @@ export async function runInit(argv) {
|
|
|
304
307
|
// caveman has no MCP / agnostic-hook path: on non-Claude agents its skills come
|
|
305
308
|
// from its own cross-agent installer. Best-effort, fail-soft — the Claude Code
|
|
306
309
|
// plugin entry is already wired in settings.json regardless.
|
|
307
|
-
if (companions.includes('caveman'))
|
|
310
|
+
if (companions.includes('caveman')) runCavemanInstaller(log);
|
|
308
311
|
|
|
309
312
|
log('\nNext steps:');
|
|
310
313
|
log(` 1. Open the vault in Obsidian: "Open folder as vault" -> ${vaultPath}`);
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Definitions layer: versioned custom agents/skills live in the vault's .brain so
|
|
2
|
+
// they travel with the project in git. They have no automatic consumer — agents
|
|
3
|
+
// read them from their own dirs — so `wendkeep sync-defs` copies them there:
|
|
4
|
+
// .brain/agents/*.toml -> <project>/.codex/agents/ (Codex agent format)
|
|
5
|
+
// .brain/skills/<name>/ -> <project>/.claude/skills/ (skill format)
|
|
6
|
+
// .brain is the source of truth; re-run sync after editing. Copy (not symlink) for
|
|
7
|
+
// cross-platform robustness.
|
|
8
|
+
import { copyFileSync, cpSync, existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
10
|
+
|
|
11
|
+
export function syncDefs(vaultBase, projectPath) {
|
|
12
|
+
const out = { agents: [], skills: [] };
|
|
13
|
+
|
|
14
|
+
const agentsSrc = join(vaultBase, '.brain', 'agents');
|
|
15
|
+
if (existsSync(agentsSrc)) {
|
|
16
|
+
const dest = join(projectPath, '.codex', 'agents');
|
|
17
|
+
for (const f of readdirSync(agentsSrc)) {
|
|
18
|
+
if (!f.endsWith('.toml')) continue; // README.md etc. are docs, not defs
|
|
19
|
+
mkdirSync(dest, { recursive: true });
|
|
20
|
+
copyFileSync(join(agentsSrc, f), join(dest, f));
|
|
21
|
+
out.agents.push(f);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const skillsSrc = join(vaultBase, '.brain', 'skills');
|
|
26
|
+
if (existsSync(skillsSrc)) {
|
|
27
|
+
for (const name of readdirSync(skillsSrc)) {
|
|
28
|
+
const dir = join(skillsSrc, name);
|
|
29
|
+
if (!statSync(dir).isDirectory()) continue; // skip skills/README.md
|
|
30
|
+
const dest = join(projectPath, '.claude', 'skills', name);
|
|
31
|
+
mkdirSync(dest, { recursive: true });
|
|
32
|
+
cpSync(dir, dest, { recursive: true });
|
|
33
|
+
out.skills.push(name);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// CLI entry for `wendkeep sync-defs`.
|
|
41
|
+
export function runSyncDefs(argv) {
|
|
42
|
+
let vault;
|
|
43
|
+
let project;
|
|
44
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
45
|
+
const a = argv[i];
|
|
46
|
+
if (a === '--vault') vault = argv[++i];
|
|
47
|
+
else if (a.startsWith('--vault=')) vault = a.slice(8);
|
|
48
|
+
else if (a === '--project') project = argv[++i];
|
|
49
|
+
else if (a.startsWith('--project=')) project = a.slice(10);
|
|
50
|
+
}
|
|
51
|
+
const base = vault || process.env.OBSIDIAN_VAULT_PATH;
|
|
52
|
+
if (!base) {
|
|
53
|
+
process.stderr.write('wendkeep sync-defs: no vault. Pass --vault <path> or set OBSIDIAN_VAULT_PATH.\n');
|
|
54
|
+
process.exit(2);
|
|
55
|
+
}
|
|
56
|
+
const vaultBase = isAbsolute(base) ? base : resolve(process.cwd(), base);
|
|
57
|
+
const projectPath = resolve(project || process.cwd());
|
|
58
|
+
const r = syncDefs(vaultBase, projectPath);
|
|
59
|
+
process.stdout.write(
|
|
60
|
+
`wendkeep sync-defs: ${r.agents.length} agent(s) -> .codex/agents, ${r.skills.length} skill(s) -> .claude/skills\n`,
|
|
61
|
+
);
|
|
62
|
+
if (r.agents.length) process.stdout.write(` agents: ${r.agents.join(', ')}\n`);
|
|
63
|
+
if (r.skills.length) process.stdout.write(` skills: ${r.skills.join(', ')}\n`);
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// --- seeding (init) ---------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
const AGENTS_README = `# .brain/agents — versioned custom agent definitions
|
|
70
|
+
|
|
71
|
+
Canonical, versioned source for your project's custom agents (Codex \`.toml\` format).
|
|
72
|
+
\`wendkeep sync-defs\` copies each \`*.toml\` here into \`<project>/.codex/agents/\` so the
|
|
73
|
+
agent loads them. Edit here (source of truth); re-run \`wendkeep sync-defs\` after changes.
|
|
74
|
+
`;
|
|
75
|
+
|
|
76
|
+
const EXAMPLE_AGENT = `# Example custom agent (Codex format). Replace with your own.
|
|
77
|
+
name = "example-agent"
|
|
78
|
+
description = "An example custom agent. Describe when to use it."
|
|
79
|
+
developer_instructions = "You are an example agent. State the role, rules, and output format here."
|
|
80
|
+
nickname_candidates = [ "example" ]
|
|
81
|
+
model = "gpt-5.5"
|
|
82
|
+
`;
|
|
83
|
+
|
|
84
|
+
const SKILLS_README = `# .brain/skills — versioned custom skill definitions
|
|
85
|
+
|
|
86
|
+
Canonical, versioned source for your project's custom skills (\`<name>/SKILL.md\`).
|
|
87
|
+
\`wendkeep sync-defs\` copies each skill folder here into \`<project>/.claude/skills/\` so
|
|
88
|
+
the agent loads them. Edit here (source of truth); re-run \`wendkeep sync-defs\` after changes.
|
|
89
|
+
`;
|
|
90
|
+
|
|
91
|
+
const EXAMPLE_SKILL = `---
|
|
92
|
+
name: example-skill
|
|
93
|
+
description: An example custom skill. Replace with your own — describe the trigger here.
|
|
94
|
+
---
|
|
95
|
+
Use this skill when ... (describe when it applies).
|
|
96
|
+
|
|
97
|
+
Rules:
|
|
98
|
+
- (what the skill should do)
|
|
99
|
+
`;
|
|
100
|
+
|
|
101
|
+
function writeIfAbsent(path, content, created) {
|
|
102
|
+
if (!existsSync(path)) {
|
|
103
|
+
writeFileSync(path, content, 'utf8');
|
|
104
|
+
created.push(path);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Seed the definitions layer in the vault's .brain (folders + README + one example
|
|
109
|
+
// each). Non-destructive. Returns the list of paths created.
|
|
110
|
+
export function seedDefinitions(brainDir) {
|
|
111
|
+
const created = [];
|
|
112
|
+
const agentsDir = join(brainDir, 'agents');
|
|
113
|
+
mkdirSync(agentsDir, { recursive: true });
|
|
114
|
+
writeIfAbsent(join(agentsDir, 'README.md'), AGENTS_README, created);
|
|
115
|
+
writeIfAbsent(join(agentsDir, 'example-agent.toml'), EXAMPLE_AGENT, created);
|
|
116
|
+
|
|
117
|
+
const exampleSkillDir = join(brainDir, 'skills', 'example-skill');
|
|
118
|
+
mkdirSync(exampleSkillDir, { recursive: true });
|
|
119
|
+
writeIfAbsent(join(brainDir, 'skills', 'README.md'), SKILLS_README, created);
|
|
120
|
+
writeIfAbsent(join(exampleSkillDir, 'SKILL.md'), EXAMPLE_SKILL, created);
|
|
121
|
+
|
|
122
|
+
return created;
|
|
123
|
+
}
|
package/src/taxonomy.mjs
CHANGED
|
@@ -110,11 +110,8 @@ export const COMPANIONS = [
|
|
|
110
110
|
default: false,
|
|
111
111
|
marketplace: { source: 'git', url: 'https://github.com/JuliusBrussee/caveman.git' },
|
|
112
112
|
plugin: 'caveman',
|
|
113
|
-
// No MCP, no agnostic hook: on non-Claude agents, run its own installer
|
|
114
|
-
|
|
115
|
-
sh: 'https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.sh',
|
|
116
|
-
ps1: 'https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.ps1',
|
|
117
|
-
},
|
|
113
|
+
// No MCP, no agnostic hook: on non-Claude agents, run its own installer
|
|
114
|
+
// (see cavemanInstallCommand — npx, non-interactive, Gemini excluded).
|
|
118
115
|
},
|
|
119
116
|
];
|
|
120
117
|
|
|
@@ -178,21 +175,18 @@ export function companionHookSpecs(ids) {
|
|
|
178
175
|
return specs;
|
|
179
176
|
}
|
|
180
177
|
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
178
|
+
// Agents wendkeep installs caveman into via its cross-agent installer. Gemini is
|
|
179
|
+
// excluded on purpose: its CLI rejects caveman's agent tool names and crashes
|
|
180
|
+
// (libuv assertion) mid-install. caveman has no --exclude flag — only an --only
|
|
181
|
+
// allow-list — so we enumerate the agents we want by their stable slugs.
|
|
182
|
+
export const CAVEMAN_AGENTS = ['claude', 'codex', 'cursor', 'copilot', 'amp', 'antigravity'];
|
|
186
183
|
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
return { command: 'powershell', args: ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', scriptPath] };
|
|
194
|
-
}
|
|
195
|
-
return { command: 'bash', args: [scriptPath] };
|
|
184
|
+
// Non-interactive command installing caveman to CAVEMAN_AGENTS (never Gemini). Runs
|
|
185
|
+
// the published installer directly via npx (no install.ps1, so no $PSCommandPath
|
|
186
|
+
// issue). Returned as a shell string; the caller spawns it with shell: true.
|
|
187
|
+
export function cavemanInstallCommand(agents = CAVEMAN_AGENTS) {
|
|
188
|
+
const only = agents.flatMap((a) => ['--only', a]).join(' ');
|
|
189
|
+
return `npx -y github:JuliusBrussee/caveman -- --non-interactive ${only}`;
|
|
196
190
|
}
|
|
197
191
|
|
|
198
192
|
// Derive the default vault folder name from the project folder: `.<project>-vault`.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// Memory-compaction protocol for the curated .brain/CORE.md layer.
|
|
2
|
+
// Ported from NutriGym-Vision's scripts/validate-brain-core.js to ESM:
|
|
3
|
+
// - cap 25 lines (hard), 22 (soft warning) — 1 durable item per line
|
|
4
|
+
// - 3 required sections
|
|
5
|
+
// - no secrets / no real-provider PII emails
|
|
6
|
+
// Plus the seeded skeleton and the protocol reference doc.
|
|
7
|
+
|
|
8
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
9
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
10
|
+
|
|
11
|
+
const HARD_LIMIT = 25;
|
|
12
|
+
const SOFT_LIMIT = 22;
|
|
13
|
+
|
|
14
|
+
const REQUIRED_SECTIONS = [
|
|
15
|
+
{ label: 'Preferências do Usuário', regex: /^##\s+Prefer[êe]ncias\s+do\s+Usu[áa]rio\s*$/im },
|
|
16
|
+
{ label: 'Padrões Ativos', regex: /^##\s+Padr[õo]es\s+Ativos\s*$/im },
|
|
17
|
+
{ label: 'Pendências Abertas', regex: /^##\s+Pend[êe]ncias\s+Abertas\s*$/im },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
// Secret patterns reject only "real" values (length floor); abstract mentions like
|
|
21
|
+
// `sk_*` / `whsec_*` (trailing asterisk) are allowed.
|
|
22
|
+
const SECRET_PATTERNS = [
|
|
23
|
+
{ name: 'Stripe secret key', regex: /\bsk_(?:live|test)_[A-Za-z0-9]{20,}\b/ },
|
|
24
|
+
{ name: 'Stripe webhook secret', regex: /\bwhsec_[A-Za-z0-9]{20,}\b/ },
|
|
25
|
+
{ name: 'JWT token', regex: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/ },
|
|
26
|
+
{ name: 'Bearer token', regex: /\bBearer\s+[A-Za-z0-9._-]{20,}\b/i },
|
|
27
|
+
{ name: 'OpenAI API key', regex: /\bsk-[A-Za-z0-9]{40,}\b/ },
|
|
28
|
+
{ name: 'Anthropic API key', regex: /\bsk-ant-[A-Za-z0-9_-]{40,}\b/ },
|
|
29
|
+
{ name: 'Google API key', regex: /\bAIza[0-9A-Za-z_-]{35}\b/ },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const PII_EMAIL_REGEX = /\b[A-Za-z0-9._%+-]+@(?!example\.(?:com|org|net)\b)(?:gmail|hotmail|yahoo|outlook|live|icloud|protonmail)\.[A-Za-z]{2,}\b/i;
|
|
33
|
+
|
|
34
|
+
// Validate CORE.md content. Returns { ok, errors, warnings, lineCount }.
|
|
35
|
+
export function validateCore(content) {
|
|
36
|
+
const text = String(content ?? '');
|
|
37
|
+
const lines = text.split('\n');
|
|
38
|
+
const lineCount = text.endsWith('\n') ? lines.length - 1 : lines.length;
|
|
39
|
+
const errors = [];
|
|
40
|
+
|
|
41
|
+
if (lineCount > HARD_LIMIT) {
|
|
42
|
+
errors.push(`Tamanho ${lineCount} > ${HARD_LIMIT} linhas (hard limit). Curar: remover itens resolvidos (detalhe vive no vault/git).`);
|
|
43
|
+
}
|
|
44
|
+
for (const { label, regex } of REQUIRED_SECTIONS) {
|
|
45
|
+
if (!regex.test(text)) errors.push(`Seção obrigatória ausente: ## ${label}`);
|
|
46
|
+
}
|
|
47
|
+
for (const { name, regex } of SECRET_PATTERNS) {
|
|
48
|
+
const m = text.match(regex);
|
|
49
|
+
if (m) errors.push(`Possível ${name} detectado: "${m[0].slice(0, 30)}..." — substituir por [REDACTED_SECRET].`);
|
|
50
|
+
}
|
|
51
|
+
const em = text.match(PII_EMAIL_REGEX);
|
|
52
|
+
if (em) errors.push(`Email real detectado: "${em[0]}" — usar user@example.com.`);
|
|
53
|
+
|
|
54
|
+
const warnings = [];
|
|
55
|
+
if (lineCount >= SOFT_LIMIT && lineCount <= HARD_LIMIT) {
|
|
56
|
+
warnings.push(`Tamanho ${lineCount}/${HARD_LIMIT} linhas — perto do limite; remover itens resolvidos (≥${SOFT_LIMIT}).`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { ok: errors.length === 0, errors, warnings, lineCount };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The seeded CORE.md (must pass validateCore). Bootstraps the 3 sections so the
|
|
63
|
+
// curated hot layer exists with the right shape from day one.
|
|
64
|
+
export function renderCoreSkeleton() {
|
|
65
|
+
return `# CORE — núcleo curado da memória (.brain)
|
|
66
|
+
|
|
67
|
+
> REGRA #1 — memória canônica do projeto. Curado à mão, cap 25 linhas (valide: \`wendkeep validate-memory\`). Volátil vive no DIGEST.md (auto). Profundidade: /brain-recall <tópico>.
|
|
68
|
+
|
|
69
|
+
## Preferências do Usuário
|
|
70
|
+
- (preferências duráveis: idioma, estilo, convenções)
|
|
71
|
+
|
|
72
|
+
## Padrões Ativos
|
|
73
|
+
- (padrões/arquitetura ativos que outro agente precise saber)
|
|
74
|
+
|
|
75
|
+
## Pendências Abertas
|
|
76
|
+
- (pendências/decisões em aberto — remova quando resolvidas)
|
|
77
|
+
`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// The compaction-protocol reference doc dropped into the vault.
|
|
81
|
+
export function renderCompactionProtocol() {
|
|
82
|
+
return `# Protocolo de Memória — núcleo curado + digest automático (.brain)
|
|
83
|
+
|
|
84
|
+
> Como cada agente recebe, consulta e persiste memória entre sessões no seu vault.
|
|
85
|
+
|
|
86
|
+
## 1. Duas camadas
|
|
87
|
+
|
|
88
|
+
- **QUENTE** (auto-injetada por sessão, budget ~45 linhas):
|
|
89
|
+
- \`.brain/CORE.md\` — curado à mão, **≤25 linhas** (1 item/linha): preferências, padrões, pendências.
|
|
90
|
+
- \`.brain/DIGEST.md\` — auto-gerado (0 token LLM, ≤15 linhas): decisões/sessões/bugs/aprendizados recentes.
|
|
91
|
+
- **FRIA** (sob demanda):
|
|
92
|
+
- \`.brain/index.jsonl\` — índice de todas as sessões (1/linha, frontmatter).
|
|
93
|
+
- Vault: \`02-Sessões/**\`, \`04-Decisões/**\`, \`05-Bugs/**\`, \`06-Aprendizados/**\`. Desce via \`/brain-recall <tópico>\`.
|
|
94
|
+
|
|
95
|
+
## 2. Compactação = regra de geração (sem trabalho manual)
|
|
96
|
+
|
|
97
|
+
- **DIGEST se auto-compacta**: caps determinísticos (5 decisões, 4 sessões, 2 bugs, 2 aprendizados + \`+N mais\`). O velho cai do quente sozinho e permanece no índice/vault. **NUNCA editar** \`DIGEST.md\`/\`index.jsonl\`.
|
|
98
|
+
- **CORE**: quando ≥22 linhas (soft warning), remover itens resolvidos/obsoletos — o detalhe já vive no vault e no histórico do git.
|
|
99
|
+
|
|
100
|
+
## 3. O que escrever no CORE
|
|
101
|
+
|
|
102
|
+
Só estado **durável** que outro agente precise saber — preferência, padrão ativo, pendência aberta. 1 linha por item. Nunca log de sessão (isso é automático no vault).
|
|
103
|
+
|
|
104
|
+
3 seções fixas (obrigatórias): \`## Preferências do Usuário\`, \`## Padrões Ativos\`, \`## Pendências Abertas\`.
|
|
105
|
+
|
|
106
|
+
## 4. Sem segredos / PII
|
|
107
|
+
|
|
108
|
+
\`CORE.md\` nunca contém tokens (\`sk_*\`, \`whsec_*\`, JWT, Bearer), API keys, senhas ou email/telefone real. Use \`[REDACTED_SECRET]\` / \`user@example.com\`.
|
|
109
|
+
|
|
110
|
+
## 5. Validação
|
|
111
|
+
|
|
112
|
+
\`\`\`bash
|
|
113
|
+
wendkeep validate-memory # valida <vault>/.brain/CORE.md
|
|
114
|
+
wendkeep validate-memory <path> # valida outro arquivo
|
|
115
|
+
\`\`\`
|
|
116
|
+
|
|
117
|
+
Checa: cap 25 (soft 22), 3 seções, sem segredos/PII. Exit 0 = OK, 1 = falha.
|
|
118
|
+
`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// CLI entry for `wendkeep validate-memory [path]`. Resolves the target from an
|
|
122
|
+
// explicit path, else <vault>/.brain/CORE.md (--vault or OBSIDIAN_VAULT_PATH).
|
|
123
|
+
export function runValidateMemory(argv) {
|
|
124
|
+
let target;
|
|
125
|
+
let vault;
|
|
126
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
127
|
+
const a = argv[i];
|
|
128
|
+
if (a === '--vault') vault = argv[++i];
|
|
129
|
+
else if (a.startsWith('--vault=')) vault = a.slice(8);
|
|
130
|
+
else if (!a.startsWith('-')) target = a;
|
|
131
|
+
}
|
|
132
|
+
if (!target) {
|
|
133
|
+
const base = vault || process.env.OBSIDIAN_VAULT_PATH;
|
|
134
|
+
if (!base) {
|
|
135
|
+
process.stderr.write('wendkeep validate-memory: no target. Pass a path, --vault <path>, or set OBSIDIAN_VAULT_PATH.\n');
|
|
136
|
+
process.exit(2);
|
|
137
|
+
}
|
|
138
|
+
target = join(base, '.brain', 'CORE.md');
|
|
139
|
+
}
|
|
140
|
+
const abs = isAbsolute(target) ? target : resolve(process.cwd(), target);
|
|
141
|
+
if (!existsSync(abs)) {
|
|
142
|
+
process.stderr.write(`wendkeep validate-memory: not found: ${abs}\n`);
|
|
143
|
+
process.exit(2);
|
|
144
|
+
}
|
|
145
|
+
const res = validateCore(readFileSync(abs, 'utf8'));
|
|
146
|
+
if (!res.ok) {
|
|
147
|
+
process.stderr.write(`❌ CORE.md viola protocolo (${res.errors.length} erro${res.errors.length > 1 ? 's' : ''}):\n`);
|
|
148
|
+
for (const e of res.errors) process.stderr.write(` - ${e}\n`);
|
|
149
|
+
process.stderr.write('\nProtocolo: .brain/COMPACTION_PROTOCOL.md\n');
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
let msg = `✅ CORE.md OK (${res.lineCount} linhas, 3/3 seções, sem segredos).`;
|
|
153
|
+
for (const w of res.warnings) msg += `\n ⚠ ${w}`;
|
|
154
|
+
process.stdout.write(`${msg}\n`);
|
|
155
|
+
process.exit(0);
|
|
156
|
+
}
|
package/src/vault-theme.mjs
CHANGED
|
@@ -1,52 +1,169 @@
|
|
|
1
|
-
// Vault color system for `wendkeep init`.
|
|
2
|
-
//
|
|
3
|
-
// -
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
1
|
+
// Vault color system for `wendkeep init`. Generated CSS snippet (ported from the
|
|
2
|
+
// NutriGym-Vision design) plus Obsidian graph color groups. Two visual layers:
|
|
3
|
+
// - file explorer: each taxonomy folder gets an accent (border + dot + hover)
|
|
4
|
+
// - open note: colored by its `cssclasses` (topic-session/decision/bug/learning)
|
|
5
|
+
// via a --note-accent var driving headings, strong, tags, tables, blockquotes
|
|
6
|
+
// Folder/topic lists are derived from maps that mirror the vault taxonomy, so the
|
|
7
|
+
// snippet stays in sync. Pure + side-effect free; init does the file I/O.
|
|
7
8
|
|
|
8
9
|
export const SNIPPET_NAME = 'wendkeep-colors';
|
|
9
10
|
|
|
10
|
-
//
|
|
11
|
+
// Base palette (hex + "r, g, b" for rgba()).
|
|
12
|
+
const PALETTE = {
|
|
13
|
+
blue: { hex: '#2f80ed', rgb: '47, 128, 237' },
|
|
14
|
+
violet: { hex: '#8f5cf7', rgb: '143, 92, 247' },
|
|
15
|
+
red: { hex: '#e03131', rgb: '224, 49, 49' },
|
|
16
|
+
green: { hex: '#2f9e44', rgb: '47, 158, 68' },
|
|
17
|
+
amber: { hex: '#f59f00', rgb: '245, 159, 0' },
|
|
18
|
+
teal: { hex: '#0ca678', rgb: '12, 166, 120' },
|
|
19
|
+
indigo: { hex: '#4263eb', rgb: '66, 99, 235' },
|
|
20
|
+
slate: { hex: '#64748b', rgb: '100, 116, 139' },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// File-explorer folder -> palette key (mirrors VAULT_FOLDERS in taxonomy.mjs).
|
|
24
|
+
const FOLDER_PALETTE = [
|
|
25
|
+
['00-Inbox', 'amber'],
|
|
26
|
+
['01-Projeto', 'indigo'],
|
|
27
|
+
['02-Sessões', 'blue'],
|
|
28
|
+
['03-Linear', 'teal'],
|
|
29
|
+
['04-Decisões', 'violet'],
|
|
30
|
+
['05-Bugs', 'red'],
|
|
31
|
+
['06-Aprendizados', 'green'],
|
|
32
|
+
['Templates', 'slate'],
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
// Note cssclass -> palette key; `folder` is reused for the graph color groups.
|
|
11
36
|
export const NOTE_COLORS = {
|
|
12
|
-
session: { cssClass: 'topic-session', folder: '02-Sessões',
|
|
13
|
-
decision: { cssClass: 'topic-decision', folder: '04-Decisões',
|
|
14
|
-
bug: { cssClass: 'topic-bug', folder: '05-Bugs',
|
|
15
|
-
learning: { cssClass: 'topic-learning', folder: '06-Aprendizados',
|
|
37
|
+
session: { cssClass: 'topic-session', folder: '02-Sessões', palette: 'blue' },
|
|
38
|
+
decision: { cssClass: 'topic-decision', folder: '04-Decisões', palette: 'violet' },
|
|
39
|
+
bug: { cssClass: 'topic-bug', folder: '05-Bugs', palette: 'red' },
|
|
40
|
+
learning: { cssClass: 'topic-learning', folder: '06-Aprendizados', palette: 'green' },
|
|
16
41
|
};
|
|
17
42
|
|
|
18
|
-
|
|
43
|
+
// cssclasses that get the note-accent treatment (the four note types + a home note).
|
|
44
|
+
const TOPIC_CLASSES = [...Object.values(NOTE_COLORS).map((n) => n.cssClass), 'vault-home'];
|
|
45
|
+
const NOTE_ACCENTS = [
|
|
46
|
+
...Object.values(NOTE_COLORS).map((n) => ({ cssClass: n.cssClass, palette: n.palette })),
|
|
47
|
+
{ cssClass: 'vault-home', palette: 'indigo' },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const FX = '.workspace-leaf-content[data-type="file-explorer"] .nav-folder-title';
|
|
51
|
+
const folderSel = FOLDER_PALETTE.map(([f]) => `[data-path^="${f}"]`).join(',\n ');
|
|
52
|
+
const topicReading = TOPIC_CLASSES.map((c) => `.${c}`).join(', ');
|
|
19
53
|
|
|
20
54
|
export function renderColorSnippetCss() {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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. */
|
|
55
|
+
const rootVars = Object.entries(PALETTE)
|
|
56
|
+
.map(([k, v]) => ` --us-${k}: ${v.hex};\n --us-${k}-rgb: ${v.rgb};`)
|
|
57
|
+
.join('\n');
|
|
58
|
+
|
|
59
|
+
const folderAccentVars = FOLDER_PALETTE.map(([folder, key]) =>
|
|
60
|
+
`${FX}[data-path^="${folder}"] {\n --folder-accent: var(--us-${key});\n --folder-accent-rgb: var(--us-${key}-rgb);\n}`,
|
|
61
|
+
).join('\n\n');
|
|
62
|
+
|
|
63
|
+
const noteAccentVars = NOTE_ACCENTS.map(({ cssClass, palette }) =>
|
|
64
|
+
`.markdown-preview-view.${cssClass},\n.markdown-source-view.${cssClass} {\n --note-accent: var(--us-${palette});\n --note-accent-rgb: var(--us-${palette}-rgb);\n}`,
|
|
65
|
+
).join('\n\n');
|
|
66
|
+
|
|
67
|
+
return `/* ${SNIPPET_NAME} — generated by wendkeep. File-explorer + note-type colors. */
|
|
45
68
|
:root {
|
|
46
|
-
${
|
|
69
|
+
${rootVars}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.theme-dark { --us-line-tint: 44%; }
|
|
73
|
+
.theme-light { --us-line-tint: 36%; }
|
|
74
|
+
|
|
75
|
+
/* --- File explorer: accented folders ------------------------------------- */
|
|
76
|
+
${FX} {
|
|
77
|
+
border-left: 3px solid transparent;
|
|
78
|
+
border-radius: 0 6px 6px 0;
|
|
79
|
+
margin: 1px 4px 1px 0;
|
|
80
|
+
transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;
|
|
47
81
|
}
|
|
48
82
|
|
|
49
|
-
${
|
|
83
|
+
${folderAccentVars}
|
|
84
|
+
|
|
85
|
+
${FX}:is(
|
|
86
|
+
${folderSel}
|
|
87
|
+
) {
|
|
88
|
+
border-left-color: var(--folder-accent);
|
|
89
|
+
color: color-mix(in srgb, var(--folder-accent) var(--us-line-tint), var(--text-normal));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
${FX}:is(
|
|
93
|
+
${folderSel}
|
|
94
|
+
):hover {
|
|
95
|
+
background-color: rgba(var(--folder-accent-rgb), 0.13);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
${FX}:is(
|
|
99
|
+
${folderSel}
|
|
100
|
+
) .nav-folder-title-content::before {
|
|
101
|
+
content: "";
|
|
102
|
+
display: inline-block;
|
|
103
|
+
width: 0.58em;
|
|
104
|
+
height: 0.58em;
|
|
105
|
+
margin-right: 0.48em;
|
|
106
|
+
border-radius: 999px;
|
|
107
|
+
background: var(--folder-accent);
|
|
108
|
+
box-shadow: 0 0 0 3px rgba(var(--folder-accent-rgb), 0.16);
|
|
109
|
+
vertical-align: 0.05em;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* --- Note types: colors from frontmatter cssclasses ---------------------- */
|
|
113
|
+
${noteAccentVars}
|
|
114
|
+
|
|
115
|
+
.markdown-rendered:is(${topicReading}) h1,
|
|
116
|
+
.markdown-source-view:is(${topicReading}) .cm-header-1 {
|
|
117
|
+
color: color-mix(in srgb, var(--note-accent) 62%, var(--text-normal));
|
|
118
|
+
}
|
|
119
|
+
.markdown-rendered:is(${topicReading}) h1 {
|
|
120
|
+
border-bottom: 2px solid rgba(var(--note-accent-rgb), 0.42);
|
|
121
|
+
padding-bottom: 0.35em;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.markdown-rendered:is(${topicReading}) h2 {
|
|
125
|
+
border-left: 4px solid var(--note-accent);
|
|
126
|
+
border-radius: 6px;
|
|
127
|
+
padding: 0.38em 0.62em;
|
|
128
|
+
background: rgba(var(--note-accent-rgb), 0.11);
|
|
129
|
+
}
|
|
130
|
+
.markdown-source-view:is(${topicReading}) .HyperMD-header-2 {
|
|
131
|
+
border-left: 4px solid var(--note-accent);
|
|
132
|
+
border-radius: 6px;
|
|
133
|
+
padding-left: 0.62em;
|
|
134
|
+
background: rgba(var(--note-accent-rgb), 0.1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.markdown-rendered:is(${topicReading}) h3,
|
|
138
|
+
.markdown-source-view:is(${topicReading}) .cm-header-3 {
|
|
139
|
+
color: color-mix(in srgb, var(--note-accent) 76%, var(--text-normal));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.markdown-rendered:is(${topicReading}) strong,
|
|
143
|
+
.markdown-source-view:is(${topicReading}) .cm-strong {
|
|
144
|
+
color: color-mix(in srgb, var(--note-accent) 70%, var(--text-normal));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.markdown-rendered:is(${topicReading}) .tag {
|
|
148
|
+
border: 1px solid rgba(var(--note-accent-rgb), 0.42);
|
|
149
|
+
background: rgba(var(--note-accent-rgb), 0.1);
|
|
150
|
+
color: color-mix(in srgb, var(--note-accent) 72%, var(--text-normal));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.markdown-rendered:is(${topicReading}) th {
|
|
154
|
+
background: rgba(var(--note-accent-rgb), 0.12);
|
|
155
|
+
color: color-mix(in srgb, var(--note-accent) 68%, var(--text-normal));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.markdown-rendered:is(${topicReading}) blockquote {
|
|
159
|
+
border-left-color: var(--note-accent);
|
|
160
|
+
background: rgba(var(--note-accent-rgb), 0.08);
|
|
161
|
+
border-radius: 0 6px 6px 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.markdown-rendered:is(${topicReading}) code {
|
|
165
|
+
border-color: rgba(var(--note-accent-rgb), 0.28);
|
|
166
|
+
}
|
|
50
167
|
`;
|
|
51
168
|
}
|
|
52
169
|
|
|
@@ -61,9 +178,9 @@ export function mergeAppearance(existing, snippetName = SNIPPET_NAME) {
|
|
|
61
178
|
|
|
62
179
|
// Graph color groups: one per note folder, colored from the palette.
|
|
63
180
|
export function graphColorGroups() {
|
|
64
|
-
return
|
|
181
|
+
return Object.values(NOTE_COLORS).map((v) => ({
|
|
65
182
|
query: `path:"${v.folder}"`,
|
|
66
|
-
color: { a: 1, rgb: parseInt(v.hex.slice(1), 16) },
|
|
183
|
+
color: { a: 1, rgb: parseInt(PALETTE[v.palette].hex.slice(1), 16) },
|
|
67
184
|
}));
|
|
68
185
|
}
|
|
69
186
|
|