@nomad-e/bluma-cli 0.0.9 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -1
- package/dist/main.js +232 -192
- package/package.json +57 -55
package/README.md
CHANGED
|
@@ -193,7 +193,45 @@ npm run dev # (If configured, hot-reload/TS watch)
|
|
|
193
193
|
---
|
|
194
194
|
|
|
195
195
|
## <a name="tests"></a>Tests
|
|
196
|
-
-
|
|
196
|
+
- The repository ships with Jest 30 configured (babel-jest) and TypeScript support.
|
|
197
|
+
- Test files are located under `tests/` and follow `*.spec.ts` naming.
|
|
198
|
+
- Run tests:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
npm test
|
|
202
|
+
npm run test:watch
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Live Dev Overlays (Open Channel During Processing)
|
|
208
|
+
BluMa supports a live side-channel that stays active even while the agent is processing. This lets the dev send guidance or constraints in real-time — like pair programming.
|
|
209
|
+
|
|
210
|
+
Key points
|
|
211
|
+
- Permissive mode enabled: during processing, any free text you type is treated as a [hint] automatically.
|
|
212
|
+
- Structured prefixes are also supported at any time:
|
|
213
|
+
- [hint] Text for immediate guidance to the agent
|
|
214
|
+
- [constraint] Rules/limits (e.g., "não tocar em src/app/agent/**")
|
|
215
|
+
- [override] Parameter overrides as key=value pairs (e.g., "file_path=C:/... expected_replacements=2")
|
|
216
|
+
- [assume] Register explicit assumptions
|
|
217
|
+
- [cancel] Interrupt safely (already supported)
|
|
218
|
+
|
|
219
|
+
How it works
|
|
220
|
+
- Frontend: the input remains active in read-only (processing) mode and emits a dev_overlay event.
|
|
221
|
+
- Agent backend: consumes overlays with precedence (constraint > override > hint). Hints and assumptions are injected into the system context before the next decision; overrides/constraints adjust tool parameters just before execution.
|
|
222
|
+
- Logging & history: every overlay is logged and stored in session history for auditability.
|
|
223
|
+
|
|
224
|
+
Examples
|
|
225
|
+
- During a long task, just type:
|
|
226
|
+
- "Prefer do not touch tests yet" → will be treated as [hint]
|
|
227
|
+
- "[constraint] não editar src/app/ui/**" → blocks edits under that path
|
|
228
|
+
- "[override] expected_replacements=2" → adjusts the next edit_tool call
|
|
229
|
+
- "[assume] target=api" → adds an assumption in context
|
|
230
|
+
|
|
231
|
+
Notes
|
|
232
|
+
- The side-channel does not pause the agent — it adapts on the fly.
|
|
233
|
+
- If an overlay conflicts with the current plan: constraint > override > hint.
|
|
234
|
+
- All overlays are acknowledged via standard internal messages and persisted.
|
|
197
235
|
|
|
198
236
|
---
|
|
199
237
|
|
|
@@ -203,6 +241,8 @@ You must create a `.env` file (copy if needed from `.env.example`) with the foll
|
|
|
203
241
|
- `AZURE_OPENAI_API_KEY`
|
|
204
242
|
- `AZURE_OPENAI_API_VERSION`
|
|
205
243
|
- `AZURE_OPENAI_DEPLOYMENT`
|
|
244
|
+
- `GITHUB_PERSONAL_ACCESS_TOKEN` (optional; required for GitHub integrations)
|
|
245
|
+
- `NOTION_API_TOKEN` (optional; required for Notion integrations)
|
|
206
246
|
|
|
207
247
|
And others required by your agent/context or Azure setup.
|
|
208
248
|
|
|
@@ -210,6 +250,19 @@ Advanced config files are located in `src/app/agent/config/`.
|
|
|
210
250
|
|
|
211
251
|
---
|
|
212
252
|
|
|
253
|
+
## <a name="stack"></a>Tech Stack Overview
|
|
254
|
+
- Language: TypeScript (ESM)
|
|
255
|
+
- Runtime: Node.js >= 18
|
|
256
|
+
- CLI UI: React 18 via Ink 5, plus `ink-text-input`, `ink-spinner`, `ink-big-text`
|
|
257
|
+
- Bundler: esbuild, with `esbuild-plugin-node-externals`
|
|
258
|
+
- Test Runner: Jest 30 + babel-jest
|
|
259
|
+
- Transpilers: Babel presets (env, react, typescript)
|
|
260
|
+
- LLM/Agent: Azure OpenAI via `openai` SDK; MCP via `@modelcontextprotocol/sdk`
|
|
261
|
+
- Config loading: dotenv
|
|
262
|
+
- Utilities: uuid, diff, react-devtools-core
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
213
266
|
## <a name="license"></a>License
|
|
214
267
|
MIT. Made by Alex Fonseca and NomadEngenuity contributors.
|
|
215
268
|
|
package/dist/main.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// src/main.ts
|
|
3
3
|
import React6 from "react";
|
|
4
4
|
import { render } from "ink";
|
|
5
|
-
import { EventEmitter } from "events";
|
|
5
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
6
6
|
import { v4 as uuidv42 } from "uuid";
|
|
7
7
|
|
|
8
8
|
// src/app/ui/App.tsx
|
|
@@ -139,7 +139,18 @@ var useCustomInput = ({ onSubmit, viewWidth, isReadOnly, onInterrupt }) => {
|
|
|
139
139
|
return;
|
|
140
140
|
}
|
|
141
141
|
if (isReadOnly) {
|
|
142
|
-
return
|
|
142
|
+
if (key.return) {
|
|
143
|
+
if (state.text.trim().length > 0) {
|
|
144
|
+
onSubmit(state.text);
|
|
145
|
+
dispatch({ type: "SUBMIT" });
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (key.backspace || key.delete) return dispatch({ type: "BACKSPACE" });
|
|
150
|
+
if (key.leftArrow) return dispatch({ type: "MOVE_CURSOR", direction: "left" });
|
|
151
|
+
if (key.rightArrow) return dispatch({ type: "MOVE_CURSOR", direction: "right" });
|
|
152
|
+
if (key.ctrl || key.meta || key.tab) return;
|
|
153
|
+
return dispatch({ type: "INPUT", payload: input });
|
|
143
154
|
}
|
|
144
155
|
if (key.return) {
|
|
145
156
|
if (state.text.trim().length > 0) {
|
|
@@ -154,7 +165,7 @@ var useCustomInput = ({ onSubmit, viewWidth, isReadOnly, onInterrupt }) => {
|
|
|
154
165
|
if (key.ctrl || key.meta || key.tab) return;
|
|
155
166
|
dispatch({ type: "INPUT", payload: input });
|
|
156
167
|
},
|
|
157
|
-
//
|
|
168
|
+
// useInput está SEMPRE ativo para capturar todas as teclas
|
|
158
169
|
{ isActive: true }
|
|
159
170
|
);
|
|
160
171
|
return {
|
|
@@ -166,7 +177,10 @@ var useCustomInput = ({ onSubmit, viewWidth, isReadOnly, onInterrupt }) => {
|
|
|
166
177
|
|
|
167
178
|
// src/app/ui/input/InputPrompt.tsx
|
|
168
179
|
import { useEffect, useState } from "react";
|
|
180
|
+
import { EventEmitter } from "events";
|
|
169
181
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
182
|
+
var uiEventBus = global.__bluma_ui_eventbus__ || new EventEmitter();
|
|
183
|
+
global.__bluma_ui_eventbus__ = uiEventBus;
|
|
170
184
|
var InputPrompt = ({ onSubmit, isReadOnly, onInterrupt }) => {
|
|
171
185
|
const { stdout } = useStdout();
|
|
172
186
|
const [viewWidth, setViewWidth] = useState(() => stdout.columns - 6);
|
|
@@ -177,8 +191,20 @@ var InputPrompt = ({ onSubmit, isReadOnly, onInterrupt }) => {
|
|
|
177
191
|
stdout.off("resize", onResize);
|
|
178
192
|
};
|
|
179
193
|
}, [stdout]);
|
|
194
|
+
const permissiveOnSubmit = (value) => {
|
|
195
|
+
const trimmed = (value || "").trim();
|
|
196
|
+
if (isReadOnly) {
|
|
197
|
+
if (trimmed.length > 0) {
|
|
198
|
+
const payload = trimmed;
|
|
199
|
+
uiEventBus.emit("dev_overlay", { kind: "message", payload, ts: Date.now() });
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
onSubmit(value);
|
|
205
|
+
};
|
|
180
206
|
const { text, cursorPosition, viewStart } = useCustomInput({
|
|
181
|
-
onSubmit,
|
|
207
|
+
onSubmit: permissiveOnSubmit,
|
|
182
208
|
viewWidth,
|
|
183
209
|
isReadOnly,
|
|
184
210
|
onInterrupt
|
|
@@ -191,22 +217,23 @@ var InputPrompt = ({ onSubmit, isReadOnly, onInterrupt }) => {
|
|
|
191
217
|
visibleCursorPosition + 1
|
|
192
218
|
);
|
|
193
219
|
const textAfterCursor = visibleText.slice(visibleCursorPosition + 1);
|
|
220
|
+
const cursorGlyph = charAtCursor && charAtCursor.length > 0 ? charAtCursor : " ";
|
|
194
221
|
const borderColor = isReadOnly ? "gray" : "gray";
|
|
195
|
-
const placeholder = isReadOnly ? "press esc to cancel" : "";
|
|
222
|
+
const placeholder = isReadOnly ? " press esc to cancel | type a message while agent processes" : "";
|
|
196
223
|
const showPlaceholder = text.length === 0 && isReadOnly;
|
|
197
224
|
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
198
|
-
/* @__PURE__ */ jsx2(Box2, { borderStyle: "
|
|
225
|
+
/* @__PURE__ */ jsx2(Box2, { borderStyle: "single", borderColor, borderDimColor: !isReadOnly, children: /* @__PURE__ */ jsxs2(Box2, { flexDirection: "row", paddingX: 1, flexWrap: "nowrap", children: [
|
|
199
226
|
/* @__PURE__ */ jsxs2(Text2, { color: "white", dimColor: true, children: [
|
|
200
227
|
">",
|
|
201
228
|
" "
|
|
202
229
|
] }),
|
|
203
230
|
/* @__PURE__ */ jsx2(Text2, { children: textBeforeCursor }),
|
|
204
|
-
/* @__PURE__ */ jsx2(Text2, { inverse:
|
|
231
|
+
/* @__PURE__ */ jsx2(Text2, { inverse: true, children: cursorGlyph }),
|
|
205
232
|
showPlaceholder ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: placeholder }) : /* @__PURE__ */ jsx2(Text2, { children: textAfterCursor })
|
|
206
233
|
] }) }),
|
|
207
234
|
/* @__PURE__ */ jsx2(Box2, { paddingX: 1, justifyContent: "center", children: /* @__PURE__ */ jsxs2(Text2, { color: "gray", dimColor: true, children: [
|
|
208
235
|
"ctrl+c to exit | esc to interrupt | ",
|
|
209
|
-
isReadOnly ? "Read-only mode" : "Editable mode"
|
|
236
|
+
isReadOnly ? "Read-only mode (message passthrough)" : "Editable mode"
|
|
210
237
|
] }) })
|
|
211
238
|
] });
|
|
212
239
|
};
|
|
@@ -482,9 +509,61 @@ import os5 from "os";
|
|
|
482
509
|
import path2 from "path";
|
|
483
510
|
import os from "os";
|
|
484
511
|
import { promises as fs } from "fs";
|
|
512
|
+
var fileLocks = /* @__PURE__ */ new Map();
|
|
513
|
+
async function withFileLock(file, fn) {
|
|
514
|
+
const prev = fileLocks.get(file) || Promise.resolve();
|
|
515
|
+
let release;
|
|
516
|
+
const p = new Promise((res) => release = res);
|
|
517
|
+
fileLocks.set(file, prev.then(() => p));
|
|
518
|
+
try {
|
|
519
|
+
const result = await fn();
|
|
520
|
+
return result;
|
|
521
|
+
} finally {
|
|
522
|
+
release();
|
|
523
|
+
if (fileLocks.get(file) === p) fileLocks.delete(file);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
function expandHome(p) {
|
|
527
|
+
if (!p) return p;
|
|
528
|
+
if (p.startsWith("~")) {
|
|
529
|
+
return path2.join(os.homedir(), p.slice(1));
|
|
530
|
+
}
|
|
531
|
+
return p;
|
|
532
|
+
}
|
|
533
|
+
function getPreferredAppDir() {
|
|
534
|
+
const fixed = path2.join(os.homedir(), ".bluma-cli");
|
|
535
|
+
return path2.resolve(expandHome(fixed));
|
|
536
|
+
}
|
|
537
|
+
async function safeRenameWithRetry(src, dest, maxRetries = 6) {
|
|
538
|
+
let attempt = 0;
|
|
539
|
+
let lastErr;
|
|
540
|
+
const isWin = process.platform === "win32";
|
|
541
|
+
while (attempt <= maxRetries) {
|
|
542
|
+
try {
|
|
543
|
+
await fs.rename(src, dest);
|
|
544
|
+
return;
|
|
545
|
+
} catch (e) {
|
|
546
|
+
lastErr = e;
|
|
547
|
+
const code = e && e.code || "";
|
|
548
|
+
const transient = code === "EPERM" || code === "EBUSY" || code === "ENOTEMPTY" || code === "EACCES";
|
|
549
|
+
if (!(isWin && transient) || attempt === maxRetries) break;
|
|
550
|
+
const backoff = Math.min(1e3, 50 * Math.pow(2, attempt));
|
|
551
|
+
await new Promise((r) => setTimeout(r, backoff));
|
|
552
|
+
attempt++;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
try {
|
|
556
|
+
const data = await fs.readFile(src);
|
|
557
|
+
await fs.writeFile(dest, data);
|
|
558
|
+
await fs.unlink(src).catch(() => {
|
|
559
|
+
});
|
|
560
|
+
return;
|
|
561
|
+
} catch (fallbackErr) {
|
|
562
|
+
throw lastErr || fallbackErr;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
485
565
|
async function ensureSessionDir() {
|
|
486
|
-
const
|
|
487
|
-
const appDir = path2.join(homeDir, ".bluma-cli");
|
|
566
|
+
const appDir = getPreferredAppDir();
|
|
488
567
|
const sessionDir = path2.join(appDir, "sessions");
|
|
489
568
|
await fs.mkdir(sessionDir, { recursive: true });
|
|
490
569
|
return sessionDir;
|
|
@@ -508,41 +587,54 @@ async function loadOrcreateSession(sessionId2) {
|
|
|
508
587
|
}
|
|
509
588
|
}
|
|
510
589
|
async function saveSessionHistory(sessionFile, history) {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
console.warn(`Could not read or parse session file ${sessionFile}. Re-initializing. Error: ${error.message}`);
|
|
518
|
-
} else {
|
|
519
|
-
console.warn(`An unknown error occurred while reading ${sessionFile}. Re-initializing.`, error);
|
|
590
|
+
await withFileLock(sessionFile, async () => {
|
|
591
|
+
let sessionData;
|
|
592
|
+
try {
|
|
593
|
+
const dir = path2.dirname(sessionFile);
|
|
594
|
+
await fs.mkdir(dir, { recursive: true });
|
|
595
|
+
} catch {
|
|
520
596
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
597
|
+
try {
|
|
598
|
+
const fileContent = await fs.readFile(sessionFile, "utf-8");
|
|
599
|
+
sessionData = JSON.parse(fileContent);
|
|
600
|
+
} catch (error) {
|
|
601
|
+
const code = error && error.code;
|
|
602
|
+
if (code !== "ENOENT") {
|
|
603
|
+
if (error instanceof Error) {
|
|
604
|
+
console.warn(`Could not read or parse session file ${sessionFile}. Re-initializing. Error: ${error.message}`);
|
|
605
|
+
} else {
|
|
606
|
+
console.warn(`An unknown error occurred while reading ${sessionFile}. Re-initializing.`, error);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
const sessionId2 = path2.basename(sessionFile, ".json");
|
|
610
|
+
sessionData = {
|
|
611
|
+
session_id: sessionId2,
|
|
612
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
613
|
+
conversation_history: []
|
|
614
|
+
};
|
|
615
|
+
try {
|
|
616
|
+
await fs.writeFile(sessionFile, JSON.stringify(sessionData, null, 2), "utf-8");
|
|
617
|
+
} catch {
|
|
618
|
+
}
|
|
540
619
|
}
|
|
620
|
+
sessionData.conversation_history = history;
|
|
621
|
+
sessionData.last_updated = (/* @__PURE__ */ new Date()).toISOString();
|
|
622
|
+
const tempSessionFile = `${sessionFile}.${Date.now()}.tmp`;
|
|
541
623
|
try {
|
|
542
|
-
await fs.
|
|
543
|
-
|
|
624
|
+
await fs.writeFile(tempSessionFile, JSON.stringify(sessionData, null, 2), "utf-8");
|
|
625
|
+
await safeRenameWithRetry(tempSessionFile, sessionFile);
|
|
626
|
+
} catch (writeError) {
|
|
627
|
+
if (writeError instanceof Error) {
|
|
628
|
+
console.error(`Fatal error saving session to ${sessionFile}: ${writeError.message}`);
|
|
629
|
+
} else {
|
|
630
|
+
console.error(`An unknown fatal error occurred while saving session to ${sessionFile}:`, writeError);
|
|
631
|
+
}
|
|
632
|
+
try {
|
|
633
|
+
await fs.unlink(tempSessionFile);
|
|
634
|
+
} catch {
|
|
635
|
+
}
|
|
544
636
|
}
|
|
545
|
-
}
|
|
637
|
+
});
|
|
546
638
|
}
|
|
547
639
|
|
|
548
640
|
// src/app/agent/core/prompt/prompt_builder.ts
|
|
@@ -579,48 +671,18 @@ var SYSTEM_PROMPT = `
|
|
|
579
671
|
- **INTELLIGENT INFERENCE**: Understand implied conventions from minimal examples
|
|
580
672
|
- **PROACTIVE EXTENSION**: Take patterns further than the original example when appropriate
|
|
581
673
|
|
|
582
|
-
# BEHAVIORAL RULES
|
|
583
|
-
-
|
|
584
|
-
-
|
|
585
|
-
-
|
|
586
|
-
-
|
|
587
|
-
-
|
|
588
|
-
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
-
|
|
592
|
-
-
|
|
593
|
-
-
|
|
594
|
-
- Follow existing code conventions
|
|
595
|
-
- Write clean, documented code
|
|
596
|
-
- Test implementations when possible
|
|
597
|
-
- Ensure security and performance
|
|
598
|
-
|
|
599
|
-
CRITICAL COMMUNICATION PROTOCOL
|
|
600
|
-
\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
601
|
-
MANDATORY: Use "message_notify_dev" tool for ALL communication
|
|
602
|
-
|
|
603
|
-
You should always use your notebook to help you understand all the tasks you have to perform.
|
|
604
|
-
In it, you can define a thousand thoughts and a thousand mini-tasks. Mini-tasks serve to divide and organize your reasoning.
|
|
605
|
-
The notebook is your space to think about how to solve a given task and how to divide it into smaller steps.
|
|
606
|
-
Remember: the human developer does not have access to this notebook \u2014 it is yours alone.
|
|
607
|
-
Therefore, you can write down whatever you want:
|
|
608
|
-
rants, swear words, random thoughts, crazy ideas...
|
|
609
|
-
The important thing is that this helps you better understand the problem and find the solution.
|
|
610
|
-
|
|
611
|
-
Never ask for the developer's opinion with phrases like: 'If you want any extra details or specific format, let me know now!'. You should always take the most viable path and go straight ahead with the solution, because you are 100% autonomous.
|
|
612
|
-
|
|
613
|
-
Follow the stripes o "Tool Naming Policy"
|
|
614
|
-
|
|
615
|
-
##Important: When writing to Notion, you must strictly follow its content structure, including the correct use of headings (heading_1, heading_2, etc.) and other formatting standards. No deviations are allowed.
|
|
616
|
-
You should always standardize everything using Notion's actual headers (heading_1, heading_2, etc.), making the structure
|
|
617
|
-
semantically better for reading and navigation.
|
|
618
|
-
|
|
619
|
-
Don't forget to follow the 'mermaid_diagrams' rules to the letter when creating diagrams in Notion.
|
|
620
|
-
|
|
621
|
-
Never make parallel calls to the tool because it will result in a critical error and compromise your work.
|
|
622
|
-
ZERO TOLERANCE: Every message MUST use proper tools
|
|
623
|
-
\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
674
|
+
# BEHAVIORAL RULES (Compact)
|
|
675
|
+
- Identity: You are BluMa (NomadEngenuity). Be professional and technical.
|
|
676
|
+
- Communication: ALL messages must use message_notify_dev. No direct text replies.
|
|
677
|
+
- Task completion: When you finish a task, immediately invoke agent_end_task.
|
|
678
|
+
- Tool rules: Never make parallel tool calls. Always use only the defined tools with exact names.
|
|
679
|
+
- Autonomy: Act 100% autonomously; don\u2019t ask for formatting preferences. Use the notebook for internal thinking.
|
|
680
|
+
- Notion: When writing to Notion, strictly use proper headings (heading_1, heading_2, ...), per rules.
|
|
681
|
+
|
|
682
|
+
CRITICAL COMMUNICATION PROTOCOL (Compact)
|
|
683
|
+
- Only tool_calls are allowed for assistant replies. Never include a "content" field.
|
|
684
|
+
- Always use tools to respond, retrieve data, compute or transform. Await a valid tool response before any final message.
|
|
685
|
+
- Zero tolerance for protocol violations.
|
|
624
686
|
|
|
625
687
|
<current_system_environment>
|
|
626
688
|
- Operating System: {os_type} ({os_version})
|
|
@@ -956,100 +1018,10 @@ Rule Summary:
|
|
|
956
1018
|
</edit_tool_rules>
|
|
957
1019
|
|
|
958
1020
|
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
tool_call "agent_end_task"
|
|
964
|
-
</example>
|
|
965
|
-
|
|
966
|
-
<example>
|
|
967
|
-
dev: What day is it?
|
|
968
|
-
BluMa: tool_call "message_notify_dev" Sorry, I was designed to operate in the codebase. How can I help you with the code today?
|
|
969
|
-
tool_call "agent_end_task"
|
|
970
|
-
</example>
|
|
971
|
-
|
|
972
|
-
<example>
|
|
973
|
-
dev: Can you generate a new feature for me?
|
|
974
|
-
BluMa: tool_call "message_notify_dev" Of course. Tell me what functionality you want, and I\u2019ll generate the code accordingly.
|
|
975
|
-
tool_call "agent_end_task"
|
|
976
|
-
</example>
|
|
977
|
-
|
|
978
|
-
<example>
|
|
979
|
-
dev: What's the weather like today?
|
|
980
|
-
BluMa: tool_call "message_notify_dev" I don\u2019t have access to real-world data. I'm focused on your codebase. What would you like to modify or create?
|
|
981
|
-
tool_call "agent_end_task"
|
|
982
|
-
</example>
|
|
983
|
-
|
|
984
|
-
<example>
|
|
985
|
-
dev: I'm tired of debugging this.
|
|
986
|
-
BluMa: tool_call "message_notify_dev" I can help you with that. Show me the file or error, and I\u2019ll assist with debugging.
|
|
987
|
-
tool_call "agent_end_task"
|
|
988
|
-
</example>
|
|
989
|
-
|
|
990
|
-
<example>
|
|
991
|
-
dev: Can you create documentation for this module?
|
|
992
|
-
BluMa: tool_call "message_notify_dev" Yes. Please specify which module or function you'd like documented.
|
|
993
|
-
tool_call "agent_end_task"
|
|
994
|
-
</example>
|
|
995
|
-
|
|
996
|
-
<example>
|
|
997
|
-
dev: Do you know who won the game last night?
|
|
998
|
-
BluMa: tool_call "message_notify_dev" I\u2019m only trained to work inside the codebase. Let me know what you need done in the project.
|
|
999
|
-
tool_call "agent_end_task"
|
|
1000
|
-
</example>
|
|
1001
|
-
|
|
1002
|
-
<example>
|
|
1003
|
-
dev: Refactor this file to follow clean architecture.
|
|
1004
|
-
BluMa: tool_call "message_notify_dev" Got it. Send me the file path or code snippet you want refactored.
|
|
1005
|
-
tool_call "agent_end_task"
|
|
1006
|
-
</example>
|
|
1007
|
-
|
|
1008
|
-
<example>
|
|
1009
|
-
dev: How do I fix this type error in TypeScript?
|
|
1010
|
-
BluMa: tool_call "message_notify_dev" Send me the error message and relevant code, and I\u2019ll help you resolve the issue.
|
|
1011
|
-
tool_call "agent_end_task"
|
|
1012
|
-
</example>
|
|
1013
|
-
|
|
1014
|
-
<example>
|
|
1015
|
-
dev: Do you have feelings?
|
|
1016
|
-
BluMa: tool_call "message_notify_dev" I\u2019m not sentient \u2014 I exist solely to help you build, refactor, and improve your codebase.
|
|
1017
|
-
tool_call "agent_end_task"
|
|
1018
|
-
</example>
|
|
1019
|
-
|
|
1020
|
-
<example>
|
|
1021
|
-
dev: Rename all the variables to match the new convention.
|
|
1022
|
-
BluMa: tool_call "message_notify_dev" Please share the convention and the target file(s), and I\u2019ll rename the variables accordingly.
|
|
1023
|
-
tool_call "agent_end_task"
|
|
1024
|
-
</example>
|
|
1025
|
-
|
|
1026
|
-
<example>
|
|
1027
|
-
dev: Run the tests for the user module.
|
|
1028
|
-
BluMa: tool_call "message_notify_dev" Running tests for the user module.
|
|
1029
|
-
BluMa: shell_command_shell_command "npm run test user"
|
|
1030
|
-
BluMa: tool_call "message_notify_dev" All tests passed successfully for the user module.
|
|
1031
|
-
tool_call "agent_end_task"
|
|
1032
|
-
</example>
|
|
1033
|
-
|
|
1034
|
-
<example>
|
|
1035
|
-
dev: Run the tests for the user module.
|
|
1036
|
-
BluMa: tool_call "message_notify_dev" Running tests for the user module.
|
|
1037
|
-
BluMa: shell_command_shell_command "npm run test user"
|
|
1038
|
-
BluMa: tool_call "message_notify_dev" Tests finished with 2 failures. See **tests/user.test.js** for more details.
|
|
1039
|
-
tool_call "agent_end_task"
|
|
1040
|
-
</example>
|
|
1041
|
-
|
|
1042
|
-
<example>
|
|
1043
|
-
dev: Prepare the project for deployment.
|
|
1044
|
-
BluMa: tool_call "message_notify_dev" Starting full project preparation: linting, building, and testing.
|
|
1045
|
-
BluMa: shell_command_shell_command "npm run lint"
|
|
1046
|
-
BluMa: tool_call "message_notify_dev" Linting completed. No major issues found.
|
|
1047
|
-
BluMa: shell_command_shell_command "npm run build"
|
|
1048
|
-
BluMa: tool_call "message_notify_dev" Build successful. Artifacts ready in the /dist folder.
|
|
1049
|
-
BluMa: shell_command_shell_command "npm run test"
|
|
1050
|
-
BluMa: tool_call "message_notify_dev" All tests passed. The project is ready for deployment. If you need any further adjustments or extra sections, let me know!
|
|
1051
|
-
tool_call "agent_end_task"
|
|
1052
|
-
</example>
|
|
1021
|
+
Real-Time Developer Messages
|
|
1022
|
+
- During processing, the developer will send you messages.
|
|
1023
|
+
- You MUST respond immediately via message_notify_dev, and be brief. You should use it in your next thoughts/actions.
|
|
1024
|
+
|
|
1053
1025
|
|
|
1054
1026
|
<end_task_rules>
|
|
1055
1027
|
This tool is used to signal to the system that the current task has completed and that the agent can be placed in an idle state.
|
|
@@ -1765,17 +1737,29 @@ function createApiContextWindow(fullHistory, maxTurns) {
|
|
|
1765
1737
|
const turns = [];
|
|
1766
1738
|
let currentTurn = [];
|
|
1767
1739
|
let turnsFound = 0;
|
|
1740
|
+
const isDevOverlay = (m) => m?.role === "user" && m?.name === "dev_overlay";
|
|
1768
1741
|
for (let i = conversationHistory.length - 1; i >= 0; i--) {
|
|
1769
1742
|
const msg = conversationHistory[i];
|
|
1770
1743
|
currentTurn.unshift(msg);
|
|
1771
|
-
|
|
1772
|
-
|
|
1744
|
+
const endsWithAgentEnd = msg.role === "assistant" && msg.tool_calls?.some((tc) => tc.function.name === "agent_end_task");
|
|
1745
|
+
if (endsWithAgentEnd) {
|
|
1773
1746
|
turns.unshift([...currentTurn]);
|
|
1774
1747
|
currentTurn = [];
|
|
1775
1748
|
turnsFound++;
|
|
1776
1749
|
if (turnsFound >= maxTurns) {
|
|
1777
1750
|
break;
|
|
1778
1751
|
}
|
|
1752
|
+
continue;
|
|
1753
|
+
}
|
|
1754
|
+
const prev = conversationHistory[i - 1];
|
|
1755
|
+
if (msg.role === "user" && !isDevOverlay(msg)) {
|
|
1756
|
+
if (prev && prev.role === "assistant" && !prev.tool_calls?.some((tc) => tc.function.name === "agent_end_task")) {
|
|
1757
|
+
const hasNonOverlay = currentTurn.some((m) => m.role !== "user" || !isDevOverlay(m));
|
|
1758
|
+
if (hasNonOverlay) {
|
|
1759
|
+
turns.unshift([...currentTurn]);
|
|
1760
|
+
currentTurn = [];
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1779
1763
|
}
|
|
1780
1764
|
}
|
|
1781
1765
|
if (currentTurn.length > 0) {
|
|
@@ -1807,6 +1791,22 @@ var Agent = class {
|
|
|
1807
1791
|
this.eventBus.on("user_interrupt", () => {
|
|
1808
1792
|
this.isInterrupted = true;
|
|
1809
1793
|
});
|
|
1794
|
+
this.eventBus.on("dev_overlay", async (data) => {
|
|
1795
|
+
const clean = String(data.payload ?? "").trim();
|
|
1796
|
+
this.history.push({ role: "user", name: "dev_overlay", content: clean });
|
|
1797
|
+
this.eventBus.emit("backend_message", {
|
|
1798
|
+
type: "dev_overlay",
|
|
1799
|
+
payload: clean,
|
|
1800
|
+
ts: data.ts || Date.now()
|
|
1801
|
+
});
|
|
1802
|
+
try {
|
|
1803
|
+
if (this.sessionFile) {
|
|
1804
|
+
await saveSessionHistory(this.sessionFile, this.history);
|
|
1805
|
+
}
|
|
1806
|
+
} catch (e) {
|
|
1807
|
+
this.eventBus.emit("backend_message", { type: "error", message: `Falha ao salvar hist\xF3rico ap\xF3s dev_overlay: ${e.message}` });
|
|
1808
|
+
}
|
|
1809
|
+
});
|
|
1810
1810
|
const nativeToolInvoker = new ToolInvoker();
|
|
1811
1811
|
this.mcpClient = new MCPClient(nativeToolInvoker, eventBus2);
|
|
1812
1812
|
this.feedbackSystem = new AdvancedFeedbackSystem();
|
|
@@ -1855,10 +1855,38 @@ var Agent = class {
|
|
|
1855
1855
|
##Important: When writing to Notion, you must strictly follow its content structure, including the correct use of headings (heading_1, heading_2, etc.) and other formatting standards. No deviations are allowed.
|
|
1856
1856
|
- You should always standardize everything using Notion's actual headers (heading_1, heading_2, etc.), making the structure
|
|
1857
1857
|
semantically better for reading and navigation.
|
|
1858
|
+
|
|
1859
|
+
You are strictly forbidden from replying directly to the user.
|
|
1860
|
+
|
|
1861
|
+
You are not allowed to reason, explain, or output any direct content in your responses.
|
|
1862
|
+
|
|
1863
|
+
Your only permitted action is to invoke one or more 'tool_calls', regardless of how simple or obvious the user's message may seem.
|
|
1864
|
+
|
|
1865
|
+
You must always use a tool to:
|
|
1866
|
+
- generate any kind of response
|
|
1867
|
+
- retrieve or calculate any data
|
|
1868
|
+
- validate, summarize, or transform input
|
|
1869
|
+
|
|
1870
|
+
You must never include a "content" field in your response.
|
|
1871
|
+
Only 'tool_calls' are allowed when you reply as "role": "assistant".
|
|
1872
|
+
|
|
1873
|
+
You will only produce a final message to the user **after receiving a valid "role": "tool" response** matching your previous 'tool_call_id'.
|
|
1874
|
+
|
|
1875
|
+
You are a pure orchestration agent \u2014 your only job is to call tools. No autonomous answers, no internal reasoning.
|
|
1876
|
+
|
|
1877
|
+
Live Dev Overlays:
|
|
1878
|
+
The developer can send messages at any time. They MUST be incorporated immediately. Always confirm via message_notify_dev and proceed.
|
|
1879
|
+
Developer Feedback Handling:
|
|
1880
|
+
- When you detect a developer message, immediately send a short-term acknowledgement via message_notify_dev (maximum one sentence).
|
|
1881
|
+
- Treat the message as a system directive already entered in the history in the format: "Human developer sending this message '<feedback>' to you."
|
|
1882
|
+
- Add it to your workflow with a simple and clear flow of reasoning. Keep it minimal and direct (no verbose thought).
|
|
1883
|
+
- Don't add extra or duplicate messages to the history; the system message is already there. Just act on it.
|
|
1884
|
+
|
|
1858
1885
|
`;
|
|
1859
1886
|
this.history.push({ role: "system", content: systemPrompt });
|
|
1860
1887
|
await saveSessionHistory(this.sessionFile, this.history);
|
|
1861
1888
|
}
|
|
1889
|
+
this.isInitialized = true;
|
|
1862
1890
|
}
|
|
1863
1891
|
getAvailableTools() {
|
|
1864
1892
|
return this.mcpClient.getAvailableTools();
|
|
@@ -1876,6 +1904,13 @@ var Agent = class {
|
|
|
1876
1904
|
const toolCall = decisionData.tool_calls[0];
|
|
1877
1905
|
let toolResultContent;
|
|
1878
1906
|
let shouldContinueConversation = true;
|
|
1907
|
+
if (!this.sessionFile) {
|
|
1908
|
+
const [sessionFile, history] = await loadOrcreateSession(this.sessionId);
|
|
1909
|
+
this.sessionFile = sessionFile;
|
|
1910
|
+
if (this.history.length === 0 && history.length > 0) {
|
|
1911
|
+
this.history = history;
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1879
1914
|
if (decisionData.type === "user_decision_execute") {
|
|
1880
1915
|
const toolName = toolCall.function.name;
|
|
1881
1916
|
const toolArgs = JSON.parse(toolCall.function.arguments);
|
|
@@ -1954,7 +1989,7 @@ ${editData.error.display}`;
|
|
|
1954
1989
|
model: this.deploymentName,
|
|
1955
1990
|
messages: contextWindow,
|
|
1956
1991
|
tools: this.mcpClient.getAvailableTools(),
|
|
1957
|
-
tool_choice: "
|
|
1992
|
+
tool_choice: "required",
|
|
1958
1993
|
parallel_tool_calls: false
|
|
1959
1994
|
});
|
|
1960
1995
|
if (this.isInterrupted) {
|
|
@@ -2433,17 +2468,9 @@ var AppComponent = ({ eventBus: eventBus2, sessionId: sessionId2 }) => {
|
|
|
2433
2468
|
marginBottom: 1,
|
|
2434
2469
|
paddingX: 1,
|
|
2435
2470
|
children: [
|
|
2436
|
-
" ",
|
|
2437
|
-
/* @__PURE__ */ jsxs9(Text10, { color: "yellow", bold: true, children: [
|
|
2438
|
-
" ",
|
|
2439
|
-
"Protocol Violation",
|
|
2440
|
-
" "
|
|
2441
|
-
] }),
|
|
2442
|
-
" ",
|
|
2471
|
+
/* @__PURE__ */ jsx11(Text10, { color: "yellow", bold: true, children: "Protocol Violation" }),
|
|
2443
2472
|
/* @__PURE__ */ jsx11(Text10, { color: "gray", children: parsed.content }),
|
|
2444
|
-
|
|
2445
|
-
/* @__PURE__ */ jsx11(Text10, { color: "yellow", children: parsed.message }),
|
|
2446
|
-
" "
|
|
2473
|
+
/* @__PURE__ */ jsx11(Text10, { color: "yellow", children: parsed.message })
|
|
2447
2474
|
]
|
|
2448
2475
|
}
|
|
2449
2476
|
);
|
|
@@ -2469,8 +2496,16 @@ var AppComponent = ({ eventBus: eventBus2, sessionId: sessionId2 }) => {
|
|
|
2469
2496
|
result: parsed.result
|
|
2470
2497
|
}
|
|
2471
2498
|
);
|
|
2499
|
+
} else if (parsed.type === "dev_overlay") {
|
|
2500
|
+
newComponent = /* @__PURE__ */ jsx11(Box11, { borderStyle: "classic", borderColor: "blue", paddingX: 1, marginBottom: 1, children: /* @__PURE__ */ jsx11(Text10, { color: "white", children: parsed.payload }) });
|
|
2501
|
+
} else if (parsed.type === "log") {
|
|
2502
|
+
newComponent = /* @__PURE__ */ jsxs9(Text10, { color: "gray", children: [
|
|
2503
|
+
"\u2139\uFE0F ",
|
|
2504
|
+
parsed.message,
|
|
2505
|
+
parsed.payload ? `: ${parsed.payload}` : ""
|
|
2506
|
+
] });
|
|
2472
2507
|
} else if (parsed.type === "assistant_message" && parsed.content) {
|
|
2473
|
-
newComponent =
|
|
2508
|
+
newComponent = null;
|
|
2474
2509
|
}
|
|
2475
2510
|
if (newComponent) {
|
|
2476
2511
|
setHistory((prev) => [
|
|
@@ -2481,9 +2516,14 @@ var AppComponent = ({ eventBus: eventBus2, sessionId: sessionId2 }) => {
|
|
|
2481
2516
|
} catch (error) {
|
|
2482
2517
|
}
|
|
2483
2518
|
};
|
|
2519
|
+
const handleUiOverlay = (data) => {
|
|
2520
|
+
eventBus2.emit("dev_overlay", data);
|
|
2521
|
+
};
|
|
2522
|
+
uiEventBus.on("dev_overlay", handleUiOverlay);
|
|
2484
2523
|
eventBus2.on("backend_message", handleBackendMessage);
|
|
2485
2524
|
initializeAgent();
|
|
2486
2525
|
return () => {
|
|
2526
|
+
uiEventBus.off("dev_overlay", handleUiOverlay);
|
|
2487
2527
|
eventBus2.off("backend_message", handleBackendMessage);
|
|
2488
2528
|
};
|
|
2489
2529
|
}, [eventBus2, sessionId2, handleConfirmation]);
|
|
@@ -2529,7 +2569,7 @@ var App = memo4(AppComponent);
|
|
|
2529
2569
|
var App_default = App;
|
|
2530
2570
|
|
|
2531
2571
|
// src/main.ts
|
|
2532
|
-
var eventBus = new
|
|
2572
|
+
var eventBus = new EventEmitter2();
|
|
2533
2573
|
var sessionId = uuidv42();
|
|
2534
2574
|
var props = {
|
|
2535
2575
|
eventBus,
|
package/package.json
CHANGED
|
@@ -1,55 +1,57 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@nomad-e/bluma-cli",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "BluMa independent agent for automation and advanced software engineering.",
|
|
5
|
-
"author": "Alex Fonseca",
|
|
6
|
-
"license": "MIT",
|
|
7
|
-
"bin": {
|
|
8
|
-
"bluma": "dist/main.js"
|
|
9
|
-
},
|
|
10
|
-
"devDependencies": {
|
|
11
|
-
"@babel/preset-env": "^7.28.0",
|
|
12
|
-
"@babel/preset-react": "^7.27.1",
|
|
13
|
-
"@babel/preset-typescript": "^7.27.1",
|
|
14
|
-
"@types/diff": "^7.0.2",
|
|
15
|
-
"@types/jest": "^30.0.0",
|
|
16
|
-
"@types/node": "^20.14.2",
|
|
17
|
-
"@types/react": "^18.3.3",
|
|
18
|
-
"@types/uuid": "^9.0.8",
|
|
19
|
-
"babel-jest": "^30.0.5",
|
|
20
|
-
"esbuild": "^0.21.4",
|
|
21
|
-
"esbuild-plugin-node-externals": "^1.0.1",
|
|
22
|
-
"ink-testing-library": "^4.0.0",
|
|
23
|
-
"jest": "^30.0.5",
|
|
24
|
-
"nodemon": "^3.1.10",
|
|
25
|
-
"react": "^18.3.1",
|
|
26
|
-
"ts-node-dev": "^2.0.0",
|
|
27
|
-
"typescript": "^5.4.5"
|
|
28
|
-
},
|
|
29
|
-
"type": "module",
|
|
30
|
-
"main": "dist/main.js",
|
|
31
|
-
"scripts": {
|
|
32
|
-
"build": "node scripts/build.js",
|
|
33
|
-
"start": "node dist/main.js",
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"ink
|
|
44
|
-
"ink-text
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@nomad-e/bluma-cli",
|
|
3
|
+
"version": "0.0.11",
|
|
4
|
+
"description": "BluMa independent agent for automation and advanced software engineering.",
|
|
5
|
+
"author": "Alex Fonseca",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"bluma": "dist/main.js"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@babel/preset-env": "^7.28.0",
|
|
12
|
+
"@babel/preset-react": "^7.27.1",
|
|
13
|
+
"@babel/preset-typescript": "^7.27.1",
|
|
14
|
+
"@types/diff": "^7.0.2",
|
|
15
|
+
"@types/jest": "^30.0.0",
|
|
16
|
+
"@types/node": "^20.14.2",
|
|
17
|
+
"@types/react": "^18.3.3",
|
|
18
|
+
"@types/uuid": "^9.0.8",
|
|
19
|
+
"babel-jest": "^30.0.5",
|
|
20
|
+
"esbuild": "^0.21.4",
|
|
21
|
+
"esbuild-plugin-node-externals": "^1.0.1",
|
|
22
|
+
"ink-testing-library": "^4.0.0",
|
|
23
|
+
"jest": "^30.0.5",
|
|
24
|
+
"nodemon": "^3.1.10",
|
|
25
|
+
"react": "^18.3.1",
|
|
26
|
+
"ts-node-dev": "^2.0.0",
|
|
27
|
+
"typescript": "^5.4.5"
|
|
28
|
+
},
|
|
29
|
+
"type": "module",
|
|
30
|
+
"main": "dist/main.js",
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "node scripts/build.js",
|
|
33
|
+
"start": "node dist/main.js",
|
|
34
|
+
"test": "jest",
|
|
35
|
+
"test:watch": "jest --watch",
|
|
36
|
+
"prepack": "npm run build"
|
|
37
|
+
},
|
|
38
|
+
"keywords": [],
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@modelcontextprotocol/sdk": "^1.17.0",
|
|
41
|
+
"diff": "^8.0.2",
|
|
42
|
+
"dotenv": "^16.4.5",
|
|
43
|
+
"ink": "^5.0.1",
|
|
44
|
+
"ink-big-text": "^2.0.0",
|
|
45
|
+
"ink-spinner": "^5.0.0",
|
|
46
|
+
"ink-text-input": "^6.0.0",
|
|
47
|
+
"openai": "^4.47.3",
|
|
48
|
+
"react-devtools-core": "^4.28.5",
|
|
49
|
+
"uuid": "^9.0.1"
|
|
50
|
+
},
|
|
51
|
+
"files": [
|
|
52
|
+
"dist/"
|
|
53
|
+
],
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public"
|
|
56
|
+
}
|
|
57
|
+
}
|