cli-atom 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/index.js +994 -0
  2. package/npm +0 -0
  3. package/package.json +25 -0
package/index.js ADDED
@@ -0,0 +1,994 @@
1
+ #!/usr/bin/env node
2
+ import { init } from "@heyputer/puter.js/src/init.cjs";
3
+ import fs from "fs";
4
+ import os from "os";
5
+ import path from "path";
6
+ import { marked } from "marked";
7
+ import { markedTerminal } from "marked-terminal";
8
+ import { exec } from "child_process";
9
+
10
+ marked.use(markedTerminal());
11
+
12
+ process.on("unhandledRejection", (err) => { console.error("unhandled rejection:", err); });
13
+ process.on("uncaughtException", (err) => { console.error("uncaught exception:", err); });
14
+
15
+ const t = {
16
+ reset: "\x1b[0m",
17
+ bold: "\x1b[1m",
18
+ dim: "\x1b[2m",
19
+ white: "\x1b[97m",
20
+ gray: "\x1b[90m",
21
+ black: "\x1b[30m",
22
+ muted: "\x1b[38;5;244m",
23
+ accent: "\x1b[38;5;255m",
24
+ subtle: "\x1b[38;5;238m",
25
+ violet: "\x1b[38;5;203m",
26
+ violetDim: "\x1b[38;5;160m",
27
+ ok: "\x1b[38;5;114m",
28
+ warn: "\x1b[38;5;179m",
29
+ err: "\x1b[38;5;167m",
30
+ info: "\x1b[38;5;110m",
31
+ bgDark: "\x1b[48;5;234m",
32
+ bgMid: "\x1b[48;5;236m",
33
+ };
34
+
35
+ const IGNORE_DIRS = ["node_modules", ".git", ".next", "dist", "build", "__pycache__", ".cache"];
36
+ const IGNORE_EXTS = [".png", ".jpg", ".jpeg", ".gif", ".ico", ".svg", ".woff", ".woff2",
37
+ ".ttf", ".eot", ".mp3", ".mp4", ".zip", ".tar", ".gz",
38
+ ".exe", ".dll", ".so", ".lock"];
39
+ const MAX_FILE_SIZE = 15_000;
40
+ let MODEL = "z-ai/glm-5-turbo";
41
+ let MODEL_LABEL = "glm-5-turbo";
42
+ const VERSION = "0.2.0";
43
+
44
+ const MODELS = [
45
+ { id: "z-ai/glm-5-turbo", label: "glm-5-turbo" },
46
+ { id: "openai/gpt-4o", label: "gpt-4o" },
47
+ { id: "openai/gpt-4-turbo", label: "gpt-4-turbo" },
48
+ { id: "meta-llama/llama-3-70b-instruct", label: "llama-3-70b" },
49
+ { id: "mistral-large", label: "mistral-large" }
50
+ ];
51
+
52
+ // ─── config management ───────────────────────────────────────────────────────
53
+
54
+ const CONFIG_DIR = path.join(os.homedir(), ".atom-cli");
55
+ const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
56
+
57
+ function getConfig() {
58
+ try {
59
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
60
+ } catch {
61
+ return {};
62
+ }
63
+ }
64
+
65
+ function setConfig(data) {
66
+ if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
67
+ const current = getConfig();
68
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify({ ...current, ...data }, null, 2));
69
+ }
70
+
71
+ function promptInput(question) {
72
+ return new Promise((resolve) => {
73
+ process.stdout.write(question);
74
+ process.stdin.setEncoding("utf8");
75
+ process.stdin.once("data", (data) => {
76
+ resolve(data.trim());
77
+ });
78
+ });
79
+ }
80
+
81
+ // ─── CLI commands (login / logout) ───────────────────────────────────────────
82
+
83
+ const args = process.argv.slice(2);
84
+
85
+ if (args[0] === "login") {
86
+ console.log();
87
+ console.log(` ${t.violet}${t.bold}ATOM${t.reset} ${t.muted}login${t.reset}`);
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();
91
+ const key = await promptInput(` ${t.violetDim}Paste your API key: ${t.reset}`);
92
+ if (!key) {
93
+ console.log(`\n ${t.err}No key provided. Aborting.${t.reset}\n`);
94
+ process.exit(1);
95
+ }
96
+ setConfig({ apiKey: key });
97
+ console.log(`\n ${t.ok}API key saved to ${t.dim}${CONFIG_FILE}${t.reset}`);
98
+ console.log(` ${t.muted}You can now run ${t.accent}atom${t.muted} to start coding!${t.reset}\n`);
99
+ process.exit(0);
100
+ }
101
+
102
+ if (args[0] === "logout") {
103
+ try { fs.unlinkSync(CONFIG_FILE); } catch { }
104
+ console.log(`\n ${t.ok}Logged out. API key removed.${t.reset}\n`);
105
+ process.exit(0);
106
+ }
107
+
108
+ // ─── puter init ──────────────────────────────────────────────────────────────
109
+
110
+ let config = getConfig();
111
+ if (!config.apiKey) {
112
+ console.log();
113
+ console.log(` ${t.err}No API key found.${t.reset}`);
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();
116
+ process.exit(1);
117
+ }
118
+
119
+ let puter = init(config.apiKey);
120
+
121
+ let conversationHistory = [];
122
+ let promptCount = 0;
123
+ let filesCreated = 0;
124
+ let filesEdited = 0;
125
+ 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
+
129
+ You MUST respond EXCLUSIVELY with a valid JSON object. Do NOT wrap the JSON in any other text, just output the JSON object.
130
+ Use the following strict schema. CRITICAL: Ensure all strings inside the JSON are properly escaped (use \\n for newlines, and escape double quotes \\").
131
+
132
+ {
133
+ "message": "A concise explanation of what you are doing (optional)",
134
+ "filesToCreate": [
135
+ {
136
+ "path": "path/to/new_file.ext",
137
+ "content": "Full content of the new file"
138
+ }
139
+ ],
140
+ "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
+ ],
147
+ "filesToDelete": [
148
+ "path/to/file_or_folder"
149
+ ],
150
+ "commandsToRun": [
151
+ "npm test",
152
+ "node script.js"
153
+ ]
154
+ }
155
+
156
+ - Keep "message" concise and normal.
157
+ - If you don't need to do an action, leave the array empty \`[]\` or omit it.
158
+ - Write compact and efficient code.
159
+ - If no file operations are needed, just provide a "message" and leave the arrays empty.`;
160
+
161
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
162
+
163
+ function cols() {
164
+ return process.stdout.columns || 80;
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
+
176
+ function elapsed() {
177
+ const s = Math.floor((Date.now() - sessionStart) / 1000);
178
+ return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`;
179
+ }
180
+
181
+ function scanDir(dirPath, prefix = "") {
182
+ let tree = "";
183
+ let files = [];
184
+ try {
185
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
186
+ for (const entry of entries) {
187
+ if (IGNORE_DIRS.includes(entry.name)) continue;
188
+ const fullPath = path.join(dirPath, entry.name);
189
+ if (entry.isDirectory()) {
190
+ tree += `${prefix}${entry.name}/\n`;
191
+ const sub = scanDir(fullPath, prefix + " ");
192
+ tree += sub.tree;
193
+ files.push(...sub.files);
194
+ } else {
195
+ const ext = path.extname(entry.name).toLowerCase();
196
+ if (IGNORE_EXTS.includes(ext)) continue;
197
+ tree += `${prefix}${entry.name}\n`;
198
+ try {
199
+ const stat = fs.statSync(fullPath);
200
+ const content = stat.size <= MAX_FILE_SIZE
201
+ ? fs.readFileSync(fullPath, "utf-8")
202
+ : "[file too large — truncated]";
203
+ files.push({ path: fullPath.replace(/\\/g, "/"), content });
204
+ } catch { /* skip unreadable files */ }
205
+ }
206
+ }
207
+ } catch { /* skip unreadable dirs */ }
208
+ return { tree, files };
209
+ }
210
+
211
+
212
+ let _contextCache = null;
213
+ let _contextSnapshot = null;
214
+
215
+ function getSnapshot(files) {
216
+ const snap = {};
217
+ for (const f of files) {
218
+ try { snap[f.path] = fs.statSync(f.path).mtimeMs; } catch { snap[f.path] = 0; }
219
+ }
220
+ return snap;
221
+ }
222
+
223
+ function snapshotChanged(files, snap) {
224
+ if (!snap) return true;
225
+ for (const f of files) {
226
+ try {
227
+ if (fs.statSync(f.path).mtimeMs !== snap[f.path]) return true;
228
+ } catch { return true; }
229
+ }
230
+ return Object.keys(snap).length !== files.length;
231
+ }
232
+
233
+ function buildContext() {
234
+ const cwd = process.cwd();
235
+ const { tree, files } = scanDir(cwd);
236
+
237
+ if (_contextCache && !snapshotChanged(files, _contextSnapshot)) {
238
+ return _contextCache;
239
+ }
240
+
241
+ let ctx = `\n--- PROJECT CONTEXT (${cwd}) ---\n`;
242
+ ctx += `File tree:\n${tree}\n`;
243
+ for (const f of files) {
244
+ const rel = path.relative(cwd, f.path).replace(/\\/g, "/");
245
+ ctx += `--- ${rel} ---\n${f.content}\n--- end ${rel} ---\n\n`;
246
+ }
247
+ ctx += `--- END PROJECT CONTEXT ---\n`;
248
+
249
+ _contextCache = ctx;
250
+ _contextSnapshot = getSnapshot(files);
251
+ return ctx;
252
+ }
253
+
254
+ setTimeout(() => buildContext(), 0);
255
+
256
+ function runCommand(cmd, cwd) {
257
+ return new Promise((resolve) => {
258
+ exec(cmd, { cwd, timeout: 15_000, maxBuffer: 512 * 1024 }, (error, stdout, stderr) => {
259
+ resolve({ error, stdout: stdout || "", stderr: stderr || "" });
260
+ });
261
+ });
262
+ }
263
+
264
+ const SPINNER_FRAMES = ["·", "·", "·", "·", "·", "·", "·", "·", "·", "·"].map(
265
+ (_, i, a) => {
266
+ const bar = a.map((__, j) => (j <= i ? "▪" : "·")).join("");
267
+ return bar;
268
+ }
269
+ );
270
+ const SPINNER_CHARS = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
271
+
272
+ function startSpinner(label) {
273
+ let i = 0;
274
+ const timer = setInterval(() => {
275
+ process.stdout.clearLine(0);
276
+ process.stdout.cursorTo(0);
277
+ process.stdout.write(
278
+ ` ${t.muted}${SPINNER_CHARS[i % SPINNER_CHARS.length]} ${label}${t.reset}`
279
+ );
280
+ i++;
281
+ }, 80);
282
+ return timer;
283
+ }
284
+
285
+ function stopSpinner(timer, msg) {
286
+ clearInterval(timer);
287
+ process.stdout.clearLine(0);
288
+ process.stdout.cursorTo(0);
289
+ if (msg) console.log(msg);
290
+ }
291
+
292
+
293
+ function sectionHeader(label) {
294
+ console.log();
295
+ console.log(`${t.violet}${label}${t.reset}`);
296
+ console.log(`${t.violetDim}${"─".repeat(cols())}${t.reset}`);
297
+ }
298
+
299
+ async function showCreatedFile(filePath, content) {
300
+ const lines = content.split("\n");
301
+ const relPath = path.relative(process.cwd(), filePath).replace(/\\/g, "/");
302
+ console.log(` ${t.ok}+${t.reset} ${t.accent}${relPath}${t.reset} ${t.muted}${lines.length} lines${t.reset}`);
303
+ await sleep(60);
304
+
305
+ const preview = lines.slice(0, 8);
306
+ for (let i = 0; i < preview.length; i++) {
307
+ const num = String(i + 1).padStart(3, " ");
308
+ console.log(` ${t.subtle}${num}${t.reset} ${t.dim}${preview[i]}${t.reset}`);
309
+ await sleep(12);
310
+ }
311
+ if (lines.length > 8) {
312
+ console.log(` ${t.subtle} ${t.reset} ${t.muted}... ${lines.length - 8} more lines${t.reset}`);
313
+ }
314
+ console.log();
315
+ }
316
+
317
+ async function showDiff(filePath, searchBlock, replaceBlock) {
318
+ const relPath = path.relative(process.cwd(), filePath).replace(/\\/g, "/");
319
+ console.log(` ${t.warn}~${t.reset} ${t.accent}${relPath}${t.reset}`);
320
+ await sleep(60);
321
+
322
+ for (const line of searchBlock.split("\n")) {
323
+ console.log(` ${t.err}- ${t.dim}${line}${t.reset}`);
324
+ await sleep(18);
325
+ }
326
+ for (const line of replaceBlock.split("\n")) {
327
+ console.log(` ${t.ok}+ ${line}${t.reset}`);
328
+ await sleep(18);
329
+ }
330
+ console.log();
331
+ }
332
+
333
+ function printStatus(icon, color, msg) {
334
+ console.log(` ${color}${icon}${t.reset} ${msg}`);
335
+ }
336
+
337
+ // ─── slash commands definition ────────────────────────────────────────────────
338
+ const SLASH_COMMANDS = [
339
+ { name: "help", desc: "Show all available commands" },
340
+ { name: "model", desc: "List or switch AI model" },
341
+ { name: "key", desc: "View your current API key" },
342
+ { name: "login", desc: "Set a new API key" },
343
+ { name: "logout", desc: "Remove your API key" },
344
+ { name: "quit", desc: "Exit the CLI" },
345
+ ];
346
+
347
+ function askPrompt() {
348
+ console.log();
349
+ console.log(rule());
350
+
351
+ const sessionStats = `${filesCreated} created ${filesEdited} edited`;
352
+ const left = `${t.subtle}${sessionStats}${t.reset}`;
353
+ const right = `${t.muted}${MODEL_LABEL} ${promptCount} req ${elapsed()}${t.reset}`;
354
+ const rightVisible = right.replace(/\x1b\[[0-9;]*m/g, "");
355
+ const gap = cols()
356
+ - left.replace(/\x1b\[[0-9;]*m/g, "").length
357
+ - rightVisible.length;
358
+ console.log(left + " ".repeat(Math.max(0, gap)) + right);
359
+ console.log();
360
+
361
+ let input = "";
362
+ let menuActive = false;
363
+ let menuIndex = 0;
364
+ let menuItems = [];
365
+ let menuLineCount = 0;
366
+
367
+ const BG = "\x1b[48;5;238m";
368
+ const FG = "\x1b[38;5;255m";
369
+ const FGP = "\x1b[38;5;245m";
370
+ const PAD = " ";
371
+
372
+ function drawBox(text, isPlaceholder) {
373
+ const currentCols = cols();
374
+ const fg = isPlaceholder ? FGP : FG;
375
+ let displayStr = text;
376
+ const maxLen = Math.max(10, currentCols - PAD.length - 2);
377
+ if (!isPlaceholder && displayStr.length > maxLen) {
378
+ displayStr = "…" + displayStr.slice(displayStr.length - maxLen + 1);
379
+ }
380
+ const content = PAD + displayStr;
381
+ const fill = " ".repeat(Math.max(0, currentCols - content.length));
382
+ const emptyRow = BG + " ".repeat(currentCols) + "\x1b[0m";
383
+ const textRow = BG + fg + content + fill + "\x1b[0m";
384
+ return emptyRow + "\n" + textRow + "\n" + emptyRow + "\n";
385
+ }
386
+
387
+ function getFilteredCommands() {
388
+ const query = input.slice(1).toLowerCase(); // strip the leading /
389
+ return SLASH_COMMANDS.filter(c => c.name.startsWith(query));
390
+ }
391
+
392
+ function renderMenu() {
393
+ const currentCols = cols();
394
+ menuItems = getFilteredCommands();
395
+
396
+ // clamp index
397
+ if (menuIndex >= menuItems.length) menuIndex = 0;
398
+
399
+ // clear previous menu lines
400
+ if (menuLineCount > 0) {
401
+ for (let i = 0; i < menuLineCount; i++) {
402
+ process.stdout.write("\x1b[1A\x1b[2K");
403
+ }
404
+ }
405
+
406
+ if (menuItems.length === 0) {
407
+ menuLineCount = 0;
408
+ return;
409
+ }
410
+
411
+ const nameWidth = Math.max(...menuItems.map(c => c.name.length)) + 2;
412
+ let out = "";
413
+ for (let i = 0; i < menuItems.length; i++) {
414
+ const item = menuItems[i];
415
+ const isSelected = i === menuIndex;
416
+ const namePad = ("/" + item.name).padEnd(nameWidth);
417
+ if (isSelected) {
418
+ out += `\x1b[48;5;236m${t.violet}${t.bold} ${namePad}${t.reset}\x1b[48;5;236m ${t.accent}${item.desc}${t.reset}`;
419
+ } else {
420
+ out += ` ${t.violetDim}${namePad}${t.reset} ${t.muted}${item.desc}${t.reset}`;
421
+ }
422
+ out += " ".repeat(Math.max(0, currentCols - namePad.length - item.desc.length - 4)) + "\n";
423
+ }
424
+
425
+ // count indicator
426
+ out += `${t.subtle}(${menuIndex + 1}/${menuItems.length})${t.reset}\n`;
427
+ menuLineCount = menuItems.length + 1;
428
+
429
+ process.stdout.write(out);
430
+ }
431
+
432
+ function clearMenu() {
433
+ if (menuLineCount > 0) {
434
+ for (let i = 0; i < menuLineCount; i++) {
435
+ process.stdout.write("\x1b[1A\x1b[2K");
436
+ }
437
+ menuLineCount = 0;
438
+ }
439
+ }
440
+
441
+ function renderBox() {
442
+ process.stdout.write("\x1b[3A\r" + drawBox(input || "", input.length === 0));
443
+ if (menuActive) renderMenu();
444
+ }
445
+
446
+ function onResize() { renderBox(); }
447
+ process.stdout.on("resize", onResize);
448
+
449
+ process.stdout.write(drawBox("insert your instruction...", true));
450
+ process.stdout.write("\x1b[?25l");
451
+
452
+ process.stdin.setRawMode(true);
453
+ process.stdin.resume();
454
+ process.stdin.setEncoding("utf8");
455
+
456
+ function cleanup() {
457
+ process.stdin.setRawMode(false);
458
+ process.stdin.pause();
459
+ process.stdin.removeListener("data", onData);
460
+ process.stdout.removeListener("resize", onResize);
461
+ process.stdout.write("\x1b[?25h");
462
+ }
463
+
464
+ function submitInput(value) {
465
+ clearMenu();
466
+ process.stdout.write("\x1b[3A\x1b[2K\x1b[1B\x1b[2K\x1b[1B\x1b[2K\x1b[1A\r");
467
+ handleInput(value);
468
+ }
469
+
470
+ function onData(key) {
471
+ // Ctrl+C
472
+ if (key === "\u0003") { process.stdout.write("\x1b[?25h"); process.exit(); }
473
+
474
+ // Arrow up
475
+ if (key === "\x1b[A") {
476
+ if (menuActive && menuItems.length > 0) {
477
+ menuIndex = (menuIndex - 1 + menuItems.length) % menuItems.length;
478
+ renderMenu();
479
+ }
480
+ return;
481
+ }
482
+
483
+ // Arrow down
484
+ if (key === "\x1b[B") {
485
+ if (menuActive && menuItems.length > 0) {
486
+ menuIndex = (menuIndex + 1) % menuItems.length;
487
+ renderMenu();
488
+ }
489
+ return;
490
+ }
491
+
492
+ // Tab — autocomplete selected menu item
493
+ if (key === "\t") {
494
+ if (menuActive && menuItems.length > 0) {
495
+ input = "/" + menuItems[menuIndex].name;
496
+ process.stdout.write("\x1b[3A\r" + drawBox(input, false));
497
+ renderMenu();
498
+ }
499
+ return;
500
+ }
501
+
502
+ // Enter
503
+ if (key === "\r" || key === "\n") {
504
+ if (menuActive && menuItems.length > 0) {
505
+ // select highlighted command
506
+ const selected = "/" + menuItems[menuIndex].name;
507
+ cleanup();
508
+ clearMenu();
509
+ process.stdout.write("\x1b[3A\x1b[2K\x1b[1B\x1b[2K\x1b[1B\x1b[2K\x1b[1A\r");
510
+ handleInput(selected);
511
+ return;
512
+ }
513
+ if (!input.trim()) return;
514
+ cleanup();
515
+ submitInput(input);
516
+ return;
517
+ }
518
+
519
+ // Escape — close menu
520
+ if (key === "\x1b") {
521
+ if (menuActive) {
522
+ menuActive = false;
523
+ clearMenu();
524
+ process.stdout.write("\x1b[3A\r" + drawBox(input, false));
525
+ }
526
+ return;
527
+ }
528
+
529
+ // Backspace
530
+ if (key === "\x7f" || key === "\b") {
531
+ input = input.slice(0, -1);
532
+ if (input === "" || input === "/") {
533
+ if (input === "") {
534
+ menuActive = false;
535
+ clearMenu();
536
+ }
537
+ }
538
+ if (input.startsWith("/")) {
539
+ menuActive = true;
540
+ menuIndex = 0;
541
+ process.stdout.write("\x1b[3A\r" + drawBox(input, false));
542
+ renderMenu();
543
+ } else {
544
+ menuActive = false;
545
+ clearMenu();
546
+ process.stdout.write("\x1b[3A\r" + drawBox(input || "", input.length === 0));
547
+ }
548
+ return;
549
+ }
550
+
551
+ // Printable chars
552
+ if (key.charCodeAt(0) >= 32) {
553
+ input += key;
554
+
555
+ // open menu when "/" is first char
556
+ if (input === "/") {
557
+ menuActive = true;
558
+ menuIndex = 0;
559
+ process.stdout.write("\x1b[3A\r" + drawBox(input, false));
560
+ renderMenu();
561
+ return;
562
+ }
563
+
564
+ // filter menu as user types
565
+ if (input.startsWith("/")) {
566
+ menuActive = true;
567
+ menuIndex = 0;
568
+ process.stdout.write("\x1b[3A\r" + drawBox(input, false));
569
+ renderMenu();
570
+ return;
571
+ }
572
+
573
+ // normal input
574
+ menuActive = false;
575
+ clearMenu();
576
+ process.stdout.write("\x1b[3A\r" + drawBox(input, false));
577
+ }
578
+ }
579
+
580
+ process.stdin.on("data", onData);
581
+ }
582
+
583
+ async function handleInput(raw) {
584
+ const userPrompt = raw.trim();
585
+
586
+ if (!userPrompt) { askPrompt(); return; }
587
+
588
+ if (userPrompt.toLowerCase() === "exit" || userPrompt.toLowerCase() === "quit") {
589
+ console.log();
590
+ console.log(` ${t.muted}session ended ${filesCreated} created ${filesEdited} edited ${elapsed()}${t.reset}`);
591
+ console.log();
592
+ process.exit(0);
593
+ return;
594
+ }
595
+
596
+ // ─── /help ───────────────────────────────────────────────────────────────
597
+ if (userPrompt.toLowerCase() === "/help") {
598
+ console.log();
599
+ console.log(` ${t.violet}${t.bold}Available commands${t.reset}`);
600
+ console.log(` ${t.violetDim}${"─".repeat(30)}${t.reset}`);
601
+ console.log(` ${t.violetDim}/help${t.reset} ${t.muted}show this help message${t.reset}`);
602
+ console.log(` ${t.violetDim}/model${t.reset} ${t.muted}list available AI models${t.reset}`);
603
+ console.log(` ${t.violetDim}/model <name>${t.reset} ${t.muted}switch to a specific model${t.reset}`);
604
+ console.log(` ${t.violetDim}/key${t.reset} ${t.muted}view your current API key${t.reset}`);
605
+ console.log(` ${t.violetDim}/login${t.reset} ${t.muted}set a new API key${t.reset}`);
606
+ console.log(` ${t.violetDim}/logout${t.reset} ${t.muted}remove your API key${t.reset}`);
607
+ console.log(` ${t.violetDim}exit / quit${t.reset} ${t.muted}end the session${t.reset}`);
608
+ console.log();
609
+ askPrompt();
610
+ return;
611
+ }
612
+
613
+ // ─── /model ──────────────────────────────────────────────────────────────
614
+ if (userPrompt.toLowerCase().startsWith("/model")) {
615
+ const parts = userPrompt.split(" ");
616
+ if (parts.length === 1) {
617
+ console.log(`\n ${t.violet}Available models:${t.reset}`);
618
+ MODELS.forEach((m, i) => console.log(` ${t.muted}${i + 1}. ${m.label}${t.reset}`));
619
+ console.log(`\n ${t.dim}Type '/model <number>' or '/model <name>' to switch.${t.reset}\n`);
620
+ askPrompt();
621
+ return;
622
+ }
623
+
624
+ const selection = parts[1].toLowerCase();
625
+ const selected = MODELS.find(m => m.label.toLowerCase() === selection) ||
626
+ MODELS.find(m => m.id.toLowerCase() === selection) ||
627
+ MODELS[parseInt(selection) - 1];
628
+
629
+ if (selected) {
630
+ MODEL = selected.id;
631
+ MODEL_LABEL = selected.label;
632
+ console.log(`\n ${t.ok}Model switched to ${t.bold}${MODEL_LABEL}${t.reset}\n`);
633
+ } else {
634
+ console.log(`\n ${t.err}Model not found. Type '/model' to see the list.${t.reset}\n`);
635
+ }
636
+ askPrompt();
637
+ return;
638
+ }
639
+
640
+ // ─── /key ────────────────────────────────────────────────────────────────
641
+ if (userPrompt.toLowerCase() === "/key") {
642
+ const masked = config.apiKey ? config.apiKey.slice(0, 10) + "..." + config.apiKey.slice(-6) : "none";
643
+ console.log(`\n ${t.violet}API Key${t.reset}`);
644
+ console.log(` ${t.muted}current: ${masked}${t.reset}`);
645
+ console.log(` ${t.dim}To change your key, type ${t.accent}/login${t.dim} or run ${t.accent}atom login${t.dim} in your terminal.${t.reset}\n`);
646
+ askPrompt();
647
+ return;
648
+ }
649
+
650
+ // ─── /logout ─────────────────────────────────────────────────────────────
651
+ if (userPrompt.toLowerCase() === "/logout") {
652
+ try { fs.unlinkSync(CONFIG_FILE); } catch { }
653
+ config.apiKey = null;
654
+ console.log(`\n ${t.ok}Logged out. API key removed.${t.reset}`);
655
+ console.log(` ${t.muted}Type ${t.accent}/login${t.muted} to authenticate again.${t.reset}\n`);
656
+ askPrompt();
657
+ return;
658
+ }
659
+
660
+ // ─── /login ──────────────────────────────────────────────────────────────
661
+ if (userPrompt.toLowerCase() === "/login") {
662
+ console.log();
663
+ console.log(` ${t.violet}${t.bold}Login${t.reset}`);
664
+ console.log(` ${t.muted}Go to ${t.accent}https://puter.com${t.muted} and generate an API token.${t.reset}`);
665
+ console.log();
666
+
667
+ process.stdin.setRawMode(false);
668
+ process.stdin.resume();
669
+ const key = await promptInput(` ${t.violetDim}Paste your API key: ${t.reset}`);
670
+
671
+ if (!key) {
672
+ console.log(`\n ${t.err}No key provided.${t.reset}\n`);
673
+ askPrompt();
674
+ return;
675
+ }
676
+
677
+ setConfig({ apiKey: key });
678
+ config.apiKey = key;
679
+ puter = init(key); // reinitialize puter with new key
680
+ console.log(`\n ${t.ok}API key saved and applied!${t.reset}`);
681
+ console.log(` ${t.muted}You can now send prompts.${t.reset}\n`);
682
+ askPrompt();
683
+ return;
684
+ }
685
+
686
+ promptCount++;
687
+
688
+ if (conversationHistory.length > 6) conversationHistory = conversationHistory.slice(-6);
689
+
690
+ const projectContext = buildContext();
691
+ let fullPrompt = systemPrompt + "\n" + projectContext + "\n";
692
+
693
+ if (conversationHistory.length > 0) {
694
+ fullPrompt += "--- Conversation History ---\n";
695
+ conversationHistory.forEach((msg) => {
696
+ fullPrompt += `${msg.role === "user" ? "User" : "Assistant"}: ${msg.content}\n\n`;
697
+ });
698
+ fullPrompt += "--- End of History ---\n\n";
699
+ }
700
+
701
+ fullPrompt += `User prompt: ${userPrompt}`;
702
+
703
+ console.log();
704
+ const spinner = startSpinner("thinking");
705
+
706
+ try {
707
+ const response = await puter.ai.chat(fullPrompt, { model: MODEL });
708
+ let content = response["message"]["content"];
709
+
710
+ let jsonStr = content.trim();
711
+ if (jsonStr.startsWith("```json")) jsonStr = jsonStr.slice(7);
712
+ else if (jsonStr.startsWith("```")) jsonStr = jsonStr.slice(3);
713
+ if (jsonStr.endsWith("```")) jsonStr = jsonStr.slice(0, -3);
714
+ jsonStr = jsonStr.trim();
715
+
716
+ let parsed = null;
717
+ try {
718
+ parsed = JSON.parse(jsonStr);
719
+ } catch (e1) {
720
+ try {
721
+ const cleanStr = jsonStr.replace(/,\s*}/g, '}').replace(/,\s*]/g, ']');
722
+ parsed = JSON.parse(cleanStr);
723
+ } catch (e2) {
724
+ try {
725
+ parsed = eval("(" + jsonStr + ")");
726
+ } catch (e3) {
727
+ /* plain text response */
728
+ }
729
+ }
730
+ }
731
+
732
+ if (!parsed) {
733
+ stopSpinner(spinner, "");
734
+ console.log(`\n${marked(content)}\n`);
735
+ console.log(` ${t.warn}Note: The model returned malformed JSON that could not be parsed automatically.${t.reset}\n`);
736
+ conversationHistory.push({ role: "user", content: userPrompt });
737
+ conversationHistory.push({ role: "assistant", content });
738
+ askPrompt();
739
+ return;
740
+ }
741
+
742
+ const actions = parsed.filesToCreate || [];
743
+ const edits = parsed.filesToEdit || [];
744
+ const deletes = parsed.filesToDelete || [];
745
+ const runs = parsed.commandsToRun || [];
746
+ const message = parsed.message || "";
747
+
748
+ const hasFileOps = actions.length > 0 || edits.length > 0 || deletes.length > 0;
749
+
750
+ // ── delete files ──────────────────────────────────────────────────────────
751
+ if (deletes.length > 0) {
752
+ stopSpinner(spinner, "");
753
+ sectionHeader("removing files");
754
+
755
+ for (const delPath of deletes) {
756
+ try {
757
+ const stat = await fs.promises.stat(delPath);
758
+ if (stat.isDirectory()) await fs.promises.rm(delPath, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 });
759
+ else await fs.promises.unlink(delPath);
760
+ const rel = path.relative(process.cwd(), delPath).replace(/\\/g, "/");
761
+ printStatus("-", t.err, `${t.dim}${rel}${t.reset}`);
762
+ } catch (err) {
763
+ if (err.code !== "ENOENT") {
764
+ printStatus("x", t.err, `${delPath} ${t.muted}${err.message}${t.reset}`);
765
+ }
766
+ }
767
+ }
768
+ }
769
+
770
+ // ── create files ──────────────────────────────────────────────────────────
771
+ if (actions.length > 0) {
772
+ if (deletes.length === 0) stopSpinner(spinner, "");
773
+ sectionHeader("creating files");
774
+
775
+ for (const action of actions) {
776
+ try {
777
+ const dir = path.dirname(action.path);
778
+ if (dir && dir !== ".") await fs.promises.mkdir(dir, { recursive: true });
779
+ await fs.promises.writeFile(action.path, action.content);
780
+ _contextCache = null;
781
+ await showCreatedFile(action.path, action.content);
782
+ filesCreated++;
783
+ } catch (err) {
784
+ printStatus("x", t.err, `${action.path} ${t.muted}${err.message}${t.reset}`);
785
+ }
786
+ }
787
+ }
788
+
789
+ // ── edit files ────────────────────────────────────────────────────────────
790
+ if (edits.length > 0) {
791
+ if (deletes.length === 0 && actions.length === 0) stopSpinner(spinner, "");
792
+ sectionHeader("editing files");
793
+
794
+ for (const edit of edits) {
795
+ try {
796
+ let fileContent = await fs.promises.readFile(edit.path, "utf-8");
797
+ if (fileContent.includes(edit.search)) {
798
+ fileContent = fileContent.replace(edit.search, edit.replace);
799
+ await fs.promises.writeFile(edit.path, fileContent);
800
+ _contextCache = null;
801
+ await showDiff(edit.path, edit.search, edit.replace);
802
+ filesEdited++;
803
+ } else {
804
+ printStatus("!", t.warn, `${edit.path} ${t.muted}search block not found${t.reset}`);
805
+ }
806
+ } catch (err) {
807
+ printStatus("x", t.err, `${edit.path} ${t.muted}${err.message}${t.reset}`);
808
+ }
809
+ }
810
+ }
811
+
812
+ if (runs.length > 0) {
813
+ if (!hasFileOps) stopSpinner(spinner, "");
814
+ sectionHeader("running");
815
+
816
+ for (const cmd of runs) {
817
+ console.log(` ${t.muted}$ ${cmd}${t.reset}`);
818
+ const result = await runCommand(cmd, process.cwd());
819
+
820
+ if (result.stdout) {
821
+ result.stdout.split("\n").forEach((l) => console.log(` ${t.dim} ${l}${t.reset}`));
822
+ }
823
+
824
+ if (result.error || result.stderr) {
825
+ const errOutput = result.stderr || result.error?.message || "";
826
+ errOutput.split("\n").forEach((l) => console.log(` ${t.err} ${l}${t.reset}`));
827
+
828
+ console.log();
829
+ console.log(` ${t.muted}error detected — sending to model for fix${t.reset}`);
830
+
831
+ const fixSpinner = startSpinner("fixing");
832
+ const fixCtx = systemPrompt + "\n" + buildContext() + "\n\n";
833
+ const fixMsg = `The command "${cmd}" produced this error:\n${errOutput}\n`
834
+ + `Fix it by returning a JSON object with the necessary filesToEdit or filesToCreate.`;
835
+
836
+ try {
837
+ const fixRes = await puter.ai.chat(fixCtx + fixMsg, { model: MODEL });
838
+ let fixStr = fixRes["message"]["content"].trim();
839
+ if (fixStr.startsWith("```json")) fixStr = fixStr.slice(7);
840
+ else if (fixStr.startsWith("```")) fixStr = fixStr.slice(3);
841
+ if (fixStr.endsWith("```")) fixStr = fixStr.slice(0, -3);
842
+
843
+ const fixParsed = JSON.parse(fixStr.trim());
844
+ stopSpinner(fixSpinner, "");
845
+ sectionHeader("applying fix");
846
+
847
+ for (const fm of (fixParsed.filesToCreate || [])) {
848
+ const dir = path.dirname(fm.path);
849
+ if (dir && dir !== ".") await fs.promises.mkdir(dir, { recursive: true });
850
+ await fs.promises.writeFile(fm.path, fm.content);
851
+ await showCreatedFile(fm.path, fm.content);
852
+ }
853
+
854
+ for (const em of (fixParsed.filesToEdit || [])) {
855
+ try {
856
+ let fc = await fs.promises.readFile(em.path, "utf-8");
857
+ if (fc.includes(em.search)) {
858
+ fc = fc.replace(em.search, em.replace);
859
+ await fs.promises.writeFile(em.path, fc);
860
+ _contextCache = null;
861
+ await showDiff(em.path, em.search, em.replace);
862
+ }
863
+ } catch { /* skip */ }
864
+ }
865
+
866
+ printStatus("", t.ok, "fix applied");
867
+ } catch (fixErr) {
868
+ stopSpinner(fixSpinner, "");
869
+ printStatus("x", t.err, `auto-fix failed ${t.muted}${fixErr.message || JSON.stringify(fixErr)}${t.reset}`);
870
+ }
871
+ } else {
872
+ console.log(` ${t.ok} ok${t.reset}`);
873
+ }
874
+ }
875
+ }
876
+
877
+ if (hasFileOps || runs.length > 0) {
878
+ console.log();
879
+ if (message) {
880
+ console.log(` ${t.muted}${message}${t.reset}`);
881
+ }
882
+ const summary = [
883
+ ...actions.map((a) => `created ${path.relative(process.cwd(), a.path).replace(/\\/g, "/")}`),
884
+ ...edits.map((e) => `edited ${path.relative(process.cwd(), e.path).replace(/\\/g, "/")}`),
885
+ ...deletes.map((d) => `deleted ${d}`),
886
+ ].join(", ");
887
+ conversationHistory.push({ role: "user", content: userPrompt });
888
+ conversationHistory.push({ role: "assistant", content: `[${summary}] ${message}` });
889
+ } else if (message) {
890
+ stopSpinner(spinner);
891
+ console.log(`\n${marked(message)}\n`);
892
+ conversationHistory.push({ role: "user", content: userPrompt });
893
+ conversationHistory.push({ role: "assistant", content: message });
894
+ } else {
895
+ stopSpinner(spinner);
896
+ }
897
+
898
+ } catch (err) {
899
+ stopSpinner(spinner, ` ${t.err}error ${t.muted}${err.message || JSON.stringify(err)}${t.reset}`);
900
+ } finally {
901
+ askPrompt();
902
+ }
903
+ }
904
+
905
+
906
+ function getGitBranch() {
907
+ try {
908
+ return require("child_process")
909
+ .execSync("git rev-parse --abbrev-ref HEAD", { stdio: ["pipe", "pipe", "pipe"] })
910
+ .toString().trim();
911
+ } catch { return null; }
912
+ }
913
+
914
+ function getGitStatus() {
915
+ try {
916
+ const out = require("child_process")
917
+ .execSync("git status --porcelain", { stdio: ["pipe", "pipe", "pipe"] })
918
+ .toString().trim();
919
+ if (!out) return "clean";
920
+ const lines = out.split("\n");
921
+ const mod = lines.filter(l => l.startsWith(" M") || l.startsWith("M")).length;
922
+ const add = lines.filter(l => l.startsWith("?")).length;
923
+ const parts = [];
924
+ if (mod) parts.push(`${mod} modified`);
925
+ if (add) parts.push(`${add} untracked`);
926
+ return parts.join(" ") || `${lines.length} changed`;
927
+ } catch { return null; }
928
+ }
929
+
930
+ function getNodeVersion() {
931
+ return process.version;
932
+ }
933
+
934
+ function getNow() {
935
+ const d = new Date();
936
+ const pad = (n) => String(n).padStart(2, "0");
937
+ return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
938
+ }
939
+
940
+ function getDate() {
941
+ const d = new Date();
942
+ return d.toLocaleDateString("en-GB", { weekday: "short", day: "2-digit", month: "short", year: "numeric" });
943
+ }
944
+
945
+
946
+ async function start() {
947
+ console.clear();
948
+
949
+ const w = cols();
950
+ const branch = getGitBranch();
951
+ const status = getGitStatus();
952
+ const cwd = process.cwd();
953
+ const node = getNodeVersion();
954
+ const now = getNow();
955
+ const date = getDate();
956
+
957
+ console.log();
958
+ console.log(rule("─"));
959
+ console.log();
960
+
961
+ const titleLeft = `${t.violet}${t.bold}ATOM${t.reset} ${t.muted}coding agent${t.reset}`;
962
+ const titleRight = `${t.violetDim}v${VERSION}${t.reset}`;
963
+ const titleRightVis = `v${VERSION}`;
964
+ const titleGap = w - "ATOM coding agent".length - titleRightVis.length;
965
+ console.log(titleLeft + " ".repeat(Math.max(0, titleGap)) + titleRight);
966
+
967
+ console.log();
968
+
969
+ const rows = [
970
+ [`model`, MODEL_LABEL],
971
+ [`node`, node],
972
+ [`cwd`, cwd.length > 48 ? "..." + cwd.slice(-45) : cwd],
973
+ branch ? [`branch`, branch] : null,
974
+ branch && status ? [`status`, status] : null,
975
+ [`date`, date],
976
+ [`time`, now],
977
+ ].filter(Boolean);
978
+
979
+ for (const [label, value] of rows) {
980
+ const l = `${t.violetDim}${label.padEnd(10)}${t.reset}`;
981
+ const v = `${t.muted}${value}${t.reset}`;
982
+ console.log(l + v);
983
+ }
984
+
985
+ console.log();
986
+ console.log(rule("─"));
987
+ console.log();
988
+
989
+ console.log(`${t.violetDim}ready${t.reset}`);
990
+
991
+ askPrompt();
992
+ }
993
+
994
+ start();
package/npm ADDED
File without changes
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "cli-atom",
3
+ "version": "0.2.0",
4
+ "description": "ATOM - Coding Agent",
5
+ "license": "ISC",
6
+ "author": "Redwxll",
7
+ "type": "module",
8
+ "main": "index.js",
9
+ "bin": {
10
+ "atom": "./index.js"
11
+ },
12
+ "scripts": {
13
+ "test": "mocha test/**/*.js"
14
+ },
15
+ "dependencies": {
16
+ "@heyputer/puter.js": "^2.5.3",
17
+ "@inquirer/prompts": "^8.5.2",
18
+ "figlet": "^1.11.0",
19
+ "marked": "^15.0.12",
20
+ "marked-terminal": "^7.3.0"
21
+ },
22
+ "devDependencies": {
23
+ "mocha": "^11.7.6"
24
+ }
25
+ }