@nomad-e/bluma-cli 0.0.105 → 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,7 +1,6 @@
1
1
  {
2
2
  "mcpServers": {
3
- "_comment": "MCP servers are disabled - using native OpenAI reasoning field instead",
4
- "_disabled_reasoning": {
3
+ "reasoning": {
5
4
  "type": "stdio",
6
5
  "command": "cmd",
7
6
  "args": [
@@ -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
@@ -172,20 +172,13 @@ import { Box as Box16, Text as Text15, Static } from "ink";
172
172
  import { Box, Text } from "ink";
173
173
  import { memo } from "react";
174
174
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
175
- var VERSION = "0.0.103";
176
175
  var HeaderComponent = ({
177
176
  sessionId: sessionId2,
178
177
  workdir
179
178
  }) => {
180
179
  const dirName = workdir.split("/").pop() || workdir;
181
180
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
182
- /* @__PURE__ */ jsxs(Box, { children: [
183
- /* @__PURE__ */ jsx(Text, { color: "magenta", bold: true, children: "bluma \u2014 coding agent" }),
184
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
185
- " ",
186
- VERSION
187
- ] })
188
- ] }),
181
+ /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { color: "magenta", bold: true, children: "bluma \u2014 coding agent" }) }),
189
182
  /* @__PURE__ */ jsxs(Box, { children: [
190
183
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "cwd " }),
191
184
  /* @__PURE__ */ jsx(Text, { color: "cyan", children: dirName }),
@@ -741,7 +734,7 @@ var TextLine = memo2(({
741
734
  const after = line.slice(cursorCol + 1);
742
735
  return /* @__PURE__ */ jsxs2(Text2, { children: [
743
736
  before,
744
- /* @__PURE__ */ jsx2(Text2, { inverse: true, color: "magenta", children: char }),
737
+ /* @__PURE__ */ jsx2(Text2, { inverse: true, color: "white", children: char }),
745
738
  after
746
739
  ] });
747
740
  }, (prev, next) => {
@@ -1323,255 +1316,16 @@ var ConfirmationPrompt = memo4(ConfirmationPromptComponent);
1323
1316
  import OpenAI from "openai";
1324
1317
  import * as dotenv from "dotenv";
1325
1318
  import path15 from "path";
1326
- import os9 from "os";
1319
+ import os8 from "os";
1327
1320
 
1328
1321
  // src/app/agent/tool_invoker.ts
1329
1322
  import { promises as fs8 } from "fs";
1330
1323
  import path10 from "path";
1331
1324
  import { fileURLToPath } from "url";
1332
1325
 
1333
- // src/app/agent/tools/natives/shell_command.ts
1334
- import os from "os";
1335
- import { spawn } from "child_process";
1336
- var MAX_OUTPUT_SIZE = 1e5;
1337
- var OUTPUT_TRUNCATION_MESSAGE = "\n\n[OUTPUT TRUNCATED - exceeded 100KB limit. Use pagination or redirect to file for full output.]";
1338
- var DANGEROUS_PATTERNS = [
1339
- // === ELEVAÇÃO DE PRIVILÉGIOS ===
1340
- /^sudo\s+/i,
1341
- // sudo commands
1342
- /^doas\s+/i,
1343
- // doas (BSD sudo alternative)
1344
- /^su\s+/i,
1345
- // switch user
1346
- /^pkexec\s+/i,
1347
- // PolicyKit execution
1348
- /\|\s*sudo\s+/i,
1349
- // piped to sudo
1350
- /;\s*sudo\s+/i,
1351
- // chained with sudo
1352
- /&&\s*sudo\s+/i,
1353
- // AND chained with sudo
1354
- // === COMANDOS DESTRUTIVOS ===
1355
- /\brm\s+(-[rf]+\s+)*[\/~]/i,
1356
- // rm com paths perigosos (/, ~)
1357
- /\brm\s+-[rf]*\s+\*/i,
1358
- // rm -rf *
1359
- /\bchmod\s+(777|666)\s+\//i,
1360
- // chmod 777/666 em paths root
1361
- /\bchown\s+.*\s+\//i,
1362
- // chown em paths root
1363
- /\bdd\s+.*of=\/dev\/(sd|hd|nvme)/i,
1364
- // dd para discos
1365
- /\bmkfs\./i,
1366
- // format filesystem
1367
- />\s*\/dev\/(sd|hd|nvme)/i,
1368
- // redirect para disco
1369
- /\bshred\s+/i,
1370
- // secure delete
1371
- // === FORK BOMB / RESOURCE EXHAUSTION ===
1372
- /:\(\)\s*\{\s*:\|:&\s*\}\s*;:/,
1373
- // fork bomb clássico
1374
- /\bwhile\s+true\s*;\s*do/i,
1375
- // infinite loop
1376
- /\byes\s+\|/i,
1377
- // yes piped (resource exhaustion)
1378
- // === NETWORK PERIGOSO ===
1379
- /\bcurl\s+.*\|\s*(ba)?sh/i,
1380
- // curl | bash (remote code exec)
1381
- /\bwget\s+.*\|\s*(ba)?sh/i,
1382
- // wget | bash
1383
- /\bnc\s+-[el]/i
1384
- // netcat listener (backdoor)
1385
- ];
1386
- var INTERACTIVE_PATTERNS = [
1387
- /^(vim|vi|nano|emacs|pico)\s*/i,
1388
- // editors
1389
- /^(less|more|most)\s*/i,
1390
- // pagers
1391
- /^(top|htop|btop|atop|nmon)\s*/i,
1392
- // monitoring tools
1393
- /^(ssh|telnet|ftp|sftp)\s+/i,
1394
- // remote connections
1395
- /^(mysql|psql|redis-cli|mongo|mongosh)\s*$/i,
1396
- // database CLIs (sem script)
1397
- /^(python|python3|node|ruby|irb|lua)\s*$/i,
1398
- // REPLs sem script
1399
- /^(gdb|lldb)\s*/i,
1400
- // debuggers
1401
- /^(bc|dc)\s*$/i
1402
- // calculators
1403
- ];
1404
- function shellCommand(args) {
1405
- const {
1406
- command,
1407
- timeout = 300,
1408
- // 5 minutos por padrão
1409
- cwd = process.cwd(),
1410
- verbose = false
1411
- } = args;
1412
- return new Promise((resolve) => {
1413
- const startTime = Date.now();
1414
- const platform = os.platform();
1415
- const commandTrimmed = command.trim();
1416
- for (const pattern of DANGEROUS_PATTERNS) {
1417
- if (pattern.test(commandTrimmed)) {
1418
- const result = {
1419
- status: "blocked",
1420
- exitCode: null,
1421
- stdout: "",
1422
- stderr: `Command blocked: "${commandTrimmed}" requires elevated privileges (sudo/doas). These commands require interactive password input which cannot be handled. Alternatives:
1423
- 1. Run the command manually in terminal
1424
- 2. Configure passwordless sudo for this specific command
1425
- 3. Run BluMa as root (not recommended)`,
1426
- command,
1427
- cwd,
1428
- platform,
1429
- duration: Date.now() - startTime,
1430
- blockedReason: "requires_sudo"
1431
- };
1432
- return resolve(formatResult(result, verbose));
1433
- }
1434
- }
1435
- for (const pattern of INTERACTIVE_PATTERNS) {
1436
- if (pattern.test(commandTrimmed)) {
1437
- const result = {
1438
- status: "blocked",
1439
- exitCode: null,
1440
- stdout: "",
1441
- stderr: `Command blocked: "${commandTrimmed}" is an interactive command. Interactive commands require user input and cannot be handled by the agent. Alternatives:
1442
- 1. Use non-interactive alternatives (e.g., 'cat' instead of 'less')
1443
- 2. Run the command manually in terminal
1444
- 3. For editors, use edit_tool instead`,
1445
- command,
1446
- cwd,
1447
- platform,
1448
- duration: Date.now() - startTime,
1449
- blockedReason: "interactive_command"
1450
- };
1451
- return resolve(formatResult(result, verbose));
1452
- }
1453
- }
1454
- let shellCmd;
1455
- let shellArgs;
1456
- if (platform === "win32") {
1457
- shellCmd = process.env.COMSPEC || "cmd.exe";
1458
- shellArgs = ["/c", command];
1459
- } else {
1460
- shellCmd = process.env.SHELL || "/bin/bash";
1461
- shellArgs = ["-c", command];
1462
- }
1463
- let stdout = "";
1464
- let stderr = "";
1465
- let stdoutTruncated = false;
1466
- let stderrTruncated = false;
1467
- let timedOut = false;
1468
- let finished = false;
1469
- const childProcess = spawn(shellCmd, shellArgs, {
1470
- cwd,
1471
- env: process.env,
1472
- // Importante: no Windows, precisamos do shell, mas spawn já lida com isso
1473
- windowsHide: true
1474
- });
1475
- const timeoutId = setTimeout(() => {
1476
- if (!finished) {
1477
- timedOut = true;
1478
- childProcess.kill("SIGTERM");
1479
- setTimeout(() => {
1480
- if (!finished) {
1481
- childProcess.kill("SIGKILL");
1482
- }
1483
- }, 2e3);
1484
- }
1485
- }, timeout * 1e3);
1486
- if (childProcess.stdout) {
1487
- childProcess.stdout.on("data", (data) => {
1488
- if (!stdoutTruncated) {
1489
- stdout += data.toString();
1490
- if (stdout.length > MAX_OUTPUT_SIZE) {
1491
- stdout = stdout.substring(0, MAX_OUTPUT_SIZE) + OUTPUT_TRUNCATION_MESSAGE;
1492
- stdoutTruncated = true;
1493
- }
1494
- }
1495
- });
1496
- }
1497
- if (childProcess.stderr) {
1498
- childProcess.stderr.on("data", (data) => {
1499
- if (!stderrTruncated) {
1500
- stderr += data.toString();
1501
- if (stderr.length > MAX_OUTPUT_SIZE) {
1502
- stderr = stderr.substring(0, MAX_OUTPUT_SIZE) + OUTPUT_TRUNCATION_MESSAGE;
1503
- stderrTruncated = true;
1504
- }
1505
- }
1506
- });
1507
- }
1508
- childProcess.on("error", (error) => {
1509
- if (!finished) {
1510
- finished = true;
1511
- clearTimeout(timeoutId);
1512
- const result = {
1513
- status: "error",
1514
- exitCode: null,
1515
- stdout: stdout.trim(),
1516
- stderr: `Failed to execute command: ${error.message}`,
1517
- command,
1518
- cwd,
1519
- platform,
1520
- duration: Date.now() - startTime
1521
- };
1522
- resolve(formatResult(result, verbose));
1523
- }
1524
- });
1525
- childProcess.on("close", (code, signal) => {
1526
- if (!finished) {
1527
- finished = true;
1528
- clearTimeout(timeoutId);
1529
- const result = {
1530
- status: timedOut ? "timeout" : code === 0 ? "success" : "error",
1531
- exitCode: code,
1532
- stdout: stdout.trim(),
1533
- stderr: timedOut ? `Command timed out after ${timeout} seconds
1534
- ${stderr.trim()}` : stderr.trim(),
1535
- command,
1536
- cwd,
1537
- platform,
1538
- duration: Date.now() - startTime,
1539
- truncated: stdoutTruncated || stderrTruncated
1540
- };
1541
- resolve(formatResult(result, verbose));
1542
- }
1543
- });
1544
- });
1545
- }
1546
- function formatResult(result, verbose) {
1547
- if (verbose) {
1548
- return JSON.stringify(result, null, 2);
1549
- }
1550
- const output = {
1551
- status: result.status,
1552
- exitCode: result.exitCode
1553
- };
1554
- if (result.stdout) {
1555
- output.stdout = result.stdout;
1556
- }
1557
- if (result.stderr) {
1558
- output.stderr = result.stderr;
1559
- }
1560
- if (result.status === "timeout") {
1561
- output.message = `Command exceeded timeout of ${result.duration / 1e3}s`;
1562
- }
1563
- if (result.status === "blocked" && result.blockedReason) {
1564
- output.blockedReason = result.blockedReason;
1565
- }
1566
- if (result.truncated) {
1567
- output.warning = "Output was truncated due to size limits (100KB max per stream)";
1568
- }
1569
- return JSON.stringify(output, null, 2);
1570
- }
1571
-
1572
1326
  // src/app/agent/tools/natives/edit.ts
1573
1327
  import path3 from "path";
1574
- import os2 from "os";
1328
+ import os from "os";
1575
1329
  import { promises as fs2 } from "fs";
1576
1330
  import { diffLines } from "diff";
1577
1331
  var MAX_DIFF_SIZE = 5e4;
@@ -1579,7 +1333,7 @@ var MAX_FILE_SIZE = 10 * 1024 * 1024;
1579
1333
  function normalizePath(filePath) {
1580
1334
  try {
1581
1335
  filePath = filePath.trim();
1582
- if (os2.platform() === "win32") {
1336
+ if (os.platform() === "win32") {
1583
1337
  const winDriveRegex = /^\/([a-zA-Z])[:/]/;
1584
1338
  const match = filePath.match(winDriveRegex);
1585
1339
  if (match) {
@@ -3000,23 +2754,42 @@ async function viewFileOutline(args) {
3000
2754
  }
3001
2755
 
3002
2756
  // src/app/agent/tools/natives/async_command.ts
3003
- import os3 from "os";
3004
- import { spawn as spawn2 } from "child_process";
2757
+ import os2 from "os";
2758
+ import { spawn } from "child_process";
3005
2759
  import { v4 as uuidv42 } from "uuid";
3006
2760
  var runningCommands = /* @__PURE__ */ new Map();
3007
- var MAX_OUTPUT_SIZE2 = 1e5;
2761
+ var MAX_OUTPUT_SIZE = 3e4;
3008
2762
  var MAX_STORED_COMMANDS = 50;
3009
- var OUTPUT_TRUNCATION_MSG = "\n[OUTPUT TRUNCATED]";
3010
- 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
3011
2766
  /^sudo\s+/i,
3012
2767
  /^doas\s+/i,
3013
2768
  /^su\s+/i,
3014
- /\|\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
3015
2784
  ];
3016
- var INTERACTIVE_PATTERNS2 = [
3017
- /^(vim|vi|nano|emacs|less|more)\s*/i,
3018
- /^(top|htop|btop)\s*/i,
3019
- /^(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
3020
2793
  ];
3021
2794
  function cleanupOldCommands() {
3022
2795
  if (runningCommands.size <= MAX_STORED_COMMANDS) return;
@@ -3028,12 +2801,12 @@ function cleanupOldCommands() {
3028
2801
  }
3029
2802
  function isDangerousCommand(command) {
3030
2803
  const trimmed = command.trim();
3031
- for (const pattern of DANGEROUS_PATTERNS2) {
2804
+ for (const pattern of DANGEROUS_PATTERNS) {
3032
2805
  if (pattern.test(trimmed)) {
3033
2806
  return "Command requires elevated privileges (sudo/doas) which cannot be handled in async mode";
3034
2807
  }
3035
2808
  }
3036
- for (const pattern of INTERACTIVE_PATTERNS2) {
2809
+ for (const pattern of INTERACTIVE_PATTERNS) {
3037
2810
  if (pattern.test(trimmed)) {
3038
2811
  return "Interactive commands are not supported in async mode";
3039
2812
  }
@@ -3061,7 +2834,7 @@ async function runCommandAsync(args) {
3061
2834
  };
3062
2835
  }
3063
2836
  const commandId = uuidv42().substring(0, 8);
3064
- const platform = os3.platform();
2837
+ const platform = os2.platform();
3065
2838
  let shellCmd;
3066
2839
  let shellArgs;
3067
2840
  if (platform === "win32") {
@@ -3081,7 +2854,7 @@ async function runCommandAsync(args) {
3081
2854
  startTime: Date.now(),
3082
2855
  process: null
3083
2856
  };
3084
- const child = spawn2(shellCmd, shellArgs, {
2857
+ const child = spawn(shellCmd, shellArgs, {
3085
2858
  cwd,
3086
2859
  env: process.env,
3087
2860
  windowsHide: true
@@ -3091,8 +2864,8 @@ async function runCommandAsync(args) {
3091
2864
  child.stdout?.on("data", (data) => {
3092
2865
  if (!stdoutTruncated) {
3093
2866
  entry.stdout += data.toString();
3094
- if (entry.stdout.length > MAX_OUTPUT_SIZE2) {
3095
- 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;
3096
2869
  stdoutTruncated = true;
3097
2870
  }
3098
2871
  }
@@ -3101,8 +2874,8 @@ async function runCommandAsync(args) {
3101
2874
  child.stderr?.on("data", (data) => {
3102
2875
  if (!stderrTruncated) {
3103
2876
  entry.stderr += data.toString();
3104
- if (entry.stderr.length > MAX_OUTPUT_SIZE2) {
3105
- 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;
3106
2879
  stderrTruncated = true;
3107
2880
  }
3108
2881
  }
@@ -3287,12 +3060,12 @@ async function killCommand(args) {
3287
3060
  // src/app/agent/tools/natives/task_boundary.ts
3288
3061
  import path9 from "path";
3289
3062
  import { promises as fs7 } from "fs";
3290
- import os4 from "os";
3063
+ import os3 from "os";
3291
3064
  var currentTask = null;
3292
3065
  var artifactsDir = null;
3293
3066
  async function getArtifactsDir() {
3294
3067
  if (artifactsDir) return artifactsDir;
3295
- const homeDir = os4.homedir();
3068
+ const homeDir = os3.homedir();
3296
3069
  const baseDir = path9.join(homeDir, ".bluma", "artifacts");
3297
3070
  const sessionId2 = Date.now().toString(36) + Math.random().toString(36).substr(2, 5);
3298
3071
  artifactsDir = path9.join(baseDir, sessionId2);
@@ -3647,7 +3420,6 @@ var ToolInvoker = class {
3647
3420
  * Este método mapeia o nome da ferramenta (string) para a função TypeScript que a executa.
3648
3421
  */
3649
3422
  registerTools() {
3650
- this.toolImplementations.set("shell_command", shellCommand);
3651
3423
  this.toolImplementations.set("edit_tool", editTool);
3652
3424
  this.toolImplementations.set("ls_tool", ls);
3653
3425
  this.toolImplementations.set("count_file_lines", countLines);
@@ -3655,7 +3427,7 @@ var ToolInvoker = class {
3655
3427
  this.toolImplementations.set("find_by_name", findByName);
3656
3428
  this.toolImplementations.set("grep_search", grepSearch);
3657
3429
  this.toolImplementations.set("view_file_outline", viewFileOutline);
3658
- this.toolImplementations.set("run_command_async", runCommandAsync);
3430
+ this.toolImplementations.set("shell_command", runCommandAsync);
3659
3431
  this.toolImplementations.set("command_status", commandStatus);
3660
3432
  this.toolImplementations.set("send_command_input", sendCommandInput);
3661
3433
  this.toolImplementations.set("kill_command", killCommand);
@@ -3697,7 +3469,7 @@ var ToolInvoker = class {
3697
3469
  // src/app/agent/tools/mcp/mcp_client.ts
3698
3470
  import { promises as fs9 } from "fs";
3699
3471
  import path11 from "path";
3700
- import os5 from "os";
3472
+ import os4 from "os";
3701
3473
  import { fileURLToPath as fileURLToPath2 } from "url";
3702
3474
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3703
3475
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
@@ -3726,7 +3498,7 @@ var MCPClient = class {
3726
3498
  const __filename = fileURLToPath2(import.meta.url);
3727
3499
  const __dirname = path11.dirname(__filename);
3728
3500
  const defaultConfigPath = path11.resolve(__dirname, "config", "bluma-mcp.json");
3729
- const userConfigPath = path11.join(os5.homedir(), ".bluma-cli", "bluma-mcp.json");
3501
+ const userConfigPath = path11.join(os4.homedir(), ".bluma-cli", "bluma-mcp.json");
3730
3502
  const defaultConfig = await this.loadMcpConfig(defaultConfigPath, "Default");
3731
3503
  const userConfig = await this.loadMcpConfig(userConfigPath, "User");
3732
3504
  const mergedConfig = {
@@ -3779,7 +3551,7 @@ var MCPClient = class {
3779
3551
  async connectToStdioServer(serverName, config2) {
3780
3552
  let commandToExecute = config2.command;
3781
3553
  let argsToExecute = config2.args || [];
3782
- const isWindows = os5.platform() === "win32";
3554
+ const isWindows = os4.platform() === "win32";
3783
3555
  if (!isWindows && commandToExecute.toLowerCase() === "cmd") {
3784
3556
  if (argsToExecute.length >= 2 && argsToExecute[0].toLowerCase() === "/c") {
3785
3557
  commandToExecute = argsToExecute[1];
@@ -3899,7 +3671,7 @@ import path14 from "path";
3899
3671
 
3900
3672
  // src/app/agent/session_manager/session_manager.ts
3901
3673
  import path12 from "path";
3902
- import os6 from "os";
3674
+ import os5 from "os";
3903
3675
  import { promises as fs10 } from "fs";
3904
3676
  var fileLocks = /* @__PURE__ */ new Map();
3905
3677
  async function withFileLock(file, fn) {
@@ -3915,15 +3687,32 @@ async function withFileLock(file, fn) {
3915
3687
  if (fileLocks.get(file) === p) fileLocks.delete(file);
3916
3688
  }
3917
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
+ }
3918
3707
  function expandHome(p) {
3919
3708
  if (!p) return p;
3920
3709
  if (p.startsWith("~")) {
3921
- return path12.join(os6.homedir(), p.slice(1));
3710
+ return path12.join(os5.homedir(), p.slice(1));
3922
3711
  }
3923
3712
  return p;
3924
3713
  }
3925
3714
  function getPreferredAppDir() {
3926
- const fixed = path12.join(os6.homedir(), ".bluma-cli");
3715
+ const fixed = path12.join(os5.homedir(), ".bluma-cli");
3927
3716
  return path12.resolve(expandHome(fixed));
3928
3717
  }
3929
3718
  async function safeRenameWithRetry(src, dest, maxRetries = 6) {
@@ -3932,25 +3721,36 @@ async function safeRenameWithRetry(src, dest, maxRetries = 6) {
3932
3721
  const isWin = process.platform === "win32";
3933
3722
  while (attempt <= maxRetries) {
3934
3723
  try {
3724
+ const dir = path12.dirname(dest);
3725
+ await fs10.mkdir(dir, { recursive: true }).catch(() => {
3726
+ });
3935
3727
  await fs10.rename(src, dest);
3936
3728
  return;
3937
3729
  } catch (e) {
3938
3730
  lastErr = e;
3939
3731
  const code = e && e.code || "";
3940
- const transient = code === "EPERM" || code === "EBUSY" || code === "ENOTEMPTY" || code === "EACCES";
3941
- 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;
3942
3734
  const backoff = Math.min(1e3, 50 * Math.pow(2, attempt));
3943
3735
  await new Promise((r) => setTimeout(r, backoff));
3944
3736
  attempt++;
3945
3737
  }
3946
3738
  }
3947
3739
  try {
3740
+ await fs10.access(src);
3948
3741
  const data = await fs10.readFile(src);
3742
+ const dir = path12.dirname(dest);
3743
+ await fs10.mkdir(dir, { recursive: true }).catch(() => {
3744
+ });
3949
3745
  await fs10.writeFile(dest, data);
3950
3746
  await fs10.unlink(src).catch(() => {
3951
3747
  });
3952
3748
  return;
3953
3749
  } catch (fallbackErr) {
3750
+ if (fallbackErr?.code === "ENOENT") {
3751
+ console.warn(`Session temp file disappeared: ${src}`);
3752
+ return;
3753
+ }
3954
3754
  throw lastErr || fallbackErr;
3955
3755
  }
3956
3756
  }
@@ -3978,7 +3778,7 @@ async function loadOrcreateSession(sessionId2) {
3978
3778
  return [sessionFile, [], []];
3979
3779
  }
3980
3780
  }
3981
- async function saveSessionHistory(sessionFile, history) {
3781
+ async function doSaveSessionHistory(sessionFile, history) {
3982
3782
  await withFileLock(sessionFile, async () => {
3983
3783
  let sessionData;
3984
3784
  try {
@@ -4028,9 +3828,12 @@ async function saveSessionHistory(sessionFile, history) {
4028
3828
  }
4029
3829
  });
4030
3830
  }
3831
+ async function saveSessionHistory(sessionFile, history) {
3832
+ debouncedSave(sessionFile, history);
3833
+ }
4031
3834
 
4032
3835
  // src/app/agent/core/prompt/prompt_builder.ts
4033
- import os7 from "os";
3836
+ import os6 from "os";
4034
3837
  import fs11 from "fs";
4035
3838
  import path13 from "path";
4036
3839
  import { execSync } from "child_process";
@@ -4225,12 +4028,6 @@ You are a **teammate who writes tests**, not an assistant who skips them.
4225
4028
  - **For edit_tool**: Provide exact content with correct whitespace (read first!)
4226
4029
  - **Check file exists** before attempting edits
4227
4030
 
4228
- ### Shell Commands:
4229
- - **NEVER use sudo** - Commands requiring sudo will be blocked
4230
- - **Long commands**: Set appropriate timeout (default 300s)
4231
- - **Interactive commands** (vim, less, ssh) are blocked - use alternatives
4232
- - **Large outputs** will be truncated at 100KB
4233
-
4234
4031
  ### Safe Auto-Approved Tools (no confirmation needed):
4235
4032
  - message_notify_user
4236
4033
  - ls_tool
@@ -4246,6 +4043,97 @@ You are a **teammate who writes tests**, not an assistant who skips them.
4246
4043
 
4247
4044
  ---
4248
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
+
4249
4137
  <communication_style>
4250
4138
  ## How You Communicate
4251
4139
 
@@ -4337,12 +4225,12 @@ Let's build something great, {username}.
4337
4225
  function getUnifiedSystemPrompt() {
4338
4226
  const cwd = process.cwd();
4339
4227
  const env = {
4340
- os_type: os7.type(),
4341
- os_version: os7.release(),
4342
- architecture: os7.arch(),
4228
+ os_type: os6.type(),
4229
+ os_version: os6.release(),
4230
+ architecture: os6.arch(),
4343
4231
  workdir: cwd,
4344
4232
  shell_type: process.env.SHELL || process.env.COMSPEC || "unknown",
4345
- username: os7.userInfo().username,
4233
+ username: os6.userInfo().username,
4346
4234
  current_date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
4347
4235
  timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
4348
4236
  is_git_repo: isGitRepo(cwd) ? "yes" : "no",
@@ -4728,7 +4616,7 @@ function getSubAgentByCommand(cmd) {
4728
4616
  }
4729
4617
 
4730
4618
  // src/app/agent/subagents/init/init_system_prompt.ts
4731
- import os8 from "os";
4619
+ import os7 from "os";
4732
4620
  var SYSTEM_PROMPT2 = `
4733
4621
 
4734
4622
  ### YOU ARE BluMa CLI \u2014 INIT SUBAGENT \u2014 AUTONOMOUS SENIOR SOFTWARE ENGINEER @ NOMADENGENUITY
@@ -4891,12 +4779,12 @@ Rule Summary:
4891
4779
  function getInitPrompt() {
4892
4780
  const now = /* @__PURE__ */ new Date();
4893
4781
  const collectedData = {
4894
- os_type: os8.type(),
4895
- os_version: os8.release(),
4896
- architecture: os8.arch(),
4782
+ os_type: os7.type(),
4783
+ os_version: os7.release(),
4784
+ architecture: os7.arch(),
4897
4785
  workdir: process.cwd(),
4898
4786
  shell_type: process.env.SHELL || process.env.COMSPEC || "Unknown",
4899
- username: os8.userInfo().username || "Unknown",
4787
+ username: os7.userInfo().username || "Unknown",
4900
4788
  current_date: now.toISOString().split("T")[0],
4901
4789
  // Formato YYYY-MM-DD
4902
4790
  timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "Unknown",
@@ -5149,7 +5037,7 @@ var SubAgentsBluMa = class {
5149
5037
  };
5150
5038
 
5151
5039
  // src/app/agent/agent.ts
5152
- var globalEnvPath = path15.join(os9.homedir(), ".bluma-cli", ".env");
5040
+ var globalEnvPath = path15.join(os8.homedir(), ".bluma-cli", ".env");
5153
5041
  dotenv.config({ path: globalEnvPath });
5154
5042
  var Agent = class {
5155
5043
  sessionId;
@@ -5527,18 +5415,6 @@ var renderViewFileOutline = ({ args }) => {
5527
5415
  ] })
5528
5416
  ] });
5529
5417
  };
5530
- var renderRunCommandAsync = ({ args }) => {
5531
- const parsed = parseArgs(args);
5532
- const command = parsed.command || "[no command]";
5533
- return /* @__PURE__ */ jsxs8(Box8, { paddingX: 1, children: [
5534
- /* @__PURE__ */ jsx8(Text8, { color: "white", bold: true, children: "async" }),
5535
- /* @__PURE__ */ jsx8(Text8, { color: "white", bold: true, children: " $" }),
5536
- /* @__PURE__ */ jsxs8(Text8, { children: [
5537
- " ",
5538
- truncate(command, 50)
5539
- ] })
5540
- ] });
5541
- };
5542
5418
  var renderCommandStatus = ({ args }) => {
5543
5419
  const parsed = parseArgs(args);
5544
5420
  const id = parsed.command_id || "[no id]";
@@ -5614,7 +5490,6 @@ var ToolRenderDisplay = {
5614
5490
  find_by_name: renderFindByName,
5615
5491
  grep_search: renderGrepSearch,
5616
5492
  view_file_outline: renderViewFileOutline,
5617
- run_command_async: renderRunCommandAsync,
5618
5493
  command_status: renderCommandStatus,
5619
5494
  task_boundary: renderTaskBoundary,
5620
5495
  search_web: renderSearchWeb
@@ -5844,10 +5719,20 @@ var ToolResultDisplayComponent = ({ toolName, result }) => {
5844
5719
  ] })
5845
5720
  ] });
5846
5721
  }
5847
- 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) {
5848
5723
  const output = parsed.stdout || parsed.output || "";
5849
5724
  const stderr = parsed.stderr || "";
5850
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
+ }
5851
5736
  if (!output && !stderr) return null;
5852
5737
  const { lines, truncated } = truncateLines(output || stderr, MAX_LINES);
5853
5738
  const isError = exitCode !== 0 || stderr;
@@ -5857,6 +5742,10 @@ var ToolResultDisplayComponent = ({ toolName, result }) => {
5857
5742
  "... +",
5858
5743
  truncated,
5859
5744
  " lines"
5745
+ ] }),
5746
+ exitCode !== 0 && /* @__PURE__ */ jsxs10(Text10, { dimColor: true, color: "red", children: [
5747
+ "exit code: ",
5748
+ exitCode
5860
5749
  ] })
5861
5750
  ] });
5862
5751
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nomad-e/bluma-cli",
3
- "version": "0.0.105",
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",