thinkwork-cli 0.9.0 → 0.9.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 (57) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +2 -2
  3. package/dist/cli.js +1315 -330
  4. package/dist/terraform/examples/greenfield/main.tf +325 -19
  5. package/dist/terraform/examples/greenfield/terraform.tfvars.example +14 -0
  6. package/dist/terraform/modules/app/agentcore-code-interpreter/Dockerfile.sandbox-base +61 -0
  7. package/dist/terraform/modules/app/agentcore-code-interpreter/README.md +54 -0
  8. package/dist/terraform/modules/app/agentcore-code-interpreter/main.tf +197 -0
  9. package/dist/terraform/modules/app/agentcore-code-interpreter/scripts/build_and_push_sandbox_base.sh +70 -0
  10. package/dist/terraform/modules/app/agentcore-flue/README.md +58 -0
  11. package/dist/terraform/modules/app/agentcore-flue/main.tf +322 -0
  12. package/dist/terraform/modules/app/agentcore-flue/outputs.tf +23 -0
  13. package/dist/terraform/modules/app/agentcore-flue/variables.tf +91 -0
  14. package/dist/terraform/modules/app/agentcore-memory/scripts/create_or_find_memory.sh +0 -0
  15. package/dist/terraform/modules/app/agentcore-runtime/main.tf +165 -0
  16. package/dist/terraform/modules/app/appsync-subscriptions/main.tf +4 -0
  17. package/dist/terraform/modules/app/appsync-subscriptions/outputs.tf +5 -0
  18. package/dist/terraform/modules/app/computer-runtime/README.md +15 -0
  19. package/dist/terraform/modules/app/computer-runtime/main.tf +406 -0
  20. package/dist/terraform/modules/app/computer-runtime/outputs.tf +75 -0
  21. package/dist/terraform/modules/app/computer-runtime/variables.tf +66 -0
  22. package/dist/terraform/modules/app/hindsight-memory/main.tf +6 -0
  23. package/dist/terraform/modules/app/lambda-api/eval-fanout.tf +128 -0
  24. package/dist/terraform/modules/app/lambda-api/handlers.tf +1454 -43
  25. package/dist/terraform/modules/app/lambda-api/main.tf +221 -12
  26. package/dist/terraform/modules/app/lambda-api/mcp-oauth.tf +118 -0
  27. package/dist/terraform/modules/app/lambda-api/oauth-secrets.tf +49 -0
  28. package/dist/terraform/modules/app/lambda-api/outputs.tf +38 -0
  29. package/dist/terraform/modules/app/lambda-api/slack-app-secrets.tf +43 -0
  30. package/dist/terraform/modules/app/lambda-api/stripe-secrets.tf +53 -0
  31. package/dist/terraform/modules/app/lambda-api/variables.tf +349 -2
  32. package/dist/terraform/modules/app/lambda-api/workspace-events.tf +125 -0
  33. package/dist/terraform/modules/app/routines-stepfunctions/main.tf +453 -0
  34. package/dist/terraform/modules/app/sandbox-log-scrubber/README.md +66 -0
  35. package/dist/terraform/modules/app/sandbox-log-scrubber/main.tf +200 -0
  36. package/dist/terraform/modules/app/static-site/main.tf +146 -5
  37. package/dist/terraform/modules/app/www-dns/main.tf +118 -15
  38. package/dist/terraform/modules/app/www-dns/outputs.tf +10 -0
  39. package/dist/terraform/modules/app/www-dns/variables.tf +42 -0
  40. package/dist/terraform/modules/data/aurora-postgres/main.tf +164 -3
  41. package/dist/terraform/modules/data/aurora-postgres/outputs.tf +34 -0
  42. package/dist/terraform/modules/data/aurora-postgres/variables.tf +16 -0
  43. package/dist/terraform/modules/data/compliance-audit-bucket/README.md +145 -0
  44. package/dist/terraform/modules/data/compliance-audit-bucket/main.tf +573 -0
  45. package/dist/terraform/modules/data/compliance-audit-bucket/outputs.tf +43 -0
  46. package/dist/terraform/modules/data/compliance-audit-bucket/variables.tf +93 -0
  47. package/dist/terraform/modules/data/compliance-exports-bucket/main.tf +269 -0
  48. package/dist/terraform/modules/data/compliance-exports-bucket/outputs.tf +23 -0
  49. package/dist/terraform/modules/data/compliance-exports-bucket/variables.tf +50 -0
  50. package/dist/terraform/modules/data/s3-backups-bucket/main.tf +123 -0
  51. package/dist/terraform/modules/data/s3-buckets/main.tf +13 -0
  52. package/dist/terraform/modules/foundation/cognito/variables.tf +2 -2
  53. package/dist/terraform/modules/thinkwork/main.tf +439 -21
  54. package/dist/terraform/modules/thinkwork/outputs.tf +121 -0
  55. package/dist/terraform/modules/thinkwork/variables.tf +153 -2
  56. package/dist/terraform/schema.graphql +17 -0
  57. package/package.json +15 -14
package/dist/cli.js CHANGED
@@ -1,4 +1,9 @@
1
1
  #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __export = (target, all) => {
4
+ for (var name in all)
5
+ __defProp(target, name, { get: all[name], enumerable: true });
6
+ };
2
7
 
3
8
  // src/cli.ts
4
9
  import { Command } from "commander";
@@ -196,7 +201,7 @@ async function ensureWorkspace(cwd, stage) {
196
201
  }
197
202
  }
198
203
  function runTerraformRaw(cwd, args) {
199
- return new Promise((resolve3, reject) => {
204
+ return new Promise((resolve4, reject) => {
200
205
  const proc = spawn("terraform", args, {
201
206
  cwd,
202
207
  stdio: ["pipe", "pipe", "pipe"]
@@ -206,13 +211,13 @@ function runTerraformRaw(cwd, args) {
206
211
  proc.stdout.on("data", (d) => stdout += d);
207
212
  proc.stderr.on("data", (d) => stderr += d);
208
213
  proc.on("close", (code) => {
209
- if (code === 0) resolve3(stdout);
214
+ if (code === 0) resolve4(stdout);
210
215
  else reject(new Error(`terraform ${args.join(" ")} failed (exit ${code}): ${stderr}`));
211
216
  });
212
217
  });
213
218
  }
214
219
  function runTerraform(cwd, args) {
215
- return new Promise((resolve3) => {
220
+ return new Promise((resolve4) => {
216
221
  console.log(`
217
222
  \u2192 terraform ${args.join(" ")}
218
223
  `);
@@ -220,7 +225,7 @@ function runTerraform(cwd, args) {
220
225
  cwd,
221
226
  stdio: "inherit"
222
227
  });
223
- proc.on("close", (code) => resolve3(code ?? 1));
228
+ proc.on("close", (code) => resolve4(code ?? 1));
224
229
  });
225
230
  }
226
231
  async function ensureInit(cwd) {
@@ -427,6 +432,12 @@ function registerPlanCommand(program2) {
427
432
  });
428
433
  }
429
434
 
435
+ // src/commands/deploy.ts
436
+ import { spawn as spawn2 } from "child_process";
437
+ import { existsSync as existsSync3 } from "fs";
438
+ import { dirname as dirname2, resolve as pathResolve } from "path";
439
+ import { fileURLToPath } from "url";
440
+
430
441
  // src/prompt.ts
431
442
  import { createInterface } from "readline";
432
443
  async function confirm(message) {
@@ -434,69 +445,125 @@ async function confirm(message) {
434
445
  input: process.stdin,
435
446
  output: process.stdout
436
447
  });
437
- return new Promise((resolve3) => {
448
+ return new Promise((resolve4) => {
438
449
  rl.question(`${message} [y/N] `, (answer) => {
439
450
  rl.close();
440
- resolve3(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
451
+ resolve4(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
441
452
  });
442
453
  });
443
454
  }
444
455
 
445
456
  // src/commands/deploy.ts
446
457
  function registerDeployCommand(program2) {
447
- program2.command("deploy").description("Run terraform apply for a stage. Prompts for stage in a TTY when omitted.").option("-p, --profile <name>", "AWS profile").option("-s, --stage <name>", "Deployment stage").option("-c, --component <tier>", "Component tier (foundation|data|app|all)", "all").option("-y, --yes", "Skip interactive confirmation (for CI)").action(async (opts) => {
448
- const startTime = Date.now();
449
- try {
450
- const stage = await resolveStage({ flag: opts.stage });
451
- const compCheck = validateComponent(opts.component);
452
- if (!compCheck.valid) {
453
- printError(compCheck.error);
454
- process.exit(1);
455
- }
456
- const identity = getAwsIdentity();
457
- printHeader("deploy", stage, identity);
458
- if (!identity) {
459
- printWarning("Could not resolve AWS identity. Is the AWS CLI configured?");
460
- }
461
- if (isProdLike(stage) && !opts.yes) {
462
- const ok = await confirm(` Stage "${stage}" is production-like. Deploy?`);
463
- if (!ok) {
464
- console.log(" Aborted.");
465
- process.exit(0);
458
+ program2.command("deploy").description(
459
+ "Run terraform apply for a stage. Prompts for stage in a TTY when omitted."
460
+ ).option("-p, --profile <name>", "AWS profile").option("-s, --stage <name>", "Deployment stage").option(
461
+ "-c, --component <tier>",
462
+ "Component tier (foundation|data|app|all)",
463
+ "all"
464
+ ).option("-y, --yes", "Skip interactive confirmation (for CI)").action(
465
+ async (opts) => {
466
+ const startTime = Date.now();
467
+ try {
468
+ const stage = await resolveStage({ flag: opts.stage });
469
+ const compCheck = validateComponent(opts.component);
470
+ if (!compCheck.valid) {
471
+ printError(compCheck.error);
472
+ process.exit(1);
466
473
  }
467
- } else if (!opts.yes) {
468
- const ok = await confirm(` Deploy to stage "${stage}"?`);
469
- if (!ok) {
470
- console.log(" Aborted.");
471
- process.exit(0);
474
+ const identity = getAwsIdentity();
475
+ printHeader("deploy", stage, identity);
476
+ if (!identity) {
477
+ printWarning(
478
+ "Could not resolve AWS identity. Is the AWS CLI configured?"
479
+ );
472
480
  }
473
- }
474
- const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
475
- const tiers = expandComponent(opts.component);
476
- for (let i = 0; i < tiers.length; i++) {
477
- const tier = tiers[i];
478
- printTierHeader(tier, i, tiers.length);
479
- const cwd = resolveTierDir(terraformDir, stage, tier);
480
- await ensureInit(cwd);
481
- await ensureWorkspace(cwd, stage);
482
- const code = await runTerraform(cwd, [
483
- "apply",
484
- "-auto-approve",
485
- `-var=stage=${stage}`
486
- ]);
487
- if (code !== 0) {
488
- printError(`Deploy failed for ${tier} (exit ${code})`);
489
- process.exit(code);
481
+ if (isProdLike(stage) && !opts.yes) {
482
+ const ok = await confirm(
483
+ ` Stage "${stage}" is production-like. Deploy?`
484
+ );
485
+ if (!ok) {
486
+ console.log(" Aborted.");
487
+ process.exit(0);
488
+ }
489
+ } else if (!opts.yes) {
490
+ const ok = await confirm(` Deploy to stage "${stage}"?`);
491
+ if (!ok) {
492
+ console.log(" Aborted.");
493
+ process.exit(0);
494
+ }
495
+ }
496
+ const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
497
+ const tiers = expandComponent(opts.component);
498
+ for (let i = 0; i < tiers.length; i++) {
499
+ const tier = tiers[i];
500
+ printTierHeader(tier, i, tiers.length);
501
+ const cwd = resolveTierDir(terraformDir, stage, tier);
502
+ await ensureInit(cwd);
503
+ await ensureWorkspace(cwd, stage);
504
+ const code = await runTerraform(cwd, [
505
+ "apply",
506
+ "-auto-approve",
507
+ `-var=stage=${stage}`
508
+ ]);
509
+ if (code !== 0) {
510
+ printError(`Deploy failed for ${tier} (exit ${code})`);
511
+ process.exit(code);
512
+ }
490
513
  }
514
+ printSuccess("Deploy complete");
515
+ await runPostDeployProbe(stage);
516
+ printSummary("deploy", stage, tiers, startTime);
517
+ } catch (err) {
518
+ if (isCancellation(err)) return;
519
+ throw err;
491
520
  }
492
- printSuccess("Deploy complete");
493
- printSummary("deploy", stage, tiers, startTime);
494
- } catch (err) {
495
- if (isCancellation(err)) return;
496
- throw err;
497
521
  }
522
+ );
523
+ }
524
+ async function runPostDeployProbe(stage) {
525
+ const scriptPath = locatePostDeployScript();
526
+ if (!scriptPath) {
527
+ printWarning(
528
+ "post-deploy probe script not found \u2014 skipping AgentCore drift check"
529
+ );
530
+ return;
531
+ }
532
+ await new Promise((resolve4) => {
533
+ const proc = spawn2("bash", [scriptPath, "--stage", stage], {
534
+ stdio: "inherit",
535
+ env: process.env
536
+ });
537
+ proc.on("close", (code) => {
538
+ if (code !== 0) {
539
+ printWarning(
540
+ `post-deploy probe exited ${code} \u2014 deploy not rolled back`
541
+ );
542
+ }
543
+ resolve4();
544
+ });
545
+ proc.on("error", (err) => {
546
+ printWarning(`post-deploy probe spawn failed: ${err.message}`);
547
+ resolve4();
548
+ });
498
549
  });
499
550
  }
551
+ function locatePostDeployScript() {
552
+ const here = dirname2(fileURLToPath(import.meta.url));
553
+ const candidates = [
554
+ pathResolve(here, "..", "..", "..", "..", "scripts", "post-deploy.sh"),
555
+ pathResolve(process.cwd(), "scripts", "post-deploy.sh"),
556
+ pathResolve(
557
+ process.env.THINKWORK_TERRAFORM_DIR || ".",
558
+ "scripts",
559
+ "post-deploy.sh"
560
+ )
561
+ ];
562
+ for (const candidate of candidates) {
563
+ if (existsSync3(candidate)) return candidate;
564
+ }
565
+ return null;
566
+ }
500
567
 
501
568
  // src/commands/destroy.ts
502
569
  function registerDestroyCommand(program2) {
@@ -687,17 +754,17 @@ function registerOutputsCommand(program2) {
687
754
  }
688
755
 
689
756
  // src/commands/config.ts
690
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync4 } from "fs";
757
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync5 } from "fs";
691
758
  import chalk4 from "chalk";
692
759
 
693
760
  // src/environments.ts
694
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync2, readdirSync } from "fs";
761
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync2, readdirSync } from "fs";
695
762
  import { join as join2 } from "path";
696
763
  import { homedir as homedir2 } from "os";
697
764
  var THINKWORK_HOME = join2(homedir2(), ".thinkwork");
698
765
  var ENVIRONMENTS_DIR = join2(THINKWORK_HOME, "environments");
699
766
  function ensureDir(dir) {
700
- if (!existsSync3(dir)) mkdirSync2(dir, { recursive: true });
767
+ if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
701
768
  }
702
769
  function saveEnvironment(config) {
703
770
  ensureDir(ENVIRONMENTS_DIR);
@@ -710,13 +777,13 @@ function saveEnvironment(config) {
710
777
  }
711
778
  function loadEnvironment(stage) {
712
779
  const configPath = join2(ENVIRONMENTS_DIR, stage, "config.json");
713
- if (!existsSync3(configPath)) return null;
780
+ if (!existsSync4(configPath)) return null;
714
781
  return JSON.parse(readFileSync2(configPath, "utf-8"));
715
782
  }
716
783
  function listEnvironments() {
717
- if (!existsSync3(ENVIRONMENTS_DIR)) return [];
784
+ if (!existsSync4(ENVIRONMENTS_DIR)) return [];
718
785
  return readdirSync(ENVIRONMENTS_DIR).filter((name) => {
719
- return existsSync3(join2(ENVIRONMENTS_DIR, name, "config.json"));
786
+ return existsSync4(join2(ENVIRONMENTS_DIR, name, "config.json"));
720
787
  }).map((name) => {
721
788
  return JSON.parse(
722
789
  readFileSync2(join2(ENVIRONMENTS_DIR, name, "config.json"), "utf-8")
@@ -725,19 +792,19 @@ function listEnvironments() {
725
792
  }
726
793
  function resolveTerraformDir(stage) {
727
794
  const env = loadEnvironment(stage);
728
- if (env?.terraformDir && existsSync3(env.terraformDir)) {
795
+ if (env?.terraformDir && existsSync4(env.terraformDir)) {
729
796
  return env.terraformDir;
730
797
  }
731
798
  const envVar = process.env.THINKWORK_TERRAFORM_DIR;
732
- if (envVar && existsSync3(envVar)) return envVar;
799
+ if (envVar && existsSync4(envVar)) return envVar;
733
800
  const cwdTf = join2(process.cwd(), "terraform");
734
- if (existsSync3(join2(cwdTf, "main.tf"))) return cwdTf;
801
+ if (existsSync4(join2(cwdTf, "main.tf"))) return cwdTf;
735
802
  return null;
736
803
  }
737
804
 
738
805
  // src/commands/config.ts
739
806
  function readTfVar(tfvarsPath, key) {
740
- if (!existsSync4(tfvarsPath)) return null;
807
+ if (!existsSync5(tfvarsPath)) return null;
741
808
  const content = readFileSync3(tfvarsPath, "utf-8");
742
809
  const quoted = content.match(new RegExp(`^${key}\\s*=\\s*"([^"]*)"`, "m"));
743
810
  if (quoted) return quoted[1];
@@ -746,7 +813,7 @@ function readTfVar(tfvarsPath, key) {
746
813
  }
747
814
  var BARE_KEYS = /* @__PURE__ */ new Set(["enable_hindsight"]);
748
815
  function setTfVar(tfvarsPath, key, value) {
749
- if (!existsSync4(tfvarsPath)) {
816
+ if (!existsSync5(tfvarsPath)) {
750
817
  throw new Error(`terraform.tfvars not found at ${tfvarsPath}`);
751
818
  }
752
819
  let content = readFileSync3(tfvarsPath, "utf-8");
@@ -766,7 +833,7 @@ function resolveTfvarsPath(stage) {
766
833
  const tfDir = resolveTerraformDir(stage);
767
834
  if (tfDir) {
768
835
  const direct = `${tfDir}/terraform.tfvars`;
769
- if (existsSync4(direct)) return direct;
836
+ if (existsSync5(direct)) return direct;
770
837
  }
771
838
  const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
772
839
  const cwd = resolveTierDir(terraformDir, stage, "app");
@@ -793,7 +860,7 @@ function registerConfigCommand(program2) {
793
860
  console.log(` ${chalk4.bold("Updated:")} ${env.updatedAt}`);
794
861
  console.log(chalk4.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
795
862
  const tfvarsPath = `${env.terraformDir}/terraform.tfvars`;
796
- if (existsSync4(tfvarsPath)) {
863
+ if (existsSync5(tfvarsPath)) {
797
864
  console.log("");
798
865
  console.log(chalk4.dim(" terraform.tfvars:"));
799
866
  const content = readFileSync3(tfvarsPath, "utf-8");
@@ -914,28 +981,28 @@ function registerConfigCommand(program2) {
914
981
  }
915
982
 
916
983
  // src/commands/bootstrap.ts
917
- import { spawn as spawn2 } from "child_process";
984
+ import { spawn as spawn3 } from "child_process";
918
985
  import { resolve } from "path";
919
986
  function getTerraformOutput(cwd, key) {
920
- return new Promise((resolve3, reject) => {
921
- const proc = spawn2("terraform", ["output", "-raw", key], {
987
+ return new Promise((resolve4, reject) => {
988
+ const proc = spawn3("terraform", ["output", "-raw", key], {
922
989
  cwd,
923
990
  stdio: ["pipe", "pipe", "pipe"]
924
991
  });
925
992
  let stdout = "";
926
993
  proc.stdout.on("data", (d) => stdout += d);
927
994
  proc.on("close", (code) => {
928
- if (code === 0) resolve3(stdout.trim());
995
+ if (code === 0) resolve4(stdout.trim());
929
996
  else reject(new Error(`terraform output ${key} failed (exit ${code})`));
930
997
  });
931
998
  });
932
999
  }
933
1000
  function runScript(scriptPath, args) {
934
- return new Promise((resolve3) => {
935
- const proc = spawn2("bash", [scriptPath, ...args], {
1001
+ return new Promise((resolve4) => {
1002
+ const proc = spawn3("bash", [scriptPath, ...args], {
936
1003
  stdio: "inherit"
937
1004
  });
938
- proc.on("close", (code) => resolve3(code ?? 1));
1005
+ proc.on("close", (code) => resolve4(code ?? 1));
939
1006
  });
940
1007
  }
941
1008
  function registerBootstrapCommand(program2) {
@@ -990,7 +1057,7 @@ import { select as select2, Separator } from "@inquirer/prompts";
990
1057
  import chalk7 from "chalk";
991
1058
 
992
1059
  // src/aws-profiles.ts
993
- import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
1060
+ import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
994
1061
  import { homedir as homedir3 } from "os";
995
1062
  import { join as join3 } from "path";
996
1063
  var CREDENTIALS_PATH = join3(homedir3(), ".aws", "credentials");
@@ -1033,7 +1100,7 @@ function classify(fields) {
1033
1100
  }
1034
1101
  function listAwsProfiles() {
1035
1102
  const byName = /* @__PURE__ */ new Map();
1036
- if (existsSync5(CREDENTIALS_PATH)) {
1103
+ if (existsSync6(CREDENTIALS_PATH)) {
1037
1104
  const sections = parseIni(readFileSync4(CREDENTIALS_PATH, "utf-8"));
1038
1105
  for (const [section, fields] of Object.entries(sections)) {
1039
1106
  byName.set(section, {
@@ -1043,7 +1110,7 @@ function listAwsProfiles() {
1043
1110
  });
1044
1111
  }
1045
1112
  }
1046
- if (existsSync5(CONFIG_PATH)) {
1113
+ if (existsSync6(CONFIG_PATH)) {
1047
1114
  const sections = parseIni(readFileSync4(CONFIG_PATH, "utf-8"));
1048
1115
  for (const [section, fields] of Object.entries(sections)) {
1049
1116
  const name = normalizeConfigSection(section);
@@ -1271,7 +1338,7 @@ function discoverCognitoConfig(stage, region) {
1271
1338
  // src/cognito-oauth.ts
1272
1339
  import { createServer } from "http";
1273
1340
  import { randomBytes } from "crypto";
1274
- import { spawn as spawn3 } from "child_process";
1341
+ import { spawn as spawn4 } from "child_process";
1275
1342
  import chalk6 from "chalk";
1276
1343
  var CLI_LOOPBACK_PORT = 42010;
1277
1344
  var CALLBACK_PATH = "/callback";
@@ -1310,7 +1377,7 @@ function buildAuthorizeUrl(cognito, redirectUri, state) {
1310
1377
  return `${cognito.domainUrl}/oauth2/authorize?${params.toString()}`;
1311
1378
  }
1312
1379
  function waitForCallbackCode(opts) {
1313
- return new Promise((resolve3, reject) => {
1380
+ return new Promise((resolve4, reject) => {
1314
1381
  const server = createServer((req, res) => handleRequest(req, res));
1315
1382
  let finished = false;
1316
1383
  const finish = (err, code) => {
@@ -1321,7 +1388,7 @@ function waitForCallbackCode(opts) {
1321
1388
  closer.closeAllConnections?.();
1322
1389
  server.close(() => {
1323
1390
  if (err) reject(err);
1324
- else resolve3(code);
1391
+ else resolve4(code);
1325
1392
  });
1326
1393
  };
1327
1394
  const timer = setTimeout(() => {
@@ -1450,7 +1517,7 @@ function openInBrowser(url) {
1450
1517
  const cmd = platform2 === "darwin" ? "open" : platform2 === "win32" ? "cmd" : "xdg-open";
1451
1518
  const args = platform2 === "win32" ? ["/c", "start", "", url] : [url];
1452
1519
  try {
1453
- spawn3(cmd, args, { stdio: "ignore", detached: true }).unref();
1520
+ spawn4(cmd, args, { stdio: "ignore", detached: true }).unref();
1454
1521
  } catch {
1455
1522
  }
1456
1523
  }
@@ -1512,10 +1579,10 @@ function escapeHtml(s) {
1512
1579
  // src/commands/login.ts
1513
1580
  function ask(prompt) {
1514
1581
  const rl = createInterface2({ input: process.stdin, output: process.stdout });
1515
- return new Promise((resolve3) => {
1582
+ return new Promise((resolve4) => {
1516
1583
  rl.question(prompt, (answer) => {
1517
1584
  rl.close();
1518
- resolve3(answer.trim());
1585
+ resolve4(answer.trim());
1519
1586
  });
1520
1587
  });
1521
1588
  }
@@ -1994,20 +2061,20 @@ Examples:
1994
2061
  }
1995
2062
 
1996
2063
  // src/commands/init.ts
1997
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, cpSync } from "fs";
1998
- import { resolve as resolve2, join as join5, dirname as dirname2 } from "path";
2064
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, cpSync } from "fs";
2065
+ import { resolve as resolve2, join as join5, dirname as dirname3 } from "path";
1999
2066
  import { execSync as execSync7 } from "child_process";
2000
- import { fileURLToPath } from "url";
2067
+ import { fileURLToPath as fileURLToPath2 } from "url";
2001
2068
  import { createInterface as createInterface3 } from "readline";
2002
2069
  import chalk8 from "chalk";
2003
- var __dirname = dirname2(fileURLToPath(import.meta.url));
2070
+ var __dirname = dirname3(fileURLToPath2(import.meta.url));
2004
2071
  function ask2(prompt, defaultVal = "") {
2005
2072
  const rl = createInterface3({ input: process.stdin, output: process.stdout });
2006
2073
  const suffix = defaultVal ? chalk8.dim(` [${defaultVal}]`) : "";
2007
- return new Promise((resolve3) => {
2074
+ return new Promise((resolve4) => {
2008
2075
  rl.question(` ${prompt}${suffix}: `, (answer) => {
2009
2076
  rl.close();
2010
- resolve3(answer.trim() || defaultVal);
2077
+ resolve4(answer.trim() || defaultVal);
2011
2078
  });
2012
2079
  });
2013
2080
  }
@@ -2025,9 +2092,9 @@ function generateSecret(length = 32) {
2025
2092
  }
2026
2093
  function findBundledTerraform() {
2027
2094
  const bundled = resolve2(__dirname, "terraform");
2028
- if (existsSync7(join5(bundled, "modules"))) return bundled;
2095
+ if (existsSync8(join5(bundled, "modules"))) return bundled;
2029
2096
  const repoTf = resolve2(__dirname, "..", "..", "..", "terraform");
2030
- if (existsSync7(join5(repoTf, "modules"))) return repoTf;
2097
+ if (existsSync8(join5(repoTf, "modules"))) return repoTf;
2031
2098
  throw new Error(
2032
2099
  "Terraform modules not found. The CLI package may be incomplete.\nTry reinstalling: npm install -g thinkwork-cli@latest"
2033
2100
  );
@@ -2087,9 +2154,9 @@ function registerInitCommand(program2) {
2087
2154
  printError("Stage name is required. Pass -s <name> or re-run in an interactive terminal.");
2088
2155
  process.exit(1);
2089
2156
  }
2090
- const { input: input5 } = await import("@inquirer/prompts");
2157
+ const { input: input4 } = await import("@inquirer/prompts");
2091
2158
  try {
2092
- stage = await input5({
2159
+ stage = await input4({
2093
2160
  message: "Stage name (e.g. dev, staging, prod):",
2094
2161
  validate: (v) => validateStage(v).error ?? true
2095
2162
  });
@@ -2119,7 +2186,7 @@ function registerInitCommand(program2) {
2119
2186
  const targetDir = resolve2(opts.dir);
2120
2187
  const tfDir = join5(targetDir, "terraform");
2121
2188
  const tfvarsPath = join5(tfDir, "terraform.tfvars");
2122
- if (existsSync7(tfvarsPath)) {
2189
+ if (existsSync8(tfvarsPath)) {
2123
2190
  printWarning(`terraform.tfvars already exists at ${tfvarsPath}`);
2124
2191
  const overwrite = await ask2("Overwrite?", "N");
2125
2192
  if (overwrite.toLowerCase() !== "y") {
@@ -2189,18 +2256,18 @@ function registerInitCommand(program2) {
2189
2256
  for (const dir of copyDirs) {
2190
2257
  const src = join5(bundledTf, dir);
2191
2258
  const dst = join5(tfDir, dir);
2192
- if (existsSync7(src) && !existsSync7(dst)) {
2259
+ if (existsSync8(src) && !existsSync8(dst)) {
2193
2260
  cpSync(src, dst, { recursive: true });
2194
2261
  }
2195
2262
  }
2196
2263
  const schemaPath = join5(bundledTf, "schema.graphql");
2197
- if (existsSync7(schemaPath) && !existsSync7(join5(tfDir, "schema.graphql"))) {
2264
+ if (existsSync8(schemaPath) && !existsSync8(join5(tfDir, "schema.graphql"))) {
2198
2265
  cpSync(schemaPath, join5(tfDir, "schema.graphql"));
2199
2266
  }
2200
2267
  const tfvars = buildTfvars(config);
2201
2268
  writeFileSync4(tfvarsPath, tfvars);
2202
2269
  const mainTfPath = join5(tfDir, "main.tf");
2203
- if (!existsSync7(mainTfPath)) {
2270
+ if (!existsSync8(mainTfPath)) {
2204
2271
  writeFileSync4(mainTfPath, `################################################################################
2205
2272
  # Thinkwork \u2014 ${config.stage}
2206
2273
  # Generated by: thinkwork init -s ${config.stage}
@@ -2629,11 +2696,121 @@ and AgentCore for per-stage detail.
2629
2696
  // src/commands/mcp.ts
2630
2697
  import chalk10 from "chalk";
2631
2698
 
2699
+ // ../../packages/admin-ops/src/client.ts
2700
+ var AdminOpsError = class extends Error {
2701
+ status;
2702
+ body;
2703
+ constructor(status, message, body) {
2704
+ super(message);
2705
+ this.name = "AdminOpsError";
2706
+ this.status = status;
2707
+ this.body = body;
2708
+ }
2709
+ };
2710
+ function createClient(config) {
2711
+ const fetchImpl = config.fetchImpl ?? fetch;
2712
+ const base = config.apiUrl.replace(/\/+$/, "");
2713
+ const doFetch = async (path2, init = {}) => {
2714
+ const headers = {
2715
+ "Content-Type": "application/json",
2716
+ Authorization: `Bearer ${config.authSecret}`,
2717
+ "x-api-key": config.authSecret
2718
+ };
2719
+ if (config.principalId) headers["x-principal-id"] = config.principalId;
2720
+ if (config.principalEmail) headers["x-principal-email"] = config.principalEmail;
2721
+ if (config.tenantId) headers["x-tenant-id"] = config.tenantId;
2722
+ if (config.agentId) headers["x-agent-id"] = config.agentId;
2723
+ const res = await fetchImpl(`${base}${path2}`, {
2724
+ ...init,
2725
+ headers: { ...headers, ...init.headers }
2726
+ });
2727
+ if (!res.ok) {
2728
+ const body = await res.json().catch(() => ({}));
2729
+ const message = body.error ?? `HTTP ${res.status}`;
2730
+ throw new AdminOpsError(res.status, message, body);
2731
+ }
2732
+ return res.json();
2733
+ };
2734
+ const doGraphql = async (query, variables) => {
2735
+ const res = await doFetch("/graphql", {
2736
+ method: "POST",
2737
+ body: JSON.stringify({ query, variables: variables ?? {} })
2738
+ });
2739
+ if (res.errors && res.errors.length > 0) {
2740
+ const msg = res.errors.map((e) => e.message).join("; ");
2741
+ throw new AdminOpsError(200, msg, res);
2742
+ }
2743
+ return res.data;
2744
+ };
2745
+ return {
2746
+ apiUrl: base,
2747
+ tenantId: config.tenantId,
2748
+ fetch: doFetch,
2749
+ graphql: doGraphql,
2750
+ withTenant(tenantId) {
2751
+ return createClient({ ...config, tenantId });
2752
+ }
2753
+ };
2754
+ }
2755
+
2756
+ // ../../packages/admin-ops/src/tenants.ts
2757
+ var tenants_exports = {};
2758
+ __export(tenants_exports, {
2759
+ getTenant: () => getTenant,
2760
+ getTenantBySlug: () => getTenantBySlug,
2761
+ listTenants: () => listTenants,
2762
+ updateTenant: () => updateTenant
2763
+ });
2764
+ async function listTenants(client) {
2765
+ return client.fetch("/api/tenants");
2766
+ }
2767
+ async function getTenant(client, id) {
2768
+ return client.fetch(`/api/tenants/${encodeURIComponent(id)}`);
2769
+ }
2770
+ async function getTenantBySlug(client, slug) {
2771
+ return client.fetch(`/api/tenants/by-slug/${encodeURIComponent(slug)}`);
2772
+ }
2773
+ async function updateTenant(client, id, input4) {
2774
+ return client.fetch(`/api/tenants/${encodeURIComponent(id)}`, {
2775
+ method: "PUT",
2776
+ body: JSON.stringify(input4)
2777
+ });
2778
+ }
2779
+
2780
+ // ../../packages/admin-ops/src/admin-keys.ts
2781
+ var admin_keys_exports = {};
2782
+ __export(admin_keys_exports, {
2783
+ createAdminKey: () => createAdminKey,
2784
+ listAdminKeys: () => listAdminKeys,
2785
+ revokeAdminKey: () => revokeAdminKey
2786
+ });
2787
+ async function createAdminKey(client, tenantIdOrSlug, input4 = {}) {
2788
+ return client.fetch(
2789
+ `/api/tenants/${encodeURIComponent(tenantIdOrSlug)}/mcp-admin-keys`,
2790
+ {
2791
+ method: "POST",
2792
+ body: JSON.stringify(input4)
2793
+ }
2794
+ );
2795
+ }
2796
+ async function listAdminKeys(client, tenantIdOrSlug) {
2797
+ const res = await client.fetch(
2798
+ `/api/tenants/${encodeURIComponent(tenantIdOrSlug)}/mcp-admin-keys`
2799
+ );
2800
+ return res.keys;
2801
+ }
2802
+ async function revokeAdminKey(client, tenantIdOrSlug, keyId) {
2803
+ await client.fetch(
2804
+ `/api/tenants/${encodeURIComponent(tenantIdOrSlug)}/mcp-admin-keys/${encodeURIComponent(keyId)}`,
2805
+ { method: "DELETE" }
2806
+ );
2807
+ }
2808
+
2632
2809
  // src/api-client.ts
2633
- import { readFileSync as readFileSync5, existsSync as existsSync8 } from "fs";
2810
+ import { readFileSync as readFileSync5, existsSync as existsSync9 } from "fs";
2634
2811
  import { execSync as execSync9 } from "child_process";
2635
2812
  function readTfVar2(tfvarsPath, key) {
2636
- if (!existsSync8(tfvarsPath)) return null;
2813
+ if (!existsSync9(tfvarsPath)) return null;
2637
2814
  const content = readFileSync5(tfvarsPath, "utf-8");
2638
2815
  const match = content.match(new RegExp(`^${key}\\s*=\\s*"([^"]*)"`, "m"));
2639
2816
  return match ? match[1] : null;
@@ -2642,7 +2819,7 @@ function resolveTfvarsPath2(stage) {
2642
2819
  const tfDir = resolveTerraformDir(stage);
2643
2820
  if (tfDir) {
2644
2821
  const direct = `${tfDir}/terraform.tfvars`;
2645
- if (existsSync8(direct)) return direct;
2822
+ if (existsSync9(direct)) return direct;
2646
2823
  }
2647
2824
  const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
2648
2825
  const cwd = resolveTierDir(terraformDir, stage, "app");
@@ -2949,14 +3126,14 @@ Examples:
2949
3126
  $ thinkwork mcp add my-tools --url https://mcp.example.com/crm \\
2950
3127
  --auth-type tenant_api_key --api-key sk-abc -s dev -t acme
2951
3128
 
2952
- # OAuth connector (users connect from the mobile app)
2953
- $ thinkwork mcp add lastmile --url https://dev-mcp.lastmile-tei.com/crm \\
2954
- --auth-type per_user_oauth --oauth-provider lastmile -s dev -t acme
3129
+ # OAuth-backed MCP integration (users connect from the mobile app)
3130
+ $ thinkwork mcp add crm-tools --url https://mcp.example.com/crm \\
3131
+ --auth-type per_user_oauth --oauth-provider crm_provider -s dev -t acme
2955
3132
  `
2956
3133
  ).action(
2957
3134
  async (nameArg, opts) => {
2958
3135
  try {
2959
- const { input: input5 } = await import("@inquirer/prompts");
3136
+ const { input: input4 } = await import("@inquirer/prompts");
2960
3137
  const { stage, api, tenant } = await resolveMcpContext(opts);
2961
3138
  let name = nameArg;
2962
3139
  if (!name) {
@@ -2964,7 +3141,7 @@ Examples:
2964
3141
  printError("Name is required. Pass it as a positional arg.");
2965
3142
  process.exit(1);
2966
3143
  }
2967
- name = await input5({ message: "Server name:" });
3144
+ name = await input4({ message: "Server name:" });
2968
3145
  }
2969
3146
  let url = opts.url;
2970
3147
  if (!url) {
@@ -2972,7 +3149,7 @@ Examples:
2972
3149
  printError("--url is required. Pass it as a flag.");
2973
3150
  process.exit(1);
2974
3151
  }
2975
- url = await input5({
3152
+ url = await input4({
2976
3153
  message: "MCP server URL:",
2977
3154
  validate: (v) => v.startsWith("http://") || v.startsWith("https://") ? true : "URL must start with http:// or https://"
2978
3155
  });
@@ -3006,13 +3183,13 @@ Examples:
3006
3183
  `
3007
3184
  Examples:
3008
3185
  # Change URL in place (preserves agent assignments, unlike remove + re-add)
3009
- $ thinkwork mcp update lastmile-routing --url https://dev-mcp.lastmile-tei.com/routing
3186
+ $ thinkwork mcp update routing-server --url https://mcp.example.com/routing
3010
3187
 
3011
3188
  # Disable without deleting
3012
- $ thinkwork mcp update lastmile-routing --disable
3189
+ $ thinkwork mcp update routing-server --disable
3013
3190
 
3014
3191
  # Rename + change transport
3015
- $ thinkwork mcp update 629dcee1-1e14-4b83-9907-cb529e6035f6 --name "LastMile Routing" --transport sse
3192
+ $ thinkwork mcp update 629dcee1-1e14-4b83-9907-cb529e6035f6 --name "Routing Server" --transport sse
3016
3193
 
3017
3194
  # Interactive \u2014 pick the server from a list
3018
3195
  $ thinkwork mcp update
@@ -3065,7 +3242,7 @@ Examples:
3065
3242
  $ thinkwork mcp remove
3066
3243
 
3067
3244
  # By slug (case-insensitive)
3068
- $ thinkwork mcp remove lastmile-routing
3245
+ $ thinkwork mcp remove routing-server
3069
3246
 
3070
3247
  # By UUID (from \`mcp list\` or --json)
3071
3248
  $ thinkwork mcp remove 629dcee1-1e14-4b83-9907-cb529e6035f6
@@ -3132,7 +3309,7 @@ Examples:
3132
3309
  ).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--agent <id>", "Agent ID").action(
3133
3310
  async (mcpServerArg, opts) => {
3134
3311
  try {
3135
- const { input: input5 } = await import("@inquirer/prompts");
3312
+ const { input: input4 } = await import("@inquirer/prompts");
3136
3313
  const { api, tenant } = await resolveMcpContext(opts);
3137
3314
  const server = await resolveServer(mcpServerArg, api, tenant.slug);
3138
3315
  let agent = opts.agent;
@@ -3141,7 +3318,7 @@ Examples:
3141
3318
  printError("--agent is required. Pass it as a flag.");
3142
3319
  process.exit(1);
3143
3320
  }
3144
- agent = await input5({ message: "Agent ID:" });
3321
+ agent = await input4({ message: "Agent ID:" });
3145
3322
  }
3146
3323
  const result = await apiFetch(
3147
3324
  api.apiUrl,
@@ -3164,7 +3341,7 @@ Examples:
3164
3341
  ).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--agent <id>", "Agent ID").action(
3165
3342
  async (mcpServerArg, opts) => {
3166
3343
  try {
3167
- const { input: input5 } = await import("@inquirer/prompts");
3344
+ const { input: input4 } = await import("@inquirer/prompts");
3168
3345
  const { api, tenant } = await resolveMcpContext(opts);
3169
3346
  const server = await resolveServer(mcpServerArg, api, tenant.slug);
3170
3347
  let agent = opts.agent;
@@ -3173,7 +3350,7 @@ Examples:
3173
3350
  printError("--agent is required. Pass it as a flag.");
3174
3351
  process.exit(1);
3175
3352
  }
3176
- agent = await input5({ message: "Agent ID:" });
3353
+ agent = await input4({ message: "Agent ID:" });
3177
3354
  }
3178
3355
  await apiFetch(
3179
3356
  api.apiUrl,
@@ -3189,6 +3366,175 @@ Examples:
3189
3366
  }
3190
3367
  }
3191
3368
  );
3369
+ const key = mcp.command("key").alias("keys").description("Manage per-tenant Bearer tokens for the admin-ops MCP server.");
3370
+ key.command("create").description(
3371
+ "Generate a new MCP admin token. Prints the raw token ONCE \u2014 save it immediately."
3372
+ ).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug or UUID").option("--name <label>", 'Human label for the key (default "default")').addHelpText(
3373
+ "after",
3374
+ `
3375
+ Examples:
3376
+ $ thinkwork mcp key create -t acme --name ci
3377
+ $ thinkwork mcp key create -t acme --json # token surfaces under .token
3378
+
3379
+ The token is shown ONCE. Hand it to the MCP client immediately; the server
3380
+ stores only the SHA-256 hash. To rotate, create a new one and revoke the
3381
+ old one.
3382
+ `
3383
+ ).action(async (opts) => {
3384
+ try {
3385
+ const ctx = await resolveMcpContext(opts);
3386
+ const client = createClient({
3387
+ apiUrl: ctx.api.apiUrl,
3388
+ authSecret: ctx.api.authSecret
3389
+ });
3390
+ const created = await admin_keys_exports.createAdminKey(client, ctx.tenant.slug, {
3391
+ name: opts.name
3392
+ });
3393
+ printJson(created);
3394
+ printWarning("This token will NOT be shown again. Copy it now.");
3395
+ printSuccess(`MCP admin key created: ${chalk10.bold(created.name)} (${created.id})`);
3396
+ console.log(` ${chalk10.dim("Token:")} ${chalk10.cyan(created.token)}`);
3397
+ } catch (err) {
3398
+ if (isCancellation(err)) return;
3399
+ printError(err instanceof Error ? err.message : String(err));
3400
+ process.exit(1);
3401
+ }
3402
+ });
3403
+ key.command("list").alias("ls").description("List MCP admin keys for a tenant (metadata only, never the raw token).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug or UUID").option("--all", "Include revoked keys").action(async (opts) => {
3404
+ try {
3405
+ const ctx = await resolveMcpContext(opts);
3406
+ const client = createClient({
3407
+ apiUrl: ctx.api.apiUrl,
3408
+ authSecret: ctx.api.authSecret
3409
+ });
3410
+ const all = await admin_keys_exports.listAdminKeys(client, ctx.tenant.slug);
3411
+ const rows = opts.all ? all : all.filter((k) => !k.revoked_at);
3412
+ printJson(rows);
3413
+ printTable(rows, [
3414
+ { key: "name", header: "NAME" },
3415
+ { key: "id", header: "ID" },
3416
+ { key: "created_at", header: "CREATED" },
3417
+ { key: "last_used_at", header: "LAST USED" },
3418
+ { key: "revoked_at", header: "REVOKED" }
3419
+ ]);
3420
+ } catch (err) {
3421
+ if (isCancellation(err)) return;
3422
+ printError(err instanceof Error ? err.message : String(err));
3423
+ process.exit(1);
3424
+ }
3425
+ });
3426
+ key.command("revoke <id>").description("Revoke an MCP admin key. Idempotent \u2014 revoking an already-revoked key is a no-op.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug or UUID").action(async (keyId, opts) => {
3427
+ try {
3428
+ const ctx = await resolveMcpContext(opts);
3429
+ const client = createClient({
3430
+ apiUrl: ctx.api.apiUrl,
3431
+ authSecret: ctx.api.authSecret
3432
+ });
3433
+ await admin_keys_exports.revokeAdminKey(client, ctx.tenant.slug, keyId);
3434
+ printSuccess(`MCP admin key revoked: ${keyId}`);
3435
+ } catch (err) {
3436
+ if (err instanceof AdminOpsError && err.status === 404) {
3437
+ printError(`Key ${keyId} not found for tenant ${opts.tenant ?? ""}`);
3438
+ process.exit(2);
3439
+ }
3440
+ if (isCancellation(err)) return;
3441
+ printError(err instanceof Error ? err.message : String(err));
3442
+ process.exit(1);
3443
+ }
3444
+ });
3445
+ mcp.command("provision").description(
3446
+ "Provision the admin-ops MCP for a tenant: mint a new key + store in Secrets Manager + register in tenant_mcp_servers."
3447
+ ).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug or UUID (omit with --all)").option(
3448
+ "--url <url>",
3449
+ "Override the MCP endpoint URL (defaults to the stage's execute-api or MCP_CUSTOM_DOMAIN)"
3450
+ ).option("--all", "Provision for every tenant the caller can see (backfill mode)").addHelpText(
3451
+ "after",
3452
+ `
3453
+ Examples:
3454
+ # One tenant (interactive picker or -t)
3455
+ $ thinkwork mcp provision -t acme
3456
+
3457
+ # Explicit custom-domain URL
3458
+ $ thinkwork mcp provision -t acme --url https://mcp.thinkwork.ai/mcp/admin
3459
+
3460
+ # Backfill every tenant at once
3461
+ $ thinkwork mcp provision --all
3462
+
3463
+ The raw token is stored in Secrets Manager and duplicated into
3464
+ tenant_mcp_servers.auth_config \u2014 it is NOT printed on stdout. After
3465
+ provisioning, each agent still needs the server assigned via the admin
3466
+ SPA (or a future \`thinkwork mcp assign\` CLI pass).
3467
+ `
3468
+ ).action(async (opts) => {
3469
+ try {
3470
+ if (opts.all && opts.tenant) {
3471
+ printError("--all and --tenant are mutually exclusive.");
3472
+ process.exit(1);
3473
+ }
3474
+ const stage = await resolveStage({ flag: opts.stage });
3475
+ const api = resolveApiConfig(stage);
3476
+ if (!api) process.exit(1);
3477
+ async function provisionOne(tenantIdOrSlug, label) {
3478
+ const res = await apiFetch(
3479
+ api.apiUrl,
3480
+ api.authSecret,
3481
+ `/api/tenants/${encodeURIComponent(tenantIdOrSlug)}/mcp-admin-provision`,
3482
+ {
3483
+ method: "POST",
3484
+ body: JSON.stringify(opts.url ? { url: opts.url } : {})
3485
+ }
3486
+ );
3487
+ printSuccess(
3488
+ `${label}: ${res.provisioned} (tenant_mcp_servers.id=${res.tenantMcpServerId}, url=${res.url})`
3489
+ );
3490
+ return res;
3491
+ }
3492
+ if (opts.all) {
3493
+ const tenantRows = await apiFetch(
3494
+ api.apiUrl,
3495
+ api.authSecret,
3496
+ "/api/tenants",
3497
+ {}
3498
+ );
3499
+ if (!Array.isArray(tenantRows) || tenantRows.length === 0) {
3500
+ printWarning("No tenants found.");
3501
+ return;
3502
+ }
3503
+ printHeader("mcp provision --all", stage);
3504
+ const results = [];
3505
+ for (const t of tenantRows) {
3506
+ try {
3507
+ await provisionOne(t.slug, `${t.name} (${t.slug})`);
3508
+ results.push({ slug: t.slug, ok: true });
3509
+ } catch (err) {
3510
+ const msg = err instanceof Error ? err.message : String(err);
3511
+ printError(` \u2717 ${t.slug}: ${msg}`);
3512
+ results.push({ slug: t.slug, ok: false, reason: msg });
3513
+ }
3514
+ }
3515
+ const ok = results.filter((r) => r.ok).length;
3516
+ const bad = results.length - ok;
3517
+ if (bad === 0) {
3518
+ printSuccess(`All ${ok} tenants provisioned.`);
3519
+ } else {
3520
+ printWarning(`${ok}/${results.length} succeeded; ${bad} failed.`);
3521
+ process.exit(1);
3522
+ }
3523
+ return;
3524
+ }
3525
+ const tenant = await resolveTenantRest({
3526
+ flag: opts.tenant,
3527
+ stage,
3528
+ apiUrl: api.apiUrl,
3529
+ authSecret: api.authSecret
3530
+ });
3531
+ await provisionOne(tenant.slug, tenant.slug);
3532
+ } catch (err) {
3533
+ if (isCancellation(err)) return;
3534
+ printError(err instanceof Error ? err.message : String(err));
3535
+ process.exit(1);
3536
+ }
3537
+ });
3192
3538
  }
3193
3539
 
3194
3540
  // src/commands/tools.ts
@@ -3463,11 +3809,11 @@ function registerUpdateCommand(program2) {
3463
3809
  }
3464
3810
 
3465
3811
  // src/commands/user.ts
3466
- import { spawn as spawn4 } from "child_process";
3812
+ import { spawn as spawn5 } from "child_process";
3467
3813
  import { input as input2, select as select7 } from "@inquirer/prompts";
3468
3814
  function getTerraformOutput2(cwd, key) {
3469
- return new Promise((resolve3, reject) => {
3470
- const proc = spawn4("terraform", ["output", "-raw", key], {
3815
+ return new Promise((resolve4, reject) => {
3816
+ const proc = spawn5("terraform", ["output", "-raw", key], {
3471
3817
  cwd,
3472
3818
  stdio: ["pipe", "pipe", "pipe"]
3473
3819
  });
@@ -3476,7 +3822,7 @@ function getTerraformOutput2(cwd, key) {
3476
3822
  proc.stdout.on("data", (d) => stdout += d);
3477
3823
  proc.stderr.on("data", (d) => stderr += d);
3478
3824
  proc.on("close", (code) => {
3479
- if (code === 0) resolve3(stdout.trim());
3825
+ if (code === 0) resolve4(stdout.trim());
3480
3826
  else
3481
3827
  reject(
3482
3828
  new Error(
@@ -3487,7 +3833,7 @@ function getTerraformOutput2(cwd, key) {
3487
3833
  });
3488
3834
  }
3489
3835
  function runAwsCognitoReset(userPoolId, username, region) {
3490
- return new Promise((resolve3) => {
3836
+ return new Promise((resolve4) => {
3491
3837
  const args = [
3492
3838
  "cognito-idp",
3493
3839
  "admin-reset-user-password",
@@ -3499,12 +3845,12 @@ function runAwsCognitoReset(userPoolId, username, region) {
3499
3845
  "json"
3500
3846
  ];
3501
3847
  if (region) args.push("--region", region);
3502
- const proc = spawn4("aws", args, { stdio: ["ignore", "pipe", "pipe"] });
3848
+ const proc = spawn5("aws", args, { stdio: ["ignore", "pipe", "pipe"] });
3503
3849
  let stdout = "";
3504
3850
  let stderr = "";
3505
3851
  proc.stdout.on("data", (d) => stdout += d);
3506
3852
  proc.stderr.on("data", (d) => stderr += d);
3507
- proc.on("close", (code) => resolve3({ code: code ?? 1, stdout, stderr }));
3853
+ proc.on("close", (code) => resolve4({ code: code ?? 1, stdout, stderr }));
3508
3854
  });
3509
3855
  }
3510
3856
  function requireTty2(label) {
@@ -3785,9 +4131,8 @@ import { gql } from "@urql/core";
3785
4131
 
3786
4132
  // src/lib/gql-client.ts
3787
4133
  import {
3788
- Client,
3789
- cacheExchange,
3790
- fetchExchange
4134
+ CombinedError,
4135
+ stringifyDocument
3791
4136
  } from "@urql/core";
3792
4137
 
3793
4138
  // src/lib/resolve-auth.ts
@@ -3883,20 +4228,7 @@ async function getGqlClient(opts) {
3883
4228
  }
3884
4229
  const url = `${baseUrl.replace(/\/+$/, "")}/graphql`;
3885
4230
  const auth = await resolveAuth({ stage: opts.stage, region });
3886
- const client = new Client({
3887
- url,
3888
- exchanges: [cacheExchange, fetchExchange],
3889
- fetchOptions: () => ({
3890
- method: "POST",
3891
- headers: {
3892
- "content-type": "application/json",
3893
- ...auth.headers
3894
- }
3895
- }),
3896
- // CLI calls are short-lived and we want server truth on every run —
3897
- // bypass the in-memory cache to avoid stale reads between quick commands.
3898
- requestPolicy: "network-only"
3899
- });
4231
+ const client = createCliGqlClient(url, auth.headers);
3900
4232
  return {
3901
4233
  client,
3902
4234
  url,
@@ -3904,14 +4236,95 @@ async function getGqlClient(opts) {
3904
4236
  tenantSlug: auth.tenantSlug
3905
4237
  };
3906
4238
  }
4239
+ function createCliGqlClient(url, headers) {
4240
+ return {
4241
+ query: (doc, variables) => ({
4242
+ toPromise: () => executeGraphql(url, headers, doc, variables)
4243
+ }),
4244
+ mutation: (doc, variables) => ({
4245
+ toPromise: () => executeGraphql(url, headers, doc, variables)
4246
+ })
4247
+ };
4248
+ }
3907
4249
  async function gqlQuery(client, doc, variables) {
3908
- const res = await client.query(doc, variables).toPromise();
4250
+ const res = await client.query(serializeDocument(doc), variables).toPromise();
3909
4251
  return unwrap(res);
3910
4252
  }
3911
4253
  async function gqlMutate(client, doc, variables) {
3912
- const res = await client.mutation(doc, variables).toPromise();
4254
+ const res = await client.mutation(serializeDocument(doc), variables).toPromise();
3913
4255
  return unwrap(res);
3914
4256
  }
4257
+ function serializeDocument(doc) {
4258
+ return stringifyDocument(doc);
4259
+ }
4260
+ async function executeGraphql(url, headers, doc, variables) {
4261
+ const query = serializeDocument(doc);
4262
+ try {
4263
+ const response = await fetch(url, {
4264
+ method: "POST",
4265
+ headers: {
4266
+ "content-type": "application/json",
4267
+ ...headers
4268
+ },
4269
+ body: JSON.stringify({ query, variables })
4270
+ });
4271
+ const text = await response.text();
4272
+ let payload = {};
4273
+ if (text) {
4274
+ try {
4275
+ payload = JSON.parse(text);
4276
+ } catch {
4277
+ return makeNetworkErrorResult(
4278
+ `GraphQL request failed with non-JSON response: ${text}`,
4279
+ response
4280
+ );
4281
+ }
4282
+ }
4283
+ if (payload.errors?.length) {
4284
+ return {
4285
+ data: payload.data,
4286
+ error: new CombinedError({
4287
+ graphQLErrors: payload.errors,
4288
+ response
4289
+ }),
4290
+ extensions: payload.extensions,
4291
+ stale: false,
4292
+ hasNext: false
4293
+ };
4294
+ }
4295
+ if (!response.ok) {
4296
+ return makeNetworkErrorResult(
4297
+ `GraphQL request failed with HTTP ${response.status}`,
4298
+ response
4299
+ );
4300
+ }
4301
+ return {
4302
+ data: payload.data,
4303
+ error: void 0,
4304
+ extensions: payload.extensions,
4305
+ stale: false,
4306
+ hasNext: false
4307
+ };
4308
+ } catch (err) {
4309
+ return {
4310
+ error: new CombinedError({
4311
+ networkError: err instanceof Error ? err : new Error(String(err))
4312
+ }),
4313
+ stale: false,
4314
+ hasNext: false
4315
+ };
4316
+ }
4317
+ }
4318
+ function makeNetworkErrorResult(message, response) {
4319
+ return {
4320
+ error: new CombinedError({
4321
+ networkError: new Error(message),
4322
+ response
4323
+ }),
4324
+ stale: false,
4325
+ hasNext: false
4326
+ };
4327
+ }
3915
4328
  function unwrap(res) {
3916
4329
  if (res.error) {
3917
4330
  const msg = res.error.graphQLErrors.map((e) => e.message).filter(Boolean).join("; ") || res.error.networkError?.message || "GraphQL request failed";
@@ -4012,20 +4425,20 @@ function notYetImplemented(commandPath, phase) {
4012
4425
  // src/commands/thread.ts
4013
4426
  function registerThreadCommand(program2) {
4014
4427
  const thread = program2.command("thread").alias("threads").description(
4015
- "Create, list, update, and comment on threads (tasks, chats, bugs, questions) in a tenant."
4428
+ "Create, list, update, and comment on threads in a tenant."
4016
4429
  );
4017
- thread.command("list").alias("ls").description("List threads in a tenant with optional filters.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--status <status>", "Filter: BACKLOG | TODO | IN_PROGRESS | IN_REVIEW | BLOCKED | DONE | CANCELLED").option("--priority <p>", "Filter: LOW | MEDIUM | HIGH | URGENT").option("--assignee <id>", "Filter by assignee (user or agent ID). Use `me` to match the caller.").option("--agent <id>", "Filter threads worked on by a specific agent").option("--search <q>", "Full-text search over title/body").option("--limit <n>", "Max rows (default 50)", "50").option("--archived", "Include archived threads").addHelpText(
4430
+ thread.command("list").alias("ls").description("List threads in a tenant with optional filters.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--assignee <id>", "Filter by assignee (user or agent ID). Use `me` to match the caller.").option("--agent <id>", "Filter threads worked on by a specific agent").option("--search <q>", "Full-text search over thread titles").option("--limit <n>", "Max rows (default 50)", "50").option("--archived", "Include archived threads").addHelpText(
4018
4431
  "after",
4019
4432
  `
4020
4433
  Examples:
4021
- # Open work on the default stage/tenant
4022
- $ thinkwork thread list --status IN_PROGRESS
4023
-
4024
- # Pipe to jq
4025
- $ thinkwork thread list --json | jq '.[] | select(.priority=="URGENT")'
4026
-
4027
4434
  # Everything assigned to me
4028
4435
  $ thinkwork thread list --assignee me
4436
+
4437
+ # Limit + JSON for piping
4438
+ $ thinkwork thread list --limit 100 --json | jq '.[] | .title'
4439
+
4440
+ # Archived threads only
4441
+ $ thinkwork thread list --archived
4029
4442
  `
4030
4443
  ).action(() => notYetImplemented("thread list", 1));
4031
4444
  thread.command("get <idOrNumber>").description("Fetch one thread by ID or by its tenant-scoped issue number.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
@@ -4037,38 +4450,29 @@ Examples:
4037
4450
  $ thinkwork thread get 42 --json | jq .assignee
4038
4451
  `
4039
4452
  ).action(() => notYetImplemented("thread get", 1));
4040
- thread.command("create [title]").description("Create a new thread. Prompts for missing fields when running in a TTY.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--type <type>", "TASK | CHAT | BUG | QUESTION", "TASK").option("--priority <p>", "LOW | MEDIUM | HIGH | URGENT", "MEDIUM").option("--assignee <id>", "Assign on create (user or agent ID)").option("--body <text>", "Description body (markdown)").option("--due <iso>", "Due date as ISO-8601").option("--label <name...>", "Attach label(s) by name (repeatable)").addHelpText(
4453
+ thread.command("create [title]").description("Create a new thread. Prompts for missing fields when running in a TTY.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--assignee <id>", "Assign on create (user or agent ID)").option("--due <iso>", "Due date as ISO-8601").option("--label <name...>", "Attach label(s) by name (repeatable)").addHelpText(
4041
4454
  "after",
4042
4455
  `
4043
4456
  Examples:
4044
- # Fully interactive \u2014 walkthrough prompts for title, type, priority, assignee.
4457
+ # Fully interactive \u2014 walkthrough prompts for title and assignee.
4045
4458
  $ thinkwork thread create
4046
4459
 
4047
4460
  # Scripted
4048
4461
  $ thinkwork thread create "Investigate latency spike" \\
4049
- --priority HIGH --assignee agt-obs-1 --label ops --label oncall
4462
+ --assignee agt-obs-1 --label ops --label oncall
4050
4463
 
4051
4464
  # Mix: pass the title, prompt for the rest.
4052
4465
  $ thinkwork thread create "Investigate latency spike"
4053
4466
  `
4054
4467
  ).action(() => notYetImplemented("thread create", 1));
4055
- thread.command("update <id>").description("Update a thread's title, status, priority, assignee, labels, or due date.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--title <t>", "Rename").option("--status <s>", "Move to a new status").option("--priority <p>", "LOW | MEDIUM | HIGH | URGENT").option("--assignee <id>", "Reassign (user or agent ID)").option("--due <iso>", "Due date").option("--body <text>", "Replace description body").addHelpText(
4468
+ thread.command("update <id>").description("Update a thread's title, assignee, labels, or due date.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--title <t>", "Rename").option("--assignee <id>", "Reassign (user or agent ID)").option("--due <iso>", "Due date").addHelpText(
4056
4469
  "after",
4057
4470
  `
4058
4471
  Examples:
4059
- $ thinkwork thread update thr-abc --status IN_REVIEW
4060
- $ thinkwork thread update thr-abc --assignee agt-ops --priority URGENT
4472
+ $ thinkwork thread update thr-abc --title "New title"
4473
+ $ thinkwork thread update thr-abc --assignee agt-ops
4061
4474
  `
4062
4475
  ).action(() => notYetImplemented("thread update", 1));
4063
- thread.command("close <id>").description("Mark a thread DONE. Shortcut for `thread update <id> --status DONE`.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--comment <text>", "Add a closing comment").addHelpText(
4064
- "after",
4065
- `
4066
- Examples:
4067
- $ thinkwork thread close thr-abc
4068
- $ thinkwork thread close thr-abc --comment "fixed in #124"
4069
- `
4070
- ).action(() => notYetImplemented("thread close", 1));
4071
- thread.command("reopen <id>").description("Move a thread from DONE/CANCELLED back to TODO.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("thread reopen", 1));
4072
4476
  thread.command("checkout <id>").description("Claim a thread so an agent can work it (locks other agents out).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--agent <id>", "Agent to check it out to (defaults to the caller)").addHelpText(
4073
4477
  "after",
4074
4478
  `
@@ -4076,7 +4480,7 @@ Examples:
4076
4480
  $ thinkwork thread checkout thr-abc --agent agt-fixer
4077
4481
  `
4078
4482
  ).action(() => notYetImplemented("thread checkout", 1));
4079
- thread.command("release <id>").description("Release a checked-out thread, optionally moving it to a new status.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--status <s>", "Status to release into").action(() => notYetImplemented("thread release", 1));
4483
+ thread.command("release <id>").description("Release a checked-out thread (unlocks it so another agent can claim it).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("thread release", 1));
4080
4484
  thread.command("comment <id> [content]").description("Add a comment to a thread. Prompts for content if omitted and TTY.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--file <path>", "Read comment content from a file (markdown)").addHelpText(
4081
4485
  "after",
4082
4486
  `
@@ -4262,21 +4666,231 @@ Examples:
4262
4666
  version.command("rollback <agentId> <versionId>").description("Restore an agent to a prior version. Creates a new version pointing at the old config.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("agent version rollback", 2));
4263
4667
  }
4264
4668
 
4265
- // src/commands/template.ts
4266
- function registerTemplateCommand(program2) {
4267
- const tpl = program2.command("template").alias("templates").description("Manage agent templates \u2014 reusable configs you spawn agents from.");
4268
- tpl.command("list").alias("ls").description("List templates in the tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("template list", 2));
4269
- tpl.command("get <id>").description("Fetch one template with its linked agents.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("template get", 2));
4270
- tpl.command("create [name]").description("Create a new template from a set of config defaults.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--from-agent <id>", "Clone config from an existing agent").option("--system-prompt-file <path>", "Prompt markdown path").option("--model <id>", "Default model").option("--description <text>", "What this template is for").addHelpText(
4271
- "after",
4272
- `
4273
- Examples:
4274
- # Capture an existing agent's config as a template
4275
- $ thinkwork template create "Ops Analyst" --from-agent agt-ops-1
4276
-
4277
- # Fresh template
4278
- $ thinkwork template create --system-prompt-file prompts/ops.md --model claude-sonnet-4-6
4279
- `
4669
+ // src/commands/computer.ts
4670
+ import chalk14 from "chalk";
4671
+ async function resolveComputerContext(opts) {
4672
+ const stage = await resolveStage({ flag: opts.stage });
4673
+ const api = resolveApiConfig(stage);
4674
+ if (!api) process.exit(1);
4675
+ return { stage, api };
4676
+ }
4677
+ function resolveTenantId(opts) {
4678
+ const tenantId = opts.tenant ?? opts.tenantId;
4679
+ if (!tenantId) {
4680
+ printError(
4681
+ "Tenant ID is required. Pass --tenant <uuid> or --tenant-id <uuid>."
4682
+ );
4683
+ process.exit(1);
4684
+ }
4685
+ return tenantId;
4686
+ }
4687
+ function resolveComputerId(opts) {
4688
+ const computerId = opts.computer ?? opts.computerId;
4689
+ if (!computerId) {
4690
+ printError(
4691
+ "Computer ID is required. Pass --computer <uuid> or --computer-id <uuid>."
4692
+ );
4693
+ process.exit(1);
4694
+ }
4695
+ return computerId;
4696
+ }
4697
+ function resolveTaskType(opts) {
4698
+ if (!opts.type?.trim()) {
4699
+ printError("Task type is required. Pass --type <task-type>.");
4700
+ process.exit(1);
4701
+ }
4702
+ return opts.type.trim().toLowerCase();
4703
+ }
4704
+ function printMigrationReport(response) {
4705
+ printJson(response);
4706
+ if (isJsonMode()) return;
4707
+ if (!response.report) return;
4708
+ const summary = response.report.summary ?? {};
4709
+ console.log("");
4710
+ console.log(chalk14.bold(" Summary"));
4711
+ for (const [status, count] of Object.entries(summary)) {
4712
+ if (!count) continue;
4713
+ console.log(` ${status.padEnd(28)} ${count}`);
4714
+ }
4715
+ const rows = (response.report.groups ?? []).map((group) => ({
4716
+ owner: group.owner?.name ?? group.owner?.email ?? group.ownerUserId ?? "unpaired",
4717
+ source: group.primaryAgent?.name ?? group.primaryAgentId ?? "\u2014",
4718
+ template: group.primaryAgent?.templateName ?? "\u2014",
4719
+ status: group.status,
4720
+ action: group.recommendedAction ?? "\u2014",
4721
+ reason: group.reasons?.[0] ?? "\u2014"
4722
+ }));
4723
+ console.log("");
4724
+ printTable(rows, [
4725
+ { key: "owner", header: "Owner" },
4726
+ { key: "source", header: "Source Agent" },
4727
+ { key: "template", header: "Template" },
4728
+ { key: "status", header: "Status" },
4729
+ { key: "action", header: "Action" },
4730
+ { key: "reason", header: "Reason" }
4731
+ ]);
4732
+ }
4733
+ function registerComputerCommand(program2) {
4734
+ const computer = program2.command("computer").alias("computers").description("Manage ThinkWork Computers and migration operations");
4735
+ const migration = computer.command("migration").description("Dry-run or apply Agent-to-Computer migration");
4736
+ migration.command("dry-run").description("Inspect Agent-to-Computer migration candidates").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <uuid>", "Tenant ID").option("--tenant-id <uuid>", "Tenant ID").action(
4737
+ async (opts) => {
4738
+ const { stage, api } = await resolveComputerContext(opts);
4739
+ const tenantId = resolveTenantId(opts);
4740
+ if (!isJsonMode()) printHeader("computer migration dry-run", stage);
4741
+ const response = await apiFetchRaw(
4742
+ api.apiUrl,
4743
+ api.authSecret,
4744
+ "/api/migrations/agents-to-computers",
4745
+ {
4746
+ method: "POST",
4747
+ body: JSON.stringify({ tenantId, mode: "dry-run" })
4748
+ }
4749
+ );
4750
+ if (!response.ok) {
4751
+ printJson(response.body);
4752
+ printError(response.body.error ?? `HTTP ${response.status}`);
4753
+ process.exit(1);
4754
+ }
4755
+ printMigrationReport(response.body);
4756
+ if (!isJsonMode()) printSuccess("Computer migration dry-run complete");
4757
+ }
4758
+ );
4759
+ migration.command("apply").description(
4760
+ "Apply Agent-to-Computer migration after reviewing dry-run output"
4761
+ ).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <uuid>", "Tenant ID").option("--tenant-id <uuid>", "Tenant ID").option("--confirm", "Confirm the migration apply operation").option("--idempotency-key <key>", "Operator-supplied migration run key").action(
4762
+ async (opts) => {
4763
+ const { stage, api } = await resolveComputerContext(opts);
4764
+ const tenantId = resolveTenantId(opts);
4765
+ if (!opts.confirm) {
4766
+ printWarning(
4767
+ "Apply is intentionally gated. Re-run with --confirm after reviewing dry-run output."
4768
+ );
4769
+ process.exit(1);
4770
+ }
4771
+ if (!isJsonMode()) printHeader("computer migration apply", stage);
4772
+ const response = await apiFetchRaw(
4773
+ api.apiUrl,
4774
+ api.authSecret,
4775
+ "/api/migrations/agents-to-computers",
4776
+ {
4777
+ method: "POST",
4778
+ body: JSON.stringify({
4779
+ tenantId,
4780
+ mode: "apply",
4781
+ idempotencyKey: opts.idempotencyKey
4782
+ })
4783
+ }
4784
+ );
4785
+ if (!response.ok) {
4786
+ printJson(response.body);
4787
+ printError(response.body.error ?? `HTTP ${response.status}`);
4788
+ if (response.status === 409 && response.body.blockers) {
4789
+ if (!isJsonMode()) {
4790
+ console.log("");
4791
+ console.log(chalk14.bold(" Blockers"));
4792
+ console.log(JSON.stringify(response.body.blockers, null, 2));
4793
+ }
4794
+ }
4795
+ process.exit(1);
4796
+ }
4797
+ printMigrationReport(response.body);
4798
+ if (!isJsonMode()) {
4799
+ printSuccess(
4800
+ `Computer migration applied: ${response.body.created?.length ?? 0} created, ${response.body.skipped?.length ?? 0} skipped`
4801
+ );
4802
+ }
4803
+ }
4804
+ );
4805
+ const runtime = computer.command("runtime").description("Provision and control ECS-backed Computer runtimes");
4806
+ for (const action of [
4807
+ "provision",
4808
+ "start",
4809
+ "stop",
4810
+ "restart",
4811
+ "status"
4812
+ ]) {
4813
+ runtime.command(action).description(`${action} a Computer runtime`).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <uuid>", "Tenant ID").option("--tenant-id <uuid>", "Tenant ID").option("-c, --computer <uuid>", "Computer ID").option("--computer-id <uuid>", "Computer ID").action(
4814
+ async (opts) => {
4815
+ const { stage, api } = await resolveComputerContext(opts);
4816
+ const tenantId = resolveTenantId(opts);
4817
+ const computerId = resolveComputerId(opts);
4818
+ if (!isJsonMode()) {
4819
+ printHeader(`computer runtime ${action}`, stage);
4820
+ }
4821
+ const response = await apiFetchRaw(
4822
+ api.apiUrl,
4823
+ api.authSecret,
4824
+ "/api/computers/manager",
4825
+ {
4826
+ method: "POST",
4827
+ body: JSON.stringify({ action, tenantId, computerId })
4828
+ }
4829
+ );
4830
+ printJson(response.body);
4831
+ if (!response.ok) {
4832
+ printError(response.body.error ?? `HTTP ${response.status}`);
4833
+ process.exit(1);
4834
+ }
4835
+ if (!isJsonMode()) {
4836
+ printSuccess(`Computer runtime ${action} complete`);
4837
+ }
4838
+ }
4839
+ );
4840
+ }
4841
+ const task = computer.command("task").description("Enqueue work for a ThinkWork Computer runtime");
4842
+ task.command("enqueue").description("Enqueue a Computer runtime task").option("--type <type>", "Task type").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <uuid>", "Tenant ID").option("--tenant-id <uuid>", "Tenant ID").option("-c, --computer <uuid>", "Computer ID").option("--computer-id <uuid>", "Computer ID").option("--path <path>", "Workspace-relative path for file-write tasks").option("--content <content>", "UTF-8 content for file-write tasks").option("--idempotency-key <key>", "Operator-supplied idempotency key").action(
4843
+ async (opts) => {
4844
+ const { stage, api } = await resolveComputerContext(opts);
4845
+ const tenantId = resolveTenantId(opts);
4846
+ const computerId = resolveComputerId(opts);
4847
+ const taskType = resolveTaskType(opts);
4848
+ const input4 = taskType === "workspace_file_write" ? { path: opts.path, content: opts.content } : void 0;
4849
+ if (!isJsonMode()) printHeader("computer task enqueue", stage);
4850
+ const response = await apiFetchRaw(
4851
+ api.apiUrl,
4852
+ api.authSecret,
4853
+ "/api/computers/runtime/tasks",
4854
+ {
4855
+ method: "POST",
4856
+ body: JSON.stringify({
4857
+ tenantId,
4858
+ computerId,
4859
+ taskType,
4860
+ input: input4,
4861
+ idempotencyKey: opts.idempotencyKey
4862
+ })
4863
+ }
4864
+ );
4865
+ printJson(response.body);
4866
+ if (!response.ok) {
4867
+ printError(response.body.error ?? `HTTP ${response.status}`);
4868
+ process.exit(1);
4869
+ }
4870
+ if (!isJsonMode()) {
4871
+ printSuccess(
4872
+ `Queued Computer task ${response.body.task?.id ?? taskType}`
4873
+ );
4874
+ }
4875
+ }
4876
+ );
4877
+ }
4878
+
4879
+ // src/commands/template.ts
4880
+ function registerTemplateCommand(program2) {
4881
+ const tpl = program2.command("template").alias("templates").description("Manage agent templates \u2014 reusable configs you spawn agents from.");
4882
+ tpl.command("list").alias("ls").description("List templates in the tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("template list", 2));
4883
+ tpl.command("get <id>").description("Fetch one template with its linked agents.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("template get", 2));
4884
+ tpl.command("create [name]").description("Create a new template from a set of config defaults.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--from-agent <id>", "Clone config from an existing agent").option("--system-prompt-file <path>", "Prompt markdown path").option("--model <id>", "Default model").option("--description <text>", "What this template is for").addHelpText(
4885
+ "after",
4886
+ `
4887
+ Examples:
4888
+ # Capture an existing agent's config as a template
4889
+ $ thinkwork template create "Ops Analyst" --from-agent agt-ops-1
4890
+
4891
+ # Fresh template
4892
+ $ thinkwork template create --system-prompt-file prompts/ops.md --model claude-sonnet-4-6
4893
+ `
4280
4894
  ).action(() => notYetImplemented("template create", 2));
4281
4895
  tpl.command("update <id>").description("Update a template. Linked agents are NOT auto-synced \u2014 use `template sync-*`.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--name <n>").option("--system-prompt-file <path>").option("--model <id>").option("--description <text>").action(() => notYetImplemented("template update", 2));
4282
4896
  tpl.command("delete <id>").description("Delete a template. Linked agents are unaffected; they just stop being in sync.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("template delete", 2));
@@ -4301,8 +4915,67 @@ Examples:
4301
4915
  // src/commands/tenant.ts
4302
4916
  function registerTenantCommand(program2) {
4303
4917
  const tenant = program2.command("tenant").alias("tenants").description("Manage tenants (workspaces) \u2014 create, rename, and configure plans / defaults.");
4304
- tenant.command("list").alias("ls").description("List tenants the caller can see.").option("-s, --stage <name>", "Deployment stage").action(() => notYetImplemented("tenant list", 2));
4305
- tenant.command("get <idOrSlug>").description("Fetch one tenant by ID or slug.").option("-s, --stage <name>", "Deployment stage").action(() => notYetImplemented("tenant get", 2));
4918
+ tenant.command("list").alias("ls").description("List tenants the caller can see.").option("-s, --stage <name>", "Deployment stage").addHelpText(
4919
+ "after",
4920
+ `
4921
+ Examples:
4922
+ $ thinkwork tenant list
4923
+ $ thinkwork tenant list -s dev
4924
+ $ thinkwork tenant list --json | jq '.[].slug'
4925
+ `
4926
+ ).action(async (opts) => {
4927
+ try {
4928
+ const stage = await resolveStage({ flag: opts.stage });
4929
+ const api = resolveApiConfig(stage);
4930
+ if (!api) process.exit(1);
4931
+ const client = createClient({ apiUrl: api.apiUrl, authSecret: api.authSecret });
4932
+ const rows = await tenants_exports.listTenants(client);
4933
+ printJson(rows);
4934
+ printTable(rows, [
4935
+ { key: "slug", header: "SLUG" },
4936
+ { key: "name", header: "NAME" },
4937
+ { key: "plan", header: "PLAN" },
4938
+ { key: "id", header: "ID" }
4939
+ ]);
4940
+ } catch (err) {
4941
+ printError(err instanceof Error ? err.message : String(err));
4942
+ process.exit(1);
4943
+ }
4944
+ });
4945
+ tenant.command("get <idOrSlug>").description("Fetch one tenant by ID or slug.").option("-s, --stage <name>", "Deployment stage").addHelpText(
4946
+ "after",
4947
+ `
4948
+ Examples:
4949
+ $ thinkwork tenant get acme
4950
+ $ thinkwork tenant get 0a2b... --json
4951
+ `
4952
+ ).action(async (idOrSlug, opts) => {
4953
+ try {
4954
+ const stage = await resolveStage({ flag: opts.stage });
4955
+ const api = resolveApiConfig(stage);
4956
+ if (!api) process.exit(1);
4957
+ const client = createClient({ apiUrl: api.apiUrl, authSecret: api.authSecret });
4958
+ const isUuid2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
4959
+ idOrSlug
4960
+ );
4961
+ const tenant2 = isUuid2 ? await tenants_exports.getTenant(client, idOrSlug) : await tenants_exports.getTenantBySlug(client, idOrSlug);
4962
+ printJson(tenant2);
4963
+ printTable([tenant2], [
4964
+ { key: "slug", header: "SLUG" },
4965
+ { key: "name", header: "NAME" },
4966
+ { key: "plan", header: "PLAN" },
4967
+ { key: "issue_prefix", header: "PREFIX" },
4968
+ { key: "id", header: "ID" }
4969
+ ]);
4970
+ } catch (err) {
4971
+ if (err instanceof AdminOpsError && err.status === 404) {
4972
+ printError(`Tenant "${idOrSlug}" not found`);
4973
+ process.exit(2);
4974
+ }
4975
+ printError(err instanceof Error ? err.message : String(err));
4976
+ process.exit(1);
4977
+ }
4978
+ });
4306
4979
  tenant.command("create [name]").description("Create a new tenant. The caller becomes its first owner.").option("-s, --stage <name>", "Deployment stage").option("--slug <slug>", "URL-safe slug (lowercase, hyphens). Generated from name if omitted.").option("--plan <plan>", "Plan tier (free, team, enterprise, \u2026)", "team").option("--issue-prefix <prefix>", "Issue-number prefix for thread numbers (e.g. ACME)").addHelpText(
4307
4980
  "after",
4308
4981
  `
@@ -4492,29 +5165,218 @@ Examples:
4492
5165
  wh.command("deliveries <id>").description("Show recent delivery attempts (success/failure, response status).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--limit <n>", "Max rows", "25").action(() => notYetImplemented("webhook deliveries", 3));
4493
5166
  }
4494
5167
 
4495
- // src/commands/connector.ts
4496
- function registerConnectorCommand(program2) {
4497
- const conn = program2.command("connector").alias("connectors").description("Manage third-party integrations (Slack, GitHub, Linear, \u2026).");
4498
- conn.command("list").alias("ls").description("List available connectors and which are enabled for this tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--enabled-only", "Only show tenant-enabled connectors").action(() => notYetImplemented("connector list", 3));
4499
- conn.command("get <slug>").description("Fetch one connector with its config schema + current status.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("connector get", 3));
4500
- conn.command("enable <slug>").description("Enable a connector. Opens the OAuth flow in your browser when required.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--config <json>", "Inline config JSON (for API-key style connectors)").option("--config-file <path>", "Load config from a file").addHelpText(
4501
- "after",
4502
- `
4503
- Examples:
4504
- # OAuth connector (Slack)
4505
- $ thinkwork connector enable slack
5168
+ // src/lib/plugin-zip.ts
5169
+ import { createReadStream, promises as fsp, statSync } from "fs";
5170
+ import { basename, join as join6, relative, resolve as resolve3, sep } from "path";
5171
+ import JSZip from "jszip";
5172
+ var PluginZipError = class extends Error {
5173
+ constructor(message, kind) {
5174
+ super(message);
5175
+ this.kind = kind;
5176
+ this.name = "PluginZipError";
5177
+ }
5178
+ kind;
5179
+ };
5180
+ async function buildPluginZip(pluginDir) {
5181
+ const root = resolve3(pluginDir);
5182
+ let stat;
5183
+ try {
5184
+ stat = await fsp.stat(root);
5185
+ } catch {
5186
+ throw new PluginZipError(
5187
+ `Plugin directory not found: ${pluginDir}`,
5188
+ "missing-directory"
5189
+ );
5190
+ }
5191
+ if (!stat.isDirectory()) {
5192
+ throw new PluginZipError(
5193
+ `Plugin path must be a directory: ${pluginDir}`,
5194
+ "missing-directory"
5195
+ );
5196
+ }
5197
+ const manifestPath = join6(root, "plugin.json");
5198
+ let manifestRaw;
5199
+ try {
5200
+ manifestRaw = await fsp.readFile(manifestPath, "utf8");
5201
+ } catch {
5202
+ throw new PluginZipError(
5203
+ `plugin.json is required at the root of the plugin folder (expected ${manifestPath}).`,
5204
+ "missing-plugin-json"
5205
+ );
5206
+ }
5207
+ let parsed;
5208
+ try {
5209
+ parsed = JSON.parse(manifestRaw);
5210
+ } catch (err) {
5211
+ throw new PluginZipError(
5212
+ `plugin.json is not valid JSON: ${err.message}`,
5213
+ "invalid-plugin-json"
5214
+ );
5215
+ }
5216
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
5217
+ throw new PluginZipError(
5218
+ `plugin.json must be a JSON object with a "name" field.`,
5219
+ "invalid-plugin-json"
5220
+ );
5221
+ }
5222
+ const manifest = parsed;
5223
+ if (typeof manifest.name !== "string" || manifest.name.trim() === "") {
5224
+ throw new PluginZipError(
5225
+ `plugin.json must declare a non-empty "name" string.`,
5226
+ "invalid-plugin-json"
5227
+ );
5228
+ }
5229
+ const zip = new JSZip();
5230
+ const entries = await walkDir(root, root);
5231
+ for (const entry of entries) {
5232
+ if (entry.isSymbolicLink) {
5233
+ throw new PluginZipError(
5234
+ `Refusing to zip symlink: ${entry.relPath} (would be rejected server-side).`,
5235
+ "unsafe-entry"
5236
+ );
5237
+ }
5238
+ if (hasParentSegment(entry.relPath)) {
5239
+ throw new PluginZipError(
5240
+ `Refusing to zip path with traversal segment: ${entry.relPath}`,
5241
+ "unsafe-entry"
5242
+ );
5243
+ }
5244
+ const archivePath = entry.relPath.split(sep).join("/");
5245
+ zip.file(archivePath, createReadStream(entry.absPath));
5246
+ }
5247
+ let buffer;
5248
+ try {
5249
+ buffer = await zip.generateAsync({
5250
+ type: "nodebuffer",
5251
+ compression: "DEFLATE",
5252
+ compressionOptions: { level: 6 }
5253
+ });
5254
+ } catch (err) {
5255
+ throw new PluginZipError(
5256
+ `Failed to compress plugin: ${err.message}`,
5257
+ "io"
5258
+ );
5259
+ }
5260
+ const metadata = {
5261
+ name: manifest.name.trim(),
5262
+ version: typeof manifest.version === "string" && manifest.version.trim() !== "" ? manifest.version.trim() : void 0,
5263
+ description: typeof manifest.description === "string" ? manifest.description.trim() || void 0 : void 0
5264
+ };
5265
+ return {
5266
+ buffer,
5267
+ plugin: metadata,
5268
+ fileCount: entries.length,
5269
+ zipFileName: `${basename(root)}.zip`
5270
+ };
5271
+ }
5272
+ async function walkDir(rootDir, currentDir) {
5273
+ const out = [];
5274
+ const dirents = await fsp.readdir(currentDir, { withFileTypes: true });
5275
+ for (const ent of dirents) {
5276
+ const abs = join6(currentDir, ent.name);
5277
+ const rel = relative(rootDir, abs);
5278
+ if (ent.name === ".git" || ent.name === ".DS_Store" || ent.name === "node_modules") {
5279
+ continue;
5280
+ }
5281
+ if (ent.isSymbolicLink()) {
5282
+ out.push({ absPath: abs, relPath: rel, isSymbolicLink: true });
5283
+ continue;
5284
+ }
5285
+ if (ent.isDirectory()) {
5286
+ out.push(...await walkDir(rootDir, abs));
5287
+ continue;
5288
+ }
5289
+ if (ent.isFile()) {
5290
+ try {
5291
+ statSync(abs);
5292
+ } catch {
5293
+ continue;
5294
+ }
5295
+ out.push({ absPath: abs, relPath: rel, isSymbolicLink: false });
5296
+ }
5297
+ }
5298
+ return out.sort(
5299
+ (a, b) => a.relPath < b.relPath ? -1 : a.relPath > b.relPath ? 1 : 0
5300
+ );
5301
+ }
5302
+ function hasParentSegment(relPath) {
5303
+ const parts = relPath.split(sep);
5304
+ return parts.some((p) => p === "..");
5305
+ }
4506
5306
 
4507
- # API-key connector (inline)
4508
- $ thinkwork connector enable linear --config '{"apiKey":"lin_\u2026"}'
4509
- `
4510
- ).action(() => notYetImplemented("connector enable", 3));
4511
- conn.command("disable <slug>").description("Disable a connector. Stored credentials are cleared.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("connector disable", 3));
4512
- conn.command("test <slug>").description("Round-trip the connector's credentials against its API. Prints pass/fail + latency.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("connector test", 3));
5307
+ // src/lib/plugin-push.ts
5308
+ async function pushPluginZip(input4) {
5309
+ const base = input4.apiUrl.replace(/\/+$/, "");
5310
+ const presignRes = await fetch(`${base}/api/plugins/presign`, {
5311
+ method: "POST",
5312
+ headers: withJson(input4.headers),
5313
+ body: JSON.stringify({ fileName: input4.fileName })
5314
+ });
5315
+ if (!presignRes.ok) {
5316
+ throw new Error(`presign failed: ${await describeHttpError(presignRes)}`);
5317
+ }
5318
+ const presign = await presignRes.json();
5319
+ if (!presign.uploadUrl || !presign.s3Key) {
5320
+ throw new Error(
5321
+ `presign returned invalid response: ${JSON.stringify(presign)}`
5322
+ );
5323
+ }
5324
+ const putRes = await fetch(presign.uploadUrl, {
5325
+ method: "PUT",
5326
+ headers: { "Content-Type": "application/zip" },
5327
+ body: new Uint8Array(input4.zipBuffer)
5328
+ });
5329
+ if (!putRes.ok) {
5330
+ throw new Error(`S3 PUT failed: HTTP ${putRes.status}`);
5331
+ }
5332
+ const installRes = await fetch(`${base}/api/plugins/upload`, {
5333
+ method: "POST",
5334
+ headers: withJson(input4.headers),
5335
+ body: JSON.stringify({ s3Key: presign.s3Key })
5336
+ });
5337
+ const installBody = await installRes.json().catch(() => ({}));
5338
+ if (installRes.status === 400 && installBody && installBody.valid === false) {
5339
+ return {
5340
+ status: "validation-failed",
5341
+ errors: installBody.errors ?? [],
5342
+ warnings: installBody.warnings ?? []
5343
+ };
5344
+ }
5345
+ if (!installRes.ok) {
5346
+ const uploadId = typeof installBody.uploadId === "string" ? installBody.uploadId : "";
5347
+ return {
5348
+ status: "failed",
5349
+ uploadId,
5350
+ phase: typeof installBody.phase === "string" ? installBody.phase : void 0,
5351
+ errorMessage: typeof installBody.errorMessage === "string" && installBody.errorMessage || typeof installBody.error === "string" && installBody.error || `HTTP ${installRes.status}`
5352
+ };
5353
+ }
5354
+ const plugin = installBody.plugin;
5355
+ if (!plugin || typeof installBody.uploadId !== "string") {
5356
+ throw new Error(
5357
+ `install response missing uploadId/plugin: ${JSON.stringify(installBody)}`
5358
+ );
5359
+ }
5360
+ return {
5361
+ status: "installed",
5362
+ uploadId: installBody.uploadId,
5363
+ plugin,
5364
+ warnings: Array.isArray(installBody.warnings) ? installBody.warnings : []
5365
+ };
5366
+ }
5367
+ function withJson(headers) {
5368
+ return { "Content-Type": "application/json", ...headers };
5369
+ }
5370
+ async function describeHttpError(res) {
5371
+ const text = await res.text().catch(() => "");
5372
+ return `HTTP ${res.status} ${text.slice(0, 200)}`;
4513
5373
  }
4514
5374
 
4515
5375
  // src/commands/skill.ts
4516
5376
  function registerSkillCommand(program2) {
4517
- const skill = program2.command("skill").alias("skills").description("Browse the catalog, install, upgrade, or publish custom skills.");
5377
+ const skill = program2.command("skill").alias("skills").description(
5378
+ "Browse the catalog, install, upgrade, or publish custom skills."
5379
+ );
4518
5380
  skill.command("catalog").description("Browse the public skill catalog (not tenant-scoped).").option("-s, --stage <name>", "Deployment stage").option("--search <q>", "Filter by keyword").option("--tag <t>", "Filter by tag").action(() => notYetImplemented("skill catalog", 3));
4519
5381
  skill.command("list").alias("ls").description("List skills installed / published in the current tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--custom-only", "Only show tenant-owned custom skills").action(() => notYetImplemented("skill list", 3));
4520
5382
  skill.command("install <slug>").description("Install a public skill into the tenant. Idempotent.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--version <v>", "Pin to a specific version (default: latest)").addHelpText(
@@ -4526,7 +5388,9 @@ Examples:
4526
5388
  `
4527
5389
  ).action(() => notYetImplemented("skill install", 3));
4528
5390
  skill.command("upgrade <slug>").description("Upgrade an installed skill to the latest catalog version.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("skill upgrade", 3));
4529
- skill.command("create [slug]").description("Publish a custom tenant-scoped skill (walkthrough for missing fields in TTY).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--name <n>").option("--description <text>").option("--manifest-file <path>", "Path to the MCP server manifest JSON").option("--endpoint <url>", "MCP server HTTP/SSE endpoint").addHelpText(
5391
+ skill.command("create [slug]").description(
5392
+ "Publish a custom tenant-scoped skill (walkthrough for missing fields in TTY)."
5393
+ ).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--name <n>").option("--description <text>").option("--manifest-file <path>", "Path to the MCP server manifest JSON").option("--endpoint <url>", "MCP server HTTP/SSE endpoint").addHelpText(
4530
5394
  "after",
4531
5395
  `
4532
5396
  Examples:
@@ -4535,7 +5399,103 @@ Examples:
4535
5399
  `
4536
5400
  ).action(() => notYetImplemented("skill create", 3));
4537
5401
  skill.command("update <slug>").description("Update a custom skill's manifest, endpoint, or description.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--name <n>").option("--description <text>").option("--manifest-file <path>").option("--endpoint <url>").action(() => notYetImplemented("skill update", 3));
4538
- skill.command("delete <slug>").description("Delete a custom skill. Public catalog skills are uninstalled via this too.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("skill delete", 3));
5402
+ skill.command("delete <slug>").description(
5403
+ "Delete a custom skill. Public catalog skills are uninstalled via this too."
5404
+ ).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("skill delete", 3));
5405
+ skill.command("push <folder>").description(
5406
+ "Zip a local plugin folder and upload it to the tenant as a pending plugin."
5407
+ ).option("-s, --stage <name>", "Deployment stage").option("--region <name>", "AWS region", "us-east-1").addHelpText(
5408
+ "after",
5409
+ `
5410
+ Examples:
5411
+ $ thinkwork skill push ./my-plugin
5412
+ $ thinkwork skill push ./my-plugin --stage dev
5413
+
5414
+ The folder must contain a plugin.json manifest. MCP servers shipped
5415
+ inside the plugin will land as 'pending' and need admin approval
5416
+ under Capabilities \u2192 MCP Servers before agents can invoke them.
5417
+ `
5418
+ ).action(
5419
+ async (folder, opts) => {
5420
+ await runPushCommand(folder, opts);
5421
+ }
5422
+ );
5423
+ }
5424
+ async function runPushCommand(folder, opts) {
5425
+ const region = opts.region ?? "us-east-1";
5426
+ const stage = await resolveStage({ flag: opts.stage, region });
5427
+ let zipped;
5428
+ try {
5429
+ zipped = await buildPluginZip(folder);
5430
+ } catch (err) {
5431
+ if (err instanceof PluginZipError) {
5432
+ printError(err.message);
5433
+ process.exit(1);
5434
+ }
5435
+ throw err;
5436
+ }
5437
+ const auth = await resolveAuth({ stage, region, requireCognito: true });
5438
+ if (auth.mode !== "cognito") {
5439
+ printError(
5440
+ `skill push requires a Cognito session. Run \`thinkwork login --stage ${stage}\`.`
5441
+ );
5442
+ process.exit(1);
5443
+ }
5444
+ const apiUrl = getApiEndpoint(stage, region);
5445
+ if (!apiUrl) {
5446
+ printError(
5447
+ `Could not discover API endpoint for stage "${stage}" in ${region}. Is the stack deployed?`
5448
+ );
5449
+ process.exit(1);
5450
+ }
5451
+ printSuccess(
5452
+ `Prepared plugin "${zipped.plugin.name}" \u2014 ${zipped.fileCount} file(s), ${formatBytes(zipped.buffer.length)}`
5453
+ );
5454
+ let result;
5455
+ try {
5456
+ result = await pushPluginZip({
5457
+ apiUrl,
5458
+ headers: auth.headers,
5459
+ zipBuffer: zipped.buffer,
5460
+ fileName: zipped.zipFileName
5461
+ });
5462
+ } catch (err) {
5463
+ printError(`Upload failed: ${err.message}`);
5464
+ process.exit(1);
5465
+ }
5466
+ if (result.status === "validation-failed") {
5467
+ printError("Plugin validation failed");
5468
+ for (const e of result.errors) console.log(` - ${e}`);
5469
+ for (const w of result.warnings) printWarning(w);
5470
+ process.exit(1);
5471
+ }
5472
+ if (result.status === "failed") {
5473
+ printError(
5474
+ `Install failed${result.phase ? ` at phase ${result.phase}` : ""}: ${result.errorMessage}`
5475
+ );
5476
+ if (result.uploadId) {
5477
+ console.log(` upload id: ${result.uploadId}`);
5478
+ }
5479
+ process.exit(1);
5480
+ }
5481
+ const skillCount = result.plugin.skills.length;
5482
+ const mcpCount = result.plugin.mcpServers.length;
5483
+ printSuccess(
5484
+ `Installed "${result.plugin.name}" \u2014 ${skillCount} skill(s)` + (mcpCount > 0 ? `, ${mcpCount} MCP server(s) pending admin approval` : "")
5485
+ );
5486
+ console.log(` upload id: ${result.uploadId}`);
5487
+ if (mcpCount > 0) {
5488
+ console.log(
5489
+ ` approve at: admin SPA \u2192 Capabilities \u2192 MCP Servers (filter: status=pending)`
5490
+ );
5491
+ }
5492
+ for (const w of result.warnings) printWarning(w);
5493
+ }
5494
+ function formatBytes(bytes) {
5495
+ if (bytes < 1024) return `${bytes} B`;
5496
+ const kb = bytes / 1024;
5497
+ if (kb < 1024) return `${kb.toFixed(1)} KB`;
5498
+ return `${(kb / 1024).toFixed(2)} MB`;
4539
5499
  }
4540
5500
 
4541
5501
  // src/commands/memory.ts
@@ -4669,7 +5629,7 @@ Examples:
4669
5629
  }
4670
5630
 
4671
5631
  // src/commands/eval/run.ts
4672
- import { select as select8, checkbox, confirm as confirm2, input as input3 } from "@inquirer/prompts";
5632
+ import { checkbox, confirm as confirm2 } from "@inquirer/prompts";
4673
5633
  import ora2 from "ora";
4674
5634
 
4675
5635
  // src/gql/graphql.ts
@@ -4678,6 +5638,7 @@ var CliEvalRunDocument = { "kind": "Document", "definitions": [{ "kind": "Operat
4678
5638
  var CliEvalRunResultsDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "CliEvalRunResults" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "runId" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "ID" } } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "evalRunResults" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "runId" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "runId" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "testCaseId" } }, { "kind": "Field", "name": { "kind": "Name", "value": "testCaseName" } }, { "kind": "Field", "name": { "kind": "Name", "value": "category" } }, { "kind": "Field", "name": { "kind": "Name", "value": "status" } }, { "kind": "Field", "name": { "kind": "Name", "value": "score" } }, { "kind": "Field", "name": { "kind": "Name", "value": "durationMs" } }, { "kind": "Field", "name": { "kind": "Name", "value": "agentSessionId" } }, { "kind": "Field", "name": { "kind": "Name", "value": "input" } }, { "kind": "Field", "name": { "kind": "Name", "value": "expected" } }, { "kind": "Field", "name": { "kind": "Name", "value": "actualOutput" } }, { "kind": "Field", "name": { "kind": "Name", "value": "evaluatorResults" } }, { "kind": "Field", "name": { "kind": "Name", "value": "assertions" } }, { "kind": "Field", "name": { "kind": "Name", "value": "errorMessage" } }, { "kind": "Field", "name": { "kind": "Name", "value": "createdAt" } }] } }] } }] };
4679
5639
  var CliEvalTestCasesDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "CliEvalTestCases" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "tenantId" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "ID" } } } }, { "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "category" } }, "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "String" } } }, { "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "search" } }, "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "String" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "evalTestCases" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "tenantId" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "tenantId" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "category" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "category" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "search" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "search" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "name" } }, { "kind": "Field", "name": { "kind": "Name", "value": "category" } }, { "kind": "Field", "name": { "kind": "Name", "value": "query" } }, { "kind": "Field", "name": { "kind": "Name", "value": "systemPrompt" } }, { "kind": "Field", "name": { "kind": "Name", "value": "agentTemplateId" } }, { "kind": "Field", "name": { "kind": "Name", "value": "agentTemplateName" } }, { "kind": "Field", "name": { "kind": "Name", "value": "agentcoreEvaluatorIds" } }, { "kind": "Field", "name": { "kind": "Name", "value": "tags" } }, { "kind": "Field", "name": { "kind": "Name", "value": "enabled" } }, { "kind": "Field", "name": { "kind": "Name", "value": "source" } }, { "kind": "Field", "name": { "kind": "Name", "value": "createdAt" } }, { "kind": "Field", "name": { "kind": "Name", "value": "updatedAt" } }] } }] } }] };
4680
5640
  var CliEvalTestCaseDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "CliEvalTestCase" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "id" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "ID" } } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "evalTestCase" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "id" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "id" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "tenantId" } }, { "kind": "Field", "name": { "kind": "Name", "value": "name" } }, { "kind": "Field", "name": { "kind": "Name", "value": "category" } }, { "kind": "Field", "name": { "kind": "Name", "value": "query" } }, { "kind": "Field", "name": { "kind": "Name", "value": "systemPrompt" } }, { "kind": "Field", "name": { "kind": "Name", "value": "agentTemplateId" } }, { "kind": "Field", "name": { "kind": "Name", "value": "agentTemplateName" } }, { "kind": "Field", "name": { "kind": "Name", "value": "assertions" } }, { "kind": "Field", "name": { "kind": "Name", "value": "agentcoreEvaluatorIds" } }, { "kind": "Field", "name": { "kind": "Name", "value": "tags" } }, { "kind": "Field", "name": { "kind": "Name", "value": "enabled" } }, { "kind": "Field", "name": { "kind": "Name", "value": "source" } }, { "kind": "Field", "name": { "kind": "Name", "value": "createdAt" } }, { "kind": "Field", "name": { "kind": "Name", "value": "updatedAt" } }] } }] } }] };
5641
+ var CliComputersForEvalDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "CliComputersForEval" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "tenantId" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "ID" } } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "computers" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "tenantId" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "tenantId" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "name" } }, { "kind": "Field", "name": { "kind": "Name", "value": "slug" } }, { "kind": "Field", "name": { "kind": "Name", "value": "runtimeStatus" } }] } }] } }] };
4681
5642
  var CliAgentTemplatesForEvalDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "CliAgentTemplatesForEval" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "tenantId" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "ID" } } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "agentTemplates" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "tenantId" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "tenantId" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "name" } }, { "kind": "Field", "name": { "kind": "Name", "value": "slug" } }, { "kind": "Field", "name": { "kind": "Name", "value": "model" } }, { "kind": "Field", "name": { "kind": "Name", "value": "isPublished" } }] } }] } }] };
4682
5643
  var CliTenantBySlugDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "CliTenantBySlug" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "slug" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "String" } } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "tenantBySlug" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "slug" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "slug" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "slug" } }, { "kind": "Field", "name": { "kind": "Name", "value": "name" } }] } }] } }] };
4683
5644
  var CliStartEvalRunDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "mutation", "name": { "kind": "Name", "value": "CliStartEvalRun" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "tenantId" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "ID" } } } }, { "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "input" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "StartEvalRunInput" } } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "startEvalRun" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "tenantId" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "tenantId" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "input" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "input" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "status" } }, { "kind": "Field", "name": { "kind": "Name", "value": "model" } }, { "kind": "Field", "name": { "kind": "Name", "value": "categories" } }, { "kind": "Field", "name": { "kind": "Name", "value": "agentTemplateId" } }, { "kind": "Field", "name": { "kind": "Name", "value": "agentTemplateName" } }, { "kind": "Field", "name": { "kind": "Name", "value": "totalTests" } }, { "kind": "Field", "name": { "kind": "Name", "value": "createdAt" } }] } }] } }] };
@@ -4696,17 +5657,18 @@ var CliWikiCompileJobsDocument = { "kind": "Document", "definitions": [{ "kind":
4696
5657
 
4697
5658
  // src/gql/gql.ts
4698
5659
  var documents = {
4699
- "\n query CliEvalRuns($tenantId: ID!, $agentId: ID, $limit: Int, $offset: Int) {\n evalRuns(tenantId: $tenantId, agentId: $agentId, limit: $limit, offset: $offset) {\n totalCount\n items {\n id\n status\n model\n categories\n agentId\n agentName\n agentTemplateId\n agentTemplateName\n totalTests\n passed\n failed\n passRate\n regression\n costUsd\n errorMessage\n startedAt\n completedAt\n createdAt\n }\n }\n }\n": CliEvalRunsDocument,
5660
+ "\n query CliEvalRuns($tenantId: ID!, $agentId: ID, $limit: Int, $offset: Int) {\n evalRuns(\n tenantId: $tenantId\n agentId: $agentId\n limit: $limit\n offset: $offset\n ) {\n totalCount\n items {\n id\n status\n model\n categories\n agentId\n agentName\n agentTemplateId\n agentTemplateName\n totalTests\n passed\n failed\n passRate\n regression\n costUsd\n errorMessage\n startedAt\n completedAt\n createdAt\n }\n }\n }\n": CliEvalRunsDocument,
4700
5661
  "\n query CliEvalRun($id: ID!) {\n evalRun(id: $id) {\n id\n status\n model\n categories\n agentId\n agentName\n agentTemplateId\n agentTemplateName\n totalTests\n passed\n failed\n passRate\n regression\n costUsd\n errorMessage\n startedAt\n completedAt\n createdAt\n }\n }\n": CliEvalRunDocument,
4701
5662
  "\n query CliEvalRunResults($runId: ID!) {\n evalRunResults(runId: $runId) {\n id\n testCaseId\n testCaseName\n category\n status\n score\n durationMs\n agentSessionId\n input\n expected\n actualOutput\n evaluatorResults\n assertions\n errorMessage\n createdAt\n }\n }\n": CliEvalRunResultsDocument,
4702
5663
  "\n query CliEvalTestCases($tenantId: ID!, $category: String, $search: String) {\n evalTestCases(tenantId: $tenantId, category: $category, search: $search) {\n id\n name\n category\n query\n systemPrompt\n agentTemplateId\n agentTemplateName\n agentcoreEvaluatorIds\n tags\n enabled\n source\n createdAt\n updatedAt\n }\n }\n": CliEvalTestCasesDocument,
4703
5664
  "\n query CliEvalTestCase($id: ID!) {\n evalTestCase(id: $id) {\n id\n tenantId\n name\n category\n query\n systemPrompt\n agentTemplateId\n agentTemplateName\n assertions\n agentcoreEvaluatorIds\n tags\n enabled\n source\n createdAt\n updatedAt\n }\n }\n": CliEvalTestCaseDocument,
5665
+ "\n query CliComputersForEval($tenantId: ID!) {\n computers(tenantId: $tenantId) {\n id\n name\n slug\n runtimeStatus\n }\n }\n": CliComputersForEvalDocument,
4704
5666
  "\n query CliAgentTemplatesForEval($tenantId: ID!) {\n agentTemplates(tenantId: $tenantId) {\n id\n name\n slug\n model\n isPublished\n }\n }\n": CliAgentTemplatesForEvalDocument,
4705
5667
  "\n query CliTenantBySlug($slug: String!) {\n tenantBySlug(slug: $slug) {\n id\n slug\n name\n }\n }\n": CliTenantBySlugDocument,
4706
5668
  "\n mutation CliStartEvalRun($tenantId: ID!, $input: StartEvalRunInput!) {\n startEvalRun(tenantId: $tenantId, input: $input) {\n id\n status\n model\n categories\n agentTemplateId\n agentTemplateName\n totalTests\n createdAt\n }\n }\n": CliStartEvalRunDocument,
4707
5669
  "\n mutation CliCancelEvalRun($id: ID!) {\n cancelEvalRun(id: $id) {\n id\n status\n completedAt\n }\n }\n": CliCancelEvalRunDocument,
4708
5670
  "\n mutation CliDeleteEvalRun($id: ID!) {\n deleteEvalRun(id: $id)\n }\n": CliDeleteEvalRunDocument,
4709
- "\n mutation CliCreateEvalTestCase($tenantId: ID!, $input: CreateEvalTestCaseInput!) {\n createEvalTestCase(tenantId: $tenantId, input: $input) {\n id\n name\n category\n }\n }\n": CliCreateEvalTestCaseDocument,
5671
+ "\n mutation CliCreateEvalTestCase(\n $tenantId: ID!\n $input: CreateEvalTestCaseInput!\n ) {\n createEvalTestCase(tenantId: $tenantId, input: $input) {\n id\n name\n category\n }\n }\n": CliCreateEvalTestCaseDocument,
4710
5672
  "\n mutation CliUpdateEvalTestCase($id: ID!, $input: UpdateEvalTestCaseInput!) {\n updateEvalTestCase(id: $id, input: $input) {\n id\n name\n category\n enabled\n }\n }\n": CliUpdateEvalTestCaseDocument,
4711
5673
  "\n mutation CliDeleteEvalTestCase($id: ID!) {\n deleteEvalTestCase(id: $id)\n }\n": CliDeleteEvalTestCaseDocument,
4712
5674
  "\n mutation CliSeedEvalTestCases($tenantId: ID!, $categories: [String!]) {\n seedEvalTestCases(tenantId: $tenantId, categories: $categories)\n }\n": CliSeedEvalTestCasesDocument,
@@ -4724,7 +5686,12 @@ function graphql(source) {
4724
5686
  // src/commands/eval/gql.ts
4725
5687
  var EvalRunsDoc = graphql(`
4726
5688
  query CliEvalRuns($tenantId: ID!, $agentId: ID, $limit: Int, $offset: Int) {
4727
- evalRuns(tenantId: $tenantId, agentId: $agentId, limit: $limit, offset: $offset) {
5689
+ evalRuns(
5690
+ tenantId: $tenantId
5691
+ agentId: $agentId
5692
+ limit: $limit
5693
+ offset: $offset
5694
+ ) {
4728
5695
  totalCount
4729
5696
  items {
4730
5697
  id
@@ -4834,6 +5801,16 @@ var EvalTestCaseDoc = graphql(`
4834
5801
  }
4835
5802
  }
4836
5803
  `);
5804
+ var ComputersForEvalDoc = graphql(`
5805
+ query CliComputersForEval($tenantId: ID!) {
5806
+ computers(tenantId: $tenantId) {
5807
+ id
5808
+ name
5809
+ slug
5810
+ runtimeStatus
5811
+ }
5812
+ }
5813
+ `);
4837
5814
  var AgentTemplatesForEvalDoc = graphql(`
4838
5815
  query CliAgentTemplatesForEval($tenantId: ID!) {
4839
5816
  agentTemplates(tenantId: $tenantId) {
@@ -4883,7 +5860,10 @@ var DeleteEvalRunDoc = graphql(`
4883
5860
  }
4884
5861
  `);
4885
5862
  var CreateEvalTestCaseDoc = graphql(`
4886
- mutation CliCreateEvalTestCase($tenantId: ID!, $input: CreateEvalTestCaseInput!) {
5863
+ mutation CliCreateEvalTestCase(
5864
+ $tenantId: ID!
5865
+ $input: CreateEvalTestCaseInput!
5866
+ ) {
4887
5867
  createEvalTestCase(tenantId: $tenantId, input: $input) {
4888
5868
  id
4889
5869
  name
@@ -4981,123 +5961,75 @@ function isTerminalStatus(status) {
4981
5961
  }
4982
5962
 
4983
5963
  // src/commands/eval/run.ts
5964
+ var DEFAULT_EVAL_MODEL_ID = "moonshotai.kimi-k2.5";
4984
5965
  async function runEvalRun(opts) {
4985
5966
  const ctx = await resolveEvalContext(opts);
4986
5967
  const interactive = isInteractive();
4987
- let agentTemplateId = opts.agentTemplate ?? null;
5968
+ const deprecatedComputerId = opts.computer ?? null;
4988
5969
  let categories = opts.category ?? null;
4989
5970
  let testCaseIds = opts.testCase ?? null;
5971
+ if (deprecatedComputerId) {
5972
+ printError(
5973
+ "--computer is no longer supported for eval runs. Evals run directly against the default Agent template."
5974
+ );
5975
+ process.exit(1);
5976
+ }
5977
+ if (opts.model && opts.model !== DEFAULT_EVAL_MODEL_ID) {
5978
+ printError(
5979
+ `--model is no longer configurable for eval runs. Evals use ${DEFAULT_EVAL_MODEL_ID}.`
5980
+ );
5981
+ process.exit(1);
5982
+ }
4990
5983
  const scopeSatisfied = testCaseIds && testCaseIds.length > 0 || categories && categories.length > 0 || opts.all === true;
4991
- if (!agentTemplateId || !scopeSatisfied) {
5984
+ if (!scopeSatisfied) {
4992
5985
  if (!interactive) {
4993
5986
  const missing = [];
4994
- if (!agentTemplateId) missing.push("--agent-template");
4995
- if (!scopeSatisfied) missing.push("one of --all | --category | --test-case");
5987
+ if (!scopeSatisfied)
5988
+ missing.push("one of --all | --category | --test-case");
4996
5989
  printError(
4997
5990
  `Missing required flag(s) in non-interactive session: ${missing.join(", ")}.`
4998
5991
  );
4999
5992
  process.exit(1);
5000
5993
  }
5001
5994
  }
5002
- if (!agentTemplateId) {
5003
- const data = await gqlQuery(ctx.client, AgentTemplatesForEvalDoc, {
5995
+ if (!scopeSatisfied) {
5996
+ const tcData = await gqlQuery(ctx.client, EvalTestCasesDoc, {
5004
5997
  tenantId: ctx.tenantId
5005
5998
  });
5006
- const templates = data.agentTemplates ?? [];
5007
- if (templates.length === 0) {
5008
- printError("No agent templates defined for this tenant. Create one first.");
5999
+ const distinctCategories = Array.from(
6000
+ new Set(
6001
+ (tcData.evalTestCases ?? []).filter((tc) => tc.enabled).map((tc) => tc.category)
6002
+ )
6003
+ ).sort();
6004
+ if (distinctCategories.length === 0) {
6005
+ printError(
6006
+ "No enabled test cases exist for this tenant yet. Run `thinkwork eval seed` to load the starter pack."
6007
+ );
5009
6008
  process.exit(1);
5010
6009
  }
5011
- requireTty("Agent template");
5012
- agentTemplateId = await promptOrExit(
5013
- () => select8({
5014
- message: "Agent template to run against?",
5015
- choices: templates.map((t) => ({
5016
- name: `${t.name}${t.model ? ` (${t.model})` : ""}${t.isPublished ? "" : " [draft]"}`,
5017
- value: t.id
5018
- })),
5019
- loop: false
5020
- })
5021
- );
5022
- }
5023
- if (!scopeSatisfied) {
5024
- const scope = await promptOrExit(
5025
- () => select8({
5026
- message: "How should we pick test cases?",
5027
- choices: [
5028
- { name: "All enabled test cases", value: "all" },
5029
- { name: "Filter by category", value: "category" },
5030
- { name: "Pick specific test cases", value: "specific" }
5031
- ],
6010
+ const picked = await promptOrExit(
6011
+ () => checkbox({
6012
+ message: "Which categories? (space to toggle, enter to confirm)",
6013
+ choices: distinctCategories.map((c) => ({ name: c, value: c })),
6014
+ required: true,
5032
6015
  loop: false
5033
6016
  })
5034
6017
  );
5035
- if (scope === "all") {
5036
- categories = null;
5037
- testCaseIds = null;
5038
- opts.all = true;
5039
- } else if (scope === "category") {
5040
- const tcData = await gqlQuery(ctx.client, EvalTestCasesDoc, {
5041
- tenantId: ctx.tenantId
5042
- });
5043
- const distinctCategories = Array.from(
5044
- new Set((tcData.evalTestCases ?? []).map((tc) => tc.category))
5045
- ).sort();
5046
- if (distinctCategories.length === 0) {
5047
- printError(
5048
- "No test cases exist for this tenant yet. Run `thinkwork eval seed` to load the starter pack."
5049
- );
5050
- process.exit(1);
5051
- }
5052
- const picked = await promptOrExit(
5053
- () => checkbox({
5054
- message: "Which categories? (space to toggle, enter to confirm)",
5055
- choices: distinctCategories.map((c) => ({ name: c, value: c })),
5056
- required: true,
5057
- loop: false
5058
- })
5059
- );
5060
- categories = picked;
5061
- } else {
5062
- const tcData = await gqlQuery(ctx.client, EvalTestCasesDoc, {
5063
- tenantId: ctx.tenantId
5064
- });
5065
- const options = (tcData.evalTestCases ?? []).filter((tc) => tc.enabled);
5066
- if (options.length === 0) {
5067
- printError("No enabled test cases to pick from.");
5068
- process.exit(1);
5069
- }
5070
- const picked = await promptOrExit(
5071
- () => checkbox({
5072
- message: "Which test cases?",
5073
- choices: options.map((tc) => ({
5074
- name: `${tc.name} (${tc.category})`,
5075
- value: tc.id
5076
- })),
5077
- required: true,
5078
- loop: false
5079
- })
5080
- );
5081
- testCaseIds = picked;
5082
- }
5083
- }
5084
- if (!opts.model && interactive) {
5085
- const entered = await promptOrExit(
5086
- () => input3({
5087
- message: "Model override? (blank for template default)",
5088
- default: ""
5089
- })
5090
- );
5091
- if (entered.trim()) opts.model = entered.trim();
6018
+ categories = picked;
5092
6019
  }
6020
+ const requestedModel = opts.model ?? null;
6021
+ opts.model = DEFAULT_EVAL_MODEL_ID;
5093
6022
  if (interactive && !isJsonMode()) {
5094
6023
  const summaryLines = [
5095
6024
  ["Stage", ctx.stage],
5096
6025
  ["Tenant", ctx.tenantSlug],
5097
- ["Agent template", agentTemplateId]
6026
+ ["Target", "Default Agent template"]
5098
6027
  ];
6028
+ if (requestedModel && requestedModel !== DEFAULT_EVAL_MODEL_ID)
6029
+ summaryLines.push(["Ignored Model", requestedModel]);
5099
6030
  if (opts.model) summaryLines.push(["Model", opts.model]);
5100
- if (categories && categories.length) summaryLines.push(["Categories", categories.join(", ")]);
6031
+ if (categories && categories.length)
6032
+ summaryLines.push(["Categories", categories.join(", ")]);
5101
6033
  if (testCaseIds && testCaseIds.length)
5102
6034
  summaryLines.push(["Test cases", `${testCaseIds.length} picked`]);
5103
6035
  if (opts.all && !categories?.length && !testCaseIds?.length)
@@ -5114,8 +6046,6 @@ async function runEvalRun(opts) {
5114
6046
  const mutRes = await gqlMutate(ctx.client, StartEvalRunDoc, {
5115
6047
  tenantId: ctx.tenantId,
5116
6048
  input: {
5117
- agentTemplateId,
5118
- agentId: opts.agent ?? null,
5119
6049
  model: opts.model ?? null,
5120
6050
  categories: categories ?? null,
5121
6051
  testCaseIds: testCaseIds ?? null
@@ -5123,7 +6053,12 @@ async function runEvalRun(opts) {
5123
6053
  });
5124
6054
  const run2 = mutRes.startEvalRun;
5125
6055
  if (isJsonMode()) {
5126
- printJson({ runId: run2.id, status: run2.status, model: run2.model, categories: run2.categories });
6056
+ printJson({
6057
+ runId: run2.id,
6058
+ status: run2.status,
6059
+ model: run2.model,
6060
+ categories: run2.categories
6061
+ });
5127
6062
  } else {
5128
6063
  printSuccess(`Started eval run ${run2.id} (status: ${run2.status}).`);
5129
6064
  }
@@ -5147,8 +6082,12 @@ async function pollUntilTerminal(client, runId, intervalSec, timeoutSec) {
5147
6082
  }
5148
6083
  if (isTerminalStatus(run2.status)) {
5149
6084
  if (spinner) {
5150
- if (run2.status === "completed") spinner.succeed(`completed \u2014 ${run2.passed}/${run2.totalTests} (${fmtPercent(run2.passRate)})`);
5151
- else if (run2.status === "failed") spinner.fail(`failed \u2014 ${run2.errorMessage ?? "unknown error"}`);
6085
+ if (run2.status === "completed")
6086
+ spinner.succeed(
6087
+ `completed \u2014 ${run2.passed}/${run2.totalTests} (${fmtPercent(run2.passRate)})`
6088
+ );
6089
+ else if (run2.status === "failed")
6090
+ spinner.fail(`failed \u2014 ${run2.errorMessage ?? "unknown error"}`);
5152
6091
  else spinner.warn("cancelled");
5153
6092
  }
5154
6093
  if (isJsonMode()) {
@@ -5466,7 +6405,7 @@ async function runEvalTestCaseGet(id, opts) {
5466
6405
 
5467
6406
  // src/commands/eval/test-case/create.ts
5468
6407
  import { readFileSync as readFileSync6 } from "fs";
5469
- import { input as input4, select as select9, checkbox as checkbox2 } from "@inquirer/prompts";
6408
+ import { input as input3, select as select8, checkbox as checkbox2 } from "@inquirer/prompts";
5470
6409
  var DEFAULT_EVALUATORS = [
5471
6410
  "Builtin.Helpfulness",
5472
6411
  "Builtin.Correctness",
@@ -5496,17 +6435,17 @@ async function runEvalTestCaseCreate(opts) {
5496
6435
  if (!name) {
5497
6436
  requireTty("Name");
5498
6437
  name = await promptOrExit(
5499
- () => input4({ message: "Test case name?", validate: (v) => v.trim().length > 0 || "Required" })
6438
+ () => input3({ message: "Test case name?", validate: (v) => v.trim().length > 0 || "Required" })
5500
6439
  );
5501
6440
  }
5502
6441
  if (!category) {
5503
6442
  category = await promptOrExit(
5504
- () => input4({ message: "Category (free-form label)?", validate: (v) => v.trim().length > 0 || "Required" })
6443
+ () => input3({ message: "Category (free-form label)?", validate: (v) => v.trim().length > 0 || "Required" })
5505
6444
  );
5506
6445
  }
5507
6446
  if (!query) {
5508
6447
  query = await promptOrExit(
5509
- () => input4({ message: "Query the agent under test will receive?", validate: (v) => v.trim().length > 0 || "Required" })
6448
+ () => input3({ message: "Query the agent under test will receive?", validate: (v) => v.trim().length > 0 || "Required" })
5510
6449
  );
5511
6450
  }
5512
6451
  if (interactive && agentTemplateId === null) {
@@ -5514,7 +6453,7 @@ async function runEvalTestCaseCreate(opts) {
5514
6453
  const templates = tpls.agentTemplates ?? [];
5515
6454
  if (templates.length > 0) {
5516
6455
  const choice = await promptOrExit(
5517
- () => select9({
6456
+ () => select8({
5518
6457
  message: "Pin to an agent template? (Enter for none)",
5519
6458
  choices: [
5520
6459
  { name: "\u2014 none \u2014 (runner picks)", value: "" },
@@ -5572,28 +6511,28 @@ async function runEvalTestCaseCreate(opts) {
5572
6511
  import { readFileSync as readFileSync7 } from "fs";
5573
6512
  async function runEvalTestCaseUpdate(id, opts) {
5574
6513
  const ctx = await resolveEvalContext(opts);
5575
- const input5 = {};
5576
- if (opts.name !== void 0) input5.name = opts.name;
5577
- if (opts.category !== void 0) input5.category = opts.category;
5578
- if (opts.query !== void 0) input5.query = opts.query;
5579
- if (opts.systemPrompt !== void 0) input5.systemPrompt = opts.systemPrompt;
5580
- if (opts.agentTemplate !== void 0) input5.agentTemplateId = opts.agentTemplate;
5581
- if (opts.evaluator !== void 0) input5.agentcoreEvaluatorIds = opts.evaluator;
5582
- if (opts.tag !== void 0) input5.tags = opts.tag;
5583
- if (opts.enabled !== void 0) input5.enabled = opts.enabled;
6514
+ const input4 = {};
6515
+ if (opts.name !== void 0) input4.name = opts.name;
6516
+ if (opts.category !== void 0) input4.category = opts.category;
6517
+ if (opts.query !== void 0) input4.query = opts.query;
6518
+ if (opts.systemPrompt !== void 0) input4.systemPrompt = opts.systemPrompt;
6519
+ if (opts.agentTemplate !== void 0) input4.agentTemplateId = opts.agentTemplate;
6520
+ if (opts.evaluator !== void 0) input4.agentcoreEvaluatorIds = opts.evaluator;
6521
+ if (opts.tag !== void 0) input4.tags = opts.tag;
6522
+ if (opts.enabled !== void 0) input4.enabled = opts.enabled;
5584
6523
  if (opts.assertionsFile) {
5585
6524
  const parsed = JSON.parse(readFileSync7(opts.assertionsFile, "utf8"));
5586
6525
  if (!Array.isArray(parsed)) {
5587
6526
  printError(`--assertions-file must contain a JSON array.`);
5588
6527
  process.exit(1);
5589
6528
  }
5590
- input5.assertions = parsed;
6529
+ input4.assertions = parsed;
5591
6530
  }
5592
- if (Object.keys(input5).length === 0) {
6531
+ if (Object.keys(input4).length === 0) {
5593
6532
  printError("No fields to update. Pass at least one --<field>.");
5594
6533
  process.exit(1);
5595
6534
  }
5596
- const res = await gqlMutate(ctx.client, UpdateEvalTestCaseDoc, { id, input: input5 });
6535
+ const res = await gqlMutate(ctx.client, UpdateEvalTestCaseDoc, { id, input: input4 });
5597
6536
  if (isJsonMode()) {
5598
6537
  printJson(res.updateEvalTestCase);
5599
6538
  return;
@@ -5631,38 +6570,84 @@ async function runEvalTestCaseDelete(id, opts) {
5631
6570
  // src/commands/eval.ts
5632
6571
  function registerEvalCommand(program2) {
5633
6572
  const evals = program2.command("eval").alias("evals").description(
5634
- "Run evaluations against your agents and manage eval test cases. Integrates with the Evaluations Studio in the admin UI."
5635
- );
6573
+ "Run evaluations against the default AgentCore agent template and manage eval test cases. Integrates with the Evaluations Studio in the admin UI."
6574
+ ).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option(
6575
+ "--computer <id>",
6576
+ "Deprecated; evals now run directly against AgentCore"
6577
+ ).option("--model <id>", "Deprecated; evals always use Kimi K2.5").option("--category <name...>", "Only run these categories (repeatable)").option(
6578
+ "--test-case <id...>",
6579
+ "Only run these specific test case IDs (repeatable)"
6580
+ ).option("--all", "Run all enabled test cases for the tenant").option("--watch", "Block and poll until the run reaches a terminal status").option(
6581
+ "--timeout <seconds>",
6582
+ "Max wait seconds for --watch (default 900)",
6583
+ "900"
6584
+ ).addHelpText(
6585
+ "after",
6586
+ `
6587
+ Default action:
6588
+ $ thinkwork evals
6589
+
6590
+ Equivalent explicit action:
6591
+ $ thinkwork eval run
6592
+ `
6593
+ ).action(runEvalRun);
5636
6594
  evals.command("run").description(
5637
6595
  "Start an evaluation run. Prompts for missing values in a TTY; fails fast in non-interactive sessions."
5638
- ).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option("--agent-template <id>", "Run-level agent template ID").option("--agent <id>", "Optional agent under test").option("--model <id>", "Optional model override").option("--category <name...>", "Only run these categories (repeatable)").option("--test-case <id...>", "Only run these specific test case IDs (repeatable)").option("--all", "Run all enabled test cases for the tenant").option("--watch", "Block and poll until the run reaches a terminal status").option("--timeout <seconds>", "Max wait seconds for --watch (default 900)", "900").addHelpText(
6596
+ ).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option(
6597
+ "--computer <id>",
6598
+ "Deprecated; evals now run directly against AgentCore"
6599
+ ).option("--model <id>", "Deprecated; evals always use Kimi K2.5").option("--category <name...>", "Only run these categories (repeatable)").option(
6600
+ "--test-case <id...>",
6601
+ "Only run these specific test case IDs (repeatable)"
6602
+ ).option("--all", "Run all enabled test cases for the tenant").option("--watch", "Block and poll until the run reaches a terminal status").option(
6603
+ "--timeout <seconds>",
6604
+ "Max wait seconds for --watch (default 900)",
6605
+ "900"
6606
+ ).addHelpText(
5639
6607
  "after",
5640
6608
  `
5641
6609
  Examples:
5642
6610
  # Fire and return \u2014 prints the runId; view results in the admin UI
5643
- $ thinkwork eval run --agent-template tpl-abc --category tool-safety
6611
+ $ thinkwork eval run --category red-team-safety-scope
5644
6612
 
5645
- # Pick categories + test cases interactively
6613
+ # Pick categories interactively
5646
6614
  $ thinkwork eval run
5647
6615
 
5648
6616
  # Block until done
5649
- $ thinkwork eval run --agent-template tpl-abc --all --watch --timeout 1800
6617
+ $ thinkwork eval run --all --watch --timeout 1800
5650
6618
  `
5651
6619
  ).action(runEvalRun);
5652
6620
  evals.command("list").alias("ls").description("List recent eval runs for the tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option("--agent <id>", "Filter by agent under test").option("--limit <n>", "Max rows (default 25)", "25").option("--offset <n>", "Skip N rows", "0").action(runEvalList);
5653
- evals.command("get <runId>").description("Show one eval run with its per-test-case results.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option("--results", "Also fetch per-test-case results (default: true)", true).option("--no-results", "Skip fetching per-test-case results").action(runEvalGet);
6621
+ evals.command("get <runId>").description("Show one eval run with its per-test-case results.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option(
6622
+ "--results",
6623
+ "Also fetch per-test-case results (default: true)",
6624
+ true
6625
+ ).option("--no-results", "Skip fetching per-test-case results").action(runEvalGet);
5654
6626
  evals.command("watch <runId>").description("Poll an eval run until it reaches a terminal status.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option("--interval <seconds>", "Poll interval (default 3)", "3").option("--timeout <seconds>", "Max wait seconds (default 900)", "900").action(runEvalWatch);
5655
6627
  evals.command("cancel <runId>").description("Cancel a running or pending eval run.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").action(runEvalCancel);
5656
- evals.command("delete <runId>").description("Delete an eval run and its results. Requires confirmation unless --yes.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option("-y, --yes", "Skip the confirmation prompt").action(runEvalDelete);
5657
- evals.command("categories").description("List distinct categories present across the tenant's test cases.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").action(runEvalCategories);
6628
+ evals.command("delete <runId>").description(
6629
+ "Delete an eval run and its results. Requires confirmation unless --yes."
6630
+ ).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option("-y, --yes", "Skip the confirmation prompt").action(runEvalDelete);
6631
+ evals.command("categories").description(
6632
+ "List distinct categories present across the tenant's test cases."
6633
+ ).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").action(runEvalCategories);
5658
6634
  evals.command("seed").description(
5659
- "Idempotently seed the maniflow starter pack (96 test cases across 9 categories). Safe to re-run."
6635
+ "Idempotently seed the ThinkWork RedTeam starter pack (189 test cases across 4 categories). Safe to re-run."
5660
6636
  ).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option("--category <name...>", "Only seed these categories (repeatable)").action(runEvalSeed);
5661
6637
  const tc = evals.command("test-case").alias("test-cases").description("Manage individual eval test cases (CRUD).");
5662
6638
  tc.command("list").alias("ls").description("List test cases, optionally filtered by category or search.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option("--category <name>", "Filter by a single category").option("--search <q>", "Substring match on test case name").action(runEvalTestCaseList);
5663
6639
  tc.command("get <id>").description("Show a single test case.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").action(runEvalTestCaseGet);
5664
- tc.command("create").description("Create a new test case. Prompts for missing values in a TTY.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option("--name <text>", "Human-readable name").option("--category <name>", "Category label (e.g. tool-safety, red-team)").option("--query <text>", "The user-facing query this agent will receive").option("--system-prompt <text>", "Optional system-prompt override").option("--agent-template <id>", "Pin to a specific agent template").option("--evaluator <id...>", "AgentCore evaluator IDs (repeatable)").option("--tag <name...>", "Tags (repeatable)").option("--enabled", "Mark enabled (default)", true).option("--no-enabled", "Mark disabled").option("--assertions-file <path>", "JSON file containing an array of assertions").action(runEvalTestCaseCreate);
5665
- tc.command("update <id>").description("Update a test case. Only supplied fields are changed.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option("--name <text>").option("--category <name>").option("--query <text>").option("--system-prompt <text>").option("--agent-template <id>").option("--evaluator <id...>", "Replace AgentCore evaluator IDs (repeatable)").option("--tag <name...>", "Replace tags (repeatable)").option("--enabled", "Mark enabled").option("--no-enabled", "Mark disabled").option("--assertions-file <path>", "JSON file containing an array of assertions (replaces all)").action(runEvalTestCaseUpdate);
6640
+ tc.command("create").description("Create a new test case. Prompts for missing values in a TTY.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option("--name <text>", "Human-readable name").option("--category <name>", "Category label (e.g. tool-safety, red-team)").option("--query <text>", "The user-facing query this agent will receive").option("--system-prompt <text>", "Optional system-prompt override").option("--agent-template <id>", "Pin to a specific agent template").option("--evaluator <id...>", "AgentCore evaluator IDs (repeatable)").option("--tag <name...>", "Tags (repeatable)").option("--enabled", "Mark enabled (default)", true).option("--no-enabled", "Mark disabled").option(
6641
+ "--assertions-file <path>",
6642
+ "JSON file containing an array of assertions"
6643
+ ).action(runEvalTestCaseCreate);
6644
+ tc.command("update <id>").description("Update a test case. Only supplied fields are changed.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option("--name <text>").option("--category <name>").option("--query <text>").option("--system-prompt <text>").option("--agent-template <id>").option(
6645
+ "--evaluator <id...>",
6646
+ "Replace AgentCore evaluator IDs (repeatable)"
6647
+ ).option("--tag <name...>", "Replace tags (repeatable)").option("--enabled", "Mark enabled").option("--no-enabled", "Mark disabled").option(
6648
+ "--assertions-file <path>",
6649
+ "JSON file containing an array of assertions (replaces all)"
6650
+ ).action(runEvalTestCaseUpdate);
5666
6651
  tc.command("delete <id>").description("Delete a test case. Requires confirmation unless --yes.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option("-y, --yes", "Skip the confirmation prompt").action(runEvalTestCaseDelete);
5667
6652
  }
5668
6653
 
@@ -5735,7 +6720,7 @@ var WikiCompileJobsDoc = graphql(`
5735
6720
  `);
5736
6721
 
5737
6722
  // src/commands/wiki/helpers.ts
5738
- import { select as select10 } from "@inquirer/prompts";
6723
+ import { select as select9 } from "@inquirer/prompts";
5739
6724
  async function resolveWikiContext(opts) {
5740
6725
  const region = opts.region ?? "us-east-1";
5741
6726
  const stage = await resolveStage({ flag: opts.stage, region });
@@ -5875,7 +6860,7 @@ async function resolveAgentScope(ctx, opts, config = {}) {
5875
6860
  choices.push({ name: `${label}${slugPart} [${a.id}]`, value: a.id });
5876
6861
  }
5877
6862
  const pick = await promptOrExit(
5878
- () => select10({
6863
+ () => select9({
5879
6864
  message: "Which agent?",
5880
6865
  choices,
5881
6866
  loop: false
@@ -6571,6 +7556,7 @@ registerMessageCommand(program);
6571
7556
  registerLabelCommand(program);
6572
7557
  registerInboxCommand(program);
6573
7558
  registerAgentCommand(program);
7559
+ registerComputerCommand(program);
6574
7560
  registerTemplateCommand(program);
6575
7561
  registerTenantCommand(program);
6576
7562
  registerMemberCommand(program);
@@ -6581,7 +7567,6 @@ registerScheduledJobCommand(program);
6581
7567
  registerTurnCommand(program);
6582
7568
  registerWakeupCommand(program);
6583
7569
  registerWebhookCommand(program);
6584
- registerConnectorCommand(program);
6585
7570
  registerSkillCommand(program);
6586
7571
  registerMemoryCommand(program);
6587
7572
  registerRecipeCommand(program);