cli-atom 0.2.6 → 0.2.9
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/atom.js +233 -369
- package/package.json +3 -2
package/atom.js
CHANGED
|
@@ -6,12 +6,15 @@ import path from "path";
|
|
|
6
6
|
import { marked } from "marked";
|
|
7
7
|
import { markedTerminal } from "marked-terminal";
|
|
8
8
|
import { exec } from "child_process";
|
|
9
|
+
import { createRequire } from "module";
|
|
10
|
+
const _require = createRequire(import.meta.url);
|
|
9
11
|
|
|
10
12
|
marked.use(markedTerminal());
|
|
11
13
|
|
|
12
14
|
process.on("unhandledRejection", (err) => { console.error("unhandled rejection:", err); });
|
|
13
15
|
process.on("uncaughtException", (err) => { console.error("uncaught exception:", err); });
|
|
14
16
|
|
|
17
|
+
// ── THEME ────────────────────────────────────────────────────────────────────
|
|
15
18
|
const t = {
|
|
16
19
|
reset: "\x1b[0m",
|
|
17
20
|
bold: "\x1b[1m",
|
|
@@ -22,11 +25,11 @@ const t = {
|
|
|
22
25
|
muted: "\x1b[38;5;244m",
|
|
23
26
|
accent: "\x1b[38;5;255m",
|
|
24
27
|
subtle: "\x1b[38;5;238m",
|
|
25
|
-
violet: "\x1b[38;5;
|
|
26
|
-
violetDim: "\x1b[38;5;
|
|
28
|
+
violet: "\x1b[38;5;141m",
|
|
29
|
+
violetDim: "\x1b[38;5;60m",
|
|
27
30
|
ok: "\x1b[38;5;114m",
|
|
28
31
|
warn: "\x1b[38;5;179m",
|
|
29
|
-
err: "\x1b[38;5;
|
|
32
|
+
err: "\x1b[38;5;174m",
|
|
30
33
|
info: "\x1b[38;5;110m",
|
|
31
34
|
bgDark: "\x1b[48;5;234m",
|
|
32
35
|
bgMid: "\x1b[48;5;236m",
|
|
@@ -37,9 +40,13 @@ const IGNORE_EXTS = [".png", ".jpg", ".jpeg", ".gif", ".ico", ".svg", ".woff", "
|
|
|
37
40
|
".ttf", ".eot", ".mp3", ".mp4", ".zip", ".tar", ".gz",
|
|
38
41
|
".exe", ".dll", ".so", ".lock"];
|
|
39
42
|
const MAX_FILE_SIZE = 15_000;
|
|
43
|
+
|
|
44
|
+
const MAX_FILES = 15;
|
|
45
|
+
const MAX_CONTEXT_CHARS = 20_000;
|
|
46
|
+
|
|
40
47
|
let MODEL = "z-ai/glm-5-turbo";
|
|
41
48
|
let MODEL_LABEL = "glm-5-turbo";
|
|
42
|
-
const VERSION = "
|
|
49
|
+
const VERSION = _require("./package.json").version;
|
|
43
50
|
|
|
44
51
|
const MODELS = [
|
|
45
52
|
{ id: "z-ai/glm-5-turbo", label: "glm-5-turbo" },
|
|
@@ -49,17 +56,13 @@ const MODELS = [
|
|
|
49
56
|
{ id: "mistral-large", label: "mistral-large" }
|
|
50
57
|
];
|
|
51
58
|
|
|
52
|
-
// ───
|
|
53
|
-
|
|
59
|
+
// ─── CONFIG MANAGEMENT ───────────────────────────────────────────────────────
|
|
54
60
|
const CONFIG_DIR = path.join(os.homedir(), ".atom-cli");
|
|
55
61
|
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
56
62
|
|
|
57
63
|
function getConfig() {
|
|
58
|
-
try {
|
|
59
|
-
|
|
60
|
-
} catch {
|
|
61
|
-
return {};
|
|
62
|
-
}
|
|
64
|
+
try { return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8")); }
|
|
65
|
+
catch { return {}; }
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
function setConfig(data) {
|
|
@@ -72,47 +75,38 @@ function promptInput(question) {
|
|
|
72
75
|
return new Promise((resolve) => {
|
|
73
76
|
process.stdout.write(question);
|
|
74
77
|
process.stdin.setEncoding("utf8");
|
|
75
|
-
process.stdin.once("data", (data) =>
|
|
76
|
-
resolve(data.trim());
|
|
77
|
-
});
|
|
78
|
+
process.stdin.once("data", (data) => resolve(data.trim()));
|
|
78
79
|
});
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
// ─── CLI
|
|
82
|
-
|
|
82
|
+
// ─── CLI COMMANDS ────────────────────────────────────────────────────────────
|
|
83
83
|
const args = process.argv.slice(2);
|
|
84
84
|
|
|
85
85
|
if (args[0] === "login") {
|
|
86
|
-
console.log();
|
|
87
|
-
console.log(` ${t.
|
|
88
|
-
console.log();
|
|
89
|
-
console.log(` ${t.muted}Go to ${t.accent}https://puter.com${t.muted} and generate an API token.${t.reset}`);
|
|
90
|
-
console.log();
|
|
86
|
+
console.log(`\n ${t.violet}${t.bold}ATOM${t.reset} ${t.muted}login${t.reset}\n`);
|
|
87
|
+
console.log(` ${t.muted}Go to ${t.accent}https://puter.com${t.muted} and generate an API token.${t.reset}\n`);
|
|
91
88
|
const key = await promptInput(` ${t.violetDim}Paste your API key: ${t.reset}`);
|
|
92
89
|
if (!key) {
|
|
93
90
|
console.log(`\n ${t.err}No key provided. Aborting.${t.reset}\n`);
|
|
94
91
|
process.exit(1);
|
|
95
92
|
}
|
|
96
93
|
setConfig({ apiKey: key });
|
|
97
|
-
console.log(`\n ${t.ok}API key saved to ${t.dim}${CONFIG_FILE}${t.reset}`);
|
|
94
|
+
console.log(`\n ${t.ok}✓ API key saved to ${t.dim}${CONFIG_FILE}${t.reset}`);
|
|
98
95
|
console.log(` ${t.muted}You can now run ${t.accent}atom${t.muted} to start coding!${t.reset}\n`);
|
|
99
96
|
process.exit(0);
|
|
100
97
|
}
|
|
101
98
|
|
|
102
99
|
if (args[0] === "logout") {
|
|
103
100
|
try { fs.unlinkSync(CONFIG_FILE); } catch { }
|
|
104
|
-
console.log(`\n ${t.ok}Logged out. API key removed.${t.reset}\n`);
|
|
101
|
+
console.log(`\n ${t.ok}✓ Logged out. API key removed.${t.reset}\n`);
|
|
105
102
|
process.exit(0);
|
|
106
103
|
}
|
|
107
104
|
|
|
108
|
-
// ───
|
|
109
|
-
|
|
105
|
+
// ─── PUTER INIT ──────────────────────────────────────────────────────────────
|
|
110
106
|
let config = getConfig();
|
|
111
107
|
if (!config.apiKey) {
|
|
112
|
-
console.log();
|
|
113
|
-
console.log(` ${t.
|
|
114
|
-
console.log(` ${t.muted}Run ${t.accent}atom login${t.muted} or type ${t.accent}/login${t.muted} after starting atom.${t.reset}`);
|
|
115
|
-
console.log();
|
|
108
|
+
console.log(`\n ${t.err}✗ No API key found.${t.reset}`);
|
|
109
|
+
console.log(` ${t.muted}Run ${t.accent}atom login${t.muted} to authenticate.${t.reset}\n`);
|
|
116
110
|
process.exit(1);
|
|
117
111
|
}
|
|
118
112
|
|
|
@@ -123,64 +117,69 @@ let promptCount = 0;
|
|
|
123
117
|
let filesCreated = 0;
|
|
124
118
|
let filesEdited = 0;
|
|
125
119
|
const sessionStart = Date.now();
|
|
126
|
-
const systemPrompt = `You are an autonomous AI coding agent. You have FULL VISIBILITY of the user's project files (provided below as context).
|
|
127
|
-
Analyze the existing code structure before deciding what to do. Be smart: if a file exists, EDIT it. If it doesn't, create it with FILE.
|
|
128
120
|
|
|
129
|
-
|
|
130
|
-
|
|
121
|
+
const systemPrompt = `You are an autonomous AI coding agent with FULL VISIBILITY of the user's project files.
|
|
122
|
+
|
|
123
|
+
# MISSION
|
|
124
|
+
Analyze the existing codebase, understand its conventions, then make precise, minimal, and correct changes to fulfill the user's request.
|
|
131
125
|
|
|
126
|
+
# CORE PRINCIPLES
|
|
127
|
+
1. **Read before write.** Always analyze existing structure, imports, naming conventions, and patterns before modifying anything.
|
|
128
|
+
2. **Edit over create.** If a file already exists, EDIT it. Only create files that are genuinely new.
|
|
129
|
+
3. **Always use folders.** When creating new files, NEVER place them directly in the project root. ALWAYS create a dedicated, logically named folder (module/directory) and put the new files inside it. Group related code logically into these subdirectories.
|
|
130
|
+
4. **Minimal diff.** Change only what is necessary. Preserve existing style, indentation, and formatting.
|
|
131
|
+
5. **Atomic edits.** The "search" string must be unique enough to match exactly ONE location. Include surrounding context lines if needed to disambiguate.
|
|
132
|
+
6. **No placeholders.** Never use "// ... rest of code", "// existing code", or "/* unchanged */". Always provide complete, runnable content.
|
|
133
|
+
7. **Strict escaping.** All strings inside the JSON must be properly escaped (\\n, \\t, \\", \\\\, etc.). Pay special attention to backticks, template literals, and regex.
|
|
134
|
+
|
|
135
|
+
# OUTPUT FORMAT
|
|
136
|
+
You MUST respond EXCLUSIVELY with a single valid JSON object.
|
|
137
|
+
NO markdown fences, NO commentary before or after, NO trailing text, NO trailing commas.
|
|
138
|
+
|
|
139
|
+
Schema:
|
|
132
140
|
{
|
|
133
|
-
"message": "
|
|
141
|
+
"message": "Concise summary of what you did and why (optional)",
|
|
134
142
|
"filesToCreate": [
|
|
135
|
-
{
|
|
136
|
-
"path": "path/to/new_file.ext",
|
|
137
|
-
"content": "Full content of the new file"
|
|
138
|
-
}
|
|
143
|
+
{ "path": "folder_name/sub_folder/new_file.ext", "content": "Full file content" }
|
|
139
144
|
],
|
|
140
145
|
"filesToEdit": [
|
|
141
|
-
{
|
|
142
|
-
"path": "path/to/existing_file.ext",
|
|
143
|
-
"search": "exact lines to find in the file",
|
|
144
|
-
"replace": "new lines to put in their place"
|
|
145
|
-
}
|
|
146
|
+
{ "path": "existing_folder/existing_file.ext", "search": "Exact lines to locate", "replace": "New lines to substitute" }
|
|
146
147
|
],
|
|
147
|
-
"filesToDelete": [
|
|
148
|
-
|
|
149
|
-
],
|
|
150
|
-
"commandsToRun": [
|
|
151
|
-
"npm test",
|
|
152
|
-
"node script.js"
|
|
153
|
-
]
|
|
148
|
+
"filesToDelete": [ "folder_name/file_or_folder" ],
|
|
149
|
+
"commandsToRun": [ "run_native_test_or_build_command" ]
|
|
154
150
|
}
|
|
155
151
|
|
|
156
|
-
|
|
157
|
-
-
|
|
158
|
-
-
|
|
159
|
-
-
|
|
152
|
+
Rules:
|
|
153
|
+
- Omit unused arrays or leave them as [].
|
|
154
|
+
- Paths are relative to the project root, use forward slashes, and MUST always include a parent folder. Never output a path with just a filename (e.g., NEVER just "script.js", always "utils/script.js").
|
|
155
|
+
- "search" must match existing content byte-for-byte (whitespace and indentation included).
|
|
156
|
+
- One file may appear in multiple edit entries if several distinct changes are needed.
|
|
157
|
+
- "commandsToRun": Dynamically detect the project's language and ecosystem from its config files (e.g., package.json, requirements.txt, Cargo.toml, go.mod, pom.xml, composer.json, Makefile, CMakeLists.txt, etc.). Run the standard verification commands for that specific ecosystem (e.g., tests, build, lint). NEVER include destructive commands (like rm -rf, dropdb, format, etc.).
|
|
158
|
+
|
|
159
|
+
# CODING STANDARDS
|
|
160
|
+
- Match the project's existing language version, framework, and conventions.
|
|
161
|
+
- Prefer standard libraries over new dependencies. If a new dependency is required, mention it in "message".
|
|
162
|
+
- Write clean, typed (where applicable), readable, and testable code.
|
|
163
|
+
- Keep functions small and focused. No dead code, no commented-out blocks.
|
|
164
|
+
- Handle errors gracefully. Never swallow exceptions silently.
|
|
165
|
+
- Respect SOLID principles and the project's architectural layering.
|
|
166
|
+
|
|
167
|
+
# CONSTRAINTS
|
|
168
|
+
- Never invent file paths or assume content you cannot see.
|
|
169
|
+
- Never modify lockfiles, .env, or CI secrets unless explicitly asked.
|
|
170
|
+
- If the request is ambiguous, make the most reasonable assumption and explain it briefly in "message".
|
|
171
|
+
- If the request is impossible or unsafe, return an empty JSON with an explanation in "message".`;
|
|
160
172
|
|
|
161
173
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
162
174
|
|
|
163
|
-
function cols() {
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function rule(char = "─", color = t.violetDim) {
|
|
168
|
-
return `${color}${char.repeat(cols())}${t.reset}`;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function pad(str, width) {
|
|
172
|
-
const visible = str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
173
|
-
return str + " ".repeat(Math.max(0, width - visible.length));
|
|
174
|
-
}
|
|
175
|
-
|
|
175
|
+
function cols() { return process.stdout.columns || 80; }
|
|
176
|
+
function rule(char = "─", color = t.violetDim) { return `${color}${char.repeat(cols())}${t.reset}`; }
|
|
176
177
|
function elapsed() {
|
|
177
178
|
const s = Math.floor((Date.now() - sessionStart) / 1000);
|
|
178
179
|
return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`;
|
|
179
180
|
}
|
|
180
181
|
|
|
181
|
-
|
|
182
|
-
const MAX_CONTEXT_CHARS = 60_000;
|
|
183
|
-
|
|
182
|
+
// ─── FILE SCANNER ────────────────────────────────────────────────────────────
|
|
184
183
|
function scanDir(dirPath, prefix = "", _count = { n: 0 }) {
|
|
185
184
|
let tree = "";
|
|
186
185
|
let files = [];
|
|
@@ -201,19 +200,16 @@ function scanDir(dirPath, prefix = "", _count = { n: 0 }) {
|
|
|
201
200
|
tree += `${prefix}${entry.name}\n`;
|
|
202
201
|
try {
|
|
203
202
|
const stat = fs.statSync(fullPath);
|
|
204
|
-
const content = stat.size <= MAX_FILE_SIZE
|
|
205
|
-
? fs.readFileSync(fullPath, "utf-8")
|
|
206
|
-
: "[file too large — truncated]";
|
|
203
|
+
const content = stat.size <= MAX_FILE_SIZE ? fs.readFileSync(fullPath, "utf-8") : "[file too large]";
|
|
207
204
|
files.push({ path: fullPath.replace(/\\/g, "/"), content });
|
|
208
205
|
_count.n++;
|
|
209
|
-
} catch { /* skip
|
|
206
|
+
} catch { /* skip */ }
|
|
210
207
|
}
|
|
211
208
|
}
|
|
212
|
-
} catch { /* skip
|
|
209
|
+
} catch { /* skip */ }
|
|
213
210
|
return { tree, files };
|
|
214
211
|
}
|
|
215
212
|
|
|
216
|
-
|
|
217
213
|
let _contextCache = null;
|
|
218
214
|
let _contextSnapshot = null;
|
|
219
215
|
|
|
@@ -228,25 +224,39 @@ function getSnapshot(files) {
|
|
|
228
224
|
function snapshotChanged(files, snap) {
|
|
229
225
|
if (!snap) return true;
|
|
230
226
|
for (const f of files) {
|
|
231
|
-
try {
|
|
232
|
-
|
|
233
|
-
} catch { return true; }
|
|
227
|
+
try { if (fs.statSync(f.path).mtimeMs !== snap[f.path]) return true; }
|
|
228
|
+
catch { return true; }
|
|
234
229
|
}
|
|
235
230
|
return Object.keys(snap).length !== files.length;
|
|
236
231
|
}
|
|
237
232
|
|
|
238
|
-
function buildContext() {
|
|
233
|
+
function buildContext(userPrompt = "") {
|
|
239
234
|
const cwd = process.cwd();
|
|
240
235
|
const { tree, files } = scanDir(cwd);
|
|
236
|
+
const unchanged = !snapshotChanged(files, _contextSnapshot);
|
|
241
237
|
|
|
242
|
-
|
|
243
|
-
|
|
238
|
+
let ctx = `\n--- PROJECT CONTEXT (${cwd}) ---\nFile tree:\n${tree}\n`;
|
|
239
|
+
|
|
240
|
+
if (unchanged && _contextCache) {
|
|
241
|
+
ctx += `[Files unchanged since last request — use file tree above]\n--- END PROJECT CONTEXT ---\n`;
|
|
242
|
+
return ctx;
|
|
244
243
|
}
|
|
245
244
|
|
|
246
|
-
|
|
247
|
-
ctx += `File tree:\n${tree}\n`;
|
|
245
|
+
const keywords = userPrompt.toLowerCase().split(/\W+/).filter(w => w.length > 3);
|
|
248
246
|
let totalChars = ctx.length;
|
|
249
|
-
|
|
247
|
+
let included = 0;
|
|
248
|
+
|
|
249
|
+
const sorted = [...files].sort((a, b) => {
|
|
250
|
+
const relA = path.relative(cwd, a.path).toLowerCase();
|
|
251
|
+
const relB = path.relative(cwd, b.path).toLowerCase();
|
|
252
|
+
const scoreA = keywords.filter(k => relA.includes(k)).length;
|
|
253
|
+
const scoreB = keywords.filter(k => relB.includes(k)).length;
|
|
254
|
+
if (scoreB !== scoreA) return scoreB - scoreA;
|
|
255
|
+
return a.content.length - b.content.length;
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
for (const f of sorted) {
|
|
259
|
+
if (included >= MAX_FILES) break;
|
|
250
260
|
const rel = path.relative(cwd, f.path).replace(/\\/g, "/");
|
|
251
261
|
const block = `--- ${rel} ---\n${f.content}\n--- end ${rel} ---\n\n`;
|
|
252
262
|
if (totalChars + block.length > MAX_CONTEXT_CHARS) {
|
|
@@ -255,9 +265,10 @@ function buildContext() {
|
|
|
255
265
|
}
|
|
256
266
|
ctx += block;
|
|
257
267
|
totalChars += block.length;
|
|
268
|
+
included++;
|
|
258
269
|
}
|
|
259
|
-
ctx += `--- END PROJECT CONTEXT ---\n`;
|
|
260
270
|
|
|
271
|
+
ctx += `--- END PROJECT CONTEXT ---\n`;
|
|
261
272
|
_contextCache = ctx;
|
|
262
273
|
_contextSnapshot = getSnapshot(files);
|
|
263
274
|
return ctx;
|
|
@@ -273,12 +284,7 @@ function runCommand(cmd, cwd) {
|
|
|
273
284
|
});
|
|
274
285
|
}
|
|
275
286
|
|
|
276
|
-
|
|
277
|
-
(_, i, a) => {
|
|
278
|
-
const bar = a.map((__, j) => (j <= i ? "▪" : "·")).join("");
|
|
279
|
-
return bar;
|
|
280
|
-
}
|
|
281
|
-
);
|
|
287
|
+
// ─── UI COMPONENTS ───────────────────────────────────────────────────────────
|
|
282
288
|
const SPINNER_CHARS = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
283
289
|
|
|
284
290
|
function startSpinner(label) {
|
|
@@ -286,9 +292,7 @@ function startSpinner(label) {
|
|
|
286
292
|
const timer = setInterval(() => {
|
|
287
293
|
process.stdout.clearLine(0);
|
|
288
294
|
process.stdout.cursorTo(0);
|
|
289
|
-
process.stdout.write(
|
|
290
|
-
` ${t.muted}${SPINNER_CHARS[i % SPINNER_CHARS.length]} ${label}${t.reset}`
|
|
291
|
-
);
|
|
295
|
+
process.stdout.write(` ${t.violet}${SPINNER_CHARS[i % SPINNER_CHARS.length]}${t.reset} ${t.muted}${label}${t.reset}`);
|
|
292
296
|
i++;
|
|
293
297
|
}, 80);
|
|
294
298
|
return timer;
|
|
@@ -301,43 +305,39 @@ function stopSpinner(timer, msg) {
|
|
|
301
305
|
if (msg) console.log(msg);
|
|
302
306
|
}
|
|
303
307
|
|
|
304
|
-
|
|
305
308
|
function sectionHeader(label) {
|
|
306
|
-
console.log();
|
|
307
|
-
console.log(
|
|
308
|
-
console.log(`${t.violetDim}${"─".repeat(cols())}${t.reset}`);
|
|
309
|
+
console.log(`\n ${t.violet}${t.bold}${label.toUpperCase()}${t.reset}`);
|
|
310
|
+
console.log(` ${t.violetDim}${"─".repeat(4)}${t.reset}`);
|
|
309
311
|
}
|
|
310
312
|
|
|
311
313
|
async function showCreatedFile(filePath, content) {
|
|
312
314
|
const lines = content.split("\n");
|
|
313
315
|
const relPath = path.relative(process.cwd(), filePath).replace(/\\/g, "/");
|
|
314
|
-
console.log(` ${t.ok}
|
|
315
|
-
await sleep(60);
|
|
316
|
+
console.log(` ${t.ok}✓${t.reset} ${t.bold}${relPath}${t.reset} ${t.muted}(${lines.length} lines)${t.reset}`);
|
|
316
317
|
|
|
317
|
-
const preview = lines.slice(0,
|
|
318
|
+
const preview = lines.slice(0, 6);
|
|
318
319
|
for (let i = 0; i < preview.length; i++) {
|
|
319
320
|
const num = String(i + 1).padStart(3, " ");
|
|
320
321
|
console.log(` ${t.subtle}${num}${t.reset} ${t.dim}${preview[i]}${t.reset}`);
|
|
321
|
-
await sleep(
|
|
322
|
+
await sleep(10);
|
|
322
323
|
}
|
|
323
|
-
if (lines.length >
|
|
324
|
-
console.log(` ${t.subtle}
|
|
324
|
+
if (lines.length > 6) {
|
|
325
|
+
console.log(` ${t.subtle} ...${t.reset} ${t.muted}${lines.length - 6} more lines${t.reset}`);
|
|
325
326
|
}
|
|
326
327
|
console.log();
|
|
327
328
|
}
|
|
328
329
|
|
|
329
330
|
async function showDiff(filePath, searchBlock, replaceBlock) {
|
|
330
331
|
const relPath = path.relative(process.cwd(), filePath).replace(/\\/g, "/");
|
|
331
|
-
console.log(` ${t.warn}~${t.reset} ${t.
|
|
332
|
-
await sleep(60);
|
|
332
|
+
console.log(` ${t.warn}~${t.reset} ${t.bold}${relPath}${t.reset}`);
|
|
333
333
|
|
|
334
334
|
for (const line of searchBlock.split("\n")) {
|
|
335
|
-
console.log(` ${t.err}-
|
|
336
|
-
await sleep(
|
|
335
|
+
console.log(` ${t.err}- ${t.dim}${line}${t.reset}`);
|
|
336
|
+
await sleep(15);
|
|
337
337
|
}
|
|
338
338
|
for (const line of replaceBlock.split("\n")) {
|
|
339
|
-
console.log(` ${t.ok}+
|
|
340
|
-
await sleep(
|
|
339
|
+
console.log(` ${t.ok}+ ${line}${t.reset}`);
|
|
340
|
+
await sleep(15);
|
|
341
341
|
}
|
|
342
342
|
console.log();
|
|
343
343
|
}
|
|
@@ -346,120 +346,93 @@ function printStatus(icon, color, msg) {
|
|
|
346
346
|
console.log(` ${color}${icon}${t.reset} ${msg}`);
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
-
// ───
|
|
349
|
+
// ─── SLASH COMMANDS ───────────────────────────────────────────────────────────
|
|
350
350
|
const SLASH_COMMANDS = [
|
|
351
351
|
{ name: "help", desc: "Show all available commands" },
|
|
352
352
|
{ name: "model", desc: "List or switch AI model" },
|
|
353
353
|
{ name: "key", desc: "View your current API key" },
|
|
354
354
|
{ name: "login", desc: "Set a new API key" },
|
|
355
355
|
{ name: "logout", desc: "Remove your API key" },
|
|
356
|
-
{ name: "
|
|
356
|
+
{ name: "exit", desc: "Exit the CLI" },
|
|
357
357
|
];
|
|
358
358
|
|
|
359
|
+
// ─── INTERACTIVE PROMPT (FIXED & ROBUST) ─────────────────────────────────────
|
|
359
360
|
function askPrompt() {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
const sessionStats = `${filesCreated} created ${filesEdited} edited`;
|
|
364
|
-
const left = `${t.subtle}${sessionStats}${t.reset}`;
|
|
361
|
+
const sessionStats = `${t.ok}${filesCreated}${t.reset} created ${t.warn}${filesEdited}${t.reset} edited`;
|
|
362
|
+
const left = `\n ${t.subtle}${sessionStats}${t.reset}`;
|
|
365
363
|
const right = `${t.muted}${MODEL_LABEL} ${promptCount} req ${elapsed()}${t.reset}`;
|
|
366
364
|
const rightVisible = right.replace(/\x1b\[[0-9;]*m/g, "");
|
|
367
|
-
const gap = cols()
|
|
368
|
-
- left.replace(/\x1b\[[0-9;]*m/g, "").length
|
|
369
|
-
- rightVisible.length;
|
|
365
|
+
const gap = cols() - left.replace(/\x1b\[[0-9;]*m/g, "").length - rightVisible.length;
|
|
370
366
|
console.log(left + " ".repeat(Math.max(0, gap)) + right);
|
|
371
|
-
console.log();
|
|
372
367
|
|
|
373
368
|
let input = "";
|
|
374
369
|
let menuActive = false;
|
|
375
370
|
let menuIndex = 0;
|
|
376
371
|
let menuItems = [];
|
|
377
|
-
let
|
|
378
|
-
|
|
379
|
-
const BG = "\x1b[48;5;238m";
|
|
380
|
-
const FG = "\x1b[38;5;255m";
|
|
381
|
-
const FGP = "\x1b[38;5;245m";
|
|
382
|
-
const PAD = " ";
|
|
383
|
-
|
|
384
|
-
function drawBox(text, isPlaceholder) {
|
|
385
|
-
const currentCols = cols();
|
|
386
|
-
const fg = isPlaceholder ? FGP : FG;
|
|
387
|
-
let displayStr = text;
|
|
388
|
-
const maxLen = Math.max(10, currentCols - PAD.length - 2);
|
|
389
|
-
if (!isPlaceholder && displayStr.length > maxLen) {
|
|
390
|
-
displayStr = "…" + displayStr.slice(displayStr.length - maxLen + 1);
|
|
391
|
-
}
|
|
392
|
-
const content = PAD + displayStr;
|
|
393
|
-
const fill = " ".repeat(Math.max(0, currentCols - content.length));
|
|
394
|
-
const emptyRow = BG + " ".repeat(currentCols) + "\x1b[0m";
|
|
395
|
-
const textRow = BG + fg + content + fill + "\x1b[0m";
|
|
396
|
-
return emptyRow + "\n" + textRow + "\n" + emptyRow + "\n";
|
|
397
|
-
}
|
|
372
|
+
let lastMenuLines = 0;
|
|
398
373
|
|
|
399
|
-
function
|
|
400
|
-
|
|
401
|
-
|
|
374
|
+
function clearPromptAndMenu() {
|
|
375
|
+
process.stdout.write("\x1b[2K\r"); // Efface la ligne de prompt
|
|
376
|
+
if (lastMenuLines > 0) {
|
|
377
|
+
for (let i = 0; i < lastMenuLines; i++) {
|
|
378
|
+
process.stdout.write("\x1b[1B\x1b[2K"); // Descend et efface
|
|
379
|
+
}
|
|
380
|
+
for (let i = 0; i < lastMenuLines; i++) {
|
|
381
|
+
process.stdout.write("\x1b[1A"); // Remonte
|
|
382
|
+
}
|
|
383
|
+
lastMenuLines = 0;
|
|
384
|
+
}
|
|
402
385
|
}
|
|
403
386
|
|
|
404
|
-
function
|
|
405
|
-
|
|
406
|
-
menuItems = getFilteredCommands();
|
|
387
|
+
function drawPrompt() {
|
|
388
|
+
clearPromptAndMenu();
|
|
407
389
|
|
|
408
|
-
|
|
409
|
-
|
|
390
|
+
const prefix = ` ${t.violet}❯${t.reset} `;
|
|
391
|
+
const visiblePrefixLen = 4;
|
|
410
392
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
if (menuItems.length === 0) {
|
|
419
|
-
menuLineCount = 0;
|
|
420
|
-
return;
|
|
393
|
+
if (input.length === 0) {
|
|
394
|
+
process.stdout.write(`${prefix}${t.muted}Insert your instruction... (type / for commands)${t.reset}`);
|
|
395
|
+
} else {
|
|
396
|
+
process.stdout.write(`${prefix}${t.accent}${input}${t.reset}`);
|
|
421
397
|
}
|
|
422
398
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
399
|
+
let currentMenuLines = 0;
|
|
400
|
+
if (menuActive) {
|
|
401
|
+
const query = input.slice(1).toLowerCase();
|
|
402
|
+
menuItems = SLASH_COMMANDS.filter(c => c.name.startsWith(query));
|
|
403
|
+
if (menuIndex >= menuItems.length) menuIndex = 0;
|
|
404
|
+
|
|
405
|
+
for (let i = 0; i < menuItems.length; i++) {
|
|
406
|
+
const item = menuItems[i];
|
|
407
|
+
const isSelected = i === menuIndex;
|
|
408
|
+
const namePad = ("/" + item.name).padEnd(12);
|
|
409
|
+
process.stdout.write(`\n`);
|
|
410
|
+
if (isSelected) {
|
|
411
|
+
process.stdout.write(` ${t.violet}${t.bold}❯ ${namePad}${t.reset} ${t.accent}${item.desc}${t.reset}`);
|
|
412
|
+
} else {
|
|
413
|
+
process.stdout.write(` ${t.violetDim}${namePad}${t.reset} ${t.muted}${item.desc}${t.reset}`);
|
|
414
|
+
}
|
|
415
|
+
currentMenuLines++;
|
|
433
416
|
}
|
|
434
|
-
out += " ".repeat(Math.max(0, currentCols - namePad.length - item.desc.length - 4)) + "\n";
|
|
435
417
|
}
|
|
436
418
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
process.stdout.write(out);
|
|
442
|
-
}
|
|
419
|
+
for (let i = 0; i < currentMenuLines; i++) {
|
|
420
|
+
process.stdout.write("\x1b[1A");
|
|
421
|
+
}
|
|
443
422
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
process.stdout.write("\x1b[1A\x1b[2K");
|
|
448
|
-
}
|
|
449
|
-
menuLineCount = 0;
|
|
423
|
+
if (currentMenuLines > 0) {
|
|
424
|
+
const col = visiblePrefixLen + input.length + 1;
|
|
425
|
+
process.stdout.write(`\x1b[${col}G`);
|
|
450
426
|
}
|
|
451
|
-
}
|
|
452
427
|
|
|
453
|
-
|
|
454
|
-
process.stdout.write("\x1b[3A\r" + drawBox(input || "", input.length === 0));
|
|
455
|
-
if (menuActive) renderMenu();
|
|
428
|
+
lastMenuLines = currentMenuLines;
|
|
456
429
|
}
|
|
457
430
|
|
|
458
|
-
function onResize() {
|
|
431
|
+
function onResize() { drawPrompt(); }
|
|
459
432
|
process.stdout.on("resize", onResize);
|
|
460
433
|
|
|
461
|
-
process.stdout.write(
|
|
462
|
-
|
|
434
|
+
process.stdout.write("\x1b[?25h");
|
|
435
|
+
drawPrompt();
|
|
463
436
|
|
|
464
437
|
process.stdin.setRawMode(true);
|
|
465
438
|
process.stdin.resume();
|
|
@@ -470,57 +443,44 @@ function askPrompt() {
|
|
|
470
443
|
process.stdin.pause();
|
|
471
444
|
process.stdin.removeListener("data", onData);
|
|
472
445
|
process.stdout.removeListener("resize", onResize);
|
|
473
|
-
process.stdout.write("\x1b[?25h");
|
|
474
446
|
}
|
|
475
447
|
|
|
476
448
|
function submitInput(value) {
|
|
477
|
-
|
|
478
|
-
|
|
449
|
+
clearPromptAndMenu();
|
|
450
|
+
console.log(` ${t.violet}❯${t.reset} ${t.accent}${value}${t.reset}`);
|
|
479
451
|
handleInput(value);
|
|
480
452
|
}
|
|
481
453
|
|
|
482
454
|
function onData(key) {
|
|
483
|
-
|
|
484
|
-
if (key === "\u0003") { process.stdout.write("\x1b[?25h"); process.exit(); }
|
|
455
|
+
if (key === "\u0003") { process.exit(); }
|
|
485
456
|
|
|
486
|
-
// Arrow up
|
|
487
457
|
if (key === "\x1b[A") {
|
|
488
458
|
if (menuActive && menuItems.length > 0) {
|
|
489
459
|
menuIndex = (menuIndex - 1 + menuItems.length) % menuItems.length;
|
|
490
|
-
|
|
460
|
+
drawPrompt();
|
|
491
461
|
}
|
|
492
462
|
return;
|
|
493
463
|
}
|
|
494
464
|
|
|
495
|
-
// Arrow down
|
|
496
465
|
if (key === "\x1b[B") {
|
|
497
466
|
if (menuActive && menuItems.length > 0) {
|
|
498
467
|
menuIndex = (menuIndex + 1) % menuItems.length;
|
|
499
|
-
|
|
468
|
+
drawPrompt();
|
|
500
469
|
}
|
|
501
470
|
return;
|
|
502
471
|
}
|
|
503
472
|
|
|
504
|
-
// Tab — autocomplete selected menu item
|
|
505
473
|
if (key === "\t") {
|
|
506
474
|
if (menuActive && menuItems.length > 0) {
|
|
507
475
|
input = "/" + menuItems[menuIndex].name;
|
|
508
|
-
|
|
509
|
-
renderMenu();
|
|
476
|
+
drawPrompt();
|
|
510
477
|
}
|
|
511
478
|
return;
|
|
512
479
|
}
|
|
513
480
|
|
|
514
|
-
// Enter
|
|
515
481
|
if (key === "\r" || key === "\n") {
|
|
516
482
|
if (menuActive && menuItems.length > 0) {
|
|
517
|
-
|
|
518
|
-
const selected = "/" + menuItems[menuIndex].name;
|
|
519
|
-
cleanup();
|
|
520
|
-
clearMenu();
|
|
521
|
-
process.stdout.write("\x1b[3A\x1b[2K\x1b[1B\x1b[2K\x1b[1B\x1b[2K\x1b[1A\r");
|
|
522
|
-
handleInput(selected);
|
|
523
|
-
return;
|
|
483
|
+
input = "/" + menuItems[menuIndex].name;
|
|
524
484
|
}
|
|
525
485
|
if (!input.trim()) return;
|
|
526
486
|
cleanup();
|
|
@@ -528,101 +488,56 @@ function askPrompt() {
|
|
|
528
488
|
return;
|
|
529
489
|
}
|
|
530
490
|
|
|
531
|
-
// Escape — close menu
|
|
532
491
|
if (key === "\x1b") {
|
|
533
492
|
if (menuActive) {
|
|
534
493
|
menuActive = false;
|
|
535
|
-
|
|
536
|
-
process.stdout.write("\x1b[3A\r" + drawBox(input, false));
|
|
494
|
+
drawPrompt();
|
|
537
495
|
}
|
|
538
496
|
return;
|
|
539
497
|
}
|
|
540
498
|
|
|
541
|
-
// Backspace
|
|
542
499
|
if (key === "\x7f" || key === "\b") {
|
|
543
|
-
input
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
clearMenu();
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
if (input.startsWith("/")) {
|
|
551
|
-
menuActive = true;
|
|
552
|
-
menuIndex = 0;
|
|
553
|
-
process.stdout.write("\x1b[3A\r" + drawBox(input, false));
|
|
554
|
-
renderMenu();
|
|
555
|
-
} else {
|
|
556
|
-
menuActive = false;
|
|
557
|
-
clearMenu();
|
|
558
|
-
process.stdout.write("\x1b[3A\r" + drawBox(input || "", input.length === 0));
|
|
500
|
+
if (input.length > 0) {
|
|
501
|
+
input = input.slice(0, -1);
|
|
502
|
+
menuActive = input.startsWith("/");
|
|
503
|
+
drawPrompt();
|
|
559
504
|
}
|
|
560
505
|
return;
|
|
561
506
|
}
|
|
562
507
|
|
|
563
|
-
// Printable chars
|
|
564
508
|
if (key.charCodeAt(0) >= 32) {
|
|
565
509
|
input += key;
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
menuActive = true;
|
|
570
|
-
menuIndex = 0;
|
|
571
|
-
process.stdout.write("\x1b[3A\r" + drawBox(input, false));
|
|
572
|
-
renderMenu();
|
|
573
|
-
return;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
// filter menu as user types
|
|
577
|
-
if (input.startsWith("/")) {
|
|
578
|
-
menuActive = true;
|
|
579
|
-
menuIndex = 0;
|
|
580
|
-
process.stdout.write("\x1b[3A\r" + drawBox(input, false));
|
|
581
|
-
renderMenu();
|
|
582
|
-
return;
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
// normal input
|
|
586
|
-
menuActive = false;
|
|
587
|
-
clearMenu();
|
|
588
|
-
process.stdout.write("\x1b[3A\r" + drawBox(input, false));
|
|
510
|
+
menuActive = input.startsWith("/");
|
|
511
|
+
menuIndex = 0;
|
|
512
|
+
drawPrompt();
|
|
589
513
|
}
|
|
590
514
|
}
|
|
591
515
|
|
|
592
516
|
process.stdin.on("data", onData);
|
|
593
517
|
}
|
|
594
518
|
|
|
519
|
+
// ─── INPUT HANDLER ───────────────────────────────────────────────────────────
|
|
595
520
|
async function handleInput(raw) {
|
|
596
521
|
const userPrompt = raw.trim();
|
|
597
522
|
|
|
598
523
|
if (!userPrompt) { askPrompt(); return; }
|
|
599
524
|
|
|
600
525
|
if (userPrompt.toLowerCase() === "exit" || userPrompt.toLowerCase() === "quit") {
|
|
601
|
-
console.log();
|
|
602
|
-
console.log(` ${t.muted}session ended ${filesCreated} created ${filesEdited} edited ${elapsed()}${t.reset}`);
|
|
603
|
-
console.log();
|
|
526
|
+
console.log(`\n ${t.muted}Session ended ${filesCreated} created ${filesEdited} edited ${elapsed()}${t.reset}\n`);
|
|
604
527
|
process.exit(0);
|
|
605
528
|
return;
|
|
606
529
|
}
|
|
607
530
|
|
|
608
|
-
// ─── /help ───────────────────────────────────────────────────────────────
|
|
609
531
|
if (userPrompt.toLowerCase() === "/help") {
|
|
610
|
-
console.log();
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
console.log(` ${t.violetDim}/model${t.reset} ${t.muted}list available AI models${t.reset}`);
|
|
615
|
-
console.log(` ${t.violetDim}/model <name>${t.reset} ${t.muted}switch to a specific model${t.reset}`);
|
|
616
|
-
console.log(` ${t.violetDim}/key${t.reset} ${t.muted}view your current API key${t.reset}`);
|
|
617
|
-
console.log(` ${t.violetDim}/login${t.reset} ${t.muted}set a new API key${t.reset}`);
|
|
618
|
-
console.log(` ${t.violetDim}/logout${t.reset} ${t.muted}remove your API key${t.reset}`);
|
|
619
|
-
console.log(` ${t.violetDim}exit / quit${t.reset} ${t.muted}end the session${t.reset}`);
|
|
532
|
+
console.log(`\n ${t.violet}${t.bold}Available commands${t.reset}`);
|
|
533
|
+
SLASH_COMMANDS.forEach(c => {
|
|
534
|
+
console.log(` ${t.violetDim}/${c.name.padEnd(10)}${t.reset} ${t.muted}${c.desc}${t.reset}`);
|
|
535
|
+
});
|
|
620
536
|
console.log();
|
|
621
537
|
askPrompt();
|
|
622
538
|
return;
|
|
623
539
|
}
|
|
624
540
|
|
|
625
|
-
// ─── /model ──────────────────────────────────────────────────────────────
|
|
626
541
|
if (userPrompt.toLowerCase().startsWith("/model")) {
|
|
627
542
|
const parts = userPrompt.split(" ");
|
|
628
543
|
if (parts.length === 1) {
|
|
@@ -641,65 +556,63 @@ async function handleInput(raw) {
|
|
|
641
556
|
if (selected) {
|
|
642
557
|
MODEL = selected.id;
|
|
643
558
|
MODEL_LABEL = selected.label;
|
|
644
|
-
console.log(`\n ${t.ok}Model switched to ${t.bold}${MODEL_LABEL}${t.reset}\n`);
|
|
559
|
+
console.log(`\n ${t.ok}✓ Model switched to ${t.bold}${MODEL_LABEL}${t.reset}\n`);
|
|
645
560
|
} else {
|
|
646
|
-
console.log(`\n ${t.err}Model not found. Type '/model' to see the list.${t.reset}\n`);
|
|
561
|
+
console.log(`\n ${t.err}✗ Model not found. Type '/model' to see the list.${t.reset}\n`);
|
|
647
562
|
}
|
|
648
563
|
askPrompt();
|
|
649
564
|
return;
|
|
650
565
|
}
|
|
651
566
|
|
|
652
|
-
// ─── /key ────────────────────────────────────────────────────────────────
|
|
653
567
|
if (userPrompt.toLowerCase() === "/key") {
|
|
654
568
|
const masked = config.apiKey ? config.apiKey.slice(0, 10) + "..." + config.apiKey.slice(-6) : "none";
|
|
655
|
-
console.log(`\n ${t.violet}API Key${t.reset}`);
|
|
656
|
-
console.log(` ${t.muted}current: ${masked}${t.reset}`);
|
|
657
|
-
console.log(` ${t.dim}To change your key, type ${t.accent}/login${t.dim}.${t.reset}\n`);
|
|
569
|
+
console.log(`\n ${t.violet}API Key${t.reset}\n ${t.muted}current: ${masked}${t.reset}\n`);
|
|
658
570
|
askPrompt();
|
|
659
571
|
return;
|
|
660
572
|
}
|
|
661
573
|
|
|
662
|
-
// ─── /logout ─────────────────────────────────────────────────────────────
|
|
663
574
|
if (userPrompt.toLowerCase() === "/logout") {
|
|
664
575
|
try { fs.unlinkSync(CONFIG_FILE); } catch { }
|
|
665
576
|
config.apiKey = null;
|
|
666
|
-
console.log(`\n ${t.ok}Logged out. API key removed.${t.reset}`);
|
|
667
|
-
console.log(` ${t.muted}Type ${t.accent}/login${t.muted} to authenticate again.${t.reset}\n`);
|
|
577
|
+
console.log(`\n ${t.ok}✓ Logged out. API key removed.${t.reset}\n`);
|
|
668
578
|
askPrompt();
|
|
669
579
|
return;
|
|
670
580
|
}
|
|
671
581
|
|
|
672
|
-
// ─── /login ──────────────────────────────────────────────────────────────
|
|
673
582
|
if (userPrompt.toLowerCase() === "/login") {
|
|
674
|
-
console.log();
|
|
675
|
-
console.log(` ${t.violet}${t.bold}Login${t.reset}`);
|
|
676
|
-
console.log(` ${t.muted}Go to ${t.accent}https://puter.com${t.muted} and generate an API token.${t.reset}`);
|
|
677
|
-
console.log();
|
|
678
|
-
|
|
583
|
+
console.log(`\n ${t.violet}${t.bold}Login${t.reset}`);
|
|
679
584
|
process.stdin.setRawMode(false);
|
|
680
585
|
process.stdin.resume();
|
|
681
586
|
const key = await promptInput(` ${t.violetDim}Paste your API key: ${t.reset}`);
|
|
682
587
|
|
|
683
588
|
if (!key) {
|
|
684
|
-
console.log(`\n ${t.err}No key provided.${t.reset}\n`);
|
|
589
|
+
console.log(`\n ${t.err}✗ No key provided.${t.reset}\n`);
|
|
685
590
|
askPrompt();
|
|
686
591
|
return;
|
|
687
592
|
}
|
|
688
593
|
|
|
689
594
|
setConfig({ apiKey: key });
|
|
690
595
|
config.apiKey = key;
|
|
691
|
-
puter = init(key);
|
|
692
|
-
console.log(`\n ${t.ok}API key saved and applied!${t.reset}`);
|
|
693
|
-
console.log(` ${t.muted}You can now send prompts.${t.reset}\n`);
|
|
596
|
+
puter = init(key);
|
|
597
|
+
console.log(`\n ${t.ok}✓ API key saved and applied!${t.reset}\n`);
|
|
694
598
|
askPrompt();
|
|
695
599
|
return;
|
|
696
600
|
}
|
|
697
601
|
|
|
698
602
|
promptCount++;
|
|
699
603
|
|
|
700
|
-
if (conversationHistory.length >
|
|
604
|
+
if (conversationHistory.length > 4) {
|
|
605
|
+
const old = conversationHistory.slice(0, -2);
|
|
606
|
+
const summary = old.map(m =>
|
|
607
|
+
`${m.role === "user" ? "U" : "A"}: ${m.content.slice(0, 120)}${m.content.length > 120 ? "…" : ""}`
|
|
608
|
+
).join("\n");
|
|
609
|
+
conversationHistory = [
|
|
610
|
+
{ role: "user", content: `[Earlier conversation summary:\n${summary}]` },
|
|
611
|
+
...conversationHistory.slice(-2)
|
|
612
|
+
];
|
|
613
|
+
}
|
|
701
614
|
|
|
702
|
-
const projectContext = buildContext();
|
|
615
|
+
const projectContext = buildContext(userPrompt);
|
|
703
616
|
let fullPrompt = systemPrompt + "\n" + projectContext + "\n";
|
|
704
617
|
|
|
705
618
|
if (conversationHistory.length > 0) {
|
|
@@ -712,7 +625,6 @@ async function handleInput(raw) {
|
|
|
712
625
|
|
|
713
626
|
fullPrompt += `User prompt: ${userPrompt}`;
|
|
714
627
|
|
|
715
|
-
console.log();
|
|
716
628
|
const spinner = startSpinner("thinking");
|
|
717
629
|
|
|
718
630
|
try {
|
|
@@ -733,18 +645,14 @@ async function handleInput(raw) {
|
|
|
733
645
|
const cleanStr = jsonStr.replace(/,\s*}/g, '}').replace(/,\s*]/g, ']');
|
|
734
646
|
parsed = JSON.parse(cleanStr);
|
|
735
647
|
} catch (e2) {
|
|
736
|
-
try {
|
|
737
|
-
parsed = eval("(" + jsonStr + ")");
|
|
738
|
-
} catch (e3) {
|
|
739
|
-
/* plain text response */
|
|
740
|
-
}
|
|
648
|
+
try { parsed = eval("(" + jsonStr + ")"); } catch (e3) { /* plain text */ }
|
|
741
649
|
}
|
|
742
650
|
}
|
|
743
651
|
|
|
744
652
|
if (!parsed) {
|
|
745
653
|
stopSpinner(spinner, "");
|
|
746
654
|
console.log(`\n${marked(content)}\n`);
|
|
747
|
-
console.log(` ${t.warn}Note: The model returned malformed JSON
|
|
655
|
+
console.log(` ${t.warn}Note: The model returned malformed JSON.${t.reset}\n`);
|
|
748
656
|
conversationHistory.push({ role: "user", content: userPrompt });
|
|
749
657
|
conversationHistory.push({ role: "assistant", content });
|
|
750
658
|
askPrompt();
|
|
@@ -759,31 +667,25 @@ async function handleInput(raw) {
|
|
|
759
667
|
|
|
760
668
|
const hasFileOps = actions.length > 0 || edits.length > 0 || deletes.length > 0;
|
|
761
669
|
|
|
762
|
-
// ── delete files ──────────────────────────────────────────────────────────
|
|
763
670
|
if (deletes.length > 0) {
|
|
764
671
|
stopSpinner(spinner, "");
|
|
765
672
|
sectionHeader("removing files");
|
|
766
|
-
|
|
767
673
|
for (const delPath of deletes) {
|
|
768
674
|
try {
|
|
769
675
|
const stat = await fs.promises.stat(delPath);
|
|
770
|
-
if (stat.isDirectory()) await fs.promises.rm(delPath, { recursive: true, force: true
|
|
676
|
+
if (stat.isDirectory()) await fs.promises.rm(delPath, { recursive: true, force: true });
|
|
771
677
|
else await fs.promises.unlink(delPath);
|
|
772
678
|
const rel = path.relative(process.cwd(), delPath).replace(/\\/g, "/");
|
|
773
679
|
printStatus("-", t.err, `${t.dim}${rel}${t.reset}`);
|
|
774
680
|
} catch (err) {
|
|
775
|
-
if (err.code !== "ENOENT") {
|
|
776
|
-
printStatus("x", t.err, `${delPath} ${t.muted}${err.message}${t.reset}`);
|
|
777
|
-
}
|
|
681
|
+
if (err.code !== "ENOENT") printStatus("x", t.err, `${delPath} ${t.muted}${err.message}${t.reset}`);
|
|
778
682
|
}
|
|
779
683
|
}
|
|
780
684
|
}
|
|
781
685
|
|
|
782
|
-
// ── create files ──────────────────────────────────────────────────────────
|
|
783
686
|
if (actions.length > 0) {
|
|
784
687
|
if (deletes.length === 0) stopSpinner(spinner, "");
|
|
785
688
|
sectionHeader("creating files");
|
|
786
|
-
|
|
787
689
|
for (const action of actions) {
|
|
788
690
|
try {
|
|
789
691
|
const dir = path.dirname(action.path);
|
|
@@ -798,11 +700,9 @@ async function handleInput(raw) {
|
|
|
798
700
|
}
|
|
799
701
|
}
|
|
800
702
|
|
|
801
|
-
// ── edit files ────────────────────────────────────────────────────────────
|
|
802
703
|
if (edits.length > 0) {
|
|
803
704
|
if (deletes.length === 0 && actions.length === 0) stopSpinner(spinner, "");
|
|
804
705
|
sectionHeader("editing files");
|
|
805
|
-
|
|
806
706
|
for (const edit of edits) {
|
|
807
707
|
try {
|
|
808
708
|
let fileContent = await fs.promises.readFile(edit.path, "utf-8");
|
|
@@ -823,27 +723,19 @@ async function handleInput(raw) {
|
|
|
823
723
|
|
|
824
724
|
if (runs.length > 0) {
|
|
825
725
|
if (!hasFileOps) stopSpinner(spinner, "");
|
|
826
|
-
sectionHeader("running");
|
|
827
|
-
|
|
726
|
+
sectionHeader("running commands");
|
|
828
727
|
for (const cmd of runs) {
|
|
829
728
|
console.log(` ${t.muted}$ ${cmd}${t.reset}`);
|
|
830
729
|
const result = await runCommand(cmd, process.cwd());
|
|
831
|
-
|
|
832
|
-
if (result.stdout) {
|
|
833
|
-
result.stdout.split("\n").forEach((l) => console.log(` ${t.dim} ${l}${t.reset}`));
|
|
834
|
-
}
|
|
835
|
-
|
|
730
|
+
if (result.stdout) result.stdout.split("\n").forEach((l) => console.log(` ${t.dim} ${l}${t.reset}`));
|
|
836
731
|
if (result.error || result.stderr) {
|
|
837
732
|
const errOutput = result.stderr || result.error?.message || "";
|
|
838
733
|
errOutput.split("\n").forEach((l) => console.log(` ${t.err} ${l}${t.reset}`));
|
|
839
|
-
|
|
840
|
-
console.log();
|
|
841
|
-
console.log(` ${t.muted}error detected — sending to model for fix${t.reset}`);
|
|
734
|
+
console.log(`\n ${t.muted}error detected — sending to model for fix${t.reset}`);
|
|
842
735
|
|
|
843
736
|
const fixSpinner = startSpinner("fixing");
|
|
844
|
-
const fixCtx = systemPrompt + "\n" + buildContext() + "\n\n";
|
|
845
|
-
const fixMsg = `The command "${cmd}" produced this error:\n${errOutput}\
|
|
846
|
-
+ `Fix it by returning a JSON object with the necessary filesToEdit or filesToCreate.`;
|
|
737
|
+
const fixCtx = systemPrompt + "\n" + buildContext(cmd) + "\n\n";
|
|
738
|
+
const fixMsg = `The command "${cmd}" produced this error:\n${errOutput}\nFix it by returning a JSON object with the necessary filesToEdit or filesToCreate.`;
|
|
847
739
|
|
|
848
740
|
try {
|
|
849
741
|
const fixRes = await puter.ai.chat(fixCtx + fixMsg, { model: MODEL });
|
|
@@ -860,9 +752,9 @@ async function handleInput(raw) {
|
|
|
860
752
|
const dir = path.dirname(fm.path);
|
|
861
753
|
if (dir && dir !== ".") await fs.promises.mkdir(dir, { recursive: true });
|
|
862
754
|
await fs.promises.writeFile(fm.path, fm.content);
|
|
755
|
+
_contextCache = null;
|
|
863
756
|
await showCreatedFile(fm.path, fm.content);
|
|
864
757
|
}
|
|
865
|
-
|
|
866
758
|
for (const em of (fixParsed.filesToEdit || [])) {
|
|
867
759
|
try {
|
|
868
760
|
let fc = await fs.promises.readFile(em.path, "utf-8");
|
|
@@ -874,23 +766,19 @@ async function handleInput(raw) {
|
|
|
874
766
|
}
|
|
875
767
|
} catch { /* skip */ }
|
|
876
768
|
}
|
|
877
|
-
|
|
878
|
-
printStatus("", t.ok, "fix applied");
|
|
769
|
+
printStatus("✓", t.ok, "fix applied");
|
|
879
770
|
} catch (fixErr) {
|
|
880
771
|
stopSpinner(fixSpinner, "");
|
|
881
772
|
printStatus("x", t.err, `auto-fix failed ${t.muted}${fixErr.message || JSON.stringify(fixErr)}${t.reset}`);
|
|
882
773
|
}
|
|
883
774
|
} else {
|
|
884
|
-
console.log(` ${t.ok} ok${t.reset}`);
|
|
775
|
+
console.log(` ${t.ok} ✓ ok${t.reset}`);
|
|
885
776
|
}
|
|
886
777
|
}
|
|
887
778
|
}
|
|
888
779
|
|
|
889
780
|
if (hasFileOps || runs.length > 0) {
|
|
890
|
-
console.log();
|
|
891
|
-
if (message) {
|
|
892
|
-
console.log(` ${t.muted}${message}${t.reset}`);
|
|
893
|
-
}
|
|
781
|
+
if (message) console.log(`\n ${t.muted}${message}${t.reset}`);
|
|
894
782
|
const summary = [
|
|
895
783
|
...actions.map((a) => `created ${path.relative(process.cwd(), a.path).replace(/\\/g, "/")}`),
|
|
896
784
|
...edits.map((e) => `edited ${path.relative(process.cwd(), e.path).replace(/\\/g, "/")}`),
|
|
@@ -899,35 +787,30 @@ async function handleInput(raw) {
|
|
|
899
787
|
conversationHistory.push({ role: "user", content: userPrompt });
|
|
900
788
|
conversationHistory.push({ role: "assistant", content: `[${summary}] ${message}` });
|
|
901
789
|
} else if (message) {
|
|
902
|
-
stopSpinner(spinner);
|
|
790
|
+
stopSpinner(spinner, "");
|
|
903
791
|
console.log(`\n${marked(message)}\n`);
|
|
904
792
|
conversationHistory.push({ role: "user", content: userPrompt });
|
|
905
793
|
conversationHistory.push({ role: "assistant", content: message });
|
|
906
794
|
} else {
|
|
907
|
-
stopSpinner(spinner);
|
|
795
|
+
stopSpinner(spinner, "");
|
|
908
796
|
}
|
|
909
797
|
|
|
910
798
|
} catch (err) {
|
|
911
|
-
stopSpinner(spinner, ` ${t.err}error ${t.muted}${err.message || JSON.stringify(err)}${t.reset}`);
|
|
799
|
+
stopSpinner(spinner, ` ${t.err}✗ error ${t.muted}${err.message || JSON.stringify(err)}${t.reset}`);
|
|
912
800
|
} finally {
|
|
913
801
|
askPrompt();
|
|
914
802
|
}
|
|
915
803
|
}
|
|
916
804
|
|
|
917
|
-
|
|
805
|
+
// ─── STARTUP BANNER ──────────────────────────────────────────────────────────
|
|
918
806
|
function getGitBranch() {
|
|
919
|
-
try {
|
|
920
|
-
|
|
921
|
-
.execSync("git rev-parse --abbrev-ref HEAD", { stdio: ["pipe", "pipe", "pipe"] })
|
|
922
|
-
.toString().trim();
|
|
923
|
-
} catch { return null; }
|
|
807
|
+
try { return require("child_process").execSync("git rev-parse --abbrev-ref HEAD", { stdio: ["pipe", "pipe", "pipe"] }).toString().trim(); }
|
|
808
|
+
catch { return null; }
|
|
924
809
|
}
|
|
925
810
|
|
|
926
811
|
function getGitStatus() {
|
|
927
812
|
try {
|
|
928
|
-
const out = require("child_process")
|
|
929
|
-
.execSync("git status --porcelain", { stdio: ["pipe", "pipe", "pipe"] })
|
|
930
|
-
.toString().trim();
|
|
813
|
+
const out = require("child_process").execSync("git status --porcelain", { stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
|
|
931
814
|
if (!out) return "clean";
|
|
932
815
|
const lines = out.split("\n");
|
|
933
816
|
const mod = lines.filter(l => l.startsWith(" M") || l.startsWith("M")).length;
|
|
@@ -939,10 +822,6 @@ function getGitStatus() {
|
|
|
939
822
|
} catch { return null; }
|
|
940
823
|
}
|
|
941
824
|
|
|
942
|
-
function getNodeVersion() {
|
|
943
|
-
return process.version;
|
|
944
|
-
}
|
|
945
|
-
|
|
946
825
|
function getNow() {
|
|
947
826
|
const d = new Date();
|
|
948
827
|
const pad = (n) => String(n).padStart(2, "0");
|
|
@@ -950,50 +829,39 @@ function getNow() {
|
|
|
950
829
|
}
|
|
951
830
|
|
|
952
831
|
function getDate() {
|
|
953
|
-
|
|
954
|
-
return d.toLocaleDateString("en-GB", { weekday: "short", day: "2-digit", month: "short", year: "numeric" });
|
|
832
|
+
return new Date().toLocaleDateString("en-GB", { weekday: "short", day: "2-digit", month: "short", year: "numeric" });
|
|
955
833
|
}
|
|
956
834
|
|
|
957
|
-
|
|
958
|
-
|
|
959
835
|
async function checkForUpdate() {
|
|
960
836
|
try {
|
|
961
837
|
const res = await fetch(`https://registry.npmjs.org/cli-atom/latest`, { signal: AbortSignal.timeout(3000) });
|
|
962
838
|
if (!res.ok) return null;
|
|
963
839
|
const data = await res.json();
|
|
964
840
|
return data.version || null;
|
|
965
|
-
} catch {
|
|
966
|
-
return null;
|
|
967
|
-
}
|
|
841
|
+
} catch { return null; }
|
|
968
842
|
}
|
|
969
843
|
|
|
970
844
|
async function start() {
|
|
971
845
|
console.clear();
|
|
972
|
-
|
|
973
846
|
const w = cols();
|
|
974
847
|
const branch = getGitBranch();
|
|
975
848
|
const status = getGitStatus();
|
|
976
849
|
const cwd = process.cwd();
|
|
977
|
-
const node =
|
|
850
|
+
const node = process.version;
|
|
978
851
|
const now = getNow();
|
|
979
852
|
const date = getDate();
|
|
980
853
|
|
|
981
|
-
// check for update in background
|
|
982
854
|
const latestVersion = await checkForUpdate();
|
|
983
855
|
const hasUpdate = latestVersion && latestVersion !== VERSION;
|
|
984
856
|
|
|
985
|
-
console.log();
|
|
986
857
|
console.log(rule("─"));
|
|
987
|
-
console.log();
|
|
988
858
|
|
|
989
|
-
const titleLeft =
|
|
859
|
+
const titleLeft = ` ${t.violet}${t.bold}ATOM${t.reset} ${t.muted}coding agent${t.reset}`;
|
|
990
860
|
const titleRight = `${t.violetDim}v${VERSION}${t.reset}`;
|
|
991
861
|
const titleRightVis = `v${VERSION}`;
|
|
992
|
-
const titleGap = w - "ATOM coding agent".length - titleRightVis.length;
|
|
862
|
+
const titleGap = w - " ATOM coding agent".length - titleRightVis.length;
|
|
993
863
|
console.log(titleLeft + " ".repeat(Math.max(0, titleGap)) + titleRight);
|
|
994
864
|
|
|
995
|
-
console.log();
|
|
996
|
-
|
|
997
865
|
const rows = [
|
|
998
866
|
[`model`, MODEL_LABEL],
|
|
999
867
|
[`node`, node],
|
|
@@ -1005,23 +873,19 @@ async function start() {
|
|
|
1005
873
|
].filter(Boolean);
|
|
1006
874
|
|
|
1007
875
|
for (const [label, value] of rows) {
|
|
1008
|
-
const l =
|
|
876
|
+
const l = ` ${t.violetDim}${label.padEnd(8)}${t.reset}`;
|
|
1009
877
|
const v = `${t.muted}${value}${t.reset}`;
|
|
1010
878
|
console.log(l + v);
|
|
1011
879
|
}
|
|
1012
880
|
|
|
1013
|
-
console.log();
|
|
1014
881
|
console.log(rule("─"));
|
|
1015
|
-
console.log();
|
|
1016
882
|
|
|
1017
883
|
if (hasUpdate) {
|
|
1018
|
-
console.log(` ${t.warn}update available${t.reset} ${t.muted}v${VERSION} → ${t.accent}v${latestVersion}${t.reset}`);
|
|
1019
|
-
console.log(` ${t.dim}run ${t.accent}npm install -g cli-atom${t.dim} to update${t.reset}`);
|
|
1020
|
-
console.log();
|
|
884
|
+
console.log(` ${t.warn}⚠ update available${t.reset} ${t.muted}v${VERSION} → ${t.accent}v${latestVersion}${t.reset}`);
|
|
885
|
+
console.log(` ${t.dim}run ${t.accent}npm install -g cli-atom${t.dim} to update${t.reset}\n`);
|
|
1021
886
|
}
|
|
1022
887
|
|
|
1023
|
-
console.log(
|
|
1024
|
-
|
|
888
|
+
console.log(` ${t.ok}✓ ready${t.reset}`);
|
|
1025
889
|
askPrompt();
|
|
1026
890
|
}
|
|
1027
891
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cli-atom",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"description": "ATOM - Coding Agent",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"author": "Redwxll",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@heyputer/puter.js": "^2.5.3",
|
|
17
17
|
"@inquirer/prompts": "^8.5.2",
|
|
18
|
+
"discord.js": "^14.26.4",
|
|
18
19
|
"figlet": "^1.11.0",
|
|
19
20
|
"marked": "^15.0.12",
|
|
20
21
|
"marked-terminal": "^7.3.0"
|
|
@@ -22,4 +23,4 @@
|
|
|
22
23
|
"devDependencies": {
|
|
23
24
|
"mocha": "^11.7.6"
|
|
24
25
|
}
|
|
25
|
-
}
|
|
26
|
+
}
|