wendkeep 0.2.5 → 0.2.7
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 +4 -2
- package/bin/wendkeep.mjs +21 -2
- package/package.json +1 -1
- package/src/dotcontext-seed.mjs +83 -0
- package/src/init.mjs +64 -30
- package/src/sync-defs.mjs +123 -0
- package/src/taxonomy.mjs +62 -20
- package/src/validate-core.mjs +156 -0
package/README.md
CHANGED
|
@@ -38,11 +38,13 @@ npx wendkeep init
|
|
|
38
38
|
1. Create the vault folder taxonomy and a templated `README.md` (default vault: `<project>/.<project-name>-vault`, e.g. `.MyApp-vault`; override with `--vault`).
|
|
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
|
-
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
|
|
41
|
+
4. Offer to pin **companion** plugins/MCP (multi-choice; `context-mode` + `dotcontext` pre-checked). Each is wired through the most agent-agnostic path it supports — `context-mode` and `dotcontext` as `.mcp.json` MCP servers (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. **`dotcontext`** (execution/PREVC harness) also gets its lifecycle hooks wired (SessionStart-last + Stop + PostToolUse, pinned CLI) and a starter `.context/config/sensors.json` seeded with a `wendkeep validate-memory` sensor plus one per detected `package.json` script (test/typecheck/lint/build) — add the `.context/runtime|cache|logs|plans|agents|skills/` paths to `.gitignore` (wendkeep doesn't touch git). If dotcontext is already configured globally (`~/.claude.json`), the project MCP entry is **skipped automatically** to avoid a duplicate; tune with `--dotcontext-mcp <auto|project|none>` and `--dotcontext-hooks <full|light|none>` (light drops PostToolUse). Control selection 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
|
-
npx wendkeep init --vault "~/vaults/work" --project . --yes # non-interactive (companions: context-mode)
|
|
47
|
+
npx wendkeep init --vault "~/vaults/work" --project . --yes # non-interactive (companions: context-mode,dotcontext)
|
|
46
48
|
npx wendkeep init --companions "context-mode,caveman" --yes
|
|
47
49
|
npx wendkeep init --no-companions --yes
|
|
48
50
|
```
|
package/bin/wendkeep.mjs
CHANGED
|
@@ -29,10 +29,13 @@ Usage:
|
|
|
29
29
|
--vault <path> Obsidian vault folder (default: <project>/.<project-name>-vault).
|
|
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
|
-
--companions <csv> Companion plugins/MCP to pin: context-mode,caveman,understand-anything
|
|
33
|
-
(default when interactive/--yes: context-mode).
|
|
32
|
+
--companions <csv> Companion plugins/MCP to pin: context-mode,dotcontext,caveman,understand-anything
|
|
33
|
+
(default when interactive/--yes: context-mode,dotcontext).
|
|
34
34
|
--no-companions Skip companion plugins/MCP entirely.
|
|
35
35
|
--no-colors Skip the Obsidian color system (.obsidian snippet + graph groups).
|
|
36
|
+
--dotcontext-mcp <v> dotcontext MCP placement: auto (default; skip project entry
|
|
37
|
+
if already global), project, or none.
|
|
38
|
+
--dotcontext-hooks <v> dotcontext hooks: full (default), light (no PostToolUse), none.
|
|
36
39
|
--yes, -y Non-interactive; accept defaults.
|
|
37
40
|
--force Overwrite existing wendkeep config blocks.
|
|
38
41
|
|
|
@@ -40,6 +43,12 @@ Usage:
|
|
|
40
43
|
agent's JSON on stdin. Names: ${RUNNABLE_HOOKS.join(', ')}.
|
|
41
44
|
|
|
42
45
|
wendkeep doctor [--vault P] Run a vault health check.
|
|
46
|
+
wendkeep validate-memory [path] Validate .brain/CORE.md against the compaction
|
|
47
|
+
protocol (cap 25, 3 sections, no secrets/PII). Uses
|
|
48
|
+
--vault <path> or OBSIDIAN_VAULT_PATH if no path given.
|
|
49
|
+
wendkeep sync-defs [opts] Copy versioned defs from the vault's .brain into the
|
|
50
|
+
project: .brain/agents/*.toml -> .codex/agents,
|
|
51
|
+
.brain/skills/<name> -> .claude/skills. --vault P --project P.
|
|
43
52
|
wendkeep --version Print version.
|
|
44
53
|
wendkeep --help Show this help.
|
|
45
54
|
`;
|
|
@@ -80,6 +89,16 @@ async function main() {
|
|
|
80
89
|
runDoctor(rest);
|
|
81
90
|
break;
|
|
82
91
|
}
|
|
92
|
+
case 'validate-memory': {
|
|
93
|
+
const { runValidateMemory } = await import('../src/validate-core.mjs');
|
|
94
|
+
runValidateMemory(rest);
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
case 'sync-defs': {
|
|
98
|
+
const { runSyncDefs } = await import('../src/sync-defs.mjs');
|
|
99
|
+
runSyncDefs(rest);
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
83
102
|
case '--version':
|
|
84
103
|
case '-v':
|
|
85
104
|
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.7",
|
|
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": {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// dotcontext .context/config seeding + MCP-placement resolution.
|
|
2
|
+
// Seeds `memory-validation` (`wendkeep validate-memory`, valid in any wendkeep
|
|
3
|
+
// project) plus a sensor for each detected package.json script (test/typecheck/
|
|
4
|
+
// lint/build). Project-specific commands beyond that are left to `context init`.
|
|
5
|
+
// Non-destructive. Also resolves whether to add the project MCP entry (skip it
|
|
6
|
+
// when dotcontext is already configured globally in ~/.claude.json).
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
|
|
11
|
+
// package.json script -> sensor (only seeded if the script exists in the project).
|
|
12
|
+
const SCRIPT_SENSORS = [
|
|
13
|
+
{ script: 'typecheck', id: 'typecheck', name: 'Typecheck', severity: 'critical' },
|
|
14
|
+
{ script: 'test', id: 'tests', name: 'Tests', severity: 'critical' },
|
|
15
|
+
{ script: 'lint', id: 'lint', name: 'Lint', severity: 'warning' },
|
|
16
|
+
{ script: 'build', id: 'build', name: 'Build', severity: 'warning' },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export function renderSensorsJson(scripts = {}) {
|
|
20
|
+
const sensors = [
|
|
21
|
+
{
|
|
22
|
+
id: 'memory-validation',
|
|
23
|
+
name: 'CORE validation',
|
|
24
|
+
description: 'valida .brain/CORE.md (cap 25 + 3 seções, sem segredos) — wendkeep',
|
|
25
|
+
severity: 'critical',
|
|
26
|
+
command: 'npx wendkeep validate-memory',
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
for (const m of SCRIPT_SENSORS) {
|
|
30
|
+
if (scripts && scripts[m.script]) {
|
|
31
|
+
sensors.push({
|
|
32
|
+
id: m.id,
|
|
33
|
+
name: m.name,
|
|
34
|
+
description: `npm run ${m.script}`,
|
|
35
|
+
severity: m.severity,
|
|
36
|
+
command: `npm run ${m.script}`,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return `${JSON.stringify({ version: 1, source: 'manual', sensors }, null, 2)}\n`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readProjectScripts(projectPath) {
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(readFileSync(join(projectPath, 'package.json'), 'utf8')).scripts || {};
|
|
46
|
+
} catch {
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Create <project>/.context/config/sensors.json if absent (sensors auto-detected
|
|
52
|
+
// from the project's package.json scripts). Returns paths created.
|
|
53
|
+
export function seedDotcontext(projectPath) {
|
|
54
|
+
const created = [];
|
|
55
|
+
const configDir = join(projectPath, '.context', 'config');
|
|
56
|
+
mkdirSync(configDir, { recursive: true });
|
|
57
|
+
const sensorsPath = join(configDir, 'sensors.json');
|
|
58
|
+
if (!existsSync(sensorsPath)) {
|
|
59
|
+
writeFileSync(sensorsPath, renderSensorsJson(readProjectScripts(projectPath)), 'utf8');
|
|
60
|
+
created.push(sensorsPath);
|
|
61
|
+
}
|
|
62
|
+
return created;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// True if ~/.claude.json already declares a global dotcontext MCP server. Best-effort.
|
|
66
|
+
export function globalHasDotcontext(claudeJsonPath = join(homedir(), '.claude.json')) {
|
|
67
|
+
try {
|
|
68
|
+
const data = JSON.parse(readFileSync(claudeJsonPath, 'utf8'));
|
|
69
|
+
return !!data?.mcpServers?.dotcontext;
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Whether to SKIP the project-scoped dotcontext MCP entry.
|
|
76
|
+
// 'none' -> always skip
|
|
77
|
+
// 'project' -> never skip
|
|
78
|
+
// 'auto'/-- -> skip iff dotcontext is already global (avoid a duplicate server)
|
|
79
|
+
export function resolveDotcontextSkipMcp(flag, globalHas) {
|
|
80
|
+
if (flag === 'none') return true;
|
|
81
|
+
if (flag === 'project') return false;
|
|
82
|
+
return !!globalHas;
|
|
83
|
+
}
|
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,8 @@ import {
|
|
|
19
18
|
companionSettingsPatch,
|
|
20
19
|
companionMcpPatch,
|
|
21
20
|
companionHookSpecs,
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
cavemanInstallCommand,
|
|
22
|
+
DOTCONTEXT_GITIGNORE,
|
|
24
23
|
} from './taxonomy.mjs';
|
|
25
24
|
import { renderVaultReadme } from './vault-readme.mjs';
|
|
26
25
|
import { canInteractiveSelect, selectCompanionsInteractive } from './companion-select.mjs';
|
|
@@ -31,6 +30,9 @@ import {
|
|
|
31
30
|
graphColorGroups,
|
|
32
31
|
mergeGraphColorGroups,
|
|
33
32
|
} from './vault-theme.mjs';
|
|
33
|
+
import { renderCoreSkeleton, renderCompactionProtocol } from './validate-core.mjs';
|
|
34
|
+
import { seedDefinitions } from './sync-defs.mjs';
|
|
35
|
+
import { seedDotcontext, globalHasDotcontext, resolveDotcontextSkipMcp } from './dotcontext-seed.mjs';
|
|
34
36
|
|
|
35
37
|
function parseArgs(argv) {
|
|
36
38
|
const args = { mcp: true, yes: false, force: false };
|
|
@@ -43,6 +45,10 @@ function parseArgs(argv) {
|
|
|
43
45
|
else if (a === '--force') args.force = true;
|
|
44
46
|
else if (a === '--no-companions') args.noCompanions = true;
|
|
45
47
|
else if (a === '--no-colors') args.noColors = true;
|
|
48
|
+
else if (a === '--dotcontext-mcp') args.dotcontextMcp = argv[++i];
|
|
49
|
+
else if (a.startsWith('--dotcontext-mcp=')) args.dotcontextMcp = a.slice(17);
|
|
50
|
+
else if (a === '--dotcontext-hooks') args.dotcontextHooks = argv[++i];
|
|
51
|
+
else if (a.startsWith('--dotcontext-hooks=')) args.dotcontextHooks = a.slice(19);
|
|
46
52
|
else if (a === '--companions') args.companions = argv[++i];
|
|
47
53
|
else if (a.startsWith('--companions=')) args.companions = a.slice(13);
|
|
48
54
|
else if (a.startsWith('--vault=')) args.vault = a.slice(8);
|
|
@@ -80,13 +86,19 @@ function backup(path) {
|
|
|
80
86
|
|
|
81
87
|
// --- merges -----------------------------------------------------------------
|
|
82
88
|
|
|
83
|
-
export function mergeSettings(existing, { vaultPath, withMcp, force, companions = [] }) {
|
|
89
|
+
export function mergeSettings(existing, { vaultPath, withMcp, force, companions = [], skipMcp = [], dotcontextHookLevel = 'full' }) {
|
|
84
90
|
const s = existing && typeof existing === 'object' ? { ...existing } : {};
|
|
85
91
|
s.hooks = { ...(s.hooks || {}) };
|
|
86
92
|
let added = 0;
|
|
87
|
-
// wendkeep's own session hooks + any companion-authored hooks
|
|
88
|
-
|
|
89
|
-
|
|
93
|
+
// wendkeep's own session hooks + any companion-authored hooks. Stable-sort by
|
|
94
|
+
// `order` (default 0) so higher-order companion hooks (e.g. dotcontext's order 100)
|
|
95
|
+
// fold AFTER wendkeep's own within each event. A spec may carry its own `command`
|
|
96
|
+
// (dotcontext dispatch) instead of a wendkeep hook `name`.
|
|
97
|
+
const allSpecs = [...SESSION_HOOKS, ...companionHookSpecs(companions, { dotcontextHookLevel })].sort(
|
|
98
|
+
(a, b) => (a.order ?? 0) - (b.order ?? 0),
|
|
99
|
+
);
|
|
100
|
+
for (const h of allSpecs) {
|
|
101
|
+
const command = h.command ?? hookCommand(h.name);
|
|
90
102
|
const groups = Array.isArray(s.hooks[h.event]) ? [...s.hooks[h.event]] : [];
|
|
91
103
|
const present = groups.some((g) => (g.hooks || []).some((x) => x.command === command));
|
|
92
104
|
if (present && !force) {
|
|
@@ -110,7 +122,7 @@ export function mergeSettings(existing, { vaultPath, withMcp, force, companions
|
|
|
110
122
|
s.enabledPlugins = { ...(s.enabledPlugins || {}), ...cp.enabledPlugins };
|
|
111
123
|
}
|
|
112
124
|
|
|
113
|
-
const companionMcp = Object.keys(companionMcpPatch(companions));
|
|
125
|
+
const companionMcp = Object.keys(companionMcpPatch(companions, skipMcp));
|
|
114
126
|
if (withMcp || companionMcp.length) {
|
|
115
127
|
const set = new Set(Array.isArray(s.enabledMcpjsonServers) ? s.enabledMcpjsonServers : []);
|
|
116
128
|
if (withMcp) set.add(MCP_SERVER_KEY);
|
|
@@ -120,11 +132,11 @@ export function mergeSettings(existing, { vaultPath, withMcp, force, companions
|
|
|
120
132
|
return { settings: s, added };
|
|
121
133
|
}
|
|
122
134
|
|
|
123
|
-
export function mergeMcp(existing, { vaultPath, withVault = true, companions = [] }) {
|
|
135
|
+
export function mergeMcp(existing, { vaultPath, withVault = true, companions = [], skipMcp = [] }) {
|
|
124
136
|
const m = existing && typeof existing === 'object' ? { ...existing } : {};
|
|
125
137
|
m.mcpServers = { ...(m.mcpServers || {}) };
|
|
126
138
|
if (withVault) m.mcpServers[MCP_SERVER_KEY] = mcpServerEntry(vaultPath);
|
|
127
|
-
Object.assign(m.mcpServers, companionMcpPatch(companions));
|
|
139
|
+
Object.assign(m.mcpServers, companionMcpPatch(companions, skipMcp));
|
|
128
140
|
return m;
|
|
129
141
|
}
|
|
130
142
|
|
|
@@ -132,25 +144,16 @@ export function mergeMcp(existing, { vaultPath, withVault = true, companions = [
|
|
|
132
144
|
// script to a temp file and runs it as a FILE — piping to iex/bash leaves the
|
|
133
145
|
// script's self-path null and the installer aborts. Best-effort, fail-soft: the
|
|
134
146
|
// 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}`);
|
|
147
|
+
function runCavemanInstaller(log) {
|
|
148
|
+
const cmd = cavemanInstallCommand();
|
|
149
|
+
log(`\n caveman: running cross-agent installer (best-effort, Gemini skipped):\n ${cmd}`);
|
|
140
150
|
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' });
|
|
151
|
+
const r = spawnSync(cmd, { stdio: 'inherit', shell: true });
|
|
147
152
|
if (r.status !== 0) {
|
|
148
153
|
log(` [!] caveman installer exited ${r.status ?? '?'} — Claude Code plugin entry still wired; rerun manually if needed.`);
|
|
149
154
|
}
|
|
150
155
|
} catch (e) {
|
|
151
156
|
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
157
|
}
|
|
155
158
|
}
|
|
156
159
|
|
|
@@ -238,6 +241,16 @@ export async function runInit(argv) {
|
|
|
238
241
|
log(` companions : ${companions.length ? companions.join(', ') : 'none'}`);
|
|
239
242
|
log(` colors : ${args.noColors ? 'skipped' : 'wendkeep-colors (snippet + graph groups)'}\n`);
|
|
240
243
|
|
|
244
|
+
// dotcontext controls (only relevant when selected): hook level + MCP placement.
|
|
245
|
+
// Skip the project MCP entry when dotcontext is already global (avoids a duplicate
|
|
246
|
+
// server / version clash); --dotcontext-mcp project|none and --dotcontext-hooks
|
|
247
|
+
// full|light|none override.
|
|
248
|
+
const dotcontextHookLevel = args.dotcontextHooks || 'full';
|
|
249
|
+
const skipMcp = [];
|
|
250
|
+
if (companions.includes('dotcontext') && resolveDotcontextSkipMcp(args.dotcontextMcp, globalHasDotcontext())) {
|
|
251
|
+
skipMcp.push('dotcontext');
|
|
252
|
+
}
|
|
253
|
+
|
|
241
254
|
// 1. Vault taxonomy ---------------------------------------------------------
|
|
242
255
|
if (!existsSync(vaultPath)) mkdirSync(vaultPath, { recursive: true });
|
|
243
256
|
let created = 0;
|
|
@@ -255,19 +268,31 @@ export async function runInit(argv) {
|
|
|
255
268
|
writeFileSync(readmePath, renderVaultReadme({ projectName: basename(projectPath), vaultPath, withMcp: args.mcp }), 'utf8');
|
|
256
269
|
readmeNote = ', README.md created';
|
|
257
270
|
}
|
|
258
|
-
|
|
271
|
+
// Seed the curated memory layer (CORE.md) + the compaction-protocol doc. The
|
|
272
|
+
// DIGEST/index are auto-generated by the hooks; CORE is hand-curated, so we
|
|
273
|
+
// bootstrap it with the 3 required sections. Non-destructive.
|
|
274
|
+
const brainDir = join(vaultPath, '.brain');
|
|
275
|
+
mkdirSync(brainDir, { recursive: true });
|
|
276
|
+
const corePath = join(brainDir, 'CORE.md');
|
|
277
|
+
if (!existsSync(corePath)) writeFileSync(corePath, renderCoreSkeleton(), 'utf8');
|
|
278
|
+
const protoPath = join(brainDir, 'COMPACTION_PROTOCOL.md');
|
|
279
|
+
if (!existsSync(protoPath)) writeFileSync(protoPath, renderCompactionProtocol(), 'utf8');
|
|
280
|
+
// Seed the definitions layer (.brain/agents + .brain/skills): versioned source of
|
|
281
|
+
// truth for custom agents/skills. `wendkeep sync-defs` copies them to the agent dirs.
|
|
282
|
+
seedDefinitions(brainDir);
|
|
283
|
+
log(` [1/4] vault taxonomy: ${VAULT_FOLDERS.length} folders (${created} created)${readmeNote}, .brain seeded (CORE + agents/skills)`);
|
|
259
284
|
|
|
260
285
|
// 2. .claude/settings.json --------------------------------------------------
|
|
261
286
|
const settingsPath = join(projectPath, '.claude', 'settings.json');
|
|
262
287
|
const settingsRead = readJsonSafe(settingsPath);
|
|
263
288
|
if (!settingsRead.ok) {
|
|
264
289
|
// Unparseable existing file — never clobber. Drop a .new for manual merge.
|
|
265
|
-
const fresh = mergeSettings(null, { vaultPath, withMcp: args.mcp, force: true, companions }).settings;
|
|
290
|
+
const fresh = mergeSettings(null, { vaultPath, withMcp: args.mcp, force: true, companions, skipMcp, dotcontextHookLevel }).settings;
|
|
266
291
|
writeJson(`${settingsPath}.new`, fresh);
|
|
267
292
|
log(` [2/4] settings.json exists but is not valid JSON -> wrote ${settingsPath}.new (merge by hand)`);
|
|
268
293
|
} else {
|
|
269
294
|
const hadFile = settingsRead.data !== null;
|
|
270
|
-
const { settings, added } = mergeSettings(settingsRead.data, { vaultPath, withMcp: args.mcp, force: args.force, companions });
|
|
295
|
+
const { settings, added } = mergeSettings(settingsRead.data, { vaultPath, withMcp: args.mcp, force: args.force, companions, skipMcp, dotcontextHookLevel });
|
|
271
296
|
if (hadFile) backup(settingsPath);
|
|
272
297
|
writeJson(settingsPath, settings);
|
|
273
298
|
log(` [2/4] settings.json ${hadFile ? 'merged' : 'created'} (${added} hook(s) wired, OBSIDIAN_VAULT_PATH set${hadFile ? ', .bak saved' : ''})`);
|
|
@@ -275,11 +300,11 @@ export async function runInit(argv) {
|
|
|
275
300
|
|
|
276
301
|
// 3. .mcp.json --------------------------------------------------------------
|
|
277
302
|
// Written when mcpvault is wanted OR a selected companion ships an MCP server.
|
|
278
|
-
const companionMcp = companionMcpPatch(companions);
|
|
303
|
+
const companionMcp = companionMcpPatch(companions, skipMcp);
|
|
279
304
|
if (args.mcp || Object.keys(companionMcp).length) {
|
|
280
305
|
const mcpPath = join(projectPath, '.mcp.json');
|
|
281
306
|
const mcpRead = readJsonSafe(mcpPath);
|
|
282
|
-
const mcpOpts = { vaultPath, withVault: args.mcp, companions };
|
|
307
|
+
const mcpOpts = { vaultPath, withVault: args.mcp, companions, skipMcp };
|
|
283
308
|
const names = [args.mcp && MCP_SERVER_KEY, ...Object.keys(companionMcp)].filter(Boolean).join(', ');
|
|
284
309
|
if (!mcpRead.ok) {
|
|
285
310
|
writeJson(`${mcpPath}.new`, mergeMcp(null, mcpOpts));
|
|
@@ -304,7 +329,16 @@ export async function runInit(argv) {
|
|
|
304
329
|
// caveman has no MCP / agnostic-hook path: on non-Claude agents its skills come
|
|
305
330
|
// from its own cross-agent installer. Best-effort, fail-soft — the Claude Code
|
|
306
331
|
// plugin entry is already wired in settings.json regardless.
|
|
307
|
-
if (companions.includes('caveman'))
|
|
332
|
+
if (companions.includes('caveman')) runCavemanInstaller(log);
|
|
333
|
+
|
|
334
|
+
// dotcontext: MCP + lifecycle hooks are wired by mergeSettings/mergeMcp above;
|
|
335
|
+
// here we seed the versioned .context/config starter and print the gitignore note.
|
|
336
|
+
if (companions.includes('dotcontext')) {
|
|
337
|
+
const seeded = seedDotcontext(projectPath);
|
|
338
|
+
const mcpNote = skipMcp.includes('dotcontext') ? 'MCP global (project entry skipped)' : 'MCP (project)';
|
|
339
|
+
log(`\n dotcontext: ${mcpNote} + hooks(${dotcontextHookLevel}); .context/config seeded (${seeded.length} file(s)).`);
|
|
340
|
+
log(` [!] add to .gitignore (wendkeep não toca git): ${DOTCONTEXT_GITIGNORE.join(' ')}`);
|
|
341
|
+
}
|
|
308
342
|
|
|
309
343
|
log('\nNext steps:');
|
|
310
344
|
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,19 @@ 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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
113
|
+
// No MCP, no agnostic hook: on non-Claude agents, run its own installer
|
|
114
|
+
// (see cavemanInstallCommand — npx, non-interactive, Gemini excluded).
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: 'dotcontext',
|
|
118
|
+
label: 'dotcontext — orquestração de execução (PREVC + sensores + gate E→V)',
|
|
119
|
+
default: true,
|
|
120
|
+
// MCP-only (no Claude Code plugin). Agent-agnostic server, @latest surface.
|
|
121
|
+
mcp: { key: 'dotcontext', entry: { type: 'stdio', command: 'npx', args: ['-y', '@dotcontext/mcp@latest'], env: {} } },
|
|
122
|
+
// Lifecycle hooks via the pinned CLI; wired by wendkeep (single writer).
|
|
123
|
+
dotcontextHooks: { cliVersion: '1.1.1', source: 'claude-code' },
|
|
124
|
+
// Seed a starter .context/config (one neutral sensor).
|
|
125
|
+
contextSeed: true,
|
|
118
126
|
},
|
|
119
127
|
];
|
|
120
128
|
|
|
@@ -142,6 +150,7 @@ export function companionSettingsPatch(ids) {
|
|
|
142
150
|
for (const id of ids) {
|
|
143
151
|
const c = COMPANION_BY_ID[id];
|
|
144
152
|
if (!c) continue;
|
|
153
|
+
if (!c.marketplace || !c.plugin) continue; // MCP-only companions (e.g. dotcontext) have no plugin layer
|
|
145
154
|
extraKnownMarketplaces[c.id] = { source: c.marketplace };
|
|
146
155
|
enabledPlugins[`${c.plugin}@${c.id}`] = true;
|
|
147
156
|
}
|
|
@@ -149,9 +158,13 @@ export function companionSettingsPatch(ids) {
|
|
|
149
158
|
}
|
|
150
159
|
|
|
151
160
|
// The .mcp.json fragment (agent-agnostic layer) for MCP-capable companions only.
|
|
152
|
-
|
|
161
|
+
// `skip` omits ids whose MCP is already configured elsewhere (e.g. dotcontext set
|
|
162
|
+
// globally in ~/.claude.json — avoids a duplicate project-scoped server).
|
|
163
|
+
export function companionMcpPatch(ids, skip = []) {
|
|
164
|
+
const skipSet = new Set(skip);
|
|
153
165
|
const servers = {};
|
|
154
166
|
for (const id of ids) {
|
|
167
|
+
if (skipSet.has(id)) continue;
|
|
155
168
|
const c = COMPANION_BY_ID[id];
|
|
156
169
|
if (c?.mcp) servers[c.mcp.key] = c.mcp.entry;
|
|
157
170
|
}
|
|
@@ -161,7 +174,10 @@ export function companionMcpPatch(ids) {
|
|
|
161
174
|
// SessionStart hook specs wendkeep must author for companions that lack a native
|
|
162
175
|
// one (only Understand-Anything's domain-graph injector today). Same shape as
|
|
163
176
|
// SESSION_HOOKS so the same merge logic wires them.
|
|
164
|
-
|
|
177
|
+
// `dotcontextHookLevel`: 'full' (SessionStart+Stop+PostToolUse), 'light' (drops the
|
|
178
|
+
// per-tool PostToolUse trace hook — cuts latency when other PostToolUse hooks exist),
|
|
179
|
+
// or 'none' (MCP only, no lifecycle hooks).
|
|
180
|
+
export function companionHookSpecs(ids, { dotcontextHookLevel = 'full' } = {}) {
|
|
165
181
|
const specs = [];
|
|
166
182
|
for (const id of ids) {
|
|
167
183
|
const c = COMPANION_BY_ID[id];
|
|
@@ -171,28 +187,54 @@ export function companionHookSpecs(ids) {
|
|
|
171
187
|
matcher: null,
|
|
172
188
|
name: c.wendkeepHook,
|
|
173
189
|
timeout: 15,
|
|
190
|
+
order: 0,
|
|
174
191
|
statusMessage: `wendkeep: ${c.id} domain graph`,
|
|
175
192
|
});
|
|
176
193
|
}
|
|
194
|
+
// dotcontext: lifecycle hooks (SessionStart last + Stop + PostToolUse) that
|
|
195
|
+
// dispatch to its pinned CLI. They carry an explicit `command` (not a wendkeep
|
|
196
|
+
// hook name) and order 100 so they fold AFTER wendkeep's own session hooks.
|
|
197
|
+
if (c?.dotcontextHooks && dotcontextHookLevel !== 'none') {
|
|
198
|
+
const command = dotcontextHookCommand(c.dotcontextHooks.cliVersion, c.dotcontextHooks.source);
|
|
199
|
+
const base = { command, timeout: 8, order: 100, statusMessage: `${c.id}: harness hook` };
|
|
200
|
+
specs.push({ event: 'SessionStart', matcher: null, ...base });
|
|
201
|
+
specs.push({ event: 'Stop', matcher: null, ...base });
|
|
202
|
+
if (dotcontextHookLevel === 'full') {
|
|
203
|
+
specs.push({ event: 'PostToolUse', matcher: 'Write|Edit|Bash', ...base });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
177
206
|
}
|
|
178
207
|
return specs;
|
|
179
208
|
}
|
|
180
209
|
|
|
181
|
-
// The
|
|
182
|
-
export function
|
|
183
|
-
|
|
184
|
-
return platform === 'win32' ? ps1 : sh;
|
|
210
|
+
// The pinned dotcontext CLI hook-dispatch command (runs on each lifecycle event).
|
|
211
|
+
export function dotcontextHookCommand(cliVersion, source) {
|
|
212
|
+
return `npx -y @dotcontext/cli@${cliVersion} hook dispatch --source ${source}`;
|
|
185
213
|
}
|
|
186
214
|
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
215
|
+
// .context/ subpaths that should be gitignored (runtime/cache/regenerable). wendkeep
|
|
216
|
+
// does not touch git — init prints these as a note for the user to add.
|
|
217
|
+
export const DOTCONTEXT_GITIGNORE = [
|
|
218
|
+
'.context/runtime/',
|
|
219
|
+
'.context/cache/',
|
|
220
|
+
'.context/logs/',
|
|
221
|
+
'.context/plans/',
|
|
222
|
+
'.context/agents/',
|
|
223
|
+
'.context/skills/',
|
|
224
|
+
];
|
|
225
|
+
|
|
226
|
+
// Agents wendkeep installs caveman into via its cross-agent installer. Gemini is
|
|
227
|
+
// excluded on purpose: its CLI rejects caveman's agent tool names and crashes
|
|
228
|
+
// (libuv assertion) mid-install. caveman has no --exclude flag — only an --only
|
|
229
|
+
// allow-list — so we enumerate the agents we want by their stable slugs.
|
|
230
|
+
export const CAVEMAN_AGENTS = ['claude', 'codex', 'cursor', 'copilot', 'amp', 'antigravity'];
|
|
231
|
+
|
|
232
|
+
// Non-interactive command installing caveman to CAVEMAN_AGENTS (never Gemini). Runs
|
|
233
|
+
// the published installer directly via npx (no install.ps1, so no $PSCommandPath
|
|
234
|
+
// issue). Returned as a shell string; the caller spawns it with shell: true.
|
|
235
|
+
export function cavemanInstallCommand(agents = CAVEMAN_AGENTS) {
|
|
236
|
+
const only = agents.flatMap((a) => ['--only', a]).join(' ');
|
|
237
|
+
return `npx -y github:JuliusBrussee/caveman -- --non-interactive ${only}`;
|
|
196
238
|
}
|
|
197
239
|
|
|
198
240
|
// 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
|
+
}
|