wendkeep 0.65.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 +32 -0
- package/README.en.md +15 -5
- package/README.md +15 -5
- package/docs/en/commands/getting-started.md +8 -0
- package/docs/pt-BR/commands/getting-started.md +8 -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/src/taxonomy.mjs +24 -78
|
@@ -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
|
+
}
|
package/src/taxonomy.mjs
CHANGED
|
@@ -3,8 +3,31 @@ import {
|
|
|
3
3
|
mcpServerEntry,
|
|
4
4
|
selectMcpServers,
|
|
5
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';
|
|
6
17
|
|
|
7
|
-
export {
|
|
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
|
+
};
|
|
8
31
|
|
|
9
32
|
// Shared, data-only constants for the wendkeep installer and CLI.
|
|
10
33
|
// Kept free of side effects so both bin/ and src/ can import it cheaply.
|
|
@@ -101,83 +124,6 @@ export const RUNNABLE_HOOKS = [
|
|
|
101
124
|
'plan-capture',
|
|
102
125
|
];
|
|
103
126
|
|
|
104
|
-
// The three Claude Code session hooks, expressed as `wendkeep hook <name>` so the
|
|
105
|
-
// installed package is the single source of truth (update with `npm update wendkeep`,
|
|
106
|
-
// no re-copying). Returned as a spec the merge logic folds into settings.json.
|
|
107
|
-
export const SESSION_HOOKS = [
|
|
108
|
-
// Memory + active-change injection. Runs FIRST on SessionStart (order -10, folds before
|
|
109
|
-
// session-start) so the agent gets CORE + DIGEST + the active change + lessons as context.
|
|
110
|
-
// matcher 'startup|clear|compact' re-injects after a compaction/clear, not only cold startup.
|
|
111
|
-
// timeout 45 (was 15): measured ~4s warm via npx, but Windows startup contention (several npx
|
|
112
|
-
// cold-starts at once — a sibling MCP took 26s in a real log) blew 15s and silently dropped the
|
|
113
|
-
// memory injection for the whole session.
|
|
114
|
-
{ event: 'SessionStart', matcher: 'startup|clear|compact', name: 'brain-inject', timeout: 45, order: -10, codex: true, statusMessage: 'wendkeep: injecting memory + active change' },
|
|
115
|
-
{ event: 'SessionStart', matcher: 'startup', name: 'session-start', timeout: 30, codex: true, statusMessage: 'wendkeep: opening Obsidian session' },
|
|
116
|
-
{ event: 'Stop', matcher: null, name: 'session-stop', timeout: 60, codex: true, statusMessage: 'wendkeep: writing session checkpoint' },
|
|
117
|
-
{ event: 'UserPromptSubmit', matcher: null, name: 'session-ensure', timeout: 30, codex: true, statusMessage: 'wendkeep: ensuring active session' },
|
|
118
|
-
// Capture an interactive decision (AskUserQuestion) — options + the user's choice — into 04-Decisões.
|
|
119
|
-
// codex: AskUserQuestion is a Claude-only tool; there is nothing to match on.
|
|
120
|
-
{ event: 'PostToolUse', matcher: 'AskUserQuestion', name: 'decision-capture', timeout: 15, statusMessage: 'wendkeep: recording decision' },
|
|
121
|
-
// Refresh subagent/workflow telemetry as each subagent finishes (resilient to a missed Stop).
|
|
122
|
-
{ event: 'SubagentStop', matcher: null, name: 'subagent-stop', timeout: 20, codex: true, statusMessage: 'wendkeep: subagent telemetry' },
|
|
123
|
-
// Log plan/task progress into the active session note when a task is marked complete.
|
|
124
|
-
// codex: TaskCompleted is not in Codex's hook event enum.
|
|
125
|
-
{ event: 'TaskCompleted', matcher: null, name: 'task-log', timeout: 10, statusMessage: 'wendkeep: plan progress' },
|
|
126
|
-
];
|
|
127
|
-
|
|
128
|
-
export function hookCommand(name) {
|
|
129
|
-
return `npx wendkeep hook ${name}`;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Forma node-direta do comando de hook: 1 processo (~100-250ms) em vez dos 3 do npx (cold-start
|
|
133
|
-
// de segundos no Windows). Usada pelos hooks de ALTA FREQUÊNCIA (por prompt / por tool-call)
|
|
134
|
-
// quando o projeto tem wendkeep instalado localmente; o init decide (hookCommandFor).
|
|
135
|
-
export function hookCommandLocal(name) {
|
|
136
|
-
return `node "${'${CLAUDE_PROJECT_DIR}'}/node_modules/wendkeep/hooks/${name}.mjs"`;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export function hookCommandLocalLegacy(name) {
|
|
140
|
-
return `node node_modules/wendkeep/hooks/${name}.mjs`;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Hooks do lifecycle de change (0.31.0) — enforcement do loop a2. Nudges (contexto/aviso/
|
|
144
|
-
// cobrança/captura de plano) e gate (deny/ask no Bash). Separados em dois grupos para
|
|
145
|
-
// preservar a opção futura de gates opt-in; hoje o init wira TODOS por default.
|
|
146
|
-
// preferLocal: alta frequência → invocação node-direta quando houver instalação local.
|
|
147
|
-
export const CHANGE_NUDGE_HOOKS = [
|
|
148
|
-
{ event: 'UserPromptSubmit', matcher: null, name: 'change-context', timeout: 15, order: 10, preferLocal: true, codex: true, statusMessage: 'wendkeep: change ping' },
|
|
149
|
-
// codex: reads tool_input.file_path, which Codex's apply_patch envelope does not carry.
|
|
150
|
-
{ event: 'PostToolUse', matcher: 'Edit|Write|MultiEdit', name: 'change-warn', timeout: 10, order: 10, preferLocal: true, statusMessage: 'wendkeep: change warn' },
|
|
151
|
-
// codex: no ExitPlanMode equivalent — update_plan is the running TODO list, not an approval.
|
|
152
|
-
{ event: 'PostToolUse', matcher: 'ExitPlanMode', name: 'plan-capture', timeout: 15, order: 10, preferLocal: true, statusMessage: 'wendkeep: capturing approved plan' },
|
|
153
|
-
{ event: 'Stop', matcher: null, name: 'change-nag', timeout: 15, order: 10, preferLocal: true, codex: true, statusMessage: 'wendkeep: open tasks check' },
|
|
154
|
-
];
|
|
155
|
-
export const CHANGE_GATE_HOOKS = [
|
|
156
|
-
// codex: reads tool_input.command; Codex's exec sends a raw string and exec_command an argv,
|
|
157
|
-
// so the guard would silently fail OPEN — worse than absent, since the docs would promise it.
|
|
158
|
-
{ event: 'PreToolUse', matcher: 'Bash', name: 'change-guard', timeout: 10, order: 10, preferLocal: true, statusMessage: 'wendkeep: change gate' },
|
|
159
|
-
];
|
|
160
|
-
|
|
161
|
-
// --- Codex projection ---------------------------------------------------------
|
|
162
|
-
// Codex reads <project>/.codex/hooks.json (PascalCase event keys, same group shape as
|
|
163
|
-
// Claude's settings.json). Only specs that opt in with `codex: true` are projected — the
|
|
164
|
-
// rest carry a `// codex:` comment above them saying why. Three deltas from Claude, each
|
|
165
|
-
// verified against codex-rs and each silent when wrong: the timeout key is `timeoutSec`
|
|
166
|
-
// (`timeout` is not a field and falls through to a 600s default), there is no
|
|
167
|
-
// ${CLAUDE_PROJECT_DIR} so `preferLocal` never applies, and matcher is only honoured on
|
|
168
|
-
// SessionStart (UserPromptSubmit/Stop null it at discovery).
|
|
169
|
-
export const CODEX_MATCHER_EVENTS = new Set(['SessionStart']);
|
|
170
|
-
|
|
171
|
-
export function codexHookSpecs(specs) {
|
|
172
|
-
return specs.filter((h) => h.codex === true && !h.command);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export function codexHookEntry(spec) {
|
|
176
|
-
const entry = { type: 'command', command: hookCommand(spec.name), timeoutSec: spec.timeout };
|
|
177
|
-
if (spec.statusMessage) entry.statusMessage = spec.statusMessage;
|
|
178
|
-
return entry;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
127
|
// --- companion plugins / MCP --------------------------------------------------
|
|
182
128
|
// Optional tools wendkeep init can pin alongside the vault. Each is wired through
|
|
183
129
|
// the MOST agent-agnostic mechanism it supports; the Claude Code plugin entry
|