@nomad-e/bluma-cli 0.1.23 → 0.1.25

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.
Files changed (3) hide show
  1. package/README.md +52 -10
  2. package/dist/main.js +381 -167
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -231,22 +231,33 @@ The JSON payload must follow this envelope:
231
231
 
232
232
  ```json
233
233
  {
234
- "message_id": "job-123", // Optional but recommended
235
- "from_agent": "sandbox-api", // Who is calling BluMa
236
- "to_agent": "bluma", // Target agent (for routing)
237
- "action": "generate_app", // High-level action label
238
- "context": { // Arbitrary JSON with task details
234
+ "session_id": "conv-uuid-stable", // Recomendado: mesma sessão entre jobs (histórico + workspace)
235
+ "message_id": "job-123", // Opcional mas recomendado
236
+ "from_agent": "sandbox-api",
237
+ "to_agent": "bluma",
238
+ "action": "generate_app",
239
+ "context": {
239
240
  "user_request": "Criar dashboard de vendas",
240
241
  "erp_models": ["sale.order"],
241
242
  "permissions": ["sales"]
242
243
  },
243
- "metadata": { // Free-form metadata for the orchestrator
244
+ "user_context": {
245
+ "userId": "13",
246
+ "userName": "Nome",
247
+ "userEmail": "user@example.com",
248
+ "companyId": "4",
249
+ "companyName": "Empresa",
250
+ "conversationId": null
251
+ },
252
+ "metadata": {
244
253
  "sandbox": true,
245
254
  "caller": "agiweb"
246
255
  }
247
256
  }
248
257
  ```
249
258
 
259
+ O campo **`user_context`** (opcional) é enviado ao FactorRouter nos headers `X-User-*` / `X-Company-*` para custos e auditoria. `context.user_request` (primeiros 300 caracteres) vai em `X-User-Message` (URL-encoded).
260
+
250
261
  Internally, BluMa will:
251
262
 
252
263
  - Initialize the agent with a dedicated `eventBus`.
@@ -479,10 +490,41 @@ Notes
479
490
  ---
480
491
 
481
492
  ## <a name="configuration-and-environment-variables"></a>Configuration and Environment Variables
482
- You must create a `.env` file (copy if needed from `.env.example`) with the following variable:
483
- - `OPENROUTER_API_KEY` (required; get from [openrouter.ai](https://openrouter.ai))
484
493
 
485
- And others required by your agent/context.
494
+ **Recommended:** set **`FACTOR_ROUTER_KEY`** and **`FACTOR_ROUTER_URL`** in your **user or system environment** (shell profile, Windows User env, CI secrets, etc.) so every process sees them.
495
+
496
+ BluMa also **loads** `~/.bluma/.env` if that file exists (optional merge via `dotenv`); use `.env.example` as a template only if you prefer a local file.
497
+
498
+ **LLM routing** uses the FactorRouter gateway (OpenAI-compatible API):
499
+
500
+ - `FACTOR_ROUTER_KEY` (required) — e.g. `sk-fai-...` from your FactorRouter admin
501
+ - `FACTOR_ROUTER_URL` (required) — gateway base URL (e.g. `http://host:8003/router-api`; the client appends `/v1` if missing)
502
+
503
+ These replace legacy `NOMAD_API_KEY`, `NOMAD_BASE_URL`, and `MODEL_NOMAD` (the router picks the model; requests use `model: "auto"`).
504
+
505
+ Optional: `BLUMA_SANDBOX`, `BLUMA_SANDBOX_NAME`, MCP tokens, etc.
506
+
507
+ ### FactorRouter — headers HTTP (CLI vs sandbox)
508
+
509
+ O SDK OpenAI (`openai` npm) envia metadados no **2.º argumento** da chamada `chat.completions.create(body, { headers })` — são **headers HTTP normais**, não um campo `extra_headers` no JSON do body.
510
+
511
+ **Modo CLI interativo** (Ink, sem envelope): em cada request ao gateway são acrescentados:
512
+
513
+ | Header | Conteúdo típico (exemplo) |
514
+ |--------|---------------------------|
515
+ | `X-Turn-Id` | UUID novo por turno (igual em todo o loop de tools desse turno) |
516
+ | `X-Session-Id` | ID da sessão BluMa (`~/.bluma/sessions/…`) |
517
+ | `X-Conversation-Id` | `null` |
518
+ | `X-User-Message` | Primeiros 300 caracteres do pedido (URL-encoded) |
519
+ | `X-User-Id` | MAC da 1.ª interface não-interna, ou `host:<hostname>` se não houver MAC útil |
520
+ | `X-User-Name` | Utilizador do SO (`os.userInfo().username`, URL-encoded) |
521
+ | `X-User-Email` | `null` |
522
+ | `X-Company-Id` | Igual a `X-User-Id` (identificador da máquina) |
523
+ | `X-Company-Name` | Igual (URL-encoded) |
524
+
525
+ **Privacidade (desenvolvedores):** na CLI, estes valores servem para **agregação de custos** no FactorRouter. Não substituem o utilizador real no **agent mode**: aí prevalece o bloco `user_context` do JSON (sandbox / Severino).
526
+
527
+ **Agent mode** (`bluma agent`): os mesmos nomes de header; valores vêm do envelope (`session_id`, `user_context`, `context.user_request`). Se `user_context` for omitido, user/company ficam `null` nos headers (não se usa a heurística MAC da CLI).
486
528
 
487
529
  Advanced config files are located in `src/app/agent/config/`.
488
530
 
@@ -495,7 +537,7 @@ Advanced config files are located in `src/app/agent/config/`.
495
537
  - Bundler: esbuild, with `esbuild-plugin-node-externals`
496
538
  - Test Runner: Jest 30 + babel-jest
497
539
  - Transpilers: Babel presets (env, react, typescript)
498
- - LLM/Agent: OpenRouter via API; MCP via `@modelcontextprotocol/sdk`
540
+ - LLM/Agent: FactorRouter (OpenAI-compatible API); MCP via `@modelcontextprotocol/sdk`
499
541
  - Config loading: dotenv
500
542
  - Utilities: uuid, diff, react-devtools-core
501
543
 
package/dist/main.js CHANGED
@@ -332,7 +332,7 @@ import React11 from "react";
332
332
  import { render } from "ink";
333
333
  import { EventEmitter as EventEmitter2 } from "events";
334
334
  import fs14 from "fs";
335
- import { v4 as uuidv43 } from "uuid";
335
+ import { v4 as uuidv46 } from "uuid";
336
336
 
337
337
  // src/app/ui/App.tsx
338
338
  import { useState as useState7, useEffect as useEffect6, useRef as useRef5, useCallback as useCallback2, memo as memo11 } from "react";
@@ -1485,7 +1485,7 @@ var ConfirmationPrompt = memo4(ConfirmationPromptComponent);
1485
1485
  // src/app/agent/agent.ts
1486
1486
  import * as dotenv from "dotenv";
1487
1487
  import path16 from "path";
1488
- import os9 from "os";
1488
+ import os10 from "os";
1489
1489
 
1490
1490
  // src/app/agent/tool_invoker.ts
1491
1491
  import { promises as fs8 } from "fs";
@@ -3704,6 +3704,7 @@ var AdvancedFeedbackSystem = class {
3704
3704
 
3705
3705
  // src/app/agent/bluma/core/bluma.ts
3706
3706
  import path15 from "path";
3707
+ import { v4 as uuidv43 } from "uuid";
3707
3708
 
3708
3709
  // src/app/agent/session_manager/session_manager.ts
3709
3710
  import path12 from "path";
@@ -3803,7 +3804,7 @@ async function loadOrcreateSession(sessionId) {
3803
3804
  await fs10.access(sessionFile);
3804
3805
  const fileContent = await fs10.readFile(sessionFile, "utf-8");
3805
3806
  const sessionData = JSON.parse(fileContent);
3806
- return [sessionFile, [], []];
3807
+ return [sessionFile, sessionData.conversation_history || [], []];
3807
3808
  } catch (error) {
3808
3809
  const newSessionData = {
3809
3810
  session_id: sessionId,
@@ -4913,21 +4914,243 @@ function createApiContextWindow(fullHistory, maxTurns) {
4913
4914
  return finalContext;
4914
4915
  }
4915
4916
 
4917
+ // src/app/agent/core/llm/llm.ts
4918
+ import os8 from "os";
4919
+ import OpenAI from "openai";
4920
+ function defaultBlumaUserContextInput(sessionId, userMessage) {
4921
+ const msg = String(userMessage || "").slice(0, 300);
4922
+ return {
4923
+ sessionId,
4924
+ conversationId: null,
4925
+ userMessage: msg,
4926
+ userId: null,
4927
+ userName: null,
4928
+ userEmail: null,
4929
+ companyId: null,
4930
+ companyName: null
4931
+ };
4932
+ }
4933
+ function getPreferredMacAddress() {
4934
+ try {
4935
+ const ifaces = os8.networkInterfaces();
4936
+ for (const name of Object.keys(ifaces)) {
4937
+ const addrs = ifaces[name];
4938
+ if (!addrs) continue;
4939
+ for (const addr of addrs) {
4940
+ if (addr.internal) continue;
4941
+ const mac = (addr.mac || "").toLowerCase();
4942
+ if (mac && mac !== "00:00:00:00:00:00") {
4943
+ return mac;
4944
+ }
4945
+ }
4946
+ }
4947
+ } catch {
4948
+ }
4949
+ try {
4950
+ return `host:${os8.hostname()}`;
4951
+ } catch {
4952
+ return "unknown";
4953
+ }
4954
+ }
4955
+ function defaultInteractiveCliUserContextInput(sessionId, userMessage) {
4956
+ const base = defaultBlumaUserContextInput(sessionId, userMessage);
4957
+ const machineId = getPreferredMacAddress();
4958
+ let userName = null;
4959
+ try {
4960
+ userName = os8.userInfo().username || null;
4961
+ } catch {
4962
+ userName = null;
4963
+ }
4964
+ return {
4965
+ ...base,
4966
+ userId: machineId,
4967
+ userName,
4968
+ userEmail: null,
4969
+ companyId: machineId,
4970
+ companyName: machineId
4971
+ };
4972
+ }
4973
+ function encodeHeader(value, maxLen = 300) {
4974
+ if (value == null || value === "") return "null";
4975
+ return encodeURIComponent(String(value).slice(0, maxLen));
4976
+ }
4977
+ function buildFactorHeaders(ctx) {
4978
+ return {
4979
+ "X-Turn-Id": ctx.turnId,
4980
+ "X-Session-Id": ctx.sessionId,
4981
+ "X-Conversation-Id": ctx.conversationId ?? "null",
4982
+ "X-User-Message": encodeHeader(ctx.userMessage),
4983
+ "X-User-Id": ctx.userId ?? "null",
4984
+ "X-User-Name": encodeHeader(ctx.userName),
4985
+ "X-User-Email": ctx.userEmail ?? "null",
4986
+ "X-Company-Id": ctx.companyId ?? "null",
4987
+ "X-Company-Name": encodeHeader(ctx.companyName)
4988
+ };
4989
+ }
4990
+ function normalizeFactorBaseUrl(raw) {
4991
+ let u = raw.trim().replace(/\/$/, "");
4992
+ u = u.replace(/^http:\/\/http:\/\//i, "http://");
4993
+ if (!u.endsWith("/v1")) {
4994
+ u = `${u}/v1`;
4995
+ }
4996
+ return u;
4997
+ }
4998
+ async function notifyFactorRouterTurnEnd(params) {
4999
+ const frUrl = (process.env.FACTOR_ROUTER_URL ?? "").trim();
5000
+ const frKey = (process.env.FACTOR_ROUTER_KEY ?? "").trim();
5001
+ if (!frUrl || !frKey) return;
5002
+ const base = normalizeFactorBaseUrl(frUrl);
5003
+ const url = `${base}/turns/${encodeURIComponent(params.turnId)}/end`;
5004
+ try {
5005
+ await fetch(url, {
5006
+ method: "POST",
5007
+ headers: {
5008
+ Authorization: `Bearer ${frKey}`,
5009
+ "Content-Type": "application/json",
5010
+ ...buildFactorHeaders(params.userContext)
5011
+ },
5012
+ body: JSON.stringify({ reason: params.reason ?? "message_result" })
5013
+ });
5014
+ } catch {
5015
+ }
5016
+ }
5017
+ function hasLegacyNomadEnvVars() {
5018
+ return Boolean(
5019
+ (process.env.NOMAD_API_KEY || "").trim() || (process.env.NOMAD_BASE_URL || "").trim() || (process.env.MODEL_NOMAD || "").trim()
5020
+ );
5021
+ }
5022
+ var LLMService = class {
5023
+ client;
5024
+ constructor() {
5025
+ const frUrl = (process.env.FACTOR_ROUTER_URL ?? "").trim();
5026
+ const frKey = (process.env.FACTOR_ROUTER_KEY ?? "").trim();
5027
+ if (!frUrl || !frKey) {
5028
+ let msg = "FACTOR_ROUTER_URL and FACTOR_ROUTER_KEY must be set in the environment (e.g. export in your shell profile or OS user variables).";
5029
+ if (hasLegacyNomadEnvVars()) {
5030
+ msg += " You still have NOMAD_API_KEY, NOMAD_BASE_URL, and/or MODEL_NOMAD set \u2014 remove those and use FactorRouter variables instead.";
5031
+ }
5032
+ throw new Error(msg);
5033
+ }
5034
+ const baseURL = normalizeFactorBaseUrl(frUrl);
5035
+ this.client = new OpenAI({
5036
+ apiKey: frKey,
5037
+ baseURL
5038
+ });
5039
+ }
5040
+ requestHeaders(ctx) {
5041
+ return buildFactorHeaders(ctx);
5042
+ }
5043
+ async chatCompletion(params) {
5044
+ if (!params.userContext) {
5045
+ throw new Error("LLMService.chatCompletion: userContext \xE9 obrigat\xF3rio");
5046
+ }
5047
+ const tools = params.tools;
5048
+ const hasTools = Array.isArray(tools) && tools.length > 0;
5049
+ const resp = await this.client.chat.completions.create(
5050
+ {
5051
+ model: "auto",
5052
+ messages: params.messages,
5053
+ tools: hasTools ? tools : void 0,
5054
+ tool_choice: hasTools ? "auto" : void 0,
5055
+ parallel_tool_calls: params.parallel_tool_calls ?? false,
5056
+ temperature: params.temperature ?? 0,
5057
+ max_tokens: params.max_tokens
5058
+ },
5059
+ { headers: this.requestHeaders(params.userContext) }
5060
+ );
5061
+ return resp;
5062
+ }
5063
+ /**
5064
+ * Streaming — mesmo turnId em todas as chamadas do loop de tools (via params.userContext).
5065
+ */
5066
+ async *chatCompletionStream(params) {
5067
+ if (!params.userContext) {
5068
+ throw new Error("LLMService.chatCompletionStream: userContext \xE9 obrigat\xF3rio");
5069
+ }
5070
+ const tools = params.tools;
5071
+ const hasTools = Array.isArray(tools) && tools.length > 0;
5072
+ const stream = await this.client.chat.completions.create(
5073
+ {
5074
+ model: "auto",
5075
+ messages: params.messages,
5076
+ tools: hasTools ? tools : void 0,
5077
+ tool_choice: hasTools ? "auto" : void 0,
5078
+ parallel_tool_calls: params.parallel_tool_calls ?? false,
5079
+ temperature: params.temperature ?? 0,
5080
+ max_tokens: params.max_tokens,
5081
+ stream: true
5082
+ },
5083
+ { headers: this.requestHeaders(params.userContext) }
5084
+ );
5085
+ const toolCallsAccumulator = /* @__PURE__ */ new Map();
5086
+ for await (const chunk of stream) {
5087
+ const choice = chunk.choices[0];
5088
+ if (!choice) continue;
5089
+ const delta = choice.delta;
5090
+ if (delta?.tool_calls) {
5091
+ for (const tc of delta.tool_calls) {
5092
+ const idx = tc.index;
5093
+ if (!toolCallsAccumulator.has(idx)) {
5094
+ toolCallsAccumulator.set(idx, {
5095
+ id: tc.id || "",
5096
+ type: tc.type || "function",
5097
+ function: { name: "", arguments: "" }
5098
+ });
5099
+ }
5100
+ const acc = toolCallsAccumulator.get(idx);
5101
+ if (tc.id) acc.id = tc.id;
5102
+ if (tc.function?.name) acc.function.name = tc.function.name;
5103
+ if (tc.function?.arguments) acc.function.arguments += tc.function.arguments;
5104
+ }
5105
+ }
5106
+ const reasoning = delta?.reasoning_content || delta?.reasoning || "";
5107
+ yield {
5108
+ delta: delta?.content || "",
5109
+ reasoning,
5110
+ tool_calls: choice.finish_reason === "tool_calls" ? Array.from(toolCallsAccumulator.values()) : void 0,
5111
+ finish_reason: choice.finish_reason
5112
+ };
5113
+ }
5114
+ if (toolCallsAccumulator.size > 0) {
5115
+ yield {
5116
+ delta: "",
5117
+ tool_calls: Array.from(toolCallsAccumulator.values()),
5118
+ finish_reason: "tool_calls"
5119
+ };
5120
+ }
5121
+ }
5122
+ /** Compat: modelo efectivo é decidido pelo gateway (`auto`). */
5123
+ getDefaultModel() {
5124
+ return "auto";
5125
+ }
5126
+ };
5127
+
4916
5128
  // src/app/agent/core/llm/tool_call_normalizer.ts
4917
5129
  import { randomUUID } from "crypto";
4918
5130
  var ToolCallNormalizer = class {
5131
+ /**
5132
+ * Com tool_calls e sem texto visível: content deve ser null (API OpenAI-compatible), nunca "" nem undefined.
5133
+ */
5134
+ static assistantContentWithToolCalls(content) {
5135
+ if (content === void 0 || content === null) return null;
5136
+ if (typeof content === "string") return content.trim() === "" ? null : content;
5137
+ return null;
5138
+ }
4919
5139
  /**
4920
5140
  * Normaliza a mensagem do assistant, convertendo diferentes formatos de tool calls
4921
5141
  */
4922
5142
  static normalizeAssistantMessage(message2) {
4923
5143
  if (message2.tool_calls && this.isOpenAIFormat(message2.tool_calls)) {
4924
- return message2;
5144
+ return {
5145
+ ...message2,
5146
+ content: this.assistantContentWithToolCalls(message2.content)
5147
+ };
4925
5148
  }
4926
5149
  const toolCalls = this.extractToolCalls(message2);
4927
5150
  if (toolCalls.length > 0) {
4928
5151
  return {
4929
5152
  role: message2.role || "assistant",
4930
- content: message2.content || null,
5153
+ content: this.assistantContentWithToolCalls(message2.content),
4931
5154
  tool_calls: toolCalls
4932
5155
  };
4933
5156
  }
@@ -5064,7 +5287,6 @@ var ToolCallNormalizer = class {
5064
5287
  // src/app/agent/bluma/core/bluma.ts
5065
5288
  var BluMaAgent = class {
5066
5289
  llm;
5067
- model;
5068
5290
  sessionId;
5069
5291
  sessionFile = "";
5070
5292
  history = [];
@@ -5074,11 +5296,12 @@ var BluMaAgent = class {
5074
5296
  skillLoader;
5075
5297
  maxContextTurns = 5;
5076
5298
  isInterrupted = false;
5077
- constructor(sessionId, eventBus, llm, model, mcpClient, feedbackSystem) {
5299
+ /** Mesmo turnId durante processTurn + todo o loop de tool_calls (FactorRouter). */
5300
+ activeTurnContext = null;
5301
+ constructor(sessionId, eventBus, llm, mcpClient, feedbackSystem) {
5078
5302
  this.sessionId = sessionId;
5079
5303
  this.eventBus = eventBus;
5080
5304
  this.llm = llm;
5081
- this.model = model;
5082
5305
  this.mcpClient = mcpClient;
5083
5306
  this.feedbackSystem = feedbackSystem;
5084
5307
  this.skillLoader = new SkillLoader(process.cwd());
@@ -5143,9 +5366,15 @@ var BluMaAgent = class {
5143
5366
  getUiToolsDetailed() {
5144
5367
  return this.mcpClient.getAvailableToolsDetailed();
5145
5368
  }
5146
- async processTurn(userInput) {
5369
+ async processTurn(userInput, userContextInput) {
5147
5370
  this.isInterrupted = false;
5148
5371
  const inputText = String(userInput.content || "").trim();
5372
+ const turnId = uuidv43();
5373
+ this.activeTurnContext = {
5374
+ ...userContextInput,
5375
+ turnId,
5376
+ sessionId: userContextInput.sessionId || this.sessionId
5377
+ };
5149
5378
  this.history.push({ role: "user", content: inputText });
5150
5379
  if (inputText === "/init") {
5151
5380
  this.eventBus.emit("dispatch", inputText);
@@ -5265,6 +5494,13 @@ var BluMaAgent = class {
5265
5494
  try {
5266
5495
  const resultObj = typeof toolResultContent === "string" ? JSON.parse(toolResultContent) : toolResultContent;
5267
5496
  if (resultObj.message_type === "result") {
5497
+ if (this.activeTurnContext) {
5498
+ await notifyFactorRouterTurnEnd({
5499
+ turnId: this.activeTurnContext.turnId,
5500
+ userContext: this.activeTurnContext,
5501
+ reason: "message_result"
5502
+ });
5503
+ }
5268
5504
  shouldContinueConversation = false;
5269
5505
  this.eventBus.emit("backend_message", { type: "done", status: "completed" });
5270
5506
  }
@@ -5302,6 +5538,12 @@ ${editData.error.display}`;
5302
5538
  return `An unexpected error occurred while generating the edit preview: ${e.message}`;
5303
5539
  }
5304
5540
  }
5541
+ getLlmUserContext() {
5542
+ if (!this.activeTurnContext) {
5543
+ throw new Error("BluMaAgent: activeTurnContext ausente (processTurn n\xE3o iniciou o turno).");
5544
+ }
5545
+ return this.activeTurnContext;
5546
+ }
5305
5547
  async _continueConversation() {
5306
5548
  try {
5307
5549
  if (this.isInterrupted) {
@@ -5330,12 +5572,11 @@ ${editData.error.display}`;
5330
5572
  let hasEmittedStart = false;
5331
5573
  let reasoningContent = "";
5332
5574
  const stream = llmService.chatCompletionStream({
5333
- model: this.model,
5334
5575
  messages: contextWindow,
5335
5576
  temperature: 0,
5336
5577
  tools: this.mcpClient.getAvailableTools(),
5337
- //tool_choice: 'required',
5338
- parallel_tool_calls: false
5578
+ parallel_tool_calls: false,
5579
+ userContext: this.getLlmUserContext()
5339
5580
  });
5340
5581
  for await (const chunk of stream) {
5341
5582
  if (this.isInterrupted) {
@@ -5368,11 +5609,14 @@ ${editData.error.display}`;
5368
5609
  }
5369
5610
  }
5370
5611
  }
5612
+ const trimmedText = accumulatedContent.trim();
5613
+ const hasToolCalls = Boolean(toolCalls && toolCalls.length > 0);
5614
+ const content = trimmedText === "" ? null : accumulatedContent;
5371
5615
  const message2 = {
5372
5616
  role: "assistant",
5373
- content: accumulatedContent || null
5617
+ content
5374
5618
  };
5375
- if (toolCalls && toolCalls.length > 0) {
5619
+ if (hasToolCalls) {
5376
5620
  message2.tool_calls = toolCalls;
5377
5621
  }
5378
5622
  const normalizedMessage = ToolCallNormalizer.normalizeAssistantMessage(message2);
@@ -5420,7 +5664,7 @@ ${editData.error.display}`;
5420
5664
  this.eventBus.emit("backend_message", { type: "confirmation_request", tool_calls: validToolCalls });
5421
5665
  }
5422
5666
  }
5423
- } else if (accumulatedContent) {
5667
+ } else if (trimmedText) {
5424
5668
  this.eventBus.emit("backend_message", { type: "assistant_message", content: accumulatedContent });
5425
5669
  const feedback = this.feedbackSystem.generateFeedback({
5426
5670
  event: "protocol_violation_direct_text",
@@ -5435,12 +5679,11 @@ ${editData.error.display}`;
5435
5679
  }
5436
5680
  async _handleNonStreamingResponse(contextWindow) {
5437
5681
  const response = await this.llm.chatCompletion({
5438
- model: this.model,
5439
5682
  messages: contextWindow,
5440
5683
  temperature: 0,
5441
5684
  tools: this.mcpClient.getAvailableTools(),
5442
- //tool_choice: 'required',
5443
- parallel_tool_calls: false
5685
+ parallel_tool_calls: false,
5686
+ userContext: this.getLlmUserContext()
5444
5687
  });
5445
5688
  if (this.isInterrupted) {
5446
5689
  this.eventBus.emit("backend_message", { type: "info", message: "Agent task cancelled by user." });
@@ -5493,7 +5736,7 @@ ${editData.error.display}`;
5493
5736
  this.eventBus.emit("backend_message", { type: "confirmation_request", tool_calls: validToolCalls });
5494
5737
  }
5495
5738
  }
5496
- } else if (message2.content) {
5739
+ } else if (typeof message2.content === "string" && message2.content.trim()) {
5497
5740
  this.eventBus.emit("backend_message", { type: "assistant_message", content: message2.content });
5498
5741
  const feedback = this.feedbackSystem.generateFeedback({
5499
5742
  event: "protocol_violation_direct_text",
@@ -5509,105 +5752,6 @@ ${editData.error.display}`;
5509
5752
  }
5510
5753
  };
5511
5754
 
5512
- // src/app/agent/core/llm/llm.ts
5513
- import OpenAI from "openai";
5514
- var LLMService = class {
5515
- client;
5516
- defaultModel;
5517
- constructor(config2) {
5518
- this.client = new OpenAI({
5519
- apiKey: config2.apiKey,
5520
- baseURL: config2.baseUrl || "",
5521
- defaultHeaders: {
5522
- "HTTP-Referer": "https://bluma.ai",
5523
- // Optional. Site URL for rankings on openrouter.ai.
5524
- "X-Title": "Bluma"
5525
- // Optional. Site title for rankings on openrouter.ai.
5526
- }
5527
- });
5528
- this.defaultModel = config2.model || "";
5529
- }
5530
- /**
5531
- * Chamada tradicional (não-streaming) - retorna resposta completa
5532
- */
5533
- async chatCompletion(params) {
5534
- const resp = await this.client.chat.completions.create({
5535
- model: params.model || this.defaultModel,
5536
- messages: params.messages,
5537
- tools: params.tools,
5538
- //tool_choice: params.tool_choice,
5539
- parallel_tool_calls: params.parallel_tool_calls,
5540
- temperature: params.temperature,
5541
- max_tokens: params.max_tokens
5542
- });
5543
- return resp;
5544
- }
5545
- /**
5546
- * Chamada com streaming - retorna chunks em tempo real
5547
- *
5548
- * Uso:
5549
- * ```
5550
- * for await (const chunk of llm.chatCompletionStream(params)) {
5551
- * process.stdout.write(chunk.delta);
5552
- * }
5553
- * ```
5554
- */
5555
- async *chatCompletionStream(params) {
5556
- const stream = await this.client.chat.completions.create({
5557
- model: params.model || this.defaultModel,
5558
- messages: params.messages,
5559
- tools: params.tools,
5560
- //tool_choice: params.tool_choice,
5561
- parallel_tool_calls: params.parallel_tool_calls,
5562
- temperature: params.temperature,
5563
- max_tokens: params.max_tokens,
5564
- stream: true
5565
- });
5566
- const toolCallsAccumulator = /* @__PURE__ */ new Map();
5567
- for await (const chunk of stream) {
5568
- const choice = chunk.choices[0];
5569
- if (!choice) continue;
5570
- const delta = choice.delta;
5571
- if (delta?.tool_calls) {
5572
- for (const tc of delta.tool_calls) {
5573
- const idx = tc.index;
5574
- if (!toolCallsAccumulator.has(idx)) {
5575
- toolCallsAccumulator.set(idx, {
5576
- id: tc.id || "",
5577
- type: tc.type || "function",
5578
- function: { name: "", arguments: "" }
5579
- });
5580
- }
5581
- const acc = toolCallsAccumulator.get(idx);
5582
- if (tc.id) acc.id = tc.id;
5583
- if (tc.function?.name) acc.function.name = tc.function.name;
5584
- if (tc.function?.arguments) acc.function.arguments += tc.function.arguments;
5585
- }
5586
- }
5587
- const reasoning = delta?.reasoning_content || delta?.reasoning || "";
5588
- yield {
5589
- delta: delta?.content || "",
5590
- reasoning,
5591
- tool_calls: choice.finish_reason === "tool_calls" ? Array.from(toolCallsAccumulator.values()) : void 0,
5592
- finish_reason: choice.finish_reason
5593
- };
5594
- }
5595
- if (toolCallsAccumulator.size > 0) {
5596
- yield {
5597
- delta: "",
5598
- tool_calls: Array.from(toolCallsAccumulator.values()),
5599
- finish_reason: "tool_calls"
5600
- };
5601
- }
5602
- }
5603
- /**
5604
- * Retorna o modelo padrão configurado
5605
- */
5606
- getDefaultModel() {
5607
- return this.defaultModel;
5608
- }
5609
- };
5610
-
5611
5755
  // src/app/agent/subagents/registry.ts
5612
5756
  var subAgentRegistry = {};
5613
5757
  function registerSubAgent(subAgent) {
@@ -5619,8 +5763,14 @@ function getSubAgentByCommand(cmd) {
5619
5763
  return subAgentRegistry[cmd];
5620
5764
  }
5621
5765
 
5766
+ // src/app/agent/subagents/init/init_subagent.ts
5767
+ import { v4 as uuidv45 } from "uuid";
5768
+
5769
+ // src/app/agent/subagents/base_llm_subagent.ts
5770
+ import { v4 as uuidv44 } from "uuid";
5771
+
5622
5772
  // src/app/agent/subagents/init/init_system_prompt.ts
5623
- import os8 from "os";
5773
+ import os9 from "os";
5624
5774
  var SYSTEM_PROMPT2 = `
5625
5775
 
5626
5776
  ### YOU ARE BluMa CLI \u2014 INIT SUBAGENT \u2014 AUTONOMOUS SENIOR SOFTWARE ENGINEER @ NOMADENGENUITY
@@ -5783,12 +5933,12 @@ Rule Summary:
5783
5933
  function getInitPrompt() {
5784
5934
  const now = /* @__PURE__ */ new Date();
5785
5935
  const collectedData = {
5786
- os_type: os8.type(),
5787
- os_version: os8.release(),
5788
- architecture: os8.arch(),
5936
+ os_type: os9.type(),
5937
+ os_version: os9.release(),
5938
+ architecture: os9.arch(),
5789
5939
  workdir: process.cwd(),
5790
5940
  shell_type: process.env.SHELL || process.env.COMSPEC || "Unknown",
5791
- username: os8.userInfo().username || "Unknown",
5941
+ username: os9.userInfo().username || "Unknown",
5792
5942
  current_date: now.toISOString().split("T")[0],
5793
5943
  // Formato YYYY-MM-DD
5794
5944
  timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "Unknown",
@@ -5822,6 +5972,8 @@ var BaseLLMSubAgent = class {
5822
5972
  sessionFile = "";
5823
5973
  maxContextTurns = 160;
5824
5974
  isInterrupted = false;
5975
+ /** Um turnId por execute(); reutilizado em todo o loop de tools do subagente. */
5976
+ subagentTurnContext = null;
5825
5977
  async execute(input, ctx) {
5826
5978
  this.ctx = ctx;
5827
5979
  this.isInterrupted = false;
@@ -5829,7 +5981,15 @@ var BaseLLMSubAgent = class {
5829
5981
  this.isInterrupted = true;
5830
5982
  });
5831
5983
  await this.initializeHistory();
5832
- this.history.push({ role: "user", content: typeof input === "string" ? input : JSON.stringify(input) });
5984
+ const rawUser = typeof input === "string" ? input : JSON.stringify(input);
5985
+ const base = ctx.blumaUserContextInput ?? defaultBlumaUserContextInput(`subagent:${this.id}`, rawUser.slice(0, 300));
5986
+ const turnId = uuidv44();
5987
+ this.subagentTurnContext = {
5988
+ ...base,
5989
+ turnId,
5990
+ sessionId: base.sessionId || `subagent:${this.id}`
5991
+ };
5992
+ this.history.push({ role: "user", content: rawUser });
5833
5993
  await this._continueConversation();
5834
5994
  return { history: this.history };
5835
5995
  }
@@ -5868,12 +6028,14 @@ ${editData.error.display}`;
5868
6028
  return;
5869
6029
  }
5870
6030
  const contextWindow = this.history.slice(-this.maxContextTurns);
6031
+ if (!this.subagentTurnContext) {
6032
+ throw new Error("BaseLLMSubAgent: subagentTurnContext n\xE3o inicializado");
6033
+ }
5871
6034
  const response = await this.ctx.llm.chatCompletion({
5872
- model: this.ctx.policy?.llmDeployment || "default",
5873
6035
  messages: contextWindow,
5874
6036
  tools: this.ctx.mcpClient.getAvailableTools(),
5875
- //tool_choice: 'required',
5876
- parallel_tool_calls: false
6037
+ parallel_tool_calls: false,
6038
+ userContext: this.subagentTurnContext
5877
6039
  });
5878
6040
  if (this.isInterrupted) {
5879
6041
  this.emitEvent("info", { message: "SubAgent task cancelled byuserr." });
@@ -5963,6 +6125,13 @@ var InitAgentImpl = class extends BaseLLMSubAgent {
5963
6125
  this.isInterrupted = true;
5964
6126
  });
5965
6127
  await this.initializeHistory();
6128
+ const preview = typeof input === "string" ? input.slice(0, 300) : JSON.stringify(input ?? "").slice(0, 300);
6129
+ const base = ctx.blumaUserContextInput ?? defaultBlumaUserContextInput(`subagent:${this.id}`, preview);
6130
+ this.subagentTurnContext = {
6131
+ ...base,
6132
+ turnId: uuidv45(),
6133
+ sessionId: base.sessionId || `subagent:${this.id}`
6134
+ };
5966
6135
  const seed = `
5967
6136
  Scan the current project repository comprehensively.
5968
6137
  Map the directory structure (excluding heavy/ignored folders), infer the technology stack from manifests and configs, identify entry points and useful scripts, and analyze module relationships and architectural patterns.
@@ -5985,7 +6154,6 @@ var SubAgentsBluMa = class {
5985
6154
  mcpClient;
5986
6155
  toolInvoker;
5987
6156
  llm;
5988
- model;
5989
6157
  projectRoot;
5990
6158
  policy;
5991
6159
  logger;
@@ -5994,7 +6162,6 @@ var SubAgentsBluMa = class {
5994
6162
  this.mcpClient = params.mcpClient;
5995
6163
  this.toolInvoker = params.toolInvoker;
5996
6164
  this.llm = params.llm;
5997
- this.model = params.model;
5998
6165
  this.projectRoot = params.projectRoot || process.cwd();
5999
6166
  this.policy = params.policy;
6000
6167
  this.logger = params.logger;
@@ -6002,7 +6169,7 @@ var SubAgentsBluMa = class {
6002
6169
  // Recebe dados do front (ex.: { content: inputText } vindo de /init)
6003
6170
  // e faz o roteamento para o subagente adequado com base no comando.
6004
6171
  async registerAndDispatch(frontPayload) {
6005
- const { command, content, ...rest } = frontPayload || {};
6172
+ const { command, content, userContext, ...rest } = frontPayload || {};
6006
6173
  const resolvedCommand = this.resolveCommand({ command, content });
6007
6174
  if (!resolvedCommand) {
6008
6175
  this.emit("error", { message: "Nenhum comando/subagente correspondente encontrado." });
@@ -6013,16 +6180,18 @@ var SubAgentsBluMa = class {
6013
6180
  this.emit("error", { message: `Subagente n\xE3o registrado para ${resolvedCommand}` });
6014
6181
  return { ok: false, error: "unregistered_subagent" };
6015
6182
  }
6183
+ const inputForAgent = content ?? JSON.stringify({ content, ...rest });
6184
+ const msgPreview = String(inputForAgent).slice(0, 300);
6016
6185
  const ctx = {
6017
6186
  projectRoot: this.projectRoot,
6018
6187
  eventBus: this.eventBus,
6019
6188
  mcpClient: this.mcpClient,
6020
6189
  toolInvoker: this.toolInvoker,
6021
6190
  llm: this.llm,
6022
- policy: { llmDeployment: this.model, ...this.policy || {} },
6023
- logger: this.logger
6191
+ policy: { llmDeployment: "auto", ...this.policy || {} },
6192
+ logger: this.logger,
6193
+ blumaUserContextInput: userContext ?? defaultBlumaUserContextInput(`subagent:${resolvedCommand}`, msgPreview)
6024
6194
  };
6025
- const inputForAgent = content ?? JSON.stringify({ content, ...rest });
6026
6195
  this.emit("info", { message: `[SubAgentsBluMa] Dispatch -> ${resolvedCommand}` });
6027
6196
  return subAgent.execute(inputForAgent, ctx);
6028
6197
  }
@@ -6055,17 +6224,18 @@ var RouteManager = class {
6055
6224
  }
6056
6225
  async handleRoute(payload) {
6057
6226
  const inputText = String(payload.content || "").trim();
6227
+ const { userContext } = payload;
6058
6228
  for (const [path18, handler] of this.routeHandlers) {
6059
6229
  if (inputText === path18 || inputText.startsWith(`${path18} `)) {
6060
- return handler({ content: inputText });
6230
+ return handler({ content: inputText, userContext });
6061
6231
  }
6062
6232
  }
6063
- await this.core.processTurn({ content: inputText });
6233
+ await this.core.processTurn({ content: inputText }, userContext);
6064
6234
  }
6065
6235
  };
6066
6236
 
6067
6237
  // src/app/agent/agent.ts
6068
- var globalEnvPath = path16.join(os9.homedir(), ".bluma", ".env");
6238
+ var globalEnvPath = path16.join(os10.homedir(), ".bluma", ".env");
6069
6239
  dotenv.config({ path: globalEnvPath });
6070
6240
  var Agent = class {
6071
6241
  sessionId;
@@ -6074,7 +6244,6 @@ var Agent = class {
6074
6244
  feedbackSystem;
6075
6245
  llm;
6076
6246
  routeManager;
6077
- model;
6078
6247
  core;
6079
6248
  subAgents;
6080
6249
  toolInvoker;
@@ -6085,55 +6254,77 @@ var Agent = class {
6085
6254
  this.toolInvoker = nativeToolInvoker;
6086
6255
  this.mcpClient = new MCPClient(nativeToolInvoker, eventBus);
6087
6256
  this.feedbackSystem = new AdvancedFeedbackSystem();
6088
- const apiKey = process.env.NOMAD_API_KEY;
6089
- const baseUrl = process.env.NOMAD_BASE_URL;
6090
- this.model = process.env.MODEL_NOMAD || "";
6091
- const requiredEnvVars = ["NOMAD_API_KEY", "NOMAD_BASE_URL"];
6092
- const missingEnvVars = requiredEnvVars.filter((v) => !process.env[v]);
6257
+ const requiredEnvVars = ["FACTOR_ROUTER_KEY", "FACTOR_ROUTER_URL"];
6258
+ const missingEnvVars = requiredEnvVars.filter((v) => !process.env[v]?.trim());
6093
6259
  if (missingEnvVars.length > 0) {
6094
6260
  const platform = process.platform;
6095
6261
  let guidance = "";
6096
6262
  if (platform === "win32") {
6097
6263
  guidance = [
6098
- "Windows (PowerShell):",
6264
+ "Current PowerShell session (quick test):",
6099
6265
  ...missingEnvVars.map((v) => ` $env:${v}="<your-value>"`),
6100
- " # Para persistir:",
6101
- ...missingEnvVars.map((v) => ` [System.Environment]::SetEnvironmentVariable("${v}", "<your-value>", "User")`)
6266
+ "",
6267
+ "Persistent (User environment \u2014 restart terminal after):",
6268
+ ...missingEnvVars.map(
6269
+ (v) => ` [System.Environment]::SetEnvironmentVariable("${v}", "<your-value>", "User")`
6270
+ )
6102
6271
  ].join("\n");
6103
- } else if (platform === "darwin" || platform === "linux") {
6272
+ } else if (platform === "darwin" || platform === "linux" || platform === "freebsd") {
6273
+ const osLabel = platform === "darwin" ? "macOS" : platform === "linux" ? "Linux" : "BSD";
6274
+ const sh = (process.env.SHELL || "").toLowerCase();
6275
+ let rcFile = "~/.profile";
6276
+ if (sh.includes("zsh")) rcFile = "~/.zshrc";
6277
+ else if (sh.includes("bash")) rcFile = platform === "darwin" ? "~/.bash_profile" : "~/.bashrc";
6104
6278
  guidance = [
6105
- "macOS/Linux (bash/zsh):",
6106
- ...missingEnvVars.map((v) => ` echo 'export ${v}="<your-value>"' >> ~/.bashrc`),
6107
- " source ~/.bashrc"
6279
+ `${osLabel} \u2014 current shell only (quick test):`,
6280
+ ...missingEnvVars.map((v) => ` export ${v}="<your-value>"`),
6281
+ "",
6282
+ `${osLabel} \u2014 persistent (append to ${rcFile} for your current $SHELL):`,
6283
+ ...missingEnvVars.map((v) => ` echo 'export ${v}="<your-value>"' >> ${rcFile}`),
6284
+ ` source ${rcFile} # or open a new terminal`
6108
6285
  ].join("\n");
6109
6286
  } else {
6110
- guidance = missingEnvVars.map((v) => ` export ${v}="<your-value>"`).join("\n");
6287
+ guidance = [
6288
+ `${platform} \u2014 current shell:`,
6289
+ ...missingEnvVars.map((v) => ` export ${v}="<your-value>"`)
6290
+ ].join("\n");
6111
6291
  }
6112
- const message2 = [
6292
+ const messageParts = [
6113
6293
  `Missing required environment variables: ${missingEnvVars.join(", ")}.`,
6114
- `Configure them globally using the commands below, or set them in: ${globalEnvPath}`,
6115
6294
  "",
6116
- guidance
6117
- ].join("\n");
6295
+ "Set them in your user or system environment so the BluMa process inherits them."
6296
+ ];
6297
+ if (hasLegacyNomadEnvVars()) {
6298
+ messageParts.push(
6299
+ "",
6300
+ "You still have NOMAD_API_KEY, NOMAD_BASE_URL, and/or MODEL_NOMAD set. Remove those and use FACTOR_ROUTER_KEY and FACTOR_ROUTER_URL instead."
6301
+ );
6302
+ }
6303
+ messageParts.push("", guidance);
6304
+ const message2 = messageParts.join("\n");
6118
6305
  this.eventBus.emit("backend_message", {
6119
6306
  type: "error",
6120
6307
  code: "missing_env",
6121
6308
  missing: missingEnvVars,
6122
- path: globalEnvPath,
6123
6309
  message: message2
6124
6310
  });
6125
6311
  throw new Error(message2);
6126
6312
  }
6127
- this.llm = new LLMService({
6128
- apiKey,
6129
- baseUrl,
6130
- model: this.model
6131
- });
6313
+ try {
6314
+ this.llm = new LLMService();
6315
+ } catch (e) {
6316
+ const message2 = e?.message || String(e);
6317
+ this.eventBus.emit("backend_message", {
6318
+ type: "error",
6319
+ code: "llm_config",
6320
+ message: message2
6321
+ });
6322
+ throw e;
6323
+ }
6132
6324
  this.core = new BluMaAgent(
6133
6325
  this.sessionId,
6134
6326
  this.eventBus,
6135
6327
  this.llm,
6136
- this.model,
6137
6328
  this.mcpClient,
6138
6329
  this.feedbackSystem
6139
6330
  );
@@ -6142,7 +6333,6 @@ var Agent = class {
6142
6333
  mcpClient: this.mcpClient,
6143
6334
  toolInvoker: this.toolInvoker,
6144
6335
  llm: this.llm,
6145
- model: this.model,
6146
6336
  projectRoot: process.cwd()
6147
6337
  });
6148
6338
  this.routeManager = new RouteManager(this.subAgents, this.core);
@@ -6156,12 +6346,13 @@ var Agent = class {
6156
6346
  getUiToolsDetailed() {
6157
6347
  return this.core.getUiToolsDetailed();
6158
6348
  }
6159
- async processTurn(userInput) {
6349
+ async processTurn(userInput, userContextInput) {
6160
6350
  const inputText = String(userInput.content || "").trim();
6351
+ const resolvedUserContext = userContextInput ?? defaultInteractiveCliUserContextInput(this.sessionId, inputText.slice(0, 300));
6161
6352
  if (inputText === "/init" || inputText.startsWith("/init ")) {
6162
6353
  this.routeManager.registerRoute("/init", this.dispatchToSubAgent);
6163
6354
  }
6164
- await this.routeManager.handleRoute({ content: inputText });
6355
+ await this.routeManager.handleRoute({ content: inputText, userContext: resolvedUserContext });
6165
6356
  }
6166
6357
  async handleToolResponse(decisionData) {
6167
6358
  await this.core.handleToolResponse(decisionData);
@@ -7709,6 +7900,18 @@ function stopTitleKeeper() {
7709
7900
  }
7710
7901
 
7711
7902
  // src/main.ts
7903
+ function extractUserMessage(envelope) {
7904
+ const c = envelope.context;
7905
+ if (c && typeof c === "object" && typeof c.user_request === "string") {
7906
+ return String(c.user_request).slice(0, 300);
7907
+ }
7908
+ if (typeof c === "string") return c.slice(0, 300);
7909
+ try {
7910
+ return JSON.stringify(c ?? {}).slice(0, 300);
7911
+ } catch {
7912
+ return "";
7913
+ }
7914
+ }
7712
7915
  function writeJsonl(event) {
7713
7916
  try {
7714
7917
  process.stdout.write(JSON.stringify(event) + "\n");
@@ -7763,7 +7966,18 @@ async function runAgentMode() {
7763
7966
  }
7764
7967
  }
7765
7968
  const eventBus = new EventEmitter2();
7766
- const sessionId = envelope.message_id || uuidv43();
7969
+ const sessionId = envelope.session_id || envelope.message_id || uuidv46();
7970
+ const uc = envelope.user_context;
7971
+ const userContextInput = {
7972
+ sessionId,
7973
+ conversationId: uc?.conversationId ?? null,
7974
+ userMessage: extractUserMessage(envelope),
7975
+ userId: uc?.userId ?? null,
7976
+ userName: uc?.userName ?? null,
7977
+ userEmail: uc?.userEmail ?? null,
7978
+ companyId: uc?.companyId ?? null,
7979
+ companyName: uc?.companyName ?? null
7980
+ };
7767
7981
  let lastAssistantMessage = null;
7768
7982
  let reasoningBuffer = null;
7769
7983
  let lastAttachments = null;
@@ -7848,7 +8062,7 @@ async function runAgentMode() {
7848
8062
  action: envelope.action || "unknown",
7849
8063
  context: envelope.context || {}
7850
8064
  });
7851
- await agent.processTurn({ content: userContent });
8065
+ await agent.processTurn({ content: userContent }, userContextInput);
7852
8066
  if (!resultEmitted) {
7853
8067
  resultEmitted = true;
7854
8068
  writeJsonl({
@@ -7884,7 +8098,7 @@ function runCliMode() {
7884
8098
  const BLUMA_TITLE = process.env.BLUMA_TITLE || "BluMa - NomadEngenuity";
7885
8099
  startTitleKeeper(BLUMA_TITLE);
7886
8100
  const eventBus = new EventEmitter2();
7887
- const sessionId = uuidv43();
8101
+ const sessionId = uuidv46();
7888
8102
  const props = {
7889
8103
  eventBus,
7890
8104
  sessionId
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nomad-e/bluma-cli",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "BluMa independent agent for automation and advanced software engineering.",
5
5
  "author": "Alex Fonseca",
6
6
  "license": "Apache-2.0",