draftify-cli 1.0.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 +47 -0
- package/dist/commands/login.js +70 -0
- package/dist/commands/refactor.js +64 -0
- package/dist/index.js +51 -0
- package/dist/repl.js +1102 -0
- package/dist/utils/api.js +106 -0
- package/dist/utils/chats.js +52 -0
- package/dist/utils/config.js +73 -0
- package/dist/utils/skills.js +45 -0
- package/dist/utils/ui.js +135 -0
- package/package.json +39 -0
package/dist/repl.js
ADDED
|
@@ -0,0 +1,1102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.globalAbortController = void 0;
|
|
7
|
+
exports.startRepl = startRepl;
|
|
8
|
+
const readline_1 = __importDefault(require("readline"));
|
|
9
|
+
const stream_1 = require("stream");
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const fs_1 = __importDefault(require("fs"));
|
|
12
|
+
const ui_1 = require("./utils/ui");
|
|
13
|
+
const login_1 = require("./commands/login");
|
|
14
|
+
const api_1 = require("./utils/api");
|
|
15
|
+
const config_1 = require("./utils/config");
|
|
16
|
+
const chats_1 = require("./utils/chats");
|
|
17
|
+
const kleur_1 = require("kleur");
|
|
18
|
+
// Helper to gather dynamic context from the workspace based on user prompt
|
|
19
|
+
function getContextForPrompt(dir, prompt) {
|
|
20
|
+
const codeFiles = [];
|
|
21
|
+
const allPaths = [];
|
|
22
|
+
const maxPaths = 200;
|
|
23
|
+
function walk(currentDir, depth = 0) {
|
|
24
|
+
if (depth > 4 || allPaths.length >= maxPaths)
|
|
25
|
+
return;
|
|
26
|
+
let entries;
|
|
27
|
+
try {
|
|
28
|
+
entries = fs_1.default.readdirSync(currentDir, { withFileTypes: true });
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
if (allPaths.length >= maxPaths)
|
|
35
|
+
break;
|
|
36
|
+
const fullPath = path_1.default.join(currentDir, entry.name);
|
|
37
|
+
const relPath = path_1.default.relative(dir, fullPath);
|
|
38
|
+
if (entry.isDirectory()) {
|
|
39
|
+
if (!['node_modules', '.git', 'dist', 'build', '.next', '__pycache__', 'venv', 'env'].includes(entry.name)) {
|
|
40
|
+
walk(fullPath, depth + 1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else if (entry.isFile()) {
|
|
44
|
+
allPaths.push(relPath);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
walk(dir, 0);
|
|
49
|
+
const lowercasePrompt = prompt.toLowerCase();
|
|
50
|
+
// Check if any specific workspace files are mentioned in the prompt
|
|
51
|
+
const matchedFiles = allPaths.filter(filePath => {
|
|
52
|
+
const baseName = path_1.default.basename(filePath).toLowerCase();
|
|
53
|
+
const nameWithoutExt = path_1.default.parse(baseName).name.toLowerCase();
|
|
54
|
+
const ext = path_1.default.extname(filePath).toLowerCase();
|
|
55
|
+
const validExts = [
|
|
56
|
+
'.ts', '.tsx', '.js', '.jsx', '.css', '.json', '.html',
|
|
57
|
+
'.py', '.md', '.txt', '.c', '.cpp', '.h', '.java', '.go', '.rs', '.php', '.rb', '.sh'
|
|
58
|
+
];
|
|
59
|
+
if (!validExts.includes(ext))
|
|
60
|
+
return false;
|
|
61
|
+
// Check if the prompt contains the full filename (e.g. 'snake.py') or name without ext (e.g. 'snake')
|
|
62
|
+
return lowercasePrompt.includes(baseName) || (nameWithoutExt.length > 2 && lowercasePrompt.includes(nameWithoutExt));
|
|
63
|
+
});
|
|
64
|
+
let filesToRead = [];
|
|
65
|
+
if (matchedFiles.length > 0) {
|
|
66
|
+
// If specific files are mentioned, only load those files
|
|
67
|
+
filesToRead = matchedFiles.slice(0, 5);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
// Fallback: Always load the most recently modified source files to maintain context
|
|
71
|
+
const sourceFiles = allPaths.filter(filePath => {
|
|
72
|
+
const ext = path_1.default.extname(filePath).toLowerCase();
|
|
73
|
+
return ['.ts', '.tsx', '.js', '.jsx', '.py', '.c', '.cpp', '.java', '.go', '.rs', '.css', '.html'].includes(ext);
|
|
74
|
+
});
|
|
75
|
+
const filesWithStats = sourceFiles.map(filePath => {
|
|
76
|
+
const fullPath = path_1.default.resolve(dir, filePath);
|
|
77
|
+
try {
|
|
78
|
+
const stats = fs_1.default.statSync(fullPath);
|
|
79
|
+
return { path: filePath, mtime: stats.mtimeMs };
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return { path: filePath, mtime: 0 };
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
// Sort by last modified time (descending)
|
|
86
|
+
filesWithStats.sort((a, b) => b.mtime - a.mtime);
|
|
87
|
+
filesToRead = filesWithStats.slice(0, 3).map(f => f.path);
|
|
88
|
+
}
|
|
89
|
+
// Load the contents of selected files
|
|
90
|
+
for (const filePath of filesToRead) {
|
|
91
|
+
const fullPath = path_1.default.resolve(dir, filePath);
|
|
92
|
+
try {
|
|
93
|
+
ui_1.ui.fileRead(filePath);
|
|
94
|
+
const content = fs_1.default.readFileSync(fullPath, 'utf-8');
|
|
95
|
+
if (content.length < 50000) { // Limit to 50KB per file
|
|
96
|
+
codeFiles.push({ path: filePath, content });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// Ignore unreadable files
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
let structureStr = allPaths.join('\n');
|
|
104
|
+
if (allPaths.length >= maxPaths) {
|
|
105
|
+
structureStr += '\n... (and more files omitted)';
|
|
106
|
+
}
|
|
107
|
+
let combinedCode = `--- PROJECT DIRECTORY STRUCTURE ---\n${structureStr}\n\n`;
|
|
108
|
+
if (codeFiles.length > 0) {
|
|
109
|
+
combinedCode += `--- ATTACHED FILE CONTENTS ---\n`;
|
|
110
|
+
combinedCode += codeFiles.map(f => `--- File: ${f.path} ---\n${f.content}\n`).join('\n');
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
fileName: codeFiles.length > 0 ? codeFiles[0].path : "workspace_context",
|
|
114
|
+
code: combinedCode
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function askSimpleQuestion(questionText) {
|
|
118
|
+
return new Promise((resolve) => {
|
|
119
|
+
process.stdout.write(questionText);
|
|
120
|
+
const isRaw = process.stdin.isRaw;
|
|
121
|
+
process.stdin.setRawMode(false);
|
|
122
|
+
const onData = (data) => {
|
|
123
|
+
process.stdin.off('data', onData);
|
|
124
|
+
process.stdin.setRawMode(isRaw);
|
|
125
|
+
resolve(data.toString().trim());
|
|
126
|
+
};
|
|
127
|
+
process.stdin.on('data', onData);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
let lastSigintTime = 0;
|
|
131
|
+
exports.globalAbortController = null;
|
|
132
|
+
const backgroundProcesses = [];
|
|
133
|
+
function cleanupBackgroundProcesses() {
|
|
134
|
+
if (backgroundProcesses.length === 0)
|
|
135
|
+
return;
|
|
136
|
+
for (const proc of backgroundProcesses) {
|
|
137
|
+
try {
|
|
138
|
+
if (process.platform === "win32") {
|
|
139
|
+
const { execSync } = require('child_process');
|
|
140
|
+
execSync(`taskkill /pid ${proc.pid} /f /t`, { stdio: 'ignore' });
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
process.kill(-proc.pid, 'SIGKILL');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch (e) {
|
|
147
|
+
try {
|
|
148
|
+
proc.kill('SIGKILL');
|
|
149
|
+
}
|
|
150
|
+
catch (err) { }
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
backgroundProcesses.length = 0;
|
|
154
|
+
}
|
|
155
|
+
process.on('exit', cleanupBackgroundProcesses);
|
|
156
|
+
// Catch global SIGINT for when readline is NOT active (e.g. during API call)
|
|
157
|
+
process.on('SIGINT', () => {
|
|
158
|
+
if (exports.globalAbortController) {
|
|
159
|
+
exports.globalAbortController.abort();
|
|
160
|
+
exports.globalAbortController = null;
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const now = Date.now();
|
|
164
|
+
if (now - lastSigintTime < 2000) {
|
|
165
|
+
cleanupBackgroundProcesses();
|
|
166
|
+
console.log("\nGoodbye!");
|
|
167
|
+
process.exit(0);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
lastSigintTime = now;
|
|
171
|
+
console.log("\nPress Ctrl+C again to exit.");
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
// Ensure keypress events are emitted on stdin globally
|
|
175
|
+
readline_1.default.emitKeypressEvents(process.stdin);
|
|
176
|
+
function getInteractiveInput(promptText, commands) {
|
|
177
|
+
return new Promise((resolve) => {
|
|
178
|
+
process.stdout.write("\x1b[1 q");
|
|
179
|
+
const inputStream = new stream_1.PassThrough();
|
|
180
|
+
const isRaw = process.stdin.isRaw;
|
|
181
|
+
if (process.stdin.isTTY) {
|
|
182
|
+
process.stdin.setRawMode(true);
|
|
183
|
+
}
|
|
184
|
+
process.stdin.resume(); // Crucial: forces the stream to resume flowing data after being paused
|
|
185
|
+
let isPasting = false;
|
|
186
|
+
let pasteLines = 0;
|
|
187
|
+
let accumulatedLines = [];
|
|
188
|
+
let pasteTimeout = null;
|
|
189
|
+
const rl = readline_1.default.createInterface({
|
|
190
|
+
input: inputStream,
|
|
191
|
+
output: process.stdout,
|
|
192
|
+
prompt: promptText,
|
|
193
|
+
completer: (line) => {
|
|
194
|
+
if (line.startsWith("/")) {
|
|
195
|
+
const hits = commands.filter((c) => c.startsWith(line));
|
|
196
|
+
return [hits.length ? hits : commands, line];
|
|
197
|
+
}
|
|
198
|
+
return [[], line];
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
const onData = (data) => {
|
|
202
|
+
const str = data.toString('utf-8');
|
|
203
|
+
// Detect Alt+Enter or Esc+Enter (which sends \x1b\r) or CSI u Shift+Enter (\x1b[13;2u)
|
|
204
|
+
if (str === '\x1b\r' || str === '\x1b\n' || str === '\x1b[13;2u') {
|
|
205
|
+
// User pressed Alt+Enter or Shift+Enter
|
|
206
|
+
accumulatedLines.push(rl.line);
|
|
207
|
+
rl.write(null, { ctrl: true, name: 'u' }); // Clear current line in readline
|
|
208
|
+
process.stdout.write('\n' + ' '.repeat(promptText.length));
|
|
209
|
+
return; // Do not pass to readline, avoiding a submit
|
|
210
|
+
}
|
|
211
|
+
// Paste detection: if data contains multiple lines in one chunk
|
|
212
|
+
if (str.length > 5 && (str.includes('\n') || str.includes('\r'))) {
|
|
213
|
+
isPasting = true;
|
|
214
|
+
const matches = str.match(/\r?\n/g);
|
|
215
|
+
if (matches) {
|
|
216
|
+
pasteLines += matches.length;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
inputStream.write(data);
|
|
220
|
+
};
|
|
221
|
+
process.stdin.on('data', onData);
|
|
222
|
+
rl.prompt();
|
|
223
|
+
const onKeypress = () => {
|
|
224
|
+
const line = rl.line;
|
|
225
|
+
process.stdout.write(`\x1b[s\x1b[K\x1b[u`);
|
|
226
|
+
if (line.startsWith("/") && line.length > 0) {
|
|
227
|
+
const hits = commands.filter((c) => c.startsWith(line));
|
|
228
|
+
if (hits.length >= 1 && hits[0] !== line) {
|
|
229
|
+
const remaining = hits[0].slice(line.length);
|
|
230
|
+
process.stdout.write(`\x1b[s\x1b[90m${remaining}\x1b[0m\x1b[u`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
const keypressHandler = (str, key) => {
|
|
235
|
+
const line = rl.line;
|
|
236
|
+
// Alt+Enter or Shift+Enter manual interception
|
|
237
|
+
if ((key && key.name === 'return' && key.shift) || (key && key.sequence === '\x1b\r')) {
|
|
238
|
+
// This doesn't trigger 'line' in readline if we intercept it, but readline still saw it.
|
|
239
|
+
}
|
|
240
|
+
if (key && key.name === 'right' && line.startsWith("/")) {
|
|
241
|
+
const hits = commands.filter((c) => c.startsWith(line));
|
|
242
|
+
if (hits.length >= 1 && hits[0] !== line) {
|
|
243
|
+
const remaining = hits[0].slice(line.length);
|
|
244
|
+
rl.write(remaining);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
setTimeout(onKeypress, 0);
|
|
249
|
+
};
|
|
250
|
+
process.stdin.on('keypress', keypressHandler);
|
|
251
|
+
rl.on('line', (line) => {
|
|
252
|
+
accumulatedLines.push(line);
|
|
253
|
+
// Handle explicit line continuation using backslash
|
|
254
|
+
if (line.trimEnd().endsWith('\\')) {
|
|
255
|
+
accumulatedLines[accumulatedLines.length - 1] = line.trimEnd().slice(0, -1);
|
|
256
|
+
rl.setPrompt(' '.repeat(promptText.length));
|
|
257
|
+
rl.prompt();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (isPasting) {
|
|
261
|
+
// If we are pasting, we don't submit immediately.
|
|
262
|
+
// We wait for the chunk to finish processing.
|
|
263
|
+
if (pasteTimeout)
|
|
264
|
+
clearTimeout(pasteTimeout);
|
|
265
|
+
pasteTimeout = setTimeout(() => {
|
|
266
|
+
isPasting = false; // Paste finished
|
|
267
|
+
if (pasteLines > 1) {
|
|
268
|
+
process.stdout.write(`\n\x1b[90m[${pasteLines} lines pasted...]\x1b[0m\n`);
|
|
269
|
+
}
|
|
270
|
+
pasteLines = 0;
|
|
271
|
+
rl.setPrompt(' '.repeat(promptText.length));
|
|
272
|
+
rl.prompt(); // Provide a prompt for them to press enter again to submit
|
|
273
|
+
}, 400);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
// If they just press Enter on an empty line while there are accumulated lines,
|
|
277
|
+
// we assume they want to submit the pasted content.
|
|
278
|
+
// Wait, if it wasn't a paste, and they just pressed Enter, we submit.
|
|
279
|
+
process.stdin.off('data', onData);
|
|
280
|
+
process.stdin.off('keypress', keypressHandler);
|
|
281
|
+
if (process.stdin.isTTY) {
|
|
282
|
+
process.stdin.setRawMode(isRaw);
|
|
283
|
+
}
|
|
284
|
+
process.stdin.pause();
|
|
285
|
+
process.stdout.write(`\x1b[s\x1b[K\x1b[u`);
|
|
286
|
+
// Clean up empty line at the end if it was from submitting a paste
|
|
287
|
+
if (accumulatedLines.length > 1 && accumulatedLines[accumulatedLines.length - 1] === "") {
|
|
288
|
+
accumulatedLines.pop();
|
|
289
|
+
}
|
|
290
|
+
const inputStr = accumulatedLines.join('\n').trim();
|
|
291
|
+
rl.close();
|
|
292
|
+
resolve(inputStr);
|
|
293
|
+
});
|
|
294
|
+
rl.on('SIGINT', () => {
|
|
295
|
+
process.stdin.off('data', onData);
|
|
296
|
+
process.stdin.off('keypress', keypressHandler);
|
|
297
|
+
if (process.stdin.isTTY) {
|
|
298
|
+
process.stdin.setRawMode(isRaw);
|
|
299
|
+
}
|
|
300
|
+
process.stdin.pause();
|
|
301
|
+
process.stdout.write(`\x1b[s\x1b[K\x1b[u`);
|
|
302
|
+
rl.close();
|
|
303
|
+
const now = Date.now();
|
|
304
|
+
if (now - lastSigintTime < 2000) {
|
|
305
|
+
console.log("\nGoodbye!");
|
|
306
|
+
process.exit(0);
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
lastSigintTime = now;
|
|
310
|
+
console.log("\nPress Ctrl+C again to exit.");
|
|
311
|
+
resolve(null);
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
async function startRepl(initialUsername) {
|
|
317
|
+
// Prevent Node.js from exiting when stdin listeners are temporarily removed
|
|
318
|
+
setInterval(() => { }, 1000000);
|
|
319
|
+
const repoName = path_1.default.basename(process.cwd());
|
|
320
|
+
let currentModel = (0, config_1.getModel)();
|
|
321
|
+
let username = initialUsername;
|
|
322
|
+
let plan = "Draftify Scale";
|
|
323
|
+
const profile = await (0, api_1.getUserProfile)();
|
|
324
|
+
if (profile) {
|
|
325
|
+
username = profile.username;
|
|
326
|
+
plan = profile.plan;
|
|
327
|
+
}
|
|
328
|
+
const renderWelcome = () => {
|
|
329
|
+
console.clear();
|
|
330
|
+
ui_1.ui.welcomeScreen(repoName, currentModel, username, plan, (0, config_1.getThinkingLevel)());
|
|
331
|
+
};
|
|
332
|
+
renderWelcome();
|
|
333
|
+
let conversationHistory = [];
|
|
334
|
+
let currentSessionId = Date.now().toString();
|
|
335
|
+
let autoPrompt = "";
|
|
336
|
+
let alwaysAllowCommands = false;
|
|
337
|
+
const loop = async () => {
|
|
338
|
+
while (true) {
|
|
339
|
+
let inputLine = "";
|
|
340
|
+
if (autoPrompt) {
|
|
341
|
+
inputLine = autoPrompt;
|
|
342
|
+
autoPrompt = "";
|
|
343
|
+
ui_1.ui.info(`Answers automatically sent to the AI.`);
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
const commandsList = ['/new-chat', '/chats', '/skills', '/model', '/thinking-level', '/login', '/logout', '/help', '/exit', '/quit'];
|
|
347
|
+
ui_1.ui.divider();
|
|
348
|
+
let answer = await getInteractiveInput((0, kleur_1.dim)("> "), commandsList);
|
|
349
|
+
ui_1.ui.divider();
|
|
350
|
+
if (answer === null)
|
|
351
|
+
continue;
|
|
352
|
+
inputLine = answer.trim();
|
|
353
|
+
}
|
|
354
|
+
if (!inputLine)
|
|
355
|
+
continue;
|
|
356
|
+
const cmdLower = inputLine.split(" ")[0].toLowerCase();
|
|
357
|
+
const token = (0, config_1.getToken)();
|
|
358
|
+
if (!token && cmdLower !== "/login" && cmdLower !== "/exit" && cmdLower !== "/quit") {
|
|
359
|
+
ui_1.ui.error("You are not logged in! Please use the /login command to continue.");
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
// 1. Slash commands (/)
|
|
363
|
+
if (inputLine.startsWith("/")) {
|
|
364
|
+
const parts = inputLine.split(" ");
|
|
365
|
+
const cmd = parts[0].slice(1).toLowerCase();
|
|
366
|
+
if (cmd === "exit" || cmd === "quit") {
|
|
367
|
+
ui_1.ui.success("Goodbye!");
|
|
368
|
+
process.exit(0);
|
|
369
|
+
}
|
|
370
|
+
else if (cmd === "clear" || cmd === "new-chat") {
|
|
371
|
+
conversationHistory = [];
|
|
372
|
+
currentSessionId = Date.now().toString(); // Reset session ID for a new chat
|
|
373
|
+
ui_1.ui.success("New chat started! Conversation history cleared.");
|
|
374
|
+
}
|
|
375
|
+
else if (cmd === "login") {
|
|
376
|
+
const newUsername = await (0, login_1.loginCommand)();
|
|
377
|
+
const p = await (0, api_1.getUserProfile)();
|
|
378
|
+
if (p) {
|
|
379
|
+
username = p.username;
|
|
380
|
+
plan = p.plan;
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
username = newUsername;
|
|
384
|
+
}
|
|
385
|
+
conversationHistory = [];
|
|
386
|
+
currentSessionId = Date.now().toString();
|
|
387
|
+
renderWelcome();
|
|
388
|
+
}
|
|
389
|
+
else if (cmd === "logout") {
|
|
390
|
+
(0, config_1.saveConfig)({ token: "" });
|
|
391
|
+
username = undefined;
|
|
392
|
+
plan = "Draftify Scale";
|
|
393
|
+
conversationHistory = [];
|
|
394
|
+
currentSessionId = Date.now().toString();
|
|
395
|
+
renderWelcome();
|
|
396
|
+
ui_1.ui.success("Successfully logged out. Use the /login command to log back in.");
|
|
397
|
+
}
|
|
398
|
+
else if (cmd === "skills") {
|
|
399
|
+
const { MultiSelect } = require('enquirer');
|
|
400
|
+
const { getAvailableSkills } = require('./utils/skills');
|
|
401
|
+
try {
|
|
402
|
+
const availableSkills = getAvailableSkills();
|
|
403
|
+
if (availableSkills.length === 0) {
|
|
404
|
+
ui_1.ui.info("No skills found in .agents/skills directories (neither globally nor locally).");
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
const currentSkills = (0, config_1.getSkills)();
|
|
408
|
+
const prompt = new MultiSelect({
|
|
409
|
+
name: 'skills',
|
|
410
|
+
message: 'Toggle Draftify Skills (Space to select, Enter to save):',
|
|
411
|
+
choices: availableSkills.map((s) => ({
|
|
412
|
+
name: s.name,
|
|
413
|
+
value: s.name,
|
|
414
|
+
message: `${s.name} ${(0, kleur_1.dim)(`(${s.source})`)}`
|
|
415
|
+
})),
|
|
416
|
+
initial: currentSkills
|
|
417
|
+
});
|
|
418
|
+
const answer = await prompt.run();
|
|
419
|
+
(0, config_1.setSkills)(answer);
|
|
420
|
+
ui_1.ui.success(`Skills saved: ${answer.join(', ') || 'None enabled'}`);
|
|
421
|
+
}
|
|
422
|
+
catch (e) {
|
|
423
|
+
ui_1.ui.info("Skill selection aborted.");
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
else if (cmd === "help") {
|
|
427
|
+
ui_1.ui.header("Available commands:");
|
|
428
|
+
console.log(` ${(0, kleur_1.cyan)("/new-chat")} - Start a new chat, clear history`);
|
|
429
|
+
console.log(` ${(0, kleur_1.cyan)("/chats")} - List and load previous chats`);
|
|
430
|
+
console.log(` ${(0, kleur_1.cyan)("/skills")} - Manage and configure skills`);
|
|
431
|
+
console.log(` ${(0, kleur_1.cyan)("/model")} - Switch model (e.g., /model Opus 4.8-level)`);
|
|
432
|
+
console.log(` ${(0, kleur_1.cyan)("/thinking-level")} - Adjust the AI's thinking process depth`);
|
|
433
|
+
console.log(` ${(0, kleur_1.cyan)("/login")} - Log in to your Draftify account`);
|
|
434
|
+
console.log(` ${(0, kleur_1.cyan)("/logout")} - Log out of your account`);
|
|
435
|
+
console.log(` ${(0, kleur_1.cyan)("/help")} - List commands`);
|
|
436
|
+
console.log(` ${(0, kleur_1.cyan)("/clear")} - Clear the terminal screen`);
|
|
437
|
+
console.log(` ${(0, kleur_1.cyan)("/exit")} - Exit the CLI`);
|
|
438
|
+
console.log(` ${(0, kleur_1.cyan)("!")}${(0, kleur_1.dim)("<command>")} - Run a system command in the terminal (e.g., !npm run build)\n`);
|
|
439
|
+
}
|
|
440
|
+
else if (cmd === "clear") {
|
|
441
|
+
renderWelcome();
|
|
442
|
+
}
|
|
443
|
+
else if (cmd === "chats") {
|
|
444
|
+
const chats = (0, chats_1.loadChats)();
|
|
445
|
+
if (chats.length === 0) {
|
|
446
|
+
ui_1.ui.info("No saved chats found.");
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
const { Select } = require('enquirer');
|
|
450
|
+
try {
|
|
451
|
+
const prompt = new Select({
|
|
452
|
+
name: 'chat',
|
|
453
|
+
message: 'Select a previous conversation:',
|
|
454
|
+
choices: [
|
|
455
|
+
...chats.map((c, idx) => ({
|
|
456
|
+
name: c.id,
|
|
457
|
+
message: `[${c.model}] ${c.title} (${new Date(c.updatedAt).toLocaleString()})`
|
|
458
|
+
})),
|
|
459
|
+
{ name: 'cancel', message: 'Cancel' }
|
|
460
|
+
]
|
|
461
|
+
});
|
|
462
|
+
const choiceId = await prompt.run();
|
|
463
|
+
if (choiceId !== 'cancel') {
|
|
464
|
+
const selected = chats.find(c => c.id === choiceId);
|
|
465
|
+
currentSessionId = selected.id;
|
|
466
|
+
conversationHistory = selected.history;
|
|
467
|
+
currentModel = selected.model;
|
|
468
|
+
(0, config_1.setModel)(currentModel);
|
|
469
|
+
renderWelcome();
|
|
470
|
+
ui_1.ui.success(`Conversation successfully loaded: "${selected.title}"`);
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
ui_1.ui.info("Loading aborted.");
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
catch (e) {
|
|
477
|
+
ui_1.ui.info("Loading aborted.");
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
else if (cmd === "model") {
|
|
481
|
+
const newModel = parts.slice(1).join(" ").trim();
|
|
482
|
+
if (newModel) {
|
|
483
|
+
currentModel = newModel;
|
|
484
|
+
(0, config_1.setModel)(currentModel);
|
|
485
|
+
ui_1.ui.success(`Model changed to ${currentModel}`);
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
const { Select } = require('enquirer');
|
|
489
|
+
try {
|
|
490
|
+
const prompt = new Select({
|
|
491
|
+
name: 'model',
|
|
492
|
+
message: 'Select the model to use:',
|
|
493
|
+
choices: [
|
|
494
|
+
'Opus 4.8-level',
|
|
495
|
+
'Opus 4.7-level',
|
|
496
|
+
'Opus 4.6-level',
|
|
497
|
+
'Sonnet 4.6-level',
|
|
498
|
+
'Haiku 4.5-level',
|
|
499
|
+
{ name: 'Cancel', message: 'Cancel' }
|
|
500
|
+
]
|
|
501
|
+
});
|
|
502
|
+
const answer = await prompt.run();
|
|
503
|
+
if (answer !== 'Cancel') {
|
|
504
|
+
currentModel = answer;
|
|
505
|
+
(0, config_1.setModel)(currentModel);
|
|
506
|
+
renderWelcome();
|
|
507
|
+
ui_1.ui.success(`Model updated to: ${currentModel}`);
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
ui_1.ui.info("Model selection aborted.");
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
catch (e) {
|
|
514
|
+
ui_1.ui.info("Model selection aborted.");
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
else if (cmd === "thinking-level") {
|
|
519
|
+
const { Select } = require('enquirer');
|
|
520
|
+
try {
|
|
521
|
+
const prompt = new Select({
|
|
522
|
+
name: 'level',
|
|
523
|
+
message: 'Select Thinking Level:',
|
|
524
|
+
choices: [
|
|
525
|
+
{ name: 'Adaptive', message: 'Adaptive - Auto-detect (best)' },
|
|
526
|
+
{ name: 'Low', message: 'Low - Ultra-fast' },
|
|
527
|
+
{ name: 'Medium', message: 'Medium - Balanced' },
|
|
528
|
+
{ name: 'High', message: 'High - Deep logic' },
|
|
529
|
+
{ name: 'Cancel', message: 'Cancel' }
|
|
530
|
+
]
|
|
531
|
+
});
|
|
532
|
+
const answer = await prompt.run();
|
|
533
|
+
if (answer !== 'Cancel') {
|
|
534
|
+
(0, config_1.setThinkingLevel)(answer);
|
|
535
|
+
renderWelcome();
|
|
536
|
+
ui_1.ui.success(`Thinking level updated to: ${answer}`);
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
ui_1.ui.info("Thinking level selection aborted.");
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
catch (e) {
|
|
543
|
+
ui_1.ui.info("Thinking level selection aborted.");
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
ui_1.ui.error(`Unknown command: /${cmd}`);
|
|
548
|
+
}
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
// 2. Shell commands (!)
|
|
552
|
+
if (inputLine.startsWith("!")) {
|
|
553
|
+
const shellCmd = inputLine.slice(1).trim();
|
|
554
|
+
if (!shellCmd) {
|
|
555
|
+
ui_1.ui.error("No shell command provided.");
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
try {
|
|
559
|
+
ui_1.ui.step(`Running: ${shellCmd}`);
|
|
560
|
+
const outputData = await new Promise((resolve, reject) => {
|
|
561
|
+
const { spawn } = require('child_process');
|
|
562
|
+
const child = spawn(shellCmd, {
|
|
563
|
+
cwd: process.cwd(),
|
|
564
|
+
shell: true
|
|
565
|
+
});
|
|
566
|
+
let outputBuffer = "";
|
|
567
|
+
child.stdout.on('data', (data) => {
|
|
568
|
+
const str = data.toString();
|
|
569
|
+
outputBuffer += str;
|
|
570
|
+
process.stdout.write((0, kleur_1.dim)(str));
|
|
571
|
+
});
|
|
572
|
+
child.stderr.on('data', (data) => {
|
|
573
|
+
const str = data.toString();
|
|
574
|
+
outputBuffer += str;
|
|
575
|
+
process.stderr.write((0, kleur_1.dim)(str));
|
|
576
|
+
});
|
|
577
|
+
child.on('close', (code) => {
|
|
578
|
+
if (code === 0) {
|
|
579
|
+
resolve(outputBuffer);
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
reject(outputBuffer || `Process exited with code ${code}`);
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
child.on('error', (err) => {
|
|
586
|
+
reject(err.message);
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
conversationHistory.push({
|
|
590
|
+
role: "user",
|
|
591
|
+
content: `Executed shell command: ${shellCmd}\nOutput:\n${outputData}`
|
|
592
|
+
});
|
|
593
|
+
ui_1.ui.success("Command executed and output added to context.");
|
|
594
|
+
}
|
|
595
|
+
catch (err) {
|
|
596
|
+
ui_1.ui.error(`Command failed:\n${err}`);
|
|
597
|
+
conversationHistory.push({
|
|
598
|
+
role: "user",
|
|
599
|
+
content: `Executed shell command: ${shellCmd}\nFailed with error:\n${err}`
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
// 3. AI Chat Prompt (default)
|
|
605
|
+
async function applyFileOperations(result) {
|
|
606
|
+
let displayResult = result;
|
|
607
|
+
const isSafePath = (targetPath) => {
|
|
608
|
+
const relative = path_1.default.relative(process.cwd(), targetPath);
|
|
609
|
+
return !relative.startsWith('..') && !path_1.default.isAbsolute(relative);
|
|
610
|
+
};
|
|
611
|
+
const createRegex = /<FILE_CREATE\s+path="([^"]+)">([\s\S]*?)<\/FILE_CREATE>/g;
|
|
612
|
+
let match;
|
|
613
|
+
while ((match = createRegex.exec(result)) !== null) {
|
|
614
|
+
const filePath = match[1];
|
|
615
|
+
const fullPath = path_1.default.resolve(process.cwd(), filePath);
|
|
616
|
+
if (!isSafePath(fullPath)) {
|
|
617
|
+
ui_1.ui.error(`Security error: Cannot create file outside workspace (${filePath})`);
|
|
618
|
+
displayResult = displayResult.replace(match[0], `\n[Blocked: ${filePath} is outside workspace]\n`);
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
// Remove leading/trailing newlines that the LLM might have added immediately inside the tag,
|
|
622
|
+
// but preserve the rest. A simple trim is usually enough, but let's be safe.
|
|
623
|
+
const content = match[2].replace(/^\n|\n$/g, '');
|
|
624
|
+
try {
|
|
625
|
+
fs_1.default.mkdirSync(path_1.default.dirname(fullPath), { recursive: true });
|
|
626
|
+
fs_1.default.writeFileSync(fullPath, content, 'utf-8');
|
|
627
|
+
ui_1.ui.fileSuccess("Created", filePath);
|
|
628
|
+
displayResult = displayResult.replace(match[0], '');
|
|
629
|
+
}
|
|
630
|
+
catch (e) {
|
|
631
|
+
ui_1.ui.error(`Failed to create ${filePath}: ${e.message}`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
const modifyRegex = /<FILE_MODIFY\s+path="([^"]+)">([\s\S]*?)<\/FILE_MODIFY>/g;
|
|
635
|
+
while ((match = modifyRegex.exec(result)) !== null) {
|
|
636
|
+
const filePath = match[1];
|
|
637
|
+
const block = match[2];
|
|
638
|
+
const fullPath = path_1.default.resolve(process.cwd(), filePath);
|
|
639
|
+
if (!isSafePath(fullPath)) {
|
|
640
|
+
ui_1.ui.error(`Security error: Cannot modify file outside workspace (${filePath})`);
|
|
641
|
+
displayResult = displayResult.replace(match[0], `[Blocked: ${filePath} is outside workspace]`);
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
if (fs_1.default.existsSync(fullPath)) {
|
|
645
|
+
let fileContent = fs_1.default.readFileSync(fullPath, 'utf-8');
|
|
646
|
+
const searchBlockRegex = /<<<<<<< SEARCH\n([\s\S]*?)\n=======\n([\s\S]*?)\n>>>>>>>/g;
|
|
647
|
+
let diffMatch;
|
|
648
|
+
let diffApplied = false;
|
|
649
|
+
let hasDiffBlocks = false;
|
|
650
|
+
while ((diffMatch = searchBlockRegex.exec(block)) !== null) {
|
|
651
|
+
hasDiffBlocks = true;
|
|
652
|
+
let search = diffMatch[1];
|
|
653
|
+
let replace = diffMatch[2];
|
|
654
|
+
// Normalize CRLF to LF for reliable string matching
|
|
655
|
+
let normalizedFile = fileContent.replace(/\r\n/g, '\n');
|
|
656
|
+
search = search.replace(/\r\n/g, '\n');
|
|
657
|
+
replace = replace.replace(/\r\n/g, '\n');
|
|
658
|
+
if (normalizedFile.includes(search)) {
|
|
659
|
+
fileContent = normalizedFile.replace(search, replace);
|
|
660
|
+
diffApplied = true;
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
// Fallback: try stripping leading/trailing empty lines/whitespace from the search block
|
|
664
|
+
const trimmedSearch = search.trim();
|
|
665
|
+
if (trimmedSearch && normalizedFile.includes(trimmedSearch)) {
|
|
666
|
+
fileContent = normalizedFile.replace(trimmedSearch, replace.trim());
|
|
667
|
+
diffApplied = true;
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
ui_1.ui.error(`Could not apply diff to ${filePath}: SEARCH block not exactly matched.`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
if (hasDiffBlocks && diffApplied) {
|
|
675
|
+
fs_1.default.writeFileSync(fullPath, fileContent, 'utf-8');
|
|
676
|
+
ui_1.ui.fileSuccess("Modified", filePath);
|
|
677
|
+
displayResult = displayResult.replace(match[0], '');
|
|
678
|
+
}
|
|
679
|
+
else if (!hasDiffBlocks) {
|
|
680
|
+
ui_1.ui.error(`Invalid FILE_MODIFY block for ${filePath}`);
|
|
681
|
+
}
|
|
682
|
+
else {
|
|
683
|
+
displayResult = displayResult.replace(match[0], `[Failed to modify: ${filePath} - SEARCH block mismatch]`);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
else {
|
|
687
|
+
ui_1.ui.error(`Cannot modify ${filePath}: File not found.`);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
const deleteRegex = /<FILE_DELETE\s+path="([^"]+)"\s*\/>/g;
|
|
691
|
+
while ((match = deleteRegex.exec(result)) !== null) {
|
|
692
|
+
const filePath = match[1];
|
|
693
|
+
const fullPath = path_1.default.resolve(process.cwd(), filePath);
|
|
694
|
+
if (!isSafePath(fullPath)) {
|
|
695
|
+
ui_1.ui.error(`Security error: Cannot delete file outside workspace (${filePath})`);
|
|
696
|
+
displayResult = displayResult.replace(match[0], `[Blocked: ${filePath} is outside workspace]`);
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
if (fs_1.default.existsSync(fullPath)) {
|
|
700
|
+
try {
|
|
701
|
+
fs_1.default.unlinkSync(fullPath);
|
|
702
|
+
ui_1.ui.fileSuccess("Deleted", filePath);
|
|
703
|
+
displayResult = displayResult.replace(match[0], '');
|
|
704
|
+
}
|
|
705
|
+
catch (e) {
|
|
706
|
+
ui_1.ui.error(`Failed to delete ${filePath}: ${e.message}`);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
const commandsToRun = [];
|
|
711
|
+
const runRegex = /<RUN_COMMAND>([\s\S]*?)<\/RUN_COMMAND>/g;
|
|
712
|
+
while ((match = runRegex.exec(result)) !== null) {
|
|
713
|
+
commandsToRun.push(match[1].trim());
|
|
714
|
+
displayResult = displayResult.replace(match[0], '');
|
|
715
|
+
}
|
|
716
|
+
const filesToReadAuto = [];
|
|
717
|
+
const readRegex = /<READ_FILE\s+path="([^"]+)"\s*\/>/g;
|
|
718
|
+
while ((match = readRegex.exec(result)) !== null) {
|
|
719
|
+
filesToReadAuto.push(match[1]);
|
|
720
|
+
displayResult = displayResult.replace(match[0], '');
|
|
721
|
+
}
|
|
722
|
+
const dirsToListAuto = [];
|
|
723
|
+
const listRegex = /<LIST_DIR\s+path="([^"]+)"\s*\/>/g;
|
|
724
|
+
while ((match = listRegex.exec(result)) !== null) {
|
|
725
|
+
dirsToListAuto.push(match[1]);
|
|
726
|
+
displayResult = displayResult.replace(match[0], '');
|
|
727
|
+
}
|
|
728
|
+
// Clean up any remaining excessive newlines from the LLM output around the tags
|
|
729
|
+
return {
|
|
730
|
+
cleanResult: displayResult.replace(/\n\s*\n\s*\n/g, '\n\n').trim(),
|
|
731
|
+
commandsToRun,
|
|
732
|
+
filesToReadAuto,
|
|
733
|
+
dirsToListAuto
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
exports.globalAbortController = new AbortController();
|
|
737
|
+
let spinner = null;
|
|
738
|
+
try {
|
|
739
|
+
// Dynamically gather context based on the current request
|
|
740
|
+
const dynamicCtx = getContextForPrompt(process.cwd(), inputLine);
|
|
741
|
+
const requestPayloadFileName = dynamicCtx.fileName;
|
|
742
|
+
const requestPayloadCode = dynamicCtx.code;
|
|
743
|
+
let currentStreamedText = "";
|
|
744
|
+
let finalPrompt = inputLine;
|
|
745
|
+
let activeSkillsData = [];
|
|
746
|
+
const activeSkillNames = (0, config_1.getSkills)();
|
|
747
|
+
if (activeSkillNames.length > 0) {
|
|
748
|
+
const { getAvailableSkills } = require('./utils/skills');
|
|
749
|
+
const allAvailable = getAvailableSkills();
|
|
750
|
+
activeSkillsData = allAvailable.filter((s) => activeSkillNames.includes(s.name));
|
|
751
|
+
}
|
|
752
|
+
const activeActions = new Set();
|
|
753
|
+
spinner = (0, ui_1.createSpinner)("Thinking...").start();
|
|
754
|
+
const rawResult = await (0, api_1.refactorCodeApi)(requestPayloadFileName, requestPayloadCode, finalPrompt, conversationHistory, currentModel, activeSkillsData, (0, config_1.getThinkingLevel)(), (chunk) => {
|
|
755
|
+
currentStreamedText += chunk;
|
|
756
|
+
// Find the last opened tag that hasn't been closed yet
|
|
757
|
+
const createMatch = currentStreamedText.match(/<FILE_CREATE\s+path="([^"]+)"[^>]*>(?![\s\S]*<\/FILE_CREATE>)/);
|
|
758
|
+
const modifyMatch = currentStreamedText.match(/<FILE_MODIFY\s+path="([^"]+)"[^>]*>(?![\s\S]*<\/FILE_MODIFY>)/);
|
|
759
|
+
const deleteMatch = currentStreamedText.match(/<FILE_DELETE\s+path="([^"]+)"/);
|
|
760
|
+
if (createMatch) {
|
|
761
|
+
const filePath = createMatch[1];
|
|
762
|
+
const actionKey = `create:${filePath}`;
|
|
763
|
+
if (!activeActions.has(actionKey)) {
|
|
764
|
+
activeActions.add(actionKey);
|
|
765
|
+
spinner.stop();
|
|
766
|
+
ui_1.ui.fileCreate(filePath);
|
|
767
|
+
spinner.start();
|
|
768
|
+
}
|
|
769
|
+
spinner.setPrefix(`Creating ${filePath}...`);
|
|
770
|
+
}
|
|
771
|
+
else if (modifyMatch) {
|
|
772
|
+
const filePath = modifyMatch[1];
|
|
773
|
+
const actionKey = `modify:${filePath}`;
|
|
774
|
+
if (!activeActions.has(actionKey)) {
|
|
775
|
+
activeActions.add(actionKey);
|
|
776
|
+
spinner.stop();
|
|
777
|
+
ui_1.ui.fileModify(filePath);
|
|
778
|
+
spinner.start();
|
|
779
|
+
}
|
|
780
|
+
spinner.setPrefix(`Modifying ${filePath}...`);
|
|
781
|
+
}
|
|
782
|
+
else if (deleteMatch) {
|
|
783
|
+
const filePath = deleteMatch[1];
|
|
784
|
+
const actionKey = `delete:${filePath}`;
|
|
785
|
+
if (!activeActions.has(actionKey)) {
|
|
786
|
+
activeActions.add(actionKey);
|
|
787
|
+
spinner.stop();
|
|
788
|
+
ui_1.ui.fileDelete(filePath);
|
|
789
|
+
spinner.start();
|
|
790
|
+
}
|
|
791
|
+
spinner.setPrefix(`Deleting ${filePath}...`);
|
|
792
|
+
}
|
|
793
|
+
else if (currentStreamedText.length > 10) {
|
|
794
|
+
spinner.setPrefix("Analyzing...");
|
|
795
|
+
}
|
|
796
|
+
}, exports.globalAbortController.signal);
|
|
797
|
+
spinner.stop();
|
|
798
|
+
const { cleanResult, commandsToRun, filesToReadAuto, dirsToListAuto } = await applyFileOperations(rawResult);
|
|
799
|
+
// --- PARSE CLARIFICATION QUESTIONS ---
|
|
800
|
+
let finalDisplayResult = cleanResult;
|
|
801
|
+
let questions = [];
|
|
802
|
+
const questionMatch = cleanResult.match(/<ASK_QUESTIONS>([\s\S]*?)<\/ASK_QUESTIONS>/);
|
|
803
|
+
if (questionMatch) {
|
|
804
|
+
try {
|
|
805
|
+
questions = JSON.parse(questionMatch[1].trim());
|
|
806
|
+
finalDisplayResult = cleanResult.replace(questionMatch[0], '').trim();
|
|
807
|
+
}
|
|
808
|
+
catch (e) { }
|
|
809
|
+
}
|
|
810
|
+
// --- TOKEN OPTIMIZATION ---
|
|
811
|
+
// Optimize the user input (especially autonomous file reads & command outputs)
|
|
812
|
+
let tokenOptimizedInput = inputLine;
|
|
813
|
+
if (tokenOptimizedInput.includes("I executed your file system requests. Here are the results:")) {
|
|
814
|
+
tokenOptimizedInput = tokenOptimizedInput
|
|
815
|
+
.replace(/File: ([^\n]+)\n```[\s\S]*?```/g, 'File: $1\n[File content read and processed]')
|
|
816
|
+
.replace(/Directory contents of ([^\n]+):\n[\s\S]*?(?=\n\n|\r?\n\r?\n|$)/g, 'Directory contents of $1:\n[Directory listed]');
|
|
817
|
+
}
|
|
818
|
+
if (tokenOptimizedInput.includes("I ran the commands you requested. Here are the results:")) {
|
|
819
|
+
tokenOptimizedInput = tokenOptimizedInput
|
|
820
|
+
.replace(/Command: ([^\n]+)\nOutput:\n[\s\S]*?(?=\n\n|\r?\n\r?\n|$)/g, 'Command: $1\n[Command output omitted from history]')
|
|
821
|
+
.replace(/Command: ([^\n]+)\nFailed with error:\n[\s\S]*?(?=\n\n|\r?\n\r?\n|$)/g, 'Command: $1\n[Command failure output omitted from history]');
|
|
822
|
+
}
|
|
823
|
+
if (tokenOptimizedInput.startsWith("Executed shell command:")) {
|
|
824
|
+
tokenOptimizedInput = tokenOptimizedInput
|
|
825
|
+
.replace(/Output:\n[\s\S]*/, 'Output:\n[Shell output omitted from history]')
|
|
826
|
+
.replace(/Failed with error:\n[\s\S]*/, 'Failed with error:\n[Shell error output omitted from history]');
|
|
827
|
+
}
|
|
828
|
+
// We strip out the huge code blocks from the AI's response before saving to history,
|
|
829
|
+
// because the files are already updated on disk. Re-sending them eats massive tokens!
|
|
830
|
+
const tokenOptimizedResult = finalDisplayResult
|
|
831
|
+
.replace(/<FILE_MODIFY([\s\S]*?)>([\s\S]*?)<\/FILE_MODIFY>/g, '<FILE_MODIFY$1>[Content modified on disk]</FILE_MODIFY>')
|
|
832
|
+
.replace(/<FILE_CREATE([\s\S]*?)>([\s\S]*?)<\/FILE_CREATE>/g, '<FILE_CREATE$1>[Content created on disk]</FILE_CREATE>')
|
|
833
|
+
.replace(/<RUN_COMMAND([\s\S]*?)>([\s\S]*?)<\/RUN_COMMAND>/g, '<RUN_COMMAND$1>[Command execution parsed]</RUN_COMMAND>');
|
|
834
|
+
conversationHistory.push({ role: "user", content: tokenOptimizedInput });
|
|
835
|
+
conversationHistory.push({ role: "assistant", content: tokenOptimizedResult });
|
|
836
|
+
// Keep only the last 6 messages (3 turns) to prevent infinite token growth
|
|
837
|
+
if (conversationHistory.length > 6) {
|
|
838
|
+
conversationHistory = conversationHistory.slice(conversationHistory.length - 6);
|
|
839
|
+
}
|
|
840
|
+
// Save session locally
|
|
841
|
+
const firstUserMsg = conversationHistory.find(h => h.role === "user")?.content || inputLine;
|
|
842
|
+
(0, chats_1.updateChatSession)(currentSessionId, firstUserMsg, currentModel, conversationHistory);
|
|
843
|
+
if (finalDisplayResult) {
|
|
844
|
+
ui_1.ui.box("Draftify AI", finalDisplayResult.split("\n"));
|
|
845
|
+
}
|
|
846
|
+
// --- INTERACTIVE MENU FOR QUESTIONS ---
|
|
847
|
+
if (questions.length > 0) {
|
|
848
|
+
const { Select, Input } = require('enquirer');
|
|
849
|
+
let answers = [];
|
|
850
|
+
for (const q of questions) {
|
|
851
|
+
console.log("");
|
|
852
|
+
const prompt = new Select({
|
|
853
|
+
name: 'answer',
|
|
854
|
+
message: q.question,
|
|
855
|
+
choices: [...q.options, "Provide a custom answer..."]
|
|
856
|
+
});
|
|
857
|
+
let answer = await prompt.run();
|
|
858
|
+
if (answer === "Provide a custom answer...") {
|
|
859
|
+
const customPrompt = new Input({
|
|
860
|
+
message: 'Type your answer:'
|
|
861
|
+
});
|
|
862
|
+
answer = await customPrompt.run();
|
|
863
|
+
}
|
|
864
|
+
answers.push(`- Question: ${q.question}\n Answer: ${answer}`);
|
|
865
|
+
}
|
|
866
|
+
let promptAdditions = [];
|
|
867
|
+
if (autoPrompt)
|
|
868
|
+
promptAdditions.push(autoPrompt);
|
|
869
|
+
promptAdditions.push("My answers to the questions:\n" + answers.join("\n"));
|
|
870
|
+
autoPrompt = promptAdditions.join("\n\n");
|
|
871
|
+
}
|
|
872
|
+
// --- AUTONOMOUS FILE SYSTEM EXPLORATION ---
|
|
873
|
+
let autoExplorationOutputs = [];
|
|
874
|
+
const isSafePathLocal = (targetPath) => {
|
|
875
|
+
const relative = path_1.default.relative(process.cwd(), targetPath);
|
|
876
|
+
return !relative.startsWith('..') && !path_1.default.isAbsolute(relative);
|
|
877
|
+
};
|
|
878
|
+
for (const dirPath of dirsToListAuto) {
|
|
879
|
+
const fullPath = path_1.default.resolve(process.cwd(), dirPath);
|
|
880
|
+
if (!isSafePathLocal(fullPath)) {
|
|
881
|
+
autoExplorationOutputs.push(`Security error: Cannot list directory outside workspace (${dirPath})`);
|
|
882
|
+
ui_1.ui.error(`Blocked attempt to list outside workspace: ${dirPath}`);
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
try {
|
|
886
|
+
if (fs_1.default.existsSync(fullPath)) {
|
|
887
|
+
const items = fs_1.default.readdirSync(fullPath, { withFileTypes: true });
|
|
888
|
+
const list = items.map((item) => `${item.isDirectory() ? '[DIR]' : '[FILE]'} ${item.name}`).join('\n');
|
|
889
|
+
autoExplorationOutputs.push(`Directory contents of ${dirPath}:\n${list}`);
|
|
890
|
+
ui_1.ui.info(`Silently listed directory: ${dirPath}`);
|
|
891
|
+
}
|
|
892
|
+
else {
|
|
893
|
+
autoExplorationOutputs.push(`Directory ${dirPath} not found.`);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
catch (e) {
|
|
897
|
+
autoExplorationOutputs.push(`Failed to list ${dirPath}: ${e.message}`);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
for (const filePath of filesToReadAuto) {
|
|
901
|
+
const fullPath = path_1.default.resolve(process.cwd(), filePath);
|
|
902
|
+
if (!isSafePathLocal(fullPath)) {
|
|
903
|
+
autoExplorationOutputs.push(`Security error: Cannot read file outside workspace (${filePath})`);
|
|
904
|
+
ui_1.ui.error(`Blocked attempt to read outside workspace: ${filePath}`);
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
try {
|
|
908
|
+
if (fs_1.default.existsSync(fullPath)) {
|
|
909
|
+
const content = fs_1.default.readFileSync(fullPath, 'utf-8');
|
|
910
|
+
autoExplorationOutputs.push(`File: ${filePath}\n\`\`\`\n${content}\n\`\`\``);
|
|
911
|
+
ui_1.ui.info(`Silently read: ${filePath}`);
|
|
912
|
+
}
|
|
913
|
+
else {
|
|
914
|
+
autoExplorationOutputs.push(`File ${filePath} not found.`);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
catch (e) {
|
|
918
|
+
autoExplorationOutputs.push(`Failed to read ${filePath}: ${e.message}`);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
if (autoExplorationOutputs.length > 0) {
|
|
922
|
+
let promptAdditions = autoPrompt ? [autoPrompt] : [];
|
|
923
|
+
promptAdditions.push(`I executed your file system requests. Here are the results:\n${autoExplorationOutputs.join('\n\n')}`);
|
|
924
|
+
autoPrompt = promptAdditions.join("\n\n");
|
|
925
|
+
}
|
|
926
|
+
// --- EXECUTE COMMANDS ---
|
|
927
|
+
if (commandsToRun.length > 0) {
|
|
928
|
+
let commandOutputs = [];
|
|
929
|
+
let userAbortedEverything = false;
|
|
930
|
+
for (const cmdToRun of commandsToRun) {
|
|
931
|
+
let runIt = alwaysAllowCommands;
|
|
932
|
+
let runInBackground = false;
|
|
933
|
+
const isLongRunning = /dev|start|server|watch|nodemon|vite|next|host/i.test(cmdToRun);
|
|
934
|
+
if (!runIt) {
|
|
935
|
+
const { Select } = require('enquirer');
|
|
936
|
+
try {
|
|
937
|
+
const choices = [
|
|
938
|
+
{ name: 'yes', message: 'Yes (foreground)' }
|
|
939
|
+
];
|
|
940
|
+
if (isLongRunning) {
|
|
941
|
+
choices.push({ name: 'background', message: 'Yes (background - keep terminal free)' });
|
|
942
|
+
}
|
|
943
|
+
else {
|
|
944
|
+
choices.push({ name: 'background', message: 'Run in background' });
|
|
945
|
+
}
|
|
946
|
+
choices.push({ name: 'no', message: 'No' });
|
|
947
|
+
choices.push({ name: 'always', message: 'Always allow for this session' });
|
|
948
|
+
const prompt = new Select({
|
|
949
|
+
name: 'allow',
|
|
950
|
+
message: `Draftify AI wants to run: ${cmdToRun}\nAllow execution?`,
|
|
951
|
+
choices: choices
|
|
952
|
+
});
|
|
953
|
+
const answer = await prompt.run();
|
|
954
|
+
if (answer === 'yes')
|
|
955
|
+
runIt = true;
|
|
956
|
+
if (answer === 'background') {
|
|
957
|
+
runIt = true;
|
|
958
|
+
runInBackground = true;
|
|
959
|
+
}
|
|
960
|
+
if (answer === 'always') {
|
|
961
|
+
runIt = true;
|
|
962
|
+
alwaysAllowCommands = true;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
catch (e) {
|
|
966
|
+
runIt = false;
|
|
967
|
+
userAbortedEverything = true;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
else {
|
|
971
|
+
if (isLongRunning) {
|
|
972
|
+
runInBackground = true;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
if (userAbortedEverything) {
|
|
976
|
+
ui_1.ui.info(`Command execution aborted by user.`);
|
|
977
|
+
break;
|
|
978
|
+
}
|
|
979
|
+
if (runIt) {
|
|
980
|
+
if (runInBackground) {
|
|
981
|
+
ui_1.ui.step(`Starting in background: ${cmdToRun}`);
|
|
982
|
+
try {
|
|
983
|
+
const { spawn } = require('child_process');
|
|
984
|
+
const child = spawn(cmdToRun, {
|
|
985
|
+
cwd: process.cwd(),
|
|
986
|
+
shell: true,
|
|
987
|
+
detached: process.platform !== 'win32'
|
|
988
|
+
});
|
|
989
|
+
backgroundProcesses.push(child);
|
|
990
|
+
let outputBuffer = "";
|
|
991
|
+
const onData = (data) => {
|
|
992
|
+
const str = data.toString();
|
|
993
|
+
outputBuffer += str;
|
|
994
|
+
process.stdout.write((0, kleur_1.dim)(str));
|
|
995
|
+
};
|
|
996
|
+
child.stdout.on('data', onData);
|
|
997
|
+
child.stderr.on('data', onData);
|
|
998
|
+
let hasExited = false;
|
|
999
|
+
let exitCode = null;
|
|
1000
|
+
child.on('close', (code) => {
|
|
1001
|
+
hasExited = true;
|
|
1002
|
+
exitCode = code;
|
|
1003
|
+
});
|
|
1004
|
+
// Let it initialize for 1.5 seconds, displaying standard log output
|
|
1005
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
1006
|
+
// Detach listeners from stdout so log lines don't garble CLI prompt
|
|
1007
|
+
child.stdout.off('data', onData);
|
|
1008
|
+
child.stderr.off('data', onData);
|
|
1009
|
+
if (hasExited) {
|
|
1010
|
+
ui_1.ui.error(`Background process failed immediately with exit code ${exitCode}.`);
|
|
1011
|
+
commandOutputs.push(`Command: ${cmdToRun}\nFailed immediately with code ${exitCode}\nOutput:\n${outputBuffer}`);
|
|
1012
|
+
}
|
|
1013
|
+
else {
|
|
1014
|
+
ui_1.ui.success(`Process is running in background (PID: ${child.pid}).`);
|
|
1015
|
+
commandOutputs.push(`Command: ${cmdToRun}\nStarted in background and is running.`);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
catch (err) {
|
|
1019
|
+
ui_1.ui.error(`Failed to start background process:\n${err}`);
|
|
1020
|
+
commandOutputs.push(`Command: ${cmdToRun}\nFailed to start in background: ${err}`);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
else {
|
|
1024
|
+
ui_1.ui.step(`Running: ${cmdToRun}`);
|
|
1025
|
+
try {
|
|
1026
|
+
const outputData = await new Promise((resolve, reject) => {
|
|
1027
|
+
const { spawn } = require('child_process');
|
|
1028
|
+
const child = spawn(cmdToRun, {
|
|
1029
|
+
cwd: process.cwd(),
|
|
1030
|
+
shell: true,
|
|
1031
|
+
signal: exports.globalAbortController?.signal
|
|
1032
|
+
});
|
|
1033
|
+
let outputBuffer = "";
|
|
1034
|
+
child.stdout.on('data', (data) => {
|
|
1035
|
+
const str = data.toString();
|
|
1036
|
+
outputBuffer += str;
|
|
1037
|
+
process.stdout.write((0, kleur_1.dim)(str));
|
|
1038
|
+
});
|
|
1039
|
+
child.stderr.on('data', (data) => {
|
|
1040
|
+
const str = data.toString();
|
|
1041
|
+
outputBuffer += str;
|
|
1042
|
+
process.stderr.write((0, kleur_1.dim)(str));
|
|
1043
|
+
});
|
|
1044
|
+
child.on('close', (code) => {
|
|
1045
|
+
if (code === 0) {
|
|
1046
|
+
resolve(outputBuffer);
|
|
1047
|
+
}
|
|
1048
|
+
else {
|
|
1049
|
+
reject(outputBuffer || `Process exited with code ${code}`);
|
|
1050
|
+
}
|
|
1051
|
+
});
|
|
1052
|
+
child.on('error', (err) => {
|
|
1053
|
+
if (err.name === 'AbortError') {
|
|
1054
|
+
reject("Command aborted by user.");
|
|
1055
|
+
}
|
|
1056
|
+
else {
|
|
1057
|
+
reject(err.message);
|
|
1058
|
+
}
|
|
1059
|
+
});
|
|
1060
|
+
});
|
|
1061
|
+
commandOutputs.push(`Command: ${cmdToRun}\nOutput:\n${outputData}`);
|
|
1062
|
+
ui_1.ui.success(`Command executed successfully.`);
|
|
1063
|
+
}
|
|
1064
|
+
catch (err) {
|
|
1065
|
+
ui_1.ui.error(`Command failed:\n${err}`);
|
|
1066
|
+
commandOutputs.push(`Command: ${cmdToRun}\nFailed with error:\n${err}`);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
else {
|
|
1071
|
+
ui_1.ui.error(`Command execution denied: ${cmdToRun}`);
|
|
1072
|
+
commandOutputs.push(`Command: ${cmdToRun}\nUser denied execution.`);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
if (commandOutputs.length > 0 && !userAbortedEverything) {
|
|
1076
|
+
let promptAdditions = autoPrompt ? [autoPrompt] : [];
|
|
1077
|
+
promptAdditions.push(`I ran the commands you requested. Here are the results:\n${commandOutputs.join('\n\n')}`);
|
|
1078
|
+
autoPrompt = promptAdditions.join("\n\n");
|
|
1079
|
+
}
|
|
1080
|
+
else if (userAbortedEverything) {
|
|
1081
|
+
autoPrompt = ""; // Clear any queued prompts
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
catch (err) {
|
|
1086
|
+
if (spinner)
|
|
1087
|
+
spinner.stop();
|
|
1088
|
+
if (err.name === "AbortError" || err.message === "terminated") {
|
|
1089
|
+
ui_1.ui.info("Generation cancelled.");
|
|
1090
|
+
}
|
|
1091
|
+
else {
|
|
1092
|
+
ui_1.ui.error(err.message || String(err));
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
finally {
|
|
1096
|
+
exports.globalAbortController = null;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
};
|
|
1100
|
+
// Start the loop
|
|
1101
|
+
await loop();
|
|
1102
|
+
}
|