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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "beast-agent",
3
3
  "productName": "Beast Agent",
4
- "version": "2.6.1",
4
+ "version": "2.6.3",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -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
+ };
@@ -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 };
@@ -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': {