wormclaude 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,851 @@
1
+ #!/usr/bin/env node
2
+ import React, { useState, useRef, useEffect } from 'react';
3
+ import { render, Box, Text, useApp, useInput } from 'ink';
4
+ import TextInput from 'ink-text-input';
5
+ import { theme, VERSION } from './theme.js';
6
+ import { loadConfig, streamChat } from './api.js';
7
+ import { allToolSchemas, executeToolCalls, executeTool, toolLabel, setToolConfig } from './tools.js';
8
+ import { summarizeTools } from './toolSummary.js';
9
+ import { pickTipId, tipText } from './tips.js';
10
+ import { t, cmdDesc, setLang, saveLang, loadLang, getLang } from './i18n.js';
11
+ import { linkify } from './links.js';
12
+ import { recordLearned } from './learn.js';
13
+ import { loadSkills, getSkills, getSkill, buildSkillPrompt } from './skills.js';
14
+ import { COMMANDS, runSlashCommand } from './commands.js';
15
+ import { tasks } from './tasks.js';
16
+ import { connectMcpServers } from './mcp.js';
17
+ import * as usage from './usage.js';
18
+ import { shouldAutoCompact, runCompact, isContextError } from './compact.js';
19
+ import { shouldExtract, triggerMemory } from './memory.js';
20
+ // --- WORMCLAUDE SUBCOMMANDS (TUI oncesi) ---
21
+ const _arg = (process.argv[2] || '').toLowerCase();
22
+ if (_arg === 'login' || _arg === 'logout' || _arg === 'whoami' || _arg === '--version' || _arg === '-v') {
23
+ const auth = await import('./auth.js');
24
+ if (_arg === 'login')
25
+ await auth.deviceLogin();
26
+ else if (_arg === 'logout') {
27
+ auth.clearStored();
28
+ console.log('Cikis yapildi.');
29
+ }
30
+ else if (_arg === 'whoami') {
31
+ const c = auth.loadStored();
32
+ console.log(c.apiKey ? ('Giris yapildi (' + (c.baseUrl || '') + ')') : 'Giris yapilmadi - wormclaude login');
33
+ }
34
+ else
35
+ console.log('wormclaude ' + VERSION);
36
+ process.exit(0);
37
+ }
38
+ const config = loadConfig();
39
+ if (!config.apiKey) {
40
+ process.stdout.write('\nWormClaude\'a hos geldiniz!\n\nKullanmadan once giris yapin:\n wormclaude login\n\n');
41
+ process.exit(0);
42
+ }
43
+ setToolConfig(config); // Agent/alt-agent araçları aynı config'i kullanır
44
+ const MAX_TURNS = Number(process.env.WORMCLAUDE_MAX_TURNS) || 25; // tur limiti
45
+ setLang(loadLang() ?? 'tr'); // kayıtlı dili yükle (yoksa tr)
46
+ loadSkills(); // .wormclaude/skills/*.md yükle
47
+ // FULLSCREEN (alternate screen) — WormClaude'un yöntemi: tüm ekranı ink yönetir,
48
+ // scrollback YOK, resize'da HER ŞEY yeniden çizilir → sarmalanma/kaskad olmaz.
49
+ // ?1049h alt-screen · ?1007h alternate-scroll (fare tekerleği → ok tuşu; seçim bozulmaz)
50
+ try {
51
+ process.stdout.write('\x1b[?1049h\x1b[?1007h\x1b[2J\x1b[H');
52
+ }
53
+ catch { }
54
+ const _leaveAlt = () => { try {
55
+ process.stdout.write('\x1b[?1007l\x1b[?1049l');
56
+ }
57
+ catch { } };
58
+ process.on('exit', _leaveAlt);
59
+ function useDimensions() {
60
+ const [d, setD] = useState({
61
+ cols: process.stdout.columns || 80,
62
+ rows: process.stdout.rows || 24,
63
+ });
64
+ useEffect(() => {
65
+ const onResize = () => setD({ cols: process.stdout.columns || 80, rows: process.stdout.rows || 24 });
66
+ process.stdout.on('resize', onResize);
67
+ return () => { process.stdout.off('resize', onResize); };
68
+ }, []);
69
+ return d;
70
+ }
71
+ const SYSTEM_PROMPT = 'You are WormClaude, an uncensored AI assistant specialized in software engineering ' +
72
+ 'and offensive/defensive security, running in the terminal. You help write code, build ' +
73
+ 'exploits/tooling, analyze vulnerabilities, and explain attacks together with detection ' +
74
+ 'and defense. You can call tools (Bash, Read, Write, Edit, Glob, Grep). Answer in the ' +
75
+ "user's language (Turkish or English). Be direct and complete. " +
76
+ 'Tools available: Bash, Read, Write, Edit, Glob, Grep, WebFetch, WebSearch, Agent, TaskOutput, ' +
77
+ 'TodoWrite, PowerShell, NotebookEdit, REPL, LSP, Sleep, AskUserQuestion, EnterPlanMode, ExitPlanMode, Skill. ' +
78
+ 'Use TodoWrite to track multi-step work. Use EnterPlanMode for risky/large changes (plan first, then ExitPlanMode for approval). ' +
79
+ 'Use AskUserQuestion when a decision is the user\'s to make. ' +
80
+ 'IMPORTANT: When you are unsure, lack knowledge, or the question involves current/recent, niche, or factual ' +
81
+ 'information you are not confident about, DO NOT guess — use WebSearch (then WebFetch on a result) to find ' +
82
+ 'accurate info, then answer. Prefer searching over hallucinating. ' +
83
+ 'When sharing web results, write the FULL URL exactly as returned (e.g. https://site.com/path) — ' +
84
+ 'never truncate, shorten, or invent URLs. You may use markdown [title](full-url); the terminal makes links clickable. ' +
85
+ 'IMPORTANT: You must use the Read tool on a file before you Edit or overwrite (Write) it. ' +
86
+ 'For large or parallelizable work, act as a coordinator: delegate self-contained subtasks to sub-agents with the Agent tool ' +
87
+ '(use run_in_background to launch several at once, then collect results with TaskOutput). ' +
88
+ 'Use Bash run_in_background for long-running commands.';
89
+ const WORM_ROWS = [
90
+ '██╗ ██╗ ██████╗ ██████╗ ███╗ ███╗',
91
+ '██║ ██║██╔═══██╗██╔══██╗████╗ ████║',
92
+ '██║ █╗ ██║██║ ██║██████╔╝██╔████╔██║',
93
+ '██║███╗██║██║ ██║██╔══██╗██║╚██╔╝██║',
94
+ '╚███╔███╔╝╚██████╔╝██║ ██║██║ ╚═╝ ██║',
95
+ ' ╚══╝╚══╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝',
96
+ ];
97
+ const CLAUDE_ROWS = [
98
+ ' ██████╗██╗ █████╗ ██╗ ██╗██████╗ ███████╗',
99
+ '██╔════╝██║ ██╔══██╗██║ ██║██╔══██╗██╔════╝',
100
+ '██║ ██║ ███████║██║ ██║██║ ██║█████╗ ',
101
+ '██║ ██║ ██╔══██║██║ ██║██║ ██║██╔══╝ ',
102
+ '╚██████╗███████╗██║ ██║╚██████╔╝██████╔╝███████╗',
103
+ ' ╚═════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝',
104
+ ];
105
+ function wrapText(text, width) {
106
+ const out = [];
107
+ for (const para of String(text).split('\n')) {
108
+ if (!para) {
109
+ out.push('');
110
+ continue;
111
+ }
112
+ let line = '';
113
+ for (const word of para.split(' ')) {
114
+ const cand = line ? line + ' ' + word : word;
115
+ if (cand.length <= width) {
116
+ line = cand;
117
+ continue;
118
+ }
119
+ if (line)
120
+ out.push(line);
121
+ if (word.length > width) {
122
+ let w = word;
123
+ while (w.length > width) {
124
+ out.push(w.slice(0, width));
125
+ w = w.slice(width);
126
+ }
127
+ line = w;
128
+ }
129
+ else
130
+ line = word;
131
+ }
132
+ out.push(line);
133
+ }
134
+ return out;
135
+ }
136
+ // Tüm öğeleri görsel satır dizisine çevirir (kaydırma satır granülerliğinde).
137
+ function buildLines(items, cols) {
138
+ const lines = [];
139
+ const W = Math.max(24, cols - 2);
140
+ const dim = theme.greyDim, red = theme.red, redB = theme.redBright, white = theme.white;
141
+ for (const it of items) {
142
+ lines.push([]); // üstte boşluk
143
+ if (it.kind === 'banner') {
144
+ const rows = cols >= 88 ? WORM_ROWS.map((w, i) => w + CLAUDE_ROWS[i]) : [...WORM_ROWS, ...CLAUDE_ROWS];
145
+ for (const r of rows)
146
+ lines.push([{ text: r, color: red, bold: true }]);
147
+ lines.push([{ text: ' ' + t('banner.subtitle'), dim: true }]);
148
+ }
149
+ else if (it.kind === 'user') {
150
+ // şeffaf kutu
151
+ const inner = W - 4;
152
+ const wrapped = wrapText('› ' + it.text, inner);
153
+ lines.push([{ text: '╭' + '─'.repeat(W - 2) + '╮', dim: true }]);
154
+ for (const ln of wrapped)
155
+ lines.push([{ text: '│ ', dim: true }, { text: ln.padEnd(inner), color: white }, { text: ' │', dim: true }]);
156
+ lines.push([{ text: '╰' + '─'.repeat(W - 2) + '╯', dim: true }]);
157
+ }
158
+ else if (it.kind === 'assistant') {
159
+ wrapText(it.text, W - 2).forEach((ln, i) => lines.push(i === 0 ? [{ text: '⏺ ', color: redB, bold: true }, { text: ln, color: white }] : [{ text: ' ' + ln, color: white }]));
160
+ }
161
+ else if (it.kind === 'tool') {
162
+ const n = it.result.split('\n').length, chars = it.result.length;
163
+ lines.push([{ text: '⏺ ', color: redB, bold: true }, { text: it.label, color: white }]);
164
+ lines.push([{ text: ' ⎿ ', dim: true }, { text: it.ok ? `${n} satır (${chars} karakter)` : '✗ ' + it.result.slice(0, 120), color: it.ok ? theme.grey : theme.errorRed }]);
165
+ }
166
+ else if (it.kind === 'note') {
167
+ for (const ln of wrapText(it.text, W - 2))
168
+ lines.push([{ text: ln, dim: true }]);
169
+ }
170
+ }
171
+ return lines;
172
+ }
173
+ function Banner() {
174
+ const { cols } = useDimensions();
175
+ const RED = theme.red; // WORM + CLAUDE tek renk (kan kırmızısı)
176
+ // Çok dar: temiz tek kelime (kırpılır, asla bozulmaz)
177
+ if (cols < 46) {
178
+ return (React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
179
+ React.createElement(Text, { color: RED, bold: true, wrap: "truncate" }, "WORMCLAUDE"),
180
+ React.createElement(Text, { color: theme.greyDim, wrap: "truncate" }, t('banner.subtitle'))));
181
+ }
182
+ // Geniş ekran: tek satır WORMCLAUDE (WORM + CLAUDE yan yana, tek renk → düzgün kırpılır)
183
+ if (cols >= 88) {
184
+ return (React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
185
+ WORM_ROWS.map((w, i) => React.createElement(Text, { key: i, color: RED, bold: true, wrap: "truncate" }, w + CLAUDE_ROWS[i])),
186
+ React.createElement(Text, { color: theme.greyDim, wrap: "truncate" }, ` ${t('banner.subtitle')}`)));
187
+ }
188
+ // Normal: WORM üstte, CLAUDE altta (iki blok, tek renk)
189
+ return (React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
190
+ [...WORM_ROWS, ...CLAUDE_ROWS].map((r, i) => React.createElement(Text, { key: i, color: RED, bold: true, wrap: "truncate" }, r)),
191
+ React.createElement(Text, { color: theme.greyDim, wrap: "truncate" }, ` ${t('banner.subtitle')}`)));
192
+ }
193
+ function RenderItem({ item }) {
194
+ if (item.kind === 'banner')
195
+ return React.createElement(Banner, null);
196
+ if (item.kind === 'user') {
197
+ // Kullanıcı mesajı: şeffaf (içi boş) kenarlıklı kutu — Claude Code tarzı
198
+ return (React.createElement(Box, { marginTop: 1, borderStyle: "round", borderColor: theme.greyDim, paddingX: 1 },
199
+ React.createElement(Text, { color: theme.greyDim }, "\u203A "),
200
+ React.createElement(Text, { color: theme.white }, item.text)));
201
+ }
202
+ if (item.kind === 'assistant') {
203
+ return (React.createElement(Box, { marginTop: 1, flexDirection: "row" },
204
+ React.createElement(Text, { color: theme.redBright, bold: true }, "\u23FA "),
205
+ React.createElement(Box, { flexDirection: "column" },
206
+ React.createElement(Text, { color: theme.white }, linkify(item.text)))));
207
+ }
208
+ if (item.kind === 'tool') {
209
+ const lines = item.result.split('\n').length;
210
+ const chars = item.result.length;
211
+ return (React.createElement(Box, { flexDirection: "column", marginTop: 1 },
212
+ React.createElement(Box, null,
213
+ React.createElement(Text, { color: theme.redBright, bold: true }, "\u23FA "),
214
+ React.createElement(Text, { color: theme.white }, item.label)),
215
+ React.createElement(Box, null,
216
+ React.createElement(Text, { color: theme.greyDim }, " \u23BF "),
217
+ React.createElement(Text, { color: item.ok ? theme.grey : theme.errorRed }, item.ok ? `${lines} satır (${chars} karakter)` : `✗ ${item.result.slice(0, 160)}`))));
218
+ }
219
+ if (item.kind === 'note')
220
+ return React.createElement(Box, { marginTop: 1 },
221
+ React.createElement(Text, { color: theme.greyDim }, linkify(item.text)));
222
+ return null;
223
+ }
224
+ const SPINNER_FRAMES = ['·', '✢', '✳', '✶', '✻', '✽', '✶', '✳', '✢'];
225
+ const VERBS = [
226
+ 'Worming', 'Forging', 'Exploiting', 'Crafting', 'Computing', 'Crunching',
227
+ 'Hacking', 'Compiling', 'Scanning', 'Cooking', 'Brewing', 'Conjuring',
228
+ 'Decrypting', 'Injecting', 'Probing', 'Assembling', 'Architecting',
229
+ 'Generating', 'Tinkering', 'Sharpening', 'Churning', 'Pwning',
230
+ ];
231
+ function WormSpinner({ label, tokens }) {
232
+ const [f, setF] = useState(0);
233
+ useEffect(() => {
234
+ const t = setInterval(() => setF((x) => x + 1), 120);
235
+ return () => clearInterval(t);
236
+ }, []);
237
+ return (React.createElement(Box, { marginTop: 1 },
238
+ React.createElement(Text, { color: theme.red },
239
+ SPINNER_FRAMES[f % SPINNER_FRAMES.length],
240
+ " "),
241
+ React.createElement(Text, { color: theme.grey },
242
+ label,
243
+ "\u2026"),
244
+ tokens ? React.createElement(Text, { color: theme.greyDim },
245
+ " \u00B7 ",
246
+ tokens,
247
+ " tokens") : null));
248
+ }
249
+ function StatusLine({ model, ctxTokens }) {
250
+ const pct = Math.min(100, Math.round((ctxTokens / 8192) * 100));
251
+ return (React.createElement(Box, null,
252
+ React.createElement(Text, { color: theme.greyDim },
253
+ ' ',
254
+ model,
255
+ " \u00B7 ~",
256
+ ctxTokens.toLocaleString(),
257
+ " tok \u00B7 ",
258
+ pct,
259
+ "% ba\u011Flam")));
260
+ }
261
+ function App() {
262
+ const { exit } = useApp();
263
+ const [items, setItems] = useState([{ kind: 'banner' }]);
264
+ const [input, setInput] = useState('');
265
+ const [busy, setBusy] = useState(false);
266
+ const [streaming, setStreaming] = useState('');
267
+ const [phase, setPhase] = useState('Düşünüyor');
268
+ const [verb, setVerb] = useState('Worming');
269
+ const [tokens, setTokens] = useState(0);
270
+ const [thinking, setThinking] = useState(true);
271
+ const [ctxTokens, setCtxTokens] = useState(0);
272
+ const [started, setStarted] = useState(false);
273
+ const [trustSel, setTrustSel] = useState(0); // 0=Evet, 1=Hayir
274
+ const [lang, setLangState] = useState(() => loadLang()); // null → dil sor
275
+ const [langSel, setLangSel] = useState(0); // 0=tr 1=en
276
+ const chooseLang = (l) => { setLang(l); saveLang(l); setLangState(l); };
277
+ const [taskPill, setTaskPill] = useState(''); // arka plan görev göstergesi
278
+ const [tipId] = useState(() => pickTipId()); // oturum ipucu (dile göre çözülür)
279
+ const [cmdSel, setCmdSel] = useState(0); // slash menüsü seçili satır
280
+ const [scroll, setScroll] = useState(0); // alttan kaç öğe yukarı kaydırıldı (0 = en alt)
281
+ const [perm, setPerm] = useState(null);
282
+ const [permSel, setPermSel] = useState(0); // 0=Evet 1=Evet(hep) 2=Hayır+yönlendir
283
+ const [permMode, setPermMode] = useState('select');
284
+ const [permFeedback, setPermFeedback] = useState('');
285
+ const abortRef = useRef(null);
286
+ const allowedToolsRef = useRef(new Set()); // oturum boyu izinli araçlar
287
+ const [ask, setAsk] = useState(null);
288
+ const [askSel, setAskSel] = useState(0); // AskUserQuestion seçimi
289
+ const historyRef = useRef([{ role: 'system', content: SYSTEM_PROMPT }]);
290
+ // Acilis: klasore guven sorusu (WormClaude tarzi) — yon tuslari/1-2 sec, Enter onay
291
+ // Dil seçimi (ilk açılış)
292
+ useInput((inp, key) => {
293
+ if (lang !== null)
294
+ return;
295
+ if (key.upArrow || inp === '1')
296
+ setLangSel(0);
297
+ else if (key.downArrow || inp === '2')
298
+ setLangSel(1);
299
+ else if (key.return)
300
+ chooseLang(langSel === 0 ? 'tr' : 'en');
301
+ else if (key.escape)
302
+ exit();
303
+ });
304
+ // Klasör güven sorusu
305
+ useInput((inp, key) => {
306
+ if (lang === null || started)
307
+ return;
308
+ if (key.upArrow || inp === '1')
309
+ setTrustSel(0);
310
+ else if (key.downArrow || inp === '2')
311
+ setTrustSel(1);
312
+ else if (key.return) {
313
+ if (trustSel === 0)
314
+ setStarted(true);
315
+ else
316
+ exit();
317
+ }
318
+ else if (key.escape)
319
+ exit();
320
+ });
321
+ // Soru dialogu (AskUserQuestion / plan onayı)
322
+ useInput((inp, key) => {
323
+ if (!ask)
324
+ return;
325
+ const n = ask.options.length;
326
+ if (key.upArrow)
327
+ setAskSel((s) => (s - 1 + n) % n);
328
+ else if (key.downArrow)
329
+ setAskSel((s) => (s + 1) % n);
330
+ else if (/^[1-9]$/.test(inp) && +inp <= n)
331
+ setAskSel(+inp - 1);
332
+ else if (key.return) {
333
+ ask.resolve(ask.options[askSel]?.label || '');
334
+ setAsk(null);
335
+ }
336
+ else if (key.escape) {
337
+ ask.resolve(ask.options[0]?.label || '');
338
+ setAsk(null);
339
+ }
340
+ });
341
+ // İzin dialogu + çalışırken Esc ile kesme
342
+ useInput((inp, key) => {
343
+ if (ask)
344
+ return;
345
+ if (perm) {
346
+ if (permMode === 'feedback') {
347
+ // feedback metni TextInput ile giriliyor; sadece Esc'i burada yakala
348
+ if (key.escape) {
349
+ perm.resolve({ deny: '' });
350
+ setPerm(null);
351
+ setPermMode('select');
352
+ }
353
+ return;
354
+ }
355
+ if (key.upArrow || inp === '1')
356
+ setPermSel(0);
357
+ else if (inp === '2')
358
+ setPermSel(1);
359
+ else if (key.downArrow || inp === '3')
360
+ setPermSel(2);
361
+ else if (key.return) {
362
+ const sel = permSel;
363
+ if (sel === 2) {
364
+ setPermMode('feedback');
365
+ setPermFeedback('');
366
+ return;
367
+ } // yönlendirme yaz
368
+ if (sel === 1)
369
+ allowedToolsRef.current.add(perm.name);
370
+ perm.resolve('allow');
371
+ setPerm(null);
372
+ }
373
+ else if (key.escape) {
374
+ perm.resolve({ deny: '' });
375
+ setPerm(null);
376
+ }
377
+ return;
378
+ }
379
+ if (busy && key.escape) {
380
+ abortRef.current?.abort();
381
+ }
382
+ });
383
+ // Slash menüsü filtresi (yerleşik komutlar + skill'ler)
384
+ const allCommands = () => [
385
+ ...COMMANDS,
386
+ ...getSkills().map((s) => ({ name: '/' + s.name, desc: s.description })),
387
+ ];
388
+ const cmdFilter = (inp) => {
389
+ const tok = inp.split(' ')[0];
390
+ return allCommands().filter((c) => c.name.startsWith(tok));
391
+ };
392
+ // Slash menüsünde ok tuşlarıyla gezinme (yukarı/aşağı)
393
+ useInput((inp, key) => {
394
+ if (!started || busy || perm)
395
+ return;
396
+ if (!input.startsWith('/'))
397
+ return;
398
+ const n = cmdFilter(input).length;
399
+ if (n === 0)
400
+ return;
401
+ if (key.upArrow)
402
+ setCmdSel((s) => (s - 1 + n) % n);
403
+ else if (key.downArrow)
404
+ setCmdSel((s) => (s + 1) % n);
405
+ });
406
+ // Geçmişte kaydırma: PageUp/PageDown her zaman; boş input'ta ok tuşları (fare
407
+ // tekerleği alternate-scroll ile ok tuşu yollar → tekerlekle kaydırma).
408
+ useInput((_inp, key) => {
409
+ if (!started || perm || ask || lang === null)
410
+ return;
411
+ // satır-temelli; üst sınır render'da off=min(scroll,maxScroll) ile kırpılır
412
+ if (key.pageUp)
413
+ setScroll((s) => s + 10);
414
+ else if (key.pageDown)
415
+ setScroll((s) => Math.max(0, s - 10));
416
+ else if (input === '' && !busy) {
417
+ if (key.upArrow)
418
+ setScroll((s) => s + 3);
419
+ else if (key.downArrow)
420
+ setScroll((s) => Math.max(0, s - 3));
421
+ }
422
+ });
423
+ // Yeni mesaj gelince en alta dön
424
+ useEffect(() => { setScroll(0); }, [items.length]);
425
+ const push = (it) => setItems((prev) => [...prev, it]);
426
+ // Arka plan görevlerini izle → footer pill
427
+ useEffect(() => {
428
+ const update = () => {
429
+ const run = tasks.running();
430
+ const all = tasks.list();
431
+ if (!all.length) {
432
+ setTaskPill('');
433
+ return;
434
+ }
435
+ const byKind = {};
436
+ for (const t of run)
437
+ byKind[t.kind] = (byKind[t.kind] || 0) + 1;
438
+ const parts = Object.entries(byKind).map(([k, n]) => `${n} ${k}`);
439
+ setTaskPill(run.length ? t('pill.running', parts.join(', ')) : t('pill.done', all.length));
440
+ };
441
+ tasks.on('change', update);
442
+ update();
443
+ return () => { tasks.off('change', update); };
444
+ }, []);
445
+ // Başlangıçta MCP sunucularına bağlan (.wormclaude/mcp.json)
446
+ useEffect(() => {
447
+ if (!started)
448
+ return;
449
+ let cancelled = false;
450
+ (async () => {
451
+ const servers = await connectMcpServers();
452
+ if (cancelled || !servers.length)
453
+ return;
454
+ const ok = servers.filter((s) => s.status === 'connected');
455
+ const bad = servers.filter((s) => s.status === 'error');
456
+ const toolCount = ok.reduce((n, s) => n + s.toolNames.length, 0);
457
+ let msg = t('mcp.connected', ok.length, toolCount);
458
+ if (bad.length)
459
+ msg += t('mcp.errors', bad.length, bad.map((b) => b.name).join(', '));
460
+ push({ kind: 'note', text: msg });
461
+ })();
462
+ return () => { cancelled = true; };
463
+ }, [started]);
464
+ async function runAgent(userText, displayText) {
465
+ const ac = new AbortController();
466
+ abortRef.current = ac;
467
+ setBusy(true);
468
+ push({ kind: 'user', text: displayText ?? userText });
469
+ historyRef.current = [...historyRef.current, { role: 'user', content: userText }];
470
+ let iter = 0;
471
+ let reactiveRetried = false;
472
+ let done = false;
473
+ let usedWeb = false; // bu turda web aracı kullanıldı mı (öğrenme için)
474
+ let lastAnswer = ''; // modelin son (sentez) cevabı
475
+ while (iter < MAX_TURNS) {
476
+ if (ac.signal.aborted) {
477
+ push({ kind: 'note', text: t('note.interrupted') });
478
+ done = true;
479
+ break;
480
+ }
481
+ // Oto-compact: bağlam eşiği aşılınca otomatik özetle
482
+ if (shouldAutoCompact(historyRef.current)) {
483
+ setThinking(false);
484
+ setPhase(t('phase.autoCompact'));
485
+ try {
486
+ const { history: nh } = await runCompact(historyRef.current, config);
487
+ historyRef.current = nh;
488
+ push({ kind: 'note', text: t('note.autoCompacted') });
489
+ }
490
+ catch { }
491
+ }
492
+ let assistantText = '';
493
+ let gotCtxError = false;
494
+ setStreaming('');
495
+ setThinking(true);
496
+ setTokens(0);
497
+ setVerb(VERBS[Math.floor(Math.random() * VERBS.length)]);
498
+ let toolCalls = [];
499
+ for await (const ev of streamChat(historyRef.current, allToolSchemas(), config, ac.signal)) {
500
+ if (ev.type === 'text') {
501
+ assistantText += ev.text;
502
+ setStreaming(assistantText);
503
+ setTokens(Math.round(assistantText.length / 4));
504
+ }
505
+ else if (ev.type === 'error') {
506
+ if (isContextError(ev.error))
507
+ gotCtxError = true;
508
+ assistantText += `\n[hata: ${ev.error}]`;
509
+ setStreaming(assistantText);
510
+ }
511
+ else if (ev.type === 'done') {
512
+ toolCalls = ev.toolCalls;
513
+ usage.record(config.model, ev.usage); // billing: token/maliyet kaydı
514
+ }
515
+ }
516
+ setStreaming('');
517
+ // Reactive compact: bağlam taştıysa bir kez özetle ve turu tekrar dene
518
+ if (gotCtxError && !reactiveRetried) {
519
+ reactiveRetried = true;
520
+ push({ kind: 'note', text: t('note.reactive') });
521
+ try {
522
+ const { history: nh } = await runCompact(historyRef.current, config);
523
+ historyRef.current = nh;
524
+ }
525
+ catch { }
526
+ continue; // tur sayılmaz
527
+ }
528
+ if (ac.signal.aborted) {
529
+ if (assistantText.trim()) {
530
+ historyRef.current = [...historyRef.current, { role: 'assistant', content: assistantText.trim() }];
531
+ push({ kind: 'assistant', text: assistantText.trim() });
532
+ }
533
+ push({ kind: 'note', text: t('note.interrupted') });
534
+ done = true;
535
+ break;
536
+ }
537
+ iter++; // gerçek tur
538
+ const assistantMsg = { role: 'assistant', content: assistantText || '' };
539
+ if (toolCalls.length) {
540
+ assistantMsg.tool_calls = toolCalls.map((t) => ({
541
+ id: t.id, type: 'function', function: { name: t.name, arguments: t.args || '{}' },
542
+ }));
543
+ }
544
+ historyRef.current = [...historyRef.current, assistantMsg];
545
+ if (assistantText.trim()) {
546
+ lastAnswer = assistantText.trim();
547
+ push({ kind: 'assistant', text: lastAnswer });
548
+ }
549
+ // Bütçe limiti kontrolü
550
+ if (usage.isOverBudget()) {
551
+ push({ kind: 'note', text: t('note.budget', usage.getBudget()) });
552
+ done = true;
553
+ break;
554
+ }
555
+ if (!toolCalls.length) {
556
+ done = true;
557
+ break;
558
+ }
559
+ // Paralel araç çalıştırma + izin onayı (yazanlar sıralı)
560
+ setThinking(false);
561
+ setPhase(toolCalls.length > 1 ? t('phase.toolsMulti', toolCalls.length) : t('phase.toolRun', toolCalls[0].name));
562
+ const results = await executeToolCalls(toolCalls, {
563
+ onResult: (c, _i, res) => {
564
+ if ((c.name === 'WebSearch' || c.name === 'WebFetch') && res.ok)
565
+ usedWeb = true;
566
+ push({ kind: 'tool', label: toolLabel(c.name, res.args), result: res.output, ok: res.ok });
567
+ },
568
+ confirm: (c, args) => new Promise((resolve) => {
569
+ if (allowedToolsRef.current.has(c.name))
570
+ return resolve('allow');
571
+ setPermSel(0);
572
+ setPerm({ label: toolLabel(c.name, args), name: c.name, resolve });
573
+ }),
574
+ ask: (q) => new Promise((resolve) => {
575
+ setAskSel(0);
576
+ setAsk({ question: q.question, options: q.options.length ? q.options : [{ label: 'OK' }], resolve });
577
+ }),
578
+ });
579
+ // Sonuçları orijinal sırada geçmişe ekle (OpenAI tool sırası şart)
580
+ for (let i = 0; i < toolCalls.length; i++) {
581
+ historyRef.current = [...historyRef.current, {
582
+ role: 'tool', tool_call_id: toolCalls[i].id, content: results[i].output.slice(0, 8000),
583
+ }];
584
+ }
585
+ // Araç-grubu özeti (2+ araçta tek satır etiket)
586
+ if (toolCalls.length >= 2) {
587
+ const sum = await summarizeTools(toolCalls.map((t, i) => ({ name: t.name, args: results[i].args })), config);
588
+ if (sum)
589
+ push({ kind: 'note', text: `⎿ ${sum}` });
590
+ }
591
+ }
592
+ if (!done)
593
+ push({ kind: 'note', text: t('note.maxTurns', MAX_TURNS) });
594
+ abortRef.current = null;
595
+ setCtxTokens(Math.round(JSON.stringify(historyRef.current).length / 4));
596
+ setBusy(false);
597
+ // Öğrenme: web'de arayıp cevap ürettiyse {soru→cevap}'ı eğitim datasına ekle
598
+ if (usedWeb && lastAnswer) {
599
+ const sources = (lastAnswer.match(/https?:\/\/[^\s<>"')\]]+/g) || []).slice(0, 8);
600
+ if (recordLearned(userText, lastAnswer, sources, config)) {
601
+ push({ kind: 'note', text: getLang() === 'en' ? '✎ saved to training data (local + server)' : '✎ eğitim datasına eklendi (yerel + sunucu)' });
602
+ }
603
+ }
604
+ // Oto-hafıza: eşik geçildiyse arka planda hafızayı güncelle
605
+ if (shouldExtract(historyRef.current))
606
+ triggerMemory(historyRef.current, config);
607
+ }
608
+ async function onSubmit(value) {
609
+ let v = value.trim();
610
+ setInput('');
611
+ setCmdSel(0);
612
+ if (!v)
613
+ return;
614
+ if (v.startsWith('/')) {
615
+ // Seçili menü öğesini çalıştır (tam eşleşme yoksa ve argüman yoksa)
616
+ const firstTok = v.split(' ')[0];
617
+ const isExact = COMMANDS.some((c) => c.name === firstTok);
618
+ if (!isExact && !v.includes(' ')) {
619
+ const filtered = cmdFilter(v);
620
+ if (filtered.length)
621
+ v = filtered[Math.min(cmdSel, filtered.length - 1)].name;
622
+ }
623
+ const tok = v.split(' ')[0];
624
+ // /agent ve /multi-agent — tek / çoklu alt-agent
625
+ if (tok === '/agent' || tok === '/multi-agent') {
626
+ const task = v.slice(tok.length).trim();
627
+ const en = getLang() === 'en';
628
+ if (!task) {
629
+ push({ kind: 'note', text: en ? `Usage: ${tok} <task>` : `Kullanım: ${tok} <görev>` });
630
+ return;
631
+ }
632
+ if (tok === '/agent') {
633
+ setBusy(true);
634
+ setThinking(false);
635
+ setPhase('Agent');
636
+ push({ kind: 'note', text: (en ? 'Sub-agent: ' : 'Alt-agent: ') + task.slice(0, 50) });
637
+ try {
638
+ const res = await executeTool('Agent', { description: 'agent', prompt: task });
639
+ push({ kind: 'assistant', text: res.output });
640
+ }
641
+ catch (e) {
642
+ push({ kind: 'note', text: t('note.cmdErr', e?.message || e) });
643
+ }
644
+ setBusy(false);
645
+ }
646
+ else {
647
+ const coord = (en
648
+ ? 'Act as a COORDINATOR. Break this task into independent subtasks and launch them as PARALLEL background sub-agents via the Agent tool with run_in_background=true. Then collect each result with TaskOutput and synthesize a final answer.\n\nTask: '
649
+ : 'KOORDİNATÖR ol. Bu görevi bağımsız alt-görevlere böl ve Agent aracını run_in_background=true ile PARALEL arka plan alt-agent\'ları olarak başlat. Sonra her sonucu TaskOutput ile topla ve nihai cevabı birleştir.\n\nGörev: ') + task;
650
+ runAgent(coord, `${tok} ${task}`);
651
+ }
652
+ return;
653
+ }
654
+ // Skill mi? (yerleşik komut değilse ve .wormclaude/skills'te varsa)
655
+ const builtin = COMMANDS.some((c) => c.name === tok);
656
+ const skill = !builtin ? getSkill(tok.slice(1)) : undefined;
657
+ if (skill) {
658
+ const skillArgs = v.slice(tok.length).trim();
659
+ const prompt = buildSkillPrompt(skill, skillArgs);
660
+ const disp = `/${skill.name}${skillArgs ? ' ' + skillArgs : ''}`;
661
+ if (skill.context === 'fork') {
662
+ setBusy(true);
663
+ setThinking(false);
664
+ setPhase(`Skill: ${skill.name}`);
665
+ push({ kind: 'note', text: `${getLang() === 'en' ? 'Skill (sub-agent): ' : 'Skill (alt-agent): '}${skill.name}` });
666
+ try {
667
+ const res = await executeTool('Agent', { description: skill.name, prompt });
668
+ push({ kind: 'assistant', text: res.output });
669
+ }
670
+ catch (e) {
671
+ push({ kind: 'note', text: t('note.cmdErr', e?.message || e) });
672
+ }
673
+ setBusy(false);
674
+ }
675
+ else {
676
+ runAgent(prompt, disp);
677
+ }
678
+ return;
679
+ }
680
+ const ctx = {
681
+ config,
682
+ getHistory: () => historyRef.current,
683
+ setHistory: (h) => { historyRef.current = h; setCtxTokens(Math.round(JSON.stringify(h).length / 4)); },
684
+ note: (text) => push({ kind: 'note', text }),
685
+ assistant: (text) => push({ kind: 'assistant', text }),
686
+ clearConv: () => { setItems([{ kind: 'banner' }]); historyRef.current = [{ role: 'system', content: SYSTEM_PROMPT }]; },
687
+ exit,
688
+ };
689
+ setBusy(true);
690
+ setThinking(false);
691
+ setPhase(t('phase.cmd', v.split(/\s+/)[0]));
692
+ try {
693
+ await runSlashCommand(v, ctx);
694
+ }
695
+ catch (e) {
696
+ push({ kind: 'note', text: t('note.cmdErr', e?.message || e) });
697
+ }
698
+ setBusy(false);
699
+ return;
700
+ }
701
+ runAgent(v);
702
+ }
703
+ const { cols, rows } = useDimensions();
704
+ // İlk açılış: dil seçimi
705
+ if (lang === null) {
706
+ return (React.createElement(Box, { flexDirection: "column", height: rows, width: cols },
707
+ React.createElement(Banner, null),
708
+ React.createElement(Box, { flexDirection: "column", marginTop: 1, paddingX: 1 },
709
+ React.createElement(Text, { color: theme.redBright, bold: true }, t('lang.title')),
710
+ React.createElement(Box, { flexDirection: "column", marginTop: 1 },
711
+ React.createElement(Text, { color: langSel === 0 ? theme.redBright : theme.grey, bold: langSel === 0 },
712
+ langSel === 0 ? '❯ ' : ' ',
713
+ t('lang.tr')),
714
+ React.createElement(Text, { color: langSel === 1 ? theme.redBright : theme.grey, bold: langSel === 1 },
715
+ langSel === 1 ? '❯ ' : ' ',
716
+ t('lang.en'))),
717
+ React.createElement(Text, { color: theme.greyDim },
718
+ " ",
719
+ t('lang.hint'))),
720
+ React.createElement(Box, { flexGrow: 1 }),
721
+ React.createElement(StatusLine, { model: config.model, ctxTokens: 0 })));
722
+ }
723
+ // Açılış: klasöre güven sorusu (WormClaude tarzı)
724
+ if (!started) {
725
+ return (React.createElement(Box, { flexDirection: "column", height: rows, width: cols },
726
+ React.createElement(Banner, null),
727
+ React.createElement(Box, { flexDirection: "column", marginTop: 1, paddingX: 1 },
728
+ React.createElement(Text, { color: theme.redBright, bold: true }, t('trust.accessing')),
729
+ React.createElement(Text, null, " "),
730
+ React.createElement(Text, { color: theme.white }, process.cwd()),
731
+ React.createElement(Text, null, " "),
732
+ t('trust.check').split('\n').map((line, i) => React.createElement(Text, { key: i, color: theme.grey }, line)),
733
+ React.createElement(Text, null, " "),
734
+ React.createElement(Text, { color: theme.grey }, t('trust.canDo')),
735
+ React.createElement(Box, { flexDirection: "column", marginTop: 1 },
736
+ React.createElement(Text, { color: trustSel === 0 ? theme.redBright : theme.grey, bold: trustSel === 0 },
737
+ trustSel === 0 ? '❯ ' : ' ',
738
+ t('trust.yes')),
739
+ React.createElement(Text, { color: trustSel === 1 ? theme.redBright : theme.grey, bold: trustSel === 1 },
740
+ trustSel === 1 ? '❯ ' : ' ',
741
+ t('trust.no'))),
742
+ React.createElement(Text, { color: theme.greyDim },
743
+ " ",
744
+ t('trust.hint'))),
745
+ React.createElement(Box, { flexGrow: 1 }),
746
+ React.createElement(StatusLine, { model: config.model, ctxTokens: 0 })));
747
+ }
748
+ return (React.createElement(Box, { flexDirection: "column", height: rows, width: cols },
749
+ React.createElement(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden" }, (() => {
750
+ // Satır-temelli kaydırma (Claude tarzı): tüm öğeleri satıra çevir, pencere göster
751
+ const all = buildLines(items, cols);
752
+ const reserve = busy ? 4 : 6;
753
+ const avail = Math.max(4, rows - reserve - (streaming ? 3 : 0) - 2); // -2: göstergeler
754
+ const maxScroll = Math.max(0, all.length - avail);
755
+ const off = Math.min(scroll, maxScroll); // alttan kaç satır yukarı
756
+ const startLine = Math.max(0, all.length - avail - off);
757
+ const view = all.slice(startLine, startLine + avail);
758
+ return (React.createElement(React.Fragment, null,
759
+ startLine > 0 ? React.createElement(Text, { color: theme.greyDim },
760
+ " \u2191 ",
761
+ startLine,
762
+ " sat\u0131r \u00B7 PageUp") : null,
763
+ view.map((segs, i) => (React.createElement(Text, { key: i }, segs.length === 0
764
+ ? ' '
765
+ : segs.map((s, j) => (React.createElement(Text, { key: j, color: s.dim ? theme.greyDim : s.color, bold: s.bold }, linkify(s.text))))))),
766
+ off > 0 ? React.createElement(Text, { color: theme.greyDim },
767
+ " \u2193 ",
768
+ off,
769
+ " sat\u0131r \u00B7 PageDown / Son") : null,
770
+ streaming && off === 0 ? (React.createElement(Box, { flexDirection: "row" },
771
+ React.createElement(Text, { color: theme.redBright, bold: true }, "\u23FA "),
772
+ React.createElement(Box, { flexDirection: "column" },
773
+ React.createElement(Text, { color: theme.white }, streaming)))) : null));
774
+ })()),
775
+ perm ? (React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.red, paddingX: 1, marginTop: 1 },
776
+ React.createElement(Text, { color: theme.redBright, bold: true }, t('perm.title', perm.label)),
777
+ permMode === 'feedback' ? (React.createElement(React.Fragment, null,
778
+ React.createElement(Text, { color: theme.grey }, t('perm.feedbackPrompt')),
779
+ React.createElement(Box, null,
780
+ React.createElement(Text, { color: theme.redBright, bold: true }, "\u276F "),
781
+ React.createElement(TextInput, { value: permFeedback, onChange: setPermFeedback, onSubmit: (val) => { perm.resolve({ deny: val.trim() }); setPerm(null); setPermMode('select'); }, placeholder: t('perm.feedbackPlaceholder') })),
782
+ React.createElement(Text, { color: theme.greyDim }, " Enter g\u00F6nder \u00B7 Esc iptal"))) : (React.createElement(React.Fragment, null,
783
+ React.createElement(Text, { color: permSel === 0 ? theme.redBright : theme.grey, bold: permSel === 0 },
784
+ permSel === 0 ? '❯ ' : ' ',
785
+ t('perm.yes')),
786
+ React.createElement(Text, { color: permSel === 1 ? theme.redBright : theme.grey, bold: permSel === 1 },
787
+ permSel === 1 ? '❯ ' : ' ',
788
+ t('perm.yesAlways')),
789
+ React.createElement(Text, { color: permSel === 2 ? theme.redBright : theme.grey, bold: permSel === 2 },
790
+ permSel === 2 ? '❯ ' : ' ',
791
+ t('perm.no')),
792
+ React.createElement(Text, { color: theme.greyDim },
793
+ " ",
794
+ t('perm.hint')))))) : null,
795
+ ask ? (React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.red, paddingX: 1, marginTop: 1 },
796
+ React.createElement(Text, { color: theme.redBright, bold: true }, ask.question.slice(0, 300)),
797
+ ask.options.map((o, i) => (React.createElement(Text, { key: i, color: askSel === i ? theme.redBright : theme.grey, bold: askSel === i },
798
+ askSel === i ? '❯ ' : ' ',
799
+ i + 1,
800
+ ". ",
801
+ o.label,
802
+ o.description ? ` — ${o.description}` : ''))),
803
+ React.createElement(Text, { color: theme.greyDim },
804
+ " \u2191\u2193 / 1-",
805
+ ask.options.length,
806
+ " se\u00E7 \u00B7 Enter onayla"))) : null,
807
+ busy && !perm && !ask ? (thinking
808
+ ? React.createElement(WormSpinner, { label: verb, tokens: tokens })
809
+ : React.createElement(WormSpinner, { label: phase })) : null,
810
+ taskPill ? (React.createElement(Box, { paddingX: 1 },
811
+ React.createElement(Text, { color: theme.greyDim }, taskPill))) : null,
812
+ !busy ? (React.createElement(Box, { flexDirection: "column" },
813
+ React.createElement(Box, { borderStyle: "round", borderColor: theme.red, paddingX: 1 },
814
+ React.createElement(Text, { color: theme.redBright, bold: true }, "\u276F "),
815
+ React.createElement(TextInput, { value: input, onChange: (val) => { setInput(val); setCmdSel(0); }, onSubmit: onSubmit, placeholder: t('input.placeholder') })),
816
+ input.startsWith('/') ? (() => {
817
+ const filtered = cmdFilter(input);
818
+ if (!filtered.length)
819
+ return React.createElement(Text, { color: theme.greyDim },
820
+ " ",
821
+ t('menu.noCmd'));
822
+ const selIdx = Math.min(cmdSel, filtered.length - 1);
823
+ const WIN = 8;
824
+ const start = Math.max(0, Math.min(selIdx - 3, filtered.length - WIN));
825
+ const view = filtered.slice(start, start + WIN);
826
+ return (React.createElement(Box, { flexDirection: "column", marginTop: 0, paddingX: 1 },
827
+ start > 0 ? React.createElement(Text, { color: theme.greyDim },
828
+ " ",
829
+ t('menu.moreUp', start)) : null,
830
+ view.map((c, k) => {
831
+ const on = start + k === selIdx;
832
+ return (React.createElement(Text, { key: c.name },
833
+ React.createElement(Text, { color: on ? theme.redBright : theme.grey, bold: on },
834
+ on ? '❯ ' : ' ',
835
+ c.name),
836
+ React.createElement(Text, { color: theme.greyDim },
837
+ ' ',
838
+ cmdDesc(c.name) || c.desc)));
839
+ }),
840
+ start + WIN < filtered.length ? React.createElement(Text, { color: theme.greyDim },
841
+ " ",
842
+ t('menu.moreDown', filtered.length - (start + WIN))) : null,
843
+ React.createElement(Text, { color: theme.greyDim },
844
+ " ",
845
+ t('menu.navHint'))));
846
+ })() : (React.createElement(Text, { color: theme.greyDim },
847
+ " \uD83D\uDCA1 ",
848
+ tipText(tipId))),
849
+ React.createElement(StatusLine, { model: config.model, ctxTokens: ctxTokens }))) : null));
850
+ }
851
+ render(React.createElement(App, null));