@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/repl.js ADDED
@@ -0,0 +1,217 @@
1
+ import readline from "readline";
2
+ import chalk from "chalk";
3
+ import { log } from "./lib/logger.js";
4
+
5
+ // Import standalone action handlers
6
+ import { launchWorkspace, stopWorkspace, listWorkspaces, getWorkspaceStatus } from "./commands/workspace.js";
7
+ import { showMemory, addMemory, clearMemory, exportMemory } from "./commands/memory.js";
8
+ import { showStatus } from "./commands/status.js";
9
+ import { listConfig, getConfigKey, setConfigKey } from "./commands/config.js";
10
+ import { loginFlow } from "./commands/login.js";
11
+ import { initProject } from "./commands/init.js";
12
+ import { showUsage } from "./commands/usage.js";
13
+
14
+ const COMMANDS = {
15
+ "workspace launch": { args: "<repo>", desc: "Launch a new cloud workspace" },
16
+ "workspace list": { args: "", desc: "List all workspaces" },
17
+ "workspace stop": { args: "<id>", desc: "Stop a running workspace" },
18
+ "workspace status": { args: "<id>", desc: "Get workspace status" },
19
+ "memory show": { args: "", desc: "Show current memory entries" },
20
+ "memory add": { args: "<text>", desc: "Add a memory entry" },
21
+ "memory clear": { args: "", desc: "Clear all memory" },
22
+ "memory export": { args: "", desc: "Export memory to JSON" },
23
+ "status": { args: "", desc: "Show account & workspace status" },
24
+ "usage": { args: "", desc: "Show API usage & rate limits" },
25
+ "config list": { args: "", desc: "Show all configuration" },
26
+ "config get": { args: "<key>", desc: "Get a config value" },
27
+ "config set": { args: "<k> <v>",desc: "Set a config value" },
28
+ "login": { args: "", desc: "Authenticate with Cheri" },
29
+ "init": { args: "", desc: "Initialize a project" },
30
+ "help": { args: "", desc: "Show this help" },
31
+ "clear": { args: "", desc: "Clear the terminal" },
32
+ "exit": { args: "", desc: "Exit Cheri" },
33
+ };
34
+
35
+ function showHelp() {
36
+ log.blank();
37
+ log.brand("Available Commands");
38
+ log.blank();
39
+
40
+ const maxCmd = 20;
41
+ const maxArgs = 10;
42
+
43
+ for (const [cmd, { args, desc }] of Object.entries(COMMANDS)) {
44
+ const cmdStr = chalk.cyan(cmd.padEnd(maxCmd));
45
+ const argsStr = chalk.dim(args.padEnd(maxArgs));
46
+ console.log(` ${cmdStr} ${argsStr} ${desc}`);
47
+ }
48
+
49
+ log.blank();
50
+ log.tip("Type a command above, or 'exit' to quit. Use arrow keys for history.");
51
+ log.blank();
52
+ }
53
+
54
+ async function dispatch(input) {
55
+ const parts = input.split(/\s+/);
56
+ const cmd = parts[0];
57
+ const sub = parts[1];
58
+ const rest = parts.slice(2).join(" ");
59
+
60
+ switch (cmd) {
61
+ case "help":
62
+ showHelp();
63
+ return;
64
+
65
+ case "clear":
66
+ console.clear();
67
+ return;
68
+
69
+ case "exit":
70
+ case "quit":
71
+ console.log(chalk.dim("\nGoodbye! 🍒\n"));
72
+ process.exit(0);
73
+
74
+ case "status":
75
+ await showStatus();
76
+ return;
77
+
78
+ case "usage":
79
+ await showUsage();
80
+ return;
81
+
82
+ case "login":
83
+ await loginFlow();
84
+ return;
85
+
86
+ case "init":
87
+ await initProject();
88
+ return;
89
+
90
+ case "workspace":
91
+ switch (sub) {
92
+ case "launch":
93
+ if (!rest) {
94
+ log.error("Usage: workspace launch <owner/repo>");
95
+ return;
96
+ }
97
+ await launchWorkspace(rest);
98
+ return;
99
+ case "list":
100
+ await listWorkspaces();
101
+ return;
102
+ case "stop":
103
+ if (!rest) {
104
+ log.error("Usage: workspace stop <id>");
105
+ return;
106
+ }
107
+ await stopWorkspace(rest);
108
+ return;
109
+ case "status":
110
+ if (!rest) {
111
+ log.error("Usage: workspace status <id>");
112
+ return;
113
+ }
114
+ await getWorkspaceStatus(rest);
115
+ return;
116
+ default:
117
+ log.error("Usage: workspace [launch|list|stop|status]");
118
+ return;
119
+ }
120
+
121
+ case "memory":
122
+ switch (sub) {
123
+ case "show":
124
+ await showMemory();
125
+ return;
126
+ case "add":
127
+ if (!rest) {
128
+ log.error("Usage: memory add <content>");
129
+ return;
130
+ }
131
+ await addMemory(rest);
132
+ return;
133
+ case "clear":
134
+ await clearMemory();
135
+ return;
136
+ case "export":
137
+ await exportMemory();
138
+ return;
139
+ default:
140
+ log.error("Usage: memory [show|add|clear|export]");
141
+ return;
142
+ }
143
+
144
+ case "config":
145
+ switch (sub) {
146
+ case "list":
147
+ listConfig();
148
+ return;
149
+ case "get":
150
+ if (!rest) {
151
+ log.error("Usage: config get <key>");
152
+ return;
153
+ }
154
+ getConfigKey(rest);
155
+ return;
156
+ case "set": {
157
+ const setParts = rest.split(/\s+/);
158
+ if (setParts.length < 2) {
159
+ log.error("Usage: config set <key> <value>");
160
+ return;
161
+ }
162
+ setConfigKey(setParts[0], setParts.slice(1).join(" "));
163
+ return;
164
+ }
165
+ default:
166
+ log.error("Usage: config [list|get|set]");
167
+ return;
168
+ }
169
+
170
+ default:
171
+ log.warn(`Unknown command: '${cmd}'. Type ${chalk.cyan("help")} for available commands.`);
172
+ }
173
+ }
174
+
175
+ export async function startCommandRepl() {
176
+ log.banner();
177
+ console.log(chalk.dim(" " + "─".repeat(40)));
178
+ log.tip(`Type ${chalk.cyan("help")} for commands, ${chalk.cyan("exit")} to quit.`);
179
+ log.blank();
180
+
181
+ const rl = readline.createInterface({
182
+ input: process.stdin,
183
+ output: process.stdout,
184
+ prompt: chalk.red("🍒 cheri") + chalk.dim(" > "),
185
+ terminal: true,
186
+ });
187
+
188
+ rl.prompt();
189
+
190
+ rl.on("line", async (line) => {
191
+ const input = line.trim();
192
+
193
+ if (!input) {
194
+ rl.prompt();
195
+ return;
196
+ }
197
+
198
+ try {
199
+ await dispatch(input);
200
+ } catch (err) {
201
+ log.error(err.message);
202
+ }
203
+
204
+ rl.prompt();
205
+ });
206
+
207
+ rl.on("close", () => {
208
+ console.log(chalk.dim("\nGoodbye! 🍒\n"));
209
+ process.exit(0);
210
+ });
211
+
212
+ // Handle Ctrl+C gracefully
213
+ rl.on("SIGINT", () => {
214
+ console.log(chalk.dim("\nGoodbye! 🍒\n"));
215
+ process.exit(0);
216
+ });
217
+ }
@@ -1,15 +0,0 @@
1
- import { startRepl } from "../lib/repl.js";
2
-
3
- export function registerChatCommand(program) {
4
- program
5
- .command("chat")
6
- .description("Start an interactive AI coding session")
7
- .option("-p, --provider <provider>", "AI provider (anthropic, openai, deepseek, gemini)")
8
- .option("-m, --model <model>", "Model to use (overrides provider default)")
9
- .action(async (options) => {
10
- await startRepl({
11
- provider: options.provider,
12
- model: options.model,
13
- });
14
- });
15
- }
@@ -1,36 +0,0 @@
1
- import chalk from "chalk";
2
- import { getConfigValue } from "./config-store.js";
3
-
4
- const CHERRY_ART = `
5
- ${chalk.red("🍒🍒")}
6
- ${chalk.red("🍒 🍒")}
7
- `;
8
-
9
- export function showStartupScreen(options = {}) {
10
- const provider = options.provider || getConfigValue("ai.provider") || "anthropic";
11
- const model = options.model || getConfigValue("ai.model") || getDefaultModel(provider);
12
- const cwd = process.cwd();
13
- const version = "0.2.0";
14
-
15
- console.log(CHERRY_ART);
16
- console.log(chalk.bold(` cheri v${version}`));
17
- console.log(chalk.dim(" AI coding agent by HeySalad"));
18
- console.log();
19
- console.log(` ${chalk.dim("Provider:")} ${chalk.cyan(provider)}`);
20
- console.log(` ${chalk.dim("Model:")} ${chalk.cyan(model)}`);
21
- console.log(` ${chalk.dim("Directory:")} ${chalk.cyan(cwd)}`);
22
- console.log();
23
- console.log(chalk.dim(" Type your request. /help for commands, Ctrl+C to exit."));
24
- console.log(chalk.dim(" " + "─".repeat(48)));
25
- console.log();
26
- }
27
-
28
- export function getDefaultModel(provider) {
29
- const defaults = {
30
- anthropic: "claude-sonnet-4-20250514",
31
- openai: "gpt-4o",
32
- deepseek: "deepseek-chat",
33
- gemini: "gemini-2.0-flash",
34
- };
35
- return defaults[provider] || "unknown";
36
- }
@@ -1,66 +0,0 @@
1
- import { BaseProvider, SYSTEM_PROMPT } from "./base.js";
2
-
3
- export class AnthropicProvider extends BaseProvider {
4
- constructor(apiKey, model = "claude-sonnet-4-20250514") {
5
- super(apiKey, model);
6
- }
7
-
8
- async *chat(messages, tools) {
9
- const { default: Anthropic } = await import("@anthropic-ai/sdk");
10
- const client = new Anthropic({ apiKey: this.apiKey });
11
-
12
- const anthropicTools = tools.map((t) => ({
13
- name: t.name,
14
- description: t.description,
15
- input_schema: t.parameters,
16
- }));
17
-
18
- const stream = await client.messages.stream({
19
- model: this.model,
20
- max_tokens: 8192,
21
- system: SYSTEM_PROMPT,
22
- messages,
23
- tools: anthropicTools.length > 0 ? anthropicTools : undefined,
24
- });
25
-
26
- let currentToolId = null;
27
- let currentToolName = null;
28
- let toolInputJson = "";
29
-
30
- for await (const event of stream) {
31
- if (event.type === "content_block_start") {
32
- if (event.content_block.type === "text") {
33
- // text block starting
34
- } else if (event.content_block.type === "tool_use") {
35
- currentToolId = event.content_block.id;
36
- currentToolName = event.content_block.name;
37
- toolInputJson = "";
38
- yield { type: "tool_use_start", id: currentToolId, name: currentToolName };
39
- }
40
- } else if (event.type === "content_block_delta") {
41
- if (event.delta.type === "text_delta") {
42
- yield { type: "text", content: event.delta.text };
43
- } else if (event.delta.type === "input_json_delta") {
44
- toolInputJson += event.delta.partial_json;
45
- yield { type: "tool_input_delta", content: event.delta.partial_json };
46
- }
47
- } else if (event.type === "content_block_stop") {
48
- if (currentToolId) {
49
- let input = {};
50
- try {
51
- input = JSON.parse(toolInputJson);
52
- } catch {}
53
- yield { type: "tool_use_end", id: currentToolId, name: currentToolName, input };
54
- currentToolId = null;
55
- currentToolName = null;
56
- toolInputJson = "";
57
- }
58
- } else if (event.type === "message_stop") {
59
- // done
60
- }
61
- }
62
-
63
- const finalMessage = await stream.finalMessage();
64
- yield { type: "done", stopReason: finalMessage.stop_reason };
65
- }
66
- }
@@ -1,34 +0,0 @@
1
- export const SYSTEM_PROMPT = `You are Cheri, an AI coding assistant by HeySalad. You help developers write, debug, and understand code.
2
-
3
- You have access to tools that let you read files, write files, edit files, run shell commands, search files, and list directories. Use them proactively to help the user.
4
-
5
- Guidelines:
6
- - Read files before modifying them to understand the existing code.
7
- - Use edit_file for targeted changes instead of rewriting entire files.
8
- - When running commands, explain what you're about to run and why.
9
- - Be concise but thorough. Show relevant code snippets in your responses.
10
- - If you're unsure about something, say so rather than guessing.
11
- - Format responses with markdown for readability.`;
12
-
13
- export class BaseProvider {
14
- constructor(apiKey, model) {
15
- this.apiKey = apiKey;
16
- this.model = model;
17
- }
18
-
19
- /**
20
- * Async generator that yields streaming events:
21
- * { type: "text", content: string }
22
- * { type: "tool_use_start", id: string, name: string }
23
- * { type: "tool_input_delta", content: string }
24
- * { type: "tool_use_end", id: string, name: string, input: object }
25
- * { type: "done", stopReason: string }
26
- */
27
- async *chat(messages, tools) {
28
- throw new Error("chat() must be implemented by subclass");
29
- }
30
-
31
- getModel() {
32
- return this.model;
33
- }
34
- }
@@ -1,89 +0,0 @@
1
- import { BaseProvider, SYSTEM_PROMPT } from "./base.js";
2
-
3
- export class GeminiProvider extends BaseProvider {
4
- constructor(apiKey, model = "gemini-2.0-flash") {
5
- super(apiKey, model);
6
- }
7
-
8
- async *chat(messages, tools) {
9
- const { GoogleGenerativeAI } = await import("@google/generative-ai");
10
- const genAI = new GoogleGenerativeAI(this.apiKey);
11
-
12
- const geminiTools = tools.length > 0 ? [{
13
- functionDeclarations: tools.map((t) => ({
14
- name: t.name,
15
- description: t.description,
16
- parameters: t.parameters,
17
- })),
18
- }] : undefined;
19
-
20
- const genModel = genAI.getGenerativeModel({
21
- model: this.model,
22
- systemInstruction: SYSTEM_PROMPT,
23
- tools: geminiTools,
24
- });
25
-
26
- // Convert messages to Gemini format
27
- const history = [];
28
- for (const msg of messages.slice(0, -1)) {
29
- const role = msg.role === "assistant" ? "model" : "user";
30
- if (typeof msg.content === "string") {
31
- history.push({ role, parts: [{ text: msg.content }] });
32
- } else if (Array.isArray(msg.content)) {
33
- const parts = [];
34
- for (const block of msg.content) {
35
- if (block.type === "text") {
36
- parts.push({ text: block.text });
37
- } else if (block.type === "tool_use") {
38
- parts.push({ functionCall: { name: block.name, args: block.input } });
39
- } else if (block.type === "tool_result") {
40
- const resultText = typeof block.content === "string" ? block.content : JSON.stringify(block.content);
41
- parts.push({ functionResponse: { name: block.name || "tool", response: { result: resultText } } });
42
- }
43
- }
44
- if (parts.length > 0) history.push({ role, parts });
45
- }
46
- }
47
-
48
- const chat = genModel.startChat({ history });
49
-
50
- // Get the last message content
51
- const lastMsg = messages[messages.length - 1];
52
- const lastContent = typeof lastMsg.content === "string"
53
- ? lastMsg.content
54
- : lastMsg.content.map((b) => {
55
- if (b.type === "text") return b.text;
56
- if (b.type === "tool_result") {
57
- return typeof b.content === "string" ? b.content : JSON.stringify(b.content);
58
- }
59
- return "";
60
- }).join("\n");
61
-
62
- const result = await chat.sendMessageStream(lastContent);
63
-
64
- let hasToolCalls = false;
65
-
66
- for await (const chunk of result.stream) {
67
- const text = chunk.text();
68
- if (text) {
69
- yield { type: "text", content: text };
70
- }
71
-
72
- // Check for function calls
73
- const candidates = chunk.candidates || [];
74
- for (const candidate of candidates) {
75
- for (const part of candidate.content?.parts || []) {
76
- if (part.functionCall) {
77
- hasToolCalls = true;
78
- const id = `gemini_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
79
- yield { type: "tool_use_start", id, name: part.functionCall.name };
80
- yield { type: "tool_input_delta", content: JSON.stringify(part.functionCall.args) };
81
- yield { type: "tool_use_end", id, name: part.functionCall.name, input: part.functionCall.args || {} };
82
- }
83
- }
84
- }
85
- }
86
-
87
- yield { type: "done", stopReason: hasToolCalls ? "tool_use" : "end_turn" };
88
- }
89
- }
@@ -1,47 +0,0 @@
1
- import { getConfigValue } from "../config-store.js";
2
- import { getDefaultModel } from "../branding.js";
3
-
4
- export async function createProvider(options = {}) {
5
- const provider = options.provider || getConfigValue("ai.provider") || "anthropic";
6
- const model = options.model || getConfigValue("ai.model") || getDefaultModel(provider);
7
-
8
- // Resolve API key: env var takes priority, then config
9
- const envKeys = {
10
- anthropic: "ANTHROPIC_API_KEY",
11
- openai: "OPENAI_API_KEY",
12
- deepseek: "DEEPSEEK_API_KEY",
13
- gemini: "GEMINI_API_KEY",
14
- };
15
-
16
- const apiKey = process.env[envKeys[provider]] || getConfigValue(`ai.keys.${provider}`);
17
-
18
- if (!apiKey) {
19
- throw new Error(
20
- `No API key found for ${provider}. Set it with:\n` +
21
- ` cheri config set ai.keys.${provider} <your-key>\n` +
22
- `Or set the ${envKeys[provider]} environment variable.`
23
- );
24
- }
25
-
26
- // Lazy import only the selected provider
27
- switch (provider) {
28
- case "anthropic": {
29
- const { AnthropicProvider } = await import("./anthropic.js");
30
- return new AnthropicProvider(apiKey, model);
31
- }
32
- case "openai": {
33
- const { OpenAIProvider } = await import("./openai.js");
34
- return new OpenAIProvider(apiKey, model);
35
- }
36
- case "deepseek": {
37
- const { DeepSeekProvider } = await import("./openai.js");
38
- return new DeepSeekProvider(apiKey, model);
39
- }
40
- case "gemini": {
41
- const { GeminiProvider } = await import("./gemini.js");
42
- return new GeminiProvider(apiKey, model);
43
- }
44
- default:
45
- throw new Error(`Unknown provider: ${provider}. Supported: anthropic, openai, deepseek, gemini`);
46
- }
47
- }
@@ -1,105 +0,0 @@
1
- import { BaseProvider, SYSTEM_PROMPT } from "./base.js";
2
-
3
- export class OpenAIProvider extends BaseProvider {
4
- constructor(apiKey, model = "gpt-4o", baseURL = undefined) {
5
- super(apiKey, model);
6
- this.baseURL = baseURL;
7
- }
8
-
9
- async *chat(messages, tools) {
10
- const { default: OpenAI } = await import("openai");
11
- const clientOpts = { apiKey: this.apiKey };
12
- if (this.baseURL) clientOpts.baseURL = this.baseURL;
13
- const client = new OpenAI(clientOpts);
14
-
15
- // Convert from Anthropic message format to OpenAI format
16
- const openaiMessages = [{ role: "system", content: SYSTEM_PROMPT }];
17
- for (const msg of messages) {
18
- if (typeof msg.content === "string") {
19
- openaiMessages.push({ role: msg.role, content: msg.content });
20
- } else if (Array.isArray(msg.content)) {
21
- // Handle tool results and multi-part content
22
- for (const block of msg.content) {
23
- if (block.type === "tool_result") {
24
- openaiMessages.push({
25
- role: "tool",
26
- tool_call_id: block.tool_use_id,
27
- content: typeof block.content === "string" ? block.content : JSON.stringify(block.content),
28
- });
29
- } else if (block.type === "text") {
30
- openaiMessages.push({ role: msg.role, content: block.text });
31
- } else if (block.type === "tool_use") {
32
- // Assistant message with tool call
33
- openaiMessages.push({
34
- role: "assistant",
35
- content: null,
36
- tool_calls: [{
37
- id: block.id,
38
- type: "function",
39
- function: { name: block.name, arguments: JSON.stringify(block.input) },
40
- }],
41
- });
42
- }
43
- }
44
- }
45
- }
46
-
47
- const openaiTools = tools.map((t) => ({
48
- type: "function",
49
- function: { name: t.name, description: t.description, parameters: t.parameters },
50
- }));
51
-
52
- const stream = await client.chat.completions.create({
53
- model: this.model,
54
- messages: openaiMessages,
55
- tools: openaiTools.length > 0 ? openaiTools : undefined,
56
- stream: true,
57
- });
58
-
59
- const toolCalls = {};
60
-
61
- for await (const chunk of stream) {
62
- const delta = chunk.choices?.[0]?.delta;
63
- const finishReason = chunk.choices?.[0]?.finish_reason;
64
-
65
- if (delta?.content) {
66
- yield { type: "text", content: delta.content };
67
- }
68
-
69
- if (delta?.tool_calls) {
70
- for (const tc of delta.tool_calls) {
71
- const idx = tc.index;
72
- if (!toolCalls[idx]) {
73
- toolCalls[idx] = { id: tc.id || "", name: "", arguments: "" };
74
- }
75
- if (tc.id) toolCalls[idx].id = tc.id;
76
- if (tc.function?.name) {
77
- toolCalls[idx].name = tc.function.name;
78
- yield { type: "tool_use_start", id: toolCalls[idx].id, name: tc.function.name };
79
- }
80
- if (tc.function?.arguments) {
81
- toolCalls[idx].arguments += tc.function.arguments;
82
- yield { type: "tool_input_delta", content: tc.function.arguments };
83
- }
84
- }
85
- }
86
-
87
- if (finishReason) {
88
- // Emit tool_use_end for any accumulated tool calls
89
- for (const idx of Object.keys(toolCalls)) {
90
- const tc = toolCalls[idx];
91
- let input = {};
92
- try { input = JSON.parse(tc.arguments); } catch {}
93
- yield { type: "tool_use_end", id: tc.id, name: tc.name, input };
94
- }
95
- yield { type: "done", stopReason: finishReason === "tool_calls" ? "tool_use" : finishReason };
96
- }
97
- }
98
- }
99
- }
100
-
101
- export class DeepSeekProvider extends OpenAIProvider {
102
- constructor(apiKey, model = "deepseek-chat") {
103
- super(apiKey, model, "https://api.deepseek.com");
104
- }
105
- }
@@ -1,44 +0,0 @@
1
- import chalk from "chalk";
2
-
3
- let markedRender = null;
4
-
5
- async function getMarked() {
6
- if (markedRender) return markedRender;
7
- const { marked } = await import("marked");
8
- const { default: TerminalRenderer } = await import("marked-terminal");
9
- marked.setOptions({
10
- renderer: new TerminalRenderer({
11
- reflowText: true,
12
- width: Math.min(process.stdout.columns || 80, 100),
13
- tab: 2,
14
- }),
15
- });
16
- markedRender = marked;
17
- return markedRender;
18
- }
19
-
20
- export async function renderMarkdown(text) {
21
- try {
22
- const marked = await getMarked();
23
- return marked.parse(text);
24
- } catch {
25
- return text;
26
- }
27
- }
28
-
29
- export function renderToolUse(name, input) {
30
- const shortInput = Object.entries(input || {})
31
- .map(([k, v]) => {
32
- const val = typeof v === "string" ? (v.length > 60 ? v.slice(0, 57) + "..." : v) : JSON.stringify(v);
33
- return `${chalk.dim(k)}=${val}`;
34
- })
35
- .join(", ");
36
- return `${chalk.magenta("🔧")} ${chalk.bold(name)}(${shortInput})`;
37
- }
38
-
39
- export function renderToolResult(name, result) {
40
- if (result.error) {
41
- return chalk.red(`❌ ${name}: ${result.error}`);
42
- }
43
- return chalk.green(`✅ ${name} completed`);
44
- }