wormclaude 1.0.33 → 1.0.35

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.35';
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,123 @@ 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
+ const W = cols();
82
+ // Sıra: BANNER (+ alt yazı) → model/plan/mail/cwd başlığı → mesajlar.
83
+ process.stdout.write(itemAnsi(displayItems[0], W) + '\n'); // banner (displayItems[0])
84
+ process.stdout.write(headerLines(W).join('\n') + '\n'); // banner ALTINA: model/sürüm/plan/mail/cwd
85
+ for (let i = 1; i < displayItems.length; i++)
86
+ process.stdout.write(itemAnsi(displayItems[i], W) + '\n');
87
+ drawFooter();
90
88
  }
91
- // ── Bir sohbet turu (Milestone 1: araç yok, saf metin) ──
89
+ // İçerik öğesini ekle + içerik alanına bas (taşarsa scrollback'e → kopyalanır). Footer sabit kalır.
90
+ const printItem = (it) => { displayItems.push(it); process.stdout.write(itemAnsi(it, cols()) + '\n'); drawFooter(); };
91
+ // ── Bir sohbet turu (M1: araç yok). Akış footer'da karakter sayacı; bitince içeriğe basılır ──
92
92
  async function runTurn(userText) {
93
93
  busy = true;
94
- streamPreview = '';
94
+ streamChars = 0;
95
95
  history.push({ role: 'user', content: userText });
96
- const spinTimer = setInterval(() => { spin++; if (busy && !streamPreview)
97
- renderFooter(); }, 120);
96
+ const timer = setInterval(() => { spin++; if (busy)
97
+ drawFooter(); }, 120);
98
98
  let answer = '';
99
99
  try {
100
100
  for await (const ev of streamChat(history, [], config)) {
101
101
  if (ev.type === 'text') {
102
102
  answer += ev.text;
103
- streamPreview = cleanModelText(answer);
104
- renderFooter();
103
+ streamChars = answer.length;
105
104
  }
106
- else if (ev.type === 'error') {
105
+ else if (ev.type === 'error')
107
106
  answer += `\n[hata: ${ev.error}]`;
108
- }
109
107
  }
110
108
  }
111
109
  catch (e) {
112
110
  answer += `\n[bağlantı hatası: ${e?.message || e}]`;
113
111
  }
114
- clearInterval(spinTimer);
112
+ clearInterval(timer);
115
113
  answer = cleanModelText(answer).trim();
116
114
  busy = false;
117
- streamPreview = '';
118
115
  if (answer) {
119
116
  history.push({ role: 'assistant', content: answer });
120
117
  printItem({ kind: 'assistant', text: answer });
121
118
  }
122
- renderFooter();
119
+ drawFooter();
123
120
  }
124
- // ── Giriş (readline keypress, raw mode) ──
121
+ // ── Kurulum ──
125
122
  readline.emitKeypressEvents(process.stdin);
126
123
  if (process.stdin.isTTY)
127
124
  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
125
  try {
131
126
  process.stdout.write('\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?1015l\x1b[?1007l');
132
127
  }
133
128
  catch { }
134
- // Temiz başlangıç + banner'ı o anki genişlikte bas (responsive: geniş→büyük ASCII, dar→tek kelime).
129
+ process.stdout.write('\x1b[?25l'); // gerçek imleci gizle (kendi bloğumuzu çiziyoruz)
135
130
  redrawAll();
136
- const quit = () => { logUpdate.clear(); process.stdout.write('\x1b[2J\x1b[3J\x1b[H'); process.exit(0); };
131
+ fetchAccount(config).then((a) => { if (a) {
132
+ account = a;
133
+ redrawAll();
134
+ } }).catch(() => { });
135
+ const quit = () => { process.stdout.write('\x1b[r\x1b[?25h\x1b[2J\x1b[3J\x1b[H'); process.exit(0); };
136
+ process.on('exit', () => { try {
137
+ process.stdout.write('\x1b[r\x1b[?25h');
138
+ }
139
+ catch { } });
137
140
  let ctrlcAt = 0;
138
141
  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
142
  if (key && key.ctrl && key.name === 'c') {
142
143
  if (inputBuf) {
143
144
  inputBuf = '';
144
- renderFooter();
145
+ drawFooter();
145
146
  return;
146
147
  }
147
148
  const now = Date.now();
@@ -157,17 +158,16 @@ export async function runTui() {
157
158
  }
158
159
  if (key && key.name === 'return') {
159
160
  if (busy)
160
- return; // tur sürerken Enter işlemez; yazılan metin durur (type-ahead)
161
+ return; // tur sürerken Enter beklemede; yazılan metin durur (type-ahead)
161
162
  const v = inputBuf.trim();
162
163
  inputBuf = '';
163
164
  if (!v) {
164
- renderFooter();
165
+ drawFooter();
165
166
  return;
166
167
  }
167
168
  if (v === '/cikis' || v === '/exit' || v === '/quit') {
168
169
  quit();
169
170
  }
170
- // /kopyala — son yanıtı OSC52 ile panoya
171
171
  if (v === '/kopyala' || v === '/copy') {
172
172
  const last = [...history].reverse().find((m) => m.role === 'assistant');
173
173
  if (last) {
@@ -182,32 +182,24 @@ export async function runTui() {
182
182
  runTurn(v);
183
183
  return;
184
184
  }
185
- if (key && (key.name === 'backspace')) {
185
+ if (key && key.name === 'backspace') {
186
186
  inputBuf = inputBuf.slice(0, -1);
187
- renderFooter();
187
+ drawFooter();
188
188
  return;
189
189
  }
190
190
  if (key && key.name === 'escape') {
191
191
  inputBuf = '';
192
- renderFooter();
192
+ drawFooter();
193
193
  return;
194
194
  }
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.
195
+ // sadece gerçek yazdırılabilir karakter (her zaman cevap üretilirken bile type-ahead)
198
196
  if (str && !key?.ctrl && !key?.meta && !str.startsWith('\x1b') && !/[\x00-\x1f]/.test(str)) {
199
197
  inputBuf += str;
200
- renderFooter();
198
+ drawFooter();
201
199
  }
202
200
  });
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).
201
+ // resize → settle sonrası her şeyi yeni boyutta yeniden bas (region + banner + footer)
205
202
  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
- });
203
+ process.stdout.on('resize', () => { if (rzTimer)
204
+ clearTimeout(rzTimer); rzTimer = setTimeout(redrawAll, 100); });
213
205
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wormclaude",
3
- "version": "1.0.33",
3
+ "version": "1.0.35",
4
4
  "description": "WormClaude CLI - uncensored security+code assistant (ink TUI, Claude-style)",
5
5
  "type": "module",
6
6
  "bin": {