u-foo 1.2.14 → 1.3.0
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/modules/online/README.md +18 -0
- package/package.json +2 -1
- package/src/agent/cliRunner.js +1 -1
- package/src/agent/launcher.js +23 -4
- package/src/agent/ptyRunner.js +39 -16
- package/src/agent/ufooAgent.js +2 -1
- package/src/assistant/agent.js +2 -1
- package/src/assistant/bridge.js +9 -3
- package/src/assistant/constants.js +15 -0
- package/src/assistant/engine.js +7 -2
- package/src/assistant/ufooEngineCli.js +9 -3
- package/src/chat/commandExecutor.js +188 -13
- package/src/chat/commands.js +11 -0
- package/src/chat/daemonMessageRouter.js +107 -0
- package/src/cli/groupCoreCommands.js +246 -0
- package/src/cli/onlineCoreCommands.js +8 -0
- package/src/cli.js +325 -2
- package/src/code/agent.js +74 -3
- package/src/code/tui.js +73 -5
- package/src/daemon/groupOrchestrator.js +557 -0
- package/src/daemon/index.js +319 -1
- package/src/daemon/status.js +48 -0
- package/src/group/diagram.js +222 -0
- package/src/group/templates.js +280 -0
- package/src/group/validateTemplate.js +234 -0
- package/src/online/bridge.js +8 -1
- package/src/online/server.js +193 -14
- package/src/shared/eventContract.js +5 -0
- package/src/ufoo/paths.js +2 -0
- package/templates/groups/dev-basic.json +78 -0
- package/templates/groups/research-quick.json +49 -0
package/src/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ const { socketPath, isRunning } = require("./daemon");
|
|
|
6
6
|
const { runBusCoreCommand } = require("./cli/busCoreCommands");
|
|
7
7
|
const { runCtxCommand } = require("./cli/ctxCoreCommands");
|
|
8
8
|
const { runOnlineCommand } = require("./cli/onlineCoreCommands");
|
|
9
|
+
const { runGroupCoreCommand } = require("./cli/groupCoreCommands");
|
|
9
10
|
|
|
10
11
|
function getPackageRoot() {
|
|
11
12
|
return path.resolve(__dirname, "..");
|
|
@@ -561,6 +562,189 @@ async function runCli(argv) {
|
|
|
561
562
|
}
|
|
562
563
|
});
|
|
563
564
|
|
|
565
|
+
const group = program.command("group").description("Agent group template commands");
|
|
566
|
+
group
|
|
567
|
+
.command("templates")
|
|
568
|
+
.description("List available group templates")
|
|
569
|
+
.argument("[action]", "list", "list")
|
|
570
|
+
.option("--json", "Output as JSON")
|
|
571
|
+
.action(async (action, opts) => {
|
|
572
|
+
const normalizedAction = String(action || "list").trim().toLowerCase();
|
|
573
|
+
if (normalizedAction !== "list" && normalizedAction !== "ls") {
|
|
574
|
+
console.error(`Unknown group templates action: ${normalizedAction}`);
|
|
575
|
+
process.exitCode = 1;
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
try {
|
|
579
|
+
await runGroupCoreCommand("templates", [normalizedAction], {
|
|
580
|
+
cwd: process.cwd(),
|
|
581
|
+
json: opts.json,
|
|
582
|
+
});
|
|
583
|
+
} catch (err) {
|
|
584
|
+
console.error(err.message || String(err));
|
|
585
|
+
process.exitCode = 1;
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
group
|
|
590
|
+
.command("template")
|
|
591
|
+
.description("Group template operations")
|
|
592
|
+
.argument("<action>", "list|show|validate|new")
|
|
593
|
+
.argument("[target]", "Template alias (or path for validate)")
|
|
594
|
+
.option("--from <alias>", "Builtin template alias (for template new)")
|
|
595
|
+
.option("--global", "Create template in ~/.ufoo/templates/groups")
|
|
596
|
+
.option("--project", "Create template in .ufoo/templates/groups (default)")
|
|
597
|
+
.option("--force", "Overwrite existing file (for template new)")
|
|
598
|
+
.option("--json", "Output as JSON")
|
|
599
|
+
.action(async (action, target, opts) => {
|
|
600
|
+
const args = [action];
|
|
601
|
+
if (target) args.push(target);
|
|
602
|
+
if (opts.from) args.push("--from", opts.from);
|
|
603
|
+
if (opts.global) args.push("--global");
|
|
604
|
+
if (opts.project) args.push("--project");
|
|
605
|
+
if (opts.force) args.push("--force");
|
|
606
|
+
if (opts.json) args.push("--json");
|
|
607
|
+
|
|
608
|
+
try {
|
|
609
|
+
await runGroupCoreCommand("template", args, {
|
|
610
|
+
cwd: process.cwd(),
|
|
611
|
+
json: opts.json,
|
|
612
|
+
});
|
|
613
|
+
} catch (err) {
|
|
614
|
+
console.error(err.message || String(err));
|
|
615
|
+
process.exitCode = 1;
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
group
|
|
620
|
+
.command("run")
|
|
621
|
+
.description("Launch an agent group from template")
|
|
622
|
+
.argument("<alias>", "Template alias")
|
|
623
|
+
.option("--instance <name>", "Group instance ID")
|
|
624
|
+
.option("--dry-run", "Validate and compile launch plan without starting agents")
|
|
625
|
+
.option("--json", "Output daemon response as JSON")
|
|
626
|
+
.action(async (alias, opts) => {
|
|
627
|
+
try {
|
|
628
|
+
const projectRoot = process.cwd();
|
|
629
|
+
await ensureDaemonRunning(projectRoot);
|
|
630
|
+
const resp = await sendDaemonRequest(projectRoot, {
|
|
631
|
+
type: "launch_group",
|
|
632
|
+
alias,
|
|
633
|
+
instance: opts.instance || "",
|
|
634
|
+
dry_run: opts.dryRun === true,
|
|
635
|
+
});
|
|
636
|
+
if (opts.json) {
|
|
637
|
+
console.log(JSON.stringify(resp?.data || {}, null, 2));
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
const reply = resp?.data?.reply || "Group run requested";
|
|
641
|
+
console.log(reply);
|
|
642
|
+
if (resp?.data?.group?.ok === false) {
|
|
643
|
+
process.exitCode = 1;
|
|
644
|
+
}
|
|
645
|
+
} catch (err) {
|
|
646
|
+
console.error(err.message || String(err));
|
|
647
|
+
process.exitCode = 1;
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
group
|
|
652
|
+
.command("status")
|
|
653
|
+
.description("Show group runtime status (single group or list)")
|
|
654
|
+
.argument("[groupId]", "Group ID (optional)")
|
|
655
|
+
.option("--json", "Output daemon response as JSON")
|
|
656
|
+
.action(async (groupId, opts) => {
|
|
657
|
+
try {
|
|
658
|
+
const projectRoot = process.cwd();
|
|
659
|
+
await ensureDaemonRunning(projectRoot);
|
|
660
|
+
const resp = await sendDaemonRequest(projectRoot, {
|
|
661
|
+
type: "group_status",
|
|
662
|
+
group_id: groupId || "",
|
|
663
|
+
});
|
|
664
|
+
if (opts.json) {
|
|
665
|
+
console.log(JSON.stringify(resp?.data || {}, null, 2));
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
const reply = resp?.data?.reply || "Group status requested";
|
|
669
|
+
console.log(reply);
|
|
670
|
+
const group = resp?.data?.group || {};
|
|
671
|
+
if (groupId && group?.group) {
|
|
672
|
+
console.log(JSON.stringify(group.group, null, 2));
|
|
673
|
+
} else if (!groupId && Array.isArray(group?.groups)) {
|
|
674
|
+
group.groups.forEach((item) => {
|
|
675
|
+
console.log(`- ${item.group_id} [${item.status}] ${item.template_alias} active=${item.members_active}/${item.members_total}`);
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
} catch (err) {
|
|
679
|
+
console.error(err.message || String(err));
|
|
680
|
+
process.exitCode = 1;
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
group
|
|
685
|
+
.command("diagram")
|
|
686
|
+
.description("Render group diagram from template alias or group runtime ID")
|
|
687
|
+
.argument("<target>", "Template alias or group ID")
|
|
688
|
+
.option("--ascii", "Render ASCII diagram (default)")
|
|
689
|
+
.option("--mermaid", "Render Mermaid flowchart")
|
|
690
|
+
.option("--json", "Output daemon response as JSON")
|
|
691
|
+
.action(async (target, opts) => {
|
|
692
|
+
try {
|
|
693
|
+
const projectRoot = process.cwd();
|
|
694
|
+
await ensureDaemonRunning(projectRoot);
|
|
695
|
+
const format = opts.mermaid ? "mermaid" : "ascii";
|
|
696
|
+
const resp = await sendDaemonRequest(projectRoot, {
|
|
697
|
+
type: "group_diagram",
|
|
698
|
+
alias: target,
|
|
699
|
+
group_id: target,
|
|
700
|
+
format,
|
|
701
|
+
});
|
|
702
|
+
if (opts.json) {
|
|
703
|
+
console.log(JSON.stringify(resp?.data || {}, null, 2));
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
const reply = resp?.data?.reply || "Group diagram requested";
|
|
707
|
+
console.log(reply);
|
|
708
|
+
if (resp?.data?.group?.diagram) {
|
|
709
|
+
console.log(resp.data.group.diagram);
|
|
710
|
+
}
|
|
711
|
+
if (resp?.data?.group?.ok === false) {
|
|
712
|
+
process.exitCode = 1;
|
|
713
|
+
}
|
|
714
|
+
} catch (err) {
|
|
715
|
+
console.error(err.message || String(err));
|
|
716
|
+
process.exitCode = 1;
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
group
|
|
721
|
+
.command("stop")
|
|
722
|
+
.description("Stop a running group instance")
|
|
723
|
+
.argument("<groupId>", "Group ID")
|
|
724
|
+
.option("--json", "Output daemon response as JSON")
|
|
725
|
+
.action(async (groupId, opts) => {
|
|
726
|
+
try {
|
|
727
|
+
const projectRoot = process.cwd();
|
|
728
|
+
await ensureDaemonRunning(projectRoot);
|
|
729
|
+
const resp = await sendDaemonRequest(projectRoot, {
|
|
730
|
+
type: "stop_group",
|
|
731
|
+
group_id: groupId,
|
|
732
|
+
});
|
|
733
|
+
if (opts.json) {
|
|
734
|
+
console.log(JSON.stringify(resp?.data || {}, null, 2));
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
const reply = resp?.data?.reply || "Group stop requested";
|
|
738
|
+
console.log(reply);
|
|
739
|
+
if (resp?.data?.group?.ok === false) {
|
|
740
|
+
process.exitCode = 1;
|
|
741
|
+
}
|
|
742
|
+
} catch (err) {
|
|
743
|
+
console.error(err.message || String(err));
|
|
744
|
+
process.exitCode = 1;
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
|
|
564
748
|
const online = program.command("online").description("ufoo online helpers");
|
|
565
749
|
online
|
|
566
750
|
.command("server")
|
|
@@ -616,6 +800,7 @@ async function runCli(argv) {
|
|
|
616
800
|
.option("--name <room>", "Room name (optional)")
|
|
617
801
|
.option("--type <type>", "Room type (public|private)")
|
|
618
802
|
.option("--password <pwd>", "Room password (private only)")
|
|
803
|
+
.option("--created-by <name>", "Room creator nickname metadata")
|
|
619
804
|
.action(async (action, opts) => {
|
|
620
805
|
try {
|
|
621
806
|
await runOnlineCommand("room", { action, opts }, {
|
|
@@ -640,6 +825,7 @@ async function runCli(argv) {
|
|
|
640
825
|
.option("--nickname <name>", "Nickname to resolve token")
|
|
641
826
|
.option("--name <name>", "Channel name (unique)")
|
|
642
827
|
.option("--type <type>", "Channel type (world|public)")
|
|
828
|
+
.option("--created-by <name>", "Channel creator nickname metadata")
|
|
643
829
|
.action(async (action, opts) => {
|
|
644
830
|
try {
|
|
645
831
|
await runOnlineCommand("channel", { action, opts }, {
|
|
@@ -934,11 +1120,17 @@ async function runCli(argv) {
|
|
|
934
1120
|
console.log(" ufoo init [--modules <list>] [--project <dir>]");
|
|
935
1121
|
console.log(" ufoo skills list");
|
|
936
1122
|
console.log(" ufoo skills install <name|all> [--target <dir> | --codex | --agents]");
|
|
1123
|
+
console.log(" ufoo group templates [list|ls] [--json]");
|
|
1124
|
+
console.log(" ufoo group template <list|show|validate|new> [target] [--from <builtin>] [--global] [--force] [--json]");
|
|
1125
|
+
console.log(" ufoo group run <alias> [--instance <name>] [--dry-run] [--json]");
|
|
1126
|
+
console.log(" ufoo group status [groupId] [--json]");
|
|
1127
|
+
console.log(" ufoo group diagram <alias|groupId> [--ascii|--mermaid] [--json]");
|
|
1128
|
+
console.log(" ufoo group stop <groupId> [--json]");
|
|
937
1129
|
console.log(" ufoo online server [--port 8787] [--host 127.0.0.1] [--token-file <path>]");
|
|
938
1130
|
console.log(" ufoo online token <subscriber> [--nickname <name>] [--server <url>] [--file <path>]");
|
|
939
|
-
console.log(" ufoo online room create [--name <room>] --type public|private [--password <pwd>] [--server <url>]");
|
|
1131
|
+
console.log(" ufoo online room create [--name <room>] --type public|private [--password <pwd>] [--created-by <name>] [--server <url>]");
|
|
940
1132
|
console.log(" ufoo online room list [--server <url>]");
|
|
941
|
-
console.log(" ufoo online channel create --name <name> [--type world|public] [--server <url>]");
|
|
1133
|
+
console.log(" ufoo online channel create --name <name> [--type world|public] [--created-by <name>] [--server <url>]");
|
|
942
1134
|
console.log(" ufoo online channel list [--server <url>]");
|
|
943
1135
|
console.log(" ufoo online connect --nickname <name> [--join <ch>] [--room <id> --room-password <pwd>] [...]");
|
|
944
1136
|
console.log(" ufoo online send --nickname <name> --text <msg> [--channel <ch>] [--room <id>]");
|
|
@@ -1271,6 +1463,137 @@ async function runCli(argv) {
|
|
|
1271
1463
|
process.exitCode = 1;
|
|
1272
1464
|
return;
|
|
1273
1465
|
}
|
|
1466
|
+
if (cmd === "group") {
|
|
1467
|
+
const sub = String(rest[0] || "").trim().toLowerCase();
|
|
1468
|
+
|
|
1469
|
+
(async () => {
|
|
1470
|
+
try {
|
|
1471
|
+
if (sub === "templates") {
|
|
1472
|
+
const action = String(rest[1] || "list").trim().toLowerCase();
|
|
1473
|
+
if (action !== "list" && action !== "ls") {
|
|
1474
|
+
throw new Error(`Unknown group templates action: ${action}`);
|
|
1475
|
+
}
|
|
1476
|
+
const json = rest.includes("--json");
|
|
1477
|
+
await runGroupCoreCommand("templates", [action], {
|
|
1478
|
+
cwd: process.cwd(),
|
|
1479
|
+
json,
|
|
1480
|
+
});
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
if (sub === "template") {
|
|
1485
|
+
const action = String(rest[1] || "list").trim().toLowerCase();
|
|
1486
|
+
const args = [action, ...rest.slice(2)];
|
|
1487
|
+
const json = rest.includes("--json");
|
|
1488
|
+
await runGroupCoreCommand("template", args, {
|
|
1489
|
+
cwd: process.cwd(),
|
|
1490
|
+
json,
|
|
1491
|
+
});
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
if (sub === "run") {
|
|
1496
|
+
const alias = String(rest[1] || "").trim();
|
|
1497
|
+
if (!alias) throw new Error("group run requires <alias>");
|
|
1498
|
+
const instanceIdx = rest.indexOf("--instance");
|
|
1499
|
+
const instance = instanceIdx !== -1 ? String(rest[instanceIdx + 1] || "").trim() : "";
|
|
1500
|
+
const dryRun = rest.includes("--dry-run");
|
|
1501
|
+
const outputJson = rest.includes("--json");
|
|
1502
|
+
|
|
1503
|
+
const projectRoot = process.cwd();
|
|
1504
|
+
await ensureDaemonRunning(projectRoot);
|
|
1505
|
+
const resp = await sendDaemonRequest(projectRoot, {
|
|
1506
|
+
type: "launch_group",
|
|
1507
|
+
alias,
|
|
1508
|
+
instance,
|
|
1509
|
+
dry_run: dryRun,
|
|
1510
|
+
});
|
|
1511
|
+
if (outputJson) {
|
|
1512
|
+
console.log(JSON.stringify(resp?.data || {}, null, 2));
|
|
1513
|
+
return;
|
|
1514
|
+
}
|
|
1515
|
+
console.log(resp?.data?.reply || "Group run requested");
|
|
1516
|
+
if (resp?.data?.group?.ok === false) process.exitCode = 1;
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
if (sub === "status") {
|
|
1521
|
+
const groupId = rest[1] && !rest[1].startsWith("--") ? rest[1] : "";
|
|
1522
|
+
const outputJson = rest.includes("--json");
|
|
1523
|
+
const projectRoot = process.cwd();
|
|
1524
|
+
await ensureDaemonRunning(projectRoot);
|
|
1525
|
+
const resp = await sendDaemonRequest(projectRoot, {
|
|
1526
|
+
type: "group_status",
|
|
1527
|
+
group_id: groupId,
|
|
1528
|
+
});
|
|
1529
|
+
if (outputJson) {
|
|
1530
|
+
console.log(JSON.stringify(resp?.data || {}, null, 2));
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
console.log(resp?.data?.reply || "Group status requested");
|
|
1534
|
+
const group = resp?.data?.group || {};
|
|
1535
|
+
if (groupId && group?.group) {
|
|
1536
|
+
console.log(JSON.stringify(group.group, null, 2));
|
|
1537
|
+
} else if (!groupId && Array.isArray(group?.groups)) {
|
|
1538
|
+
group.groups.forEach((item) => {
|
|
1539
|
+
console.log(`- ${item.group_id} [${item.status}] ${item.template_alias} active=${item.members_active}/${item.members_total}`);
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
if (sub === "stop") {
|
|
1546
|
+
const groupId = String(rest[1] || "").trim();
|
|
1547
|
+
if (!groupId) throw new Error("group stop requires <groupId>");
|
|
1548
|
+
const outputJson = rest.includes("--json");
|
|
1549
|
+
const projectRoot = process.cwd();
|
|
1550
|
+
await ensureDaemonRunning(projectRoot);
|
|
1551
|
+
const resp = await sendDaemonRequest(projectRoot, {
|
|
1552
|
+
type: "stop_group",
|
|
1553
|
+
group_id: groupId,
|
|
1554
|
+
});
|
|
1555
|
+
if (outputJson) {
|
|
1556
|
+
console.log(JSON.stringify(resp?.data || {}, null, 2));
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
console.log(resp?.data?.reply || "Group stop requested");
|
|
1560
|
+
if (resp?.data?.group?.ok === false) process.exitCode = 1;
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
if (sub === "diagram") {
|
|
1565
|
+
const target = String(rest[1] || "").trim();
|
|
1566
|
+
if (!target) throw new Error("group diagram requires <alias|groupId>");
|
|
1567
|
+
const outputJson = rest.includes("--json");
|
|
1568
|
+
const format = rest.includes("--mermaid") ? "mermaid" : "ascii";
|
|
1569
|
+
const projectRoot = process.cwd();
|
|
1570
|
+
await ensureDaemonRunning(projectRoot);
|
|
1571
|
+
const resp = await sendDaemonRequest(projectRoot, {
|
|
1572
|
+
type: "group_diagram",
|
|
1573
|
+
alias: target,
|
|
1574
|
+
group_id: target,
|
|
1575
|
+
format,
|
|
1576
|
+
});
|
|
1577
|
+
if (outputJson) {
|
|
1578
|
+
console.log(JSON.stringify(resp?.data || {}, null, 2));
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
console.log(resp?.data?.reply || "Group diagram requested");
|
|
1582
|
+
if (resp?.data?.group?.diagram) {
|
|
1583
|
+
console.log(resp.data.group.diagram);
|
|
1584
|
+
}
|
|
1585
|
+
if (resp?.data?.group?.ok === false) process.exitCode = 1;
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
throw new Error("group requires templates|template|run|status|diagram|stop subcommand");
|
|
1590
|
+
} catch (err) {
|
|
1591
|
+
console.error(err.message || String(err));
|
|
1592
|
+
process.exitCode = 1;
|
|
1593
|
+
}
|
|
1594
|
+
})();
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1274
1597
|
if (cmd === "online") {
|
|
1275
1598
|
const sub = rest[0] || "";
|
|
1276
1599
|
if (!sub) {
|
package/src/code/agent.js
CHANGED
|
@@ -43,6 +43,14 @@ function normalizeLine(input = "") {
|
|
|
43
43
|
return String(input || "").trim();
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
function parseProbeMarkerCommand(input = "") {
|
|
47
|
+
const text = String(input || "").trim();
|
|
48
|
+
if (!text) return "";
|
|
49
|
+
// Accept only strict probe markers: "<prefix> <single-marker-token>".
|
|
50
|
+
const match = text.match(/^(?:\$ufoo|\/ufoo|ufoo)\s+([A-Za-z0-9][A-Za-z0-9._:-]{0,63})$/);
|
|
51
|
+
return match ? String(match[1] || "").trim() : "";
|
|
52
|
+
}
|
|
53
|
+
|
|
46
54
|
function parseJson(text = "") {
|
|
47
55
|
const raw = String(text || "").trim();
|
|
48
56
|
if (!raw) return {};
|
|
@@ -1209,16 +1217,18 @@ function runSingleCommand(line = "", workspaceRoot = process.cwd()) {
|
|
|
1209
1217
|
" help",
|
|
1210
1218
|
" exit|quit",
|
|
1211
1219
|
" ubus|/ubus",
|
|
1220
|
+
" bg|/bg <task>",
|
|
1212
1221
|
" resume <session-id>",
|
|
1213
1222
|
" tool <read|write|edit|bash> <args-json>",
|
|
1214
1223
|
" run <read|write|edit|bash> <args-json>",
|
|
1215
1224
|
].join("\n"),
|
|
1216
1225
|
};
|
|
1217
1226
|
}
|
|
1218
|
-
|
|
1227
|
+
const probeMarker = parseProbeMarkerCommand(text);
|
|
1228
|
+
if (probeMarker) {
|
|
1219
1229
|
return {
|
|
1220
1230
|
kind: "probe",
|
|
1221
|
-
|
|
1231
|
+
marker: probeMarker,
|
|
1222
1232
|
};
|
|
1223
1233
|
}
|
|
1224
1234
|
if (text === "ubus" || text === "/ubus") {
|
|
@@ -1226,6 +1236,26 @@ function runSingleCommand(line = "", workspaceRoot = process.cwd()) {
|
|
|
1226
1236
|
kind: "ubus",
|
|
1227
1237
|
};
|
|
1228
1238
|
}
|
|
1239
|
+
if (text === "bg" || text === "/bg") {
|
|
1240
|
+
return {
|
|
1241
|
+
kind: "error",
|
|
1242
|
+
output: "usage: bg <task>",
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
const bgMatch = text.match(/^(?:\/bg|bg)\s+(.+)$/i);
|
|
1246
|
+
if (bgMatch) {
|
|
1247
|
+
const task = String(bgMatch[1] || "").trim();
|
|
1248
|
+
if (!task) {
|
|
1249
|
+
return {
|
|
1250
|
+
kind: "error",
|
|
1251
|
+
output: "usage: bg <task>",
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
return {
|
|
1255
|
+
kind: "nl_bg",
|
|
1256
|
+
task,
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1229
1259
|
const resumeMatch = text.match(/^resume(?:\s+(.+))?$/i);
|
|
1230
1260
|
if (resumeMatch) {
|
|
1231
1261
|
const session = String(resumeMatch[1] || "").trim();
|
|
@@ -1345,6 +1375,8 @@ async function runUcodeCoreAgent({
|
|
|
1345
1375
|
});
|
|
1346
1376
|
return new Promise((resolve) => {
|
|
1347
1377
|
let chain = Promise.resolve();
|
|
1378
|
+
let backgroundSeq = 0;
|
|
1379
|
+
const backgroundRuns = new Map();
|
|
1348
1380
|
const subscriberId = String(process.env.UFOO_SUBSCRIBER_ID || "").trim();
|
|
1349
1381
|
const autoBusEnabled = shouldAutoConsumeBus(subscriberId);
|
|
1350
1382
|
let autoBusTimer = null;
|
|
@@ -1400,6 +1432,38 @@ async function runUcodeCoreAgent({
|
|
|
1400
1432
|
scheduleAutoBus();
|
|
1401
1433
|
}
|
|
1402
1434
|
|
|
1435
|
+
const startBackgroundTask = (task = "") => {
|
|
1436
|
+
backgroundSeq += 1;
|
|
1437
|
+
const jobId = `bg-${Date.now().toString(36)}-${backgroundSeq.toString(36)}`;
|
|
1438
|
+
const bgState = {
|
|
1439
|
+
workspaceRoot: state.workspaceRoot,
|
|
1440
|
+
provider: state.provider,
|
|
1441
|
+
model: state.model,
|
|
1442
|
+
engine: state.engine,
|
|
1443
|
+
context: state.context,
|
|
1444
|
+
nlMessages: Array.isArray(state.nlMessages) ? state.nlMessages.slice() : [],
|
|
1445
|
+
sessionId: "",
|
|
1446
|
+
timeoutMs: state.timeoutMs,
|
|
1447
|
+
jsonOutput: false,
|
|
1448
|
+
};
|
|
1449
|
+
const run = runNaturalLanguageTask(task, bgState)
|
|
1450
|
+
.then((nlResult) => {
|
|
1451
|
+
const summary = String(formatNlResult(nlResult, false) || "").trim();
|
|
1452
|
+
const title = nlResult && nlResult.ok ? "done" : "failed";
|
|
1453
|
+
stdout.write(`[${jobId}] ${title}: ${summary || "no summary"}\n`);
|
|
1454
|
+
printPrompt();
|
|
1455
|
+
})
|
|
1456
|
+
.catch((err) => {
|
|
1457
|
+
stdout.write(`[${jobId}] failed: ${err && err.message ? err.message : "background task failed"}\n`);
|
|
1458
|
+
printPrompt();
|
|
1459
|
+
})
|
|
1460
|
+
.finally(() => {
|
|
1461
|
+
backgroundRuns.delete(jobId);
|
|
1462
|
+
});
|
|
1463
|
+
backgroundRuns.set(jobId, run);
|
|
1464
|
+
return jobId;
|
|
1465
|
+
};
|
|
1466
|
+
|
|
1403
1467
|
const handleLine = async (line) => {
|
|
1404
1468
|
const runtimeWorkspace = String(state.workspaceRoot || workspaceRoot || process.cwd());
|
|
1405
1469
|
const result = runSingleCommand(line, runtimeWorkspace);
|
|
@@ -1407,7 +1471,10 @@ async function runUcodeCoreAgent({
|
|
|
1407
1471
|
rl.close();
|
|
1408
1472
|
return;
|
|
1409
1473
|
}
|
|
1410
|
-
if (result.kind === "
|
|
1474
|
+
if (result.kind === "probe") {
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
if (result.kind === "help" || result.kind === "tool" || result.kind === "error") {
|
|
1411
1478
|
stdout.write(`${result.output}\n`);
|
|
1412
1479
|
}
|
|
1413
1480
|
if (result.kind === "ubus") {
|
|
@@ -1442,6 +1509,10 @@ async function runUcodeCoreAgent({
|
|
|
1442
1509
|
stdout.write(`Resumed session ${resumed.sessionId} (${resumed.restoredMessages} messages).\n`);
|
|
1443
1510
|
}
|
|
1444
1511
|
}
|
|
1512
|
+
if (result.kind === "nl_bg") {
|
|
1513
|
+
const jobId = startBackgroundTask(result.task);
|
|
1514
|
+
stdout.write(`[${jobId}] started in background.\n`);
|
|
1515
|
+
}
|
|
1445
1516
|
if (result.kind === "nl") {
|
|
1446
1517
|
let streamBuffer = null;
|
|
1447
1518
|
let streamedVisible = false;
|
package/src/code/tui.js
CHANGED
|
@@ -468,7 +468,7 @@ function runUcodeTui({
|
|
|
468
468
|
} = {}) {
|
|
469
469
|
return new Promise((resolve) => {
|
|
470
470
|
const blessed = require("blessed");
|
|
471
|
-
const {
|
|
471
|
+
const { execFileSync } = require("child_process");
|
|
472
472
|
const { createChatLayout } = require("../chat/layout");
|
|
473
473
|
const { computeDashboardContent } = require("../chat/dashboardView");
|
|
474
474
|
const { escapeBlessed, stripBlessedTags } = require("../chat/text");
|
|
@@ -490,6 +490,8 @@ function runUcodeTui({
|
|
|
490
490
|
let agentListWindowStart = 0;
|
|
491
491
|
let agentSelectionMode = false;
|
|
492
492
|
let pendingTask = null;
|
|
493
|
+
const backgroundTasks = new Map();
|
|
494
|
+
let backgroundSeq = 0;
|
|
493
495
|
const logRenderState = { inCodeBlock: false };
|
|
494
496
|
const inputHistory = [];
|
|
495
497
|
let historyIndex = -1;
|
|
@@ -900,12 +902,27 @@ function runUcodeTui({
|
|
|
900
902
|
};
|
|
901
903
|
|
|
902
904
|
const updateStatus = (message = "", type = "thinking", options = {}) => {
|
|
905
|
+
const getBackgroundSuffix = () => {
|
|
906
|
+
if (!backgroundTasks || backgroundTasks.size === 0) return "";
|
|
907
|
+
let running = 0;
|
|
908
|
+
let done = 0;
|
|
909
|
+
let failed = 0;
|
|
910
|
+
for (const task of backgroundTasks.values()) {
|
|
911
|
+
const status = String(task && task.status || "").trim().toLowerCase();
|
|
912
|
+
if (status === "running") running += 1;
|
|
913
|
+
else if (status === "done") done += 1;
|
|
914
|
+
else if (status === "failed") failed += 1;
|
|
915
|
+
}
|
|
916
|
+
const total = running + done + failed;
|
|
917
|
+
if (total <= 0) return "";
|
|
918
|
+
return ` · BG ${running}/${done}/${failed}`;
|
|
919
|
+
};
|
|
903
920
|
if (statusInterval) {
|
|
904
921
|
clearInterval(statusInterval);
|
|
905
922
|
statusInterval = null;
|
|
906
923
|
}
|
|
907
924
|
if (!message) {
|
|
908
|
-
statusLine.setContent(
|
|
925
|
+
statusLine.setContent(escapeBlessed(`UCODE · Ready${getBackgroundSuffix()}`));
|
|
909
926
|
screen.render();
|
|
910
927
|
return;
|
|
911
928
|
}
|
|
@@ -918,7 +935,7 @@ function runUcodeTui({
|
|
|
918
935
|
const timerText = showTimer
|
|
919
936
|
? ` (${formatPendingElapsed(Date.now() - startedAt)},esc cancel)`
|
|
920
937
|
: "";
|
|
921
|
-
statusLine.setContent(escapeBlessed(`${indicator} ${message}${timerText}`));
|
|
938
|
+
statusLine.setContent(escapeBlessed(`${indicator} ${message}${timerText}${getBackgroundSuffix()}`));
|
|
922
939
|
statusIndex += 1;
|
|
923
940
|
screen.render();
|
|
924
941
|
};
|
|
@@ -1082,7 +1099,7 @@ function runUcodeTui({
|
|
|
1082
1099
|
if (isBusMessage && targetAgent) {
|
|
1083
1100
|
updateStatus("Sending message...", "typing");
|
|
1084
1101
|
try {
|
|
1085
|
-
|
|
1102
|
+
execFileSync("ufoo", ["bus", "send", targetAgent, actualLine], {
|
|
1086
1103
|
cwd: workspaceRoot,
|
|
1087
1104
|
encoding: "utf8",
|
|
1088
1105
|
});
|
|
@@ -1118,7 +1135,10 @@ function runUcodeTui({
|
|
|
1118
1135
|
}, payload);
|
|
1119
1136
|
return;
|
|
1120
1137
|
}
|
|
1121
|
-
if (result.kind === "
|
|
1138
|
+
if (result.kind === "probe") {
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
if (result.kind === "help" || result.kind === "error") {
|
|
1122
1142
|
logText(result.output || "");
|
|
1123
1143
|
return;
|
|
1124
1144
|
}
|
|
@@ -1166,6 +1186,54 @@ function runUcodeTui({
|
|
|
1166
1186
|
return;
|
|
1167
1187
|
}
|
|
1168
1188
|
|
|
1189
|
+
if (result.kind === "nl_bg") {
|
|
1190
|
+
backgroundSeq += 1;
|
|
1191
|
+
const jobId = `bg-${Date.now().toString(36)}-${backgroundSeq.toString(36)}`;
|
|
1192
|
+
const taskRecord = {
|
|
1193
|
+
id: jobId,
|
|
1194
|
+
task: result.task,
|
|
1195
|
+
status: "running",
|
|
1196
|
+
startedAt: Date.now(),
|
|
1197
|
+
summary: "",
|
|
1198
|
+
};
|
|
1199
|
+
backgroundTasks.set(jobId, taskRecord);
|
|
1200
|
+
updateStatus("", "none");
|
|
1201
|
+
logText(`[${jobId}] started in background.`);
|
|
1202
|
+
|
|
1203
|
+
const bgState = {
|
|
1204
|
+
workspaceRoot: state.workspaceRoot,
|
|
1205
|
+
provider: state.provider,
|
|
1206
|
+
model: state.model,
|
|
1207
|
+
engine: state.engine,
|
|
1208
|
+
context: state.context,
|
|
1209
|
+
nlMessages: Array.isArray(state.nlMessages) ? state.nlMessages.slice() : [],
|
|
1210
|
+
sessionId: "",
|
|
1211
|
+
timeoutMs: state.timeoutMs,
|
|
1212
|
+
jsonOutput: false,
|
|
1213
|
+
};
|
|
1214
|
+
|
|
1215
|
+
Promise.resolve()
|
|
1216
|
+
.then(() => runNaturalLanguageTask(result.task, bgState))
|
|
1217
|
+
.then((nlResult) => {
|
|
1218
|
+
taskRecord.status = nlResult && nlResult.ok ? "done" : "failed";
|
|
1219
|
+
taskRecord.finishedAt = Date.now();
|
|
1220
|
+
taskRecord.summary = String(formatNlResult(nlResult, false) || "").trim();
|
|
1221
|
+
const title = taskRecord.status === "done" ? "done" : "failed";
|
|
1222
|
+
logText(`[${jobId}] ${title}: ${taskRecord.summary || "no summary"}`);
|
|
1223
|
+
})
|
|
1224
|
+
.catch((err) => {
|
|
1225
|
+
taskRecord.status = "failed";
|
|
1226
|
+
taskRecord.finishedAt = Date.now();
|
|
1227
|
+
taskRecord.summary = err && err.message ? String(err.message) : "background task failed";
|
|
1228
|
+
logText(`[${jobId}] failed: ${taskRecord.summary}`);
|
|
1229
|
+
})
|
|
1230
|
+
.finally(() => {
|
|
1231
|
+
updateStatus("", "none");
|
|
1232
|
+
screen.render();
|
|
1233
|
+
});
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1169
1237
|
if (result.kind === "nl") {
|
|
1170
1238
|
const statusMessages = [
|
|
1171
1239
|
"Thinking...",
|