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
package/hooks/session-stop.mjs
CHANGED
|
@@ -22,6 +22,13 @@ import {
|
|
|
22
22
|
recordStopMemoryOutcome,
|
|
23
23
|
stageStopMemoryAttempt,
|
|
24
24
|
} from './session-memory-lifecycle.mjs';
|
|
25
|
+
import {
|
|
26
|
+
parseClaudeTranscriptContent,
|
|
27
|
+
parseCodexTranscriptContent,
|
|
28
|
+
parseTranscriptContent,
|
|
29
|
+
resolveTurnIdentity,
|
|
30
|
+
} from '../packages/integrations/src/transcripts.mjs';
|
|
31
|
+
export { resolveTurnIdentity };
|
|
25
32
|
import {
|
|
26
33
|
ensureDir,
|
|
27
34
|
findActiveSessionByTranscript,
|
|
@@ -52,16 +59,6 @@ import {
|
|
|
52
59
|
applyStopActivation,
|
|
53
60
|
} from './obsidian-common.mjs';
|
|
54
61
|
|
|
55
|
-
function extractContentText(content) {
|
|
56
|
-
if (typeof content === 'string') return content;
|
|
57
|
-
if (!Array.isArray(content)) return '';
|
|
58
|
-
return content
|
|
59
|
-
.map((item) => item?.text || item?.input_text || item?.output_text || '')
|
|
60
|
-
.filter(Boolean)
|
|
61
|
-
.join('\n')
|
|
62
|
-
.trim();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
62
|
// Tags injetadas pelo harness (não são fala humana): notificações de task,
|
|
66
63
|
// reminders do sistema, stdout de comando local, wrappers de slash-command e
|
|
67
64
|
// contexto da IDE. Nunca devem virar título/Pedido/Usuário de iteração no Vault.
|
|
@@ -101,36 +98,6 @@ function createTurn(turnId = '', timestamp = '') {
|
|
|
101
98
|
};
|
|
102
99
|
}
|
|
103
100
|
|
|
104
|
-
function addConversation(turn, role, value) {
|
|
105
|
-
if (!turn) return;
|
|
106
|
-
const text = redactSecrets(String(value || '').trim());
|
|
107
|
-
if (!text) return;
|
|
108
|
-
const exists = turn.conversation.some((item) => item.role === role && item.text === text);
|
|
109
|
-
if (!exists) turn.conversation.push({ role, text });
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function extractPaths(text) {
|
|
113
|
-
const paths = [];
|
|
114
|
-
const addPath = (value) => {
|
|
115
|
-
const path = normalizeExtractedPath(value);
|
|
116
|
-
if (!shouldIgnoreExtractedPath(path) && !paths.includes(path)) paths.push(path);
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
const windowsRegex = /[A-Za-z]:[\\/]+[^"'`\r\n{}()[\],]+\.[A-Za-z0-9]+(?::\d+)?/g;
|
|
120
|
-
let match;
|
|
121
|
-
const source = String(text || '');
|
|
122
|
-
while ((match = windowsRegex.exec(source)) !== null) {
|
|
123
|
-
addPath(match[0]);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const masked = source.replace(windowsRegex, ' ');
|
|
127
|
-
const regex = /(?:^|[\s"'`(])((?:\/(?:home|mnt)\/|\.{1,2}\/|[A-Za-z0-9_.-]+\/)[A-Za-z0-9_./@+:-]+\.[A-Za-z0-9]+(?::\d+)?)/g;
|
|
128
|
-
while ((match = regex.exec(masked)) !== null) {
|
|
129
|
-
addPath(match[1]);
|
|
130
|
-
}
|
|
131
|
-
return paths.slice(0, 20);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
101
|
const REPO_ROOT = String(process.cwd() || '')
|
|
135
102
|
.replace(/\\+/g, '/')
|
|
136
103
|
.replace(/\/+$/, '');
|
|
@@ -196,361 +163,28 @@ function normalizeFileListLine(line) {
|
|
|
196
163
|
return `- \`${normalizeExtractedPath(match[1])}\``;
|
|
197
164
|
}
|
|
198
165
|
|
|
199
|
-
function
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
while ((match = regex.exec(text || '')) !== null) addUnique(files, match[1]);
|
|
204
|
-
return files;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function parseToolArguments(args) {
|
|
208
|
-
if (!args) return {};
|
|
209
|
-
if (typeof args === 'object') return args;
|
|
210
|
-
try {
|
|
211
|
-
return JSON.parse(args);
|
|
212
|
-
} catch {
|
|
213
|
-
return { raw: String(args) };
|
|
214
|
-
}
|
|
166
|
+
function transcriptContent(transcriptPath) {
|
|
167
|
+
return transcriptPath && existsSync(transcriptPath)
|
|
168
|
+
? readFileSync(transcriptPath, 'utf-8')
|
|
169
|
+
: '';
|
|
215
170
|
}
|
|
216
171
|
|
|
217
|
-
function
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (typeof value === 'object') return Object.values(value).map(toolArgumentText).filter(Boolean).join('\n');
|
|
222
|
-
return String(value);
|
|
172
|
+
function transcriptOptions() {
|
|
173
|
+
let vaultRoot = '';
|
|
174
|
+
try { vaultRoot = getVaultBase(); } catch { /* projeto sem binding */ }
|
|
175
|
+
return { repoRoot: process.cwd(), vaultRoot };
|
|
223
176
|
}
|
|
224
177
|
|
|
225
178
|
export function parseCodexTranscript(transcriptPath) {
|
|
226
|
-
|
|
227
|
-
provider: 'codex',
|
|
228
|
-
sessionId: '',
|
|
229
|
-
model: '',
|
|
230
|
-
latestTurnId: '',
|
|
231
|
-
latestUserPrompt: '',
|
|
232
|
-
latestAssistantMessage: '',
|
|
233
|
-
userPrompts: [],
|
|
234
|
-
assistantMessages: [],
|
|
235
|
-
tools: [],
|
|
236
|
-
consultedFiles: [],
|
|
237
|
-
changedFiles: [],
|
|
238
|
-
turns: [],
|
|
239
|
-
rawTextForDetection: '',
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
if (!transcriptPath || !existsSync(transcriptPath)) return result;
|
|
243
|
-
|
|
244
|
-
const eventUserPrompts = [];
|
|
245
|
-
let currentTurn = null;
|
|
246
|
-
const ensureTurn = (turnId = '', timestamp = '') => {
|
|
247
|
-
const normalized = turnId || currentTurn?.turnId || `turn-${result.turns.length + 1}`;
|
|
248
|
-
const existing = result.turns.find((turn) => turn.turnId === normalized);
|
|
249
|
-
if (existing) {
|
|
250
|
-
currentTurn = existing;
|
|
251
|
-
return existing;
|
|
252
|
-
}
|
|
253
|
-
currentTurn = createTurn(normalized, timestamp);
|
|
254
|
-
result.turns.push(currentTurn);
|
|
255
|
-
return currentTurn;
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
const lines = readFileSync(transcriptPath, 'utf-8').split('\n').filter(Boolean);
|
|
259
|
-
for (const line of lines) {
|
|
260
|
-
let event;
|
|
261
|
-
try { event = JSON.parse(line); } catch { continue; }
|
|
262
|
-
|
|
263
|
-
if (event.type === 'session_meta') {
|
|
264
|
-
result.sessionId = event.payload?.id || result.sessionId;
|
|
265
|
-
result.model = event.payload?.model || event.payload?.model_provider || result.model;
|
|
266
|
-
continue;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
if (event.type === 'event_msg' && event.payload?.type === 'task_started') {
|
|
270
|
-
result.latestTurnId = event.payload.turn_id || result.latestTurnId;
|
|
271
|
-
ensureTurn(result.latestTurnId, event.timestamp);
|
|
272
|
-
continue;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
if (event.type === 'turn_context') {
|
|
276
|
-
result.latestTurnId = event.payload?.turn_id || result.latestTurnId;
|
|
277
|
-
result.model = event.payload?.model || result.model;
|
|
278
|
-
ensureTurn(result.latestTurnId, event.timestamp);
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (event.type === 'event_msg' && event.payload?.type === 'user_message') {
|
|
283
|
-
const text = event.payload.message || '';
|
|
284
|
-
if (text && !shouldIgnoreUserText(text)) {
|
|
285
|
-
const turn = ensureTurn(event.payload.turn_id || result.latestTurnId, event.timestamp);
|
|
286
|
-
addUnique(eventUserPrompts, text);
|
|
287
|
-
addUnique(result.userPrompts, text);
|
|
288
|
-
addUnique(turn.userPrompts, text);
|
|
289
|
-
addConversation(turn, 'Usuário', text);
|
|
290
|
-
}
|
|
291
|
-
continue;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
if (event.type === 'event_msg' && event.payload?.type === 'agent_message') {
|
|
295
|
-
const text = event.payload.message || event.payload.text || '';
|
|
296
|
-
if (text) {
|
|
297
|
-
const turn = ensureTurn(event.payload.turn_id || result.latestTurnId, event.timestamp);
|
|
298
|
-
addUnique(result.assistantMessages, text);
|
|
299
|
-
addUnique(turn.assistantMessages, text);
|
|
300
|
-
addConversation(turn, 'Assistente', text);
|
|
301
|
-
}
|
|
302
|
-
continue;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
if (event.type === 'event_msg' && event.payload?.type === 'token_count') {
|
|
306
|
-
const raw = event.payload?.info?.last_token_usage;
|
|
307
|
-
if (raw) {
|
|
308
|
-
const turn = currentTurn || ensureTurn(result.latestTurnId, event.timestamp);
|
|
309
|
-
addUsage(turn.usage, normalizeCodexUsage(raw));
|
|
310
|
-
if (event.payload?.info?.model) turn.model = event.payload.info.model;
|
|
311
|
-
}
|
|
312
|
-
continue;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (event.type !== 'response_item') continue;
|
|
316
|
-
const payload = event.payload || {};
|
|
317
|
-
|
|
318
|
-
if (payload.type === 'message') {
|
|
319
|
-
const text = extractContentText(payload.content);
|
|
320
|
-
if (!text) continue;
|
|
321
|
-
const turn = ensureTurn(payload.turn_id || event.turn_id || result.latestTurnId, event.timestamp);
|
|
322
|
-
if (payload.role === 'user' && !shouldIgnoreUserText(text)) {
|
|
323
|
-
addUnique(result.userPrompts, text);
|
|
324
|
-
addUnique(turn.userPrompts, text);
|
|
325
|
-
addConversation(turn, 'Usuário', text);
|
|
326
|
-
}
|
|
327
|
-
if (payload.role === 'assistant') {
|
|
328
|
-
addUnique(result.assistantMessages, text);
|
|
329
|
-
addUnique(turn.assistantMessages, text);
|
|
330
|
-
addConversation(turn, 'Assistente', text);
|
|
331
|
-
}
|
|
332
|
-
continue;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
if (payload.type === 'function_call') {
|
|
336
|
-
addUnique(result.tools, payload.name || 'function_call');
|
|
337
|
-
const turn = ensureTurn(payload.turn_id || event.turn_id || result.latestTurnId, event.timestamp);
|
|
338
|
-
addUnique(turn.tools, payload.name || 'function_call');
|
|
339
|
-
const parsed = parseToolArguments(payload.arguments);
|
|
340
|
-
const combined = typeof parsed.raw === 'string'
|
|
341
|
-
? parsed.raw
|
|
342
|
-
: toolArgumentText(parsed);
|
|
343
|
-
|
|
344
|
-
for (const path of extractPaths(combined)) {
|
|
345
|
-
addUnique(result.consultedFiles, path);
|
|
346
|
-
addUnique(turn.consultedFiles, path);
|
|
347
|
-
}
|
|
348
|
-
for (const path of extractPatchFiles(combined)) {
|
|
349
|
-
addUnique(result.changedFiles, path);
|
|
350
|
-
addUnique(turn.changedFiles, path);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
if (/apply_patch|edit|write|create/i.test(payload.name || '')) {
|
|
354
|
-
for (const path of extractPaths(combined)) {
|
|
355
|
-
addUnique(result.changedFiles, path);
|
|
356
|
-
addUnique(turn.changedFiles, path);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
if (payload.type === 'tool_search_call') {
|
|
362
|
-
const turn = ensureTurn(payload.turn_id || event.turn_id || result.latestTurnId, event.timestamp);
|
|
363
|
-
addUnique(result.tools, 'tool_search');
|
|
364
|
-
addUnique(turn.tools, 'tool_search');
|
|
365
|
-
}
|
|
366
|
-
if (payload.type === 'web_search_call') {
|
|
367
|
-
const turn = ensureTurn(payload.turn_id || event.turn_id || result.latestTurnId, event.timestamp);
|
|
368
|
-
addUnique(result.tools, 'web_search');
|
|
369
|
-
addUnique(turn.tools, 'web_search');
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
for (const prompt of eventUserPrompts) addUnique(result.userPrompts, prompt);
|
|
374
|
-
const latestTurn = result.turns.find((turn) => turn.turnId === result.latestTurnId)
|
|
375
|
-
|| result.turns.at(-1);
|
|
376
|
-
result.latestUserPrompt = latestTurn?.userPrompts.at(-1)
|
|
377
|
-
|| eventUserPrompts.at(-1)
|
|
378
|
-
|| result.userPrompts.at(-1)
|
|
379
|
-
|| '';
|
|
380
|
-
result.latestAssistantMessage = latestTurn?.assistantMessages.at(-1)
|
|
381
|
-
|| result.assistantMessages.at(-1)
|
|
382
|
-
|| '';
|
|
383
|
-
result.rawTextForDetection = redactSecrets([
|
|
384
|
-
...result.userPrompts,
|
|
385
|
-
...result.assistantMessages,
|
|
386
|
-
].join('\n\n'));
|
|
387
|
-
|
|
388
|
-
return result;
|
|
179
|
+
return parseCodexTranscriptContent(transcriptContent(transcriptPath), transcriptOptions());
|
|
389
180
|
}
|
|
390
181
|
|
|
391
|
-
// Texto humano de uma mensagem de usuário do Claude Code: mantém só blocos
|
|
392
|
-
// `text`, descartando tool_result e contexto injetado (system-reminder etc.).
|
|
393
|
-
function claudeUserText(content) {
|
|
394
|
-
if (typeof content === 'string') return content.trim();
|
|
395
|
-
if (!Array.isArray(content)) return '';
|
|
396
|
-
return content
|
|
397
|
-
.map((block) => (typeof block === 'string' ? block : (block?.type === 'text' ? block.text || '' : '')))
|
|
398
|
-
.map((text) => String(text || '').trim())
|
|
399
|
-
.filter((text) => text && !text.startsWith('<'))
|
|
400
|
-
.join('\n')
|
|
401
|
-
.trim();
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// Parser do transcript do Claude Code. Schema por linha:
|
|
405
|
-
// { type:'user'|'assistant', message:{ role, content:[{type:'text'|'thinking'|'tool_use'|'tool_result',...}] } }.
|
|
406
|
-
// Diferente do Codex (sem `payload`), por isso precisa de parser próprio.
|
|
407
182
|
export function parseClaudeTranscript(transcriptPath) {
|
|
408
|
-
|
|
409
|
-
provider: 'claude',
|
|
410
|
-
sessionId: '',
|
|
411
|
-
model: '',
|
|
412
|
-
latestTurnId: '',
|
|
413
|
-
latestUserPrompt: '',
|
|
414
|
-
latestAssistantMessage: '',
|
|
415
|
-
userPrompts: [],
|
|
416
|
-
assistantMessages: [],
|
|
417
|
-
tools: [],
|
|
418
|
-
consultedFiles: [],
|
|
419
|
-
changedFiles: [],
|
|
420
|
-
turns: [],
|
|
421
|
-
rawTextForDetection: '',
|
|
422
|
-
};
|
|
423
|
-
|
|
424
|
-
if (!transcriptPath || !existsSync(transcriptPath)) return result;
|
|
425
|
-
|
|
426
|
-
let currentTurn = null;
|
|
427
|
-
const ensureTurn = (turnId = '', timestamp = '') => {
|
|
428
|
-
const normalized = turnId || currentTurn?.turnId || `turn-${result.turns.length + 1}`;
|
|
429
|
-
const existing = result.turns.find((turn) => turn.turnId === normalized);
|
|
430
|
-
if (existing) {
|
|
431
|
-
currentTurn = existing;
|
|
432
|
-
return existing;
|
|
433
|
-
}
|
|
434
|
-
currentTurn = createTurn(normalized, timestamp);
|
|
435
|
-
result.turns.push(currentTurn);
|
|
436
|
-
return currentTurn;
|
|
437
|
-
};
|
|
438
|
-
|
|
439
|
-
const recordToolFiles = (turn, name, input) => {
|
|
440
|
-
const text = toolArgumentText(input);
|
|
441
|
-
for (const path of extractPaths(text)) {
|
|
442
|
-
addUnique(result.consultedFiles, path);
|
|
443
|
-
addUnique(turn.consultedFiles, path);
|
|
444
|
-
}
|
|
445
|
-
for (const path of extractPatchFiles(text)) {
|
|
446
|
-
addUnique(result.changedFiles, path);
|
|
447
|
-
addUnique(turn.changedFiles, path);
|
|
448
|
-
}
|
|
449
|
-
if (/edit|write|create|apply_patch|notebook/i.test(name)) {
|
|
450
|
-
for (const path of extractPaths(text)) {
|
|
451
|
-
addUnique(result.changedFiles, path);
|
|
452
|
-
addUnique(turn.changedFiles, path);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
};
|
|
456
|
-
|
|
457
|
-
const lines = readFileSync(transcriptPath, 'utf-8').split('\n').filter(Boolean);
|
|
458
|
-
for (const line of lines) {
|
|
459
|
-
let event;
|
|
460
|
-
try { event = JSON.parse(line); } catch { continue; }
|
|
461
|
-
if (event.isSidechain || event.isMeta) continue;
|
|
462
|
-
if (event.sessionId && !result.sessionId) result.sessionId = event.sessionId;
|
|
463
|
-
|
|
464
|
-
if (event.type === 'user') {
|
|
465
|
-
const text = claudeUserText(event.message?.content);
|
|
466
|
-
if (!text || shouldIgnoreUserText(text)) continue;
|
|
467
|
-
const turn = ensureTurn(event.uuid || event.promptId || event.timestamp || '', event.timestamp || '');
|
|
468
|
-
result.latestTurnId = turn.turnId;
|
|
469
|
-
addUnique(result.userPrompts, text);
|
|
470
|
-
addUnique(turn.userPrompts, text);
|
|
471
|
-
addConversation(turn, 'Usuário', text);
|
|
472
|
-
continue;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
if (event.type === 'assistant') {
|
|
476
|
-
const turn = currentTurn || ensureTurn(event.uuid || event.timestamp || '', event.timestamp || '');
|
|
477
|
-
result.model = event.message?.model || result.model;
|
|
478
|
-
if (event.message?.model) turn.model = event.message.model;
|
|
479
|
-
if (event.message?.usage) addUsage(turn.usage, normalizeClaudeUsage(event.message.usage));
|
|
480
|
-
const content = Array.isArray(event.message?.content) ? event.message.content : [];
|
|
481
|
-
for (const block of content) {
|
|
482
|
-
if (!block) continue;
|
|
483
|
-
if (block.type === 'text' && block.text && block.text.trim()) {
|
|
484
|
-
addUnique(result.assistantMessages, block.text);
|
|
485
|
-
addUnique(turn.assistantMessages, block.text);
|
|
486
|
-
addConversation(turn, 'Assistente', block.text);
|
|
487
|
-
} else if (block.type === 'tool_use') {
|
|
488
|
-
const name = block.name || 'tool_use';
|
|
489
|
-
addUnique(result.tools, name);
|
|
490
|
-
addUnique(turn.tools, name);
|
|
491
|
-
recordToolFiles(turn, name, block.input);
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
continue;
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
const latestTurn = result.turns.find((turn) => turn.turnId === result.latestTurnId)
|
|
499
|
-
|| result.turns.at(-1);
|
|
500
|
-
result.latestUserPrompt = latestTurn?.userPrompts.at(-1) || result.userPrompts.at(-1) || '';
|
|
501
|
-
result.latestAssistantMessage = latestTurn?.assistantMessages.at(-1) || result.assistantMessages.at(-1) || '';
|
|
502
|
-
result.rawTextForDetection = redactSecrets([
|
|
503
|
-
...result.userPrompts,
|
|
504
|
-
...result.assistantMessages,
|
|
505
|
-
].join('\n\n'));
|
|
506
|
-
|
|
507
|
-
return result;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
function looksLikeCodexEvent(event) {
|
|
511
|
-
return event.payload !== undefined
|
|
512
|
-
|| event.type === 'session_meta'
|
|
513
|
-
|| event.type === 'response_item'
|
|
514
|
-
|| event.type === 'turn_context'
|
|
515
|
-
|| event.type === 'event_msg';
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
function looksLikeClaudeEvent(event) {
|
|
519
|
-
return (event.type === 'user' || event.type === 'assistant') && event.message !== undefined;
|
|
183
|
+
return parseClaudeTranscriptContent(transcriptContent(transcriptPath), transcriptOptions());
|
|
520
184
|
}
|
|
521
185
|
|
|
522
|
-
// Despacha para o parser certo conforme o schema do transcript (Codex x Claude),
|
|
523
|
-
// já que o mesmo hook Stop atende os dois agentes.
|
|
524
186
|
export function parseTranscript(transcriptPath) {
|
|
525
|
-
|
|
526
|
-
const lines = readFileSync(transcriptPath, 'utf-8').split('\n').filter(Boolean);
|
|
527
|
-
for (const line of lines) {
|
|
528
|
-
let event;
|
|
529
|
-
try { event = JSON.parse(line); } catch { continue; }
|
|
530
|
-
if (looksLikeCodexEvent(event)) return parseCodexTranscript(transcriptPath);
|
|
531
|
-
if (looksLikeClaudeEvent(event)) return parseClaudeTranscript(transcriptPath);
|
|
532
|
-
}
|
|
533
|
-
return parseCodexTranscript(transcriptPath);
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
export function resolveTurnIdentity(transcript, requestedTurnId = '') {
|
|
537
|
-
const turns = Array.isArray(transcript?.turns) ? transcript.turns : [];
|
|
538
|
-
const requested = String(requestedTurnId || '');
|
|
539
|
-
let index = requested
|
|
540
|
-
? turns.findIndex((turn) => String(turn?.turnId || '') === requested)
|
|
541
|
-
: -1;
|
|
542
|
-
if (requested && index < 0) return null;
|
|
543
|
-
if (index < 0 && transcript?.latestTurnId) {
|
|
544
|
-
index = turns.findIndex((turn) => String(turn?.turnId || '') === String(transcript.latestTurnId));
|
|
545
|
-
}
|
|
546
|
-
if (index < 0) index = turns.length - 1;
|
|
547
|
-
const turn = turns[index];
|
|
548
|
-
if (!turn?.turnId) return null;
|
|
549
|
-
return {
|
|
550
|
-
id: String(turn.turnId),
|
|
551
|
-
order: index + 1,
|
|
552
|
-
observedAt: String(turn.timestamp || ''),
|
|
553
|
-
};
|
|
187
|
+
return parseTranscriptContent(transcriptContent(transcriptPath), transcriptOptions());
|
|
554
188
|
}
|
|
555
189
|
|
|
556
190
|
function escapeMarkdownBackticks(text) {
|
package/hooks/token-usage.mjs
CHANGED
|
@@ -8,6 +8,18 @@ import {
|
|
|
8
8
|
readControl,
|
|
9
9
|
truncate,
|
|
10
10
|
} from './obsidian-common.mjs';
|
|
11
|
+
import {
|
|
12
|
+
addUsage,
|
|
13
|
+
emptyTokenUsage,
|
|
14
|
+
normalizeClaudeUsage,
|
|
15
|
+
normalizeCodexUsage,
|
|
16
|
+
} from '../packages/integrations/src/transcript-usage.mjs';
|
|
17
|
+
export {
|
|
18
|
+
addUsage,
|
|
19
|
+
emptyTokenUsage,
|
|
20
|
+
normalizeClaudeUsage,
|
|
21
|
+
normalizeCodexUsage,
|
|
22
|
+
};
|
|
11
23
|
|
|
12
24
|
// Preços API por milhão de tokens. cachedInput = cache read.
|
|
13
25
|
// Cache write: 5m = 1.25x input, 1h = 2x input (multiplicadores em calculateCost).
|
|
@@ -180,62 +192,6 @@ const MANAGED_FRONTMATTER_KEYS = new Set([
|
|
|
180
192
|
// input = tokens de entrada NÃO cacheados; cached = cache read; cacheWrite = cache write
|
|
181
193
|
// (cacheWrite1h = subparcela 1h, para custo 2x); thinking = tokens de raciocínio
|
|
182
194
|
// (Claude: estimado de chars/3.5; Codex: reasoning_output_tokens, já contidos em output).
|
|
183
|
-
export function emptyTokenUsage() {
|
|
184
|
-
return {
|
|
185
|
-
input: 0,
|
|
186
|
-
cached: 0,
|
|
187
|
-
cacheWrite: 0,
|
|
188
|
-
cacheWrite1h: 0,
|
|
189
|
-
output: 0,
|
|
190
|
-
reasoning: 0,
|
|
191
|
-
total: 0,
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Formato Codex: cached_input_tokens é SUBCONJUNTO de input_tokens — separa aqui.
|
|
196
|
-
export function normalizeCodexUsage(raw = {}) {
|
|
197
|
-
const inputAll = Number(raw.input_tokens || 0);
|
|
198
|
-
const cached = Math.min(Number(raw.cached_input_tokens || 0), inputAll);
|
|
199
|
-
const output = Number(raw.output_tokens || 0);
|
|
200
|
-
return {
|
|
201
|
-
input: inputAll - cached,
|
|
202
|
-
cached,
|
|
203
|
-
cacheWrite: 0,
|
|
204
|
-
cacheWrite1h: 0,
|
|
205
|
-
output,
|
|
206
|
-
reasoning: Number(raw.reasoning_output_tokens || 0),
|
|
207
|
-
total: Number(raw.total_tokens || 0) || inputAll + output,
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Formato Claude Code: campos já disjuntos.
|
|
212
|
-
export function normalizeClaudeUsage(raw = {}) {
|
|
213
|
-
const input = Number(raw.input_tokens || 0);
|
|
214
|
-
const cached = Number(raw.cache_read_input_tokens || 0);
|
|
215
|
-
const cacheWrite = Number(raw.cache_creation_input_tokens || 0);
|
|
216
|
-
const cacheWrite1h = Number(raw.cache_creation?.ephemeral_1h_input_tokens || 0);
|
|
217
|
-
const output = Number(raw.output_tokens || 0);
|
|
218
|
-
return {
|
|
219
|
-
input,
|
|
220
|
-
cached,
|
|
221
|
-
cacheWrite,
|
|
222
|
-
cacheWrite1h: Math.min(cacheWrite1h, cacheWrite),
|
|
223
|
-
output,
|
|
224
|
-
reasoning: 0,
|
|
225
|
-
total: input + cached + cacheWrite + output,
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
export function addUsage(target, usage) {
|
|
230
|
-
target.input += usage.input;
|
|
231
|
-
target.cached += usage.cached;
|
|
232
|
-
target.cacheWrite += usage.cacheWrite;
|
|
233
|
-
target.cacheWrite1h += usage.cacheWrite1h;
|
|
234
|
-
target.output += usage.output;
|
|
235
|
-
target.reasoning += usage.reasoning;
|
|
236
|
-
target.total += usage.total;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
195
|
function normalizeModelName(model) {
|
|
240
196
|
const clean = String(model || 'unknown').trim() || 'unknown';
|
|
241
197
|
// Strip a trailing context-window tag (e.g. `claude-opus-4-8[1m]`, `claude-fable-5[1m]`) so the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.66.0",
|
|
4
4
|
"description": "Vault-first persistent memory for AI coding agents, with an optional profile-aware governance runtime: OFF, FLOW, GUIDE, GOVERN, or ASSURE. Local-first and agent-agnostic (Claude Code, Codex, Cursor…).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
@@ -9,7 +9,16 @@
|
|
|
9
9
|
"exports": {
|
|
10
10
|
"./harness": "./packages/harness/src/index.mjs",
|
|
11
11
|
"./vault": "./packages/vault/src/index.mjs",
|
|
12
|
-
"
|
|
12
|
+
"./hooks/*": "./hooks/*",
|
|
13
|
+
"./src/*": "./src/*",
|
|
14
|
+
"./bin/*": "./bin/*",
|
|
15
|
+
"./schema/*": "./schema/*",
|
|
16
|
+
"./docs/*": "./docs/*",
|
|
17
|
+
"./package.json": "./package.json",
|
|
18
|
+
"./README.md": "./README.md",
|
|
19
|
+
"./README.en.md": "./README.en.md",
|
|
20
|
+
"./CHANGELOG.md": "./CHANGELOG.md",
|
|
21
|
+
"./LICENSE": "./LICENSE"
|
|
13
22
|
},
|
|
14
23
|
"bin": {
|
|
15
24
|
"wendkeep": "bin/wendkeep.mjs",
|
|
@@ -31,8 +40,8 @@
|
|
|
31
40
|
"node": ">=18"
|
|
32
41
|
},
|
|
33
42
|
"scripts": {
|
|
34
|
-
"check": "node --check bin/wendkeep.mjs && node --check packages/cli/src/index.mjs && node --check src/init.mjs && node --check src/doctor.mjs && node --check src/project-vault.mjs && node --check src/operating-profile.mjs && node --check src/profile.mjs && node --check src/flow.mjs && node --check hooks/operating-profile-runtime.mjs && node --check hooks/flow-core.mjs && node --check hooks/flow-protected-policy.mjs && node --check hooks/git-snapshot.mjs && node --check hooks/vault-path-safety.mjs && node --check hooks/vault-runtime-store.mjs && node --check packages/harness/src/index.mjs && node --check packages/harness/src/flow-store.mjs && node --check packages/harness/src/operating-profile.mjs && node --check packages/harness/src/sensors-core.mjs && node --check packages/vault/src/index.mjs && node --check packages/vault/src/project-vault.mjs && node --check packages/vault/src/vault-path-safety.mjs && node --check packages/vault/src/locale.mjs && node --check packages/vault/src/memory-schema.mjs && node --check packages/vault/src/memory-mode.mjs && node --check packages/vault/src/memory-handoff.mjs && node --check packages/vault/src/memory-store.mjs && node --check packages/vault/src/validate-core.mjs && node --check packages/vault/src/validate-memory.mjs",
|
|
35
|
-
"test": "node --test",
|
|
43
|
+
"check": "node --check bin/wendkeep.mjs && node --check packages/cli/src/index.mjs && node --check src/init.mjs && node --check src/doctor.mjs && node --check src/project-vault.mjs && node --check src/operating-profile.mjs && node --check src/profile.mjs && node --check src/flow.mjs && node --check hooks/operating-profile-runtime.mjs && node --check hooks/flow-core.mjs && node --check hooks/flow-protected-policy.mjs && node --check hooks/git-snapshot.mjs && node --check hooks/vault-path-safety.mjs && node --check hooks/vault-runtime-store.mjs && node --check packages/harness/src/index.mjs && node --check packages/harness/src/flow-store.mjs && node --check packages/harness/src/operating-profile.mjs && node --check packages/harness/src/sensors-core.mjs && node --check packages/integrations/src/host-hooks.mjs && node --check packages/integrations/src/hook-envelope.mjs && node --check packages/integrations/src/prompt-content.mjs && node --check packages/integrations/src/transcript-usage.mjs && node --check packages/integrations/src/transcripts.mjs && node --check packages/integrations/src/session-identity.mjs && node --check packages/integrations/src/index.mjs && node --check packages/mcp/src/config.mjs && node --check packages/mcp/src/index.mjs && node --check packages/vault/src/index.mjs && node --check packages/vault/src/project-vault.mjs && node --check packages/vault/src/vault-path-safety.mjs && node --check packages/vault/src/locale.mjs && node --check packages/vault/src/memory-schema.mjs && node --check packages/vault/src/memory-mode.mjs && node --check packages/vault/src/memory-handoff.mjs && node --check packages/vault/src/memory-store.mjs && node --check packages/vault/src/validate-core.mjs && node --check packages/vault/src/validate-memory.mjs",
|
|
44
|
+
"test": "node --test --test-concurrency=2",
|
|
36
45
|
"release": "node scripts/release.mjs",
|
|
37
46
|
"release:dry": "node scripts/release.mjs --dry-run",
|
|
38
47
|
"prepack": "node scripts/readme-pack.mjs pre",
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Pure host-envelope rules. This module deliberately has no ambient process or I/O access:
|
|
2
|
+
// legacy adapters inject stdin, stdout, and environment at their boundary.
|
|
3
|
+
|
|
4
|
+
// Codex on Windows can serialize a Stop payload with the final
|
|
5
|
+
// `last_assistant_message` cut mid-string. Everything consumed by WendKeep precedes that
|
|
6
|
+
// field, so retain the last complete top-level object prefix without inventing truncated
|
|
7
|
+
// content. A single pass avoids repeatedly parsing payloads that may be tens of KB.
|
|
8
|
+
export function salvageTruncatedJson(raw) {
|
|
9
|
+
const text = String(raw ?? '');
|
|
10
|
+
if (text[0] !== '{') return null;
|
|
11
|
+
|
|
12
|
+
let inString = false;
|
|
13
|
+
let escaped = false;
|
|
14
|
+
let depth = 0;
|
|
15
|
+
let lastBoundary = -1;
|
|
16
|
+
|
|
17
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
18
|
+
const character = text[index];
|
|
19
|
+
if (escaped) {
|
|
20
|
+
escaped = false;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (character === '\\') {
|
|
24
|
+
if (inString) escaped = true;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (character === '"') {
|
|
28
|
+
inString = !inString;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (inString) continue;
|
|
32
|
+
|
|
33
|
+
if (character === '{' || character === '[') depth += 1;
|
|
34
|
+
else if (character === '}' || character === ']') depth -= 1;
|
|
35
|
+
else if (character === ',' && depth === 1) lastBoundary = index;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (lastBoundary === -1) return null;
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(`${text.slice(0, lastBoundary)}}`);
|
|
41
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function parseHookInput(raw) {
|
|
48
|
+
const text = String(raw ?? '').trim();
|
|
49
|
+
if (!text) return {};
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(text);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
const salvaged = salvageTruncatedJson(text);
|
|
55
|
+
if (salvaged) return { ...salvaged, _wkSalvaged: true };
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function stringifyHookOutput(payload = {}) {
|
|
61
|
+
return JSON.stringify(payload);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function detectProvider(environment = {}) {
|
|
65
|
+
if (environment.CLAUDECODE === '1'
|
|
66
|
+
|| environment.CLAUDE_CODE_SESSION_ID
|
|
67
|
+
|| environment.CLAUDE_PROJECT_DIR) {
|
|
68
|
+
return 'claude';
|
|
69
|
+
}
|
|
70
|
+
return 'codex';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function providerMeta(provider) {
|
|
74
|
+
if (provider === 'claude') {
|
|
75
|
+
return { id: 'claude', label: 'Claude Code', tag: 'claude', source: 'claude-hook' };
|
|
76
|
+
}
|
|
77
|
+
return { id: 'codex', label: 'Codex', tag: 'codex', source: 'codex-hook' };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function extractHookPrompt(input = {}) {
|
|
81
|
+
const candidates = [
|
|
82
|
+
input.prompt,
|
|
83
|
+
input.user_prompt,
|
|
84
|
+
input.userPrompt,
|
|
85
|
+
input.message,
|
|
86
|
+
input.input,
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
for (const candidate of candidates) {
|
|
90
|
+
if (typeof candidate === 'string' && candidate.trim()) return candidate.trim();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (Array.isArray(input.messages)) {
|
|
94
|
+
const text = input.messages
|
|
95
|
+
.map((message) => message?.content || message?.text || '')
|
|
96
|
+
.filter((item) => typeof item === 'string' && item.trim())
|
|
97
|
+
.join('\n')
|
|
98
|
+
.trim();
|
|
99
|
+
if (text) return text;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return '';
|
|
103
|
+
}
|