@heysalad/cheri-cli 0.4.0 → 0.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heysalad/cheri-cli",
3
- "version": "0.4.0",
3
+ "version": "0.5.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": {
@@ -1,6 +1,6 @@
1
1
  import { apiClient } from "../lib/api-client.js";
2
2
  import { getConfigValue, setConfigValue } from "../lib/config-store.js";
3
- import { streamChatCompletion } from "../lib/deepseek-client.js";
3
+ import { createProvider } from "../lib/providers/index.js";
4
4
  import { log } from "../lib/logger.js";
5
5
  import chalk from "chalk";
6
6
 
@@ -8,129 +8,96 @@ const SYSTEM_PROMPT = `You are Cheri Agent, an AI assistant for the Cheri cloud
8
8
 
9
9
  const TOOLS = [
10
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
- },
11
+ name: "get_account_info",
12
+ description: "Get the current user's account information",
13
+ parameters: { type: "object", properties: {}, required: [] },
17
14
  },
18
15
  {
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
- },
16
+ name: "list_workspaces",
17
+ description: "List all cloud workspaces for the current user",
18
+ parameters: { type: "object", properties: {}, required: [] },
25
19
  },
26
20
  {
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"],
21
+ name: "create_workspace",
22
+ description: "Launch a new cloud workspace for a GitHub repository",
23
+ parameters: {
24
+ type: "object",
25
+ properties: {
26
+ repo: { type: "string", description: "GitHub repo in owner/name format" },
37
27
  },
28
+ required: ["repo"],
38
29
  },
39
30
  },
40
31
  {
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"],
32
+ name: "stop_workspace",
33
+ description: "Stop and delete a running workspace",
34
+ parameters: {
35
+ type: "object",
36
+ properties: {
37
+ id: { type: "string", description: "Workspace ID to stop" },
51
38
  },
39
+ required: ["id"],
52
40
  },
53
41
  },
54
42
  {
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"],
43
+ name: "get_workspace_status",
44
+ description: "Get the status of a specific workspace",
45
+ parameters: {
46
+ type: "object",
47
+ properties: {
48
+ id: { type: "string", description: "Workspace ID" },
65
49
  },
50
+ required: ["id"],
66
51
  },
67
52
  },
68
53
  {
69
- type: "function",
70
- function: {
71
- name: "get_memory",
72
- description: "Retrieve all stored memory entries",
73
- parameters: { type: "object", properties: {}, required: [] },
74
- },
54
+ name: "get_memory",
55
+ description: "Retrieve all stored memory entries",
56
+ parameters: { type: "object", properties: {}, required: [] },
75
57
  },
76
58
  {
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"],
59
+ name: "add_memory",
60
+ description: "Add a new memory entry for the user",
61
+ parameters: {
62
+ type: "object",
63
+ properties: {
64
+ content: { type: "string", description: "Memory content to store" },
65
+ category: { type: "string", description: "Optional category (defaults to 'general')" },
88
66
  },
67
+ required: ["content"],
89
68
  },
90
69
  },
91
70
  {
92
- type: "function",
93
- function: {
94
- name: "clear_memory",
95
- description: "Clear all stored memory entries",
96
- parameters: { type: "object", properties: {}, required: [] },
97
- },
71
+ name: "clear_memory",
72
+ description: "Clear all stored memory entries",
73
+ parameters: { type: "object", properties: {}, required: [] },
98
74
  },
99
75
  {
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
- },
76
+ name: "get_usage",
77
+ description: "Get the user's API usage and rate limit statistics",
78
+ parameters: { type: "object", properties: {}, required: [] },
106
79
  },
107
80
  {
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"],
81
+ name: "get_config",
82
+ description: "Get a configuration value by key (dot notation supported)",
83
+ parameters: {
84
+ type: "object",
85
+ properties: {
86
+ key: { type: "string", description: "Config key, e.g. 'ai.provider'" },
118
87
  },
88
+ required: ["key"],
119
89
  },
120
90
  },
121
91
  {
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"],
92
+ name: "set_config",
93
+ description: "Set a configuration value",
94
+ parameters: {
95
+ type: "object",
96
+ properties: {
97
+ key: { type: "string", description: "Config key" },
98
+ value: { type: "string", description: "Value to set" },
133
99
  },
100
+ required: ["key", "value"],
134
101
  },
135
102
  },
136
103
  ];
@@ -170,87 +137,86 @@ async function executeTool(name, args) {
170
137
  }
171
138
 
172
139
  export async function runAgent(userRequest) {
140
+ const providerName = getConfigValue("agent.provider") || getConfigValue("ai.provider") || "anthropic";
141
+ const model = getConfigValue("agent.model") || undefined;
142
+
143
+ const provider = await createProvider({ provider: providerName, model });
144
+
173
145
  const messages = [
174
- { role: "system", content: SYSTEM_PROMPT },
175
146
  { role: "user", content: userRequest },
176
147
  ];
177
148
 
178
149
  const MAX_ITERATIONS = 10;
179
150
 
180
151
  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: "" } };
152
+ let fullText = "";
153
+ const toolCalls = [];
154
+
155
+ for await (const event of provider.chat(messages, TOOLS, { systemPrompt: SYSTEM_PROMPT })) {
156
+ switch (event.type) {
157
+ case "text":
158
+ process.stdout.write(event.content);
159
+ fullText += event.content;
160
+ break;
161
+
162
+ case "tool_use_start":
163
+ toolCalls.push({ id: event.id, name: event.name, input: {} });
164
+ break;
165
+
166
+ case "tool_input_delta":
167
+ // accumulated by provider, final input comes in tool_use_end
168
+ break;
169
+
170
+ case "tool_use_end":
171
+ if (toolCalls.length > 0) {
172
+ toolCalls[toolCalls.length - 1].input = event.input;
200
173
  }
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
- }
174
+ break;
175
+
176
+ case "done":
177
+ break;
205
178
  }
206
179
  }
207
180
 
208
- const toolCallList = Object.values(toolCalls);
209
-
210
181
  // No tool calls — final text response, done
211
- if (toolCallList.length === 0) {
212
- if (textContent) process.stdout.write("\n");
182
+ if (toolCalls.length === 0) {
183
+ if (fullText) process.stdout.write("\n");
213
184
  return;
214
185
  }
215
186
 
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);
187
+ if (fullText) process.stdout.write("\n");
226
188
 
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
- }
189
+ // Build assistant message in Anthropic content-block format
190
+ const assistantContent = [];
191
+ if (fullText) {
192
+ assistantContent.push({ type: "text", text: fullText });
193
+ }
194
+ for (const tc of toolCalls) {
195
+ assistantContent.push({ type: "tool_use", id: tc.id, name: tc.name, input: tc.input });
196
+ }
197
+ messages.push({ role: "assistant", content: assistantContent });
236
198
 
237
- log.info(`Calling ${chalk.cyan(fnName)}${Object.keys(args).length ? chalk.dim(" " + JSON.stringify(args)) : ""}`);
199
+ // Execute each tool call and build tool results
200
+ const toolResults = [];
201
+ for (const tc of toolCalls) {
202
+ log.info(`Calling ${chalk.cyan(tc.name)}${Object.keys(tc.input).length ? chalk.dim(" " + JSON.stringify(tc.input)) : ""}`);
238
203
 
239
- const result = await executeTool(fnName, args);
240
- const resultStr = JSON.stringify(result);
204
+ const result = await executeTool(tc.name, tc.input);
241
205
 
242
206
  if (result.error) {
243
207
  log.error(result.error);
244
208
  } else {
245
- log.success(fnName);
209
+ log.success(tc.name);
246
210
  }
247
211
 
248
- messages.push({
249
- role: "tool",
250
- tool_call_id: tc.id,
251
- content: resultStr,
212
+ toolResults.push({
213
+ type: "tool_result",
214
+ tool_use_id: tc.id,
215
+ content: JSON.stringify(result),
252
216
  });
253
217
  }
218
+
219
+ messages.push({ role: "user", content: toolResults });
254
220
  }
255
221
 
256
222
  log.warn("Agent reached maximum iterations (10). Stopping.");
@@ -31,6 +31,7 @@ export function getDefaultModel(provider) {
31
31
  openai: "gpt-4o",
32
32
  deepseek: "deepseek-chat",
33
33
  gemini: "gemini-2.0-flash",
34
+ bedrock: "anthropic.claude-opus-4-6-v1:0",
34
35
  };
35
36
  return defaults[provider] || "unknown";
36
37
  }
@@ -82,7 +82,11 @@ function getDefaultConfig() {
82
82
  openai: "",
83
83
  deepseek: "",
84
84
  gemini: "",
85
+ bedrock: "",
85
86
  },
86
87
  },
88
+ bedrock: {
89
+ region: "us-east-1",
90
+ },
87
91
  };
88
92
  }
@@ -5,7 +5,7 @@ export class AnthropicProvider extends BaseProvider {
5
5
  super(apiKey, model);
6
6
  }
7
7
 
8
- async *chat(messages, tools) {
8
+ async *chat(messages, tools, options = {}) {
9
9
  const { default: Anthropic } = await import("@anthropic-ai/sdk");
10
10
  const client = new Anthropic({ apiKey: this.apiKey });
11
11
 
@@ -18,7 +18,7 @@ export class AnthropicProvider extends BaseProvider {
18
18
  const stream = await client.messages.stream({
19
19
  model: this.model,
20
20
  max_tokens: 8192,
21
- system: SYSTEM_PROMPT,
21
+ system: options.systemPrompt || SYSTEM_PROMPT,
22
22
  messages,
23
23
  tools: anthropicTools.length > 0 ? anthropicTools : undefined,
24
24
  });
@@ -24,7 +24,7 @@ export class BaseProvider {
24
24
  * { type: "tool_use_end", id: string, name: string, input: object }
25
25
  * { type: "done", stopReason: string }
26
26
  */
27
- async *chat(messages, tools) {
27
+ async *chat(messages, tools, options = {}) {
28
28
  throw new Error("chat() must be implemented by subclass");
29
29
  }
30
30
 
@@ -0,0 +1,7 @@
1
+ import { OpenAIProvider } from "./openai.js";
2
+
3
+ export class BedrockProvider extends OpenAIProvider {
4
+ constructor(apiKey, model = "anthropic.claude-opus-4-6-v1:0", region = "us-east-1") {
5
+ super(apiKey, model, `https://bedrock-runtime.${region}.amazonaws.com/openai/v1`);
6
+ }
7
+ }
@@ -5,7 +5,7 @@ export class GeminiProvider extends BaseProvider {
5
5
  super(apiKey, model);
6
6
  }
7
7
 
8
- async *chat(messages, tools) {
8
+ async *chat(messages, tools, options = {}) {
9
9
  const { GoogleGenerativeAI } = await import("@google/generative-ai");
10
10
  const genAI = new GoogleGenerativeAI(this.apiKey);
11
11
 
@@ -19,7 +19,7 @@ export class GeminiProvider extends BaseProvider {
19
19
 
20
20
  const genModel = genAI.getGenerativeModel({
21
21
  model: this.model,
22
- systemInstruction: SYSTEM_PROMPT,
22
+ systemInstruction: options.systemPrompt || SYSTEM_PROMPT,
23
23
  tools: geminiTools,
24
24
  });
25
25
 
@@ -11,6 +11,7 @@ export async function createProvider(options = {}) {
11
11
  openai: "OPENAI_API_KEY",
12
12
  deepseek: "DEEPSEEK_API_KEY",
13
13
  gemini: "GEMINI_API_KEY",
14
+ bedrock: "AWS_BEARER_TOKEN_BEDROCK",
14
15
  };
15
16
 
16
17
  const apiKey = process.env[envKeys[provider]] || getConfigValue(`ai.keys.${provider}`);
@@ -41,7 +42,12 @@ export async function createProvider(options = {}) {
41
42
  const { GeminiProvider } = await import("./gemini.js");
42
43
  return new GeminiProvider(apiKey, model);
43
44
  }
45
+ case "bedrock": {
46
+ const { BedrockProvider } = await import("./bedrock.js");
47
+ const region = getConfigValue("bedrock.region") || "us-east-1";
48
+ return new BedrockProvider(apiKey, model, region);
49
+ }
44
50
  default:
45
- throw new Error(`Unknown provider: ${provider}. Supported: anthropic, openai, deepseek, gemini`);
51
+ throw new Error(`Unknown provider: ${provider}. Supported: anthropic, openai, deepseek, gemini, bedrock`);
46
52
  }
47
53
  }
@@ -6,14 +6,14 @@ export class OpenAIProvider extends BaseProvider {
6
6
  this.baseURL = baseURL;
7
7
  }
8
8
 
9
- async *chat(messages, tools) {
9
+ async *chat(messages, tools, options = {}) {
10
10
  const { default: OpenAI } = await import("openai");
11
11
  const clientOpts = { apiKey: this.apiKey };
12
12
  if (this.baseURL) clientOpts.baseURL = this.baseURL;
13
13
  const client = new OpenAI(clientOpts);
14
14
 
15
15
  // Convert from Anthropic message format to OpenAI format
16
- const openaiMessages = [{ role: "system", content: SYSTEM_PROMPT }];
16
+ const openaiMessages = [{ role: "system", content: options.systemPrompt || SYSTEM_PROMPT }];
17
17
  for (const msg of messages) {
18
18
  if (typeof msg.content === "string") {
19
19
  openaiMessages.push({ role: msg.role, content: msg.content });
@@ -1,64 +0,0 @@
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
- }