@nomad-e/bluma-cli 0.1.58 → 0.1.60

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.
@@ -3,7 +3,7 @@ Merge multiple PDF files into a single output PDF.
3
3
 
4
4
  Usage:
5
5
  python merge_pdfs.py --output merged.pdf file1.pdf file2.pdf file3.pdf
6
- python merge_pdfs.py --output ./artifacts/combined.pdf *.pdf
6
+ python merge_pdfs.py --output ./.bluma/artifacts/combined.pdf *.pdf
7
7
  """
8
8
  import argparse
9
9
  import sys
@@ -193,7 +193,7 @@ Example script header:
193
193
  {description of what this script does}
194
194
 
195
195
  Usage:
196
- python {script_name}.py --input data.csv --output ./artifacts/result.pdf
196
+ python {script_name}.py --input data.csv --output ./.bluma/artifacts/result.pdf
197
197
  """
198
198
  import argparse
199
199
  ```
package/dist/main.js CHANGED
@@ -113,6 +113,20 @@ function isPathInsideWorkspace(targetPath, policy = getSandboxPolicy()) {
113
113
  const relative = path6.relative(policy.workspaceRoot, resolved);
114
114
  return relative === "" || !relative.startsWith("..") && !path6.isAbsolute(relative);
115
115
  }
116
+ function redirectTopLevelArtifactsPath(resolvedAbsolute, workspaceRoot) {
117
+ const wr = path6.resolve(workspaceRoot);
118
+ const abs = path6.resolve(resolvedAbsolute);
119
+ const rel = path6.relative(wr, abs);
120
+ if (rel.startsWith("..") || path6.isAbsolute(rel)) {
121
+ return abs;
122
+ }
123
+ const segments = rel.split(path6.sep).filter((s) => s.length > 0);
124
+ if (segments.length === 0 || segments[0] !== "artifacts") {
125
+ return abs;
126
+ }
127
+ const tail = segments.slice(1);
128
+ return tail.length > 0 ? path6.join(wr, ".bluma", "artifacts", ...tail) : path6.join(wr, ".bluma", "artifacts");
129
+ }
116
130
  function resolveWorkspacePath(inputPath, policy = getSandboxPolicy()) {
117
131
  const candidate = path6.isAbsolute(inputPath) ? path6.resolve(inputPath) : path6.resolve(policy.workspaceRoot, inputPath);
118
132
  if (policy.isSandbox && !isPathInsideWorkspace(candidate, policy)) {
@@ -120,7 +134,7 @@ function resolveWorkspacePath(inputPath, policy = getSandboxPolicy()) {
120
134
  `Path "${inputPath}" escapes the sandbox workspace root ${policy.workspaceRoot}`
121
135
  );
122
136
  }
123
- return candidate;
137
+ return redirectTopLevelArtifactsPath(candidate, policy.workspaceRoot);
124
138
  }
125
139
  function resolveCommandCwd(cwd, policy = getSandboxPolicy()) {
126
140
  const base = cwd ? path6.resolve(cwd) : policy.workspaceRoot;
@@ -207,6 +221,9 @@ __export(async_command_exports, {
207
221
  import os6 from "os";
208
222
  import { spawn as spawn2 } from "child_process";
209
223
  import { v4 as uuidv42 } from "uuid";
224
+ function normalizeCommandId(raw) {
225
+ return String(raw ?? "").trim().replace(/^[#:\s]+/, "");
226
+ }
210
227
  function cleanupOldCommands() {
211
228
  if (runningCommands.size <= MAX_STORED_COMMANDS) return;
212
229
  const commands = Array.from(runningCommands.entries()).filter(([_, cmd]) => cmd.status !== "running").sort((a, b) => (a[1].endTime || 0) - (b[1].endTime || 0));
@@ -356,13 +373,14 @@ async function commandStatus(args) {
356
373
  error: "command_id is required"
357
374
  };
358
375
  }
359
- const entry = runningCommands.get(command_id);
376
+ const normalizedCommandId = normalizeCommandId(command_id);
377
+ const entry = runningCommands.get(normalizedCommandId);
360
378
  if (!entry) {
361
379
  return {
362
380
  success: false,
363
- command_id,
381
+ command_id: normalizedCommandId,
364
382
  status: "not_found",
365
- error: `Command with id "${command_id}" not found. It may have expired or never existed.`
383
+ error: `Command with id "${normalizedCommandId}" not found. It may have expired or never existed.`
366
384
  };
367
385
  }
368
386
  const maxWait = Math.min(wait_seconds, 15);
@@ -394,7 +412,7 @@ async function commandStatus(args) {
394
412
  const duration = entry.endTime ? (entry.endTime - entry.startTime) / 1e3 : (Date.now() - entry.startTime) / 1e3;
395
413
  return {
396
414
  success: true,
397
- command_id,
415
+ command_id: normalizedCommandId,
398
416
  status: entry.status,
399
417
  stdout: stdout || void 0,
400
418
  stderr: stderr || void 0,
@@ -420,11 +438,12 @@ async function sendCommandInput(args) {
420
438
  error: "command_id and input are required"
421
439
  };
422
440
  }
423
- const entry = runningCommands.get(command_id);
441
+ const normalizedCommandId = normalizeCommandId(command_id);
442
+ const entry = runningCommands.get(normalizedCommandId);
424
443
  if (!entry) {
425
444
  return {
426
445
  success: false,
427
- error: `Command with id "${command_id}" not found`
446
+ error: `Command with id "${normalizedCommandId}" not found`
428
447
  };
429
448
  }
430
449
  if (entry.status !== "running" || !entry.process) {
@@ -436,7 +455,7 @@ async function sendCommandInput(args) {
436
455
  entry.process.stdin?.write(input);
437
456
  return {
438
457
  success: true,
439
- message: `Sent ${input.length} characters to command ${command_id}`
458
+ message: `Sent ${input.length} characters to command ${normalizedCommandId}`
440
459
  };
441
460
  } catch (error) {
442
461
  return {
@@ -448,11 +467,12 @@ async function sendCommandInput(args) {
448
467
  async function killCommand(args) {
449
468
  try {
450
469
  const { command_id } = args;
451
- const entry = runningCommands.get(command_id);
470
+ const normalizedCommandId = normalizeCommandId(command_id);
471
+ const entry = runningCommands.get(normalizedCommandId);
452
472
  if (!entry) {
453
473
  return {
454
474
  success: false,
455
- error: `Command with id "${command_id}" not found`
475
+ error: `Command with id "${normalizedCommandId}" not found`
456
476
  };
457
477
  }
458
478
  if (entry.status !== "running" || !entry.process) {
@@ -466,7 +486,7 @@ async function killCommand(args) {
466
486
  entry.endTime = Date.now();
467
487
  return {
468
488
  success: true,
469
- message: `Command ${command_id} killed`
489
+ message: `Command ${normalizedCommandId} killed`
470
490
  };
471
491
  } catch (error) {
472
492
  return {
@@ -3913,7 +3933,7 @@ var renderCommandStatus = ({ args }) => {
3913
3933
  const parsed = parseArgs(args);
3914
3934
  const id = parsed.command_id || "[no id]";
3915
3935
  return /* @__PURE__ */ jsx7(Box7, { children: /* @__PURE__ */ jsxs7(Text7, { color: BLUMA_TERMINAL.muted, children: [
3916
- "#",
3936
+ "id ",
3917
3937
  id
3918
3938
  ] }) });
3919
3939
  };
@@ -8519,10 +8539,16 @@ var AdvancedFeedbackSystem = class {
8519
8539
  score: penalty,
8520
8540
  message: "You are attempting a direct message without a tool_call. All replies must contain tool_call.",
8521
8541
  correction: `
8522
- ## PROTOCOL VIOLATION \u2014 SERIOUS
8523
- You are sending a direct response without tool_call, which is strictly prohibited.
8524
- PENALTY APPLIED: ${penalty.toFixed(1)} points deducted.
8525
- You MUST always use tool_call without exception.
8542
+ ## PROTOCOL VIOLATION \u2014 STOP WRITING PLAIN ASSISTANT TEXT
8543
+
8544
+ You streamed or returned **user-visible markdown as assistant content** instead of using the **\`message\` tool**. That is prohibited and **does not end the turn** \u2014 the runtime will loop until timeout.
8545
+
8546
+ Do this **immediately** in your next step (single tool call, no prose outside tools):
8547
+
8548
+ - Call **\`message\`** with **\`message_type\`: \`"result"\`**, put the user-facing summary in **\`content\`**, and put deliverable paths in **\`attachments\`** (absolute paths).
8549
+
8550
+ Do **not** repeat the same summary as plain assistant text again.
8551
+ PENALTY APPLIED: ${penalty.toFixed(1)} points deducted.
8526
8552
  `.trim()
8527
8553
  };
8528
8554
  }
@@ -9945,6 +9971,7 @@ The \\\`message\\\` tool has TWO types \u2014 use them CORRECTLY:
9945
9971
  - **Use when**: Task is complete, artifacts ready for delivery
9946
9972
  - **Use ONCE per turn** \u2014 only at the very end
9947
9973
  - **Ends the turn** \u2014 agent waits for next input
9974
+ - **CRITICAL:** Plain assistant markdown (streaming or not) **does not** end the worker or close the HTTP job \u2014 only a \\\`message\\\` tool call with \\\`message_type: "result"\\\` does. If you only write text in chat, the process loops until **timeout** (e.g. 300s).
9948
9975
 
9949
9976
  #### \u274C WRONG: Using "info" to ask questions
9950
9977
  \\\`\\\`\\\`typescript
@@ -10063,9 +10090,18 @@ You (Bluma):
10063
10090
 
10064
10091
  - **Sandbox is safe** - You can't break the host system
10065
10092
  - **But workspace matters** - Don't pollute /workspace with junk files
10093
+ - **Deliverables path** - Never use a top-level \`./artifacts/\` folder in the job root; use \`./.bluma/artifacts/\` (or the \`artifacts_dir\` from \`task_boundary\`). Shell redirects must use that path \u2014 \`file_write\` remaps \`artifacts/...\` to \`.bluma/artifacts/...\` automatically.
10066
10094
  - **Clean up after yourself** - Remove temporary files when done
10067
10095
  - **Respect session boundaries** - Stay in your session workspace
10068
10096
 
10097
+ ### Job wall-clock timeout (orchestrator) \u2014 read this
10098
+
10099
+ The coordinator sets a **single deadline** for the whole stream (e.g. \`timeout_seconds: 60\`). The timer **starts at job start** and counts **everything**: first LLM call (often **30\u201360s+** of \u201CThinking\u201D), tools, follow-up LLM calls, and your final \`message\`+\`result\`.
10100
+
10101
+ - **60 seconds is usually too short** for \`generate_document\` / PDF / multi-step work \u2014 the job can die **after** \`file_write\` succeeds but **before** \`shell_command\` or \`message(result)\`, with \`Job excedeu 60.0s\` / exit \`-9\`. That is **not** proof the sandbox is broken; it means the **budget was too tight**.
10102
+ - Prefer asking coordinators (or docs) to use **\u2265180s** for document generation, **\u2265300s** for heavy tasks.
10103
+ - When a job times out, **do not** claim \u201Csandbox unavailable\u201D unless you have real infra evidence (connection errors, 5xx). Timeout = **deadline exceeded**, often fixable by **raising \`timeout_seconds\`** on the caller side.
10104
+
10069
10105
  ### You Represent the Platform
10070
10106
 
10071
10107
  - **Severino trusts you** - Don't let him down
@@ -10288,6 +10324,10 @@ Auto-generated map (may be stale after pull/install). Confirm with tools before
10288
10324
  <<<BLUMA_WORKSPACE_SNAPSHOT_BODY>>>
10289
10325
  </workspace_snapshot>
10290
10326
 
10327
+ <deliverables>
10328
+ **Local and sandbox:** generated artifacts (reports, PDFs, exports, plans you attach) must live under \`<workdir>/.bluma/\` \u2014 use \`.bluma/artifacts/\` (or the \`artifacts_dir\` path returned by \`task_boundary\` after starting a task). Do **not** create a top-level \`./artifacts/\` folder in the project root. \`file_write\` / \`edit_tool\` / \`read_file_lines\` automatically remap \`artifacts/...\` \u2192 \`.bluma/artifacts/...\`. For \`shell_command\` redirects (\`>\` / \`>>\`), target \`.bluma/artifacts/...\` explicitly.
10329
+ </deliverables>
10330
+
10291
10331
  <coding_memory>
10292
10332
  Persistent store (~/.bluma/coding_memory.json). Do not invent entries: \`list\` / \`search\` if unsure. \`<coding_memory_snapshot>\` is bootstrap only \u2014 after add/update/remove, list or search again. Operations: add | list | search | update (id) | remove (id), one mutating call at a time.
10293
10333
  </coding_memory>
@@ -10313,7 +10353,7 @@ Output is truncated (~30KB / ~200 lines); use head/tail or write to a file. Use
10313
10353
  The user **only** sees chat content you send through the \`message\` tool (\`content\` as Markdown). Bare assistant text is **not** a substitute \u2014 **you should use \`message\` liberally**.
10314
10354
 
10315
10355
  **Types**
10316
- - \`message_type: "result"\` \u2014 **ends the turn**: final answer, deliverable, or a **question** that needs a user reply; then the agent waits for the user.
10356
+ - \`message_type: "result"\` \u2014 **ends the turn**: final answer, deliverable, or a **question** that needs a user reply; then the agent waits for the user. **Sandbox/worker:** only this stops the job; writing markdown as normal assistant output does **not** finish the task and can cause a **timeout loop**.
10317
10357
  - \`message_type: "info"\` \u2014 **non-terminal**: shown in chat, does **not** end the turn. **Expected behavior:** call \`info\` **multiple times** in a single turn whenever there is something worth saying (even briefly). Under-using \`info\` is a **mistake** in this product.
10318
10358
 
10319
10359
  **\u26A0\uFE0F CRITICAL: "info" is for INFORMATION ONLY \u2014 NEVER for asking questions**
@@ -11449,6 +11489,8 @@ var BluMaAgent = class {
11449
11489
  factorRouterTurnClosed = false;
11450
11490
  /** Passos seguidos sem tool_calls nem texto visível (só raciocínio) — evita loop lento no mesmo turno. */
11451
11491
  emptyAssistantReplySteps = 0;
11492
+ /** Passos seguidos com texto do assistente sem tool_calls (violação de protocolo) — evita loop até timeout do job. */
11493
+ directTextProtocolSteps = 0;
11452
11494
  constructor(sessionId, eventBus, llm, mcpClient, feedbackSystem) {
11453
11495
  this.sessionId = sessionId;
11454
11496
  this.eventBus = eventBus;
@@ -11592,6 +11634,7 @@ var BluMaAgent = class {
11592
11634
  const userContent = buildUserMessageContent(inputText, process.cwd());
11593
11635
  this.history.push({ role: "user", content: userContent });
11594
11636
  this.emptyAssistantReplySteps = 0;
11637
+ this.directTextProtocolSteps = 0;
11595
11638
  this.eventBus.emit(
11596
11639
  "backend_message",
11597
11640
  buildTurnStartBackendMessage({
@@ -12149,6 +12192,7 @@ ${editData.error.display}`;
12149
12192
  this.history.push(normalizedMessage);
12150
12193
  if (normalizedMessage.tool_calls && normalizedMessage.tool_calls.length > 0) {
12151
12194
  this.emptyAssistantReplySteps = 0;
12195
+ this.directTextProtocolSteps = 0;
12152
12196
  const validToolCalls = normalizedMessage.tool_calls.filter(
12153
12197
  (call) => ToolCallNormalizer.isValidToolCall(call)
12154
12198
  );
@@ -12188,9 +12232,20 @@ ${editData.error.display}`;
12188
12232
  }
12189
12233
  } else if (trimmedText) {
12190
12234
  this.emptyAssistantReplySteps = 0;
12235
+ this.directTextProtocolSteps += 1;
12236
+ const MAX_DIRECT_TEXT_PROTOCOL = 3;
12191
12237
  if (!hasEmittedStart) {
12192
12238
  this.eventBus.emit("backend_message", { type: "assistant_message", content: accumulatedContent });
12193
12239
  }
12240
+ if (this.directTextProtocolSteps >= MAX_DIRECT_TEXT_PROTOCOL) {
12241
+ this.eventBus.emit("backend_message", {
12242
+ type: "error",
12243
+ message: 'Agent kept answering with plain assistant text instead of the `message` tool with message_type "result". Turn forcibly closed to avoid job timeout; fix prompts or model routing.'
12244
+ });
12245
+ await this.notifyFactorTurnEndIfNeeded("protocol_direct_text_exhausted");
12246
+ this.emitTurnCompleted();
12247
+ return;
12248
+ }
12194
12249
  const feedback = this.feedbackSystem.generateFeedback({
12195
12250
  event: "protocol_violation_direct_text",
12196
12251
  details: { violationContent: accumulatedContent }
@@ -12226,6 +12281,7 @@ ${editData.error.display}`;
12226
12281
  this.history.push(message2);
12227
12282
  if (message2.tool_calls && message2.tool_calls.length > 0) {
12228
12283
  this.emptyAssistantReplySteps = 0;
12284
+ this.directTextProtocolSteps = 0;
12229
12285
  const validToolCalls = message2.tool_calls.filter(
12230
12286
  (call) => ToolCallNormalizer.isValidToolCall(call)
12231
12287
  );
@@ -12265,7 +12321,18 @@ ${editData.error.display}`;
12265
12321
  }
12266
12322
  } else if (typeof message2.content === "string" && message2.content.trim()) {
12267
12323
  this.emptyAssistantReplySteps = 0;
12324
+ this.directTextProtocolSteps += 1;
12325
+ const MAX_DIRECT_TEXT_PROTOCOL = 3;
12268
12326
  this.eventBus.emit("backend_message", { type: "assistant_message", content: message2.content });
12327
+ if (this.directTextProtocolSteps >= MAX_DIRECT_TEXT_PROTOCOL) {
12328
+ this.eventBus.emit("backend_message", {
12329
+ type: "error",
12330
+ message: 'Agent kept answering with plain assistant text instead of the `message` tool with message_type "result". Turn forcibly closed to avoid job timeout.'
12331
+ });
12332
+ await this.notifyFactorTurnEndIfNeeded("protocol_direct_text_exhausted");
12333
+ this.emitTurnCompleted();
12334
+ return;
12335
+ }
12269
12336
  const feedback = this.feedbackSystem.generateFeedback({
12270
12337
  event: "protocol_violation_direct_text",
12271
12338
  details: { violationContent: message2.content }
@@ -14182,6 +14249,40 @@ var ToolResultDisplayComponent = ({
14182
14249
  }
14183
14250
  return /* @__PURE__ */ jsx12(ResultGutter, { children: /* @__PURE__ */ jsx12(MarkdownRenderer, { markdown: String(body) }) });
14184
14251
  }
14252
+ if (toolName.includes("ask_user_question")) {
14253
+ const success = parsed?.success === true;
14254
+ const selectedLabel = typeof parsed?.selected_label === "string" ? parsed.selected_label : "";
14255
+ const selectedIndex = typeof parsed?.selected_index === "number" ? parsed.selected_index : null;
14256
+ const questionIndex = typeof parsed?.question_index === "number" ? parsed.question_index : 0;
14257
+ const qs = Array.isArray(args?.questions) ? args.questions : [];
14258
+ const q = qs[questionIndex];
14259
+ const questionText = typeof q?.question === "string" ? q.question : "";
14260
+ if (success && selectedLabel) {
14261
+ return /* @__PURE__ */ jsx12(ResultGutter, { children: /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", children: [
14262
+ /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
14263
+ /* @__PURE__ */ jsx12(Text12, { bold: true, children: "Response" }),
14264
+ " \xB7 ",
14265
+ selectedLabel
14266
+ ] }),
14267
+ questionText ? /* @__PURE__ */ jsxs12(Text12, { dimColor: true, wrap: "wrap", children: [
14268
+ truncate3(questionText, 140),
14269
+ selectedIndex !== null ? ` \xB7 option ${selectedIndex + 1}` : ""
14270
+ ] }) : null
14271
+ ] }) });
14272
+ }
14273
+ if (parsed?.cancelled === true) {
14274
+ return /* @__PURE__ */ jsx12(ResultGutter, { children: /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
14275
+ /* @__PURE__ */ jsx12(Text12, { bold: true, children: "Response" }),
14276
+ " \xB7 cancelled by user"
14277
+ ] }) });
14278
+ }
14279
+ const err = typeof parsed?.error === "string" ? parsed.error : "";
14280
+ return /* @__PURE__ */ jsx12(ResultGutter, { children: /* @__PURE__ */ jsxs12(Text12, { color: err ? BLUMA_TERMINAL.err : void 0, dimColor: !err, wrap: "wrap", children: [
14281
+ /* @__PURE__ */ jsx12(Text12, { bold: true, children: "Response" }),
14282
+ " \xB7 ",
14283
+ err || "No answer returned"
14284
+ ] }) });
14285
+ }
14185
14286
  if (toolName.includes("file_write") && parsed) {
14186
14287
  return /* @__PURE__ */ jsx12(ResultGutter, { children: /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
14187
14288
  parsed.created ? "Created " : "Wrote to ",
@@ -15990,6 +16091,9 @@ Run: npm i -g ${BLUMA_PACKAGE_NAME} to update.`;
15990
16091
  }
15991
16092
  }
15992
16093
 
16094
+ // src/app/ui/App.tsx
16095
+ init_sandbox_policy();
16096
+
15993
16097
  // src/app/ui/components/UpdateNotice.tsx
15994
16098
  import { Box as Box17, Text as Text16 } from "ink";
15995
16099
  import { jsx as jsx18, jsxs as jsxs16 } from "react/jsx-runtime";
@@ -16640,7 +16744,7 @@ var AppComponent = ({ eventBus, sessionId, cliVersion }) => {
16640
16744
  const [liveToolArgs, setLiveToolArgs] = useState11(void 0);
16641
16745
  const [isReasoning, setIsReasoning] = useState11(false);
16642
16746
  const alwaysAcceptList = useRef6([]);
16643
- const workdir = process.cwd();
16747
+ const workdir = getSandboxPolicy().workspaceRoot;
16644
16748
  const turnStartedAtRef = useRef6(null);
16645
16749
  const [processingStartMs, setProcessingStartMs] = useState11(null);
16646
16750
  const markTurnStarted = useCallback4(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nomad-e/bluma-cli",
3
- "version": "0.1.58",
3
+ "version": "0.1.60",
4
4
  "description": "BluMa independent agent for automation and advanced software engineering.",
5
5
  "author": "Alex Fonseca",
6
6
  "license": "Apache-2.0",