wendkeep 0.64.0 → 0.66.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.
@@ -0,0 +1,423 @@
1
+ import { isBootstrapPrompt, redactSecrets } from './prompt-content.mjs';
2
+ import {
3
+ addUsage,
4
+ emptyTokenUsage,
5
+ normalizeClaudeUsage,
6
+ normalizeCodexUsage,
7
+ } from './transcript-usage.mjs';
8
+
9
+ function extractContentText(content) {
10
+ if (typeof content === 'string') return content;
11
+ if (!Array.isArray(content)) return '';
12
+ return content
13
+ .map((item) => item?.text || item?.input_text || item?.output_text || '')
14
+ .filter(Boolean)
15
+ .join('\n')
16
+ .trim();
17
+ }
18
+
19
+ const SYNTHETIC_EVENT_TAG = /^<\/?(?:task-notification|system-reminder|local-command-stdout|local-command-stderr|command-message|command-name|command-args|user-prompt-submit-hook|ide_selection|ide_opened_file|environment_context)\b/i;
20
+
21
+ function shouldIgnoreUserText(text) {
22
+ const trimmed = String(text || '').trim();
23
+ return SYNTHETIC_EVENT_TAG.test(trimmed)
24
+ || isBootstrapPrompt(trimmed)
25
+ || /^Generate a concise( UI)? title/i.test(trimmed)
26
+ || /^You are a helpful assistant\. You will be presented with a user prompt/i.test(trimmed);
27
+ }
28
+
29
+ function addUnique(list, value) {
30
+ const clean = redactSecrets(String(value || '').trim());
31
+ if (clean && !list.includes(clean)) list.push(clean);
32
+ }
33
+
34
+ function createTurn(turnId = '', timestamp = '') {
35
+ return {
36
+ turnId,
37
+ timestamp,
38
+ userPrompts: [],
39
+ assistantMessages: [],
40
+ tools: [],
41
+ consultedFiles: [],
42
+ changedFiles: [],
43
+ conversation: [],
44
+ usage: emptyTokenUsage(),
45
+ model: '',
46
+ };
47
+ }
48
+
49
+ function createResult(provider) {
50
+ return {
51
+ provider,
52
+ sessionId: '',
53
+ model: '',
54
+ latestTurnId: '',
55
+ latestUserPrompt: '',
56
+ latestAssistantMessage: '',
57
+ userPrompts: [],
58
+ assistantMessages: [],
59
+ tools: [],
60
+ consultedFiles: [],
61
+ changedFiles: [],
62
+ turns: [],
63
+ rawTextForDetection: '',
64
+ };
65
+ }
66
+
67
+ function addConversation(turn, role, value) {
68
+ if (!turn) return;
69
+ const text = redactSecrets(String(value || '').trim());
70
+ if (!text) return;
71
+ if (!turn.conversation.some((item) => item.role === role && item.text === text)) {
72
+ turn.conversation.push({ role, text });
73
+ }
74
+ }
75
+
76
+ function normalizeRoot(value) {
77
+ return String(value || '').replace(/\\+/g, '/').replace(/\/+$/, '');
78
+ }
79
+
80
+ function pathContext(options = {}) {
81
+ const repoRoot = normalizeRoot(options.repoRoot);
82
+ const vaultRoot = normalizeRoot(options.vaultRoot).toLowerCase();
83
+ const repoLower = repoRoot.toLowerCase();
84
+ const vaultRel = vaultRoot && repoLower && vaultRoot.startsWith(`${repoLower}/`)
85
+ ? vaultRoot.slice(repoLower.length + 1)
86
+ : '';
87
+ return { repoRoot, repoLower, vaultRoot, vaultRel };
88
+ }
89
+
90
+ function normalizeExtractedPath(value, context) {
91
+ const cleaned = String(value || '')
92
+ .replace(/\\+/g, '/')
93
+ .replace(/\/+/g, '/')
94
+ .replace(/^(?:\.\/)+/, '')
95
+ .replace(/[:.,;)}\]]+$/, '');
96
+ if (context.repoRoot && cleaned.toLowerCase().startsWith(`${context.repoLower}/`)) {
97
+ return cleaned.slice(context.repoRoot.length + 1);
98
+ }
99
+ return cleaned;
100
+ }
101
+
102
+ function shouldIgnoreExtractedPath(path, context) {
103
+ if (!path) return true;
104
+ const lower = path.toLowerCase();
105
+ if (context.vaultRoot && lower.startsWith(`${context.vaultRoot}/`)) return true;
106
+ if (context.vaultRel && lower.startsWith(`${context.vaultRel}/`)) return true;
107
+ if (lower.includes('/.codex/sessions/')) return true;
108
+ if (lower.includes('/.claude/projects/')) return true;
109
+ if (path.startsWith('../') || path.includes('/../')) return true;
110
+ if (/(?:^|\/)(?:CURRENT_SESSION\.md|SESSION_REGISTRY\.json)$/i.test(path)) return true;
111
+ if (/^[A-Za-z]:\/[A-Za-z]:\//.test(path)) return true;
112
+ if (/^Alves\/\.codex\//i.test(path)) return true;
113
+ if (/\/\.[A-Za-z0-9]+(?::\d+)?$/.test(path)) return true;
114
+ return false;
115
+ }
116
+
117
+ function extractPaths(text, context) {
118
+ const paths = [];
119
+ const addPath = (value) => {
120
+ const path = normalizeExtractedPath(value, context);
121
+ if (!shouldIgnoreExtractedPath(path, context) && !paths.includes(path)) paths.push(path);
122
+ };
123
+ const windowsRegex = /[A-Za-z]:[\\/]+[^"'`\r\n{}()[\],]+\.[A-Za-z0-9]+(?::\d+)?/g;
124
+ const source = String(text || '');
125
+ let match;
126
+ while ((match = windowsRegex.exec(source)) !== null) addPath(match[0]);
127
+ const masked = source.replace(windowsRegex, ' ');
128
+ const regex = /(?:^|[\s"'`(])((?:\/(?:home|mnt)\/|\.{1,2}\/|[A-Za-z0-9_.-]+\/)[A-Za-z0-9_./@+:-]+\.[A-Za-z0-9]+(?::\d+)?)/g;
129
+ while ((match = regex.exec(masked)) !== null) addPath(match[1]);
130
+ return paths.slice(0, 20);
131
+ }
132
+
133
+ function extractPatchFiles(text) {
134
+ const files = [];
135
+ const regex = /^\*\*\* (?:Add|Update|Delete) File:\s+(.+)$/gm;
136
+ let match;
137
+ while ((match = regex.exec(text || '')) !== null) addUnique(files, match[1]);
138
+ return files;
139
+ }
140
+
141
+ function parseToolArguments(args) {
142
+ if (!args) return {};
143
+ if (typeof args === 'object') return args;
144
+ try { return JSON.parse(args); } catch { return { raw: String(args) }; }
145
+ }
146
+
147
+ function toolArgumentText(value) {
148
+ if (value == null) return '';
149
+ if (typeof value === 'string') return value;
150
+ if (Array.isArray(value)) return value.map(toolArgumentText).filter(Boolean).join('\n');
151
+ if (typeof value === 'object') return Object.values(value).map(toolArgumentText).filter(Boolean).join('\n');
152
+ return String(value);
153
+ }
154
+
155
+ function jsonLines(content) {
156
+ return String(content || '').split('\n').filter(Boolean).map((line) => {
157
+ try { return JSON.parse(line); } catch { return null; }
158
+ }).filter(Boolean);
159
+ }
160
+
161
+ export function parseCodexTranscriptContent(content, options = {}) {
162
+ const result = createResult('codex');
163
+ const eventUserPrompts = [];
164
+ const paths = pathContext(options);
165
+ let currentTurn = null;
166
+ const ensureTurn = (turnId = '', timestamp = '') => {
167
+ const normalized = turnId || currentTurn?.turnId || `turn-${result.turns.length + 1}`;
168
+ const existing = result.turns.find((turn) => turn.turnId === normalized);
169
+ if (existing) {
170
+ currentTurn = existing;
171
+ return existing;
172
+ }
173
+ currentTurn = createTurn(normalized, timestamp);
174
+ result.turns.push(currentTurn);
175
+ return currentTurn;
176
+ };
177
+
178
+ for (const event of jsonLines(content)) {
179
+ if (event.type === 'session_meta') {
180
+ result.sessionId = event.payload?.id || result.sessionId;
181
+ result.model = event.payload?.model || event.payload?.model_provider || result.model;
182
+ continue;
183
+ }
184
+ if (event.type === 'event_msg' && event.payload?.type === 'task_started') {
185
+ result.latestTurnId = event.payload.turn_id || result.latestTurnId;
186
+ ensureTurn(result.latestTurnId, event.timestamp);
187
+ continue;
188
+ }
189
+ if (event.type === 'turn_context') {
190
+ result.latestTurnId = event.payload?.turn_id || result.latestTurnId;
191
+ result.model = event.payload?.model || result.model;
192
+ ensureTurn(result.latestTurnId, event.timestamp);
193
+ continue;
194
+ }
195
+ if (event.type === 'event_msg' && event.payload?.type === 'user_message') {
196
+ const text = event.payload.message || '';
197
+ if (text && !shouldIgnoreUserText(text)) {
198
+ const turn = ensureTurn(event.payload.turn_id || result.latestTurnId, event.timestamp);
199
+ addUnique(eventUserPrompts, text);
200
+ addUnique(result.userPrompts, text);
201
+ addUnique(turn.userPrompts, text);
202
+ addConversation(turn, 'Usuário', text);
203
+ }
204
+ continue;
205
+ }
206
+ if (event.type === 'event_msg' && event.payload?.type === 'agent_message') {
207
+ const text = event.payload.message || event.payload.text || '';
208
+ if (text) {
209
+ const turn = ensureTurn(event.payload.turn_id || result.latestTurnId, event.timestamp);
210
+ addUnique(result.assistantMessages, text);
211
+ addUnique(turn.assistantMessages, text);
212
+ addConversation(turn, 'Assistente', text);
213
+ }
214
+ continue;
215
+ }
216
+ if (event.type === 'event_msg' && event.payload?.type === 'token_count') {
217
+ const raw = event.payload?.info?.last_token_usage;
218
+ if (raw) {
219
+ const turn = currentTurn || ensureTurn(result.latestTurnId, event.timestamp);
220
+ addUsage(turn.usage, normalizeCodexUsage(raw));
221
+ if (event.payload?.info?.model) turn.model = event.payload.info.model;
222
+ }
223
+ continue;
224
+ }
225
+ if (event.type !== 'response_item') continue;
226
+ const payload = event.payload || {};
227
+ if (payload.type === 'message') {
228
+ const text = extractContentText(payload.content);
229
+ if (!text) continue;
230
+ const turn = ensureTurn(payload.turn_id || event.turn_id || result.latestTurnId, event.timestamp);
231
+ if (payload.role === 'user' && !shouldIgnoreUserText(text)) {
232
+ addUnique(result.userPrompts, text);
233
+ addUnique(turn.userPrompts, text);
234
+ addConversation(turn, 'Usuário', text);
235
+ }
236
+ if (payload.role === 'assistant') {
237
+ addUnique(result.assistantMessages, text);
238
+ addUnique(turn.assistantMessages, text);
239
+ addConversation(turn, 'Assistente', text);
240
+ }
241
+ continue;
242
+ }
243
+ if (payload.type === 'function_call') {
244
+ const name = payload.name || 'function_call';
245
+ const turn = ensureTurn(payload.turn_id || event.turn_id || result.latestTurnId, event.timestamp);
246
+ addUnique(result.tools, name);
247
+ addUnique(turn.tools, name);
248
+ const parsed = parseToolArguments(payload.arguments);
249
+ const combined = typeof parsed.raw === 'string' ? parsed.raw : toolArgumentText(parsed);
250
+ for (const path of extractPaths(combined, paths)) {
251
+ addUnique(result.consultedFiles, path);
252
+ addUnique(turn.consultedFiles, path);
253
+ }
254
+ for (const path of extractPatchFiles(combined)) {
255
+ addUnique(result.changedFiles, path);
256
+ addUnique(turn.changedFiles, path);
257
+ }
258
+ if (/apply_patch|edit|write|create/i.test(name)) {
259
+ for (const path of extractPaths(combined, paths)) {
260
+ addUnique(result.changedFiles, path);
261
+ addUnique(turn.changedFiles, path);
262
+ }
263
+ }
264
+ }
265
+ if (payload.type === 'tool_search_call') {
266
+ const turn = ensureTurn(payload.turn_id || event.turn_id || result.latestTurnId, event.timestamp);
267
+ addUnique(result.tools, 'tool_search');
268
+ addUnique(turn.tools, 'tool_search');
269
+ }
270
+ if (payload.type === 'web_search_call') {
271
+ const turn = ensureTurn(payload.turn_id || event.turn_id || result.latestTurnId, event.timestamp);
272
+ addUnique(result.tools, 'web_search');
273
+ addUnique(turn.tools, 'web_search');
274
+ }
275
+ }
276
+
277
+ for (const prompt of eventUserPrompts) addUnique(result.userPrompts, prompt);
278
+ const latestTurn = result.turns.find((turn) => turn.turnId === result.latestTurnId)
279
+ || result.turns.at(-1);
280
+ result.latestUserPrompt = latestTurn?.userPrompts.at(-1)
281
+ || eventUserPrompts.at(-1)
282
+ || result.userPrompts.at(-1)
283
+ || '';
284
+ result.latestAssistantMessage = latestTurn?.assistantMessages.at(-1)
285
+ || result.assistantMessages.at(-1)
286
+ || '';
287
+ result.rawTextForDetection = redactSecrets([
288
+ ...result.userPrompts,
289
+ ...result.assistantMessages,
290
+ ].join('\n\n'));
291
+ return result;
292
+ }
293
+
294
+ function claudeUserText(content) {
295
+ if (typeof content === 'string') return content.trim();
296
+ if (!Array.isArray(content)) return '';
297
+ return content
298
+ .map((block) => (typeof block === 'string' ? block : (block?.type === 'text' ? block.text || '' : '')))
299
+ .map((text) => String(text || '').trim())
300
+ .filter((text) => text && !text.startsWith('<'))
301
+ .join('\n')
302
+ .trim();
303
+ }
304
+
305
+ export function parseClaudeTranscriptContent(content, options = {}) {
306
+ const result = createResult('claude');
307
+ const paths = pathContext(options);
308
+ let currentTurn = null;
309
+ const ensureTurn = (turnId = '', timestamp = '') => {
310
+ const normalized = turnId || currentTurn?.turnId || `turn-${result.turns.length + 1}`;
311
+ const existing = result.turns.find((turn) => turn.turnId === normalized);
312
+ if (existing) {
313
+ currentTurn = existing;
314
+ return existing;
315
+ }
316
+ currentTurn = createTurn(normalized, timestamp);
317
+ result.turns.push(currentTurn);
318
+ return currentTurn;
319
+ };
320
+ const recordToolFiles = (turn, name, input) => {
321
+ const text = toolArgumentText(input);
322
+ for (const path of extractPaths(text, paths)) {
323
+ addUnique(result.consultedFiles, path);
324
+ addUnique(turn.consultedFiles, path);
325
+ }
326
+ for (const path of extractPatchFiles(text)) {
327
+ addUnique(result.changedFiles, path);
328
+ addUnique(turn.changedFiles, path);
329
+ }
330
+ if (/edit|write|create|apply_patch|notebook/i.test(name)) {
331
+ for (const path of extractPaths(text, paths)) {
332
+ addUnique(result.changedFiles, path);
333
+ addUnique(turn.changedFiles, path);
334
+ }
335
+ }
336
+ };
337
+
338
+ for (const event of jsonLines(content)) {
339
+ if (event.isSidechain || event.isMeta) continue;
340
+ if (event.sessionId && !result.sessionId) result.sessionId = event.sessionId;
341
+ if (event.type === 'user') {
342
+ const text = claudeUserText(event.message?.content);
343
+ if (!text || shouldIgnoreUserText(text)) continue;
344
+ const turn = ensureTurn(event.uuid || event.promptId || event.timestamp || '', event.timestamp || '');
345
+ result.latestTurnId = turn.turnId;
346
+ addUnique(result.userPrompts, text);
347
+ addUnique(turn.userPrompts, text);
348
+ addConversation(turn, 'Usuário', text);
349
+ continue;
350
+ }
351
+ if (event.type === 'assistant') {
352
+ const turn = currentTurn || ensureTurn(event.uuid || event.timestamp || '', event.timestamp || '');
353
+ result.model = event.message?.model || result.model;
354
+ if (event.message?.model) turn.model = event.message.model;
355
+ if (event.message?.usage) addUsage(turn.usage, normalizeClaudeUsage(event.message.usage));
356
+ const blocks = Array.isArray(event.message?.content) ? event.message.content : [];
357
+ for (const block of blocks) {
358
+ if (block?.type === 'text' && block.text && block.text.trim()) {
359
+ addUnique(result.assistantMessages, block.text);
360
+ addUnique(turn.assistantMessages, block.text);
361
+ addConversation(turn, 'Assistente', block.text);
362
+ } else if (block?.type === 'tool_use') {
363
+ const name = block.name || 'tool_use';
364
+ addUnique(result.tools, name);
365
+ addUnique(turn.tools, name);
366
+ recordToolFiles(turn, name, block.input);
367
+ }
368
+ }
369
+ }
370
+ }
371
+
372
+ const latestTurn = result.turns.find((turn) => turn.turnId === result.latestTurnId)
373
+ || result.turns.at(-1);
374
+ result.latestUserPrompt = latestTurn?.userPrompts.at(-1) || result.userPrompts.at(-1) || '';
375
+ result.latestAssistantMessage = latestTurn?.assistantMessages.at(-1)
376
+ || result.assistantMessages.at(-1)
377
+ || '';
378
+ result.rawTextForDetection = redactSecrets([
379
+ ...result.userPrompts,
380
+ ...result.assistantMessages,
381
+ ].join('\n\n'));
382
+ return result;
383
+ }
384
+
385
+ function looksLikeCodexEvent(event) {
386
+ return event.payload !== undefined
387
+ || event.type === 'session_meta'
388
+ || event.type === 'response_item'
389
+ || event.type === 'turn_context'
390
+ || event.type === 'event_msg';
391
+ }
392
+
393
+ function looksLikeClaudeEvent(event) {
394
+ return (event.type === 'user' || event.type === 'assistant') && event.message !== undefined;
395
+ }
396
+
397
+ export function parseTranscriptContent(content, options = {}) {
398
+ for (const event of jsonLines(content)) {
399
+ if (looksLikeCodexEvent(event)) return parseCodexTranscriptContent(content, options);
400
+ if (looksLikeClaudeEvent(event)) return parseClaudeTranscriptContent(content, options);
401
+ }
402
+ return parseCodexTranscriptContent(content, options);
403
+ }
404
+
405
+ export function resolveTurnIdentity(transcript, requestedTurnId = '') {
406
+ const turns = Array.isArray(transcript?.turns) ? transcript.turns : [];
407
+ const requested = String(requestedTurnId || '');
408
+ let index = requested
409
+ ? turns.findIndex((turn) => String(turn?.turnId || '') === requested)
410
+ : -1;
411
+ if (requested && index < 0) return null;
412
+ if (index < 0 && transcript?.latestTurnId) {
413
+ index = turns.findIndex((turn) => String(turn?.turnId || '') === String(transcript.latestTurnId));
414
+ }
415
+ if (index < 0) index = turns.length - 1;
416
+ const turn = turns[index];
417
+ if (!turn?.turnId) return null;
418
+ return {
419
+ id: String(turn.turnId),
420
+ order: index + 1,
421
+ observedAt: String(turn.timestamp || ''),
422
+ };
423
+ }
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "name": "@wendkeep/mcp",
3
3
  "private": true,
4
- "type": "module"
4
+ "type": "module",
5
+ "exports": "./src/index.mjs"
5
6
  }
@@ -0,0 +1,33 @@
1
+ export const MCP_SERVER_KEY = 'wendkeep-vault';
2
+
3
+ export function mcpServerEntry(vaultPath) {
4
+ return {
5
+ type: 'stdio',
6
+ command: 'npx',
7
+ args: ['-y', '@bitbonsai/mcpvault@latest', vaultPath],
8
+ };
9
+ }
10
+
11
+ export function selectMcpServers(descriptors, skipIds = []) {
12
+ const skipSet = new Set(skipIds);
13
+ const servers = {};
14
+ for (const descriptor of descriptors || []) {
15
+ if (!descriptor || skipSet.has(descriptor.id)) continue;
16
+ if (typeof descriptor.key !== 'string' || !descriptor.key) continue;
17
+ if (!descriptor.entry || typeof descriptor.entry !== 'object') continue;
18
+ servers[descriptor.key] = descriptor.entry;
19
+ }
20
+ return servers;
21
+ }
22
+
23
+ export function mergeMcpConfig(existing, {
24
+ vaultPath,
25
+ withVault = true,
26
+ servers = {},
27
+ } = {}) {
28
+ const config = existing && typeof existing === 'object' ? { ...existing } : {};
29
+ config.mcpServers = { ...(config.mcpServers || {}) };
30
+ if (withVault) config.mcpServers[MCP_SERVER_KEY] = mcpServerEntry(vaultPath);
31
+ Object.assign(config.mcpServers, servers);
32
+ return config;
33
+ }
@@ -0,0 +1 @@
1
+ export * from './config.mjs';
package/src/init.mjs CHANGED
@@ -6,13 +6,12 @@ import { spawnSync } from 'node:child_process';
6
6
  import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
7
7
  import { basename, isAbsolute, join, resolve } from 'node:path';
8
8
  import { createInterface } from 'node:readline/promises';
9
+ import { MCP_SERVER_KEY, mergeMcpConfig } from '../packages/mcp/src/index.mjs';
9
10
  import {
10
11
  VAULT_FOLDERS,
11
12
  SESSION_HOOKS,
12
13
  CHANGE_NUDGE_HOOKS,
13
14
  CHANGE_GATE_HOOKS,
14
- MCP_SERVER_KEY,
15
- mcpServerEntry,
16
15
  hookCommand,
17
16
  hookCommandLocal,
18
17
  hookCommandLocalLegacy,
@@ -245,11 +244,11 @@ export function mergeCodexHooks(existing, { force = false } = {}) {
245
244
  }
246
245
 
247
246
  export function mergeMcp(existing, { vaultPath, withVault = true, companions = [], skipMcp = [] }) {
248
- const m = existing && typeof existing === 'object' ? { ...existing } : {};
249
- m.mcpServers = { ...(m.mcpServers || {}) };
250
- if (withVault) m.mcpServers[MCP_SERVER_KEY] = mcpServerEntry(vaultPath);
251
- Object.assign(m.mcpServers, companionMcpPatch(companions, skipMcp));
252
- return m;
247
+ return mergeMcpConfig(existing, {
248
+ vaultPath,
249
+ withVault,
250
+ servers: companionMcpPatch(companions, skipMcp),
251
+ });
253
252
  }
254
253
 
255
254
  // Run caveman's cross-agent installer (non-Claude skill coverage). Downloads the
package/src/taxonomy.mjs CHANGED
@@ -1,3 +1,34 @@
1
+ import {
2
+ MCP_SERVER_KEY,
3
+ mcpServerEntry,
4
+ selectMcpServers,
5
+ } from '../packages/mcp/src/index.mjs';
6
+ import {
7
+ CHANGE_GATE_HOOKS,
8
+ CHANGE_NUDGE_HOOKS,
9
+ CODEX_MATCHER_EVENTS,
10
+ SESSION_HOOKS,
11
+ codexHookEntry,
12
+ codexHookSpecs,
13
+ hookCommand,
14
+ hookCommandLocal,
15
+ hookCommandLocalLegacy,
16
+ } from '../packages/integrations/src/host-hooks.mjs';
17
+
18
+ export {
19
+ CHANGE_GATE_HOOKS,
20
+ CHANGE_NUDGE_HOOKS,
21
+ CODEX_MATCHER_EVENTS,
22
+ MCP_SERVER_KEY,
23
+ SESSION_HOOKS,
24
+ codexHookEntry,
25
+ codexHookSpecs,
26
+ hookCommand,
27
+ hookCommandLocal,
28
+ hookCommandLocalLegacy,
29
+ mcpServerEntry,
30
+ };
31
+
1
32
  // Shared, data-only constants for the wendkeep installer and CLI.
2
33
  // Kept free of side effects so both bin/ and src/ can import it cheaply.
3
34
 
@@ -93,94 +124,6 @@ export const RUNNABLE_HOOKS = [
93
124
  'plan-capture',
94
125
  ];
95
126
 
96
- // The MCP server entry wendkeep wires into .mcp.json so the agent can read/write the
97
- // vault. Uses the published mcpvault server (no secrets).
98
- export function mcpServerEntry(vaultPath) {
99
- return {
100
- type: 'stdio',
101
- command: 'npx',
102
- args: ['-y', '@bitbonsai/mcpvault@latest', vaultPath],
103
- };
104
- }
105
- export const MCP_SERVER_KEY = 'wendkeep-vault';
106
-
107
- // The three Claude Code session hooks, expressed as `wendkeep hook <name>` so the
108
- // installed package is the single source of truth (update with `npm update wendkeep`,
109
- // no re-copying). Returned as a spec the merge logic folds into settings.json.
110
- export const SESSION_HOOKS = [
111
- // Memory + active-change injection. Runs FIRST on SessionStart (order -10, folds before
112
- // session-start) so the agent gets CORE + DIGEST + the active change + lessons as context.
113
- // matcher 'startup|clear|compact' re-injects after a compaction/clear, not only cold startup.
114
- // timeout 45 (was 15): measured ~4s warm via npx, but Windows startup contention (several npx
115
- // cold-starts at once — a sibling MCP took 26s in a real log) blew 15s and silently dropped the
116
- // memory injection for the whole session.
117
- { event: 'SessionStart', matcher: 'startup|clear|compact', name: 'brain-inject', timeout: 45, order: -10, codex: true, statusMessage: 'wendkeep: injecting memory + active change' },
118
- { event: 'SessionStart', matcher: 'startup', name: 'session-start', timeout: 30, codex: true, statusMessage: 'wendkeep: opening Obsidian session' },
119
- { event: 'Stop', matcher: null, name: 'session-stop', timeout: 60, codex: true, statusMessage: 'wendkeep: writing session checkpoint' },
120
- { event: 'UserPromptSubmit', matcher: null, name: 'session-ensure', timeout: 30, codex: true, statusMessage: 'wendkeep: ensuring active session' },
121
- // Capture an interactive decision (AskUserQuestion) — options + the user's choice — into 04-Decisões.
122
- // codex: AskUserQuestion is a Claude-only tool; there is nothing to match on.
123
- { event: 'PostToolUse', matcher: 'AskUserQuestion', name: 'decision-capture', timeout: 15, statusMessage: 'wendkeep: recording decision' },
124
- // Refresh subagent/workflow telemetry as each subagent finishes (resilient to a missed Stop).
125
- { event: 'SubagentStop', matcher: null, name: 'subagent-stop', timeout: 20, codex: true, statusMessage: 'wendkeep: subagent telemetry' },
126
- // Log plan/task progress into the active session note when a task is marked complete.
127
- // codex: TaskCompleted is not in Codex's hook event enum.
128
- { event: 'TaskCompleted', matcher: null, name: 'task-log', timeout: 10, statusMessage: 'wendkeep: plan progress' },
129
- ];
130
-
131
- export function hookCommand(name) {
132
- return `npx wendkeep hook ${name}`;
133
- }
134
-
135
- // Forma node-direta do comando de hook: 1 processo (~100-250ms) em vez dos 3 do npx (cold-start
136
- // de segundos no Windows). Usada pelos hooks de ALTA FREQUÊNCIA (por prompt / por tool-call)
137
- // quando o projeto tem wendkeep instalado localmente; o init decide (hookCommandFor).
138
- export function hookCommandLocal(name) {
139
- return `node "${'${CLAUDE_PROJECT_DIR}'}/node_modules/wendkeep/hooks/${name}.mjs"`;
140
- }
141
-
142
- export function hookCommandLocalLegacy(name) {
143
- return `node node_modules/wendkeep/hooks/${name}.mjs`;
144
- }
145
-
146
- // Hooks do lifecycle de change (0.31.0) — enforcement do loop a2. Nudges (contexto/aviso/
147
- // cobrança/captura de plano) e gate (deny/ask no Bash). Separados em dois grupos para
148
- // preservar a opção futura de gates opt-in; hoje o init wira TODOS por default.
149
- // preferLocal: alta frequência → invocação node-direta quando houver instalação local.
150
- export const CHANGE_NUDGE_HOOKS = [
151
- { event: 'UserPromptSubmit', matcher: null, name: 'change-context', timeout: 15, order: 10, preferLocal: true, codex: true, statusMessage: 'wendkeep: change ping' },
152
- // codex: reads tool_input.file_path, which Codex's apply_patch envelope does not carry.
153
- { event: 'PostToolUse', matcher: 'Edit|Write|MultiEdit', name: 'change-warn', timeout: 10, order: 10, preferLocal: true, statusMessage: 'wendkeep: change warn' },
154
- // codex: no ExitPlanMode equivalent — update_plan is the running TODO list, not an approval.
155
- { event: 'PostToolUse', matcher: 'ExitPlanMode', name: 'plan-capture', timeout: 15, order: 10, preferLocal: true, statusMessage: 'wendkeep: capturing approved plan' },
156
- { event: 'Stop', matcher: null, name: 'change-nag', timeout: 15, order: 10, preferLocal: true, codex: true, statusMessage: 'wendkeep: open tasks check' },
157
- ];
158
- export const CHANGE_GATE_HOOKS = [
159
- // codex: reads tool_input.command; Codex's exec sends a raw string and exec_command an argv,
160
- // so the guard would silently fail OPEN — worse than absent, since the docs would promise it.
161
- { event: 'PreToolUse', matcher: 'Bash', name: 'change-guard', timeout: 10, order: 10, preferLocal: true, statusMessage: 'wendkeep: change gate' },
162
- ];
163
-
164
- // --- Codex projection ---------------------------------------------------------
165
- // Codex reads <project>/.codex/hooks.json (PascalCase event keys, same group shape as
166
- // Claude's settings.json). Only specs that opt in with `codex: true` are projected — the
167
- // rest carry a `// codex:` comment above them saying why. Three deltas from Claude, each
168
- // verified against codex-rs and each silent when wrong: the timeout key is `timeoutSec`
169
- // (`timeout` is not a field and falls through to a 600s default), there is no
170
- // ${CLAUDE_PROJECT_DIR} so `preferLocal` never applies, and matcher is only honoured on
171
- // SessionStart (UserPromptSubmit/Stop null it at discovery).
172
- export const CODEX_MATCHER_EVENTS = new Set(['SessionStart']);
173
-
174
- export function codexHookSpecs(specs) {
175
- return specs.filter((h) => h.codex === true && !h.command);
176
- }
177
-
178
- export function codexHookEntry(spec) {
179
- const entry = { type: 'command', command: hookCommand(spec.name), timeoutSec: spec.timeout };
180
- if (spec.statusMessage) entry.statusMessage = spec.statusMessage;
181
- return entry;
182
- }
183
-
184
127
  // --- companion plugins / MCP --------------------------------------------------
185
128
  // Optional tools wendkeep init can pin alongside the vault. Each is wired through
186
129
  // the MOST agent-agnostic mechanism it supports; the Claude Code plugin entry
@@ -281,14 +224,11 @@ export function companionSettingsPatch(ids) {
281
224
  // `skip` omits ids whose MCP is already configured elsewhere (e.g. dotcontext set
282
225
  // globally in ~/.claude.json — avoids a duplicate project-scoped server).
283
226
  export function companionMcpPatch(ids, skip = []) {
284
- const skipSet = new Set(skip);
285
- const servers = {};
286
- for (const id of ids) {
287
- if (skipSet.has(id)) continue;
288
- const c = COMPANION_BY_ID[id];
289
- if (c?.mcp) servers[c.mcp.key] = c.mcp.entry;
290
- }
291
- return servers;
227
+ const descriptors = ids.map((id) => {
228
+ const mcp = COMPANION_BY_ID[id]?.mcp;
229
+ return mcp ? { id, key: mcp.key, entry: mcp.entry } : { id };
230
+ });
231
+ return selectMcpServers(descriptors, skip);
292
232
  }
293
233
 
294
234
  // SessionStart hook specs wendkeep must author for companions that lack a native