u-foo 2.3.11 → 2.3.13

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.
@@ -1,706 +0,0 @@
1
- const { spawn } = require("child_process");
2
- const { randomUUID } = require("crypto");
3
-
4
- const DEFAULT_CLI_TIMEOUT_MS = 600000;
5
-
6
- const ROUTER_JSON_SCHEMA = JSON.stringify({
7
- type: "object",
8
- properties: {
9
- reply: { type: "string" },
10
- dispatch: {
11
- type: "array",
12
- items: {
13
- type: "object",
14
- properties: {
15
- target: { type: "string" },
16
- message: { type: "string" },
17
- },
18
- required: ["target", "message"],
19
- },
20
- },
21
- ops: {
22
- type: "array",
23
- items: {
24
- type: "object",
25
- properties: {
26
- action: { type: "string", enum: ["launch", "close", "rename", "cron"] },
27
- agent: { type: "string" },
28
- count: { type: "integer" },
29
- agent_id: { type: "string" },
30
- nickname: { type: "string" },
31
- operation: { type: "string", enum: ["start", "list", "stop", "add", "create", "ls", "rm", "remove"] },
32
- every: { type: "string" },
33
- interval_ms: { type: "integer" },
34
- target: { type: "string" },
35
- targets: {
36
- type: "array",
37
- items: { type: "string" },
38
- },
39
- prompt: { type: "string" },
40
- id: { type: "string" },
41
- },
42
- required: ["action"],
43
- },
44
- },
45
- disambiguate: {
46
- type: "object",
47
- properties: {
48
- prompt: { type: "string" },
49
- candidates: {
50
- type: "array",
51
- items: {
52
- type: "object",
53
- properties: {
54
- agent_id: { type: "string" },
55
- reason: { type: "string" },
56
- },
57
- required: ["agent_id"],
58
- },
59
- },
60
- },
61
- },
62
- },
63
- required: ["reply", "dispatch", "ops"],
64
- });
65
-
66
- function collectJsonl(text) {
67
- const lines = text.split(/\r?\n/).filter((l) => l.trim());
68
- const items = [];
69
- for (const line of lines) {
70
- try {
71
- items.push(JSON.parse(line));
72
- } catch {
73
- // Ignore malformed lines
74
- }
75
- }
76
- return items;
77
- }
78
-
79
- function collectJson(text) {
80
- try {
81
- return JSON.parse(text);
82
- } catch {
83
- return null;
84
- }
85
- }
86
-
87
- function safeInvoke(callback, ...args) {
88
- if (typeof callback !== "function") return;
89
- try {
90
- callback(...args);
91
- } catch {
92
- // Swallow stream callback errors to avoid breaking CLI execution.
93
- }
94
- }
95
-
96
- function normalizeDelta(value) {
97
- if (typeof value === "string") return value;
98
- return "";
99
- }
100
-
101
- const CORE_TOOL_NAMES = new Set(["read", "write", "edit", "bash"]);
102
-
103
- function normalizeCoreToolName(value = "") {
104
- const text = String(value || "").trim().toLowerCase();
105
- if (!text) return "";
106
- return CORE_TOOL_NAMES.has(text) ? text : "";
107
- }
108
-
109
- function parseMaybeJsonObject(value) {
110
- if (value && typeof value === "object" && !Array.isArray(value)) return value;
111
- const raw = typeof value === "string" ? value.trim() : "";
112
- if (!raw) return {};
113
- try {
114
- const parsed = JSON.parse(raw);
115
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
116
- } catch {
117
- // ignore invalid json
118
- }
119
- return {};
120
- }
121
-
122
- function collectNestedObjects(root, maxDepth = 4) {
123
- const out = [];
124
- const seen = new Set();
125
-
126
- function walk(node, depth) {
127
- if (!node || typeof node !== "object" || depth > maxDepth) return;
128
- if (seen.has(node)) return;
129
- seen.add(node);
130
- out.push(node);
131
- if (Array.isArray(node)) {
132
- for (const item of node) {
133
- walk(item, depth + 1);
134
- }
135
- return;
136
- }
137
- for (const value of Object.values(node)) {
138
- if (value && typeof value === "object") {
139
- walk(value, depth + 1);
140
- }
141
- }
142
- }
143
-
144
- walk(root, 0);
145
- return out;
146
- }
147
-
148
- function inferToolPhase(event = {}, candidate = {}) {
149
- const source = [
150
- event.type,
151
- event.event,
152
- event.status,
153
- candidate.type,
154
- candidate.status,
155
- ]
156
- .map((part) => String(part || "").toLowerCase())
157
- .join(" ");
158
-
159
- if (!source) return "update";
160
- if (/error|failed|failure|cancelled|canceled|abort/.test(source)) return "error";
161
- if (/done|completed|finished|result|end|succeeded/.test(source)) return "end";
162
- if (/start|started|begin|call|invoke|created|added|delta|progress/.test(source)) return "start";
163
- return "update";
164
- }
165
-
166
- function buildToolArgs(tool = "", candidate = {}) {
167
- const rawArgs = candidate.args
168
- || candidate.arguments
169
- || candidate.input
170
- || candidate.params
171
- || candidate.payload
172
- || {};
173
- const parsed = parseMaybeJsonObject(rawArgs);
174
- if (Object.keys(parsed).length > 0) return parsed;
175
-
176
- // Common direct fields seen in tool events.
177
- if (tool === "bash") {
178
- const command = String(candidate.command || candidate.cmd || "").trim();
179
- return command ? { command } : {};
180
- }
181
- if (tool === "read" || tool === "write" || tool === "edit") {
182
- const filePath = String(candidate.path || candidate.file || "").trim();
183
- if (filePath) return { path: filePath };
184
- }
185
- return {};
186
- }
187
-
188
- function buildToolEventKey(event = {}, candidate = {}, tool = "", phase = "", args = {}) {
189
- const id = String(
190
- event.id
191
- || event.item_id
192
- || candidate.id
193
- || candidate.call_id
194
- || candidate.tool_call_id
195
- || ""
196
- ).trim();
197
- if (id) return `${tool}|${phase}|${id}`;
198
-
199
- const details = JSON.stringify({
200
- path: args.path || args.file || "",
201
- command: args.command || args.cmd || "",
202
- });
203
- return `${tool}|${phase}|${details}`;
204
- }
205
-
206
- function extractCodexToolEvent(event = {}, state = null) {
207
- const objects = collectNestedObjects(event, 4);
208
- for (const candidate of objects) {
209
- if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) continue;
210
- const tool = normalizeCoreToolName(
211
- candidate.tool
212
- || candidate.tool_name
213
- || candidate.name
214
- || candidate.function_name
215
- || candidate.action
216
- || candidate.type
217
- );
218
- if (!tool) continue;
219
-
220
- const args = buildToolArgs(tool, candidate);
221
- const phase = inferToolPhase(event, candidate);
222
- const error = String(candidate.error || candidate.message || "").trim();
223
- const key = buildToolEventKey(event, candidate, tool, phase, args);
224
- if (state && state.seenToolEventKeys instanceof Set) {
225
- if (state.seenToolEventKeys.has(key)) continue;
226
- state.seenToolEventKeys.add(key);
227
- }
228
-
229
- return {
230
- tool,
231
- phase,
232
- args,
233
- error,
234
- rawType: String(event.type || ""),
235
- };
236
- }
237
- return null;
238
- }
239
-
240
- function extractTextFromContentBlock(block) {
241
- if (!block || typeof block !== "object") return "";
242
- if (typeof block.text === "string") return block.text;
243
- if (typeof block.content === "string") return block.content;
244
- if (typeof block.output_text === "string") return block.output_text;
245
- if (typeof block.delta === "string") return block.delta;
246
- return "";
247
- }
248
-
249
- function extractTextFromCodexItem(item) {
250
- if (!item || typeof item !== "object") return "";
251
- if (typeof item.text === "string") return item.text;
252
- if (typeof item.delta === "string") return item.delta;
253
- if (typeof item.output_text === "string") return item.output_text;
254
- if (Array.isArray(item.content)) {
255
- const text = item.content
256
- .map((part) => extractTextFromContentBlock(part))
257
- .filter(Boolean)
258
- .join("");
259
- if (text) return text;
260
- }
261
- if (item.item && typeof item.item === "object") {
262
- return extractTextFromCodexItem(item.item);
263
- }
264
- return "";
265
- }
266
-
267
- function extractCodexStreamDelta(event) {
268
- if (!event || typeof event !== "object") return "";
269
-
270
- if (
271
- event.assistantMessageEvent
272
- && typeof event.assistantMessageEvent === "object"
273
- && typeof event.assistantMessageEvent.delta === "string"
274
- ) {
275
- return event.assistantMessageEvent.delta;
276
- }
277
-
278
- if (typeof event.delta === "string") return event.delta;
279
- if (typeof event.output_text === "string") return event.output_text;
280
- if (event.item && typeof event.item === "object") {
281
- return extractTextFromCodexItem(event.item);
282
- }
283
- if (event.message && typeof event.message === "object") {
284
- return extractTextFromCodexItem(event.message);
285
- }
286
- return "";
287
- }
288
-
289
- function createCodexJsonlStreamParser(onDeltaOrOptions, maybeOnToolEvent) {
290
- let onDelta = null;
291
- let onToolEvent = null;
292
- if (typeof onDeltaOrOptions === "function") {
293
- onDelta = onDeltaOrOptions;
294
- onToolEvent = typeof maybeOnToolEvent === "function" ? maybeOnToolEvent : null;
295
- } else if (onDeltaOrOptions && typeof onDeltaOrOptions === "object") {
296
- onDelta = typeof onDeltaOrOptions.onDelta === "function" ? onDeltaOrOptions.onDelta : null;
297
- onToolEvent = typeof onDeltaOrOptions.onToolEvent === "function"
298
- ? onDeltaOrOptions.onToolEvent
299
- : null;
300
- }
301
-
302
- let buffer = "";
303
- const toolState = { seenToolEventKeys: new Set() };
304
-
305
- function parseLine(line) {
306
- const trimmed = String(line || "").trim();
307
- if (!trimmed) return;
308
- let parsed;
309
- try {
310
- parsed = JSON.parse(trimmed);
311
- } catch {
312
- return;
313
- }
314
- const delta = normalizeDelta(extractCodexStreamDelta(parsed));
315
- if (delta) {
316
- safeInvoke(onDelta, delta, parsed);
317
- }
318
- const toolEvent = extractCodexToolEvent(parsed, toolState);
319
- if (toolEvent) {
320
- safeInvoke(onToolEvent, toolEvent, parsed);
321
- }
322
- }
323
-
324
- return {
325
- onChunk(chunk) {
326
- const text = String(chunk || "");
327
- if (!text) return;
328
- buffer += text;
329
- const lines = buffer.split(/\r?\n/);
330
- buffer = lines.pop() || "";
331
- for (const line of lines) {
332
- parseLine(line);
333
- }
334
- },
335
- flush() {
336
- if (!buffer) return;
337
- parseLine(buffer);
338
- buffer = "";
339
- },
340
- };
341
- }
342
-
343
- function runCommand(command, args, options = {}) {
344
- return new Promise((resolve, reject) => {
345
- const child = spawn(command, args, {
346
- stdio: ["pipe", "pipe", "pipe"],
347
- ...options,
348
- });
349
- let settled = false;
350
-
351
- const settleReject = (err) => {
352
- if (settled) return;
353
- settled = true;
354
- reject(err);
355
- };
356
- const settleResolve = (value) => {
357
- if (settled) return;
358
- settled = true;
359
- resolve(value);
360
- };
361
-
362
- if (typeof options.onSpawn === "function") {
363
- try {
364
- options.onSpawn(child);
365
- } catch {
366
- // ignore callback failures
367
- }
368
- }
369
-
370
- let stdout = "";
371
- let stderr = "";
372
- child.stdout.on("data", (d) => {
373
- const chunk = d.toString("utf8");
374
- stdout += chunk;
375
- if (options.onStdout) {
376
- options.onStdout(chunk);
377
- }
378
- });
379
- child.stderr.on("data", (d) => {
380
- const chunk = d.toString("utf8");
381
- stderr += chunk;
382
- if (options.onStderr) {
383
- options.onStderr(chunk);
384
- }
385
- });
386
- let timeout = null;
387
- if (options.timeoutMs) {
388
- timeout = setTimeout(() => {
389
- try {
390
- child.kill("SIGTERM");
391
- } catch {
392
- // ignore
393
- }
394
- settleReject(new Error(`CLI timeout (${options.timeoutMs}ms)`));
395
- }, options.timeoutMs);
396
- }
397
-
398
- let abortHandler = null;
399
- if (options.signal && typeof options.signal.addEventListener === "function") {
400
- abortHandler = () => {
401
- try {
402
- child.kill("SIGTERM");
403
- } catch {
404
- // ignore
405
- }
406
- settleReject(new Error("CLI cancelled"));
407
- };
408
- if (options.signal.aborted) {
409
- abortHandler();
410
- } else {
411
- options.signal.addEventListener("abort", abortHandler, { once: true });
412
- }
413
- }
414
-
415
- child.on("error", (err) => {
416
- if (timeout) clearTimeout(timeout);
417
- if (abortHandler && options.signal && typeof options.signal.removeEventListener === "function") {
418
- options.signal.removeEventListener("abort", abortHandler);
419
- }
420
- settleReject(err);
421
- });
422
- child.on("close", (code) => {
423
- if (timeout) clearTimeout(timeout);
424
- if (abortHandler && options.signal && typeof options.signal.removeEventListener === "function") {
425
- options.signal.removeEventListener("abort", abortHandler);
426
- }
427
- settleResolve({ code, stdout, stderr });
428
- });
429
-
430
- if (options.input) {
431
- child.stdin.write(options.input);
432
- }
433
- child.stdin.end();
434
- });
435
- }
436
-
437
- const DEFAULT_CLAUDE = {
438
- command: "claude",
439
- args: [
440
- "-p",
441
- "--output-format",
442
- "json",
443
- "--dangerously-skip-permissions",
444
- "--no-session-persistence",
445
- "--json-schema",
446
- ROUTER_JSON_SCHEMA,
447
- ],
448
- fallbackArgs: [
449
- "-p",
450
- "--output-format",
451
- "json",
452
- "--dangerously-skip-permissions",
453
- "--json-schema",
454
- ROUTER_JSON_SCHEMA,
455
- ],
456
- output: "json",
457
- input: "arg",
458
- modelArg: "--model",
459
- sessionArg: "--session-id",
460
- systemPromptArg: "--append-system-prompt",
461
- };
462
-
463
- const DEFAULT_CODEX = {
464
- command: "codex",
465
- args: ["exec", "--json", "--color", "never", "--sandbox", "read-only", "--skip-git-repo-check"],
466
- output: "jsonl",
467
- input: "arg",
468
- modelArg: "--model",
469
- sessionArg: null,
470
- fallbackArgs: ["exec", "--json", "--color", "never", "--sandbox", "read-only"],
471
- };
472
-
473
- function buildArgs(backend, prompt, opts) {
474
- const args = [...(backend.args || [])];
475
- const extraArgs = Array.isArray(opts.extraArgs) ? opts.extraArgs.filter(Boolean) : [];
476
- if (extraArgs.length > 0) {
477
- args.push(...extraArgs);
478
- }
479
- if (opts.model && backend.modelArg) {
480
- args.push(backend.modelArg, opts.model);
481
- }
482
- if (opts.sessionId && backend.sessionArg && !opts.disableSession) {
483
- args.push(backend.sessionArg, opts.sessionId);
484
- }
485
- if (opts.systemPrompt && backend.systemPromptArg) {
486
- args.push(backend.systemPromptArg, opts.systemPrompt);
487
- }
488
- if (backend.input === "arg") {
489
- args.push(prompt);
490
- return { args, stdin: "" };
491
- }
492
- return { args, stdin: prompt };
493
- }
494
-
495
- function applySandboxOverride(args, sandbox) {
496
- if (!sandbox) return;
497
- const idx = args.indexOf("--sandbox");
498
- if (idx >= 0) {
499
- if (idx + 1 < args.length) {
500
- args[idx + 1] = sandbox;
501
- } else {
502
- args.push(sandbox);
503
- }
504
- } else {
505
- args.push("--sandbox", sandbox);
506
- }
507
- }
508
-
509
- function applyClaudeJsonSchema(args, jsonSchema) {
510
- if (!jsonSchema) return;
511
- const schema = typeof jsonSchema === "string" ? jsonSchema : JSON.stringify(jsonSchema);
512
- const idx = args.indexOf("--json-schema");
513
- if (idx >= 0) {
514
- if (idx + 1 < args.length) {
515
- args[idx + 1] = schema;
516
- } else {
517
- args.push(schema);
518
- }
519
- return;
520
- }
521
- args.push("--json-schema", schema);
522
- }
523
-
524
- function isUnsupportedArgError(errText) {
525
- const text = (errText || "").toLowerCase();
526
- return text.includes("unknown option")
527
- || text.includes("unknown argument")
528
- || text.includes("unexpected argument")
529
- || text.includes("unrecognized option");
530
- }
531
-
532
- function extractUnsupportedOption(errText) {
533
- const text = String(errText || "");
534
- const quoted = text.match(/['"`](--[a-z0-9-]+)['"`]/i);
535
- if (quoted && quoted[1]) return quoted[1];
536
- const plain = text.match(/(--[a-z0-9-]+)/i);
537
- return plain && plain[1] ? plain[1] : "";
538
- }
539
-
540
- function removeUnsupportedOption(args, option) {
541
- const out = Array.isArray(args) ? args.slice() : [];
542
- const target = String(option || "").trim();
543
- if (!target) return { changed: false, args: out };
544
- const idx = out.indexOf(target);
545
- if (idx < 0) return { changed: false, args: out };
546
-
547
- const optionsWithValue = new Set([
548
- "--json-schema",
549
- "--model",
550
- "--session-id",
551
- "--append-system-prompt",
552
- "--output-format",
553
- "--sandbox",
554
- ]);
555
- const takesValue = optionsWithValue.has(target);
556
- out.splice(idx, takesValue ? 2 : 1);
557
- return { changed: true, args: out };
558
- }
559
-
560
- async function runCliAgent(params) {
561
- const backend = params.provider === "codex-cli" ? DEFAULT_CODEX : DEFAULT_CLAUDE;
562
- const sessionId = params.sessionId || randomUUID();
563
- const streamState = { emitted: false };
564
- const emitStreamDelta = (delta, meta = null) => {
565
- const text = normalizeDelta(delta);
566
- if (!text) return;
567
- streamState.emitted = true;
568
- safeInvoke(params.onStreamDelta, text, meta);
569
- };
570
- const emitToolEvent = (event, meta = null) => {
571
- if (!event || typeof event !== "object") return;
572
- safeInvoke(params.onToolEvent, event, meta);
573
- };
574
- const prompt =
575
- params.systemPrompt && !backend.systemPromptArg
576
- ? `${params.systemPrompt}\n\n${params.prompt}`
577
- : params.prompt;
578
- const { args, stdin } = buildArgs(backend, prompt, {
579
- model: params.model,
580
- sessionId,
581
- systemPrompt: params.systemPrompt,
582
- disableSession: params.disableSession,
583
- extraArgs: params.extraArgs,
584
- });
585
- if (backend === DEFAULT_CODEX && params.sandbox) {
586
- applySandboxOverride(args, params.sandbox);
587
- }
588
- if (backend === DEFAULT_CLAUDE && params.jsonSchema) {
589
- applyClaudeJsonSchema(args, params.jsonSchema);
590
- }
591
-
592
- let res;
593
- const env = { ...process.env, ...(params.env || {}) };
594
- // Clean up ufoo-specific env vars to avoid interference with CLI agents
595
- delete env.UFOO_SUBSCRIBER_ID;
596
- let codexParser = null;
597
- if (
598
- backend === DEFAULT_CODEX
599
- && (typeof params.onStreamDelta === "function" || typeof params.onToolEvent === "function")
600
- ) {
601
- codexParser = createCodexJsonlStreamParser({
602
- onDelta: (delta, event) =>
603
- emitStreamDelta(delta, { backend: "codex", event }),
604
- onToolEvent: (event, rawEvent) =>
605
- emitToolEvent(event, { backend: "codex", event: rawEvent }),
606
- });
607
- }
608
- try {
609
- res = await runCommand(backend.command, args, {
610
- cwd: params.cwd,
611
- env,
612
- input: stdin,
613
- timeoutMs: params.timeoutMs || DEFAULT_CLI_TIMEOUT_MS,
614
- onStdout: codexParser ? (chunk) => codexParser.onChunk(chunk) : null,
615
- signal: params.signal,
616
- });
617
- if (codexParser) codexParser.flush();
618
- } catch (err) {
619
- return { ok: false, error: err.message || String(err), sessionId, streamed: streamState.emitted };
620
- }
621
-
622
- if (res.code !== 0) {
623
- let lastErr = res.stderr || res.stdout || "CLI failed";
624
- let retryArgs = args.slice();
625
- let retryStdin = stdin;
626
- let usedFallbackPreset = false;
627
-
628
- for (let attempt = 0; attempt < 3 && isUnsupportedArgError(lastErr); attempt += 1) {
629
- if (!usedFallbackPreset && backend.fallbackArgs) {
630
- const retry = buildArgs(
631
- { ...backend, args: backend.fallbackArgs },
632
- prompt,
633
- {
634
- model: params.model,
635
- sessionId,
636
- systemPrompt: params.systemPrompt,
637
- disableSession: params.disableSession,
638
- extraArgs: params.extraArgs,
639
- },
640
- );
641
- retryArgs = retry.args;
642
- retryStdin = retry.stdin;
643
- if (params.sandbox) {
644
- applySandboxOverride(retryArgs, params.sandbox);
645
- }
646
- if (backend === DEFAULT_CLAUDE && params.jsonSchema) {
647
- applyClaudeJsonSchema(retryArgs, params.jsonSchema);
648
- }
649
- usedFallbackPreset = true;
650
- } else {
651
- const unsupportedOption = extractUnsupportedOption(lastErr);
652
- const dropped = removeUnsupportedOption(retryArgs, unsupportedOption);
653
- if (!dropped.changed) {
654
- break;
655
- }
656
- retryArgs = dropped.args;
657
- }
658
-
659
- let retryParser = null;
660
- if (
661
- backend === DEFAULT_CODEX
662
- && (typeof params.onStreamDelta === "function" || typeof params.onToolEvent === "function")
663
- ) {
664
- retryParser = createCodexJsonlStreamParser({
665
- onDelta: (delta, event) =>
666
- emitStreamDelta(delta, { backend: "codex", event }),
667
- onToolEvent: (event, rawEvent) =>
668
- emitToolEvent(event, { backend: "codex", event: rawEvent }),
669
- });
670
- }
671
- try {
672
- res = await runCommand(backend.command, retryArgs, {
673
- cwd: params.cwd,
674
- env,
675
- input: retryStdin,
676
- timeoutMs: params.timeoutMs || DEFAULT_CLI_TIMEOUT_MS,
677
- onStdout: retryParser ? (chunk) => retryParser.onChunk(chunk) : null,
678
- signal: params.signal,
679
- });
680
- if (retryParser) retryParser.flush();
681
- } catch (err2) {
682
- return { ok: false, error: err2.message || String(err2), sessionId, streamed: streamState.emitted };
683
- }
684
-
685
- if (res.code === 0) break;
686
- lastErr = res.stderr || res.stdout || "CLI failed";
687
- }
688
-
689
- if (res.code !== 0) {
690
- return { ok: false, error: lastErr, sessionId, streamed: streamState.emitted };
691
- }
692
- }
693
-
694
- if (backend.output === "jsonl") {
695
- return { ok: true, sessionId, output: collectJsonl(res.stdout), streamed: streamState.emitted };
696
- }
697
-
698
- return { ok: true, sessionId, output: collectJson(res.stdout), streamed: streamState.emitted };
699
- }
700
-
701
- module.exports = {
702
- runCliAgent,
703
- extractCodexStreamDelta,
704
- extractCodexToolEvent,
705
- createCodexJsonlStreamParser,
706
- };