tillerhand 3.27.0 → 3.27.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.
@@ -8,8 +8,8 @@
8
8
  import fs from 'node:fs';
9
9
  import path from 'node:path';
10
10
  import { execFileSync, spawnSync } from 'node:child_process';
11
- import { pathToFileURL } from 'node:url';
12
11
  import { isTestFile } from './test-guard.mjs';
12
+ import { isMain } from './shared.mjs';
13
13
 
14
14
  export const BUDGET = { complexity: 15, function: 80, file: 400 };
15
15
  const LIZARD = ['pipx', 'run', 'lizard==1.24.1'];
@@ -58,7 +58,7 @@ function check() {
58
58
  console.log(`Karmaşıklık bütçesi uygun: ${files.length} dosya.`);
59
59
  }
60
60
 
61
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
61
+ if (isMain(import.meta.url)) {
62
62
  if (process.argv.includes('--check')) check();
63
63
  else { console.error('Kullanım: node .claude/hooks/budget.mjs --check [--base <commit>]'); process.exit(1); }
64
64
  }
@@ -6,7 +6,7 @@
6
6
  // Rollback does not delete history: unsaved work is stashed first, files are set to the target state and
7
7
  // a NEW commit is made. So a rollback can be undone too. reset --hard is not used.
8
8
  import { execFileSync } from 'node:child_process';
9
- import { pathToFileURL } from 'node:url';
9
+ import { isMain } from './shared.mjs';
10
10
 
11
11
  // Tag prefix in users' repos: it stays as it is, existing tags must keep matching
12
12
  const TAG_PREFIX = 'calisan/';
@@ -47,7 +47,7 @@ export function restore(root, target) {
47
47
  return { changed: true, stashed, target: short, newCommit: git(root, 'rev-parse', '--short', 'HEAD') };
48
48
  }
49
49
 
50
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
50
+ if (isMain(import.meta.url)) {
51
51
  const root = process.env.CLAUDE_PROJECT_DIR || process.cwd();
52
52
  const arg = process.argv.slice(2);
53
53
  // ONLY the first argument sets the mode: the permission list matches by prefix ("--list:*", "--save:*"),
@@ -8,8 +8,7 @@ import fs from 'node:fs';
8
8
  import path from 'node:path';
9
9
  import crypto from 'node:crypto';
10
10
  import { execSync } from 'node:child_process';
11
- import { fileURLToPath } from 'node:url';
12
- import { findRoot } from './shared.mjs';
11
+ import { findRoot, isMain } from './shared.mjs';
13
12
  import { unquote } from './destructive-guard.mjs';
14
13
  import { approvedCommands, commandList } from './gate.mjs'; // circular import: used at run time only
15
14
 
@@ -161,7 +160,7 @@ function hook() {
161
160
  process.exit(2);
162
161
  }
163
162
 
164
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
163
+ if (isMain(import.meta.url)) {
165
164
  const i = process.argv.indexOf('--approve');
166
165
  if (i > -1) approve(process.argv[i + 1] || 'belirtilmedi');
167
166
  else hook();
@@ -4,8 +4,8 @@
4
4
  // node .claude/hooks/council.mjs docs/workflow/council/<tutanak>.md
5
5
  // The tally is done in code: a model cannot add up the ranking wrong, the chair cannot change the count.
6
6
  import fs from 'node:fs';
7
- import { pathToFileURL } from 'node:url';
8
7
  import { sections, findSection } from './status-read.mjs';
8
+ import { isMain } from './shared.mjs';
9
9
 
10
10
  const VOTE = /^\s*[-*]?\s*([A-Z]\d+)\s*[:=]\s*(DOĞRU|YANLIŞ|ABARTILI)(?!\p{L})/gimu; // \b does not work on Turkish letters (Ş)
11
11
 
@@ -99,7 +99,7 @@ export function writeMinutes(minutes, s) {
99
99
  return `${minutes.trimEnd()}\n\n${section}`;
100
100
  }
101
101
 
102
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
102
+ if (isMain(import.meta.url)) {
103
103
  const file = process.argv[2];
104
104
  if (!file || !fs.existsSync(file)) { console.error('Kullanım: node .claude/hooks/council.mjs <tutanak.md>'); process.exit(1); }
105
105
  const text = fs.readFileSync(file, 'utf8');
@@ -7,8 +7,7 @@
7
7
  import fs from 'node:fs';
8
8
  import os from 'node:os';
9
9
  import path from 'node:path';
10
- import { pathToFileURL } from 'node:url';
11
- import { readInput, decide } from './shared.mjs';
10
+ import { readInput, decide, isMain } from './shared.mjs';
12
11
  import { isTestFile, isTestConfig } from './test-guard.mjs';
13
12
  import { readLock, readPlan, allowedInLock, inScope, realPath, relPath, protectedRule } from './freeze-guard.mjs';
14
13
 
@@ -319,7 +318,7 @@ function evaluateOne(command) {
319
318
  return null;
320
319
  }
321
320
 
322
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
321
+ if (isMain(import.meta.url)) {
323
322
  const g = readInput();
324
323
  // Of the lists, the stricter wins (deny > ask)
325
324
  const results = g ? [evaluate(g.command), g.tool !== 'claude' ? permissionEquivalent(g.command) : null, scopeIssue(g.command, g.dir, g.root)].filter(Boolean) : [];
@@ -6,8 +6,7 @@
6
6
  // State: .claude/state/edit-rounds.json → { session, counts: { file: n }, pending: [note] }; reset when the session changes.
7
7
  import fs from 'node:fs';
8
8
  import path from 'node:path';
9
- import { pathToFileURL } from 'node:url';
10
- import { readInput, findRoot } from './shared.mjs';
9
+ import { readInput, findRoot, isMain } from './shared.mjs';
11
10
 
12
11
  export const ROUND = 5;
13
12
  const CODE = /\.(m?[jt]sx?|cjs|py|go|rb|php|java|kt|swift|rs|sql|vue|svelte)$/i;
@@ -66,6 +65,6 @@ function inject() {
66
65
  process.exit(0);
67
66
  }
68
67
 
69
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
68
+ if (isMain(import.meta.url)) {
70
69
  process.argv.includes('--inject') ? inject() : hook();
71
70
  }
@@ -16,8 +16,7 @@
16
16
  // node .claude/hooks/freeze-guard.mjs --plan-end close when the task is done
17
17
  import fs from 'node:fs';
18
18
  import path from 'node:path';
19
- import { pathToFileURL } from 'node:url';
20
- import { readInput, decide, findRoot } from './shared.mjs';
19
+ import { readInput, decide, findRoot, isMain } from './shared.mjs';
21
20
 
22
21
  // Project root even when called from a subfolder: lock and plan are written where the hook reads them; paths are root-relative
23
22
  const root = findRoot();
@@ -157,7 +156,7 @@ function hook() {
157
156
  process.exit(0);
158
157
  }
159
158
 
160
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
159
+ if (isMain(import.meta.url)) {
161
160
  const [mode, value] = process.argv.slice(2);
162
161
  // A mistyped or removed flag must fail loudly: run as the hook it would pass silently and the AI would think the plan is saved
163
162
  if (mode?.startsWith('--') && !MODES.includes(mode)) {
@@ -14,10 +14,9 @@ import os from 'node:os';
14
14
  import path from 'node:path';
15
15
  import crypto from 'node:crypto';
16
16
  import { spawnSync } from 'node:child_process';
17
- import { pathToFileURL } from 'node:url';
18
17
  import { changes } from './commit-gate.mjs';
19
18
  import { isCodeFile } from './edit-rounds.mjs';
20
- import { findRoot } from './shared.mjs';
19
+ import { findRoot, isMain } from './shared.mjs';
21
20
  import { specInvariants } from './dashboard-data.mjs';
22
21
 
23
22
  // Keys of the AGENTS.md → Komutlar block (Turkish template text, matched as written)
@@ -139,7 +138,7 @@ export function runGate(root) {
139
138
  return { passed, text: [`## Kapı · ${files.length} dosya değişti`, ...checks.map(line), result].join('\n') };
140
139
  }
141
140
 
142
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
141
+ if (isMain(import.meta.url)) {
143
142
  const arg = process.argv[2];
144
143
  if (arg === '--approve') { console.log(approve(findRoot())); process.exit(0); }
145
144
  if (arg !== '--run') { console.error('Kullanım: node .claude/hooks/gate.mjs --run | --approve'); process.exit(1); }
@@ -5,8 +5,7 @@
5
5
  // 2) Git pre-commit: `node .claude/hooks/guard-secrets.mjs --staged` scans the staged changes.
6
6
  // High-confidence patterns only; no generic "password=" search, to avoid false alarms.
7
7
  import { execSync } from 'node:child_process';
8
- import { pathToFileURL } from 'node:url';
9
- import { readInput, decide } from './shared.mjs';
8
+ import { readInput, decide, isMain } from './shared.mjs';
10
9
 
11
10
  export const PATTERNS = [
12
11
  ['Anthropic/OpenAI anahtarı', /\bsk-(?:ant-|proj-)?[A-Za-z0-9_-]{32,}/],
@@ -59,6 +58,6 @@ function stagedMode() {
59
58
  }
60
59
 
61
60
  // Run only when called directly, not when tests import this file
62
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
61
+ if (isMain(import.meta.url)) {
63
62
  process.argv.includes('--staged') ? stagedMode() : hookMode();
64
63
  }
@@ -4,8 +4,8 @@
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
- import { pathToFileURL } from 'node:url';
8
- import { readInput, decide } from './shared.mjs';
7
+
8
+ import { readInput, decide, isMain } from './shared.mjs';
9
9
 
10
10
  export const THRESHOLD = { newDays: 30, npmWeeklyDownloads: 500 };
11
11
 
@@ -128,4 +128,4 @@ async function hook() {
128
128
  process.exit(0);
129
129
  }
130
130
 
131
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) hook();
131
+ if (isMain(import.meta.url)) hook();
@@ -9,7 +9,7 @@
9
9
  import fs from 'node:fs';
10
10
  import path from 'node:path';
11
11
  import { execFileSync } from 'node:child_process';
12
- import { pathToFileURL } from 'node:url';
12
+ import { isMain } from './shared.mjs';
13
13
 
14
14
  // Windows paths are case-insensitive; other systems are case-sensitive
15
15
  const norm = (p) => (process.platform === 'win32' ? path.resolve(p).toLowerCase() : path.resolve(p));
@@ -33,7 +33,7 @@ export function disabledHere(wanted, installed, root) {
33
33
  .map(([id]) => id);
34
34
  }
35
35
 
36
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
36
+ if (isMain(import.meta.url)) {
37
37
  const root = process.env.CLAUDE_PROJECT_DIR || process.cwd();
38
38
  // No shell: arguments go straight to claude (claude is a single-file executable, .exe on Windows)
39
39
  const run = (...arg) => execFileSync('claude', arg, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 180000 });
@@ -9,9 +9,8 @@ import fs from 'node:fs';
9
9
  import os from 'node:os';
10
10
  import path from 'node:path';
11
11
  import { spawn, spawnSync } from 'node:child_process';
12
- import { pathToFileURL } from 'node:url';
13
12
  import { approvedCommands, approvalNeeded, isApproved } from './gate.mjs';
14
- import { findRoot } from './shared.mjs';
13
+ import { findRoot, isMain } from './shared.mjs';
15
14
 
16
15
  const CLI = 'npx -y @playwright/cli@0.1.22';
17
16
  const SIZES = [['desktop', 1440, 900], ['mobile', 390, 844]];
@@ -120,7 +119,7 @@ export async function preview(root, task) {
120
119
  }
121
120
  }
122
121
 
123
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
122
+ if (isMain(import.meta.url)) {
124
123
  const { code, text } = await preview(findRoot(), process.argv[2]);
125
124
  console.log(text);
126
125
  process.exit(code);
@@ -7,9 +7,8 @@
7
7
  // Exit code: 0 nothing failed, 1 a criterion failed, 2 nothing to report (no JUnit file, no PRD criterion, no test ran).
8
8
  import fs from 'node:fs';
9
9
  import path from 'node:path';
10
- import { pathToFileURL } from 'node:url';
11
10
  import { prdCriteria, junitCases } from './dashboard-data.mjs';
12
- import { findRoot } from './shared.mjs';
11
+ import { findRoot, isMain } from './shared.mjs';
13
12
 
14
13
  // A pipe would split the table cell (the dashboard's table reader does not know \|)
15
14
  const cell = (s) => String(s ?? '').split(/\r?\n/)[0].trim().slice(0, 160).replace(/\|/g, '/');
@@ -62,7 +61,7 @@ export function qaReport(root, junit = 'test-results-qa/junit.xml', now = new Da
62
61
  return { code: count('kaldı') ? 1 : 0, text: `QA: ${summary} → ${file}${stale}` };
63
62
  }
64
63
 
65
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
64
+ if (isMain(import.meta.url)) {
66
65
  const { code, text } = qaReport(findRoot(), process.argv[2]);
67
66
  console.log(text);
68
67
  process.exit(code);
@@ -11,6 +11,17 @@ import fs from 'node:fs';
11
11
  import path from 'node:path';
12
12
  import { fileURLToPath } from 'node:url';
13
13
 
14
+ // Was this file started as the program (not imported)? Compared as real paths: a project opened through a symlink or a
15
+ // junction (macOS /tmp, linked folders, npx's node_modules/.bin) starts the script by its linked path, while
16
+ // import.meta.url is the target. A plain comparison would skip the guard silently there.
17
+ export function isMain(metaUrl) {
18
+ try {
19
+ return Boolean(process.argv[1]) && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(metaUrl));
20
+ } catch {
21
+ return false;
22
+ }
23
+ }
24
+
14
25
  const AG_WRITE = ['write_to_file', 'replace_file_content', 'multi_replace_file_content'];
15
26
 
16
27
  // Claude Code passes CLAUDE_PROJECT_DIR to hooks; Codex does not. Codex has no "ask", so a misdetection stays on the safe side.
@@ -7,8 +7,7 @@
7
7
  import fs from 'node:fs';
8
8
  import path from 'node:path';
9
9
  import { execFileSync } from 'node:child_process';
10
- import { pathToFileURL } from 'node:url';
11
- import { readInput, decide } from './shared.mjs';
10
+ import { readInput, decide, isMain } from './shared.mjs';
12
11
 
13
12
  const root = process.env.CLAUDE_PROJECT_DIR || process.cwd();
14
13
 
@@ -80,6 +79,6 @@ function checkAll() {
80
79
  process.exit(1);
81
80
  }
82
81
 
83
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
82
+ if (isMain(import.meta.url)) {
84
83
  process.argv.includes('--check') ? checkAll() : hook();
85
84
  }
@@ -8,8 +8,7 @@
8
8
  // It does not block: the user approves a justified change.
9
9
  import fs from 'node:fs';
10
10
  import path from 'node:path';
11
- import { pathToFileURL } from 'node:url';
12
- import { readInput, decide } from './shared.mjs';
11
+ import { readInput, decide, isMain } from './shared.mjs';
13
12
 
14
13
  const TEST = /(^|\/)(tests?|__tests__|e2e|spec)\/|\.(test|spec|e2e-spec)\.[cm]?[jt]sx?$|(^|\/)test_[^/]*\.py$|_test\.(py|go)$/i;
15
14
  export const isTestFile = (file) => TEST.test(file.replace(/\\/g, '/'));
@@ -77,4 +76,4 @@ function hook() {
77
76
  process.exit(0);
78
77
  }
79
78
 
80
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) hook();
79
+ if (isMain(import.meta.url)) hook();
@@ -11,8 +11,8 @@
11
11
  import fs from 'node:fs';
12
12
  import os from 'node:os';
13
13
  import path from 'node:path';
14
- import { pathToFileURL } from 'node:url';
15
14
  import { missingPlugins } from './plugins-sync.mjs';
15
+ import { isMain } from './shared.mjs';
16
16
 
17
17
  // Keys are the package names users type (/tools ac tasarim): they stay Turkish
18
18
  export const PACKAGES = {
@@ -142,7 +142,7 @@ const tokenOf = (measurement, ids) => {
142
142
  return found.length ? `~${found.reduce((s, o) => s + o.skillTokens + o.agentTokens, 0)} token` : '? token (kurulu değil)';
143
143
  };
144
144
  const NOTE = 'Açtıktan sonra /reload-plugins. Oturum ortasında açmak prompt önbelleğini bozar; en iyisi faz sınırında, /clear ile.';
145
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
145
+ if (isMain(import.meta.url)) {
146
146
  const root = process.env.CLAUDE_PROJECT_DIR || process.cwd();
147
147
  const [flag, name] = process.argv.slice(2);
148
148
  try {
@@ -6,7 +6,7 @@
6
6
  // node .claude/hooks/usage.mjs --json machine-readable
7
7
  import fs from 'node:fs';
8
8
  import path from 'node:path';
9
- import { fileURLToPath } from 'node:url';
9
+ import { isMain } from './shared.mjs';
10
10
 
11
11
  export const THRESHOLDS = { five_hour: { yellow: 60, red: 85 }, seven_day: { yellow: 70, red: 90 } };
12
12
  const WINDOWS = { five_hour: '5 saatlik', seven_day: 'haftalık' };
@@ -82,7 +82,7 @@ export function summary(data, now = Date.now()) {
82
82
  return `${parts.join(' · ')} · seviye: ${LEVEL_LABEL[level(data)]} · ${data.ageMin} dk önce okundu${stale}`;
83
83
  }
84
84
 
85
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
85
+ if (isMain(import.meta.url)) {
86
86
  const root = process.env.CLAUDE_PROJECT_DIR || process.cwd();
87
87
  const data = readLimits(root);
88
88
  if (process.argv.includes('--json')) console.log(JSON.stringify({ ...data, level: level(data), thresholds: THRESHOLDS }));
package/kit/CHANGELOG.md CHANGED
@@ -3,6 +3,13 @@
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.27.1] - 2026-10-25
7
+
8
+ ### Düzeltilen
9
+ - **Koruma hook'ları, proje sembolik bağlantı veya junction üzerinden açılınca sessizce çalışmıyordu.** Sır engeli, yıkıcı komut kalkanı, commit kapısı, yapı ve test korumaları dahil 17 betik, başlatıldıkları yolu gerçek yolla karşılaştırırken bağlantıyı çözmüyordu; macOS'ta `/tmp` gibi bağlantılı klasörlerde veya bağlantılı proje klasörlerinde hiçbir şey yapmadan çıkıyorlardı. Artık iki yol da gerçek yola çevrilip karşılaştırılıyor (`shared.mjs` → `isMain`).
10
+ - **`npx tillerhand init` Linux ve macOS'ta hiçbir şey yapmıyordu** (aynı neden: npx kiti `node_modules/.bin` bağlantısıyla başlatıyor). Windows'ta çalışıyordu. 3.27.0'ı Linux veya macOS'ta denediysen tekrar çalıştır.
11
+ - Elle uyarla: yok; `/kit-update` hook'ları yeniler.
12
+
6
13
  ## [3.27.0] - 2026-10-25
7
14
 
8
15
  ### Yeni
package/kit/VERSION CHANGED
@@ -1 +1 @@
1
- 3.27.0
1
+ 3.27.1
package/kit/kit.mjs CHANGED
@@ -474,7 +474,9 @@ async function askTools() {
474
474
  return c.split(',').map((s) => TOOLS[Number(s.trim()) - 1] ?? s.trim().toLowerCase()).filter(Boolean);
475
475
  }
476
476
 
477
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
477
+ // Real paths: npx starts the CLI through a link (node_modules/.bin/tillerhand), while import.meta.url is the target
478
+ const realArg = () => { try { return fs.realpathSync(process.argv[1]); } catch { return ''; } };
479
+ if (process.argv[1] && realArg() === fs.realpathSync(fileURLToPath(import.meta.url))) {
478
480
  const [command, ...rest] = process.argv.slice(2);
479
481
  try {
480
482
  const a = parseArgs(rest);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tillerhand",
3
- "version": "3.27.0",
3
+ "version": "3.27.1",
4
4
  "description": "Claude Code, Codex ve Antigravity için Türkçe vibe coding kiti: kurallar, komutlar, korumalar ve proje şablonları.",
5
5
  "license": "MIT",
6
6
  "type": "module",