@haposoft/cafekit 0.3.12 → 0.4.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.
package/README.md CHANGED
@@ -43,6 +43,44 @@ CafeKit is a **multi-platform** CLI tool that installs a structured workflow for
43
43
  - **📦 No global install** - Use directly with `npx`
44
44
  - **🚀 Future-proof** - Easy to add support for new AI editors
45
45
 
46
+ ## Claude Code Statusline (Claude Code Only)
47
+
48
+ CafeKit automatically installs an enhanced statusline for Claude Code that provides real-time session context.
49
+
50
+ **What it shows:**
51
+ - **Context usage** - Percentage and token count (e.g., `23% 45K/200K`)
52
+ - **Session timer** - Elapsed time since session start
53
+ - **Git status** - Current branch and dirty state indicator
54
+ - **Active agents** - Count of running subagents
55
+ - **Todo items** - Count of pending tasks
56
+
57
+ **Installation:**
58
+ - Automatically installed when running `npx @haposoft/cafekit` in a Claude Code project
59
+ - Merges with existing `settings.json` configuration without overwriting user settings
60
+ - Safe to re-run - preserves non-CafeKit statusline configurations
61
+ - Upgrade mode (`--upgrade`) refreshes managed runtime files
62
+
63
+ **Configuration:**
64
+ The statusline is configured via `.claude/settings.json`:
65
+ ```json
66
+ {
67
+ "statusLine": {
68
+ "type": "command",
69
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/status.cjs\"",
70
+ "padding": 0
71
+ }
72
+ }
73
+ ```
74
+
75
+ **Runtime files installed:**
76
+ - `.claude/status.cjs` - Main statusline script
77
+ - `.claude/hooks/session.cjs` - Session initialization hook
78
+ - `.claude/hooks/agent.cjs` - Subagent context injection hook
79
+ - `.claude/hooks/usage.cjs` - Usage tracking hook
80
+ - `.claude/hooks/lib/*.cjs` - Shared utilities (color, parser, git, config, etc.)
81
+
82
+ **Note:** This feature is Claude Code exclusive and not available for Antigravity.
83
+
46
84
  ## Installation
47
85
 
48
86
  ### Prerequisites
@@ -63,7 +101,8 @@ The installer will:
63
101
  2. **Prompt** you to select platform if not detected
64
102
  3. **Copy** workflow commands to the appropriate directory
65
103
  4. **Install** shared skills for spec-driven development
66
- 5. **Ensure dependencies** for `code - test - review` by installing missing command/agent templates
104
+ 5. **[Claude Code only]** Install unprefixed skill directories (`spec-init`, `spec-requirements`, `spec-design`, `spec-tasks`, `code`, `test`, `review`) that expose `hapo:`-prefixed skill names
105
+ 6. **Ensure dependencies** for `code - test - review` by installing missing command/agent templates
67
106
 
68
107
  Installer modes:
69
108
  - **Default install mode**: `npx @haposoft/cafekit` (skip existing files)
@@ -72,33 +111,40 @@ Installer modes:
72
111
 
73
112
  **Example output (Claude Code):**
74
113
  ```
75
- CafeKit Spec Installer
76
- ===============================
77
-
78
- Detected platforms: claude
79
-
80
- Installing for: .claude/
81
- ------------------------
82
- [.claude/skills] Installed skill: spec-driven-development
83
- [.claude/commands] Copied: spec-init.md
84
- [.claude/commands] Copied: spec-requirements.md
85
- [.claude/commands] Copied: spec-design.md
86
- [.claude/commands] Copied: spec-tasks.md
87
- [.claude/commands] Copied: code.md
88
- [.claude/commands] Copied: spec-status.md
89
-
90
- Installation complete!
91
- Copied Files: 7
92
- Skipped Files: 0
93
- Installed Skills: Yes
94
- Dependency Checks: 6
95
- Installed Deps: 6
96
- Missing Deps: 0
97
- Targets: .claude/commands
114
+ CafeKit Installer v0.3.12
115
+ ========================================
116
+
117
+ Installing for: Claude Code
118
+ Mode: install (skip existing files)
119
+
120
+ Claude Code (.claude/)
121
+ ----------------------------------------
122
+ Skill installed: specs
123
+ Skill installed: spec-init
124
+ Skill installed: spec-requirements
125
+ Skill installed: spec-design
126
+ Skill installed: spec-tasks
127
+ Skill installed: code
128
+ ✓ Skill installed: test
129
+ Skill installed: review
130
+ Copied: spec-init.md
131
+ Copied: spec-requirements.md
132
+ ...
133
+
134
+ ╔════════════════════════════════════════════════════════╗
135
+ ║ Installation Complete! ║
136
+ ╚════════════════════════════════════════════════════════╝
137
+
138
+ Installed Skills: Yes ✓
98
139
 
99
140
  Next steps:
100
- 1. Run /spec-init <feature-name>
101
- 2. Follow the spec workflow: requirements - design - tasks - code - test - review
141
+ 1. Start your AI editor
142
+
143
+ For Claude Code:
144
+ Run: /spec-init <feature-name>
145
+ Or use skill: /hapo:spec-init <feature-description>
146
+
147
+ 2. Follow the workflow: requirements - design - tasks - code - test - review
102
148
 
103
149
  Documentation: https://github.com/haposoft/cafekit
104
150
  ```
@@ -565,7 +611,15 @@ User clicks -> dispatch action -> update context -> localStorage -> re-render
565
611
  │ ├── spec-status.md
566
612
  │ └── docs.md # Docs workflows
567
613
  └── skills/
568
- └── spec-driven-development/
614
+ ├── specs/
615
+ ├── impact-analysis/
616
+ ├── spec-init/
617
+ ├── spec-requirements/
618
+ ├── spec-design/
619
+ ├── spec-tasks/
620
+ ├── code/
621
+ ├── test/
622
+ └── review/
569
623
  ```
570
624
 
571
625
  **Antigravity** (`.agent/`):
@@ -583,7 +637,8 @@ User clicks -> dispatch action -> update context -> localStorage -> re-render
583
637
  │ ├── docs-init.md # Docs workflows
584
638
  │ └── docs-update.md
585
639
  ├── skills/
586
- └── spec-driven-development/
640
+ ├── specs/
641
+ │ └── impact-analysis/
587
642
  └── rules/
588
643
  └── GEMINI.md # System rules (always_on)
589
644
  ```
package/bin/install.js CHANGED
@@ -19,6 +19,13 @@ const path = require('path');
19
19
  const readline = require('readline');
20
20
  const packageJson = require('../package.json');
21
21
 
22
+ function validateManifestV2(manifest) {
23
+ if (!manifest || manifest.version !== 2) return false;
24
+ if (!manifest.runtime?.files || !Array.isArray(manifest.runtime.files)) return false;
25
+ if (!manifest.settings?.template) return false;
26
+ return true;
27
+ }
28
+
22
29
  function loadClaudeMigrationManifest() {
23
30
  const manifestPath = path.join(__dirname, '../src/claude/migration-manifest.json');
24
31
 
@@ -27,7 +34,15 @@ function loadClaudeMigrationManifest() {
27
34
  }
28
35
 
29
36
  try {
30
- return JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
37
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
38
+
39
+ if (!validateManifestV2(manifest)) {
40
+ console.error('✗ Invalid manifest v2 schema - missing required fields');
41
+ console.error(' Expected: version=2, runtime.files[], settings.template');
42
+ process.exit(1);
43
+ }
44
+
45
+ return manifest;
31
46
  } catch (error) {
32
47
  console.warn(`⚠ Failed to parse Claude migration manifest: ${error.message}`);
33
48
  return null;
@@ -649,6 +664,110 @@ function copyGeminiFile(platformKey, results, options = {}) {
649
664
  }
650
665
  }
651
666
 
667
+ // Copy Claude runtime files (statusline bundle)
668
+ function copyClaudeRuntimeFiles(platformKey, results, options = {}) {
669
+ if (platformKey !== 'claude') return;
670
+
671
+ const manifest = CLAUDE_MIGRATION_MANIFEST;
672
+ if (!manifest?.runtime?.files) return;
673
+
674
+ const shouldOverwriteManagedFiles = Boolean(options.upgrade);
675
+ const srcBase = path.join(__dirname, '../src/claude');
676
+ const targetBase = path.join(PLATFORMS.claude.folder);
677
+
678
+ manifest.runtime.files.forEach(relPath => {
679
+ const srcPath = path.join(srcBase, relPath);
680
+ const targetPath = path.join(targetBase, relPath);
681
+
682
+ if (!fs.existsSync(srcPath)) {
683
+ console.log(` ⚠ Runtime file not found: ${relPath}`);
684
+ results.missingDependencies++;
685
+ return;
686
+ }
687
+
688
+ const targetExists = fs.existsSync(targetPath);
689
+ const shouldCopy = shouldOverwriteManagedFiles || !targetExists;
690
+
691
+ if (shouldCopy) {
692
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
693
+ fs.copyFileSync(srcPath, targetPath);
694
+
695
+ if (targetExists) {
696
+ console.log(` ↻ Runtime updated: ${relPath}`);
697
+ results.updated++;
698
+ } else {
699
+ console.log(` ✓ Runtime installed: ${relPath}`);
700
+ results.copied++;
701
+ }
702
+ } else {
703
+ results.skipped++;
704
+ }
705
+ });
706
+ }
707
+
708
+ // Merge Claude settings.json
709
+ function mergeClaudeSettings(platformKey, results, options = {}) {
710
+ if (platformKey !== 'claude') return;
711
+
712
+ const manifest = CLAUDE_MIGRATION_MANIFEST;
713
+ if (!manifest?.settings?.template) return;
714
+
715
+ const templatePath = path.join(__dirname, '../src/claude', manifest.settings.template);
716
+ const targetPath = path.join(PLATFORMS.claude.folder, 'settings.json');
717
+
718
+ if (!fs.existsSync(templatePath)) {
719
+ console.log(` ⚠ Settings template not found: ${manifest.settings.template}`);
720
+ return;
721
+ }
722
+
723
+ const managedSettings = JSON.parse(fs.readFileSync(templatePath, 'utf8'));
724
+ let existingSettings = {};
725
+
726
+ if (fs.existsSync(targetPath)) {
727
+ existingSettings = JSON.parse(fs.readFileSync(targetPath, 'utf8'));
728
+ }
729
+
730
+ const mergedSettings = { ...existingSettings };
731
+
732
+ // Merge statusLine
733
+ if (managedSettings.statusLine) {
734
+ const existingCommand = existingSettings.statusLine?.command || '';
735
+ const isCafeKitOwned = existingCommand.includes('status.cjs') || existingCommand.includes('statusline.cjs');
736
+
737
+ if (options.upgrade || !existingSettings.statusLine || isCafeKitOwned) {
738
+ mergedSettings.statusLine = managedSettings.statusLine;
739
+ console.log(` ✓ Settings: statusLine merged`);
740
+ }
741
+ }
742
+
743
+ // Merge hooks
744
+ if (managedSettings.hooks) {
745
+ mergedSettings.hooks = mergedSettings.hooks || {};
746
+
747
+ Object.keys(managedSettings.hooks).forEach(eventName => {
748
+ const managedHooks = managedSettings.hooks[eventName];
749
+ const existingHooks = mergedSettings.hooks[eventName] || [];
750
+ const mergedHooks = [...existingHooks];
751
+
752
+ managedHooks.forEach(managedHook => {
753
+ const managedCommand = managedHook.hooks?.[0]?.command || '';
754
+ const isDuplicate = mergedHooks.some(existingHook => {
755
+ return existingHook.hooks?.some(h => h.command === managedCommand);
756
+ });
757
+
758
+ if (!isDuplicate) {
759
+ mergedHooks.push(managedHook);
760
+ console.log(` ✓ Settings: hook ${eventName} merged`);
761
+ }
762
+ });
763
+
764
+ mergedSettings.hooks[eventName] = mergedHooks;
765
+ });
766
+ }
767
+
768
+ fs.writeFileSync(targetPath, JSON.stringify(mergedSettings, null, 2), 'utf8');
769
+ }
770
+
652
771
  // ═══════════════════════════════════════════════════════════
653
772
  // MAIN
654
773
  // ═══════════════════════════════════════════════════════════
@@ -718,6 +837,8 @@ async function main() {
718
837
  // Copy ROUTING.md for Claude Code platform
719
838
  if (platformKey === 'claude') {
720
839
  copyRoutingFile(platformKey, results, installerOptions);
840
+ copyClaudeRuntimeFiles(platformKey, results, installerOptions);
841
+ mergeClaudeSettings(platformKey, results, installerOptions);
721
842
  }
722
843
 
723
844
  // Copy GEMINI.md for Antigravity platform
@@ -756,6 +877,9 @@ async function main() {
756
877
  const platform = PLATFORMS[platformKey];
757
878
  console.log(`\n For ${platform.name}:`);
758
879
  console.log(` Run: ${platform.commandPrefix}spec-init <feature-name>`);
880
+ if (platformKey === 'claude') {
881
+ console.log(' Or use skill: /hapo:spec-init <feature-description>');
882
+ }
759
883
  }
760
884
 
761
885
  console.log('\n 2. Follow the workflow: requirements - design - tasks - code - test - review');
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@haposoft/cafekit",
3
- "version": "0.3.12",
4
- "description": "Spec-Driven Development workflow for AI coding assistants. Supports Claude Code and Antigravity with spec-first and code-test-review workflows.",
3
+ "version": "0.4.0",
4
+ "description": "Spec-Driven Development workflow for AI coding assistants. Supports Claude Code and Antigravity with spec-first workflows plus Claude Code hapo: skills.",
5
5
  "author": "Haposoft <nghialt@haposoft.com>",
6
6
  "license": "MIT",
7
7
  "private": false,
@@ -32,7 +32,9 @@
32
32
  "ai-coding",
33
33
  "specification",
34
34
  "requirements",
35
- "sdd"
35
+ "sdd",
36
+ "skills",
37
+ "hapo"
36
38
  ],
37
39
  "engines": {
38
40
  "node": ">=18.0.0"
@@ -0,0 +1,203 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * SubagentStart Hook - Injects context to subagents (Optimized)
4
+ *
5
+ * Fires: When a subagent (Task tool call) is started
6
+ * Purpose: Inject minimal context using env vars from SessionStart
7
+ * Target: ~200 tokens (down from ~350)
8
+ *
9
+ * Exit Codes:
10
+ * 0 - Success (non-blocking, allows continuation)
11
+ */
12
+
13
+ // Crash wrapper
14
+ try {
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const {
18
+ loadConfig,
19
+ resolveNamingPattern,
20
+ getGitBranch,
21
+ getGitRoot,
22
+ resolvePlanPath,
23
+ getReportsPath,
24
+ normalizePath,
25
+ extractTaskListId,
26
+ isHookEnabled
27
+ } = require('./lib/config.cjs');
28
+ const { resolveSkillsVenv } = require('./lib/context.cjs');
29
+
30
+ // Early exit if hook disabled in config
31
+ if (!isHookEnabled('subagent-init')) {
32
+ process.exit(0);
33
+ }
34
+
35
+ /**
36
+ * Get agent-specific context from config
37
+ */
38
+ function getAgentContext(agentType, config) {
39
+ const agentConfig = config.subagent?.agents?.[agentType];
40
+ if (!agentConfig?.contextPrefix) return null;
41
+ return agentConfig.contextPrefix;
42
+ }
43
+
44
+ /**
45
+ * Build trust verification section if enabled
46
+ */
47
+ function buildTrustVerification(config) {
48
+ if (!config.trust?.enabled || !config.trust?.passphrase) return [];
49
+ return [
50
+ ``,
51
+ `## Trust Verification`,
52
+ `Passphrase: "${config.trust.passphrase}"`
53
+ ];
54
+ }
55
+
56
+ /**
57
+ * Main hook execution
58
+ */
59
+ async function main() {
60
+ try {
61
+ const stdin = fs.readFileSync(0, 'utf-8').trim();
62
+ if (!stdin) process.exit(0);
63
+
64
+ const payload = JSON.parse(stdin);
65
+ const agentType = payload.agent_type || 'unknown';
66
+ const agentId = payload.agent_id || 'unknown';
67
+
68
+ // Load config for trust verification, naming, and agent-specific context
69
+ const config = loadConfig({ includeProject: false, includeAssertions: false });
70
+
71
+ // Use payload.cwd if provided for git operations (monorepo support)
72
+ // This ensures subagent resolves paths relative to its own CWD, not process.cwd()
73
+ // Issue #327: Use trim() to handle empty string edge case
74
+ const effectiveCwd = payload.cwd?.trim() || process.cwd();
75
+
76
+ // Compute naming pattern directly (don't rely on env vars which may not propagate)
77
+ // Pass effectiveCwd to git commands to support monorepo/submodule scenarios
78
+ const gitBranch = getGitBranch(effectiveCwd);
79
+ const gitRoot = getGitRoot(effectiveCwd);
80
+ // Issue #327: Use CWD as base for subdirectory workflow support
81
+ // Git root is kept for reference but CWD determines where files are created
82
+ const baseDir = effectiveCwd;
83
+
84
+ // Debug logging for path resolution troubleshooting
85
+ if (process.env.CK_DEBUG) {
86
+ console.error(`[subagent-init] effectiveCwd=${effectiveCwd}, gitRoot=${gitRoot}, baseDir=${baseDir}`);
87
+ }
88
+ const namePattern = resolveNamingPattern(config.plan, gitBranch);
89
+
90
+ // Resolve plan and reports path - use absolute paths based on CWD (Issue #327)
91
+ // Use session_id from payload to resolve active plan context (Issue #321)
92
+ const sessionId = payload.session_id || process.env.CK_SESSION_ID || null;
93
+ const resolved = resolvePlanPath(sessionId, config);
94
+ const reportsPath = getReportsPath(resolved.path, resolved.resolvedBy, config.plan, config.paths, baseDir);
95
+ const activePlan = resolved.resolvedBy === 'session' ? resolved.path : '';
96
+ const suggestedPlan = resolved.resolvedBy === 'branch' ? resolved.path : '';
97
+
98
+ // Extract task list ID for Claude Code Tasks coordination (shared helper, DRY)
99
+ const taskListId = extractTaskListId(resolved);
100
+ const plansPath = path.join(baseDir, normalizePath(config.paths?.plans) || 'plans');
101
+ const docsPath = path.join(baseDir, normalizePath(config.paths?.docs) || 'docs');
102
+ const thinkingLanguage = config.locale?.thinkingLanguage || '';
103
+ const responseLanguage = config.locale?.responseLanguage || '';
104
+ // Auto-default thinkingLanguage to 'en' when only responseLanguage is set
105
+ const effectiveThinking = thinkingLanguage || (responseLanguage ? 'en' : '');
106
+
107
+ // Build compact context (~200 tokens)
108
+ const lines = [];
109
+
110
+ // Subagent identification
111
+ lines.push(`## Subagent: ${agentType}`);
112
+ lines.push(`ID: ${agentId} | CWD: ${effectiveCwd}`);
113
+ lines.push(``);
114
+
115
+ // Plan context (from env vars)
116
+ lines.push(`## Context`);
117
+ if (activePlan) {
118
+ lines.push(`- Plan: ${activePlan}`);
119
+ if (taskListId) {
120
+ lines.push(`- Task List: ${taskListId} (shared with session)`);
121
+ }
122
+ } else if (suggestedPlan) {
123
+ lines.push(`- Plan: none | Suggested: ${suggestedPlan}`);
124
+ } else {
125
+ lines.push(`- Plan: none`);
126
+ }
127
+ lines.push(`- Reports: ${reportsPath}`);
128
+ lines.push(`- Paths: ${plansPath}/ | ${docsPath}/`);
129
+ lines.push(``);
130
+
131
+ // Language (thinking + response, if configured)
132
+ const hasThinking = effectiveThinking && effectiveThinking !== responseLanguage;
133
+ if (hasThinking || responseLanguage) {
134
+ lines.push(`## Language`);
135
+ if (hasThinking) {
136
+ lines.push(`- Thinking: Use ${effectiveThinking} for reasoning (logic, precision).`);
137
+ }
138
+ if (responseLanguage) {
139
+ lines.push(`- Response: Respond in ${responseLanguage} (natural, fluent).`);
140
+ }
141
+ lines.push(``);
142
+ }
143
+
144
+ // Resolve Python venv path for subagent instructions
145
+ const skillsVenv = resolveSkillsVenv();
146
+
147
+ // Core rules (minimal)
148
+ lines.push(`## Rules`);
149
+ lines.push(`- Reports → ${reportsPath}`);
150
+ lines.push(`- YAGNI / KISS / DRY`);
151
+ lines.push(`- Concise, list unresolved Qs at end`);
152
+ // Python venv rules (if venv exists)
153
+ if (skillsVenv) {
154
+ lines.push(`- Python scripts in .claude/skills/: Use \`${skillsVenv}\``);
155
+ lines.push(`- Never use global pip install`);
156
+ }
157
+
158
+ // Naming templates (computed directly for reliable injection)
159
+ lines.push(``);
160
+ lines.push(`## Naming`);
161
+ lines.push(`- Report: ${path.join(reportsPath, `${agentType}-${namePattern}.md`)}`);
162
+ lines.push(`- Plan dir: ${path.join(plansPath, namePattern)}/`);
163
+
164
+ // Trust verification (if enabled)
165
+ lines.push(...buildTrustVerification(config));
166
+
167
+ // Agent-specific context (if configured)
168
+ const agentContext = getAgentContext(agentType, config);
169
+ if (agentContext) {
170
+ lines.push(``);
171
+ lines.push(`## Agent Instructions`);
172
+ lines.push(agentContext);
173
+ }
174
+
175
+ // CRITICAL: SubagentStart requires hookSpecificOutput.additionalContext format
176
+ const output = {
177
+ hookSpecificOutput: {
178
+ hookEventName: "SubagentStart",
179
+ additionalContext: lines.join('\n')
180
+ }
181
+ };
182
+
183
+ console.log(JSON.stringify(output));
184
+ process.exit(0);
185
+ } catch (error) {
186
+ console.error(`SubagentStart hook error: ${error.message}`);
187
+ process.exit(0); // Fail-open
188
+ }
189
+ }
190
+
191
+ main();
192
+ } catch (e) {
193
+ // Minimal crash logging (zero deps — only Node builtins)
194
+ try {
195
+ const fs = require('fs');
196
+ const p = require('path');
197
+ const logDir = p.join(__dirname, '.logs');
198
+ if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
199
+ fs.appendFileSync(p.join(logDir, 'hook-log.jsonl'),
200
+ JSON.stringify({ ts: new Date().toISOString(), hook: p.basename(__filename, '.cjs'), status: 'crash', error: e.message }) + '\n');
201
+ } catch (_) {}
202
+ process.exit(0); // fail-open
203
+ }
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * ANSI Terminal Colors - Cross-platform color support for statusline
6
+ * Supports NO_COLOR, FORCE_COLOR, COLORTERM auto-detection
7
+ * @module colors
8
+ */
9
+
10
+ // ANSI escape codes (8-color basic palette)
11
+ const RESET = '\x1b[0m';
12
+ const DIM = '\x1b[2m';
13
+ const RED = '\x1b[31m';
14
+ const GREEN = '\x1b[32m';
15
+ const YELLOW = '\x1b[33m';
16
+ const MAGENTA = '\x1b[35m';
17
+ const CYAN = '\x1b[36m';
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 = (() => {
22
+ if (process.env.NO_COLOR) return false;
23
+ if (process.env.FORCE_COLOR) return true;
24
+ // Default true for statusline context (Claude Code handles TTY display)
25
+ return true;
26
+ })();
27
+
28
+ // Detect 256-color support via COLORTERM
29
+ const has256Color = (() => {
30
+ const ct = process.env.COLORTERM;
31
+ return ct === 'truecolor' || ct === '24bit' || ct === '256color';
32
+ })();
33
+
34
+ /**
35
+ * Wrap text with ANSI color code
36
+ * @param {string} text - Text to colorize
37
+ * @param {string} code - ANSI escape code
38
+ * @returns {string} Colorized text or plain text if colors disabled
39
+ */
40
+ function colorize(text, code) {
41
+ if (!shouldUseColor) return String(text);
42
+ return `${code}${text}${RESET}`;
43
+ }
44
+
45
+ function green(text) { return colorize(text, GREEN); }
46
+ function yellow(text) { return colorize(text, YELLOW); }
47
+ function red(text) { return colorize(text, RED); }
48
+ function cyan(text) { return colorize(text, CYAN); }
49
+ function magenta(text) { return colorize(text, MAGENTA); }
50
+ function dim(text) { return colorize(text, DIM); }
51
+
52
+ /**
53
+ * Get color code based on context percentage threshold
54
+ * @param {number} percent - Context usage percentage (0-100)
55
+ * @returns {string} ANSI color code
56
+ */
57
+ function getContextColor(percent) {
58
+ if (percent >= 85) return RED;
59
+ if (percent >= 70) return YELLOW;
60
+ return GREEN;
61
+ }
62
+
63
+ /**
64
+ * Generate colored progress bar for context window
65
+ * Uses ▰▱ characters (smooth horizontal rectangles) for consistent rendering
66
+ * @param {number} percent - Usage percentage (0-100)
67
+ * @param {number} width - Bar width in characters (default 12)
68
+ * @returns {string} Unicode progress bar with threshold-based colors
69
+ */
70
+ function coloredBar(percent, width = 12) {
71
+ const clamped = Math.max(0, Math.min(100, percent));
72
+ const filled = Math.round((clamped / 100) * width);
73
+ const empty = width - filled;
74
+
75
+ if (!shouldUseColor) {
76
+ return '▰'.repeat(filled) + '▱'.repeat(empty);
77
+ }
78
+
79
+ const color = getContextColor(percent);
80
+ return `${color}${'▰'.repeat(filled)}${DIM}${'▱'.repeat(empty)}${RESET}`;
81
+ }
82
+
83
+ module.exports = {
84
+ RESET,
85
+ green,
86
+ yellow,
87
+ red,
88
+ cyan,
89
+ magenta,
90
+ dim,
91
+ getContextColor,
92
+ coloredBar,
93
+ shouldUseColor,
94
+ has256Color
95
+ };