termux-dev 1.4.5 → 1.4.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/assets/banner.svg CHANGED
@@ -25,7 +25,7 @@
25
25
 
26
26
  <!-- Tagline & Badge -->
27
27
  <text x="425" y="142" font-family="system-ui, -apple-system, sans-serif" font-size="14" font-weight="600" fill="#8b949e" text-anchor="middle" letter-spacing="3">
28
- THE TERMINAL-NATIVE AI CODING AGENT <tspan fill="#00f2fe" font-weight="bold">v1.4.5</tspan>
28
+ THE TERMINAL-NATIVE AI CODING AGENT <tspan fill="#00f2fe" font-weight="bold">v1.4.7</tspan>
29
29
  </text>
30
30
 
31
31
  <!-- Top Accent Line -->
@@ -18,7 +18,7 @@ async function pingUrl(url) {
18
18
  const req = https.get(url, {
19
19
  timeout: 3000,
20
20
  headers: {
21
- 'User-Agent': 'devx-doctor/1.4.5'
21
+ 'User-Agent': 'devx-doctor/1.4.7'
22
22
  }
23
23
  }, (res) => {
24
24
  res.resume(); // consume response data to free up memory and release socket
@@ -35,7 +35,7 @@ export async function runHeadlessMode(userPrompt, config, options = {}) {
35
35
  });
36
36
  try {
37
37
  if (!isQuiet && !isJson) {
38
- console.log(pc.bold(pc.cyan(`⚡ devx v1.4.5 (headless) | ${planMode ? 'PLAN' : 'AGENT'} | ${config.model}`)));
38
+ console.log(pc.bold(pc.cyan(`⚡ devx v1.4.7 (headless) | ${planMode ? 'PLAN' : 'AGENT'} | ${config.model}`)));
39
39
  console.log(pc.dim(`Task: ${userPrompt}\n`));
40
40
  }
41
41
  for await (const event of agent.run(abortController.signal)) {
package/dist/cli/index.js CHANGED
@@ -484,7 +484,7 @@ function drawLogo() {
484
484
  theme.colorFn(' █▀▀▄ █▀▀▀ █ █ █ █'),
485
485
  theme.colorFn(' █ █ █▀▀▀ ▀▄▀ ▀▄▀ '),
486
486
  theme.colorFn(' █▄▄▀ █▄▄▄ ▀ ▀ ▀ '),
487
- ' ' + theme.boldFn('v1.4.5'),
487
+ ' ' + theme.boldFn('v1.4.7'),
488
488
  ''
489
489
  ];
490
490
  for (const line of logo) {
@@ -499,7 +499,7 @@ function drawLogo() {
499
499
  indent + theme.colorFn('▀▀▀█▀▀▀ █▀▀▀ █▀▀█ █▄ ▄█ █ █ ▀▄ ▄▀ █▀▀▄ █▀▀▀ █ █'),
500
500
  indent + theme.colorFn(' █ █▀▀▀ █▄▄▀ █ █ █ █ █ █ ▀▀ █ █ █▀▀▀ █ █'),
501
501
  indent + theme.colorFn(' █ █▄▄▄ █ ▀▄ █ █ ▀▄▄▀ ▄▀ ▀▄ █▄▄▀ █▄▄▄ ▀▄▀ '),
502
- indent + theme.boldFn('v1.4.5'),
502
+ indent + theme.boldFn('v1.4.7'),
503
503
  ''
504
504
  ];
505
505
  for (const line of logo) {
@@ -645,7 +645,7 @@ async function handleSettings(config) {
645
645
  const maxIterLabel = maxIter >= 9999 ? 'Unlimited' : `${maxIter} steps`;
646
646
  const currentTh = getCurrentTheme();
647
647
  const choice = await select({
648
- message: `${pc.bold('⚙️ Settings')} ${pc.dim(`(devx v1.4.5 • theme: ${currentTh.name})`)}`,
648
+ message: `${pc.bold('⚙️ Settings')} ${pc.dim(`(devx v1.4.7 • theme: ${currentTh.name})`)}`,
649
649
  choices: [
650
650
  {
651
651
  name: `🎨 Color Theme: ${currentTh.emoji} ${currentTh.name}`,
@@ -686,7 +686,7 @@ async function handleSettings(config) {
686
686
  description: 'Limit how many tool steps (file edits, terminal commands) agent can do per request'
687
687
  },
688
688
  {
689
- name: `${currentTh.colorFn('✨ About devx')} ${pc.dim('(v1.4.5 by ApvCode)')}`,
689
+ name: `${currentTh.colorFn('✨ About devx')} ${pc.dim('(v1.4.7 by ApvCode)')}`,
690
690
  value: 'about',
691
691
  description: 'Terminal-Native AI Coding Agent created by ApvCode (https://github.com/apvcode/Termux-Dev)'
692
692
  },
@@ -702,7 +702,7 @@ async function handleSettings(config) {
702
702
  continue;
703
703
  }
704
704
  if (choice === 'about') {
705
- p.note(`⚡ devx v1.4.5 — Terminal-Native AI Coding Agent\n` +
705
+ p.note(`⚡ devx v1.4.7 — Terminal-Native AI Coding Agent\n` +
706
706
  `🎨 Theme: ${currentTh.emoji} ${currentTh.name}\n` +
707
707
  `👤 Author: ApvCode (https://github.com/apvcode)\n` +
708
708
  `🌟 Repository: https://github.com/apvcode/Termux-Dev\n` +
@@ -771,7 +771,7 @@ export async function main() {
771
771
  program
772
772
  .name('devx')
773
773
  .description('Terminal-native AI coding assistant and vibe-coding agent')
774
- .version('1.4.5')
774
+ .version('1.4.7')
775
775
  .option('-p, --prompt <task>', 'Run one-shot task non-interactively (headless mode)')
776
776
  .option('-y, --yolo', 'Automatically approve all tool executions without confirmation')
777
777
  .option('-m, --model <model>', 'Specify AI model to use for this execution')
@@ -1,7 +1,8 @@
1
+ import readline from 'readline';
1
2
  import pc from 'picocolors';
2
3
  import { scanProjectFiles } from './files.js';
3
4
  import { saveClipboardImage, processPastedFilePath } from './clipboard.js';
4
- import { getCurrentTheme, listThemes } from './theme.js';
5
+ import { getCurrentTheme } from './theme.js';
5
6
  import { CustomCommandManager } from '../core/commands.js';
6
7
  export const SLASH_COMMANDS = [
7
8
  { cmd: '/new', desc: 'Start a new clean chat session' },
@@ -36,407 +37,110 @@ export const SLASH_COMMANDS = [
36
37
  const GLOBAL_PROMPT_HISTORY = [];
37
38
  export function askPrompt(opts = {}) {
38
39
  return new Promise((resolve) => {
39
- process.stdin.resume();
40
- const msg = opts.message ? `${opts.message} ${pc.dim(`(Tab = ${opts.planMode ? 'AGENT' : 'PLAN'})`)}` : `Ask anything... ${pc.dim(`(Tab = ${opts.planMode ? 'AGENT' : 'PLAN'})`)}`;
41
- const placeholder = opts.placeholder || 'Describe a task, @file, /help, paste image, or press Tab to switch mode';
42
- let input = opts.initialValue || '';
43
- let cursorPos = input.length;
44
- let selectedIndex = 0;
45
- let lastDropdownLines = 0;
46
- let lastCursorLine = 0;
47
- let disposed = false;
48
- let historyIndex = GLOBAL_PROMPT_HISTORY.length;
49
- let tempDraft = '';
40
+ const msg = opts.message
41
+ ? `${opts.message} ${pc.dim(`(Tab = ${opts.planMode ? 'AGENT' : 'PLAN'})`)}`
42
+ : `Ask anything... ${pc.dim(`(Tab = ${opts.planMode ? 'AGENT' : 'PLAN'})`)}`;
43
+ const theme = getCurrentTheme();
44
+ console.log(theme.colorFn('◆') + ' ' + pc.bold(msg));
50
45
  let availableFiles = [];
51
46
  let customCommandsList = [];
52
- // Preload project files and custom slash commands
53
- scanProjectFiles().then(files => {
54
- availableFiles = files;
55
- }).catch(() => { });
56
- CustomCommandManager.listCommands().then(cmds => {
57
- customCommandsList = cmds.map(c => ({ cmd: c.cmd, desc: c.desc }));
58
- }).catch(() => { });
59
- const pastes = [];
47
+ scanProjectFiles().then((f) => (availableFiles = f)).catch(() => { });
48
+ CustomCommandManager.listCommands()
49
+ .then((c) => (customCommandsList = c.map((x) => ({ cmd: x.cmd, desc: x.desc }))))
50
+ .catch(() => { });
60
51
  const imageAttachments = [];
61
52
  const usedImageNames = new Set();
62
- const theme = getCurrentTheme();
63
- // Header printed once
64
- console.log(theme.colorFn('◆') + ' ' + pc.bold(msg));
65
- if (process.stdin.isTTY) {
66
- process.stdin.setRawMode(true);
67
- }
68
- if (process.stdout.isTTY) {
69
- process.stdout.write('\x1b[?2004h');
70
- }
71
- function getDropdownItems() {
72
- if (input.startsWith('/theme ') || input.startsWith('/themes ') || input === '/theme') {
73
- const afterCmd = input.replace(/^\/(?:theme|themes)\s*/i, '').trim().toLowerCase();
74
- const themes = listThemes();
75
- const matched = themes.filter(t => !afterCmd ||
76
- t.id.toLowerCase().startsWith(afterCmd) ||
77
- t.name.toLowerCase().includes(afterCmd));
78
- const list = [];
79
- if (!afterCmd || '/theme'.startsWith(input.trim().toLowerCase())) {
80
- list.push({
81
- label: '/theme',
82
- desc: 'Interactive UI theme picker menu',
83
- replacement: '/theme',
84
- replaceStart: 0,
85
- replaceLen: input.length
86
- });
87
- }
88
- for (const t of matched) {
89
- list.push({
90
- label: `/theme ${t.id}`,
91
- desc: `${t.emoji} ${t.name} (${t.desc})`,
92
- replacement: `/theme ${t.id}`,
93
- replaceStart: 0,
94
- replaceLen: input.length
95
- });
96
- }
97
- return list;
98
- }
99
- if (input.startsWith('/')) {
100
- const q = input.trim().toLowerCase();
101
- const baseList = [...SLASH_COMMANDS];
102
- for (const cc of customCommandsList) {
103
- if (!baseList.some(b => b.cmd === cc.cmd)) {
104
- baseList.push({ cmd: cc.cmd, desc: cc.desc });
105
- }
106
- }
107
- const filtered = baseList.filter(c => c.cmd.toLowerCase().startsWith(q) || q === '/');
108
- return filtered.map(c => ({
109
- label: c.cmd,
110
- desc: c.desc,
111
- replacement: c.cmd,
112
- replaceStart: 0,
113
- replaceLen: input.length
114
- }));
115
- }
116
- // Check for @ mention at cursor position
117
- const beforeCursor = input.slice(0, cursorPos);
118
- const atMatch = beforeCursor.match(/@([a-zA-Z0-9_\-./]*)$/);
53
+ const completer = (line) => {
54
+ const allCmds = [
55
+ ...SLASH_COMMANDS.map((c) => c.cmd),
56
+ ...customCommandsList.map((c) => c.cmd)
57
+ ];
58
+ // 1. Slash commands autocomplete
59
+ if (line.startsWith('/')) {
60
+ const q = line.trim().toLowerCase();
61
+ const hits = allCmds.filter((cmd) => cmd.toLowerCase().startsWith(q));
62
+ return [hits.length ? hits : allCmds, line];
63
+ }
64
+ // 2. @file autocomplete
65
+ const atMatch = line.match(/@([a-zA-Z0-9_\-./]*)$/);
119
66
  if (atMatch && availableFiles.length > 0) {
120
67
  const query = atMatch[1].toLowerCase();
121
- const matched = availableFiles
122
- .filter(f => f.toLowerCase().includes(query) || query === '')
123
- .slice(0, 8);
124
- return matched.map(f => ({
125
- label: `@${f}`,
126
- desc: f.endsWith('/') ? 'directory' : 'file',
127
- replacement: `@${f}${f.endsWith('/') ? '' : ' '}`,
128
- replaceStart: cursorPos - atMatch[0].length,
129
- replaceLen: atMatch[0].length
130
- }));
131
- }
132
- return [];
133
- }
134
- function formatInputWithBadges(rawText) {
135
- let formatted = rawText;
136
- for (const p of pastes) {
137
- const styled = pc.bold(pc.cyan(`[Pasted text #${p.id} +${p.linesCount} lines]`));
138
- formatted = formatted.split(p.tag).join(styled);
139
- }
140
- for (const img of imageAttachments) {
141
- const styled = pc.bold(pc.magenta(img.tag));
142
- formatted = formatted.split(img.tag).join(styled);
143
- }
144
- return formatted;
145
- }
146
- function expandPastes(rawText) {
147
- let result = rawText;
148
- for (const p of pastes) {
149
- result = result.split(p.tag).join(p.content);
150
- }
151
- for (const img of imageAttachments) {
152
- result = result.split(img.tag).join(`@${img.filePath.replace(/\\/g, '/')} `);
153
- }
154
- return result;
68
+ const hits = availableFiles
69
+ .filter((f) => f.toLowerCase().includes(query) || query === '')
70
+ .slice(0, 15)
71
+ .map((f) => `@${f}${f.endsWith('/') ? '' : ' '}`);
72
+ return [hits, atMatch[0]];
73
+ }
74
+ return [[], line];
75
+ };
76
+ const rl = readline.createInterface({
77
+ input: process.stdin,
78
+ output: process.stdout,
79
+ prompt: pc.dim('│') + ' ',
80
+ completer,
81
+ historySize: 100
82
+ });
83
+ // Populate history in readline
84
+ if (GLOBAL_PROMPT_HISTORY.length > 0) {
85
+ rl.history = [...GLOBAL_PROMPT_HISTORY].reverse();
155
86
  }
156
- function stripAnsi(str) {
157
- return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
158
- }
159
- function render() {
160
- if (disposed)
161
- return;
162
- const items = getDropdownItems();
163
- const dropdownLines = [];
164
- const cols = Math.max(28, process.stdout.columns || 80);
165
- const rows = Math.max(8, process.stdout.rows || 24);
166
- if (items.length > 0) {
167
- if (selectedIndex >= items.length)
168
- selectedIndex = 0;
169
- if (selectedIndex < 0)
170
- selectedIndex = items.length - 1;
171
- // Ensure inner box width strictly fits: innerBoxWidth + 5 <= cols - 2
172
- const innerBoxWidth = Math.max(16, Math.min(cols - 8, 54));
173
- const isNarrowOrShort = rows <= 16 || cols < 50;
174
- const pageSize = isNarrowOrShort
175
- ? Math.max(2, Math.min(3, rows - 5))
176
- : Math.min(5, Math.max(3, Math.floor((rows - 4) / 2)));
177
- const total = items.length;
178
- let startIndex = 0;
179
- if (total > pageSize) {
180
- if (selectedIndex >= pageSize) {
181
- startIndex = Math.min(selectedIndex - pageSize + 1, total - pageSize);
182
- }
183
- }
184
- const endIndex = Math.min(startIndex + pageSize, total);
185
- const hasMoreUp = startIndex > 0;
186
- const hasMoreDown = endIndex < total;
187
- let topBorderStr = '─'.repeat(innerBoxWidth);
188
- if (hasMoreUp) {
189
- const mid = Math.max(0, Math.floor(innerBoxWidth / 2) - 2);
190
- topBorderStr = '─'.repeat(mid) + ' ▲ ' + '─'.repeat(Math.max(0, innerBoxWidth - mid - 3));
191
- }
192
- let botBorderStr = '─'.repeat(innerBoxWidth);
193
- if (hasMoreDown) {
194
- const mid = Math.max(0, Math.floor(innerBoxWidth / 2) - 2);
195
- botBorderStr = '─'.repeat(mid) + ' ▼ ' + '─'.repeat(Math.max(0, innerBoxWidth - mid - 3));
196
- }
197
- dropdownLines.push(pc.dim('│') + ' ' + pc.dim('╭' + topBorderStr + '╮'));
198
- for (let i = startIndex; i < endIndex; i++) {
199
- const item = items[i];
200
- const isSelected = i === selectedIndex;
201
- const pointer = isSelected ? '› ' : ' ';
202
- const availWidth = innerBoxWidth - 2;
203
- let row = '';
204
- if (availWidth < 20) {
205
- const labelStr = item.label.length > availWidth ? item.label.slice(0, availWidth - 1) + '…' : item.label.padEnd(availWidth);
206
- const plain = pointer + labelStr;
207
- row = isSelected ? theme.badgeFn(plain) : (pointer + theme.boldFn(labelStr));
208
- }
209
- else {
210
- const labelMax = Math.min(15, Math.floor(availWidth * 0.45));
211
- const labelStr = item.label.length > labelMax ? item.label.slice(0, labelMax - 1) + '…' : item.label.padEnd(labelMax);
212
- const descMax = availWidth - labelMax - 1;
213
- const descStr = item.desc.length > descMax ? item.desc.slice(0, descMax - 1) + '…' : item.desc.padEnd(descMax);
214
- const plain = `${pointer}${labelStr} ${descStr}`;
215
- if (isSelected) {
216
- row = theme.badgeFn(plain);
217
- }
218
- else {
219
- row = `${pointer}${theme.boldFn(labelStr)} ${pc.gray(descStr)}`;
220
- }
221
- }
222
- const currentLen = stripAnsi(row).length;
223
- if (currentLen < innerBoxWidth) {
224
- row += ' '.repeat(innerBoxWidth - currentLen);
225
- }
226
- dropdownLines.push(pc.dim('│') + ' ' + pc.dim('│') + row + pc.dim('│'));
227
- }
228
- dropdownLines.push(pc.dim('│') + ' ' + pc.dim('╰' + botBorderStr + '╯'));
229
- }
230
- // === SCROLLING SINGLE-LINE INPUT (never wraps, works on all terminals) ===
231
- const prefix = pc.dim('│') + ' ';
232
- const prefixLen = 3; // visible width of "│ "
233
- // Reserve 1 char safety margin so terminal never auto-wraps
234
- const maxVisible = cols - prefixLen - 1;
235
- let lineContent;
236
- let screenCursorCol;
237
- if (input.length === 0) {
238
- // Show placeholder
239
- const displayPlace = placeholder.length > maxVisible ? placeholder.slice(0, maxVisible - 1) + '…' : placeholder;
240
- lineContent = prefix + pc.dim(displayPlace);
241
- screenCursorCol = prefixLen;
242
- }
243
- else if (input.length <= maxVisible) {
244
- // Input fits on screen entirely
245
- lineContent = prefix + formatInputWithBadges(input);
246
- screenCursorCol = prefixLen + cursorPos;
247
- }
248
- else {
249
- // Scrolling window: show a slice of input around cursor
250
- const scrollPad = Math.floor(maxVisible / 3);
251
- let winStart = 0;
252
- if (cursorPos > maxVisible - scrollPad) {
253
- winStart = cursorPos - (maxVisible - scrollPad);
254
- }
255
- if (winStart > input.length - maxVisible) {
256
- winStart = Math.max(0, input.length - maxVisible);
257
- }
258
- const winEnd = Math.min(winStart + maxVisible, input.length);
259
- let visibleSlice = input.slice(winStart, winEnd);
260
- // Add scroll indicators
261
- if (winStart > 0) {
262
- visibleSlice = '…' + visibleSlice.slice(1);
263
- }
264
- if (winEnd < input.length) {
265
- visibleSlice = visibleSlice.slice(0, -1) + '…';
266
- }
267
- lineContent = prefix + formatInputWithBadges(visibleSlice);
268
- screenCursorCol = prefixLen + (cursorPos - winStart);
269
- // Clamp cursor position for scroll indicator adjustments
270
- if (winStart > 0 && cursorPos === winStart) {
271
- screenCursorCol = prefixLen + 1;
272
- }
273
- }
274
- // Pad line to exactly cols-1 to overwrite any stale characters
275
- const visLen = stripAnsi(lineContent).length;
276
- if (visLen < cols - 1) {
277
- lineContent += ' '.repeat(cols - 1 - visLen);
278
- }
279
- // Move cursor up past any previous dropdown lines, then clear and redraw
280
- if (lastDropdownLines > 0) {
281
- process.stdout.write(`\x1b[${lastDropdownLines}A`);
282
- }
283
- // Write input line (always exactly 1 physical line, no wrap)
284
- process.stdout.write(`\r\x1b[2K${lineContent}`);
285
- // Write dropdown lines below
286
- for (let i = 0; i < Math.max(dropdownLines.length, lastDropdownLines); i++) {
287
- if (i < dropdownLines.length) {
288
- process.stdout.write(`\n\x1b[2K${dropdownLines[i]}`);
289
- }
290
- else {
291
- process.stdout.write(`\n\x1b[2K`);
87
+ let isClosed = false;
88
+ const cleanup = () => {
89
+ if (isClosed)
90
+ return;
91
+ isClosed = true;
92
+ process.stdin.removeListener('keypress', onKeypress);
93
+ rl.close();
94
+ };
95
+ const onKeypress = (str, key) => {
96
+ if (isClosed)
97
+ return;
98
+ // Tab key pressed
99
+ if (key && key.name === 'tab') {
100
+ const curLine = rl.line || '';
101
+ // If line is empty or does not start with / or contain @, switch mode
102
+ if (curLine.trim() === '' || (!curLine.startsWith('/') && !curLine.includes('@'))) {
103
+ readline.cursorTo(process.stdout, 0);
104
+ readline.clearLine(process.stdout, 0);
105
+ cleanup();
106
+ resolve(`__TOGGLE_MODE__:${curLine}`);
107
+ return;
292
108
  }
293
109
  }
294
- // Move cursor back up to the input line
295
- const totalBelow = Math.max(dropdownLines.length, lastDropdownLines);
296
- if (totalBelow > 0) {
297
- process.stdout.write(`\x1b[${totalBelow}A`);
110
+ // Ctrl+V or Ctrl+P: Clipboard image paste
111
+ if ((key && key.ctrl && (key.name === 'v' || key.name === 'p')) || str === '\x16' || str === '\x10') {
112
+ saveClipboardImage('image.png', usedImageNames)
113
+ .then((imgRes) => {
114
+ if (imgRes && !isClosed) {
115
+ const tag = `[${imgRes.fileName} ${imgRes.sizeStr}]`;
116
+ imageAttachments.push({
117
+ tag,
118
+ fileName: imgRes.fileName,
119
+ filePath: imgRes.filePath,
120
+ sizeStr: imgRes.sizeStr
121
+ });
122
+ usedImageNames.add(imgRes.fileName);
123
+ rl.write(`${tag} `);
124
+ }
125
+ })
126
+ .catch(() => { });
298
127
  }
299
- // Position cursor at the correct column
300
- process.stdout.write(`\r\x1b[${screenCursorCol}C`);
301
- lastCursorLine = 0; // Always line 0 now
302
- lastDropdownLines = dropdownLines.length;
128
+ };
129
+ if (process.stdin.isTTY) {
130
+ readline.emitKeypressEvents(process.stdin, rl);
131
+ process.stdin.on('keypress', onKeypress);
303
132
  }
304
- render();
305
- function cleanup() {
306
- disposed = true;
307
- if (process.stdout.isTTY) {
308
- process.stdout.write('\x1b[?2004l');
309
- }
310
- process.stdin.removeListener('data', onData);
311
- if (process.stdin.isTTY) {
312
- process.stdin.setRawMode(false);
313
- }
314
- process.stdin.pause();
133
+ if (opts.initialValue) {
134
+ rl.write(opts.initialValue);
315
135
  }
316
- function clearBoxAndExit(finalInput) {
317
- // Clear dropdown lines below
318
- if (lastDropdownLines > 0) {
319
- for (let i = 0; i < lastDropdownLines; i++) {
320
- process.stdout.write(`\n\x1b[2K`);
321
- }
322
- process.stdout.write(`\x1b[${lastDropdownLines}A`);
323
- }
324
- process.stdout.write(`\r\x1b[2K`);
325
- const fullText = expandPastes(finalInput);
326
- if (fullText.trim()) {
327
- if (GLOBAL_PROMPT_HISTORY.length === 0 || GLOBAL_PROMPT_HISTORY[GLOBAL_PROMPT_HISTORY.length - 1] !== fullText.trim()) {
328
- GLOBAL_PROMPT_HISTORY.push(fullText.trim());
329
- }
330
- }
331
- const lines = fullText.split('\n');
332
- process.stdout.write(`${pc.dim('│')} ${formatInputWithBadges(lines[0])}\n`);
333
- for (let i = 1; i < lines.length; i++) {
334
- process.stdout.write(pc.dim('│') + ' ' + lines[i] + '\n');
335
- }
336
- process.stdout.write('\n');
136
+ rl.prompt();
137
+ rl.on('line', async (rawLine) => {
337
138
  cleanup();
338
- resolve(fullText);
339
- }
340
- async function handlePaste(pastedContent) {
341
- const cleaned = pastedContent.replace(/\x1b\[200~/g, '').replace(/\x1b\[201~/g, '');
342
- // Check if pastedContent contains file path(s) to images
343
- const pathCandidates = cleaned.split(/[&;\n\r]+/).map(s => s.trim()).filter(Boolean);
344
- let foundAnyImage = false;
345
- for (const cand of pathCandidates) {
346
- const imgRes = await processPastedFilePath(cand, usedImageNames);
347
- if (imgRes) {
348
- foundAnyImage = true;
349
- const tag = `[${imgRes.fileName} ${imgRes.sizeStr}]`;
350
- imageAttachments.push({
351
- tag,
352
- fileName: imgRes.fileName,
353
- filePath: imgRes.filePath,
354
- sizeStr: imgRes.sizeStr
355
- });
356
- usedImageNames.add(imgRes.fileName);
357
- input = input.slice(0, cursorPos) + tag + ' ' + input.slice(cursorPos);
358
- cursorPos += tag.length + 1;
359
- }
360
- }
361
- if (foundAnyImage) {
362
- selectedIndex = 0;
363
- render();
364
- return;
365
- }
366
- const normalized = cleaned.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
367
- const lines = normalized.split('\n');
368
- if (lines.length >= 2 || normalized.length > 80) {
369
- const linesCount = lines.length;
370
- const id = pastes.length + 1;
371
- const tag = `[Pasted text #${id} +${linesCount} lines]`;
372
- pastes.push({ id, tag, content: normalized, linesCount });
373
- input = input.slice(0, cursorPos) + tag + input.slice(cursorPos);
374
- cursorPos += tag.length;
375
- selectedIndex = 0;
376
- render();
377
- }
378
- else {
379
- input = input.slice(0, cursorPos) + normalized + input.slice(cursorPos);
380
- cursorPos += normalized.length;
381
- selectedIndex = 0;
382
- render();
383
- }
384
- }
385
- function getTagSpans() {
386
- const spans = [];
387
- for (const p of pastes) {
388
- let pos = 0;
389
- while ((pos = input.indexOf(p.tag, pos)) !== -1) {
390
- spans.push({
391
- start: pos,
392
- end: pos + p.tag.length,
393
- tag: p.tag
394
- });
395
- pos += p.tag.length;
396
- }
397
- }
398
- for (const img of imageAttachments) {
399
- let pos = 0;
400
- while ((pos = input.indexOf(img.tag, pos)) !== -1) {
401
- spans.push({
402
- start: pos,
403
- end: pos + img.tag.length,
404
- tag: img.tag
405
- });
406
- pos += img.tag.length;
407
- }
408
- }
409
- return spans.sort((a, b) => a.start - b.start);
410
- }
411
- let pasteBuffer = '';
412
- let inBracketedPaste = false;
413
- function onData(chunk) {
414
- if (disposed)
415
- return;
416
- const str = chunk.toString('utf8');
417
- // Check for bracketed paste start/end
418
- if (str.includes('\x1b[200~')) {
419
- inBracketedPaste = true;
420
- pasteBuffer = '';
421
- }
422
- if (inBracketedPaste) {
423
- pasteBuffer += str;
424
- if (pasteBuffer.includes('\x1b[201~')) {
425
- inBracketedPaste = false;
426
- handlePaste(pasteBuffer);
427
- pasteBuffer = '';
428
- }
429
- return;
430
- }
431
- // Check for raw multi-character paste with newlines or large size
432
- if (str.length > 1 && (str.includes('\n') || str.includes('\r') || str.length > 80)) {
433
- handlePaste(str);
434
- return;
435
- }
436
- // Handle individual keys
437
- // Ctrl+V (ASCII 22 / 0x16) or Ctrl+P (ASCII 16 / 0x10)
438
- if (str === '\x16' || str === '\x10' || (str.length === 1 && (str.charCodeAt(0) === 22 || str.charCodeAt(0) === 16))) {
439
- saveClipboardImage('image.png', usedImageNames).then((imgRes) => {
139
+ let line = rawLine.trim();
140
+ // Handle direct /image command
141
+ if (line === '/image') {
142
+ try {
143
+ const imgRes = await saveClipboardImage('image.png', usedImageNames);
440
144
  if (imgRes) {
441
145
  const tag = `[${imgRes.fileName} ${imgRes.sizeStr}]`;
442
146
  imageAttachments.push({
@@ -446,242 +150,52 @@ export function askPrompt(opts = {}) {
446
150
  sizeStr: imgRes.sizeStr
447
151
  });
448
152
  usedImageNames.add(imgRes.fileName);
449
- input = input.slice(0, cursorPos) + tag + ' ' + input.slice(cursorPos);
450
- cursorPos += tag.length + 1;
451
- render();
452
- }
453
- }).catch(() => { });
454
- return;
455
- }
456
- // Ctrl+C
457
- if (str === '\x03' || (str.length === 1 && str.charCodeAt(0) === 3)) {
458
- if (lastDropdownLines > 0) {
459
- for (let i = 0; i < lastDropdownLines; i++) {
460
- process.stdout.write(`\n\x1b[2K`);
461
- }
462
- process.stdout.write(`\x1b[${lastDropdownLines}A`);
463
- }
464
- process.stdout.write('\r\x1b[2K\n');
465
- cleanup();
466
- resolve('__CANCEL__');
467
- return;
468
- }
469
- const items = getDropdownItems();
470
- // Enter / Return
471
- if (str === '\r' || str === '\n' || (str.length === 1 && (str.charCodeAt(0) === 13 || str.charCodeAt(0) === 10))) {
472
- // Direct /image command
473
- if (input.trim() === '/image' || (items.length > 0 && selectedIndex >= 0 && selectedIndex < items.length && items[selectedIndex].label === '/image')) {
474
- saveClipboardImage('image.png', usedImageNames).then((imgRes) => {
475
- if (imgRes) {
476
- const tag = `[${imgRes.fileName} ${imgRes.sizeStr}]`;
477
- imageAttachments.push({
478
- tag,
479
- fileName: imgRes.fileName,
480
- filePath: imgRes.filePath,
481
- sizeStr: imgRes.sizeStr
482
- });
483
- usedImageNames.add(imgRes.fileName);
484
- input = `${tag} `;
485
- cursorPos = input.length;
486
- render();
487
- }
488
- else {
489
- process.stdout.write(`\r\x1b[2K${pc.yellow('⚠️ No image in clipboard. Take a screenshot first (Win+Shift+S)\n')}`);
490
- input = '';
491
- cursorPos = 0;
492
- render();
493
- }
494
- }).catch(() => { });
495
- return;
496
- }
497
- if (items.length > 0 && selectedIndex >= 0 && selectedIndex < items.length) {
498
- const selected = items[selectedIndex];
499
- if (input.startsWith('/')) {
500
- input = selected.replacement;
501
- clearBoxAndExit(input);
502
- return;
153
+ line = tag;
503
154
  }
504
155
  else {
505
- // @ mention autocomplete on Enter
506
- input = input.slice(0, selected.replaceStart) + selected.replacement + input.slice(selected.replaceStart + selected.replaceLen);
507
- cursorPos = selected.replaceStart + selected.replacement.length;
508
- render();
156
+ console.log(pc.yellow('⚠️ No image in clipboard. Take a screenshot first (Win+Shift+S)\n'));
157
+ resolve('');
509
158
  return;
510
159
  }
511
160
  }
512
- clearBoxAndExit(input);
513
- return;
514
- }
515
- // Tab
516
- if (str === '\t' || (str.length === 1 && str.charCodeAt(0) === 9)) {
517
- if (items.length > 0 && selectedIndex >= 0 && selectedIndex < items.length) {
518
- const selected = items[selectedIndex];
519
- input = input.slice(0, selected.replaceStart) + selected.replacement + input.slice(selected.replaceStart + selected.replaceLen);
520
- cursorPos = selected.replaceStart + selected.replacement.length;
521
- render();
522
- }
523
- else {
524
- if (lastDropdownLines > 0) {
525
- for (let i = 0; i < lastDropdownLines; i++) {
526
- process.stdout.write(`\n\x1b[2K`);
527
- }
528
- process.stdout.write(`\x1b[${lastDropdownLines}A`);
529
- }
530
- process.stdout.write('\r\x1b[2K');
531
- cleanup();
532
- resolve(`__TOGGLE_MODE__:${input}`);
533
- }
534
- return;
535
- }
536
- // Up Arrow
537
- if (str === '\x1b[A' || str === '\x1bOA') {
538
- if (items.length > 0) {
539
- selectedIndex = (selectedIndex - 1 + items.length) % items.length;
540
- render();
541
- }
542
- else if (GLOBAL_PROMPT_HISTORY.length > 0 && historyIndex > 0) {
543
- if (historyIndex === GLOBAL_PROMPT_HISTORY.length) {
544
- tempDraft = input;
545
- }
546
- historyIndex--;
547
- input = GLOBAL_PROMPT_HISTORY[historyIndex];
548
- cursorPos = input.length;
549
- render();
550
- }
551
- return;
552
- }
553
- // Down Arrow
554
- if (str === '\x1b[B' || str === '\x1bOB') {
555
- if (items.length > 0) {
556
- selectedIndex = (selectedIndex + 1) % items.length;
557
- render();
558
- }
559
- else if (historyIndex < GLOBAL_PROMPT_HISTORY.length) {
560
- historyIndex++;
561
- if (historyIndex === GLOBAL_PROMPT_HISTORY.length) {
562
- input = tempDraft;
563
- }
564
- else {
565
- input = GLOBAL_PROMPT_HISTORY[historyIndex];
566
- }
567
- cursorPos = input.length;
568
- render();
569
- }
570
- return;
571
- }
572
- // Left Arrow (Jump across whole paste badge atomically)
573
- if (str === '\x1b[D' || str === '\x1bOD') {
574
- if (cursorPos > 0) {
575
- const spans = getTagSpans();
576
- const endingSpan = spans.find(s => s.end === cursorPos);
577
- if (endingSpan) {
578
- cursorPos = endingSpan.start;
579
- }
580
- else {
581
- cursorPos--;
582
- const inside = spans.find(s => cursorPos > s.start && cursorPos < s.end);
583
- if (inside)
584
- cursorPos = inside.start;
585
- }
586
- render();
161
+ catch {
162
+ resolve('');
163
+ return;
587
164
  }
588
- return;
589
165
  }
590
- // Right Arrow (Jump across whole paste badge atomically)
591
- if (str === '\x1b[C' || str === '\x1bOC') {
592
- if (cursorPos < input.length) {
593
- const spans = getTagSpans();
594
- const startingSpan = spans.find(s => s.start === cursorPos);
595
- if (startingSpan) {
596
- cursorPos = startingSpan.end;
597
- }
598
- else {
599
- cursorPos++;
600
- const inside = spans.find(s => cursorPos > s.start && cursorPos < s.end);
601
- if (inside)
602
- cursorPos = inside.end;
603
- }
604
- render();
166
+ // Check for pasted file path to image
167
+ if (line.match(/\.(png|jpe?g|webp|gif|bmp)$/i)) {
168
+ const imgRes = await processPastedFilePath(line, usedImageNames);
169
+ if (imgRes) {
170
+ const tag = `[${imgRes.fileName} ${imgRes.sizeStr}]`;
171
+ imageAttachments.push({
172
+ tag,
173
+ fileName: imgRes.fileName,
174
+ filePath: imgRes.filePath,
175
+ sizeStr: imgRes.sizeStr
176
+ });
177
+ usedImageNames.add(imgRes.fileName);
178
+ line = tag;
605
179
  }
606
- return;
607
- }
608
- // Home
609
- if (str === '\x1b[H' || str === '\x1b[1~') {
610
- cursorPos = 0;
611
- render();
612
- return;
613
- }
614
- // End
615
- if (str === '\x1b[F' || str === '\x1b[4~') {
616
- cursorPos = input.length;
617
- render();
618
- return;
619
180
  }
620
- // Delete key (\x1b[3~)
621
- if (str === '\x1b[3~') {
622
- if (cursorPos < input.length) {
623
- const spans = getTagSpans();
624
- const startingSpan = spans.find(s => s.start === cursorPos);
625
- if (startingSpan) {
626
- input = input.slice(0, startingSpan.start) + input.slice(startingSpan.end);
627
- }
628
- else {
629
- input = input.slice(0, cursorPos) + input.slice(cursorPos + 1);
630
- }
631
- selectedIndex = 0;
632
- render();
633
- }
634
- return;
181
+ // Expand image attachment tags to @path
182
+ let expanded = line;
183
+ for (const img of imageAttachments) {
184
+ expanded = expanded.split(img.tag).join(`@${img.filePath.replace(/\\/g, '/')} `);
635
185
  }
636
- // Backspace / DEL handling (handles single or multiple backspaces from mobile IME)
637
- if (str.includes('\x08') || str.includes('\x7f')) {
638
- for (let i = 0; i < str.length; i++) {
639
- const ch = str[i];
640
- if (ch === '\x08' || ch === '\x7f') {
641
- if (cursorPos > 0) {
642
- const spans = getTagSpans();
643
- const endingSpan = spans.find(s => s.end === cursorPos);
644
- if (endingSpan) {
645
- input = input.slice(0, endingSpan.start) + input.slice(endingSpan.end);
646
- cursorPos = endingSpan.start;
647
- }
648
- else {
649
- input = input.slice(0, cursorPos - 1) + input.slice(cursorPos);
650
- cursorPos--;
651
- }
652
- }
653
- }
654
- else if (ch.charCodeAt(0) >= 32) {
655
- const spans = getTagSpans();
656
- const inside = spans.find(s => cursorPos > s.start && cursorPos < s.end);
657
- if (inside)
658
- cursorPos = inside.end;
659
- input = input.slice(0, cursorPos) + ch + input.slice(cursorPos);
660
- cursorPos++;
661
- }
186
+ if (expanded.trim()) {
187
+ if (GLOBAL_PROMPT_HISTORY.length === 0 ||
188
+ GLOBAL_PROMPT_HISTORY[GLOBAL_PROMPT_HISTORY.length - 1] !== expanded.trim()) {
189
+ GLOBAL_PROMPT_HISTORY.push(expanded.trim());
662
190
  }
663
- selectedIndex = 0;
664
- render();
665
- return;
666
- }
667
- // Ignore unknown escape sequences
668
- if (str.startsWith('\x1b')) {
669
- return;
670
- }
671
- // Normal character input - clean control characters
672
- const cleanStr = str.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '');
673
- if (!cleanStr)
674
- return;
675
- const spans = getTagSpans();
676
- const inside = spans.find(s => cursorPos > s.start && cursorPos < s.end);
677
- if (inside) {
678
- cursorPos = inside.end;
679
191
  }
680
- input = input.slice(0, cursorPos) + cleanStr + input.slice(cursorPos);
681
- cursorPos += cleanStr.length;
682
- selectedIndex = 0;
683
- render();
684
- }
685
- process.stdin.on('data', onData);
192
+ console.log();
193
+ resolve(expanded);
194
+ });
195
+ rl.on('SIGINT', () => {
196
+ cleanup();
197
+ console.log();
198
+ resolve('__CANCEL__');
199
+ });
686
200
  });
687
201
  }
@@ -139,7 +139,7 @@ function renderDirectoryHtml(dirPath, relPath, files, port) {
139
139
  ${parentLink}
140
140
  ${items || '<li style="padding: 20px; text-align: center; color: #6e7681;">No visible files in this directory</li>'}
141
141
  </ul>
142
- <div class="footer">devx v1.4.5 &bull; Terminal-Native AI Assistant</div>
142
+ <div class="footer">devx v1.4.7 &bull; Terminal-Native AI Assistant</div>
143
143
  </div>
144
144
  </body>
145
145
  </html>`;
@@ -63,7 +63,7 @@ export class MCPClient {
63
63
  capabilities: {},
64
64
  clientInfo: {
65
65
  name: 'devx',
66
- version: '1.4.5'
66
+ version: '1.4.7'
67
67
  }
68
68
  });
69
69
  if (!initResult) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "termux-dev",
3
- "version": "1.4.5",
3
+ "version": "1.4.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",