rafaygen-cli 1.3.2 → 1.3.3

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/src/agent.js CHANGED
@@ -1,277 +1,1383 @@
1
- import { getToken, getApiUrl, getModel, setModel } from "./auth.js";
2
- import { executeAction } from "./executor.js";
3
- import { printError, printStep, printSuccess } from "./ui.js";
4
- import { getSessionState, updateSessionState } from "./state.js";
5
1
  import fs from "fs";
6
2
  import path from "path";
3
+ import os from "os";
4
+ import crypto from "crypto";
5
+ import { execSync } from "child_process";
6
+ import { getToken, getApiUrl, getModel, setModel } from "./auth.js";
7
+ import { executeAction } from "./executor.js";
8
+ import {
9
+ printError,
10
+ printStep,
11
+ printSuccess,
12
+ renderBox,
13
+ renderCodeBox,
14
+ printAsciiLogo,
15
+ printRandomWelcome,
16
+ } from "./ui.js";
17
+ import {
18
+ getSessionState,
19
+ updateSessionState,
20
+ addToHistory,
21
+ saveSession,
22
+ loadSession,
23
+ listSessions,
24
+ clearHistory,
25
+ } from "./state.js";
7
26
 
8
- function readDirRecursive(dirPath, maxFiles = 30, currentFiles = []) {
9
- if (currentFiles.length >= maxFiles) return currentFiles;
10
-
11
- const entries = fs.readdirSync(dirPath, { withFileTypes: true });
12
- for (const entry of entries) {
13
- if (currentFiles.length >= maxFiles) break;
14
- if (entry.name === 'node_modules' || entry.name === '.git' || entry.name.startsWith('.')) continue;
15
-
16
- const fullPath = path.join(dirPath, entry.name);
17
- if (entry.isDirectory()) {
18
- readDirRecursive(fullPath, maxFiles, currentFiles);
19
- } else {
20
- if (entry.name.match(/\.(png|jpg|jpeg|gif|exe|bin|pdf|zip|tar|gz)$/i)) continue;
21
- currentFiles.push(fullPath);
27
+ /* ─── helpers that may not be in auth.js yet ─── */
28
+ function getSkillsDir() {
29
+ const dir = path.join(os.homedir(), ".rgcli", "skills");
30
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
31
+ return dir;
32
+ }
33
+
34
+ function getMcpConfigPath() {
35
+ const dir = path.join(os.homedir(), ".rgcli");
36
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
37
+ return path.join(dir, "mcp.json");
38
+ }
39
+
40
+ /* ─── UI helpers not yet in ui.js ─── */
41
+ async function _chalk() {
42
+ return (await import("chalk")).default;
43
+ }
44
+
45
+ function printWarning(msg) {
46
+ const c = _chalkSync();
47
+ console.log(c.yellow.bold("\n⚠ Warning: ") + c.yellow(msg) + "\n");
48
+ }
49
+
50
+ function printInfo(msg) {
51
+ const c = _chalkSync();
52
+ console.log(c.blueBright.bold("\nℹ ") + c.blueBright(msg) + "\n");
53
+ }
54
+
55
+ function printModelBadge(model) {
56
+ const c = _chalkSync();
57
+ console.log(
58
+ c.bgMagenta.white.bold(` MODEL `) +
59
+ " " +
60
+ c.magentaBright.bold(model) +
61
+ "\n"
62
+ );
63
+ }
64
+
65
+ function printSessionStatus(state) {
66
+ const c = _chalkSync();
67
+ const lines = [
68
+ `${c.bold("Session ID:")} ${state.sessionId}`,
69
+ `${c.bold("Sandbox:")} ${state.sandboxMode}`,
70
+ `${c.bold("Approvals:")} ${state.approvalMode}`,
71
+ `${c.bold("Model:")} ${getModel()}`,
72
+ `${c.bold("Reasoning:")} ${state.reasoningEffort}`,
73
+ `${c.bold("Compact:")} ${state.compactMode ? "ON" : "OFF"}`,
74
+ `${c.bold("CWD:")} ${state.cwd}`,
75
+ `${c.bold("Attached:")} ${state.attachedFiles.size} file(s)`,
76
+ `${c.bold("Active Skill:")} ${state.activeSkill || "none"}`,
77
+ `${c.bold("History:")} ${state.conversationHistory.length} messages`,
78
+ `${c.bold("Image:")} ${state.imageAttached || "none"}`,
79
+ ];
80
+ renderBox(" Session Status ", lines.join("\n"), "cyan");
81
+ }
82
+
83
+ function printAgentThinking() {
84
+ const c = _chalkSync();
85
+ console.log(c.gray(" 🧠 Agent is thinking..."));
86
+ }
87
+
88
+ function printToolExecution(tool) {
89
+ const c = _chalkSync();
90
+ console.log(c.yellow(` 🔧 Executing tool: ${tool}`));
91
+ }
92
+
93
+ function renderMarkdown(text) {
94
+ const c = _chalkSync();
95
+ if (!text) return;
96
+ // Basic markdown rendering: headers, bold, inline code, code blocks
97
+ const lines = text.split("\n");
98
+ const output = [];
99
+ let inCodeBlock = false;
100
+ let codeBuffer = [];
101
+ let codeLang = "";
102
+
103
+ for (const line of lines) {
104
+ if (line.startsWith("```") && !inCodeBlock) {
105
+ inCodeBlock = true;
106
+ codeLang = line.slice(3).trim();
107
+ codeBuffer = [];
108
+ continue;
109
+ }
110
+ if (line.startsWith("```") && inCodeBlock) {
111
+ inCodeBlock = false;
112
+ try {
113
+ const { highlight } = await import("cli-highlight");
114
+ output.push(
115
+ highlight(codeBuffer.join("\n"), {
116
+ language: codeLang || "plaintext",
117
+ ignoreIllegals: true,
118
+ })
119
+ );
120
+ } catch {
121
+ output.push(c.green(codeBuffer.join("\n")));
122
+ }
123
+ continue;
124
+ }
125
+ if (inCodeBlock) {
126
+ codeBuffer.push(line);
127
+ continue;
128
+ }
129
+
130
+ let processed = line;
131
+ // Headers
132
+ if (processed.startsWith("### "))
133
+ processed = c.cyan.bold(processed.slice(4));
134
+ else if (processed.startsWith("## "))
135
+ processed = c.cyan.bold.underline(processed.slice(3));
136
+ else if (processed.startsWith("# "))
137
+ processed = c.cyan.bold.underline(processed.slice(2));
138
+ // Bold
139
+ processed = processed.replace(
140
+ /\*\*(.+?)\*\*/g,
141
+ (_, m) => c.bold(m)
142
+ );
143
+ // Italic
144
+ processed = processed.replace(
145
+ /\*(.+?)\*/g,
146
+ (_, m) => c.italic(m)
147
+ );
148
+ // Inline code
149
+ processed = processed.replace(
150
+ /`([^`]+)`/g,
151
+ (_, m) => c.bgGray.white(` ${m} `)
152
+ );
153
+ // Bullet points
154
+ if (processed.startsWith("- "))
155
+ processed = c.green(" • ") + processed.slice(2);
156
+ if (/^\d+\.\s/.test(processed))
157
+ processed = c.green(" " + processed);
158
+
159
+ output.push(processed);
160
+ }
161
+
162
+ if (inCodeBlock && codeBuffer.length) {
163
+ output.push(c.green(codeBuffer.join("\n")));
164
+ }
165
+
166
+ console.log(output.join("\n"));
167
+ }
168
+
169
+ // Synchronous chalk import (cached after first dynamic import)
170
+ let _chalkCache = null;
171
+ function _chalkSync() {
172
+ if (!_chalkCache) {
173
+ // chalk 5.x is ESM-only but we already import it at top-level transitively via ui.js
174
+ // Fallback: use a basic wrapper
175
+ try {
176
+ // attempt require for chalk 4.x compat
177
+ _chalkCache = require("chalk");
178
+ } catch {
179
+ // provide a pass-through if chalk not yet loaded
180
+ const identity = (s) => s;
181
+ const handler = {
182
+ get(_, prop) {
183
+ if (prop === "bold" || prop === "italic" || prop === "underline")
184
+ return new Proxy(identity, handler);
185
+ if (typeof identity[prop] === "function") return identity[prop];
186
+ return new Proxy(identity, handler);
187
+ },
188
+ apply(target, _, args) {
189
+ return args[0];
190
+ },
191
+ };
192
+ _chalkCache = new Proxy(identity, handler);
22
193
  }
23
194
  }
24
- return currentFiles;
195
+ return _chalkCache;
25
196
  }
26
197
 
27
- function extractFileContext(input) {
28
- let contextString = "";
29
- const words = input.split(/\s+/);
30
- const remainingWords = [];
198
+ // Pre-load chalk
199
+ (async () => {
200
+ try {
201
+ _chalkCache = (await import("chalk")).default;
202
+ } catch {}
203
+ })();
204
+
205
+ /* ─── Binary / skip detection ─── */
206
+ const BINARY_EXTS = new Set([
207
+ "png","jpg","jpeg","gif","bmp","ico","webp","svg","mp3","mp4","avi",
208
+ "mov","mkv","zip","tar","gz","rar","7z","exe","dll","so","dylib",
209
+ "bin","dat","pdf","doc","docx","xls","xlsx","ppt","pptx","woff",
210
+ "woff2","ttf","eot","class","o","pyc","pyo",
211
+ ]);
212
+ const SKIP_DIRS = new Set([
213
+ "node_modules",".git",".next","dist","build","__pycache__",".cache",
214
+ ".vscode",".idea","coverage","vendor","target",
215
+ ]);
31
216
 
32
- for (const word of words) {
33
- if (word.includes('/') || word.includes('\\') || /^[A-Za-z]:[\\/]/.test(word)) {
34
- try {
35
- const cleanPath = word.replace(/['"]/g, '');
36
- if (fs.existsSync(cleanPath)) {
37
- const stat = fs.statSync(cleanPath);
38
- if (stat.isDirectory()) {
39
- contextString += `\n--- DIRECTORY ATTACHED: ${cleanPath} ---\n`;
40
- const files = readDirRecursive(cleanPath);
41
- for (const f of files) {
42
- const content = fs.readFileSync(f, 'utf-8');
43
- contextString += `\n// File: ${f}\n${content.substring(0, 5000)}\n`;
44
- }
45
- continue;
46
- } else if (stat.isFile()) {
47
- if (!cleanPath.match(/\.(png|jpg|jpeg|gif|exe|bin|pdf|zip|tar|gz)$/i)) {
48
- const content = fs.readFileSync(cleanPath, 'utf-8');
49
- contextString += `\n--- FILE ATTACHED: ${cleanPath} ---\n${content.substring(0, 10000)}\n`;
50
- continue;
51
- }
217
+ function isBinary(filePath) {
218
+ const ext = path.extname(filePath).slice(1).toLowerCase();
219
+ return BINARY_EXTS.has(ext);
220
+ }
221
+
222
+ /* ═══════════════════════════════════════════
223
+ extractFileContext(input)
224
+ ═══════════════════════════════════════════ */
225
+ export function extractFileContext(input) {
226
+ const regex = /"([^"]+)"|'([^']+)'|(\S+)/g;
227
+ let match;
228
+ const paths = [];
229
+ const nonPaths = [];
230
+
231
+ while ((match = regex.exec(input)) !== null) {
232
+ const token = match[1] || match[2] || match[3];
233
+ const resolved = path.isAbsolute(token)
234
+ ? token
235
+ : path.resolve(process.cwd(), token);
236
+ if (fs.existsSync(resolved)) {
237
+ paths.push(resolved);
238
+ } else {
239
+ nonPaths.push(match[0]);
240
+ }
241
+ }
242
+
243
+ const extractedContext = [];
244
+ const visited = new Set();
245
+
246
+ function readDir(dir, depth = 0) {
247
+ if (depth > 4 || visited.size >= 30) return;
248
+ let entries;
249
+ try {
250
+ entries = fs.readdirSync(dir, { withFileTypes: true });
251
+ } catch {
252
+ return;
253
+ }
254
+ for (const entry of entries) {
255
+ if (visited.size >= 30) break;
256
+ if (SKIP_DIRS.has(entry.name)) continue;
257
+ const full = path.join(dir, entry.name);
258
+ if (entry.isDirectory()) {
259
+ readDir(full, depth + 1);
260
+ } else if (entry.isFile()) {
261
+ if (visited.has(full)) continue;
262
+ visited.add(full);
263
+ if (isBinary(full)) {
264
+ extractedContext.push({
265
+ path: full,
266
+ type: "binary",
267
+ content: `[Binary file: ${entry.name}]`,
268
+ });
269
+ } else {
270
+ try {
271
+ const raw = fs.readFileSync(full, "utf-8");
272
+ extractedContext.push({
273
+ path: full,
274
+ type: "text",
275
+ content: raw.slice(0, 10000),
276
+ });
277
+ } catch {
278
+ extractedContext.push({
279
+ path: full,
280
+ type: "error",
281
+ content: `[Could not read: ${full}]`,
282
+ });
52
283
  }
53
284
  }
54
- } catch (e) {
55
- // ignore
56
285
  }
57
286
  }
58
- remainingWords.push(word);
59
287
  }
60
288
 
61
- return { cleanPrompt: remainingWords.join(" "), extractedContext: contextString };
289
+ for (const p of paths) {
290
+ const stat = fs.statSync(p);
291
+ if (stat.isDirectory()) {
292
+ readDir(p);
293
+ } else if (stat.isFile()) {
294
+ visited.add(p);
295
+ if (isBinary(p)) {
296
+ extractedContext.push({
297
+ path: p,
298
+ type: "binary",
299
+ content: `[Binary file: ${path.basename(p)}]`,
300
+ });
301
+ } else {
302
+ try {
303
+ const raw = fs.readFileSync(p, "utf-8");
304
+ extractedContext.push({
305
+ path: p,
306
+ type: "text",
307
+ content: raw.slice(0, 10000),
308
+ });
309
+ } catch {
310
+ extractedContext.push({
311
+ path: p,
312
+ type: "error",
313
+ content: `[Could not read: ${p}]`,
314
+ });
315
+ }
316
+ }
317
+ }
318
+ }
319
+
320
+ return {
321
+ cleanPrompt: nonPaths.join(" "),
322
+ extractedContext,
323
+ };
62
324
  }
63
325
 
64
- function loadSkillContext(skillName) {
65
- const os = require('os');
66
- const skillsDir = path.join(os.homedir(), '.rgcli', 'skills');
67
- const skillPromptPath = path.join(skillsDir, skillName, 'prompt.md');
68
-
69
- if (fs.existsSync(skillPromptPath)) {
70
- return fs.readFileSync(skillPromptPath, 'utf-8');
326
+ /* ═══════════════════════════════════════════
327
+ loadSkillContext(skillName)
328
+ ═══════════════════════════════════════════ */
329
+ export function loadSkillContext(skillName) {
330
+ const promptPath = path.join(getSkillsDir(), skillName, "prompt.md");
331
+ if (fs.existsSync(promptPath)) {
332
+ try {
333
+ return fs.readFileSync(promptPath, "utf-8");
334
+ } catch {
335
+ return `You are an AI assistant specialized in "${skillName}". Follow best practices and produce clean, production-ready output.`;
336
+ }
71
337
  }
72
-
73
- // If no local file exists but it's invoked, we provide a dynamic fallback
74
- return `You are now operating strictly as the '${skillName}' expert. Apply all industry best practices, deep technical knowledge, and hyper-focused analysis associated with this role.`;
338
+ // dynamic generic prompt
339
+ return [
340
+ `You are an AI coding agent with the "${skillName}" skill activated.`,
341
+ `Focus all responses on the domain of "${skillName}".`,
342
+ `Provide expert-level guidance, code, and explanations.`,
343
+ `Always produce complete, working implementations.`,
344
+ ].join("\n");
75
345
  }
76
346
 
347
+ /* ═══════════════════════════════════════════
348
+ askAgent(promptText, extraContext)
349
+ ═══════════════════════════════════════════ */
77
350
  export async function askAgent(promptText, extraContext = "") {
351
+ const chalk = (await import("chalk")).default;
352
+ const ora = (await import("ora")).default;
353
+
78
354
  const token = getToken();
79
355
  if (!token) {
80
- throw new Error("Not logged in. Please run 'rgcli login <token>' first.");
356
+ printError(
357
+ "Not authenticated. Run " +
358
+ chalk.cyan("rg login") +
359
+ " first."
360
+ );
361
+ return null;
81
362
  }
82
363
 
83
- const ora = (await import("ora")).default;
84
- const chalk = (await import("chalk")).default;
85
-
86
- const spinner = ora({
87
- text: chalk.magenta("RafayGen Agent is analyzing your request..."),
88
- spinner: "dots12"
89
- }).start();
364
+ // Build final prompt
365
+ const state = getSessionState();
366
+ const parts = [promptText];
367
+ if (extraContext) parts.push(`\n---\nContext:\n${extraContext}`);
368
+
369
+ // Attach file context from state
370
+ if (state.attachedFiles.size > 0) {
371
+ const fileCtx = [];
372
+ for (const fp of state.attachedFiles) {
373
+ if (fs.existsSync(fp)) {
374
+ if (isBinary(fp)) {
375
+ fileCtx.push(`[Binary: ${path.basename(fp)}]`);
376
+ } else {
377
+ try {
378
+ const c = fs.readFileSync(fp, "utf-8").slice(0, 10000);
379
+ fileCtx.push(`--- ${fp} ---\n${c}\n---`);
380
+ } catch {
381
+ fileCtx.push(`[Unreadable: ${fp}]`);
382
+ }
383
+ }
384
+ }
385
+ }
386
+ if (fileCtx.length) {
387
+ parts.push(`\n---\nAttached Files:\n${fileCtx.join("\n\n")}`);
388
+ }
389
+ }
390
+
391
+ // Active skill context
392
+ if (state.activeSkill) {
393
+ const skillCtx = loadSkillContext(state.activeSkill);
394
+ parts.push(`\n---\nActive Skill (${state.activeSkill}):\n${skillCtx}`);
395
+ }
396
+
397
+ // Image context
398
+ if (state.imageAttached) {
399
+ parts.push(`\n---\n[Image attached: ${state.imageAttached}]`);
400
+ }
401
+
402
+ // Reasoning effort
403
+ if (state.reasoningEffort && state.reasoningEffort !== "medium") {
404
+ parts.push(`\n[Reasoning effort: ${state.reasoningEffort}]`);
405
+ }
406
+
407
+ const finalPrompt = parts.join("\n");
408
+
409
+ const spinner = ora({ text: "Thinking...", spinner: "dots12" }).start();
90
410
 
91
411
  try {
92
- const finalPrompt = extraContext ? `[CONTEXT: ${extraContext}]\n${promptText}` : promptText;
93
-
94
- // We pass the token via Bearer, and Google-Access-Token header for OAuth natively
95
412
  const headers = {
96
413
  "Content-Type": "application/json",
97
- "Authorization": `Bearer ${token}`
414
+ Authorization: `Bearer ${token}`,
98
415
  };
99
416
 
100
- // If it's a long JWT/OAuth token, also pass it to Google-Access-Token for backend intercept
417
+ // Google access token detection (OAuth tokens are long)
101
418
  if (token.length > 100) {
102
419
  headers["Google-Access-Token"] = token;
103
420
  }
104
-
105
- // Send model preference
106
- const modelPref = getModel();
107
- if (modelPref && modelPref !== 'default') {
108
- headers["X-Model-Override"] = modelPref;
421
+
422
+ const model = getModel();
423
+ if (model && model !== "default") {
424
+ headers["X-Model-Override"] = model;
109
425
  }
110
426
 
111
- const res = await fetch(getApiUrl(), {
427
+ const apiUrl = getApiUrl();
428
+ const resp = await fetch(apiUrl, {
112
429
  method: "POST",
113
430
  headers,
114
431
  body: JSON.stringify({ prompt: finalPrompt }),
115
432
  });
116
433
 
117
- if (!res.ok) {
118
- const err = await res.json().catch(() => ({}));
119
- throw new Error(err.error || `HTTP error! status: ${res.status}`);
434
+ const rawText = await resp.text();
435
+
436
+ // Check for HTML error pages
437
+ if (rawText.toLowerCase().includes("<html")) {
438
+ spinner.stop();
439
+ printError(
440
+ "Endpoint not available. The server returned an HTML page instead of JSON."
441
+ );
442
+ return null;
443
+ }
444
+
445
+ let data;
446
+ try {
447
+ data = JSON.parse(rawText);
448
+ } catch {
449
+ spinner.stop();
450
+ printError("Invalid response from backend. Could not parse JSON.");
451
+ if (rawText.length < 500) {
452
+ console.log(_chalkSync().gray(`Raw response: ${rawText.slice(0, 300)}`));
453
+ }
454
+ return null;
455
+ }
456
+
457
+ if (!resp.ok) {
458
+ spinner.stop();
459
+ const errMsg =
460
+ data.error || data.message || data.msg || `HTTP ${resp.status}`;
461
+ printError(`Backend error: ${errMsg}`);
462
+ return null;
120
463
  }
121
464
 
122
- const data = await res.json();
123
-
124
- spinner.stop(); // Stop the loader once we get a response!
125
-
126
- if (data.message) {
127
- console.log(`\n🤖 ${data.message}\n`);
465
+ spinner.stop();
466
+
467
+ // Render the response message
468
+ const message = data.message || data.response || data.text || data.content || "";
469
+ if (message) {
470
+ console.log("");
471
+ renderMarkdown(message);
472
+ console.log("");
128
473
  }
129
474
 
130
- if (data.actions && data.actions.length > 0) {
475
+ // Execute any actions the agent returned
476
+ if (Array.isArray(data.actions)) {
131
477
  for (const action of data.actions) {
132
478
  await executeAction(action);
133
479
  }
134
- printSuccess("All actions executed successfully!");
135
480
  }
136
481
 
137
- } catch (error) {
482
+ // Save to conversation history
483
+ addToHistory("user", promptText);
484
+ addToHistory("assistant", message);
485
+
486
+ return data;
487
+ } catch (err) {
138
488
  spinner.stop();
139
- printError(error.message);
489
+ if (err.code === "ECONNREFUSED") {
490
+ printError(
491
+ "Cannot connect to the RafayGen backend. Is the server running?"
492
+ );
493
+ } else if (err.code === "ENOTFOUND") {
494
+ printError("DNS resolution failed. Check your API URL.");
495
+ } else {
496
+ printError(`Request failed: ${err.message}`);
497
+ }
498
+ return null;
140
499
  }
141
500
  }
142
501
 
502
+ /* ═══════════════════════════════════════════
503
+ MCP helpers
504
+ ═══════════════════════════════════════════ */
505
+ function loadMcpConfig() {
506
+ const p = getMcpConfigPath();
507
+ if (fs.existsSync(p)) {
508
+ try {
509
+ return JSON.parse(fs.readFileSync(p, "utf-8"));
510
+ } catch {
511
+ return { servers: [] };
512
+ }
513
+ }
514
+ return { servers: [] };
515
+ }
516
+
517
+ function saveMcpConfig(config) {
518
+ const p = getMcpConfigPath();
519
+ fs.writeFileSync(p, JSON.stringify(config, null, 2), "utf-8");
520
+ }
521
+
522
+ /* ═══════════════════════════════════════════
523
+ startInteractiveLoop()
524
+ ═══════════════════════════════════════════ */
143
525
  export async function startInteractiveLoop() {
144
- const inquirer = (await import("inquirer")).default;
145
526
  const chalk = (await import("chalk")).default;
527
+ const inquirer = (await import("inquirer")).default;
528
+ _chalkCache = chalk;
146
529
 
147
- console.log(chalk.cyan("\nWelcome to RafayGen CLI. Type '/help' for commands, or '/exit' to quit.\n"));
530
+ // ── Welcome ──
531
+ printAsciiLogo();
532
+ printRandomWelcome();
533
+ const state = getSessionState();
534
+ console.log(
535
+ chalk.gray(
536
+ ` Session: ${state.sessionId.slice(0, 8)}... | Model: ${getModel()} | Sandbox: ${state.sandboxMode}`
537
+ )
538
+ );
539
+ console.log(
540
+ chalk.gray(` Type ${chalk.white("/help")} for commands, or just start chatting.\n`)
541
+ );
148
542
 
543
+ // ── REPL ──
149
544
  while (true) {
150
- const { promptText } = await inquirer.prompt([
151
- {
152
- type: "input",
153
- name: "promptText",
154
- message: "rgcli>",
155
- prefix: ""
156
- }
157
- ]);
545
+ let input;
546
+ try {
547
+ const answers = await inquirer.prompt([
548
+ {
549
+ type: "input",
550
+ name: "input",
551
+ message: chalk.greenBright("❯"),
552
+ prefix: "",
553
+ },
554
+ ]);
555
+ input = (answers.input || "").trim();
556
+ } catch {
557
+ // Ctrl+C or EOF
558
+ saveSession();
559
+ console.log(chalk.gray("\nSession saved. Goodbye!\n"));
560
+ process.exit(0);
561
+ }
158
562
 
159
- const input = promptText.trim();
160
563
  if (!input) continue;
161
564
 
162
- // Handle Local Slash Commands
163
- if (input.toLowerCase() === "/exit") {
164
- console.log(chalk.gray("Goodbye!"));
565
+ /* ─────────────────────────────────────
566
+ SLASH COMMANDS
567
+ ───────────────────────────────────── */
568
+
569
+ // /exit, /quit
570
+ if (input === "/exit" || input === "/quit") {
571
+ saveSession();
572
+ console.log(chalk.cyan("\n 💾 Session saved. See you next time!\n"));
165
573
  process.exit(0);
166
- } else if (input.toLowerCase() === "/clear") {
574
+ }
575
+
576
+ // /clear
577
+ if (input === "/clear") {
167
578
  console.clear();
168
579
  continue;
169
- } else if (input.toLowerCase() === "/help") {
170
- console.log(chalk.cyan("\nAvailable Commands:"));
171
- console.log(" /exit - Exit the CLI");
172
- console.log(" /clear - Clear the terminal screen");
173
- console.log(" /help - Show this help message");
174
- console.log(" /models - Open interactive menu to switch AI model globally");
175
- console.log(" /skills - List all available skills");
176
- console.log(" /skill - Manage skills (install, remove, enable)");
177
- console.log(" /web <query> - Force a web search for context");
178
- console.log(" /vision <path> - Attach an image to your prompt");
179
- console.log(" /sandbox - Manage sandbox modes (read-only, danger-full-access)");
180
- console.log(" /status - View current session state and flags");
181
- console.log("\nAny other input will be sent to the RafayGen agent.\n");
182
- continue;
183
- } else if (input.toLowerCase() === "/skills") {
184
- const os = require('os');
185
- const skillsDir = path.join(os.homedir(), '.rgcli', 'skills');
186
- console.log(chalk.cyan(`\n--- INSTALLED SKILLS ---`));
187
- if (fs.existsSync(skillsDir)) {
188
- const skills = fs.readdirSync(skillsDir, { withFileTypes: true }).filter(d => d.isDirectory());
189
- if (skills.length === 0) console.log(chalk.gray("No custom skills installed yet."));
190
- skills.forEach(s => console.log(chalk.white(` $${s.name}`)));
191
- } else {
192
- console.log(chalk.gray(`No skills directory found at ${skillsDir}`));
580
+ }
581
+
582
+ // / (command palette)
583
+ if (input === "/") {
584
+ const commands = [
585
+ { name: "📋 /help — Show all commands", value: "/help" },
586
+ { name: "🤖 /models — Switch AI model", value: "/models" },
587
+ { name: "📊 /status — Show session status", value: "/status" },
588
+ { name: "📝 /history — View conversation history", value: "/history" },
589
+ { name: "🔀 /fork — Fork current session", value: "/fork" },
590
+ { name: "/resume Resume a saved session", value: "/resume" },
591
+ { name: "📦 /compact — Toggle compact mode", value: "/compact" },
592
+ { name: "🔍 /review — Review code in CWD", value: "/review" },
593
+ { name: "📄 /diff — Show git diff", value: "/diff" },
594
+ { name: "📎 /attach — Attach file/folder", value: "/attach" },
595
+ { name: "🗑 /detach — Clear attachments", value: "/detach" },
596
+ { name: "🌐 /web — Web search", value: "/web" },
597
+ { name: "🖼 /vision — Attach image", value: "/vision" },
598
+ { name: "🔎 /search — Search in CWD", value: "/search" },
599
+ { name: "🔒 /sandbox — Set sandbox mode", value: "/sandbox" },
600
+ { name: "✅ /approvals — Set approval mode", value: "/approvals" },
601
+ { name: "🧩 /skills List skills", value: "/skills" },
602
+ { name: "🔌 /mcp — MCP servers", value: "/mcp" },
603
+ { name: "🧠 /reasoning — Set reasoning effort", value: "/reasoning" },
604
+ { name: "🚪 /exit — Save & exit", value: "/exit" },
605
+ ];
606
+ const { cmd } = await inquirer.prompt([
607
+ {
608
+ type: "list",
609
+ name: "cmd",
610
+ message: "Command Palette",
611
+ choices: commands,
612
+ pageSize: 20,
613
+ },
614
+ ]);
615
+ // Re-process the selected command
616
+ input = cmd;
617
+ if (input === "/exit") {
618
+ saveSession();
619
+ console.log(chalk.cyan("\n 💾 Session saved. See you next time!\n"));
620
+ process.exit(0);
193
621
  }
194
- console.log(chalk.yellow("\n(Built-in dynamic skills like $planner, $debugger, $react-expert are always available!)\n"));
622
+ // fall through to handle below
623
+ }
624
+
625
+ // /help
626
+ if (input === "/help") {
627
+ const helpText = [
628
+ chalk.cyan.bold.underline(" RafayGen CLI — Command Reference\n"),
629
+ chalk.yellow.bold(" Basic:"),
630
+ ` ${chalk.white("/help")} Show this help`,
631
+ ` ${chalk.white("/clear")} Clear console`,
632
+ ` ${chalk.white("/exit, /quit")} Save session & exit`,
633
+ "",
634
+ chalk.yellow.bold(" Model Management:"),
635
+ ` ${chalk.white("/models, /model")} Pick an AI model`,
636
+ "",
637
+ chalk.yellow.bold(" Session Management:"),
638
+ ` ${chalk.white("/status")} Show full session state`,
639
+ ` ${chalk.white("/compact")} Toggle compact output mode`,
640
+ ` ${chalk.white("/fork")} Fork session with same history`,
641
+ ` ${chalk.white("/resume")} Resume a saved session`,
642
+ ` ${chalk.white("/history")} Show last 10 conversation turns`,
643
+ "",
644
+ chalk.yellow.bold(" Code Workflows:"),
645
+ ` ${chalk.white("/review")} Ask agent to review code in CWD`,
646
+ ` ${chalk.white("/diff")} Show git diff of CWD`,
647
+ ` ${chalk.white("/attach <path>")} Attach file or folder to context`,
648
+ ` ${chalk.white("/detach")} Clear all attached files`,
649
+ "",
650
+ chalk.yellow.bold(" File Operations:"),
651
+ ` ${chalk.white("/web <query>")} Force web search context`,
652
+ ` ${chalk.white("/vision <path>")} Attach image to context`,
653
+ ` ${chalk.white("/search <query>")} Grep search in CWD`,
654
+ "",
655
+ chalk.yellow.bold(" Sandbox & Approvals:"),
656
+ ` ${chalk.white("/sandbox [mode]")} Set sandbox (read-only|workspace-write|danger-full-access)`,
657
+ ` ${chalk.white("/approvals [mode]")} Set approval (suggest|auto-edit|full-auto|never)`,
658
+ "",
659
+ chalk.yellow.bold(" Skills:"),
660
+ ` ${chalk.white("/skills")} List installed & built-in skills`,
661
+ ` ${chalk.white("/skill install <name>")} Install a new skill`,
662
+ ` ${chalk.white("/skill remove <name>")} Remove a skill`,
663
+ ` ${chalk.white("/skill enable <name>")} Set active skill`,
664
+ ` ${chalk.white("/skill disable <name>")} Deactivate skill`,
665
+ ` ${chalk.white("$<skill> <prompt>")} Run prompt with skill context`,
666
+ "",
667
+ chalk.yellow.bold(" MCP (Model Context Protocol):"),
668
+ ` ${chalk.white("/mcp")} List MCP servers`,
669
+ ` ${chalk.white("/mcp add")} Add an MCP server`,
670
+ ` ${chalk.white("/mcp remove <name>")} Remove an MCP server`,
671
+ ` ${chalk.white("/mcp inspect <name>")} Inspect MCP server config`,
672
+ "",
673
+ chalk.yellow.bold(" Agent Reasoning:"),
674
+ ` ${chalk.white("/reasoning <level>")} Set reasoning effort (low|medium|high)`,
675
+ "",
676
+ chalk.yellow.bold(" Tips:"),
677
+ ` ${chalk.gray("•")} Drag & drop files into the prompt to auto-attach context`,
678
+ ` ${chalk.gray("•")} Type ${chalk.white("/")} alone to open the command palette`,
679
+ "",
680
+ ].join("\n");
681
+ console.log(helpText);
195
682
  continue;
196
- } else if (input.toLowerCase().startsWith("/skill ")) {
197
- const parts = input.split(" ");
198
- const action = parts[1];
199
- const skillName = parts[2];
200
- console.log(chalk.blue(`\n[Skill Manager] Action '${action}' for skill '${skillName}' executed. (Registry sync pending)\n`));
683
+ }
684
+
685
+ // /models, /model
686
+ if (input === "/models" || input === "/model") {
687
+ const modelChoices = [
688
+ { name: "Google Gemini (Default)", value: "default" },
689
+ { name: "Groq — Llama 3.1 70B", value: "groq-llama-3.1-70b" },
690
+ { name: "Mistral — Large", value: "mistral-large" },
691
+ { name: "DeepSeek — Coder", value: "deepseek-coder" },
692
+ { name: "Qwen — Max", value: "qwen-max" },
693
+ { name: "Ollama — Cloud", value: "ollama-cloud" },
694
+ { name: "MuleRouter — Auto", value: "mulerouter-auto" },
695
+ { name: "HuggingFace — Zephyr", value: "huggingface-zephyr" },
696
+ ];
697
+ const { selectedModel } = await inquirer.prompt([
698
+ {
699
+ type: "list",
700
+ name: "selectedModel",
701
+ message: "Select an AI model:",
702
+ choices: modelChoices,
703
+ },
704
+ ]);
705
+ setModel(selectedModel);
706
+ printModelBadge(
707
+ modelChoices.find((m) => m.value === selectedModel)?.name ||
708
+ selectedModel
709
+ );
710
+ printSuccess(`Model switched to ${selectedModel}`);
201
711
  continue;
202
- } else if (input.toLowerCase().startsWith("/sandbox ")) {
203
- const mode = input.split(" ")[1];
204
- updateSessionState({ sandboxMode: mode });
205
- console.log(chalk.yellow(`\n⚠️ Sandbox mode updated to: ${mode}\n`));
712
+ }
713
+
714
+ // /status
715
+ if (input === "/status") {
716
+ printSessionStatus(getSessionState());
206
717
  continue;
207
- } else if (input.toLowerCase() === "/status") {
208
- const state = getSessionState();
209
- console.log(chalk.cyan(`\n--- ACTIVE SESSION STATUS ---`));
210
- console.log(chalk.white(`Model Override: ${getModel()}`));
211
- console.log(chalk.white(`Sandbox Mode: ${state.sandboxMode}`));
212
- console.log(chalk.white(`Approval Mode: ${state.approvalMode}`));
213
- console.log(chalk.white(`Reasoning: ${state.reasoningEffort}`));
214
- console.log(chalk.white(`CWD: ${state.cwd}`));
215
- console.log("");
718
+ }
719
+
720
+ // /compact
721
+ if (input === "/compact") {
722
+ const cur = getSessionState();
723
+ updateSessionState({ compactMode: !cur.compactMode });
724
+ const newVal = getSessionState().compactMode;
725
+ printSuccess(`Compact mode ${newVal ? "ENABLED" : "DISABLED"}`);
216
726
  continue;
217
- } else if (input.toLowerCase() === "/mcp" || input.toLowerCase() === "/tools" || input.toLowerCase() === "/agents") {
218
- console.log(chalk.blue(`\n[Feature '${input}'] Local architecture loaded. Awaiting backend orchestration sync in upcoming release.\n`));
727
+ }
728
+
729
+ // /fork
730
+ if (input === "/fork") {
731
+ saveSession();
732
+ const oldId = getSessionState().sessionId;
733
+ const newId = crypto.randomUUID();
734
+ updateSessionState({ sessionId: newId });
735
+ saveSession();
736
+ console.log(chalk.green(`\n 🔀 Session forked!`));
737
+ console.log(chalk.gray(` Old: ${oldId}`));
738
+ console.log(chalk.cyan(` New: ${newId}\n`));
219
739
  continue;
220
- } else if (input.toLowerCase() === "/models") {
221
- const { selectedModel } = await inquirer.prompt([
740
+ }
741
+
742
+ // /resume
743
+ if (input === "/resume") {
744
+ const sessions = listSessions();
745
+ if (sessions.length === 0) {
746
+ printWarning("No saved sessions found.");
747
+ continue;
748
+ }
749
+ const choices = sessions.map((s) => ({
750
+ name: `${s.sessionId.slice(0, 8)}... | ${s.messageCount} msgs | ${s.savedAt || "unknown"}`,
751
+ value: s.sessionId,
752
+ }));
753
+ const { sessionId } = await inquirer.prompt([
222
754
  {
223
755
  type: "list",
224
- name: "selectedModel",
225
- message: "Select an AI Provider to use globally:",
226
- choices: [
227
- { name: "Google Gemini", value: "default" },
228
- { name: "Groq (Llama 3.1 70B)", value: "groq" },
229
- { name: "Mistral (Large)", value: "mistral" },
230
- { name: "DeepSeek (Coder)", value: "deepseek" },
231
- { name: "Qwen (Max)", value: "qwen" },
232
- { name: "Ollama (Cloud)", value: "ollama" },
233
- { name: "MuleRouter (Auto)", value: "mulerouter" }
234
- ]
235
- }
756
+ name: "sessionId",
757
+ message: "Select a session to resume:",
758
+ choices,
759
+ pageSize: 15,
760
+ },
236
761
  ]);
237
- setModel(selectedModel);
238
- console.log(chalk.green(`\n✅ Active model set globally to: ${selectedModel}\n`));
762
+ if (loadSession(sessionId)) {
763
+ printSuccess(`Resumed session ${sessionId.slice(0, 8)}...`);
764
+ printSessionStatus(getSessionState());
765
+ } else {
766
+ printError(`Failed to load session ${sessionId}`);
767
+ }
768
+ continue;
769
+ }
770
+
771
+ // /history
772
+ if (input === "/history") {
773
+ const hist = getSessionState().conversationHistory;
774
+ if (hist.length === 0) {
775
+ printInfo("No conversation history yet.");
776
+ continue;
777
+ }
778
+ const last10 = hist.slice(-10);
779
+ console.log(chalk.cyan.bold("\n 📝 Conversation History (last 10):\n"));
780
+ for (const entry of last10) {
781
+ const role =
782
+ entry.role === "user"
783
+ ? chalk.greenBright.bold(" YOU: ")
784
+ : entry.role === "assistant"
785
+ ? chalk.magentaBright.bold(" AI: ")
786
+ : chalk.gray.bold(" SYS: ");
787
+ const ts = entry.timestamp
788
+ ? chalk.gray(` [${new Date(entry.timestamp).toLocaleTimeString()}]`)
789
+ : "";
790
+ const content =
791
+ entry.content.length > 200
792
+ ? entry.content.slice(0, 200) + "..."
793
+ : entry.content;
794
+ console.log(role + content + ts);
795
+ console.log(chalk.gray(" " + "─".repeat(60)));
796
+ }
797
+ console.log("");
798
+ continue;
799
+ }
800
+
801
+ // /review
802
+ if (input === "/review") {
803
+ const cwd = getSessionState().cwd;
804
+ printStep(`Scanning ${cwd} for code review...`);
805
+ const { extractedContext } = extractFileContext(cwd);
806
+ const contextStr = extractedContext
807
+ .filter((f) => f.type === "text")
808
+ .map((f) => `--- ${f.path} ---\n${f.content}`)
809
+ .join("\n\n");
810
+ if (!contextStr) {
811
+ printWarning("No readable files found in current directory.");
812
+ continue;
813
+ }
814
+ await askAgent(
815
+ "Please review the following code. Identify bugs, security issues, performance problems, and suggest improvements.",
816
+ contextStr.slice(0, 50000)
817
+ );
818
+ continue;
819
+ }
820
+
821
+ // /diff
822
+ if (input === "/diff") {
823
+ const cwd = getSessionState().cwd;
824
+ try {
825
+ const diffOutput = execSync("git diff", {
826
+ cwd,
827
+ encoding: "utf-8",
828
+ timeout: 10000,
829
+ });
830
+ if (diffOutput.trim()) {
831
+ renderBox(" Git Diff ", diffOutput, "yellow");
832
+ } else {
833
+ printInfo("No uncommitted changes found.");
834
+ }
835
+ } catch (err) {
836
+ if (err.message.includes("not a git repository")) {
837
+ printError("Not a git repository.");
838
+ } else {
839
+ printError(`git diff failed: ${err.message}`);
840
+ }
841
+ }
842
+ continue;
843
+ }
844
+
845
+ // /attach <path>
846
+ if (input.startsWith("/attach")) {
847
+ const arg = input.slice(7).trim();
848
+ if (!arg) {
849
+ const { filePath } = await inquirer.prompt([
850
+ {
851
+ type: "input",
852
+ name: "filePath",
853
+ message: "Path to attach:",
854
+ },
855
+ ]);
856
+ if (filePath.trim()) {
857
+ const resolved = path.isAbsolute(filePath.trim())
858
+ ? filePath.trim()
859
+ : path.resolve(process.cwd(), filePath.trim());
860
+ if (fs.existsSync(resolved)) {
861
+ const cur = getSessionState();
862
+ cur.attachedFiles.add(resolved);
863
+ updateSessionState({ attachedFiles: cur.attachedFiles });
864
+ printSuccess(`Attached: ${resolved}`);
865
+ } else {
866
+ printError(`Path not found: ${filePath.trim()}`);
867
+ }
868
+ }
869
+ } else {
870
+ const resolved = path.isAbsolute(arg)
871
+ ? arg
872
+ : path.resolve(process.cwd(), arg);
873
+ if (fs.existsSync(resolved)) {
874
+ const cur = getSessionState();
875
+ cur.attachedFiles.add(resolved);
876
+ updateSessionState({ attachedFiles: cur.attachedFiles });
877
+ printSuccess(`Attached: ${resolved}`);
878
+ } else {
879
+ printError(`Path not found: ${arg}`);
880
+ }
881
+ }
882
+ continue;
883
+ }
884
+
885
+ // /detach
886
+ if (input === "/detach") {
887
+ updateSessionState({ attachedFiles: new Set() });
888
+ printSuccess("All attached files cleared.");
889
+ continue;
890
+ }
891
+
892
+ // /web <query>
893
+ if (input.startsWith("/web")) {
894
+ const query = input.slice(4).trim();
895
+ if (!query) {
896
+ printWarning("Usage: /web <search query>");
897
+ continue;
898
+ }
899
+ await askAgent(
900
+ `Perform a web search for: "${query}" and summarize the results.`,
901
+ `[Web search context requested for: ${query}]`
902
+ );
903
+ continue;
904
+ }
905
+
906
+ // /vision <path>
907
+ if (input.startsWith("/vision")) {
908
+ const imgPath = input.slice(7).trim();
909
+ if (!imgPath) {
910
+ printWarning("Usage: /vision <image-path>");
911
+ continue;
912
+ }
913
+ const resolved = path.isAbsolute(imgPath)
914
+ ? imgPath
915
+ : path.resolve(process.cwd(), imgPath);
916
+ if (!fs.existsSync(resolved)) {
917
+ printError(`Image not found: ${resolved}`);
918
+ continue;
919
+ }
920
+ updateSessionState({ imageAttached: resolved });
921
+ printSuccess(`Image attached: ${resolved}`);
922
+ continue;
923
+ }
924
+
925
+ // /search <query>
926
+ if (input.startsWith("/search")) {
927
+ const query = input.slice(7).trim();
928
+ if (!query) {
929
+ printWarning("Usage: /search <query>");
930
+ continue;
931
+ }
932
+ const cwd = getSessionState().cwd;
933
+ try {
934
+ let result;
935
+ try {
936
+ result = execSync(
937
+ `grep -rnI --include="*.{js,ts,py,json,md,jsx,tsx,css,html,yaml,yml,toml,go,rs,java,c,cpp,h}" "${query}" .`,
938
+ { cwd, encoding: "utf-8", timeout: 15000 }
939
+ );
940
+ } catch {
941
+ // fallback to simple grep
942
+ result = execSync(`grep -rnI "${query}" . 2>/dev/null | head -50`, {
943
+ cwd,
944
+ encoding: "utf-8",
945
+ timeout: 15000,
946
+ });
947
+ }
948
+ if (result.trim()) {
949
+ renderBox(` Search: "${query}" `, result.trim().slice(0, 5000), "green");
950
+ } else {
951
+ printInfo(`No results found for "${query}".`);
952
+ }
953
+ } catch {
954
+ printInfo(`No results found for "${query}".`);
955
+ }
956
+ continue;
957
+ }
958
+
959
+ // /sandbox [mode]
960
+ if (input.startsWith("/sandbox")) {
961
+ const arg = input.slice(8).trim();
962
+ const validModes = ["read-only", "workspace-write", "danger-full-access"];
963
+ if (arg && validModes.includes(arg)) {
964
+ updateSessionState({ sandboxMode: arg });
965
+ printSuccess(`Sandbox mode set to: ${arg}`);
966
+ } else {
967
+ const { mode } = await inquirer.prompt([
968
+ {
969
+ type: "list",
970
+ name: "mode",
971
+ message: "Select sandbox mode:",
972
+ choices: [
973
+ { name: "🔒 Read-Only — No file writes allowed", value: "read-only" },
974
+ { name: "📝 Workspace Write — Write to project files", value: "workspace-write" },
975
+ { name: "⚠️ Danger Full Access — Full system access", value: "danger-full-access" },
976
+ ],
977
+ },
978
+ ]);
979
+ updateSessionState({ sandboxMode: mode });
980
+ printSuccess(`Sandbox mode set to: ${mode}`);
981
+ }
982
+ continue;
983
+ }
984
+
985
+ // /approvals [mode]
986
+ if (input.startsWith("/approvals")) {
987
+ const arg = input.slice(10).trim();
988
+ const validModes = ["suggest", "auto-edit", "full-auto", "never"];
989
+ if (arg && validModes.includes(arg)) {
990
+ updateSessionState({ approvalMode: arg });
991
+ printSuccess(`Approval mode set to: ${arg}`);
992
+ } else {
993
+ const { mode } = await inquirer.prompt([
994
+ {
995
+ type: "list",
996
+ name: "mode",
997
+ message: "Select approval mode:",
998
+ choices: [
999
+ { name: "💬 Suggest — Show diffs, ask before writing", value: "suggest" },
1000
+ { name: "✏️ Auto-Edit — Auto-apply edits, ask for commands", value: "auto-edit" },
1001
+ { name: "🚀 Full-Auto — Apply everything automatically", value: "full-auto" },
1002
+ { name: "🚫 Never — Block all actions", value: "never" },
1003
+ ],
1004
+ },
1005
+ ]);
1006
+ updateSessionState({ approvalMode: mode });
1007
+ printSuccess(`Approval mode set to: ${mode}`);
1008
+ }
1009
+ continue;
1010
+ }
1011
+
1012
+ // /skills
1013
+ if (input === "/skills") {
1014
+ const skillsDir = getSkillsDir();
1015
+ let installed = [];
1016
+ try {
1017
+ installed = fs
1018
+ .readdirSync(skillsDir, { withFileTypes: true })
1019
+ .filter((d) => d.isDirectory())
1020
+ .map((d) => d.name);
1021
+ } catch {}
1022
+
1023
+ const builtIn = [
1024
+ "code-review",
1025
+ "refactor",
1026
+ "test-gen",
1027
+ "docs-gen",
1028
+ "debug",
1029
+ "optimize",
1030
+ "security-audit",
1031
+ "api-design",
1032
+ "database-schema",
1033
+ "devops",
1034
+ ];
1035
+
1036
+ console.log(chalk.cyan.bold("\n 🧩 Installed Skills:\n"));
1037
+ if (installed.length === 0) {
1038
+ console.log(chalk.gray(" (none installed)\n"));
1039
+ } else {
1040
+ for (const s of installed) {
1041
+ const active =
1042
+ getSessionState().activeSkill === s ? chalk.green(" ● ACTIVE") : "";
1043
+ const hasPrompt = fs.existsSync(
1044
+ path.join(skillsDir, s, "prompt.md")
1045
+ );
1046
+ console.log(
1047
+ ` ${chalk.white("•")} ${chalk.bold(s)}${active} ${
1048
+ hasPrompt ? chalk.gray("[prompt.md ✓]") : chalk.yellow("[no prompt]")
1049
+ }`
1050
+ );
1051
+ }
1052
+ console.log("");
1053
+ }
1054
+
1055
+ console.log(chalk.cyan.bold(" 📦 Built-in Skills:\n"));
1056
+ for (const s of builtIn) {
1057
+ console.log(` ${chalk.white("•")} ${chalk.gray(s)}`);
1058
+ }
1059
+ console.log(
1060
+ chalk.gray(
1061
+ `\n Use ${chalk.white("$skillname <prompt>")} to invoke a skill.\n`
1062
+ )
1063
+ );
239
1064
  continue;
240
- } else if (input.toLowerCase().startsWith("/web ")) {
241
- const query = input.substring(5);
242
- console.log(chalk.blue(`\n🔍 Searching the web for: ${query} ...`));
243
- await askAgent("Please perform this task.", `USER REQUESTED WEB SEARCH: ${query}`);
1065
+ }
1066
+
1067
+ // /skill install|remove|enable|disable <name>
1068
+ if (input.startsWith("/skill ")) {
1069
+ const parts = input.slice(7).trim().split(/\s+/);
1070
+ const action = parts[0];
1071
+ const name = parts.slice(1).join(" ");
1072
+
1073
+ if (action === "install") {
1074
+ if (!name) {
1075
+ printWarning("Usage: /skill install <name>");
1076
+ continue;
1077
+ }
1078
+ const skillDir = path.join(getSkillsDir(), name);
1079
+ if (fs.existsSync(skillDir)) {
1080
+ printWarning(`Skill "${name}" already exists.`);
1081
+ continue;
1082
+ }
1083
+ fs.mkdirSync(skillDir, { recursive: true });
1084
+ // Create prompt.md
1085
+ const promptContent = [
1086
+ `# ${name} Skill`,
1087
+ "",
1088
+ `You are an expert assistant specialized in "${name}".`,
1089
+ "",
1090
+ "## Instructions",
1091
+ "",
1092
+ `- Focus all responses on the "${name}" domain.`,
1093
+ "- Provide complete, working implementations.",
1094
+ "- Follow best practices and industry standards.",
1095
+ "- Include error handling and edge cases.",
1096
+ "- Add clear documentation and comments.",
1097
+ "",
1098
+ "## Output Format",
1099
+ "",
1100
+ "- Provide code in fenced code blocks with language tags.",
1101
+ "- Explain your reasoning before showing code.",
1102
+ "- Highlight any assumptions or trade-offs.",
1103
+ "",
1104
+ ].join("\n");
1105
+ fs.writeFileSync(path.join(skillDir, "prompt.md"), promptContent, "utf-8");
1106
+
1107
+ // Create config.json
1108
+ const configContent = {
1109
+ name,
1110
+ version: "1.0.0",
1111
+ description: `Custom skill for ${name}`,
1112
+ author: os.userInfo().username,
1113
+ createdAt: new Date().toISOString(),
1114
+ tags: [name],
1115
+ };
1116
+ fs.writeFileSync(
1117
+ path.join(skillDir, "config.json"),
1118
+ JSON.stringify(configContent, null, 2),
1119
+ "utf-8"
1120
+ );
1121
+
1122
+ printSuccess(
1123
+ `Skill "${name}" installed at ${skillDir}\n Edit ${path.join(skillDir, "prompt.md")} to customize.`
1124
+ );
1125
+ continue;
1126
+ }
1127
+
1128
+ if (action === "remove") {
1129
+ if (!name) {
1130
+ printWarning("Usage: /skill remove <name>");
1131
+ continue;
1132
+ }
1133
+ const skillDir = path.join(getSkillsDir(), name);
1134
+ if (!fs.existsSync(skillDir)) {
1135
+ printError(`Skill "${name}" not found.`);
1136
+ continue;
1137
+ }
1138
+ fs.rmSync(skillDir, { recursive: true, force: true });
1139
+ if (getSessionState().activeSkill === name) {
1140
+ updateSessionState({ activeSkill: null });
1141
+ }
1142
+ printSuccess(`Skill "${name}" removed.`);
1143
+ continue;
1144
+ }
1145
+
1146
+ if (action === "enable") {
1147
+ if (!name) {
1148
+ printWarning("Usage: /skill enable <name>");
1149
+ continue;
1150
+ }
1151
+ updateSessionState({ activeSkill: name });
1152
+ printSuccess(`Skill "${name}" enabled as active skill.`);
1153
+ continue;
1154
+ }
1155
+
1156
+ if (action === "disable") {
1157
+ if (!name) {
1158
+ printWarning("Usage: /skill disable <name>");
1159
+ continue;
1160
+ }
1161
+ if (getSessionState().activeSkill === name) {
1162
+ updateSessionState({ activeSkill: null });
1163
+ printSuccess(`Skill "${name}" disabled.`);
1164
+ } else {
1165
+ printInfo(`Skill "${name}" was not active.`);
1166
+ }
1167
+ continue;
1168
+ }
1169
+
1170
+ printWarning(
1171
+ "Unknown skill command. Use: install, remove, enable, disable"
1172
+ );
244
1173
  continue;
245
- } else if (input.toLowerCase().startsWith("/vision ")) {
246
- const path = input.substring(8);
247
- console.log(chalk.magenta(`\n👁️ Attaching vision context from: ${path} ...`));
248
- await askAgent("Please analyze this image and perform the task.", `USER ATTACHED IMAGE AT PATH: ${path}`);
1174
+ }
1175
+
1176
+ // /mcp
1177
+ if (input === "/mcp" || input === "/mcp list") {
1178
+ const config = loadMcpConfig();
1179
+ const servers = config.servers || [];
1180
+ if (servers.length === 0) {
1181
+ printInfo("No MCP servers configured.");
1182
+ console.log(
1183
+ chalk.gray(` Use ${chalk.white("/mcp add")} to add one.\n`)
1184
+ );
1185
+ } else {
1186
+ console.log(chalk.cyan.bold("\n 🔌 MCP Servers:\n"));
1187
+ for (const s of servers) {
1188
+ console.log(
1189
+ ` ${chalk.white("•")} ${chalk.bold(s.name)} — ${chalk.gray(s.url)} ${
1190
+ s.auth ? chalk.yellow("[auth]") : chalk.gray("[no auth]")
1191
+ }`
1192
+ );
1193
+ }
1194
+ console.log("");
1195
+ }
249
1196
  continue;
250
1197
  }
251
1198
 
252
- // Extract attached files via drag-and-drop
253
- let { cleanPrompt, extractedContext } = extractFileContext(input);
1199
+ // /mcp add
1200
+ if (input === "/mcp add") {
1201
+ const answers = await inquirer.prompt([
1202
+ {
1203
+ type: "input",
1204
+ name: "name",
1205
+ message: "Server name:",
1206
+ validate: (v) => (v.trim() ? true : "Name is required"),
1207
+ },
1208
+ {
1209
+ type: "input",
1210
+ name: "url",
1211
+ message: "Server URL:",
1212
+ validate: (v) => (v.trim() ? true : "URL is required"),
1213
+ },
1214
+ {
1215
+ type: "input",
1216
+ name: "auth",
1217
+ message: "Auth token (leave empty for none):",
1218
+ },
1219
+ {
1220
+ type: "input",
1221
+ name: "description",
1222
+ message: "Description (optional):",
1223
+ },
1224
+ ]);
1225
+ const config = loadMcpConfig();
1226
+ config.servers = config.servers || [];
1227
+ config.servers.push({
1228
+ name: answers.name.trim(),
1229
+ url: answers.url.trim(),
1230
+ auth: answers.auth.trim() || null,
1231
+ description: answers.description.trim() || "",
1232
+ addedAt: new Date().toISOString(),
1233
+ });
1234
+ saveMcpConfig(config);
1235
+ printSuccess(`MCP server "${answers.name.trim()}" added.`);
1236
+ continue;
1237
+ }
254
1238
 
255
- if (extractedContext) {
256
- console.log(chalk.blue(`\n📎 Attached file context detected and loaded.`));
1239
+ // /mcp remove <name>
1240
+ if (input.startsWith("/mcp remove ")) {
1241
+ const name = input.slice(12).trim();
1242
+ if (!name) {
1243
+ printWarning("Usage: /mcp remove <name>");
1244
+ continue;
1245
+ }
1246
+ const config = loadMcpConfig();
1247
+ const before = (config.servers || []).length;
1248
+ config.servers = (config.servers || []).filter(
1249
+ (s) => s.name.toLowerCase() !== name.toLowerCase()
1250
+ );
1251
+ if (config.servers.length < before) {
1252
+ saveMcpConfig(config);
1253
+ printSuccess(`MCP server "${name}" removed.`);
1254
+ } else {
1255
+ printError(`MCP server "${name}" not found.`);
1256
+ }
1257
+ continue;
257
1258
  }
258
1259
 
259
- // Detect Skill Execution (e.g. $planner do something)
260
- let systemContext = extractedContext;
261
- if (cleanPrompt.startsWith("$")) {
262
- const skillName = cleanPrompt.split(" ")[0].substring(1);
263
- cleanPrompt = cleanPrompt.substring(skillName.length + 2).trim();
264
-
265
- const skillContext = loadSkillContext(skillName);
266
- systemContext += `\n[ACTIVE SKILL: ${skillName}]\n${skillContext}\n`;
267
- console.log(chalk.magenta(`\n🔮 Active Skill Injected: ${skillName}`));
1260
+ // /mcp inspect <name>
1261
+ if (input.startsWith("/mcp inspect ")) {
1262
+ const name = input.slice(13).trim();
1263
+ if (!name) {
1264
+ printWarning("Usage: /mcp inspect <name>");
1265
+ continue;
1266
+ }
1267
+ const config = loadMcpConfig();
1268
+ const server = (config.servers || []).find(
1269
+ (s) => s.name.toLowerCase() === name.toLowerCase()
1270
+ );
1271
+ if (server) {
1272
+ renderBox(
1273
+ ` MCP: ${server.name} `,
1274
+ [
1275
+ `Name: ${server.name}`,
1276
+ `URL: ${server.url}`,
1277
+ `Auth: ${server.auth ? "••••••" + server.auth.slice(-4) : "none"}`,
1278
+ `Description: ${server.description || "(none)"}`,
1279
+ `Added: ${server.addedAt || "unknown"}`,
1280
+ ].join("\n"),
1281
+ "magenta"
1282
+ );
1283
+ } else {
1284
+ printError(`MCP server "${name}" not found.`);
1285
+ }
1286
+ continue;
268
1287
  }
269
1288
 
270
- // Append session state constraints to context
271
- const state = getSessionState();
272
- systemContext += `\n[CLI STATE: Sandbox=${state.sandboxMode}, Approvals=${state.approvalMode}]`;
1289
+ // /reasoning <level>
1290
+ if (input.startsWith("/reasoning")) {
1291
+ const level = input.slice(10).trim().toLowerCase();
1292
+ const validLevels = ["low", "medium", "high"];
1293
+ if (level && validLevels.includes(level)) {
1294
+ updateSessionState({ reasoningEffort: level });
1295
+ printSuccess(`Reasoning effort set to: ${level}`);
1296
+ } else {
1297
+ const { selected } = await inquirer.prompt([
1298
+ {
1299
+ type: "list",
1300
+ name: "selected",
1301
+ message: "Select reasoning effort:",
1302
+ choices: [
1303
+ { name: "🟢 Low — Fast, concise answers", value: "low" },
1304
+ { name: "🟡 Medium — Balanced (default)", value: "medium" },
1305
+ { name: "🔴 High — Deep analysis, slower", value: "high" },
1306
+ ],
1307
+ default: getSessionState().reasoningEffort,
1308
+ },
1309
+ ]);
1310
+ updateSessionState({ reasoningEffort: selected });
1311
+ printSuccess(`Reasoning effort set to: ${selected}`);
1312
+ }
1313
+ continue;
1314
+ }
1315
+
1316
+ /* ─────────────────────────────────────
1317
+ $ SKILL EXECUTION
1318
+ ───────────────────────────────────── */
1319
+ if (input.startsWith("$")) {
1320
+ const withoutDollar = input.slice(1);
1321
+ const spaceIdx = withoutDollar.indexOf(" ");
1322
+ let skillName, skillPrompt;
1323
+ if (spaceIdx === -1) {
1324
+ skillName = withoutDollar.trim();
1325
+ skillPrompt = "";
1326
+ } else {
1327
+ skillName = withoutDollar.slice(0, spaceIdx).trim();
1328
+ skillPrompt = withoutDollar.slice(spaceIdx + 1).trim();
1329
+ }
1330
+
1331
+ if (!skillName) {
1332
+ printWarning("Usage: $<skillname> <prompt>");
1333
+ continue;
1334
+ }
1335
+
1336
+ const skillCtx = loadSkillContext(skillName);
1337
+ printStep(`Invoking skill: ${skillName}`);
1338
+
1339
+ if (!skillPrompt) {
1340
+ const { prompt } = await inquirer.prompt([
1341
+ {
1342
+ type: "input",
1343
+ name: "prompt",
1344
+ message: `[${skillName}] Enter your prompt:`,
1345
+ },
1346
+ ]);
1347
+ skillPrompt = prompt;
1348
+ }
1349
+
1350
+ if (skillPrompt.trim()) {
1351
+ await askAgent(skillPrompt, `Skill Context (${skillName}):\n${skillCtx}`);
1352
+ }
1353
+ continue;
1354
+ }
1355
+
1356
+ /* ─────────────────────────────────────
1357
+ AUTO FILE CONTEXT DETECTION
1358
+ ───────────────────────────────────── */
1359
+ let finalInput = input;
1360
+ let extraCtx = "";
1361
+
1362
+ // detect potential file paths in the input
1363
+ const { cleanPrompt, extractedContext } = extractFileContext(input);
1364
+ if (extractedContext.length > 0) {
1365
+ const fileContextStr = extractedContext
1366
+ .map((f) => {
1367
+ if (f.type === "text") return `--- ${f.path} ---\n${f.content}`;
1368
+ return `[${f.type}: ${f.path}]`;
1369
+ })
1370
+ .join("\n\n");
1371
+ extraCtx = fileContextStr;
1372
+ // Only use cleanPrompt if we actually found files and there's remaining text
1373
+ if (cleanPrompt.trim()) {
1374
+ finalInput = cleanPrompt.trim();
1375
+ }
1376
+ }
273
1377
 
274
- // Pass parsed input and file context to the agent
275
- await askAgent(cleanPrompt, systemContext);
1378
+ /* ─────────────────────────────────────
1379
+ DEFAULT: Send to Agent
1380
+ ───────────────────────────────────── */
1381
+ await askAgent(finalInput, extraCtx);
276
1382
  }
277
1383
  }