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/bin/ucode.js +9 -0
- package/package.json +1 -1
- package/src/agents/launch/notifier.js +6 -0
- package/src/agents/launch/ptyRunner.js +2 -2
- package/src/agents/launch/ptyWrapper.js +2 -2
- package/src/agents/prompts/native/index.js +2 -2
- package/src/agents/prompts/native/toolDescriptions/bash.js +1 -1
- package/src/agents/prompts/native/toolDescriptions/edit.js +1 -0
- package/src/agents/prompts/native/toolDescriptions/read.js +3 -2
- package/src/code/agent.js +77 -1086
- package/src/code/busConsumer.js +504 -0
- package/src/code/dispatch.js +1 -6
- package/src/code/launcher/ucode.js +8 -254
- package/src/code/launcher/ucodeBootstrap.js +18 -1
- package/src/code/launcher/ucodeBuild.js +0 -3
- package/src/code/launcher/ucodeDoctor.js +26 -8
- package/src/code/launcher/ucodeRuntimeConfig.js +12 -3
- package/src/code/nativeRunner.js +93 -113
- package/src/code/repl.js +610 -0
- package/src/code/sessionStore.js +5 -1
- package/src/code/skills/injection.js +17 -1
- package/src/code/taskDecomposer.js +47 -31
- package/src/code/tools/bash.js +19 -2
- package/src/code/tools/common.js +34 -0
- package/src/code/tools/edit.js +11 -3
- package/src/code/tools/read.js +20 -2
- package/src/coordination/bus/inject.js +52 -7
- package/src/coordination/bus/subscriber.js +33 -6
- package/src/runtime/daemon/deliveryScheduler.js +102 -2
- package/src/runtime/daemon/index.js +8 -1
- package/src/runtime/daemon/ops.js +23 -0
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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 =
|
|
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) =>
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
-
|
|
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,
|