wormclaude 1.0.38 → 1.0.39

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/theme.js CHANGED
@@ -16,4 +16,4 @@ export const theme = {
16
16
  synType: '#a78bfa', // tip/sınıf adları, sabitler
17
17
  synProp: '#e0e0e0', // özellik/anahtar adları
18
18
  };
19
- export const VERSION = '1.0.38';
19
+ export const VERSION = '1.0.39';
package/dist/tui.js CHANGED
@@ -6,6 +6,8 @@
6
6
  import readline from 'node:readline';
7
7
  import stringWidth from 'string-width';
8
8
  import { loadConfig, streamChat, fetchAccount } from './api.js';
9
+ import { allToolSchemas, executeToolCalls, toolLabel, setToolConfig } from './tools.js';
10
+ import { sanitizeOutput } from './errorsan.js';
9
11
  import { itemAnsi } from './ansi.js';
10
12
  import { theme, VERSION } from './theme.js';
11
13
  import { cleanModelText } from './textclean.js';
@@ -43,7 +45,11 @@ export async function runTui() {
43
45
  const displayItems = [{ kind: 'banner' }];
44
46
  let inputBuf = '', busy = false, streamChars = 0, spin = 0;
45
47
  const SPIN = ['·', '✢', '✳', '✶', '✻', '✽', '✶', '✳', '✢'];
46
- const FOOTER_H = 4; // kutu (üst çizgi + giriş + alt çizgi) + ipucu satırı
48
+ const FOOTER_H = 4; // kutu (üst çizgi + giriş/dialog + alt çizgi) + ipucu satırı
49
+ setToolConfig(config); // araçlar (Write/Bash/Edit…) aynı config'i kullansın
50
+ // İzin (araç onayı) durumu
51
+ let perm = null;
52
+ const allowAll = new Set(); // "Tümüne izin" denen araçlar (oturum boyu)
47
53
  // ── Claude tarzı başlık (model/plan/mail/cwd) — responsive ──
48
54
  function headerLines(W) {
49
55
  const a = paint(' WormClaude ', theme.greyDim) + paint('v' + VERSION, theme.red, true)
@@ -61,18 +67,28 @@ export async function runTui() {
61
67
  function drawFooter() {
62
68
  const W = Math.max(8, cols());
63
69
  const start = rows() - FOOTER_H + 1;
64
- let shown = inputBuf;
65
- const avail = W - 3;
66
- if (vis(shown) > avail)
67
- shown = '…' + shown.slice(-(avail - 1));
68
- // İki uzun KIRMIZI çizgi arasında giriş (köşesiz → resize'da bozulmaz). Altında ipucu.
69
70
  const line = paint('─'.repeat(W), theme.red);
70
- const inputLine = paint('❯ ', theme.redBright, true) + paint(shown, theme.white) + paint('▌', theme.greyDim);
71
- const hint = busy
72
- ? paint(`${SPIN[spin % SPIN.length]} yanıt geliyor… ${streamChars} karakter`, theme.grey)
73
- : paint(' /kopyala panoya · /help komutlar · Ctrl+C çıkış', theme.greyDim);
71
+ let body;
72
+ if (perm) {
73
+ // İzin dialogu: araç adı + Evet/Tümü/Hayır
74
+ const q = paint(' ', theme.redBright, true) + paint(perm.label, theme.white) + paint(' çalıştırılsın mı?', theme.grey);
75
+ const opts = paint(' [E] Evet', theme.redBright, true) + paint(' · ', theme.greyDim)
76
+ + paint('[T] Tümüne izin', theme.grey) + paint(' · ', theme.greyDim) + paint('[H] Hayır', theme.grey);
77
+ body = [line, q, opts, line];
78
+ }
79
+ else {
80
+ let shown = inputBuf;
81
+ const avail = W - 3;
82
+ if (vis(shown) > avail)
83
+ shown = '…' + shown.slice(-(avail - 1));
84
+ const inputLine = paint('❯ ', theme.redBright, true) + paint(shown, theme.white) + paint('▌', theme.greyDim);
85
+ const hint = busy
86
+ ? paint(`${SPIN[spin % SPIN.length]} çalışıyor… ${streamChars ? streamChars + ' karakter' : ''}`, theme.grey)
87
+ : paint(' /kopyala panoya · /help komutlar · Ctrl+C çıkış', theme.greyDim);
88
+ body = [line, inputLine, line, hint];
89
+ }
74
90
  let out = '\x1b7'; // imleci kaydet
75
- [fit(line, W), fit(inputLine, W), fit(line, W), fit(hint, W)].forEach((l, i) => { out += `\x1b[${start + i};1H\x1b[2K` + l; });
91
+ body.forEach((l, i) => { out += `\x1b[${start + i};1H\x1b[2K` + fit(l, W); });
76
92
  out += '\x1b8'; // imleci geri yükle (içerik alanı)
77
93
  process.stdout.write(out);
78
94
  }
@@ -90,34 +106,71 @@ export async function runTui() {
90
106
  }
91
107
  // İçerik öğesini ekle + içerik alanına bas (taşarsa scrollback'e → kopyalanır). Footer sabit kalır.
92
108
  const printItem = (it) => { displayItems.push(it); process.stdout.write(itemAnsi(it, cols()) + '\n'); drawFooter(); };
93
- // ── Bir sohbet turu (M1: araç yok). Akış footer'da karakter sayacı; bitince içeriğe basılır ──
109
+ // ── Agent döngüsü (M2: araçlar + izin). Model tool çağırırsa izin sorulup çalıştırılır,
110
+ // sonuç geçmişe eklenip döngü devam eder; tool yoksa biter. ──
94
111
  async function runTurn(userText) {
95
112
  busy = true;
96
113
  streamChars = 0;
97
114
  history.push({ role: 'user', content: userText });
98
- const timer = setInterval(() => { spin++; if (busy)
115
+ const timer = setInterval(() => { spin++; if (busy && !perm)
99
116
  drawFooter(); }, 120);
100
- let answer = '';
117
+ let lastSig = '', sameCount = 0;
101
118
  try {
102
- for await (const ev of streamChat(history, [], config)) {
103
- if (ev.type === 'text') {
104
- answer += ev.text;
105
- streamChars = answer.length;
119
+ for (let iter = 0; iter < 25; iter++) {
120
+ streamChars = 0;
121
+ let answer = '';
122
+ let toolCalls = [];
123
+ for await (const ev of streamChat(history, allToolSchemas(), config)) {
124
+ if (ev.type === 'text') {
125
+ answer += ev.text;
126
+ streamChars = answer.length;
127
+ drawFooter();
128
+ }
129
+ else if (ev.type === 'done')
130
+ toolCalls = ev.toolCalls || [];
131
+ else if (ev.type === 'error')
132
+ answer += `\n[hata: ${ev.error}]`;
133
+ }
134
+ answer = cleanModelText(answer).trim();
135
+ const am = { role: 'assistant', content: answer || '' };
136
+ if (toolCalls.length)
137
+ am.tool_calls = toolCalls.map((t) => ({ id: t.id, type: 'function', function: { name: t.name, arguments: t.args || '{}' } }));
138
+ history.push(am);
139
+ if (answer)
140
+ printItem({ kind: 'assistant', text: answer });
141
+ if (!toolCalls.length)
142
+ break;
143
+ // takılma koruması: aynı araç-çağrısı 3 kez üst üste → dur
144
+ const sig = JSON.stringify(toolCalls.map((tc) => tc.name + ':' + (tc.args || '')));
145
+ if (sig === lastSig)
146
+ sameCount++;
147
+ else {
148
+ sameCount = 0;
149
+ lastSig = sig;
150
+ }
151
+ if (sameCount >= 2) {
152
+ printItem({ kind: 'note', text: 'Aynı adım tekrarlandı, döngü önlemek için durduruldu.' });
153
+ break;
106
154
  }
107
- else if (ev.type === 'error')
108
- answer += `\n[hata: ${ev.error}]`;
155
+ const results = await executeToolCalls(toolCalls, {
156
+ confirm: (c, args) => new Promise((resolve) => {
157
+ if (allowAll.has(c.name))
158
+ return resolve('allow');
159
+ perm = { label: toolLabel(c.name, args), name: c.name, resolve };
160
+ drawFooter();
161
+ }),
162
+ onResult: (c, _i, res) => printItem({ kind: 'tool', label: toolLabel(c.name, res.args), result: sanitizeOutput(res.output), ok: res.ok }),
163
+ });
164
+ for (let i = 0; i < toolCalls.length; i++)
165
+ history.push({ role: 'tool', tool_call_id: toolCalls[i].id, content: (results[i].output || '').slice(0, 8000) });
109
166
  }
110
167
  }
111
168
  catch (e) {
112
- answer += `\n[bağlantı hatası: ${e?.message || e}]`;
169
+ printItem({ kind: 'note', text: `[bağlantı hatası: ${e?.message || e}]` });
113
170
  }
114
171
  clearInterval(timer);
115
- answer = cleanModelText(answer).trim();
116
172
  busy = false;
117
- if (answer) {
118
- history.push({ role: 'assistant', content: answer });
119
- printItem({ kind: 'assistant', text: answer });
120
- }
173
+ perm = null;
121
174
  drawFooter();
122
175
  }
123
176
  // ── Kurulum ──
@@ -141,6 +194,34 @@ export async function runTui() {
141
194
  catch { } });
142
195
  let ctrlcAt = 0;
143
196
  process.stdin.on('keypress', (str, key) => {
197
+ // İzin dialogu aktifse: E=Evet, T=Tümüne izin, H/Esc=Hayır (Ctrl+C de Hayır)
198
+ if (perm) {
199
+ const k = (str || '').toLowerCase();
200
+ const r = perm.resolve, nm = perm.name;
201
+ if (key && key.ctrl && key.name === 'c') {
202
+ perm = null;
203
+ r({ deny: 'kullanıcı iptal etti' });
204
+ drawFooter();
205
+ return;
206
+ }
207
+ if (k === 'e' || k === '1' || (key && key.name === 'return')) {
208
+ perm = null;
209
+ r('allow');
210
+ drawFooter();
211
+ }
212
+ else if (k === 't' || k === '2') {
213
+ allowAll.add(nm);
214
+ perm = null;
215
+ r('allow');
216
+ drawFooter();
217
+ }
218
+ else if (k === 'h' || k === '3' || (key && key.name === 'escape')) {
219
+ perm = null;
220
+ r({ deny: '' });
221
+ drawFooter();
222
+ }
223
+ return;
224
+ }
144
225
  if (key && key.ctrl && key.name === 'c') {
145
226
  if (inputBuf) {
146
227
  inputBuf = '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wormclaude",
3
- "version": "1.0.38",
3
+ "version": "1.0.39",
4
4
  "description": "WormClaude CLI - uncensored security+code assistant (ink TUI, Claude-style)",
5
5
  "type": "module",
6
6
  "bin": {