thinkwork-cli 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -64,6 +64,8 @@ No repo clone required — `thinkwork init` scaffolds all Terraform modules from
64
64
 
65
65
  | Command | Description |
66
66
  |---------|-------------|
67
+ | `thinkwork status` | Discover all deployed environments in AWS (clickable URLs) |
68
+ | `thinkwork status -s <stage>` | Detailed view for one environment |
67
69
  | `thinkwork outputs -s <stage>` | Show deployment outputs (API URL, Cognito IDs, etc.) |
68
70
  | `thinkwork config list` | List all initialized environments |
69
71
  | `thinkwork config list -s <stage>` | Show full config for an environment (secrets masked) |
@@ -110,6 +112,25 @@ All initialized environments are saved to `~/.thinkwork/environments/<stage>/con
110
112
 
111
113
  ## Examples
112
114
 
115
+ ### Check all environments
116
+
117
+ ```bash
118
+ thinkwork status
119
+ ```
120
+
121
+ ```
122
+ Environments
123
+ ──────────────────────────────────────────────────────────────────────
124
+ Stage Source Lambdas AgentCore Memory URLs
125
+ ──────────────────────────────────────────────────────────────────────
126
+ ● dev aws+cli 42 active hindsight ✓ API: https://ho7oy...
127
+ WS: dcrs2r...
128
+ Mem: http://tw-dev...
129
+ ──────────────────────────────────────────────────────────────────────
130
+ ```
131
+
132
+ URLs are clickable in supported terminals (iTerm2, Windows Terminal, VS Code, etc.).
133
+
113
134
  ### Switch memory engine
114
135
 
115
136
  ```bash
package/dist/cli.js CHANGED
@@ -738,8 +738,8 @@ function registerBootstrapCommand(program2) {
738
738
  bucket = await getTerraformOutput(cwd, "bucket_name");
739
739
  dbEndpoint = await getTerraformOutput(cwd, "db_cluster_endpoint");
740
740
  const secretArn = await getTerraformOutput(cwd, "db_secret_arn");
741
- const { execSync: execSync7 } = await import("child_process");
742
- const secretJson = execSync7(
741
+ const { execSync: execSync8 } = await import("child_process");
742
+ const secretJson = execSync8(
743
743
  `aws secretsmanager get-secret-value --secret-id "${secretArn}" --query SecretString --output text`,
744
744
  { encoding: "utf-8" }
745
745
  ).trim();
@@ -1276,6 +1276,10 @@ output "hindsight_endpoint" { value = module.thinkwork.hindsight_endpoint }
1276
1276
  // src/commands/status.ts
1277
1277
  import { execSync as execSync6 } from "child_process";
1278
1278
  import chalk6 from "chalk";
1279
+ function link(url, label) {
1280
+ const text = label || url;
1281
+ return `\x1B]8;;${url}\x1B\\${text}\x1B]8;;\x1B\\`;
1282
+ }
1279
1283
  function runAws(cmd) {
1280
1284
  try {
1281
1285
  return execSync6(`aws ${cmd}`, {
@@ -1308,6 +1312,14 @@ function discoverAwsStages(region) {
1308
1312
  `apigatewayv2 get-apis --region ${region} --query "Items[?Name=='thinkwork-${stage}-api'].ApiEndpoint|[0]" --output text`
1309
1313
  );
1310
1314
  if (apiRaw && apiRaw !== "None") info.apiEndpoint = apiRaw;
1315
+ const appsyncRaw = runAws(
1316
+ `appsync list-graphql-apis --region ${region} --query "graphqlApis[?name=='thinkwork-${stage}-subscriptions'].uris.REALTIME|[0]" --output text`
1317
+ );
1318
+ if (appsyncRaw && appsyncRaw !== "None") info.appsyncUrl = appsyncRaw;
1319
+ const appsyncApiRaw = runAws(
1320
+ `appsync list-graphql-apis --region ${region} --query "graphqlApis[?name=='thinkwork-${stage}-subscriptions'].uris.GRAPHQL|[0]" --output text`
1321
+ );
1322
+ if (appsyncApiRaw && appsyncApiRaw !== "None") info.appsyncApiUrl = appsyncApiRaw;
1311
1323
  const acRaw = runAws(
1312
1324
  `lambda get-function --function-name thinkwork-${stage}-agentcore --region ${region} --query "Configuration.State" --output text 2>/dev/null`
1313
1325
  );
@@ -1325,6 +1337,7 @@ function discoverAwsStages(region) {
1325
1337
  `elbv2 describe-load-balancers --region ${region} --query "LoadBalancers[?contains(LoadBalancerName, 'tw-${stage}-hindsight')].DNSName|[0]" --output text`
1326
1338
  );
1327
1339
  if (albRaw && albRaw !== "None") {
1340
+ info.hindsightEndpoint = `http://${albRaw}`;
1328
1341
  try {
1329
1342
  const health = execSync6(`curl -s --max-time 3 http://${albRaw}/health`, { encoding: "utf-8" }).trim();
1330
1343
  info.hindsightHealth = health.includes("healthy") ? "healthy" : "unhealthy";
@@ -1335,6 +1348,27 @@ function discoverAwsStages(region) {
1335
1348
  } else {
1336
1349
  info.memoryEngine = "managed";
1337
1350
  }
1351
+ const dbRaw = runAws(
1352
+ `rds describe-db-clusters --region ${region} --query "DBClusters[?starts_with(DBClusterIdentifier, 'thinkwork-${stage}')].Endpoint|[0]" --output text`
1353
+ );
1354
+ if (dbRaw && dbRaw !== "None") info.dbEndpoint = dbRaw;
1355
+ const ecrRaw = runAws(
1356
+ `ecr describe-repositories --region ${region} --query "repositories[?repositoryName=='thinkwork-${stage}-agentcore'].repositoryUri|[0]" --output text`
1357
+ );
1358
+ if (ecrRaw && ecrRaw !== "None") info.ecrUrl = ecrRaw;
1359
+ const cfJson = runAws(
1360
+ `cloudfront list-distributions --query "DistributionList.Items[?contains(Origins.Items[0].DomainName, 'thinkwork-${stage}-')].{Origin:Origins.Items[0].DomainName,Domain:DomainName}" --output json`
1361
+ );
1362
+ if (cfJson) {
1363
+ try {
1364
+ const dists = JSON.parse(cfJson);
1365
+ for (const d of dists) {
1366
+ if (d.Origin.includes(`thinkwork-${stage}-admin`)) info.adminUrl = `https://${d.Domain}`;
1367
+ if (d.Origin.includes(`thinkwork-${stage}-docs`)) info.docsUrl = `https://${d.Domain}`;
1368
+ }
1369
+ } catch {
1370
+ }
1371
+ }
1338
1372
  }
1339
1373
  return stages;
1340
1374
  }
@@ -1381,19 +1415,26 @@ function registerStatusCommand(program2) {
1381
1415
  console.log(` ${chalk6.bold("Source:")} ${info.source === "both" ? "AWS + local config" : info.source === "aws" ? "AWS (no local config)" : "local only (not in AWS)"}`);
1382
1416
  console.log(` ${chalk6.bold("Region:")} ${info.region}`);
1383
1417
  console.log(` ${chalk6.bold("Account:")} ${info.accountId}`);
1384
- if (info.apiEndpoint) console.log(` ${chalk6.bold("API:")} ${info.apiEndpoint}`);
1385
- if (info.lambdaCount) console.log(` ${chalk6.bold("Lambda fns:")} ${info.lambdaCount}`);
1418
+ console.log(` ${chalk6.bold("Lambda fns:")} ${info.lambdaCount || "\u2014"}`);
1386
1419
  console.log(` ${chalk6.bold("AgentCore:")} ${info.agentcoreStatus || "unknown"}`);
1387
1420
  console.log(` ${chalk6.bold("Memory:")} ${info.memoryEngine || "unknown"}`);
1388
1421
  if (info.hindsightHealth) console.log(` ${chalk6.bold("Hindsight:")} ${info.hindsightHealth}`);
1389
1422
  if (info.bucketName) console.log(` ${chalk6.bold("S3 bucket:")} ${info.bucketName}`);
1423
+ if (info.dbEndpoint) console.log(` ${chalk6.bold("Database:")} ${info.dbEndpoint}`);
1424
+ if (info.ecrUrl) console.log(` ${chalk6.bold("ECR:")} ${info.ecrUrl}`);
1425
+ console.log("");
1426
+ console.log(chalk6.bold(" URLs:"));
1427
+ if (info.adminUrl) console.log(` Admin: ${link(info.adminUrl)}`);
1428
+ if (info.docsUrl) console.log(` Docs: ${link(info.docsUrl)}`);
1429
+ if (info.apiEndpoint) console.log(` API: ${link(info.apiEndpoint)}`);
1430
+ if (info.appsyncApiUrl) console.log(` AppSync: ${link(info.appsyncApiUrl)}`);
1431
+ if (info.appsyncUrl) console.log(` WebSocket: ${link(info.appsyncUrl)}`);
1432
+ if (info.hindsightEndpoint) console.log(` Hindsight: ${link(info.hindsightEndpoint)}`);
1390
1433
  console.log(chalk6.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1391
1434
  const local = loadEnvironment(opts.stage);
1392
1435
  if (local) {
1393
- console.log("");
1394
1436
  console.log(chalk6.dim(` Terraform dir: ${local.terraformDir}`));
1395
1437
  } else {
1396
- console.log("");
1397
1438
  console.log(chalk6.dim(` No local config. Run: thinkwork init -s ${opts.stage}`));
1398
1439
  }
1399
1440
  console.log("");
@@ -1405,28 +1446,228 @@ function registerStatusCommand(program2) {
1405
1446
  console.log("");
1406
1447
  return;
1407
1448
  }
1449
+ const COL1 = 16;
1450
+ const COL2 = 10;
1451
+ const COL3 = 10;
1452
+ const COL4 = 14;
1453
+ const COL5 = 14;
1454
+ const pad = " ".repeat(2 + 2 + COL1 + COL2 + COL3 + COL4 + COL5);
1408
1455
  console.log(chalk6.bold(" Environments"));
1409
- console.log(chalk6.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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"));
1456
+ console.log(chalk6.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1410
1457
  console.log(
1411
- chalk6.dim(" ") + "Stage".padEnd(16) + "Source".padEnd(10) + "Lambdas".padEnd(10) + "AgentCore".padEnd(14) + "Memory".padEnd(14) + "API"
1458
+ chalk6.dim(" ") + "Stage".padEnd(COL1) + "Source".padEnd(COL2) + "Lambdas".padEnd(COL3) + "AgentCore".padEnd(COL4) + "Memory".padEnd(COL5) + "URLs"
1412
1459
  );
1413
- console.log(chalk6.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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"));
1460
+ console.log(chalk6.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1414
1461
  for (const [, info] of [...merged].sort((a, b) => a[0].localeCompare(b[0]))) {
1415
1462
  const sourceBadge = info.source === "both" ? chalk6.green("\u25CF") : info.source === "aws" ? chalk6.yellow("\u25CF") : chalk6.dim("\u25CB");
1416
1463
  const acStatus = info.agentcoreStatus === "Active" ? chalk6.green("active") : info.agentcoreStatus === "not deployed" ? chalk6.dim("\u2014") : chalk6.yellow(info.agentcoreStatus || "\u2014");
1417
1464
  const memBadge = info.memoryEngine === "hindsight" ? info.hindsightHealth === "healthy" ? chalk6.magenta("hindsight \u2713") : chalk6.yellow("hindsight ?") : chalk6.dim(info.memoryEngine || "\u2014");
1418
- console.log(
1419
- ` ${sourceBadge} ` + chalk6.bold(info.stage.padEnd(14)) + (info.source === "both" ? "aws+cli" : info.source).padEnd(10) + String(info.lambdaCount || "\u2014").padEnd(10) + acStatus.padEnd(22) + memBadge.padEnd(22) + chalk6.dim(info.apiEndpoint || "\u2014")
1420
- );
1465
+ const prefix = ` ${sourceBadge} ` + chalk6.bold(info.stage.padEnd(COL1 - 1)) + " " + (info.source === "both" ? "aws+cli" : info.source).padEnd(COL2) + String(info.lambdaCount || "\u2014").padEnd(COL3);
1466
+ const urls = [];
1467
+ if (info.adminUrl) urls.push(`Admin: ${link(info.adminUrl, info.adminUrl)}`);
1468
+ if (info.docsUrl) urls.push(`Docs: ${link(info.docsUrl, info.docsUrl)}`);
1469
+ if (info.apiEndpoint) urls.push(`API: ${link(info.apiEndpoint, info.apiEndpoint)}`);
1470
+ if (info.appsyncUrl) urls.push(`WS: ${link(info.appsyncUrl, info.appsyncUrl.replace("wss://", "").split(".")[0] + "...")}`);
1471
+ if (info.hindsightEndpoint) urls.push(`Mem: ${link(info.hindsightEndpoint, info.hindsightEndpoint)}`);
1472
+ if (urls.length === 0) {
1473
+ console.log(prefix + acStatus.padEnd(22) + memBadge.padEnd(22) + chalk6.dim("\u2014"));
1474
+ } else {
1475
+ console.log(prefix + acStatus.padEnd(22) + memBadge.padEnd(22) + chalk6.dim(urls[0]));
1476
+ for (let i = 1; i < urls.length; i++) {
1477
+ console.log(pad + chalk6.dim(urls[i]));
1478
+ }
1479
+ }
1421
1480
  }
1422
- console.log(chalk6.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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"));
1423
- console.log(chalk6.dim(` ${merged.size} environment(s) `) + chalk6.green("\u25CF") + chalk6.dim(" aws+cli ") + chalk6.yellow("\u25CF") + chalk6.dim(" aws only ") + chalk6.dim("\u25CB local only"));
1481
+ console.log(chalk6.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1482
+ console.log(
1483
+ chalk6.dim(` ${merged.size} environment(s) `) + chalk6.green("\u25CF") + chalk6.dim(" aws+cli ") + chalk6.yellow("\u25CF") + chalk6.dim(" aws only ") + chalk6.dim("\u25CB local only")
1484
+ );
1424
1485
  console.log("");
1425
1486
  console.log(` Details: ${chalk6.cyan("thinkwork status -s <stage>")}`);
1426
1487
  console.log("");
1427
1488
  });
1428
1489
  }
1429
1490
 
1491
+ // src/commands/mcp.ts
1492
+ import { readFileSync as readFileSync3, existsSync as existsSync6 } from "fs";
1493
+ import { execSync as execSync7 } from "child_process";
1494
+ import chalk7 from "chalk";
1495
+ function readTfVar2(tfvarsPath, key) {
1496
+ if (!existsSync6(tfvarsPath)) return null;
1497
+ const content = readFileSync3(tfvarsPath, "utf-8");
1498
+ const match = content.match(new RegExp(`^${key}\\s*=\\s*"([^"]*)"`, "m"));
1499
+ return match ? match[1] : null;
1500
+ }
1501
+ function resolveTfvarsPath2(stage) {
1502
+ const tfDir = resolveTerraformDir(stage);
1503
+ if (tfDir) {
1504
+ const direct = `${tfDir}/terraform.tfvars`;
1505
+ if (existsSync6(direct)) return direct;
1506
+ }
1507
+ const terraformDir = process.env.THINKWORK_TERRAFORM_DIR || process.cwd();
1508
+ const cwd = resolveTierDir(terraformDir, stage, "app");
1509
+ return `${cwd}/terraform.tfvars`;
1510
+ }
1511
+ function getApiEndpoint(stage, region) {
1512
+ try {
1513
+ const raw = execSync7(
1514
+ `aws apigatewayv2 get-apis --region ${region} --query "Items[?Name=='thinkwork-${stage}-api'].ApiEndpoint|[0]" --output text`,
1515
+ { encoding: "utf-8", timeout: 15e3, stdio: ["pipe", "pipe", "pipe"] }
1516
+ ).trim();
1517
+ return raw && raw !== "None" ? raw : null;
1518
+ } catch {
1519
+ return null;
1520
+ }
1521
+ }
1522
+ async function apiFetch(apiUrl, authSecret, path2, options = {}) {
1523
+ const res = await fetch(`${apiUrl}${path2}`, {
1524
+ ...options,
1525
+ headers: {
1526
+ "Content-Type": "application/json",
1527
+ Authorization: `Bearer ${authSecret}`,
1528
+ ...options.headers
1529
+ }
1530
+ });
1531
+ if (!res.ok) {
1532
+ const body = await res.json().catch(() => ({}));
1533
+ throw new Error(body.error || `HTTP ${res.status}`);
1534
+ }
1535
+ return res.json();
1536
+ }
1537
+ function resolveApiConfig(stage) {
1538
+ const tfvarsPath = resolveTfvarsPath2(stage);
1539
+ const authSecret = readTfVar2(tfvarsPath, "api_auth_secret");
1540
+ if (!authSecret) {
1541
+ printError(`Cannot read api_auth_secret from ${tfvarsPath}`);
1542
+ return null;
1543
+ }
1544
+ const region = readTfVar2(tfvarsPath, "region") || "us-east-1";
1545
+ const apiUrl = getApiEndpoint(stage, region);
1546
+ if (!apiUrl) {
1547
+ printError(`Cannot discover API endpoint for stage "${stage}". Is the stack deployed?`);
1548
+ return null;
1549
+ }
1550
+ return { apiUrl, authSecret };
1551
+ }
1552
+ function registerMcpCommand(program2) {
1553
+ const mcp = program2.command("mcp").description("Manage MCP servers for agents");
1554
+ mcp.command("list").description("List MCP servers registered for an agent").requiredOption("-s, --stage <name>", "Deployment stage").requiredOption("--agent <id>", "Agent ID").action(async (opts) => {
1555
+ const check = validateStage(opts.stage);
1556
+ if (!check.valid) {
1557
+ printError(check.error);
1558
+ process.exit(1);
1559
+ }
1560
+ const api = resolveApiConfig(opts.stage);
1561
+ if (!api) process.exit(1);
1562
+ printHeader("mcp list", opts.stage);
1563
+ try {
1564
+ const { servers } = await apiFetch(api.apiUrl, api.authSecret, `/api/skills/agent/${opts.agent}/mcp-servers`);
1565
+ if (!servers || servers.length === 0) {
1566
+ console.log(chalk7.dim(" No MCP servers registered for this agent."));
1567
+ return;
1568
+ }
1569
+ console.log("");
1570
+ for (const s of servers) {
1571
+ const status = s.enabled ? chalk7.green("enabled") : chalk7.dim("disabled");
1572
+ console.log(` ${chalk7.bold(s.name)} ${status}`);
1573
+ console.log(` URL: ${s.url}`);
1574
+ console.log(` Transport: ${s.transport}`);
1575
+ console.log(` Auth: ${s.authType || "none"}`);
1576
+ if (s.tools?.length) {
1577
+ console.log(` Tools: ${s.tools.join(", ")}`);
1578
+ }
1579
+ console.log("");
1580
+ }
1581
+ } catch (err) {
1582
+ printError(err.message);
1583
+ process.exit(1);
1584
+ }
1585
+ });
1586
+ mcp.command("add <name>").description("Register an MCP server for an agent").requiredOption("-s, --stage <name>", "Deployment stage").requiredOption("--agent <id>", "Agent ID").requiredOption("--url <url>", "MCP server URL").option("--transport <type>", "Transport type (streamable-http|sse)", "streamable-http").option("--auth-type <type>", "Auth type (none|bearer|api-key)", "none").option("--auth-value <token>", "Auth token or API key").option("--connection-id <uuid>", "OAuth connection ID").option("--provider-id <uuid>", "OAuth provider ID (for connection-based auth)").option("--tools <list>", "Comma-separated tool allowlist").action(async (name, opts) => {
1587
+ const check = validateStage(opts.stage);
1588
+ if (!check.valid) {
1589
+ printError(check.error);
1590
+ process.exit(1);
1591
+ }
1592
+ const api = resolveApiConfig(opts.stage);
1593
+ if (!api) process.exit(1);
1594
+ printHeader("mcp add", opts.stage);
1595
+ const body = {
1596
+ name,
1597
+ url: opts.url,
1598
+ transport: opts.transport,
1599
+ authType: opts.authType !== "none" ? opts.authType : void 0
1600
+ };
1601
+ if (opts.authValue) body.apiKey = opts.authValue;
1602
+ if (opts.connectionId) body.connectionId = opts.connectionId;
1603
+ if (opts.providerId) body.providerId = opts.providerId;
1604
+ if (opts.tools) body.tools = opts.tools.split(",").map((t) => t.trim());
1605
+ try {
1606
+ const result = await apiFetch(api.apiUrl, api.authSecret, `/api/skills/agent/${opts.agent}/mcp-servers`, {
1607
+ method: "POST",
1608
+ body: JSON.stringify(body)
1609
+ });
1610
+ printSuccess(`MCP server "${name}" ${result.created ? "added" : "updated"} (skill: ${result.skillId})`);
1611
+ } catch (err) {
1612
+ printError(err.message);
1613
+ process.exit(1);
1614
+ }
1615
+ });
1616
+ mcp.command("remove <name>").description("Remove an MCP server from an agent").requiredOption("-s, --stage <name>", "Deployment stage").requiredOption("--agent <id>", "Agent ID").action(async (name, opts) => {
1617
+ const check = validateStage(opts.stage);
1618
+ if (!check.valid) {
1619
+ printError(check.error);
1620
+ process.exit(1);
1621
+ }
1622
+ const api = resolveApiConfig(opts.stage);
1623
+ if (!api) process.exit(1);
1624
+ try {
1625
+ await apiFetch(api.apiUrl, api.authSecret, `/api/skills/agent/${opts.agent}/mcp-servers/${name}`, {
1626
+ method: "DELETE"
1627
+ });
1628
+ printSuccess(`MCP server "${name}" removed.`);
1629
+ } catch (err) {
1630
+ printError(err.message);
1631
+ process.exit(1);
1632
+ }
1633
+ });
1634
+ mcp.command("test <name>").description("Test connection to an MCP server and list its tools").requiredOption("-s, --stage <name>", "Deployment stage").requiredOption("--agent <id>", "Agent ID").action(async (name, opts) => {
1635
+ const check = validateStage(opts.stage);
1636
+ if (!check.valid) {
1637
+ printError(check.error);
1638
+ process.exit(1);
1639
+ }
1640
+ const api = resolveApiConfig(opts.stage);
1641
+ if (!api) process.exit(1);
1642
+ printHeader("mcp test", opts.stage);
1643
+ try {
1644
+ const result = await apiFetch(api.apiUrl, api.authSecret, `/api/skills/agent/${opts.agent}/mcp-servers/${name}/test`, {
1645
+ method: "POST"
1646
+ });
1647
+ if (result.ok) {
1648
+ printSuccess(`Connection to "${name}" successful.`);
1649
+ if (result.tools?.length) {
1650
+ console.log(chalk7.bold(`
1651
+ Available tools (${result.tools.length}):
1652
+ `));
1653
+ for (const t of result.tools) {
1654
+ console.log(` ${chalk7.cyan(t.name)}${t.description ? chalk7.dim(` \u2014 ${t.description}`) : ""}`);
1655
+ }
1656
+ console.log("");
1657
+ } else {
1658
+ printWarning("Server connected but reported no tools.");
1659
+ }
1660
+ } else {
1661
+ printError(`Connection failed: ${result.error}`);
1662
+ process.exit(1);
1663
+ }
1664
+ } catch (err) {
1665
+ printError(err.message);
1666
+ process.exit(1);
1667
+ }
1668
+ });
1669
+ }
1670
+
1430
1671
  // src/cli.ts
1431
1672
  var program = new Command();
1432
1673
  program.name("thinkwork").description(
@@ -1451,4 +1692,5 @@ registerDestroyCommand(program);
1451
1692
  registerStatusCommand(program);
1452
1693
  registerOutputsCommand(program);
1453
1694
  registerConfigCommand(program);
1695
+ registerMcpCommand(program);
1454
1696
  program.parse();
@@ -31,9 +31,13 @@ terraform {
31
31
  }
32
32
  }
33
33
 
34
- # For the example, use local state. Production deployments should
35
- # use S3 + DynamoDB backend — see the docs for configuration.
36
- # backend "s3" { ... }
34
+ backend "s3" {
35
+ bucket = "thinkwork-terraform-state"
36
+ key = "thinkwork/dev/terraform.tfstate"
37
+ region = "us-east-1"
38
+ dynamodb_table = "thinkwork-terraform-locks"
39
+ encrypt = true
40
+ }
37
41
  }
38
42
 
39
43
  provider "aws" {
@@ -134,11 +138,27 @@ output "api_endpoint" {
134
138
  value = module.thinkwork.api_endpoint
135
139
  }
136
140
 
141
+ output "appsync_api_url" {
142
+ description = "AppSync GraphQL URL"
143
+ value = module.thinkwork.appsync_api_url
144
+ }
145
+
137
146
  output "appsync_realtime_url" {
138
147
  description = "AppSync realtime WebSocket URL (for frontend subscription clients)"
139
148
  value = module.thinkwork.appsync_realtime_url
140
149
  }
141
150
 
151
+ output "appsync_api_key" {
152
+ description = "AppSync API key"
153
+ value = module.thinkwork.appsync_api_key
154
+ sensitive = true
155
+ }
156
+
157
+ output "auth_domain" {
158
+ description = "Cognito hosted UI domain"
159
+ value = module.thinkwork.auth_domain
160
+ }
161
+
142
162
  output "user_pool_id" {
143
163
  description = "Cognito user pool ID"
144
164
  value = module.thinkwork.user_pool_id
@@ -188,3 +208,33 @@ output "hindsight_endpoint" {
188
208
  description = "Hindsight API endpoint (null when memory_engine = managed)"
189
209
  value = module.thinkwork.hindsight_endpoint
190
210
  }
211
+
212
+ output "admin_url" {
213
+ description = "Admin app URL"
214
+ value = "https://${module.thinkwork.admin_distribution_domain}"
215
+ }
216
+
217
+ output "admin_distribution_id" {
218
+ description = "CloudFront distribution ID for admin (for cache invalidation)"
219
+ value = module.thinkwork.admin_distribution_id
220
+ }
221
+
222
+ output "admin_bucket_name" {
223
+ description = "S3 bucket for admin app assets"
224
+ value = module.thinkwork.admin_bucket_name
225
+ }
226
+
227
+ output "docs_url" {
228
+ description = "Docs site URL"
229
+ value = "https://${module.thinkwork.docs_distribution_domain}"
230
+ }
231
+
232
+ output "docs_distribution_id" {
233
+ description = "CloudFront distribution ID for docs (for cache invalidation)"
234
+ value = module.thinkwork.docs_distribution_id
235
+ }
236
+
237
+ output "docs_bucket_name" {
238
+ description = "S3 bucket for docs site assets"
239
+ value = module.thinkwork.docs_bucket_name
240
+ }
@@ -58,8 +58,14 @@ module "cognito" {
58
58
  google_oauth_client_secret = var.google_oauth_client_secret
59
59
  pre_signup_lambda_zip = var.pre_signup_lambda_zip
60
60
 
61
- admin_callback_urls = var.admin_callback_urls
62
- admin_logout_urls = var.admin_logout_urls
61
+ admin_callback_urls = concat(
62
+ var.admin_callback_urls,
63
+ ["https://${module.admin_site.distribution_domain}", "https://${module.admin_site.distribution_domain}/auth/callback"]
64
+ )
65
+ admin_logout_urls = concat(
66
+ var.admin_logout_urls,
67
+ ["https://${module.admin_site.distribution_domain}"]
68
+ )
63
69
  mobile_callback_urls = var.mobile_callback_urls
64
70
  mobile_logout_urls = var.mobile_logout_urls
65
71
  }
@@ -210,3 +216,27 @@ module "ses" {
210
216
  stage = var.stage
211
217
  account_id = var.account_id
212
218
  }
219
+
220
+ ################################################################################
221
+ # Admin Static Site
222
+ ################################################################################
223
+
224
+ module "admin_site" {
225
+ source = "../app/static-site"
226
+
227
+ stage = var.stage
228
+ site_name = "admin"
229
+ }
230
+
231
+ ################################################################################
232
+ # Docs Static Site
233
+ ################################################################################
234
+
235
+ module "docs_site" {
236
+ source = "../app/static-site"
237
+
238
+ stage = var.stage
239
+ site_name = "docs"
240
+ custom_domain = var.docs_domain
241
+ certificate_arn = var.docs_certificate_arn
242
+ }
@@ -72,6 +72,11 @@ output "appsync_api_key" {
72
72
  sensitive = true
73
73
  }
74
74
 
75
+ output "auth_domain" {
76
+ description = "Cognito hosted UI domain"
77
+ value = module.cognito.auth_domain
78
+ }
79
+
75
80
  output "ecr_repository_url" {
76
81
  value = module.agentcore.ecr_repository_url
77
82
  }
@@ -85,3 +90,35 @@ output "hindsight_endpoint" {
85
90
  description = "Hindsight API endpoint (only when memory_engine = hindsight)"
86
91
  value = var.memory_engine == "hindsight" ? module.hindsight[0].hindsight_endpoint : null
87
92
  }
93
+
94
+ # Admin static site
95
+ output "admin_distribution_id" {
96
+ description = "CloudFront distribution ID for the admin app"
97
+ value = module.admin_site.distribution_id
98
+ }
99
+
100
+ output "admin_distribution_domain" {
101
+ description = "CloudFront domain for the admin app"
102
+ value = module.admin_site.distribution_domain
103
+ }
104
+
105
+ output "admin_bucket_name" {
106
+ description = "S3 bucket for admin app assets"
107
+ value = module.admin_site.bucket_name
108
+ }
109
+
110
+ # Docs static site
111
+ output "docs_distribution_id" {
112
+ description = "CloudFront distribution ID for the docs site"
113
+ value = module.docs_site.distribution_id
114
+ }
115
+
116
+ output "docs_distribution_domain" {
117
+ description = "CloudFront domain for the docs site"
118
+ value = module.docs_site.distribution_domain
119
+ }
120
+
121
+ output "docs_bucket_name" {
122
+ description = "S3 bucket for docs site assets"
123
+ value = module.docs_site.bucket_name
124
+ }
@@ -152,7 +152,7 @@ variable "memory_engine" {
152
152
  variable "hindsight_image_tag" {
153
153
  description = "Hindsight Docker image tag (only used when memory_engine = 'hindsight')"
154
154
  type = string
155
- default = "0.4.22"
155
+ default = "0.5.0"
156
156
  }
157
157
 
158
158
  variable "agentcore_memory_id" {
@@ -239,3 +239,19 @@ variable "pre_signup_lambda_zip" {
239
239
  type = string
240
240
  default = ""
241
241
  }
242
+
243
+ # ---------------------------------------------------------------------------
244
+ # Docs site (custom domain — optional)
245
+ # ---------------------------------------------------------------------------
246
+
247
+ variable "docs_domain" {
248
+ description = "Custom domain for the docs site (e.g. docs.thinkwork.ai). Leave empty for CloudFront default."
249
+ type = string
250
+ default = ""
251
+ }
252
+
253
+ variable "docs_certificate_arn" {
254
+ description = "ACM certificate ARN for the docs domain (us-east-1, required for CloudFront custom domains)"
255
+ type = string
256
+ default = ""
257
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkwork-cli",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Thinkwork CLI — deploy, manage, and interact with your Thinkwork stack",
5
5
  "license": "MIT",
6
6
  "type": "module",