lastgen-cli 1.0.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,1169 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/agent-init.ts
4
+ import { join as join3, resolve as resolve2 } from "path";
5
+ import { readFile as readFile3, writeFile as writeFile3, access } from "fs/promises";
6
+ import { existsSync as existsSync3 } from "fs";
7
+
8
+ // src/agent-tools.ts
9
+ import { join, resolve, relative, dirname, basename } from "path";
10
+ import {
11
+ readdir,
12
+ readFile,
13
+ writeFile,
14
+ mkdir,
15
+ stat,
16
+ copyFile
17
+ } from "fs/promises";
18
+ import { existsSync, realpathSync } from "fs";
19
+ import { spawn } from "child_process";
20
+ function isSafePath(projectRoot, targetPath) {
21
+ const root = resolve(projectRoot);
22
+ const resolved = resolve(root, targetPath);
23
+ const rel = relative(root, resolved);
24
+ if (rel.startsWith("..") || rel.startsWith("/")) return false;
25
+ try {
26
+ const realRoot = realpathSync(root);
27
+ let check = resolved;
28
+ while (check !== root && check !== dirname(check)) {
29
+ if (existsSync(check)) {
30
+ const real = realpathSync(check);
31
+ const realRel = relative(realRoot, real);
32
+ if (realRel.startsWith("..") || realRel.startsWith("/")) return false;
33
+ break;
34
+ }
35
+ check = dirname(check);
36
+ }
37
+ } catch {
38
+ return false;
39
+ }
40
+ return true;
41
+ }
42
+ var ALLOWED_COMMAND_PREFIXES = [
43
+ "pytest",
44
+ "python",
45
+ "python3",
46
+ "npm",
47
+ "node",
48
+ "go test",
49
+ "go build",
50
+ "cargo test",
51
+ "cargo build",
52
+ "mvn test",
53
+ "mvn --version",
54
+ "mvn -version",
55
+ "gradle test",
56
+ "./mvnw test",
57
+ "mvnw test",
58
+ "mvnw.cmd test",
59
+ "./gradlew test",
60
+ "gradlew test",
61
+ "gradlew.bat test",
62
+ "java -version",
63
+ "java --version",
64
+ "make"
65
+ ];
66
+ function isAllowedCommand(command, projectRoot) {
67
+ const parts = command.split("&&").map((p) => p.trim()).filter(Boolean);
68
+ let currentDir = projectRoot;
69
+ return parts.every((part) => {
70
+ const lower = part.toLowerCase();
71
+ if (lower.startsWith("cd ") || lower === "cd") {
72
+ if (lower === "cd") return true;
73
+ const target = part.slice(3).trim();
74
+ const resolved = resolve(currentDir, target);
75
+ const rel = relative(projectRoot, resolved);
76
+ if (rel.startsWith("..") || rel.startsWith("/")) return false;
77
+ currentDir = resolved;
78
+ return true;
79
+ }
80
+ const dangerousFlags = /\s(-e|--eval|-c\b)/;
81
+ if (dangerousFlags.test(part)) return false;
82
+ if (lower.startsWith("npx")) return false;
83
+ if (lower.startsWith("./mvnw") || lower.startsWith("mvnw") || lower.startsWith("mvnw.cmd")) {
84
+ return /\b(test|clean|compile|package|verify|install|spring-boot:run)\b/.test(lower) || /-{1,2}version\b/.test(lower);
85
+ }
86
+ if (lower.startsWith("./gradlew") || lower.startsWith("gradlew") || lower.startsWith("gradlew.bat")) {
87
+ return /\b(test|build|clean|assemble|bootRun|compileKotlin|compileJava)\b/.test(lower) || /-{1,2}version\b/.test(lower);
88
+ }
89
+ return ALLOWED_COMMAND_PREFIXES.some((p) => lower.startsWith(p));
90
+ });
91
+ }
92
+ async function listFiles(projectRoot, directory) {
93
+ if (!isSafePath(projectRoot, directory)) {
94
+ return { success: false, output: `G\xFCvenlik hatas\u0131: "${directory}" proje dizini d\u0131\u015F\u0131nda.` };
95
+ }
96
+ const absDir = resolve(projectRoot, directory);
97
+ try {
98
+ const lines = [];
99
+ const IGNORE = /* @__PURE__ */ new Set(["node_modules", ".git", "__pycache__", ".venv", "dist", "build", ".next"]);
100
+ async function walk(dir, prefix, depth) {
101
+ if (depth > 3) return;
102
+ const entries = await readdir(dir, { withFileTypes: true });
103
+ for (const e of entries) {
104
+ if (IGNORE.has(e.name) || e.name.startsWith(".")) continue;
105
+ if (e.isDirectory()) {
106
+ lines.push(`${prefix}${e.name}/`);
107
+ await walk(join(dir, e.name), prefix + " ", depth + 1);
108
+ } else {
109
+ lines.push(`${prefix}${e.name}`);
110
+ }
111
+ }
112
+ }
113
+ await walk(absDir, "", 0);
114
+ return { success: true, output: lines.join("\n") || "(bo\u015F dizin)" };
115
+ } catch (err) {
116
+ return { success: false, output: `Dizin okunamad\u0131: ${String(err)}` };
117
+ }
118
+ }
119
+ var LARGE_FILE_THRESHOLD = 300;
120
+ async function readFile_(projectRoot, filePath, startLine, endLine, full) {
121
+ if (!isSafePath(projectRoot, filePath)) {
122
+ return { success: false, output: `G\xFCvenlik hatas\u0131: "${filePath}" proje dizini d\u0131\u015F\u0131nda.` };
123
+ }
124
+ const abs = resolve(projectRoot, filePath);
125
+ try {
126
+ const s = await stat(abs);
127
+ if (s.size > 5e5) {
128
+ return { success: false, output: `Dosya \xE7ok b\xFCy\xFCk (${s.size} byte). grep_files ile ilgili sat\u0131r\u0131 bulup start_line/end_line parametresiyle okuyun.` };
129
+ }
130
+ const content = await readFile(abs, "utf-8");
131
+ const allLines = content.split("\n");
132
+ const totalLines = allLines.length;
133
+ if (startLine !== void 0) {
134
+ const s0 = Math.max(1, startLine) - 1;
135
+ const e = endLine !== void 0 ? Math.min(totalLines, endLine) : Math.min(totalLines, s0 + 80);
136
+ const header = `[${filePath} \u2014 sat\u0131r ${s0 + 1}-${e} / toplam ${totalLines}]
137
+ `;
138
+ return { success: true, output: header + allLines.slice(s0, e).map((l, i) => `${s0 + i + 1}: ${l}`).join("\n") };
139
+ }
140
+ if (full) {
141
+ return { success: true, output: `[${filePath} \u2014 ${totalLines} sat\u0131r]
142
+ ` + allLines.map((l, i) => `${i + 1}: ${l}`).join("\n") };
143
+ }
144
+ if (totalLines > LARGE_FILE_THRESHOLD) {
145
+ const preview = allLines.slice(0, 80).map((l, i) => `${i + 1}: ${l}`).join("\n");
146
+ return {
147
+ success: true,
148
+ output: `[${filePath} \u2014 ${totalLines} sat\u0131r. \xD6nce grep_files ile ilgili sat\u0131r\u0131 bulun, sonra start_line/end_line ile okuyun.]
149
+
150
+ \u0130lk 80 sat\u0131r:
151
+ ${preview}`
152
+ };
153
+ }
154
+ return { success: true, output: allLines.map((l, i) => `${i + 1}: ${l}`).join("\n") };
155
+ } catch (err) {
156
+ const msg = err.code === "ENOENT" ? `${filePath} mevcut de\u011Fil.` : `Dosya okunamad\u0131: ${err.message}`;
157
+ return { success: false, output: msg };
158
+ }
159
+ }
160
+ async function createFile(projectRoot, filePath, content) {
161
+ if (!isSafePath(projectRoot, filePath)) {
162
+ return { success: false, output: `G\xFCvenlik hatas\u0131: "${filePath}" proje dizini d\u0131\u015F\u0131nda.` };
163
+ }
164
+ const abs = resolve(projectRoot, filePath);
165
+ try {
166
+ const exists = existsSync(abs);
167
+ if (exists) {
168
+ return { success: false, output: `Dosya zaten var: ${filePath}. De\u011Fi\u015Ftirmek i\xE7in apply_patch kullan\u0131n.` };
169
+ }
170
+ await mkdir(dirname(abs), { recursive: true });
171
+ await writeFile(abs, content, "utf-8");
172
+ return { success: true, output: `\u2713 Dosya olu\u015Fturuldu: ${filePath} (${content.split("\n").length} sat\u0131r)` };
173
+ } catch (err) {
174
+ return { success: false, output: `Dosya olu\u015Fturulamad\u0131: ${String(err)}` };
175
+ }
176
+ }
177
+ async function grepFiles(projectRoot, pattern, directory = ".", fileGlob = "*") {
178
+ if (!isSafePath(projectRoot, directory)) {
179
+ return { success: false, output: `G\xFCvenlik hatas\u0131: "${directory}" proje dizini d\u0131\u015F\u0131nda.` };
180
+ }
181
+ const absDir = resolve(projectRoot, directory);
182
+ const IGNORE = /* @__PURE__ */ new Set(["node_modules", ".git", "__pycache__", ".venv", "dist", "build", ".next"]);
183
+ const results = [];
184
+ const MAX_RESULTS = 100;
185
+ let regex;
186
+ try {
187
+ regex = new RegExp(pattern, "i");
188
+ } catch {
189
+ return { success: false, output: `Ge\xE7ersiz regex pattern: "${pattern}"` };
190
+ }
191
+ const globToRegex = (glob) => {
192
+ const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
193
+ return new RegExp(`^${escaped}$`, "i");
194
+ };
195
+ const filePattern = globToRegex(fileGlob);
196
+ async function walk(dir) {
197
+ if (results.length >= MAX_RESULTS) return;
198
+ let entries;
199
+ try {
200
+ entries = await readdir(dir, { withFileTypes: true });
201
+ } catch {
202
+ return;
203
+ }
204
+ for (const e of entries) {
205
+ if (results.length >= MAX_RESULTS) return;
206
+ if (IGNORE.has(e.name) || e.name.startsWith(".")) continue;
207
+ const fullPath = join(dir, e.name);
208
+ if (e.isDirectory()) {
209
+ await walk(fullPath);
210
+ } else if (filePattern.test(e.name)) {
211
+ let content;
212
+ try {
213
+ content = await readFile(fullPath, "utf-8");
214
+ } catch {
215
+ continue;
216
+ }
217
+ const lines = content.split("\n");
218
+ const relPath = relative(projectRoot, fullPath);
219
+ for (let i = 0; i < lines.length; i++) {
220
+ if (regex.test(lines[i])) {
221
+ results.push(`${relPath}:${i + 1}:${lines[i].trim().slice(0, 120)}`);
222
+ if (results.length >= MAX_RESULTS) break;
223
+ }
224
+ }
225
+ }
226
+ }
227
+ }
228
+ await walk(absDir);
229
+ if (results.length === 0) return { success: true, output: `"${pattern}" i\xE7in e\u015Fle\u015Fme bulunamad\u0131.` };
230
+ const out = results.join("\n");
231
+ return { success: true, output: results.length >= MAX_RESULTS ? out + `
232
+ ... (max ${MAX_RESULTS} sonu\xE7)` : out };
233
+ }
234
+ async function findFiles(projectRoot, namePattern, directory = ".") {
235
+ if (!isSafePath(projectRoot, directory)) {
236
+ return { success: false, output: `G\xFCvenlik hatas\u0131: "${directory}" proje dizini d\u0131\u015F\u0131nda.` };
237
+ }
238
+ const absDir = resolve(projectRoot, directory);
239
+ const IGNORE = /* @__PURE__ */ new Set(["node_modules", ".git", "__pycache__", ".venv", "dist", "build", ".next"]);
240
+ const results = [];
241
+ const globToRegex = (glob) => {
242
+ const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
243
+ return new RegExp(`^${escaped}$`, "i");
244
+ };
245
+ const pattern = globToRegex(namePattern);
246
+ async function walk(dir, depth) {
247
+ if (depth > 6 || results.length >= 200) return;
248
+ let entries;
249
+ try {
250
+ entries = await readdir(dir, { withFileTypes: true });
251
+ } catch {
252
+ return;
253
+ }
254
+ for (const e of entries) {
255
+ if (IGNORE.has(e.name) || e.name.startsWith(".")) continue;
256
+ const fullPath = join(dir, e.name);
257
+ if (e.isDirectory()) {
258
+ await walk(fullPath, depth + 1);
259
+ } else if (pattern.test(e.name)) {
260
+ results.push(relative(projectRoot, fullPath));
261
+ }
262
+ }
263
+ }
264
+ await walk(absDir, 0);
265
+ if (results.length === 0) return { success: true, output: `"${namePattern}" ad\u0131nda dosya bulunamad\u0131.` };
266
+ return { success: true, output: results.join("\n") };
267
+ }
268
+ function findNormalized(original, searchString) {
269
+ const origLines = original.split("\n");
270
+ const searchLines = searchString.split("\n").map((l) => l.replace(/\r$/, "").trim()).filter((l, i, arr) => !(i === 0 || i === arr.length - 1) || l !== "");
271
+ if (searchLines.length === 0) return null;
272
+ for (let oi = 0; oi <= origLines.length - searchLines.length; oi++) {
273
+ let match = true;
274
+ for (let si = 0; si < searchLines.length; si++) {
275
+ const origTrimmed = origLines[oi + si].replace(/\r$/, "").trim();
276
+ if (origTrimmed !== searchLines[si]) {
277
+ match = false;
278
+ break;
279
+ }
280
+ }
281
+ if (match) {
282
+ const start = origLines.slice(0, oi).join("\n").length + (oi > 0 ? 1 : 0);
283
+ const end = origLines.slice(0, oi + searchLines.length).join("\n").length + (oi + searchLines.length < origLines.length ? 1 : 0);
284
+ return { start, end };
285
+ }
286
+ }
287
+ return null;
288
+ }
289
+ function buildNotFoundHint(original, searchString) {
290
+ const origLines = original.split("\n").map((l) => l.replace(/\r$/, ""));
291
+ const searchLines = searchString.split("\n").map((l) => l.replace(/\r$/, "").trim()).filter(Boolean);
292
+ const diffs = [];
293
+ for (const sl of searchLines) {
294
+ const exactIdx = origLines.findIndex((l) => l.trim() === sl);
295
+ if (exactIdx !== -1) continue;
296
+ const prefix = sl.slice(0, 20);
297
+ const closeIdx = origLines.findIndex((l) => l.trim().startsWith(prefix) || l.includes(prefix));
298
+ if (closeIdx !== -1) {
299
+ diffs.push(` YANLI\u015E: "${sl.slice(0, 80)}"`);
300
+ diffs.push(` DO\u011ERU : "${origLines[closeIdx].trim().slice(0, 80)}" (sat\u0131r ${closeIdx + 1})`);
301
+ }
302
+ }
303
+ if (diffs.length > 0) {
304
+ return `
305
+
306
+ Fark tespit edildi:
307
+ ${diffs.join("\n")}
308
+ \u2192 DO\u011ERU sat\u0131r\u0131 search_string olarak kullan\u0131n, birebir kopyalay\u0131n.`;
309
+ }
310
+ const firstLine = searchLines[0] ?? "";
311
+ const keyword = firstLine.split(/[.(]/).find((p) => p.trim().length > 4)?.trim() ?? firstLine.slice(0, 15);
312
+ const nearby = origLines.map((l, i) => ({ l: l.trim(), i })).filter(({ l }) => l.includes(keyword)).slice(0, 3).map(({ l, i }) => ` sat\u0131r ${i + 1}: ${l.slice(0, 100)}`);
313
+ return nearby.length > 0 ? `
314
+
315
+ "${keyword}" i\xE7eren sat\u0131rlar:
316
+ ${nearby.join("\n")}
317
+ \u2192 read_file ile dosyay\u0131 okuyun, ger\xE7ek i\xE7eri\u011Fi kullan\u0131n.` : `
318
+ \u2192 read_file ile dosyay\u0131 okuyun, ard\u0131ndan ger\xE7ek kodu search_string olarak birebir kopyalay\u0131n.`;
319
+ }
320
+ async function applyPatch(projectRoot, filePath, searchString, replaceString) {
321
+ if (!isSafePath(projectRoot, filePath)) {
322
+ return { success: false, output: `G\xFCvenlik hatas\u0131: "${filePath}" proje dizini d\u0131\u015F\u0131nda.` };
323
+ }
324
+ const abs = resolve(projectRoot, filePath);
325
+ try {
326
+ const original = await readFile(abs, "utf-8");
327
+ if (original.includes(searchString)) {
328
+ const bakPath = abs + `.${Date.now()}.bak`;
329
+ await copyFile(abs, bakPath);
330
+ const patched = original.split(searchString).join(replaceString);
331
+ await mkdir(dirname(abs), { recursive: true });
332
+ await writeFile(abs, patched, "utf-8");
333
+ return {
334
+ success: true,
335
+ output: `\u2713 Patch uyguland\u0131: ${filePath} (${replaceString.split("\n").length} sat\u0131r). Yedek: ${basename(bakPath)}`
336
+ };
337
+ }
338
+ const match = findNormalized(original, searchString);
339
+ if (match) {
340
+ const bakPath = abs + `.${Date.now()}.bak`;
341
+ await copyFile(abs, bakPath);
342
+ const patched = original.slice(0, match.start) + replaceString + original.slice(match.end);
343
+ await mkdir(dirname(abs), { recursive: true });
344
+ await writeFile(abs, patched, "utf-8");
345
+ return {
346
+ success: true,
347
+ output: `\u2713 Patch uyguland\u0131 (whitespace normalize): ${filePath} (${replaceString.split("\n").length} sat\u0131r). Yedek: ${basename(bakPath)}`
348
+ };
349
+ }
350
+ if (replaceString.trim().length > 0 && original.includes(replaceString.trim())) {
351
+ return {
352
+ success: true,
353
+ output: `\u2713 Patch zaten uygulanm\u0131\u015F (idempotent): ${filePath} \u2014 search_string bulunamad\u0131 ama replace_string dosyada mevcut.`
354
+ };
355
+ }
356
+ const normalizedReplace = replaceString.split("\n").map((l) => l.trim()).filter(Boolean).join(" ");
357
+ const normalizedOriginal = original.split("\n").map((l) => l.trim()).filter(Boolean).join(" ");
358
+ if (normalizedReplace.length > 10 && normalizedOriginal.includes(normalizedReplace)) {
359
+ return {
360
+ success: true,
361
+ output: `\u2713 Patch zaten uygulanm\u0131\u015F (whitespace normalize, idempotent): ${filePath}`
362
+ };
363
+ }
364
+ const hint = buildNotFoundHint(original, searchString);
365
+ return {
366
+ success: false,
367
+ output: `Arama dizesi bulunamad\u0131: ${filePath}
368
+ Aranan (ilk 300 karakter):
369
+ ${searchString.slice(0, 300)}${hint}`
370
+ };
371
+ } catch (err) {
372
+ return { success: false, output: `Patch ba\u015Far\u0131s\u0131z: ${String(err)}` };
373
+ }
374
+ }
375
+ async function runCommand(projectRoot, command) {
376
+ if (!isAllowedCommand(command, projectRoot)) {
377
+ return {
378
+ success: false,
379
+ output: `G\xFCvenlik hatas\u0131: "${command}" izin verilenler listesinde de\u011Fil.
380
+ \u0130zin verilenler: ${ALLOWED_COMMAND_PREFIXES.join(", ")}
381
+ \u0130pucu: Alt dizindeki wrapper i\xE7in "cd subdir && mvnw.cmd test" format\u0131n\u0131 kullan \u2014 cd ve komut ayn\u0131 && zincirinde olabilir.`
382
+ };
383
+ }
384
+ return new Promise((resolve3) => {
385
+ const child = spawn(command, [], {
386
+ cwd: projectRoot,
387
+ shell: true,
388
+ env: { ...process.env }
389
+ });
390
+ const MAX_OUTPUT = 1e4;
391
+ let stdout2 = "";
392
+ let stderr = "";
393
+ child.stdout?.on("data", (d) => {
394
+ stdout2 += d.toString();
395
+ if (stdout2.length > MAX_OUTPUT) stdout2 = stdout2.slice(-MAX_OUTPUT);
396
+ });
397
+ child.stderr?.on("data", (d) => {
398
+ stderr += d.toString();
399
+ if (stderr.length > MAX_OUTPUT) stderr = stderr.slice(-MAX_OUTPUT);
400
+ });
401
+ let settled = false;
402
+ const timeout = setTimeout(() => {
403
+ if (settled) return;
404
+ settled = true;
405
+ child.kill("SIGKILL");
406
+ resolve3({ success: false, output: `Zaman a\u015F\u0131m\u0131 (180s): ${command}` });
407
+ }, 18e4);
408
+ child.on("close", (code) => {
409
+ if (settled) return;
410
+ settled = true;
411
+ clearTimeout(timeout);
412
+ const combined = [stdout2, stderr].filter(Boolean).join("\n").trim();
413
+ resolve3({
414
+ success: code === 0,
415
+ output: combined || `(\xE7\u0131kt\u0131 yok, exit code: ${code})`
416
+ });
417
+ });
418
+ child.on("error", (err) => {
419
+ if (settled) return;
420
+ settled = true;
421
+ clearTimeout(timeout);
422
+ resolve3({ success: false, output: `Komut ba\u015Flat\u0131lamad\u0131: ${String(err)}` });
423
+ });
424
+ });
425
+ }
426
+ var TOOL_DEFINITIONS = [
427
+ {
428
+ type: "function",
429
+ function: {
430
+ name: "list_files",
431
+ description: "Proje dizini i\xE7indeki dosya ve klas\xF6rleri listeler (max 3 derinlik).",
432
+ parameters: {
433
+ type: "object",
434
+ properties: {
435
+ directory: { type: "string", description: "Listelenecek dizin (proje k\xF6k\xFCne g\xF6re relative). \xD6rn: 'src', '.'" }
436
+ },
437
+ required: ["directory"]
438
+ }
439
+ }
440
+ },
441
+ {
442
+ type: "function",
443
+ function: {
444
+ name: "read_file",
445
+ description: "Dosya i\xE7eri\u011Fini okur. 300+ sat\u0131rl\u0131 dosyalarda \xF6nce grep_files ile sat\u0131r numaras\u0131n\u0131 bul, sonra start_line/end_line ile sadece ilgili b\xF6l\xFCm\xFC oku. K\xFC\xE7\xFCk dosyalarda parametresiz kullanabilirsin.",
446
+ parameters: {
447
+ type: "object",
448
+ properties: {
449
+ path: { type: "string", description: "Okunacak dosyan\u0131n yolu (proje k\xF6k\xFCne g\xF6re relative)." },
450
+ start_line: { type: "string", description: "Okunmaya ba\u015Flanacak sat\u0131r numaras\u0131 (1-tabanl\u0131). \xD6rn: '35'" },
451
+ end_line: { type: "string", description: "Okunacak son sat\u0131r numaras\u0131. Belirtilmezse start_line+80 kullan\u0131l\u0131r." }
452
+ },
453
+ required: ["path"]
454
+ }
455
+ }
456
+ },
457
+ {
458
+ type: "function",
459
+ function: {
460
+ name: "grep_files",
461
+ description: "Proje dosyalar\u0131nda regex/kelime arar. Hangi dosyalar\u0131n ilgili oldu\u011Funu bulmak i\xE7in read_file'dan \xF6nce kullan. grep -r gibi \xE7al\u0131\u015F\u0131r.",
462
+ parameters: {
463
+ type: "object",
464
+ properties: {
465
+ pattern: { type: "string", description: "Aranacak kelime veya regex. \xD6rn: 'phoneNo', 'username.*phone', 'UserService'" },
466
+ directory: { type: "string", description: "Arama yap\u0131lacak dizin (proje k\xF6k\xFCne g\xF6re). Varsay\u0131lan: '.'" },
467
+ file_glob: { type: "string", description: "Dosya ad\u0131 filtresi. \xD6rn: '*.ts', '*.java', '*.py'. Varsay\u0131lan: '*'" }
468
+ },
469
+ required: ["pattern"]
470
+ }
471
+ }
472
+ },
473
+ {
474
+ type: "function",
475
+ function: {
476
+ name: "find_files",
477
+ description: "Dosya ad\u0131na g\xF6re proje i\xE7inde dosya arar. find . -name gibi \xE7al\u0131\u015F\u0131r.",
478
+ parameters: {
479
+ type: "object",
480
+ properties: {
481
+ name_pattern: { type: "string", description: "Dosya ad\u0131 pattern'i. Glob desteklenir. \xD6rn: '*.service.ts', 'User*.java', 'test_*.py'" },
482
+ directory: { type: "string", description: "Arama yap\u0131lacak dizin. Varsay\u0131lan: '.'" }
483
+ },
484
+ required: ["name_pattern"]
485
+ }
486
+ }
487
+ },
488
+ {
489
+ type: "function",
490
+ function: {
491
+ name: "apply_patch",
492
+ description: "Dosyada belirtilen kod blo\u011Funu bulup yeni kodla de\u011Fi\u015Ftirir. search_string tam e\u015Fle\u015Fme gerektirir.",
493
+ parameters: {
494
+ type: "object",
495
+ properties: {
496
+ path: { type: "string", description: "De\u011Fi\u015Ftirilecek dosyan\u0131n yolu." },
497
+ search_string: { type: "string", description: "Dosyada bulunup de\u011Fi\u015Ftirilecek mevcut kod blo\u011Fu." },
498
+ replace_string: { type: "string", description: "Eski kodun yerine ge\xE7ecek yeni kod blo\u011Fu." }
499
+ },
500
+ required: ["path", "search_string", "replace_string"]
501
+ }
502
+ }
503
+ },
504
+ {
505
+ type: "function",
506
+ function: {
507
+ name: "create_file",
508
+ description: "Yeni bir dosya olu\u015Fturur ve i\xE7eri\u011Fini yazar. Sadece var olmayan dosyalar i\xE7in kullan\u0131n \u2014 mevcut dosya varsa apply_patch kullan\u0131n.",
509
+ parameters: {
510
+ type: "object",
511
+ properties: {
512
+ path: { type: "string", description: "Olu\u015Fturulacak dosyan\u0131n yolu (proje k\xF6k\xFCne g\xF6re relative)." },
513
+ content: { type: "string", description: "Dosyaya yaz\u0131lacak tam i\xE7erik." }
514
+ },
515
+ required: ["path", "content"]
516
+ }
517
+ }
518
+ },
519
+ {
520
+ type: "function",
521
+ function: {
522
+ name: "run_command",
523
+ description: "Test veya build komutu \xE7al\u0131\u015Ft\u0131r\u0131r. Yaln\u0131zca pytest, npm test, go test vb. izinlidir.",
524
+ parameters: {
525
+ type: "object",
526
+ properties: {
527
+ command: { type: "string", description: "\xC7al\u0131\u015Ft\u0131r\u0131lacak komut. \xD6rn: 'pytest tests/', 'npm test'" }
528
+ },
529
+ required: ["command"]
530
+ }
531
+ }
532
+ }
533
+ ];
534
+ async function executeTool(name, args, projectRoot) {
535
+ switch (name) {
536
+ case "list_files":
537
+ return listFiles(projectRoot, args.directory ?? ".");
538
+ case "read_file":
539
+ return readFile_(projectRoot, args.path ?? "", args.start_line ? parseInt(args.start_line) : void 0, args.end_line ? parseInt(args.end_line) : void 0, args.full === "true");
540
+ case "grep_files":
541
+ return grepFiles(projectRoot, args.pattern ?? "", args.directory ?? ".", args.file_glob ?? "*");
542
+ case "find_files":
543
+ return findFiles(projectRoot, args.name_pattern ?? "", args.directory ?? ".");
544
+ case "apply_patch":
545
+ return applyPatch(projectRoot, args.path ?? "", args.search_string ?? "", args.replace_string ?? "");
546
+ case "create_file":
547
+ return createFile(projectRoot, args.path ?? "", args.content ?? "");
548
+ case "run_command":
549
+ return runCommand(projectRoot, args.command ?? "");
550
+ default:
551
+ return { success: false, output: `Bilinmeyen ara\xE7: ${name}` };
552
+ }
553
+ }
554
+
555
+ // src/agent-plan.ts
556
+ import { stdin, stdout } from "process";
557
+ import chalk2 from "chalk";
558
+ import { join as join2 } from "path";
559
+ import { mkdir as mkdir2, writeFile as writeFile2, readFile as readFile2, readdir as readdir2 } from "fs/promises";
560
+ import { existsSync as existsSync2 } from "fs";
561
+
562
+ // src/rich-format.ts
563
+ import chalk from "chalk";
564
+ var C = {
565
+ file: chalk.hex("#e5c07b").underline,
566
+ // dosya yolu — sarı/amber
567
+ line: chalk.hex("#d19a66"),
568
+ // satır numarası — turuncu
569
+ cls: chalk.hex("#56b6c2").bold,
570
+ // class/tip adı — cyan
571
+ fn: chalk.hex("#98c379").bold,
572
+ // fonksiyon/metot — yeşil
573
+ kw: chalk.hex("#c678dd"),
574
+ // keyword — mor
575
+ str: chalk.hex("#e5c07b"),
576
+ // string — sarı
577
+ num: chalk.hex("#d19a66"),
578
+ // sayı — turuncu
579
+ dim: chalk.dim,
580
+ bold: chalk.white.bold,
581
+ gutter: chalk.hex("#4b5263"),
582
+ // satır çizgisi
583
+ gutterN: chalk.hex("#636d83")
584
+ // satır numarası kolon
585
+ };
586
+ function formatFilePath(path, lineStart, lineEnd) {
587
+ const lineInfo = lineStart !== void 0 ? C.line(`:${lineStart}`) + (lineEnd !== void 0 ? C.dim(`\u2013${lineEnd}`) : "") : "";
588
+ return C.file(path) + lineInfo;
589
+ }
590
+ function formatToolCall(name, args) {
591
+ const icon = toolIcon(name);
592
+ const detail = formatToolArgs(name, args);
593
+ return chalk.hex("#c678dd").bold(`\u2699 ${name}`) + " " + detail;
594
+ }
595
+ function toolIcon(name) {
596
+ switch (name) {
597
+ case "read_file":
598
+ return "\u{1F4C4}";
599
+ case "list_files":
600
+ return "\u{1F4C1}";
601
+ case "apply_patch":
602
+ return "\u270F\uFE0F ";
603
+ case "run_command":
604
+ return "\u25B6 ";
605
+ default:
606
+ return "\u2699";
607
+ }
608
+ }
609
+ function trimPath(p, max = 60) {
610
+ if (!p) return "";
611
+ if ([...p].length <= max) return p;
612
+ return "\u2026" + [...p].slice(-max).join("");
613
+ }
614
+ function formatToolArgs(name, args) {
615
+ switch (name) {
616
+ case "read_file":
617
+ return C.file(trimPath(args.path ?? ""));
618
+ case "list_files":
619
+ return C.dim("dir: ") + C.file(trimPath(args.directory ?? "."));
620
+ case "apply_patch":
621
+ return C.file(trimPath(args.path ?? "")) + C.dim(" (patch)");
622
+ case "run_command":
623
+ return chalk.hex("#98c379")(args.command ?? "");
624
+ default:
625
+ return C.dim(JSON.stringify(args).slice(0, 60));
626
+ }
627
+ }
628
+ function formatInstructions(text) {
629
+ return text.replace(/\*\*([^*]+)\*\*/g, (_, m) => chalk.bold(m)).replace(/(?<!\w)[*_]([^*_]+)[*_](?!\w)/g, (_, m) => chalk.italic(m)).replace(/\b((?:[\w-]+\/)+[\w.-]+\.\w+)\b/g, (m) => C.file(m)).replace(
630
+ /\b(?:L|line |satır )(\d+)(?:-(\d+))?\b/gi,
631
+ (_, s, e) => C.line(`L${s}`) + (e ? C.dim(`-${e}`) : "")
632
+ ).replace(/\b([a-z][a-zA-Z0-9]+)\(\)/g, (_, m) => C.fn(m) + C.dim("()")).replace(/@([A-Z][A-Za-z0-9]+)/g, (_, m) => "@" + C.cls(m)).replace(/`([^`]+)`/g, (_, m) => chalk.bgHex("#2b2b2b").whiteBright(` ${m} `));
633
+ }
634
+
635
+ // src/agent-plan.ts
636
+ var PLANS_DIR = ".trello_plans";
637
+ var EXECUTES_DIR = ".trello_executes";
638
+ function plansDir(projectRoot) {
639
+ return join2(projectRoot, PLANS_DIR);
640
+ }
641
+ function executesDir(projectRoot) {
642
+ return join2(projectRoot, EXECUTES_DIR);
643
+ }
644
+ async function saveExecuteRecord(projectRoot, plan, record) {
645
+ const dir = executesDir(projectRoot);
646
+ await mkdir2(dir, { recursive: true });
647
+ const slug = plan.trelloCardName.toLowerCase().replace(/[^\w\s-]/g, "").trim().replace(/\s+/g, "-").slice(0, 50);
648
+ const now = /* @__PURE__ */ new Date();
649
+ const pad = (n) => String(n).padStart(2, "0");
650
+ const ts = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}T${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}`;
651
+ const fileName = slug ? `${slug}_${ts}.json` : `${record.id}_${ts}.json`;
652
+ const filePath = join2(dir, fileName);
653
+ await writeFile2(filePath, JSON.stringify(record, null, 2), "utf-8");
654
+ return filePath;
655
+ }
656
+ async function savePlan(projectRoot, plan) {
657
+ const dir = plansDir(projectRoot);
658
+ await mkdir2(dir, { recursive: true });
659
+ const slug = plan.trelloCardName.toLowerCase().replace(/[^\w\s-]/g, "").trim().replace(/\s+/g, "-").slice(0, 50);
660
+ const now = /* @__PURE__ */ new Date();
661
+ const pad = (n) => String(n).padStart(2, "0");
662
+ const ts = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}T${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}`;
663
+ const fileName = slug ? `${slug}_${ts}.json` : `${plan.id}_${ts}.json`;
664
+ const filePath = join2(dir, fileName);
665
+ await writeFile2(filePath, JSON.stringify(plan, null, 2), "utf-8");
666
+ return filePath;
667
+ }
668
+ async function listPlans(projectRoot) {
669
+ const dir = plansDir(projectRoot);
670
+ if (!existsSync2(dir)) return [];
671
+ try {
672
+ const files = (await readdir2(dir)).filter((f) => f.endsWith(".json"));
673
+ const plans = [];
674
+ for (const f of files) {
675
+ try {
676
+ const raw = await readFile2(join2(dir, f), "utf-8");
677
+ plans.push(JSON.parse(raw));
678
+ } catch {
679
+ }
680
+ }
681
+ return plans.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
682
+ } catch {
683
+ return [];
684
+ }
685
+ }
686
+ var PLAN_SYSTEM_PROMPT = `Sen Lastgen (GLM 5.2) mimarisinin K\u0131demli Yaz\u0131l\u0131m Mimar\u0131 ve Teknik Liderisin.
687
+ G\xF6revin, sana sunulan harici g\xF6rev tan\u0131m\u0131n\u0131 (Jira bileti, Trello kart\u0131 veya kullan\u0131c\u0131n\u0131n kopyala-yap\u0131\u015Ft\u0131r ile do\u011Frudan girdi\u011Fi ham metin/kod blo\u011Fu) ve projenin mevcut dosya/kod yap\u0131s\u0131n\u0131 analiz ederek, bir yaz\u0131l\u0131m ajan\u0131n\u0131n (coding agent) hatas\u0131z uygulayabilece\u011Fi detayl\u0131 bir "Yaz\u0131l\u0131m Uygulama Plan\u0131" (Implementation Plan) haz\u0131rlamakt\u0131r.
688
+
689
+ Girdinin hangi platformdan (Jira, Trello, Manuel) geldi\u011Fi \xF6nemsizdir; sen i\xE7eri\u011Fin teknik \xF6z\xFCne ve projenin kaynak kodlar\u0131na odaklanmal\u0131s\u0131n.
690
+
691
+ ARA\u015ETIRMA A\u015EAMASI (\xF6nce yap \u2014 terminal ara\xE7lar\u0131 gibi kullan):
692
+ 1. list_files ile dizin yap\u0131s\u0131n\u0131 ke\u015Ffet \u2014 proje yap\u0131s\u0131n\u0131 anla (backend/frontend/mod\xFCller)
693
+ 2. G\xF6rev a\xE7\u0131klamas\u0131ndaki her kavram\u0131 kodda nas\u0131l ge\xE7ebilece\u011Fini tahmin ederek ara:
694
+ - G\xF6rev T\xFCrk\xE7eyse: kavram\u0131n \u0130ngilizce kar\u015F\u0131l\u0131klar\u0131n\u0131, yayg\u0131n k\u0131saltmalar\u0131n\u0131 ve camelCase/snake_case varyantlar\u0131n\u0131 dene
695
+ - G\xF6rev \u0130ngilizceyse: farkl\u0131 yaz\u0131m bi\xE7imlerini (camelCase, snake_case, PascalCase) dene
696
+ - \u0130lk grep_files sonu\xE7 vermezse farkl\u0131 bir terimle tekrar dene \u2014 tek aramada vazge\xE7me
697
+ - Bir kavram i\xE7in 2-3 farkl\u0131 terim denemesi normaldir
698
+ 3. find_files ile ilgili dosyalar\u0131 ada g\xF6re bul (\xF6rn. "*.service.ts", "User*.java")
699
+ 4. read_file ile bulunan dosyalar\u0131 oku \u2014 tahmin etme, g\xF6r
700
+ 5. DERINLEMESINE ETK\u0130 ANAL\u0130Z\u0130 \u2014 de\u011Fi\u015Ftirilecek her bile\u015Fen i\xE7in:
701
+ a. Bu bile\u015Feni kim kullan\u0131yor? grep_files ile bile\u015Fen ad\u0131n\u0131 ara \u2014 \xE7a\u011F\u0131ran yerleri bul ve oku
702
+ b. De\u011Fi\u015Fiklik mevcut davran\u0131\u015F\u0131 nas\u0131l etkiler? \xC7a\u011F\u0131ran taraf yeni imzaya/davran\u0131\u015Fa uyum sa\u011Flamal\u0131 m\u0131?
703
+ c. Ayn\u0131 katmanda paralel bile\u015Fenler var m\u0131? (\xF6rn. ba\u015Fka Controller'lar, Service implementasyonlar\u0131) Onlar da etkileniyor mu?
704
+ d. Frontend\u2013backend aray\xFCz\xFC de\u011Fi\u015Fiyor mu? API response yap\u0131s\u0131, field adlar\u0131, HTTP status kodlar\u0131 de\u011Fi\u015Firse frontend'de neyin g\xFCncellenmesi gerekiyor?
705
+ 6. Mevcut test dosyalar\u0131n\u0131 oku \u2014 ne test ediliyor, neyi mock'luyor, de\u011Fi\u015Fiklik sonras\u0131 hangi testler k\u0131r\u0131lacak?
706
+
707
+ A\u015Fa\u011F\u0131daki kurallara kesinlikle uymal\u0131s\u0131n:
708
+ 1. G\u0130RD\u0130 ANAL\u0130Z\u0130: G\xF6revin veya hatan\u0131n (bug) projenin hangi bile\u015Fenlerini etkiledi\u011Fini derinlemesine analiz et. Do\u011Frudan de\u011Fi\u015Fen dosyalar\u0131n yan\u0131 s\u0131ra bunlar\u0131 kullanan (\xE7a\u011F\u0131ran, import eden, extend eden) bile\u015Fenleri de tespit et \u2014 zincirleme etkiyi plan\u0131na yans\u0131t.
709
+ 2. ETK\u0130 ALANI: De\u011Fi\u015Fmesi veya yeni eklenmesi gereken dosyalar\u0131 tam yollar\u0131yla (path) tespit et. Path'leri ASLA k\u0131saltma \u2014 "\u2026" veya "..." kullanma. Tam relative path ver (\xF6rn: src/main/java/com/sirket/exception/InvalidFilterException.java).
710
+ 3. ATOM\u0130K ADIMLAR VE ADIM SIRASI:
711
+ - MEVCUT DOSYA de\u011Fi\u015Fikliklerinde: bir ad\u0131mda tek dosya, tek apply_patch. Import eklemesi ayr\u0131 ad\u0131m, metot de\u011Fi\u015Fikli\u011Fi ayr\u0131 ad\u0131m. Coding agent tek apply_patch yapabilecek gran\xFClerlikte tut.
712
+ - YEN\u0130 DOSYA olu\u015Fturmada: create_file ile yaz\u0131lacak dosyalar\u0131n t\xFCm i\xE7eri\u011Fi (tam, \xE7al\u0131\u015F\u0131r kod) TEK ad\u0131mda instructions alan\u0131na yaz\u0131l\u0131r. GlobalExceptionHandler gibi yeni bir s\u0131n\u0131f\u0131n t\xFCm metodlar\u0131 tek ad\u0131mda olu\u015Fturulur \u2014 her metot i\xE7in ayr\u0131 ad\u0131m a\xE7ma.
713
+ - BA\u011EIMLILIK SIRASI: Bir ad\u0131m ba\u015Fka ad\u0131m\u0131n olu\u015Fturdu\u011Fu s\u0131n\u0131fa/dosyaya ba\u011F\u0131ml\u0131ysa, \xF6nce olu\u015Ftur, sonra kullan. \xD6rn: Exception s\u0131n\u0131f\u0131 olu\u015Fturma ad\u0131m\u0131 \u2192 o exception'\u0131 import eden ad\u0131m \u2192 o exception'\u0131 f\u0131rlatan ad\u0131m s\u0131ralamas\u0131 zorunlu.
714
+ 4. KAPSAMLI OLDU\u011EUNDAN EM\u0130N OL: Okudu\u011Fun t\xFCm dosyalardaki t\xFCm gerekli de\u011Fi\u015Fiklikleri steps'e ekle. Hi\xE7bir dosyay\u0131 atlama. Eksik ad\u0131m b\u0131rakma.
715
+ 5. TEST ODAKLILIK: Mevcut test dosyalar\u0131n\u0131 ara\u015Ft\u0131rma a\u015Famas\u0131nda oku \u2014 ne test edildi\u011Fini, hangi mock'lar\u0131n kullan\u0131ld\u0131\u011F\u0131n\u0131 anla. De\u011Fi\u015Fiklik sonras\u0131 k\u0131r\u0131lacak testleri g\xFCncelle, yeni davran\u0131\u015Flar\u0131 test eden ad\u0131mlar ekle. verification_command olarak \xE7al\u0131\u015Ft\u0131r\u0131labilir test komutunu belirt. M\xFCmk\xFCnse ilgili test s\u0131n\u0131flar\u0131n\u0131 hedefle (\xF6rn. mvnw.cmd test -Dtest=PaymentControllerTest).
716
+ 6. INSTRUCTIONS KALITESI: Her ad\u0131m\u0131n instructions alan\u0131 execute agent'\u0131n kodu okumadan anlayabilece\u011Fi kadar spesifik olmal\u0131:
717
+ - Mevcut dosya: "X s\u0131n\u0131f\u0131ndaki Y metodunda Z blo\u011Funu bul ve \u015Fununla de\u011Fi\u015Ftir. Import'a W ekle."
718
+ - Yeni dosya: instructions alan\u0131na dosyan\u0131n TAM ve \xC7ALI\u015EIR kodunu yaz \u2014 agent bu kodu create_file ile dosyaya yazacak.
719
+ - Hedef sat\u0131r/blok belirtilmemi\u015Fse agent tahmin etmek zorunda kal\u0131r ve iterasyon harcar \u2014 bunu \xF6nle.
720
+ 7. DE\u011EI\u015EKEN/STATE TANIMLAMASI: Yeni kod blo\u011Fu bir de\u011Fi\u015Fken, state veya ref kullan\u0131yorsa, o de\u011Fi\u015Fkenin nerede tan\u0131mland\u0131\u011F\u0131n\u0131 do\u011Frula. React bile\u015Fenlerinde:
721
+ - Yeni state gerekiyorsa \u2192 useState bildirimi AYRI bir ad\u0131m olarak components'\u0131n en \xFCst\xFCne ekle
722
+ - Yeni ref gerekiyorsa \u2192 useRef bildirimi AYRI bir ad\u0131m olarak ekle
723
+ - Import gerekiyorsa \u2192 import ad\u0131m\u0131 ba\u011F\u0131ml\u0131l\u0131k s\u0131ras\u0131na g\xF6re \xF6nce gelmeli
724
+ Bu kural t\xFCm diller i\xE7in ge\xE7erli: Java'da field tan\u0131m\u0131, Python'da __ init__ de\u011Fi\u015Fkeni, TypeScript'te interface property \u2014 \xF6nce tan\u0131mla, sonra kullan.
725
+ 9. FRAMEWORK YA\u015EAM D\xD6NG\xDCS\xDC: Framework'\xFCn zaten ele ald\u0131\u011F\u0131 durumlar i\xE7in gereksiz kod ekleme. Spring'de @RequestParam/@PathVariable olarak tan\u0131mlanm\u0131\u015F UUID/Integer gibi tipler i\xE7in controller'a ula\u015Fmadan \xF6nce MethodArgumentTypeMismatchException f\u0131rlat\u0131l\u0131r \u2014 bu parametreler i\xE7in service katman\u0131nda ayr\u0131ca try-catch yazma. Genel olarak: bir hata framework taraf\u0131ndan yakalan\u0131yorsa (validator, param binding, type conversion), uygulama kodunda tekrar yakalamaya \xE7al\u0131\u015Fma.
726
+ 10. MEVCUT SINIF/UTIL KONTROL\xDC: Yeni bir exception, util veya helper s\u0131n\u0131f\u0131 olu\u015Fturmadan \xF6nce projede benzer bir s\u0131n\u0131f olup olmad\u0131\u011F\u0131n\u0131 grep_files ve find_files ile kontrol et. \u0130sim benzerli\u011Fi yetmez \u2014 i\xE7eri\u011Fini oku. E\u011Fer mevcut s\u0131n\u0131f g\xF6revi kar\u015F\u0131l\u0131yorsa yeni s\u0131n\u0131f olu\u015Fturma, mevcut olan\u0131 kullan.
727
+ 11. GLOBAL DE\u011E\u0130\u015E\u0130KL\u0130K ETK\u0130S\u0130: @RestControllerAdvice, Filter, Interceptor, AOP aspect gibi global etki yaratan bile\u015Fenler eklendi\u011Finde ya da de\u011Fi\u015Ftirildi\u011Finde, mevcut controller ve servislerda catch bloklar\u0131 veya \xF6zel hata yan\u0131tlar\u0131 var m\u0131 grep_files ile kontrol et. Bu t\xFCr global de\u011Fi\u015Fikliklerin mevcut davran\u0131\u015F\u0131 nas\u0131l etkileyece\u011Fini ad\u0131m a\xE7\u0131klamas\u0131na ekle.
728
+ 12. VERIFICATION COMMAND:
729
+ - Windows'ta mvnw/gradlew wrapper: ./mvnw yerine mvnw.cmd kullan. Alt dizindeyse "cd submodule && mvnw.cmd test" format\u0131.
730
+ - React/CRA projelerinde (package.json'da react-scripts varsa): "npm test" yerine "CI=true npm test" yaz \u2014 aksi h\xE2lde watch mode'da tak\u0131l\u0131r, asla bitmez.
731
+ - Test komutu alt dizinde \xE7al\u0131\u015F\u0131yorsa: "cd subdir && CI=true npm test" format\u0131.
732
+
733
+ \xC7IKTI FORMATI:
734
+ Cevab\u0131n\u0131 KES\u0130NL\u0130KLE hi\xE7bir sohbet c\xFCmlesi (\xD6rn: "\u0130\u015Fte plan\u0131n\u0131z:") eklemeden, SADECE ve SADECE a\u015Fa\u011F\u0131daki JSON format\u0131nda vermelisin. \xC7\u0131kt\u0131n ge\xE7erli bir JSON objesi olmal\u0131d\u0131r.
735
+
736
+ {
737
+ "task_source": "G\xF6revin geldi\u011Fi kaynak (\xD6rn: 'Jira', 'Trello', 'Manual_Entry')",
738
+ "task_id": "Varsa benzersiz g\xF6rev ID'si (\xD6rn: 'PROJ-123', 'card_99' veya manuel ise null)",
739
+ "title": "G\xF6revin k\u0131sa teknik ba\u015Fl\u0131\u011F\u0131",
740
+ "summary": "Problemin veya isterlerin k\u0131sa teknik \xF6zeti",
741
+ "affected_files": [
742
+ "Dosya yollar\u0131n\u0131n listesi"
743
+ ],
744
+ "steps": [
745
+ {
746
+ "step_number": 1,
747
+ "description": "Ad\u0131m\u0131n k\u0131sa a\xE7\u0131klamas\u0131",
748
+ "target_file": "De\u011Fi\u015Fiklik yap\u0131lacak dosyan\u0131n tam yolu",
749
+ "instructions": "Kodlama ajan\u0131na bu ad\u0131mda ne yapmas\u0131 gerekti\u011Fine dair spesifik talimat (Hangi fonksiyon eklenecek/de\u011Fi\u015Fecek)."
750
+ }
751
+ ],
752
+ "verification_command": "T\xFCm plan bitti\u011Finde sistemin \xE7al\u0131\u015Ft\u0131raca\u011F\u0131 ana test komutu (\xD6rn: pytest tests/test_helper.py)"
753
+ }
754
+
755
+ D\u0130L: T\xFCm d\xFC\u015F\xFCnme (thinking) ve a\xE7\u0131klama metinleri T\xFCrk\xE7e olsun. \u0130ngilizce yazma.`;
756
+ function describeFetchError(err, url) {
757
+ if (!(err instanceof Error)) return String(err);
758
+ const cause = err.cause;
759
+ const causeMsg = cause instanceof Error ? cause.message : typeof cause === "string" ? cause : null;
760
+ if (err.name === "TimeoutError" || err.message.includes("timed out")) {
761
+ return `API iste\u011Fi zaman a\u015F\u0131m\u0131na u\u011Frad\u0131 (120s) \u2014 ${url}`;
762
+ }
763
+ if (causeMsg?.includes("ECONNREFUSED")) return `Ba\u011Flant\u0131 reddedildi \u2014 sunucu \xE7al\u0131\u015F\u0131yor mu? (${url})`;
764
+ if (causeMsg?.includes("ENOTFOUND") || causeMsg?.includes("getaddrinfo")) return `DNS \xE7\xF6z\xFCmlenemedi \u2014 URL do\u011Fru mu? (${url})`;
765
+ if (causeMsg?.includes("ECONNRESET")) return `Ba\u011Flant\u0131 s\u0131f\u0131rland\u0131 (ECONNRESET) \u2014 ${url}`;
766
+ if (causeMsg?.includes("ETIMEDOUT")) return `Ba\u011Flant\u0131 zaman a\u015F\u0131m\u0131 (ETIMEDOUT) \u2014 ${url}`;
767
+ if (causeMsg?.includes("certificate") || causeMsg?.includes("SSL")) return `SSL/TLS hatas\u0131 \u2014 ${causeMsg}`;
768
+ return `A\u011F hatas\u0131: ${err.message}${causeMsg ? ` (${causeMsg})` : ""} \u2014 ${url}`;
769
+ }
770
+ var MAX_RETRIES = 4;
771
+ var DELAY_429_MS = 8e3;
772
+ var DELAY_NET_MS = 3e3;
773
+ function isTransientNet(cause) {
774
+ return cause.includes("ECONNRESET") || cause.includes("ECONNABORTED") || cause.includes("EPIPE") || cause.includes("UND_ERR_SOCKET") || cause.includes("UND_ERR_CONNECT") || cause.includes("ETIMEDOUT");
775
+ }
776
+ async function fetchWithRetry(url, options, maxRetries = MAX_RETRIES) {
777
+ let res;
778
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
779
+ try {
780
+ res = await fetch(url, options);
781
+ } catch (err) {
782
+ const e = err;
783
+ const cause = e.cause?.message ?? e.message ?? "";
784
+ if (e.name === "TimeoutError" || isTransientNet(cause)) {
785
+ if (attempt < maxRetries) {
786
+ await new Promise((r) => setTimeout(r, DELAY_NET_MS * (attempt + 1)));
787
+ continue;
788
+ }
789
+ throw new Error(describeFetchError(err, url));
790
+ }
791
+ throw new Error(describeFetchError(err, url));
792
+ }
793
+ if (res.status === 429) {
794
+ if (attempt === maxRetries) {
795
+ const text = await res.text().catch(() => "");
796
+ throw new Error(`API 429 \u2014 servis a\u015F\u0131r\u0131 y\xFCkl\xFC, tekrar deneme t\xFCkendi: ${text.slice(0, 200)}`);
797
+ }
798
+ await new Promise((r) => setTimeout(r, DELAY_429_MS * Math.pow(2, attempt)));
799
+ continue;
800
+ }
801
+ if (res.status >= 500) {
802
+ if (attempt < maxRetries) {
803
+ await new Promise((r) => setTimeout(r, DELAY_NET_MS * (attempt + 1)));
804
+ continue;
805
+ }
806
+ }
807
+ return res;
808
+ }
809
+ return res;
810
+ }
811
+ async function callPlanModel(config, messages) {
812
+ const url = `${config.baseUrl.replace(/\/+$/, "")}/chat/completions`;
813
+ const res = await fetchWithRetry(url, {
814
+ method: "POST",
815
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${config.apiKey}` },
816
+ body: JSON.stringify({
817
+ model: config.model,
818
+ messages,
819
+ tools: TOOL_DEFINITIONS.filter((t) => ["list_files", "read_file", "grep_files", "find_files"].includes(t.function.name)),
820
+ tool_choice: "auto",
821
+ temperature: 0.1
822
+ }),
823
+ signal: AbortSignal.timeout(24e4)
824
+ });
825
+ if (!res.ok) {
826
+ const text = await res.text().catch(() => "");
827
+ throw new Error(`API ${res.status} ${res.statusText}: ${text.slice(0, 300)}`);
828
+ }
829
+ const data = await res.json();
830
+ const choice = data.choices?.[0];
831
+ return {
832
+ content: choice?.message?.content ?? null,
833
+ toolCalls: choice?.message?.tool_calls ?? [],
834
+ stopped: choice?.finish_reason === "stop"
835
+ };
836
+ }
837
+ async function fetchPlanJson(config, messages) {
838
+ const url = `${config.baseUrl.replace(/\/+$/, "")}/chat/completions`;
839
+ const res = await fetchWithRetry(url, {
840
+ method: "POST",
841
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${config.apiKey}` },
842
+ body: JSON.stringify({
843
+ model: config.model,
844
+ messages: [...messages, {
845
+ role: "user",
846
+ content: `Ara\u015Ft\u0131rma \xF6zetini ve okudu\u011Fun t\xFCm dosyalar\u0131 g\xF6z \xF6n\xFCnde bulundurarak \u015Fimdi Implementation Plan'\u0131 yaz.
847
+
848
+ KES\u0130NL\u0130KLE uyulmas\u0131 gereken kurallar:
849
+ 1. Ara\u015Ft\u0131rma s\u0131ras\u0131nda okudu\u011Fun HER dosya i\xE7in gerekli de\u011Fi\u015Fiklikleri ad\u0131mlara ekle \u2014 hi\xE7bir dosyay\u0131 atlama.
850
+ 2. Her dosya de\u011Fi\u015Fikli\u011Fi AYRI bir ad\u0131m olmal\u0131 \u2014 birle\u015Ftirme.
851
+ 3. Import ekleme ayr\u0131 ad\u0131m, metot de\u011Fi\u015Fikli\u011Fi ayr\u0131 ad\u0131m, yeni s\u0131n\u0131f ayr\u0131 ad\u0131m.
852
+ 4. Test dosyas\u0131 mevcutsa onu da ayr\u0131 ad\u0131m olarak ekle.
853
+ 5. \xD6zette belirtti\u011Fin t\xFCm s\u0131n\u0131f/metotlar\u0131 steps i\xE7inde g\xF6ster.
854
+ 6. SADECE ge\xE7erli JSON d\xF6nd\xFCr, ba\u015Fka hi\xE7bir \u015Fey yazma.
855
+ 7. steps dizisi ne kadar uzun olursa olsun hepsini ekle \u2014 k\u0131saltma.`
856
+ }],
857
+ temperature: 0.3
858
+ }),
859
+ signal: AbortSignal.timeout(24e4)
860
+ });
861
+ if (!res.ok) {
862
+ const text = await res.text().catch(() => "");
863
+ throw new Error(`API ${res.status} ${res.statusText}: ${text.slice(0, 300)}`);
864
+ }
865
+ const data = await res.json();
866
+ return data.choices?.[0]?.message?.content ?? "";
867
+ }
868
+ function renderPlanJson(raw, onStep) {
869
+ const cleaned = raw.replace(/^```json\s*/i, "").replace(/^```\s*/i, "").replace(/```\s*$/, "").trim();
870
+ let plan;
871
+ try {
872
+ plan = JSON.parse(cleaned);
873
+ } catch {
874
+ onStep("plan", raw);
875
+ return null;
876
+ }
877
+ const sep = chalk2.dim(" " + "\xB7".repeat(40));
878
+ let display = "";
879
+ display += chalk2.white.bold(plan.title ?? "(ba\u015Fl\u0131ks\u0131z)") + "\n";
880
+ display += chalk2.dim(plan.summary ?? "") + "\n\n";
881
+ const files = Array.isArray(plan.affected_files) ? plan.affected_files : [];
882
+ if (files.length > 0) {
883
+ display += chalk2.dim("Etkilenecek dosyalar:\n");
884
+ display += files.map((f) => " " + formatFilePath(f)).join("\n") + "\n\n";
885
+ }
886
+ const steps = Array.isArray(plan.steps) ? plan.steps : [];
887
+ for (const step of steps) {
888
+ display += sep + "\n";
889
+ display += chalk2.greenBright.bold(`Ad\u0131m ${step.step_number}`) + chalk2.white.bold(` ${step.description}`) + "\n";
890
+ display += chalk2.dim("Dosya: ") + formatFilePath(step.target_file) + "\n";
891
+ display += formatInstructions(step.instructions) + "\n\n";
892
+ }
893
+ display += sep + "\n";
894
+ display += chalk2.dim("Test: ") + chalk2.hex("#98c379")(plan.verification_command) + "\n";
895
+ onStep("plan", display);
896
+ return plan;
897
+ }
898
+ function buildInsight(name, args, result) {
899
+ const shortPath = (p) => p.split(/[/\\]/).slice(-2).join("/");
900
+ if (name === "list_files") {
901
+ const dir = args.directory ?? ".";
902
+ const count = result.output.split("\n").filter(Boolean).length;
903
+ return `Dizin taran\u0131yor: ${dir} (${count} \xF6\u011Fe)`;
904
+ }
905
+ if (name === "grep_files") {
906
+ const pattern = args.pattern ?? "";
907
+ const lines = result.output.split("\n").filter((l) => l.includes(":"));
908
+ if (lines.length === 0) return `"${pattern}" bulunamad\u0131 \u2014 farkl\u0131 terim denenecek`;
909
+ const files = [...new Set(lines.map((l) => shortPath(l.split(":")[0])))];
910
+ return files.length === 1 ? `"${pattern}" bulundu \u2192 ${files[0]} (${lines.length} sat\u0131r)` : `"${pattern}" \u2192 ${lines.length} e\u015Fle\u015Fme: ${files.slice(0, 2).join(", ")}${files.length > 2 ? " +" + (files.length - 2) : ""}`;
911
+ }
912
+ if (name === "find_files") {
913
+ const pattern = args.name_pattern ?? "";
914
+ const found = result.output.split("\n").filter(Boolean);
915
+ if (found.length === 0 || result.output.includes("bulunamad\u0131")) return `"${pattern}" dosyas\u0131 bulunamad\u0131`;
916
+ return `Dosya bulundu: ${found.map((f) => shortPath(f)).slice(0, 3).join(", ")}`;
917
+ }
918
+ if (name === "read_file") {
919
+ const p = args.path ?? "";
920
+ const start = args.start_line;
921
+ const end = args.end_line;
922
+ return start ? `\u0130nceleniyor: ${shortPath(p)} sat\u0131r ${start}\u2013${end ?? "+"}` : `Okunuyor: ${shortPath(p)}`;
923
+ }
924
+ return null;
925
+ }
926
+ async function generateAgentPlan(config, task, boardName, projectRoot, testCommand, onStep, onProgress, onInsight) {
927
+ const lastgenContext = await readLastgenContext(projectRoot);
928
+ const contextNote = lastgenContext ? `
929
+
930
+ ## Proje Ba\u011Flam\u0131 (${LASTGEN_CONTEXT_FILE})
931
+ Bu dosya projeyi \xF6nceden analiz ederek olu\u015Fturulmu\u015Ftur. Ba\u011Flam\u0131 referans al, daha az dosya taramas\u0131 yap:
932
+
933
+ ${lastgenContext}` : "";
934
+ const sourceLabel = task.source === "Jira" ? "Jira Task" : task.source === "Trello" ? "Trello Task" : "Manuel G\xF6rev";
935
+ const descBlock = task.desc?.trim() ? `
936
+ **A\xE7\u0131klama:**
937
+ ${task.desc}
938
+
939
+ \u26A0\uFE0F A\xE7\u0131klamay\u0131 dikkatlice oku \u2014 gereksinimler ve kabul kriterleri burada. Ara\u015Ft\u0131rma yaparken a\xE7\u0131klamada ge\xE7en her bile\u015Fen/\xF6zelli\u011Fi koda ba\u011Fla.` : "\n**A\xE7\u0131klama:** (a\xE7\u0131klama yok)";
940
+ const userMessage = `## ${sourceLabel}
941
+ **Kaynak:** ${task.source}
942
+ **Pano/Proje:** ${boardName}
943
+ **G\xF6rev:** ${task.name}
944
+ **Tip/Liste:** ${task.listName}
945
+ ` + (task.labels.length ? `**Etiketler:** ${task.labels.join(", ")}
946
+ ` : "") + descBlock + `
947
+
948
+ **Proje K\xF6k\xFC:** ${projectRoot}
949
+ **Test Komutu:** ${testCommand}` + contextNote + `
950
+
951
+ \xD6nce a\xE7\u0131klamay\u0131 tam olarak kavra, ard\u0131ndan projeyi ara\xE7larla incele${lastgenContext ? " (LASTGEN.md ba\u011Flam\u0131n\u0131 kullan, gerekli dosyalar\u0131 do\u011Frula)" : ""}, sonra uygulama plan\u0131n\u0131 yaz.`;
952
+ const messages = [
953
+ { role: "system", content: PLAN_SYSTEM_PROMPT },
954
+ { role: "user", content: userMessage }
955
+ ];
956
+ let liveTokens = 0;
957
+ let totalToolCalls = 0;
958
+ const MAX_TOOL_CALLS = 20;
959
+ for (let i = 0; i < 15; i++) {
960
+ const { content, toolCalls, stopped } = await callPlanModel(config, messages);
961
+ liveTokens += Math.ceil([...content ?? ""].length / 4);
962
+ onProgress?.(liveTokens);
963
+ const forceStop = totalToolCalls >= MAX_TOOL_CALLS;
964
+ if (toolCalls.length === 0 || stopped || forceStop) {
965
+ if (forceStop && toolCalls.length > 0) {
966
+ messages.push({ role: "assistant", content: content ?? null, tool_calls: toolCalls });
967
+ for (const tc of toolCalls) {
968
+ messages.push({
969
+ role: "tool",
970
+ tool_call_id: tc.id,
971
+ name: tc.function.name,
972
+ content: "Ara\u015Ft\u0131rma limiti doldu \u2014 plan a\u015Famas\u0131na ge\xE7iliyor."
973
+ });
974
+ }
975
+ } else {
976
+ messages.push({ role: "assistant", content });
977
+ }
978
+ const summaryRes = await callPlanModel(config, [
979
+ ...messages,
980
+ {
981
+ role: "user",
982
+ content: "Ara\u015Ft\u0131rman\u0131 tamamlad\u0131n. A\u015Fa\u011F\u0131daki sorular\u0131 k\u0131saca yan\u0131tla:\n1. Hangi dosyalar\u0131 okudun? (tam yollar\u0131yla liste)\n2. Her dosyada taskla ilgili hangi s\u0131n\u0131f/metot/blok buldun?\n3. Hangi dosyalar de\u011Fi\u015Fmeli ve her birinde tam olarak ne de\u011Fi\u015Fecek?\n4. De\u011Fi\u015Fiklikten etkilenen ba\u011F\u0131ml\u0131 bile\u015Fenler neler? (kim bu kodu \xE7a\u011F\u0131r\u0131yor, frontend etkileniyor mu?)\n5. Mevcut testlerde ne k\u0131r\u0131lacak veya g\xFCncellenmesi gerekecek?\nSadece bu \xF6zeti yaz, hen\xFCz plan yazma."
983
+ }
984
+ ]);
985
+ messages.push({ role: "assistant", content: "Ara\u015Ft\u0131rma \xF6zeti:\n" + (summaryRes.content ?? "") });
986
+ const raw = await fetchPlanJson(config, messages);
987
+ liveTokens += Math.ceil([...raw].length / 4);
988
+ onProgress?.(liveTokens);
989
+ renderPlanJson(raw, onStep);
990
+ return raw;
991
+ }
992
+ messages.push({ role: "assistant", content, tool_calls: toolCalls });
993
+ for (const tc of toolCalls) {
994
+ const name = tc.function.name;
995
+ let args = {};
996
+ try {
997
+ args = JSON.parse(tc.function.arguments);
998
+ } catch {
999
+ }
1000
+ onStep("tool", `${name}(${JSON.stringify(args)})`);
1001
+ const result = await executeTool(name, args, projectRoot);
1002
+ totalToolCalls++;
1003
+ liveTokens += Math.ceil([...result.output].length / 4);
1004
+ onProgress?.(liveTokens);
1005
+ const insight = buildInsight(name, args, result);
1006
+ if (insight) onInsight?.(insight);
1007
+ messages.push({
1008
+ role: "tool",
1009
+ tool_call_id: tc.id,
1010
+ name,
1011
+ content: result.success ? result.output : `HATA: ${result.output}`
1012
+ });
1013
+ }
1014
+ }
1015
+ return "(plan \xFCretilemedi \u2014 maksimum iterasyon)";
1016
+ }
1017
+
1018
+ // src/agent-init.ts
1019
+ var LASTGEN_CONTEXT_FILE = "LASTGEN.md";
1020
+ function lastgenContextPath(projectRoot) {
1021
+ return join3(projectRoot, LASTGEN_CONTEXT_FILE);
1022
+ }
1023
+ async function hasLastgenContext(projectRoot) {
1024
+ try {
1025
+ await access(lastgenContextPath(projectRoot));
1026
+ return true;
1027
+ } catch {
1028
+ return false;
1029
+ }
1030
+ }
1031
+ async function readLastgenContext(projectRoot) {
1032
+ try {
1033
+ return await readFile3(lastgenContextPath(projectRoot), "utf-8");
1034
+ } catch {
1035
+ return null;
1036
+ }
1037
+ }
1038
+ var ANCHOR_FILES = [
1039
+ "package.json",
1040
+ "pom.xml",
1041
+ "build.gradle",
1042
+ "build.gradle.kts",
1043
+ "Cargo.toml",
1044
+ "go.mod",
1045
+ "pyproject.toml",
1046
+ "setup.py",
1047
+ "requirements.txt",
1048
+ "README.md",
1049
+ "README.rst",
1050
+ "tsconfig.json",
1051
+ "vite.config.ts",
1052
+ "webpack.config.js",
1053
+ "application.properties",
1054
+ "application.yml",
1055
+ "application.yaml",
1056
+ "Dockerfile",
1057
+ "docker-compose.yml"
1058
+ ];
1059
+ var INIT_SYSTEM_PROMPT = `Sen bir k\u0131demli yaz\u0131l\u0131m mimar\u0131s\u0131n. Sana verilen proje yap\u0131s\u0131n\u0131 ve kaynak dosyalar\u0131 analiz ederek a\u015Fa\u011F\u0131daki formatta bir "Proje Ba\u011Flam Belgesi" yaz.
1060
+
1061
+ Bu belge, gelecekteki kodlama g\xF6revlerinde projeyi ba\u015Ftan taramak zorunda kalmadan h\u0131zl\u0131ca ba\u011Flam\u0131 anlamak i\xE7in kullan\u0131lacak.
1062
+
1063
+ \xC7IKTI FORMATI (Markdown):
1064
+
1065
+ # Lastgen Proje Ba\u011Flam\u0131
1066
+
1067
+ ## Proje \xD6zeti
1068
+ (Projenin amac\u0131, temel i\u015Flevi \u2014 2-3 c\xFCmle)
1069
+
1070
+ ## Teknoloji Y\u0131\u011F\u0131n\u0131
1071
+ - Backend: (dil, framework, versiyon)
1072
+ - Frontend: (varsa)
1073
+ - Veritaban\u0131: (varsa)
1074
+ - Build ara\xE7lar\u0131: (Maven/Gradle/npm vb.)
1075
+
1076
+ ## Proje Yap\u0131s\u0131
1077
+ (Dizin a\u011Fac\u0131n\u0131n a\xE7\u0131klamas\u0131 \u2014 hangi klas\xF6r ne i\u015Fe yarar)
1078
+
1079
+ ## Kritik Dosyalar ve Sorumluluklar\u0131
1080
+ - \`dosya/yolu\`: ne i\u015Fe yarar
1081
+ (\xD6nemli service, controller, entity, util s\u0131n\u0131flar\u0131)
1082
+
1083
+ ## Paket/Mod\xFCl Yap\u0131s\u0131
1084
+ (Ana Java paketi / Python mod\xFCl\xFC / Node mod\xFCl\xFC vb.)
1085
+
1086
+ ## Test Komutu
1087
+ \`komut\`
1088
+
1089
+ ## Mimari Notlar
1090
+ (Dikkat edilmesi gereken mimari kararlar, design pattern'lar)
1091
+
1092
+ KURALLAR:
1093
+ - Dosyalardan okudu\u011Fun ger\xE7ek bilgileri yaz, tahmin etme
1094
+ - Teknik ve \xF6zl\xFC ol \u2014 gereksiz a\xE7\u0131klama yapma
1095
+ - T\xFCrk\xE7e yaz`;
1096
+ async function generateLastgenContext(config, projectRoot, onProgress, onThinkingStart, onThinkingStop) {
1097
+ onProgress("Proje yap\u0131s\u0131 taran\u0131yor...");
1098
+ const structResult = await listFiles(projectRoot, ".");
1099
+ const structure = structResult.success ? structResult.output : "(yap\u0131 okunamad\u0131)";
1100
+ onProgress("Temel dosyalar okunuyor...");
1101
+ const fileContents = [];
1102
+ for (const anchor of ANCHOR_FILES) {
1103
+ const fullPath = join3(projectRoot, anchor);
1104
+ if (!existsSync3(fullPath)) continue;
1105
+ const result = await readFile_(projectRoot, anchor);
1106
+ if (result.success && result.output.length < 8e3) {
1107
+ fileContents.push(`### ${anchor}
1108
+ \`\`\`
1109
+ ${result.output.slice(0, 4e3)}
1110
+ \`\`\``);
1111
+ onProgress(`Okundu: ${anchor}`);
1112
+ }
1113
+ }
1114
+ onProgress("Ba\u011Flam belgesi \xFCretiliyor...");
1115
+ onThinkingStart?.();
1116
+ const userMessage = `Proje k\xF6k\xFC: ${resolve2(projectRoot)}
1117
+
1118
+ ## Dizin Yap\u0131s\u0131
1119
+ \`\`\`
1120
+ ${structure}
1121
+ \`\`\`
1122
+
1123
+ ` + (fileContents.length > 0 ? `## Temel Dosya \u0130\xE7erikleri
1124
+
1125
+ ${fileContents.join("\n\n")}` : "(anchor dosya bulunamad\u0131)");
1126
+ const url = `${config.baseUrl.replace(/\/+$/, "")}/chat/completions`;
1127
+ const res = await fetchWithRetry(url, {
1128
+ method: "POST",
1129
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${config.apiKey}` },
1130
+ body: JSON.stringify({
1131
+ model: config.model,
1132
+ messages: [
1133
+ { role: "system", content: INIT_SYSTEM_PROMPT },
1134
+ { role: "user", content: userMessage }
1135
+ ],
1136
+ temperature: 0.2
1137
+ }),
1138
+ signal: AbortSignal.timeout(12e4)
1139
+ });
1140
+ if (!res.ok) {
1141
+ onThinkingStop?.(0);
1142
+ const text = await res.text().catch(() => "");
1143
+ throw new Error(`API ${res.status}: ${text.slice(0, 200)}`);
1144
+ }
1145
+ const data = await res.json();
1146
+ const content = data.choices?.[0]?.message?.content ?? "";
1147
+ const totalTokens = data.usage?.total_tokens ?? Math.ceil(([...userMessage].length + [...content].length) / 4);
1148
+ onThinkingStop?.(totalTokens);
1149
+ const filePath = lastgenContextPath(projectRoot);
1150
+ await writeFile3(filePath, content, "utf-8");
1151
+ return content;
1152
+ }
1153
+
1154
+ export {
1155
+ TOOL_DEFINITIONS,
1156
+ executeTool,
1157
+ formatFilePath,
1158
+ formatToolCall,
1159
+ formatInstructions,
1160
+ saveExecuteRecord,
1161
+ savePlan,
1162
+ listPlans,
1163
+ generateAgentPlan,
1164
+ LASTGEN_CONTEXT_FILE,
1165
+ lastgenContextPath,
1166
+ hasLastgenContext,
1167
+ readLastgenContext,
1168
+ generateLastgenContext
1169
+ };