wendkeep 0.26.0 → 0.28.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/CHANGELOG.md +59 -0
- package/hooks/decision-capture.mjs +108 -0
- package/hooks/subagent-stop.mjs +35 -0
- package/hooks/task-log.mjs +68 -0
- package/package.json +1 -1
- package/src/init.mjs +38 -4
- package/src/taxonomy.mjs +20 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,65 @@ All notable changes to **wendkeep** are documented here. Format based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this project follows
|
|
5
5
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.28.1] — 2026-07-09
|
|
8
|
+
|
|
9
|
+
Startup-contention fixes — root-caused from a real VSCode startup log where the memory injection
|
|
10
|
+
silently dropped and MCPs timed out.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **`brain-inject` timeout 15 → 45s.** The hook is healthy (~2.5s direct, ~4s via npx warm), but
|
|
14
|
+
Windows session startup runs several `npx` cold-starts at once (a sibling MCP took **26s** in the
|
|
15
|
+
log) and 15s silently killed the CORE+DIGEST injection for the whole session.
|
|
16
|
+
- **context-mode double-registration eliminated.** Its plugin ships its **own** MCP server; wiring
|
|
17
|
+
an `.mcp.json` entry too registered it twice — two concurrent `npx context-mode` cold-starts,
|
|
18
|
+
both timing out. The companion is now **plugin-only** (on non-Claude agents add the MCP manually:
|
|
19
|
+
`npx -y context-mode`).
|
|
20
|
+
- **`MCP_TIMEOUT=60000` default** added to the settings `env` by init (only when absent — a user
|
|
21
|
+
value is never clobbered), giving npx-launched stdio MCPs (wendkeep-vault included) headroom over
|
|
22
|
+
Claude Code's 30s default.
|
|
23
|
+
|
|
24
|
+
### Upgrade
|
|
25
|
+
- Existing installs: re-run `wendkeep init` (now recognizes your vault) to pick up the timeout +
|
|
26
|
+
`MCP_TIMEOUT`; remove a duplicated `context-mode` entry from `.mcp.json`/`enabledMcpjsonServers`
|
|
27
|
+
by hand if present.
|
|
28
|
+
|
|
29
|
+
## [0.28.0] — 2026-07-09
|
|
30
|
+
|
|
31
|
+
Three new hooks: decisions, subagents, plan progress.
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
- **Decision capture** (`PostToolUse` / `AskUserQuestion` → `hooks/decision-capture.mjs`): when the
|
|
35
|
+
agent asks the user to choose between options, the decision is recorded in `04-Decisões/` — the
|
|
36
|
+
question, **every** option (label + description), the user's choice (✅), and a wikilink to the
|
|
37
|
+
session. Explicit, high-signal decisions get full traceability in the graph. Shape validated
|
|
38
|
+
against real transcripts.
|
|
39
|
+
- **Live subagent telemetry** (`SubagentStop` → `hooks/subagent-stop.mjs`): refreshes the session's
|
|
40
|
+
subagent/workflow cost notes the moment each subagent finishes (reuses `upsertSubagentUsage`), so
|
|
41
|
+
a session that never reaches `Stop` still has its telemetry. *Model choice stays the harness's
|
|
42
|
+
job — wendkeep observes, it does not impose a routing rule.*
|
|
43
|
+
- **Plan progress log** (`TaskCompleted` → `hooks/task-log.mjs`): when a task is marked complete,
|
|
44
|
+
appends it to a durable `## Progresso do plano` section in the active session note (before
|
|
45
|
+
`## Encerramento`, so reopen can't strip it). A progress trail, not a fuzzy map to `tarefas.md`.
|
|
46
|
+
|
|
47
|
+
All three are wired by `wendkeep init`, are fail-open, and localize (pt-BR / en). `--force`-free —
|
|
48
|
+
they only read + append.
|
|
49
|
+
|
|
50
|
+
## [0.27.0] — 2026-07-08
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
- **Re-running `wendkeep init` no longer re-asks for the vault (or language) — and can't split your
|
|
54
|
+
data.** On a project already set up, init now reads the registered vault from
|
|
55
|
+
`.claude/settings.json` (`OBSIDIAN_VAULT_PATH`) and the locked locale from the vault's
|
|
56
|
+
`.brain/config.json`, reuses both, and skips the prompts. Previously a re-run (e.g. after
|
|
57
|
+
`npm i -D wendkeep@latest`) offered the *derived* default (`.<project>-vault`); accepting it — or
|
|
58
|
+
mistyping the name — created a **second, divergent vault**. `--vault` / `--locale` still override.
|
|
59
|
+
New exported `detectRegisteredVault()` / `readVaultLocale()`. `src/init.mjs`.
|
|
60
|
+
|
|
61
|
+
### Note
|
|
62
|
+
- You do **not** need `wendkeep init` for a routine update: the hooks live in the package
|
|
63
|
+
(`settings.json` calls `npx wendkeep hook …`), so `npm i -D wendkeep@latest` updates them.
|
|
64
|
+
Re-run `init` only when a release adds new wiring (the CHANGELOG says so); it's idempotent.
|
|
65
|
+
|
|
7
66
|
## [0.26.0] — 2026-07-08
|
|
8
67
|
|
|
9
68
|
### Fixed
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// PostToolUse hook (matcher: AskUserQuestion). When the agent asks the user to choose between
|
|
3
|
+
// options, this records the decision — the question, EVERY option (label + description), and the
|
|
4
|
+
// user's choice — as a note in 04-Decisões, wikilinked to the session. Explicit, high-signal
|
|
5
|
+
// decisions get full traceability in the graph (better than heuristic extraction). Fail-open.
|
|
6
|
+
import { existsSync, writeFileSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { pathToFileURL } from 'url';
|
|
9
|
+
import {
|
|
10
|
+
readHookInput, writeHookOutput, getVaultBase, providerMeta, ensureDir, formatDate,
|
|
11
|
+
formatLocalIso, monthFolderRelFromDateStr, slugify, findActiveSessionByTranscript,
|
|
12
|
+
wikilinkFromRel, readControl, toVaultRelative,
|
|
13
|
+
} from './obsidian-common.mjs';
|
|
14
|
+
import { getLocale } from './locale.mjs';
|
|
15
|
+
|
|
16
|
+
// The AskUserQuestion tool_output reads: `... "Question"="chosen labels" "Q2"="..."`.
|
|
17
|
+
export function parseAnswers(output) {
|
|
18
|
+
const map = {};
|
|
19
|
+
const re = /"([^"]+)"\s*=\s*"([^"]*)"/g;
|
|
20
|
+
let m;
|
|
21
|
+
while ((m = re.exec(String(output || '')))) map[m[1].trim()] = m[2].trim();
|
|
22
|
+
return map;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const clean = (s) => String(s || '').replace(/\r/g, '').replace(/\n{3,}/g, '\n\n').trim();
|
|
26
|
+
|
|
27
|
+
// Render the decision note from an AskUserQuestion tool call.
|
|
28
|
+
export function buildDecisionCaptureNote({ questions, answers, dateStr, startedAt, sessionRel, provider, localeId }) {
|
|
29
|
+
const en = localeId === 'en';
|
|
30
|
+
const src = sessionRel ? `\n - "${wikilinkFromRel(sessionRel)}"` : ' []';
|
|
31
|
+
const title = clean(questions[0]?.question || (en ? 'User decision' : 'Decisão do usuário')).slice(0, 90);
|
|
32
|
+
|
|
33
|
+
const blocks = questions.map((q) => {
|
|
34
|
+
const chosen = (answers[clean(q.question)] || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
35
|
+
const isChosen = (label) => chosen.some((c) => c === label.trim() || c.includes(label.trim()) || label.trim().includes(c));
|
|
36
|
+
const opts = (q.options || [])
|
|
37
|
+
.map((o) => `| ${isChosen(o.label) ? '✅' : ''} | ${clean(o.label)} | ${clean(o.description).slice(0, 200)} |`)
|
|
38
|
+
.join('\n');
|
|
39
|
+
return `### ${clean(q.question)}
|
|
40
|
+
${q.multiSelect ? (en ? '_(multiple choice)_' : '_(múltipla escolha)_') : ''}
|
|
41
|
+
|
|
42
|
+
| | ${en ? 'Option' : 'Opção'} | ${en ? 'Description' : 'Descrição'} |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
${opts || `| | — | — |`}
|
|
45
|
+
|
|
46
|
+
**${en ? 'Chosen' : 'Escolhido'}:** ${chosen.length ? chosen.map((c) => `\`${c}\``).join(', ') : (en ? '(not recorded)' : '(não registrado)')}`;
|
|
47
|
+
}).join('\n\n');
|
|
48
|
+
|
|
49
|
+
return `---
|
|
50
|
+
type: decision
|
|
51
|
+
subtype: user-choice
|
|
52
|
+
date: ${dateStr}
|
|
53
|
+
started_at: ${startedAt}
|
|
54
|
+
provider: ${provider.id}
|
|
55
|
+
cssclasses:
|
|
56
|
+
- topic-decision
|
|
57
|
+
tags:
|
|
58
|
+
- decisao
|
|
59
|
+
- escolha-usuario
|
|
60
|
+
source:${src}
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
# ${title}
|
|
64
|
+
|
|
65
|
+
> ${en ? 'Decision captured from an interactive question (options + the user\'s choice).' : 'Decisão capturada de uma pergunta interativa (opções + a escolha do usuário).'}
|
|
66
|
+
|
|
67
|
+
${blocks}
|
|
68
|
+
`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function captureDecision(vaultBase, input) {
|
|
72
|
+
const toolIn = input.tool_input || input.toolInput || {};
|
|
73
|
+
const questions = Array.isArray(toolIn.questions) ? toolIn.questions : [];
|
|
74
|
+
if (!questions.length) return null;
|
|
75
|
+
|
|
76
|
+
const answers = parseAnswers(input.tool_output ?? input.toolOutput ?? input.tool_response ?? '');
|
|
77
|
+
const loc = getLocale(vaultBase);
|
|
78
|
+
const now = new Date();
|
|
79
|
+
const dateStr = formatDate(now);
|
|
80
|
+
const provider = providerMeta(input.provider);
|
|
81
|
+
|
|
82
|
+
const matched = input.transcript_path ? findActiveSessionByTranscript(vaultBase, input.transcript_path) : null;
|
|
83
|
+
const sessionRel = matched?.session_file || readControl(vaultBase).session_file || '';
|
|
84
|
+
|
|
85
|
+
const dir = join(vaultBase, monthFolderRelFromDateStr(loc.folders.decisions, dateStr, vaultBase));
|
|
86
|
+
ensureDir(dir);
|
|
87
|
+
const slug = slugify(questions[0]?.question || 'decisao', 'decisao', 50);
|
|
88
|
+
const filePath = join(dir, `${dateStr}-escolha-${slug}.md`);
|
|
89
|
+
if (existsSync(filePath)) return { rel: toVaultRelative(vaultBase, filePath), skipped: true };
|
|
90
|
+
|
|
91
|
+
writeFileSync(filePath, buildDecisionCaptureNote({
|
|
92
|
+
questions, answers, dateStr, startedAt: formatLocalIso(now), sessionRel, provider, localeId: loc.id,
|
|
93
|
+
}), 'utf-8');
|
|
94
|
+
return { rel: toVaultRelative(vaultBase, filePath), skipped: false };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
98
|
+
try {
|
|
99
|
+
const input = readHookInput();
|
|
100
|
+
if ((input.tool_name || input.toolName) === 'AskUserQuestion') {
|
|
101
|
+
captureDecision(getVaultBase(input), input);
|
|
102
|
+
}
|
|
103
|
+
writeHookOutput({});
|
|
104
|
+
} catch (error) {
|
|
105
|
+
process.stderr.write(`[wendkeep] decision-capture falhou: ${error.message}\n`);
|
|
106
|
+
writeHookOutput({});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SubagentStop hook: refresh this session's subagent/workflow telemetry the MOMENT a subagent
|
|
3
|
+
// finishes — not only at the main Stop. Resilience: a session that never reaches Stop (crash,
|
|
4
|
+
// window closed) still gets its subagent cost notes. Reuses the same upsertSubagentUsage the Stop
|
|
5
|
+
// hook runs, so the output is identical; it just runs earlier + incrementally. Fail-open.
|
|
6
|
+
//
|
|
7
|
+
// Model choice for subagents stays the harness's job (agent frontmatter `model:` / the Task/
|
|
8
|
+
// workflow `model` param). wendkeep OBSERVES (this telemetry) rather than dictating a routing rule.
|
|
9
|
+
import { existsSync } from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import { pathToFileURL } from 'url';
|
|
12
|
+
import { readHookInput, writeHookOutput, getVaultBase, findActiveSessionByTranscript, readControl } from './obsidian-common.mjs';
|
|
13
|
+
import { upsertSubagentUsage } from './subagent-usage.mjs';
|
|
14
|
+
|
|
15
|
+
export function refreshSubagents(vaultBase, input) {
|
|
16
|
+
const transcriptPath = input.transcript_path || input.transcriptPath || '';
|
|
17
|
+
const matched = transcriptPath ? findActiveSessionByTranscript(vaultBase, transcriptPath) : null;
|
|
18
|
+
const sessionRel = matched?.session_file || readControl(vaultBase).session_file || '';
|
|
19
|
+
if (!sessionRel) return false;
|
|
20
|
+
const sessionPath = join(vaultBase, sessionRel);
|
|
21
|
+
if (!existsSync(sessionPath)) return false;
|
|
22
|
+
upsertSubagentUsage(sessionPath, transcriptPath);
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
27
|
+
try {
|
|
28
|
+
const input = readHookInput();
|
|
29
|
+
refreshSubagents(getVaultBase(input), input);
|
|
30
|
+
writeHookOutput({});
|
|
31
|
+
} catch (error) {
|
|
32
|
+
process.stderr.write(`[wendkeep] subagent-stop falhou: ${error.message}\n`);
|
|
33
|
+
writeHookOutput({});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// TaskCompleted hook: log plan/task progress into the active session note, so the session records
|
|
3
|
+
// what the plan actually advanced through. It writes a durable "## Progresso do plano" section
|
|
4
|
+
// placed BEFORE ## Encerramento (survives the reopen/strip cycle) and does NOT try to map to a
|
|
5
|
+
// change's tarefas.md N.N (id-spaces differ, fuzzy) — it's a progress trail, not a task tracker.
|
|
6
|
+
// The TaskCompleted payload shape isn't fully pinned, so the task text is pulled defensively from
|
|
7
|
+
// any plausible field. Fail-open.
|
|
8
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { pathToFileURL } from 'url';
|
|
11
|
+
import { readHookInput, writeHookOutput, getVaultBase, findActiveSessionByTranscript, readControl, formatHourMinute } from './obsidian-common.mjs';
|
|
12
|
+
import { getLocale } from './locale.mjs';
|
|
13
|
+
|
|
14
|
+
// Pull the task's human text from whatever field the payload carries.
|
|
15
|
+
export function taskText(input) {
|
|
16
|
+
const t = input.task || input.tool_input || input.payload || input;
|
|
17
|
+
const cand = t?.content || t?.text || t?.description || t?.title || t?.subject || t?.name
|
|
18
|
+
|| t?.prompt || input.content || input.text || input.description || '';
|
|
19
|
+
return String(cand || '').replace(/[\r\n]+/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 160);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Append `line` at the END of a "## <heading>" section placed before ## Encerramento, so entries
|
|
23
|
+
// read chronologically. Deduped. Pure.
|
|
24
|
+
export function appendProgress(content, line, heading) {
|
|
25
|
+
if (content.includes(line)) return content; // dedup
|
|
26
|
+
const marker = `\n## ${heading}\n`;
|
|
27
|
+
const at = content.indexOf(marker);
|
|
28
|
+
if (at !== -1) {
|
|
29
|
+
const sectionStart = at + marker.length;
|
|
30
|
+
let end = content.indexOf('\n## ', sectionStart);
|
|
31
|
+
if (end === -1) end = content.length;
|
|
32
|
+
const head = content.slice(0, end).replace(/\s+$/, '');
|
|
33
|
+
return `${head}\n${line}\n${content.slice(end)}`;
|
|
34
|
+
}
|
|
35
|
+
const block = `## ${heading}\n\n${line}\n\n`;
|
|
36
|
+
const enc = content.indexOf('\n## Encerramento');
|
|
37
|
+
if (enc === -1) return `${content.trimEnd()}\n\n${block}`;
|
|
38
|
+
return `${content.slice(0, enc).trimEnd()}\n\n${block}${content.slice(enc + 1)}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function logTask(vaultBase, input) {
|
|
42
|
+
const text = taskText(input);
|
|
43
|
+
if (!text) return false;
|
|
44
|
+
const transcriptPath = input.transcript_path || input.transcriptPath || '';
|
|
45
|
+
const matched = transcriptPath ? findActiveSessionByTranscript(vaultBase, transcriptPath) : null;
|
|
46
|
+
const sessionRel = matched?.session_file || readControl(vaultBase).session_file || '';
|
|
47
|
+
if (!sessionRel) return false;
|
|
48
|
+
const sessionPath = join(vaultBase, sessionRel);
|
|
49
|
+
if (!existsSync(sessionPath)) return false;
|
|
50
|
+
|
|
51
|
+
const heading = getLocale(vaultBase).id === 'en' ? 'Plan progress' : 'Progresso do plano';
|
|
52
|
+
const line = `- [x] ${formatHourMinute(new Date()).replace('-', ':')} ${text}`;
|
|
53
|
+
const content = readFileSync(sessionPath, 'utf8');
|
|
54
|
+
const next = appendProgress(content, line, heading);
|
|
55
|
+
if (next !== content) { writeFileSync(sessionPath, next, 'utf8'); return true; }
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
60
|
+
try {
|
|
61
|
+
const input = readHookInput();
|
|
62
|
+
logTask(getVaultBase(input), input);
|
|
63
|
+
writeHookOutput({});
|
|
64
|
+
} catch (error) {
|
|
65
|
+
process.stderr.write(`[wendkeep] task-log falhou: ${error.message}\n`);
|
|
66
|
+
writeHookOutput({});
|
|
67
|
+
}
|
|
68
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.1",
|
|
4
4
|
"description": "A persistent-memory harness for AI coding agents on your Obsidian vault: turn-by-turn session capture plus a native, zero-dependency spec→change→verify→archive loop (sensor-gated, independent verdict, mutation discrimination). Local-first, agent-agnostic (Claude Code, Codex, Cursor…).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/init.mjs
CHANGED
|
@@ -126,6 +126,10 @@ export function mergeSettings(existing, { vaultPath, withMcp, force, companions
|
|
|
126
126
|
added += 1;
|
|
127
127
|
}
|
|
128
128
|
s.env = { ...(s.env || {}), OBSIDIAN_VAULT_PATH: vaultPath };
|
|
129
|
+
// npx-launched stdio MCPs (wendkeep-vault included) can cold-start near/over Claude Code's 30s
|
|
130
|
+
// default under Windows startup contention (26s observed in a real log). Give them headroom —
|
|
131
|
+
// but never clobber a value the user already set.
|
|
132
|
+
if (!('MCP_TIMEOUT' in s.env)) s.env.MCP_TIMEOUT = '60000';
|
|
129
133
|
|
|
130
134
|
// Claude Code plugin layer for the selected companions (additive, non-clobbering).
|
|
131
135
|
const cp = companionSettingsPatch(companions);
|
|
@@ -249,6 +253,7 @@ const MESSAGES = {
|
|
|
249
253
|
step3: ' 3. Abra o Claude Code neste projeto e envie um prompt de teste.',
|
|
250
254
|
step4: ' 4. Confirme que uma nota aparece em 02-Sessões/<ano>/<mês>/DIA <dd>/ no vault.',
|
|
251
255
|
updateLater: ' Atualize depois com: npm update wendkeep (sem recopiar — os hooks vivem no pacote).\n',
|
|
256
|
+
vaultRegistered: (v, loc) => `\n cofre já registrado neste projeto: ${v} (locale ${loc}) — pergunta pulada. Use --vault para mudar.`,
|
|
252
257
|
},
|
|
253
258
|
en: {
|
|
254
259
|
header: '\nwendkeep init',
|
|
@@ -273,6 +278,7 @@ const MESSAGES = {
|
|
|
273
278
|
step3: ' 3. Open Claude Code in this project and send a test prompt.',
|
|
274
279
|
step4: ' 4. Confirm a note appears under 02-Sessões/<year>/<month>/DIA <dd>/ in the vault.',
|
|
275
280
|
updateLater: ' Update later with: npm update wendkeep (no re-copying — hooks live in the package).\n',
|
|
281
|
+
vaultRegistered: (v, loc) => `\n vault already registered for this project: ${v} (locale ${loc}) — prompt skipped. Use --vault to change.`,
|
|
276
282
|
},
|
|
277
283
|
};
|
|
278
284
|
|
|
@@ -287,13 +293,39 @@ export function parseLocaleAnswer(ans) {
|
|
|
287
293
|
return 'pt-BR';
|
|
288
294
|
}
|
|
289
295
|
|
|
296
|
+
// The vault path this project was set up with — read from .claude/settings.json's
|
|
297
|
+
// OBSIDIAN_VAULT_PATH (written by a prior init). Empty when the project isn't configured yet.
|
|
298
|
+
export function detectRegisteredVault(projectPath) {
|
|
299
|
+
try {
|
|
300
|
+
const s = JSON.parse(readFileSync(join(projectPath, '.claude', 'settings.json'), 'utf8'));
|
|
301
|
+
const v = s && s.env && s.env.OBSIDIAN_VAULT_PATH;
|
|
302
|
+
return typeof v === 'string' && v ? v : '';
|
|
303
|
+
} catch { return ''; }
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// The locale locked in a vault's .brain/config.json (empty if absent/invalid).
|
|
307
|
+
export function readVaultLocale(vaultPath) {
|
|
308
|
+
try {
|
|
309
|
+
const c = JSON.parse(readFileSync(join(vaultPath, '.brain', 'config.json'), 'utf8'));
|
|
310
|
+
return c && LOCALES[c.locale] ? c.locale : '';
|
|
311
|
+
} catch { return ''; }
|
|
312
|
+
}
|
|
313
|
+
|
|
290
314
|
export async function runInit(argv) {
|
|
291
315
|
const args = parseArgs(argv);
|
|
292
316
|
const projectPath = resolve(args.project || process.cwd());
|
|
293
317
|
const log = (s) => process.stdout.write(`${s}\n`);
|
|
294
318
|
|
|
295
|
-
//
|
|
296
|
-
//
|
|
319
|
+
// Recognize an already-configured project: the vault is registered in the project's
|
|
320
|
+
// settings.json (OBSIDIAN_VAULT_PATH) and its locale is locked in the vault's config.json.
|
|
321
|
+
// On re-run (e.g. after `npm i -D wendkeep@latest`) we reuse both and SKIP the language + vault
|
|
322
|
+
// prompts — asking again risks a divergent vault from a mistyped name. `--vault` / `--locale`
|
|
323
|
+
// override; the vault question is a once-per-project thing.
|
|
324
|
+
const registeredVault = args.vault ? '' : detectRegisteredVault(projectPath);
|
|
325
|
+
if (registeredVault && !args.locale) args.locale = readVaultLocale(registeredVault) || args.locale;
|
|
326
|
+
|
|
327
|
+
// Language first (i18n): an interactive TTY without --locale (and not already registered) is
|
|
328
|
+
// asked the vault language; folders, prompts and scaffold all follow the answer.
|
|
297
329
|
if (!args.locale && process.stdin.isTTY && !args.yes) {
|
|
298
330
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
299
331
|
const ans = await rl.question('Idioma do vault / Vault language:\n [1] Português (pt-BR) [2] English (en)\n> ');
|
|
@@ -304,8 +336,10 @@ export async function runInit(argv) {
|
|
|
304
336
|
const P = promptStrings(resolvedLocale);
|
|
305
337
|
const M = initMessages(resolvedLocale);
|
|
306
338
|
|
|
307
|
-
let vaultPath = args.vault;
|
|
308
|
-
if (
|
|
339
|
+
let vaultPath = args.vault || registeredVault;
|
|
340
|
+
if (registeredVault) {
|
|
341
|
+
log(M.vaultRegistered(registeredVault, resolvedLocale));
|
|
342
|
+
} else if (!vaultPath) {
|
|
309
343
|
const fallback = join(projectPath, deriveVaultDirName(projectPath));
|
|
310
344
|
if (process.stdin.isTTY && !args.yes) {
|
|
311
345
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
package/src/taxonomy.mjs
CHANGED
|
@@ -44,6 +44,9 @@ export const HOOK_FILES = [
|
|
|
44
44
|
'brain-reindex.mjs',
|
|
45
45
|
'session-backfill.mjs',
|
|
46
46
|
'import-sessions.mjs',
|
|
47
|
+
'decision-capture.mjs',
|
|
48
|
+
'subagent-stop.mjs',
|
|
49
|
+
'task-log.mjs',
|
|
47
50
|
'vault-health.mjs',
|
|
48
51
|
'understand-inject.mjs',
|
|
49
52
|
];
|
|
@@ -56,6 +59,9 @@ export const RUNNABLE_HOOKS = [
|
|
|
56
59
|
'session-ensure',
|
|
57
60
|
'session-stop',
|
|
58
61
|
'session-backfill',
|
|
62
|
+
'decision-capture',
|
|
63
|
+
'subagent-stop',
|
|
64
|
+
'task-log',
|
|
59
65
|
'brain-inject',
|
|
60
66
|
'brain-recall',
|
|
61
67
|
'brain-reindex',
|
|
@@ -81,10 +87,19 @@ export const SESSION_HOOKS = [
|
|
|
81
87
|
// Memory + active-change injection. Runs FIRST on SessionStart (order -10, folds before
|
|
82
88
|
// session-start) so the agent gets CORE + DIGEST + the active change + lessons as context.
|
|
83
89
|
// matcher 'startup|clear|compact' re-injects after a compaction/clear, not only cold startup.
|
|
84
|
-
|
|
90
|
+
// timeout 45 (was 15): measured ~4s warm via npx, but Windows startup contention (several npx
|
|
91
|
+
// cold-starts at once — a sibling MCP took 26s in a real log) blew 15s and silently dropped the
|
|
92
|
+
// memory injection for the whole session.
|
|
93
|
+
{ event: 'SessionStart', matcher: 'startup|clear|compact', name: 'brain-inject', timeout: 45, order: -10, statusMessage: 'wendkeep: injecting memory + active change' },
|
|
85
94
|
{ event: 'SessionStart', matcher: 'startup', name: 'session-start', timeout: 30, statusMessage: 'wendkeep: opening Obsidian session' },
|
|
86
95
|
{ event: 'Stop', matcher: null, name: 'session-stop', timeout: 60, statusMessage: 'wendkeep: writing session checkpoint' },
|
|
87
96
|
{ event: 'UserPromptSubmit', matcher: null, name: 'session-ensure', timeout: 30, statusMessage: 'wendkeep: ensuring active session' },
|
|
97
|
+
// Capture an interactive decision (AskUserQuestion) — options + the user's choice — into 04-Decisões.
|
|
98
|
+
{ event: 'PostToolUse', matcher: 'AskUserQuestion', name: 'decision-capture', timeout: 15, statusMessage: 'wendkeep: recording decision' },
|
|
99
|
+
// Refresh subagent/workflow telemetry as each subagent finishes (resilient to a missed Stop).
|
|
100
|
+
{ event: 'SubagentStop', matcher: null, name: 'subagent-stop', timeout: 20, statusMessage: 'wendkeep: subagent telemetry' },
|
|
101
|
+
// Log plan/task progress into the active session note when a task is marked complete.
|
|
102
|
+
{ event: 'TaskCompleted', matcher: null, name: 'task-log', timeout: 10, statusMessage: 'wendkeep: plan progress' },
|
|
88
103
|
];
|
|
89
104
|
|
|
90
105
|
export function hookCommand(name) {
|
|
@@ -108,8 +123,10 @@ export const COMPANIONS = [
|
|
|
108
123
|
default: false,
|
|
109
124
|
marketplace: { source: 'git', url: 'https://github.com/mksglu/context-mode.git' },
|
|
110
125
|
plugin: 'context-mode',
|
|
111
|
-
//
|
|
112
|
-
|
|
126
|
+
// NO .mcp.json entry on purpose: the plugin ships its OWN MCP server, so wiring both
|
|
127
|
+
// double-registered it (two concurrent `npx context-mode` cold-starts at session start —
|
|
128
|
+
// both timed out in a real log). Plugin is the single source; on non-Claude agents add the
|
|
129
|
+
// MCP manually (`npx -y context-mode`).
|
|
113
130
|
},
|
|
114
131
|
{
|
|
115
132
|
id: 'understand-anything',
|