u-foo 2.5.6 → 2.5.8

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