thinkwork-cli 0.8.2 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +18 -2
- package/dist/cli.js +3004 -215
- 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 +204 -4
- 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 +1557 -42
- package/dist/terraform/modules/app/lambda-api/main.tf +299 -15
- 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 +5 -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 +165 -6
- package/dist/terraform/schema.graphql +45 -0
- package/package.json +15 -14
package/dist/cli.js
CHANGED
|
@@ -68,6 +68,32 @@ function printKeyValue(pairs) {
|
|
|
68
68
|
console.log(` ${label}${v ?? chalk.dim("\u2014")}`);
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
|
+
function printTable(rows, columns) {
|
|
72
|
+
if (jsonMode) return;
|
|
73
|
+
if (rows.length === 0) {
|
|
74
|
+
console.log(chalk.dim(" (no results)"));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const widths = columns.map((c) => {
|
|
78
|
+
const header2 = c.header.length;
|
|
79
|
+
const maxRow = Math.max(
|
|
80
|
+
...rows.map((r) => (c.format ? c.format(r[c.key]) : String(r[c.key] ?? "")).length)
|
|
81
|
+
);
|
|
82
|
+
return Math.max(header2, maxRow);
|
|
83
|
+
});
|
|
84
|
+
const header = columns.map((c, i) => chalk.bold(c.header.padEnd(widths[i]))).join(" ");
|
|
85
|
+
console.log(` ${header}`);
|
|
86
|
+
console.log(
|
|
87
|
+
" " + widths.map((w) => chalk.dim("\u2500".repeat(w))).join(" ")
|
|
88
|
+
);
|
|
89
|
+
for (const row of rows) {
|
|
90
|
+
const line = columns.map((c, i) => {
|
|
91
|
+
const v = c.format ? c.format(row[c.key]) : String(row[c.key] ?? "");
|
|
92
|
+
return v.padEnd(widths[i]);
|
|
93
|
+
}).join(" ");
|
|
94
|
+
console.log(` ${line}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
71
97
|
function logStderr(message) {
|
|
72
98
|
process.stderr.write(message + "\n");
|
|
73
99
|
}
|
|
@@ -170,7 +196,7 @@ async function ensureWorkspace(cwd, stage) {
|
|
|
170
196
|
}
|
|
171
197
|
}
|
|
172
198
|
function runTerraformRaw(cwd, args) {
|
|
173
|
-
return new Promise((
|
|
199
|
+
return new Promise((resolve4, reject) => {
|
|
174
200
|
const proc = spawn("terraform", args, {
|
|
175
201
|
cwd,
|
|
176
202
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -180,13 +206,13 @@ function runTerraformRaw(cwd, args) {
|
|
|
180
206
|
proc.stdout.on("data", (d) => stdout += d);
|
|
181
207
|
proc.stderr.on("data", (d) => stderr += d);
|
|
182
208
|
proc.on("close", (code) => {
|
|
183
|
-
if (code === 0)
|
|
209
|
+
if (code === 0) resolve4(stdout);
|
|
184
210
|
else reject(new Error(`terraform ${args.join(" ")} failed (exit ${code}): ${stderr}`));
|
|
185
211
|
});
|
|
186
212
|
});
|
|
187
213
|
}
|
|
188
214
|
function runTerraform(cwd, args) {
|
|
189
|
-
return new Promise((
|
|
215
|
+
return new Promise((resolve4) => {
|
|
190
216
|
console.log(`
|
|
191
217
|
\u2192 terraform ${args.join(" ")}
|
|
192
218
|
`);
|
|
@@ -194,7 +220,7 @@ function runTerraform(cwd, args) {
|
|
|
194
220
|
cwd,
|
|
195
221
|
stdio: "inherit"
|
|
196
222
|
});
|
|
197
|
-
proc.on("close", (code) =>
|
|
223
|
+
proc.on("close", (code) => resolve4(code ?? 1));
|
|
198
224
|
});
|
|
199
225
|
}
|
|
200
226
|
async function ensureInit(cwd) {
|
|
@@ -312,6 +338,18 @@ function requireTty(label) {
|
|
|
312
338
|
process.exit(1);
|
|
313
339
|
}
|
|
314
340
|
}
|
|
341
|
+
async function promptOrExit(fn) {
|
|
342
|
+
try {
|
|
343
|
+
return await fn();
|
|
344
|
+
} catch (err) {
|
|
345
|
+
if (isCancellation(err)) {
|
|
346
|
+
console.log("");
|
|
347
|
+
console.log(" Cancelled.");
|
|
348
|
+
process.exit(0);
|
|
349
|
+
}
|
|
350
|
+
throw err;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
315
353
|
|
|
316
354
|
// src/lib/resolve-stage.ts
|
|
317
355
|
async function resolveStage(opts = {}) {
|
|
@@ -389,6 +427,12 @@ function registerPlanCommand(program2) {
|
|
|
389
427
|
});
|
|
390
428
|
}
|
|
391
429
|
|
|
430
|
+
// src/commands/deploy.ts
|
|
431
|
+
import { spawn as spawn2 } from "child_process";
|
|
432
|
+
import { existsSync as existsSync3 } from "fs";
|
|
433
|
+
import { dirname as dirname2, resolve as pathResolve } from "path";
|
|
434
|
+
import { fileURLToPath } from "url";
|
|
435
|
+
|
|
392
436
|
// src/prompt.ts
|
|
393
437
|
import { createInterface } from "readline";
|
|
394
438
|
async function confirm(message) {
|
|
@@ -396,69 +440,125 @@ async function confirm(message) {
|
|
|
396
440
|
input: process.stdin,
|
|
397
441
|
output: process.stdout
|
|
398
442
|
});
|
|
399
|
-
return new Promise((
|
|
443
|
+
return new Promise((resolve4) => {
|
|
400
444
|
rl.question(`${message} [y/N] `, (answer) => {
|
|
401
445
|
rl.close();
|
|
402
|
-
|
|
446
|
+
resolve4(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
403
447
|
});
|
|
404
448
|
});
|
|
405
449
|
}
|
|
406
450
|
|
|
407
451
|
// src/commands/deploy.ts
|
|
408
452
|
function registerDeployCommand(program2) {
|
|
409
|
-
program2.command("deploy").description(
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
const ok = await confirm(` Stage "${stage}" is production-like. Deploy?`);
|
|
425
|
-
if (!ok) {
|
|
426
|
-
console.log(" Aborted.");
|
|
427
|
-
process.exit(0);
|
|
453
|
+
program2.command("deploy").description(
|
|
454
|
+
"Run terraform apply for a stage. Prompts for stage in a TTY when omitted."
|
|
455
|
+
).option("-p, --profile <name>", "AWS profile").option("-s, --stage <name>", "Deployment stage").option(
|
|
456
|
+
"-c, --component <tier>",
|
|
457
|
+
"Component tier (foundation|data|app|all)",
|
|
458
|
+
"all"
|
|
459
|
+
).option("-y, --yes", "Skip interactive confirmation (for CI)").action(
|
|
460
|
+
async (opts) => {
|
|
461
|
+
const startTime = Date.now();
|
|
462
|
+
try {
|
|
463
|
+
const stage = await resolveStage({ flag: opts.stage });
|
|
464
|
+
const compCheck = validateComponent(opts.component);
|
|
465
|
+
if (!compCheck.valid) {
|
|
466
|
+
printError(compCheck.error);
|
|
467
|
+
process.exit(1);
|
|
428
468
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
if (!
|
|
432
|
-
|
|
433
|
-
|
|
469
|
+
const identity = getAwsIdentity();
|
|
470
|
+
printHeader("deploy", stage, identity);
|
|
471
|
+
if (!identity) {
|
|
472
|
+
printWarning(
|
|
473
|
+
"Could not resolve AWS identity. Is the AWS CLI configured?"
|
|
474
|
+
);
|
|
434
475
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
476
|
+
if (isProdLike(stage) && !opts.yes) {
|
|
477
|
+
const ok = await confirm(
|
|
478
|
+
` Stage "${stage}" is production-like. Deploy?`
|
|
479
|
+
);
|
|
480
|
+
if (!ok) {
|
|
481
|
+
console.log(" Aborted.");
|
|
482
|
+
process.exit(0);
|
|
483
|
+
}
|
|
484
|
+
} else if (!opts.yes) {
|
|
485
|
+
const ok = await confirm(` Deploy to stage "${stage}"?`);
|
|
486
|
+
if (!ok) {
|
|
487
|
+
console.log(" Aborted.");
|
|
488
|
+
process.exit(0);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
|
|
492
|
+
const tiers = expandComponent(opts.component);
|
|
493
|
+
for (let i = 0; i < tiers.length; i++) {
|
|
494
|
+
const tier = tiers[i];
|
|
495
|
+
printTierHeader(tier, i, tiers.length);
|
|
496
|
+
const cwd = resolveTierDir(terraformDir, stage, tier);
|
|
497
|
+
await ensureInit(cwd);
|
|
498
|
+
await ensureWorkspace(cwd, stage);
|
|
499
|
+
const code = await runTerraform(cwd, [
|
|
500
|
+
"apply",
|
|
501
|
+
"-auto-approve",
|
|
502
|
+
`-var=stage=${stage}`
|
|
503
|
+
]);
|
|
504
|
+
if (code !== 0) {
|
|
505
|
+
printError(`Deploy failed for ${tier} (exit ${code})`);
|
|
506
|
+
process.exit(code);
|
|
507
|
+
}
|
|
452
508
|
}
|
|
509
|
+
printSuccess("Deploy complete");
|
|
510
|
+
await runPostDeployProbe(stage);
|
|
511
|
+
printSummary("deploy", stage, tiers, startTime);
|
|
512
|
+
} catch (err) {
|
|
513
|
+
if (isCancellation(err)) return;
|
|
514
|
+
throw err;
|
|
453
515
|
}
|
|
454
|
-
printSuccess("Deploy complete");
|
|
455
|
-
printSummary("deploy", stage, tiers, startTime);
|
|
456
|
-
} catch (err) {
|
|
457
|
-
if (isCancellation(err)) return;
|
|
458
|
-
throw err;
|
|
459
516
|
}
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
async function runPostDeployProbe(stage) {
|
|
520
|
+
const scriptPath = locatePostDeployScript();
|
|
521
|
+
if (!scriptPath) {
|
|
522
|
+
printWarning(
|
|
523
|
+
"post-deploy probe script not found \u2014 skipping AgentCore drift check"
|
|
524
|
+
);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
await new Promise((resolve4) => {
|
|
528
|
+
const proc = spawn2("bash", [scriptPath, "--stage", stage], {
|
|
529
|
+
stdio: "inherit",
|
|
530
|
+
env: process.env
|
|
531
|
+
});
|
|
532
|
+
proc.on("close", (code) => {
|
|
533
|
+
if (code !== 0) {
|
|
534
|
+
printWarning(
|
|
535
|
+
`post-deploy probe exited ${code} \u2014 deploy not rolled back`
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
resolve4();
|
|
539
|
+
});
|
|
540
|
+
proc.on("error", (err) => {
|
|
541
|
+
printWarning(`post-deploy probe spawn failed: ${err.message}`);
|
|
542
|
+
resolve4();
|
|
543
|
+
});
|
|
460
544
|
});
|
|
461
545
|
}
|
|
546
|
+
function locatePostDeployScript() {
|
|
547
|
+
const here = dirname2(fileURLToPath(import.meta.url));
|
|
548
|
+
const candidates = [
|
|
549
|
+
pathResolve(here, "..", "..", "..", "..", "scripts", "post-deploy.sh"),
|
|
550
|
+
pathResolve(process.cwd(), "scripts", "post-deploy.sh"),
|
|
551
|
+
pathResolve(
|
|
552
|
+
process.env.THINKWORK_TERRAFORM_DIR || ".",
|
|
553
|
+
"scripts",
|
|
554
|
+
"post-deploy.sh"
|
|
555
|
+
)
|
|
556
|
+
];
|
|
557
|
+
for (const candidate of candidates) {
|
|
558
|
+
if (existsSync3(candidate)) return candidate;
|
|
559
|
+
}
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
462
562
|
|
|
463
563
|
// src/commands/destroy.ts
|
|
464
564
|
function registerDestroyCommand(program2) {
|
|
@@ -649,17 +749,17 @@ function registerOutputsCommand(program2) {
|
|
|
649
749
|
}
|
|
650
750
|
|
|
651
751
|
// src/commands/config.ts
|
|
652
|
-
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as
|
|
752
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync5 } from "fs";
|
|
653
753
|
import chalk4 from "chalk";
|
|
654
754
|
|
|
655
755
|
// src/environments.ts
|
|
656
|
-
import { existsSync as
|
|
756
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync2, readdirSync } from "fs";
|
|
657
757
|
import { join as join2 } from "path";
|
|
658
758
|
import { homedir as homedir2 } from "os";
|
|
659
759
|
var THINKWORK_HOME = join2(homedir2(), ".thinkwork");
|
|
660
760
|
var ENVIRONMENTS_DIR = join2(THINKWORK_HOME, "environments");
|
|
661
761
|
function ensureDir(dir) {
|
|
662
|
-
if (!
|
|
762
|
+
if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
|
|
663
763
|
}
|
|
664
764
|
function saveEnvironment(config) {
|
|
665
765
|
ensureDir(ENVIRONMENTS_DIR);
|
|
@@ -672,13 +772,13 @@ function saveEnvironment(config) {
|
|
|
672
772
|
}
|
|
673
773
|
function loadEnvironment(stage) {
|
|
674
774
|
const configPath = join2(ENVIRONMENTS_DIR, stage, "config.json");
|
|
675
|
-
if (!
|
|
775
|
+
if (!existsSync4(configPath)) return null;
|
|
676
776
|
return JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
677
777
|
}
|
|
678
778
|
function listEnvironments() {
|
|
679
|
-
if (!
|
|
779
|
+
if (!existsSync4(ENVIRONMENTS_DIR)) return [];
|
|
680
780
|
return readdirSync(ENVIRONMENTS_DIR).filter((name) => {
|
|
681
|
-
return
|
|
781
|
+
return existsSync4(join2(ENVIRONMENTS_DIR, name, "config.json"));
|
|
682
782
|
}).map((name) => {
|
|
683
783
|
return JSON.parse(
|
|
684
784
|
readFileSync2(join2(ENVIRONMENTS_DIR, name, "config.json"), "utf-8")
|
|
@@ -687,19 +787,19 @@ function listEnvironments() {
|
|
|
687
787
|
}
|
|
688
788
|
function resolveTerraformDir(stage) {
|
|
689
789
|
const env = loadEnvironment(stage);
|
|
690
|
-
if (env?.terraformDir &&
|
|
790
|
+
if (env?.terraformDir && existsSync4(env.terraformDir)) {
|
|
691
791
|
return env.terraformDir;
|
|
692
792
|
}
|
|
693
793
|
const envVar = process.env.THINKWORK_TERRAFORM_DIR;
|
|
694
|
-
if (envVar &&
|
|
794
|
+
if (envVar && existsSync4(envVar)) return envVar;
|
|
695
795
|
const cwdTf = join2(process.cwd(), "terraform");
|
|
696
|
-
if (
|
|
796
|
+
if (existsSync4(join2(cwdTf, "main.tf"))) return cwdTf;
|
|
697
797
|
return null;
|
|
698
798
|
}
|
|
699
799
|
|
|
700
800
|
// src/commands/config.ts
|
|
701
801
|
function readTfVar(tfvarsPath, key) {
|
|
702
|
-
if (!
|
|
802
|
+
if (!existsSync5(tfvarsPath)) return null;
|
|
703
803
|
const content = readFileSync3(tfvarsPath, "utf-8");
|
|
704
804
|
const quoted = content.match(new RegExp(`^${key}\\s*=\\s*"([^"]*)"`, "m"));
|
|
705
805
|
if (quoted) return quoted[1];
|
|
@@ -708,7 +808,7 @@ function readTfVar(tfvarsPath, key) {
|
|
|
708
808
|
}
|
|
709
809
|
var BARE_KEYS = /* @__PURE__ */ new Set(["enable_hindsight"]);
|
|
710
810
|
function setTfVar(tfvarsPath, key, value) {
|
|
711
|
-
if (!
|
|
811
|
+
if (!existsSync5(tfvarsPath)) {
|
|
712
812
|
throw new Error(`terraform.tfvars not found at ${tfvarsPath}`);
|
|
713
813
|
}
|
|
714
814
|
let content = readFileSync3(tfvarsPath, "utf-8");
|
|
@@ -728,7 +828,7 @@ function resolveTfvarsPath(stage) {
|
|
|
728
828
|
const tfDir = resolveTerraformDir(stage);
|
|
729
829
|
if (tfDir) {
|
|
730
830
|
const direct = `${tfDir}/terraform.tfvars`;
|
|
731
|
-
if (
|
|
831
|
+
if (existsSync5(direct)) return direct;
|
|
732
832
|
}
|
|
733
833
|
const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
|
|
734
834
|
const cwd = resolveTierDir(terraformDir, stage, "app");
|
|
@@ -755,7 +855,7 @@ function registerConfigCommand(program2) {
|
|
|
755
855
|
console.log(` ${chalk4.bold("Updated:")} ${env.updatedAt}`);
|
|
756
856
|
console.log(chalk4.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
757
857
|
const tfvarsPath = `${env.terraformDir}/terraform.tfvars`;
|
|
758
|
-
if (
|
|
858
|
+
if (existsSync5(tfvarsPath)) {
|
|
759
859
|
console.log("");
|
|
760
860
|
console.log(chalk4.dim(" terraform.tfvars:"));
|
|
761
861
|
const content = readFileSync3(tfvarsPath, "utf-8");
|
|
@@ -876,28 +976,28 @@ function registerConfigCommand(program2) {
|
|
|
876
976
|
}
|
|
877
977
|
|
|
878
978
|
// src/commands/bootstrap.ts
|
|
879
|
-
import { spawn as
|
|
979
|
+
import { spawn as spawn3 } from "child_process";
|
|
880
980
|
import { resolve } from "path";
|
|
881
981
|
function getTerraformOutput(cwd, key) {
|
|
882
|
-
return new Promise((
|
|
883
|
-
const proc =
|
|
982
|
+
return new Promise((resolve4, reject) => {
|
|
983
|
+
const proc = spawn3("terraform", ["output", "-raw", key], {
|
|
884
984
|
cwd,
|
|
885
985
|
stdio: ["pipe", "pipe", "pipe"]
|
|
886
986
|
});
|
|
887
987
|
let stdout = "";
|
|
888
988
|
proc.stdout.on("data", (d) => stdout += d);
|
|
889
989
|
proc.on("close", (code) => {
|
|
890
|
-
if (code === 0)
|
|
990
|
+
if (code === 0) resolve4(stdout.trim());
|
|
891
991
|
else reject(new Error(`terraform output ${key} failed (exit ${code})`));
|
|
892
992
|
});
|
|
893
993
|
});
|
|
894
994
|
}
|
|
895
995
|
function runScript(scriptPath, args) {
|
|
896
|
-
return new Promise((
|
|
897
|
-
const proc =
|
|
996
|
+
return new Promise((resolve4) => {
|
|
997
|
+
const proc = spawn3("bash", [scriptPath, ...args], {
|
|
898
998
|
stdio: "inherit"
|
|
899
999
|
});
|
|
900
|
-
proc.on("close", (code) =>
|
|
1000
|
+
proc.on("close", (code) => resolve4(code ?? 1));
|
|
901
1001
|
});
|
|
902
1002
|
}
|
|
903
1003
|
function registerBootstrapCommand(program2) {
|
|
@@ -952,7 +1052,7 @@ import { select as select2, Separator } from "@inquirer/prompts";
|
|
|
952
1052
|
import chalk7 from "chalk";
|
|
953
1053
|
|
|
954
1054
|
// src/aws-profiles.ts
|
|
955
|
-
import { existsSync as
|
|
1055
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
|
|
956
1056
|
import { homedir as homedir3 } from "os";
|
|
957
1057
|
import { join as join3 } from "path";
|
|
958
1058
|
var CREDENTIALS_PATH = join3(homedir3(), ".aws", "credentials");
|
|
@@ -995,7 +1095,7 @@ function classify(fields) {
|
|
|
995
1095
|
}
|
|
996
1096
|
function listAwsProfiles() {
|
|
997
1097
|
const byName = /* @__PURE__ */ new Map();
|
|
998
|
-
if (
|
|
1098
|
+
if (existsSync6(CREDENTIALS_PATH)) {
|
|
999
1099
|
const sections = parseIni(readFileSync4(CREDENTIALS_PATH, "utf-8"));
|
|
1000
1100
|
for (const [section, fields] of Object.entries(sections)) {
|
|
1001
1101
|
byName.set(section, {
|
|
@@ -1005,7 +1105,7 @@ function listAwsProfiles() {
|
|
|
1005
1105
|
});
|
|
1006
1106
|
}
|
|
1007
1107
|
}
|
|
1008
|
-
if (
|
|
1108
|
+
if (existsSync6(CONFIG_PATH)) {
|
|
1009
1109
|
const sections = parseIni(readFileSync4(CONFIG_PATH, "utf-8"));
|
|
1010
1110
|
for (const [section, fields] of Object.entries(sections)) {
|
|
1011
1111
|
const name = normalizeConfigSection(section);
|
|
@@ -1233,7 +1333,7 @@ function discoverCognitoConfig(stage, region) {
|
|
|
1233
1333
|
// src/cognito-oauth.ts
|
|
1234
1334
|
import { createServer } from "http";
|
|
1235
1335
|
import { randomBytes } from "crypto";
|
|
1236
|
-
import { spawn as
|
|
1336
|
+
import { spawn as spawn4 } from "child_process";
|
|
1237
1337
|
import chalk6 from "chalk";
|
|
1238
1338
|
var CLI_LOOPBACK_PORT = 42010;
|
|
1239
1339
|
var CALLBACK_PATH = "/callback";
|
|
@@ -1272,7 +1372,7 @@ function buildAuthorizeUrl(cognito, redirectUri, state) {
|
|
|
1272
1372
|
return `${cognito.domainUrl}/oauth2/authorize?${params.toString()}`;
|
|
1273
1373
|
}
|
|
1274
1374
|
function waitForCallbackCode(opts) {
|
|
1275
|
-
return new Promise((
|
|
1375
|
+
return new Promise((resolve4, reject) => {
|
|
1276
1376
|
const server = createServer((req, res) => handleRequest(req, res));
|
|
1277
1377
|
let finished = false;
|
|
1278
1378
|
const finish = (err, code) => {
|
|
@@ -1283,7 +1383,7 @@ function waitForCallbackCode(opts) {
|
|
|
1283
1383
|
closer.closeAllConnections?.();
|
|
1284
1384
|
server.close(() => {
|
|
1285
1385
|
if (err) reject(err);
|
|
1286
|
-
else
|
|
1386
|
+
else resolve4(code);
|
|
1287
1387
|
});
|
|
1288
1388
|
};
|
|
1289
1389
|
const timer = setTimeout(() => {
|
|
@@ -1412,7 +1512,7 @@ function openInBrowser(url) {
|
|
|
1412
1512
|
const cmd = platform2 === "darwin" ? "open" : platform2 === "win32" ? "cmd" : "xdg-open";
|
|
1413
1513
|
const args = platform2 === "win32" ? ["/c", "start", "", url] : [url];
|
|
1414
1514
|
try {
|
|
1415
|
-
|
|
1515
|
+
spawn4(cmd, args, { stdio: "ignore", detached: true }).unref();
|
|
1416
1516
|
} catch {
|
|
1417
1517
|
}
|
|
1418
1518
|
}
|
|
@@ -1474,10 +1574,10 @@ function escapeHtml(s) {
|
|
|
1474
1574
|
// src/commands/login.ts
|
|
1475
1575
|
function ask(prompt) {
|
|
1476
1576
|
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
1477
|
-
return new Promise((
|
|
1577
|
+
return new Promise((resolve4) => {
|
|
1478
1578
|
rl.question(prompt, (answer) => {
|
|
1479
1579
|
rl.close();
|
|
1480
|
-
|
|
1580
|
+
resolve4(answer.trim());
|
|
1481
1581
|
});
|
|
1482
1582
|
});
|
|
1483
1583
|
}
|
|
@@ -1956,20 +2056,20 @@ Examples:
|
|
|
1956
2056
|
}
|
|
1957
2057
|
|
|
1958
2058
|
// src/commands/init.ts
|
|
1959
|
-
import { existsSync as
|
|
1960
|
-
import { resolve as resolve2, join as join5, dirname as
|
|
2059
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, cpSync } from "fs";
|
|
2060
|
+
import { resolve as resolve2, join as join5, dirname as dirname3 } from "path";
|
|
1961
2061
|
import { execSync as execSync7 } from "child_process";
|
|
1962
|
-
import { fileURLToPath } from "url";
|
|
2062
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1963
2063
|
import { createInterface as createInterface3 } from "readline";
|
|
1964
2064
|
import chalk8 from "chalk";
|
|
1965
|
-
var __dirname =
|
|
2065
|
+
var __dirname = dirname3(fileURLToPath2(import.meta.url));
|
|
1966
2066
|
function ask2(prompt, defaultVal = "") {
|
|
1967
2067
|
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
1968
2068
|
const suffix = defaultVal ? chalk8.dim(` [${defaultVal}]`) : "";
|
|
1969
|
-
return new Promise((
|
|
2069
|
+
return new Promise((resolve4) => {
|
|
1970
2070
|
rl.question(` ${prompt}${suffix}: `, (answer) => {
|
|
1971
2071
|
rl.close();
|
|
1972
|
-
|
|
2072
|
+
resolve4(answer.trim() || defaultVal);
|
|
1973
2073
|
});
|
|
1974
2074
|
});
|
|
1975
2075
|
}
|
|
@@ -1987,9 +2087,9 @@ function generateSecret(length = 32) {
|
|
|
1987
2087
|
}
|
|
1988
2088
|
function findBundledTerraform() {
|
|
1989
2089
|
const bundled = resolve2(__dirname, "terraform");
|
|
1990
|
-
if (
|
|
2090
|
+
if (existsSync8(join5(bundled, "modules"))) return bundled;
|
|
1991
2091
|
const repoTf = resolve2(__dirname, "..", "..", "..", "terraform");
|
|
1992
|
-
if (
|
|
2092
|
+
if (existsSync8(join5(repoTf, "modules"))) return repoTf;
|
|
1993
2093
|
throw new Error(
|
|
1994
2094
|
"Terraform modules not found. The CLI package may be incomplete.\nTry reinstalling: npm install -g thinkwork-cli@latest"
|
|
1995
2095
|
);
|
|
@@ -2049,9 +2149,9 @@ function registerInitCommand(program2) {
|
|
|
2049
2149
|
printError("Stage name is required. Pass -s <name> or re-run in an interactive terminal.");
|
|
2050
2150
|
process.exit(1);
|
|
2051
2151
|
}
|
|
2052
|
-
const { input:
|
|
2152
|
+
const { input: input4 } = await import("@inquirer/prompts");
|
|
2053
2153
|
try {
|
|
2054
|
-
stage = await
|
|
2154
|
+
stage = await input4({
|
|
2055
2155
|
message: "Stage name (e.g. dev, staging, prod):",
|
|
2056
2156
|
validate: (v) => validateStage(v).error ?? true
|
|
2057
2157
|
});
|
|
@@ -2081,7 +2181,7 @@ function registerInitCommand(program2) {
|
|
|
2081
2181
|
const targetDir = resolve2(opts.dir);
|
|
2082
2182
|
const tfDir = join5(targetDir, "terraform");
|
|
2083
2183
|
const tfvarsPath = join5(tfDir, "terraform.tfvars");
|
|
2084
|
-
if (
|
|
2184
|
+
if (existsSync8(tfvarsPath)) {
|
|
2085
2185
|
printWarning(`terraform.tfvars already exists at ${tfvarsPath}`);
|
|
2086
2186
|
const overwrite = await ask2("Overwrite?", "N");
|
|
2087
2187
|
if (overwrite.toLowerCase() !== "y") {
|
|
@@ -2151,18 +2251,18 @@ function registerInitCommand(program2) {
|
|
|
2151
2251
|
for (const dir of copyDirs) {
|
|
2152
2252
|
const src = join5(bundledTf, dir);
|
|
2153
2253
|
const dst = join5(tfDir, dir);
|
|
2154
|
-
if (
|
|
2254
|
+
if (existsSync8(src) && !existsSync8(dst)) {
|
|
2155
2255
|
cpSync(src, dst, { recursive: true });
|
|
2156
2256
|
}
|
|
2157
2257
|
}
|
|
2158
2258
|
const schemaPath = join5(bundledTf, "schema.graphql");
|
|
2159
|
-
if (
|
|
2259
|
+
if (existsSync8(schemaPath) && !existsSync8(join5(tfDir, "schema.graphql"))) {
|
|
2160
2260
|
cpSync(schemaPath, join5(tfDir, "schema.graphql"));
|
|
2161
2261
|
}
|
|
2162
2262
|
const tfvars = buildTfvars(config);
|
|
2163
2263
|
writeFileSync4(tfvarsPath, tfvars);
|
|
2164
2264
|
const mainTfPath = join5(tfDir, "main.tf");
|
|
2165
|
-
if (!
|
|
2265
|
+
if (!existsSync8(mainTfPath)) {
|
|
2166
2266
|
writeFileSync4(mainTfPath, `################################################################################
|
|
2167
2267
|
# Thinkwork \u2014 ${config.stage}
|
|
2168
2268
|
# Generated by: thinkwork init -s ${config.stage}
|
|
@@ -2590,12 +2690,13 @@ and AgentCore for per-stage detail.
|
|
|
2590
2690
|
|
|
2591
2691
|
// src/commands/mcp.ts
|
|
2592
2692
|
import chalk10 from "chalk";
|
|
2693
|
+
import { createClient, adminKeys, AdminOpsError } from "@thinkwork/admin-ops";
|
|
2593
2694
|
|
|
2594
2695
|
// src/api-client.ts
|
|
2595
|
-
import { readFileSync as readFileSync5, existsSync as
|
|
2696
|
+
import { readFileSync as readFileSync5, existsSync as existsSync9 } from "fs";
|
|
2596
2697
|
import { execSync as execSync9 } from "child_process";
|
|
2597
2698
|
function readTfVar2(tfvarsPath, key) {
|
|
2598
|
-
if (!
|
|
2699
|
+
if (!existsSync9(tfvarsPath)) return null;
|
|
2599
2700
|
const content = readFileSync5(tfvarsPath, "utf-8");
|
|
2600
2701
|
const match = content.match(new RegExp(`^${key}\\s*=\\s*"([^"]*)"`, "m"));
|
|
2601
2702
|
return match ? match[1] : null;
|
|
@@ -2604,7 +2705,7 @@ function resolveTfvarsPath2(stage) {
|
|
|
2604
2705
|
const tfDir = resolveTerraformDir(stage);
|
|
2605
2706
|
if (tfDir) {
|
|
2606
2707
|
const direct = `${tfDir}/terraform.tfvars`;
|
|
2607
|
-
if (
|
|
2708
|
+
if (existsSync9(direct)) return direct;
|
|
2608
2709
|
}
|
|
2609
2710
|
const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
|
|
2610
2711
|
const cwd = resolveTierDir(terraformDir, stage, "app");
|
|
@@ -2911,14 +3012,14 @@ Examples:
|
|
|
2911
3012
|
$ thinkwork mcp add my-tools --url https://mcp.example.com/crm \\
|
|
2912
3013
|
--auth-type tenant_api_key --api-key sk-abc -s dev -t acme
|
|
2913
3014
|
|
|
2914
|
-
# OAuth
|
|
2915
|
-
$ thinkwork mcp add
|
|
2916
|
-
--auth-type per_user_oauth --oauth-provider
|
|
3015
|
+
# OAuth-backed MCP integration (users connect from the mobile app)
|
|
3016
|
+
$ thinkwork mcp add crm-tools --url https://mcp.example.com/crm \\
|
|
3017
|
+
--auth-type per_user_oauth --oauth-provider crm_provider -s dev -t acme
|
|
2917
3018
|
`
|
|
2918
3019
|
).action(
|
|
2919
3020
|
async (nameArg, opts) => {
|
|
2920
3021
|
try {
|
|
2921
|
-
const { input:
|
|
3022
|
+
const { input: input4 } = await import("@inquirer/prompts");
|
|
2922
3023
|
const { stage, api, tenant } = await resolveMcpContext(opts);
|
|
2923
3024
|
let name = nameArg;
|
|
2924
3025
|
if (!name) {
|
|
@@ -2926,7 +3027,7 @@ Examples:
|
|
|
2926
3027
|
printError("Name is required. Pass it as a positional arg.");
|
|
2927
3028
|
process.exit(1);
|
|
2928
3029
|
}
|
|
2929
|
-
name = await
|
|
3030
|
+
name = await input4({ message: "Server name:" });
|
|
2930
3031
|
}
|
|
2931
3032
|
let url = opts.url;
|
|
2932
3033
|
if (!url) {
|
|
@@ -2934,7 +3035,7 @@ Examples:
|
|
|
2934
3035
|
printError("--url is required. Pass it as a flag.");
|
|
2935
3036
|
process.exit(1);
|
|
2936
3037
|
}
|
|
2937
|
-
url = await
|
|
3038
|
+
url = await input4({
|
|
2938
3039
|
message: "MCP server URL:",
|
|
2939
3040
|
validate: (v) => v.startsWith("http://") || v.startsWith("https://") ? true : "URL must start with http:// or https://"
|
|
2940
3041
|
});
|
|
@@ -2968,13 +3069,13 @@ Examples:
|
|
|
2968
3069
|
`
|
|
2969
3070
|
Examples:
|
|
2970
3071
|
# Change URL in place (preserves agent assignments, unlike remove + re-add)
|
|
2971
|
-
$ thinkwork mcp update
|
|
3072
|
+
$ thinkwork mcp update routing-server --url https://mcp.example.com/routing
|
|
2972
3073
|
|
|
2973
3074
|
# Disable without deleting
|
|
2974
|
-
$ thinkwork mcp update
|
|
3075
|
+
$ thinkwork mcp update routing-server --disable
|
|
2975
3076
|
|
|
2976
3077
|
# Rename + change transport
|
|
2977
|
-
$ thinkwork mcp update 629dcee1-1e14-4b83-9907-cb529e6035f6 --name "
|
|
3078
|
+
$ thinkwork mcp update 629dcee1-1e14-4b83-9907-cb529e6035f6 --name "Routing Server" --transport sse
|
|
2978
3079
|
|
|
2979
3080
|
# Interactive \u2014 pick the server from a list
|
|
2980
3081
|
$ thinkwork mcp update
|
|
@@ -3027,7 +3128,7 @@ Examples:
|
|
|
3027
3128
|
$ thinkwork mcp remove
|
|
3028
3129
|
|
|
3029
3130
|
# By slug (case-insensitive)
|
|
3030
|
-
$ thinkwork mcp remove
|
|
3131
|
+
$ thinkwork mcp remove routing-server
|
|
3031
3132
|
|
|
3032
3133
|
# By UUID (from \`mcp list\` or --json)
|
|
3033
3134
|
$ thinkwork mcp remove 629dcee1-1e14-4b83-9907-cb529e6035f6
|
|
@@ -3094,7 +3195,7 @@ Examples:
|
|
|
3094
3195
|
).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--agent <id>", "Agent ID").action(
|
|
3095
3196
|
async (mcpServerArg, opts) => {
|
|
3096
3197
|
try {
|
|
3097
|
-
const { input:
|
|
3198
|
+
const { input: input4 } = await import("@inquirer/prompts");
|
|
3098
3199
|
const { api, tenant } = await resolveMcpContext(opts);
|
|
3099
3200
|
const server = await resolveServer(mcpServerArg, api, tenant.slug);
|
|
3100
3201
|
let agent = opts.agent;
|
|
@@ -3103,7 +3204,7 @@ Examples:
|
|
|
3103
3204
|
printError("--agent is required. Pass it as a flag.");
|
|
3104
3205
|
process.exit(1);
|
|
3105
3206
|
}
|
|
3106
|
-
agent = await
|
|
3207
|
+
agent = await input4({ message: "Agent ID:" });
|
|
3107
3208
|
}
|
|
3108
3209
|
const result = await apiFetch(
|
|
3109
3210
|
api.apiUrl,
|
|
@@ -3126,7 +3227,7 @@ Examples:
|
|
|
3126
3227
|
).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--agent <id>", "Agent ID").action(
|
|
3127
3228
|
async (mcpServerArg, opts) => {
|
|
3128
3229
|
try {
|
|
3129
|
-
const { input:
|
|
3230
|
+
const { input: input4 } = await import("@inquirer/prompts");
|
|
3130
3231
|
const { api, tenant } = await resolveMcpContext(opts);
|
|
3131
3232
|
const server = await resolveServer(mcpServerArg, api, tenant.slug);
|
|
3132
3233
|
let agent = opts.agent;
|
|
@@ -3135,7 +3236,7 @@ Examples:
|
|
|
3135
3236
|
printError("--agent is required. Pass it as a flag.");
|
|
3136
3237
|
process.exit(1);
|
|
3137
3238
|
}
|
|
3138
|
-
agent = await
|
|
3239
|
+
agent = await input4({ message: "Agent ID:" });
|
|
3139
3240
|
}
|
|
3140
3241
|
await apiFetch(
|
|
3141
3242
|
api.apiUrl,
|
|
@@ -3151,6 +3252,175 @@ Examples:
|
|
|
3151
3252
|
}
|
|
3152
3253
|
}
|
|
3153
3254
|
);
|
|
3255
|
+
const key = mcp.command("key").alias("keys").description("Manage per-tenant Bearer tokens for the admin-ops MCP server.");
|
|
3256
|
+
key.command("create").description(
|
|
3257
|
+
"Generate a new MCP admin token. Prints the raw token ONCE \u2014 save it immediately."
|
|
3258
|
+
).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug or UUID").option("--name <label>", 'Human label for the key (default "default")').addHelpText(
|
|
3259
|
+
"after",
|
|
3260
|
+
`
|
|
3261
|
+
Examples:
|
|
3262
|
+
$ thinkwork mcp key create -t acme --name ci
|
|
3263
|
+
$ thinkwork mcp key create -t acme --json # token surfaces under .token
|
|
3264
|
+
|
|
3265
|
+
The token is shown ONCE. Hand it to the MCP client immediately; the server
|
|
3266
|
+
stores only the SHA-256 hash. To rotate, create a new one and revoke the
|
|
3267
|
+
old one.
|
|
3268
|
+
`
|
|
3269
|
+
).action(async (opts) => {
|
|
3270
|
+
try {
|
|
3271
|
+
const ctx = await resolveMcpContext(opts);
|
|
3272
|
+
const client = createClient({
|
|
3273
|
+
apiUrl: ctx.api.apiUrl,
|
|
3274
|
+
authSecret: ctx.api.authSecret
|
|
3275
|
+
});
|
|
3276
|
+
const created = await adminKeys.createAdminKey(client, ctx.tenant.slug, {
|
|
3277
|
+
name: opts.name
|
|
3278
|
+
});
|
|
3279
|
+
printJson(created);
|
|
3280
|
+
printWarning("This token will NOT be shown again. Copy it now.");
|
|
3281
|
+
printSuccess(`MCP admin key created: ${chalk10.bold(created.name)} (${created.id})`);
|
|
3282
|
+
console.log(` ${chalk10.dim("Token:")} ${chalk10.cyan(created.token)}`);
|
|
3283
|
+
} catch (err) {
|
|
3284
|
+
if (isCancellation(err)) return;
|
|
3285
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
3286
|
+
process.exit(1);
|
|
3287
|
+
}
|
|
3288
|
+
});
|
|
3289
|
+
key.command("list").alias("ls").description("List MCP admin keys for a tenant (metadata only, never the raw token).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug or UUID").option("--all", "Include revoked keys").action(async (opts) => {
|
|
3290
|
+
try {
|
|
3291
|
+
const ctx = await resolveMcpContext(opts);
|
|
3292
|
+
const client = createClient({
|
|
3293
|
+
apiUrl: ctx.api.apiUrl,
|
|
3294
|
+
authSecret: ctx.api.authSecret
|
|
3295
|
+
});
|
|
3296
|
+
const all = await adminKeys.listAdminKeys(client, ctx.tenant.slug);
|
|
3297
|
+
const rows = opts.all ? all : all.filter((k) => !k.revoked_at);
|
|
3298
|
+
printJson(rows);
|
|
3299
|
+
printTable(rows, [
|
|
3300
|
+
{ key: "name", header: "NAME" },
|
|
3301
|
+
{ key: "id", header: "ID" },
|
|
3302
|
+
{ key: "created_at", header: "CREATED" },
|
|
3303
|
+
{ key: "last_used_at", header: "LAST USED" },
|
|
3304
|
+
{ key: "revoked_at", header: "REVOKED" }
|
|
3305
|
+
]);
|
|
3306
|
+
} catch (err) {
|
|
3307
|
+
if (isCancellation(err)) return;
|
|
3308
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
3309
|
+
process.exit(1);
|
|
3310
|
+
}
|
|
3311
|
+
});
|
|
3312
|
+
key.command("revoke <id>").description("Revoke an MCP admin key. Idempotent \u2014 revoking an already-revoked key is a no-op.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug or UUID").action(async (keyId, opts) => {
|
|
3313
|
+
try {
|
|
3314
|
+
const ctx = await resolveMcpContext(opts);
|
|
3315
|
+
const client = createClient({
|
|
3316
|
+
apiUrl: ctx.api.apiUrl,
|
|
3317
|
+
authSecret: ctx.api.authSecret
|
|
3318
|
+
});
|
|
3319
|
+
await adminKeys.revokeAdminKey(client, ctx.tenant.slug, keyId);
|
|
3320
|
+
printSuccess(`MCP admin key revoked: ${keyId}`);
|
|
3321
|
+
} catch (err) {
|
|
3322
|
+
if (err instanceof AdminOpsError && err.status === 404) {
|
|
3323
|
+
printError(`Key ${keyId} not found for tenant ${opts.tenant ?? ""}`);
|
|
3324
|
+
process.exit(2);
|
|
3325
|
+
}
|
|
3326
|
+
if (isCancellation(err)) return;
|
|
3327
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
3328
|
+
process.exit(1);
|
|
3329
|
+
}
|
|
3330
|
+
});
|
|
3331
|
+
mcp.command("provision").description(
|
|
3332
|
+
"Provision the admin-ops MCP for a tenant: mint a new key + store in Secrets Manager + register in tenant_mcp_servers."
|
|
3333
|
+
).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug or UUID (omit with --all)").option(
|
|
3334
|
+
"--url <url>",
|
|
3335
|
+
"Override the MCP endpoint URL (defaults to the stage's execute-api or MCP_CUSTOM_DOMAIN)"
|
|
3336
|
+
).option("--all", "Provision for every tenant the caller can see (backfill mode)").addHelpText(
|
|
3337
|
+
"after",
|
|
3338
|
+
`
|
|
3339
|
+
Examples:
|
|
3340
|
+
# One tenant (interactive picker or -t)
|
|
3341
|
+
$ thinkwork mcp provision -t acme
|
|
3342
|
+
|
|
3343
|
+
# Explicit custom-domain URL
|
|
3344
|
+
$ thinkwork mcp provision -t acme --url https://mcp.thinkwork.ai/mcp/admin
|
|
3345
|
+
|
|
3346
|
+
# Backfill every tenant at once
|
|
3347
|
+
$ thinkwork mcp provision --all
|
|
3348
|
+
|
|
3349
|
+
The raw token is stored in Secrets Manager and duplicated into
|
|
3350
|
+
tenant_mcp_servers.auth_config \u2014 it is NOT printed on stdout. After
|
|
3351
|
+
provisioning, each agent still needs the server assigned via the admin
|
|
3352
|
+
SPA (or a future \`thinkwork mcp assign\` CLI pass).
|
|
3353
|
+
`
|
|
3354
|
+
).action(async (opts) => {
|
|
3355
|
+
try {
|
|
3356
|
+
if (opts.all && opts.tenant) {
|
|
3357
|
+
printError("--all and --tenant are mutually exclusive.");
|
|
3358
|
+
process.exit(1);
|
|
3359
|
+
}
|
|
3360
|
+
const stage = await resolveStage({ flag: opts.stage });
|
|
3361
|
+
const api = resolveApiConfig(stage);
|
|
3362
|
+
if (!api) process.exit(1);
|
|
3363
|
+
async function provisionOne(tenantIdOrSlug, label) {
|
|
3364
|
+
const res = await apiFetch(
|
|
3365
|
+
api.apiUrl,
|
|
3366
|
+
api.authSecret,
|
|
3367
|
+
`/api/tenants/${encodeURIComponent(tenantIdOrSlug)}/mcp-admin-provision`,
|
|
3368
|
+
{
|
|
3369
|
+
method: "POST",
|
|
3370
|
+
body: JSON.stringify(opts.url ? { url: opts.url } : {})
|
|
3371
|
+
}
|
|
3372
|
+
);
|
|
3373
|
+
printSuccess(
|
|
3374
|
+
`${label}: ${res.provisioned} (tenant_mcp_servers.id=${res.tenantMcpServerId}, url=${res.url})`
|
|
3375
|
+
);
|
|
3376
|
+
return res;
|
|
3377
|
+
}
|
|
3378
|
+
if (opts.all) {
|
|
3379
|
+
const tenantRows = await apiFetch(
|
|
3380
|
+
api.apiUrl,
|
|
3381
|
+
api.authSecret,
|
|
3382
|
+
"/api/tenants",
|
|
3383
|
+
{}
|
|
3384
|
+
);
|
|
3385
|
+
if (!Array.isArray(tenantRows) || tenantRows.length === 0) {
|
|
3386
|
+
printWarning("No tenants found.");
|
|
3387
|
+
return;
|
|
3388
|
+
}
|
|
3389
|
+
printHeader("mcp provision --all", stage);
|
|
3390
|
+
const results = [];
|
|
3391
|
+
for (const t of tenantRows) {
|
|
3392
|
+
try {
|
|
3393
|
+
await provisionOne(t.slug, `${t.name} (${t.slug})`);
|
|
3394
|
+
results.push({ slug: t.slug, ok: true });
|
|
3395
|
+
} catch (err) {
|
|
3396
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3397
|
+
printError(` \u2717 ${t.slug}: ${msg}`);
|
|
3398
|
+
results.push({ slug: t.slug, ok: false, reason: msg });
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
const ok = results.filter((r) => r.ok).length;
|
|
3402
|
+
const bad = results.length - ok;
|
|
3403
|
+
if (bad === 0) {
|
|
3404
|
+
printSuccess(`All ${ok} tenants provisioned.`);
|
|
3405
|
+
} else {
|
|
3406
|
+
printWarning(`${ok}/${results.length} succeeded; ${bad} failed.`);
|
|
3407
|
+
process.exit(1);
|
|
3408
|
+
}
|
|
3409
|
+
return;
|
|
3410
|
+
}
|
|
3411
|
+
const tenant = await resolveTenantRest({
|
|
3412
|
+
flag: opts.tenant,
|
|
3413
|
+
stage,
|
|
3414
|
+
apiUrl: api.apiUrl,
|
|
3415
|
+
authSecret: api.authSecret
|
|
3416
|
+
});
|
|
3417
|
+
await provisionOne(tenant.slug, tenant.slug);
|
|
3418
|
+
} catch (err) {
|
|
3419
|
+
if (isCancellation(err)) return;
|
|
3420
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
3421
|
+
process.exit(1);
|
|
3422
|
+
}
|
|
3423
|
+
});
|
|
3154
3424
|
}
|
|
3155
3425
|
|
|
3156
3426
|
// src/commands/tools.ts
|
|
@@ -3425,11 +3695,11 @@ function registerUpdateCommand(program2) {
|
|
|
3425
3695
|
}
|
|
3426
3696
|
|
|
3427
3697
|
// src/commands/user.ts
|
|
3428
|
-
import { spawn as
|
|
3698
|
+
import { spawn as spawn5 } from "child_process";
|
|
3429
3699
|
import { input as input2, select as select7 } from "@inquirer/prompts";
|
|
3430
3700
|
function getTerraformOutput2(cwd, key) {
|
|
3431
|
-
return new Promise((
|
|
3432
|
-
const proc =
|
|
3701
|
+
return new Promise((resolve4, reject) => {
|
|
3702
|
+
const proc = spawn5("terraform", ["output", "-raw", key], {
|
|
3433
3703
|
cwd,
|
|
3434
3704
|
stdio: ["pipe", "pipe", "pipe"]
|
|
3435
3705
|
});
|
|
@@ -3438,7 +3708,7 @@ function getTerraformOutput2(cwd, key) {
|
|
|
3438
3708
|
proc.stdout.on("data", (d) => stdout += d);
|
|
3439
3709
|
proc.stderr.on("data", (d) => stderr += d);
|
|
3440
3710
|
proc.on("close", (code) => {
|
|
3441
|
-
if (code === 0)
|
|
3711
|
+
if (code === 0) resolve4(stdout.trim());
|
|
3442
3712
|
else
|
|
3443
3713
|
reject(
|
|
3444
3714
|
new Error(
|
|
@@ -3449,7 +3719,7 @@ function getTerraformOutput2(cwd, key) {
|
|
|
3449
3719
|
});
|
|
3450
3720
|
}
|
|
3451
3721
|
function runAwsCognitoReset(userPoolId, username, region) {
|
|
3452
|
-
return new Promise((
|
|
3722
|
+
return new Promise((resolve4) => {
|
|
3453
3723
|
const args = [
|
|
3454
3724
|
"cognito-idp",
|
|
3455
3725
|
"admin-reset-user-password",
|
|
@@ -3461,12 +3731,12 @@ function runAwsCognitoReset(userPoolId, username, region) {
|
|
|
3461
3731
|
"json"
|
|
3462
3732
|
];
|
|
3463
3733
|
if (region) args.push("--region", region);
|
|
3464
|
-
const proc =
|
|
3734
|
+
const proc = spawn5("aws", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
3465
3735
|
let stdout = "";
|
|
3466
3736
|
let stderr = "";
|
|
3467
3737
|
proc.stdout.on("data", (d) => stdout += d);
|
|
3468
3738
|
proc.stderr.on("data", (d) => stderr += d);
|
|
3469
|
-
proc.on("close", (code) =>
|
|
3739
|
+
proc.on("close", (code) => resolve4({ code: code ?? 1, stdout, stderr }));
|
|
3470
3740
|
});
|
|
3471
3741
|
}
|
|
3472
3742
|
function requireTty2(label) {
|
|
@@ -3747,9 +4017,8 @@ import { gql } from "@urql/core";
|
|
|
3747
4017
|
|
|
3748
4018
|
// src/lib/gql-client.ts
|
|
3749
4019
|
import {
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
fetchExchange
|
|
4020
|
+
CombinedError,
|
|
4021
|
+
stringifyDocument
|
|
3753
4022
|
} from "@urql/core";
|
|
3754
4023
|
|
|
3755
4024
|
// src/lib/resolve-auth.ts
|
|
@@ -3845,27 +4114,113 @@ async function getGqlClient(opts) {
|
|
|
3845
4114
|
}
|
|
3846
4115
|
const url = `${baseUrl.replace(/\/+$/, "")}/graphql`;
|
|
3847
4116
|
const auth = await resolveAuth({ stage: opts.stage, region });
|
|
3848
|
-
const client =
|
|
4117
|
+
const client = createCliGqlClient(url, auth.headers);
|
|
4118
|
+
return {
|
|
4119
|
+
client,
|
|
3849
4120
|
url,
|
|
3850
|
-
|
|
3851
|
-
|
|
4121
|
+
tenantId: auth.tenantId,
|
|
4122
|
+
tenantSlug: auth.tenantSlug
|
|
4123
|
+
};
|
|
4124
|
+
}
|
|
4125
|
+
function createCliGqlClient(url, headers) {
|
|
4126
|
+
return {
|
|
4127
|
+
query: (doc, variables) => ({
|
|
4128
|
+
toPromise: () => executeGraphql(url, headers, doc, variables)
|
|
4129
|
+
}),
|
|
4130
|
+
mutation: (doc, variables) => ({
|
|
4131
|
+
toPromise: () => executeGraphql(url, headers, doc, variables)
|
|
4132
|
+
})
|
|
4133
|
+
};
|
|
4134
|
+
}
|
|
4135
|
+
async function gqlQuery(client, doc, variables) {
|
|
4136
|
+
const res = await client.query(serializeDocument(doc), variables).toPromise();
|
|
4137
|
+
return unwrap(res);
|
|
4138
|
+
}
|
|
4139
|
+
async function gqlMutate(client, doc, variables) {
|
|
4140
|
+
const res = await client.mutation(serializeDocument(doc), variables).toPromise();
|
|
4141
|
+
return unwrap(res);
|
|
4142
|
+
}
|
|
4143
|
+
function serializeDocument(doc) {
|
|
4144
|
+
return stringifyDocument(doc);
|
|
4145
|
+
}
|
|
4146
|
+
async function executeGraphql(url, headers, doc, variables) {
|
|
4147
|
+
const query = serializeDocument(doc);
|
|
4148
|
+
try {
|
|
4149
|
+
const response = await fetch(url, {
|
|
3852
4150
|
method: "POST",
|
|
3853
4151
|
headers: {
|
|
3854
4152
|
"content-type": "application/json",
|
|
3855
|
-
...
|
|
4153
|
+
...headers
|
|
4154
|
+
},
|
|
4155
|
+
body: JSON.stringify({ query, variables })
|
|
4156
|
+
});
|
|
4157
|
+
const text = await response.text();
|
|
4158
|
+
let payload = {};
|
|
4159
|
+
if (text) {
|
|
4160
|
+
try {
|
|
4161
|
+
payload = JSON.parse(text);
|
|
4162
|
+
} catch {
|
|
4163
|
+
return makeNetworkErrorResult(
|
|
4164
|
+
`GraphQL request failed with non-JSON response: ${text}`,
|
|
4165
|
+
response
|
|
4166
|
+
);
|
|
3856
4167
|
}
|
|
3857
|
-
}
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
4168
|
+
}
|
|
4169
|
+
if (payload.errors?.length) {
|
|
4170
|
+
return {
|
|
4171
|
+
data: payload.data,
|
|
4172
|
+
error: new CombinedError({
|
|
4173
|
+
graphQLErrors: payload.errors,
|
|
4174
|
+
response
|
|
4175
|
+
}),
|
|
4176
|
+
extensions: payload.extensions,
|
|
4177
|
+
stale: false,
|
|
4178
|
+
hasNext: false
|
|
4179
|
+
};
|
|
4180
|
+
}
|
|
4181
|
+
if (!response.ok) {
|
|
4182
|
+
return makeNetworkErrorResult(
|
|
4183
|
+
`GraphQL request failed with HTTP ${response.status}`,
|
|
4184
|
+
response
|
|
4185
|
+
);
|
|
4186
|
+
}
|
|
4187
|
+
return {
|
|
4188
|
+
data: payload.data,
|
|
4189
|
+
error: void 0,
|
|
4190
|
+
extensions: payload.extensions,
|
|
4191
|
+
stale: false,
|
|
4192
|
+
hasNext: false
|
|
4193
|
+
};
|
|
4194
|
+
} catch (err) {
|
|
4195
|
+
return {
|
|
4196
|
+
error: new CombinedError({
|
|
4197
|
+
networkError: err instanceof Error ? err : new Error(String(err))
|
|
4198
|
+
}),
|
|
4199
|
+
stale: false,
|
|
4200
|
+
hasNext: false
|
|
4201
|
+
};
|
|
4202
|
+
}
|
|
4203
|
+
}
|
|
4204
|
+
function makeNetworkErrorResult(message, response) {
|
|
3862
4205
|
return {
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
4206
|
+
error: new CombinedError({
|
|
4207
|
+
networkError: new Error(message),
|
|
4208
|
+
response
|
|
4209
|
+
}),
|
|
4210
|
+
stale: false,
|
|
4211
|
+
hasNext: false
|
|
3867
4212
|
};
|
|
3868
4213
|
}
|
|
4214
|
+
function unwrap(res) {
|
|
4215
|
+
if (res.error) {
|
|
4216
|
+
const msg = res.error.graphQLErrors.map((e) => e.message).filter(Boolean).join("; ") || res.error.networkError?.message || "GraphQL request failed";
|
|
4217
|
+
throw new Error(msg);
|
|
4218
|
+
}
|
|
4219
|
+
if (!res.data) {
|
|
4220
|
+
throw new Error("GraphQL request returned no data.");
|
|
4221
|
+
}
|
|
4222
|
+
return res.data;
|
|
4223
|
+
}
|
|
3869
4224
|
|
|
3870
4225
|
// src/commands/me.ts
|
|
3871
4226
|
var ME_QUERY = gql`
|
|
@@ -3956,20 +4311,20 @@ function notYetImplemented(commandPath, phase) {
|
|
|
3956
4311
|
// src/commands/thread.ts
|
|
3957
4312
|
function registerThreadCommand(program2) {
|
|
3958
4313
|
const thread = program2.command("thread").alias("threads").description(
|
|
3959
|
-
"Create, list, update, and comment on threads
|
|
4314
|
+
"Create, list, update, and comment on threads in a tenant."
|
|
3960
4315
|
);
|
|
3961
|
-
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("--
|
|
4316
|
+
thread.command("list").alias("ls").description("List threads in a tenant with optional filters.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--assignee <id>", "Filter by assignee (user or agent ID). Use `me` to match the caller.").option("--agent <id>", "Filter threads worked on by a specific agent").option("--search <q>", "Full-text search over thread titles").option("--limit <n>", "Max rows (default 50)", "50").option("--archived", "Include archived threads").addHelpText(
|
|
3962
4317
|
"after",
|
|
3963
4318
|
`
|
|
3964
4319
|
Examples:
|
|
3965
|
-
# Open work on the default stage/tenant
|
|
3966
|
-
$ thinkwork thread list --status IN_PROGRESS
|
|
3967
|
-
|
|
3968
|
-
# Pipe to jq
|
|
3969
|
-
$ thinkwork thread list --json | jq '.[] | select(.priority=="URGENT")'
|
|
3970
|
-
|
|
3971
4320
|
# Everything assigned to me
|
|
3972
4321
|
$ thinkwork thread list --assignee me
|
|
4322
|
+
|
|
4323
|
+
# Limit + JSON for piping
|
|
4324
|
+
$ thinkwork thread list --limit 100 --json | jq '.[] | .title'
|
|
4325
|
+
|
|
4326
|
+
# Archived threads only
|
|
4327
|
+
$ thinkwork thread list --archived
|
|
3973
4328
|
`
|
|
3974
4329
|
).action(() => notYetImplemented("thread list", 1));
|
|
3975
4330
|
thread.command("get <idOrNumber>").description("Fetch one thread by ID or by its tenant-scoped issue number.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
|
|
@@ -3981,38 +4336,29 @@ Examples:
|
|
|
3981
4336
|
$ thinkwork thread get 42 --json | jq .assignee
|
|
3982
4337
|
`
|
|
3983
4338
|
).action(() => notYetImplemented("thread get", 1));
|
|
3984
|
-
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("--
|
|
4339
|
+
thread.command("create [title]").description("Create a new thread. Prompts for missing fields when running in a TTY.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--assignee <id>", "Assign on create (user or agent ID)").option("--due <iso>", "Due date as ISO-8601").option("--label <name...>", "Attach label(s) by name (repeatable)").addHelpText(
|
|
3985
4340
|
"after",
|
|
3986
4341
|
`
|
|
3987
4342
|
Examples:
|
|
3988
|
-
# Fully interactive \u2014 walkthrough prompts for title
|
|
4343
|
+
# Fully interactive \u2014 walkthrough prompts for title and assignee.
|
|
3989
4344
|
$ thinkwork thread create
|
|
3990
4345
|
|
|
3991
4346
|
# Scripted
|
|
3992
4347
|
$ thinkwork thread create "Investigate latency spike" \\
|
|
3993
|
-
--
|
|
4348
|
+
--assignee agt-obs-1 --label ops --label oncall
|
|
3994
4349
|
|
|
3995
4350
|
# Mix: pass the title, prompt for the rest.
|
|
3996
4351
|
$ thinkwork thread create "Investigate latency spike"
|
|
3997
4352
|
`
|
|
3998
4353
|
).action(() => notYetImplemented("thread create", 1));
|
|
3999
|
-
thread.command("update <id>").description("Update a thread's title,
|
|
4354
|
+
thread.command("update <id>").description("Update a thread's title, assignee, labels, or due date.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--title <t>", "Rename").option("--assignee <id>", "Reassign (user or agent ID)").option("--due <iso>", "Due date").addHelpText(
|
|
4000
4355
|
"after",
|
|
4001
4356
|
`
|
|
4002
4357
|
Examples:
|
|
4003
|
-
$ thinkwork thread update thr-abc --
|
|
4004
|
-
$ thinkwork thread update thr-abc --assignee agt-ops
|
|
4358
|
+
$ thinkwork thread update thr-abc --title "New title"
|
|
4359
|
+
$ thinkwork thread update thr-abc --assignee agt-ops
|
|
4005
4360
|
`
|
|
4006
4361
|
).action(() => notYetImplemented("thread update", 1));
|
|
4007
|
-
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(
|
|
4008
|
-
"after",
|
|
4009
|
-
`
|
|
4010
|
-
Examples:
|
|
4011
|
-
$ thinkwork thread close thr-abc
|
|
4012
|
-
$ thinkwork thread close thr-abc --comment "fixed in #124"
|
|
4013
|
-
`
|
|
4014
|
-
).action(() => notYetImplemented("thread close", 1));
|
|
4015
|
-
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));
|
|
4016
4362
|
thread.command("checkout <id>").description("Claim a thread so an agent can work it (locks other agents out).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--agent <id>", "Agent to check it out to (defaults to the caller)").addHelpText(
|
|
4017
4363
|
"after",
|
|
4018
4364
|
`
|
|
@@ -4020,7 +4366,7 @@ Examples:
|
|
|
4020
4366
|
$ thinkwork thread checkout thr-abc --agent agt-fixer
|
|
4021
4367
|
`
|
|
4022
4368
|
).action(() => notYetImplemented("thread checkout", 1));
|
|
4023
|
-
thread.command("release <id>").description("Release a checked-out thread
|
|
4369
|
+
thread.command("release <id>").description("Release a checked-out thread (unlocks it so another agent can claim it).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("thread release", 1));
|
|
4024
4370
|
thread.command("comment <id> [content]").description("Add a comment to a thread. Prompts for content if omitted and TTY.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--file <path>", "Read comment content from a file (markdown)").addHelpText(
|
|
4025
4371
|
"after",
|
|
4026
4372
|
`
|
|
@@ -4206,47 +4552,317 @@ Examples:
|
|
|
4206
4552
|
version.command("rollback <agentId> <versionId>").description("Restore an agent to a prior version. Creates a new version pointing at the old config.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("agent version rollback", 2));
|
|
4207
4553
|
}
|
|
4208
4554
|
|
|
4209
|
-
// src/commands/
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4555
|
+
// src/commands/computer.ts
|
|
4556
|
+
import chalk14 from "chalk";
|
|
4557
|
+
async function resolveComputerContext(opts) {
|
|
4558
|
+
const stage = await resolveStage({ flag: opts.stage });
|
|
4559
|
+
const api = resolveApiConfig(stage);
|
|
4560
|
+
if (!api) process.exit(1);
|
|
4561
|
+
return { stage, api };
|
|
4562
|
+
}
|
|
4563
|
+
function resolveTenantId(opts) {
|
|
4564
|
+
const tenantId = opts.tenant ?? opts.tenantId;
|
|
4565
|
+
if (!tenantId) {
|
|
4566
|
+
printError(
|
|
4567
|
+
"Tenant ID is required. Pass --tenant <uuid> or --tenant-id <uuid>."
|
|
4568
|
+
);
|
|
4569
|
+
process.exit(1);
|
|
4570
|
+
}
|
|
4571
|
+
return tenantId;
|
|
4572
|
+
}
|
|
4573
|
+
function resolveComputerId(opts) {
|
|
4574
|
+
const computerId = opts.computer ?? opts.computerId;
|
|
4575
|
+
if (!computerId) {
|
|
4576
|
+
printError(
|
|
4577
|
+
"Computer ID is required. Pass --computer <uuid> or --computer-id <uuid>."
|
|
4578
|
+
);
|
|
4579
|
+
process.exit(1);
|
|
4580
|
+
}
|
|
4581
|
+
return computerId;
|
|
4582
|
+
}
|
|
4583
|
+
function resolveTaskType(opts) {
|
|
4584
|
+
if (!opts.type?.trim()) {
|
|
4585
|
+
printError("Task type is required. Pass --type <task-type>.");
|
|
4586
|
+
process.exit(1);
|
|
4587
|
+
}
|
|
4588
|
+
return opts.type.trim().toLowerCase();
|
|
4589
|
+
}
|
|
4590
|
+
function printMigrationReport(response) {
|
|
4591
|
+
printJson(response);
|
|
4592
|
+
if (isJsonMode()) return;
|
|
4593
|
+
if (!response.report) return;
|
|
4594
|
+
const summary = response.report.summary ?? {};
|
|
4595
|
+
console.log("");
|
|
4596
|
+
console.log(chalk14.bold(" Summary"));
|
|
4597
|
+
for (const [status, count] of Object.entries(summary)) {
|
|
4598
|
+
if (!count) continue;
|
|
4599
|
+
console.log(` ${status.padEnd(28)} ${count}`);
|
|
4600
|
+
}
|
|
4601
|
+
const rows = (response.report.groups ?? []).map((group) => ({
|
|
4602
|
+
owner: group.owner?.name ?? group.owner?.email ?? group.ownerUserId ?? "unpaired",
|
|
4603
|
+
source: group.primaryAgent?.name ?? group.primaryAgentId ?? "\u2014",
|
|
4604
|
+
template: group.primaryAgent?.templateName ?? "\u2014",
|
|
4605
|
+
status: group.status,
|
|
4606
|
+
action: group.recommendedAction ?? "\u2014",
|
|
4607
|
+
reason: group.reasons?.[0] ?? "\u2014"
|
|
4608
|
+
}));
|
|
4609
|
+
console.log("");
|
|
4610
|
+
printTable(rows, [
|
|
4611
|
+
{ key: "owner", header: "Owner" },
|
|
4612
|
+
{ key: "source", header: "Source Agent" },
|
|
4613
|
+
{ key: "template", header: "Template" },
|
|
4614
|
+
{ key: "status", header: "Status" },
|
|
4615
|
+
{ key: "action", header: "Action" },
|
|
4616
|
+
{ key: "reason", header: "Reason" }
|
|
4617
|
+
]);
|
|
4618
|
+
}
|
|
4619
|
+
function registerComputerCommand(program2) {
|
|
4620
|
+
const computer = program2.command("computer").alias("computers").description("Manage ThinkWork Computers and migration operations");
|
|
4621
|
+
const migration = computer.command("migration").description("Dry-run or apply Agent-to-Computer migration");
|
|
4622
|
+
migration.command("dry-run").description("Inspect Agent-to-Computer migration candidates").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <uuid>", "Tenant ID").option("--tenant-id <uuid>", "Tenant ID").action(
|
|
4623
|
+
async (opts) => {
|
|
4624
|
+
const { stage, api } = await resolveComputerContext(opts);
|
|
4625
|
+
const tenantId = resolveTenantId(opts);
|
|
4626
|
+
if (!isJsonMode()) printHeader("computer migration dry-run", stage);
|
|
4627
|
+
const response = await apiFetchRaw(
|
|
4628
|
+
api.apiUrl,
|
|
4629
|
+
api.authSecret,
|
|
4630
|
+
"/api/migrations/agents-to-computers",
|
|
4631
|
+
{
|
|
4632
|
+
method: "POST",
|
|
4633
|
+
body: JSON.stringify({ tenantId, mode: "dry-run" })
|
|
4634
|
+
}
|
|
4635
|
+
);
|
|
4636
|
+
if (!response.ok) {
|
|
4637
|
+
printJson(response.body);
|
|
4638
|
+
printError(response.body.error ?? `HTTP ${response.status}`);
|
|
4639
|
+
process.exit(1);
|
|
4640
|
+
}
|
|
4641
|
+
printMigrationReport(response.body);
|
|
4642
|
+
if (!isJsonMode()) printSuccess("Computer migration dry-run complete");
|
|
4643
|
+
}
|
|
4644
|
+
);
|
|
4645
|
+
migration.command("apply").description(
|
|
4646
|
+
"Apply Agent-to-Computer migration after reviewing dry-run output"
|
|
4647
|
+
).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <uuid>", "Tenant ID").option("--tenant-id <uuid>", "Tenant ID").option("--confirm", "Confirm the migration apply operation").option("--idempotency-key <key>", "Operator-supplied migration run key").action(
|
|
4648
|
+
async (opts) => {
|
|
4649
|
+
const { stage, api } = await resolveComputerContext(opts);
|
|
4650
|
+
const tenantId = resolveTenantId(opts);
|
|
4651
|
+
if (!opts.confirm) {
|
|
4652
|
+
printWarning(
|
|
4653
|
+
"Apply is intentionally gated. Re-run with --confirm after reviewing dry-run output."
|
|
4654
|
+
);
|
|
4655
|
+
process.exit(1);
|
|
4656
|
+
}
|
|
4657
|
+
if (!isJsonMode()) printHeader("computer migration apply", stage);
|
|
4658
|
+
const response = await apiFetchRaw(
|
|
4659
|
+
api.apiUrl,
|
|
4660
|
+
api.authSecret,
|
|
4661
|
+
"/api/migrations/agents-to-computers",
|
|
4662
|
+
{
|
|
4663
|
+
method: "POST",
|
|
4664
|
+
body: JSON.stringify({
|
|
4665
|
+
tenantId,
|
|
4666
|
+
mode: "apply",
|
|
4667
|
+
idempotencyKey: opts.idempotencyKey
|
|
4668
|
+
})
|
|
4669
|
+
}
|
|
4670
|
+
);
|
|
4671
|
+
if (!response.ok) {
|
|
4672
|
+
printJson(response.body);
|
|
4673
|
+
printError(response.body.error ?? `HTTP ${response.status}`);
|
|
4674
|
+
if (response.status === 409 && response.body.blockers) {
|
|
4675
|
+
if (!isJsonMode()) {
|
|
4676
|
+
console.log("");
|
|
4677
|
+
console.log(chalk14.bold(" Blockers"));
|
|
4678
|
+
console.log(JSON.stringify(response.body.blockers, null, 2));
|
|
4679
|
+
}
|
|
4680
|
+
}
|
|
4681
|
+
process.exit(1);
|
|
4682
|
+
}
|
|
4683
|
+
printMigrationReport(response.body);
|
|
4684
|
+
if (!isJsonMode()) {
|
|
4685
|
+
printSuccess(
|
|
4686
|
+
`Computer migration applied: ${response.body.created?.length ?? 0} created, ${response.body.skipped?.length ?? 0} skipped`
|
|
4687
|
+
);
|
|
4688
|
+
}
|
|
4689
|
+
}
|
|
4690
|
+
);
|
|
4691
|
+
const runtime = computer.command("runtime").description("Provision and control ECS-backed Computer runtimes");
|
|
4692
|
+
for (const action of [
|
|
4693
|
+
"provision",
|
|
4694
|
+
"start",
|
|
4695
|
+
"stop",
|
|
4696
|
+
"restart",
|
|
4697
|
+
"status"
|
|
4698
|
+
]) {
|
|
4699
|
+
runtime.command(action).description(`${action} a Computer runtime`).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <uuid>", "Tenant ID").option("--tenant-id <uuid>", "Tenant ID").option("-c, --computer <uuid>", "Computer ID").option("--computer-id <uuid>", "Computer ID").action(
|
|
4700
|
+
async (opts) => {
|
|
4701
|
+
const { stage, api } = await resolveComputerContext(opts);
|
|
4702
|
+
const tenantId = resolveTenantId(opts);
|
|
4703
|
+
const computerId = resolveComputerId(opts);
|
|
4704
|
+
if (!isJsonMode()) {
|
|
4705
|
+
printHeader(`computer runtime ${action}`, stage);
|
|
4706
|
+
}
|
|
4707
|
+
const response = await apiFetchRaw(
|
|
4708
|
+
api.apiUrl,
|
|
4709
|
+
api.authSecret,
|
|
4710
|
+
"/api/computers/manager",
|
|
4711
|
+
{
|
|
4712
|
+
method: "POST",
|
|
4713
|
+
body: JSON.stringify({ action, tenantId, computerId })
|
|
4714
|
+
}
|
|
4715
|
+
);
|
|
4716
|
+
printJson(response.body);
|
|
4717
|
+
if (!response.ok) {
|
|
4718
|
+
printError(response.body.error ?? `HTTP ${response.status}`);
|
|
4719
|
+
process.exit(1);
|
|
4720
|
+
}
|
|
4721
|
+
if (!isJsonMode()) {
|
|
4722
|
+
printSuccess(`Computer runtime ${action} complete`);
|
|
4723
|
+
}
|
|
4724
|
+
}
|
|
4725
|
+
);
|
|
4726
|
+
}
|
|
4727
|
+
const task = computer.command("task").description("Enqueue work for a ThinkWork Computer runtime");
|
|
4728
|
+
task.command("enqueue").description("Enqueue a Computer runtime task").option("--type <type>", "Task type").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <uuid>", "Tenant ID").option("--tenant-id <uuid>", "Tenant ID").option("-c, --computer <uuid>", "Computer ID").option("--computer-id <uuid>", "Computer ID").option("--path <path>", "Workspace-relative path for file-write tasks").option("--content <content>", "UTF-8 content for file-write tasks").option("--idempotency-key <key>", "Operator-supplied idempotency key").action(
|
|
4729
|
+
async (opts) => {
|
|
4730
|
+
const { stage, api } = await resolveComputerContext(opts);
|
|
4731
|
+
const tenantId = resolveTenantId(opts);
|
|
4732
|
+
const computerId = resolveComputerId(opts);
|
|
4733
|
+
const taskType = resolveTaskType(opts);
|
|
4734
|
+
const input4 = taskType === "workspace_file_write" ? { path: opts.path, content: opts.content } : void 0;
|
|
4735
|
+
if (!isJsonMode()) printHeader("computer task enqueue", stage);
|
|
4736
|
+
const response = await apiFetchRaw(
|
|
4737
|
+
api.apiUrl,
|
|
4738
|
+
api.authSecret,
|
|
4739
|
+
"/api/computers/runtime/tasks",
|
|
4740
|
+
{
|
|
4741
|
+
method: "POST",
|
|
4742
|
+
body: JSON.stringify({
|
|
4743
|
+
tenantId,
|
|
4744
|
+
computerId,
|
|
4745
|
+
taskType,
|
|
4746
|
+
input: input4,
|
|
4747
|
+
idempotencyKey: opts.idempotencyKey
|
|
4748
|
+
})
|
|
4749
|
+
}
|
|
4750
|
+
);
|
|
4751
|
+
printJson(response.body);
|
|
4752
|
+
if (!response.ok) {
|
|
4753
|
+
printError(response.body.error ?? `HTTP ${response.status}`);
|
|
4754
|
+
process.exit(1);
|
|
4755
|
+
}
|
|
4756
|
+
if (!isJsonMode()) {
|
|
4757
|
+
printSuccess(
|
|
4758
|
+
`Queued Computer task ${response.body.task?.id ?? taskType}`
|
|
4759
|
+
);
|
|
4760
|
+
}
|
|
4761
|
+
}
|
|
4762
|
+
);
|
|
4763
|
+
}
|
|
4764
|
+
|
|
4765
|
+
// src/commands/template.ts
|
|
4766
|
+
function registerTemplateCommand(program2) {
|
|
4767
|
+
const tpl = program2.command("template").alias("templates").description("Manage agent templates \u2014 reusable configs you spawn agents from.");
|
|
4768
|
+
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));
|
|
4769
|
+
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));
|
|
4770
|
+
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(
|
|
4771
|
+
"after",
|
|
4772
|
+
`
|
|
4773
|
+
Examples:
|
|
4774
|
+
# Capture an existing agent's config as a template
|
|
4775
|
+
$ thinkwork template create "Ops Analyst" --from-agent agt-ops-1
|
|
4776
|
+
|
|
4777
|
+
# Fresh template
|
|
4778
|
+
$ thinkwork template create --system-prompt-file prompts/ops.md --model claude-sonnet-4-6
|
|
4779
|
+
`
|
|
4780
|
+
).action(() => notYetImplemented("template create", 2));
|
|
4781
|
+
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));
|
|
4782
|
+
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));
|
|
4783
|
+
tpl.command("diff <templateId> <agentId>").description("Show what would change if we synced <agentId> to <templateId>.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("template diff", 2));
|
|
4784
|
+
tpl.command("sync-agent <templateId> <agentId>").description("Apply template changes to one linked agent.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("template sync-agent", 2));
|
|
4785
|
+
tpl.command("sync-all <templateId>").description("Apply template changes to every linked agent. Requires confirmation.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").addHelpText(
|
|
4786
|
+
"after",
|
|
4787
|
+
`
|
|
4788
|
+
Examples:
|
|
4789
|
+
# Preview first
|
|
4790
|
+
$ thinkwork template diff tpl-ops agt-ops-1
|
|
4791
|
+
|
|
4792
|
+
# Apply to one agent
|
|
4793
|
+
$ thinkwork template sync-agent tpl-ops agt-ops-1
|
|
4794
|
+
|
|
4795
|
+
# Apply to every linked agent
|
|
4796
|
+
$ thinkwork template sync-all tpl-ops
|
|
4797
|
+
`
|
|
4242
4798
|
).action(() => notYetImplemented("template sync-all", 2));
|
|
4243
4799
|
}
|
|
4244
4800
|
|
|
4245
4801
|
// src/commands/tenant.ts
|
|
4802
|
+
import { createClient as createClient2, tenants as tenantOps, AdminOpsError as AdminOpsError2 } from "@thinkwork/admin-ops";
|
|
4246
4803
|
function registerTenantCommand(program2) {
|
|
4247
4804
|
const tenant = program2.command("tenant").alias("tenants").description("Manage tenants (workspaces) \u2014 create, rename, and configure plans / defaults.");
|
|
4248
|
-
tenant.command("list").alias("ls").description("List tenants the caller can see.").option("-s, --stage <name>", "Deployment stage").
|
|
4249
|
-
|
|
4805
|
+
tenant.command("list").alias("ls").description("List tenants the caller can see.").option("-s, --stage <name>", "Deployment stage").addHelpText(
|
|
4806
|
+
"after",
|
|
4807
|
+
`
|
|
4808
|
+
Examples:
|
|
4809
|
+
$ thinkwork tenant list
|
|
4810
|
+
$ thinkwork tenant list -s dev
|
|
4811
|
+
$ thinkwork tenant list --json | jq '.[].slug'
|
|
4812
|
+
`
|
|
4813
|
+
).action(async (opts) => {
|
|
4814
|
+
try {
|
|
4815
|
+
const stage = await resolveStage({ flag: opts.stage });
|
|
4816
|
+
const api = resolveApiConfig(stage);
|
|
4817
|
+
if (!api) process.exit(1);
|
|
4818
|
+
const client = createClient2({ apiUrl: api.apiUrl, authSecret: api.authSecret });
|
|
4819
|
+
const rows = await tenantOps.listTenants(client);
|
|
4820
|
+
printJson(rows);
|
|
4821
|
+
printTable(rows, [
|
|
4822
|
+
{ key: "slug", header: "SLUG" },
|
|
4823
|
+
{ key: "name", header: "NAME" },
|
|
4824
|
+
{ key: "plan", header: "PLAN" },
|
|
4825
|
+
{ key: "id", header: "ID" }
|
|
4826
|
+
]);
|
|
4827
|
+
} catch (err) {
|
|
4828
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
4829
|
+
process.exit(1);
|
|
4830
|
+
}
|
|
4831
|
+
});
|
|
4832
|
+
tenant.command("get <idOrSlug>").description("Fetch one tenant by ID or slug.").option("-s, --stage <name>", "Deployment stage").addHelpText(
|
|
4833
|
+
"after",
|
|
4834
|
+
`
|
|
4835
|
+
Examples:
|
|
4836
|
+
$ thinkwork tenant get acme
|
|
4837
|
+
$ thinkwork tenant get 0a2b... --json
|
|
4838
|
+
`
|
|
4839
|
+
).action(async (idOrSlug, opts) => {
|
|
4840
|
+
try {
|
|
4841
|
+
const stage = await resolveStage({ flag: opts.stage });
|
|
4842
|
+
const api = resolveApiConfig(stage);
|
|
4843
|
+
if (!api) process.exit(1);
|
|
4844
|
+
const client = createClient2({ apiUrl: api.apiUrl, authSecret: api.authSecret });
|
|
4845
|
+
const isUuid2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
|
|
4846
|
+
idOrSlug
|
|
4847
|
+
);
|
|
4848
|
+
const tenant2 = isUuid2 ? await tenantOps.getTenant(client, idOrSlug) : await tenantOps.getTenantBySlug(client, idOrSlug);
|
|
4849
|
+
printJson(tenant2);
|
|
4850
|
+
printTable([tenant2], [
|
|
4851
|
+
{ key: "slug", header: "SLUG" },
|
|
4852
|
+
{ key: "name", header: "NAME" },
|
|
4853
|
+
{ key: "plan", header: "PLAN" },
|
|
4854
|
+
{ key: "issue_prefix", header: "PREFIX" },
|
|
4855
|
+
{ key: "id", header: "ID" }
|
|
4856
|
+
]);
|
|
4857
|
+
} catch (err) {
|
|
4858
|
+
if (err instanceof AdminOpsError2 && err.status === 404) {
|
|
4859
|
+
printError(`Tenant "${idOrSlug}" not found`);
|
|
4860
|
+
process.exit(2);
|
|
4861
|
+
}
|
|
4862
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
4863
|
+
process.exit(1);
|
|
4864
|
+
}
|
|
4865
|
+
});
|
|
4250
4866
|
tenant.command("create [name]").description("Create a new tenant. The caller becomes its first owner.").option("-s, --stage <name>", "Deployment stage").option("--slug <slug>", "URL-safe slug (lowercase, hyphens). Generated from name if omitted.").option("--plan <plan>", "Plan tier (free, team, enterprise, \u2026)", "team").option("--issue-prefix <prefix>", "Issue-number prefix for thread numbers (e.g. ACME)").addHelpText(
|
|
4251
4867
|
"after",
|
|
4252
4868
|
`
|
|
@@ -4436,29 +5052,218 @@ Examples:
|
|
|
4436
5052
|
wh.command("deliveries <id>").description("Show recent delivery attempts (success/failure, response status).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--limit <n>", "Max rows", "25").action(() => notYetImplemented("webhook deliveries", 3));
|
|
4437
5053
|
}
|
|
4438
5054
|
|
|
4439
|
-
// src/
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
5055
|
+
// src/lib/plugin-zip.ts
|
|
5056
|
+
import { createReadStream, promises as fsp, statSync } from "fs";
|
|
5057
|
+
import { basename, join as join6, relative, resolve as resolve3, sep } from "path";
|
|
5058
|
+
import JSZip from "jszip";
|
|
5059
|
+
var PluginZipError = class extends Error {
|
|
5060
|
+
constructor(message, kind) {
|
|
5061
|
+
super(message);
|
|
5062
|
+
this.kind = kind;
|
|
5063
|
+
this.name = "PluginZipError";
|
|
5064
|
+
}
|
|
5065
|
+
kind;
|
|
5066
|
+
};
|
|
5067
|
+
async function buildPluginZip(pluginDir) {
|
|
5068
|
+
const root = resolve3(pluginDir);
|
|
5069
|
+
let stat;
|
|
5070
|
+
try {
|
|
5071
|
+
stat = await fsp.stat(root);
|
|
5072
|
+
} catch {
|
|
5073
|
+
throw new PluginZipError(
|
|
5074
|
+
`Plugin directory not found: ${pluginDir}`,
|
|
5075
|
+
"missing-directory"
|
|
5076
|
+
);
|
|
5077
|
+
}
|
|
5078
|
+
if (!stat.isDirectory()) {
|
|
5079
|
+
throw new PluginZipError(
|
|
5080
|
+
`Plugin path must be a directory: ${pluginDir}`,
|
|
5081
|
+
"missing-directory"
|
|
5082
|
+
);
|
|
5083
|
+
}
|
|
5084
|
+
const manifestPath = join6(root, "plugin.json");
|
|
5085
|
+
let manifestRaw;
|
|
5086
|
+
try {
|
|
5087
|
+
manifestRaw = await fsp.readFile(manifestPath, "utf8");
|
|
5088
|
+
} catch {
|
|
5089
|
+
throw new PluginZipError(
|
|
5090
|
+
`plugin.json is required at the root of the plugin folder (expected ${manifestPath}).`,
|
|
5091
|
+
"missing-plugin-json"
|
|
5092
|
+
);
|
|
5093
|
+
}
|
|
5094
|
+
let parsed;
|
|
5095
|
+
try {
|
|
5096
|
+
parsed = JSON.parse(manifestRaw);
|
|
5097
|
+
} catch (err) {
|
|
5098
|
+
throw new PluginZipError(
|
|
5099
|
+
`plugin.json is not valid JSON: ${err.message}`,
|
|
5100
|
+
"invalid-plugin-json"
|
|
5101
|
+
);
|
|
5102
|
+
}
|
|
5103
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
5104
|
+
throw new PluginZipError(
|
|
5105
|
+
`plugin.json must be a JSON object with a "name" field.`,
|
|
5106
|
+
"invalid-plugin-json"
|
|
5107
|
+
);
|
|
5108
|
+
}
|
|
5109
|
+
const manifest = parsed;
|
|
5110
|
+
if (typeof manifest.name !== "string" || manifest.name.trim() === "") {
|
|
5111
|
+
throw new PluginZipError(
|
|
5112
|
+
`plugin.json must declare a non-empty "name" string.`,
|
|
5113
|
+
"invalid-plugin-json"
|
|
5114
|
+
);
|
|
5115
|
+
}
|
|
5116
|
+
const zip = new JSZip();
|
|
5117
|
+
const entries = await walkDir(root, root);
|
|
5118
|
+
for (const entry of entries) {
|
|
5119
|
+
if (entry.isSymbolicLink) {
|
|
5120
|
+
throw new PluginZipError(
|
|
5121
|
+
`Refusing to zip symlink: ${entry.relPath} (would be rejected server-side).`,
|
|
5122
|
+
"unsafe-entry"
|
|
5123
|
+
);
|
|
5124
|
+
}
|
|
5125
|
+
if (hasParentSegment(entry.relPath)) {
|
|
5126
|
+
throw new PluginZipError(
|
|
5127
|
+
`Refusing to zip path with traversal segment: ${entry.relPath}`,
|
|
5128
|
+
"unsafe-entry"
|
|
5129
|
+
);
|
|
5130
|
+
}
|
|
5131
|
+
const archivePath = entry.relPath.split(sep).join("/");
|
|
5132
|
+
zip.file(archivePath, createReadStream(entry.absPath));
|
|
5133
|
+
}
|
|
5134
|
+
let buffer;
|
|
5135
|
+
try {
|
|
5136
|
+
buffer = await zip.generateAsync({
|
|
5137
|
+
type: "nodebuffer",
|
|
5138
|
+
compression: "DEFLATE",
|
|
5139
|
+
compressionOptions: { level: 6 }
|
|
5140
|
+
});
|
|
5141
|
+
} catch (err) {
|
|
5142
|
+
throw new PluginZipError(
|
|
5143
|
+
`Failed to compress plugin: ${err.message}`,
|
|
5144
|
+
"io"
|
|
5145
|
+
);
|
|
5146
|
+
}
|
|
5147
|
+
const metadata = {
|
|
5148
|
+
name: manifest.name.trim(),
|
|
5149
|
+
version: typeof manifest.version === "string" && manifest.version.trim() !== "" ? manifest.version.trim() : void 0,
|
|
5150
|
+
description: typeof manifest.description === "string" ? manifest.description.trim() || void 0 : void 0
|
|
5151
|
+
};
|
|
5152
|
+
return {
|
|
5153
|
+
buffer,
|
|
5154
|
+
plugin: metadata,
|
|
5155
|
+
fileCount: entries.length,
|
|
5156
|
+
zipFileName: `${basename(root)}.zip`
|
|
5157
|
+
};
|
|
5158
|
+
}
|
|
5159
|
+
async function walkDir(rootDir, currentDir) {
|
|
5160
|
+
const out = [];
|
|
5161
|
+
const dirents = await fsp.readdir(currentDir, { withFileTypes: true });
|
|
5162
|
+
for (const ent of dirents) {
|
|
5163
|
+
const abs = join6(currentDir, ent.name);
|
|
5164
|
+
const rel = relative(rootDir, abs);
|
|
5165
|
+
if (ent.name === ".git" || ent.name === ".DS_Store" || ent.name === "node_modules") {
|
|
5166
|
+
continue;
|
|
5167
|
+
}
|
|
5168
|
+
if (ent.isSymbolicLink()) {
|
|
5169
|
+
out.push({ absPath: abs, relPath: rel, isSymbolicLink: true });
|
|
5170
|
+
continue;
|
|
5171
|
+
}
|
|
5172
|
+
if (ent.isDirectory()) {
|
|
5173
|
+
out.push(...await walkDir(rootDir, abs));
|
|
5174
|
+
continue;
|
|
5175
|
+
}
|
|
5176
|
+
if (ent.isFile()) {
|
|
5177
|
+
try {
|
|
5178
|
+
statSync(abs);
|
|
5179
|
+
} catch {
|
|
5180
|
+
continue;
|
|
5181
|
+
}
|
|
5182
|
+
out.push({ absPath: abs, relPath: rel, isSymbolicLink: false });
|
|
5183
|
+
}
|
|
5184
|
+
}
|
|
5185
|
+
return out.sort(
|
|
5186
|
+
(a, b) => a.relPath < b.relPath ? -1 : a.relPath > b.relPath ? 1 : 0
|
|
5187
|
+
);
|
|
5188
|
+
}
|
|
5189
|
+
function hasParentSegment(relPath) {
|
|
5190
|
+
const parts = relPath.split(sep);
|
|
5191
|
+
return parts.some((p) => p === "..");
|
|
5192
|
+
}
|
|
4450
5193
|
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
5194
|
+
// src/lib/plugin-push.ts
|
|
5195
|
+
async function pushPluginZip(input4) {
|
|
5196
|
+
const base = input4.apiUrl.replace(/\/+$/, "");
|
|
5197
|
+
const presignRes = await fetch(`${base}/api/plugins/presign`, {
|
|
5198
|
+
method: "POST",
|
|
5199
|
+
headers: withJson(input4.headers),
|
|
5200
|
+
body: JSON.stringify({ fileName: input4.fileName })
|
|
5201
|
+
});
|
|
5202
|
+
if (!presignRes.ok) {
|
|
5203
|
+
throw new Error(`presign failed: ${await describeHttpError(presignRes)}`);
|
|
5204
|
+
}
|
|
5205
|
+
const presign = await presignRes.json();
|
|
5206
|
+
if (!presign.uploadUrl || !presign.s3Key) {
|
|
5207
|
+
throw new Error(
|
|
5208
|
+
`presign returned invalid response: ${JSON.stringify(presign)}`
|
|
5209
|
+
);
|
|
5210
|
+
}
|
|
5211
|
+
const putRes = await fetch(presign.uploadUrl, {
|
|
5212
|
+
method: "PUT",
|
|
5213
|
+
headers: { "Content-Type": "application/zip" },
|
|
5214
|
+
body: new Uint8Array(input4.zipBuffer)
|
|
5215
|
+
});
|
|
5216
|
+
if (!putRes.ok) {
|
|
5217
|
+
throw new Error(`S3 PUT failed: HTTP ${putRes.status}`);
|
|
5218
|
+
}
|
|
5219
|
+
const installRes = await fetch(`${base}/api/plugins/upload`, {
|
|
5220
|
+
method: "POST",
|
|
5221
|
+
headers: withJson(input4.headers),
|
|
5222
|
+
body: JSON.stringify({ s3Key: presign.s3Key })
|
|
5223
|
+
});
|
|
5224
|
+
const installBody = await installRes.json().catch(() => ({}));
|
|
5225
|
+
if (installRes.status === 400 && installBody && installBody.valid === false) {
|
|
5226
|
+
return {
|
|
5227
|
+
status: "validation-failed",
|
|
5228
|
+
errors: installBody.errors ?? [],
|
|
5229
|
+
warnings: installBody.warnings ?? []
|
|
5230
|
+
};
|
|
5231
|
+
}
|
|
5232
|
+
if (!installRes.ok) {
|
|
5233
|
+
const uploadId = typeof installBody.uploadId === "string" ? installBody.uploadId : "";
|
|
5234
|
+
return {
|
|
5235
|
+
status: "failed",
|
|
5236
|
+
uploadId,
|
|
5237
|
+
phase: typeof installBody.phase === "string" ? installBody.phase : void 0,
|
|
5238
|
+
errorMessage: typeof installBody.errorMessage === "string" && installBody.errorMessage || typeof installBody.error === "string" && installBody.error || `HTTP ${installRes.status}`
|
|
5239
|
+
};
|
|
5240
|
+
}
|
|
5241
|
+
const plugin = installBody.plugin;
|
|
5242
|
+
if (!plugin || typeof installBody.uploadId !== "string") {
|
|
5243
|
+
throw new Error(
|
|
5244
|
+
`install response missing uploadId/plugin: ${JSON.stringify(installBody)}`
|
|
5245
|
+
);
|
|
5246
|
+
}
|
|
5247
|
+
return {
|
|
5248
|
+
status: "installed",
|
|
5249
|
+
uploadId: installBody.uploadId,
|
|
5250
|
+
plugin,
|
|
5251
|
+
warnings: Array.isArray(installBody.warnings) ? installBody.warnings : []
|
|
5252
|
+
};
|
|
5253
|
+
}
|
|
5254
|
+
function withJson(headers) {
|
|
5255
|
+
return { "Content-Type": "application/json", ...headers };
|
|
5256
|
+
}
|
|
5257
|
+
async function describeHttpError(res) {
|
|
5258
|
+
const text = await res.text().catch(() => "");
|
|
5259
|
+
return `HTTP ${res.status} ${text.slice(0, 200)}`;
|
|
4457
5260
|
}
|
|
4458
5261
|
|
|
4459
5262
|
// src/commands/skill.ts
|
|
4460
5263
|
function registerSkillCommand(program2) {
|
|
4461
|
-
const skill = program2.command("skill").alias("skills").description(
|
|
5264
|
+
const skill = program2.command("skill").alias("skills").description(
|
|
5265
|
+
"Browse the catalog, install, upgrade, or publish custom skills."
|
|
5266
|
+
);
|
|
4462
5267
|
skill.command("catalog").description("Browse the public skill catalog (not tenant-scoped).").option("-s, --stage <name>", "Deployment stage").option("--search <q>", "Filter by keyword").option("--tag <t>", "Filter by tag").action(() => notYetImplemented("skill catalog", 3));
|
|
4463
5268
|
skill.command("list").alias("ls").description("List skills installed / published in the current tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--custom-only", "Only show tenant-owned custom skills").action(() => notYetImplemented("skill list", 3));
|
|
4464
5269
|
skill.command("install <slug>").description("Install a public skill into the tenant. Idempotent.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--version <v>", "Pin to a specific version (default: latest)").addHelpText(
|
|
@@ -4470,7 +5275,9 @@ Examples:
|
|
|
4470
5275
|
`
|
|
4471
5276
|
).action(() => notYetImplemented("skill install", 3));
|
|
4472
5277
|
skill.command("upgrade <slug>").description("Upgrade an installed skill to the latest catalog version.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("skill upgrade", 3));
|
|
4473
|
-
skill.command("create [slug]").description(
|
|
5278
|
+
skill.command("create [slug]").description(
|
|
5279
|
+
"Publish a custom tenant-scoped skill (walkthrough for missing fields in TTY)."
|
|
5280
|
+
).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--name <n>").option("--description <text>").option("--manifest-file <path>", "Path to the MCP server manifest JSON").option("--endpoint <url>", "MCP server HTTP/SSE endpoint").addHelpText(
|
|
4474
5281
|
"after",
|
|
4475
5282
|
`
|
|
4476
5283
|
Examples:
|
|
@@ -4479,7 +5286,103 @@ Examples:
|
|
|
4479
5286
|
`
|
|
4480
5287
|
).action(() => notYetImplemented("skill create", 3));
|
|
4481
5288
|
skill.command("update <slug>").description("Update a custom skill's manifest, endpoint, or description.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--name <n>").option("--description <text>").option("--manifest-file <path>").option("--endpoint <url>").action(() => notYetImplemented("skill update", 3));
|
|
4482
|
-
skill.command("delete <slug>").description(
|
|
5289
|
+
skill.command("delete <slug>").description(
|
|
5290
|
+
"Delete a custom skill. Public catalog skills are uninstalled via this too."
|
|
5291
|
+
).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("skill delete", 3));
|
|
5292
|
+
skill.command("push <folder>").description(
|
|
5293
|
+
"Zip a local plugin folder and upload it to the tenant as a pending plugin."
|
|
5294
|
+
).option("-s, --stage <name>", "Deployment stage").option("--region <name>", "AWS region", "us-east-1").addHelpText(
|
|
5295
|
+
"after",
|
|
5296
|
+
`
|
|
5297
|
+
Examples:
|
|
5298
|
+
$ thinkwork skill push ./my-plugin
|
|
5299
|
+
$ thinkwork skill push ./my-plugin --stage dev
|
|
5300
|
+
|
|
5301
|
+
The folder must contain a plugin.json manifest. MCP servers shipped
|
|
5302
|
+
inside the plugin will land as 'pending' and need admin approval
|
|
5303
|
+
under Capabilities \u2192 MCP Servers before agents can invoke them.
|
|
5304
|
+
`
|
|
5305
|
+
).action(
|
|
5306
|
+
async (folder, opts) => {
|
|
5307
|
+
await runPushCommand(folder, opts);
|
|
5308
|
+
}
|
|
5309
|
+
);
|
|
5310
|
+
}
|
|
5311
|
+
async function runPushCommand(folder, opts) {
|
|
5312
|
+
const region = opts.region ?? "us-east-1";
|
|
5313
|
+
const stage = await resolveStage({ flag: opts.stage, region });
|
|
5314
|
+
let zipped;
|
|
5315
|
+
try {
|
|
5316
|
+
zipped = await buildPluginZip(folder);
|
|
5317
|
+
} catch (err) {
|
|
5318
|
+
if (err instanceof PluginZipError) {
|
|
5319
|
+
printError(err.message);
|
|
5320
|
+
process.exit(1);
|
|
5321
|
+
}
|
|
5322
|
+
throw err;
|
|
5323
|
+
}
|
|
5324
|
+
const auth = await resolveAuth({ stage, region, requireCognito: true });
|
|
5325
|
+
if (auth.mode !== "cognito") {
|
|
5326
|
+
printError(
|
|
5327
|
+
`skill push requires a Cognito session. Run \`thinkwork login --stage ${stage}\`.`
|
|
5328
|
+
);
|
|
5329
|
+
process.exit(1);
|
|
5330
|
+
}
|
|
5331
|
+
const apiUrl = getApiEndpoint(stage, region);
|
|
5332
|
+
if (!apiUrl) {
|
|
5333
|
+
printError(
|
|
5334
|
+
`Could not discover API endpoint for stage "${stage}" in ${region}. Is the stack deployed?`
|
|
5335
|
+
);
|
|
5336
|
+
process.exit(1);
|
|
5337
|
+
}
|
|
5338
|
+
printSuccess(
|
|
5339
|
+
`Prepared plugin "${zipped.plugin.name}" \u2014 ${zipped.fileCount} file(s), ${formatBytes(zipped.buffer.length)}`
|
|
5340
|
+
);
|
|
5341
|
+
let result;
|
|
5342
|
+
try {
|
|
5343
|
+
result = await pushPluginZip({
|
|
5344
|
+
apiUrl,
|
|
5345
|
+
headers: auth.headers,
|
|
5346
|
+
zipBuffer: zipped.buffer,
|
|
5347
|
+
fileName: zipped.zipFileName
|
|
5348
|
+
});
|
|
5349
|
+
} catch (err) {
|
|
5350
|
+
printError(`Upload failed: ${err.message}`);
|
|
5351
|
+
process.exit(1);
|
|
5352
|
+
}
|
|
5353
|
+
if (result.status === "validation-failed") {
|
|
5354
|
+
printError("Plugin validation failed");
|
|
5355
|
+
for (const e of result.errors) console.log(` - ${e}`);
|
|
5356
|
+
for (const w of result.warnings) printWarning(w);
|
|
5357
|
+
process.exit(1);
|
|
5358
|
+
}
|
|
5359
|
+
if (result.status === "failed") {
|
|
5360
|
+
printError(
|
|
5361
|
+
`Install failed${result.phase ? ` at phase ${result.phase}` : ""}: ${result.errorMessage}`
|
|
5362
|
+
);
|
|
5363
|
+
if (result.uploadId) {
|
|
5364
|
+
console.log(` upload id: ${result.uploadId}`);
|
|
5365
|
+
}
|
|
5366
|
+
process.exit(1);
|
|
5367
|
+
}
|
|
5368
|
+
const skillCount = result.plugin.skills.length;
|
|
5369
|
+
const mcpCount = result.plugin.mcpServers.length;
|
|
5370
|
+
printSuccess(
|
|
5371
|
+
`Installed "${result.plugin.name}" \u2014 ${skillCount} skill(s)` + (mcpCount > 0 ? `, ${mcpCount} MCP server(s) pending admin approval` : "")
|
|
5372
|
+
);
|
|
5373
|
+
console.log(` upload id: ${result.uploadId}`);
|
|
5374
|
+
if (mcpCount > 0) {
|
|
5375
|
+
console.log(
|
|
5376
|
+
` approve at: admin SPA \u2192 Capabilities \u2192 MCP Servers (filter: status=pending)`
|
|
5377
|
+
);
|
|
5378
|
+
}
|
|
5379
|
+
for (const w of result.warnings) printWarning(w);
|
|
5380
|
+
}
|
|
5381
|
+
function formatBytes(bytes) {
|
|
5382
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
5383
|
+
const kb = bytes / 1024;
|
|
5384
|
+
if (kb < 1024) return `${kb.toFixed(1)} KB`;
|
|
5385
|
+
return `${(kb / 1024).toFixed(2)} MB`;
|
|
4483
5386
|
}
|
|
4484
5387
|
|
|
4485
5388
|
// src/commands/memory.ts
|
|
@@ -4612,6 +5515,1890 @@ Examples:
|
|
|
4612
5515
|
).action(() => notYetImplemented("dashboard", 5));
|
|
4613
5516
|
}
|
|
4614
5517
|
|
|
5518
|
+
// src/commands/eval/run.ts
|
|
5519
|
+
import { checkbox, confirm as confirm2 } from "@inquirer/prompts";
|
|
5520
|
+
import ora2 from "ora";
|
|
5521
|
+
|
|
5522
|
+
// src/gql/graphql.ts
|
|
5523
|
+
var CliEvalRunsDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "CliEvalRuns" }, "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": "agentId" } }, "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "ID" } } }, { "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "limit" } }, "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "Int" } } }, { "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "offset" } }, "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "Int" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "evalRuns" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "tenantId" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "tenantId" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "agentId" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "agentId" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "limit" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "limit" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "offset" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "offset" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "totalCount" } }, { "kind": "Field", "name": { "kind": "Name", "value": "items" }, "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": "agentId" } }, { "kind": "Field", "name": { "kind": "Name", "value": "agentName" } }, { "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": "passed" } }, { "kind": "Field", "name": { "kind": "Name", "value": "failed" } }, { "kind": "Field", "name": { "kind": "Name", "value": "passRate" } }, { "kind": "Field", "name": { "kind": "Name", "value": "regression" } }, { "kind": "Field", "name": { "kind": "Name", "value": "costUsd" } }, { "kind": "Field", "name": { "kind": "Name", "value": "errorMessage" } }, { "kind": "Field", "name": { "kind": "Name", "value": "startedAt" } }, { "kind": "Field", "name": { "kind": "Name", "value": "completedAt" } }, { "kind": "Field", "name": { "kind": "Name", "value": "createdAt" } }] } }] } }] } }] };
|
|
5524
|
+
var CliEvalRunDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "CliEvalRun" }, "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": "evalRun" }, "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": "status" } }, { "kind": "Field", "name": { "kind": "Name", "value": "model" } }, { "kind": "Field", "name": { "kind": "Name", "value": "categories" } }, { "kind": "Field", "name": { "kind": "Name", "value": "agentId" } }, { "kind": "Field", "name": { "kind": "Name", "value": "agentName" } }, { "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": "passed" } }, { "kind": "Field", "name": { "kind": "Name", "value": "failed" } }, { "kind": "Field", "name": { "kind": "Name", "value": "passRate" } }, { "kind": "Field", "name": { "kind": "Name", "value": "regression" } }, { "kind": "Field", "name": { "kind": "Name", "value": "costUsd" } }, { "kind": "Field", "name": { "kind": "Name", "value": "errorMessage" } }, { "kind": "Field", "name": { "kind": "Name", "value": "startedAt" } }, { "kind": "Field", "name": { "kind": "Name", "value": "completedAt" } }, { "kind": "Field", "name": { "kind": "Name", "value": "createdAt" } }] } }] } }] };
|
|
5525
|
+
var CliEvalRunResultsDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "CliEvalRunResults" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "runId" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "ID" } } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "evalRunResults" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "runId" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "runId" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "testCaseId" } }, { "kind": "Field", "name": { "kind": "Name", "value": "testCaseName" } }, { "kind": "Field", "name": { "kind": "Name", "value": "category" } }, { "kind": "Field", "name": { "kind": "Name", "value": "status" } }, { "kind": "Field", "name": { "kind": "Name", "value": "score" } }, { "kind": "Field", "name": { "kind": "Name", "value": "durationMs" } }, { "kind": "Field", "name": { "kind": "Name", "value": "agentSessionId" } }, { "kind": "Field", "name": { "kind": "Name", "value": "input" } }, { "kind": "Field", "name": { "kind": "Name", "value": "expected" } }, { "kind": "Field", "name": { "kind": "Name", "value": "actualOutput" } }, { "kind": "Field", "name": { "kind": "Name", "value": "evaluatorResults" } }, { "kind": "Field", "name": { "kind": "Name", "value": "assertions" } }, { "kind": "Field", "name": { "kind": "Name", "value": "errorMessage" } }, { "kind": "Field", "name": { "kind": "Name", "value": "createdAt" } }] } }] } }] };
|
|
5526
|
+
var CliEvalTestCasesDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "CliEvalTestCases" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "tenantId" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "ID" } } } }, { "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "category" } }, "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "String" } } }, { "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "search" } }, "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "String" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "evalTestCases" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "tenantId" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "tenantId" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "category" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "category" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "search" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "search" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "name" } }, { "kind": "Field", "name": { "kind": "Name", "value": "category" } }, { "kind": "Field", "name": { "kind": "Name", "value": "query" } }, { "kind": "Field", "name": { "kind": "Name", "value": "systemPrompt" } }, { "kind": "Field", "name": { "kind": "Name", "value": "agentTemplateId" } }, { "kind": "Field", "name": { "kind": "Name", "value": "agentTemplateName" } }, { "kind": "Field", "name": { "kind": "Name", "value": "agentcoreEvaluatorIds" } }, { "kind": "Field", "name": { "kind": "Name", "value": "tags" } }, { "kind": "Field", "name": { "kind": "Name", "value": "enabled" } }, { "kind": "Field", "name": { "kind": "Name", "value": "source" } }, { "kind": "Field", "name": { "kind": "Name", "value": "createdAt" } }, { "kind": "Field", "name": { "kind": "Name", "value": "updatedAt" } }] } }] } }] };
|
|
5527
|
+
var CliEvalTestCaseDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "CliEvalTestCase" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "id" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "ID" } } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "evalTestCase" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "id" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "id" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "tenantId" } }, { "kind": "Field", "name": { "kind": "Name", "value": "name" } }, { "kind": "Field", "name": { "kind": "Name", "value": "category" } }, { "kind": "Field", "name": { "kind": "Name", "value": "query" } }, { "kind": "Field", "name": { "kind": "Name", "value": "systemPrompt" } }, { "kind": "Field", "name": { "kind": "Name", "value": "agentTemplateId" } }, { "kind": "Field", "name": { "kind": "Name", "value": "agentTemplateName" } }, { "kind": "Field", "name": { "kind": "Name", "value": "assertions" } }, { "kind": "Field", "name": { "kind": "Name", "value": "agentcoreEvaluatorIds" } }, { "kind": "Field", "name": { "kind": "Name", "value": "tags" } }, { "kind": "Field", "name": { "kind": "Name", "value": "enabled" } }, { "kind": "Field", "name": { "kind": "Name", "value": "source" } }, { "kind": "Field", "name": { "kind": "Name", "value": "createdAt" } }, { "kind": "Field", "name": { "kind": "Name", "value": "updatedAt" } }] } }] } }] };
|
|
5528
|
+
var CliComputersForEvalDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "CliComputersForEval" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "tenantId" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "ID" } } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "computers" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "tenantId" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "tenantId" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "name" } }, { "kind": "Field", "name": { "kind": "Name", "value": "slug" } }, { "kind": "Field", "name": { "kind": "Name", "value": "runtimeStatus" } }] } }] } }] };
|
|
5529
|
+
var CliAgentTemplatesForEvalDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "CliAgentTemplatesForEval" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "tenantId" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "ID" } } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "agentTemplates" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "tenantId" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "tenantId" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "name" } }, { "kind": "Field", "name": { "kind": "Name", "value": "slug" } }, { "kind": "Field", "name": { "kind": "Name", "value": "model" } }, { "kind": "Field", "name": { "kind": "Name", "value": "isPublished" } }] } }] } }] };
|
|
5530
|
+
var CliTenantBySlugDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "CliTenantBySlug" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "slug" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "String" } } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "tenantBySlug" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "slug" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "slug" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "slug" } }, { "kind": "Field", "name": { "kind": "Name", "value": "name" } }] } }] } }] };
|
|
5531
|
+
var CliStartEvalRunDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "mutation", "name": { "kind": "Name", "value": "CliStartEvalRun" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "tenantId" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "ID" } } } }, { "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "input" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "StartEvalRunInput" } } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "startEvalRun" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "tenantId" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "tenantId" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "input" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "input" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "status" } }, { "kind": "Field", "name": { "kind": "Name", "value": "model" } }, { "kind": "Field", "name": { "kind": "Name", "value": "categories" } }, { "kind": "Field", "name": { "kind": "Name", "value": "agentTemplateId" } }, { "kind": "Field", "name": { "kind": "Name", "value": "agentTemplateName" } }, { "kind": "Field", "name": { "kind": "Name", "value": "totalTests" } }, { "kind": "Field", "name": { "kind": "Name", "value": "createdAt" } }] } }] } }] };
|
|
5532
|
+
var CliCancelEvalRunDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "mutation", "name": { "kind": "Name", "value": "CliCancelEvalRun" }, "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": "cancelEvalRun" }, "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": "status" } }, { "kind": "Field", "name": { "kind": "Name", "value": "completedAt" } }] } }] } }] };
|
|
5533
|
+
var CliDeleteEvalRunDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "mutation", "name": { "kind": "Name", "value": "CliDeleteEvalRun" }, "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": "deleteEvalRun" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "id" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "id" } } }] }] } }] };
|
|
5534
|
+
var CliCreateEvalTestCaseDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "mutation", "name": { "kind": "Name", "value": "CliCreateEvalTestCase" }, "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": "CreateEvalTestCaseInput" } } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "createEvalTestCase" }, "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": "name" } }, { "kind": "Field", "name": { "kind": "Name", "value": "category" } }] } }] } }] };
|
|
5535
|
+
var CliUpdateEvalTestCaseDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "mutation", "name": { "kind": "Name", "value": "CliUpdateEvalTestCase" }, "variableDefinitions": [{ "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "id" } }, "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": "UpdateEvalTestCaseInput" } } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "updateEvalTestCase" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "id" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "id" } } }, { "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": "name" } }, { "kind": "Field", "name": { "kind": "Name", "value": "category" } }, { "kind": "Field", "name": { "kind": "Name", "value": "enabled" } }] } }] } }] };
|
|
5536
|
+
var CliDeleteEvalTestCaseDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "mutation", "name": { "kind": "Name", "value": "CliDeleteEvalTestCase" }, "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": "deleteEvalTestCase" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "id" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "id" } } }] }] } }] };
|
|
5537
|
+
var CliSeedEvalTestCasesDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "mutation", "name": { "kind": "Name", "value": "CliSeedEvalTestCases" }, "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": "categories" } }, "type": { "kind": "ListType", "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "String" } } } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "seedEvalTestCases" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "tenantId" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "tenantId" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "categories" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "categories" } } }] }] } }] };
|
|
5538
|
+
var CliMeDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "CliMe" }, "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "me" }, "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "email" } }, { "kind": "Field", "name": { "kind": "Name", "value": "name" } }, { "kind": "Field", "name": { "kind": "Name", "value": "tenantId" } }] } }] } }] };
|
|
5539
|
+
var CliWikiTenantBySlugDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "CliWikiTenantBySlug" }, "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" } }] } }] } }] };
|
|
5540
|
+
var CliAllTenantAgentsForWikiDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "CliAllTenantAgentsForWiki" }, "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": "allTenantAgents" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "tenantId" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "tenantId" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "includeSystem" }, "value": { "kind": "BooleanValue", "value": false } }, { "kind": "Argument", "name": { "kind": "Name", "value": "includeSubAgents" }, "value": { "kind": "BooleanValue", "value": false } }], "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": "type" } }, { "kind": "Field", "name": { "kind": "Name", "value": "status" } }] } }] } }] };
|
|
5541
|
+
var CliCompileWikiNowDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "mutation", "name": { "kind": "Name", "value": "CliCompileWikiNow" }, "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": "ownerId" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "ID" } } } }, { "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "modelId" } }, "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "String" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "compileWikiNow" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "tenantId" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "tenantId" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "ownerId" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "ownerId" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "modelId" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "modelId" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "tenantId" } }, { "kind": "Field", "name": { "kind": "Name", "value": "ownerId" } }, { "kind": "Field", "name": { "kind": "Name", "value": "status" } }, { "kind": "Field", "name": { "kind": "Name", "value": "trigger" } }, { "kind": "Field", "name": { "kind": "Name", "value": "dedupeKey" } }, { "kind": "Field", "name": { "kind": "Name", "value": "attempt" } }, { "kind": "Field", "name": { "kind": "Name", "value": "createdAt" } }] } }] } }] };
|
|
5542
|
+
var CliResetWikiCursorDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "mutation", "name": { "kind": "Name", "value": "CliResetWikiCursor" }, "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": "ownerId" } }, "type": { "kind": "NonNullType", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "ID" } } } }, { "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "force" } }, "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "Boolean" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "resetWikiCursor" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "tenantId" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "tenantId" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "ownerId" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "ownerId" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "force" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "force" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "tenantId" } }, { "kind": "Field", "name": { "kind": "Name", "value": "ownerId" } }, { "kind": "Field", "name": { "kind": "Name", "value": "cursorCleared" } }, { "kind": "Field", "name": { "kind": "Name", "value": "pagesArchived" } }] } }] } }] };
|
|
5543
|
+
var CliWikiCompileJobsDocument = { "kind": "Document", "definitions": [{ "kind": "OperationDefinition", "operation": "query", "name": { "kind": "Name", "value": "CliWikiCompileJobs" }, "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": "ownerId" } }, "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "ID" } } }, { "kind": "VariableDefinition", "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "limit" } }, "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "Int" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "wikiCompileJobs" }, "arguments": [{ "kind": "Argument", "name": { "kind": "Name", "value": "tenantId" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "tenantId" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "ownerId" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "ownerId" } } }, { "kind": "Argument", "name": { "kind": "Name", "value": "limit" }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "limit" } } }], "selectionSet": { "kind": "SelectionSet", "selections": [{ "kind": "Field", "name": { "kind": "Name", "value": "id" } }, { "kind": "Field", "name": { "kind": "Name", "value": "tenantId" } }, { "kind": "Field", "name": { "kind": "Name", "value": "ownerId" } }, { "kind": "Field", "name": { "kind": "Name", "value": "status" } }, { "kind": "Field", "name": { "kind": "Name", "value": "trigger" } }, { "kind": "Field", "name": { "kind": "Name", "value": "dedupeKey" } }, { "kind": "Field", "name": { "kind": "Name", "value": "attempt" } }, { "kind": "Field", "name": { "kind": "Name", "value": "claimedAt" } }, { "kind": "Field", "name": { "kind": "Name", "value": "startedAt" } }, { "kind": "Field", "name": { "kind": "Name", "value": "finishedAt" } }, { "kind": "Field", "name": { "kind": "Name", "value": "error" } }, { "kind": "Field", "name": { "kind": "Name", "value": "metrics" } }, { "kind": "Field", "name": { "kind": "Name", "value": "createdAt" } }] } }] } }] };
|
|
5544
|
+
|
|
5545
|
+
// src/gql/gql.ts
|
|
5546
|
+
var documents = {
|
|
5547
|
+
"\n query CliEvalRuns($tenantId: ID!, $agentId: ID, $limit: Int, $offset: Int) {\n evalRuns(\n tenantId: $tenantId\n agentId: $agentId\n limit: $limit\n offset: $offset\n ) {\n totalCount\n items {\n id\n status\n model\n categories\n agentId\n agentName\n agentTemplateId\n agentTemplateName\n totalTests\n passed\n failed\n passRate\n regression\n costUsd\n errorMessage\n startedAt\n completedAt\n createdAt\n }\n }\n }\n": CliEvalRunsDocument,
|
|
5548
|
+
"\n query CliEvalRun($id: ID!) {\n evalRun(id: $id) {\n id\n status\n model\n categories\n agentId\n agentName\n agentTemplateId\n agentTemplateName\n totalTests\n passed\n failed\n passRate\n regression\n costUsd\n errorMessage\n startedAt\n completedAt\n createdAt\n }\n }\n": CliEvalRunDocument,
|
|
5549
|
+
"\n query CliEvalRunResults($runId: ID!) {\n evalRunResults(runId: $runId) {\n id\n testCaseId\n testCaseName\n category\n status\n score\n durationMs\n agentSessionId\n input\n expected\n actualOutput\n evaluatorResults\n assertions\n errorMessage\n createdAt\n }\n }\n": CliEvalRunResultsDocument,
|
|
5550
|
+
"\n query CliEvalTestCases($tenantId: ID!, $category: String, $search: String) {\n evalTestCases(tenantId: $tenantId, category: $category, search: $search) {\n id\n name\n category\n query\n systemPrompt\n agentTemplateId\n agentTemplateName\n agentcoreEvaluatorIds\n tags\n enabled\n source\n createdAt\n updatedAt\n }\n }\n": CliEvalTestCasesDocument,
|
|
5551
|
+
"\n query CliEvalTestCase($id: ID!) {\n evalTestCase(id: $id) {\n id\n tenantId\n name\n category\n query\n systemPrompt\n agentTemplateId\n agentTemplateName\n assertions\n agentcoreEvaluatorIds\n tags\n enabled\n source\n createdAt\n updatedAt\n }\n }\n": CliEvalTestCaseDocument,
|
|
5552
|
+
"\n query CliComputersForEval($tenantId: ID!) {\n computers(tenantId: $tenantId) {\n id\n name\n slug\n runtimeStatus\n }\n }\n": CliComputersForEvalDocument,
|
|
5553
|
+
"\n query CliAgentTemplatesForEval($tenantId: ID!) {\n agentTemplates(tenantId: $tenantId) {\n id\n name\n slug\n model\n isPublished\n }\n }\n": CliAgentTemplatesForEvalDocument,
|
|
5554
|
+
"\n query CliTenantBySlug($slug: String!) {\n tenantBySlug(slug: $slug) {\n id\n slug\n name\n }\n }\n": CliTenantBySlugDocument,
|
|
5555
|
+
"\n mutation CliStartEvalRun($tenantId: ID!, $input: StartEvalRunInput!) {\n startEvalRun(tenantId: $tenantId, input: $input) {\n id\n status\n model\n categories\n agentTemplateId\n agentTemplateName\n totalTests\n createdAt\n }\n }\n": CliStartEvalRunDocument,
|
|
5556
|
+
"\n mutation CliCancelEvalRun($id: ID!) {\n cancelEvalRun(id: $id) {\n id\n status\n completedAt\n }\n }\n": CliCancelEvalRunDocument,
|
|
5557
|
+
"\n mutation CliDeleteEvalRun($id: ID!) {\n deleteEvalRun(id: $id)\n }\n": CliDeleteEvalRunDocument,
|
|
5558
|
+
"\n mutation CliCreateEvalTestCase(\n $tenantId: ID!\n $input: CreateEvalTestCaseInput!\n ) {\n createEvalTestCase(tenantId: $tenantId, input: $input) {\n id\n name\n category\n }\n }\n": CliCreateEvalTestCaseDocument,
|
|
5559
|
+
"\n mutation CliUpdateEvalTestCase($id: ID!, $input: UpdateEvalTestCaseInput!) {\n updateEvalTestCase(id: $id, input: $input) {\n id\n name\n category\n enabled\n }\n }\n": CliUpdateEvalTestCaseDocument,
|
|
5560
|
+
"\n mutation CliDeleteEvalTestCase($id: ID!) {\n deleteEvalTestCase(id: $id)\n }\n": CliDeleteEvalTestCaseDocument,
|
|
5561
|
+
"\n mutation CliSeedEvalTestCases($tenantId: ID!, $categories: [String!]) {\n seedEvalTestCases(tenantId: $tenantId, categories: $categories)\n }\n": CliSeedEvalTestCasesDocument,
|
|
5562
|
+
"\n query CliMe {\n me {\n id\n email\n name\n tenantId\n }\n }\n": CliMeDocument,
|
|
5563
|
+
"\n query CliWikiTenantBySlug($slug: String!) {\n tenantBySlug(slug: $slug) {\n id\n slug\n name\n }\n }\n": CliWikiTenantBySlugDocument,
|
|
5564
|
+
"\n query CliAllTenantAgentsForWiki($tenantId: ID!) {\n allTenantAgents(tenantId: $tenantId, includeSystem: false, includeSubAgents: false) {\n id\n name\n slug\n type\n status\n }\n }\n": CliAllTenantAgentsForWikiDocument,
|
|
5565
|
+
"\n mutation CliCompileWikiNow($tenantId: ID!, $ownerId: ID!, $modelId: String) {\n compileWikiNow(tenantId: $tenantId, ownerId: $ownerId, modelId: $modelId) {\n id\n tenantId\n ownerId\n status\n trigger\n dedupeKey\n attempt\n createdAt\n }\n }\n": CliCompileWikiNowDocument,
|
|
5566
|
+
"\n mutation CliResetWikiCursor($tenantId: ID!, $ownerId: ID!, $force: Boolean) {\n resetWikiCursor(tenantId: $tenantId, ownerId: $ownerId, force: $force) {\n tenantId\n ownerId\n cursorCleared\n pagesArchived\n }\n }\n": CliResetWikiCursorDocument,
|
|
5567
|
+
"\n query CliWikiCompileJobs($tenantId: ID!, $ownerId: ID, $limit: Int) {\n wikiCompileJobs(tenantId: $tenantId, ownerId: $ownerId, limit: $limit) {\n id\n tenantId\n ownerId\n status\n trigger\n dedupeKey\n attempt\n claimedAt\n startedAt\n finishedAt\n error\n metrics\n createdAt\n }\n }\n": CliWikiCompileJobsDocument
|
|
5568
|
+
};
|
|
5569
|
+
function graphql(source) {
|
|
5570
|
+
return documents[source] ?? {};
|
|
5571
|
+
}
|
|
5572
|
+
|
|
5573
|
+
// src/commands/eval/gql.ts
|
|
5574
|
+
var EvalRunsDoc = graphql(`
|
|
5575
|
+
query CliEvalRuns($tenantId: ID!, $agentId: ID, $limit: Int, $offset: Int) {
|
|
5576
|
+
evalRuns(
|
|
5577
|
+
tenantId: $tenantId
|
|
5578
|
+
agentId: $agentId
|
|
5579
|
+
limit: $limit
|
|
5580
|
+
offset: $offset
|
|
5581
|
+
) {
|
|
5582
|
+
totalCount
|
|
5583
|
+
items {
|
|
5584
|
+
id
|
|
5585
|
+
status
|
|
5586
|
+
model
|
|
5587
|
+
categories
|
|
5588
|
+
agentId
|
|
5589
|
+
agentName
|
|
5590
|
+
agentTemplateId
|
|
5591
|
+
agentTemplateName
|
|
5592
|
+
totalTests
|
|
5593
|
+
passed
|
|
5594
|
+
failed
|
|
5595
|
+
passRate
|
|
5596
|
+
regression
|
|
5597
|
+
costUsd
|
|
5598
|
+
errorMessage
|
|
5599
|
+
startedAt
|
|
5600
|
+
completedAt
|
|
5601
|
+
createdAt
|
|
5602
|
+
}
|
|
5603
|
+
}
|
|
5604
|
+
}
|
|
5605
|
+
`);
|
|
5606
|
+
var EvalRunDoc = graphql(`
|
|
5607
|
+
query CliEvalRun($id: ID!) {
|
|
5608
|
+
evalRun(id: $id) {
|
|
5609
|
+
id
|
|
5610
|
+
status
|
|
5611
|
+
model
|
|
5612
|
+
categories
|
|
5613
|
+
agentId
|
|
5614
|
+
agentName
|
|
5615
|
+
agentTemplateId
|
|
5616
|
+
agentTemplateName
|
|
5617
|
+
totalTests
|
|
5618
|
+
passed
|
|
5619
|
+
failed
|
|
5620
|
+
passRate
|
|
5621
|
+
regression
|
|
5622
|
+
costUsd
|
|
5623
|
+
errorMessage
|
|
5624
|
+
startedAt
|
|
5625
|
+
completedAt
|
|
5626
|
+
createdAt
|
|
5627
|
+
}
|
|
5628
|
+
}
|
|
5629
|
+
`);
|
|
5630
|
+
var EvalRunResultsDoc = graphql(`
|
|
5631
|
+
query CliEvalRunResults($runId: ID!) {
|
|
5632
|
+
evalRunResults(runId: $runId) {
|
|
5633
|
+
id
|
|
5634
|
+
testCaseId
|
|
5635
|
+
testCaseName
|
|
5636
|
+
category
|
|
5637
|
+
status
|
|
5638
|
+
score
|
|
5639
|
+
durationMs
|
|
5640
|
+
agentSessionId
|
|
5641
|
+
input
|
|
5642
|
+
expected
|
|
5643
|
+
actualOutput
|
|
5644
|
+
evaluatorResults
|
|
5645
|
+
assertions
|
|
5646
|
+
errorMessage
|
|
5647
|
+
createdAt
|
|
5648
|
+
}
|
|
5649
|
+
}
|
|
5650
|
+
`);
|
|
5651
|
+
var EvalTestCasesDoc = graphql(`
|
|
5652
|
+
query CliEvalTestCases($tenantId: ID!, $category: String, $search: String) {
|
|
5653
|
+
evalTestCases(tenantId: $tenantId, category: $category, search: $search) {
|
|
5654
|
+
id
|
|
5655
|
+
name
|
|
5656
|
+
category
|
|
5657
|
+
query
|
|
5658
|
+
systemPrompt
|
|
5659
|
+
agentTemplateId
|
|
5660
|
+
agentTemplateName
|
|
5661
|
+
agentcoreEvaluatorIds
|
|
5662
|
+
tags
|
|
5663
|
+
enabled
|
|
5664
|
+
source
|
|
5665
|
+
createdAt
|
|
5666
|
+
updatedAt
|
|
5667
|
+
}
|
|
5668
|
+
}
|
|
5669
|
+
`);
|
|
5670
|
+
var EvalTestCaseDoc = graphql(`
|
|
5671
|
+
query CliEvalTestCase($id: ID!) {
|
|
5672
|
+
evalTestCase(id: $id) {
|
|
5673
|
+
id
|
|
5674
|
+
tenantId
|
|
5675
|
+
name
|
|
5676
|
+
category
|
|
5677
|
+
query
|
|
5678
|
+
systemPrompt
|
|
5679
|
+
agentTemplateId
|
|
5680
|
+
agentTemplateName
|
|
5681
|
+
assertions
|
|
5682
|
+
agentcoreEvaluatorIds
|
|
5683
|
+
tags
|
|
5684
|
+
enabled
|
|
5685
|
+
source
|
|
5686
|
+
createdAt
|
|
5687
|
+
updatedAt
|
|
5688
|
+
}
|
|
5689
|
+
}
|
|
5690
|
+
`);
|
|
5691
|
+
var ComputersForEvalDoc = graphql(`
|
|
5692
|
+
query CliComputersForEval($tenantId: ID!) {
|
|
5693
|
+
computers(tenantId: $tenantId) {
|
|
5694
|
+
id
|
|
5695
|
+
name
|
|
5696
|
+
slug
|
|
5697
|
+
runtimeStatus
|
|
5698
|
+
}
|
|
5699
|
+
}
|
|
5700
|
+
`);
|
|
5701
|
+
var AgentTemplatesForEvalDoc = graphql(`
|
|
5702
|
+
query CliAgentTemplatesForEval($tenantId: ID!) {
|
|
5703
|
+
agentTemplates(tenantId: $tenantId) {
|
|
5704
|
+
id
|
|
5705
|
+
name
|
|
5706
|
+
slug
|
|
5707
|
+
model
|
|
5708
|
+
isPublished
|
|
5709
|
+
}
|
|
5710
|
+
}
|
|
5711
|
+
`);
|
|
5712
|
+
var TenantBySlugDoc = graphql(`
|
|
5713
|
+
query CliTenantBySlug($slug: String!) {
|
|
5714
|
+
tenantBySlug(slug: $slug) {
|
|
5715
|
+
id
|
|
5716
|
+
slug
|
|
5717
|
+
name
|
|
5718
|
+
}
|
|
5719
|
+
}
|
|
5720
|
+
`);
|
|
5721
|
+
var StartEvalRunDoc = graphql(`
|
|
5722
|
+
mutation CliStartEvalRun($tenantId: ID!, $input: StartEvalRunInput!) {
|
|
5723
|
+
startEvalRun(tenantId: $tenantId, input: $input) {
|
|
5724
|
+
id
|
|
5725
|
+
status
|
|
5726
|
+
model
|
|
5727
|
+
categories
|
|
5728
|
+
agentTemplateId
|
|
5729
|
+
agentTemplateName
|
|
5730
|
+
totalTests
|
|
5731
|
+
createdAt
|
|
5732
|
+
}
|
|
5733
|
+
}
|
|
5734
|
+
`);
|
|
5735
|
+
var CancelEvalRunDoc = graphql(`
|
|
5736
|
+
mutation CliCancelEvalRun($id: ID!) {
|
|
5737
|
+
cancelEvalRun(id: $id) {
|
|
5738
|
+
id
|
|
5739
|
+
status
|
|
5740
|
+
completedAt
|
|
5741
|
+
}
|
|
5742
|
+
}
|
|
5743
|
+
`);
|
|
5744
|
+
var DeleteEvalRunDoc = graphql(`
|
|
5745
|
+
mutation CliDeleteEvalRun($id: ID!) {
|
|
5746
|
+
deleteEvalRun(id: $id)
|
|
5747
|
+
}
|
|
5748
|
+
`);
|
|
5749
|
+
var CreateEvalTestCaseDoc = graphql(`
|
|
5750
|
+
mutation CliCreateEvalTestCase(
|
|
5751
|
+
$tenantId: ID!
|
|
5752
|
+
$input: CreateEvalTestCaseInput!
|
|
5753
|
+
) {
|
|
5754
|
+
createEvalTestCase(tenantId: $tenantId, input: $input) {
|
|
5755
|
+
id
|
|
5756
|
+
name
|
|
5757
|
+
category
|
|
5758
|
+
}
|
|
5759
|
+
}
|
|
5760
|
+
`);
|
|
5761
|
+
var UpdateEvalTestCaseDoc = graphql(`
|
|
5762
|
+
mutation CliUpdateEvalTestCase($id: ID!, $input: UpdateEvalTestCaseInput!) {
|
|
5763
|
+
updateEvalTestCase(id: $id, input: $input) {
|
|
5764
|
+
id
|
|
5765
|
+
name
|
|
5766
|
+
category
|
|
5767
|
+
enabled
|
|
5768
|
+
}
|
|
5769
|
+
}
|
|
5770
|
+
`);
|
|
5771
|
+
var DeleteEvalTestCaseDoc = graphql(`
|
|
5772
|
+
mutation CliDeleteEvalTestCase($id: ID!) {
|
|
5773
|
+
deleteEvalTestCase(id: $id)
|
|
5774
|
+
}
|
|
5775
|
+
`);
|
|
5776
|
+
var SeedEvalTestCasesDoc = graphql(`
|
|
5777
|
+
mutation CliSeedEvalTestCases($tenantId: ID!, $categories: [String!]) {
|
|
5778
|
+
seedEvalTestCases(tenantId: $tenantId, categories: $categories)
|
|
5779
|
+
}
|
|
5780
|
+
`);
|
|
5781
|
+
|
|
5782
|
+
// src/commands/eval/helpers.ts
|
|
5783
|
+
async function resolveEvalContext(opts) {
|
|
5784
|
+
const region = opts.region ?? "us-east-1";
|
|
5785
|
+
const stage = await resolveStage({ flag: opts.stage, region });
|
|
5786
|
+
const session = loadStageSession(stage);
|
|
5787
|
+
const { client, tenantSlug: ctxTenantSlug } = await getGqlClient({ stage, region });
|
|
5788
|
+
const flagOrEnv = opts.tenant ?? process.env.THINKWORK_TENANT;
|
|
5789
|
+
if (flagOrEnv) {
|
|
5790
|
+
if (session?.tenantSlug === flagOrEnv && session.tenantId) {
|
|
5791
|
+
return { stage, region, client, tenantId: session.tenantId, tenantSlug: flagOrEnv };
|
|
5792
|
+
}
|
|
5793
|
+
const data = await gqlQuery(client, TenantBySlugDoc, { slug: flagOrEnv });
|
|
5794
|
+
if (!data.tenantBySlug) {
|
|
5795
|
+
printError(`Tenant "${flagOrEnv}" not found.`);
|
|
5796
|
+
process.exit(1);
|
|
5797
|
+
}
|
|
5798
|
+
return {
|
|
5799
|
+
stage,
|
|
5800
|
+
region,
|
|
5801
|
+
client,
|
|
5802
|
+
tenantId: data.tenantBySlug.id,
|
|
5803
|
+
tenantSlug: data.tenantBySlug.slug
|
|
5804
|
+
};
|
|
5805
|
+
}
|
|
5806
|
+
if (session?.tenantId && session.tenantSlug) {
|
|
5807
|
+
return {
|
|
5808
|
+
stage,
|
|
5809
|
+
region,
|
|
5810
|
+
client,
|
|
5811
|
+
tenantId: session.tenantId,
|
|
5812
|
+
tenantSlug: session.tenantSlug
|
|
5813
|
+
};
|
|
5814
|
+
}
|
|
5815
|
+
if (ctxTenantSlug) {
|
|
5816
|
+
const data = await gqlQuery(client, TenantBySlugDoc, { slug: ctxTenantSlug });
|
|
5817
|
+
if (data.tenantBySlug) {
|
|
5818
|
+
return {
|
|
5819
|
+
stage,
|
|
5820
|
+
region,
|
|
5821
|
+
client,
|
|
5822
|
+
tenantId: data.tenantBySlug.id,
|
|
5823
|
+
tenantSlug: data.tenantBySlug.slug
|
|
5824
|
+
};
|
|
5825
|
+
}
|
|
5826
|
+
}
|
|
5827
|
+
printError(
|
|
5828
|
+
`No tenant resolved for stage "${stage}". Pass --tenant <slug>, set THINKWORK_TENANT, or run \`thinkwork login --stage ${stage}\`.`
|
|
5829
|
+
);
|
|
5830
|
+
process.exit(1);
|
|
5831
|
+
}
|
|
5832
|
+
function fmtIso(iso) {
|
|
5833
|
+
if (!iso) return "\u2014";
|
|
5834
|
+
const d = new Date(iso);
|
|
5835
|
+
if (Number.isNaN(d.getTime())) return iso;
|
|
5836
|
+
return d.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
5837
|
+
}
|
|
5838
|
+
function fmtPercent(value) {
|
|
5839
|
+
if (value == null) return "\u2014";
|
|
5840
|
+
return `${(value * 100).toFixed(1)}%`;
|
|
5841
|
+
}
|
|
5842
|
+
function fmtUsd(value) {
|
|
5843
|
+
if (value == null) return "\u2014";
|
|
5844
|
+
return `$${value.toFixed(4)}`;
|
|
5845
|
+
}
|
|
5846
|
+
function isTerminalStatus(status) {
|
|
5847
|
+
return status === "completed" || status === "failed" || status === "cancelled";
|
|
5848
|
+
}
|
|
5849
|
+
|
|
5850
|
+
// src/commands/eval/run.ts
|
|
5851
|
+
var DEFAULT_EVAL_MODEL_ID = "moonshotai.kimi-k2.5";
|
|
5852
|
+
async function runEvalRun(opts) {
|
|
5853
|
+
const ctx = await resolveEvalContext(opts);
|
|
5854
|
+
const interactive = isInteractive();
|
|
5855
|
+
const deprecatedComputerId = opts.computer ?? null;
|
|
5856
|
+
let categories = opts.category ?? null;
|
|
5857
|
+
let testCaseIds = opts.testCase ?? null;
|
|
5858
|
+
if (deprecatedComputerId) {
|
|
5859
|
+
printError(
|
|
5860
|
+
"--computer is no longer supported for eval runs. Evals run directly against the default Agent template."
|
|
5861
|
+
);
|
|
5862
|
+
process.exit(1);
|
|
5863
|
+
}
|
|
5864
|
+
if (opts.model && opts.model !== DEFAULT_EVAL_MODEL_ID) {
|
|
5865
|
+
printError(
|
|
5866
|
+
`--model is no longer configurable for eval runs. Evals use ${DEFAULT_EVAL_MODEL_ID}.`
|
|
5867
|
+
);
|
|
5868
|
+
process.exit(1);
|
|
5869
|
+
}
|
|
5870
|
+
const scopeSatisfied = testCaseIds && testCaseIds.length > 0 || categories && categories.length > 0 || opts.all === true;
|
|
5871
|
+
if (!scopeSatisfied) {
|
|
5872
|
+
if (!interactive) {
|
|
5873
|
+
const missing = [];
|
|
5874
|
+
if (!scopeSatisfied)
|
|
5875
|
+
missing.push("one of --all | --category | --test-case");
|
|
5876
|
+
printError(
|
|
5877
|
+
`Missing required flag(s) in non-interactive session: ${missing.join(", ")}.`
|
|
5878
|
+
);
|
|
5879
|
+
process.exit(1);
|
|
5880
|
+
}
|
|
5881
|
+
}
|
|
5882
|
+
if (!scopeSatisfied) {
|
|
5883
|
+
const tcData = await gqlQuery(ctx.client, EvalTestCasesDoc, {
|
|
5884
|
+
tenantId: ctx.tenantId
|
|
5885
|
+
});
|
|
5886
|
+
const distinctCategories = Array.from(
|
|
5887
|
+
new Set(
|
|
5888
|
+
(tcData.evalTestCases ?? []).filter((tc) => tc.enabled).map((tc) => tc.category)
|
|
5889
|
+
)
|
|
5890
|
+
).sort();
|
|
5891
|
+
if (distinctCategories.length === 0) {
|
|
5892
|
+
printError(
|
|
5893
|
+
"No enabled test cases exist for this tenant yet. Run `thinkwork eval seed` to load the starter pack."
|
|
5894
|
+
);
|
|
5895
|
+
process.exit(1);
|
|
5896
|
+
}
|
|
5897
|
+
const picked = await promptOrExit(
|
|
5898
|
+
() => checkbox({
|
|
5899
|
+
message: "Which categories? (space to toggle, enter to confirm)",
|
|
5900
|
+
choices: distinctCategories.map((c) => ({ name: c, value: c })),
|
|
5901
|
+
required: true,
|
|
5902
|
+
loop: false
|
|
5903
|
+
})
|
|
5904
|
+
);
|
|
5905
|
+
categories = picked;
|
|
5906
|
+
}
|
|
5907
|
+
const requestedModel = opts.model ?? null;
|
|
5908
|
+
opts.model = DEFAULT_EVAL_MODEL_ID;
|
|
5909
|
+
if (interactive && !isJsonMode()) {
|
|
5910
|
+
const summaryLines = [
|
|
5911
|
+
["Stage", ctx.stage],
|
|
5912
|
+
["Tenant", ctx.tenantSlug],
|
|
5913
|
+
["Target", "Default Agent template"]
|
|
5914
|
+
];
|
|
5915
|
+
if (requestedModel && requestedModel !== DEFAULT_EVAL_MODEL_ID)
|
|
5916
|
+
summaryLines.push(["Ignored Model", requestedModel]);
|
|
5917
|
+
if (opts.model) summaryLines.push(["Model", opts.model]);
|
|
5918
|
+
if (categories && categories.length)
|
|
5919
|
+
summaryLines.push(["Categories", categories.join(", ")]);
|
|
5920
|
+
if (testCaseIds && testCaseIds.length)
|
|
5921
|
+
summaryLines.push(["Test cases", `${testCaseIds.length} picked`]);
|
|
5922
|
+
if (opts.all && !categories?.length && !testCaseIds?.length)
|
|
5923
|
+
summaryLines.push(["Scope", "all enabled test cases"]);
|
|
5924
|
+
printKeyValue(summaryLines);
|
|
5925
|
+
const proceed = await promptOrExit(
|
|
5926
|
+
() => confirm2({ message: "Start run?", default: true })
|
|
5927
|
+
);
|
|
5928
|
+
if (!proceed) {
|
|
5929
|
+
logStderr("Cancelled.");
|
|
5930
|
+
process.exit(0);
|
|
5931
|
+
}
|
|
5932
|
+
}
|
|
5933
|
+
const mutRes = await gqlMutate(ctx.client, StartEvalRunDoc, {
|
|
5934
|
+
tenantId: ctx.tenantId,
|
|
5935
|
+
input: {
|
|
5936
|
+
model: opts.model ?? null,
|
|
5937
|
+
categories: categories ?? null,
|
|
5938
|
+
testCaseIds: testCaseIds ?? null
|
|
5939
|
+
}
|
|
5940
|
+
});
|
|
5941
|
+
const run2 = mutRes.startEvalRun;
|
|
5942
|
+
if (isJsonMode()) {
|
|
5943
|
+
printJson({
|
|
5944
|
+
runId: run2.id,
|
|
5945
|
+
status: run2.status,
|
|
5946
|
+
model: run2.model,
|
|
5947
|
+
categories: run2.categories
|
|
5948
|
+
});
|
|
5949
|
+
} else {
|
|
5950
|
+
printSuccess(`Started eval run ${run2.id} (status: ${run2.status}).`);
|
|
5951
|
+
}
|
|
5952
|
+
if (!opts.watch) return;
|
|
5953
|
+
const timeoutSec = Number.parseInt(opts.timeout ?? "900", 10);
|
|
5954
|
+
await pollUntilTerminal(ctx.client, run2.id, 3, timeoutSec);
|
|
5955
|
+
}
|
|
5956
|
+
async function pollUntilTerminal(client, runId, intervalSec, timeoutSec) {
|
|
5957
|
+
const deadline = Date.now() + timeoutSec * 1e3;
|
|
5958
|
+
const spinner = isJsonMode() ? null : ora2({ text: "Waiting for run to complete\u2026" }).start();
|
|
5959
|
+
try {
|
|
5960
|
+
while (Date.now() < deadline) {
|
|
5961
|
+
const data = await gqlQuery(client, EvalRunDoc, { id: runId });
|
|
5962
|
+
const run2 = data.evalRun;
|
|
5963
|
+
if (!run2) {
|
|
5964
|
+
if (spinner) spinner.fail("Run disappeared from the database.");
|
|
5965
|
+
process.exit(1);
|
|
5966
|
+
}
|
|
5967
|
+
if (spinner) {
|
|
5968
|
+
spinner.text = `status=${run2.status} ${run2.passed}/${run2.totalTests} passed (${fmtPercent(run2.passRate)})`;
|
|
5969
|
+
}
|
|
5970
|
+
if (isTerminalStatus(run2.status)) {
|
|
5971
|
+
if (spinner) {
|
|
5972
|
+
if (run2.status === "completed")
|
|
5973
|
+
spinner.succeed(
|
|
5974
|
+
`completed \u2014 ${run2.passed}/${run2.totalTests} (${fmtPercent(run2.passRate)})`
|
|
5975
|
+
);
|
|
5976
|
+
else if (run2.status === "failed")
|
|
5977
|
+
spinner.fail(`failed \u2014 ${run2.errorMessage ?? "unknown error"}`);
|
|
5978
|
+
else spinner.warn("cancelled");
|
|
5979
|
+
}
|
|
5980
|
+
if (isJsonMode()) {
|
|
5981
|
+
printJson({
|
|
5982
|
+
runId: run2.id,
|
|
5983
|
+
status: run2.status,
|
|
5984
|
+
passed: run2.passed,
|
|
5985
|
+
failed: run2.failed,
|
|
5986
|
+
totalTests: run2.totalTests,
|
|
5987
|
+
passRate: run2.passRate,
|
|
5988
|
+
errorMessage: run2.errorMessage
|
|
5989
|
+
});
|
|
5990
|
+
}
|
|
5991
|
+
if (run2.status === "completed") process.exit(0);
|
|
5992
|
+
process.exit(1);
|
|
5993
|
+
}
|
|
5994
|
+
await new Promise((r) => setTimeout(r, intervalSec * 1e3));
|
|
5995
|
+
}
|
|
5996
|
+
if (spinner) spinner.warn(`timeout after ${timeoutSec}s`);
|
|
5997
|
+
process.exit(2);
|
|
5998
|
+
} catch (err) {
|
|
5999
|
+
if (spinner) spinner.fail(err instanceof Error ? err.message : String(err));
|
|
6000
|
+
throw err;
|
|
6001
|
+
}
|
|
6002
|
+
}
|
|
6003
|
+
|
|
6004
|
+
// src/commands/eval/list.ts
|
|
6005
|
+
async function runEvalList(opts) {
|
|
6006
|
+
const ctx = await resolveEvalContext(opts);
|
|
6007
|
+
const data = await gqlQuery(ctx.client, EvalRunsDoc, {
|
|
6008
|
+
tenantId: ctx.tenantId,
|
|
6009
|
+
agentId: opts.agent ?? null,
|
|
6010
|
+
limit: Number.parseInt(opts.limit ?? "25", 10),
|
|
6011
|
+
offset: Number.parseInt(opts.offset ?? "0", 10)
|
|
6012
|
+
});
|
|
6013
|
+
const rows = (data.evalRuns.items ?? []).map((r) => ({
|
|
6014
|
+
id: r.id,
|
|
6015
|
+
status: r.status,
|
|
6016
|
+
template: r.agentTemplateName ?? r.agentTemplateId ?? "\u2014",
|
|
6017
|
+
categories: (r.categories ?? []).join(", ") || "\u2014",
|
|
6018
|
+
tests: `${r.passed}/${r.totalTests}`,
|
|
6019
|
+
passRate: fmtPercent(r.passRate),
|
|
6020
|
+
cost: fmtUsd(r.costUsd),
|
|
6021
|
+
started: fmtIso(r.startedAt)
|
|
6022
|
+
}));
|
|
6023
|
+
if (isJsonMode()) {
|
|
6024
|
+
printJson({ totalCount: data.evalRuns.totalCount, items: data.evalRuns.items });
|
|
6025
|
+
return;
|
|
6026
|
+
}
|
|
6027
|
+
printTable(rows, [
|
|
6028
|
+
{ key: "id", header: "RUN ID" },
|
|
6029
|
+
{ key: "status", header: "STATUS" },
|
|
6030
|
+
{ key: "template", header: "TEMPLATE" },
|
|
6031
|
+
{ key: "categories", header: "CATEGORIES" },
|
|
6032
|
+
{ key: "tests", header: "PASS/TOTAL" },
|
|
6033
|
+
{ key: "passRate", header: "PASS RATE" },
|
|
6034
|
+
{ key: "cost", header: "COST" },
|
|
6035
|
+
{ key: "started", header: "STARTED" }
|
|
6036
|
+
]);
|
|
6037
|
+
}
|
|
6038
|
+
|
|
6039
|
+
// src/commands/eval/get.ts
|
|
6040
|
+
async function runEvalGet(runId, opts) {
|
|
6041
|
+
const ctx = await resolveEvalContext(opts);
|
|
6042
|
+
const data = await gqlQuery(ctx.client, EvalRunDoc, { id: runId });
|
|
6043
|
+
if (!data.evalRun) {
|
|
6044
|
+
printError(`Run ${runId} not found.`);
|
|
6045
|
+
process.exit(1);
|
|
6046
|
+
}
|
|
6047
|
+
const run2 = data.evalRun;
|
|
6048
|
+
const results = opts.results === false ? [] : (await gqlQuery(ctx.client, EvalRunResultsDoc, { runId })).evalRunResults ?? [];
|
|
6049
|
+
if (isJsonMode()) {
|
|
6050
|
+
printJson({ run: run2, results });
|
|
6051
|
+
return;
|
|
6052
|
+
}
|
|
6053
|
+
printKeyValue([
|
|
6054
|
+
["Run ID", run2.id],
|
|
6055
|
+
["Status", run2.status],
|
|
6056
|
+
["Agent template", run2.agentTemplateName ?? run2.agentTemplateId ?? "\u2014"],
|
|
6057
|
+
["Agent", run2.agentName ?? run2.agentId ?? "\u2014"],
|
|
6058
|
+
["Model", run2.model ?? "\u2014"],
|
|
6059
|
+
["Categories", (run2.categories ?? []).join(", ") || "\u2014"],
|
|
6060
|
+
["Pass/Total", `${run2.passed}/${run2.totalTests}`],
|
|
6061
|
+
["Pass rate", fmtPercent(run2.passRate)],
|
|
6062
|
+
["Regression", run2.regression ? "YES" : "no"],
|
|
6063
|
+
["Cost", fmtUsd(run2.costUsd)],
|
|
6064
|
+
["Error", run2.errorMessage ?? "\u2014"],
|
|
6065
|
+
["Started", fmtIso(run2.startedAt)],
|
|
6066
|
+
["Completed", fmtIso(run2.completedAt)]
|
|
6067
|
+
]);
|
|
6068
|
+
if (opts.results !== false && results.length > 0) {
|
|
6069
|
+
console.log("");
|
|
6070
|
+
const rows = results.map((r) => ({
|
|
6071
|
+
name: r.testCaseName ?? "\u2014",
|
|
6072
|
+
category: r.category ?? "\u2014",
|
|
6073
|
+
status: r.status,
|
|
6074
|
+
score: r.score == null ? "\u2014" : r.score.toFixed(3),
|
|
6075
|
+
duration: r.durationMs == null ? "\u2014" : `${r.durationMs}ms`
|
|
6076
|
+
}));
|
|
6077
|
+
printTable(rows, [
|
|
6078
|
+
{ key: "name", header: "TEST CASE" },
|
|
6079
|
+
{ key: "category", header: "CATEGORY" },
|
|
6080
|
+
{ key: "status", header: "STATUS" },
|
|
6081
|
+
{ key: "score", header: "SCORE" },
|
|
6082
|
+
{ key: "duration", header: "DURATION" }
|
|
6083
|
+
]);
|
|
6084
|
+
}
|
|
6085
|
+
}
|
|
6086
|
+
|
|
6087
|
+
// src/commands/eval/watch.ts
|
|
6088
|
+
import ora3 from "ora";
|
|
6089
|
+
async function runEvalWatch(runId, opts) {
|
|
6090
|
+
const ctx = await resolveEvalContext(opts);
|
|
6091
|
+
const intervalSec = Number.parseInt(opts.interval ?? "3", 10);
|
|
6092
|
+
const timeoutSec = Number.parseInt(opts.timeout ?? "900", 10);
|
|
6093
|
+
const deadline = Date.now() + timeoutSec * 1e3;
|
|
6094
|
+
const spinner = isJsonMode() ? null : ora3({ text: `Watching run ${runId}\u2026` }).start();
|
|
6095
|
+
try {
|
|
6096
|
+
while (Date.now() < deadline) {
|
|
6097
|
+
const data = await gqlQuery(ctx.client, EvalRunDoc, { id: runId });
|
|
6098
|
+
const run2 = data.evalRun;
|
|
6099
|
+
if (!run2) {
|
|
6100
|
+
if (spinner) spinner.fail(`Run ${runId} not found.`);
|
|
6101
|
+
process.exit(1);
|
|
6102
|
+
}
|
|
6103
|
+
if (spinner) {
|
|
6104
|
+
spinner.text = `status=${run2.status} ${run2.passed}/${run2.totalTests} passed (${fmtPercent(run2.passRate)})`;
|
|
6105
|
+
}
|
|
6106
|
+
if (isTerminalStatus(run2.status)) {
|
|
6107
|
+
if (spinner) {
|
|
6108
|
+
if (run2.status === "completed")
|
|
6109
|
+
spinner.succeed(`completed \u2014 ${run2.passed}/${run2.totalTests} (${fmtPercent(run2.passRate)})`);
|
|
6110
|
+
else if (run2.status === "failed") spinner.fail(`failed \u2014 ${run2.errorMessage ?? "unknown error"}`);
|
|
6111
|
+
else spinner.warn("cancelled");
|
|
6112
|
+
}
|
|
6113
|
+
if (isJsonMode()) {
|
|
6114
|
+
printJson({
|
|
6115
|
+
runId: run2.id,
|
|
6116
|
+
status: run2.status,
|
|
6117
|
+
passed: run2.passed,
|
|
6118
|
+
failed: run2.failed,
|
|
6119
|
+
totalTests: run2.totalTests,
|
|
6120
|
+
passRate: run2.passRate,
|
|
6121
|
+
errorMessage: run2.errorMessage
|
|
6122
|
+
});
|
|
6123
|
+
}
|
|
6124
|
+
process.exit(run2.status === "completed" ? 0 : 1);
|
|
6125
|
+
}
|
|
6126
|
+
await new Promise((r) => setTimeout(r, intervalSec * 1e3));
|
|
6127
|
+
}
|
|
6128
|
+
if (spinner) spinner.warn(`timeout after ${timeoutSec}s`);
|
|
6129
|
+
process.exit(2);
|
|
6130
|
+
} catch (err) {
|
|
6131
|
+
if (spinner) spinner.fail(err instanceof Error ? err.message : String(err));
|
|
6132
|
+
throw err;
|
|
6133
|
+
}
|
|
6134
|
+
}
|
|
6135
|
+
|
|
6136
|
+
// src/commands/eval/cancel.ts
|
|
6137
|
+
async function runEvalCancel(runId, opts) {
|
|
6138
|
+
const ctx = await resolveEvalContext(opts);
|
|
6139
|
+
const data = await gqlMutate(ctx.client, CancelEvalRunDoc, { id: runId });
|
|
6140
|
+
if (isJsonMode()) {
|
|
6141
|
+
printJson({ runId: data.cancelEvalRun.id, status: data.cancelEvalRun.status });
|
|
6142
|
+
return;
|
|
6143
|
+
}
|
|
6144
|
+
printSuccess(`Cancelled run ${data.cancelEvalRun.id} (status: ${data.cancelEvalRun.status}).`);
|
|
6145
|
+
}
|
|
6146
|
+
|
|
6147
|
+
// src/commands/eval/delete.ts
|
|
6148
|
+
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
6149
|
+
async function runEvalDelete(runId, opts) {
|
|
6150
|
+
const ctx = await resolveEvalContext(opts);
|
|
6151
|
+
if (!opts.yes) {
|
|
6152
|
+
if (!isInteractive()) {
|
|
6153
|
+
printError("Refusing to delete without --yes in a non-interactive session.");
|
|
6154
|
+
process.exit(1);
|
|
6155
|
+
}
|
|
6156
|
+
requireTty("Confirmation");
|
|
6157
|
+
const go = await promptOrExit(
|
|
6158
|
+
() => confirm3({
|
|
6159
|
+
message: `Permanently delete run ${runId} and its results?`,
|
|
6160
|
+
default: false
|
|
6161
|
+
})
|
|
6162
|
+
);
|
|
6163
|
+
if (!go) {
|
|
6164
|
+
logStderr("Cancelled.");
|
|
6165
|
+
process.exit(0);
|
|
6166
|
+
}
|
|
6167
|
+
}
|
|
6168
|
+
const data = await gqlMutate(ctx.client, DeleteEvalRunDoc, { id: runId });
|
|
6169
|
+
if (isJsonMode()) {
|
|
6170
|
+
printJson({ runId, deleted: data.deleteEvalRun });
|
|
6171
|
+
return;
|
|
6172
|
+
}
|
|
6173
|
+
if (data.deleteEvalRun) printSuccess(`Deleted run ${runId}.`);
|
|
6174
|
+
else printError(`Server reported not-deleted for ${runId}.`);
|
|
6175
|
+
}
|
|
6176
|
+
|
|
6177
|
+
// src/commands/eval/categories.ts
|
|
6178
|
+
async function runEvalCategories(opts) {
|
|
6179
|
+
const ctx = await resolveEvalContext(opts);
|
|
6180
|
+
const data = await gqlQuery(ctx.client, EvalTestCasesDoc, { tenantId: ctx.tenantId });
|
|
6181
|
+
const counts = /* @__PURE__ */ new Map();
|
|
6182
|
+
for (const tc of data.evalTestCases ?? []) {
|
|
6183
|
+
const entry = counts.get(tc.category) ?? { total: 0, enabled: 0 };
|
|
6184
|
+
entry.total += 1;
|
|
6185
|
+
if (tc.enabled) entry.enabled += 1;
|
|
6186
|
+
counts.set(tc.category, entry);
|
|
6187
|
+
}
|
|
6188
|
+
const rows = Array.from(counts.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([category, { total, enabled }]) => ({
|
|
6189
|
+
category,
|
|
6190
|
+
enabled: String(enabled),
|
|
6191
|
+
total: String(total)
|
|
6192
|
+
}));
|
|
6193
|
+
if (isJsonMode()) {
|
|
6194
|
+
printJson(rows);
|
|
6195
|
+
return;
|
|
6196
|
+
}
|
|
6197
|
+
printTable(rows, [
|
|
6198
|
+
{ key: "category", header: "CATEGORY" },
|
|
6199
|
+
{ key: "enabled", header: "ENABLED" },
|
|
6200
|
+
{ key: "total", header: "TOTAL" }
|
|
6201
|
+
]);
|
|
6202
|
+
}
|
|
6203
|
+
|
|
6204
|
+
// src/commands/eval/seed.ts
|
|
6205
|
+
async function runEvalSeed(opts) {
|
|
6206
|
+
const ctx = await resolveEvalContext(opts);
|
|
6207
|
+
const data = await gqlMutate(ctx.client, SeedEvalTestCasesDoc, {
|
|
6208
|
+
tenantId: ctx.tenantId,
|
|
6209
|
+
categories: opts.category && opts.category.length > 0 ? opts.category : null
|
|
6210
|
+
});
|
|
6211
|
+
if (isJsonMode()) {
|
|
6212
|
+
printJson({ inserted: data.seedEvalTestCases });
|
|
6213
|
+
return;
|
|
6214
|
+
}
|
|
6215
|
+
printSuccess(`Seeded ${data.seedEvalTestCases} new test case(s). (Duplicates were skipped.)`);
|
|
6216
|
+
}
|
|
6217
|
+
|
|
6218
|
+
// src/commands/eval/test-case/list.ts
|
|
6219
|
+
async function runEvalTestCaseList(opts) {
|
|
6220
|
+
const ctx = await resolveEvalContext(opts);
|
|
6221
|
+
const data = await gqlQuery(ctx.client, EvalTestCasesDoc, {
|
|
6222
|
+
tenantId: ctx.tenantId,
|
|
6223
|
+
category: opts.category ?? null,
|
|
6224
|
+
search: opts.search ?? null
|
|
6225
|
+
});
|
|
6226
|
+
const rows = (data.evalTestCases ?? []).map((tc) => ({
|
|
6227
|
+
id: tc.id,
|
|
6228
|
+
name: tc.name,
|
|
6229
|
+
category: tc.category,
|
|
6230
|
+
template: tc.agentTemplateName ?? "\u2014",
|
|
6231
|
+
evaluators: (tc.agentcoreEvaluatorIds ?? []).join(", ") || "\u2014",
|
|
6232
|
+
enabled: tc.enabled ? "yes" : "no",
|
|
6233
|
+
updated: fmtIso(tc.updatedAt)
|
|
6234
|
+
}));
|
|
6235
|
+
if (isJsonMode()) {
|
|
6236
|
+
printJson(data.evalTestCases ?? []);
|
|
6237
|
+
return;
|
|
6238
|
+
}
|
|
6239
|
+
printTable(rows, [
|
|
6240
|
+
{ key: "id", header: "ID" },
|
|
6241
|
+
{ key: "name", header: "NAME" },
|
|
6242
|
+
{ key: "category", header: "CATEGORY" },
|
|
6243
|
+
{ key: "template", header: "TEMPLATE" },
|
|
6244
|
+
{ key: "evaluators", header: "EVALUATORS" },
|
|
6245
|
+
{ key: "enabled", header: "ENABLED" },
|
|
6246
|
+
{ key: "updated", header: "UPDATED" }
|
|
6247
|
+
]);
|
|
6248
|
+
}
|
|
6249
|
+
|
|
6250
|
+
// src/commands/eval/test-case/get.ts
|
|
6251
|
+
async function runEvalTestCaseGet(id, opts) {
|
|
6252
|
+
const ctx = await resolveEvalContext(opts);
|
|
6253
|
+
const data = await gqlQuery(ctx.client, EvalTestCaseDoc, { id });
|
|
6254
|
+
if (!data.evalTestCase) {
|
|
6255
|
+
printError(`Test case ${id} not found.`);
|
|
6256
|
+
process.exit(1);
|
|
6257
|
+
}
|
|
6258
|
+
const tc = data.evalTestCase;
|
|
6259
|
+
if (isJsonMode()) {
|
|
6260
|
+
printJson(tc);
|
|
6261
|
+
return;
|
|
6262
|
+
}
|
|
6263
|
+
printKeyValue([
|
|
6264
|
+
["ID", tc.id],
|
|
6265
|
+
["Name", tc.name],
|
|
6266
|
+
["Category", tc.category],
|
|
6267
|
+
["Agent template", tc.agentTemplateName ?? tc.agentTemplateId ?? "\u2014"],
|
|
6268
|
+
["Source", tc.source],
|
|
6269
|
+
["Enabled", tc.enabled ? "yes" : "no"],
|
|
6270
|
+
["Evaluators", (tc.agentcoreEvaluatorIds ?? []).join(", ") || "\u2014"],
|
|
6271
|
+
["Tags", (tc.tags ?? []).join(", ") || "\u2014"],
|
|
6272
|
+
["Created", fmtIso(tc.createdAt)],
|
|
6273
|
+
["Updated", fmtIso(tc.updatedAt)]
|
|
6274
|
+
]);
|
|
6275
|
+
console.log("");
|
|
6276
|
+
console.log(" QUERY");
|
|
6277
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500");
|
|
6278
|
+
console.log(` ${tc.query.split("\n").join("\n ")}`);
|
|
6279
|
+
if (tc.systemPrompt) {
|
|
6280
|
+
console.log("");
|
|
6281
|
+
console.log(" SYSTEM PROMPT");
|
|
6282
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
6283
|
+
console.log(` ${tc.systemPrompt.split("\n").join("\n ")}`);
|
|
6284
|
+
}
|
|
6285
|
+
if (tc.assertions) {
|
|
6286
|
+
console.log("");
|
|
6287
|
+
console.log(" ASSERTIONS");
|
|
6288
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
6289
|
+
console.log(` ${tc.assertions}`);
|
|
6290
|
+
}
|
|
6291
|
+
}
|
|
6292
|
+
|
|
6293
|
+
// src/commands/eval/test-case/create.ts
|
|
6294
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
6295
|
+
import { input as input3, select as select8, checkbox as checkbox2 } from "@inquirer/prompts";
|
|
6296
|
+
var DEFAULT_EVALUATORS = [
|
|
6297
|
+
"Builtin.Helpfulness",
|
|
6298
|
+
"Builtin.Correctness",
|
|
6299
|
+
"Builtin.Faithfulness",
|
|
6300
|
+
"Builtin.ToolSelectionAccuracy",
|
|
6301
|
+
"Builtin.ToolParameterAccuracy",
|
|
6302
|
+
"Builtin.GoalSuccessRate"
|
|
6303
|
+
];
|
|
6304
|
+
async function runEvalTestCaseCreate(opts) {
|
|
6305
|
+
const ctx = await resolveEvalContext(opts);
|
|
6306
|
+
const interactive = isInteractive();
|
|
6307
|
+
let name = opts.name;
|
|
6308
|
+
let category = opts.category;
|
|
6309
|
+
let query = opts.query;
|
|
6310
|
+
let evaluators = opts.evaluator;
|
|
6311
|
+
let agentTemplateId = opts.agentTemplate ?? null;
|
|
6312
|
+
if (!name || !category || !query) {
|
|
6313
|
+
if (!interactive) {
|
|
6314
|
+
const missing = [];
|
|
6315
|
+
if (!name) missing.push("--name");
|
|
6316
|
+
if (!category) missing.push("--category");
|
|
6317
|
+
if (!query) missing.push("--query");
|
|
6318
|
+
printError(`Missing required flag(s): ${missing.join(", ")}.`);
|
|
6319
|
+
process.exit(1);
|
|
6320
|
+
}
|
|
6321
|
+
}
|
|
6322
|
+
if (!name) {
|
|
6323
|
+
requireTty("Name");
|
|
6324
|
+
name = await promptOrExit(
|
|
6325
|
+
() => input3({ message: "Test case name?", validate: (v) => v.trim().length > 0 || "Required" })
|
|
6326
|
+
);
|
|
6327
|
+
}
|
|
6328
|
+
if (!category) {
|
|
6329
|
+
category = await promptOrExit(
|
|
6330
|
+
() => input3({ message: "Category (free-form label)?", validate: (v) => v.trim().length > 0 || "Required" })
|
|
6331
|
+
);
|
|
6332
|
+
}
|
|
6333
|
+
if (!query) {
|
|
6334
|
+
query = await promptOrExit(
|
|
6335
|
+
() => input3({ message: "Query the agent under test will receive?", validate: (v) => v.trim().length > 0 || "Required" })
|
|
6336
|
+
);
|
|
6337
|
+
}
|
|
6338
|
+
if (interactive && agentTemplateId === null) {
|
|
6339
|
+
const tpls = await gqlQuery(ctx.client, AgentTemplatesForEvalDoc, { tenantId: ctx.tenantId });
|
|
6340
|
+
const templates = tpls.agentTemplates ?? [];
|
|
6341
|
+
if (templates.length > 0) {
|
|
6342
|
+
const choice = await promptOrExit(
|
|
6343
|
+
() => select8({
|
|
6344
|
+
message: "Pin to an agent template? (Enter for none)",
|
|
6345
|
+
choices: [
|
|
6346
|
+
{ name: "\u2014 none \u2014 (runner picks)", value: "" },
|
|
6347
|
+
...templates.map((t) => ({ name: `${t.name}${t.model ? ` (${t.model})` : ""}`, value: t.id }))
|
|
6348
|
+
],
|
|
6349
|
+
loop: false
|
|
6350
|
+
})
|
|
6351
|
+
);
|
|
6352
|
+
agentTemplateId = choice === "" ? null : choice;
|
|
6353
|
+
}
|
|
6354
|
+
}
|
|
6355
|
+
if (interactive && (!evaluators || evaluators.length === 0)) {
|
|
6356
|
+
const picked = await promptOrExit(
|
|
6357
|
+
() => checkbox2({
|
|
6358
|
+
message: "Evaluators to run for this test case?",
|
|
6359
|
+
choices: DEFAULT_EVALUATORS.map((e) => ({ name: e, value: e, checked: e === "Builtin.Helpfulness" })),
|
|
6360
|
+
loop: false
|
|
6361
|
+
})
|
|
6362
|
+
);
|
|
6363
|
+
evaluators = picked;
|
|
6364
|
+
}
|
|
6365
|
+
let assertions = null;
|
|
6366
|
+
if (opts.assertionsFile) {
|
|
6367
|
+
const parsed = JSON.parse(readFileSync6(opts.assertionsFile, "utf8"));
|
|
6368
|
+
if (!Array.isArray(parsed)) {
|
|
6369
|
+
printError(`--assertions-file must contain a JSON array.`);
|
|
6370
|
+
process.exit(1);
|
|
6371
|
+
}
|
|
6372
|
+
assertions = parsed;
|
|
6373
|
+
}
|
|
6374
|
+
const mutation = await gqlMutate(ctx.client, CreateEvalTestCaseDoc, {
|
|
6375
|
+
tenantId: ctx.tenantId,
|
|
6376
|
+
input: {
|
|
6377
|
+
name,
|
|
6378
|
+
category,
|
|
6379
|
+
query,
|
|
6380
|
+
systemPrompt: opts.systemPrompt ?? null,
|
|
6381
|
+
agentTemplateId,
|
|
6382
|
+
agentcoreEvaluatorIds: evaluators && evaluators.length > 0 ? evaluators : null,
|
|
6383
|
+
tags: opts.tag && opts.tag.length > 0 ? opts.tag : null,
|
|
6384
|
+
enabled: opts.enabled ?? true,
|
|
6385
|
+
assertions
|
|
6386
|
+
}
|
|
6387
|
+
});
|
|
6388
|
+
if (isJsonMode()) {
|
|
6389
|
+
printJson(mutation.createEvalTestCase);
|
|
6390
|
+
return;
|
|
6391
|
+
}
|
|
6392
|
+
printSuccess(
|
|
6393
|
+
`Created test case ${mutation.createEvalTestCase.id} "${mutation.createEvalTestCase.name}" (${mutation.createEvalTestCase.category}).`
|
|
6394
|
+
);
|
|
6395
|
+
}
|
|
6396
|
+
|
|
6397
|
+
// src/commands/eval/test-case/update.ts
|
|
6398
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
6399
|
+
async function runEvalTestCaseUpdate(id, opts) {
|
|
6400
|
+
const ctx = await resolveEvalContext(opts);
|
|
6401
|
+
const input4 = {};
|
|
6402
|
+
if (opts.name !== void 0) input4.name = opts.name;
|
|
6403
|
+
if (opts.category !== void 0) input4.category = opts.category;
|
|
6404
|
+
if (opts.query !== void 0) input4.query = opts.query;
|
|
6405
|
+
if (opts.systemPrompt !== void 0) input4.systemPrompt = opts.systemPrompt;
|
|
6406
|
+
if (opts.agentTemplate !== void 0) input4.agentTemplateId = opts.agentTemplate;
|
|
6407
|
+
if (opts.evaluator !== void 0) input4.agentcoreEvaluatorIds = opts.evaluator;
|
|
6408
|
+
if (opts.tag !== void 0) input4.tags = opts.tag;
|
|
6409
|
+
if (opts.enabled !== void 0) input4.enabled = opts.enabled;
|
|
6410
|
+
if (opts.assertionsFile) {
|
|
6411
|
+
const parsed = JSON.parse(readFileSync7(opts.assertionsFile, "utf8"));
|
|
6412
|
+
if (!Array.isArray(parsed)) {
|
|
6413
|
+
printError(`--assertions-file must contain a JSON array.`);
|
|
6414
|
+
process.exit(1);
|
|
6415
|
+
}
|
|
6416
|
+
input4.assertions = parsed;
|
|
6417
|
+
}
|
|
6418
|
+
if (Object.keys(input4).length === 0) {
|
|
6419
|
+
printError("No fields to update. Pass at least one --<field>.");
|
|
6420
|
+
process.exit(1);
|
|
6421
|
+
}
|
|
6422
|
+
const res = await gqlMutate(ctx.client, UpdateEvalTestCaseDoc, { id, input: input4 });
|
|
6423
|
+
if (isJsonMode()) {
|
|
6424
|
+
printJson(res.updateEvalTestCase);
|
|
6425
|
+
return;
|
|
6426
|
+
}
|
|
6427
|
+
printSuccess(`Updated test case ${res.updateEvalTestCase.id}.`);
|
|
6428
|
+
}
|
|
6429
|
+
|
|
6430
|
+
// src/commands/eval/test-case/delete.ts
|
|
6431
|
+
import { confirm as confirm4 } from "@inquirer/prompts";
|
|
6432
|
+
async function runEvalTestCaseDelete(id, opts) {
|
|
6433
|
+
const ctx = await resolveEvalContext(opts);
|
|
6434
|
+
if (!opts.yes) {
|
|
6435
|
+
if (!isInteractive()) {
|
|
6436
|
+
printError("Refusing to delete without --yes in a non-interactive session.");
|
|
6437
|
+
process.exit(1);
|
|
6438
|
+
}
|
|
6439
|
+
requireTty("Confirmation");
|
|
6440
|
+
const go = await promptOrExit(
|
|
6441
|
+
() => confirm4({ message: `Permanently delete test case ${id}?`, default: false })
|
|
6442
|
+
);
|
|
6443
|
+
if (!go) {
|
|
6444
|
+
logStderr("Cancelled.");
|
|
6445
|
+
process.exit(0);
|
|
6446
|
+
}
|
|
6447
|
+
}
|
|
6448
|
+
const res = await gqlMutate(ctx.client, DeleteEvalTestCaseDoc, { id });
|
|
6449
|
+
if (isJsonMode()) {
|
|
6450
|
+
printJson({ id, deleted: res.deleteEvalTestCase });
|
|
6451
|
+
return;
|
|
6452
|
+
}
|
|
6453
|
+
if (res.deleteEvalTestCase) printSuccess(`Deleted test case ${id}.`);
|
|
6454
|
+
else printError(`Server reported not-deleted for ${id}.`);
|
|
6455
|
+
}
|
|
6456
|
+
|
|
6457
|
+
// src/commands/eval.ts
|
|
6458
|
+
function registerEvalCommand(program2) {
|
|
6459
|
+
const evals = program2.command("eval").alias("evals").description(
|
|
6460
|
+
"Run evaluations against the default AgentCore agent template and manage eval test cases. Integrates with the Evaluations Studio in the admin UI."
|
|
6461
|
+
).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option(
|
|
6462
|
+
"--computer <id>",
|
|
6463
|
+
"Deprecated; evals now run directly against AgentCore"
|
|
6464
|
+
).option("--model <id>", "Deprecated; evals always use Kimi K2.5").option("--category <name...>", "Only run these categories (repeatable)").option(
|
|
6465
|
+
"--test-case <id...>",
|
|
6466
|
+
"Only run these specific test case IDs (repeatable)"
|
|
6467
|
+
).option("--all", "Run all enabled test cases for the tenant").option("--watch", "Block and poll until the run reaches a terminal status").option(
|
|
6468
|
+
"--timeout <seconds>",
|
|
6469
|
+
"Max wait seconds for --watch (default 900)",
|
|
6470
|
+
"900"
|
|
6471
|
+
).addHelpText(
|
|
6472
|
+
"after",
|
|
6473
|
+
`
|
|
6474
|
+
Default action:
|
|
6475
|
+
$ thinkwork evals
|
|
6476
|
+
|
|
6477
|
+
Equivalent explicit action:
|
|
6478
|
+
$ thinkwork eval run
|
|
6479
|
+
`
|
|
6480
|
+
).action(runEvalRun);
|
|
6481
|
+
evals.command("run").description(
|
|
6482
|
+
"Start an evaluation run. Prompts for missing values in a TTY; fails fast in non-interactive sessions."
|
|
6483
|
+
).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option(
|
|
6484
|
+
"--computer <id>",
|
|
6485
|
+
"Deprecated; evals now run directly against AgentCore"
|
|
6486
|
+
).option("--model <id>", "Deprecated; evals always use Kimi K2.5").option("--category <name...>", "Only run these categories (repeatable)").option(
|
|
6487
|
+
"--test-case <id...>",
|
|
6488
|
+
"Only run these specific test case IDs (repeatable)"
|
|
6489
|
+
).option("--all", "Run all enabled test cases for the tenant").option("--watch", "Block and poll until the run reaches a terminal status").option(
|
|
6490
|
+
"--timeout <seconds>",
|
|
6491
|
+
"Max wait seconds for --watch (default 900)",
|
|
6492
|
+
"900"
|
|
6493
|
+
).addHelpText(
|
|
6494
|
+
"after",
|
|
6495
|
+
`
|
|
6496
|
+
Examples:
|
|
6497
|
+
# Fire and return \u2014 prints the runId; view results in the admin UI
|
|
6498
|
+
$ thinkwork eval run --category red-team-safety-scope
|
|
6499
|
+
|
|
6500
|
+
# Pick categories interactively
|
|
6501
|
+
$ thinkwork eval run
|
|
6502
|
+
|
|
6503
|
+
# Block until done
|
|
6504
|
+
$ thinkwork eval run --all --watch --timeout 1800
|
|
6505
|
+
`
|
|
6506
|
+
).action(runEvalRun);
|
|
6507
|
+
evals.command("list").alias("ls").description("List recent eval runs for the tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option("--agent <id>", "Filter by agent under test").option("--limit <n>", "Max rows (default 25)", "25").option("--offset <n>", "Skip N rows", "0").action(runEvalList);
|
|
6508
|
+
evals.command("get <runId>").description("Show one eval run with its per-test-case results.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option(
|
|
6509
|
+
"--results",
|
|
6510
|
+
"Also fetch per-test-case results (default: true)",
|
|
6511
|
+
true
|
|
6512
|
+
).option("--no-results", "Skip fetching per-test-case results").action(runEvalGet);
|
|
6513
|
+
evals.command("watch <runId>").description("Poll an eval run until it reaches a terminal status.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option("--interval <seconds>", "Poll interval (default 3)", "3").option("--timeout <seconds>", "Max wait seconds (default 900)", "900").action(runEvalWatch);
|
|
6514
|
+
evals.command("cancel <runId>").description("Cancel a running or pending eval run.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").action(runEvalCancel);
|
|
6515
|
+
evals.command("delete <runId>").description(
|
|
6516
|
+
"Delete an eval run and its results. Requires confirmation unless --yes."
|
|
6517
|
+
).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option("-y, --yes", "Skip the confirmation prompt").action(runEvalDelete);
|
|
6518
|
+
evals.command("categories").description(
|
|
6519
|
+
"List distinct categories present across the tenant's test cases."
|
|
6520
|
+
).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").action(runEvalCategories);
|
|
6521
|
+
evals.command("seed").description(
|
|
6522
|
+
"Idempotently seed the ThinkWork RedTeam starter pack (189 test cases across 4 categories). Safe to re-run."
|
|
6523
|
+
).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option("--category <name...>", "Only seed these categories (repeatable)").action(runEvalSeed);
|
|
6524
|
+
const tc = evals.command("test-case").alias("test-cases").description("Manage individual eval test cases (CRUD).");
|
|
6525
|
+
tc.command("list").alias("ls").description("List test cases, optionally filtered by category or search.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option("--category <name>", "Filter by a single category").option("--search <q>", "Substring match on test case name").action(runEvalTestCaseList);
|
|
6526
|
+
tc.command("get <id>").description("Show a single test case.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").action(runEvalTestCaseGet);
|
|
6527
|
+
tc.command("create").description("Create a new test case. Prompts for missing values in a TTY.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option("--name <text>", "Human-readable name").option("--category <name>", "Category label (e.g. tool-safety, red-team)").option("--query <text>", "The user-facing query this agent will receive").option("--system-prompt <text>", "Optional system-prompt override").option("--agent-template <id>", "Pin to a specific agent template").option("--evaluator <id...>", "AgentCore evaluator IDs (repeatable)").option("--tag <name...>", "Tags (repeatable)").option("--enabled", "Mark enabled (default)", true).option("--no-enabled", "Mark disabled").option(
|
|
6528
|
+
"--assertions-file <path>",
|
|
6529
|
+
"JSON file containing an array of assertions"
|
|
6530
|
+
).action(runEvalTestCaseCreate);
|
|
6531
|
+
tc.command("update <id>").description("Update a test case. Only supplied fields are changed.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option("--name <text>").option("--category <name>").option("--query <text>").option("--system-prompt <text>").option("--agent-template <id>").option(
|
|
6532
|
+
"--evaluator <id...>",
|
|
6533
|
+
"Replace AgentCore evaluator IDs (repeatable)"
|
|
6534
|
+
).option("--tag <name...>", "Replace tags (repeatable)").option("--enabled", "Mark enabled").option("--no-enabled", "Mark disabled").option(
|
|
6535
|
+
"--assertions-file <path>",
|
|
6536
|
+
"JSON file containing an array of assertions (replaces all)"
|
|
6537
|
+
).action(runEvalTestCaseUpdate);
|
|
6538
|
+
tc.command("delete <id>").description("Delete a test case. Requires confirmation unless --yes.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-r, --region <region>", "AWS region", "us-east-1").option("-y, --yes", "Skip the confirmation prompt").action(runEvalTestCaseDelete);
|
|
6539
|
+
}
|
|
6540
|
+
|
|
6541
|
+
// src/commands/wiki/compile.ts
|
|
6542
|
+
import ora4 from "ora";
|
|
6543
|
+
|
|
6544
|
+
// src/commands/wiki/gql.ts
|
|
6545
|
+
var TenantBySlugDoc2 = graphql(`
|
|
6546
|
+
query CliWikiTenantBySlug($slug: String!) {
|
|
6547
|
+
tenantBySlug(slug: $slug) {
|
|
6548
|
+
id
|
|
6549
|
+
slug
|
|
6550
|
+
name
|
|
6551
|
+
}
|
|
6552
|
+
}
|
|
6553
|
+
`);
|
|
6554
|
+
var AllTenantAgentsForWikiDoc = graphql(`
|
|
6555
|
+
query CliAllTenantAgentsForWiki($tenantId: ID!) {
|
|
6556
|
+
allTenantAgents(tenantId: $tenantId, includeSystem: false, includeSubAgents: false) {
|
|
6557
|
+
id
|
|
6558
|
+
name
|
|
6559
|
+
slug
|
|
6560
|
+
type
|
|
6561
|
+
status
|
|
6562
|
+
}
|
|
6563
|
+
}
|
|
6564
|
+
`);
|
|
6565
|
+
var CompileWikiNowDoc = graphql(`
|
|
6566
|
+
mutation CliCompileWikiNow($tenantId: ID!, $ownerId: ID!, $modelId: String) {
|
|
6567
|
+
compileWikiNow(tenantId: $tenantId, ownerId: $ownerId, modelId: $modelId) {
|
|
6568
|
+
id
|
|
6569
|
+
tenantId
|
|
6570
|
+
ownerId
|
|
6571
|
+
status
|
|
6572
|
+
trigger
|
|
6573
|
+
dedupeKey
|
|
6574
|
+
attempt
|
|
6575
|
+
createdAt
|
|
6576
|
+
}
|
|
6577
|
+
}
|
|
6578
|
+
`);
|
|
6579
|
+
var ResetWikiCursorDoc = graphql(`
|
|
6580
|
+
mutation CliResetWikiCursor($tenantId: ID!, $ownerId: ID!, $force: Boolean) {
|
|
6581
|
+
resetWikiCursor(tenantId: $tenantId, ownerId: $ownerId, force: $force) {
|
|
6582
|
+
tenantId
|
|
6583
|
+
ownerId
|
|
6584
|
+
cursorCleared
|
|
6585
|
+
pagesArchived
|
|
6586
|
+
}
|
|
6587
|
+
}
|
|
6588
|
+
`);
|
|
6589
|
+
var WikiCompileJobsDoc = graphql(`
|
|
6590
|
+
query CliWikiCompileJobs($tenantId: ID!, $ownerId: ID, $limit: Int) {
|
|
6591
|
+
wikiCompileJobs(tenantId: $tenantId, ownerId: $ownerId, limit: $limit) {
|
|
6592
|
+
id
|
|
6593
|
+
tenantId
|
|
6594
|
+
ownerId
|
|
6595
|
+
status
|
|
6596
|
+
trigger
|
|
6597
|
+
dedupeKey
|
|
6598
|
+
attempt
|
|
6599
|
+
claimedAt
|
|
6600
|
+
startedAt
|
|
6601
|
+
finishedAt
|
|
6602
|
+
error
|
|
6603
|
+
metrics
|
|
6604
|
+
createdAt
|
|
6605
|
+
}
|
|
6606
|
+
}
|
|
6607
|
+
`);
|
|
6608
|
+
|
|
6609
|
+
// src/commands/wiki/helpers.ts
|
|
6610
|
+
import { select as select9 } from "@inquirer/prompts";
|
|
6611
|
+
async function resolveWikiContext(opts) {
|
|
6612
|
+
const region = opts.region ?? "us-east-1";
|
|
6613
|
+
const stage = await resolveStage({ flag: opts.stage, region });
|
|
6614
|
+
const session = loadStageSession(stage);
|
|
6615
|
+
const { client, tenantSlug: ctxTenantSlug } = await getGqlClient({
|
|
6616
|
+
stage,
|
|
6617
|
+
region
|
|
6618
|
+
});
|
|
6619
|
+
const flagOrEnv = opts.tenant ?? process.env.THINKWORK_TENANT;
|
|
6620
|
+
if (flagOrEnv) {
|
|
6621
|
+
if (session?.tenantSlug === flagOrEnv && session.tenantId) {
|
|
6622
|
+
return {
|
|
6623
|
+
stage,
|
|
6624
|
+
region,
|
|
6625
|
+
client,
|
|
6626
|
+
tenantId: session.tenantId,
|
|
6627
|
+
tenantSlug: flagOrEnv
|
|
6628
|
+
};
|
|
6629
|
+
}
|
|
6630
|
+
const data = await gqlQuery(client, TenantBySlugDoc2, { slug: flagOrEnv });
|
|
6631
|
+
if (!data.tenantBySlug) {
|
|
6632
|
+
printError(`Tenant "${flagOrEnv}" not found.`);
|
|
6633
|
+
process.exit(1);
|
|
6634
|
+
}
|
|
6635
|
+
return {
|
|
6636
|
+
stage,
|
|
6637
|
+
region,
|
|
6638
|
+
client,
|
|
6639
|
+
tenantId: data.tenantBySlug.id,
|
|
6640
|
+
tenantSlug: data.tenantBySlug.slug
|
|
6641
|
+
};
|
|
6642
|
+
}
|
|
6643
|
+
if (session?.tenantId && session.tenantSlug) {
|
|
6644
|
+
return {
|
|
6645
|
+
stage,
|
|
6646
|
+
region,
|
|
6647
|
+
client,
|
|
6648
|
+
tenantId: session.tenantId,
|
|
6649
|
+
tenantSlug: session.tenantSlug
|
|
6650
|
+
};
|
|
6651
|
+
}
|
|
6652
|
+
if (ctxTenantSlug) {
|
|
6653
|
+
const data = await gqlQuery(client, TenantBySlugDoc2, {
|
|
6654
|
+
slug: ctxTenantSlug
|
|
6655
|
+
});
|
|
6656
|
+
if (data.tenantBySlug) {
|
|
6657
|
+
return {
|
|
6658
|
+
stage,
|
|
6659
|
+
region,
|
|
6660
|
+
client,
|
|
6661
|
+
tenantId: data.tenantBySlug.id,
|
|
6662
|
+
tenantSlug: data.tenantBySlug.slug
|
|
6663
|
+
};
|
|
6664
|
+
}
|
|
6665
|
+
}
|
|
6666
|
+
printError(
|
|
6667
|
+
`No tenant resolved for stage "${stage}". Pass --tenant <slug>, set THINKWORK_TENANT, or run \`thinkwork login --stage ${stage}\`.`
|
|
6668
|
+
);
|
|
6669
|
+
process.exit(1);
|
|
6670
|
+
}
|
|
6671
|
+
async function resolveAgentScope(ctx, opts, config = {}) {
|
|
6672
|
+
const allowAll = config.allowAll ?? true;
|
|
6673
|
+
const needList = opts.agent != null && !isUuid(opts.agent) || opts.all === true || opts.agent == null && !opts.all;
|
|
6674
|
+
let agents = [];
|
|
6675
|
+
const loadAgents = async () => {
|
|
6676
|
+
const data = await gqlQuery(ctx.client, AllTenantAgentsForWikiDoc, {
|
|
6677
|
+
tenantId: ctx.tenantId
|
|
6678
|
+
});
|
|
6679
|
+
return data.allTenantAgents ?? [];
|
|
6680
|
+
};
|
|
6681
|
+
if (opts.agent) {
|
|
6682
|
+
if (isUuid(opts.agent)) {
|
|
6683
|
+
return {
|
|
6684
|
+
mode: "single",
|
|
6685
|
+
agentId: opts.agent,
|
|
6686
|
+
agentLabel: opts.agent
|
|
6687
|
+
};
|
|
6688
|
+
}
|
|
6689
|
+
agents = await loadAgents();
|
|
6690
|
+
const needle = opts.agent.toLowerCase();
|
|
6691
|
+
const matches = agents.filter(
|
|
6692
|
+
(a) => [a.name, a.slug].some(
|
|
6693
|
+
(v) => v != null && String(v).toLowerCase() === needle
|
|
6694
|
+
)
|
|
6695
|
+
);
|
|
6696
|
+
if (matches.length === 0) {
|
|
6697
|
+
printError(
|
|
6698
|
+
`Agent "${opts.agent}" not found. Pass the UUID or a matching name/slug.`
|
|
6699
|
+
);
|
|
6700
|
+
process.exit(1);
|
|
6701
|
+
}
|
|
6702
|
+
if (matches.length > 1) {
|
|
6703
|
+
printError(
|
|
6704
|
+
`"${opts.agent}" matches ${matches.length} agents. Pass the UUID instead \u2014 candidates: ${matches.map((a) => a.id).join(", ")}`
|
|
6705
|
+
);
|
|
6706
|
+
process.exit(1);
|
|
6707
|
+
}
|
|
6708
|
+
return {
|
|
6709
|
+
mode: "single",
|
|
6710
|
+
agentId: matches[0].id,
|
|
6711
|
+
agentLabel: matches[0].name ?? matches[0].id
|
|
6712
|
+
};
|
|
6713
|
+
}
|
|
6714
|
+
if (opts.all) {
|
|
6715
|
+
if (!allowAll) {
|
|
6716
|
+
printError(
|
|
6717
|
+
"--all is not supported for this command. Rebuild one agent at a time."
|
|
6718
|
+
);
|
|
6719
|
+
process.exit(1);
|
|
6720
|
+
}
|
|
6721
|
+
if (!needList) agents = await loadAgents();
|
|
6722
|
+
else if (agents.length === 0) agents = await loadAgents();
|
|
6723
|
+
return {
|
|
6724
|
+
mode: "all",
|
|
6725
|
+
agentIds: agents.map((a) => a.id),
|
|
6726
|
+
agentLabels: Object.fromEntries(
|
|
6727
|
+
agents.map((a) => [a.id, a.name ?? a.id])
|
|
6728
|
+
)
|
|
6729
|
+
};
|
|
6730
|
+
}
|
|
6731
|
+
if (!isInteractive()) {
|
|
6732
|
+
requireTty(allowAll ? "Agent (or --all)" : "Agent");
|
|
6733
|
+
throw new Error("unreachable");
|
|
6734
|
+
}
|
|
6735
|
+
agents = await loadAgents();
|
|
6736
|
+
if (agents.length === 0) {
|
|
6737
|
+
printError("No agents found for this tenant.");
|
|
6738
|
+
process.exit(1);
|
|
6739
|
+
}
|
|
6740
|
+
const choices = [];
|
|
6741
|
+
if (allowAll) {
|
|
6742
|
+
choices.push({ name: "All agents (fan out)", value: "__all__" });
|
|
6743
|
+
}
|
|
6744
|
+
for (const a of agents) {
|
|
6745
|
+
const label = a.name ?? a.id;
|
|
6746
|
+
const slugPart = a.slug ? ` (${a.slug})` : "";
|
|
6747
|
+
choices.push({ name: `${label}${slugPart} [${a.id}]`, value: a.id });
|
|
6748
|
+
}
|
|
6749
|
+
const pick = await promptOrExit(
|
|
6750
|
+
() => select9({
|
|
6751
|
+
message: "Which agent?",
|
|
6752
|
+
choices,
|
|
6753
|
+
loop: false
|
|
6754
|
+
})
|
|
6755
|
+
);
|
|
6756
|
+
if (pick === "__all__") {
|
|
6757
|
+
return {
|
|
6758
|
+
mode: "all",
|
|
6759
|
+
agentIds: agents.map((a) => a.id),
|
|
6760
|
+
agentLabels: Object.fromEntries(
|
|
6761
|
+
agents.map((a) => [a.id, a.name ?? a.id])
|
|
6762
|
+
)
|
|
6763
|
+
};
|
|
6764
|
+
}
|
|
6765
|
+
const picked = agents.find((a) => a.id === pick);
|
|
6766
|
+
return {
|
|
6767
|
+
mode: "single",
|
|
6768
|
+
agentId: picked.id,
|
|
6769
|
+
agentLabel: picked.name ?? picked.id
|
|
6770
|
+
};
|
|
6771
|
+
}
|
|
6772
|
+
function classifyMutationError(err) {
|
|
6773
|
+
const message = err?.message ?? String(err);
|
|
6774
|
+
const forbidden = /Admin-only|Access denied|tenant mismatch|outside tenant/i.test(message);
|
|
6775
|
+
return { forbidden, message };
|
|
6776
|
+
}
|
|
6777
|
+
function printForbiddenHint(tenantSlug) {
|
|
6778
|
+
printError(
|
|
6779
|
+
`Admin access to tenant "${tenantSlug}" is required for wiki operations. Ask your tenant owner to promote your membership or use an admin API key.`
|
|
6780
|
+
);
|
|
6781
|
+
}
|
|
6782
|
+
function isTerminalCompileStatus(status) {
|
|
6783
|
+
return status === "succeeded" || status === "failed" || status === "cancelled" || status === "skipped";
|
|
6784
|
+
}
|
|
6785
|
+
function shortJobId(id) {
|
|
6786
|
+
return id.slice(0, 8);
|
|
6787
|
+
}
|
|
6788
|
+
|
|
6789
|
+
// src/commands/wiki/compile.ts
|
|
6790
|
+
async function runWikiCompile(opts) {
|
|
6791
|
+
const ctx = await resolveWikiContext(opts);
|
|
6792
|
+
const scope = await resolveAgentScope(ctx, opts, { allowAll: true });
|
|
6793
|
+
const targets = scope.mode === "single" ? [{ id: scope.agentId, label: scope.agentLabel }] : scope.agentIds.map((id) => ({
|
|
6794
|
+
id,
|
|
6795
|
+
label: scope.agentLabels[id] ?? id
|
|
6796
|
+
}));
|
|
6797
|
+
if (targets.length === 0) {
|
|
6798
|
+
if (isJsonMode()) {
|
|
6799
|
+
printJson({
|
|
6800
|
+
ok: true,
|
|
6801
|
+
scope: { tenantId: ctx.tenantId, tenantSlug: ctx.tenantSlug, agentIds: [] },
|
|
6802
|
+
jobs: [],
|
|
6803
|
+
errors: []
|
|
6804
|
+
});
|
|
6805
|
+
} else {
|
|
6806
|
+
printWarning("No agents found for this tenant \u2014 nothing to compile.");
|
|
6807
|
+
}
|
|
6808
|
+
return;
|
|
6809
|
+
}
|
|
6810
|
+
const jobs = [];
|
|
6811
|
+
const errors = [];
|
|
6812
|
+
let forbiddenHit = false;
|
|
6813
|
+
for (const target of targets) {
|
|
6814
|
+
const spinner = isJsonMode() || scope.mode === "all" ? null : ora4({
|
|
6815
|
+
text: `Enqueuing compile for ${target.label}\u2026`,
|
|
6816
|
+
prefixText: " "
|
|
6817
|
+
}).start();
|
|
6818
|
+
try {
|
|
6819
|
+
const data = await gqlMutate(ctx.client, CompileWikiNowDoc, {
|
|
6820
|
+
tenantId: ctx.tenantId,
|
|
6821
|
+
ownerId: target.id,
|
|
6822
|
+
modelId: opts.model ?? null
|
|
6823
|
+
});
|
|
6824
|
+
const job = data.compileWikiNow;
|
|
6825
|
+
jobs.push({
|
|
6826
|
+
agentId: target.id,
|
|
6827
|
+
agentLabel: target.label,
|
|
6828
|
+
jobId: job.id,
|
|
6829
|
+
status: job.status,
|
|
6830
|
+
error: null
|
|
6831
|
+
});
|
|
6832
|
+
if (spinner) {
|
|
6833
|
+
spinner.succeed(
|
|
6834
|
+
`${target.label} \u2192 job=${shortJobId(job.id)} (${job.status})`
|
|
6835
|
+
);
|
|
6836
|
+
} else if (!isJsonMode()) {
|
|
6837
|
+
console.log(
|
|
6838
|
+
` \u2713 ${target.label} \u2192 job=${shortJobId(job.id)} (${job.status})`
|
|
6839
|
+
);
|
|
6840
|
+
}
|
|
6841
|
+
} catch (err) {
|
|
6842
|
+
const classified = classifyMutationError(err);
|
|
6843
|
+
errors.push({ agentId: target.id, message: classified.message });
|
|
6844
|
+
jobs.push({
|
|
6845
|
+
agentId: target.id,
|
|
6846
|
+
agentLabel: target.label,
|
|
6847
|
+
jobId: null,
|
|
6848
|
+
status: null,
|
|
6849
|
+
error: classified.message
|
|
6850
|
+
});
|
|
6851
|
+
if (spinner) spinner.fail(`${target.label} \u2192 ${classified.message}`);
|
|
6852
|
+
else if (!isJsonMode())
|
|
6853
|
+
console.log(` \u2717 ${target.label} \u2192 ${classified.message}`);
|
|
6854
|
+
if (classified.forbidden) {
|
|
6855
|
+
forbiddenHit = true;
|
|
6856
|
+
if (scope.mode === "all") {
|
|
6857
|
+
break;
|
|
6858
|
+
}
|
|
6859
|
+
}
|
|
6860
|
+
}
|
|
6861
|
+
}
|
|
6862
|
+
const anyFailed = errors.length > 0;
|
|
6863
|
+
const ok = !anyFailed;
|
|
6864
|
+
if (isJsonMode()) {
|
|
6865
|
+
printJson({
|
|
6866
|
+
ok,
|
|
6867
|
+
scope: {
|
|
6868
|
+
tenantId: ctx.tenantId,
|
|
6869
|
+
tenantSlug: ctx.tenantSlug,
|
|
6870
|
+
mode: scope.mode,
|
|
6871
|
+
agentIds: targets.map((t) => t.id)
|
|
6872
|
+
},
|
|
6873
|
+
model: opts.model ?? null,
|
|
6874
|
+
jobs,
|
|
6875
|
+
errors
|
|
6876
|
+
});
|
|
6877
|
+
} else if (scope.mode === "all") {
|
|
6878
|
+
console.log("");
|
|
6879
|
+
printKeyValue([
|
|
6880
|
+
["Tenant", ctx.tenantSlug],
|
|
6881
|
+
["Agents queued", `${jobs.filter((j) => j.jobId).length} / ${targets.length}`],
|
|
6882
|
+
["Failures", String(errors.length)],
|
|
6883
|
+
["Model override", opts.model ?? "(default)"]
|
|
6884
|
+
]);
|
|
6885
|
+
}
|
|
6886
|
+
if (forbiddenHit) {
|
|
6887
|
+
printForbiddenHint(ctx.tenantSlug);
|
|
6888
|
+
process.exit(2);
|
|
6889
|
+
}
|
|
6890
|
+
if (anyFailed) {
|
|
6891
|
+
process.exit(1);
|
|
6892
|
+
}
|
|
6893
|
+
if (!isJsonMode() && jobs.length === 1) {
|
|
6894
|
+
printSuccess(`Compile enqueued for ${jobs[0].agentLabel}.`);
|
|
6895
|
+
console.log(
|
|
6896
|
+
` Use \`thinkwork wiki status --tenant ${ctx.tenantSlug} --agent ${jobs[0].agentId} --watch\` to follow the job.`
|
|
6897
|
+
);
|
|
6898
|
+
}
|
|
6899
|
+
if (opts.watch && scope.mode === "single" && jobs.length === 1 && jobs[0].jobId) {
|
|
6900
|
+
await watchSingleJob(ctx, {
|
|
6901
|
+
agentId: jobs[0].agentId,
|
|
6902
|
+
jobId: jobs[0].jobId,
|
|
6903
|
+
agentLabel: jobs[0].agentLabel
|
|
6904
|
+
});
|
|
6905
|
+
} else if (opts.watch && scope.mode === "all") {
|
|
6906
|
+
printWarning(
|
|
6907
|
+
"--watch is ignored for --all. Use `thinkwork wiki status --tenant " + ctx.tenantSlug + " --watch` instead."
|
|
6908
|
+
);
|
|
6909
|
+
}
|
|
6910
|
+
}
|
|
6911
|
+
async function watchSingleJob(ctx, target) {
|
|
6912
|
+
const spinner = isJsonMode() ? null : ora4({
|
|
6913
|
+
text: `Watching job ${shortJobId(target.jobId)} for ${target.agentLabel}\u2026`,
|
|
6914
|
+
prefixText: " "
|
|
6915
|
+
}).start();
|
|
6916
|
+
const intervalMs = 3e3;
|
|
6917
|
+
const deadline = Date.now() + 15 * 60 * 1e3;
|
|
6918
|
+
try {
|
|
6919
|
+
while (Date.now() < deadline) {
|
|
6920
|
+
const data = await gqlQuery(ctx.client, WikiCompileJobsDoc, {
|
|
6921
|
+
tenantId: ctx.tenantId,
|
|
6922
|
+
ownerId: target.agentId,
|
|
6923
|
+
limit: 5
|
|
6924
|
+
});
|
|
6925
|
+
const job = data.wikiCompileJobs.find((j) => j.id === target.jobId);
|
|
6926
|
+
if (!job) {
|
|
6927
|
+
if (spinner) spinner.warn("job not visible yet \u2014 polling\u2026");
|
|
6928
|
+
} else {
|
|
6929
|
+
if (spinner) spinner.text = `status=${job.status} attempt=${job.attempt}`;
|
|
6930
|
+
if (isTerminalCompileStatus(job.status)) {
|
|
6931
|
+
if (spinner) {
|
|
6932
|
+
if (job.status === "succeeded") spinner.succeed(`succeeded`);
|
|
6933
|
+
else if (job.status === "skipped") spinner.info("skipped");
|
|
6934
|
+
else spinner.fail(`${job.status}${job.error ? ` \u2014 ${job.error}` : ""}`);
|
|
6935
|
+
}
|
|
6936
|
+
if (isJsonMode()) {
|
|
6937
|
+
printJson({
|
|
6938
|
+
ok: job.status === "succeeded",
|
|
6939
|
+
jobId: job.id,
|
|
6940
|
+
status: job.status,
|
|
6941
|
+
error: job.error,
|
|
6942
|
+
metrics: job.metrics
|
|
6943
|
+
});
|
|
6944
|
+
}
|
|
6945
|
+
process.exit(job.status === "succeeded" ? 0 : 1);
|
|
6946
|
+
}
|
|
6947
|
+
}
|
|
6948
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
6949
|
+
}
|
|
6950
|
+
if (spinner) spinner.warn("watch timeout \u2014 job still in progress.");
|
|
6951
|
+
process.exit(2);
|
|
6952
|
+
} catch (err) {
|
|
6953
|
+
if (spinner) spinner.fail(err instanceof Error ? err.message : String(err));
|
|
6954
|
+
printError("Watch failed. The compile job itself may still complete.");
|
|
6955
|
+
process.exit(1);
|
|
6956
|
+
}
|
|
6957
|
+
}
|
|
6958
|
+
|
|
6959
|
+
// src/commands/wiki/rebuild.ts
|
|
6960
|
+
import { confirm as confirm5 } from "@inquirer/prompts";
|
|
6961
|
+
import ora5 from "ora";
|
|
6962
|
+
async function runWikiRebuild(opts) {
|
|
6963
|
+
if (opts.all) {
|
|
6964
|
+
printError(
|
|
6965
|
+
"--all is not supported for rebuild. Rebuild one agent at a time to avoid mass-archiving pages across the tenant."
|
|
6966
|
+
);
|
|
6967
|
+
process.exit(1);
|
|
6968
|
+
}
|
|
6969
|
+
const ctx = await resolveWikiContext(opts);
|
|
6970
|
+
const scope = await resolveAgentScope(ctx, opts, { allowAll: false });
|
|
6971
|
+
if (scope.mode !== "single") {
|
|
6972
|
+
printError("rebuild requires a single agent.");
|
|
6973
|
+
process.exit(1);
|
|
6974
|
+
}
|
|
6975
|
+
const { agentId, agentLabel } = scope;
|
|
6976
|
+
const skipConfirm = opts.yes === true || isJsonMode();
|
|
6977
|
+
if (!skipConfirm) {
|
|
6978
|
+
if (!isInteractive()) {
|
|
6979
|
+
requireTty("Rebuild confirmation (--yes)");
|
|
6980
|
+
}
|
|
6981
|
+
const ok = await promptOrExit(
|
|
6982
|
+
() => confirm5({
|
|
6983
|
+
message: `Rebuild wiki for ${agentLabel}? This archives every active page in the scope and recompiles from scratch.`,
|
|
6984
|
+
default: false
|
|
6985
|
+
})
|
|
6986
|
+
);
|
|
6987
|
+
if (!ok) {
|
|
6988
|
+
if (!isJsonMode()) console.log(" Cancelled.");
|
|
6989
|
+
process.exit(0);
|
|
6990
|
+
}
|
|
6991
|
+
}
|
|
6992
|
+
const resetSpinner = isJsonMode() ? null : ora5({
|
|
6993
|
+
text: `Archiving active pages for ${agentLabel}\u2026`,
|
|
6994
|
+
prefixText: " "
|
|
6995
|
+
}).start();
|
|
6996
|
+
let pagesArchived = 0;
|
|
6997
|
+
try {
|
|
6998
|
+
const data = await gqlMutate(ctx.client, ResetWikiCursorDoc, {
|
|
6999
|
+
tenantId: ctx.tenantId,
|
|
7000
|
+
ownerId: agentId,
|
|
7001
|
+
force: true
|
|
7002
|
+
});
|
|
7003
|
+
pagesArchived = data.resetWikiCursor.pagesArchived;
|
|
7004
|
+
if (resetSpinner)
|
|
7005
|
+
resetSpinner.succeed(
|
|
7006
|
+
`${pagesArchived} page${pagesArchived === 1 ? "" : "s"} archived, cursor cleared.`
|
|
7007
|
+
);
|
|
7008
|
+
} catch (err) {
|
|
7009
|
+
const classified = classifyMutationError(err);
|
|
7010
|
+
if (resetSpinner) resetSpinner.fail(`Reset failed: ${classified.message}`);
|
|
7011
|
+
const result2 = {
|
|
7012
|
+
ok: false,
|
|
7013
|
+
scope: {
|
|
7014
|
+
tenantId: ctx.tenantId,
|
|
7015
|
+
tenantSlug: ctx.tenantSlug,
|
|
7016
|
+
agentId
|
|
7017
|
+
},
|
|
7018
|
+
pagesArchived: null,
|
|
7019
|
+
jobId: null,
|
|
7020
|
+
error: classified.message
|
|
7021
|
+
};
|
|
7022
|
+
if (isJsonMode()) printJson(result2);
|
|
7023
|
+
if (classified.forbidden) {
|
|
7024
|
+
printForbiddenHint(ctx.tenantSlug);
|
|
7025
|
+
process.exit(2);
|
|
7026
|
+
}
|
|
7027
|
+
process.exit(1);
|
|
7028
|
+
}
|
|
7029
|
+
const compileSpinner = isJsonMode() ? null : ora5({
|
|
7030
|
+
text: `Enqueuing fresh compile for ${agentLabel}\u2026`,
|
|
7031
|
+
prefixText: " "
|
|
7032
|
+
}).start();
|
|
7033
|
+
let jobId = null;
|
|
7034
|
+
try {
|
|
7035
|
+
const data = await gqlMutate(ctx.client, CompileWikiNowDoc, {
|
|
7036
|
+
tenantId: ctx.tenantId,
|
|
7037
|
+
ownerId: agentId,
|
|
7038
|
+
modelId: opts.model ?? null
|
|
7039
|
+
});
|
|
7040
|
+
jobId = data.compileWikiNow.id;
|
|
7041
|
+
if (compileSpinner)
|
|
7042
|
+
compileSpinner.succeed(
|
|
7043
|
+
`Compile enqueued \u2014 job=${shortJobId(jobId)} status=${data.compileWikiNow.status}`
|
|
7044
|
+
);
|
|
7045
|
+
} catch (err) {
|
|
7046
|
+
const classified = classifyMutationError(err);
|
|
7047
|
+
if (compileSpinner)
|
|
7048
|
+
compileSpinner.fail(`Compile enqueue failed: ${classified.message}`);
|
|
7049
|
+
const result2 = {
|
|
7050
|
+
ok: false,
|
|
7051
|
+
scope: {
|
|
7052
|
+
tenantId: ctx.tenantId,
|
|
7053
|
+
tenantSlug: ctx.tenantSlug,
|
|
7054
|
+
agentId
|
|
7055
|
+
},
|
|
7056
|
+
pagesArchived,
|
|
7057
|
+
jobId: null,
|
|
7058
|
+
error: classified.message
|
|
7059
|
+
};
|
|
7060
|
+
if (isJsonMode()) printJson(result2);
|
|
7061
|
+
else {
|
|
7062
|
+
printWarning(
|
|
7063
|
+
`Reset succeeded (${pagesArchived} page${pagesArchived === 1 ? "" : "s"} archived) but compile enqueue failed. Retry with:
|
|
7064
|
+
thinkwork wiki compile --tenant ${ctx.tenantSlug} --agent ${agentId}`
|
|
7065
|
+
);
|
|
7066
|
+
}
|
|
7067
|
+
if (classified.forbidden) {
|
|
7068
|
+
printForbiddenHint(ctx.tenantSlug);
|
|
7069
|
+
process.exit(2);
|
|
7070
|
+
}
|
|
7071
|
+
process.exit(1);
|
|
7072
|
+
}
|
|
7073
|
+
const result = {
|
|
7074
|
+
ok: true,
|
|
7075
|
+
scope: {
|
|
7076
|
+
tenantId: ctx.tenantId,
|
|
7077
|
+
tenantSlug: ctx.tenantSlug,
|
|
7078
|
+
agentId
|
|
7079
|
+
},
|
|
7080
|
+
pagesArchived,
|
|
7081
|
+
jobId,
|
|
7082
|
+
error: null
|
|
7083
|
+
};
|
|
7084
|
+
if (isJsonMode()) {
|
|
7085
|
+
printJson(result);
|
|
7086
|
+
} else {
|
|
7087
|
+
console.log("");
|
|
7088
|
+
printKeyValue([
|
|
7089
|
+
["Tenant", ctx.tenantSlug],
|
|
7090
|
+
["Agent", agentLabel],
|
|
7091
|
+
["Pages archived", String(pagesArchived)],
|
|
7092
|
+
["Compile job", jobId ?? "\u2014"],
|
|
7093
|
+
["Model override", opts.model ?? "(default)"]
|
|
7094
|
+
]);
|
|
7095
|
+
printSuccess(`Rebuild enqueued for ${agentLabel}.`);
|
|
7096
|
+
}
|
|
7097
|
+
if (opts.watch && jobId) {
|
|
7098
|
+
await watchRebuildJob(ctx, { agentId, jobId, agentLabel });
|
|
7099
|
+
}
|
|
7100
|
+
}
|
|
7101
|
+
async function watchRebuildJob(ctx, target) {
|
|
7102
|
+
const spinner = isJsonMode() ? null : ora5({
|
|
7103
|
+
text: `Watching rebuild job ${shortJobId(target.jobId)}\u2026`,
|
|
7104
|
+
prefixText: " "
|
|
7105
|
+
}).start();
|
|
7106
|
+
const intervalMs = 3e3;
|
|
7107
|
+
const deadline = Date.now() + 15 * 60 * 1e3;
|
|
7108
|
+
try {
|
|
7109
|
+
while (Date.now() < deadline) {
|
|
7110
|
+
const data = await gqlQuery(ctx.client, WikiCompileJobsDoc, {
|
|
7111
|
+
tenantId: ctx.tenantId,
|
|
7112
|
+
ownerId: target.agentId,
|
|
7113
|
+
limit: 5
|
|
7114
|
+
});
|
|
7115
|
+
const job = data.wikiCompileJobs.find((j) => j.id === target.jobId);
|
|
7116
|
+
if (job) {
|
|
7117
|
+
if (spinner) spinner.text = `status=${job.status} attempt=${job.attempt}`;
|
|
7118
|
+
if (isTerminalCompileStatus(job.status)) {
|
|
7119
|
+
if (spinner) {
|
|
7120
|
+
if (job.status === "succeeded") spinner.succeed("rebuild succeeded");
|
|
7121
|
+
else if (job.status === "skipped") spinner.info("rebuild skipped");
|
|
7122
|
+
else spinner.fail(`${job.status}${job.error ? ` \u2014 ${job.error}` : ""}`);
|
|
7123
|
+
}
|
|
7124
|
+
process.exit(job.status === "succeeded" ? 0 : 1);
|
|
7125
|
+
}
|
|
7126
|
+
}
|
|
7127
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
7128
|
+
}
|
|
7129
|
+
if (spinner) spinner.warn("watch timeout \u2014 rebuild still in progress.");
|
|
7130
|
+
process.exit(2);
|
|
7131
|
+
} catch (err) {
|
|
7132
|
+
if (spinner) spinner.fail(err instanceof Error ? err.message : String(err));
|
|
7133
|
+
process.exit(1);
|
|
7134
|
+
}
|
|
7135
|
+
}
|
|
7136
|
+
|
|
7137
|
+
// src/commands/wiki/status.ts
|
|
7138
|
+
import ora6 from "ora";
|
|
7139
|
+
var DEFAULT_WATCH_INTERVAL_MS = 3e3;
|
|
7140
|
+
async function runWikiStatus(opts) {
|
|
7141
|
+
const ctx = await resolveWikiContext(opts);
|
|
7142
|
+
const ownerId = opts.agent ?? null;
|
|
7143
|
+
const limit = toInt(opts.limit, 10);
|
|
7144
|
+
const timeoutSec = toInt(opts.timeout, 900);
|
|
7145
|
+
let agentNameById = null;
|
|
7146
|
+
const resolveAgentName = async () => {
|
|
7147
|
+
if (agentNameById) return agentNameById;
|
|
7148
|
+
const data = await gqlQuery(ctx.client, AllTenantAgentsForWikiDoc, {
|
|
7149
|
+
tenantId: ctx.tenantId
|
|
7150
|
+
});
|
|
7151
|
+
agentNameById = Object.fromEntries(
|
|
7152
|
+
(data.allTenantAgents ?? []).map((a) => [a.id, a.name ?? a.id])
|
|
7153
|
+
);
|
|
7154
|
+
return agentNameById;
|
|
7155
|
+
};
|
|
7156
|
+
let jobs;
|
|
7157
|
+
try {
|
|
7158
|
+
jobs = await fetchJobs(ctx, { ownerId, limit });
|
|
7159
|
+
} catch (err) {
|
|
7160
|
+
const classified = classifyMutationError(err);
|
|
7161
|
+
printError(classified.message);
|
|
7162
|
+
if (classified.forbidden) {
|
|
7163
|
+
printForbiddenHint(ctx.tenantSlug);
|
|
7164
|
+
process.exit(2);
|
|
7165
|
+
}
|
|
7166
|
+
process.exit(1);
|
|
7167
|
+
}
|
|
7168
|
+
if (!opts.watch) {
|
|
7169
|
+
await renderJobs(jobs, {
|
|
7170
|
+
tenantSlug: ctx.tenantSlug,
|
|
7171
|
+
scope: ownerId ? { agentId: ownerId } : { tenantWide: true },
|
|
7172
|
+
resolveAgentName
|
|
7173
|
+
});
|
|
7174
|
+
process.exit(0);
|
|
7175
|
+
}
|
|
7176
|
+
if (jobs.length === 0) {
|
|
7177
|
+
printWarning(
|
|
7178
|
+
`No compile jobs yet for this scope. --watch exits immediately; re-run once activity starts.`
|
|
7179
|
+
);
|
|
7180
|
+
if (isJsonMode()) {
|
|
7181
|
+
printJson({
|
|
7182
|
+
ok: true,
|
|
7183
|
+
scope: { tenantId: ctx.tenantId, ownerId },
|
|
7184
|
+
jobs: []
|
|
7185
|
+
});
|
|
7186
|
+
}
|
|
7187
|
+
process.exit(0);
|
|
7188
|
+
}
|
|
7189
|
+
const latestId = jobs[0].id;
|
|
7190
|
+
const spinner = isJsonMode() ? null : ora6({
|
|
7191
|
+
text: `Watching job ${shortJobId(latestId)}\u2026`,
|
|
7192
|
+
prefixText: " "
|
|
7193
|
+
}).start();
|
|
7194
|
+
const deadline = Date.now() + timeoutSec * 1e3;
|
|
7195
|
+
try {
|
|
7196
|
+
while (Date.now() < deadline) {
|
|
7197
|
+
const next = await fetchJobs(ctx, { ownerId, limit: 5 });
|
|
7198
|
+
const latest = next.find((j) => j.id === latestId) ?? next[0];
|
|
7199
|
+
if (latest) {
|
|
7200
|
+
if (spinner)
|
|
7201
|
+
spinner.text = `status=${latest.status} attempt=${latest.attempt}`;
|
|
7202
|
+
if (isTerminalCompileStatus(latest.status)) {
|
|
7203
|
+
if (spinner) {
|
|
7204
|
+
if (latest.status === "succeeded") spinner.succeed("succeeded");
|
|
7205
|
+
else if (latest.status === "skipped") spinner.info("skipped");
|
|
7206
|
+
else
|
|
7207
|
+
spinner.fail(
|
|
7208
|
+
`${latest.status}${latest.error ? ` \u2014 ${latest.error}` : ""}`
|
|
7209
|
+
);
|
|
7210
|
+
}
|
|
7211
|
+
if (isJsonMode()) {
|
|
7212
|
+
printJson({
|
|
7213
|
+
ok: latest.status === "succeeded",
|
|
7214
|
+
scope: { tenantId: ctx.tenantId, ownerId },
|
|
7215
|
+
job: latest
|
|
7216
|
+
});
|
|
7217
|
+
}
|
|
7218
|
+
process.exit(latest.status === "succeeded" ? 0 : 2);
|
|
7219
|
+
}
|
|
7220
|
+
}
|
|
7221
|
+
await new Promise((r) => setTimeout(r, DEFAULT_WATCH_INTERVAL_MS));
|
|
7222
|
+
}
|
|
7223
|
+
if (spinner) spinner.warn(`watch timeout after ${timeoutSec}s.`);
|
|
7224
|
+
process.exit(2);
|
|
7225
|
+
} catch (err) {
|
|
7226
|
+
if (spinner) spinner.fail(err instanceof Error ? err.message : String(err));
|
|
7227
|
+
process.exit(1);
|
|
7228
|
+
}
|
|
7229
|
+
}
|
|
7230
|
+
async function fetchJobs(ctx, args) {
|
|
7231
|
+
const data = await gqlQuery(ctx.client, WikiCompileJobsDoc, {
|
|
7232
|
+
tenantId: ctx.tenantId,
|
|
7233
|
+
ownerId: args.ownerId,
|
|
7234
|
+
limit: args.limit
|
|
7235
|
+
});
|
|
7236
|
+
return data.wikiCompileJobs;
|
|
7237
|
+
}
|
|
7238
|
+
async function renderJobs(jobs, args) {
|
|
7239
|
+
if (isJsonMode()) {
|
|
7240
|
+
printJson({
|
|
7241
|
+
ok: true,
|
|
7242
|
+
scope: "agentId" in args.scope ? { agentId: args.scope.agentId } : { tenantWide: true },
|
|
7243
|
+
jobs
|
|
7244
|
+
});
|
|
7245
|
+
return;
|
|
7246
|
+
}
|
|
7247
|
+
if (jobs.length === 0) {
|
|
7248
|
+
const label = "agentId" in args.scope ? `agent ${args.scope.agentId}` : `tenant ${args.tenantSlug}`;
|
|
7249
|
+
console.log(` No recent compile jobs for ${label}.`);
|
|
7250
|
+
return;
|
|
7251
|
+
}
|
|
7252
|
+
const names = "tenantWide" in args.scope ? await args.resolveAgentName() : {};
|
|
7253
|
+
const rows = jobs.map((j) => ({
|
|
7254
|
+
id: j.id.slice(0, 8),
|
|
7255
|
+
agent: "tenantWide" in args.scope ? names[j.ownerId] ?? j.ownerId.slice(0, 8) : "\u2014",
|
|
7256
|
+
status: j.status,
|
|
7257
|
+
trigger: j.trigger,
|
|
7258
|
+
attempt: String(j.attempt),
|
|
7259
|
+
duration: fmtDuration(j.startedAt, j.finishedAt),
|
|
7260
|
+
records: extractMetric(j.metrics, "records_read"),
|
|
7261
|
+
pages: extractMetric(j.metrics, "pages_upserted"),
|
|
7262
|
+
started: fmtIso2(j.startedAt ?? j.createdAt)
|
|
7263
|
+
}));
|
|
7264
|
+
const columns = [
|
|
7265
|
+
{ key: "id", header: "JOB" }
|
|
7266
|
+
];
|
|
7267
|
+
if ("tenantWide" in args.scope) columns.push({ key: "agent", header: "AGENT" });
|
|
7268
|
+
columns.push(
|
|
7269
|
+
{ key: "status", header: "STATUS" },
|
|
7270
|
+
{ key: "trigger", header: "TRIGGER" },
|
|
7271
|
+
{ key: "attempt", header: "TRY" },
|
|
7272
|
+
{ key: "duration", header: "DUR" },
|
|
7273
|
+
{ key: "records", header: "RECS" },
|
|
7274
|
+
{ key: "pages", header: "PAGES" },
|
|
7275
|
+
{ key: "started", header: "STARTED" }
|
|
7276
|
+
);
|
|
7277
|
+
printTable(rows, columns);
|
|
7278
|
+
}
|
|
7279
|
+
function toInt(v, fallback) {
|
|
7280
|
+
if (v == null || v === "") return fallback;
|
|
7281
|
+
const n = typeof v === "number" ? v : Number.parseInt(v, 10);
|
|
7282
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
7283
|
+
}
|
|
7284
|
+
function fmtIso2(iso) {
|
|
7285
|
+
if (!iso) return "\u2014";
|
|
7286
|
+
const d = new Date(iso);
|
|
7287
|
+
if (Number.isNaN(d.getTime())) return iso;
|
|
7288
|
+
return d.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
7289
|
+
}
|
|
7290
|
+
function fmtDuration(startedAt, finishedAt) {
|
|
7291
|
+
if (!startedAt) return "\u2014";
|
|
7292
|
+
const start = new Date(startedAt).getTime();
|
|
7293
|
+
const end = finishedAt ? new Date(finishedAt).getTime() : Date.now();
|
|
7294
|
+
if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return "\u2014";
|
|
7295
|
+
const sec = Math.round((end - start) / 1e3);
|
|
7296
|
+
if (sec < 60) return `${sec}s`;
|
|
7297
|
+
const m = Math.floor(sec / 60);
|
|
7298
|
+
const s = sec % 60;
|
|
7299
|
+
return `${m}m${s.toString().padStart(2, "0")}s`;
|
|
7300
|
+
}
|
|
7301
|
+
function extractMetric(metrics, key) {
|
|
7302
|
+
if (!metrics || typeof metrics !== "object") return "\u2014";
|
|
7303
|
+
const v = metrics[key];
|
|
7304
|
+
if (v == null) return "\u2014";
|
|
7305
|
+
if (typeof v === "number") return String(v);
|
|
7306
|
+
return String(v);
|
|
7307
|
+
}
|
|
7308
|
+
|
|
7309
|
+
// src/commands/wiki.ts
|
|
7310
|
+
function registerWikiCommand(program2) {
|
|
7311
|
+
const wiki = program2.command("wiki").description(
|
|
7312
|
+
"Compile and rebuild agent wiki pages (Compounding Memory). Admin-only."
|
|
7313
|
+
);
|
|
7314
|
+
wiki.command("compile").description(
|
|
7315
|
+
"Enqueue a wiki compile for a single agent (--agent) or all tenant agents (--all)."
|
|
7316
|
+
).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--agent <id>", "Agent ID, slug, or name. Bypasses the picker.").option("--all", "Fan out to every non-system agent in the tenant.").option(
|
|
7317
|
+
"--model <id>",
|
|
7318
|
+
"Bedrock model ID override for this run. Defaults to server BEDROCK_MODEL_ID."
|
|
7319
|
+
).option(
|
|
7320
|
+
"--watch",
|
|
7321
|
+
"After enqueue, poll wiki_compile_jobs until the job reaches a terminal state (single-agent only)."
|
|
7322
|
+
).addHelpText(
|
|
7323
|
+
"after",
|
|
7324
|
+
`
|
|
7325
|
+
Examples:
|
|
7326
|
+
# Interactive: pick the agent (or "All agents") from a list
|
|
7327
|
+
$ thinkwork wiki compile
|
|
7328
|
+
|
|
7329
|
+
# Single agent, scripted
|
|
7330
|
+
$ thinkwork wiki compile --tenant acme --agent agt-xyz --json
|
|
7331
|
+
|
|
7332
|
+
# Fan-out across every agent in the tenant
|
|
7333
|
+
$ thinkwork wiki compile --tenant acme --all
|
|
7334
|
+
|
|
7335
|
+
# Spike a different Bedrock model for one run
|
|
7336
|
+
$ thinkwork wiki compile --tenant acme --agent agt-xyz \\
|
|
7337
|
+
--model anthropic.claude-sonnet-4-6-v1:0
|
|
7338
|
+
`
|
|
7339
|
+
).action(async (opts, cmd) => {
|
|
7340
|
+
const parent = cmd.parent?.parent;
|
|
7341
|
+
await runWikiCompile({
|
|
7342
|
+
...opts,
|
|
7343
|
+
stage: opts.stage ?? parent?.opts().stage,
|
|
7344
|
+
json: parent?.opts().json === true
|
|
7345
|
+
});
|
|
7346
|
+
});
|
|
7347
|
+
wiki.command("rebuild").description(
|
|
7348
|
+
"Destructive: archive an agent's active wiki pages and enqueue a fresh compile. Single-agent only."
|
|
7349
|
+
).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--agent <id>", "Agent ID, slug, or name. Bypasses the picker.").option(
|
|
7350
|
+
"--model <id>",
|
|
7351
|
+
"Bedrock model ID override for the post-reset compile."
|
|
7352
|
+
).option("-y, --yes", "Skip the confirmation prompt.").option(
|
|
7353
|
+
"--watch",
|
|
7354
|
+
"After enqueue, poll wiki_compile_jobs until the job reaches a terminal state."
|
|
7355
|
+
).addHelpText(
|
|
7356
|
+
"after",
|
|
7357
|
+
`
|
|
7358
|
+
Examples:
|
|
7359
|
+
# Interactive confirm
|
|
7360
|
+
$ thinkwork wiki rebuild --tenant acme --agent agt-xyz
|
|
7361
|
+
|
|
7362
|
+
# Scripted (no prompt)
|
|
7363
|
+
$ thinkwork wiki rebuild --tenant acme --agent agt-xyz --yes --json
|
|
7364
|
+
`
|
|
7365
|
+
).action(async (opts, cmd) => {
|
|
7366
|
+
const parent = cmd.parent?.parent;
|
|
7367
|
+
await runWikiRebuild({
|
|
7368
|
+
...opts,
|
|
7369
|
+
stage: opts.stage ?? parent?.opts().stage,
|
|
7370
|
+
json: parent?.opts().json === true
|
|
7371
|
+
});
|
|
7372
|
+
});
|
|
7373
|
+
wiki.command("status").description(
|
|
7374
|
+
"Show recent compile jobs for a tenant (optionally filtered to one agent)."
|
|
7375
|
+
).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option(
|
|
7376
|
+
"--agent <id>",
|
|
7377
|
+
"Restrict to a single agent. Omit for tenant-wide recent activity."
|
|
7378
|
+
).option("-n, --limit <n>", "Max jobs to return.", "10").option("--watch", "Poll until the most-recent job reaches a terminal state.").option(
|
|
7379
|
+
"--timeout <seconds>",
|
|
7380
|
+
"Max seconds to watch (default 900 = 15m).",
|
|
7381
|
+
"900"
|
|
7382
|
+
).addHelpText(
|
|
7383
|
+
"after",
|
|
7384
|
+
`
|
|
7385
|
+
Examples:
|
|
7386
|
+
# Tenant-wide (admin)
|
|
7387
|
+
$ thinkwork wiki status --tenant acme
|
|
7388
|
+
|
|
7389
|
+
# Single agent, watching until a job settles
|
|
7390
|
+
$ thinkwork wiki status --tenant acme --agent agt-xyz --watch
|
|
7391
|
+
`
|
|
7392
|
+
).action(async (opts, cmd) => {
|
|
7393
|
+
const parent = cmd.parent?.parent;
|
|
7394
|
+
await runWikiStatus({
|
|
7395
|
+
...opts,
|
|
7396
|
+
stage: opts.stage ?? parent?.opts().stage,
|
|
7397
|
+
json: parent?.opts().json === true
|
|
7398
|
+
});
|
|
7399
|
+
});
|
|
7400
|
+
}
|
|
7401
|
+
|
|
4615
7402
|
// src/cli.ts
|
|
4616
7403
|
var program = new Command();
|
|
4617
7404
|
program.name("thinkwork").description(
|
|
@@ -4656,6 +7443,7 @@ registerMessageCommand(program);
|
|
|
4656
7443
|
registerLabelCommand(program);
|
|
4657
7444
|
registerInboxCommand(program);
|
|
4658
7445
|
registerAgentCommand(program);
|
|
7446
|
+
registerComputerCommand(program);
|
|
4659
7447
|
registerTemplateCommand(program);
|
|
4660
7448
|
registerTenantCommand(program);
|
|
4661
7449
|
registerMemberCommand(program);
|
|
@@ -4666,7 +7454,6 @@ registerScheduledJobCommand(program);
|
|
|
4666
7454
|
registerTurnCommand(program);
|
|
4667
7455
|
registerWakeupCommand(program);
|
|
4668
7456
|
registerWebhookCommand(program);
|
|
4669
|
-
registerConnectorCommand(program);
|
|
4670
7457
|
registerSkillCommand(program);
|
|
4671
7458
|
registerMemoryCommand(program);
|
|
4672
7459
|
registerRecipeCommand(program);
|
|
@@ -4676,6 +7463,8 @@ registerBudgetCommand(program);
|
|
|
4676
7463
|
registerPerformanceCommand(program);
|
|
4677
7464
|
registerTraceCommand(program);
|
|
4678
7465
|
registerDashboardCommand(program);
|
|
7466
|
+
registerEvalCommand(program);
|
|
7467
|
+
registerWikiCommand(program);
|
|
4679
7468
|
for (const cmd of program.commands) {
|
|
4680
7469
|
if (!cmd.options.some((o) => o.long === "--json")) {
|
|
4681
7470
|
cmd.option("--json", "Emit machine-readable JSON on stdout.");
|