termux-dev 1.4.13 → 1.4.14
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/assets/banner.svg +1 -1
- package/dist/cli/clipboard.js +10 -1
- package/dist/cli/doctor.js +1 -1
- package/dist/cli/files.js +1 -1
- package/dist/cli/headless.js +1 -1
- package/dist/cli/index.js +34 -7
- package/dist/cli/prompt.js +10 -3
- package/dist/cli/server.js +13 -10
- package/dist/core/commands.js +1 -1
- package/dist/core/history.js +23 -5
- package/dist/core/loop.js +13 -0
- package/dist/core/model-limits.json +6543 -0
- package/dist/core/model-pricing.json +26166 -0
- package/dist/core/models.js +13 -1
- package/dist/core/notify.js +15 -1
- package/dist/core/pricing.js +3 -2
- package/dist/mcp/client.js +1 -1
- package/dist/permissions/guard.js +19 -4
- package/dist/providers/openai.js +69 -30
- package/dist/tools/bash.js +16 -4
- package/dist/tools/diagnostics.js +4 -3
- package/dist/tools/fs.js +11 -2
- package/dist/tools/packages.js +3 -1
- package/dist/tools/questions.js +55 -38
- package/package.json +2 -2
package/dist/core/models.js
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
|
+
const FALLBACK_LIMITS = {
|
|
5
|
+
'claude-3.5-sonnet': 200000,
|
|
6
|
+
'claude-3-opus': 200000,
|
|
7
|
+
'gemini-2.5-flash': 1048576,
|
|
8
|
+
'gemini-2.5-pro': 1048576,
|
|
9
|
+
'gemini-2.0-flash': 1048576,
|
|
10
|
+
'gpt-4o': 128000,
|
|
11
|
+
'gpt-4o-mini': 128000,
|
|
12
|
+
'deepseek-chat': 128000,
|
|
13
|
+
'deepseek-r1': 128000,
|
|
14
|
+
'qwen-2.5-coder': 131072,
|
|
15
|
+
};
|
|
4
16
|
let limitsCache = null;
|
|
5
17
|
function loadLimits() {
|
|
6
18
|
if (limitsCache)
|
|
@@ -22,7 +34,7 @@ function loadLimits() {
|
|
|
22
34
|
}
|
|
23
35
|
}
|
|
24
36
|
catch { }
|
|
25
|
-
limitsCache = {};
|
|
37
|
+
limitsCache = { ...FALLBACK_LIMITS };
|
|
26
38
|
return limitsCache;
|
|
27
39
|
}
|
|
28
40
|
export function getModelContextLimit(modelName) {
|
package/dist/core/notify.js
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
2
|
export function isTermux() {
|
|
3
|
-
return !!process.env.PREFIX?.includes('com.termux');
|
|
3
|
+
return !!process.env.TERMUX_VERSION || !!process.env.PREFIX?.includes('com.termux');
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Acquires a Termux CPU wake lock to prevent Android from putting the process to sleep
|
|
7
|
+
* when Termux is in the background or the screen is turned off.
|
|
8
|
+
*/
|
|
9
|
+
export function acquireWakeLock() {
|
|
10
|
+
if (isTermux()) {
|
|
11
|
+
try {
|
|
12
|
+
const proc = spawn('termux-wake-lock', [], { stdio: 'ignore', detached: true });
|
|
13
|
+
proc.on('error', () => { });
|
|
14
|
+
proc.unref();
|
|
15
|
+
}
|
|
16
|
+
catch { }
|
|
17
|
+
}
|
|
4
18
|
}
|
|
5
19
|
/**
|
|
6
20
|
* Sends a notification and haptic vibration to the device.
|
package/dist/core/pricing.js
CHANGED
|
@@ -27,9 +27,10 @@ function loadPricing() {
|
|
|
27
27
|
}
|
|
28
28
|
export function getModelPricing(modelName) {
|
|
29
29
|
const clean = (modelName || '').trim().toLowerCase();
|
|
30
|
-
if (clean.
|
|
30
|
+
if (clean.endsWith(':free') ||
|
|
31
31
|
clean.includes('/free') ||
|
|
32
|
-
clean
|
|
32
|
+
clean === 'free' ||
|
|
33
|
+
clean.includes('free-models/') ||
|
|
33
34
|
clean.includes('stealth') ||
|
|
34
35
|
clean.includes('local') ||
|
|
35
36
|
clean.includes('ollama') ||
|
package/dist/mcp/client.js
CHANGED
|
@@ -149,10 +149,25 @@ export class CLIConsoleGuard {
|
|
|
149
149
|
else {
|
|
150
150
|
console.log(` ${pc.dim('Arguments:')} ${JSON.stringify(args, null, 2)}`);
|
|
151
151
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
152
|
+
// Pause raw mode for interactive prompt
|
|
153
|
+
const wasRaw = process.stdin.isTTY && process.stdin.isRaw;
|
|
154
|
+
if (wasRaw) {
|
|
155
|
+
process.stdin.setRawMode(false);
|
|
156
|
+
}
|
|
157
|
+
let allowed = false;
|
|
158
|
+
try {
|
|
159
|
+
allowed = await p.confirm({
|
|
160
|
+
message: isDangerous ? pc.red('Confirm executing this dangerous command?') : 'Allow execution?',
|
|
161
|
+
initialValue: !isDangerous
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
// Restore raw mode
|
|
166
|
+
if (wasRaw && process.stdin.isTTY) {
|
|
167
|
+
process.stdin.setRawMode(true);
|
|
168
|
+
process.stdin.resume();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
156
171
|
return allowed === true;
|
|
157
172
|
}
|
|
158
173
|
}
|
package/dist/providers/openai.js
CHANGED
|
@@ -200,6 +200,7 @@ export class OpenAIProvider {
|
|
|
200
200
|
let accumulatedContent = '';
|
|
201
201
|
let accumulatedReasoning = '';
|
|
202
202
|
let inThinkTag = false;
|
|
203
|
+
let thinkBuffer = '';
|
|
203
204
|
const toolMap = new Map();
|
|
204
205
|
let usageData = null;
|
|
205
206
|
try {
|
|
@@ -243,39 +244,67 @@ export class OpenAIProvider {
|
|
|
243
244
|
}
|
|
244
245
|
// 2. Check content (and handle <think> tags if embedded in content)
|
|
245
246
|
if (delta.content) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
inThinkTag
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
247
|
+
thinkBuffer += delta.content;
|
|
248
|
+
while (thinkBuffer.length > 0) {
|
|
249
|
+
if (inThinkTag) {
|
|
250
|
+
const closeIdx = thinkBuffer.indexOf('</think>');
|
|
251
|
+
if (closeIdx !== -1) {
|
|
252
|
+
const reasoning = thinkBuffer.slice(0, closeIdx);
|
|
253
|
+
if (reasoning) {
|
|
254
|
+
accumulatedReasoning += reasoning;
|
|
255
|
+
yield { type: 'reasoning_delta', delta: reasoning };
|
|
256
|
+
}
|
|
257
|
+
inThinkTag = false;
|
|
258
|
+
thinkBuffer = thinkBuffer.slice(closeIdx + 8);
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
const partialIdx = thinkBuffer.lastIndexOf('<');
|
|
262
|
+
if (partialIdx !== -1 && thinkBuffer.length - partialIdx < 8) {
|
|
263
|
+
const reasoning = thinkBuffer.slice(0, partialIdx);
|
|
264
|
+
if (reasoning) {
|
|
265
|
+
accumulatedReasoning += reasoning;
|
|
266
|
+
yield { type: 'reasoning_delta', delta: reasoning };
|
|
267
|
+
}
|
|
268
|
+
thinkBuffer = thinkBuffer.slice(partialIdx);
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
accumulatedReasoning += thinkBuffer;
|
|
273
|
+
yield { type: 'reasoning_delta', delta: thinkBuffer };
|
|
274
|
+
thinkBuffer = '';
|
|
275
|
+
}
|
|
276
|
+
}
|
|
265
277
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
278
|
+
else {
|
|
279
|
+
const openIdx = thinkBuffer.indexOf('<think>');
|
|
280
|
+
if (openIdx !== -1) {
|
|
281
|
+
const content = thinkBuffer.slice(0, openIdx);
|
|
282
|
+
if (content) {
|
|
283
|
+
accumulatedContent += content;
|
|
284
|
+
yield { type: 'content_delta', delta: content };
|
|
285
|
+
}
|
|
286
|
+
inThinkTag = true;
|
|
287
|
+
thinkBuffer = thinkBuffer.slice(openIdx + 7);
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
const partialIdx = thinkBuffer.lastIndexOf('<');
|
|
291
|
+
if (partialIdx !== -1 && thinkBuffer.length - partialIdx < 8) {
|
|
292
|
+
const content = thinkBuffer.slice(0, partialIdx);
|
|
293
|
+
if (content) {
|
|
294
|
+
accumulatedContent += content;
|
|
295
|
+
yield { type: 'content_delta', delta: content };
|
|
296
|
+
}
|
|
297
|
+
thinkBuffer = thinkBuffer.slice(partialIdx);
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
accumulatedContent += thinkBuffer;
|
|
302
|
+
yield { type: 'content_delta', delta: thinkBuffer };
|
|
303
|
+
thinkBuffer = '';
|
|
304
|
+
}
|
|
305
|
+
}
|
|
269
306
|
}
|
|
270
307
|
}
|
|
271
|
-
else if (inThinkTag) {
|
|
272
|
-
accumulatedReasoning += text;
|
|
273
|
-
yield { type: 'reasoning_delta', delta: text };
|
|
274
|
-
}
|
|
275
|
-
else {
|
|
276
|
-
accumulatedContent += text;
|
|
277
|
-
yield { type: 'content_delta', delta: text };
|
|
278
|
-
}
|
|
279
308
|
}
|
|
280
309
|
// 3. Accumulate tool calls and yield live progress
|
|
281
310
|
if (delta.tool_calls) {
|
|
@@ -331,6 +360,16 @@ export class OpenAIProvider {
|
|
|
331
360
|
finally {
|
|
332
361
|
reader.releaseLock();
|
|
333
362
|
}
|
|
363
|
+
if (thinkBuffer.length > 0) {
|
|
364
|
+
if (inThinkTag) {
|
|
365
|
+
accumulatedReasoning += thinkBuffer;
|
|
366
|
+
yield { type: 'reasoning_delta', delta: thinkBuffer };
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
accumulatedContent += thinkBuffer;
|
|
370
|
+
yield { type: 'content_delta', delta: thinkBuffer };
|
|
371
|
+
}
|
|
372
|
+
}
|
|
334
373
|
const toolCalls = Array.from(toolMap.values()).map(tc => {
|
|
335
374
|
let parsedArgs = {};
|
|
336
375
|
try {
|
package/dist/tools/bash.js
CHANGED
|
@@ -18,11 +18,23 @@ export const bashTool = {
|
|
|
18
18
|
},
|
|
19
19
|
async execute(args) {
|
|
20
20
|
return new Promise((resolve, reject) => {
|
|
21
|
-
const
|
|
21
|
+
const isWin = process.platform === 'win32';
|
|
22
|
+
const proc = spawn(args.command, { shell: true, detached: !isWin });
|
|
22
23
|
let output = '';
|
|
23
24
|
let isTruncated = false;
|
|
25
|
+
const killProc = () => {
|
|
26
|
+
try {
|
|
27
|
+
if (!isWin && proc.pid) {
|
|
28
|
+
process.kill(-proc.pid, 'SIGTERM');
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
proc.kill();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch { }
|
|
35
|
+
};
|
|
24
36
|
const timeout = setTimeout(() => {
|
|
25
|
-
|
|
37
|
+
killProc();
|
|
26
38
|
resolve(output + '\n[Process killed due to timeout]');
|
|
27
39
|
}, 30000);
|
|
28
40
|
proc.stdout.on('data', (data) => {
|
|
@@ -32,7 +44,7 @@ export const bashTool = {
|
|
|
32
44
|
if (output.length > 20000) {
|
|
33
45
|
isTruncated = true;
|
|
34
46
|
output = output.substring(0, 20000) + '\n[Output truncated]';
|
|
35
|
-
|
|
47
|
+
killProc();
|
|
36
48
|
}
|
|
37
49
|
});
|
|
38
50
|
proc.stderr.on('data', (data) => {
|
|
@@ -42,7 +54,7 @@ export const bashTool = {
|
|
|
42
54
|
if (output.length > 20000) {
|
|
43
55
|
isTruncated = true;
|
|
44
56
|
output = output.substring(0, 20000) + '\n[Output truncated]';
|
|
45
|
-
|
|
57
|
+
killProc();
|
|
46
58
|
}
|
|
47
59
|
});
|
|
48
60
|
proc.on('close', (code) => {
|
|
@@ -17,19 +17,20 @@ export const diagnoseCodeTool = {
|
|
|
17
17
|
return new Promise((resolve) => {
|
|
18
18
|
let cmd = '';
|
|
19
19
|
if (args.file) {
|
|
20
|
+
const quotedFile = `"${args.file}"`;
|
|
20
21
|
if (args.file.endsWith('.ts') || args.file.endsWith('.tsx')) {
|
|
21
22
|
if (fsSync.existsSync('tsconfig.json')) {
|
|
22
23
|
cmd = 'npx tsc --noEmit';
|
|
23
24
|
}
|
|
24
25
|
else {
|
|
25
|
-
cmd = `node --check
|
|
26
|
+
cmd = `echo "node --check does not work for TypeScript. Please use tsc or tsx."`;
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
else if (args.file.endsWith('.js') || args.file.endsWith('.mjs') || args.file.endsWith('.cjs')) {
|
|
29
|
-
cmd = `node --check ${
|
|
30
|
+
cmd = `node --check ${quotedFile}`;
|
|
30
31
|
}
|
|
31
32
|
else if (args.file.endsWith('.py')) {
|
|
32
|
-
cmd = `python -m py_compile ${
|
|
33
|
+
cmd = `python -m py_compile ${quotedFile}`;
|
|
33
34
|
}
|
|
34
35
|
else if (args.file.endsWith('.rs')) {
|
|
35
36
|
cmd = 'cargo check';
|
package/dist/tools/fs.js
CHANGED
|
@@ -153,7 +153,14 @@ export const editFileTool = {
|
|
|
153
153
|
let addCounter = startLine;
|
|
154
154
|
addedArr.forEach((l) => diffLines.push(`${addCounter++} + ${l}`));
|
|
155
155
|
contextAfter.forEach((l) => diffLines.push(`${addCounter++} ${l}`));
|
|
156
|
-
const
|
|
156
|
+
const hasCRLF = oldContent.includes('\r\n');
|
|
157
|
+
let newContent = content.slice(0, targetIndex) + args.replacement + content.slice(targetIndex + target.length);
|
|
158
|
+
if (hasCRLF) {
|
|
159
|
+
newContent = newContent.replace(/\r?\n/g, '\r\n');
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
newContent = newContent.replace(/\r\n/g, '\n');
|
|
163
|
+
}
|
|
157
164
|
await fs.writeFile(args.path, newContent, 'utf8');
|
|
158
165
|
return JSON.stringify({
|
|
159
166
|
status: 'success',
|
|
@@ -191,7 +198,9 @@ export const listDirTool = {
|
|
|
191
198
|
async execute(args) {
|
|
192
199
|
try {
|
|
193
200
|
const files = await fs.readdir(args.path, { withFileTypes: true });
|
|
194
|
-
|
|
201
|
+
const ignores = ['node_modules', '.git', '.next', '.cache', 'dist'];
|
|
202
|
+
const filtered = files.filter(f => !ignores.includes(f.name));
|
|
203
|
+
return filtered.map(f => `${f.isDirectory() ? '[DIR]' : '[FILE]'} ${f.name}`).join('\n');
|
|
195
204
|
}
|
|
196
205
|
catch (err) {
|
|
197
206
|
throw new Error(`Failed to list directory: ${err.message}`);
|
package/dist/tools/packages.js
CHANGED
|
@@ -51,7 +51,9 @@ export const installPackageTool = {
|
|
|
51
51
|
cmd = `pnpm add ${args.dev ? '-D ' : ''}${args.package}`;
|
|
52
52
|
}
|
|
53
53
|
else if (pkgManager === 'pip') {
|
|
54
|
-
|
|
54
|
+
const isUnix = process.platform !== 'win32';
|
|
55
|
+
const extraFlag = isUnix ? ' --break-system-packages' : '';
|
|
56
|
+
cmd = `pip install ${args.package}${extraFlag}`;
|
|
55
57
|
}
|
|
56
58
|
else if (pkgManager === 'cargo') {
|
|
57
59
|
cmd = `cargo add ${args.package}`;
|
package/dist/tools/questions.js
CHANGED
|
@@ -36,56 +36,73 @@ export const askQuestionsTool = {
|
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
execute: async (args) => {
|
|
39
|
+
if (!process.stdin.isTTY) {
|
|
40
|
+
return JSON.stringify({ note: 'Questions skipped in non-interactive mode', answers: {} });
|
|
41
|
+
}
|
|
39
42
|
if (!args.questions || !Array.isArray(args.questions) || args.questions.length === 0) {
|
|
40
43
|
return 'No questions provided.';
|
|
41
44
|
}
|
|
42
45
|
const results = [];
|
|
43
46
|
const total = args.questions.length;
|
|
44
47
|
console.log();
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
// Pause raw mode for interactive prompt
|
|
49
|
+
const wasRaw = process.stdin.isTTY && process.stdin.isRaw;
|
|
50
|
+
if (wasRaw) {
|
|
51
|
+
process.stdin.setRawMode(false);
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
for (let i = 0; i < total; i++) {
|
|
55
|
+
const q = args.questions[i];
|
|
56
|
+
const stepHeader = pc.cyan(`‹ ${i + 1} of ${total} ›`);
|
|
57
|
+
const messageTitle = `${pc.bold(q.question)} ${stepHeader}`;
|
|
58
|
+
const choices = q.options.map((opt, idx) => ({
|
|
59
|
+
name: `${pc.cyan(String(idx + 1))} ${opt}`,
|
|
60
|
+
value: opt
|
|
61
|
+
}));
|
|
62
|
+
if (q.allowCustom !== false) {
|
|
63
|
+
choices.push({
|
|
64
|
+
name: `${pc.dim('✏️ Type custom answer...')}`,
|
|
65
|
+
value: '__custom__'
|
|
66
|
+
});
|
|
67
|
+
}
|
|
54
68
|
choices.push({
|
|
55
|
-
name: `${pc.dim('
|
|
56
|
-
value: '
|
|
69
|
+
name: `${pc.dim('⏭️ Skip')}`,
|
|
70
|
+
value: '__skip__'
|
|
57
71
|
});
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
try {
|
|
73
|
+
let answer = await select({
|
|
74
|
+
message: messageTitle,
|
|
75
|
+
choices,
|
|
76
|
+
pageSize: Math.min(8, choices.length)
|
|
77
|
+
});
|
|
78
|
+
if (answer === '__custom__') {
|
|
79
|
+
const customAns = await input({
|
|
80
|
+
message: pc.cyan('Your custom answer:'),
|
|
81
|
+
validate: (v) => v.trim().length > 0 || 'Answer cannot be empty'
|
|
82
|
+
});
|
|
83
|
+
answer = customAns.trim();
|
|
84
|
+
}
|
|
85
|
+
else if (answer === '__skip__') {
|
|
86
|
+
answer = '(Skipped)';
|
|
87
|
+
}
|
|
88
|
+
results.push({
|
|
89
|
+
question: q.question,
|
|
90
|
+
answer
|
|
73
91
|
});
|
|
74
|
-
answer = customAns.trim();
|
|
75
92
|
}
|
|
76
|
-
|
|
77
|
-
|
|
93
|
+
catch {
|
|
94
|
+
results.push({
|
|
95
|
+
question: q.question,
|
|
96
|
+
answer: '(Skipped)'
|
|
97
|
+
});
|
|
78
98
|
}
|
|
79
|
-
results.push({
|
|
80
|
-
question: q.question,
|
|
81
|
-
answer
|
|
82
|
-
});
|
|
83
99
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
// Restore raw mode
|
|
103
|
+
if (wasRaw && process.stdin.isTTY) {
|
|
104
|
+
process.stdin.setRawMode(true);
|
|
105
|
+
process.stdin.resume();
|
|
89
106
|
}
|
|
90
107
|
}
|
|
91
108
|
// Print styled Q&A block in console
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "termux-dev",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.14",
|
|
4
4
|
"description": "Ultra-fast, terminal-native AI coding assistant and agent built for Android Termux, Windows, macOS, and Linux.",
|
|
5
5
|
"main": "dist/cli/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"assets"
|
|
19
19
|
],
|
|
20
20
|
"scripts": {
|
|
21
|
-
"build": "tsc",
|
|
21
|
+
"build": "tsc && node ./scripts/copy-assets.js",
|
|
22
22
|
"start": "node ./bin/devx.js",
|
|
23
23
|
"dev": "tsc --watch",
|
|
24
24
|
"prepublishOnly": "npm run build"
|