wormclaude 1.0.13 → 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))
package/dist/cmdsec.js ADDED
@@ -0,0 +1,306 @@
1
+ // Komut güvenliği — Bash aracı çalışmadan ÖNCE kontrol eder.
2
+ // Gemini/Blackbox CLI shell-utils.js + shellReadOnlyChecker.js'ten uyarlandı, WormClaude'a özel.
3
+ // - Hard-deny blocklist (rm -rf /, fork bomb, dd of=/dev, mkfs, curl|bash...)
4
+ // - Command-substitution engeli ($(), <(), backtick) -> hard deny
5
+ // - Read-only allowlist (ls, cat, git status... -> izinsiz geçer)
6
+ // - Kalıcı kullanıcı allowlist'i (kök komut bazlı: "git", "npm")
7
+ // - escapeShellArg (PowerShell/cmd/bash) -> !{} injection güvenliği
8
+ import * as os from 'node:os';
9
+ import * as fs from 'node:fs';
10
+ import * as path from 'node:path';
11
+ // ── Kalıcı kullanıcı allowlist'i (kök komut: git, npm...) ───────────────────
12
+ const ALLOW_FILE = path.join(os.homedir(), '.wormclaude', 'approved-commands.json');
13
+ let _approved = null;
14
+ function loadApproved() {
15
+ if (_approved)
16
+ return _approved;
17
+ try {
18
+ const arr = JSON.parse(fs.readFileSync(ALLOW_FILE, 'utf8'));
19
+ _approved = new Set(Array.isArray(arr) ? arr.map(String) : []);
20
+ }
21
+ catch {
22
+ _approved = new Set();
23
+ }
24
+ return _approved;
25
+ }
26
+ export function getApprovedCommands() { return [...loadApproved()].sort(); }
27
+ function saveApproved(set) {
28
+ _approved = set;
29
+ try {
30
+ fs.mkdirSync(path.dirname(ALLOW_FILE), { recursive: true });
31
+ fs.writeFileSync(ALLOW_FILE, JSON.stringify([...set], null, 2));
32
+ }
33
+ catch { }
34
+ }
35
+ export function approveCommands(roots) {
36
+ const set = loadApproved();
37
+ const before = set.size;
38
+ for (const r of roots)
39
+ if (r)
40
+ set.add(r);
41
+ saveApproved(set);
42
+ return set.size - before;
43
+ }
44
+ export function unapproveCommands(roots) {
45
+ const set = loadApproved();
46
+ const before = set.size;
47
+ for (const r of roots)
48
+ set.delete(r);
49
+ saveApproved(set);
50
+ return before - set.size;
51
+ }
52
+ export function clearApproved() { saveApproved(new Set()); }
53
+ // ── Komut ayrıştırma (shell-utils.js'ten) ──────────────────────────────────
54
+ export function splitCommands(command) {
55
+ const commands = [];
56
+ let cur = '';
57
+ let inS = false, inD = false;
58
+ let i = 0;
59
+ while (i < command.length) {
60
+ const c = command[i], n = command[i + 1];
61
+ if (c === '\\' && i < command.length - 1) {
62
+ cur += c + n;
63
+ i += 2;
64
+ continue;
65
+ }
66
+ if (c === "'" && !inD)
67
+ inS = !inS;
68
+ else if (c === '"' && !inS)
69
+ inD = !inD;
70
+ if (!inS && !inD) {
71
+ if ((c === '&' && n === '&') || (c === '|' && n === '|')) {
72
+ commands.push(cur.trim());
73
+ cur = '';
74
+ i++;
75
+ }
76
+ else if (c === ';' || c === '&' || c === '|') {
77
+ commands.push(cur.trim());
78
+ cur = '';
79
+ }
80
+ else
81
+ cur += c;
82
+ }
83
+ else
84
+ cur += c;
85
+ i++;
86
+ }
87
+ if (cur.trim())
88
+ commands.push(cur.trim());
89
+ return commands.filter(Boolean);
90
+ }
91
+ export function getCommandRoot(command) {
92
+ const t = command.trim();
93
+ if (!t)
94
+ return undefined;
95
+ const m = t.match(/^"([^"]+)"|^'([^']+)'|^(\S+)/);
96
+ if (m) {
97
+ const root = m[1] || m[2] || m[3];
98
+ if (root)
99
+ return root.split(/[\\/]/).pop();
100
+ }
101
+ return undefined;
102
+ }
103
+ export function getCommandRoots(command) {
104
+ if (!command)
105
+ return [];
106
+ return splitCommands(command).map(getCommandRoot).filter((c) => !!c);
107
+ }
108
+ export function stripShellWrapper(command) {
109
+ const m = command.match(/^\s*(?:sh|bash|zsh|cmd\.exe|powershell|pwsh)\s+(?:\/c|-c|-Command)\s+/i);
110
+ if (m) {
111
+ let c = command.substring(m[0].length).trim();
112
+ if ((c.startsWith('"') && c.endsWith('"')) || (c.startsWith("'") && c.endsWith("'")))
113
+ c = c.slice(1, -1);
114
+ return c;
115
+ }
116
+ return command.trim();
117
+ }
118
+ // $(), <(), backtick komut-ikamesi tespiti (bash kurallarına göre)
119
+ export function detectCommandSubstitution(command) {
120
+ let inS = false, inD = false, inB = false;
121
+ let i = 0;
122
+ while (i < command.length) {
123
+ const c = command[i], n = command[i + 1];
124
+ if (c === '\\' && !inS) {
125
+ i += 2;
126
+ continue;
127
+ }
128
+ if (c === "'" && !inD && !inB)
129
+ inS = !inS;
130
+ else if (c === '"' && !inS && !inB)
131
+ inD = !inD;
132
+ else if (c === '`' && !inS)
133
+ inB = !inB;
134
+ if (!inS) {
135
+ if (c === '$' && n === '(')
136
+ return true;
137
+ if (c === '<' && n === '(' && !inD && !inB)
138
+ return true;
139
+ if (c === '`' && !inB)
140
+ return true;
141
+ }
142
+ i++;
143
+ }
144
+ return false;
145
+ }
146
+ export function escapeShellArg(arg, shell = process.platform === 'win32' ? 'cmd' : 'bash') {
147
+ if (!arg)
148
+ return shell === 'bash' ? "''" : '""';
149
+ switch (shell) {
150
+ case 'powershell': return `'${arg.replace(/'/g, "''")}'`;
151
+ case 'cmd': return `"${arg.replace(/"/g, '""')}"`;
152
+ case 'bash':
153
+ default: return `'${arg.replace(/'/g, "'\\''")}'`;
154
+ }
155
+ }
156
+ // ── Tehlikeli komut blocklist (HARD DENY) ──────────────────────────────────
157
+ const DANGER = [
158
+ { re: /\brm\s+(?:-[a-zA-Z]+\s+)*-[a-zA-Z]*[rf][a-zA-Z]*\s+(?:-[a-zA-Z]+\s+)*(?:\/|~|\$HOME|\*|\.)(?:\s|$)/, reason: 'Tehlikeli geniş silme (rm -rf /, ~, *, .)' },
159
+ { re: /:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/, reason: 'Fork bomb' },
160
+ { re: /\bmkfs(\.\w+)?\b/, reason: 'Dosya sistemi formatlama (mkfs)' },
161
+ { re: /\bdd\b[^\n]*\bof=\/dev\/(sd|nvme|hd|mmcblk|disk)/, reason: 'Diske ham yazma (dd of=/dev/...)' },
162
+ { re: />\s*\/dev\/(sd|nvme|hd|mmcblk|disk)/, reason: 'Blok aygıtına yazma (> /dev/sd...)' },
163
+ { re: /\bchmod\s+-R\s+0*777\s+\//, reason: 'Kök dizinde chmod -R 777 /' },
164
+ { re: /\bchown\s+-R\s+[^\s]+\s+\/(?:\s|$)/, reason: 'Kök dizinde chown -R' },
165
+ { re: /\b(?:curl|wget)\b[^\n|]*\|\s*(?:sudo\s+)?(?:bash|sh|zsh)\b/, reason: 'İnternetten indirip shell\'e pipe (curl|bash)' },
166
+ { re: /\b(?:shutdown|reboot|halt|poweroff|init\s+0)\b/, reason: 'Sistemi kapatma/yeniden başlatma' },
167
+ { re: /\bmv\s+[^\n]*\s+\/dev\/null\b/, reason: 'Veriyi /dev/null\'a taşıma' },
168
+ { re: />\s*\/dev\/null\s+2>&1\s*;\s*rm/, reason: 'Gizli silme' },
169
+ ];
170
+ // ── Read-only komut tespiti (shellReadOnlyChecker.js'ten — sağlam) ──────────
171
+ const READONLY_ROOTS = new Set([
172
+ 'awk', 'basename', 'cat', 'cd', 'column', 'cut', 'df', 'dirname', 'du', 'echo', 'env', 'find',
173
+ 'git', 'grep', 'egrep', 'fgrep', 'head', 'less', 'more', 'printenv', 'printf', 'ps', 'pwd', 'rg',
174
+ 'ripgrep', 'sed', 'sort', 'stat', 'tail', 'tree', 'uniq', 'wc', 'which', 'where', 'whoami',
175
+ 'id', 'hostname', 'uname', 'date', 'cal', 'uptime', 'file', 'realpath', 'nl', 'tac', 'cmp',
176
+ 'md5sum', 'sha256sum', 'seq', 'test', 'true', 'false',
177
+ 'node', 'python', 'python3', 'java', 'go', 'rustc', 'php', 'ruby', // yalnız --version
178
+ 'npm', 'pnpm', 'yarn', 'pip', 'pip3', 'docker', 'kubectl', 'cargo', // yalnız read-only alt-komut
179
+ ]);
180
+ // Alt-komutu read-only olanlar (yazan alt-komutlar — config/run/install — DAHİL DEĞİL)
181
+ const READONLY_SUB = {
182
+ git: new Set(['blame', 'branch', 'cat-file', 'diff', 'grep', 'log', 'ls-files', 'remote', 'rev-parse', 'show', 'status', 'describe']),
183
+ npm: new Set(['ls', 'list', 'view', 'info', 'outdated', 'show', 'search', 'ping', 'whoami', 'doctor', 'why', 'audit']),
184
+ pnpm: new Set(['ls', 'list', 'view', 'outdated', 'why', 'audit']),
185
+ yarn: new Set(['list', 'info', 'why', 'outdated', 'audit']),
186
+ pip: new Set(['show', 'list', 'freeze', 'check']),
187
+ pip3: new Set(['show', 'list', 'freeze', 'check']),
188
+ docker: new Set(['ps', 'images', 'logs', 'inspect', 'version', 'info', 'stats', 'top', 'port', 'diff', 'history']),
189
+ kubectl: new Set(['get', 'describe', 'logs', 'top', 'explain', 'version', 'api-resources']),
190
+ cargo: new Set(['tree', 'search', 'metadata']),
191
+ };
192
+ const VERSION_ONLY = new Set(['node', 'python', 'python3', 'java', 'go', 'rustc', 'php', 'ruby']);
193
+ const BLOCKED_FIND = new Set(['-delete', '-exec', '-execdir', '-ok', '-okdir']);
194
+ const BLOCKED_FIND_PREFIX = ['-fprint'];
195
+ const BLOCKED_GIT_REMOTE = new Set(['add', 'remove', 'rename', 'set-url', 'prune', 'update']);
196
+ const BLOCKED_GIT_BRANCH = new Set(['-d', '-D', '--delete', '--move', '-m']);
197
+ const ENV_ASSIGN = /^[A-Za-z_][A-Za-z0-9_]*=/;
198
+ // Tırnak dışında '>' (yazma yönlendirmesi) -> read-only DEĞİL.
199
+ function hasWriteRedirection(cmd) {
200
+ let inS = false, inD = false, esc = false;
201
+ for (const ch of cmd) {
202
+ if (esc) {
203
+ esc = false;
204
+ continue;
205
+ }
206
+ if (ch === '\\' && !inS) {
207
+ esc = true;
208
+ continue;
209
+ }
210
+ if (ch === "'" && !inD) {
211
+ inS = !inS;
212
+ continue;
213
+ }
214
+ if (ch === '"' && !inS) {
215
+ inD = !inD;
216
+ continue;
217
+ }
218
+ if (!inS && !inD && ch === '>')
219
+ return true;
220
+ }
221
+ return false;
222
+ }
223
+ function cmdIsReadOnly(single) {
224
+ const s = stripShellWrapper(single);
225
+ if (!s.trim())
226
+ return true;
227
+ if (detectCommandSubstitution(s))
228
+ return false;
229
+ if (hasWriteRedirection(s))
230
+ return false;
231
+ let tk = s.trim().split(/\s+/).filter(Boolean);
232
+ let i = 0;
233
+ while (i < tk.length && ENV_ASSIGN.test(tk[i]))
234
+ i++; // FOO=bar cmd -> atla
235
+ tk = tk.slice(i);
236
+ if (tk.length === 0)
237
+ return true;
238
+ const root = (tk[0].split(/[\\/]/).pop() || '').toLowerCase();
239
+ if (!READONLY_ROOTS.has(root))
240
+ return false;
241
+ const args = tk.slice(1);
242
+ if (VERSION_ONLY.has(root)) {
243
+ return args.some((a) => /^(--version|-v|-V|version)$/.test(a)) &&
244
+ !args.some((a) => a.startsWith('-') && !/^(-v|-V|--version)$/.test(a));
245
+ }
246
+ if (root === 'find') {
247
+ return !args.some((a) => BLOCKED_FIND.has(a.toLowerCase()) || BLOCKED_FIND_PREFIX.some((p) => a.toLowerCase().startsWith(p)));
248
+ }
249
+ if (root === 'sed') {
250
+ return !args.some((a) => a.startsWith('-i') || a === '--in-place');
251
+ }
252
+ if (READONLY_SUB[root]) {
253
+ let j = 0;
254
+ while (j < args.length && args[j].startsWith('-')) {
255
+ const f = args[j].toLowerCase();
256
+ if (f === '--version' || f === '--help')
257
+ return true;
258
+ j++;
259
+ }
260
+ if (j >= args.length)
261
+ return true;
262
+ const sub = args[j].toLowerCase();
263
+ if (!READONLY_SUB[root].has(sub))
264
+ return false;
265
+ const subArgs = args.slice(j + 1);
266
+ if (root === 'git' && sub === 'remote')
267
+ return !subArgs.some((a) => BLOCKED_GIT_REMOTE.has(a.toLowerCase()));
268
+ if (root === 'git' && sub === 'branch')
269
+ return !subArgs.some((a) => BLOCKED_GIT_BRANCH.has(a));
270
+ return true;
271
+ }
272
+ return true;
273
+ }
274
+ export function isShellCommandReadOnly(command) {
275
+ if (typeof command !== 'string' || !command.trim())
276
+ return false;
277
+ const segs = splitCommands(command);
278
+ return segs.length > 0 && segs.every(cmdIsReadOnly);
279
+ }
280
+ // ── Asıl güvenlik motoru ───────────────────────────────────────────────────
281
+ export function checkCommand(rawCommand) {
282
+ const command = stripShellWrapper(String(rawCommand || ''));
283
+ const roots = getCommandRoots(command);
284
+ // 1) Command substitution -> HARD DENY
285
+ if (detectCommandSubstitution(command)) {
286
+ return { decision: 'deny', reason: 'Komut ikamesi ($(), <(), backtick) güvenlik nedeniyle engellendi', roots };
287
+ }
288
+ // 2) Tehlikeli blocklist -> HARD DENY
289
+ for (const d of DANGER) {
290
+ try {
291
+ if (d.re instanceof RegExp && d.re.test(command))
292
+ return { decision: 'deny', reason: d.reason, roots };
293
+ }
294
+ catch { }
295
+ }
296
+ // 3) Read-only -> izinsiz geç
297
+ if (isShellCommandReadOnly(command))
298
+ return { decision: 'allow', reason: 'read-only', roots };
299
+ // 4) Kullanıcı allowlist'i (tüm kökler onaylıysa) -> izinsiz geç
300
+ const approved = loadApproved();
301
+ if (roots.length > 0 && roots.every((r) => approved.has(r))) {
302
+ return { decision: 'allow', reason: 'kullanıcı onaylı', roots };
303
+ }
304
+ // 5) Kalan -> onay iste
305
+ return { decision: 'confirm', roots };
306
+ }
package/dist/commands.js CHANGED
@@ -10,6 +10,7 @@ import * as usage from './usage.js';
10
10
  import { runCompact, contextPercent, autoCompactThreshold } from './compact.js';
11
11
  import { cmdDesc, setLang, saveLang, getLang } from './i18n.js';
12
12
  import { loadSkills, getSkills, getSkillsDir, installSkill, updateSkill, removeSkill, getRegistry } from './skills.js';
13
+ import { getApprovedCommands, approveCommands, unapproveCommands, clearApproved } from './cmdsec.js';
13
14
  import { isLearnEnabled, setLearnEnabled, getLearnFile, getLearnCount } from './learn.js';
14
15
  export const COMMANDS = [
15
16
  { name: '/help', desc: 'komutları ve ipuçlarını göster' },
@@ -36,6 +37,7 @@ export const COMMANDS = [
36
37
  { name: '/dream', desc: 'arka planda hafızayı konsolide et (.wormclaude/memory.md)' },
37
38
  { name: '/learn', desc: 'web-öğrenme datasını göster / aç-kapa (eğitim için)' },
38
39
  { name: '/copy', desc: 'son yaniti panoya kopyala (/copy all = tum sohbet)' },
40
+ { name: '/izinler', desc: 'onayli shell komutlarini yonet (list/add/remove/clear)' },
39
41
  { name: '/export', desc: 'sohbeti dosyaya kaydet' },
40
42
  { name: '/resume', desc: 'en son kaydedilen oturumu yükle' },
41
43
  { name: '/quit', desc: 'çıkış' },
@@ -537,6 +539,30 @@ export async function runSlashCommand(input, ctx) {
537
539
  : 'Pano aracı yok (win: clip · mac: pbcopy · linux: xclip/wl-copy). /export ile dosyaya kaydedebilirsin.');
538
540
  return true;
539
541
  }
542
+ case '/izinler': {
543
+ const sub = (arg.split(/\s+/)[0] || '').toLowerCase();
544
+ const vals = arg.split(/\s+/).slice(1).filter(Boolean);
545
+ if (sub === 'add' && vals.length) {
546
+ const n = approveCommands(vals);
547
+ ctx.note(n > 0 ? `✓ ${n} komut onaylandı: ${vals.join(', ')}` : 'Belirtilenler zaten onaylıydı.');
548
+ }
549
+ else if (sub === 'remove' && vals.length) {
550
+ const n = unapproveCommands(vals);
551
+ ctx.note(n > 0 ? `✓ ${n} komut onaydan çıkarıldı.` : 'Belirtilenler onaylı listede yoktu.');
552
+ }
553
+ else if (sub === 'clear') {
554
+ clearApproved();
555
+ ctx.note('✓ Tüm onaylı komutlar temizlendi.');
556
+ }
557
+ else {
558
+ const list = getApprovedCommands();
559
+ const body = list.length ? list.map((c) => ` • ${c}`).join('\n') : ' (henüz yok — "Evet, hep" seçince eklenir)';
560
+ ctx.note(`Onaylı shell komutları (${list.length}) — bunlar onaysız çalışır:\n${body}\n\n` +
561
+ `Kullanım:\n /izinler → listele\n /izinler add git npm → onayla\n /izinler remove git → çıkar\n /izinler clear → hepsini temizle\n` +
562
+ `(Tehlikeli komutlar — rm -rf /, fork bomb vb. — onaylasan da engellenir.)`);
563
+ }
564
+ return true;
565
+ }
540
566
  case '/export': {
541
567
  fs.mkdirSync(SESSION_DIR, { recursive: true });
542
568
  const file = path.join(SESSION_DIR, `session-${tsStamp()}.json`);
@@ -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
+ }
@@ -11,6 +11,7 @@ import * as fs from 'node:fs';
11
11
  import * as os from 'node:os';
12
12
  import * as path from 'node:path';
13
13
  import { fileURLToPath } from 'node:url';
14
+ import { resolveInjections } from './injections.js';
14
15
  let EXTENSIONS = [];
15
16
  // Pakete gömülü extensions (npm ile gelir) + kullanıcı + proje dizinleri.
16
17
  const BUNDLED_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'extensions');
@@ -121,15 +122,20 @@ export function getExtCommand(name) {
121
122
  export function getExcludedTools() {
122
123
  return [...new Set(EXTENSIONS.flatMap((x) => x.excludeTools))];
123
124
  }
124
- // Komut prompt'unu kök dizin referansı + kullanıcı argümanı ile birleştirir.
125
- // {{args}} placeholder'ı varsa onunla değişir; yoksa argüman sona eklenir.
125
+ // Komut prompt'unu çözer: !{komut} (shell çıktısı, cmdsec'li) · @{dosya} (içerik) · {{args}}.
126
+ // {{args}} placeholder'ı yoksa kullanıcı argümanı sona eklenir.
126
127
  export function buildExtCommandPrompt(cmd, args) {
127
- let p = cmd.prompt;
128
- if (p.includes('{{args}}')) {
129
- p = p.replace(/\{\{args\}\}/g, args || '');
128
+ const a = args || '';
129
+ const hasArgsPlaceholder = cmd.prompt.includes('{{args}}');
130
+ let p;
131
+ try {
132
+ p = resolveInjections(cmd.prompt, { args: a, cwd: process.cwd(), contextName: cmd.name });
133
+ }
134
+ catch (e) {
135
+ p = cmd.prompt + `\n\n[injection hatası: ${e?.message || e}]`;
130
136
  }
131
- else if (args && args.trim()) {
132
- p += `\n\n## User input\n\n${args.trim()}`;
137
+ if (!hasArgsPlaceholder && a.trim()) {
138
+ p += `\n\n## User input\n\n${a.trim()}`;
133
139
  }
134
140
  return (`Extension base directory: ${cmd.dir}\n` +
135
141
  `(You may Read files here — templates/, context, assets — as needed to complete this command.)\n\n` +