@torka/claude-qol 0.1.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.
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Claude Code Context Monitor
4
+ Real-time context usage monitoring with visual indicators and session analytics
5
+ https://code.claude.com/docs/en/statusline
6
+ """
7
+
8
+ import json
9
+ import sys
10
+ import os
11
+
12
+
13
+ def context_window_info(window):
14
+ """
15
+ Build context info from the statusline `context_window` payload.
16
+ Expected shape:
17
+ {
18
+ "context_window_size": int,
19
+ "current_usage": {
20
+ "input_tokens": int,
21
+ "output_tokens": int,
22
+ "cache_creation_input_tokens": int,
23
+ "cache_read_input_tokens": int
24
+ }
25
+ }
26
+ """
27
+ if not isinstance(window, dict):
28
+ return None
29
+
30
+ size = window.get("context_window_size")
31
+ if not size or size <= 0:
32
+ return None
33
+
34
+ usage = window.get("current_usage")
35
+
36
+ # If no calls yet, usage may be null; treat as 0% used.
37
+ if usage is None:
38
+ return {
39
+ "percent": 0,
40
+ "tokens": 0,
41
+ "method": "context_window",
42
+ }
43
+
44
+ if not isinstance(usage, dict):
45
+ return None
46
+
47
+ tokens = (
48
+ usage.get("input_tokens", 0)
49
+ + usage.get("output_tokens", 0)
50
+ + usage.get("cache_creation_input_tokens", 0)
51
+ + usage.get("cache_read_input_tokens", 0)
52
+ )
53
+
54
+ percent = (tokens / size) * 100 if size > 0 else 0
55
+
56
+ return {
57
+ "percent": max(0, min(100, percent)),
58
+ "tokens": tokens,
59
+ "method": "context_window",
60
+ }
61
+
62
+
63
+ def get_context_display(context_info):
64
+ """Generate context display with visual indicators."""
65
+ if not context_info:
66
+ return "🔵 ???"
67
+
68
+ percent = context_info.get('percent', 0)
69
+ percent = max(0, min(100, percent))
70
+ warning = context_info.get('warning')
71
+
72
+ # Color based on usage level
73
+ if percent >= 95:
74
+ color = "\033[31;1m" # Blinking red
75
+ alert = "CRIT"
76
+ elif percent >= 90:
77
+ color = "\033[31m" # Red
78
+ alert = "HIGH"
79
+ elif percent >= 75:
80
+ color = "\033[91m" # Light red
81
+ alert = ""
82
+ elif percent >= 50:
83
+ color = "\033[33m" # Yellow
84
+ alert = ""
85
+ else:
86
+ color = "\033[32m" # Green
87
+ alert = ""
88
+
89
+ # Create progress bar
90
+ segments = 8
91
+ filled = int((percent / 100) * segments)
92
+ bar = "█" * filled + "▁" * (segments - filled)
93
+
94
+ # Special warnings
95
+ if warning == 'auto-compact':
96
+ alert = "AUTO-COMPACT!"
97
+ elif warning == 'low':
98
+ alert = "LOW!"
99
+
100
+ reset = "\033[0m"
101
+ alert_str = f" {alert}" if alert else ""
102
+
103
+ return f"{color}{bar}{reset} {percent:.0f}%{alert_str}"
104
+
105
+ def get_directory_display(workspace_data):
106
+ """Get directory display name."""
107
+ current_dir = workspace_data.get('current_dir', '')
108
+ project_dir = workspace_data.get('project_dir', '')
109
+ cwd = workspace_data.get('cwd', '')
110
+
111
+ if current_dir and project_dir:
112
+ if current_dir.startswith(project_dir):
113
+ rel_path = current_dir[len(project_dir):].lstrip('/')
114
+ return rel_path or os.path.basename(project_dir)
115
+ else:
116
+ return os.path.basename(current_dir)
117
+ elif project_dir:
118
+ return os.path.basename(project_dir)
119
+ elif cwd:
120
+ return os.path.basename(cwd)
121
+ elif current_dir:
122
+ return os.path.basename(current_dir)
123
+ else:
124
+ return "unknown"
125
+
126
+ def get_git_branch():
127
+ """Get the current git branch name by reading .git/HEAD directly."""
128
+ try:
129
+ git_dir = ".git"
130
+ if os.path.isdir(git_dir):
131
+ head_file = os.path.join(git_dir, "HEAD")
132
+ if os.path.isfile(head_file):
133
+ with open(head_file, 'r') as f:
134
+ ref = f.read().strip()
135
+ if ref.startswith('ref: refs/heads/'):
136
+ return ref.replace('ref: refs/heads/', '')
137
+ # Detached HEAD state
138
+ return ref[:8]
139
+ return None
140
+ except Exception:
141
+ return None
142
+
143
+ def main():
144
+ try:
145
+ # Read JSON input from Claude Code
146
+ data = json.load(sys.stdin)
147
+
148
+ # Extract information
149
+ model_data = data.get('model', {})
150
+ model_name = model_data.get('display_name') or model_data.get('name') or model_data.get('id') or 'Claude'
151
+ model_id = model_data.get('id') or model_data.get('name') or model_name
152
+
153
+ workspace = data.get('workspace', {})
154
+ context_window = data.get('context_window') or {}
155
+
156
+ # Build status components
157
+ context_info = context_window_info(context_window)
158
+ context_display = get_context_display(context_info)
159
+ directory = get_directory_display(workspace)
160
+ git_branch = get_git_branch()
161
+ git_display = f" \033[96m🌿 {git_branch}\033[0m" if git_branch else ""
162
+
163
+ model_display = f"\033[94m[{model_name}]\033[0m"
164
+
165
+ # Combine all components
166
+ status_line = f"{model_display} \033[93m📁 {directory}\033[0m{git_display} 🧠 {context_display}"
167
+
168
+ print(status_line)
169
+
170
+ except Exception as e:
171
+ # Fallback display on any error
172
+ print(f"\033[94m[Claude]\033[0m \033[93m📁 {os.path.basename(os.getcwd())}\033[0m 🧠 \033[31m[Error: {str(e)[:20]}]\033[0m")
173
+
174
+ if __name__ == "__main__":
175
+ main()
package/uninstall.js ADDED
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @torka/claude-qol - Pre-uninstall script
4
+ * Removes QoL files from the .claude directory
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const os = require('os');
10
+
11
+ // ANSI colors for output
12
+ const colors = {
13
+ green: '\x1b[32m',
14
+ yellow: '\x1b[33m',
15
+ blue: '\x1b[34m',
16
+ red: '\x1b[31m',
17
+ reset: '\x1b[0m',
18
+ bold: '\x1b[1m',
19
+ };
20
+
21
+ function log(message, color = 'reset') {
22
+ console.log(`${colors[color]}${message}${colors.reset}`);
23
+ }
24
+
25
+ function logSuccess(message) {
26
+ log(` ✓ ${message}`, 'green');
27
+ }
28
+
29
+ function logSkip(message) {
30
+ log(` ○ ${message}`, 'yellow');
31
+ }
32
+
33
+ /**
34
+ * Files that were installed by this package
35
+ * Only remove files we installed, not user-modified files
36
+ */
37
+ const INSTALLED_FILES = {
38
+ scripts: [
39
+ 'auto_approve_safe.py',
40
+ 'auto_approve_safe.rules.json',
41
+ 'context-monitor.py',
42
+ ],
43
+ commands: [
44
+ 'optimize-auto-approve-hook.md',
45
+ ],
46
+ };
47
+
48
+ /**
49
+ * Determine the target .claude directory based on installation context
50
+ */
51
+ function getTargetBase() {
52
+ const isGlobal = process.env.npm_config_global === 'true';
53
+
54
+ if (isGlobal) {
55
+ return path.join(os.homedir(), '.claude');
56
+ }
57
+
58
+ let projectRoot = process.env.INIT_CWD || process.cwd();
59
+
60
+ while (projectRoot !== path.dirname(projectRoot)) {
61
+ const packageJsonPath = path.join(projectRoot, 'package.json');
62
+ if (fs.existsSync(packageJsonPath)) {
63
+ try {
64
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
65
+ if (pkg.name !== '@torka/claude-qol') {
66
+ return path.join(projectRoot, '.claude');
67
+ }
68
+ } catch (e) {
69
+ // Continue walking up
70
+ }
71
+ }
72
+ projectRoot = path.dirname(projectRoot);
73
+ }
74
+
75
+ return path.join(process.env.INIT_CWD || process.cwd(), '.claude');
76
+ }
77
+
78
+ /**
79
+ * Remove a file if it exists and matches what we installed
80
+ */
81
+ function removeFile(filePath, stats) {
82
+ if (fs.existsSync(filePath)) {
83
+ try {
84
+ fs.unlinkSync(filePath);
85
+ stats.removed.push(filePath);
86
+ logSuccess(`Removed: ${path.relative(stats.targetBase, filePath)}`);
87
+ } catch (error) {
88
+ stats.failed.push({ path: filePath, error: error.message });
89
+ log(` ✗ Failed to remove: ${path.relative(stats.targetBase, filePath)}`, 'red');
90
+ }
91
+ } else {
92
+ stats.notFound.push(filePath);
93
+ logSkip(`Not found: ${path.relative(stats.targetBase, filePath)}`);
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Remove empty directories
99
+ */
100
+ function removeEmptyDir(dirPath) {
101
+ if (fs.existsSync(dirPath)) {
102
+ const files = fs.readdirSync(dirPath);
103
+ if (files.length === 0) {
104
+ fs.rmdirSync(dirPath);
105
+ return true;
106
+ }
107
+ }
108
+ return false;
109
+ }
110
+
111
+ /**
112
+ * Main uninstall function
113
+ */
114
+ function uninstall() {
115
+ const targetBase = getTargetBase();
116
+ const isGlobal = process.env.npm_config_global === 'true';
117
+
118
+ log('\n' + colors.bold + '📦 @torka/claude-qol - Uninstalling...' + colors.reset);
119
+ log(` Target: ${targetBase}`, 'blue');
120
+ log(` Mode: ${isGlobal ? 'Global' : 'Project-level'}\n`, 'blue');
121
+
122
+ if (!fs.existsSync(targetBase)) {
123
+ log(' .claude directory not found, nothing to remove.\n', 'yellow');
124
+ return;
125
+ }
126
+
127
+ const stats = {
128
+ removed: [],
129
+ notFound: [],
130
+ failed: [],
131
+ targetBase,
132
+ };
133
+
134
+ // Remove files by category
135
+ for (const [dir, files] of Object.entries(INSTALLED_FILES)) {
136
+ log(`\n${colors.bold}${dir}/${colors.reset}`);
137
+ for (const file of files) {
138
+ const filePath = path.join(targetBase, dir, file);
139
+ removeFile(filePath, stats);
140
+ }
141
+
142
+ // Try to remove empty directories
143
+ const dirPath = path.join(targetBase, dir);
144
+ if (removeEmptyDir(dirPath)) {
145
+ log(` ○ Removed empty directory: ${dir}/`, 'yellow');
146
+ }
147
+ }
148
+
149
+ // Summary
150
+ log('\n' + colors.bold + '📊 Uninstall Summary' + colors.reset);
151
+ log(` Files removed: ${stats.removed.length}`, 'green');
152
+ log(` Files not found: ${stats.notFound.length}`, 'yellow');
153
+ if (stats.failed.length > 0) {
154
+ log(` Failed to remove: ${stats.failed.length}`, 'red');
155
+ }
156
+
157
+ log('\n' + colors.yellow + '⚠️ Note: settings.local.json was not modified.' + colors.reset);
158
+ log(' You may want to manually remove hook/statusLine configurations.\n');
159
+ }
160
+
161
+ // Run uninstall
162
+ try {
163
+ uninstall();
164
+ } catch (error) {
165
+ log(`Uninstall warning: ${error.message}`, 'yellow');
166
+ // Don't exit with error code - allow npm uninstall to complete
167
+ }