claude-code-termux 1.0.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,188 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Claude Code Termux Installation Verification
5
+ *
6
+ * This script checks that all components are properly installed
7
+ * and configured for running Claude Code on Termux.
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const { execSync } = require('child_process');
15
+
16
+ // ANSI colors
17
+ const colors = {
18
+ reset: '\x1b[0m',
19
+ red: '\x1b[31m',
20
+ green: '\x1b[32m',
21
+ yellow: '\x1b[33m',
22
+ blue: '\x1b[34m',
23
+ };
24
+
25
+ const PASS = `${colors.green}✓${colors.reset}`;
26
+ const FAIL = `${colors.red}✗${colors.reset}`;
27
+ const WARN = `${colors.yellow}!${colors.reset}`;
28
+
29
+ let passCount = 0;
30
+ let failCount = 0;
31
+ let warnCount = 0;
32
+
33
+ function check(name, condition, warning = false) {
34
+ if (condition) {
35
+ console.log(`${PASS} ${name}`);
36
+ passCount++;
37
+ return true;
38
+ } else if (warning) {
39
+ console.log(`${WARN} ${name}`);
40
+ warnCount++;
41
+ return false;
42
+ } else {
43
+ console.log(`${FAIL} ${name}`);
44
+ failCount++;
45
+ return false;
46
+ }
47
+ }
48
+
49
+ function runCommand(cmd) {
50
+ try {
51
+ return execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
52
+ } catch (err) {
53
+ return null;
54
+ }
55
+ }
56
+
57
+ console.log('');
58
+ console.log('='.repeat(50));
59
+ console.log(' Claude Code Termux - Installation Verification');
60
+ console.log('='.repeat(50));
61
+ console.log('');
62
+
63
+ // Environment checks
64
+ console.log(`${colors.blue}Environment:${colors.reset}`);
65
+
66
+ const isTermux = process.platform === 'android' ||
67
+ process.env.PREFIX?.includes('com.termux') ||
68
+ process.env.HOME?.includes('com.termux');
69
+
70
+ check('Termux environment detected', isTermux, true);
71
+ check('HOME directory exists', fs.existsSync(process.env.HOME || ''));
72
+ console.log(` Platform: ${process.platform}`);
73
+ console.log(` Architecture: ${process.arch}`);
74
+ console.log(` HOME: ${process.env.HOME}`);
75
+ console.log('');
76
+
77
+ // Node.js checks
78
+ console.log(`${colors.blue}Node.js:${colors.reset}`);
79
+
80
+ const nodeVersion = process.version;
81
+ const nodeMajor = parseInt(nodeVersion.slice(1).split('.')[0]);
82
+
83
+ check('Node.js version >= 18', nodeMajor >= 18);
84
+ check('Node.js version < 25', nodeMajor < 25, true);
85
+ console.log(` Version: ${nodeVersion}`);
86
+ console.log('');
87
+
88
+ // Ripgrep checks
89
+ console.log(`${colors.blue}Ripgrep:${colors.reset}`);
90
+
91
+ const rgPath = runCommand('which rg');
92
+ const rgVersion = runCommand('rg --version');
93
+
94
+ check('Ripgrep installed', !!rgPath);
95
+ if (rgPath) {
96
+ console.log(` Path: ${rgPath}`);
97
+ console.log(` Version: ${rgVersion?.split('\n')[0] || 'unknown'}`);
98
+ }
99
+ console.log('');
100
+
101
+ // Claude Code checks
102
+ console.log(`${colors.blue}Claude Code:${colors.reset}`);
103
+
104
+ let claudeCodePath = null;
105
+ try {
106
+ claudeCodePath = require.resolve('@anthropic-ai/claude-code/package.json');
107
+ } catch (err) {
108
+ // Not found
109
+ }
110
+
111
+ check('Claude Code installed', !!claudeCodePath);
112
+
113
+ if (claudeCodePath) {
114
+ const pkgPath = path.dirname(claudeCodePath);
115
+ const pkg = require(claudeCodePath);
116
+ console.log(` Version: ${pkg.version}`);
117
+ console.log(` Path: ${pkgPath}`);
118
+
119
+ // Check vendor binaries
120
+ const vendorRg = path.join(pkgPath, 'vendor', 'ripgrep', 'arm64-android', 'rg');
121
+ const vendorRgLinux = path.join(pkgPath, 'vendor', 'ripgrep', 'arm64-linux', 'rg');
122
+
123
+ check('Vendor ripgrep (android-arm64)', fs.existsSync(vendorRg), true);
124
+ check('Vendor ripgrep (linux-arm64)', fs.existsSync(vendorRgLinux), true);
125
+ }
126
+ console.log('');
127
+
128
+ // Sharp checks
129
+ console.log(`${colors.blue}Sharp (Image Support):${colors.reset}`);
130
+
131
+ let sharpWasm = false;
132
+ let sharpNative = false;
133
+
134
+ try {
135
+ require.resolve('@img/sharp-wasm32');
136
+ sharpWasm = true;
137
+ } catch (err) {
138
+ // Not found
139
+ }
140
+
141
+ try {
142
+ require.resolve('sharp');
143
+ sharpNative = true;
144
+ } catch (err) {
145
+ // Not found
146
+ }
147
+
148
+ check('Sharp WASM available', sharpWasm);
149
+ check('Sharp native available', sharpNative, true);
150
+ console.log('');
151
+
152
+ // Configuration checks
153
+ console.log(`${colors.blue}Configuration:${colors.reset}`);
154
+
155
+ const homeDir = process.env.HOME || '/data/data/com.termux/files/home';
156
+ const claudeDir = path.join(homeDir, '.claude');
157
+ const commandsDir = path.join(claudeDir, 'commands');
158
+
159
+ check('.claude directory exists', fs.existsSync(claudeDir), true);
160
+ check('.claude/commands directory exists', fs.existsSync(commandsDir), true);
161
+
162
+ const hasApiKey = !!process.env.ANTHROPIC_API_KEY;
163
+ check('ANTHROPIC_API_KEY set', hasApiKey, true);
164
+
165
+ if (!hasApiKey) {
166
+ console.log(` ${colors.yellow}Tip: export ANTHROPIC_API_KEY=your-key${colors.reset}`);
167
+ }
168
+ console.log('');
169
+
170
+ // Summary
171
+ console.log('='.repeat(50));
172
+ console.log(' Summary');
173
+ console.log('='.repeat(50));
174
+ console.log(` ${PASS} Passed: ${passCount}`);
175
+ console.log(` ${WARN} Warnings: ${warnCount}`);
176
+ console.log(` ${FAIL} Failed: ${failCount}`);
177
+ console.log('');
178
+
179
+ if (failCount > 0) {
180
+ console.log(`${colors.red}Some checks failed. Please review the errors above.${colors.reset}`);
181
+ process.exit(1);
182
+ } else if (warnCount > 0) {
183
+ console.log(`${colors.yellow}Installation OK with some warnings.${colors.reset}`);
184
+ process.exit(0);
185
+ } else {
186
+ console.log(`${colors.green}All checks passed! Claude Code is ready to use.${colors.reset}`);
187
+ process.exit(0);
188
+ }
@@ -0,0 +1,29 @@
1
+ # Binary Files
2
+
3
+ This directory contains pre-compiled binaries for platforms that don't have native support.
4
+
5
+ ## Ripgrep (rg)
6
+
7
+ The `rg` binary is not included in the repository due to size constraints. It is downloaded automatically during installation on Termux.
8
+
9
+ ### Manual Download
10
+
11
+ To download the binary manually:
12
+
13
+ ```bash
14
+ ./scripts/download-ripgrep.sh
15
+ ```
16
+
17
+ Or download from: https://github.com/BurntSushi/ripgrep/releases
18
+
19
+ Use the `aarch64-unknown-linux-gnu` build for Termux ARM64.
20
+
21
+ ### Alternative: System Ripgrep
22
+
23
+ On Termux, you can also install ripgrep via the package manager:
24
+
25
+ ```bash
26
+ pkg install ripgrep
27
+ ```
28
+
29
+ The wrapper will automatically detect and use the system ripgrep.
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Apply All Termux Patches
3
+ *
4
+ * This module applies all necessary runtime patches for Termux/Android compatibility.
5
+ * It must be loaded before the main Claude Code CLI.
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const path = require('path');
11
+
12
+ // Patch modules to load
13
+ const patches = [
14
+ './sharp-fallback',
15
+ './ripgrep-fallback',
16
+ './path-normalization',
17
+ './oauth-storage',
18
+ './hook-events',
19
+ ];
20
+
21
+ console.log('[claude-code-termux] Applying runtime patches...');
22
+
23
+ let appliedCount = 0;
24
+ let failedCount = 0;
25
+
26
+ for (const patchPath of patches) {
27
+ try {
28
+ const patch = require(patchPath);
29
+ if (typeof patch.apply === 'function') {
30
+ patch.apply();
31
+ appliedCount++;
32
+ console.log(`[claude-code-termux] Applied: ${path.basename(patchPath)}`);
33
+ }
34
+ } catch (err) {
35
+ failedCount++;
36
+ // Don't fail hard - some patches may not be needed
37
+ if (process.env.DEBUG) {
38
+ console.error(`[claude-code-termux] Failed to apply ${patchPath}:`, err.message);
39
+ }
40
+ }
41
+ }
42
+
43
+ console.log(`[claude-code-termux] Patches applied: ${appliedCount}, skipped: ${failedCount}`);
44
+
45
+ module.exports = { appliedCount, failedCount };
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Hook Events Patch
3
+ *
4
+ * This patch attempts to fix the PostToolUse hook not firing on Termux.
5
+ * The issue seems to be related to event emission on the Android platform.
6
+ *
7
+ * Issue: https://github.com/anthropics/claude-code/issues/15617
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const EventEmitter = require('events');
13
+
14
+ // Detect Termux environment
15
+ const isTermux = process.platform === 'android' ||
16
+ process.env.PREFIX?.includes('com.termux') ||
17
+ process.env.HOME?.includes('com.termux');
18
+
19
+ // Store original methods
20
+ const originalEmit = EventEmitter.prototype.emit;
21
+ const originalOn = EventEmitter.prototype.on;
22
+
23
+ // Track hook-related events
24
+ const hookEvents = new Set([
25
+ 'PreToolUse',
26
+ 'PostToolUse',
27
+ 'PreApiCall',
28
+ 'PostApiCall',
29
+ 'Notification',
30
+ 'Stop',
31
+ ]);
32
+
33
+ // Debug hook events
34
+ const hookDebugEnabled = process.env.DEBUG_HOOKS === '1';
35
+
36
+ /**
37
+ * Wrap emit to ensure hook events are properly fired
38
+ */
39
+ function patchedEmit(eventName, ...args) {
40
+ if (hookDebugEnabled && hookEvents.has(eventName)) {
41
+ console.log(`[claude-code-termux] Hook event: ${eventName}`, args.length > 0 ? '(with args)' : '');
42
+ }
43
+
44
+ // Call original emit
45
+ const result = originalEmit.call(this, eventName, ...args);
46
+
47
+ // On Termux, sometimes events need to be queued for next tick
48
+ // to ensure all listeners have been registered
49
+ if (isTermux && hookEvents.has(eventName) && !result) {
50
+ // If no listeners handled it, try again on next tick
51
+ setImmediate(() => {
52
+ const retryResult = originalEmit.call(this, eventName, ...args);
53
+ if (hookDebugEnabled && retryResult) {
54
+ console.log(`[claude-code-termux] Hook event handled on retry: ${eventName}`);
55
+ }
56
+ });
57
+ }
58
+
59
+ return result;
60
+ }
61
+
62
+ /**
63
+ * Wrap on/addListener to track hook registrations
64
+ */
65
+ function patchedOn(eventName, listener) {
66
+ if (hookDebugEnabled && hookEvents.has(eventName)) {
67
+ console.log(`[claude-code-termux] Hook listener registered: ${eventName}`);
68
+ }
69
+
70
+ return originalOn.call(this, eventName, listener);
71
+ }
72
+
73
+ /**
74
+ * Apply the hook events patch
75
+ */
76
+ function apply() {
77
+ if (!isTermux) {
78
+ return;
79
+ }
80
+
81
+ // Patch EventEmitter prototype
82
+ EventEmitter.prototype.emit = patchedEmit;
83
+ EventEmitter.prototype.on = patchedOn;
84
+ EventEmitter.prototype.addListener = patchedOn;
85
+
86
+ // Increase max listeners to prevent warnings in hook-heavy scenarios
87
+ EventEmitter.defaultMaxListeners = 20;
88
+
89
+ if (process.env.DEBUG) {
90
+ console.log('[claude-code-termux] Hook events patch applied');
91
+ console.log('[claude-code-termux] Set DEBUG_HOOKS=1 to trace hook events');
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Restore original EventEmitter methods
97
+ */
98
+ function restore() {
99
+ EventEmitter.prototype.emit = originalEmit;
100
+ EventEmitter.prototype.on = originalOn;
101
+ EventEmitter.prototype.addListener = originalOn;
102
+ }
103
+
104
+ module.exports = {
105
+ apply,
106
+ restore,
107
+ hookEvents,
108
+ };
@@ -0,0 +1,163 @@
1
+ /**
2
+ * OAuth Storage Fallback Patch
3
+ *
4
+ * This patch provides a fallback token storage mechanism for Termux
5
+ * where the system keychain is not available.
6
+ *
7
+ * Issue: https://github.com/anthropics/claude-code/issues/6244
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const crypto = require('crypto');
15
+
16
+ // Detect Termux environment
17
+ const isTermux = process.platform === 'android' ||
18
+ process.env.PREFIX?.includes('com.termux') ||
19
+ process.env.HOME?.includes('com.termux');
20
+
21
+ // Token storage file
22
+ const TOKEN_FILE = path.join(
23
+ process.env.HOME || '/data/data/com.termux/files/home',
24
+ '.claude',
25
+ '.oauth-tokens.enc'
26
+ );
27
+
28
+ // Encryption key derived from device-specific info
29
+ function getEncryptionKey() {
30
+ // Use a combination of device-specific values for the key
31
+ const keyMaterial = [
32
+ process.env.HOME || '',
33
+ process.env.USER || '',
34
+ process.env.PREFIX || '',
35
+ 'claude-code-termux-v1',
36
+ ].join(':');
37
+
38
+ return crypto.createHash('sha256').update(keyMaterial).digest();
39
+ }
40
+
41
+ /**
42
+ * Encrypt data for storage
43
+ */
44
+ function encrypt(data) {
45
+ const key = getEncryptionKey();
46
+ const iv = crypto.randomBytes(16);
47
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
48
+
49
+ let encrypted = cipher.update(JSON.stringify(data), 'utf8', 'hex');
50
+ encrypted += cipher.final('hex');
51
+
52
+ const authTag = cipher.getAuthTag();
53
+
54
+ return JSON.stringify({
55
+ iv: iv.toString('hex'),
56
+ authTag: authTag.toString('hex'),
57
+ data: encrypted,
58
+ });
59
+ }
60
+
61
+ /**
62
+ * Decrypt stored data
63
+ */
64
+ function decrypt(encryptedData) {
65
+ try {
66
+ const parsed = JSON.parse(encryptedData);
67
+ const key = getEncryptionKey();
68
+ const iv = Buffer.from(parsed.iv, 'hex');
69
+ const authTag = Buffer.from(parsed.authTag, 'hex');
70
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
71
+ decipher.setAuthTag(authTag);
72
+
73
+ let decrypted = decipher.update(parsed.data, 'hex', 'utf8');
74
+ decrypted += decipher.final('utf8');
75
+
76
+ return JSON.parse(decrypted);
77
+ } catch (err) {
78
+ return null;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Store OAuth tokens
84
+ */
85
+ function storeTokens(tokens) {
86
+ const dir = path.dirname(TOKEN_FILE);
87
+ if (!fs.existsSync(dir)) {
88
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
89
+ }
90
+
91
+ const encrypted = encrypt(tokens);
92
+ fs.writeFileSync(TOKEN_FILE, encrypted, { mode: 0o600 });
93
+ }
94
+
95
+ /**
96
+ * Retrieve OAuth tokens
97
+ */
98
+ function getTokens() {
99
+ if (!fs.existsSync(TOKEN_FILE)) {
100
+ return null;
101
+ }
102
+
103
+ try {
104
+ const encrypted = fs.readFileSync(TOKEN_FILE, 'utf8');
105
+ return decrypt(encrypted);
106
+ } catch (err) {
107
+ return null;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Clear stored OAuth tokens
113
+ */
114
+ function clearTokens() {
115
+ if (fs.existsSync(TOKEN_FILE)) {
116
+ fs.unlinkSync(TOKEN_FILE);
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Check if tokens are stored
122
+ */
123
+ function hasTokens() {
124
+ return fs.existsSync(TOKEN_FILE);
125
+ }
126
+
127
+ /**
128
+ * Apply the OAuth storage patch
129
+ */
130
+ function apply() {
131
+ if (!isTermux) {
132
+ return;
133
+ }
134
+
135
+ // Export functions globally for Claude Code to potentially use
136
+ global.__claudeCodeTermuxOAuth = {
137
+ storeTokens,
138
+ getTokens,
139
+ clearTokens,
140
+ hasTokens,
141
+ };
142
+
143
+ // Note: The actual integration with Claude Code's auth system
144
+ // requires patching the obfuscated cli.mjs, which is complex.
145
+ // For now, we recommend using API key authentication:
146
+ // export ANTHROPIC_API_KEY=your-key
147
+
148
+ if (process.env.DEBUG) {
149
+ console.log('[claude-code-termux] OAuth fallback storage initialized');
150
+ console.log('[claude-code-termux] Token file:', TOKEN_FILE);
151
+ console.log('[claude-code-termux] Has stored tokens:', hasTokens());
152
+ }
153
+ }
154
+
155
+ module.exports = {
156
+ apply,
157
+ storeTokens,
158
+ getTokens,
159
+ clearTokens,
160
+ hasTokens,
161
+ encrypt,
162
+ decrypt,
163
+ };
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Path Normalization Patch
3
+ *
4
+ * This patch fixes path resolution issues on Termux where:
5
+ * - Home directory is at /data/data/com.termux/files/home
6
+ * - Glob patterns may not work correctly with these paths
7
+ * - Custom slash commands in .claude/commands/ are not discovered
8
+ *
9
+ * Issue: https://github.com/anthropics/claude-code/issues/9435
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ const path = require('path');
15
+ const fs = require('fs');
16
+ const os = require('os');
17
+
18
+ // Detect Termux environment
19
+ const isTermux = process.platform === 'android' ||
20
+ process.env.PREFIX?.includes('com.termux') ||
21
+ process.env.HOME?.includes('com.termux');
22
+
23
+ // Termux-specific paths
24
+ const TERMUX_HOME = '/data/data/com.termux/files/home';
25
+ const TERMUX_PREFIX = '/data/data/com.termux/files/usr';
26
+
27
+ /**
28
+ * Get the correct home directory for Termux
29
+ */
30
+ function getHomeDir() {
31
+ if (isTermux) {
32
+ return process.env.HOME || TERMUX_HOME;
33
+ }
34
+ return os.homedir();
35
+ }
36
+
37
+ /**
38
+ * Normalize a path for Termux compatibility
39
+ */
40
+ function normalizePath(inputPath) {
41
+ if (!inputPath || !isTermux) {
42
+ return inputPath;
43
+ }
44
+
45
+ let normalized = inputPath;
46
+
47
+ // Replace ~ with actual home directory
48
+ if (normalized.startsWith('~')) {
49
+ normalized = normalized.replace(/^~/, getHomeDir());
50
+ }
51
+
52
+ // Ensure path is absolute
53
+ if (!path.isAbsolute(normalized)) {
54
+ normalized = path.resolve(process.cwd(), normalized);
55
+ }
56
+
57
+ return normalized;
58
+ }
59
+
60
+ /**
61
+ * Get paths for Claude configuration directories
62
+ */
63
+ function getClaudePaths() {
64
+ const home = getHomeDir();
65
+ return {
66
+ home,
67
+ claudeDir: path.join(home, '.claude'),
68
+ commandsDir: path.join(home, '.claude', 'commands'),
69
+ agentsDir: path.join(home, '.claude', 'agents'),
70
+ configFile: path.join(home, '.claude', 'config.json'),
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Ensure all required Claude directories exist
76
+ */
77
+ function ensureClaudeDirs() {
78
+ const paths = getClaudePaths();
79
+
80
+ const dirs = [paths.claudeDir, paths.commandsDir, paths.agentsDir];
81
+
82
+ for (const dir of dirs) {
83
+ if (!fs.existsSync(dir)) {
84
+ try {
85
+ fs.mkdirSync(dir, { recursive: true, mode: 0o755 });
86
+ } catch (err) {
87
+ // Directory might already exist or we don't have permission
88
+ }
89
+ }
90
+ }
91
+
92
+ return paths;
93
+ }
94
+
95
+ /**
96
+ * Apply the path normalization patches
97
+ */
98
+ function apply() {
99
+ if (!isTermux) {
100
+ return;
101
+ }
102
+
103
+ // Ensure Claude directories exist
104
+ const paths = ensureClaudeDirs();
105
+
106
+ // Override os.homedir to return Termux home
107
+ const originalHomedir = os.homedir;
108
+ os.homedir = function() {
109
+ if (isTermux) {
110
+ return getHomeDir();
111
+ }
112
+ return originalHomedir();
113
+ };
114
+
115
+ // Set environment variables that Claude Code might use
116
+ process.env.HOME = getHomeDir();
117
+ process.env.CLAUDE_CONFIG_DIR = paths.claudeDir;
118
+
119
+ // Log for debugging
120
+ if (process.env.DEBUG) {
121
+ console.log('[claude-code-termux] Path normalization applied');
122
+ console.log('[claude-code-termux] HOME:', process.env.HOME);
123
+ console.log('[claude-code-termux] Claude dir:', paths.claudeDir);
124
+ }
125
+ }
126
+
127
+ module.exports = {
128
+ apply,
129
+ getHomeDir,
130
+ normalizePath,
131
+ getClaudePaths,
132
+ ensureClaudeDirs,
133
+ isTermux,
134
+ };