@heysalad/cheri-cli 0.9.0 → 1.0.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/bin/cheri.js +7 -1
- package/package.json +4 -1
- package/src/commands/agent.js +317 -85
- package/src/lib/approval.js +120 -0
- package/src/lib/command-safety.js +170 -0
- package/src/lib/config-store.js +142 -37
- package/src/lib/context.js +103 -37
- package/src/lib/diff-tracker.js +157 -0
- package/src/lib/logger.js +15 -1
- package/src/lib/markdown.js +62 -0
- package/src/lib/mcp/client.js +239 -0
- package/src/lib/multi-agent.js +153 -0
- package/src/lib/providers/index.js +285 -0
- package/src/lib/sandbox.js +164 -0
package/bin/cheri.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { readFileSync } from "fs";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { dirname, join } from "path";
|
|
3
6
|
import { program } from "commander";
|
|
4
7
|
import { registerInitCommand } from "../src/commands/init.js";
|
|
5
8
|
import { registerLoginCommand } from "../src/commands/login.js";
|
|
@@ -10,10 +13,13 @@ import { registerWorkspaceCommand } from "../src/commands/workspace.js";
|
|
|
10
13
|
import { registerUsageCommand } from "../src/commands/usage.js";
|
|
11
14
|
import { registerAgentCommand } from "../src/commands/agent.js";
|
|
12
15
|
|
|
16
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
18
|
+
|
|
13
19
|
program
|
|
14
20
|
.name("cheri")
|
|
15
21
|
.description("Cheri CLI - AI-powered cloud IDE by HeySalad")
|
|
16
|
-
.version(
|
|
22
|
+
.version(pkg.version);
|
|
17
23
|
|
|
18
24
|
registerLoginCommand(program);
|
|
19
25
|
registerInitCommand(program);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heysalad/cheri-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.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": {
|
|
@@ -39,7 +39,10 @@
|
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"chalk": "^5.3.0",
|
|
41
41
|
"commander": "^12.1.0",
|
|
42
|
+
"diff": "^8.0.3",
|
|
42
43
|
"inquirer": "^9.2.23",
|
|
44
|
+
"marked": "^15.0.12",
|
|
45
|
+
"marked-terminal": "^7.3.0",
|
|
43
46
|
"ora": "^8.0.1"
|
|
44
47
|
}
|
|
45
48
|
}
|
package/src/commands/agent.js
CHANGED
|
@@ -1,20 +1,29 @@
|
|
|
1
|
-
import { apiClient } from "../lib/api-client.js";
|
|
2
1
|
import { log } from "../lib/logger.js";
|
|
2
|
+
import { getConfigValue } from "../lib/config-store.js";
|
|
3
3
|
import { getToolDefinitions, executeTool as executeLocalTool, requiresConfirmation as toolRequiresConfirmation } from "../lib/tools/index.js";
|
|
4
4
|
import { generateSessionId, saveSession, loadSession, listSessions } from "../lib/sessions/index.js";
|
|
5
|
-
import { compactMessages, shouldCompact, estimateMessagesTokens } from "../lib/context.js";
|
|
5
|
+
import { compactMessages, shouldCompact, estimateMessagesTokens, getContextStats } from "../lib/context.js";
|
|
6
6
|
import { loadHooks, runHooks } from "../lib/hooks/index.js";
|
|
7
7
|
import { loadPlugins, getSkillContext, getSlashCommand } from "../lib/plugins/index.js";
|
|
8
|
-
import {
|
|
8
|
+
import { loadPermissions, checkPermission } from "../lib/permissions.js";
|
|
9
9
|
import { getMemoryContext } from "../lib/memory.js";
|
|
10
|
+
import { createProvider } from "../lib/providers/index.js";
|
|
11
|
+
import { renderMarkdown, renderDiff } from "../lib/markdown.js";
|
|
12
|
+
import { classifyCommand, getSafetyLabel } from "../lib/command-safety.js";
|
|
13
|
+
import { sandboxExec, SandboxLevel, getSandboxInfo } from "../lib/sandbox.js";
|
|
14
|
+
import { snapshotFile, recordChange, getSessionChanges, generateDiff, rollbackChange, rollbackAll, clearSessionChanges } from "../lib/diff-tracker.js";
|
|
15
|
+
import { shouldApprove, promptApproval, getApprovalMode } from "../lib/approval.js";
|
|
16
|
+
import { McpManager } from "../lib/mcp/client.js";
|
|
17
|
+
import { AgentOrchestrator } from "../lib/multi-agent.js";
|
|
10
18
|
import chalk from "chalk";
|
|
11
19
|
import readline from "readline";
|
|
12
20
|
|
|
13
21
|
const SYSTEM_PROMPT = `You are Cheri, an AI coding assistant by HeySalad. You are a powerful agentic coding tool that can read, write, edit, and search code, execute shell commands, and manage cloud workspaces.
|
|
14
22
|
|
|
15
|
-
You have
|
|
16
|
-
1. LOCAL CODING TOOLS — read_file, write_file, edit_file, run_command, search_files, search_content, list_directory
|
|
17
|
-
2. CLOUD PLATFORM TOOLS — get_account_info, list_workspaces, create_workspace, stop_workspace, get_workspace_status, get_memory, add_memory, clear_memory, get_usage, get_config, set_config
|
|
23
|
+
You have these tool categories:
|
|
24
|
+
1. LOCAL CODING TOOLS — read_file, write_file, edit_file, run_command, search_files, search_content, list_directory
|
|
25
|
+
2. CLOUD PLATFORM TOOLS — get_account_info, list_workspaces, create_workspace, stop_workspace, get_workspace_status, get_memory, add_memory, clear_memory, get_usage, get_config, set_config
|
|
26
|
+
3. AGENT TOOLS — spawn_agent (delegate subtasks to child agents), get_changes (view file changes this session), rollback_change (undo a file change)
|
|
18
27
|
|
|
19
28
|
Guidelines:
|
|
20
29
|
- Read files before editing them. Understand existing code before making changes.
|
|
@@ -24,9 +33,10 @@ Guidelines:
|
|
|
24
33
|
- For shell commands, prefer specific commands over broad ones.
|
|
25
34
|
- Be concise. Show what you did and the result.
|
|
26
35
|
- Never guess file contents — always read first.
|
|
36
|
+
- Use spawn_agent for independent subtasks that can run in parallel.
|
|
27
37
|
- Current working directory: ${process.cwd()}`;
|
|
28
38
|
|
|
29
|
-
// Cloud platform tools
|
|
39
|
+
// ── Cloud platform tools ──────────────────────────────────────────────────────
|
|
30
40
|
const CLOUD_TOOLS = [
|
|
31
41
|
{ name: "get_account_info", description: "Get the current user's account information", parameters: { type: "object", properties: {}, required: [] } },
|
|
32
42
|
{ name: "list_workspaces", description: "List all cloud workspaces for the current user", parameters: { type: "object", properties: {}, required: [] } },
|
|
@@ -41,22 +51,37 @@ const CLOUD_TOOLS = [
|
|
|
41
51
|
{ name: "set_config", description: "Set a configuration value", parameters: { type: "object", properties: { key: { type: "string", description: "Config key" }, value: { type: "string", description: "Value to set" } }, required: ["key", "value"] } },
|
|
42
52
|
];
|
|
43
53
|
|
|
44
|
-
|
|
54
|
+
// ── Agent meta-tools ────────────────────────────────────────────────────────
|
|
55
|
+
const AGENT_TOOLS = [
|
|
56
|
+
{ name: "spawn_agent", description: "Spawn a child agent to work on a subtask independently. Use for tasks that can be parallelized.", parameters: { type: "object", properties: { name: { type: "string", description: "Short name for this sub-agent" }, task: { type: "string", description: "The task description for the sub-agent" } }, required: ["name", "task"] } },
|
|
57
|
+
{ name: "get_changes", description: "View all file changes made this session with diffs", parameters: { type: "object", properties: {}, required: [] } },
|
|
58
|
+
{ name: "rollback_change", description: "Undo a specific file change by index", parameters: { type: "object", properties: { index: { type: "number", description: "Change index (from get_changes)" } }, required: ["index"] } },
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const CLOUD_TOOL_NAMES = new Set(CLOUD_TOOLS.map(t => t.name));
|
|
62
|
+
const AGENT_TOOL_NAMES = new Set(AGENT_TOOLS.map(t => t.name));
|
|
45
63
|
|
|
46
|
-
function buildTools() {
|
|
47
|
-
const localDefs = getToolDefinitions().map(
|
|
48
|
-
type: "function",
|
|
49
|
-
|
|
64
|
+
function buildTools(mcpTools = []) {
|
|
65
|
+
const localDefs = getToolDefinitions().map(t => ({
|
|
66
|
+
type: "function", function: { name: t.name, description: t.description, parameters: t.parameters },
|
|
67
|
+
}));
|
|
68
|
+
const cloudDefs = CLOUD_TOOLS.map(t => ({
|
|
69
|
+
type: "function", function: { name: t.name, description: t.description, parameters: t.parameters },
|
|
50
70
|
}));
|
|
51
|
-
const
|
|
52
|
-
type: "function",
|
|
53
|
-
function: { name: t.name, description: t.description, parameters: t.parameters },
|
|
71
|
+
const agentDefs = AGENT_TOOLS.map(t => ({
|
|
72
|
+
type: "function", function: { name: t.name, description: t.description, parameters: t.parameters },
|
|
54
73
|
}));
|
|
55
|
-
|
|
74
|
+
const mcpDefs = mcpTools.map(t => ({
|
|
75
|
+
type: "function", function: { name: t.name, description: t.description, parameters: t.parameters },
|
|
76
|
+
}));
|
|
77
|
+
return [...localDefs, ...cloudDefs, ...agentDefs, ...mcpDefs];
|
|
56
78
|
}
|
|
57
79
|
|
|
80
|
+
// ── Cloud tool executor ───────────────────────────────────────────────────────
|
|
58
81
|
async function executeCloudTool(name, args) {
|
|
59
82
|
try {
|
|
83
|
+
// Dynamic import to avoid circular dependency
|
|
84
|
+
const { apiClient } = await import("../lib/api-client.js");
|
|
60
85
|
switch (name) {
|
|
61
86
|
case "get_account_info": return await apiClient.getMe();
|
|
62
87
|
case "list_workspaces": return await apiClient.listWorkspaces();
|
|
@@ -67,10 +92,7 @@ async function executeCloudTool(name, args) {
|
|
|
67
92
|
case "add_memory": return await apiClient.addMemory(args.content, args.category);
|
|
68
93
|
case "clear_memory": return await apiClient.clearMemory();
|
|
69
94
|
case "get_usage": return await apiClient.getUsage();
|
|
70
|
-
case "get_config": {
|
|
71
|
-
const { getConfigValue } = await import("../lib/config-store.js");
|
|
72
|
-
return { key: args.key, value: getConfigValue(args.key) };
|
|
73
|
-
}
|
|
95
|
+
case "get_config": return { key: args.key, value: getConfigValue(args.key) };
|
|
74
96
|
case "set_config": {
|
|
75
97
|
const { setConfigValue } = await import("../lib/config-store.js");
|
|
76
98
|
setConfigValue(args.key, args.value);
|
|
@@ -83,28 +105,96 @@ async function executeCloudTool(name, args) {
|
|
|
83
105
|
}
|
|
84
106
|
}
|
|
85
107
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
108
|
+
// ── Agent meta-tool executor ──────────────────────────────────────────────────
|
|
109
|
+
async function executeAgentTool(name, args, orchestrator, allTools, parseSSE) {
|
|
110
|
+
switch (name) {
|
|
111
|
+
case "spawn_agent": {
|
|
112
|
+
if (!orchestrator) return { error: "Agent orchestration not available" };
|
|
113
|
+
orchestrator.createAgent(args.name, args.task);
|
|
114
|
+
const result = await orchestrator.runAgent(args.name, args.task, allTools, parseSSE);
|
|
115
|
+
return { agent: args.name, result: result.slice(0, 4000) };
|
|
116
|
+
}
|
|
117
|
+
case "get_changes": {
|
|
118
|
+
const changes = getSessionChanges();
|
|
119
|
+
if (changes.length === 0) return { changes: [], message: "No file changes this session" };
|
|
120
|
+
return { changes };
|
|
121
|
+
}
|
|
122
|
+
case "rollback_change": {
|
|
123
|
+
return rollbackChange(args.index);
|
|
124
|
+
}
|
|
125
|
+
default:
|
|
126
|
+
return { error: `Unknown agent tool: ${name}` };
|
|
127
|
+
}
|
|
95
128
|
}
|
|
96
129
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (
|
|
101
|
-
|
|
130
|
+
// ── Enhanced local tool executor with sandbox + diff tracking ─────────────────
|
|
131
|
+
async function executeLocalToolEnhanced(name, args) {
|
|
132
|
+
// Sandbox for run_command
|
|
133
|
+
if (name === "run_command") {
|
|
134
|
+
const sandboxLevel = getConfigValue("sandbox.level") || "basic";
|
|
135
|
+
const allowNetwork = getConfigValue("sandbox.allowNetwork") || false;
|
|
136
|
+
|
|
137
|
+
if (sandboxLevel !== "none") {
|
|
138
|
+
const result = await sandboxExec(args.command, {
|
|
139
|
+
cwd: args.cwd || process.cwd(),
|
|
140
|
+
timeout: args.timeout || 120000,
|
|
141
|
+
level: sandboxLevel === "strict" ? SandboxLevel.STRICT : SandboxLevel.BASIC,
|
|
142
|
+
allowNetwork,
|
|
143
|
+
});
|
|
144
|
+
return {
|
|
145
|
+
stdout: result.stdout,
|
|
146
|
+
stderr: result.stderr,
|
|
147
|
+
exitCode: result.exitCode,
|
|
148
|
+
...(result.timedOut ? { timedOut: true } : {}),
|
|
149
|
+
sandbox: getSandboxInfo(sandboxLevel),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Diff tracking for file modifications
|
|
155
|
+
if (name === "write_file" || name === "edit_file") {
|
|
156
|
+
const filePath = args.path || args.file_path;
|
|
157
|
+
const snapshot = snapshotFile(filePath);
|
|
158
|
+
const result = await executeLocalTool(name, args);
|
|
159
|
+
if (!result.error) {
|
|
160
|
+
const { readFileSync, existsSync } = await import("fs");
|
|
161
|
+
const absPath = filePath.startsWith("/") ? filePath : `${process.cwd()}/${filePath}`;
|
|
162
|
+
if (existsSync(absPath)) {
|
|
163
|
+
const newContent = readFileSync(absPath, "utf-8");
|
|
164
|
+
const change = recordChange(snapshot, newContent, name === "write_file" ? "write" : "edit");
|
|
165
|
+
result.diff = generateDiff(change).split("\n").slice(0, 20).join("\n");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return result;
|
|
102
169
|
}
|
|
103
170
|
|
|
104
|
-
|
|
171
|
+
return executeLocalTool(name, args);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ── Main tool execution with approval + hooks + permissions ───────────────────
|
|
175
|
+
let sessionAutoApprove = false;
|
|
176
|
+
|
|
177
|
+
async function executeTool(name, args, orchestrator, allTools, parseSSE, mcpManager) {
|
|
178
|
+
// Permission rules check
|
|
179
|
+
const permission = checkPermission(name, args);
|
|
180
|
+
if (permission === "deny") return { error: `Tool ${name} is denied by permission rules` };
|
|
181
|
+
|
|
182
|
+
// Pre-tool hooks
|
|
105
183
|
const hookResult = await runHooks("PreToolUse", { toolName: name, input: args });
|
|
106
|
-
if (!hookResult.allowed) {
|
|
107
|
-
|
|
184
|
+
if (!hookResult.allowed) return { error: hookResult.reason || "Blocked by hook" };
|
|
185
|
+
|
|
186
|
+
// MCP tools
|
|
187
|
+
if (mcpManager?.isMcpTool(name)) {
|
|
188
|
+
const result = await mcpManager.callTool(name, args);
|
|
189
|
+
await runHooks("PostToolUse", { toolName: name, input: args, result });
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Agent meta-tools
|
|
194
|
+
if (AGENT_TOOL_NAMES.has(name)) {
|
|
195
|
+
const result = await executeAgentTool(name, args, orchestrator, allTools, parseSSE);
|
|
196
|
+
await runHooks("PostToolUse", { toolName: name, input: args, result });
|
|
197
|
+
return result;
|
|
108
198
|
}
|
|
109
199
|
|
|
110
200
|
// Cloud tools
|
|
@@ -114,31 +204,42 @@ async function executeTool(name, args) {
|
|
|
114
204
|
return result;
|
|
115
205
|
}
|
|
116
206
|
|
|
117
|
-
// Local tools —
|
|
118
|
-
if (
|
|
119
|
-
const
|
|
120
|
-
if (
|
|
207
|
+
// Local tools — enhanced approval system
|
|
208
|
+
if (!sessionAutoApprove) {
|
|
209
|
+
const decision = shouldApprove(name, args);
|
|
210
|
+
if (decision === "deny") return { error: "Denied by approval policy" };
|
|
211
|
+
if (decision === "ask" || decision === "suggest") {
|
|
212
|
+
// Override with permission rules
|
|
213
|
+
if (permission === "allow") {
|
|
214
|
+
// Permission rules say allow, skip approval
|
|
215
|
+
} else {
|
|
216
|
+
const approved = await promptApproval(name, args, decision);
|
|
217
|
+
if (approved === "auto") {
|
|
218
|
+
sessionAutoApprove = true;
|
|
219
|
+
} else if (!approved) {
|
|
220
|
+
return { error: "User denied execution" };
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
121
224
|
}
|
|
122
225
|
|
|
123
|
-
const result = await
|
|
226
|
+
const result = await executeLocalToolEnhanced(name, args);
|
|
124
227
|
await runHooks("PostToolUse", { toolName: name, input: args, result });
|
|
125
228
|
return result;
|
|
126
229
|
}
|
|
127
230
|
|
|
231
|
+
// ── SSE Stream Parser ─────────────────────────────────────────────────────────
|
|
128
232
|
async function* parseSSEStream(response) {
|
|
129
233
|
const reader = response.body.getReader();
|
|
130
234
|
const decoder = new TextDecoder();
|
|
131
235
|
let buffer = "";
|
|
132
|
-
|
|
133
236
|
try {
|
|
134
237
|
while (true) {
|
|
135
238
|
const { done, value } = await reader.read();
|
|
136
239
|
if (done) break;
|
|
137
|
-
|
|
138
240
|
buffer += decoder.decode(value, { stream: true });
|
|
139
241
|
const lines = buffer.split("\n");
|
|
140
242
|
buffer = lines.pop() || "";
|
|
141
|
-
|
|
142
243
|
for (const line of lines) {
|
|
143
244
|
if (line.startsWith("data: ")) {
|
|
144
245
|
const data = line.slice(6).trim();
|
|
@@ -152,7 +253,7 @@ async function* parseSSEStream(response) {
|
|
|
152
253
|
}
|
|
153
254
|
}
|
|
154
255
|
|
|
155
|
-
// Active session state
|
|
256
|
+
// ── Active session state ──────────────────────────────────────────────────────
|
|
156
257
|
let currentSession = null;
|
|
157
258
|
|
|
158
259
|
export async function runAgent(userRequest, options = {}) {
|
|
@@ -163,17 +264,28 @@ export async function runAgent(userRequest, options = {}) {
|
|
|
163
264
|
const memoryContext = getMemoryContext();
|
|
164
265
|
const skillContext = getSkillContext(plugins.skills, userRequest);
|
|
165
266
|
|
|
166
|
-
//
|
|
267
|
+
// Initialize MCP servers
|
|
268
|
+
const mcpManager = new McpManager();
|
|
269
|
+
const mcpServers = getConfigValue("mcp.servers") || {};
|
|
270
|
+
for (const [name, config] of Object.entries(mcpServers)) {
|
|
271
|
+
if (config.command) await mcpManager.addServer(name, config);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Create provider
|
|
275
|
+
const providerName = getConfigValue("agent.provider") || getConfigValue("ai.provider") || "cheri";
|
|
276
|
+
const provider = createProvider(providerName);
|
|
277
|
+
const agentModel = getConfigValue("agent.model") || getConfigValue("ai.model") || "";
|
|
278
|
+
|
|
279
|
+
// Handle slash commands
|
|
167
280
|
if (userRequest.startsWith("/")) {
|
|
168
281
|
const cmdName = userRequest.slice(1).split(/\s+/)[0];
|
|
169
282
|
const cmdArgs = userRequest.slice(1 + cmdName.length).trim();
|
|
170
283
|
|
|
171
|
-
// Built-in slash commands
|
|
172
284
|
if (cmdName === "sessions" || cmdName === "history") {
|
|
173
285
|
const sessions = listSessions();
|
|
174
286
|
if (sessions.length === 0) { log.info("No saved sessions."); return; }
|
|
175
287
|
log.brand("Sessions");
|
|
176
|
-
sessions.slice(0, 10).forEach(
|
|
288
|
+
sessions.slice(0, 10).forEach(s => {
|
|
177
289
|
console.log(` ${chalk.dim(s.id.slice(0, 12))} ${s.title} ${chalk.dim(`(${s.messageCount} msgs)`)}`);
|
|
178
290
|
});
|
|
179
291
|
return;
|
|
@@ -181,7 +293,7 @@ export async function runAgent(userRequest, options = {}) {
|
|
|
181
293
|
if (cmdName === "resume") {
|
|
182
294
|
const sessions = listSessions();
|
|
183
295
|
if (sessions.length === 0) { log.info("No sessions to resume."); return; }
|
|
184
|
-
const target = cmdArgs ? sessions.find(
|
|
296
|
+
const target = cmdArgs ? sessions.find(s => s.id.includes(cmdArgs)) : sessions[0];
|
|
185
297
|
if (!target) { log.error("Session not found."); return; }
|
|
186
298
|
const data = loadSession(target.id);
|
|
187
299
|
if (data) {
|
|
@@ -202,9 +314,68 @@ export async function runAgent(userRequest, options = {}) {
|
|
|
202
314
|
}
|
|
203
315
|
if (cmdName === "new") {
|
|
204
316
|
currentSession = null;
|
|
317
|
+
clearSessionChanges();
|
|
318
|
+
sessionAutoApprove = false;
|
|
205
319
|
log.success("Started new session.");
|
|
206
320
|
return;
|
|
207
321
|
}
|
|
322
|
+
if (cmdName === "changes") {
|
|
323
|
+
const changes = getSessionChanges();
|
|
324
|
+
if (changes.length === 0) { log.info("No file changes this session."); return; }
|
|
325
|
+
log.brand("File Changes");
|
|
326
|
+
changes.forEach((c, i) => {
|
|
327
|
+
console.log(` ${chalk.dim(`[${i}]`)} ${chalk.cyan(c.operation)} ${c.path} ${chalk.dim(`(${c.linesAdded > 0 ? "+" : ""}${c.linesAdded} lines)`)}`);
|
|
328
|
+
});
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (cmdName === "rollback") {
|
|
332
|
+
if (cmdArgs === "all") {
|
|
333
|
+
const results = rollbackAll();
|
|
334
|
+
log.success(`Rolled back ${results.length} changes.`);
|
|
335
|
+
} else {
|
|
336
|
+
const idx = parseInt(cmdArgs);
|
|
337
|
+
if (isNaN(idx)) { log.error("Usage: /rollback <index> or /rollback all"); return; }
|
|
338
|
+
const result = rollbackChange(idx);
|
|
339
|
+
if (result.error) log.error(result.error);
|
|
340
|
+
else log.success(`Rolled back: ${result.path}`);
|
|
341
|
+
}
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (cmdName === "stats") {
|
|
345
|
+
if (!currentSession) { log.info("No active session."); return; }
|
|
346
|
+
const stats = getContextStats(currentSession.messages);
|
|
347
|
+
log.brand("Session Stats");
|
|
348
|
+
log.keyValue("Tokens", `~${Math.round(stats.tokens / 1000)}k`);
|
|
349
|
+
log.keyValue("Messages", stats.messageCount);
|
|
350
|
+
log.keyValue("User turns", stats.turns);
|
|
351
|
+
log.keyValue("Tool calls", stats.toolCalls);
|
|
352
|
+
log.keyValue("Provider", providerName);
|
|
353
|
+
log.keyValue("Model", agentModel || "(default)");
|
|
354
|
+
log.keyValue("Approval", getApprovalMode());
|
|
355
|
+
log.keyValue("Sandbox", getConfigValue("sandbox.level") || "basic");
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (cmdName === "mode") {
|
|
359
|
+
if (!cmdArgs) {
|
|
360
|
+
log.info(`Current approval mode: ${chalk.cyan(getApprovalMode())}`);
|
|
361
|
+
log.dim(" Modes: auto, suggest, ask, deny");
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const { setConfigValue } = await import("../lib/config-store.js");
|
|
365
|
+
setConfigValue("approval.mode", cmdArgs);
|
|
366
|
+
log.success(`Approval mode set to: ${cmdArgs}`);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (cmdName === "model") {
|
|
370
|
+
if (!cmdArgs) {
|
|
371
|
+
log.info(`Current model: ${chalk.cyan(agentModel || "(provider default)")}`);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const { setConfigValue } = await import("../lib/config-store.js");
|
|
375
|
+
setConfigValue("agent.model", cmdArgs);
|
|
376
|
+
log.success(`Agent model set to: ${cmdArgs}`);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
208
379
|
|
|
209
380
|
// Plugin slash commands
|
|
210
381
|
const cmd = getSlashCommand(plugins.commands, cmdName);
|
|
@@ -214,10 +385,8 @@ export async function runAgent(userRequest, options = {}) {
|
|
|
214
385
|
}
|
|
215
386
|
}
|
|
216
387
|
|
|
217
|
-
// Run SessionStart hooks
|
|
218
|
-
if (!currentSession) {
|
|
219
|
-
await runHooks("SessionStart", { cwd: process.cwd() });
|
|
220
|
-
}
|
|
388
|
+
// Run SessionStart hooks
|
|
389
|
+
if (!currentSession) await runHooks("SessionStart", { cwd: process.cwd() });
|
|
221
390
|
|
|
222
391
|
// Build or continue session
|
|
223
392
|
if (!currentSession) {
|
|
@@ -227,20 +396,15 @@ export async function runAgent(userRequest, options = {}) {
|
|
|
227
396
|
title: userRequest.slice(0, 60),
|
|
228
397
|
createdAt: Date.now(),
|
|
229
398
|
};
|
|
230
|
-
} else {
|
|
231
|
-
|
|
232
|
-
if (skillContext)
|
|
233
|
-
const sysMsg = currentSession.messages.find((m) => m.role === "system");
|
|
234
|
-
if (sysMsg && !sysMsg.content.includes(skillContext)) {
|
|
235
|
-
sysMsg.content += skillContext;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
399
|
+
} else if (skillContext) {
|
|
400
|
+
const sysMsg = currentSession.messages.find(m => m.role === "system");
|
|
401
|
+
if (sysMsg && !sysMsg.content.includes(skillContext)) sysMsg.content += skillContext;
|
|
238
402
|
}
|
|
239
403
|
|
|
240
404
|
currentSession.messages.push({ role: "user", content: userRequest });
|
|
241
405
|
currentSession.updatedAt = Date.now();
|
|
242
406
|
|
|
243
|
-
// Auto-compact if
|
|
407
|
+
// Auto-compact if needed
|
|
244
408
|
if (shouldCompact(currentSession.messages)) {
|
|
245
409
|
const { messages, compacted } = compactMessages(currentSession.messages);
|
|
246
410
|
if (compacted) {
|
|
@@ -249,11 +413,22 @@ export async function runAgent(userRequest, options = {}) {
|
|
|
249
413
|
}
|
|
250
414
|
}
|
|
251
415
|
|
|
252
|
-
|
|
253
|
-
const
|
|
416
|
+
// Build tools including MCP
|
|
417
|
+
const mcpTools = mcpManager.getAllToolDefinitions();
|
|
418
|
+
const ALL_TOOLS = buildTools(mcpTools);
|
|
419
|
+
|
|
420
|
+
// Create orchestrator for sub-agents
|
|
421
|
+
const orchestrator = new AgentOrchestrator(
|
|
422
|
+
(msgs, tools) => provider.chatStream(msgs, tools, { model: agentModel || undefined }),
|
|
423
|
+
(name, args) => executeTool(name, args, null, ALL_TOOLS, parseSSEStream, mcpManager)
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
const MAX_ITERATIONS = getConfigValue("agent.maxIterations") || 15;
|
|
254
427
|
|
|
255
428
|
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
|
256
|
-
const response = await
|
|
429
|
+
const response = await provider.chatStream(currentSession.messages, ALL_TOOLS, {
|
|
430
|
+
model: agentModel || undefined,
|
|
431
|
+
});
|
|
257
432
|
|
|
258
433
|
let fullText = "";
|
|
259
434
|
const toolCalls = {};
|
|
@@ -283,49 +458,83 @@ export async function runAgent(userRequest, options = {}) {
|
|
|
283
458
|
const toolCallList = Object.values(toolCalls);
|
|
284
459
|
|
|
285
460
|
if (toolCallList.length === 0) {
|
|
286
|
-
|
|
287
|
-
|
|
461
|
+
// Render markdown for final text output
|
|
462
|
+
if (fullText) {
|
|
463
|
+
process.stdout.write("\r\x1b[K"); // clear streaming line
|
|
464
|
+
console.log(renderMarkdown(fullText));
|
|
465
|
+
}
|
|
288
466
|
currentSession.messages.push({ role: "assistant", content: fullText });
|
|
289
467
|
saveSession(currentSession.id, currentSession);
|
|
468
|
+
mcpManager.disconnectAll();
|
|
290
469
|
return;
|
|
291
470
|
}
|
|
292
471
|
|
|
293
472
|
if (fullText) process.stdout.write("\n");
|
|
294
473
|
|
|
295
474
|
const assistantMsg = { role: "assistant", content: fullText || null };
|
|
296
|
-
assistantMsg.tool_calls = toolCallList.map(
|
|
475
|
+
assistantMsg.tool_calls = toolCallList.map(tc => ({
|
|
297
476
|
id: tc.id, type: "function", function: { name: tc.name, arguments: tc.arguments },
|
|
298
477
|
}));
|
|
299
478
|
currentSession.messages.push(assistantMsg);
|
|
300
479
|
|
|
480
|
+
// Execute tools — parallel for safe tools, sequential for others
|
|
481
|
+
const safeTools = [];
|
|
482
|
+
const unsafeTools = [];
|
|
483
|
+
|
|
301
484
|
for (const tc of toolCallList) {
|
|
302
485
|
let input = {};
|
|
303
486
|
try { input = JSON.parse(tc.arguments); } catch {}
|
|
487
|
+
const isSafe = tc.name === "read_file" || tc.name === "list_directory" ||
|
|
488
|
+
tc.name === "search_files" || tc.name === "search_content" ||
|
|
489
|
+
CLOUD_TOOL_NAMES.has(tc.name);
|
|
490
|
+
(isSafe ? safeTools : unsafeTools).push({ tc, input });
|
|
491
|
+
}
|
|
304
492
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
493
|
+
// Execute safe tools in parallel
|
|
494
|
+
const executeOne = async ({ tc, input }) => {
|
|
495
|
+
const isMcp = mcpManager.isMcpTool(tc.name);
|
|
496
|
+
const isLocal = !CLOUD_TOOL_NAMES.has(tc.name) && !AGENT_TOOL_NAMES.has(tc.name) && !isMcp;
|
|
497
|
+
const prefix = isMcp ? chalk.magenta("mcp") : isLocal ? chalk.magenta("local") : chalk.blue("cloud");
|
|
498
|
+
|
|
499
|
+
// Command safety label
|
|
500
|
+
let safetyStr = "";
|
|
501
|
+
if (tc.name === "run_command" && input.command) {
|
|
502
|
+
const safety = getSafetyLabel(input.command);
|
|
503
|
+
safetyStr = ` ${chalk[safety.color](`[${safety.label}]`)}`;
|
|
504
|
+
}
|
|
308
505
|
|
|
309
|
-
|
|
506
|
+
log.info(`${prefix} ${chalk.cyan(tc.name)}${safetyStr}${Object.keys(input).length ? chalk.dim(" " + truncate(JSON.stringify(input), 80)) : ""}`);
|
|
310
507
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
508
|
+
const result = await executeTool(tc.name, input, orchestrator, ALL_TOOLS, parseSSEStream, mcpManager);
|
|
509
|
+
|
|
510
|
+
if (result.error) log.error(result.error);
|
|
511
|
+
else log.success(tc.name);
|
|
512
|
+
|
|
513
|
+
return { id: tc.id, result };
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
// Parallel execution for safe tools
|
|
517
|
+
const safeResults = safeTools.length > 0 ? await Promise.all(safeTools.map(executeOne)) : [];
|
|
316
518
|
|
|
519
|
+
// Sequential execution for unsafe tools
|
|
520
|
+
const unsafeResults = [];
|
|
521
|
+
for (const item of unsafeTools) {
|
|
522
|
+
unsafeResults.push(await executeOne(item));
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Add all results to messages
|
|
526
|
+
for (const { id, result } of [...safeResults, ...unsafeResults]) {
|
|
317
527
|
const resultStr = JSON.stringify(result);
|
|
318
528
|
const truncatedResult = resultStr.length > 8000 ? resultStr.slice(0, 8000) + "...(truncated)" : resultStr;
|
|
319
|
-
|
|
320
|
-
currentSession.messages.push({ role: "tool", tool_call_id: tc.id, content: truncatedResult });
|
|
529
|
+
currentSession.messages.push({ role: "tool", tool_call_id: id, content: truncatedResult });
|
|
321
530
|
}
|
|
322
531
|
|
|
323
|
-
// Save session after each tool round
|
|
324
532
|
saveSession(currentSession.id, currentSession);
|
|
325
533
|
}
|
|
326
534
|
|
|
327
|
-
log.warn("Agent reached maximum iterations
|
|
535
|
+
log.warn("Agent reached maximum iterations. Stopping.");
|
|
328
536
|
await runHooks("Stop", { reason: "max_iterations" });
|
|
537
|
+
mcpManager.disconnectAll();
|
|
329
538
|
}
|
|
330
539
|
|
|
331
540
|
function truncate(str, max) {
|
|
@@ -337,9 +546,32 @@ export function registerAgentCommand(program) {
|
|
|
337
546
|
.command("agent")
|
|
338
547
|
.argument("<request...>")
|
|
339
548
|
.description("AI coding agent — natural language command interface")
|
|
340
|
-
.
|
|
549
|
+
.option("-p, --provider <name>", "AI provider (cheri, openai, anthropic, deepseek, ollama, ...)")
|
|
550
|
+
.option("-m, --model <id>", "Model ID to use")
|
|
551
|
+
.option("--auto", "Auto-approve all tool executions")
|
|
552
|
+
.option("--sandbox <level>", "Sandbox level: none, basic, strict")
|
|
553
|
+
.action(async (requestParts, opts) => {
|
|
341
554
|
const request = requestParts.join(" ");
|
|
342
|
-
try {
|
|
343
|
-
|
|
555
|
+
try {
|
|
556
|
+
if (opts.provider) {
|
|
557
|
+
const { setConfigValue } = await import("../lib/config-store.js");
|
|
558
|
+
setConfigValue("agent.provider", opts.provider);
|
|
559
|
+
}
|
|
560
|
+
if (opts.model) {
|
|
561
|
+
const { setConfigValue } = await import("../lib/config-store.js");
|
|
562
|
+
setConfigValue("agent.model", opts.model);
|
|
563
|
+
}
|
|
564
|
+
if (opts.auto) {
|
|
565
|
+
const { setConfigValue } = await import("../lib/config-store.js");
|
|
566
|
+
setConfigValue("approval.mode", "auto");
|
|
567
|
+
}
|
|
568
|
+
if (opts.sandbox) {
|
|
569
|
+
const { setConfigValue } = await import("../lib/config-store.js");
|
|
570
|
+
setConfigValue("sandbox.level", opts.sandbox);
|
|
571
|
+
}
|
|
572
|
+
await runAgent(request);
|
|
573
|
+
} catch (err) {
|
|
574
|
+
log.error(err.message);
|
|
575
|
+
}
|
|
344
576
|
});
|
|
345
577
|
}
|