@haposoft/cafekit 0.8.12 → 0.8.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haposoft/cafekit",
3
- "version": "0.8.12",
3
+ "version": "0.8.13",
4
4
  "description": "Claude Code-first spec-driven workflow for AI coding assistants. Bundles CafeKit hapo: skills, runtime hooks, agents, and installer scaffolding.",
5
5
  "author": "Haposoft <nghialt@haposoft.com>",
6
6
  "license": "MIT",
@@ -15,13 +15,15 @@ try {
15
15
  const fs = require('fs');
16
16
  const path = require('path');
17
17
  const { execSync } = require('child_process');
18
+ const { loadConfig } = require('./lib/config.cjs');
18
19
 
19
20
  // Đọc stdin theo chuẩn hook
20
21
  const stdin = fs.readFileSync(0, 'utf8').trim();
21
22
  const payload = stdin ? JSON.parse(stdin) : {};
22
23
  const cwd = payload.cwd || process.cwd();
24
+ const config = loadConfig({ cwd, includeProject: false, includeAssertions: false, includeLocale: false });
23
25
 
24
- const docsDir = path.join(cwd, 'docs');
26
+ const docsDir = path.join(cwd, config.paths?.docs || 'docs');
25
27
 
26
28
  // Xác định dự án đã có cốt lõi code hay chưa?
27
29
  const hasCode = fs.existsSync(path.join(cwd, 'src')) ||
@@ -16,14 +16,20 @@ const YELLOW = '\x1b[33m';
16
16
  const MAGENTA = '\x1b[35m';
17
17
  const CYAN = '\x1b[36m';
18
18
 
19
- // Detect color support at module load (cached)
20
- // Claude Code statusline runs via pipe but output displays in TTY - default to true
21
- const shouldUseColor = (() => {
19
+ function detectColorDefault() {
22
20
  if (process.env.NO_COLOR) return false;
23
21
  if (process.env.FORCE_COLOR) return true;
24
22
  // Default true for statusline context (Claude Code handles TTY display)
25
23
  return true;
26
- })();
24
+ }
25
+
26
+ // Detect color support at module load (cached)
27
+ // Claude Code statusline runs via pipe but output displays in TTY - default to true
28
+ let shouldUseColor = detectColorDefault();
29
+
30
+ function setColorEnabled(enabled) {
31
+ shouldUseColor = enabled === false ? false : detectColorDefault();
32
+ }
27
33
 
28
34
  // Detect 256-color support via COLORTERM
29
35
  const has256Color = (() => {
@@ -90,6 +96,7 @@ module.exports = {
90
96
  dim,
91
97
  getContextColor,
92
98
  coloredBar,
93
- shouldUseColor,
99
+ get shouldUseColor() { return shouldUseColor; },
100
+ setColorEnabled,
94
101
  has256Color
95
102
  };
@@ -9,11 +9,9 @@ const fs = require('fs');
9
9
  const path = require('path');
10
10
  const os = require('os');
11
11
 
12
- const LOCAL_CONFIG_PATH = '.claude/.ck.json';
13
- const GLOBAL_CONFIG_PATH = path.join(os.homedir(), '.claude', '.ck.json');
12
+ const RUNTIME_CONFIG_PATH = '.claude/runtime.json';
14
13
 
15
- // Legacy export for backward compatibility
16
- const CONFIG_PATH = LOCAL_CONFIG_PATH;
14
+ const CONFIG_PATH = RUNTIME_CONFIG_PATH;
17
15
 
18
16
  const DEFAULT_CONFIG = {
19
17
  plan: {
@@ -61,6 +59,7 @@ const DEFAULT_CONFIG = {
61
59
  },
62
60
  assertions: [],
63
61
  statusline: 'full',
62
+ statuslineColors: true,
64
63
  hooks: {
65
64
  'session-init': true,
66
65
  'subagent-init': true,
@@ -81,8 +80,8 @@ const DEFAULT_CONFIG = {
81
80
  * Arrays are replaced entirely (not concatenated) to avoid duplicate entries
82
81
  *
83
82
  * IMPORTANT: Empty objects {} are treated as "inherit from parent", not "replace with empty".
84
- * This allows global config to set hooks.foo: false and have it persist even when
85
- * local config has hooks: {} (empty = inherit, not reset to defaults).
83
+ * This allows runtime config to leave hooks as {} (inherit defaults)
84
+ * without resetting all default hook toggles.
86
85
  *
87
86
  * @param {Object} target - Base object
88
87
  * @param {Object} source - Object to merge (takes precedence)
@@ -464,36 +463,39 @@ function sanitizeConfig(config, projectRoot) {
464
463
  }
465
464
 
466
465
  /**
467
- * Load config with cascading resolution: DEFAULT → global → local
466
+ * Load config with cascading resolution: DEFAULT → runtime
468
467
  *
469
468
  * Resolution order (each layer overrides the previous):
470
469
  * 1. DEFAULT_CONFIG (hardcoded defaults)
471
- * 2. Global config (~/.claude/.ck.json) - user preferences
472
- * 3. Local config (./.claude/.ck.json) - project-specific overrides
470
+ * 2. Runtime config (./.claude/runtime.json) - installed CafeKit runtime config
473
471
  *
474
472
  * @param {Object} options - Options for config loading
475
473
  * @param {boolean} options.includeProject - Include project section (default: true)
476
474
  * @param {boolean} options.includeAssertions - Include assertions (default: true)
477
475
  * @param {boolean} options.includeLocale - Include locale section (default: true)
476
+ * @param {string} options.cwd - Project root for local config lookup (default: process.cwd())
478
477
  */
479
478
  function loadConfig(options = {}) {
480
- const { includeProject = true, includeAssertions = true, includeLocale = true } = options;
481
- const projectRoot = process.cwd();
479
+ const {
480
+ includeProject = true,
481
+ includeAssertions = true,
482
+ includeLocale = true,
483
+ cwd = process.cwd()
484
+ } = options;
485
+ const projectRoot = cwd;
482
486
 
483
- // Load configs from both locations
484
- const globalConfig = loadConfigFromPath(GLOBAL_CONFIG_PATH);
485
- const localConfig = loadConfigFromPath(LOCAL_CONFIG_PATH);
487
+ // Load config from the installed CafeKit runtime config.
488
+ const runtimeConfig = loadConfigFromPath(path.join(projectRoot, RUNTIME_CONFIG_PATH));
486
489
 
487
490
  // No config files found - use defaults
488
- if (!globalConfig && !localConfig) {
491
+ if (!runtimeConfig) {
489
492
  return getDefaultConfig(includeProject, includeAssertions, includeLocale);
490
493
  }
491
494
 
492
495
  try {
493
- // Deep merge: DEFAULT → global → local (local wins)
496
+ // Deep merge: DEFAULT → runtime (runtime wins)
494
497
  let merged = deepMerge({}, DEFAULT_CONFIG);
495
- if (globalConfig) merged = deepMerge(merged, globalConfig);
496
- if (localConfig) merged = deepMerge(merged, localConfig);
498
+ if (runtimeConfig) merged = deepMerge(merged, runtimeConfig);
497
499
 
498
500
  // Build result with optional sections
499
501
  const result = {
@@ -523,6 +525,7 @@ function loadConfig(options = {}) {
523
525
  result.hooks = merged.hooks || DEFAULT_CONFIG.hooks;
524
526
  // Statusline mode
525
527
  result.statusline = merged.statusline || 'full';
528
+ result.statuslineColors = merged.statuslineColors !== false;
526
529
 
527
530
  return sanitizeConfig(result, projectRoot);
528
531
  } catch (e) {
@@ -541,7 +544,8 @@ function getDefaultConfig(includeProject = true, includeAssertions = true, inclu
541
544
  codingLevel: -1, // Default: disabled (no injection, saves tokens)
542
545
  skills: { ...DEFAULT_CONFIG.skills },
543
546
  hooks: { ...DEFAULT_CONFIG.hooks },
544
- statusline: 'full'
547
+ statusline: 'full',
548
+ statuslineColors: true
545
549
  };
546
550
  if (includeLocale) {
547
551
  result.locale = { ...DEFAULT_CONFIG.locale };
@@ -790,8 +794,13 @@ function extractTaskListId(resolved) {
790
794
  * @param {string} hookName - Hook name (script basename without .cjs)
791
795
  * @returns {boolean} Whether hook is enabled
792
796
  */
793
- function isHookEnabled(hookName) {
794
- const config = loadConfig({ includeProject: false, includeAssertions: false, includeLocale: false });
797
+ function isHookEnabled(hookName, options = {}) {
798
+ const config = loadConfig({
799
+ includeProject: false,
800
+ includeAssertions: false,
801
+ includeLocale: false,
802
+ cwd: options.cwd
803
+ });
795
804
  const hooks = config.hooks || {};
796
805
  // Return true if undefined (default enabled), otherwise return the boolean value
797
806
  return hooks[hookName] !== false;
@@ -799,8 +808,7 @@ function isHookEnabled(hookName) {
799
808
 
800
809
  module.exports = {
801
810
  CONFIG_PATH,
802
- LOCAL_CONFIG_PATH,
803
- GLOBAL_CONFIG_PATH,
811
+ RUNTIME_CONFIG_PATH,
804
812
  DEFAULT_CONFIG,
805
813
  INVALID_FILENAME_CHARS,
806
814
  deepMerge,
@@ -27,8 +27,8 @@ const ROUTES = [
27
27
  negative: ['bug', 'debug', 'review', 'test only', 'commit'],
28
28
  }),
29
29
  route('hapo:test', 'test, verification, QA, or runtime validation', 70, {
30
- strong: ['unit test', 'integration test', 'e2e', 'playwright', 'coverage', 'kiểm thử', 'kiem thu', '単体テスト', '結合テスト', 'カバレッジ', 'テストして'],
31
- medium: ['test', 'tests', 'testing', 'qa', 'verify', 'verification', 'kiểm tra chạy', 'kiem tra chay', 'xác minh', 'xac minh', 'テスト', '検証', '確認', '動作確認'],
30
+ strong: ['unit test', 'integration test', 'e2e', 'playwright', 'coverage', 'test all', 'run all tests', 'test toàn bộ', 'test toan bo', 'kiểm thử', 'kiem thu', 'kiểm tra toàn bộ', 'kiem tra toan bo', '単体テスト', '結合テスト', '全体テスト', 'カバレッジ', 'テストして'],
31
+ medium: ['test', 'tests', 'testing', 'qa', 'verify', 'verification', 'kiểm tra chạy', 'kiem tra chay', 'xác minh', 'xac minh', 'テスト', '検証', '確認', '動作確認', '全体確認'],
32
32
  weak: ['assert', 'runtime proof', 'manual qa', 'end to end', 'smoke test', 'アサート', 'スモークテスト'],
33
33
  negative: ['spec', 'requirements', 'commit', 'push'],
34
34
  }),
@@ -45,8 +45,8 @@ const ROUTES = [
45
45
  negative: ['deploy', 'test', 'review only'],
46
46
  }),
47
47
  route('hapo:inspect', 'codebase discovery, file search, structure scan, or locating implementation areas', 64, {
48
- strong: ['inspect', 'codebase scan', 'scan codebase', 'scan source', 'file discovery', 'find files', 'locate files', 'xem source', 'xem codebase', 'quét source', 'quet source', 'quét codebase', 'quet codebase', 'kiểm tra cấu trúc', 'kiem tra cau truc', 'コード構造', 'ソース確認', 'コードベース確認', 'ファイル探索', '構造を確認'],
49
- medium: ['search files', 'find where', 'where is', 'project structure', 'code structure', 'repo structure', 'tìm file', 'tim file', 'tìm trong source', 'tim trong source', 'cấu trúc project', 'cau truc project', 'ở đâu', 'o dau', '関連ファイル', 'どこにある', 'プロジェクト構造', 'リポジトリ構造', '探して'],
48
+ strong: ['inspect', 'codebase scan', 'scan codebase', 'scan source', 'file discovery', 'find files', 'locate files', 'xem source', 'xem codebase', 'kiểm tra source', 'kiem tra source', 'kiểm tra source code', 'kiem tra source code', 'quét source', 'quet source', 'quét codebase', 'quet codebase', 'kiểm tra cấu trúc', 'kiem tra cau truc', 'コード構造', 'ソース確認', 'コードベース確認', 'ファイル探索', '構造を確認'],
49
+ medium: ['search files', 'find where', 'where is', 'project structure', 'code structure', 'repo structure', 'source code', 'tìm file', 'tim file', 'tìm trong source', 'tim trong source', 'cấu trúc project', 'cau truc project', 'ở đâu', 'o dau', 'nằm đâu', 'nam dau', '関連ファイル', 'どこにある', 'プロジェクト構造', 'リポジトリ構造', '探して'],
50
50
  weak: ['scan', 'inspect code', 'explore code', 'xem qua', 'xem giúp', '調べて', '確認して'],
51
51
  negative: ['bug', 'error', 'lỗi', 'loi', 'fail', 'failure', 'production', 'hotfix', 'fix', 'sửa', 'sua', 'debug', 'develop', 'implement', 'test', 'commit', 'push', 'slide', 'pptx'],
52
52
  }),
@@ -63,8 +63,8 @@ const ROUTES = [
63
63
  negative: ['backend', 'api', 'database'],
64
64
  }),
65
65
  route('hapo:react-best-practices', 'React and Next.js performance patterns, rerender optimization, and Vercel best practices', 60, {
66
- strong: ['react best practices', 'next.js best practices', 'vercel react best practices', 'react performance', 'next.js performance', 'optimize react', 'optimize next.js', 'tối ưu react', 'toi uu react', 'tối ưu next.js', 'toi uu next.js', 'reactベストプラクティス', 'next.jsベストプラクティス', 'react最適化', 'next.js最適化', 'react性能'],
67
- medium: ['bundle optimization', 'bundle size', 'rerender optimization', 're-render optimization', 'data fetching', 'server component', 'client component', 'suspense', 'hydration', 'waterfall', 'usememo', 'usecallback', 'react cache', 'tối ưu rerender', 'toi uu rerender', 'tối ưu bundle', 'toi uu bundle', 'バンドル最適化', '再レンダー最適化', 'データ取得', 'サーバーコンポーネント', 'クライアントコンポーネント', 'ハイドレーション'],
66
+ strong: ['react best practices', 'next.js best practices', 'vercel react best practices', 'react performance', 'next.js performance', 'optimize react', 'optimize next.js', 'rerender nhiều', 'rerender nhieu', 'nhiều rerender', 'nhieu rerender', 'tối ưu react', 'toi uu react', 'tối ưu next.js', 'toi uu next.js', 'reactベストプラクティス', 'next.jsベストプラクティス', 'react最適化', 'next.js最適化', 'react性能'],
67
+ medium: ['bundle optimization', 'bundle size', 'rerender optimization', 're-render optimization', 'data fetching', 'server component', 'client component', 'suspense', 'hydration', 'waterfall', 'usememo', 'usecallback', 'react cache', 'optimize', 'optimization', 'tối ưu', 'toi uu', 'tối ưu rerender', 'toi uu rerender', 'tối ưu bundle', 'toi uu bundle', 'バンドル最適化', '再レンダー最適化', '最適化', 'データ取得', 'サーバーコンポーネント', 'クライアントコンポーネント', 'ハイドレーション'],
68
68
  weak: ['react', 'next.js', 'memo', 'rerender', 're-render', 'render performance', 'component performance', 'waterfalls', 'lazy state', 'dynamic import', 'react pattern', 'next.js pattern', 'レンダー性能', 'コンポーネント性能', '動的インポート'],
69
69
  negative: ['backend', 'api', 'database', 'slide', 'pptx', 'commit', 'push'],
70
70
  }),
@@ -118,7 +118,7 @@ const ROUTES = [
118
118
  }),
119
119
  route('hapo:agent-browser', 'browser automation with snapshot refs, web interaction, recording, or Browserbase cloud browser workflows', 45, {
120
120
  strong: ['agent-browser', 'browser automation', 'web automation', 'browserbase', 'cloud browser', 'snapshot refs', 'browser snapshot', 'automate browser', 'tự động trình duyệt', 'tu dong trinh duyet', 'tự động thao tác trình duyệt', 'tu dong thao tac trinh duyet', 'ブラウザ自動化', 'クラウドブラウザ', 'ブラウザ操作', 'ブラウザスナップショット'],
121
- medium: ['open url', 'navigate site', 'click in browser', 'fill form in browser', 'record browser', 'browser session', 'multi tab', 'browser test session', 'mở website', 'mo website', 'truy cập website', 'truy cap website', 'click trên web', 'click tren web', 'điền form web', 'dien form web', 'サイトを開く', 'ブラウザで開く', 'フォーム入力', 'クリック操作', '録画'],
121
+ medium: ['open url', 'navigate site', 'click in browser', 'fill form in browser', 'record browser', 'browser session', 'multi tab', 'browser test session', 'mở website', 'mo website', 'truy cập website', 'truy cap website', 'click trên web', 'click tren web', 'điền form web', 'dien form web', 'サイトを開く', 'ブラウザで開く', 'ブラウザで', 'フォーム入力', 'フォーム入力を自動化', '自動化して', 'クリック操作', '録画'],
122
122
  weak: ['click button', 'fill form', 'open site', 'web session', 'browser ref', 'viewport', 'cookies', 'localstorage', 'ボタンをクリック', 'ビューポート', 'クッキー'],
123
123
  negative: ['attached screenshot', 'ảnh đính kèm', 'anh dinh kem', '画像添付', 'source code', 'codebase', 'commit', 'push', 'pptx', 'pdf'],
124
124
  }),
@@ -38,13 +38,13 @@ try {
38
38
  return trimmed.startsWith('/') || /^hapo:[a-z-]+/i.test(trimmed);
39
39
  }
40
40
 
41
- if (!isHookEnabled('skill-router')) process.exit(0);
42
-
43
41
  const stdin = fs.readFileSync(0, 'utf8').trim();
44
42
  if (!stdin) process.exit(0);
45
43
 
46
44
  const payload = JSON.parse(stdin);
45
+ const cwd = payload.cwd || process.cwd();
47
46
  const prompt = payload.prompt || '';
47
+ if (!isHookEnabled('skill-router', { cwd })) process.exit(0);
48
48
  if (!prompt || isExplicitCommand(prompt)) process.exit(0);
49
49
 
50
50
  const route = findRoute(prompt);
@@ -21,14 +21,15 @@ try {
21
21
  const os = require("os");
22
22
  const { execSync } = require("child_process");
23
23
 
24
- // Check if usage tracking is disabled in runtime.json
25
- try {
26
- const runtimePath = path.join(process.cwd(), '.claude', 'runtime.json');
27
- if (fs.existsSync(runtimePath)) {
28
- const runtime = JSON.parse(fs.readFileSync(runtimePath, 'utf-8'));
29
- if (runtime.usage?.enabled === false) process.exit(0);
30
- }
31
- } catch { /* fail-open */ }
24
+ function readRuntime(cwd) {
25
+ try {
26
+ const runtimePath = path.join(cwd, '.claude', 'runtime.json');
27
+ if (fs.existsSync(runtimePath)) {
28
+ return JSON.parse(fs.readFileSync(runtimePath, 'utf-8'));
29
+ }
30
+ } catch { /* fail-open */ }
31
+ return {};
32
+ }
32
33
 
33
34
  // Cache configuration
34
35
  const USAGE_CACHE_FILE = path.join(os.tmpdir(), "ck-usage-limits-cache.json");
@@ -151,6 +152,12 @@ async function main() {
151
152
  } catch {}
152
153
 
153
154
  const input = JSON.parse(inputStr || "{}");
155
+ const cwd = input.cwd || process.cwd();
156
+ const runtime = readRuntime(cwd);
157
+ if (runtime.usage?.enabled === false) {
158
+ console.log(JSON.stringify(result));
159
+ return;
160
+ }
154
161
 
155
162
  // Detect hook type
156
163
  const isUserPrompt = typeof input.prompt === "string";
@@ -35,7 +35,7 @@ export GEMINI_API_KEY_2="key2" # auto-rotates on rate limit
35
35
 
36
36
  **Verify setup**: `python scripts/check_setup.py`
37
37
  **Analyze media**: `python scripts/gemini_batch_process.py --files <file> --task <analyze|transcribe|extract>`
38
- - TIP: When you're asked to analyze an image, check if `gemini` command is available, then use `echo "<prompt to analyze image>" | gemini -y -m <gemini.model>` command (read model from `$HOME/packages/spec/src/claude/.ck.json`: `gemini.model`). If `gemini` command is not available, use `python scripts/gemini_batch_process.py --files <file> --task analyze` command.
38
+ - TIP: When you're asked to analyze an image, check if `gemini` command is available, then use `echo "<prompt to analyze image>" | gemini -y -m <gemini.model>` command (read model from `.claude/runtime.json`: `gemini.model`). If `gemini` command is not available, use `python scripts/gemini_batch_process.py --files <file> --task analyze` command.
39
39
 
40
40
  > **Stdin support**: Pipe files via stdin for Gemini analysis (auto-detects PNG/JPG/PDF/WAV/MP3).
41
41
 
@@ -14,7 +14,8 @@ const fs = require('fs');
14
14
  const path = require('path');
15
15
 
16
16
  // Import modular components
17
- const { green, yellow, red, cyan, magenta, dim, coloredBar, RESET, shouldUseColor } = require('./hooks/lib/color.cjs');
17
+ const colors = require('./hooks/lib/color.cjs');
18
+ const { green, yellow, red, cyan, magenta, dim, coloredBar, RESET } = colors;
18
19
  const { parseTranscript } = require('./hooks/lib/parser.cjs');
19
20
  const { countConfigs } = require('./hooks/lib/counter.cjs');
20
21
  const { loadConfig } = require('./hooks/lib/config.cjs');
@@ -358,7 +359,7 @@ function render(ctx, singleLineMode = false) {
358
359
 
359
360
  // Output all lines with non-breaking spaces for alignment
360
361
  for (const line of lines) {
361
- const outputLine = shouldUseColor ? `${RESET}${line.replace(/ /g, '\u00A0')}` : line;
362
+ const outputLine = colors.shouldUseColor ? `${RESET}${line.replace(/ /g, '\u00A0')}` : line;
362
363
  console.log(outputLine);
363
364
  }
364
365
  }
@@ -509,6 +510,7 @@ async function main() {
509
510
  // Load config and get statusline mode
510
511
  const config = loadConfig({ includeProject: false, includeAssertions: false, includeLocale: false });
511
512
  const statuslineMode = config.statusline || 'full';
513
+ colors.setColorEnabled(config.statuslineColors !== false);
512
514
 
513
515
  // Render based on mode
514
516
  switch (statuslineMode) {