wormclaude 1.0.14 → 1.0.15

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/agent.js CHANGED
@@ -20,6 +20,7 @@ export async function runAgentLoop(opts) {
20
20
  const maxIters = opts.maxIters ?? 12;
21
21
  let finalText = '';
22
22
  let iter = 0;
23
+ let nudged = false;
23
24
  for (; iter < maxIters; iter++) {
24
25
  if (opts.signal?.aborted)
25
26
  break;
@@ -46,8 +47,16 @@ export async function runAgentLoop(opts) {
46
47
  messages.push(asMsg);
47
48
  if (assistantText.trim())
48
49
  finalText = assistantText.trim();
49
- if (!toolCalls.length)
50
+ if (!toolCalls.length) {
51
+ // Nudge: model ne metin ne araç döndürdüyse bir kez "final sonucu ver" diye dürt.
52
+ if (!assistantText.trim() && !nudged) {
53
+ nudged = true;
54
+ messages.push({ role: 'user', content: 'Lütfen şimdi nihai sonucu metin olarak ver ve araç çağırmayı bırak.' });
55
+ continue;
56
+ }
50
57
  break;
58
+ }
59
+ nudged = false; // ilerleme oldu → nudge sıfırla
51
60
  // Paralel çalıştırma (salt-okunur araçlar eşzamanlı). Sonuçlar orijinal sırada.
52
61
  const results = await executeToolCalls(toolCalls, {
53
62
  onStart: (c) => opts.hooks?.onTool?.(c.name, undefined),
package/dist/api.js CHANGED
@@ -2,6 +2,8 @@
2
2
  // Dayanıklılık: bağlantı kurulumunda exponential backoff + jitter retry.
3
3
  // Ölçüm: usage (token) bilgisini 'done' olayında döndürür (billing için).
4
4
  import { loadStored, DEFAULT_BASE_URL } from './auth.js';
5
+ import { StreamingToolCallParser } from './streamparser.js';
6
+ import { safeJsonStringify } from './safejson.js';
5
7
  export function loadConfig() {
6
8
  const stored = loadStored();
7
9
  return {
@@ -93,7 +95,7 @@ export async function* streamChat(messages, tools, config, signal) {
93
95
  const reader = res.body.getReader();
94
96
  const decoder = new TextDecoder();
95
97
  let buf = '';
96
- const toolAcc = {};
98
+ const toolParser = new StreamingToolCallParser();
97
99
  let usage;
98
100
  while (true) {
99
101
  if (signal?.aborted) {
@@ -147,17 +149,17 @@ export async function* streamChat(messages, tools, config, signal) {
147
149
  if (delta.tool_calls) {
148
150
  for (const tc of delta.tool_calls) {
149
151
  const idx = tc.index ?? 0;
150
- if (!toolAcc[idx])
151
- toolAcc[idx] = { id: tc.id || `call_${idx}`, name: '', args: '' };
152
- if (tc.id)
153
- toolAcc[idx].id = tc.id;
154
- if (tc.function?.name)
155
- toolAcc[idx].name += tc.function.name;
156
- if (tc.function?.arguments)
157
- toolAcc[idx].args += tc.function.arguments;
152
+ toolParser.addChunk(idx, tc.function?.arguments || '', tc.id, tc.function?.name);
158
153
  }
159
154
  }
160
155
  }
161
156
  }
162
- yield { type: 'done', toolCalls: Object.values(toolAcc), usage };
157
+ // Tamamlanan çağrıları sağlam biçimde topla; args'ı GEÇERLİ JSON string olarak emit et
158
+ // (tools.ts tarafındaki JSON.parse artık asla patlamaz — onarım burada yapıldı).
159
+ const toolCalls = toolParser.getCompleted().map((c) => ({
160
+ id: c.id,
161
+ name: c.name,
162
+ args: safeJsonStringify(c.args),
163
+ }));
164
+ yield { type: 'done', toolCalls, usage };
163
165
  }
package/dist/auth.js CHANGED
@@ -37,6 +37,16 @@ function apiOrigin(baseUrl) {
37
37
  }
38
38
  function openBrowser(url) {
39
39
  try {
40
+ // Güvenlik: yalnız http/https aç. (Windows 'start' rastgele şema/dosya açabilir; savunma.)
41
+ let parsed;
42
+ try {
43
+ parsed = new URL(url);
44
+ }
45
+ catch {
46
+ return;
47
+ }
48
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
49
+ return;
40
50
  let child;
41
51
  if (process.platform === 'win32') {
42
52
  child = spawn('cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore' });
package/dist/cli.js CHANGED
@@ -6,6 +6,8 @@ import * as path from 'node:path';
6
6
  import { theme, VERSION } from './theme.js';
7
7
  import { loadConfig, streamChat } from './api.js';
8
8
  import { allToolSchemas, executeToolCalls, executeTool, toolLabel, setToolConfig } from './tools.js';
9
+ import { sanitizeError, sanitizeOutput } from './errorsan.js';
10
+ import { cleanModelText } from './textclean.js';
9
11
  import { summarizeTools } from './toolSummary.js';
10
12
  import { pickTipId, tipText } from './tips.js';
11
13
  import { t, cmdDesc, setLang, saveLang, loadLang, getLang } from './i18n.js';
@@ -535,13 +537,13 @@ function App() {
535
537
  for await (const ev of streamChat(historyRef.current, allToolSchemas(), config, ac.signal)) {
536
538
  if (ev.type === 'text') {
537
539
  assistantText += ev.text;
538
- setStreaming(assistantText);
540
+ setStreaming(cleanModelText(assistantText));
539
541
  setTokens(Math.round(assistantText.length / 4));
540
542
  }
541
543
  else if (ev.type === 'error') {
542
544
  if (isContextError(ev.error))
543
545
  gotCtxError = true;
544
- assistantText += `\n[hata: ${ev.error}]`;
546
+ assistantText += `\n[hata: ${sanitizeError(ev.error)}]`;
545
547
  setStreaming(assistantText);
546
548
  }
547
549
  else if (ev.type === 'done') {
@@ -550,6 +552,8 @@ function App() {
550
552
  }
551
553
  }
552
554
  setStreaming('');
555
+ // Sızan özel-token / tool-call markup'ını bir kez temizle (hem geçmiş hem gösterim için).
556
+ assistantText = cleanModelText(assistantText);
553
557
  // Reactive compact: bağlam taştıysa bir kez özetle ve turu tekrar dene
554
558
  if (gotCtxError && !reactiveRetried) {
555
559
  reactiveRetried = true;
@@ -613,7 +617,7 @@ function App() {
613
617
  onResult: (c, _i, res) => {
614
618
  if ((c.name === 'WebSearch' || c.name === 'WebFetch') && res.ok)
615
619
  usedWeb = true;
616
- push({ kind: 'tool', label: toolLabel(c.name, res.args), result: res.output, ok: res.ok });
620
+ push({ kind: 'tool', label: toolLabel(c.name, res.args), result: sanitizeOutput(res.output), ok: res.ok });
617
621
  },
618
622
  confirm: (c, args) => new Promise((resolve) => {
619
623
  if (allowedToolsRef.current.has(c.name))
@@ -0,0 +1,94 @@
1
+ // Hata/çıktı temizleme: (1) gizli anahtar redaksiyonu, (2) backend kimlik maskeleme (white-label),
2
+ // (3) OpenAI-tarzı JSON hata ayrıştırma, (4) dostça timeout mesajı.
3
+ // Gemini errorSanitizer (sadece provider-maskeleme) + errorHandler'dan uyarlandı; gerçek secret-redaction eklendi.
4
+ const PRODUCT = 'WormClaude';
5
+ // ── (1) Gizli anahtar redaksiyonu ─────────────────────────────────────────────
6
+ // Yalnız NET secret desenleri — kullanıcının normal metnini bozmamak için dar tutuldu.
7
+ const SECRET_PATTERNS = [
8
+ [/\bwc_live_[A-Za-z0-9]{6,}/g, 'wc_live_[REDACTED]'],
9
+ [/\bwc_sk_[A-Za-z0-9]{6,}/g, 'wc_sk_[REDACTED]'],
10
+ [/\bsk-[A-Za-z0-9_-]{16,}/g, 'sk-[REDACTED]'],
11
+ [/\bghp_[A-Za-z0-9]{20,}/g, 'ghp_[REDACTED]'],
12
+ [/\bAKIA[A-Z0-9]{16}\b/g, 'AKIA[REDACTED]'],
13
+ [/\b(Bearer)\s+[A-Za-z0-9._\-]{12,}/gi, '$1 [REDACTED]'],
14
+ // key/secret/token/password [:= veya boşluk] <değer> (env dump, config echo)
15
+ [/\b(api[_-]?key|secret|token|password|passwd|vllm[_-]?key)\b(["']?\s*[:=\s]\s*["']?)([A-Za-z0-9._\-]{6,})/gi,
16
+ '$1$2[REDACTED]'],
17
+ ];
18
+ // Agresif: çıplak uzun-hex anahtarlar (48+ hex). git SHA (40) ve kısa hash'leri KORUR.
19
+ // SADECE hata mesajlarında kullanılır — tool çıktısında değil (git log SHA'larını bozmasın).
20
+ const BARE_HEX = /\b[0-9a-f]{48,}\b/gi;
21
+ export function redactSecrets(text, aggressive = false) {
22
+ if (!text)
23
+ return text;
24
+ let out = String(text);
25
+ for (const [re, rep] of SECRET_PATTERNS)
26
+ out = out.replace(re, rep);
27
+ if (aggressive)
28
+ out = out.replace(BARE_HEX, '[REDACTED]');
29
+ return out;
30
+ }
31
+ // ── (2) Backend kimlik maskeleme (white-label) ────────────────────────────────
32
+ // SADECE backend-kaynaklı metne uygulanır (hata mesajları). Kullanıcının kendi içeriğine UYGULANMAZ
33
+ // (ör. tool çıktısındaki "localhost" kullanıcının kendi dev sunucusu olabilir).
34
+ const BACKEND_PATTERNS = [
35
+ [/\bQwen[0-9]?(?:\.[0-9]+)?-?[A-Za-z0-9.\-]*/gi, PRODUCT],
36
+ [/\bqwen[_-]?\w*/gi, PRODUCT],
37
+ [/\bvllm\b/gi, `${PRODUCT} engine`],
38
+ [/\bhermes\b/gi, `${PRODUCT}`],
39
+ [/\b127\.0\.0\.1(?::\d+)?/g, PRODUCT],
40
+ [/\b0\.0\.0\.0(?::\d+)?/g, PRODUCT],
41
+ [/\blocalhost:(?:8000|8001|8788)\b/gi, PRODUCT],
42
+ ];
43
+ export function maskBackend(text) {
44
+ if (!text)
45
+ return text;
46
+ let out = String(text);
47
+ for (const [re, rep] of BACKEND_PATTERNS)
48
+ out = out.replace(re, rep);
49
+ return out;
50
+ }
51
+ // ── (3) OpenAI-tarzı JSON hata ayrıştırma ─────────────────────────────────────
52
+ function parseApiError(text) {
53
+ const t = (text || '').trim();
54
+ // "HTTP 500: {...}" veya düz JSON gövde içinde {"error":{"message":...}}
55
+ const jsonStart = t.indexOf('{');
56
+ if (jsonStart === -1)
57
+ return text;
58
+ try {
59
+ const obj = JSON.parse(t.slice(jsonStart));
60
+ const msg = obj?.error?.message || obj?.message || obj?.detail;
61
+ if (typeof msg === 'string' && msg.trim()) {
62
+ const prefix = t.slice(0, jsonStart).trim();
63
+ return prefix ? `${prefix} ${msg}` : msg;
64
+ }
65
+ }
66
+ catch { /* JSON değil — olduğu gibi bırak */ }
67
+ return text;
68
+ }
69
+ // ── (4) Dostça timeout mesajı ─────────────────────────────────────────────────
70
+ const TIMEOUT_HINTS = [
71
+ 'timeout', 'timed out', 'connection timeout', 'read timeout',
72
+ 'etimedout', 'esockettimedout', 'deadline exceeded', 'und_err_socket',
73
+ ];
74
+ export function isTimeoutError(text) {
75
+ const t = (text || '').toLowerCase();
76
+ return TIMEOUT_HINTS.some((h) => t.includes(h));
77
+ }
78
+ function friendlyTimeout(text) {
79
+ return `${text}\n • Girdiyi kısaltmayı veya bağlamı sadeleştirmeyi dene.\n • Ağ bağlantını kontrol et.\n • Sorun sürerse birkaç saniye sonra tekrar dene.`;
80
+ }
81
+ // ── Birleşik: kullanıcıya gösterilecek hata ───────────────────────────────────
82
+ export function sanitizeError(input) {
83
+ let text = input instanceof Error ? input.message : String(input ?? '');
84
+ text = parseApiError(text);
85
+ text = maskBackend(text);
86
+ text = redactSecrets(text, true); // hata mesajı → agresif (çıplak hex dahil)
87
+ if (isTimeoutError(text))
88
+ text = friendlyTimeout(text);
89
+ return text.trim();
90
+ }
91
+ // Tool çıktısı için: yalnız gizli anahtar redaksiyonu (kimlik maskeleme YOK — kullanıcının kendi içeriği).
92
+ export function sanitizeOutput(text) {
93
+ return redactSecrets(text);
94
+ }
package/dist/memory.js CHANGED
@@ -2,6 +2,7 @@
2
2
  // Tetik eşikleri ve kilit mekanizması Claude Code'dan uyarlandı.
3
3
  import * as fs from 'node:fs';
4
4
  import * as path from 'node:path';
5
+ import * as os from 'node:os';
5
6
  import { completeText } from './agent.js';
6
7
  import { approxTokens } from './usage.js';
7
8
  import { tasks } from './tasks.js';
@@ -133,6 +134,46 @@ export function dreamTimeGatePassed() {
133
134
  }
134
135
  }
135
136
  export function getMemoryPath() { return MEM_FILE; }
137
+ // ── Açık kayıt: save_memory tool'u (otomatik hafızayı tamamlar) ────────────────
138
+ const MEMORY_HEADER = '## Eklenen Hatıralar';
139
+ /**
140
+ * Modelin/kullanıcının açıkça istediği tek bir kalıcı bilgiyi hafıza dosyasına ekler.
141
+ * Otomatik konsolidasyondan bağımsız; "## Eklenen Hatıralar" başlığı altına "- <fact>" yazar.
142
+ * scope: 'project' → cwd/.wormclaude/memory.md (varsayılan), 'global' → ~/.wormclaude/memory.md.
143
+ */
144
+ export function saveMemoryFact(fact, scope = 'project') {
145
+ const clean = String(fact || '').trim().replace(/^(-+\s*)+/, '').trim();
146
+ if (!clean)
147
+ throw new Error('boş hatıra');
148
+ const dir = scope === 'global'
149
+ ? path.join(os.homedir(), '.wormclaude')
150
+ : MEM_DIR;
151
+ const file = path.join(dir, 'memory.md');
152
+ fs.mkdirSync(dir, { recursive: true });
153
+ let content = '';
154
+ try {
155
+ content = fs.readFileSync(file, 'utf8');
156
+ }
157
+ catch { }
158
+ const item = `- ${clean}`;
159
+ const idx = content.indexOf(MEMORY_HEADER);
160
+ if (idx === -1) {
161
+ const sep = content.length === 0 ? '' : content.endsWith('\n') ? '\n' : '\n\n';
162
+ content += `${sep}${MEMORY_HEADER}\n${item}\n`;
163
+ }
164
+ else {
165
+ // Başlıktan sonraki bölümün sonuna ekle (sonraki ## başlığına kadar)
166
+ const start = idx + MEMORY_HEADER.length;
167
+ let end = content.indexOf('\n## ', start);
168
+ if (end === -1)
169
+ end = content.length;
170
+ const before = content.slice(0, end).replace(/\s+$/, '');
171
+ const after = content.slice(end);
172
+ content = `${before}\n${item}${after.startsWith('\n') ? '' : '\n'}${after}`;
173
+ }
174
+ fs.writeFileSync(file, content);
175
+ return file;
176
+ }
136
177
  // Kalıcı hafızayı başlangıçta context'e yüklemek için oku.
137
178
  // .wormclaude/memory.md (oturumlar arası hafıza) + WORMCLAUDE.md (proje notu).
138
179
  export function loadMemoryContext() {
@@ -0,0 +1,166 @@
1
+ // Qwen-14B'nin bozuk JSON tool-call argümanlarını kurtarmak için dayanıklı JSON yardımcıları.
2
+ // Bağımlılıksız (jsonrepair YOK) — elle onarım stratejileri. Gemini safeJsonParse + fixBooleanCasing'den
3
+ // uyarlandı; WormClaude'a özel sadeleştirildi.
4
+ /**
5
+ * Python-tarzı True/False/None'ı JSON karşılıklarına çevirir (özyinelemeli, string-güvenli).
6
+ * Qwen bazen `{"flag": True}` üretir → geçerli JSON değil.
7
+ */
8
+ export function fixBooleanCasing(text) {
9
+ let out = '';
10
+ let inString = false;
11
+ let escape = false;
12
+ for (let i = 0; i < text.length; i++) {
13
+ const ch = text[i];
14
+ if (inString) {
15
+ out += ch;
16
+ if (escape)
17
+ escape = false;
18
+ else if (ch === '\\')
19
+ escape = true;
20
+ else if (ch === '"')
21
+ inString = false;
22
+ continue;
23
+ }
24
+ if (ch === '"') {
25
+ inString = true;
26
+ out += ch;
27
+ continue;
28
+ }
29
+ // String dışında: Python literal'lerini kelime-sınırında değiştir
30
+ const rest = text.slice(i);
31
+ const m = /^(True|False|None)\b/.exec(rest);
32
+ if (m) {
33
+ out += m[1] === 'True' ? 'true' : m[1] === 'False' ? 'false' : 'null';
34
+ i += m[1].length - 1;
35
+ continue;
36
+ }
37
+ out += ch;
38
+ }
39
+ return out;
40
+ }
41
+ /** String dışındaki trailing virgülleri kaldırır: `{"a":1,}` → `{"a":1}` */
42
+ function stripTrailingCommas(text) {
43
+ let out = '';
44
+ let inString = false;
45
+ let escape = false;
46
+ for (let i = 0; i < text.length; i++) {
47
+ const ch = text[i];
48
+ if (inString) {
49
+ out += ch;
50
+ if (escape)
51
+ escape = false;
52
+ else if (ch === '\\')
53
+ escape = true;
54
+ else if (ch === '"')
55
+ inString = false;
56
+ continue;
57
+ }
58
+ if (ch === '"') {
59
+ inString = true;
60
+ out += ch;
61
+ continue;
62
+ }
63
+ if (ch === ',') {
64
+ // Sonraki anlamlı karakter } veya ] ise virgülü at
65
+ let j = i + 1;
66
+ while (j < text.length && /\s/.test(text[j]))
67
+ j++;
68
+ if (text[j] === '}' || text[j] === ']')
69
+ continue;
70
+ }
71
+ out += ch;
72
+ }
73
+ return out;
74
+ }
75
+ /** Açık kalan string/parantez/köşeli ayraçları kapatır (streaming kesintisi onarımı). */
76
+ function closeOpenStructures(text) {
77
+ let inString = false;
78
+ let escape = false;
79
+ const stack = [];
80
+ for (let i = 0; i < text.length; i++) {
81
+ const ch = text[i];
82
+ if (inString) {
83
+ if (escape)
84
+ escape = false;
85
+ else if (ch === '\\')
86
+ escape = true;
87
+ else if (ch === '"')
88
+ inString = false;
89
+ continue;
90
+ }
91
+ if (ch === '"')
92
+ inString = true;
93
+ else if (ch === '{')
94
+ stack.push('}');
95
+ else if (ch === '[')
96
+ stack.push(']');
97
+ else if (ch === '}' || ch === ']')
98
+ stack.pop();
99
+ }
100
+ let out = text;
101
+ if (inString)
102
+ out += '"';
103
+ while (stack.length)
104
+ out += stack.pop();
105
+ return out;
106
+ }
107
+ /**
108
+ * Bozuk/yarım JSON'u en iyi çabayla parse eder. Sırayla: düz parse → fixBooleanCasing →
109
+ * trailing-comma temizliği → açık yapıları kapatma. Hepsi başarısızsa fallback döner.
110
+ */
111
+ export function safeJsonParse(text, fallback) {
112
+ if (text == null)
113
+ return fallback;
114
+ const raw = String(text).trim();
115
+ if (!raw)
116
+ return fallback;
117
+ // 1) Düz
118
+ try {
119
+ return JSON.parse(raw);
120
+ }
121
+ catch { }
122
+ // 2) Boolean casing
123
+ let repaired = fixBooleanCasing(raw);
124
+ try {
125
+ return JSON.parse(repaired);
126
+ }
127
+ catch { }
128
+ // 3) Trailing virgüller
129
+ repaired = stripTrailingCommas(repaired);
130
+ try {
131
+ return JSON.parse(repaired);
132
+ }
133
+ catch { }
134
+ // 4) Açık yapıları kapat
135
+ repaired = closeOpenStructures(repaired);
136
+ try {
137
+ return JSON.parse(repaired);
138
+ }
139
+ catch { }
140
+ // 5) Pes
141
+ return fallback;
142
+ }
143
+ /**
144
+ * Döngüsel referansları ve hata/undefined değerleri tolere ederek JSON üretir.
145
+ * Asla fırlatmaz; başarısızsa fallback string döner.
146
+ */
147
+ export function safeJsonStringify(value, space, fallback = '"[unserializable]"') {
148
+ const seen = new WeakSet();
149
+ try {
150
+ return JSON.stringify(value, (_k, v) => {
151
+ if (typeof v === 'bigint')
152
+ return v.toString();
153
+ if (v instanceof Error)
154
+ return { name: v.name, message: v.message };
155
+ if (typeof v === 'object' && v !== null) {
156
+ if (seen.has(v))
157
+ return '[Circular]';
158
+ seen.add(v);
159
+ }
160
+ return v;
161
+ }, space);
162
+ }
163
+ catch {
164
+ return fallback;
165
+ }
166
+ }
@@ -0,0 +1,158 @@
1
+ // Streaming tool-call delta'larını dayanıklı biçimde birleştirir.
2
+ // Qwen-14B + vLLM(hermes) parçalı JSON arg, index çakışması, ID'siz devam chunk'ı üretebilir.
3
+ // Gemini streamingToolCallParser'dan uyarlandı; bizim {index, id?, name?, argChunk} delta'mıza göre sade.
4
+ import { safeJsonParse } from './safejson.js';
5
+ export class StreamingToolCallParser {
6
+ buffers = new Map();
7
+ depths = new Map();
8
+ inStrings = new Map();
9
+ escapes = new Map();
10
+ meta = new Map();
11
+ idToIndex = new Map();
12
+ nextIndex = 0;
13
+ /**
14
+ * Bir delta chunk'ı ekler. index streaming yanıtından gelir (çakışabilir); id yeni çağrı başlangıcı.
15
+ * @returns chunk işlendikten sonra o index'te JSON tamamlandıysa parse edilmiş değer.
16
+ */
17
+ addChunk(index, chunk, id, name) {
18
+ let actual = index;
19
+ if (id) {
20
+ if (this.idToIndex.has(id)) {
21
+ actual = this.idToIndex.get(id);
22
+ }
23
+ else {
24
+ // Bu index'te FARKLI id'li, tamamlanmış bir çağrı varsa yeni slot bul
25
+ if (this.buffers.has(index)) {
26
+ const buf = this.buffers.get(index);
27
+ const depth = this.depths.get(index);
28
+ const m = this.meta.get(index);
29
+ if (buf.trim() && depth === 0 && m?.id && m.id !== id) {
30
+ try {
31
+ JSON.parse(buf);
32
+ actual = this.findNextIndex();
33
+ }
34
+ catch { /* yarım → güvenle yeniden kullan */ }
35
+ }
36
+ }
37
+ this.idToIndex.set(id, actual);
38
+ }
39
+ }
40
+ else if (this.buffers.has(index)) {
41
+ // ID'siz devam: bu index tamamlandıysa en son yarım çağrıya yönlendir
42
+ const buf = this.buffers.get(index);
43
+ const depth = this.depths.get(index);
44
+ if (depth === 0 && buf.trim()) {
45
+ try {
46
+ JSON.parse(buf);
47
+ actual = this.findMostRecentIncomplete();
48
+ }
49
+ catch {
50
+ actual = index;
51
+ }
52
+ }
53
+ }
54
+ if (!this.buffers.has(actual)) {
55
+ this.buffers.set(actual, '');
56
+ this.depths.set(actual, 0);
57
+ this.inStrings.set(actual, false);
58
+ this.escapes.set(actual, false);
59
+ this.meta.set(actual, {});
60
+ }
61
+ const m = this.meta.get(actual);
62
+ if (id)
63
+ m.id = id;
64
+ if (name)
65
+ m.name = (m.name || '') + name;
66
+ const newBuffer = this.buffers.get(actual) + chunk;
67
+ this.buffers.set(actual, newBuffer);
68
+ // Derinlik takibi — sadece string DIŞINDAKİ ayraçları say
69
+ let depth = this.depths.get(actual);
70
+ let inString = this.inStrings.get(actual);
71
+ let escape = this.escapes.get(actual);
72
+ for (const ch of chunk) {
73
+ if (!inString) {
74
+ if (ch === '{' || ch === '[')
75
+ depth++;
76
+ else if (ch === '}' || ch === ']')
77
+ depth--;
78
+ }
79
+ if (ch === '"' && !escape)
80
+ inString = !inString;
81
+ escape = ch === '\\' && !escape;
82
+ }
83
+ this.depths.set(actual, depth);
84
+ this.inStrings.set(actual, inString);
85
+ this.escapes.set(actual, escape);
86
+ if (depth === 0 && newBuffer.trim().length > 0) {
87
+ try {
88
+ return { complete: true, value: JSON.parse(newBuffer) };
89
+ }
90
+ catch {
91
+ return { complete: false };
92
+ }
93
+ }
94
+ return { complete: false };
95
+ }
96
+ /** Stream bittiğinde (finish_reason) çağrılır. name'i olan ve buffer'ı dolu tüm çağrıları döndürür. */
97
+ getCompleted() {
98
+ const out = [];
99
+ for (const [index, buffer] of this.buffers.entries()) {
100
+ const m = this.meta.get(index);
101
+ if (!m?.name)
102
+ continue; // isimsiz parça → çağrı değil
103
+ // Boş buffer = argümansız tool (ör. {}). safeJsonParse onarım dener, başarısızsa {} fallback.
104
+ const args = buffer.trim() ? safeJsonParse(buffer, {}) : {};
105
+ out.push({ id: m.id || `call_${index}`, name: m.name, args, index });
106
+ }
107
+ return out;
108
+ }
109
+ reset() {
110
+ this.buffers.clear();
111
+ this.depths.clear();
112
+ this.inStrings.clear();
113
+ this.escapes.clear();
114
+ this.meta.clear();
115
+ this.idToIndex.clear();
116
+ this.nextIndex = 0;
117
+ }
118
+ findNextIndex() {
119
+ while (this.buffers.has(this.nextIndex)) {
120
+ const buf = this.buffers.get(this.nextIndex);
121
+ const depth = this.depths.get(this.nextIndex);
122
+ const m = this.meta.get(this.nextIndex);
123
+ if (!buf.trim() || depth > 0 || !m?.id)
124
+ return this.nextIndex;
125
+ try {
126
+ JSON.parse(buf);
127
+ if (depth === 0) {
128
+ this.nextIndex++;
129
+ continue;
130
+ }
131
+ }
132
+ catch {
133
+ return this.nextIndex;
134
+ }
135
+ this.nextIndex++;
136
+ }
137
+ return this.nextIndex++;
138
+ }
139
+ findMostRecentIncomplete() {
140
+ let maxIndex = -1;
141
+ for (const [index, buffer] of this.buffers.entries()) {
142
+ const depth = this.depths.get(index);
143
+ const m = this.meta.get(index);
144
+ if (m?.id && (depth > 0 || !buffer.trim())) {
145
+ maxIndex = Math.max(maxIndex, index);
146
+ }
147
+ else if (buffer.trim()) {
148
+ try {
149
+ JSON.parse(buffer);
150
+ }
151
+ catch {
152
+ maxIndex = Math.max(maxIndex, index);
153
+ }
154
+ }
155
+ }
156
+ return maxIndex >= 0 ? maxIndex : this.findNextIndex();
157
+ }
158
+ }
@@ -0,0 +1,119 @@
1
+ // İsimli/uzman alt-ajan tanımları + kullanıcı-seviyesi loader.
2
+ // Agent tool'u opsiyonel `subagent_type` ile çağrılınca buradan uzman prompt + tool-kısıtı çözülür.
3
+ // Gemini builtin-agents + subagent-manager'dan uyarlandı; WormClaude güvenlik kimliğine göre.
4
+ import * as fs from 'node:fs';
5
+ import * as os from 'node:os';
6
+ import * as path from 'node:path';
7
+ // ── Gömülü uzman ajanlar ──────────────────────────────────────────────────────
8
+ const BUILTINS = [
9
+ {
10
+ name: 'general-purpose',
11
+ description: 'Karmaşık soruları araştırma, kod arama ve çok adımlı görevler için genel amaçlı ajan.',
12
+ system: 'Sen bir WormClaude alt-ajanısın: tek bir görevi uçtan uca bitirmek için doğmuş otonom bir işçi. ' +
13
+ 'İstenen neyse onu yap — fazlası değil, eksiği değil. Geniş arama için Grep/Glob, bilinen yol için Read kullan. ' +
14
+ 'Gereksiz dosya YARATMA; *.md/README dosyalarını yalnız açıkça istenirse oluştur. ' +
15
+ 'Bu görev dışında hafızan yok. Bitince yaptıklarının ve istenen sonuçların kısa, net bir raporunu döndür. ' +
16
+ 'Yanıtta paylaştığın dosya yolları MUTLAK olmalı; göreli yol kullanma. Emoji kullanma.',
17
+ },
18
+ {
19
+ name: 'security-recon',
20
+ description: 'YETKİLİ güvenlik testlerinde keşif: port/servis/teknoloji tespiti, yüzey analizi, raporlama.',
21
+ system: 'Sen bir WormClaude güvenlik-keşif alt-ajanısın. SADECE yetkili/izinli hedeflerde çalış. ' +
22
+ 'Görevin: pasif ve aktif keşif (port/servis tespiti, teknoloji parmak izi, dizin/endpoint yüzeyi, ' +
23
+ 'sürüm tespiti) yapıp bulgularını yapılandırılmış bir rapor olarak döndürmek. ' +
24
+ 'Yıkıcı işlem, gerçek istismar veya hizmet-dışı bırakma YAPMA — yalnız keşif ve analiz. ' +
25
+ 'Bulguları önem derecesiyle (bilgi/düşük/orta/yüksek) sınıflandır. Komut çıktısını ham bırakma, yorumla. Emoji kullanma.',
26
+ tools: ['Bash', 'Read', 'Grep', 'Glob', 'WebFetch', 'WebSearch', 'TaskOutput'],
27
+ },
28
+ {
29
+ name: 'code-explorer',
30
+ description: 'Büyük kod tabanlarını haritalama: mimari, modül ilişkileri, giriş noktaları.',
31
+ system: 'Sen bir WormClaude kod-keşif alt-ajanısın. Görevin bir kod tabanını hızla haritalamak: ' +
32
+ 'dizin yapısı, ana modüller, giriş noktaları, önemli soyutlamalar ve bağımlılıklar. ' +
33
+ 'Grep/Glob ile geniş tara, Read ile kritik dosyaları incele. Dosya YARATMA/DEĞİŞTİRME. ' +
34
+ 'Sonucu: kısa mimari özet + ilgili dosya yolları (MUTLAK) + kod parçaları olarak döndür. Emoji kullanma.',
35
+ tools: ['Grep', 'Glob', 'Read', 'TaskOutput'],
36
+ },
37
+ ];
38
+ // ── Kullanıcı-seviyesi ajanlar: ~/.wormclaude/agents/*.md ─────────────────────
39
+ function agentsDir() {
40
+ return path.join(os.homedir(), '.wormclaude', 'agents');
41
+ }
42
+ /** Minimal frontmatter ayrıştırma (yaml-parser gelene kadar; name/description/tools + body). */
43
+ function parseAgentFile(content, fallbackName) {
44
+ const m = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/.exec(content);
45
+ if (!m)
46
+ return null;
47
+ const [, fm, body] = m;
48
+ const get = (key) => {
49
+ const r = new RegExp(`^${key}\\s*:\\s*(.+)$`, 'mi').exec(fm);
50
+ return r ? r[1].trim().replace(/^["']|["']$/g, '') : undefined;
51
+ };
52
+ const name = (get('name') || fallbackName).trim();
53
+ const description = get('description') || '';
54
+ const system = body.trim();
55
+ if (!name || !system)
56
+ return null;
57
+ // tools: [A, B] veya A,B
58
+ let tools;
59
+ const rawTools = get('tools');
60
+ if (rawTools) {
61
+ tools = rawTools.replace(/[\[\]]/g, '').split(',').map((s) => s.trim()).filter(Boolean);
62
+ if (!tools.length)
63
+ tools = undefined;
64
+ }
65
+ // İsim guard: rezerve/biçim
66
+ const RESERVED = new Set(['self', 'system', 'user', 'model', 'tool', 'config', 'default']);
67
+ if (!/^[a-zA-Z0-9_-]{2,50}$/.test(name) || RESERVED.has(name.toLowerCase()))
68
+ return null;
69
+ return { name, description, system, tools };
70
+ }
71
+ function loadUserAgents() {
72
+ try {
73
+ const dir = agentsDir();
74
+ if (!fs.existsSync(dir))
75
+ return [];
76
+ return fs.readdirSync(dir)
77
+ .filter((f) => f.endsWith('.md'))
78
+ .map((f) => {
79
+ try {
80
+ const def = parseAgentFile(fs.readFileSync(path.join(dir, f), 'utf8'), f.replace(/\.md$/, ''));
81
+ return def;
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ })
87
+ .filter((d) => d !== null);
88
+ }
89
+ catch {
90
+ return [];
91
+ }
92
+ }
93
+ /** Ajanı isme göre çöz. Öncelik: kullanıcı dosyası > gömülü. Bulunamazsa null. */
94
+ export function resolveSubagent(type) {
95
+ if (!type)
96
+ return null;
97
+ const t = type.trim().toLowerCase();
98
+ const user = loadUserAgents().find((a) => a.name.toLowerCase() === t);
99
+ if (user)
100
+ return user;
101
+ return BUILTINS.find((a) => a.name.toLowerCase() === t) || null;
102
+ }
103
+ /** Tüm kullanılabilir ajanları listele (kullanıcı dosyaları gömülüleri gölgeler). */
104
+ export function listSubagents() {
105
+ const seen = new Set();
106
+ const out = [];
107
+ for (const a of [...loadUserAgents(), ...BUILTINS]) {
108
+ const k = a.name.toLowerCase();
109
+ if (seen.has(k))
110
+ continue;
111
+ seen.add(k);
112
+ out.push(a);
113
+ }
114
+ return out;
115
+ }
116
+ /** Agent tool açıklamasına eklenecek "kullanılabilir tipler" satırı. */
117
+ export function subagentTypesHint() {
118
+ return listSubagents().map((a) => `- ${a.name}: ${a.description}`).join('\n');
119
+ }
@@ -0,0 +1,37 @@
1
+ // Model metnindeki sızıntıları temizler: özel tokenlar + parçalı tool-call markup.
2
+ // Qwen-14B + vLLM(hermes) bazen ham çıktıya kontrol token'ları veya tool-call etiketleri sızdırır.
3
+ // ChatML / Qwen özel token'ları
4
+ const SPECIAL_TOKENS = [
5
+ '<|endoftext|>', '<|im_start|>', '<|im_end|>', '<|im_sep|>',
6
+ '<|endofturn|>', '<|fim_prefix|>', '<|fim_middle|>', '<|fim_suffix|>',
7
+ ];
8
+ // Hermes tool-call markup (metne sızarsa) — bloğun TAMAMINI kaldır.
9
+ const TOOL_CALL_BLOCK = /<tool_call>[\s\S]*?<\/tool_call>/g;
10
+ const TOOL_RESPONSE_BLOCK = /<tool_response>[\s\S]*?<\/tool_response>/g;
11
+ // Açık kalan (kapanışı gelmemiş) tek etiketler
12
+ const STRAY_TOOL_TAGS = /<\/?(tool_call|tool_response)>/g;
13
+ /**
14
+ * Görüntülenecek/saklanacak model metnini temizler. İçeriği bozmadan yalnız
15
+ * sızan kontrol token'larını ve tool-call markup'ını çıkarır.
16
+ */
17
+ export function cleanModelText(text) {
18
+ if (!text)
19
+ return text;
20
+ let out = text;
21
+ out = out.replace(TOOL_CALL_BLOCK, '');
22
+ out = out.replace(TOOL_RESPONSE_BLOCK, '');
23
+ out = out.replace(STRAY_TOOL_TAGS, '');
24
+ for (const tok of SPECIAL_TOKENS) {
25
+ if (out.includes(tok))
26
+ out = out.split(tok).join('');
27
+ }
28
+ return out;
29
+ }
30
+ /**
31
+ * Streaming sırasında güvenli temizlik: yalnız TAM özel-token'ları ve kapanmış blokları
32
+ * temizler — yarım gelen bir etiketi (ör. "<tool_") sonraki chunk'ı bekleyeceği için bırakır.
33
+ * Akan tamponun tamamına uygulanır (parçalı token'ı bölmemek için).
34
+ */
35
+ export function cleanStreamingBuffer(buffer) {
36
+ return cleanModelText(buffer);
37
+ }
package/dist/tools.js CHANGED
@@ -8,6 +8,8 @@ import * as os from 'node:os';
8
8
  import * as path from 'node:path';
9
9
  import { loadConfig } from './api.js';
10
10
  import { runAgentLoop } from './agent.js';
11
+ import { resolveSubagent, subagentTypesHint } from './subagents.js';
12
+ import { saveMemoryFact } from './memory.js';
11
13
  import { tasks } from './tasks.js';
12
14
  import { getMcpToolSchemas, callMcpTool } from './mcp.js';
13
15
  import { getSkills, getSkill, buildSkillPrompt } from './skills.js';
@@ -301,12 +303,13 @@ export const toolSchemas = [
301
303
  type: 'function',
302
304
  function: {
303
305
  name: 'Agent',
304
- description: AGENT_DESCRIPTION,
306
+ description: AGENT_DESCRIPTION + '\n\nAvailable subagent_type values (specialized prompt + restricted tools):\n' + subagentTypesHint(),
305
307
  parameters: {
306
308
  type: 'object',
307
309
  properties: {
308
310
  description: { type: 'string', description: 'A short (3-5 word) description of the task' },
309
311
  prompt: { type: 'string', description: 'The detailed, self-contained task for the sub-agent to perform' },
312
+ subagent_type: { type: 'string', description: 'Optional: specialized agent to use (e.g. general-purpose, security-recon, code-explorer). Selects a tailored system prompt and a restricted tool set. Omit for a general sub-agent.' },
310
313
  run_in_background: { type: 'boolean', description: 'Run the sub-agent in the background and return a task id (default false)' },
311
314
  },
312
315
  required: ['description', 'prompt'],
@@ -327,6 +330,21 @@ export const toolSchemas = [
327
330
  },
328
331
  },
329
332
  },
333
+ {
334
+ type: 'function',
335
+ function: {
336
+ name: 'SaveMemory',
337
+ description: 'Save a single concise fact to long-term memory so it persists across sessions. Use when the user explicitly asks you to remember something, or when you detect a durable preference, decision, project convention, or constraint worth keeping. Keep the fact short and self-contained. Do NOT use for transient or session-only details.',
338
+ parameters: {
339
+ type: 'object',
340
+ properties: {
341
+ fact: { type: 'string', description: 'The specific, self-contained fact to remember (e.g. "User prefers Turkish responses", "Project uses pnpm not npm").' },
342
+ scope: { type: 'string', enum: ['project', 'global'], description: 'project = this project only (default), global = all projects.' },
343
+ },
344
+ required: ['fact'],
345
+ },
346
+ },
347
+ },
330
348
  {
331
349
  type: 'function',
332
350
  function: {
@@ -540,6 +558,7 @@ const TOOL_META = {
540
558
  Scroll: { needsPermission: true, validate: (a) => (a && a.direction ? null : 'direction gerekli') },
541
559
  WebSearch: { readOnly: true, needsPermission: true, validate: (a) => (a && a.query ? null : 'query gerekli') },
542
560
  TodoWrite: { readOnly: true, validate: (a) => (a && Array.isArray(a.todos) ? null : 'todos dizisi gerekli') },
561
+ SaveMemory: { validate: (a) => (a && a.fact && String(a.fact).trim() ? null : 'fact gerekli') },
543
562
  PowerShell: { needsPermission: true, validate: (a) => (a && a.command ? null : 'command gerekli') },
544
563
  NotebookEdit: { needsPermission: true, validate: (a) => (a && a.notebook_path ? null : 'notebook_path gerekli') },
545
564
  REPL: { needsPermission: true, validate: (a) => (a && a.language && a.code ? null : 'language ve code gerekli') },
@@ -664,13 +683,53 @@ export async function executeToolCalls(calls, hooks) {
664
683
  return results;
665
684
  }
666
685
  // Alt-agent'a verilecek araç seti (özyineleme/iç içe agent engellenir).
667
- function subAgentTools() {
668
- return allToolSchemas().filter((t) => t.function.name !== 'Agent' && t.function.name !== 'Skill');
686
+ // allow verilirse yalnız o adlardaki araçlar bırakılır (uzman ajan tool-kısıtı).
687
+ function subAgentTools(allow) {
688
+ let list = allToolSchemas().filter((t) => t.function.name !== 'Agent' && t.function.name !== 'Skill');
689
+ if (allow && allow.length) {
690
+ const set = new Set(allow);
691
+ const filtered = list.filter((t) => set.has(t.function.name));
692
+ if (filtered.length)
693
+ list = filtered; // boş kalırsa kısıtı yok say (güvenli geri dönüş)
694
+ }
695
+ return list;
669
696
  }
670
697
  const SUBAGENT_SYSTEM = 'You are a WormClaude sub-agent: an autonomous worker spawned to complete one specific task. ' +
671
698
  'Use your tools (Bash, Read, Write, Edit, Glob, Grep, WebFetch, TaskOutput) to do the work, ' +
672
699
  'then return a concise final report of what you did and any results requested. ' +
673
700
  'You have no memory beyond this task. Be thorough and finish the task end-to-end.';
701
+ // Levenshtein mesafesi (küçük, dep'siz) — bilinmeyen tool adına en yakın öneriler.
702
+ function levenshtein(a, b) {
703
+ const m = a.length, n = b.length;
704
+ if (!m)
705
+ return n;
706
+ if (!n)
707
+ return m;
708
+ let prev = Array.from({ length: n + 1 }, (_, i) => i);
709
+ let cur = new Array(n + 1);
710
+ for (let i = 1; i <= m; i++) {
711
+ cur[0] = i;
712
+ for (let j = 1; j <= n; j++) {
713
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
714
+ cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + cost);
715
+ }
716
+ [prev, cur] = [cur, prev];
717
+ }
718
+ return prev[n];
719
+ }
720
+ // Bilinmeyen tool adı için " Did you mean 'X'?" önerisi (en yakın ≤3, makul mesafe).
721
+ function suggestTools(unknown) {
722
+ const names = allToolSchemas().map((t) => t.function.name);
723
+ const ranked = names
724
+ .map((n) => ({ n, d: levenshtein(unknown.toLowerCase(), n.toLowerCase()) }))
725
+ .filter((x) => x.d <= Math.max(3, Math.floor(unknown.length / 2)))
726
+ .sort((a, b) => a.d - b.d)
727
+ .slice(0, 3)
728
+ .map((x) => `"${x.n}"`);
729
+ if (!ranked.length)
730
+ return '';
731
+ return ranked.length > 1 ? ` Did you mean one of: ${ranked.join(', ')}?` : ` Did you mean ${ranked[0]}?`;
732
+ }
674
733
  export function toolLabel(name, args) {
675
734
  try {
676
735
  if (name === 'Bash')
@@ -688,9 +747,11 @@ export function toolLabel(name, args) {
688
747
  if (name === 'WebFetch')
689
748
  return `WebFetch(${args.url})`;
690
749
  if (name === 'Agent')
691
- return `Agent(${args.description || ''}${args.run_in_background ? ', bg' : ''})`;
750
+ return `Agent(${args.description || ''}${args.subagent_type ? ':' + args.subagent_type : ''}${args.run_in_background ? ', bg' : ''})`;
692
751
  if (name === 'TaskOutput')
693
752
  return `TaskOutput(${args.task_id})`;
753
+ if (name === 'SaveMemory')
754
+ return `SaveMemory(${String(args.fact || '').slice(0, 50)})`;
694
755
  if (name === 'Skill')
695
756
  return `Skill(${args.name})`;
696
757
  if (name === 'WebSearch')
@@ -755,9 +816,9 @@ function walk(dir, out, depth = 0) {
755
816
  function globToRegex(pattern) {
756
817
  let re = pattern
757
818
  .replace(/[.+^${}()|[\]\\]/g, '\\$&')
758
- .replace(/\*\*/g, '')
819
+ .replace(/\*\*/g, '\u0000')
759
820
  .replace(/\*/g, '[^/\\\\]*')
760
- .replace(//g, '.*')
821
+ .replace(/\u0000/g, '.*')
761
822
  .replace(/\?/g, '.');
762
823
  return new RegExp(re + '$', 'i');
763
824
  }
@@ -839,8 +900,12 @@ export async function executeTool(name, args) {
839
900
  return { ok: true, output: (out || '(no output)').slice(0, 20000) };
840
901
  }
841
902
  if (name === 'Agent') {
903
+ // Uzman ajan seçimi: subagent_type verilirse özel prompt + tool-kısıtı uygulanır.
904
+ const def = resolveSubagent(args.subagent_type);
905
+ const sysPrompt = def ? def.system : SUBAGENT_SYSTEM;
906
+ const subTools = subAgentTools(def?.tools);
842
907
  const subMessages = [
843
- { role: 'system', content: SUBAGENT_SYSTEM },
908
+ { role: 'system', content: sysPrompt },
844
909
  { role: 'user', content: String(args.prompt || '') },
845
910
  ];
846
911
  if (args.run_in_background) {
@@ -850,7 +915,7 @@ export async function executeTool(name, args) {
850
915
  const { finalText } = await runAgentLoop({
851
916
  config: cfg(),
852
917
  messages: subMessages,
853
- tools: subAgentTools(),
918
+ tools: subTools,
854
919
  executeTool,
855
920
  hooks: {
856
921
  onText: (t) => tasks.append(task.id, t),
@@ -870,7 +935,7 @@ export async function executeTool(name, args) {
870
935
  const { finalText } = await runAgentLoop({
871
936
  config: cfg(),
872
937
  messages: subMessages,
873
- tools: subAgentTools(),
938
+ tools: subTools,
874
939
  executeTool,
875
940
  });
876
941
  return { ok: true, output: finalText || '(sub-agent returned no text)' };
@@ -882,6 +947,16 @@ export async function executeTool(name, args) {
882
947
  const body = t.output.slice(-9000) || '(no output yet)';
883
948
  return { ok: true, output: `[${t.id}] ${t.kind} · ${t.status} · ${t.label}\n\n${body}` };
884
949
  }
950
+ if (name === 'SaveMemory') {
951
+ try {
952
+ const scope = args.scope === 'global' ? 'global' : 'project';
953
+ const file = saveMemoryFact(String(args.fact || ''), scope);
954
+ return { ok: true, output: `Hatıra kaydedildi (${scope}): "${String(args.fact).trim()}" → ${file}` };
955
+ }
956
+ catch (e) {
957
+ return { ok: false, output: `Hatıra kaydedilemedi: ${e?.message || e}` };
958
+ }
959
+ }
885
960
  if (name === 'Read') {
886
961
  const fp = args.file_path;
887
962
  if (!fs.existsSync(fp))
@@ -1220,7 +1295,7 @@ export async function executeTool(name, args) {
1220
1295
  }
1221
1296
  if (name.startsWith('mcp__'))
1222
1297
  return await callMcpTool(name, args);
1223
- return { ok: false, output: `Unknown tool: ${name}` };
1298
+ return { ok: false, output: `Unknown tool: ${name}.${suggestTools(name)} Use exactly the registered tool names.` };
1224
1299
  }
1225
1300
  catch (e) {
1226
1301
  return { ok: false, output: `Error: ${e?.message || String(e)}` };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wormclaude",
3
- "version": "1.0.14",
3
+ "version": "1.0.15",
4
4
  "description": "WormClaude CLI - uncensored security+code assistant (ink TUI, Claude-style)",
5
5
  "type": "module",
6
6
  "bin": {