wormclaude 1.0.13 → 1.0.14

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/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`);
@@ -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` +
@@ -0,0 +1,108 @@
1
+ // Komut şablonu injection'ları — extension komutlarında dinamik içerik.
2
+ // Gemini/Blackbox CLI injectionParser + shellProcessor + atFileProcessor'dan uyarlandı.
3
+ // {{args}} -> kullanıcı argümanı (ham; !{} içinde shell-escape'li)
4
+ // !{komut} -> shell çıktısı gömer (cmdsec güvenlik kontrolüyle)
5
+ // @{dosya} -> dosya içeriği gömer (cwd'ye göre)
6
+ import { execSync } from 'node:child_process';
7
+ import * as fs from 'node:fs';
8
+ import * as path from 'node:path';
9
+ import { checkCommand, escapeShellArg } from './cmdsec.js';
10
+ const ARGS = '{{args}}';
11
+ const SHELL_TRIG = '!{';
12
+ const FILE_TRIG = '@{';
13
+ // Brace-sayan parser (injectionParser.js — temiz, birebir).
14
+ export function extractInjections(prompt, trigger, contextName) {
15
+ const injections = [];
16
+ let index = 0;
17
+ while (index < prompt.length) {
18
+ const startIndex = prompt.indexOf(trigger, index);
19
+ if (startIndex === -1)
20
+ break;
21
+ let i = startIndex + trigger.length;
22
+ let depth = 1;
23
+ let closed = false;
24
+ while (i < prompt.length) {
25
+ const ch = prompt[i];
26
+ if (ch === '{')
27
+ depth++;
28
+ else if (ch === '}') {
29
+ depth--;
30
+ if (depth === 0) {
31
+ injections.push({ content: prompt.substring(startIndex + trigger.length, i).trim(), startIndex, endIndex: i + 1 });
32
+ index = i + 1;
33
+ closed = true;
34
+ break;
35
+ }
36
+ }
37
+ i++;
38
+ }
39
+ if (!closed) {
40
+ const ctx = contextName ? ` ('${contextName}')` : '';
41
+ throw new Error(`Geçersiz injection${ctx}: ${trigger} kapanmamış (index ${startIndex}). Süslü parantezler dengeli olmalı.`);
42
+ }
43
+ }
44
+ return injections;
45
+ }
46
+ function runShell(cmd, cwd) {
47
+ const chk = checkCommand(cmd);
48
+ if (chk.decision === 'deny')
49
+ return `[engellendi: ${chk.reason || 'tehlikeli komut'}]`;
50
+ try {
51
+ const out = execSync(cmd, {
52
+ cwd, encoding: 'utf8', timeout: 30000, maxBuffer: 1024 * 1024,
53
+ windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'],
54
+ });
55
+ return String(out).trim();
56
+ }
57
+ catch (e) {
58
+ const o = (e?.stdout ? String(e.stdout) : '') + (e?.stderr ? String(e.stderr) : '');
59
+ return (o.trim() || `[komut hatası: ${e?.message || e}]`);
60
+ }
61
+ }
62
+ function readFileInjection(p, cwd) {
63
+ try {
64
+ const abs = path.isAbsolute(p) ? p : path.resolve(cwd, p);
65
+ const data = fs.readFileSync(abs, 'utf8');
66
+ return `--- ${p} ---\n${data}\n--- /${p} ---`;
67
+ }
68
+ catch {
69
+ return `[dosya okunamadı: ${p}]`;
70
+ }
71
+ }
72
+ // Bir injection türünü çözer; resolver(content) çıktıyı döndürür.
73
+ function replaceTrigger(text, trigger, resolver, ctx) {
74
+ const injs = extractInjections(text, trigger, ctx);
75
+ if (injs.length === 0)
76
+ return text;
77
+ let out = '';
78
+ let last = 0;
79
+ for (const inj of injs) {
80
+ out += text.substring(last, inj.startIndex);
81
+ out += resolver(inj.content);
82
+ last = inj.endIndex;
83
+ }
84
+ out += text.substring(last);
85
+ return out;
86
+ }
87
+ // Şablonu çöz: !{komut} -> shell çıktısı, @{dosya} -> dosya, {{args}} -> argüman.
88
+ export function resolveInjections(text, opts = {}) {
89
+ const args = opts.args ?? '';
90
+ const cwd = opts.cwd ?? process.cwd();
91
+ const ctx = opts.contextName;
92
+ let out = text;
93
+ // 1) !{komut}: içindeki {{args}} shell-escape'li, sonra cmdsec ile çalıştır.
94
+ if (out.includes(SHELL_TRIG)) {
95
+ const shell = process.platform === 'win32' ? 'cmd' : 'bash';
96
+ out = replaceTrigger(out, SHELL_TRIG, (cmd) => {
97
+ const resolved = cmd.split(ARGS).join(escapeShellArg(args, shell));
98
+ return runShell(resolved, cwd);
99
+ }, ctx);
100
+ }
101
+ // 2) @{dosya}: içindeki {{args}} ham, sonra oku.
102
+ if (out.includes(FILE_TRIG)) {
103
+ out = replaceTrigger(out, FILE_TRIG, (p) => readFileInjection(p.split(ARGS).join(args), cwd), ctx);
104
+ }
105
+ // 3) Kalan {{args}} -> ham argüman.
106
+ out = out.split(ARGS).join(args);
107
+ return out;
108
+ }
package/dist/theme.js CHANGED
@@ -8,4 +8,4 @@ export const theme = {
8
8
  green: '#4ade80',
9
9
  errorRed: '#ff6b6b',
10
10
  };
11
- export const VERSION = '1.0.13';
11
+ export const VERSION = '1.0.14';
package/dist/tools.js CHANGED
@@ -12,6 +12,8 @@ import { tasks } from './tasks.js';
12
12
  import { getMcpToolSchemas, callMcpTool } from './mcp.js';
13
13
  import { getSkills, getSkill, buildSkillPrompt } from './skills.js';
14
14
  import { getExcludedTools } from './extensions.js';
15
+ import { checkCommand } from './cmdsec.js';
16
+ import * as Diff from 'diff';
15
17
  import * as computer from './computer.js';
16
18
  // Agent/alt-agent araçlarının backend'e ulaşması için config. cli.tsx başlangıçta
17
19
  // setToolConfig ile aynı (mutable) config nesnesini verir → /config değişiklikleri görülür.
@@ -602,6 +604,17 @@ async function execOne(call, hooks) {
602
604
  if (planMode && !isReadOnly(call.name)) {
603
605
  return { ok: false, output: 'Plan modunda — yazma/komut engellendi. Önce ExitPlanMode ile planı onaylat.', args };
604
606
  }
607
+ // 3.5) Komut güvenliği (Bash/PowerShell) — cmdsec: deny→blokla, allow→izinsiz, confirm→izin akışı
608
+ if ((call.name === 'Bash' || call.name === 'PowerShell') && args && args.command) {
609
+ const chk = checkCommand(String(args.command));
610
+ if (chk.decision === 'deny') {
611
+ return { ok: false, output: `⛔ Güvenlik: komut engellendi — ${chk.reason || 'tehlikeli komut'}`, args };
612
+ }
613
+ if (chk.decision === 'allow') {
614
+ const res = await executeTool(call.name, args);
615
+ return { ...res, args };
616
+ }
617
+ }
605
618
  // 4) İzin (gerekiyorsa)
606
619
  if (needsPermission(call.name) && hooks?.confirm) {
607
620
  const decision = await hooks.confirm(call, args);
@@ -764,6 +777,23 @@ const TYPE_EXT = {
764
777
  sh: ['sh', 'bash', 'zsh'],
765
778
  };
766
779
  // ── Executor ──────────────────────────────────────────────────────────────────
780
+ // Edit/Write icin +/- satir ozeti (Diff paketiyle).
781
+ function diffStat(oldStr, newStr) {
782
+ try {
783
+ let added = 0, removed = 0;
784
+ for (const part of Diff.diffLines(oldStr || '', newStr || '')) {
785
+ const n = part.count || (part.value ? part.value.split('\n').filter(Boolean).length : 0);
786
+ if (part.added)
787
+ added += n;
788
+ else if (part.removed)
789
+ removed += n;
790
+ }
791
+ return (added || removed) ? ` (+${added} -${removed})` : '';
792
+ }
793
+ catch {
794
+ return '';
795
+ }
796
+ }
767
797
  export async function executeTool(name, args) {
768
798
  try {
769
799
  if (name === 'See') {
@@ -877,9 +907,16 @@ export async function executeTool(name, args) {
877
907
  if (fs.existsSync(fp) && !readFiles.has(norm(fp)))
878
908
  return { ok: false, output: 'Error: existing file must be read first. Use the Read tool before overwriting.' };
879
909
  fs.mkdirSync(path.dirname(path.resolve(fp)), { recursive: true });
880
- fs.writeFileSync(fp, args.content ?? '');
910
+ const _wnew = args.content ?? '';
911
+ const _wold = (() => { try {
912
+ return fs.readFileSync(fp, 'utf8');
913
+ }
914
+ catch {
915
+ return '';
916
+ } })();
917
+ fs.writeFileSync(fp, _wnew);
881
918
  readFiles.add(norm(fp));
882
- return { ok: true, output: `Wrote ${fp} (${(args.content || '').length} chars)` };
919
+ return { ok: true, output: `Wrote ${fp} (${_wnew.length} chars)${diffStat(_wold, _wnew)}` };
883
920
  }
884
921
  if (name === 'Edit') {
885
922
  const fp = args.file_path;
@@ -900,9 +937,10 @@ export async function executeTool(name, args) {
900
937
  ok: false,
901
938
  output: `Error: old_string is not unique (${count} matches). Provide more surrounding context or set replace_all: true.`,
902
939
  };
940
+ const _ebefore = c;
903
941
  c = args.replace_all ? c.split(oldStr).join(newStr) : c.replace(oldStr, newStr);
904
942
  fs.writeFileSync(fp, c);
905
- return { ok: true, output: `Edited ${fp}${args.replace_all ? ` (${count} occurrences)` : ''}` };
943
+ return { ok: true, output: `Edited ${fp}${args.replace_all ? ` (${count} occurrences)` : ''}${diffStat(_ebefore, c)}` };
906
944
  }
907
945
  if (name === 'Glob') {
908
946
  const base = args.path || process.cwd();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wormclaude",
3
- "version": "1.0.13",
3
+ "version": "1.0.14",
4
4
  "description": "WormClaude CLI - uncensored security+code assistant (ink TUI, Claude-style)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,6 +14,8 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "@modelcontextprotocol/sdk": "^1.29.0",
17
+ "@types/diff": "^7.0.2",
18
+ "diff": "^9.0.0",
17
19
  "ink": "^5.0.1",
18
20
  "ink-spinner": "^5.0.0",
19
21
  "ink-text-input": "^6.0.0",