u-foo 2.5.5 → 2.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/code/agent.js CHANGED
@@ -1,22 +1,8 @@
1
- const readline = require("readline");
2
1
  const fs = require("fs");
3
2
  const path = require("path");
4
- const { execSync } = require("child_process");
5
3
  const { runToolCall } = require("./dispatch");
6
4
  const { runNativeAgentTask } = require("./nativeRunner");
7
- const {
8
- runDecomposedTask,
9
- createBusProgressReporter,
10
- } = require("./taskDecomposer");
11
- const {
12
- runUcodeTui,
13
- shouldUseUcodeTui,
14
- buildUcodeBannerLines,
15
- StreamBuffer,
16
- createEscapeTagStripper,
17
- stripLeakedEscapeTags,
18
- } = require("./tui");
19
- const { stripBlessedTags } = require("../app/chat/text");
5
+ const { runDecomposedTask } = require("./taskDecomposer");
20
6
  const { loadConfig, defaultAgentModelForProvider, sameModelProvider } = require("../config");
21
7
  const {
22
8
  resolveSessionId,
@@ -25,77 +11,48 @@ const {
25
11
  loadSessionSnapshot,
26
12
  } = require("./sessionStore");
27
13
  const { buildPromptContext } = require("../agents/prompts/native");
14
+ const { buildSkillInjections } = require("./skills");
28
15
  const {
29
- buildSkillInjections,
30
- formatSkillsList,
31
- listUcodeSkills,
32
- showSkill,
33
- } = require("./skills");
34
- const { DeliveryQueue } = require("../coordination/bus/deliveryQueue");
35
-
36
- function printPrompt() {
37
- process.stdout.write("> ");
38
- }
39
-
40
- function printUcodeBanner(stdout = process.stdout, { model = "", workspaceRoot = process.cwd(), sessionId = "" } = {}) {
41
- stdout.write(`${buildUcodeBannerLines({
42
- model,
43
- engine: "ufoo-core",
44
- workspaceRoot,
45
- sessionId,
46
- width: (stdout && stdout.columns) || 0,
47
- }).join("\n")}\n`);
48
- }
49
-
50
- function normalizeLine(input = "") {
51
- return String(input || "").trim();
52
- }
53
-
54
- function parseLegacyUfooMarkerCommand(input = "") {
55
- const text = String(input || "").trim();
56
- if (!text) return "";
57
- // Old daemons injected strict "<prefix> <single-token>" commands for
58
- // session discovery. Keep ignoring those inputs after removing injection.
59
- const match = text.match(/^(?:\$ufoo|\/ufoo|ufoo)\s+([A-Za-z0-9][A-Za-z0-9._:-]{0,63})$/);
60
- return match ? String(match[1] || "").trim() : "";
61
- }
62
-
63
- function parseJson(text = "") {
64
- const raw = String(text || "").trim();
65
- if (!raw) return {};
66
- const parsed = JSON.parse(raw);
67
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
68
- return parsed;
69
- }
16
+ runUbusCommand,
17
+ parseBusCheckOutput,
18
+ extractBusMessageTask,
19
+ runShellCapture,
20
+ stripAnsi,
21
+ busCheckOutputIndicatesPending,
22
+ resolvePendingQueueFile,
23
+ resolveUfooProjectRoot,
24
+ countPendingQueueLines,
25
+ getPendingBusCount,
26
+ drainJsonlFile,
27
+ extractTaskFromBusEvent,
28
+ shouldAutoConsumeBus,
29
+ } = require("./busConsumer");
30
+ const {
31
+ runUcodeCoreAgent,
32
+ runSingleCommand,
33
+ extractAgentNickname,
34
+ parseAgentArgs,
35
+ } = require("./repl");
70
36
 
71
37
  function readTextOrFile(value = "") {
72
38
  const raw = String(value || "").trim();
73
39
  if (!raw) return "";
74
- try {
75
- if (fs.existsSync(raw)) return String(fs.readFileSync(raw, "utf8") || "");
76
- } catch {
77
- // ignore
40
+ // Only read from disk when the value clearly looks like a path; otherwise a
41
+ // prompt that happens to match an existing file would be silently replaced
42
+ // by that file's contents.
43
+ const looksLikePath = !/[\r\n]/.test(raw)
44
+ && (raw.startsWith("./") || raw.startsWith("/") || raw.startsWith("~")
45
+ || /\.(?:md|txt)$/i.test(raw));
46
+ if (looksLikePath) {
47
+ try {
48
+ if (fs.existsSync(raw)) return String(fs.readFileSync(raw, "utf8") || "");
49
+ } catch {
50
+ // ignore
51
+ }
78
52
  }
79
53
  return raw;
80
54
  }
81
55
 
82
- function extractAgentNickname(agentId = "") {
83
- // Extract nickname from agent ID like "ufoo-agent:abc123" -> "ufoo"
84
- const id = String(agentId || "").trim();
85
- if (!id) return "";
86
-
87
- // Remove the instance ID part (after colon)
88
- const base = id.split(":")[0];
89
-
90
- // Common agent nickname mappings
91
- if (base === "ufoo-agent") return "ufoo";
92
- if (base === "claude-code") return "claude";
93
- if (base === "ufoo-code") return "ucode";
94
-
95
- // Return base name as-is for others
96
- return base;
97
- }
98
-
99
56
  function resolveUcodeProviderModel({
100
57
  workspaceRoot = process.cwd(),
101
58
  provider = "",
@@ -440,6 +397,7 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
440
397
  const timeoutMs = Number.isFinite(state.timeoutMs) ? state.timeoutMs : 600000;
441
398
  let streamed = false;
442
399
  let streamLastChar = "";
400
+ let toolEventsThisAttempt = 0;
443
401
  const onDelta = typeof options.onDelta === "function"
444
402
  ? options.onDelta
445
403
  : null;
@@ -450,7 +408,7 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
450
408
  const pushToolLog = createToolLogCollector(logs, onToolLog);
451
409
 
452
410
  // Detect bug fix tasks and use decomposed runner
453
- const isBugFixTask = /fix|bug|issue|problem|error|broken|not work/i.test(taskText);
411
+ const isBugFixTask = /\b(?:fix(?:es|ed|ing)?|bugs?|issues?|problems?|errors?|broken)\b|doesn't work|not work/i.test(taskText);
454
412
  const useDecomposition = isBugFixTask && !options.disableDecomposition;
455
413
  const analysisTask = isProjectAnalysisTask(taskText);
456
414
  const workspaceRoot = String(state.workspaceRoot || process.cwd());
@@ -495,23 +453,27 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
495
453
  : runNativeAgentTask;
496
454
  const onPhase = typeof options.onPhase === "function" ? options.onPhase : null;
497
455
  const onThinkingDelta = typeof options.onThinkingDelta === "function" ? options.onThinkingDelta : null;
498
- const invokeNative = (sessionIdValue = "", timeoutOverrideMs = timeoutMs) => runNativeAgentImpl({
499
- workspaceRoot,
500
- provider,
501
- model,
502
- prompt: effectiveTaskPrompt,
503
- systemPrompt: systemContext,
504
- messages: Array.isArray(state.nlMessages) ? state.nlMessages : [],
505
- sessionId: String(sessionIdValue || ""),
506
- timeoutMs: timeoutOverrideMs,
507
- onStreamDelta: onStream,
508
- onThinkingDelta,
509
- onPhase,
510
- onToolEvent: (event) => {
511
- pushToolLog(event);
512
- },
513
- signal: options.signal,
514
- });
456
+ const invokeNative = (sessionIdValue = "", timeoutOverrideMs = timeoutMs) => {
457
+ toolEventsThisAttempt = 0;
458
+ return runNativeAgentImpl({
459
+ workspaceRoot,
460
+ provider,
461
+ model,
462
+ prompt: effectiveTaskPrompt,
463
+ systemPrompt: systemContext,
464
+ messages: Array.isArray(state.nlMessages) ? state.nlMessages : [],
465
+ sessionId: String(sessionIdValue || ""),
466
+ timeoutMs: timeoutOverrideMs,
467
+ onStreamDelta: onStream,
468
+ onThinkingDelta,
469
+ onPhase,
470
+ onToolEvent: (event) => {
471
+ toolEventsThisAttempt += 1;
472
+ pushToolLog(event);
473
+ },
474
+ signal: options.signal,
475
+ });
476
+ };
515
477
 
516
478
  try {
517
479
  let cliRes;
@@ -520,7 +482,6 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
520
482
  if (useDecomposition) {
521
483
  const decomposedResult = await runDecomposedTask({
522
484
  task: effectiveTaskPrompt,
523
- state,
524
485
  onProgress: options.onProgress,
525
486
  onToolEvent: pushToolLog,
526
487
  signal: options.signal,
@@ -551,7 +512,9 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
551
512
 
552
513
  if (!cliRes || cliRes.ok === false) {
553
514
  const errMsg = String((cliRes && cliRes.error) || "");
554
- if (isCliTimeoutError(errMsg)) {
515
+ // Only replay the whole task when this attempt ran no tool calls;
516
+ // retrying after executed write/edit/bash steps would replay side effects.
517
+ if (isCliTimeoutError(errMsg) && toolEventsThisAttempt === 0) {
555
518
  const extendedTimeoutMs = computeExtendedTimeout(timeoutMs);
556
519
  cliRes = await invokeNative(String(state.sessionId || ""), extendedTimeoutMs);
557
520
  }
@@ -672,6 +635,21 @@ function buildSessionSnapshotFromState(state = {}) {
672
635
 
673
636
  function persistSessionState(state = {}) {
674
637
  const snapshot = buildSessionSnapshotFromState(state);
638
+ if (!state.sessionId && snapshot.sessionId) {
639
+ state.sessionId = snapshot.sessionId;
640
+ }
641
+ // Skip writing sessions that carry no messages yet; otherwise every launch
642
+ // (even an immediate quit) leaves an empty session file behind and the
643
+ // sessions directory grows without bound.
644
+ if (!Array.isArray(snapshot.nlMessages) || snapshot.nlMessages.length === 0) {
645
+ return {
646
+ ok: true,
647
+ skipped: true,
648
+ error: "",
649
+ sessionId: snapshot.sessionId,
650
+ filePath: "",
651
+ };
652
+ }
675
653
  const saved = saveSessionSnapshot(snapshot.workspaceRoot, snapshot);
676
654
  if (saved && saved.ok) {
677
655
  state.sessionId = saved.sessionId;
@@ -724,995 +702,6 @@ function resumeSessionState(state = {}, sessionId = "", workspaceRoot = process.
724
702
  };
725
703
  }
726
704
 
727
- function shellQuote(value = "") {
728
- const text = String(value == null ? "" : value);
729
- return `'${text.replace(/'/g, `'\"'\"'`)}'`;
730
- }
731
-
732
- function toText(value = "") {
733
- if (typeof value === "string") return value;
734
- if (Buffer.isBuffer(value)) return value.toString("utf8");
735
- return String(value == null ? "" : value);
736
- }
737
-
738
- function stripAnsi(text = "") {
739
- const raw = String(text || "");
740
- if (!raw) return "";
741
- // CSI + OSC sequences (best-effort).
742
- return raw
743
- .replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "")
744
- .replace(/\x1b\][^\x07]*\x07/g, "")
745
- .replace(/\x1b\][^\x1b]*(?:\x1b\\)/g, "");
746
- }
747
-
748
- function runShellCapture(command = "", workspaceRoot = process.cwd()) {
749
- try {
750
- const output = execSync(String(command || ""), {
751
- cwd: workspaceRoot,
752
- encoding: "utf8",
753
- stdio: ["pipe", "pipe", "pipe"],
754
- });
755
- return {
756
- ok: true,
757
- output: toText(output),
758
- error: "",
759
- };
760
- } catch (err) {
761
- const stdout = toText(err && err.stdout);
762
- const stderr = toText(err && err.stderr);
763
- const detail = [stdout, stderr].filter(Boolean).join("\n").trim();
764
- return {
765
- ok: false,
766
- output: detail,
767
- error: detail || (err && err.message ? err.message : "shell command failed"),
768
- };
769
- }
770
- }
771
-
772
- function safeSubscriberName(subscriberId = "") {
773
- return String(subscriberId || "").replace(/:/g, "_");
774
- }
775
-
776
- function resolvePendingQueueFile(workspaceRoot = process.cwd(), subscriberId = "") {
777
- const root = String(workspaceRoot || process.cwd()).trim() || process.cwd();
778
- const sub = String(subscriberId || "").trim();
779
- if (!sub) return "";
780
- return path.join(root, ".ufoo", "bus", "queues", safeSubscriberName(sub), "pending.jsonl");
781
- }
782
-
783
- function resolveUfooProjectRoot(preferredRoot = "", env = process.env) {
784
- const candidates = [
785
- String(preferredRoot || "").trim(),
786
- String((env && env.UFOO_UCODE_PROJECT_ROOT) || "").trim(),
787
- String((env && env.UFOO_PROJECT_ROOT) || "").trim(),
788
- process.cwd(),
789
- ].filter(Boolean);
790
-
791
- for (const root of candidates) {
792
- try {
793
- const busDir = path.join(root, ".ufoo", "bus");
794
- if (fs.existsSync(busDir)) return root;
795
- } catch {
796
- // ignore
797
- }
798
- }
799
-
800
- return candidates[0] || process.cwd();
801
- }
802
-
803
- function countPendingQueueLines(filePath = "") {
804
- const target = String(filePath || "").trim();
805
- if (!target) return 0;
806
- try {
807
- if (!fs.existsSync(target)) return 0;
808
- const content = String(fs.readFileSync(target, "utf8") || "");
809
- if (!content.trim()) return 0;
810
- return content.split(/\r?\n/).filter((line) => line.trim()).length;
811
- } catch {
812
- return 0;
813
- }
814
- }
815
-
816
- function isPidAlive(pid) {
817
- const p = parseInt(String(pid || "").trim(), 10);
818
- if (!Number.isFinite(p) || p <= 0) return false;
819
- try {
820
- process.kill(p, 0);
821
- return true;
822
- } catch {
823
- return false;
824
- }
825
- }
826
-
827
- function listProcessingFiles(pendingFilePath = "") {
828
- const pendingFile = String(pendingFilePath || "").trim();
829
- if (!pendingFile) return [];
830
- const dir = path.dirname(pendingFile);
831
- const base = path.basename(pendingFile);
832
- const prefix = `${base}.processing.`;
833
- try {
834
- if (!fs.existsSync(dir)) return [];
835
- return fs.readdirSync(dir)
836
- .filter((name) => name && name.startsWith(prefix))
837
- .map((name) => path.join(dir, name));
838
- } catch {
839
- return [];
840
- }
841
- }
842
-
843
- function countRecoverableProcessingFiles(pendingFilePath = "", options = {}) {
844
- const pendingFile = String(pendingFilePath || "").trim();
845
- if (!pendingFile) return 0;
846
- const maxAgeMs = Number.isFinite(options.maxAgeMs) ? options.maxAgeMs : 60000;
847
- const now = Date.now();
848
- const files = listProcessingFiles(pendingFile);
849
- let count = 0;
850
-
851
- for (const file of files) {
852
- const name = path.basename(file);
853
- const m = name.match(/\.processing\.(\d+)\./);
854
- const pid = m ? parseInt(m[1], 10) : NaN;
855
-
856
- if (Number.isFinite(pid) && pid > 0 && !isPidAlive(pid)) {
857
- count += 1;
858
- continue;
859
- }
860
-
861
- if (!Number.isFinite(maxAgeMs) || maxAgeMs <= 0) continue;
862
- try {
863
- const stat = fs.statSync(file);
864
- if (stat && stat.isFile() && (now - stat.mtimeMs > maxAgeMs)) {
865
- count += 1;
866
- }
867
- } catch {
868
- // ignore
869
- }
870
- }
871
-
872
- return count;
873
- }
874
-
875
- function getPendingBusCount(workspaceRoot = process.cwd(), subscriberId = "") {
876
- const pendingFile = resolvePendingQueueFile(workspaceRoot, subscriberId);
877
- const pendingLines = countPendingQueueLines(pendingFile);
878
- if (!pendingFile) return pendingLines;
879
- // If a prior crash left `.processing.*` behind, count it so autoBus can self-heal.
880
- const recoverable = countRecoverableProcessingFiles(pendingFile, { maxAgeMs: 60000 });
881
- return pendingLines + recoverable;
882
- }
883
-
884
- function drainJsonlFile(filePath = "") {
885
- const target = String(filePath || "").trim();
886
- if (!target) return { drained: [], rawLines: [], error: "" };
887
- const queue = new DeliveryQueue(target);
888
- const drained = [];
889
- const rawLines = [];
890
- const claims = [];
891
- try {
892
- queue.recover();
893
- while (true) {
894
- const claim = queue.claimNext();
895
- if (!claim) break;
896
- claims.push(claim);
897
- drained.push(claim.event);
898
- rawLines.push(JSON.stringify(claim.event));
899
- queue.completeClaim(claim);
900
- }
901
- } catch (err) {
902
- for (const claim of claims) queue.restoreClaim(claim);
903
- return { drained: [], rawLines: [], error: err && err.message ? err.message : "drain failed" };
904
- }
905
- return {
906
- drained,
907
- rawLines,
908
- error: "",
909
- claims,
910
- processingFile: claims[0] ? claims[0].processingFile : "",
911
- };
912
- }
913
-
914
- function extractTaskFromBusEvent(evt) {
915
- if (!evt || typeof evt !== "object") return null;
916
- if (String(evt.event || "").trim().toLowerCase() !== "message") return null;
917
- let publisher = "";
918
- if (typeof evt.publisher === "string") {
919
- publisher = String(evt.publisher || "").trim();
920
- } else if (evt.publisher && typeof evt.publisher === "object") {
921
- publisher = String(evt.publisher.subscriber || evt.publisher.nickname || "").trim();
922
- } else {
923
- publisher = String(evt.publisher || "").trim();
924
- }
925
- if (publisher === "[object Object]") publisher = "";
926
- if (!publisher) return null;
927
- const data = evt.data && typeof evt.data === "object" ? evt.data : {};
928
- const message = typeof data.message === "string"
929
- ? data.message
930
- : (typeof data.text === "string" ? data.text : "");
931
- const task = String(message || "").trim();
932
- if (!task) return null;
933
- return { publisher, task };
934
- }
935
-
936
- function shouldAutoConsumeBus(subscriberId = "") {
937
- const id = String(subscriberId || "").trim().toLowerCase();
938
- if (!id) return false;
939
- return id.startsWith("ufoo-code:")
940
- || id.startsWith("ucode:")
941
- || id.startsWith("ufoo:");
942
- }
943
-
944
- function extractBusMessageTask(contentRaw = "") {
945
- const raw = String(contentRaw || "").trim();
946
- if (!raw) return "";
947
- try {
948
- const parsed = JSON.parse(raw);
949
- if (parsed && typeof parsed === "object") {
950
- if (typeof parsed.message === "string" && parsed.message.trim()) return parsed.message.trim();
951
- if (typeof parsed.text === "string" && parsed.text.trim()) return parsed.text.trim();
952
- if (typeof parsed.prompt === "string" && parsed.prompt.trim()) return parsed.prompt.trim();
953
- }
954
- } catch {
955
- // treat as plain text below
956
- }
957
- return raw;
958
- }
959
-
960
- function busCheckOutputIndicatesPending(raw = "") {
961
- const text = stripAnsi(String(raw || ""));
962
- if (!text.trim()) return false;
963
- if (/no pending messages/i.test(text)) return false;
964
- if (/you have\s+\d+\s+pending/i.test(text)) return true;
965
- if (/after handling,\s*run:\s*ufoo bus ack/i.test(text)) return true;
966
- if (/pending event/i.test(text)) return true;
967
- return false;
968
- }
969
-
970
- function parseBusCheckOutput(raw = "") {
971
- const text = stripAnsi(String(raw || ""));
972
- if (!text.trim()) return [];
973
- if (/no pending messages/i.test(text)) return [];
974
-
975
- const lines = text.split(/\r?\n/);
976
- const rows = [];
977
- let current = null;
978
-
979
- for (const line of lines) {
980
- const trimmed = String(line || "").trim();
981
- if (!trimmed) continue;
982
-
983
- const header = trimmed.match(/^@.+\s+from\s+([^\s]+)\s*$/i);
984
- if (header) {
985
- if (current && current.publisher) rows.push(current);
986
- current = {
987
- publisher: String(header[1] || "").trim(),
988
- content: "",
989
- };
990
- continue;
991
- }
992
-
993
- if (!current) continue;
994
-
995
- const contentMatch = trimmed.match(/^content:\s*(.*)$/i);
996
- if (contentMatch) {
997
- current.content = String(contentMatch[1] || "").trim();
998
- continue;
999
- }
1000
-
1001
- if (
1002
- current.content
1003
- && !/^(type|event|seq|target|timestamp):\s*/i.test(trimmed)
1004
- && !trimmed.startsWith("@")
1005
- ) {
1006
- current.content = `${current.content}\n${trimmed}`;
1007
- }
1008
- }
1009
-
1010
- if (current && current.publisher) rows.push(current);
1011
-
1012
- return rows
1013
- .map((entry) => {
1014
- const publisher = String(entry.publisher || "").trim();
1015
- const content = String(entry.content || "").trim();
1016
- const task = extractBusMessageTask(content);
1017
- if (!publisher || !task) return null;
1018
- return {
1019
- publisher,
1020
- content,
1021
- task,
1022
- };
1023
- })
1024
- .filter(Boolean);
1025
- }
1026
-
1027
- async function runUbusCommand(state = {}, options = {}) {
1028
- const runtimeWorkspace = resolveUfooProjectRoot(String(
1029
- options.workspaceRoot
1030
- || (state && state.workspaceRoot)
1031
- || ""
1032
- ));
1033
- const shell = typeof options.execShell === "function"
1034
- ? options.execShell
1035
- : (command) => runShellCapture(command, runtimeWorkspace);
1036
- const runNl = typeof options.runNaturalLanguageTaskImpl === "function"
1037
- ? options.runNaturalLanguageTaskImpl
1038
- : runNaturalLanguageTask;
1039
- const formatNl = typeof options.formatNlResultImpl === "function"
1040
- ? options.formatNlResultImpl
1041
- : formatNlResult;
1042
- const onMessageReceived = typeof options.onMessageReceived === "function"
1043
- ? options.onMessageReceived
1044
- : null;
1045
-
1046
- const explicitSubscriber = String(options.subscriberId || "").trim();
1047
- const envSubscriber = String(process.env.UFOO_SUBSCRIBER_ID || "").trim();
1048
- let subscriberId = explicitSubscriber || envSubscriber;
1049
- if (!subscriberId) {
1050
- const whoami = shell("ufoo bus whoami 2>/dev/null || true");
1051
- subscriberId = String((whoami && whoami.output) || "").trim();
1052
- }
1053
- if (!subscriberId) {
1054
- const joined = shell("ufoo bus join | tail -1");
1055
- subscriberId = String((joined && joined.output) || "").trim();
1056
- }
1057
- if (!subscriberId) {
1058
- return {
1059
- ok: false,
1060
- summary: "",
1061
- error: "failed to resolve bus subscriber id",
1062
- handled: 0,
1063
- subscriberId: "",
1064
- };
1065
- }
1066
-
1067
- // Prefer consuming pending.jsonl directly (stable, ANSI/wrapping-proof).
1068
- const pendingFile = resolvePendingQueueFile(runtimeWorkspace, subscriberId);
1069
- const queue = pendingFile ? new DeliveryQueue(pendingFile) : null;
1070
- if (queue) queue.recover();
1071
- const hasPendingFile = Boolean(pendingFile && fs.existsSync(pendingFile));
1072
- let handled = 0;
1073
- const sendErrors = [];
1074
- const messageExchanges = [];
1075
-
1076
- if (queue && hasPendingFile) {
1077
- while (fs.existsSync(pendingFile)) {
1078
- const claim = queue.claimNext();
1079
- if (!claim) break;
1080
- const message = extractTaskFromBusEvent(claim.event);
1081
- if (!message) {
1082
- queue.completeClaim(claim);
1083
- continue;
1084
- }
1085
- let nlResult;
1086
-
1087
- // Notify that we received the message (for immediate display)
1088
- if (onMessageReceived) {
1089
- onMessageReceived({
1090
- from: message.publisher,
1091
- task: message.task,
1092
- });
1093
- }
1094
-
1095
- // Create progress reporter for this message
1096
- const progressReporter = createBusProgressReporter(shell, message.publisher);
1097
-
1098
- try {
1099
- // Send initial acknowledgment
1100
- shell(`ufoo bus send ${shellQuote(message.publisher)} ${shellQuote("🚀 Starting task...")}`);
1101
-
1102
- // eslint-disable-next-line no-await-in-loop
1103
- nlResult = await runNl(message.task, state, {
1104
- onProgress: progressReporter,
1105
- signal: options.signal,
1106
- });
1107
- } catch (err) {
1108
- const errorMessage = err && err.message ? err.message : "task failed";
1109
- sendErrors.push(`task from ${message.publisher} failed: ${errorMessage}`);
1110
- queue.restoreClaim(claim);
1111
- // Send error notification
1112
- shell(`ufoo bus send ${shellQuote(message.publisher)} ${shellQuote(`❌ Error: ${errorMessage}`)}`);
1113
- break;
1114
- }
1115
- const reply = String(formatNl(nlResult, false) || "").replace(/\s+/g, " ").trim() || "Done.";
1116
- const sendRes = shell(`ufoo bus send ${shellQuote(message.publisher)} ${shellQuote(reply.slice(0, 2000))}`);
1117
- if (!sendRes.ok) {
1118
- sendErrors.push(`reply to ${message.publisher} failed: ${sendRes.error || "send failed"}`);
1119
- queue.restoreClaim(claim);
1120
- break;
1121
- }
1122
- handled += 1;
1123
- queue.completeClaim(claim);
1124
- messageExchanges.push({
1125
- from: message.publisher,
1126
- task: message.task,
1127
- reply,
1128
- });
1129
- }
1130
- }
1131
-
1132
- // Fallback: if there is no pending file, fall back to CLI `bus check` parsing.
1133
- if (!hasPendingFile) {
1134
- const checked = shell(`ufoo bus check ${shellQuote(subscriberId)}`);
1135
- if (!checked.ok) {
1136
- return {
1137
- ok: false,
1138
- summary: "",
1139
- error: checked.error || "ufoo bus check failed",
1140
- handled: 0,
1141
- subscriberId,
1142
- };
1143
- }
1144
- const parsed = parseBusCheckOutput(checked.output);
1145
- if (parsed.length === 0 && busCheckOutputIndicatesPending(checked.output)) {
1146
- return {
1147
- ok: false,
1148
- summary: "",
1149
- error: "failed to parse ufoo bus check output (pending events detected).",
1150
- handled: 0,
1151
- subscriberId,
1152
- };
1153
- }
1154
- for (const item of parsed) {
1155
- // Notify that we received the message (for immediate display)
1156
- if (onMessageReceived) {
1157
- onMessageReceived({
1158
- from: item.publisher,
1159
- task: item.task,
1160
- });
1161
- }
1162
-
1163
- const nlResult = await runNl(item.task, state, {
1164
- signal: options.signal,
1165
- });
1166
- const reply = String(formatNl(nlResult, false) || "").replace(/\s+/g, " ").trim() || "Done.";
1167
- const sendRes = shell(`ufoo bus send ${shellQuote(item.publisher)} ${shellQuote(reply.slice(0, 2000))}`);
1168
- if (!sendRes.ok) {
1169
- sendErrors.push(`reply to ${item.publisher} failed: ${sendRes.error || "send failed"}`);
1170
- continue;
1171
- }
1172
- handled += 1;
1173
- messageExchanges.push({
1174
- from: item.publisher,
1175
- task: item.task,
1176
- reply,
1177
- });
1178
- }
1179
- }
1180
-
1181
- if (sendErrors.length > 0) {
1182
- return {
1183
- ok: false,
1184
- summary: "",
1185
- error: sendErrors.join("; "),
1186
- handled,
1187
- subscriberId,
1188
- messageExchanges,
1189
- };
1190
- }
1191
-
1192
- const summary = handled > 0
1193
- ? `ubus: handled ${handled} message${handled === 1 ? "" : "s"} for ${subscriberId}.`
1194
- : `ubus: no pending messages for ${subscriberId}.`;
1195
- return {
1196
- ok: true,
1197
- summary,
1198
- error: "",
1199
- handled,
1200
- subscriberId,
1201
- messageExchanges,
1202
- };
1203
- }
1204
-
1205
- function runSingleCommand(line = "", workspaceRoot = process.cwd()) {
1206
- const text = normalizeLine(line);
1207
- if (!text) return { kind: "empty" };
1208
- if (text === "exit" || text === "quit") return { kind: "exit" };
1209
- if (text === "help") {
1210
- return {
1211
- kind: "help",
1212
- output: [
1213
- "Commands:",
1214
- " help",
1215
- " exit|quit",
1216
- " ubus|/ubus",
1217
- " skills [list]",
1218
- " skills show <name>",
1219
- " bg|/bg <task>",
1220
- " resume <session-id>",
1221
- " tool <read|write|edit|bash> <args-json>",
1222
- " run <read|write|edit|bash> <args-json>",
1223
- ].join("\n"),
1224
- };
1225
- }
1226
- const legacyUfooMarker = parseLegacyUfooMarkerCommand(text);
1227
- if (legacyUfooMarker) {
1228
- return {
1229
- kind: "legacy_ufoo_marker",
1230
- marker: legacyUfooMarker,
1231
- };
1232
- }
1233
- if (text === "ubus" || text === "/ubus") {
1234
- return {
1235
- kind: "ubus",
1236
- };
1237
- }
1238
- const skillsMatch = text.match(/^(?:\/skills|skills)(?:\s+(.*))?$/i);
1239
- if (skillsMatch) {
1240
- const args = String(skillsMatch[1] || "").trim().split(/\s+/).filter(Boolean);
1241
- const action = String(args[0] || "list").toLowerCase();
1242
- if (action === "list" || action === "ls") {
1243
- const outcome = listUcodeSkills({ workspaceRoot });
1244
- return {
1245
- kind: "skills",
1246
- output: formatSkillsList(outcome),
1247
- skills: outcome.skills,
1248
- errors: outcome.errors,
1249
- };
1250
- }
1251
- if (action === "show") {
1252
- const name = String(args[1] || "").trim();
1253
- if (!name) {
1254
- return {
1255
- kind: "error",
1256
- output: "usage: skills show <name>",
1257
- };
1258
- }
1259
- const result = showSkill({ name, workspaceRoot });
1260
- if (!result.ok) {
1261
- return {
1262
- kind: "error",
1263
- output: result.error,
1264
- };
1265
- }
1266
- return {
1267
- kind: "skills",
1268
- output: result.output,
1269
- skill: result.skill,
1270
- };
1271
- }
1272
- return {
1273
- kind: "error",
1274
- output: "usage: skills [list] | skills show <name>",
1275
- };
1276
- }
1277
- if (text === "bg" || text === "/bg") {
1278
- return {
1279
- kind: "error",
1280
- output: "usage: bg <task>",
1281
- };
1282
- }
1283
- const bgMatch = text.match(/^(?:\/bg|bg)\s+(.+)$/i);
1284
- if (bgMatch) {
1285
- const task = String(bgMatch[1] || "").trim();
1286
- if (!task) {
1287
- return {
1288
- kind: "error",
1289
- output: "usage: bg <task>",
1290
- };
1291
- }
1292
- return {
1293
- kind: "nl_bg",
1294
- task,
1295
- };
1296
- }
1297
- const resumeMatch = text.match(/^resume(?:\s+(.+))?$/i);
1298
- if (resumeMatch) {
1299
- const session = String(resumeMatch[1] || "").trim();
1300
- if (!session) {
1301
- return {
1302
- kind: "error",
1303
- output: "usage: resume <session-id>",
1304
- };
1305
- }
1306
- return {
1307
- kind: "resume",
1308
- sessionId: session,
1309
- };
1310
- }
1311
-
1312
- const match = text.match(/^(tool|run)\s+([a-zA-Z_-]+)\s*(.*)$/);
1313
- if (!match) {
1314
- return {
1315
- kind: "nl",
1316
- task: text,
1317
- };
1318
- }
1319
- const tool = String(match[2] || "").trim().toLowerCase();
1320
- const payload = String(match[3] || "").trim();
1321
- let args = {};
1322
- try {
1323
- args = parseJson(payload);
1324
- } catch (err) {
1325
- return {
1326
- kind: "error",
1327
- output: JSON.stringify({ ok: false, error: err && err.message ? err.message : "invalid json" }),
1328
- };
1329
- }
1330
- const result = runToolCall(
1331
- { tool, args },
1332
- { workspaceRoot, cwd: workspaceRoot }
1333
- );
1334
- return {
1335
- kind: "tool",
1336
- tool,
1337
- args,
1338
- result,
1339
- output: JSON.stringify(result),
1340
- };
1341
- }
1342
-
1343
- async function runUcodeCoreAgent({
1344
- stdin = process.stdin,
1345
- stdout = process.stdout,
1346
- workspaceRoot = process.cwd(),
1347
- provider = "",
1348
- model = "",
1349
- appendSystemPrompt = "",
1350
- systemPrompt = "",
1351
- sessionId = "",
1352
- timeoutMs = 600000,
1353
- jsonOutput = false,
1354
- forceTui = false,
1355
- disableTui = false,
1356
- } = {}) {
1357
- const resolvedWorkspaceRoot = resolveUfooProjectRoot(workspaceRoot);
1358
- const resolvedUcode = resolveUcodeProviderModel({
1359
- workspaceRoot: resolvedWorkspaceRoot,
1360
- provider,
1361
- model,
1362
- });
1363
- const state = {
1364
- workspaceRoot: resolvedWorkspaceRoot,
1365
- provider: resolvedUcode.provider,
1366
- model: resolvedUcode.model,
1367
- engine: "ufoo-core",
1368
- context: buildNlContext({
1369
- appendSystemPrompt,
1370
- systemPrompt,
1371
- workspaceRoot: resolvedWorkspaceRoot,
1372
- model: resolvedUcode.model,
1373
- provider: resolvedUcode.provider,
1374
- }),
1375
- nlMessages: [],
1376
- sessionId: resolveSessionId(String(sessionId || "").trim()),
1377
- timeoutMs,
1378
- jsonOutput,
1379
- };
1380
- persistSessionState(state);
1381
-
1382
- if (shouldUseUcodeTui({
1383
- stdin,
1384
- stdout,
1385
- jsonOutput,
1386
- forceTui,
1387
- disableTui: disableTui || process.env.UFOO_UCODE_NO_TUI === "1",
1388
- })) {
1389
- return runUcodeTui({
1390
- stdin,
1391
- stdout,
1392
- runSingleCommand,
1393
- runNaturalLanguageTask,
1394
- runUbusCommand,
1395
- formatNlResult,
1396
- workspaceRoot,
1397
- state,
1398
- resumeSessionState,
1399
- persistSessionState,
1400
- autoBus: {
1401
- enabled: shouldAutoConsumeBus(process.env.UFOO_SUBSCRIBER_ID || ""),
1402
- getPendingCount: () => getPendingBusCount(state.workspaceRoot || workspaceRoot, process.env.UFOO_SUBSCRIBER_ID || ""),
1403
- subscriberId: String(process.env.UFOO_SUBSCRIBER_ID || "").trim(),
1404
- },
1405
- });
1406
- }
1407
-
1408
- printUcodeBanner(stdout, {
1409
- model: state.model || "default",
1410
- workspaceRoot: workspaceRoot,
1411
- sessionId: state.sessionId,
1412
- });
1413
- printPrompt();
1414
- const rl = readline.createInterface({
1415
- input: stdin,
1416
- output: stdout,
1417
- terminal: true,
1418
- historySize: 200,
1419
- });
1420
- return new Promise((resolve) => {
1421
- let chain = Promise.resolve();
1422
- let backgroundSeq = 0;
1423
- const backgroundRuns = new Map();
1424
- const subscriberId = String(process.env.UFOO_SUBSCRIBER_ID || "").trim();
1425
- const autoBusEnabled = shouldAutoConsumeBus(subscriberId);
1426
- let autoBusTimer = null;
1427
- let autoBusQueued = false;
1428
- let autoBusError = "";
1429
- let closing = false;
1430
-
1431
- const runAutoBusOnce = async () => {
1432
- if (!autoBusEnabled || closing) return;
1433
- if (getPendingBusCount(state.workspaceRoot || workspaceRoot, subscriberId) <= 0) {
1434
- autoBusError = "";
1435
- return;
1436
- }
1437
- const ubusResult = await runUbusCommand(state, {
1438
- workspaceRoot: state.workspaceRoot || workspaceRoot,
1439
- subscriberId,
1440
- });
1441
- if (!ubusResult.ok) {
1442
- const nextError = String(ubusResult.error || "ubus failed");
1443
- if (nextError !== autoBusError) {
1444
- autoBusError = nextError;
1445
- stdout.write(`Error: ${nextError}\n`);
1446
- printPrompt();
1447
- }
1448
- return;
1449
- }
1450
- autoBusError = "";
1451
- if (ubusResult.handled > 0) {
1452
- const persisted = persistSessionState(state);
1453
- if (!persisted || persisted.ok === false) {
1454
- stdout.write(`Warning: failed to persist session ${state.sessionId}: ${(persisted && persisted.error) || "unknown error"}\n`);
1455
- printPrompt();
1456
- }
1457
- }
1458
- };
1459
-
1460
- const scheduleAutoBus = () => {
1461
- if (!autoBusEnabled || closing || autoBusQueued) return;
1462
- if (getPendingBusCount(state.workspaceRoot || workspaceRoot, subscriberId) <= 0) return;
1463
- autoBusQueued = true;
1464
- chain = chain
1465
- .then(() => runAutoBusOnce())
1466
- .catch(() => {})
1467
- .finally(() => {
1468
- autoBusQueued = false;
1469
- });
1470
- };
1471
-
1472
- if (autoBusEnabled) {
1473
- autoBusTimer = setInterval(() => {
1474
- scheduleAutoBus();
1475
- }, 800);
1476
- scheduleAutoBus();
1477
- }
1478
-
1479
- const startBackgroundTask = (task = "") => {
1480
- backgroundSeq += 1;
1481
- const jobId = `bg-${Date.now().toString(36)}-${backgroundSeq.toString(36)}`;
1482
- const bgState = {
1483
- workspaceRoot: state.workspaceRoot,
1484
- provider: state.provider,
1485
- model: state.model,
1486
- engine: state.engine,
1487
- context: state.context,
1488
- nlMessages: Array.isArray(state.nlMessages) ? state.nlMessages.slice() : [],
1489
- sessionId: "",
1490
- timeoutMs: state.timeoutMs,
1491
- jsonOutput: false,
1492
- };
1493
- const run = runNaturalLanguageTask(task, bgState)
1494
- .then((nlResult) => {
1495
- const summary = String(formatNlResult(nlResult, false) || "").trim();
1496
- const title = nlResult && nlResult.ok ? "done" : "failed";
1497
- stdout.write(`[${jobId}] ${title}: ${summary || "no summary"}\n`);
1498
- printPrompt();
1499
- })
1500
- .catch((err) => {
1501
- stdout.write(`[${jobId}] failed: ${err && err.message ? err.message : "background task failed"}\n`);
1502
- printPrompt();
1503
- })
1504
- .finally(() => {
1505
- backgroundRuns.delete(jobId);
1506
- });
1507
- backgroundRuns.set(jobId, run);
1508
- return jobId;
1509
- };
1510
-
1511
- const handleLine = async (line) => {
1512
- const runtimeWorkspace = String(state.workspaceRoot || workspaceRoot || process.cwd());
1513
- const result = runSingleCommand(line, runtimeWorkspace);
1514
- if (result.kind === "exit") {
1515
- rl.close();
1516
- return;
1517
- }
1518
- if (result.kind === "legacy_ufoo_marker") {
1519
- return;
1520
- }
1521
- if (result.kind === "help" || result.kind === "tool" || result.kind === "skills" || result.kind === "error") {
1522
- stdout.write(`${result.output}\n`);
1523
- }
1524
- if (result.kind === "ubus") {
1525
- const ubusResult = await runUbusCommand(state, {
1526
- workspaceRoot: runtimeWorkspace,
1527
- onMessageReceived: (msg) => {
1528
- // Display the incoming message immediately
1529
- const nickname = extractAgentNickname(msg.from) || msg.from;
1530
- stdout.write(`${nickname}: ${msg.task}\n`);
1531
- },
1532
- });
1533
- if (!ubusResult.ok) {
1534
- stdout.write(`Error: ${ubusResult.error}\n`);
1535
- } else {
1536
- // Display replies for each message
1537
- if (ubusResult.messageExchanges && ubusResult.messageExchanges.length > 0) {
1538
- for (const exchange of ubusResult.messageExchanges) {
1539
- const nickname = extractAgentNickname(exchange.from) || exchange.from;
1540
- stdout.write(`@${nickname} ${exchange.reply}\n`);
1541
- }
1542
- } else {
1543
- stdout.write(`${ubusResult.summary}\n`);
1544
- }
1545
- persistSessionState(state);
1546
- }
1547
- }
1548
- if (result.kind === "resume") {
1549
- const resumed = resumeSessionState(state, result.sessionId, workspaceRoot);
1550
- if (!resumed.ok) {
1551
- stdout.write(`Error: ${resumed.error}\n`);
1552
- } else {
1553
- stdout.write(`Resumed session ${resumed.sessionId} (${resumed.restoredMessages} messages).\n`);
1554
- }
1555
- }
1556
- if (result.kind === "nl_bg") {
1557
- const jobId = startBackgroundTask(result.task);
1558
- stdout.write(`[${jobId}] started in background.\n`);
1559
- }
1560
- if (result.kind === "nl") {
1561
- let streamBuffer = null;
1562
- let streamedVisible = false;
1563
- const escapeStripper = createEscapeTagStripper();
1564
- if (!state.jsonOutput) {
1565
- streamBuffer = new StreamBuffer(stdout.write.bind(stdout), {
1566
- delay: 10,
1567
- chunkSize: 4,
1568
- });
1569
- }
1570
-
1571
- const nlResult = await runNaturalLanguageTask(result.task, state, {
1572
- onDelta: state.jsonOutput
1573
- ? null
1574
- : async (delta) => {
1575
- const text = escapeStripper.write(String(delta || ""));
1576
- const safeText = stripBlessedTags(stripLeakedEscapeTags(text));
1577
- if (!safeText) return;
1578
- if (/[^\s]/.test(safeText)) {
1579
- streamedVisible = true;
1580
- }
1581
- if (streamBuffer) {
1582
- await streamBuffer.write(safeText);
1583
- } else {
1584
- stdout.write(safeText);
1585
- }
1586
- },
1587
- });
1588
-
1589
- if (!state.jsonOutput) {
1590
- const tail = escapeStripper.flush();
1591
- const safeTail = stripBlessedTags(stripLeakedEscapeTags(tail));
1592
- if (safeTail) {
1593
- if (/[^\s]/.test(safeTail)) {
1594
- streamedVisible = true;
1595
- }
1596
- if (streamBuffer) {
1597
- await streamBuffer.write(safeTail);
1598
- } else {
1599
- stdout.write(safeTail);
1600
- }
1601
- }
1602
- }
1603
-
1604
- // Ensure buffer is flushed
1605
- if (streamBuffer) {
1606
- await streamBuffer.finish();
1607
- }
1608
-
1609
- const streamed = !state.jsonOutput && Boolean(nlResult && nlResult.streamed);
1610
- if (streamed && streamedVisible && nlResult && nlResult.streamLastChar !== "\n") {
1611
- stdout.write("\n");
1612
- }
1613
- const shouldSkipSummary = Boolean(streamed && nlResult && nlResult.ok && streamedVisible);
1614
- if (!shouldSkipSummary) {
1615
- const formatted = formatNlResult(nlResult, state.jsonOutput);
1616
- const safeOutput = state.jsonOutput
1617
- ? formatted
1618
- : stripBlessedTags(stripLeakedEscapeTags(formatted));
1619
- stdout.write(`${safeOutput}\n`);
1620
- }
1621
- const persisted = persistSessionState(state);
1622
- if (!state.jsonOutput && (!persisted || persisted.ok === false)) {
1623
- stdout.write(`Warning: failed to persist session ${state.sessionId}: ${(persisted && persisted.error) || "unknown error"}\n`);
1624
- }
1625
- }
1626
- printPrompt();
1627
- };
1628
-
1629
- rl.on("line", (line) => {
1630
- chain = chain.then(() => handleLine(line)).catch((err) => {
1631
- stdout.write(`${JSON.stringify({ ok: false, error: err && err.message ? err.message : "agent loop failed" })}\n`);
1632
- printPrompt();
1633
- });
1634
- });
1635
-
1636
- rl.on("close", () => {
1637
- closing = true;
1638
- if (autoBusTimer) {
1639
- clearInterval(autoBusTimer);
1640
- autoBusTimer = null;
1641
- }
1642
- chain.finally(() => resolve({ code: 0 }));
1643
- });
1644
- });
1645
- }
1646
-
1647
- function parseAgentArgs(argv = []) {
1648
- const args = Array.isArray(argv) ? argv.slice() : [];
1649
- const out = {
1650
- workspaceRoot: "",
1651
- provider: "",
1652
- model: "",
1653
- appendSystemPrompt: "",
1654
- systemPrompt: "",
1655
- sessionId: "",
1656
- timeoutMs: 600000,
1657
- jsonOutput: false,
1658
- forceTui: false,
1659
- disableTui: false,
1660
- };
1661
- for (let i = 0; i < args.length; i += 1) {
1662
- const item = String(args[i] || "").trim();
1663
- if (!item) continue;
1664
- if (item === "--workspace" || item === "--cwd") {
1665
- out.workspaceRoot = String(args[i + 1] || "").trim();
1666
- i += 1;
1667
- continue;
1668
- }
1669
- if (item === "--provider") {
1670
- out.provider = String(args[i + 1] || "").trim();
1671
- i += 1;
1672
- continue;
1673
- }
1674
- if (item === "--model") {
1675
- out.model = String(args[i + 1] || "").trim();
1676
- i += 1;
1677
- continue;
1678
- }
1679
- if (item === "--append-system-prompt") {
1680
- out.appendSystemPrompt = String(args[i + 1] || "").trim();
1681
- i += 1;
1682
- continue;
1683
- }
1684
- if (item === "--system-prompt") {
1685
- out.systemPrompt = String(args[i + 1] || "").trim();
1686
- i += 1;
1687
- continue;
1688
- }
1689
- if (item === "--session-id") {
1690
- out.sessionId = String(args[i + 1] || "").trim();
1691
- i += 1;
1692
- continue;
1693
- }
1694
- if (item === "--timeout-ms") {
1695
- const parsed = Number(args[i + 1]);
1696
- if (Number.isFinite(parsed)) out.timeoutMs = Math.max(1000, Math.floor(parsed));
1697
- i += 1;
1698
- continue;
1699
- }
1700
- if (item === "--json") {
1701
- out.jsonOutput = true;
1702
- continue;
1703
- }
1704
- if (item === "--tui") {
1705
- out.forceTui = true;
1706
- continue;
1707
- }
1708
- if (item === "--no-tui") {
1709
- out.disableTui = true;
1710
- continue;
1711
- }
1712
- }
1713
- return out;
1714
- }
1715
-
1716
705
  module.exports = {
1717
706
  runUcodeCoreAgent,
1718
707
  runSingleCommand,
@@ -1733,6 +722,8 @@ module.exports = {
1733
722
  parseBusCheckOutput,
1734
723
  extractBusMessageTask,
1735
724
  runUbusCommand,
725
+ runShellCapture,
726
+ readTextOrFile,
1736
727
  stripAnsi,
1737
728
  busCheckOutputIndicatesPending,
1738
729
  resolvePendingQueueFile,