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