@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.
- package/LICENSE +227 -0
- package/README.md +324 -0
- package/config/models.json +50 -0
- package/out/ajan-engine.js +8306 -0
- package/package.json +84 -0
- package/src/client/client.ts +289 -0
- package/src/config/configManager.ts +300 -0
- package/src/config/types.ts +139 -0
- package/src/core/agent.ts +379 -0
- package/src/daemon/gpu.ts +54 -0
- package/src/daemon/main.ts +87 -0
- package/src/daemon/runner.ts +261 -0
- package/src/daemon/server.ts +231 -0
- package/src/engine/llama.ts +329 -0
- package/src/engine/modelManager.ts +84 -0
- package/src/index.ts +63 -0
- package/src/protocol/channel.ts +151 -0
- package/src/protocol/messages.ts +138 -0
- package/src/service.ts +410 -0
- package/src/tools/data.ts +101 -0
- package/src/tools/edit.ts +54 -0
- package/src/tools/file.ts +261 -0
- package/src/tools/filesys.ts +70 -0
- package/src/tools/memory.ts +76 -0
- package/src/tools/patch.ts +138 -0
- package/src/tools/playground.ts +130 -0
- package/src/tools/registry.ts +79 -0
- package/src/tools/taskComplete.ts +34 -0
- package/src/tools/terminal.ts +138 -0
- package/src/tools/utils.ts +63 -0
- package/src/tools/web.ts +204 -0
- package/src/utils/paths.ts +80 -0
- package/src/utils/projectContext.ts +23 -0
- package/src/utils/sessions.ts +91 -0
- package/src/version.ts +4 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
2
|
+
import type { AgentTool } from "../config/types.js";
|
|
3
|
+
import { resolveUserPath } from "./utils.js";
|
|
4
|
+
|
|
5
|
+
function normalizeLines(text: string): string {
|
|
6
|
+
return text.replace(/\r\n/g, "\n");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const editFileTool: AgentTool = {
|
|
10
|
+
name: "edit_file",
|
|
11
|
+
description:
|
|
12
|
+
"Bir dosyada tam eşleşen eski metni yeni metinle değiştirir (hedefli düzenleme). " +
|
|
13
|
+
"Eski metin dosyada yalnızca BİR kez geçmelidir; birden fazla geçiyorsa replaceAll=true kullanın. " +
|
|
14
|
+
"Küçük ve hedefli değişikliklerde write_file'a göre daha güvenilirdir.",
|
|
15
|
+
parameters: {
|
|
16
|
+
type: "object",
|
|
17
|
+
properties: {
|
|
18
|
+
path: { type: "string", description: "Düzenlenecek dosyanın yolu" },
|
|
19
|
+
oldString: { type: "string", description: "Değiştirilecek mevcut metin (birebir eşleşme)" },
|
|
20
|
+
newString: { type: "string", description: "Eski metnin yerine geçecek yeni metin" },
|
|
21
|
+
replaceAll: { type: "boolean", description: "true ise tüm eşleşmeleri değiştirir (varsayılan: false)" },
|
|
22
|
+
cwd: { type: "string", description: "Göreli yolların çözüleceği dizin" }
|
|
23
|
+
},
|
|
24
|
+
required: ["path", "oldString", "newString"]
|
|
25
|
+
},
|
|
26
|
+
handler: async (params: { path: string; oldString: string; newString: string; replaceAll?: boolean; cwd?: string }, ctx) => {
|
|
27
|
+
const filePath = resolveUserPath(params.cwd ?? ctx.cwd, params.path);
|
|
28
|
+
if (!existsSync(filePath)) {
|
|
29
|
+
return { ok: false, output: `Dosya bulunamadı: ${params.path}` };
|
|
30
|
+
}
|
|
31
|
+
const original = normalizeLines(readFileSync(filePath, "utf8"));
|
|
32
|
+
const oldText = normalizeLines(params.oldString);
|
|
33
|
+
const newText = normalizeLines(params.newString);
|
|
34
|
+
|
|
35
|
+
if (oldText.length === 0) {
|
|
36
|
+
return { ok: false, output: "oldString boş olamaz." };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const count = original.split(oldText).length - 1;
|
|
40
|
+
if (count === 0) {
|
|
41
|
+
return { ok: false, output: `Eşleşme bulunamadı: "${params.oldString.slice(0, 120)}". Dosya içeriği değişmiş olabilir; read_file ile kontrol edin.` };
|
|
42
|
+
}
|
|
43
|
+
if (count > 1 && !params.replaceAll) {
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
output: `"${params.oldString.slice(0, 120)}" dosyada ${count} kez geçiyor. Tek bir eşleşmeyi hedefleyin ya da replaceAll=true kullanın.`
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const result = params.replaceAll ? original.split(oldText).join(newText) : original.replace(oldText, newText);
|
|
51
|
+
writeFileSync(filePath, result, "utf8");
|
|
52
|
+
return { ok: true, output: `Dosya düzenlendi: ${filePath} (${params.replaceAll ? count : 1} değişiklik)` };
|
|
53
|
+
}
|
|
54
|
+
};
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, readdirSync, statSync, existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { join, resolve, dirname, isAbsolute, relative } from "node:path";
|
|
3
|
+
import type { AgentTool } from "../config/types.js";
|
|
4
|
+
import { limitOutput, resolveUserPath, shouldSkipDir } from "./utils.js";
|
|
5
|
+
|
|
6
|
+
function resolvePath(cwd: string, input: string): string {
|
|
7
|
+
return resolveUserPath(cwd, input);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function globToRegExp(glob: string): RegExp {
|
|
11
|
+
const escaped = glob
|
|
12
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
13
|
+
.replace(/\*\*/g, "@@DOUBLE_STAR@@")
|
|
14
|
+
.replace(/\*/g, "[^/]*")
|
|
15
|
+
.replace(/\?/g, "[^/]")
|
|
16
|
+
.replace(/@@DOUBLE_STAR@@/g, ".*");
|
|
17
|
+
return new RegExp(`^${escaped}$`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizeLines(text: string): string {
|
|
21
|
+
return text.replace(/\r\n/g, "\n");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const readFileTool: AgentTool = {
|
|
25
|
+
name: "read_file",
|
|
26
|
+
description: "Bir dosyanın içeriğini okur. Büyük dosyalar için offset/limit ile kısmi okuma yapılabilir.",
|
|
27
|
+
parameters: {
|
|
28
|
+
type: "object",
|
|
29
|
+
properties: {
|
|
30
|
+
path: { type: "string", description: "Okunacak dosyanın yolu (cwd'ye göre göreli veya mutlak)" },
|
|
31
|
+
offset: { type: "integer", description: "Başlangıç satır numarası (1 tabanlı)" },
|
|
32
|
+
limit: { type: "integer", description: "Okunacak maksimum satır sayısı" }
|
|
33
|
+
},
|
|
34
|
+
required: ["path"]
|
|
35
|
+
},
|
|
36
|
+
handler: async (params: { path: string; offset?: number; limit?: number }, ctx) => {
|
|
37
|
+
const filePath = resolvePath(ctx.cwd, params.path);
|
|
38
|
+
if (!existsSync(filePath)) {
|
|
39
|
+
return { ok: false, output: `Dosya bulunamadı: ${params.path}` };
|
|
40
|
+
}
|
|
41
|
+
const stat = statSync(filePath);
|
|
42
|
+
if (!stat.isFile()) {
|
|
43
|
+
return { ok: false, output: `Bu bir dosya değil: ${params.path}` };
|
|
44
|
+
}
|
|
45
|
+
if (stat.size > 2 * 1024 * 1024 && !params.offset && !params.limit) {
|
|
46
|
+
return {
|
|
47
|
+
ok: false,
|
|
48
|
+
output: `Dosya çok büyük (${stat.size} bayt). offset/limit parametreleriyle kısmi okuma yapın.`
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const content = normalizeLines(readFileSync(filePath, "utf8"));
|
|
52
|
+
const lines = content.split("\n");
|
|
53
|
+
const totalLines = lines.length;
|
|
54
|
+
const start = params.offset ? Math.max(1, params.offset) : 1;
|
|
55
|
+
const end = params.limit ? Math.min(totalLines, start + params.limit - 1) : totalLines;
|
|
56
|
+
const slice = lines.slice(start - 1, end);
|
|
57
|
+
const numbered = slice.map((line, i) => `${String(start + i).padStart(6)} | ${line}`).join("\n");
|
|
58
|
+
const header = `Dosya: ${filePath}\nSatırlar: ${start}-${end} / ${totalLines}\n`;
|
|
59
|
+
return { ok: true, output: header + numbered };
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const writeFileTool: AgentTool = {
|
|
64
|
+
name: "write_file",
|
|
65
|
+
description:
|
|
66
|
+
"Bir dosyaya içerik yazar (oluşturur veya üzerine yazar). append=true ise sona ekler. " +
|
|
67
|
+
"Klasörler otomatik oluşturulur.",
|
|
68
|
+
parameters: {
|
|
69
|
+
type: "object",
|
|
70
|
+
properties: {
|
|
71
|
+
path: { type: "string", description: "Yazılacak dosyanın yolu" },
|
|
72
|
+
content: { type: "string", description: "Dosya içeriği" },
|
|
73
|
+
append: { type: "boolean", description: "true ise içeriği dosyanın sonuna ekler (varsayılan: false)" }
|
|
74
|
+
},
|
|
75
|
+
required: ["path", "content"]
|
|
76
|
+
},
|
|
77
|
+
handler: async (params: { path: string; content: string; append?: boolean }, ctx) => {
|
|
78
|
+
const filePath = resolvePath(ctx.cwd, params.path);
|
|
79
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
80
|
+
const content = normalizeLines(params.content);
|
|
81
|
+
writeFileSync(filePath, content, { encoding: "utf8", flag: params.append ? "a" : "w" });
|
|
82
|
+
const action = params.append ? "eklendi" : "yazıldı";
|
|
83
|
+
return { ok: true, output: `Dosya ${action}: ${filePath} (${Buffer.byteLength(content)} bayt)` };
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const listDirTool: AgentTool = {
|
|
88
|
+
name: "list_dir",
|
|
89
|
+
description: "Bir dizindeki dosya ve klasörleri listeler. recursive=true ile alt dizinler de dahil edilir.",
|
|
90
|
+
parameters: {
|
|
91
|
+
type: "object",
|
|
92
|
+
properties: {
|
|
93
|
+
path: { type: "string", description: "Listelenecek dizin (varsayılan: cwd)" },
|
|
94
|
+
recursive: { type: "boolean", description: "Alt dizinleri de listele (varsayılan: false)" }
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
handler: async (params: { path?: string; recursive?: boolean }, ctx) => {
|
|
98
|
+
const dirPath = resolvePath(ctx.cwd, params.path ?? ".");
|
|
99
|
+
if (!existsSync(dirPath)) {
|
|
100
|
+
return { ok: false, output: `Dizin bulunamadı: ${params.path ?? "."}` };
|
|
101
|
+
}
|
|
102
|
+
if (!statSync(dirPath).isDirectory()) {
|
|
103
|
+
return { ok: false, output: `Bu bir dizin değil: ${params.path ?? "."}` };
|
|
104
|
+
}
|
|
105
|
+
const entries: string[] = [];
|
|
106
|
+
let skippedCount = 0;
|
|
107
|
+
const MAX_ENTRIES = 300;
|
|
108
|
+
const walk = (current: string, depth: number): void => {
|
|
109
|
+
if (depth > 5 || entries.length >= MAX_ENTRIES) return;
|
|
110
|
+
let children;
|
|
111
|
+
try {
|
|
112
|
+
children = readdirSync(current, { withFileTypes: true });
|
|
113
|
+
} catch (err) {
|
|
114
|
+
entries.push(` ${" ".repeat(depth)}[hata: ${(err as Error).message}]`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
for (const child of children) {
|
|
118
|
+
if (entries.length >= MAX_ENTRIES) break;
|
|
119
|
+
const childPath = join(current, child.name);
|
|
120
|
+
const isDir = child.isDirectory();
|
|
121
|
+
if (isDir && shouldSkipDir(child.name)) {
|
|
122
|
+
skippedCount++;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const indent = " ".repeat(depth);
|
|
126
|
+
const rel = relative(dirPath, childPath).replace(/\\/g, "/");
|
|
127
|
+
if (isDir) {
|
|
128
|
+
entries.push(`${indent}${rel}/`);
|
|
129
|
+
if (params.recursive) walk(childPath, depth + 1);
|
|
130
|
+
} else {
|
|
131
|
+
let size = 0;
|
|
132
|
+
try {
|
|
133
|
+
size = statSync(childPath).size;
|
|
134
|
+
} catch {
|
|
135
|
+
size = 0;
|
|
136
|
+
}
|
|
137
|
+
entries.push(`${indent}${rel} (${size} bayt)`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
walk(dirPath, 0);
|
|
142
|
+
let output = `Dizin: ${dirPath}\n${entries.join("\n")}`;
|
|
143
|
+
if (skippedCount > 0) output += `\n(... ${skippedCount} ağır/gizli klasör atlandı: node_modules, .git, dist, build vb.)`;
|
|
144
|
+
if (entries.length >= MAX_ENTRIES) output += `\n(... ${entries.length} öğe sınırına ulaşıldı)`;
|
|
145
|
+
return { ok: true, output: limitOutput(output, ctx.config.maxToolOutput) };
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export const searchFilesTool: AgentTool = {
|
|
150
|
+
name: "search_files",
|
|
151
|
+
description: "Bir dizindeki dosyaları isim desenine (glob) göre arar.",
|
|
152
|
+
parameters: {
|
|
153
|
+
type: "object",
|
|
154
|
+
properties: {
|
|
155
|
+
path: { type: "string", description: "Aranacak dizin (varsayılan: cwd)" },
|
|
156
|
+
pattern: { type: "string", description: "Glob deseni, ör. '*.ts' veya '**/*.ts'" }
|
|
157
|
+
},
|
|
158
|
+
required: ["pattern"]
|
|
159
|
+
},
|
|
160
|
+
handler: async (params: { path?: string; pattern: string }, ctx) => {
|
|
161
|
+
const dirPath = resolvePath(ctx.cwd, params.path ?? ".");
|
|
162
|
+
const matches: string[] = [];
|
|
163
|
+
const pattern = params.pattern.replace(/^\/+/, "").replace(/\\/g, "/");
|
|
164
|
+
const regex = globToRegExp(pattern);
|
|
165
|
+
const walk = (current: string, depth: number): void => {
|
|
166
|
+
if (depth > 8) return;
|
|
167
|
+
let children;
|
|
168
|
+
try {
|
|
169
|
+
children = readdirSync(current, { withFileTypes: true });
|
|
170
|
+
} catch {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
for (const child of children) {
|
|
174
|
+
const childPath = join(current, child.name);
|
|
175
|
+
const rel = relative(dirPath, childPath).replace(/\\/g, "/");
|
|
176
|
+
if (child.isDirectory()) {
|
|
177
|
+
if (shouldSkipDir(child.name)) continue;
|
|
178
|
+
if (regex.test(`${rel}/`)) matches.push(`${rel}/`);
|
|
179
|
+
walk(childPath, depth + 1);
|
|
180
|
+
} else {
|
|
181
|
+
if (regex.test(rel)) matches.push(rel);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
walk(dirPath, 0);
|
|
186
|
+
const result = matches.length > 0 ? matches.join("\n") : "Eşleşen dosya bulunamadı.";
|
|
187
|
+
return { ok: true, output: `Glob: ${params.pattern}\n${result}` };
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export const grepTool: AgentTool = {
|
|
192
|
+
name: "grep_files",
|
|
193
|
+
description: "Dosyalarda içerik araması yapar. pattern bir regex'dir.",
|
|
194
|
+
parameters: {
|
|
195
|
+
type: "object",
|
|
196
|
+
properties: {
|
|
197
|
+
path: { type: "string", description: "Aranacak dizin (varsayılan: cwd)" },
|
|
198
|
+
pattern: { type: "string", description: "Regex deseni" },
|
|
199
|
+
include: { type: "string", description: "Dosya deseni filtresi, ör. '*.ts'" }
|
|
200
|
+
},
|
|
201
|
+
required: ["pattern"]
|
|
202
|
+
},
|
|
203
|
+
handler: async (params: { path?: string; pattern: string; include?: string }, ctx) => {
|
|
204
|
+
const dirPath = resolvePath(ctx.cwd, params.path ?? ".");
|
|
205
|
+
let regex: RegExp;
|
|
206
|
+
try {
|
|
207
|
+
regex = new RegExp(params.pattern);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
return { ok: false, output: `Geçersiz regex deseni: ${(err as Error).message}` };
|
|
210
|
+
}
|
|
211
|
+
let includeRe: RegExp | null = null;
|
|
212
|
+
if (params.include) {
|
|
213
|
+
try {
|
|
214
|
+
includeRe = new RegExp(
|
|
215
|
+
`^${params.include.replace(/\./g, "\\.").replace(/\*/g, ".*")}$`
|
|
216
|
+
);
|
|
217
|
+
} catch {
|
|
218
|
+
includeRe = null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const results: string[] = [];
|
|
222
|
+
let fileCount = 0;
|
|
223
|
+
const walk = (current: string, depth: number): void => {
|
|
224
|
+
if (depth > 8 || fileCount > 500) return;
|
|
225
|
+
let children;
|
|
226
|
+
try {
|
|
227
|
+
children = readdirSync(current, { withFileTypes: true });
|
|
228
|
+
} catch {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
for (const child of children) {
|
|
232
|
+
if (shouldSkipDir(child.name)) continue;
|
|
233
|
+
const childPath = join(current, child.name);
|
|
234
|
+
if (child.isDirectory()) {
|
|
235
|
+
walk(childPath, depth + 1);
|
|
236
|
+
} else {
|
|
237
|
+
if (includeRe && !includeRe.test(child.name)) continue;
|
|
238
|
+
fileCount++;
|
|
239
|
+
try {
|
|
240
|
+
if (statSync(childPath).size > 2 * 1024 * 1024) continue;
|
|
241
|
+
} catch {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
const lines = normalizeLines(readFileSync(childPath, "utf8")).split("\n");
|
|
246
|
+
for (let i = 0; i < lines.length; i++) {
|
|
247
|
+
const line = lines[i] ?? "";
|
|
248
|
+
if (regex.test(line)) {
|
|
249
|
+
results.push(`${relative(dirPath, childPath).replace(/\\/g, "/")}:${i + 1}: ${line.trim().slice(0, 200)}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
} catch {
|
|
253
|
+
/* okunamayan dosyayı atla */
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
walk(dirPath, 0);
|
|
259
|
+
return { ok: true, output: limitOutput(results.join("\n"), ctx.config.maxToolOutput) };
|
|
260
|
+
}
|
|
261
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, renameSync, rmSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import type { AgentTool } from "../config/types.js";
|
|
4
|
+
import { resolveUserPath } from "./utils.js";
|
|
5
|
+
|
|
6
|
+
export const moveFileTool: AgentTool = {
|
|
7
|
+
name: "move_file",
|
|
8
|
+
description:
|
|
9
|
+
"Dosya veya klasörü taşır / yeniden adlandırır. Hedef dizin yoksa oluşturulur. Var olan dosyanın üzerine yazmaz.",
|
|
10
|
+
parameters: {
|
|
11
|
+
type: "object",
|
|
12
|
+
properties: {
|
|
13
|
+
source: { type: "string", description: "Kaynak yol" },
|
|
14
|
+
destination: { type: "string", description: "Hedef yol (dosya veya klasör)" }
|
|
15
|
+
},
|
|
16
|
+
required: ["source", "destination"]
|
|
17
|
+
},
|
|
18
|
+
handler: async (params: { source?: string; destination?: string }, ctx) => {
|
|
19
|
+
const src = params.source?.trim();
|
|
20
|
+
const dest = params.destination?.trim();
|
|
21
|
+
if (!src || !dest) return { ok: false, output: "source ve destination zorunludur." };
|
|
22
|
+
const srcPath = resolveUserPath(ctx.cwd, src);
|
|
23
|
+
const destPath = resolveUserPath(ctx.cwd, dest);
|
|
24
|
+
if (srcPath === destPath) return { ok: false, output: "Kaynak ve hedef aynı." };
|
|
25
|
+
if (!existsSync(srcPath)) return { ok: false, output: `Kaynak bulunamadı: ${src}` };
|
|
26
|
+
if (existsSync(destPath)) return { ok: false, output: `Hedef zaten var: ${dest} (önce silin)` };
|
|
27
|
+
try {
|
|
28
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
29
|
+
renameSync(srcPath, destPath);
|
|
30
|
+
return { ok: true, output: `Taşındı: ${srcPath} → ${destPath}` };
|
|
31
|
+
} catch (err) {
|
|
32
|
+
return { ok: false, output: `Taşıma başarısız: ${(err as Error).message}` };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const deleteFileTool: AgentTool = {
|
|
38
|
+
name: "delete_file",
|
|
39
|
+
description:
|
|
40
|
+
"Dosya veya klasör siler. Klasörler için recursive:true gerekir. Dikkatli kullan — geri alınamaz.",
|
|
41
|
+
parameters: {
|
|
42
|
+
type: "object",
|
|
43
|
+
properties: {
|
|
44
|
+
path: { type: "string", description: "Silinecek yol" },
|
|
45
|
+
recursive: { type: "boolean", description: "Klasörü içeriğiyle sil (varsayılan: false)" }
|
|
46
|
+
},
|
|
47
|
+
required: ["path"]
|
|
48
|
+
},
|
|
49
|
+
handler: async (params: { path?: string; recursive?: boolean }, ctx) => {
|
|
50
|
+
const p = params.path?.trim();
|
|
51
|
+
if (!p) return { ok: false, output: "path zorunludur." };
|
|
52
|
+
const target = resolveUserPath(ctx.cwd, p);
|
|
53
|
+
if (!existsSync(target)) return { ok: false, output: `Bulunamadı: ${p}` };
|
|
54
|
+
const cwdResolved = resolve(ctx.cwd);
|
|
55
|
+
if (target === cwdResolved || cwdResolved.startsWith(target + "\\") || cwdResolved.startsWith(target + "/")) {
|
|
56
|
+
return { ok: false, output: "Çalışma dizinini veya üstünü silemezsin." };
|
|
57
|
+
}
|
|
58
|
+
const stat = statSync(target);
|
|
59
|
+
const isDir = stat.isDirectory();
|
|
60
|
+
if (isDir && !params.recursive) {
|
|
61
|
+
return { ok: false, output: `"${p}" bir klasör; silmek için recursive:true ver.` };
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
rmSync(target, { recursive: isDir, force: true });
|
|
65
|
+
return { ok: true, output: `Silindi: ${target}${isDir ? " (klasör)" : ""}` };
|
|
66
|
+
} catch (err) {
|
|
67
|
+
return { ok: false, output: `Silme başarısız: ${(err as Error).message}` };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } 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
|
+
type Note = { id: number; text: string; createdAt: string };
|
|
7
|
+
|
|
8
|
+
type NotesFile = { notes: Note[] };
|
|
9
|
+
|
|
10
|
+
function notesPath(): string {
|
|
11
|
+
const base = process.env.AJAN_HOME ?? join(process.env.USERPROFILE ?? process.env.HOME ?? ".", ".ajan");
|
|
12
|
+
return join(base, "notes.json");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function loadNotes(): NotesFile {
|
|
16
|
+
try {
|
|
17
|
+
if (existsSync(notesPath())) {
|
|
18
|
+
const parsed = JSON.parse(readFileSync(notesPath(), "utf8")) as Partial<NotesFile>;
|
|
19
|
+
return { notes: Array.isArray(parsed.notes) ? parsed.notes : [] };
|
|
20
|
+
}
|
|
21
|
+
} catch {
|
|
22
|
+
/* bozuk dosya: sıfırla */
|
|
23
|
+
}
|
|
24
|
+
return { notes: [] };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function saveNotes(data: NotesFile): void {
|
|
28
|
+
writeFileSync(notesPath(), JSON.stringify(data, null, 2), "utf8");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const noteAddTool: AgentTool = {
|
|
32
|
+
name: "not_al",
|
|
33
|
+
description:
|
|
34
|
+
"Kalıcı hafızaya not alır. Kullanıcı tercihleri, proje kararları, önemli bilgiler için kullan. " +
|
|
35
|
+
"Notlar oturumlar arasında korunur (~/.ajan/notes.json).",
|
|
36
|
+
parameters: {
|
|
37
|
+
type: "object",
|
|
38
|
+
properties: {
|
|
39
|
+
text: { type: "string", description: "Kaydedilecek not metni" }
|
|
40
|
+
},
|
|
41
|
+
required: ["text"]
|
|
42
|
+
},
|
|
43
|
+
handler: async (params: { text?: string }) => {
|
|
44
|
+
const text = (params.text ?? "").trim();
|
|
45
|
+
if (!text) return { ok: false, output: "Not metni boş olamaz." };
|
|
46
|
+
const data = loadNotes();
|
|
47
|
+
const id = data.notes.reduce((max, n) => Math.max(max, n.id), 0) + 1;
|
|
48
|
+
data.notes.push({ id, text, createdAt: new Date().toISOString() });
|
|
49
|
+
saveNotes(data);
|
|
50
|
+
return { ok: true, output: `Not #${id} kaydedildi.` };
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const noteReadTool: AgentTool = {
|
|
55
|
+
name: "not_oku",
|
|
56
|
+
description:
|
|
57
|
+
"Kalıcı hafızadaki notları okur. query verilirse içinde o kelime geçen notlar döner; yoksa son 30 not.",
|
|
58
|
+
parameters: {
|
|
59
|
+
type: "object",
|
|
60
|
+
properties: {
|
|
61
|
+
query: { type: "string", description: "Notlar içinde aranacak kelime (opsiyonel)" }
|
|
62
|
+
},
|
|
63
|
+
required: []
|
|
64
|
+
},
|
|
65
|
+
handler: async (params: { query?: string }, ctx) => {
|
|
66
|
+
const data = loadNotes();
|
|
67
|
+
let notes = data.notes;
|
|
68
|
+
const q = params.query?.trim().toLowerCase();
|
|
69
|
+
if (q) notes = notes.filter((n) => n.text.toLowerCase().includes(q));
|
|
70
|
+
if (notes.length === 0) return { ok: true, output: q ? `"${params.query}" ile eşleşen not yok.` : "Hafızada not yok." };
|
|
71
|
+
const slice = notes.slice(-30);
|
|
72
|
+
const lines = slice.map((n) => `#${n.id} [${n.createdAt.slice(0, 10)}] ${n.text}`);
|
|
73
|
+
const header = `${notes.length} not${q ? ` ("${params.query}")` : ""}${notes.length > slice.length ? ` (son ${slice.length} gösteriliyor)` : ""}:\n`;
|
|
74
|
+
return { ok: true, output: limitOutput(header + lines.join("\n"), ctx.config.maxToolOutput) };
|
|
75
|
+
}
|
|
76
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import type { AgentTool } from "../config/types.js";
|
|
4
|
+
import { logger } from "../utils/paths.js";
|
|
5
|
+
import { resolveUserPath } from "./utils.js";
|
|
6
|
+
|
|
7
|
+
function resolvePath(cwd: string, input: string): string {
|
|
8
|
+
return resolveUserPath(cwd, input);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type Hunk = {
|
|
12
|
+
oldStart: number;
|
|
13
|
+
newStart: number;
|
|
14
|
+
lines: { type: " " | "+" | "-"; text: string }[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function parseUnifiedDiff(diff: string): { file: string; hunks: Hunk[] } | null {
|
|
18
|
+
const lines = diff.replace(/\r\n/g, "\n").split("\n");
|
|
19
|
+
let file: string | undefined;
|
|
20
|
+
const hunks: Hunk[] = [];
|
|
21
|
+
let current: Hunk | undefined;
|
|
22
|
+
|
|
23
|
+
for (const rawLine of lines) {
|
|
24
|
+
if (rawLine.startsWith("--- ")) {
|
|
25
|
+
file = rawLine.slice(4).replace(/^\s*[ab]\//, "").trim();
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (rawLine.startsWith("+++ ")) continue;
|
|
29
|
+
const hunkMatch = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/.exec(rawLine);
|
|
30
|
+
if (hunkMatch) {
|
|
31
|
+
current = {
|
|
32
|
+
oldStart: parseInt(hunkMatch[1] ?? "1", 10),
|
|
33
|
+
newStart: parseInt(hunkMatch[3] ?? "1", 10),
|
|
34
|
+
lines: []
|
|
35
|
+
};
|
|
36
|
+
hunks.push(current);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (!current) continue;
|
|
40
|
+
if (rawLine.startsWith("+")) {
|
|
41
|
+
current.lines.push({ type: "+", text: rawLine.slice(1) });
|
|
42
|
+
} else if (rawLine.startsWith("-")) {
|
|
43
|
+
current.lines.push({ type: "-", text: rawLine.slice(1) });
|
|
44
|
+
} else if (rawLine.startsWith(" ")) {
|
|
45
|
+
current.lines.push({ type: " ", text: rawLine.slice(1) });
|
|
46
|
+
} else if (rawLine.startsWith("\\")) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!file || hunks.length === 0) return null;
|
|
52
|
+
return { file, hunks };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function applyHunks(original: string, hunks: Hunk[]): { result: string; changedLines: number } {
|
|
56
|
+
const lines = original.split("\n");
|
|
57
|
+
const newLines: string[] = [];
|
|
58
|
+
let oldLine = 1;
|
|
59
|
+
let changedLines = 0;
|
|
60
|
+
|
|
61
|
+
for (const hunk of hunks) {
|
|
62
|
+
while (oldLine < hunk.oldStart && oldLine <= lines.length) {
|
|
63
|
+
newLines.push(lines[oldLine - 1] ?? "");
|
|
64
|
+
oldLine++;
|
|
65
|
+
}
|
|
66
|
+
let oldPos = hunk.oldStart;
|
|
67
|
+
let contextMatched = true;
|
|
68
|
+
const contextCheck = hunk.lines.filter((l) => l.type !== "+");
|
|
69
|
+
for (const cl of contextCheck) {
|
|
70
|
+
const current = lines[oldPos - 1];
|
|
71
|
+
if (current !== cl.text) {
|
|
72
|
+
contextMatched = false;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
oldPos++;
|
|
76
|
+
}
|
|
77
|
+
if (!contextMatched) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`Hunk satırları eşleşmiyor (${hunk.oldStart}. satır civarı). Dosya içeriği güncelliğini yitirmiş olabilir.`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
for (const line of hunk.lines) {
|
|
83
|
+
if (line.type === "-") {
|
|
84
|
+
oldLine++;
|
|
85
|
+
changedLines++;
|
|
86
|
+
} else if (line.type === "+") {
|
|
87
|
+
newLines.push(line.text);
|
|
88
|
+
changedLines++;
|
|
89
|
+
} else {
|
|
90
|
+
newLines.push(line.text);
|
|
91
|
+
oldLine++;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
oldPos = 0;
|
|
95
|
+
}
|
|
96
|
+
while (oldLine <= lines.length) {
|
|
97
|
+
newLines.push(lines[oldLine - 1] ?? "");
|
|
98
|
+
oldLine++;
|
|
99
|
+
}
|
|
100
|
+
return { result: newLines.join("\n"), changedLines };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const applyPatchTool: AgentTool = {
|
|
104
|
+
name: "apply_patch",
|
|
105
|
+
description:
|
|
106
|
+
"Unified diff formatında bir yamayı dosyaya uygular. Eksiksiz dosya değişiklikleri için write_file'ı tercih edin. " +
|
|
107
|
+
"Yama formatı: --- a/yol + +++ b/yol + @@ -eskiSatır,sayı +yeniSatır,sayı @@ + satırlar. " +
|
|
108
|
+
"Küçük ve hedefli değişikliklerde daha güvenilirdir.",
|
|
109
|
+
parameters: {
|
|
110
|
+
type: "object",
|
|
111
|
+
properties: {
|
|
112
|
+
patch: { type: "string", description: "Uygulanacak unified diff içeriği" },
|
|
113
|
+
cwd: { type: "string", description: "Göreli yolların çözüleceği dizin" }
|
|
114
|
+
},
|
|
115
|
+
required: ["patch"]
|
|
116
|
+
},
|
|
117
|
+
handler: async (params: { patch: string; cwd?: string }, ctx) => {
|
|
118
|
+
const parsed = parseUnifiedDiff(params.patch);
|
|
119
|
+
if (!parsed) {
|
|
120
|
+
return { ok: false, output: "Geçerli bir unified diff bulunamadı. --- a/yol ve @@ hunk satırları gerekli." };
|
|
121
|
+
}
|
|
122
|
+
const filePath = resolvePath(params.cwd ?? ctx.cwd, parsed.file);
|
|
123
|
+
if (!existsSync(filePath)) {
|
|
124
|
+
return { ok: false, output: `Yamalanacak dosya bulunamadı: ${parsed.file}` };
|
|
125
|
+
}
|
|
126
|
+
const original = readFileSync(filePath, "utf8");
|
|
127
|
+
try {
|
|
128
|
+
const { result, changedLines } = applyHunks(original, parsed.hunks);
|
|
129
|
+
writeFileSync(filePath, result, "utf8");
|
|
130
|
+
logger.debug(`Patch uygulandi: ${filePath} (${changedLines} satir degisti)`);
|
|
131
|
+
return { ok: true, output: `Yama uygulandı: ${filePath} (${changedLines} satır değişti)` };
|
|
132
|
+
} catch (err) {
|
|
133
|
+
return { ok: false, output: `Yama uygulanamadı: ${(err as Error).message}` };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export { parseUnifiedDiff };
|