cli-atom 0.2.7 → 0.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/atom.js 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,109 @@ 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) ─────────────────────────────────────
360
+ // ─── INTERACTIVE PROMPT (FIXED & ROBUST) ─────────────────────────────────────
361
361
  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}`;
362
+ const sessionStats = `${t.ok}${filesCreated}${t.reset} created ${t.warn}${filesEdited}${t.reset} edited`;
363
+ const left = `\n ${t.subtle}${sessionStats}${t.reset}`;
367
364
  const right = `${t.muted}${MODEL_LABEL} ${promptCount} req ${elapsed()}${t.reset}`;
368
365
  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;
366
+ const gap = cols() - left.replace(/\x1b\[[0-9;]*m/g, "").length - rightVisible.length;
372
367
  console.log(left + " ".repeat(Math.max(0, gap)) + right);
373
- console.log();
374
368
 
375
369
  let input = "";
376
370
  let menuActive = false;
377
371
  let menuIndex = 0;
378
372
  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);
373
+ let lastMenuLines = 0;
374
+ let lastVisibleLen = 0;
375
+
376
+ function clearPromptAndMenu() {
377
+ // 1. Effacer le menu s'il existe
378
+ if (lastMenuLines > 0) {
379
+ for (let i = 0; i < lastMenuLines; i++) {
380
+ process.stdout.write("\x1b[1B\x1b[2K"); // Descend et efface
381
+ }
382
+ for (let i = 0; i < lastMenuLines; i++) {
383
+ process.stdout.write("\x1b[1A"); // Remonte
384
+ }
385
+ lastMenuLines = 0;
393
386
  }
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
- }
400
387
 
401
- function getFilteredCommands() {
402
- const query = input.slice(1).toLowerCase(); // strip the leading /
403
- return SLASH_COMMANDS.filter(c => c.name.startsWith(query));
388
+ // 2. Effacer le prompt (gère le retour à la ligne automatique)
389
+ const linesToClear = Math.max(0, Math.floor((lastVisibleLen - 1) / cols()));
390
+ process.stdout.write("\x1b[2K\r"); // Efface la ligne actuelle
391
+ for (let i = 0; i < linesToClear; i++) {
392
+ process.stdout.write("\x1b[1A\x1b[2K\r"); // Monte d'une ligne et efface
393
+ }
404
394
  }
405
395
 
406
- function renderMenu() {
407
- const currentCols = cols();
408
- menuItems = getFilteredCommands();
396
+ function drawPrompt() {
397
+ clearPromptAndMenu();
409
398
 
410
- // clamp index
411
- if (menuIndex >= menuItems.length) menuIndex = 0;
399
+ const prefix = ` ${t.violet}❯${t.reset} `;
400
+ const visiblePrefixLen = 4;
401
+ let baseText = "";
402
+ let currentVisibleLen = 0;
412
403
 
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;
404
+ if (input.length === 0) {
405
+ baseText = `${t.muted}Insert your instruction... (type / for commands)${t.reset}`;
406
+ currentVisibleLen = visiblePrefixLen + "Insert your instruction... (type / for commands)".length;
407
+ } else {
408
+ baseText = `${t.accent}${input}${t.reset}`;
409
+ currentVisibleLen = visiblePrefixLen + input.length;
423
410
  }
424
411
 
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}`;
412
+ process.stdout.write(prefix + baseText);
413
+ lastVisibleLen = currentVisibleLen;
414
+
415
+ let currentMenuLines = 0;
416
+ if (menuActive) {
417
+ const query = input.slice(1).toLowerCase();
418
+ menuItems = SLASH_COMMANDS.filter(c => c.name.startsWith(query));
419
+ if (menuIndex >= menuItems.length) menuIndex = 0;
420
+
421
+ for (let i = 0; i < menuItems.length; i++) {
422
+ const item = menuItems[i];
423
+ const isSelected = i === menuIndex;
424
+ const namePad = ("/" + item.name).padEnd(12);
425
+ process.stdout.write(`\r\n`); // S'assure d'aller à la ligne proprement
426
+ if (isSelected) {
427
+ process.stdout.write(` ${t.violet}${t.bold}❯ ${namePad}${t.reset} ${t.accent}${item.desc}${t.reset}`);
428
+ } else {
429
+ process.stdout.write(` ${t.violetDim}${namePad}${t.reset} ${t.muted}${item.desc}${t.reset}`);
430
+ }
431
+ currentMenuLines++;
435
432
  }
436
- out += " ".repeat(Math.max(0, currentCols - namePad.length - item.desc.length - 4)) + "\n";
437
433
  }
438
434
 
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
- }
445
-
446
- function clearMenu() {
447
- if (menuLineCount > 0) {
448
- for (let i = 0; i < menuLineCount; i++) {
449
- process.stdout.write("\x1b[1A\x1b[2K");
435
+ // Repositionner le curseur si le menu est ouvert
436
+ if (currentMenuLines > 0) {
437
+ for (let i = 0; i < currentMenuLines; i++) {
438
+ process.stdout.write("\x1b[1A"); // Remonte à la ligne du prompt
450
439
  }
451
- menuLineCount = 0;
440
+ const cursorCol = (currentVisibleLen % cols()) || cols();
441
+ process.stdout.write(`\x1b[${cursorCol}G`); // Définit la colonne exacte
452
442
  }
453
- }
454
443
 
455
- function renderBox() {
456
- process.stdout.write("\x1b[3A\r" + drawBox(input || "", input.length === 0));
457
- if (menuActive) renderMenu();
444
+ lastMenuLines = currentMenuLines;
458
445
  }
459
446
 
460
- function onResize() { renderBox(); }
447
+ function onResize() { drawPrompt(); }
461
448
  process.stdout.on("resize", onResize);
462
449
 
463
- process.stdout.write(drawBox("insert your instruction...", true));
464
- process.stdout.write("\x1b[?25l");
450
+ process.stdout.write("\x1b[?25h");
451
+ drawPrompt();
465
452
 
466
453
  process.stdin.setRawMode(true);
467
454
  process.stdin.resume();
@@ -472,57 +459,44 @@ function askPrompt() {
472
459
  process.stdin.pause();
473
460
  process.stdin.removeListener("data", onData);
474
461
  process.stdout.removeListener("resize", onResize);
475
- process.stdout.write("\x1b[?25h");
476
462
  }
477
463
 
478
464
  function submitInput(value) {
479
- clearMenu();
480
- process.stdout.write("\x1b[3A\x1b[2K\x1b[1B\x1b[2K\x1b[1B\x1b[2K\x1b[1A\r");
465
+ clearPromptAndMenu();
466
+ console.log(` ${t.violet}❯${t.reset} ${t.accent}${value}${t.reset}`);
481
467
  handleInput(value);
482
468
  }
483
469
 
484
470
  function onData(key) {
485
- // Ctrl+C
486
- if (key === "\u0003") { process.stdout.write("\x1b[?25h"); process.exit(); }
471
+ if (key === "\u0003") { process.exit(); }
487
472
 
488
- // Arrow up
489
473
  if (key === "\x1b[A") {
490
474
  if (menuActive && menuItems.length > 0) {
491
475
  menuIndex = (menuIndex - 1 + menuItems.length) % menuItems.length;
492
- renderMenu();
476
+ drawPrompt();
493
477
  }
494
478
  return;
495
479
  }
496
480
 
497
- // Arrow down
498
481
  if (key === "\x1b[B") {
499
482
  if (menuActive && menuItems.length > 0) {
500
483
  menuIndex = (menuIndex + 1) % menuItems.length;
501
- renderMenu();
484
+ drawPrompt();
502
485
  }
503
486
  return;
504
487
  }
505
488
 
506
- // Tab — autocomplete selected menu item
507
489
  if (key === "\t") {
508
490
  if (menuActive && menuItems.length > 0) {
509
491
  input = "/" + menuItems[menuIndex].name;
510
- process.stdout.write("\x1b[3A\r" + drawBox(input, false));
511
- renderMenu();
492
+ drawPrompt();
512
493
  }
513
494
  return;
514
495
  }
515
496
 
516
- // Enter
517
497
  if (key === "\r" || key === "\n") {
518
498
  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;
499
+ input = "/" + menuItems[menuIndex].name;
526
500
  }
527
501
  if (!input.trim()) return;
528
502
  cleanup();
@@ -530,101 +504,56 @@ function askPrompt() {
530
504
  return;
531
505
  }
532
506
 
533
- // Escape — close menu
534
507
  if (key === "\x1b") {
535
508
  if (menuActive) {
536
509
  menuActive = false;
537
- clearMenu();
538
- process.stdout.write("\x1b[3A\r" + drawBox(input, false));
510
+ drawPrompt();
539
511
  }
540
512
  return;
541
513
  }
542
514
 
543
- // Backspace
544
515
  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));
516
+ if (input.length > 0) {
517
+ input = input.slice(0, -1);
518
+ menuActive = input.startsWith("/");
519
+ drawPrompt();
561
520
  }
562
521
  return;
563
522
  }
564
523
 
565
- // Printable chars
566
524
  if (key.charCodeAt(0) >= 32) {
567
525
  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));
526
+ menuActive = input.startsWith("/");
527
+ menuIndex = 0;
528
+ drawPrompt();
591
529
  }
592
530
  }
593
531
 
594
532
  process.stdin.on("data", onData);
595
533
  }
596
534
 
535
+ // ─── INPUT HANDLER ───────────────────────────────────────────────────────────
597
536
  async function handleInput(raw) {
598
537
  const userPrompt = raw.trim();
599
538
 
600
539
  if (!userPrompt) { askPrompt(); return; }
601
540
 
602
541
  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();
542
+ console.log(`\n ${t.muted}Session ended ${filesCreated} created ${filesEdited} edited ${elapsed()}${t.reset}\n`);
606
543
  process.exit(0);
607
544
  return;
608
545
  }
609
546
 
610
- // ─── /help ───────────────────────────────────────────────────────────────
611
547
  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}`);
548
+ console.log(`\n ${t.violet}${t.bold}Available commands${t.reset}`);
549
+ SLASH_COMMANDS.forEach(c => {
550
+ console.log(` ${t.violetDim}/${c.name.padEnd(10)}${t.reset} ${t.muted}${c.desc}${t.reset}`);
551
+ });
622
552
  console.log();
623
553
  askPrompt();
624
554
  return;
625
555
  }
626
556
 
627
- // ─── /model ──────────────────────────────────────────────────────────────
628
557
  if (userPrompt.toLowerCase().startsWith("/model")) {
629
558
  const parts = userPrompt.split(" ");
630
559
  if (parts.length === 1) {
@@ -643,65 +572,63 @@ async function handleInput(raw) {
643
572
  if (selected) {
644
573
  MODEL = selected.id;
645
574
  MODEL_LABEL = selected.label;
646
- console.log(`\n ${t.ok}Model switched to ${t.bold}${MODEL_LABEL}${t.reset}\n`);
575
+ console.log(`\n ${t.ok}Model switched to ${t.bold}${MODEL_LABEL}${t.reset}\n`);
647
576
  } else {
648
- console.log(`\n ${t.err}Model not found. Type '/model' to see the list.${t.reset}\n`);
577
+ console.log(`\n ${t.err}Model not found. Type '/model' to see the list.${t.reset}\n`);
649
578
  }
650
579
  askPrompt();
651
580
  return;
652
581
  }
653
582
 
654
- // ─── /key ────────────────────────────────────────────────────────────────
655
583
  if (userPrompt.toLowerCase() === "/key") {
656
584
  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`);
585
+ console.log(`\n ${t.violet}API Key${t.reset}\n ${t.muted}current: ${masked}${t.reset}\n`);
660
586
  askPrompt();
661
587
  return;
662
588
  }
663
589
 
664
- // ─── /logout ─────────────────────────────────────────────────────────────
665
590
  if (userPrompt.toLowerCase() === "/logout") {
666
591
  try { fs.unlinkSync(CONFIG_FILE); } catch { }
667
592
  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`);
593
+ console.log(`\n ${t.ok}Logged out. API key removed.${t.reset}\n`);
670
594
  askPrompt();
671
595
  return;
672
596
  }
673
597
 
674
- // ─── /login ──────────────────────────────────────────────────────────────
675
598
  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
-
599
+ console.log(`\n ${t.violet}${t.bold}Login${t.reset}`);
681
600
  process.stdin.setRawMode(false);
682
601
  process.stdin.resume();
683
602
  const key = await promptInput(` ${t.violetDim}Paste your API key: ${t.reset}`);
684
603
 
685
604
  if (!key) {
686
- console.log(`\n ${t.err}No key provided.${t.reset}\n`);
605
+ console.log(`\n ${t.err}No key provided.${t.reset}\n`);
687
606
  askPrompt();
688
607
  return;
689
608
  }
690
609
 
691
610
  setConfig({ apiKey: key });
692
611
  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`);
612
+ puter = init(key);
613
+ console.log(`\n ${t.ok}API key saved and applied!${t.reset}\n`);
696
614
  askPrompt();
697
615
  return;
698
616
  }
699
617
 
700
618
  promptCount++;
701
619
 
702
- if (conversationHistory.length > 6) conversationHistory = conversationHistory.slice(-6);
620
+ if (conversationHistory.length > 4) {
621
+ const old = conversationHistory.slice(0, -2);
622
+ const summary = old.map(m =>
623
+ `${m.role === "user" ? "U" : "A"}: ${m.content.slice(0, 120)}${m.content.length > 120 ? "…" : ""}`
624
+ ).join("\n");
625
+ conversationHistory = [
626
+ { role: "user", content: `[Earlier conversation summary:\n${summary}]` },
627
+ ...conversationHistory.slice(-2)
628
+ ];
629
+ }
703
630
 
704
- const projectContext = buildContext();
631
+ const projectContext = buildContext(userPrompt);
705
632
  let fullPrompt = systemPrompt + "\n" + projectContext + "\n";
706
633
 
707
634
  if (conversationHistory.length > 0) {
@@ -714,7 +641,6 @@ async function handleInput(raw) {
714
641
 
715
642
  fullPrompt += `User prompt: ${userPrompt}`;
716
643
 
717
- console.log();
718
644
  const spinner = startSpinner("thinking");
719
645
 
720
646
  try {
@@ -735,21 +661,18 @@ async function handleInput(raw) {
735
661
  const cleanStr = jsonStr.replace(/,\s*}/g, '}').replace(/,\s*]/g, ']');
736
662
  parsed = JSON.parse(cleanStr);
737
663
  } catch (e2) {
738
- try {
739
- parsed = eval("(" + jsonStr + ")");
740
- } catch (e3) {
741
- /* plain text response */
742
- }
664
+ try { parsed = eval("(" + jsonStr + ")"); } catch (e3) { /* plain text */ }
743
665
  }
744
666
  }
745
667
 
746
668
  if (!parsed) {
747
669
  stopSpinner(spinner, "");
748
670
  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`);
671
+ console.log(` ${t.warn}Note: The model returned malformed JSON.${t.reset}\n`);
750
672
  conversationHistory.push({ role: "user", content: userPrompt });
751
673
  conversationHistory.push({ role: "assistant", content });
752
- askPrompt();
674
+ // FIX: Removed askPrompt() here to prevent multiple listeners from being registered.
675
+ // The finally block below will handle calling askPrompt().
753
676
  return;
754
677
  }
755
678
 
@@ -761,31 +684,25 @@ async function handleInput(raw) {
761
684
 
762
685
  const hasFileOps = actions.length > 0 || edits.length > 0 || deletes.length > 0;
763
686
 
764
- // ── delete files ──────────────────────────────────────────────────────────
765
687
  if (deletes.length > 0) {
766
688
  stopSpinner(spinner, "");
767
689
  sectionHeader("removing files");
768
-
769
690
  for (const delPath of deletes) {
770
691
  try {
771
692
  const stat = await fs.promises.stat(delPath);
772
- if (stat.isDirectory()) await fs.promises.rm(delPath, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 });
693
+ if (stat.isDirectory()) await fs.promises.rm(delPath, { recursive: true, force: true });
773
694
  else await fs.promises.unlink(delPath);
774
695
  const rel = path.relative(process.cwd(), delPath).replace(/\\/g, "/");
775
696
  printStatus("-", t.err, `${t.dim}${rel}${t.reset}`);
776
697
  } catch (err) {
777
- if (err.code !== "ENOENT") {
778
- printStatus("x", t.err, `${delPath} ${t.muted}${err.message}${t.reset}`);
779
- }
698
+ if (err.code !== "ENOENT") printStatus("x", t.err, `${delPath} ${t.muted}${err.message}${t.reset}`);
780
699
  }
781
700
  }
782
701
  }
783
702
 
784
- // ── create files ──────────────────────────────────────────────────────────
785
703
  if (actions.length > 0) {
786
704
  if (deletes.length === 0) stopSpinner(spinner, "");
787
705
  sectionHeader("creating files");
788
-
789
706
  for (const action of actions) {
790
707
  try {
791
708
  const dir = path.dirname(action.path);
@@ -800,11 +717,9 @@ async function handleInput(raw) {
800
717
  }
801
718
  }
802
719
 
803
- // ── edit files ────────────────────────────────────────────────────────────
804
720
  if (edits.length > 0) {
805
721
  if (deletes.length === 0 && actions.length === 0) stopSpinner(spinner, "");
806
722
  sectionHeader("editing files");
807
-
808
723
  for (const edit of edits) {
809
724
  try {
810
725
  let fileContent = await fs.promises.readFile(edit.path, "utf-8");
@@ -825,27 +740,19 @@ async function handleInput(raw) {
825
740
 
826
741
  if (runs.length > 0) {
827
742
  if (!hasFileOps) stopSpinner(spinner, "");
828
- sectionHeader("running");
829
-
743
+ sectionHeader("running commands");
830
744
  for (const cmd of runs) {
831
745
  console.log(` ${t.muted}$ ${cmd}${t.reset}`);
832
746
  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
-
747
+ if (result.stdout) result.stdout.split("\n").forEach((l) => console.log(` ${t.dim} ${l}${t.reset}`));
838
748
  if (result.error || result.stderr) {
839
749
  const errOutput = result.stderr || result.error?.message || "";
840
750
  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}`);
751
+ console.log(`\n ${t.muted}error detected — sending to model for fix${t.reset}`);
844
752
 
845
753
  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.`;
754
+ const fixCtx = systemPrompt + "\n" + buildContext(cmd) + "\n\n";
755
+ const fixMsg = `The command "${cmd}" produced this error:\n${errOutput}\nFix it by returning a JSON object with the necessary filesToEdit or filesToCreate.`;
849
756
 
850
757
  try {
851
758
  const fixRes = await puter.ai.chat(fixCtx + fixMsg, { model: MODEL });
@@ -862,9 +769,9 @@ async function handleInput(raw) {
862
769
  const dir = path.dirname(fm.path);
863
770
  if (dir && dir !== ".") await fs.promises.mkdir(dir, { recursive: true });
864
771
  await fs.promises.writeFile(fm.path, fm.content);
772
+ _contextCache = null;
865
773
  await showCreatedFile(fm.path, fm.content);
866
774
  }
867
-
868
775
  for (const em of (fixParsed.filesToEdit || [])) {
869
776
  try {
870
777
  let fc = await fs.promises.readFile(em.path, "utf-8");
@@ -876,23 +783,19 @@ async function handleInput(raw) {
876
783
  }
877
784
  } catch { /* skip */ }
878
785
  }
879
-
880
- printStatus("", t.ok, "fix applied");
786
+ printStatus("✓", t.ok, "fix applied");
881
787
  } catch (fixErr) {
882
788
  stopSpinner(fixSpinner, "");
883
789
  printStatus("x", t.err, `auto-fix failed ${t.muted}${fixErr.message || JSON.stringify(fixErr)}${t.reset}`);
884
790
  }
885
791
  } else {
886
- console.log(` ${t.ok} ok${t.reset}`);
792
+ console.log(` ${t.ok} ok${t.reset}`);
887
793
  }
888
794
  }
889
795
  }
890
796
 
891
797
  if (hasFileOps || runs.length > 0) {
892
- console.log();
893
- if (message) {
894
- console.log(` ${t.muted}${message}${t.reset}`);
895
- }
798
+ if (message) console.log(`\n ${t.muted}${message}${t.reset}`);
896
799
  const summary = [
897
800
  ...actions.map((a) => `created ${path.relative(process.cwd(), a.path).replace(/\\/g, "/")}`),
898
801
  ...edits.map((e) => `edited ${path.relative(process.cwd(), e.path).replace(/\\/g, "/")}`),
@@ -901,35 +804,31 @@ async function handleInput(raw) {
901
804
  conversationHistory.push({ role: "user", content: userPrompt });
902
805
  conversationHistory.push({ role: "assistant", content: `[${summary}] ${message}` });
903
806
  } else if (message) {
904
- stopSpinner(spinner);
807
+ stopSpinner(spinner, "");
905
808
  console.log(`\n${marked(message)}\n`);
906
809
  conversationHistory.push({ role: "user", content: userPrompt });
907
810
  conversationHistory.push({ role: "assistant", content: message });
908
811
  } else {
909
- stopSpinner(spinner);
812
+ stopSpinner(spinner, "");
910
813
  }
911
814
 
912
815
  } catch (err) {
913
- stopSpinner(spinner, ` ${t.err}error ${t.muted}${err.message || JSON.stringify(err)}${t.reset}`);
816
+ stopSpinner(spinner, ` ${t.err}error ${t.muted}${err.message || JSON.stringify(err)}${t.reset}`);
914
817
  } finally {
818
+ // This will safely handle restoring the prompt after everything (including early returns)
915
819
  askPrompt();
916
820
  }
917
821
  }
918
822
 
919
-
823
+ // ─── STARTUP BANNER ──────────────────────────────────────────────────────────
920
824
  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; }
825
+ try { return require("child_process").execSync("git rev-parse --abbrev-ref HEAD", { stdio: ["pipe", "pipe", "pipe"] }).toString().trim(); }
826
+ catch { return null; }
926
827
  }
927
828
 
928
829
  function getGitStatus() {
929
830
  try {
930
- const out = require("child_process")
931
- .execSync("git status --porcelain", { stdio: ["pipe", "pipe", "pipe"] })
932
- .toString().trim();
831
+ const out = require("child_process").execSync("git status --porcelain", { stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
933
832
  if (!out) return "clean";
934
833
  const lines = out.split("\n");
935
834
  const mod = lines.filter(l => l.startsWith(" M") || l.startsWith("M")).length;
@@ -941,10 +840,6 @@ function getGitStatus() {
941
840
  } catch { return null; }
942
841
  }
943
842
 
944
- function getNodeVersion() {
945
- return process.version;
946
- }
947
-
948
843
  function getNow() {
949
844
  const d = new Date();
950
845
  const pad = (n) => String(n).padStart(2, "0");
@@ -952,50 +847,39 @@ function getNow() {
952
847
  }
953
848
 
954
849
  function getDate() {
955
- const d = new Date();
956
- return d.toLocaleDateString("en-GB", { weekday: "short", day: "2-digit", month: "short", year: "numeric" });
850
+ return new Date().toLocaleDateString("en-GB", { weekday: "short", day: "2-digit", month: "short", year: "numeric" });
957
851
  }
958
852
 
959
-
960
-
961
853
  async function checkForUpdate() {
962
854
  try {
963
855
  const res = await fetch(`https://registry.npmjs.org/cli-atom/latest`, { signal: AbortSignal.timeout(3000) });
964
856
  if (!res.ok) return null;
965
857
  const data = await res.json();
966
858
  return data.version || null;
967
- } catch {
968
- return null;
969
- }
859
+ } catch { return null; }
970
860
  }
971
861
 
972
862
  async function start() {
973
863
  console.clear();
974
-
975
864
  const w = cols();
976
865
  const branch = getGitBranch();
977
866
  const status = getGitStatus();
978
867
  const cwd = process.cwd();
979
- const node = getNodeVersion();
868
+ const node = process.version;
980
869
  const now = getNow();
981
870
  const date = getDate();
982
871
 
983
- // check for update in background
984
872
  const latestVersion = await checkForUpdate();
985
873
  const hasUpdate = latestVersion && latestVersion !== VERSION;
986
874
 
987
- console.log();
988
875
  console.log(rule("─"));
989
- console.log();
990
876
 
991
- const titleLeft = `${t.violet}${t.bold}ATOM${t.reset} ${t.muted}coding agent${t.reset}`;
877
+ const titleLeft = ` ${t.violet}${t.bold}ATOM${t.reset} ${t.muted}coding agent${t.reset}`;
992
878
  const titleRight = `${t.violetDim}v${VERSION}${t.reset}`;
993
879
  const titleRightVis = `v${VERSION}`;
994
- const titleGap = w - "ATOM coding agent".length - titleRightVis.length;
880
+ const titleGap = w - " ATOM coding agent".length - titleRightVis.length;
995
881
  console.log(titleLeft + " ".repeat(Math.max(0, titleGap)) + titleRight);
996
882
 
997
- console.log();
998
-
999
883
  const rows = [
1000
884
  [`model`, MODEL_LABEL],
1001
885
  [`node`, node],
@@ -1007,23 +891,19 @@ async function start() {
1007
891
  ].filter(Boolean);
1008
892
 
1009
893
  for (const [label, value] of rows) {
1010
- const l = `${t.violetDim}${label.padEnd(10)}${t.reset}`;
894
+ const l = ` ${t.violetDim}${label.padEnd(8)}${t.reset}`;
1011
895
  const v = `${t.muted}${value}${t.reset}`;
1012
896
  console.log(l + v);
1013
897
  }
1014
898
 
1015
- console.log();
1016
899
  console.log(rule("─"));
1017
- console.log();
1018
900
 
1019
901
  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();
902
+ console.log(` ${t.warn}update available${t.reset} ${t.muted}v${VERSION} → ${t.accent}v${latestVersion}${t.reset}`);
903
+ console.log(` ${t.dim}run ${t.accent}npm install -g cli-atom${t.dim} to update${t.reset}\n`);
1023
904
  }
1024
905
 
1025
- console.log(`${t.violetDim}ready${t.reset}`);
1026
-
906
+ console.log(` ${t.ok}ready${t.reset}`);
1027
907
  askPrompt();
1028
908
  }
1029
909
 
@@ -0,0 +1,109 @@
1
+ # ⚛️ Atom-AI
2
+
3
+ <div align="center">
4
+
5
+ **Un module Node.js léger et performant pour intégrer des fonctionnalités d'IA dans vos projets.**
6
+
7
+ [![npm version](https://img.shields.io/npm/v/atom-ai.svg?style=flat-square)](https://www.npmjs.com/package/atom-ai)
8
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE)
9
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D14.0.0-brightgreen.svg?style=flat-square)](https://nodejs.org)
10
+
11
+ </div>
12
+
13
+ ---
14
+
15
+ ## 📖 À propos
16
+
17
+ **Atom-AI** est un module Node.js conçu pour simplifier l'intégration d'intelligence artificielle dans vos applications. Léger, modulaire et facile à prendre en main, il s'intègre rapidement dans n'importe quel projet JavaScript.
18
+
19
+ ## ✨ Fonctionnalités
20
+
21
+ > ⚠️ *Cette section est à compléter avec les fonctionnalités exactes de votre script. Voici une base qu vous pouvez adapter :*
22
+
23
+ - 🚀 **Démarrage rapide** — Prêt à l'emploi en quelques lignes de code
24
+ - 🧠 **Intégration IA** — Connectez-vous facilement à des modèles d'IA
25
+ - 📦 **Léger** — Minimal en dépendances
26
+ - 🔧 **Configurable** — Adaptez le comportement à vos besoins
27
+ - 📝 **Simple** — API claire et intuitive
28
+
29
+ ## 📦 Installation
30
+
31
+ ```bash
32
+ # Via npm
33
+ npm install atom-ai
34
+
35
+ # Via yarn
36
+ yarn add atom-ai
37
+
38
+ # Via pnpm
39
+ pnpm add atom-ai
40
+ ```
41
+
42
+ ## 🚀 Utilisation
43
+
44
+ ```javascript
45
+ const Atom = require('atom-ai');
46
+
47
+ // Initialisation
48
+ const atom = new Atom({
49
+ // Vos options de configuration
50
+ });
51
+
52
+ // Utilisation
53
+ atom.run();
54
+ ```
55
+
56
+ ## ⚙️ Configuration
57
+
58
+ | Option | Type | Description | Défaut |
59
+ |--------|------|-------------|--------|
60
+ | `option1` | `string` | Description de l'option | `"valeur"` |
61
+
62
+ > 📝 *Complétez ce tableau avec les vraies options de configuration.*
63
+
64
+ ## 📁 Structure du projet
65
+
66
+ ```
67
+ Atom-Ai/
68
+ ├── atom.js # Point d'entrée principal du module
69
+ ├── package.json # Métadonnées et dépendances
70
+ ├── .npmignore # Fichiers exclus du package npm
71
+ ├── .gitignore # Fichiers exclus de Git
72
+ └── README.md # Ce fichier
73
+ ```
74
+
75
+ ## 🛠️ Développement
76
+
77
+ ```bash
78
+ # Cloner le dépôt
79
+ git clone https://github.com/Redwxll-atm/Atom-Ai.git
80
+ cd Atom-Ai
81
+
82
+ # Installer les dépendances
83
+ npm install
84
+
85
+ # Lancer le module
86
+ node atom.js
87
+ ```
88
+
89
+ ## 🤝 Contribuer
90
+
91
+ Les contributions sont les bienvenues ! Voici comment participer :
92
+
93
+ 1. **Fork** le projet
94
+ 2. Créez une branche fonctionnelle (`git checkout -b feature/ma-fonctionnalite`)
95
+ 3. **Commit** vos changements (`git commit -m 'Ajout de ma fonctionnalité'`)
96
+ 4. **Push** vers la branche (`git push origin feature/ma-fonctionnalite`)
97
+ 5. Ouvrez une **Pull Request**
98
+
99
+ ## 📄 Licence
100
+
101
+ Ce projet est sous licence **MIT**. Voir le fichier [LICENSE](LICENSE) pour plus de détails.
102
+
103
+ ---
104
+
105
+ <div align="center">
106
+
107
+ Développé avec ❤️ par **[Redwxll](https://github.com/Redwxll-atm)**
108
+
109
+ </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cli-atom",
3
- "version": "0.2.7",
3
+ "version": "0.2.10",
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
+ }