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.
- package/LICENSE +21 -0
- package/README.md +198 -0
- package/bin/claude-termux.js +80 -0
- package/docs/INSTALLATION.md +136 -0
- package/docs/KNOWN-ISSUES.md +135 -0
- package/docs/TROUBLESHOOTING.md +225 -0
- package/package.json +50 -0
- package/postinstall.js +204 -0
- package/scripts/download-ripgrep.sh +55 -0
- package/scripts/setup-termux.sh +120 -0
- package/scripts/verify-install.js +188 -0
- package/src/binaries/README.md +29 -0
- package/src/patches/apply-all.js +45 -0
- package/src/patches/hook-events.js +108 -0
- package/src/patches/oauth-storage.js +163 -0
- package/src/patches/path-normalization.js +134 -0
- package/src/patches/ripgrep-fallback.js +95 -0
- package/src/patches/sharp-fallback.js +65 -0
|
@@ -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
|
+
};
|