thinkwork-cli 0.4.1 → 0.5.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
@@ -1486,12 +1486,13 @@ function getApiEndpoint(stage, region) {
1486
1486
  return null;
1487
1487
  }
1488
1488
  }
1489
- async function apiFetch(apiUrl, authSecret, path2, options = {}) {
1489
+ async function apiFetch(apiUrl, authSecret, path2, options = {}, extraHeaders = {}) {
1490
1490
  const res = await fetch(`${apiUrl}${path2}`, {
1491
1491
  ...options,
1492
1492
  headers: {
1493
1493
  "Content-Type": "application/json",
1494
1494
  Authorization: `Bearer ${authSecret}`,
1495
+ ...extraHeaders,
1495
1496
  ...options.headers
1496
1497
  }
1497
1498
  });
@@ -1517,8 +1518,8 @@ function resolveApiConfig(stage) {
1517
1518
  return { apiUrl, authSecret };
1518
1519
  }
1519
1520
  function registerMcpCommand(program2) {
1520
- const mcp = program2.command("mcp").description("Manage MCP servers for agents");
1521
- 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) => {
1521
+ const mcp = program2.command("mcp").description("Manage MCP servers for your tenant");
1522
+ mcp.command("list").description("List registered MCP servers").requiredOption("-s, --stage <name>", "Deployment stage").requiredOption("--tenant <slug>", "Tenant slug").action(async (opts) => {
1522
1523
  const check = validateStage(opts.stage);
1523
1524
  if (!check.valid) {
1524
1525
  printError(check.error);
@@ -1528,20 +1529,21 @@ function registerMcpCommand(program2) {
1528
1529
  if (!api) process.exit(1);
1529
1530
  printHeader("mcp list", opts.stage);
1530
1531
  try {
1531
- const { servers } = await apiFetch(api.apiUrl, api.authSecret, `/api/skills/agent/${opts.agent}/mcp-servers`);
1532
+ const { servers } = await apiFetch(api.apiUrl, api.authSecret, "/api/skills/mcp-servers", {}, { "x-tenant-slug": opts.tenant });
1532
1533
  if (!servers || servers.length === 0) {
1533
- console.log(chalk7.dim(" No MCP servers registered for this agent."));
1534
+ console.log(chalk7.dim(" No MCP servers registered."));
1534
1535
  return;
1535
1536
  }
1536
1537
  console.log("");
1537
1538
  for (const s of servers) {
1538
1539
  const status = s.enabled ? chalk7.green("enabled") : chalk7.dim("disabled");
1539
- console.log(` ${chalk7.bold(s.name)} ${status}`);
1540
+ const authLabel = s.authType === "per_user_oauth" ? `OAuth (${s.oauthProvider})` : s.authType === "tenant_api_key" ? "API Key" : "none";
1541
+ console.log(` ${chalk7.bold(s.name)} ${chalk7.dim(s.slug)} ${status}`);
1540
1542
  console.log(` URL: ${s.url}`);
1541
1543
  console.log(` Transport: ${s.transport}`);
1542
- console.log(` Auth: ${s.authType || "none"}`);
1544
+ console.log(` Auth: ${authLabel}`);
1543
1545
  if (s.tools?.length) {
1544
- console.log(` Tools: ${s.tools.join(", ")}`);
1546
+ console.log(` Tools: ${s.tools.length} cached`);
1545
1547
  }
1546
1548
  console.log("");
1547
1549
  }
@@ -1550,7 +1552,7 @@ function registerMcpCommand(program2) {
1550
1552
  process.exit(1);
1551
1553
  }
1552
1554
  });
1553
- 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) => {
1555
+ 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) => {
1554
1556
  const check = validateStage(opts.stage);
1555
1557
  if (!check.valid) {
1556
1558
  printError(check.error);
@@ -1558,29 +1560,26 @@ function registerMcpCommand(program2) {
1558
1560
  }
1559
1561
  const api = resolveApiConfig(opts.stage);
1560
1562
  if (!api) process.exit(1);
1561
- printHeader("mcp add", opts.stage);
1562
1563
  const body = {
1563
1564
  name,
1564
1565
  url: opts.url,
1565
- transport: opts.transport,
1566
- authType: opts.authType !== "none" ? opts.authType : void 0
1566
+ transport: opts.transport
1567
1567
  };
1568
- if (opts.authValue) body.apiKey = opts.authValue;
1569
- if (opts.connectionId) body.connectionId = opts.connectionId;
1570
- if (opts.providerId) body.providerId = opts.providerId;
1571
- if (opts.tools) body.tools = opts.tools.split(",").map((t) => t.trim());
1568
+ if (opts.authType !== "none") body.authType = opts.authType;
1569
+ if (opts.apiKey) body.apiKey = opts.apiKey;
1570
+ if (opts.oauthProvider) body.oauthProvider = opts.oauthProvider;
1572
1571
  try {
1573
- const result = await apiFetch(api.apiUrl, api.authSecret, `/api/skills/agent/${opts.agent}/mcp-servers`, {
1572
+ const result = await apiFetch(api.apiUrl, api.authSecret, "/api/skills/mcp-servers", {
1574
1573
  method: "POST",
1575
1574
  body: JSON.stringify(body)
1576
- });
1577
- printSuccess(`MCP server "${name}" ${result.created ? "added" : "updated"} (skill: ${result.skillId})`);
1575
+ }, { "x-tenant-slug": opts.tenant });
1576
+ printSuccess(`MCP server "${name}" ${result.created ? "registered" : "updated"} (slug: ${result.slug})`);
1578
1577
  } catch (err) {
1579
1578
  printError(err.message);
1580
1579
  process.exit(1);
1581
1580
  }
1582
1581
  });
1583
- 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) => {
1582
+ mcp.command("remove <id>").description("Remove an MCP server").requiredOption("-s, --stage <name>", "Deployment stage").requiredOption("--tenant <slug>", "Tenant slug").action(async (id, opts) => {
1584
1583
  const check = validateStage(opts.stage);
1585
1584
  if (!check.valid) {
1586
1585
  printError(check.error);
@@ -1589,16 +1588,16 @@ function registerMcpCommand(program2) {
1589
1588
  const api = resolveApiConfig(opts.stage);
1590
1589
  if (!api) process.exit(1);
1591
1590
  try {
1592
- await apiFetch(api.apiUrl, api.authSecret, `/api/skills/agent/${opts.agent}/mcp-servers/${name}`, {
1591
+ await apiFetch(api.apiUrl, api.authSecret, `/api/skills/mcp-servers/${id}`, {
1593
1592
  method: "DELETE"
1594
- });
1595
- printSuccess(`MCP server "${name}" removed.`);
1593
+ }, { "x-tenant-slug": opts.tenant });
1594
+ printSuccess(`MCP server removed.`);
1596
1595
  } catch (err) {
1597
1596
  printError(err.message);
1598
1597
  process.exit(1);
1599
1598
  }
1600
1599
  });
1601
- 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) => {
1600
+ 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) => {
1602
1601
  const check = validateStage(opts.stage);
1603
1602
  if (!check.valid) {
1604
1603
  printError(check.error);
@@ -1608,17 +1607,17 @@ function registerMcpCommand(program2) {
1608
1607
  if (!api) process.exit(1);
1609
1608
  printHeader("mcp test", opts.stage);
1610
1609
  try {
1611
- const result = await apiFetch(api.apiUrl, api.authSecret, `/api/skills/agent/${opts.agent}/mcp-servers/${name}/test`, {
1610
+ const result = await apiFetch(api.apiUrl, api.authSecret, `/api/skills/mcp-servers/${id}/test`, {
1612
1611
  method: "POST"
1613
- });
1612
+ }, { "x-tenant-slug": opts.tenant });
1614
1613
  if (result.ok) {
1615
- printSuccess(`Connection to "${name}" successful.`);
1614
+ printSuccess("Connection successful.");
1616
1615
  if (result.tools?.length) {
1617
1616
  console.log(chalk7.bold(`
1618
- Available tools (${result.tools.length}):
1617
+ Discovered tools (${result.tools.length}):
1619
1618
  `));
1620
1619
  for (const t of result.tools) {
1621
- console.log(` ${chalk7.cyan(t.name)}${t.description ? chalk7.dim(` \u2014 ${t.description}`) : ""}`);
1620
+ console.log(` ${chalk7.cyan(t.name)}${t.description ? chalk7.dim(` - ${t.description}`) : ""}`);
1622
1621
  }
1623
1622
  console.log("");
1624
1623
  } else {
@@ -1633,6 +1632,43 @@ function registerMcpCommand(program2) {
1633
1632
  process.exit(1);
1634
1633
  }
1635
1634
  });
1635
+ 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) => {
1636
+ const check = validateStage(opts.stage);
1637
+ if (!check.valid) {
1638
+ printError(check.error);
1639
+ process.exit(1);
1640
+ }
1641
+ const api = resolveApiConfig(opts.stage);
1642
+ if (!api) process.exit(1);
1643
+ try {
1644
+ const result = await apiFetch(api.apiUrl, api.authSecret, `/api/skills/agents/${opts.agent}/mcp-servers`, {
1645
+ method: "POST",
1646
+ body: JSON.stringify({ mcpServerId })
1647
+ });
1648
+ printSuccess(`MCP server assigned to agent. (${result.created ? "new" : "updated"})`);
1649
+ } catch (err) {
1650
+ printError(err.message);
1651
+ process.exit(1);
1652
+ }
1653
+ });
1654
+ 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) => {
1655
+ const check = validateStage(opts.stage);
1656
+ if (!check.valid) {
1657
+ printError(check.error);
1658
+ process.exit(1);
1659
+ }
1660
+ const api = resolveApiConfig(opts.stage);
1661
+ if (!api) process.exit(1);
1662
+ try {
1663
+ await apiFetch(api.apiUrl, api.authSecret, `/api/skills/agents/${opts.agent}/mcp-servers/${mcpServerId}`, {
1664
+ method: "DELETE"
1665
+ });
1666
+ printSuccess("MCP server unassigned from agent.");
1667
+ } catch (err) {
1668
+ printError(err.message);
1669
+ process.exit(1);
1670
+ }
1671
+ });
1636
1672
  }
1637
1673
 
1638
1674
  // src/cli.ts
@@ -92,7 +92,7 @@ resource "aws_iam_role" "agentcore" {
92
92
  Version = "2012-10-17"
93
93
  Statement = [{
94
94
  Effect = "Allow"
95
- Principal = { Service = ["ecs-tasks.amazonaws.com", "lambda.amazonaws.com"] }
95
+ Principal = { Service = ["ecs-tasks.amazonaws.com", "lambda.amazonaws.com", "bedrock-agentcore.amazonaws.com"] }
96
96
  Action = "sts:AssumeRole"
97
97
  }]
98
98
  })
@@ -118,18 +118,24 @@ resource "aws_iam_role_policy" "agentcore" {
118
118
  Sid = "BedrockInvoke"
119
119
  Effect = "Allow"
120
120
  Action = ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream", "bedrock:InvokeAgent"]
121
- Resource = "*"
121
+ Resource = "arn:aws:bedrock:${var.region}::foundation-model/*"
122
122
  },
123
123
  {
124
124
  Sid = "CloudWatchLogs"
125
125
  Effect = "Allow"
126
126
  Action = ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"]
127
- Resource = "arn:aws:logs:${var.region}:${var.account_id}:*"
127
+ Resource = "arn:aws:logs:${var.region}:${var.account_id}:log-group:/aws/lambda/thinkwork-${var.stage}-*"
128
128
  },
129
129
  {
130
130
  Sid = "ECRPull"
131
131
  Effect = "Allow"
132
- Action = ["ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage", "ecr:GetAuthorizationToken"]
132
+ Action = ["ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage"]
133
+ Resource = "arn:aws:ecr:${var.region}:${var.account_id}:repository/thinkwork-${var.stage}-*"
134
+ },
135
+ {
136
+ Sid = "ECRAuth"
137
+ Effect = "Allow"
138
+ Action = ["ecr:GetAuthorizationToken"]
133
139
  Resource = "*"
134
140
  },
135
141
  {
@@ -27,9 +27,9 @@ resource "aws_apigatewayv2_api" "main" {
27
27
  protocol_type = "HTTP"
28
28
 
29
29
  cors_configuration {
30
- allow_headers = ["*"]
31
- allow_methods = ["*"]
32
- allow_origins = ["*"]
30
+ allow_headers = ["Content-Type", "Authorization", "x-api-key", "x-tenant-id", "x-tenant-slug", "x-principal-id"]
31
+ allow_methods = ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
32
+ allow_origins = var.cors_allowed_origins
33
33
  max_age = 3600
34
34
  }
35
35
 
@@ -118,11 +118,22 @@ resource "aws_iam_role_policy" "lambda_secrets" {
118
118
 
119
119
  policy = jsonencode({
120
120
  Version = "2012-10-17"
121
- Statement = [{
122
- Effect = "Allow"
123
- Action = ["secretsmanager:GetSecretValue"]
124
- Resource = var.graphql_db_secret_arn
125
- }]
121
+ Statement = [
122
+ {
123
+ Effect = "Allow"
124
+ Action = ["secretsmanager:GetSecretValue"]
125
+ Resource = var.graphql_db_secret_arn
126
+ },
127
+ {
128
+ Effect = "Allow"
129
+ Action = [
130
+ "secretsmanager:CreateSecret",
131
+ "secretsmanager:UpdateSecret",
132
+ "secretsmanager:GetSecretValue"
133
+ ]
134
+ Resource = "arn:aws:secretsmanager:${var.region}:${var.account_id}:secret:thinkwork/*"
135
+ }
136
+ ]
126
137
  })
127
138
  }
128
139
 
@@ -175,7 +186,7 @@ resource "aws_iam_role_policy" "lambda_bedrock" {
175
186
  Statement = [{
176
187
  Effect = "Allow"
177
188
  Action = ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"]
178
- Resource = "*"
189
+ Resource = "arn:aws:bedrock:${var.region}::foundation-model/*"
179
190
  }]
180
191
  })
181
192
  }
@@ -151,3 +151,9 @@ variable "agentcore_function_name" {
151
151
  type = string
152
152
  default = ""
153
153
  }
154
+
155
+ variable "cors_allowed_origins" {
156
+ description = "Allowed CORS origins for the API Gateway. Use [\"*\"] for development."
157
+ type = list(string)
158
+ default = ["*"]
159
+ }
@@ -69,6 +69,31 @@ resource "aws_cloudfront_origin_access_control" "site" {
69
69
  signing_protocol = "sigv4"
70
70
  }
71
71
 
72
+ ################################################################################
73
+ # CloudFront Function — rewrite directory URIs to index.html
74
+ #
75
+ # S3 with OAC doesn't auto-serve index.html for subdirectory requests.
76
+ # /getting-started/ → /getting-started/index.html
77
+ ################################################################################
78
+
79
+ resource "aws_cloudfront_function" "rewrite" {
80
+ name = "thinkwork-${var.stage}-${var.site_name}-rewrite"
81
+ runtime = "cloudfront-js-2.0"
82
+ publish = true
83
+ code = <<-EOF
84
+ function handler(event) {
85
+ var request = event.request;
86
+ var uri = request.uri;
87
+ if (uri.endsWith('/')) {
88
+ request.uri += 'index.html';
89
+ } else if (!uri.includes('.')) {
90
+ request.uri += '/index.html';
91
+ }
92
+ return request;
93
+ }
94
+ EOF
95
+ }
96
+
72
97
  ################################################################################
73
98
  # CloudFront Distribution
74
99
  ################################################################################
@@ -92,6 +117,11 @@ resource "aws_cloudfront_distribution" "site" {
92
117
  cached_methods = ["GET", "HEAD"]
93
118
  compress = true
94
119
 
120
+ function_association {
121
+ event_type = "viewer-request"
122
+ function_arn = aws_cloudfront_function.rewrite.arn
123
+ }
124
+
95
125
  forwarded_values {
96
126
  query_string = false
97
127
  cookies {
@@ -100,16 +130,17 @@ resource "aws_cloudfront_distribution" "site" {
100
130
  }
101
131
  }
102
132
 
133
+ # Fallback for true 404s (e.g. deleted pages) — serve the 404 page
103
134
  custom_error_response {
104
135
  error_code = 404
105
- response_code = 200
106
- response_page_path = "/index.html"
136
+ response_code = 404
137
+ response_page_path = "/404.html"
107
138
  }
108
139
 
109
140
  custom_error_response {
110
141
  error_code = 403
111
- response_code = 200
112
- response_page_path = "/index.html"
142
+ response_code = 404
143
+ response_page_path = "/404.html"
113
144
  }
114
145
 
115
146
  restrictions {
@@ -20,6 +20,12 @@ variable "bucket_name" {
20
20
  type = string
21
21
  }
22
22
 
23
+ variable "cors_allowed_origins" {
24
+ description = "Allowed CORS origins. Use [\"*\"] for development."
25
+ type = list(string)
26
+ default = ["*"]
27
+ }
28
+
23
29
  resource "aws_s3_bucket" "main" {
24
30
  bucket = var.bucket_name
25
31
 
@@ -33,9 +39,9 @@ resource "aws_s3_bucket_cors_configuration" "main" {
33
39
  bucket = aws_s3_bucket.main.id
34
40
 
35
41
  cors_rule {
36
- allowed_headers = ["*"]
37
- allowed_methods = ["GET", "PUT", "POST", "HEAD"]
38
- allowed_origins = ["*"]
42
+ allowed_headers = ["Content-Type", "Authorization", "x-amz-*"]
43
+ allowed_methods = ["GET", "PUT", "HEAD"]
44
+ allowed_origins = var.cors_allowed_origins
39
45
  expose_headers = ["ETag"]
40
46
  max_age_seconds = 3000
41
47
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkwork-cli",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Thinkwork CLI — deploy, manage, and interact with your Thinkwork stack",
5
5
  "license": "MIT",
6
6
  "type": "module",