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 +21 -0
- package/dist/cli.js +256 -14
- package/dist/terraform/examples/greenfield/main.tf +53 -3
- package/dist/terraform/modules/thinkwork/main.tf +32 -2
- package/dist/terraform/modules/thinkwork/outputs.tf +37 -0
- package/dist/terraform/modules/thinkwork/variables.tf +17 -1
- package/package.json +1 -1
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:
|
|
742
|
-
const secretJson =
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
1419
|
-
|
|
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(
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
62
|
-
|
|
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.
|
|
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
|
+
}
|