@sakupa/mcp 0.7.6 → 0.7.8
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/bin.js +273 -146
- package/dist/index.js +274 -145
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ var FREE_SITE_URL_SUFFIX = `.${SERVICE_DOMAIN}`;
|
|
|
5
5
|
var TEST_ACCESS_HEADER = "x-sakupa-test-token";
|
|
6
6
|
var FREE_SITE_TTL_HOURS = 24;
|
|
7
7
|
var FREE_SITE_MAX_TOTAL_BYTES = 10 * 1024 * 1024;
|
|
8
|
+
var FREE_ACTIVE_SITES_PER_IP = 3;
|
|
8
9
|
var PAID_SITE_MAX_TOTAL_BYTES = 2 * 1024 * 1024 * 1024;
|
|
9
10
|
var MAX_FILE_COUNT = 5e3;
|
|
10
11
|
var MAX_SINGLE_FILE_BYTES = 25 * 1024 * 1024;
|
|
@@ -123,7 +124,7 @@ var FORBIDDEN_PATH_SEGMENTS = [
|
|
|
123
124
|
var ALLOWED_HIDDEN_PATHS = [".well-known/"];
|
|
124
125
|
|
|
125
126
|
// ../core/dist/domain/version.js
|
|
126
|
-
var SAKUPA_MCP_VERSION = "0.7.
|
|
127
|
+
var SAKUPA_MCP_VERSION = "0.7.8";
|
|
127
128
|
|
|
128
129
|
// ../core/dist/domain/errors.js
|
|
129
130
|
var HTTP_STATUS = {
|
|
@@ -402,22 +403,13 @@ function validateDeployableFiles(files, opts) {
|
|
|
402
403
|
severity: "warning",
|
|
403
404
|
code: "missing_html_lang",
|
|
404
405
|
path: entryHtmlPath,
|
|
405
|
-
message: 'The entry HTML has no lang attribute.
|
|
406
|
+
message: 'The entry HTML has no lang attribute. Before deploying, add one matching the content language (html lang="en" | "ja" | "zh-CN") \u2014 it drives screen-reader pronunciation and search-engine language detection, and Sakupa surfaces follow it.'
|
|
406
407
|
});
|
|
407
408
|
}
|
|
408
409
|
}
|
|
409
410
|
}
|
|
410
411
|
const jsCount = files.filter((f) => ["js", "mjs"].includes(fileExtension(f.path))).length;
|
|
411
412
|
const looksLikeSpa = htmlPaths.length === 1 && entryHtmlPath === "index.html" && jsCount > 0;
|
|
412
|
-
const wantsSpa = opts.spaFallbackRequested === true || looksLikeSpa;
|
|
413
|
-
const spaFallbackConfirmationRequired = wantsSpa && opts.spaFallbackConfirmed !== true;
|
|
414
|
-
if (opts.spaFallbackRequested === true && opts.spaFallbackConfirmed !== true) {
|
|
415
|
-
issues.push({
|
|
416
|
-
severity: "error",
|
|
417
|
-
code: "spa_fallback_confirmation_required",
|
|
418
|
-
message: "SPA fallback rewrites unknown paths to index.html and changes normal 404 behavior. It must be explicitly confirmed."
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
413
|
const ok = issues.every((i) => i.severity !== "error");
|
|
422
414
|
return {
|
|
423
415
|
ok,
|
|
@@ -427,8 +419,7 @@ function validateDeployableFiles(files, opts) {
|
|
|
427
419
|
entryHtmlPath,
|
|
428
420
|
htmlLang,
|
|
429
421
|
supportedLang,
|
|
430
|
-
looksLikeSpa
|
|
431
|
-
spaFallbackConfirmationRequired
|
|
422
|
+
looksLikeSpa
|
|
432
423
|
};
|
|
433
424
|
}
|
|
434
425
|
function safeDecode(bytes) {
|
|
@@ -1215,7 +1206,7 @@ async function analyzeProject(projectDir, opts = {}) {
|
|
|
1215
1206
|
fileCount: 0,
|
|
1216
1207
|
issues: [],
|
|
1217
1208
|
ssrRisks,
|
|
1218
|
-
spa: { looksLikeSpa: false,
|
|
1209
|
+
spa: { looksLikeSpa: false, autoFallback: false },
|
|
1219
1210
|
deployable: false,
|
|
1220
1211
|
suggestedNextAction: suggestedNextAction2
|
|
1221
1212
|
};
|
|
@@ -1235,30 +1226,23 @@ async function analyzeProject(projectDir, opts = {}) {
|
|
|
1235
1226
|
}
|
|
1236
1227
|
candidates.push({ path: file.path, size: file.size, ...content ? { content } : {} });
|
|
1237
1228
|
}
|
|
1238
|
-
const validation = validateDeployableFiles(candidates, {
|
|
1239
|
-
mode: "free",
|
|
1240
|
-
...opts.spaFallbackRequested !== void 0 ? { spaFallbackRequested: opts.spaFallbackRequested } : {},
|
|
1241
|
-
...opts.spaFallbackConfirmed !== void 0 ? { spaFallbackConfirmed: opts.spaFallbackConfirmed } : {}
|
|
1242
|
-
});
|
|
1229
|
+
const validation = validateDeployableFiles(candidates, { mode: "free" });
|
|
1243
1230
|
ssrRisks.push(...serverAndDbDepRisks(pkg, true));
|
|
1244
1231
|
const deployable = validation.ok && walked.length > 0;
|
|
1245
1232
|
const spa = {
|
|
1246
1233
|
looksLikeSpa: validation.looksLikeSpa,
|
|
1247
|
-
|
|
1248
|
-
confirmationRequired: validation.spaFallbackConfirmationRequired
|
|
1234
|
+
autoFallback: validation.looksLikeSpa
|
|
1249
1235
|
};
|
|
1250
1236
|
let suggestedNextAction;
|
|
1251
1237
|
if (!deployable) {
|
|
1252
1238
|
const firstError = validation.issues.find((i) => i.severity === "error");
|
|
1253
1239
|
if (firstError?.code === "missing_index_html") {
|
|
1254
1240
|
suggestedNextAction = `No index.html at the root of "${outputDirRel}". Deploy the built static output (the directory whose root contains index.html), not the source project. Build locally first if needed (${buildCommandHint ?? "npm run build"}), then re-run analyze_site.`;
|
|
1255
|
-
} else if (firstError?.code === "spa_fallback_confirmation_required") {
|
|
1256
|
-
suggestedNextAction = "SPA fallback rewrites unknown paths to index.html and changes normal 404 behavior. Confirm it explicitly: re-run with spaFallbackRequested: true and spaFallbackConfirmed: true.";
|
|
1257
1241
|
} else {
|
|
1258
1242
|
suggestedNextAction = "Fix the listed issues (remove forbidden/secret files, reduce size, add missing entry HTML), then re-run analyze_site.";
|
|
1259
1243
|
}
|
|
1260
|
-
} else if (spa.
|
|
1261
|
-
suggestedNextAction = `
|
|
1244
|
+
} else if (spa.looksLikeSpa) {
|
|
1245
|
+
suggestedNextAction = `Run deploy_site to publish the static output in "${outputDirRel}". It looks like a single-page app, so SPA fallback (unknown paths rewrite to index.html) will be enabled automatically; pass spaFallback: false to opt out.`;
|
|
1262
1246
|
} else {
|
|
1263
1247
|
suggestedNextAction = `Run deploy_site to publish the static output in "${outputDirRel}".`;
|
|
1264
1248
|
}
|
|
@@ -1282,6 +1266,12 @@ async function analyzeProject(projectDir, opts = {}) {
|
|
|
1282
1266
|
};
|
|
1283
1267
|
}
|
|
1284
1268
|
|
|
1269
|
+
// src/tools/context.ts
|
|
1270
|
+
import { z as z2 } from "zod";
|
|
1271
|
+
import { statSync } from "node:fs";
|
|
1272
|
+
import { homedir } from "node:os";
|
|
1273
|
+
import { isAbsolute, parse, resolve as resolve2 } from "node:path";
|
|
1274
|
+
|
|
1285
1275
|
// src/tools/result.ts
|
|
1286
1276
|
import { z } from "zod";
|
|
1287
1277
|
var STRUCTURED_TOOL_OUTPUT_SCHEMA = {
|
|
@@ -1324,22 +1314,64 @@ function structuredToolResult(envelope) {
|
|
|
1324
1314
|
}
|
|
1325
1315
|
|
|
1326
1316
|
// src/tools/context.ts
|
|
1317
|
+
var LocalGuidanceError = class extends SakupaError {
|
|
1318
|
+
constructor(code, message) {
|
|
1319
|
+
super(code, message);
|
|
1320
|
+
}
|
|
1321
|
+
};
|
|
1322
|
+
var projectDirInput = z2.string().optional().describe(
|
|
1323
|
+
"Absolute path of the project directory the user is CURRENTLY working in (the folder holding the site files and .sakupa). Always pass it explicitly; when omitted the server falls back to its startup directory, which may not be where the user is working now."
|
|
1324
|
+
);
|
|
1325
|
+
function withProjectDir(ctx, projectDirArg) {
|
|
1326
|
+
if (projectDirArg === void 0) return ctx;
|
|
1327
|
+
if (!isAbsolute(projectDirArg)) {
|
|
1328
|
+
throw new LocalGuidanceError(
|
|
1329
|
+
"invalid_request",
|
|
1330
|
+
`projectDir must be an ABSOLUTE path (got "${projectDirArg}"). Pass the full path of the directory the user is currently working in.`
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
const dir = resolve2(projectDirArg);
|
|
1334
|
+
if (parse(dir).root === dir || dir === homedir()) {
|
|
1335
|
+
throw new LocalGuidanceError(
|
|
1336
|
+
"invalid_request",
|
|
1337
|
+
`projectDir "${dir}" is a filesystem root or the home directory. Pass the specific project folder that holds the site's files, not a top-level directory.`
|
|
1338
|
+
);
|
|
1339
|
+
}
|
|
1340
|
+
const stat = statSync(dir, { throwIfNoEntry: false });
|
|
1341
|
+
if (!stat?.isDirectory()) {
|
|
1342
|
+
throw new LocalGuidanceError(
|
|
1343
|
+
"invalid_request",
|
|
1344
|
+
`projectDir "${dir}" does not exist or is not a directory. Pass the absolute path of the directory the user is currently working in.`
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
return { ...ctx, projectDir: dir };
|
|
1348
|
+
}
|
|
1327
1349
|
function requireSiteFile(ctx) {
|
|
1328
1350
|
const state = loadSiteFile(ctx.projectDir);
|
|
1329
1351
|
if (state.kind === "corrupted") {
|
|
1330
|
-
throw new
|
|
1352
|
+
throw new LocalGuidanceError(
|
|
1331
1353
|
"invalid_request",
|
|
1332
1354
|
`.sakupa/site.json in ${ctx.projectDir} is damaged: ${state.problem}. ` + siteFileRecoveryGuidance(ctx.projectDir)
|
|
1333
1355
|
);
|
|
1334
1356
|
}
|
|
1335
1357
|
if (state.kind === "absent") {
|
|
1336
|
-
throw new
|
|
1358
|
+
throw new LocalGuidanceError(
|
|
1337
1359
|
"not_found",
|
|
1338
1360
|
`No .sakupa/site.json found in ${ctx.projectDir}. This project has no Sakupa site binding yet \u2014 run deploy_site first to publish it (the management credential will be stored locally in .sakupa/site.json). If this was a paid custom-domain site whose project file was lost, use recover_domain_site instead.`
|
|
1339
1361
|
);
|
|
1340
1362
|
}
|
|
1341
1363
|
return state.file;
|
|
1342
1364
|
}
|
|
1365
|
+
var STATIC_SUMMARY = {
|
|
1366
|
+
not_found: "The required local project binding or resource is unavailable; if this project has no .sakupa/site.json yet, run deploy_site first.",
|
|
1367
|
+
unauthorized: "The server rejected the site credential: the one in .sakupa/site.json no longer matches the server-side verifier. The site itself is intact on the server \u2014 only the local binding file is the problem. Repair the file (restore a backup or undo the local edit). Do NOT delete the .sakupa directory to work around this: the credential is unrecoverable by design, so abandoning it permanently orphans the existing site.",
|
|
1368
|
+
invalid_request: "The request arguments or local project checks did not pass.",
|
|
1369
|
+
validation_failed: "The request arguments or local project checks did not pass.",
|
|
1370
|
+
state_conflict: "The resource state has changed; re-query the current status before deciding the next step.",
|
|
1371
|
+
confirmation_required: "The site or billing state changed, so the previous confirmation is stale; run the preview again and confirm against the fresh snapshot.",
|
|
1372
|
+
payment_required: "This operation requires an active subscription; check billing_status first.",
|
|
1373
|
+
rate_limited: "The server rate limit was reached; retry after the returned wait time."
|
|
1374
|
+
};
|
|
1343
1375
|
function toolError(e) {
|
|
1344
1376
|
const errorCode = isSakupaError(e) ? e.code : "internal";
|
|
1345
1377
|
const retryable = errorCode === "rate_limited" || errorCode === "internal";
|
|
@@ -1358,7 +1390,7 @@ function toolError(e) {
|
|
|
1358
1390
|
)
|
|
1359
1391
|
) : void 0;
|
|
1360
1392
|
const minimumVersion = rawDetails && typeof rawDetails["minimumVersion"] === "string" ? rawDetails["minimumVersion"] : void 0;
|
|
1361
|
-
const safeSummary = errorCode === "upgrade_required" ? `This Sakupa MCP client is v${MCP_VERSION}, older than the server's minimum supported version${minimumVersion !== void 0 ? ` (v${minimumVersion})` : ""}, so the server refused the call. To fix it: ask the user to fully restart their MCP client session \u2014 "npx -y @sakupa/mcp@latest" setups fetch the current version on restart (run "npx clear-npx-cache" first if the old version persists); global installs need "npm install -g @sakupa/mcp@latest". After the restart, retry this exact tool call.` : errorCode
|
|
1393
|
+
const safeSummary = e instanceof LocalGuidanceError ? e.message : errorCode === "upgrade_required" ? `This Sakupa MCP client is v${MCP_VERSION}, older than the server's minimum supported version${minimumVersion !== void 0 ? ` (v${minimumVersion})` : ""}, so the server refused the call. To fix it: ask the user to fully restart their MCP client session \u2014 "npx -y @sakupa/mcp@latest" setups fetch the current version on restart (run "npx clear-npx-cache" first if the old version persists); global installs need "npm install -g @sakupa/mcp@latest". After the restart, retry this exact tool call.` : STATIC_SUMMARY[errorCode] ?? (retryable ? "An upstream service is temporarily unavailable or busy; retry shortly." : "The operation failed; no server-internal details are exposed.");
|
|
1362
1394
|
const result = structuredToolResult({
|
|
1363
1395
|
schemaVersion: 1,
|
|
1364
1396
|
outcome: "failed",
|
|
@@ -1377,8 +1409,54 @@ function toolError(e) {
|
|
|
1377
1409
|
// src/tools/definitions.ts
|
|
1378
1410
|
import { randomUUID } from "node:crypto";
|
|
1379
1411
|
import { promises as fs2 } from "node:fs";
|
|
1380
|
-
import { join as
|
|
1381
|
-
import { z as
|
|
1412
|
+
import { join as join4, resolve as resolve3 } from "node:path";
|
|
1413
|
+
import { z as z3 } from "zod";
|
|
1414
|
+
|
|
1415
|
+
// src/creation-registry.ts
|
|
1416
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1417
|
+
import { homedir as homedir2 } from "node:os";
|
|
1418
|
+
import { dirname as dirname2, join as join3 } from "node:path";
|
|
1419
|
+
var RECENT_WINDOW_MS = FREE_SITE_TTL_HOURS * 60 * 60 * 1e3;
|
|
1420
|
+
function creationRegistryPath() {
|
|
1421
|
+
const base = process.env["SAKUPA_STATE_DIR"] ?? homedir2();
|
|
1422
|
+
return join3(base, ".sakupa", "created-sites.json");
|
|
1423
|
+
}
|
|
1424
|
+
function readAll() {
|
|
1425
|
+
const path = creationRegistryPath();
|
|
1426
|
+
if (!existsSync2(path)) return [];
|
|
1427
|
+
try {
|
|
1428
|
+
const parsed = JSON.parse(readFileSync2(path, "utf-8"));
|
|
1429
|
+
if (!Array.isArray(parsed)) return [];
|
|
1430
|
+
return parsed.filter(
|
|
1431
|
+
(e) => typeof e === "object" && e !== null && typeof e.siteId === "string" && typeof e.createdAt === "string"
|
|
1432
|
+
);
|
|
1433
|
+
} catch {
|
|
1434
|
+
return [];
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
function writeAll(records) {
|
|
1438
|
+
const path = creationRegistryPath();
|
|
1439
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
1440
|
+
writeFileSync2(path, `${JSON.stringify(records, null, 2)}
|
|
1441
|
+
`, "utf-8");
|
|
1442
|
+
}
|
|
1443
|
+
function listRecentCreations(nowMs) {
|
|
1444
|
+
return readAll().filter((e) => {
|
|
1445
|
+
const t = Date.parse(e.createdAt);
|
|
1446
|
+
return Number.isFinite(t) && nowMs - t < RECENT_WINDOW_MS;
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
function recordCreation(record) {
|
|
1450
|
+
const rest = readAll().filter((e) => e.siteId !== record.siteId);
|
|
1451
|
+
writeAll([...rest, record]);
|
|
1452
|
+
}
|
|
1453
|
+
function removeCreation(siteId) {
|
|
1454
|
+
const all = readAll();
|
|
1455
|
+
const rest = all.filter((e) => e.siteId !== siteId);
|
|
1456
|
+
if (rest.length !== all.length) writeAll(rest);
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
// src/tools/definitions.ts
|
|
1382
1460
|
function text(resultCode, t, data = {}, outcome = "completed") {
|
|
1383
1461
|
return structuredToolResult({
|
|
1384
1462
|
schemaVersion: 1,
|
|
@@ -1401,12 +1479,12 @@ ${JSON.stringify(obj, null, 2)}`;
|
|
|
1401
1479
|
nextActions: []
|
|
1402
1480
|
});
|
|
1403
1481
|
}
|
|
1404
|
-
var planEnum =
|
|
1405
|
-
var severityEnum =
|
|
1482
|
+
var planEnum = z3.enum(["water", "personal", "share", "business"]);
|
|
1483
|
+
var severityEnum = z3.enum(["low", "medium", "high", "critical"]);
|
|
1406
1484
|
function planCatalog() {
|
|
1407
|
-
return TIER_ORDER.map((p) => `${p}
|
|
1485
|
+
return TIER_ORDER.map((p) => `${p} JPY ${tierPriceJpy(p)}/month`).join(", ");
|
|
1408
1486
|
}
|
|
1409
|
-
var ticketCategoryEnum =
|
|
1487
|
+
var ticketCategoryEnum = z3.enum([
|
|
1410
1488
|
"billing",
|
|
1411
1489
|
"payment",
|
|
1412
1490
|
"refund_review",
|
|
@@ -1432,22 +1510,6 @@ Analysis:`,
|
|
|
1432
1510
|
"blocked"
|
|
1433
1511
|
);
|
|
1434
1512
|
}
|
|
1435
|
-
function spaConfirmationResult(analysis) {
|
|
1436
|
-
return text(
|
|
1437
|
-
"spa_fallback_confirmation_required",
|
|
1438
|
-
`SPA fallback confirmation required \u2014 nothing was deployed yet.
|
|
1439
|
-
|
|
1440
|
-
This site looks like a single-page application (one index.html plus JavaScript). SPA fallback rewrites every unknown path to index.html so client-side routes work, but it CHANGES normal 404 behavior: visitors never see a not-found page.
|
|
1441
|
-
|
|
1442
|
-
Please ask the user to choose, then re-run deploy_site with:
|
|
1443
|
-
- spaFallback: true, spaFallbackConfirmed: true -> enable SPA fallback
|
|
1444
|
-
- spaFallbackConfirmed: true (spaFallback omitted or false) -> deploy WITHOUT fallback (unknown paths return 404)
|
|
1445
|
-
|
|
1446
|
-
Output directory: "${analysis.recommendedOutputDir ?? "."}", ${analysis.fileCount} files.`,
|
|
1447
|
-
{ analysis: analysisSummary(analysis), requestedConfirmation: "spa_fallback" },
|
|
1448
|
-
"waiting_user"
|
|
1449
|
-
);
|
|
1450
|
-
}
|
|
1451
1513
|
var MB2 = 1024 * 1024;
|
|
1452
1514
|
function ensureUploadSizeWithinLimits(manifest, isFirstFreeDeploy) {
|
|
1453
1515
|
const oversized = manifest.find((f) => f.size > MAX_SINGLE_FILE_BYTES);
|
|
@@ -1470,7 +1532,7 @@ function ensureUploadSizeWithinLimits(manifest, isFirstFreeDeploy) {
|
|
|
1470
1532
|
async function buildHashedManifest(files, outputAbs) {
|
|
1471
1533
|
const manifest = [];
|
|
1472
1534
|
for (const file of files) {
|
|
1473
|
-
const bytes = new Uint8Array(await fs2.readFile(
|
|
1535
|
+
const bytes = new Uint8Array(await fs2.readFile(join4(outputAbs, file.path)));
|
|
1474
1536
|
manifest.push({ path: file.path, size: file.size, contentHash: await sha256Hex(bytes) });
|
|
1475
1537
|
}
|
|
1476
1538
|
return manifest;
|
|
@@ -1489,7 +1551,7 @@ async function uploadAll(ctx, targets, files, outputAbs) {
|
|
|
1489
1551
|
`No local file matches upload target "${target.path}"; aborting upload.`
|
|
1490
1552
|
);
|
|
1491
1553
|
}
|
|
1492
|
-
const bytes = new Uint8Array(await fs2.readFile(
|
|
1554
|
+
const bytes = new Uint8Array(await fs2.readFile(join4(outputAbs, match.path)));
|
|
1493
1555
|
if (bytes.byteLength !== match.size) {
|
|
1494
1556
|
throw new SakupaError(
|
|
1495
1557
|
"validation_failed",
|
|
@@ -1500,8 +1562,23 @@ async function uploadAll(ctx, targets, files, outputAbs) {
|
|
|
1500
1562
|
}
|
|
1501
1563
|
return targets.length;
|
|
1502
1564
|
}
|
|
1503
|
-
function
|
|
1504
|
-
const
|
|
1565
|
+
function freeSiteCreationBarrier() {
|
|
1566
|
+
const recent = listRecentCreations(Date.now());
|
|
1567
|
+
if (recent.length < FREE_ACTIVE_SITES_PER_IP) return null;
|
|
1568
|
+
const registryPath = creationRegistryPath();
|
|
1569
|
+
return text(
|
|
1570
|
+
"local_site_limit_reached",
|
|
1571
|
+
`This machine already created ${recent.length} free sites in the last 24 hours (the server also enforces ${FREE_ACTIVE_SITES_PER_IP} active free sites per IP). No new site was created.
|
|
1572
|
+
|
|
1573
|
+
` + recent.map((r) => `- ${r.url} (project: ${r.projectDir}, created: ${r.createdAt})`).join("\n") + `
|
|
1574
|
+
|
|
1575
|
+
Options: delete one of these sites (run delete_site with its projectDir), wait for a free site to expire, or \u2014 if this list is stale because sites were deleted or subscribed elsewhere \u2014 remove the local registry file at ${registryPath} and retry.`,
|
|
1576
|
+
{ recentCreations: recent, limit: FREE_ACTIVE_SITES_PER_IP, registryPath },
|
|
1577
|
+
"blocked"
|
|
1578
|
+
);
|
|
1579
|
+
}
|
|
1580
|
+
function registerTools(server, baseCtx) {
|
|
1581
|
+
const previewHostPattern = previewHostPatternFor(baseCtx.apiBaseUrl);
|
|
1505
1582
|
server.registerTool(
|
|
1506
1583
|
"analyze_site",
|
|
1507
1584
|
{
|
|
@@ -1509,17 +1586,15 @@ function registerTools(server, ctx) {
|
|
|
1509
1586
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1510
1587
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
1511
1588
|
inputSchema: {
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
spaFallbackConfirmed: z2.boolean().optional().describe("User explicitly confirmed the SPA fallback 404-behavior change.")
|
|
1589
|
+
projectDir: projectDirInput,
|
|
1590
|
+
outputDir: z3.string().optional().describe("Output directory relative to the project root (overrides detection).")
|
|
1515
1591
|
}
|
|
1516
1592
|
},
|
|
1517
1593
|
async (args) => {
|
|
1518
1594
|
try {
|
|
1595
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1519
1596
|
const analysis = await analyzeProject(ctx.projectDir, {
|
|
1520
|
-
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
|
|
1521
|
-
...args.spaFallbackRequested !== void 0 ? { spaFallbackRequested: args.spaFallbackRequested } : {},
|
|
1522
|
-
...args.spaFallbackConfirmed !== void 0 ? { spaFallbackConfirmed: args.spaFallbackConfirmed } : {}
|
|
1597
|
+
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
|
|
1523
1598
|
});
|
|
1524
1599
|
return textJson(
|
|
1525
1600
|
"site_analysis_completed",
|
|
@@ -1539,30 +1614,28 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1539
1614
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1540
1615
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
1541
1616
|
inputSchema: {
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1617
|
+
projectDir: projectDirInput,
|
|
1618
|
+
outputDir: z3.string().optional().describe("Output directory relative to the project root (overrides detection)."),
|
|
1619
|
+
spaFallback: z3.boolean().optional().describe(
|
|
1620
|
+
"Override automatic SPA-fallback detection (single index.html + JS auto-enables rewriting unknown paths to index.html; multiple HTML pages auto-disable it). Pass only to force the behavior against the detected structure."
|
|
1621
|
+
),
|
|
1622
|
+
publicConfirmed: z3.boolean().optional().describe(
|
|
1546
1623
|
"Required only for the first deployment: user explicitly confirmed creation of a public 24-hour URL."
|
|
1547
1624
|
),
|
|
1548
|
-
lang:
|
|
1625
|
+
lang: z3.string().optional().describe("Site language override (en | ja | zh-CN); defaults to the html lang.")
|
|
1549
1626
|
}
|
|
1550
1627
|
},
|
|
1551
1628
|
async (args) => {
|
|
1552
1629
|
try {
|
|
1630
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1553
1631
|
const analysis = await analyzeProject(ctx.projectDir, {
|
|
1554
|
-
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
|
|
1555
|
-
...args.spaFallback !== void 0 ? { spaFallbackRequested: args.spaFallback } : {},
|
|
1556
|
-
...args.spaFallbackConfirmed !== void 0 ? { spaFallbackConfirmed: args.spaFallbackConfirmed } : {}
|
|
1632
|
+
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
|
|
1557
1633
|
});
|
|
1558
|
-
if (analysis.spa.confirmationRequired && args.spaFallbackConfirmed !== true) {
|
|
1559
|
-
return spaConfirmationResult(analysis);
|
|
1560
|
-
}
|
|
1561
1634
|
if (!analysis.deployable || !analysis.files) {
|
|
1562
1635
|
return notDeployableResult(analysis);
|
|
1563
1636
|
}
|
|
1564
1637
|
const files = analysis.files;
|
|
1565
|
-
const outputAbs =
|
|
1638
|
+
const outputAbs = resolve3(ctx.projectDir, analysis.recommendedOutputDir ?? ".");
|
|
1566
1639
|
const manifest = await buildHashedManifest(files, outputAbs);
|
|
1567
1640
|
const siteFileState = loadSiteFile(ctx.projectDir);
|
|
1568
1641
|
if (siteFileState.kind === "corrupted") {
|
|
@@ -1576,6 +1649,10 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1576
1649
|
);
|
|
1577
1650
|
}
|
|
1578
1651
|
const existing = siteFileState.kind === "ok" ? siteFileState.file : null;
|
|
1652
|
+
if (!existing) {
|
|
1653
|
+
const barrier = freeSiteCreationBarrier();
|
|
1654
|
+
if (barrier) return barrier;
|
|
1655
|
+
}
|
|
1579
1656
|
if (!existing && args.publicConfirmed !== true) {
|
|
1580
1657
|
return text(
|
|
1581
1658
|
"public_deployment_confirmation_required",
|
|
@@ -1589,8 +1666,7 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1589
1666
|
const created = await ctx.client.createSite({
|
|
1590
1667
|
manifest,
|
|
1591
1668
|
...args.lang !== void 0 ? { lang: args.lang } : {},
|
|
1592
|
-
spaFallback: args.spaFallback
|
|
1593
|
-
...args.spaFallbackConfirmed !== void 0 ? { spaFallbackConfirmed: args.spaFallbackConfirmed } : {}
|
|
1669
|
+
...args.spaFallback !== void 0 ? { spaFallback: args.spaFallback } : {}
|
|
1594
1670
|
});
|
|
1595
1671
|
const uploaded2 = await uploadAll(ctx, created.uploadTargets, files, outputAbs);
|
|
1596
1672
|
const finalized2 = await ctx.client.finalizeDeployment(
|
|
@@ -1598,17 +1674,25 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1598
1674
|
created.siteId,
|
|
1599
1675
|
created.credential
|
|
1600
1676
|
);
|
|
1677
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1601
1678
|
writeSiteFile(ctx.projectDir, {
|
|
1602
1679
|
siteId: created.siteId,
|
|
1603
1680
|
shortId: created.shortId,
|
|
1604
1681
|
url: finalized2.url,
|
|
1605
1682
|
credential: created.credential,
|
|
1606
|
-
createdAt
|
|
1683
|
+
createdAt,
|
|
1607
1684
|
apiBaseUrl: ctx.apiBaseUrl
|
|
1608
1685
|
});
|
|
1686
|
+
recordCreation({
|
|
1687
|
+
siteId: created.siteId,
|
|
1688
|
+
projectDir: ctx.projectDir,
|
|
1689
|
+
url: finalized2.url,
|
|
1690
|
+
createdAt
|
|
1691
|
+
});
|
|
1609
1692
|
return text(
|
|
1610
1693
|
"site_published",
|
|
1611
1694
|
`Site published: ${finalized2.url}
|
|
1695
|
+
Project directory: ${ctx.projectDir}
|
|
1612
1696
|
Files uploaded: ${uploaded2} (${finalized2.totalBytes} bytes)
|
|
1613
1697
|
` + (finalized2.expiresAt ? `Expires at: ${finalized2.expiresAt}
|
|
1614
1698
|
` : "") + `
|
|
@@ -1626,7 +1710,8 @@ ${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
|
|
|
1626
1710
|
filesUploaded: uploaded2,
|
|
1627
1711
|
totalBytes: finalized2.totalBytes,
|
|
1628
1712
|
warnings: finalized2.warnings,
|
|
1629
|
-
credentialStoredLocally: true
|
|
1713
|
+
credentialStoredLocally: true,
|
|
1714
|
+
projectDir: ctx.projectDir
|
|
1630
1715
|
}
|
|
1631
1716
|
);
|
|
1632
1717
|
}
|
|
@@ -1634,8 +1719,7 @@ ${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
|
|
|
1634
1719
|
const req = {
|
|
1635
1720
|
manifest,
|
|
1636
1721
|
...args.lang !== void 0 ? { lang: args.lang } : {},
|
|
1637
|
-
spaFallback: args.spaFallback
|
|
1638
|
-
...args.spaFallbackConfirmed !== void 0 ? { spaFallbackConfirmed: args.spaFallbackConfirmed } : {},
|
|
1722
|
+
...args.spaFallback !== void 0 ? { spaFallback: args.spaFallback } : {},
|
|
1639
1723
|
...forceFullUpload ? { forceFullUpload: true } : {}
|
|
1640
1724
|
};
|
|
1641
1725
|
const deployment = await ctx.client.createDeployment(
|
|
@@ -1673,6 +1757,7 @@ ${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
|
|
|
1673
1757
|
return text(
|
|
1674
1758
|
"site_updated",
|
|
1675
1759
|
`Site updated: ${finalized.url}
|
|
1760
|
+
Project directory: ${ctx.projectDir}
|
|
1676
1761
|
Files uploaded: ${uploaded} (${finalized.totalBytes} bytes)
|
|
1677
1762
|
` + (finalized.expiresAt ? `Validity refreshed \u2014 expires at: ${finalized.expiresAt}
|
|
1678
1763
|
` : "") + (finalized.mode === "free" ? `
|
|
@@ -1684,6 +1769,7 @@ ${JSON.stringify(finalized.warnings, null, 2)}` : ""),
|
|
|
1684
1769
|
siteId: existing.siteId,
|
|
1685
1770
|
url: finalized.url,
|
|
1686
1771
|
mode: finalized.mode,
|
|
1772
|
+
projectDir: ctx.projectDir,
|
|
1687
1773
|
expiresAt: finalized.expiresAt,
|
|
1688
1774
|
filesUploaded: uploaded,
|
|
1689
1775
|
totalBytes: finalized.totalBytes,
|
|
@@ -1701,17 +1787,18 @@ ${JSON.stringify(finalized.warnings, null, 2)}` : ""),
|
|
|
1701
1787
|
description: "Refresh the validity of the free temporary site WITHOUT uploading content. Uses the local credential in .sakupa/site.json. Subscribed sites are permanent and need no refresh.",
|
|
1702
1788
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1703
1789
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1704
|
-
inputSchema: {}
|
|
1790
|
+
inputSchema: { projectDir: projectDirInput }
|
|
1705
1791
|
},
|
|
1706
|
-
async () => {
|
|
1792
|
+
async (args) => {
|
|
1707
1793
|
try {
|
|
1794
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1708
1795
|
const site = requireSiteFile(ctx);
|
|
1709
1796
|
const res = await ctx.client.refreshSite(site.siteId, site.credential);
|
|
1710
1797
|
return text(
|
|
1711
1798
|
"site_refreshed",
|
|
1712
|
-
`Site validity refreshed. New expiry: ${res.expiresAt}
|
|
1713
|
-
Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refresh.`,
|
|
1714
|
-
{ siteId: site.siteId, expiresAt: res.expiresAt }
|
|
1799
|
+
`Site validity refreshed (project: ${ctx.projectDir}). New expiry: ${res.expiresAt}
|
|
1800
|
+
NO content was uploaded or changed by this call \u2014 to publish new or edited files, run deploy_site. Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refresh.`,
|
|
1801
|
+
{ siteId: site.siteId, expiresAt: res.expiresAt, projectDir: ctx.projectDir }
|
|
1715
1802
|
);
|
|
1716
1803
|
} catch (e) {
|
|
1717
1804
|
return toolError(e);
|
|
@@ -1724,13 +1811,17 @@ Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refre
|
|
|
1724
1811
|
description: "Show the current status of this project's Sakupa site: URL, mode (free/paid), expiry, subscription state, custom domains, size, last deployment and warnings.",
|
|
1725
1812
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1726
1813
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
1727
|
-
inputSchema: {}
|
|
1814
|
+
inputSchema: { projectDir: projectDirInput }
|
|
1728
1815
|
},
|
|
1729
|
-
async () => {
|
|
1816
|
+
async (args) => {
|
|
1730
1817
|
try {
|
|
1818
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1731
1819
|
const site = requireSiteFile(ctx);
|
|
1732
1820
|
const res = await ctx.client.getSiteStatus(site.siteId, site.credential);
|
|
1733
|
-
return textJson("site_status_returned", "Site status:",
|
|
1821
|
+
return textJson("site_status_returned", "Site status:", {
|
|
1822
|
+
...res,
|
|
1823
|
+
projectDir: ctx.projectDir
|
|
1824
|
+
});
|
|
1734
1825
|
} catch (e) {
|
|
1735
1826
|
return toolError(e);
|
|
1736
1827
|
}
|
|
@@ -1743,6 +1834,7 @@ Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refre
|
|
|
1743
1834
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1744
1835
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1745
1836
|
inputSchema: {
|
|
1837
|
+
projectDir: projectDirInput,
|
|
1746
1838
|
plan: planEnum.describe(
|
|
1747
1839
|
"Monthly plan: water (very light personal pages), personal (personal brand / small shop), share (small-business site), business (steadier traffic, more headroom)."
|
|
1748
1840
|
)
|
|
@@ -1750,6 +1842,7 @@ Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refre
|
|
|
1750
1842
|
},
|
|
1751
1843
|
async (args) => {
|
|
1752
1844
|
try {
|
|
1845
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1753
1846
|
const site = requireSiteFile(ctx);
|
|
1754
1847
|
const res = await ctx.client.createPlanCheckout(
|
|
1755
1848
|
{
|
|
@@ -1761,7 +1854,7 @@ Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refre
|
|
|
1761
1854
|
);
|
|
1762
1855
|
return text(
|
|
1763
1856
|
"subscription_checkout_ready",
|
|
1764
|
-
`Stripe Checkout link \u2014 Sakupa Hosting for this site: ${res.plan} plan,
|
|
1857
|
+
`Stripe Checkout link \u2014 Sakupa Hosting for this site: ${res.plan} plan, JPY ${res.monthlyPriceJpy}/month (Japanese yen)
|
|
1765
1858
|
${res.checkoutUrl}
|
|
1766
1859
|
|
|
1767
1860
|
Open this link in a browser to subscribe. Card data is entered only on the Stripe-hosted page \u2014 never give card numbers, passwords or security codes to the AI tool.
|
|
@@ -1788,13 +1881,15 @@ Once payment confirms, the site becomes permanent on its current URL. Binding a
|
|
|
1788
1881
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1789
1882
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
1790
1883
|
inputSchema: {
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1884
|
+
projectDir: projectDirInput,
|
|
1885
|
+
action: z3.enum(["start", "status"]),
|
|
1886
|
+
hostname: z3.string().optional().describe("Required for start."),
|
|
1887
|
+
verificationId: z3.string().optional().describe("Required for status.")
|
|
1794
1888
|
}
|
|
1795
1889
|
},
|
|
1796
1890
|
async (args) => {
|
|
1797
1891
|
try {
|
|
1892
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1798
1893
|
const site = requireSiteFile(ctx);
|
|
1799
1894
|
if (args.action === "status") {
|
|
1800
1895
|
if (!args.verificationId) {
|
|
@@ -1864,16 +1959,17 @@ Then run bind_domain again with verificationId: "${res.verificationId}" to check
|
|
|
1864
1959
|
description: "Show this site's hosting subscription: plan, payment state, current paid period, reconciled usage, estimated usage tier, bound custom domains and risks. Owner-only (uses the credential in .sakupa/site.json).",
|
|
1865
1960
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1866
1961
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
1867
|
-
inputSchema: {}
|
|
1962
|
+
inputSchema: { projectDir: projectDirInput }
|
|
1868
1963
|
},
|
|
1869
|
-
async () => {
|
|
1964
|
+
async (args) => {
|
|
1870
1965
|
try {
|
|
1966
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1871
1967
|
const site = requireSiteFile(ctx);
|
|
1872
1968
|
const res = await ctx.client.getBillingStatus(site.siteId, site.credential);
|
|
1873
1969
|
const lines = [
|
|
1874
1970
|
`Billing status for site ${res.siteId} (mode: ${res.mode})`,
|
|
1875
1971
|
res.permanentUrl ? `Permanent URL: ${res.permanentUrl}` : void 0,
|
|
1876
|
-
res.plan ? `Plan: ${res.plan} (
|
|
1972
|
+
res.plan ? `Plan: ${res.plan} (JPY ${res.monthlyPriceJpy ?? tierPriceJpy(res.plan)}/month)` : "Plan: (no subscription yet)",
|
|
1877
1973
|
res.subscriptionStatus ? `Subscription payment state: ${res.subscriptionStatus}` : void 0,
|
|
1878
1974
|
res.cancelAtPeriodEnd ? "Renewal: CANCELED \u2014 the site reverts to free at the end of the already-paid month" : void 0,
|
|
1879
1975
|
res.currentPeriodStart ? `Current paid period: ${res.currentPeriodStart} -> ${res.currentPeriodEnd ?? "?"}` : void 0,
|
|
@@ -1897,11 +1993,13 @@ Full status:`, res);
|
|
|
1897
1993
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1898
1994
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1899
1995
|
inputSchema: {
|
|
1900
|
-
|
|
1996
|
+
projectDir: projectDirInput,
|
|
1997
|
+
scope: z3.enum(["site", "public_recovery"])
|
|
1901
1998
|
}
|
|
1902
1999
|
},
|
|
1903
2000
|
async (args) => {
|
|
1904
2001
|
try {
|
|
2002
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1905
2003
|
if (args.scope === "site") {
|
|
1906
2004
|
const site = requireSiteFile(ctx);
|
|
1907
2005
|
const res2 = await ctx.client.createBillingPortal(site.siteId, site.credential);
|
|
@@ -1952,14 +2050,16 @@ Full status:`, res);
|
|
|
1952
2050
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1953
2051
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
1954
2052
|
inputSchema: {
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
2053
|
+
projectDir: projectDirInput,
|
|
2054
|
+
action: z3.enum(["start", "status", "complete"]),
|
|
2055
|
+
hostname: z3.string().optional().describe("Required for start."),
|
|
2056
|
+
verificationId: z3.string().optional().describe("Required for status or complete."),
|
|
2057
|
+
preserveExistingCredentials: z3.boolean().optional().describe("Explicitly keep old local credentials working (default: revoke them all).")
|
|
1959
2058
|
}
|
|
1960
2059
|
},
|
|
1961
2060
|
async (args) => {
|
|
1962
2061
|
try {
|
|
2062
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1963
2063
|
if (args.action === "start") {
|
|
1964
2064
|
if (!args.hostname) {
|
|
1965
2065
|
throw new SakupaError("invalid_request", "hostname is required for start");
|
|
@@ -2057,14 +2157,16 @@ ${res.archiveUrl}`,
|
|
|
2057
2157
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
2058
2158
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
2059
2159
|
inputSchema: {
|
|
2160
|
+
projectDir: projectDirInput,
|
|
2060
2161
|
category: ticketCategoryEnum,
|
|
2061
|
-
subject:
|
|
2062
|
-
description:
|
|
2063
|
-
contactEmail:
|
|
2162
|
+
subject: z3.string().describe("Short subject line."),
|
|
2163
|
+
description: z3.string().describe("Problem description (no secrets, no card data)."),
|
|
2164
|
+
contactEmail: z3.string().optional().describe("Optional contact email for follow-up.")
|
|
2064
2165
|
}
|
|
2065
2166
|
},
|
|
2066
2167
|
async (args) => {
|
|
2067
2168
|
try {
|
|
2169
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
2068
2170
|
const site = requireSiteFile(ctx);
|
|
2069
2171
|
const res = await ctx.client.createTicket(site.credential, {
|
|
2070
2172
|
siteId: site.siteId,
|
|
@@ -2090,18 +2192,20 @@ ${res.archiveUrl}`,
|
|
|
2090
2192
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
2091
2193
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
2092
2194
|
inputSchema: {
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2195
|
+
projectDir: projectDirInput,
|
|
2196
|
+
toolName: z3.string().describe('The Sakupa tool that failed, e.g. "deploy_site".'),
|
|
2197
|
+
errorCode: z3.string().optional(),
|
|
2198
|
+
errorMessage: z3.string().optional().describe("Sanitized error message (no secrets)."),
|
|
2199
|
+
requestId: z3.string().optional(),
|
|
2200
|
+
deploymentId: z3.string().optional(),
|
|
2098
2201
|
severity: severityEnum.optional(),
|
|
2099
|
-
description:
|
|
2100
|
-
confirmSubmit:
|
|
2202
|
+
description: z3.string().optional().describe("What happened, in the user's words (no secrets)."),
|
|
2203
|
+
confirmSubmit: z3.boolean().optional().describe("User reviewed the report payload and approved submission.")
|
|
2101
2204
|
}
|
|
2102
2205
|
},
|
|
2103
2206
|
async (args) => {
|
|
2104
2207
|
try {
|
|
2208
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
2105
2209
|
const siteState = loadSiteFile(ctx.projectDir);
|
|
2106
2210
|
const site = siteState.kind === "ok" ? siteState.file : null;
|
|
2107
2211
|
const diagnostics = {
|
|
@@ -2145,39 +2249,40 @@ Summary: ${res.sanitizedSummary}`,
|
|
|
2145
2249
|
}
|
|
2146
2250
|
|
|
2147
2251
|
// src/tools/lifecycle.ts
|
|
2148
|
-
import { z as
|
|
2149
|
-
var deleteConfirmation =
|
|
2150
|
-
siteId:
|
|
2151
|
-
expectedSiteUpdatedAt:
|
|
2152
|
-
expectedStatus:
|
|
2153
|
-
expectedMode:
|
|
2154
|
-
expectedServingMode:
|
|
2155
|
-
expectedShortId:
|
|
2156
|
-
expectedSubscriptionStatus:
|
|
2157
|
-
expectedPlan:
|
|
2158
|
-
expectedCancelAtPeriodEnd:
|
|
2159
|
-
expectedCurrentPeriodEnd:
|
|
2160
|
-
expectedLastDeploymentId:
|
|
2161
|
-
expectedBoundHostnames:
|
|
2162
|
-
acknowledge:
|
|
2252
|
+
import { z as z4 } from "zod";
|
|
2253
|
+
var deleteConfirmation = z4.object({
|
|
2254
|
+
siteId: z4.string().min(1),
|
|
2255
|
+
expectedSiteUpdatedAt: z4.string().datetime(),
|
|
2256
|
+
expectedStatus: z4.enum(["active", "expired", "deleted"]),
|
|
2257
|
+
expectedMode: z4.enum(["free", "paid"]),
|
|
2258
|
+
expectedServingMode: z4.enum(["normal", "over_limit_notice", "risk_notice", "stopped"]),
|
|
2259
|
+
expectedShortId: z4.string().optional(),
|
|
2260
|
+
expectedSubscriptionStatus: z4.enum(["incomplete", "active", "past_due", "canceled"]).optional(),
|
|
2261
|
+
expectedPlan: z4.enum(["water", "personal", "share", "business"]).optional(),
|
|
2262
|
+
expectedCancelAtPeriodEnd: z4.boolean().optional(),
|
|
2263
|
+
expectedCurrentPeriodEnd: z4.string().datetime().optional(),
|
|
2264
|
+
expectedLastDeploymentId: z4.string().optional(),
|
|
2265
|
+
expectedBoundHostnames: z4.array(z4.string()),
|
|
2266
|
+
acknowledge: z4.literal("delete_site_and_cancel_renewal")
|
|
2163
2267
|
});
|
|
2164
|
-
var unbindConfirmation =
|
|
2165
|
-
siteId:
|
|
2166
|
-
bindingId:
|
|
2167
|
-
expectedBindingUpdatedAt:
|
|
2168
|
-
expectedBindingStatus:
|
|
2169
|
-
apexDomain:
|
|
2170
|
-
expectedBoundHostnames:
|
|
2171
|
-
acknowledge:
|
|
2268
|
+
var unbindConfirmation = z4.object({
|
|
2269
|
+
siteId: z4.string().min(1),
|
|
2270
|
+
bindingId: z4.string().min(1),
|
|
2271
|
+
expectedBindingUpdatedAt: z4.string().datetime(),
|
|
2272
|
+
expectedBindingStatus: z4.enum(["provisioning", "active"]),
|
|
2273
|
+
apexDomain: z4.string().min(1),
|
|
2274
|
+
expectedBoundHostnames: z4.array(z4.string()),
|
|
2275
|
+
acknowledge: z4.literal("unbind_domain_and_remove_custom_hostnames")
|
|
2172
2276
|
});
|
|
2173
|
-
function registerLifecycleTools(server,
|
|
2277
|
+
function registerLifecycleTools(server, baseCtx) {
|
|
2174
2278
|
server.registerTool(
|
|
2175
2279
|
"delete_site",
|
|
2176
2280
|
{
|
|
2177
2281
|
description: "Preview or execute deletion of this Sakupa site. Execution requires an exact server-validated confirmation bound to the current site state.",
|
|
2178
2282
|
inputSchema: {
|
|
2179
|
-
|
|
2180
|
-
|
|
2283
|
+
projectDir: projectDirInput,
|
|
2284
|
+
action: z4.enum(["preview", "confirm"]),
|
|
2285
|
+
operationId: z4.string().min(1).optional(),
|
|
2181
2286
|
confirmation: deleteConfirmation.optional()
|
|
2182
2287
|
},
|
|
2183
2288
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
@@ -2185,6 +2290,7 @@ function registerLifecycleTools(server, ctx) {
|
|
|
2185
2290
|
},
|
|
2186
2291
|
async (args) => {
|
|
2187
2292
|
try {
|
|
2293
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
2188
2294
|
const site = requireSiteFile(ctx);
|
|
2189
2295
|
if (!args.operationId) {
|
|
2190
2296
|
throw new Error("operationId is required for delete_site");
|
|
@@ -2211,13 +2317,14 @@ function registerLifecycleTools(server, ctx) {
|
|
|
2211
2317
|
confirmation: args.confirmation
|
|
2212
2318
|
});
|
|
2213
2319
|
deleteSiteFile(ctx.projectDir);
|
|
2320
|
+
removeCreation(site.siteId);
|
|
2214
2321
|
return structuredToolResult({
|
|
2215
2322
|
schemaVersion: 1,
|
|
2216
2323
|
outcome: result.servingDeletionPending ? "pending_provider" : "completed",
|
|
2217
2324
|
resultCode: "site_deleted",
|
|
2218
2325
|
operationId: args.operationId,
|
|
2219
|
-
summary:
|
|
2220
|
-
data: { result },
|
|
2326
|
+
summary: `Site deleted; the local management credential file was removed from ${ctx.projectDir}.`,
|
|
2327
|
+
data: { result, projectDir: ctx.projectDir },
|
|
2221
2328
|
nextActions: []
|
|
2222
2329
|
});
|
|
2223
2330
|
} catch (error) {
|
|
@@ -2230,8 +2337,9 @@ function registerLifecycleTools(server, ctx) {
|
|
|
2230
2337
|
{
|
|
2231
2338
|
description: "Preview or execute removal of the custom apex/www serving surface while preserving the subscription and permanent Sakupa URL.",
|
|
2232
2339
|
inputSchema: {
|
|
2233
|
-
|
|
2234
|
-
|
|
2340
|
+
projectDir: projectDirInput,
|
|
2341
|
+
action: z4.enum(["preview", "confirm"]),
|
|
2342
|
+
operationId: z4.string().min(1).optional(),
|
|
2235
2343
|
confirmation: unbindConfirmation.optional()
|
|
2236
2344
|
},
|
|
2237
2345
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
@@ -2239,6 +2347,7 @@ function registerLifecycleTools(server, ctx) {
|
|
|
2239
2347
|
},
|
|
2240
2348
|
async (args) => {
|
|
2241
2349
|
try {
|
|
2350
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
2242
2351
|
const site = requireSiteFile(ctx);
|
|
2243
2352
|
if (!args.operationId) throw new Error("operationId is required for unbind_domain");
|
|
2244
2353
|
if (args.action === "preview") {
|
|
@@ -2269,7 +2378,7 @@ function registerLifecycleTools(server, ctx) {
|
|
|
2269
2378
|
outcome: result.servingDeletionPending ? "pending_provider" : "completed",
|
|
2270
2379
|
resultCode: "domain_unbound",
|
|
2271
2380
|
operationId: args.operationId,
|
|
2272
|
-
summary:
|
|
2381
|
+
summary: `Custom domain unbound; the subscription, deployed content, and permanent Sakupa URL are unchanged. (project: ${ctx.projectDir})`,
|
|
2273
2382
|
data: { result },
|
|
2274
2383
|
nextActions: [{ tool: "site_status", allowed: true }]
|
|
2275
2384
|
});
|
|
@@ -2284,19 +2393,20 @@ function registerLifecycleTools(server, ctx) {
|
|
|
2284
2393
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2285
2394
|
|
|
2286
2395
|
// src/tools/billing.ts
|
|
2287
|
-
import { z as
|
|
2288
|
-
var plan =
|
|
2289
|
-
function registerBillingTools(server,
|
|
2396
|
+
import { z as z5 } from "zod";
|
|
2397
|
+
var plan = z5.enum(["water", "personal", "share", "business"]);
|
|
2398
|
+
function registerBillingTools(server, baseCtx) {
|
|
2290
2399
|
server.registerTool(
|
|
2291
2400
|
"list_billing_plans",
|
|
2292
2401
|
{
|
|
2293
2402
|
description: "Return the authoritative Sakupa monthly plan catalog, exact limits, prices, catalog version and plan-change billing rules. This is read-only and does not require a site.",
|
|
2294
|
-
inputSchema: {},
|
|
2403
|
+
inputSchema: { projectDir: projectDirInput },
|
|
2295
2404
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
2296
2405
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true }
|
|
2297
2406
|
},
|
|
2298
|
-
async () => {
|
|
2407
|
+
async (args) => {
|
|
2299
2408
|
try {
|
|
2409
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
2300
2410
|
const catalog = await ctx.client.getBillingPlanCatalog();
|
|
2301
2411
|
return structuredToolResult({
|
|
2302
2412
|
schemaVersion: 1,
|
|
@@ -2316,14 +2426,16 @@ function registerBillingTools(server, ctx) {
|
|
|
2316
2426
|
{
|
|
2317
2427
|
description: "Create a Stripe-hosted confirmation link for a manually selected subscription plan. Creating the link does not change billing; only the user can confirm on Stripe.",
|
|
2318
2428
|
inputSchema: {
|
|
2429
|
+
projectDir: projectDirInput,
|
|
2319
2430
|
targetPlan: plan,
|
|
2320
|
-
operationId:
|
|
2431
|
+
operationId: z5.string().min(1)
|
|
2321
2432
|
},
|
|
2322
2433
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
2323
2434
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
|
|
2324
2435
|
},
|
|
2325
2436
|
async (args) => {
|
|
2326
2437
|
try {
|
|
2438
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
2327
2439
|
const site = requireSiteFile(ctx);
|
|
2328
2440
|
const result = await ctx.client.changeSubscriptionPlan(site.credential, {
|
|
2329
2441
|
siteId: site.siteId,
|
|
@@ -2382,11 +2494,28 @@ Workflow:
|
|
|
2382
2494
|
5. create_support_ticket (subscribed sites) opens a support ticket; report_bug sends a
|
|
2383
2495
|
sanitized diagnostic report after the user explicitly confirms it.
|
|
2384
2496
|
|
|
2497
|
+
Project directory contract: ONE directory = ONE site (its .sakupa/site.json holds the
|
|
2498
|
+
binding). Every project-scoped tool accepts projectDir \u2014 ALWAYS pass the absolute path of
|
|
2499
|
+
the directory the user is currently working in, on every call. Without it the server falls
|
|
2500
|
+
back to its startup directory, which may be a different project than the one the user is
|
|
2501
|
+
looking at. analyze_site, deploy_site, site_status, refresh_site, delete_site and unbind_domain echo
|
|
2502
|
+
the directory they acted on \u2014 verify it matches the user's active project.
|
|
2503
|
+
|
|
2385
2504
|
Safety boundaries:
|
|
2386
2505
|
- Static output only: no SSR, API routes, middleware, server actions, databases or online builds.
|
|
2387
2506
|
- Never upload source projects, secrets, .env files, private keys, archives, videos or audio.
|
|
2388
2507
|
- Payment card data is entered only on Stripe-hosted pages \u2014 never through the AI tool.
|
|
2389
2508
|
- A subscription never grants domain ownership; only DNS verification does.
|
|
2509
|
+
- Never repeat, echo, or memorize the credential value from .sakupa/site.json \u2014 quoting it
|
|
2510
|
+
into the conversation copies the site's only key outside the protected local file. Read it
|
|
2511
|
+
only through the tools.
|
|
2512
|
+
- Before deploying, if the entry HTML lacks a lang attribute, add one matching the content
|
|
2513
|
+
language (infer it from the content) and then deploy; only skip when the user explicitly
|
|
2514
|
+
wants no lang attribute.
|
|
2515
|
+
- Prices are authoritative in JPY (Japanese yen). When talking with a user in a language
|
|
2516
|
+
other than Japanese, look up the approximate exchange rate and show an estimated local
|
|
2517
|
+
price next to the JPY amount, clearly marked as an estimate \u2014 Stripe always settles the
|
|
2518
|
+
real charge in JPY. Never show a bare Yen sign.
|
|
2390
2519
|
- The management credential lives only in .sakupa/site.json; never share or upload it. Without
|
|
2391
2520
|
a bound custom domain, a lost credential is unrecoverable by design. manage_billing then opens
|
|
2392
2521
|
Stripe's public no-code portal login, where the customer verifies the checkout email with a
|