@nomad-e/bluma-cli 0.1.32 → 0.1.34

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 (2) hide show
  1. package/dist/main.js +1199 -318
  2. package/package.json +5 -1
package/dist/main.js CHANGED
@@ -17,8 +17,8 @@ __export(async_command_exports, {
17
17
  runCommandAsync: () => runCommandAsync,
18
18
  sendCommandInput: () => sendCommandInput
19
19
  });
20
- import os2 from "os";
21
- import { spawn } from "child_process";
20
+ import os5 from "os";
21
+ import { spawn as spawn2 } from "child_process";
22
22
  import { v4 as uuidv42 } from "uuid";
23
23
  function cleanupOldCommands() {
24
24
  if (runningCommands.size <= MAX_STORED_COMMANDS) return;
@@ -63,7 +63,7 @@ async function runCommandAsync(args) {
63
63
  };
64
64
  }
65
65
  const commandId = uuidv42().substring(0, 8);
66
- const platform = os2.platform();
66
+ const platform = os5.platform();
67
67
  let shellCmd;
68
68
  let shellArgs;
69
69
  if (platform === "win32") {
@@ -83,7 +83,7 @@ async function runCommandAsync(args) {
83
83
  startTime: Date.now(),
84
84
  process: null
85
85
  };
86
- const child = spawn(shellCmd, shellArgs, {
86
+ const child = spawn2(shellCmd, shellArgs, {
87
87
  cwd,
88
88
  env: process.env,
89
89
  windowsHide: true
@@ -331,16 +331,19 @@ var init_async_command = __esm({
331
331
  import React12 from "react";
332
332
  import { render } from "ink";
333
333
  import { EventEmitter as EventEmitter3 } from "events";
334
- import fs15 from "fs";
334
+ import fs17 from "fs";
335
+ import path21 from "path";
336
+ import { fileURLToPath as fileURLToPath4 } from "url";
335
337
  import { v4 as uuidv46 } from "uuid";
336
338
 
337
339
  // src/app/ui/App.tsx
338
- import { useState as useState6, useEffect as useEffect7, useRef as useRef5, useCallback as useCallback2, memo as memo12 } from "react";
340
+ import { useState as useState6, useEffect as useEffect7, useRef as useRef5, useCallback as useCallback3, memo as memo12 } from "react";
339
341
  import { Box as Box20, Text as Text19, Static } from "ink";
340
342
 
341
343
  // src/app/ui/layout.tsx
342
- import { Box, Text } from "ink";
344
+ import { Box, Text, useStdout } from "ink";
343
345
  import { memo } from "react";
346
+ import os from "os";
344
347
 
345
348
  // src/app/ui/theme/blumaTerminal.ts
346
349
  var BLUMA_TERMINAL = {
@@ -375,24 +378,106 @@ var BLUMA_TERMINAL = {
375
378
 
376
379
  // src/app/ui/layout.tsx
377
380
  import { jsx, jsxs } from "react/jsx-runtime";
381
+ function buildTopBorder(cols, leftW, rightW, titleInLeft) {
382
+ const t = titleInLeft.length <= leftW ? titleInLeft : titleInLeft.slice(0, leftW);
383
+ const leftFill = t + "\u2500".repeat(Math.max(0, leftW - t.length));
384
+ const rightFill = "\u2500".repeat(rightW);
385
+ return "\u250C" + leftFill + "\u252C" + rightFill + "\u2510";
386
+ }
387
+ function buildBottomBorder(leftW, rightW) {
388
+ return "\u2514" + "\u2500".repeat(leftW) + "\u2534" + "\u2500".repeat(rightW) + "\u2518";
389
+ }
378
390
  var HeaderComponent = ({
379
391
  sessionId,
380
- workdir
392
+ workdir,
393
+ cliVersion = "?",
394
+ recentActivitySummary
381
395
  }) => {
382
- const dirName = workdir.split("/").pop() || workdir;
383
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
384
- /* @__PURE__ */ jsxs(Box, { flexDirection: "row", flexWrap: "wrap", children: [
385
- /* @__PURE__ */ jsx(Text, { color: BLUMA_TERMINAL.brandBlue, bold: true, children: "BluMa" }),
386
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2014 " }),
387
- /* @__PURE__ */ jsx(Text, { children: "Base Language Unit" }),
388
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " }),
389
- /* @__PURE__ */ jsx(Text, { color: BLUMA_TERMINAL.brandMagenta, bold: true, children: "Model Agent" })
396
+ const { stdout } = useStdout();
397
+ const cols = Math.max(52, stdout?.columns ?? 80);
398
+ let username = "there";
399
+ try {
400
+ username = os.userInfo().username || username;
401
+ } catch {
402
+ }
403
+ const inner = cols - 3;
404
+ const leftW = Math.max(24, Math.floor(inner * 0.48));
405
+ const rightW = Math.max(22, inner - leftW);
406
+ const borderTitle = ` BluMa v${cliVersion} `;
407
+ const topLine = buildTopBorder(cols, leftW, rightW, borderTitle);
408
+ const bottomLine = buildBottomBorder(leftW, rightW);
409
+ const M = BLUMA_TERMINAL.brandMagenta;
410
+ const B = BLUMA_TERMINAL.panelBorder;
411
+ const ruleW = Math.max(6, rightW - 2);
412
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, width: cols, children: [
413
+ /* @__PURE__ */ jsx(Text, { color: B, children: topLine }),
414
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", width: cols, children: [
415
+ /* @__PURE__ */ jsx(Text, { color: B, children: "\u2502" }),
416
+ /* @__PURE__ */ jsxs(
417
+ Box,
418
+ {
419
+ width: leftW,
420
+ flexDirection: "column",
421
+ alignItems: "center",
422
+ paddingX: 1,
423
+ children: [
424
+ /* @__PURE__ */ jsxs(Text, { wrap: "wrap", children: [
425
+ /* @__PURE__ */ jsx(Text, { bold: true, color: M, children: "BluMa" }),
426
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2014 " }),
427
+ /* @__PURE__ */ jsx(Text, { bold: true, color: B, children: "Base Language Unit" }),
428
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " }),
429
+ /* @__PURE__ */ jsx(Text, { bold: true, color: M, children: "Model Agent" })
430
+ ] }),
431
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, flexDirection: "column", alignItems: "center", children: /* @__PURE__ */ jsxs(Text, { children: [
432
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Welcome back " }),
433
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "white", children: username }),
434
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "!" })
435
+ ] }) }),
436
+ /* @__PURE__ */ jsxs(Box, { marginY: 1, flexDirection: "column", alignItems: "center", children: [
437
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(Math.min(13, Math.max(7, leftW - 6))) }),
438
+ /* @__PURE__ */ jsx(Text, { bold: true, color: M, children: " \u27E8 \u03BB \u27E9 " }),
439
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(Math.min(13, Math.max(7, leftW - 6))) })
440
+ ] }),
441
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", alignSelf: "flex-start", width: leftW - 2, children: [
442
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, wrap: "wrap", children: [
443
+ /* @__PURE__ */ jsx(Text, { color: "white", children: "auto" }),
444
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 FactorRouter \xB7 session " }),
445
+ /* @__PURE__ */ jsx(Text, { color: B, children: sessionId.slice(0, 8) })
446
+ ] }),
447
+ /* @__PURE__ */ jsx(Text, { dimColor: true, wrap: "wrap", children: workdir })
448
+ ] })
449
+ ]
450
+ }
451
+ ),
452
+ /* @__PURE__ */ jsx(Text, { color: B, children: "\u2502" }),
453
+ /* @__PURE__ */ jsxs(Box, { width: rightW, flexDirection: "column", paddingX: 1, children: [
454
+ /* @__PURE__ */ jsx(Text, { bold: true, color: M, children: "Tips for getting started" }),
455
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, wrap: "wrap", children: [
456
+ "Run ",
457
+ /* @__PURE__ */ jsx(Text, { color: "white", children: "/init" }),
458
+ " to scaffold project context (BluMa.md). Use",
459
+ " ",
460
+ /* @__PURE__ */ jsx(Text, { color: "white", children: "/help" }),
461
+ ", ",
462
+ /* @__PURE__ */ jsx(Text, { color: "white", children: "/img" }),
463
+ ", ",
464
+ /* @__PURE__ */ jsx(Text, { color: "white", children: "!" }),
465
+ " ",
466
+ "as needed."
467
+ ] }),
468
+ /* @__PURE__ */ jsx(Box, { marginY: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(ruleW) }) }),
469
+ /* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { bold: true, color: M, children: "Recent activity" }) }),
470
+ /* @__PURE__ */ jsx(Text, { dimColor: true, wrap: "wrap", children: recentActivitySummary?.trim() ? recentActivitySummary.trim() : "No recent activity." })
471
+ ] }),
472
+ /* @__PURE__ */ jsx(Text, { color: B, children: "\u2502" })
390
473
  ] }),
391
- /* @__PURE__ */ jsxs(Box, { flexDirection: "row", flexWrap: "wrap", children: [
392
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "cwd " }),
393
- /* @__PURE__ */ jsx(Text, { color: BLUMA_TERMINAL.brandBlue, children: dirName }),
394
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2502 session " }),
395
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: sessionId.slice(0, 8) })
474
+ /* @__PURE__ */ jsx(Text, { color: B, children: bottomLine }),
475
+ /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
476
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7 " }),
477
+ /* @__PURE__ */ jsx(Text, { color: B, bold: true, children: "NomadEngenuity" }),
478
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 Factor stack \xB7 " }),
479
+ /* @__PURE__ */ jsx(Text, { bold: true, color: M, children: "/help" }),
480
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " for shortcuts" })
396
481
  ] })
397
482
  ] });
398
483
  };
@@ -441,7 +526,7 @@ var TaskStatusBarComponent = ({
441
526
  var TaskStatusBar = memo(TaskStatusBarComponent);
442
527
 
443
528
  // src/app/ui/components/InputPrompt.tsx
444
- import { Box as Box2, Text as Text2, useStdout, useInput as useInput2 } from "ink";
529
+ import { Box as Box2, Text as Text2, useStdout as useStdout2, useInput as useInput2 } from "ink";
445
530
 
446
531
  // src/app/ui/utils/useSimpleInputBuffer.ts
447
532
  import { useReducer, useRef, useCallback, useEffect } from "react";
@@ -505,6 +590,13 @@ function inputReducer(state, action, viewWidth) {
505
590
  }
506
591
  return { text: newText, cursorPosition: newCursorPosition, viewStart: newViewStart };
507
592
  }
593
+ case "INSERT_AT_CURSOR": {
594
+ const cleanInput = action.payload.replace(/(\r\n|\r)/gm, "\n");
595
+ const newText = state.text.slice(0, state.cursorPosition) + cleanInput + state.text.slice(state.cursorPosition);
596
+ const newCursorPosition = state.cursorPosition + cleanInput.length;
597
+ const newViewStart = adjustView(newCursorPosition, state.viewStart);
598
+ return { text: newText, cursorPosition: newCursorPosition, viewStart: newViewStart };
599
+ }
508
600
  case "NEWLINE": {
509
601
  const newText = state.text.slice(0, state.cursorPosition) + "\n" + state.text.slice(state.cursorPosition);
510
602
  const newCursorPosition = state.cursorPosition + 1;
@@ -590,11 +682,28 @@ function inputReducer(state, action, viewWidth) {
590
682
  return state;
591
683
  }
592
684
  }
685
+ function isClipboardPasteChord(input, key) {
686
+ if (key.ctrl && key.shift && input === "i") {
687
+ return true;
688
+ }
689
+ if (key.meta && (input === "v" || input === "V")) {
690
+ return true;
691
+ }
692
+ if (key.ctrl && (input === "v" || input === "V")) {
693
+ return true;
694
+ }
695
+ if (key.ctrl && input.length >= 1 && input.charCodeAt(0) === 22) {
696
+ return true;
697
+ }
698
+ return false;
699
+ }
593
700
  var useCustomInput = ({
594
701
  onSubmit,
595
702
  viewWidth,
596
703
  isReadOnly,
597
- onInterrupt
704
+ onInterrupt,
705
+ onChordPaste,
706
+ transformInputChunk
598
707
  }) => {
599
708
  const [state, dispatch] = useReducer(
600
709
  (s, a) => inputReducer(s, a, viewWidth),
@@ -602,14 +711,31 @@ var useCustomInput = ({
602
711
  );
603
712
  const inputBuffer = useRef("");
604
713
  const flushScheduled = useRef(false);
714
+ const applyChunkTransform = useCallback(
715
+ (buffered) => transformInputChunk ? transformInputChunk(buffered) : buffered,
716
+ [transformInputChunk]
717
+ );
605
718
  const flushInputBuffer = useCallback(() => {
606
719
  if (inputBuffer.current.length > 0) {
607
720
  const buffered = inputBuffer.current;
608
721
  inputBuffer.current = "";
609
- dispatch({ type: "INPUT", payload: buffered });
722
+ dispatch({ type: "INPUT", payload: applyChunkTransform(buffered) });
610
723
  }
611
724
  flushScheduled.current = false;
612
- }, []);
725
+ }, [applyChunkTransform]);
726
+ const insertAtCursor = useCallback(
727
+ (s) => {
728
+ if (inputBuffer.current.length > 0) {
729
+ const buffered = inputBuffer.current;
730
+ inputBuffer.current = "";
731
+ flushScheduled.current = false;
732
+ dispatch({ type: "INPUT", payload: applyChunkTransform(buffered) });
733
+ }
734
+ const clean = s.replace(/(\r\n|\r)/gm, "\n");
735
+ dispatch({ type: "INSERT_AT_CURSOR", payload: clean });
736
+ },
737
+ [applyChunkTransform]
738
+ );
613
739
  useEffect(() => {
614
740
  return () => {
615
741
  if (flushScheduled.current) {
@@ -670,6 +796,13 @@ var useCustomInput = ({
670
796
  if (key.rightArrow) return dispatch({ type: "MOVE_CURSOR", direction: "right" });
671
797
  if (key.upArrow) return dispatch({ type: "MOVE_CURSOR", direction: "up" });
672
798
  if (key.downArrow) return dispatch({ type: "MOVE_CURSOR", direction: "down" });
799
+ if (isClipboardPasteChord(input, key) && onChordPaste) {
800
+ if (inputBuffer.current.length > 0) {
801
+ flushInputBuffer();
802
+ }
803
+ void Promise.resolve(onChordPaste(insertAtCursor));
804
+ return;
805
+ }
673
806
  if (key.ctrl || key.meta || key.tab) return;
674
807
  inputBuffer.current += input;
675
808
  if (!flushScheduled.current) {
@@ -704,6 +837,13 @@ var useCustomInput = ({
704
837
  if (key.ctrl && input === "e") {
705
838
  return dispatch({ type: "MOVE_LINE_END" });
706
839
  }
840
+ if (isClipboardPasteChord(input, key) && onChordPaste) {
841
+ if (inputBuffer.current.length > 0) {
842
+ flushInputBuffer();
843
+ }
844
+ void Promise.resolve(onChordPaste(insertAtCursor));
845
+ return;
846
+ }
707
847
  if (key.ctrl || key.meta || key.tab) return;
708
848
  inputBuffer.current += input;
709
849
  if (!flushScheduled.current) {
@@ -732,22 +872,116 @@ var useCustomInput = ({
732
872
  flushInputBuffer();
733
873
  }
734
874
  dispatch({ type: "SET_CURSOR", payload: pos });
735
- }, [flushInputBuffer])
875
+ }, [flushInputBuffer]),
876
+ insertAtCursor
736
877
  };
737
878
  };
738
879
 
739
880
  // src/app/ui/components/InputPrompt.tsx
740
- import { useEffect as useEffect3, useMemo, useState as useState2, memo as memo2 } from "react";
881
+ import {
882
+ useEffect as useEffect3,
883
+ useMemo,
884
+ useState as useState2,
885
+ useRef as useRef3,
886
+ memo as memo2,
887
+ useLayoutEffect,
888
+ useCallback as useCallback2
889
+ } from "react";
741
890
  import { EventEmitter as EventEmitter2 } from "events";
742
891
 
743
892
  // src/app/ui/utils/slashRegistry.ts
744
893
  var getSlashCommands = () => [
745
- { name: "/help", description: "list commands" },
746
- { name: "/mcp", description: "list tools connected via MCP" },
747
- { name: "/tools", description: "list native tools" },
748
- { name: "/init", description: "create a new BluMa.md file with codebase documentation" },
749
- { name: "/clear", description: "clear history" }
894
+ {
895
+ name: "Ctrl+V / Cmd+V",
896
+ description: "paste from clipboard: image \u2192 file path under ~/.cache/bluma/clipboard; else text (all OS via clipboardy)",
897
+ category: "input"
898
+ },
899
+ {
900
+ name: "Ctrl+Shift+I",
901
+ description: "same as Ctrl+V / Cmd+V (paste image or text)",
902
+ category: "input"
903
+ },
904
+ {
905
+ name: "/help",
906
+ description: "list all slash commands (grouped)",
907
+ category: "help"
908
+ },
909
+ {
910
+ name: "/clear",
911
+ description: "clear chat below the welcome panel (welcome + session file unchanged)",
912
+ category: "session"
913
+ },
914
+ {
915
+ name: "/img",
916
+ description: "send local image(s) to the model: /img ./shot.png [your question]",
917
+ category: "agent"
918
+ },
919
+ {
920
+ name: "/image",
921
+ description: "alias of /img",
922
+ category: "agent"
923
+ },
924
+ {
925
+ name: "/skills",
926
+ description: "list load_skill modules (bundled + project + ~/.bluma)",
927
+ category: "inspect"
928
+ },
929
+ {
930
+ name: "/tools",
931
+ description: "list native tools (optional filter: /tools grep)",
932
+ category: "inspect"
933
+ },
934
+ {
935
+ name: "/mcp",
936
+ description: "list MCP tools (optional filter: /mcp fs)",
937
+ category: "inspect"
938
+ },
939
+ {
940
+ name: "/init",
941
+ description: "run init subagent \u2014 BluMa.md codebase documentation",
942
+ category: "agent"
943
+ }
750
944
  ];
945
+ var SLASH_ROUTE_KEYWORDS = new Set(
946
+ getSlashCommands().filter((c) => c.name.startsWith("/")).map((c) => c.name.slice(1).toLowerCase())
947
+ );
948
+ function isSlashRoutingLine(text) {
949
+ const t = text.trimStart();
950
+ if (!t.startsWith("/")) {
951
+ return false;
952
+ }
953
+ const rest = t.slice(1).trim();
954
+ if (!rest) {
955
+ return true;
956
+ }
957
+ const first = rest.split(/\s+/)[0];
958
+ if (first.includes("/") || first.includes("\\")) {
959
+ return false;
960
+ }
961
+ return SLASH_ROUTE_KEYWORDS.has(first.toLowerCase());
962
+ }
963
+ var CATEGORY_LABEL = {
964
+ session: "Session & UI",
965
+ inspect: "Inspect",
966
+ agent: "Agent",
967
+ help: "Help",
968
+ input: "Input"
969
+ };
970
+ function formatSlashHelpLines() {
971
+ const cmds = getSlashCommands();
972
+ const byCat = (cat) => cmds.filter((c) => c.category === cat);
973
+ const lines = [];
974
+ for (const cat of ["help", "session", "input", "agent", "inspect"]) {
975
+ const group = byCat(cat);
976
+ if (group.length === 0) continue;
977
+ lines.push(`${CATEGORY_LABEL[cat]}:`);
978
+ for (const c of group) {
979
+ lines.push(` ${c.name.padEnd(14)} ${c.description}`);
980
+ }
981
+ lines.push("");
982
+ }
983
+ return lines;
984
+ }
751
985
  var filterSlashCommands = (query) => {
752
986
  const list = getSlashCommands();
753
987
  const q = (query || "").toLowerCase();
@@ -942,6 +1176,390 @@ var TOOL_PREVIEW_MAX_LINES = 8;
942
1176
  var EXPAND_OVERLAY_MAX_LINES = 60;
943
1177
  var TERMINAL_RULE_CHAR = "\u2500";
944
1178
 
1179
+ // src/app/ui/utils/clipboardImage.ts
1180
+ import fs2 from "fs";
1181
+ import os2 from "os";
1182
+ import path2 from "path";
1183
+ import { spawn, execFile as execFileCb } from "child_process";
1184
+ import { promisify } from "util";
1185
+ var execFile = promisify(execFileCb);
1186
+ var CLIPBOARD_MAX_BYTES = 4 * 1024 * 1024;
1187
+ function extFromMagic(buf) {
1188
+ if (buf.length < 12) return null;
1189
+ if (buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71) {
1190
+ return ".png";
1191
+ }
1192
+ if (buf[0] === 255 && buf[1] === 216 && buf[2] === 255) {
1193
+ return ".jpg";
1194
+ }
1195
+ if (buf.slice(0, 6).toString("ascii") === "GIF87a" || buf.slice(0, 6).toString("ascii") === "GIF89a") {
1196
+ return ".gif";
1197
+ }
1198
+ if (buf.slice(0, 4).toString("ascii") === "RIFF" && buf.slice(8, 12).toString("ascii") === "WEBP") {
1199
+ return ".webp";
1200
+ }
1201
+ return null;
1202
+ }
1203
+ function runCapture(cmd, args) {
1204
+ return new Promise((resolve2) => {
1205
+ const chunks = [];
1206
+ const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
1207
+ child.stdout.on("data", (c) => chunks.push(c));
1208
+ child.stderr.on("data", () => {
1209
+ });
1210
+ child.on("error", () => resolve2(Buffer.alloc(0)));
1211
+ child.on("close", () => {
1212
+ resolve2(Buffer.concat(chunks));
1213
+ });
1214
+ });
1215
+ }
1216
+ async function tryWlPaste(mime) {
1217
+ const args = mime ? ["--type", mime] : [];
1218
+ return runCapture("wl-paste", args);
1219
+ }
1220
+ async function tryXclip(mime) {
1221
+ return runCapture("xclip", ["-selection", "clipboard", "-t", mime, "-o"]);
1222
+ }
1223
+ async function tryPngpaste() {
1224
+ return runCapture("pngpaste", ["-"]);
1225
+ }
1226
+ function writeBufferIfImage(baseDir, buf) {
1227
+ if (buf.length < 80 || buf.length > CLIPBOARD_MAX_BYTES) {
1228
+ return null;
1229
+ }
1230
+ const ext = extFromMagic(buf);
1231
+ if (!ext) {
1232
+ return null;
1233
+ }
1234
+ const out = path2.join(
1235
+ baseDir,
1236
+ `clip-${Date.now()}-${Math.random().toString(36).slice(2, 8)}${ext}`
1237
+ );
1238
+ fs2.writeFileSync(out, buf);
1239
+ return out;
1240
+ }
1241
+ function unlinkQuiet(p) {
1242
+ try {
1243
+ if (fs2.existsSync(p)) {
1244
+ fs2.unlinkSync(p);
1245
+ }
1246
+ } catch {
1247
+ }
1248
+ }
1249
+ async function tryDarwinClipboardy(baseDir) {
1250
+ let tmpPaths = [];
1251
+ try {
1252
+ const { default: cb } = await import("clipboardy");
1253
+ if (typeof cb.hasImages !== "function" || !await cb.hasImages()) {
1254
+ return null;
1255
+ }
1256
+ tmpPaths = await cb.readImages();
1257
+ if (!tmpPaths?.length) {
1258
+ return null;
1259
+ }
1260
+ for (const src of tmpPaths) {
1261
+ if (!src || !fs2.existsSync(src)) {
1262
+ continue;
1263
+ }
1264
+ let st;
1265
+ try {
1266
+ st = fs2.statSync(src);
1267
+ } catch {
1268
+ continue;
1269
+ }
1270
+ if (st.size < 80 || st.size > CLIPBOARD_MAX_BYTES) {
1271
+ continue;
1272
+ }
1273
+ const ext = path2.extname(src).toLowerCase();
1274
+ const safeExt = ext && /^\.(png|jpe?g|gif|webp)$/i.test(ext) ? ext : ".png";
1275
+ const out = path2.join(
1276
+ baseDir,
1277
+ `clip-${Date.now()}-${Math.random().toString(36).slice(2, 8)}${safeExt}`
1278
+ );
1279
+ fs2.copyFileSync(src, out);
1280
+ for (const p of tmpPaths) {
1281
+ unlinkQuiet(p);
1282
+ }
1283
+ return out;
1284
+ }
1285
+ } catch {
1286
+ }
1287
+ for (const p of tmpPaths) {
1288
+ unlinkQuiet(p);
1289
+ }
1290
+ return null;
1291
+ }
1292
+ async function tryWindowsPowerShell(outFile) {
1293
+ const ps = process.env.SystemRoot != null ? path2.join(
1294
+ process.env.SystemRoot,
1295
+ "System32",
1296
+ "WindowsPowerShell",
1297
+ "v1.0",
1298
+ "powershell.exe"
1299
+ ) : "powershell.exe";
1300
+ if (!fs2.existsSync(ps)) {
1301
+ return false;
1302
+ }
1303
+ const script = "$ErrorActionPreference='Stop';Add-Type -AssemblyName System.Windows.Forms;Add-Type -AssemblyName System.Drawing;$img=[System.Windows.Forms.Clipboard]::GetImage();if($null -eq $img){exit 2};$img.Save($env:BLUMA_CLIP_OUT,[System.Drawing.Imaging.ImageFormat]::Png);exit 0";
1304
+ try {
1305
+ await execFile(
1306
+ ps,
1307
+ ["-NoProfile", "-Sta", "-WindowStyle", "Hidden", "-Command", script],
1308
+ {
1309
+ env: { ...process.env, BLUMA_CLIP_OUT: outFile },
1310
+ windowsHide: true,
1311
+ timeout: 25e3
1312
+ }
1313
+ );
1314
+ } catch (e) {
1315
+ const status = e && typeof e === "object" && "status" in e ? e.status : null;
1316
+ if (status === 2) {
1317
+ return false;
1318
+ }
1319
+ return false;
1320
+ }
1321
+ try {
1322
+ const st = fs2.statSync(outFile);
1323
+ return st.size >= 80 && st.size <= CLIPBOARD_MAX_BYTES;
1324
+ } catch {
1325
+ return false;
1326
+ }
1327
+ }
1328
+ async function tryUnixLikeClipboard(baseDir) {
1329
+ const attempts = [];
1330
+ if (process.env.WAYLAND_DISPLAY) {
1331
+ attempts.push(() => tryWlPaste("image/png"));
1332
+ attempts.push(() => tryWlPaste("image/jpeg"));
1333
+ attempts.push(() => tryWlPaste());
1334
+ }
1335
+ attempts.push(() => tryXclip("image/png"));
1336
+ attempts.push(() => tryXclip("image/jpeg"));
1337
+ attempts.push(() => tryXclip("image/webp"));
1338
+ for (const run of attempts) {
1339
+ try {
1340
+ const buf = await run();
1341
+ const saved = writeBufferIfImage(baseDir, buf);
1342
+ if (saved) {
1343
+ return saved;
1344
+ }
1345
+ } catch {
1346
+ }
1347
+ }
1348
+ try {
1349
+ const buf = await tryPngpaste();
1350
+ return writeBufferIfImage(baseDir, buf);
1351
+ } catch {
1352
+ return null;
1353
+ }
1354
+ }
1355
+ async function readClipboardImageToTempFile() {
1356
+ const baseDir = path2.join(os2.homedir(), ".cache", "bluma", "clipboard");
1357
+ fs2.mkdirSync(baseDir, { recursive: true });
1358
+ if (process.platform === "darwin") {
1359
+ const fromCb = await tryDarwinClipboardy(baseDir);
1360
+ if (fromCb) {
1361
+ return fromCb;
1362
+ }
1363
+ try {
1364
+ const buf = await tryPngpaste();
1365
+ return writeBufferIfImage(baseDir, buf);
1366
+ } catch {
1367
+ return null;
1368
+ }
1369
+ }
1370
+ if (process.platform === "win32") {
1371
+ const outFile = path2.join(
1372
+ baseDir,
1373
+ `clip-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.png`
1374
+ );
1375
+ const ok = await tryWindowsPowerShell(outFile);
1376
+ return ok ? outFile : null;
1377
+ }
1378
+ return tryUnixLikeClipboard(baseDir);
1379
+ }
1380
+
1381
+ // src/app/ui/utils/inlineImageInputLabels.ts
1382
+ function resolveInlineImageLabels(raw, idToPath) {
1383
+ return raw.replace(/\[IMAGE #(\d+)\]/g, (full, numStr) => {
1384
+ const id = Number.parseInt(numStr, 10);
1385
+ if (Number.isNaN(id)) {
1386
+ return full;
1387
+ }
1388
+ const p = idToPath.get(id);
1389
+ if (!p) {
1390
+ return full;
1391
+ }
1392
+ return p.includes(" ") ? `"${p}"` : p;
1393
+ });
1394
+ }
1395
+
1396
+ // src/app/agent/utils/user_message_images.ts
1397
+ import fs3 from "fs";
1398
+ import os3 from "os";
1399
+ import path3 from "path";
1400
+ var IMAGE_EXT = /\.(png|jpe?g|gif|webp|bmp)$/i;
1401
+ var MAX_IMAGE_BYTES = 4 * 1024 * 1024;
1402
+ var MAX_IMAGES = 6;
1403
+ var MIME = {
1404
+ ".png": "image/png",
1405
+ ".jpg": "image/jpeg",
1406
+ ".jpeg": "image/jpeg",
1407
+ ".gif": "image/gif",
1408
+ ".webp": "image/webp",
1409
+ ".bmp": "image/bmp"
1410
+ };
1411
+ function expandUserPath(p) {
1412
+ const t = p.trim();
1413
+ if (t.startsWith("~")) {
1414
+ return path3.join(os3.homedir(), t.slice(1).replace(/^\//, ""));
1415
+ }
1416
+ return t;
1417
+ }
1418
+ function isPathAllowed(absResolved, cwd) {
1419
+ const resolved = path3.normalize(path3.resolve(absResolved));
1420
+ const cwdR = path3.normalize(path3.resolve(cwd));
1421
+ const homeR = path3.normalize(path3.resolve(os3.homedir()));
1422
+ const underCwd = resolved === cwdR || resolved.startsWith(cwdR + path3.sep);
1423
+ const underHome = resolved === homeR || resolved.startsWith(homeR + path3.sep);
1424
+ return underCwd || underHome;
1425
+ }
1426
+ function mimeFor(abs) {
1427
+ const ext = path3.extname(abs).toLowerCase();
1428
+ return MIME[ext] || "application/octet-stream";
1429
+ }
1430
+ var IMAGE_EXT_SRC = String.raw`(?:png|jpe?g|gif|webp|bmp)`;
1431
+ var UNIX_PATH_WITH_IMAGE_EXT = new RegExp(
1432
+ String.raw`(?:^|[\s"'(])(\/(?:[^\s/'"<>|]+\/)*[^\s/'"<>|]+\.${IMAGE_EXT_SRC})\b`,
1433
+ "gi"
1434
+ );
1435
+ var WIN_PATH_WITH_IMAGE_EXT = new RegExp(
1436
+ String.raw`(?:^|[\s"'(])(([A-Za-z]:(?:\\[^\\/:*?"<>|\r\n]+)+\.${IMAGE_EXT_SRC}))\b`,
1437
+ "gi"
1438
+ );
1439
+ function collectImagePathStrings(raw) {
1440
+ const found = [];
1441
+ const seen = /* @__PURE__ */ new Set();
1442
+ const push = (p) => {
1443
+ const t = p.trim();
1444
+ if (!t || seen.has(t)) return;
1445
+ seen.add(t);
1446
+ found.push(t);
1447
+ };
1448
+ const quoted = /(["'])((?:(?!\1).)*?\.(?:png|jpe?g|gif|webp|bmp))\1/gi;
1449
+ for (const m of raw.matchAll(quoted)) {
1450
+ push(m[2].trim());
1451
+ }
1452
+ const withoutQuoted = raw.replace(quoted, " ");
1453
+ const tokens = withoutQuoted.split(/\s+/);
1454
+ for (let tok of tokens) {
1455
+ tok = tok.replace(/^[([{]+/, "").replace(/[)\]},;:]+$/, "");
1456
+ if (!tok || !IMAGE_EXT.test(tok)) continue;
1457
+ push(tok);
1458
+ }
1459
+ for (const m of raw.matchAll(UNIX_PATH_WITH_IMAGE_EXT)) {
1460
+ if (m[1]) {
1461
+ push(m[1]);
1462
+ }
1463
+ }
1464
+ for (const m of raw.matchAll(WIN_PATH_WITH_IMAGE_EXT)) {
1465
+ push(m[1]);
1466
+ }
1467
+ return found;
1468
+ }
1469
+ function resolveImagePath(candidate, cwd) {
1470
+ const expanded = expandUserPath(candidate);
1471
+ const abs = path3.isAbsolute(expanded) ? path3.normalize(expanded) : path3.normalize(path3.resolve(cwd, expanded));
1472
+ if (!isPathAllowed(abs, cwd)) return null;
1473
+ try {
1474
+ if (!fs3.existsSync(abs) || !fs3.statSync(abs).isFile()) return null;
1475
+ } catch {
1476
+ return null;
1477
+ }
1478
+ if (!IMAGE_EXT.test(abs)) return null;
1479
+ return abs;
1480
+ }
1481
+ function tryPasteChunkAsSingleImagePath(chunk, cwd) {
1482
+ const normalized = chunk.replace(/\r\n/g, "\n");
1483
+ const lines = normalized.split("\n");
1484
+ const t = (lines[0] ?? "").trim();
1485
+ if (!t) {
1486
+ return null;
1487
+ }
1488
+ if (lines.length > 1 && lines.slice(1).some((l) => l.trim().length > 0)) {
1489
+ return null;
1490
+ }
1491
+ const paths = collectImagePathStrings(t);
1492
+ if (paths.length !== 1) {
1493
+ return null;
1494
+ }
1495
+ if (stripImagePathStrings(t, paths).trim().length > 0) {
1496
+ return null;
1497
+ }
1498
+ const abs = resolveImagePath(paths[0], cwd);
1499
+ if (!abs) {
1500
+ return null;
1501
+ }
1502
+ try {
1503
+ const st = fs3.statSync(abs);
1504
+ if (!st.isFile() || st.size > MAX_IMAGE_BYTES) {
1505
+ return null;
1506
+ }
1507
+ } catch {
1508
+ return null;
1509
+ }
1510
+ return abs;
1511
+ }
1512
+ function stripImagePathStrings(text, paths) {
1513
+ let out = text;
1514
+ for (const p of paths) {
1515
+ const q = p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1516
+ out = out.replace(new RegExp(`["']${q}["']`, "gi"), " ");
1517
+ out = out.replace(new RegExp(`(?<=^|\\s)${q}(?=\\s|$)`, "gi"), " ");
1518
+ }
1519
+ return out.replace(/\s{2,}/g, " ").trim();
1520
+ }
1521
+ function buildUserMessageContent(raw, cwd) {
1522
+ const trimmed = raw.trim();
1523
+ if (!trimmed) return trimmed;
1524
+ const candidates = collectImagePathStrings(trimmed);
1525
+ const resolvedAbs = [];
1526
+ const usedStrings = [];
1527
+ for (const c of candidates) {
1528
+ if (resolvedAbs.length >= MAX_IMAGES) break;
1529
+ const abs = resolveImagePath(c, cwd);
1530
+ if (!abs) continue;
1531
+ try {
1532
+ const st = fs3.statSync(abs);
1533
+ if (st.size > MAX_IMAGE_BYTES) continue;
1534
+ } catch {
1535
+ continue;
1536
+ }
1537
+ resolvedAbs.push(abs);
1538
+ usedStrings.push(c);
1539
+ }
1540
+ if (resolvedAbs.length === 0) {
1541
+ return trimmed;
1542
+ }
1543
+ let textPart = stripImagePathStrings(trimmed, usedStrings);
1544
+ if (!textPart) {
1545
+ textPart = "(User attached image(s) only \u2014 describe, compare, or answer using the image(s).)";
1546
+ }
1547
+ const parts = [{ type: "text", text: textPart }];
1548
+ for (const abs of resolvedAbs) {
1549
+ const buf = fs3.readFileSync(abs);
1550
+ const b64 = buf.toString("base64");
1551
+ const mime = mimeFor(abs);
1552
+ parts.push({
1553
+ type: "image_url",
1554
+ image_url: {
1555
+ url: `data:${mime};base64,${b64}`,
1556
+ detail: "auto"
1557
+ }
1558
+ });
1559
+ }
1560
+ return parts;
1561
+ }
1562
+
945
1563
  // src/app/ui/components/InputPrompt.tsx
946
1564
  import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
947
1565
  var uiEventBus = global.__bluma_ui_eventbus__ || new EventEmitter2();
@@ -1006,7 +1624,9 @@ var SlashSuggestions = memo2(({
1006
1624
  s.name,
1007
1625
  " ",
1008
1626
  /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
1009
- "- ",
1627
+ "[",
1628
+ s.category,
1629
+ "] ",
1010
1630
  s.description
1011
1631
  ] })
1012
1632
  ] })
@@ -1014,13 +1634,21 @@ var SlashSuggestions = memo2(({
1014
1634
  }) }));
1015
1635
  SlashSuggestions.displayName = "SlashSuggestions";
1016
1636
  var InputRuleLine = memo2(() => {
1017
- const { stdout } = useStdout();
1637
+ const { stdout } = useStdout2();
1018
1638
  const cols = stdout?.columns ?? 80;
1019
1639
  const n = Math.max(8, cols);
1020
1640
  return /* @__PURE__ */ jsx2(Text2, { color: "white", children: TERMINAL_RULE_CHAR.repeat(n) });
1021
1641
  });
1022
1642
  InputRuleLine.displayName = "InputRuleLine";
1023
- var Footer = memo2(({ isReadOnly }) => /* @__PURE__ */ jsx2(Box2, { justifyContent: "center", children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: isReadOnly ? "ctrl+c to exit | Enter to send message | Shift+Enter for new line | esc interrupt | Ctrl+O expand last clip" : "ctrl+c to exit | Enter to submit | Shift+Enter for new line | /help commands | esc interrupt | Ctrl+O expand last clip" }) }));
1643
+ var Footer = memo2(
1644
+ ({
1645
+ isReadOnly,
1646
+ clipHint
1647
+ }) => /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", alignItems: "center", children: [
1648
+ clipHint ? /* @__PURE__ */ jsx2(Box2, { marginBottom: 0, children: /* @__PURE__ */ jsx2(Text2, { color: "yellow", dimColor: true, children: clipHint }) }) : null,
1649
+ /* @__PURE__ */ jsx2(Box2, { justifyContent: "center", children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: isReadOnly ? "ctrl+c to exit | Enter to send message | Shift+Enter for new line | esc interrupt | Ctrl+O expand last clip" : "ctrl+c to exit | Enter submit | Shift+Enter newline | Ctrl+V / Cmd+V paste | /help \xB7 /img \xB7 esc \xB7 Ctrl+O" }) })
1650
+ ] })
1651
+ );
1024
1652
  Footer.displayName = "Footer";
1025
1653
  var TextLinesRenderer = memo2(({
1026
1654
  lines,
@@ -1058,19 +1686,52 @@ var InputPrompt = memo2(({
1058
1686
  onInterrupt,
1059
1687
  disableWhileProcessing = false
1060
1688
  }) => {
1061
- const { stdout } = useStdout();
1689
+ const { stdout } = useStdout2();
1062
1690
  const [viewWidth] = useState2(() => stdout.columns - 6);
1063
1691
  const [slashOpen, setSlashOpen] = useState2(false);
1064
1692
  const [slashIndex, setSlashIndex] = useState2(0);
1693
+ const [clipHint, setClipHint] = useState2(null);
1694
+ const textRef = useRef3("");
1695
+ const cursorRef = useRef3(0);
1696
+ const inlineImageByIdRef = useRef3(/* @__PURE__ */ new Map());
1697
+ const nextInlineImageIdRef = useRef3(1);
1698
+ const clearInlineImageSlots = () => {
1699
+ inlineImageByIdRef.current.clear();
1700
+ nextInlineImageIdRef.current = 1;
1701
+ };
1702
+ const cwd = process.cwd();
1703
+ const transformInputChunk = useCallback2(
1704
+ (chunk) => {
1705
+ const abs = tryPasteChunkAsSingleImagePath(chunk, cwd);
1706
+ if (!abs) {
1707
+ return chunk;
1708
+ }
1709
+ const id = nextInlineImageIdRef.current++;
1710
+ inlineImageByIdRef.current.set(id, abs);
1711
+ return /\n$/.test(chunk) ? `[IMAGE #${id}]
1712
+ ` : `[IMAGE #${id}] `;
1713
+ },
1714
+ [cwd]
1715
+ );
1065
1716
  const permissiveOnSubmit = (value) => {
1066
1717
  const trimmed = (value || "").trim();
1718
+ const map = inlineImageByIdRef.current;
1719
+ const resolved = resolveInlineImageLabels(value, map);
1067
1720
  if (isReadOnly) {
1068
1721
  if (trimmed.length > 0) {
1069
- uiEventBus.emit("user_overlay", { kind: "message", payload: trimmed, ts: Date.now() });
1722
+ uiEventBus.emit("user_overlay", {
1723
+ kind: "message",
1724
+ payload: resolved.trim(),
1725
+ ts: Date.now()
1726
+ });
1727
+ clearInlineImageSlots();
1070
1728
  }
1071
1729
  return;
1072
1730
  }
1073
- onSubmit(value);
1731
+ if (trimmed.length > 0) {
1732
+ onSubmit(resolved);
1733
+ clearInlineImageSlots();
1734
+ }
1074
1735
  };
1075
1736
  const { text, cursorPosition, setText } = useCustomInput({
1076
1737
  onSubmit: (value) => {
@@ -1080,8 +1741,48 @@ var InputPrompt = memo2(({
1080
1741
  },
1081
1742
  viewWidth,
1082
1743
  isReadOnly,
1083
- onInterrupt
1744
+ onInterrupt,
1745
+ transformInputChunk: !disableWhileProcessing ? transformInputChunk : void 0,
1746
+ onChordPaste: !disableWhileProcessing ? (insertAtCursor) => {
1747
+ void (async () => {
1748
+ const padBefore = () => {
1749
+ const t = textRef.current;
1750
+ const c = cursorRef.current;
1751
+ return t.length > 0 && c > 0 && t[c - 1] !== "\n" && !/\s/.test(t[c - 1]) ? " " : "";
1752
+ };
1753
+ const filePath = await readClipboardImageToTempFile();
1754
+ if (filePath) {
1755
+ const id = nextInlineImageIdRef.current++;
1756
+ inlineImageByIdRef.current.set(id, filePath);
1757
+ insertAtCursor(`${padBefore()}[IMAGE #${id}] `);
1758
+ return;
1759
+ }
1760
+ try {
1761
+ const { default: cb } = await import("clipboardy");
1762
+ const pasted = await cb.read();
1763
+ const t = pasted != null ? String(pasted).replace(/\r\n/g, "\n") : "";
1764
+ if (t.length > 0) {
1765
+ const abs = tryPasteChunkAsSingleImagePath(t, cwd);
1766
+ if (abs) {
1767
+ const id = nextInlineImageIdRef.current++;
1768
+ inlineImageByIdRef.current.set(id, abs);
1769
+ insertAtCursor(`${padBefore()}[IMAGE #${id}] `);
1770
+ return;
1771
+ }
1772
+ insertAtCursor(`${padBefore()}${t}`);
1773
+ return;
1774
+ }
1775
+ } catch {
1776
+ }
1777
+ setClipHint("Clipboard: empty or unreadable (GUI session; Linux: wl-clipboard/xclip).");
1778
+ setTimeout(() => setClipHint(null), 4e3);
1779
+ })();
1780
+ } : void 0
1084
1781
  });
1782
+ useLayoutEffect(() => {
1783
+ textRef.current = text;
1784
+ cursorRef.current = cursorPosition;
1785
+ }, [text, cursorPosition]);
1085
1786
  const linesData = useMemo(() => {
1086
1787
  const lines = text.split("\n");
1087
1788
  let remainingChars = cursorPosition;
@@ -1148,7 +1849,6 @@ var InputPrompt = memo2(({
1148
1849
  delete globalThis.__BLUMA_FORCE_CURSOR_END__;
1149
1850
  }
1150
1851
  }, [text, setText]);
1151
- const cwd = process.cwd();
1152
1852
  const pathAutocomplete = useAtCompletion({ cwd, text, cursorPosition, setText });
1153
1853
  useInput2((input, key) => {
1154
1854
  if (key.backspace || key.delete || key.ctrl || key.meta) return;
@@ -1208,7 +1908,7 @@ var InputPrompt = memo2(({
1208
1908
  slashOpen && slashSuggestions.length > 0 && /* @__PURE__ */ jsx2(SlashSuggestions, { suggestions: slashSuggestions, selectedIndex: slashIndex })
1209
1909
  ] }) }),
1210
1910
  /* @__PURE__ */ jsx2(InputRuleLine, {}),
1211
- /* @__PURE__ */ jsx2(Footer, { isReadOnly })
1911
+ /* @__PURE__ */ jsx2(Footer, { isReadOnly, clipHint })
1212
1912
  ] });
1213
1913
  });
1214
1914
  InputPrompt.displayName = "InputPrompt";
@@ -1266,7 +1966,7 @@ var InteractiveMenu = memo3(InteractiveMenuComponent);
1266
1966
 
1267
1967
  // src/app/ui/components/promptRenderers.tsx
1268
1968
  import { Box as Box5, Text as Text5 } from "ink";
1269
- import path2 from "path";
1969
+ import path4 from "path";
1270
1970
 
1271
1971
  // src/app/ui/components/SimpleDiff.tsx
1272
1972
  import { Box as Box4, Text as Text4 } from "ink";
@@ -1312,7 +2012,7 @@ var SimpleDiff = ({ text, maxHeight }) => {
1312
2012
  // src/app/ui/components/promptRenderers.tsx
1313
2013
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1314
2014
  var getBasePath = (filePath) => {
1315
- return path2.basename(filePath);
2015
+ return path4.basename(filePath);
1316
2016
  };
1317
2017
  var renderShellCommand = ({ toolCall }) => {
1318
2018
  let command = "";
@@ -1526,6 +2226,25 @@ var promptRenderers = {
1526
2226
 
1527
2227
  // src/app/ui/theme/m3Layout.tsx
1528
2228
  import { Box as Box6, Text as Text6 } from "ink";
2229
+
2230
+ // src/app/ui/utils/formatTurnDurationMs.ts
2231
+ function formatTurnDurationMs(ms) {
2232
+ if (ms < 0) {
2233
+ return "0s";
2234
+ }
2235
+ const secTotal = Math.round(ms / 1e3);
2236
+ if (secTotal < 60) {
2237
+ if (ms < 1e4) {
2238
+ return `${(ms / 1e3).toFixed(1)}s`;
2239
+ }
2240
+ return `${secTotal}s`;
2241
+ }
2242
+ const min = Math.floor(secTotal / 60);
2243
+ const sec = secTotal % 60;
2244
+ return `${min}min \u25CF ${sec}s`;
2245
+ }
2246
+
2247
+ // src/app/ui/theme/m3Layout.tsx
1529
2248
  import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1530
2249
  function ChatBlock({
1531
2250
  children,
@@ -1533,6 +2252,24 @@ function ChatBlock({
1533
2252
  }) {
1534
2253
  return /* @__PURE__ */ jsx6(Box6, { flexDirection: "column", marginBottom, children });
1535
2254
  }
2255
+ function ChatUserImageBlock({
2256
+ imageCount,
2257
+ caption,
2258
+ captionDim = false
2259
+ }) {
2260
+ if (imageCount < 1) {
2261
+ return null;
2262
+ }
2263
+ const cap = caption?.trim() ?? "";
2264
+ const capLines = cap.length > 0 ? cap.split("\n") : [];
2265
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", marginBottom: 0, children: [
2266
+ Array.from({ length: imageCount }, (_, i) => /* @__PURE__ */ jsx6(Text6, { bold: true, color: BLUMA_TERMINAL.brandMagenta, children: `[IMAGE #${i + 1}]` }, `img-${i}`)),
2267
+ capLines.map((line, i) => /* @__PURE__ */ jsxs6(Box6, { flexDirection: "row", flexWrap: "wrap", children: [
2268
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: i === 0 ? "\u2514\u2500 " : " " }),
2269
+ captionDim ? /* @__PURE__ */ jsx6(Text6, { dimColor: true, wrap: "wrap", children: line }) : /* @__PURE__ */ jsx6(Text6, { wrap: "wrap", children: line })
2270
+ ] }, `cap-${i}`))
2271
+ ] });
2272
+ }
1536
2273
  function ChatUserMessage({ children }) {
1537
2274
  return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "row", marginBottom: 1, flexWrap: "wrap", alignItems: "flex-start", children: [
1538
2275
  /* @__PURE__ */ jsxs6(Text6, { color: "white", bold: true, children: [
@@ -1554,13 +2291,10 @@ function ChatStatusRow({ children }) {
1554
2291
  children
1555
2292
  ] });
1556
2293
  }
1557
- function ChatTurnDuration({ secondsFormatted }) {
2294
+ function ChatTurnDuration({ durationMs }) {
1558
2295
  return /* @__PURE__ */ jsx6(Box6, { marginBottom: 1, children: /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
1559
2296
  /* @__PURE__ */ jsx6(Text6, { color: BLUMA_TERMINAL.brandBlue, children: "\xB7 " }),
1560
- /* @__PURE__ */ jsxs6(Text6, { color: BLUMA_TERMINAL.brandMagenta, children: [
1561
- secondsFormatted,
1562
- "s"
1563
- ] })
2297
+ /* @__PURE__ */ jsx6(Text6, { color: BLUMA_TERMINAL.brandMagenta, children: formatTurnDurationMs(durationMs) })
1564
2298
  ] }) });
1565
2299
  }
1566
2300
  var M3StatusStrip = ChatStatusRow;
@@ -1583,25 +2317,25 @@ var ConfirmationPrompt = memo4(ConfirmationPromptComponent);
1583
2317
 
1584
2318
  // src/app/agent/agent.ts
1585
2319
  import * as dotenv from "dotenv";
1586
- import path17 from "path";
1587
- import os11 from "os";
2320
+ import path19 from "path";
2321
+ import os14 from "os";
1588
2322
 
1589
2323
  // src/app/agent/tool_invoker.ts
1590
- import { promises as fs9 } from "fs";
1591
- import path11 from "path";
2324
+ import { promises as fs11 } from "fs";
2325
+ import path13 from "path";
1592
2326
  import { fileURLToPath } from "url";
1593
2327
 
1594
2328
  // src/app/agent/tools/natives/edit.ts
1595
- import path3 from "path";
1596
- import os from "os";
1597
- import { promises as fs2 } from "fs";
2329
+ import path5 from "path";
2330
+ import os4 from "os";
2331
+ import { promises as fs4 } from "fs";
1598
2332
  import { diffLines } from "diff";
1599
2333
  var MAX_DIFF_SIZE = 5e4;
1600
2334
  var MAX_FILE_SIZE = 10 * 1024 * 1024;
1601
2335
  function normalizePath(filePath) {
1602
2336
  try {
1603
2337
  filePath = filePath.trim();
1604
- if (os.platform() === "win32") {
2338
+ if (os4.platform() === "win32") {
1605
2339
  const winDriveRegex = /^\/([a-zA-Z])[:/]/;
1606
2340
  const match = filePath.match(winDriveRegex);
1607
2341
  if (match) {
@@ -1611,7 +2345,7 @@ function normalizePath(filePath) {
1611
2345
  }
1612
2346
  filePath = filePath.replace(/\//g, "\\");
1613
2347
  }
1614
- return path3.normalize(path3.resolve(filePath));
2348
+ return path5.normalize(path5.resolve(filePath));
1615
2349
  } catch (e) {
1616
2350
  throw new Error(`Failed to normalize path "${filePath}": ${e.message}`);
1617
2351
  }
@@ -1735,7 +2469,7 @@ async function calculateEdit(filePath, oldString, newString, expectedReplacement
1735
2469
  let normalizedOldString = oldString.replace(/\r\n/g, "\n");
1736
2470
  let occurrences = 0;
1737
2471
  try {
1738
- const stats = await fs2.stat(normalizedFilePath);
2472
+ const stats = await fs4.stat(normalizedFilePath);
1739
2473
  if (stats.size > MAX_FILE_SIZE) {
1740
2474
  error = {
1741
2475
  display: `File too large (${(stats.size / 1024 / 1024).toFixed(2)}MB). Maximum allowed: ${MAX_FILE_SIZE / 1024 / 1024}MB`,
@@ -1743,7 +2477,7 @@ async function calculateEdit(filePath, oldString, newString, expectedReplacement
1743
2477
  };
1744
2478
  return { currentContent, newContent: "", occurrences: 0, error, isNewFile };
1745
2479
  }
1746
- currentContent = await fs2.readFile(normalizedFilePath, "utf-8");
2480
+ currentContent = await fs4.readFile(normalizedFilePath, "utf-8");
1747
2481
  currentContent = currentContent.replace(/\r\n/g, "\n");
1748
2482
  } catch (e) {
1749
2483
  if (e.code !== "ENOENT") {
@@ -1872,7 +2606,7 @@ async function editTool(args) {
1872
2606
  };
1873
2607
  }
1874
2608
  const cwd = process.cwd();
1875
- if (!normalizedFilePath.startsWith(cwd) && !path3.isAbsolute(file_path)) {
2609
+ if (!normalizedFilePath.startsWith(cwd) && !path5.isAbsolute(file_path)) {
1876
2610
  return {
1877
2611
  success: false,
1878
2612
  error: `Invalid parameters: file_path must be within the current working directory or be an absolute path.`,
@@ -1893,11 +2627,11 @@ async function editTool(args) {
1893
2627
  file_path: normalizedFilePath
1894
2628
  };
1895
2629
  }
1896
- const dirPath = path3.dirname(normalizedFilePath);
1897
- await fs2.mkdir(dirPath, { recursive: true });
1898
- await fs2.writeFile(normalizedFilePath, editData.newContent, "utf-8");
1899
- const relativePath = path3.relative(process.cwd(), normalizedFilePath);
1900
- const filename = path3.basename(normalizedFilePath);
2630
+ const dirPath = path5.dirname(normalizedFilePath);
2631
+ await fs4.mkdir(dirPath, { recursive: true });
2632
+ await fs4.writeFile(normalizedFilePath, editData.newContent, "utf-8");
2633
+ const relativePath = path5.relative(process.cwd(), normalizedFilePath);
2634
+ const filename = path5.basename(normalizedFilePath);
1901
2635
  if (editData.isNewFile) {
1902
2636
  return {
1903
2637
  success: true,
@@ -1951,8 +2685,8 @@ function message(args) {
1951
2685
  }
1952
2686
 
1953
2687
  // src/app/agent/tools/natives/ls.ts
1954
- import { promises as fs3 } from "fs";
1955
- import path4 from "path";
2688
+ import { promises as fs5 } from "fs";
2689
+ import path6 from "path";
1956
2690
  var DEFAULT_IGNORE = /* @__PURE__ */ new Set([
1957
2691
  ".git",
1958
2692
  ".gitignore",
@@ -1980,8 +2714,8 @@ async function ls(args) {
1980
2714
  max_depth
1981
2715
  } = args;
1982
2716
  try {
1983
- const basePath = path4.resolve(directory_path);
1984
- if (!(await fs3.stat(basePath)).isDirectory()) {
2717
+ const basePath = path6.resolve(directory_path);
2718
+ if (!(await fs5.stat(basePath)).isDirectory()) {
1985
2719
  throw new Error(`Directory '${directory_path}' not found.`);
1986
2720
  }
1987
2721
  const allIgnorePatterns = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore_patterns]);
@@ -1990,11 +2724,11 @@ async function ls(args) {
1990
2724
  const allDirs = [];
1991
2725
  const walk = async (currentDir, currentDepth) => {
1992
2726
  if (max_depth !== void 0 && currentDepth > max_depth) return;
1993
- const entries = await fs3.readdir(currentDir, { withFileTypes: true });
2727
+ const entries = await fs5.readdir(currentDir, { withFileTypes: true });
1994
2728
  for (const entry of entries) {
1995
2729
  const entryName = entry.name;
1996
- const fullPath = path4.join(currentDir, entryName);
1997
- const posixPath = fullPath.split(path4.sep).join("/");
2730
+ const fullPath = path6.join(currentDir, entryName);
2731
+ const posixPath = fullPath.split(path6.sep).join("/");
1998
2732
  const isHidden = entryName.startsWith(".");
1999
2733
  if (allIgnorePatterns.has(entryName) || isHidden && !show_hidden) {
2000
2734
  continue;
@@ -2005,7 +2739,7 @@ async function ls(args) {
2005
2739
  await walk(fullPath, currentDepth + 1);
2006
2740
  }
2007
2741
  } else if (entry.isFile()) {
2008
- if (!normalizedExtensions || normalizedExtensions.includes(path4.extname(entryName).toLowerCase())) {
2742
+ if (!normalizedExtensions || normalizedExtensions.includes(path6.extname(entryName).toLowerCase())) {
2009
2743
  allFiles.push(posixPath);
2010
2744
  }
2011
2745
  }
@@ -2016,7 +2750,7 @@ async function ls(args) {
2016
2750
  allDirs.sort();
2017
2751
  return {
2018
2752
  success: true,
2019
- path: basePath.split(path4.sep).join("/"),
2753
+ path: basePath.split(path6.sep).join("/"),
2020
2754
  recursive,
2021
2755
  total_files: allFiles.length,
2022
2756
  total_directories: allDirs.length,
@@ -2032,17 +2766,17 @@ async function ls(args) {
2032
2766
  }
2033
2767
 
2034
2768
  // src/app/agent/tools/natives/readLines.ts
2035
- import { promises as fs4 } from "fs";
2769
+ import { promises as fs6 } from "fs";
2036
2770
  async function readLines(args) {
2037
2771
  const { filepath, start_line, end_line } = args;
2038
2772
  try {
2039
- if (!(await fs4.stat(filepath)).isFile()) {
2773
+ if (!(await fs6.stat(filepath)).isFile()) {
2040
2774
  throw new Error(`File '${filepath}' not found or is not a file.`);
2041
2775
  }
2042
2776
  if (start_line < 1 || end_line < start_line) {
2043
2777
  throw new Error("Invalid line range. start_line must be >= 1 and end_line must be >= start_line.");
2044
2778
  }
2045
- const fileContent = await fs4.readFile(filepath, "utf-8");
2779
+ const fileContent = await fs6.readFile(filepath, "utf-8");
2046
2780
  const lines = fileContent.split("\n");
2047
2781
  const total_lines = lines.length;
2048
2782
  const startIndex = start_line - 1;
@@ -2070,12 +2804,12 @@ async function readLines(args) {
2070
2804
 
2071
2805
  // src/app/agent/tools/natives/count_lines.ts
2072
2806
  import { createReadStream } from "fs";
2073
- import { promises as fs5 } from "fs";
2807
+ import { promises as fs7 } from "fs";
2074
2808
  import readline from "readline";
2075
2809
  async function countLines(args) {
2076
2810
  const { filepath } = args;
2077
2811
  try {
2078
- if (!(await fs5.stat(filepath)).isFile()) {
2812
+ if (!(await fs7.stat(filepath)).isFile()) {
2079
2813
  throw new Error(`File '${filepath}' not found or is not a file.`);
2080
2814
  }
2081
2815
  const fileStream = createReadStream(filepath);
@@ -2094,18 +2828,18 @@ async function countLines(args) {
2094
2828
  }
2095
2829
 
2096
2830
  // src/app/agent/tools/natives/todo.ts
2097
- import * as fs6 from "fs";
2098
- import * as path5 from "path";
2831
+ import * as fs8 from "fs";
2832
+ import * as path7 from "path";
2099
2833
  var taskStore = [];
2100
2834
  var nextId = 1;
2101
2835
  function getTodoFilePath() {
2102
- return path5.join(process.cwd(), ".bluma", "todo.json");
2836
+ return path7.join(process.cwd(), ".bluma", "todo.json");
2103
2837
  }
2104
2838
  function loadTasksFromFile() {
2105
2839
  try {
2106
2840
  const filePath = getTodoFilePath();
2107
- if (fs6.existsSync(filePath)) {
2108
- const data = fs6.readFileSync(filePath, "utf-8");
2841
+ if (fs8.existsSync(filePath)) {
2842
+ const data = fs8.readFileSync(filePath, "utf-8");
2109
2843
  const parsed = JSON.parse(data);
2110
2844
  if (Array.isArray(parsed.tasks)) {
2111
2845
  taskStore = parsed.tasks;
@@ -2118,11 +2852,11 @@ function loadTasksFromFile() {
2118
2852
  function saveTasksToFile() {
2119
2853
  try {
2120
2854
  const filePath = getTodoFilePath();
2121
- const dir = path5.dirname(filePath);
2122
- if (!fs6.existsSync(dir)) {
2123
- fs6.mkdirSync(dir, { recursive: true });
2855
+ const dir = path7.dirname(filePath);
2856
+ if (!fs8.existsSync(dir)) {
2857
+ fs8.mkdirSync(dir, { recursive: true });
2124
2858
  }
2125
- fs6.writeFileSync(filePath, JSON.stringify({
2859
+ fs8.writeFileSync(filePath, JSON.stringify({
2126
2860
  tasks: taskStore,
2127
2861
  nextId,
2128
2862
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -2297,7 +3031,7 @@ async function todo(args) {
2297
3031
  }
2298
3032
 
2299
3033
  // src/app/agent/tools/natives/find_by_name.ts
2300
- import path6 from "path";
3034
+ import path8 from "path";
2301
3035
  import { promises as fsPromises } from "fs";
2302
3036
  var DEFAULT_IGNORE_PATTERNS = [
2303
3037
  "node_modules",
@@ -2337,7 +3071,7 @@ function shouldIgnore(name, ignorePatterns, includeHidden) {
2337
3071
  }
2338
3072
  function matchesExtensions(filename, extensions) {
2339
3073
  if (!extensions || extensions.length === 0) return true;
2340
- const ext = path6.extname(filename).toLowerCase();
3074
+ const ext = path8.extname(filename).toLowerCase();
2341
3075
  return extensions.some((e) => {
2342
3076
  const normalizedExt = e.startsWith(".") ? e.toLowerCase() : `.${e.toLowerCase()}`;
2343
3077
  return ext === normalizedExt;
@@ -2359,8 +3093,8 @@ async function searchDirectory(dir, pattern, baseDir, options, results) {
2359
3093
  if (shouldIgnore(name, options.ignorePatterns, options.includeHidden)) {
2360
3094
  continue;
2361
3095
  }
2362
- const fullPath = path6.join(dir, name);
2363
- const relativePath = path6.relative(baseDir, fullPath);
3096
+ const fullPath = path8.join(dir, name);
3097
+ const relativePath = path8.relative(baseDir, fullPath);
2364
3098
  if (entry.isDirectory()) {
2365
3099
  if (pattern.test(name)) {
2366
3100
  results.push({
@@ -2416,7 +3150,7 @@ async function findByName(args) {
2416
3150
  error: "Pattern is required and must be a string"
2417
3151
  };
2418
3152
  }
2419
- const resolvedDir = path6.resolve(directory);
3153
+ const resolvedDir = path8.resolve(directory);
2420
3154
  try {
2421
3155
  const stats = await fsPromises.stat(resolvedDir);
2422
3156
  if (!stats.isDirectory()) {
@@ -2477,7 +3211,7 @@ async function findByName(args) {
2477
3211
  }
2478
3212
 
2479
3213
  // src/app/agent/tools/natives/grep_search.ts
2480
- import path7 from "path";
3214
+ import path9 from "path";
2481
3215
  import { promises as fsPromises2 } from "fs";
2482
3216
  var MAX_RESULTS3 = 100;
2483
3217
  var MAX_FILE_SIZE2 = 1024 * 1024;
@@ -2560,8 +3294,8 @@ var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
2560
3294
  "Rakefile"
2561
3295
  ]);
2562
3296
  function isTextFile(filepath) {
2563
- const ext = path7.extname(filepath).toLowerCase();
2564
- const basename = path7.basename(filepath);
3297
+ const ext = path9.extname(filepath).toLowerCase();
3298
+ const basename = path9.basename(filepath);
2565
3299
  if (TEXT_EXTENSIONS.has(ext)) return true;
2566
3300
  if (TEXT_EXTENSIONS.has(basename)) return true;
2567
3301
  if (basename.startsWith(".") && !ext) return true;
@@ -2606,7 +3340,7 @@ async function searchFile(filepath, baseDir, pattern, contextLines, matches, max
2606
3340
  if (stats.size > MAX_FILE_SIZE2) return 0;
2607
3341
  const content = await fsPromises2.readFile(filepath, "utf-8");
2608
3342
  const lines = content.split("\n");
2609
- const relativePath = path7.relative(baseDir, filepath);
3343
+ const relativePath = path9.relative(baseDir, filepath);
2610
3344
  for (let i = 0; i < lines.length && matches.length < maxResults; i++) {
2611
3345
  const line = lines[i];
2612
3346
  pattern.lastIndex = 0;
@@ -2646,7 +3380,7 @@ async function searchDirectory2(dir, baseDir, pattern, includePatterns, contextL
2646
3380
  if (matches.length >= maxResults) break;
2647
3381
  const name = entry.name;
2648
3382
  if (shouldIgnore2(name)) continue;
2649
- const fullPath = path7.join(dir, name);
3383
+ const fullPath = path9.join(dir, name);
2650
3384
  if (entry.isDirectory()) {
2651
3385
  await searchDirectory2(fullPath, baseDir, pattern, includePatterns, contextLines, matches, maxResults, stats);
2652
3386
  } else if (entry.isFile()) {
@@ -2695,7 +3429,7 @@ async function grepSearch(args) {
2695
3429
  error: "Search path is required"
2696
3430
  };
2697
3431
  }
2698
- const resolvedPath = path7.resolve(searchPath);
3432
+ const resolvedPath = path9.resolve(searchPath);
2699
3433
  let stats;
2700
3434
  try {
2701
3435
  stats = await fsPromises2.stat(resolvedPath);
@@ -2743,7 +3477,7 @@ async function grepSearch(args) {
2743
3477
  );
2744
3478
  } else if (stats.isFile()) {
2745
3479
  searchStats.filesSearched = 1;
2746
- const found = await searchFile(resolvedPath, path7.dirname(resolvedPath), pattern, context_lines, matches, max_results);
3480
+ const found = await searchFile(resolvedPath, path9.dirname(resolvedPath), pattern, context_lines, matches, max_results);
2747
3481
  if (found > 0) searchStats.filesWithMatches = 1;
2748
3482
  }
2749
3483
  return {
@@ -2772,7 +3506,7 @@ async function grepSearch(args) {
2772
3506
  }
2773
3507
 
2774
3508
  // src/app/agent/tools/natives/view_file_outline.ts
2775
- import path8 from "path";
3509
+ import path10 from "path";
2776
3510
  import { promises as fsPromises3 } from "fs";
2777
3511
  var LANGUAGE_MAP = {
2778
3512
  ".ts": "typescript",
@@ -2886,7 +3620,7 @@ var PATTERNS = {
2886
3620
  ]
2887
3621
  };
2888
3622
  function detectLanguage(filepath) {
2889
- const ext = path8.extname(filepath).toLowerCase();
3623
+ const ext = path10.extname(filepath).toLowerCase();
2890
3624
  return LANGUAGE_MAP[ext] || "unknown";
2891
3625
  }
2892
3626
  function determineItemType(line, language) {
@@ -2982,7 +3716,7 @@ async function viewFileOutline(args) {
2982
3716
  error: "file_path is required and must be a string"
2983
3717
  };
2984
3718
  }
2985
- const resolvedPath = path8.resolve(file_path);
3719
+ const resolvedPath = path10.resolve(file_path);
2986
3720
  let content;
2987
3721
  try {
2988
3722
  content = await fsPromises3.readFile(resolvedPath, "utf-8");
@@ -3026,23 +3760,23 @@ async function viewFileOutline(args) {
3026
3760
  init_async_command();
3027
3761
 
3028
3762
  // src/app/agent/tools/natives/task_boundary.ts
3029
- import path9 from "path";
3030
- import { promises as fs7 } from "fs";
3031
- import os3 from "os";
3763
+ import path11 from "path";
3764
+ import { promises as fs9 } from "fs";
3765
+ import os6 from "os";
3032
3766
  var currentTask = null;
3033
3767
  var artifactsDir = null;
3034
3768
  async function getArtifactsDir() {
3035
3769
  if (artifactsDir) return artifactsDir;
3036
- const homeDir = os3.homedir();
3037
- const baseDir = path9.join(homeDir, ".bluma", "artifacts");
3770
+ const homeDir = os6.homedir();
3771
+ const baseDir = path11.join(homeDir, ".bluma", "artifacts");
3038
3772
  const sessionId = Date.now().toString(36) + Math.random().toString(36).substr(2, 5);
3039
- artifactsDir = path9.join(baseDir, sessionId);
3040
- await fs7.mkdir(artifactsDir, { recursive: true });
3773
+ artifactsDir = path11.join(baseDir, sessionId);
3774
+ await fs9.mkdir(artifactsDir, { recursive: true });
3041
3775
  return artifactsDir;
3042
3776
  }
3043
3777
  async function updateTaskFile(task) {
3044
3778
  const dir = await getArtifactsDir();
3045
- const taskFile = path9.join(dir, "task.md");
3779
+ const taskFile = path11.join(dir, "task.md");
3046
3780
  const content = `# ${task.taskName}
3047
3781
 
3048
3782
  **Mode:** ${task.mode}
@@ -3054,7 +3788,7 @@ async function updateTaskFile(task) {
3054
3788
  ## Summary
3055
3789
  ${task.summary}
3056
3790
  `;
3057
- await fs7.writeFile(taskFile, content, "utf-8");
3791
+ await fs9.writeFile(taskFile, content, "utf-8");
3058
3792
  }
3059
3793
  async function taskBoundary(args) {
3060
3794
  try {
@@ -3134,8 +3868,8 @@ async function createArtifact(args) {
3134
3868
  return { success: false, error: "content is required" };
3135
3869
  }
3136
3870
  const dir = await getArtifactsDir();
3137
- const filepath = path9.join(dir, filename);
3138
- await fs7.writeFile(filepath, content, "utf-8");
3871
+ const filepath = path11.join(dir, filename);
3872
+ await fs9.writeFile(filepath, content, "utf-8");
3139
3873
  return {
3140
3874
  success: true,
3141
3875
  path: filepath
@@ -3154,8 +3888,8 @@ async function readArtifact(args) {
3154
3888
  return { success: false, error: "filename is required" };
3155
3889
  }
3156
3890
  const dir = await getArtifactsDir();
3157
- const filepath = path9.join(dir, filename);
3158
- const content = await fs7.readFile(filepath, "utf-8");
3891
+ const filepath = path11.join(dir, filename);
3892
+ const content = await fs9.readFile(filepath, "utf-8");
3159
3893
  return {
3160
3894
  success: true,
3161
3895
  content
@@ -3524,9 +4258,9 @@ ${skill.content}`;
3524
4258
  }
3525
4259
 
3526
4260
  // src/app/agent/tools/natives/coding_memory.ts
3527
- import * as fs8 from "fs";
3528
- import * as path10 from "path";
3529
- import os4 from "os";
4261
+ import * as fs10 from "fs";
4262
+ import * as path12 from "path";
4263
+ import os7 from "os";
3530
4264
  var PROMPT_DEFAULT_MAX_TOTAL = 1e4;
3531
4265
  var PROMPT_DEFAULT_MAX_NOTES = 25;
3532
4266
  var PROMPT_DEFAULT_PREVIEW = 500;
@@ -3534,14 +4268,14 @@ function readCodingMemoryForPrompt(options) {
3534
4268
  const maxTotal = options?.maxTotalChars ?? PROMPT_DEFAULT_MAX_TOTAL;
3535
4269
  const maxNotes = options?.maxNotes ?? PROMPT_DEFAULT_MAX_NOTES;
3536
4270
  const preview = options?.previewCharsPerNote ?? PROMPT_DEFAULT_PREVIEW;
3537
- const globalPath = path10.join(os4.homedir(), ".bluma", "coding_memory.json");
3538
- const legacyPath = path10.join(process.cwd(), ".bluma", "coding_memory.json");
4271
+ const globalPath = path12.join(os7.homedir(), ".bluma", "coding_memory.json");
4272
+ const legacyPath = path12.join(process.cwd(), ".bluma", "coding_memory.json");
3539
4273
  let raw = null;
3540
4274
  try {
3541
- if (fs8.existsSync(globalPath)) {
3542
- raw = fs8.readFileSync(globalPath, "utf-8");
3543
- } else if (path10.resolve(globalPath) !== path10.resolve(legacyPath) && fs8.existsSync(legacyPath)) {
3544
- raw = fs8.readFileSync(legacyPath, "utf-8");
4275
+ if (fs10.existsSync(globalPath)) {
4276
+ raw = fs10.readFileSync(globalPath, "utf-8");
4277
+ } else if (path12.resolve(globalPath) !== path12.resolve(legacyPath) && fs10.existsSync(legacyPath)) {
4278
+ raw = fs10.readFileSync(legacyPath, "utf-8");
3545
4279
  }
3546
4280
  } catch {
3547
4281
  return "";
@@ -3579,10 +4313,10 @@ var memoryStore = [];
3579
4313
  var nextId2 = 1;
3580
4314
  var loaded = false;
3581
4315
  function getMemoryFilePath() {
3582
- return path10.join(os4.homedir(), ".bluma", "coding_memory.json");
4316
+ return path12.join(os7.homedir(), ".bluma", "coding_memory.json");
3583
4317
  }
3584
4318
  function getLegacyMemoryFilePath() {
3585
- return path10.join(process.cwd(), ".bluma", "coding_memory.json");
4319
+ return path12.join(process.cwd(), ".bluma", "coding_memory.json");
3586
4320
  }
3587
4321
  function loadMemoryFromFile() {
3588
4322
  if (loaded) return;
@@ -3592,19 +4326,19 @@ function loadMemoryFromFile() {
3592
4326
  try {
3593
4327
  const filePath = getMemoryFilePath();
3594
4328
  const legacy = getLegacyMemoryFilePath();
3595
- const legacyDistinct = path10.resolve(legacy) !== path10.resolve(filePath);
4329
+ const legacyDistinct = path12.resolve(legacy) !== path12.resolve(filePath);
3596
4330
  const readIntoStore = (p) => {
3597
- const raw = fs8.readFileSync(p, "utf-8");
4331
+ const raw = fs10.readFileSync(p, "utf-8");
3598
4332
  const parsed = JSON.parse(raw);
3599
4333
  if (Array.isArray(parsed.entries)) {
3600
4334
  memoryStore = parsed.entries;
3601
4335
  nextId2 = typeof parsed.nextId === "number" ? parsed.nextId : memoryStore.length + 1;
3602
4336
  }
3603
4337
  };
3604
- if (fs8.existsSync(filePath)) {
4338
+ if (fs10.existsSync(filePath)) {
3605
4339
  readIntoStore(filePath);
3606
4340
  }
3607
- if (memoryStore.length === 0 && legacyDistinct && fs8.existsSync(legacy)) {
4341
+ if (memoryStore.length === 0 && legacyDistinct && fs10.existsSync(legacy)) {
3608
4342
  readIntoStore(legacy);
3609
4343
  if (memoryStore.length > 0) {
3610
4344
  saveMemoryToFile();
@@ -3618,16 +4352,16 @@ function loadMemoryFromFile() {
3618
4352
  function saveMemoryToFile() {
3619
4353
  try {
3620
4354
  const filePath = getMemoryFilePath();
3621
- const dir = path10.dirname(filePath);
3622
- if (!fs8.existsSync(dir)) {
3623
- fs8.mkdirSync(dir, { recursive: true });
4355
+ const dir = path12.dirname(filePath);
4356
+ if (!fs10.existsSync(dir)) {
4357
+ fs10.mkdirSync(dir, { recursive: true });
3624
4358
  }
3625
4359
  const payload = {
3626
4360
  entries: memoryStore,
3627
4361
  nextId: nextId2,
3628
4362
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3629
4363
  };
3630
- fs8.writeFileSync(filePath, JSON.stringify(payload, null, 2));
4364
+ fs10.writeFileSync(filePath, JSON.stringify(payload, null, 2));
3631
4365
  } catch {
3632
4366
  }
3633
4367
  }
@@ -3824,9 +4558,9 @@ var ToolInvoker = class {
3824
4558
  async initialize() {
3825
4559
  try {
3826
4560
  const __filename = fileURLToPath(import.meta.url);
3827
- const __dirname2 = path11.dirname(__filename);
3828
- const configPath = path11.resolve(__dirname2, "config", "native_tools.json");
3829
- const fileContent = await fs9.readFile(configPath, "utf-8");
4561
+ const __dirname2 = path13.dirname(__filename);
4562
+ const configPath = path13.resolve(__dirname2, "config", "native_tools.json");
4563
+ const fileContent = await fs11.readFile(configPath, "utf-8");
3830
4564
  const config2 = JSON.parse(fileContent);
3831
4565
  this.toolDefinitions = config2.nativeTools;
3832
4566
  } catch (error) {
@@ -3887,9 +4621,9 @@ var ToolInvoker = class {
3887
4621
  };
3888
4622
 
3889
4623
  // src/app/agent/tools/mcp/mcp_client.ts
3890
- import { promises as fs10 } from "fs";
3891
- import path12 from "path";
3892
- import os5 from "os";
4624
+ import { promises as fs12 } from "fs";
4625
+ import path14 from "path";
4626
+ import os8 from "os";
3893
4627
  import { fileURLToPath as fileURLToPath2 } from "url";
3894
4628
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3895
4629
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
@@ -3916,9 +4650,9 @@ var MCPClient = class {
3916
4650
  });
3917
4651
  }
3918
4652
  const __filename = fileURLToPath2(import.meta.url);
3919
- const __dirname2 = path12.dirname(__filename);
3920
- const defaultConfigPath = path12.resolve(__dirname2, "config", "bluma-mcp.json");
3921
- const userConfigPath = path12.join(os5.homedir(), ".bluma", "bluma-mcp.json");
4653
+ const __dirname2 = path14.dirname(__filename);
4654
+ const defaultConfigPath = path14.resolve(__dirname2, "config", "bluma-mcp.json");
4655
+ const userConfigPath = path14.join(os8.homedir(), ".bluma", "bluma-mcp.json");
3922
4656
  const defaultConfig = await this.loadMcpConfig(defaultConfigPath, "Default");
3923
4657
  const userConfig = await this.loadMcpConfig(userConfigPath, "User");
3924
4658
  const mergedConfig = {
@@ -3952,7 +4686,7 @@ var MCPClient = class {
3952
4686
  }
3953
4687
  async loadMcpConfig(configPath, configType) {
3954
4688
  try {
3955
- const fileContent = await fs10.readFile(configPath, "utf-8");
4689
+ const fileContent = await fs12.readFile(configPath, "utf-8");
3956
4690
  const processedContent = this.replaceEnvPlaceholders(fileContent);
3957
4691
  return JSON.parse(processedContent);
3958
4692
  } catch (error) {
@@ -3971,7 +4705,7 @@ var MCPClient = class {
3971
4705
  async connectToStdioServer(serverName, config2) {
3972
4706
  let commandToExecute = config2.command;
3973
4707
  let argsToExecute = config2.args || [];
3974
- const isWindows = os5.platform() === "win32";
4708
+ const isWindows = os8.platform() === "win32";
3975
4709
  if (!isWindows && commandToExecute.toLowerCase() === "cmd") {
3976
4710
  if (argsToExecute.length >= 2 && argsToExecute[0].toLowerCase() === "/c") {
3977
4711
  commandToExecute = argsToExecute[1];
@@ -4087,13 +4821,13 @@ var AdvancedFeedbackSystem = class {
4087
4821
  };
4088
4822
 
4089
4823
  // src/app/agent/bluma/core/bluma.ts
4090
- import path16 from "path";
4824
+ import path18 from "path";
4091
4825
  import { v4 as uuidv43 } from "uuid";
4092
4826
 
4093
4827
  // src/app/agent/session_manager/session_manager.ts
4094
- import path13 from "path";
4095
- import os6 from "os";
4096
- import { promises as fs11 } from "fs";
4828
+ import path15 from "path";
4829
+ import os9 from "os";
4830
+ import { promises as fs13 } from "fs";
4097
4831
  var fileLocks = /* @__PURE__ */ new Map();
4098
4832
  async function withFileLock(file, fn) {
4099
4833
  const prev = fileLocks.get(file) || Promise.resolve();
@@ -4129,13 +4863,13 @@ function debouncedSave(sessionFile, history, memory) {
4129
4863
  function expandHome(p) {
4130
4864
  if (!p) return p;
4131
4865
  if (p.startsWith("~")) {
4132
- return path13.join(os6.homedir(), p.slice(1));
4866
+ return path15.join(os9.homedir(), p.slice(1));
4133
4867
  }
4134
4868
  return p;
4135
4869
  }
4136
4870
  function getPreferredAppDir() {
4137
- const fixed = path13.join(os6.homedir(), ".bluma");
4138
- return path13.resolve(expandHome(fixed));
4871
+ const fixed = path15.join(os9.homedir(), ".bluma");
4872
+ return path15.resolve(expandHome(fixed));
4139
4873
  }
4140
4874
  async function safeRenameWithRetry(src, dest, maxRetries = 6) {
4141
4875
  let attempt = 0;
@@ -4143,10 +4877,10 @@ async function safeRenameWithRetry(src, dest, maxRetries = 6) {
4143
4877
  const isWin = process.platform === "win32";
4144
4878
  while (attempt <= maxRetries) {
4145
4879
  try {
4146
- const dir = path13.dirname(dest);
4147
- await fs11.mkdir(dir, { recursive: true }).catch(() => {
4880
+ const dir = path15.dirname(dest);
4881
+ await fs13.mkdir(dir, { recursive: true }).catch(() => {
4148
4882
  });
4149
- await fs11.rename(src, dest);
4883
+ await fs13.rename(src, dest);
4150
4884
  return;
4151
4885
  } catch (e) {
4152
4886
  lastErr = e;
@@ -4159,13 +4893,13 @@ async function safeRenameWithRetry(src, dest, maxRetries = 6) {
4159
4893
  }
4160
4894
  }
4161
4895
  try {
4162
- await fs11.access(src);
4163
- const data = await fs11.readFile(src);
4164
- const dir = path13.dirname(dest);
4165
- await fs11.mkdir(dir, { recursive: true }).catch(() => {
4896
+ await fs13.access(src);
4897
+ const data = await fs13.readFile(src);
4898
+ const dir = path15.dirname(dest);
4899
+ await fs13.mkdir(dir, { recursive: true }).catch(() => {
4166
4900
  });
4167
- await fs11.writeFile(dest, data);
4168
- await fs11.unlink(src).catch(() => {
4901
+ await fs13.writeFile(dest, data);
4902
+ await fs13.unlink(src).catch(() => {
4169
4903
  });
4170
4904
  return;
4171
4905
  } catch (fallbackErr) {
@@ -4178,16 +4912,16 @@ async function safeRenameWithRetry(src, dest, maxRetries = 6) {
4178
4912
  }
4179
4913
  async function ensureSessionDir() {
4180
4914
  const appDir = getPreferredAppDir();
4181
- const sessionDir = path13.join(appDir, "sessions");
4182
- await fs11.mkdir(sessionDir, { recursive: true });
4915
+ const sessionDir = path15.join(appDir, "sessions");
4916
+ await fs13.mkdir(sessionDir, { recursive: true });
4183
4917
  return sessionDir;
4184
4918
  }
4185
4919
  async function loadOrcreateSession(sessionId) {
4186
4920
  const sessionDir = await ensureSessionDir();
4187
- const sessionFile = path13.join(sessionDir, `${sessionId}.json`);
4921
+ const sessionFile = path15.join(sessionDir, `${sessionId}.json`);
4188
4922
  try {
4189
- await fs11.access(sessionFile);
4190
- const fileContent = await fs11.readFile(sessionFile, "utf-8");
4923
+ await fs13.access(sessionFile);
4924
+ const fileContent = await fs13.readFile(sessionFile, "utf-8");
4191
4925
  const sessionData = JSON.parse(fileContent);
4192
4926
  const memory = {
4193
4927
  historyAnchor: sessionData.history_anchor ?? null,
@@ -4200,7 +4934,7 @@ async function loadOrcreateSession(sessionId) {
4200
4934
  created_at: (/* @__PURE__ */ new Date()).toISOString(),
4201
4935
  conversation_history: []
4202
4936
  };
4203
- await fs11.writeFile(sessionFile, JSON.stringify(newSessionData, null, 2), "utf-8");
4937
+ await fs13.writeFile(sessionFile, JSON.stringify(newSessionData, null, 2), "utf-8");
4204
4938
  const emptyMemory = {
4205
4939
  historyAnchor: null,
4206
4940
  compressedTurnSliceCount: 0
@@ -4212,12 +4946,12 @@ async function doSaveSessionHistory(sessionFile, history, memory) {
4212
4946
  await withFileLock(sessionFile, async () => {
4213
4947
  let sessionData;
4214
4948
  try {
4215
- const dir = path13.dirname(sessionFile);
4216
- await fs11.mkdir(dir, { recursive: true });
4949
+ const dir = path15.dirname(sessionFile);
4950
+ await fs13.mkdir(dir, { recursive: true });
4217
4951
  } catch {
4218
4952
  }
4219
4953
  try {
4220
- const fileContent = await fs11.readFile(sessionFile, "utf-8");
4954
+ const fileContent = await fs13.readFile(sessionFile, "utf-8");
4221
4955
  sessionData = JSON.parse(fileContent);
4222
4956
  } catch (error) {
4223
4957
  const code = error && error.code;
@@ -4228,14 +4962,14 @@ async function doSaveSessionHistory(sessionFile, history, memory) {
4228
4962
  console.warn(`An unknown error occurred while reading ${sessionFile}. Re-initializing.`, error);
4229
4963
  }
4230
4964
  }
4231
- const sessionId = path13.basename(sessionFile, ".json");
4965
+ const sessionId = path15.basename(sessionFile, ".json");
4232
4966
  sessionData = {
4233
4967
  session_id: sessionId,
4234
4968
  created_at: (/* @__PURE__ */ new Date()).toISOString(),
4235
4969
  conversation_history: []
4236
4970
  };
4237
4971
  try {
4238
- await fs11.writeFile(sessionFile, JSON.stringify(sessionData, null, 2), "utf-8");
4972
+ await fs13.writeFile(sessionFile, JSON.stringify(sessionData, null, 2), "utf-8");
4239
4973
  } catch {
4240
4974
  }
4241
4975
  }
@@ -4251,7 +4985,7 @@ async function doSaveSessionHistory(sessionFile, history, memory) {
4251
4985
  }
4252
4986
  const tempSessionFile = `${sessionFile}.${Date.now()}.tmp`;
4253
4987
  try {
4254
- await fs11.writeFile(tempSessionFile, JSON.stringify(sessionData, null, 2), "utf-8");
4988
+ await fs13.writeFile(tempSessionFile, JSON.stringify(sessionData, null, 2), "utf-8");
4255
4989
  await safeRenameWithRetry(tempSessionFile, sessionFile);
4256
4990
  } catch (writeError) {
4257
4991
  if (writeError instanceof Error) {
@@ -4260,7 +4994,7 @@ async function doSaveSessionHistory(sessionFile, history, memory) {
4260
4994
  console.error(`An unknown fatal error occurred while saving session to ${sessionFile}:`, writeError);
4261
4995
  }
4262
4996
  try {
4263
- await fs11.unlink(tempSessionFile);
4997
+ await fs13.unlink(tempSessionFile);
4264
4998
  } catch {
4265
4999
  }
4266
5000
  }
@@ -4277,15 +5011,15 @@ async function saveSessionHistory(sessionFile, history, memory) {
4277
5011
  }
4278
5012
 
4279
5013
  // src/app/agent/core/prompt/prompt_builder.ts
4280
- import os8 from "os";
4281
- import fs13 from "fs";
4282
- import path15 from "path";
5014
+ import os11 from "os";
5015
+ import fs15 from "fs";
5016
+ import path17 from "path";
4283
5017
  import { execSync } from "child_process";
4284
5018
 
4285
5019
  // src/app/agent/skills/skill_loader.ts
4286
- import fs12 from "fs";
4287
- import path14 from "path";
4288
- import os7 from "os";
5020
+ import fs14 from "fs";
5021
+ import path16 from "path";
5022
+ import os10 from "os";
4289
5023
  var SkillLoader = class _SkillLoader {
4290
5024
  bundledSkillsDir;
4291
5025
  projectSkillsDir;
@@ -4293,8 +5027,8 @@ var SkillLoader = class _SkillLoader {
4293
5027
  cache = /* @__PURE__ */ new Map();
4294
5028
  conflicts = [];
4295
5029
  constructor(projectRoot, bundledDir) {
4296
- this.projectSkillsDir = path14.join(projectRoot, ".bluma", "skills");
4297
- this.globalSkillsDir = path14.join(os7.homedir(), ".bluma", "skills");
5030
+ this.projectSkillsDir = path16.join(projectRoot, ".bluma", "skills");
5031
+ this.globalSkillsDir = path16.join(os10.homedir(), ".bluma", "skills");
4298
5032
  this.bundledSkillsDir = bundledDir || _SkillLoader.resolveBundledDir();
4299
5033
  }
4300
5034
  /**
@@ -4303,34 +5037,34 @@ var SkillLoader = class _SkillLoader {
4303
5037
  */
4304
5038
  static resolveBundledDir() {
4305
5039
  if (process.env.JEST_WORKER_ID !== void 0 || process.env.NODE_ENV === "test") {
4306
- return path14.join(process.cwd(), "dist", "config", "skills");
5040
+ return path16.join(process.cwd(), "dist", "config", "skills");
4307
5041
  }
4308
5042
  const candidates = [];
4309
5043
  const argv1 = process.argv[1];
4310
5044
  if (argv1 && !argv1.startsWith("-")) {
4311
5045
  try {
4312
- const scriptDir = path14.dirname(path14.resolve(argv1));
4313
- candidates.push(path14.join(scriptDir, "config", "skills"));
5046
+ const scriptDir = path16.dirname(path16.resolve(argv1));
5047
+ candidates.push(path16.join(scriptDir, "config", "skills"));
4314
5048
  } catch {
4315
5049
  }
4316
5050
  }
4317
5051
  candidates.push(
4318
- path14.join(process.cwd(), "dist", "config", "skills"),
4319
- path14.join(process.cwd(), "node_modules", "@nomad-e", "bluma-cli", "dist", "config", "skills")
5052
+ path16.join(process.cwd(), "dist", "config", "skills"),
5053
+ path16.join(process.cwd(), "node_modules", "@nomad-e", "bluma-cli", "dist", "config", "skills")
4320
5054
  );
4321
5055
  if (typeof __dirname !== "undefined") {
4322
5056
  candidates.push(
4323
- path14.join(__dirname, "config", "skills"),
4324
- path14.join(__dirname, "..", "..", "..", "config", "skills")
5057
+ path16.join(__dirname, "config", "skills"),
5058
+ path16.join(__dirname, "..", "..", "..", "config", "skills")
4325
5059
  );
4326
5060
  }
4327
5061
  for (const c of candidates) {
4328
- const abs = path14.resolve(c);
4329
- if (fs12.existsSync(abs)) {
5062
+ const abs = path16.resolve(c);
5063
+ if (fs14.existsSync(abs)) {
4330
5064
  return abs;
4331
5065
  }
4332
5066
  }
4333
- return path14.join(process.cwd(), "dist", "config", "skills");
5067
+ return path16.join(process.cwd(), "dist", "config", "skills");
4334
5068
  }
4335
5069
  /**
4336
5070
  * Lista skills disponíveis de todas as fontes.
@@ -4358,8 +5092,8 @@ var SkillLoader = class _SkillLoader {
4358
5092
  this.conflicts.push({
4359
5093
  name: skill.name,
4360
5094
  userSource: source,
4361
- userPath: path14.join(dir, skill.name, "SKILL.md"),
4362
- bundledPath: path14.join(this.bundledSkillsDir, skill.name, "SKILL.md")
5095
+ userPath: path16.join(dir, skill.name, "SKILL.md"),
5096
+ bundledPath: path16.join(this.bundledSkillsDir, skill.name, "SKILL.md")
4363
5097
  });
4364
5098
  continue;
4365
5099
  }
@@ -4367,20 +5101,20 @@ var SkillLoader = class _SkillLoader {
4367
5101
  }
4368
5102
  }
4369
5103
  listFromDir(dir, source) {
4370
- if (!fs12.existsSync(dir)) return [];
5104
+ if (!fs14.existsSync(dir)) return [];
4371
5105
  try {
4372
- return fs12.readdirSync(dir).filter((d) => {
4373
- const fullPath = path14.join(dir, d);
4374
- return fs12.statSync(fullPath).isDirectory() && fs12.existsSync(path14.join(fullPath, "SKILL.md"));
4375
- }).map((d) => this.loadMetadataFromPath(path14.join(dir, d, "SKILL.md"), d, source)).filter((m) => m !== null);
5106
+ return fs14.readdirSync(dir).filter((d) => {
5107
+ const fullPath = path16.join(dir, d);
5108
+ return fs14.statSync(fullPath).isDirectory() && fs14.existsSync(path16.join(fullPath, "SKILL.md"));
5109
+ }).map((d) => this.loadMetadataFromPath(path16.join(dir, d, "SKILL.md"), d, source)).filter((m) => m !== null);
4376
5110
  } catch {
4377
5111
  return [];
4378
5112
  }
4379
5113
  }
4380
5114
  loadMetadataFromPath(skillPath, skillName, source) {
4381
- if (!fs12.existsSync(skillPath)) return null;
5115
+ if (!fs14.existsSync(skillPath)) return null;
4382
5116
  try {
4383
- const raw = fs12.readFileSync(skillPath, "utf-8");
5117
+ const raw = fs14.readFileSync(skillPath, "utf-8");
4384
5118
  const parsed = this.parseFrontmatter(raw);
4385
5119
  return {
4386
5120
  name: parsed.name || skillName,
@@ -4402,12 +5136,12 @@ var SkillLoader = class _SkillLoader {
4402
5136
  */
4403
5137
  load(name) {
4404
5138
  if (this.cache.has(name)) return this.cache.get(name);
4405
- const bundledPath = path14.join(this.bundledSkillsDir, name, "SKILL.md");
4406
- const projectPath = path14.join(this.projectSkillsDir, name, "SKILL.md");
4407
- const globalPath = path14.join(this.globalSkillsDir, name, "SKILL.md");
4408
- const existsBundled = fs12.existsSync(bundledPath);
4409
- const existsProject = fs12.existsSync(projectPath);
4410
- const existsGlobal = fs12.existsSync(globalPath);
5139
+ const bundledPath = path16.join(this.bundledSkillsDir, name, "SKILL.md");
5140
+ const projectPath = path16.join(this.projectSkillsDir, name, "SKILL.md");
5141
+ const globalPath = path16.join(this.globalSkillsDir, name, "SKILL.md");
5142
+ const existsBundled = fs14.existsSync(bundledPath);
5143
+ const existsProject = fs14.existsSync(projectPath);
5144
+ const existsGlobal = fs14.existsSync(globalPath);
4411
5145
  if (existsBundled && (existsProject || existsGlobal)) {
4412
5146
  const conflictSource = existsProject ? "project" : "global";
4413
5147
  const conflictPath = existsProject ? projectPath : globalPath;
@@ -4446,9 +5180,9 @@ var SkillLoader = class _SkillLoader {
4446
5180
  }
4447
5181
  loadFromPath(skillPath, name, source) {
4448
5182
  try {
4449
- const raw = fs12.readFileSync(skillPath, "utf-8");
5183
+ const raw = fs14.readFileSync(skillPath, "utf-8");
4450
5184
  const parsed = this.parseFrontmatter(raw);
4451
- const skillDir = path14.dirname(skillPath);
5185
+ const skillDir = path16.dirname(skillPath);
4452
5186
  return {
4453
5187
  name: parsed.name || name,
4454
5188
  description: parsed.description || "",
@@ -4457,22 +5191,22 @@ var SkillLoader = class _SkillLoader {
4457
5191
  version: parsed.version,
4458
5192
  author: parsed.author,
4459
5193
  license: parsed.license,
4460
- references: this.scanAssets(path14.join(skillDir, "references")),
4461
- scripts: this.scanAssets(path14.join(skillDir, "scripts"))
5194
+ references: this.scanAssets(path16.join(skillDir, "references")),
5195
+ scripts: this.scanAssets(path16.join(skillDir, "scripts"))
4462
5196
  };
4463
5197
  } catch {
4464
5198
  return null;
4465
5199
  }
4466
5200
  }
4467
5201
  scanAssets(dir) {
4468
- if (!fs12.existsSync(dir)) return [];
5202
+ if (!fs14.existsSync(dir)) return [];
4469
5203
  try {
4470
- return fs12.readdirSync(dir).filter((f) => {
4471
- const fp = path14.join(dir, f);
4472
- return fs12.statSync(fp).isFile();
5204
+ return fs14.readdirSync(dir).filter((f) => {
5205
+ const fp = path16.join(dir, f);
5206
+ return fs14.statSync(fp).isFile();
4473
5207
  }).map((f) => ({
4474
5208
  name: f,
4475
- path: path14.resolve(dir, f)
5209
+ path: path16.resolve(dir, f)
4476
5210
  }));
4477
5211
  } catch {
4478
5212
  return [];
@@ -4529,10 +5263,10 @@ var SkillLoader = class _SkillLoader {
4529
5263
  this.cache.clear();
4530
5264
  }
4531
5265
  exists(name) {
4532
- const bundledPath = path14.join(this.bundledSkillsDir, name, "SKILL.md");
4533
- const projectPath = path14.join(this.projectSkillsDir, name, "SKILL.md");
4534
- const globalPath = path14.join(this.globalSkillsDir, name, "SKILL.md");
4535
- return fs12.existsSync(bundledPath) || fs12.existsSync(projectPath) || fs12.existsSync(globalPath);
5266
+ const bundledPath = path16.join(this.bundledSkillsDir, name, "SKILL.md");
5267
+ const projectPath = path16.join(this.projectSkillsDir, name, "SKILL.md");
5268
+ const globalPath = path16.join(this.globalSkillsDir, name, "SKILL.md");
5269
+ return fs14.existsSync(bundledPath) || fs14.existsSync(projectPath) || fs14.existsSync(globalPath);
4536
5270
  }
4537
5271
  /**
4538
5272
  * Retorna conflitos detetados (skills do utilizador com mesmo nome de nativas).
@@ -4591,10 +5325,10 @@ function getGitBranch(dir) {
4591
5325
  }
4592
5326
  function getPackageManager(dir) {
4593
5327
  try {
4594
- if (fs13.existsSync(path15.join(dir, "pnpm-lock.yaml"))) return "pnpm";
4595
- if (fs13.existsSync(path15.join(dir, "yarn.lock"))) return "yarn";
4596
- if (fs13.existsSync(path15.join(dir, "bun.lockb"))) return "bun";
4597
- if (fs13.existsSync(path15.join(dir, "package-lock.json"))) return "npm";
5328
+ if (fs15.existsSync(path17.join(dir, "pnpm-lock.yaml"))) return "pnpm";
5329
+ if (fs15.existsSync(path17.join(dir, "yarn.lock"))) return "yarn";
5330
+ if (fs15.existsSync(path17.join(dir, "bun.lockb"))) return "bun";
5331
+ if (fs15.existsSync(path17.join(dir, "package-lock.json"))) return "npm";
4598
5332
  return "unknown";
4599
5333
  } catch {
4600
5334
  return "unknown";
@@ -4602,9 +5336,9 @@ function getPackageManager(dir) {
4602
5336
  }
4603
5337
  function getProjectType(dir) {
4604
5338
  try {
4605
- const files = fs13.readdirSync(dir);
5339
+ const files = fs15.readdirSync(dir);
4606
5340
  if (files.includes("package.json")) {
4607
- const pkg = JSON.parse(fs13.readFileSync(path15.join(dir, "package.json"), "utf-8"));
5341
+ const pkg = JSON.parse(fs15.readFileSync(path17.join(dir, "package.json"), "utf-8"));
4608
5342
  if (pkg.dependencies?.next || pkg.devDependencies?.next) return "Next.js";
4609
5343
  if (pkg.dependencies?.react || pkg.devDependencies?.react) return "React";
4610
5344
  if (pkg.dependencies?.express || pkg.devDependencies?.express) return "Express";
@@ -4623,9 +5357,9 @@ function getProjectType(dir) {
4623
5357
  }
4624
5358
  function getTestFramework(dir) {
4625
5359
  try {
4626
- const pkgPath = path15.join(dir, "package.json");
4627
- if (fs13.existsSync(pkgPath)) {
4628
- const pkg = JSON.parse(fs13.readFileSync(pkgPath, "utf-8"));
5360
+ const pkgPath = path17.join(dir, "package.json");
5361
+ if (fs15.existsSync(pkgPath)) {
5362
+ const pkg = JSON.parse(fs15.readFileSync(pkgPath, "utf-8"));
4629
5363
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
4630
5364
  if (deps.jest) return "jest";
4631
5365
  if (deps.vitest) return "vitest";
@@ -4634,7 +5368,7 @@ function getTestFramework(dir) {
4634
5368
  if (deps["@playwright/test"]) return "playwright";
4635
5369
  if (deps.cypress) return "cypress";
4636
5370
  }
4637
- if (fs13.existsSync(path15.join(dir, "pytest.ini")) || fs13.existsSync(path15.join(dir, "conftest.py"))) return "pytest";
5371
+ if (fs15.existsSync(path17.join(dir, "pytest.ini")) || fs15.existsSync(path17.join(dir, "conftest.py"))) return "pytest";
4638
5372
  return "unknown";
4639
5373
  } catch {
4640
5374
  return "unknown";
@@ -4642,9 +5376,9 @@ function getTestFramework(dir) {
4642
5376
  }
4643
5377
  function getTestCommand(dir) {
4644
5378
  try {
4645
- const pkgPath = path15.join(dir, "package.json");
4646
- if (fs13.existsSync(pkgPath)) {
4647
- const pkg = JSON.parse(fs13.readFileSync(pkgPath, "utf-8"));
5379
+ const pkgPath = path17.join(dir, "package.json");
5380
+ if (fs15.existsSync(pkgPath)) {
5381
+ const pkg = JSON.parse(fs15.readFileSync(pkgPath, "utf-8"));
4648
5382
  if (pkg.scripts?.test) return `npm test`;
4649
5383
  if (pkg.scripts?.["test:unit"]) return `npm run test:unit`;
4650
5384
  }
@@ -5121,12 +5855,12 @@ In sandbox mode you are a Python-focused, non-interactive, deterministic agent t
5121
5855
  function getUnifiedSystemPrompt(availableSkills) {
5122
5856
  const cwd = process.cwd();
5123
5857
  const env = {
5124
- os_type: os8.type(),
5125
- os_version: os8.release(),
5126
- architecture: os8.arch(),
5858
+ os_type: os11.type(),
5859
+ os_version: os11.release(),
5860
+ architecture: os11.arch(),
5127
5861
  workdir: cwd,
5128
5862
  shell_type: process.env.SHELL || process.env.COMSPEC || "unknown",
5129
- username: os8.userInfo().username,
5863
+ username: os11.userInfo().username,
5130
5864
  current_date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
5131
5865
  timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
5132
5866
  is_git_repo: isGitRepo(cwd) ? "yes" : "no",
@@ -5311,8 +6045,8 @@ ${memorySnapshot.trim().length > 0 ? memorySnapshot : "(No entries yet. Use codi
5311
6045
  }
5312
6046
  function isGitRepo(dir) {
5313
6047
  try {
5314
- const gitPath = path15.join(dir, ".git");
5315
- return fs13.existsSync(gitPath) && fs13.lstatSync(gitPath).isDirectory();
6048
+ const gitPath = path17.join(dir, ".git");
6049
+ return fs15.existsSync(gitPath) && fs15.lstatSync(gitPath).isDirectory();
5316
6050
  } catch {
5317
6051
  return false;
5318
6052
  }
@@ -5521,21 +6255,9 @@ function partitionConversationIntoTurnSlices(conversationHistory) {
5521
6255
  }
5522
6256
  return turns;
5523
6257
  }
5524
- var DEFAULT_TOKEN_BUDGET = 6e4;
5525
- var DEFAULT_COMPRESS_THRESHOLD = 0.7;
5526
- var DEFAULT_KEEP_RECENT_TURNS = 8;
5527
- function readContextOptionInt(envKey, fallback) {
5528
- const raw = (process.env[envKey] ?? "").trim();
5529
- if (!raw) return fallback;
5530
- const n = Number(raw);
5531
- return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
5532
- }
5533
- function readContextOptionFloat(envKey, fallback) {
5534
- const raw = (process.env[envKey] ?? "").trim();
5535
- if (!raw) return fallback;
5536
- const n = Number(raw);
5537
- return Number.isFinite(n) && n > 0 && n <= 1 ? n : fallback;
5538
- }
6258
+ var CONTEXT_TOKEN_BUDGET = 24e4;
6259
+ var COMPRESS_THRESHOLD = 0.7;
6260
+ var KEEP_RECENT_TURNS = 10;
5539
6261
  function buildContextMessages(systemMessages, anchor, pendingRaw, recentFlat) {
5540
6262
  const anchorMsg = anchor ? [anchorToSystemMessage(anchor)] : [];
5541
6263
  return [...systemMessages, ...anchorMsg, ...pendingRaw, ...recentFlat];
@@ -5548,9 +6270,9 @@ async function createApiContextWindow(fullHistory, currentAnchor, compressedTurn
5548
6270
  newCompressedTurnSliceCount: compressedTurnSliceCount
5549
6271
  };
5550
6272
  }
5551
- const tokenBudget = options?.tokenBudget ?? readContextOptionInt("BLUMA_CONTEXT_TOKEN_BUDGET", DEFAULT_TOKEN_BUDGET);
5552
- const compressThreshold = options?.compressThreshold ?? readContextOptionFloat("BLUMA_COMPRESS_THRESHOLD", DEFAULT_COMPRESS_THRESHOLD);
5553
- const keepRecentTurns = options?.keepRecentTurns ?? readContextOptionInt("BLUMA_KEEP_RECENT_TURNS", DEFAULT_KEEP_RECENT_TURNS);
6273
+ const tokenBudget = options?.tokenBudget ?? CONTEXT_TOKEN_BUDGET;
6274
+ const compressThreshold = options?.compressThreshold ?? COMPRESS_THRESHOLD;
6275
+ const keepRecentTurns = options?.keepRecentTurns ?? KEEP_RECENT_TURNS;
5554
6276
  const systemMessages = [];
5555
6277
  let historyStartIndex = 0;
5556
6278
  while (historyStartIndex < fullHistory.length && fullHistory[historyStartIndex].role === "system") {
@@ -5592,7 +6314,7 @@ async function createApiContextWindow(fullHistory, currentAnchor, compressedTurn
5592
6314
  }
5593
6315
 
5594
6316
  // src/app/agent/core/llm/llm.ts
5595
- import os9 from "os";
6317
+ import os12 from "os";
5596
6318
  import OpenAI from "openai";
5597
6319
  function defaultBlumaUserContextInput(sessionId, userMessage) {
5598
6320
  const msg = String(userMessage || "").slice(0, 300);
@@ -5609,7 +6331,7 @@ function defaultBlumaUserContextInput(sessionId, userMessage) {
5609
6331
  }
5610
6332
  function getPreferredMacAddress() {
5611
6333
  try {
5612
- const ifaces = os9.networkInterfaces();
6334
+ const ifaces = os12.networkInterfaces();
5613
6335
  for (const name of Object.keys(ifaces)) {
5614
6336
  const addrs = ifaces[name];
5615
6337
  if (!addrs) continue;
@@ -5624,7 +6346,7 @@ function getPreferredMacAddress() {
5624
6346
  } catch {
5625
6347
  }
5626
6348
  try {
5627
- return `host:${os9.hostname()}`;
6349
+ return `host:${os12.hostname()}`;
5628
6350
  } catch {
5629
6351
  return "unknown";
5630
6352
  }
@@ -5634,7 +6356,7 @@ function defaultInteractiveCliUserContextInput(sessionId, userMessage) {
5634
6356
  const machineId = getPreferredMacAddress();
5635
6357
  let userName = null;
5636
6358
  try {
5637
- userName = os9.userInfo().username || null;
6359
+ userName = os12.userInfo().username || null;
5638
6360
  } catch {
5639
6361
  userName = null;
5640
6362
  }
@@ -6072,6 +6794,12 @@ var BluMaAgent = class {
6072
6794
  getUiToolsDetailed() {
6073
6795
  return this.mcpClient.getAvailableToolsDetailed();
6074
6796
  }
6797
+ listAvailableSkills() {
6798
+ return this.skillLoader.listAvailable();
6799
+ }
6800
+ getSkillsDirs() {
6801
+ return this.skillLoader.getSkillsDirs();
6802
+ }
6075
6803
  async processTurn(userInput, userContextInput) {
6076
6804
  this.isInterrupted = false;
6077
6805
  this.factorRouterTurnClosed = false;
@@ -6082,7 +6810,8 @@ var BluMaAgent = class {
6082
6810
  turnId,
6083
6811
  sessionId: userContextInput.sessionId || this.sessionId
6084
6812
  };
6085
- this.history.push({ role: "user", content: inputText });
6813
+ const userContent = buildUserMessageContent(inputText, process.cwd());
6814
+ this.history.push({ role: "user", content: userContent });
6086
6815
  if (inputText === "/init") {
6087
6816
  this.eventBus.emit("dispatch", inputText);
6088
6817
  }
@@ -6235,7 +6964,7 @@ var BluMaAgent = class {
6235
6964
 
6236
6965
  ${editData.error.display}`;
6237
6966
  }
6238
- const filename = path16.basename(toolArgs.file_path);
6967
+ const filename = path18.basename(toolArgs.file_path);
6239
6968
  return createDiff(filename, editData.currentContent || "", editData.newContent);
6240
6969
  } catch (e) {
6241
6970
  return `An unexpected error occurred while generating the edit preview: ${e.message}`;
@@ -6493,7 +7222,7 @@ import { v4 as uuidv45 } from "uuid";
6493
7222
  import { v4 as uuidv44 } from "uuid";
6494
7223
 
6495
7224
  // src/app/agent/subagents/init/init_system_prompt.ts
6496
- import os10 from "os";
7225
+ import os13 from "os";
6497
7226
  var SYSTEM_PROMPT2 = `
6498
7227
 
6499
7228
  ### YOU ARE BluMa CLI \u2014 INIT SUBAGENT \u2014 AUTONOMOUS SENIOR SOFTWARE ENGINEER @ NOMADENGENUITY
@@ -6656,12 +7385,12 @@ Rule Summary:
6656
7385
  function getInitPrompt() {
6657
7386
  const now = /* @__PURE__ */ new Date();
6658
7387
  const collectedData = {
6659
- os_type: os10.type(),
6660
- os_version: os10.release(),
6661
- architecture: os10.arch(),
7388
+ os_type: os13.type(),
7389
+ os_version: os13.release(),
7390
+ architecture: os13.arch(),
6662
7391
  workdir: process.cwd(),
6663
7392
  shell_type: process.env.SHELL || process.env.COMSPEC || "Unknown",
6664
- username: os10.userInfo().username || "Unknown",
7393
+ username: os13.userInfo().username || "Unknown",
6665
7394
  current_date: now.toISOString().split("T")[0],
6666
7395
  // Formato YYYY-MM-DD
6667
7396
  timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "Unknown",
@@ -6942,14 +7671,14 @@ var RouteManager = class {
6942
7671
  this.subAgents = subAgents;
6943
7672
  this.core = core;
6944
7673
  }
6945
- registerRoute(path19, handler) {
6946
- this.routeHandlers.set(path19, handler);
7674
+ registerRoute(path22, handler) {
7675
+ this.routeHandlers.set(path22, handler);
6947
7676
  }
6948
7677
  async handleRoute(payload) {
6949
7678
  const inputText = String(payload.content || "").trim();
6950
7679
  const { userContext } = payload;
6951
- for (const [path19, handler] of this.routeHandlers) {
6952
- if (inputText === path19 || inputText.startsWith(`${path19} `)) {
7680
+ for (const [path22, handler] of this.routeHandlers) {
7681
+ if (inputText === path22 || inputText.startsWith(`${path22} `)) {
6953
7682
  return handler({ content: inputText, userContext });
6954
7683
  }
6955
7684
  }
@@ -6958,7 +7687,7 @@ var RouteManager = class {
6958
7687
  };
6959
7688
 
6960
7689
  // src/app/agent/agent.ts
6961
- var globalEnvPath = path17.join(os11.homedir(), ".bluma", ".env");
7690
+ var globalEnvPath = path19.join(os14.homedir(), ".bluma", ".env");
6962
7691
  dotenv.config({ path: globalEnvPath });
6963
7692
  var Agent = class {
6964
7693
  sessionId;
@@ -7069,6 +7798,13 @@ var Agent = class {
7069
7798
  getUiToolsDetailed() {
7070
7799
  return this.core.getUiToolsDetailed();
7071
7800
  }
7801
+ /** Skills list for UI (/skills) — same source as system prompt & load_skill. */
7802
+ listAvailableSkills() {
7803
+ return this.core.listAvailableSkills();
7804
+ }
7805
+ getSkillsDirs() {
7806
+ return this.core.getSkillsDirs();
7807
+ }
7072
7808
  async processTurn(userInput, userContextInput) {
7073
7809
  const inputText = String(userInput.content || "").trim();
7074
7810
  const resolvedUserContext = userContextInput ?? defaultInteractiveCliUserContextInput(this.sessionId, inputText.slice(0, 300));
@@ -7175,12 +7911,12 @@ var renderShellCommand2 = ({ args }) => {
7175
7911
  };
7176
7912
  var renderLsTool2 = ({ args }) => {
7177
7913
  const parsed = parseArgs(args);
7178
- const path19 = parsed.directory_path || ".";
7914
+ const path22 = parsed.directory_path || ".";
7179
7915
  return /* @__PURE__ */ jsxs9(Box9, { children: [
7180
7916
  /* @__PURE__ */ jsx9(Text9, { color: BLUMA_TERMINAL.brandBlue, bold: true, children: "ls" }),
7181
7917
  /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
7182
7918
  " ",
7183
- path19
7919
+ path22
7184
7920
  ] })
7185
7921
  ] });
7186
7922
  };
@@ -7316,7 +8052,7 @@ var renderFindByName = ({ args }) => {
7316
8052
  var renderGrepSearch = ({ args }) => {
7317
8053
  const parsed = parseArgs(args);
7318
8054
  const query = parsed.query || "";
7319
- const path19 = parsed.path || ".";
8055
+ const path22 = parsed.path || ".";
7320
8056
  return /* @__PURE__ */ jsxs9(Box9, { children: [
7321
8057
  /* @__PURE__ */ jsx9(Text9, { color: BLUMA_TERMINAL.brandBlue, bold: true, children: "grep" }),
7322
8058
  /* @__PURE__ */ jsxs9(Text9, { color: BLUMA_TERMINAL.brandMagenta, children: [
@@ -7326,7 +8062,7 @@ var renderGrepSearch = ({ args }) => {
7326
8062
  ] }),
7327
8063
  /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
7328
8064
  " ",
7329
- path19
8065
+ path22
7330
8066
  ] })
7331
8067
  ] });
7332
8068
  };
@@ -7895,10 +8631,10 @@ var ToolResultDisplayComponent = ({ toolName, result }) => {
7895
8631
  ] }),
7896
8632
  matches.slice(0, 5).map((m, i) => {
7897
8633
  const row = m;
7898
- const path19 = row.file || row.path || row.name || m;
8634
+ const path22 = row.file || row.path || row.name || m;
7899
8635
  const line = row.line;
7900
8636
  return /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
7901
- String(path19),
8637
+ String(path22),
7902
8638
  line != null ? `:${line}` : ""
7903
8639
  ] }, i);
7904
8640
  }),
@@ -8030,8 +8766,18 @@ var SessionInfoConnectingMCP_default = SessionInfoConnectingMCP;
8030
8766
 
8031
8767
  // src/app/ui/components/SlashCommands.tsx
8032
8768
  import { Box as Box14, Text as Text13 } from "ink";
8769
+
8770
+ // src/app/ui/constants/historyLayout.ts
8771
+ var HEADER_PANEL_HISTORY_ID = 0;
8772
+
8773
+ // src/app/ui/components/SlashCommands.tsx
8033
8774
  import { Fragment as Fragment2, jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
8034
- var SlashCommands = ({ input, setHistory, agentRef }) => {
8775
+ var SlashCommands = ({
8776
+ input,
8777
+ setHistory,
8778
+ agentRef,
8779
+ onClearRecent
8780
+ }) => {
8035
8781
  const [cmd, ...args] = input.slice(1).trim().split(/\s+/);
8036
8782
  const outBox = (children) => /* @__PURE__ */ jsx14(ChatBlock, { marginBottom: 1, children: /* @__PURE__ */ jsx14(Box14, { paddingLeft: 1, flexDirection: "column", children }) });
8037
8783
  const render2 = () => {
@@ -8039,19 +8785,59 @@ var SlashCommands = ({ input, setHistory, agentRef }) => {
8039
8785
  return null;
8040
8786
  }
8041
8787
  if (cmd === "help") {
8042
- const cmds = getSlashCommands();
8788
+ const lines = formatSlashHelpLines();
8789
+ return outBox(
8790
+ /* @__PURE__ */ jsxs13(Fragment2, { children: [
8791
+ /* @__PURE__ */ jsxs13(Box14, { marginBottom: 1, children: [
8792
+ /* @__PURE__ */ jsx14(Text13, { bold: true, color: BLUMA_TERMINAL.brandMagenta, children: "Slash commands" }),
8793
+ /* @__PURE__ */ jsx14(Text13, { dimColor: true, children: " \xB7 " }),
8794
+ /* @__PURE__ */ jsx14(Text13, { dimColor: true, children: "put .png/.jpg/.webp paths in a normal message to attach images (project dir or ~)" })
8795
+ ] }),
8796
+ /* @__PURE__ */ jsx14(Box14, { flexDirection: "column", children: lines.map((line, i) => /* @__PURE__ */ jsx14(Text13, { dimColor: line.trim().length > 0, children: line || " " }, i)) })
8797
+ ] })
8798
+ );
8799
+ }
8800
+ if (cmd === "skills") {
8801
+ const list = agentRef.current?.listAvailableSkills?.() || [];
8802
+ const dirs = agentRef.current?.getSkillsDirs?.();
8043
8803
  return outBox(
8044
8804
  /* @__PURE__ */ jsxs13(Fragment2, { children: [
8045
- /* @__PURE__ */ jsx14(Box14, { marginBottom: 1, children: /* @__PURE__ */ jsx14(Text13, { bold: true, color: BLUMA_TERMINAL.brandMagenta, children: "Available Commands" }) }),
8046
- /* @__PURE__ */ jsx14(Box14, { flexDirection: "column", children: cmds.map((c, i) => /* @__PURE__ */ jsxs13(Box14, { children: [
8047
- /* @__PURE__ */ jsx14(Text13, { color: BLUMA_TERMINAL.brandBlue, children: c.name.padEnd(12) }),
8048
- /* @__PURE__ */ jsx14(Text13, { dimColor: true, children: c.description })
8805
+ /* @__PURE__ */ jsxs13(Box14, { marginBottom: 1, children: [
8806
+ /* @__PURE__ */ jsx14(Text13, { bold: true, color: BLUMA_TERMINAL.brandMagenta, children: "Skills (load_skill)" }),
8807
+ /* @__PURE__ */ jsxs13(Text13, { dimColor: true, children: [
8808
+ " \xB7 ",
8809
+ list.length,
8810
+ " available"
8811
+ ] })
8812
+ ] }),
8813
+ dirs ? /* @__PURE__ */ jsxs13(Box14, { marginBottom: 1, flexDirection: "column", children: [
8814
+ /* @__PURE__ */ jsxs13(Text13, { dimColor: true, children: [
8815
+ "bundled: ",
8816
+ String(dirs.bundled || "")
8817
+ ] }),
8818
+ /* @__PURE__ */ jsxs13(Text13, { dimColor: true, children: [
8819
+ "project: ",
8820
+ String(dirs.project || "")
8821
+ ] }),
8822
+ /* @__PURE__ */ jsxs13(Text13, { dimColor: true, children: [
8823
+ "global: ",
8824
+ String(dirs.global || "")
8825
+ ] })
8826
+ ] }) : null,
8827
+ list.length === 0 ? /* @__PURE__ */ jsx14(Text13, { color: "yellow", children: "No skills found (check bundled dist/config/skills)." }) : /* @__PURE__ */ jsx14(Box14, { flexDirection: "column", children: list.map((s, i) => /* @__PURE__ */ jsxs13(Box14, { flexDirection: "column", marginBottom: 1, children: [
8828
+ /* @__PURE__ */ jsx14(Text13, { color: BLUMA_TERMINAL.brandBlue, bold: true, children: s.name }),
8829
+ /* @__PURE__ */ jsxs13(Text13, { dimColor: true, children: [
8830
+ s.source,
8831
+ " \u2014 ",
8832
+ s.description || "\u2014"
8833
+ ] })
8049
8834
  ] }, i)) })
8050
8835
  ] })
8051
8836
  );
8052
8837
  }
8053
8838
  if (cmd === "clear") {
8054
- setHistory((prev) => prev.filter((item) => item.id === 0 || item.id === 1));
8839
+ onClearRecent?.();
8840
+ setHistory((prev) => prev.filter((item) => item.id === HEADER_PANEL_HISTORY_ID));
8055
8841
  return outBox(
8056
8842
  /* @__PURE__ */ jsxs13(Box14, { children: [
8057
8843
  /* @__PURE__ */ jsx14(Text13, { color: "green", children: "[ok]" }),
@@ -8195,16 +8981,16 @@ var SlashCommands_default = SlashCommands;
8195
8981
  // src/app/agent/utils/update_check.ts
8196
8982
  import updateNotifier from "update-notifier";
8197
8983
  import { fileURLToPath as fileURLToPath3 } from "url";
8198
- import path18 from "path";
8199
- import fs14 from "fs";
8984
+ import path20 from "path";
8985
+ import fs16 from "fs";
8200
8986
  var BLUMA_PACKAGE_NAME = "@nomad-e/bluma-cli";
8201
8987
  function findBlumaPackageJson(startDir) {
8202
8988
  let dir = startDir;
8203
8989
  for (let i = 0; i < 10; i++) {
8204
- const candidate = path18.join(dir, "package.json");
8205
- if (fs14.existsSync(candidate)) {
8990
+ const candidate = path20.join(dir, "package.json");
8991
+ if (fs16.existsSync(candidate)) {
8206
8992
  try {
8207
- const raw = fs14.readFileSync(candidate, "utf8");
8993
+ const raw = fs16.readFileSync(candidate, "utf8");
8208
8994
  const parsed = JSON.parse(raw);
8209
8995
  if (parsed?.name === BLUMA_PACKAGE_NAME && parsed?.version) {
8210
8996
  return { name: parsed.name, version: parsed.version };
@@ -8212,7 +8998,7 @@ function findBlumaPackageJson(startDir) {
8212
8998
  } catch {
8213
8999
  }
8214
9000
  }
8215
- const parent = path18.dirname(dir);
9001
+ const parent = path20.dirname(dir);
8216
9002
  if (parent === dir) break;
8217
9003
  dir = parent;
8218
9004
  }
@@ -8225,12 +9011,12 @@ async function checkForUpdates() {
8225
9011
  }
8226
9012
  const binPath = process.argv?.[1];
8227
9013
  let pkg = null;
8228
- if (binPath && fs14.existsSync(binPath)) {
8229
- pkg = findBlumaPackageJson(path18.dirname(binPath));
9014
+ if (binPath && fs16.existsSync(binPath)) {
9015
+ pkg = findBlumaPackageJson(path20.dirname(binPath));
8230
9016
  }
8231
9017
  if (!pkg) {
8232
9018
  const __filename = fileURLToPath3(import.meta.url);
8233
- const __dirname2 = path18.dirname(__filename);
9019
+ const __dirname2 = path20.dirname(__filename);
8234
9020
  pkg = findBlumaPackageJson(__dirname2);
8235
9021
  }
8236
9022
  if (!pkg) {
@@ -8460,7 +9246,42 @@ var SAFE_AUTO_APPROVE_TOOLS = [
8460
9246
  // Status de comandos (read-only)
8461
9247
  "command_status"
8462
9248
  ];
8463
- var AppComponent = ({ eventBus, sessionId }) => {
9249
+ function trimRecentActivity(s, max = 72) {
9250
+ const t = String(s ?? "").replace(/\s+/g, " ").trim();
9251
+ if (!t) return "";
9252
+ return t.length <= max ? t : `${t.slice(0, max - 1)}\u2026`;
9253
+ }
9254
+ function UserMessageWithOptionalImages({
9255
+ raw,
9256
+ variant
9257
+ }) {
9258
+ if (variant === "slash-img") {
9259
+ const pathStrs = collectImagePathStrings(raw);
9260
+ const stripped2 = pathStrs.length > 0 ? stripImagePathStrings(raw, pathStrs) : raw;
9261
+ const cap = stripped2.trim();
9262
+ const capDisp = cap.length > 800 ? `${cap.slice(0, 800)}\u2026` : cap;
9263
+ const fallbackDisp = raw.length > 800 ? `${raw.slice(0, 800)}\u2026` : raw;
9264
+ return /* @__PURE__ */ jsx20(ChatUserMessage, { children: pathStrs.length > 0 ? /* @__PURE__ */ jsx20(
9265
+ ChatUserImageBlock,
9266
+ {
9267
+ imageCount: pathStrs.length,
9268
+ caption: cap.length > 0 ? capDisp : null,
9269
+ captionDim: true
9270
+ }
9271
+ ) : /* @__PURE__ */ jsx20(Text19, { dimColor: true, wrap: "wrap", children: fallbackDisp }) });
9272
+ }
9273
+ const displayRaw = raw.length > 1e4 ? `${raw.substring(0, 1e4)}...` : raw;
9274
+ const paths = collectImagePathStrings(displayRaw);
9275
+ const stripped = paths.length > 0 ? stripImagePathStrings(displayRaw, paths) : displayRaw;
9276
+ return /* @__PURE__ */ jsx20(ChatUserMessage, { children: paths.length > 0 ? /* @__PURE__ */ jsx20(
9277
+ ChatUserImageBlock,
9278
+ {
9279
+ imageCount: paths.length,
9280
+ caption: stripped.trim().length > 0 ? stripped : null
9281
+ }
9282
+ ) : /* @__PURE__ */ jsx20(Text19, { wrap: "wrap", children: displayRaw }) });
9283
+ }
9284
+ var AppComponent = ({ eventBus, sessionId, cliVersion }) => {
8464
9285
  const agentInstance = useRef5(null);
8465
9286
  const [history, setHistory] = useState6([]);
8466
9287
  const [statusMessage, setStatusMessage] = useState6(
@@ -8478,11 +9299,12 @@ var AppComponent = ({ eventBus, sessionId }) => {
8478
9299
  null
8479
9300
  );
8480
9301
  const [isInitAgentActive, setIsInitAgentActive] = useState6(false);
9302
+ const [recentActivityLine, setRecentActivityLine] = useState6(null);
8481
9303
  const alwaysAcceptList = useRef5([]);
8482
9304
  const workdir = process.cwd();
8483
9305
  const updateCheckRan = useRef5(false);
8484
9306
  const turnStartedAtRef = useRef5(null);
8485
- const appendExpandPreviewToHistory = useCallback2(() => {
9307
+ const appendExpandPreviewToHistory = useCallback3(() => {
8486
9308
  const p = peekLatestExpandable();
8487
9309
  setHistory((prev) => {
8488
9310
  const id = prev.length;
@@ -8510,7 +9332,27 @@ var AppComponent = ({ eventBus, sessionId }) => {
8510
9332
  expandPreviewHotkeyBus.off("expand", appendExpandPreviewToHistory);
8511
9333
  };
8512
9334
  }, [appendExpandPreviewToHistory]);
8513
- const handleInterrupt = useCallback2(() => {
9335
+ useEffect7(() => {
9336
+ setHistory((prev) => {
9337
+ const tail = prev.filter((h) => h.id !== HEADER_PANEL_HISTORY_ID);
9338
+ return [
9339
+ {
9340
+ id: HEADER_PANEL_HISTORY_ID,
9341
+ component: /* @__PURE__ */ jsx20(
9342
+ Header,
9343
+ {
9344
+ sessionId,
9345
+ workdir,
9346
+ cliVersion,
9347
+ recentActivitySummary: recentActivityLine
9348
+ }
9349
+ )
9350
+ },
9351
+ ...tail
9352
+ ];
9353
+ });
9354
+ }, [sessionId, workdir, cliVersion, recentActivityLine]);
9355
+ const handleInterrupt = useCallback3(() => {
8514
9356
  if (!isProcessing) return;
8515
9357
  eventBus.emit("user_interrupt");
8516
9358
  turnStartedAtRef.current = null;
@@ -8523,15 +9365,41 @@ var AppComponent = ({ eventBus, sessionId }) => {
8523
9365
  }
8524
9366
  ]);
8525
9367
  }, [isProcessing, eventBus]);
8526
- const handleSubmit = useCallback2(
9368
+ const handleSubmit = useCallback3(
8527
9369
  (text) => {
8528
9370
  if (!text || isProcessing || !agentInstance.current) return;
8529
- if (text.startsWith("/")) {
9371
+ if (/^\/img\s+/i.test(text) || /^\/image\s+/i.test(text)) {
9372
+ const payload = text.replace(/^\/img\s+/i, "").replace(/^\/image\s+/i, "").trim();
9373
+ if (!payload) {
9374
+ setHistory((prev) => [
9375
+ ...prev,
9376
+ {
9377
+ id: prev.length,
9378
+ component: /* @__PURE__ */ jsx20(ChatMeta, { children: "Usage: /img ./screenshot.png \u2014 optional text after the path is sent too" })
9379
+ }
9380
+ ]);
9381
+ return;
9382
+ }
9383
+ setIsProcessing(true);
9384
+ turnStartedAtRef.current = Date.now();
9385
+ setHistory((prev) => [
9386
+ ...prev,
9387
+ {
9388
+ id: prev.length,
9389
+ component: /* @__PURE__ */ jsx20(UserMessageWithOptionalImages, { raw: payload, variant: "slash-img" })
9390
+ }
9391
+ ]);
9392
+ setRecentActivityLine(`Prompt: ${trimRecentActivity(payload)}`);
9393
+ agentInstance.current.processTurn({ content: payload });
9394
+ return;
9395
+ }
9396
+ if (isSlashRoutingLine(text)) {
8530
9397
  const [cmd] = text.slice(1).trim().split(/\s+/);
8531
9398
  if (!cmd) {
8532
9399
  setIsProcessing(false);
8533
9400
  return;
8534
9401
  }
9402
+ setRecentActivityLine(`You: ${trimRecentActivity(text)}`);
8535
9403
  if (cmd === "init") {
8536
9404
  setIsInitAgentActive(true);
8537
9405
  setIsProcessing(true);
@@ -8553,7 +9421,8 @@ var AppComponent = ({ eventBus, sessionId }) => {
8553
9421
  {
8554
9422
  input: text,
8555
9423
  setHistory,
8556
- agentRef: agentInstance
9424
+ agentRef: agentInstance,
9425
+ onClearRecent: () => setRecentActivityLine(null)
8557
9426
  }
8558
9427
  )
8559
9428
  }
@@ -8578,6 +9447,7 @@ var AppComponent = ({ eventBus, sessionId }) => {
8578
9447
  ] }) })
8579
9448
  }
8580
9449
  ]);
9450
+ setRecentActivityLine(`Shell: ${trimRecentActivity(command)}`);
8581
9451
  Promise.resolve().then(() => (init_async_command(), async_command_exports)).then(async ({ runCommandAsync: runCommandAsync2 }) => {
8582
9452
  try {
8583
9453
  const result = await runCommandAsync2({ command, cwd: workdir });
@@ -8621,19 +9491,19 @@ Please use command_status to check the result and report back to the user.`;
8621
9491
  }
8622
9492
  setIsProcessing(true);
8623
9493
  turnStartedAtRef.current = Date.now();
8624
- const displayText = text.length > 1e4 ? text.substring(0, 1e4) + "..." : text;
8625
9494
  setHistory((prev) => [
8626
9495
  ...prev,
8627
9496
  {
8628
9497
  id: prev.length,
8629
- component: /* @__PURE__ */ jsx20(ChatUserMessage, { children: /* @__PURE__ */ jsx20(Text19, { children: displayText }) })
9498
+ component: /* @__PURE__ */ jsx20(UserMessageWithOptionalImages, { raw: text, variant: "plain" })
8630
9499
  }
8631
9500
  ]);
9501
+ setRecentActivityLine(`You: ${trimRecentActivity(text)}`);
8632
9502
  agentInstance.current.processTurn({ content: text });
8633
9503
  },
8634
9504
  [isProcessing]
8635
9505
  );
8636
- const handleConfirmation = useCallback2(
9506
+ const handleConfirmation = useCallback3(
8637
9507
  (decision, toolCalls) => {
8638
9508
  if (!agentInstance.current) return;
8639
9509
  setPendingConfirmation(null);
@@ -8655,7 +9525,6 @@ Please use command_status to check the result and report back to the user.`;
8655
9525
  []
8656
9526
  );
8657
9527
  useEffect7(() => {
8658
- setHistory([{ id: 0, component: /* @__PURE__ */ jsx20(Header, { sessionId, workdir }) }]);
8659
9528
  const initializeAgent = async () => {
8660
9529
  try {
8661
9530
  agentInstance.current = new Agent(sessionId, eventBus);
@@ -8680,12 +9549,11 @@ Please use command_status to check the result and report back to the user.`;
8680
9549
  if (t == null) return;
8681
9550
  turnStartedAtRef.current = null;
8682
9551
  const ms = Date.now() - t;
8683
- const sec = ms < 1e4 ? (ms / 1e3).toFixed(1) : String(Math.round(ms / 1e3));
8684
9552
  setHistory((prev) => [
8685
9553
  ...prev,
8686
9554
  {
8687
9555
  id: prev.length,
8688
- component: /* @__PURE__ */ jsx20(ChatTurnDuration, { secondsFormatted: sec })
9556
+ component: /* @__PURE__ */ jsx20(ChatTurnDuration, { durationMs: ms })
8689
9557
  }
8690
9558
  ]);
8691
9559
  };
@@ -8724,10 +9592,6 @@ Please use command_status to check the result and report back to the user.`;
8724
9592
  setToolsCount(parsed.tools);
8725
9593
  setMcpStatus("connected");
8726
9594
  setIsProcessing(false);
8727
- setHistory((prev) => {
8728
- const newHistory = [...prev];
8729
- return newHistory;
8730
- });
8731
9595
  if (!updateCheckRan.current) {
8732
9596
  updateCheckRan.current = true;
8733
9597
  Promise.resolve().then(() => checkForUpdates()).then((msg) => {
@@ -8766,7 +9630,9 @@ Please use command_status to check the result and report back to the user.`;
8766
9630
  }
8767
9631
  );
8768
9632
  } else if (parsed.type === "tool_call") {
8769
- const nextId3 = history.length;
9633
+ if (parsed.tool_name) {
9634
+ setRecentActivityLine(`Tool: ${String(parsed.tool_name)}`);
9635
+ }
8770
9636
  newComponent = /* @__PURE__ */ jsx20(
8771
9637
  ToolCallDisplay,
8772
9638
  {
@@ -8784,6 +9650,11 @@ Please use command_status to check the result and report back to the user.`;
8784
9650
  }
8785
9651
  );
8786
9652
  } else if (parsed.type === "user_overlay") {
9653
+ if (parsed.payload != null && String(parsed.payload).trim()) {
9654
+ setRecentActivityLine(
9655
+ `Context: ${trimRecentActivity(String(parsed.payload))}`
9656
+ );
9657
+ }
8787
9658
  newComponent = /* @__PURE__ */ jsx20(ChatUserMessage, { children: /* @__PURE__ */ jsx20(Text19, { dimColor: true, children: parsed.payload }) });
8788
9659
  } else if (parsed.type === "reasoning") {
8789
9660
  newComponent = /* @__PURE__ */ jsx20(ReasoningDisplay, { reasoning: parsed.content });
@@ -8806,10 +9677,9 @@ Please use command_status to check the result and report back to the user.`;
8806
9677
  if (t != null) {
8807
9678
  turnStartedAtRef.current = null;
8808
9679
  const ms = Date.now() - t;
8809
- const sec = ms < 1e4 ? (ms / 1e3).toFixed(1) : String(Math.round(ms / 1e3));
8810
9680
  next.push({
8811
9681
  id: next.length,
8812
- component: /* @__PURE__ */ jsx20(ChatTurnDuration, { secondsFormatted: sec })
9682
+ component: /* @__PURE__ */ jsx20(ChatTurnDuration, { durationMs: ms })
8813
9683
  });
8814
9684
  }
8815
9685
  }
@@ -8974,9 +9844,9 @@ async function runAgentMode() {
8974
9844
  try {
8975
9845
  if (inputFileIndex !== -1 && args[inputFileIndex + 1]) {
8976
9846
  const filePath = args[inputFileIndex + 1];
8977
- rawPayload = fs15.readFileSync(filePath, "utf-8");
9847
+ rawPayload = fs17.readFileSync(filePath, "utf-8");
8978
9848
  } else {
8979
- rawPayload = fs15.readFileSync(0, "utf-8");
9849
+ rawPayload = fs17.readFileSync(0, "utf-8");
8980
9850
  }
8981
9851
  } catch (err) {
8982
9852
  writeJsonl({
@@ -9142,6 +10012,16 @@ async function runAgentMode() {
9142
10012
  process.exit(1);
9143
10013
  }
9144
10014
  }
10015
+ function readCliPackageVersion() {
10016
+ try {
10017
+ const base = path21.dirname(fileURLToPath4(import.meta.url));
10018
+ const pkgPath = path21.join(base, "..", "package.json");
10019
+ const j = JSON.parse(fs17.readFileSync(pkgPath, "utf8"));
10020
+ return String(j.version || "0.0.0");
10021
+ } catch {
10022
+ return "0.0.0";
10023
+ }
10024
+ }
9145
10025
  function runCliMode() {
9146
10026
  const BLUMA_TITLE = process.env.BLUMA_TITLE || "BluMa - NomadEngenuity";
9147
10027
  startTitleKeeper(BLUMA_TITLE);
@@ -9149,7 +10029,8 @@ function runCliMode() {
9149
10029
  const sessionId = uuidv46();
9150
10030
  const props = {
9151
10031
  eventBus,
9152
- sessionId
10032
+ sessionId,
10033
+ cliVersion: readCliPackageVersion()
9153
10034
  };
9154
10035
  render(React12.createElement(App_default, props));
9155
10036
  }