thinkwork-cli 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +727 -548
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -251,41 +251,141 @@ function printSummary(command, stage, tiers, startTime) {
251
251
  console.log(chalk2.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"));
252
252
  }
253
253
 
254
- // src/commands/plan.ts
255
- function registerPlanCommand(program2) {
256
- program2.command("plan").description("Run terraform plan for a stage").option("-p, --profile <name>", "AWS profile").requiredOption("-s, --stage <name>", "Deployment stage").option("-c, --component <tier>", "Component tier (foundation|data|app|all)", "all").action(async (opts) => {
257
- const startTime = Date.now();
258
- const stageCheck = validateStage(opts.stage);
259
- if (!stageCheck.valid) {
260
- printError(stageCheck.error);
261
- process.exit(1);
254
+ // src/lib/resolve-stage.ts
255
+ import { select } from "@inquirer/prompts";
256
+
257
+ // src/aws-discovery.ts
258
+ import { execSync as execSync2 } from "child_process";
259
+ function runAws(cmd) {
260
+ try {
261
+ return execSync2(`aws ${cmd}`, {
262
+ encoding: "utf-8",
263
+ timeout: 15e3,
264
+ stdio: ["pipe", "pipe", "pipe"]
265
+ }).trim();
266
+ } catch {
267
+ return null;
268
+ }
269
+ }
270
+ function listDeployedStages(region) {
271
+ const raw = runAws(
272
+ `lambda list-functions --region ${region} --query "Functions[?starts_with(FunctionName, 'thinkwork-')].FunctionName" --output json`
273
+ );
274
+ if (!raw) return [];
275
+ try {
276
+ const functions = JSON.parse(raw);
277
+ const stages = /* @__PURE__ */ new Set();
278
+ for (const fn of functions) {
279
+ const m = fn.match(/^thinkwork-(.+?)-api-graphql-http$/);
280
+ if (m) stages.add(m[1]);
262
281
  }
263
- const compCheck = validateComponent(opts.component);
264
- if (!compCheck.valid) {
265
- printError(compCheck.error);
282
+ return [...stages].sort();
283
+ } catch {
284
+ return [];
285
+ }
286
+ }
287
+ function getApiEndpoint(stage, region) {
288
+ const raw = runAws(
289
+ `apigatewayv2 get-apis --region ${region} --query "Items[?Name=='thinkwork-${stage}-api'].ApiEndpoint|[0]" --output text`
290
+ );
291
+ return raw && raw !== "None" ? raw : null;
292
+ }
293
+ function getApiAuthSecretFromLambda(stage, region) {
294
+ const raw = runAws(
295
+ `lambda get-function-configuration --function-name thinkwork-${stage}-api-tenants --region ${region} --query "Environment.Variables.API_AUTH_SECRET" --output text`
296
+ );
297
+ return raw && raw !== "None" ? raw : null;
298
+ }
299
+
300
+ // src/lib/interactive.ts
301
+ function isCancellation(err) {
302
+ return err instanceof Error && err.name === "ExitPromptError";
303
+ }
304
+ function isInteractive() {
305
+ return Boolean(process.stdin.isTTY);
306
+ }
307
+ function requireTty(label) {
308
+ if (!isInteractive()) {
309
+ printError(
310
+ `${label} is required. Pass it as a flag or re-run in an interactive terminal.`
311
+ );
312
+ process.exit(1);
313
+ }
314
+ }
315
+
316
+ // src/lib/resolve-stage.ts
317
+ async function resolveStage(opts = {}) {
318
+ const region = opts.region ?? "us-east-1";
319
+ const validate = opts.validate ?? true;
320
+ const raw = opts.flag ?? process.env.THINKWORK_STAGE ?? loadCliConfig().defaultStage ?? await pickStage(region);
321
+ if (!raw) {
322
+ printError(
323
+ "No stage specified. Pass `--stage <name>`, set THINKWORK_STAGE, or run `thinkwork login --stage <name>`."
324
+ );
325
+ process.exit(1);
326
+ }
327
+ if (validate) {
328
+ const check = validateStage(raw);
329
+ if (!check.valid) {
330
+ printError(check.error);
266
331
  process.exit(1);
267
332
  }
268
- const identity = getAwsIdentity();
269
- printHeader("plan", opts.stage, identity);
270
- const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
271
- const tiers = expandComponent(opts.component);
272
- for (let i = 0; i < tiers.length; i++) {
273
- const tier = tiers[i];
274
- printTierHeader(tier, i, tiers.length);
275
- const cwd = resolveTierDir(terraformDir, opts.stage, tier);
276
- await ensureInit(cwd);
277
- await ensureWorkspace(cwd, opts.stage);
278
- const code = await runTerraform(cwd, [
279
- "plan",
280
- `-var=stage=${opts.stage}`
281
- ]);
282
- if (code !== 0) {
283
- printError(`Plan failed for ${tier} (exit ${code})`);
284
- process.exit(code);
333
+ }
334
+ return raw;
335
+ }
336
+ async function pickStage(region) {
337
+ const stages = listDeployedStages(region);
338
+ if (stages.length === 0) {
339
+ printError(
340
+ `No Thinkwork deployments found in ${region}. Run \`thinkwork list\` or pass --region.`
341
+ );
342
+ process.exit(1);
343
+ }
344
+ if (stages.length === 1) {
345
+ console.log(` Using the only deployed stage: ${stages[0]}`);
346
+ return stages[0];
347
+ }
348
+ requireTty("Stage");
349
+ return await select({
350
+ message: "Which stage?",
351
+ choices: stages.map((s) => ({ name: s, value: s })),
352
+ loop: false
353
+ });
354
+ }
355
+
356
+ // src/commands/plan.ts
357
+ function registerPlanCommand(program2) {
358
+ program2.command("plan").description("Run terraform plan for a stage. Prompts for stage in a TTY when omitted.").option("-p, --profile <name>", "AWS profile").option("-s, --stage <name>", "Deployment stage").option("-c, --component <tier>", "Component tier (foundation|data|app|all)", "all").action(async (opts) => {
359
+ const startTime = Date.now();
360
+ try {
361
+ const stage = await resolveStage({ flag: opts.stage });
362
+ const compCheck = validateComponent(opts.component);
363
+ if (!compCheck.valid) {
364
+ printError(compCheck.error);
365
+ process.exit(1);
366
+ }
367
+ const identity = getAwsIdentity();
368
+ printHeader("plan", stage, identity);
369
+ const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
370
+ const tiers = expandComponent(opts.component);
371
+ for (let i = 0; i < tiers.length; i++) {
372
+ const tier = tiers[i];
373
+ printTierHeader(tier, i, tiers.length);
374
+ const cwd = resolveTierDir(terraformDir, stage, tier);
375
+ await ensureInit(cwd);
376
+ await ensureWorkspace(cwd, stage);
377
+ const code = await runTerraform(cwd, ["plan", `-var=stage=${stage}`]);
378
+ if (code !== 0) {
379
+ printError(`Plan failed for ${tier} (exit ${code})`);
380
+ process.exit(code);
381
+ }
285
382
  }
383
+ printSuccess("Plan complete");
384
+ printSummary("plan", stage, tiers, startTime);
385
+ } catch (err) {
386
+ if (isCancellation(err)) return;
387
+ throw err;
286
388
  }
287
- printSuccess("Plan complete");
288
- printSummary("plan", opts.stage, tiers, startTime);
289
389
  });
290
390
  }
291
391
 
@@ -306,131 +406,129 @@ async function confirm(message) {
306
406
 
307
407
  // src/commands/deploy.ts
308
408
  function registerDeployCommand(program2) {
309
- program2.command("deploy").description("Run terraform apply for a stage").option("-p, --profile <name>", "AWS profile").requiredOption("-s, --stage <name>", "Deployment stage").option("-c, --component <tier>", "Component tier (foundation|data|app|all)", "all").option("-y, --yes", "Skip interactive confirmation (for CI)").action(async (opts) => {
409
+ program2.command("deploy").description("Run terraform apply for a stage. Prompts for stage in a TTY when omitted.").option("-p, --profile <name>", "AWS profile").option("-s, --stage <name>", "Deployment stage").option("-c, --component <tier>", "Component tier (foundation|data|app|all)", "all").option("-y, --yes", "Skip interactive confirmation (for CI)").action(async (opts) => {
310
410
  const startTime = Date.now();
311
- const stageCheck = validateStage(opts.stage);
312
- if (!stageCheck.valid) {
313
- printError(stageCheck.error);
314
- process.exit(1);
315
- }
316
- const compCheck = validateComponent(opts.component);
317
- if (!compCheck.valid) {
318
- printError(compCheck.error);
319
- process.exit(1);
320
- }
321
- const identity = getAwsIdentity();
322
- printHeader("deploy", opts.stage, identity);
323
- if (!identity) {
324
- printWarning("Could not resolve AWS identity. Is the AWS CLI configured?");
325
- }
326
- if (isProdLike(opts.stage) && !opts.yes) {
327
- const ok = await confirm(
328
- ` Stage "${opts.stage}" is production-like. Deploy?`
329
- );
330
- if (!ok) {
331
- console.log(" Aborted.");
332
- process.exit(0);
411
+ try {
412
+ const stage = await resolveStage({ flag: opts.stage });
413
+ const compCheck = validateComponent(opts.component);
414
+ if (!compCheck.valid) {
415
+ printError(compCheck.error);
416
+ process.exit(1);
333
417
  }
334
- } else if (!opts.yes) {
335
- const ok = await confirm(` Deploy to stage "${opts.stage}"?`);
336
- if (!ok) {
337
- console.log(" Aborted.");
338
- process.exit(0);
418
+ const identity = getAwsIdentity();
419
+ printHeader("deploy", stage, identity);
420
+ if (!identity) {
421
+ printWarning("Could not resolve AWS identity. Is the AWS CLI configured?");
339
422
  }
340
- }
341
- const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
342
- const tiers = expandComponent(opts.component);
343
- for (let i = 0; i < tiers.length; i++) {
344
- const tier = tiers[i];
345
- printTierHeader(tier, i, tiers.length);
346
- const cwd = resolveTierDir(terraformDir, opts.stage, tier);
347
- await ensureInit(cwd);
348
- await ensureWorkspace(cwd, opts.stage);
349
- const code = await runTerraform(cwd, [
350
- "apply",
351
- "-auto-approve",
352
- `-var=stage=${opts.stage}`
353
- ]);
354
- if (code !== 0) {
355
- printError(`Deploy failed for ${tier} (exit ${code})`);
356
- process.exit(code);
423
+ if (isProdLike(stage) && !opts.yes) {
424
+ const ok = await confirm(` Stage "${stage}" is production-like. Deploy?`);
425
+ if (!ok) {
426
+ console.log(" Aborted.");
427
+ process.exit(0);
428
+ }
429
+ } else if (!opts.yes) {
430
+ const ok = await confirm(` Deploy to stage "${stage}"?`);
431
+ if (!ok) {
432
+ console.log(" Aborted.");
433
+ process.exit(0);
434
+ }
435
+ }
436
+ const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
437
+ const tiers = expandComponent(opts.component);
438
+ for (let i = 0; i < tiers.length; i++) {
439
+ const tier = tiers[i];
440
+ printTierHeader(tier, i, tiers.length);
441
+ const cwd = resolveTierDir(terraformDir, stage, tier);
442
+ await ensureInit(cwd);
443
+ await ensureWorkspace(cwd, stage);
444
+ const code = await runTerraform(cwd, [
445
+ "apply",
446
+ "-auto-approve",
447
+ `-var=stage=${stage}`
448
+ ]);
449
+ if (code !== 0) {
450
+ printError(`Deploy failed for ${tier} (exit ${code})`);
451
+ process.exit(code);
452
+ }
357
453
  }
454
+ printSuccess("Deploy complete");
455
+ printSummary("deploy", stage, tiers, startTime);
456
+ } catch (err) {
457
+ if (isCancellation(err)) return;
458
+ throw err;
358
459
  }
359
- printSuccess("Deploy complete");
360
- printSummary("deploy", opts.stage, tiers, startTime);
361
460
  });
362
461
  }
363
462
 
364
463
  // src/commands/destroy.ts
365
464
  function registerDestroyCommand(program2) {
366
- program2.command("destroy").description("Run terraform destroy for a stage").option("-p, --profile <name>", "AWS profile").requiredOption("-s, --stage <name>", "Deployment stage").option("-c, --component <tier>", "Component tier (foundation|data|app|all)", "all").option("-y, --yes", "Skip interactive confirmation (for CI)").action(async (opts) => {
465
+ program2.command("destroy").description("Run terraform destroy for a stage. Prompts for stage in a TTY when omitted; always requires a destructive confirmation.").option("-p, --profile <name>", "AWS profile").option("-s, --stage <name>", "Deployment stage").option("-c, --component <tier>", "Component tier (foundation|data|app|all)", "all").option("-y, --yes", "Skip interactive confirmation (for CI)").action(async (opts) => {
367
466
  const startTime = Date.now();
368
- const stageCheck = validateStage(opts.stage);
369
- if (!stageCheck.valid) {
370
- printError(stageCheck.error);
371
- process.exit(1);
372
- }
373
- const compCheck = validateComponent(opts.component);
374
- if (!compCheck.valid) {
375
- printError(compCheck.error);
376
- process.exit(1);
377
- }
378
- const identity = getAwsIdentity();
379
- printHeader("destroy", opts.stage, identity);
380
- if (!identity) {
381
- printWarning("Could not resolve AWS identity. Is the AWS CLI configured?");
382
- }
383
- if (isProdLike(opts.stage)) {
384
- printWarning(`Stage "${opts.stage}" is production-like.`);
385
- if (!opts.yes) {
386
- const ok = await confirm(
387
- ` Type 'y' to confirm destruction of stage "${opts.stage}":`
388
- );
467
+ try {
468
+ const stage = await resolveStage({ flag: opts.stage });
469
+ const compCheck = validateComponent(opts.component);
470
+ if (!compCheck.valid) {
471
+ printError(compCheck.error);
472
+ process.exit(1);
473
+ }
474
+ const identity = getAwsIdentity();
475
+ printHeader("destroy", stage, identity);
476
+ if (!identity) {
477
+ printWarning("Could not resolve AWS identity. Is the AWS CLI configured?");
478
+ }
479
+ if (isProdLike(stage)) {
480
+ printWarning(`Stage "${stage}" is production-like.`);
481
+ if (!opts.yes) {
482
+ const ok = await confirm(` Type 'y' to confirm destruction of stage "${stage}":`);
483
+ if (!ok) {
484
+ console.log(" Aborted.");
485
+ process.exit(0);
486
+ }
487
+ }
488
+ console.log(` Proceeding with destroy of "${stage}" (--yes provided).`);
489
+ } else if (!opts.yes) {
490
+ const ok = await confirm(` Destroy stage "${stage}"?`);
389
491
  if (!ok) {
390
492
  console.log(" Aborted.");
391
493
  process.exit(0);
392
494
  }
393
495
  }
394
- console.log(` Proceeding with destroy of "${opts.stage}" (--yes provided).`);
395
- } else if (!opts.yes) {
396
- const ok = await confirm(` Destroy stage "${opts.stage}"?`);
397
- if (!ok) {
398
- console.log(" Aborted.");
399
- process.exit(0);
400
- }
401
- }
402
- const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
403
- const tiers = expandComponent(opts.component).reverse();
404
- for (let i = 0; i < tiers.length; i++) {
405
- const tier = tiers[i];
406
- printTierHeader(tier, i, tiers.length);
407
- const cwd = resolveTierDir(terraformDir, opts.stage, tier);
408
- await ensureInit(cwd);
409
- await ensureWorkspace(cwd, opts.stage);
410
- const code = await runTerraform(cwd, [
411
- "destroy",
412
- "-auto-approve",
413
- `-var=stage=${opts.stage}`
414
- ]);
415
- if (code !== 0) {
416
- printError(`Destroy failed for ${tier} (exit ${code})`);
417
- process.exit(code);
496
+ const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
497
+ const tiers = expandComponent(opts.component).reverse();
498
+ for (let i = 0; i < tiers.length; i++) {
499
+ const tier = tiers[i];
500
+ printTierHeader(tier, i, tiers.length);
501
+ const cwd = resolveTierDir(terraformDir, stage, tier);
502
+ await ensureInit(cwd);
503
+ await ensureWorkspace(cwd, stage);
504
+ const code = await runTerraform(cwd, [
505
+ "destroy",
506
+ "-auto-approve",
507
+ `-var=stage=${stage}`
508
+ ]);
509
+ if (code !== 0) {
510
+ printError(`Destroy failed for ${tier} (exit ${code})`);
511
+ process.exit(code);
512
+ }
418
513
  }
514
+ printSuccess("Destroy complete");
515
+ printSummary("destroy", stage, tiers, startTime);
516
+ } catch (err) {
517
+ if (isCancellation(err)) return;
518
+ throw err;
419
519
  }
420
- printSuccess("Destroy complete");
421
- printSummary("destroy", opts.stage, tiers, startTime);
422
520
  });
423
521
  }
424
522
 
425
523
  // src/commands/doctor.ts
426
524
  import chalk3 from "chalk";
427
- import { execSync as execSync2 } from "child_process";
525
+ import { execSync as execSync3 } from "child_process";
428
526
  function checkAwsCli() {
429
527
  return {
430
528
  name: "AWS CLI installed",
431
529
  run: () => {
432
530
  try {
433
- const v = execSync2("aws --version", { encoding: "utf-8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
531
+ const v = execSync3("aws --version", { encoding: "utf-8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
434
532
  return { pass: true, detail: v.split(" ")[0] ?? v };
435
533
  } catch {
436
534
  return { pass: false, detail: "aws CLI not found. Install: https://aws.amazon.com/cli/" };
@@ -443,7 +541,7 @@ function checkTerraformCli() {
443
541
  name: "Terraform CLI installed",
444
542
  run: () => {
445
543
  try {
446
- const v = execSync2("terraform version -json", { encoding: "utf-8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] });
544
+ const v = execSync3("terraform version -json", { encoding: "utf-8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] });
447
545
  const parsed = JSON.parse(v);
448
546
  return { pass: true, detail: `v${parsed.terraform_version}` };
449
547
  } catch {
@@ -469,7 +567,7 @@ function checkBedrockAccess() {
469
567
  name: "Bedrock model access",
470
568
  run: () => {
471
569
  try {
472
- execSync2(
570
+ execSync3(
473
571
  "aws bedrock get-foundation-model --model-identifier anthropic.claude-3-haiku-20240307-v1:0 --output json --region us-east-1",
474
572
  { encoding: "utf-8", timeout: 1e4, stdio: ["pipe", "pipe", "pipe"] }
475
573
  );
@@ -484,13 +582,15 @@ function checkBedrockAccess() {
484
582
  };
485
583
  }
486
584
  function registerDoctorCommand(program2) {
487
- program2.command("doctor").description("Check AWS account prerequisites for a Thinkwork deployment").option("-p, --profile <name>", "AWS profile").requiredOption("-s, --stage <name>", "Deployment stage").action((opts) => {
488
- const stageCheck = validateStage(opts.stage);
489
- if (!stageCheck.valid) {
490
- printError(stageCheck.error);
491
- process.exit(1);
585
+ program2.command("doctor").description("Check AWS account prerequisites for a Thinkwork deployment. Prompts for stage in a TTY when omitted.").option("-p, --profile <name>", "AWS profile").option("-s, --stage <name>", "Deployment stage").action(async (opts) => {
586
+ let stage;
587
+ try {
588
+ stage = await resolveStage({ flag: opts.stage });
589
+ } catch (err) {
590
+ if (isCancellation(err)) return;
591
+ throw err;
492
592
  }
493
- printHeader("doctor", opts.stage);
593
+ printHeader("doctor", stage);
494
594
  const checks = [
495
595
  checkAwsCli(),
496
596
  checkTerraformCli(),
@@ -518,31 +618,32 @@ function registerDoctorCommand(program2) {
518
618
 
519
619
  // src/commands/outputs.ts
520
620
  function registerOutputsCommand(program2) {
521
- program2.command("outputs").description("Show terraform outputs for a stage").option("-p, --profile <name>", "AWS profile").requiredOption("-s, --stage <name>", "Deployment stage").option("-c, --component <tier>", "Component tier (foundation|data|app|all)", "all").action(async (opts) => {
522
- const stageCheck = validateStage(opts.stage);
523
- if (!stageCheck.valid) {
524
- printError(stageCheck.error);
525
- process.exit(1);
526
- }
527
- const compCheck = validateComponent(opts.component);
528
- if (!compCheck.valid) {
529
- printError(compCheck.error);
530
- process.exit(1);
531
- }
532
- printHeader("outputs", opts.stage);
533
- const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
534
- const tiers = expandComponent(opts.component);
535
- for (let i = 0; i < tiers.length; i++) {
536
- const tier = tiers[i];
537
- printTierHeader(tier, i, tiers.length);
538
- const cwd = resolveTierDir(terraformDir, opts.stage, tier);
539
- await ensureInit(cwd);
540
- await ensureWorkspace(cwd, opts.stage);
541
- const code = await runTerraform(cwd, ["output"]);
542
- if (code !== 0) {
543
- printError(`Outputs failed for ${tier} (exit ${code})`);
544
- process.exit(code);
621
+ program2.command("outputs").description("Show terraform outputs for a stage. Prompts for stage in a TTY when omitted.").option("-p, --profile <name>", "AWS profile").option("-s, --stage <name>", "Deployment stage").option("-c, --component <tier>", "Component tier (foundation|data|app|all)", "all").action(async (opts) => {
622
+ try {
623
+ const stage = await resolveStage({ flag: opts.stage });
624
+ const compCheck = validateComponent(opts.component);
625
+ if (!compCheck.valid) {
626
+ printError(compCheck.error);
627
+ process.exit(1);
545
628
  }
629
+ printHeader("outputs", stage);
630
+ const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
631
+ const tiers = expandComponent(opts.component);
632
+ for (let i = 0; i < tiers.length; i++) {
633
+ const tier = tiers[i];
634
+ printTierHeader(tier, i, tiers.length);
635
+ const cwd = resolveTierDir(terraformDir, stage, tier);
636
+ await ensureInit(cwd);
637
+ await ensureWorkspace(cwd, stage);
638
+ const code = await runTerraform(cwd, ["output"]);
639
+ if (code !== 0) {
640
+ printError(`Outputs failed for ${tier} (exit ${code})`);
641
+ process.exit(code);
642
+ }
643
+ }
644
+ } catch (err) {
645
+ if (isCancellation(err)) return;
646
+ throw err;
546
647
  }
547
648
  });
548
649
  }
@@ -701,13 +802,15 @@ function registerConfigCommand(program2) {
701
802
  console.log(` Show details: ${chalk4.cyan("thinkwork config list -s <stage>")}`);
702
803
  console.log("");
703
804
  });
704
- config.command("get <key>").description("Get a configuration value (e.g. enable-hindsight)").requiredOption("-s, --stage <name>", "Deployment stage").action((key, opts) => {
705
- const stageCheck = validateStage(opts.stage);
706
- if (!stageCheck.valid) {
707
- printError(stageCheck.error);
708
- process.exit(1);
805
+ config.command("get <key>").description("Get a configuration value (e.g. enable-hindsight). Prompts for stage in a TTY when omitted.").option("-s, --stage <name>", "Deployment stage").action(async (key, opts) => {
806
+ let stage;
807
+ try {
808
+ stage = await resolveStage({ flag: opts.stage });
809
+ } catch (err) {
810
+ if (isCancellation(err)) return;
811
+ throw err;
709
812
  }
710
- const tfvarsPath = resolveTfvarsPath(opts.stage);
813
+ const tfvarsPath = resolveTfvarsPath(stage);
711
814
  const tfKey = key.replace(/-/g, "_");
712
815
  const value = readTfVar(tfvarsPath, tfKey);
713
816
  if (value === null) {
@@ -716,11 +819,13 @@ function registerConfigCommand(program2) {
716
819
  console.log(` ${key} = ${value}`);
717
820
  }
718
821
  });
719
- config.command("set <key> <value>").description("Set a configuration value and optionally deploy").requiredOption("-s, --stage <name>", "Deployment stage").option("--apply", "Run terraform apply after changing the value").action(async (key, value, opts) => {
720
- const stageCheck = validateStage(opts.stage);
721
- if (!stageCheck.valid) {
722
- printError(stageCheck.error);
723
- process.exit(1);
822
+ config.command("set <key> <value>").description("Set a configuration value and optionally deploy. Prompts for stage in a TTY when omitted.").option("-s, --stage <name>", "Deployment stage").option("--apply", "Run terraform apply after changing the value").action(async (key, value, opts) => {
823
+ let stage;
824
+ try {
825
+ stage = await resolveStage({ flag: opts.stage });
826
+ } catch (err) {
827
+ if (isCancellation(err)) return;
828
+ throw err;
724
829
  }
725
830
  let tfKey = key.replace(/-/g, "_");
726
831
  let tfValue = value;
@@ -738,13 +843,13 @@ function registerConfigCommand(program2) {
738
843
  process.exit(1);
739
844
  }
740
845
  const identity = getAwsIdentity();
741
- printHeader("config set", opts.stage, identity);
742
- const tfvarsPath = resolveTfvarsPath(opts.stage);
846
+ printHeader("config set", stage, identity);
847
+ const tfvarsPath = resolveTfvarsPath(stage);
743
848
  const oldValue = readTfVar(tfvarsPath, tfKey);
744
849
  setTfVar(tfvarsPath, tfKey, tfValue);
745
850
  console.log(` ${tfKey}: ${oldValue ?? "(unset)"} \u2192 ${tfValue}`);
746
851
  if (opts.apply) {
747
- const tfDir = resolveTerraformDir(opts.stage);
852
+ const tfDir = resolveTerraformDir(stage);
748
853
  if (!tfDir) {
749
854
  printError("Cannot find terraform directory. Run `thinkwork init` first.");
750
855
  process.exit(1);
@@ -752,11 +857,11 @@ function registerConfigCommand(program2) {
752
857
  console.log("");
753
858
  console.log(" Applying configuration change...");
754
859
  await ensureInit(tfDir);
755
- await ensureWorkspace(tfDir, opts.stage);
860
+ await ensureWorkspace(tfDir, stage);
756
861
  const code = await runTerraform(tfDir, [
757
862
  "apply",
758
863
  "-auto-approve",
759
- `-var=stage=${opts.stage}`
864
+ `-var=stage=${stage}`
760
865
  ]);
761
866
  if (code !== 0) {
762
867
  printError(`Deploy failed (exit ${code})`);
@@ -796,18 +901,20 @@ function runScript(scriptPath, args) {
796
901
  });
797
902
  }
798
903
  function registerBootstrapCommand(program2) {
799
- program2.command("bootstrap").description("Seed workspace defaults, skill catalog, and per-tenant files for a stage").requiredOption("-s, --stage <name>", "Deployment stage").action(async (opts) => {
800
- const stageCheck = validateStage(opts.stage);
801
- if (!stageCheck.valid) {
802
- printError(stageCheck.error);
803
- process.exit(1);
904
+ program2.command("bootstrap").description("Seed workspace defaults, skill catalog, and per-tenant files for a stage. Prompts for stage in a TTY when omitted.").option("-s, --stage <name>", "Deployment stage").action(async (opts) => {
905
+ let stage;
906
+ try {
907
+ stage = await resolveStage({ flag: opts.stage });
908
+ } catch (err) {
909
+ if (isCancellation(err)) return;
910
+ throw err;
804
911
  }
805
912
  const identity = getAwsIdentity();
806
- printHeader("bootstrap", opts.stage, identity);
913
+ printHeader("bootstrap", stage, identity);
807
914
  const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
808
- const cwd = resolveTierDir(terraformDir, opts.stage, "app");
915
+ const cwd = resolveTierDir(terraformDir, stage, "app");
809
916
  await ensureInit(cwd);
810
- await ensureWorkspace(cwd, opts.stage);
917
+ await ensureWorkspace(cwd, stage);
811
918
  let bucket;
812
919
  let dbEndpoint;
813
920
  let dbPassword;
@@ -829,7 +936,7 @@ function registerBootstrapCommand(program2) {
829
936
  const databaseUrl = `postgresql://thinkwork_admin:${encodeURIComponent(dbPassword)}@${dbEndpoint}:5432/thinkwork?sslmode=no-verify`;
830
937
  const repoRoot = resolve(terraformDir);
831
938
  const scriptPath = resolve(repoRoot, "scripts/bootstrap-workspace.sh");
832
- const code = await runScript(scriptPath, [opts.stage, bucket, databaseUrl]);
939
+ const code = await runScript(scriptPath, [stage, bucket, databaseUrl]);
833
940
  if (code !== 0) {
834
941
  printError(`Bootstrap failed (exit ${code})`);
835
942
  process.exit(code);
@@ -841,7 +948,7 @@ function registerBootstrapCommand(program2) {
841
948
  // src/commands/login.ts
842
949
  import { execSync as execSync6 } from "child_process";
843
950
  import { createInterface as createInterface2 } from "readline";
844
- import { select, Separator } from "@inquirer/prompts";
951
+ import { select as select2, Separator } from "@inquirer/prompts";
845
952
  import chalk7 from "chalk";
846
953
 
847
954
  // src/aws-profiles.ts
@@ -921,14 +1028,14 @@ function listAwsProfiles() {
921
1028
  }
922
1029
 
923
1030
  // src/prerequisites.ts
924
- import { execSync as execSync3 } from "child_process";
1031
+ import { execSync as execSync4 } from "child_process";
925
1032
  import { mkdirSync as mkdirSync3, createWriteStream, chmodSync } from "fs";
926
1033
  import { join as join4 } from "path";
927
1034
  import { homedir as homedir4, platform, arch } from "os";
928
1035
  import chalk5 from "chalk";
929
1036
  function run(cmd, opts) {
930
1037
  try {
931
- return execSync3(cmd, {
1038
+ return execSync4(cmd, {
932
1039
  encoding: "utf-8",
933
1040
  timeout: 3e4,
934
1041
  stdio: opts?.silent ? ["pipe", "pipe", "pipe"] : void 0
@@ -1042,10 +1149,10 @@ async function ensurePrerequisites() {
1042
1149
  }
1043
1150
 
1044
1151
  // src/cognito-discovery.ts
1045
- import { execSync as execSync4 } from "child_process";
1046
- function runAws(cmd) {
1152
+ import { execSync as execSync5 } from "child_process";
1153
+ function runAws2(cmd) {
1047
1154
  try {
1048
- return execSync4(`aws ${cmd}`, {
1155
+ return execSync5(`aws ${cmd}`, {
1049
1156
  encoding: "utf-8",
1050
1157
  timeout: 15e3,
1051
1158
  stdio: ["pipe", "pipe", "pipe"]
@@ -1065,7 +1172,7 @@ function tryTerraformOutput(stage) {
1065
1172
  }
1066
1173
  const read = (key) => {
1067
1174
  try {
1068
- return execSync4(`terraform output -raw ${key}`, {
1175
+ return execSync5(`terraform output -raw ${key}`, {
1069
1176
  cwd,
1070
1177
  encoding: "utf-8",
1071
1178
  timeout: 15e3,
@@ -1081,7 +1188,7 @@ function tryTerraformOutput(stage) {
1081
1188
  return { userPoolId, clientId, domain };
1082
1189
  }
1083
1190
  function tryAwsDiscovery(stage, region) {
1084
- const listRaw = runAws(
1191
+ const listRaw = runAws2(
1085
1192
  `cognito-idp list-user-pools --max-results 60 --region ${region} --output json`
1086
1193
  );
1087
1194
  if (!listRaw) return {};
@@ -1093,7 +1200,7 @@ function tryAwsDiscovery(stage, region) {
1093
1200
  )
1094
1201
  );
1095
1202
  if (!pool) return {};
1096
- const clientsRaw = runAws(
1203
+ const clientsRaw = runAws2(
1097
1204
  `cognito-idp list-user-pool-clients --user-pool-id ${pool.Id} --region ${region} --output json`
1098
1205
  );
1099
1206
  let clientId;
@@ -1364,70 +1471,11 @@ function escapeHtml(s) {
1364
1471
  });
1365
1472
  }
1366
1473
 
1367
- // src/aws-discovery.ts
1368
- import { execSync as execSync5 } from "child_process";
1369
- function runAws2(cmd) {
1370
- try {
1371
- return execSync5(`aws ${cmd}`, {
1372
- encoding: "utf-8",
1373
- timeout: 15e3,
1374
- stdio: ["pipe", "pipe", "pipe"]
1375
- }).trim();
1376
- } catch {
1377
- return null;
1378
- }
1379
- }
1380
- function listDeployedStages(region) {
1381
- const raw = runAws2(
1382
- `lambda list-functions --region ${region} --query "Functions[?starts_with(FunctionName, 'thinkwork-')].FunctionName" --output json`
1383
- );
1384
- if (!raw) return [];
1385
- try {
1386
- const functions = JSON.parse(raw);
1387
- const stages = /* @__PURE__ */ new Set();
1388
- for (const fn of functions) {
1389
- const m = fn.match(/^thinkwork-(.+?)-api-graphql-http$/);
1390
- if (m) stages.add(m[1]);
1391
- }
1392
- return [...stages].sort();
1393
- } catch {
1394
- return [];
1395
- }
1396
- }
1397
- function getApiEndpoint(stage, region) {
1398
- const raw = runAws2(
1399
- `apigatewayv2 get-apis --region ${region} --query "Items[?Name=='thinkwork-${stage}-api'].ApiEndpoint|[0]" --output text`
1400
- );
1401
- return raw && raw !== "None" ? raw : null;
1402
- }
1403
- function getApiAuthSecretFromLambda(stage, region) {
1404
- const raw = runAws2(
1405
- `lambda get-function-configuration --function-name thinkwork-${stage}-api-tenants --region ${region} --query "Environment.Variables.API_AUTH_SECRET" --output text`
1406
- );
1407
- return raw && raw !== "None" ? raw : null;
1408
- }
1409
-
1410
- // src/lib/interactive.ts
1411
- function isCancellation(err) {
1412
- return err instanceof Error && err.name === "ExitPromptError";
1413
- }
1414
- function isInteractive() {
1415
- return Boolean(process.stdin.isTTY);
1416
- }
1417
- function requireTty(label) {
1418
- if (!isInteractive()) {
1419
- printError(
1420
- `${label} is required. Pass it as a flag or re-run in an interactive terminal.`
1421
- );
1422
- process.exit(1);
1423
- }
1424
- }
1425
-
1426
1474
  // src/commands/login.ts
1427
- function ask(prompt2) {
1475
+ function ask(prompt) {
1428
1476
  const rl = createInterface2({ input: process.stdin, output: process.stdout });
1429
1477
  return new Promise((resolve3) => {
1430
- rl.question(prompt2, (answer) => {
1478
+ rl.question(prompt, (answer) => {
1431
1479
  rl.close();
1432
1480
  resolve3(answer.trim());
1433
1481
  });
@@ -1480,7 +1528,7 @@ async function pickProfile(profiles) {
1480
1528
  description: "Run `aws sso login` against the configured SSO profile."
1481
1529
  });
1482
1530
  try {
1483
- const picked = await select({
1531
+ const picked = await select2({
1484
1532
  message: "Pick an AWS profile for Thinkwork:",
1485
1533
  choices,
1486
1534
  loop: false,
@@ -1842,7 +1890,7 @@ Registered callback URL:
1842
1890
  }
1843
1891
 
1844
1892
  // src/commands/logout.ts
1845
- import { select as select2 } from "@inquirer/prompts";
1893
+ import { select as select3 } from "@inquirer/prompts";
1846
1894
  function registerLogoutCommand(program2) {
1847
1895
  program2.command("logout").description(
1848
1896
  "Forget a stored session. Touches only ~/.thinkwork/config.json; your AWS profile and Cognito pool are untouched."
@@ -1880,7 +1928,7 @@ Examples:
1880
1928
  console.log(` Only one session stored: ${stage}`);
1881
1929
  } else {
1882
1930
  requireTty("Stage");
1883
- stage = await select2({
1931
+ stage = await select3({
1884
1932
  message: "Forget which stage's session?",
1885
1933
  choices: keys.map((s) => ({ name: s, value: s })),
1886
1934
  loop: false
@@ -1915,19 +1963,19 @@ import { fileURLToPath } from "url";
1915
1963
  import { createInterface as createInterface3 } from "readline";
1916
1964
  import chalk8 from "chalk";
1917
1965
  var __dirname = dirname2(fileURLToPath(import.meta.url));
1918
- function ask2(prompt2, defaultVal = "") {
1966
+ function ask2(prompt, defaultVal = "") {
1919
1967
  const rl = createInterface3({ input: process.stdin, output: process.stdout });
1920
1968
  const suffix = defaultVal ? chalk8.dim(` [${defaultVal}]`) : "";
1921
1969
  return new Promise((resolve3) => {
1922
- rl.question(` ${prompt2}${suffix}: `, (answer) => {
1970
+ rl.question(` ${prompt}${suffix}: `, (answer) => {
1923
1971
  rl.close();
1924
1972
  resolve3(answer.trim() || defaultVal);
1925
1973
  });
1926
1974
  });
1927
1975
  }
1928
- function choose(prompt2, options, defaultVal) {
1976
+ function choose(prompt, options, defaultVal) {
1929
1977
  const optStr = options.map((o) => o === defaultVal ? chalk8.bold(o) : chalk8.dim(o)).join(" / ");
1930
- return ask2(`${prompt2} (${optStr})`, defaultVal);
1978
+ return ask2(`${prompt} (${optStr})`, defaultVal);
1931
1979
  }
1932
1980
  function generateSecret(length = 32) {
1933
1981
  const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
@@ -1994,14 +2042,34 @@ function buildTfvars(config) {
1994
2042
  return lines.join("\n");
1995
2043
  }
1996
2044
  function registerInitCommand(program2) {
1997
- program2.command("init").description("Initialize a new Thinkwork environment").requiredOption("-s, --stage <name>", "Stage name (e.g. dev, staging, prod)").option("-d, --dir <path>", "Target directory", ".").option("--defaults", "Skip interactive prompts, use all defaults").action(async (opts) => {
1998
- const stageCheck = validateStage(opts.stage);
2045
+ program2.command("init").description("Initialize a new Thinkwork environment. Prompts for a stage name in a TTY when omitted (init creates a stage \u2014 the picker isn't applicable here).").option("-s, --stage <name>", "Stage name (e.g. dev, staging, prod)").option("-d, --dir <path>", "Target directory", ".").option("--defaults", "Skip interactive prompts, use all defaults").action(async (opts) => {
2046
+ let stage = opts.stage;
2047
+ if (!stage) {
2048
+ if (!process.stdin.isTTY) {
2049
+ printError("Stage name is required. Pass -s <name> or re-run in an interactive terminal.");
2050
+ process.exit(1);
2051
+ }
2052
+ const { input: input3 } = await import("@inquirer/prompts");
2053
+ try {
2054
+ stage = await input3({
2055
+ message: "Stage name (e.g. dev, staging, prod):",
2056
+ validate: (v) => validateStage(v).error ?? true
2057
+ });
2058
+ } catch (err) {
2059
+ if (err instanceof Error && err.name === "ExitPromptError") {
2060
+ console.log(" Cancelled.");
2061
+ return;
2062
+ }
2063
+ throw err;
2064
+ }
2065
+ }
2066
+ const stageCheck = validateStage(stage);
1999
2067
  if (!stageCheck.valid) {
2000
2068
  printError(stageCheck.error);
2001
2069
  process.exit(1);
2002
2070
  }
2003
2071
  const identity = getAwsIdentity();
2004
- printHeader("init", opts.stage, identity);
2072
+ printHeader("init", stage, identity);
2005
2073
  const prereqsOk = await ensurePrerequisites();
2006
2074
  if (!prereqsOk) {
2007
2075
  process.exit(1);
@@ -2023,10 +2091,10 @@ function registerInitCommand(program2) {
2023
2091
  console.log("");
2024
2092
  }
2025
2093
  const config = {
2026
- stage: opts.stage,
2094
+ stage,
2027
2095
  account_id: identity.account,
2028
2096
  db_password: generateSecret(24),
2029
- api_auth_secret: `tw-${opts.stage}-${generateSecret(16)}`
2097
+ api_auth_secret: `tw-${stage}-${generateSecret(16)}`
2030
2098
  };
2031
2099
  if (opts.defaults) {
2032
2100
  config.region = identity.region !== "unknown" ? identity.region : "us-east-1";
@@ -2287,7 +2355,7 @@ output "agentcore_memory_id" {
2287
2355
  try {
2288
2356
  execSync7("terraform init", { cwd: tfDir, stdio: "inherit" });
2289
2357
  } catch {
2290
- printWarning("Terraform init failed. Run `thinkwork doctor -s " + opts.stage + "` to check prerequisites.");
2358
+ printWarning("Terraform init failed. Run `thinkwork doctor -s " + stage + "` to check prerequisites.");
2291
2359
  return;
2292
2360
  }
2293
2361
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -2301,13 +2369,13 @@ output "agentcore_memory_id" {
2301
2369
  createdAt: now,
2302
2370
  updatedAt: now
2303
2371
  });
2304
- printSuccess(`Environment "${opts.stage}" initialized`);
2372
+ printSuccess(`Environment "${stage}" initialized`);
2305
2373
  console.log("");
2306
2374
  console.log(" Next steps:");
2307
- console.log(` ${chalk8.cyan("1.")} thinkwork plan -s ${opts.stage} ${chalk8.dim("# Review infrastructure plan")}`);
2308
- console.log(` ${chalk8.cyan("2.")} thinkwork deploy -s ${opts.stage} ${chalk8.dim("# Deploy to AWS (~5 min)")}`);
2309
- console.log(` ${chalk8.cyan("3.")} thinkwork bootstrap -s ${opts.stage} ${chalk8.dim("# Seed workspace files + skills")}`);
2310
- console.log(` ${chalk8.cyan("4.")} thinkwork outputs -s ${opts.stage} ${chalk8.dim("# Show API URL, Cognito IDs, etc.")}`);
2375
+ console.log(` ${chalk8.cyan("1.")} thinkwork plan -s ${stage} ${chalk8.dim("# Review infrastructure plan")}`);
2376
+ console.log(` ${chalk8.cyan("2.")} thinkwork deploy -s ${stage} ${chalk8.dim("# Deploy to AWS (~5 min)")}`);
2377
+ console.log(` ${chalk8.cyan("3.")} thinkwork bootstrap -s ${stage} ${chalk8.dim("# Seed workspace files + skills")}`);
2378
+ console.log(` ${chalk8.cyan("4.")} thinkwork outputs -s ${stage} ${chalk8.dim("# Show API URL, Cognito IDs, etc.")}`);
2311
2379
  console.log("");
2312
2380
  });
2313
2381
  }
@@ -2604,20 +2672,112 @@ function resolveApiConfig(stage, regionOverride) {
2604
2672
  return { apiUrl, authSecret };
2605
2673
  }
2606
2674
 
2675
+ // src/lib/resolve-tenant.ts
2676
+ import { select as select4 } from "@inquirer/prompts";
2677
+ async function resolveTenant(opts) {
2678
+ const override = opts.flag ?? process.env.THINKWORK_TENANT;
2679
+ if (override) {
2680
+ const cached = loadStageSession(opts.stage);
2681
+ if (cached && cached.tenantSlug === override) {
2682
+ return { slug: override, id: cached.tenantId };
2683
+ }
2684
+ return { slug: override };
2685
+ }
2686
+ const session = loadStageSession(opts.stage);
2687
+ if (session?.tenantSlug) {
2688
+ return { slug: session.tenantSlug, id: session.tenantId };
2689
+ }
2690
+ if (!opts.listTenants) {
2691
+ printError(
2692
+ `No tenant resolved for stage "${opts.stage}". Pass --tenant <slug>, set THINKWORK_TENANT, or re-run \`thinkwork login --stage ${opts.stage}\`.`
2693
+ );
2694
+ process.exit(1);
2695
+ }
2696
+ const tenants = await opts.listTenants();
2697
+ if (tenants.length === 0) {
2698
+ printError(
2699
+ "No tenants available. You may need to be invited to a workspace first."
2700
+ );
2701
+ process.exit(1);
2702
+ }
2703
+ if (tenants.length === 1) {
2704
+ const only = tenants[0];
2705
+ console.log(` Using the only tenant: ${only.name} (${only.slug})`);
2706
+ cacheTenant(opts.stage, only);
2707
+ return { slug: only.slug, id: only.id };
2708
+ }
2709
+ requireTty("Tenant");
2710
+ const slug = await select4({
2711
+ message: "Which tenant?",
2712
+ choices: tenants.map((t) => ({
2713
+ name: `${t.name} (slug: ${t.slug})`,
2714
+ value: t.slug
2715
+ })),
2716
+ loop: false
2717
+ });
2718
+ const picked = tenants.find((t) => t.slug === slug);
2719
+ cacheTenant(opts.stage, picked);
2720
+ return { slug: picked.slug, id: picked.id };
2721
+ }
2722
+ function cacheTenant(stage, tenant) {
2723
+ const session = loadStageSession(stage);
2724
+ if (!session) return;
2725
+ saveStageSession(stage, { ...session, tenantId: tenant.id, tenantSlug: tenant.slug });
2726
+ }
2727
+
2728
+ // src/lib/resolve-tenant-rest.ts
2729
+ async function resolveTenantRest(opts) {
2730
+ return resolveTenant({
2731
+ flag: opts.flag,
2732
+ stage: opts.stage,
2733
+ listTenants: async () => {
2734
+ const list = await apiFetch(
2735
+ opts.apiUrl,
2736
+ opts.authSecret,
2737
+ "/api/tenants"
2738
+ );
2739
+ return list;
2740
+ }
2741
+ });
2742
+ }
2743
+
2607
2744
  // src/commands/mcp.ts
2745
+ async function resolveMcpContext(opts) {
2746
+ const stage = await resolveStage({ flag: opts.stage });
2747
+ const api = resolveApiConfig(stage);
2748
+ if (!api) process.exit(1);
2749
+ const tenant = await resolveTenantRest({
2750
+ flag: opts.tenant,
2751
+ stage,
2752
+ apiUrl: api.apiUrl,
2753
+ authSecret: api.authSecret
2754
+ });
2755
+ return { stage, api, tenant };
2756
+ }
2608
2757
  function registerMcpCommand(program2) {
2609
2758
  const mcp = program2.command("mcp").description("Manage MCP servers for your tenant");
2610
- mcp.command("list").description("List registered MCP servers").requiredOption("-s, --stage <name>", "Deployment stage").requiredOption("--tenant <slug>", "Tenant slug").action(async (opts) => {
2611
- const check = validateStage(opts.stage);
2612
- if (!check.valid) {
2613
- printError(check.error);
2614
- process.exit(1);
2615
- }
2616
- const api = resolveApiConfig(opts.stage);
2617
- if (!api) process.exit(1);
2618
- printHeader("mcp list", opts.stage);
2759
+ mcp.command("list").alias("ls").description("List registered MCP servers. Prompts for stage/tenant in a TTY when omitted.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
2760
+ "after",
2761
+ `
2762
+ Examples:
2763
+ # Interactive \u2014 picks stage + tenant from the deployed ones
2764
+ $ thinkwork mcp list
2765
+
2766
+ # Scriptable
2767
+ $ thinkwork mcp list -s dev -t acme
2768
+ $ thinkwork mcp list -s prod -t acme --json | jq '.[].slug'
2769
+ `
2770
+ ).action(async (opts) => {
2619
2771
  try {
2620
- const { servers } = await apiFetch(api.apiUrl, api.authSecret, "/api/skills/mcp-servers", {}, { "x-tenant-slug": opts.tenant });
2772
+ const { stage, api, tenant } = await resolveMcpContext(opts);
2773
+ printHeader("mcp list", stage);
2774
+ const { servers } = await apiFetch(
2775
+ api.apiUrl,
2776
+ api.authSecret,
2777
+ "/api/skills/mcp-servers",
2778
+ {},
2779
+ { "x-tenant-slug": tenant.slug }
2780
+ );
2621
2781
  if (!servers || servers.length === 0) {
2622
2782
  console.log(chalk10.dim(" No MCP servers registered."));
2623
2783
  return;
@@ -2630,74 +2790,104 @@ function registerMcpCommand(program2) {
2630
2790
  console.log(` URL: ${s.url}`);
2631
2791
  console.log(` Transport: ${s.transport}`);
2632
2792
  console.log(` Auth: ${authLabel}`);
2633
- if (s.tools?.length) {
2634
- console.log(` Tools: ${s.tools.length} cached`);
2635
- }
2793
+ if (s.tools?.length) console.log(` Tools: ${s.tools.length} cached`);
2636
2794
  console.log("");
2637
2795
  }
2638
2796
  } catch (err) {
2639
- printError(err.message);
2640
- process.exit(1);
2641
- }
2642
- });
2643
- mcp.command("add <name>").description("Register an MCP server").requiredOption("-s, --stage <name>", "Deployment stage").requiredOption("--tenant <slug>", "Tenant slug").requiredOption("--url <url>", "MCP server URL").option("--transport <type>", "Transport type (streamable-http|sse)", "streamable-http").option("--auth-type <type>", "Auth type (none|tenant_api_key|per_user_oauth)", "none").option("--api-key <token>", "API key (for tenant_api_key auth)").option("--oauth-provider <name>", "OAuth provider name (for per_user_oauth auth)").action(async (name, opts) => {
2644
- const check = validateStage(opts.stage);
2645
- if (!check.valid) {
2646
- printError(check.error);
2647
- process.exit(1);
2648
- }
2649
- const api = resolveApiConfig(opts.stage);
2650
- if (!api) process.exit(1);
2651
- const body = {
2652
- name,
2653
- url: opts.url,
2654
- transport: opts.transport
2655
- };
2656
- if (opts.authType !== "none") body.authType = opts.authType;
2657
- if (opts.apiKey) body.apiKey = opts.apiKey;
2658
- if (opts.oauthProvider) body.oauthProvider = opts.oauthProvider;
2659
- try {
2660
- const result = await apiFetch(api.apiUrl, api.authSecret, "/api/skills/mcp-servers", {
2661
- method: "POST",
2662
- body: JSON.stringify(body)
2663
- }, { "x-tenant-slug": opts.tenant });
2664
- printSuccess(`MCP server "${name}" ${result.created ? "registered" : "updated"} (slug: ${result.slug})`);
2665
- } catch (err) {
2666
- printError(err.message);
2797
+ if (isCancellation(err)) return;
2798
+ printError(err instanceof Error ? err.message : String(err));
2667
2799
  process.exit(1);
2668
2800
  }
2669
2801
  });
2670
- mcp.command("remove <id>").description("Remove an MCP server").requiredOption("-s, --stage <name>", "Deployment stage").requiredOption("--tenant <slug>", "Tenant slug").action(async (id, opts) => {
2671
- const check = validateStage(opts.stage);
2672
- if (!check.valid) {
2673
- printError(check.error);
2674
- process.exit(1);
2802
+ mcp.command("add [name]").description("Register an MCP server. Prompts for missing fields in a TTY.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--url <url>", "MCP server URL").option("--transport <type>", "Transport type (streamable-http|sse)", "streamable-http").option("--auth-type <type>", "Auth type (none|tenant_api_key|per_user_oauth)", "none").option("--api-key <token>", "API key (for tenant_api_key auth)").option("--oauth-provider <name>", "OAuth provider name (for per_user_oauth auth)").addHelpText(
2803
+ "after",
2804
+ `
2805
+ Examples:
2806
+ # Fully interactive \u2014 prompts for name, URL, and auth
2807
+ $ thinkwork mcp add
2808
+
2809
+ # Scripted \u2014 API-key auth
2810
+ $ thinkwork mcp add my-tools --url https://mcp.example.com/crm \\
2811
+ --auth-type tenant_api_key --api-key sk-abc -s dev -t acme
2812
+
2813
+ # OAuth connector (users connect from the mobile app)
2814
+ $ thinkwork mcp add lastmile --url https://mcp-dev.lastmile-tei.com/crm \\
2815
+ --auth-type per_user_oauth --oauth-provider lastmile -s dev -t acme
2816
+ `
2817
+ ).action(
2818
+ async (nameArg, opts) => {
2819
+ try {
2820
+ const { input: input3 } = await import("@inquirer/prompts");
2821
+ const { stage, api, tenant } = await resolveMcpContext(opts);
2822
+ let name = nameArg;
2823
+ if (!name) {
2824
+ if (!process.stdin.isTTY) {
2825
+ printError("Name is required. Pass it as a positional arg.");
2826
+ process.exit(1);
2827
+ }
2828
+ name = await input3({ message: "Server name:" });
2829
+ }
2830
+ let url = opts.url;
2831
+ if (!url) {
2832
+ if (!process.stdin.isTTY) {
2833
+ printError("--url is required. Pass it as a flag.");
2834
+ process.exit(1);
2835
+ }
2836
+ url = await input3({
2837
+ message: "MCP server URL:",
2838
+ validate: (v) => v.startsWith("http://") || v.startsWith("https://") ? true : "URL must start with http:// or https://"
2839
+ });
2840
+ }
2841
+ const body = { name, url, transport: opts.transport };
2842
+ if (opts.authType !== "none") body.authType = opts.authType;
2843
+ if (opts.apiKey) body.apiKey = opts.apiKey;
2844
+ if (opts.oauthProvider) body.oauthProvider = opts.oauthProvider;
2845
+ printHeader("mcp add", stage);
2846
+ const result = await apiFetch(
2847
+ api.apiUrl,
2848
+ api.authSecret,
2849
+ "/api/skills/mcp-servers",
2850
+ { method: "POST", body: JSON.stringify(body) },
2851
+ { "x-tenant-slug": tenant.slug }
2852
+ );
2853
+ printSuccess(
2854
+ `MCP server "${name}" ${result.created ? "registered" : "updated"} (slug: ${result.slug})`
2855
+ );
2856
+ } catch (err) {
2857
+ if (isCancellation(err)) return;
2858
+ printError(err instanceof Error ? err.message : String(err));
2859
+ process.exit(1);
2860
+ }
2675
2861
  }
2676
- const api = resolveApiConfig(opts.stage);
2677
- if (!api) process.exit(1);
2862
+ );
2863
+ mcp.command("remove <id>").alias("rm").description("Remove an MCP server. Prompts for stage/tenant in a TTY when omitted.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(async (id, opts) => {
2678
2864
  try {
2679
- await apiFetch(api.apiUrl, api.authSecret, `/api/skills/mcp-servers/${id}`, {
2680
- method: "DELETE"
2681
- }, { "x-tenant-slug": opts.tenant });
2682
- printSuccess(`MCP server removed.`);
2865
+ const { api, tenant } = await resolveMcpContext(opts);
2866
+ await apiFetch(
2867
+ api.apiUrl,
2868
+ api.authSecret,
2869
+ `/api/skills/mcp-servers/${id}`,
2870
+ { method: "DELETE" },
2871
+ { "x-tenant-slug": tenant.slug }
2872
+ );
2873
+ printSuccess("MCP server removed.");
2683
2874
  } catch (err) {
2684
- printError(err.message);
2875
+ if (isCancellation(err)) return;
2876
+ printError(err instanceof Error ? err.message : String(err));
2685
2877
  process.exit(1);
2686
2878
  }
2687
2879
  });
2688
- mcp.command("test <id>").description("Test connection and discover tools").requiredOption("-s, --stage <name>", "Deployment stage").requiredOption("--tenant <slug>", "Tenant slug").action(async (id, opts) => {
2689
- const check = validateStage(opts.stage);
2690
- if (!check.valid) {
2691
- printError(check.error);
2692
- process.exit(1);
2693
- }
2694
- const api = resolveApiConfig(opts.stage);
2695
- if (!api) process.exit(1);
2696
- printHeader("mcp test", opts.stage);
2880
+ mcp.command("test <id>").description("Test connection and discover tools. Prompts for stage/tenant in a TTY when omitted.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(async (id, opts) => {
2697
2881
  try {
2698
- const result = await apiFetch(api.apiUrl, api.authSecret, `/api/skills/mcp-servers/${id}/test`, {
2699
- method: "POST"
2700
- }, { "x-tenant-slug": opts.tenant });
2882
+ const { stage, api, tenant } = await resolveMcpContext(opts);
2883
+ printHeader("mcp test", stage);
2884
+ const result = await apiFetch(
2885
+ api.apiUrl,
2886
+ api.authSecret,
2887
+ `/api/skills/mcp-servers/${id}/test`,
2888
+ { method: "POST" },
2889
+ { "x-tenant-slug": tenant.slug }
2890
+ );
2701
2891
  if (result.ok) {
2702
2892
  printSuccess("Connection successful.");
2703
2893
  if (result.tools?.length) {
@@ -2705,7 +2895,9 @@ function registerMcpCommand(program2) {
2705
2895
  Discovered tools (${result.tools.length}):
2706
2896
  `));
2707
2897
  for (const t of result.tools) {
2708
- console.log(` ${chalk10.cyan(t.name)}${t.description ? chalk10.dim(` - ${t.description}`) : ""}`);
2898
+ console.log(
2899
+ ` ${chalk10.cyan(t.name)}${t.description ? chalk10.dim(` - ${t.description}`) : ""}`
2900
+ );
2709
2901
  }
2710
2902
  console.log("");
2711
2903
  } else {
@@ -2716,86 +2908,112 @@ function registerMcpCommand(program2) {
2716
2908
  process.exit(1);
2717
2909
  }
2718
2910
  } catch (err) {
2719
- printError(err.message);
2720
- process.exit(1);
2721
- }
2722
- });
2723
- mcp.command("assign <mcpServerId>").description("Assign an MCP server to an agent").requiredOption("-s, --stage <name>", "Deployment stage").requiredOption("--agent <id>", "Agent ID").action(async (mcpServerId, opts) => {
2724
- const check = validateStage(opts.stage);
2725
- if (!check.valid) {
2726
- printError(check.error);
2727
- process.exit(1);
2728
- }
2729
- const api = resolveApiConfig(opts.stage);
2730
- if (!api) process.exit(1);
2731
- try {
2732
- const result = await apiFetch(api.apiUrl, api.authSecret, `/api/skills/agents/${opts.agent}/mcp-servers`, {
2733
- method: "POST",
2734
- body: JSON.stringify({ mcpServerId })
2735
- });
2736
- printSuccess(`MCP server assigned to agent. (${result.created ? "new" : "updated"})`);
2737
- } catch (err) {
2738
- printError(err.message);
2911
+ if (isCancellation(err)) return;
2912
+ printError(err instanceof Error ? err.message : String(err));
2739
2913
  process.exit(1);
2740
2914
  }
2741
2915
  });
2742
- mcp.command("unassign <mcpServerId>").description("Remove an MCP server from an agent").requiredOption("-s, --stage <name>", "Deployment stage").requiredOption("--agent <id>", "Agent ID").action(async (mcpServerId, opts) => {
2743
- const check = validateStage(opts.stage);
2744
- if (!check.valid) {
2745
- printError(check.error);
2746
- process.exit(1);
2916
+ mcp.command("assign <mcpServerId>").description("Assign an MCP server to an agent. Prompts for stage/agent when omitted in a TTY.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--agent <id>", "Agent ID").action(
2917
+ async (mcpServerId, opts) => {
2918
+ try {
2919
+ const { input: input3 } = await import("@inquirer/prompts");
2920
+ const { api } = await resolveMcpContext(opts);
2921
+ let agent = opts.agent;
2922
+ if (!agent) {
2923
+ if (!process.stdin.isTTY) {
2924
+ printError("--agent is required. Pass it as a flag.");
2925
+ process.exit(1);
2926
+ }
2927
+ agent = await input3({ message: "Agent ID:" });
2928
+ }
2929
+ const result = await apiFetch(
2930
+ api.apiUrl,
2931
+ api.authSecret,
2932
+ `/api/skills/agents/${agent}/mcp-servers`,
2933
+ { method: "POST", body: JSON.stringify({ mcpServerId }) }
2934
+ );
2935
+ printSuccess(`MCP server assigned to agent. (${result.created ? "new" : "updated"})`);
2936
+ } catch (err) {
2937
+ if (isCancellation(err)) return;
2938
+ printError(err instanceof Error ? err.message : String(err));
2939
+ process.exit(1);
2940
+ }
2747
2941
  }
2748
- const api = resolveApiConfig(opts.stage);
2749
- if (!api) process.exit(1);
2750
- try {
2751
- await apiFetch(api.apiUrl, api.authSecret, `/api/skills/agents/${opts.agent}/mcp-servers/${mcpServerId}`, {
2752
- method: "DELETE"
2753
- });
2754
- printSuccess("MCP server unassigned from agent.");
2755
- } catch (err) {
2756
- printError(err.message);
2757
- process.exit(1);
2942
+ );
2943
+ mcp.command("unassign <mcpServerId>").description("Remove an MCP server from an agent. Prompts for stage/agent when omitted in a TTY.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--agent <id>", "Agent ID").action(
2944
+ async (mcpServerId, opts) => {
2945
+ try {
2946
+ const { input: input3 } = await import("@inquirer/prompts");
2947
+ const { api } = await resolveMcpContext(opts);
2948
+ let agent = opts.agent;
2949
+ if (!agent) {
2950
+ if (!process.stdin.isTTY) {
2951
+ printError("--agent is required. Pass it as a flag.");
2952
+ process.exit(1);
2953
+ }
2954
+ agent = await input3({ message: "Agent ID:" });
2955
+ }
2956
+ await apiFetch(
2957
+ api.apiUrl,
2958
+ api.authSecret,
2959
+ `/api/skills/agents/${agent}/mcp-servers/${mcpServerId}`,
2960
+ { method: "DELETE" }
2961
+ );
2962
+ printSuccess("MCP server unassigned from agent.");
2963
+ } catch (err) {
2964
+ if (isCancellation(err)) return;
2965
+ printError(err instanceof Error ? err.message : String(err));
2966
+ process.exit(1);
2967
+ }
2758
2968
  }
2759
- });
2969
+ );
2760
2970
  }
2761
2971
 
2762
2972
  // src/commands/tools.ts
2763
- import { createInterface as createInterface4 } from "readline";
2973
+ import { select as select5, password } from "@inquirer/prompts";
2764
2974
  import chalk11 from "chalk";
2765
- function prompt(question) {
2766
- const rl = createInterface4({ input: process.stdin, output: process.stdout });
2767
- return new Promise((resolve3) => {
2768
- rl.question(question, (answer) => {
2769
- rl.close();
2770
- resolve3(answer.trim());
2771
- });
2772
- });
2773
- }
2774
2975
  var TOOL_PROVIDERS = {
2775
2976
  "web-search": ["exa", "serpapi"]
2776
2977
  };
2978
+ async function resolveCtx(opts) {
2979
+ const stage = await resolveStage({ flag: opts.stage });
2980
+ const api = resolveApiConfig(stage);
2981
+ if (!api) process.exit(1);
2982
+ const tenant = await resolveTenantRest({
2983
+ flag: opts.tenant,
2984
+ stage,
2985
+ apiUrl: api.apiUrl,
2986
+ authSecret: api.authSecret
2987
+ });
2988
+ return { stage, api, tenant };
2989
+ }
2777
2990
  function registerToolsCommand(program2) {
2778
2991
  const tools = program2.command("tools").description("Configure built-in agent tools (web_search, \u2026) for your tenant");
2779
- tools.command("list").description("List configured built-in tools").requiredOption("-s, --stage <name>", "Deployment stage").requiredOption("--tenant <slug>", "Tenant slug").action(async (opts) => {
2780
- const check = validateStage(opts.stage);
2781
- if (!check.valid) {
2782
- printError(check.error);
2783
- process.exit(1);
2784
- }
2785
- const api = resolveApiConfig(opts.stage);
2786
- if (!api) process.exit(1);
2787
- printHeader("tools list", opts.stage);
2992
+ tools.command("list").alias("ls").description("List configured built-in tools. Prompts for stage/tenant in a TTY when omitted.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
2993
+ "after",
2994
+ `
2995
+ Examples:
2996
+ # Interactive \u2014 picks stage + tenant
2997
+ $ thinkwork tools list
2998
+
2999
+ # Scripted
3000
+ $ thinkwork tools list -s dev -t acme
3001
+ $ thinkwork tools list --json | jq '.[] | .toolSlug'
3002
+ `
3003
+ ).action(async (opts) => {
2788
3004
  try {
3005
+ const { stage, api, tenant } = await resolveCtx(opts);
3006
+ printHeader("tools list", stage);
2789
3007
  const { tools: rows } = await apiFetch(
2790
3008
  api.apiUrl,
2791
3009
  api.authSecret,
2792
3010
  "/api/skills/builtin-tools",
2793
3011
  {},
2794
- { "x-tenant-slug": opts.tenant }
3012
+ { "x-tenant-slug": tenant.slug }
2795
3013
  );
2796
3014
  if (!rows || rows.length === 0) {
2797
3015
  console.log(chalk11.dim(" No built-in tools configured."));
2798
- console.log(chalk11.dim(" Try: thinkwork tools web-search set --tenant <slug> -s <stage>"));
3016
+ console.log(chalk11.dim(" Try: thinkwork tools web-search set"));
2799
3017
  return;
2800
3018
  }
2801
3019
  console.log("");
@@ -2806,75 +3024,84 @@ function registerToolsCommand(program2) {
2806
3024
  console.log(` ${chalk11.bold(r.toolSlug)} ${status}`);
2807
3025
  console.log(` Provider: ${provider}`);
2808
3026
  console.log(` Has key: ${key}`);
2809
- if (r.lastTestedAt) {
2810
- console.log(` Tested: ${new Date(r.lastTestedAt).toLocaleString()}`);
2811
- }
3027
+ if (r.lastTestedAt) console.log(` Tested: ${new Date(r.lastTestedAt).toLocaleString()}`);
2812
3028
  console.log("");
2813
3029
  }
2814
3030
  } catch (err) {
2815
- printError(err.message);
3031
+ if (isCancellation(err)) return;
3032
+ printError(err instanceof Error ? err.message : String(err));
2816
3033
  process.exit(1);
2817
3034
  }
2818
3035
  });
2819
3036
  const webSearch = tools.command("web-search").description("Configure the web_search built-in tool");
2820
- webSearch.command("set").description("Set or update web_search provider + API key (enables the tool)").requiredOption("-s, --stage <name>", "Deployment stage").requiredOption("--tenant <slug>", "Tenant slug").option("--provider <name>", `Provider (${TOOL_PROVIDERS["web-search"].join("|")})`).option("--key <key>", "API key").action(async (opts) => {
2821
- const check = validateStage(opts.stage);
2822
- if (!check.valid) {
2823
- printError(check.error);
2824
- process.exit(1);
2825
- }
2826
- const api = resolveApiConfig(opts.stage);
2827
- if (!api) process.exit(1);
2828
- let provider = opts.provider;
2829
- if (!provider) {
2830
- provider = (await prompt(`Provider [${TOOL_PROVIDERS["web-search"].join("/")}]: `)).toLowerCase();
2831
- }
2832
- if (!TOOL_PROVIDERS["web-search"].includes(provider)) {
2833
- printError(`provider must be one of: ${TOOL_PROVIDERS["web-search"].join(", ")}`);
2834
- process.exit(1);
2835
- }
2836
- let apiKey = opts.key;
2837
- if (!apiKey) {
2838
- apiKey = await prompt(`${provider} API key: `);
2839
- }
2840
- if (!apiKey) {
2841
- printError("API key is required");
2842
- process.exit(1);
2843
- }
2844
- try {
2845
- await apiFetch(
2846
- api.apiUrl,
2847
- api.authSecret,
2848
- "/api/skills/builtin-tools/web-search",
2849
- {
2850
- method: "PUT",
2851
- body: JSON.stringify({ provider, apiKey, enabled: true })
2852
- },
2853
- { "x-tenant-slug": opts.tenant }
2854
- );
2855
- printSuccess(`web_search configured with provider=${provider}, enabled=true`);
2856
- printWarning("Run `thinkwork tools web-search test` to verify connectivity.");
2857
- } catch (err) {
2858
- printError(err.message);
2859
- process.exit(1);
2860
- }
2861
- });
2862
- webSearch.command("test").description("Test the stored web_search provider + key against the provider API").requiredOption("-s, --stage <name>", "Deployment stage").requiredOption("--tenant <slug>", "Tenant slug").action(async (opts) => {
2863
- const check = validateStage(opts.stage);
2864
- if (!check.valid) {
2865
- printError(check.error);
2866
- process.exit(1);
3037
+ webSearch.command("set").description("Set or update web_search provider + API key (enables the tool). Prompts when flags are missing in a TTY.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--provider <name>", `Provider (${TOOL_PROVIDERS["web-search"].join("|")})`).option("--key <key>", "API key (will prompt hidden if omitted)").addHelpText(
3038
+ "after",
3039
+ `
3040
+ Examples:
3041
+ # Fully interactive \u2014 stage/tenant picker, provider picker, hidden key prompt
3042
+ $ thinkwork tools web-search set
3043
+
3044
+ # Scripted (secret from env)
3045
+ $ thinkwork tools web-search set -s dev -t acme --provider exa --key "$EXA_KEY"
3046
+ `
3047
+ ).action(
3048
+ async (opts) => {
3049
+ try {
3050
+ const { api, tenant } = await resolveCtx(opts);
3051
+ let provider = opts.provider;
3052
+ if (!provider) {
3053
+ if (!process.stdin.isTTY) {
3054
+ printError(`--provider is required. One of: ${TOOL_PROVIDERS["web-search"].join(", ")}`);
3055
+ process.exit(1);
3056
+ }
3057
+ provider = await select5({
3058
+ message: "Provider:",
3059
+ choices: TOOL_PROVIDERS["web-search"].map((p) => ({ name: p, value: p })),
3060
+ loop: false
3061
+ });
3062
+ }
3063
+ if (!TOOL_PROVIDERS["web-search"].includes(provider)) {
3064
+ printError(`provider must be one of: ${TOOL_PROVIDERS["web-search"].join(", ")}`);
3065
+ process.exit(1);
3066
+ }
3067
+ let apiKey = opts.key;
3068
+ if (!apiKey) {
3069
+ if (!process.stdin.isTTY) {
3070
+ printError("--key is required. Pass it as a flag or pipe via env.");
3071
+ process.exit(1);
3072
+ }
3073
+ apiKey = await password({ message: `${provider} API key:`, mask: "*" });
3074
+ }
3075
+ if (!apiKey) {
3076
+ printError("API key is required");
3077
+ process.exit(1);
3078
+ }
3079
+ await apiFetch(
3080
+ api.apiUrl,
3081
+ api.authSecret,
3082
+ "/api/skills/builtin-tools/web-search",
3083
+ { method: "PUT", body: JSON.stringify({ provider, apiKey, enabled: true }) },
3084
+ { "x-tenant-slug": tenant.slug }
3085
+ );
3086
+ printSuccess(`web_search configured with provider=${provider}, enabled=true`);
3087
+ printWarning("Run `thinkwork tools web-search test` to verify connectivity.");
3088
+ } catch (err) {
3089
+ if (isCancellation(err)) return;
3090
+ printError(err instanceof Error ? err.message : String(err));
3091
+ process.exit(1);
3092
+ }
2867
3093
  }
2868
- const api = resolveApiConfig(opts.stage);
2869
- if (!api) process.exit(1);
2870
- printHeader("tools web-search test", opts.stage);
3094
+ );
3095
+ webSearch.command("test").description("Test the stored web_search provider + key against the provider API.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(async (opts) => {
2871
3096
  try {
3097
+ const { stage, api, tenant } = await resolveCtx(opts);
3098
+ printHeader("tools web-search test", stage);
2872
3099
  const result = await apiFetch(
2873
3100
  api.apiUrl,
2874
3101
  api.authSecret,
2875
3102
  "/api/skills/builtin-tools/web-search/test",
2876
3103
  { method: "POST", body: "{}" },
2877
- { "x-tenant-slug": opts.tenant }
3104
+ { "x-tenant-slug": tenant.slug }
2878
3105
  );
2879
3106
  if (result.ok) {
2880
3107
  printSuccess(`${result.provider}: ${result.resultCount} result(s) returned.`);
@@ -2883,51 +3110,42 @@ function registerToolsCommand(program2) {
2883
3110
  process.exit(1);
2884
3111
  }
2885
3112
  } catch (err) {
2886
- printError(err.message);
3113
+ if (isCancellation(err)) return;
3114
+ printError(err instanceof Error ? err.message : String(err));
2887
3115
  process.exit(1);
2888
3116
  }
2889
3117
  });
2890
- webSearch.command("disable").description("Disable web_search without deleting the stored key").requiredOption("-s, --stage <name>", "Deployment stage").requiredOption("--tenant <slug>", "Tenant slug").action(async (opts) => {
2891
- const check = validateStage(opts.stage);
2892
- if (!check.valid) {
2893
- printError(check.error);
2894
- process.exit(1);
2895
- }
2896
- const api = resolveApiConfig(opts.stage);
2897
- if (!api) process.exit(1);
3118
+ webSearch.command("disable").description("Disable web_search without deleting the stored key.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(async (opts) => {
2898
3119
  try {
3120
+ const { api, tenant } = await resolveCtx(opts);
2899
3121
  await apiFetch(
2900
3122
  api.apiUrl,
2901
3123
  api.authSecret,
2902
3124
  "/api/skills/builtin-tools/web-search",
2903
3125
  { method: "PUT", body: JSON.stringify({ enabled: false }) },
2904
- { "x-tenant-slug": opts.tenant }
3126
+ { "x-tenant-slug": tenant.slug }
2905
3127
  );
2906
3128
  printSuccess("web_search disabled.");
2907
3129
  } catch (err) {
2908
- printError(err.message);
3130
+ if (isCancellation(err)) return;
3131
+ printError(err instanceof Error ? err.message : String(err));
2909
3132
  process.exit(1);
2910
3133
  }
2911
3134
  });
2912
- webSearch.command("clear").description("Remove web_search config entirely (deletes stored key)").requiredOption("-s, --stage <name>", "Deployment stage").requiredOption("--tenant <slug>", "Tenant slug").action(async (opts) => {
2913
- const check = validateStage(opts.stage);
2914
- if (!check.valid) {
2915
- printError(check.error);
2916
- process.exit(1);
2917
- }
2918
- const api = resolveApiConfig(opts.stage);
2919
- if (!api) process.exit(1);
3135
+ webSearch.command("clear").description("Remove web_search config entirely (deletes stored key).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(async (opts) => {
2920
3136
  try {
3137
+ const { api, tenant } = await resolveCtx(opts);
2921
3138
  await apiFetch(
2922
3139
  api.apiUrl,
2923
3140
  api.authSecret,
2924
3141
  "/api/skills/builtin-tools/web-search",
2925
3142
  { method: "DELETE" },
2926
- { "x-tenant-slug": opts.tenant }
3143
+ { "x-tenant-slug": tenant.slug }
2927
3144
  );
2928
3145
  printSuccess("web_search configuration cleared.");
2929
3146
  } catch (err) {
2930
- printError(err.message);
3147
+ if (isCancellation(err)) return;
3148
+ printError(err instanceof Error ? err.message : String(err));
2931
3149
  process.exit(1);
2932
3150
  }
2933
3151
  });
@@ -3024,7 +3242,7 @@ function registerUpdateCommand(program2) {
3024
3242
 
3025
3243
  // src/commands/user.ts
3026
3244
  import { spawn as spawn4 } from "child_process";
3027
- import { input, select as select3 } from "@inquirer/prompts";
3245
+ import { input as input2, select as select6 } from "@inquirer/prompts";
3028
3246
  function getTerraformOutput2(cwd, key) {
3029
3247
  return new Promise((resolve3, reject) => {
3030
3248
  const proc = spawn4("terraform", ["output", "-raw", key], {
@@ -3067,9 +3285,6 @@ function runAwsCognitoReset(userPoolId, username, region) {
3067
3285
  proc.on("close", (code) => resolve3({ code: code ?? 1, stdout, stderr }));
3068
3286
  });
3069
3287
  }
3070
- function isCancellation2(err) {
3071
- return err instanceof Error && err.name === "ExitPromptError";
3072
- }
3073
3288
  function requireTty2(label) {
3074
3289
  if (!process.stdin.isTTY) {
3075
3290
  printError(
@@ -3080,7 +3295,7 @@ function requireTty2(label) {
3080
3295
  }
3081
3296
  async function promptEmail() {
3082
3297
  requireTty2("Email");
3083
- return await input({
3298
+ return await input2({
3084
3299
  message: "Email address of the person to invite:",
3085
3300
  validate: (v) => v.trim().includes("@") ? true : "That doesn't look like an email."
3086
3301
  });
@@ -3098,7 +3313,7 @@ async function promptStage(region) {
3098
3313
  console.log(` Using the only deployed stage: ${stages[0]}`);
3099
3314
  return stages[0];
3100
3315
  }
3101
- return await select3({
3316
+ return await select6({
3102
3317
  message: "Which stage?",
3103
3318
  choices: stages.map((s) => ({ name: s, value: s })),
3104
3319
  loop: false
@@ -3117,7 +3332,7 @@ async function promptTenant(apiUrl, authSecret) {
3117
3332
  console.log(` Using the only tenant: ${list[0].name} (${list[0].slug})`);
3118
3333
  return list[0].slug;
3119
3334
  }
3120
- return await select3({
3335
+ return await select6({
3121
3336
  message: "Which tenant?",
3122
3337
  choices: list.map((t) => ({
3123
3338
  name: `${t.name} (slug: ${t.slug})`,
@@ -3128,7 +3343,7 @@ async function promptTenant(apiUrl, authSecret) {
3128
3343
  }
3129
3344
  async function promptOptionalName() {
3130
3345
  if (!process.stdin.isTTY) return void 0;
3131
- const answer = await input({
3346
+ const answer = await input2({
3132
3347
  message: "Display name (optional, press Enter to skip):",
3133
3348
  default: ""
3134
3349
  });
@@ -3136,7 +3351,7 @@ async function promptOptionalName() {
3136
3351
  }
3137
3352
  async function promptRole() {
3138
3353
  if (!process.stdin.isTTY) return "member";
3139
- return await select3({
3354
+ return await select6({
3140
3355
  message: "Role:",
3141
3356
  choices: [
3142
3357
  { name: "member \u2014 regular access", value: "member" },
@@ -3245,7 +3460,7 @@ Agents / scripts that pass all flags stay non-interactive.
3245
3460
  `Invited ${email} to "${tenant}" (role: ${result.body.role}). Cognito has emailed a temporary password; the user sets a new password on first sign-in.`
3246
3461
  );
3247
3462
  } catch (err) {
3248
- if (isCancellation2(err)) {
3463
+ if (isCancellation(err)) {
3249
3464
  console.log("");
3250
3465
  console.log(" Cancelled.");
3251
3466
  return;
@@ -3258,15 +3473,18 @@ Agents / scripts that pass all flags stay non-interactive.
3258
3473
  }
3259
3474
  );
3260
3475
  user.command("reset-password <email>").description(
3261
- "Trigger Cognito's forgot-password flow for a user (admin-initiated). Sends them a verification code email."
3262
- ).option("-p, --profile <name>", "AWS profile").requiredOption("-s, --stage <name>", "Deployment stage (e.g. dev, prod)").option(
3476
+ "Trigger Cognito's forgot-password flow for a user (admin-initiated). Prompts for stage in a TTY when omitted."
3477
+ ).option("-p, --profile <name>", "AWS profile").option("-s, --stage <name>", "Deployment stage (e.g. dev, prod)").option(
3263
3478
  "-r, --region <name>",
3264
3479
  "AWS region (defaults to AWS CLI default / AWS_REGION)"
3265
3480
  ).addHelpText(
3266
3481
  "after",
3267
3482
  `
3268
3483
  Examples:
3269
- # Admin-triggered password reset \u2014 works even if the account is locked
3484
+ # Fully interactive \u2014 picks stage from the deployed ones
3485
+ $ thinkwork user reset-password alice@example.com
3486
+
3487
+ # Scripted
3270
3488
  $ thinkwork user reset-password alice@example.com -s dev
3271
3489
 
3272
3490
  # Target a specific AWS profile + region
@@ -3279,10 +3497,12 @@ FORCE_CHANGE_PASSWORD or has been disabled.
3279
3497
  `
3280
3498
  ).action(
3281
3499
  async (email, opts) => {
3282
- const stageCheck = validateStage(opts.stage);
3283
- if (!stageCheck.valid) {
3284
- printError(stageCheck.error);
3285
- process.exit(1);
3500
+ let stage;
3501
+ try {
3502
+ stage = await resolveStage({ flag: opts.stage, region: opts.region });
3503
+ } catch (err) {
3504
+ if (isCancellation(err)) return;
3505
+ throw err;
3286
3506
  }
3287
3507
  if (!email || !email.includes("@")) {
3288
3508
  printError(
@@ -3290,11 +3510,11 @@ FORCE_CHANGE_PASSWORD or has been disabled.
3290
3510
  );
3291
3511
  process.exit(1);
3292
3512
  }
3293
- printHeader("user reset-password", opts.stage);
3513
+ printHeader("user reset-password", stage);
3294
3514
  const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
3295
- const cwd = resolveTierDir(terraformDir, opts.stage, "app");
3515
+ const cwd = resolveTierDir(terraformDir, stage, "app");
3296
3516
  await ensureInit(cwd);
3297
- await ensureWorkspace(cwd, opts.stage);
3517
+ await ensureWorkspace(cwd, stage);
3298
3518
  let userPoolId;
3299
3519
  try {
3300
3520
  userPoolId = await getTerraformOutput2(cwd, "user_pool_id");
@@ -3341,47 +3561,6 @@ FORCE_CHANGE_PASSWORD or has been disabled.
3341
3561
  // src/commands/me.ts
3342
3562
  import { gql } from "@urql/core";
3343
3563
 
3344
- // src/lib/resolve-stage.ts
3345
- import { select as select4 } from "@inquirer/prompts";
3346
- async function resolveStage(opts = {}) {
3347
- const region = opts.region ?? "us-east-1";
3348
- const validate = opts.validate ?? true;
3349
- const raw = opts.flag ?? process.env.THINKWORK_STAGE ?? loadCliConfig().defaultStage ?? await pickStage(region);
3350
- if (!raw) {
3351
- printError(
3352
- "No stage specified. Pass `--stage <name>`, set THINKWORK_STAGE, or run `thinkwork login --stage <name>`."
3353
- );
3354
- process.exit(1);
3355
- }
3356
- if (validate) {
3357
- const check = validateStage(raw);
3358
- if (!check.valid) {
3359
- printError(check.error);
3360
- process.exit(1);
3361
- }
3362
- }
3363
- return raw;
3364
- }
3365
- async function pickStage(region) {
3366
- const stages = listDeployedStages(region);
3367
- if (stages.length === 0) {
3368
- printError(
3369
- `No Thinkwork deployments found in ${region}. Run \`thinkwork list\` or pass --region.`
3370
- );
3371
- process.exit(1);
3372
- }
3373
- if (stages.length === 1) {
3374
- console.log(` Using the only deployed stage: ${stages[0]}`);
3375
- return stages[0];
3376
- }
3377
- requireTty("Stage");
3378
- return await select4({
3379
- message: "Which stage?",
3380
- choices: stages.map((s) => ({ name: s, value: s })),
3381
- loop: false
3382
- });
3383
- }
3384
-
3385
3564
  // src/lib/gql-client.ts
3386
3565
  import {
3387
3566
  Client,
@@ -3675,8 +3854,8 @@ Examples:
3675
3854
  $ thinkwork thread label remove thr-abc lbl-ops
3676
3855
  `
3677
3856
  ).action(() => notYetImplemented("thread label", 1));
3678
- thread.command("escalate <id>").description("Escalate a thread to another agent (carries context, records actor).").requiredOption("--to-agent <id>", "Agent to escalate to").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--reason <text>", "Reason note (appears in activity log)").action(() => notYetImplemented("thread escalate", 1));
3679
- thread.command("delegate <id>").description("Delegate ownership to another agent without the 'escalation' semantics.").requiredOption("--to-agent <id>", "Agent to delegate to").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("thread delegate", 1));
3857
+ thread.command("escalate <id>").description("Escalate a thread to another agent (carries context, records actor).").option("--to-agent <id>", "Agent to escalate to").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--reason <text>", "Reason note (appears in activity log)").action(() => notYetImplemented("thread escalate", 1));
3858
+ thread.command("delegate <id>").description("Delegate ownership to another agent without the 'escalation' semantics.").option("--to-agent <id>", "Agent to delegate to").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("thread delegate", 1));
3680
3859
  thread.command("delete <id>").description("Permanently delete a thread (not just close). Requires confirmation.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip the confirmation prompt").addHelpText(
3681
3860
  "after",
3682
3861
  `
@@ -3759,7 +3938,7 @@ Examples:
3759
3938
  `
3760
3939
  ).action(() => notYetImplemented("inbox approve", 1));
3761
3940
  inbox.command("reject <id>").description("Reject an inbox item. Downstream agents stop.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--notes <text>", "Rejection reason").action(() => notYetImplemented("inbox reject", 1));
3762
- inbox.command("request-revision <id>").description("Ask for changes \u2014 the agent gets the item back with your notes.").requiredOption("--notes <text>", "What needs to change").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("inbox request-revision", 1));
3941
+ inbox.command("request-revision <id>").description("Ask for changes \u2014 the agent gets the item back with your notes.").option("--notes <text>", "What needs to change").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("inbox request-revision", 1));
3763
3942
  inbox.command("resubmit <id>").description("Resubmit a revised inbox item for approval.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--notes <text>", "What changed").action(() => notYetImplemented("inbox resubmit", 1));
3764
3943
  inbox.command("cancel <id>").description("Cancel a pending approval request (originator or admin).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("inbox cancel", 1));
3765
3944
  inbox.command("comment <id> [content]").description("Add a comment to an inbox item without deciding on it yet.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--file <path>", "Read comment body from a file").action(() => notYetImplemented("inbox comment", 1));
@@ -3811,15 +3990,15 @@ Examples:
3811
3990
  ).action(() => notYetImplemented("agent status", 2));
3812
3991
  agent.command("unpause <id>").description("Resume an agent paused by a budget policy trigger.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("agent unpause", 2));
3813
3992
  const capabilities = agent.command("capabilities").alias("cap").description("Toggle built-in capabilities (email inbox, web search, etc.).");
3814
- capabilities.command("set <agentId>").description("Enable/disable capabilities on an agent.").requiredOption("--capability <name>", "Capability name (email, web-search, file-upload, \u2026)").option("--enabled", "Enable (default if flag present)").option("--disabled", "Disable").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("agent capabilities set", 2));
3993
+ capabilities.command("set <agentId>").description("Enable/disable capabilities on an agent.").option("--capability <name>", "Capability name (email, web-search, file-upload, \u2026)").option("--enabled", "Enable (default if flag present)").option("--disabled", "Disable").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("agent capabilities set", 2));
3815
3994
  const skills = agent.command("skills").description("Attach or configure MCP-backed skills on an agent.");
3816
- skills.command("set <agentId>").description("Enable/disable/configure a skill for an agent.").requiredOption("--skill <id>", "Skill ID (see `thinkwork skill list`)").option("--enabled", "Enable").option("--disabled", "Disable").option("--config <json>", "Inline JSON config for the skill").option("--rate-limit <rps>", "Rate limit in requests/sec").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("agent skills set", 2));
3995
+ skills.command("set <agentId>").description("Enable/disable/configure a skill for an agent.").option("--skill <id>", "Skill ID (see `thinkwork skill list`)").option("--enabled", "Enable").option("--disabled", "Disable").option("--config <json>", "Inline JSON config for the skill").option("--rate-limit <rps>", "Rate limit in requests/sec").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("agent skills set", 2));
3817
3996
  const budget = agent.command("budget").description("Per-agent spend caps \u2014 pause or alert when exceeded.");
3818
- budget.command("set <agentId>").description("Set or update an agent's budget policy.").requiredOption("--limit-usd <amount>", "USD ceiling for the window").option("--window <w>", "daily | weekly | monthly", "monthly").option("--action <a>", "PAUSE | ALERT", "PAUSE").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("agent budget set", 2));
3997
+ budget.command("set <agentId>").description("Set or update an agent's budget policy.").option("--limit-usd <amount>", "USD ceiling for the window").option("--window <w>", "daily | weekly | monthly", "monthly").option("--action <a>", "PAUSE | ALERT", "PAUSE").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("agent budget set", 2));
3819
3998
  budget.command("clear <agentId>").description("Remove an agent's budget policy (falls back to tenant-wide).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("agent budget clear", 2));
3820
3999
  const apiKey = agent.command("api-key").description("Agent API keys \u2014 service-to-service credentials tied to one agent.");
3821
4000
  apiKey.command("list <agentId>").description("List API keys for an agent (metadata only; plaintext shown on create).").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("agent api-key list", 2));
3822
- apiKey.command("create <agentId>").description("Generate a new API key. The plaintext is printed once \u2014 save it.").requiredOption("--name <n>", "Human label for the key (e.g. 'GitHub Actions')").option("--expires <iso>", "Expiration (ISO-8601). Omit for no expiry.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
4001
+ apiKey.command("create <agentId>").description("Generate a new API key. The plaintext is printed once \u2014 save it.").option("--name <n>", "Human label for the key (e.g. 'GitHub Actions')").option("--expires <iso>", "Expiration (ISO-8601). Omit for no expiry.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
3823
4002
  "after",
3824
4003
  `
3825
4004
  Examples:
@@ -3956,7 +4135,7 @@ Examples:
3956
4135
  kb.command("update <id>").description("Update knowledge base metadata (name, description). Source changes need re-create.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--name <n>").option("--description <text>").action(() => notYetImplemented("kb update", 2));
3957
4136
  kb.command("delete <id>").description("Delete a knowledge base. Embeddings + index are destroyed.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("kb delete", 2));
3958
4137
  kb.command("sync <id>").description("Re-embed from S3. Idempotent; safe to re-run.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--wait", "Block until the sync finishes").action(() => notYetImplemented("kb sync", 2));
3959
- kb.command("attach <kbId>").description("Attach a knowledge base to an agent.").requiredOption("--agent <id>", "Agent ID").option("--config <json>", "Retrieval config (topK, score threshold, \u2026)").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
4138
+ kb.command("attach <kbId>").description("Attach a knowledge base to an agent.").option("--agent <id>", "Agent ID").option("--config <json>", "Retrieval config (topK, score threshold, \u2026)").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
3960
4139
  "after",
3961
4140
  `
3962
4141
  Examples:
@@ -3964,7 +4143,7 @@ Examples:
3964
4143
  $ thinkwork kb attach kb-runbooks --agent agt-oncall --config '{"topK":5}'
3965
4144
  `
3966
4145
  ).action(() => notYetImplemented("kb attach", 2));
3967
- kb.command("detach <kbId>").description("Detach a knowledge base from an agent.").requiredOption("--agent <id>", "Agent ID").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("kb detach", 2));
4146
+ kb.command("detach <kbId>").description("Detach a knowledge base from an agent.").option("--agent <id>", "Agent ID").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("kb detach", 2));
3968
4147
  }
3969
4148
 
3970
4149
  // src/commands/routine.ts
@@ -3987,7 +4166,7 @@ Examples:
3987
4166
  run2.command("list <routineId>").alias("ls").description("List recent runs of a routine.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("--limit <n>", "Max rows", "25").option("--cursor <c>", "Pagination cursor").action(() => notYetImplemented("routine run list", 3));
3988
4167
  run2.command("get <runId>").description("Fetch one run with its step outputs.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("routine run get", 3));
3989
4168
  const trigger = routine.command("trigger-config").description("Manage a routine's triggers (cron, webhook, event).");
3990
- trigger.command("set <routineId>").description("Set or replace a trigger for a routine.").requiredOption("--type <t>", "CRON | WEBHOOK | EVENT").option("--schedule <cron>", "Cron expression (for CRON triggers)").option("--event <name>", "Event name (for EVENT triggers)").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
4169
+ trigger.command("set <routineId>").description("Set or replace a trigger for a routine.").option("--type <t>", "CRON | WEBHOOK | EVENT").option("--schedule <cron>", "Cron expression (for CRON triggers)").option("--event <name>", "Event name (for EVENT triggers)").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
3991
4170
  "after",
3992
4171
  `
3993
4172
  Examples:
@@ -4040,7 +4219,7 @@ Examples:
4040
4219
  function registerWakeupCommand(program2) {
4041
4220
  const wake = program2.command("wakeup").alias("wakeups").description("View and create agent wakeup requests (deferred/enqueued invocations).");
4042
4221
  wake.command("list").alias("ls").description("List queued wakeups in the tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("wakeup list", 3));
4043
- wake.command("create").description("Queue a wakeup for an agent (ad-hoc or deferred).").requiredOption("--agent <id>", "Target agent").option("--thread <id>", "Thread to operate on (optional)").option("--delay-seconds <n>", "Wait N seconds before firing", "0").option("--payload <json>", "Optional input payload").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
4222
+ wake.command("create").description("Queue a wakeup for an agent (ad-hoc or deferred).").option("--agent <id>", "Target agent").option("--thread <id>", "Thread to operate on (optional)").option("--delay-seconds <n>", "Wait N seconds before firing", "0").option("--payload <json>", "Optional input payload").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
4044
4223
  "after",
4045
4224
  `
4046
4225
  Examples:
@@ -4122,12 +4301,12 @@ Examples:
4122
4301
  // src/commands/memory.ts
4123
4302
  function registerMemoryCommand(program2) {
4124
4303
  const memory = program2.command("memory").description("Inspect, search, and edit an agent's memory records and graph.");
4125
- memory.command("list").alias("ls").description("List memory records for an agent in a namespace.").requiredOption("--agent <id>", "Agent (assistant) ID").option(
4304
+ memory.command("list").alias("ls").description("List memory records for an agent in a namespace.").option("--agent <id>", "Agent (assistant) ID").option(
4126
4305
  "--namespace <ns>",
4127
4306
  "Memory namespace (semantic | preferences | episodes | reflections)",
4128
4307
  "semantic"
4129
4308
  ).option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("memory list", 4));
4130
- memory.command("search").description("Search an agent's memory by query string.").requiredOption("--agent <id>", "Agent (assistant) ID").requiredOption("--query <q>", "Search query").option("--strategy <s>", "Retrieval strategy (semantic | keyword | hybrid)", "semantic").option("--limit <n>", "Max results", "10").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
4309
+ memory.command("search").description("Search an agent's memory by query string.").option("--agent <id>", "Agent (assistant) ID").option("--query <q>", "Search query").option("--strategy <s>", "Retrieval strategy (semantic | keyword | hybrid)", "semantic").option("--limit <n>", "Max results", "10").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
4131
4310
  "after",
4132
4311
  `
4133
4312
  Examples:
@@ -4136,9 +4315,9 @@ Examples:
4136
4315
  `
4137
4316
  ).action(() => notYetImplemented("memory search", 4));
4138
4317
  memory.command("get <recordId>").description("Fetch one memory record in full.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("memory get", 4));
4139
- memory.command("update <recordId>").description("Replace a memory record's content.").requiredOption("--content <text>", "New content").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("memory update", 4));
4318
+ memory.command("update <recordId>").description("Replace a memory record's content.").option("--content <text>", "New content").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("memory update", 4));
4140
4319
  memory.command("delete <recordId>").description("Remove a memory record.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").option("-y, --yes", "Skip confirmation").action(() => notYetImplemented("memory delete", 4));
4141
- memory.command("graph").description("Print the agent's memory graph (summary in human mode; full JSON with --json).").requiredOption("--agent <id>", "Agent (assistant) ID").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("memory graph", 4));
4320
+ memory.command("graph").description("Print the agent's memory graph (summary in human mode; full JSON with --json).").option("--agent <id>", "Agent (assistant) ID").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("memory graph", 4));
4142
4321
  }
4143
4322
 
4144
4323
  // src/commands/recipe.ts
@@ -4199,7 +4378,7 @@ function registerBudgetCommand(program2) {
4199
4378
  const budget = program2.command("budget").alias("budgets").description("Manage budget policies (tenant-wide or per-agent) and inspect current status.");
4200
4379
  budget.command("list").alias("ls").description("List budget policies in the tenant.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("budget list", 5));
4201
4380
  budget.command("status").description("Show each budget's current spend vs. limit.").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").action(() => notYetImplemented("budget status", 5));
4202
- budget.command("upsert").description("Create or update a budget policy.").requiredOption("--limit-usd <amount>", "USD ceiling for the window").option("--scope <s>", "tenant | agent", "tenant").option("--agent <id>", "Required if --scope=agent").option("--window <w>", "daily | weekly | monthly", "monthly").option("--action <a>", "PAUSE | ALERT", "PAUSE").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
4381
+ budget.command("upsert").description("Create or update a budget policy.").option("--limit-usd <amount>", "USD ceiling for the window").option("--scope <s>", "tenant | agent", "tenant").option("--agent <id>", "Required if --scope=agent").option("--window <w>", "daily | weekly | monthly", "monthly").option("--action <a>", "PAUSE | ALERT", "PAUSE").option("-s, --stage <name>", "Deployment stage").option("-t, --tenant <slug>", "Tenant slug").addHelpText(
4203
4382
  "after",
4204
4383
  `
4205
4384
  Examples: