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 +248 -368
- package/docs/README_CONTENT.md +109 -0
- package/package.json +3 -2
package/atom.js
CHANGED
|
@@ -14,6 +14,7 @@ marked.use(markedTerminal());
|
|
|
14
14
|
process.on("unhandledRejection", (err) => { console.error("unhandled rejection:", err); });
|
|
15
15
|
process.on("uncaughtException", (err) => { console.error("uncaught exception:", err); });
|
|
16
16
|
|
|
17
|
+
// ── THEME ────────────────────────────────────────────────────────────────────
|
|
17
18
|
const t = {
|
|
18
19
|
reset: "\x1b[0m",
|
|
19
20
|
bold: "\x1b[1m",
|
|
@@ -24,11 +25,11 @@ const t = {
|
|
|
24
25
|
muted: "\x1b[38;5;244m",
|
|
25
26
|
accent: "\x1b[38;5;255m",
|
|
26
27
|
subtle: "\x1b[38;5;238m",
|
|
27
|
-
violet: "\x1b[38;5;
|
|
28
|
-
violetDim: "\x1b[38;5;
|
|
28
|
+
violet: "\x1b[38;5;141m",
|
|
29
|
+
violetDim: "\x1b[38;5;60m",
|
|
29
30
|
ok: "\x1b[38;5;114m",
|
|
30
31
|
warn: "\x1b[38;5;179m",
|
|
31
|
-
err: "\x1b[38;5;
|
|
32
|
+
err: "\x1b[38;5;174m",
|
|
32
33
|
info: "\x1b[38;5;110m",
|
|
33
34
|
bgDark: "\x1b[48;5;234m",
|
|
34
35
|
bgMid: "\x1b[48;5;236m",
|
|
@@ -39,6 +40,10 @@ const IGNORE_EXTS = [".png", ".jpg", ".jpeg", ".gif", ".ico", ".svg", ".woff", "
|
|
|
39
40
|
".ttf", ".eot", ".mp3", ".mp4", ".zip", ".tar", ".gz",
|
|
40
41
|
".exe", ".dll", ".so", ".lock"];
|
|
41
42
|
const MAX_FILE_SIZE = 15_000;
|
|
43
|
+
|
|
44
|
+
const MAX_FILES = 15;
|
|
45
|
+
const MAX_CONTEXT_CHARS = 20_000;
|
|
46
|
+
|
|
42
47
|
let MODEL = "z-ai/glm-5-turbo";
|
|
43
48
|
let MODEL_LABEL = "glm-5-turbo";
|
|
44
49
|
const VERSION = _require("./package.json").version;
|
|
@@ -51,17 +56,13 @@ const MODELS = [
|
|
|
51
56
|
{ id: "mistral-large", label: "mistral-large" }
|
|
52
57
|
];
|
|
53
58
|
|
|
54
|
-
// ───
|
|
55
|
-
|
|
59
|
+
// ─── CONFIG MANAGEMENT ───────────────────────────────────────────────────────
|
|
56
60
|
const CONFIG_DIR = path.join(os.homedir(), ".atom-cli");
|
|
57
61
|
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
58
62
|
|
|
59
63
|
function getConfig() {
|
|
60
|
-
try {
|
|
61
|
-
|
|
62
|
-
} catch {
|
|
63
|
-
return {};
|
|
64
|
-
}
|
|
64
|
+
try { return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8")); }
|
|
65
|
+
catch { return {}; }
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
function setConfig(data) {
|
|
@@ -74,47 +75,38 @@ function promptInput(question) {
|
|
|
74
75
|
return new Promise((resolve) => {
|
|
75
76
|
process.stdout.write(question);
|
|
76
77
|
process.stdin.setEncoding("utf8");
|
|
77
|
-
process.stdin.once("data", (data) =>
|
|
78
|
-
resolve(data.trim());
|
|
79
|
-
});
|
|
78
|
+
process.stdin.once("data", (data) => resolve(data.trim()));
|
|
80
79
|
});
|
|
81
80
|
}
|
|
82
81
|
|
|
83
|
-
// ─── CLI
|
|
84
|
-
|
|
82
|
+
// ─── CLI COMMANDS ────────────────────────────────────────────────────────────
|
|
85
83
|
const args = process.argv.slice(2);
|
|
86
84
|
|
|
87
85
|
if (args[0] === "login") {
|
|
88
|
-
console.log();
|
|
89
|
-
console.log(` ${t.
|
|
90
|
-
console.log();
|
|
91
|
-
console.log(` ${t.muted}Go to ${t.accent}https://puter.com${t.muted} and generate an API token.${t.reset}`);
|
|
92
|
-
console.log();
|
|
86
|
+
console.log(`\n ${t.violet}${t.bold}ATOM${t.reset} ${t.muted}login${t.reset}\n`);
|
|
87
|
+
console.log(` ${t.muted}Go to ${t.accent}https://puter.com${t.muted} and generate an API token.${t.reset}\n`);
|
|
93
88
|
const key = await promptInput(` ${t.violetDim}Paste your API key: ${t.reset}`);
|
|
94
89
|
if (!key) {
|
|
95
90
|
console.log(`\n ${t.err}No key provided. Aborting.${t.reset}\n`);
|
|
96
91
|
process.exit(1);
|
|
97
92
|
}
|
|
98
93
|
setConfig({ apiKey: key });
|
|
99
|
-
console.log(`\n ${t.ok}API key saved to ${t.dim}${CONFIG_FILE}${t.reset}`);
|
|
94
|
+
console.log(`\n ${t.ok}✓ API key saved to ${t.dim}${CONFIG_FILE}${t.reset}`);
|
|
100
95
|
console.log(` ${t.muted}You can now run ${t.accent}atom${t.muted} to start coding!${t.reset}\n`);
|
|
101
96
|
process.exit(0);
|
|
102
97
|
}
|
|
103
98
|
|
|
104
99
|
if (args[0] === "logout") {
|
|
105
100
|
try { fs.unlinkSync(CONFIG_FILE); } catch { }
|
|
106
|
-
console.log(`\n ${t.ok}Logged out. API key removed.${t.reset}\n`);
|
|
101
|
+
console.log(`\n ${t.ok}✓ Logged out. API key removed.${t.reset}\n`);
|
|
107
102
|
process.exit(0);
|
|
108
103
|
}
|
|
109
104
|
|
|
110
|
-
// ───
|
|
111
|
-
|
|
105
|
+
// ─── PUTER INIT ──────────────────────────────────────────────────────────────
|
|
112
106
|
let config = getConfig();
|
|
113
107
|
if (!config.apiKey) {
|
|
114
|
-
console.log();
|
|
115
|
-
console.log(` ${t.
|
|
116
|
-
console.log(` ${t.muted}Run ${t.accent}atom login${t.muted} or type ${t.accent}/login${t.muted} after starting atom.${t.reset}`);
|
|
117
|
-
console.log();
|
|
108
|
+
console.log(`\n ${t.err}✗ No API key found.${t.reset}`);
|
|
109
|
+
console.log(` ${t.muted}Run ${t.accent}atom login${t.muted} to authenticate.${t.reset}\n`);
|
|
118
110
|
process.exit(1);
|
|
119
111
|
}
|
|
120
112
|
|
|
@@ -125,64 +117,69 @@ let promptCount = 0;
|
|
|
125
117
|
let filesCreated = 0;
|
|
126
118
|
let filesEdited = 0;
|
|
127
119
|
const sessionStart = Date.now();
|
|
128
|
-
const systemPrompt = `You are an autonomous AI coding agent. You have FULL VISIBILITY of the user's project files (provided below as context).
|
|
129
|
-
Analyze the existing code structure before deciding what to do. Be smart: if a file exists, EDIT it. If it doesn't, create it with FILE.
|
|
130
120
|
|
|
131
|
-
|
|
132
|
-
|
|
121
|
+
const systemPrompt = `You are an autonomous AI coding agent with FULL VISIBILITY of the user's project files.
|
|
122
|
+
|
|
123
|
+
# MISSION
|
|
124
|
+
Analyze the existing codebase, understand its conventions, then make precise, minimal, and correct changes to fulfill the user's request.
|
|
133
125
|
|
|
126
|
+
# CORE PRINCIPLES
|
|
127
|
+
1. **Read before write.** Always analyze existing structure, imports, naming conventions, and patterns before modifying anything.
|
|
128
|
+
2. **Edit over create.** If a file already exists, EDIT it. Only create files that are genuinely new.
|
|
129
|
+
3. **Always use folders.** When creating new files, NEVER place them directly in the project root. ALWAYS create a dedicated, logically named folder (module/directory) and put the new files inside it. Group related code logically into these subdirectories.
|
|
130
|
+
4. **Minimal diff.** Change only what is necessary. Preserve existing style, indentation, and formatting.
|
|
131
|
+
5. **Atomic edits.** The "search" string must be unique enough to match exactly ONE location. Include surrounding context lines if needed to disambiguate.
|
|
132
|
+
6. **No placeholders.** Never use "// ... rest of code", "// existing code", or "/* unchanged */". Always provide complete, runnable content.
|
|
133
|
+
7. **Strict escaping.** All strings inside the JSON must be properly escaped (\\n, \\t, \\", \\\\, etc.). Pay special attention to backticks, template literals, and regex.
|
|
134
|
+
|
|
135
|
+
# OUTPUT FORMAT
|
|
136
|
+
You MUST respond EXCLUSIVELY with a single valid JSON object.
|
|
137
|
+
NO markdown fences, NO commentary before or after, NO trailing text, NO trailing commas.
|
|
138
|
+
|
|
139
|
+
Schema:
|
|
134
140
|
{
|
|
135
|
-
"message": "
|
|
141
|
+
"message": "Concise summary of what you did and why (optional)",
|
|
136
142
|
"filesToCreate": [
|
|
137
|
-
{
|
|
138
|
-
"path": "path/to/new_file.ext",
|
|
139
|
-
"content": "Full content of the new file"
|
|
140
|
-
}
|
|
143
|
+
{ "path": "folder_name/sub_folder/new_file.ext", "content": "Full file content" }
|
|
141
144
|
],
|
|
142
145
|
"filesToEdit": [
|
|
143
|
-
{
|
|
144
|
-
"path": "path/to/existing_file.ext",
|
|
145
|
-
"search": "exact lines to find in the file",
|
|
146
|
-
"replace": "new lines to put in their place"
|
|
147
|
-
}
|
|
146
|
+
{ "path": "existing_folder/existing_file.ext", "search": "Exact lines to locate", "replace": "New lines to substitute" }
|
|
148
147
|
],
|
|
149
|
-
"filesToDelete": [
|
|
150
|
-
|
|
151
|
-
],
|
|
152
|
-
"commandsToRun": [
|
|
153
|
-
"npm test",
|
|
154
|
-
"node script.js"
|
|
155
|
-
]
|
|
148
|
+
"filesToDelete": [ "folder_name/file_or_folder" ],
|
|
149
|
+
"commandsToRun": [ "run_native_test_or_build_command" ]
|
|
156
150
|
}
|
|
157
151
|
|
|
158
|
-
|
|
159
|
-
-
|
|
160
|
-
-
|
|
161
|
-
-
|
|
152
|
+
Rules:
|
|
153
|
+
- Omit unused arrays or leave them as [].
|
|
154
|
+
- Paths are relative to the project root, use forward slashes, and MUST always include a parent folder. Never output a path with just a filename (e.g., NEVER just "script.js", always "utils/script.js").
|
|
155
|
+
- "search" must match existing content byte-for-byte (whitespace and indentation included).
|
|
156
|
+
- One file may appear in multiple edit entries if several distinct changes are needed.
|
|
157
|
+
- "commandsToRun": Dynamically detect the project's language and ecosystem from its config files (e.g., package.json, requirements.txt, Cargo.toml, go.mod, pom.xml, composer.json, Makefile, CMakeLists.txt, etc.). Run the standard verification commands for that specific ecosystem (e.g., tests, build, lint). NEVER include destructive commands (like rm -rf, dropdb, format, etc.).
|
|
158
|
+
|
|
159
|
+
# CODING STANDARDS
|
|
160
|
+
- Match the project's existing language version, framework, and conventions.
|
|
161
|
+
- Prefer standard libraries over new dependencies. If a new dependency is required, mention it in "message".
|
|
162
|
+
- Write clean, typed (where applicable), readable, and testable code.
|
|
163
|
+
- Keep functions small and focused. No dead code, no commented-out blocks.
|
|
164
|
+
- Handle errors gracefully. Never swallow exceptions silently.
|
|
165
|
+
- Respect SOLID principles and the project's architectural layering.
|
|
166
|
+
|
|
167
|
+
# CONSTRAINTS
|
|
168
|
+
- Never invent file paths or assume content you cannot see.
|
|
169
|
+
- Never modify lockfiles, .env, or CI secrets unless explicitly asked.
|
|
170
|
+
- If the request is ambiguous, make the most reasonable assumption and explain it briefly in "message".
|
|
171
|
+
- If the request is impossible or unsafe, return an empty JSON with an explanation in "message".`;
|
|
162
172
|
|
|
163
173
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
164
174
|
|
|
165
|
-
function cols() {
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function rule(char = "─", color = t.violetDim) {
|
|
170
|
-
return `${color}${char.repeat(cols())}${t.reset}`;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function pad(str, width) {
|
|
174
|
-
const visible = str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
175
|
-
return str + " ".repeat(Math.max(0, width - visible.length));
|
|
176
|
-
}
|
|
177
|
-
|
|
175
|
+
function cols() { return process.stdout.columns || 80; }
|
|
176
|
+
function rule(char = "─", color = t.violetDim) { return `${color}${char.repeat(cols())}${t.reset}`; }
|
|
178
177
|
function elapsed() {
|
|
179
178
|
const s = Math.floor((Date.now() - sessionStart) / 1000);
|
|
180
179
|
return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`;
|
|
181
180
|
}
|
|
182
181
|
|
|
183
|
-
|
|
184
|
-
const MAX_CONTEXT_CHARS = 60_000;
|
|
185
|
-
|
|
182
|
+
// ─── FILE SCANNER ────────────────────────────────────────────────────────────
|
|
186
183
|
function scanDir(dirPath, prefix = "", _count = { n: 0 }) {
|
|
187
184
|
let tree = "";
|
|
188
185
|
let files = [];
|
|
@@ -203,19 +200,16 @@ function scanDir(dirPath, prefix = "", _count = { n: 0 }) {
|
|
|
203
200
|
tree += `${prefix}${entry.name}\n`;
|
|
204
201
|
try {
|
|
205
202
|
const stat = fs.statSync(fullPath);
|
|
206
|
-
const content = stat.size <= MAX_FILE_SIZE
|
|
207
|
-
? fs.readFileSync(fullPath, "utf-8")
|
|
208
|
-
: "[file too large — truncated]";
|
|
203
|
+
const content = stat.size <= MAX_FILE_SIZE ? fs.readFileSync(fullPath, "utf-8") : "[file too large]";
|
|
209
204
|
files.push({ path: fullPath.replace(/\\/g, "/"), content });
|
|
210
205
|
_count.n++;
|
|
211
|
-
} catch { /* skip
|
|
206
|
+
} catch { /* skip */ }
|
|
212
207
|
}
|
|
213
208
|
}
|
|
214
|
-
} catch { /* skip
|
|
209
|
+
} catch { /* skip */ }
|
|
215
210
|
return { tree, files };
|
|
216
211
|
}
|
|
217
212
|
|
|
218
|
-
|
|
219
213
|
let _contextCache = null;
|
|
220
214
|
let _contextSnapshot = null;
|
|
221
215
|
|
|
@@ -230,25 +224,39 @@ function getSnapshot(files) {
|
|
|
230
224
|
function snapshotChanged(files, snap) {
|
|
231
225
|
if (!snap) return true;
|
|
232
226
|
for (const f of files) {
|
|
233
|
-
try {
|
|
234
|
-
|
|
235
|
-
} catch { return true; }
|
|
227
|
+
try { if (fs.statSync(f.path).mtimeMs !== snap[f.path]) return true; }
|
|
228
|
+
catch { return true; }
|
|
236
229
|
}
|
|
237
230
|
return Object.keys(snap).length !== files.length;
|
|
238
231
|
}
|
|
239
232
|
|
|
240
|
-
function buildContext() {
|
|
233
|
+
function buildContext(userPrompt = "") {
|
|
241
234
|
const cwd = process.cwd();
|
|
242
235
|
const { tree, files } = scanDir(cwd);
|
|
236
|
+
const unchanged = !snapshotChanged(files, _contextSnapshot);
|
|
243
237
|
|
|
244
|
-
|
|
245
|
-
|
|
238
|
+
let ctx = `\n--- PROJECT CONTEXT (${cwd}) ---\nFile tree:\n${tree}\n`;
|
|
239
|
+
|
|
240
|
+
if (unchanged && _contextCache) {
|
|
241
|
+
ctx += `[Files unchanged since last request — use file tree above]\n--- END PROJECT CONTEXT ---\n`;
|
|
242
|
+
return ctx;
|
|
246
243
|
}
|
|
247
244
|
|
|
248
|
-
|
|
249
|
-
ctx += `File tree:\n${tree}\n`;
|
|
245
|
+
const keywords = userPrompt.toLowerCase().split(/\W+/).filter(w => w.length > 3);
|
|
250
246
|
let totalChars = ctx.length;
|
|
251
|
-
|
|
247
|
+
let included = 0;
|
|
248
|
+
|
|
249
|
+
const sorted = [...files].sort((a, b) => {
|
|
250
|
+
const relA = path.relative(cwd, a.path).toLowerCase();
|
|
251
|
+
const relB = path.relative(cwd, b.path).toLowerCase();
|
|
252
|
+
const scoreA = keywords.filter(k => relA.includes(k)).length;
|
|
253
|
+
const scoreB = keywords.filter(k => relB.includes(k)).length;
|
|
254
|
+
if (scoreB !== scoreA) return scoreB - scoreA;
|
|
255
|
+
return a.content.length - b.content.length;
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
for (const f of sorted) {
|
|
259
|
+
if (included >= MAX_FILES) break;
|
|
252
260
|
const rel = path.relative(cwd, f.path).replace(/\\/g, "/");
|
|
253
261
|
const block = `--- ${rel} ---\n${f.content}\n--- end ${rel} ---\n\n`;
|
|
254
262
|
if (totalChars + block.length > MAX_CONTEXT_CHARS) {
|
|
@@ -257,9 +265,10 @@ function buildContext() {
|
|
|
257
265
|
}
|
|
258
266
|
ctx += block;
|
|
259
267
|
totalChars += block.length;
|
|
268
|
+
included++;
|
|
260
269
|
}
|
|
261
|
-
ctx += `--- END PROJECT CONTEXT ---\n`;
|
|
262
270
|
|
|
271
|
+
ctx += `--- END PROJECT CONTEXT ---\n`;
|
|
263
272
|
_contextCache = ctx;
|
|
264
273
|
_contextSnapshot = getSnapshot(files);
|
|
265
274
|
return ctx;
|
|
@@ -275,12 +284,7 @@ function runCommand(cmd, cwd) {
|
|
|
275
284
|
});
|
|
276
285
|
}
|
|
277
286
|
|
|
278
|
-
|
|
279
|
-
(_, i, a) => {
|
|
280
|
-
const bar = a.map((__, j) => (j <= i ? "▪" : "·")).join("");
|
|
281
|
-
return bar;
|
|
282
|
-
}
|
|
283
|
-
);
|
|
287
|
+
// ─── UI COMPONENTS ───────────────────────────────────────────────────────────
|
|
284
288
|
const SPINNER_CHARS = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
285
289
|
|
|
286
290
|
function startSpinner(label) {
|
|
@@ -288,9 +292,7 @@ function startSpinner(label) {
|
|
|
288
292
|
const timer = setInterval(() => {
|
|
289
293
|
process.stdout.clearLine(0);
|
|
290
294
|
process.stdout.cursorTo(0);
|
|
291
|
-
process.stdout.write(
|
|
292
|
-
` ${t.muted}${SPINNER_CHARS[i % SPINNER_CHARS.length]} ${label}${t.reset}`
|
|
293
|
-
);
|
|
295
|
+
process.stdout.write(` ${t.violet}${SPINNER_CHARS[i % SPINNER_CHARS.length]}${t.reset} ${t.muted}${label}${t.reset}`);
|
|
294
296
|
i++;
|
|
295
297
|
}, 80);
|
|
296
298
|
return timer;
|
|
@@ -303,43 +305,39 @@ function stopSpinner(timer, msg) {
|
|
|
303
305
|
if (msg) console.log(msg);
|
|
304
306
|
}
|
|
305
307
|
|
|
306
|
-
|
|
307
308
|
function sectionHeader(label) {
|
|
308
|
-
console.log();
|
|
309
|
-
console.log(
|
|
310
|
-
console.log(`${t.violetDim}${"─".repeat(cols())}${t.reset}`);
|
|
309
|
+
console.log(`\n ${t.violet}${t.bold}${label.toUpperCase()}${t.reset}`);
|
|
310
|
+
console.log(` ${t.violetDim}${"─".repeat(4)}${t.reset}`);
|
|
311
311
|
}
|
|
312
312
|
|
|
313
313
|
async function showCreatedFile(filePath, content) {
|
|
314
314
|
const lines = content.split("\n");
|
|
315
315
|
const relPath = path.relative(process.cwd(), filePath).replace(/\\/g, "/");
|
|
316
|
-
console.log(` ${t.ok}
|
|
317
|
-
await sleep(60);
|
|
316
|
+
console.log(` ${t.ok}✓${t.reset} ${t.bold}${relPath}${t.reset} ${t.muted}(${lines.length} lines)${t.reset}`);
|
|
318
317
|
|
|
319
|
-
const preview = lines.slice(0,
|
|
318
|
+
const preview = lines.slice(0, 6);
|
|
320
319
|
for (let i = 0; i < preview.length; i++) {
|
|
321
320
|
const num = String(i + 1).padStart(3, " ");
|
|
322
321
|
console.log(` ${t.subtle}${num}${t.reset} ${t.dim}${preview[i]}${t.reset}`);
|
|
323
|
-
await sleep(
|
|
322
|
+
await sleep(10);
|
|
324
323
|
}
|
|
325
|
-
if (lines.length >
|
|
326
|
-
console.log(` ${t.subtle}
|
|
324
|
+
if (lines.length > 6) {
|
|
325
|
+
console.log(` ${t.subtle} ...${t.reset} ${t.muted}${lines.length - 6} more lines${t.reset}`);
|
|
327
326
|
}
|
|
328
327
|
console.log();
|
|
329
328
|
}
|
|
330
329
|
|
|
331
330
|
async function showDiff(filePath, searchBlock, replaceBlock) {
|
|
332
331
|
const relPath = path.relative(process.cwd(), filePath).replace(/\\/g, "/");
|
|
333
|
-
console.log(` ${t.warn}~${t.reset} ${t.
|
|
334
|
-
await sleep(60);
|
|
332
|
+
console.log(` ${t.warn}~${t.reset} ${t.bold}${relPath}${t.reset}`);
|
|
335
333
|
|
|
336
334
|
for (const line of searchBlock.split("\n")) {
|
|
337
|
-
console.log(` ${t.err}-
|
|
338
|
-
await sleep(
|
|
335
|
+
console.log(` ${t.err}- ${t.dim}${line}${t.reset}`);
|
|
336
|
+
await sleep(15);
|
|
339
337
|
}
|
|
340
338
|
for (const line of replaceBlock.split("\n")) {
|
|
341
|
-
console.log(` ${t.ok}+
|
|
342
|
-
await sleep(
|
|
339
|
+
console.log(` ${t.ok}+ ${line}${t.reset}`);
|
|
340
|
+
await sleep(15);
|
|
343
341
|
}
|
|
344
342
|
console.log();
|
|
345
343
|
}
|
|
@@ -348,120 +346,109 @@ function printStatus(icon, color, msg) {
|
|
|
348
346
|
console.log(` ${color}${icon}${t.reset} ${msg}`);
|
|
349
347
|
}
|
|
350
348
|
|
|
351
|
-
// ───
|
|
349
|
+
// ─── SLASH COMMANDS ───────────────────────────────────────────────────────────
|
|
352
350
|
const SLASH_COMMANDS = [
|
|
353
351
|
{ name: "help", desc: "Show all available commands" },
|
|
354
352
|
{ name: "model", desc: "List or switch AI model" },
|
|
355
353
|
{ name: "key", desc: "View your current API key" },
|
|
356
354
|
{ name: "login", desc: "Set a new API key" },
|
|
357
355
|
{ name: "logout", desc: "Remove your API key" },
|
|
358
|
-
{ name: "
|
|
356
|
+
{ name: "exit", desc: "Exit the CLI" },
|
|
359
357
|
];
|
|
360
358
|
|
|
359
|
+
// ─── INTERACTIVE PROMPT (FIXED & ROBUST) ─────────────────────────────────────
|
|
360
|
+
// ─── INTERACTIVE PROMPT (FIXED & ROBUST) ─────────────────────────────────────
|
|
361
361
|
function askPrompt() {
|
|
362
|
-
|
|
363
|
-
|
|
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
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
402
|
-
const
|
|
403
|
-
|
|
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
|
|
407
|
-
|
|
408
|
-
menuItems = getFilteredCommands();
|
|
396
|
+
function drawPrompt() {
|
|
397
|
+
clearPromptAndMenu();
|
|
409
398
|
|
|
410
|
-
|
|
411
|
-
|
|
399
|
+
const prefix = ` ${t.violet}❯${t.reset} `;
|
|
400
|
+
const visiblePrefixLen = 4;
|
|
401
|
+
let baseText = "";
|
|
402
|
+
let currentVisibleLen = 0;
|
|
412
403
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
//
|
|
440
|
-
|
|
441
|
-
|
|
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
|
-
|
|
440
|
+
const cursorCol = (currentVisibleLen % cols()) || cols();
|
|
441
|
+
process.stdout.write(`\x1b[${cursorCol}G`); // Définit la colonne exacte
|
|
452
442
|
}
|
|
453
|
-
}
|
|
454
443
|
|
|
455
|
-
|
|
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() {
|
|
447
|
+
function onResize() { drawPrompt(); }
|
|
461
448
|
process.stdout.on("resize", onResize);
|
|
462
449
|
|
|
463
|
-
process.stdout.write(
|
|
464
|
-
|
|
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
|
-
|
|
480
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
clearMenu();
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
if (input.startsWith("/")) {
|
|
553
|
-
menuActive = true;
|
|
554
|
-
menuIndex = 0;
|
|
555
|
-
process.stdout.write("\x1b[3A\r" + drawBox(input, false));
|
|
556
|
-
renderMenu();
|
|
557
|
-
} else {
|
|
558
|
-
menuActive = false;
|
|
559
|
-
clearMenu();
|
|
560
|
-
process.stdout.write("\x1b[3A\r" + drawBox(input || "", input.length === 0));
|
|
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
|
-
|
|
570
|
-
|
|
571
|
-
menuActive = true;
|
|
572
|
-
menuIndex = 0;
|
|
573
|
-
process.stdout.write("\x1b[3A\r" + drawBox(input, false));
|
|
574
|
-
renderMenu();
|
|
575
|
-
return;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
// filter menu as user types
|
|
579
|
-
if (input.startsWith("/")) {
|
|
580
|
-
menuActive = true;
|
|
581
|
-
menuIndex = 0;
|
|
582
|
-
process.stdout.write("\x1b[3A\r" + drawBox(input, false));
|
|
583
|
-
renderMenu();
|
|
584
|
-
return;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
// normal input
|
|
588
|
-
menuActive = false;
|
|
589
|
-
clearMenu();
|
|
590
|
-
process.stdout.write("\x1b[3A\r" + drawBox(input, false));
|
|
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
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
console.log(` ${t.violetDim}/model${t.reset} ${t.muted}list available AI models${t.reset}`);
|
|
617
|
-
console.log(` ${t.violetDim}/model <name>${t.reset} ${t.muted}switch to a specific model${t.reset}`);
|
|
618
|
-
console.log(` ${t.violetDim}/key${t.reset} ${t.muted}view your current API key${t.reset}`);
|
|
619
|
-
console.log(` ${t.violetDim}/login${t.reset} ${t.muted}set a new API key${t.reset}`);
|
|
620
|
-
console.log(` ${t.violetDim}/logout${t.reset} ${t.muted}remove your API key${t.reset}`);
|
|
621
|
-
console.log(` ${t.violetDim}exit / quit${t.reset} ${t.muted}end the session${t.reset}`);
|
|
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);
|
|
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 >
|
|
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
|
|
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
|
|
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}\
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
+
[](https://www.npmjs.com/package/atom-ai)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
[](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.
|
|
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
|
+
}
|