wormclaude 1.0.14 → 1.0.16
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 +10 -1
- package/dist/api.js +12 -10
- package/dist/atmention.js +117 -0
- package/dist/auth.js +10 -0
- package/dist/cli.js +40 -6
- package/dist/commands.js +44 -8
- package/dist/errorsan.js +94 -0
- package/dist/markdown.js +221 -0
- package/dist/memory.js +41 -0
- package/dist/safejson.js +166 -0
- package/dist/streamparser.js +158 -0
- package/dist/subagents.js +119 -0
- package/dist/textclean.js +37 -0
- package/dist/theme.js +1 -1
- package/dist/tools.js +128 -14
- package/package.json +3 -2
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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/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,10 @@ 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';
|
|
11
|
+
import { MarkdownDisplay } from './markdown.js';
|
|
12
|
+
import { resolveAtMentions } from './atmention.js';
|
|
9
13
|
import { summarizeTools } from './toolSummary.js';
|
|
10
14
|
import { pickTipId, tipText } from './tips.js';
|
|
11
15
|
import { t, cmdDesc, setLang, saveLang, loadLang, getLang } from './i18n.js';
|
|
@@ -219,8 +223,8 @@ function RenderItem({ item }) {
|
|
|
219
223
|
if (item.kind === 'assistant') {
|
|
220
224
|
return (React.createElement(Box, { marginTop: 1, flexDirection: "row" },
|
|
221
225
|
React.createElement(Text, { color: theme.redBright, bold: true }, "\u23FA "),
|
|
222
|
-
React.createElement(Box, { flexDirection: "column" },
|
|
223
|
-
React.createElement(
|
|
226
|
+
React.createElement(Box, { flexDirection: "column", flexGrow: 1 },
|
|
227
|
+
React.createElement(MarkdownDisplay, { text: item.text }))));
|
|
224
228
|
}
|
|
225
229
|
if (item.kind === 'tool') {
|
|
226
230
|
const lines = item.result.split('\n').length;
|
|
@@ -535,13 +539,13 @@ function App() {
|
|
|
535
539
|
for await (const ev of streamChat(historyRef.current, allToolSchemas(), config, ac.signal)) {
|
|
536
540
|
if (ev.type === 'text') {
|
|
537
541
|
assistantText += ev.text;
|
|
538
|
-
setStreaming(assistantText);
|
|
542
|
+
setStreaming(cleanModelText(assistantText));
|
|
539
543
|
setTokens(Math.round(assistantText.length / 4));
|
|
540
544
|
}
|
|
541
545
|
else if (ev.type === 'error') {
|
|
542
546
|
if (isContextError(ev.error))
|
|
543
547
|
gotCtxError = true;
|
|
544
|
-
assistantText += `\n[hata: ${ev.error}]`;
|
|
548
|
+
assistantText += `\n[hata: ${sanitizeError(ev.error)}]`;
|
|
545
549
|
setStreaming(assistantText);
|
|
546
550
|
}
|
|
547
551
|
else if (ev.type === 'done') {
|
|
@@ -550,6 +554,8 @@ function App() {
|
|
|
550
554
|
}
|
|
551
555
|
}
|
|
552
556
|
setStreaming('');
|
|
557
|
+
// Sızan özel-token / tool-call markup'ını bir kez temizle (hem geçmiş hem gösterim için).
|
|
558
|
+
assistantText = cleanModelText(assistantText);
|
|
553
559
|
// Reactive compact: bağlam taştıysa bir kez özetle ve turu tekrar dene
|
|
554
560
|
if (gotCtxError && !reactiveRetried) {
|
|
555
561
|
reactiveRetried = true;
|
|
@@ -613,7 +619,7 @@ function App() {
|
|
|
613
619
|
onResult: (c, _i, res) => {
|
|
614
620
|
if ((c.name === 'WebSearch' || c.name === 'WebFetch') && res.ok)
|
|
615
621
|
usedWeb = true;
|
|
616
|
-
push({ kind: 'tool', label: toolLabel(c.name, res.args), result: res.output, ok: res.ok });
|
|
622
|
+
push({ kind: 'tool', label: toolLabel(c.name, res.args), result: sanitizeOutput(res.output), ok: res.ok });
|
|
617
623
|
},
|
|
618
624
|
confirm: (c, args) => new Promise((resolve) => {
|
|
619
625
|
if (allowedToolsRef.current.has(c.name))
|
|
@@ -661,6 +667,29 @@ function App() {
|
|
|
661
667
|
setCmdSel(0);
|
|
662
668
|
if (!v)
|
|
663
669
|
return;
|
|
670
|
+
// ! shell modu — LLM'siz doğrudan shell komutu (cmdsec ile gate'li). Sonucu modele bağlam olarak ekle.
|
|
671
|
+
if (v.startsWith('!') && v.length > 1) {
|
|
672
|
+
const cmd = v.slice(1).trim();
|
|
673
|
+
if (!cmd)
|
|
674
|
+
return;
|
|
675
|
+
push({ kind: 'user', text: v });
|
|
676
|
+
setBusy(true);
|
|
677
|
+
setThinking(false);
|
|
678
|
+
setPhase(t('phase.toolRun', 'shell'));
|
|
679
|
+
try {
|
|
680
|
+
const res = await executeTool('Bash', { command: cmd });
|
|
681
|
+
push({ kind: 'tool', label: `! ${cmd.slice(0, 60)}`, result: sanitizeOutput(res.output), ok: res.ok });
|
|
682
|
+
historyRef.current = [...historyRef.current, {
|
|
683
|
+
role: 'user',
|
|
684
|
+
content: `Şu shell komutunu çalıştırdım:\n\`\`\`\n${cmd}\n\`\`\`\nÇıktı:\n\`\`\`\n${(res.output || '').slice(0, 4000)}\n\`\`\``,
|
|
685
|
+
}];
|
|
686
|
+
}
|
|
687
|
+
catch (e) {
|
|
688
|
+
push({ kind: 'note', text: t('note.cmdErr', e?.message || e) });
|
|
689
|
+
}
|
|
690
|
+
setBusy(false);
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
664
693
|
if (v.startsWith('/')) {
|
|
665
694
|
// Seçili menü öğesini çalıştır (tam eşleşme yoksa ve argüman yoksa)
|
|
666
695
|
const firstTok = v.split(' ')[0];
|
|
@@ -756,7 +785,12 @@ function App() {
|
|
|
756
785
|
setBusy(false);
|
|
757
786
|
return;
|
|
758
787
|
}
|
|
759
|
-
|
|
788
|
+
// @dosya mention — referanslanan dosya/dizin içeriğini modele enjekte et, kullanıcıya orijinali göster
|
|
789
|
+
const at = resolveAtMentions(v);
|
|
790
|
+
if (at.files.length)
|
|
791
|
+
runAgent(at.augmented, v);
|
|
792
|
+
else
|
|
793
|
+
runAgent(v);
|
|
760
794
|
}
|
|
761
795
|
const { cols, rows } = useDimensions();
|
|
762
796
|
// İlk açılış: dil seçimi
|
package/dist/commands.js
CHANGED
|
@@ -12,6 +12,7 @@ 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';
|
|
15
16
|
export const COMMANDS = [
|
|
16
17
|
{ name: '/help', desc: 'komutları ve ipuçlarını göster' },
|
|
17
18
|
{ name: '/clear', desc: 'sohbeti ve geçmişi temizle' },
|
|
@@ -24,7 +25,7 @@ export const COMMANDS = [
|
|
|
24
25
|
{ name: '/commit', desc: 'stage edilmiş değişiklikleri modelle commit et' },
|
|
25
26
|
{ name: '/review', desc: 'mevcut diff’i modelle incele' },
|
|
26
27
|
{ name: '/init', desc: 'projeyi tarayıp WORMCLAUDE.md üret' },
|
|
27
|
-
{ name: '/memory', desc: '
|
|
28
|
+
{ name: '/memory', desc: 'hafızayı göster / ekle / temizle (/memory <metin> · --global · temizle)' },
|
|
28
29
|
{ name: '/doctor', desc: 'sistem ve backend sağlık kontrolü' },
|
|
29
30
|
{ name: '/lang', desc: 'arayüz dilini değiştir (tr/en)' },
|
|
30
31
|
{ name: '/skills', desc: 'skill\'leri listele / indir / yeniden yükle' },
|
|
@@ -245,6 +246,11 @@ export async function runSlashCommand(input, ctx) {
|
|
|
245
246
|
return true;
|
|
246
247
|
}
|
|
247
248
|
case '/init': {
|
|
249
|
+
const initDest = path.join(process.cwd(), 'WORMCLAUDE.md');
|
|
250
|
+
if (fs.existsSync(initDest) && fs.readFileSync(initDest, 'utf8').trim() && (arg || '').trim() !== 'force') {
|
|
251
|
+
ctx.note('WORMCLAUDE.md zaten var ve dolu. Üzerine yazmak için: /init force');
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
248
254
|
const files = [];
|
|
249
255
|
const walk = (d, depth = 0) => {
|
|
250
256
|
if (depth > 3 || files.length > 400)
|
|
@@ -283,15 +289,45 @@ export async function runSlashCommand(input, ctx) {
|
|
|
283
289
|
return true;
|
|
284
290
|
}
|
|
285
291
|
case '/memory': {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
292
|
+
// .wormclaude/memory.md (oturumlar-arası hatıralar) — SaveMemory tool'u + oto-hafıza ile AYNI yer.
|
|
293
|
+
const a = (arg || '').trim();
|
|
294
|
+
// /memory temizle — hatıra dosyasını sıfırla
|
|
295
|
+
if (a === 'temizle' || a === 'clear') {
|
|
296
|
+
try {
|
|
297
|
+
fs.writeFileSync(getMemoryPath(), '');
|
|
298
|
+
ctx.note('Hatıralar temizlendi.');
|
|
299
|
+
}
|
|
300
|
+
catch (e) {
|
|
301
|
+
ctx.note('Temizlenemedi: ' + (e?.message || e));
|
|
302
|
+
}
|
|
303
|
+
return true;
|
|
291
304
|
}
|
|
292
|
-
|
|
293
|
-
|
|
305
|
+
// /memory ekle <metin> veya /memory <metin> veya /memory --global <metin>
|
|
306
|
+
let scope = 'project';
|
|
307
|
+
let fact = a;
|
|
308
|
+
if (fact.startsWith('ekle '))
|
|
309
|
+
fact = fact.slice(5).trim();
|
|
310
|
+
if (fact.startsWith('--global ')) {
|
|
311
|
+
scope = 'global';
|
|
312
|
+
fact = fact.slice(9).trim();
|
|
313
|
+
}
|
|
314
|
+
else if (fact.startsWith('--project ')) {
|
|
315
|
+
scope = 'project';
|
|
316
|
+
fact = fact.slice(10).trim();
|
|
317
|
+
}
|
|
318
|
+
if (fact) {
|
|
319
|
+
try {
|
|
320
|
+
const file = saveMemoryFact(fact, scope);
|
|
321
|
+
ctx.note(`Hafızaya eklendi (${scope}): ${fact} → ${file}`);
|
|
322
|
+
}
|
|
323
|
+
catch (e) {
|
|
324
|
+
ctx.note('Eklenemedi: ' + (e?.message || e));
|
|
325
|
+
}
|
|
326
|
+
return true;
|
|
294
327
|
}
|
|
328
|
+
// arg yok → mevcut hafızayı göster (memory.md + WORMCLAUDE.md birlikte)
|
|
329
|
+
const ctxStr = loadMemoryContext();
|
|
330
|
+
ctx.note(ctxStr ? ctxStr.slice(0, 6000) : 'Hafıza boş. /memory <metin> ile ekle, /init ile WORMCLAUDE.md üret.');
|
|
295
331
|
return true;
|
|
296
332
|
}
|
|
297
333
|
case '/doctor': {
|
package/dist/errorsan.js
ADDED
|
@@ -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
|
+
}
|