beast-agent 2.6.1 → 2.6.3
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/package.json +1 -1
- package/src/agent/engine.js +6 -1
- package/src/agent/gittools.js +276 -0
- package/src/agent/perception.js +4 -0
- package/src/agent/repomap.js +265 -0
- package/src/agent/tools.js +16 -0
- package/src/agent/xlsx.js +474 -0
- package/src/agent/xlsxtools.js +235 -0
- package/src/main.js +34 -7
- package/src/renderer/index.html +1 -1
- package/src/renderer/renderer.js +10 -3
- package/src/renderer/style.css +8 -9
package/package.json
CHANGED
package/src/agent/engine.js
CHANGED
|
@@ -126,6 +126,7 @@ const PERM_TOOL_SETS = {
|
|
|
126
126
|
'web_search', 'http_fetch', 'webfetch', 'deep_search',
|
|
127
127
|
'browser_open', 'browser_read', 'browser_snapshot',
|
|
128
128
|
'list_dir', 'read_file', 'grep', 'glob',
|
|
129
|
+
'git_diff_review', 'repo_map', 'repo_symbols', 'xlsx_read',
|
|
129
130
|
]),
|
|
130
131
|
chat: new Set([]), // sadece sohbet
|
|
131
132
|
};
|
|
@@ -138,6 +139,7 @@ const PLAN_ALLOW_TOOLS = new Set([
|
|
|
138
139
|
'web_search', 'http_fetch', 'webfetch', 'deep_search',
|
|
139
140
|
'browser_open', 'browser_read', 'browser_snapshot',
|
|
140
141
|
'todo_write', 'memory_search', 'kb_search', 'ocr_read',
|
|
142
|
+
'git_diff_review', 'repo_map', 'repo_symbols', 'xlsx_read',
|
|
141
143
|
]);
|
|
142
144
|
|
|
143
145
|
/* İzin değerini normalize eder: 'all' → ['all'], 'web' → ['web'],
|
|
@@ -4021,6 +4023,9 @@ const skills = require('./skills');
|
|
|
4021
4023
|
/* opencode general-agent portu: alt-ajan da edit/grep/glob kullanır */
|
|
4022
4024
|
if (!(name === 'run_command' || name === 'read_file' || name === 'write_file' || name === 'edit_file' ||
|
|
4023
4025
|
name === 'list_dir' || name === 'grep' || name === 'glob' ||
|
|
4026
|
+
name === 'git_commit' || name === 'git_diff_review' || name === 'git_pr_create' ||
|
|
4027
|
+
name === 'repo_map' || name === 'repo_symbols' ||
|
|
4028
|
+
name === 'xlsx_read' || name === 'xlsx_write' || name === 'xlsx_edit' ||
|
|
4024
4029
|
name === 'web_search' || name === 'http_fetch' || name === 'webfetch' || name === 'python_run')) {
|
|
4025
4030
|
return JSON.stringify({ ok: false, error: `unknown tool ${name}` });
|
|
4026
4031
|
}
|
|
@@ -4034,7 +4039,7 @@ const skills = require('./skills');
|
|
|
4034
4039
|
|
|
4035
4040
|
/* Onay gerektiren araçlar — tepkiyle onay kapısına takılır.
|
|
4036
4041
|
opencode'da edit+write aynı "edit" iznine takılır — burada da ikisi birlikte */
|
|
4037
|
-
static RISKY_TOOLS = new Set(['run_command', 'write_file', 'edit_file', 'python_run', 'email_send', 'watcher_add']);
|
|
4042
|
+
static RISKY_TOOLS = new Set(['run_command', 'write_file', 'edit_file', 'python_run', 'email_send', 'watcher_add', 'git_commit', 'git_pr_create', 'xlsx_write', 'xlsx_edit']);
|
|
4038
4043
|
|
|
4039
4044
|
/* ANA KOD KİLİDİ: yıkıcı işlem desenleri (korumalı yol + bu desen = engel) */
|
|
4040
4045
|
static DESTRUCTIVE_RE =
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/* Yerleşik git araçları — ajan commit/diff/PR işlerini kabuk komutu yazmadan
|
|
4
|
+
yapar. execFile arg-dizisiyle çağrılır (quoting/enjeksiyon riski yok).
|
|
5
|
+
gh CLI yoksa git_pr_create net yönlendirmeyle hata döner.
|
|
6
|
+
Bildirim: tüm sonuçlar JSON'a çevrilerek tools.exec'ten döner. */
|
|
7
|
+
|
|
8
|
+
const { execFile } = require('child_process');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
|
|
12
|
+
const MAX_DIFF_CHARS = 30000;
|
|
13
|
+
const GIT_TIMEOUT = 60000;
|
|
14
|
+
const NET_TIMEOUT = 120000;
|
|
15
|
+
|
|
16
|
+
/* git/gh çağrısı: { ok, code, out, err, missing } döner — missing = binary yok */
|
|
17
|
+
function run(bin, args, cwd, timeoutMs = GIT_TIMEOUT) {
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
execFile(
|
|
20
|
+
bin,
|
|
21
|
+
args,
|
|
22
|
+
{
|
|
23
|
+
cwd: cwd || process.cwd(),
|
|
24
|
+
timeout: timeoutMs,
|
|
25
|
+
windowsHide: true,
|
|
26
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
27
|
+
env: process.env,
|
|
28
|
+
},
|
|
29
|
+
(err, stdout, stderr) => {
|
|
30
|
+
if (err && err.code === 'ENOENT') {
|
|
31
|
+
resolve({ ok: false, code: null, out: '', err: '', missing: true });
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const code = err ? (typeof err.code === 'number' ? err.code : null) : 0;
|
|
35
|
+
const killed = !!(err && err.killed);
|
|
36
|
+
resolve({
|
|
37
|
+
ok: !err,
|
|
38
|
+
code,
|
|
39
|
+
killed,
|
|
40
|
+
out: String(stdout || ''),
|
|
41
|
+
err: String(stderr || (err && err.message) || ''),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function insideWorkTree(cwd) {
|
|
49
|
+
const r = await run('git', ['rev-parse', '--is-inside-work-tree'], cwd);
|
|
50
|
+
return r.ok && r.out.trim() === 'true';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function branchName(cwd) {
|
|
54
|
+
const r = await run('git', ['rev-parse', '--abbrev-ref', 'HEAD'], cwd);
|
|
55
|
+
return r.ok ? r.out.trim() : '';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function cleanRef(ref) {
|
|
59
|
+
return /^[\w./\-~^+]{1,120}$/.test(String(ref || '')) ? String(ref) : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* ---------- git_commit ---------- */
|
|
63
|
+
async function gitCommit(args, ctx) {
|
|
64
|
+
const cwd = ctx && ctx.cwd;
|
|
65
|
+
if (!(await insideWorkTree(cwd))) {
|
|
66
|
+
return { ok: false, error: 'git deposu değil: ' + (cwd || '.') };
|
|
67
|
+
}
|
|
68
|
+
const message = String(args.message || '').trim();
|
|
69
|
+
if (!message) return { ok: false, error: 'commit mesajı boş olamaz' };
|
|
70
|
+
if (message.length > 2000) return { ok: false, error: 'commit mesajı çok uzun (max 2000 karakter)' };
|
|
71
|
+
|
|
72
|
+
/* 1) stage: paths > add_all > (yoksa mevcut staged hali commit'lenir) */
|
|
73
|
+
let staged = false;
|
|
74
|
+
if (Array.isArray(args.paths) && args.paths.length) {
|
|
75
|
+
const paths = args.paths.map((p) => path.resolve(String(cwd || '.'), String(p))).slice(0, 100);
|
|
76
|
+
const r = await run('git', ['add', '--', ...paths], cwd);
|
|
77
|
+
if (!r.ok) return { ok: false, error: 'git add başarısız: ' + (r.err || r.out).trim() };
|
|
78
|
+
staged = true;
|
|
79
|
+
} else if (args.add_all) {
|
|
80
|
+
const r = await run('git', ['add', '-A'], cwd);
|
|
81
|
+
if (!r.ok) return { ok: false, error: 'git add -A başarısız: ' + (r.err || r.out).trim() };
|
|
82
|
+
staged = true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* 2) staged değişiklik var mı? (diff --cached --quiet: 0=yok, 1=var) */
|
|
86
|
+
const q = await run('git', ['diff', '--cached', '--quiet'], cwd);
|
|
87
|
+
if (q.ok) {
|
|
88
|
+
return { ok: false, error: 'commit\'lenecek değişiklik yok — paths/add_all ver ya da önce değişiklik yap' };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* 3) commit */
|
|
92
|
+
const c = await run('git', ['commit', '-m', message], cwd);
|
|
93
|
+
if (!c.ok) {
|
|
94
|
+
return { ok: false, error: ('git commit başarısız: ' + (c.err || c.out)).trim() };
|
|
95
|
+
}
|
|
96
|
+
const hash = (await run('git', ['rev-parse', 'HEAD'], cwd)).out.trim();
|
|
97
|
+
const branch = await branchName(cwd);
|
|
98
|
+
const stat = (await run('git', ['show', '--stat', '--oneline', '-s'], cwd)).out.trim();
|
|
99
|
+
|
|
100
|
+
/* 4) opsiyonel push */
|
|
101
|
+
let pushResult = null;
|
|
102
|
+
if (args.push) {
|
|
103
|
+
const p = await run('git', ['push'], cwd, NET_TIMEOUT);
|
|
104
|
+
if (!p.ok) {
|
|
105
|
+
const p2 = await run('git', ['push', '-u', 'origin', branch || 'HEAD'], cwd, NET_TIMEOUT);
|
|
106
|
+
pushResult = p2.ok
|
|
107
|
+
? { ok: true, note: 'upstream ayarlanarak push edildi' }
|
|
108
|
+
: { ok: false, error: (p2.err || p2.out).trim() };
|
|
109
|
+
} else pushResult = { ok: true };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { ok: true, hash, branch, stat, ...(pushResult ? { push: pushResult } : {}) };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* ---------- git_diff_review ---------- */
|
|
116
|
+
async function gitDiffReview(args, ctx) {
|
|
117
|
+
const cwd = ctx && ctx.cwd;
|
|
118
|
+
if (!(await insideWorkTree(cwd))) {
|
|
119
|
+
return { ok: false, error: 'git deposu değil: ' + (cwd || '.') };
|
|
120
|
+
}
|
|
121
|
+
const staged = !!args.staged;
|
|
122
|
+
const ref = args.ref ? cleanRef(args.ref) : null;
|
|
123
|
+
if (args.ref && !ref) return { ok: false, error: 'geçersiz ref: ' + String(args.ref).slice(0, 60) };
|
|
124
|
+
const ctxN = Math.min(20, Math.max(0, Math.floor(Number(args.context) || 3)));
|
|
125
|
+
const maxChars = Math.min(60000, Math.max(500, Math.floor(Number(args.max_chars) || MAX_DIFF_CHARS)));
|
|
126
|
+
|
|
127
|
+
const branch = await branchName(cwd);
|
|
128
|
+
const status = (await run('git', ['status', '--porcelain=v1'], cwd)).out
|
|
129
|
+
.split('\n')
|
|
130
|
+
.filter(Boolean)
|
|
131
|
+
.slice(0, 60);
|
|
132
|
+
|
|
133
|
+
const statArgs = ['diff', '--stat'];
|
|
134
|
+
const diffArgs = ['diff', '--unified=' + ctxN];
|
|
135
|
+
if (staged) {
|
|
136
|
+
statArgs.splice(1, 0, '--staged');
|
|
137
|
+
diffArgs.splice(1, 0, '--staged');
|
|
138
|
+
}
|
|
139
|
+
if (ref) {
|
|
140
|
+
statArgs.push(ref);
|
|
141
|
+
diffArgs.push(ref);
|
|
142
|
+
}
|
|
143
|
+
const stat = (await run('git', statArgs, cwd)).out.trim();
|
|
144
|
+
const dr = await run('git', diffArgs, cwd);
|
|
145
|
+
let diff = (dr.out || '').trim();
|
|
146
|
+
let truncated = false;
|
|
147
|
+
if (diff.length > maxChars) {
|
|
148
|
+
diff = diff.slice(0, maxChars);
|
|
149
|
+
truncated = true;
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
ok: true,
|
|
153
|
+
branch,
|
|
154
|
+
staged,
|
|
155
|
+
...(ref ? { ref } : {}),
|
|
156
|
+
status,
|
|
157
|
+
stat: stat || '(fark yok)',
|
|
158
|
+
diff: diff || '(fark yok)',
|
|
159
|
+
...(truncated ? { truncated: true, note: `çıktı ${maxChars} karakterde kesildi — max_chars'i artır ya da ref daralt` } : {}),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* ---------- git_pr_create ---------- */
|
|
164
|
+
async function gitPrCreate(args, ctx) {
|
|
165
|
+
const cwd = ctx && ctx.cwd;
|
|
166
|
+
if (!(await insideWorkTree(cwd))) {
|
|
167
|
+
return { ok: false, error: 'git deposu değil: ' + (cwd || '.') };
|
|
168
|
+
}
|
|
169
|
+
const title = String(args.title || '').trim();
|
|
170
|
+
if (!title) return { ok: false, error: 'PR başlığı boş olamaz' };
|
|
171
|
+
const branch = await branchName(cwd);
|
|
172
|
+
if (!branch || branch === 'HEAD') {
|
|
173
|
+
return { ok: false, error: 'dal okunamadı — depo boş olabilir (önce bir commit yap)' };
|
|
174
|
+
}
|
|
175
|
+
if (/^(main|master)$/i.test(branch)) {
|
|
176
|
+
return { ok: false, error: `şu an '${branch}' dalındasın — önce bir özellik dalı aç (git switch -c feature/...), sonra PR oluştur` };
|
|
177
|
+
}
|
|
178
|
+
const base = args.base ? cleanRef(args.base) : null;
|
|
179
|
+
if (args.base && !base) return { ok: false, error: 'geçersiz base: ' + String(args.base).slice(0, 60) };
|
|
180
|
+
|
|
181
|
+
/* gh var mı? */
|
|
182
|
+
const v = await run('gh', ['--version'], cwd, 15000);
|
|
183
|
+
if (v.missing) {
|
|
184
|
+
return { ok: false, error: 'gh CLI bulunamadı — kur: winget install GitHub.cli (sonra: gh auth login)' };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* dal uzakta yoksa push et — PR için şart */
|
|
188
|
+
const push = await run('git', ['push', '-u', 'origin', branch], cwd, NET_TIMEOUT);
|
|
189
|
+
if (!push.ok && !/up.to.date|everything.up.to.date/i.test(push.err + push.out)) {
|
|
190
|
+
return { ok: false, error: 'dal push edilemedi: ' + (push.err || push.out).trim() };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const prArgs = ['pr', 'create', '--title', title.slice(0, 300), '--body', String(args.body || '').slice(0, 8000)];
|
|
194
|
+
if (base) prArgs.push('--base', base);
|
|
195
|
+
if (args.draft) prArgs.push('--draft');
|
|
196
|
+
const r = await run('gh', prArgs, cwd, NET_TIMEOUT);
|
|
197
|
+
if (!r.ok) {
|
|
198
|
+
const e = (r.err || r.out).trim();
|
|
199
|
+
if (/already exists/i.test(e)) return { ok: false, error: 'bu dal için PR zaten açık: ' + e };
|
|
200
|
+
return { ok: false, error: 'gh pr create başarısız: ' + e };
|
|
201
|
+
}
|
|
202
|
+
const url = (r.out.match(/https?:\/\/\S+/) || [null])[0];
|
|
203
|
+
return { ok: true, branch, ...(url ? { url } : { output: r.out.trim() }) };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const definitions = [
|
|
207
|
+
{
|
|
208
|
+
type: 'function',
|
|
209
|
+
function: {
|
|
210
|
+
name: 'git_commit',
|
|
211
|
+
description:
|
|
212
|
+
'Commit staged or specified changes to the local git repository. Optionally pushes. Returns commit hash, branch and a stat summary. Prefer this over raw `git` shell commands. Usage:\n' +
|
|
213
|
+
'- Provide `message` (required). Stage with `paths` (array of files) or `add_all: true` (everything, including untracked); if neither is given, whatever is already staged is committed.\n' +
|
|
214
|
+
'- The tool refuses when there is nothing staged to commit.\n' +
|
|
215
|
+
'- Set `push: true` to push the branch after committing.',
|
|
216
|
+
parameters: {
|
|
217
|
+
type: 'object',
|
|
218
|
+
properties: {
|
|
219
|
+
message: { type: 'string', description: 'Commit message (concise, matches repo style)' },
|
|
220
|
+
paths: { type: 'array', items: { type: 'string' }, description: 'Specific files/dirs to stage before committing' },
|
|
221
|
+
add_all: { type: 'boolean', description: 'Stage all changes (git add -A) before committing' },
|
|
222
|
+
push: { type: 'boolean', description: 'Push the branch to its remote after committing' },
|
|
223
|
+
},
|
|
224
|
+
required: ['message'],
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
type: 'function',
|
|
230
|
+
function: {
|
|
231
|
+
name: 'git_diff_review',
|
|
232
|
+
description:
|
|
233
|
+
'Review git changes: returns status, --stat summary and the unified diff of unstaged, staged (--staged) or ref-compared (ref: "HEAD~1", "main...", "a..b") changes. Use to self-review edits before committing or to summarize what changed.',
|
|
234
|
+
parameters: {
|
|
235
|
+
type: 'object',
|
|
236
|
+
properties: {
|
|
237
|
+
staged: { type: 'boolean', description: 'Diff the index (staged) instead of the working tree' },
|
|
238
|
+
ref: { type: 'string', description: 'Compare against a ref, e.g. "HEAD~1", "main", "v1.2.0"' },
|
|
239
|
+
context: { type: 'number', description: 'Unified context lines (default 3, max 20)' },
|
|
240
|
+
max_chars: { type: 'number', description: 'Cap diff output length (default 30000)' },
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
type: 'function',
|
|
247
|
+
function: {
|
|
248
|
+
name: 'git_pr_create',
|
|
249
|
+
description:
|
|
250
|
+
'Create a GitHub pull request from the current branch via the gh CLI. Pushes the branch first (git push -u origin <branch>). Refuses on main/master — switch to a feature branch first. Returns the PR URL.',
|
|
251
|
+
parameters: {
|
|
252
|
+
type: 'object',
|
|
253
|
+
properties: {
|
|
254
|
+
title: { type: 'string', description: 'PR title' },
|
|
255
|
+
body: { type: 'string', description: 'PR description (markdown)' },
|
|
256
|
+
base: { type: 'string', description: 'Target branch (default: repo default branch)' },
|
|
257
|
+
draft: { type: 'boolean', description: 'Create as draft PR' },
|
|
258
|
+
},
|
|
259
|
+
required: ['title'],
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
];
|
|
264
|
+
|
|
265
|
+
module.exports = {
|
|
266
|
+
definitions,
|
|
267
|
+
handlers: {
|
|
268
|
+
git_commit: gitCommit,
|
|
269
|
+
git_diff_review: gitDiffReview,
|
|
270
|
+
git_pr_create: gitPrCreate,
|
|
271
|
+
},
|
|
272
|
+
/* test + dış kullanım */
|
|
273
|
+
run,
|
|
274
|
+
insideWorkTree,
|
|
275
|
+
branchName,
|
|
276
|
+
};
|
package/src/agent/perception.js
CHANGED
|
@@ -584,6 +584,10 @@ async function runCycle({ cfg, signals, llmFilter, now = new Date(), log = () =>
|
|
|
584
584
|
}
|
|
585
585
|
counts.queued = kept.length;
|
|
586
586
|
|
|
587
|
+
/* 'yok sayıldı' olaylar depoda TUTULMAZ — son olaylar listesi temiz kalır
|
|
588
|
+
(eski depoda kalan ignored'lar da bu filtreyle bir temizlikte silinir) */
|
|
589
|
+
st.events = st.events.filter((e) => e.status !== 'ignored');
|
|
590
|
+
|
|
587
591
|
/* depo tavanı */
|
|
588
592
|
if (st.events.length > EVENT_CAP) st.events = st.events.slice(st.events.length - EVENT_CAP);
|
|
589
593
|
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/* Hafif repo haritası / sembol arama — "hangi fonksiyon nerede" sorusuna
|
|
4
|
+
LSP kurmadan cevap verir. Satır-bazlı regex çıkarımı: js/ts/jsx/tsx,
|
|
5
|
+
python, go, rust, c/cpp, c#/java/kt, php, ruby, swift.
|
|
6
|
+
Ağır klasörler (node_modules, .git, dist…) tools.js SKIP_DIRS ile aynı
|
|
7
|
+
mantıkta burada da atlanır. */
|
|
8
|
+
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
|
|
12
|
+
const SKIP_DIRS = new Set([
|
|
13
|
+
'node_modules', '.git', '__pycache__', '.venv', 'venv', 'env',
|
|
14
|
+
'dist', 'build', 'out', 'coverage', '.next', '.turbo', '.cache',
|
|
15
|
+
'vendor', 'target', 'bin', 'obj', '.idea', '.vscode',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
const MAX_FILE_BYTES = 600 * 1024;
|
|
19
|
+
|
|
20
|
+
/* satır → { kind, name } listesi; kind: function | class | method */
|
|
21
|
+
const RULES = [
|
|
22
|
+
{
|
|
23
|
+
ext: /\.(m?js|cjs|jsx|tsx?|mts|cts)$/i,
|
|
24
|
+
lines: [
|
|
25
|
+
[/^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s*\*?\s*([A-Za-z_$][\w$]*)/, 'function'],
|
|
26
|
+
[/^\s*(?:export\s+)?(?:default\s+)?(?:abstract\s+)?class\s+([A-Za-z_$][\w$]*)/, 'class'],
|
|
27
|
+
[/^\s*(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s+)?(?:function\b|\([^)]*\)\s*=>|[A-Za-z_$][\w$]*\s*=>)/, 'function'],
|
|
28
|
+
[/^\s+(?:static\s+)?(?:async\s+)?(?:get\s+|set\s+)?([A-Za-z_$][\w$]*)\s*\([^()]*\)\s*\{/, 'method'],
|
|
29
|
+
],
|
|
30
|
+
methodKw: /^(if|for|while|switch|catch|function|return|else|do|new|typeof|delete|void|await|yield|super|try)$/,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
ext: /\.py$/i,
|
|
34
|
+
lines: [
|
|
35
|
+
[/^\s*(?:async\s+)?def\s+([A-Za-z_]\w*)/, 'function'],
|
|
36
|
+
[/^\s*class\s+([A-Za-z_]\w*)/, 'class'],
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
ext: /\.go$/i,
|
|
41
|
+
lines: [
|
|
42
|
+
[/^func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)/, 'function'],
|
|
43
|
+
[/^type\s+([A-Za-z_]\w*)\s+(?:struct|interface)\b/, 'class'],
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
ext: /\.rs$/i,
|
|
48
|
+
lines: [
|
|
49
|
+
[/^\s*(?:pub\s+)?(?:async\s+)?fn\s+([A-Za-z_]\w*)/, 'function'],
|
|
50
|
+
[/^\s*(?:pub\s+)?(?:struct|enum|trait)\s+([A-Za-z_]\w*)/, 'class'],
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
ext: /\.(c|h|cpp|hpp|cc|hh)$/i,
|
|
55
|
+
lines: [
|
|
56
|
+
[/^[A-Za-z_][\w\s\*&:<>]*?\b([A-Za-z_]\w*)\s*\([^;]*\)\s*\{\s*$/, 'function'],
|
|
57
|
+
[/^\s*(?:class|struct)\s+([A-Za-z_]\w*)/, 'class'],
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
ext: /\.(cs|java|kt|kts|swift|scala)$/i,
|
|
62
|
+
lines: [
|
|
63
|
+
[/^\s*(?:(?:public|private|protected|internal|static|final|abstract|sealed|override|data|open|case)\s+)*(?:class|interface|enum|record|object)\s+([A-Za-z_]\w*)/, 'class'],
|
|
64
|
+
[/^\s*(?:(?:public|private|protected|internal|static|async|override)\s+)+[\w<>\[\],\s]+\s+([A-Za-z_]\w*)\s*\([^;]*\)\s*\{/, 'method'],
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
ext: /\.php$/i,
|
|
69
|
+
lines: [
|
|
70
|
+
[/^\s*(?:abstract\s+)?class\s+([A-Za-z_]\w*)/i, 'class'],
|
|
71
|
+
[/^\s*(?:(?:public|private|protected|static)\s+)*function\s+([A-Za-z_]\w*)/i, 'function'],
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
ext: /\.rb$/i,
|
|
76
|
+
lines: [
|
|
77
|
+
[/^\s*def\s+(?:self\.)?([A-Za-z_]\w*[?!]?)/, 'function'],
|
|
78
|
+
[/^\s*(?:class|module)\s+([A-Z]\w*)/, 'class'],
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
function rulesFor(file) {
|
|
84
|
+
for (const r of RULES) if (r.ext.test(file)) return r;
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* tek dosyadan sembolleri çıkar: [{ line, kind, name }] */
|
|
89
|
+
function extractSymbols(file, src) {
|
|
90
|
+
const rule = rulesFor(file);
|
|
91
|
+
if (!rule) return [];
|
|
92
|
+
const out = [];
|
|
93
|
+
const lines = src.split('\n');
|
|
94
|
+
for (let i = 0; i < lines.length && i < 20000; i++) {
|
|
95
|
+
const line = lines[i];
|
|
96
|
+
if (line.length > 500) continue;
|
|
97
|
+
for (const [re, kind] of rule.lines) {
|
|
98
|
+
const m = line.match(re);
|
|
99
|
+
if (!m) continue;
|
|
100
|
+
const name = m[1];
|
|
101
|
+
if (rule.methodKw && rule.methodKw.test(name)) break; /* if/for/while... yakalama */
|
|
102
|
+
out.push({ line: i + 1, kind: kind === 'method' ? 'method' : kind, name });
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* dizin yürüyüşü (kod dosyaları) — walkFiles'tan bağımsız: SKIP_DIRS +
|
|
110
|
+
gizli klasör atlar, dosya sayısı tavanlı */
|
|
111
|
+
function walk(root, cb, state, depth = 0) {
|
|
112
|
+
if (depth > 20) return;
|
|
113
|
+
let entries = [];
|
|
114
|
+
try {
|
|
115
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
116
|
+
} catch {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
for (const e of entries) {
|
|
120
|
+
if (state.files >= state.maxFiles) return;
|
|
121
|
+
const full = path.join(root, e.name);
|
|
122
|
+
if (e.isDirectory()) {
|
|
123
|
+
if (SKIP_DIRS.has(e.name) || (e.name.startsWith('.') && e.name !== '.github')) continue;
|
|
124
|
+
walk(full, cb, state, depth + 1);
|
|
125
|
+
} else if (e.isFile()) {
|
|
126
|
+
if (!rulesFor(e.name)) continue;
|
|
127
|
+
state.files++;
|
|
128
|
+
cb(full);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function readSymbols(full) {
|
|
134
|
+
try {
|
|
135
|
+
const st = fs.statSync(full);
|
|
136
|
+
if (st.size > MAX_FILE_BYTES) return [];
|
|
137
|
+
return extractSymbols(full, fs.readFileSync(full, 'utf8'));
|
|
138
|
+
} catch {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* ---------- repo_symbols ---------- */
|
|
144
|
+
async function repoSymbols(args, ctx) {
|
|
145
|
+
const root = path.resolve(String((ctx && ctx.cwd) || '.'), String(args.path || '.'));
|
|
146
|
+
if (!fs.existsSync(root)) return { ok: false, error: 'yol bulunamadı: ' + root };
|
|
147
|
+
const query = String(args.query || '').trim();
|
|
148
|
+
let re = null;
|
|
149
|
+
if (/^\/.+\/[a-z]*$/i.test(query)) {
|
|
150
|
+
try { re = new RegExp(query.slice(1, query.lastIndexOf('/')), query.slice(query.lastIndexOf('/') + 1)); } catch {}
|
|
151
|
+
}
|
|
152
|
+
const needle = query.toLowerCase();
|
|
153
|
+
const kindF = ['function', 'class', 'method'].includes(String(args.kind)) ? String(args.kind) : null;
|
|
154
|
+
const limit = Math.min(500, Math.max(1, Math.floor(Number(args.limit) || 100)));
|
|
155
|
+
|
|
156
|
+
const state = { files: 0, maxFiles: 4000 };
|
|
157
|
+
const hits = [];
|
|
158
|
+
let scanned = 0;
|
|
159
|
+
walk(root, (full) => {
|
|
160
|
+
scanned++;
|
|
161
|
+
const syms = readSymbols(full);
|
|
162
|
+
for (const s of syms) {
|
|
163
|
+
if (kindF && s.kind !== kindF) continue;
|
|
164
|
+
if (query) {
|
|
165
|
+
const match = re ? re.test(s.name) : s.name.toLowerCase().includes(needle);
|
|
166
|
+
if (!match) continue;
|
|
167
|
+
}
|
|
168
|
+
hits.push({ file: path.relative(root, full).replace(/\\/g, '/'), line: s.line, kind: s.kind, name: s.name });
|
|
169
|
+
}
|
|
170
|
+
}, state);
|
|
171
|
+
hits.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : a.line - b.line));
|
|
172
|
+
return {
|
|
173
|
+
ok: true,
|
|
174
|
+
...(query ? { query } : {}),
|
|
175
|
+
root: path.basename(root) || root,
|
|
176
|
+
scannedFiles: scanned,
|
|
177
|
+
total: hits.length,
|
|
178
|
+
results: hits.slice(0, limit),
|
|
179
|
+
...(hits.length > limit ? { note: `ilk ${limit} sonuç — limit'i artır ya da query daralt` } : {}),
|
|
180
|
+
...(!hits.length && !query ? { note: 'query ver: fonksiyon/sınıf adı, "login" gibi ya da /regex/ biçiminde' } : {}),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/* ---------- repo_map ---------- */
|
|
185
|
+
async function repoMap(args, ctx) {
|
|
186
|
+
const root = path.resolve(String((ctx && ctx.cwd) || '.'), String(args.path || '.'));
|
|
187
|
+
if (!fs.existsSync(root)) return { ok: false, error: 'yol bulunamadı: ' + root };
|
|
188
|
+
const maxFiles = Math.min(2000, Math.max(10, Math.floor(Number(args.max_files) || 300)));
|
|
189
|
+
const perFile = Math.min(30, Math.max(1, Math.floor(Number(args.max_symbols_per_file) || 10)));
|
|
190
|
+
|
|
191
|
+
const state = { files: 0, maxFiles };
|
|
192
|
+
const entries = [];
|
|
193
|
+
let truncated = false;
|
|
194
|
+
walk(root, (full) => {
|
|
195
|
+
if (entries.length >= maxFiles) {
|
|
196
|
+
truncated = true;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const syms = readSymbols(full);
|
|
200
|
+
const rel = path.relative(root, full).replace(/\\/g, '/');
|
|
201
|
+
if (!syms.length) {
|
|
202
|
+
entries.push({ file: rel, symbols: [] });
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const groups = { class: [], function: [], method: [] };
|
|
206
|
+
for (const s of syms) {
|
|
207
|
+
if (groups[s.kind] && groups[s.kind].length < perFile) groups[s.kind].push(s.name);
|
|
208
|
+
}
|
|
209
|
+
entries.push({
|
|
210
|
+
file: rel,
|
|
211
|
+
symbols: [
|
|
212
|
+
...groups.class.map((n) => 'class ' + n),
|
|
213
|
+
...groups.function.map((n) => n + '()'),
|
|
214
|
+
...(groups.method.length ? [groups.method.length + ' method'] : []),
|
|
215
|
+
],
|
|
216
|
+
...(syms.length > perFile * 2 ? { more: syms.length - perFile } : {}),
|
|
217
|
+
});
|
|
218
|
+
}, state);
|
|
219
|
+
entries.sort((a, b) => (a.file < b.file ? -1 : 1));
|
|
220
|
+
return {
|
|
221
|
+
ok: true,
|
|
222
|
+
root: path.basename(root) || root,
|
|
223
|
+
totalFiles: entries.length,
|
|
224
|
+
...(truncated ? { truncated: true, note: `max_files=${maxFiles} — daha büyük harita için artır` } : {}),
|
|
225
|
+
files: entries,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const definitions = [
|
|
230
|
+
{
|
|
231
|
+
type: 'function',
|
|
232
|
+
function: {
|
|
233
|
+
name: 'repo_map',
|
|
234
|
+
description:
|
|
235
|
+
'Lightweight repo overview (no LSP): walks code files and lists each file with its top-level symbols (classes, functions). Use to orient yourself in an unfamiliar project or to pick the right file before reading it. Heavy dirs (node_modules, dist, .git…) are skipped.',
|
|
236
|
+
parameters: {
|
|
237
|
+
type: 'object',
|
|
238
|
+
properties: {
|
|
239
|
+
path: { type: 'string', description: 'Repo/subdirectory root (default: workspace)' },
|
|
240
|
+
max_files: { type: 'number', description: 'Max files in the map (default 300, max 2000)' },
|
|
241
|
+
max_symbols_per_file: { type: 'number', description: 'Max symbols listed per file (default 10)' },
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
type: 'function',
|
|
248
|
+
function: {
|
|
249
|
+
name: 'repo_symbols',
|
|
250
|
+
description:
|
|
251
|
+
'Find where a function/class/method is DEFINED across the repo (lightweight LSP). Query matches symbol names case-insensitively; "/regex/" form is also accepted. Returns file:line locations. Much cheaper than grep when looking for definitions.',
|
|
252
|
+
parameters: {
|
|
253
|
+
type: 'object',
|
|
254
|
+
properties: {
|
|
255
|
+
query: { type: 'string', description: 'Symbol name substring, or /regex/ — empty lists everything (capped)' },
|
|
256
|
+
kind: { type: 'string', enum: ['function', 'class', 'method'], description: 'Filter by symbol kind' },
|
|
257
|
+
path: { type: 'string', description: 'Repo/subdirectory root (default: workspace)' },
|
|
258
|
+
limit: { type: 'number', description: 'Max results (default 100, max 500)' },
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
];
|
|
264
|
+
|
|
265
|
+
module.exports = { definitions, handlers: { repo_map: repoMap, repo_symbols: repoSymbols }, extractSymbols, rulesFor };
|
package/src/agent/tools.js
CHANGED
|
@@ -8,6 +8,9 @@ const { execFile } = require('child_process');
|
|
|
8
8
|
const { spawn } = require('child_process');
|
|
9
9
|
const research = require('./research');
|
|
10
10
|
const searxng = require('./searxng');
|
|
11
|
+
const gittools = require('./gittools');
|
|
12
|
+
const repomap = require('./repomap');
|
|
13
|
+
const xlsxtools = require('./xlsxtools');
|
|
11
14
|
|
|
12
15
|
const MAX_CMD_OUTPUT = 16000;
|
|
13
16
|
const MAX_FILE_CHARS = 200000;
|
|
@@ -1465,6 +1468,10 @@ function readCacheDrop(abs) {
|
|
|
1465
1468
|
}
|
|
1466
1469
|
|
|
1467
1470
|
const definitions = [
|
|
1471
|
+
/* yerleşik modül araçları: git, repo haritası, excel */
|
|
1472
|
+
...gittools.definitions,
|
|
1473
|
+
...repomap.definitions,
|
|
1474
|
+
...xlsxtools.definitions,
|
|
1468
1475
|
{
|
|
1469
1476
|
type: 'function',
|
|
1470
1477
|
function: {
|
|
@@ -1714,6 +1721,15 @@ const definitions = [
|
|
|
1714
1721
|
|
|
1715
1722
|
async function exec(name, args, ctx) {
|
|
1716
1723
|
const cwd = ctx.cwd;
|
|
1724
|
+
/* modül araçları: gittools / repomap / xlsxtools — kendi handler'larında */
|
|
1725
|
+
const modHandler = gittools.handlers[name] || repomap.handlers[name] || xlsxtools.handlers[name];
|
|
1726
|
+
if (modHandler) {
|
|
1727
|
+
try {
|
|
1728
|
+
return JSON.stringify(await modHandler(args, ctx));
|
|
1729
|
+
} catch (e) {
|
|
1730
|
+
return JSON.stringify({ ok: false, error: String((e && e.message) || e) });
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1717
1733
|
try {
|
|
1718
1734
|
switch (name) {
|
|
1719
1735
|
case 'run_command': {
|