termux-dev 1.5.0 → 1.8.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.
Files changed (98) hide show
  1. package/README.md +215 -163
  2. package/assets/banner.svg +23 -28
  3. package/assets/preview.png +0 -0
  4. package/assets/preview.svg +94 -0
  5. package/dist/cli/banner.js +7 -3
  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/headless.js +29 -14
  15. package/dist/cli/index.js +1395 -474
  16. package/dist/cli/keys.js +69 -0
  17. package/dist/cli/liveblock.js +45 -0
  18. package/dist/cli/log.js +19 -0
  19. package/dist/cli/markdown.js +158 -36
  20. package/dist/cli/mcp-panel.js +432 -0
  21. package/dist/cli/permission.js +84 -0
  22. package/dist/cli/picker.js +135 -0
  23. package/dist/cli/prompt.js +338 -121
  24. package/dist/cli/prompt_palette.js +6 -0
  25. package/dist/cli/resume.js +15 -0
  26. package/dist/cli/rewind.js +51 -0
  27. package/dist/cli/runtime_errors.js +6 -0
  28. package/dist/cli/server.js +85 -24
  29. package/dist/cli/settings.js +307 -0
  30. package/dist/cli/statusline.js +27 -1
  31. package/dist/cli/theme.js +126 -108
  32. package/dist/cli/transcript.js +114 -0
  33. package/dist/cli/typeahead.js +371 -0
  34. package/dist/cli/ui.js +1021 -0
  35. package/dist/cli/updater.js +23 -21
  36. package/dist/cli/usage-panel.js +242 -0
  37. package/dist/cli/width.js +178 -0
  38. package/dist/core/capabilities.js +433 -0
  39. package/dist/core/catalog-util.js +27 -0
  40. package/dist/core/checkpoint.js +90 -0
  41. package/dist/core/context-stats.js +29 -0
  42. package/dist/core/cost.js +191 -0
  43. package/dist/core/formats.js +199 -0
  44. package/dist/core/history.js +51 -1
  45. package/dist/core/hooks.js +79 -0
  46. package/dist/core/i18n.js +42 -0
  47. package/dist/core/local-config.js +69 -0
  48. package/dist/core/locales/de.js +210 -0
  49. package/dist/core/locales/en.js +221 -0
  50. package/dist/core/locales/es.js +210 -0
  51. package/dist/core/locales/fr.js +210 -0
  52. package/dist/core/locales/ja.js +210 -0
  53. package/dist/core/locales/pt.js +210 -0
  54. package/dist/core/locales/ru.js +210 -0
  55. package/dist/core/locales/uk.js +210 -0
  56. package/dist/core/locales/zh.js +210 -0
  57. package/dist/core/loop.js +125 -49
  58. package/dist/core/memory.js +39 -12
  59. package/dist/core/models.js +33 -20
  60. package/dist/core/network.js +12 -11
  61. package/dist/core/notify.js +54 -0
  62. package/dist/core/planfile.js +43 -0
  63. package/dist/core/price-catalog.json +1 -0
  64. package/dist/core/prices.js +249 -0
  65. package/dist/core/reasoning-catalog.json +1 -0
  66. package/dist/core/reasoning.js +393 -0
  67. package/dist/core/redact.js +41 -0
  68. package/dist/core/secure-file.js +22 -0
  69. package/dist/core/session.js +16 -5
  70. package/dist/core/snapshot.js +14 -2
  71. package/dist/core/title.js +61 -0
  72. package/dist/core/types.js +1 -1
  73. package/dist/core/usage.js +43 -7
  74. package/dist/core/vision-catalog.json +1 -0
  75. package/dist/core/vision.js +123 -0
  76. package/dist/mcp/client.js +36 -5
  77. package/dist/mcp/http-client.js +174 -0
  78. package/dist/mcp/manager.js +330 -93
  79. package/dist/permissions/guard.js +385 -117
  80. package/dist/prompts/builder.js +46 -38
  81. package/dist/providers/anthropic.js +517 -0
  82. package/dist/providers/index.js +18 -1
  83. package/dist/providers/openai.js +138 -125
  84. package/dist/providers/shared.js +116 -0
  85. package/dist/providers/toolguard.js +101 -0
  86. package/dist/tools/bash.js +141 -51
  87. package/dist/tools/diagnostics.js +76 -48
  88. package/dist/tools/fs.js +77 -5
  89. package/dist/tools/index.js +2 -1
  90. package/dist/tools/mcp.js +72 -0
  91. package/dist/tools/packages.js +16 -1
  92. package/dist/tools/plan.js +8 -8
  93. package/dist/tools/questions.js +50 -54
  94. package/dist/tools/server.js +6 -0
  95. package/dist/tools/todo.js +20 -18
  96. package/dist/tools/web.js +143 -8
  97. package/package.json +4 -2
  98. package/assets/preview_work1.png +0 -0
@@ -34,7 +34,7 @@ async function pingUrl(url) {
34
34
  }
35
35
  export async function runDoctor(config) {
36
36
  const theme = getCurrentTheme();
37
- console.log('\n' + theme.boldFn('🩺 devx Doctor — System & Environment Health Diagnostics'));
37
+ console.log('\n' + theme.boldFn('devx Doctor — System & Environment Health Diagnostics'));
38
38
  console.log(pc.dim('──────────────────────────────────────────────────────────────────'));
39
39
  const envChecks = [];
40
40
  const toolChecks = [];
@@ -124,7 +124,7 @@ export async function runDoctor(config) {
124
124
  const renderSection = (title, items) => {
125
125
  console.log('\n' + theme.colorFn(pc.bold(`• ${title}`)));
126
126
  for (const item of items) {
127
- let icon = pc.green('✔');
127
+ let icon = pc.green('✓');
128
128
  if (item.status === 'warn')
129
129
  icon = pc.yellow('▲');
130
130
  if (item.status === 'fail')
@@ -139,10 +139,10 @@ export async function runDoctor(config) {
139
139
  console.log('\n' + pc.dim('──────────────────────────────────────────────────────────────────'));
140
140
  const hasFailures = [...envChecks, ...toolChecks, ...keyChecks, ...netChecks].some(i => i.status === 'fail');
141
141
  if (hasFailures) {
142
- console.log(pc.red(pc.bold('⚠ Doctor found critical issues that might prevent devx from functioning properly.')));
142
+ console.log(pc.red(pc.bold('Doctor found critical issues that might prevent devx from functioning properly.')));
143
143
  }
144
144
  else {
145
- console.log(theme.boldFn('🎉 Everything looks healthy! devx is fully configured and ready for vibe-coding.'));
145
+ console.log(theme.boldFn('Everything looks healthy! devx is fully configured and ready for vibe-coding.'));
146
146
  }
147
147
  console.log();
148
148
  }
@@ -0,0 +1,80 @@
1
+ import { LEVELS, effectiveReasoning, parseEffortArg, withEffort, } from '../core/reasoning.js';
2
+ const modelName = (config) => config.model || 'the current model';
3
+ /** Where the levels in the menu come from. */
4
+ const SOURCE_NOTE = {
5
+ endpoint: 'Levels reported by the endpoint.',
6
+ catalog: 'Levels from the models.dev catalogue.',
7
+ rules: 'Levels inferred from the model name.',
8
+ profile: '',
9
+ none: '',
10
+ };
11
+ /** What would actually be sent for a level, in one line. */
12
+ export function sentText(params) {
13
+ return params ? `sends ${JSON.stringify(params)}` : 'sends nothing (the model default applies)';
14
+ }
15
+ function offered(levels) {
16
+ return ['auto', ...levels].join(', ');
17
+ }
18
+ /** Sets the level for the current model (or clears it) and describes what happened. */
19
+ export function chooseEffort(config, choice) {
20
+ const model = modelName(config);
21
+ if (choice === 'auto') {
22
+ config.effort = withEffort(config.effort, config.model, null);
23
+ return { kind: 'ok', text: `effort auto for ${model}`, detail: 'the model default applies, no reasoning setting is sent' };
24
+ }
25
+ config.effort = withEffort(config.effort, config.model, choice);
26
+ const { caps, params } = effectiveReasoning({ ...config, effortOverride: undefined });
27
+ return { kind: 'ok', text: `effort ${choice} for ${model}`, detail: `${caps.describe(choice)}; ${sentText(params)}` };
28
+ }
29
+ export function buildEffortPicker(config) {
30
+ const { caps, level } = effectiveReasoning({ ...config, effortOverride: undefined });
31
+ const missing = LEVELS.filter((l) => !caps.levels.includes(l));
32
+ const notes = [];
33
+ if (missing.length > 0)
34
+ notes.push(`Not offered by this model: ${missing.join(', ')}.`);
35
+ if (caps.alwaysOn)
36
+ notes.push('This model cannot switch reasoning off.');
37
+ const source = SOURCE_NOTE[caps.source];
38
+ if (source)
39
+ notes.push(source);
40
+ if (caps.confidence === 'guess')
41
+ notes.push('How the setting is sent to this endpoint is inferred. If the endpoint rejects it, devx retries without it.');
42
+ if (caps.confidence === 'custom')
43
+ notes.push(caps.note);
44
+ return {
45
+ title: 'effort',
46
+ subtitle: modelName(config),
47
+ current: level ?? 'auto',
48
+ items: [
49
+ { value: 'auto', label: 'auto', detail: 'model default, nothing is sent' },
50
+ ...caps.levels.map((l) => ({ value: l, label: l, detail: caps.describe(l) })),
51
+ ],
52
+ notes,
53
+ };
54
+ }
55
+ /**
56
+ * Handles `/effort [argument]`. Without an argument it returns a picker with the levels of
57
+ * the current model; with one it sets it directly. Everything else is an explanation.
58
+ */
59
+ export function effortCommand(config, arg) {
60
+ const model = modelName(config);
61
+ const { caps } = effectiveReasoning({ ...config, effortOverride: undefined });
62
+ if (!caps.supported) {
63
+ const hint = caps.alwaysOn
64
+ ? ''
65
+ : ' If you know how this model takes a reasoning setting, describe it in ~/.devxrc.json under "reasoningProfiles" (see the README).';
66
+ return { type: 'say', say: { kind: 'warn', text: `${model} has no reasoning setting.`, detail: `${caps.note}${hint}` } };
67
+ }
68
+ if (!arg.trim())
69
+ return { type: 'pick', picker: buildEffortPicker(config) };
70
+ const parsed = parseEffortArg(arg);
71
+ if (parsed.kind === 'auto')
72
+ return { type: 'say', say: chooseEffort(config, 'auto') };
73
+ if (parsed.kind === 'invalid') {
74
+ return { type: 'say', say: { kind: 'warn', text: `Unknown level "${parsed.text}".`, detail: `${model} offers: ${offered(caps.levels)}.` } };
75
+ }
76
+ if (!caps.levels.includes(parsed.level)) {
77
+ return { type: 'say', say: { kind: 'warn', text: `${model} does not offer "${parsed.level}".`, detail: `It offers: ${offered(caps.levels)}.` } };
78
+ }
79
+ return { type: 'say', say: chooseEffort(config, parsed.level) };
80
+ }
@@ -9,7 +9,7 @@ export class SessionExporter {
9
9
  fileName += '.md';
10
10
  const filePath = path.resolve(process.cwd(), fileName);
11
11
  const lines = [
12
- `# 📝 devx Session Transcript`,
12
+ `# devx Session Transcript`,
13
13
  ``,
14
14
  `- **Date:** ${new Date().toLocaleString()}`,
15
15
  `- **Model:** \`${model}\``,
@@ -23,7 +23,7 @@ export class SessionExporter {
23
23
  if (msg.role === 'system')
24
24
  continue; // Skip raw system prompt
25
25
  if (msg.role === 'user') {
26
- lines.push(`## 👤 User\n`);
26
+ lines.push(`## User\n`);
27
27
  lines.push(msg.content || '');
28
28
  if (msg.images && msg.images.length > 0) {
29
29
  lines.push(`\n*Attached Images: ${msg.images.map(i => i.path).join(', ')}*`);
@@ -31,7 +31,7 @@ export class SessionExporter {
31
31
  lines.push(`\n---\n`);
32
32
  }
33
33
  else if (msg.role === 'assistant') {
34
- lines.push(`## 🤖 Assistant\n`);
34
+ lines.push(`## Assistant\n`);
35
35
  if (msg.content) {
36
36
  lines.push(msg.content);
37
37
  }
package/dist/cli/files.js CHANGED
@@ -49,22 +49,28 @@ export async function scanProjectFiles(force = false) {
49
49
  lastScanTime = now;
50
50
  return results;
51
51
  }
52
+ // Formats the image APIs accept (SVG is text, not an image for a model).
52
53
  const IMAGE_EXTS = {
53
54
  '.png': 'image/png',
54
55
  '.jpg': 'image/jpeg',
55
56
  '.jpeg': 'image/jpeg',
56
57
  '.webp': 'image/webp',
57
- '.gif': 'image/gif',
58
- '.svg': 'image/svg+xml'
58
+ '.gif': 'image/gif'
59
59
  };
60
+ /** Above this an API usually refuses the image (Anthropic: 5 MB). */
61
+ export const IMAGE_WARN_BYTES = 5 * 1024 * 1024;
62
+ /** Above this the image is not sent at all. */
63
+ export const IMAGE_MAX_BYTES = 20 * 1024 * 1024;
60
64
  export async function resolveAtMentions(text) {
61
- const mentionRegex = /@([a-zA-Z0-9_\-./\u0400-\u04FF]+)/g;
65
+ // @path, or @"path with spaces" for a path that has unusual characters
66
+ const mentionRegex = /@(?:"([^"\n]+)"|([a-zA-Z0-9_\-./\u0400-\u04FF]+))/g;
62
67
  const attachments = [];
63
68
  const images = [];
69
+ const warnings = [];
64
70
  const seenPaths = new Set();
65
71
  let match;
66
72
  while ((match = mentionRegex.exec(text)) !== null) {
67
- const rawPath = match[1];
73
+ const rawPath = match[1] ?? match[2];
68
74
  const resolved = path.resolve(process.cwd(), rawPath);
69
75
  if (seenPaths.has(resolved))
70
76
  continue;
@@ -75,6 +81,14 @@ export async function resolveAtMentions(text) {
75
81
  const ext = path.extname(resolved).toLowerCase();
76
82
  if (stat.isFile()) {
77
83
  if (IMAGE_EXTS[ext]) {
84
+ const name = path.basename(rawPath);
85
+ if (stat.size > IMAGE_MAX_BYTES) {
86
+ warnings.push(`${name} is ${(stat.size / 1048576).toFixed(0)} MB: too large, it was not sent.`);
87
+ continue;
88
+ }
89
+ if (stat.size > IMAGE_WARN_BYTES) {
90
+ warnings.push(`${name} is ${(stat.size / 1048576).toFixed(1)} MB: some providers refuse images above 5 MB.`);
91
+ }
78
92
  const buf = await fs.readFile(resolved);
79
93
  const b64 = buf.toString('base64');
80
94
  const dataUrl = `data:${IMAGE_EXTS[ext]};base64,${b64}`;
@@ -89,5 +103,5 @@ export async function resolveAtMentions(text) {
89
103
  catch { }
90
104
  }
91
105
  }
92
- return { text, attachments, images };
106
+ return { text, attachments, images, warnings };
93
107
  }
@@ -4,10 +4,12 @@ import { History } from '../core/history.js';
4
4
  import { Agent } from '../core/loop.js';
5
5
  import { buildSystemPrompt } from '../prompts/builder.js';
6
6
  import { createProvider } from '../providers/index.js';
7
+ import { refreshFormats } from '../core/formats.js';
7
8
  import { getTools } from '../tools/index.js';
8
9
  import { CLIConsoleGuard } from '../permissions/guard.js';
9
10
  import { globalSnapshotManager } from '../core/snapshot.js';
10
11
  import { UsageTracker } from '../core/usage.js';
12
+ import { MCPManager } from '../mcp/manager.js';
11
13
  export async function runHeadlessMode(userPrompt, config, options = {}) {
12
14
  const planMode = !!options.planMode;
13
15
  const isYolo = !!options.yolo || !!config.autoApprove;
@@ -17,26 +19,33 @@ export async function runHeadlessMode(userPrompt, config, options = {}) {
17
19
  const sysPrompt = await buildSystemPrompt(planMode);
18
20
  history.updateSystemPrompt(sysPrompt);
19
21
  history.addMessage({ role: 'user', content: userPrompt });
20
- const provider = createProvider(config);
21
- const tools = getTools(planMode);
22
- const guard = new CLIConsoleGuard(isYolo);
23
- const agent = new Agent(config, provider, tools, history, guard);
24
22
  let executedTools = [];
25
23
  let accumulatedText = '';
26
24
  let accumulatedReasoning = '';
27
25
  let isFatalError = false;
28
26
  let errorMessage = '';
29
27
  const abortController = new AbortController();
30
- process.on('SIGINT', () => {
28
+ const onAbort = () => {
29
+ if (abortController.signal.aborted)
30
+ return;
31
31
  abortController.abort();
32
32
  if (!isJson) {
33
33
  console.error(pc.yellow('\n[devx headless] Aborted by user (SIGINT)'));
34
34
  }
35
- process.exit(130);
36
- });
35
+ };
36
+ process.on('SIGINT', onAbort);
37
+ options.signal?.addEventListener('abort', onAbort, { once: true });
38
+ if (options.signal?.aborted)
39
+ onAbort();
37
40
  try {
41
+ await MCPManager.getInstance().init(undefined, config.projectMcpTrusted === true);
42
+ await refreshFormats(config).catch(() => null);
43
+ const provider = createProvider(config);
44
+ const tools = getTools(planMode);
45
+ const guard = new CLIConsoleGuard(isYolo);
46
+ const agent = new Agent(config, provider, tools, history, guard);
38
47
  if (!isQuiet && !isJson) {
39
- console.log(pc.bold(pc.cyan(`⚡ devx v${DEVX_VERSION} (headless) | ${planMode ? 'PLAN' : 'AGENT'} | ${config.model}`)));
48
+ console.log(pc.bold(pc.cyan(`devx v${DEVX_VERSION} (headless) | ${planMode ? 'PLAN' : 'AGENT'} | ${config.model}`)));
40
49
  console.log(pc.dim(`Task: ${userPrompt}\n`));
41
50
  }
42
51
  for await (const event of agent.run(abortController.signal)) {
@@ -68,10 +77,13 @@ export async function runHeadlessMode(userPrompt, config, options = {}) {
68
77
  }
69
78
  }
70
79
  }
80
+ const cancelled = abortController.signal.aborted;
81
+ if (cancelled)
82
+ errorMessage = 'Operation cancelled by user';
71
83
  if (isJson) {
72
84
  const summary = UsageTracker.getInstance().getSummary();
73
85
  const output = {
74
- success: !isFatalError,
86
+ success: !isFatalError && !cancelled,
75
87
  model: config.model,
76
88
  prompt: userPrompt,
77
89
  result: accumulatedText,
@@ -83,7 +95,7 @@ export async function runHeadlessMode(userPrompt, config, options = {}) {
83
95
  cost: summary.totalCost,
84
96
  totalBytes: summary.totalBytes
85
97
  },
86
- error: isFatalError ? errorMessage : undefined
98
+ error: isFatalError || cancelled ? errorMessage : undefined
87
99
  };
88
100
  console.log(JSON.stringify(output, null, 2));
89
101
  }
@@ -92,22 +104,25 @@ export async function runHeadlessMode(userPrompt, config, options = {}) {
92
104
  process.stdout.write('\n');
93
105
  }
94
106
  if (isFatalError) {
95
- console.error(pc.red(`\n❌ Error: ${errorMessage}`));
107
+ console.error(pc.red(`\nError: ${errorMessage}`));
96
108
  return 1;
97
109
  }
98
110
  }
99
- return isFatalError ? 1 : 0;
111
+ return cancelled ? 130 : isFatalError ? 1 : 0;
100
112
  }
101
113
  catch (err) {
102
114
  if (isJson) {
103
115
  console.log(JSON.stringify({ success: false, error: err.message }, null, 2));
104
116
  }
105
117
  else {
106
- console.error(pc.red(`\n❌ Fatal Headless Execution Error: ${err.message}`));
118
+ console.error(pc.red(`\nFatal Headless Execution Error: ${err.message}`));
107
119
  }
108
- return 1;
120
+ return abortController.signal.aborted ? 130 : 1;
109
121
  }
110
122
  finally {
123
+ process.removeListener('SIGINT', onAbort);
124
+ options.signal?.removeEventListener('abort', onAbort);
111
125
  globalSnapshotManager.finishTurn();
126
+ MCPManager.getInstance().stopAll();
112
127
  }
113
128
  }