@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/bin.js
CHANGED
|
@@ -10,6 +10,7 @@ var FREE_SITE_URL_SUFFIX = `.${SERVICE_DOMAIN}`;
|
|
|
10
10
|
var TEST_ACCESS_HEADER = "x-sakupa-test-token";
|
|
11
11
|
var FREE_SITE_TTL_HOURS = 24;
|
|
12
12
|
var FREE_SITE_MAX_TOTAL_BYTES = 10 * 1024 * 1024;
|
|
13
|
+
var FREE_ACTIVE_SITES_PER_IP = 3;
|
|
13
14
|
var PAID_SITE_MAX_TOTAL_BYTES = 2 * 1024 * 1024 * 1024;
|
|
14
15
|
var MAX_FILE_COUNT = 5e3;
|
|
15
16
|
var MAX_SINGLE_FILE_BYTES = 25 * 1024 * 1024;
|
|
@@ -128,7 +129,7 @@ var FORBIDDEN_PATH_SEGMENTS = [
|
|
|
128
129
|
var ALLOWED_HIDDEN_PATHS = [".well-known/"];
|
|
129
130
|
|
|
130
131
|
// ../core/dist/domain/version.js
|
|
131
|
-
var SAKUPA_MCP_VERSION = "0.7.
|
|
132
|
+
var SAKUPA_MCP_VERSION = "0.7.8";
|
|
132
133
|
|
|
133
134
|
// ../core/dist/domain/errors.js
|
|
134
135
|
var HTTP_STATUS = {
|
|
@@ -407,22 +408,13 @@ function validateDeployableFiles(files, opts) {
|
|
|
407
408
|
severity: "warning",
|
|
408
409
|
code: "missing_html_lang",
|
|
409
410
|
path: entryHtmlPath,
|
|
410
|
-
message: 'The entry HTML has no lang attribute.
|
|
411
|
+
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.'
|
|
411
412
|
});
|
|
412
413
|
}
|
|
413
414
|
}
|
|
414
415
|
}
|
|
415
416
|
const jsCount = files.filter((f) => ["js", "mjs"].includes(fileExtension(f.path))).length;
|
|
416
417
|
const looksLikeSpa = htmlPaths.length === 1 && entryHtmlPath === "index.html" && jsCount > 0;
|
|
417
|
-
const wantsSpa = opts.spaFallbackRequested === true || looksLikeSpa;
|
|
418
|
-
const spaFallbackConfirmationRequired = wantsSpa && opts.spaFallbackConfirmed !== true;
|
|
419
|
-
if (opts.spaFallbackRequested === true && opts.spaFallbackConfirmed !== true) {
|
|
420
|
-
issues.push({
|
|
421
|
-
severity: "error",
|
|
422
|
-
code: "spa_fallback_confirmation_required",
|
|
423
|
-
message: "SPA fallback rewrites unknown paths to index.html and changes normal 404 behavior. It must be explicitly confirmed."
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
418
|
const ok = issues.every((i) => i.severity !== "error");
|
|
427
419
|
return {
|
|
428
420
|
ok,
|
|
@@ -432,8 +424,7 @@ function validateDeployableFiles(files, opts) {
|
|
|
432
424
|
entryHtmlPath,
|
|
433
425
|
htmlLang,
|
|
434
426
|
supportedLang,
|
|
435
|
-
looksLikeSpa
|
|
436
|
-
spaFallbackConfirmationRequired
|
|
427
|
+
looksLikeSpa
|
|
437
428
|
};
|
|
438
429
|
}
|
|
439
430
|
function safeDecode(bytes) {
|
|
@@ -690,8 +681,8 @@ var HttpApiClient = class {
|
|
|
690
681
|
// src/tools/definitions.ts
|
|
691
682
|
import { randomUUID } from "node:crypto";
|
|
692
683
|
import { promises as fs2 } from "node:fs";
|
|
693
|
-
import { join as
|
|
694
|
-
import { z as
|
|
684
|
+
import { join as join4, resolve as resolve3 } from "node:path";
|
|
685
|
+
import { z as z3 } from "zod";
|
|
695
686
|
|
|
696
687
|
// src/analyze/analyzer.ts
|
|
697
688
|
import { promises as fs } from "node:fs";
|
|
@@ -1052,7 +1043,7 @@ async function analyzeProject(projectDir, opts = {}) {
|
|
|
1052
1043
|
fileCount: 0,
|
|
1053
1044
|
issues: [],
|
|
1054
1045
|
ssrRisks,
|
|
1055
|
-
spa: { looksLikeSpa: false,
|
|
1046
|
+
spa: { looksLikeSpa: false, autoFallback: false },
|
|
1056
1047
|
deployable: false,
|
|
1057
1048
|
suggestedNextAction: suggestedNextAction2
|
|
1058
1049
|
};
|
|
@@ -1072,30 +1063,23 @@ async function analyzeProject(projectDir, opts = {}) {
|
|
|
1072
1063
|
}
|
|
1073
1064
|
candidates.push({ path: file.path, size: file.size, ...content ? { content } : {} });
|
|
1074
1065
|
}
|
|
1075
|
-
const validation = validateDeployableFiles(candidates, {
|
|
1076
|
-
mode: "free",
|
|
1077
|
-
...opts.spaFallbackRequested !== void 0 ? { spaFallbackRequested: opts.spaFallbackRequested } : {},
|
|
1078
|
-
...opts.spaFallbackConfirmed !== void 0 ? { spaFallbackConfirmed: opts.spaFallbackConfirmed } : {}
|
|
1079
|
-
});
|
|
1066
|
+
const validation = validateDeployableFiles(candidates, { mode: "free" });
|
|
1080
1067
|
ssrRisks.push(...serverAndDbDepRisks(pkg, true));
|
|
1081
1068
|
const deployable = validation.ok && walked.length > 0;
|
|
1082
1069
|
const spa = {
|
|
1083
1070
|
looksLikeSpa: validation.looksLikeSpa,
|
|
1084
|
-
|
|
1085
|
-
confirmationRequired: validation.spaFallbackConfirmationRequired
|
|
1071
|
+
autoFallback: validation.looksLikeSpa
|
|
1086
1072
|
};
|
|
1087
1073
|
let suggestedNextAction;
|
|
1088
1074
|
if (!deployable) {
|
|
1089
1075
|
const firstError = validation.issues.find((i) => i.severity === "error");
|
|
1090
1076
|
if (firstError?.code === "missing_index_html") {
|
|
1091
1077
|
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.`;
|
|
1092
|
-
} else if (firstError?.code === "spa_fallback_confirmation_required") {
|
|
1093
|
-
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.";
|
|
1094
1078
|
} else {
|
|
1095
1079
|
suggestedNextAction = "Fix the listed issues (remove forbidden/secret files, reduce size, add missing entry HTML), then re-run analyze_site.";
|
|
1096
1080
|
}
|
|
1097
|
-
} else if (spa.
|
|
1098
|
-
suggestedNextAction = `
|
|
1081
|
+
} else if (spa.looksLikeSpa) {
|
|
1082
|
+
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.`;
|
|
1099
1083
|
} else {
|
|
1100
1084
|
suggestedNextAction = `Run deploy_site to publish the static output in "${outputDirRel}".`;
|
|
1101
1085
|
}
|
|
@@ -1220,10 +1204,60 @@ function credentialGitReminder(projectDir) {
|
|
|
1220
1204
|
return '\nNOTE: this project is inside a git repository. The management credential in .sakupa/site.json is the key to this site \u2014 do NOT commit it to a PUBLIC repository (add ".sakupa/" to .gitignore yourself if you want to keep it out of version control).';
|
|
1221
1205
|
}
|
|
1222
1206
|
|
|
1207
|
+
// src/creation-registry.ts
|
|
1208
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1209
|
+
import { homedir } from "node:os";
|
|
1210
|
+
import { dirname as dirname2, join as join3 } from "node:path";
|
|
1211
|
+
var RECENT_WINDOW_MS = FREE_SITE_TTL_HOURS * 60 * 60 * 1e3;
|
|
1212
|
+
function creationRegistryPath() {
|
|
1213
|
+
const base = process.env["SAKUPA_STATE_DIR"] ?? homedir();
|
|
1214
|
+
return join3(base, ".sakupa", "created-sites.json");
|
|
1215
|
+
}
|
|
1216
|
+
function readAll() {
|
|
1217
|
+
const path = creationRegistryPath();
|
|
1218
|
+
if (!existsSync2(path)) return [];
|
|
1219
|
+
try {
|
|
1220
|
+
const parsed = JSON.parse(readFileSync2(path, "utf-8"));
|
|
1221
|
+
if (!Array.isArray(parsed)) return [];
|
|
1222
|
+
return parsed.filter(
|
|
1223
|
+
(e) => typeof e === "object" && e !== null && typeof e.siteId === "string" && typeof e.createdAt === "string"
|
|
1224
|
+
);
|
|
1225
|
+
} catch {
|
|
1226
|
+
return [];
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
function writeAll(records) {
|
|
1230
|
+
const path = creationRegistryPath();
|
|
1231
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
1232
|
+
writeFileSync2(path, `${JSON.stringify(records, null, 2)}
|
|
1233
|
+
`, "utf-8");
|
|
1234
|
+
}
|
|
1235
|
+
function listRecentCreations(nowMs) {
|
|
1236
|
+
return readAll().filter((e) => {
|
|
1237
|
+
const t = Date.parse(e.createdAt);
|
|
1238
|
+
return Number.isFinite(t) && nowMs - t < RECENT_WINDOW_MS;
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
function recordCreation(record) {
|
|
1242
|
+
const rest = readAll().filter((e) => e.siteId !== record.siteId);
|
|
1243
|
+
writeAll([...rest, record]);
|
|
1244
|
+
}
|
|
1245
|
+
function removeCreation(siteId) {
|
|
1246
|
+
const all = readAll();
|
|
1247
|
+
const rest = all.filter((e) => e.siteId !== siteId);
|
|
1248
|
+
if (rest.length !== all.length) writeAll(rest);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1223
1251
|
// src/version.ts
|
|
1224
1252
|
var MCP_VERSION = SAKUPA_MCP_VERSION;
|
|
1225
1253
|
var CLIENT_TYPE = "sakupa-mcp";
|
|
1226
1254
|
|
|
1255
|
+
// src/tools/context.ts
|
|
1256
|
+
import { z as z2 } from "zod";
|
|
1257
|
+
import { statSync } from "node:fs";
|
|
1258
|
+
import { homedir as homedir2 } from "node:os";
|
|
1259
|
+
import { isAbsolute, parse, resolve as resolve2 } from "node:path";
|
|
1260
|
+
|
|
1227
1261
|
// src/tools/result.ts
|
|
1228
1262
|
import { z } from "zod";
|
|
1229
1263
|
var STRUCTURED_TOOL_OUTPUT_SCHEMA = {
|
|
@@ -1266,22 +1300,64 @@ function structuredToolResult(envelope) {
|
|
|
1266
1300
|
}
|
|
1267
1301
|
|
|
1268
1302
|
// src/tools/context.ts
|
|
1303
|
+
var LocalGuidanceError = class extends SakupaError {
|
|
1304
|
+
constructor(code, message) {
|
|
1305
|
+
super(code, message);
|
|
1306
|
+
}
|
|
1307
|
+
};
|
|
1308
|
+
var projectDirInput = z2.string().optional().describe(
|
|
1309
|
+
"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."
|
|
1310
|
+
);
|
|
1311
|
+
function withProjectDir(ctx, projectDirArg) {
|
|
1312
|
+
if (projectDirArg === void 0) return ctx;
|
|
1313
|
+
if (!isAbsolute(projectDirArg)) {
|
|
1314
|
+
throw new LocalGuidanceError(
|
|
1315
|
+
"invalid_request",
|
|
1316
|
+
`projectDir must be an ABSOLUTE path (got "${projectDirArg}"). Pass the full path of the directory the user is currently working in.`
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
const dir = resolve2(projectDirArg);
|
|
1320
|
+
if (parse(dir).root === dir || dir === homedir2()) {
|
|
1321
|
+
throw new LocalGuidanceError(
|
|
1322
|
+
"invalid_request",
|
|
1323
|
+
`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.`
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
const stat = statSync(dir, { throwIfNoEntry: false });
|
|
1327
|
+
if (!stat?.isDirectory()) {
|
|
1328
|
+
throw new LocalGuidanceError(
|
|
1329
|
+
"invalid_request",
|
|
1330
|
+
`projectDir "${dir}" does not exist or is not a directory. Pass the absolute path of the directory the user is currently working in.`
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
return { ...ctx, projectDir: dir };
|
|
1334
|
+
}
|
|
1269
1335
|
function requireSiteFile(ctx) {
|
|
1270
1336
|
const state = loadSiteFile(ctx.projectDir);
|
|
1271
1337
|
if (state.kind === "corrupted") {
|
|
1272
|
-
throw new
|
|
1338
|
+
throw new LocalGuidanceError(
|
|
1273
1339
|
"invalid_request",
|
|
1274
1340
|
`.sakupa/site.json in ${ctx.projectDir} is damaged: ${state.problem}. ` + siteFileRecoveryGuidance(ctx.projectDir)
|
|
1275
1341
|
);
|
|
1276
1342
|
}
|
|
1277
1343
|
if (state.kind === "absent") {
|
|
1278
|
-
throw new
|
|
1344
|
+
throw new LocalGuidanceError(
|
|
1279
1345
|
"not_found",
|
|
1280
1346
|
`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.`
|
|
1281
1347
|
);
|
|
1282
1348
|
}
|
|
1283
1349
|
return state.file;
|
|
1284
1350
|
}
|
|
1351
|
+
var STATIC_SUMMARY = {
|
|
1352
|
+
not_found: "The required local project binding or resource is unavailable; if this project has no .sakupa/site.json yet, run deploy_site first.",
|
|
1353
|
+
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.",
|
|
1354
|
+
invalid_request: "The request arguments or local project checks did not pass.",
|
|
1355
|
+
validation_failed: "The request arguments or local project checks did not pass.",
|
|
1356
|
+
state_conflict: "The resource state has changed; re-query the current status before deciding the next step.",
|
|
1357
|
+
confirmation_required: "The site or billing state changed, so the previous confirmation is stale; run the preview again and confirm against the fresh snapshot.",
|
|
1358
|
+
payment_required: "This operation requires an active subscription; check billing_status first.",
|
|
1359
|
+
rate_limited: "The server rate limit was reached; retry after the returned wait time."
|
|
1360
|
+
};
|
|
1285
1361
|
function toolError(e) {
|
|
1286
1362
|
const errorCode = isSakupaError(e) ? e.code : "internal";
|
|
1287
1363
|
const retryable = errorCode === "rate_limited" || errorCode === "internal";
|
|
@@ -1300,7 +1376,7 @@ function toolError(e) {
|
|
|
1300
1376
|
)
|
|
1301
1377
|
) : void 0;
|
|
1302
1378
|
const minimumVersion = rawDetails && typeof rawDetails["minimumVersion"] === "string" ? rawDetails["minimumVersion"] : void 0;
|
|
1303
|
-
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
|
|
1379
|
+
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.");
|
|
1304
1380
|
const result = structuredToolResult({
|
|
1305
1381
|
schemaVersion: 1,
|
|
1306
1382
|
outcome: "failed",
|
|
@@ -1339,12 +1415,12 @@ ${JSON.stringify(obj, null, 2)}`;
|
|
|
1339
1415
|
nextActions: []
|
|
1340
1416
|
});
|
|
1341
1417
|
}
|
|
1342
|
-
var planEnum =
|
|
1343
|
-
var severityEnum =
|
|
1418
|
+
var planEnum = z3.enum(["water", "personal", "share", "business"]);
|
|
1419
|
+
var severityEnum = z3.enum(["low", "medium", "high", "critical"]);
|
|
1344
1420
|
function planCatalog() {
|
|
1345
|
-
return TIER_ORDER.map((p) => `${p}
|
|
1421
|
+
return TIER_ORDER.map((p) => `${p} JPY ${tierPriceJpy(p)}/month`).join(", ");
|
|
1346
1422
|
}
|
|
1347
|
-
var ticketCategoryEnum =
|
|
1423
|
+
var ticketCategoryEnum = z3.enum([
|
|
1348
1424
|
"billing",
|
|
1349
1425
|
"payment",
|
|
1350
1426
|
"refund_review",
|
|
@@ -1370,22 +1446,6 @@ Analysis:`,
|
|
|
1370
1446
|
"blocked"
|
|
1371
1447
|
);
|
|
1372
1448
|
}
|
|
1373
|
-
function spaConfirmationResult(analysis) {
|
|
1374
|
-
return text(
|
|
1375
|
-
"spa_fallback_confirmation_required",
|
|
1376
|
-
`SPA fallback confirmation required \u2014 nothing was deployed yet.
|
|
1377
|
-
|
|
1378
|
-
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.
|
|
1379
|
-
|
|
1380
|
-
Please ask the user to choose, then re-run deploy_site with:
|
|
1381
|
-
- spaFallback: true, spaFallbackConfirmed: true -> enable SPA fallback
|
|
1382
|
-
- spaFallbackConfirmed: true (spaFallback omitted or false) -> deploy WITHOUT fallback (unknown paths return 404)
|
|
1383
|
-
|
|
1384
|
-
Output directory: "${analysis.recommendedOutputDir ?? "."}", ${analysis.fileCount} files.`,
|
|
1385
|
-
{ analysis: analysisSummary(analysis), requestedConfirmation: "spa_fallback" },
|
|
1386
|
-
"waiting_user"
|
|
1387
|
-
);
|
|
1388
|
-
}
|
|
1389
1449
|
var MB2 = 1024 * 1024;
|
|
1390
1450
|
function ensureUploadSizeWithinLimits(manifest, isFirstFreeDeploy) {
|
|
1391
1451
|
const oversized = manifest.find((f) => f.size > MAX_SINGLE_FILE_BYTES);
|
|
@@ -1408,7 +1468,7 @@ function ensureUploadSizeWithinLimits(manifest, isFirstFreeDeploy) {
|
|
|
1408
1468
|
async function buildHashedManifest(files, outputAbs) {
|
|
1409
1469
|
const manifest = [];
|
|
1410
1470
|
for (const file of files) {
|
|
1411
|
-
const bytes = new Uint8Array(await fs2.readFile(
|
|
1471
|
+
const bytes = new Uint8Array(await fs2.readFile(join4(outputAbs, file.path)));
|
|
1412
1472
|
manifest.push({ path: file.path, size: file.size, contentHash: await sha256Hex(bytes) });
|
|
1413
1473
|
}
|
|
1414
1474
|
return manifest;
|
|
@@ -1427,7 +1487,7 @@ async function uploadAll(ctx, targets, files, outputAbs) {
|
|
|
1427
1487
|
`No local file matches upload target "${target.path}"; aborting upload.`
|
|
1428
1488
|
);
|
|
1429
1489
|
}
|
|
1430
|
-
const bytes = new Uint8Array(await fs2.readFile(
|
|
1490
|
+
const bytes = new Uint8Array(await fs2.readFile(join4(outputAbs, match.path)));
|
|
1431
1491
|
if (bytes.byteLength !== match.size) {
|
|
1432
1492
|
throw new SakupaError(
|
|
1433
1493
|
"validation_failed",
|
|
@@ -1438,8 +1498,23 @@ async function uploadAll(ctx, targets, files, outputAbs) {
|
|
|
1438
1498
|
}
|
|
1439
1499
|
return targets.length;
|
|
1440
1500
|
}
|
|
1441
|
-
function
|
|
1442
|
-
const
|
|
1501
|
+
function freeSiteCreationBarrier() {
|
|
1502
|
+
const recent = listRecentCreations(Date.now());
|
|
1503
|
+
if (recent.length < FREE_ACTIVE_SITES_PER_IP) return null;
|
|
1504
|
+
const registryPath = creationRegistryPath();
|
|
1505
|
+
return text(
|
|
1506
|
+
"local_site_limit_reached",
|
|
1507
|
+
`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.
|
|
1508
|
+
|
|
1509
|
+
` + recent.map((r) => `- ${r.url} (project: ${r.projectDir}, created: ${r.createdAt})`).join("\n") + `
|
|
1510
|
+
|
|
1511
|
+
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.`,
|
|
1512
|
+
{ recentCreations: recent, limit: FREE_ACTIVE_SITES_PER_IP, registryPath },
|
|
1513
|
+
"blocked"
|
|
1514
|
+
);
|
|
1515
|
+
}
|
|
1516
|
+
function registerTools(server, baseCtx) {
|
|
1517
|
+
const previewHostPattern = previewHostPatternFor(baseCtx.apiBaseUrl);
|
|
1443
1518
|
server.registerTool(
|
|
1444
1519
|
"analyze_site",
|
|
1445
1520
|
{
|
|
@@ -1447,17 +1522,15 @@ function registerTools(server, ctx) {
|
|
|
1447
1522
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1448
1523
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
1449
1524
|
inputSchema: {
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
spaFallbackConfirmed: z2.boolean().optional().describe("User explicitly confirmed the SPA fallback 404-behavior change.")
|
|
1525
|
+
projectDir: projectDirInput,
|
|
1526
|
+
outputDir: z3.string().optional().describe("Output directory relative to the project root (overrides detection).")
|
|
1453
1527
|
}
|
|
1454
1528
|
},
|
|
1455
1529
|
async (args) => {
|
|
1456
1530
|
try {
|
|
1531
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1457
1532
|
const analysis = await analyzeProject(ctx.projectDir, {
|
|
1458
|
-
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
|
|
1459
|
-
...args.spaFallbackRequested !== void 0 ? { spaFallbackRequested: args.spaFallbackRequested } : {},
|
|
1460
|
-
...args.spaFallbackConfirmed !== void 0 ? { spaFallbackConfirmed: args.spaFallbackConfirmed } : {}
|
|
1533
|
+
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
|
|
1461
1534
|
});
|
|
1462
1535
|
return textJson(
|
|
1463
1536
|
"site_analysis_completed",
|
|
@@ -1477,30 +1550,28 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1477
1550
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1478
1551
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
1479
1552
|
inputSchema: {
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1553
|
+
projectDir: projectDirInput,
|
|
1554
|
+
outputDir: z3.string().optional().describe("Output directory relative to the project root (overrides detection)."),
|
|
1555
|
+
spaFallback: z3.boolean().optional().describe(
|
|
1556
|
+
"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."
|
|
1557
|
+
),
|
|
1558
|
+
publicConfirmed: z3.boolean().optional().describe(
|
|
1484
1559
|
"Required only for the first deployment: user explicitly confirmed creation of a public 24-hour URL."
|
|
1485
1560
|
),
|
|
1486
|
-
lang:
|
|
1561
|
+
lang: z3.string().optional().describe("Site language override (en | ja | zh-CN); defaults to the html lang.")
|
|
1487
1562
|
}
|
|
1488
1563
|
},
|
|
1489
1564
|
async (args) => {
|
|
1490
1565
|
try {
|
|
1566
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1491
1567
|
const analysis = await analyzeProject(ctx.projectDir, {
|
|
1492
|
-
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
|
|
1493
|
-
...args.spaFallback !== void 0 ? { spaFallbackRequested: args.spaFallback } : {},
|
|
1494
|
-
...args.spaFallbackConfirmed !== void 0 ? { spaFallbackConfirmed: args.spaFallbackConfirmed } : {}
|
|
1568
|
+
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
|
|
1495
1569
|
});
|
|
1496
|
-
if (analysis.spa.confirmationRequired && args.spaFallbackConfirmed !== true) {
|
|
1497
|
-
return spaConfirmationResult(analysis);
|
|
1498
|
-
}
|
|
1499
1570
|
if (!analysis.deployable || !analysis.files) {
|
|
1500
1571
|
return notDeployableResult(analysis);
|
|
1501
1572
|
}
|
|
1502
1573
|
const files = analysis.files;
|
|
1503
|
-
const outputAbs =
|
|
1574
|
+
const outputAbs = resolve3(ctx.projectDir, analysis.recommendedOutputDir ?? ".");
|
|
1504
1575
|
const manifest = await buildHashedManifest(files, outputAbs);
|
|
1505
1576
|
const siteFileState = loadSiteFile(ctx.projectDir);
|
|
1506
1577
|
if (siteFileState.kind === "corrupted") {
|
|
@@ -1514,6 +1585,10 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1514
1585
|
);
|
|
1515
1586
|
}
|
|
1516
1587
|
const existing = siteFileState.kind === "ok" ? siteFileState.file : null;
|
|
1588
|
+
if (!existing) {
|
|
1589
|
+
const barrier = freeSiteCreationBarrier();
|
|
1590
|
+
if (barrier) return barrier;
|
|
1591
|
+
}
|
|
1517
1592
|
if (!existing && args.publicConfirmed !== true) {
|
|
1518
1593
|
return text(
|
|
1519
1594
|
"public_deployment_confirmation_required",
|
|
@@ -1527,8 +1602,7 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1527
1602
|
const created = await ctx.client.createSite({
|
|
1528
1603
|
manifest,
|
|
1529
1604
|
...args.lang !== void 0 ? { lang: args.lang } : {},
|
|
1530
|
-
spaFallback: args.spaFallback
|
|
1531
|
-
...args.spaFallbackConfirmed !== void 0 ? { spaFallbackConfirmed: args.spaFallbackConfirmed } : {}
|
|
1605
|
+
...args.spaFallback !== void 0 ? { spaFallback: args.spaFallback } : {}
|
|
1532
1606
|
});
|
|
1533
1607
|
const uploaded2 = await uploadAll(ctx, created.uploadTargets, files, outputAbs);
|
|
1534
1608
|
const finalized2 = await ctx.client.finalizeDeployment(
|
|
@@ -1536,17 +1610,25 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1536
1610
|
created.siteId,
|
|
1537
1611
|
created.credential
|
|
1538
1612
|
);
|
|
1613
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1539
1614
|
writeSiteFile(ctx.projectDir, {
|
|
1540
1615
|
siteId: created.siteId,
|
|
1541
1616
|
shortId: created.shortId,
|
|
1542
1617
|
url: finalized2.url,
|
|
1543
1618
|
credential: created.credential,
|
|
1544
|
-
createdAt
|
|
1619
|
+
createdAt,
|
|
1545
1620
|
apiBaseUrl: ctx.apiBaseUrl
|
|
1546
1621
|
});
|
|
1622
|
+
recordCreation({
|
|
1623
|
+
siteId: created.siteId,
|
|
1624
|
+
projectDir: ctx.projectDir,
|
|
1625
|
+
url: finalized2.url,
|
|
1626
|
+
createdAt
|
|
1627
|
+
});
|
|
1547
1628
|
return text(
|
|
1548
1629
|
"site_published",
|
|
1549
1630
|
`Site published: ${finalized2.url}
|
|
1631
|
+
Project directory: ${ctx.projectDir}
|
|
1550
1632
|
Files uploaded: ${uploaded2} (${finalized2.totalBytes} bytes)
|
|
1551
1633
|
` + (finalized2.expiresAt ? `Expires at: ${finalized2.expiresAt}
|
|
1552
1634
|
` : "") + `
|
|
@@ -1564,7 +1646,8 @@ ${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
|
|
|
1564
1646
|
filesUploaded: uploaded2,
|
|
1565
1647
|
totalBytes: finalized2.totalBytes,
|
|
1566
1648
|
warnings: finalized2.warnings,
|
|
1567
|
-
credentialStoredLocally: true
|
|
1649
|
+
credentialStoredLocally: true,
|
|
1650
|
+
projectDir: ctx.projectDir
|
|
1568
1651
|
}
|
|
1569
1652
|
);
|
|
1570
1653
|
}
|
|
@@ -1572,8 +1655,7 @@ ${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
|
|
|
1572
1655
|
const req = {
|
|
1573
1656
|
manifest,
|
|
1574
1657
|
...args.lang !== void 0 ? { lang: args.lang } : {},
|
|
1575
|
-
spaFallback: args.spaFallback
|
|
1576
|
-
...args.spaFallbackConfirmed !== void 0 ? { spaFallbackConfirmed: args.spaFallbackConfirmed } : {},
|
|
1658
|
+
...args.spaFallback !== void 0 ? { spaFallback: args.spaFallback } : {},
|
|
1577
1659
|
...forceFullUpload ? { forceFullUpload: true } : {}
|
|
1578
1660
|
};
|
|
1579
1661
|
const deployment = await ctx.client.createDeployment(
|
|
@@ -1611,6 +1693,7 @@ ${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
|
|
|
1611
1693
|
return text(
|
|
1612
1694
|
"site_updated",
|
|
1613
1695
|
`Site updated: ${finalized.url}
|
|
1696
|
+
Project directory: ${ctx.projectDir}
|
|
1614
1697
|
Files uploaded: ${uploaded} (${finalized.totalBytes} bytes)
|
|
1615
1698
|
` + (finalized.expiresAt ? `Validity refreshed \u2014 expires at: ${finalized.expiresAt}
|
|
1616
1699
|
` : "") + (finalized.mode === "free" ? `
|
|
@@ -1622,6 +1705,7 @@ ${JSON.stringify(finalized.warnings, null, 2)}` : ""),
|
|
|
1622
1705
|
siteId: existing.siteId,
|
|
1623
1706
|
url: finalized.url,
|
|
1624
1707
|
mode: finalized.mode,
|
|
1708
|
+
projectDir: ctx.projectDir,
|
|
1625
1709
|
expiresAt: finalized.expiresAt,
|
|
1626
1710
|
filesUploaded: uploaded,
|
|
1627
1711
|
totalBytes: finalized.totalBytes,
|
|
@@ -1639,17 +1723,18 @@ ${JSON.stringify(finalized.warnings, null, 2)}` : ""),
|
|
|
1639
1723
|
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.",
|
|
1640
1724
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1641
1725
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1642
|
-
inputSchema: {}
|
|
1726
|
+
inputSchema: { projectDir: projectDirInput }
|
|
1643
1727
|
},
|
|
1644
|
-
async () => {
|
|
1728
|
+
async (args) => {
|
|
1645
1729
|
try {
|
|
1730
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1646
1731
|
const site = requireSiteFile(ctx);
|
|
1647
1732
|
const res = await ctx.client.refreshSite(site.siteId, site.credential);
|
|
1648
1733
|
return text(
|
|
1649
1734
|
"site_refreshed",
|
|
1650
|
-
`Site validity refreshed. New expiry: ${res.expiresAt}
|
|
1651
|
-
Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refresh.`,
|
|
1652
|
-
{ siteId: site.siteId, expiresAt: res.expiresAt }
|
|
1735
|
+
`Site validity refreshed (project: ${ctx.projectDir}). New expiry: ${res.expiresAt}
|
|
1736
|
+
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.`,
|
|
1737
|
+
{ siteId: site.siteId, expiresAt: res.expiresAt, projectDir: ctx.projectDir }
|
|
1653
1738
|
);
|
|
1654
1739
|
} catch (e) {
|
|
1655
1740
|
return toolError(e);
|
|
@@ -1662,13 +1747,17 @@ Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refre
|
|
|
1662
1747
|
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.",
|
|
1663
1748
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1664
1749
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
1665
|
-
inputSchema: {}
|
|
1750
|
+
inputSchema: { projectDir: projectDirInput }
|
|
1666
1751
|
},
|
|
1667
|
-
async () => {
|
|
1752
|
+
async (args) => {
|
|
1668
1753
|
try {
|
|
1754
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1669
1755
|
const site = requireSiteFile(ctx);
|
|
1670
1756
|
const res = await ctx.client.getSiteStatus(site.siteId, site.credential);
|
|
1671
|
-
return textJson("site_status_returned", "Site status:",
|
|
1757
|
+
return textJson("site_status_returned", "Site status:", {
|
|
1758
|
+
...res,
|
|
1759
|
+
projectDir: ctx.projectDir
|
|
1760
|
+
});
|
|
1672
1761
|
} catch (e) {
|
|
1673
1762
|
return toolError(e);
|
|
1674
1763
|
}
|
|
@@ -1681,6 +1770,7 @@ Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refre
|
|
|
1681
1770
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1682
1771
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1683
1772
|
inputSchema: {
|
|
1773
|
+
projectDir: projectDirInput,
|
|
1684
1774
|
plan: planEnum.describe(
|
|
1685
1775
|
"Monthly plan: water (very light personal pages), personal (personal brand / small shop), share (small-business site), business (steadier traffic, more headroom)."
|
|
1686
1776
|
)
|
|
@@ -1688,6 +1778,7 @@ Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refre
|
|
|
1688
1778
|
},
|
|
1689
1779
|
async (args) => {
|
|
1690
1780
|
try {
|
|
1781
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1691
1782
|
const site = requireSiteFile(ctx);
|
|
1692
1783
|
const res = await ctx.client.createPlanCheckout(
|
|
1693
1784
|
{
|
|
@@ -1699,7 +1790,7 @@ Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refre
|
|
|
1699
1790
|
);
|
|
1700
1791
|
return text(
|
|
1701
1792
|
"subscription_checkout_ready",
|
|
1702
|
-
`Stripe Checkout link \u2014 Sakupa Hosting for this site: ${res.plan} plan,
|
|
1793
|
+
`Stripe Checkout link \u2014 Sakupa Hosting for this site: ${res.plan} plan, JPY ${res.monthlyPriceJpy}/month (Japanese yen)
|
|
1703
1794
|
${res.checkoutUrl}
|
|
1704
1795
|
|
|
1705
1796
|
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.
|
|
@@ -1726,13 +1817,15 @@ Once payment confirms, the site becomes permanent on its current URL. Binding a
|
|
|
1726
1817
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1727
1818
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
1728
1819
|
inputSchema: {
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1820
|
+
projectDir: projectDirInput,
|
|
1821
|
+
action: z3.enum(["start", "status"]),
|
|
1822
|
+
hostname: z3.string().optional().describe("Required for start."),
|
|
1823
|
+
verificationId: z3.string().optional().describe("Required for status.")
|
|
1732
1824
|
}
|
|
1733
1825
|
},
|
|
1734
1826
|
async (args) => {
|
|
1735
1827
|
try {
|
|
1828
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1736
1829
|
const site = requireSiteFile(ctx);
|
|
1737
1830
|
if (args.action === "status") {
|
|
1738
1831
|
if (!args.verificationId) {
|
|
@@ -1802,16 +1895,17 @@ Then run bind_domain again with verificationId: "${res.verificationId}" to check
|
|
|
1802
1895
|
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).",
|
|
1803
1896
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1804
1897
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
1805
|
-
inputSchema: {}
|
|
1898
|
+
inputSchema: { projectDir: projectDirInput }
|
|
1806
1899
|
},
|
|
1807
|
-
async () => {
|
|
1900
|
+
async (args) => {
|
|
1808
1901
|
try {
|
|
1902
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1809
1903
|
const site = requireSiteFile(ctx);
|
|
1810
1904
|
const res = await ctx.client.getBillingStatus(site.siteId, site.credential);
|
|
1811
1905
|
const lines = [
|
|
1812
1906
|
`Billing status for site ${res.siteId} (mode: ${res.mode})`,
|
|
1813
1907
|
res.permanentUrl ? `Permanent URL: ${res.permanentUrl}` : void 0,
|
|
1814
|
-
res.plan ? `Plan: ${res.plan} (
|
|
1908
|
+
res.plan ? `Plan: ${res.plan} (JPY ${res.monthlyPriceJpy ?? tierPriceJpy(res.plan)}/month)` : "Plan: (no subscription yet)",
|
|
1815
1909
|
res.subscriptionStatus ? `Subscription payment state: ${res.subscriptionStatus}` : void 0,
|
|
1816
1910
|
res.cancelAtPeriodEnd ? "Renewal: CANCELED \u2014 the site reverts to free at the end of the already-paid month" : void 0,
|
|
1817
1911
|
res.currentPeriodStart ? `Current paid period: ${res.currentPeriodStart} -> ${res.currentPeriodEnd ?? "?"}` : void 0,
|
|
@@ -1835,11 +1929,13 @@ Full status:`, res);
|
|
|
1835
1929
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1836
1930
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1837
1931
|
inputSchema: {
|
|
1838
|
-
|
|
1932
|
+
projectDir: projectDirInput,
|
|
1933
|
+
scope: z3.enum(["site", "public_recovery"])
|
|
1839
1934
|
}
|
|
1840
1935
|
},
|
|
1841
1936
|
async (args) => {
|
|
1842
1937
|
try {
|
|
1938
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1843
1939
|
if (args.scope === "site") {
|
|
1844
1940
|
const site = requireSiteFile(ctx);
|
|
1845
1941
|
const res2 = await ctx.client.createBillingPortal(site.siteId, site.credential);
|
|
@@ -1890,14 +1986,16 @@ Full status:`, res);
|
|
|
1890
1986
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1891
1987
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
1892
1988
|
inputSchema: {
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1989
|
+
projectDir: projectDirInput,
|
|
1990
|
+
action: z3.enum(["start", "status", "complete"]),
|
|
1991
|
+
hostname: z3.string().optional().describe("Required for start."),
|
|
1992
|
+
verificationId: z3.string().optional().describe("Required for status or complete."),
|
|
1993
|
+
preserveExistingCredentials: z3.boolean().optional().describe("Explicitly keep old local credentials working (default: revoke them all).")
|
|
1897
1994
|
}
|
|
1898
1995
|
},
|
|
1899
1996
|
async (args) => {
|
|
1900
1997
|
try {
|
|
1998
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1901
1999
|
if (args.action === "start") {
|
|
1902
2000
|
if (!args.hostname) {
|
|
1903
2001
|
throw new SakupaError("invalid_request", "hostname is required for start");
|
|
@@ -1995,14 +2093,16 @@ ${res.archiveUrl}`,
|
|
|
1995
2093
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1996
2094
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1997
2095
|
inputSchema: {
|
|
2096
|
+
projectDir: projectDirInput,
|
|
1998
2097
|
category: ticketCategoryEnum,
|
|
1999
|
-
subject:
|
|
2000
|
-
description:
|
|
2001
|
-
contactEmail:
|
|
2098
|
+
subject: z3.string().describe("Short subject line."),
|
|
2099
|
+
description: z3.string().describe("Problem description (no secrets, no card data)."),
|
|
2100
|
+
contactEmail: z3.string().optional().describe("Optional contact email for follow-up.")
|
|
2002
2101
|
}
|
|
2003
2102
|
},
|
|
2004
2103
|
async (args) => {
|
|
2005
2104
|
try {
|
|
2105
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
2006
2106
|
const site = requireSiteFile(ctx);
|
|
2007
2107
|
const res = await ctx.client.createTicket(site.credential, {
|
|
2008
2108
|
siteId: site.siteId,
|
|
@@ -2028,18 +2128,20 @@ ${res.archiveUrl}`,
|
|
|
2028
2128
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
2029
2129
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
2030
2130
|
inputSchema: {
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2131
|
+
projectDir: projectDirInput,
|
|
2132
|
+
toolName: z3.string().describe('The Sakupa tool that failed, e.g. "deploy_site".'),
|
|
2133
|
+
errorCode: z3.string().optional(),
|
|
2134
|
+
errorMessage: z3.string().optional().describe("Sanitized error message (no secrets)."),
|
|
2135
|
+
requestId: z3.string().optional(),
|
|
2136
|
+
deploymentId: z3.string().optional(),
|
|
2036
2137
|
severity: severityEnum.optional(),
|
|
2037
|
-
description:
|
|
2038
|
-
confirmSubmit:
|
|
2138
|
+
description: z3.string().optional().describe("What happened, in the user's words (no secrets)."),
|
|
2139
|
+
confirmSubmit: z3.boolean().optional().describe("User reviewed the report payload and approved submission.")
|
|
2039
2140
|
}
|
|
2040
2141
|
},
|
|
2041
2142
|
async (args) => {
|
|
2042
2143
|
try {
|
|
2144
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
2043
2145
|
const siteState = loadSiteFile(ctx.projectDir);
|
|
2044
2146
|
const site = siteState.kind === "ok" ? siteState.file : null;
|
|
2045
2147
|
const diagnostics = {
|
|
@@ -2083,19 +2185,20 @@ Summary: ${res.sanitizedSummary}`,
|
|
|
2083
2185
|
}
|
|
2084
2186
|
|
|
2085
2187
|
// src/tools/billing.ts
|
|
2086
|
-
import { z as
|
|
2087
|
-
var plan =
|
|
2088
|
-
function registerBillingTools(server,
|
|
2188
|
+
import { z as z4 } from "zod";
|
|
2189
|
+
var plan = z4.enum(["water", "personal", "share", "business"]);
|
|
2190
|
+
function registerBillingTools(server, baseCtx) {
|
|
2089
2191
|
server.registerTool(
|
|
2090
2192
|
"list_billing_plans",
|
|
2091
2193
|
{
|
|
2092
2194
|
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.",
|
|
2093
|
-
inputSchema: {},
|
|
2195
|
+
inputSchema: { projectDir: projectDirInput },
|
|
2094
2196
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
2095
2197
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true }
|
|
2096
2198
|
},
|
|
2097
|
-
async () => {
|
|
2199
|
+
async (args) => {
|
|
2098
2200
|
try {
|
|
2201
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
2099
2202
|
const catalog = await ctx.client.getBillingPlanCatalog();
|
|
2100
2203
|
return structuredToolResult({
|
|
2101
2204
|
schemaVersion: 1,
|
|
@@ -2115,14 +2218,16 @@ function registerBillingTools(server, ctx) {
|
|
|
2115
2218
|
{
|
|
2116
2219
|
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.",
|
|
2117
2220
|
inputSchema: {
|
|
2221
|
+
projectDir: projectDirInput,
|
|
2118
2222
|
targetPlan: plan,
|
|
2119
|
-
operationId:
|
|
2223
|
+
operationId: z4.string().min(1)
|
|
2120
2224
|
},
|
|
2121
2225
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
2122
2226
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
|
|
2123
2227
|
},
|
|
2124
2228
|
async (args) => {
|
|
2125
2229
|
try {
|
|
2230
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
2126
2231
|
const site = requireSiteFile(ctx);
|
|
2127
2232
|
const result = await ctx.client.changeSubscriptionPlan(site.credential, {
|
|
2128
2233
|
siteId: site.siteId,
|
|
@@ -2152,39 +2257,40 @@ function registerBillingTools(server, ctx) {
|
|
|
2152
2257
|
}
|
|
2153
2258
|
|
|
2154
2259
|
// src/tools/lifecycle.ts
|
|
2155
|
-
import { z as
|
|
2156
|
-
var deleteConfirmation =
|
|
2157
|
-
siteId:
|
|
2158
|
-
expectedSiteUpdatedAt:
|
|
2159
|
-
expectedStatus:
|
|
2160
|
-
expectedMode:
|
|
2161
|
-
expectedServingMode:
|
|
2162
|
-
expectedShortId:
|
|
2163
|
-
expectedSubscriptionStatus:
|
|
2164
|
-
expectedPlan:
|
|
2165
|
-
expectedCancelAtPeriodEnd:
|
|
2166
|
-
expectedCurrentPeriodEnd:
|
|
2167
|
-
expectedLastDeploymentId:
|
|
2168
|
-
expectedBoundHostnames:
|
|
2169
|
-
acknowledge:
|
|
2260
|
+
import { z as z5 } from "zod";
|
|
2261
|
+
var deleteConfirmation = z5.object({
|
|
2262
|
+
siteId: z5.string().min(1),
|
|
2263
|
+
expectedSiteUpdatedAt: z5.string().datetime(),
|
|
2264
|
+
expectedStatus: z5.enum(["active", "expired", "deleted"]),
|
|
2265
|
+
expectedMode: z5.enum(["free", "paid"]),
|
|
2266
|
+
expectedServingMode: z5.enum(["normal", "over_limit_notice", "risk_notice", "stopped"]),
|
|
2267
|
+
expectedShortId: z5.string().optional(),
|
|
2268
|
+
expectedSubscriptionStatus: z5.enum(["incomplete", "active", "past_due", "canceled"]).optional(),
|
|
2269
|
+
expectedPlan: z5.enum(["water", "personal", "share", "business"]).optional(),
|
|
2270
|
+
expectedCancelAtPeriodEnd: z5.boolean().optional(),
|
|
2271
|
+
expectedCurrentPeriodEnd: z5.string().datetime().optional(),
|
|
2272
|
+
expectedLastDeploymentId: z5.string().optional(),
|
|
2273
|
+
expectedBoundHostnames: z5.array(z5.string()),
|
|
2274
|
+
acknowledge: z5.literal("delete_site_and_cancel_renewal")
|
|
2170
2275
|
});
|
|
2171
|
-
var unbindConfirmation =
|
|
2172
|
-
siteId:
|
|
2173
|
-
bindingId:
|
|
2174
|
-
expectedBindingUpdatedAt:
|
|
2175
|
-
expectedBindingStatus:
|
|
2176
|
-
apexDomain:
|
|
2177
|
-
expectedBoundHostnames:
|
|
2178
|
-
acknowledge:
|
|
2276
|
+
var unbindConfirmation = z5.object({
|
|
2277
|
+
siteId: z5.string().min(1),
|
|
2278
|
+
bindingId: z5.string().min(1),
|
|
2279
|
+
expectedBindingUpdatedAt: z5.string().datetime(),
|
|
2280
|
+
expectedBindingStatus: z5.enum(["provisioning", "active"]),
|
|
2281
|
+
apexDomain: z5.string().min(1),
|
|
2282
|
+
expectedBoundHostnames: z5.array(z5.string()),
|
|
2283
|
+
acknowledge: z5.literal("unbind_domain_and_remove_custom_hostnames")
|
|
2179
2284
|
});
|
|
2180
|
-
function registerLifecycleTools(server,
|
|
2285
|
+
function registerLifecycleTools(server, baseCtx) {
|
|
2181
2286
|
server.registerTool(
|
|
2182
2287
|
"delete_site",
|
|
2183
2288
|
{
|
|
2184
2289
|
description: "Preview or execute deletion of this Sakupa site. Execution requires an exact server-validated confirmation bound to the current site state.",
|
|
2185
2290
|
inputSchema: {
|
|
2186
|
-
|
|
2187
|
-
|
|
2291
|
+
projectDir: projectDirInput,
|
|
2292
|
+
action: z5.enum(["preview", "confirm"]),
|
|
2293
|
+
operationId: z5.string().min(1).optional(),
|
|
2188
2294
|
confirmation: deleteConfirmation.optional()
|
|
2189
2295
|
},
|
|
2190
2296
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
@@ -2192,6 +2298,7 @@ function registerLifecycleTools(server, ctx) {
|
|
|
2192
2298
|
},
|
|
2193
2299
|
async (args) => {
|
|
2194
2300
|
try {
|
|
2301
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
2195
2302
|
const site = requireSiteFile(ctx);
|
|
2196
2303
|
if (!args.operationId) {
|
|
2197
2304
|
throw new Error("operationId is required for delete_site");
|
|
@@ -2218,13 +2325,14 @@ function registerLifecycleTools(server, ctx) {
|
|
|
2218
2325
|
confirmation: args.confirmation
|
|
2219
2326
|
});
|
|
2220
2327
|
deleteSiteFile(ctx.projectDir);
|
|
2328
|
+
removeCreation(site.siteId);
|
|
2221
2329
|
return structuredToolResult({
|
|
2222
2330
|
schemaVersion: 1,
|
|
2223
2331
|
outcome: result.servingDeletionPending ? "pending_provider" : "completed",
|
|
2224
2332
|
resultCode: "site_deleted",
|
|
2225
2333
|
operationId: args.operationId,
|
|
2226
|
-
summary:
|
|
2227
|
-
data: { result },
|
|
2334
|
+
summary: `Site deleted; the local management credential file was removed from ${ctx.projectDir}.`,
|
|
2335
|
+
data: { result, projectDir: ctx.projectDir },
|
|
2228
2336
|
nextActions: []
|
|
2229
2337
|
});
|
|
2230
2338
|
} catch (error) {
|
|
@@ -2237,8 +2345,9 @@ function registerLifecycleTools(server, ctx) {
|
|
|
2237
2345
|
{
|
|
2238
2346
|
description: "Preview or execute removal of the custom apex/www serving surface while preserving the subscription and permanent Sakupa URL.",
|
|
2239
2347
|
inputSchema: {
|
|
2240
|
-
|
|
2241
|
-
|
|
2348
|
+
projectDir: projectDirInput,
|
|
2349
|
+
action: z5.enum(["preview", "confirm"]),
|
|
2350
|
+
operationId: z5.string().min(1).optional(),
|
|
2242
2351
|
confirmation: unbindConfirmation.optional()
|
|
2243
2352
|
},
|
|
2244
2353
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
@@ -2246,6 +2355,7 @@ function registerLifecycleTools(server, ctx) {
|
|
|
2246
2355
|
},
|
|
2247
2356
|
async (args) => {
|
|
2248
2357
|
try {
|
|
2358
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
2249
2359
|
const site = requireSiteFile(ctx);
|
|
2250
2360
|
if (!args.operationId) throw new Error("operationId is required for unbind_domain");
|
|
2251
2361
|
if (args.action === "preview") {
|
|
@@ -2276,7 +2386,7 @@ function registerLifecycleTools(server, ctx) {
|
|
|
2276
2386
|
outcome: result.servingDeletionPending ? "pending_provider" : "completed",
|
|
2277
2387
|
resultCode: "domain_unbound",
|
|
2278
2388
|
operationId: args.operationId,
|
|
2279
|
-
summary:
|
|
2389
|
+
summary: `Custom domain unbound; the subscription, deployed content, and permanent Sakupa URL are unchanged. (project: ${ctx.projectDir})`,
|
|
2280
2390
|
data: { result },
|
|
2281
2391
|
nextActions: [{ tool: "site_status", allowed: true }]
|
|
2282
2392
|
});
|
|
@@ -2389,11 +2499,28 @@ Workflow:
|
|
|
2389
2499
|
5. create_support_ticket (subscribed sites) opens a support ticket; report_bug sends a
|
|
2390
2500
|
sanitized diagnostic report after the user explicitly confirms it.
|
|
2391
2501
|
|
|
2502
|
+
Project directory contract: ONE directory = ONE site (its .sakupa/site.json holds the
|
|
2503
|
+
binding). Every project-scoped tool accepts projectDir \u2014 ALWAYS pass the absolute path of
|
|
2504
|
+
the directory the user is currently working in, on every call. Without it the server falls
|
|
2505
|
+
back to its startup directory, which may be a different project than the one the user is
|
|
2506
|
+
looking at. analyze_site, deploy_site, site_status, refresh_site, delete_site and unbind_domain echo
|
|
2507
|
+
the directory they acted on \u2014 verify it matches the user's active project.
|
|
2508
|
+
|
|
2392
2509
|
Safety boundaries:
|
|
2393
2510
|
- Static output only: no SSR, API routes, middleware, server actions, databases or online builds.
|
|
2394
2511
|
- Never upload source projects, secrets, .env files, private keys, archives, videos or audio.
|
|
2395
2512
|
- Payment card data is entered only on Stripe-hosted pages \u2014 never through the AI tool.
|
|
2396
2513
|
- A subscription never grants domain ownership; only DNS verification does.
|
|
2514
|
+
- Never repeat, echo, or memorize the credential value from .sakupa/site.json \u2014 quoting it
|
|
2515
|
+
into the conversation copies the site's only key outside the protected local file. Read it
|
|
2516
|
+
only through the tools.
|
|
2517
|
+
- Before deploying, if the entry HTML lacks a lang attribute, add one matching the content
|
|
2518
|
+
language (infer it from the content) and then deploy; only skip when the user explicitly
|
|
2519
|
+
wants no lang attribute.
|
|
2520
|
+
- Prices are authoritative in JPY (Japanese yen). When talking with a user in a language
|
|
2521
|
+
other than Japanese, look up the approximate exchange rate and show an estimated local
|
|
2522
|
+
price next to the JPY amount, clearly marked as an estimate \u2014 Stripe always settles the
|
|
2523
|
+
real charge in JPY. Never show a bare Yen sign.
|
|
2397
2524
|
- The management credential lives only in .sakupa/site.json; never share or upload it. Without
|
|
2398
2525
|
a bound custom domain, a lost credential is unrecoverable by design. manage_billing then opens
|
|
2399
2526
|
Stripe's public no-code portal login, where the customer verifies the checkout email with a
|
|
@@ -2431,7 +2558,7 @@ async function main() {
|
|
|
2431
2558
|
const transport = new StdioServerTransport();
|
|
2432
2559
|
await server.connect(transport);
|
|
2433
2560
|
console.error(
|
|
2434
|
-
`[sakupa-mcp] v${MCP_VERSION} connected (api: ${config.apiBaseUrl}, project: ${config.projectDir})`
|
|
2561
|
+
`[sakupa-mcp] v${MCP_VERSION} connected (api: ${config.apiBaseUrl}, default project: ${config.projectDir}; tools accept per-call projectDir)`
|
|
2435
2562
|
);
|
|
2436
2563
|
}
|
|
2437
2564
|
main().catch((err) => {
|