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.
Files changed (45) hide show
  1. package/dist/cli.js +12 -1
  2. package/dist/cmdsec.js +306 -0
  3. package/dist/commands.js +26 -0
  4. package/dist/extensions.js +143 -0
  5. package/dist/injections.js +108 -0
  6. package/dist/theme.js +1 -1
  7. package/dist/tools.js +45 -4
  8. package/extensions/code-review/README.md +22 -0
  9. package/extensions/code-review/commands/review/best-practices.toml +15 -0
  10. package/extensions/code-review/commands/review/performance.toml +14 -0
  11. package/extensions/code-review/commands/review/security.toml +16 -0
  12. package/extensions/code-review/wormclaude-extension.json +6 -0
  13. package/extensions/conductor/README.md +250 -0
  14. package/extensions/conductor/commands/conductor/implement.toml +15 -0
  15. package/extensions/conductor/commands/conductor/newTrack.toml +16 -0
  16. package/extensions/conductor/commands/conductor/revert.toml +14 -0
  17. package/extensions/conductor/commands/conductor/setup.toml +16 -0
  18. package/extensions/conductor/commands/conductor/status.toml +15 -0
  19. package/extensions/conductor/templates/.wormclaudeignore +37 -0
  20. package/extensions/conductor/templates/code_styleguides/go.md +48 -0
  21. package/extensions/conductor/templates/code_styleguides/html-css.md +49 -0
  22. package/extensions/conductor/templates/code_styleguides/javascript.md +51 -0
  23. package/extensions/conductor/templates/code_styleguides/python.md +37 -0
  24. package/extensions/conductor/templates/code_styleguides/typescript.md +43 -0
  25. package/extensions/conductor/templates/general.md +23 -0
  26. package/extensions/conductor/templates/plan.md +18 -0
  27. package/extensions/conductor/templates/product-guidelines.md +25 -0
  28. package/extensions/conductor/templates/product.md +21 -0
  29. package/extensions/conductor/templates/tech-stack.md +25 -0
  30. package/extensions/conductor/templates/workflow.md +138 -0
  31. package/extensions/conductor/wormclaude-extension.json +6 -0
  32. package/extensions/git-helper/README.md +16 -0
  33. package/extensions/git-helper/commands/git/branch-cleanup.toml +13 -0
  34. package/extensions/git-helper/commands/git/smart-commit.toml +13 -0
  35. package/extensions/git-helper/wormclaude-extension.json +6 -0
  36. package/extensions/greet/README.md +76 -0
  37. package/extensions/greet/commands/greet.toml +4 -0
  38. package/extensions/greet/wormclaude-extension.json +6 -0
  39. package/extensions/registry.json +52 -0
  40. package/extensions/test-generator/README.md +22 -0
  41. package/extensions/test-generator/commands/test/coverage.toml +12 -0
  42. package/extensions/test-generator/commands/test/integration.toml +13 -0
  43. package/extensions/test-generator/commands/test/unit.toml +13 -0
  44. package/extensions/test-generator/wormclaude-extension.json +6 -0
  45. 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
@@ -8,4 +8,4 @@ export const theme = {
8
8
  green: '#4ade80',
9
9
  errorRed: '#ff6b6b',
10
10
  };
11
- export const VERSION = '1.0.12';
11
+ export const VERSION = '1.0.14';