wormclaude 1.0.12 → 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/cli.js +12 -1
- package/dist/cmdsec.js +306 -0
- package/dist/commands.js +26 -0
- package/dist/extensions.js +143 -0
- package/dist/injections.js +108 -0
- package/dist/theme.js +1 -1
- package/dist/tools.js +45 -4
- package/extensions/code-review/README.md +22 -0
- package/extensions/code-review/commands/review/best-practices.toml +15 -0
- package/extensions/code-review/commands/review/performance.toml +14 -0
- package/extensions/code-review/commands/review/security.toml +16 -0
- package/extensions/code-review/wormclaude-extension.json +6 -0
- package/extensions/conductor/README.md +250 -0
- package/extensions/conductor/commands/conductor/implement.toml +15 -0
- package/extensions/conductor/commands/conductor/newTrack.toml +16 -0
- package/extensions/conductor/commands/conductor/revert.toml +14 -0
- package/extensions/conductor/commands/conductor/setup.toml +16 -0
- package/extensions/conductor/commands/conductor/status.toml +15 -0
- package/extensions/conductor/templates/.wormclaudeignore +37 -0
- package/extensions/conductor/templates/code_styleguides/go.md +48 -0
- package/extensions/conductor/templates/code_styleguides/html-css.md +49 -0
- package/extensions/conductor/templates/code_styleguides/javascript.md +51 -0
- package/extensions/conductor/templates/code_styleguides/python.md +37 -0
- package/extensions/conductor/templates/code_styleguides/typescript.md +43 -0
- package/extensions/conductor/templates/general.md +23 -0
- package/extensions/conductor/templates/plan.md +18 -0
- package/extensions/conductor/templates/product-guidelines.md +25 -0
- package/extensions/conductor/templates/product.md +21 -0
- package/extensions/conductor/templates/tech-stack.md +25 -0
- package/extensions/conductor/templates/workflow.md +138 -0
- package/extensions/conductor/wormclaude-extension.json +6 -0
- package/extensions/git-helper/README.md +16 -0
- package/extensions/git-helper/commands/git/branch-cleanup.toml +13 -0
- package/extensions/git-helper/commands/git/smart-commit.toml +13 -0
- package/extensions/git-helper/wormclaude-extension.json +6 -0
- package/extensions/greet/README.md +76 -0
- package/extensions/greet/commands/greet.toml +4 -0
- package/extensions/greet/wormclaude-extension.json +6 -0
- package/extensions/registry.json +52 -0
- package/extensions/test-generator/README.md +22 -0
- package/extensions/test-generator/commands/test/coverage.toml +12 -0
- package/extensions/test-generator/commands/test/integration.toml +13 -0
- package/extensions/test-generator/commands/test/unit.toml +13 -0
- package/extensions/test-generator/wormclaude-extension.json +6 -0
- package/package.json +5 -2
package/dist/cli.js
CHANGED
|
@@ -12,6 +12,7 @@ import { t, cmdDesc, setLang, saveLang, loadLang, getLang } from './i18n.js';
|
|
|
12
12
|
import { linkify } from './links.js';
|
|
13
13
|
import { recordLearned } from './learn.js';
|
|
14
14
|
import { loadSkills, getSkills, getSkill, buildSkillPrompt } from './skills.js';
|
|
15
|
+
import { loadExtensions, getExtCommands, getExtCommand, buildExtCommandPrompt } from './extensions.js';
|
|
15
16
|
import { COMMANDS, runSlashCommand } from './commands.js';
|
|
16
17
|
import { tasks } from './tasks.js';
|
|
17
18
|
import { connectMcpServers } from './mcp.js';
|
|
@@ -61,6 +62,7 @@ setToolConfig(config); // Agent/alt-agent araçları aynı config'i kullanır
|
|
|
61
62
|
const MAX_TURNS = Number(process.env.WORMCLAUDE_MAX_TURNS) || 50; // tur limiti
|
|
62
63
|
setLang(loadLang() ?? 'tr'); // kayıtlı dili yükle (yoksa tr)
|
|
63
64
|
loadSkills(); // .wormclaude/skills/*.md yükle
|
|
65
|
+
loadExtensions(); // extensions/<ad>/ (gömülü + kullanıcı + proje) yükle
|
|
64
66
|
// Kalıcı hafıza: açılışta .wormclaude/memory.md + WORMCLAUDE.md'yi context'e yükle
|
|
65
67
|
const _memCtx = loadMemoryContext();
|
|
66
68
|
const _envContext = () => {
|
|
@@ -411,10 +413,11 @@ function App() {
|
|
|
411
413
|
abortRef.current?.abort();
|
|
412
414
|
}
|
|
413
415
|
});
|
|
414
|
-
// Slash menüsü filtresi (yerleşik komutlar + skill'ler)
|
|
416
|
+
// Slash menüsü filtresi (yerleşik komutlar + skill'ler + extension komutları)
|
|
415
417
|
const allCommands = () => [
|
|
416
418
|
...COMMANDS,
|
|
417
419
|
...getSkills().map((s) => ({ name: '/' + s.name, desc: s.description })),
|
|
420
|
+
...getExtCommands().map((c) => ({ name: '/' + c.name, desc: c.description })),
|
|
418
421
|
];
|
|
419
422
|
const cmdFilter = (inp) => {
|
|
420
423
|
const tok = inp.split(' ')[0];
|
|
@@ -724,6 +727,14 @@ function App() {
|
|
|
724
727
|
}
|
|
725
728
|
return;
|
|
726
729
|
}
|
|
730
|
+
// Extension komutu mu? (/ns:cmd veya /cmd — yerleşik komut/skill değilse)
|
|
731
|
+
const extCmd = (!builtin && !skill) ? getExtCommand(tok.slice(1)) : undefined;
|
|
732
|
+
if (extCmd) {
|
|
733
|
+
const extArgs = v.slice(tok.length).trim();
|
|
734
|
+
const prompt = buildExtCommandPrompt(extCmd, extArgs);
|
|
735
|
+
runAgent(prompt, `/${extCmd.name}${extArgs ? ' ' + extArgs : ''}`);
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
727
738
|
const ctx = {
|
|
728
739
|
config,
|
|
729
740
|
getHistory: () => historyRef.current,
|
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,143 @@
|
|
|
1
|
+
// Extension sistemi — wormclaude-extension.json (manifest) + commands/**/*.toml + context dosyası.
|
|
2
|
+
// Namespaced slash komutları (/conductor:setup, /git:smart-commit) + excludeTools desteği.
|
|
3
|
+
//
|
|
4
|
+
// extensions/<ad>/
|
|
5
|
+
// wormclaude-extension.json { name, version, contextFileName?, excludeTools? }
|
|
6
|
+
// commands/<ns>/<cmd>.toml → /<ns>:<cmd> (description = "...", prompt = """...""")
|
|
7
|
+
// commands/<cmd>.toml → /<cmd> (namespace yoksa)
|
|
8
|
+
// <contextFileName> (opsiyonel; aktifken context'e yüklenebilir)
|
|
9
|
+
// templates/ ... (opsiyonel; komutlar Read ile kullanır)
|
|
10
|
+
import * as fs from 'node:fs';
|
|
11
|
+
import * as os from 'node:os';
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { resolveInjections } from './injections.js';
|
|
15
|
+
let EXTENSIONS = [];
|
|
16
|
+
// Pakete gömülü extensions (npm ile gelir) + kullanıcı + proje dizinleri.
|
|
17
|
+
const BUNDLED_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'extensions');
|
|
18
|
+
const USER_DIR = path.join(os.homedir(), '.wormclaude', 'extensions');
|
|
19
|
+
const PROJECT_DIR = path.join(process.cwd(), '.wormclaude', 'extensions');
|
|
20
|
+
// Minimal TOML: yalnızca `description = "..."` ve `prompt = """..."""` (veya tek satır) okur.
|
|
21
|
+
function parseToml(raw) {
|
|
22
|
+
let description = '';
|
|
23
|
+
let prompt = '';
|
|
24
|
+
const dm = raw.match(/^\s*description\s*=\s*(?:"([^"]*)"|'([^']*)')/m);
|
|
25
|
+
if (dm)
|
|
26
|
+
description = (dm[1] ?? dm[2] ?? '').trim();
|
|
27
|
+
const pMulti = raw.match(/prompt\s*=\s*"""([\s\S]*?)"""/);
|
|
28
|
+
if (pMulti) {
|
|
29
|
+
prompt = pMulti[1].replace(/^\r?\n/, '').replace(/\s+$/, '');
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
const pSingle = raw.match(/^\s*prompt\s*=\s*"([\s\S]*?)"\s*$/m);
|
|
33
|
+
if (pSingle)
|
|
34
|
+
prompt = pSingle[1];
|
|
35
|
+
}
|
|
36
|
+
return { description, prompt };
|
|
37
|
+
}
|
|
38
|
+
function walkCommands(commandsDir, extName, extDir, ns, out) {
|
|
39
|
+
let entries = [];
|
|
40
|
+
try {
|
|
41
|
+
entries = fs.readdirSync(commandsDir, { withFileTypes: true });
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
for (const e of entries) {
|
|
47
|
+
const full = path.join(commandsDir, e.name);
|
|
48
|
+
if (e.isDirectory()) {
|
|
49
|
+
walkCommands(full, extName, extDir, ns ? `${ns}:${e.name}` : e.name, out);
|
|
50
|
+
}
|
|
51
|
+
else if (e.name.endsWith('.toml')) {
|
|
52
|
+
let raw = '';
|
|
53
|
+
try {
|
|
54
|
+
raw = fs.readFileSync(full, 'utf8');
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const { description, prompt } = parseToml(raw);
|
|
60
|
+
if (!prompt)
|
|
61
|
+
continue;
|
|
62
|
+
const base = e.name.replace(/\.toml$/, '');
|
|
63
|
+
const name = ns ? `${ns}:${base}` : base;
|
|
64
|
+
out.push({ name, description: description || name, prompt, ext: extName, dir: extDir });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function scanExtDir(dir) {
|
|
69
|
+
let entries = [];
|
|
70
|
+
try {
|
|
71
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
for (const e of entries) {
|
|
77
|
+
if (!e.isDirectory())
|
|
78
|
+
continue;
|
|
79
|
+
const extDir = path.join(dir, e.name);
|
|
80
|
+
let manifest = {};
|
|
81
|
+
try {
|
|
82
|
+
manifest = JSON.parse(fs.readFileSync(path.join(extDir, 'wormclaude-extension.json'), 'utf8'));
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const name = String(manifest.name || e.name).trim();
|
|
88
|
+
const commands = [];
|
|
89
|
+
walkCommands(path.join(extDir, 'commands'), name, extDir, '', commands);
|
|
90
|
+
const ctxName = manifest.contextFileName;
|
|
91
|
+
let contextFile;
|
|
92
|
+
if (ctxName && fs.existsSync(path.join(extDir, ctxName)))
|
|
93
|
+
contextFile = path.join(extDir, ctxName);
|
|
94
|
+
const ext = {
|
|
95
|
+
name,
|
|
96
|
+
version: String(manifest.version || '0.0.0'),
|
|
97
|
+
dir: extDir,
|
|
98
|
+
contextFile,
|
|
99
|
+
excludeTools: Array.isArray(manifest.excludeTools) ? manifest.excludeTools.map(String) : [],
|
|
100
|
+
commands,
|
|
101
|
+
};
|
|
102
|
+
// Proje > kullanıcı > gömülü: aynı ada sahip extension override edilir.
|
|
103
|
+
const i = EXTENSIONS.findIndex((x) => x.name === name);
|
|
104
|
+
if (i >= 0)
|
|
105
|
+
EXTENSIONS[i] = ext;
|
|
106
|
+
else
|
|
107
|
+
EXTENSIONS.push(ext);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export function loadExtensions() {
|
|
111
|
+
EXTENSIONS = [];
|
|
112
|
+
scanExtDir(BUNDLED_DIR); // gömülü
|
|
113
|
+
scanExtDir(USER_DIR); // kullanıcı geneli
|
|
114
|
+
scanExtDir(PROJECT_DIR); // proje (en yüksek öncelik)
|
|
115
|
+
return EXTENSIONS;
|
|
116
|
+
}
|
|
117
|
+
export function getExtensions() { return EXTENSIONS; }
|
|
118
|
+
export function getExtCommands() { return EXTENSIONS.flatMap((x) => x.commands); }
|
|
119
|
+
export function getExtCommand(name) {
|
|
120
|
+
return getExtCommands().find((c) => c.name === name);
|
|
121
|
+
}
|
|
122
|
+
export function getExcludedTools() {
|
|
123
|
+
return [...new Set(EXTENSIONS.flatMap((x) => x.excludeTools))];
|
|
124
|
+
}
|
|
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.
|
|
127
|
+
export function buildExtCommandPrompt(cmd, 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}]`;
|
|
136
|
+
}
|
|
137
|
+
if (!hasArgsPlaceholder && a.trim()) {
|
|
138
|
+
p += `\n\n## User input\n\n${a.trim()}`;
|
|
139
|
+
}
|
|
140
|
+
return (`Extension base directory: ${cmd.dir}\n` +
|
|
141
|
+
`(You may Read files here — templates/, context, assets — as needed to complete this command.)\n\n` +
|
|
142
|
+
p);
|
|
143
|
+
}
|
|
@@ -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