wendkeep 0.1.0 → 0.2.1
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 +5 -2
- package/bin/wendkeep.mjs +4 -1
- package/hooks/obsidian-common.mjs +41 -1
- package/hooks/session-ensure.mjs +6 -1
- package/hooks/session-start.mjs +2 -0
- package/hooks/session-stop.mjs +2 -0
- package/hooks/understand-inject.mjs +71 -0
- package/package.json +3 -2
- package/src/companion-select.mjs +111 -0
- package/src/init.mjs +102 -20
- package/src/taxonomy.mjs +133 -0
- package/src/vault-readme.mjs +73 -0
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
|
|
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
|
|
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
|
|
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
|
package/hooks/session-ensure.mjs
CHANGED
|
@@ -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
|
|
package/hooks/session-start.mjs
CHANGED
|
@@ -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);
|
package/hooks/session-stop.mjs
CHANGED
|
@@ -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
|
|
3
|
+
"version": "0.2.1",
|
|
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",
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Interactive companion checkbox selector for `wendkeep init`.
|
|
2
|
+
// The state machine (initial/reduce/render/mapKey) is pure and tested; the raw-TTY
|
|
3
|
+
// event loop is a thin shell over it, with a text fallback when raw mode is absent.
|
|
4
|
+
import readline from 'node:readline';
|
|
5
|
+
|
|
6
|
+
const HINT = 'Espaço marca/desmarca · ↑/↓ move · a=todos · n=nenhum · Enter confirma';
|
|
7
|
+
|
|
8
|
+
export function initialCompanionMenu(companions) {
|
|
9
|
+
return {
|
|
10
|
+
items: companions.map((c) => ({ id: c.id, label: c.label, checked: !!c.default })),
|
|
11
|
+
cursor: 0,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function reduceCompanionMenu(state, action) {
|
|
16
|
+
const n = state.items.length;
|
|
17
|
+
switch (action) {
|
|
18
|
+
case 'up':
|
|
19
|
+
return { ...state, cursor: (state.cursor - 1 + n) % n };
|
|
20
|
+
case 'down':
|
|
21
|
+
return { ...state, cursor: (state.cursor + 1) % n };
|
|
22
|
+
case 'space':
|
|
23
|
+
return {
|
|
24
|
+
...state,
|
|
25
|
+
items: state.items.map((it, i) => (i === state.cursor ? { ...it, checked: !it.checked } : it)),
|
|
26
|
+
};
|
|
27
|
+
case 'all':
|
|
28
|
+
return { ...state, items: state.items.map((it) => ({ ...it, checked: true })) };
|
|
29
|
+
case 'none':
|
|
30
|
+
return { ...state, items: state.items.map((it) => ({ ...it, checked: false })) };
|
|
31
|
+
case 'enter':
|
|
32
|
+
return { ...state, done: true, selected: state.items.filter((it) => it.checked).map((it) => it.id) };
|
|
33
|
+
default:
|
|
34
|
+
return state;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function renderCompanionMenu(state) {
|
|
39
|
+
const lines = [`Companions — ${HINT}:`];
|
|
40
|
+
state.items.forEach((it, i) => {
|
|
41
|
+
const cursor = i === state.cursor ? '>' : ' ';
|
|
42
|
+
const box = it.checked ? '[x]' : '[ ]';
|
|
43
|
+
lines.push(`${cursor} ${box} ${it.label}`);
|
|
44
|
+
});
|
|
45
|
+
return lines.join('\n');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function mapKey(str, key = {}) {
|
|
49
|
+
const name = key.name;
|
|
50
|
+
if (name === 'up' || name === 'k') return 'up';
|
|
51
|
+
if (name === 'down' || name === 'j') return 'down';
|
|
52
|
+
if (name === 'space' || str === ' ') return 'space';
|
|
53
|
+
if (name === 'return' || name === 'enter') return 'enter';
|
|
54
|
+
if (str === 'a') return 'all';
|
|
55
|
+
if (str === 'n') return 'none';
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// True when stdin/stdout can drive an interactive raw-mode menu.
|
|
60
|
+
export function canInteractiveSelect(input = process.stdin, output = process.stdout) {
|
|
61
|
+
return !!(input && output && input.isTTY && output.isTTY && typeof input.setRawMode === 'function');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Run the raw-TTY checkbox menu; resolves with the selected ids. Caller must check
|
|
65
|
+
// canInteractiveSelect() first (otherwise use the text fallback).
|
|
66
|
+
export function selectCompanionsInteractive(companions, { input = process.stdin, output = process.stdout } = {}) {
|
|
67
|
+
return new Promise((resolve) => {
|
|
68
|
+
let state = initialCompanionMenu(companions);
|
|
69
|
+
let drawnLines = 0;
|
|
70
|
+
|
|
71
|
+
readline.emitKeypressEvents(input);
|
|
72
|
+
input.setRawMode(true);
|
|
73
|
+
input.resume();
|
|
74
|
+
|
|
75
|
+
const draw = () => {
|
|
76
|
+
if (drawnLines) {
|
|
77
|
+
readline.moveCursor(output, 0, -drawnLines);
|
|
78
|
+
readline.clearScreenDown(output);
|
|
79
|
+
}
|
|
80
|
+
const text = renderCompanionMenu(state);
|
|
81
|
+
output.write(`${text}\n`);
|
|
82
|
+
drawnLines = text.split('\n').length;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const cleanup = () => {
|
|
86
|
+
input.removeListener('keypress', onKey);
|
|
87
|
+
if (typeof input.setRawMode === 'function') input.setRawMode(false);
|
|
88
|
+
input.pause();
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const onKey = (str, key = {}) => {
|
|
92
|
+
if (key.ctrl && key.name === 'c') {
|
|
93
|
+
cleanup();
|
|
94
|
+
output.write('\n');
|
|
95
|
+
process.exit(130);
|
|
96
|
+
}
|
|
97
|
+
const action = mapKey(str, key);
|
|
98
|
+
if (!action) return;
|
|
99
|
+
state = reduceCompanionMenu(state, action);
|
|
100
|
+
if (state.done) {
|
|
101
|
+
cleanup();
|
|
102
|
+
resolve(state.selected);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
draw();
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
input.on('keypress', onKey);
|
|
109
|
+
draw();
|
|
110
|
+
});
|
|
111
|
+
}
|
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,16 @@ 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';
|
|
24
|
+
import { canInteractiveSelect, selectCompanionsInteractive } from './companion-select.mjs';
|
|
15
25
|
|
|
16
26
|
function parseArgs(argv) {
|
|
17
27
|
const args = { mcp: true, yes: false, force: false };
|
|
@@ -22,6 +32,9 @@ function parseArgs(argv) {
|
|
|
22
32
|
else if (a === '--no-mcp') args.mcp = false;
|
|
23
33
|
else if (a === '--yes' || a === '-y') args.yes = true;
|
|
24
34
|
else if (a === '--force') args.force = true;
|
|
35
|
+
else if (a === '--no-companions') args.noCompanions = true;
|
|
36
|
+
else if (a === '--companions') args.companions = argv[++i];
|
|
37
|
+
else if (a.startsWith('--companions=')) args.companions = a.slice(13);
|
|
25
38
|
else if (a.startsWith('--vault=')) args.vault = a.slice(8);
|
|
26
39
|
else if (a.startsWith('--project=')) args.project = a.slice(10);
|
|
27
40
|
}
|
|
@@ -57,11 +70,12 @@ function backup(path) {
|
|
|
57
70
|
|
|
58
71
|
// --- merges -----------------------------------------------------------------
|
|
59
72
|
|
|
60
|
-
function mergeSettings(existing, { vaultPath, withMcp, force }) {
|
|
73
|
+
export function mergeSettings(existing, { vaultPath, withMcp, force, companions = [] }) {
|
|
61
74
|
const s = existing && typeof existing === 'object' ? { ...existing } : {};
|
|
62
75
|
s.hooks = { ...(s.hooks || {}) };
|
|
63
76
|
let added = 0;
|
|
64
|
-
|
|
77
|
+
// wendkeep's own session hooks + any companion-authored hooks (e.g. understand-inject).
|
|
78
|
+
for (const h of [...SESSION_HOOKS, ...companionHookSpecs(companions)]) {
|
|
65
79
|
const command = hookCommand(h.name);
|
|
66
80
|
const groups = Array.isArray(s.hooks[h.event]) ? [...s.hooks[h.event]] : [];
|
|
67
81
|
const present = groups.some((g) => (g.hooks || []).some((x) => x.command === command));
|
|
@@ -76,18 +90,31 @@ function mergeSettings(existing, { vaultPath, withMcp, force }) {
|
|
|
76
90
|
added += 1;
|
|
77
91
|
}
|
|
78
92
|
s.env = { ...(s.env || {}), OBSIDIAN_VAULT_PATH: vaultPath };
|
|
79
|
-
|
|
93
|
+
|
|
94
|
+
// Claude Code plugin layer for the selected companions (additive, non-clobbering).
|
|
95
|
+
const cp = companionSettingsPatch(companions);
|
|
96
|
+
if (Object.keys(cp.extraKnownMarketplaces).length) {
|
|
97
|
+
s.extraKnownMarketplaces = { ...(s.extraKnownMarketplaces || {}), ...cp.extraKnownMarketplaces };
|
|
98
|
+
}
|
|
99
|
+
if (Object.keys(cp.enabledPlugins).length) {
|
|
100
|
+
s.enabledPlugins = { ...(s.enabledPlugins || {}), ...cp.enabledPlugins };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const companionMcp = Object.keys(companionMcpPatch(companions));
|
|
104
|
+
if (withMcp || companionMcp.length) {
|
|
80
105
|
const set = new Set(Array.isArray(s.enabledMcpjsonServers) ? s.enabledMcpjsonServers : []);
|
|
81
|
-
set.add(MCP_SERVER_KEY);
|
|
106
|
+
if (withMcp) set.add(MCP_SERVER_KEY);
|
|
107
|
+
for (const key of companionMcp) set.add(key);
|
|
82
108
|
s.enabledMcpjsonServers = [...set];
|
|
83
109
|
}
|
|
84
110
|
return { settings: s, added };
|
|
85
111
|
}
|
|
86
112
|
|
|
87
|
-
function mergeMcp(existing, vaultPath) {
|
|
113
|
+
export function mergeMcp(existing, { vaultPath, withVault = true, companions = [] }) {
|
|
88
114
|
const m = existing && typeof existing === 'object' ? { ...existing } : {};
|
|
89
115
|
m.mcpServers = { ...(m.mcpServers || {}) };
|
|
90
|
-
m.mcpServers[MCP_SERVER_KEY] = mcpServerEntry(vaultPath);
|
|
116
|
+
if (withVault) m.mcpServers[MCP_SERVER_KEY] = mcpServerEntry(vaultPath);
|
|
117
|
+
Object.assign(m.mcpServers, companionMcpPatch(companions));
|
|
91
118
|
return m;
|
|
92
119
|
}
|
|
93
120
|
|
|
@@ -100,10 +127,10 @@ export async function runInit(argv) {
|
|
|
100
127
|
|
|
101
128
|
let vaultPath = args.vault;
|
|
102
129
|
if (!vaultPath) {
|
|
103
|
-
const fallback = join(projectPath,
|
|
130
|
+
const fallback = join(projectPath, deriveVaultDirName(projectPath));
|
|
104
131
|
if (process.stdin.isTTY && !args.yes) {
|
|
105
132
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
106
|
-
const ans = (await rl.question(`Obsidian vault path [${fallback}]
|
|
133
|
+
const ans = (await rl.question(`Obsidian vault path (Enter aceita o padrão, ou digite outro caminho)\n [${fallback}]\n> `)).trim();
|
|
107
134
|
rl.close();
|
|
108
135
|
vaultPath = ans || fallback;
|
|
109
136
|
} else {
|
|
@@ -112,10 +139,38 @@ export async function runInit(argv) {
|
|
|
112
139
|
}
|
|
113
140
|
vaultPath = isAbsolute(vaultPath) ? vaultPath : resolve(projectPath, vaultPath);
|
|
114
141
|
|
|
142
|
+
// Companion plugins/MCP selection. --no-companions wins; --companions <csv> is
|
|
143
|
+
// explicit; an interactive TTY gets a multi-choice prompt (context-mode pre-checked);
|
|
144
|
+
// otherwise the non-interactive default (context-mode only).
|
|
145
|
+
let companions;
|
|
146
|
+
if (args.noCompanions) {
|
|
147
|
+
companions = [];
|
|
148
|
+
} else if (args.companions !== undefined) {
|
|
149
|
+
companions = resolveCompanions({ companionsFlag: args.companions });
|
|
150
|
+
} else if (process.stdin.isTTY && !args.yes) {
|
|
151
|
+
log('\nCompanions (plugins/MCP opcionais — context-mode é a principal):');
|
|
152
|
+
if (canInteractiveSelect()) {
|
|
153
|
+
companions = await selectCompanionsInteractive(COMPANIONS);
|
|
154
|
+
} else {
|
|
155
|
+
// Text fallback (no raw-mode TTY): list + comma input with clear instructions.
|
|
156
|
+
for (const c of COMPANIONS) log(` ${c.default ? '[x]' : '[ ]'} ${c.label}`);
|
|
157
|
+
const def = resolveCompanions({}).join(',');
|
|
158
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
159
|
+
const ans = (await rl.question(
|
|
160
|
+
`Digite os ids separados por vírgula (Enter aceita [${def}], "none" p/ nenhum): `,
|
|
161
|
+
)).trim();
|
|
162
|
+
rl.close();
|
|
163
|
+
companions = ans.toLowerCase() === 'none' ? [] : resolveCompanions({ companionsFlag: ans || def });
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
companions = resolveCompanions({});
|
|
167
|
+
}
|
|
168
|
+
|
|
115
169
|
log('\nwendkeep init');
|
|
116
|
-
log(` project
|
|
117
|
-
log(` vault
|
|
118
|
-
log(` mcp
|
|
170
|
+
log(` project : ${projectPath}`);
|
|
171
|
+
log(` vault : ${vaultPath}`);
|
|
172
|
+
log(` mcp : ${args.mcp ? 'mcpvault (wendkeep-vault)' : 'skipped'}`);
|
|
173
|
+
log(` companions : ${companions.length ? companions.join(', ') : 'none'}\n`);
|
|
119
174
|
|
|
120
175
|
// 1. Vault taxonomy ---------------------------------------------------------
|
|
121
176
|
if (!existsSync(vaultPath)) mkdirSync(vaultPath, { recursive: true });
|
|
@@ -127,39 +182,66 @@ export async function runInit(argv) {
|
|
|
127
182
|
created += 1;
|
|
128
183
|
}
|
|
129
184
|
}
|
|
130
|
-
|
|
185
|
+
// Generate a project-templated vault README (non-destructive: never clobber one).
|
|
186
|
+
const readmePath = join(vaultPath, 'README.md');
|
|
187
|
+
let readmeNote = '';
|
|
188
|
+
if (!existsSync(readmePath)) {
|
|
189
|
+
writeFileSync(readmePath, renderVaultReadme({ projectName: basename(projectPath), vaultPath, withMcp: args.mcp }), 'utf8');
|
|
190
|
+
readmeNote = ', README.md created';
|
|
191
|
+
}
|
|
192
|
+
log(` [1/3] vault taxonomy: ${VAULT_FOLDERS.length} folders (${created} created)${readmeNote}`);
|
|
131
193
|
|
|
132
194
|
// 2. .claude/settings.json --------------------------------------------------
|
|
133
195
|
const settingsPath = join(projectPath, '.claude', 'settings.json');
|
|
134
196
|
const settingsRead = readJsonSafe(settingsPath);
|
|
135
197
|
if (!settingsRead.ok) {
|
|
136
198
|
// Unparseable existing file — never clobber. Drop a .new for manual merge.
|
|
137
|
-
const fresh = mergeSettings(null, { vaultPath, withMcp: args.mcp, force: true }).settings;
|
|
199
|
+
const fresh = mergeSettings(null, { vaultPath, withMcp: args.mcp, force: true, companions }).settings;
|
|
138
200
|
writeJson(`${settingsPath}.new`, fresh);
|
|
139
201
|
log(` [2/3] settings.json exists but is not valid JSON -> wrote ${settingsPath}.new (merge by hand)`);
|
|
140
202
|
} else {
|
|
141
203
|
const hadFile = settingsRead.data !== null;
|
|
142
|
-
const { settings, added } = mergeSettings(settingsRead.data, { vaultPath, withMcp: args.mcp, force: args.force });
|
|
204
|
+
const { settings, added } = mergeSettings(settingsRead.data, { vaultPath, withMcp: args.mcp, force: args.force, companions });
|
|
143
205
|
if (hadFile) backup(settingsPath);
|
|
144
206
|
writeJson(settingsPath, settings);
|
|
145
207
|
log(` [2/3] settings.json ${hadFile ? 'merged' : 'created'} (${added} hook(s) wired, OBSIDIAN_VAULT_PATH set${hadFile ? ', .bak saved' : ''})`);
|
|
146
208
|
}
|
|
147
209
|
|
|
148
210
|
// 3. .mcp.json --------------------------------------------------------------
|
|
149
|
-
|
|
211
|
+
// Written when mcpvault is wanted OR a selected companion ships an MCP server.
|
|
212
|
+
const companionMcp = companionMcpPatch(companions);
|
|
213
|
+
if (args.mcp || Object.keys(companionMcp).length) {
|
|
150
214
|
const mcpPath = join(projectPath, '.mcp.json');
|
|
151
215
|
const mcpRead = readJsonSafe(mcpPath);
|
|
216
|
+
const mcpOpts = { vaultPath, withVault: args.mcp, companions };
|
|
217
|
+
const names = [args.mcp && MCP_SERVER_KEY, ...Object.keys(companionMcp)].filter(Boolean).join(', ');
|
|
152
218
|
if (!mcpRead.ok) {
|
|
153
|
-
writeJson(`${mcpPath}.new`, mergeMcp(null,
|
|
219
|
+
writeJson(`${mcpPath}.new`, mergeMcp(null, mcpOpts));
|
|
154
220
|
log(` [3/3] .mcp.json exists but is not valid JSON -> wrote ${mcpPath}.new (merge by hand)`);
|
|
155
221
|
} else {
|
|
156
222
|
const hadFile = mcpRead.data !== null;
|
|
157
223
|
if (hadFile) backup(mcpPath);
|
|
158
|
-
writeJson(mcpPath, mergeMcp(mcpRead.data,
|
|
159
|
-
log(` [3/3] .mcp.json ${hadFile ? 'merged' : 'created'} (
|
|
224
|
+
writeJson(mcpPath, mergeMcp(mcpRead.data, mcpOpts));
|
|
225
|
+
log(` [3/3] .mcp.json ${hadFile ? 'merged' : 'created'} (${names}${hadFile ? ', .bak saved' : ''})`);
|
|
160
226
|
}
|
|
161
227
|
} else {
|
|
162
|
-
log(' [3/3] .mcp.json skipped (--no-mcp)');
|
|
228
|
+
log(' [3/3] .mcp.json skipped (--no-mcp, no MCP companions)');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// caveman has no MCP / agnostic-hook path: on non-Claude agents its skills come
|
|
232
|
+
// from its own cross-agent installer. Best-effort, fail-soft — the Claude Code
|
|
233
|
+
// plugin entry is already wired in settings.json regardless.
|
|
234
|
+
if (companions.includes('caveman')) {
|
|
235
|
+
const cmd = cavemanInstallerCommand();
|
|
236
|
+
log(`\n caveman: running cross-agent installer (best-effort): ${cmd.command} ${cmd.args.join(' ')}`);
|
|
237
|
+
try {
|
|
238
|
+
const r = spawnSync(cmd.command, cmd.args, { stdio: 'inherit' });
|
|
239
|
+
if (r.status !== 0) {
|
|
240
|
+
log(` [!] caveman installer exited ${r.status ?? '?'} — Claude Code plugin entry still wired; rerun manually if needed.`);
|
|
241
|
+
}
|
|
242
|
+
} catch (e) {
|
|
243
|
+
log(` [!] caveman installer could not run (${e.code || e.message}) — Claude Code plugin entry still wired.`);
|
|
244
|
+
}
|
|
163
245
|
}
|
|
164
246
|
|
|
165
247
|
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
|
+
}
|