@sakupa/mcp 0.7.5 → 0.7.7
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 +32 -64
- package/dist/index.js +32 -64
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -128,7 +128,7 @@ var FORBIDDEN_PATH_SEGMENTS = [
|
|
|
128
128
|
var ALLOWED_HIDDEN_PATHS = [".well-known/"];
|
|
129
129
|
|
|
130
130
|
// ../core/dist/domain/version.js
|
|
131
|
-
var SAKUPA_MCP_VERSION = "0.7.
|
|
131
|
+
var SAKUPA_MCP_VERSION = "0.7.7";
|
|
132
132
|
|
|
133
133
|
// ../core/dist/domain/errors.js
|
|
134
134
|
var HTTP_STATUS = {
|
|
@@ -407,22 +407,13 @@ function validateDeployableFiles(files, opts) {
|
|
|
407
407
|
severity: "warning",
|
|
408
408
|
code: "missing_html_lang",
|
|
409
409
|
path: entryHtmlPath,
|
|
410
|
-
message: 'The entry HTML has no lang attribute.
|
|
410
|
+
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
411
|
});
|
|
412
412
|
}
|
|
413
413
|
}
|
|
414
414
|
}
|
|
415
415
|
const jsCount = files.filter((f) => ["js", "mjs"].includes(fileExtension(f.path))).length;
|
|
416
416
|
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
417
|
const ok = issues.every((i) => i.severity !== "error");
|
|
427
418
|
return {
|
|
428
419
|
ok,
|
|
@@ -432,8 +423,7 @@ function validateDeployableFiles(files, opts) {
|
|
|
432
423
|
entryHtmlPath,
|
|
433
424
|
htmlLang,
|
|
434
425
|
supportedLang,
|
|
435
|
-
looksLikeSpa
|
|
436
|
-
spaFallbackConfirmationRequired
|
|
426
|
+
looksLikeSpa
|
|
437
427
|
};
|
|
438
428
|
}
|
|
439
429
|
function safeDecode(bytes) {
|
|
@@ -1052,7 +1042,7 @@ async function analyzeProject(projectDir, opts = {}) {
|
|
|
1052
1042
|
fileCount: 0,
|
|
1053
1043
|
issues: [],
|
|
1054
1044
|
ssrRisks,
|
|
1055
|
-
spa: { looksLikeSpa: false,
|
|
1045
|
+
spa: { looksLikeSpa: false, autoFallback: false },
|
|
1056
1046
|
deployable: false,
|
|
1057
1047
|
suggestedNextAction: suggestedNextAction2
|
|
1058
1048
|
};
|
|
@@ -1072,30 +1062,23 @@ async function analyzeProject(projectDir, opts = {}) {
|
|
|
1072
1062
|
}
|
|
1073
1063
|
candidates.push({ path: file.path, size: file.size, ...content ? { content } : {} });
|
|
1074
1064
|
}
|
|
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
|
-
});
|
|
1065
|
+
const validation = validateDeployableFiles(candidates, { mode: "free" });
|
|
1080
1066
|
ssrRisks.push(...serverAndDbDepRisks(pkg, true));
|
|
1081
1067
|
const deployable = validation.ok && walked.length > 0;
|
|
1082
1068
|
const spa = {
|
|
1083
1069
|
looksLikeSpa: validation.looksLikeSpa,
|
|
1084
|
-
|
|
1085
|
-
confirmationRequired: validation.spaFallbackConfirmationRequired
|
|
1070
|
+
autoFallback: validation.looksLikeSpa
|
|
1086
1071
|
};
|
|
1087
1072
|
let suggestedNextAction;
|
|
1088
1073
|
if (!deployable) {
|
|
1089
1074
|
const firstError = validation.issues.find((i) => i.severity === "error");
|
|
1090
1075
|
if (firstError?.code === "missing_index_html") {
|
|
1091
1076
|
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
1077
|
} else {
|
|
1095
1078
|
suggestedNextAction = "Fix the listed issues (remove forbidden/secret files, reduce size, add missing entry HTML), then re-run analyze_site.";
|
|
1096
1079
|
}
|
|
1097
|
-
} else if (spa.
|
|
1098
|
-
suggestedNextAction = `
|
|
1080
|
+
} else if (spa.looksLikeSpa) {
|
|
1081
|
+
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
1082
|
} else {
|
|
1100
1083
|
suggestedNextAction = `Run deploy_site to publish the static output in "${outputDirRel}".`;
|
|
1101
1084
|
}
|
|
@@ -1299,7 +1282,8 @@ function toolError(e) {
|
|
|
1299
1282
|
([key, value]) => safeDetailKeys.has(key) && (typeof value === "string" || typeof value === "number" || typeof value === "boolean")
|
|
1300
1283
|
)
|
|
1301
1284
|
) : void 0;
|
|
1302
|
-
const
|
|
1285
|
+
const minimumVersion = rawDetails && typeof rawDetails["minimumVersion"] === "string" ? rawDetails["minimumVersion"] : void 0;
|
|
1286
|
+
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 === "not_found" ? "The required local project binding or resource is unavailable; if this project has no .sakupa/site.json yet, run deploy_site first." : errorCode === "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." : errorCode === "invalid_request" || errorCode === "validation_failed" ? "The request arguments or local project checks did not pass." : errorCode === "state_conflict" ? "The resource state has changed; re-query the current status before deciding the next step." : errorCode === "confirmation_required" ? "The site or billing state changed, so the previous confirmation is stale; run the preview again and confirm against the fresh snapshot." : errorCode === "payment_required" ? "This operation requires an active subscription; check billing_status first." : errorCode === "rate_limited" ? "The server rate limit was reached; retry after the returned wait time." : retryable ? "An upstream service is temporarily unavailable or busy; retry shortly." : "The operation failed; no server-internal details are exposed.";
|
|
1303
1287
|
const result = structuredToolResult({
|
|
1304
1288
|
schemaVersion: 1,
|
|
1305
1289
|
outcome: "failed",
|
|
@@ -1341,7 +1325,7 @@ ${JSON.stringify(obj, null, 2)}`;
|
|
|
1341
1325
|
var planEnum = z2.enum(["water", "personal", "share", "business"]);
|
|
1342
1326
|
var severityEnum = z2.enum(["low", "medium", "high", "critical"]);
|
|
1343
1327
|
function planCatalog() {
|
|
1344
|
-
return TIER_ORDER.map((p) => `${p}
|
|
1328
|
+
return TIER_ORDER.map((p) => `${p} JPY ${tierPriceJpy(p)}/month`).join(", ");
|
|
1345
1329
|
}
|
|
1346
1330
|
var ticketCategoryEnum = z2.enum([
|
|
1347
1331
|
"billing",
|
|
@@ -1369,22 +1353,6 @@ Analysis:`,
|
|
|
1369
1353
|
"blocked"
|
|
1370
1354
|
);
|
|
1371
1355
|
}
|
|
1372
|
-
function spaConfirmationResult(analysis) {
|
|
1373
|
-
return text(
|
|
1374
|
-
"spa_fallback_confirmation_required",
|
|
1375
|
-
`SPA fallback confirmation required \u2014 nothing was deployed yet.
|
|
1376
|
-
|
|
1377
|
-
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.
|
|
1378
|
-
|
|
1379
|
-
Please ask the user to choose, then re-run deploy_site with:
|
|
1380
|
-
- spaFallback: true, spaFallbackConfirmed: true -> enable SPA fallback
|
|
1381
|
-
- spaFallbackConfirmed: true (spaFallback omitted or false) -> deploy WITHOUT fallback (unknown paths return 404)
|
|
1382
|
-
|
|
1383
|
-
Output directory: "${analysis.recommendedOutputDir ?? "."}", ${analysis.fileCount} files.`,
|
|
1384
|
-
{ analysis: analysisSummary(analysis), requestedConfirmation: "spa_fallback" },
|
|
1385
|
-
"waiting_user"
|
|
1386
|
-
);
|
|
1387
|
-
}
|
|
1388
1356
|
var MB2 = 1024 * 1024;
|
|
1389
1357
|
function ensureUploadSizeWithinLimits(manifest, isFirstFreeDeploy) {
|
|
1390
1358
|
const oversized = manifest.find((f) => f.size > MAX_SINGLE_FILE_BYTES);
|
|
@@ -1446,17 +1414,13 @@ function registerTools(server, ctx) {
|
|
|
1446
1414
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1447
1415
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
1448
1416
|
inputSchema: {
|
|
1449
|
-
outputDir: z2.string().optional().describe("Output directory relative to the project root (overrides detection).")
|
|
1450
|
-
spaFallbackRequested: z2.boolean().optional().describe("User asked for SPA fallback (unknown paths rewritten to index.html)."),
|
|
1451
|
-
spaFallbackConfirmed: z2.boolean().optional().describe("User explicitly confirmed the SPA fallback 404-behavior change.")
|
|
1417
|
+
outputDir: z2.string().optional().describe("Output directory relative to the project root (overrides detection).")
|
|
1452
1418
|
}
|
|
1453
1419
|
},
|
|
1454
1420
|
async (args) => {
|
|
1455
1421
|
try {
|
|
1456
1422
|
const analysis = await analyzeProject(ctx.projectDir, {
|
|
1457
|
-
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
|
|
1458
|
-
...args.spaFallbackRequested !== void 0 ? { spaFallbackRequested: args.spaFallbackRequested } : {},
|
|
1459
|
-
...args.spaFallbackConfirmed !== void 0 ? { spaFallbackConfirmed: args.spaFallbackConfirmed } : {}
|
|
1423
|
+
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
|
|
1460
1424
|
});
|
|
1461
1425
|
return textJson(
|
|
1462
1426
|
"site_analysis_completed",
|
|
@@ -1477,8 +1441,9 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1477
1441
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
1478
1442
|
inputSchema: {
|
|
1479
1443
|
outputDir: z2.string().optional().describe("Output directory relative to the project root (overrides detection)."),
|
|
1480
|
-
spaFallback: z2.boolean().optional().describe(
|
|
1481
|
-
|
|
1444
|
+
spaFallback: z2.boolean().optional().describe(
|
|
1445
|
+
"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."
|
|
1446
|
+
),
|
|
1482
1447
|
publicConfirmed: z2.boolean().optional().describe(
|
|
1483
1448
|
"Required only for the first deployment: user explicitly confirmed creation of a public 24-hour URL."
|
|
1484
1449
|
),
|
|
@@ -1488,13 +1453,8 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1488
1453
|
async (args) => {
|
|
1489
1454
|
try {
|
|
1490
1455
|
const analysis = await analyzeProject(ctx.projectDir, {
|
|
1491
|
-
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
|
|
1492
|
-
...args.spaFallback !== void 0 ? { spaFallbackRequested: args.spaFallback } : {},
|
|
1493
|
-
...args.spaFallbackConfirmed !== void 0 ? { spaFallbackConfirmed: args.spaFallbackConfirmed } : {}
|
|
1456
|
+
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
|
|
1494
1457
|
});
|
|
1495
|
-
if (analysis.spa.confirmationRequired && args.spaFallbackConfirmed !== true) {
|
|
1496
|
-
return spaConfirmationResult(analysis);
|
|
1497
|
-
}
|
|
1498
1458
|
if (!analysis.deployable || !analysis.files) {
|
|
1499
1459
|
return notDeployableResult(analysis);
|
|
1500
1460
|
}
|
|
@@ -1526,8 +1486,7 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1526
1486
|
const created = await ctx.client.createSite({
|
|
1527
1487
|
manifest,
|
|
1528
1488
|
...args.lang !== void 0 ? { lang: args.lang } : {},
|
|
1529
|
-
spaFallback: args.spaFallback
|
|
1530
|
-
...args.spaFallbackConfirmed !== void 0 ? { spaFallbackConfirmed: args.spaFallbackConfirmed } : {}
|
|
1489
|
+
...args.spaFallback !== void 0 ? { spaFallback: args.spaFallback } : {}
|
|
1531
1490
|
});
|
|
1532
1491
|
const uploaded2 = await uploadAll(ctx, created.uploadTargets, files, outputAbs);
|
|
1533
1492
|
const finalized2 = await ctx.client.finalizeDeployment(
|
|
@@ -1571,8 +1530,7 @@ ${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
|
|
|
1571
1530
|
const req = {
|
|
1572
1531
|
manifest,
|
|
1573
1532
|
...args.lang !== void 0 ? { lang: args.lang } : {},
|
|
1574
|
-
spaFallback: args.spaFallback
|
|
1575
|
-
...args.spaFallbackConfirmed !== void 0 ? { spaFallbackConfirmed: args.spaFallbackConfirmed } : {},
|
|
1533
|
+
...args.spaFallback !== void 0 ? { spaFallback: args.spaFallback } : {},
|
|
1576
1534
|
...forceFullUpload ? { forceFullUpload: true } : {}
|
|
1577
1535
|
};
|
|
1578
1536
|
const deployment = await ctx.client.createDeployment(
|
|
@@ -1647,7 +1605,7 @@ ${JSON.stringify(finalized.warnings, null, 2)}` : ""),
|
|
|
1647
1605
|
return text(
|
|
1648
1606
|
"site_refreshed",
|
|
1649
1607
|
`Site validity refreshed. New expiry: ${res.expiresAt}
|
|
1650
|
-
Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refresh.`,
|
|
1608
|
+
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.`,
|
|
1651
1609
|
{ siteId: site.siteId, expiresAt: res.expiresAt }
|
|
1652
1610
|
);
|
|
1653
1611
|
} catch (e) {
|
|
@@ -1698,7 +1656,7 @@ Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refre
|
|
|
1698
1656
|
);
|
|
1699
1657
|
return text(
|
|
1700
1658
|
"subscription_checkout_ready",
|
|
1701
|
-
`Stripe Checkout link \u2014 Sakupa Hosting for this site: ${res.plan} plan,
|
|
1659
|
+
`Stripe Checkout link \u2014 Sakupa Hosting for this site: ${res.plan} plan, JPY ${res.monthlyPriceJpy}/month (Japanese yen)
|
|
1702
1660
|
${res.checkoutUrl}
|
|
1703
1661
|
|
|
1704
1662
|
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.
|
|
@@ -1810,7 +1768,7 @@ Then run bind_domain again with verificationId: "${res.verificationId}" to check
|
|
|
1810
1768
|
const lines = [
|
|
1811
1769
|
`Billing status for site ${res.siteId} (mode: ${res.mode})`,
|
|
1812
1770
|
res.permanentUrl ? `Permanent URL: ${res.permanentUrl}` : void 0,
|
|
1813
|
-
res.plan ? `Plan: ${res.plan} (
|
|
1771
|
+
res.plan ? `Plan: ${res.plan} (JPY ${res.monthlyPriceJpy ?? tierPriceJpy(res.plan)}/month)` : "Plan: (no subscription yet)",
|
|
1814
1772
|
res.subscriptionStatus ? `Subscription payment state: ${res.subscriptionStatus}` : void 0,
|
|
1815
1773
|
res.cancelAtPeriodEnd ? "Renewal: CANCELED \u2014 the site reverts to free at the end of the already-paid month" : void 0,
|
|
1816
1774
|
res.currentPeriodStart ? `Current paid period: ${res.currentPeriodStart} -> ${res.currentPeriodEnd ?? "?"}` : void 0,
|
|
@@ -2393,6 +2351,16 @@ Safety boundaries:
|
|
|
2393
2351
|
- Never upload source projects, secrets, .env files, private keys, archives, videos or audio.
|
|
2394
2352
|
- Payment card data is entered only on Stripe-hosted pages \u2014 never through the AI tool.
|
|
2395
2353
|
- A subscription never grants domain ownership; only DNS verification does.
|
|
2354
|
+
- Never repeat, echo, or memorize the credential value from .sakupa/site.json \u2014 quoting it
|
|
2355
|
+
into the conversation copies the site's only key outside the protected local file. Read it
|
|
2356
|
+
only through the tools.
|
|
2357
|
+
- Before deploying, if the entry HTML lacks a lang attribute, add one matching the content
|
|
2358
|
+
language (infer it from the content) and then deploy; only skip when the user explicitly
|
|
2359
|
+
wants no lang attribute.
|
|
2360
|
+
- Prices are authoritative in JPY (Japanese yen). When talking with a user in a language
|
|
2361
|
+
other than Japanese, look up the approximate exchange rate and show an estimated local
|
|
2362
|
+
price next to the JPY amount, clearly marked as an estimate \u2014 Stripe always settles the
|
|
2363
|
+
real charge in JPY. Never show a bare Yen sign.
|
|
2396
2364
|
- The management credential lives only in .sakupa/site.json; never share or upload it. Without
|
|
2397
2365
|
a bound custom domain, a lost credential is unrecoverable by design. manage_billing then opens
|
|
2398
2366
|
Stripe's public no-code portal login, where the customer verifies the checkout email with a
|
package/dist/index.js
CHANGED
|
@@ -123,7 +123,7 @@ var FORBIDDEN_PATH_SEGMENTS = [
|
|
|
123
123
|
var ALLOWED_HIDDEN_PATHS = [".well-known/"];
|
|
124
124
|
|
|
125
125
|
// ../core/dist/domain/version.js
|
|
126
|
-
var SAKUPA_MCP_VERSION = "0.7.
|
|
126
|
+
var SAKUPA_MCP_VERSION = "0.7.7";
|
|
127
127
|
|
|
128
128
|
// ../core/dist/domain/errors.js
|
|
129
129
|
var HTTP_STATUS = {
|
|
@@ -402,22 +402,13 @@ function validateDeployableFiles(files, opts) {
|
|
|
402
402
|
severity: "warning",
|
|
403
403
|
code: "missing_html_lang",
|
|
404
404
|
path: entryHtmlPath,
|
|
405
|
-
message: 'The entry HTML has no lang attribute.
|
|
405
|
+
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
406
|
});
|
|
407
407
|
}
|
|
408
408
|
}
|
|
409
409
|
}
|
|
410
410
|
const jsCount = files.filter((f) => ["js", "mjs"].includes(fileExtension(f.path))).length;
|
|
411
411
|
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
412
|
const ok = issues.every((i) => i.severity !== "error");
|
|
422
413
|
return {
|
|
423
414
|
ok,
|
|
@@ -427,8 +418,7 @@ function validateDeployableFiles(files, opts) {
|
|
|
427
418
|
entryHtmlPath,
|
|
428
419
|
htmlLang,
|
|
429
420
|
supportedLang,
|
|
430
|
-
looksLikeSpa
|
|
431
|
-
spaFallbackConfirmationRequired
|
|
421
|
+
looksLikeSpa
|
|
432
422
|
};
|
|
433
423
|
}
|
|
434
424
|
function safeDecode(bytes) {
|
|
@@ -1215,7 +1205,7 @@ async function analyzeProject(projectDir, opts = {}) {
|
|
|
1215
1205
|
fileCount: 0,
|
|
1216
1206
|
issues: [],
|
|
1217
1207
|
ssrRisks,
|
|
1218
|
-
spa: { looksLikeSpa: false,
|
|
1208
|
+
spa: { looksLikeSpa: false, autoFallback: false },
|
|
1219
1209
|
deployable: false,
|
|
1220
1210
|
suggestedNextAction: suggestedNextAction2
|
|
1221
1211
|
};
|
|
@@ -1235,30 +1225,23 @@ async function analyzeProject(projectDir, opts = {}) {
|
|
|
1235
1225
|
}
|
|
1236
1226
|
candidates.push({ path: file.path, size: file.size, ...content ? { content } : {} });
|
|
1237
1227
|
}
|
|
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
|
-
});
|
|
1228
|
+
const validation = validateDeployableFiles(candidates, { mode: "free" });
|
|
1243
1229
|
ssrRisks.push(...serverAndDbDepRisks(pkg, true));
|
|
1244
1230
|
const deployable = validation.ok && walked.length > 0;
|
|
1245
1231
|
const spa = {
|
|
1246
1232
|
looksLikeSpa: validation.looksLikeSpa,
|
|
1247
|
-
|
|
1248
|
-
confirmationRequired: validation.spaFallbackConfirmationRequired
|
|
1233
|
+
autoFallback: validation.looksLikeSpa
|
|
1249
1234
|
};
|
|
1250
1235
|
let suggestedNextAction;
|
|
1251
1236
|
if (!deployable) {
|
|
1252
1237
|
const firstError = validation.issues.find((i) => i.severity === "error");
|
|
1253
1238
|
if (firstError?.code === "missing_index_html") {
|
|
1254
1239
|
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
1240
|
} else {
|
|
1258
1241
|
suggestedNextAction = "Fix the listed issues (remove forbidden/secret files, reduce size, add missing entry HTML), then re-run analyze_site.";
|
|
1259
1242
|
}
|
|
1260
|
-
} else if (spa.
|
|
1261
|
-
suggestedNextAction = `
|
|
1243
|
+
} else if (spa.looksLikeSpa) {
|
|
1244
|
+
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
1245
|
} else {
|
|
1263
1246
|
suggestedNextAction = `Run deploy_site to publish the static output in "${outputDirRel}".`;
|
|
1264
1247
|
}
|
|
@@ -1357,7 +1340,8 @@ function toolError(e) {
|
|
|
1357
1340
|
([key, value]) => safeDetailKeys.has(key) && (typeof value === "string" || typeof value === "number" || typeof value === "boolean")
|
|
1358
1341
|
)
|
|
1359
1342
|
) : void 0;
|
|
1360
|
-
const
|
|
1343
|
+
const minimumVersion = rawDetails && typeof rawDetails["minimumVersion"] === "string" ? rawDetails["minimumVersion"] : void 0;
|
|
1344
|
+
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 === "not_found" ? "The required local project binding or resource is unavailable; if this project has no .sakupa/site.json yet, run deploy_site first." : errorCode === "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." : errorCode === "invalid_request" || errorCode === "validation_failed" ? "The request arguments or local project checks did not pass." : errorCode === "state_conflict" ? "The resource state has changed; re-query the current status before deciding the next step." : errorCode === "confirmation_required" ? "The site or billing state changed, so the previous confirmation is stale; run the preview again and confirm against the fresh snapshot." : errorCode === "payment_required" ? "This operation requires an active subscription; check billing_status first." : errorCode === "rate_limited" ? "The server rate limit was reached; retry after the returned wait time." : retryable ? "An upstream service is temporarily unavailable or busy; retry shortly." : "The operation failed; no server-internal details are exposed.";
|
|
1361
1345
|
const result = structuredToolResult({
|
|
1362
1346
|
schemaVersion: 1,
|
|
1363
1347
|
outcome: "failed",
|
|
@@ -1403,7 +1387,7 @@ ${JSON.stringify(obj, null, 2)}`;
|
|
|
1403
1387
|
var planEnum = z2.enum(["water", "personal", "share", "business"]);
|
|
1404
1388
|
var severityEnum = z2.enum(["low", "medium", "high", "critical"]);
|
|
1405
1389
|
function planCatalog() {
|
|
1406
|
-
return TIER_ORDER.map((p) => `${p}
|
|
1390
|
+
return TIER_ORDER.map((p) => `${p} JPY ${tierPriceJpy(p)}/month`).join(", ");
|
|
1407
1391
|
}
|
|
1408
1392
|
var ticketCategoryEnum = z2.enum([
|
|
1409
1393
|
"billing",
|
|
@@ -1431,22 +1415,6 @@ Analysis:`,
|
|
|
1431
1415
|
"blocked"
|
|
1432
1416
|
);
|
|
1433
1417
|
}
|
|
1434
|
-
function spaConfirmationResult(analysis) {
|
|
1435
|
-
return text(
|
|
1436
|
-
"spa_fallback_confirmation_required",
|
|
1437
|
-
`SPA fallback confirmation required \u2014 nothing was deployed yet.
|
|
1438
|
-
|
|
1439
|
-
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.
|
|
1440
|
-
|
|
1441
|
-
Please ask the user to choose, then re-run deploy_site with:
|
|
1442
|
-
- spaFallback: true, spaFallbackConfirmed: true -> enable SPA fallback
|
|
1443
|
-
- spaFallbackConfirmed: true (spaFallback omitted or false) -> deploy WITHOUT fallback (unknown paths return 404)
|
|
1444
|
-
|
|
1445
|
-
Output directory: "${analysis.recommendedOutputDir ?? "."}", ${analysis.fileCount} files.`,
|
|
1446
|
-
{ analysis: analysisSummary(analysis), requestedConfirmation: "spa_fallback" },
|
|
1447
|
-
"waiting_user"
|
|
1448
|
-
);
|
|
1449
|
-
}
|
|
1450
1418
|
var MB2 = 1024 * 1024;
|
|
1451
1419
|
function ensureUploadSizeWithinLimits(manifest, isFirstFreeDeploy) {
|
|
1452
1420
|
const oversized = manifest.find((f) => f.size > MAX_SINGLE_FILE_BYTES);
|
|
@@ -1508,17 +1476,13 @@ function registerTools(server, ctx) {
|
|
|
1508
1476
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1509
1477
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
1510
1478
|
inputSchema: {
|
|
1511
|
-
outputDir: z2.string().optional().describe("Output directory relative to the project root (overrides detection).")
|
|
1512
|
-
spaFallbackRequested: z2.boolean().optional().describe("User asked for SPA fallback (unknown paths rewritten to index.html)."),
|
|
1513
|
-
spaFallbackConfirmed: z2.boolean().optional().describe("User explicitly confirmed the SPA fallback 404-behavior change.")
|
|
1479
|
+
outputDir: z2.string().optional().describe("Output directory relative to the project root (overrides detection).")
|
|
1514
1480
|
}
|
|
1515
1481
|
},
|
|
1516
1482
|
async (args) => {
|
|
1517
1483
|
try {
|
|
1518
1484
|
const analysis = await analyzeProject(ctx.projectDir, {
|
|
1519
|
-
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
|
|
1520
|
-
...args.spaFallbackRequested !== void 0 ? { spaFallbackRequested: args.spaFallbackRequested } : {},
|
|
1521
|
-
...args.spaFallbackConfirmed !== void 0 ? { spaFallbackConfirmed: args.spaFallbackConfirmed } : {}
|
|
1485
|
+
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
|
|
1522
1486
|
});
|
|
1523
1487
|
return textJson(
|
|
1524
1488
|
"site_analysis_completed",
|
|
@@ -1539,8 +1503,9 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1539
1503
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
1540
1504
|
inputSchema: {
|
|
1541
1505
|
outputDir: z2.string().optional().describe("Output directory relative to the project root (overrides detection)."),
|
|
1542
|
-
spaFallback: z2.boolean().optional().describe(
|
|
1543
|
-
|
|
1506
|
+
spaFallback: z2.boolean().optional().describe(
|
|
1507
|
+
"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."
|
|
1508
|
+
),
|
|
1544
1509
|
publicConfirmed: z2.boolean().optional().describe(
|
|
1545
1510
|
"Required only for the first deployment: user explicitly confirmed creation of a public 24-hour URL."
|
|
1546
1511
|
),
|
|
@@ -1550,13 +1515,8 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1550
1515
|
async (args) => {
|
|
1551
1516
|
try {
|
|
1552
1517
|
const analysis = await analyzeProject(ctx.projectDir, {
|
|
1553
|
-
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
|
|
1554
|
-
...args.spaFallback !== void 0 ? { spaFallbackRequested: args.spaFallback } : {},
|
|
1555
|
-
...args.spaFallbackConfirmed !== void 0 ? { spaFallbackConfirmed: args.spaFallbackConfirmed } : {}
|
|
1518
|
+
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
|
|
1556
1519
|
});
|
|
1557
|
-
if (analysis.spa.confirmationRequired && args.spaFallbackConfirmed !== true) {
|
|
1558
|
-
return spaConfirmationResult(analysis);
|
|
1559
|
-
}
|
|
1560
1520
|
if (!analysis.deployable || !analysis.files) {
|
|
1561
1521
|
return notDeployableResult(analysis);
|
|
1562
1522
|
}
|
|
@@ -1588,8 +1548,7 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1588
1548
|
const created = await ctx.client.createSite({
|
|
1589
1549
|
manifest,
|
|
1590
1550
|
...args.lang !== void 0 ? { lang: args.lang } : {},
|
|
1591
|
-
spaFallback: args.spaFallback
|
|
1592
|
-
...args.spaFallbackConfirmed !== void 0 ? { spaFallbackConfirmed: args.spaFallbackConfirmed } : {}
|
|
1551
|
+
...args.spaFallback !== void 0 ? { spaFallback: args.spaFallback } : {}
|
|
1593
1552
|
});
|
|
1594
1553
|
const uploaded2 = await uploadAll(ctx, created.uploadTargets, files, outputAbs);
|
|
1595
1554
|
const finalized2 = await ctx.client.finalizeDeployment(
|
|
@@ -1633,8 +1592,7 @@ ${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
|
|
|
1633
1592
|
const req = {
|
|
1634
1593
|
manifest,
|
|
1635
1594
|
...args.lang !== void 0 ? { lang: args.lang } : {},
|
|
1636
|
-
spaFallback: args.spaFallback
|
|
1637
|
-
...args.spaFallbackConfirmed !== void 0 ? { spaFallbackConfirmed: args.spaFallbackConfirmed } : {},
|
|
1595
|
+
...args.spaFallback !== void 0 ? { spaFallback: args.spaFallback } : {},
|
|
1638
1596
|
...forceFullUpload ? { forceFullUpload: true } : {}
|
|
1639
1597
|
};
|
|
1640
1598
|
const deployment = await ctx.client.createDeployment(
|
|
@@ -1709,7 +1667,7 @@ ${JSON.stringify(finalized.warnings, null, 2)}` : ""),
|
|
|
1709
1667
|
return text(
|
|
1710
1668
|
"site_refreshed",
|
|
1711
1669
|
`Site validity refreshed. New expiry: ${res.expiresAt}
|
|
1712
|
-
Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refresh.`,
|
|
1670
|
+
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.`,
|
|
1713
1671
|
{ siteId: site.siteId, expiresAt: res.expiresAt }
|
|
1714
1672
|
);
|
|
1715
1673
|
} catch (e) {
|
|
@@ -1760,7 +1718,7 @@ Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refre
|
|
|
1760
1718
|
);
|
|
1761
1719
|
return text(
|
|
1762
1720
|
"subscription_checkout_ready",
|
|
1763
|
-
`Stripe Checkout link \u2014 Sakupa Hosting for this site: ${res.plan} plan,
|
|
1721
|
+
`Stripe Checkout link \u2014 Sakupa Hosting for this site: ${res.plan} plan, JPY ${res.monthlyPriceJpy}/month (Japanese yen)
|
|
1764
1722
|
${res.checkoutUrl}
|
|
1765
1723
|
|
|
1766
1724
|
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.
|
|
@@ -1872,7 +1830,7 @@ Then run bind_domain again with verificationId: "${res.verificationId}" to check
|
|
|
1872
1830
|
const lines = [
|
|
1873
1831
|
`Billing status for site ${res.siteId} (mode: ${res.mode})`,
|
|
1874
1832
|
res.permanentUrl ? `Permanent URL: ${res.permanentUrl}` : void 0,
|
|
1875
|
-
res.plan ? `Plan: ${res.plan} (
|
|
1833
|
+
res.plan ? `Plan: ${res.plan} (JPY ${res.monthlyPriceJpy ?? tierPriceJpy(res.plan)}/month)` : "Plan: (no subscription yet)",
|
|
1876
1834
|
res.subscriptionStatus ? `Subscription payment state: ${res.subscriptionStatus}` : void 0,
|
|
1877
1835
|
res.cancelAtPeriodEnd ? "Renewal: CANCELED \u2014 the site reverts to free at the end of the already-paid month" : void 0,
|
|
1878
1836
|
res.currentPeriodStart ? `Current paid period: ${res.currentPeriodStart} -> ${res.currentPeriodEnd ?? "?"}` : void 0,
|
|
@@ -2386,6 +2344,16 @@ Safety boundaries:
|
|
|
2386
2344
|
- Never upload source projects, secrets, .env files, private keys, archives, videos or audio.
|
|
2387
2345
|
- Payment card data is entered only on Stripe-hosted pages \u2014 never through the AI tool.
|
|
2388
2346
|
- A subscription never grants domain ownership; only DNS verification does.
|
|
2347
|
+
- Never repeat, echo, or memorize the credential value from .sakupa/site.json \u2014 quoting it
|
|
2348
|
+
into the conversation copies the site's only key outside the protected local file. Read it
|
|
2349
|
+
only through the tools.
|
|
2350
|
+
- Before deploying, if the entry HTML lacks a lang attribute, add one matching the content
|
|
2351
|
+
language (infer it from the content) and then deploy; only skip when the user explicitly
|
|
2352
|
+
wants no lang attribute.
|
|
2353
|
+
- Prices are authoritative in JPY (Japanese yen). When talking with a user in a language
|
|
2354
|
+
other than Japanese, look up the approximate exchange rate and show an estimated local
|
|
2355
|
+
price next to the JPY amount, clearly marked as an estimate \u2014 Stripe always settles the
|
|
2356
|
+
real charge in JPY. Never show a bare Yen sign.
|
|
2389
2357
|
- The management credential lives only in .sakupa/site.json; never share or upload it. Without
|
|
2390
2358
|
a bound custom domain, a lost credential is unrecoverable by design. manage_billing then opens
|
|
2391
2359
|
Stripe's public no-code portal login, where the customer verifies the checkout email with a
|