thinkwork-cli 0.9.0 → 0.9.1

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 +1187 -315
  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
@@ -196,7 +196,7 @@ async function ensureWorkspace(cwd, stage) {
196
196
  }
197
197
  }
198
198
  function runTerraformRaw(cwd, args) {
199
- return new Promise((resolve3, reject) => {
199
+ return new Promise((resolve4, reject) => {
200
200
  const proc = spawn("terraform", args, {
201
201
  cwd,
202
202
  stdio: ["pipe", "pipe", "pipe"]
@@ -206,13 +206,13 @@ function runTerraformRaw(cwd, args) {
206
206
  proc.stdout.on("data", (d) => stdout += d);
207
207
  proc.stderr.on("data", (d) => stderr += d);
208
208
  proc.on("close", (code) => {
209
- if (code === 0) resolve3(stdout);
209
+ if (code === 0) resolve4(stdout);
210
210
  else reject(new Error(`terraform ${args.join(" ")} failed (exit ${code}): ${stderr}`));
211
211
  });
212
212
  });
213
213
  }
214
214
  function runTerraform(cwd, args) {
215
- return new Promise((resolve3) => {
215
+ return new Promise((resolve4) => {
216
216
  console.log(`
217
217
  \u2192 terraform ${args.join(" ")}
218
218
  `);
@@ -220,7 +220,7 @@ function runTerraform(cwd, args) {
220
220
  cwd,
221
221
  stdio: "inherit"
222
222
  });
223
- proc.on("close", (code) => resolve3(code ?? 1));
223
+ proc.on("close", (code) => resolve4(code ?? 1));
224
224
  });
225
225
  }
226
226
  async function ensureInit(cwd) {
@@ -427,6 +427,12 @@ function registerPlanCommand(program2) {
427
427
  });
428
428
  }
429
429
 
430
+ // src/commands/deploy.ts
431
+ import { spawn as spawn2 } from "child_process";
432
+ import { existsSync as existsSync3 } from "fs";
433
+ import { dirname as dirname2, resolve as pathResolve } from "path";
434
+ import { fileURLToPath } from "url";
435
+
430
436
  // src/prompt.ts
431
437
  import { createInterface } from "readline";
432
438
  async function confirm(message) {
@@ -434,69 +440,125 @@ async function confirm(message) {
434
440
  input: process.stdin,
435
441
  output: process.stdout
436
442
  });
437
- return new Promise((resolve3) => {
443
+ return new Promise((resolve4) => {
438
444
  rl.question(`${message} [y/N] `, (answer) => {
439
445
  rl.close();
440
- resolve3(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
446
+ resolve4(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
441
447
  });
442
448
  });
443
449
  }
444
450
 
445
451
  // src/commands/deploy.ts
446
452
  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);
453
+ program2.command("deploy").description(
454
+ "Run terraform apply for a stage. Prompts for stage in a TTY when omitted."
455
+ ).option("-p, --profile <name>", "AWS profile").option("-s, --stage <name>", "Deployment stage").option(
456
+ "-c, --component <tier>",
457
+ "Component tier (foundation|data|app|all)",
458
+ "all"
459
+ ).option("-y, --yes", "Skip interactive confirmation (for CI)").action(
460
+ async (opts) => {
461
+ const startTime = Date.now();
462
+ try {
463
+ const stage = await resolveStage({ flag: opts.stage });
464
+ const compCheck = validateComponent(opts.component);
465
+ if (!compCheck.valid) {
466
+ printError(compCheck.error);
467
+ process.exit(1);
466
468
  }
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);
469
+ const identity = getAwsIdentity();
470
+ printHeader("deploy", stage, identity);
471
+ if (!identity) {
472
+ printWarning(
473
+ "Could not resolve AWS identity. Is the AWS CLI configured?"
474
+ );
472
475
  }
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);
476
+ if (isProdLike(stage) && !opts.yes) {
477
+ const ok = await confirm(
478
+ ` Stage "${stage}" is production-like. Deploy?`
479
+ );
480
+ if (!ok) {
481
+ console.log(" Aborted.");
482
+ process.exit(0);
483
+ }
484
+ } else if (!opts.yes) {
485
+ const ok = await confirm(` Deploy to stage "${stage}"?`);
486
+ if (!ok) {
487
+ console.log(" Aborted.");
488
+ process.exit(0);
489
+ }
490
+ }
491
+ const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
492
+ const tiers = expandComponent(opts.component);
493
+ for (let i = 0; i < tiers.length; i++) {
494
+ const tier = tiers[i];
495
+ printTierHeader(tier, i, tiers.length);
496
+ const cwd = resolveTierDir(terraformDir, stage, tier);
497
+ await ensureInit(cwd);
498
+ await ensureWorkspace(cwd, stage);
499
+ const code = await runTerraform(cwd, [
500
+ "apply",
501
+ "-auto-approve",
502
+ `-var=stage=${stage}`
503
+ ]);
504
+ if (code !== 0) {
505
+ printError(`Deploy failed for ${tier} (exit ${code})`);
506
+ process.exit(code);
507
+ }
490
508
  }
509
+ printSuccess("Deploy complete");
510
+ await runPostDeployProbe(stage);
511
+ printSummary("deploy", stage, tiers, startTime);
512
+ } catch (err) {
513
+ if (isCancellation(err)) return;
514
+ throw err;
491
515
  }
492
- printSuccess("Deploy complete");
493
- printSummary("deploy", stage, tiers, startTime);
494
- } catch (err) {
495
- if (isCancellation(err)) return;
496
- throw err;
497
516
  }
517
+ );
518
+ }
519
+ async function runPostDeployProbe(stage) {
520
+ const scriptPath = locatePostDeployScript();
521
+ if (!scriptPath) {
522
+ printWarning(
523
+ "post-deploy probe script not found \u2014 skipping AgentCore drift check"
524
+ );
525
+ return;
526
+ }
527
+ await new Promise((resolve4) => {
528
+ const proc = spawn2("bash", [scriptPath, "--stage", stage], {
529
+ stdio: "inherit",
530
+ env: process.env
531
+ });
532
+ proc.on("close", (code) => {
533
+ if (code !== 0) {
534
+ printWarning(
535
+ `post-deploy probe exited ${code} \u2014 deploy not rolled back`
536
+ );
537
+ }
538
+ resolve4();
539
+ });
540
+ proc.on("error", (err) => {
541
+ printWarning(`post-deploy probe spawn failed: ${err.message}`);
542
+ resolve4();
543
+ });
498
544
  });
499
545
  }
546
+ function locatePostDeployScript() {
547
+ const here = dirname2(fileURLToPath(import.meta.url));
548
+ const candidates = [
549
+ pathResolve(here, "..", "..", "..", "..", "scripts", "post-deploy.sh"),
550
+ pathResolve(process.cwd(), "scripts", "post-deploy.sh"),
551
+ pathResolve(
552
+ process.env.THINKWORK_TERRAFORM_DIR || ".",
553
+ "scripts",
554
+ "post-deploy.sh"
555
+ )
556
+ ];
557
+ for (const candidate of candidates) {
558
+ if (existsSync3(candidate)) return candidate;
559
+ }
560
+ return null;
561
+ }
500
562
 
501
563
  // src/commands/destroy.ts
502
564
  function registerDestroyCommand(program2) {
@@ -687,17 +749,17 @@ function registerOutputsCommand(program2) {
687
749
  }
688
750
 
689
751
  // src/commands/config.ts
690
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync4 } from "fs";
752
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync5 } from "fs";
691
753
  import chalk4 from "chalk";
692
754
 
693
755
  // src/environments.ts
694
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync2, readdirSync } from "fs";
756
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync2, readdirSync } from "fs";
695
757
  import { join as join2 } from "path";
696
758
  import { homedir as homedir2 } from "os";
697
759
  var THINKWORK_HOME = join2(homedir2(), ".thinkwork");
698
760
  var ENVIRONMENTS_DIR = join2(THINKWORK_HOME, "environments");
699
761
  function ensureDir(dir) {
700
- if (!existsSync3(dir)) mkdirSync2(dir, { recursive: true });
762
+ if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
701
763
  }
702
764
  function saveEnvironment(config) {
703
765
  ensureDir(ENVIRONMENTS_DIR);
@@ -710,13 +772,13 @@ function saveEnvironment(config) {
710
772
  }
711
773
  function loadEnvironment(stage) {
712
774
  const configPath = join2(ENVIRONMENTS_DIR, stage, "config.json");
713
- if (!existsSync3(configPath)) return null;
775
+ if (!existsSync4(configPath)) return null;
714
776
  return JSON.parse(readFileSync2(configPath, "utf-8"));
715
777
  }
716
778
  function listEnvironments() {
717
- if (!existsSync3(ENVIRONMENTS_DIR)) return [];
779
+ if (!existsSync4(ENVIRONMENTS_DIR)) return [];
718
780
  return readdirSync(ENVIRONMENTS_DIR).filter((name) => {
719
- return existsSync3(join2(ENVIRONMENTS_DIR, name, "config.json"));
781
+ return existsSync4(join2(ENVIRONMENTS_DIR, name, "config.json"));
720
782
  }).map((name) => {
721
783
  return JSON.parse(
722
784
  readFileSync2(join2(ENVIRONMENTS_DIR, name, "config.json"), "utf-8")
@@ -725,19 +787,19 @@ function listEnvironments() {
725
787
  }
726
788
  function resolveTerraformDir(stage) {
727
789
  const env = loadEnvironment(stage);
728
- if (env?.terraformDir && existsSync3(env.terraformDir)) {
790
+ if (env?.terraformDir && existsSync4(env.terraformDir)) {
729
791
  return env.terraformDir;
730
792
  }
731
793
  const envVar = process.env.THINKWORK_TERRAFORM_DIR;
732
- if (envVar && existsSync3(envVar)) return envVar;
794
+ if (envVar && existsSync4(envVar)) return envVar;
733
795
  const cwdTf = join2(process.cwd(), "terraform");
734
- if (existsSync3(join2(cwdTf, "main.tf"))) return cwdTf;
796
+ if (existsSync4(join2(cwdTf, "main.tf"))) return cwdTf;
735
797
  return null;
736
798
  }
737
799
 
738
800
  // src/commands/config.ts
739
801
  function readTfVar(tfvarsPath, key) {
740
- if (!existsSync4(tfvarsPath)) return null;
802
+ if (!existsSync5(tfvarsPath)) return null;
741
803
  const content = readFileSync3(tfvarsPath, "utf-8");
742
804
  const quoted = content.match(new RegExp(`^${key}\\s*=\\s*"([^"]*)"`, "m"));
743
805
  if (quoted) return quoted[1];
@@ -746,7 +808,7 @@ function readTfVar(tfvarsPath, key) {
746
808
  }
747
809
  var BARE_KEYS = /* @__PURE__ */ new Set(["enable_hindsight"]);
748
810
  function setTfVar(tfvarsPath, key, value) {
749
- if (!existsSync4(tfvarsPath)) {
811
+ if (!existsSync5(tfvarsPath)) {
750
812
  throw new Error(`terraform.tfvars not found at ${tfvarsPath}`);
751
813
  }
752
814
  let content = readFileSync3(tfvarsPath, "utf-8");
@@ -766,7 +828,7 @@ function resolveTfvarsPath(stage) {
766
828
  const tfDir = resolveTerraformDir(stage);
767
829
  if (tfDir) {
768
830
  const direct = `${tfDir}/terraform.tfvars`;
769
- if (existsSync4(direct)) return direct;
831
+ if (existsSync5(direct)) return direct;
770
832
  }
771
833
  const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
772
834
  const cwd = resolveTierDir(terraformDir, stage, "app");
@@ -793,7 +855,7 @@ function registerConfigCommand(program2) {
793
855
  console.log(` ${chalk4.bold("Updated:")} ${env.updatedAt}`);
794
856
  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
857
  const tfvarsPath = `${env.terraformDir}/terraform.tfvars`;
796
- if (existsSync4(tfvarsPath)) {
858
+ if (existsSync5(tfvarsPath)) {
797
859
  console.log("");
798
860
  console.log(chalk4.dim(" terraform.tfvars:"));
799
861
  const content = readFileSync3(tfvarsPath, "utf-8");
@@ -914,28 +976,28 @@ function registerConfigCommand(program2) {
914
976
  }
915
977
 
916
978
  // src/commands/bootstrap.ts
917
- import { spawn as spawn2 } from "child_process";
979
+ import { spawn as spawn3 } from "child_process";
918
980
  import { resolve } from "path";
919
981
  function getTerraformOutput(cwd, key) {
920
- return new Promise((resolve3, reject) => {
921
- const proc = spawn2("terraform", ["output", "-raw", key], {
982
+ return new Promise((resolve4, reject) => {
983
+ const proc = spawn3("terraform", ["output", "-raw", key], {
922
984
  cwd,
923
985
  stdio: ["pipe", "pipe", "pipe"]
924
986
  });
925
987
  let stdout = "";
926
988
  proc.stdout.on("data", (d) => stdout += d);
927
989
  proc.on("close", (code) => {
928
- if (code === 0) resolve3(stdout.trim());
990
+ if (code === 0) resolve4(stdout.trim());
929
991
  else reject(new Error(`terraform output ${key} failed (exit ${code})`));
930
992
  });
931
993
  });
932
994
  }
933
995
  function runScript(scriptPath, args) {
934
- return new Promise((resolve3) => {
935
- const proc = spawn2("bash", [scriptPath, ...args], {
996
+ return new Promise((resolve4) => {
997
+ const proc = spawn3("bash", [scriptPath, ...args], {
936
998
  stdio: "inherit"
937
999
  });
938
- proc.on("close", (code) => resolve3(code ?? 1));
1000
+ proc.on("close", (code) => resolve4(code ?? 1));
939
1001
  });
940
1002
  }
941
1003
  function registerBootstrapCommand(program2) {
@@ -990,7 +1052,7 @@ import { select as select2, Separator } from "@inquirer/prompts";
990
1052
  import chalk7 from "chalk";
991
1053
 
992
1054
  // src/aws-profiles.ts
993
- import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
1055
+ import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
994
1056
  import { homedir as homedir3 } from "os";
995
1057
  import { join as join3 } from "path";
996
1058
  var CREDENTIALS_PATH = join3(homedir3(), ".aws", "credentials");
@@ -1033,7 +1095,7 @@ function classify(fields) {
1033
1095
  }
1034
1096
  function listAwsProfiles() {
1035
1097
  const byName = /* @__PURE__ */ new Map();
1036
- if (existsSync5(CREDENTIALS_PATH)) {
1098
+ if (existsSync6(CREDENTIALS_PATH)) {
1037
1099
  const sections = parseIni(readFileSync4(CREDENTIALS_PATH, "utf-8"));
1038
1100
  for (const [section, fields] of Object.entries(sections)) {
1039
1101
  byName.set(section, {
@@ -1043,7 +1105,7 @@ function listAwsProfiles() {
1043
1105
  });
1044
1106
  }
1045
1107
  }
1046
- if (existsSync5(CONFIG_PATH)) {
1108
+ if (existsSync6(CONFIG_PATH)) {
1047
1109
  const sections = parseIni(readFileSync4(CONFIG_PATH, "utf-8"));
1048
1110
  for (const [section, fields] of Object.entries(sections)) {
1049
1111
  const name = normalizeConfigSection(section);
@@ -1271,7 +1333,7 @@ function discoverCognitoConfig(stage, region) {
1271
1333
  // src/cognito-oauth.ts
1272
1334
  import { createServer } from "http";
1273
1335
  import { randomBytes } from "crypto";
1274
- import { spawn as spawn3 } from "child_process";
1336
+ import { spawn as spawn4 } from "child_process";
1275
1337
  import chalk6 from "chalk";
1276
1338
  var CLI_LOOPBACK_PORT = 42010;
1277
1339
  var CALLBACK_PATH = "/callback";
@@ -1310,7 +1372,7 @@ function buildAuthorizeUrl(cognito, redirectUri, state) {
1310
1372
  return `${cognito.domainUrl}/oauth2/authorize?${params.toString()}`;
1311
1373
  }
1312
1374
  function waitForCallbackCode(opts) {
1313
- return new Promise((resolve3, reject) => {
1375
+ return new Promise((resolve4, reject) => {
1314
1376
  const server = createServer((req, res) => handleRequest(req, res));
1315
1377
  let finished = false;
1316
1378
  const finish = (err, code) => {
@@ -1321,7 +1383,7 @@ function waitForCallbackCode(opts) {
1321
1383
  closer.closeAllConnections?.();
1322
1384
  server.close(() => {
1323
1385
  if (err) reject(err);
1324
- else resolve3(code);
1386
+ else resolve4(code);
1325
1387
  });
1326
1388
  };
1327
1389
  const timer = setTimeout(() => {
@@ -1450,7 +1512,7 @@ function openInBrowser(url) {
1450
1512
  const cmd = platform2 === "darwin" ? "open" : platform2 === "win32" ? "cmd" : "xdg-open";
1451
1513
  const args = platform2 === "win32" ? ["/c", "start", "", url] : [url];
1452
1514
  try {
1453
- spawn3(cmd, args, { stdio: "ignore", detached: true }).unref();
1515
+ spawn4(cmd, args, { stdio: "ignore", detached: true }).unref();
1454
1516
  } catch {
1455
1517
  }
1456
1518
  }
@@ -1512,10 +1574,10 @@ function escapeHtml(s) {
1512
1574
  // src/commands/login.ts
1513
1575
  function ask(prompt) {
1514
1576
  const rl = createInterface2({ input: process.stdin, output: process.stdout });
1515
- return new Promise((resolve3) => {
1577
+ return new Promise((resolve4) => {
1516
1578
  rl.question(prompt, (answer) => {
1517
1579
  rl.close();
1518
- resolve3(answer.trim());
1580
+ resolve4(answer.trim());
1519
1581
  });
1520
1582
  });
1521
1583
  }
@@ -1994,20 +2056,20 @@ Examples:
1994
2056
  }
1995
2057
 
1996
2058
  // 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";
2059
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, cpSync } from "fs";
2060
+ import { resolve as resolve2, join as join5, dirname as dirname3 } from "path";
1999
2061
  import { execSync as execSync7 } from "child_process";
2000
- import { fileURLToPath } from "url";
2062
+ import { fileURLToPath as fileURLToPath2 } from "url";
2001
2063
  import { createInterface as createInterface3 } from "readline";
2002
2064
  import chalk8 from "chalk";
2003
- var __dirname = dirname2(fileURLToPath(import.meta.url));
2065
+ var __dirname = dirname3(fileURLToPath2(import.meta.url));
2004
2066
  function ask2(prompt, defaultVal = "") {
2005
2067
  const rl = createInterface3({ input: process.stdin, output: process.stdout });
2006
2068
  const suffix = defaultVal ? chalk8.dim(` [${defaultVal}]`) : "";
2007
- return new Promise((resolve3) => {
2069
+ return new Promise((resolve4) => {
2008
2070
  rl.question(` ${prompt}${suffix}: `, (answer) => {
2009
2071
  rl.close();
2010
- resolve3(answer.trim() || defaultVal);
2072
+ resolve4(answer.trim() || defaultVal);
2011
2073
  });
2012
2074
  });
2013
2075
  }
@@ -2025,9 +2087,9 @@ function generateSecret(length = 32) {
2025
2087
  }
2026
2088
  function findBundledTerraform() {
2027
2089
  const bundled = resolve2(__dirname, "terraform");
2028
- if (existsSync7(join5(bundled, "modules"))) return bundled;
2090
+ if (existsSync8(join5(bundled, "modules"))) return bundled;
2029
2091
  const repoTf = resolve2(__dirname, "..", "..", "..", "terraform");
2030
- if (existsSync7(join5(repoTf, "modules"))) return repoTf;
2092
+ if (existsSync8(join5(repoTf, "modules"))) return repoTf;
2031
2093
  throw new Error(
2032
2094
  "Terraform modules not found. The CLI package may be incomplete.\nTry reinstalling: npm install -g thinkwork-cli@latest"
2033
2095
  );
@@ -2087,9 +2149,9 @@ function registerInitCommand(program2) {
2087
2149
  printError("Stage name is required. Pass -s <name> or re-run in an interactive terminal.");
2088
2150
  process.exit(1);
2089
2151
  }
2090
- const { input: input5 } = await import("@inquirer/prompts");
2152
+ const { input: input4 } = await import("@inquirer/prompts");
2091
2153
  try {
2092
- stage = await input5({
2154
+ stage = await input4({
2093
2155
  message: "Stage name (e.g. dev, staging, prod):",
2094
2156
  validate: (v) => validateStage(v).error ?? true
2095
2157
  });
@@ -2119,7 +2181,7 @@ function registerInitCommand(program2) {
2119
2181
  const targetDir = resolve2(opts.dir);
2120
2182
  const tfDir = join5(targetDir, "terraform");
2121
2183
  const tfvarsPath = join5(tfDir, "terraform.tfvars");
2122
- if (existsSync7(tfvarsPath)) {
2184
+ if (existsSync8(tfvarsPath)) {
2123
2185
  printWarning(`terraform.tfvars already exists at ${tfvarsPath}`);
2124
2186
  const overwrite = await ask2("Overwrite?", "N");
2125
2187
  if (overwrite.toLowerCase() !== "y") {
@@ -2189,18 +2251,18 @@ function registerInitCommand(program2) {
2189
2251
  for (const dir of copyDirs) {
2190
2252
  const src = join5(bundledTf, dir);
2191
2253
  const dst = join5(tfDir, dir);
2192
- if (existsSync7(src) && !existsSync7(dst)) {
2254
+ if (existsSync8(src) && !existsSync8(dst)) {
2193
2255
  cpSync(src, dst, { recursive: true });
2194
2256
  }
2195
2257
  }
2196
2258
  const schemaPath = join5(bundledTf, "schema.graphql");
2197
- if (existsSync7(schemaPath) && !existsSync7(join5(tfDir, "schema.graphql"))) {
2259
+ if (existsSync8(schemaPath) && !existsSync8(join5(tfDir, "schema.graphql"))) {
2198
2260
  cpSync(schemaPath, join5(tfDir, "schema.graphql"));
2199
2261
  }
2200
2262
  const tfvars = buildTfvars(config);
2201
2263
  writeFileSync4(tfvarsPath, tfvars);
2202
2264
  const mainTfPath = join5(tfDir, "main.tf");
2203
- if (!existsSync7(mainTfPath)) {
2265
+ if (!existsSync8(mainTfPath)) {
2204
2266
  writeFileSync4(mainTfPath, `################################################################################
2205
2267
  # Thinkwork \u2014 ${config.stage}
2206
2268
  # Generated by: thinkwork init -s ${config.stage}
@@ -2628,12 +2690,13 @@ and AgentCore for per-stage detail.
2628
2690
 
2629
2691
  // src/commands/mcp.ts
2630
2692
  import chalk10 from "chalk";
2693
+ import { createClient, adminKeys, AdminOpsError } from "@thinkwork/admin-ops";
2631
2694
 
2632
2695
  // src/api-client.ts
2633
- import { readFileSync as readFileSync5, existsSync as existsSync8 } from "fs";
2696
+ import { readFileSync as readFileSync5, existsSync as existsSync9 } from "fs";
2634
2697
  import { execSync as execSync9 } from "child_process";
2635
2698
  function readTfVar2(tfvarsPath, key) {
2636
- if (!existsSync8(tfvarsPath)) return null;
2699
+ if (!existsSync9(tfvarsPath)) return null;
2637
2700
  const content = readFileSync5(tfvarsPath, "utf-8");
2638
2701
  const match = content.match(new RegExp(`^${key}\\s*=\\s*"([^"]*)"`, "m"));
2639
2702
  return match ? match[1] : null;
@@ -2642,7 +2705,7 @@ function resolveTfvarsPath2(stage) {
2642
2705
  const tfDir = resolveTerraformDir(stage);
2643
2706
  if (tfDir) {
2644
2707
  const direct = `${tfDir}/terraform.tfvars`;
2645
- if (existsSync8(direct)) return direct;
2708
+ if (existsSync9(direct)) return direct;
2646
2709
  }
2647
2710
  const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
2648
2711
  const cwd = resolveTierDir(terraformDir, stage, "app");
@@ -2949,14 +3012,14 @@ Examples:
2949
3012
  $ thinkwork mcp add my-tools --url https://mcp.example.com/crm \\
2950
3013
  --auth-type tenant_api_key --api-key sk-abc -s dev -t acme
2951
3014
 
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
3015
+ # OAuth-backed MCP integration (users connect from the mobile app)
3016
+ $ thinkwork mcp add crm-tools --url https://mcp.example.com/crm \\
3017
+ --auth-type per_user_oauth --oauth-provider crm_provider -s dev -t acme
2955
3018
  `
2956
3019
  ).action(
2957
3020
  async (nameArg, opts) => {
2958
3021
  try {
2959
- const { input: input5 } = await import("@inquirer/prompts");
3022
+ const { input: input4 } = await import("@inquirer/prompts");
2960
3023
  const { stage, api, tenant } = await resolveMcpContext(opts);
2961
3024
  let name = nameArg;
2962
3025
  if (!name) {
@@ -2964,7 +3027,7 @@ Examples:
2964
3027
  printError("Name is required. Pass it as a positional arg.");
2965
3028
  process.exit(1);
2966
3029
  }
2967
- name = await input5({ message: "Server name:" });
3030
+ name = await input4({ message: "Server name:" });
2968
3031
  }
2969
3032
  let url = opts.url;
2970
3033
  if (!url) {
@@ -2972,7 +3035,7 @@ Examples:
2972
3035
  printError("--url is required. Pass it as a flag.");
2973
3036
  process.exit(1);
2974
3037
  }
2975
- url = await input5({
3038
+ url = await input4({
2976
3039
  message: "MCP server URL:",
2977
3040
  validate: (v) => v.startsWith("http://") || v.startsWith("https://") ? true : "URL must start with http:// or https://"
2978
3041
  });
@@ -3006,13 +3069,13 @@ Examples:
3006
3069
  `
3007
3070
  Examples:
3008
3071
  # 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
3072
+ $ thinkwork mcp update routing-server --url https://mcp.example.com/routing
3010
3073
 
3011
3074
  # Disable without deleting
3012
- $ thinkwork mcp update lastmile-routing --disable
3075
+ $ thinkwork mcp update routing-server --disable
3013
3076
 
3014
3077
  # Rename + change transport
3015
- $ thinkwork mcp update 629dcee1-1e14-4b83-9907-cb529e6035f6 --name "LastMile Routing" --transport sse
3078
+ $ thinkwork mcp update 629dcee1-1e14-4b83-9907-cb529e6035f6 --name "Routing Server" --transport sse
3016
3079
 
3017
3080
  # Interactive \u2014 pick the server from a list
3018
3081
  $ thinkwork mcp update
@@ -3065,7 +3128,7 @@ Examples:
3065
3128
  $ thinkwork mcp remove
3066
3129
 
3067
3130
  # By slug (case-insensitive)
3068
- $ thinkwork mcp remove lastmile-routing
3131
+ $ thinkwork mcp remove routing-server
3069
3132
 
3070
3133
  # By UUID (from \`mcp list\` or --json)
3071
3134
  $ thinkwork mcp remove 629dcee1-1e14-4b83-9907-cb529e6035f6
@@ -3132,7 +3195,7 @@ Examples:
3132
3195
  ).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--agent <id>", "Agent ID").action(
3133
3196
  async (mcpServerArg, opts) => {
3134
3197
  try {
3135
- const { input: input5 } = await import("@inquirer/prompts");
3198
+ const { input: input4 } = await import("@inquirer/prompts");
3136
3199
  const { api, tenant } = await resolveMcpContext(opts);
3137
3200
  const server = await resolveServer(mcpServerArg, api, tenant.slug);
3138
3201
  let agent = opts.agent;
@@ -3141,7 +3204,7 @@ Examples:
3141
3204
  printError("--agent is required. Pass it as a flag.");
3142
3205
  process.exit(1);
3143
3206
  }
3144
- agent = await input5({ message: "Agent ID:" });
3207
+ agent = await input4({ message: "Agent ID:" });
3145
3208
  }
3146
3209
  const result = await apiFetch(
3147
3210
  api.apiUrl,
@@ -3164,7 +3227,7 @@ Examples:
3164
3227
  ).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--agent <id>", "Agent ID").action(
3165
3228
  async (mcpServerArg, opts) => {
3166
3229
  try {
3167
- const { input: input5 } = await import("@inquirer/prompts");
3230
+ const { input: input4 } = await import("@inquirer/prompts");
3168
3231
  const { api, tenant } = await resolveMcpContext(opts);
3169
3232
  const server = await resolveServer(mcpServerArg, api, tenant.slug);
3170
3233
  let agent = opts.agent;
@@ -3173,7 +3236,7 @@ Examples:
3173
3236
  printError("--agent is required. Pass it as a flag.");
3174
3237
  process.exit(1);
3175
3238
  }
3176
- agent = await input5({ message: "Agent ID:" });
3239
+ agent = await input4({ message: "Agent ID:" });
3177
3240
  }
3178
3241
  await apiFetch(
3179
3242
  api.apiUrl,
@@ -3189,6 +3252,175 @@ Examples:
3189
3252
  }
3190
3253
  }
3191
3254
  );
3255
+ const key = mcp.command("key").alias("keys").description("Manage per-tenant Bearer tokens for the admin-ops MCP server.");
3256
+ key.command("create").description(
3257
+ "Generate a new MCP admin token. Prints the raw token ONCE \u2014 save it immediately."
3258
+ ).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(
3259
+ "after",
3260
+ `
3261
+ Examples:
3262
+ $ thinkwork mcp key create -t acme --name ci
3263
+ $ thinkwork mcp key create -t acme --json # token surfaces under .token
3264
+
3265
+ The token is shown ONCE. Hand it to the MCP client immediately; the server
3266
+ stores only the SHA-256 hash. To rotate, create a new one and revoke the
3267
+ old one.
3268
+ `
3269
+ ).action(async (opts) => {
3270
+ try {
3271
+ const ctx = await resolveMcpContext(opts);
3272
+ const client = createClient({
3273
+ apiUrl: ctx.api.apiUrl,
3274
+ authSecret: ctx.api.authSecret
3275
+ });
3276
+ const created = await adminKeys.createAdminKey(client, ctx.tenant.slug, {
3277
+ name: opts.name
3278
+ });
3279
+ printJson(created);
3280
+ printWarning("This token will NOT be shown again. Copy it now.");
3281
+ printSuccess(`MCP admin key created: ${chalk10.bold(created.name)} (${created.id})`);
3282
+ console.log(` ${chalk10.dim("Token:")} ${chalk10.cyan(created.token)}`);
3283
+ } catch (err) {
3284
+ if (isCancellation(err)) return;
3285
+ printError(err instanceof Error ? err.message : String(err));
3286
+ process.exit(1);
3287
+ }
3288
+ });
3289
+ 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) => {
3290
+ try {
3291
+ const ctx = await resolveMcpContext(opts);
3292
+ const client = createClient({
3293
+ apiUrl: ctx.api.apiUrl,
3294
+ authSecret: ctx.api.authSecret
3295
+ });
3296
+ const all = await adminKeys.listAdminKeys(client, ctx.tenant.slug);
3297
+ const rows = opts.all ? all : all.filter((k) => !k.revoked_at);
3298
+ printJson(rows);
3299
+ printTable(rows, [
3300
+ { key: "name", header: "NAME" },
3301
+ { key: "id", header: "ID" },
3302
+ { key: "created_at", header: "CREATED" },
3303
+ { key: "last_used_at", header: "LAST USED" },
3304
+ { key: "revoked_at", header: "REVOKED" }
3305
+ ]);
3306
+ } catch (err) {
3307
+ if (isCancellation(err)) return;
3308
+ printError(err instanceof Error ? err.message : String(err));
3309
+ process.exit(1);
3310
+ }
3311
+ });
3312
+ 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) => {
3313
+ try {
3314
+ const ctx = await resolveMcpContext(opts);
3315
+ const client = createClient({
3316
+ apiUrl: ctx.api.apiUrl,
3317
+ authSecret: ctx.api.authSecret
3318
+ });
3319
+ await adminKeys.revokeAdminKey(client, ctx.tenant.slug, keyId);
3320
+ printSuccess(`MCP admin key revoked: ${keyId}`);
3321
+ } catch (err) {
3322
+ if (err instanceof AdminOpsError && err.status === 404) {
3323
+ printError(`Key ${keyId} not found for tenant ${opts.tenant ?? ""}`);
3324
+ process.exit(2);
3325
+ }
3326
+ if (isCancellation(err)) return;
3327
+ printError(err instanceof Error ? err.message : String(err));
3328
+ process.exit(1);
3329
+ }
3330
+ });
3331
+ mcp.command("provision").description(
3332
+ "Provision the admin-ops MCP for a tenant: mint a new key + store in Secrets Manager + register in tenant_mcp_servers."
3333
+ ).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug or UUID (omit with --all)").option(
3334
+ "--url <url>",
3335
+ "Override the MCP endpoint URL (defaults to the stage's execute-api or MCP_CUSTOM_DOMAIN)"
3336
+ ).option("--all", "Provision for every tenant the caller can see (backfill mode)").addHelpText(
3337
+ "after",
3338
+ `
3339
+ Examples:
3340
+ # One tenant (interactive picker or -t)
3341
+ $ thinkwork mcp provision -t acme
3342
+
3343
+ # Explicit custom-domain URL
3344
+ $ thinkwork mcp provision -t acme --url https://mcp.thinkwork.ai/mcp/admin
3345
+
3346
+ # Backfill every tenant at once
3347
+ $ thinkwork mcp provision --all
3348
+
3349
+ The raw token is stored in Secrets Manager and duplicated into
3350
+ tenant_mcp_servers.auth_config \u2014 it is NOT printed on stdout. After
3351
+ provisioning, each agent still needs the server assigned via the admin
3352
+ SPA (or a future \`thinkwork mcp assign\` CLI pass).
3353
+ `
3354
+ ).action(async (opts) => {
3355
+ try {
3356
+ if (opts.all && opts.tenant) {
3357
+ printError("--all and --tenant are mutually exclusive.");
3358
+ process.exit(1);
3359
+ }
3360
+ const stage = await resolveStage({ flag: opts.stage });
3361
+ const api = resolveApiConfig(stage);
3362
+ if (!api) process.exit(1);
3363
+ async function provisionOne(tenantIdOrSlug, label) {
3364
+ const res = await apiFetch(
3365
+ api.apiUrl,
3366
+ api.authSecret,
3367
+ `/api/tenants/${encodeURIComponent(tenantIdOrSlug)}/mcp-admin-provision`,
3368
+ {
3369
+ method: "POST",
3370
+ body: JSON.stringify(opts.url ? { url: opts.url } : {})
3371
+ }
3372
+ );
3373
+ printSuccess(
3374
+ `${label}: ${res.provisioned} (tenant_mcp_servers.id=${res.tenantMcpServerId}, url=${res.url})`
3375
+ );
3376
+ return res;
3377
+ }
3378
+ if (opts.all) {
3379
+ const tenantRows = await apiFetch(
3380
+ api.apiUrl,
3381
+ api.authSecret,
3382
+ "/api/tenants",
3383
+ {}
3384
+ );
3385
+ if (!Array.isArray(tenantRows) || tenantRows.length === 0) {
3386
+ printWarning("No tenants found.");
3387
+ return;
3388
+ }
3389
+ printHeader("mcp provision --all", stage);
3390
+ const results = [];
3391
+ for (const t of tenantRows) {
3392
+ try {
3393
+ await provisionOne(t.slug, `${t.name} (${t.slug})`);
3394
+ results.push({ slug: t.slug, ok: true });
3395
+ } catch (err) {
3396
+ const msg = err instanceof Error ? err.message : String(err);
3397
+ printError(` \u2717 ${t.slug}: ${msg}`);
3398
+ results.push({ slug: t.slug, ok: false, reason: msg });
3399
+ }
3400
+ }
3401
+ const ok = results.filter((r) => r.ok).length;
3402
+ const bad = results.length - ok;
3403
+ if (bad === 0) {
3404
+ printSuccess(`All ${ok} tenants provisioned.`);
3405
+ } else {
3406
+ printWarning(`${ok}/${results.length} succeeded; ${bad} failed.`);
3407
+ process.exit(1);
3408
+ }
3409
+ return;
3410
+ }
3411
+ const tenant = await resolveTenantRest({
3412
+ flag: opts.tenant,
3413
+ stage,
3414
+ apiUrl: api.apiUrl,
3415
+ authSecret: api.authSecret
3416
+ });
3417
+ await provisionOne(tenant.slug, tenant.slug);
3418
+ } catch (err) {
3419
+ if (isCancellation(err)) return;
3420
+ printError(err instanceof Error ? err.message : String(err));
3421
+ process.exit(1);
3422
+ }
3423
+ });
3192
3424
  }
3193
3425
 
3194
3426
  // src/commands/tools.ts
@@ -3463,11 +3695,11 @@ function registerUpdateCommand(program2) {
3463
3695
  }
3464
3696
 
3465
3697
  // src/commands/user.ts
3466
- import { spawn as spawn4 } from "child_process";
3698
+ import { spawn as spawn5 } from "child_process";
3467
3699
  import { input as input2, select as select7 } from "@inquirer/prompts";
3468
3700
  function getTerraformOutput2(cwd, key) {
3469
- return new Promise((resolve3, reject) => {
3470
- const proc = spawn4("terraform", ["output", "-raw", key], {
3701
+ return new Promise((resolve4, reject) => {
3702
+ const proc = spawn5("terraform", ["output", "-raw", key], {
3471
3703
  cwd,
3472
3704
  stdio: ["pipe", "pipe", "pipe"]
3473
3705
  });
@@ -3476,7 +3708,7 @@ function getTerraformOutput2(cwd, key) {
3476
3708
  proc.stdout.on("data", (d) => stdout += d);
3477
3709
  proc.stderr.on("data", (d) => stderr += d);
3478
3710
  proc.on("close", (code) => {
3479
- if (code === 0) resolve3(stdout.trim());
3711
+ if (code === 0) resolve4(stdout.trim());
3480
3712
  else
3481
3713
  reject(
3482
3714
  new Error(
@@ -3487,7 +3719,7 @@ function getTerraformOutput2(cwd, key) {
3487
3719
  });
3488
3720
  }
3489
3721
  function runAwsCognitoReset(userPoolId, username, region) {
3490
- return new Promise((resolve3) => {
3722
+ return new Promise((resolve4) => {
3491
3723
  const args = [
3492
3724
  "cognito-idp",
3493
3725
  "admin-reset-user-password",
@@ -3499,12 +3731,12 @@ function runAwsCognitoReset(userPoolId, username, region) {
3499
3731
  "json"
3500
3732
  ];
3501
3733
  if (region) args.push("--region", region);
3502
- const proc = spawn4("aws", args, { stdio: ["ignore", "pipe", "pipe"] });
3734
+ const proc = spawn5("aws", args, { stdio: ["ignore", "pipe", "pipe"] });
3503
3735
  let stdout = "";
3504
3736
  let stderr = "";
3505
3737
  proc.stdout.on("data", (d) => stdout += d);
3506
3738
  proc.stderr.on("data", (d) => stderr += d);
3507
- proc.on("close", (code) => resolve3({ code: code ?? 1, stdout, stderr }));
3739
+ proc.on("close", (code) => resolve4({ code: code ?? 1, stdout, stderr }));
3508
3740
  });
3509
3741
  }
3510
3742
  function requireTty2(label) {
@@ -3785,9 +4017,8 @@ import { gql } from "@urql/core";
3785
4017
 
3786
4018
  // src/lib/gql-client.ts
3787
4019
  import {
3788
- Client,
3789
- cacheExchange,
3790
- fetchExchange
4020
+ CombinedError,
4021
+ stringifyDocument
3791
4022
  } from "@urql/core";
3792
4023
 
3793
4024
  // src/lib/resolve-auth.ts
@@ -3883,20 +4114,7 @@ async function getGqlClient(opts) {
3883
4114
  }
3884
4115
  const url = `${baseUrl.replace(/\/+$/, "")}/graphql`;
3885
4116
  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
- });
4117
+ const client = createCliGqlClient(url, auth.headers);
3900
4118
  return {
3901
4119
  client,
3902
4120
  url,
@@ -3904,14 +4122,95 @@ async function getGqlClient(opts) {
3904
4122
  tenantSlug: auth.tenantSlug
3905
4123
  };
3906
4124
  }
4125
+ function createCliGqlClient(url, headers) {
4126
+ return {
4127
+ query: (doc, variables) => ({
4128
+ toPromise: () => executeGraphql(url, headers, doc, variables)
4129
+ }),
4130
+ mutation: (doc, variables) => ({
4131
+ toPromise: () => executeGraphql(url, headers, doc, variables)
4132
+ })
4133
+ };
4134
+ }
3907
4135
  async function gqlQuery(client, doc, variables) {
3908
- const res = await client.query(doc, variables).toPromise();
4136
+ const res = await client.query(serializeDocument(doc), variables).toPromise();
3909
4137
  return unwrap(res);
3910
4138
  }
3911
4139
  async function gqlMutate(client, doc, variables) {
3912
- const res = await client.mutation(doc, variables).toPromise();
4140
+ const res = await client.mutation(serializeDocument(doc), variables).toPromise();
3913
4141
  return unwrap(res);
3914
4142
  }
4143
+ function serializeDocument(doc) {
4144
+ return stringifyDocument(doc);
4145
+ }
4146
+ async function executeGraphql(url, headers, doc, variables) {
4147
+ const query = serializeDocument(doc);
4148
+ try {
4149
+ const response = await fetch(url, {
4150
+ method: "POST",
4151
+ headers: {
4152
+ "content-type": "application/json",
4153
+ ...headers
4154
+ },
4155
+ body: JSON.stringify({ query, variables })
4156
+ });
4157
+ const text = await response.text();
4158
+ let payload = {};
4159
+ if (text) {
4160
+ try {
4161
+ payload = JSON.parse(text);
4162
+ } catch {
4163
+ return makeNetworkErrorResult(
4164
+ `GraphQL request failed with non-JSON response: ${text}`,
4165
+ response
4166
+ );
4167
+ }
4168
+ }
4169
+ if (payload.errors?.length) {
4170
+ return {
4171
+ data: payload.data,
4172
+ error: new CombinedError({
4173
+ graphQLErrors: payload.errors,
4174
+ response
4175
+ }),
4176
+ extensions: payload.extensions,
4177
+ stale: false,
4178
+ hasNext: false
4179
+ };
4180
+ }
4181
+ if (!response.ok) {
4182
+ return makeNetworkErrorResult(
4183
+ `GraphQL request failed with HTTP ${response.status}`,
4184
+ response
4185
+ );
4186
+ }
4187
+ return {
4188
+ data: payload.data,
4189
+ error: void 0,
4190
+ extensions: payload.extensions,
4191
+ stale: false,
4192
+ hasNext: false
4193
+ };
4194
+ } catch (err) {
4195
+ return {
4196
+ error: new CombinedError({
4197
+ networkError: err instanceof Error ? err : new Error(String(err))
4198
+ }),
4199
+ stale: false,
4200
+ hasNext: false
4201
+ };
4202
+ }
4203
+ }
4204
+ function makeNetworkErrorResult(message, response) {
4205
+ return {
4206
+ error: new CombinedError({
4207
+ networkError: new Error(message),
4208
+ response
4209
+ }),
4210
+ stale: false,
4211
+ hasNext: false
4212
+ };
4213
+ }
3915
4214
  function unwrap(res) {
3916
4215
  if (res.error) {
3917
4216
  const msg = res.error.graphQLErrors.map((e) => e.message).filter(Boolean).join("; ") || res.error.networkError?.message || "GraphQL request failed";
@@ -4012,20 +4311,20 @@ function notYetImplemented(commandPath, phase) {
4012
4311
  // src/commands/thread.ts
4013
4312
  function registerThreadCommand(program2) {
4014
4313
  const thread = program2.command("thread").alias("threads").description(
4015
- "Create, list, update, and comment on threads (tasks, chats, bugs, questions) in a tenant."
4314
+ "Create, list, update, and comment on threads in a tenant."
4016
4315
  );
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(
4316
+ 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
4317
  "after",
4019
4318
  `
4020
4319
  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
4320
  # Everything assigned to me
4028
4321
  $ thinkwork thread list --assignee me
4322
+
4323
+ # Limit + JSON for piping
4324
+ $ thinkwork thread list --limit 100 --json | jq '.[] | .title'
4325
+
4326
+ # Archived threads only
4327
+ $ thinkwork thread list --archived
4029
4328
  `
4030
4329
  ).action(() => notYetImplemented("thread list", 1));
4031
4330
  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 +4336,29 @@ Examples:
4037
4336
  $ thinkwork thread get 42 --json | jq .assignee
4038
4337
  `
4039
4338
  ).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(
4339
+ 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
4340
  "after",
4042
4341
  `
4043
4342
  Examples:
4044
- # Fully interactive \u2014 walkthrough prompts for title, type, priority, assignee.
4343
+ # Fully interactive \u2014 walkthrough prompts for title and assignee.
4045
4344
  $ thinkwork thread create
4046
4345
 
4047
4346
  # Scripted
4048
4347
  $ thinkwork thread create "Investigate latency spike" \\
4049
- --priority HIGH --assignee agt-obs-1 --label ops --label oncall
4348
+ --assignee agt-obs-1 --label ops --label oncall
4050
4349
 
4051
4350
  # Mix: pass the title, prompt for the rest.
4052
4351
  $ thinkwork thread create "Investigate latency spike"
4053
4352
  `
4054
4353
  ).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(
4354
+ 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
4355
  "after",
4057
4356
  `
4058
4357
  Examples:
4059
- $ thinkwork thread update thr-abc --status IN_REVIEW
4060
- $ thinkwork thread update thr-abc --assignee agt-ops --priority URGENT
4358
+ $ thinkwork thread update thr-abc --title "New title"
4359
+ $ thinkwork thread update thr-abc --assignee agt-ops
4061
4360
  `
4062
4361
  ).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
4362
  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
4363
  "after",
4074
4364
  `
@@ -4076,7 +4366,7 @@ Examples:
4076
4366
  $ thinkwork thread checkout thr-abc --agent agt-fixer
4077
4367
  `
4078
4368
  ).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));
4369
+ 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
4370
  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
4371
  "after",
4082
4372
  `
@@ -4262,6 +4552,216 @@ Examples:
4262
4552
  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
4553
  }
4264
4554
 
4555
+ // src/commands/computer.ts
4556
+ import chalk14 from "chalk";
4557
+ async function resolveComputerContext(opts) {
4558
+ const stage = await resolveStage({ flag: opts.stage });
4559
+ const api = resolveApiConfig(stage);
4560
+ if (!api) process.exit(1);
4561
+ return { stage, api };
4562
+ }
4563
+ function resolveTenantId(opts) {
4564
+ const tenantId = opts.tenant ?? opts.tenantId;
4565
+ if (!tenantId) {
4566
+ printError(
4567
+ "Tenant ID is required. Pass --tenant <uuid> or --tenant-id <uuid>."
4568
+ );
4569
+ process.exit(1);
4570
+ }
4571
+ return tenantId;
4572
+ }
4573
+ function resolveComputerId(opts) {
4574
+ const computerId = opts.computer ?? opts.computerId;
4575
+ if (!computerId) {
4576
+ printError(
4577
+ "Computer ID is required. Pass --computer <uuid> or --computer-id <uuid>."
4578
+ );
4579
+ process.exit(1);
4580
+ }
4581
+ return computerId;
4582
+ }
4583
+ function resolveTaskType(opts) {
4584
+ if (!opts.type?.trim()) {
4585
+ printError("Task type is required. Pass --type <task-type>.");
4586
+ process.exit(1);
4587
+ }
4588
+ return opts.type.trim().toLowerCase();
4589
+ }
4590
+ function printMigrationReport(response) {
4591
+ printJson(response);
4592
+ if (isJsonMode()) return;
4593
+ if (!response.report) return;
4594
+ const summary = response.report.summary ?? {};
4595
+ console.log("");
4596
+ console.log(chalk14.bold(" Summary"));
4597
+ for (const [status, count] of Object.entries(summary)) {
4598
+ if (!count) continue;
4599
+ console.log(` ${status.padEnd(28)} ${count}`);
4600
+ }
4601
+ const rows = (response.report.groups ?? []).map((group) => ({
4602
+ owner: group.owner?.name ?? group.owner?.email ?? group.ownerUserId ?? "unpaired",
4603
+ source: group.primaryAgent?.name ?? group.primaryAgentId ?? "\u2014",
4604
+ template: group.primaryAgent?.templateName ?? "\u2014",
4605
+ status: group.status,
4606
+ action: group.recommendedAction ?? "\u2014",
4607
+ reason: group.reasons?.[0] ?? "\u2014"
4608
+ }));
4609
+ console.log("");
4610
+ printTable(rows, [
4611
+ { key: "owner", header: "Owner" },
4612
+ { key: "source", header: "Source Agent" },
4613
+ { key: "template", header: "Template" },
4614
+ { key: "status", header: "Status" },
4615
+ { key: "action", header: "Action" },
4616
+ { key: "reason", header: "Reason" }
4617
+ ]);
4618
+ }
4619
+ function registerComputerCommand(program2) {
4620
+ const computer = program2.command("computer").alias("computers").description("Manage ThinkWork Computers and migration operations");
4621
+ const migration = computer.command("migration").description("Dry-run or apply Agent-to-Computer migration");
4622
+ 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(
4623
+ async (opts) => {
4624
+ const { stage, api } = await resolveComputerContext(opts);
4625
+ const tenantId = resolveTenantId(opts);
4626
+ if (!isJsonMode()) printHeader("computer migration dry-run", stage);
4627
+ const response = await apiFetchRaw(
4628
+ api.apiUrl,
4629
+ api.authSecret,
4630
+ "/api/migrations/agents-to-computers",
4631
+ {
4632
+ method: "POST",
4633
+ body: JSON.stringify({ tenantId, mode: "dry-run" })
4634
+ }
4635
+ );
4636
+ if (!response.ok) {
4637
+ printJson(response.body);
4638
+ printError(response.body.error ?? `HTTP ${response.status}`);
4639
+ process.exit(1);
4640
+ }
4641
+ printMigrationReport(response.body);
4642
+ if (!isJsonMode()) printSuccess("Computer migration dry-run complete");
4643
+ }
4644
+ );
4645
+ migration.command("apply").description(
4646
+ "Apply Agent-to-Computer migration after reviewing dry-run output"
4647
+ ).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(
4648
+ async (opts) => {
4649
+ const { stage, api } = await resolveComputerContext(opts);
4650
+ const tenantId = resolveTenantId(opts);
4651
+ if (!opts.confirm) {
4652
+ printWarning(
4653
+ "Apply is intentionally gated. Re-run with --confirm after reviewing dry-run output."
4654
+ );
4655
+ process.exit(1);
4656
+ }
4657
+ if (!isJsonMode()) printHeader("computer migration apply", stage);
4658
+ const response = await apiFetchRaw(
4659
+ api.apiUrl,
4660
+ api.authSecret,
4661
+ "/api/migrations/agents-to-computers",
4662
+ {
4663
+ method: "POST",
4664
+ body: JSON.stringify({
4665
+ tenantId,
4666
+ mode: "apply",
4667
+ idempotencyKey: opts.idempotencyKey
4668
+ })
4669
+ }
4670
+ );
4671
+ if (!response.ok) {
4672
+ printJson(response.body);
4673
+ printError(response.body.error ?? `HTTP ${response.status}`);
4674
+ if (response.status === 409 && response.body.blockers) {
4675
+ if (!isJsonMode()) {
4676
+ console.log("");
4677
+ console.log(chalk14.bold(" Blockers"));
4678
+ console.log(JSON.stringify(response.body.blockers, null, 2));
4679
+ }
4680
+ }
4681
+ process.exit(1);
4682
+ }
4683
+ printMigrationReport(response.body);
4684
+ if (!isJsonMode()) {
4685
+ printSuccess(
4686
+ `Computer migration applied: ${response.body.created?.length ?? 0} created, ${response.body.skipped?.length ?? 0} skipped`
4687
+ );
4688
+ }
4689
+ }
4690
+ );
4691
+ const runtime = computer.command("runtime").description("Provision and control ECS-backed Computer runtimes");
4692
+ for (const action of [
4693
+ "provision",
4694
+ "start",
4695
+ "stop",
4696
+ "restart",
4697
+ "status"
4698
+ ]) {
4699
+ 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(
4700
+ async (opts) => {
4701
+ const { stage, api } = await resolveComputerContext(opts);
4702
+ const tenantId = resolveTenantId(opts);
4703
+ const computerId = resolveComputerId(opts);
4704
+ if (!isJsonMode()) {
4705
+ printHeader(`computer runtime ${action}`, stage);
4706
+ }
4707
+ const response = await apiFetchRaw(
4708
+ api.apiUrl,
4709
+ api.authSecret,
4710
+ "/api/computers/manager",
4711
+ {
4712
+ method: "POST",
4713
+ body: JSON.stringify({ action, tenantId, computerId })
4714
+ }
4715
+ );
4716
+ printJson(response.body);
4717
+ if (!response.ok) {
4718
+ printError(response.body.error ?? `HTTP ${response.status}`);
4719
+ process.exit(1);
4720
+ }
4721
+ if (!isJsonMode()) {
4722
+ printSuccess(`Computer runtime ${action} complete`);
4723
+ }
4724
+ }
4725
+ );
4726
+ }
4727
+ const task = computer.command("task").description("Enqueue work for a ThinkWork Computer runtime");
4728
+ 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(
4729
+ async (opts) => {
4730
+ const { stage, api } = await resolveComputerContext(opts);
4731
+ const tenantId = resolveTenantId(opts);
4732
+ const computerId = resolveComputerId(opts);
4733
+ const taskType = resolveTaskType(opts);
4734
+ const input4 = taskType === "workspace_file_write" ? { path: opts.path, content: opts.content } : void 0;
4735
+ if (!isJsonMode()) printHeader("computer task enqueue", stage);
4736
+ const response = await apiFetchRaw(
4737
+ api.apiUrl,
4738
+ api.authSecret,
4739
+ "/api/computers/runtime/tasks",
4740
+ {
4741
+ method: "POST",
4742
+ body: JSON.stringify({
4743
+ tenantId,
4744
+ computerId,
4745
+ taskType,
4746
+ input: input4,
4747
+ idempotencyKey: opts.idempotencyKey
4748
+ })
4749
+ }
4750
+ );
4751
+ printJson(response.body);
4752
+ if (!response.ok) {
4753
+ printError(response.body.error ?? `HTTP ${response.status}`);
4754
+ process.exit(1);
4755
+ }
4756
+ if (!isJsonMode()) {
4757
+ printSuccess(
4758
+ `Queued Computer task ${response.body.task?.id ?? taskType}`
4759
+ );
4760
+ }
4761
+ }
4762
+ );
4763
+ }
4764
+
4265
4765
  // src/commands/template.ts
4266
4766
  function registerTemplateCommand(program2) {
4267
4767
  const tpl = program2.command("template").alias("templates").description("Manage agent templates \u2014 reusable configs you spawn agents from.");
@@ -4299,10 +4799,70 @@ Examples:
4299
4799
  }
4300
4800
 
4301
4801
  // src/commands/tenant.ts
4802
+ import { createClient as createClient2, tenants as tenantOps, AdminOpsError as AdminOpsError2 } from "@thinkwork/admin-ops";
4302
4803
  function registerTenantCommand(program2) {
4303
4804
  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));
4805
+ tenant.command("list").alias("ls").description("List tenants the caller can see.").option("-s, --stage <name>", "Deployment stage").addHelpText(
4806
+ "after",
4807
+ `
4808
+ Examples:
4809
+ $ thinkwork tenant list
4810
+ $ thinkwork tenant list -s dev
4811
+ $ thinkwork tenant list --json | jq '.[].slug'
4812
+ `
4813
+ ).action(async (opts) => {
4814
+ try {
4815
+ const stage = await resolveStage({ flag: opts.stage });
4816
+ const api = resolveApiConfig(stage);
4817
+ if (!api) process.exit(1);
4818
+ const client = createClient2({ apiUrl: api.apiUrl, authSecret: api.authSecret });
4819
+ const rows = await tenantOps.listTenants(client);
4820
+ printJson(rows);
4821
+ printTable(rows, [
4822
+ { key: "slug", header: "SLUG" },
4823
+ { key: "name", header: "NAME" },
4824
+ { key: "plan", header: "PLAN" },
4825
+ { key: "id", header: "ID" }
4826
+ ]);
4827
+ } catch (err) {
4828
+ printError(err instanceof Error ? err.message : String(err));
4829
+ process.exit(1);
4830
+ }
4831
+ });
4832
+ tenant.command("get <idOrSlug>").description("Fetch one tenant by ID or slug.").option("-s, --stage <name>", "Deployment stage").addHelpText(
4833
+ "after",
4834
+ `
4835
+ Examples:
4836
+ $ thinkwork tenant get acme
4837
+ $ thinkwork tenant get 0a2b... --json
4838
+ `
4839
+ ).action(async (idOrSlug, opts) => {
4840
+ try {
4841
+ const stage = await resolveStage({ flag: opts.stage });
4842
+ const api = resolveApiConfig(stage);
4843
+ if (!api) process.exit(1);
4844
+ const client = createClient2({ apiUrl: api.apiUrl, authSecret: api.authSecret });
4845
+ const isUuid2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
4846
+ idOrSlug
4847
+ );
4848
+ const tenant2 = isUuid2 ? await tenantOps.getTenant(client, idOrSlug) : await tenantOps.getTenantBySlug(client, idOrSlug);
4849
+ printJson(tenant2);
4850
+ printTable([tenant2], [
4851
+ { key: "slug", header: "SLUG" },
4852
+ { key: "name", header: "NAME" },
4853
+ { key: "plan", header: "PLAN" },
4854
+ { key: "issue_prefix", header: "PREFIX" },
4855
+ { key: "id", header: "ID" }
4856
+ ]);
4857
+ } catch (err) {
4858
+ if (err instanceof AdminOpsError2 && err.status === 404) {
4859
+ printError(`Tenant "${idOrSlug}" not found`);
4860
+ process.exit(2);
4861
+ }
4862
+ printError(err instanceof Error ? err.message : String(err));
4863
+ process.exit(1);
4864
+ }
4865
+ });
4306
4866
  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
4867
  "after",
4308
4868
  `
@@ -4492,29 +5052,218 @@ Examples:
4492
5052
  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
5053
  }
4494
5054
 
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
5055
+ // src/lib/plugin-zip.ts
5056
+ import { createReadStream, promises as fsp, statSync } from "fs";
5057
+ import { basename, join as join6, relative, resolve as resolve3, sep } from "path";
5058
+ import JSZip from "jszip";
5059
+ var PluginZipError = class extends Error {
5060
+ constructor(message, kind) {
5061
+ super(message);
5062
+ this.kind = kind;
5063
+ this.name = "PluginZipError";
5064
+ }
5065
+ kind;
5066
+ };
5067
+ async function buildPluginZip(pluginDir) {
5068
+ const root = resolve3(pluginDir);
5069
+ let stat;
5070
+ try {
5071
+ stat = await fsp.stat(root);
5072
+ } catch {
5073
+ throw new PluginZipError(
5074
+ `Plugin directory not found: ${pluginDir}`,
5075
+ "missing-directory"
5076
+ );
5077
+ }
5078
+ if (!stat.isDirectory()) {
5079
+ throw new PluginZipError(
5080
+ `Plugin path must be a directory: ${pluginDir}`,
5081
+ "missing-directory"
5082
+ );
5083
+ }
5084
+ const manifestPath = join6(root, "plugin.json");
5085
+ let manifestRaw;
5086
+ try {
5087
+ manifestRaw = await fsp.readFile(manifestPath, "utf8");
5088
+ } catch {
5089
+ throw new PluginZipError(
5090
+ `plugin.json is required at the root of the plugin folder (expected ${manifestPath}).`,
5091
+ "missing-plugin-json"
5092
+ );
5093
+ }
5094
+ let parsed;
5095
+ try {
5096
+ parsed = JSON.parse(manifestRaw);
5097
+ } catch (err) {
5098
+ throw new PluginZipError(
5099
+ `plugin.json is not valid JSON: ${err.message}`,
5100
+ "invalid-plugin-json"
5101
+ );
5102
+ }
5103
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
5104
+ throw new PluginZipError(
5105
+ `plugin.json must be a JSON object with a "name" field.`,
5106
+ "invalid-plugin-json"
5107
+ );
5108
+ }
5109
+ const manifest = parsed;
5110
+ if (typeof manifest.name !== "string" || manifest.name.trim() === "") {
5111
+ throw new PluginZipError(
5112
+ `plugin.json must declare a non-empty "name" string.`,
5113
+ "invalid-plugin-json"
5114
+ );
5115
+ }
5116
+ const zip = new JSZip();
5117
+ const entries = await walkDir(root, root);
5118
+ for (const entry of entries) {
5119
+ if (entry.isSymbolicLink) {
5120
+ throw new PluginZipError(
5121
+ `Refusing to zip symlink: ${entry.relPath} (would be rejected server-side).`,
5122
+ "unsafe-entry"
5123
+ );
5124
+ }
5125
+ if (hasParentSegment(entry.relPath)) {
5126
+ throw new PluginZipError(
5127
+ `Refusing to zip path with traversal segment: ${entry.relPath}`,
5128
+ "unsafe-entry"
5129
+ );
5130
+ }
5131
+ const archivePath = entry.relPath.split(sep).join("/");
5132
+ zip.file(archivePath, createReadStream(entry.absPath));
5133
+ }
5134
+ let buffer;
5135
+ try {
5136
+ buffer = await zip.generateAsync({
5137
+ type: "nodebuffer",
5138
+ compression: "DEFLATE",
5139
+ compressionOptions: { level: 6 }
5140
+ });
5141
+ } catch (err) {
5142
+ throw new PluginZipError(
5143
+ `Failed to compress plugin: ${err.message}`,
5144
+ "io"
5145
+ );
5146
+ }
5147
+ const metadata = {
5148
+ name: manifest.name.trim(),
5149
+ version: typeof manifest.version === "string" && manifest.version.trim() !== "" ? manifest.version.trim() : void 0,
5150
+ description: typeof manifest.description === "string" ? manifest.description.trim() || void 0 : void 0
5151
+ };
5152
+ return {
5153
+ buffer,
5154
+ plugin: metadata,
5155
+ fileCount: entries.length,
5156
+ zipFileName: `${basename(root)}.zip`
5157
+ };
5158
+ }
5159
+ async function walkDir(rootDir, currentDir) {
5160
+ const out = [];
5161
+ const dirents = await fsp.readdir(currentDir, { withFileTypes: true });
5162
+ for (const ent of dirents) {
5163
+ const abs = join6(currentDir, ent.name);
5164
+ const rel = relative(rootDir, abs);
5165
+ if (ent.name === ".git" || ent.name === ".DS_Store" || ent.name === "node_modules") {
5166
+ continue;
5167
+ }
5168
+ if (ent.isSymbolicLink()) {
5169
+ out.push({ absPath: abs, relPath: rel, isSymbolicLink: true });
5170
+ continue;
5171
+ }
5172
+ if (ent.isDirectory()) {
5173
+ out.push(...await walkDir(rootDir, abs));
5174
+ continue;
5175
+ }
5176
+ if (ent.isFile()) {
5177
+ try {
5178
+ statSync(abs);
5179
+ } catch {
5180
+ continue;
5181
+ }
5182
+ out.push({ absPath: abs, relPath: rel, isSymbolicLink: false });
5183
+ }
5184
+ }
5185
+ return out.sort(
5186
+ (a, b) => a.relPath < b.relPath ? -1 : a.relPath > b.relPath ? 1 : 0
5187
+ );
5188
+ }
5189
+ function hasParentSegment(relPath) {
5190
+ const parts = relPath.split(sep);
5191
+ return parts.some((p) => p === "..");
5192
+ }
4506
5193
 
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));
5194
+ // src/lib/plugin-push.ts
5195
+ async function pushPluginZip(input4) {
5196
+ const base = input4.apiUrl.replace(/\/+$/, "");
5197
+ const presignRes = await fetch(`${base}/api/plugins/presign`, {
5198
+ method: "POST",
5199
+ headers: withJson(input4.headers),
5200
+ body: JSON.stringify({ fileName: input4.fileName })
5201
+ });
5202
+ if (!presignRes.ok) {
5203
+ throw new Error(`presign failed: ${await describeHttpError(presignRes)}`);
5204
+ }
5205
+ const presign = await presignRes.json();
5206
+ if (!presign.uploadUrl || !presign.s3Key) {
5207
+ throw new Error(
5208
+ `presign returned invalid response: ${JSON.stringify(presign)}`
5209
+ );
5210
+ }
5211
+ const putRes = await fetch(presign.uploadUrl, {
5212
+ method: "PUT",
5213
+ headers: { "Content-Type": "application/zip" },
5214
+ body: new Uint8Array(input4.zipBuffer)
5215
+ });
5216
+ if (!putRes.ok) {
5217
+ throw new Error(`S3 PUT failed: HTTP ${putRes.status}`);
5218
+ }
5219
+ const installRes = await fetch(`${base}/api/plugins/upload`, {
5220
+ method: "POST",
5221
+ headers: withJson(input4.headers),
5222
+ body: JSON.stringify({ s3Key: presign.s3Key })
5223
+ });
5224
+ const installBody = await installRes.json().catch(() => ({}));
5225
+ if (installRes.status === 400 && installBody && installBody.valid === false) {
5226
+ return {
5227
+ status: "validation-failed",
5228
+ errors: installBody.errors ?? [],
5229
+ warnings: installBody.warnings ?? []
5230
+ };
5231
+ }
5232
+ if (!installRes.ok) {
5233
+ const uploadId = typeof installBody.uploadId === "string" ? installBody.uploadId : "";
5234
+ return {
5235
+ status: "failed",
5236
+ uploadId,
5237
+ phase: typeof installBody.phase === "string" ? installBody.phase : void 0,
5238
+ errorMessage: typeof installBody.errorMessage === "string" && installBody.errorMessage || typeof installBody.error === "string" && installBody.error || `HTTP ${installRes.status}`
5239
+ };
5240
+ }
5241
+ const plugin = installBody.plugin;
5242
+ if (!plugin || typeof installBody.uploadId !== "string") {
5243
+ throw new Error(
5244
+ `install response missing uploadId/plugin: ${JSON.stringify(installBody)}`
5245
+ );
5246
+ }
5247
+ return {
5248
+ status: "installed",
5249
+ uploadId: installBody.uploadId,
5250
+ plugin,
5251
+ warnings: Array.isArray(installBody.warnings) ? installBody.warnings : []
5252
+ };
5253
+ }
5254
+ function withJson(headers) {
5255
+ return { "Content-Type": "application/json", ...headers };
5256
+ }
5257
+ async function describeHttpError(res) {
5258
+ const text = await res.text().catch(() => "");
5259
+ return `HTTP ${res.status} ${text.slice(0, 200)}`;
4513
5260
  }
4514
5261
 
4515
5262
  // src/commands/skill.ts
4516
5263
  function registerSkillCommand(program2) {
4517
- const skill = program2.command("skill").alias("skills").description("Browse the catalog, install, upgrade, or publish custom skills.");
5264
+ const skill = program2.command("skill").alias("skills").description(
5265
+ "Browse the catalog, install, upgrade, or publish custom skills."
5266
+ );
4518
5267
  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
5268
  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
5269
  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 +5275,9 @@ Examples:
4526
5275
  `
4527
5276
  ).action(() => notYetImplemented("skill install", 3));
4528
5277
  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(
5278
+ skill.command("create [slug]").description(
5279
+ "Publish a custom tenant-scoped skill (walkthrough for missing fields in TTY)."
5280
+ ).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
5281
  "after",
4531
5282
  `
4532
5283
  Examples:
@@ -4535,7 +5286,103 @@ Examples:
4535
5286
  `
4536
5287
  ).action(() => notYetImplemented("skill create", 3));
4537
5288
  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));
5289
+ skill.command("delete <slug>").description(
5290
+ "Delete a custom skill. Public catalog skills are uninstalled via this too."
5291
+ ).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("skill delete", 3));
5292
+ skill.command("push <folder>").description(
5293
+ "Zip a local plugin folder and upload it to the tenant as a pending plugin."
5294
+ ).option("-s, --stage <name>", "Deployment stage").option("--region <name>", "AWS region", "us-east-1").addHelpText(
5295
+ "after",
5296
+ `
5297
+ Examples:
5298
+ $ thinkwork skill push ./my-plugin
5299
+ $ thinkwork skill push ./my-plugin --stage dev
5300
+
5301
+ The folder must contain a plugin.json manifest. MCP servers shipped
5302
+ inside the plugin will land as 'pending' and need admin approval
5303
+ under Capabilities \u2192 MCP Servers before agents can invoke them.
5304
+ `
5305
+ ).action(
5306
+ async (folder, opts) => {
5307
+ await runPushCommand(folder, opts);
5308
+ }
5309
+ );
5310
+ }
5311
+ async function runPushCommand(folder, opts) {
5312
+ const region = opts.region ?? "us-east-1";
5313
+ const stage = await resolveStage({ flag: opts.stage, region });
5314
+ let zipped;
5315
+ try {
5316
+ zipped = await buildPluginZip(folder);
5317
+ } catch (err) {
5318
+ if (err instanceof PluginZipError) {
5319
+ printError(err.message);
5320
+ process.exit(1);
5321
+ }
5322
+ throw err;
5323
+ }
5324
+ const auth = await resolveAuth({ stage, region, requireCognito: true });
5325
+ if (auth.mode !== "cognito") {
5326
+ printError(
5327
+ `skill push requires a Cognito session. Run \`thinkwork login --stage ${stage}\`.`
5328
+ );
5329
+ process.exit(1);
5330
+ }
5331
+ const apiUrl = getApiEndpoint(stage, region);
5332
+ if (!apiUrl) {
5333
+ printError(
5334
+ `Could not discover API endpoint for stage "${stage}" in ${region}. Is the stack deployed?`
5335
+ );
5336
+ process.exit(1);
5337
+ }
5338
+ printSuccess(
5339
+ `Prepared plugin "${zipped.plugin.name}" \u2014 ${zipped.fileCount} file(s), ${formatBytes(zipped.buffer.length)}`
5340
+ );
5341
+ let result;
5342
+ try {
5343
+ result = await pushPluginZip({
5344
+ apiUrl,
5345
+ headers: auth.headers,
5346
+ zipBuffer: zipped.buffer,
5347
+ fileName: zipped.zipFileName
5348
+ });
5349
+ } catch (err) {
5350
+ printError(`Upload failed: ${err.message}`);
5351
+ process.exit(1);
5352
+ }
5353
+ if (result.status === "validation-failed") {
5354
+ printError("Plugin validation failed");
5355
+ for (const e of result.errors) console.log(` - ${e}`);
5356
+ for (const w of result.warnings) printWarning(w);
5357
+ process.exit(1);
5358
+ }
5359
+ if (result.status === "failed") {
5360
+ printError(
5361
+ `Install failed${result.phase ? ` at phase ${result.phase}` : ""}: ${result.errorMessage}`
5362
+ );
5363
+ if (result.uploadId) {
5364
+ console.log(` upload id: ${result.uploadId}`);
5365
+ }
5366
+ process.exit(1);
5367
+ }
5368
+ const skillCount = result.plugin.skills.length;
5369
+ const mcpCount = result.plugin.mcpServers.length;
5370
+ printSuccess(
5371
+ `Installed "${result.plugin.name}" \u2014 ${skillCount} skill(s)` + (mcpCount > 0 ? `, ${mcpCount} MCP server(s) pending admin approval` : "")
5372
+ );
5373
+ console.log(` upload id: ${result.uploadId}`);
5374
+ if (mcpCount > 0) {
5375
+ console.log(
5376
+ ` approve at: admin SPA \u2192 Capabilities \u2192 MCP Servers (filter: status=pending)`
5377
+ );
5378
+ }
5379
+ for (const w of result.warnings) printWarning(w);
5380
+ }
5381
+ function formatBytes(bytes) {
5382
+ if (bytes < 1024) return `${bytes} B`;
5383
+ const kb = bytes / 1024;
5384
+ if (kb < 1024) return `${kb.toFixed(1)} KB`;
5385
+ return `${(kb / 1024).toFixed(2)} MB`;
4539
5386
  }
4540
5387
 
4541
5388
  // src/commands/memory.ts
@@ -4669,7 +5516,7 @@ Examples:
4669
5516
  }
4670
5517
 
4671
5518
  // src/commands/eval/run.ts
4672
- import { select as select8, checkbox, confirm as confirm2, input as input3 } from "@inquirer/prompts";
5519
+ import { checkbox, confirm as confirm2 } from "@inquirer/prompts";
4673
5520
  import ora2 from "ora";
4674
5521
 
4675
5522
  // src/gql/graphql.ts
@@ -4678,6 +5525,7 @@ var CliEvalRunDocument = { "kind": "Document", "definitions": [{ "kind": "Operat
4678
5525
  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
5526
  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
5527
  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" } }] } }] } }] };
5528
+ 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
5529
  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
5530
  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
5531
  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 +5544,18 @@ var CliWikiCompileJobsDocument = { "kind": "Document", "definitions": [{ "kind":
4696
5544
 
4697
5545
  // src/gql/gql.ts
4698
5546
  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,
5547
+ "\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
5548
  "\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
5549
  "\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
5550
  "\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
5551
  "\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,
5552
+ "\n query CliComputersForEval($tenantId: ID!) {\n computers(tenantId: $tenantId) {\n id\n name\n slug\n runtimeStatus\n }\n }\n": CliComputersForEvalDocument,
4704
5553
  "\n query CliAgentTemplatesForEval($tenantId: ID!) {\n agentTemplates(tenantId: $tenantId) {\n id\n name\n slug\n model\n isPublished\n }\n }\n": CliAgentTemplatesForEvalDocument,
4705
5554
  "\n query CliTenantBySlug($slug: String!) {\n tenantBySlug(slug: $slug) {\n id\n slug\n name\n }\n }\n": CliTenantBySlugDocument,
4706
5555
  "\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
5556
  "\n mutation CliCancelEvalRun($id: ID!) {\n cancelEvalRun(id: $id) {\n id\n status\n completedAt\n }\n }\n": CliCancelEvalRunDocument,
4708
5557
  "\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,
5558
+ "\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
5559
  "\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
5560
  "\n mutation CliDeleteEvalTestCase($id: ID!) {\n deleteEvalTestCase(id: $id)\n }\n": CliDeleteEvalTestCaseDocument,
4712
5561
  "\n mutation CliSeedEvalTestCases($tenantId: ID!, $categories: [String!]) {\n seedEvalTestCases(tenantId: $tenantId, categories: $categories)\n }\n": CliSeedEvalTestCasesDocument,
@@ -4724,7 +5573,12 @@ function graphql(source) {
4724
5573
  // src/commands/eval/gql.ts
4725
5574
  var EvalRunsDoc = graphql(`
4726
5575
  query CliEvalRuns($tenantId: ID!, $agentId: ID, $limit: Int, $offset: Int) {
4727
- evalRuns(tenantId: $tenantId, agentId: $agentId, limit: $limit, offset: $offset) {
5576
+ evalRuns(
5577
+ tenantId: $tenantId
5578
+ agentId: $agentId
5579
+ limit: $limit
5580
+ offset: $offset
5581
+ ) {
4728
5582
  totalCount
4729
5583
  items {
4730
5584
  id
@@ -4834,6 +5688,16 @@ var EvalTestCaseDoc = graphql(`
4834
5688
  }
4835
5689
  }
4836
5690
  `);
5691
+ var ComputersForEvalDoc = graphql(`
5692
+ query CliComputersForEval($tenantId: ID!) {
5693
+ computers(tenantId: $tenantId) {
5694
+ id
5695
+ name
5696
+ slug
5697
+ runtimeStatus
5698
+ }
5699
+ }
5700
+ `);
4837
5701
  var AgentTemplatesForEvalDoc = graphql(`
4838
5702
  query CliAgentTemplatesForEval($tenantId: ID!) {
4839
5703
  agentTemplates(tenantId: $tenantId) {
@@ -4883,7 +5747,10 @@ var DeleteEvalRunDoc = graphql(`
4883
5747
  }
4884
5748
  `);
4885
5749
  var CreateEvalTestCaseDoc = graphql(`
4886
- mutation CliCreateEvalTestCase($tenantId: ID!, $input: CreateEvalTestCaseInput!) {
5750
+ mutation CliCreateEvalTestCase(
5751
+ $tenantId: ID!
5752
+ $input: CreateEvalTestCaseInput!
5753
+ ) {
4887
5754
  createEvalTestCase(tenantId: $tenantId, input: $input) {
4888
5755
  id
4889
5756
  name
@@ -4981,123 +5848,75 @@ function isTerminalStatus(status) {
4981
5848
  }
4982
5849
 
4983
5850
  // src/commands/eval/run.ts
5851
+ var DEFAULT_EVAL_MODEL_ID = "moonshotai.kimi-k2.5";
4984
5852
  async function runEvalRun(opts) {
4985
5853
  const ctx = await resolveEvalContext(opts);
4986
5854
  const interactive = isInteractive();
4987
- let agentTemplateId = opts.agentTemplate ?? null;
5855
+ const deprecatedComputerId = opts.computer ?? null;
4988
5856
  let categories = opts.category ?? null;
4989
5857
  let testCaseIds = opts.testCase ?? null;
5858
+ if (deprecatedComputerId) {
5859
+ printError(
5860
+ "--computer is no longer supported for eval runs. Evals run directly against the default Agent template."
5861
+ );
5862
+ process.exit(1);
5863
+ }
5864
+ if (opts.model && opts.model !== DEFAULT_EVAL_MODEL_ID) {
5865
+ printError(
5866
+ `--model is no longer configurable for eval runs. Evals use ${DEFAULT_EVAL_MODEL_ID}.`
5867
+ );
5868
+ process.exit(1);
5869
+ }
4990
5870
  const scopeSatisfied = testCaseIds && testCaseIds.length > 0 || categories && categories.length > 0 || opts.all === true;
4991
- if (!agentTemplateId || !scopeSatisfied) {
5871
+ if (!scopeSatisfied) {
4992
5872
  if (!interactive) {
4993
5873
  const missing = [];
4994
- if (!agentTemplateId) missing.push("--agent-template");
4995
- if (!scopeSatisfied) missing.push("one of --all | --category | --test-case");
5874
+ if (!scopeSatisfied)
5875
+ missing.push("one of --all | --category | --test-case");
4996
5876
  printError(
4997
5877
  `Missing required flag(s) in non-interactive session: ${missing.join(", ")}.`
4998
5878
  );
4999
5879
  process.exit(1);
5000
5880
  }
5001
5881
  }
5002
- if (!agentTemplateId) {
5003
- const data = await gqlQuery(ctx.client, AgentTemplatesForEvalDoc, {
5882
+ if (!scopeSatisfied) {
5883
+ const tcData = await gqlQuery(ctx.client, EvalTestCasesDoc, {
5004
5884
  tenantId: ctx.tenantId
5005
5885
  });
5006
- const templates = data.agentTemplates ?? [];
5007
- if (templates.length === 0) {
5008
- printError("No agent templates defined for this tenant. Create one first.");
5886
+ const distinctCategories = Array.from(
5887
+ new Set(
5888
+ (tcData.evalTestCases ?? []).filter((tc) => tc.enabled).map((tc) => tc.category)
5889
+ )
5890
+ ).sort();
5891
+ if (distinctCategories.length === 0) {
5892
+ printError(
5893
+ "No enabled test cases exist for this tenant yet. Run `thinkwork eval seed` to load the starter pack."
5894
+ );
5009
5895
  process.exit(1);
5010
5896
  }
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
- ],
5897
+ const picked = await promptOrExit(
5898
+ () => checkbox({
5899
+ message: "Which categories? (space to toggle, enter to confirm)",
5900
+ choices: distinctCategories.map((c) => ({ name: c, value: c })),
5901
+ required: true,
5032
5902
  loop: false
5033
5903
  })
5034
5904
  );
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();
5905
+ categories = picked;
5092
5906
  }
5907
+ const requestedModel = opts.model ?? null;
5908
+ opts.model = DEFAULT_EVAL_MODEL_ID;
5093
5909
  if (interactive && !isJsonMode()) {
5094
5910
  const summaryLines = [
5095
5911
  ["Stage", ctx.stage],
5096
5912
  ["Tenant", ctx.tenantSlug],
5097
- ["Agent template", agentTemplateId]
5913
+ ["Target", "Default Agent template"]
5098
5914
  ];
5915
+ if (requestedModel && requestedModel !== DEFAULT_EVAL_MODEL_ID)
5916
+ summaryLines.push(["Ignored Model", requestedModel]);
5099
5917
  if (opts.model) summaryLines.push(["Model", opts.model]);
5100
- if (categories && categories.length) summaryLines.push(["Categories", categories.join(", ")]);
5918
+ if (categories && categories.length)
5919
+ summaryLines.push(["Categories", categories.join(", ")]);
5101
5920
  if (testCaseIds && testCaseIds.length)
5102
5921
  summaryLines.push(["Test cases", `${testCaseIds.length} picked`]);
5103
5922
  if (opts.all && !categories?.length && !testCaseIds?.length)
@@ -5114,8 +5933,6 @@ async function runEvalRun(opts) {
5114
5933
  const mutRes = await gqlMutate(ctx.client, StartEvalRunDoc, {
5115
5934
  tenantId: ctx.tenantId,
5116
5935
  input: {
5117
- agentTemplateId,
5118
- agentId: opts.agent ?? null,
5119
5936
  model: opts.model ?? null,
5120
5937
  categories: categories ?? null,
5121
5938
  testCaseIds: testCaseIds ?? null
@@ -5123,7 +5940,12 @@ async function runEvalRun(opts) {
5123
5940
  });
5124
5941
  const run2 = mutRes.startEvalRun;
5125
5942
  if (isJsonMode()) {
5126
- printJson({ runId: run2.id, status: run2.status, model: run2.model, categories: run2.categories });
5943
+ printJson({
5944
+ runId: run2.id,
5945
+ status: run2.status,
5946
+ model: run2.model,
5947
+ categories: run2.categories
5948
+ });
5127
5949
  } else {
5128
5950
  printSuccess(`Started eval run ${run2.id} (status: ${run2.status}).`);
5129
5951
  }
@@ -5147,8 +5969,12 @@ async function pollUntilTerminal(client, runId, intervalSec, timeoutSec) {
5147
5969
  }
5148
5970
  if (isTerminalStatus(run2.status)) {
5149
5971
  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"}`);
5972
+ if (run2.status === "completed")
5973
+ spinner.succeed(
5974
+ `completed \u2014 ${run2.passed}/${run2.totalTests} (${fmtPercent(run2.passRate)})`
5975
+ );
5976
+ else if (run2.status === "failed")
5977
+ spinner.fail(`failed \u2014 ${run2.errorMessage ?? "unknown error"}`);
5152
5978
  else spinner.warn("cancelled");
5153
5979
  }
5154
5980
  if (isJsonMode()) {
@@ -5466,7 +6292,7 @@ async function runEvalTestCaseGet(id, opts) {
5466
6292
 
5467
6293
  // src/commands/eval/test-case/create.ts
5468
6294
  import { readFileSync as readFileSync6 } from "fs";
5469
- import { input as input4, select as select9, checkbox as checkbox2 } from "@inquirer/prompts";
6295
+ import { input as input3, select as select8, checkbox as checkbox2 } from "@inquirer/prompts";
5470
6296
  var DEFAULT_EVALUATORS = [
5471
6297
  "Builtin.Helpfulness",
5472
6298
  "Builtin.Correctness",
@@ -5496,17 +6322,17 @@ async function runEvalTestCaseCreate(opts) {
5496
6322
  if (!name) {
5497
6323
  requireTty("Name");
5498
6324
  name = await promptOrExit(
5499
- () => input4({ message: "Test case name?", validate: (v) => v.trim().length > 0 || "Required" })
6325
+ () => input3({ message: "Test case name?", validate: (v) => v.trim().length > 0 || "Required" })
5500
6326
  );
5501
6327
  }
5502
6328
  if (!category) {
5503
6329
  category = await promptOrExit(
5504
- () => input4({ message: "Category (free-form label)?", validate: (v) => v.trim().length > 0 || "Required" })
6330
+ () => input3({ message: "Category (free-form label)?", validate: (v) => v.trim().length > 0 || "Required" })
5505
6331
  );
5506
6332
  }
5507
6333
  if (!query) {
5508
6334
  query = await promptOrExit(
5509
- () => input4({ message: "Query the agent under test will receive?", validate: (v) => v.trim().length > 0 || "Required" })
6335
+ () => input3({ message: "Query the agent under test will receive?", validate: (v) => v.trim().length > 0 || "Required" })
5510
6336
  );
5511
6337
  }
5512
6338
  if (interactive && agentTemplateId === null) {
@@ -5514,7 +6340,7 @@ async function runEvalTestCaseCreate(opts) {
5514
6340
  const templates = tpls.agentTemplates ?? [];
5515
6341
  if (templates.length > 0) {
5516
6342
  const choice = await promptOrExit(
5517
- () => select9({
6343
+ () => select8({
5518
6344
  message: "Pin to an agent template? (Enter for none)",
5519
6345
  choices: [
5520
6346
  { name: "\u2014 none \u2014 (runner picks)", value: "" },
@@ -5572,28 +6398,28 @@ async function runEvalTestCaseCreate(opts) {
5572
6398
  import { readFileSync as readFileSync7 } from "fs";
5573
6399
  async function runEvalTestCaseUpdate(id, opts) {
5574
6400
  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;
6401
+ const input4 = {};
6402
+ if (opts.name !== void 0) input4.name = opts.name;
6403
+ if (opts.category !== void 0) input4.category = opts.category;
6404
+ if (opts.query !== void 0) input4.query = opts.query;
6405
+ if (opts.systemPrompt !== void 0) input4.systemPrompt = opts.systemPrompt;
6406
+ if (opts.agentTemplate !== void 0) input4.agentTemplateId = opts.agentTemplate;
6407
+ if (opts.evaluator !== void 0) input4.agentcoreEvaluatorIds = opts.evaluator;
6408
+ if (opts.tag !== void 0) input4.tags = opts.tag;
6409
+ if (opts.enabled !== void 0) input4.enabled = opts.enabled;
5584
6410
  if (opts.assertionsFile) {
5585
6411
  const parsed = JSON.parse(readFileSync7(opts.assertionsFile, "utf8"));
5586
6412
  if (!Array.isArray(parsed)) {
5587
6413
  printError(`--assertions-file must contain a JSON array.`);
5588
6414
  process.exit(1);
5589
6415
  }
5590
- input5.assertions = parsed;
6416
+ input4.assertions = parsed;
5591
6417
  }
5592
- if (Object.keys(input5).length === 0) {
6418
+ if (Object.keys(input4).length === 0) {
5593
6419
  printError("No fields to update. Pass at least one --<field>.");
5594
6420
  process.exit(1);
5595
6421
  }
5596
- const res = await gqlMutate(ctx.client, UpdateEvalTestCaseDoc, { id, input: input5 });
6422
+ const res = await gqlMutate(ctx.client, UpdateEvalTestCaseDoc, { id, input: input4 });
5597
6423
  if (isJsonMode()) {
5598
6424
  printJson(res.updateEvalTestCase);
5599
6425
  return;
@@ -5631,38 +6457,84 @@ async function runEvalTestCaseDelete(id, opts) {
5631
6457
  // src/commands/eval.ts
5632
6458
  function registerEvalCommand(program2) {
5633
6459
  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
- );
6460
+ "Run evaluations against the default AgentCore agent template and manage eval test cases. Integrates with the Evaluations Studio in the admin UI."
6461
+ ).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option(
6462
+ "--computer <id>",
6463
+ "Deprecated; evals now run directly against AgentCore"
6464
+ ).option("--model <id>", "Deprecated; evals always use Kimi K2.5").option("--category <name...>", "Only run these categories (repeatable)").option(
6465
+ "--test-case <id...>",
6466
+ "Only run these specific test case IDs (repeatable)"
6467
+ ).option("--all", "Run all enabled test cases for the tenant").option("--watch", "Block and poll until the run reaches a terminal status").option(
6468
+ "--timeout <seconds>",
6469
+ "Max wait seconds for --watch (default 900)",
6470
+ "900"
6471
+ ).addHelpText(
6472
+ "after",
6473
+ `
6474
+ Default action:
6475
+ $ thinkwork evals
6476
+
6477
+ Equivalent explicit action:
6478
+ $ thinkwork eval run
6479
+ `
6480
+ ).action(runEvalRun);
5636
6481
  evals.command("run").description(
5637
6482
  "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(
6483
+ ).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option(
6484
+ "--computer <id>",
6485
+ "Deprecated; evals now run directly against AgentCore"
6486
+ ).option("--model <id>", "Deprecated; evals always use Kimi K2.5").option("--category <name...>", "Only run these categories (repeatable)").option(
6487
+ "--test-case <id...>",
6488
+ "Only run these specific test case IDs (repeatable)"
6489
+ ).option("--all", "Run all enabled test cases for the tenant").option("--watch", "Block and poll until the run reaches a terminal status").option(
6490
+ "--timeout <seconds>",
6491
+ "Max wait seconds for --watch (default 900)",
6492
+ "900"
6493
+ ).addHelpText(
5639
6494
  "after",
5640
6495
  `
5641
6496
  Examples:
5642
6497
  # Fire and return \u2014 prints the runId; view results in the admin UI
5643
- $ thinkwork eval run --agent-template tpl-abc --category tool-safety
6498
+ $ thinkwork eval run --category red-team-safety-scope
5644
6499
 
5645
- # Pick categories + test cases interactively
6500
+ # Pick categories interactively
5646
6501
  $ thinkwork eval run
5647
6502
 
5648
6503
  # Block until done
5649
- $ thinkwork eval run --agent-template tpl-abc --all --watch --timeout 1800
6504
+ $ thinkwork eval run --all --watch --timeout 1800
5650
6505
  `
5651
6506
  ).action(runEvalRun);
5652
6507
  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);
6508
+ 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(
6509
+ "--results",
6510
+ "Also fetch per-test-case results (default: true)",
6511
+ true
6512
+ ).option("--no-results", "Skip fetching per-test-case results").action(runEvalGet);
5654
6513
  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
6514
  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);
6515
+ evals.command("delete <runId>").description(
6516
+ "Delete an eval run and its results. Requires confirmation unless --yes."
6517
+ ).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);
6518
+ evals.command("categories").description(
6519
+ "List distinct categories present across the tenant's test cases."
6520
+ ).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").action(runEvalCategories);
5658
6521
  evals.command("seed").description(
5659
- "Idempotently seed the maniflow starter pack (96 test cases across 9 categories). Safe to re-run."
6522
+ "Idempotently seed the ThinkWork RedTeam starter pack (189 test cases across 4 categories). Safe to re-run."
5660
6523
  ).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
6524
  const tc = evals.command("test-case").alias("test-cases").description("Manage individual eval test cases (CRUD).");
5662
6525
  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
6526
  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);
6527
+ 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(
6528
+ "--assertions-file <path>",
6529
+ "JSON file containing an array of assertions"
6530
+ ).action(runEvalTestCaseCreate);
6531
+ 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(
6532
+ "--evaluator <id...>",
6533
+ "Replace AgentCore evaluator IDs (repeatable)"
6534
+ ).option("--tag <name...>", "Replace tags (repeatable)").option("--enabled", "Mark enabled").option("--no-enabled", "Mark disabled").option(
6535
+ "--assertions-file <path>",
6536
+ "JSON file containing an array of assertions (replaces all)"
6537
+ ).action(runEvalTestCaseUpdate);
5666
6538
  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
6539
  }
5668
6540
 
@@ -5735,7 +6607,7 @@ var WikiCompileJobsDoc = graphql(`
5735
6607
  `);
5736
6608
 
5737
6609
  // src/commands/wiki/helpers.ts
5738
- import { select as select10 } from "@inquirer/prompts";
6610
+ import { select as select9 } from "@inquirer/prompts";
5739
6611
  async function resolveWikiContext(opts) {
5740
6612
  const region = opts.region ?? "us-east-1";
5741
6613
  const stage = await resolveStage({ flag: opts.stage, region });
@@ -5875,7 +6747,7 @@ async function resolveAgentScope(ctx, opts, config = {}) {
5875
6747
  choices.push({ name: `${label}${slugPart} [${a.id}]`, value: a.id });
5876
6748
  }
5877
6749
  const pick = await promptOrExit(
5878
- () => select10({
6750
+ () => select9({
5879
6751
  message: "Which agent?",
5880
6752
  choices,
5881
6753
  loop: false
@@ -6571,6 +7443,7 @@ registerMessageCommand(program);
6571
7443
  registerLabelCommand(program);
6572
7444
  registerInboxCommand(program);
6573
7445
  registerAgentCommand(program);
7446
+ registerComputerCommand(program);
6574
7447
  registerTemplateCommand(program);
6575
7448
  registerTenantCommand(program);
6576
7449
  registerMemberCommand(program);
@@ -6581,7 +7454,6 @@ registerScheduledJobCommand(program);
6581
7454
  registerTurnCommand(program);
6582
7455
  registerWakeupCommand(program);
6583
7456
  registerWebhookCommand(program);
6584
- registerConnectorCommand(program);
6585
7457
  registerSkillCommand(program);
6586
7458
  registerMemoryCommand(program);
6587
7459
  registerRecipeCommand(program);