@heysalad/cheri-cli 1.0.0 → 1.1.0

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.
@@ -1,5 +1,5 @@
1
1
  // File diff tracking — tracks changes made by the agent for review and rollback
2
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
2
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync } from "fs";
3
3
  import { join, dirname } from "path";
4
4
  import { createTwoFilesPatch, structuredPatch } from "diff";
5
5
  import { homedir } from "os";
@@ -47,8 +47,8 @@ export function recordChange(snapshot, newContent, operation = "edit") {
47
47
  // Persist snapshot for rollback
48
48
  try {
49
49
  if (!existsSync(SNAPSHOTS_DIR)) mkdirSync(SNAPSHOTS_DIR, { recursive: true });
50
- const snapshotFile = join(SNAPSHOTS_DIR, `${Date.now()}-${sanitizeFilename(snapshot.path)}`);
51
- writeFileSync(snapshotFile, JSON.stringify(change), "utf-8");
50
+ const snapshotPath = join(SNAPSHOTS_DIR, `${Date.now()}-${sanitizeFilename(snapshot.path)}`);
51
+ writeFileSync(snapshotPath, JSON.stringify(change), "utf-8");
52
52
  } catch {}
53
53
 
54
54
  return change;
@@ -101,7 +101,6 @@ export function rollbackChange(index) {
101
101
  try {
102
102
  if (!change.existed) {
103
103
  // File was created — delete it
104
- const { unlinkSync } = require("fs");
105
104
  unlinkSync(change.path);
106
105
  } else {
107
106
  // Restore original content
package/src/lib/logger.js CHANGED
@@ -13,51 +13,70 @@ function getVersion() {
13
13
  return _version;
14
14
  }
15
15
 
16
+ // Gradient colors
17
+ const gradients = {
18
+ cheri: (text) => chalk.hex('#FF6B6B')(text),
19
+ purple: (text) => chalk.hex('#A855F7')(text),
20
+ blue: (text) => chalk.hex('#3B82F6')(text),
21
+ green: (text) => chalk.hex('#10B981')(text),
22
+ yellow: (text) => chalk.hex('#F59E0B')(text),
23
+ cyan: (text) => chalk.hex('#06B6D4')(text),
24
+ };
25
+
26
+ // Enhanced icons
27
+ const icons = {
28
+ info: 'ℹ',
29
+ success: '✓',
30
+ warning: '⚠',
31
+ error: '✗',
32
+ tip: '💡',
33
+ };
34
+
16
35
  export const log = {
17
36
  info(msg) {
18
- console.log(chalk.blue("info") + " " + msg);
37
+ console.log(gradients.blue(`${icons.info} info`) + " " + chalk.dim(msg));
19
38
  },
20
39
  success(msg) {
21
- console.log(chalk.green("done") + " " + msg);
40
+ console.log(gradients.green(`${icons.success} done`) + " " + msg);
22
41
  },
23
42
  warn(msg) {
24
- console.log(chalk.yellow("warn") + " " + msg);
43
+ console.log(gradients.yellow(`${icons.warning} warn`) + " " + msg);
25
44
  },
26
45
  error(msg) {
27
- console.log(chalk.red("error") + " " + msg);
46
+ console.log(gradients.cheri(`${icons.error} error`) + " " + chalk.red(msg));
28
47
  },
29
48
  dim(msg) {
30
49
  console.log(chalk.dim(msg));
31
50
  },
32
51
  brand(msg) {
33
- console.log(chalk.red.bold("cheri") + " " + msg);
52
+ console.log(gradients.cheri("🍒 cheri") + " " + chalk.bold(msg));
34
53
  },
35
54
  blank() {
36
55
  console.log();
37
56
  },
38
57
  header(title) {
39
58
  console.log();
40
- console.log(chalk.bold(title));
41
- console.log(chalk.dim("".repeat(title.length)));
59
+ console.log(gradients.cyan("╭─") + " " + chalk.bold(title));
60
+ console.log(gradients.cyan(""));
42
61
  },
43
62
  keyValue(key, value) {
44
- console.log(` ${chalk.dim(key.padEnd(16))} ${value}`);
63
+ console.log(` ${chalk.dim(key.padEnd(18))} ${gradients.cyan("→")} ${value}`);
45
64
  },
46
65
  tree(items) {
47
66
  items.forEach((item, i) => {
48
67
  const prefix = i === items.length - 1 ? " └─" : " ├─";
49
- console.log(chalk.dim(prefix) + " " + item);
68
+ console.log(gradients.cyan(prefix) + " " + item);
50
69
  });
51
70
  },
52
71
  banner(version) {
53
72
  if (!version) version = getVersion();
54
73
  console.log();
55
- console.log(` ${chalk.red("🍒")} ${chalk.red.bold("Cheri")}`);
74
+ console.log(` ${gradients.cheri("🍒")} ${chalk.red.bold("Cheri")}`);
56
75
  console.log(` ${chalk.dim("AI-powered cloud IDE by HeySalad")}`);
57
- console.log(` ${chalk.dim("v" + version)}`);
76
+ console.log(` ${gradients.cyan("✨")} ${chalk.dim("v" + version)}`);
58
77
  console.log();
59
78
  },
60
79
  tip(msg) {
61
- console.log(` ${chalk.blue("tip")} ${chalk.dim(msg)}`);
80
+ console.log(` ${gradients.yellow(`${icons.tip} tip`)} ${chalk.dim(msg)}`);
62
81
  },
63
82
  };
@@ -1,32 +1,50 @@
1
1
  // Rich markdown rendering for terminal output
2
- import { Marked } from "marked";
3
- import { markedTerminal } from "marked-terminal";
4
2
  import chalk from "chalk";
5
3
 
6
- const marked = new Marked(markedTerminal({
7
- reflowText: true,
8
- width: Math.min(process.stdout.columns || 100, 120),
9
- showSectionPrefix: false,
10
- tab: 2,
11
- code: chalk.cyan,
12
- codespan: chalk.cyan,
13
- blockquote: chalk.dim.italic,
14
- strong: chalk.bold,
15
- em: chalk.italic,
16
- del: chalk.strikethrough,
17
- link: chalk.blue.underline,
18
- href: chalk.blue.underline,
19
- listitem: (text) => ` ${chalk.dim("•")} ${text}`,
20
- }));
21
-
22
4
  /**
23
- * Render markdown text to terminal-formatted string
5
+ * Simple markdown renderer for terminal
6
+ * Handles basic formatting without external renderer
24
7
  */
25
8
  export function renderMarkdown(text) {
26
9
  if (!text) return "";
10
+
27
11
  try {
28
- return marked.parse(text).trim();
29
- } catch {
12
+ let output = text;
13
+
14
+ // Code blocks ```code```
15
+ output = output.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => {
16
+ const header = lang ? chalk.dim(` ${lang}`) + "\n" : "";
17
+ const lines = code.split("\n").map(l => chalk.cyan(" " + l)).join("\n");
18
+ return "\n" + header + lines + "\n";
19
+ });
20
+
21
+ // Inline code `code`
22
+ output = output.replace(/`([^`]+)`/g, (match, code) => chalk.cyan(code));
23
+
24
+ // Bold **text**
25
+ output = output.replace(/\*\*([^*]+)\*\*/g, (match, text) => chalk.bold(text));
26
+
27
+ // Italic *text*
28
+ output = output.replace(/\*([^*]+)\*/g, (match, text) => chalk.italic(text));
29
+
30
+ // Links [text](url)
31
+ output = output.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, url) =>
32
+ chalk.blue.underline(text) + chalk.dim(` (${url})`)
33
+ );
34
+
35
+ // Headers ### text
36
+ output = output.replace(/^(#{1,6})\s+(.+)$/gm, (match, hashes, text) => {
37
+ return chalk.bold.cyan(text);
38
+ });
39
+
40
+ // List items - text or * text
41
+ output = output.replace(/^[\-\*]\s+(.+)$/gm, (match, text) =>
42
+ ` ${chalk.dim("•")} ${text}`
43
+ );
44
+
45
+ return output.trim();
46
+ } catch (err) {
47
+ // Fallback to plain text if rendering fails
30
48
  return text;
31
49
  }
32
50
  }
@@ -118,7 +118,13 @@ export class McpServer {
118
118
  params,
119
119
  });
120
120
 
121
- this.process.stdin.write(message + "\n");
121
+ try {
122
+ this.process.stdin.write(message + "\n");
123
+ } catch (err) {
124
+ this.pendingRequests.delete(id);
125
+ clearTimeout(timeout);
126
+ reject(new Error(`Failed to write to MCP server '${this.name}': ${err.message}`));
127
+ }
122
128
  });
123
129
  }
124
130
 
@@ -128,7 +134,9 @@ export class McpServer {
128
134
  method,
129
135
  params,
130
136
  });
131
- this.process.stdin.write(message + "\n");
137
+ try {
138
+ this.process.stdin.write(message + "\n");
139
+ } catch {}
132
140
  }
133
141
 
134
142
  _processBuffer() {
@@ -148,7 +156,9 @@ export class McpServer {
148
156
  resolve(msg.result);
149
157
  }
150
158
  }
151
- } catch {}
159
+ } catch (err) {
160
+ log.dim(`[mcp:${this.name}] Failed to parse response: ${line.slice(0, 100)}`);
161
+ }
152
162
  }
153
163
  }
154
164
 
@@ -206,13 +216,22 @@ export class McpManager {
206
216
  }
207
217
 
208
218
  async callTool(fullName, args) {
209
- // Parse mcp_{server}_{tool} format
210
- const match = fullName.match(/^mcp_([^_]+)_(.+)$/);
211
- if (!match) return { error: `Invalid MCP tool name: ${fullName}` };
212
-
213
- const [, serverName, toolName] = match;
214
- const server = this.servers.get(serverName);
215
- if (!server) return { error: `MCP server '${serverName}' not connected` };
219
+ // Find the server by checking registered tool names (avoids underscore ambiguity)
220
+ let server = null;
221
+ let toolName = null;
222
+ for (const s of this.servers.values()) {
223
+ const def = s.getToolDefinitions().find(t => t.name === fullName);
224
+ if (def) { server = s; toolName = def.mcpToolName; break; }
225
+ }
226
+ if (!server || !toolName) {
227
+ // Fallback: try regex parse
228
+ const match = fullName.match(/^mcp_([^_]+)_(.+)$/);
229
+ if (!match) return { error: `Invalid MCP tool name: ${fullName}` };
230
+ const [, serverName, tn] = match;
231
+ server = this.servers.get(serverName);
232
+ toolName = tn;
233
+ if (!server) return { error: `MCP server '${serverName}' not connected` };
234
+ }
216
235
 
217
236
  try {
218
237
  const result = await server.callTool(toolName, args);
@@ -210,8 +210,13 @@ class AnthropicProvider {
210
210
  }
211
211
  }
212
212
  await writer.write(encoder.encode("data: [DONE]\n\n"));
213
- } catch {} finally {
214
- await writer.close();
213
+ } catch (err) {
214
+ try {
215
+ const errChunk = { choices: [{ index: 0, delta: { content: `\n[Stream error: ${err.message || "unknown"}]` }, finish_reason: "stop" }] };
216
+ await writer.write(encoder.encode(`data: ${JSON.stringify(errChunk)}\n\n`));
217
+ } catch {}
218
+ } finally {
219
+ try { await writer.close(); } catch {}
215
220
  }
216
221
  })();
217
222
 
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync } from "fs";
1
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, unlinkSync } from "fs";
2
2
  import { join } from "path";
3
3
  import { homedir } from "os";
4
4
 
@@ -48,7 +48,6 @@ export function listSessions() {
48
48
  export function deleteSession(sessionId) {
49
49
  const filePath = join(SESSIONS_DIR, `${sessionId}.json`);
50
50
  if (existsSync(filePath)) {
51
- const { unlinkSync } = require("fs");
52
51
  unlinkSync(filePath);
53
52
  return true;
54
53
  }
@@ -1,6 +1,8 @@
1
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, statSync } from "fs";
2
2
  import { dirname, resolve } from "path";
3
3
 
4
+ const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB limit
5
+
4
6
  export const readFile = {
5
7
  name: "read_file",
6
8
  description: "Read the contents of a file at the given path. Returns the file contents as a string.",
@@ -12,12 +14,32 @@ export const readFile = {
12
14
  required: ["path"],
13
15
  },
14
16
  handler: async ({ path }) => {
15
- const resolved = resolve(path);
16
- if (!existsSync(resolved)) {
17
- return { error: `File not found: ${resolved}` };
17
+ try {
18
+ const resolved = resolve(path);
19
+ if (!existsSync(resolved)) {
20
+ return { error: `File not found: ${resolved}` };
21
+ }
22
+ const stat = statSync(resolved);
23
+ if (stat.isDirectory()) {
24
+ return { error: `Path is a directory, not a file: ${resolved}` };
25
+ }
26
+ if (stat.size > MAX_FILE_SIZE) {
27
+ return { error: `File too large (${(stat.size / 1024 / 1024).toFixed(1)}MB). Max: 5MB` };
28
+ }
29
+ // Detect binary files by checking for null bytes in first 8KB
30
+ const buf = Buffer.alloc(Math.min(8192, stat.size));
31
+ const fd = await import("fs").then(fs => fs.openSync(resolved, "r"));
32
+ const { readSync, closeSync } = await import("fs");
33
+ readSync(fd, buf, 0, buf.length, 0);
34
+ closeSync(fd);
35
+ if (buf.includes(0)) {
36
+ return { error: `Binary file detected: ${resolved}. Cannot read binary files as text.` };
37
+ }
38
+ const content = readFileSync(resolved, "utf-8");
39
+ return { path: resolved, content, lines: content.split("\n").length };
40
+ } catch (err) {
41
+ return { error: `Failed to read file: ${err.message}` };
18
42
  }
19
- const content = readFileSync(resolved, "utf-8");
20
- return { path: resolved, content, lines: content.split("\n").length };
21
43
  },
22
44
  };
23
45
 
@@ -33,13 +55,17 @@ export const writeFile = {
33
55
  required: ["path", "content"],
34
56
  },
35
57
  handler: async ({ path, content }) => {
36
- const resolved = resolve(path);
37
- const dir = dirname(resolved);
38
- if (!existsSync(dir)) {
39
- mkdirSync(dir, { recursive: true });
58
+ try {
59
+ const resolved = resolve(path);
60
+ const dir = dirname(resolved);
61
+ if (!existsSync(dir)) {
62
+ mkdirSync(dir, { recursive: true });
63
+ }
64
+ writeFileSync(resolved, content, "utf-8");
65
+ return { path: resolved, bytesWritten: Buffer.byteLength(content, "utf-8") };
66
+ } catch (err) {
67
+ return { error: `Failed to write file: ${err.message}` };
40
68
  }
41
- writeFileSync(resolved, content, "utf-8");
42
- return { path: resolved, bytesWritten: Buffer.byteLength(content, "utf-8") };
43
69
  },
44
70
  };
45
71
 
@@ -56,17 +82,24 @@ export const editFile = {
56
82
  required: ["path", "old_string", "new_string"],
57
83
  },
58
84
  handler: async ({ path, old_string, new_string }) => {
59
- const resolved = resolve(path);
60
- if (!existsSync(resolved)) {
61
- return { error: `File not found: ${resolved}` };
62
- }
63
- const content = readFileSync(resolved, "utf-8");
64
- if (!content.includes(old_string)) {
65
- return { error: "old_string not found in file. Make sure it matches exactly, including whitespace." };
85
+ try {
86
+ const resolved = resolve(path);
87
+ if (!existsSync(resolved)) {
88
+ return { error: `File not found: ${resolved}` };
89
+ }
90
+ const content = readFileSync(resolved, "utf-8");
91
+ if (!content.includes(old_string)) {
92
+ return { error: "old_string not found in file. Make sure it matches exactly, including whitespace." };
93
+ }
94
+ const count = content.split(old_string).length - 1;
95
+ if (count > 1) {
96
+ return { error: `old_string found ${count} times. It must be unique — add more surrounding context.` };
97
+ }
98
+ const newContent = content.replace(old_string, new_string);
99
+ writeFileSync(resolved, newContent, "utf-8");
100
+ return { path: resolved, replacements: 1 };
101
+ } catch (err) {
102
+ return { error: `Failed to edit file: ${err.message}` };
66
103
  }
67
- const count = content.split(old_string).length - 1;
68
- const newContent = content.replace(old_string, new_string);
69
- writeFileSync(resolved, newContent, "utf-8");
70
- return { path: resolved, replacements: 1, totalOccurrences: count };
71
104
  },
72
105
  };
@@ -43,7 +43,7 @@ export const searchContent = {
43
43
  handler: async ({ pattern, path, include }) => {
44
44
  const dir = resolve(path || ".");
45
45
  try {
46
- let cmd = `grep -rn --include='${include || "*"}' ${JSON.stringify(pattern)} ${JSON.stringify(dir)} 2>/dev/null | head -50`;
46
+ let cmd = `grep -rn --include=${JSON.stringify(include || "*")} ${JSON.stringify(pattern)} ${JSON.stringify(dir)} 2>/dev/null | head -50`;
47
47
  const result = execSync(cmd, { encoding: "utf-8", timeout: 10_000 });
48
48
  const lines = result.trim().split("\n").filter(Boolean);
49
49
  const matches = lines.map((line) => {