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.
- package/CHANGELOG.md +52 -0
- package/README.en.md +24 -6
- package/README.md +24 -6
- package/docs/en/commands/getting-started.md +13 -0
- package/docs/pt-BR/commands/getting-started.md +13 -0
- package/hooks/obsidian-common.mjs +24 -124
- package/hooks/session-identity.mjs +21 -136
- package/hooks/session-stop.mjs +18 -384
- package/hooks/token-usage.mjs +12 -56
- package/package.json +13 -4
- package/packages/integrations/package.json +2 -1
- package/packages/integrations/src/hook-envelope.mjs +103 -0
- package/packages/integrations/src/host-hooks.mjs +80 -0
- package/packages/integrations/src/index.mjs +6 -0
- package/packages/integrations/src/prompt-content.mjs +20 -0
- package/packages/integrations/src/session-identity.mjs +171 -0
- package/packages/integrations/src/transcript-usage.mjs +53 -0
- package/packages/integrations/src/transcripts.mjs +423 -0
- package/packages/mcp/package.json +2 -1
- package/packages/mcp/src/config.mjs +33 -0
- package/packages/mcp/src/index.mjs +1 -0
- package/src/init.mjs +6 -7
- package/src/taxonomy.mjs +36 -96
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Host hook specifications shared by the installer and CLI. This catalog is
|
|
2
|
+
// data-only and side-effect free so host adapters can import it cheaply.
|
|
3
|
+
|
|
4
|
+
// The three Claude Code session hooks, expressed as `wendkeep hook <name>` so the
|
|
5
|
+
// installed package is the single source of truth (update with `npm update wendkeep`,
|
|
6
|
+
// no re-copying). Returned as a spec the merge logic folds into settings.json.
|
|
7
|
+
export const SESSION_HOOKS = [
|
|
8
|
+
// Memory + active-change injection. Runs FIRST on SessionStart (order -10, folds before
|
|
9
|
+
// session-start) so the agent gets CORE + DIGEST + the active change + lessons as context.
|
|
10
|
+
// matcher 'startup|clear|compact' re-injects after a compaction/clear, not only cold startup.
|
|
11
|
+
// timeout 45 (was 15): measured ~4s warm via npx, but Windows startup contention (several npx
|
|
12
|
+
// cold-starts at once — a sibling MCP took 26s in a real log) blew 15s and silently dropped the
|
|
13
|
+
// memory injection for the whole session.
|
|
14
|
+
{ event: 'SessionStart', matcher: 'startup|clear|compact', name: 'brain-inject', timeout: 45, order: -10, codex: true, statusMessage: 'wendkeep: injecting memory + active change' },
|
|
15
|
+
{ event: 'SessionStart', matcher: 'startup', name: 'session-start', timeout: 30, codex: true, statusMessage: 'wendkeep: opening Obsidian session' },
|
|
16
|
+
{ event: 'Stop', matcher: null, name: 'session-stop', timeout: 60, codex: true, statusMessage: 'wendkeep: writing session checkpoint' },
|
|
17
|
+
{ event: 'UserPromptSubmit', matcher: null, name: 'session-ensure', timeout: 30, codex: true, statusMessage: 'wendkeep: ensuring active session' },
|
|
18
|
+
// Capture an interactive decision (AskUserQuestion) — options + the user's choice — into 04-Decisões.
|
|
19
|
+
// codex: AskUserQuestion is a Claude-only tool; there is nothing to match on.
|
|
20
|
+
{ event: 'PostToolUse', matcher: 'AskUserQuestion', name: 'decision-capture', timeout: 15, statusMessage: 'wendkeep: recording decision' },
|
|
21
|
+
// Refresh subagent/workflow telemetry as each subagent finishes (resilient to a missed Stop).
|
|
22
|
+
{ event: 'SubagentStop', matcher: null, name: 'subagent-stop', timeout: 20, codex: true, statusMessage: 'wendkeep: subagent telemetry' },
|
|
23
|
+
// Log plan/task progress into the active session note when a task is marked complete.
|
|
24
|
+
// codex: TaskCompleted is not in Codex's hook event enum.
|
|
25
|
+
{ event: 'TaskCompleted', matcher: null, name: 'task-log', timeout: 10, statusMessage: 'wendkeep: plan progress' },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export function hookCommand(name) {
|
|
29
|
+
return `npx wendkeep hook ${name}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Forma node-direta do comando de hook: 1 processo (~100-250ms) em vez dos 3 do npx (cold-start
|
|
33
|
+
// de segundos no Windows). Usada pelos hooks de ALTA FREQUÊNCIA (por prompt / por tool-call)
|
|
34
|
+
// quando o projeto tem wendkeep instalado localmente; o init decide (hookCommandFor).
|
|
35
|
+
export function hookCommandLocal(name) {
|
|
36
|
+
return `node "${'${CLAUDE_PROJECT_DIR}'}/node_modules/wendkeep/hooks/${name}.mjs"`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function hookCommandLocalLegacy(name) {
|
|
40
|
+
return `node node_modules/wendkeep/hooks/${name}.mjs`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Hooks do lifecycle de change (0.31.0) — enforcement do loop a2. Nudges (contexto/aviso/
|
|
44
|
+
// cobrança/captura de plano) e gate (deny/ask no Bash). Separados em dois grupos para
|
|
45
|
+
// preservar a opção futura de gates opt-in; hoje o init wira TODOS por default.
|
|
46
|
+
// preferLocal: alta frequência → invocação node-direta quando houver instalação local.
|
|
47
|
+
export const CHANGE_NUDGE_HOOKS = [
|
|
48
|
+
{ event: 'UserPromptSubmit', matcher: null, name: 'change-context', timeout: 15, order: 10, preferLocal: true, codex: true, statusMessage: 'wendkeep: change ping' },
|
|
49
|
+
// codex: reads tool_input.file_path, which Codex's apply_patch envelope does not carry.
|
|
50
|
+
{ event: 'PostToolUse', matcher: 'Edit|Write|MultiEdit', name: 'change-warn', timeout: 10, order: 10, preferLocal: true, statusMessage: 'wendkeep: change warn' },
|
|
51
|
+
// codex: no ExitPlanMode equivalent — update_plan is the running TODO list, not an approval.
|
|
52
|
+
{ event: 'PostToolUse', matcher: 'ExitPlanMode', name: 'plan-capture', timeout: 15, order: 10, preferLocal: true, statusMessage: 'wendkeep: capturing approved plan' },
|
|
53
|
+
{ event: 'Stop', matcher: null, name: 'change-nag', timeout: 15, order: 10, preferLocal: true, codex: true, statusMessage: 'wendkeep: open tasks check' },
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
export const CHANGE_GATE_HOOKS = [
|
|
57
|
+
// codex: reads tool_input.command; Codex's exec sends a raw string and exec_command an argv,
|
|
58
|
+
// so the guard would silently fail OPEN — worse than absent, since the docs would promise it.
|
|
59
|
+
{ event: 'PreToolUse', matcher: 'Bash', name: 'change-guard', timeout: 10, order: 10, preferLocal: true, statusMessage: 'wendkeep: change gate' },
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
// --- Codex projection ---------------------------------------------------------
|
|
63
|
+
// Codex reads <project>/.codex/hooks.json (PascalCase event keys, same group shape as
|
|
64
|
+
// Claude's settings.json). Only specs that opt in with `codex: true` are projected — the
|
|
65
|
+
// rest carry a `// codex:` comment above them saying why. Three deltas from Claude, each
|
|
66
|
+
// verified against codex-rs and each silent when wrong: the timeout key is `timeoutSec`
|
|
67
|
+
// (`timeout` is not a field and falls through to a 600s default), there is no
|
|
68
|
+
// ${CLAUDE_PROJECT_DIR} so `preferLocal` never applies, and matcher is only honoured on
|
|
69
|
+
// SessionStart (UserPromptSubmit/Stop null it at discovery).
|
|
70
|
+
export const CODEX_MATCHER_EVENTS = new Set(['SessionStart']);
|
|
71
|
+
|
|
72
|
+
export function codexHookSpecs(specs) {
|
|
73
|
+
return specs.filter((h) => h.codex === true && !h.command);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function codexHookEntry(spec) {
|
|
77
|
+
const entry = { type: 'command', command: hookCommand(spec.name), timeoutSec: spec.timeout };
|
|
78
|
+
if (spec.statusMessage) entry.statusMessage = spec.statusMessage;
|
|
79
|
+
return entry;
|
|
80
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function isBootstrapPrompt(text = '') {
|
|
2
|
+
const clean = String(text || '').trim();
|
|
3
|
+
return clean.startsWith('# AGENTS.md instructions')
|
|
4
|
+
|| clean.startsWith('<environment_context>')
|
|
5
|
+
|| clean.startsWith('<permissions instructions>')
|
|
6
|
+
|| clean.startsWith('<recommended_plugins>')
|
|
7
|
+
|| clean.includes('You are Codex, a coding agent')
|
|
8
|
+
|| clean.startsWith('## Memory');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function redactSecrets(text) {
|
|
12
|
+
if (!text) return '';
|
|
13
|
+
return String(text)
|
|
14
|
+
.replace(/\b(sk-[A-Za-z0-9_-]{12,})\b/g, '[REDACTED_SECRET]')
|
|
15
|
+
.replace(/\b(whsec_[A-Za-z0-9_/-]{8,})\b/g, '[REDACTED_SECRET]')
|
|
16
|
+
.replace(/\b(gh[pousr]_[A-Za-z0-9_]{12,})\b/g, '[REDACTED_SECRET]')
|
|
17
|
+
.replace(/\b(xox[baprs]-[A-Za-z0-9-]{12,})\b/g, '[REDACTED_SECRET]')
|
|
18
|
+
.replace(/\b([A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|API_KEY)[A-Z0-9_]*)\s*[:=]\s*["']?[^"'\s]+/gi, '$1=[REDACTED_SECRET]')
|
|
19
|
+
.replace(/:\/\/([^:\s/@]+):([^@\s/]+)@/g, '://[REDACTED_SECRET]@');
|
|
20
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
function parseJsonLines(content = '') {
|
|
2
|
+
return String(content || '').split('\n').filter(Boolean).map((line) => {
|
|
3
|
+
try { return JSON.parse(line); } catch { return null; }
|
|
4
|
+
}).filter(Boolean);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function inspectTranscriptIdentityContent(content, { fallbackTranscriptId = '' } = {}) {
|
|
8
|
+
const lines = parseJsonLines(content);
|
|
9
|
+
const codexMeta = lines.find((event) => event.type === 'session_meta')?.payload;
|
|
10
|
+
if (codexMeta) {
|
|
11
|
+
return {
|
|
12
|
+
transcriptProvider: 'openai',
|
|
13
|
+
provider: 'codex',
|
|
14
|
+
canonicalConversationId: codexMeta.session_id || codexMeta.id || '',
|
|
15
|
+
transcriptId: codexMeta.id || fallbackTranscriptId,
|
|
16
|
+
parentConversationId: codexMeta.parent_thread_id || codexMeta.forked_from_id || '',
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
const claudeEvent = lines.find((event) => event.sessionId);
|
|
20
|
+
if (claudeEvent) {
|
|
21
|
+
return {
|
|
22
|
+
transcriptProvider: 'anthropic',
|
|
23
|
+
provider: 'claude',
|
|
24
|
+
canonicalConversationId: claudeEvent.sessionId,
|
|
25
|
+
transcriptId: fallbackTranscriptId,
|
|
26
|
+
parentConversationId: '',
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
transcriptProvider: 'unknown',
|
|
31
|
+
provider: 'unknown',
|
|
32
|
+
canonicalConversationId: '',
|
|
33
|
+
transcriptId: '',
|
|
34
|
+
parentConversationId: '',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function compatible(provider, transcriptProvider) {
|
|
39
|
+
return (provider === 'codex' && transcriptProvider === 'openai')
|
|
40
|
+
|| (provider === 'claude' && transcriptProvider === 'anthropic');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function canonicalUuid(value = '') {
|
|
44
|
+
const id = String(value || '').trim().toLowerCase();
|
|
45
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(id) ? id : '';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function pathBasename(path = '') {
|
|
49
|
+
const normalized = String(path || '').replace(/\\/g, '/');
|
|
50
|
+
return normalized.slice(normalized.lastIndexOf('/') + 1).replace(/\.jsonl$/i, '');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function providerMatches(entry, provider) {
|
|
54
|
+
return entry?.provider === provider;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function transcriptsMatch(a, b) {
|
|
58
|
+
if (!a || !b) return false;
|
|
59
|
+
const normalize = (path) => String(path || '').replace(/\\/g, '/').toLowerCase();
|
|
60
|
+
const normalizedA = normalize(a);
|
|
61
|
+
const normalizedB = normalize(b);
|
|
62
|
+
if (normalizedA === normalizedB) return true;
|
|
63
|
+
const basename = (path) => path.slice(path.lastIndexOf('/') + 1);
|
|
64
|
+
const basenameA = basename(normalizedA);
|
|
65
|
+
return Boolean(basenameA) && basenameA === basename(normalizedB);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function resolveSessionIdentitySnapshot({
|
|
69
|
+
input = {},
|
|
70
|
+
provider = 'codex',
|
|
71
|
+
codexThreadId = '',
|
|
72
|
+
transcriptPath = '',
|
|
73
|
+
inspected = {},
|
|
74
|
+
registry = { version: 2, sessions: {} },
|
|
75
|
+
} = {}) {
|
|
76
|
+
const explicitTranscriptPath = String(transcriptPath || '');
|
|
77
|
+
const hookId = input.session_id || input.sessionId || '';
|
|
78
|
+
const threadId = canonicalUuid(
|
|
79
|
+
input.codex_thread_id || input.codexThreadId || codexThreadId || '',
|
|
80
|
+
);
|
|
81
|
+
const transcriptConversationId = canonicalUuid(inspected.canonicalConversationId);
|
|
82
|
+
|
|
83
|
+
if (provider === 'codex' && transcriptConversationId && threadId
|
|
84
|
+
&& transcriptConversationId !== threadId) {
|
|
85
|
+
return {
|
|
86
|
+
state: 'deferred',
|
|
87
|
+
provider,
|
|
88
|
+
transcriptPath: explicitTranscriptPath,
|
|
89
|
+
diagnostics: [`CODEX_THREAD_ID diverge do session_id do transcript (${threadId} != ${transcriptConversationId})`],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (provider === 'codex' && !inspected.canonicalConversationId && threadId) {
|
|
94
|
+
return {
|
|
95
|
+
state: 'resolved',
|
|
96
|
+
provider,
|
|
97
|
+
canonicalConversationId: threadId,
|
|
98
|
+
hookSessionId: hookId,
|
|
99
|
+
transcriptPath: explicitTranscriptPath,
|
|
100
|
+
transcriptId: explicitTranscriptPath ? pathBasename(explicitTranscriptPath) : threadId,
|
|
101
|
+
parentConversationId: '',
|
|
102
|
+
diagnostics: [],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (provider === 'claude' && hookId && !inspected.canonicalConversationId) {
|
|
107
|
+
return {
|
|
108
|
+
state: 'resolved',
|
|
109
|
+
provider,
|
|
110
|
+
canonicalConversationId: hookId,
|
|
111
|
+
hookSessionId: hookId,
|
|
112
|
+
transcriptPath: explicitTranscriptPath,
|
|
113
|
+
transcriptId: explicitTranscriptPath ? pathBasename(explicitTranscriptPath) : hookId,
|
|
114
|
+
parentConversationId: '',
|
|
115
|
+
diagnostics: [],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!explicitTranscriptPath && hookId) {
|
|
120
|
+
const entry = registry?.sessions?.[hookId];
|
|
121
|
+
if (entry?.transcript_path && providerMatches(entry, provider)) {
|
|
122
|
+
return {
|
|
123
|
+
state: 'resolved',
|
|
124
|
+
provider,
|
|
125
|
+
canonicalConversationId: hookId,
|
|
126
|
+
hookSessionId: hookId,
|
|
127
|
+
transcriptPath: entry.transcript_path,
|
|
128
|
+
transcriptId: entry.transcript_id || pathBasename(entry.transcript_path),
|
|
129
|
+
parentConversationId: '',
|
|
130
|
+
diagnostics: ['transcript recuperado do SESSION_REGISTRY'],
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!explicitTranscriptPath || !inspected.canonicalConversationId) {
|
|
136
|
+
return {
|
|
137
|
+
state: 'deferred',
|
|
138
|
+
provider,
|
|
139
|
+
transcriptPath: explicitTranscriptPath,
|
|
140
|
+
diagnostics: ['transcript ausente ou sem identidade canônica'],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (!compatible(provider, inspected.transcriptProvider)) {
|
|
144
|
+
return {
|
|
145
|
+
state: 'deferred',
|
|
146
|
+
provider,
|
|
147
|
+
transcriptPath: explicitTranscriptPath,
|
|
148
|
+
diagnostics: [`provider ${provider} incompatível com transcript ${inspected.transcriptProvider}`],
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const byTranscript = Object.entries(registry?.sessions || {}).find(([, entry]) => {
|
|
153
|
+
if (!providerMatches(entry, provider)) return false;
|
|
154
|
+
const paths = [
|
|
155
|
+
...(Array.isArray(entry?.transcript_paths) ? entry.transcript_paths : []),
|
|
156
|
+
entry?.transcript_path,
|
|
157
|
+
].filter(Boolean);
|
|
158
|
+
return paths.some((path) => transcriptsMatch(path, explicitTranscriptPath));
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
state: 'resolved',
|
|
163
|
+
provider,
|
|
164
|
+
canonicalConversationId: byTranscript?.[0] || inspected.canonicalConversationId,
|
|
165
|
+
hookSessionId: hookId,
|
|
166
|
+
transcriptPath: explicitTranscriptPath,
|
|
167
|
+
transcriptId: inspected.transcriptId,
|
|
168
|
+
parentConversationId: inspected.parentConversationId,
|
|
169
|
+
diagnostics: [],
|
|
170
|
+
};
|
|
171
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export function emptyTokenUsage() {
|
|
2
|
+
return {
|
|
3
|
+
input: 0,
|
|
4
|
+
cached: 0,
|
|
5
|
+
cacheWrite: 0,
|
|
6
|
+
cacheWrite1h: 0,
|
|
7
|
+
output: 0,
|
|
8
|
+
reasoning: 0,
|
|
9
|
+
total: 0,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function normalizeCodexUsage(raw = {}) {
|
|
14
|
+
const inputAll = Number(raw.input_tokens || 0);
|
|
15
|
+
const cached = Math.min(Number(raw.cached_input_tokens || 0), inputAll);
|
|
16
|
+
const output = Number(raw.output_tokens || 0);
|
|
17
|
+
return {
|
|
18
|
+
input: inputAll - cached,
|
|
19
|
+
cached,
|
|
20
|
+
cacheWrite: 0,
|
|
21
|
+
cacheWrite1h: 0,
|
|
22
|
+
output,
|
|
23
|
+
reasoning: Number(raw.reasoning_output_tokens || 0),
|
|
24
|
+
total: Number(raw.total_tokens || 0) || inputAll + output,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function normalizeClaudeUsage(raw = {}) {
|
|
29
|
+
const input = Number(raw.input_tokens || 0);
|
|
30
|
+
const cached = Number(raw.cache_read_input_tokens || 0);
|
|
31
|
+
const cacheWrite = Number(raw.cache_creation_input_tokens || 0);
|
|
32
|
+
const cacheWrite1h = Number(raw.cache_creation?.ephemeral_1h_input_tokens || 0);
|
|
33
|
+
const output = Number(raw.output_tokens || 0);
|
|
34
|
+
return {
|
|
35
|
+
input,
|
|
36
|
+
cached,
|
|
37
|
+
cacheWrite,
|
|
38
|
+
cacheWrite1h: Math.min(cacheWrite1h, cacheWrite),
|
|
39
|
+
output,
|
|
40
|
+
reasoning: 0,
|
|
41
|
+
total: input + cached + cacheWrite + output,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function addUsage(target, usage) {
|
|
46
|
+
target.input += usage.input;
|
|
47
|
+
target.cached += usage.cached;
|
|
48
|
+
target.cacheWrite += usage.cacheWrite;
|
|
49
|
+
target.cacheWrite1h += usage.cacheWrite1h;
|
|
50
|
+
target.output += usage.output;
|
|
51
|
+
target.reasoning += usage.reasoning;
|
|
52
|
+
target.total += usage.total;
|
|
53
|
+
}
|