wormclaude 1.0.33 → 1.0.34

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/api.js CHANGED
@@ -61,6 +61,8 @@ export async function fetchAccount(config) {
61
61
  plan: String(d.plan ?? 'free'),
62
62
  tokenBalance: Number(d.token_balance ?? 0),
63
63
  trustLevel: Number(d.trust_level ?? 0),
64
+ email: String(d.email ?? ''),
65
+ name: String(d.name ?? ''),
64
66
  };
65
67
  }
66
68
  catch {
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.33';
19
+ export const VERSION = '1.0.34';
package/dist/tui.js CHANGED
@@ -1,11 +1,11 @@
1
- // Kendi renderer (ink YOK) — Milestone 1: saf sohbet.
2
- // Mimari: geçmiş console.log (kalıcı scrollback fare ile kopyalama + doğal kaydırma);
3
- // canlı footer log-update (kendi bölgesini yönetir, resize'da kaskad yok); giriş → readline keypress.
4
- // Renkli kod + markdown: ansi.ts (itemAnsi). WORMCLAUDE_TUI=1 ile çalışır; ink sürümü dokunulmadan kalır.
1
+ // Kendi renderer (ink YOK) — Milestone 1b: scroll-region + sabit-dip giriş kutusu.
2
+ // Mimari: terminal SCROLL REGION (DECSTBM) ile ekran ikiye bölünür üstte içerik (banner/header/
3
+ // mesajlar) kayar, ALTTA giriş kutusu SABİT kalır. İçerik bölgesi tepeden taşınca terminal
4
+ // scrollback'ine gider (fareyle kopyalama). Giriş kutusu mutlak konumla (save/restore imleç)
5
+ // çizilir. Renkli kod + markdown: ansi.ts. WORMCLAUDE_TUI=1 ile çalışır; ink sürümü dokunulmaz.
5
6
  import readline from 'node:readline';
6
- import logUpdate from 'log-update';
7
7
  import stringWidth from 'string-width';
8
- import { loadConfig, streamChat } from './api.js';
8
+ import { loadConfig, streamChat, fetchAccount } from './api.js';
9
9
  import { itemAnsi } from './ansi.js';
10
10
  import { theme, VERSION } from './theme.js';
11
11
  import { cleanModelText } from './textclean.js';
@@ -13,8 +13,8 @@ const RESET = '\x1b[0m';
13
13
  const hex = (h) => { const m = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(h); return m ? `\x1b[38;2;${parseInt(m[1], 16)};${parseInt(m[2], 16)};${parseInt(m[3], 16)}m` : ''; };
14
14
  const paint = (s, c, bold = false) => `${bold ? '\x1b[1m' : ''}${c ? hex(c) : ''}${s}${RESET}`;
15
15
  const cols = () => process.stdout.columns || 80;
16
- // Footer satırını terminal genişliğine ANSI-korumalı kırp (otomatik sarmayı önler log-update
17
- // satır sayımı şaşmaz, küçük terminalde bozulmaz). ANSI escape'leri genişliğe saymaz, korur.
16
+ const rows = () => process.stdout.rows || 24;
17
+ // Satırı genişliğe ANSI-korumalı kırp (sarma olmaz sabit footer şaşmaz).
18
18
  function fit(line, max) {
19
19
  let out = '', w = 0, i = 0;
20
20
  while (i < line.length) {
@@ -26,122 +26,120 @@ function fit(line, max) {
26
26
  continue;
27
27
  }
28
28
  }
29
- const ch = line[i];
30
- const cw = stringWidth(ch) || 1;
29
+ const cw = stringWidth(line[i]) || 1;
31
30
  if (w + cw > max)
32
31
  break;
33
- out += ch;
32
+ out += line[i];
34
33
  w += cw;
35
34
  i++;
36
35
  }
37
36
  return out + RESET;
38
37
  }
39
- // Görünür (ANSI'siz) genişlik
40
38
  const vis = (s) => stringWidth(s.replace(/\x1b\[[0-9;]*m/g, ''));
41
39
  export async function runTui() {
42
40
  const config = loadConfig();
43
- const history = []; // model bağlamı {role, content}
44
- let inputBuf = '';
45
- let busy = false;
46
- let streamPreview = ''; // akış sırasında footer'da gösterilen ham metin
47
- let spin = 0;
41
+ let account = { plan: '', email: '', name: '' };
42
+ const history = [];
43
+ const displayItems = [{ kind: 'banner' }];
44
+ let inputBuf = '', busy = false, streamChars = 0, spin = 0;
48
45
  const SPIN = ['·', '✢', '✳', '✶', '✻', '✽', '✶', '✳', '✢'];
49
- // Tüm görsel öğeler bellekte (resize'da yeni genişlikte yeniden basmak için).
50
- const displayItems = [
51
- { kind: 'banner' },
52
- { kind: 'note', text: `v${VERSION} · özel renderer (deneysel) · /help komutlar · /kopyala panoya` },
53
- ];
54
- // ── Geçmişe kalıcı yaz (scrollback) + belleğe ekle ──
55
- const printItem = (it) => { displayItems.push(it); logUpdate.clear(); process.stdout.write(itemAnsi(it, cols()) + '\n'); renderFooter(); };
56
- // ── Resize: ekranı temizle, HER ŞEYİ o anki genişlikte yeniden bas (banner + kod responsive) ──
57
- function redrawAll() {
58
- logUpdate.clear(); // ÖNCE log-update sayacını sıfırla (yoksa
59
- process.stdout.write('\x1b[2J\x1b[3J\x1b[H'); // sonraki render eraseLines ile banner'ı siler → garble)
60
- for (const it of displayItems)
61
- process.stdout.write(itemAnsi(it, cols()) + '\n');
62
- renderFooter();
46
+ const FOOTER_H = 2; // giriş satırı + ipucu satırı
47
+ // ── Claude tarzı başlık (model/plan/mail/cwd) — responsive ──
48
+ function headerLines(W) {
49
+ const a = paint(' WormClaude ', theme.greyDim) + paint('v' + VERSION, theme.red, true)
50
+ + paint(' · model: ', theme.greyDim) + paint(config.model, theme.redBright)
51
+ + (account.plan ? paint(' · plan: ', theme.greyDim) + paint(account.plan, theme.white) : '');
52
+ const b = paint(' ' + (account.email ? account.email + ' · ' : '') + process.cwd(), theme.greyDim);
53
+ return [a, b].map((l) => fit(l, W));
54
+ }
55
+ // ── Scroll region: üst = içerik (kayar), alt = sabit footer ──
56
+ function setRegion() {
57
+ const bottom = Math.max(1, rows() - FOOTER_H - 1); // footer üstünde 1 boş satır
58
+ process.stdout.write(`\x1b[1;${bottom}r`);
63
59
  }
64
- // ── Canlı footer (log-update yönetir) ──
65
- function renderFooter() {
60
+ // ── Sabit footer en alta mutlak konumla çiz (imleci kaydet/geri yükle → içerik bozulmaz) ──
61
+ function drawFooter() {
66
62
  const W = Math.max(8, cols());
67
- const lines = [];
68
- if (busy && !streamPreview)
69
- lines.push(paint(`${SPIN[spin % SPIN.length]} `, theme.red) + paint('yanıt bekleniyor…', theme.grey));
70
- if (streamPreview) {
71
- const maxTail = Math.max(2, Math.min(12, (process.stdout.rows || 24) - 4)); // ekran boyunu aşma
72
- const tail = streamPreview.split('\n').slice(-maxTail);
73
- lines.push(paint('› ', theme.redBright, true) + paint(tail.shift() || '', theme.white));
74
- for (const l of tail)
75
- lines.push(' ' + paint(l, theme.white));
76
- }
77
- lines.push('');
78
- // Giriş satırı: uzunsa SONU göster (yazdıkça imleç görünür kalsın)
79
- const prompt = paint('❯ ', theme.redBright, true);
63
+ const start = rows() - FOOTER_H + 1;
80
64
  let shown = inputBuf;
81
- const avail = W - 3; // "❯ " + imleç payı
65
+ const avail = W - 3;
82
66
  if (vis(shown) > avail)
83
67
  shown = '…' + shown.slice(-(avail - 1));
84
- lines.push(prompt + shown + (busy ? '' : paint('▌', theme.greyDim)));
85
- lines.push(paint('/kopyala panoya · Ctrl+C çıkış', theme.greyDim));
86
- // Canlı bölge KÜÇÜK tutulur (sadece footer). Büyük pad'li bölge, yukarı kaydırınca
87
- // log-update'in yanlış yeri silmesine input'un kaybolmasına yol açıyordu. Input içeriğin
88
- // hemen altında oturur (üstte 1 boş satır boşluk var).
89
- logUpdate(lines.map((l) => fit(l, W)).join('\n'));
68
+ const inputLine = paint('❯ ', theme.redBright, true) + shown + paint('▌', theme.greyDim);
69
+ const hint = busy
70
+ ? paint(`${SPIN[spin % SPIN.length]} yanıt geliyor… ${streamChars} karakter`, theme.grey)
71
+ : paint(' /kopyala panoya · /help komutlar · Ctrl+C çıkış', theme.greyDim);
72
+ let out = '\x1b7'; // imleci kaydet
73
+ [fit(inputLine, W), fit(hint, W)].forEach((l, i) => { out += `\x1b[${start + i};1H\x1b[2K` + l; });
74
+ out += '\x1b8'; // imleci geri yükle (içerik alanı)
75
+ process.stdout.write(out);
76
+ }
77
+ // ── Her şeyi yeni boyutta yeniden bas (banner/kod responsive); içerik bölgesine ──
78
+ function redrawAll() {
79
+ setRegion();
80
+ process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
81
+ process.stdout.write(headerLines(cols()).join('\n') + '\n');
82
+ for (const it of displayItems)
83
+ process.stdout.write(itemAnsi(it, cols()) + '\n');
84
+ drawFooter();
90
85
  }
91
- // ── Bir sohbet turu (Milestone 1: araç yok, saf metin) ──
86
+ // İçerik öğesini ekle + içerik alanına bas (taşarsa scrollback'e → kopyalanır). Footer sabit kalır.
87
+ const printItem = (it) => { displayItems.push(it); process.stdout.write(itemAnsi(it, cols()) + '\n'); drawFooter(); };
88
+ // ── Bir sohbet turu (M1: araç yok). Akış footer'da karakter sayacı; bitince içeriğe basılır ──
92
89
  async function runTurn(userText) {
93
90
  busy = true;
94
- streamPreview = '';
91
+ streamChars = 0;
95
92
  history.push({ role: 'user', content: userText });
96
- const spinTimer = setInterval(() => { spin++; if (busy && !streamPreview)
97
- renderFooter(); }, 120);
93
+ const timer = setInterval(() => { spin++; if (busy)
94
+ drawFooter(); }, 120);
98
95
  let answer = '';
99
96
  try {
100
97
  for await (const ev of streamChat(history, [], config)) {
101
98
  if (ev.type === 'text') {
102
99
  answer += ev.text;
103
- streamPreview = cleanModelText(answer);
104
- renderFooter();
100
+ streamChars = answer.length;
105
101
  }
106
- else if (ev.type === 'error') {
102
+ else if (ev.type === 'error')
107
103
  answer += `\n[hata: ${ev.error}]`;
108
- }
109
104
  }
110
105
  }
111
106
  catch (e) {
112
107
  answer += `\n[bağlantı hatası: ${e?.message || e}]`;
113
108
  }
114
- clearInterval(spinTimer);
109
+ clearInterval(timer);
115
110
  answer = cleanModelText(answer).trim();
116
111
  busy = false;
117
- streamPreview = '';
118
112
  if (answer) {
119
113
  history.push({ role: 'assistant', content: answer });
120
114
  printItem({ kind: 'assistant', text: answer });
121
115
  }
122
- renderFooter();
116
+ drawFooter();
123
117
  }
124
- // ── Giriş (readline keypress, raw mode) ──
118
+ // ── Kurulum ──
125
119
  readline.emitKeypressEvents(process.stdin);
126
120
  if (process.stdin.isTTY)
127
121
  process.stdin.setRawMode(true);
128
- // TÜM mouse-reporting modlarını kapat (ink oturumundan ?1007h sızmış olabilir). Kapalıyken
129
- // fare sürükleme/seçimi tamamen terminalin işi olur → yukarı sürükleyip seçmek çalışır.
130
122
  try {
131
123
  process.stdout.write('\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?1015l\x1b[?1007l');
132
124
  }
133
125
  catch { }
134
- // Temiz başlangıç + banner'ı o anki genişlikte bas (responsive: geniş→büyük ASCII, dar→tek kelime).
126
+ process.stdout.write('\x1b[?25l'); // gerçek imleci gizle (kendi bloğumuzu çiziyoruz)
135
127
  redrawAll();
136
- const quit = () => { logUpdate.clear(); process.stdout.write('\x1b[2J\x1b[3J\x1b[H'); process.exit(0); };
128
+ fetchAccount(config).then((a) => { if (a) {
129
+ account = a;
130
+ redrawAll();
131
+ } }).catch(() => { });
132
+ const quit = () => { process.stdout.write('\x1b[r\x1b[?25h\x1b[2J\x1b[3J\x1b[H'); process.exit(0); };
133
+ process.on('exit', () => { try {
134
+ process.stdout.write('\x1b[r\x1b[?25h');
135
+ }
136
+ catch { } });
137
137
  let ctrlcAt = 0;
138
138
  process.stdin.on('keypress', (str, key) => {
139
- // Ctrl+C tek başına ÇIKMAZ (Windows'ta seçimi Ctrl+C ile kopyalarken uygulama kapanmasın).
140
- // Giriş varsa temizler; boşsa 2 sn içinde ikinci Ctrl+C ile çıkar.
141
139
  if (key && key.ctrl && key.name === 'c') {
142
140
  if (inputBuf) {
143
141
  inputBuf = '';
144
- renderFooter();
142
+ drawFooter();
145
143
  return;
146
144
  }
147
145
  const now = Date.now();
@@ -157,17 +155,16 @@ export async function runTui() {
157
155
  }
158
156
  if (key && key.name === 'return') {
159
157
  if (busy)
160
- return; // tur sürerken Enter işlemez; yazılan metin durur (type-ahead)
158
+ return; // tur sürerken Enter beklemede; yazılan metin durur (type-ahead)
161
159
  const v = inputBuf.trim();
162
160
  inputBuf = '';
163
161
  if (!v) {
164
- renderFooter();
162
+ drawFooter();
165
163
  return;
166
164
  }
167
165
  if (v === '/cikis' || v === '/exit' || v === '/quit') {
168
166
  quit();
169
167
  }
170
- // /kopyala — son yanıtı OSC52 ile panoya
171
168
  if (v === '/kopyala' || v === '/copy') {
172
169
  const last = [...history].reverse().find((m) => m.role === 'assistant');
173
170
  if (last) {
@@ -182,32 +179,24 @@ export async function runTui() {
182
179
  runTurn(v);
183
180
  return;
184
181
  }
185
- if (key && (key.name === 'backspace')) {
182
+ if (key && key.name === 'backspace') {
186
183
  inputBuf = inputBuf.slice(0, -1);
187
- renderFooter();
184
+ drawFooter();
188
185
  return;
189
186
  }
190
187
  if (key && key.name === 'escape') {
191
188
  inputBuf = '';
192
- renderFooter();
189
+ drawFooter();
193
190
  return;
194
191
  }
195
- // SADECE gerçek yazdırılabilir karakter ekle. Escape/kontrol dizileri (fare raporları, ok
196
- // tuşları, fonksiyon tuşları) girişi BOZMAZ ve footer'ı yeniden çizdirmez → fareyle seçim
197
- // sırasında ekran en alta snap edip seçimi düşürmez.
192
+ // sadece gerçek yazdırılabilir karakter (her zaman cevap üretilirken bile type-ahead)
198
193
  if (str && !key?.ctrl && !key?.meta && !str.startsWith('\x1b') && !/[\x00-\x1f]/.test(str)) {
199
194
  inputBuf += str;
200
- renderFooter();
195
+ drawFooter();
201
196
  }
202
197
  });
203
- // resize → her event'te ANINDA temizle (sürüklerken garbled reflow gösterme), settle sonrası
204
- // (100ms) HER ŞEYİ yeni genişlikte yeniden bas (banner/kod responsive, bozulmaz).
198
+ // resize → settle sonrası her şeyi yeni boyutta yeniden bas (region + banner + footer)
205
199
  let rzTimer = null;
206
- process.stdout.on('resize', () => {
207
- logUpdate.clear();
208
- process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
209
- if (rzTimer)
210
- clearTimeout(rzTimer);
211
- rzTimer = setTimeout(redrawAll, 100);
212
- });
200
+ process.stdout.on('resize', () => { if (rzTimer)
201
+ clearTimeout(rzTimer); rzTimer = setTimeout(redrawAll, 100); });
213
202
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wormclaude",
3
- "version": "1.0.33",
3
+ "version": "1.0.34",
4
4
  "description": "WormClaude CLI - uncensored security+code assistant (ink TUI, Claude-style)",
5
5
  "type": "module",
6
6
  "bin": {