termux-dev 1.6.0 → 1.8.1

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.
Files changed (99) hide show
  1. package/README.md +214 -166
  2. package/assets/banner.svg +23 -28
  3. package/assets/preview.png +0 -0
  4. package/assets/preview.svg +101 -10
  5. package/dist/cli/banner.js +3 -4
  6. package/dist/cli/clipboard-write.js +55 -0
  7. package/dist/cli/clipboard.js +182 -106
  8. package/dist/cli/colors.js +67 -0
  9. package/dist/cli/confirm.js +50 -0
  10. package/dist/cli/doctor.js +4 -4
  11. package/dist/cli/effort.js +80 -0
  12. package/dist/cli/export.js +3 -3
  13. package/dist/cli/files.js +19 -5
  14. package/dist/cli/finder.js +241 -0
  15. package/dist/cli/headless.js +23 -11
  16. package/dist/cli/index.js +1557 -744
  17. package/dist/cli/keys.js +69 -0
  18. package/dist/cli/liveblock.js +45 -0
  19. package/dist/cli/log.js +19 -0
  20. package/dist/cli/logo.js +30 -0
  21. package/dist/cli/markdown.js +174 -37
  22. package/dist/cli/mcp-panel.js +432 -0
  23. package/dist/cli/permission.js +84 -0
  24. package/dist/cli/picker.js +143 -0
  25. package/dist/cli/prompt.js +256 -56
  26. package/dist/cli/resume.js +15 -0
  27. package/dist/cli/rewind.js +51 -0
  28. package/dist/cli/runtime_errors.js +6 -0
  29. package/dist/cli/server.js +85 -24
  30. package/dist/cli/settings.js +307 -0
  31. package/dist/cli/statusline.js +1 -1
  32. package/dist/cli/theme.js +126 -108
  33. package/dist/cli/transcript.js +134 -0
  34. package/dist/cli/typeahead.js +371 -0
  35. package/dist/cli/ui.js +1022 -0
  36. package/dist/cli/updater.js +23 -21
  37. package/dist/cli/usage-panel.js +242 -0
  38. package/dist/cli/welcome.js +56 -0
  39. package/dist/cli/width.js +178 -0
  40. package/dist/core/capabilities.js +433 -0
  41. package/dist/core/catalog-util.js +27 -0
  42. package/dist/core/checkpoint.js +90 -0
  43. package/dist/core/context-stats.js +29 -0
  44. package/dist/core/cost.js +191 -0
  45. package/dist/core/formats.js +199 -0
  46. package/dist/core/history.js +51 -1
  47. package/dist/core/hooks.js +79 -0
  48. package/dist/core/i18n.js +42 -0
  49. package/dist/core/local-config.js +69 -0
  50. package/dist/core/locales/de.js +265 -0
  51. package/dist/core/locales/en.js +276 -0
  52. package/dist/core/locales/es.js +265 -0
  53. package/dist/core/locales/fr.js +265 -0
  54. package/dist/core/locales/ja.js +265 -0
  55. package/dist/core/locales/pt.js +265 -0
  56. package/dist/core/locales/ru.js +265 -0
  57. package/dist/core/locales/uk.js +265 -0
  58. package/dist/core/locales/zh.js +265 -0
  59. package/dist/core/loop.js +125 -49
  60. package/dist/core/memory.js +39 -12
  61. package/dist/core/models.js +33 -20
  62. package/dist/core/network.js +12 -11
  63. package/dist/core/notify.js +54 -0
  64. package/dist/core/planfile.js +43 -0
  65. package/dist/core/price-catalog.json +1 -0
  66. package/dist/core/prices.js +249 -0
  67. package/dist/core/reasoning-catalog.json +1 -0
  68. package/dist/core/reasoning.js +393 -0
  69. package/dist/core/redact.js +41 -0
  70. package/dist/core/secure-file.js +22 -0
  71. package/dist/core/session.js +16 -5
  72. package/dist/core/snapshot.js +14 -2
  73. package/dist/core/title.js +61 -0
  74. package/dist/core/types.js +1 -1
  75. package/dist/core/usage.js +43 -7
  76. package/dist/core/vision-catalog.json +1 -0
  77. package/dist/core/vision.js +123 -0
  78. package/dist/mcp/client.js +36 -5
  79. package/dist/mcp/http-client.js +174 -0
  80. package/dist/mcp/manager.js +330 -93
  81. package/dist/permissions/guard.js +385 -117
  82. package/dist/prompts/builder.js +46 -38
  83. package/dist/providers/anthropic.js +517 -0
  84. package/dist/providers/index.js +18 -1
  85. package/dist/providers/openai.js +138 -125
  86. package/dist/providers/shared.js +116 -0
  87. package/dist/providers/toolguard.js +101 -0
  88. package/dist/tools/bash.js +141 -51
  89. package/dist/tools/diagnostics.js +76 -48
  90. package/dist/tools/fs.js +77 -5
  91. package/dist/tools/index.js +2 -1
  92. package/dist/tools/mcp.js +72 -0
  93. package/dist/tools/packages.js +16 -1
  94. package/dist/tools/plan.js +8 -8
  95. package/dist/tools/questions.js +50 -54
  96. package/dist/tools/server.js +6 -0
  97. package/dist/tools/todo.js +20 -18
  98. package/dist/tools/web.js +143 -8
  99. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { fileURLToPath } from 'url';
4
+ import { catalogContextWindow } from './vision.js';
4
5
  const FALLBACK_LIMITS = {
5
6
  'claude-3.5-sonnet': 200000,
6
7
  'claude-3-opus': 200000,
@@ -38,47 +39,59 @@ function loadLimits() {
38
39
  return limitsCache;
39
40
  }
40
41
  export function getModelContextLimit(modelName) {
42
+ return getModelContextInfo(modelName).limit;
43
+ }
44
+ /**
45
+ * The context window of a model and how sure we are. The models.dev catalogue is asked first
46
+ * (it knows the maker's own number for thousands of models and is refreshed with /effort refresh),
47
+ * then devx's table, then rules by name, and only then a default of 128k.
48
+ */
49
+ export function getModelContextInfo(modelName) {
50
+ const fromCatalogue = catalogContextWindow(modelName || '');
51
+ if (fromCatalogue)
52
+ return { limit: fromCatalogue, source: 'catalogue' };
53
+ const rule = (limit) => ({ limit, source: 'rule' });
41
54
  const cache = loadLimits();
42
55
  const clean = (modelName || '').trim().toLowerCase();
43
56
  const short = clean.split('/').pop() || clean;
44
57
  if (cache[clean])
45
- return cache[clean];
58
+ return { limit: cache[clean], source: 'table' };
46
59
  if (cache[short])
47
- return cache[short];
60
+ return { limit: cache[short], source: 'table' };
48
61
  const baseName = short.replace(/-\d{4,8}$/, '').replace(/:latest$/, '');
49
62
  if (cache[baseName])
50
- return cache[baseName];
63
+ return { limit: cache[baseName], source: 'table' };
51
64
  if (clean.includes('kimi-k2') || clean.includes('kimi'))
52
- return 256000;
65
+ return rule(256000);
53
66
  if (clean.includes('ox-alpha') || clean.includes('stealth'))
54
- return 1048576;
67
+ return rule(1048576);
55
68
  if (clean.includes('gemini-3') || clean.includes('gemini-2') || clean.includes('gemini-1.5') || clean.includes('gemini'))
56
- return 1048576;
69
+ return rule(1048576);
57
70
  if (clean.includes('claude-opus-4') || clean.includes('claude-4'))
58
- return 1000000;
71
+ return rule(1000000);
59
72
  if (clean.includes('claude-3-5') || clean.includes('claude-3.5') || clean.includes('claude-3-7') || clean.includes('claude-3.7') || clean.includes('claude'))
60
- return 200000;
73
+ return rule(200000);
61
74
  if (clean.includes('gpt-5'))
62
- return 1050000;
75
+ return rule(1050000);
63
76
  if (clean.includes('gpt-4o') || clean.includes('gpt-4-turbo') || clean.includes('o1') || clean.includes('o3'))
64
- return 128000;
77
+ return rule(128000);
65
78
  if (clean.includes('deepseek-v4') || clean.includes('deepseek-v3') || clean.includes('deepseek-r1') || clean.includes('deepseek'))
66
- return 128000;
79
+ return rule(128000);
67
80
  if (clean.includes('qwen3.5') || clean.includes('qwen-3.5'))
68
- return 262144;
81
+ return rule(262144);
69
82
  if (clean.includes('qwen2.5') || clean.includes('qwen-2.5') || clean.includes('qwen'))
70
- return 131072;
83
+ return rule(131072);
71
84
  if (clean.includes('glm-5') || clean.includes('glm-4.7') || clean.includes('glm-4.6'))
72
- return 200000;
85
+ return rule(200000);
73
86
  if (clean.includes('glm-4'))
74
- return 128000;
87
+ return rule(128000);
75
88
  if (clean.includes('minimax-m1') || clean.includes('minimax'))
76
- return 1000000;
89
+ return rule(1000000);
77
90
  if (clean.includes('doubao'))
78
- return 256000;
91
+ return rule(256000);
79
92
  if (clean.includes('llama-3.3') || clean.includes('llama-3.1') || clean.includes('llama-3'))
80
- return 128000;
93
+ return rule(128000);
81
94
  if (clean.includes('mistral-large') || clean.includes('codestral'))
82
- return 128000;
83
- return 128000;
95
+ return rule(128000);
96
+ return { limit: 128000, source: 'default' };
84
97
  }
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Network Resilience & Error Utilities for devx
3
3
  */
4
+ import { t } from './i18n.js';
4
5
  export function isNetworkOrRetryableError(err) {
5
6
  if (!err)
6
7
  return false;
@@ -70,45 +71,45 @@ export function isNetworkOrRetryableError(err) {
70
71
  }
71
72
  export function getHumanReadableNetworkError(err) {
72
73
  if (!err)
73
- return 'Неизвестная ошибка сети (Network error)';
74
+ return t('net.unknown');
74
75
  const msg = (err.message || String(err)).toLowerCase();
75
76
  const code = (err.code || err.cause?.code || '').toLowerCase();
76
77
  const status = err.status || err.statusCode || 0;
77
78
  if (status === 429 || msg.includes('429') || msg.includes('rate limit')) {
78
- return 'Превышен лимит запросов API (Rate limit / 429)';
79
+ return t('net.rateLimit');
79
80
  }
80
81
  if (status === 502 || msg.includes('502') || msg.includes('bad gateway')) {
81
- return 'Шлюз провайдера временно недоступен (Bad Gateway / 502)';
82
+ return t('net.badGateway');
82
83
  }
83
84
  if (status === 503 || msg.includes('503') || msg.includes('service unavailable')) {
84
- return 'Сервер провайдера перегружен (Service Unavailable / 503)';
85
+ return t('net.unavailable');
85
86
  }
86
87
  if (status === 504 || msg.includes('504') || msg.includes('gateway timeout')) {
87
- return 'Таймаут шлюза провайдера (Gateway Timeout / 504)';
88
+ return t('net.gatewayTimeout');
88
89
  }
89
90
  if (code.includes('enotfound') || msg.includes('enotfound') || code.includes('eai_again')) {
90
- return 'Нет подключения к сети или DNS (Host not found / Offline)';
91
+ return t('net.offline');
91
92
  }
92
93
  if (code.includes('econnreset') ||
93
94
  msg.includes('econnreset') ||
94
95
  msg.includes('connection reset') ||
95
96
  msg.includes('socket hang up') ||
96
97
  msg.includes('terminated')) {
97
- return 'Обрыв соединения с сервером (Connection reset / Socket closed)';
98
+ return t('net.reset');
98
99
  }
99
100
  if (code.includes('etimedout') ||
100
101
  msg.includes('timeout') ||
101
102
  msg.includes('time out') ||
102
103
  code.includes('und_err_connect_timeout')) {
103
- return 'Таймаут сетевого соединения (Network timeout)';
104
+ return t('net.timeout');
104
105
  }
105
106
  if (code.includes('econnrefused') || msg.includes('connection refused')) {
106
- return 'Сервер отклонил подключение (Connection refused)';
107
+ return t('net.refused');
107
108
  }
108
109
  if (msg.includes('fetch failed')) {
109
- return 'Сбой сетевого соединения (Fetch failed / Connection dropped)';
110
+ return t('net.fetchFailed');
110
111
  }
111
- return err.message || 'Сбой интернет-соединения';
112
+ return err.message || t('net.generic');
112
113
  }
113
114
  export function waitDelay(ms, signal) {
114
115
  return new Promise((resolve) => {
@@ -52,3 +52,57 @@ export function notifyDevice(title, message, options = { vibrate: true, sound: t
52
52
  }
53
53
  }
54
54
  }
55
+ // ───────────────────────── desktop notification when the terminal is not in front ─────────────────────────
56
+ /** The start of an answer as plain text for a notification: no markdown, code or links, cut at a word. */
57
+ export function notificationBody(markdown, max = 160) {
58
+ let text = String(markdown ?? '')
59
+ .replace(/```[\s\S]*?(```|$)/g, ' ')
60
+ .replace(/`([^`]*)`/g, '$1')
61
+ .replace(/!?\[([^\]]*)\]\([^)]*\)/g, '$1')
62
+ .replace(/^\s{0,3}(#{1,6}|>|[-*+]|\d+[.)])\s+/gm, '')
63
+ .replace(/[*_~]{1,3}([^*_~\n]+)[*_~]{1,3}/g, '$1')
64
+ .replace(/[\x00-\x1f\x7f]/g, ' ')
65
+ .replace(/\s+/g, ' ')
66
+ .trim();
67
+ if (Array.from(text).length <= max)
68
+ return text;
69
+ const cut = Array.from(text).slice(0, max).join('');
70
+ text = cut.replace(/\s+\S*$/, '') || cut;
71
+ return text.replace(/[\s,;:.\-]+$/, '') + '…';
72
+ }
73
+ /** kitty's own notification protocol (OSC 99): a title, then the body of the same notification. */
74
+ export function kittyNotificationSequence(title, body) {
75
+ const clean = (s) => s.replace(/[\x00-\x1f\x7f]/g, ' ');
76
+ return `\x1b]99;i=devx:d=0;${clean(title)}\x1b\\\x1b]99;i=devx:p=body;${clean(body)}\x1b\\`;
77
+ }
78
+ /**
79
+ * Shows a notification on the desktop: through kitty's protocol in kitty, `notify-send` on Linux,
80
+ * `osascript` on macOS. Returns which way was used, or 'none'. Never throws.
81
+ */
82
+ export function sendDesktopNotification(title, body, io = {}) {
83
+ const platform = io.platform ?? process.platform;
84
+ const env = io.env ?? process.env;
85
+ const write = io.write ?? ((text) => process.stdout.write(text));
86
+ const run = io.run ?? ((command, args) => {
87
+ const child = spawn(command, args, { stdio: 'ignore', detached: true });
88
+ child.on('error', () => { });
89
+ child.unref();
90
+ });
91
+ try {
92
+ if (env.KITTY_WINDOW_ID) {
93
+ write(kittyNotificationSequence(title, body));
94
+ return 'kitty';
95
+ }
96
+ if (platform === 'linux') {
97
+ run('notify-send', ['--app-name=devx', title, body]);
98
+ return 'notify-send';
99
+ }
100
+ if (platform === 'darwin') {
101
+ const q = (s) => `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
102
+ run('osascript', ['-e', `display notification ${q(body)} with title ${q(title)}`]);
103
+ return 'osascript';
104
+ }
105
+ }
106
+ catch { }
107
+ return 'none';
108
+ }
@@ -0,0 +1,43 @@
1
+ import fs from 'fs/promises';
2
+ import fsSync from 'fs';
3
+ import path from 'path';
4
+ const planFile = (cwd) => path.join(cwd, '.devx', 'plan.md');
5
+ export const PLAN_FRESH_MS = 24 * 60 * 60 * 1000;
6
+ export const PLAN_PROMPT_MAX_CHARS = 3000;
7
+ export function planToMarkdown(plan, when = new Date()) {
8
+ const stamp = when.toISOString().slice(0, 16).replace('T', ' ');
9
+ const lines = ['# Plan', '', `_Approved ${stamp}_`, '', plan.summary.trim(), ''];
10
+ const files = (plan.files ?? []).filter(Boolean);
11
+ if (files.length > 0)
12
+ lines.push('## Files', '', ...files.map((f) => `- ${f}`), '');
13
+ const steps = (plan.steps ?? []).filter(Boolean);
14
+ if (steps.length > 0)
15
+ lines.push('## Steps', '', ...steps.map((s, i) => `${i + 1}. ${s}`), '');
16
+ return lines.join('\n');
17
+ }
18
+ /** Writes the plan (structured data, or the text of a plan the model wrote in its answer). Returns the path. */
19
+ export async function savePlanFile(cwd, plan, when = new Date()) {
20
+ const markdown = typeof plan === 'string'
21
+ ? `# Plan\n\n_Approved ${when.toISOString().slice(0, 16).replace('T', ' ')}_\n\n${plan.trim()}\n`
22
+ : planToMarkdown(plan, when);
23
+ const file = planFile(cwd);
24
+ await fs.mkdir(path.dirname(file), { recursive: true });
25
+ await fs.writeFile(file, markdown, 'utf8');
26
+ return file;
27
+ }
28
+ /** The plan for the system prompt: only if it is recent, cut to a size that does not eat the context. */
29
+ export function loadPlanForPrompt(cwd, now = Date.now()) {
30
+ const file = planFile(cwd);
31
+ try {
32
+ const stat = fsSync.statSync(file);
33
+ if (now - stat.mtimeMs > PLAN_FRESH_MS)
34
+ return '';
35
+ const text = fsSync.readFileSync(file, 'utf8').trim();
36
+ if (text.length <= PLAN_PROMPT_MAX_CHARS)
37
+ return text;
38
+ return text.slice(0, PLAN_PROMPT_MAX_CHARS).replace(/\n[^\n]*$/, '') + '\n… (the rest is in .devx/plan.md)';
39
+ }
40
+ catch {
41
+ return '';
42
+ }
43
+ }