@haposoft/cafekit 0.8.11 → 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.11",
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,10 +59,12 @@ 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,
67
66
  'dev-rules-reminder': true,
67
+ 'skill-router': true,
68
68
  'usage': true,
69
69
  'context-tracking': true,
70
70
  'scout-block': true,
@@ -80,8 +80,8 @@ const DEFAULT_CONFIG = {
80
80
  * Arrays are replaced entirely (not concatenated) to avoid duplicate entries
81
81
  *
82
82
  * IMPORTANT: Empty objects {} are treated as "inherit from parent", not "replace with empty".
83
- * This allows global config to set hooks.foo: false and have it persist even when
84
- * 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.
85
85
  *
86
86
  * @param {Object} target - Base object
87
87
  * @param {Object} source - Object to merge (takes precedence)
@@ -463,36 +463,39 @@ function sanitizeConfig(config, projectRoot) {
463
463
  }
464
464
 
465
465
  /**
466
- * Load config with cascading resolution: DEFAULT → global → local
466
+ * Load config with cascading resolution: DEFAULT → runtime
467
467
  *
468
468
  * Resolution order (each layer overrides the previous):
469
469
  * 1. DEFAULT_CONFIG (hardcoded defaults)
470
- * 2. Global config (~/.claude/.ck.json) - user preferences
471
- * 3. Local config (./.claude/.ck.json) - project-specific overrides
470
+ * 2. Runtime config (./.claude/runtime.json) - installed CafeKit runtime config
472
471
  *
473
472
  * @param {Object} options - Options for config loading
474
473
  * @param {boolean} options.includeProject - Include project section (default: true)
475
474
  * @param {boolean} options.includeAssertions - Include assertions (default: true)
476
475
  * @param {boolean} options.includeLocale - Include locale section (default: true)
476
+ * @param {string} options.cwd - Project root for local config lookup (default: process.cwd())
477
477
  */
478
478
  function loadConfig(options = {}) {
479
- const { includeProject = true, includeAssertions = true, includeLocale = true } = options;
480
- 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;
481
486
 
482
- // Load configs from both locations
483
- const globalConfig = loadConfigFromPath(GLOBAL_CONFIG_PATH);
484
- 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));
485
489
 
486
490
  // No config files found - use defaults
487
- if (!globalConfig && !localConfig) {
491
+ if (!runtimeConfig) {
488
492
  return getDefaultConfig(includeProject, includeAssertions, includeLocale);
489
493
  }
490
494
 
491
495
  try {
492
- // Deep merge: DEFAULT → global → local (local wins)
496
+ // Deep merge: DEFAULT → runtime (runtime wins)
493
497
  let merged = deepMerge({}, DEFAULT_CONFIG);
494
- if (globalConfig) merged = deepMerge(merged, globalConfig);
495
- if (localConfig) merged = deepMerge(merged, localConfig);
498
+ if (runtimeConfig) merged = deepMerge(merged, runtimeConfig);
496
499
 
497
500
  // Build result with optional sections
498
501
  const result = {
@@ -522,6 +525,7 @@ function loadConfig(options = {}) {
522
525
  result.hooks = merged.hooks || DEFAULT_CONFIG.hooks;
523
526
  // Statusline mode
524
527
  result.statusline = merged.statusline || 'full';
528
+ result.statuslineColors = merged.statuslineColors !== false;
525
529
 
526
530
  return sanitizeConfig(result, projectRoot);
527
531
  } catch (e) {
@@ -540,7 +544,8 @@ function getDefaultConfig(includeProject = true, includeAssertions = true, inclu
540
544
  codingLevel: -1, // Default: disabled (no injection, saves tokens)
541
545
  skills: { ...DEFAULT_CONFIG.skills },
542
546
  hooks: { ...DEFAULT_CONFIG.hooks },
543
- statusline: 'full'
547
+ statusline: 'full',
548
+ statuslineColors: true
544
549
  };
545
550
  if (includeLocale) {
546
551
  result.locale = { ...DEFAULT_CONFIG.locale };
@@ -789,8 +794,13 @@ function extractTaskListId(resolved) {
789
794
  * @param {string} hookName - Hook name (script basename without .cjs)
790
795
  * @returns {boolean} Whether hook is enabled
791
796
  */
792
- function isHookEnabled(hookName) {
793
- 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
+ });
794
804
  const hooks = config.hooks || {};
795
805
  // Return true if undefined (default enabled), otherwise return the boolean value
796
806
  return hooks[hookName] !== false;
@@ -798,8 +808,7 @@ function isHookEnabled(hookName) {
798
808
 
799
809
  module.exports = {
800
810
  CONFIG_PATH,
801
- LOCAL_CONFIG_PATH,
802
- GLOBAL_CONFIG_PATH,
811
+ RUNTIME_CONFIG_PATH,
803
812
  DEFAULT_CONFIG,
804
813
  INVALID_FILENAME_CHARS,
805
814
  deepMerge,
@@ -0,0 +1,190 @@
1
+ const MIN_SCORE = 4;
2
+ const WEIGHTS = { strong: 6, medium: 3, weak: 1, negative: -5 };
3
+
4
+ const ROUTES = [
5
+ route('hapo:hotfix', 'urgent fix or production regression', 100, {
6
+ strong: ['hotfix', 'fix bug', 'fix lỗi', 'fix loi', 'sửa lỗi', 'sua loi', 'production bug', 'prod bug', 'regression', 'lỗi production', 'loi production', 'sửa gấp', 'sua gap', 'バグ修正', '不具合修正', '本番障害', '本番バグ', '緊急修正', '至急修正', 'リグレッション'],
7
+ medium: ['khẩn cấp', 'khan cap', 'production', 'critical bug', 'rollback', '修正して', '直して', '緊急', '本番', '重大バグ', 'ロールバック', '障害対応'],
8
+ weak: ['fix', 'fixing', 'sửa', 'sua', 'urgent', 'emergency', 'release is broken', '修正', '直す', '至急', 'リリースが壊れた'],
9
+ negative: ['slide', 'pptx', 'spec', 'brainstorm'],
10
+ }),
11
+ route('hapo:debug', 'bug investigation or failure diagnosis', 90, {
12
+ strong: ['debug', 'root cause', 'diagnose', 'stack trace', 'không chạy', 'khong chay', 'デバッグ', '原因調査', '根本原因', 'スタックトレース', '動かない'],
13
+ medium: ['bug', 'error', 'exception', 'failing', 'failed', 'failure', 'broken', 'lỗi', 'loi', 'tại sao', 'tai sao', 'vì sao', 'vi sao', 'nguyên nhân', 'nguyen nhan', 'ci fail', 'build fail', 'バグ', 'エラー', '例外', '失敗', '壊れている', 'なぜ', '原因', 'ci失敗', 'ビルド失敗'],
14
+ weak: ['sai', 'fail', 'issue', 'problem', '問題', '不具合'],
15
+ negative: ['commit', 'push', 'slide', 'pptx'],
16
+ }),
17
+ route('hapo:specs', 'specification, requirements, design, tasks, or spec validation', 80, {
18
+ strong: ['spec', 'specs', 'requirements', 'acceptance criteria', 'task breakdown', 'đặc tả', 'dac ta', '仕様', '仕様書', '要件', '受け入れ条件', 'タスク分解', '仕様を作って', '仕様を作成'],
19
+ medium: ['requirement', 'ears', 'design doc', 'scope', '--validate', 'yêu cầu', 'yeu cau', 'phạm vi', 'pham vi', 'validate spec', 'kiểm tra spec', 'kiem tra spec', '要求', '設計書', 'スコープ', '検証', '仕様を確認'],
20
+ weak: ['tính năng mới', 'tinh nang moi', 'feature idea', 'user story', 'criteria', 'task list', '新機能', 'ユーザーストーリー', '基準', 'タスクリスト'],
21
+ negative: ['commit', 'push', 'bug', 'error', 'production', 'pptx', 'pdf'],
22
+ }),
23
+ route('hapo:develop', 'implementation from an approved spec or task list', 75, {
24
+ strong: ['develop', 'implement', 'implementation', 'theo spec', 'theo specs', 'approved spec', 'làm theo spec', 'lam theo spec', '実装', '開発', '仕様に沿って', '仕様どおり', '承認済み仕様'],
25
+ medium: ['build this', 'code this', 'start task', 'run task', 'thực hiện task', 'thuc hien task', 'phát triển', 'phat trien', 'bắt đầu implement', 'bat dau implement', 'vào code', 'vao code', '作って', 'コードを書いて', 'タスクを開始', 'タスクを実行', '開発して', '実装して'],
26
+ weak: ['triển khai', 'trien khai', 'đưa vào code', 'dua vao code', 'code feature', 'コード化', '機能を作る'],
27
+ negative: ['bug', 'debug', 'review', 'test only', 'commit'],
28
+ }),
29
+ route('hapo:test', 'test, verification, QA, or runtime validation', 70, {
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
+ weak: ['assert', 'runtime proof', 'manual qa', 'end to end', 'smoke test', 'アサート', 'スモークテスト'],
33
+ negative: ['spec', 'requirements', 'commit', 'push'],
34
+ }),
35
+ route('hapo:code-review', 'code review, audit, security, or quality assessment', 68, {
36
+ strong: ['code review', 'security review', 'performance review', 'đánh giá code', 'danh gia code', 'コードレビュー', 'セキュリティレビュー', '性能レビュー'],
37
+ medium: ['review', 'audit', 'quality', 'đánh giá', 'danh gia', 'kiểm tra chất lượng', 'kiem tra chat luong', 'レビュー', '監査', '品質', '品質確認'],
38
+ weak: ['risk', 'maintainability', 'readability', 'vulnerability', 'lỗ hổng', 'lo hong', 'リスク', '保守性', '可読性', '脆弱性'],
39
+ negative: ['slide', 'pptx', 'commit and push'],
40
+ }),
41
+ route('hapo:git', 'git, commit, push, branch, tag, or pull request workflow', 65, {
42
+ strong: ['commit', 'push', 'pull request', 'git', 'đẩy lên', 'day len', 'コミット', 'プッシュ', 'プルリク', 'リリースして'],
43
+ medium: ['pr ', 'branch', 'tag', 'release', 'publish', 'merge', 'rebase', 'ブランチ', 'タグ', 'リリース', '公開', 'マージ', 'リベース'],
44
+ weak: ['origin', 'remote', 'checkout', 'stash', 'version bump', 'リモート', 'チェックアウト', 'スタッシュ', 'バージョン更新'],
45
+ negative: ['deploy', 'test', 'review only'],
46
+ }),
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', '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
+ weak: ['scan', 'inspect code', 'explore code', 'xem qua', 'xem giúp', '調べて', '確認して'],
51
+ negative: ['bug', 'error', 'lỗi', 'loi', 'fail', 'failure', 'production', 'hotfix', 'fix', 'sửa', 'sua', 'debug', 'develop', 'implement', 'test', 'commit', 'push', 'slide', 'pptx'],
52
+ }),
53
+ route('hapo:impact-analysis', 'impact analysis before changing existing behavior', 64, {
54
+ strong: ['impact analysis', 'blast radius', 'ảnh hưởng', 'anh huong', 'tác động', 'tac dong', '影響分析', '影響範囲', '影響', '副作用'],
55
+ medium: ['impact', 'liên quan những đâu', 'lien quan nhung dau', 'affected files', 'dependency impact', '関連箇所', '影響ファイル', '依存関係'],
56
+ weak: ['before changing', 'risk area', 'downstream', 'upstream', 'side effect', '変更前', 'リスク範囲', '下流', '上流'],
57
+ negative: ['slide', 'pptx'],
58
+ }),
59
+ route('hapo:frontend-design', 'UI/UX design, visual style, layout, or color system', 62, {
60
+ strong: ['ui design', 'visual style', 'color system', 'thiết kế giao diện', 'thiet ke giao dien', 'màu sắc', 'mau sac', 'uiデザイン', '画面デザイン', 'ビジュアルスタイル', '配色'],
61
+ medium: ['ux', 'layout', 'style', 'theme', 'responsive design', 'giao diện', 'giao dien', 'wireframe', 'レイアウト', 'スタイル', 'テーマ', 'レスポンシブデザイン', '画面', 'ワイヤーフレーム', '色'],
62
+ weak: ['palette', 'typography', 'spacing', 'polish ui', 'mockup', 'prototype', 'パレット', 'タイポグラフィ', '余白', 'モックアップ', 'プロトタイプ'],
63
+ negative: ['backend', 'api', 'database'],
64
+ }),
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', '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
+ weak: ['react', 'next.js', 'memo', 'rerender', 're-render', 'render performance', 'component performance', 'waterfalls', 'lazy state', 'dynamic import', 'react pattern', 'next.js pattern', 'レンダー性能', 'コンポーネント性能', '動的インポート'],
69
+ negative: ['backend', 'api', 'database', 'slide', 'pptx', 'commit', 'push'],
70
+ }),
71
+ route('hapo:frontend-development', 'frontend implementation work', 58, {
72
+ strong: ['react', 'next.js', 'vite', 'frontend', 'tailwind', 'web app', 'フロントエンド', 'webアプリ'],
73
+ medium: ['component', 'css', 'html', 'browser ui', 'client side', 'giao diện react', 'コンポーネント', 'ブラウザui', 'クライアント側', 'react画面'],
74
+ weak: ['state management', 'hook', 'form', 'table', 'dashboard ui', '状態管理', 'フック', 'フォーム', 'テーブル', 'ダッシュボードui'],
75
+ negative: ['best practices', 'performance', 'optimize', 'optimization', 'rerender', 're-render', 'tối ưu', 'toi uu', '再レンダー', '最適化', '性能', 'backend only', 'database only', 'slide', 'pptx'],
76
+ }),
77
+ route('hapo:backend-development', 'backend, API, database, or service implementation', 58, {
78
+ strong: ['backend', 'api', 'database', 'endpoint', 'server', 'service', 'バックエンド', 'データベース', 'エンドポイント', 'サーバー', 'サービス'],
79
+ medium: ['db', 'sql', 'postgres', 'mysql', 'migration', 'schema', 'worker', 'queue', 'マイグレーション', 'スキーマ', 'ワーカー', 'キュー'],
80
+ weak: ['controller', 'route handler', 'repository', 'model', 'auth service', 'コントローラ', 'ルートハンドラ', 'リポジトリ', 'モデル', '認証サービス'],
81
+ negative: ['frontend only', 'slide', 'pptx'],
82
+ }),
83
+ route('hapo:mobile-development', 'mobile app implementation', 56, {
84
+ strong: ['mobile', 'ios', 'android', 'react native', 'flutter', 'モバイル'],
85
+ medium: ['app store', 'play store', 'native app', 'mobile screen', 'アプリストア', 'playストア', 'ネイティブアプリ', 'モバイル画面'],
86
+ weak: ['gesture', 'push notification', 'offline sync', 'ジェスチャー', 'プッシュ通知', 'オフライン同期'],
87
+ negative: ['web only', 'desktop only'],
88
+ }),
89
+ route('hapo:devops', 'deployment, infrastructure, CI/CD, or operations work', 54, {
90
+ strong: ['deploy', 'deployment', 'docker', 'kubernetes', 'ci/cd', 'github actions', 'デプロイ', 'デプロイメント'],
91
+ medium: ['vercel', 'infra', 'infrastructure', 'devops', 'pipeline', 'environment variable', 'インフラ', 'パイプライン', '環境変数'],
92
+ weak: ['build server', 'container', 'helm', 'terraform', 'monitoring', 'ビルドサーバー', 'コンテナ', '監視'],
93
+ negative: ['slide', 'pptx', 'spec only'],
94
+ }),
95
+ route('hapo:generate-graph', 'diagram, graph, architecture map, or flow visualization', 52, {
96
+ strong: ['diagram', 'graph', 'mermaid', 'flowchart', 'architecture diagram', 'sơ đồ', 'so do', '図', '図解', 'ダイアグラム', 'グラフ', 'フローチャート', '構成図'],
97
+ medium: ['biểu đồ', 'bieu do', 'visualize', 'sequence diagram', 'data flow', '可視化', 'シーケンス図', 'データフロー'],
98
+ weak: ['mind map', 'dependency map', 'system map', 'マインドマップ', '依存関係図', 'システム図'],
99
+ negative: ['pptx', 'slide deck'],
100
+ }),
101
+ route('hapo:brainstorm', 'early ideation or unclear solution direction', 50, {
102
+ strong: ['brainstorm', 'ý tưởng', 'y tuong', 'phương án', 'phuong an', 'gợi ý', 'goi y', 'ブレスト', 'アイデア', '案', '提案して', '相談'],
103
+ medium: ['idea', 'ideas', 'approach', 'options', 'tradeoff', 'chủ đề', 'chu de', 'cần làm gì', 'can lam gi', 'アプローチ', '選択肢', 'トレードオフ', 'テーマ', '何をすれば'],
104
+ weak: ['proposal', 'strategy', 'plan options', 'explore', 'direction', 'seminar topic', '提案', '戦略', '方向性', '検討'],
105
+ negative: ['commit', 'push', 'bug', 'error'],
106
+ }),
107
+ route('hapo:research', 'technical research or best-practice lookup', 48, {
108
+ strong: ['research', 'best practice', 'tìm hiểu', 'tim hieu', 'nghiên cứu', 'nghien cuu', '調査', 'リサーチ', 'ベストプラクティス'],
109
+ medium: ['documentation', 'docs', 'compare tools', 'latest docs', 'official docs', 'ドキュメント', '比較', '最新ドキュメント', '公式ドキュメント'],
110
+ weak: ['investigate options', 'market scan', 'reference', 'source material', '選択肢を調べる', '参考資料', '資料'],
111
+ negative: ['commit', 'push'],
112
+ }),
113
+ route('hapo:pptx', 'presentation or PowerPoint work', 46, {
114
+ strong: ['pptx', 'powerpoint', 'slide deck', 'presentation deck', 'スライド資料', 'プレゼン資料'],
115
+ medium: ['slide', 'slides', 'deck', 'presentation', 'seminar slides', 'スライド', 'プレゼン', 'セミナー資料'],
116
+ weak: ['speaker notes', 'appendix', 'mục lục slide', 'muc luc slide', '発表ノート', '付録', '目次'],
117
+ negative: ['source code', 'api', 'database'],
118
+ }),
119
+ route('hapo:agent-browser', 'browser automation with snapshot refs, web interaction, recording, or Browserbase cloud browser workflows', 45, {
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', 'サイトを開く', 'ブラウザで開く', 'ブラウザで', 'フォーム入力', 'フォーム入力を自動化', '自動化して', 'クリック操作', '録画'],
122
+ weak: ['click button', 'fill form', 'open site', 'web session', 'browser ref', 'viewport', 'cookies', 'localstorage', 'ボタンをクリック', 'ビューポート', 'クッキー'],
123
+ negative: ['attached screenshot', 'ảnh đính kèm', 'anh dinh kem', '画像添付', 'source code', 'codebase', 'commit', 'push', 'pptx', 'pdf'],
124
+ }),
125
+ route('hapo:pdf', 'PDF reading, extraction, or generation', 44, {
126
+ strong: ['pdf'], medium: ['export pdf', 'read pdf', 'extract pdf', 'pdf出力', 'pdfを読む', 'pdf抽出'], weak: ['page render', 'ページレンダー'], negative: [],
127
+ }),
128
+ route('hapo:docx', 'Word document work', 42, {
129
+ strong: ['docx', 'word document', 'word file', 'word文書', 'wordファイル'], medium: ['word'], weak: ['tracked changes', 'document file', '変更履歴', '文書ファイル'], negative: [],
130
+ }),
131
+ route('hapo:xlsx', 'spreadsheet or Excel work', 42, {
132
+ strong: ['xlsx', 'excel', 'spreadsheet', 'スプレッドシート'], medium: ['csv', 'sheet', 'workbook', 'シート', 'ワークブック'], weak: ['formula', 'pivot table', '数式', 'ピボットテーブル'], negative: [],
133
+ }),
134
+ route('hapo:ai-multimodal', 'image, video, audio, or multimodal artifact analysis', 40, {
135
+ strong: ['screenshot', 'video', 'audio', 'multimodal', 'ảnh', 'anh', 'hình', 'hinh', 'スクリーンショット', '動画', '音声', '画像', '写真'],
136
+ medium: ['image', 'screen capture', 'recording', 'file attached', 'イメージ', '画面キャプチャ', '録画', '添付ファイル'],
137
+ weak: ['visual', 'describe image', 'ocr', 'ビジュアル', '画像説明'],
138
+ negative: ['pptx', 'pdf export'],
139
+ }),
140
+ ];
141
+
142
+ function route(skill, reason, priority, signals) {
143
+ return { skill, reason, priority, signals };
144
+ }
145
+
146
+ function normalize(value) {
147
+ return String(value || '')
148
+ .normalize('NFKC')
149
+ .normalize('NFD')
150
+ .replace(/[\u0300-\u036f]/g, '')
151
+ .normalize('NFC')
152
+ .toLowerCase();
153
+ }
154
+
155
+ function scoreRoute(prompt, routeItem) {
156
+ const normalized = normalize(prompt);
157
+ const matched = [];
158
+ let score = 0;
159
+ for (const [bucket, weight] of Object.entries(WEIGHTS)) {
160
+ for (const term of routeItem.signals[bucket] || []) {
161
+ if (!normalized.includes(normalize(term))) continue;
162
+ score += weight;
163
+ matched.push({ bucket, term, weight });
164
+ }
165
+ }
166
+ return { ...routeItem, score, matched, confidence: confidence(score) };
167
+ }
168
+
169
+ function confidence(score) {
170
+ if (score >= 12) return 'high';
171
+ if (score >= 7) return 'medium';
172
+ if (score >= MIN_SCORE) return 'low';
173
+ return 'none';
174
+ }
175
+
176
+ function findRoute(prompt) {
177
+ const candidates = ROUTES
178
+ .map((routeItem, index) => ({ ...scoreRoute(prompt, routeItem), index }))
179
+ .filter((routeItem) => routeItem.score >= MIN_SCORE)
180
+ .sort((a, b) => b.score - a.score || b.priority - a.priority || a.index - b.index);
181
+ return candidates[0] || null;
182
+ }
183
+
184
+ module.exports = {
185
+ MIN_SCORE,
186
+ ROUTES,
187
+ findRoute,
188
+ normalize,
189
+ scoreRoute,
190
+ };
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Copyright (c) 2026 Haposoft. MIT License.
4
+ *
5
+ * UserPromptSubmit Hook - skill-router.cjs
6
+ *
7
+ * Adds a deterministic CafeKit skill suggestion for natural-language prompts.
8
+ * It never overrides explicit slash commands; it only injects a short routing hint.
9
+ *
10
+ * Exit: 0 always (fail-open)
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+
16
+ function logCrash(error) {
17
+ try {
18
+ const dir = path.join(__dirname, '.logs');
19
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
20
+ fs.appendFileSync(
21
+ path.join(dir, 'hook-log.jsonl'),
22
+ JSON.stringify({
23
+ ts: new Date().toISOString(),
24
+ hook: 'skill-router',
25
+ status: 'crash',
26
+ error: error.message,
27
+ }) + '\n'
28
+ );
29
+ } catch (_) {}
30
+ }
31
+
32
+ try {
33
+ const { isHookEnabled } = require('./lib/config.cjs');
34
+ const { findRoute } = require('./lib/skill-router-routes.cjs');
35
+
36
+ function isExplicitCommand(prompt) {
37
+ const trimmed = prompt.trim();
38
+ return trimmed.startsWith('/') || /^hapo:[a-z-]+/i.test(trimmed);
39
+ }
40
+
41
+ const stdin = fs.readFileSync(0, 'utf8').trim();
42
+ if (!stdin) process.exit(0);
43
+
44
+ const payload = JSON.parse(stdin);
45
+ const cwd = payload.cwd || process.cwd();
46
+ const prompt = payload.prompt || '';
47
+ if (!isHookEnabled('skill-router', { cwd })) process.exit(0);
48
+ if (!prompt || isExplicitCommand(prompt)) process.exit(0);
49
+
50
+ const route = findRoute(prompt);
51
+ if (!route) process.exit(0);
52
+
53
+ const skillDir = route.skill.replace(/^hapo:/, '');
54
+ const lines = [
55
+ '## CafeKit Skill Router',
56
+ `- Suggested skill: \`${route.skill}\``,
57
+ `- Why: ${route.reason}.`,
58
+ `- Confidence: ${route.confidence} (score ${route.score}).`,
59
+ `- Action: before acting, read \`.claude/skills/${skillDir}/SKILL.md\` and follow that workflow.`,
60
+ '- If the user explicitly names another workflow or asks for direct answering only, follow the user request.',
61
+ ];
62
+
63
+ console.log(lines.join('\n'));
64
+ process.exit(0);
65
+ } catch (error) {
66
+ try { logCrash(error); } catch (_) {}
67
+ process.exit(0);
68
+ }
@@ -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";
@@ -78,6 +78,7 @@
78
78
  "hooks/privacy-block.cjs",
79
79
  "hooks/inspect-block.cjs",
80
80
  "hooks/rules.cjs",
81
+ "hooks/skill-router.cjs",
81
82
  "hooks/spec-state.cjs",
82
83
  "hooks/state.cjs",
83
84
  "hooks/lib/color.cjs",
@@ -86,7 +87,8 @@
86
87
  "hooks/lib/git.cjs",
87
88
  "hooks/lib/config.cjs",
88
89
  "hooks/lib/context.cjs",
89
- "hooks/lib/detect.cjs"
90
+ "hooks/lib/detect.cjs",
91
+ "hooks/lib/skill-router-routes.cjs"
90
92
  ]
91
93
  },
92
94
  "settings": {
@@ -42,6 +42,10 @@
42
42
  "type": "command",
43
43
  "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/rules.cjs\""
44
44
  },
45
+ {
46
+ "type": "command",
47
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/skill-router.cjs\""
48
+ },
45
49
  {
46
50
  "type": "command",
47
51
  "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/spec-state.cjs\""
@@ -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