thinkwork-cli 0.3.2 → 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/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();
@@ -1316,6 +1316,10 @@ function discoverAwsStages(region) {
1316
1316
  `appsync list-graphql-apis --region ${region} --query "graphqlApis[?name=='thinkwork-${stage}-subscriptions'].uris.REALTIME|[0]" --output text`
1317
1317
  );
1318
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;
1319
1323
  const acRaw = runAws(
1320
1324
  `lambda get-function --function-name thinkwork-${stage}-agentcore --region ${region} --query "Configuration.State" --output text 2>/dev/null`
1321
1325
  );
@@ -1344,6 +1348,27 @@ function discoverAwsStages(region) {
1344
1348
  } else {
1345
1349
  info.memoryEngine = "managed";
1346
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
+ }
1347
1372
  }
1348
1373
  return stages;
1349
1374
  }
@@ -1395,9 +1420,14 @@ function registerStatusCommand(program2) {
1395
1420
  console.log(` ${chalk6.bold("Memory:")} ${info.memoryEngine || "unknown"}`);
1396
1421
  if (info.hindsightHealth) console.log(` ${chalk6.bold("Hindsight:")} ${info.hindsightHealth}`);
1397
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}`);
1398
1425
  console.log("");
1399
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)}`);
1400
1429
  if (info.apiEndpoint) console.log(` API: ${link(info.apiEndpoint)}`);
1430
+ if (info.appsyncApiUrl) console.log(` AppSync: ${link(info.appsyncApiUrl)}`);
1401
1431
  if (info.appsyncUrl) console.log(` WebSocket: ${link(info.appsyncUrl)}`);
1402
1432
  if (info.hindsightEndpoint) console.log(` Hindsight: ${link(info.hindsightEndpoint)}`);
1403
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"));
@@ -1434,9 +1464,11 @@ function registerStatusCommand(program2) {
1434
1464
  const memBadge = info.memoryEngine === "hindsight" ? info.hindsightHealth === "healthy" ? chalk6.magenta("hindsight \u2713") : chalk6.yellow("hindsight ?") : chalk6.dim(info.memoryEngine || "\u2014");
1435
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);
1436
1466
  const urls = [];
1437
- if (info.apiEndpoint) urls.push(`API: ${link(info.apiEndpoint, info.apiEndpoint)}`);
1438
- if (info.appsyncUrl) urls.push(`WS: ${link(info.appsyncUrl, info.appsyncUrl.replace("wss://", "").split(".")[0] + "...")}`);
1439
- if (info.hindsightEndpoint) urls.push(`Mem: ${link(info.hindsightEndpoint, info.hindsightEndpoint)}`);
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)}`);
1440
1472
  if (urls.length === 0) {
1441
1473
  console.log(prefix + acStatus.padEnd(22) + memBadge.padEnd(22) + chalk6.dim("\u2014"));
1442
1474
  } else {
@@ -1456,6 +1488,186 @@ function registerStatusCommand(program2) {
1456
1488
  });
1457
1489
  }
1458
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
+
1459
1671
  // src/cli.ts
1460
1672
  var program = new Command();
1461
1673
  program.name("thinkwork").description(
@@ -1480,4 +1692,5 @@ registerDestroyCommand(program);
1480
1692
  registerStatusCommand(program);
1481
1693
  registerOutputsCommand(program);
1482
1694
  registerConfigCommand(program);
1695
+ registerMcpCommand(program);
1483
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
+ }
@@ -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.2",
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",