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.
Files changed (2) hide show
  1. package/atom.js +233 -369
  2. 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;203m",
26
- violetDim: "\x1b[38;5;160m",
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;167m",
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 = "0.2.6";
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
- // ─── config management ───────────────────────────────────────────────────────
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
- return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
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 commands (login / logout) ───────────────────────────────────────────
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.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();
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
- // ─── puter init ──────────────────────────────────────────────────────────────
109
-
105
+ // ─── PUTER INIT ──────────────────────────────────────────────────────────────
110
106
  let config = getConfig();
111
107
  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();
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
- 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 \\").
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": "A concise explanation of what you are doing (optional)",
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
- "path/to/file_or_folder"
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
- - 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.`;
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
- 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
-
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
- const MAX_FILES = 50;
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 unreadable files */ }
206
+ } catch { /* skip */ }
210
207
  }
211
208
  }
212
- } catch { /* skip unreadable dirs */ }
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
- if (fs.statSync(f.path).mtimeMs !== snap[f.path]) return true;
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
- if (_contextCache && !snapshotChanged(files, _contextSnapshot)) {
243
- return _contextCache;
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
- let ctx = `\n--- PROJECT CONTEXT (${cwd}) ---\n`;
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
- for (const f of files) {
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
- const SPINNER_FRAMES = ["·", "·", "·", "·", "·", "·", "·", "·", "·", "·"].map(
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(`${t.violet}${label}${t.reset}`);
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}+${t.reset} ${t.accent}${relPath}${t.reset} ${t.muted}${lines.length} lines${t.reset}`);
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, 8);
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(12);
322
+ await sleep(10);
322
323
  }
323
- if (lines.length > 8) {
324
- console.log(` ${t.subtle} ${t.reset} ${t.muted}... ${lines.length - 8} more lines${t.reset}`);
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.accent}${relPath}${t.reset}`);
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}- ${t.dim}${line}${t.reset}`);
336
- await sleep(18);
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}+ ${line}${t.reset}`);
340
- await sleep(18);
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
- // ─── slash commands definition ────────────────────────────────────────────────
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: "quit", desc: "Exit the CLI" },
356
+ { name: "exit", desc: "Exit the CLI" },
357
357
  ];
358
358
 
359
+ // ─── INTERACTIVE PROMPT (FIXED & ROBUST) ─────────────────────────────────────
359
360
  function askPrompt() {
360
- console.log();
361
- console.log(rule());
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 menuLineCount = 0;
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 getFilteredCommands() {
400
- const query = input.slice(1).toLowerCase(); // strip the leading /
401
- return SLASH_COMMANDS.filter(c => c.name.startsWith(query));
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 renderMenu() {
405
- const currentCols = cols();
406
- menuItems = getFilteredCommands();
387
+ function drawPrompt() {
388
+ clearPromptAndMenu();
407
389
 
408
- // clamp index
409
- if (menuIndex >= menuItems.length) menuIndex = 0;
390
+ const prefix = ` ${t.violet}❯${t.reset} `;
391
+ const visiblePrefixLen = 4;
410
392
 
411
- // clear previous menu lines
412
- if (menuLineCount > 0) {
413
- for (let i = 0; i < menuLineCount; i++) {
414
- process.stdout.write("\x1b[1A\x1b[2K");
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
- const nameWidth = Math.max(...menuItems.map(c => c.name.length)) + 2;
424
- let out = "";
425
- for (let i = 0; i < menuItems.length; i++) {
426
- const item = menuItems[i];
427
- const isSelected = i === menuIndex;
428
- const namePad = ("/" + item.name).padEnd(nameWidth);
429
- if (isSelected) {
430
- out += `\x1b[48;5;236m${t.violet}${t.bold} ${namePad}${t.reset}\x1b[48;5;236m ${t.accent}${item.desc}${t.reset}`;
431
- } else {
432
- out += ` ${t.violetDim}${namePad}${t.reset} ${t.muted}${item.desc}${t.reset}`;
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
- // count indicator
438
- out += `${t.subtle}(${menuIndex + 1}/${menuItems.length})${t.reset}\n`;
439
- menuLineCount = menuItems.length + 1;
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
- function clearMenu() {
445
- if (menuLineCount > 0) {
446
- for (let i = 0; i < menuLineCount; i++) {
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
- function renderBox() {
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() { renderBox(); }
431
+ function onResize() { drawPrompt(); }
459
432
  process.stdout.on("resize", onResize);
460
433
 
461
- process.stdout.write(drawBox("insert your instruction...", true));
462
- process.stdout.write("\x1b[?25l");
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
- clearMenu();
478
- process.stdout.write("\x1b[3A\x1b[2K\x1b[1B\x1b[2K\x1b[1B\x1b[2K\x1b[1A\r");
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
- // Ctrl+C
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
- renderMenu();
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
- renderMenu();
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
- process.stdout.write("\x1b[3A\r" + drawBox(input, false));
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
- // select highlighted command
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
- clearMenu();
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 = input.slice(0, -1);
544
- if (input === "" || input === "/") {
545
- if (input === "") {
546
- menuActive = false;
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
- // open menu when "/" is first char
568
- if (input === "/") {
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
- console.log(` ${t.violet}${t.bold}Available commands${t.reset}`);
612
- console.log(` ${t.violetDim}${"─".repeat(30)}${t.reset}`);
613
- console.log(` ${t.violetDim}/help${t.reset} ${t.muted}show this help message${t.reset}`);
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); // reinitialize puter with new 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 > 6) conversationHistory = conversationHistory.slice(-6);
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 that could not be parsed automatically.${t.reset}\n`);
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, maxRetries: 5, retryDelay: 200 });
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}\n`
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
- return require("child_process")
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
- const d = new Date();
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 = getNodeVersion();
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 = `${t.violet}${t.bold}ATOM${t.reset} ${t.muted}coding agent${t.reset}`;
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 = `${t.violetDim}${label.padEnd(10)}${t.reset}`;
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(`${t.violetDim}ready${t.reset}`);
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.6",
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
+ }