tillerhand 3.48.1 → 3.49.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.
Files changed (108) hide show
  1. package/.claude/agents/board-chair.md +1 -1
  2. package/.claude/agents/security-auditor.md +1 -1
  3. package/.claude/agents/test-writer.md +1 -1
  4. package/.claude/board/board-product.md +1 -1
  5. package/.claude/board/council.md +1 -1
  6. package/.claude/board/rules.md +6 -6
  7. package/.claude/commands/adopt.md +3 -3
  8. package/.claude/commands/ask-mode.md +4 -4
  9. package/.claude/commands/board.md +1 -1
  10. package/.claude/commands/build.md +1 -1
  11. package/.claude/commands/design.md +4 -4
  12. package/.claude/commands/findings-business.md +1 -1
  13. package/.claude/commands/findings-legal.md +1 -1
  14. package/.claude/commands/findings-perf.md +1 -1
  15. package/.claude/commands/findings-security.md +2 -2
  16. package/.claude/commands/findings-tech.md +1 -1
  17. package/.claude/commands/findings-ux.md +1 -1
  18. package/.claude/commands/fix-bug.md +2 -2
  19. package/.claude/commands/handoff.md +2 -2
  20. package/.claude/commands/kit-update.md +2 -2
  21. package/.claude/commands/qa.md +1 -1
  22. package/.claude/commands/release.md +2 -2
  23. package/.claude/commands/research.md +3 -3
  24. package/.claude/commands/retro.md +1 -1
  25. package/.claude/commands/rollback.md +3 -3
  26. package/.claude/commands/start.md +4 -4
  27. package/.claude/commands/tools.md +1 -1
  28. package/.claude/commands/user-feedback.md +1 -1
  29. package/.claude/findings/lenses/business.md +1 -1
  30. package/.claude/findings/protocol.md +2 -2
  31. package/.claude/hooks/budget.mjs +1 -1
  32. package/.claude/hooks/checkpoint.mjs +2 -2
  33. package/.claude/hooks/commit-gate.mjs +14 -15
  34. package/.claude/hooks/council.mjs +37 -52
  35. package/.claude/hooks/dashboard-data.mjs +34 -42
  36. package/.claude/hooks/dashboard.mjs +18 -20
  37. package/.claude/hooks/destructive-guard.mjs +2 -2
  38. package/.claude/hooks/flow.mjs +1 -1
  39. package/.claude/hooks/freeze-guard.mjs +2 -2
  40. package/.claude/hooks/gate.mjs +18 -11
  41. package/.claude/hooks/labels.mjs +92 -152
  42. package/.claude/hooks/lang.mjs +25 -9
  43. package/.claude/hooks/messages.mjs +496 -494
  44. package/.claude/hooks/notify.mjs +1 -1
  45. package/.claude/hooks/prefs.mjs +10 -18
  46. package/.claude/hooks/preview.mjs +1 -1
  47. package/.claude/hooks/progress.mjs +7 -9
  48. package/.claude/hooks/qa-report.mjs +1 -3
  49. package/.claude/hooks/session-start.mjs +2 -3
  50. package/.claude/hooks/status-read.mjs +5 -5
  51. package/.claude/hooks/statusline.mjs +4 -4
  52. package/.claude/hooks/tools.mjs +3 -7
  53. package/.claude/hooks/usage.mjs +2 -2
  54. package/.claude/i18n/tr.json +495 -0
  55. package/.claude/security-patterns.json +1 -1
  56. package/.claude/skills/logo-design/SKILL.md +2 -2
  57. package/.claude/skills/motion-video/SKILL.md +1 -1
  58. package/.claude/skills/source-research/SKILL.md +1 -1
  59. package/.claude/skills/ui-motion/SKILL.md +1 -1
  60. package/AGENTS.md +1 -1
  61. package/CHANGELOG.md +1 -1
  62. package/README.md +2 -2
  63. package/README.tr.md +2 -2
  64. package/docs/architecture/overview.md +1 -1
  65. package/docs/decisions/log.md +1 -1
  66. package/docs/legal/privacy-kvkk.md +35 -35
  67. package/docs/operations/deploy-setup.md +1 -1
  68. package/docs/operations/feature-flags.md +1 -1
  69. package/docs/product/glossary.md +3 -3
  70. package/docs/product/launch.md +1 -1
  71. package/docs/product/prd.md +2 -2
  72. package/docs/product/roadmap.md +1 -1
  73. package/docs/product/specs/_template.md +1 -1
  74. package/docs/product/unit-economics.md +1 -1
  75. package/docs/quality/test-plan.md +5 -5
  76. package/docs/workflow/decision-protocol.md +1 -2
  77. package/docs/workflow/mcp-catalog.md +1 -1
  78. package/docs/workflow/skills.md +2 -2
  79. package/docs/workflow/usage-limits.md +1 -1
  80. package/kit/CHANGELOG.md +28 -0
  81. package/kit/VERSION +1 -1
  82. package/kit/guide.html +56 -56
  83. package/kit/guide.tr.html +10 -10
  84. package/kit/kit.mjs +7 -42
  85. package/kit/templates/addons/ai/.env.example.append +3 -3
  86. package/kit/templates/addons/tr/docs/legal/turkiye.md +55 -55
  87. package/kit/templates/addons/tr/types/backend/.env.example.append +2 -2
  88. package/kit/templates/addons/tr/types/backend/app/shared/payments.py +1 -1
  89. package/kit/templates/addons/tr/types/backend/tests/security/test_payment_webhook.py +3 -3
  90. package/kit/templates/addons/tr/types/web-saas/.env.example.append +2 -2
  91. package/kit/templates/addons/tr/types/web-saas/.github/workflows/payment-webhook.yml +3 -3
  92. package/kit/templates/addons/tr/types/web-saas/src/lib/payments/webhook-signature.ts +1 -1
  93. package/kit/templates/addons/tr/types/web-saas/tests/security/payment-webhook.mjs +4 -4
  94. package/kit/templates/backend/.github/dependabot.yml +1 -1
  95. package/kit/templates/backend/.github/workflows/live-monitoring.yml +1 -1
  96. package/kit/templates/backend/docs/architecture/stack.md +3 -3
  97. package/kit/templates/mobile/.github/dependabot.yml +1 -1
  98. package/kit/templates/mobile/.github/workflows/ci.yml +2 -2
  99. package/kit/templates/mobile/docs/architecture/stack.md +4 -4
  100. package/kit/templates/mobile/docs/architecture/structure.md +1 -1
  101. package/kit/templates/mobile/docs/design/design-guide.md +1 -1
  102. package/kit/templates/web-saas/.github/dependabot.yml +1 -1
  103. package/kit/templates/web-saas/.github/workflows/ci.yml +2 -2
  104. package/kit/templates/web-saas/.github/workflows/live-monitoring.yml +1 -1
  105. package/kit/templates/web-saas/docs/architecture/stack.md +4 -4
  106. package/kit/templates/web-saas/docs/design/design-guide.md +1 -1
  107. package/kit/tools/antigravity/.agents/hooks.json +5 -5
  108. package/package.json +1 -1
@@ -38,7 +38,7 @@ function main() {
38
38
  if (process.argv.includes('--stop')) {
39
39
  if (input.stop_hook_active) process.exit(0);
40
40
  let startedAt = 0;
41
- 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
41
+ try { const r = JSON.parse(fs.readFileSync(path.join(root, '.claude', 'state', 'reminder.json'), 'utf8')); startedAt = r.turnAt ?? 0; } catch { /* */ }
42
42
  const duration = (Date.now() - startedAt) / 1000;
43
43
  if (!startedAt || duration < THRESHOLD_SEC) process.exit(0);
44
44
  message = t(root, 'notify.done', { minutes: Math.round(duration / 60) });
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  // Personal preferences: how this user likes to work (talk, questions, reports, code style, things to avoid).
3
- // One file for all projects: ~/.vibe-kit/preferences.md (tercihler.md before 3.48, renamed on first use). Git never sees
3
+ // One file for all projects: ~/.vibe-kit/preferences.md. Git never sees
4
4
  // it; the session-start hook loads it every session.
5
5
  // node .claude/hooks/prefs.mjs --list numbered list
6
6
  // node .claude/hooks/prefs.mjs --add <category> "<preference>" only with the user's approval (asked)
7
7
  // node .claude/hooks/prefs.mjs --remove <no> (asked)
8
- // Categories: conversation, questions, reports, code, avoid (the pre-3.48 konusma, sorular, raporlar, kod, kacin still work).
8
+ // Categories: conversation, questions, reports, code, avoid.
9
9
  // A project rule goes to AGENTS.md → Learned rules, not here.
10
10
  // Why a script: the AI never writes this file directly; one line per preference, no secrets, a size cap.
11
11
  // The same rules apply when reading: any program on the machine can write this file, so what is loaded is checked
@@ -17,27 +17,19 @@ import { findSecrets } from './guard-secrets.mjs';
17
17
  import { isMain } from './shared.mjs';
18
18
 
19
19
  export const CATEGORIES = { conversation: 'Conversation', questions: 'Questions and decisions', reports: 'Reports', code: 'Code', avoid: 'Avoid' };
20
- // Pre-3.48 keys and headings, still read
21
- const OLD = { konusma: ['conversation', 'Konuşma'], sorular: ['questions', 'Sorular ve kararlar'], raporlar: ['reports', 'Raporlar'], kod: ['code', 'Kod'], kacin: ['avoid', 'Kaçınılacaklar'] };
22
- export const categoryKey = (k) => (Object.hasOwn(CATEGORIES, k ?? '') ? k : Object.hasOwn(OLD, k ?? '') ? OLD[k][0] : null);
20
+ export const categoryKey = (k) => (Object.hasOwn(CATEGORIES, k ?? '') ? k : null);
23
21
  export const MAX_ITEMS = 40;
24
22
  const MAX_LENGTH = 200;
25
23
  const MAX_BYTES = 64 * 1024;
26
24
 
27
25
  // os.homedir() follows HOME / USERPROFILE; the file name is fixed, so no other file can be written through it.
28
- // A pre-3.48 tercihler.md is renamed once (decision: a 3.47 project on the same machine no longer sees it).
29
- export const prefsFile = () => {
30
- const file = path.join(os.homedir(), '.vibe-kit', 'preferences.md');
31
- const old = path.join(os.homedir(), '.vibe-kit', 'tercihler.md');
32
- if (fs.existsSync(file) || !fs.existsSync(old)) return file;
33
- try { fs.renameSync(old, file); return file; } catch { return old; }
34
- };
26
+ export const prefsFile = () => path.join(os.homedir(), '.vibe-kit', 'preferences.md');
35
27
 
36
- // A heading is matched by key or title, ignoring case and Turkish letters (## kod, ## Kacinilacaklar)
37
- const fold = (s) => s.toLocaleLowerCase('tr').normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/ı/g, 'i').trim();
38
- const categoryOf = (heading) => Object.keys(CATEGORIES).find((k) => fold(k) === fold(heading) || fold(CATEGORIES[k]) === fold(heading))
39
- ?? Object.values(OLD).find(([, title]) => fold(title) === fold(heading))?.[0]
40
- ?? categoryKey(Object.keys(OLD).find((k) => fold(k) === fold(heading)));
28
+ // A heading is matched by key or title, ignoring case (## code, ## Questions and decisions)
29
+ const fold = (s) => s.toLowerCase().trim();
30
+ const categoryOf = (heading) => Object.keys(CATEGORIES).find((k) => fold(k) === fold(heading) || fold(CATEGORIES[k]) === fold(heading));
31
+ // Duplicate check on user text in any language: case and accents folded, dotless i as i ("KISA" = "kısa", "İ" = "i")
32
+ const folded = (s) => s.normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/\u0131/g, 'i');
41
33
  const clean = (s) => String(s ?? '').replace(/[\u0000-\u001f\u007f-\u009f]/g, ' ').replace(/\s+/g, ' ').trim();
42
34
  const valid = (t) => t && t.length <= MAX_LENGTH && !findSecrets(t).length;
43
35
 
@@ -85,7 +77,7 @@ export function addPref(key, text, file = prefsFile()) {
85
77
  if (findSecrets(t).length) return { error: 'The preference contains a value that looks like a secret; secrets are not written to this file.' };
86
78
  const { items, unknown } = parsePrefs(file);
87
79
  if (unknown.length) return { error: `The file has unreadable lines; nothing was written, so they are not lost. Fix or remove them (${file}):\n${unknown.slice(0, 5).join('\n')}` };
88
- const same = items.find((p) => p.text.toLocaleLowerCase('tr') === t.toLocaleLowerCase('tr'));
80
+ const same = items.find((p) => folded(p.text) === folded(t));
89
81
  if (same) return { existing: same.category };
90
82
  if (items.length >= MAX_ITEMS) return { error: `At most ${MAX_ITEMS} preferences are kept. Delete an old one first: --list, then --remove <no>.` };
91
83
  const error = writePrefs([...items, { category, text: t }], file);
@@ -57,7 +57,7 @@ export async function preview(root, task) {
57
57
  if (!url) return fail('AGENTS.md → Commands has no `preview:` line, or it is empty. Add it (example: `preview: http://localhost:3000`) or produce the evidence by hand in this task.');
58
58
  if (!LOCAL.test(url)) return fail(`the preview address must be local (http://localhost:<port>, 127.0.0.1 or [::1]). Now: ${url}`);
59
59
  if (!isApproved(root, list)) return { code: 1, text: `Preview · ${approvalNeeded(list)}` };
60
- const dir = `.previews/${String(task || 'son').replace(/[^\w-]/g, '-')}`;
60
+ const dir = `.previews/${String(task || 'latest').replace(/[^\w-]/g, '-')}`;
61
61
  // A linked folder from the repo would send the files elsewhere
62
62
  for (const d of ['.previews', dir]) {
63
63
  try { if (fs.lstatSync(path.join(root, d)).isSymbolicLink()) return fail(`${d} is a link (symlink/junction); it must be a real folder.`); } catch { /* missing: created below */ }
@@ -6,27 +6,25 @@
6
6
  // node .claude/hooks/progress.mjs --cost "<job>" <size> ["<note>"] [--limit N] start from --start, end now (once)
7
7
  // node .claude/hooks/progress.mjs --task "<active task>" --did "<done>" --next "<next step>"
8
8
  // node .claude/hooks/progress.mjs --check progress.md labels the status line and dashboard read
9
- // Every progress write also sets **Last updated:** (**Son güncelleme:** in a Turkish file) to today. Flags can be combined in one call.
9
+ // Every progress write also sets **Last updated:** to today. Flags can be combined in one call.
10
10
  import fs from 'node:fs';
11
11
  import path from 'node:path';
12
12
  import { findRoot, isMain } from './shared.mjs';
13
13
  import { clean, labels } from './status-read.mjs';
14
- import { label, labelIn, normalize } from './labels.mjs';
14
+ import { label } from './labels.mjs';
15
15
  import { readLimits } from './usage.mjs';
16
16
 
17
17
  const REQUIRED = ['lastUpdated', 'activePhase', 'activeTask', 'setupStep', 'questionMode', 'profile'];
18
18
  const SIZES = ['S', 'M', 'L', 'BOARD', 'DESIGN', 'FINDINGS'];
19
- // Sizes before 3.48 (küçük / orta / büyük); still accepted, written as the English one
20
- export const OLD_SIZES = { K: 'S', O: 'M', B: 'L' };
21
19
  const USAGE = 'Usage: node .claude/hooks/progress.mjs [--done G-NNN] [--start] [--cost "<job>" <S|M|L|BOARD|DESIGN|FINDINGS> [<start %> <end %> <min>] ["<note>"]] [--limit N] [--task "<text>"] [--did "<text>"] [--next "<text>"] [--check]';
22
20
  // One line per value: a newline would break a label, a list item or a table row
23
21
  const line = (s) => clean(String(s).replace(/\s*\r?\n\s*/g, ' ')).trim();
24
22
  const today = () => new Date().toLocaleDateString('sv-SE');
25
- // Labels and headings are written in the language the file already uses (labelIn). Function replacements: a value
23
+ // Labels and headings are written in English. Function replacements: a value
26
24
  // with $& or $' is text, not a replacement pattern.
27
25
  const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
28
- const setLabel = (text, key, v) => text.replace(new RegExp(`^(\\*\\*${esc(labelIn(text, key))}:\\*\\*).*$`, 'm'), (_, l) => `${l} ${v}`);
29
- const sectionRe = (text, key) => new RegExp(`^(## ${esc(labelIn(text, key))}[^\\n]*\\n)([\\s\\S]*?)(?=^## |(?![\\s\\S]))`, 'm');
26
+ const setLabel = (text, key, v) => text.replace(new RegExp(`^(\\*\\*${esc(label(key))}:\\*\\*).*$`, 'm'), (_, l) => `${l} ${v}`);
27
+ const sectionRe = (text, key) => new RegExp(`^(## ${esc(label(key))}[^\\n]*\\n)([\\s\\S]*?)(?=^## |(?![\\s\\S]))`, 'm');
30
28
  const setSection = (text, key, body) => text.replace(sectionRe(text, key), (_, h) => `${h}\n${body}\n\n`);
31
29
  const section = (text, key) => text.match(sectionRe(text, key))?.[2] ?? '';
32
30
 
@@ -40,7 +38,7 @@ export function markDone(text, id) {
40
38
 
41
39
  export function costRow(args, date = today()) {
42
40
  const [job, typed, start, end, min, note = ''] = args;
43
- const size = OLD_SIZES[typed] ?? typed;
41
+ const size = typed;
44
42
  const nums = [start, end, min].map(Number);
45
43
  if (!job || !SIZES.includes(size) || nums.some((n) => !Number.isFinite(n) || n < 0) || args.length > 6) return null;
46
44
  // The end below the start means the 5-hour window renewed in between: the difference is unknown
@@ -62,7 +60,7 @@ export function updateProgress(text, { task, did, next }) {
62
60
 
63
61
  // By their English names, whichever language the file uses; an example inside a code block does not count
64
62
  export const missingLabels = (text) => {
65
- const found = labels(normalize(text).replace(/^```[\s\S]*?^```.*$/gm, ''));
63
+ const found = labels(text.replace(/^```[\s\S]*?^```.*$/gm, ''));
66
64
  return REQUIRED.map((k) => label(k)).filter((k) => !(k in found));
67
65
  };
68
66
 
@@ -42,9 +42,7 @@ export function qaReport(root, junit = 'test-results-qa/junit.xml', now = new Da
42
42
  const rows = criteria.map((k) => {
43
43
  const r = results.get(k.key);
44
44
  let evidence = '';
45
- // The test may name the screenshot in the other id form (PRD "Ö-01.1", test "AC-01.1", or the other way round)
46
- const names = [k.id, k.id.replace(/^Ö-/, 'AC-'), k.id.replace(/^AC-/, 'Ö-')];
47
- const shot = [...new Set(names)].map((n) => path.join(root, path.dirname(junit), 'shots', `${n}.png`))
45
+ const shot = [path.join(root, path.dirname(junit), 'shots', `${k.id}.png`)]
48
46
  .find((p) => { try { return fs.lstatSync(p).isFile(); } catch { return false; } });
49
47
  // Only a real screenshot file: a shots/<id>.png link would copy any local file into .previews/
50
48
  const real = !!shot;
@@ -21,7 +21,7 @@ const root = findRoot();
21
21
 
22
22
  const lang = readLang(root);
23
23
  notes.push(lang
24
- ? `Conversation language: ${lang}. Write every question, option, answer, status block and user-facing document text in this language, even when the user, a file or a tool writes in another; code, commands and paths stay as they are.`
24
+ ? `Conversation language: ${lang}. Write every question, option, answer, status block and user-facing document text in this language, even when the user, a file or a tool writes in another; code, commands and paths stay as they are. Labels, headings, keys and verdict words in project files (**Active phase:**, ## Where we left off, type check:, APPROVE / CONDITIONAL / REJECT) stay English in every language: tools read them.`
25
25
  : 'Language not chosen yet: /start and /adopt ask it first, in English.');
26
26
 
27
27
  try {
@@ -33,8 +33,7 @@ try {
33
33
  const a = d.fields;
34
34
  notes.push(`Phase: ${a['Active phase'] ?? '-'} · Task: ${a['Active task'] ?? '-'} · Question mode: ${a['Question mode'] ?? 'on'} · Board: ${d.board.state}`);
35
35
  if (isValue(a['Active phase'], 'setup') && d.installed) {
36
- // "Setup (adopt)", before 3.48 "Başlangıç (uyum)"
37
- const command = /\((adopt|uyum)\)/i.test(a['Active phase']) ? '/adopt' : '/start';
36
+ const command = /\(adopt\)/i.test(a['Active phase']) ? '/adopt' : '/start';
38
37
  notes.push(`${command} was left unfinished (step ${a['Setup step']}). Suggest the user continue with ${command} where they left off.`);
39
38
  next = t(root, 'session.resume', { command, step: a['Setup step'] });
40
39
  } else {
@@ -1,17 +1,17 @@
1
1
  // Reads the project state from the docs. Used by statusline, session-start and the dashboard.
2
2
  // Formats read: **Label:** lines and ## sections in docs/workflow/progress.md, checkboxes under "## Phase" headings
3
3
  // in docs/product/roadmap.md, the "Decision log" in docs/decisions/log.md and the docs/workflow/board.md tables.
4
- // Labels are read in English: readText turns the Turkish ones of pre-3.48 projects into English.
4
+ // Labels are read in English.
5
5
  // Must stay fast: the status line calls it on every refresh (git and heavy reads are in dashboard.mjs).
6
6
  import fs from 'node:fs';
7
7
  import path from 'node:path';
8
8
  import { t } from './lang.mjs';
9
- import { normalize, isValue } from './labels.mjs';
9
+ import { isValue } from './labels.mjs';
10
10
 
11
11
  // Only a regular file: a folder or a link to a folder named like a test or report file must not throw (EISDIR)
12
- // The project name placeholder in AGENTS.md (before 3.48 '[PROJE ADI]')
13
- export const PLACEHOLDER_NAMES = ['[PROJECT NAME]', '[PROJE ADI]'];
14
- export const readText = (p) => { try { return fs.statSync(p).isFile() ? normalize(fs.readFileSync(p, 'utf8')) : ''; } catch { return ''; } };
12
+ // The project name placeholder in AGENTS.md
13
+ export const PLACEHOLDER_NAMES = ['[PROJECT NAME]'];
14
+ export const readText = (p) => { try { return fs.statSync(p).isFile() ? fs.readFileSync(p, 'utf8') : ''; } catch { return ''; } };
15
15
 
16
16
  export function sections(text, level = '## ') {
17
17
  const result = {};
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // Claude Code bottom status line: phase, task progress, question mode and usage limits.
3
- // Example: ▶ Faz 1 · G-012 · task 7/23 · questions: open (closed: tasarim) · 5h 34% · weekly 5%
3
+ // Example: ▶ Phase 1 · G-012 · task 7/23 · questions: open (closed: design) · 5h 34% · weekly 5%
4
4
  // Also writes the rate_limits Claude Code sends into .claude/state/usage.json (usage.mjs reads it).
5
5
  import fs from 'node:fs';
6
6
  import { readStatus } from './status-read.mjs';
@@ -49,12 +49,12 @@ try {
49
49
  parts.push(t(root, 'statusline.notSetUp'));
50
50
  } else {
51
51
  const a = d.fields;
52
- // progress.md says on / off (açık / kapalı before 3.48); only what the line shows is localized
52
+ // progress.md says on / off; only what the line shows is localized
53
53
  const mode = a['Question mode'] ?? 'on';
54
54
  const question = mode
55
55
  .replace(/^\S+/, (m) => (isValue(m, 'on') ? t(root, 'statusline.modeOpen') : isValue(m, 'off') ? t(root, 'statusline.modeOff') : m))
56
- .replace(/\s*·\s*(?:kapalı konular|closed topics):\s*(?:yok|none)\s*$/i, '')
57
- .replace(/\s*·\s*(?:kapalı konular|closed topics):\s*(.*)$/i, (_, topics) => ` ${t(root, 'statusline.closed', { topics })}`);
56
+ .replace(/\s*·\s*closed topics:\s*none\s*$/i, '')
57
+ .replace(/\s*·\s*closed topics:\s*(.*)$/i, (_, topics) => ` ${t(root, 'statusline.closed', { topics })}`);
58
58
  parts.push(`▶ ${a['Active phase'] ?? '-'}`);
59
59
  if (isValue(a['Active phase'], 'setup')) parts.push(t(root, 'statusline.step', { step: a['Setup step'] ?? '-' }));
60
60
  else parts.push(a['Active task'] ?? '-', t(root, 'statusline.task', { done: d.taskCount.done, total: d.taskCount.total }));
@@ -50,13 +50,9 @@ function readJson(p) {
50
50
  }
51
51
  }
52
52
 
53
- // Package names before 3.48; they still work as input
54
- const OLD_NAMES = { tasarim: 'design', guvenlik: 'security', yayin: 'release' };
55
- export const packageName = (name) => (Object.hasOwn(OLD_NAMES, name ?? '') ? OLD_NAMES[name] : name);
56
-
57
53
  function pkg(name) {
58
- if (!Object.hasOwn(PACKAGES, packageName(name ?? ''))) throw new Error(`Unknown package: ${name}. Options: ${Object.keys(PACKAGES).join(', ')}`);
59
- return PACKAGES[packageName(name)];
54
+ if (!Object.hasOwn(PACKAGES, name ?? '')) throw new Error(`Unknown package: ${name}. Options: ${Object.keys(PACKAGES).join(', ')}`);
55
+ return PACKAGES[name];
60
56
  }
61
57
 
62
58
  export function packageStatus(root) {
@@ -149,7 +145,7 @@ const NOTE = 'After enabling, run /reload-plugins. Enabling mid-session breaks t
149
145
  if (isMain(import.meta.url)) {
150
146
  const root = findRoot();
151
147
  const [flag, typed] = process.argv.slice(2);
152
- const name = packageName(typed);
148
+ const name = typed;
153
149
  try {
154
150
  if (flag === '--enable' || flag === '--disable') {
155
151
  const ids = setPackage(root, name, flag === '--enable');
@@ -7,7 +7,7 @@
7
7
  import fs from 'node:fs';
8
8
  import path from 'node:path';
9
9
  import { findRoot, isMain } from './shared.mjs';
10
- import { t, readLang } from './lang.mjs';
10
+ import { t, readLang, locale } from './lang.mjs';
11
11
 
12
12
  export const THRESHOLDS = { five_hour: { yellow: 60, red: 85 }, seven_day: { yellow: 70, red: 90 } };
13
13
  // Window → message key of its name
@@ -68,7 +68,7 @@ export function level(data) {
68
68
  function timeLeft(resetsAt, now, root) {
69
69
  if (!resetsAt) return '';
70
70
  const min = Math.max(0, Math.round((resetsAt * 1000 - now) / 60000));
71
- const clock = new Date(resetsAt * 1000).toLocaleString(readLang(root) === 'tr' ? 'tr-TR' : 'en-GB', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' });
71
+ const clock = new Date(resetsAt * 1000).toLocaleString(locale(readLang(root)), { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' });
72
72
  const left = min >= 1440
73
73
  ? t(root, 'usage.leftDays', { days: Math.floor(min / 1440), hours: Math.floor((min % 1440) / 60) })
74
74
  : t(root, 'usage.leftHours', { hours: Math.floor(min / 60), minutes: min % 60 });