@heysalad/cheri-cli 0.3.0 → 0.4.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.
package/README.md CHANGED
@@ -1,88 +1,60 @@
1
- # cheri-cli
1
+ # Cheri CLI
2
2
 
3
- CLI for [Cheri](https://cheri.heysalad.app) the AI-powered cloud IDE that never forgets.
4
-
5
- Manage workspaces, track API usage, and access your AI memory from the terminal.
3
+ AI-powered cloud IDE by [HeySalad](https://heysalad.app). Like Claude Code, but for cloud workspaces.
6
4
 
7
5
  ## Install
8
6
 
9
7
  ```bash
10
- npm install -g cheri-cli
8
+ npm install -g @heysalad/cheri-cli
11
9
  ```
12
10
 
13
- Requires Node.js 18+.
14
-
15
- ## Quick Start
11
+ ## Usage
16
12
 
17
13
  ```bash
18
- # Authenticate with your Cheri account
14
+ # Login to your Cheri account
19
15
  cheri login
20
16
 
21
- # Launch a cloud workspace
22
- cheri workspace launch owner/my-repo
23
-
24
17
  # Check account status
25
18
  cheri status
26
19
 
27
- # View API usage and rate limits
28
- cheri usage
29
- ```
20
+ # Launch a cloud workspace
21
+ cheri workspace launch owner/repo
30
22
 
31
- ## Commands
32
-
33
- | Command | Description |
34
- |---|---|
35
- | `cheri login` | Authenticate with GitHub |
36
- | `cheri status` | Show account and workspace status |
37
- | `cheri usage` | Show API usage and rate limit status |
38
- | `cheri workspace launch <repo>` | Launch a new cloud workspace |
39
- | `cheri workspace list` | List all workspaces |
40
- | `cheri workspace stop <id>` | Stop a running workspace |
41
- | `cheri workspace status <id>` | Get workspace status |
42
- | `cheri memory show` | Show current memory entries |
43
- | `cheri memory add <text>` | Add a memory entry |
44
- | `cheri memory clear` | Clear all memory |
45
- | `cheri memory export` | Export memory to JSON |
46
- | `cheri config list` | Show all configuration |
47
- | `cheri config get <key>` | Get a config value |
48
- | `cheri config set <key> <value>` | Set a config value |
49
- | `cheri init` | Initialize a project |
50
-
51
- ## Interactive REPL
52
-
53
- Run `cheri` with no arguments to enter the interactive REPL:
23
+ # List your workspaces
24
+ cheri workspace list
54
25
 
55
- ```
56
- $ cheri
57
- 🍒 cheri > help
58
- 🍒 cheri > workspace list
59
- 🍒 cheri > usage
60
- 🍒 cheri > exit
61
- ```
26
+ # Stop a workspace
27
+ cheri workspace stop
28
+
29
+ # Initialize AI project config
30
+ cheri init
62
31
 
63
- ## Rate Limits
32
+ # Manage persistent memory
33
+ cheri memory show
34
+ cheri memory add "Always use TypeScript strict mode"
35
+ cheri memory clear
64
36
 
65
- | Plan | Limit |
66
- |---|---|
67
- | Free | 100 requests/hour |
68
- | Pro | 1,000 requests/hour |
37
+ # View/update configuration
38
+ cheri config list
39
+ cheri config set apiUrl https://cheri.heysalad.app
40
+ ```
69
41
 
70
- Use `cheri usage` to check your current rate limit status.
42
+ ## How it works
71
43
 
72
- ## Configuration
44
+ 1. **`cheri login`** opens your browser for GitHub OAuth, then you paste your API token
45
+ 2. **`cheri workspace launch`** spins up a cloud workspace with code-server (VS Code in browser)
46
+ 3. **`cheri memory`** stores persistent context that follows you across sessions
47
+ 4. **`cheri init`** creates a local `.ai/` directory with project constitution files
73
48
 
74
- Config is stored in `~/.cheri/`. Set the API URL if self-hosting:
49
+ ## Requirements
75
50
 
76
- ```bash
77
- cheri config set apiUrl https://your-instance.example.com
78
- ```
51
+ - Node.js >= 18
79
52
 
80
53
  ## Links
81
54
 
82
55
  - [Cheri Cloud IDE](https://cheri.heysalad.app)
83
- - [Dashboard](https://cheri.heysalad.app/dashboard)
84
- - [GitHub](https://github.com/chilu18/cloud-ide)
56
+ - [GitHub](https://github.com/Hey-Salad/cheri-cli)
85
57
 
86
58
  ## License
87
59
 
88
- MIT
60
+ MIT - HeySalad
package/bin/cheri.js CHANGED
@@ -7,12 +7,13 @@ import { registerStatusCommand } from "../src/commands/status.js";
7
7
  import { registerMemoryCommand } from "../src/commands/memory.js";
8
8
  import { registerConfigCommand } from "../src/commands/config.js";
9
9
  import { registerWorkspaceCommand } from "../src/commands/workspace.js";
10
- import { registerUsageCommand } from "../src/commands/usage.js";
10
+ import { registerChatCommand } from "../src/commands/chat.js";
11
+ import { registerAgentCommand } from "../src/commands/agent.js";
11
12
 
12
13
  program
13
14
  .name("cheri")
14
15
  .description("Cheri CLI - AI-powered cloud IDE by HeySalad")
15
- .version("0.1.0");
16
+ .version("0.2.0");
16
17
 
17
18
  registerLoginCommand(program);
18
19
  registerInitCommand(program);
@@ -20,7 +21,8 @@ registerStatusCommand(program);
20
21
  registerMemoryCommand(program);
21
22
  registerConfigCommand(program);
22
23
  registerWorkspaceCommand(program);
23
- registerUsageCommand(program);
24
+ registerChatCommand(program);
25
+ registerAgentCommand(program);
24
26
 
25
27
  // If no args, launch interactive command REPL
26
28
  if (!process.argv.slice(2).length) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heysalad/cheri-cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Cheri CLI - AI-powered cloud IDE by HeySalad. Like Claude Code, but for cloud workspaces.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,14 +8,15 @@
8
8
  },
9
9
  "files": [
10
10
  "bin/",
11
- "src/"
11
+ "src/",
12
+ "README.md"
12
13
  ],
13
14
  "scripts": {
14
15
  "start": "node bin/cheri.js",
15
16
  "dev": "node bin/cheri.js",
16
- "release:patch": "npm version patch && npm publish && git push && git push --tags",
17
- "release:minor": "npm version minor && npm publish && git push && git push --tags",
18
- "release:major": "npm version major && npm publish && git push && git push --tags"
17
+ "release:patch": "npm version patch && npm publish --access public && git push && git push --tags",
18
+ "release:minor": "npm version minor && npm publish --access public && git push && git push --tags",
19
+ "release:major": "npm version major && npm publish --access public && git push && git push --tags"
19
20
  },
20
21
  "keywords": [
21
22
  "cloud-ide",
@@ -28,18 +29,23 @@
28
29
  ],
29
30
  "repository": {
30
31
  "type": "git",
31
- "url": "https://github.com/chilu18/cloud-ide.git",
32
- "directory": "cli"
32
+ "url": "https://github.com/Hey-Salad/cheri-cli.git"
33
33
  },
34
+ "homepage": "https://cheri.heysalad.app",
34
35
  "author": "HeySalad",
35
36
  "license": "MIT",
36
37
  "engines": {
37
38
  "node": ">=18"
38
39
  },
39
40
  "dependencies": {
41
+ "@anthropic-ai/sdk": "^0.74.0",
42
+ "@google/generative-ai": "^0.24.1",
40
43
  "chalk": "^5.3.0",
41
44
  "commander": "^12.1.0",
42
45
  "inquirer": "^9.2.23",
46
+ "marked": "^15.0.12",
47
+ "marked-terminal": "^7.3.0",
48
+ "openai": "^6.22.0",
43
49
  "ora": "^8.0.1"
44
50
  }
45
51
  }
@@ -0,0 +1,272 @@
1
+ import { apiClient } from "../lib/api-client.js";
2
+ import { getConfigValue, setConfigValue } from "../lib/config-store.js";
3
+ import { streamChatCompletion } from "../lib/deepseek-client.js";
4
+ import { log } from "../lib/logger.js";
5
+ import chalk from "chalk";
6
+
7
+ const SYSTEM_PROMPT = `You are Cheri Agent, an AI assistant for the Cheri cloud IDE platform. You help users manage cloud workspaces, memory, configuration, and their account. Use the provided tools to get real data — never guess or fabricate information. Be concise. After performing actions, briefly summarize what happened and the result.`;
8
+
9
+ const TOOLS = [
10
+ {
11
+ type: "function",
12
+ function: {
13
+ name: "get_account_info",
14
+ description: "Get the current user's account information",
15
+ parameters: { type: "object", properties: {}, required: [] },
16
+ },
17
+ },
18
+ {
19
+ type: "function",
20
+ function: {
21
+ name: "list_workspaces",
22
+ description: "List all cloud workspaces for the current user",
23
+ parameters: { type: "object", properties: {}, required: [] },
24
+ },
25
+ },
26
+ {
27
+ type: "function",
28
+ function: {
29
+ name: "create_workspace",
30
+ description: "Launch a new cloud workspace for a GitHub repository",
31
+ parameters: {
32
+ type: "object",
33
+ properties: {
34
+ repo: { type: "string", description: "GitHub repo in owner/name format" },
35
+ },
36
+ required: ["repo"],
37
+ },
38
+ },
39
+ },
40
+ {
41
+ type: "function",
42
+ function: {
43
+ name: "stop_workspace",
44
+ description: "Stop and delete a running workspace",
45
+ parameters: {
46
+ type: "object",
47
+ properties: {
48
+ id: { type: "string", description: "Workspace ID to stop" },
49
+ },
50
+ required: ["id"],
51
+ },
52
+ },
53
+ },
54
+ {
55
+ type: "function",
56
+ function: {
57
+ name: "get_workspace_status",
58
+ description: "Get the status of a specific workspace",
59
+ parameters: {
60
+ type: "object",
61
+ properties: {
62
+ id: { type: "string", description: "Workspace ID" },
63
+ },
64
+ required: ["id"],
65
+ },
66
+ },
67
+ },
68
+ {
69
+ type: "function",
70
+ function: {
71
+ name: "get_memory",
72
+ description: "Retrieve all stored memory entries",
73
+ parameters: { type: "object", properties: {}, required: [] },
74
+ },
75
+ },
76
+ {
77
+ type: "function",
78
+ function: {
79
+ name: "add_memory",
80
+ description: "Add a new memory entry for the user",
81
+ parameters: {
82
+ type: "object",
83
+ properties: {
84
+ content: { type: "string", description: "Memory content to store" },
85
+ category: { type: "string", description: "Optional category (defaults to 'general')" },
86
+ },
87
+ required: ["content"],
88
+ },
89
+ },
90
+ },
91
+ {
92
+ type: "function",
93
+ function: {
94
+ name: "clear_memory",
95
+ description: "Clear all stored memory entries",
96
+ parameters: { type: "object", properties: {}, required: [] },
97
+ },
98
+ },
99
+ {
100
+ type: "function",
101
+ function: {
102
+ name: "get_usage",
103
+ description: "Get the user's API usage and rate limit statistics",
104
+ parameters: { type: "object", properties: {}, required: [] },
105
+ },
106
+ },
107
+ {
108
+ type: "function",
109
+ function: {
110
+ name: "get_config",
111
+ description: "Get a configuration value by key (dot notation supported)",
112
+ parameters: {
113
+ type: "object",
114
+ properties: {
115
+ key: { type: "string", description: "Config key, e.g. 'ai.provider'" },
116
+ },
117
+ required: ["key"],
118
+ },
119
+ },
120
+ },
121
+ {
122
+ type: "function",
123
+ function: {
124
+ name: "set_config",
125
+ description: "Set a configuration value",
126
+ parameters: {
127
+ type: "object",
128
+ properties: {
129
+ key: { type: "string", description: "Config key" },
130
+ value: { type: "string", description: "Value to set" },
131
+ },
132
+ required: ["key", "value"],
133
+ },
134
+ },
135
+ },
136
+ ];
137
+
138
+ async function executeTool(name, args) {
139
+ try {
140
+ switch (name) {
141
+ case "get_account_info":
142
+ return await apiClient.getMe();
143
+ case "list_workspaces":
144
+ return await apiClient.listWorkspaces();
145
+ case "create_workspace":
146
+ return await apiClient.createWorkspace(args.repo);
147
+ case "stop_workspace":
148
+ return await apiClient.deleteWorkspace(args.id);
149
+ case "get_workspace_status":
150
+ return await apiClient.getWorkspaceStatus(args.id);
151
+ case "get_memory":
152
+ return await apiClient.getMemory();
153
+ case "add_memory":
154
+ return await apiClient.addMemory(args.content, args.category);
155
+ case "clear_memory":
156
+ return await apiClient.clearMemory();
157
+ case "get_usage":
158
+ return await apiClient.getUsage();
159
+ case "get_config":
160
+ return { key: args.key, value: getConfigValue(args.key) };
161
+ case "set_config":
162
+ setConfigValue(args.key, args.value);
163
+ return { key: args.key, value: args.value, status: "updated" };
164
+ default:
165
+ return { error: `Unknown tool: ${name}` };
166
+ }
167
+ } catch (err) {
168
+ return { error: err.message };
169
+ }
170
+ }
171
+
172
+ export async function runAgent(userRequest) {
173
+ const messages = [
174
+ { role: "system", content: SYSTEM_PROMPT },
175
+ { role: "user", content: userRequest },
176
+ ];
177
+
178
+ const MAX_ITERATIONS = 10;
179
+
180
+ for (let i = 0; i < MAX_ITERATIONS; i++) {
181
+ let textContent = "";
182
+ const toolCalls = {};
183
+
184
+ for await (const chunk of streamChatCompletion(messages, TOOLS)) {
185
+ const delta = chunk.choices?.[0]?.delta;
186
+ if (!delta) continue;
187
+
188
+ // Stream text content to stdout
189
+ if (delta.content) {
190
+ process.stdout.write(delta.content);
191
+ textContent += delta.content;
192
+ }
193
+
194
+ // Accumulate tool call deltas
195
+ if (delta.tool_calls) {
196
+ for (const tc of delta.tool_calls) {
197
+ const idx = tc.index;
198
+ if (!toolCalls[idx]) {
199
+ toolCalls[idx] = { id: tc.id || "", function: { name: "", arguments: "" } };
200
+ }
201
+ if (tc.id) toolCalls[idx].id = tc.id;
202
+ if (tc.function?.name) toolCalls[idx].function.name += tc.function.name;
203
+ if (tc.function?.arguments) toolCalls[idx].function.arguments += tc.function.arguments;
204
+ }
205
+ }
206
+ }
207
+
208
+ const toolCallList = Object.values(toolCalls);
209
+
210
+ // No tool calls — final text response, done
211
+ if (toolCallList.length === 0) {
212
+ if (textContent) process.stdout.write("\n");
213
+ return;
214
+ }
215
+
216
+ // Append assistant message with tool calls
217
+ const assistantMsg = { role: "assistant", content: textContent || null, tool_calls: [] };
218
+ for (const tc of toolCallList) {
219
+ assistantMsg.tool_calls.push({
220
+ id: tc.id,
221
+ type: "function",
222
+ function: { name: tc.function.name, arguments: tc.function.arguments },
223
+ });
224
+ }
225
+ messages.push(assistantMsg);
226
+
227
+ // Execute each tool call and append results
228
+ for (const tc of toolCallList) {
229
+ const fnName = tc.function.name;
230
+ let args = {};
231
+ try {
232
+ args = JSON.parse(tc.function.arguments || "{}");
233
+ } catch {
234
+ args = {};
235
+ }
236
+
237
+ log.info(`Calling ${chalk.cyan(fnName)}${Object.keys(args).length ? chalk.dim(" " + JSON.stringify(args)) : ""}`);
238
+
239
+ const result = await executeTool(fnName, args);
240
+ const resultStr = JSON.stringify(result);
241
+
242
+ if (result.error) {
243
+ log.error(result.error);
244
+ } else {
245
+ log.success(fnName);
246
+ }
247
+
248
+ messages.push({
249
+ role: "tool",
250
+ tool_call_id: tc.id,
251
+ content: resultStr,
252
+ });
253
+ }
254
+ }
255
+
256
+ log.warn("Agent reached maximum iterations (10). Stopping.");
257
+ }
258
+
259
+ export function registerAgentCommand(program) {
260
+ program
261
+ .command("agent")
262
+ .argument("<request...>")
263
+ .description("AI agent — natural language command interface")
264
+ .action(async (requestParts) => {
265
+ const request = requestParts.join(" ");
266
+ try {
267
+ await runAgent(request);
268
+ } catch (err) {
269
+ log.error(err.message);
270
+ }
271
+ });
272
+ }
@@ -0,0 +1,15 @@
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
+ }
@@ -53,13 +53,21 @@ export async function showMemory(options = {}) {
53
53
  }
54
54
 
55
55
  export async function addMemory(content, category = "general") {
56
- const { entry, count } = await apiClient.addMemory(content, category);
57
- log.success(`Memory saved (${count} total). Category: ${chalk.cyan(entry.category)}`);
56
+ try {
57
+ const { entry, count } = await apiClient.addMemory(content, category);
58
+ log.success(`Memory saved (${count} total). Category: ${chalk.cyan(entry.category)}`);
59
+ } catch (err) {
60
+ throw err;
61
+ }
58
62
  }
59
63
 
60
64
  export async function clearMemory() {
61
- await apiClient.clearMemory();
62
- log.success("All memories cleared.");
65
+ try {
66
+ await apiClient.clearMemory();
67
+ log.success("All memories cleared.");
68
+ } catch (err) {
69
+ throw err;
70
+ }
63
71
  }
64
72
 
65
73
  export async function exportMemory(options = {}) {
@@ -0,0 +1,36 @@
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
+ }
@@ -74,5 +74,15 @@ function getDefaultConfig() {
74
74
  theme: "dark",
75
75
  fontSize: 14,
76
76
  },
77
+ ai: {
78
+ provider: "anthropic",
79
+ model: "",
80
+ keys: {
81
+ anthropic: "",
82
+ openai: "",
83
+ deepseek: "",
84
+ gemini: "",
85
+ },
86
+ },
77
87
  };
78
88
  }
@@ -0,0 +1,64 @@
1
+ import { getConfigValue } from "./config-store.js";
2
+
3
+ export function getApiKey() {
4
+ const key = getConfigValue("deepseekApiKey") || getConfigValue("ai.keys.deepseek");
5
+ if (!key) {
6
+ throw new Error(
7
+ "DeepSeek API key not set. Run: cheri config set deepseekApiKey sk-..."
8
+ );
9
+ }
10
+ return key;
11
+ }
12
+
13
+ export function getModel() {
14
+ return getConfigValue("agent.model") || "deepseek-chat";
15
+ }
16
+
17
+ export async function* streamChatCompletion(messages, tools) {
18
+ const apiKey = getApiKey();
19
+ const model = getModel();
20
+
21
+ const body = {
22
+ model,
23
+ messages,
24
+ stream: true,
25
+ };
26
+ if (tools && tools.length > 0) {
27
+ body.tools = tools;
28
+ }
29
+
30
+ const res = await fetch("https://api.deepseek.com/chat/completions", {
31
+ method: "POST",
32
+ headers: {
33
+ "Content-Type": "application/json",
34
+ Authorization: `Bearer ${apiKey}`,
35
+ },
36
+ body: JSON.stringify(body),
37
+ });
38
+
39
+ if (!res.ok) {
40
+ const text = await res.text();
41
+ throw new Error(`DeepSeek API error (${res.status}): ${text}`);
42
+ }
43
+
44
+ const decoder = new TextDecoder();
45
+ let buffer = "";
46
+
47
+ for await (const chunk of res.body) {
48
+ buffer += decoder.decode(chunk, { stream: true });
49
+ const lines = buffer.split("\n");
50
+ buffer = lines.pop();
51
+
52
+ for (const line of lines) {
53
+ const trimmed = line.trim();
54
+ if (!trimmed || !trimmed.startsWith("data: ")) continue;
55
+ const data = trimmed.slice(6);
56
+ if (data === "[DONE]") return;
57
+ try {
58
+ yield JSON.parse(data);
59
+ } catch {
60
+ // skip malformed chunks
61
+ }
62
+ }
63
+ }
64
+ }
package/src/lib/logger.js CHANGED
@@ -36,7 +36,7 @@ export const log = {
36
36
  console.log(chalk.dim(prefix) + " " + item);
37
37
  });
38
38
  },
39
- banner(version = "0.1.0") {
39
+ banner(version = "0.2.0") {
40
40
  console.log();
41
41
  console.log(` ${chalk.red("🍒")} ${chalk.red.bold("Cheri")}`);
42
42
  console.log(` ${chalk.dim("AI-powered cloud IDE by HeySalad")}`);