termux-dev 1.4.12 → 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.
@@ -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) {
@@ -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.
@@ -27,9 +27,10 @@ function loadPricing() {
27
27
  }
28
28
  export function getModelPricing(modelName) {
29
29
  const clean = (modelName || '').trim().toLowerCase();
30
- if (clean.includes(':free') ||
30
+ if (clean.endsWith(':free') ||
31
31
  clean.includes('/free') ||
32
- clean.includes('free') ||
32
+ clean === 'free' ||
33
+ clean.includes('free-models/') ||
33
34
  clean.includes('stealth') ||
34
35
  clean.includes('local') ||
35
36
  clean.includes('ollama') ||
@@ -63,7 +63,7 @@ export class MCPClient {
63
63
  capabilities: {},
64
64
  clientInfo: {
65
65
  name: 'devx',
66
- version: '1.4.12'
66
+ version: '1.4.14'
67
67
  }
68
68
  });
69
69
  if (!initResult) {
@@ -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
- const allowed = await p.confirm({
153
- message: isDangerous ? pc.red('Confirm executing this dangerous command?') : 'Allow execution?',
154
- initialValue: !isDangerous
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
  }
@@ -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
- const text = delta.content;
247
- if (text.includes('<think>')) {
248
- inThinkTag = true;
249
- const parts = text.split('<think>');
250
- if (parts[0]) {
251
- accumulatedContent += parts[0];
252
- yield { type: 'content_delta', delta: parts[0] };
253
- }
254
- if (parts[1]) {
255
- accumulatedReasoning += parts[1];
256
- yield { type: 'reasoning_delta', delta: parts[1] };
257
- }
258
- }
259
- else if (text.includes('</think>')) {
260
- inThinkTag = false;
261
- const parts = text.split('</think>');
262
- if (parts[0]) {
263
- accumulatedReasoning += parts[0];
264
- yield { type: 'reasoning_delta', delta: parts[0] };
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
- if (parts[1]) {
267
- accumulatedContent += parts[1];
268
- yield { type: 'content_delta', delta: parts[1] };
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 {
@@ -18,11 +18,23 @@ export const bashTool = {
18
18
  },
19
19
  async execute(args) {
20
20
  return new Promise((resolve, reject) => {
21
- const proc = spawn(args.command, { shell: true });
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
- proc.kill();
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
- proc.kill();
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
- proc.kill();
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 ${args.file}`;
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 ${args.file}`;
30
+ cmd = `node --check ${quotedFile}`;
30
31
  }
31
32
  else if (args.file.endsWith('.py')) {
32
- cmd = `python -m py_compile ${args.file}`;
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 newContent = content.slice(0, targetIndex) + args.replacement + content.slice(targetIndex + target.length);
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
- return files.map(f => `${f.isDirectory() ? '[DIR]' : '[FILE]'} ${f.name}`).join('\n');
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}`);
@@ -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
- cmd = `pip install ${args.package}`;
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}`;
@@ -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
- for (let i = 0; i < total; i++) {
46
- const q = args.questions[i];
47
- const stepHeader = pc.cyan(`‹ ${i + 1} of ${total} ›`);
48
- const messageTitle = `${pc.bold(q.question)} ${stepHeader}`;
49
- const choices = q.options.map((opt, idx) => ({
50
- name: `${pc.cyan(String(idx + 1))} ${opt}`,
51
- value: opt
52
- }));
53
- if (q.allowCustom !== false) {
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('✏️ Type custom answer...')}`,
56
- value: '__custom__'
69
+ name: `${pc.dim('⏭️ Skip')}`,
70
+ value: '__skip__'
57
71
  });
58
- }
59
- choices.push({
60
- name: `${pc.dim('⏭️ Skip')}`,
61
- value: '__skip__'
62
- });
63
- try {
64
- let answer = await select({
65
- message: messageTitle,
66
- choices,
67
- pageSize: Math.min(8, choices.length)
68
- });
69
- if (answer === '__custom__') {
70
- const customAns = await input({
71
- message: pc.cyan('Your custom answer:'),
72
- validate: (v) => v.trim().length > 0 || 'Answer cannot be empty'
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
- else if (answer === '__skip__') {
77
- answer = '(Skipped)';
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
- catch {
85
- results.push({
86
- question: q.question,
87
- answer: '(Skipped)'
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.12",
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"