u-foo 1.0.3 → 1.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -11
- package/README.zh-CN.md +9 -7
- package/SKILLS/ufoo/SKILL.md +132 -0
- package/SKILLS/uinit/SKILL.md +78 -0
- package/SKILLS/ustatus/SKILL.md +36 -0
- package/bin/uclaude.js +13 -0
- package/bin/ucode-core.js +15 -0
- package/bin/ucode.js +125 -0
- package/bin/ucodex.js +13 -0
- package/bin/ufoo +9 -31
- package/bin/ufoo-assistant-agent.js +5 -0
- package/bin/ufoo-engine.js +25 -0
- package/bin/ufoo.js +17 -0
- package/modules/AGENTS.template.md +29 -11
- package/modules/bus/README.md +33 -25
- package/modules/bus/SKILLS/ubus/SKILL.md +19 -8
- package/modules/context/README.md +18 -40
- package/modules/context/SKILLS/uctx/SKILL.md +63 -1
- package/modules/online/SKILLS/ufoo-online/SKILL.md +144 -0
- package/package.json +25 -4
- package/scripts/import-pi-mono.js +124 -0
- package/scripts/postinstall.js +30 -0
- package/scripts/sync-claude-skills.sh +21 -0
- package/src/agent/cliRunner.js +554 -33
- package/src/agent/internalRunner.js +150 -56
- package/src/agent/launcher.js +754 -0
- package/src/agent/normalizeOutput.js +1 -1
- package/src/agent/notifier.js +340 -0
- package/src/agent/ptyRunner.js +847 -0
- package/src/agent/ptyWrapper.js +379 -0
- package/src/agent/readyDetector.js +175 -0
- package/src/agent/ucode.js +443 -0
- package/src/agent/ucodeBootstrap.js +113 -0
- package/src/agent/ucodeBuild.js +67 -0
- package/src/agent/ucodeDoctor.js +184 -0
- package/src/agent/ucodeRuntimeConfig.js +129 -0
- package/src/agent/ufooAgent.js +46 -42
- package/src/assistant/agent.js +260 -0
- package/src/assistant/bridge.js +172 -0
- package/src/assistant/engine.js +252 -0
- package/src/assistant/stdio.js +58 -0
- package/src/assistant/ufooEngineCli.js +306 -0
- package/src/bus/activate.js +172 -0
- package/src/bus/daemon.js +436 -0
- package/src/bus/index.js +842 -0
- package/src/bus/inject.js +315 -0
- package/src/bus/message.js +430 -0
- package/src/bus/nickname.js +88 -0
- package/src/bus/queue.js +136 -0
- package/src/bus/shake.js +26 -0
- package/src/bus/store.js +189 -0
- package/src/bus/subscriber.js +312 -0
- package/src/bus/utils.js +363 -0
- package/src/chat/agentBar.js +117 -0
- package/src/chat/agentDirectory.js +88 -0
- package/src/chat/agentSockets.js +225 -0
- package/src/chat/agentViewController.js +298 -0
- package/src/chat/chatLogController.js +115 -0
- package/src/chat/commandExecutor.js +700 -0
- package/src/chat/commands.js +132 -0
- package/src/chat/completionController.js +414 -0
- package/src/chat/cronScheduler.js +160 -0
- package/src/chat/daemonConnection.js +166 -0
- package/src/chat/daemonCoordinator.js +64 -0
- package/src/chat/daemonMessageRouter.js +257 -0
- package/src/chat/daemonReconnect.js +41 -0
- package/src/chat/daemonTransport.js +36 -0
- package/src/chat/daemonTransportDefaults.js +10 -0
- package/src/chat/dashboardKeyController.js +480 -0
- package/src/chat/dashboardView.js +154 -0
- package/src/chat/index.js +1011 -1392
- package/src/chat/inputHistoryController.js +105 -0
- package/src/chat/inputListenerController.js +304 -0
- package/src/chat/inputMath.js +104 -0
- package/src/chat/inputSubmitHandler.js +171 -0
- package/src/chat/layout.js +165 -0
- package/src/chat/pasteController.js +81 -0
- package/src/chat/rawKeyMap.js +42 -0
- package/src/chat/settingsController.js +132 -0
- package/src/chat/statusLineController.js +177 -0
- package/src/chat/streamTracker.js +138 -0
- package/src/chat/text.js +70 -0
- package/src/chat/transport.js +61 -0
- package/src/cli/busCoreCommands.js +59 -0
- package/src/cli/ctxCoreCommands.js +199 -0
- package/src/cli/onlineCoreCommands.js +379 -0
- package/src/cli.js +1162 -96
- package/src/code/README.md +29 -0
- package/src/code/UCODE_PROMPT.md +32 -0
- package/src/code/agent.js +1651 -0
- package/src/code/cli.js +158 -0
- package/src/code/config +0 -0
- package/src/code/dispatch.js +42 -0
- package/src/code/index.js +70 -0
- package/src/code/nativeRunner.js +1213 -0
- package/src/code/runtime.js +154 -0
- package/src/code/sessionStore.js +162 -0
- package/src/code/taskDecomposer.js +269 -0
- package/src/code/tools/bash.js +53 -0
- package/src/code/tools/common.js +42 -0
- package/src/code/tools/edit.js +70 -0
- package/src/code/tools/read.js +44 -0
- package/src/code/tools/write.js +35 -0
- package/src/code/tui.js +1580 -0
- package/src/config.js +56 -3
- package/src/context/decisions.js +324 -0
- package/src/context/doctor.js +183 -0
- package/src/context/index.js +55 -0
- package/src/context/sync.js +127 -0
- package/src/daemon/agentProcessManager.js +74 -0
- package/src/daemon/cronOps.js +241 -0
- package/src/daemon/index.js +998 -170
- package/src/daemon/ipcServer.js +99 -0
- package/src/daemon/ops.js +630 -48
- package/src/daemon/promptLoop.js +319 -0
- package/src/daemon/promptRequest.js +101 -0
- package/src/daemon/providerSessions.js +306 -0
- package/src/daemon/reporting.js +90 -0
- package/src/daemon/run.js +31 -1
- package/src/daemon/status.js +48 -8
- package/src/doctor/index.js +50 -0
- package/src/init/index.js +318 -0
- package/src/online/bridge.js +663 -0
- package/src/online/client.js +245 -0
- package/src/online/runner.js +253 -0
- package/src/online/server.js +992 -0
- package/src/online/tokens.js +103 -0
- package/src/report/store.js +331 -0
- package/src/shared/eventContract.js +35 -0
- package/src/shared/ptySocketContract.js +21 -0
- package/src/skills/index.js +159 -0
- package/src/status/index.js +285 -0
- package/src/terminal/adapterContract.js +87 -0
- package/src/terminal/adapterRouter.js +84 -0
- package/src/terminal/adapters/externalAdapter.js +14 -0
- package/src/terminal/adapters/internalAdapter.js +13 -0
- package/src/terminal/adapters/internalPtyAdapter.js +42 -0
- package/src/terminal/adapters/internalQueueAdapter.js +37 -0
- package/src/terminal/adapters/terminalAdapter.js +31 -0
- package/src/terminal/adapters/tmuxAdapter.js +30 -0
- package/src/terminal/detect.js +64 -0
- package/src/terminal/index.js +8 -0
- package/src/terminal/iterm2.js +126 -0
- package/src/ufoo/agentsStore.js +107 -0
- package/src/ufoo/paths.js +46 -0
- package/src/utils/banner.js +76 -0
- package/bin/uclaude +0 -65
- package/bin/ucodex +0 -65
- package/modules/bus/scripts/bus-alert.sh +0 -185
- package/modules/bus/scripts/bus-listen.sh +0 -117
- package/modules/context/ASSUMPTIONS.md +0 -7
- package/modules/context/CONSTRAINTS.md +0 -7
- package/modules/context/CONTEXT-STRUCTURE.md +0 -49
- package/modules/context/DECISION-PROTOCOL.md +0 -62
- package/modules/context/HANDOFF.md +0 -33
- package/modules/context/RULES.md +0 -15
- package/modules/context/SKILLS/README.md +0 -14
- package/modules/context/SYSTEM.md +0 -18
- package/modules/context/TEMPLATES/assumptions.md +0 -4
- package/modules/context/TEMPLATES/constraints.md +0 -4
- package/modules/context/TEMPLATES/decision.md +0 -16
- package/modules/context/TEMPLATES/project-context-readme.md +0 -6
- package/modules/context/TEMPLATES/system.md +0 -3
- package/modules/context/TEMPLATES/terminology.md +0 -4
- package/modules/context/TERMINOLOGY.md +0 -10
- package/scripts/banner.sh +0 -89
- package/scripts/bus-alert.sh +0 -6
- package/scripts/bus-autotrigger.sh +0 -6
- package/scripts/bus-daemon.sh +0 -231
- package/scripts/bus-inject.sh +0 -144
- package/scripts/bus-listen.sh +0 -6
- package/scripts/bus.sh +0 -984
- package/scripts/context-decisions.sh +0 -167
- package/scripts/context-doctor.sh +0 -72
- package/scripts/context-lint.sh +0 -110
- package/scripts/doctor.sh +0 -22
- package/scripts/init.sh +0 -247
- package/scripts/skills.sh +0 -113
- package/scripts/status.sh +0 -125
package/src/agent/cliRunner.js
CHANGED
|
@@ -1,6 +1,79 @@
|
|
|
1
1
|
const { spawn } = require("child_process");
|
|
2
2
|
const { randomUUID } = require("crypto");
|
|
3
3
|
|
|
4
|
+
const ROUTER_JSON_SCHEMA = JSON.stringify({
|
|
5
|
+
type: "object",
|
|
6
|
+
properties: {
|
|
7
|
+
reply: { type: "string" },
|
|
8
|
+
assistant_call: {
|
|
9
|
+
type: "object",
|
|
10
|
+
properties: {
|
|
11
|
+
kind: { type: "string", enum: ["explore", "bash", "mixed"] },
|
|
12
|
+
task: { type: "string" },
|
|
13
|
+
context: { type: "string" },
|
|
14
|
+
expect: { type: "string" },
|
|
15
|
+
provider: { type: "string" },
|
|
16
|
+
model: { type: "string" },
|
|
17
|
+
timeout_ms: { type: "integer" },
|
|
18
|
+
},
|
|
19
|
+
required: ["task"],
|
|
20
|
+
},
|
|
21
|
+
dispatch: {
|
|
22
|
+
type: "array",
|
|
23
|
+
items: {
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {
|
|
26
|
+
target: { type: "string" },
|
|
27
|
+
message: { type: "string" },
|
|
28
|
+
},
|
|
29
|
+
required: ["target", "message"],
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
ops: {
|
|
33
|
+
type: "array",
|
|
34
|
+
items: {
|
|
35
|
+
type: "object",
|
|
36
|
+
properties: {
|
|
37
|
+
action: { type: "string", enum: ["launch", "close", "rename", "cron"] },
|
|
38
|
+
agent: { type: "string" },
|
|
39
|
+
count: { type: "integer" },
|
|
40
|
+
agent_id: { type: "string" },
|
|
41
|
+
nickname: { type: "string" },
|
|
42
|
+
operation: { type: "string", enum: ["start", "list", "stop", "add", "create", "ls", "rm", "remove"] },
|
|
43
|
+
every: { type: "string" },
|
|
44
|
+
interval_ms: { type: "integer" },
|
|
45
|
+
target: { type: "string" },
|
|
46
|
+
targets: {
|
|
47
|
+
type: "array",
|
|
48
|
+
items: { type: "string" },
|
|
49
|
+
},
|
|
50
|
+
prompt: { type: "string" },
|
|
51
|
+
id: { type: "string" },
|
|
52
|
+
},
|
|
53
|
+
required: ["action"],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
disambiguate: {
|
|
57
|
+
type: "object",
|
|
58
|
+
properties: {
|
|
59
|
+
prompt: { type: "string" },
|
|
60
|
+
candidates: {
|
|
61
|
+
type: "array",
|
|
62
|
+
items: {
|
|
63
|
+
type: "object",
|
|
64
|
+
properties: {
|
|
65
|
+
agent_id: { type: "string" },
|
|
66
|
+
reason: { type: "string" },
|
|
67
|
+
},
|
|
68
|
+
required: ["agent_id"],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
required: ["reply", "dispatch", "ops"],
|
|
75
|
+
});
|
|
76
|
+
|
|
4
77
|
function collectJsonl(text) {
|
|
5
78
|
const lines = text.split(/\r?\n/).filter((l) => l.trim());
|
|
6
79
|
const items = [];
|
|
@@ -22,20 +95,304 @@ function collectJson(text) {
|
|
|
22
95
|
}
|
|
23
96
|
}
|
|
24
97
|
|
|
98
|
+
function safeInvoke(callback, ...args) {
|
|
99
|
+
if (typeof callback !== "function") return;
|
|
100
|
+
try {
|
|
101
|
+
callback(...args);
|
|
102
|
+
} catch {
|
|
103
|
+
// Swallow stream callback errors to avoid breaking CLI execution.
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function normalizeDelta(value) {
|
|
108
|
+
if (typeof value === "string") return value;
|
|
109
|
+
return "";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const CORE_TOOL_NAMES = new Set(["read", "write", "edit", "bash"]);
|
|
113
|
+
|
|
114
|
+
function normalizeCoreToolName(value = "") {
|
|
115
|
+
const text = String(value || "").trim().toLowerCase();
|
|
116
|
+
if (!text) return "";
|
|
117
|
+
return CORE_TOOL_NAMES.has(text) ? text : "";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function parseMaybeJsonObject(value) {
|
|
121
|
+
if (value && typeof value === "object" && !Array.isArray(value)) return value;
|
|
122
|
+
const raw = typeof value === "string" ? value.trim() : "";
|
|
123
|
+
if (!raw) return {};
|
|
124
|
+
try {
|
|
125
|
+
const parsed = JSON.parse(raw);
|
|
126
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
|
|
127
|
+
} catch {
|
|
128
|
+
// ignore invalid json
|
|
129
|
+
}
|
|
130
|
+
return {};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function collectNestedObjects(root, maxDepth = 4) {
|
|
134
|
+
const out = [];
|
|
135
|
+
const seen = new Set();
|
|
136
|
+
|
|
137
|
+
function walk(node, depth) {
|
|
138
|
+
if (!node || typeof node !== "object" || depth > maxDepth) return;
|
|
139
|
+
if (seen.has(node)) return;
|
|
140
|
+
seen.add(node);
|
|
141
|
+
out.push(node);
|
|
142
|
+
if (Array.isArray(node)) {
|
|
143
|
+
for (const item of node) {
|
|
144
|
+
walk(item, depth + 1);
|
|
145
|
+
}
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
for (const value of Object.values(node)) {
|
|
149
|
+
if (value && typeof value === "object") {
|
|
150
|
+
walk(value, depth + 1);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
walk(root, 0);
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function inferToolPhase(event = {}, candidate = {}) {
|
|
160
|
+
const source = [
|
|
161
|
+
event.type,
|
|
162
|
+
event.event,
|
|
163
|
+
event.status,
|
|
164
|
+
candidate.type,
|
|
165
|
+
candidate.status,
|
|
166
|
+
]
|
|
167
|
+
.map((part) => String(part || "").toLowerCase())
|
|
168
|
+
.join(" ");
|
|
169
|
+
|
|
170
|
+
if (!source) return "update";
|
|
171
|
+
if (/error|failed|failure|cancelled|canceled|abort/.test(source)) return "error";
|
|
172
|
+
if (/done|completed|finished|result|end|succeeded/.test(source)) return "end";
|
|
173
|
+
if (/start|started|begin|call|invoke|created|added|delta|progress/.test(source)) return "start";
|
|
174
|
+
return "update";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function buildToolArgs(tool = "", candidate = {}) {
|
|
178
|
+
const rawArgs = candidate.args
|
|
179
|
+
|| candidate.arguments
|
|
180
|
+
|| candidate.input
|
|
181
|
+
|| candidate.params
|
|
182
|
+
|| candidate.payload
|
|
183
|
+
|| {};
|
|
184
|
+
const parsed = parseMaybeJsonObject(rawArgs);
|
|
185
|
+
if (Object.keys(parsed).length > 0) return parsed;
|
|
186
|
+
|
|
187
|
+
// Common direct fields seen in tool events.
|
|
188
|
+
if (tool === "bash") {
|
|
189
|
+
const command = String(candidate.command || candidate.cmd || "").trim();
|
|
190
|
+
return command ? { command } : {};
|
|
191
|
+
}
|
|
192
|
+
if (tool === "read" || tool === "write" || tool === "edit") {
|
|
193
|
+
const filePath = String(candidate.path || candidate.file || "").trim();
|
|
194
|
+
if (filePath) return { path: filePath };
|
|
195
|
+
}
|
|
196
|
+
return {};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function buildToolEventKey(event = {}, candidate = {}, tool = "", phase = "", args = {}) {
|
|
200
|
+
const id = String(
|
|
201
|
+
event.id
|
|
202
|
+
|| event.item_id
|
|
203
|
+
|| candidate.id
|
|
204
|
+
|| candidate.call_id
|
|
205
|
+
|| candidate.tool_call_id
|
|
206
|
+
|| ""
|
|
207
|
+
).trim();
|
|
208
|
+
if (id) return `${tool}|${phase}|${id}`;
|
|
209
|
+
|
|
210
|
+
const details = JSON.stringify({
|
|
211
|
+
path: args.path || args.file || "",
|
|
212
|
+
command: args.command || args.cmd || "",
|
|
213
|
+
});
|
|
214
|
+
return `${tool}|${phase}|${details}`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function extractCodexToolEvent(event = {}, state = null) {
|
|
218
|
+
const objects = collectNestedObjects(event, 4);
|
|
219
|
+
for (const candidate of objects) {
|
|
220
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) continue;
|
|
221
|
+
const tool = normalizeCoreToolName(
|
|
222
|
+
candidate.tool
|
|
223
|
+
|| candidate.tool_name
|
|
224
|
+
|| candidate.name
|
|
225
|
+
|| candidate.function_name
|
|
226
|
+
|| candidate.action
|
|
227
|
+
|| candidate.type
|
|
228
|
+
);
|
|
229
|
+
if (!tool) continue;
|
|
230
|
+
|
|
231
|
+
const args = buildToolArgs(tool, candidate);
|
|
232
|
+
const phase = inferToolPhase(event, candidate);
|
|
233
|
+
const error = String(candidate.error || candidate.message || "").trim();
|
|
234
|
+
const key = buildToolEventKey(event, candidate, tool, phase, args);
|
|
235
|
+
if (state && state.seenToolEventKeys instanceof Set) {
|
|
236
|
+
if (state.seenToolEventKeys.has(key)) continue;
|
|
237
|
+
state.seenToolEventKeys.add(key);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
tool,
|
|
242
|
+
phase,
|
|
243
|
+
args,
|
|
244
|
+
error,
|
|
245
|
+
rawType: String(event.type || ""),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function extractTextFromContentBlock(block) {
|
|
252
|
+
if (!block || typeof block !== "object") return "";
|
|
253
|
+
if (typeof block.text === "string") return block.text;
|
|
254
|
+
if (typeof block.content === "string") return block.content;
|
|
255
|
+
if (typeof block.output_text === "string") return block.output_text;
|
|
256
|
+
if (typeof block.delta === "string") return block.delta;
|
|
257
|
+
return "";
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function extractTextFromCodexItem(item) {
|
|
261
|
+
if (!item || typeof item !== "object") return "";
|
|
262
|
+
if (typeof item.text === "string") return item.text;
|
|
263
|
+
if (typeof item.delta === "string") return item.delta;
|
|
264
|
+
if (typeof item.output_text === "string") return item.output_text;
|
|
265
|
+
if (Array.isArray(item.content)) {
|
|
266
|
+
const text = item.content
|
|
267
|
+
.map((part) => extractTextFromContentBlock(part))
|
|
268
|
+
.filter(Boolean)
|
|
269
|
+
.join("");
|
|
270
|
+
if (text) return text;
|
|
271
|
+
}
|
|
272
|
+
if (item.item && typeof item.item === "object") {
|
|
273
|
+
return extractTextFromCodexItem(item.item);
|
|
274
|
+
}
|
|
275
|
+
return "";
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function extractCodexStreamDelta(event) {
|
|
279
|
+
if (!event || typeof event !== "object") return "";
|
|
280
|
+
|
|
281
|
+
if (
|
|
282
|
+
event.assistantMessageEvent
|
|
283
|
+
&& typeof event.assistantMessageEvent === "object"
|
|
284
|
+
&& typeof event.assistantMessageEvent.delta === "string"
|
|
285
|
+
) {
|
|
286
|
+
return event.assistantMessageEvent.delta;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (typeof event.delta === "string") return event.delta;
|
|
290
|
+
if (typeof event.output_text === "string") return event.output_text;
|
|
291
|
+
if (event.item && typeof event.item === "object") {
|
|
292
|
+
return extractTextFromCodexItem(event.item);
|
|
293
|
+
}
|
|
294
|
+
if (event.message && typeof event.message === "object") {
|
|
295
|
+
return extractTextFromCodexItem(event.message);
|
|
296
|
+
}
|
|
297
|
+
return "";
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function createCodexJsonlStreamParser(onDeltaOrOptions, maybeOnToolEvent) {
|
|
301
|
+
let onDelta = null;
|
|
302
|
+
let onToolEvent = null;
|
|
303
|
+
if (typeof onDeltaOrOptions === "function") {
|
|
304
|
+
onDelta = onDeltaOrOptions;
|
|
305
|
+
onToolEvent = typeof maybeOnToolEvent === "function" ? maybeOnToolEvent : null;
|
|
306
|
+
} else if (onDeltaOrOptions && typeof onDeltaOrOptions === "object") {
|
|
307
|
+
onDelta = typeof onDeltaOrOptions.onDelta === "function" ? onDeltaOrOptions.onDelta : null;
|
|
308
|
+
onToolEvent = typeof onDeltaOrOptions.onToolEvent === "function"
|
|
309
|
+
? onDeltaOrOptions.onToolEvent
|
|
310
|
+
: null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
let buffer = "";
|
|
314
|
+
const toolState = { seenToolEventKeys: new Set() };
|
|
315
|
+
|
|
316
|
+
function parseLine(line) {
|
|
317
|
+
const trimmed = String(line || "").trim();
|
|
318
|
+
if (!trimmed) return;
|
|
319
|
+
let parsed;
|
|
320
|
+
try {
|
|
321
|
+
parsed = JSON.parse(trimmed);
|
|
322
|
+
} catch {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const delta = normalizeDelta(extractCodexStreamDelta(parsed));
|
|
326
|
+
if (delta) {
|
|
327
|
+
safeInvoke(onDelta, delta, parsed);
|
|
328
|
+
}
|
|
329
|
+
const toolEvent = extractCodexToolEvent(parsed, toolState);
|
|
330
|
+
if (toolEvent) {
|
|
331
|
+
safeInvoke(onToolEvent, toolEvent, parsed);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
onChunk(chunk) {
|
|
337
|
+
const text = String(chunk || "");
|
|
338
|
+
if (!text) return;
|
|
339
|
+
buffer += text;
|
|
340
|
+
const lines = buffer.split(/\r?\n/);
|
|
341
|
+
buffer = lines.pop() || "";
|
|
342
|
+
for (const line of lines) {
|
|
343
|
+
parseLine(line);
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
flush() {
|
|
347
|
+
if (!buffer) return;
|
|
348
|
+
parseLine(buffer);
|
|
349
|
+
buffer = "";
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
25
354
|
function runCommand(command, args, options = {}) {
|
|
26
355
|
return new Promise((resolve, reject) => {
|
|
27
356
|
const child = spawn(command, args, {
|
|
28
357
|
stdio: ["pipe", "pipe", "pipe"],
|
|
29
358
|
...options,
|
|
30
359
|
});
|
|
360
|
+
let settled = false;
|
|
361
|
+
|
|
362
|
+
const settleReject = (err) => {
|
|
363
|
+
if (settled) return;
|
|
364
|
+
settled = true;
|
|
365
|
+
reject(err);
|
|
366
|
+
};
|
|
367
|
+
const settleResolve = (value) => {
|
|
368
|
+
if (settled) return;
|
|
369
|
+
settled = true;
|
|
370
|
+
resolve(value);
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
if (typeof options.onSpawn === "function") {
|
|
374
|
+
try {
|
|
375
|
+
options.onSpawn(child);
|
|
376
|
+
} catch {
|
|
377
|
+
// ignore callback failures
|
|
378
|
+
}
|
|
379
|
+
}
|
|
31
380
|
|
|
32
381
|
let stdout = "";
|
|
33
382
|
let stderr = "";
|
|
34
383
|
child.stdout.on("data", (d) => {
|
|
35
|
-
|
|
384
|
+
const chunk = d.toString("utf8");
|
|
385
|
+
stdout += chunk;
|
|
386
|
+
if (options.onStdout) {
|
|
387
|
+
options.onStdout(chunk);
|
|
388
|
+
}
|
|
36
389
|
});
|
|
37
390
|
child.stderr.on("data", (d) => {
|
|
38
|
-
|
|
391
|
+
const chunk = d.toString("utf8");
|
|
392
|
+
stderr += chunk;
|
|
393
|
+
if (options.onStderr) {
|
|
394
|
+
options.onStderr(chunk);
|
|
395
|
+
}
|
|
39
396
|
});
|
|
40
397
|
let timeout = null;
|
|
41
398
|
if (options.timeoutMs) {
|
|
@@ -45,17 +402,40 @@ function runCommand(command, args, options = {}) {
|
|
|
45
402
|
} catch {
|
|
46
403
|
// ignore
|
|
47
404
|
}
|
|
48
|
-
|
|
405
|
+
settleReject(new Error(`CLI timeout (${options.timeoutMs}ms)`));
|
|
49
406
|
}, options.timeoutMs);
|
|
50
407
|
}
|
|
51
408
|
|
|
409
|
+
let abortHandler = null;
|
|
410
|
+
if (options.signal && typeof options.signal.addEventListener === "function") {
|
|
411
|
+
abortHandler = () => {
|
|
412
|
+
try {
|
|
413
|
+
child.kill("SIGTERM");
|
|
414
|
+
} catch {
|
|
415
|
+
// ignore
|
|
416
|
+
}
|
|
417
|
+
settleReject(new Error("CLI cancelled"));
|
|
418
|
+
};
|
|
419
|
+
if (options.signal.aborted) {
|
|
420
|
+
abortHandler();
|
|
421
|
+
} else {
|
|
422
|
+
options.signal.addEventListener("abort", abortHandler, { once: true });
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
52
426
|
child.on("error", (err) => {
|
|
53
427
|
if (timeout) clearTimeout(timeout);
|
|
54
|
-
|
|
428
|
+
if (abortHandler && options.signal && typeof options.signal.removeEventListener === "function") {
|
|
429
|
+
options.signal.removeEventListener("abort", abortHandler);
|
|
430
|
+
}
|
|
431
|
+
settleReject(err);
|
|
55
432
|
});
|
|
56
433
|
child.on("close", (code) => {
|
|
57
434
|
if (timeout) clearTimeout(timeout);
|
|
58
|
-
|
|
435
|
+
if (abortHandler && options.signal && typeof options.signal.removeEventListener === "function") {
|
|
436
|
+
options.signal.removeEventListener("abort", abortHandler);
|
|
437
|
+
}
|
|
438
|
+
settleResolve({ code, stdout, stderr });
|
|
59
439
|
});
|
|
60
440
|
|
|
61
441
|
if (options.input) {
|
|
@@ -74,7 +454,15 @@ const DEFAULT_CLAUDE = {
|
|
|
74
454
|
"--dangerously-skip-permissions",
|
|
75
455
|
"--no-session-persistence",
|
|
76
456
|
"--json-schema",
|
|
77
|
-
|
|
457
|
+
ROUTER_JSON_SCHEMA,
|
|
458
|
+
],
|
|
459
|
+
fallbackArgs: [
|
|
460
|
+
"-p",
|
|
461
|
+
"--output-format",
|
|
462
|
+
"json",
|
|
463
|
+
"--dangerously-skip-permissions",
|
|
464
|
+
"--json-schema",
|
|
465
|
+
ROUTER_JSON_SCHEMA,
|
|
78
466
|
],
|
|
79
467
|
output: "json",
|
|
80
468
|
input: "arg",
|
|
@@ -111,6 +499,35 @@ function buildArgs(backend, prompt, opts) {
|
|
|
111
499
|
return { args, stdin: prompt };
|
|
112
500
|
}
|
|
113
501
|
|
|
502
|
+
function applySandboxOverride(args, sandbox) {
|
|
503
|
+
if (!sandbox) return;
|
|
504
|
+
const idx = args.indexOf("--sandbox");
|
|
505
|
+
if (idx >= 0) {
|
|
506
|
+
if (idx + 1 < args.length) {
|
|
507
|
+
args[idx + 1] = sandbox;
|
|
508
|
+
} else {
|
|
509
|
+
args.push(sandbox);
|
|
510
|
+
}
|
|
511
|
+
} else {
|
|
512
|
+
args.push("--sandbox", sandbox);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function applyClaudeJsonSchema(args, jsonSchema) {
|
|
517
|
+
if (!jsonSchema) return;
|
|
518
|
+
const schema = typeof jsonSchema === "string" ? jsonSchema : JSON.stringify(jsonSchema);
|
|
519
|
+
const idx = args.indexOf("--json-schema");
|
|
520
|
+
if (idx >= 0) {
|
|
521
|
+
if (idx + 1 < args.length) {
|
|
522
|
+
args[idx + 1] = schema;
|
|
523
|
+
} else {
|
|
524
|
+
args.push(schema);
|
|
525
|
+
}
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
args.push("--json-schema", schema);
|
|
529
|
+
}
|
|
530
|
+
|
|
114
531
|
function isUnsupportedArgError(errText) {
|
|
115
532
|
const text = (errText || "").toLowerCase();
|
|
116
533
|
return text.includes("unknown option")
|
|
@@ -119,9 +536,48 @@ function isUnsupportedArgError(errText) {
|
|
|
119
536
|
|| text.includes("unrecognized option");
|
|
120
537
|
}
|
|
121
538
|
|
|
539
|
+
function extractUnsupportedOption(errText) {
|
|
540
|
+
const text = String(errText || "");
|
|
541
|
+
const quoted = text.match(/['"`](--[a-z0-9-]+)['"`]/i);
|
|
542
|
+
if (quoted && quoted[1]) return quoted[1];
|
|
543
|
+
const plain = text.match(/(--[a-z0-9-]+)/i);
|
|
544
|
+
return plain && plain[1] ? plain[1] : "";
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function removeUnsupportedOption(args, option) {
|
|
548
|
+
const out = Array.isArray(args) ? args.slice() : [];
|
|
549
|
+
const target = String(option || "").trim();
|
|
550
|
+
if (!target) return { changed: false, args: out };
|
|
551
|
+
const idx = out.indexOf(target);
|
|
552
|
+
if (idx < 0) return { changed: false, args: out };
|
|
553
|
+
|
|
554
|
+
const optionsWithValue = new Set([
|
|
555
|
+
"--json-schema",
|
|
556
|
+
"--model",
|
|
557
|
+
"--session-id",
|
|
558
|
+
"--append-system-prompt",
|
|
559
|
+
"--output-format",
|
|
560
|
+
"--sandbox",
|
|
561
|
+
]);
|
|
562
|
+
const takesValue = optionsWithValue.has(target);
|
|
563
|
+
out.splice(idx, takesValue ? 2 : 1);
|
|
564
|
+
return { changed: true, args: out };
|
|
565
|
+
}
|
|
566
|
+
|
|
122
567
|
async function runCliAgent(params) {
|
|
123
568
|
const backend = params.provider === "codex-cli" ? DEFAULT_CODEX : DEFAULT_CLAUDE;
|
|
124
569
|
const sessionId = params.sessionId || randomUUID();
|
|
570
|
+
const streamState = { emitted: false };
|
|
571
|
+
const emitStreamDelta = (delta, meta = null) => {
|
|
572
|
+
const text = normalizeDelta(delta);
|
|
573
|
+
if (!text) return;
|
|
574
|
+
streamState.emitted = true;
|
|
575
|
+
safeInvoke(params.onStreamDelta, text, meta);
|
|
576
|
+
};
|
|
577
|
+
const emitToolEvent = (event, meta = null) => {
|
|
578
|
+
if (!event || typeof event !== "object") return;
|
|
579
|
+
safeInvoke(params.onToolEvent, event, meta);
|
|
580
|
+
};
|
|
125
581
|
const prompt =
|
|
126
582
|
params.systemPrompt && !backend.systemPromptArg
|
|
127
583
|
? `${params.systemPrompt}\n\n${params.prompt}`
|
|
@@ -132,59 +588,124 @@ async function runCliAgent(params) {
|
|
|
132
588
|
systemPrompt: params.systemPrompt,
|
|
133
589
|
disableSession: params.disableSession,
|
|
134
590
|
});
|
|
591
|
+
if (backend === DEFAULT_CODEX && params.sandbox) {
|
|
592
|
+
applySandboxOverride(args, params.sandbox);
|
|
593
|
+
}
|
|
594
|
+
if (backend === DEFAULT_CLAUDE && params.jsonSchema) {
|
|
595
|
+
applyClaudeJsonSchema(args, params.jsonSchema);
|
|
596
|
+
}
|
|
135
597
|
|
|
136
598
|
let res;
|
|
137
599
|
const env = { ...process.env, ...(params.env || {}) };
|
|
138
|
-
|
|
139
|
-
delete env.
|
|
600
|
+
// Clean up ufoo-specific env vars to avoid interference with CLI agents
|
|
601
|
+
delete env.UFOO_SUBSCRIBER_ID;
|
|
602
|
+
let codexParser = null;
|
|
603
|
+
if (
|
|
604
|
+
backend === DEFAULT_CODEX
|
|
605
|
+
&& (typeof params.onStreamDelta === "function" || typeof params.onToolEvent === "function")
|
|
606
|
+
) {
|
|
607
|
+
codexParser = createCodexJsonlStreamParser({
|
|
608
|
+
onDelta: (delta, event) =>
|
|
609
|
+
emitStreamDelta(delta, { backend: "codex", event }),
|
|
610
|
+
onToolEvent: (event, rawEvent) =>
|
|
611
|
+
emitToolEvent(event, { backend: "codex", event: rawEvent }),
|
|
612
|
+
});
|
|
613
|
+
}
|
|
140
614
|
try {
|
|
141
615
|
res = await runCommand(backend.command, args, {
|
|
142
616
|
cwd: params.cwd,
|
|
143
617
|
env,
|
|
144
618
|
input: stdin,
|
|
145
619
|
timeoutMs: params.timeoutMs || 300000, // 5 minutes for complex tasks
|
|
620
|
+
onStdout: codexParser ? (chunk) => codexParser.onChunk(chunk) : null,
|
|
621
|
+
signal: params.signal,
|
|
146
622
|
});
|
|
623
|
+
if (codexParser) codexParser.flush();
|
|
147
624
|
} catch (err) {
|
|
148
|
-
return { ok: false, error: err.message || String(err), sessionId };
|
|
625
|
+
return { ok: false, error: err.message || String(err), sessionId, streamed: streamState.emitted };
|
|
149
626
|
}
|
|
150
627
|
|
|
151
628
|
if (res.code !== 0) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
629
|
+
let lastErr = res.stderr || res.stdout || "CLI failed";
|
|
630
|
+
let retryArgs = args.slice();
|
|
631
|
+
let retryStdin = stdin;
|
|
632
|
+
let usedFallbackPreset = false;
|
|
633
|
+
|
|
634
|
+
for (let attempt = 0; attempt < 3 && isUnsupportedArgError(lastErr); attempt += 1) {
|
|
635
|
+
if (!usedFallbackPreset && backend.fallbackArgs) {
|
|
636
|
+
const retry = buildArgs(
|
|
637
|
+
{ ...backend, args: backend.fallbackArgs },
|
|
638
|
+
prompt,
|
|
639
|
+
{
|
|
640
|
+
model: params.model,
|
|
641
|
+
sessionId,
|
|
642
|
+
systemPrompt: params.systemPrompt,
|
|
643
|
+
disableSession: params.disableSession,
|
|
644
|
+
},
|
|
645
|
+
);
|
|
646
|
+
retryArgs = retry.args;
|
|
647
|
+
retryStdin = retry.stdin;
|
|
648
|
+
if (params.sandbox) {
|
|
649
|
+
applySandboxOverride(retryArgs, params.sandbox);
|
|
650
|
+
}
|
|
651
|
+
if (backend === DEFAULT_CLAUDE && params.jsonSchema) {
|
|
652
|
+
applyClaudeJsonSchema(retryArgs, params.jsonSchema);
|
|
653
|
+
}
|
|
654
|
+
usedFallbackPreset = true;
|
|
655
|
+
} else {
|
|
656
|
+
const unsupportedOption = extractUnsupportedOption(lastErr);
|
|
657
|
+
const dropped = removeUnsupportedOption(retryArgs, unsupportedOption);
|
|
658
|
+
if (!dropped.changed) {
|
|
659
|
+
break;
|
|
660
|
+
}
|
|
661
|
+
retryArgs = dropped.args;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
let retryParser = null;
|
|
665
|
+
if (
|
|
666
|
+
backend === DEFAULT_CODEX
|
|
667
|
+
&& (typeof params.onStreamDelta === "function" || typeof params.onToolEvent === "function")
|
|
668
|
+
) {
|
|
669
|
+
retryParser = createCodexJsonlStreamParser({
|
|
670
|
+
onDelta: (delta, event) =>
|
|
671
|
+
emitStreamDelta(delta, { backend: "codex", event }),
|
|
672
|
+
onToolEvent: (event, rawEvent) =>
|
|
673
|
+
emitToolEvent(event, { backend: "codex", event: rawEvent }),
|
|
674
|
+
});
|
|
675
|
+
}
|
|
164
676
|
try {
|
|
165
|
-
res = await runCommand(backend.command,
|
|
677
|
+
res = await runCommand(backend.command, retryArgs, {
|
|
166
678
|
cwd: params.cwd,
|
|
167
679
|
env,
|
|
168
|
-
input:
|
|
680
|
+
input: retryStdin,
|
|
169
681
|
timeoutMs: params.timeoutMs || 60000,
|
|
682
|
+
onStdout: retryParser ? (chunk) => retryParser.onChunk(chunk) : null,
|
|
683
|
+
signal: params.signal,
|
|
170
684
|
});
|
|
685
|
+
if (retryParser) retryParser.flush();
|
|
171
686
|
} catch (err2) {
|
|
172
|
-
return { ok: false, error: err2.message || String(err2), sessionId };
|
|
687
|
+
return { ok: false, error: err2.message || String(err2), sessionId, streamed: streamState.emitted };
|
|
173
688
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
689
|
+
|
|
690
|
+
if (res.code === 0) break;
|
|
691
|
+
lastErr = res.stderr || res.stdout || "CLI failed";
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (res.code !== 0) {
|
|
695
|
+
return { ok: false, error: lastErr, sessionId, streamed: streamState.emitted };
|
|
180
696
|
}
|
|
181
697
|
}
|
|
182
698
|
|
|
183
699
|
if (backend.output === "jsonl") {
|
|
184
|
-
return { ok: true, sessionId, output: collectJsonl(res.stdout) };
|
|
700
|
+
return { ok: true, sessionId, output: collectJsonl(res.stdout), streamed: streamState.emitted };
|
|
185
701
|
}
|
|
186
702
|
|
|
187
|
-
return { ok: true, sessionId, output: collectJson(res.stdout) };
|
|
703
|
+
return { ok: true, sessionId, output: collectJson(res.stdout), streamed: streamState.emitted };
|
|
188
704
|
}
|
|
189
705
|
|
|
190
|
-
module.exports = {
|
|
706
|
+
module.exports = {
|
|
707
|
+
runCliAgent,
|
|
708
|
+
extractCodexStreamDelta,
|
|
709
|
+
extractCodexToolEvent,
|
|
710
|
+
createCodexJsonlStreamParser,
|
|
711
|
+
};
|