wormclaude 1.0.15 → 1.0.17
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 +20 -0
- package/dist/atmention.js +117 -0
- package/dist/cli.js +51 -6
- package/dist/commands.js +52 -8
- package/dist/markdown.js +221 -0
- package/dist/program.js +36 -0
- package/dist/theme.js +1 -1
- package/dist/tools.js +43 -4
- package/package.json +3 -2
package/dist/api.js
CHANGED
|
@@ -12,6 +12,26 @@ export function loadConfig() {
|
|
|
12
12
|
model: process.env.WORMCLAUDE_MODEL || stored.model || 'wormclaude',
|
|
13
13
|
};
|
|
14
14
|
}
|
|
15
|
+
// Giriş yapan kullanıcının hesabı (plan + kalan token + güven seviyesi). Hata olursa null.
|
|
16
|
+
export async function fetchAccount(config) {
|
|
17
|
+
try {
|
|
18
|
+
const r = await fetch(`${config.baseUrl}/account`, {
|
|
19
|
+
headers: { ...(config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {}) },
|
|
20
|
+
signal: AbortSignal.timeout(8000),
|
|
21
|
+
});
|
|
22
|
+
if (!r.ok)
|
|
23
|
+
return null;
|
|
24
|
+
const d = await r.json();
|
|
25
|
+
return {
|
|
26
|
+
plan: String(d.plan ?? 'free'),
|
|
27
|
+
tokenBalance: Number(d.token_balance ?? 0),
|
|
28
|
+
trustLevel: Number(d.trust_level ?? 0),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
15
35
|
// ── Retry/backoff (api/withRetry.ts'den uyarlandı) ────────────────────────────
|
|
16
36
|
const MAX_RETRIES = Number(process.env.WORMCLAUDE_MAX_RETRIES) || 5;
|
|
17
37
|
const BASE_DELAY_MS = 500;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Sohbet girişindeki @<yol> referanslarını çözer: dosya içeriğini / dizin listesini mesaja enjekte eder.
|
|
2
|
+
// Gemini atCommandProcessor parser'ından uyarlandı; sadeleştirildi (data-file/workspace yok). bashCwd'ye göreli çözer.
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { getBashCwd } from './tools.js';
|
|
6
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', '.wormclaude', 'venv', '.next', 'build']);
|
|
7
|
+
const MAX_FILE_BYTES = 256 * 1024;
|
|
8
|
+
const MAX_INJECT_CHARS = 60000;
|
|
9
|
+
/** Metindeki kaçışsız @<yol>'ları ayrıştırır (boşluk/noktalamada biter, '.' uzantı-farkında). */
|
|
10
|
+
export function parseAtPaths(text) {
|
|
11
|
+
const out = [];
|
|
12
|
+
let i = 0;
|
|
13
|
+
while (i < text.length) {
|
|
14
|
+
// sonraki kaçışsız @
|
|
15
|
+
let at = -1;
|
|
16
|
+
for (let j = i; j < text.length; j++) {
|
|
17
|
+
if (text[j] === '@' && (j === 0 || text[j - 1] !== '\\')) {
|
|
18
|
+
at = j;
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (at === -1)
|
|
23
|
+
break;
|
|
24
|
+
let end = at + 1;
|
|
25
|
+
let esc = false;
|
|
26
|
+
while (end < text.length) {
|
|
27
|
+
const c = text[end];
|
|
28
|
+
if (esc)
|
|
29
|
+
esc = false;
|
|
30
|
+
else if (c === '\\')
|
|
31
|
+
esc = true;
|
|
32
|
+
else if (/[\s,;!?()[\]{}]/.test(c))
|
|
33
|
+
break;
|
|
34
|
+
else if (c === '.') {
|
|
35
|
+
const n = text[end + 1] || '';
|
|
36
|
+
if (n === '' || /\s/.test(n))
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
end++;
|
|
40
|
+
}
|
|
41
|
+
const raw = text.slice(at + 1, end).replace(/\\(.)/g, '$1');
|
|
42
|
+
if (raw)
|
|
43
|
+
out.push(raw);
|
|
44
|
+
i = end;
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
function fuzzyFind(root, name, depth = 0, budget = { n: 0 }) {
|
|
49
|
+
if (depth > 4 || budget.n > 3000)
|
|
50
|
+
return null;
|
|
51
|
+
let entries;
|
|
52
|
+
try {
|
|
53
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
// önce bu dizindeki dosyalar
|
|
59
|
+
for (const e of entries) {
|
|
60
|
+
budget.n++;
|
|
61
|
+
if (e.isFile() && e.name.toLowerCase().includes(name.toLowerCase()))
|
|
62
|
+
return path.join(root, e.name);
|
|
63
|
+
}
|
|
64
|
+
for (const e of entries) {
|
|
65
|
+
if (e.isDirectory() && !SKIP_DIRS.has(e.name) && !e.name.startsWith('.')) {
|
|
66
|
+
const hit = fuzzyFind(path.join(root, e.name), name, depth + 1, budget);
|
|
67
|
+
if (hit)
|
|
68
|
+
return hit;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* @-mention'ları çözer. Dosya bulunursa içeriği, dizinse listesi mesaj sonuna eklenir.
|
|
75
|
+
* @returns { augmented: modele gidecek (içerik eklenmiş) metin, files: çözülen yollar }
|
|
76
|
+
*/
|
|
77
|
+
export function resolveAtMentions(text) {
|
|
78
|
+
const paths = parseAtPaths(text);
|
|
79
|
+
if (!paths.length)
|
|
80
|
+
return { augmented: text, files: [] };
|
|
81
|
+
const cwd = getBashCwd();
|
|
82
|
+
const blocks = [];
|
|
83
|
+
const found = [];
|
|
84
|
+
for (const p of paths) {
|
|
85
|
+
let target = path.resolve(cwd, p);
|
|
86
|
+
if (!fs.existsSync(target)) {
|
|
87
|
+
const hit = fuzzyFind(cwd, path.basename(p));
|
|
88
|
+
if (!hit)
|
|
89
|
+
continue;
|
|
90
|
+
target = hit;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const st = fs.statSync(target);
|
|
94
|
+
const rel = path.relative(cwd, target) || target;
|
|
95
|
+
if (st.isDirectory()) {
|
|
96
|
+
const list = fs.readdirSync(target).slice(0, 100).join('\n');
|
|
97
|
+
blocks.push(`--- Dizin: ${rel} ---\n${list}`);
|
|
98
|
+
found.push(rel);
|
|
99
|
+
}
|
|
100
|
+
else if (st.size <= MAX_FILE_BYTES) {
|
|
101
|
+
const content = fs.readFileSync(target, 'utf8').slice(0, MAX_INJECT_CHARS);
|
|
102
|
+
blocks.push(`--- Dosya: ${rel} ---\n${content}`);
|
|
103
|
+
found.push(rel);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
blocks.push(`--- ${rel}: çok büyük (${Math.round(st.size / 1024)}KB), atlandı ---`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch { /* erişilemedi, atla */ }
|
|
110
|
+
}
|
|
111
|
+
if (!blocks.length)
|
|
112
|
+
return { augmented: text, files: [] };
|
|
113
|
+
return {
|
|
114
|
+
augmented: `${text}\n\n[Kullanıcının @ ile referansladığı içerik:]\n${blocks.join('\n\n')}`,
|
|
115
|
+
files: found,
|
|
116
|
+
};
|
|
117
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -4,10 +4,13 @@ import { render, Box, Text, useApp, useInput } from 'ink';
|
|
|
4
4
|
import TextInput from 'ink-text-input';
|
|
5
5
|
import * as path from 'node:path';
|
|
6
6
|
import { theme, VERSION } from './theme.js';
|
|
7
|
-
import { loadConfig, streamChat } from './api.js';
|
|
7
|
+
import { loadConfig, streamChat, fetchAccount } from './api.js';
|
|
8
|
+
import { tier } from './program.js';
|
|
8
9
|
import { allToolSchemas, executeToolCalls, executeTool, toolLabel, setToolConfig } from './tools.js';
|
|
9
10
|
import { sanitizeError, sanitizeOutput } from './errorsan.js';
|
|
10
11
|
import { cleanModelText } from './textclean.js';
|
|
12
|
+
import { MarkdownDisplay } from './markdown.js';
|
|
13
|
+
import { resolveAtMentions } from './atmention.js';
|
|
11
14
|
import { summarizeTools } from './toolSummary.js';
|
|
12
15
|
import { pickTipId, tipText } from './tips.js';
|
|
13
16
|
import { t, cmdDesc, setLang, saveLang, loadLang, getLang } from './i18n.js';
|
|
@@ -221,8 +224,8 @@ function RenderItem({ item }) {
|
|
|
221
224
|
if (item.kind === 'assistant') {
|
|
222
225
|
return (React.createElement(Box, { marginTop: 1, flexDirection: "row" },
|
|
223
226
|
React.createElement(Text, { color: theme.redBright, bold: true }, "\u23FA "),
|
|
224
|
-
React.createElement(Box, { flexDirection: "column" },
|
|
225
|
-
React.createElement(
|
|
227
|
+
React.createElement(Box, { flexDirection: "column", flexGrow: 1 },
|
|
228
|
+
React.createElement(MarkdownDisplay, { text: item.text }))));
|
|
226
229
|
}
|
|
227
230
|
if (item.kind === 'tool') {
|
|
228
231
|
const lines = item.result.split('\n').length;
|
|
@@ -265,13 +268,14 @@ function WormSpinner({ label, tokens }) {
|
|
|
265
268
|
tokens,
|
|
266
269
|
" tokens") : null));
|
|
267
270
|
}
|
|
268
|
-
function StatusLine({ model, ctxTokens }) {
|
|
271
|
+
function StatusLine({ model, ctxTokens, trust = 0 }) {
|
|
269
272
|
const max = Number(process.env.WORMCLAUDE_CTX) || 12288;
|
|
270
273
|
const pct = Math.min(100, Math.round((ctxTokens / max) * 100));
|
|
271
274
|
const filled = Math.round(pct / 10);
|
|
272
275
|
const bar = '█'.repeat(filled) + '░'.repeat(10 - filled);
|
|
273
276
|
const barColor = pct > 85 ? theme.errorRed : pct > 60 ? theme.redBright : theme.grey;
|
|
274
277
|
const cwd = path.basename(process.cwd()) || process.cwd();
|
|
278
|
+
const badge = tier(trust).badge;
|
|
275
279
|
return (React.createElement(Box, null,
|
|
276
280
|
React.createElement(Text, { color: theme.greyDim },
|
|
277
281
|
' ',
|
|
@@ -279,6 +283,9 @@ function StatusLine({ model, ctxTokens }) {
|
|
|
279
283
|
React.createElement(Text, { color: theme.red, bold: true },
|
|
280
284
|
"v",
|
|
281
285
|
VERSION),
|
|
286
|
+
badge ? React.createElement(Text, { color: trust >= 3 ? theme.green : theme.redBright, bold: true },
|
|
287
|
+
" \u25C6 ",
|
|
288
|
+
badge) : null,
|
|
282
289
|
React.createElement(Text, { color: theme.greyDim },
|
|
283
290
|
" \u00B7 ",
|
|
284
291
|
model,
|
|
@@ -313,6 +320,7 @@ function App() {
|
|
|
313
320
|
const [tipId] = useState(() => pickTipId()); // oturum ipucu (dile göre çözülür)
|
|
314
321
|
const [cmdSel, setCmdSel] = useState(0); // slash menüsü seçili satır
|
|
315
322
|
const [scroll, setScroll] = useState(0); // alttan kaç öğe yukarı kaydırıldı (0 = en alt)
|
|
323
|
+
const [trustLevel, setTrustLevel] = useState(0); // Doğrulanmış Araştırmacı Programı seviyesi
|
|
316
324
|
const [perm, setPerm] = useState(null);
|
|
317
325
|
const [permSel, setPermSel] = useState(0); // 0=Evet 1=Evet(hep) 2=Hayır+yönlendir
|
|
318
326
|
const [permMode, setPermMode] = useState('select');
|
|
@@ -458,6 +466,15 @@ function App() {
|
|
|
458
466
|
});
|
|
459
467
|
// Yeni mesaj gelince en alta dön
|
|
460
468
|
useEffect(() => { setScroll(0); }, [items.length]);
|
|
469
|
+
// Başlangıçta güven seviyesini (Doğrulanmış Araştırmacı Programı) çek → StatusLine rozeti
|
|
470
|
+
useEffect(() => {
|
|
471
|
+
if (!started)
|
|
472
|
+
return;
|
|
473
|
+
let alive = true;
|
|
474
|
+
fetchAccount(config).then((a) => { if (alive && a)
|
|
475
|
+
setTrustLevel(a.trustLevel); }).catch(() => { });
|
|
476
|
+
return () => { alive = false; };
|
|
477
|
+
}, [started]);
|
|
461
478
|
const push = (it) => setItems((prev) => [...prev, it]);
|
|
462
479
|
// Arka plan görevlerini izle → footer pill
|
|
463
480
|
useEffect(() => {
|
|
@@ -665,6 +682,29 @@ function App() {
|
|
|
665
682
|
setCmdSel(0);
|
|
666
683
|
if (!v)
|
|
667
684
|
return;
|
|
685
|
+
// ! shell modu — LLM'siz doğrudan shell komutu (cmdsec ile gate'li). Sonucu modele bağlam olarak ekle.
|
|
686
|
+
if (v.startsWith('!') && v.length > 1) {
|
|
687
|
+
const cmd = v.slice(1).trim();
|
|
688
|
+
if (!cmd)
|
|
689
|
+
return;
|
|
690
|
+
push({ kind: 'user', text: v });
|
|
691
|
+
setBusy(true);
|
|
692
|
+
setThinking(false);
|
|
693
|
+
setPhase(t('phase.toolRun', 'shell'));
|
|
694
|
+
try {
|
|
695
|
+
const res = await executeTool('Bash', { command: cmd });
|
|
696
|
+
push({ kind: 'tool', label: `! ${cmd.slice(0, 60)}`, result: sanitizeOutput(res.output), ok: res.ok });
|
|
697
|
+
historyRef.current = [...historyRef.current, {
|
|
698
|
+
role: 'user',
|
|
699
|
+
content: `Şu shell komutunu çalıştırdım:\n\`\`\`\n${cmd}\n\`\`\`\nÇıktı:\n\`\`\`\n${(res.output || '').slice(0, 4000)}\n\`\`\``,
|
|
700
|
+
}];
|
|
701
|
+
}
|
|
702
|
+
catch (e) {
|
|
703
|
+
push({ kind: 'note', text: t('note.cmdErr', e?.message || e) });
|
|
704
|
+
}
|
|
705
|
+
setBusy(false);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
668
708
|
if (v.startsWith('/')) {
|
|
669
709
|
// Seçili menü öğesini çalıştır (tam eşleşme yoksa ve argüman yoksa)
|
|
670
710
|
const firstTok = v.split(' ')[0];
|
|
@@ -760,7 +800,12 @@ function App() {
|
|
|
760
800
|
setBusy(false);
|
|
761
801
|
return;
|
|
762
802
|
}
|
|
763
|
-
|
|
803
|
+
// @dosya mention — referanslanan dosya/dizin içeriğini modele enjekte et, kullanıcıya orijinali göster
|
|
804
|
+
const at = resolveAtMentions(v);
|
|
805
|
+
if (at.files.length)
|
|
806
|
+
runAgent(at.augmented, v);
|
|
807
|
+
else
|
|
808
|
+
runAgent(v);
|
|
764
809
|
}
|
|
765
810
|
const { cols, rows } = useDimensions();
|
|
766
811
|
// İlk açılış: dil seçimi
|
|
@@ -908,6 +953,6 @@ function App() {
|
|
|
908
953
|
})() : (React.createElement(Text, { color: theme.greyDim },
|
|
909
954
|
" \uD83D\uDCA1 ",
|
|
910
955
|
tipText(tipId))),
|
|
911
|
-
React.createElement(StatusLine, { model: config.model, ctxTokens: ctxTokens }))) : null));
|
|
956
|
+
React.createElement(StatusLine, { model: config.model, ctxTokens: ctxTokens, trust: trustLevel }))) : null));
|
|
912
957
|
}
|
|
913
958
|
render(React.createElement(App, null));
|
package/dist/commands.js
CHANGED
|
@@ -12,6 +12,9 @@ import { cmdDesc, setLang, saveLang, getLang } from './i18n.js';
|
|
|
12
12
|
import { loadSkills, getSkills, getSkillsDir, installSkill, updateSkill, removeSkill, getRegistry } from './skills.js';
|
|
13
13
|
import { getApprovedCommands, approveCommands, unapproveCommands, clearApproved } from './cmdsec.js';
|
|
14
14
|
import { isLearnEnabled, setLearnEnabled, getLearnFile, getLearnCount } from './learn.js';
|
|
15
|
+
import { saveMemoryFact, getMemoryPath, loadMemoryContext } from './memory.js';
|
|
16
|
+
import { fetchAccount } from './api.js';
|
|
17
|
+
import { programText } from './program.js';
|
|
15
18
|
export const COMMANDS = [
|
|
16
19
|
{ name: '/help', desc: 'komutları ve ipuçlarını göster' },
|
|
17
20
|
{ name: '/clear', desc: 'sohbeti ve geçmişi temizle' },
|
|
@@ -24,7 +27,7 @@ export const COMMANDS = [
|
|
|
24
27
|
{ name: '/commit', desc: 'stage edilmiş değişiklikleri modelle commit et' },
|
|
25
28
|
{ name: '/review', desc: 'mevcut diff’i modelle incele' },
|
|
26
29
|
{ name: '/init', desc: 'projeyi tarayıp WORMCLAUDE.md üret' },
|
|
27
|
-
{ name: '/memory', desc: '
|
|
30
|
+
{ name: '/memory', desc: 'hafızayı göster / ekle / temizle (/memory <metin> · --global · temizle)' },
|
|
28
31
|
{ name: '/doctor', desc: 'sistem ve backend sağlık kontrolü' },
|
|
29
32
|
{ name: '/lang', desc: 'arayüz dilini değiştir (tr/en)' },
|
|
30
33
|
{ name: '/skills', desc: 'skill\'leri listele / indir / yeniden yükle' },
|
|
@@ -38,6 +41,7 @@ export const COMMANDS = [
|
|
|
38
41
|
{ name: '/learn', desc: 'web-öğrenme datasını göster / aç-kapa (eğitim için)' },
|
|
39
42
|
{ name: '/copy', desc: 'son yaniti panoya kopyala (/copy all = tum sohbet)' },
|
|
40
43
|
{ name: '/izinler', desc: 'onayli shell komutlarini yonet (list/add/remove/clear)' },
|
|
44
|
+
{ name: '/program', desc: 'Doğrulanmış Araştırmacı Programı — güven seviyeni göster' },
|
|
41
45
|
{ name: '/export', desc: 'sohbeti dosyaya kaydet' },
|
|
42
46
|
{ name: '/resume', desc: 'en son kaydedilen oturumu yükle' },
|
|
43
47
|
{ name: '/quit', desc: 'çıkış' },
|
|
@@ -245,6 +249,11 @@ export async function runSlashCommand(input, ctx) {
|
|
|
245
249
|
return true;
|
|
246
250
|
}
|
|
247
251
|
case '/init': {
|
|
252
|
+
const initDest = path.join(process.cwd(), 'WORMCLAUDE.md');
|
|
253
|
+
if (fs.existsSync(initDest) && fs.readFileSync(initDest, 'utf8').trim() && (arg || '').trim() !== 'force') {
|
|
254
|
+
ctx.note('WORMCLAUDE.md zaten var ve dolu. Üzerine yazmak için: /init force');
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
248
257
|
const files = [];
|
|
249
258
|
const walk = (d, depth = 0) => {
|
|
250
259
|
if (depth > 3 || files.length > 400)
|
|
@@ -283,15 +292,45 @@ export async function runSlashCommand(input, ctx) {
|
|
|
283
292
|
return true;
|
|
284
293
|
}
|
|
285
294
|
case '/memory': {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
295
|
+
// .wormclaude/memory.md (oturumlar-arası hatıralar) — SaveMemory tool'u + oto-hafıza ile AYNI yer.
|
|
296
|
+
const a = (arg || '').trim();
|
|
297
|
+
// /memory temizle — hatıra dosyasını sıfırla
|
|
298
|
+
if (a === 'temizle' || a === 'clear') {
|
|
299
|
+
try {
|
|
300
|
+
fs.writeFileSync(getMemoryPath(), '');
|
|
301
|
+
ctx.note('Hatıralar temizlendi.');
|
|
302
|
+
}
|
|
303
|
+
catch (e) {
|
|
304
|
+
ctx.note('Temizlenemedi: ' + (e?.message || e));
|
|
305
|
+
}
|
|
306
|
+
return true;
|
|
291
307
|
}
|
|
292
|
-
|
|
293
|
-
|
|
308
|
+
// /memory ekle <metin> veya /memory <metin> veya /memory --global <metin>
|
|
309
|
+
let scope = 'project';
|
|
310
|
+
let fact = a;
|
|
311
|
+
if (fact.startsWith('ekle '))
|
|
312
|
+
fact = fact.slice(5).trim();
|
|
313
|
+
if (fact.startsWith('--global ')) {
|
|
314
|
+
scope = 'global';
|
|
315
|
+
fact = fact.slice(9).trim();
|
|
294
316
|
}
|
|
317
|
+
else if (fact.startsWith('--project ')) {
|
|
318
|
+
scope = 'project';
|
|
319
|
+
fact = fact.slice(10).trim();
|
|
320
|
+
}
|
|
321
|
+
if (fact) {
|
|
322
|
+
try {
|
|
323
|
+
const file = saveMemoryFact(fact, scope);
|
|
324
|
+
ctx.note(`Hafızaya eklendi (${scope}): ${fact} → ${file}`);
|
|
325
|
+
}
|
|
326
|
+
catch (e) {
|
|
327
|
+
ctx.note('Eklenemedi: ' + (e?.message || e));
|
|
328
|
+
}
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
// arg yok → mevcut hafızayı göster (memory.md + WORMCLAUDE.md birlikte)
|
|
332
|
+
const ctxStr = loadMemoryContext();
|
|
333
|
+
ctx.note(ctxStr ? ctxStr.slice(0, 6000) : 'Hafıza boş. /memory <metin> ile ekle, /init ile WORMCLAUDE.md üret.');
|
|
295
334
|
return true;
|
|
296
335
|
}
|
|
297
336
|
case '/doctor': {
|
|
@@ -563,6 +602,11 @@ export async function runSlashCommand(input, ctx) {
|
|
|
563
602
|
}
|
|
564
603
|
return true;
|
|
565
604
|
}
|
|
605
|
+
case '/program': {
|
|
606
|
+
const acc = await fetchAccount(ctx.config);
|
|
607
|
+
ctx.note(programText(acc ? acc.trustLevel : 0));
|
|
608
|
+
return true;
|
|
609
|
+
}
|
|
566
610
|
case '/export': {
|
|
567
611
|
fs.mkdirSync(SESSION_DIR, { recursive: true });
|
|
568
612
|
const file = path.join(SESSION_DIR, `session-${tsStamp()}.json`);
|
package/dist/markdown.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// Terminal markdown render (ink). Gemini MarkdownDisplay + InlineMarkdownRenderer + TableRenderer'dan
|
|
2
|
+
// uyarlandı; WormClaude theme.ts'e bağlandı; lowlight YOK (kod blokları tek accent renk — görsel cila v1).
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { Text, Box } from 'ink';
|
|
5
|
+
import stringWidth from 'string-width';
|
|
6
|
+
import { theme } from './theme.js';
|
|
7
|
+
// ── Inline markdown: **bold** *italik* ~~strike~~ `code` [text](url) <u>u</u> url ──
|
|
8
|
+
const INLINE_RE = /(\*\*.*?\*\*|\*.*?\*|_.*?_|~~.*?~~|\[.*?\]\(.*?\)|`+.+?`+|<u>.*?<\/u>|https?:\/\/\S+)/g;
|
|
9
|
+
export function RenderInline({ text }) {
|
|
10
|
+
if (!/[*_~`<[]|https?:/.test(text))
|
|
11
|
+
return React.createElement(Text, null, text);
|
|
12
|
+
const nodes = [];
|
|
13
|
+
let last = 0;
|
|
14
|
+
let m;
|
|
15
|
+
INLINE_RE.lastIndex = 0;
|
|
16
|
+
while ((m = INLINE_RE.exec(text)) !== null) {
|
|
17
|
+
if (m.index > last)
|
|
18
|
+
nodes.push(React.createElement(Text, { key: `t${last}` }, text.slice(last, m.index)));
|
|
19
|
+
const f = m[0];
|
|
20
|
+
const key = `m${m.index}`;
|
|
21
|
+
let node = null;
|
|
22
|
+
if (f.startsWith('**') && f.endsWith('**') && f.length > 4) {
|
|
23
|
+
node = React.createElement(Text, { key: key, bold: true }, f.slice(2, -2));
|
|
24
|
+
}
|
|
25
|
+
else if (f.length > 2 && ((f.startsWith('*') && f.endsWith('*')) || (f.startsWith('_') && f.endsWith('_'))) &&
|
|
26
|
+
// path-güvenli: kelime/yol-ayracı bitişiğinde italik sayma (dosya_adı, a/b_c bozulmasın)
|
|
27
|
+
!/\w/.test(text.substring(m.index - 1, m.index)) &&
|
|
28
|
+
!/\w/.test(text.substring(INLINE_RE.lastIndex, INLINE_RE.lastIndex + 1)) &&
|
|
29
|
+
!/\S[./\\]/.test(text.substring(m.index - 2, m.index)) &&
|
|
30
|
+
!/[./\\]\S/.test(text.substring(INLINE_RE.lastIndex, INLINE_RE.lastIndex + 2))) {
|
|
31
|
+
node = React.createElement(Text, { key: key, italic: true }, f.slice(1, -1));
|
|
32
|
+
}
|
|
33
|
+
else if (f.startsWith('~~') && f.endsWith('~~') && f.length > 4) {
|
|
34
|
+
node = React.createElement(Text, { key: key, strikethrough: true }, f.slice(2, -2));
|
|
35
|
+
}
|
|
36
|
+
else if (f.startsWith('`') && f.endsWith('`')) {
|
|
37
|
+
const cm = f.match(/^(`+)(.+?)\1$/s);
|
|
38
|
+
if (cm && cm[2])
|
|
39
|
+
node = React.createElement(Text, { key: key, color: theme.green }, cm[2]);
|
|
40
|
+
}
|
|
41
|
+
else if (f.startsWith('[') && f.includes('](') && f.endsWith(')')) {
|
|
42
|
+
const lm = f.match(/\[(.*?)\]\((.*?)\)/);
|
|
43
|
+
if (lm)
|
|
44
|
+
node = React.createElement(Text, { key: key },
|
|
45
|
+
lm[1],
|
|
46
|
+
React.createElement(Text, { color: theme.redBright },
|
|
47
|
+
" (",
|
|
48
|
+
lm[2],
|
|
49
|
+
")"));
|
|
50
|
+
}
|
|
51
|
+
else if (f.startsWith('<u>') && f.endsWith('</u>')) {
|
|
52
|
+
node = React.createElement(Text, { key: key, underline: true }, f.slice(3, -4));
|
|
53
|
+
}
|
|
54
|
+
else if (/^https?:\/\//.test(f)) {
|
|
55
|
+
node = React.createElement(Text, { key: key, color: theme.redBright }, f);
|
|
56
|
+
}
|
|
57
|
+
nodes.push(node ?? React.createElement(Text, { key: key }, f));
|
|
58
|
+
last = INLINE_RE.lastIndex;
|
|
59
|
+
}
|
|
60
|
+
if (last < text.length)
|
|
61
|
+
nodes.push(React.createElement(Text, { key: `t${last}` }, text.slice(last)));
|
|
62
|
+
return React.createElement(Text, null, nodes);
|
|
63
|
+
}
|
|
64
|
+
// markdown'ı soyup görüntü genişliği (tablo hizalama)
|
|
65
|
+
export function plainLen(text) {
|
|
66
|
+
const clean = text
|
|
67
|
+
.replace(/\*\*(.*?)\*\*/g, '$1').replace(/\*(.*?)\*/g, '$1').replace(/_(.*?)_/g, '$1')
|
|
68
|
+
.replace(/~~(.*?)~~/g, '$1').replace(/`(.*?)`/g, '$1').replace(/<u>(.*?)<\/u>/g, '$1')
|
|
69
|
+
.replace(/\[(.*?)\]\(.*?\)/g, '$1');
|
|
70
|
+
return stringWidth(clean);
|
|
71
|
+
}
|
|
72
|
+
// ── Tablo (box-drawing) ──────────────────────────────────────────────────────
|
|
73
|
+
function Table({ headers, rows, width }) {
|
|
74
|
+
const cols = headers.map((h, i) => Math.max(plainLen(h), ...rows.map((r) => plainLen(r[i] || ''))) + 2);
|
|
75
|
+
const total = cols.reduce((s, w) => s + w + 1, 1);
|
|
76
|
+
const scale = total > width ? width / total : 1;
|
|
77
|
+
const w = cols.map((c) => Math.max(3, Math.floor(c * scale)));
|
|
78
|
+
const cell = (c, width, header) => {
|
|
79
|
+
const cw = Math.max(0, width - 2);
|
|
80
|
+
let s = c;
|
|
81
|
+
if (plainLen(c) > cw) {
|
|
82
|
+
let lo = 0, hi = c.length, best = '';
|
|
83
|
+
while (lo <= hi) {
|
|
84
|
+
const mid = (lo + hi) >> 1;
|
|
85
|
+
const cand = c.slice(0, mid);
|
|
86
|
+
if (plainLen(cand) <= cw - 1) {
|
|
87
|
+
best = cand;
|
|
88
|
+
lo = mid + 1;
|
|
89
|
+
}
|
|
90
|
+
else
|
|
91
|
+
hi = mid - 1;
|
|
92
|
+
}
|
|
93
|
+
s = best + '…';
|
|
94
|
+
}
|
|
95
|
+
const pad = ' '.repeat(Math.max(0, cw - plainLen(s)));
|
|
96
|
+
return header ? React.createElement(Text, { bold: true, color: theme.redBright },
|
|
97
|
+
React.createElement(RenderInline, { text: s }),
|
|
98
|
+
pad) : React.createElement(Text, null,
|
|
99
|
+
React.createElement(RenderInline, { text: s }),
|
|
100
|
+
pad);
|
|
101
|
+
};
|
|
102
|
+
const border = (l, mid, r) => (React.createElement(Text, { color: theme.greyDim }, l + w.map((x) => '─'.repeat(x)).join(mid) + r));
|
|
103
|
+
const row = (cells, header) => (React.createElement(Text, { color: theme.greyDim },
|
|
104
|
+
"\u2502 ",
|
|
105
|
+
w.map((x, i) => (React.createElement(React.Fragment, { key: i },
|
|
106
|
+
cell(cells[i] || '', x, header),
|
|
107
|
+
i < w.length - 1 ? ' │ ' : ''))),
|
|
108
|
+
" \u2502"));
|
|
109
|
+
return (React.createElement(Box, { flexDirection: "column", marginY: 1 },
|
|
110
|
+
border('┌', '┬', '┐'),
|
|
111
|
+
row(headers, true),
|
|
112
|
+
border('├', '┼', '┤'),
|
|
113
|
+
rows.map((r, i) => React.createElement(React.Fragment, { key: i }, row(r, false))),
|
|
114
|
+
border('└', '┴', '┘')));
|
|
115
|
+
}
|
|
116
|
+
// ── Blok parser ──────────────────────────────────────────────────────────────
|
|
117
|
+
export function MarkdownDisplay({ text, width }) {
|
|
118
|
+
if (!text)
|
|
119
|
+
return null;
|
|
120
|
+
const W = Math.max(20, (width ?? (process.stdout.columns || 80)) - 4);
|
|
121
|
+
const lines = text.split('\n');
|
|
122
|
+
const blocks = [];
|
|
123
|
+
let key = 0;
|
|
124
|
+
const push = (n) => blocks.push(React.createElement(React.Fragment, { key: key++ }, n));
|
|
125
|
+
let inCode = false, codeFence = '', codeLines = [];
|
|
126
|
+
let inTable = false, tHeaders = [], tRows = [];
|
|
127
|
+
const flushTable = () => { if (tHeaders.length)
|
|
128
|
+
push(React.createElement(Table, { headers: tHeaders, rows: tRows, width: W })); inTable = false; tHeaders = []; tRows = []; };
|
|
129
|
+
const headerRe = /^ *(#{1,4}) +(.*)/;
|
|
130
|
+
const fenceRe = /^ *(`{3,}|~{3,}) *(\w*)? *$/;
|
|
131
|
+
const ulRe = /^([ \t]*)([-*+]) +(.*)/;
|
|
132
|
+
const olRe = /^([ \t]*)(\d+)\. +(.*)/;
|
|
133
|
+
const hrRe = /^ *([-*_] *){3,} *$/;
|
|
134
|
+
const rowRe = /^\s*\|(.+)\|\s*$/;
|
|
135
|
+
const sepRe = /^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)+\|?\s*$/;
|
|
136
|
+
lines.forEach((line, idx) => {
|
|
137
|
+
if (inCode) {
|
|
138
|
+
const fm = line.match(fenceRe);
|
|
139
|
+
if (fm && fm[1][0] === codeFence[0] && fm[1].length >= codeFence.length) {
|
|
140
|
+
push(React.createElement(Box, { flexDirection: "column", marginY: 1 }, codeLines.map((c, i) => React.createElement(Text, { key: i, color: theme.green }, c))));
|
|
141
|
+
inCode = false;
|
|
142
|
+
codeFence = '';
|
|
143
|
+
codeLines = [];
|
|
144
|
+
}
|
|
145
|
+
else
|
|
146
|
+
codeLines.push(line);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const fm = line.match(fenceRe);
|
|
150
|
+
const rm = line.match(rowRe);
|
|
151
|
+
if (fm) {
|
|
152
|
+
if (inTable)
|
|
153
|
+
flushTable();
|
|
154
|
+
inCode = true;
|
|
155
|
+
codeFence = fm[1];
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (rm && !inTable) {
|
|
159
|
+
if (idx + 1 < lines.length && sepRe.test(lines[idx + 1])) {
|
|
160
|
+
inTable = true;
|
|
161
|
+
tHeaders = rm[1].split('|').map((c) => c.trim());
|
|
162
|
+
tRows = [];
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
push(React.createElement(Text, { color: theme.white },
|
|
166
|
+
React.createElement(RenderInline, { text: line })));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (inTable && sepRe.test(line))
|
|
170
|
+
return;
|
|
171
|
+
if (inTable && rm) {
|
|
172
|
+
const cells = rm[1].split('|').map((c) => c.trim());
|
|
173
|
+
while (cells.length < tHeaders.length)
|
|
174
|
+
cells.push('');
|
|
175
|
+
cells.length = tHeaders.length;
|
|
176
|
+
tRows.push(cells);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (inTable && !rm)
|
|
180
|
+
flushTable();
|
|
181
|
+
const hm = line.match(headerRe);
|
|
182
|
+
const um = line.match(ulRe);
|
|
183
|
+
const om = line.match(olRe);
|
|
184
|
+
if (hrRe.test(line) && line.trim().length >= 3) {
|
|
185
|
+
push(React.createElement(Text, { color: theme.greyDim }, '─'.repeat(Math.min(W, 40))));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (hm) {
|
|
189
|
+
const lvl = hm[1].length;
|
|
190
|
+
const inner = React.createElement(RenderInline, { text: hm[2] });
|
|
191
|
+
if (lvl <= 2)
|
|
192
|
+
push(React.createElement(Text, { bold: true, color: theme.redBright }, inner));
|
|
193
|
+
else if (lvl === 3)
|
|
194
|
+
push(React.createElement(Text, { bold: true, color: theme.white }, inner));
|
|
195
|
+
else
|
|
196
|
+
push(React.createElement(Text, { italic: true, color: theme.grey }, inner));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (um || om) {
|
|
200
|
+
const [, ws, marker, itemText] = (um || om);
|
|
201
|
+
const prefix = om ? `${marker}. ` : '• ';
|
|
202
|
+
push(React.createElement(Box, { paddingLeft: ws.length },
|
|
203
|
+
React.createElement(Text, { color: theme.grey }, prefix),
|
|
204
|
+
React.createElement(Box, { flexGrow: 1 },
|
|
205
|
+
React.createElement(Text, { color: theme.white },
|
|
206
|
+
React.createElement(RenderInline, { text: itemText })))));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (line.trim().length === 0) {
|
|
210
|
+
push(React.createElement(Box, { height: 1 }));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
push(React.createElement(Text, { color: theme.white, wrap: "wrap" },
|
|
214
|
+
React.createElement(RenderInline, { text: line })));
|
|
215
|
+
});
|
|
216
|
+
if (inCode)
|
|
217
|
+
push(React.createElement(Box, { flexDirection: "column", marginY: 1 }, codeLines.map((c, i) => React.createElement(Text, { key: i, color: theme.green }, c))));
|
|
218
|
+
if (inTable)
|
|
219
|
+
flushTable();
|
|
220
|
+
return React.createElement(Box, { flexDirection: "column" }, blocks);
|
|
221
|
+
}
|
package/dist/program.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// WormClaude Doğrulanmış Araştırmacı Programı — güven seviyesi (trust_level) meta verisi.
|
|
2
|
+
// Backend (gateway) min_trust'lı prompt modüllerini/araçları/bütçeyi bu seviyeye göre açar.
|
|
3
|
+
export const TIERS = [
|
|
4
|
+
{ level: 0, name: 'Normal Kullanıcı', badge: '', desc: 'Temel erişim. Yetkili bağlamda güvenlik+kod yardımı.' },
|
|
5
|
+
{ level: 1, name: 'Doğrulanmış Kullanıcı', badge: 'Doğrulanmış', desc: 'E-posta/kimlik doğrulandı. Artırılmış bütçe.' },
|
|
6
|
+
{ level: 2, name: 'Araştırmacı', badge: 'Araştırmacı', desc: 'Güvenlik skill/extension erişimi.' },
|
|
7
|
+
{ level: 3, name: 'Doğrulanmış Güvenlik Araştırmacısı', badge: 'Güvenlik Arş.', desc: 'Tam saldırgan güvenlik modülü + gelişmiş araçlar + yüksek bütçe.' },
|
|
8
|
+
{ level: 4, name: 'Kurumsal Müşteri', badge: 'Kurumsal', desc: 'Çok-koltuk, denetim günlüğü, özel extension.' },
|
|
9
|
+
{ level: 5, name: 'Dahili Ekip', badge: 'Dahili', desc: 'Sınırsız erişim.' },
|
|
10
|
+
];
|
|
11
|
+
export function tier(level) {
|
|
12
|
+
return TIERS[Math.max(0, Math.min(5, level | 0))];
|
|
13
|
+
}
|
|
14
|
+
/** /program komutunun gösterdiği açıklama (mevcut seviye işaretli). */
|
|
15
|
+
export function programText(current) {
|
|
16
|
+
const lines = [];
|
|
17
|
+
lines.push('WormClaude Doğrulanmış Araştırmacı Programı');
|
|
18
|
+
lines.push('');
|
|
19
|
+
lines.push('Sansürsüz güvenlik yetenekleri anonim herkese değil, kimliği doğrulanmış');
|
|
20
|
+
lines.push('ve hesap verebilir araştırmacılara açılır. Seviyeler:');
|
|
21
|
+
lines.push('');
|
|
22
|
+
for (const t of TIERS) {
|
|
23
|
+
const mark = t.level === current ? '❯' : ' ';
|
|
24
|
+
lines.push(`${mark} Seviye ${t.level} — ${t.name}`);
|
|
25
|
+
lines.push(` ${t.desc}`);
|
|
26
|
+
}
|
|
27
|
+
lines.push('');
|
|
28
|
+
lines.push(`Şu anki seviyen: ${current} (${tier(current).name}).`);
|
|
29
|
+
if (current < 3) {
|
|
30
|
+
lines.push('');
|
|
31
|
+
lines.push('Yükselmek için: HackerOne/Bugcrowd profilin, şirket/kurum bilgin veya');
|
|
32
|
+
lines.push('kimlik doğrulaman ile başvur → admin incelemesi sonrası seviyen atanır.');
|
|
33
|
+
lines.push('Başvuru: sezaiyilmaz6060@gmail.com (veya panel üzerinden).');
|
|
34
|
+
}
|
|
35
|
+
return lines.join('\n');
|
|
36
|
+
}
|
package/dist/theme.js
CHANGED
package/dist/tools.js
CHANGED
|
@@ -27,6 +27,47 @@ export function getTodos() { return todosStore; }
|
|
|
27
27
|
const MAX_LINES_TO_READ = 2000;
|
|
28
28
|
const DEFAULT_BASH_TIMEOUT_MS = 120000;
|
|
29
29
|
const MAX_BASH_TIMEOUT_MS = 600000;
|
|
30
|
+
// Bash çalışma dizini — oturum boyunca kalıcı. execSync her çağrıda taze shell açtığı için
|
|
31
|
+
// `cd` normalde kaybolur; burada cwd'yi takip edip her komuta geçiriyoruz.
|
|
32
|
+
let bashCwd;
|
|
33
|
+
export function getBashCwd() { return bashCwd || process.cwd(); }
|
|
34
|
+
export function setBashCwd(dir) { bashCwd = dir; }
|
|
35
|
+
// Komutu çalıştırır ve sonrasında cwd değişikliğini (cd) yakalayıp bashCwd'yi günceller.
|
|
36
|
+
// POSIX: pwd'yi temp dosyaya yazan, çıkış kodunu KORUYAN sarmalayıcı (hata propagasyonu bozulmaz).
|
|
37
|
+
// Windows: komutu olduğu gibi çalıştır, sonra baştaki `cd <hedef>`'i regex ile yakala (best-effort).
|
|
38
|
+
function runBashCapturingCwd(command, timeout) {
|
|
39
|
+
const cwd = getBashCwd();
|
|
40
|
+
const opts = { encoding: 'utf8', timeout, maxBuffer: 10 * 1024 * 1024, windowsHide: true, cwd };
|
|
41
|
+
if (process.platform === 'win32') {
|
|
42
|
+
const out = execSync(command, opts);
|
|
43
|
+
// best-effort: "cd <hedef>" / "cd /d <hedef>" (zincirsiz tek komut)
|
|
44
|
+
const m = /^\s*cd\s+(?:\/d\s+)?"?([^"&|<>]+?)"?\s*$/i.exec(command);
|
|
45
|
+
if (m) {
|
|
46
|
+
const nd = path.resolve(cwd, m[1].trim());
|
|
47
|
+
try {
|
|
48
|
+
if (fs.statSync(nd).isDirectory())
|
|
49
|
+
bashCwd = nd;
|
|
50
|
+
}
|
|
51
|
+
catch { /* yok say */ }
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
// POSIX
|
|
56
|
+
const pwdFile = path.join(os.tmpdir(), `wc_pwd_${process.pid}_${Date.now()}.tmp`);
|
|
57
|
+
const wrapped = `{ ${command}\n}; __wc=$?; pwd > '${pwdFile}' 2>/dev/null; exit $__wc`;
|
|
58
|
+
try {
|
|
59
|
+
return execSync(wrapped, { ...opts, shell: '/bin/sh' });
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
try {
|
|
63
|
+
const p = fs.readFileSync(pwdFile, 'utf8').trim();
|
|
64
|
+
if (p)
|
|
65
|
+
bashCwd = p;
|
|
66
|
+
fs.unlinkSync(pwdFile);
|
|
67
|
+
}
|
|
68
|
+
catch { /* dosya yoksa yok say */ }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
30
71
|
// WormClaude davranışı: bir dosyayı düzenlemeden/üzerine yazmadan önce
|
|
31
72
|
// en az bir kez okumuş olman gerekir. Okunan dosyaları burada izliyoruz.
|
|
32
73
|
const readFiles = new Set();
|
|
@@ -880,7 +921,7 @@ export async function executeTool(name, args) {
|
|
|
880
921
|
if (name === 'Bash') {
|
|
881
922
|
if (args.run_in_background) {
|
|
882
923
|
const task = tasks.create('shell', String(args.command).slice(0, 60));
|
|
883
|
-
const child = spawn(String(args.command), { shell: true, windowsHide: true });
|
|
924
|
+
const child = spawn(String(args.command), { shell: true, windowsHide: true, cwd: getBashCwd() });
|
|
884
925
|
task.child = child;
|
|
885
926
|
child.stdout?.on('data', (d) => tasks.append(task.id, d.toString()));
|
|
886
927
|
child.stderr?.on('data', (d) => tasks.append(task.id, d.toString()));
|
|
@@ -894,9 +935,7 @@ export async function executeTool(name, args) {
|
|
|
894
935
|
let timeout = Number(args.timeout) || DEFAULT_BASH_TIMEOUT_MS;
|
|
895
936
|
if (timeout > MAX_BASH_TIMEOUT_MS)
|
|
896
937
|
timeout = MAX_BASH_TIMEOUT_MS;
|
|
897
|
-
const out =
|
|
898
|
-
encoding: 'utf8', timeout, maxBuffer: 10 * 1024 * 1024, windowsHide: true,
|
|
899
|
-
});
|
|
938
|
+
const out = runBashCapturingCwd(String(args.command), timeout);
|
|
900
939
|
return { ok: true, output: (out || '(no output)').slice(0, 20000) };
|
|
901
940
|
}
|
|
902
941
|
if (name === 'Agent') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wormclaude",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.17",
|
|
4
4
|
"description": "WormClaude CLI - uncensored security+code assistant (ink TUI, Claude-style)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"ink": "^5.0.1",
|
|
20
20
|
"ink-spinner": "^5.0.0",
|
|
21
21
|
"ink-text-input": "^6.0.0",
|
|
22
|
-
"react": "^18.3.1"
|
|
22
|
+
"react": "^18.3.1",
|
|
23
|
+
"string-width": "^7.2.0"
|
|
23
24
|
},
|
|
24
25
|
"devDependencies": {
|
|
25
26
|
"@types/node": "^22.10.2",
|