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 +306 -0
- package/dist/commands.js +26 -0
- package/dist/extensions.js +13 -7
- package/dist/injections.js +108 -0
- package/dist/theme.js +1 -1
- package/dist/tools.js +41 -3
- package/package.json +3 -1
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`);
|
package/dist/extensions.js
CHANGED
|
@@ -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
|
|
125
|
-
// {{args}} placeholder'ı
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
132
|
-
p += `\n\n## User input\n\n${
|
|
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
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
|
-
|
|
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} (${
|
|
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.
|
|
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",
|