thinkwork-cli 0.8.1 → 0.8.2
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/dist/cli.js +209 -25
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2741,6 +2741,83 @@ async function resolveTenantRest(opts) {
|
|
|
2741
2741
|
});
|
|
2742
2742
|
}
|
|
2743
2743
|
|
|
2744
|
+
// src/lib/resolve-identifier.ts
|
|
2745
|
+
import { select as select5 } from "@inquirer/prompts";
|
|
2746
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2747
|
+
function isUuid(s) {
|
|
2748
|
+
return UUID_RE.test(s);
|
|
2749
|
+
}
|
|
2750
|
+
async function resolveIdentifier(opts) {
|
|
2751
|
+
const { identifier, list, getId, getAliases, resourceLabel } = opts;
|
|
2752
|
+
if (!identifier) {
|
|
2753
|
+
if (!isInteractive()) {
|
|
2754
|
+
requireTty(`${capitalize(resourceLabel)} identifier`);
|
|
2755
|
+
throw new Error("unreachable: requireTty must exit in non-TTY");
|
|
2756
|
+
}
|
|
2757
|
+
const items2 = await list();
|
|
2758
|
+
if (items2.length === 0) {
|
|
2759
|
+
printError(`No ${resourceLabel}s found. Nothing to pick.`);
|
|
2760
|
+
process.exit(1);
|
|
2761
|
+
}
|
|
2762
|
+
if (items2.length === 1) {
|
|
2763
|
+
console.log(` Using the only ${resourceLabel}: ${defaultLabel(items2[0], opts)}`);
|
|
2764
|
+
return items2[0];
|
|
2765
|
+
}
|
|
2766
|
+
const chosenId = await select5({
|
|
2767
|
+
message: `Which ${resourceLabel}?`,
|
|
2768
|
+
choices: items2.map((it) => ({
|
|
2769
|
+
name: (opts.pickerLabel ?? defaultLabelFor(opts))(it),
|
|
2770
|
+
value: getId(it)
|
|
2771
|
+
})),
|
|
2772
|
+
loop: false
|
|
2773
|
+
});
|
|
2774
|
+
return items2.find((it) => getId(it) === chosenId);
|
|
2775
|
+
}
|
|
2776
|
+
const items = await list();
|
|
2777
|
+
if (isUuid(identifier)) {
|
|
2778
|
+
const hit = items.find((it) => getId(it) === identifier);
|
|
2779
|
+
if (hit) return hit;
|
|
2780
|
+
printError(
|
|
2781
|
+
`${capitalize(resourceLabel)} with ID "${identifier}" not found. Available: ${formatAvailable(items, opts)}`
|
|
2782
|
+
);
|
|
2783
|
+
process.exit(1);
|
|
2784
|
+
}
|
|
2785
|
+
const needle = identifier.toLowerCase();
|
|
2786
|
+
const matches = items.filter(
|
|
2787
|
+
(it) => getAliases(it).some((a) => a != null && String(a).toLowerCase() === needle)
|
|
2788
|
+
);
|
|
2789
|
+
if (matches.length === 0) {
|
|
2790
|
+
printError(
|
|
2791
|
+
`${capitalize(resourceLabel)} "${identifier}" not found. Available: ${formatAvailable(items, opts)}`
|
|
2792
|
+
);
|
|
2793
|
+
process.exit(1);
|
|
2794
|
+
}
|
|
2795
|
+
if (matches.length > 1) {
|
|
2796
|
+
printError(
|
|
2797
|
+
`"${identifier}" matches ${matches.length} ${resourceLabel}s. Pass the UUID instead \u2014 candidates: ${matches.map(getId).join(", ")}`
|
|
2798
|
+
);
|
|
2799
|
+
process.exit(1);
|
|
2800
|
+
}
|
|
2801
|
+
return matches[0];
|
|
2802
|
+
}
|
|
2803
|
+
function capitalize(s) {
|
|
2804
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
2805
|
+
}
|
|
2806
|
+
function defaultLabelFor(opts) {
|
|
2807
|
+
return (item) => defaultLabel(item, opts);
|
|
2808
|
+
}
|
|
2809
|
+
function defaultLabel(item, opts) {
|
|
2810
|
+
const aliases = opts.getAliases(item).filter(Boolean);
|
|
2811
|
+
const primary = aliases[0] ?? "(no name)";
|
|
2812
|
+
return `${primary} (${opts.getId(item)})`;
|
|
2813
|
+
}
|
|
2814
|
+
function formatAvailable(items, opts) {
|
|
2815
|
+
if (items.length === 0) return "(none)";
|
|
2816
|
+
const names = items.map((it) => opts.getAliases(it)[0]).filter((n) => Boolean(n)).slice(0, 10);
|
|
2817
|
+
const suffix = items.length > names.length ? `, \u2026(${items.length - names.length} more)` : "";
|
|
2818
|
+
return names.join(", ") + suffix;
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2744
2821
|
// src/commands/mcp.ts
|
|
2745
2822
|
async function resolveMcpContext(opts) {
|
|
2746
2823
|
const stage = await resolveStage({ flag: opts.stage });
|
|
@@ -2754,6 +2831,30 @@ async function resolveMcpContext(opts) {
|
|
|
2754
2831
|
});
|
|
2755
2832
|
return { stage, api, tenant };
|
|
2756
2833
|
}
|
|
2834
|
+
async function resolveServer(identifier, api, tenantSlug) {
|
|
2835
|
+
return resolveIdentifier({
|
|
2836
|
+
identifier,
|
|
2837
|
+
list: async () => {
|
|
2838
|
+
const res = await apiFetch(
|
|
2839
|
+
api.apiUrl,
|
|
2840
|
+
api.authSecret,
|
|
2841
|
+
"/api/skills/mcp-servers",
|
|
2842
|
+
{},
|
|
2843
|
+
{ "x-tenant-slug": tenantSlug }
|
|
2844
|
+
);
|
|
2845
|
+
return res.servers ?? [];
|
|
2846
|
+
},
|
|
2847
|
+
getId: (s) => s.id,
|
|
2848
|
+
getAliases: (s) => [s.slug, s.name],
|
|
2849
|
+
resourceLabel: "MCP server",
|
|
2850
|
+
pickerLabel: (s) => `${s.name} ${chalk10.dim(`(${s.slug}, ${s.id})`)}`
|
|
2851
|
+
});
|
|
2852
|
+
}
|
|
2853
|
+
function formatAuth(s) {
|
|
2854
|
+
if (s.authType === "per_user_oauth") return `OAuth (${s.oauthProvider})`;
|
|
2855
|
+
if (s.authType === "tenant_api_key") return "API Key";
|
|
2856
|
+
return "none";
|
|
2857
|
+
}
|
|
2757
2858
|
function registerMcpCommand(program2) {
|
|
2758
2859
|
const mcp = program2.command("mcp").description("Manage MCP servers for your tenant");
|
|
2759
2860
|
mcp.command("list").alias("ls").description("List registered MCP servers. Prompts for stage/tenant in a TTY when omitted.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
|
|
@@ -2765,7 +2866,7 @@ Examples:
|
|
|
2765
2866
|
|
|
2766
2867
|
# Scriptable
|
|
2767
2868
|
$ thinkwork mcp list -s dev -t acme
|
|
2768
|
-
$ thinkwork mcp list -s prod -t acme --json | jq '.[].
|
|
2869
|
+
$ thinkwork mcp list -s prod -t acme --json | jq '.[].id'
|
|
2769
2870
|
`
|
|
2770
2871
|
).action(async (opts) => {
|
|
2771
2872
|
try {
|
|
@@ -2785,11 +2886,11 @@ Examples:
|
|
|
2785
2886
|
console.log("");
|
|
2786
2887
|
for (const s of servers) {
|
|
2787
2888
|
const status = s.enabled ? chalk10.green("enabled") : chalk10.dim("disabled");
|
|
2788
|
-
const authLabel = s.authType === "per_user_oauth" ? `OAuth (${s.oauthProvider})` : s.authType === "tenant_api_key" ? "API Key" : "none";
|
|
2789
2889
|
console.log(` ${chalk10.bold(s.name)} ${chalk10.dim(s.slug)} ${status}`);
|
|
2890
|
+
console.log(` ID: ${s.id}`);
|
|
2790
2891
|
console.log(` URL: ${s.url}`);
|
|
2791
2892
|
console.log(` Transport: ${s.transport}`);
|
|
2792
|
-
console.log(` Auth: ${
|
|
2893
|
+
console.log(` Auth: ${formatAuth(s)}`);
|
|
2793
2894
|
if (s.tools?.length) console.log(` Tools: ${s.tools.length} cached`);
|
|
2794
2895
|
console.log("");
|
|
2795
2896
|
}
|
|
@@ -2860,36 +2961,111 @@ Examples:
|
|
|
2860
2961
|
}
|
|
2861
2962
|
}
|
|
2862
2963
|
);
|
|
2863
|
-
mcp.command("
|
|
2964
|
+
mcp.command("update [id]").description(
|
|
2965
|
+
"Update an MCP server's URL / transport / auth / enabled state. Accepts UUID, slug, or name; prompts in a TTY when the positional is omitted. Preserves agent assignments (unlike remove + re-add)."
|
|
2966
|
+
).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--url <url>", "New URL").option("--transport <type>", "streamable-http | sse").option("--auth-type <type>", "none | tenant_api_key | per_user_oauth").option("--api-key <token>", "API key (for tenant_api_key auth)").option("--oauth-provider <name>", "OAuth provider name (for per_user_oauth auth)").option("--name <n>", "Rename").option("--enable", "Enable the server").option("--disable", "Disable the server (doesn't delete)").addHelpText(
|
|
2967
|
+
"after",
|
|
2968
|
+
`
|
|
2969
|
+
Examples:
|
|
2970
|
+
# Change URL in place (preserves agent assignments, unlike remove + re-add)
|
|
2971
|
+
$ thinkwork mcp update lastmile-routing --url https://dev-mcp.lastmile-tei.com/routing
|
|
2972
|
+
|
|
2973
|
+
# Disable without deleting
|
|
2974
|
+
$ thinkwork mcp update lastmile-routing --disable
|
|
2975
|
+
|
|
2976
|
+
# Rename + change transport
|
|
2977
|
+
$ thinkwork mcp update 629dcee1-1e14-4b83-9907-cb529e6035f6 --name "LastMile Routing" --transport sse
|
|
2978
|
+
|
|
2979
|
+
# Interactive \u2014 pick the server from a list
|
|
2980
|
+
$ thinkwork mcp update
|
|
2981
|
+
`
|
|
2982
|
+
).action(
|
|
2983
|
+
async (idArg, opts) => {
|
|
2984
|
+
try {
|
|
2985
|
+
const { stage, api, tenant } = await resolveMcpContext(opts);
|
|
2986
|
+
const server = await resolveServer(idArg, api, tenant.slug);
|
|
2987
|
+
const body = {};
|
|
2988
|
+
if (opts.url !== void 0) body.url = opts.url;
|
|
2989
|
+
if (opts.transport !== void 0) body.transport = opts.transport;
|
|
2990
|
+
if (opts.authType !== void 0) body.authType = opts.authType;
|
|
2991
|
+
if (opts.apiKey !== void 0) body.apiKey = opts.apiKey;
|
|
2992
|
+
if (opts.oauthProvider !== void 0) body.oauthProvider = opts.oauthProvider;
|
|
2993
|
+
if (opts.name !== void 0) body.name = opts.name;
|
|
2994
|
+
if (opts.enable) body.enabled = true;
|
|
2995
|
+
if (opts.disable) body.enabled = false;
|
|
2996
|
+
if (Object.keys(body).length === 0) {
|
|
2997
|
+
printError(
|
|
2998
|
+
"Nothing to update. Pass at least one of: --url, --transport, --auth-type, --api-key, --oauth-provider, --name, --enable, --disable."
|
|
2999
|
+
);
|
|
3000
|
+
process.exit(1);
|
|
3001
|
+
}
|
|
3002
|
+
printHeader("mcp update", stage);
|
|
3003
|
+
await apiFetch(
|
|
3004
|
+
api.apiUrl,
|
|
3005
|
+
api.authSecret,
|
|
3006
|
+
`/api/skills/mcp-servers/${server.id}`,
|
|
3007
|
+
{ method: "PUT", body: JSON.stringify(body) },
|
|
3008
|
+
{ "x-tenant-slug": tenant.slug }
|
|
3009
|
+
);
|
|
3010
|
+
printSuccess(
|
|
3011
|
+
`Updated ${server.name} (${server.slug}) \u2014 changed ${Object.keys(body).join(", ")}.`
|
|
3012
|
+
);
|
|
3013
|
+
} catch (err) {
|
|
3014
|
+
if (isCancellation(err)) return;
|
|
3015
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
3016
|
+
process.exit(1);
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
);
|
|
3020
|
+
mcp.command("remove [id]").alias("rm").description(
|
|
3021
|
+
"Remove an MCP server. Accepts UUID, slug, or name; prompts from a list when omitted in a TTY."
|
|
3022
|
+
).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
|
|
3023
|
+
"after",
|
|
3024
|
+
`
|
|
3025
|
+
Examples:
|
|
3026
|
+
# Interactive picker
|
|
3027
|
+
$ thinkwork mcp remove
|
|
3028
|
+
|
|
3029
|
+
# By slug (case-insensitive)
|
|
3030
|
+
$ thinkwork mcp remove lastmile-routing
|
|
3031
|
+
|
|
3032
|
+
# By UUID (from \`mcp list\` or --json)
|
|
3033
|
+
$ thinkwork mcp remove 629dcee1-1e14-4b83-9907-cb529e6035f6
|
|
3034
|
+
`
|
|
3035
|
+
).action(async (idArg, opts) => {
|
|
2864
3036
|
try {
|
|
2865
3037
|
const { api, tenant } = await resolveMcpContext(opts);
|
|
3038
|
+
const server = await resolveServer(idArg, api, tenant.slug);
|
|
2866
3039
|
await apiFetch(
|
|
2867
3040
|
api.apiUrl,
|
|
2868
3041
|
api.authSecret,
|
|
2869
|
-
`/api/skills/mcp-servers/${id}`,
|
|
3042
|
+
`/api/skills/mcp-servers/${server.id}`,
|
|
2870
3043
|
{ method: "DELETE" },
|
|
2871
3044
|
{ "x-tenant-slug": tenant.slug }
|
|
2872
3045
|
);
|
|
2873
|
-
printSuccess(
|
|
3046
|
+
printSuccess(`MCP server removed: ${server.name} (${server.slug}).`);
|
|
2874
3047
|
} catch (err) {
|
|
2875
3048
|
if (isCancellation(err)) return;
|
|
2876
3049
|
printError(err instanceof Error ? err.message : String(err));
|
|
2877
3050
|
process.exit(1);
|
|
2878
3051
|
}
|
|
2879
3052
|
});
|
|
2880
|
-
mcp.command("test
|
|
3053
|
+
mcp.command("test [id]").description(
|
|
3054
|
+
"Test connection and discover tools. Accepts UUID, slug, or name; prompts from a list when omitted in a TTY."
|
|
3055
|
+
).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(async (idArg, opts) => {
|
|
2881
3056
|
try {
|
|
2882
3057
|
const { stage, api, tenant } = await resolveMcpContext(opts);
|
|
3058
|
+
const server = await resolveServer(idArg, api, tenant.slug);
|
|
2883
3059
|
printHeader("mcp test", stage);
|
|
2884
3060
|
const result = await apiFetch(
|
|
2885
3061
|
api.apiUrl,
|
|
2886
3062
|
api.authSecret,
|
|
2887
|
-
`/api/skills/mcp-servers/${id}/test`,
|
|
3063
|
+
`/api/skills/mcp-servers/${server.id}/test`,
|
|
2888
3064
|
{ method: "POST" },
|
|
2889
3065
|
{ "x-tenant-slug": tenant.slug }
|
|
2890
3066
|
);
|
|
2891
3067
|
if (result.ok) {
|
|
2892
|
-
printSuccess(
|
|
3068
|
+
printSuccess(`Connection successful: ${server.name}.`);
|
|
2893
3069
|
if (result.tools?.length) {
|
|
2894
3070
|
console.log(chalk10.bold(`
|
|
2895
3071
|
Discovered tools (${result.tools.length}):
|
|
@@ -2913,11 +3089,14 @@ Examples:
|
|
|
2913
3089
|
process.exit(1);
|
|
2914
3090
|
}
|
|
2915
3091
|
});
|
|
2916
|
-
mcp.command("assign
|
|
2917
|
-
|
|
3092
|
+
mcp.command("assign [mcpServer]").description(
|
|
3093
|
+
"Assign an MCP server to an agent. Accepts UUID, slug, or name for the server; prompts from a list when omitted in a TTY."
|
|
3094
|
+
).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--agent <id>", "Agent ID").action(
|
|
3095
|
+
async (mcpServerArg, opts) => {
|
|
2918
3096
|
try {
|
|
2919
3097
|
const { input: input3 } = await import("@inquirer/prompts");
|
|
2920
|
-
const { api } = await resolveMcpContext(opts);
|
|
3098
|
+
const { api, tenant } = await resolveMcpContext(opts);
|
|
3099
|
+
const server = await resolveServer(mcpServerArg, api, tenant.slug);
|
|
2921
3100
|
let agent = opts.agent;
|
|
2922
3101
|
if (!agent) {
|
|
2923
3102
|
if (!process.stdin.isTTY) {
|
|
@@ -2930,9 +3109,11 @@ Examples:
|
|
|
2930
3109
|
api.apiUrl,
|
|
2931
3110
|
api.authSecret,
|
|
2932
3111
|
`/api/skills/agents/${agent}/mcp-servers`,
|
|
2933
|
-
{ method: "POST", body: JSON.stringify({ mcpServerId }) }
|
|
3112
|
+
{ method: "POST", body: JSON.stringify({ mcpServerId: server.id }) }
|
|
3113
|
+
);
|
|
3114
|
+
printSuccess(
|
|
3115
|
+
`MCP server assigned to agent. (${result.created ? "new" : "updated"}) \u2014 ${server.name} \u2192 ${agent}`
|
|
2934
3116
|
);
|
|
2935
|
-
printSuccess(`MCP server assigned to agent. (${result.created ? "new" : "updated"})`);
|
|
2936
3117
|
} catch (err) {
|
|
2937
3118
|
if (isCancellation(err)) return;
|
|
2938
3119
|
printError(err instanceof Error ? err.message : String(err));
|
|
@@ -2940,11 +3121,14 @@ Examples:
|
|
|
2940
3121
|
}
|
|
2941
3122
|
}
|
|
2942
3123
|
);
|
|
2943
|
-
mcp.command("unassign
|
|
2944
|
-
|
|
3124
|
+
mcp.command("unassign [mcpServer]").description(
|
|
3125
|
+
"Remove an MCP server from an agent. Accepts UUID, slug, or name for the server; prompts from a list when omitted in a TTY."
|
|
3126
|
+
).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--agent <id>", "Agent ID").action(
|
|
3127
|
+
async (mcpServerArg, opts) => {
|
|
2945
3128
|
try {
|
|
2946
3129
|
const { input: input3 } = await import("@inquirer/prompts");
|
|
2947
|
-
const { api } = await resolveMcpContext(opts);
|
|
3130
|
+
const { api, tenant } = await resolveMcpContext(opts);
|
|
3131
|
+
const server = await resolveServer(mcpServerArg, api, tenant.slug);
|
|
2948
3132
|
let agent = opts.agent;
|
|
2949
3133
|
if (!agent) {
|
|
2950
3134
|
if (!process.stdin.isTTY) {
|
|
@@ -2956,10 +3140,10 @@ Examples:
|
|
|
2956
3140
|
await apiFetch(
|
|
2957
3141
|
api.apiUrl,
|
|
2958
3142
|
api.authSecret,
|
|
2959
|
-
`/api/skills/agents/${agent}/mcp-servers/${
|
|
3143
|
+
`/api/skills/agents/${agent}/mcp-servers/${server.id}`,
|
|
2960
3144
|
{ method: "DELETE" }
|
|
2961
3145
|
);
|
|
2962
|
-
printSuccess(
|
|
3146
|
+
printSuccess(`MCP server unassigned from agent: ${server.name} \u219B ${agent}`);
|
|
2963
3147
|
} catch (err) {
|
|
2964
3148
|
if (isCancellation(err)) return;
|
|
2965
3149
|
printError(err instanceof Error ? err.message : String(err));
|
|
@@ -2970,7 +3154,7 @@ Examples:
|
|
|
2970
3154
|
}
|
|
2971
3155
|
|
|
2972
3156
|
// src/commands/tools.ts
|
|
2973
|
-
import { select as
|
|
3157
|
+
import { select as select6, password } from "@inquirer/prompts";
|
|
2974
3158
|
import chalk11 from "chalk";
|
|
2975
3159
|
var TOOL_PROVIDERS = {
|
|
2976
3160
|
"web-search": ["exa", "serpapi"]
|
|
@@ -3054,7 +3238,7 @@ Examples:
|
|
|
3054
3238
|
printError(`--provider is required. One of: ${TOOL_PROVIDERS["web-search"].join(", ")}`);
|
|
3055
3239
|
process.exit(1);
|
|
3056
3240
|
}
|
|
3057
|
-
provider = await
|
|
3241
|
+
provider = await select6({
|
|
3058
3242
|
message: "Provider:",
|
|
3059
3243
|
choices: TOOL_PROVIDERS["web-search"].map((p) => ({ name: p, value: p })),
|
|
3060
3244
|
loop: false
|
|
@@ -3242,7 +3426,7 @@ function registerUpdateCommand(program2) {
|
|
|
3242
3426
|
|
|
3243
3427
|
// src/commands/user.ts
|
|
3244
3428
|
import { spawn as spawn4 } from "child_process";
|
|
3245
|
-
import { input as input2, select as
|
|
3429
|
+
import { input as input2, select as select7 } from "@inquirer/prompts";
|
|
3246
3430
|
function getTerraformOutput2(cwd, key) {
|
|
3247
3431
|
return new Promise((resolve3, reject) => {
|
|
3248
3432
|
const proc = spawn4("terraform", ["output", "-raw", key], {
|
|
@@ -3313,7 +3497,7 @@ async function promptStage(region) {
|
|
|
3313
3497
|
console.log(` Using the only deployed stage: ${stages[0]}`);
|
|
3314
3498
|
return stages[0];
|
|
3315
3499
|
}
|
|
3316
|
-
return await
|
|
3500
|
+
return await select7({
|
|
3317
3501
|
message: "Which stage?",
|
|
3318
3502
|
choices: stages.map((s) => ({ name: s, value: s })),
|
|
3319
3503
|
loop: false
|
|
@@ -3332,7 +3516,7 @@ async function promptTenant(apiUrl, authSecret) {
|
|
|
3332
3516
|
console.log(` Using the only tenant: ${list[0].name} (${list[0].slug})`);
|
|
3333
3517
|
return list[0].slug;
|
|
3334
3518
|
}
|
|
3335
|
-
return await
|
|
3519
|
+
return await select7({
|
|
3336
3520
|
message: "Which tenant?",
|
|
3337
3521
|
choices: list.map((t) => ({
|
|
3338
3522
|
name: `${t.name} (slug: ${t.slug})`,
|
|
@@ -3351,7 +3535,7 @@ async function promptOptionalName() {
|
|
|
3351
3535
|
}
|
|
3352
3536
|
async function promptRole() {
|
|
3353
3537
|
if (!process.stdin.isTTY) return "member";
|
|
3354
|
-
return await
|
|
3538
|
+
return await select7({
|
|
3355
3539
|
message: "Role:",
|
|
3356
3540
|
choices: [
|
|
3357
3541
|
{ name: "member \u2014 regular access", value: "member" },
|