@heysalad/cheri-cli 1.3.0 โ†’ 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,29 @@
2
2
 
3
3
  All notable changes to Cheri CLI will be documented in this file.
4
4
 
5
+ ## [1.3.1] - 2026-02-17
6
+
7
+ ### ๐Ÿ› Bug Fixes
8
+
9
+ #### Permission System
10
+ - **Fixed spinner blocking approval prompts**: Approval prompts now appear BEFORE spinners start, preventing terminal blocking
11
+ - **Added upfront permission request**: Tasks with write operations now ask for permissions at the start
12
+ - **Better user experience**: Users can approve entire session with [a]lways option
13
+
14
+ #### How It Works
15
+ 1. Agent analyzes task for write operations (build, create, modify, etc.)
16
+ 2. Shows upfront permission request: `[Y]es, [n]o, [a]lways`
17
+ 3. If approved, proceeds with task
18
+ 4. Individual risky operations still prompt for confirmation (unless 'always' was chosen)
19
+
20
+ #### Changes
21
+ - Moved approval logic before `ToolPanel` creation to avoid spinner blocking input
22
+ - Added `requestUpfrontPermissions()` function to analyze and request batch permissions
23
+ - Added `skipApproval` parameter to `executeTool()` to avoid double prompting
24
+ - Improved permission flow for better UX
25
+
26
+ ---
27
+
5
28
  ## [1.3.0] - 2026-02-17
6
29
 
7
30
  ### ๐ŸŽฏ Major Feature - Token-Tracked Sessions with Predictable Costs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heysalad/cheri-cli",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "Cheri CLI - AI-powered cloud IDE by HeySaladยฎ. With token-tracked sessions, predictable AI costs, and automatic workspace management.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -181,7 +181,7 @@ async function executeLocalToolEnhanced(name, args) {
181
181
  // โ”€โ”€ Main tool execution with approval + hooks + permissions โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
182
182
  let sessionAutoApprove = false;
183
183
 
184
- async function executeTool(name, args, orchestrator, allTools, parseSSE, mcpManager) {
184
+ async function executeTool(name, args, orchestrator, allTools, parseSSE, mcpManager, skipApproval = false) {
185
185
  // Permission rules check
186
186
  const permission = checkPermission(name, args);
187
187
  if (permission === "deny") return { error: `Tool ${name} is denied by permission rules` };
@@ -212,7 +212,9 @@ async function executeTool(name, args, orchestrator, allTools, parseSSE, mcpMana
212
212
  }
213
213
 
214
214
  // Local tools โ€” enhanced approval system
215
- if (!sessionAutoApprove) {
215
+ // Note: For tools executed via executeOne in agent loop, approval is handled
216
+ // BEFORE spinner creation to avoid blocking terminal. This is a fallback.
217
+ if (!sessionAutoApprove && !skipApproval) {
216
218
  const decision = shouldApprove(name, args);
217
219
  if (decision === "deny") return { error: "Denied by approval policy" };
218
220
  if (decision === "ask" || decision === "suggest") {
@@ -286,6 +288,51 @@ async function* parseSSEStream(response) {
286
288
  }
287
289
  }
288
290
 
291
+ // โ”€โ”€ Upfront permission request โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
292
+ async function requestUpfrontPermissions(userRequest) {
293
+ const approvalMode = getApprovalMode();
294
+
295
+ // Skip if in auto mode or already auto-approved this session
296
+ if (approvalMode === "auto" || sessionAutoApprove) {
297
+ return true;
298
+ }
299
+
300
+ // Check if request likely involves write operations
301
+ const writeKeywords = ['build', 'create', 'make', 'write', 'add', 'implement', 'generate', 'fix', 'update', 'modify', 'delete', 'install', 'setup'];
302
+ const hasWriteIntent = writeKeywords.some(kw => userRequest.toLowerCase().includes(kw));
303
+
304
+ if (!hasWriteIntent) {
305
+ return true; // No write operations expected, proceed
306
+ }
307
+
308
+ // Show upfront permission request
309
+ console.log("");
310
+ log.info("This task may require file modifications and command execution.");
311
+ console.log("");
312
+
313
+ return new Promise((resolve) => {
314
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
315
+ rl.question(
316
+ chalk.yellow(` Grant permissions for this session? ${chalk.dim("[Y]es, [n]o, [a]lways")} `),
317
+ (answer) => {
318
+ rl.close();
319
+ const a = answer.trim().toLowerCase();
320
+ if (a === "a" || a === "always") {
321
+ sessionAutoApprove = true;
322
+ log.success("Auto-approve enabled for this session");
323
+ resolve(true);
324
+ } else if (a === "n" || a === "no") {
325
+ log.warn("Permissions denied");
326
+ resolve(false);
327
+ } else {
328
+ log.success("Permissions granted - will ask before risky operations");
329
+ resolve(true);
330
+ }
331
+ }
332
+ );
333
+ });
334
+ }
335
+
289
336
  // โ”€โ”€ Active session state โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
290
337
  let currentSession = null;
291
338
 
@@ -297,6 +344,13 @@ export async function runAgent(userRequest, options = {}) {
297
344
  const memoryContext = getMemoryContext();
298
345
  const skillContext = getSkillContext(plugins.skills, userRequest);
299
346
 
347
+ // Request upfront permissions if needed
348
+ const hasPermission = await requestUpfrontPermissions(userRequest);
349
+ if (!hasPermission) {
350
+ log.error("Task cancelled - permissions denied");
351
+ return;
352
+ }
353
+
300
354
  // Create provider (defaults to Cheri cloud which uses AWS Bedrock)
301
355
  const providerName = getConfigValue("agent.provider") || getConfigValue("ai.provider") || "cheri";
302
356
  let provider;
@@ -567,7 +621,26 @@ export async function runAgent(userRequest, options = {}) {
567
621
  const isMcp = mcpManager.isMcpTool(tc.name);
568
622
  const isLocal = !CLOUD_TOOL_NAMES.has(tc.name) && !AGENT_TOOL_NAMES.has(tc.name) && !isMcp;
569
623
 
570
- // Create animated panel for this tool execution
624
+ // Check if approval is needed BEFORE creating spinner
625
+ let needsApproval = false;
626
+ if (isLocal && !sessionAutoApprove) {
627
+ const decision = shouldApprove(tc.name, input);
628
+ if (decision === "ask" || decision === "suggest") {
629
+ needsApproval = true;
630
+
631
+ // Get approval WITHOUT spinner running
632
+ const approved = await promptApproval(tc.name, input, decision);
633
+ if (approved === "auto") {
634
+ sessionAutoApprove = true;
635
+ } else if (!approved) {
636
+ return { id: tc.id, result: { error: "User denied execution" } };
637
+ }
638
+ } else if (decision === "deny") {
639
+ return { id: tc.id, result: { error: "Denied by approval policy" } };
640
+ }
641
+ }
642
+
643
+ // Create animated panel for this tool execution (AFTER approval)
571
644
  const panel = new ToolPanel(tc.name, input);
572
645
 
573
646
  // Command safety label for run_command
@@ -577,7 +650,8 @@ export async function runAgent(userRequest, options = {}) {
577
650
  panel.update(`running ${safetyLabel}`);
578
651
  }
579
652
 
580
- const result = await executeTool(tc.name, input, orchestrator, ALL_TOOLS, parseSSEStream, mcpManager);
653
+ // Skip approval in executeTool since we already handled it above
654
+ const result = await executeTool(tc.name, input, orchestrator, ALL_TOOLS, parseSSEStream, mcpManager, needsApproval);
581
655
 
582
656
  // Determine result message based on tool type
583
657
  let resultMsg = '';