thinkwork-cli 0.4.0 → 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
@@ -1372,6 +1372,36 @@ function discoverAwsStages(region) {
1372
1372
  }
1373
1373
  return stages;
1374
1374
  }
1375
+ function printStageDetail(info) {
1376
+ console.log(chalk6.bold.cyan(` \u2B21 ${info.stage}`));
1377
+ 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"));
1378
+ console.log(` ${chalk6.bold("Source:")} ${info.source === "both" ? "AWS + local config" : info.source === "aws" ? "AWS (no local config)" : "local only (not in AWS)"}`);
1379
+ console.log(` ${chalk6.bold("Region:")} ${info.region}`);
1380
+ console.log(` ${chalk6.bold("Account:")} ${info.accountId}`);
1381
+ console.log(` ${chalk6.bold("Lambda fns:")} ${info.lambdaCount || "\u2014"}`);
1382
+ console.log(` ${chalk6.bold("AgentCore:")} ${info.agentcoreStatus || "unknown"}`);
1383
+ console.log(` ${chalk6.bold("Memory:")} ${info.memoryEngine || "unknown"}`);
1384
+ if (info.hindsightHealth) console.log(` ${chalk6.bold("Hindsight:")} ${info.hindsightHealth}`);
1385
+ if (info.bucketName) console.log(` ${chalk6.bold("S3 bucket:")} ${info.bucketName}`);
1386
+ if (info.dbEndpoint) console.log(` ${chalk6.bold("Database:")} ${info.dbEndpoint}`);
1387
+ if (info.ecrUrl) console.log(` ${chalk6.bold("ECR:")} ${info.ecrUrl}`);
1388
+ console.log("");
1389
+ console.log(chalk6.bold(" URLs:"));
1390
+ if (info.adminUrl) console.log(` Admin: ${link(info.adminUrl)}`);
1391
+ if (info.docsUrl) console.log(` Docs: ${link(info.docsUrl)}`);
1392
+ if (info.apiEndpoint) console.log(` API: ${link(info.apiEndpoint)}`);
1393
+ if (info.appsyncApiUrl) console.log(` AppSync: ${link(info.appsyncApiUrl)}`);
1394
+ if (info.appsyncUrl) console.log(` WebSocket: ${link(info.appsyncUrl)}`);
1395
+ if (info.hindsightEndpoint) console.log(` Hindsight: ${link(info.hindsightEndpoint)}`);
1396
+ 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"));
1397
+ const local = loadEnvironment(info.stage);
1398
+ if (local) {
1399
+ console.log(chalk6.dim(` Terraform dir: ${local.terraformDir}`));
1400
+ } else {
1401
+ console.log(chalk6.dim(` No local config. Run: thinkwork init -s ${info.stage}`));
1402
+ }
1403
+ console.log("");
1404
+ }
1375
1405
  function registerStatusCommand(program2) {
1376
1406
  program2.command("status").description("Show all Thinkwork environments (AWS + local)").option("-s, --stage <name>", "Show details for a specific stage").option("--region <region>", "AWS region to scan", "us-east-1").action(async (opts) => {
1377
1407
  const identity = getAwsIdentity();
@@ -1410,34 +1440,7 @@ function registerStatusCommand(program2) {
1410
1440
  printError(`No environment "${opts.stage}" found in AWS or local config.`);
1411
1441
  process.exit(1);
1412
1442
  }
1413
- console.log(chalk6.bold.cyan(` \u2B21 ${info.stage}`));
1414
- 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"));
1415
- console.log(` ${chalk6.bold("Source:")} ${info.source === "both" ? "AWS + local config" : info.source === "aws" ? "AWS (no local config)" : "local only (not in AWS)"}`);
1416
- console.log(` ${chalk6.bold("Region:")} ${info.region}`);
1417
- console.log(` ${chalk6.bold("Account:")} ${info.accountId}`);
1418
- console.log(` ${chalk6.bold("Lambda fns:")} ${info.lambdaCount || "\u2014"}`);
1419
- console.log(` ${chalk6.bold("AgentCore:")} ${info.agentcoreStatus || "unknown"}`);
1420
- console.log(` ${chalk6.bold("Memory:")} ${info.memoryEngine || "unknown"}`);
1421
- if (info.hindsightHealth) console.log(` ${chalk6.bold("Hindsight:")} ${info.hindsightHealth}`);
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)}`);
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
- const local = loadEnvironment(opts.stage);
1435
- if (local) {
1436
- console.log(chalk6.dim(` Terraform dir: ${local.terraformDir}`));
1437
- } else {
1438
- console.log(chalk6.dim(` No local config. Run: thinkwork init -s ${opts.stage}`));
1439
- }
1440
- console.log("");
1443
+ printStageDetail(info);
1441
1444
  return;
1442
1445
  }
1443
1446
  if (merged.size === 0) {
@@ -1446,45 +1449,9 @@ function registerStatusCommand(program2) {
1446
1449
  console.log("");
1447
1450
  return;
1448
1451
  }
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);
1455
- console.log(chalk6.bold(" Environments"));
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"));
1457
- console.log(
1458
- chalk6.dim(" ") + "Stage".padEnd(COL1) + "Source".padEnd(COL2) + "Lambdas".padEnd(COL3) + "AgentCore".padEnd(COL4) + "Memory".padEnd(COL5) + "URLs"
1459
- );
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"));
1461
1452
  for (const [, info] of [...merged].sort((a, b) => a[0].localeCompare(b[0]))) {
1462
- const sourceBadge = info.source === "both" ? chalk6.green("\u25CF") : info.source === "aws" ? chalk6.yellow("\u25CF") : chalk6.dim("\u25CB");
1463
- const acStatus = info.agentcoreStatus === "Active" ? chalk6.green("active") : info.agentcoreStatus === "not deployed" ? chalk6.dim("\u2014") : chalk6.yellow(info.agentcoreStatus || "\u2014");
1464
- const memBadge = info.memoryEngine === "hindsight" ? info.hindsightHealth === "healthy" ? chalk6.magenta("hindsight \u2713") : chalk6.yellow("hindsight ?") : chalk6.dim(info.memoryEngine || "\u2014");
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
- }
1453
+ printStageDetail(info);
1480
1454
  }
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
- );
1485
- console.log("");
1486
- console.log(` Details: ${chalk6.cyan("thinkwork status -s <stage>")}`);
1487
- console.log("");
1488
1455
  });
1489
1456
  }
1490
1457
 
@@ -1519,12 +1486,13 @@ function getApiEndpoint(stage, region) {
1519
1486
  return null;
1520
1487
  }
1521
1488
  }
1522
- async function apiFetch(apiUrl, authSecret, path2, options = {}) {
1489
+ async function apiFetch(apiUrl, authSecret, path2, options = {}, extraHeaders = {}) {
1523
1490
  const res = await fetch(`${apiUrl}${path2}`, {
1524
1491
  ...options,
1525
1492
  headers: {
1526
1493
  "Content-Type": "application/json",
1527
1494
  Authorization: `Bearer ${authSecret}`,
1495
+ ...extraHeaders,
1528
1496
  ...options.headers
1529
1497
  }
1530
1498
  });
@@ -1550,8 +1518,8 @@ function resolveApiConfig(stage) {
1550
1518
  return { apiUrl, authSecret };
1551
1519
  }
1552
1520
  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) => {
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) => {
1555
1523
  const check = validateStage(opts.stage);
1556
1524
  if (!check.valid) {
1557
1525
  printError(check.error);
@@ -1561,20 +1529,21 @@ function registerMcpCommand(program2) {
1561
1529
  if (!api) process.exit(1);
1562
1530
  printHeader("mcp list", opts.stage);
1563
1531
  try {
1564
- 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 });
1565
1533
  if (!servers || servers.length === 0) {
1566
- console.log(chalk7.dim(" No MCP servers registered for this agent."));
1534
+ console.log(chalk7.dim(" No MCP servers registered."));
1567
1535
  return;
1568
1536
  }
1569
1537
  console.log("");
1570
1538
  for (const s of servers) {
1571
1539
  const status = s.enabled ? chalk7.green("enabled") : chalk7.dim("disabled");
1572
- 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}`);
1573
1542
  console.log(` URL: ${s.url}`);
1574
1543
  console.log(` Transport: ${s.transport}`);
1575
- console.log(` Auth: ${s.authType || "none"}`);
1544
+ console.log(` Auth: ${authLabel}`);
1576
1545
  if (s.tools?.length) {
1577
- console.log(` Tools: ${s.tools.join(", ")}`);
1546
+ console.log(` Tools: ${s.tools.length} cached`);
1578
1547
  }
1579
1548
  console.log("");
1580
1549
  }
@@ -1583,7 +1552,7 @@ function registerMcpCommand(program2) {
1583
1552
  process.exit(1);
1584
1553
  }
1585
1554
  });
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) => {
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) => {
1587
1556
  const check = validateStage(opts.stage);
1588
1557
  if (!check.valid) {
1589
1558
  printError(check.error);
@@ -1591,29 +1560,26 @@ function registerMcpCommand(program2) {
1591
1560
  }
1592
1561
  const api = resolveApiConfig(opts.stage);
1593
1562
  if (!api) process.exit(1);
1594
- printHeader("mcp add", opts.stage);
1595
1563
  const body = {
1596
1564
  name,
1597
1565
  url: opts.url,
1598
- transport: opts.transport,
1599
- authType: opts.authType !== "none" ? opts.authType : void 0
1566
+ transport: opts.transport
1600
1567
  };
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());
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;
1605
1571
  try {
1606
- 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", {
1607
1573
  method: "POST",
1608
1574
  body: JSON.stringify(body)
1609
- });
1610
- 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})`);
1611
1577
  } catch (err) {
1612
1578
  printError(err.message);
1613
1579
  process.exit(1);
1614
1580
  }
1615
1581
  });
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) => {
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) => {
1617
1583
  const check = validateStage(opts.stage);
1618
1584
  if (!check.valid) {
1619
1585
  printError(check.error);
@@ -1622,16 +1588,16 @@ function registerMcpCommand(program2) {
1622
1588
  const api = resolveApiConfig(opts.stage);
1623
1589
  if (!api) process.exit(1);
1624
1590
  try {
1625
- 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}`, {
1626
1592
  method: "DELETE"
1627
- });
1628
- printSuccess(`MCP server "${name}" removed.`);
1593
+ }, { "x-tenant-slug": opts.tenant });
1594
+ printSuccess(`MCP server removed.`);
1629
1595
  } catch (err) {
1630
1596
  printError(err.message);
1631
1597
  process.exit(1);
1632
1598
  }
1633
1599
  });
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) => {
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) => {
1635
1601
  const check = validateStage(opts.stage);
1636
1602
  if (!check.valid) {
1637
1603
  printError(check.error);
@@ -1641,17 +1607,17 @@ function registerMcpCommand(program2) {
1641
1607
  if (!api) process.exit(1);
1642
1608
  printHeader("mcp test", opts.stage);
1643
1609
  try {
1644
- 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`, {
1645
1611
  method: "POST"
1646
- });
1612
+ }, { "x-tenant-slug": opts.tenant });
1647
1613
  if (result.ok) {
1648
- printSuccess(`Connection to "${name}" successful.`);
1614
+ printSuccess("Connection successful.");
1649
1615
  if (result.tools?.length) {
1650
1616
  console.log(chalk7.bold(`
1651
- Available tools (${result.tools.length}):
1617
+ Discovered tools (${result.tools.length}):
1652
1618
  `));
1653
1619
  for (const t of result.tools) {
1654
- 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}`) : ""}`);
1655
1621
  }
1656
1622
  console.log("");
1657
1623
  } else {
@@ -1666,6 +1632,43 @@ function registerMcpCommand(program2) {
1666
1632
  process.exit(1);
1667
1633
  }
1668
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
+ });
1669
1672
  }
1670
1673
 
1671
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.0",
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",