@taj-special/dravix-code 1.1.28 → 1.2.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/dist/cli/commands.js +34 -8
- package/dist/cli/index.js +24 -84
- package/dist/cli/repl.js +211 -61
- package/dist/services/ai.js +89 -11
- package/dist/services/auth.js +2 -2
- package/dist/services/context.js +107 -8
- package/dist/services/executor.js +244 -8
- package/dist/services/undo.js +181 -0
- package/dist/utils/display.js +58 -47
- package/package.json +1 -1
package/dist/cli/repl.js
CHANGED
|
@@ -289,7 +289,7 @@ function highlightCode(line, lang) {
|
|
|
289
289
|
function applyInline(text) {
|
|
290
290
|
return text
|
|
291
291
|
.replace(/\*\*(.+?)\*\*|__(.+?)__/g, (_, a, b) => chalk.bold(a ?? b))
|
|
292
|
-
.replace(/`([^`\n]+)`/g, (_, c) => chalk.hex('#fbbf24')(c))
|
|
292
|
+
.replace(/`([^`\n]+)`/g, (_, c) => '`' + chalk.hex('#fbbf24')(c) + '`')
|
|
293
293
|
.replace(/(?<!\*)\*([^*\n]+)\*(?!\*)/g, (_, t) => chalk.italic(t));
|
|
294
294
|
}
|
|
295
295
|
class MarkdownRenderer {
|
|
@@ -409,30 +409,40 @@ class MarkdownRenderer {
|
|
|
409
409
|
this.tableRows = [];
|
|
410
410
|
if (rows.length === 0)
|
|
411
411
|
return '';
|
|
412
|
-
|
|
412
|
+
// Filter out separator rows (--- | --- | ---)
|
|
413
|
+
const dataRows = rows.filter(r => !r.every(c => /^:?-+:?$/.test(c.trim())));
|
|
414
|
+
if (dataRows.length === 0)
|
|
415
|
+
return '';
|
|
416
|
+
const cols = Math.max(...dataRows.map(r => r.length));
|
|
413
417
|
const termW = process.stdout.columns ?? 80;
|
|
414
418
|
// max usable width per column (distribute evenly, min 6)
|
|
415
419
|
const maxColW = Math.max(6, Math.floor((termW - 4 - (cols + 1)) / cols));
|
|
416
420
|
const widths = Array(cols).fill(0);
|
|
417
|
-
for (const row of
|
|
421
|
+
for (const row of dataRows) {
|
|
418
422
|
for (let i = 0; i < cols; i++) {
|
|
419
|
-
|
|
423
|
+
// Strip markdown formatting for width calculation
|
|
424
|
+
const rawCell = (row[i] ?? '').replace(/\*\*/g, '').replace(/`/g, '').trim();
|
|
425
|
+
const cellLen = Math.min(rawCell.length, maxColW);
|
|
420
426
|
widths[i] = Math.max(widths[i], cellLen + 2);
|
|
421
427
|
}
|
|
422
428
|
}
|
|
423
429
|
const bar = (l, m, r) => colors.dim(' ' + l + widths.map(w => '─'.repeat(w)).join(m) + r);
|
|
424
430
|
let out = '\n' + bar('┌', '┬', '┐') + '\n';
|
|
425
|
-
for (let ri = 0; ri <
|
|
426
|
-
const row =
|
|
431
|
+
for (let ri = 0; ri < dataRows.length; ri++) {
|
|
432
|
+
const row = dataRows[ri];
|
|
427
433
|
const cells = widths.map((w, i) => {
|
|
428
|
-
const raw = row[i] ?? '';
|
|
429
|
-
|
|
434
|
+
const raw = (row[i] ?? '').trim();
|
|
435
|
+
// Strip markdown for display but keep formatting
|
|
436
|
+
const plain = raw.replace(/\*\*/g, '').replace(/`/g, '');
|
|
437
|
+
const cell = plain.length > w - 2 ? plain.slice(0, w - 3) + '…' : plain;
|
|
430
438
|
const pad = w - cell.length - 1;
|
|
439
|
+
// Apply formatting to the cell content
|
|
440
|
+
const formatted = applyInline(raw);
|
|
431
441
|
const text = ri === 0 ? chalk.bold.white(cell) : colors.ai(cell);
|
|
432
442
|
return ' ' + text + ' '.repeat(Math.max(pad, 0));
|
|
433
443
|
});
|
|
434
444
|
out += colors.dim(' │') + cells.join(colors.dim('│')) + colors.dim('│') + '\n';
|
|
435
|
-
if (ri === 0 &&
|
|
445
|
+
if (ri === 0 && dataRows.length > 1)
|
|
436
446
|
out += bar('├', '┼', '┤') + '\n';
|
|
437
447
|
}
|
|
438
448
|
out += bar('└', '┴', '┘') + '\n';
|
|
@@ -462,14 +472,14 @@ class MarkdownRenderer {
|
|
|
462
472
|
const capped = Math.min(inner, maxInner);
|
|
463
473
|
const dashAfterLang = Math.max(0, capped - lang.length - 2);
|
|
464
474
|
const top = lang
|
|
465
|
-
? colors.dim(' ╭─ ') +
|
|
475
|
+
? colors.dim(' ╭─ ') + colors.primary(lang) + colors.dim(' ' + '─'.repeat(dashAfterLang))
|
|
466
476
|
: colors.dim(' ╭' + '─'.repeat(capped + 2));
|
|
467
477
|
const bot = colors.dim(' ╰' + '─'.repeat(capped + 2));
|
|
468
478
|
const canHighlight = lang && !['text', 'txt', 'plain', 'output', 'log'].includes(lang.toLowerCase());
|
|
469
479
|
let out = '\n' + top + '\n';
|
|
470
480
|
for (const l of lines) {
|
|
471
481
|
const display = l.length > capped ? l.slice(0, capped - 1) + '…' : l;
|
|
472
|
-
const colorized = canHighlight ? highlightCode(display, lang) :
|
|
482
|
+
const colorized = canHighlight ? highlightCode(display, lang) : colors.text(display);
|
|
473
483
|
out += colors.dim(' │ ') + colorized + '\n';
|
|
474
484
|
}
|
|
475
485
|
out += bot + '\n';
|
|
@@ -734,7 +744,6 @@ async function askPermission(label, key, alwaysAllowed, noAlways, diffShown, pre
|
|
|
734
744
|
drawn = true;
|
|
735
745
|
let lines;
|
|
736
746
|
if (diffShown) {
|
|
737
|
-
// Diff already shown above — just show options, no redundant box
|
|
738
747
|
lines = [
|
|
739
748
|
'',
|
|
740
749
|
...optLabels.map((lbl, i) => i === sel
|
|
@@ -846,25 +855,22 @@ async function readLine(prompt, cwd) {
|
|
|
846
855
|
let suffix = '';
|
|
847
856
|
let ctrlCCount = 0;
|
|
848
857
|
let ctrlCTimer = null;
|
|
849
|
-
// @ file-completion state
|
|
850
858
|
let atMode = false;
|
|
851
859
|
let atQuery = '';
|
|
852
860
|
let atSel = 0;
|
|
853
|
-
let atBoxLines = 0;
|
|
861
|
+
let atBoxLines = 0;
|
|
854
862
|
let allFiles = [];
|
|
855
|
-
// / slash-command picker state
|
|
856
863
|
let slashMode = false;
|
|
857
864
|
let slashQuery = '';
|
|
858
865
|
let slashSel = 0;
|
|
859
866
|
let slashBoxLines = 0;
|
|
860
867
|
let slashDrawn = false;
|
|
861
|
-
// Tab path-autocomplete state
|
|
862
868
|
let tabMode = false;
|
|
863
869
|
let tabQuery = '';
|
|
864
870
|
let tabSel = 0;
|
|
865
871
|
let tabBoxLines = 0;
|
|
866
872
|
let tabDrawn = false;
|
|
867
|
-
let tabPreWord = '';
|
|
873
|
+
let tabPreWord = '';
|
|
868
874
|
function onResize() {
|
|
869
875
|
const newCols = process.stdout.columns ?? 80;
|
|
870
876
|
const oldSepRows = Math.ceil(sepVisualLen / newCols);
|
|
@@ -979,9 +985,9 @@ async function readLine(prompt, cwd) {
|
|
|
979
985
|
atDrawn = true;
|
|
980
986
|
}
|
|
981
987
|
atBoxLines = 0;
|
|
982
|
-
const inputLine = colors.primary(' › ') +
|
|
983
|
-
(pasteBlock !== null ? colors.muted(`[paste: ${pasteCount} lines]`) +
|
|
984
|
-
|
|
988
|
+
const inputLine = colors.primary(' › ') + colors.text(prefix) +
|
|
989
|
+
(pasteBlock !== null ? colors.muted(`[paste: ${pasteCount} lines]`) + colors.text(suffix) : '') +
|
|
990
|
+
colors.primary('@') + colors.text(atQuery);
|
|
985
991
|
process.stdout.write(inputLine + '\r\n' + SEP + '\r\n');
|
|
986
992
|
const DW = Math.min(Math.max((process.stdout.columns ?? 80) - 6, 44), 72);
|
|
987
993
|
const IW = DW - 6;
|
|
@@ -1009,8 +1015,8 @@ async function readLine(prompt, cwd) {
|
|
|
1009
1015
|
const pad = ' '.repeat(Math.max(IW - clip.length, 0));
|
|
1010
1016
|
const name = sel && isDir ? colors.primary.bold(clip) + pad
|
|
1011
1017
|
: sel ? colors.primary.bold(clip) + pad
|
|
1012
|
-
: isDir ? chalk.hex('#
|
|
1013
|
-
:
|
|
1018
|
+
: isDir ? chalk.hex('#74b9ff')(clip) + pad
|
|
1019
|
+
: colors.subtext(clip + pad);
|
|
1014
1020
|
process.stdout.write(colors.dim(' │') + ` ${mark} ` + name + colors.dim('│') + '\n');
|
|
1015
1021
|
atBoxLines++;
|
|
1016
1022
|
}
|
|
@@ -1048,7 +1054,7 @@ async function readLine(prompt, cwd) {
|
|
|
1048
1054
|
slashDrawn = true;
|
|
1049
1055
|
}
|
|
1050
1056
|
slashBoxLines = 0;
|
|
1051
|
-
const inputLine = colors.primary(' › ') + colors.primary('/') +
|
|
1057
|
+
const inputLine = colors.primary(' › ') + colors.primary('/') + colors.text(slashQuery);
|
|
1052
1058
|
process.stdout.write(inputLine + '\r\n' + SEP + '\r\n');
|
|
1053
1059
|
const DW = Math.min(Math.max((process.stdout.columns ?? 80) - 6, 44), 72);
|
|
1054
1060
|
const nameW = 10;
|
|
@@ -1074,8 +1080,8 @@ async function readLine(prompt, cwd) {
|
|
|
1074
1080
|
const nameStr = cmd.name.padEnd(nameW);
|
|
1075
1081
|
const descStr = cmd.desc.length > descW ? cmd.desc.slice(0, descW - 1) + '…' : cmd.desc;
|
|
1076
1082
|
const descPad = ' '.repeat(Math.max(descW - descStr.length, 0));
|
|
1077
|
-
const namePaint = sel ? colors.primary.bold(nameStr) :
|
|
1078
|
-
const descPaint = sel ? colors.muted(descStr) :
|
|
1083
|
+
const namePaint = sel ? colors.primary.bold(nameStr) : colors.primary(nameStr);
|
|
1084
|
+
const descPaint = sel ? colors.muted(descStr) : colors.subtext(descStr);
|
|
1079
1085
|
process.stdout.write(colors.dim(' │') + ` ${mark} ` + namePaint + ' ' + descPaint + descPad + colors.dim('│') + '\n');
|
|
1080
1086
|
slashBoxLines++;
|
|
1081
1087
|
}
|
|
@@ -1113,8 +1119,8 @@ async function readLine(prompt, cwd) {
|
|
|
1113
1119
|
tabDrawn = true;
|
|
1114
1120
|
}
|
|
1115
1121
|
tabBoxLines = 0;
|
|
1116
|
-
const TC =
|
|
1117
|
-
const inputLine = colors.primary(' › ') +
|
|
1122
|
+
const TC = colors.success;
|
|
1123
|
+
const inputLine = colors.primary(' › ') + colors.text(tabPreWord) + TC(tabQuery);
|
|
1118
1124
|
process.stdout.write(inputLine + '\r\n' + SEP + '\r\n');
|
|
1119
1125
|
const DW = Math.min(Math.max((process.stdout.columns ?? 80) - 6, 44), 72);
|
|
1120
1126
|
const IW = DW - 6;
|
|
@@ -1141,8 +1147,8 @@ async function readLine(prompt, cwd) {
|
|
|
1141
1147
|
const clip = f.length > IW ? f.slice(0, IW - 1) + '…' : f;
|
|
1142
1148
|
const pad = ' '.repeat(Math.max(IW - clip.length, 0));
|
|
1143
1149
|
const name = sel ? TC.bold(clip) + pad
|
|
1144
|
-
: isDir ? chalk.hex('#
|
|
1145
|
-
:
|
|
1150
|
+
: isDir ? chalk.hex('#74b9ff')(clip) + pad
|
|
1151
|
+
: colors.subtext(clip + pad);
|
|
1146
1152
|
process.stdout.write(colors.dim(' │') + ` ${mark} ` + name + colors.dim('│') + '\n');
|
|
1147
1153
|
tabBoxLines++;
|
|
1148
1154
|
}
|
|
@@ -1178,11 +1184,11 @@ async function readLine(prompt, cwd) {
|
|
|
1178
1184
|
displayPre = '…' + displayPre.slice(-(maxLen - 1));
|
|
1179
1185
|
let inp;
|
|
1180
1186
|
if (pasteBlock !== null) {
|
|
1181
|
-
inp = colors.primary(' › ') +
|
|
1182
|
-
colors.muted(`[paste: ${pasteCount} lines]`) +
|
|
1187
|
+
inp = colors.primary(' › ') + colors.text(displayPre) +
|
|
1188
|
+
colors.muted(`[paste: ${pasteCount} lines]`) + colors.text(suffix);
|
|
1183
1189
|
}
|
|
1184
1190
|
else {
|
|
1185
|
-
inp = colors.primary(' › ') +
|
|
1191
|
+
inp = colors.primary(' › ') + colors.text(displayPre);
|
|
1186
1192
|
}
|
|
1187
1193
|
process.stdout.write('\r\x1b[K' + inp);
|
|
1188
1194
|
process.stdout.write('\r\n' + SEP + '\x1b[1A\r' + inp);
|
|
@@ -2120,12 +2126,63 @@ export async function startRepl(cwd) {
|
|
|
2120
2126
|
// Rules appended to every system prompt to enforce safe file-operation behavior
|
|
2121
2127
|
const SAFE_FILE_RULES = `
|
|
2122
2128
|
|
|
2123
|
-
##
|
|
2129
|
+
## CORE RULES
|
|
2130
|
+
|
|
2131
|
+
### READ FIRST, THEN ANSWER
|
|
2132
|
+
When the user asks about their code:
|
|
2133
|
+
1. Find the relevant file(s)
|
|
2134
|
+
2. Read them silently
|
|
2135
|
+
3. Give a direct, accurate answer based on what you found
|
|
2136
|
+
Never guess or fabricate information.
|
|
2137
|
+
|
|
2138
|
+
### BE ACCURATE
|
|
2139
|
+
- Only report what you actually see in the code
|
|
2140
|
+
- If you are unsure, say so — do not fabricate answers
|
|
2141
|
+
- Never add features or items that do not exist in the code
|
|
2142
|
+
|
|
2143
|
+
### RESPOND IN THE SAME LANGUAGE
|
|
2144
|
+
- Respond in the same language the user writes in
|
|
2145
|
+
- If user writes in English → respond in English
|
|
2146
|
+
- If user writes in Russian → respond in Russian
|
|
2147
|
+
- And so on for any language
|
|
2148
|
+
|
|
2149
|
+
### FORMAT ANSWERS CLEARLY
|
|
2150
|
+
- Use bullet points or numbered lists for multiple items
|
|
2151
|
+
- Put each item on its own line
|
|
2152
|
+
- Keep answers concise — 3-10 lines maximum
|
|
2153
|
+
- Do not dump entire file contents unless asked
|
|
2154
|
+
- Do not add unnecessary commentary
|
|
2155
|
+
|
|
2156
|
+
### WORK SILENTLY
|
|
2157
|
+
- Do not say "I will read the file" — just read it and answer
|
|
2158
|
+
- Do not narrate your thought process
|
|
2159
|
+
- Do not say "I already answered your question"
|
|
2160
|
+
- Give your answer ONCE and STOP
|
|
2161
|
+
|
|
2162
|
+
### WHEN CREATING/EDITING FILES
|
|
2163
|
+
- write_file = CREATE new files only
|
|
2164
|
+
- edit_file = MODIFY existing (read first, then find/replace)
|
|
2165
|
+
- Before editing: ALWAYS read_file first
|
|
2166
|
+
- Complete the ENTIRE task in ONE response
|
|
2167
|
+
- Use tags, NOT code blocks
|
|
2168
|
+
|
|
2169
|
+
### WHEN CREATING/EDITING FILES
|
|
2170
|
+
- write_file = CREATE new files only
|
|
2171
|
+
- edit_file = MODIFY existing (read first, then find/replace)
|
|
2172
|
+
- Before editing: ALWAYS read_file first
|
|
2173
|
+
- Multiple edits: order from BOTTOM to TOP
|
|
2174
|
+
- Complete the ENTIRE task in ONE response
|
|
2175
|
+
- Use tags, NOT code blocks
|
|
2176
|
+
|
|
2177
|
+
### When the user asks you to do something:
|
|
2178
|
+
1. Read the relevant files first
|
|
2179
|
+
2. Do the work using <write_file> / <edit_file> tags
|
|
2180
|
+
3. Summarize what you did in 1-2 sentences
|
|
2124
2181
|
|
|
2125
2182
|
### Act decisively
|
|
2126
2183
|
- Use judgment and execute immediately when intent is clear.
|
|
2127
2184
|
- Do NOT ask unnecessary clarifying questions — just do the task.
|
|
2128
|
-
- Make reasonable assumptions and proceed.
|
|
2185
|
+
- Make reasonable assumptions and proceed.
|
|
2129
2186
|
|
|
2130
2187
|
### Creating files
|
|
2131
2188
|
- NEVER show code in code blocks when the user asks to create files or a project.
|
|
@@ -2139,9 +2196,39 @@ export async function startRepl(cwd) {
|
|
|
2139
2196
|
- Before editing any file: use <read_file> to see its current content first.
|
|
2140
2197
|
- NEVER overwrite an existing file with write_file — it destroys user code.
|
|
2141
2198
|
|
|
2199
|
+
### How to use edit_file correctly
|
|
2200
|
+
1. ALWAYS read_file first to see the current content
|
|
2201
|
+
2. Copy the EXACT text from the file into <find> — character by character
|
|
2202
|
+
3. The <find> text must match EXACTLY (including whitespace, indentation)
|
|
2203
|
+
4. If edit fails, the system will show you the actual content — use it to retry
|
|
2204
|
+
5. Do NOT put line numbers in <find> (e.g., "42 │ code" is WRONG)
|
|
2205
|
+
6. Do NOT modify the text in <find> — copy it EXACTLY from the file
|
|
2206
|
+
|
|
2207
|
+
### Searching and navigating
|
|
2208
|
+
- Use <glob_files pattern="**/*.ts" /> to find files by extension
|
|
2209
|
+
- Use <search_code pattern="functionName" /> to find code
|
|
2210
|
+
- Use <search_code pattern="regex.*pattern" regex="true" /> for regex search
|
|
2211
|
+
- Use <search_code pattern="error" include="*.ts" /> to search specific file types
|
|
2212
|
+
- Use <search_code pattern="TODO" context="2" /> to show context around matches
|
|
2213
|
+
- Read files before editing them — always!
|
|
2214
|
+
|
|
2215
|
+
### Multi-file tasks
|
|
2216
|
+
- Plan all files FIRST (list them in your response)
|
|
2217
|
+
- Create/edit files in LOGICAL ORDER (dependencies first)
|
|
2218
|
+
- Use write_file for ALL new files
|
|
2219
|
+
- Use edit_file for ALL modifications
|
|
2220
|
+
- Run commands LAST (after all files are created)
|
|
2221
|
+
|
|
2222
|
+
### Error recovery
|
|
2223
|
+
- If an edit_file fails, read the file again and retry with the actual content
|
|
2224
|
+
- If a run_command fails, read the error message and fix the issue
|
|
2225
|
+
- Never give up — keep trying until the task is complete
|
|
2226
|
+
|
|
2142
2227
|
### Scope
|
|
2143
2228
|
- Only touch files the user explicitly mentioned or that are clearly required by the task.
|
|
2144
|
-
- Do NOT restructure, rename, or "improve" anything that wasn't asked
|
|
2229
|
+
- Do NOT restructure, rename, or "improve" anything that wasn't asked.
|
|
2230
|
+
- When creating a new project, create ALL necessary files (not just some).
|
|
2231
|
+
- When fixing a bug, fix the ROOT CAUSE, not just the symptom.`;
|
|
2145
2232
|
const buildSystemMsg = (dir) => ({
|
|
2146
2233
|
role: 'system',
|
|
2147
2234
|
content: SYSTEM_PROMPT + SAFE_FILE_RULES + '\n\nProject context:\n' + buildContext(dir),
|
|
@@ -2160,6 +2247,10 @@ export async function startRepl(cwd) {
|
|
|
2160
2247
|
let pendingOutputTokens = 0;
|
|
2161
2248
|
let pendingUserTokens = 0;
|
|
2162
2249
|
let hasPendingReport = false;
|
|
2250
|
+
// Auto-retry state
|
|
2251
|
+
const MAX_AUTO_RETRIES = 3;
|
|
2252
|
+
let autoRetryCount = 0;
|
|
2253
|
+
let lastFailedOp = null;
|
|
2163
2254
|
process.stdin.once('end', () => {
|
|
2164
2255
|
console.log(colors.muted('\n Goodbye!\n'));
|
|
2165
2256
|
process.exit(0);
|
|
@@ -2312,6 +2403,8 @@ export async function startRepl(cwd) {
|
|
|
2312
2403
|
if (!skipInput) {
|
|
2313
2404
|
readFileTurnCount = 0; // reset loop counter for each new user message
|
|
2314
2405
|
forcedEditMode = false; // reset forced edit mode for each new user message
|
|
2406
|
+
autoRetryCount = 0; // reset auto-retry counter for each new user message
|
|
2407
|
+
lastFailedOp = null;
|
|
2315
2408
|
// ── Refresh project context (files may have changed since last turn) ──
|
|
2316
2409
|
history[0] = buildSystemMsg(activeCwd);
|
|
2317
2410
|
// ── Clear stale operational messages left from previous auto-continue turns ──
|
|
@@ -2470,7 +2563,7 @@ export async function startRepl(cwd) {
|
|
|
2470
2563
|
}
|
|
2471
2564
|
catch { /* ignore */ }
|
|
2472
2565
|
}
|
|
2473
|
-
const previewLines = (op.type === 'write' || op.type === 'edit') ? printDiffPreview(op, activeCwd) : 0;
|
|
2566
|
+
const previewLines = (!alwaysAllowed.has(key) && (op.type === 'write' || op.type === 'edit')) ? printDiffPreview(op, activeCwd) : 0;
|
|
2474
2567
|
const allowed = await askPermission(label, key, alwaysAllowed, op.type === 'run', previewLines > 0, previewLines);
|
|
2475
2568
|
executedInlineOps.add(inlineOpFingerprint(op));
|
|
2476
2569
|
if (!allowed) {
|
|
@@ -2506,8 +2599,7 @@ export async function startRepl(cwd) {
|
|
|
2506
2599
|
const cur = fs.readFileSync(fp, 'utf-8');
|
|
2507
2600
|
const curLines = cur.split('\n');
|
|
2508
2601
|
const nLines = curLines.length;
|
|
2509
|
-
|
|
2510
|
-
// Auto-search for what AI was trying to find — inject exact block
|
|
2602
|
+
// Smart error recovery: find approximate location and provide fix instructions
|
|
2511
2603
|
const findLines = (op.find ?? '').split('\n').map(l => l.replace(/^\s*\d+\s*[│|:]\s?/, '').trim()).filter(Boolean);
|
|
2512
2604
|
const searchTerm = findLines[0]?.slice(0, 60) ?? '';
|
|
2513
2605
|
let foundAt = -1;
|
|
@@ -2520,27 +2612,51 @@ export async function startRepl(cwd) {
|
|
|
2520
2612
|
}
|
|
2521
2613
|
}
|
|
2522
2614
|
if (foundAt >= 0) {
|
|
2523
|
-
// Found
|
|
2524
|
-
const start = Math.max(0, foundAt -
|
|
2525
|
-
const end = Math.min(curLines.length - 1, foundAt +
|
|
2615
|
+
// Found approximate location — show surrounding context
|
|
2616
|
+
const start = Math.max(0, foundAt - 3);
|
|
2617
|
+
const end = Math.min(curLines.length - 1, foundAt + 50);
|
|
2526
2618
|
const block = curLines.slice(start, end + 1).join('\n');
|
|
2527
|
-
errMsg += `\n\n[
|
|
2528
|
-
errMsg +=
|
|
2529
|
-
? `\nDELETE FIX: Copy the EXACT full container (from its opening <div>/<section> tag through the matching closing tag) from above. Put it in <edit_file path="${op.path}"><find>EXACT_TEXT</find><replace></replace>. Text must match CHARACTER FOR CHARACTER.`
|
|
2530
|
-
: `\nEDIT FIX: Copy the EXACT text you want to change from above. Use <edit_file path="${op.path}"><find>EXACT_TEXT</find><replace>NEW_TEXT</replace>.`;
|
|
2619
|
+
errMsg += `\n\n[FILE CONTENT — ${op.path} lines ${start + 1}-${end + 1}]:\n${block}`;
|
|
2620
|
+
errMsg += `\n\nFIX: The edit failed. Copy the EXACT text from the file above into <find>. Do NOT include line numbers.`;
|
|
2531
2621
|
}
|
|
2532
2622
|
else if (nLines <= 400) {
|
|
2533
|
-
|
|
2623
|
+
// File is small — show full content
|
|
2624
|
+
errMsg += `\n\n[FILE CONTENT — ${op.path} (${nLines} lines)]:\n${cur}`;
|
|
2625
|
+
errMsg += `\n\nFIX: Copy the EXACT text from the file above into <find>.`;
|
|
2534
2626
|
}
|
|
2535
2627
|
else {
|
|
2536
|
-
|
|
2537
|
-
|
|
2628
|
+
// File is large — show first 100 lines and instruct to search
|
|
2629
|
+
const preview = curLines.slice(0, 100).join('\n');
|
|
2630
|
+
errMsg += `\n\n[FILE CONTENT — ${op.path} (first 100 of ${nLines} lines)]:\n${preview}`;
|
|
2631
|
+
errMsg += `\n\nFIX: Use <read_file path="${op.path}" lines="N-M"/> to read the correct section, then copy EXACT text into <find>.`;
|
|
2538
2632
|
}
|
|
2633
|
+
// Auto-retry: inject file content and force continuation
|
|
2634
|
+
history.push({
|
|
2635
|
+
role: 'system',
|
|
2636
|
+
content: `[AUTO-RETRY] The edit failed. Here is the current file content:\n\n${nLines <= 200 ? cur : curLines.slice(0, 200).join('\n')}\n\nRetry the edit using the EXACT content above. Copy text CHARACTER FOR CHARACTER into <find>.`,
|
|
2637
|
+
});
|
|
2638
|
+
readFileContinue = true;
|
|
2539
2639
|
}
|
|
2540
2640
|
}
|
|
2541
2641
|
catch { /* ignore */ }
|
|
2542
2642
|
}
|
|
2543
2643
|
inlineFileOpErrors.push(errMsg);
|
|
2644
|
+
// Auto-retry logic: if edit_file fails, automatically retry with updated context
|
|
2645
|
+
if (op.type === 'edit' && autoRetryCount < MAX_AUTO_RETRIES) {
|
|
2646
|
+
autoRetryCount++;
|
|
2647
|
+
lastFailedOp = op.path ?? null;
|
|
2648
|
+
}
|
|
2649
|
+
else if (autoRetryCount >= MAX_AUTO_RETRIES) {
|
|
2650
|
+
// Reset retry counter after max retries — prevent infinite loop
|
|
2651
|
+
autoRetryCount = 0;
|
|
2652
|
+
lastFailedOp = null;
|
|
2653
|
+
// Tell AI to stop retrying this specific edit
|
|
2654
|
+
history.push({
|
|
2655
|
+
role: 'system',
|
|
2656
|
+
content: `[STOP] Edit failed ${MAX_AUTO_RETRIES} times. Do NOT retry this specific edit. Move on to other tasks or ask the user for help.`,
|
|
2657
|
+
});
|
|
2658
|
+
readFileContinue = true;
|
|
2659
|
+
}
|
|
2544
2660
|
}
|
|
2545
2661
|
else if ((opResult.type === 'modified' || opResult.type === 'created') && op.path &&
|
|
2546
2662
|
(op.type === 'edit' || op.type === 'write')) {
|
|
@@ -2737,9 +2853,11 @@ export async function startRepl(cwd) {
|
|
|
2737
2853
|
return;
|
|
2738
2854
|
}
|
|
2739
2855
|
const normalized = normalizeResponse(fullResponse);
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2856
|
+
// Clean up the response — remove trailing newlines
|
|
2857
|
+
const stripped = normalized.replace(/\n+$/, '').trim();
|
|
2858
|
+
lastAIResponse = stripped;
|
|
2859
|
+
if (stripped.trim()) {
|
|
2860
|
+
history.push({ role: 'assistant', content: stripped });
|
|
2743
2861
|
}
|
|
2744
2862
|
if (conversationTitle === null) {
|
|
2745
2863
|
const _msgs = history.slice(1).filter(m => m.role === 'user' || m.role === 'assistant');
|
|
@@ -2777,16 +2895,38 @@ export async function startRepl(cwd) {
|
|
|
2777
2895
|
history.push({ role: 'system', content: inlineFileOpErrors.join('\n\n') + '\n\nFix the above errors and retry the failed operations.' });
|
|
2778
2896
|
readFileContinue = true;
|
|
2779
2897
|
}
|
|
2780
|
-
const allOps = parseOps(
|
|
2898
|
+
const allOps = parseOps(fullResponse);
|
|
2781
2899
|
// ── Detect incomplete plan: AI described steps but didn't output tags ──
|
|
2782
2900
|
if (!readFileContinue && !streamCancelled) {
|
|
2783
2901
|
const hasAnyReadOp = allOps.some(op => op.type === 'read_file' || op.type === 'read_folder' || op.type === 'search_code');
|
|
2902
|
+
const hasAnyWriteOp = allOps.some(op => op.type === 'write' || op.type === 'edit' || op.type === 'delete' || op.type === 'mkdir' || op.type === 'run');
|
|
2784
2903
|
const writeOps = allOps.filter(op => op.type !== 'read_file' && op.type !== 'read_folder' && op.type !== 'search_code').length;
|
|
2785
|
-
const plannedSteps = (
|
|
2786
|
-
const hasCodeFence =
|
|
2787
|
-
const hasStepList = (
|
|
2904
|
+
const plannedSteps = (stripped.match(/^\s*[2-9]\.\s+\S/mg) ?? []).length;
|
|
2905
|
+
const hasCodeFence = stripped.includes('```');
|
|
2906
|
+
const hasStepList = (stripped.match(/^\s*\d+[.)]\s+\S/mg) ?? []).length >= 2;
|
|
2907
|
+
// Case 0: AI only output read/search ops with NO substantial text answer — force continuation
|
|
2908
|
+
// BUT: if AI wrote a substantial answer along with read ops, it already answered
|
|
2909
|
+
const textLength = stripped.replace(/<[^>]+>/g, '').trim().length;
|
|
2910
|
+
// Also check if the answer contains useful content (bullet points, lists, specific info)
|
|
2911
|
+
const hasUsefulContent = /[•\-\*]\s|^\d+\.\s|\b\d+\s+(забон|function|file|language|item)/m.test(stripped);
|
|
2912
|
+
// Check for loop: if same answer was given before, STOP
|
|
2913
|
+
const isDuplicate = lastAIResponse && stripped === lastAIResponse;
|
|
2914
|
+
// Check if answer contains specific info (numbers, names, etc.)
|
|
2915
|
+
const hasSpecificInfo = /\d+\s+(забон|function|file|language|item|line|row)|`\w+`/i.test(stripped);
|
|
2916
|
+
// Check if AI already gave a complete answer (more than 3 lines or contains list items)
|
|
2917
|
+
const hasCompleteAnswer = stripped.split('\n').length > 3 || /\n\s*[-•*]\s/.test(stripped) || /\n\s*\d+\.\s/.test(stripped);
|
|
2918
|
+
// Check if this is a repeat of recent answers (compare first 50 chars)
|
|
2919
|
+
const recentAnswerPrefix = lastAIResponse.slice(0, 50);
|
|
2920
|
+
const currentPrefix = stripped.slice(0, 50);
|
|
2921
|
+
const isSimilarToRecent = recentAnswerPrefix.length > 10 && currentPrefix === recentAnswerPrefix;
|
|
2922
|
+
if (hasAnyReadOp && !hasAnyWriteOp && readFileTurnCount < 3 && textLength < 20 && !hasUsefulContent && !isDuplicate && !hasSpecificInfo && !hasCompleteAnswer && !isSimilarToRecent) {
|
|
2923
|
+
// AI read files but didn't produce any answer — force it to answer
|
|
2924
|
+
const readPrompt = `[FILE CONTENTS LOADED] The files have been read and their contents are now in your context above.\n\nThe user's question was: "${lastUserLine.slice(0, 300)}"\n\nANSWER THE QUESTION NOW based on what you read. Do NOT say "let me read" — you already read the files. Give a DIRECT, SPECIFIC answer. Do NOT repeat yourself.`;
|
|
2925
|
+
history.push({ role: 'system', content: readPrompt });
|
|
2926
|
+
readFileContinue = true;
|
|
2927
|
+
}
|
|
2788
2928
|
// Case 1: AI listed numbered steps (2+) but produced zero file ops
|
|
2789
|
-
if (plannedSteps >= 1 && writeOps === 0 && !hasAnyReadOp) {
|
|
2929
|
+
else if (plannedSteps >= 1 && writeOps === 0 && !hasAnyReadOp) {
|
|
2790
2930
|
history.push({ role: 'system', content: 'CRITICAL ERROR: You described steps but produced NO file operation tags. You MUST output <write_file> or <edit_file> tags to create/modify actual files. Do NOT show code in ``` blocks — use <write_file path="filename"> tags instead. Output all the tags NOW. No explanation.' });
|
|
2791
2931
|
readFileContinue = true;
|
|
2792
2932
|
// Case 2: AI showed code blocks + numbered list but no tags — regardless of language
|
|
@@ -2890,7 +3030,6 @@ export async function startRepl(cwd) {
|
|
|
2890
3030
|
const lastLine = (res.output ?? '').split('\n').filter(Boolean).pop() ?? '';
|
|
2891
3031
|
rightCol = chalk.hex('#6b7280')(lastLine.startsWith('Total:') ? lastLine.slice(7).trim() : `${ms}ms`);
|
|
2892
3032
|
displayLabel = `"${op.pattern}"`;
|
|
2893
|
-
// Auto-read section around first match so AI has exact text without another round-trip
|
|
2894
3033
|
const firstMatchLine = (res.output ?? '').match(/^([^\n:]+):(\d+):/m);
|
|
2895
3034
|
if (firstMatchLine) {
|
|
2896
3035
|
const matchFile = firstMatchLine[1].trim();
|
|
@@ -2919,7 +3058,6 @@ export async function startRepl(cwd) {
|
|
|
2919
3058
|
ctxLabel = `[File content — ${res.message ?? op.path}]${lineTag}:\n${res.output ?? ''}`;
|
|
2920
3059
|
rightCol = chalk.hex('#6b7280')(op.lines ? `lines ${op.lines}` : `${ms}ms`);
|
|
2921
3060
|
displayLabel = op.lines ? `${op.path ?? ''} :${op.lines}` : (op.path ?? '');
|
|
2922
|
-
// Auto-continue paginated reads: if file has more pages, queue next page automatically
|
|
2923
3061
|
const pageMatch = (res.output ?? '').match(/\.\.\. \((\d+) more lines — next page: lines (\d+)-(\d+)\)/);
|
|
2924
3062
|
if (pageMatch && op.path) {
|
|
2925
3063
|
const [, , nextStart, nextEnd] = pageMatch;
|
|
@@ -2927,7 +3065,6 @@ export async function startRepl(cwd) {
|
|
|
2927
3065
|
role: 'system',
|
|
2928
3066
|
content: `[Auto-reading next page of ${op.path}...]`,
|
|
2929
3067
|
});
|
|
2930
|
-
// Queue the next page read by injecting it as a system op
|
|
2931
3068
|
validOps.push({ type: 'read_file', path: op.path, lines: `${nextStart}-${nextEnd}` });
|
|
2932
3069
|
}
|
|
2933
3070
|
}
|
|
@@ -3006,7 +3143,7 @@ export async function startRepl(cwd) {
|
|
|
3006
3143
|
continue;
|
|
3007
3144
|
}
|
|
3008
3145
|
}
|
|
3009
|
-
const previewLines2 = (op.type === 'write' || op.type === 'edit') ? printDiffPreview(op, activeCwd) : 0;
|
|
3146
|
+
const previewLines2 = (!alwaysAllowed.has(key) && (op.type === 'write' || op.type === 'edit')) ? printDiffPreview(op, activeCwd) : 0;
|
|
3010
3147
|
const allowed = await askPermission(label, key, alwaysAllowed, op.type === 'run', previewLines2 > 0, previewLines2);
|
|
3011
3148
|
if (!allowed) {
|
|
3012
3149
|
if (op.path)
|
|
@@ -3024,7 +3161,7 @@ export async function startRepl(cwd) {
|
|
|
3024
3161
|
stageSpinFrame = 0;
|
|
3025
3162
|
stageSpinInterval = setInterval(() => {
|
|
3026
3163
|
const f = SPINNER_FRAMES[stageSpinFrame++ % SPINNER_FRAMES.length];
|
|
3027
|
-
process.stdout.write(`\r\x1b[K ${colors.muted(f)} ${
|
|
3164
|
+
process.stdout.write(`\r\x1b[K ${colors.muted(f)} ${colors.subtext(stageSpinMsg)}`);
|
|
3028
3165
|
}, 80);
|
|
3029
3166
|
}
|
|
3030
3167
|
});
|
|
@@ -3093,6 +3230,19 @@ export async function startRepl(cwd) {
|
|
|
3093
3230
|
? `[Command failed — ${r.cmd}]:\n${r.output}`
|
|
3094
3231
|
: `[Command output — ${r.cmd}]:\n${r.output}`).join('\n\n');
|
|
3095
3232
|
history.push({ role: 'system', content });
|
|
3233
|
+
// Auto-retry for failed commands
|
|
3234
|
+
const failedCmds = runOutputs.filter(r => r.isError);
|
|
3235
|
+
if (failedCmds.length > 0 && autoRetryCount < MAX_AUTO_RETRIES) {
|
|
3236
|
+
autoRetryCount++;
|
|
3237
|
+
const retryHint = `[Auto-retry ${autoRetryCount}/${MAX_AUTO_RETRIES}] ` +
|
|
3238
|
+
`Previous command(s) failed. Analyze the error and try again with a fix. ` +
|
|
3239
|
+
`Common fixes: check file paths, install missing dependencies, verify syntax.`;
|
|
3240
|
+
history.push({ role: 'system', content: retryHint });
|
|
3241
|
+
readFileContinue = true;
|
|
3242
|
+
}
|
|
3243
|
+
else if (autoRetryCount >= MAX_AUTO_RETRIES) {
|
|
3244
|
+
autoRetryCount = 0;
|
|
3245
|
+
}
|
|
3096
3246
|
// Only auto-continue if there is real output to react to, or an error to explain
|
|
3097
3247
|
const needsReaction = runOutputs.some(r => r.isError || r.output !== NO_OUTPUT_MARKER);
|
|
3098
3248
|
if (needsReaction)
|
package/dist/services/ai.js
CHANGED
|
@@ -2,6 +2,36 @@ const AI_PROXY = 'https://dravix.app/ai-proxy.php';
|
|
|
2
2
|
const PROMPT_URL = 'https://dravix.app/cli-prompt.php';
|
|
3
3
|
export const FLASH_MODEL = 'deepseek/deepseek-v4-flash'; // fast, cheap — default for all tasks
|
|
4
4
|
export const PRO_MODEL = 'deepseek/deepseek-r1'; // high-reasoning — used after errors/retries
|
|
5
|
+
export const CLAUDE_MODEL = 'anthropic/claude-sonnet-4'; // high-quality — for complex tasks
|
|
6
|
+
// Model routing: decide which model to use based on task complexity
|
|
7
|
+
export function selectModel(userMessage, retryCount = 0) {
|
|
8
|
+
// Always use PRO_MODEL for retries (better at fixing errors)
|
|
9
|
+
if (retryCount > 0)
|
|
10
|
+
return PRO_MODEL;
|
|
11
|
+
const msg = userMessage.toLowerCase();
|
|
12
|
+
// Complex task indicators
|
|
13
|
+
const COMPLEX_INDICATORS = [
|
|
14
|
+
'refactor', 'architecture', 'complex', 'redesign', 'migrate',
|
|
15
|
+
'debug', 'fix error', 'error handling', 'optimization', 'performance',
|
|
16
|
+
'security', 'authentication', 'database', 'deploy', 'infrastructure',
|
|
17
|
+
'test', 'testing', 'integration', 'api', 'microservice',
|
|
18
|
+
'typescript', 'type safety', 'interface', 'generic',
|
|
19
|
+
];
|
|
20
|
+
// Long messages are usually more complex
|
|
21
|
+
if (userMessage.length > 500)
|
|
22
|
+
return PRO_MODEL;
|
|
23
|
+
// Check for complex indicators
|
|
24
|
+
for (const indicator of COMPLEX_INDICATORS) {
|
|
25
|
+
if (msg.includes(indicator))
|
|
26
|
+
return PRO_MODEL;
|
|
27
|
+
}
|
|
28
|
+
// Multi-file tasks (mentions multiple files)
|
|
29
|
+
const fileMentions = (userMessage.match(/\.\w{2,5}/g) ?? []).length;
|
|
30
|
+
if (fileMentions > 5)
|
|
31
|
+
return PRO_MODEL;
|
|
32
|
+
// Default: use flash for speed
|
|
33
|
+
return FLASH_MODEL;
|
|
34
|
+
}
|
|
5
35
|
export async function fetchSystemPrompt(token) {
|
|
6
36
|
try {
|
|
7
37
|
const res = await fetch(`${PROMPT_URL}?token=${encodeURIComponent(token)}`, {
|
|
@@ -24,20 +54,68 @@ const TIMEOUT_MS = 300_000; // 5 min — AI may generate large responses
|
|
|
24
54
|
const CHUNK_TIMEOUT_MS = 60_000; // 1 min without any chunk = abort
|
|
25
55
|
const MAX_CONTINUATIONS = 5; // auto-continue up to 5 times on length cutoff
|
|
26
56
|
const MAX_429_RETRIES = 3; // retry on rate-limit with exponential backoff
|
|
27
|
-
//
|
|
28
|
-
// Only the first message can be role:system — everything else must alternate user/assistant.
|
|
29
|
-
// Mid-conversation system messages (file contents, /add files) become role:user.
|
|
30
|
-
// Consecutive user/system-as-user messages are merged to maintain proper alternation.
|
|
57
|
+
// Smart context management: priority-based trimming with summarization
|
|
31
58
|
function prepareMessages(messages) {
|
|
32
|
-
|
|
33
|
-
const
|
|
59
|
+
const MAX_CHARS = 150_000; // ~45k tokens
|
|
60
|
+
const MIN_KEEP = 6; // Always keep at least last 6 messages (3 user + 3 assistant)
|
|
34
61
|
let totalChars = messages.reduce((s, m) => s + String(m.content).length, 0);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
62
|
+
// If under limit, just convert and return
|
|
63
|
+
if (totalChars <= MAX_CHARS) {
|
|
64
|
+
return convertAndMerge(messages);
|
|
65
|
+
}
|
|
66
|
+
// Smart trimming strategy:
|
|
67
|
+
// 1. Keep system prompt (index 0)
|
|
68
|
+
// 2. Keep last MIN_KEEP messages
|
|
69
|
+
// 3. Summarize middle messages
|
|
70
|
+
// 4. Drop oldest middle messages if still over limit
|
|
71
|
+
const systemMsg = messages[0];
|
|
72
|
+
const recentMessages = messages.slice(-MIN_KEEP);
|
|
73
|
+
const middleMessages = messages.slice(1, messages.length - MIN_KEEP);
|
|
74
|
+
// Calculate chars for system + recent
|
|
75
|
+
const systemChars = String(systemMsg.content).length;
|
|
76
|
+
const recentChars = recentMessages.reduce((s, m) => s + String(m.content).length, 0);
|
|
77
|
+
const availableForMiddle = MAX_CHARS - systemChars - recentChars;
|
|
78
|
+
if (availableForMiddle <= 0 || middleMessages.length === 0) {
|
|
79
|
+
// No room for middle messages — just use system + recent
|
|
80
|
+
return convertAndMerge([systemMsg, ...recentMessages]);
|
|
39
81
|
}
|
|
40
|
-
|
|
82
|
+
// Summarize middle messages into a compact summary
|
|
83
|
+
const summary = summarizeMessages(middleMessages, availableForMiddle);
|
|
84
|
+
const summarizedMessages = [
|
|
85
|
+
systemMsg,
|
|
86
|
+
{ role: 'system', content: `[Previous conversation summary]\n${summary}` },
|
|
87
|
+
...recentMessages,
|
|
88
|
+
];
|
|
89
|
+
return convertAndMerge(summarizedMessages);
|
|
90
|
+
}
|
|
91
|
+
// Summarize messages into a compact format
|
|
92
|
+
function summarizeMessages(messages, maxChars) {
|
|
93
|
+
const parts = [];
|
|
94
|
+
let totalChars = 0;
|
|
95
|
+
for (const msg of messages) {
|
|
96
|
+
const content = String(msg.content);
|
|
97
|
+
const role = msg.role === 'user' ? 'User' : 'AI';
|
|
98
|
+
// For short messages, include them fully
|
|
99
|
+
if (content.length < 200) {
|
|
100
|
+
const line = `${role}: ${content.slice(0, 200)}`;
|
|
101
|
+
if (totalChars + line.length < maxChars) {
|
|
102
|
+
parts.push(line);
|
|
103
|
+
totalChars += line.length;
|
|
104
|
+
}
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
// For long messages, include first 100 chars + "... [truncated]"
|
|
108
|
+
const truncated = `${role}: ${content.slice(0, 100)}... [${content.length} chars total]`;
|
|
109
|
+
if (totalChars + truncated.length < maxChars) {
|
|
110
|
+
parts.push(truncated);
|
|
111
|
+
totalChars += truncated.length;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return parts.join('\n') || '[No previous context available]';
|
|
115
|
+
}
|
|
116
|
+
// Convert messages to OpenAI format and merge consecutive user messages
|
|
117
|
+
function convertAndMerge(messages) {
|
|
118
|
+
const converted = messages.map((msg, i) => {
|
|
41
119
|
if (msg.role === 'system' && i > 0)
|
|
42
120
|
return { role: 'user', content: msg.content };
|
|
43
121
|
return msg;
|