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