@heysalad/cheri-cli 1.0.0 → 1.1.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/CHANGELOG.md +95 -0
- package/package.json +9 -3
- package/src/commands/agent.js +174 -98
- package/src/commands/login.js +30 -10
- package/src/commands/memory.js +2 -2
- package/src/commands/status.js +1 -1
- package/src/commands/usage.js +13 -9
- package/src/commands/workspace.js +1 -1
- package/src/lib/command-safety.js +7 -6
- package/src/lib/config-store.js +3 -13
- package/src/lib/diff-tracker.js +3 -4
- package/src/lib/logger.js +31 -12
- package/src/lib/markdown.js +39 -21
- package/src/lib/mcp/client.js +29 -10
- package/src/lib/providers/index.js +7 -2
- package/src/lib/sessions/index.js +1 -2
- package/src/lib/tools/file-tools.js +56 -23
- package/src/lib/tools/search-tools.js +1 -1
- package/src/lib/ui.js +554 -0
- package/src/repl.js +5 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to Cheri CLI will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [1.1.0] - 2026-02-17
|
|
6
|
+
|
|
7
|
+
### ✨ Added - Beautiful UI & Animations
|
|
8
|
+
|
|
9
|
+
#### Enhanced Visual Feedback
|
|
10
|
+
- **Thinking Animation**: Beautiful animated spinner while AI processes requests
|
|
11
|
+
- **Tool Execution Panels**: Rich, color-coded panels for each tool with:
|
|
12
|
+
- Tool-specific icons (📖 for reading, ✍️ for writing, ⚡ for executing)
|
|
13
|
+
- Real-time elapsed time tracking
|
|
14
|
+
- Success/failure animations with context
|
|
15
|
+
- Preview of results in success messages
|
|
16
|
+
|
|
17
|
+
#### New UI Components
|
|
18
|
+
- **Gradient Colors**: Beautiful gradient text for enhanced readability
|
|
19
|
+
- Cheri red (#FF6B6B), Purple (#A855F7), Blue (#3B82F6), Green (#10B981)
|
|
20
|
+
- Cyan (#06B6D4), Yellow (#F59E0B), Pink (#EC4899)
|
|
21
|
+
- **Custom Spinners**: Operation-specific animated spinners
|
|
22
|
+
- File operations: Reading, writing, editing, deleting
|
|
23
|
+
- Cloud operations: API calls, workspace management
|
|
24
|
+
- Search operations: File and content search
|
|
25
|
+
- Command execution: Shell command progress
|
|
26
|
+
- AI thinking: Multi-frame thinking animation
|
|
27
|
+
|
|
28
|
+
#### Enhanced Logging
|
|
29
|
+
- **Enhanced Icons**: Unicode icons for all status messages (✓ ✗ ⚠ ℹ 💡)
|
|
30
|
+
- **Box Drawing**: Beautiful borders with unicode box characters
|
|
31
|
+
- **Progress Bars**: Animated progress indicators with percentage
|
|
32
|
+
- **Stream Renderer**: Colored streaming text for AI responses
|
|
33
|
+
- **Tool Panels**: Live-updating panels for tool execution with timing
|
|
34
|
+
|
|
35
|
+
#### Developer Experience
|
|
36
|
+
- **Managed Spinners**: Automatic cleanup of all active spinners
|
|
37
|
+
- **Error Context**: Better error messages with truncated stack traces
|
|
38
|
+
- **Visual Separators**: Clear section dividers between operations
|
|
39
|
+
- **Status Messages**: Context-aware success/failure messages
|
|
40
|
+
|
|
41
|
+
### 🔧 Improved
|
|
42
|
+
|
|
43
|
+
#### Agent Command
|
|
44
|
+
- Streaming responses now display in gradient cyan for better readability
|
|
45
|
+
- Tool executions show in animated panels with real-time feedback
|
|
46
|
+
- Visual separator between AI response and tool execution
|
|
47
|
+
- Thinking spinner appears while waiting for AI response
|
|
48
|
+
- Better error display with color-coded severity
|
|
49
|
+
|
|
50
|
+
#### Logger
|
|
51
|
+
- Enhanced all log methods with gradient colors
|
|
52
|
+
- Added icons to all status messages
|
|
53
|
+
- Improved visual hierarchy with better spacing
|
|
54
|
+
- Enhanced banner with animated logo
|
|
55
|
+
|
|
56
|
+
### 📚 Files Added
|
|
57
|
+
- `src/lib/ui.js` - Complete UI system with animations and components
|
|
58
|
+
|
|
59
|
+
### 🐛 Bug Fixes
|
|
60
|
+
- Fixed missing newlines after tool execution
|
|
61
|
+
- Better handling of long command outputs in spinners
|
|
62
|
+
- Improved truncation of long file paths in status messages
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## [1.0.0] - 2026-02-16
|
|
67
|
+
|
|
68
|
+
### Initial Release
|
|
69
|
+
- AI-powered coding agent with tool execution
|
|
70
|
+
- Cloud workspace management
|
|
71
|
+
- Session persistence and resume
|
|
72
|
+
- MCP server integration
|
|
73
|
+
- Multi-agent orchestration
|
|
74
|
+
- Sandbox command execution
|
|
75
|
+
- File operations (read, write, edit)
|
|
76
|
+
- Search capabilities
|
|
77
|
+
- Configuration management
|
|
78
|
+
- Memory system
|
|
79
|
+
- Plugin/skill support
|
|
80
|
+
- Approval workflows
|
|
81
|
+
- Diff tracking and rollback
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Future Roadmap
|
|
86
|
+
|
|
87
|
+
### Planned Features
|
|
88
|
+
- Interactive mode with REPL
|
|
89
|
+
- Diff viewer with syntax highlighting
|
|
90
|
+
- File tree visualization
|
|
91
|
+
- Real-time collaboration indicators
|
|
92
|
+
- Custom theme support
|
|
93
|
+
- Notification sounds (optional)
|
|
94
|
+
- Performance metrics dashboard
|
|
95
|
+
- Export session transcripts with formatting
|
package/package.json
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heysalad/cheri-cli",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Cheri CLI - AI-powered cloud IDE by HeySalad. Like Claude Code, but for cloud workspaces.",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Cheri CLI - AI-powered cloud IDE by HeySalad. Like Claude Code, but for cloud workspaces. Now with beautiful animations and enhanced UI!",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"cheri": "./bin/cheri.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bin/",
|
|
11
|
-
"src
|
|
11
|
+
"src/**/*.js",
|
|
12
|
+
"!src/**/*.fixed.js",
|
|
13
|
+
"!src/**/*.backup.js",
|
|
14
|
+
"!src/**/*.test.js",
|
|
15
|
+
"!src/**/*.spec.js",
|
|
16
|
+
"CHANGELOG.md",
|
|
17
|
+
"README.md"
|
|
12
18
|
],
|
|
13
19
|
"scripts": {
|
|
14
20
|
"start": "node bin/cheri.js",
|
package/src/commands/agent.js
CHANGED
|
@@ -15,6 +15,7 @@ import { snapshotFile, recordChange, getSessionChanges, generateDiff, rollbackCh
|
|
|
15
15
|
import { shouldApprove, promptApproval, getApprovalMode } from "../lib/approval.js";
|
|
16
16
|
import { McpManager } from "../lib/mcp/client.js";
|
|
17
17
|
import { AgentOrchestrator } from "../lib/multi-agent.js";
|
|
18
|
+
import { ToolPanel, status, StreamRenderer, gradients, icons } from "../lib/ui.js";
|
|
18
19
|
import chalk from "chalk";
|
|
19
20
|
import readline from "readline";
|
|
20
21
|
|
|
@@ -229,27 +230,53 @@ async function executeTool(name, args, orchestrator, allTools, parseSSE, mcpMana
|
|
|
229
230
|
}
|
|
230
231
|
|
|
231
232
|
// ── SSE Stream Parser ─────────────────────────────────────────────────────────
|
|
233
|
+
const STREAM_TIMEOUT_MS = 120000; // 2 minutes with no data = timeout
|
|
234
|
+
|
|
232
235
|
async function* parseSSEStream(response) {
|
|
233
236
|
const reader = response.body.getReader();
|
|
234
237
|
const decoder = new TextDecoder();
|
|
235
238
|
let buffer = "";
|
|
239
|
+
let lastDataAt = Date.now();
|
|
240
|
+
|
|
236
241
|
try {
|
|
237
242
|
while (true) {
|
|
238
|
-
|
|
243
|
+
// Race between next chunk and timeout
|
|
244
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
245
|
+
const remaining = STREAM_TIMEOUT_MS - (Date.now() - lastDataAt);
|
|
246
|
+
if (remaining <= 0) reject(new Error("Stream timed out — no data received for 2 minutes"));
|
|
247
|
+
else setTimeout(() => reject(new Error("Stream timed out — no data received for 2 minutes")), remaining);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
let result;
|
|
251
|
+
try {
|
|
252
|
+
result = await Promise.race([reader.read(), timeoutPromise]);
|
|
253
|
+
} catch (err) {
|
|
254
|
+
log.warn(err.message);
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const { done, value } = result;
|
|
239
259
|
if (done) break;
|
|
260
|
+
|
|
261
|
+
lastDataAt = Date.now();
|
|
240
262
|
buffer += decoder.decode(value, { stream: true });
|
|
241
263
|
const lines = buffer.split("\n");
|
|
242
264
|
buffer = lines.pop() || "";
|
|
265
|
+
|
|
243
266
|
for (const line of lines) {
|
|
244
267
|
if (line.startsWith("data: ")) {
|
|
245
268
|
const data = line.slice(6).trim();
|
|
246
269
|
if (data === "[DONE]") return;
|
|
247
|
-
try {
|
|
270
|
+
try {
|
|
271
|
+
yield JSON.parse(data);
|
|
272
|
+
} catch (parseErr) {
|
|
273
|
+
log.dim(`SSE parse warning: ${parseErr.message}`);
|
|
274
|
+
}
|
|
248
275
|
}
|
|
249
276
|
}
|
|
250
277
|
}
|
|
251
278
|
} finally {
|
|
252
|
-
reader.releaseLock();
|
|
279
|
+
try { reader.releaseLock(); } catch {}
|
|
253
280
|
}
|
|
254
281
|
}
|
|
255
282
|
|
|
@@ -264,19 +291,12 @@ export async function runAgent(userRequest, options = {}) {
|
|
|
264
291
|
const memoryContext = getMemoryContext();
|
|
265
292
|
const skillContext = getSkillContext(plugins.skills, userRequest);
|
|
266
293
|
|
|
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
294
|
// Create provider
|
|
275
295
|
const providerName = getConfigValue("agent.provider") || getConfigValue("ai.provider") || "cheri";
|
|
276
296
|
const provider = createProvider(providerName);
|
|
277
297
|
const agentModel = getConfigValue("agent.model") || getConfigValue("ai.model") || "";
|
|
278
298
|
|
|
279
|
-
// Handle slash commands
|
|
299
|
+
// Handle slash commands (before MCP init to avoid leaking child processes)
|
|
280
300
|
if (userRequest.startsWith("/")) {
|
|
281
301
|
const cmdName = userRequest.slice(1).split(/\s+/)[0];
|
|
282
302
|
const cmdArgs = userRequest.slice(1 + cmdName.length).trim();
|
|
@@ -385,6 +405,13 @@ export async function runAgent(userRequest, options = {}) {
|
|
|
385
405
|
}
|
|
386
406
|
}
|
|
387
407
|
|
|
408
|
+
// Initialize MCP servers (after slash commands to avoid leaking child processes on early returns)
|
|
409
|
+
const mcpManager = new McpManager();
|
|
410
|
+
const mcpServers = getConfigValue("mcp.servers") || {};
|
|
411
|
+
for (const [name, config] of Object.entries(mcpServers)) {
|
|
412
|
+
if (config.command) await mcpManager.addServer(name, config);
|
|
413
|
+
}
|
|
414
|
+
|
|
388
415
|
// Run SessionStart hooks
|
|
389
416
|
if (!currentSession) await runHooks("SessionStart", { cwd: process.cwd() });
|
|
390
417
|
|
|
@@ -425,116 +452,165 @@ export async function runAgent(userRequest, options = {}) {
|
|
|
425
452
|
|
|
426
453
|
const MAX_ITERATIONS = getConfigValue("agent.maxIterations") || 15;
|
|
427
454
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
455
|
+
try {
|
|
456
|
+
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
|
457
|
+
// Show thinking animation while waiting for response
|
|
458
|
+
const thinkingSpinner = status.thinking('Analyzing request...');
|
|
459
|
+
thinkingSpinner.start();
|
|
432
460
|
|
|
433
|
-
|
|
434
|
-
|
|
461
|
+
const response = await provider.chatStream(currentSession.messages, ALL_TOOLS, {
|
|
462
|
+
model: agentModel || undefined,
|
|
463
|
+
});
|
|
435
464
|
|
|
436
|
-
|
|
437
|
-
const
|
|
438
|
-
|
|
465
|
+
let fullText = "";
|
|
466
|
+
const toolCalls = {};
|
|
467
|
+
let firstContent = false;
|
|
468
|
+
|
|
469
|
+
for await (const chunk of parseSSEStream(response)) {
|
|
470
|
+
const delta = chunk.choices?.[0]?.delta;
|
|
471
|
+
const finishReason = chunk.choices?.[0]?.finish_reason;
|
|
472
|
+
|
|
473
|
+
if (delta?.content) {
|
|
474
|
+
// Stop thinking spinner on first content
|
|
475
|
+
if (!firstContent) {
|
|
476
|
+
thinkingSpinner.stop();
|
|
477
|
+
firstContent = true;
|
|
478
|
+
}
|
|
479
|
+
// Stream with gradient color
|
|
480
|
+
process.stdout.write(gradients.cyan(delta.content));
|
|
481
|
+
fullText += delta.content;
|
|
482
|
+
}
|
|
439
483
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
484
|
+
if (delta?.tool_calls) {
|
|
485
|
+
// Stop spinner if we're getting tool calls
|
|
486
|
+
if (!firstContent) {
|
|
487
|
+
thinkingSpinner.stop();
|
|
488
|
+
firstContent = true;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
for (const tc of delta.tool_calls) {
|
|
492
|
+
const idx = tc.index;
|
|
493
|
+
if (!toolCalls[idx]) toolCalls[idx] = { id: tc.id || "", name: "", arguments: "" };
|
|
494
|
+
if (tc.id) toolCalls[idx].id = tc.id;
|
|
495
|
+
if (tc.function?.name) toolCalls[idx].name = tc.function.name;
|
|
496
|
+
if (tc.function?.arguments) toolCalls[idx].arguments += tc.function.arguments;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
444
499
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
if (!toolCalls[idx]) toolCalls[idx] = { id: tc.id || "", name: "", arguments: "" };
|
|
449
|
-
if (tc.id) toolCalls[idx].id = tc.id;
|
|
450
|
-
if (tc.function?.name) toolCalls[idx].name = tc.function.name;
|
|
451
|
-
if (tc.function?.arguments) toolCalls[idx].arguments += tc.function.arguments;
|
|
500
|
+
if (finishReason) {
|
|
501
|
+
if (!firstContent) thinkingSpinner.stop();
|
|
502
|
+
break;
|
|
452
503
|
}
|
|
453
504
|
}
|
|
454
505
|
|
|
455
|
-
|
|
456
|
-
}
|
|
506
|
+
const toolCallList = Object.values(toolCalls);
|
|
457
507
|
|
|
458
|
-
|
|
508
|
+
if (toolCallList.length === 0) {
|
|
509
|
+
// Text was already streamed to stdout, just add a newline
|
|
510
|
+
if (fullText) {
|
|
511
|
+
process.stdout.write("\n");
|
|
512
|
+
}
|
|
513
|
+
currentSession.messages.push({ role: "assistant", content: fullText });
|
|
514
|
+
saveSession(currentSession.id, currentSession);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
459
517
|
|
|
460
|
-
if (toolCallList.length === 0) {
|
|
461
|
-
// Render markdown for final text output
|
|
462
518
|
if (fullText) {
|
|
463
|
-
process.stdout.write("\
|
|
464
|
-
|
|
519
|
+
process.stdout.write("\n");
|
|
520
|
+
// Add visual separator after text response
|
|
521
|
+
if (toolCallList.length > 0) {
|
|
522
|
+
console.log(chalk.dim("─".repeat(60)));
|
|
523
|
+
}
|
|
465
524
|
}
|
|
466
|
-
currentSession.messages.push({ role: "assistant", content: fullText });
|
|
467
|
-
saveSession(currentSession.id, currentSession);
|
|
468
|
-
mcpManager.disconnectAll();
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
if (fullText) process.stdout.write("\n");
|
|
473
|
-
|
|
474
|
-
const assistantMsg = { role: "assistant", content: fullText || null };
|
|
475
|
-
assistantMsg.tool_calls = toolCallList.map(tc => ({
|
|
476
|
-
id: tc.id, type: "function", function: { name: tc.name, arguments: tc.arguments },
|
|
477
|
-
}));
|
|
478
|
-
currentSession.messages.push(assistantMsg);
|
|
479
|
-
|
|
480
|
-
// Execute tools — parallel for safe tools, sequential for others
|
|
481
|
-
const safeTools = [];
|
|
482
|
-
const unsafeTools = [];
|
|
483
|
-
|
|
484
|
-
for (const tc of toolCallList) {
|
|
485
|
-
let input = {};
|
|
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
|
-
}
|
|
492
525
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
//
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
526
|
+
const assistantMsg = { role: "assistant", content: fullText || null };
|
|
527
|
+
assistantMsg.tool_calls = toolCallList.map(tc => ({
|
|
528
|
+
id: tc.id, type: "function", function: { name: tc.name, arguments: tc.arguments },
|
|
529
|
+
}));
|
|
530
|
+
currentSession.messages.push(assistantMsg);
|
|
531
|
+
|
|
532
|
+
// Execute tools — parallel for safe tools, sequential for others
|
|
533
|
+
const safeTools = [];
|
|
534
|
+
const unsafeTools = [];
|
|
535
|
+
|
|
536
|
+
for (const tc of toolCallList) {
|
|
537
|
+
let input = {};
|
|
538
|
+
try { input = JSON.parse(tc.arguments); } catch {}
|
|
539
|
+
const isSafe = tc.name === "read_file" || tc.name === "list_directory" ||
|
|
540
|
+
tc.name === "search_files" || tc.name === "search_content" ||
|
|
541
|
+
CLOUD_TOOL_NAMES.has(tc.name);
|
|
542
|
+
(isSafe ? safeTools : unsafeTools).push({ tc, input });
|
|
504
543
|
}
|
|
505
544
|
|
|
506
|
-
|
|
545
|
+
// Execute safe tools in parallel
|
|
546
|
+
const executeOne = async ({ tc, input }) => {
|
|
547
|
+
const isMcp = mcpManager.isMcpTool(tc.name);
|
|
548
|
+
const isLocal = !CLOUD_TOOL_NAMES.has(tc.name) && !AGENT_TOOL_NAMES.has(tc.name) && !isMcp;
|
|
507
549
|
|
|
508
|
-
|
|
550
|
+
// Create animated panel for this tool execution
|
|
551
|
+
const panel = new ToolPanel(tc.name, input);
|
|
509
552
|
|
|
510
|
-
|
|
511
|
-
|
|
553
|
+
// Command safety label for run_command
|
|
554
|
+
if (tc.name === "run_command" && input.command) {
|
|
555
|
+
const safety = getSafetyLabel(input.command);
|
|
556
|
+
const safetyLabel = chalk[safety.color](`[${safety.label}]`);
|
|
557
|
+
panel.update(`running ${safetyLabel}`);
|
|
558
|
+
}
|
|
512
559
|
|
|
513
|
-
|
|
514
|
-
};
|
|
560
|
+
const result = await executeTool(tc.name, input, orchestrator, ALL_TOOLS, parseSSEStream, mcpManager);
|
|
515
561
|
|
|
516
|
-
|
|
517
|
-
|
|
562
|
+
// Determine result message based on tool type
|
|
563
|
+
let resultMsg = '';
|
|
564
|
+
if (result.stdout && tc.name === "run_command") {
|
|
565
|
+
const preview = result.stdout.split('\n')[0].slice(0, 50);
|
|
566
|
+
resultMsg = preview + (result.stdout.length > 50 ? '...' : '');
|
|
567
|
+
} else if (result.path || result.file_path) {
|
|
568
|
+
resultMsg = result.path || result.file_path;
|
|
569
|
+
}
|
|
518
570
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
571
|
+
if (result.error) {
|
|
572
|
+
panel.fail(result.error.slice(0, 80));
|
|
573
|
+
} else {
|
|
574
|
+
panel.succeed(resultMsg);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return { id: tc.id, result };
|
|
578
|
+
};
|
|
524
579
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
580
|
+
// Parallel execution for safe tools — use allSettled to avoid losing results on single failure
|
|
581
|
+
const safeResults = [];
|
|
582
|
+
if (safeTools.length > 0) {
|
|
583
|
+
const settled = await Promise.allSettled(safeTools.map(executeOne));
|
|
584
|
+
for (let j = 0; j < settled.length; j++) {
|
|
585
|
+
if (settled[j].status === "fulfilled") {
|
|
586
|
+
safeResults.push(settled[j].value);
|
|
587
|
+
} else {
|
|
588
|
+
safeResults.push({ id: safeTools[j].tc.id, result: { error: settled[j].reason?.message || "Tool execution failed" } });
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Sequential execution for unsafe tools
|
|
594
|
+
const unsafeResults = [];
|
|
595
|
+
for (const item of unsafeTools) {
|
|
596
|
+
unsafeResults.push(await executeOne(item));
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Add all results to messages
|
|
600
|
+
for (const { id, result } of [...safeResults, ...unsafeResults]) {
|
|
601
|
+
const resultStr = JSON.stringify(result);
|
|
602
|
+
const truncatedResult = resultStr.length > 8000 ? resultStr.slice(0, 8000) + "...(truncated)" : resultStr;
|
|
603
|
+
currentSession.messages.push({ role: "tool", tool_call_id: id, content: truncatedResult });
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
saveSession(currentSession.id, currentSession);
|
|
530
607
|
}
|
|
531
608
|
|
|
532
|
-
|
|
609
|
+
log.warn("Agent reached maximum iterations. Stopping.");
|
|
610
|
+
await runHooks("Stop", { reason: "max_iterations" });
|
|
611
|
+
} finally {
|
|
612
|
+
mcpManager.disconnectAll();
|
|
533
613
|
}
|
|
534
|
-
|
|
535
|
-
log.warn("Agent reached maximum iterations. Stopping.");
|
|
536
|
-
await runHooks("Stop", { reason: "max_iterations" });
|
|
537
|
-
mcpManager.disconnectAll();
|
|
538
614
|
}
|
|
539
615
|
|
|
540
616
|
function truncate(str, max) {
|
package/src/commands/login.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import
|
|
2
|
+
import readline from "readline";
|
|
3
3
|
import { setConfigValue, getConfigValue } from "../lib/config-store.js";
|
|
4
4
|
import { apiClient } from "../lib/api-client.js";
|
|
5
5
|
import { log } from "../lib/logger.js";
|
|
@@ -15,18 +15,38 @@ export async function loginFlow() {
|
|
|
15
15
|
log.blank();
|
|
16
16
|
console.log(` ${chalk.cyan.underline(`${apiUrl}/auth/github?source=cli`)}`);
|
|
17
17
|
log.blank();
|
|
18
|
-
log.info("Step 2: After GitHub login, your token will be shown
|
|
18
|
+
log.info("Step 2: After GitHub login, your token will be shown.");
|
|
19
19
|
log.info(" Copy it and paste it below.");
|
|
20
20
|
log.blank();
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
22
|
+
// Use raw readline to avoid inquirer's readline conflicts with REPL
|
|
23
|
+
const token = await new Promise((resolve) => {
|
|
24
|
+
const rl = readline.createInterface({
|
|
25
|
+
input: process.stdin,
|
|
26
|
+
output: process.stdout,
|
|
27
|
+
terminal: true,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Mask input for security
|
|
31
|
+
let input = "";
|
|
32
|
+
const originalWrite = process.stdout.write.bind(process.stdout);
|
|
33
|
+
|
|
34
|
+
rl.question(chalk.cyan(" Paste your API token: "), (answer) => {
|
|
35
|
+
rl.close();
|
|
36
|
+
resolve(answer);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Override _writeToOutput to mask characters
|
|
40
|
+
rl._writeToOutput = function (str) {
|
|
41
|
+
if (str.includes("\n") || str.includes("\r")) {
|
|
42
|
+
originalWrite.call(process.stdout, str);
|
|
43
|
+
} else if (str.length === 1 && str !== " " && !str.startsWith("\x1b")) {
|
|
44
|
+
originalWrite.call(process.stdout, "*");
|
|
45
|
+
} else {
|
|
46
|
+
originalWrite.call(process.stdout, str);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
});
|
|
30
50
|
|
|
31
51
|
if (!token || !token.trim()) {
|
|
32
52
|
throw new Error("No token provided.");
|
package/src/commands/memory.js
CHANGED
|
@@ -8,7 +8,7 @@ export async function showMemory(options = {}) {
|
|
|
8
8
|
const spinner = ora("Fetching memories...").start();
|
|
9
9
|
|
|
10
10
|
try {
|
|
11
|
-
const { memories } = await apiClient.getMemory();
|
|
11
|
+
const { memories = [] } = await apiClient.getMemory();
|
|
12
12
|
spinner.stop();
|
|
13
13
|
|
|
14
14
|
log.blank();
|
|
@@ -66,7 +66,7 @@ export async function exportMemory(options = {}) {
|
|
|
66
66
|
const spinner = ora("Exporting memories...").start();
|
|
67
67
|
|
|
68
68
|
try {
|
|
69
|
-
const { memories } = await apiClient.getMemory();
|
|
69
|
+
const { memories = [] } = await apiClient.getMemory();
|
|
70
70
|
|
|
71
71
|
const exportData = {
|
|
72
72
|
version: "0.1.0",
|
package/src/commands/status.js
CHANGED
|
@@ -27,7 +27,7 @@ export async function showStatus() {
|
|
|
27
27
|
// Workspaces
|
|
28
28
|
log.header("Workspaces");
|
|
29
29
|
try {
|
|
30
|
-
const { workspaces } = await apiClient.listWorkspaces();
|
|
30
|
+
const { workspaces = [] } = await apiClient.listWorkspaces();
|
|
31
31
|
if (workspaces.length === 0) {
|
|
32
32
|
log.keyValue("Count", "0");
|
|
33
33
|
log.dim(` Run ${chalk.cyan("cheri workspace launch owner/repo")} to create one.`);
|
package/src/commands/usage.js
CHANGED
|
@@ -14,19 +14,22 @@ export async function showUsage() {
|
|
|
14
14
|
spinner.stop();
|
|
15
15
|
|
|
16
16
|
// Rate limit
|
|
17
|
+
const rateLimit = data.rateLimit || {};
|
|
17
18
|
log.header("Rate Limit");
|
|
18
19
|
log.keyValue("Plan", data.plan === "pro" ? chalk.green("Pro") : "Free");
|
|
19
|
-
log.keyValue("Limit", `${
|
|
20
|
-
const remaining =
|
|
21
|
-
const limit =
|
|
20
|
+
log.keyValue("Limit", `${rateLimit.limit ?? "N/A"} requests/hour`);
|
|
21
|
+
const remaining = rateLimit.remaining ?? 0;
|
|
22
|
+
const limit = rateLimit.limit ?? 1;
|
|
22
23
|
const remainColor = remaining > limit * 0.5 ? chalk.green : remaining > limit * 0.1 ? chalk.yellow : chalk.red;
|
|
23
24
|
log.keyValue("Remaining", remainColor(`${remaining}`));
|
|
24
|
-
log.keyValue("Resets at",
|
|
25
|
+
log.keyValue("Resets at", rateLimit.resetsAt ?? "N/A");
|
|
25
26
|
|
|
26
27
|
// Today's usage
|
|
28
|
+
const usage = data.usage || {};
|
|
29
|
+
const today = usage.today || {};
|
|
27
30
|
log.header("Today");
|
|
28
|
-
log.keyValue("Requests", `${
|
|
29
|
-
const endpoints =
|
|
31
|
+
log.keyValue("Requests", `${today.requests ?? 0}`);
|
|
32
|
+
const endpoints = today.endpoints || {};
|
|
30
33
|
if (Object.keys(endpoints).length > 0) {
|
|
31
34
|
for (const [ep, count] of Object.entries(endpoints)) {
|
|
32
35
|
console.log(` ${chalk.dim(ep)} ${chalk.cyan(count)}`);
|
|
@@ -34,10 +37,11 @@ export async function showUsage() {
|
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
// Summary
|
|
40
|
+
const summary = data.summary || {};
|
|
37
41
|
log.header("Summary");
|
|
38
|
-
log.keyValue("Last 7 days", `${
|
|
39
|
-
log.keyValue("Last 30 days", `${
|
|
40
|
-
log.keyValue("All time", `${
|
|
42
|
+
log.keyValue("Last 7 days", `${usage.last7d?.requests ?? 0} requests`);
|
|
43
|
+
log.keyValue("Last 30 days", `${usage.last30d?.requests ?? 0} requests`);
|
|
44
|
+
log.keyValue("All time", `${summary.totalRequests ?? 0} requests`);
|
|
41
45
|
if (data.summary.memberSince) {
|
|
42
46
|
log.keyValue("Member since", new Date(data.summary.memberSince).toLocaleDateString());
|
|
43
47
|
}
|
|
@@ -70,7 +70,7 @@ export async function listWorkspaces() {
|
|
|
70
70
|
const statusColor =
|
|
71
71
|
ws.status === "running" ? chalk.green : chalk.dim;
|
|
72
72
|
console.log(
|
|
73
|
-
` ${ws.id.padEnd(24)} ${(ws.repo || "").padEnd(24)} ${statusIcon} ${statusColor(ws.status.padEnd(9))} ${chalk.cyan(ws.url || "")}`
|
|
73
|
+
` ${ws.id.padEnd(24)} ${(ws.repo || "").padEnd(24)} ${statusIcon} ${statusColor((ws.status || "unknown").padEnd(9))} ${chalk.cyan(ws.url || "")}`
|
|
74
74
|
);
|
|
75
75
|
});
|
|
76
76
|
|
|
@@ -135,7 +135,13 @@ const MODERATE_PATTERNS = [
|
|
|
135
135
|
export function classifyCommand(command) {
|
|
136
136
|
const trimmed = command.trim();
|
|
137
137
|
|
|
138
|
-
// Check
|
|
138
|
+
// Check dangerous patterns FIRST (before safe commands, since safe commands
|
|
139
|
+
// like "cat", "echo", "sed" can be dangerous with certain arguments)
|
|
140
|
+
for (const pattern of DANGEROUS_PATTERNS) {
|
|
141
|
+
if (pattern.test(trimmed)) return "dangerous";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Check exact safe commands
|
|
139
145
|
if (SAFE_COMMANDS.has(trimmed)) return "safe";
|
|
140
146
|
|
|
141
147
|
// Check if it starts with a safe command (with arguments)
|
|
@@ -143,11 +149,6 @@ export function classifyCommand(command) {
|
|
|
143
149
|
if (trimmed.startsWith(safe + " ") || trimmed === safe) return "safe";
|
|
144
150
|
}
|
|
145
151
|
|
|
146
|
-
// Check dangerous patterns
|
|
147
|
-
for (const pattern of DANGEROUS_PATTERNS) {
|
|
148
|
-
if (pattern.test(trimmed)) return "dangerous";
|
|
149
|
-
}
|
|
150
|
-
|
|
151
152
|
// Check moderate patterns
|
|
152
153
|
for (const pattern of MODERATE_PATTERNS) {
|
|
153
154
|
if (pattern.test(trimmed)) return "moderate";
|
package/src/lib/config-store.js
CHANGED
|
@@ -149,20 +149,10 @@ export function getConfigValue(key) {
|
|
|
149
149
|
*/
|
|
150
150
|
export function setConfigValue(key, value) {
|
|
151
151
|
ensureConfigDir();
|
|
152
|
-
// Only load user config (not merged) to avoid persisting
|
|
152
|
+
// Only load user config (not merged) to avoid persisting defaults
|
|
153
153
|
const userConfig = loadJsonFile(CONFIG_FILE);
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const keys = key.split(".");
|
|
157
|
-
let current = merged;
|
|
158
|
-
for (let i = 0; i < keys.length - 1; i++) {
|
|
159
|
-
if (!current[keys[i]] || typeof current[keys[i]] !== "object") {
|
|
160
|
-
current[keys[i]] = {};
|
|
161
|
-
}
|
|
162
|
-
current = current[keys[i]];
|
|
163
|
-
}
|
|
164
|
-
current[keys[keys.length - 1]] = value;
|
|
165
|
-
setConfig(merged);
|
|
154
|
+
setNestedValue(userConfig, key, value);
|
|
155
|
+
setConfig(userConfig);
|
|
166
156
|
}
|
|
167
157
|
|
|
168
158
|
/**
|