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.
- package/README.md +214 -166
- package/assets/banner.svg +23 -28
- package/assets/preview.png +0 -0
- package/assets/preview.svg +101 -10
- package/dist/cli/banner.js +3 -4
- package/dist/cli/clipboard-write.js +55 -0
- package/dist/cli/clipboard.js +182 -106
- package/dist/cli/colors.js +67 -0
- package/dist/cli/confirm.js +50 -0
- package/dist/cli/doctor.js +4 -4
- package/dist/cli/effort.js +80 -0
- package/dist/cli/export.js +3 -3
- package/dist/cli/files.js +19 -5
- package/dist/cli/finder.js +241 -0
- package/dist/cli/headless.js +23 -11
- package/dist/cli/index.js +1557 -744
- package/dist/cli/keys.js +69 -0
- package/dist/cli/liveblock.js +45 -0
- package/dist/cli/log.js +19 -0
- package/dist/cli/logo.js +30 -0
- package/dist/cli/markdown.js +174 -37
- package/dist/cli/mcp-panel.js +432 -0
- package/dist/cli/permission.js +84 -0
- package/dist/cli/picker.js +143 -0
- package/dist/cli/prompt.js +256 -56
- package/dist/cli/resume.js +15 -0
- package/dist/cli/rewind.js +51 -0
- package/dist/cli/runtime_errors.js +6 -0
- package/dist/cli/server.js +85 -24
- package/dist/cli/settings.js +307 -0
- package/dist/cli/statusline.js +1 -1
- package/dist/cli/theme.js +126 -108
- package/dist/cli/transcript.js +134 -0
- package/dist/cli/typeahead.js +371 -0
- package/dist/cli/ui.js +1022 -0
- package/dist/cli/updater.js +23 -21
- package/dist/cli/usage-panel.js +242 -0
- package/dist/cli/welcome.js +56 -0
- package/dist/cli/width.js +178 -0
- package/dist/core/capabilities.js +433 -0
- package/dist/core/catalog-util.js +27 -0
- package/dist/core/checkpoint.js +90 -0
- package/dist/core/context-stats.js +29 -0
- package/dist/core/cost.js +191 -0
- package/dist/core/formats.js +199 -0
- package/dist/core/history.js +51 -1
- package/dist/core/hooks.js +79 -0
- package/dist/core/i18n.js +42 -0
- package/dist/core/local-config.js +69 -0
- package/dist/core/locales/de.js +265 -0
- package/dist/core/locales/en.js +276 -0
- package/dist/core/locales/es.js +265 -0
- package/dist/core/locales/fr.js +265 -0
- package/dist/core/locales/ja.js +265 -0
- package/dist/core/locales/pt.js +265 -0
- package/dist/core/locales/ru.js +265 -0
- package/dist/core/locales/uk.js +265 -0
- package/dist/core/locales/zh.js +265 -0
- package/dist/core/loop.js +125 -49
- package/dist/core/memory.js +39 -12
- package/dist/core/models.js +33 -20
- package/dist/core/network.js +12 -11
- package/dist/core/notify.js +54 -0
- package/dist/core/planfile.js +43 -0
- package/dist/core/price-catalog.json +1 -0
- package/dist/core/prices.js +249 -0
- package/dist/core/reasoning-catalog.json +1 -0
- package/dist/core/reasoning.js +393 -0
- package/dist/core/redact.js +41 -0
- package/dist/core/secure-file.js +22 -0
- package/dist/core/session.js +16 -5
- package/dist/core/snapshot.js +14 -2
- package/dist/core/title.js +61 -0
- package/dist/core/types.js +1 -1
- package/dist/core/usage.js +43 -7
- package/dist/core/vision-catalog.json +1 -0
- package/dist/core/vision.js +123 -0
- package/dist/mcp/client.js +36 -5
- package/dist/mcp/http-client.js +174 -0
- package/dist/mcp/manager.js +330 -93
- package/dist/permissions/guard.js +385 -117
- package/dist/prompts/builder.js +46 -38
- package/dist/providers/anthropic.js +517 -0
- package/dist/providers/index.js +18 -1
- package/dist/providers/openai.js +138 -125
- package/dist/providers/shared.js +116 -0
- package/dist/providers/toolguard.js +101 -0
- package/dist/tools/bash.js +141 -51
- package/dist/tools/diagnostics.js +76 -48
- package/dist/tools/fs.js +77 -5
- package/dist/tools/index.js +2 -1
- package/dist/tools/mcp.js +72 -0
- package/dist/tools/packages.js +16 -1
- package/dist/tools/plan.js +8 -8
- package/dist/tools/questions.js +50 -54
- package/dist/tools/server.js +6 -0
- package/dist/tools/todo.js +20 -18
- package/dist/tools/web.js +143 -8
- package/package.json +1 -1
package/dist/cli/doctor.js
CHANGED
|
@@ -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('
|
|
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('
|
|
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('
|
|
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
|
+
}
|
package/dist/cli/export.js
CHANGED
|
@@ -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
|
-
`#
|
|
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(`##
|
|
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(`##
|
|
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
|
-
|
|
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
|
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { LiveBlock } from './liveblock.js';
|
|
2
|
+
import { WORD_BACKSPACE, normalizeKey, splitKeys } from './keys.js';
|
|
3
|
+
import { windowItems } from './picker.js';
|
|
4
|
+
import { palette } from './ui.js';
|
|
5
|
+
import { displayWidth, padEndWidth, tailWidth, truncateWidth, wrapWords } from './width.js';
|
|
6
|
+
import { t } from '../core/i18n.js';
|
|
7
|
+
/** Every word typed must be found in the label, the id or the keywords; matches at the start of the label come first. */
|
|
8
|
+
export function filterItems(items, query) {
|
|
9
|
+
const words = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
10
|
+
const pinned = items.filter((i) => i.pinned);
|
|
11
|
+
const rest = items.filter((i) => !i.pinned);
|
|
12
|
+
if (words.length === 0)
|
|
13
|
+
return [...pinned, ...rest];
|
|
14
|
+
const scored = [];
|
|
15
|
+
for (const item of rest) {
|
|
16
|
+
const hay = `${item.label} ${item.keywords ?? ''}`.toLowerCase();
|
|
17
|
+
if (!words.every((w) => hay.includes(w)))
|
|
18
|
+
continue;
|
|
19
|
+
const label = item.label.toLowerCase();
|
|
20
|
+
scored.push({ item, score: label.startsWith(words[0]) ? 0 : label.includes(words[0]) ? 1 : 2 });
|
|
21
|
+
}
|
|
22
|
+
scored.sort((a, b) => a.score - b.score); // stable: the original order is kept inside a score
|
|
23
|
+
return [...pinned, ...scored.map((s) => s.item)];
|
|
24
|
+
}
|
|
25
|
+
function hintLine(width, typing) {
|
|
26
|
+
const variants = [
|
|
27
|
+
typing
|
|
28
|
+
? [['type', t('finder.hint.filter')], ['↑↓', t('picker.hint.move')], ['enter', t('picker.hint.select')], ['esc', t('picker.hint.cancel')]]
|
|
29
|
+
: [['enter', t('picker.hint.confirm')], ['esc', t('picker.hint.cancel')]],
|
|
30
|
+
[['↑↓', ''], ['enter', ''], ['esc', '']],
|
|
31
|
+
[['enter', ''], ['esc', '']],
|
|
32
|
+
];
|
|
33
|
+
for (const items of variants) {
|
|
34
|
+
const text = items.map(([k, label]) => palette.text(k) + (label ? ' ' + palette.faint(label) : '')).join(palette.faint(items[0][1] ? ' · ' : ' '));
|
|
35
|
+
if (displayWidth(text) <= width)
|
|
36
|
+
return text;
|
|
37
|
+
}
|
|
38
|
+
return '';
|
|
39
|
+
}
|
|
40
|
+
/** The card around some content rows; `rows` are already padded to the inner width by the caller's `box`. */
|
|
41
|
+
function frame(title, subtitle, cols, body, footer) {
|
|
42
|
+
if (cols < 60) {
|
|
43
|
+
const width = Math.max(20, cols - 1);
|
|
44
|
+
const head = ' ' + palette.bold(palette.accent(title)) + (subtitle ? palette.faint(' · ' + truncateWidth(subtitle, Math.max(6, width - displayWidth(title) - 6))) : '');
|
|
45
|
+
const plain = (c, shaded = false) => (shaded ? palette.shade(padEndWidth(' ' + c, width)) : ' ' + c);
|
|
46
|
+
return [head, ...body(width - 1, plain), ' ' + footer];
|
|
47
|
+
}
|
|
48
|
+
const W = Math.min(cols - 1, 76);
|
|
49
|
+
const iw = W - 4;
|
|
50
|
+
const rule = palette.rule;
|
|
51
|
+
const box = (content, shaded = false) => rule('│') + (shaded ? palette.shade(' ' + padEndWidth(content, iw) + ' ') : ' ' + padEndWidth(content, iw) + ' ') + rule('│');
|
|
52
|
+
const head = ' ' + palette.bold(palette.accent(title)) + ' ';
|
|
53
|
+
const sub = subtitle ? ' ' + palette.muted(truncateWidth(subtitle, Math.max(6, W - 12 - displayWidth(title)))) + ' ' : '';
|
|
54
|
+
return [
|
|
55
|
+
rule('╭─') + head + rule('─'.repeat(Math.max(0, W - 4 - displayWidth(head) - displayWidth(sub)))) + sub + rule('─╮'),
|
|
56
|
+
box(''),
|
|
57
|
+
...body(iw, box),
|
|
58
|
+
rule('╰' + '─'.repeat(W - 2) + '╯'),
|
|
59
|
+
' ' + footer,
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
/** Where the focus starts: on the first match, not on a pinned row such as "type a name". */
|
|
63
|
+
export function firstMatch(shown) {
|
|
64
|
+
const i = shown.findIndex((item) => !item.pinned);
|
|
65
|
+
return i === -1 ? 0 : i;
|
|
66
|
+
}
|
|
67
|
+
export function renderFinder(options, query, shown, focus, cols, termRows = 40) {
|
|
68
|
+
const maxItems = Math.max(3, termRows - 9 - (options.notes?.length ?? 0));
|
|
69
|
+
return frame(options.title, options.subtitle ?? `${shown.filter((i) => !i.pinned).length}/${options.items.filter((i) => !i.pinned).length}`, cols, (iw, box) => {
|
|
70
|
+
const lines = [];
|
|
71
|
+
const typed = query ? palette.text(tailWidth(query, Math.max(4, iw - 6))) : palette.faint(truncateWidth(options.placeholder ?? t('finder.placeholder'), Math.max(4, iw - 6)));
|
|
72
|
+
lines.push(box(' ' + palette.accent('/') + ' ' + (query ? typed + palette.accent('█') : palette.accent('█') + typed)));
|
|
73
|
+
lines.push(box(palette.rule('─'.repeat(Math.max(0, iw - 1)))));
|
|
74
|
+
if (shown.length === 0) {
|
|
75
|
+
lines.push(box(' ' + palette.faint(t('finder.empty'))));
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
const labelWidth = Math.min(Math.min(options.labelWidth ?? 24, Math.max(...shown.map((i) => displayWidth(i.label)), 4)) + 2, Math.max(10, iw - 12));
|
|
79
|
+
const rows = shown.map((item, index) => {
|
|
80
|
+
const focused = index === focus;
|
|
81
|
+
const isCurrent = options.current !== undefined && item.value === options.current;
|
|
82
|
+
const mark = focused ? palette.accent('❯') : ' ';
|
|
83
|
+
const dot = item.pinned ? palette.accent('+') : isCurrent ? palette.accent('●') : palette.faint('○');
|
|
84
|
+
const label = focused ? palette.bold(palette.text(truncateWidth(item.label, labelWidth - 1))) : palette.muted(truncateWidth(item.label, labelWidth - 1));
|
|
85
|
+
const prefix = ` ${mark} ${dot} ${padEndWidth(label, labelWidth)}`;
|
|
86
|
+
const room = Math.max(0, iw - displayWidth(prefix) - 1);
|
|
87
|
+
const detail = item.detail && room > 6 ? palette.faint(truncateWidth(item.detail, room)) : '';
|
|
88
|
+
return { text: prefix + detail, index };
|
|
89
|
+
});
|
|
90
|
+
for (const r of windowItems(rows, focus, maxItems, iw))
|
|
91
|
+
lines.push(box(r.text, r.index === focus));
|
|
92
|
+
}
|
|
93
|
+
const notes = (options.notes ?? []).flatMap((n) => wrapWords(n, iw - 3));
|
|
94
|
+
if (notes.length > 0) {
|
|
95
|
+
lines.push(box(''));
|
|
96
|
+
for (const line of notes)
|
|
97
|
+
lines.push(box(' ' + palette.faint(line)));
|
|
98
|
+
}
|
|
99
|
+
return lines;
|
|
100
|
+
}, hintLine(cols - 2, true));
|
|
101
|
+
}
|
|
102
|
+
/** What a key does to a text that is being typed: `undefined` when the key is not an edit. */
|
|
103
|
+
export function editText(value, key) {
|
|
104
|
+
if (key === '\x7f' || key === '\b')
|
|
105
|
+
return Array.from(value).slice(0, -1).join('');
|
|
106
|
+
if (key === WORD_BACKSPACE)
|
|
107
|
+
return value.replace(/\s*\S*$/, '');
|
|
108
|
+
if (key === '\x15')
|
|
109
|
+
return ''; // Ctrl+U
|
|
110
|
+
if (/^[^\x00-\x1f\x7f\x1b]+$/.test(key))
|
|
111
|
+
return value + key; // printable text, also a pasted chunk
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
function openIO(io) {
|
|
115
|
+
const { input } = io;
|
|
116
|
+
const wasRaw = !!input.isRaw;
|
|
117
|
+
input.setRawMode?.(true);
|
|
118
|
+
input.resume();
|
|
119
|
+
return () => { if (!wasRaw)
|
|
120
|
+
input.setRawMode?.(false); };
|
|
121
|
+
}
|
|
122
|
+
/** The list narrowed by typing. Resolves with the chosen value, or undefined when cancelled. */
|
|
123
|
+
export function runFinder(options, io = { input: process.stdin, output: process.stdout }) {
|
|
124
|
+
return new Promise((resolve) => {
|
|
125
|
+
const { input, output } = io;
|
|
126
|
+
const block = new LiveBlock(output);
|
|
127
|
+
let query = '';
|
|
128
|
+
let shown = filterItems(options.items, query);
|
|
129
|
+
const at = shown.findIndex((i) => options.current !== undefined && i.value === options.current);
|
|
130
|
+
let focus = at >= 0 ? at : firstMatch(shown);
|
|
131
|
+
let done = false;
|
|
132
|
+
const rows = () => output.rows || 40;
|
|
133
|
+
const draw = () => block.draw(renderFinder(options, query, shown, focus, output.columns || 80, rows()));
|
|
134
|
+
const restore = openIO(io);
|
|
135
|
+
const finish = (value) => {
|
|
136
|
+
done = true;
|
|
137
|
+
input.off('data', onData);
|
|
138
|
+
block.erase();
|
|
139
|
+
restore();
|
|
140
|
+
resolve(value);
|
|
141
|
+
};
|
|
142
|
+
const move = (delta) => {
|
|
143
|
+
const next = Math.max(0, Math.min(shown.length - 1, focus + delta));
|
|
144
|
+
if (next !== focus) {
|
|
145
|
+
focus = next;
|
|
146
|
+
draw();
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
const handleKey = (raw) => {
|
|
150
|
+
const key = normalizeKey(raw);
|
|
151
|
+
if (key === '\x1b[A' || key === '\x1bOA')
|
|
152
|
+
return move(-1);
|
|
153
|
+
if (key === '\x1b[B' || key === '\x1bOB')
|
|
154
|
+
return move(1);
|
|
155
|
+
if (key === '\x1b[5~')
|
|
156
|
+
return move(-8);
|
|
157
|
+
if (key === '\x1b[6~')
|
|
158
|
+
return move(8);
|
|
159
|
+
if (key === '\r' || key === '\n')
|
|
160
|
+
return shown.length > 0 ? finish(shown[focus].value) : undefined;
|
|
161
|
+
if (key === '\x1b' || key === '\x03')
|
|
162
|
+
return finish(undefined);
|
|
163
|
+
const edited = editText(query, key);
|
|
164
|
+
if (edited !== undefined && edited !== query) {
|
|
165
|
+
query = edited;
|
|
166
|
+
shown = filterItems(options.items, query);
|
|
167
|
+
focus = firstMatch(shown);
|
|
168
|
+
draw();
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
const onData = (chunk) => {
|
|
172
|
+
for (const key of splitKeys(chunk.toString())) {
|
|
173
|
+
if (done)
|
|
174
|
+
return;
|
|
175
|
+
handleKey(key);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
input.on('data', onData);
|
|
179
|
+
draw();
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
export function renderTextInput(options, value, cols) {
|
|
183
|
+
return frame(options.title, options.subtitle, cols, (iw, box) => {
|
|
184
|
+
const lines = [];
|
|
185
|
+
for (const line of wrapWords(options.label, iw - 2))
|
|
186
|
+
lines.push(box(' ' + palette.bold(palette.text(line))));
|
|
187
|
+
const shownValue = options.secret ? '•'.repeat(Array.from(value).length) : value;
|
|
188
|
+
const room = Math.max(4, iw - 6);
|
|
189
|
+
const text = value
|
|
190
|
+
? palette.text(tailWidth(shownValue, room)) + palette.accent('█')
|
|
191
|
+
: palette.accent('█') + palette.faint(truncateWidth(options.placeholder ?? '', room));
|
|
192
|
+
lines.push(box(''));
|
|
193
|
+
lines.push(box(' ' + palette.accent('❯') + ' ' + text, true));
|
|
194
|
+
const notes = (options.notes ?? []).flatMap((n) => wrapWords(n, iw - 3));
|
|
195
|
+
if (notes.length > 0) {
|
|
196
|
+
lines.push(box(''));
|
|
197
|
+
for (const line of notes)
|
|
198
|
+
lines.push(box(' ' + palette.faint(line)));
|
|
199
|
+
}
|
|
200
|
+
return lines;
|
|
201
|
+
}, hintLine(cols - 2, false));
|
|
202
|
+
}
|
|
203
|
+
/** A one-line input. Resolves with the text (never empty), or undefined when cancelled. */
|
|
204
|
+
export function runTextInput(options, io = { input: process.stdin, output: process.stdout }) {
|
|
205
|
+
return new Promise((resolve) => {
|
|
206
|
+
const { input, output } = io;
|
|
207
|
+
const block = new LiveBlock(output);
|
|
208
|
+
let value = options.initial ?? '';
|
|
209
|
+
let done = false;
|
|
210
|
+
const draw = () => block.draw(renderTextInput(options, value, output.columns || 80));
|
|
211
|
+
const restore = openIO(io);
|
|
212
|
+
const finish = (result) => {
|
|
213
|
+
done = true;
|
|
214
|
+
input.off('data', onData);
|
|
215
|
+
block.erase();
|
|
216
|
+
restore();
|
|
217
|
+
resolve(result);
|
|
218
|
+
};
|
|
219
|
+
const handleKey = (raw) => {
|
|
220
|
+
const key = normalizeKey(raw);
|
|
221
|
+
if (key === '\r' || key === '\n')
|
|
222
|
+
return value.trim() ? finish(value.trim()) : undefined;
|
|
223
|
+
if (key === '\x1b' || key === '\x03')
|
|
224
|
+
return finish(undefined);
|
|
225
|
+
const edited = editText(value, key);
|
|
226
|
+
if (edited !== undefined && edited !== value) {
|
|
227
|
+
value = edited;
|
|
228
|
+
draw();
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
const onData = (chunk) => {
|
|
232
|
+
for (const key of splitKeys(chunk.toString())) {
|
|
233
|
+
if (done)
|
|
234
|
+
return;
|
|
235
|
+
handleKey(key);
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
input.on('data', onData);
|
|
239
|
+
draw();
|
|
240
|
+
});
|
|
241
|
+
}
|
package/dist/cli/headless.js
CHANGED
|
@@ -4,6 +4,7 @@ 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';
|
|
@@ -24,21 +25,27 @@ export async function runHeadlessMode(userPrompt, config, options = {}) {
|
|
|
24
25
|
let isFatalError = false;
|
|
25
26
|
let errorMessage = '';
|
|
26
27
|
const abortController = new AbortController();
|
|
27
|
-
|
|
28
|
+
const onAbort = () => {
|
|
29
|
+
if (abortController.signal.aborted)
|
|
30
|
+
return;
|
|
28
31
|
abortController.abort();
|
|
29
32
|
if (!isJson) {
|
|
30
33
|
console.error(pc.yellow('\n[devx headless] Aborted by user (SIGINT)'));
|
|
31
34
|
}
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
};
|
|
36
|
+
process.on('SIGINT', onAbort);
|
|
37
|
+
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
38
|
+
if (options.signal?.aborted)
|
|
39
|
+
onAbort();
|
|
34
40
|
try {
|
|
35
|
-
await MCPManager.getInstance().init();
|
|
41
|
+
await MCPManager.getInstance().init(undefined, config.projectMcpTrusted === true);
|
|
42
|
+
await refreshFormats(config).catch(() => null);
|
|
36
43
|
const provider = createProvider(config);
|
|
37
44
|
const tools = getTools(planMode);
|
|
38
45
|
const guard = new CLIConsoleGuard(isYolo);
|
|
39
46
|
const agent = new Agent(config, provider, tools, history, guard);
|
|
40
47
|
if (!isQuiet && !isJson) {
|
|
41
|
-
console.log(pc.bold(pc.cyan(
|
|
48
|
+
console.log(pc.bold(pc.cyan(`devx v${DEVX_VERSION} (headless) | ${planMode ? 'PLAN' : 'AGENT'} | ${config.model}`)));
|
|
42
49
|
console.log(pc.dim(`Task: ${userPrompt}\n`));
|
|
43
50
|
}
|
|
44
51
|
for await (const event of agent.run(abortController.signal)) {
|
|
@@ -70,10 +77,13 @@ export async function runHeadlessMode(userPrompt, config, options = {}) {
|
|
|
70
77
|
}
|
|
71
78
|
}
|
|
72
79
|
}
|
|
80
|
+
const cancelled = abortController.signal.aborted;
|
|
81
|
+
if (cancelled)
|
|
82
|
+
errorMessage = 'Operation cancelled by user';
|
|
73
83
|
if (isJson) {
|
|
74
84
|
const summary = UsageTracker.getInstance().getSummary();
|
|
75
85
|
const output = {
|
|
76
|
-
success: !isFatalError,
|
|
86
|
+
success: !isFatalError && !cancelled,
|
|
77
87
|
model: config.model,
|
|
78
88
|
prompt: userPrompt,
|
|
79
89
|
result: accumulatedText,
|
|
@@ -85,7 +95,7 @@ export async function runHeadlessMode(userPrompt, config, options = {}) {
|
|
|
85
95
|
cost: summary.totalCost,
|
|
86
96
|
totalBytes: summary.totalBytes
|
|
87
97
|
},
|
|
88
|
-
error: isFatalError ? errorMessage : undefined
|
|
98
|
+
error: isFatalError || cancelled ? errorMessage : undefined
|
|
89
99
|
};
|
|
90
100
|
console.log(JSON.stringify(output, null, 2));
|
|
91
101
|
}
|
|
@@ -94,22 +104,24 @@ export async function runHeadlessMode(userPrompt, config, options = {}) {
|
|
|
94
104
|
process.stdout.write('\n');
|
|
95
105
|
}
|
|
96
106
|
if (isFatalError) {
|
|
97
|
-
console.error(pc.red(`\
|
|
107
|
+
console.error(pc.red(`\nError: ${errorMessage}`));
|
|
98
108
|
return 1;
|
|
99
109
|
}
|
|
100
110
|
}
|
|
101
|
-
return isFatalError ? 1 : 0;
|
|
111
|
+
return cancelled ? 130 : isFatalError ? 1 : 0;
|
|
102
112
|
}
|
|
103
113
|
catch (err) {
|
|
104
114
|
if (isJson) {
|
|
105
115
|
console.log(JSON.stringify({ success: false, error: err.message }, null, 2));
|
|
106
116
|
}
|
|
107
117
|
else {
|
|
108
|
-
console.error(pc.red(`\
|
|
118
|
+
console.error(pc.red(`\nFatal Headless Execution Error: ${err.message}`));
|
|
109
119
|
}
|
|
110
|
-
return 1;
|
|
120
|
+
return abortController.signal.aborted ? 130 : 1;
|
|
111
121
|
}
|
|
112
122
|
finally {
|
|
123
|
+
process.removeListener('SIGINT', onAbort);
|
|
124
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
113
125
|
globalSnapshotManager.finishTurn();
|
|
114
126
|
MCPManager.getInstance().stopAll();
|
|
115
127
|
}
|