metheus-governance-mcp-cli 0.2.113 → 0.2.114
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 +10 -0
- package/cli.mjs +632 -2
- package/lib/bot-commands.mjs +19 -37
- package/lib/selftest-bot-commands.mjs +4 -12
- package/lib/selftest-runner-scenarios.mjs +49 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -388,11 +388,20 @@ Commands:
|
|
|
388
388
|
|
|
389
389
|
```bash
|
|
390
390
|
metheus-governance-mcp-cli runner list
|
|
391
|
+
metheus-governance-mcp-cli runner route add
|
|
392
|
+
metheus-governance-mcp-cli runner route edit --route-name telegram-monitor
|
|
393
|
+
metheus-governance-mcp-cli runner route remove --route-name telegram-monitor
|
|
391
394
|
metheus-governance-mcp-cli runner show --route-name telegram-monitor
|
|
392
395
|
metheus-governance-mcp-cli runner once --route-name telegram-monitor
|
|
393
396
|
metheus-governance-mcp-cli runner start --route-name telegram-monitor
|
|
394
397
|
```
|
|
395
398
|
|
|
399
|
+
Route management:
|
|
400
|
+
- `runner route add` creates one executable route by selecting the project, provider, role, server bot, and project chat destination in order.
|
|
401
|
+
- `runner route edit` updates one stored route. Use it when the destination, role, or server bot for an existing route changes.
|
|
402
|
+
- `runner route remove` deletes one stored route from `~/.metheus/bot-runner.json`.
|
|
403
|
+
- `runner list` and `runner route list` are equivalent. They show which routes are executable and how `--bot-name` would resolve.
|
|
404
|
+
|
|
396
405
|
Recommended operational path:
|
|
397
406
|
|
|
398
407
|
```bash
|
|
@@ -405,6 +414,7 @@ What `runner list` means:
|
|
|
405
414
|
- `server_bot_name_source=route_config` means the route file already stores the name directly
|
|
406
415
|
- `server_bot_name_source=telegram_env_lookup` means the CLI resolved the name from the local Telegram bot file
|
|
407
416
|
- `route_alias_by_server_bot_name` is a convenience selector, not a separate execution target
|
|
417
|
+
- `run_once_by_server_bot_name` in `runner show` or docs means "find the one enabled route that matches this server bot name, then run that route"
|
|
408
418
|
|
|
409
419
|
Debug/selection overrides:
|
|
410
420
|
|
package/cli.mjs
CHANGED
|
@@ -91,7 +91,15 @@ import {
|
|
|
91
91
|
writeSelftestReport,
|
|
92
92
|
} from "./lib/selftest-support.mjs";
|
|
93
93
|
import { runSelftestBotCommands } from "./lib/selftest-bot-commands.mjs";
|
|
94
|
-
import {
|
|
94
|
+
import {
|
|
95
|
+
createPrompter,
|
|
96
|
+
promptChoice,
|
|
97
|
+
promptConfirmChoice,
|
|
98
|
+
promptLine,
|
|
99
|
+
promptRequiredLine,
|
|
100
|
+
shouldRenderPromptChrome,
|
|
101
|
+
runBotCommand,
|
|
102
|
+
} from "./lib/bot-commands.mjs";
|
|
95
103
|
import { handleLocalBotMessageToolCall as handleLocalBotMessageToolCallImpl } from "./lib/local-tool-shims.mjs";
|
|
96
104
|
import { handleLocalProjectToolDispatch as handleLocalProjectToolDispatchImpl } from "./lib/local-project-dispatch.mjs";
|
|
97
105
|
import {
|
|
@@ -255,6 +263,10 @@ function printUsage() {
|
|
|
255
263
|
` ${cmd} selftest [--json <true|false>]`,
|
|
256
264
|
` ${cmd} local-bot-bridge [--client <gpt|claude|gemini|sample>] [--cwd <path>] [--model <name>] [--permission-mode <read_only|workspace_write|danger_full_access>] [--reasoning-effort <low|medium|high>]`,
|
|
257
265
|
` ${cmd} runner list [--json <true|false>]`,
|
|
266
|
+
` ${cmd} runner route list [--json <true|false>]`,
|
|
267
|
+
` ${cmd} runner route add [--project-id <uuid>] [--provider <telegram|slack|kakaotalk>] [--role <monitor|review|worker|approval>] [--bot-name <server_name> | --bot-id <uuid>] [--destination-label <label> | --destination-id <uuid>] [--poll-interval-ms <n>] [--enabled <true|false>]`,
|
|
268
|
+
` ${cmd} runner route edit [--route-name <name> | --bot-name <server_name> | --bot-id <uuid>]`,
|
|
269
|
+
` ${cmd} runner route remove [--route-name <name> | --bot-name <server_name> | --bot-id <uuid>]`,
|
|
258
270
|
` ${cmd} runner show [--route-name <name> | --bot-name <server_name> | --bot-id <uuid>] [--json <true|false>]`,
|
|
259
271
|
` ${cmd} runner once [--route-name <name> | --bot-name <server_name when one enabled route matches> | --bot-id <uuid when one enabled route matches>] [--project-id <uuid>] [--provider <telegram|slack|kakaotalk>] [--role <monitor|review|worker|approval>] [--role-profile <name>] [--mentions-only <true|false>] [--reply-to-bot-messages <true|false>] [--direct-messages <true|false>] [--ignore-edited-messages <true|false>] [--dry-run-delivery <true|false>] [--context-comments <n>] [--archive-replies <true|false>]`,
|
|
260
272
|
` ${cmd} runner start [--route-name <name> | --bot-name <server_name when one enabled route matches> | --bot-id <uuid when one enabled route matches>] [--project-id <uuid>] [--provider <telegram|slack|kakaotalk>] [--role <monitor|review|worker|approval>] [--role-profile <name>] [--mentions-only <true|false>] [--reply-to-bot-messages <true|false>] [--direct-messages <true|false>] [--ignore-edited-messages <true|false>] [--dry-run-delivery <true|false>] [--poll-interval-ms <n>] [--context-comments <n>] [--archive-replies <true|false>]`,
|
|
@@ -2661,6 +2673,595 @@ function buildRunnerRouteListRows() {
|
|
|
2661
2673
|
});
|
|
2662
2674
|
}
|
|
2663
2675
|
|
|
2676
|
+
function slugifyRunnerRouteSegment(rawValue) {
|
|
2677
|
+
return String(rawValue || "")
|
|
2678
|
+
.trim()
|
|
2679
|
+
.toLowerCase()
|
|
2680
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
2681
|
+
.replace(/^-+|-+$/g, "");
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
function buildRunnerRouteNameSuggestion({ provider = "", role = "", botName = "" }, existingNames = []) {
|
|
2685
|
+
const segments = [
|
|
2686
|
+
slugifyRunnerRouteSegment(provider),
|
|
2687
|
+
slugifyRunnerRouteSegment(role),
|
|
2688
|
+
slugifyRunnerRouteSegment(botName),
|
|
2689
|
+
].filter(Boolean);
|
|
2690
|
+
const base = segments.join("-") || "runner-route";
|
|
2691
|
+
const existing = new Set(ensureArray(existingNames).map((name) => String(name || "").trim().toLowerCase()).filter(Boolean));
|
|
2692
|
+
if (!existing.has(base)) {
|
|
2693
|
+
return base;
|
|
2694
|
+
}
|
|
2695
|
+
let suffix = 2;
|
|
2696
|
+
while (existing.has(`${base}-${suffix}`)) {
|
|
2697
|
+
suffix += 1;
|
|
2698
|
+
}
|
|
2699
|
+
return `${base}-${suffix}`;
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
function upsertRunnerRouteConfig(config, route) {
|
|
2703
|
+
const normalizedConfig = safeObject(config);
|
|
2704
|
+
const normalizedRoute = normalizeRunnerRoute(route);
|
|
2705
|
+
const routeName = String(normalizedRoute.name || "").trim();
|
|
2706
|
+
if (!routeName) {
|
|
2707
|
+
throw new Error("route name is required");
|
|
2708
|
+
}
|
|
2709
|
+
const routes = ensureArray(normalizedConfig.routes).map((item) => normalizeRunnerRoute(item));
|
|
2710
|
+
const index = routes.findIndex((item) => String(item.name || "").trim().toLowerCase() === routeName.toLowerCase());
|
|
2711
|
+
if (index >= 0) {
|
|
2712
|
+
routes[index] = normalizedRoute;
|
|
2713
|
+
} else {
|
|
2714
|
+
routes.push(normalizedRoute);
|
|
2715
|
+
}
|
|
2716
|
+
return {
|
|
2717
|
+
...normalizedConfig,
|
|
2718
|
+
routes,
|
|
2719
|
+
};
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
function removeRunnerRouteFromConfig(config, routeName) {
|
|
2723
|
+
const normalizedConfig = safeObject(config);
|
|
2724
|
+
const target = String(routeName || "").trim().toLowerCase();
|
|
2725
|
+
return {
|
|
2726
|
+
...normalizedConfig,
|
|
2727
|
+
routes: ensureArray(normalizedConfig.routes)
|
|
2728
|
+
.map((route) => normalizeRunnerRoute(route))
|
|
2729
|
+
.filter((route) => String(route.name || "").trim().toLowerCase() !== target),
|
|
2730
|
+
};
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
function formatRunnerRouteChoiceLabel(route, telegramEntries = []) {
|
|
2734
|
+
const normalizedRoute = normalizeRunnerRoute(route);
|
|
2735
|
+
const resolvedName = resolveConfiguredRunnerRouteServerBotName(normalizedRoute, telegramEntries);
|
|
2736
|
+
const routeName = normalizedRoute.name || runnerRouteKey(normalizedRoute);
|
|
2737
|
+
const role = normalizedRoute.role || "-";
|
|
2738
|
+
const botName = resolvedName || normalizedRoute.botName || normalizedRoute.botID || "-";
|
|
2739
|
+
const destination = normalizedRoute.destinationLabel || normalizedRoute.destinationID || "-";
|
|
2740
|
+
return `${routeName}${normalizedRoute.enabled ? "" : " [disabled]"} - ${normalizedRoute.provider || "-"} | ${role} | ${botName} | ${destination}`;
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
async function selectRunnerManagementRoute(ui, flags, { title = "Select runner route" } = {}) {
|
|
2744
|
+
const config = loadBotRunnerConfig({ persistIfNeeded: true });
|
|
2745
|
+
const routes = ensureArray(config.routes).map((route) => normalizeRunnerRoute(route));
|
|
2746
|
+
if (!routes.length) {
|
|
2747
|
+
throw new Error(`No runner route is configured in ${config.filePath}.`);
|
|
2748
|
+
}
|
|
2749
|
+
const selectionFilters = {
|
|
2750
|
+
"route-name": flags["route-name"],
|
|
2751
|
+
"bot-name": flags["bot-name"],
|
|
2752
|
+
"bot-id": flags["bot-id"],
|
|
2753
|
+
};
|
|
2754
|
+
const filtered = routes.filter((route) => {
|
|
2755
|
+
if (selectionFilters["route-name"] && !matchesRunnerRouteText(route.name, selectionFilters["route-name"])) return false;
|
|
2756
|
+
const candidateBotName = resolveConfiguredRunnerRouteServerBotName(route, ensureArray(readTelegramEnvState().entries));
|
|
2757
|
+
if (selectionFilters["bot-name"] && !matchesRunnerRouteText(candidateBotName, selectionFilters["bot-name"])) return false;
|
|
2758
|
+
if (selectionFilters["bot-id"] && String(route.botID || "").trim() !== String(selectionFilters["bot-id"] || "").trim()) return false;
|
|
2759
|
+
return true;
|
|
2760
|
+
});
|
|
2761
|
+
if (filtered.length === 1) {
|
|
2762
|
+
return {
|
|
2763
|
+
route: filtered[0],
|
|
2764
|
+
config,
|
|
2765
|
+
};
|
|
2766
|
+
}
|
|
2767
|
+
if (filtered.length > 1) {
|
|
2768
|
+
const matches = filtered.map((route) => route.name || runnerRouteKey(route)).join(", ");
|
|
2769
|
+
throw new Error(`Multiple runner routes matched the provided selectors. Narrow with --route-name. Matches: ${matches}`);
|
|
2770
|
+
}
|
|
2771
|
+
const telegramEntries = ensureArray(readTelegramEnvState().entries);
|
|
2772
|
+
const selected = await promptChoice(
|
|
2773
|
+
ui,
|
|
2774
|
+
title,
|
|
2775
|
+
routes.map((route) => ({
|
|
2776
|
+
value: route.name || runnerRouteKey(route),
|
|
2777
|
+
label: route.name || runnerRouteKey(route),
|
|
2778
|
+
description: formatRunnerRouteChoiceLabel(route, telegramEntries).replace(/^[^-]+ - /, ""),
|
|
2779
|
+
})),
|
|
2780
|
+
{ defaultIndex: 0 },
|
|
2781
|
+
);
|
|
2782
|
+
const selectedName = String(selected?.value || "").trim().toLowerCase();
|
|
2783
|
+
const matchedRoute = routes.find((route) => String(route.name || runnerRouteKey(route)).trim().toLowerCase() === selectedName);
|
|
2784
|
+
if (!matchedRoute) {
|
|
2785
|
+
throw new Error("selected runner route was not found");
|
|
2786
|
+
}
|
|
2787
|
+
return {
|
|
2788
|
+
route: matchedRoute,
|
|
2789
|
+
config,
|
|
2790
|
+
};
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2793
|
+
async function selectRunnerRouteProvider(ui, flags, currentProvider = "") {
|
|
2794
|
+
const flagged = normalizeBotProvider(flags.provider || "");
|
|
2795
|
+
if (flagged) {
|
|
2796
|
+
return flagged;
|
|
2797
|
+
}
|
|
2798
|
+
if (currentProvider) {
|
|
2799
|
+
return currentProvider;
|
|
2800
|
+
}
|
|
2801
|
+
const options = PROVIDER_ENV_ORDER.map((provider) => ({
|
|
2802
|
+
value: provider,
|
|
2803
|
+
label: providerEnvConfig(provider).label,
|
|
2804
|
+
description: summarizeProviderSupport(provider),
|
|
2805
|
+
}));
|
|
2806
|
+
const selected = await promptChoice(ui, "Select runner route provider", options, { defaultIndex: 0 });
|
|
2807
|
+
return normalizeBotProvider(selected?.value || "telegram") || "telegram";
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
async function resolveRunnerRouteProjectID(ui, flags, config, currentProjectID = "") {
|
|
2811
|
+
const flagged = String(flags["project-id"] || "").trim();
|
|
2812
|
+
if (flagged) {
|
|
2813
|
+
if (!isUUID(flagged)) {
|
|
2814
|
+
throw new Error("project_id must be a valid UUID");
|
|
2815
|
+
}
|
|
2816
|
+
return flagged;
|
|
2817
|
+
}
|
|
2818
|
+
if (currentProjectID && isUUID(currentProjectID)) {
|
|
2819
|
+
return currentProjectID;
|
|
2820
|
+
}
|
|
2821
|
+
const projectMappings = safeObject(config.projectMappings);
|
|
2822
|
+
const options = Object.entries(projectMappings)
|
|
2823
|
+
.filter(([projectID]) => isUUID(projectID))
|
|
2824
|
+
.map(([projectID, rawMapping]) => ({
|
|
2825
|
+
value: projectID,
|
|
2826
|
+
label: projectID,
|
|
2827
|
+
description: String(safeObject(rawMapping).workspaceDir || "").trim() || "-",
|
|
2828
|
+
}));
|
|
2829
|
+
if (options.length === 1) {
|
|
2830
|
+
return options[0].value;
|
|
2831
|
+
}
|
|
2832
|
+
if (options.length > 1) {
|
|
2833
|
+
const selected = await promptChoice(ui, "Select runner route project", options, { defaultIndex: 0 });
|
|
2834
|
+
return String(selected?.value || "").trim();
|
|
2835
|
+
}
|
|
2836
|
+
while (true) {
|
|
2837
|
+
const projectID = String(await promptRequiredLine(ui, "Project UUID", "") || "").trim();
|
|
2838
|
+
if (isUUID(projectID)) return projectID;
|
|
2839
|
+
process.stdout.write("Project UUID must be valid.\n");
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2843
|
+
async function selectRunnerRole(ui, flags, currentRole = "") {
|
|
2844
|
+
const flagged = normalizeBotRole(flags.role || "");
|
|
2845
|
+
if (flagged) {
|
|
2846
|
+
return flagged;
|
|
2847
|
+
}
|
|
2848
|
+
const roleOptions = ["monitor", "review", "worker", "approval"].map((role) => ({
|
|
2849
|
+
value: role,
|
|
2850
|
+
label: role,
|
|
2851
|
+
description: role === "monitor"
|
|
2852
|
+
? "inbound triage and monitoring"
|
|
2853
|
+
: role === "review"
|
|
2854
|
+
? "review and analysis"
|
|
2855
|
+
: role === "worker"
|
|
2856
|
+
? "workspace execution"
|
|
2857
|
+
: "final approval actions",
|
|
2858
|
+
}));
|
|
2859
|
+
const defaultIndex = Math.max(0, roleOptions.findIndex((item) => item.value === currentRole));
|
|
2860
|
+
const selected = await promptChoice(ui, "Select runner route role", roleOptions, { defaultIndex });
|
|
2861
|
+
return normalizeBotRole(selected?.value || currentRole) || "monitor";
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2864
|
+
async function resolveRunnerCommandToken(baseURL, timeoutSeconds) {
|
|
2865
|
+
const resolved = await resolveAccessTokenForCommand(baseURL || DEFAULT_SITE_URL, timeoutSeconds);
|
|
2866
|
+
if (!resolved.token) {
|
|
2867
|
+
throw new Error("auth token missing; run auth login first");
|
|
2868
|
+
}
|
|
2869
|
+
return {
|
|
2870
|
+
siteBaseURL: normalizeSiteBaseURL(baseURL || DEFAULT_SITE_URL),
|
|
2871
|
+
token: resolved.token,
|
|
2872
|
+
};
|
|
2873
|
+
}
|
|
2874
|
+
|
|
2875
|
+
async function selectRunnerServerBotProfile(ui, { provider, role, flags, currentBotID = "", currentBotName = "" }) {
|
|
2876
|
+
const flaggedBotID = String(flags["bot-id"] || "").trim();
|
|
2877
|
+
const flaggedBotName = String(flags["bot-name"] || "").trim();
|
|
2878
|
+
const timeoutSeconds = intFromRaw(flags["timeout-seconds"], 15) || 15;
|
|
2879
|
+
const { siteBaseURL, token } = await resolveRunnerCommandToken(flags["base-url"], timeoutSeconds);
|
|
2880
|
+
const bots = await listUserBotsForRunner({
|
|
2881
|
+
siteBaseURL,
|
|
2882
|
+
token,
|
|
2883
|
+
timeoutSeconds,
|
|
2884
|
+
});
|
|
2885
|
+
const candidates = ensureArray(bots)
|
|
2886
|
+
.filter((bot) => bot.isActive && bot.provider === provider && bot.role === role);
|
|
2887
|
+
if (!candidates.length) {
|
|
2888
|
+
throw new Error(`No active ${providerEnvConfig(provider).label} server bot exists for role "${role}".`);
|
|
2889
|
+
}
|
|
2890
|
+
const resolveChoice = (bot) => ({
|
|
2891
|
+
name: String(bot.name || "").trim(),
|
|
2892
|
+
id: String(bot.id || "").trim(),
|
|
2893
|
+
});
|
|
2894
|
+
if (flaggedBotID) {
|
|
2895
|
+
const matched = candidates.find((bot) => bot.id === flaggedBotID);
|
|
2896
|
+
if (!matched) {
|
|
2897
|
+
throw new Error(`No active ${providerEnvConfig(provider).label} server bot matched bot-id ${flaggedBotID} for role "${role}".`);
|
|
2898
|
+
}
|
|
2899
|
+
return resolveChoice(matched);
|
|
2900
|
+
}
|
|
2901
|
+
if (flaggedBotName) {
|
|
2902
|
+
const matchedByName = candidates.filter((bot) => matchesRunnerRouteText(bot.name, flaggedBotName));
|
|
2903
|
+
if (matchedByName.length === 1) {
|
|
2904
|
+
return resolveChoice(matchedByName[0]);
|
|
2905
|
+
}
|
|
2906
|
+
if (matchedByName.length > 1) {
|
|
2907
|
+
throw new Error(`Multiple active ${providerEnvConfig(provider).label} server bots matched "${flaggedBotName}" for role "${role}". Use --bot-id instead.`);
|
|
2908
|
+
}
|
|
2909
|
+
throw new Error(`No active ${providerEnvConfig(provider).label} server bot matched "${flaggedBotName}" for role "${role}".`);
|
|
2910
|
+
}
|
|
2911
|
+
const currentIndex = Math.max(0, candidates.findIndex((bot) => (
|
|
2912
|
+
(currentBotID && bot.id === currentBotID)
|
|
2913
|
+
|| (currentBotName && matchesRunnerRouteText(bot.name, currentBotName))
|
|
2914
|
+
)));
|
|
2915
|
+
if (candidates.length === 1) {
|
|
2916
|
+
return resolveChoice(candidates[0]);
|
|
2917
|
+
}
|
|
2918
|
+
const selected = await promptChoice(
|
|
2919
|
+
ui,
|
|
2920
|
+
`Select server bot for role "${role}"`,
|
|
2921
|
+
candidates.map((bot) => ({
|
|
2922
|
+
value: bot.id,
|
|
2923
|
+
label: String(bot.name || "").trim() || bot.id,
|
|
2924
|
+
description: `server_bot_id:${bot.id}`,
|
|
2925
|
+
})),
|
|
2926
|
+
{ defaultIndex: currentIndex },
|
|
2927
|
+
);
|
|
2928
|
+
const matched = candidates.find((bot) => bot.id === String(selected?.value || "").trim());
|
|
2929
|
+
if (!matched) {
|
|
2930
|
+
throw new Error("selected server bot was not found");
|
|
2931
|
+
}
|
|
2932
|
+
return resolveChoice(matched);
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
async function selectRunnerDestination(ui, { projectID, provider, flags, currentDestinationID = "", currentDestinationLabel = "" }) {
|
|
2936
|
+
const timeoutSeconds = intFromRaw(flags["timeout-seconds"], 15) || 15;
|
|
2937
|
+
const { siteBaseURL, token } = await resolveRunnerCommandToken(flags["base-url"], timeoutSeconds);
|
|
2938
|
+
const destinations = await listProjectChatDestinations({
|
|
2939
|
+
siteBaseURL,
|
|
2940
|
+
projectID,
|
|
2941
|
+
token,
|
|
2942
|
+
timeoutSeconds,
|
|
2943
|
+
});
|
|
2944
|
+
const candidates = ensureArray(destinations)
|
|
2945
|
+
.map((item) => normalizeChatDestination(item))
|
|
2946
|
+
.filter((item) => item.isActive && item.provider === provider);
|
|
2947
|
+
const flaggedID = String(flags["destination-id"] || "").trim();
|
|
2948
|
+
const flaggedLabel = String(flags["destination-label"] || "").trim();
|
|
2949
|
+
if (flaggedID) {
|
|
2950
|
+
const matched = candidates.find((item) => item.id === flaggedID);
|
|
2951
|
+
if (!matched) {
|
|
2952
|
+
throw new Error(`No active ${providerEnvConfig(provider).label} destination matched destination-id ${flaggedID}.`);
|
|
2953
|
+
}
|
|
2954
|
+
return { id: matched.id, label: matched.label || matched.id };
|
|
2955
|
+
}
|
|
2956
|
+
if (flaggedLabel) {
|
|
2957
|
+
const matchedByLabel = candidates.filter((item) => matchesRunnerRouteText(item.label, flaggedLabel));
|
|
2958
|
+
if (matchedByLabel.length === 1) {
|
|
2959
|
+
return { id: matchedByLabel[0].id, label: matchedByLabel[0].label || matchedByLabel[0].id };
|
|
2960
|
+
}
|
|
2961
|
+
if (matchedByLabel.length > 1) {
|
|
2962
|
+
throw new Error(`Multiple active ${providerEnvConfig(provider).label} destinations matched "${flaggedLabel}". Use --destination-id instead.`);
|
|
2963
|
+
}
|
|
2964
|
+
throw new Error(`No active ${providerEnvConfig(provider).label} destination matched "${flaggedLabel}".`);
|
|
2965
|
+
}
|
|
2966
|
+
const currentIndex = Math.max(0, candidates.findIndex((item) => (
|
|
2967
|
+
(currentDestinationID && item.id === currentDestinationID)
|
|
2968
|
+
|| (currentDestinationLabel && matchesRunnerRouteText(item.label, currentDestinationLabel))
|
|
2969
|
+
)));
|
|
2970
|
+
if (candidates.length === 1) {
|
|
2971
|
+
return { id: candidates[0].id, label: candidates[0].label || candidates[0].id };
|
|
2972
|
+
}
|
|
2973
|
+
if (!candidates.length) {
|
|
2974
|
+
throw new Error(`No active ${providerEnvConfig(provider).label} destination exists for project ${projectID}.`);
|
|
2975
|
+
}
|
|
2976
|
+
const selected = await promptChoice(
|
|
2977
|
+
ui,
|
|
2978
|
+
"Select project chat destination",
|
|
2979
|
+
candidates.map((item) => ({
|
|
2980
|
+
value: item.id,
|
|
2981
|
+
label: item.label || item.id,
|
|
2982
|
+
description: `chat_id:${item.chatID}`,
|
|
2983
|
+
})),
|
|
2984
|
+
{ defaultIndex: currentIndex },
|
|
2985
|
+
);
|
|
2986
|
+
const matched = candidates.find((item) => item.id === String(selected?.value || "").trim());
|
|
2987
|
+
if (!matched) {
|
|
2988
|
+
throw new Error("selected destination was not found");
|
|
2989
|
+
}
|
|
2990
|
+
return { id: matched.id, label: matched.label || matched.id };
|
|
2991
|
+
}
|
|
2992
|
+
|
|
2993
|
+
function buildRunnerRoutePayload({
|
|
2994
|
+
currentRoute = null,
|
|
2995
|
+
name = "",
|
|
2996
|
+
enabled = true,
|
|
2997
|
+
projectID = "",
|
|
2998
|
+
provider = "",
|
|
2999
|
+
role = "",
|
|
3000
|
+
roleProfile = "",
|
|
3001
|
+
serverBotName = "",
|
|
3002
|
+
serverBotID = "",
|
|
3003
|
+
destinationID = "",
|
|
3004
|
+
destinationLabel = "",
|
|
3005
|
+
pollIntervalMs = 5000,
|
|
3006
|
+
}) {
|
|
3007
|
+
const triggerPolicy = currentRoute
|
|
3008
|
+
? safeObject(currentRoute.triggerPolicy)
|
|
3009
|
+
: defaultRunnerTriggerPolicyForRole(roleProfile || role);
|
|
3010
|
+
const archivePolicy = currentRoute
|
|
3011
|
+
? safeObject(currentRoute.archivePolicy)
|
|
3012
|
+
: defaultRunnerArchivePolicyForRole(roleProfile || role);
|
|
3013
|
+
return normalizeRunnerRoute({
|
|
3014
|
+
...(currentRoute ? serializeRunnerRoute(currentRoute) : {}),
|
|
3015
|
+
name,
|
|
3016
|
+
enabled,
|
|
3017
|
+
project_id: projectID,
|
|
3018
|
+
provider,
|
|
3019
|
+
role,
|
|
3020
|
+
role_profile: roleProfile,
|
|
3021
|
+
server_bot_name: serverBotName,
|
|
3022
|
+
server_bot_id: serverBotID,
|
|
3023
|
+
destination_id: destinationID,
|
|
3024
|
+
destination_label: destinationLabel,
|
|
3025
|
+
trigger_policy: triggerPolicy,
|
|
3026
|
+
archive_policy: archivePolicy,
|
|
3027
|
+
poll_interval_ms: pollIntervalMs,
|
|
3028
|
+
});
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
async function runRunnerRouteAdd(flags) {
|
|
3032
|
+
const ui = createPrompter();
|
|
3033
|
+
try {
|
|
3034
|
+
if (shouldRenderPromptChrome(flags)) {
|
|
3035
|
+
ui.setFlow("RUNNER ROUTE ADD", "Create one executable runner route from server bot and destination");
|
|
3036
|
+
}
|
|
3037
|
+
const config = loadBotRunnerConfig({ persistIfNeeded: true });
|
|
3038
|
+
const provider = await selectRunnerRouteProvider(ui, flags, "");
|
|
3039
|
+
const projectID = await resolveRunnerRouteProjectID(ui, flags, config);
|
|
3040
|
+
const role = await selectRunnerRole(ui, flags, "");
|
|
3041
|
+
const botSelection = await selectRunnerServerBotProfile(ui, { provider, role, flags });
|
|
3042
|
+
const destination = await selectRunnerDestination(ui, {
|
|
3043
|
+
projectID,
|
|
3044
|
+
provider,
|
|
3045
|
+
flags,
|
|
3046
|
+
});
|
|
3047
|
+
const suggestedName = buildRunnerRouteNameSuggestion({
|
|
3048
|
+
provider,
|
|
3049
|
+
role,
|
|
3050
|
+
botName: botSelection.name,
|
|
3051
|
+
}, ensureArray(config.routes).map((route) => normalizeRunnerRoute(route).name));
|
|
3052
|
+
const routeName = String(flags.name || "").trim() || await promptRequiredLine(ui, "Runner route name", suggestedName);
|
|
3053
|
+
const pollIntervalMs = intFromRaw(flags["poll-interval-ms"], 0) || intFromRaw(await promptLine(ui, "Poll interval (ms)", "5000"), 5000);
|
|
3054
|
+
const enabled = !Object.prototype.hasOwnProperty.call(flags, "enabled")
|
|
3055
|
+
? true
|
|
3056
|
+
: boolFromRaw(flags.enabled, true);
|
|
3057
|
+
if (!Object.prototype.hasOwnProperty.call(flags, "enabled")) {
|
|
3058
|
+
const enabledChoice = await promptChoice(
|
|
3059
|
+
ui,
|
|
3060
|
+
"Route enabled state",
|
|
3061
|
+
[
|
|
3062
|
+
{ value: "enabled", label: "Enabled", description: "route will run immediately" },
|
|
3063
|
+
{ value: "disabled", label: "Disabled", description: "save the route but keep it inactive" },
|
|
3064
|
+
],
|
|
3065
|
+
{ defaultIndex: enabled ? 0 : 1 },
|
|
3066
|
+
);
|
|
3067
|
+
flags.enabled = enabledChoice?.value === "disabled" ? "false" : "true";
|
|
3068
|
+
}
|
|
3069
|
+
const nextRoute = buildRunnerRoutePayload({
|
|
3070
|
+
name: routeName,
|
|
3071
|
+
enabled: boolFromRaw(flags.enabled, true),
|
|
3072
|
+
projectID,
|
|
3073
|
+
provider,
|
|
3074
|
+
role,
|
|
3075
|
+
roleProfile: role,
|
|
3076
|
+
serverBotName: botSelection.name,
|
|
3077
|
+
serverBotID: botSelection.id,
|
|
3078
|
+
destinationID: destination.id,
|
|
3079
|
+
destinationLabel: destination.label,
|
|
3080
|
+
pollIntervalMs,
|
|
3081
|
+
});
|
|
3082
|
+
const saved = upsertRunnerRouteConfig(config, nextRoute);
|
|
3083
|
+
const filePath = saveBotRunnerConfig(saved, config.filePath);
|
|
3084
|
+
process.stdout.write(`Saved runner route "${nextRoute.name}" to ${filePath}\n`);
|
|
3085
|
+
} finally {
|
|
3086
|
+
ui.close();
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
|
|
3090
|
+
async function runRunnerRouteEdit(flags) {
|
|
3091
|
+
const ui = createPrompter();
|
|
3092
|
+
try {
|
|
3093
|
+
if (shouldRenderPromptChrome(flags)) {
|
|
3094
|
+
ui.setFlow("RUNNER ROUTE EDIT", "Update one executable runner route");
|
|
3095
|
+
}
|
|
3096
|
+
const selection = await selectRunnerManagementRoute(ui, flags, { title: "Select runner route to edit" });
|
|
3097
|
+
const currentRoute = normalizeRunnerRoute(selection.route);
|
|
3098
|
+
const config = selection.config;
|
|
3099
|
+
|
|
3100
|
+
const projectChange = await promptChoice(
|
|
3101
|
+
ui,
|
|
3102
|
+
"Project selection",
|
|
3103
|
+
[
|
|
3104
|
+
{ value: "keep", label: "Keep current project", description: currentRoute.projectID || "-" },
|
|
3105
|
+
{ value: "change", label: "Change project", description: "select another mapped project or enter a UUID" },
|
|
3106
|
+
],
|
|
3107
|
+
{ defaultIndex: 0 },
|
|
3108
|
+
);
|
|
3109
|
+
const projectID = projectChange?.value === "change"
|
|
3110
|
+
? await resolveRunnerRouteProjectID(ui, {}, config)
|
|
3111
|
+
: currentRoute.projectID;
|
|
3112
|
+
|
|
3113
|
+
const nameAction = await promptChoice(
|
|
3114
|
+
ui,
|
|
3115
|
+
"Runner route name",
|
|
3116
|
+
[
|
|
3117
|
+
{ value: "keep", label: "Keep current value", description: currentRoute.name || runnerRouteKey(currentRoute) },
|
|
3118
|
+
{ value: "change", label: "Change value", description: "rename this route" },
|
|
3119
|
+
],
|
|
3120
|
+
{ defaultIndex: 0 },
|
|
3121
|
+
);
|
|
3122
|
+
const routeName = nameAction?.value === "change"
|
|
3123
|
+
? await promptRequiredLine(ui, "Runner route name", currentRoute.name || "")
|
|
3124
|
+
: currentRoute.name;
|
|
3125
|
+
|
|
3126
|
+
const roleAction = await promptChoice(
|
|
3127
|
+
ui,
|
|
3128
|
+
"Runner role",
|
|
3129
|
+
[
|
|
3130
|
+
{ value: "keep", label: "Keep current role", description: currentRoute.role || "-" },
|
|
3131
|
+
{ value: "change", label: "Change role", description: "select a different execution role" },
|
|
3132
|
+
],
|
|
3133
|
+
{ defaultIndex: 0 },
|
|
3134
|
+
);
|
|
3135
|
+
const role = roleAction?.value === "change"
|
|
3136
|
+
? await selectRunnerRole(ui, {}, currentRoute.role)
|
|
3137
|
+
: currentRoute.role;
|
|
3138
|
+
|
|
3139
|
+
const botAction = await promptChoice(
|
|
3140
|
+
ui,
|
|
3141
|
+
"Server bot selection",
|
|
3142
|
+
[
|
|
3143
|
+
{ value: "keep", label: "Keep current server bot", description: currentRoute.botName || currentRoute.botID || "-" },
|
|
3144
|
+
{ value: "change", label: "Change server bot", description: "pick the server bot that should own this route" },
|
|
3145
|
+
],
|
|
3146
|
+
{ defaultIndex: role !== currentRoute.role ? 1 : 0 },
|
|
3147
|
+
);
|
|
3148
|
+
const botSelection = botAction?.value === "change" || role !== currentRoute.role
|
|
3149
|
+
? await selectRunnerServerBotProfile(ui, {
|
|
3150
|
+
provider: currentRoute.provider,
|
|
3151
|
+
role,
|
|
3152
|
+
flags: {},
|
|
3153
|
+
currentBotID: currentRoute.botID,
|
|
3154
|
+
currentBotName: currentRoute.botName,
|
|
3155
|
+
})
|
|
3156
|
+
: { name: currentRoute.botName, id: currentRoute.botID };
|
|
3157
|
+
|
|
3158
|
+
const destinationAction = await promptChoice(
|
|
3159
|
+
ui,
|
|
3160
|
+
"Project chat destination",
|
|
3161
|
+
[
|
|
3162
|
+
{ value: "keep", label: "Keep current destination", description: currentRoute.destinationLabel || currentRoute.destinationID || "-" },
|
|
3163
|
+
{ value: "change", label: "Change destination", description: "select another active project destination" },
|
|
3164
|
+
],
|
|
3165
|
+
{ defaultIndex: projectID !== currentRoute.projectID ? 1 : 0 },
|
|
3166
|
+
);
|
|
3167
|
+
const destination = destinationAction?.value === "change" || projectID !== currentRoute.projectID
|
|
3168
|
+
? await selectRunnerDestination(ui, {
|
|
3169
|
+
projectID,
|
|
3170
|
+
provider: currentRoute.provider,
|
|
3171
|
+
flags: {},
|
|
3172
|
+
currentDestinationID: currentRoute.destinationID,
|
|
3173
|
+
currentDestinationLabel: currentRoute.destinationLabel,
|
|
3174
|
+
})
|
|
3175
|
+
: { id: currentRoute.destinationID, label: currentRoute.destinationLabel };
|
|
3176
|
+
|
|
3177
|
+
const pollAction = await promptChoice(
|
|
3178
|
+
ui,
|
|
3179
|
+
"Poll interval (ms)",
|
|
3180
|
+
[
|
|
3181
|
+
{ value: "keep", label: "Keep current value", description: String(currentRoute.pollIntervalMs || 5000) },
|
|
3182
|
+
{ value: "change", label: "Change value", description: "set a different poll interval" },
|
|
3183
|
+
],
|
|
3184
|
+
{ defaultIndex: 0 },
|
|
3185
|
+
);
|
|
3186
|
+
const pollIntervalMs = pollAction?.value === "change"
|
|
3187
|
+
? intFromRaw(await promptRequiredLine(ui, "Poll interval (ms)", String(currentRoute.pollIntervalMs || 5000)), currentRoute.pollIntervalMs || 5000)
|
|
3188
|
+
: currentRoute.pollIntervalMs;
|
|
3189
|
+
|
|
3190
|
+
const enabledChoice = await promptChoice(
|
|
3191
|
+
ui,
|
|
3192
|
+
"Route enabled state",
|
|
3193
|
+
[
|
|
3194
|
+
{ value: "keep", label: "Keep current state", description: currentRoute.enabled ? "enabled" : "disabled" },
|
|
3195
|
+
{ value: "enabled", label: "Enable route", description: "runner can execute this route" },
|
|
3196
|
+
{ value: "disabled", label: "Disable route", description: "keep it configured but inactive" },
|
|
3197
|
+
],
|
|
3198
|
+
{ defaultIndex: 0 },
|
|
3199
|
+
);
|
|
3200
|
+
const enabled = enabledChoice?.value === "enabled"
|
|
3201
|
+
? true
|
|
3202
|
+
: enabledChoice?.value === "disabled"
|
|
3203
|
+
? false
|
|
3204
|
+
: currentRoute.enabled;
|
|
3205
|
+
|
|
3206
|
+
const confirmed = await promptConfirmChoice(ui, "Save runner route changes now?", {
|
|
3207
|
+
confirmDescription: "write the updated route to bot-runner.json",
|
|
3208
|
+
cancelDescription: "leave the route unchanged",
|
|
3209
|
+
});
|
|
3210
|
+
if (!confirmed) {
|
|
3211
|
+
process.stdout.write("Cancelled.\n");
|
|
3212
|
+
return;
|
|
3213
|
+
}
|
|
3214
|
+
const nextRoute = buildRunnerRoutePayload({
|
|
3215
|
+
currentRoute,
|
|
3216
|
+
name: routeName,
|
|
3217
|
+
enabled,
|
|
3218
|
+
projectID,
|
|
3219
|
+
provider: currentRoute.provider,
|
|
3220
|
+
role,
|
|
3221
|
+
roleProfile: role,
|
|
3222
|
+
serverBotName: botSelection.name,
|
|
3223
|
+
serverBotID: botSelection.id,
|
|
3224
|
+
destinationID: destination.id,
|
|
3225
|
+
destinationLabel: destination.label,
|
|
3226
|
+
pollIntervalMs,
|
|
3227
|
+
});
|
|
3228
|
+
const nextConfig = {
|
|
3229
|
+
...config,
|
|
3230
|
+
routes: ensureArray(config.routes)
|
|
3231
|
+
.map((route) => normalizeRunnerRoute(route))
|
|
3232
|
+
.filter((route) => String(route.name || "").trim().toLowerCase() !== String(currentRoute.name || "").trim().toLowerCase()),
|
|
3233
|
+
};
|
|
3234
|
+
const filePath = saveBotRunnerConfig(upsertRunnerRouteConfig(nextConfig, nextRoute), config.filePath);
|
|
3235
|
+
process.stdout.write(`Updated runner route "${nextRoute.name}" in ${filePath}\n`);
|
|
3236
|
+
} finally {
|
|
3237
|
+
ui.close();
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
async function runRunnerRouteRemove(flags) {
|
|
3242
|
+
const ui = createPrompter();
|
|
3243
|
+
try {
|
|
3244
|
+
if (shouldRenderPromptChrome(flags)) {
|
|
3245
|
+
ui.setFlow("RUNNER ROUTE REMOVE", "Delete one executable runner route");
|
|
3246
|
+
}
|
|
3247
|
+
const selection = await selectRunnerManagementRoute(ui, flags, { title: "Select runner route to remove" });
|
|
3248
|
+
const currentRoute = normalizeRunnerRoute(selection.route);
|
|
3249
|
+
const confirmed = await promptConfirmChoice(ui, `Remove runner route "${currentRoute.name || runnerRouteKey(currentRoute)}"?`, {
|
|
3250
|
+
confirmDescription: "delete the route from bot-runner.json",
|
|
3251
|
+
cancelDescription: "keep the route unchanged",
|
|
3252
|
+
});
|
|
3253
|
+
if (!confirmed) {
|
|
3254
|
+
process.stdout.write("Cancelled.\n");
|
|
3255
|
+
return;
|
|
3256
|
+
}
|
|
3257
|
+
const nextConfig = removeRunnerRouteFromConfig(selection.config, currentRoute.name);
|
|
3258
|
+
const filePath = saveBotRunnerConfig(nextConfig, selection.config.filePath);
|
|
3259
|
+
process.stdout.write(`Removed runner route "${currentRoute.name}" from ${filePath}\n`);
|
|
3260
|
+
} finally {
|
|
3261
|
+
ui.close();
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
|
|
2664
3265
|
async function runRunnerList(flags) {
|
|
2665
3266
|
const rows = buildRunnerRouteListRows();
|
|
2666
3267
|
if (boolFromRaw(flags.json, false)) {
|
|
@@ -2875,6 +3476,31 @@ async function runRunnerStart(flags) {
|
|
|
2875
3476
|
async function runRunner(argv) {
|
|
2876
3477
|
const [subcommandRaw = "", ...rest] = argv;
|
|
2877
3478
|
const subcommand = String(subcommandRaw || "").trim().toLowerCase();
|
|
3479
|
+
if (subcommand === "route" || subcommand === "routes") {
|
|
3480
|
+
const [routeSubcommandRaw = "", ...routeRest] = rest;
|
|
3481
|
+
const routeSubcommand = String(routeSubcommandRaw || "").trim().toLowerCase();
|
|
3482
|
+
const routeArgv = !routeSubcommand || routeSubcommand.startsWith("-")
|
|
3483
|
+
? [routeSubcommandRaw, ...routeRest].filter((value) => String(value || "").trim())
|
|
3484
|
+
: routeRest;
|
|
3485
|
+
const routeFlags = parseArgs(routeArgv);
|
|
3486
|
+
if (!routeSubcommand || routeSubcommand === "list" || routeSubcommand.startsWith("-")) {
|
|
3487
|
+
await runRunnerList(routeFlags);
|
|
3488
|
+
return;
|
|
3489
|
+
}
|
|
3490
|
+
if (routeSubcommand === "add") {
|
|
3491
|
+
await runRunnerRouteAdd(routeFlags);
|
|
3492
|
+
return;
|
|
3493
|
+
}
|
|
3494
|
+
if (routeSubcommand === "edit") {
|
|
3495
|
+
await runRunnerRouteEdit(routeFlags);
|
|
3496
|
+
return;
|
|
3497
|
+
}
|
|
3498
|
+
if (routeSubcommand === "remove" || routeSubcommand === "rm" || routeSubcommand === "delete") {
|
|
3499
|
+
await runRunnerRouteRemove(routeFlags);
|
|
3500
|
+
return;
|
|
3501
|
+
}
|
|
3502
|
+
throw new Error("runner route requires a subcommand: list | add | edit | remove");
|
|
3503
|
+
}
|
|
2878
3504
|
const flags = parseArgs(rest);
|
|
2879
3505
|
if (subcommand === "list") {
|
|
2880
3506
|
await runRunnerList(flags);
|
|
@@ -2892,7 +3518,7 @@ async function runRunner(argv) {
|
|
|
2892
3518
|
await runRunnerStart(flags);
|
|
2893
3519
|
return;
|
|
2894
3520
|
}
|
|
2895
|
-
throw new Error("runner requires a subcommand: list | once | start");
|
|
3521
|
+
throw new Error("runner requires a subcommand: list | show | once | start | route");
|
|
2896
3522
|
}
|
|
2897
3523
|
|
|
2898
3524
|
async function runLocalBotBridge(argv) {
|
|
@@ -5973,6 +6599,10 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
|
|
|
5973
6599
|
defaultLocalBotBridgeCommand,
|
|
5974
6600
|
resolveRunnerExecutionPlan,
|
|
5975
6601
|
normalizeRunnerRoute,
|
|
6602
|
+
buildRunnerRouteNameSuggestion,
|
|
6603
|
+
buildRunnerRoutePayload,
|
|
6604
|
+
upsertRunnerRouteConfig,
|
|
6605
|
+
removeRunnerRouteFromConfig,
|
|
5976
6606
|
buildRunnerExecutionDeps,
|
|
5977
6607
|
defaultBotRunnerRoleProfiles,
|
|
5978
6608
|
resolveRunnerRoutes,
|
package/lib/bot-commands.mjs
CHANGED
|
@@ -201,7 +201,7 @@ function providerTokenKey(provider, deps) {
|
|
|
201
201
|
return safeObject(requireDependency(deps, "providerEnvConfig")(provider)).tokenKey || "";
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
function shouldRenderPromptChrome(flags) {
|
|
204
|
+
export function shouldRenderPromptChrome(flags) {
|
|
205
205
|
const parsedFlags = safeObject(flags);
|
|
206
206
|
if (boolFromRaw(parsedFlags["non-interactive"] ?? parsedFlags.yes, false)) return false;
|
|
207
207
|
if (boolFromRaw(parsedFlags.json, false)) return false;
|
|
@@ -240,7 +240,7 @@ function printBotUsage(deps) {
|
|
|
240
240
|
);
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
-
function createPrompter() {
|
|
243
|
+
export function createPrompter() {
|
|
244
244
|
let flowTitle = "";
|
|
245
245
|
let flowSubtitle = "";
|
|
246
246
|
let stepIndex = 0;
|
|
@@ -311,7 +311,7 @@ function createPrompter() {
|
|
|
311
311
|
};
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
-
async function promptLine(ui, promptText, defaultValue = "") {
|
|
314
|
+
export async function promptLine(ui, promptText, defaultValue = "") {
|
|
315
315
|
ui.beginPrompt(promptText, [
|
|
316
316
|
String(defaultValue || "").trim()
|
|
317
317
|
? `Press Enter to keep the current value: ${defaultValue}`
|
|
@@ -323,7 +323,7 @@ async function promptLine(ui, promptText, defaultValue = "") {
|
|
|
323
323
|
return text || String(defaultValue || "").trim();
|
|
324
324
|
}
|
|
325
325
|
|
|
326
|
-
async function promptRequiredLine(ui, promptText, defaultValue = "") {
|
|
326
|
+
export async function promptRequiredLine(ui, promptText, defaultValue = "") {
|
|
327
327
|
while (true) {
|
|
328
328
|
const answer = await promptLine(ui, promptText, defaultValue);
|
|
329
329
|
if (answer) return answer;
|
|
@@ -331,7 +331,7 @@ async function promptRequiredLine(ui, promptText, defaultValue = "") {
|
|
|
331
331
|
}
|
|
332
332
|
}
|
|
333
333
|
|
|
334
|
-
async function promptYesNo(ui, promptText, defaultValue = true) {
|
|
334
|
+
export async function promptYesNo(ui, promptText, defaultValue = true) {
|
|
335
335
|
ui.beginPrompt(promptText, ["Choose y or n, then press Enter."]);
|
|
336
336
|
const hint = defaultValue ? "Y/n" : "y/N";
|
|
337
337
|
while (true) {
|
|
@@ -343,7 +343,7 @@ async function promptYesNo(ui, promptText, defaultValue = true) {
|
|
|
343
343
|
}
|
|
344
344
|
}
|
|
345
345
|
|
|
346
|
-
async function promptConfirmChoice(
|
|
346
|
+
export async function promptConfirmChoice(
|
|
347
347
|
ui,
|
|
348
348
|
title,
|
|
349
349
|
{
|
|
@@ -367,7 +367,7 @@ function formatChoiceLabel(option) {
|
|
|
367
367
|
return `${String(option.label || option.value || "").trim()}${option.description ? ` - ${option.description}` : ""}`;
|
|
368
368
|
}
|
|
369
369
|
|
|
370
|
-
async function promptChoice(ui, title, options, { defaultIndex = 0, allowCancel = false } = {}) {
|
|
370
|
+
export async function promptChoice(ui, title, options, { defaultIndex = 0, allowCancel = false } = {}) {
|
|
371
371
|
const list = ensureArray(options).filter((item) => item && item.value !== undefined);
|
|
372
372
|
if (!list.length) {
|
|
373
373
|
throw new Error(`no options available for ${title}`);
|
|
@@ -399,7 +399,7 @@ async function promptChoice(ui, title, options, { defaultIndex = 0, allowCancel
|
|
|
399
399
|
}
|
|
400
400
|
}
|
|
401
401
|
|
|
402
|
-
async function promptKeepChangeClear(ui, title, { allowClear = true, defaultValue = "keep" } = {}) {
|
|
402
|
+
export async function promptKeepChangeClear(ui, title, { allowClear = true, defaultValue = "keep" } = {}) {
|
|
403
403
|
const options = [
|
|
404
404
|
{ value: "keep", label: "Keep current value" },
|
|
405
405
|
{ value: "change", label: "Change value" },
|
|
@@ -2054,36 +2054,18 @@ async function maybePromptGroupedServerRoleProfiles(ui, serverBot, deps) {
|
|
|
2054
2054
|
}
|
|
2055
2055
|
return true;
|
|
2056
2056
|
}
|
|
2057
|
-
const
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
{
|
|
2069
|
-
value: "__done__",
|
|
2070
|
-
label: "Done",
|
|
2071
|
-
description: "finish grouped role editing",
|
|
2072
|
-
},
|
|
2073
|
-
],
|
|
2074
|
-
{ defaultIndex: 0 },
|
|
2075
|
-
);
|
|
2076
|
-
if (!roleChoice || roleChoice.value === "__done__") {
|
|
2077
|
-
break;
|
|
2078
|
-
}
|
|
2057
|
+
const roleChoice = await promptChoice(
|
|
2058
|
+
ui,
|
|
2059
|
+
"Select role to edit",
|
|
2060
|
+
roles.map((role) => ({
|
|
2061
|
+
value: role,
|
|
2062
|
+
label: role,
|
|
2063
|
+
description: formatRoleProfileSummary(currentRoleProfileState(role, deps)),
|
|
2064
|
+
})),
|
|
2065
|
+
{ defaultIndex: 0 },
|
|
2066
|
+
);
|
|
2067
|
+
if (roleChoice?.value) {
|
|
2079
2068
|
await promptRoleExecutionProfile(ui, roleChoice.value, deps);
|
|
2080
|
-
remaining.delete(roleChoice.value);
|
|
2081
|
-
if (!remaining.size) {
|
|
2082
|
-
break;
|
|
2083
|
-
}
|
|
2084
|
-
if (!await promptYesNo(ui, "Edit another role?", false)) {
|
|
2085
|
-
break;
|
|
2086
|
-
}
|
|
2087
2069
|
}
|
|
2088
2070
|
return true;
|
|
2089
2071
|
}
|
|
@@ -479,14 +479,6 @@ export async function runSelftestBotCommands(push, deps) {
|
|
|
479
479
|
"2", // worker AI model: Sonnet 4.6r
|
|
480
480
|
"4", // worker permission: danger_full_access
|
|
481
481
|
"4", // worker reasoning: high
|
|
482
|
-
"y", // edit another role
|
|
483
|
-
"3", // select role to edit: approval
|
|
484
|
-
"2", // approval: edit settings
|
|
485
|
-
"4", // approval AI client: gemini
|
|
486
|
-
"2", // approval AI model: gemini-3.1-pro
|
|
487
|
-
"4", // approval permission: danger_full_access
|
|
488
|
-
"4", // approval reasoning: high
|
|
489
|
-
"n", // stop editing roles
|
|
490
482
|
"1", // keep current default setting
|
|
491
483
|
"y", // save
|
|
492
484
|
]),
|
|
@@ -501,13 +493,13 @@ export async function runSelftestBotCommands(push, deps) {
|
|
|
501
493
|
const groupedRunnerConfig = readJSON(fs.readFileSync(groupedRunnerConfigPath, "utf8"));
|
|
502
494
|
const groupedRoleProfiles = safeObject(groupedRunnerConfig.role_profiles || groupedRunnerConfig.roleProfiles);
|
|
503
495
|
push(
|
|
504
|
-
|
|
496
|
+
"bot_edit_grouped_server_roles_updates_role_profiles",
|
|
505
497
|
String(safeObject(safeObject(groupedRoleProfiles).worker).client || "") === "claude"
|
|
506
498
|
&& String(safeObject(safeObject(groupedRoleProfiles).worker).model || "") === "Sonnet 4.6r"
|
|
507
499
|
&& String(safeObject(safeObject(groupedRoleProfiles).worker).permission_mode || "") === "danger_full_access"
|
|
508
500
|
&& String(safeObject(safeObject(groupedRoleProfiles).worker).reasoning_effort || "") === "high"
|
|
509
|
-
&& String(safeObject(safeObject(groupedRoleProfiles).approval).client || "") === "
|
|
510
|
-
&& String(safeObject(safeObject(groupedRoleProfiles).approval).model || "") === "
|
|
501
|
+
&& String(safeObject(safeObject(groupedRoleProfiles).approval).client || "") === "claude"
|
|
502
|
+
&& String(safeObject(safeObject(groupedRoleProfiles).approval).model || "") === "Sonnet 4.6r",
|
|
511
503
|
`worker=${JSON.stringify(safeObject(safeObject(groupedRoleProfiles).worker))} approval=${JSON.stringify(safeObject(safeObject(groupedRoleProfiles).approval))}`,
|
|
512
504
|
);
|
|
513
505
|
|
|
@@ -527,7 +519,7 @@ export async function runSelftestBotCommands(push, deps) {
|
|
|
527
519
|
"bot_show_reports_grouped_server_roles",
|
|
528
520
|
safeObject(groupedShowPayload.serverBinding).mode === "group"
|
|
529
521
|
&& safeObject(safeObject(groupedShowPayload.serverBinding).effectiveRoleProfiles).worker?.client === "claude"
|
|
530
|
-
&& safeObject(safeObject(groupedShowPayload.serverBinding).effectiveRoleProfiles).approval?.client === "
|
|
522
|
+
&& safeObject(safeObject(groupedShowPayload.serverBinding).effectiveRoleProfiles).approval?.client === "claude"
|
|
531
523
|
&& String(safeObject(safeObject(groupedShowPayload.serverBinding).effectiveRoleProfiles).monitor?.model || "") === "gpt-5.4",
|
|
532
524
|
`mode=${String(safeObject(groupedShowPayload.serverBinding).mode || "")} worker=${String(safeObject(safeObject(groupedShowPayload.serverBinding).effectiveRoleProfiles).worker?.client || "")} approval=${String(safeObject(safeObject(groupedShowPayload.serverBinding).effectiveRoleProfiles).approval?.client || "")} monitor_model=${String(safeObject(safeObject(groupedShowPayload.serverBinding).effectiveRoleProfiles).monitor?.model || "")}`,
|
|
533
525
|
);
|
|
@@ -28,6 +28,10 @@ export async function runSelftestRunnerScenarios(push, deps) {
|
|
|
28
28
|
const defaultLocalBotBridgeCommand = requireDependency(deps, "defaultLocalBotBridgeCommand");
|
|
29
29
|
const resolveRunnerExecutionPlan = requireDependency(deps, "resolveRunnerExecutionPlan");
|
|
30
30
|
const normalizeRunnerRoute = requireDependency(deps, "normalizeRunnerRoute");
|
|
31
|
+
const buildRunnerRouteNameSuggestion = requireDependency(deps, "buildRunnerRouteNameSuggestion");
|
|
32
|
+
const buildRunnerRoutePayload = requireDependency(deps, "buildRunnerRoutePayload");
|
|
33
|
+
const upsertRunnerRouteConfig = requireDependency(deps, "upsertRunnerRouteConfig");
|
|
34
|
+
const removeRunnerRouteFromConfig = requireDependency(deps, "removeRunnerRouteFromConfig");
|
|
31
35
|
const buildRunnerExecutionDeps = requireDependency(deps, "buildRunnerExecutionDeps");
|
|
32
36
|
const defaultBotRunnerRoleProfiles = requireDependency(deps, "defaultBotRunnerRoleProfiles");
|
|
33
37
|
const resolveRunnerRoutes = requireDependency(deps, "resolveRunnerRoutes");
|
|
@@ -139,6 +143,51 @@ export async function runSelftestRunnerScenarios(push, deps) {
|
|
|
139
143
|
`mapping=${String(migratedRunnerConfig.projectMappings?.[selftestProjectID]?.workspaceDir || "(none)")}`,
|
|
140
144
|
);
|
|
141
145
|
|
|
146
|
+
const suggestedRouteName = buildRunnerRouteNameSuggestion(
|
|
147
|
+
{ provider: "telegram", role: "monitor", botName: "Server Protocol Monitor Bot" },
|
|
148
|
+
["telegram-monitor-server-protocol-monitor-bot"],
|
|
149
|
+
);
|
|
150
|
+
push(
|
|
151
|
+
"runner_route_name_suggestion_is_slugged_and_unique",
|
|
152
|
+
suggestedRouteName === "telegram-monitor-server-protocol-monitor-bot-2",
|
|
153
|
+
`suggested=${suggestedRouteName}`,
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const routePayload = buildRunnerRoutePayload({
|
|
157
|
+
name: "telegram-monitor-server-protocol-monitor-bot",
|
|
158
|
+
enabled: true,
|
|
159
|
+
projectID: selftestProjectID,
|
|
160
|
+
provider: "telegram",
|
|
161
|
+
role: "monitor",
|
|
162
|
+
roleProfile: "monitor",
|
|
163
|
+
serverBotName: "Server Protocol Monitor Bot",
|
|
164
|
+
serverBotID: "11111111-2222-3333-4444-555555555555",
|
|
165
|
+
destinationID: "dest-1",
|
|
166
|
+
destinationLabel: "Main Room",
|
|
167
|
+
pollIntervalMs: 5000,
|
|
168
|
+
});
|
|
169
|
+
const routeConfigWithPayload = upsertRunnerRouteConfig(
|
|
170
|
+
normalizeBotRunnerConfigContents({ version: 2, routes: [] }, "selftest-runner-route-add.json"),
|
|
171
|
+
routePayload,
|
|
172
|
+
);
|
|
173
|
+
const savedRoute = (Array.isArray(routeConfigWithPayload.routes) ? routeConfigWithPayload.routes : [])
|
|
174
|
+
.map((route) => normalizeRunnerRoute(route))[0];
|
|
175
|
+
push(
|
|
176
|
+
"runner_route_payload_persists_server_identity_fields",
|
|
177
|
+
String(savedRoute.name || "") === "telegram-monitor-server-protocol-monitor-bot"
|
|
178
|
+
&& String(savedRoute.botName || "") === "Server Protocol Monitor Bot"
|
|
179
|
+
&& String(savedRoute.botID || "") === "11111111-2222-3333-4444-555555555555"
|
|
180
|
+
&& String(savedRoute.destinationID || "") === "dest-1",
|
|
181
|
+
`name=${String(savedRoute.name || "")} bot_name=${String(savedRoute.botName || "")} bot_id=${String(savedRoute.botID || "")} destination_id=${String(savedRoute.destinationID || "")}`,
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const routeConfigWithoutPayload = removeRunnerRouteFromConfig(routeConfigWithPayload, "telegram-monitor-server-protocol-monitor-bot");
|
|
185
|
+
push(
|
|
186
|
+
"runner_route_remove_drops_named_route",
|
|
187
|
+
(Array.isArray(routeConfigWithoutPayload.routes) ? routeConfigWithoutPayload.routes.length : 0) === 0,
|
|
188
|
+
`remaining=${Array.isArray(routeConfigWithoutPayload.routes) ? routeConfigWithoutPayload.routes.length : 0}`,
|
|
189
|
+
);
|
|
190
|
+
|
|
142
191
|
const roleProfileOverrideConfig = normalizeBotRunnerConfigContents(
|
|
143
192
|
{
|
|
144
193
|
version: 2,
|