termux-dev 1.4.18 → 1.5.0

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
@@ -46,6 +46,10 @@ Built specifically for **Android Termux**, **Windows**, **macOS**, and **Linux**
46
46
  Deep OLED black background designed for distraction-free CLI vibe-coding.
47
47
  - **📜 Smart Session History (`/resume` & `/session`):**
48
48
  Resume sessions with last 20 messages neatly rendered in full Markdown and track unique session IDs (`#a8f9z`).
49
+ - **🔍 Smart Multi-Line Editor (`edit_file 2.0`):**
50
+ Collision-safe code editing with line-number error reporting on duplicate matches and `replaceAll` support.
51
+ - **🧪 Comprehensive Test Suite (Vitest):**
52
+ Full test coverage across core agent loop, file systems, security guard, snapshots, and context history with automated CI.
49
53
  - **⚡ Universal AI Provider Support:**
50
54
  **OpenRouter**, **Google Gemini**, **DeepSeek**, **Groq**, **Mistral**, **OpenAI**, **Anthropic**, **Alibaba**, and local models (**Ollama**, **LM Studio**).
51
55
 
@@ -198,6 +202,10 @@ If you find **devx** useful, please consider giving it a ⭐️ star on [GitHub]
198
202
  Глубокий OLED-чёрный фон в стиле OpenCode для комфортной ночной разработки.
199
203
  - **📜 История сессий с уникальными ID (`/resume` и `/session`):**
200
204
  Восстановление сессий с предпросмотром последних 20 сообщений в Markdown и короткими хэш-ID (`#a8f9z`).
205
+ - **🔍 Умный редактор `edit_file 2.0`:**
206
+ Безопасная замена фрагментов кода с защитой от коллизий при дубликатах, указанием номеров строк и поддержкой `replaceAll`.
207
+ - **🧪 Тестовый фундамент (Vitest):**
208
+ Полное покрытие юнит-тестами ядра агента, файловой системы, безопасности, снимков и истории контекста с авто-тестами в CI.
201
209
  - **⚡ Поддержка любых провайдеров:**
202
210
  **OpenRouter**, **Google Gemini**, **DeepSeek**, **Groq**, **Mistral**, **OpenAI**, **Anthropic**, **Alibaba**, а также локальные **Ollama** и **LM Studio**.
203
211
 
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.18</tspan>
28
+ THE TERMINAL-NATIVE AI CODING AGENT <tspan fill="#00f2fe" font-weight="bold">v1.5.0</tspan>
29
29
  </text>
30
30
 
31
31
  <!-- Top Accent Line -->
Binary file
package/bin/devx.js CHANGED
File without changes
@@ -0,0 +1,41 @@
1
+ import pc from 'picocolors';
2
+ import { getCurrentTheme } from './theme.js';
3
+ export function renderBannerLines(mode = 'full', version = '1.5.0', cols = 80) {
4
+ if (mode === 'off') {
5
+ return [];
6
+ }
7
+ const theme = getCurrentTheme();
8
+ if (mode === 'minimal') {
9
+ return [
10
+ theme.colorFn('⚡ ') + theme.boldFn('TERMUX·DEV') + ' ' + pc.dim(`v${version}`) + pc.dim(' · Type /help for commands')
11
+ ];
12
+ }
13
+ // Full ASCII Banner
14
+ if (cols < 68) {
15
+ // Compact ASCII for narrow mobile screens (~26 chars)
16
+ return [
17
+ theme.colorFn(' █▀▀▄ █▀▀▀ █ █ █ █'),
18
+ theme.colorFn(' █ █ █▀▀▀ ▀▄▀ ▀▄▀ '),
19
+ theme.colorFn(' █▄▄▀ █▄▄▄ ▀ ▀ ▀ '),
20
+ ' ' + theme.boldFn(`v${version}`)
21
+ ];
22
+ }
23
+ // Full wide ASCII banner (53 chars)
24
+ const indent = ' ';
25
+ return [
26
+ indent + theme.colorFn('▀▀▀█▀▀▀ █▀▀▀ █▀▀█ █▄ ▄█ █ █ ▀▄ ▄▀ █▀▀▄ █▀▀▀ █ █'),
27
+ indent + theme.colorFn(' █ █▀▀▀ █▄▄▀ █ █ █ █ █ █ ▀▀ █ █ █▀▀▀ █ █'),
28
+ indent + theme.colorFn(' █ █▄▄▄ █ ▀▄ █ █ ▀▄▄▀ ▄▀ ▀▄ █▄▄▀ █▄▄▄ ▀▄▀ '),
29
+ indent + theme.boldFn(`v${version}`)
30
+ ];
31
+ }
32
+ export function drawBanner(mode = 'full', version = '1.5.0', cols = 80) {
33
+ const lines = renderBannerLines(mode, version, cols);
34
+ if (lines.length > 0) {
35
+ console.log();
36
+ for (const l of lines) {
37
+ console.log(l);
38
+ }
39
+ console.log();
40
+ }
41
+ }
@@ -0,0 +1,64 @@
1
+ import pc from 'picocolors';
2
+ import { getCurrentTheme } from './theme.js';
3
+ export function formatDiffBox(fileName, diffLines, cols = 80, summary, diffColors = false) {
4
+ const theme = getCurrentTheme();
5
+ const boxWidth = Math.max(30, Math.min(cols - 4, 76));
6
+ const cleanFileName = typeof fileName === 'string' ? fileName.split(/[\/\\]/).pop() || 'file' : 'file';
7
+ const fillCount = Math.max(2, boxWidth - 5 - cleanFileName.length);
8
+ const lines = [
9
+ theme.colorFn('┌─ ') + pc.bold(cleanFileName) + ' ' + theme.colorFn('─'.repeat(fillCount) + '┐')
10
+ ];
11
+ const maxShown = Math.min(diffLines.length, 30);
12
+ for (let i = 0; i < maxShown; i++) {
13
+ const rawLine = diffLines[i];
14
+ const match = rawLine.match(/^(\d+)(\s+[+\- ]\s+)(.*)$/);
15
+ if (match) {
16
+ const lineNum = match[1].padStart(4, ' ');
17
+ const symbol = match[2];
18
+ const contentStr = match[3] || '';
19
+ const isAdd = symbol.includes('+');
20
+ const isRemove = symbol.includes('-');
21
+ const prefix = isRemove ? '-' : isAdd ? '+' : ' ';
22
+ const maxCodeLen = Math.max(10, boxWidth - 11);
23
+ const paddedCode = contentStr.length > maxCodeLen
24
+ ? contentStr.substring(0, maxCodeLen - 1) + '…'
25
+ : contentStr.padEnd(maxCodeLen, ' ');
26
+ const innerRow = ` ${lineNum} ${prefix} ${paddedCode} `;
27
+ if (diffColors) {
28
+ if (isRemove) {
29
+ lines.push(theme.colorFn('│') + pc.red(innerRow) + theme.colorFn('│'));
30
+ }
31
+ else if (isAdd) {
32
+ lines.push(theme.colorFn('│') + pc.green(innerRow) + theme.colorFn('│'));
33
+ }
34
+ else {
35
+ lines.push(theme.colorFn('│') + pc.dim(innerRow) + theme.colorFn('│'));
36
+ }
37
+ }
38
+ else {
39
+ if (isRemove || isAdd) {
40
+ lines.push(theme.colorFn('│') + pc.white(innerRow) + theme.colorFn('│'));
41
+ }
42
+ else {
43
+ lines.push(theme.colorFn('│') + pc.dim(innerRow) + theme.colorFn('│'));
44
+ }
45
+ }
46
+ }
47
+ else {
48
+ const maxLen = Math.max(10, boxWidth - 4);
49
+ const padded = rawLine.length > maxLen ? rawLine.substring(0, maxLen - 1) + '…' : rawLine.padEnd(maxLen, ' ');
50
+ lines.push(theme.colorFn('│ ') + pc.white(padded) + theme.colorFn(' │'));
51
+ }
52
+ }
53
+ if (diffLines.length > maxShown) {
54
+ const dots = `... +${diffLines.length - maxShown} more lines`;
55
+ const maxLen = Math.max(10, boxWidth - 4);
56
+ const paddedDots = dots.length > maxLen ? dots.substring(0, maxLen - 1) + '…' : dots.padEnd(maxLen, ' ');
57
+ lines.push(theme.colorFn('│ ') + pc.dim(paddedDots) + theme.colorFn(' │'));
58
+ }
59
+ lines.push(theme.colorFn('└' + '─'.repeat(boxWidth - 2) + '┘'));
60
+ if (summary) {
61
+ lines.push(theme.boldFn(` └─ ${summary}`));
62
+ }
63
+ return lines.join('\n');
64
+ }
@@ -2,6 +2,7 @@ import os from 'os';
2
2
  import { execSync } from 'child_process';
3
3
  import https from 'https';
4
4
  import pc from 'picocolors';
5
+ import { DEVX_VERSION } from '../core/types.js';
5
6
  import { getCurrentTheme } from './theme.js';
6
7
  import { isTermux } from '../core/notify.js';
7
8
  function checkCmd(cmd) {
@@ -18,7 +19,7 @@ async function pingUrl(url) {
18
19
  const req = https.get(url, {
19
20
  timeout: 3000,
20
21
  headers: {
21
- 'User-Agent': 'devx-doctor/1.4.18'
22
+ 'User-Agent': `devx-doctor/${DEVX_VERSION}`
22
23
  }
23
24
  }, (res) => {
24
25
  res.resume(); // consume response data to free up memory and release socket
@@ -1,4 +1,5 @@
1
1
  import pc from 'picocolors';
2
+ import { DEVX_VERSION } from '../core/types.js';
2
3
  import { History } from '../core/history.js';
3
4
  import { Agent } from '../core/loop.js';
4
5
  import { buildSystemPrompt } from '../prompts/builder.js';
@@ -14,13 +15,13 @@ export async function runHeadlessMode(userPrompt, config, options = {}) {
14
15
  const isJson = !!options.json;
15
16
  let history = new History();
16
17
  const sysPrompt = await buildSystemPrompt(planMode);
17
- history.addMessage({ role: 'system', content: sysPrompt });
18
+ history.updateSystemPrompt(sysPrompt);
18
19
  history.addMessage({ role: 'user', content: userPrompt });
19
20
  const provider = createProvider(config);
20
21
  const tools = getTools(planMode);
21
- const guard = new CLIConsoleGuard(isYolo, config.bashAllowlist || []);
22
+ const guard = new CLIConsoleGuard(isYolo);
22
23
  const agent = new Agent(config, provider, tools, history, guard);
23
- const executedTools = [];
24
+ let executedTools = [];
24
25
  let accumulatedText = '';
25
26
  let accumulatedReasoning = '';
26
27
  let isFatalError = false;
@@ -35,7 +36,7 @@ export async function runHeadlessMode(userPrompt, config, options = {}) {
35
36
  });
36
37
  try {
37
38
  if (!isQuiet && !isJson) {
38
- console.log(pc.bold(pc.cyan(`⚡ devx v1.4.18 (headless) | ${planMode ? 'PLAN' : 'AGENT'} | ${config.model}`)));
39
+ console.log(pc.bold(pc.cyan(`⚡ devx v${DEVX_VERSION} (headless) | ${planMode ? 'PLAN' : 'AGENT'} | ${config.model}`)));
39
40
  console.log(pc.dim(`Task: ${userPrompt}\n`));
40
41
  }
41
42
  for await (const event of agent.run(abortController.signal)) {
@@ -67,7 +68,6 @@ export async function runHeadlessMode(userPrompt, config, options = {}) {
67
68
  }
68
69
  }
69
70
  }
70
- globalSnapshotManager.finishTurn();
71
71
  if (isJson) {
72
72
  const summary = UsageTracker.getInstance().getSummary();
73
73
  const output = {
@@ -107,4 +107,7 @@ export async function runHeadlessMode(userPrompt, config, options = {}) {
107
107
  }
108
108
  return 1;
109
109
  }
110
+ finally {
111
+ globalSnapshotManager.finishTurn();
112
+ }
110
113
  }
@@ -0,0 +1,111 @@
1
+ import fs from 'fs/promises';
2
+ import fsSync from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ export const HISTORY_FILE = path.join(os.homedir(), '.devx', 'history.jsonl');
6
+ const MAX_HISTORY_LINES = 500;
7
+ export class PromptHistoryNavigator {
8
+ history = [];
9
+ cursor = -1;
10
+ savedDraft = '';
11
+ filterQuery = '';
12
+ fuzzy = false;
13
+ constructor(initialHistory = [], fuzzy = false) {
14
+ this.history = [...initialHistory];
15
+ this.cursor = this.history.length;
16
+ this.fuzzy = fuzzy;
17
+ }
18
+ static async loadPersistent() {
19
+ try {
20
+ if (!fsSync.existsSync(HISTORY_FILE)) {
21
+ return [];
22
+ }
23
+ const raw = await fs.readFile(HISTORY_FILE, 'utf8');
24
+ const lines = raw
25
+ .split('\n')
26
+ .map((l) => l.trim())
27
+ .filter(Boolean)
28
+ .map((l) => {
29
+ try {
30
+ const parsed = JSON.parse(l);
31
+ return parsed.prompt || parsed.text || l;
32
+ }
33
+ catch {
34
+ return l;
35
+ }
36
+ });
37
+ return lines.slice(-MAX_HISTORY_LINES);
38
+ }
39
+ catch {
40
+ return [];
41
+ }
42
+ }
43
+ static async appendPersistent(prompt) {
44
+ try {
45
+ const dir = path.dirname(HISTORY_FILE);
46
+ if (!fsSync.existsSync(dir)) {
47
+ await fs.mkdir(dir, { recursive: true });
48
+ }
49
+ const entry = JSON.stringify({ prompt, timestamp: Date.now() }) + '\n';
50
+ await fs.appendFile(HISTORY_FILE, entry, 'utf8');
51
+ }
52
+ catch { }
53
+ }
54
+ add(entry) {
55
+ const trimmed = entry.trim();
56
+ if (!trimmed)
57
+ return;
58
+ if (this.history.length > 0 && this.history[this.history.length - 1] === trimmed) {
59
+ return;
60
+ }
61
+ this.history.push(trimmed);
62
+ if (this.history.length > MAX_HISTORY_LINES) {
63
+ this.history.shift();
64
+ }
65
+ this.cursor = this.history.length;
66
+ this.filterQuery = '';
67
+ PromptHistoryNavigator.appendPersistent(trimmed).catch(() => { });
68
+ }
69
+ getHistory() {
70
+ return [...this.history];
71
+ }
72
+ navigateUp(currentText) {
73
+ if (this.history.length === 0)
74
+ return null;
75
+ if (this.cursor === this.history.length) {
76
+ this.savedDraft = currentText;
77
+ this.filterQuery = currentText.toLowerCase().trim();
78
+ }
79
+ let nextCursor = this.cursor - 1;
80
+ while (nextCursor >= 0) {
81
+ const item = this.history[nextCursor];
82
+ if (!this.fuzzy || !this.filterQuery || item.toLowerCase().includes(this.filterQuery)) {
83
+ this.cursor = nextCursor;
84
+ return item;
85
+ }
86
+ nextCursor--;
87
+ }
88
+ return null;
89
+ }
90
+ navigateDown() {
91
+ if (this.cursor >= this.history.length)
92
+ return null;
93
+ let nextCursor = this.cursor + 1;
94
+ while (nextCursor < this.history.length) {
95
+ const item = this.history[nextCursor];
96
+ if (!this.fuzzy || !this.filterQuery || item.toLowerCase().includes(this.filterQuery)) {
97
+ this.cursor = nextCursor;
98
+ return item;
99
+ }
100
+ nextCursor++;
101
+ }
102
+ // Reached bottom, restore draft
103
+ this.cursor = this.history.length;
104
+ return this.savedDraft;
105
+ }
106
+ reset() {
107
+ this.cursor = this.history.length;
108
+ this.savedDraft = '';
109
+ this.filterQuery = '';
110
+ }
111
+ }