wendkeep 0.1.0 → 0.2.0

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 CHANGED
@@ -35,12 +35,15 @@ npx wendkeep init
35
35
 
36
36
  `wendkeep init` is interactive and **idempotent**. It will:
37
37
 
38
- 1. Create the vault folder taxonomy (default vault: `<project>/.wendkeep-vault`, override with `--vault`).
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 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.)
41
42
 
42
43
  ```bash
43
- npx wendkeep init --vault "~/vaults/work" --project . --yes # non-interactive
44
+ npx wendkeep init --vault "~/vaults/work" --project . --yes # non-interactive (companions: context-mode)
45
+ npx wendkeep init --companions "context-mode,caveman" --yes
46
+ npx wendkeep init --no-companions --yes
44
47
  ```
45
48
 
46
49
  Then open the vault in Obsidian, send a test prompt in your agent, and confirm a note appears under `02-Sessões/…`.
package/bin/wendkeep.mjs CHANGED
@@ -26,9 +26,12 @@ const HELP = `wendkeep ${version()} — capture AI coding agent sessions into yo
26
26
 
27
27
  Usage:
28
28
  wendkeep init [options] Set up wendkeep in a project (cross-platform).
29
- --vault <path> Obsidian vault folder (default: <project>/.wendkeep-vault).
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).
34
+ --no-companions Skip companion plugins/MCP entirely.
32
35
  --yes, -y Non-interactive; accept defaults.
33
36
  --force Overwrite existing wendkeep config blocks.
34
37
 
@@ -30,8 +30,48 @@ export function writeHookOutput(payload = {}) {
30
30
  process.stdout.write(JSON.stringify(payload));
31
31
  }
32
32
 
33
+ // Resolve the vault and report WHERE the path came from, so callers can react to
34
+ // an unconfigured install instead of silently writing to the home fallback.
35
+ // env -> OBSIDIAN_VAULT_PATH (set by `wendkeep init`)
36
+ // payload -> obsidian_vault_path from the hook's JSON input
37
+ // default -> DEFAULT_VAULT_BASE (~/wendkeep-vault) — a phantom vault nobody opened
38
+ export function resolveVault(input = {}) {
39
+ if (process.env.OBSIDIAN_VAULT_PATH) {
40
+ return { base: process.env.OBSIDIAN_VAULT_PATH, source: 'env' };
41
+ }
42
+ if (input && input.obsidian_vault_path) {
43
+ return { base: input.obsidian_vault_path, source: 'payload' };
44
+ }
45
+ return { base: DEFAULT_VAULT_BASE, source: 'default' };
46
+ }
47
+
33
48
  export function getVaultBase(input = {}) {
34
- return process.env.OBSIDIAN_VAULT_PATH || input.obsidian_vault_path || DEFAULT_VAULT_BASE;
49
+ return resolveVault(input).base;
50
+ }
51
+
52
+ // Diagnostic logger. No-op unless WENDKEEP_DEBUG is set, so it never pollutes the
53
+ // stdout hook contract during normal runs but makes fail-open paths debuggable.
54
+ export function debugLog(...args) {
55
+ if (!process.env.WENDKEEP_DEBUG) return;
56
+ const text = args
57
+ .map((a) => (a && a.stack ? a.stack : String(a)))
58
+ .join(' ');
59
+ process.stderr.write(`[wendkeep] ${text}\n`);
60
+ }
61
+
62
+ // Warn loudly (stderr) when the vault resolved to the home fallback — i.e. neither
63
+ // OBSIDIAN_VAULT_PATH nor a payload path was provided. Without this the hooks write
64
+ // notes into ~/wendkeep-vault with zero signal that the install is misconfigured.
65
+ // Returns the resolution source so callers can branch if they want.
66
+ export function warnIfDefaultVault(input = {}) {
67
+ const { base, source } = resolveVault(input);
68
+ if (source === 'default') {
69
+ process.stderr.write(
70
+ `[wendkeep] WARNING: OBSIDIAN_VAULT_PATH não definido — gravando no fallback "${base}". ` +
71
+ 'Rode `wendkeep init` ou defina OBSIDIAN_VAULT_PATH apontando ao seu vault Obsidian.\n',
72
+ );
73
+ }
74
+ return source;
35
75
  }
36
76
 
37
77
  // Detecta o agente real que está executando o hook. Claude Code expõe
@@ -9,6 +9,8 @@ import {
9
9
  formatLocalIso,
10
10
  formatTime,
11
11
  getVaultBase,
12
+ warnIfDefaultVault,
13
+ debugLog,
12
14
  readControl,
13
15
  readHookInput,
14
16
  readSessionRegistry,
@@ -298,6 +300,7 @@ function outputActiveContext({ relPath, startedAt, vaultBase, message }) {
298
300
  function main() {
299
301
  const input = readHookInput();
300
302
  const vaultBase = getVaultBase(input);
303
+ warnIfDefaultVault(input);
301
304
  const now = new Date();
302
305
  const sessionId = sessionIdFromInput(input);
303
306
 
@@ -318,7 +321,9 @@ function main() {
318
321
  return;
319
322
  }
320
323
  }
321
- } catch {}
324
+ } catch (err) {
325
+ debugLog('session-ensure fast-path skipped:', err);
326
+ }
322
327
 
323
328
  const control = readControl(vaultBase);
324
329
 
@@ -10,6 +10,7 @@ import {
10
10
  formatLocalIso,
11
11
  formatTime,
12
12
  getVaultBase,
13
+ warnIfDefaultVault,
13
14
  providerMeta,
14
15
  readControl,
15
16
  readHookInput,
@@ -146,6 +147,7 @@ function buildAdditionalContext({ relPath, startedAt, vaultBase }) {
146
147
  function main() {
147
148
  const input = readHookInput();
148
149
  const vaultBase = getVaultBase(input);
150
+ warnIfDefaultVault(input);
149
151
  const now = new Date();
150
152
  const sessionId = input.session_id || input.sessionId || '';
151
153
  const control = readControl(vaultBase);
@@ -14,6 +14,7 @@ import {
14
14
  formatLocalIso,
15
15
  getNextAdrNumber,
16
16
  getVaultBase,
17
+ warnIfDefaultVault,
17
18
  listMarkdownFiles,
18
19
  readControl,
19
20
  readHookInput,
@@ -997,6 +998,7 @@ function main() {
997
998
  }
998
999
 
999
1000
  const vaultBase = getVaultBase(input);
1001
+ warnIfDefaultVault(input);
1000
1002
  const control = readControl(vaultBase);
1001
1003
  const transcriptPath = input.transcript_path || input.transcriptPath || '';
1002
1004
 
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ // understand-inject — SessionStart hook (agent-agnostic, run via `npx wendkeep hook
3
+ // understand-inject`). If the Understand-Anything domain graph has been generated
4
+ // (`.understand-anything/knowledge-graph.json` at the project root), inject a cheap
5
+ // slice of it into the session; otherwise stay silent. Never breaks the session.
6
+ import { readFileSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { pathToFileURL } from 'node:url';
9
+ import { readHookInput, writeHookOutput } from './obsidian-common.mjs';
10
+
11
+ const DOMAIN_TYPES = new Set(['domain', 'flow', 'step']);
12
+
13
+ // Build the injection text from the project's domain graph. Returns '' when the
14
+ // graph is absent/unreadable/empty so the caller can stay silent (fail-quiet).
15
+ // Reads only a bounded slice — the full knowledge-graph.json can be large.
16
+ export function buildUnderstandInjection(projectRoot, { maxNodes = 30 } = {}) {
17
+ const graphPath = join(projectRoot, '.understand-anything', 'knowledge-graph.json');
18
+ let graph;
19
+ try {
20
+ graph = JSON.parse(readFileSync(graphPath, 'utf8'));
21
+ } catch {
22
+ return '';
23
+ }
24
+ const nodes = Array.isArray(graph?.nodes) ? graph.nodes : [];
25
+ if (!nodes.length) return '';
26
+
27
+ const domainNodes = nodes.filter((n) => DOMAIN_TYPES.has(n?.type));
28
+ const source = domainNodes.length ? domainNodes : nodes;
29
+ const picked = source.slice(0, maxNodes);
30
+
31
+ const lines = picked.map((n) => {
32
+ const title = n?.name || n?.title || n?.id || '(sem nome)';
33
+ const kind = n?.type ? `[${n.type}] ` : '';
34
+ const summary = String(n?.summary || n?.description || '')
35
+ .split('\n')[0]
36
+ .slice(0, 200);
37
+ return `- ${kind}${title}${summary ? ` — ${summary}` : ''}`;
38
+ });
39
+
40
+ const remaining = source.length - picked.length;
41
+ const more = remaining > 0
42
+ ? `*…+${remaining} nós. Grafo completo: .understand-anything/knowledge-graph.json*`
43
+ : '';
44
+
45
+ return [
46
+ '<understand_domain_graph>',
47
+ 'Domain-graph do projeto (Understand-Anything) — mapa de domínios/fluxos:',
48
+ ...lines,
49
+ more,
50
+ '</understand_domain_graph>',
51
+ ]
52
+ .filter(Boolean)
53
+ .join('\n');
54
+ }
55
+
56
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
57
+ try {
58
+ readHookInput(); // drain stdin (agent JSON); project root comes from cwd
59
+ const context = buildUnderstandInjection(process.cwd());
60
+ if (!context) {
61
+ writeHookOutput({});
62
+ } else {
63
+ writeHookOutput({
64
+ hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: context },
65
+ });
66
+ }
67
+ } catch (error) {
68
+ process.stderr.write(`[understand-inject] falhou: ${error.message}\n`);
69
+ writeHookOutput({});
70
+ }
71
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
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": {
@@ -17,7 +17,8 @@
17
17
  "node": ">=18"
18
18
  },
19
19
  "scripts": {
20
- "check": "node --check bin/wendkeep.mjs && node --check src/init.mjs && node --check src/doctor.mjs"
20
+ "check": "node --check bin/wendkeep.mjs && node --check src/init.mjs && node --check src/doctor.mjs",
21
+ "test": "node --test"
21
22
  },
22
23
  "keywords": [
23
24
  "claude-code",
package/src/init.mjs CHANGED
@@ -2,8 +2,9 @@
2
2
  // Creates the vault taxonomy, NON-DESTRUCTIVELY merges the session hooks +
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
+ import { spawnSync } from 'node:child_process';
5
6
  import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
6
- import { isAbsolute, join, resolve } from 'node:path';
7
+ import { basename, isAbsolute, join, resolve } from 'node:path';
7
8
  import { createInterface } from 'node:readline/promises';
8
9
  import {
9
10
  VAULT_FOLDERS,
@@ -11,7 +12,15 @@ import {
11
12
  MCP_SERVER_KEY,
12
13
  mcpServerEntry,
13
14
  hookCommand,
15
+ deriveVaultDirName,
16
+ COMPANIONS,
17
+ resolveCompanions,
18
+ companionSettingsPatch,
19
+ companionMcpPatch,
20
+ companionHookSpecs,
21
+ cavemanInstallerCommand,
14
22
  } from './taxonomy.mjs';
23
+ import { renderVaultReadme } from './vault-readme.mjs';
15
24
 
16
25
  function parseArgs(argv) {
17
26
  const args = { mcp: true, yes: false, force: false };
@@ -22,6 +31,9 @@ function parseArgs(argv) {
22
31
  else if (a === '--no-mcp') args.mcp = false;
23
32
  else if (a === '--yes' || a === '-y') args.yes = true;
24
33
  else if (a === '--force') args.force = true;
34
+ else if (a === '--no-companions') args.noCompanions = true;
35
+ else if (a === '--companions') args.companions = argv[++i];
36
+ else if (a.startsWith('--companions=')) args.companions = a.slice(13);
25
37
  else if (a.startsWith('--vault=')) args.vault = a.slice(8);
26
38
  else if (a.startsWith('--project=')) args.project = a.slice(10);
27
39
  }
@@ -57,11 +69,12 @@ function backup(path) {
57
69
 
58
70
  // --- merges -----------------------------------------------------------------
59
71
 
60
- function mergeSettings(existing, { vaultPath, withMcp, force }) {
72
+ export function mergeSettings(existing, { vaultPath, withMcp, force, companions = [] }) {
61
73
  const s = existing && typeof existing === 'object' ? { ...existing } : {};
62
74
  s.hooks = { ...(s.hooks || {}) };
63
75
  let added = 0;
64
- for (const h of SESSION_HOOKS) {
76
+ // wendkeep's own session hooks + any companion-authored hooks (e.g. understand-inject).
77
+ for (const h of [...SESSION_HOOKS, ...companionHookSpecs(companions)]) {
65
78
  const command = hookCommand(h.name);
66
79
  const groups = Array.isArray(s.hooks[h.event]) ? [...s.hooks[h.event]] : [];
67
80
  const present = groups.some((g) => (g.hooks || []).some((x) => x.command === command));
@@ -76,18 +89,31 @@ function mergeSettings(existing, { vaultPath, withMcp, force }) {
76
89
  added += 1;
77
90
  }
78
91
  s.env = { ...(s.env || {}), OBSIDIAN_VAULT_PATH: vaultPath };
79
- if (withMcp) {
92
+
93
+ // Claude Code plugin layer for the selected companions (additive, non-clobbering).
94
+ const cp = companionSettingsPatch(companions);
95
+ if (Object.keys(cp.extraKnownMarketplaces).length) {
96
+ s.extraKnownMarketplaces = { ...(s.extraKnownMarketplaces || {}), ...cp.extraKnownMarketplaces };
97
+ }
98
+ if (Object.keys(cp.enabledPlugins).length) {
99
+ s.enabledPlugins = { ...(s.enabledPlugins || {}), ...cp.enabledPlugins };
100
+ }
101
+
102
+ const companionMcp = Object.keys(companionMcpPatch(companions));
103
+ if (withMcp || companionMcp.length) {
80
104
  const set = new Set(Array.isArray(s.enabledMcpjsonServers) ? s.enabledMcpjsonServers : []);
81
- set.add(MCP_SERVER_KEY);
105
+ if (withMcp) set.add(MCP_SERVER_KEY);
106
+ for (const key of companionMcp) set.add(key);
82
107
  s.enabledMcpjsonServers = [...set];
83
108
  }
84
109
  return { settings: s, added };
85
110
  }
86
111
 
87
- function mergeMcp(existing, vaultPath) {
112
+ export function mergeMcp(existing, { vaultPath, withVault = true, companions = [] }) {
88
113
  const m = existing && typeof existing === 'object' ? { ...existing } : {};
89
114
  m.mcpServers = { ...(m.mcpServers || {}) };
90
- m.mcpServers[MCP_SERVER_KEY] = mcpServerEntry(vaultPath);
115
+ if (withVault) m.mcpServers[MCP_SERVER_KEY] = mcpServerEntry(vaultPath);
116
+ Object.assign(m.mcpServers, companionMcpPatch(companions));
91
117
  return m;
92
118
  }
93
119
 
@@ -100,7 +126,7 @@ export async function runInit(argv) {
100
126
 
101
127
  let vaultPath = args.vault;
102
128
  if (!vaultPath) {
103
- const fallback = join(projectPath, '.wendkeep-vault');
129
+ const fallback = join(projectPath, deriveVaultDirName(projectPath));
104
130
  if (process.stdin.isTTY && !args.yes) {
105
131
  const rl = createInterface({ input: process.stdin, output: process.stdout });
106
132
  const ans = (await rl.question(`Obsidian vault path [${fallback}]: `)).trim();
@@ -112,10 +138,31 @@ export async function runInit(argv) {
112
138
  }
113
139
  vaultPath = isAbsolute(vaultPath) ? vaultPath : resolve(projectPath, vaultPath);
114
140
 
141
+ // Companion plugins/MCP selection. --no-companions wins; --companions <csv> is
142
+ // explicit; an interactive TTY gets a multi-choice prompt (context-mode pre-checked);
143
+ // otherwise the non-interactive default (context-mode only).
144
+ let companions;
145
+ if (args.noCompanions) {
146
+ companions = [];
147
+ } else if (args.companions !== undefined) {
148
+ companions = resolveCompanions({ companionsFlag: args.companions });
149
+ } else if (process.stdin.isTTY && !args.yes) {
150
+ log('\nCompanions (plugins/MCP opcionais — context-mode é a principal):');
151
+ for (const c of COMPANIONS) log(` ${c.default ? '[x]' : '[ ]'} ${c.label}`);
152
+ const def = resolveCompanions({}).join(',');
153
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
154
+ const ans = (await rl.question(`Instalar quais? (vírgula) [${def}], "none" p/ nenhum: `)).trim();
155
+ rl.close();
156
+ companions = ans.toLowerCase() === 'none' ? [] : resolveCompanions({ companionsFlag: ans || def });
157
+ } else {
158
+ companions = resolveCompanions({});
159
+ }
160
+
115
161
  log('\nwendkeep init');
116
- log(` project : ${projectPath}`);
117
- log(` vault : ${vaultPath}`);
118
- log(` mcp : ${args.mcp ? 'mcpvault (wendkeep-vault)' : 'skipped'}\n`);
162
+ log(` project : ${projectPath}`);
163
+ log(` vault : ${vaultPath}`);
164
+ log(` mcp : ${args.mcp ? 'mcpvault (wendkeep-vault)' : 'skipped'}`);
165
+ log(` companions : ${companions.length ? companions.join(', ') : 'none'}\n`);
119
166
 
120
167
  // 1. Vault taxonomy ---------------------------------------------------------
121
168
  if (!existsSync(vaultPath)) mkdirSync(vaultPath, { recursive: true });
@@ -127,39 +174,66 @@ export async function runInit(argv) {
127
174
  created += 1;
128
175
  }
129
176
  }
130
- log(` [1/3] vault taxonomy: ${VAULT_FOLDERS.length} folders (${created} created)`);
177
+ // Generate a project-templated vault README (non-destructive: never clobber one).
178
+ const readmePath = join(vaultPath, 'README.md');
179
+ let readmeNote = '';
180
+ if (!existsSync(readmePath)) {
181
+ writeFileSync(readmePath, renderVaultReadme({ projectName: basename(projectPath), vaultPath, withMcp: args.mcp }), 'utf8');
182
+ readmeNote = ', README.md created';
183
+ }
184
+ log(` [1/3] vault taxonomy: ${VAULT_FOLDERS.length} folders (${created} created)${readmeNote}`);
131
185
 
132
186
  // 2. .claude/settings.json --------------------------------------------------
133
187
  const settingsPath = join(projectPath, '.claude', 'settings.json');
134
188
  const settingsRead = readJsonSafe(settingsPath);
135
189
  if (!settingsRead.ok) {
136
190
  // Unparseable existing file — never clobber. Drop a .new for manual merge.
137
- const fresh = mergeSettings(null, { vaultPath, withMcp: args.mcp, force: true }).settings;
191
+ const fresh = mergeSettings(null, { vaultPath, withMcp: args.mcp, force: true, companions }).settings;
138
192
  writeJson(`${settingsPath}.new`, fresh);
139
193
  log(` [2/3] settings.json exists but is not valid JSON -> wrote ${settingsPath}.new (merge by hand)`);
140
194
  } else {
141
195
  const hadFile = settingsRead.data !== null;
142
- const { settings, added } = mergeSettings(settingsRead.data, { vaultPath, withMcp: args.mcp, force: args.force });
196
+ const { settings, added } = mergeSettings(settingsRead.data, { vaultPath, withMcp: args.mcp, force: args.force, companions });
143
197
  if (hadFile) backup(settingsPath);
144
198
  writeJson(settingsPath, settings);
145
199
  log(` [2/3] settings.json ${hadFile ? 'merged' : 'created'} (${added} hook(s) wired, OBSIDIAN_VAULT_PATH set${hadFile ? ', .bak saved' : ''})`);
146
200
  }
147
201
 
148
202
  // 3. .mcp.json --------------------------------------------------------------
149
- if (args.mcp) {
203
+ // Written when mcpvault is wanted OR a selected companion ships an MCP server.
204
+ const companionMcp = companionMcpPatch(companions);
205
+ if (args.mcp || Object.keys(companionMcp).length) {
150
206
  const mcpPath = join(projectPath, '.mcp.json');
151
207
  const mcpRead = readJsonSafe(mcpPath);
208
+ const mcpOpts = { vaultPath, withVault: args.mcp, companions };
209
+ const names = [args.mcp && MCP_SERVER_KEY, ...Object.keys(companionMcp)].filter(Boolean).join(', ');
152
210
  if (!mcpRead.ok) {
153
- writeJson(`${mcpPath}.new`, mergeMcp(null, vaultPath));
211
+ writeJson(`${mcpPath}.new`, mergeMcp(null, mcpOpts));
154
212
  log(` [3/3] .mcp.json exists but is not valid JSON -> wrote ${mcpPath}.new (merge by hand)`);
155
213
  } else {
156
214
  const hadFile = mcpRead.data !== null;
157
215
  if (hadFile) backup(mcpPath);
158
- writeJson(mcpPath, mergeMcp(mcpRead.data, vaultPath));
159
- log(` [3/3] .mcp.json ${hadFile ? 'merged' : 'created'} (wendkeep-vault server${hadFile ? ', .bak saved' : ''})`);
216
+ writeJson(mcpPath, mergeMcp(mcpRead.data, mcpOpts));
217
+ log(` [3/3] .mcp.json ${hadFile ? 'merged' : 'created'} (${names}${hadFile ? ', .bak saved' : ''})`);
160
218
  }
161
219
  } else {
162
- log(' [3/3] .mcp.json skipped (--no-mcp)');
220
+ log(' [3/3] .mcp.json skipped (--no-mcp, no MCP companions)');
221
+ }
222
+
223
+ // caveman has no MCP / agnostic-hook path: on non-Claude agents its skills come
224
+ // from its own cross-agent installer. Best-effort, fail-soft — the Claude Code
225
+ // plugin entry is already wired in settings.json regardless.
226
+ if (companions.includes('caveman')) {
227
+ const cmd = cavemanInstallerCommand();
228
+ log(`\n caveman: running cross-agent installer (best-effort): ${cmd.command} ${cmd.args.join(' ')}`);
229
+ try {
230
+ const r = spawnSync(cmd.command, cmd.args, { stdio: 'inherit' });
231
+ if (r.status !== 0) {
232
+ log(` [!] caveman installer exited ${r.status ?? '?'} — Claude Code plugin entry still wired; rerun manually if needed.`);
233
+ }
234
+ } catch (e) {
235
+ log(` [!] caveman installer could not run (${e.code || e.message}) — Claude Code plugin entry still wired.`);
236
+ }
163
237
  }
164
238
 
165
239
  log('\nNext steps:');
package/src/taxonomy.mjs CHANGED
@@ -35,6 +35,7 @@ export const HOOK_FILES = [
35
35
  'brain-reindex.mjs',
36
36
  'session-backfill.mjs',
37
37
  'vault-health.mjs',
38
+ 'understand-inject.mjs',
38
39
  ];
39
40
 
40
41
  // Hook scripts that are safe to invoke directly via `wendkeep hook <name>`.
@@ -49,6 +50,7 @@ export const RUNNABLE_HOOKS = [
49
50
  'brain-recall',
50
51
  'brain-reindex',
51
52
  'vault-health',
53
+ 'understand-inject',
52
54
  ];
53
55
 
54
56
  // The MCP server entry wendkeep wires into .mcp.json so the agent can read/write the
@@ -74,3 +76,134 @@ export const SESSION_HOOKS = [
74
76
  export function hookCommand(name) {
75
77
  return `npx wendkeep hook ${name}`;
76
78
  }
79
+
80
+ // --- companion plugins / MCP --------------------------------------------------
81
+ // Optional tools wendkeep init can pin alongside the vault. Each is wired through
82
+ // the MOST agent-agnostic mechanism it supports; the Claude Code plugin entry
83
+ // (extraKnownMarketplaces + enabledPlugins) is an additive bonus, never the base.
84
+ // - mcp: .mcp.json server entry (works on any MCP-capable agent)
85
+ // - wendkeepHook: a wendkeep-authored hook (runs via `npx wendkeep hook`, any agent)
86
+ // - installer: cross-agent install script (used on non-Claude agents)
87
+ // marketplace/plugin shapes are verified against a real ~/.claude/settings.json.
88
+ export const COMPANIONS = [
89
+ {
90
+ id: 'context-mode',
91
+ label: 'context-mode — otimizador de contexto + memória FTS5 (principal)',
92
+ default: true,
93
+ marketplace: { source: 'git', url: 'https://github.com/mksglu/context-mode.git' },
94
+ plugin: 'context-mode',
95
+ // Agent-agnostic: MCP server, self-updating via unpinned npx.
96
+ mcp: { key: 'context-mode', entry: { type: 'stdio', command: 'npx', args: ['-y', 'context-mode'] } },
97
+ },
98
+ {
99
+ id: 'understand-anything',
100
+ label: 'Understand-Anything — domain-graph do projeto',
101
+ default: false,
102
+ marketplace: { source: 'github', repo: 'Egonex-AI/Understand-Anything' },
103
+ plugin: 'understand-anything',
104
+ // No native SessionStart: wendkeep authors a conditional domain-graph injector.
105
+ wendkeepHook: 'understand-inject',
106
+ },
107
+ {
108
+ id: 'caveman',
109
+ label: 'caveman — modo compressão de tokens',
110
+ default: false,
111
+ marketplace: { source: 'git', url: 'https://github.com/JuliusBrussee/caveman.git' },
112
+ plugin: 'caveman',
113
+ // No MCP, no agnostic hook: on non-Claude agents, run its own installer.
114
+ installer: {
115
+ sh: 'https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.sh',
116
+ ps1: 'https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.ps1',
117
+ },
118
+ },
119
+ ];
120
+
121
+ const COMPANION_BY_ID = Object.fromEntries(COMPANIONS.map((c) => [c.id, c]));
122
+
123
+ // Resolve the companion ids for the NON-interactive path (the interactive prompt
124
+ // supplies its own selection). --no-companions wins; an explicit flag selects the
125
+ // named ids (unknown dropped) in registry order; otherwise the defaults.
126
+ export function resolveCompanions({ companionsFlag, noCompanions } = {}) {
127
+ if (noCompanions) return [];
128
+ let wanted;
129
+ if (typeof companionsFlag === 'string' && companionsFlag.trim()) {
130
+ const set = new Set(companionsFlag.split(',').map((s) => s.trim()).filter(Boolean));
131
+ wanted = (id) => set.has(id);
132
+ } else {
133
+ wanted = (id) => COMPANION_BY_ID[id]?.default === true;
134
+ }
135
+ return COMPANIONS.filter((c) => wanted(c.id)).map((c) => c.id);
136
+ }
137
+
138
+ // The settings.json fragment (Claude Code plugin layer) for the selected companions.
139
+ export function companionSettingsPatch(ids) {
140
+ const extraKnownMarketplaces = {};
141
+ const enabledPlugins = {};
142
+ for (const id of ids) {
143
+ const c = COMPANION_BY_ID[id];
144
+ if (!c) continue;
145
+ extraKnownMarketplaces[c.id] = { source: c.marketplace };
146
+ enabledPlugins[`${c.plugin}@${c.id}`] = true;
147
+ }
148
+ return { extraKnownMarketplaces, enabledPlugins };
149
+ }
150
+
151
+ // The .mcp.json fragment (agent-agnostic layer) for MCP-capable companions only.
152
+ export function companionMcpPatch(ids) {
153
+ const servers = {};
154
+ for (const id of ids) {
155
+ const c = COMPANION_BY_ID[id];
156
+ if (c?.mcp) servers[c.mcp.key] = c.mcp.entry;
157
+ }
158
+ return servers;
159
+ }
160
+
161
+ // SessionStart hook specs wendkeep must author for companions that lack a native
162
+ // one (only Understand-Anything's domain-graph injector today). Same shape as
163
+ // SESSION_HOOKS so the same merge logic wires them.
164
+ export function companionHookSpecs(ids) {
165
+ const specs = [];
166
+ for (const id of ids) {
167
+ const c = COMPANION_BY_ID[id];
168
+ if (c?.wendkeepHook) {
169
+ specs.push({
170
+ event: 'SessionStart',
171
+ matcher: null,
172
+ name: c.wendkeepHook,
173
+ timeout: 15,
174
+ statusMessage: `wendkeep: ${c.id} domain graph`,
175
+ });
176
+ }
177
+ }
178
+ return specs;
179
+ }
180
+
181
+ // Build the platform-appropriate command to run caveman's own cross-agent installer
182
+ // (used on non-Claude agents — caveman has no MCP/agnostic-hook path). Pure: returns
183
+ // {command, args}; the caller decides whether to spawn it.
184
+ export function cavemanInstallerCommand(platform = process.platform) {
185
+ const { sh, ps1 } = COMPANION_BY_ID.caveman.installer;
186
+ if (platform === 'win32') {
187
+ return { command: 'powershell', args: ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', `irm ${ps1} | iex`] };
188
+ }
189
+ return { command: 'bash', args: ['-c', `curl -fsSL ${sh} | bash`] };
190
+ }
191
+
192
+ // Derive the default vault folder name from the project folder: `.<project>-vault`.
193
+ // Splits on both separators so it is correct regardless of host OS, sanitizes
194
+ // filesystem-unsafe characters, preserves case, and falls back to a stable name
195
+ // when the basename is empty (root paths). Pure + side-effect free.
196
+ export function deriveVaultDirName(projectPath) {
197
+ const base = String(projectPath || '')
198
+ .replace(/[\\/]+$/, '')
199
+ .split(/[\\/]/)
200
+ .pop() || '';
201
+ const clean = base
202
+ .replace(/^[.\s]+/, '') // drop leading dots/space so we never get `..name`
203
+ .replace(/[<>:"/\\|?*-]/g, '-') // FS-unsafe chars -> dash
204
+ .replace(/\s+/g, '-') // whitespace -> dash
205
+ .replace(/-+/g, '-') // collapse dash runs
206
+ .replace(/^-+|-+$/g, '') // trim edge dashes
207
+ .replace(/\.+$/, ''); // trim trailing dots
208
+ return clean ? `.${clean}-vault` : '.wendkeep-vault';
209
+ }
@@ -0,0 +1,73 @@
1
+ // Renders a neutral, project-templated README for a freshly created wendkeep vault.
2
+ // The structure table is derived from VAULT_FOLDERS so it can never drift from the
3
+ // folders `wendkeep init` actually creates.
4
+ import { VAULT_FOLDERS } from './taxonomy.mjs';
5
+
6
+ const FOLDER_DESCRIPTIONS = {
7
+ '00-Inbox': 'Notas rápidas, captura sem classificação',
8
+ '01-Projeto': 'Arquitetura, padrões, memória do projeto',
9
+ '02-Sessões': 'Uma nota por sessão de trabalho com o agente IA',
10
+ '03-Linear': 'Espelho de issues/tarefas do seu tracker (Linear, Jira, GitHub Issues…)',
11
+ '04-Decisões': 'Architecture Decision Records (ADRs)',
12
+ '05-Bugs': 'Bugs resolvidos com causa raiz documentada',
13
+ '06-Aprendizados': 'Lições extraídas das sessões',
14
+ Templates: 'Templates para novas notas',
15
+ '.brain': 'Memória canônica: CORE.md (curado) + DIGEST.md (auto), injetada no SessionStart',
16
+ };
17
+
18
+ function structureTable() {
19
+ const rows = VAULT_FOLDERS.map((f) => {
20
+ const desc = FOLDER_DESCRIPTIONS[f] || 'Notas do projeto';
21
+ return `| \`${f}/\` | ${desc} |`;
22
+ });
23
+ return ['| Pasta | Conteúdo |', '| --- | --- |', ...rows].join('\n');
24
+ }
25
+
26
+ export function renderVaultReadme({ projectName, vaultPath, withMcp = true }) {
27
+ const name = projectName || 'projeto';
28
+ const mcpIntro = withMcp ? ', e lida/escrita pelo MCP server **MCPVault**' : '';
29
+
30
+ const access = [
31
+ `- **Obsidian:** abra esta pasta com "Open folder as vault" → \`${vaultPath}\``,
32
+ ];
33
+ if (withMcp) {
34
+ access.push(
35
+ '- **Agente (MCP):** o servidor `wendkeep-vault` (MCPVault) é apontado para este vault\n' +
36
+ ' pelo `wendkeep init` (em `.mcp.json`), dando ao agente leitura/escrita das notas.',
37
+ );
38
+ }
39
+ access.push(
40
+ '- **Hooks:** `settings.json` chama `npx wendkeep hook <name>`; o vault é localizado\n' +
41
+ ' via a env `OBSIDIAN_VAULT_PATH` (também gravada pelo `wendkeep init`).',
42
+ );
43
+
44
+ return `# Vault Obsidian — ${name}
45
+
46
+ > Base de conhecimento de **${name}**, capturada automaticamente pelo wendkeep a
47
+ > partir das sessões dos agentes de código IA (**Claude Code** e **Codex**) via
48
+ > hooks de sessão${mcpIntro}.
49
+
50
+ ## Estrutura
51
+
52
+ ${structureTable()}
53
+
54
+ ## Acesso
55
+
56
+ ${access.join('\n')}
57
+
58
+ ## Regras
59
+
60
+ 1. Toda sessão de trabalho relevante gera nota em \`02-Sessões/\` (automático, pelos hooks).
61
+ 2. Decisões de arquitetura viram ADR em \`04-Decisões/\`.
62
+ 3. Bugs com causa raiz vão para \`05-Bugs/\`; lições para \`06-Aprendizados/\`.
63
+ 4. Memória canônica: \`.brain/CORE.md\` (curado) + \`.brain/DIGEST.md\` (auto) — injetados no SessionStart.
64
+ 5. Tags em frontmatter YAML, não inline.
65
+ 6. Wikilinks \`[[nota]]\` para conectar notas entre si.
66
+
67
+ ## Notas
68
+
69
+ - **Idioma das pastas:** os nomes são PT-BR (limitação conhecida; i18n no roadmap do wendkeep).
70
+ - **Versionamento:** as notas de sessão podem conter trechos de transcript — avalie
71
+ antes de commitar este vault em um repositório compartilhado.
72
+ `;
73
+ }