@heysalad/cheri-cli 0.2.0 → 0.3.1

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/lib/repl.js DELETED
@@ -1,225 +0,0 @@
1
- import readline from "readline";
2
- import chalk from "chalk";
3
- import ora from "ora";
4
- import { createProvider } from "./providers/index.js";
5
- import { getToolDefinitions, executeTool, requiresConfirmation } from "./tools/index.js";
6
- import { showStartupScreen } from "./branding.js";
7
- import { renderMarkdown, renderToolUse, renderToolResult } from "./renderer.js";
8
-
9
- export async function startRepl(options = {}) {
10
- showStartupScreen(options);
11
-
12
- let provider;
13
- try {
14
- provider = await createProvider(options);
15
- } catch (err) {
16
- console.error(chalk.red(`\n${err.message}`));
17
- process.exit(1);
18
- }
19
-
20
- const messages = [];
21
- const tools = getToolDefinitions();
22
-
23
- const rl = readline.createInterface({
24
- input: process.stdin,
25
- output: process.stdout,
26
- prompt: "🍒 > ",
27
- });
28
-
29
- rl.prompt();
30
-
31
- rl.on("line", async (line) => {
32
- const input = line.trim();
33
- if (!input) {
34
- rl.prompt();
35
- return;
36
- }
37
-
38
- // Slash commands
39
- if (input.startsWith("/")) {
40
- await handleSlashCommand(input, messages, provider, options, rl);
41
- rl.prompt();
42
- return;
43
- }
44
-
45
- // Add user message
46
- messages.push({ role: "user", content: input });
47
-
48
- // Run agent loop
49
- await agentLoop(provider, messages, tools, rl);
50
- rl.prompt();
51
- });
52
-
53
- rl.on("close", () => {
54
- console.log(chalk.dim("\nGoodbye! 🍒"));
55
- process.exit(0);
56
- });
57
- }
58
-
59
- async function agentLoop(provider, messages, tools, rl) {
60
- let continueLoop = true;
61
-
62
- while (continueLoop) {
63
- continueLoop = false;
64
- const spinner = ora({ text: "Thinking...", color: "yellow" }).start();
65
-
66
- let fullText = "";
67
- const toolCalls = [];
68
- let streamingStarted = false;
69
-
70
- try {
71
- for await (const event of provider.chat(messages, tools)) {
72
- switch (event.type) {
73
- case "text":
74
- if (!streamingStarted) {
75
- spinner.stop();
76
- streamingStarted = true;
77
- }
78
- process.stdout.write(event.content);
79
- fullText += event.content;
80
- break;
81
-
82
- case "tool_use_start":
83
- if (!streamingStarted) {
84
- spinner.stop();
85
- streamingStarted = true;
86
- }
87
- toolCalls.push({ id: event.id, name: event.name, inputJson: "", input: {} });
88
- break;
89
-
90
- case "tool_input_delta":
91
- if (toolCalls.length > 0) {
92
- toolCalls[toolCalls.length - 1].inputJson += event.content;
93
- }
94
- break;
95
-
96
- case "tool_use_end":
97
- if (toolCalls.length > 0) {
98
- const tc = toolCalls[toolCalls.length - 1];
99
- tc.input = event.input;
100
- }
101
- break;
102
-
103
- case "done":
104
- spinner.stop();
105
- if (event.stopReason === "tool_use" || toolCalls.length > 0) {
106
- continueLoop = true;
107
- }
108
- break;
109
- }
110
- }
111
- } catch (err) {
112
- spinner.stop();
113
- console.error(chalk.red(`\n❌ API Error: ${err.message}`));
114
- // Remove the last user message on error so conversation stays valid
115
- if (messages.length > 0 && messages[messages.length - 1].role === "user") {
116
- messages.pop();
117
- }
118
- return;
119
- }
120
-
121
- // End text output with newline if we streamed any text
122
- if (fullText) {
123
- process.stdout.write("\n");
124
- }
125
-
126
- // Build assistant message content
127
- const assistantContent = [];
128
- if (fullText) {
129
- assistantContent.push({ type: "text", text: fullText });
130
- }
131
- for (const tc of toolCalls) {
132
- assistantContent.push({ type: "tool_use", id: tc.id, name: tc.name, input: tc.input });
133
- }
134
- if (assistantContent.length > 0) {
135
- messages.push({ role: "assistant", content: assistantContent });
136
- }
137
-
138
- // Execute tool calls if any
139
- if (toolCalls.length > 0) {
140
- const toolResults = [];
141
-
142
- for (const tc of toolCalls) {
143
- console.log(renderToolUse(tc.name, tc.input));
144
-
145
- // Confirmation for dangerous commands
146
- if (requiresConfirmation(tc.name)) {
147
- const confirmed = await askConfirmation(rl, `Run command: ${chalk.bold(tc.input.command || "")}`);
148
- if (!confirmed) {
149
- toolResults.push({
150
- type: "tool_result",
151
- tool_use_id: tc.id,
152
- content: "User denied execution of this command.",
153
- });
154
- console.log(chalk.yellow("⏭️ Skipped"));
155
- continue;
156
- }
157
- const cmdSpinner = ora({ text: `Running: ${tc.input.command}`, color: "red" }).start();
158
- const result = await executeTool(tc.name, tc.input);
159
- cmdSpinner.stop();
160
- console.log(renderToolResult(tc.name, result));
161
- toolResults.push({
162
- type: "tool_result",
163
- tool_use_id: tc.id,
164
- content: JSON.stringify(result),
165
- });
166
- } else {
167
- const toolSpinner = ora({ text: `${tc.name}...`, color: "magenta" }).start();
168
- const result = await executeTool(tc.name, tc.input);
169
- toolSpinner.stop();
170
- console.log(renderToolResult(tc.name, result));
171
- toolResults.push({
172
- type: "tool_result",
173
- tool_use_id: tc.id,
174
- content: JSON.stringify(result),
175
- });
176
- }
177
- }
178
-
179
- messages.push({ role: "user", content: toolResults });
180
- }
181
- }
182
- }
183
-
184
- function askConfirmation(rl, message) {
185
- return new Promise((resolve) => {
186
- rl.question(`${chalk.yellow("⚡")} ${message} ${chalk.dim("[Y/n]")} `, (answer) => {
187
- const a = answer.trim().toLowerCase();
188
- resolve(a === "" || a === "y" || a === "yes");
189
- });
190
- });
191
- }
192
-
193
- async function handleSlashCommand(input, messages, provider, options, rl) {
194
- const [cmd, ...args] = input.split(/\s+/);
195
-
196
- switch (cmd) {
197
- case "/help":
198
- console.log(`
199
- ${chalk.bold("Cheri REPL Commands:")}
200
- ${chalk.cyan("/help")} Show this help
201
- ${chalk.cyan("/clear")} Clear conversation history
202
- ${chalk.cyan("/model")} Show current model info
203
- ${chalk.cyan("/exit")} Exit the REPL
204
- `);
205
- break;
206
-
207
- case "/clear":
208
- messages.length = 0;
209
- console.log(chalk.dim("Conversation cleared."));
210
- break;
211
-
212
- case "/model":
213
- console.log(` Provider: ${chalk.cyan(options.provider || "anthropic")}`);
214
- console.log(` Model: ${chalk.cyan(provider.getModel())}`);
215
- break;
216
-
217
- case "/exit":
218
- console.log(chalk.dim("Goodbye! 🍒"));
219
- process.exit(0);
220
- break;
221
-
222
- default:
223
- console.log(chalk.yellow(`Unknown command: ${cmd}. Type /help for available commands.`));
224
- }
225
- }
@@ -1,34 +0,0 @@
1
- import { execSync } from "child_process";
2
-
3
- export const runCommand = {
4
- name: "run_command",
5
- description: "Execute a shell command and return its output. Requires user confirmation before execution.",
6
- parameters: {
7
- type: "object",
8
- properties: {
9
- command: { type: "string", description: "The shell command to execute" },
10
- cwd: { type: "string", description: "Working directory (optional, defaults to current directory)" },
11
- },
12
- required: ["command"],
13
- },
14
- requiresConfirmation: true,
15
- handler: async ({ command, cwd }) => {
16
- try {
17
- const output = execSync(command, {
18
- cwd: cwd || process.cwd(),
19
- encoding: "utf-8",
20
- timeout: 120_000,
21
- maxBuffer: 1024 * 1024 * 10,
22
- stdio: ["pipe", "pipe", "pipe"],
23
- });
24
- return { command, exitCode: 0, stdout: output, stderr: "" };
25
- } catch (err) {
26
- return {
27
- command,
28
- exitCode: err.status ?? 1,
29
- stdout: err.stdout || "",
30
- stderr: err.stderr || err.message,
31
- };
32
- }
33
- },
34
- };
@@ -1,73 +0,0 @@
1
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
2
- import { dirname } from "path";
3
- import { resolve } from "path";
4
-
5
- export const readFile = {
6
- name: "read_file",
7
- description: "Read the contents of a file at the given path. Returns the file contents as a string.",
8
- parameters: {
9
- type: "object",
10
- properties: {
11
- path: { type: "string", description: "Absolute or relative file path to read" },
12
- },
13
- required: ["path"],
14
- },
15
- handler: async ({ path }) => {
16
- const resolved = resolve(path);
17
- if (!existsSync(resolved)) {
18
- return { error: `File not found: ${resolved}` };
19
- }
20
- const content = readFileSync(resolved, "utf-8");
21
- return { path: resolved, content, lines: content.split("\n").length };
22
- },
23
- };
24
-
25
- export const writeFile = {
26
- name: "write_file",
27
- description: "Create or overwrite a file with the given content. Creates parent directories if needed.",
28
- parameters: {
29
- type: "object",
30
- properties: {
31
- path: { type: "string", description: "Absolute or relative file path to write" },
32
- content: { type: "string", description: "The content to write to the file" },
33
- },
34
- required: ["path", "content"],
35
- },
36
- handler: async ({ path, content }) => {
37
- const resolved = resolve(path);
38
- const dir = dirname(resolved);
39
- if (!existsSync(dir)) {
40
- mkdirSync(dir, { recursive: true });
41
- }
42
- writeFileSync(resolved, content, "utf-8");
43
- return { path: resolved, bytesWritten: Buffer.byteLength(content, "utf-8") };
44
- },
45
- };
46
-
47
- export const editFile = {
48
- name: "edit_file",
49
- description: "Edit a file by replacing an exact string match with new content. The old_string must match exactly (including whitespace and indentation).",
50
- parameters: {
51
- type: "object",
52
- properties: {
53
- path: { type: "string", description: "Absolute or relative file path to edit" },
54
- old_string: { type: "string", description: "The exact string to find and replace" },
55
- new_string: { type: "string", description: "The replacement string" },
56
- },
57
- required: ["path", "old_string", "new_string"],
58
- },
59
- handler: async ({ path, old_string, new_string }) => {
60
- const resolved = resolve(path);
61
- if (!existsSync(resolved)) {
62
- return { error: `File not found: ${resolved}` };
63
- }
64
- const content = readFileSync(resolved, "utf-8");
65
- if (!content.includes(old_string)) {
66
- return { error: "old_string not found in file. Make sure it matches exactly, including whitespace." };
67
- }
68
- const count = content.split(old_string).length - 1;
69
- const newContent = content.replace(old_string, new_string);
70
- writeFileSync(resolved, newContent, "utf-8");
71
- return { path: resolved, replacements: 1, totalOccurrences: count };
72
- },
73
- };
@@ -1,32 +0,0 @@
1
- import { readFile, writeFile, editFile } from "./file-tools.js";
2
- import { runCommand } from "./command-tools.js";
3
- import { searchFiles, searchContent, listDirectory } from "./search-tools.js";
4
-
5
- const tools = [readFile, writeFile, editFile, runCommand, searchFiles, searchContent, listDirectory];
6
-
7
- const toolMap = new Map(tools.map((t) => [t.name, t]));
8
-
9
- export function getToolDefinitions() {
10
- return tools.map(({ name, description, parameters }) => ({ name, description, parameters }));
11
- }
12
-
13
- export function getTool(name) {
14
- return toolMap.get(name);
15
- }
16
-
17
- export async function executeTool(name, input) {
18
- const tool = toolMap.get(name);
19
- if (!tool) {
20
- return { error: `Unknown tool: ${name}` };
21
- }
22
- try {
23
- return await tool.handler(input);
24
- } catch (err) {
25
- return { error: err.message };
26
- }
27
- }
28
-
29
- export function requiresConfirmation(name) {
30
- const tool = toolMap.get(name);
31
- return tool?.requiresConfirmation === true;
32
- }
@@ -1,95 +0,0 @@
1
- import { readdirSync, statSync, existsSync } from "fs";
2
- import { resolve, join, relative } from "path";
3
- import { execSync } from "child_process";
4
-
5
- export const searchFiles = {
6
- name: "search_files",
7
- description: "Search for files matching a glob/name pattern. Returns matching file paths.",
8
- parameters: {
9
- type: "object",
10
- properties: {
11
- pattern: { type: "string", description: "File name pattern to search for (e.g., '*.js', 'config*', 'test')" },
12
- path: { type: "string", description: "Directory to search in (defaults to current directory)" },
13
- },
14
- required: ["pattern"],
15
- },
16
- handler: async ({ pattern, path }) => {
17
- const dir = resolve(path || ".");
18
- try {
19
- const result = execSync(`find ${JSON.stringify(dir)} -name ${JSON.stringify(pattern)} -not -path '*/node_modules/*' -not -path '*/.git/*' 2>/dev/null | head -50`, {
20
- encoding: "utf-8",
21
- timeout: 10_000,
22
- });
23
- const files = result.trim().split("\n").filter(Boolean);
24
- return { pattern, searchPath: dir, matches: files, count: files.length };
25
- } catch {
26
- return { pattern, searchPath: dir, matches: [], count: 0 };
27
- }
28
- },
29
- };
30
-
31
- export const searchContent = {
32
- name: "search_content",
33
- description: "Search for a text pattern inside files (like grep). Returns matching lines with file paths and line numbers.",
34
- parameters: {
35
- type: "object",
36
- properties: {
37
- pattern: { type: "string", description: "Text or regex pattern to search for" },
38
- path: { type: "string", description: "Directory to search in (defaults to current directory)" },
39
- include: { type: "string", description: "File glob to filter (e.g., '*.js')" },
40
- },
41
- required: ["pattern"],
42
- },
43
- handler: async ({ pattern, path, include }) => {
44
- const dir = resolve(path || ".");
45
- try {
46
- let cmd = `grep -rn --include='${include || "*"}' ${JSON.stringify(pattern)} ${JSON.stringify(dir)} 2>/dev/null | head -50`;
47
- const result = execSync(cmd, { encoding: "utf-8", timeout: 10_000 });
48
- const lines = result.trim().split("\n").filter(Boolean);
49
- const matches = lines.map((line) => {
50
- const match = line.match(/^(.+?):(\d+):(.*)$/);
51
- if (match) return { file: match[1], line: parseInt(match[2]), content: match[3].trim() };
52
- return { raw: line };
53
- });
54
- return { pattern, searchPath: dir, matches, count: matches.length };
55
- } catch {
56
- return { pattern, searchPath: dir, matches: [], count: 0 };
57
- }
58
- },
59
- };
60
-
61
- export const listDirectory = {
62
- name: "list_directory",
63
- description: "List files and directories at the given path. Shows names, types, and sizes.",
64
- parameters: {
65
- type: "object",
66
- properties: {
67
- path: { type: "string", description: "Directory path to list (defaults to current directory)" },
68
- },
69
- required: [],
70
- },
71
- handler: async ({ path }) => {
72
- const dir = resolve(path || ".");
73
- if (!existsSync(dir)) {
74
- return { error: `Directory not found: ${dir}` };
75
- }
76
- try {
77
- const entries = readdirSync(dir).map((name) => {
78
- try {
79
- const fullPath = join(dir, name);
80
- const stat = statSync(fullPath);
81
- return {
82
- name,
83
- type: stat.isDirectory() ? "directory" : "file",
84
- size: stat.isDirectory() ? undefined : stat.size,
85
- };
86
- } catch {
87
- return { name, type: "unknown" };
88
- }
89
- });
90
- return { path: dir, entries, count: entries.length };
91
- } catch (err) {
92
- return { error: err.message };
93
- }
94
- },
95
- };