agentel 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,553 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { spawnSync } = require("child_process");
6
+
7
+ const DEFAULT_COMMIT_WINDOW_MS = 15 * 60 * 1000;
8
+
9
+ function parseAiderHistoryFile(file, options = {}) {
10
+ const stat = safeStat(file);
11
+ const fallbackTime = toIso(options.fallbackTime || stat?.mtimeMs || Date.now());
12
+ const text = fs.readFileSync(file, "utf8");
13
+ const cwd = options.cwd || findGitRoot(path.dirname(file)) || path.dirname(file);
14
+ const sourceFiles = aiderSourceFiles(file, options);
15
+ const llmHistoryFile = sourceFiles.find((candidate) => path.basename(candidate) === ".aider.llm.history");
16
+ const llmEntries = llmHistoryFile ? parseAiderLlmHistoryFile(llmHistoryFile) : [];
17
+ let messages = parseAiderChatHistoryText(text, { fallbackTime });
18
+ messages = applyAiderLlmHistory(messages, llmEntries);
19
+ if (options.includeGitDiffs !== false) {
20
+ messages = correlateAiderMessagesWithGit(messages, cwd, {
21
+ maxTimeDeltaMs: options.maxTimeDeltaMs,
22
+ maxCommits: options.maxCommits
23
+ });
24
+ }
25
+ return { cwd, messages, sourceFiles, llmEntries };
26
+ }
27
+
28
+ function parseAiderChatHistoryText(text, options = {}) {
29
+ const fallbackTime = toIso(options.fallbackTime || Date.now());
30
+ const lines = String(text || "").split(/\r?\n/);
31
+ const messages = [];
32
+ let pendingUser = "";
33
+ let assistant = [];
34
+ const flush = () => {
35
+ if (pendingUser.trim()) {
36
+ messages.push({
37
+ role: "user",
38
+ content: pendingUser.trim(),
39
+ timestamp: offsetTimestamp(fallbackTime, messages.length),
40
+ metadata: { provider: "aider", source: "chat_history" }
41
+ });
42
+ }
43
+ const assistantText = assistant.join("\n").trim();
44
+ if (assistantText) {
45
+ messages.push({
46
+ role: "assistant",
47
+ content: assistantText,
48
+ timestamp: offsetTimestamp(fallbackTime, messages.length),
49
+ metadata: { provider: "aider", source: "chat_history" }
50
+ });
51
+ }
52
+ pendingUser = "";
53
+ assistant = [];
54
+ };
55
+
56
+ for (const line of lines) {
57
+ const heading = line.match(/^####\s+(.+?)\s*$/);
58
+ if (heading) {
59
+ flush();
60
+ pendingUser = heading[1];
61
+ continue;
62
+ }
63
+ if (pendingUser) assistant.push(line);
64
+ }
65
+ flush();
66
+ return dedupeAdjacentMessages(messages);
67
+ }
68
+
69
+ function parseAiderLlmHistoryFile(file) {
70
+ if (!safeStat(file)) return [];
71
+ return parseAiderLlmHistoryText(fs.readFileSync(file, "utf8"));
72
+ }
73
+
74
+ function parseAiderLlmHistoryText(text) {
75
+ const entries = [];
76
+ const lines = String(text || "").split(/\r?\n/);
77
+ for (const line of lines) {
78
+ const trimmed = line.trim();
79
+ if (!trimmed) continue;
80
+ const parsed = tryParseJson(trimmed);
81
+ if (!parsed) continue;
82
+ const extracted = extractAiderLlmEntry(parsed);
83
+ if (extracted) entries.push(extracted);
84
+ }
85
+ if (entries.length) return entries;
86
+ return parseRoleDelimitedLlmHistory(text);
87
+ }
88
+
89
+ function applyAiderLlmHistory(messages, entries) {
90
+ if (!Array.isArray(entries) || !entries.length) return messages;
91
+ const nextMessages = messages.map((message) => ({ ...message, metadata: { ...(message.metadata || {}) } }));
92
+ const assistantIndexes = nextMessages.map((message, index) => (message.role === "assistant" ? index : -1)).filter((index) => index >= 0);
93
+ const used = new Set();
94
+ for (const entry of entries) {
95
+ const index = bestAssistantIndexForLlmEntry(nextMessages, entry, used, assistantIndexes);
96
+ if (index < 0) continue;
97
+ used.add(index);
98
+ const metadata = nextMessages[index].metadata || {};
99
+ nextMessages[index].metadata = {
100
+ ...metadata,
101
+ ...(entry.model && !metadata.model ? { model: entry.model } : {}),
102
+ ...(entry.usage && !metadata.usage ? { usage: entry.usage } : {}),
103
+ llmHistory: compactObject({
104
+ timestamp: entry.timestamp,
105
+ requestId: entry.requestId,
106
+ model: entry.model,
107
+ usage: entry.usage
108
+ })
109
+ };
110
+ }
111
+ return nextMessages;
112
+ }
113
+
114
+ function correlateAiderMessagesWithGit(messages, cwd, options = {}) {
115
+ if (!cwd || !safeStat(path.join(cwd, ".git"))) return messages;
116
+ const commits = Array.isArray(options.commits) ? options.commits : readAiderGitCommits(cwd, options);
117
+ if (!commits.length) return messages;
118
+
119
+ const nextMessages = messages.map((message) => ({
120
+ ...message,
121
+ metadata: { ...(message.metadata || {}) }
122
+ }));
123
+ const turns = aiderTurns(nextMessages);
124
+
125
+ for (const commit of commits) {
126
+ const match = bestTurnForCommit(commit, turns, options);
127
+ if (!match) continue;
128
+ const diff = commit.diff || readCommitDiff(cwd, commit.hash);
129
+ if (!diff.trim()) continue;
130
+ const assistant = nextMessages[match.assistantIndex];
131
+ const existingToolCalls = Array.isArray(assistant.metadata.toolCalls) ? assistant.metadata.toolCalls : [];
132
+ const commitMetadata = {
133
+ hash: commit.hash,
134
+ subject: commit.subject,
135
+ timestamp: commit.timestamp,
136
+ source: "aider"
137
+ };
138
+ const existingCommits = Array.isArray(assistant.metadata.gitCommits) ? assistant.metadata.gitCommits : [];
139
+ assistant.metadata = {
140
+ ...assistant.metadata,
141
+ toolCalls: [
142
+ ...existingToolCalls,
143
+ aiderDiffToolCall(commit, diff, match.prompt)
144
+ ],
145
+ gitCommit: assistant.metadata.gitCommit || commitMetadata,
146
+ gitCommits: [...existingCommits, commitMetadata]
147
+ };
148
+ }
149
+ return nextMessages;
150
+ }
151
+
152
+ function readAiderGitCommits(cwd, options = {}) {
153
+ const args = [
154
+ "-C",
155
+ cwd,
156
+ "log",
157
+ `--max-count=${Number(options.maxCommits || 100)}`,
158
+ "--date=iso-strict",
159
+ "--format=%x1e%H%x00%aI%x00%an%x00%ae%x00%s%x00%b"
160
+ ];
161
+ const result = spawnSync("git", args, { encoding: "utf8", maxBuffer: 10 * 1024 * 1024 });
162
+ if (result.error || result.status !== 0) return [];
163
+ return result.stdout
164
+ .split("\x1e")
165
+ .map((record) => parseGitLogRecord(record))
166
+ .filter(Boolean);
167
+ }
168
+
169
+ function aiderSourceFiles(file, options = {}) {
170
+ const dir = path.dirname(file);
171
+ const candidates = [
172
+ file,
173
+ options.llmHistoryFile,
174
+ path.join(dir, ".aider.llm.history"),
175
+ options.inputHistoryFile,
176
+ path.join(dir, ".aider.input.history")
177
+ ];
178
+ const files = [];
179
+ for (const candidate of candidates) {
180
+ if (candidate && safeStat(candidate) && !files.includes(candidate)) files.push(candidate);
181
+ }
182
+ return files;
183
+ }
184
+
185
+ function extractAiderLlmEntry(value) {
186
+ if (!value || typeof value !== "object") return null;
187
+ const prompt = firstNonEmptyString(
188
+ value.prompt,
189
+ value.input,
190
+ value.user,
191
+ value.userPrompt,
192
+ lastMessageContent(value.messages, "user"),
193
+ lastMessageContent(value.request?.messages, "user"),
194
+ lastMessageContent(value.kwargs?.messages, "user")
195
+ );
196
+ const response = firstNonEmptyString(
197
+ value.response,
198
+ value.output,
199
+ value.assistant,
200
+ value.completion,
201
+ contentFromMessage(value.message),
202
+ contentFromMessage(value.response?.message),
203
+ contentFromChoice(value.choices),
204
+ contentFromChoice(value.response?.choices),
205
+ lastMessageContent(value.messages, "assistant")
206
+ );
207
+ const model = firstNonEmptyString(value.model, value.response?.model, value.request?.model, value.kwargs?.model, value.params?.model);
208
+ const usage = normalizeUsage(value.usage || value.response?.usage || value.token_usage || value.tokenUsage || value.metrics?.usage);
209
+ const timestamp = firstTimestamp(value.timestamp, value.created_at, value.createdAt, value.time, value.ts);
210
+ const requestId = firstNonEmptyString(value.request_id, value.requestId, value.id, value.response?.id);
211
+ if (!prompt && !response && !model && !usage) return null;
212
+ return compactObject({ prompt, response, model, usage, timestamp, requestId, raw: value });
213
+ }
214
+
215
+ function parseRoleDelimitedLlmHistory(text) {
216
+ const entries = [];
217
+ let current = null;
218
+ let role = null;
219
+ for (const rawLine of String(text || "").split(/\r?\n/)) {
220
+ const line = rawLine.trimEnd();
221
+ const marker = line.match(/^(?:#{1,6}\s*)?(USER|ASSISTANT|MODEL|RESPONSE|SYSTEM)\s*:?\s*$/i);
222
+ if (marker) {
223
+ if (!current) current = {};
224
+ role = marker[1].toLowerCase();
225
+ continue;
226
+ }
227
+ const inline = line.match(/^(USER|ASSISTANT|MODEL|RESPONSE):\s+(.+)$/i);
228
+ if (inline) {
229
+ if (!current) current = {};
230
+ role = inline[1].toLowerCase();
231
+ appendRoleText(current, role, inline[2]);
232
+ continue;
233
+ }
234
+ if (line.match(/^-{3,}$/) && current) {
235
+ const entry = extractAiderLlmEntry(current);
236
+ if (entry) entries.push(entry);
237
+ current = null;
238
+ role = null;
239
+ continue;
240
+ }
241
+ if (current && role) appendRoleText(current, role, line);
242
+ }
243
+ if (current) {
244
+ const entry = extractAiderLlmEntry(current);
245
+ if (entry) entries.push(entry);
246
+ }
247
+ return entries;
248
+ }
249
+
250
+ function appendRoleText(target, role, line) {
251
+ const key = role === "assistant" || role === "model" || role === "response" ? "response" : "prompt";
252
+ target[key] = [target[key], line].filter(Boolean).join("\n").trim();
253
+ }
254
+
255
+ function bestAssistantIndexForLlmEntry(messages, entry, used, assistantIndexes) {
256
+ let best = { index: -1, score: 0 };
257
+ for (const index of assistantIndexes) {
258
+ if (used.has(index)) continue;
259
+ const previousUser = previousUserMessage(messages, index);
260
+ let score = 0;
261
+ if (entry.prompt && previousUser) score += textSimilarity(entry.prompt, previousUser.content) * 6;
262
+ if (entry.response && messages[index].content) score += textSimilarity(entry.response, messages[index].content) * 3;
263
+ if (entry.timestamp && messages[index].timestamp) {
264
+ const delta = Math.abs(Date.parse(entry.timestamp) - Date.parse(messages[index].timestamp));
265
+ if (Number.isFinite(delta) && delta <= DEFAULT_COMMIT_WINDOW_MS) score += 2;
266
+ }
267
+ if (!entry.prompt && !entry.response && !used.size) score += 1;
268
+ if (score > best.score) best = { index, score };
269
+ }
270
+ return best.score >= 1.5 ? best.index : -1;
271
+ }
272
+
273
+ function aiderTurns(messages) {
274
+ const turns = [];
275
+ for (let index = 0; index < messages.length; index++) {
276
+ if (messages[index].role !== "assistant") continue;
277
+ const userIndex = previousUserIndex(messages, index);
278
+ if (userIndex < 0) continue;
279
+ turns.push({
280
+ index: turns.length,
281
+ userIndex,
282
+ assistantIndex: index,
283
+ prompt: messages[userIndex].content || "",
284
+ timestamp: messages[index].timestamp || messages[userIndex].timestamp
285
+ });
286
+ }
287
+ return turns;
288
+ }
289
+
290
+ function bestTurnForCommit(commit, turns, options = {}) {
291
+ let best = null;
292
+ for (const turn of turns) {
293
+ const score = aiderCommitMatchScore(commit, turn, options);
294
+ if (!best || score > best.score) best = { ...turn, score };
295
+ }
296
+ return best && best.score >= 5 ? best : null;
297
+ }
298
+
299
+ function aiderCommitMatchScore(commit, turn, options = {}) {
300
+ const maxTimeDeltaMs = Number(options.maxTimeDeltaMs || DEFAULT_COMMIT_WINDOW_MS);
301
+ const message = [commit.subject, commit.body].filter(Boolean).join("\n");
302
+ let score = 0;
303
+ if (isLikelyAiderCommit(commit)) score += 3;
304
+ const similarity = textSimilarity(turn.prompt, message);
305
+ if (similarity >= 0.8) score += 4;
306
+ else if (similarity >= 0.35) score += 2;
307
+ if (commit.timestamp && turn.timestamp) {
308
+ const delta = Math.abs(Date.parse(commit.timestamp) - Date.parse(turn.timestamp));
309
+ if (Number.isFinite(delta) && delta <= maxTimeDeltaMs) score += 2;
310
+ }
311
+ return score;
312
+ }
313
+
314
+ function isLikelyAiderCommit(commit) {
315
+ const text = [commit.subject, commit.body, commit.authorName, commit.authorEmail].filter(Boolean).join("\n");
316
+ return /\baider\b/i.test(text) || /^\s*(AI|LLM)\s*:/i.test(commit.subject || "");
317
+ }
318
+
319
+ function aiderDiffToolCall(commit, diff, prompt) {
320
+ const shortHash = commit.hash ? commit.hash.slice(0, 12) : "";
321
+ const title = shortHash ? `Aider commit ${shortHash}` : "Aider commit diff";
322
+ const summary = firstNonEmptyString(commit.subject, prompt, "Aider code changes");
323
+ return {
324
+ name: "aider_git_diff",
325
+ displayName: "Aider Git Diff",
326
+ category: "edit",
327
+ title,
328
+ status: "completed",
329
+ argument: summary,
330
+ rawInputSummary: summary,
331
+ inputPreview: summary,
332
+ arguments: compactObject({
333
+ diff,
334
+ commit: commit.hash,
335
+ subject: commit.subject,
336
+ timestamp: commit.timestamp
337
+ }),
338
+ provider: "aider"
339
+ };
340
+ }
341
+
342
+ function parseGitLogRecord(record) {
343
+ const trimmed = record.replace(/^\n+|\n+$/g, "");
344
+ if (!trimmed) return null;
345
+ const parts = trimmed.split("\x00");
346
+ if (parts.length < 5) return null;
347
+ const [hash, timestamp, authorName, authorEmail, subject, ...bodyParts] = parts;
348
+ return {
349
+ hash,
350
+ timestamp,
351
+ authorName,
352
+ authorEmail,
353
+ subject: subject || "",
354
+ body: bodyParts.join("\x00").trim()
355
+ };
356
+ }
357
+
358
+ function readCommitDiff(cwd, hash) {
359
+ if (!hash) return "";
360
+ const result = spawnSync(
361
+ "git",
362
+ ["-C", cwd, "show", "--format=", "--no-ext-diff", "--patch", "--find-renames", "--find-copies", "--unified=3", hash],
363
+ { encoding: "utf8", maxBuffer: 20 * 1024 * 1024 }
364
+ );
365
+ if (result.error || result.status !== 0) return "";
366
+ return result.stdout.trim();
367
+ }
368
+
369
+ function previousUserMessage(messages, index) {
370
+ const userIndex = previousUserIndex(messages, index);
371
+ return userIndex >= 0 ? messages[userIndex] : null;
372
+ }
373
+
374
+ function previousUserIndex(messages, index) {
375
+ for (let cursor = index - 1; cursor >= 0; cursor--) {
376
+ if (messages[cursor].role === "user") return cursor;
377
+ }
378
+ return -1;
379
+ }
380
+
381
+ function contentFromChoice(choices) {
382
+ if (!Array.isArray(choices)) return "";
383
+ for (const choice of choices) {
384
+ const content = contentFromMessage(choice?.message) || contentFromMessage(choice?.delta) || firstNonEmptyString(choice?.text, choice?.content);
385
+ if (content) return content;
386
+ }
387
+ return "";
388
+ }
389
+
390
+ function lastMessageContent(messages, role) {
391
+ if (!Array.isArray(messages)) return "";
392
+ for (let index = messages.length - 1; index >= 0; index--) {
393
+ const message = messages[index];
394
+ if (!role || message?.role === role) {
395
+ const content = contentFromMessage(message);
396
+ if (content) return content;
397
+ }
398
+ }
399
+ return "";
400
+ }
401
+
402
+ function contentFromMessage(message) {
403
+ if (!message) return "";
404
+ if (typeof message === "string") return message.trim();
405
+ if (Array.isArray(message)) return message.map(contentFromMessage).filter(Boolean).join("\n").trim();
406
+ if (typeof message !== "object") return String(message).trim();
407
+ if (typeof message.content === "string") return message.content.trim();
408
+ if (Array.isArray(message.content)) return message.content.map(contentFromPart).filter(Boolean).join("\n").trim();
409
+ return firstNonEmptyString(message.text, message.message, message.output);
410
+ }
411
+
412
+ function contentFromPart(part) {
413
+ if (!part) return "";
414
+ if (typeof part === "string") return part.trim();
415
+ if (typeof part !== "object") return String(part).trim();
416
+ return firstNonEmptyString(part.text, part.content, part.input?.text);
417
+ }
418
+
419
+ function normalizeUsage(usage) {
420
+ if (!usage || typeof usage !== "object") return null;
421
+ return compactObject({
422
+ inputTokens: firstNumber(usage.input_tokens, usage.prompt_tokens, usage.inputTokens, usage.promptTokens),
423
+ outputTokens: firstNumber(usage.output_tokens, usage.completion_tokens, usage.outputTokens, usage.completionTokens),
424
+ totalTokens: firstNumber(usage.total_tokens, usage.totalTokens),
425
+ cacheCreationInputTokens: firstNumber(usage.cache_creation_input_tokens, usage.cacheCreationInputTokens),
426
+ cacheReadInputTokens: firstNumber(usage.cache_read_input_tokens, usage.cacheReadInputTokens),
427
+ costUsd: firstNumber(usage.cost, usage.cost_usd, usage.costUsd)
428
+ });
429
+ }
430
+
431
+ function textSimilarity(a, b) {
432
+ const left = tokenSet(a);
433
+ const right = tokenSet(b);
434
+ if (!left.size || !right.size) return 0;
435
+ let overlap = 0;
436
+ for (const token of left) {
437
+ if (right.has(token)) overlap++;
438
+ }
439
+ return overlap / Math.min(left.size, right.size);
440
+ }
441
+
442
+ function tokenSet(value) {
443
+ return new Set(
444
+ String(value || "")
445
+ .toLowerCase()
446
+ .replace(/[`"'’]/g, "")
447
+ .split(/[^a-z0-9_.-]+/)
448
+ .filter((token) => token.length >= 2)
449
+ );
450
+ }
451
+
452
+ function dedupeAdjacentMessages(messages) {
453
+ const deduped = [];
454
+ for (const message of messages) {
455
+ const previous = deduped[deduped.length - 1];
456
+ if (previous && previous.role === message.role && previous.content === message.content) continue;
457
+ deduped.push(message);
458
+ }
459
+ return deduped;
460
+ }
461
+
462
+ function findGitRoot(start) {
463
+ let dir = path.resolve(start);
464
+ while (dir && dir !== path.dirname(dir)) {
465
+ if (safeStat(path.join(dir, ".git"))) return dir;
466
+ dir = path.dirname(dir);
467
+ }
468
+ return null;
469
+ }
470
+
471
+ function firstTimestamp(...values) {
472
+ for (const value of values) {
473
+ const iso = toIso(value);
474
+ if (iso) return iso;
475
+ }
476
+ return "";
477
+ }
478
+
479
+ function toIso(value) {
480
+ if (value === undefined || value === null || value === "") return "";
481
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? "" : value.toISOString();
482
+ if (typeof value === "number") {
483
+ const date = new Date(value < 10000000000 ? value * 1000 : value);
484
+ return Number.isNaN(date.getTime()) ? "" : date.toISOString();
485
+ }
486
+ const date = new Date(value);
487
+ return Number.isNaN(date.getTime()) ? "" : date.toISOString();
488
+ }
489
+
490
+ function offsetTimestamp(base, offset) {
491
+ const time = Date.parse(base);
492
+ if (!Number.isFinite(time)) return new Date(Date.now() + offset).toISOString();
493
+ return new Date(time + offset).toISOString();
494
+ }
495
+
496
+ function safeStat(file) {
497
+ if (!file) return null;
498
+ try {
499
+ return fs.statSync(file);
500
+ } catch {
501
+ return null;
502
+ }
503
+ }
504
+
505
+ function tryParseJson(value) {
506
+ try {
507
+ return JSON.parse(value);
508
+ } catch {
509
+ return null;
510
+ }
511
+ }
512
+
513
+ function firstNonEmptyString(...values) {
514
+ for (const value of values) {
515
+ if (typeof value === "string" && value.trim()) return value.trim();
516
+ if (value !== undefined && value !== null && typeof value !== "object") {
517
+ const text = String(value).trim();
518
+ if (text) return text;
519
+ }
520
+ }
521
+ return "";
522
+ }
523
+
524
+ function firstNumber(...values) {
525
+ for (const value of values) {
526
+ const number = Number(value);
527
+ if (Number.isFinite(number)) return number;
528
+ }
529
+ return undefined;
530
+ }
531
+
532
+ function compactObject(value) {
533
+ const out = {};
534
+ for (const [key, item] of Object.entries(value || {})) {
535
+ if (item === undefined || item === null || item === "") continue;
536
+ if (typeof item === "object" && !Array.isArray(item) && !Object.keys(item).length) continue;
537
+ out[key] = item;
538
+ }
539
+ return out;
540
+ }
541
+
542
+ module.exports = {
543
+ aiderCommitMatchScore,
544
+ aiderSourceFiles,
545
+ applyAiderLlmHistory,
546
+ correlateAiderMessagesWithGit,
547
+ isLikelyAiderCommit,
548
+ parseAiderChatHistoryText,
549
+ parseAiderHistoryFile,
550
+ parseAiderLlmHistoryFile,
551
+ parseAiderLlmHistoryText,
552
+ readAiderGitCommits
553
+ };