@nomad-e/bluma-cli 0.0.106 → 0.0.107

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.
@@ -1,40 +1,5 @@
1
1
  {
2
2
  "nativeTools": [
3
- {
4
- "type": "function",
5
- "function": {
6
- "name": "shell_command",
7
- "description": "Executes terminal commands in a universal and robust way. Automatically detects the appropriate shell (bash/sh on Linux/macOS, cmd/PowerShell on Windows). Handles timeouts gracefully and captures all output. Use for: installing packages (npm/pip/cargo), running tests, building projects, git operations, and any shell commands.",
8
- "parameters": {
9
- "type": "object",
10
- "properties": {
11
- "command": {
12
- "type": "string",
13
- "description": "The shell command to execute. Examples: 'npm install express', 'git status', 'pytest tests/', 'cargo build --release'. The command will be executed in the appropriate shell for the OS."
14
- },
15
- "timeout": {
16
- "type": "integer",
17
- "description": "Maximum execution time in seconds. Default is 300 (5 minutes). Increase for long-running commands like large builds or npm installs. The process will be terminated gracefully if timeout is exceeded.",
18
- "default": 300,
19
- "minimum": 1,
20
- "maximum": 3600
21
- },
22
- "cwd": {
23
- "type": "string",
24
- "description": "Working directory for command execution. Must be an absolute path. Defaults to current working directory if not specified. Use this to execute commands in specific project folders."
25
- },
26
- "verbose": {
27
- "type": "boolean",
28
- "description": "If true, returns detailed execution report including platform info, duration, and full output. If false, returns concise output with just status, exit code, and stdout/stderr. Use verbose=true for debugging command issues.",
29
- "default": false
30
- }
31
- },
32
- "required": [
33
- "command"
34
- ]
35
- }
36
- }
37
- },
38
3
  {
39
4
  "type": "function",
40
5
  "function": {
@@ -348,22 +313,22 @@
348
313
  {
349
314
  "type": "function",
350
315
  "function": {
351
- "name": "run_command_async",
352
- "description": "Start a command in background and get a command_id to track it. Use this for long-running commands like npm install, builds, or tests. The command runs asynchronously and you can check its status with command_status.",
316
+ "name": "shell_command",
317
+ "description": "Execute a shell command. The command runs in background and returns a command_id. Use command_status to check progress and get output. Security: blocks sudo, rm -rf, fork bombs, and other dangerous commands. Output is limited to 30KB/200 lines to avoid context overflow.",
353
318
  "parameters": {
354
319
  "type": "object",
355
320
  "properties": {
356
321
  "command": {
357
322
  "type": "string",
358
- "description": "The shell command to execute in background."
323
+ "description": "The shell command to execute. Examples: 'npm install', 'git status', 'docker build', 'pytest'. Cross-platform (bash/sh on Unix, cmd on Windows)."
359
324
  },
360
325
  "cwd": {
361
326
  "type": "string",
362
- "description": "Working directory for command execution. Defaults to current directory."
327
+ "description": "Working directory. Defaults to current directory."
363
328
  },
364
329
  "timeout": {
365
330
  "type": "integer",
366
- "description": "Timeout in seconds. 0 means no timeout. Default is 0.",
331
+ "description": "Timeout in seconds. 0 = no timeout. Default is 0.",
367
332
  "default": 0
368
333
  }
369
334
  },
@@ -377,13 +342,13 @@
377
342
  "type": "function",
378
343
  "function": {
379
344
  "name": "command_status",
380
- "description": "Check the status of a background command started with run_command_async. Returns current status, output, and exit code if completed.",
345
+ "description": "Check the status of a command started with shell_command. Returns status, output, and exit code.",
381
346
  "parameters": {
382
347
  "type": "object",
383
348
  "properties": {
384
349
  "command_id": {
385
350
  "type": "string",
386
- "description": "The command ID returned by run_command_async."
351
+ "description": "The command ID returned by shell_command."
387
352
  },
388
353
  "wait_seconds": {
389
354
  "type": "integer",
package/dist/main.js CHANGED
@@ -1316,255 +1316,16 @@ var ConfirmationPrompt = memo4(ConfirmationPromptComponent);
1316
1316
  import OpenAI from "openai";
1317
1317
  import * as dotenv from "dotenv";
1318
1318
  import path15 from "path";
1319
- import os9 from "os";
1319
+ import os8 from "os";
1320
1320
 
1321
1321
  // src/app/agent/tool_invoker.ts
1322
1322
  import { promises as fs8 } from "fs";
1323
1323
  import path10 from "path";
1324
1324
  import { fileURLToPath } from "url";
1325
1325
 
1326
- // src/app/agent/tools/natives/shell_command.ts
1327
- import os from "os";
1328
- import { spawn } from "child_process";
1329
- var MAX_OUTPUT_SIZE = 1e5;
1330
- var OUTPUT_TRUNCATION_MESSAGE = "\n\n[OUTPUT TRUNCATED - exceeded 100KB limit. Use pagination or redirect to file for full output.]";
1331
- var DANGEROUS_PATTERNS = [
1332
- // === ELEVAÇÃO DE PRIVILÉGIOS ===
1333
- /^sudo\s+/i,
1334
- // sudo commands
1335
- /^doas\s+/i,
1336
- // doas (BSD sudo alternative)
1337
- /^su\s+/i,
1338
- // switch user
1339
- /^pkexec\s+/i,
1340
- // PolicyKit execution
1341
- /\|\s*sudo\s+/i,
1342
- // piped to sudo
1343
- /;\s*sudo\s+/i,
1344
- // chained with sudo
1345
- /&&\s*sudo\s+/i,
1346
- // AND chained with sudo
1347
- // === COMANDOS DESTRUTIVOS ===
1348
- /\brm\s+(-[rf]+\s+)*[\/~]/i,
1349
- // rm com paths perigosos (/, ~)
1350
- /\brm\s+-[rf]*\s+\*/i,
1351
- // rm -rf *
1352
- /\bchmod\s+(777|666)\s+\//i,
1353
- // chmod 777/666 em paths root
1354
- /\bchown\s+.*\s+\//i,
1355
- // chown em paths root
1356
- /\bdd\s+.*of=\/dev\/(sd|hd|nvme)/i,
1357
- // dd para discos
1358
- /\bmkfs\./i,
1359
- // format filesystem
1360
- />\s*\/dev\/(sd|hd|nvme)/i,
1361
- // redirect para disco
1362
- /\bshred\s+/i,
1363
- // secure delete
1364
- // === FORK BOMB / RESOURCE EXHAUSTION ===
1365
- /:\(\)\s*\{\s*:\|:&\s*\}\s*;:/,
1366
- // fork bomb clássico
1367
- /\bwhile\s+true\s*;\s*do/i,
1368
- // infinite loop
1369
- /\byes\s+\|/i,
1370
- // yes piped (resource exhaustion)
1371
- // === NETWORK PERIGOSO ===
1372
- /\bcurl\s+.*\|\s*(ba)?sh/i,
1373
- // curl | bash (remote code exec)
1374
- /\bwget\s+.*\|\s*(ba)?sh/i,
1375
- // wget | bash
1376
- /\bnc\s+-[el]/i
1377
- // netcat listener (backdoor)
1378
- ];
1379
- var INTERACTIVE_PATTERNS = [
1380
- /^(vim|vi|nano|emacs|pico)\s*/i,
1381
- // editors
1382
- /^(less|more|most)\s*/i,
1383
- // pagers
1384
- /^(top|htop|btop|atop|nmon)\s*/i,
1385
- // monitoring tools
1386
- /^(ssh|telnet|ftp|sftp)\s+/i,
1387
- // remote connections
1388
- /^(mysql|psql|redis-cli|mongo|mongosh)\s*$/i,
1389
- // database CLIs (sem script)
1390
- /^(python|python3|node|ruby|irb|lua)\s*$/i,
1391
- // REPLs sem script
1392
- /^(gdb|lldb)\s*/i,
1393
- // debuggers
1394
- /^(bc|dc)\s*$/i
1395
- // calculators
1396
- ];
1397
- function shellCommand(args) {
1398
- const {
1399
- command,
1400
- timeout = 300,
1401
- // 5 minutos por padrão
1402
- cwd = process.cwd(),
1403
- verbose = false
1404
- } = args;
1405
- return new Promise((resolve) => {
1406
- const startTime = Date.now();
1407
- const platform = os.platform();
1408
- const commandTrimmed = command.trim();
1409
- for (const pattern of DANGEROUS_PATTERNS) {
1410
- if (pattern.test(commandTrimmed)) {
1411
- const result = {
1412
- status: "blocked",
1413
- exitCode: null,
1414
- stdout: "",
1415
- stderr: `Command blocked: "${commandTrimmed}" requires elevated privileges (sudo/doas). These commands require interactive password input which cannot be handled. Alternatives:
1416
- 1. Run the command manually in terminal
1417
- 2. Configure passwordless sudo for this specific command
1418
- 3. Run BluMa as root (not recommended)`,
1419
- command,
1420
- cwd,
1421
- platform,
1422
- duration: Date.now() - startTime,
1423
- blockedReason: "requires_sudo"
1424
- };
1425
- return resolve(formatResult(result, verbose));
1426
- }
1427
- }
1428
- for (const pattern of INTERACTIVE_PATTERNS) {
1429
- if (pattern.test(commandTrimmed)) {
1430
- const result = {
1431
- status: "blocked",
1432
- exitCode: null,
1433
- stdout: "",
1434
- stderr: `Command blocked: "${commandTrimmed}" is an interactive command. Interactive commands require user input and cannot be handled by the agent. Alternatives:
1435
- 1. Use non-interactive alternatives (e.g., 'cat' instead of 'less')
1436
- 2. Run the command manually in terminal
1437
- 3. For editors, use edit_tool instead`,
1438
- command,
1439
- cwd,
1440
- platform,
1441
- duration: Date.now() - startTime,
1442
- blockedReason: "interactive_command"
1443
- };
1444
- return resolve(formatResult(result, verbose));
1445
- }
1446
- }
1447
- let shellCmd;
1448
- let shellArgs;
1449
- if (platform === "win32") {
1450
- shellCmd = process.env.COMSPEC || "cmd.exe";
1451
- shellArgs = ["/c", command];
1452
- } else {
1453
- shellCmd = process.env.SHELL || "/bin/bash";
1454
- shellArgs = ["-c", command];
1455
- }
1456
- let stdout = "";
1457
- let stderr = "";
1458
- let stdoutTruncated = false;
1459
- let stderrTruncated = false;
1460
- let timedOut = false;
1461
- let finished = false;
1462
- const childProcess = spawn(shellCmd, shellArgs, {
1463
- cwd,
1464
- env: process.env,
1465
- // Importante: no Windows, precisamos do shell, mas spawn já lida com isso
1466
- windowsHide: true
1467
- });
1468
- const timeoutId = setTimeout(() => {
1469
- if (!finished) {
1470
- timedOut = true;
1471
- childProcess.kill("SIGTERM");
1472
- setTimeout(() => {
1473
- if (!finished) {
1474
- childProcess.kill("SIGKILL");
1475
- }
1476
- }, 2e3);
1477
- }
1478
- }, timeout * 1e3);
1479
- if (childProcess.stdout) {
1480
- childProcess.stdout.on("data", (data) => {
1481
- if (!stdoutTruncated) {
1482
- stdout += data.toString();
1483
- if (stdout.length > MAX_OUTPUT_SIZE) {
1484
- stdout = stdout.substring(0, MAX_OUTPUT_SIZE) + OUTPUT_TRUNCATION_MESSAGE;
1485
- stdoutTruncated = true;
1486
- }
1487
- }
1488
- });
1489
- }
1490
- if (childProcess.stderr) {
1491
- childProcess.stderr.on("data", (data) => {
1492
- if (!stderrTruncated) {
1493
- stderr += data.toString();
1494
- if (stderr.length > MAX_OUTPUT_SIZE) {
1495
- stderr = stderr.substring(0, MAX_OUTPUT_SIZE) + OUTPUT_TRUNCATION_MESSAGE;
1496
- stderrTruncated = true;
1497
- }
1498
- }
1499
- });
1500
- }
1501
- childProcess.on("error", (error) => {
1502
- if (!finished) {
1503
- finished = true;
1504
- clearTimeout(timeoutId);
1505
- const result = {
1506
- status: "error",
1507
- exitCode: null,
1508
- stdout: stdout.trim(),
1509
- stderr: `Failed to execute command: ${error.message}`,
1510
- command,
1511
- cwd,
1512
- platform,
1513
- duration: Date.now() - startTime
1514
- };
1515
- resolve(formatResult(result, verbose));
1516
- }
1517
- });
1518
- childProcess.on("close", (code, signal) => {
1519
- if (!finished) {
1520
- finished = true;
1521
- clearTimeout(timeoutId);
1522
- const result = {
1523
- status: timedOut ? "timeout" : code === 0 ? "success" : "error",
1524
- exitCode: code,
1525
- stdout: stdout.trim(),
1526
- stderr: timedOut ? `Command timed out after ${timeout} seconds
1527
- ${stderr.trim()}` : stderr.trim(),
1528
- command,
1529
- cwd,
1530
- platform,
1531
- duration: Date.now() - startTime,
1532
- truncated: stdoutTruncated || stderrTruncated
1533
- };
1534
- resolve(formatResult(result, verbose));
1535
- }
1536
- });
1537
- });
1538
- }
1539
- function formatResult(result, verbose) {
1540
- if (verbose) {
1541
- return JSON.stringify(result, null, 2);
1542
- }
1543
- const output = {
1544
- status: result.status,
1545
- exitCode: result.exitCode
1546
- };
1547
- if (result.stdout) {
1548
- output.stdout = result.stdout;
1549
- }
1550
- if (result.stderr) {
1551
- output.stderr = result.stderr;
1552
- }
1553
- if (result.status === "timeout") {
1554
- output.message = `Command exceeded timeout of ${result.duration / 1e3}s`;
1555
- }
1556
- if (result.status === "blocked" && result.blockedReason) {
1557
- output.blockedReason = result.blockedReason;
1558
- }
1559
- if (result.truncated) {
1560
- output.warning = "Output was truncated due to size limits (100KB max per stream)";
1561
- }
1562
- return JSON.stringify(output, null, 2);
1563
- }
1564
-
1565
1326
  // src/app/agent/tools/natives/edit.ts
1566
1327
  import path3 from "path";
1567
- import os2 from "os";
1328
+ import os from "os";
1568
1329
  import { promises as fs2 } from "fs";
1569
1330
  import { diffLines } from "diff";
1570
1331
  var MAX_DIFF_SIZE = 5e4;
@@ -1572,7 +1333,7 @@ var MAX_FILE_SIZE = 10 * 1024 * 1024;
1572
1333
  function normalizePath(filePath) {
1573
1334
  try {
1574
1335
  filePath = filePath.trim();
1575
- if (os2.platform() === "win32") {
1336
+ if (os.platform() === "win32") {
1576
1337
  const winDriveRegex = /^\/([a-zA-Z])[:/]/;
1577
1338
  const match = filePath.match(winDriveRegex);
1578
1339
  if (match) {
@@ -2993,23 +2754,42 @@ async function viewFileOutline(args) {
2993
2754
  }
2994
2755
 
2995
2756
  // src/app/agent/tools/natives/async_command.ts
2996
- import os3 from "os";
2997
- import { spawn as spawn2 } from "child_process";
2757
+ import os2 from "os";
2758
+ import { spawn } from "child_process";
2998
2759
  import { v4 as uuidv42 } from "uuid";
2999
2760
  var runningCommands = /* @__PURE__ */ new Map();
3000
- var MAX_OUTPUT_SIZE2 = 1e5;
2761
+ var MAX_OUTPUT_SIZE = 3e4;
3001
2762
  var MAX_STORED_COMMANDS = 50;
3002
- var OUTPUT_TRUNCATION_MSG = "\n[OUTPUT TRUNCATED]";
3003
- var DANGEROUS_PATTERNS2 = [
2763
+ var OUTPUT_TRUNCATION_MSG = "\n[OUTPUT TRUNCATED - 30KB/200 lines max]";
2764
+ var DANGEROUS_PATTERNS = [
2765
+ // Elevação de privilégios
3004
2766
  /^sudo\s+/i,
3005
2767
  /^doas\s+/i,
3006
2768
  /^su\s+/i,
3007
- /\|\s*sudo\s+/i
2769
+ /^pkexec\s+/i,
2770
+ /\|\s*sudo\s+/i,
2771
+ /;\s*sudo\s+/i,
2772
+ /&&\s*sudo\s+/i,
2773
+ // Comandos destrutivos
2774
+ /\brm\s+(-[rf]+\s+)*[\/~]/i,
2775
+ /\brm\s+-[rf]*\s+\*/i,
2776
+ /\bchmod\s+(777|666)\s+\//i,
2777
+ /\bdd\s+.*of=\/dev\/(sd|hd|nvme)/i,
2778
+ /\bmkfs\./i,
2779
+ // Fork bombs
2780
+ /:\(\)\s*\{\s*:\|:&\s*\}\s*;:/,
2781
+ // Remote code exec
2782
+ /\bcurl\s+.*\|\s*(ba)?sh/i,
2783
+ /\bwget\s+.*\|\s*(ba)?sh/i
3008
2784
  ];
3009
- var INTERACTIVE_PATTERNS2 = [
3010
- /^(vim|vi|nano|emacs|less|more)\s*/i,
3011
- /^(top|htop|btop)\s*/i,
3012
- /^(mysql|psql|redis-cli|mongo)\s*$/i
2785
+ var INTERACTIVE_PATTERNS = [
2786
+ /^(vim|vi|nano|emacs|pico)\s*/i,
2787
+ /^(less|more|most)\s*/i,
2788
+ /^(top|htop|btop|atop|nmon)\s*/i,
2789
+ /^(ssh|telnet|ftp|sftp)\s+/i,
2790
+ /^(mysql|psql|redis-cli|mongo|mongosh)\s*$/i,
2791
+ /^(python|python3|node|ruby|irb|lua)\s*$/i,
2792
+ /^(gdb|lldb)\s*/i
3013
2793
  ];
3014
2794
  function cleanupOldCommands() {
3015
2795
  if (runningCommands.size <= MAX_STORED_COMMANDS) return;
@@ -3021,12 +2801,12 @@ function cleanupOldCommands() {
3021
2801
  }
3022
2802
  function isDangerousCommand(command) {
3023
2803
  const trimmed = command.trim();
3024
- for (const pattern of DANGEROUS_PATTERNS2) {
2804
+ for (const pattern of DANGEROUS_PATTERNS) {
3025
2805
  if (pattern.test(trimmed)) {
3026
2806
  return "Command requires elevated privileges (sudo/doas) which cannot be handled in async mode";
3027
2807
  }
3028
2808
  }
3029
- for (const pattern of INTERACTIVE_PATTERNS2) {
2809
+ for (const pattern of INTERACTIVE_PATTERNS) {
3030
2810
  if (pattern.test(trimmed)) {
3031
2811
  return "Interactive commands are not supported in async mode";
3032
2812
  }
@@ -3054,7 +2834,7 @@ async function runCommandAsync(args) {
3054
2834
  };
3055
2835
  }
3056
2836
  const commandId = uuidv42().substring(0, 8);
3057
- const platform = os3.platform();
2837
+ const platform = os2.platform();
3058
2838
  let shellCmd;
3059
2839
  let shellArgs;
3060
2840
  if (platform === "win32") {
@@ -3074,7 +2854,7 @@ async function runCommandAsync(args) {
3074
2854
  startTime: Date.now(),
3075
2855
  process: null
3076
2856
  };
3077
- const child = spawn2(shellCmd, shellArgs, {
2857
+ const child = spawn(shellCmd, shellArgs, {
3078
2858
  cwd,
3079
2859
  env: process.env,
3080
2860
  windowsHide: true
@@ -3084,8 +2864,8 @@ async function runCommandAsync(args) {
3084
2864
  child.stdout?.on("data", (data) => {
3085
2865
  if (!stdoutTruncated) {
3086
2866
  entry.stdout += data.toString();
3087
- if (entry.stdout.length > MAX_OUTPUT_SIZE2) {
3088
- entry.stdout = entry.stdout.substring(0, MAX_OUTPUT_SIZE2) + OUTPUT_TRUNCATION_MSG;
2867
+ if (entry.stdout.length > MAX_OUTPUT_SIZE) {
2868
+ entry.stdout = entry.stdout.substring(0, MAX_OUTPUT_SIZE) + OUTPUT_TRUNCATION_MSG;
3089
2869
  stdoutTruncated = true;
3090
2870
  }
3091
2871
  }
@@ -3094,8 +2874,8 @@ async function runCommandAsync(args) {
3094
2874
  child.stderr?.on("data", (data) => {
3095
2875
  if (!stderrTruncated) {
3096
2876
  entry.stderr += data.toString();
3097
- if (entry.stderr.length > MAX_OUTPUT_SIZE2) {
3098
- entry.stderr = entry.stderr.substring(0, MAX_OUTPUT_SIZE2) + OUTPUT_TRUNCATION_MSG;
2877
+ if (entry.stderr.length > MAX_OUTPUT_SIZE) {
2878
+ entry.stderr = entry.stderr.substring(0, MAX_OUTPUT_SIZE) + OUTPUT_TRUNCATION_MSG;
3099
2879
  stderrTruncated = true;
3100
2880
  }
3101
2881
  }
@@ -3280,12 +3060,12 @@ async function killCommand(args) {
3280
3060
  // src/app/agent/tools/natives/task_boundary.ts
3281
3061
  import path9 from "path";
3282
3062
  import { promises as fs7 } from "fs";
3283
- import os4 from "os";
3063
+ import os3 from "os";
3284
3064
  var currentTask = null;
3285
3065
  var artifactsDir = null;
3286
3066
  async function getArtifactsDir() {
3287
3067
  if (artifactsDir) return artifactsDir;
3288
- const homeDir = os4.homedir();
3068
+ const homeDir = os3.homedir();
3289
3069
  const baseDir = path9.join(homeDir, ".bluma", "artifacts");
3290
3070
  const sessionId2 = Date.now().toString(36) + Math.random().toString(36).substr(2, 5);
3291
3071
  artifactsDir = path9.join(baseDir, sessionId2);
@@ -3640,7 +3420,6 @@ var ToolInvoker = class {
3640
3420
  * Este método mapeia o nome da ferramenta (string) para a função TypeScript que a executa.
3641
3421
  */
3642
3422
  registerTools() {
3643
- this.toolImplementations.set("shell_command", shellCommand);
3644
3423
  this.toolImplementations.set("edit_tool", editTool);
3645
3424
  this.toolImplementations.set("ls_tool", ls);
3646
3425
  this.toolImplementations.set("count_file_lines", countLines);
@@ -3648,7 +3427,7 @@ var ToolInvoker = class {
3648
3427
  this.toolImplementations.set("find_by_name", findByName);
3649
3428
  this.toolImplementations.set("grep_search", grepSearch);
3650
3429
  this.toolImplementations.set("view_file_outline", viewFileOutline);
3651
- this.toolImplementations.set("run_command_async", runCommandAsync);
3430
+ this.toolImplementations.set("shell_command", runCommandAsync);
3652
3431
  this.toolImplementations.set("command_status", commandStatus);
3653
3432
  this.toolImplementations.set("send_command_input", sendCommandInput);
3654
3433
  this.toolImplementations.set("kill_command", killCommand);
@@ -3690,7 +3469,7 @@ var ToolInvoker = class {
3690
3469
  // src/app/agent/tools/mcp/mcp_client.ts
3691
3470
  import { promises as fs9 } from "fs";
3692
3471
  import path11 from "path";
3693
- import os5 from "os";
3472
+ import os4 from "os";
3694
3473
  import { fileURLToPath as fileURLToPath2 } from "url";
3695
3474
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3696
3475
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
@@ -3719,7 +3498,7 @@ var MCPClient = class {
3719
3498
  const __filename = fileURLToPath2(import.meta.url);
3720
3499
  const __dirname = path11.dirname(__filename);
3721
3500
  const defaultConfigPath = path11.resolve(__dirname, "config", "bluma-mcp.json");
3722
- const userConfigPath = path11.join(os5.homedir(), ".bluma-cli", "bluma-mcp.json");
3501
+ const userConfigPath = path11.join(os4.homedir(), ".bluma-cli", "bluma-mcp.json");
3723
3502
  const defaultConfig = await this.loadMcpConfig(defaultConfigPath, "Default");
3724
3503
  const userConfig = await this.loadMcpConfig(userConfigPath, "User");
3725
3504
  const mergedConfig = {
@@ -3772,7 +3551,7 @@ var MCPClient = class {
3772
3551
  async connectToStdioServer(serverName, config2) {
3773
3552
  let commandToExecute = config2.command;
3774
3553
  let argsToExecute = config2.args || [];
3775
- const isWindows = os5.platform() === "win32";
3554
+ const isWindows = os4.platform() === "win32";
3776
3555
  if (!isWindows && commandToExecute.toLowerCase() === "cmd") {
3777
3556
  if (argsToExecute.length >= 2 && argsToExecute[0].toLowerCase() === "/c") {
3778
3557
  commandToExecute = argsToExecute[1];
@@ -3892,7 +3671,7 @@ import path14 from "path";
3892
3671
 
3893
3672
  // src/app/agent/session_manager/session_manager.ts
3894
3673
  import path12 from "path";
3895
- import os6 from "os";
3674
+ import os5 from "os";
3896
3675
  import { promises as fs10 } from "fs";
3897
3676
  var fileLocks = /* @__PURE__ */ new Map();
3898
3677
  async function withFileLock(file, fn) {
@@ -3908,15 +3687,32 @@ async function withFileLock(file, fn) {
3908
3687
  if (fileLocks.get(file) === p) fileLocks.delete(file);
3909
3688
  }
3910
3689
  }
3690
+ var pendingSaves = /* @__PURE__ */ new Map();
3691
+ var DEBOUNCE_DELAY_MS = 100;
3692
+ function debouncedSave(sessionFile, history) {
3693
+ const existing = pendingSaves.get(sessionFile);
3694
+ if (existing) {
3695
+ clearTimeout(existing.timer);
3696
+ }
3697
+ const timer = setTimeout(async () => {
3698
+ pendingSaves.delete(sessionFile);
3699
+ try {
3700
+ await doSaveSessionHistory(sessionFile, history);
3701
+ } catch (e) {
3702
+ console.warn(`Debounced save failed for ${sessionFile}: ${e.message}`);
3703
+ }
3704
+ }, DEBOUNCE_DELAY_MS);
3705
+ pendingSaves.set(sessionFile, { history: [...history], timer });
3706
+ }
3911
3707
  function expandHome(p) {
3912
3708
  if (!p) return p;
3913
3709
  if (p.startsWith("~")) {
3914
- return path12.join(os6.homedir(), p.slice(1));
3710
+ return path12.join(os5.homedir(), p.slice(1));
3915
3711
  }
3916
3712
  return p;
3917
3713
  }
3918
3714
  function getPreferredAppDir() {
3919
- const fixed = path12.join(os6.homedir(), ".bluma-cli");
3715
+ const fixed = path12.join(os5.homedir(), ".bluma-cli");
3920
3716
  return path12.resolve(expandHome(fixed));
3921
3717
  }
3922
3718
  async function safeRenameWithRetry(src, dest, maxRetries = 6) {
@@ -3925,25 +3721,36 @@ async function safeRenameWithRetry(src, dest, maxRetries = 6) {
3925
3721
  const isWin = process.platform === "win32";
3926
3722
  while (attempt <= maxRetries) {
3927
3723
  try {
3724
+ const dir = path12.dirname(dest);
3725
+ await fs10.mkdir(dir, { recursive: true }).catch(() => {
3726
+ });
3928
3727
  await fs10.rename(src, dest);
3929
3728
  return;
3930
3729
  } catch (e) {
3931
3730
  lastErr = e;
3932
3731
  const code = e && e.code || "";
3933
- const transient = code === "EPERM" || code === "EBUSY" || code === "ENOTEMPTY" || code === "EACCES";
3934
- if (!(isWin && transient) || attempt === maxRetries) break;
3732
+ const transient = code === "EPERM" || code === "EBUSY" || code === "ENOTEMPTY" || code === "EACCES" || code === "ENOENT";
3733
+ if (!transient || attempt === maxRetries) break;
3935
3734
  const backoff = Math.min(1e3, 50 * Math.pow(2, attempt));
3936
3735
  await new Promise((r) => setTimeout(r, backoff));
3937
3736
  attempt++;
3938
3737
  }
3939
3738
  }
3940
3739
  try {
3740
+ await fs10.access(src);
3941
3741
  const data = await fs10.readFile(src);
3742
+ const dir = path12.dirname(dest);
3743
+ await fs10.mkdir(dir, { recursive: true }).catch(() => {
3744
+ });
3942
3745
  await fs10.writeFile(dest, data);
3943
3746
  await fs10.unlink(src).catch(() => {
3944
3747
  });
3945
3748
  return;
3946
3749
  } catch (fallbackErr) {
3750
+ if (fallbackErr?.code === "ENOENT") {
3751
+ console.warn(`Session temp file disappeared: ${src}`);
3752
+ return;
3753
+ }
3947
3754
  throw lastErr || fallbackErr;
3948
3755
  }
3949
3756
  }
@@ -3971,7 +3778,7 @@ async function loadOrcreateSession(sessionId2) {
3971
3778
  return [sessionFile, [], []];
3972
3779
  }
3973
3780
  }
3974
- async function saveSessionHistory(sessionFile, history) {
3781
+ async function doSaveSessionHistory(sessionFile, history) {
3975
3782
  await withFileLock(sessionFile, async () => {
3976
3783
  let sessionData;
3977
3784
  try {
@@ -4021,9 +3828,12 @@ async function saveSessionHistory(sessionFile, history) {
4021
3828
  }
4022
3829
  });
4023
3830
  }
3831
+ async function saveSessionHistory(sessionFile, history) {
3832
+ debouncedSave(sessionFile, history);
3833
+ }
4024
3834
 
4025
3835
  // src/app/agent/core/prompt/prompt_builder.ts
4026
- import os7 from "os";
3836
+ import os6 from "os";
4027
3837
  import fs11 from "fs";
4028
3838
  import path13 from "path";
4029
3839
  import { execSync } from "child_process";
@@ -4218,12 +4028,6 @@ You are a **teammate who writes tests**, not an assistant who skips them.
4218
4028
  - **For edit_tool**: Provide exact content with correct whitespace (read first!)
4219
4029
  - **Check file exists** before attempting edits
4220
4030
 
4221
- ### Shell Commands:
4222
- - **NEVER use sudo** - Commands requiring sudo will be blocked
4223
- - **Long commands**: Set appropriate timeout (default 300s)
4224
- - **Interactive commands** (vim, less, ssh) are blocked - use alternatives
4225
- - **Large outputs** will be truncated at 100KB
4226
-
4227
4031
  ### Safe Auto-Approved Tools (no confirmation needed):
4228
4032
  - message_notify_user
4229
4033
  - ls_tool
@@ -4239,6 +4043,97 @@ You are a **teammate who writes tests**, not an assistant who skips them.
4239
4043
 
4240
4044
  ---
4241
4045
 
4046
+ <shell_command>
4047
+ ## Shell Command System (CRITICAL TO UNDERSTAND)
4048
+
4049
+ The \`shell_command\` tool runs commands in **background** and returns a \`command_id\`.
4050
+ You MUST use \`command_status\` to get the output.
4051
+
4052
+ ### Workflow:
4053
+ \`\`\`
4054
+ 1. shell_command({ command: "npm install" })
4055
+ \u2192 Returns: { success: true, command_id: "abc123" }
4056
+
4057
+ 2. command_status({ command_id: "abc123", wait_seconds: 60 })
4058
+ \u2192 Returns: { status: "completed", stdout: "...", exit_code: 0 }
4059
+ \`\`\`
4060
+
4061
+ ### Security Rules (BLOCKED COMMANDS):
4062
+ - [BLOCKED] \`sudo\`, \`doas\`, \`su\`, \`pkexec\` - Elevation blocked
4063
+ - [BLOCKED] \`rm -rf /\`, \`rm -rf *\`, \`rm -rf ~\` - Destructive
4064
+ - [BLOCKED] \`chmod 777 /\`, \`chmod 666 /\` - Insecure permissions
4065
+ - [BLOCKED] \`curl ... | bash\`, \`wget ... | sh\` - Remote code execution
4066
+ - [BLOCKED] \`vim\`, \`nano\`, \`less\`, \`ssh\`, \`mysql\` - Interactive prompts
4067
+ - [BLOCKED] Fork bombs, \`dd\` to disk, \`mkfs.*\` - System destruction
4068
+
4069
+ ### Output Limits:
4070
+ - Maximum 30KB or 200 lines per output
4071
+ - Long outputs are truncated (head + tail shown)
4072
+ - For large outputs, use: \`command | head -n 50\` or redirect to file
4073
+
4074
+ ### Examples:
4075
+
4076
+ **[OK] Install dependencies:**
4077
+ \`\`\`
4078
+ [Step 1] shell_command({ command: "npm install", cwd: "/project" })
4079
+ \u2192 { command_id: "cmd_001" }
4080
+
4081
+ [Step 2] command_status({ command_id: "cmd_001", wait_seconds: 120 })
4082
+ \u2192 { status: "completed", exit_code: 0, stdout: "added 150 packages..." }
4083
+
4084
+ [Step 3] message_notify_user("Dependencies installed successfully.")
4085
+ \`\`\`
4086
+
4087
+ **[OK] Run tests:**
4088
+ \`\`\`
4089
+ [Step 1] shell_command({ command: "npm test" })
4090
+ \u2192 { command_id: "cmd_002" }
4091
+
4092
+ [Step 2] command_status({ command_id: "cmd_002", wait_seconds: 60 })
4093
+ \u2192 { status: "completed", exit_code: 0, stdout: "47 tests passed" }
4094
+ \`\`\`
4095
+
4096
+ **[OK] Quick command (git status):**
4097
+ \`\`\`
4098
+ [Step 1] shell_command({ command: "git status --short" })
4099
+ \u2192 { command_id: "cmd_003" }
4100
+
4101
+ [Step 2] command_status({ command_id: "cmd_003", wait_seconds: 5 })
4102
+ \u2192 { status: "completed", stdout: "M src/index.ts\\nA src/utils.ts" }
4103
+ \`\`\`
4104
+
4105
+ **[OK] Long build with timeout:**
4106
+ \`\`\`
4107
+ [Step 1] shell_command({ command: "docker build -t myapp .", timeout: 600 })
4108
+ \u2192 { command_id: "cmd_004" }
4109
+
4110
+ [Step 2] command_status({ command_id: "cmd_004", wait_seconds: 300 })
4111
+ \u2192 { status: "completed", exit_code: 0, truncated: true, stdout: "..." }
4112
+ \`\`\`
4113
+
4114
+ **[WRONG] Never forget command_status:**
4115
+ \`\`\`
4116
+ shell_command({ command: "npm install" })
4117
+ message_notify_user("Installed!") // WRONG: You don't know if it succeeded!
4118
+ \`\`\`
4119
+
4120
+ **[WRONG] Never use blocked commands:**
4121
+ \`\`\`
4122
+ shell_command({ command: "sudo apt install ..." }) // BLOCKED
4123
+ shell_command({ command: "rm -rf /" }) // BLOCKED
4124
+ shell_command({ command: "vim file.txt" }) // BLOCKED (interactive)
4125
+ \`\`\`
4126
+
4127
+ ### Tips:
4128
+ - Always check \`exit_code\` - 0 means success, non-zero is error
4129
+ - If \`status: "running"\`, call command_status again with longer wait
4130
+ - If \`truncated: true\`, the output was too long - use \`| head\` or \`| tail\`
4131
+ - Use \`send_command_input\` for commands that need input (y/n prompts)
4132
+ - Use \`kill_command\` to stop a stuck command
4133
+ </shell_command>
4134
+
4135
+ ---
4136
+
4242
4137
  <communication_style>
4243
4138
  ## How You Communicate
4244
4139
 
@@ -4330,12 +4225,12 @@ Let's build something great, {username}.
4330
4225
  function getUnifiedSystemPrompt() {
4331
4226
  const cwd = process.cwd();
4332
4227
  const env = {
4333
- os_type: os7.type(),
4334
- os_version: os7.release(),
4335
- architecture: os7.arch(),
4228
+ os_type: os6.type(),
4229
+ os_version: os6.release(),
4230
+ architecture: os6.arch(),
4336
4231
  workdir: cwd,
4337
4232
  shell_type: process.env.SHELL || process.env.COMSPEC || "unknown",
4338
- username: os7.userInfo().username,
4233
+ username: os6.userInfo().username,
4339
4234
  current_date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
4340
4235
  timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
4341
4236
  is_git_repo: isGitRepo(cwd) ? "yes" : "no",
@@ -4721,7 +4616,7 @@ function getSubAgentByCommand(cmd) {
4721
4616
  }
4722
4617
 
4723
4618
  // src/app/agent/subagents/init/init_system_prompt.ts
4724
- import os8 from "os";
4619
+ import os7 from "os";
4725
4620
  var SYSTEM_PROMPT2 = `
4726
4621
 
4727
4622
  ### YOU ARE BluMa CLI \u2014 INIT SUBAGENT \u2014 AUTONOMOUS SENIOR SOFTWARE ENGINEER @ NOMADENGENUITY
@@ -4884,12 +4779,12 @@ Rule Summary:
4884
4779
  function getInitPrompt() {
4885
4780
  const now = /* @__PURE__ */ new Date();
4886
4781
  const collectedData = {
4887
- os_type: os8.type(),
4888
- os_version: os8.release(),
4889
- architecture: os8.arch(),
4782
+ os_type: os7.type(),
4783
+ os_version: os7.release(),
4784
+ architecture: os7.arch(),
4890
4785
  workdir: process.cwd(),
4891
4786
  shell_type: process.env.SHELL || process.env.COMSPEC || "Unknown",
4892
- username: os8.userInfo().username || "Unknown",
4787
+ username: os7.userInfo().username || "Unknown",
4893
4788
  current_date: now.toISOString().split("T")[0],
4894
4789
  // Formato YYYY-MM-DD
4895
4790
  timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "Unknown",
@@ -5142,7 +5037,7 @@ var SubAgentsBluMa = class {
5142
5037
  };
5143
5038
 
5144
5039
  // src/app/agent/agent.ts
5145
- var globalEnvPath = path15.join(os9.homedir(), ".bluma-cli", ".env");
5040
+ var globalEnvPath = path15.join(os8.homedir(), ".bluma-cli", ".env");
5146
5041
  dotenv.config({ path: globalEnvPath });
5147
5042
  var Agent = class {
5148
5043
  sessionId;
@@ -5520,18 +5415,6 @@ var renderViewFileOutline = ({ args }) => {
5520
5415
  ] })
5521
5416
  ] });
5522
5417
  };
5523
- var renderRunCommandAsync = ({ args }) => {
5524
- const parsed = parseArgs(args);
5525
- const command = parsed.command || "[no command]";
5526
- return /* @__PURE__ */ jsxs8(Box8, { paddingX: 1, children: [
5527
- /* @__PURE__ */ jsx8(Text8, { color: "white", bold: true, children: "async" }),
5528
- /* @__PURE__ */ jsx8(Text8, { color: "white", bold: true, children: " $" }),
5529
- /* @__PURE__ */ jsxs8(Text8, { children: [
5530
- " ",
5531
- truncate(command, 50)
5532
- ] })
5533
- ] });
5534
- };
5535
5418
  var renderCommandStatus = ({ args }) => {
5536
5419
  const parsed = parseArgs(args);
5537
5420
  const id = parsed.command_id || "[no id]";
@@ -5607,7 +5490,6 @@ var ToolRenderDisplay = {
5607
5490
  find_by_name: renderFindByName,
5608
5491
  grep_search: renderGrepSearch,
5609
5492
  view_file_outline: renderViewFileOutline,
5610
- run_command_async: renderRunCommandAsync,
5611
5493
  command_status: renderCommandStatus,
5612
5494
  task_boundary: renderTaskBoundary,
5613
5495
  search_web: renderSearchWeb
@@ -5837,10 +5719,20 @@ var ToolResultDisplayComponent = ({ toolName, result }) => {
5837
5719
  ] })
5838
5720
  ] });
5839
5721
  }
5840
- if ((toolName.includes("shell_command") || toolName.includes("run_command")) && parsed) {
5722
+ if ((toolName.includes("shell_command") || toolName.includes("run_command") || toolName.includes("command_status")) && parsed) {
5841
5723
  const output = parsed.stdout || parsed.output || "";
5842
5724
  const stderr = parsed.stderr || "";
5843
5725
  const exitCode = parsed.exit_code ?? parsed.exitCode ?? 0;
5726
+ const status = parsed.status || "";
5727
+ if (parsed.command_id && !output && !stderr && !status) {
5728
+ return /* @__PURE__ */ jsx11(Box11, { paddingLeft: 3, children: /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
5729
+ "started #",
5730
+ parsed.command_id.slice(0, 8)
5731
+ ] }) });
5732
+ }
5733
+ if (status === "running") {
5734
+ return /* @__PURE__ */ jsx11(Box11, { paddingLeft: 3, children: /* @__PURE__ */ jsx11(Text10, { dimColor: true, color: "yellow", children: "still running..." }) });
5735
+ }
5844
5736
  if (!output && !stderr) return null;
5845
5737
  const { lines, truncated } = truncateLines(output || stderr, MAX_LINES);
5846
5738
  const isError = exitCode !== 0 || stderr;
@@ -5850,6 +5742,10 @@ var ToolResultDisplayComponent = ({ toolName, result }) => {
5850
5742
  "... +",
5851
5743
  truncated,
5852
5744
  " lines"
5745
+ ] }),
5746
+ exitCode !== 0 && /* @__PURE__ */ jsxs10(Text10, { dimColor: true, color: "red", children: [
5747
+ "exit code: ",
5748
+ exitCode
5853
5749
  ] })
5854
5750
  ] });
5855
5751
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nomad-e/bluma-cli",
3
- "version": "0.0.106",
3
+ "version": "0.0.107",
4
4
  "description": "BluMa independent agent for automation and advanced software engineering.",
5
5
  "author": "Alex Fonseca",
6
6
  "license": "Apache-2.0",