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.
Files changed (2) hide show
  1. package/dist/cli.js +209 -25
  2. 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 '.[].slug'
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: ${authLabel}`);
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("remove <id>").alias("rm").description("Remove an MCP server. Prompts for stage/tenant in a TTY when omitted.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(async (id, opts) => {
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("MCP server removed.");
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 <id>").description("Test connection and discover tools. Prompts for stage/tenant in a TTY when omitted.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(async (id, opts) => {
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("Connection successful.");
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 <mcpServerId>").description("Assign an MCP server to an agent. Prompts for stage/agent when omitted in a TTY.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--agent <id>", "Agent ID").action(
2917
- async (mcpServerId, opts) => {
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 <mcpServerId>").description("Remove an MCP server from an agent. Prompts for stage/agent when omitted in a TTY.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--agent <id>", "Agent ID").action(
2944
- async (mcpServerId, opts) => {
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/${mcpServerId}`,
3143
+ `/api/skills/agents/${agent}/mcp-servers/${server.id}`,
2960
3144
  { method: "DELETE" }
2961
3145
  );
2962
- printSuccess("MCP server unassigned from agent.");
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 select5, password } from "@inquirer/prompts";
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 select5({
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 select6 } from "@inquirer/prompts";
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 select6({
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 select6({
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 select6({
3538
+ return await select7({
3355
3539
  message: "Role:",
3356
3540
  choices: [
3357
3541
  { name: "member \u2014 regular access", value: "member" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkwork-cli",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "description": "Thinkwork CLI — deploy, manage, and interact with your Thinkwork stack",
5
5
  "license": "MIT",
6
6
  "type": "module",