termux-dev 1.9.5 → 1.9.7

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/README.md CHANGED
@@ -67,6 +67,8 @@ The terminal interface is built around a framed prompt that carries the status i
67
67
  Built-in support for TypeScript (`tsc`), JavaScript (`node --check`), Python (`py_compile`), and Rust (`cargo check`). The AI automatically finds and fixes errors before finishing turns.
68
68
  - **Project Memory Bank (`.devx/memory.md`):**
69
69
  Persistent project context across sessions with `/memory add <rule>`.
70
+ - **Notifications when devx waits for you:**
71
+ When the agent needs an approval or asks you a question (`◆ Clarify`) and the terminal is not in front, you get a notification (kitty's own protocol, `notify-send` on Linux, `osascript` on macOS; on Termux an Android notification with a short vibration). A finished task is announced too, with the start of the answer. The terminal has to report focus changes (most modern ones do); when devx cannot tell whether you are looking, it stays quiet instead of popping up in front of you.
70
72
  - **Past chats (`search_chats`):**
71
73
  When you say "as we decided last time" or "in that chat about the parser", the agent can look through your saved chats by keywords (any language, other word forms match: page / pages) and read the one it needs, a few messages at a time. It is read-only and local, so it asks nothing; the chat that is open is not searched; tool output is not indexed; and what it reads is handed to the model as data, not as instructions. The agent does not use it on a normal task.
72
74
  - **18 Colour Themes on a Pure Black Background (`#0a0a0c`):**
@@ -92,6 +94,33 @@ The terminal interface is built around a framed prompt that carries the status i
92
94
  - **Node.js**: `v20.0.0` or higher
93
95
  - **npm**, **pnpm**, or **yarn**
94
96
 
97
+ ### Installing Node.js and npm
98
+
99
+ devx needs Node.js 20 or newer (npm comes with it). Pick your system:
100
+
101
+ ```bash
102
+ # Windows (PowerShell)
103
+ winget install OpenJS.NodeJS.LTS
104
+
105
+ # macOS (Homebrew)
106
+ brew install node
107
+
108
+ # Ubuntu / Debian (NodeSource: the version in the distribution's own repository is often older than 20)
109
+ curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
110
+ sudo apt-get install -y nodejs
111
+
112
+ # Fedora
113
+ sudo dnf install -y nodejs
114
+
115
+ # Arch Linux
116
+ sudo pacman -S nodejs npm
117
+
118
+ # Android (Termux)
119
+ pkg update && pkg install -y nodejs-lts
120
+ ```
121
+
122
+ Reopen the terminal and check: `node -v` (should be `v20` or higher) and `npm -v`. Any system: the installer at [nodejs.org](https://nodejs.org/en/download) or [nvm](https://github.com/nvm-sh/nvm).
123
+
95
124
  ### Fast Global Install (Recommended)
96
125
 
97
126
  ```bash
package/assets/banner.svg CHANGED
@@ -16,7 +16,7 @@
16
16
  <rect width="1000" height="280" fill="url(#bg)"/>
17
17
  </g>
18
18
  <rect x="1.5" y="1.5" width="997" height="277" rx="18" fill="none" stroke="#23252c" stroke-width="1.5"/>
19
- <text x="500.0" y="70" text-anchor="middle" font-family="'Inter',system-ui,-apple-system,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif" font-size="13" font-weight="600" letter-spacing="5" fill="#7d8896">THE TERMINAL-NATIVE AI CODING AGENT<tspan dx="16" fill="#2fe3ee" font-weight="700">v1.9.5</tspan></text>
19
+ <text x="500.0" y="70" text-anchor="middle" font-family="'Inter',system-ui,-apple-system,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif" font-size="13" font-weight="600" letter-spacing="5" fill="#7d8896">THE TERMINAL-NATIVE AI CODING AGENT<tspan dx="16" fill="#2fe3ee" font-weight="700">v1.9.7</tspan></text>
20
20
  <g>
21
21
  <text x="500.0" y="178" text-anchor="middle" font-family="'JetBrains Mono','Fira Code','SF Mono',ui-monospace,Menlo,Consolas,'DejaVu Sans Mono',monospace" font-size="112" font-weight="800" letter-spacing="14" fill="#2fe3ee">DEVX</text>
22
22
  </g>
@@ -32,7 +32,7 @@
32
32
  <rect x="595.2" y="574.0" width="243.2" height="19" fill="#000000"/>
33
33
  <text x="44.8" y="62.0" class="t" fill="#b794f6">▌</text>
34
34
  <text x="61.6" y="62.0" class="t b" fill="#f4f7fb">devx</text>
35
- <text x="112.0" y="62.0" class="t" fill="#5f6b7a">v1.9.5</text>
35
+ <text x="112.0" y="62.0" class="t" fill="#5f6b7a">v1.9.7</text>
36
36
  <text x="61.6" y="106.0" class="t" fill="#5f6b7a">model</text>
37
37
  <text x="128.8" y="106.0" class="t" fill="#e5eaf1">deepseek-v4.1-flash · think high</text>
38
38
  <text x="406.0" y="106.0" class="t" fill="#5f6b7a">/model</text>
package/dist/cli/index.js CHANGED
@@ -17,6 +17,7 @@ import { globalSnapshotManager } from '../core/snapshot.js';
17
17
  import { MemoryManager, memoryForPrompt } from '../core/memory.js';
18
18
  import { startServer, stopServer, displayServerBanner } from './server.js';
19
19
  import { notifyDevice, acquireWakeLock, isTermux, notificationBody, sendDesktopNotification } from '../core/notify.js';
20
+ import { isTerminalFocused, setFocusTracking, setTerminalFocused } from '../core/attention.js';
20
21
  import { copyToClipboard } from './clipboard-write.js';
21
22
  import { REWIND_CURRENT, messageDraft, rewindItems, userEntries } from './rewind.js';
22
23
  import { runPicker } from './picker.js';
@@ -933,9 +934,14 @@ export async function main() {
933
934
  const sessionManager = new SessionManager(config.model, planMode);
934
935
  drawLogo(config.banner || 'full');
935
936
  await runStartupUpdateCheck(config, { save: saveConfig });
936
- await MCPManager.getInstance().init(undefined, config.projectMcpTrusted === true);
937
- // After the servers are up: the prompt says how the agent reaches the tools it has to load.
938
937
  history.addMessage({ role: 'system', content: await buildSystemPrompt(planMode) });
938
+ // The servers start while the prompt is already there (starting them takes seconds: `npx` and the
939
+ // network). What needs them waits for `mcpReady`: the first request, and /mcp. When they are up the
940
+ // prompt is written again, so that it says how the agent reaches the tools that wait to be loaded.
941
+ const mcpReady = MCPManager.getInstance()
942
+ .init(undefined, config.projectMcpTrusted === true)
943
+ .then(async () => { history.updateSystemPrompt(await buildSystemPrompt(planMode)); })
944
+ .catch(() => undefined);
939
945
  // Ask a custom endpoint what the current model can do (cached for a day; never blocks the prompt).
940
946
  void refreshEndpoint(config).catch(() => null);
941
947
  const sessionCost = new CostLedger();
@@ -988,7 +994,6 @@ export async function main() {
988
994
  // The name of the chat: in the title of the terminal window (`◈ Short title`) and in the list of sessions.
989
995
  let windowTitle = '';
990
996
  let titleAttempts = 0;
991
- let terminalFocused = true; // updated by the terminal's focus reports while a task runs
992
997
  const setWindowTitle = (title) => {
993
998
  windowTitle = title;
994
999
  if (process.stdout.isTTY)
@@ -1858,6 +1863,7 @@ export async function main() {
1858
1863
  continue;
1859
1864
  }
1860
1865
  if (cmd === '/mcp') {
1866
+ await mcpReady;
1861
1867
  const sub = (parts[1] || '').toLowerCase();
1862
1868
  if (sub === 'reload' || sub === 'restart' || sub === 'r') {
1863
1869
  const s = new UiSpinner();
@@ -2109,6 +2115,8 @@ export async function main() {
2109
2115
  });
2110
2116
  // The format of the gateway is learned once (cached); the first request waits for it.
2111
2117
  await refreshFormats(config).catch(() => null);
2118
+ // The MCP servers may still be starting (see mcpReady): the request goes out with their tools.
2119
+ await mcpReady;
2112
2120
  const provider = createProvider(config);
2113
2121
  const tools = getToolSource(planMode);
2114
2122
  const guard = new CLIConsoleGuard(config.autoApprove, [...(config.bashAllowlist || [])], {
@@ -2424,7 +2432,7 @@ export async function main() {
2424
2432
  let text = (Buffer.isBuffer(data) ? data : Buffer.from(String(data))).toString('utf8');
2425
2433
  // Focus reports: ESC [ I (got the focus) and ESC [ O (lost it). They are not keys.
2426
2434
  if (text.includes('\x1b[I') || text.includes('\x1b[O')) {
2427
- terminalFocused = text.lastIndexOf('\x1b[I') > text.lastIndexOf('\x1b[O');
2435
+ setTerminalFocused(text.lastIndexOf('\x1b[I') > text.lastIndexOf('\x1b[O'));
2428
2436
  text = text.replace(/\x1b\[[IO]/g, '');
2429
2437
  if (!text)
2430
2438
  return;
@@ -2468,6 +2476,7 @@ export async function main() {
2468
2476
  process.stdout.on('resize', onResize);
2469
2477
  uiSpinner?.mute(true);
2470
2478
  process.stdout.write('\x1b[?1004h'); // the terminal tells when it loses or gets the focus
2479
+ setFocusTracking(true); // approval cards and questions notify when it is not in front
2471
2480
  }
2472
2481
  }
2473
2482
  catch { }
@@ -2843,7 +2852,7 @@ export async function main() {
2843
2852
  if (shouldNotify)
2844
2853
  notifyDevice(heading, preview || `Task completed in ${secs}s!`);
2845
2854
  }
2846
- else if (!terminalFocused) {
2855
+ else if (!isTerminalFocused()) {
2847
2856
  // The terminal is not in front: a notification with the start of the answer.
2848
2857
  if (sendDesktopNotification(heading, preview || `Task completed in ${secs}s`) === 'none' && shouldNotify)
2849
2858
  notifyDevice(heading, preview || `Task completed in ${secs}s!`);
@@ -2932,6 +2941,7 @@ export async function main() {
2932
2941
  const unsent = composer.stop();
2933
2942
  if (process.stdout.isTTY)
2934
2943
  process.stdout.write('\x1b[?1004l');
2944
+ setFocusTracking(false);
2935
2945
  releasePauseHook();
2936
2946
  process.stdout.removeListener('resize', onResize);
2937
2947
  if (!currentDraft && (unsent.draft || unsent.queued.length > 0)) {
@@ -4,7 +4,7 @@ import { runPicker } from './picker.js';
4
4
  import { palette, renderNotice } from './ui.js';
5
5
  import { displayWidth, truncateWidth } from './width.js';
6
6
  import { getLanguage, t } from '../core/i18n.js';
7
- import { UpdateError, buildUpdateSteps, checkForUpdates, detectInstallMethod, getCurrentVersion, restartDevx, runUpdateSteps, } from './updater.js';
7
+ import { UpdateError, buildUpdateSteps, checkForUpdates, checkForUpdatesRemembered, detectInstallMethod, readCachedCheck, getCurrentVersion, restartDevx, runUpdateSteps, } from './updater.js';
8
8
  /**
9
9
  * The update screens, in the style of the rest of devx:
10
10
  *
@@ -344,7 +344,9 @@ export async function runStartupUpdateCheck(config, deps = {}) {
344
344
  if (!out.isTTY)
345
345
  return;
346
346
  const current = await getCurrentVersion();
347
- const result = await withCheckingLine(out, current, () => (deps.check ?? checkForUpdates)(6000));
347
+ // A recent answer is used as it is: no network, no "checking" line, no wait before the prompt.
348
+ const cached = deps.check ? undefined : await readCachedCheck();
349
+ const result = cached ?? await withCheckingLine(out, current, () => (deps.check ?? checkForUpdatesRemembered)(6000));
348
350
  if (result.updateAvailable) {
349
351
  if (config.skippedUpdate === result.latestVersion)
350
352
  return;
@@ -359,6 +361,8 @@ export async function runStartupUpdateCheck(config, deps = {}) {
359
361
  }
360
362
  if (result.error)
361
363
  return; // offline at start-up is not worth a word
364
+ if (cached)
365
+ return; // up to date, as the last check said: nothing to show
362
366
  // Up to date: a short line that goes away by itself.
363
367
  out.write(` ${palette.ok('✓')} ${palette.muted(t('update.upToDate', { version: result.currentVersion }))}\n`);
364
368
  await sleep(deps.tickMs !== undefined ? 0 : 450);
@@ -1,5 +1,6 @@
1
1
  import { spawn } from 'child_process';
2
2
  import path from 'path';
3
+ import os from 'os';
3
4
  import fs from 'fs/promises';
4
5
  import { fileURLToPath } from 'url';
5
6
  const GITHUB_REPO_URL = 'https://github.com/apvcode/Termux-Dev';
@@ -40,6 +41,12 @@ export async function checkForUpdates(timeoutMs = 10000) {
40
41
  // mobile connection a stalled second request used to block devx at startup forever.
41
42
  const signal = AbortSignal.timeout(timeoutMs);
42
43
  try {
44
+ // The release is asked at the same time as the package.json, not after it: the check takes as long
45
+ // as the slower of the two, not both. A failure of the release request is not an error of the check.
46
+ const releaseRequest = fetch(`${REMOTE_RELEASE_URL}?t=${Date.now()}`, {
47
+ signal,
48
+ headers: { 'User-Agent': 'devx-updater' }
49
+ }).catch(() => null);
43
50
  // 1. Try fetching latest package.json with cache-busting timestamp
44
51
  const cacheBusterUrl = `${REMOTE_PKG_URL}?t=${Date.now()}`;
45
52
  const res = await fetch(cacheBusterUrl, {
@@ -59,11 +66,8 @@ export async function checkForUpdates(timeoutMs = 10000) {
59
66
  }
60
67
  // 2. Also check latest GitHub release tag
61
68
  try {
62
- const relRes = await fetch(`${REMOTE_RELEASE_URL}?t=${Date.now()}`, {
63
- signal,
64
- headers: { 'User-Agent': 'devx-updater' }
65
- });
66
- if (relRes.ok) {
69
+ const relRes = await releaseRequest;
70
+ if (relRes?.ok) {
67
71
  const relData = await relRes.json();
68
72
  const relTag = (relData.tag_name || relData.name || '').replace(/^v/, '').trim();
69
73
  if (relTag && (!latestVersion || isNewerVersion(relTag, latestVersion))) {
@@ -99,6 +103,56 @@ export async function checkForUpdates(timeoutMs = 10000) {
99
103
  };
100
104
  }
101
105
  }
106
+ // ───────────────────────── remembering the check ─────────────────────────
107
+ /** An answer is good for this long; after it devx asks again. */
108
+ const CHECK_FRESH_MS = 6 * 60 * 60 * 1000;
109
+ /** A failed check (offline, a stalled connection) is not repeated at every start: that would stall every start. */
110
+ const CHECK_FAILED_MS = 30 * 60 * 1000;
111
+ const checkFile = () => path.join(os.homedir(), '.devx', 'update-check.json');
112
+ /** The last check, when it is recent enough to be trusted at start-up; undefined when devx has to ask again. */
113
+ export async function readCachedCheck(now = Date.now()) {
114
+ try {
115
+ const saved = JSON.parse(await fs.readFile(checkFile(), 'utf8'));
116
+ const age = now - Number(saved.checkedAt);
117
+ // A clock that went backwards makes the age negative: such an answer is not trusted.
118
+ if (!Number.isFinite(age) || age < 0)
119
+ return undefined;
120
+ const currentVersion = await getCurrentVersion();
121
+ if (saved.error) {
122
+ return age < CHECK_FAILED_MS ? { updateAvailable: false, currentVersion, latestVersion: currentVersion, error: saved.error } : undefined;
123
+ }
124
+ if (age >= CHECK_FRESH_MS || !saved.latestVersion)
125
+ return undefined;
126
+ // Judged against the version that runs now: after an update the old answer no longer says "update".
127
+ return {
128
+ updateAvailable: isNewerVersion(saved.latestVersion, currentVersion),
129
+ currentVersion,
130
+ latestVersion: saved.latestVersion,
131
+ releaseNotes: saved.releaseNotes || undefined,
132
+ publishedAt: saved.publishedAt || undefined,
133
+ };
134
+ }
135
+ catch {
136
+ return undefined;
137
+ }
138
+ }
139
+ /** Keeps the answer of a check for the next start-ups. Never throws: this is only a shortcut. */
140
+ export async function rememberCheck(result, now = Date.now()) {
141
+ try {
142
+ const saved = result.error
143
+ ? { checkedAt: now, error: result.error }
144
+ : { checkedAt: now, latestVersion: result.latestVersion, releaseNotes: result.releaseNotes, publishedAt: result.publishedAt };
145
+ await fs.mkdir(path.dirname(checkFile()), { recursive: true, mode: 0o700 });
146
+ await fs.writeFile(checkFile(), JSON.stringify(saved));
147
+ }
148
+ catch { }
149
+ }
150
+ /** `checkForUpdates`, and the answer is kept for the next start-ups (the start-up check reads it, see readCachedCheck). */
151
+ export async function checkForUpdatesRemembered(timeoutMs = 10000) {
152
+ const result = await checkForUpdates(timeoutMs);
153
+ await rememberCheck(result);
154
+ return result;
155
+ }
102
156
  /** How this devx was installed: from the npm package, or from a git clone (a developer's copy). */
103
157
  export async function detectInstallMethod() {
104
158
  const here = fileURLToPath(import.meta.url);
@@ -0,0 +1,63 @@
1
+ import { isTermux, notificationBody, notifyDevice, sendDesktopNotification } from './notify.js';
2
+ /**
3
+ * "devx is waiting for you": a notification when the agent needs an answer (an approval card, a
4
+ * question) and the person is not looking at the terminal. A finished task is announced elsewhere
5
+ * (the end of a turn); this is for the moments when the work is stopped until someone answers.
6
+ */
7
+ let tracking = false;
8
+ let focused = true;
9
+ /**
10
+ * The terminal reports when it loses and gets the focus (while a task runs). Without those reports
11
+ * devx cannot tell whether anybody is looking, and a notification that pops up in front of someone who is
12
+ * typing in the terminal is worse than none, so nothing is sent then.
13
+ */
14
+ export function setFocusTracking(on) {
15
+ tracking = on;
16
+ // A task starts because the person just pressed Enter here.
17
+ if (on)
18
+ focused = true;
19
+ }
20
+ export function setTerminalFocused(value) {
21
+ focused = value;
22
+ }
23
+ export function isTerminalFocused() {
24
+ return focused;
25
+ }
26
+ /**
27
+ * Tells the person that devx waits for them. Returns whether something was sent. Never throws.
28
+ * On Termux (Android) a notification always goes out: the person is most likely in another app, and
29
+ * the phone cannot tell devx. On a desktop it goes out only when the terminal is not in front.
30
+ */
31
+ export function notifyAttention(title, body, deps = {}) {
32
+ try {
33
+ const text = notificationBody(body, 140) || title;
34
+ if (deps.termux ?? isTermux()) {
35
+ (deps.device ?? notifyDevice)(title, text);
36
+ return true;
37
+ }
38
+ if (!tracking || focused)
39
+ return false;
40
+ const via = (deps.desktop ?? sendDesktopNotification)(title, text);
41
+ // No desktop notification service: the terminal bell at least marks the window as asking for attention.
42
+ if (via === 'none')
43
+ (deps.device ?? notifyDevice)(title, text);
44
+ return true;
45
+ }
46
+ catch {
47
+ return false;
48
+ }
49
+ }
50
+ /** What an approval card asks, in a line for a notification: `bash: npm test`. */
51
+ export function approvalNotice(title, lines, danger) {
52
+ const first = (lines.find((l) => l.trim()) ?? '').trim();
53
+ return {
54
+ title: danger ? 'devx · dangerous action, approval needed' : 'devx · approval needed',
55
+ body: first ? `${title}: ${first}` : title,
56
+ };
57
+ }
58
+ /** The question the agent asks, for a notification (and how many more follow). */
59
+ export function questionNotice(questions) {
60
+ const first = String(questions[0]?.question ?? '').trim();
61
+ const more = questions.length > 1 ? ` (+${questions.length - 1} more)` : '';
62
+ return { title: 'devx · question for you', body: `${first || 'The agent has a question'}${more}` };
63
+ }
@@ -1 +1 @@
1
- export const DEVX_VERSION = '1.9.5';
1
+ export const DEVX_VERSION = '1.9.7';
@@ -1,3 +1,4 @@
1
+ import { approvalNotice, notifyAttention } from '../core/attention.js';
1
2
  import pc from 'picocolors';
2
3
  import path from 'path';
3
4
  import os from 'os';
@@ -377,6 +378,9 @@ export class CLIConsoleGuard {
377
378
  return false;
378
379
  }
379
380
  const request = buildPermissionRequest(toolName, args);
381
+ // The work stops here until somebody answers: if the terminal is not in front, a notification says so.
382
+ const notice = approvalNotice(request.title, request.lines, request.danger);
383
+ notifyAttention(notice.title, notice.body);
380
384
  // Only requests that are safe to remember offer "Session" and "Always".
381
385
  const remember = request.danger ? null : rememberKeyFor(toolName, args);
382
386
  const offer = remember ? { session: true, always: true } : {};
@@ -3,6 +3,7 @@ import { runPicker } from '../cli/picker.js';
3
3
  import { palette } from '../cli/ui.js';
4
4
  import { wrapWords } from '../cli/width.js';
5
5
  import { t } from '../core/i18n.js';
6
+ import { notifyAttention, questionNotice } from '../core/attention.js';
6
7
  export const askQuestionsTool = {
7
8
  name: 'ask_questions',
8
9
  definition: {
@@ -45,6 +46,9 @@ export const askQuestionsTool = {
45
46
  if (!args.questions || !Array.isArray(args.questions) || args.questions.length === 0) {
46
47
  return 'No questions provided.';
47
48
  }
49
+ // The agent waits for the answer: if the terminal is not in front, a notification says so.
50
+ const notice = questionNotice(args.questions);
51
+ notifyAttention(notice.title, notice.body);
48
52
  const results = [];
49
53
  const total = args.questions.length;
50
54
  const cols = process.stdout.columns || 80;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "termux-dev",
3
- "version": "1.9.5",
3
+ "version": "1.9.7",
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",