tillerhand 3.43.0 → 3.47.1

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 (185) hide show
  1. package/.claude/agents/board-chair.md +20 -20
  2. package/.claude/agents/code-reviewer.md +20 -20
  3. package/.claude/agents/council-advisor.md +14 -14
  4. package/.claude/agents/security-auditor.md +30 -30
  5. package/.claude/agents/test-writer.md +22 -22
  6. package/.claude/board/board-ai.md +25 -25
  7. package/.claude/board/board-api.md +25 -25
  8. package/.claude/board/board-architect.md +28 -28
  9. package/.claude/board/board-devops.md +25 -25
  10. package/.claude/board/board-mobile.md +25 -25
  11. package/.claude/board/board-performance.md +24 -24
  12. package/.claude/board/board-privacy.md +25 -25
  13. package/.claude/board/board-product.md +26 -26
  14. package/.claude/board/board-qa.md +28 -28
  15. package/.claude/board/board-ux.md +26 -26
  16. package/.claude/board/council.md +76 -76
  17. package/.claude/board/rules.md +61 -61
  18. package/.claude/claude-security-guidance.md +10 -10
  19. package/.claude/commands/adopt.md +70 -69
  20. package/.claude/commands/ask-mode.md +9 -9
  21. package/.claude/commands/board.md +15 -15
  22. package/.claude/commands/build.md +47 -47
  23. package/.claude/commands/dashboard.md +17 -17
  24. package/.claude/commands/design.md +31 -31
  25. package/.claude/commands/findings-all.md +14 -14
  26. package/.claude/commands/findings-business.md +7 -7
  27. package/.claude/commands/findings-legal.md +7 -7
  28. package/.claude/commands/findings-perf.md +7 -7
  29. package/.claude/commands/findings-security.md +9 -9
  30. package/.claude/commands/findings-tech.md +7 -7
  31. package/.claude/commands/findings-ux.md +7 -7
  32. package/.claude/commands/fix-bug.md +15 -15
  33. package/.claude/commands/freeze.md +8 -8
  34. package/.claude/commands/handoff.md +38 -38
  35. package/.claude/commands/kit-update.md +17 -14
  36. package/.claude/commands/language.md +13 -0
  37. package/.claude/commands/maintain.md +19 -19
  38. package/.claude/commands/plan-task.md +32 -32
  39. package/.claude/commands/qa.md +17 -17
  40. package/.claude/commands/release.md +40 -40
  41. package/.claude/commands/research.md +31 -31
  42. package/.claude/commands/retro.md +32 -32
  43. package/.claude/commands/review-changes.md +15 -15
  44. package/.claude/commands/rollback.md +24 -24
  45. package/.claude/commands/start.md +116 -113
  46. package/.claude/commands/tools.md +11 -11
  47. package/.claude/commands/user-feedback.md +11 -11
  48. package/.claude/commands/wrap-up.md +18 -18
  49. package/.claude/findings/lenses/business.md +24 -24
  50. package/.claude/findings/lenses/legal.md +29 -29
  51. package/.claude/findings/lenses/perf.md +19 -19
  52. package/.claude/findings/lenses/security.md +30 -30
  53. package/.claude/findings/lenses/tech.md +23 -23
  54. package/.claude/findings/lenses/ux.md +19 -19
  55. package/.claude/findings/protocol.md +54 -54
  56. package/.claude/findings/report-template.md +30 -30
  57. package/.claude/hooks/budget.mjs +7 -7
  58. package/.claude/hooks/checkpoint.mjs +12 -12
  59. package/.claude/hooks/commit-gate.mjs +38 -37
  60. package/.claude/hooks/cost.mjs +2 -2
  61. package/.claude/hooks/council.mjs +3 -3
  62. package/.claude/hooks/dashboard-data.mjs +4 -3
  63. package/.claude/hooks/dashboard.mjs +164 -149
  64. package/.claude/hooks/destructive-guard.mjs +74 -64
  65. package/.claude/hooks/edit-rounds.mjs +3 -3
  66. package/.claude/hooks/flow.mjs +10 -10
  67. package/.claude/hooks/freeze-guard.mjs +27 -23
  68. package/.claude/hooks/gate.mjs +25 -25
  69. package/.claude/hooks/guard-secrets.mjs +11 -11
  70. package/.claude/hooks/lang.mjs +114 -0
  71. package/.claude/hooks/messages.mjs +495 -0
  72. package/.claude/hooks/models.mjs +14 -11
  73. package/.claude/hooks/notify.mjs +3 -2
  74. package/.claude/hooks/package-gate.mjs +17 -15
  75. package/.claude/hooks/plugins-sync.mjs +9 -9
  76. package/.claude/hooks/prefs.mjs +19 -19
  77. package/.claude/hooks/preview.mjs +11 -11
  78. package/.claude/hooks/progress.mjs +14 -14
  79. package/.claude/hooks/qa-report.mjs +11 -10
  80. package/.claude/hooks/reminder.mjs +6 -3
  81. package/.claude/hooks/session-start.mjs +23 -17
  82. package/.claude/hooks/shared.mjs +1 -1
  83. package/.claude/hooks/status-read.mjs +5 -4
  84. package/.claude/hooks/statusline.mjs +14 -11
  85. package/.claude/hooks/structure-guard.mjs +9 -9
  86. package/.claude/hooks/test-guard.mjs +12 -12
  87. package/.claude/hooks/tools.mjs +28 -29
  88. package/.claude/hooks/usage.mjs +18 -15
  89. package/.claude/output-styles/anlatici.md +28 -28
  90. package/.claude/security-patterns.json +3 -3
  91. package/.claude/settings.json +2 -0
  92. package/.claude/skills/logo-design/SKILL.md +42 -42
  93. package/.claude/skills/motion-video/SKILL.md +52 -52
  94. package/.claude/skills/source-research/SKILL.md +35 -35
  95. package/.claude/skills/ui-motion/SKILL.md +38 -38
  96. package/.github/workflows/kit-tests.yml +7 -7
  97. package/.github/workflows/npm-publish.yml +16 -16
  98. package/AGENTS.md +56 -56
  99. package/CHANGELOG.md +7 -7
  100. package/CLAUDE.md +5 -5
  101. package/README.md +199 -172
  102. package/README.tr.md +292 -0
  103. package/docs/architecture/overview.md +31 -31
  104. package/docs/architecture/stack.md +2 -2
  105. package/docs/archive/README.md +4 -4
  106. package/docs/decisions/adr-template.md +14 -14
  107. package/docs/decisions/log.md +10 -10
  108. package/docs/findings/README.md +13 -13
  109. package/docs/findings/config.md +13 -13
  110. package/docs/legal/privacy-kvkk.md +1 -1
  111. package/docs/operations/accounts.md +25 -25
  112. package/docs/operations/deploy-setup.md +64 -64
  113. package/docs/operations/feature-flags.md +17 -17
  114. package/docs/operations/maintenance.md +48 -48
  115. package/docs/operations/release-checklist.md +61 -61
  116. package/docs/operations/spend-alarms.md +28 -28
  117. package/docs/product/feedback.md +9 -8
  118. package/docs/product/glossary.md +16 -16
  119. package/docs/product/launch.md +28 -28
  120. package/docs/product/prd.md +43 -42
  121. package/docs/product/roadmap.md +31 -30
  122. package/docs/product/specs/_template.md +13 -12
  123. package/docs/product/unit-economics.md +23 -22
  124. package/docs/quality/metrics.md +14 -14
  125. package/docs/quality/retros.md +11 -11
  126. package/docs/quality/test-plan.md +48 -48
  127. package/docs/security/checklist.md +57 -57
  128. package/docs/workflow/board.md +7 -7
  129. package/docs/workflow/cost-log.md +4 -4
  130. package/docs/workflow/decision-protocol.md +70 -70
  131. package/docs/workflow/learning.md +4 -4
  132. package/docs/workflow/mcp-catalog.md +31 -31
  133. package/docs/workflow/models.md +35 -35
  134. package/docs/workflow/progress.md +7 -7
  135. package/docs/workflow/skills.md +108 -108
  136. package/docs/workflow/usage-limits.md +62 -62
  137. package/kit/CHANGELOG.md +882 -841
  138. package/kit/VERSION +1 -1
  139. package/kit/guide.html +713 -477
  140. package/kit/guide.tr.html +1328 -0
  141. package/kit/kit.mjs +56 -54
  142. package/kit/project/README.md +29 -29
  143. package/kit/project/gitignore +12 -12
  144. package/kit/templates/addons/ai/.env.example.append +5 -5
  145. package/kit/templates/addons/ai/ai/evals/README.md +11 -11
  146. package/kit/templates/addons/ai/ai/evals/cases.jsonl +2 -2
  147. package/kit/templates/addons/ai/ai/prompts/README.md +8 -8
  148. package/kit/templates/addons/ai/docs/ai/guide.md +56 -56
  149. package/kit/templates/backend/.claude/hooks/format.mjs +1 -1
  150. package/kit/templates/backend/.claude/structure.json +1 -1
  151. package/kit/templates/backend/.env.example +6 -6
  152. package/kit/templates/backend/.github/dependabot.yml +1 -1
  153. package/kit/templates/backend/.github/workflows/ci.yml +13 -13
  154. package/kit/templates/backend/.github/workflows/claude-ci-fix.yml +19 -19
  155. package/kit/templates/backend/.github/workflows/claude-maintenance.yml +18 -18
  156. package/kit/templates/backend/.github/workflows/live-monitoring.yml +14 -14
  157. package/kit/templates/backend/.pre-commit-config.yaml +3 -3
  158. package/kit/templates/backend/docs/architecture/stack.md +98 -98
  159. package/kit/templates/backend/docs/architecture/structure.md +34 -34
  160. package/kit/templates/mobile/.claude/structure.json +1 -1
  161. package/kit/templates/mobile/.env.example +6 -6
  162. package/kit/templates/mobile/.github/dependabot.yml +2 -2
  163. package/kit/templates/mobile/.github/workflows/ci.yml +18 -18
  164. package/kit/templates/mobile/.github/workflows/claude-ci-fix.yml +19 -19
  165. package/kit/templates/mobile/.github/workflows/claude-maintenance.yml +18 -18
  166. package/kit/templates/mobile/.gitignore.append +2 -2
  167. package/kit/templates/mobile/docs/architecture/stack.md +113 -113
  168. package/kit/templates/mobile/docs/architecture/structure.md +33 -33
  169. package/kit/templates/mobile/docs/design/design-guide.md +37 -37
  170. package/kit/templates/mobile/tests/security/rls.mjs +22 -22
  171. package/kit/templates/web-saas/.claude/structure.json +1 -1
  172. package/kit/templates/web-saas/.env.example +11 -11
  173. package/kit/templates/web-saas/.github/dependabot.yml +3 -3
  174. package/kit/templates/web-saas/.github/workflows/ci.yml +20 -20
  175. package/kit/templates/web-saas/.github/workflows/claude-ci-fix.yml +19 -19
  176. package/kit/templates/web-saas/.github/workflows/claude-maintenance.yml +18 -18
  177. package/kit/templates/web-saas/.github/workflows/live-monitoring.yml +12 -12
  178. package/kit/templates/web-saas/docs/architecture/stack.md +116 -116
  179. package/kit/templates/web-saas/docs/architecture/structure.md +44 -44
  180. package/kit/templates/web-saas/docs/design/design-guide.md +38 -38
  181. package/kit/templates/web-saas/tests/live/smoke.spec.ts +4 -4
  182. package/kit/templates/web-saas/tests/security/rls.mjs +22 -22
  183. package/kit/tools/codex/.codex/hooks.json +1 -1
  184. package/package.json +2 -2
  185. package/README.en.md +0 -267
@@ -7,7 +7,7 @@
7
7
  // The result goes to .claude/state/gate.json with the same change signature as the commit gate; the commit gate stops
8
8
  // the commit on a red or stale result. A green check's output is not written, the last 30 lines of a red one are.
9
9
  // If no check passed (commands empty, no Semgrep) the result is EMPTY: the commit gate stays closed.
10
- // "Mutlak yol": an added line with this machine's project or home path is red; a generic C:\Users\<name>, /home/<name> is a note.
10
+ // "Absolute path": an added line with this machine's project or home path is red; a generic C:\Users\<name>, /home/<name> is a note.
11
11
  // node .claude/hooks/gate.mjs --approve saves the commands with user approval. A new or changed command does not run unapproved:
12
12
  // an untrusted repo's AGENTS.md must not run code through the gate. The approval lives in the user folder (~/.vibe-kit/).
13
13
  import fs from 'node:fs';
@@ -34,7 +34,7 @@ function failed(root, name, r) {
34
34
  const rel = `.claude/state/gate-log/${name.toLowerCase().replace(/\s+/g, '-')}.log`;
35
35
  fs.mkdirSync(path.join(root, path.dirname(rel)), { recursive: true });
36
36
  fs.writeFileSync(path.join(root, rel), all);
37
- return { output: lines.slice(-30).join('\n'), note: `son 30 satır; tamamı ${rel}` };
37
+ return { output: lines.slice(-30).join('\n'), note: `last 30 lines; full output in ${rel}` };
38
38
  }
39
39
 
40
40
  // Only the "## Komutlar" section is read: a "test:" line elsewhere must not count as a command
@@ -64,7 +64,7 @@ export function approve(root) {
64
64
  const file = approvalPath(root);
65
65
  fs.mkdirSync(path.dirname(file), { recursive: true });
66
66
  fs.writeFileSync(file, JSON.stringify({ digest: digest(list), commands: list, date: new Date().toISOString() }, null, 2));
67
- return `Kapı komutları onaylandı:\n${commandList(list) || '(tanımlı komut yok)'}`;
67
+ return `Gate commands approved:\n${commandList(list) || '(no commands defined)'}`;
68
68
  }
69
69
 
70
70
  export function isApproved(root, list = approvedCommands(root)) {
@@ -85,7 +85,7 @@ function check(root, { name, command }) {
85
85
  function semgrep(root, files) {
86
86
  const name = 'Semgrep';
87
87
  const code = files.filter((f) => isCodeFile(f) && fs.existsSync(path.join(root, f)));
88
- if (!code.length) return { name, status: 'skipped', note: 'değişen kod dosyası yok' };
88
+ if (!code.length) return { name, status: 'skipped', note: 'no changed code files' };
89
89
  const start = Date.now();
90
90
  const cmd = process.env.SEMGREP || 'semgrep';
91
91
  // --metrics=off: the code scan must not send usage data (docs.semgrep.dev/cli-reference, 2026-10-24)
@@ -97,8 +97,8 @@ function semgrep(root, files) {
97
97
  const outside = (y) => y && path.relative(root, y).startsWith('..');
98
98
  const found = r.error.code === 'EINVAL' ? cmd
99
99
  : process.platform === 'win32' ? (spawnSync('where', [cmd], { encoding: 'utf8' }).stdout ?? '').split(/\r?\n/).map((y) => y.trim()).find(outside) ?? '' : '';
100
- return { name, status: 'skipped', note: found ? `${found} bulundu ama çalıştırılamadı (.cmd/.bat); SEMGREP=<semgrep.exe yolu> ile ver`
101
- : 'kurulu değil; CI\'daki semgrep işi çalıştırır' };
100
+ return { name, status: 'skipped', note: found ? `${found} found but cannot run (.cmd/.bat); pass SEMGREP=<path to semgrep.exe>`
101
+ : 'not installed; the semgrep job in CI runs it' };
102
102
  }
103
103
  const sec = Math.round((Date.now() - start) / 1000);
104
104
  if (r.status === 0) return { name, status: 'passed', sec };
@@ -115,7 +115,7 @@ function semgrep(root, files) {
115
115
  // read whole. Paths are read from the git top level, so a project in a monorepo subfolder is checked too.
116
116
  const ESCAPE = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
117
117
  export function absPathCheck(root, files) {
118
- const name = 'Mutlak yol';
118
+ const name = 'Absolute path';
119
119
  const git = (...a) => spawnSync('git', a, { cwd: root, encoding: 'utf8', maxBuffer: BUFFER });
120
120
  const top = git('rev-parse', '--show-toplevel').stdout?.trim() || root;
121
121
  const variants = (p) => { const f = p.replace(/\\/g, '/').replace(/\/+$/, ''); return [f, f.replace(/\//g, '\\'), f.replace(/\//g, '\\\\')]; };
@@ -148,14 +148,14 @@ export function absPathCheck(root, files) {
148
148
  if (mine?.test(l)) red.push(at); else if (generic.test(l)) notes.push(at);
149
149
  });
150
150
  }
151
- const list = (a) => [...a.slice(0, 15), a.length > 15 ? `… ve ${a.length - 15} satır daha` : ''].filter(Boolean).join('\n');
152
- if (red.length) return { name, status: 'failed', note: 'bu makineye özel yol; import.meta.url, __dirname, path.join(kök, …), os.tmpdir() kullan. Bilinçliyse satıra ya da üstüne kit:yol-izinli yaz',
151
+ const list = (a) => [...a.slice(0, 15), a.length > 15 ? `… and ${a.length - 15} more lines` : ''].filter(Boolean).join('\n');
152
+ if (red.length) return { name, status: 'failed', note: 'path specific to this machine; use import.meta.url, __dirname, path.join(root, …), os.tmpdir(). If deliberate, write kit:yol-izinli on the line or the line above',
153
153
  output: list(red) };
154
- return { name, status: 'passed', ...(notes.length && { note: `${notes.length} satırda kullanıcı klasörüne benzeyen yol var; makineye özelse düzelt (engellemez)`, output: list(notes) }) };
154
+ return { name, status: 'passed', ...(notes.length && { note: `${notes.length} lines have a path that looks like a user folder; fix it if it is machine-specific (does not block)`, output: list(notes) }) };
155
155
  }
156
156
 
157
- export const approvalNeeded = (list) => `komut onayı gerekli\nAGENTS.md → Komutlar yeni veya değişmiş; bu komutlar onaysız çalışmaz:\n${commandList(list)}\n` +
158
- 'Komutları kullanıcıya göster ve sor. Onaylarsa: node .claude/hooks/gate.mjs --approve, sonra komutu tekrar çalıştır.';
157
+ export const approvalNeeded = (list) => `command approval needed\nAGENTS.md → Komutlar is new or changed; these commands do not run unapproved:\n${commandList(list)}\n` +
158
+ 'Show the commands to the user and ask. If they approve: node .claude/hooks/gate.mjs --approve, then run the command again.';
159
159
 
160
160
  // Spec drift (F-23): a "must not change" behavior whose test is missing or red stops the gate; no result is only a note
161
161
  // Only reports written during this gate run count (since = gate start): an older report says nothing about the code now.
@@ -164,17 +164,17 @@ function specCheck(root, since) {
164
164
  if (!list.length && !unreadable.length) return null;
165
165
  const bad = list.filter((k) => !k.test || k.result === 'failed');
166
166
  const unknown = list.filter((k) => k.test && !k.result).length;
167
- const notes = [`${list.length} değişmemesi gereken davranış`,
168
- unknown && `${unknown} testin bu koşuda sonucu yok (test komutu JUnit raporu yazmıyor)`,
169
- unreadable.length && `${unreadable.length} satır okunamadı, "- Ö-01.1: ..." biçiminde yaz: ${unreadable.map((u) => `${u.spec}: ${u.line}`).join(' · ')}`];
167
+ const notes = [`${list.length} behaviors that must not change`,
168
+ unknown && `${unknown} tests have no result in this run (the test command writes no JUnit report)`,
169
+ unreadable.length && `${unreadable.length} lines could not be read, write them as "- Ö-01.1: ...": ${unreadable.map((u) => `${u.spec}: ${u.line}`).join(' · ')}`];
170
170
  return { name: 'Spec', status: bad.length ? 'failed' : 'passed', note: notes.filter(Boolean).join('; '),
171
- output: bad.length ? bad.map((k) => `${k.id} (${k.spec}): ${k.test ? 'test kırmızı' : 'test yok'}`).join('\n') : undefined };
171
+ output: bad.length ? bad.map((k) => `${k.id} (${k.spec}): ${k.test ? 'test red' : 'no test'}`).join('\n') : undefined };
172
172
  }
173
173
 
174
- // Status label and mark shown to the user
175
- export const STATUS_LABEL = { passed: 'geçti', failed: 'kaldı', unset: 'tanımsız', skipped: 'atlandı' };
174
+ // Status label and mark in the gate output
175
+ export const STATUS_LABEL = { passed: 'passed', failed: 'failed', unset: 'unset', skipped: 'skipped' };
176
176
  const MARK = { passed: '✅', failed: '❌', unset: '⚪', skipped: '⚪' };
177
- const line = (k) => `${MARK[k.status]} ${k.name}${k.sec != null ? ` (${k.sec} sn)` : ''}${k.status === 'passed' ? '' : `: ${STATUS_LABEL[k.status]}`}${k.note ? ` (${k.note})` : ''}` +
177
+ const line = (k) => `${MARK[k.status]} ${k.name}${k.sec != null ? ` (${k.sec} s)` : ''}${k.status === 'passed' ? '' : `: ${STATUS_LABEL[k.status]}`}${k.note ? ` (${k.note})` : ''}` +
178
178
  (k.output ? `\n\`\`\`\n${k.output}\n\`\`\`` : '');
179
179
 
180
180
  export function runGate(root) {
@@ -182,7 +182,7 @@ export function runGate(root) {
182
182
  let signature = null, files = [];
183
183
  try { ({ signature, files } = changes()); } catch { /* not a git repo: the commands still run */ }
184
184
  const list = approvedCommands(root);
185
- if (!isApproved(root, list)) return { passed: false, text: `## Kapı · ${approvalNeeded(list)}` };
185
+ if (!isApproved(root, list)) return { passed: false, text: `## Gate · ${approvalNeeded(list)}` };
186
186
  const checks = [...list.filter((k) => CHECKS.includes(k.name)).map((k) => check(root, k)), semgrep(root, files)];
187
187
  const empty = !checks.some((k) => k.status === 'passed');
188
188
  const spec = specCheck(root, start); // after the test command, which may have refreshed the JUnit report; it does not fill an empty gate
@@ -192,16 +192,16 @@ export function runGate(root) {
192
192
  const file = path.join(root, '.claude', 'state', 'gate.json');
193
193
  fs.mkdirSync(path.dirname(file), { recursive: true });
194
194
  fs.writeFileSync(file, JSON.stringify({ signature, date: new Date().toISOString(), passed, empty, checks }, null, 2));
195
- const result = checks.some((k) => k.status === 'failed') ? 'Sonuç: KIRMIZI. Commit kapısı açılmaz: düzelt ve kapıyı tekrar çalıştır.'
196
- : empty ? 'Sonuç: BOŞ, hiçbir kontrol çalışmadı. AGENTS.md → Komutlar\'ı doldur; commit kapısı açılmaz (kullanıcı isterse commit-gate.mjs --skip).'
197
- : 'Sonuç: YEŞİL.';
198
- return { passed, text: [`## Kapı · ${files.length} dosya değişti`, ...checks.map(line), result].join('\n') };
195
+ const result = checks.some((k) => k.status === 'failed') ? 'Result: RED. The commit gate stays closed: fix it and run the gate again.'
196
+ : empty ? 'Result: EMPTY, no check ran. Fill in AGENTS.md → Komutlar; the commit gate stays closed (if the user wants, commit-gate.mjs --skip).'
197
+ : 'Result: GREEN.';
198
+ return { passed, text: [`## Gate · ${files.length} files changed`, ...checks.map(line), result].join('\n') };
199
199
  }
200
200
 
201
201
  if (isMain(import.meta.url)) {
202
202
  const arg = process.argv[2];
203
203
  if (arg === '--approve') { console.log(approve(findRoot())); process.exit(0); }
204
- if (arg !== '--run') { console.error('Kullanım: node .claude/hooks/gate.mjs --run | --approve'); process.exit(1); }
204
+ if (arg !== '--run') { console.error('Usage: node .claude/hooks/gate.mjs --run | --approve'); process.exit(1); }
205
205
  const { passed, text } = runGate(findRoot());
206
206
  console.log(text);
207
207
  process.exit(passed ? 0 : 1);
@@ -8,14 +8,14 @@ import { execSync } from 'node:child_process';
8
8
  import { readInput, decide, isMain } from './shared.mjs';
9
9
 
10
10
  export const PATTERNS = [
11
- ['Anthropic/OpenAI anahtarı', /\bsk-(?:ant-|proj-)?[A-Za-z0-9_-]{32,}/],
12
- ['Stripe gizli anahtarı', /\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{20,}/],
13
- ['AWS erişim anahtarı', /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/],
11
+ ['Anthropic/OpenAI key', /\bsk-(?:ant-|proj-)?[A-Za-z0-9_-]{32,}/],
12
+ ['Stripe secret key', /\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{20,}/],
13
+ ['AWS access key', /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/],
14
14
  ['GitHub token', /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}|\bgithub_pat_[A-Za-z0-9_]{50,}/],
15
15
  ['Slack token', /\bxox[abprs]-[A-Za-z0-9-]{10,}/],
16
- ['Özel anahtar', /-----BEGIN (?:[A-Z]+ )?PRIVATE KEY-----/],
17
- ['Resend anahtarı', /\bre_[A-Za-z0-9]{8,}_[A-Za-z0-9]{16,}/],
18
- ['Supabase gizli anahtarı', /\bsb_secret_(?=[\w-]*\d)[\w-]{16,}/],
16
+ ['Private key', /-----BEGIN (?:[A-Z]+ )?PRIVATE KEY-----/],
17
+ ['Resend key', /\bre_[A-Za-z0-9]{8,}_[A-Za-z0-9]{16,}/],
18
+ ['Supabase secret key', /\bsb_secret_(?=[\w-]*\d)[\w-]{16,}/],
19
19
  ];
20
20
 
21
21
  export function findSecrets(text) {
@@ -27,14 +27,14 @@ function hookMode() {
27
27
  if (!g) process.exit(0); // unreadable input: do not block the work
28
28
  // Codex and Antigravity also scan shell commands (heredoc, echo > file); Claude Code only file tools
29
29
  if (g.tool !== 'claude' && findSecrets(g.command).length) {
30
- decide(g.tool, 'deny', `ENGELLENDİ: komut sır içeriyor (${findSecrets(g.command).join(', ')}). Sırrı komuta veya dosyaya yazma; ortam değişkeninden oku.`);
30
+ decide(g.tool, 'deny', `BLOCKED: the command contains a secret (${findSecrets(g.command).join(', ')}). Do not write the secret into a command or a file; read it from an environment variable.`);
31
31
  }
32
32
  for (const { path, text } of g.files) {
33
33
  const found = findSecrets(text);
34
34
  if (found.length) {
35
35
  decide(g.tool, 'deny',
36
- `ENGELLENDİ: ${path} dosyasına sır yazılıyor (${found.join(', ')}). ` +
37
- 'Sırrı koda yazma; ortam değişkeninden oku ve değişken adını .env.example dosyasına boş değerle ekle.');
36
+ `BLOCKED: a secret is being written to ${path} (${found.join(', ')}). ` +
37
+ 'Do not put the secret in code; read it from an environment variable and add the variable name to .env.example with an empty value.');
38
38
  }
39
39
  }
40
40
  }
@@ -51,8 +51,8 @@ function stagedMode() {
51
51
  }
52
52
  }
53
53
  if (findings.length) {
54
- console.error('Commit durduruldu, sır bulundu:\n ' + findings.join('\n '));
55
- console.error('Sırrı kaldır, ortam değişkenine taşı. Sızmış olabilecek anahtarı iptal edip yenile.');
54
+ console.error('Commit stopped, secret found:\n ' + findings.join('\n '));
55
+ console.error('Remove the secret and move it to an environment variable. Revoke and replace any key that may have leaked.');
56
56
  process.exit(1);
57
57
  }
58
58
  }
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+ // Conversation language of the project. /start and /adopt ask it first, /language changes it, /kit-update asks old projects once.
3
+ // node .claude/hooks/lang.mjs --show current code (none: not chosen yet)
4
+ // node .claude/hooks/lang.mjs --set <code> BCP-47 code (tr, en, de, pt-BR); .kit.json → lang, before install .claude/state/lang.json
5
+ // node .claude/hooks/lang.mjs --export [--missing] English texts as JSON, to translate for a language other than tr/en
6
+ // node .claude/hooks/lang.mjs --write < x.json saves the translation to .claude/i18n/<code>.json (merged)
7
+ // node .claude/hooks/lang.mjs --check exit 1 and lists the keys still missing a translation
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import { findRoot, isMain } from './shared.mjs';
11
+ import { MESSAGES } from './messages.mjs';
12
+
13
+ // The code becomes a file name: nothing that can leave .claude/i18n/
14
+ export const LANG_RE = /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
15
+ const PLACEHOLDER = /\{(\w+)\}/g;
16
+
17
+ const readJson = (file) => { try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; } };
18
+ // Windows device names (nul.json is not a file) and over-long names cannot become a file either
19
+ const DEVICE = /^(con|prn|aux|nul|com\d|lpt\d)$/;
20
+ const validCode = (code) => typeof code === 'string' && code.length <= 35 && LANG_RE.test(code) && !DEVICE.test(code) ? code : null;
21
+ const kitFile = (root) => path.join(root, '.kit.json');
22
+ const stateFile = (root) => path.join(root, '.claude', 'state', 'lang.json');
23
+ const i18nFile = (root, lang) => path.join(root, '.claude', 'i18n', `${lang}.json`);
24
+ const placeholders = (s) => [...new Set([...s.matchAll(PLACEHOLDER)].map((m) => m[1]))].sort().join(',');
25
+
26
+ // null: not set up and not chosen yet (/start asks). Projects set up before languages existed were Turkish.
27
+ export function readLang(root = findRoot()) {
28
+ const kit = readJson(kitFile(root));
29
+ const chosen = validCode(kit?.lang) ?? validCode(readJson(stateFile(root))?.lang);
30
+ if (chosen) return chosen;
31
+ // same rule as status-read kitPending: a broken .kit.json counts as missing
32
+ const notSetUp = kit ? kit.type === null : fs.existsSync(path.join(root, 'kit', 'kit.mjs'));
33
+ return notSetUp ? null : 'tr';
34
+ }
35
+
36
+ export function setLang(root, code) {
37
+ if (!validCode(code)) throw new Error(`Invalid language code: ${code}. Use a BCP-47 code such as tr, en, de, pt-BR.`);
38
+ if (fs.existsSync(kitFile(root))) {
39
+ const kit = readJson(kitFile(root));
40
+ if (!kit || typeof kit !== 'object' || Array.isArray(kit)) throw new Error('.kit.json is not a valid JSON object: repair it first, then set the language.');
41
+ fs.writeFileSync(kitFile(root), JSON.stringify({ ...kit, lang: code }, null, 2) + '\n');
42
+ return kitFile(root);
43
+ }
44
+ fs.mkdirSync(path.dirname(stateFile(root)), { recursive: true });
45
+ fs.writeFileSync(stateFile(root), JSON.stringify({ lang: code }) + '\n');
46
+ return stateFile(root);
47
+ }
48
+
49
+ const translation = (root, lang) => {
50
+ const j = readJson(i18nFile(root, lang));
51
+ return j && typeof j === 'object' && !Array.isArray(j) ? j : {};
52
+ };
53
+
54
+ export function t(root, key, vars = {}) {
55
+ if (!Object.hasOwn(MESSAGES, key)) return key;
56
+ const lang = readLang(root) ?? 'en';
57
+ let text = MESSAGES[key][lang];
58
+ if (text === undefined) {
59
+ const own = translation(root, lang);
60
+ text = Object.hasOwn(own, key) && typeof own[key] === 'string' ? own[key] : MESSAGES[key].en;
61
+ }
62
+ return text.replace(PLACEHOLDER, (m, name) => Object.hasOwn(vars, name) ? String(vars[name]) : m);
63
+ }
64
+
65
+ const needsTranslation = (lang) => lang !== null && lang !== 'tr' && lang !== 'en';
66
+
67
+ export function missingKeys(root = findRoot()) {
68
+ const lang = readLang(root);
69
+ if (!needsTranslation(lang)) return [];
70
+ const own = translation(root, lang);
71
+ return Object.keys(MESSAGES).filter((k) => !(Object.hasOwn(own, k) && typeof own[k] === 'string'));
72
+ }
73
+
74
+ export function writeTranslation(root, obj) {
75
+ const lang = readLang(root);
76
+ if (!needsTranslation(lang)) throw new Error(`No translation file for language ${lang ?? 'none'}: tr and en are built in.`);
77
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj) || Object.getPrototypeOf(obj) !== Object.prototype) throw new Error('Translation must be a JSON object { key: text }.');
78
+ for (const [k, v] of Object.entries(obj)) {
79
+ if (!Object.hasOwn(MESSAGES, k)) throw new Error(`Unknown key: ${k}`);
80
+ if (typeof v !== 'string') throw new Error(`Not a text: ${k}`);
81
+ if (placeholders(v) !== placeholders(MESSAGES[k].en)) throw new Error(`${k}: keep the placeholders exactly ({${placeholders(MESSAGES[k].en).split(',').join('}, {')}}).`);
82
+ }
83
+ const file = i18nFile(root, lang);
84
+ fs.mkdirSync(path.dirname(file), { recursive: true });
85
+ fs.writeFileSync(file, JSON.stringify({ ...translation(root, lang), ...obj }, null, 2) + '\n');
86
+ return file;
87
+ }
88
+
89
+ if (isMain(import.meta.url)) {
90
+ const root = findRoot();
91
+ const [cmd, arg] = process.argv.slice(2);
92
+ try {
93
+ if (cmd === '--show') console.log(readLang(root) ?? 'none');
94
+ else if (cmd === '--set') console.log(`Language: ${arg} → ${path.relative(root, setLang(root, arg))}`);
95
+ else if (cmd === '--export') {
96
+ const keys = arg === '--missing' ? missingKeys(root) : Object.keys(MESSAGES);
97
+ console.log(JSON.stringify(Object.fromEntries(keys.map((k) => [k, MESSAGES[k].en])), null, 2));
98
+ } else if (cmd === '--write') {
99
+ let obj;
100
+ try { obj = JSON.parse(fs.readFileSync(0, 'utf8')); } catch { throw new Error('stdin is not valid JSON.'); }
101
+ console.log(`Written: ${path.relative(root, writeTranslation(root, obj))}`);
102
+ } else if (cmd === '--check') {
103
+ const missing = missingKeys(root);
104
+ if (missing.length) { console.log(missing.join('\n')); process.exit(1); }
105
+ console.log('ok');
106
+ } else {
107
+ console.error('Usage: --show | --set <code> | --export [--missing] | --write < file.json | --check');
108
+ process.exit(1);
109
+ }
110
+ } catch (err) {
111
+ console.error(err.message);
112
+ process.exit(1);
113
+ }
114
+ }