wendkeep 0.65.0 → 0.66.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+ }
@@ -108,6 +108,31 @@ export function validateMemoryEvent(event, { projectId } = {}) {
108
108
  errors.push(`project_id não pertence ao vault esperado (${projectId}).`);
109
109
  }
110
110
 
111
+ if (event.candidate_decision !== undefined) {
112
+ const decision = event.candidate_decision;
113
+ if (!decision || typeof decision !== 'object' || Array.isArray(decision)) {
114
+ errors.push('candidate_decision deve ser objeto.');
115
+ } else {
116
+ if (typeof decision.candidate_id !== 'string' || !decision.candidate_id) {
117
+ errors.push('candidate_decision.candidate_id deve ser string não vazia.');
118
+ }
119
+ if (!['promote', 'reject'].includes(decision.action)) {
120
+ errors.push('candidate_decision.action deve ser promote ou reject.');
121
+ }
122
+ if (!Array.isArray(decision.event_ids)
123
+ || decision.event_ids.some((item) => typeof item !== 'string' || !item)) {
124
+ errors.push('candidate_decision.event_ids deve ser array de strings não vazias.');
125
+ }
126
+ if (decision.action === 'promote' && decision.selected_event_id !== undefined
127
+ && (typeof decision.selected_event_id !== 'string' || !decision.selected_event_id)) {
128
+ errors.push('candidate_decision.selected_event_id deve ser string não vazia.');
129
+ }
130
+ if (decision.action === 'reject' && decision.selected_event_id !== undefined) {
131
+ errors.push('candidate_decision.selected_event_id não é permitido em reject.');
132
+ }
133
+ }
134
+ }
135
+
111
136
  for (const field of ['value', 'evidence']) sanitizedField(event, field, errors);
112
137
  return { ok: errors.length === 0, errors, warnings };
113
138
  }
@@ -461,6 +461,19 @@ export function reduceMemoryEvents(inputEvents = [], { coreInvariants = new Map(
461
461
  if (!existing) unique.set(event.event_id, event);
462
462
  }
463
463
  const events = [...unique.values()].sort(eventOrder);
464
+ const candidateDecisions = new Map();
465
+ for (const item of events) {
466
+ const decision = item.candidate_decision;
467
+ if (!decision) continue;
468
+ const existing = candidateDecisions.get(decision.candidate_id);
469
+ if (existing && canonicalMemoryJson(existing.decision) !== canonicalMemoryJson(decision)) {
470
+ throw new MemoryEventCollision(
471
+ decision.candidate_id,
472
+ `Ledger contains incompatible decisions for candidate ${decision.candidate_id}`,
473
+ );
474
+ }
475
+ if (!existing) candidateDecisions.set(decision.candidate_id, { decision, event: item });
476
+ }
464
477
 
465
478
  const peerGroups = new Map();
466
479
  for (const item of events) {
@@ -488,6 +501,11 @@ export function reduceMemoryEvents(inputEvents = [], { coreInvariants = new Map(
488
501
  let revision = 0;
489
502
 
490
503
  for (const item of events) {
504
+ if (item.candidate_decision && item.candidate_decision.action === 'reject') {
505
+ appliedEventIds.push(item.event_id);
506
+ continue;
507
+ }
508
+
491
509
  if (protectedValues.has(item.memory_key)) {
492
510
  const coreValue = protectedValues.get(item.memory_key);
493
511
  const agreesWithCore = item.operation === 'assert'
@@ -604,7 +622,9 @@ export function reduceMemoryEvents(inputEvents = [], { coreInvariants = new Map(
604
622
  const state = sortedObject(stateEntries);
605
623
  const recordObject = sortedObject(recordEntries);
606
624
  const tombstoneObject = sortedObject(tombstoneEntries);
607
- candidates.sort((left, right) => left.candidate_id.localeCompare(right.candidate_id));
625
+ const unresolvedCandidates = candidates
626
+ .filter((item) => !candidateDecisions.has(item.candidate_id));
627
+ unresolvedCandidates.sort((left, right) => left.candidate_id.localeCompare(right.candidate_id));
608
628
  superseded.sort((left, right) => left.event_id.localeCompare(right.event_id));
609
629
  const activeEvents = Object.entries(recordObject).map(([memoryKey, record]) => ({
610
630
  ...record.source,
@@ -618,7 +638,7 @@ export function reduceMemoryEvents(inputEvents = [], { coreInvariants = new Map(
618
638
  return {
619
639
  state,
620
640
  records: recordObject,
621
- candidates,
641
+ candidates: unresolvedCandidates,
622
642
  tombstones: tombstoneObject,
623
643
  superseded,
624
644
  appliedEventIds,