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.
Files changed (179) hide show
  1. package/README.md +110 -11
  2. package/README.zh-CN.md +9 -7
  3. package/SKILLS/ufoo/SKILL.md +132 -0
  4. package/SKILLS/uinit/SKILL.md +78 -0
  5. package/SKILLS/ustatus/SKILL.md +36 -0
  6. package/bin/uclaude.js +13 -0
  7. package/bin/ucode-core.js +15 -0
  8. package/bin/ucode.js +125 -0
  9. package/bin/ucodex.js +13 -0
  10. package/bin/ufoo +9 -31
  11. package/bin/ufoo-assistant-agent.js +5 -0
  12. package/bin/ufoo-engine.js +25 -0
  13. package/bin/ufoo.js +17 -0
  14. package/modules/AGENTS.template.md +29 -11
  15. package/modules/bus/README.md +33 -25
  16. package/modules/bus/SKILLS/ubus/SKILL.md +19 -8
  17. package/modules/context/README.md +18 -40
  18. package/modules/context/SKILLS/uctx/SKILL.md +63 -1
  19. package/modules/online/SKILLS/ufoo-online/SKILL.md +144 -0
  20. package/package.json +25 -4
  21. package/scripts/import-pi-mono.js +124 -0
  22. package/scripts/postinstall.js +30 -0
  23. package/scripts/sync-claude-skills.sh +21 -0
  24. package/src/agent/cliRunner.js +554 -33
  25. package/src/agent/internalRunner.js +150 -56
  26. package/src/agent/launcher.js +754 -0
  27. package/src/agent/normalizeOutput.js +1 -1
  28. package/src/agent/notifier.js +340 -0
  29. package/src/agent/ptyRunner.js +847 -0
  30. package/src/agent/ptyWrapper.js +379 -0
  31. package/src/agent/readyDetector.js +175 -0
  32. package/src/agent/ucode.js +443 -0
  33. package/src/agent/ucodeBootstrap.js +113 -0
  34. package/src/agent/ucodeBuild.js +67 -0
  35. package/src/agent/ucodeDoctor.js +184 -0
  36. package/src/agent/ucodeRuntimeConfig.js +129 -0
  37. package/src/agent/ufooAgent.js +46 -42
  38. package/src/assistant/agent.js +260 -0
  39. package/src/assistant/bridge.js +172 -0
  40. package/src/assistant/engine.js +252 -0
  41. package/src/assistant/stdio.js +58 -0
  42. package/src/assistant/ufooEngineCli.js +306 -0
  43. package/src/bus/activate.js +172 -0
  44. package/src/bus/daemon.js +436 -0
  45. package/src/bus/index.js +842 -0
  46. package/src/bus/inject.js +315 -0
  47. package/src/bus/message.js +430 -0
  48. package/src/bus/nickname.js +88 -0
  49. package/src/bus/queue.js +136 -0
  50. package/src/bus/shake.js +26 -0
  51. package/src/bus/store.js +189 -0
  52. package/src/bus/subscriber.js +312 -0
  53. package/src/bus/utils.js +363 -0
  54. package/src/chat/agentBar.js +117 -0
  55. package/src/chat/agentDirectory.js +88 -0
  56. package/src/chat/agentSockets.js +225 -0
  57. package/src/chat/agentViewController.js +298 -0
  58. package/src/chat/chatLogController.js +115 -0
  59. package/src/chat/commandExecutor.js +700 -0
  60. package/src/chat/commands.js +132 -0
  61. package/src/chat/completionController.js +414 -0
  62. package/src/chat/cronScheduler.js +160 -0
  63. package/src/chat/daemonConnection.js +166 -0
  64. package/src/chat/daemonCoordinator.js +64 -0
  65. package/src/chat/daemonMessageRouter.js +257 -0
  66. package/src/chat/daemonReconnect.js +41 -0
  67. package/src/chat/daemonTransport.js +36 -0
  68. package/src/chat/daemonTransportDefaults.js +10 -0
  69. package/src/chat/dashboardKeyController.js +480 -0
  70. package/src/chat/dashboardView.js +154 -0
  71. package/src/chat/index.js +1011 -1392
  72. package/src/chat/inputHistoryController.js +105 -0
  73. package/src/chat/inputListenerController.js +304 -0
  74. package/src/chat/inputMath.js +104 -0
  75. package/src/chat/inputSubmitHandler.js +171 -0
  76. package/src/chat/layout.js +165 -0
  77. package/src/chat/pasteController.js +81 -0
  78. package/src/chat/rawKeyMap.js +42 -0
  79. package/src/chat/settingsController.js +132 -0
  80. package/src/chat/statusLineController.js +177 -0
  81. package/src/chat/streamTracker.js +138 -0
  82. package/src/chat/text.js +70 -0
  83. package/src/chat/transport.js +61 -0
  84. package/src/cli/busCoreCommands.js +59 -0
  85. package/src/cli/ctxCoreCommands.js +199 -0
  86. package/src/cli/onlineCoreCommands.js +379 -0
  87. package/src/cli.js +1162 -96
  88. package/src/code/README.md +29 -0
  89. package/src/code/UCODE_PROMPT.md +32 -0
  90. package/src/code/agent.js +1651 -0
  91. package/src/code/cli.js +158 -0
  92. package/src/code/config +0 -0
  93. package/src/code/dispatch.js +42 -0
  94. package/src/code/index.js +70 -0
  95. package/src/code/nativeRunner.js +1213 -0
  96. package/src/code/runtime.js +154 -0
  97. package/src/code/sessionStore.js +162 -0
  98. package/src/code/taskDecomposer.js +269 -0
  99. package/src/code/tools/bash.js +53 -0
  100. package/src/code/tools/common.js +42 -0
  101. package/src/code/tools/edit.js +70 -0
  102. package/src/code/tools/read.js +44 -0
  103. package/src/code/tools/write.js +35 -0
  104. package/src/code/tui.js +1580 -0
  105. package/src/config.js +56 -3
  106. package/src/context/decisions.js +324 -0
  107. package/src/context/doctor.js +183 -0
  108. package/src/context/index.js +55 -0
  109. package/src/context/sync.js +127 -0
  110. package/src/daemon/agentProcessManager.js +74 -0
  111. package/src/daemon/cronOps.js +241 -0
  112. package/src/daemon/index.js +998 -170
  113. package/src/daemon/ipcServer.js +99 -0
  114. package/src/daemon/ops.js +630 -48
  115. package/src/daemon/promptLoop.js +319 -0
  116. package/src/daemon/promptRequest.js +101 -0
  117. package/src/daemon/providerSessions.js +306 -0
  118. package/src/daemon/reporting.js +90 -0
  119. package/src/daemon/run.js +31 -1
  120. package/src/daemon/status.js +48 -8
  121. package/src/doctor/index.js +50 -0
  122. package/src/init/index.js +318 -0
  123. package/src/online/bridge.js +663 -0
  124. package/src/online/client.js +245 -0
  125. package/src/online/runner.js +253 -0
  126. package/src/online/server.js +992 -0
  127. package/src/online/tokens.js +103 -0
  128. package/src/report/store.js +331 -0
  129. package/src/shared/eventContract.js +35 -0
  130. package/src/shared/ptySocketContract.js +21 -0
  131. package/src/skills/index.js +159 -0
  132. package/src/status/index.js +285 -0
  133. package/src/terminal/adapterContract.js +87 -0
  134. package/src/terminal/adapterRouter.js +84 -0
  135. package/src/terminal/adapters/externalAdapter.js +14 -0
  136. package/src/terminal/adapters/internalAdapter.js +13 -0
  137. package/src/terminal/adapters/internalPtyAdapter.js +42 -0
  138. package/src/terminal/adapters/internalQueueAdapter.js +37 -0
  139. package/src/terminal/adapters/terminalAdapter.js +31 -0
  140. package/src/terminal/adapters/tmuxAdapter.js +30 -0
  141. package/src/terminal/detect.js +64 -0
  142. package/src/terminal/index.js +8 -0
  143. package/src/terminal/iterm2.js +126 -0
  144. package/src/ufoo/agentsStore.js +107 -0
  145. package/src/ufoo/paths.js +46 -0
  146. package/src/utils/banner.js +76 -0
  147. package/bin/uclaude +0 -65
  148. package/bin/ucodex +0 -65
  149. package/modules/bus/scripts/bus-alert.sh +0 -185
  150. package/modules/bus/scripts/bus-listen.sh +0 -117
  151. package/modules/context/ASSUMPTIONS.md +0 -7
  152. package/modules/context/CONSTRAINTS.md +0 -7
  153. package/modules/context/CONTEXT-STRUCTURE.md +0 -49
  154. package/modules/context/DECISION-PROTOCOL.md +0 -62
  155. package/modules/context/HANDOFF.md +0 -33
  156. package/modules/context/RULES.md +0 -15
  157. package/modules/context/SKILLS/README.md +0 -14
  158. package/modules/context/SYSTEM.md +0 -18
  159. package/modules/context/TEMPLATES/assumptions.md +0 -4
  160. package/modules/context/TEMPLATES/constraints.md +0 -4
  161. package/modules/context/TEMPLATES/decision.md +0 -16
  162. package/modules/context/TEMPLATES/project-context-readme.md +0 -6
  163. package/modules/context/TEMPLATES/system.md +0 -3
  164. package/modules/context/TEMPLATES/terminology.md +0 -4
  165. package/modules/context/TERMINOLOGY.md +0 -10
  166. package/scripts/banner.sh +0 -89
  167. package/scripts/bus-alert.sh +0 -6
  168. package/scripts/bus-autotrigger.sh +0 -6
  169. package/scripts/bus-daemon.sh +0 -231
  170. package/scripts/bus-inject.sh +0 -144
  171. package/scripts/bus-listen.sh +0 -6
  172. package/scripts/bus.sh +0 -984
  173. package/scripts/context-decisions.sh +0 -167
  174. package/scripts/context-doctor.sh +0 -72
  175. package/scripts/context-lint.sh +0 -110
  176. package/scripts/doctor.sh +0 -22
  177. package/scripts/init.sh +0 -247
  178. package/scripts/skills.sh +0 -113
  179. package/scripts/status.sh +0 -125
@@ -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
- stdout += d.toString("utf8");
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
- stderr += d.toString("utf8");
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
- reject(new Error("CLI timeout"));
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
- reject(err);
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
- resolve({ code, stdout, stderr });
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
- '{"type":"object","properties":{"reply":{"type":"string"},"dispatch":{"type":"array","items":{"type":"object","properties":{"target":{"type":"string"},"message":{"type":"string"}},"required":["target","message"]}},"ops":{"type":"array","items":{"type":"object","properties":{"action":{"type":"string"},"agent":{"type":"string"},"count":{"type":"integer"},"agent_id":{"type":"string"},"nickname":{"type":"string"}},"required":["action"]}},"disambiguate":{"type":"object","properties":{"prompt":{"type":"string"},"candidates":{"type":"array","items":{"type":"object","properties":{"agent_id":{"type":"string"},"reason":{"type":"string"}},"required":["agent_id"]}}}}},"required":["reply","dispatch","ops"]}',
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
- delete env.CLAUDE_SESSION_ID;
139
- delete env.CODEX_SESSION_ID;
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
- const err = res.stderr || res.stdout || "CLI failed";
153
- if (backend === DEFAULT_CODEX && backend.fallbackArgs && isUnsupportedArgError(err)) {
154
- const retry = buildArgs(
155
- { ...backend, args: backend.fallbackArgs },
156
- prompt,
157
- {
158
- model: params.model,
159
- sessionId,
160
- systemPrompt: params.systemPrompt,
161
- disableSession: params.disableSession,
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, retry.args, {
677
+ res = await runCommand(backend.command, retryArgs, {
166
678
  cwd: params.cwd,
167
679
  env,
168
- input: retry.stdin,
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
- if (res.code !== 0) {
175
- const err2 = res.stderr || res.stdout || "CLI failed";
176
- return { ok: false, error: err2, sessionId };
177
- }
178
- } else {
179
- return { ok: false, error: err, sessionId };
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 = { runCliAgent };
706
+ module.exports = {
707
+ runCliAgent,
708
+ extractCodexStreamDelta,
709
+ extractCodexToolEvent,
710
+ createCodexJsonlStreamParser,
711
+ };