wendkeep 0.2.6 → 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 +2 -2
- package/bin/wendkeep.mjs +5 -2
- package/package.json +1 -1
- package/src/dotcontext-seed.mjs +83 -0
- package/src/init.mjs +42 -11
- package/src/taxonomy.mjs +50 -2
package/README.md
CHANGED
|
@@ -38,13 +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
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
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/`).
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
|
-
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)
|
|
48
48
|
npx wendkeep init --companions "context-mode,caveman" --yes
|
|
49
49
|
npx wendkeep init --no-companions --yes
|
|
50
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
|
|
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
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
companionMcpPatch,
|
|
20
20
|
companionHookSpecs,
|
|
21
21
|
cavemanInstallCommand,
|
|
22
|
+
DOTCONTEXT_GITIGNORE,
|
|
22
23
|
} from './taxonomy.mjs';
|
|
23
24
|
import { renderVaultReadme } from './vault-readme.mjs';
|
|
24
25
|
import { canInteractiveSelect, selectCompanionsInteractive } from './companion-select.mjs';
|
|
@@ -31,6 +32,7 @@ import {
|
|
|
31
32
|
} from './vault-theme.mjs';
|
|
32
33
|
import { renderCoreSkeleton, renderCompactionProtocol } from './validate-core.mjs';
|
|
33
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
|
|
|
@@ -229,6 +241,16 @@ export async function runInit(argv) {
|
|
|
229
241
|
log(` companions : ${companions.length ? companions.join(', ') : 'none'}`);
|
|
230
242
|
log(` colors : ${args.noColors ? 'skipped' : 'wendkeep-colors (snippet + graph groups)'}\n`);
|
|
231
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
|
+
|
|
232
254
|
// 1. Vault taxonomy ---------------------------------------------------------
|
|
233
255
|
if (!existsSync(vaultPath)) mkdirSync(vaultPath, { recursive: true });
|
|
234
256
|
let created = 0;
|
|
@@ -265,12 +287,12 @@ export async function runInit(argv) {
|
|
|
265
287
|
const settingsRead = readJsonSafe(settingsPath);
|
|
266
288
|
if (!settingsRead.ok) {
|
|
267
289
|
// Unparseable existing file — never clobber. Drop a .new for manual merge.
|
|
268
|
-
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;
|
|
269
291
|
writeJson(`${settingsPath}.new`, fresh);
|
|
270
292
|
log(` [2/4] settings.json exists but is not valid JSON -> wrote ${settingsPath}.new (merge by hand)`);
|
|
271
293
|
} else {
|
|
272
294
|
const hadFile = settingsRead.data !== null;
|
|
273
|
-
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 });
|
|
274
296
|
if (hadFile) backup(settingsPath);
|
|
275
297
|
writeJson(settingsPath, settings);
|
|
276
298
|
log(` [2/4] settings.json ${hadFile ? 'merged' : 'created'} (${added} hook(s) wired, OBSIDIAN_VAULT_PATH set${hadFile ? ', .bak saved' : ''})`);
|
|
@@ -278,11 +300,11 @@ export async function runInit(argv) {
|
|
|
278
300
|
|
|
279
301
|
// 3. .mcp.json --------------------------------------------------------------
|
|
280
302
|
// Written when mcpvault is wanted OR a selected companion ships an MCP server.
|
|
281
|
-
const companionMcp = companionMcpPatch(companions);
|
|
303
|
+
const companionMcp = companionMcpPatch(companions, skipMcp);
|
|
282
304
|
if (args.mcp || Object.keys(companionMcp).length) {
|
|
283
305
|
const mcpPath = join(projectPath, '.mcp.json');
|
|
284
306
|
const mcpRead = readJsonSafe(mcpPath);
|
|
285
|
-
const mcpOpts = { vaultPath, withVault: args.mcp, companions };
|
|
307
|
+
const mcpOpts = { vaultPath, withVault: args.mcp, companions, skipMcp };
|
|
286
308
|
const names = [args.mcp && MCP_SERVER_KEY, ...Object.keys(companionMcp)].filter(Boolean).join(', ');
|
|
287
309
|
if (!mcpRead.ok) {
|
|
288
310
|
writeJson(`${mcpPath}.new`, mergeMcp(null, mcpOpts));
|
|
@@ -309,6 +331,15 @@ export async function runInit(argv) {
|
|
|
309
331
|
// plugin entry is already wired in settings.json regardless.
|
|
310
332
|
if (companions.includes('caveman')) runCavemanInstaller(log);
|
|
311
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
|
+
}
|
|
342
|
+
|
|
312
343
|
log('\nNext steps:');
|
|
313
344
|
log(` 1. Open the vault in Obsidian: "Open folder as vault" -> ${vaultPath}`);
|
|
314
345
|
log(' 2. Make sure wendkeep is installed where the agent runs (npm i -D wendkeep, or -g).');
|
package/src/taxonomy.mjs
CHANGED
|
@@ -113,6 +113,17 @@ export const COMPANIONS = [
|
|
|
113
113
|
// No MCP, no agnostic hook: on non-Claude agents, run its own installer
|
|
114
114
|
// (see cavemanInstallCommand — npx, non-interactive, Gemini excluded).
|
|
115
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,
|
|
126
|
+
},
|
|
116
127
|
];
|
|
117
128
|
|
|
118
129
|
const COMPANION_BY_ID = Object.fromEntries(COMPANIONS.map((c) => [c.id, c]));
|
|
@@ -139,6 +150,7 @@ export function companionSettingsPatch(ids) {
|
|
|
139
150
|
for (const id of ids) {
|
|
140
151
|
const c = COMPANION_BY_ID[id];
|
|
141
152
|
if (!c) continue;
|
|
153
|
+
if (!c.marketplace || !c.plugin) continue; // MCP-only companions (e.g. dotcontext) have no plugin layer
|
|
142
154
|
extraKnownMarketplaces[c.id] = { source: c.marketplace };
|
|
143
155
|
enabledPlugins[`${c.plugin}@${c.id}`] = true;
|
|
144
156
|
}
|
|
@@ -146,9 +158,13 @@ export function companionSettingsPatch(ids) {
|
|
|
146
158
|
}
|
|
147
159
|
|
|
148
160
|
// The .mcp.json fragment (agent-agnostic layer) for MCP-capable companions only.
|
|
149
|
-
|
|
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);
|
|
150
165
|
const servers = {};
|
|
151
166
|
for (const id of ids) {
|
|
167
|
+
if (skipSet.has(id)) continue;
|
|
152
168
|
const c = COMPANION_BY_ID[id];
|
|
153
169
|
if (c?.mcp) servers[c.mcp.key] = c.mcp.entry;
|
|
154
170
|
}
|
|
@@ -158,7 +174,10 @@ export function companionMcpPatch(ids) {
|
|
|
158
174
|
// SessionStart hook specs wendkeep must author for companions that lack a native
|
|
159
175
|
// one (only Understand-Anything's domain-graph injector today). Same shape as
|
|
160
176
|
// SESSION_HOOKS so the same merge logic wires them.
|
|
161
|
-
|
|
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' } = {}) {
|
|
162
181
|
const specs = [];
|
|
163
182
|
for (const id of ids) {
|
|
164
183
|
const c = COMPANION_BY_ID[id];
|
|
@@ -168,13 +187,42 @@ export function companionHookSpecs(ids) {
|
|
|
168
187
|
matcher: null,
|
|
169
188
|
name: c.wendkeepHook,
|
|
170
189
|
timeout: 15,
|
|
190
|
+
order: 0,
|
|
171
191
|
statusMessage: `wendkeep: ${c.id} domain graph`,
|
|
172
192
|
});
|
|
173
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
|
+
}
|
|
174
206
|
}
|
|
175
207
|
return specs;
|
|
176
208
|
}
|
|
177
209
|
|
|
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}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
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
|
+
|
|
178
226
|
// Agents wendkeep installs caveman into via its cross-agent installer. Gemini is
|
|
179
227
|
// excluded on purpose: its CLI rejects caveman's agent tool names and crashes
|
|
180
228
|
// (libuv assertion) mid-install. caveman has no --exclude flag — only an --only
|