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.
Files changed (2) hide show
  1. package/atom.js +230 -368
  2. 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;203m",
28
- violetDim: "\x1b[38;5;160m",
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;167m",
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
- // ─── config management ───────────────────────────────────────────────────────
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
- return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
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 commands (login / logout) ───────────────────────────────────────────
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.violet}${t.bold}ATOM${t.reset} ${t.muted}login${t.reset}`);
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
- // ─── puter init ──────────────────────────────────────────────────────────────
111
-
105
+ // ─── PUTER INIT ──────────────────────────────────────────────────────────────
112
106
  let config = getConfig();
113
107
  if (!config.apiKey) {
114
- console.log();
115
- console.log(` ${t.err}No API key found.${t.reset}`);
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
- You MUST respond EXCLUSIVELY with a valid JSON object. Do NOT wrap the JSON in any other text, just output the JSON object.
132
- 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.
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": "A concise explanation of what you are doing (optional)",
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
- "path/to/file_or_folder"
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
- - Keep "message" concise and normal.
159
- - If you don't need to do an action, leave the array empty \`[]\` or omit it.
160
- - Write compact and efficient code.
161
- - 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".`;
162
172
 
163
173
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
164
174
 
165
- function cols() {
166
- return process.stdout.columns || 80;
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
- const MAX_FILES = 50;
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 unreadable files */ }
206
+ } catch { /* skip */ }
212
207
  }
213
208
  }
214
- } catch { /* skip unreadable dirs */ }
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
- if (fs.statSync(f.path).mtimeMs !== snap[f.path]) return true;
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
- if (_contextCache && !snapshotChanged(files, _contextSnapshot)) {
245
- 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;
246
243
  }
247
244
 
248
- let ctx = `\n--- PROJECT CONTEXT (${cwd}) ---\n`;
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
- 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;
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
- const SPINNER_FRAMES = ["·", "·", "·", "·", "·", "·", "·", "·", "·", "·"].map(
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(`${t.violet}${label}${t.reset}`);
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}+${t.reset} ${t.accent}${relPath}${t.reset} ${t.muted}${lines.length} lines${t.reset}`);
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, 8);
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(12);
322
+ await sleep(10);
324
323
  }
325
- if (lines.length > 8) {
326
- 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}`);
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.accent}${relPath}${t.reset}`);
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}- ${t.dim}${line}${t.reset}`);
338
- await sleep(18);
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}+ ${line}${t.reset}`);
342
- await sleep(18);
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
- // ─── slash commands definition ────────────────────────────────────────────────
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: "quit", desc: "Exit the CLI" },
356
+ { name: "exit", desc: "Exit the CLI" },
359
357
  ];
360
358
 
359
+ // ─── INTERACTIVE PROMPT (FIXED & ROBUST) ─────────────────────────────────────
361
360
  function askPrompt() {
362
- console.log();
363
- console.log(rule());
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 menuLineCount = 0;
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 getFilteredCommands() {
402
- const query = input.slice(1).toLowerCase(); // strip the leading /
403
- 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
+ }
404
385
  }
405
386
 
406
- function renderMenu() {
407
- const currentCols = cols();
408
- menuItems = getFilteredCommands();
387
+ function drawPrompt() {
388
+ clearPromptAndMenu();
409
389
 
410
- // clamp index
411
- if (menuIndex >= menuItems.length) menuIndex = 0;
390
+ const prefix = ` ${t.violet}❯${t.reset} `;
391
+ const visiblePrefixLen = 4;
412
392
 
413
- // clear previous menu lines
414
- if (menuLineCount > 0) {
415
- for (let i = 0; i < menuLineCount; i++) {
416
- process.stdout.write("\x1b[1A\x1b[2K");
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
- const nameWidth = Math.max(...menuItems.map(c => c.name.length)) + 2;
426
- let out = "";
427
- for (let i = 0; i < menuItems.length; i++) {
428
- const item = menuItems[i];
429
- const isSelected = i === menuIndex;
430
- const namePad = ("/" + item.name).padEnd(nameWidth);
431
- if (isSelected) {
432
- out += `\x1b[48;5;236m${t.violet}${t.bold} ${namePad}${t.reset}\x1b[48;5;236m ${t.accent}${item.desc}${t.reset}`;
433
- } else {
434
- 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++;
435
416
  }
436
- out += " ".repeat(Math.max(0, currentCols - namePad.length - item.desc.length - 4)) + "\n";
437
417
  }
438
418
 
439
- // count indicator
440
- out += `${t.subtle}(${menuIndex + 1}/${menuItems.length})${t.reset}\n`;
441
- menuLineCount = menuItems.length + 1;
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
- function clearMenu() {
447
- if (menuLineCount > 0) {
448
- for (let i = 0; i < menuLineCount; i++) {
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
- function renderBox() {
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() { renderBox(); }
431
+ function onResize() { drawPrompt(); }
461
432
  process.stdout.on("resize", onResize);
462
433
 
463
- process.stdout.write(drawBox("insert your instruction...", true));
464
- process.stdout.write("\x1b[?25l");
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
- clearMenu();
480
- 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}`);
481
451
  handleInput(value);
482
452
  }
483
453
 
484
454
  function onData(key) {
485
- // Ctrl+C
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
- renderMenu();
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
- renderMenu();
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
- process.stdout.write("\x1b[3A\r" + drawBox(input, false));
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
- // select highlighted command
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
- clearMenu();
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 = input.slice(0, -1);
546
- if (input === "" || input === "/") {
547
- if (input === "") {
548
- menuActive = false;
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
- // open menu when "/" is first char
570
- if (input === "/") {
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
- console.log(` ${t.violet}${t.bold}Available commands${t.reset}`);
614
- console.log(` ${t.violetDim}${"─".repeat(30)}${t.reset}`);
615
- console.log(` ${t.violetDim}/help${t.reset} ${t.muted}show this help message${t.reset}`);
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); // reinitialize puter with new 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 > 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
+ }
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 that could not be parsed automatically.${t.reset}\n`);
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, maxRetries: 5, retryDelay: 200 });
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}\n`
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
- return require("child_process")
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
- const d = new Date();
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 = getNodeVersion();
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 = `${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}`;
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 = `${t.violetDim}${label.padEnd(10)}${t.reset}`;
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(`${t.violetDim}ready${t.reset}`);
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.7",
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
+ }