@takimcizgisi/ajan 0.5.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,130 @@
1
+ import { mkdirSync, writeFileSync, readFileSync, existsSync, readdirSync, rmSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import type { AgentTool } from "../config/types.js";
4
+ import { limitOutput } from "./utils.js";
5
+
6
+ /** Kullanıcının açık klasöründe, projenin içindeki AJAN test klasörü: <cwd>/.ajan */
7
+ function playgroundPath(cwd: string): string {
8
+ const dir = join(cwd, ".ajan");
9
+ mkdirSync(dir, { recursive: true });
10
+ return dir;
11
+ }
12
+
13
+ function todosPath(cwd: string): string {
14
+ return join(playgroundPath(cwd), "todos.json");
15
+ }
16
+
17
+ type TodoItem = { text: string; done: boolean };
18
+
19
+ function loadTodos(cwd: string): TodoItem[] {
20
+ try {
21
+ if (existsSync(todosPath(cwd))) {
22
+ const parsed = JSON.parse(readFileSync(todosPath(cwd), "utf8"));
23
+ if (Array.isArray(parsed)) return parsed as TodoItem[];
24
+ }
25
+ } catch {
26
+ /* bozuk dosya: sıfırla */
27
+ }
28
+ return [];
29
+ }
30
+
31
+ function clearDir(dir: string): number {
32
+ let count = 0;
33
+ for (const name of readdirSync(dir)) {
34
+ rmSync(join(dir, name), { recursive: true, force: true });
35
+ count++;
36
+ }
37
+ return count;
38
+ }
39
+
40
+ export const openPlaygroundTool: AgentTool = {
41
+ name: "open_playground",
42
+ description:
43
+ "Açık klasörün içindeki AJAN TEST KLASÖRÜNÜ (<cwd>/.ajan) hazırlar. " +
44
+ "Deneme/test/prototip yapmak istediğinde önce bunu çağır. " +
45
+ "reset=false iken masaarkadaşları korur; reset=true ise klasörü bomboş hale getirir. " +
46
+ "Kullanıcının gerçek proje dosyalarına kesinlikle dokunmaz; tüm denemeler bu klasörde yapılır.",
47
+ parameters: {
48
+ type: "object",
49
+ properties: {
50
+ reset: {
51
+ type: "boolean",
52
+ description: "true ise .ajan içeriğini komple temizler (varsayılan: false)"
53
+ },
54
+ note: { type: "string", description: "Ne yapacağın hakkında kısa bir not (opsiyonel)" }
55
+ },
56
+ required: []
57
+ },
58
+ handler: async (params: { reset?: boolean; note?: string }, ctx) => {
59
+ const dir = playgroundPath(ctx.cwd);
60
+ const before = readdirSync(dir).length;
61
+ let cleared = 0;
62
+ if (params.reset) cleared = clearDir(dir);
63
+ const after = readdirSync(dir).length;
64
+ const lines = [
65
+ `.ajan test klasörü hazır: ${dir}`,
66
+ `Önceki içerik: ${before} öğe; sonraki içerik: ${after} öğe${cleared > 0 ? ` (${cleared} temizlendi)` : ""}`
67
+ ];
68
+ if (params.note) lines.push(`Not: ${params.note}`);
69
+ lines.push("KURAL: tüm denemeleri, geçici dosyaları ve riskli testleri BURADA yap; asıl projeye dokunma.");
70
+ return { ok: true, output: lines.join("\n") };
71
+ }
72
+ };
73
+
74
+ export const setTodosTool: AgentTool = {
75
+ name: "set_todos",
76
+ description:
77
+ "Görev programını (.ajan/todos.json) günceller. " +
78
+ "Bir göreve başlarken önce kısa bir TODO listesi planla ve bu araçla kaydet. " +
79
+ "done=false adım halen beklemede, done=true tamamlandı demektir.",
80
+ parameters: {
81
+ type: "object",
82
+ properties: {
83
+ todos: {
84
+ type: "array",
85
+ description: "Yapılacaklar listesi",
86
+ items: {
87
+ type: "object",
88
+ properties: {
89
+ text: { type: "string", description: "Adım açıklaması" },
90
+ done: { type: "boolean", description: "Tamamlandı mı (varsayılan false)" }
91
+ },
92
+ required: ["text"]
93
+ }
94
+ }
95
+ },
96
+ required: ["todos"]
97
+ },
98
+ handler: async (params: { todos?: TodoItem[] }, ctx) => {
99
+ const raw = params.todos;
100
+ if (!raw || raw.length === 0) return { ok: false, output: "todos listesi boş olamaz." };
101
+ const items: TodoItem[] = raw.map((t) => ({
102
+ text: String(t.text ?? "").trim(),
103
+ done: t.done === true
104
+ }));
105
+ if (items.some((t) => !t.text)) return { ok: false, output: "Her TODO maddesi için text gerekli." };
106
+ writeFileSync(todosPath(ctx.cwd), JSON.stringify(items, null, 2), "utf8");
107
+ const lines = items.map((t, i) => `${t.done ? "[x]" : "[ ]"} ${i + 1}. ${t.text}`);
108
+ return {
109
+ ok: true,
110
+ output: `TODO listesi kaydedildi (${items.length} madde): ${todosPath(ctx.cwd)}\n${lines.join("\n")}`
111
+ };
112
+ }
113
+ };
114
+
115
+ export const getTodosTool: AgentTool = {
116
+ name: "get_todos",
117
+ description: "Görev programını (.ajan/todos.json) okur. Kayıtlı TODO varsa döndürür, yoksa boş döner.",
118
+ parameters: {
119
+ type: "object",
120
+ properties: {}
121
+ },
122
+ handler: async (_params: unknown, ctx) => {
123
+ const items = loadTodos(ctx.cwd);
124
+ if (items.length === 0) {
125
+ return { ok: true, output: "Kayıtlı TODO yok. Göreve başlarken set_todos ile plan oluştur." };
126
+ }
127
+ const lines = items.map((t, i) => `${t.done ? "[x]" : "[ ]"} ${i + 1}. ${t.text}`);
128
+ return { ok: true, output: limitOutput(`Görev programı (${items.length} madde):\n${lines.join("\n")}`, ctx.config.maxToolOutput) };
129
+ }
130
+ };
@@ -0,0 +1,79 @@
1
+ import type { ChatSessionModelFunction, ChatSessionModelFunctions } from "node-llama-cpp";
2
+ import type { AgentTool, ToolExecutionContext, ToolExecutionSignal } from "../config/types.js";
3
+ import { readFileTool, writeFileTool, listDirTool, searchFilesTool, grepTool } from "./file.js";
4
+ import { runCommandTool } from "./terminal.js";
5
+ import { applyPatchTool } from "./patch.js";
6
+ import { editFileTool } from "./edit.js";
7
+ import { webSearchTool, fetchUrlTool } from "./web.js";
8
+ import { moveFileTool, deleteFileTool } from "./filesys.js";
9
+ import { readDataTool, unzipFileTool } from "./data.js";
10
+ import { noteAddTool, noteReadTool } from "./memory.js";
11
+ import { taskCompleteTool } from "./taskComplete.js";
12
+ import { openPlaygroundTool, setTodosTool, getTodosTool } from "./playground.js";
13
+ import { logger } from "../utils/paths.js";
14
+
15
+ export type ToolEvents = {
16
+ onToolCall?: (name: string, params: unknown) => void;
17
+ onToolResult?: (name: string, ok: boolean, output: string) => void;
18
+ };
19
+
20
+ export function createCoreTools(ctx: ToolExecutionContext): AgentTool[] {
21
+ return [
22
+ readFileTool,
23
+ writeFileTool,
24
+ listDirTool,
25
+ searchFilesTool,
26
+ grepTool,
27
+ editFileTool,
28
+ moveFileTool,
29
+ deleteFileTool,
30
+ readDataTool,
31
+ unzipFileTool,
32
+ runCommandTool,
33
+ applyPatchTool,
34
+ webSearchTool,
35
+ fetchUrlTool,
36
+ noteAddTool,
37
+ noteReadTool,
38
+ taskCompleteTool,
39
+ openPlaygroundTool,
40
+ setTodosTool,
41
+ getTodosTool
42
+ ];
43
+ }
44
+
45
+ export function buildSessionFunctions(
46
+ tools: AgentTool[],
47
+ ctx: ToolExecutionContext,
48
+ events: ToolEvents = {}
49
+ ): ChatSessionModelFunctions {
50
+ const result: Record<string, ChatSessionModelFunction<any>> = {};
51
+ for (const tool of tools) {
52
+ result[tool.name] = {
53
+ description: tool.description,
54
+ params: tool.parameters as never,
55
+ handler: async (params: any) => {
56
+ events.onToolCall?.(tool.name, params);
57
+ logger.debug(`Arac cagirildi: ${tool.name}`);
58
+ try {
59
+ const res = await tool.handler(params, ctx);
60
+ events.onToolResult?.(tool.name, res.ok, res.output);
61
+ if (res.ok) return res.output;
62
+ return (
63
+ `HATA (${tool.name}): ${res.output} | ` +
64
+ "NOT: Araç başarısız oldu; bu işlem GERÇEKLEŞMEDİ. Hatayı kullanıcıya bildir veya sorunu çözüp tekrar dene. Başarılı diye raporlama."
65
+ );
66
+ } catch (err) {
67
+ const msg = (err as Error).message;
68
+ const out = `HATA (${tool.name}): ${msg} | NOT: Araç hata verdi; işlem gerçekleşmedi. Başarılı diye raporlama.`;
69
+ logger.error(`Araç hatasi: ${tool.name}: ${msg}`);
70
+ events.onToolResult?.(tool.name, false, msg);
71
+ return out;
72
+ }
73
+ }
74
+ };
75
+ }
76
+ return result as ChatSessionModelFunctions;
77
+ }
78
+
79
+ export type { ToolExecutionSignal };
@@ -0,0 +1,34 @@
1
+ import type { AgentTool } from "../config/types.js";
2
+
3
+ export const taskCompleteTool: AgentTool = {
4
+ name: "task_complete",
5
+ description:
6
+ "Görev akışını KENDİ kararınla kontrol eder. " +
7
+ "İşin henüz bitmediyse, kalan adımlara devam etmek için continue:true ile çağır (yeni bir tur açılır). " +
8
+ "Görev TAMAMLANDIĞINDA ve doğrulandığında continue:false (veya continue vermeden) ile çağır; " +
9
+ "report parametresine yapılan işlerin özetini yaz.",
10
+ parameters: {
11
+ type: "object",
12
+ properties: {
13
+ report: { type: "string", description: "İşin durumu / tamamlanan işlerin kısa özeti" },
14
+ continue: {
15
+ type: "boolean",
16
+ description:
17
+ "true: göreve devam et, yeni bir tur iste (tur sonunda model tekrar görüşmelere devam eder). " +
18
+ "false veya verilmezse: görevi bitir."
19
+ }
20
+ },
21
+ required: []
22
+ },
23
+ handler: async (params: { report?: string; continue?: boolean }, ctx) => {
24
+ if (ctx.signals) {
25
+ if (params.continue === true) {
26
+ ctx.signals.continueRequested = true;
27
+ return { ok: true, output: "Devam turu istendi (continue:true). Model kalan adımına devam edecek." };
28
+ }
29
+ ctx.signals.taskCompleted = true;
30
+ }
31
+ const report = (params.report ?? "").trim();
32
+ return { ok: true, output: report || "Görev tamamlandı." };
33
+ }
34
+ };
@@ -0,0 +1,138 @@
1
+ import { spawn } from "node:child_process";
2
+ import type { AgentTool } from "../config/types.js";
3
+ import { limitOutput, resolveUserPath } from "./utils.js";
4
+
5
+ const DEFAULT_BLOCKED_COMMANDS: string[] = [
6
+ "^(\\s*)(format|fdisk|diskpart|mkfs|shutdown)\\b",
7
+ "^\\s*(rm|del|erase|rd|rmdir|reg)\\s+.*(-rf|-r\\s+-f|/s|/q|delete)\\b",
8
+ "^\\s*taskkill\\s+/f\\s+/im\\s+system\\b",
9
+ "rm\\s+(-rf|-r\\s+-f)\\s+[/\\\\]"
10
+ ];
11
+
12
+ function blockedBy(patterns: string[], command: string): string | null {
13
+ for (const pat of patterns) {
14
+ try {
15
+ if (new RegExp(pat, "i").test(command)) return pat;
16
+ } catch {
17
+ /* geçersiz deseni yut */
18
+ }
19
+ }
20
+ return null;
21
+ }
22
+
23
+ export const runCommandTool: AgentTool = {
24
+ name: "run_command",
25
+ description:
26
+ "Terminal komutu çalıştırır. cwd, komutun çalıştırılacağı dizindir (varsayılan: ajanın çalışma dizini). " +
27
+ "timeout ms cinsinden zaman aşımıdır (varsayılan: 60000). Çıktı maksimum uzunlukta kesilir.",
28
+ parameters: {
29
+ type: "object",
30
+ properties: {
31
+ command: { type: "string", description: "Çalıştırılacak terminal komutu" },
32
+ cwd: { type: "string", description: "Çalışma dizini (ajanın dizinine göre göreli veya mutlak yol)" },
33
+ timeout: { type: "integer", description: "Zaman aşımı milisaniye" }
34
+ },
35
+ required: ["command"]
36
+ },
37
+ handler: async (
38
+ params: { command: string; cwd?: string; timeout?: number },
39
+ ctx
40
+ ) => {
41
+ const timeoutMs = params.timeout ?? ctx.config.toolTimeout ?? 60_000;
42
+ const cwd = params.cwd ? resolveUserPath(ctx.cwd, params.cwd) : ctx.cwd;
43
+
44
+ const blocked = blockedBy(ctx.config.blockedCommands ?? DEFAULT_BLOCKED_COMMANDS, params.command);
45
+ if (blocked) {
46
+ return {
47
+ ok: false,
48
+ output:
49
+ `Komut engellendi (tehlikeli komut deseni: ${blocked}).\n` +
50
+ `Engellenen komut: ${params.command}\n` +
51
+ 'İzin vermek için config.json\'daki "blockedCommands" alanını değiştirin ([] boş liste = tümü serbest).'
52
+ };
53
+ }
54
+
55
+ const result = await new Promise<{ code: number | null; stdout: string; stderr: string; timedOut: boolean }>(
56
+ (resolvePromise) => {
57
+ const isWin = process.platform === "win32";
58
+ const shell = isWin ? "cmd.exe" : "/bin/sh";
59
+ const shellArgs = isWin ? ["/d", "/s", "/c"] : ["-c"];
60
+ const child = spawn(shell, [...shellArgs, params.command], {
61
+ cwd,
62
+ windowsHide: true,
63
+ detached: !isWin,
64
+ env: process.env,
65
+ stdio: ["ignore", "pipe", "pipe"]
66
+ });
67
+
68
+ let stdout = "";
69
+ let stderr = "";
70
+ let timedOut = false;
71
+ let settled = false;
72
+ const finish = (code: number | null): void => {
73
+ if (settled) return;
74
+ settled = true;
75
+ clearTimeout(timer);
76
+ clearTimeout(safetyTimer);
77
+ resolvePromise({ code, stdout, stderr, timedOut });
78
+ };
79
+
80
+ const timer = setTimeout(() => {
81
+ timedOut = true;
82
+ try {
83
+ if (isWin && child.pid != null) {
84
+ // Önce taskkill /T ile SÜREÇ AĞACINI (cmd + alt komutlar) öldür.
85
+ // Önce ağaçta çocuk öldürülür, ardından taskkill "process not found"
86
+ // ile ağacı kaçırır; bu yüzden önce taskkill, TerminateProcess'ten önce gelir.
87
+ const killer = spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
88
+ windowsHide: true,
89
+ stdio: "ignore"
90
+ });
91
+ killer.unref();
92
+ killer.on("error", () => {
93
+ /* taskkill başlatılamadıysa sessiz geç */
94
+ });
95
+ } else if (child.pid != null) {
96
+ process.kill(-child.pid, "SIGKILL");
97
+ }
98
+ } catch {
99
+ /* zaten ölmüş olabilir */
100
+ }
101
+ }, timeoutMs);
102
+
103
+ // 'close' ancak süreç bitip tüm pipe'lar kapanınca tetiklenir; askıda kalmamak için
104
+ // güvenlik zamanlayıcısı pipe'ları serbest bırakıp sonucu zorla döndürür.
105
+ const safetyTimer = setTimeout(() => {
106
+ if (!settled) {
107
+ child.stdout?.destroy();
108
+ child.stderr?.destroy();
109
+ finish(child.exitCode ?? null);
110
+ }
111
+ }, timeoutMs + 5_000);
112
+
113
+ child.stdout?.on("data", (chunk: Buffer) => {
114
+ stdout += chunk.toString("utf8");
115
+ if (stdout.length > 2_000_000) child.stdout?.pause();
116
+ });
117
+ child.stderr?.on("data", (chunk: Buffer) => {
118
+ stderr += chunk.toString("utf8");
119
+ if (stderr.length > 2_000_000) child.stderr?.pause();
120
+ });
121
+ child.on("error", (err) => {
122
+ stderr += `Komut başlatılamadı: ${err.message}`;
123
+ finish(null);
124
+ });
125
+ child.on("close", (code) => finish(code));
126
+ }
127
+ );
128
+
129
+ const parts: string[] = [];
130
+ if (result.timedOut) parts.push(`[ZAMAN AŞIMI ${timeoutMs}ms]`);
131
+ parts.push(`Çıkış kodu: ${result.code}`);
132
+ if (result.stdout.trim()) parts.push(`--- STDOUT ---\n${result.stdout.trimEnd()}`);
133
+ if (result.stderr.trim()) parts.push(`--- STDERR ---\n${result.stderr.trimEnd()}`);
134
+
135
+ const ok = result.code === 0 && !result.timedOut;
136
+ return { ok, output: limitOutput(parts.join("\n"), ctx.config.maxToolOutput) };
137
+ }
138
+ };
@@ -0,0 +1,63 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import { existsSync } from "node:fs";
4
+
5
+ export function limitOutput(text: string, maxChars?: number): string {
6
+ const limit = maxChars ?? 40_000;
7
+ if (text.length <= limit) return text;
8
+ const truncated = text.slice(0, limit);
9
+ return `${truncated}\n... [çıktı ${text.length - limit} karakter kesildi]`;
10
+ }
11
+
12
+ export const SKIP_DIRS = new Set<string>([
13
+ "node_modules", ".git", "dist", "build", ".next", ".cache",
14
+ "__pycache__", ".venv", "venv", ".idea", ".vscode", "coverage"
15
+ ]);
16
+
17
+ /** Ağır veya gizli klasörlerde gezinmeyi atla (node_modules, dist, .git vb.) */
18
+ export function shouldSkipDir(name: string): boolean {
19
+ return name.startsWith(".") || SKIP_DIRS.has(name);
20
+ }
21
+
22
+ export function toMb(bytes: number): string {
23
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
24
+ }
25
+
26
+ function homeFolder(key: string): string | null {
27
+ const candidates: string[] = [];
28
+ const home = os.homedir();
29
+ candidates.push(path.join(home, key));
30
+ const userProfile = process.env.USERPROFILE ?? process.env.HOME;
31
+ if (userProfile) candidates.push(path.join(userProfile, key));
32
+ const oneDrive = process.env.ONEDRIVE;
33
+ if (oneDrive) candidates.push(path.join(oneDrive, key));
34
+ for (const c of candidates) {
35
+ try {
36
+ if (existsSync(c)) return c;
37
+ } catch {
38
+ /* yut */
39
+ }
40
+ }
41
+ return candidates[0] ?? null;
42
+ }
43
+
44
+ const FOLDER_PATTERNS: Array<{ regex: RegExp; key: "Desktop" | "Documents" | "Downloads" }> = [
45
+ { regex: /masaüstü|masaustu|desktop/i, key: "Desktop" },
46
+ { regex: /belgeler|belgelerim|belge|documents/i, key: "Documents" },
47
+ { regex: /indirilenler|indirilen|indirmeler|downloads/i, key: "Downloads" }
48
+ ];
49
+
50
+ /** 'masaüstü', 'belgeler', 'indirilenler' gibi adları kullanıcının gerçek klasörlerine çözer */
51
+ export function resolveUserPath(cwd: string, input: string): string {
52
+ const trimmed = input.trim();
53
+ for (const { regex, key } of FOLDER_PATTERNS) {
54
+ const match = trimmed.match(new RegExp(`^(${regex.source})(?:[\\\\/](.*))?$`, "i"));
55
+ if (!match) continue;
56
+ const base = homeFolder(key);
57
+ if (!base) continue;
58
+ const rest = match[2];
59
+ return rest ? path.resolve(base, rest) : base;
60
+ }
61
+ return path.isAbsolute(input) ? path.resolve(input) : path.resolve(cwd, input);
62
+ }
63
+
@@ -0,0 +1,204 @@
1
+ import type { AgentTool } from "../config/types.js";
2
+ import { limitOutput } from "./utils.js";
3
+
4
+ export type WebSearchResult = {
5
+ title: string;
6
+ url: string;
7
+ snippet: string;
8
+ };
9
+
10
+ function decodeEntities(text: string): string {
11
+ return text
12
+ .replace(/&amp;/g, "&")
13
+ .replace(/&lt;/g, "<")
14
+ .replace(/&gt;/g, ">")
15
+ .replace(/&quot;/g, '"')
16
+ .replace(/&#39;/g, "'")
17
+ .replace(/&nbsp;/g, " ")
18
+ .replace(/&#(\d+);/g, (_, code: string) => {
19
+ const cp = Number(code);
20
+ return cp > 0 ? String.fromCodePoint(cp) : "";
21
+ });
22
+ }
23
+
24
+ function stripTags(text: string): string {
25
+ return decodeEntities(text.replace(/<[^>]*>/g, "").replace(/\s+/g, " ").trim());
26
+ }
27
+
28
+ function decodeRedirectUrl(href: string): string {
29
+ try {
30
+ if (href.startsWith("//")) href = `https:${href}`;
31
+ const u = new URL(href);
32
+ if (u.hostname.includes("duckduckgo.com")) {
33
+ const target = u.searchParams.get("uddg");
34
+ if (target) return decodeURIComponent(target);
35
+ }
36
+ return href;
37
+ } catch {
38
+ return href;
39
+ }
40
+ }
41
+
42
+ function parseResults(html: string, maxResults: number): WebSearchResult[] {
43
+ const results: WebSearchResult[] = [];
44
+ const resultRe =
45
+ /<a[^>]*class="(?:result__a|result-link)"[^>]*href="([^"]*)"[^>]*>(.*?)<\/a>/gi;
46
+ const snippetRe =
47
+ /<a[^>]*class="(?:result__snippet|result-snippet)"[^>]*>(.*?)<\/a>/gi;
48
+
49
+ let match: RegExpExecArray | null;
50
+ const titles: { href: string; title: string }[] = [];
51
+ while ((match = resultRe.exec(html)) !== null && titles.length < maxResults) {
52
+ titles.push({ href: match[1] ?? "", title: stripTags(match[2] ?? "") });
53
+ }
54
+
55
+ if (titles.length === 0) {
56
+ const genericRe = /<a[^>]*href="(https?:\/\/[^"]+)"[^>]*>(.*?)<\/a>/gi;
57
+ while ((match = genericRe.exec(html)) !== null && titles.length < maxResults) {
58
+ const title = stripTags(match[2] ?? "");
59
+ if (!title) continue;
60
+ titles.push({ href: match[1] ?? "", title });
61
+ }
62
+ }
63
+
64
+ const snippets: string[] = [];
65
+ while ((match = snippetRe.exec(html)) !== null && snippets.length < titles.length) {
66
+ snippets.push(stripTags(match[1] ?? ""));
67
+ }
68
+
69
+ for (let i = 0; i < titles.length; i++) {
70
+ const item = titles[i];
71
+ if (!item || !item.title) continue;
72
+ results.push({
73
+ title: item.title,
74
+ url: decodeRedirectUrl(item.href),
75
+ snippet: snippets[i] ?? ""
76
+ });
77
+ }
78
+ return results;
79
+ }
80
+
81
+ export const webSearchTool: AgentTool = {
82
+ name: "web_search",
83
+ description:
84
+ "Web'de arama yapar (DuckDuckGo, API anahtarı gerektirmez). Güncel bilgi, haber veya doküman bulmak için kullan. " +
85
+ "maxResults (varsayılan 5, en fazla 10) kadar sonuç döner: başlık, özet ve URL.",
86
+ parameters: {
87
+ type: "object",
88
+ properties: {
89
+ query: { type: "string", description: "Arama sorgusu (İngilizce daha iyi sonuç verir)" },
90
+ maxResults: { type: "integer", description: "Döndürülecek sonuç sayısı (varsayılan: 5, maks: 10)" }
91
+ },
92
+ required: ["query"]
93
+ },
94
+ handler: async (params: { query: string; maxResults?: number }, ctx) => {
95
+ const query = params.query.trim();
96
+ if (!query) return { ok: false, output: "Arama sorgusu boş olamaz." };
97
+ const maxResults = Math.min(Math.max(params.maxResults ?? 5, 1), 10);
98
+ const timeoutMs = ctx.config.toolTimeout ?? 15_000;
99
+
100
+ const controller = new AbortController();
101
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
102
+ let html: string;
103
+ try {
104
+ const res = await fetch(
105
+ `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`,
106
+ {
107
+ headers: {
108
+ "User-Agent":
109
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36"
110
+ },
111
+ signal: controller.signal,
112
+ redirect: "follow"
113
+ }
114
+ );
115
+ if (!res.ok) {
116
+ return { ok: false, output: `Arama başarısız (HTTP ${res.status}): ${res.statusText}` };
117
+ }
118
+ html = await res.text();
119
+ } catch (err) {
120
+ const aborted = controller.signal.aborted;
121
+ return {
122
+ ok: false,
123
+ output: aborted
124
+ ? `Arama zaman aşımı (${timeoutMs}ms) veya iptal edildi.`
125
+ : `Arama hatası: ${(err as Error).message}`
126
+ };
127
+ } finally {
128
+ clearTimeout(timer);
129
+ }
130
+
131
+ const results = parseResults(html, maxResults);
132
+ if (results.length === 0) {
133
+ return { ok: true, output: `"${query}" için sonuç bulunamadı.` };
134
+ }
135
+
136
+ const lines = results.map((r, i) =>
137
+ `${i + 1}. ${r.title}\n ${r.url}\n ${r.snippet || "(özet yok)"}`
138
+ );
139
+ return { ok: true, output: limitOutput(lines.join("\n\n"), ctx.config.maxToolOutput) };
140
+ }
141
+ };
142
+
143
+ function htmlToText(html: string): string {
144
+ return stripTags(
145
+ html
146
+ .replace(/<script[\s\S]*?<\/script>/gi, " ")
147
+ .replace(/<style[\s\S]*?<\/style>/gi, " ")
148
+ .replace(/<(br|p|div|li|h[1-6]|tr)[^>]*>/gi, "\n")
149
+ );
150
+ }
151
+
152
+ export const fetchUrlTool: AgentTool = {
153
+ name: "fetch_url",
154
+ description:
155
+ "Bir URL'nin içeriğini indirip metin olarak döner. HTML sayfalar düz metne çevrilir. " +
156
+ "web_search ile bulunan bir sayfanın detayını okumak için kullan.",
157
+ parameters: {
158
+ type: "object",
159
+ properties: {
160
+ url: { type: "string", description: "İndirilecek adres (http/https)" },
161
+ maxLength: { type: "integer", description: "Döndürülecek maksimum karakter (varsayılan 12000, maks 40000)" }
162
+ },
163
+ required: ["url"]
164
+ },
165
+ handler: async (params: { url?: string; maxLength?: number }, ctx) => {
166
+ const url = params.url?.trim();
167
+ if (!url || !/^https?:\/\//i.test(url)) {
168
+ return { ok: false, output: "Geçerli bir http(s) URL gerekli." };
169
+ }
170
+ const timeoutMs = ctx.config.toolTimeout ?? 20_000;
171
+ const controller = new AbortController();
172
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
173
+ try {
174
+ const res = await fetch(url, {
175
+ headers: {
176
+ "User-Agent":
177
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36"
178
+ },
179
+ signal: controller.signal,
180
+ redirect: "follow"
181
+ });
182
+ if (!res.ok) {
183
+ return { ok: false, output: `İndirme başarısız (HTTP ${res.status}): ${res.statusText}` };
184
+ }
185
+ const contentType = res.headers.get("content-type") ?? "";
186
+ const body = await res.text();
187
+ const isHtml = contentType.includes("text/html") || /^\s*<(!doctype|html)/i.test(body);
188
+ const text = isHtml ? htmlToText(body) : body;
189
+ const maxLen = Math.min(Math.max(params.maxLength ?? 12_000, 500), 40_000);
190
+ const header = `URL: ${url}\nTür: ${contentType || "bilinmiyor"}\nUzunluk: ${text.length} karakter\n\n`;
191
+ return { ok: true, output: limitOutput(header + text, maxLen + header.length + 100) };
192
+ } catch (err) {
193
+ const aborted = controller.signal.aborted;
194
+ return {
195
+ ok: false,
196
+ output: aborted
197
+ ? `Zaman aşımı (${timeoutMs}ms) veya iptal edildi.`
198
+ : `İndirme hatası: ${(err as Error).message}`
199
+ };
200
+ } finally {
201
+ clearTimeout(timer);
202
+ }
203
+ }
204
+ };