tillerhand 3.27.2 → 3.28.0
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/.claude/board/rules.md +2 -2
- package/.claude/commands/build.md +1 -1
- package/.claude/commands/plan-task.md +1 -1
- package/.claude/hooks/commit-gate.mjs +43 -14
- package/.claude/hooks/cost.mjs +9 -2
- package/.claude/hooks/destructive-guard.mjs +18 -3
- package/.claude/hooks/freeze-guard.mjs +2 -2
- package/.claude/hooks/gate.mjs +1 -1
- package/.claude/hooks/notify.mjs +47 -36
- package/.claude/hooks/package-gate.mjs +74 -7
- package/.claude/settings.json +3 -2
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/kit/CHANGELOG.md +17 -0
- package/kit/VERSION +1 -1
- package/kit/guide.html +4 -4
- package/kit/kit.mjs +3 -1
- package/kit/templates/mobile/.claude/hooks/format.mjs +5 -3
- package/kit/templates/web-saas/.claude/hooks/format.mjs +5 -3
- package/package.json +1 -1
package/.claude/board/rules.md
CHANGED
|
@@ -44,7 +44,7 @@ Kurulda olmayan bir üyeyi çağırma; ilgili alan kurulda yoksa durum bloğunda
|
|
|
44
44
|
## 4. Toplantı nasıl yapılır
|
|
45
45
|
|
|
46
46
|
1. **Limit kontrolü** (`docs/workflow/usage-limits.md`): Seviye sarıysa üye sayısını yarıya indir; kırmızıysa kullanıcıya sor (kurulu hafiflet seçeneği dahil).
|
|
47
|
-
1b. **Deterministik kapı** (commit, görev sonu, faz sonu; plan toplantısında yok): `node .claude/hooks/gate.mjs --run`. AGENTS.md → Komutlar'daki lint, tip kontrolü ve test ile Semgrep çalışır; sonuç değişiklik imzasıyla saklanır. Kırmızıysa veya boşsa (hiçbir kontrol çalışmadı) commit kapısı açılmaz: önce düzelt ya da Komutlar'ı doldur. Kullanıcı ara commit için açıkça isterse
|
|
47
|
+
1b. **Deterministik kapı** (commit, görev sonu, faz sonu; plan toplantısında yok): `node .claude/hooks/gate.mjs --run`. AGENTS.md → Komutlar'daki lint, tip kontrolü ve test ile Semgrep çalışır; sonuç değişiklik imzasıyla saklanır. Kırmızıysa veya boşsa (hiçbir kontrol çalışmadı) commit kapısı açılmaz: önce düzelt ya da Komutlar'ı doldur. Kullanıcı ara commit için açıkça isterse `node .claude/hooks/commit-gate.mjs --skip "kullanıcı isteği"` (her zaman onaya sorulur). Kapı "komut onayı gerekli" derse komutları kullanıcıya göster; onaylarsa `node .claude/hooks/gate.mjs --approve` (yeni veya değişen komut onaysız çalışmaz).
|
|
48
48
|
2. **Üyeleri paralel çalıştır** (Agent aracı, her üye ayrı alt ajan). Her üyeye şunları ver: toplantı türü, kapsam (plan metni / diff aralığı / faz), görev kodu, kabul kriterleri ve kapı çıktısı (varsa). Commit toplantısında `model: "haiku"` ver.
|
|
49
49
|
2b. **Konsey toplantısıysa** (§2) `.claude/board/council.md` aşama 1-4'ü uygula. Sentezi başkanın kararından kur; görüş ayrılığı satırına azınlık görüşünü yaz.
|
|
50
50
|
3. **Sentezle** (ana ajan; konseyde başkanın kararına göre):
|
|
@@ -62,7 +62,7 @@ Kullanıcıya sorulacaklar: [üyelerin işaretlediği kararlar]
|
|
|
62
62
|
5. **Kaydet:** `docs/workflow/board.md` → toplantı günlüğüne bir satır ekle. Kararları `docs/decisions/log.md` günlüğüne ekle. Açık kalan bulguları `docs/workflow/progress.md` → "Açık sorunlar"a yaz.
|
|
63
63
|
6. **Commit toplantısı sonucu ONAY veya KOŞULLU ONAY ise** commit kapısını aç: `node .claude/hooks/commit-gate.mjs --approve "<katılan üyeler>"`. RET ise açma; önce kullanıcıya sor.
|
|
64
64
|
|
|
65
|
-
Kullanıcı commit denetimini o an için atlamak isterse açıkça söylemesi gerekir. O durumda `node .claude/hooks/commit-gate.mjs --
|
|
65
|
+
Kullanıcı commit denetimini o an için atlamak isterse açıkça söylemesi gerekir. O durumda `node .claude/hooks/commit-gate.mjs --skip "kullanıcı isteği"` çalıştır (onaya sorulur) ve günlüğe yaz. Kalıcı olarak kapatmak için: `docs/workflow/board.md` → `**Commit denetimi:** kapalı`.
|
|
66
66
|
|
|
67
67
|
## 5. Kurul nasıl kurulur (`/start` Kurul adımı veya `/board setup`)
|
|
68
68
|
|
|
@@ -31,7 +31,7 @@ Boşsa `docs/workflow/progress.md` → "Sıradaki adım" veya `docs/product/road
|
|
|
31
31
|
6. Arayüz değiştiyse `impeccable:impeccable` → `critique` çalıştır; önemli bulguları düzeltip düzeltmeyeceğimi sor.
|
|
32
32
|
6b. **Görev sonu kurul toplantısı** (`.claude/board/rules.md`; kurul kurulduysa): Değişikliğe ilgili en fazla 3 üye. RET varsa görev bitmez; bulguyu ve seçenekleri sor. Toplantıyı `docs/workflow/board.md` günlüğüne yaz.
|
|
33
33
|
7. `AGENTS.md` → "Bitti tanımı" listesini tek tek kontrol et.
|
|
34
|
-
8. Kapsam kalkanını kapat: `node .claude/hooks/freeze-guard.mjs --plan-end`. `docs/product/roadmap.md`'de görevi işaretle; PRD'de sadece testi yeşil olan kriterleri işaretle. Testleri JUnit raporuyla çalıştır (`docs/architecture/stack.md` → Kabul testleri) ki pano güncellensin. Limiti tekrar oku ve `docs/workflow/cost-log.md`'ye satır ekle (başlangıç/bitiş 5 saatlik %, süre dk).
|
|
34
|
+
8. Kapsam kalkanını kapat (komut kullanıcıya sorulur): `node .claude/hooks/freeze-guard.mjs --plan-end`. `docs/product/roadmap.md`'de görevi işaretle; PRD'de sadece testi yeşil olan kriterleri işaretle. Testleri JUnit raporuyla çalıştır (`docs/architecture/stack.md` → Kabul testleri) ki pano güncellensin. Limiti tekrar oku ve `docs/workflow/cost-log.md`'ye satır ekle (başlangıç/bitiş 5 saatlik %, süre dk).
|
|
35
35
|
9. Rapor ver:
|
|
36
36
|
- Ne yapıldı (2-3 satır)
|
|
37
37
|
- Bu görevde alınan kararlar (kullanıcı / otomatik)
|
|
@@ -41,4 +41,4 @@ Bu adımda **kod yazma**, sadece plan çıkar.
|
|
|
41
41
|
|
|
42
42
|
Planı onaylatmak için AskUserQuestion kullan: "Onayla / Şunu değiştir / Görevi böl". Onayımı bekle. Onaydan sonra `/build` ile uygulanacak.
|
|
43
43
|
|
|
44
|
-
8. **Onaydan sonra kapsam kalkanını aç:** `node .claude/hooks/freeze-guard.mjs --plan-save <görev kodu> <dosya...>` ("Dokunulacak dosyalar"daki yollar; bütün bir klasör için sonuna `/`). Plan aktifken listede olmayan mevcut dosyaya yazım (dosya aracıyla da, `sed -i`, `mv`, `>` gibi kabuk komutlarıyla da) kullanıcıya sorulur; yeni dosyalar ve `docs/` serbest. Uygularken listede olmayan bir dosya gerekirse kullanıcıya sor, onaylarsa `--plan-add <yol>`.
|
|
44
|
+
8. **Onaydan sonra kapsam kalkanını aç:** `node .claude/hooks/freeze-guard.mjs --plan-save <görev kodu> <dosya...>` ("Dokunulacak dosyalar"daki yollar; bütün bir klasör için sonuna `/`). Komut kullanıcıya sorulur: listeyi görüp onaylar. Plan aktifken listede olmayan mevcut dosyaya yazım (dosya aracıyla da, `sed -i`, `mv`, `>` gibi kabuk komutlarıyla da) kullanıcıya sorulur; yeni dosyalar ve `docs/` serbest. Uygularken listede olmayan bir dosya gerekirse kullanıcıya sor, onaylarsa `--plan-add <yol>`.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Commit gate: before every AI `git commit` it requires a light board review and a green deterministic gate (gate.mjs).
|
|
3
3
|
// PreToolUse (Bash) hook: without review approval it stops the commit with exit code 2 and says what to do.
|
|
4
4
|
// node .claude/hooks/commit-gate.mjs --approve "<members who took part, or a note>" binds the approval to the current changes.
|
|
5
|
+
// node .claude/hooks/commit-gate.mjs --skip "<reason>" only when the user asks: skips the board and the gate (always asked).
|
|
5
6
|
// Exempt: no board set up, "Commit denetimi: kapalı" in docs/workflow/board.md, or only docs changed.
|
|
6
7
|
// Commits the user makes from their own terminal are not affected (this is a Claude Code hook).
|
|
7
8
|
import fs from 'node:fs';
|
|
@@ -63,7 +64,19 @@ function reviewOff() {
|
|
|
63
64
|
}
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
// A note starting with "atlandı" skips the board and the gate. --approve refuses it whatever the spelling
|
|
68
|
+
// ($'atland\xc4\xb1', $(printf ...)); only --skip writes it, and --skip is always asked.
|
|
69
|
+
const SKIPPED = /^atlandı/i;
|
|
70
|
+
|
|
66
71
|
function approve(note) {
|
|
72
|
+
if (SKIPPED.test(note)) {
|
|
73
|
+
console.error('"atlandı" notu --approve ile yazılmaz. Kullanıcı açıkça isterse: node .claude/hooks/commit-gate.mjs --skip "<neden>"');
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
save(note);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function save(note) {
|
|
67
80
|
const { signature, files } = changes();
|
|
68
81
|
fs.mkdirSync(path.dirname(approvalFile), { recursive: true });
|
|
69
82
|
fs.writeFileSync(approvalFile, JSON.stringify({ signature, note, files, date: new Date().toISOString() }));
|
|
@@ -83,25 +96,40 @@ function readApproval() {
|
|
|
83
96
|
return a && { signature: a.signature, note: a.note };
|
|
84
97
|
}
|
|
85
98
|
|
|
99
|
+
// This project's own commit-gate.mjs, resolved from the shell's folder ($CLAUDE_PROJECT_DIR, absolute, relative or ./ spelling),
|
|
100
|
+
// is masked so it is not read as the gate approval; any other *gate.mjs (sandbox/.claude/hooks/commit-gate.mjs, a renamed
|
|
101
|
+
// copy of gate.mjs) still is. A cd in the command moves the folder the text cannot follow: then nothing is masked.
|
|
102
|
+
const real = (p) => { try { return fs.realpathSync(p); } catch { return path.resolve(p); } };
|
|
103
|
+
const samePath = (a, b) => (process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b);
|
|
104
|
+
export function maskOwnHook(command, dir) {
|
|
105
|
+
if (/(^|[;&|(]\s*)(cd|pushd|popd|Set-Location|sl|chdir)\b/i.test(command)) return command;
|
|
106
|
+
const own = real(path.join(root, '.claude', 'hooks', 'commit-gate.mjs'));
|
|
107
|
+
return command.replace(/"([^"\n]*commit-gate\.mjs)"|'([^'\n]*commit-gate\.mjs)'|([^\s"';&|]*commit-gate\.mjs)\b/gi, (m, a, b, c) => {
|
|
108
|
+
const p = (a ?? b ?? c).replace(/\$\{?CLAUDE_PROJECT_DIR\}?|%CLAUDE_PROJECT_DIR%|\$env:CLAUDE_PROJECT_DIR/gi, process.env.CLAUDE_PROJECT_DIR || root);
|
|
109
|
+
return samePath(real(path.resolve(dir, p)), own) ? 'KIT-COMMIT-HOOK' : m;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
86
113
|
function hook() {
|
|
87
|
-
let command = '';
|
|
114
|
+
let command = '', dir = root;
|
|
88
115
|
try {
|
|
89
|
-
|
|
116
|
+
const input = JSON.parse(fs.readFileSync(0, 'utf8'));
|
|
117
|
+
command = input.tool_input?.command ?? '';
|
|
118
|
+
if (typeof input.cwd === 'string' && input.cwd) dir = input.cwd;
|
|
90
119
|
} catch {
|
|
91
120
|
process.exit(0);
|
|
92
121
|
}
|
|
93
|
-
//
|
|
122
|
+
// --skip bypasses the gate and the board: the user is asked in every spelling (the AI must not write it on its own)
|
|
94
123
|
const open = unquote(command);
|
|
95
|
-
if (/commit-gate\.mjs\b[^;&|\n]*--
|
|
124
|
+
if ([command, open].some((c) => /commit-gate\.mjs\b[^;&|\n]*--skip\b/i.test(c))) {
|
|
96
125
|
console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'ask',
|
|
97
|
-
permissionDecisionReason: 'Commit kapısı:
|
|
126
|
+
permissionDecisionReason: 'Commit kapısı: --skip kurul denetimini ve deterministik kapıyı aşar. Sadece kullanıcı açıkça isterse onayla.' } }));
|
|
98
127
|
process.exit(0);
|
|
99
128
|
}
|
|
100
129
|
// The gate command approval gives the commands in AGENTS.md permission to run: the user is asked (also covers spellings the permission list misses).
|
|
101
|
-
// Only
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if ([command, open].some((c) => /(?<!\.claude[\\/]?hooks[\\/]?commit-)gate\.mjs\b[^;&|\n]*--approve\b/i.test(c))) {
|
|
130
|
+
// Only this project's commit-gate.mjs --approve is left out (maskOwnHook); both the raw and unquoted text are tested.
|
|
131
|
+
const masked = maskOwnHook(command, dir);
|
|
132
|
+
if ([masked, unquote(masked)].some((c) => /gate\.mjs\b[^;&|\n]*--approve\b/i.test(c))) {
|
|
105
133
|
// The prompt reads AGENTS.md now: a chain, an env variable, a node flag or a redirect must not change it after the
|
|
106
134
|
// prompt and get another list approved. Only the exact spelling is accepted.
|
|
107
135
|
if (!/^\s*node\s+(\.\/)?\.claude[\\/]hooks[\\/]gate\.mjs\s+(['"]?)--approve\2\s*$/.test(command)) {
|
|
@@ -134,16 +162,16 @@ function hook() {
|
|
|
134
162
|
const approval = readApproval();
|
|
135
163
|
// The deterministic gate (gate.mjs) must be green for the same changes; not required if the user said "atlandı"
|
|
136
164
|
const gate = readGate();
|
|
137
|
-
const skipped =
|
|
165
|
+
const skipped = SKIPPED.test(String(approval?.note ?? ''));
|
|
138
166
|
if (approval?.signature === d.signature && (skipped || (gate?.signature === d.signature && gate.passed === true))) process.exit(0);
|
|
139
167
|
if (approval?.signature === d.signature) {
|
|
140
168
|
const failed = gate?.signature === d.signature ? gate.checks.filter((k) => k.status === 'failed').map((k) => k.name) : null;
|
|
141
169
|
process.stderr.write(failed && !failed.length && gate.empty
|
|
142
170
|
? 'Commit durduruldu: deterministik kapı boş (hiçbir kontrol çalışmadı). AGENTS.md → Komutlar\'ı doldur ve kapıyı tekrar çalıştır.\n' +
|
|
143
|
-
'Kullanıcı bu commit için açıkça isterse --
|
|
171
|
+
'Kullanıcı bu commit için açıkça isterse --skip "kullanıcı isteği" kullan.\n'
|
|
144
172
|
: failed
|
|
145
173
|
? `Commit durduruldu: deterministik kapı kırmızı (${failed.join(', ') || 'kaldı'}). Düzelt, node .claude/hooks/gate.mjs --run ile tekrar çalıştır, sonra onayla.\n` +
|
|
146
|
-
'Kullanıcı ara commit (WIP) için açıkça isterse --
|
|
174
|
+
'Kullanıcı ara commit (WIP) için açıkça isterse --skip "kullanıcı isteği" kullan.\n'
|
|
147
175
|
: 'Commit durduruldu: bu değişiklikler için deterministik kapı çalışmadı (ya da sonrasında değişiklik oldu). ' +
|
|
148
176
|
'node .claude/hooks/gate.mjs --run çalıştır; kırmızı yoksa kurul onayını yenile ve commit\'i tekrar dene.\n');
|
|
149
177
|
process.exit(2);
|
|
@@ -155,13 +183,14 @@ function hook() {
|
|
|
155
183
|
'1. .claude/board/rules.md §3\'e göre değişen dosyalara ilgili en fazla 2 kurul üyesini model "haiku" ile, sadece diff üzerinde çalıştır.\n' +
|
|
156
184
|
'2. RET yoksa: node .claude/hooks/commit-gate.mjs --approve "<katılan üyeler>" ve commit\'i tekrar dene.\n' +
|
|
157
185
|
'3. RET varsa commit atma; bulguyu ve seçenekleri kullanıcıya sor.\n' +
|
|
158
|
-
'Kullanıcı bu commit için denetimi açıkça atlamak isterse --
|
|
186
|
+
'Kullanıcı bu commit için denetimi açıkça atlamak isterse --skip "kullanıcı isteği" kullan.\n'
|
|
159
187
|
);
|
|
160
188
|
process.exit(2);
|
|
161
189
|
}
|
|
162
190
|
|
|
163
191
|
if (isMain(import.meta.url)) {
|
|
164
|
-
const i = process.argv.indexOf('--approve');
|
|
192
|
+
const i = process.argv.indexOf('--approve'), j = process.argv.indexOf('--skip');
|
|
165
193
|
if (i > -1) approve(process.argv[i + 1] || 'belirtilmedi');
|
|
194
|
+
else if (j > -1) save(`atlandı: ${process.argv[j + 1] || 'kullanıcı isteği'}`);
|
|
166
195
|
else hook();
|
|
167
196
|
}
|
package/.claude/hooks/cost.mjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// Run by /dashboard and /wrap-up. Not in the Stop hook: npx downloads the package the first time, it is slow.
|
|
5
5
|
// On a subscription (Pro/Max) this is not a real bill; it is what the work would cost at API prices.
|
|
6
6
|
import fs from 'node:fs';
|
|
7
|
+
import os from 'node:os';
|
|
7
8
|
import path from 'node:path';
|
|
8
9
|
import { execSync } from 'node:child_process';
|
|
9
10
|
import { ccusageProjectName } from './dashboard-data.mjs';
|
|
@@ -12,10 +13,14 @@ const CCUSAGE = 'ccusage@20.0.30'; // pinned version; updating it is a technical
|
|
|
12
13
|
const root = path.resolve(process.argv[2] ?? process.env.CLAUDE_PROJECT_DIR ?? process.cwd());
|
|
13
14
|
const since = new Date(Date.now() - 30 * 864e5).toISOString().slice(0, 10).replace(/-/g, '');
|
|
14
15
|
|
|
16
|
+
// Run in a fresh folder of its own: in the project npx would read a repo .npmrc or node_modules/.bin/ccusage (code from the
|
|
17
|
+
// repo), and in a shared /tmp it would walk up to another user's /tmp/node_modules. The empty package.json stops that walk.
|
|
18
|
+
const work = fs.mkdtempSync(path.join(os.tmpdir(), 'kit-cost-'));
|
|
15
19
|
try {
|
|
16
|
-
|
|
20
|
+
fs.writeFileSync(path.join(work, 'package.json'), '{}');
|
|
21
|
+
// Arguments contain only letters, digits and "-"; on Windows npx.cmd needs a shell.
|
|
17
22
|
const output = execSync(`npx -y ${CCUSAGE} claude daily --json --instances --project ${ccusageProjectName(root)} --since ${since} --offline`,
|
|
18
|
-
{ cwd:
|
|
23
|
+
{ cwd: work, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 120000 });
|
|
19
24
|
const data = JSON.parse(output);
|
|
20
25
|
fs.mkdirSync(path.join(root, '.claude', 'state'), { recursive: true });
|
|
21
26
|
fs.writeFileSync(path.join(root, '.claude', 'state', 'cost.json'), JSON.stringify({ readAt: new Date().toISOString(), data }));
|
|
@@ -23,4 +28,6 @@ try {
|
|
|
23
28
|
console.log(`Son 30 gün: ${Math.round((t.totalTokens ?? 0) / 1e6)}M token · API karşılığı $${(t.totalCost ?? 0).toFixed(2)}`);
|
|
24
29
|
} catch (h) {
|
|
25
30
|
console.error(`ccusage okunamadı (${h.code ?? h.message}). Node ve ağ erişimi gerekir; pano bu kartı boş gösterir.`);
|
|
31
|
+
} finally {
|
|
32
|
+
fs.rmSync(work, { recursive: true, force: true });
|
|
26
33
|
}
|
|
@@ -239,7 +239,8 @@ export function scopeIssue(command, dir, root) {
|
|
|
239
239
|
|
|
240
240
|
// Commands denied/asked in Claude Code via .claude/settings.json → permissions. Codex and Antigravity have no such
|
|
241
241
|
// list; this is its counterpart (applied only on those tools, Claude's behavior does not change).
|
|
242
|
-
// Only the real .claude/hooks/commit-gate.mjs --approve is kept out of the gate rule (separators optional: unquote drops backslashes
|
|
242
|
+
// Only the real .claude/hooks/commit-gate.mjs --approve is kept out of the gate rule (separators optional: unquote drops backslashes;
|
|
243
|
+
// anchored at the start of the path, so sandbox/.claude/hooks/commit-gate.mjs, a renamed gate.mjs, is not exempt).
|
|
243
244
|
const PERMISSION_EQUIVALENT = [
|
|
244
245
|
['deny', /\brm\b(?=[^;&|]*\s--?[a-z]*r)(?=[^;&|]*\s--?[a-z]*f)/i, 'rm -rf ile toplu silme'],
|
|
245
246
|
['deny', /\b(Remove-Item|rm|ri|del|erase)\b[^;&|]*\s-Recurse\b|\b(rmdir|rd|del)\s+[^;&|]*\/s\b/i, 'Klasör toplu siliniyor'],
|
|
@@ -249,7 +250,10 @@ const PERMISSION_EQUIVALENT = [
|
|
|
249
250
|
['ask', /freeze-guard\.mjs['"]?\s+['"]?--off\b/i, 'Düzenleme kilidi kaldırılıyor'],
|
|
250
251
|
['ask', /tools\.mjs['"]?\s+['"]?--commands\b/i, 'Paylaşılan .claude/settings.json → skillOverrides değişiyor (komut listesi)'],
|
|
251
252
|
['ask', /freeze-guard\.mjs['"]?\s+['"]?--plan-add\b/i, 'Onaylı planın dosya listesi genişliyor (kapsam kalkanı)'],
|
|
252
|
-
['ask', /
|
|
253
|
+
['ask', /freeze-guard\.mjs['"]?\s+['"]?--plan-save\b/i, 'Kapsam kalkanının dosya listesi yazılıyor'],
|
|
254
|
+
['ask', /freeze-guard\.mjs['"]?\s+['"]?--plan-end\b/i, 'Kapsam kalkanı kapatılıyor'],
|
|
255
|
+
['ask', /commit-gate\.mjs['"]?\s+['"]?--skip\b/i, 'Commit kapısı ve kurul denetimi atlanıyor'],
|
|
256
|
+
['ask', /(?<!(?:^|[\s'"=])(?:\.[\\/]|\$\{?CLAUDE_PROJECT_DIR\}?[\\/]?)?\.claude[\\/]?hooks[\\/]?commit-)gate\.mjs['"]?\s+['"]?--approve\b/i, 'Deterministik kapının çalıştıracağı komutlar onaylanıyor'],
|
|
253
257
|
// Shell counterpart of the PROTECTED rule on file tools: a command that names a rule or guard file and writes/deletes is asked
|
|
254
258
|
// (.git: the gate command approval and git hooks live there)
|
|
255
259
|
['ask', /(?=.*((^|[\s'"/\\])\.(claude|codex|agents|git)(?=$|[\s/\\'"])|\bAGENTS\.md|\bCLAUDE\.md))(?=.*(\b(rm|del|erase|rmdir|rd|mv|move|cp|copy|tee|truncate|checkout|restore|Set-Content|Add-Content|Out-File|Remove-Item|Move-Item|Copy-Item)\b|\bsed\s+-i|>|\bnode\s+-e|\bpython\d?\s+-c))/i, 'Kural veya koruma dosyası kabuk komutuyla değiştiriliyor'],
|
|
@@ -290,10 +294,21 @@ function obfuscationOne(command) {
|
|
|
290
294
|
? { decision: 'ask', reason: 'Komut gizlenmiş görünüyor (satır sonu \\, $\'..\', $(...), `...` veya ${...}) ve korunan bir işlem içeriyor. Kullanıcı onayı gerekli.' } : null;
|
|
291
295
|
}
|
|
292
296
|
|
|
297
|
+
// A kit guard script (.claude/hooks) run with an indirect spelling: $VAR, $(..), `..`, ${..}, $'..' or a glob * ? (commit-gate.mjs $S,
|
|
298
|
+
// --sk$()ip, commit-*.mjs). The text rules for --skip, --approve and --plan-* cannot see through them: asked (user decision 2026-10-25).
|
|
299
|
+
// Heredocs and single-quoted text are not expanded; $CLAUDE_PROJECT_DIR is the documented way to name the folder.
|
|
300
|
+
export function hookScriptIssue(command) {
|
|
301
|
+
const k = command.replace(/<<-?\s*(['"]?)(\w+)\1[^\n]*\n[\s\S]*?\n[ \t]*\2[ \t]*(?=\n|$)/g, '').replace(/'[^']*'/g, "''")
|
|
302
|
+
.replace(/"(?:[^"\\]|\\.)*"/g, (q) => q.replace(/[*?]/g, '')).replace(/\$\{?CLAUDE_PROJECT_DIR\}?|\$env:CLAUDE_PROJECT_DIR/gi, '');
|
|
303
|
+
if (!/\bnode\b/i.test(k) || !/\.claude|hooks|commit-|gate|freeze/i.test(command)) return null;
|
|
304
|
+
return /\$\(|`|\$\{|\$'|\$[A-Za-z_]|[*?]/.test(k)
|
|
305
|
+
? { decision: 'ask', reason: 'Kitin koruma betiği dolaylı yazımla çalıştırılıyor ($DEĞİŞKEN, $(...), `...`, ${...} veya * ?); kural metni bunun içini göremez. Kullanıcı onayı gerekli.' } : null;
|
|
306
|
+
}
|
|
307
|
+
|
|
293
308
|
// Separators inside quotes turned into spaces: a quoted & or | ("a & b") must not split a command so its later flags escape the rules
|
|
294
309
|
const blankQuoted = (s) => s.replace(/"(?:[^"\\]|\\.)*"|'[^']*'/g, (q) => q.replace(/[;&|\n]/g, ' '));
|
|
295
310
|
export function evaluate(command) {
|
|
296
|
-
return strictest(evaluateOne(command), evaluateOne(unquote(command)), evaluateOne(blankQuoted(command)), obfuscationOne(command));
|
|
311
|
+
return strictest(evaluateOne(command), evaluateOne(unquote(command)), evaluateOne(blankQuoted(command)), obfuscationOne(command), hookScriptIssue(command));
|
|
297
312
|
}
|
|
298
313
|
function evaluateOne(command) {
|
|
299
314
|
const k = command.replace(/\s+/g, ' ');
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
// Scope shield (F-06): after /plan-task approval the plan's file list is saved; while a plan is active a write to an
|
|
12
12
|
// EXISTING file not on the list is asked. New files are the structure guard's job; docs/, CHANGELOG.md are free. No plan: silent.
|
|
13
13
|
// State: .claude/state/plan.json → { "task": "G-012", "paths": ["src/a.ts", "src/features/fatura/"] } (ending in /: folder)
|
|
14
|
-
// node .claude/hooks/freeze-guard.mjs --plan-save <task> <path...> save when no plan is active (
|
|
14
|
+
// node .claude/hooks/freeze-guard.mjs --plan-save <task> <path...> save when no plan is active (asked: the user sees the list)
|
|
15
15
|
// node .claude/hooks/freeze-guard.mjs --plan-add <path...> widen the list (with user approval)
|
|
16
|
-
// node .claude/hooks/freeze-guard.mjs --plan-end close when the task is done
|
|
16
|
+
// node .claude/hooks/freeze-guard.mjs --plan-end close when the task is done (asked)
|
|
17
17
|
import fs from 'node:fs';
|
|
18
18
|
import path from 'node:path';
|
|
19
19
|
import { readInput, decide, findRoot, isMain } from './shared.mjs';
|
package/.claude/hooks/gate.mjs
CHANGED
|
@@ -133,7 +133,7 @@ export function runGate(root) {
|
|
|
133
133
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
134
134
|
fs.writeFileSync(file, JSON.stringify({ signature, date: new Date().toISOString(), passed, empty, checks }, null, 2));
|
|
135
135
|
const result = checks.some((k) => k.status === 'failed') ? 'Sonuç: KIRMIZI. Commit kapısı açılmaz: düzelt ve kapıyı tekrar çalıştır.'
|
|
136
|
-
: empty ? 'Sonuç: BOŞ, hiçbir kontrol çalışmadı. AGENTS.md → Komutlar\'ı doldur; commit kapısı açılmaz (kullanıcı isterse
|
|
136
|
+
: empty ? 'Sonuç: BOŞ, hiçbir kontrol çalışmadı. AGENTS.md → Komutlar\'ı doldur; commit kapısı açılmaz (kullanıcı isterse commit-gate.mjs --skip).'
|
|
137
137
|
: 'Sonuç: YEŞİL.';
|
|
138
138
|
return { passed, text: [`## Kapı · ${files.length} dosya değişti`, ...checks.map(line), result].join('\n') };
|
|
139
139
|
}
|
package/.claude/hooks/notify.mjs
CHANGED
|
@@ -6,48 +6,59 @@
|
|
|
6
6
|
import fs from 'node:fs';
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import { spawn } from 'node:child_process';
|
|
9
|
+
import { isMain } from './shared.mjs';
|
|
9
10
|
|
|
10
|
-
const root = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
11
11
|
export const THRESHOLD_SEC = 90;
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
try { const name = fs.readFileSync(path.join(root, f), 'utf8').match(/^# (.+)$/m)?.[1]; if (name && name !== '[PROJE ADI]') { project = name; break; } } catch { /* */ }
|
|
13
|
+
// Title and message go in env, never into the script text: PowerShell also treats ’ ‘ ‚ ‛ as quotes.
|
|
14
|
+
export function winCommand(title, message) {
|
|
15
|
+
const script = 'Add-Type -AssemblyName System.Windows.Forms; Add-Type -AssemblyName System.Drawing; ' +
|
|
16
|
+
'$n = New-Object System.Windows.Forms.NotifyIcon; $n.Icon = [System.Drawing.SystemIcons]::Information; $n.Visible = $true; ' +
|
|
17
|
+
'$n.ShowBalloonTip(6000, $env:KIT_T, $env:KIT_M, [System.Windows.Forms.ToolTipIcon]::Info); Start-Sleep -Seconds 7; $n.Dispose()';
|
|
18
|
+
return { args: ['-NoProfile', '-NonInteractive', '-WindowStyle', 'Hidden', '-Command', script], env: { ...process.env, KIT_T: title, KIT_M: message } };
|
|
20
19
|
}
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
try { const r = JSON.parse(fs.readFileSync(path.join(root, '.claude', 'state', 'reminder.json'), 'utf8')); startedAt = r.turnAt ?? r.tur ?? 0; } catch { /* */ } // tur: key before 3.15.0
|
|
27
|
-
const duration = (Date.now() - startedAt) / 1000;
|
|
28
|
-
if (!startedAt || duration < THRESHOLD_SEC) process.exit(0);
|
|
29
|
-
message = `İş bitti (${Math.round(duration / 60)} dk). Sonuca bakabilirsin.`;
|
|
30
|
-
} else {
|
|
31
|
-
message = input.message || 'Claude cevabını bekliyor.';
|
|
32
|
-
}
|
|
21
|
+
if (isMain(import.meta.url)) main();
|
|
22
|
+
|
|
23
|
+
function main() {
|
|
24
|
+
const root = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
33
25
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
26
|
+
let input = {};
|
|
27
|
+
try { input = JSON.parse(fs.readFileSync(0, 'utf8')); } catch { /* no input */ }
|
|
28
|
+
if (process.env.KIT_NOTIFY === 'off') return;
|
|
29
|
+
|
|
30
|
+
let project = path.basename(root);
|
|
31
|
+
for (const f of ['AGENTS.md', 'CLAUDE.md']) { // in projects before 3.9.0 the name is in CLAUDE.md
|
|
32
|
+
try { const name = fs.readFileSync(path.join(root, f), 'utf8').match(/^# (.+)$/m)?.[1]; if (name && name !== '[PROJE ADI]') { project = name; break; } } catch { /* */ }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let message;
|
|
36
|
+
if (process.argv.includes('--stop')) {
|
|
37
|
+
if (input.stop_hook_active) process.exit(0);
|
|
38
|
+
let startedAt = 0;
|
|
39
|
+
try { const r = JSON.parse(fs.readFileSync(path.join(root, '.claude', 'state', 'reminder.json'), 'utf8')); startedAt = r.turnAt ?? r.tur ?? 0; } catch { /* */ } // tur: key before 3.15.0
|
|
40
|
+
const duration = (Date.now() - startedAt) / 1000;
|
|
41
|
+
if (!startedAt || duration < THRESHOLD_SEC) process.exit(0);
|
|
42
|
+
message = `İş bitti (${Math.round(duration / 60)} dk). Sonuca bakabilirsin.`;
|
|
48
43
|
} else {
|
|
49
|
-
|
|
44
|
+
message = input.message || 'Claude cevabını bekliyor.';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const title = `Claude Code · ${project}`.slice(0, 60);
|
|
48
|
+
message = String(message).replace(/\s+/g, ' ').slice(0, 180);
|
|
49
|
+
const detachedOpts = { detached: true, stdio: 'ignore', windowsHide: true };
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
if (process.platform === 'win32') {
|
|
53
|
+
const { args, env } = winCommand(title, message);
|
|
54
|
+
spawn('powershell', args, { ...detachedOpts, env }).unref();
|
|
55
|
+
} else if (process.platform === 'darwin') {
|
|
56
|
+
const as = (s) => s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
57
|
+
spawn('osascript', ['-e', `display notification "${as(message)}" with title "${as(title)}"`], detachedOpts).unref();
|
|
58
|
+
} else {
|
|
59
|
+
spawn('notify-send', ['--', title, message], detachedOpts).unref();
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
// no notification tool: skip silently
|
|
50
63
|
}
|
|
51
|
-
} catch {
|
|
52
|
-
// no notification tool: skip silently
|
|
53
64
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Package gate (PreToolUse, Bash): checks a package the AI wants to install before it
|
|
2
|
+
// Package gate (PreToolUse, Bash): checks a package the AI wants to install, or download and run (npx, uvx …), before it reaches the machine.
|
|
3
3
|
// Why: AIs invent package names that do not exist; attackers register those names (slopsquatting).
|
|
4
4
|
// Package missing -> denied (deny)
|
|
5
5
|
// Very new / few downloads / very close to a popular package / unverified -> the user is asked (ask)
|
|
6
6
|
// The decision goes through Claude Code's permission UI; the AI cannot skip it.
|
|
7
7
|
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
8
10
|
import { readInput, decide, isMain } from './shared.mjs';
|
|
9
11
|
|
|
10
12
|
export const THRESHOLD = { newDays: 30, npmWeeklyDownloads: 500 };
|
|
@@ -26,6 +28,60 @@ const COMMANDS = [
|
|
|
26
28
|
[/^uv\s+(add|pip\s+install)\b/, 'pypi'], [/^(python3?\s+-m\s+)?pip3?\s+install\b/, 'pypi'], [/^poetry\s+add\b/, 'pypi'],
|
|
27
29
|
];
|
|
28
30
|
|
|
31
|
+
// Download-and-run: the first positional is the package (the rest are its arguments), or the values of the package flags.
|
|
32
|
+
// local: the runner uses a bin the project already has before downloading (npx, npm exec, bunx); pnpm/yarn dlx always download.
|
|
33
|
+
// positional: false for uv run (its positional is a script; only --with names packages).
|
|
34
|
+
const EXEC = [
|
|
35
|
+
{ re: /^(npx|npm\s+(exec|x)|bunx|bun\s+x)\b/, eco: 'npm', local: true, positional: true },
|
|
36
|
+
{ re: /^(pnpx|pnpm\s+dlx|yarn\s+dlx)\b/, eco: 'npm', local: false, positional: true },
|
|
37
|
+
{ re: /^(uvx|uv\s+tool\s+run|pipx\s+run)\b/, eco: 'pypi', local: false, positional: true },
|
|
38
|
+
{ re: /^uv\s+run\b/, eco: 'pypi', local: false, positional: false },
|
|
39
|
+
];
|
|
40
|
+
const FLAGS = {
|
|
41
|
+
npm: { pkg: /^(-p|--package)$/, value: /^(-p|--package|-c|--call|--registry|--prefix|--cache|--userconfig|--globalconfig|-w|--workspace|--node-options|--script-shell|--shell|--loglevel|-C|--dir|--filter)$/ },
|
|
42
|
+
pypi: { pkg: /^(--from|--spec)$/, value: /^(--from|--spec|--with|--with-requirements|--with-editable|-p|--python|-i|--index|--index-url|--extra-index-url|--default-index|--pip-args|-c|--constraints|--overrides)$/ },
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Returns [{ arg, fromFlag, unsure }]. After an unknown flag its value may be the next word: that word and the one after are
|
|
46
|
+
// both checked, unsure (a missing one is asked, not denied: it may be a file name).
|
|
47
|
+
function execArgs(args, eco, positional) {
|
|
48
|
+
const named = [], extra = [], out = [];
|
|
49
|
+
let unknown = false;
|
|
50
|
+
for (let i = 0; i < args.length; i++) {
|
|
51
|
+
const [flag, inline] = args[i].split(/=(.*)/s);
|
|
52
|
+
if (args[i] === '--') { if (positional && args[i + 1]) out.push({ arg: args[i + 1] }); break; }
|
|
53
|
+
if (FLAGS[eco].value.test(flag)) {
|
|
54
|
+
const v = inline ?? args[++i] ?? '';
|
|
55
|
+
if (FLAGS[eco].pkg.test(flag)) named.push({ arg: v, fromFlag: true });
|
|
56
|
+
else if (flag === '--with') extra.push(...v.split(',').map((arg) => ({ arg, fromFlag: true })));
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (args[i].startsWith('-')) { unknown ||= inline === undefined && /^--?\w/.test(flag) && !/^(-y|--yes|--no|-q|--quiet|-s|--silent|--no-install|--ignore-existing|--offline|-v|--verbose)$/.test(flag); continue; }
|
|
60
|
+
if (!positional || named.length) break;
|
|
61
|
+
out.push({ arg: args[i], unsure: unknown });
|
|
62
|
+
if (unknown && args[i + 1] && !args[i + 1].startsWith('-')) out.push({ arg: args[i + 1], unsure: true });
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
return [...(named.length ? named : out), ...extra];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// What runs a segment: wrappers, VAR=x, a path or .cmd/.exe on the tool, and the manager's own flags before its subcommand are
|
|
69
|
+
// dropped (FOO=1 npx x, cmd /c npx x, bash -c "npx x", /usr/bin/npx x, npx.cmd x, pnpm -C . dlx x, npm --silent exec x).
|
|
70
|
+
const WRAPPER = /^(?:\(|[A-Za-z_]\w*=\S*\s+|(?:sudo|env|time|nohup|exec|command|xargs|corepack|call|start)\s+|(?:bash|sh|zsh|dash|cmd|pwsh|powershell)(?:\.exe)?\s+(?:-c|\/c|\/k|-Command)\s+["']?)/i;
|
|
71
|
+
const SUBCOMMAND = /^(install|i|add|exec|x|dlx|tool|pip|run|global)$/;
|
|
72
|
+
export function normalize(part) {
|
|
73
|
+
let s = part.trim(), prev;
|
|
74
|
+
do { prev = s; s = s.replace(WRAPPER, '').trim(); } while (s !== prev);
|
|
75
|
+
s = s.replace(/^["']?(?:[^\s"']*[\/])?([\w.-]+?)(?:\.cmd|\.exe|\.ps1)?["']?(?=\s|$)/i, '$1');
|
|
76
|
+
const t = s.split(/\s+/);
|
|
77
|
+
if (/^(npm|pnpm|yarn|bun|uv)$/.test(t[0])) {
|
|
78
|
+
let i = 1;
|
|
79
|
+
while (i < t.length && t[i].startsWith('-')) i += !t[i].includes('=') && t[i + 1] && !t[i + 1].startsWith('-') && !SUBCOMMAND.test(t[i + 1]) ? 2 : 1;
|
|
80
|
+
s = [t[0], ...t.slice(i)].join(' ');
|
|
81
|
+
}
|
|
82
|
+
return s;
|
|
83
|
+
}
|
|
84
|
+
|
|
29
85
|
function npmName(arg) {
|
|
30
86
|
if (/^(\.|\/|file:|link:|git\+|git:|https?:|github:|workspace:)/.test(arg)) return null;
|
|
31
87
|
if (arg.startsWith('@')) { const i = arg.indexOf('@', 1); return i > 0 ? arg.slice(0, i) : arg; }
|
|
@@ -40,12 +96,20 @@ function pypiName(arg) {
|
|
|
40
96
|
export function findPackages(command) {
|
|
41
97
|
const result = [];
|
|
42
98
|
for (const part of command.split(/&&|\|\||;|\|/)) {
|
|
43
|
-
const s = part
|
|
99
|
+
const s = normalize(part);
|
|
44
100
|
const match = COMMANDS.find(([re]) => re.test(s));
|
|
45
|
-
|
|
46
|
-
|
|
101
|
+
const exec = match ? null : EXEC.find((e) => e.re.test(s));
|
|
102
|
+
if (!match && !exec) continue;
|
|
103
|
+
const re = match?.[0] ?? exec.re, eco = match?.[1] ?? exec.eco;
|
|
47
104
|
// Redirects (2>&1, > log, 2>/dev/null, &> x) are not arguments; a quoted "pkg>=1" has no space before ">" and stays
|
|
48
|
-
const args = s.replace(re, '').replace(/\s+(\d*|&)(>>?|<)(&\d+|\s*[^\s'"]+)/g, '').trim().split(/\s+/).map((x) => x.replace(/^['"]
|
|
105
|
+
const args = s.replace(re, '').replace(/\s+(\d*|&)(>>?|<)(&\d+|\s*[^\s'"]+)/g, '').trim().split(/\s+/).map((x) => x.replace(/^['"(]+|['")]+$/g, '')).filter(Boolean);
|
|
106
|
+
if (exec) {
|
|
107
|
+
for (const { arg, fromFlag, unsure } of execArgs(args, eco, exec.positional)) {
|
|
108
|
+
const name = eco === 'npm' ? npmName(arg) : pypiName(arg);
|
|
109
|
+
if (name) result.push({ eco, name, exec: true, ...(exec.local && !fromFlag && { local: true }), ...(unsure && { unsure: true }) });
|
|
110
|
+
}
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
49
113
|
for (let i = 0; i < args.length; i++) {
|
|
50
114
|
const a = args[i];
|
|
51
115
|
if (a.startsWith('-')) {
|
|
@@ -114,10 +178,13 @@ export async function check({ eco, name }, get = fetch, now = Date.now()) {
|
|
|
114
178
|
async function hook() {
|
|
115
179
|
const g = readInput();
|
|
116
180
|
if (!g) process.exit(0);
|
|
117
|
-
|
|
181
|
+
// npx/bunx run a bin the project already installed without downloading: a local bin (tsc, eas) is not a package name
|
|
182
|
+
const bin = (name) => /^\w[\w.-]*$/.test(name) && [g.dir, g.root].some((d) => fs.existsSync(path.join(d, 'node_modules', '.bin', name)));
|
|
183
|
+
// The kit's own package (/start and /adopt run npx -y tillerhand@<version> install): the kit already trusts itself
|
|
184
|
+
const packages = findPackages(g.command).filter((p) => !(p.local && bin(p.name)) && !(p.eco === 'npm' && p.name === 'tillerhand'));
|
|
118
185
|
if (!packages.length) process.exit(0);
|
|
119
186
|
const results = await Promise.all(packages.map(async (p) => ({ ...p, ...(await check(p)) })));
|
|
120
|
-
const missing = results.filter((s) => s.status === 'missing');
|
|
187
|
+
const missing = results.filter((s) => s.status === 'missing' && !s.unsure);
|
|
121
188
|
if (missing.length) {
|
|
122
189
|
decide(g.tool, 'deny', `Paket kapısı: ${missing.map((s) => `${s.name} (${s.detail})`).join(', ')}. Bu ad uydurulmuş olabilir. Doğru paket adını resmi dokümandan bul; kullanıcıya bildir.`);
|
|
123
190
|
}
|
package/.claude/settings.json
CHANGED
|
@@ -11,8 +11,6 @@
|
|
|
11
11
|
"Bash(node .claude/hooks/cost.mjs)",
|
|
12
12
|
"Bash(node .claude/hooks/freeze-guard.mjs --set:*)",
|
|
13
13
|
"Bash(node .claude/hooks/freeze-guard.mjs --status)",
|
|
14
|
-
"Bash(node .claude/hooks/freeze-guard.mjs --plan-save:*)",
|
|
15
|
-
"Bash(node .claude/hooks/freeze-guard.mjs --plan-end)",
|
|
16
14
|
"Bash(node .claude/hooks/gate.mjs --run)",
|
|
17
15
|
"Bash(node .claude/hooks/preview.mjs:*)",
|
|
18
16
|
"Bash(node .claude/hooks/checkpoint.mjs --list:*)",
|
|
@@ -31,6 +29,9 @@
|
|
|
31
29
|
"Bash(node .claude/hooks/tools.mjs --enable:*)",
|
|
32
30
|
"Bash(node .claude/hooks/tools.mjs --commands:*)",
|
|
33
31
|
"Bash(node .claude/hooks/freeze-guard.mjs --plan-add:*)",
|
|
32
|
+
"Bash(node .claude/hooks/freeze-guard.mjs --plan-save:*)",
|
|
33
|
+
"Bash(node .claude/hooks/freeze-guard.mjs --plan-end)",
|
|
34
|
+
"Bash(node .claude/hooks/commit-gate.mjs --skip:*)",
|
|
34
35
|
"Bash(node .claude/hooks/gate.mjs --approve)",
|
|
35
36
|
"Edit(./CLAUDE.md)",
|
|
36
37
|
"Edit(./AGENTS.md)",
|
package/README.en.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
</p>
|
|
7
7
|
|
|
8
8
|
<p align="center">
|
|
9
|
-
<a href="kit/CHANGELOG.md"><img alt="Version" src="https://img.shields.io/badge/version-3.
|
|
9
|
+
<a href="kit/CHANGELOG.md"><img alt="Version" src="https://img.shields.io/badge/version-3.28.0-2a78d6?style=flat-square"></a>
|
|
10
10
|
<a href="LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-2a78d6?style=flat-square"></a>
|
|
11
11
|
<img alt="Claude Code" src="https://img.shields.io/badge/Claude%20Code-ready-d97757?style=flat-square">
|
|
12
12
|
<img alt="Also Codex and Antigravity" src="https://img.shields.io/badge/also-Codex%20%C2%B7%20Antigravity-5b6577?style=flat-square">
|
|
@@ -198,7 +198,7 @@ A new file outside the map is asked about, clutter like `*.bak` and `*-old.*` is
|
|
|
198
198
|
|---|---|
|
|
199
199
|
| Secret guard | Blocks API keys written into code or docs |
|
|
200
200
|
| Destructive-command shield | Blocks `DROP`, unconditional `DELETE`, `db reset`; asks before prod deploys and rollbacks |
|
|
201
|
-
| Package gate | Blocks packages that don't exist; asks about very new ones or lookalikes of popular ones |
|
|
201
|
+
| Package gate | Blocks packages that don't exist (on install and on `npx`, `uvx` runs); asks about very new ones or lookalikes of popular ones |
|
|
202
202
|
| Structure guard | Asks about files outside the map, blocks clutter files |
|
|
203
203
|
| Test guard | Asks before a test is deleted, skipped or weakened |
|
|
204
204
|
| Deterministic gate | No commit until lint, type check, tests and Semgrep have run |
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
</p>
|
|
7
7
|
|
|
8
8
|
<p align="center">
|
|
9
|
-
<a href="kit/CHANGELOG.md"><img alt="Sürüm" src="https://img.shields.io/badge/s%C3%BCr%C3%BCm-3.
|
|
9
|
+
<a href="kit/CHANGELOG.md"><img alt="Sürüm" src="https://img.shields.io/badge/s%C3%BCr%C3%BCm-3.28.0-2a78d6?style=flat-square"></a>
|
|
10
10
|
<a href="LICENSE"><img alt="Lisans: MIT" src="https://img.shields.io/badge/lisans-MIT-2a78d6?style=flat-square"></a>
|
|
11
11
|
<img alt="Claude Code" src="https://img.shields.io/badge/Claude%20Code-haz%C4%B1r-d97757?style=flat-square">
|
|
12
12
|
<img alt="Codex ve Antigravity" src="https://img.shields.io/badge/ayr%C4%B1ca-Codex%20%C2%B7%20Antigravity-5b6577?style=flat-square">
|
|
@@ -196,7 +196,7 @@ Harita dışı yeni dosya sorulur, `*.bak` ve `*-old.*` gibi dağınıklık dosy
|
|
|
196
196
|
|---|---|
|
|
197
197
|
| Sır engeli | Koda veya dokümana yazılan API anahtarını engeller |
|
|
198
198
|
| Yıkıcı komut kalkanı | `DROP`, koşulsuz `DELETE`, `db reset` engellenir; prod deploy ve geri alma sorulur |
|
|
199
|
-
| Paket kapısı | Var olmayan paketi engeller; çok yeni veya popüler bir pakete benzeyeni sorar |
|
|
199
|
+
| Paket kapısı | Var olmayan paketi engeller (kurulumda ve `npx`, `uvx` ile çalıştırmada); çok yeni veya popüler bir pakete benzeyeni sorar |
|
|
200
200
|
| Yapı koruyucu | Harita dışı dosyayı sorar, dağınıklık dosyalarını engeller |
|
|
201
201
|
| Test koruması | Silinen, atlanan veya gevşetilen testi sorar |
|
|
202
202
|
| Deterministik kapı | Lint, tip, test ve Semgrep çalışmadan commit açılmaz |
|
package/kit/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,23 @@
|
|
|
3
3
|
Kit sürümü `kit/VERSION` dosyasında. Bir projeyi güncellemek: projede `/kit-update`.
|
|
4
4
|
"Elle uyarla" notu olan maddeler projeye ait dosyalardadır; güncelleme bunlara dokunmaz.
|
|
5
5
|
|
|
6
|
+
## [3.28.0] - 2026-10-25
|
|
7
|
+
|
|
8
|
+
### Değişen
|
|
9
|
+
- **Commit denetimini atlamak artık ayrı bir komut: `commit-gate.mjs --skip "<neden>"`.** Bu komut her seferinde kullanıcıya sorulur. `--approve` "atlandı" ile başlayan notu artık kabul etmiyor. Önceden hook komut metninde "atlandı" kelimesini arıyordu; kelime `$'atland\xc4\xb1'` gibi kaçışlı yazılınca soru çıkmadan kapı ve kurul atlanabiliyordu.
|
|
10
|
+
- **Kapsam kalkanını açmak ve kapatmak (`freeze-guard.mjs --plan-save`, `--plan-end`) artık sorulur.** İkisi de serbestti. AI planı kapatıp listede olmayan dosyalara yazabiliyor ya da planı daha geniş bir listeyle yeniden kaydedebiliyordu. `/kit-update` eski izin satırlarını kaldırıp yenilerini ekler.
|
|
11
|
+
- **Koruma betiği dolaylı yazımla çalıştırılırsa sorulur.** `.claude/hooks` altındaki bir betik `$DEĞİŞKEN`, `$(...)`, ters tırnak, `${...}` ya da `*` `?` ile çağrılırsa (`commit-gate.mjs $S`, `commit-*.mjs`) kurallar komutun içini göremez; bu yüzden kullanıcı onayı istenir. `$CLAUDE_PROJECT_DIR` ve heredoc içindeki commit mesajları bu kurala takılmaz.
|
|
12
|
+
- **Paket kapısı indirip çalıştıran komutlara da bakıyor:** `npx`, `bunx`, `bun x`, `npm exec`, `pnpm dlx`, `yarn dlx`, `uvx`, `uv tool run`, `uv run --with`, `pipx run`. Paket adı uydurmaysa komut engellenir. Çok yeni, az indirilmiş ya da popüler bir pakete benzeyen paket kullanıcıya sorulur. Projede zaten kurulu bir komut (`node_modules/.bin`) `npx`, `npm exec` ve `bunx` ile çalıştırılırken denetlenmez; `dlx` her zaman indirdiği için denetlenir. Kitin kendi paketi (`tillerhand`) denetlenmez.
|
|
13
|
+
- **Paket kapısı önekleri ve sarmalayıcıları da görüyor** (kurulum komutlarında da): `FOO=1 npx`, `cmd /c`, `bash -c`, `time`, `/usr/bin/npx`, `npx.cmd`, `pnpm -C . dlx`, `npm --prefix web install`. Bilinmeyen bir bayraktan sonraki ad kesin paket sayılmaz; kayıt defterinde yoksa engellenmez, sorulur.
|
|
14
|
+
|
|
15
|
+
### Düzeltilen
|
|
16
|
+
- **Windows'ta masaüstü bildirimi proje adından komut çalıştırabiliyordu.** Proje adı PowerShell betiğinin metnine gömülüyordu ve yalnız düz `'` kaçırılıyordu; PowerShell `’ ‘ ‚ ‛` işaretlerini de tırnak sayar. Başlık ve mesaj artık betiğe ortam değişkeniyle geçiyor.
|
|
17
|
+
- **Biçimlendirme hook'u (web, mobil) Windows'ta boşluklu klasörde çalışmıyordu** ve dosya yolunu kabuğa kaçışsız veriyordu. Prettier artık kabuk olmadan, doğrudan Node ile çalışıyor (Prettier 3).
|
|
18
|
+
- **Maliyet okuma (`cost.mjs`) `npx`'i proje klasöründe çalıştırıyordu.** Bu yüzden depodaki bir `.npmrc` ya da `node_modules/.bin/ccusage` devreye girebiliyordu. Artık her çalışmada açılıp silinen kendi geçici klasöründe çalışıyor.
|
|
19
|
+
- **Commit kapısındaki muafiyet sadece projenin kendi `commit-gate.mjs` dosyası için geçerli.** Yol çözülerek karşılaştırılır: göreli, mutlak ya da `$CLAUDE_PROJECT_DIR` ile yazılan yol muaf kalır. Başka bir klasörde (`sandbox/.claude/hooks/commit-gate.mjs`) bu adla duran bir `gate.mjs` kopyası ise kapı komut onayı sayılır.
|
|
20
|
+
- Linux bildiriminde `-` ile başlayan mesaj seçenek sanılmaz.
|
|
21
|
+
- Elle uyarla: yok; `/kit-update` hook'ları, kuralları ve izin satırlarını yeniler.
|
|
22
|
+
|
|
6
23
|
## [3.27.2] - 2026-10-25
|
|
7
24
|
|
|
8
25
|
### Belgeler
|
package/kit/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.28.0
|
package/kit/guide.html
CHANGED
|
@@ -287,7 +287,7 @@ footer .container { display: flex; flex-wrap: wrap; gap: 16px; justify-content:
|
|
|
287
287
|
<section class="hero">
|
|
288
288
|
<div class="container">
|
|
289
289
|
<div>
|
|
290
|
-
<span class="pill"><b>v3.
|
|
290
|
+
<span class="pill"><b>v3.28.0</b> Claude Code için başlangıç kiti · MIT</span>
|
|
291
291
|
<h1>Kodu AI yazar.<br><em>Dümen sende.</em></h1>
|
|
292
292
|
<p class="sub">TillerHand, Claude Code ile bir ürünü fikirden canlıya ve bakıma kadar götürür. Her önemli kararda AI sana seçenekleri, önerisini ve her seçeneğin bedelini sunar. Bir uzman kurulu işi denetler, korumalar hatayı daha olmadan durdurur.</p>
|
|
293
293
|
<div class="install"><span id="cmd">npx tillerhand init</span><button class="copy" type="button" data-copy="cmd">Kopyala</button></div>
|
|
@@ -760,14 +760,14 @@ node vibe-kit/kit/kit.mjs init
|
|
|
760
760
|
<tbody>
|
|
761
761
|
<tr><td>Sır engeli</td><td>Koda veya dokümana yazılan API anahtarını (Anthropic, OpenAI, Stripe, AWS, GitHub, Supabase…) engeller; commit öncesi de tarar</td></tr>
|
|
762
762
|
<tr><td>Yıkıcı komut kalkanı</td><td><code>DROP</code>, <code>TRUNCATE</code>, koşulsuz <code>DELETE</code>, <code>db reset</code> ve prod ortam değişkenlerini makineye çekmek engellenir; prod deploy, geri alma, mağazaya gönderim ve migration sorulur</td></tr>
|
|
763
|
-
<tr><td>Paket kapısı</td><td>
|
|
763
|
+
<tr><td>Paket kapısı</td><td>Kurulan ya da <code>npx</code>, <code>uvx</code> ile indirilip çalıştırılan paket kayıt defterinden doğrulanır: yoksa engellenir; çok yeni, az indirilmiş veya popüler bir pakete benziyorsa sorulur</td></tr>
|
|
764
764
|
<tr><td>Yapı koruyucu</td><td>Harita dışı yeni dosya sorulur; <code>*.bak</code>, <code>*-old.*</code>, kopya ve <code>temp/</code> gibi dağınıklıklar engellenir</td></tr>
|
|
765
765
|
<tr><td>Yapılandırma onayı</td><td><code>CLAUDE.md</code>, <code>AGENTS.md</code>, <code>.claude/</code>, <code>.codex/</code>, <code>.mcp.json</code> ve iş akışı dosyaları onayın olmadan değişmez</td></tr>
|
|
766
766
|
<tr><td>Test koruması</td><td>Beklenti silmek veya yoruma almak, <code>.skip</code>/<code>.only</code>, gevşek beklenti, test dosyasını silmek veya taşımak ve test ayarında kapsamı daraltmak sorulur; kabukla test dosyasına yazmak da</td></tr>
|
|
767
767
|
<tr><td>Deterministik kapı</td><td><code>gate.mjs --run</code>: lint, tip kontrolü, test ve Semgrep çalışır; çıktı kurula girdi olur, kırmızı kontrol RET sayılır. Spec'teki "değişmemesi gereken" davranışların testi yoksa veya kırmızıysa kapı kırmızıdır. Yeni veya değişen komut önce sana gösterilip onaylatılır</td></tr>
|
|
768
|
-
<tr><td>Commit kapısı</td><td>Kurul denetimi ve aynı değişiklikler için yeşil kapı olmadan commit atılmaz</td></tr>
|
|
768
|
+
<tr><td>Commit kapısı</td><td>Kurul denetimi ve aynı değişiklikler için yeşil kapı olmadan commit atılmaz; denetimi atlamak (<code>--skip</code>) her seferinde sorulur</td></tr>
|
|
769
769
|
<tr><td>Düzenleme kilidi</td><td><code>/freeze <klasör></code> açıkken AI sadece o klasöre yazar; kabukla dışarı yazmak sorulur, sembolik bağdan kaçılamaz</td></tr>
|
|
770
|
-
<tr><td>Kapsam kalkanı</td><td><code>/plan-task</code> onayından sonra planın dosya listesi kaydedilir; listede olmayan mevcut dosyaya yazım sorulur</td></tr>
|
|
770
|
+
<tr><td>Kapsam kalkanı</td><td><code>/plan-task</code> onayından sonra planın dosya listesi kaydedilir; listede olmayan mevcut dosyaya yazım sorulur. Kalkanı açmak ve kapatmak da sorulur</td></tr>
|
|
771
771
|
<tr><td>Düzenleme turu sayacı</td><td>Aynı kod dosyası bir oturumda 5., 10. kez düzenlenince Semgrep ve güvenlik incelemesi hatırlatılır</td></tr>
|
|
772
772
|
<tr><td>Dev/prod ayrımı</td><td>Yerel ortam ve AI (salt-okunur MCP dahil) sadece dev projesine bağlanır; prod sırları barındırma panelinde kalır</td></tr>
|
|
773
773
|
<tr><td>Güvenlik incelemesi</td><td><code>security-guidance</code>: yazarken riskli kalıp uyarısı (istemcide service_role, kişisel veri logu, innerHTML), commit'te ayrı modelle inceleme</td></tr>
|
package/kit/kit.mjs
CHANGED
|
@@ -117,7 +117,9 @@ const OLD_RULES = {
|
|
|
117
117
|
allow: ['Bash(node .claude/hooks/freeze-guard.mjs --plan-kaydet:*)', 'Bash(node .claude/hooks/freeze-guard.mjs --plan-bitir)',
|
|
118
118
|
'Bash(node .claude/hooks/tools.mjs --gerekli:*)', 'Bash(node .claude/hooks/tools.mjs --olc)', 'Bash(node .claude/hooks/tools.mjs --kapat:*)',
|
|
119
119
|
// 3.19.1: in the project folder the CLI loads a repo's .playwright/cli.config.json (it can run a program); asked, not allowed
|
|
120
|
-
...['open', 'snapshot', 'console', 'requests', 'screenshot'].map((c) => `Bash(npx -y @playwright/cli@0.1.22 ${c}:*)`)
|
|
120
|
+
...['open', 'snapshot', 'console', 'requests', 'screenshot'].map((c) => `Bash(npx -y @playwright/cli@0.1.22 ${c}:*)`),
|
|
121
|
+
// 3.28.0: --plan-end + --plan-save widened or dropped the scope shield unasked; both are asked now
|
|
122
|
+
'Bash(node .claude/hooks/freeze-guard.mjs --plan-save:*)', 'Bash(node .claude/hooks/freeze-guard.mjs --plan-end)'],
|
|
121
123
|
ask: ['Bash(node .claude/hooks/freeze-guard.mjs --plan-ekle:*)', 'Bash(node .claude/hooks/gate.mjs --onayla)', 'Bash(node .claude/hooks/tools.mjs --ac:*)'],
|
|
122
124
|
};
|
|
123
125
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// If Prettier is not installed in the project it passes silently; it never blocks the work.
|
|
4
4
|
import { readFileSync, existsSync } from 'node:fs';
|
|
5
5
|
import { spawnSync } from 'node:child_process';
|
|
6
|
-
import { join, extname } from 'node:path';
|
|
6
|
+
import { join, extname, resolve } from 'node:path';
|
|
7
7
|
|
|
8
8
|
const EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.json', '.css', '.md']);
|
|
9
9
|
|
|
@@ -11,9 +11,11 @@ try {
|
|
|
11
11
|
const input = JSON.parse(readFileSync(0, 'utf8'));
|
|
12
12
|
const file = input.tool_input?.file_path;
|
|
13
13
|
const root = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
14
|
-
|
|
14
|
+
// Prettier 3's own entry run with node: no shell, so a spaced or special-character path is passed as is
|
|
15
|
+
const bin = join(root, 'node_modules', 'prettier', 'bin', 'prettier.cjs');
|
|
15
16
|
if (file && EXTENSIONS.has(extname(file)) && existsSync(bin)) {
|
|
16
|
-
|
|
17
|
+
// shortcut: Prettier 3 only (v2 has bin-prettier.js and --loglevel); the kit's stack installs the latest. Absolute path: never read as a flag
|
|
18
|
+
spawnSync(process.execPath, [bin, '--write', '--log-level', 'warn', resolve(root, file)], { cwd: root, stdio: 'ignore' });
|
|
17
19
|
}
|
|
18
20
|
} catch {
|
|
19
21
|
// a formatting error must not break the edit
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// If Prettier is not installed in the project it passes silently; it never blocks the work.
|
|
4
4
|
import { readFileSync, existsSync } from 'node:fs';
|
|
5
5
|
import { spawnSync } from 'node:child_process';
|
|
6
|
-
import { join, extname } from 'node:path';
|
|
6
|
+
import { join, extname, resolve } from 'node:path';
|
|
7
7
|
|
|
8
8
|
const EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.json', '.css', '.md']);
|
|
9
9
|
|
|
@@ -11,9 +11,11 @@ try {
|
|
|
11
11
|
const input = JSON.parse(readFileSync(0, 'utf8'));
|
|
12
12
|
const file = input.tool_input?.file_path;
|
|
13
13
|
const root = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
14
|
-
|
|
14
|
+
// Prettier 3's own entry run with node: no shell, so a spaced or special-character path is passed as is
|
|
15
|
+
const bin = join(root, 'node_modules', 'prettier', 'bin', 'prettier.cjs');
|
|
15
16
|
if (file && EXTENSIONS.has(extname(file)) && existsSync(bin)) {
|
|
16
|
-
|
|
17
|
+
// shortcut: Prettier 3 only (v2 has bin-prettier.js and --loglevel); the kit's stack installs the latest. Absolute path: never read as a flag
|
|
18
|
+
spawnSync(process.execPath, [bin, '--write', '--log-level', 'warn', resolve(root, file)], { cwd: root, stdio: 'ignore' });
|
|
17
19
|
}
|
|
18
20
|
} catch {
|
|
19
21
|
// a formatting error must not break the edit
|
package/package.json
CHANGED