@sakupa/mcp 0.7.6 → 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.
Files changed (3) hide show
  1. package/dist/bin.js +30 -63
  2. package/dist/index.js +30 -63
  3. 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.6";
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. Add html lang="en" | "ja" | "zh-CN" so Sakupa surfaces match the site language.'
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, fallbackRecommended: false, confirmationRequired: 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
- fallbackRecommended: validation.looksLikeSpa,
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.confirmationRequired) {
1098
- suggestedNextAction = `The output in "${outputDirRel}" is deployable, but it looks like a single-page app. Decide about SPA fallback first: run deploy_site with spaFallback: true and spaFallbackConfirmed: true to enable it, or with spaFallbackConfirmed: true alone to deploy without fallback.`;
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
  }
@@ -1342,7 +1325,7 @@ ${JSON.stringify(obj, null, 2)}`;
1342
1325
  var planEnum = z2.enum(["water", "personal", "share", "business"]);
1343
1326
  var severityEnum = z2.enum(["low", "medium", "high", "critical"]);
1344
1327
  function planCatalog() {
1345
- return TIER_ORDER.map((p) => `${p} \xA5${tierPriceJpy(p)}/month`).join(", ");
1328
+ return TIER_ORDER.map((p) => `${p} JPY ${tierPriceJpy(p)}/month`).join(", ");
1346
1329
  }
1347
1330
  var ticketCategoryEnum = z2.enum([
1348
1331
  "billing",
@@ -1370,22 +1353,6 @@ Analysis:`,
1370
1353
  "blocked"
1371
1354
  );
1372
1355
  }
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
1356
  var MB2 = 1024 * 1024;
1390
1357
  function ensureUploadSizeWithinLimits(manifest, isFirstFreeDeploy) {
1391
1358
  const oversized = manifest.find((f) => f.size > MAX_SINGLE_FILE_BYTES);
@@ -1447,17 +1414,13 @@ function registerTools(server, ctx) {
1447
1414
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
1448
1415
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
1449
1416
  inputSchema: {
1450
- outputDir: z2.string().optional().describe("Output directory relative to the project root (overrides detection)."),
1451
- spaFallbackRequested: z2.boolean().optional().describe("User asked for SPA fallback (unknown paths rewritten to index.html)."),
1452
- 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).")
1453
1418
  }
1454
1419
  },
1455
1420
  async (args) => {
1456
1421
  try {
1457
1422
  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 } : {}
1423
+ ...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
1461
1424
  });
1462
1425
  return textJson(
1463
1426
  "site_analysis_completed",
@@ -1478,8 +1441,9 @@ Next action: ${analysis.suggestedNextAction}`,
1478
1441
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
1479
1442
  inputSchema: {
1480
1443
  outputDir: z2.string().optional().describe("Output directory relative to the project root (overrides detection)."),
1481
- spaFallback: z2.boolean().optional().describe("Enable SPA fallback (requires spaFallbackConfirmed: true)."),
1482
- spaFallbackConfirmed: z2.boolean().optional().describe("User explicitly confirmed the SPA fallback 404-behavior change."),
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
+ ),
1483
1447
  publicConfirmed: z2.boolean().optional().describe(
1484
1448
  "Required only for the first deployment: user explicitly confirmed creation of a public 24-hour URL."
1485
1449
  ),
@@ -1489,13 +1453,8 @@ Next action: ${analysis.suggestedNextAction}`,
1489
1453
  async (args) => {
1490
1454
  try {
1491
1455
  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 } : {}
1456
+ ...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
1495
1457
  });
1496
- if (analysis.spa.confirmationRequired && args.spaFallbackConfirmed !== true) {
1497
- return spaConfirmationResult(analysis);
1498
- }
1499
1458
  if (!analysis.deployable || !analysis.files) {
1500
1459
  return notDeployableResult(analysis);
1501
1460
  }
@@ -1527,8 +1486,7 @@ Next action: ${analysis.suggestedNextAction}`,
1527
1486
  const created = await ctx.client.createSite({
1528
1487
  manifest,
1529
1488
  ...args.lang !== void 0 ? { lang: args.lang } : {},
1530
- spaFallback: args.spaFallback === true,
1531
- ...args.spaFallbackConfirmed !== void 0 ? { spaFallbackConfirmed: args.spaFallbackConfirmed } : {}
1489
+ ...args.spaFallback !== void 0 ? { spaFallback: args.spaFallback } : {}
1532
1490
  });
1533
1491
  const uploaded2 = await uploadAll(ctx, created.uploadTargets, files, outputAbs);
1534
1492
  const finalized2 = await ctx.client.finalizeDeployment(
@@ -1572,8 +1530,7 @@ ${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
1572
1530
  const req = {
1573
1531
  manifest,
1574
1532
  ...args.lang !== void 0 ? { lang: args.lang } : {},
1575
- spaFallback: args.spaFallback === true,
1576
- ...args.spaFallbackConfirmed !== void 0 ? { spaFallbackConfirmed: args.spaFallbackConfirmed } : {},
1533
+ ...args.spaFallback !== void 0 ? { spaFallback: args.spaFallback } : {},
1577
1534
  ...forceFullUpload ? { forceFullUpload: true } : {}
1578
1535
  };
1579
1536
  const deployment = await ctx.client.createDeployment(
@@ -1648,7 +1605,7 @@ ${JSON.stringify(finalized.warnings, null, 2)}` : ""),
1648
1605
  return text(
1649
1606
  "site_refreshed",
1650
1607
  `Site validity refreshed. New expiry: ${res.expiresAt}
1651
- 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.`,
1652
1609
  { siteId: site.siteId, expiresAt: res.expiresAt }
1653
1610
  );
1654
1611
  } catch (e) {
@@ -1699,7 +1656,7 @@ Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refre
1699
1656
  );
1700
1657
  return text(
1701
1658
  "subscription_checkout_ready",
1702
- `Stripe Checkout link \u2014 Sakupa Hosting for this site: ${res.plan} plan, \xA5${res.monthlyPriceJpy}/month
1659
+ `Stripe Checkout link \u2014 Sakupa Hosting for this site: ${res.plan} plan, JPY ${res.monthlyPriceJpy}/month (Japanese yen)
1703
1660
  ${res.checkoutUrl}
1704
1661
 
1705
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.
@@ -1811,7 +1768,7 @@ Then run bind_domain again with verificationId: "${res.verificationId}" to check
1811
1768
  const lines = [
1812
1769
  `Billing status for site ${res.siteId} (mode: ${res.mode})`,
1813
1770
  res.permanentUrl ? `Permanent URL: ${res.permanentUrl}` : void 0,
1814
- res.plan ? `Plan: ${res.plan} (\xA5${res.monthlyPriceJpy ?? tierPriceJpy(res.plan)}/month)` : "Plan: (no subscription yet)",
1771
+ res.plan ? `Plan: ${res.plan} (JPY ${res.monthlyPriceJpy ?? tierPriceJpy(res.plan)}/month)` : "Plan: (no subscription yet)",
1815
1772
  res.subscriptionStatus ? `Subscription payment state: ${res.subscriptionStatus}` : void 0,
1816
1773
  res.cancelAtPeriodEnd ? "Renewal: CANCELED \u2014 the site reverts to free at the end of the already-paid month" : void 0,
1817
1774
  res.currentPeriodStart ? `Current paid period: ${res.currentPeriodStart} -> ${res.currentPeriodEnd ?? "?"}` : void 0,
@@ -2394,6 +2351,16 @@ Safety boundaries:
2394
2351
  - Never upload source projects, secrets, .env files, private keys, archives, videos or audio.
2395
2352
  - Payment card data is entered only on Stripe-hosted pages \u2014 never through the AI tool.
2396
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.
2397
2364
  - The management credential lives only in .sakupa/site.json; never share or upload it. Without
2398
2365
  a bound custom domain, a lost credential is unrecoverable by design. manage_billing then opens
2399
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.6";
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. Add html lang="en" | "ja" | "zh-CN" so Sakupa surfaces match the site language.'
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, fallbackRecommended: false, confirmationRequired: 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
- fallbackRecommended: validation.looksLikeSpa,
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.confirmationRequired) {
1261
- suggestedNextAction = `The output in "${outputDirRel}" is deployable, but it looks like a single-page app. Decide about SPA fallback first: run deploy_site with spaFallback: true and spaFallbackConfirmed: true to enable it, or with spaFallbackConfirmed: true alone to deploy without fallback.`;
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
  }
@@ -1404,7 +1387,7 @@ ${JSON.stringify(obj, null, 2)}`;
1404
1387
  var planEnum = z2.enum(["water", "personal", "share", "business"]);
1405
1388
  var severityEnum = z2.enum(["low", "medium", "high", "critical"]);
1406
1389
  function planCatalog() {
1407
- return TIER_ORDER.map((p) => `${p} \xA5${tierPriceJpy(p)}/month`).join(", ");
1390
+ return TIER_ORDER.map((p) => `${p} JPY ${tierPriceJpy(p)}/month`).join(", ");
1408
1391
  }
1409
1392
  var ticketCategoryEnum = z2.enum([
1410
1393
  "billing",
@@ -1432,22 +1415,6 @@ Analysis:`,
1432
1415
  "blocked"
1433
1416
  );
1434
1417
  }
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
1418
  var MB2 = 1024 * 1024;
1452
1419
  function ensureUploadSizeWithinLimits(manifest, isFirstFreeDeploy) {
1453
1420
  const oversized = manifest.find((f) => f.size > MAX_SINGLE_FILE_BYTES);
@@ -1509,17 +1476,13 @@ function registerTools(server, ctx) {
1509
1476
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
1510
1477
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
1511
1478
  inputSchema: {
1512
- outputDir: z2.string().optional().describe("Output directory relative to the project root (overrides detection)."),
1513
- spaFallbackRequested: z2.boolean().optional().describe("User asked for SPA fallback (unknown paths rewritten to index.html)."),
1514
- 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).")
1515
1480
  }
1516
1481
  },
1517
1482
  async (args) => {
1518
1483
  try {
1519
1484
  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 } : {}
1485
+ ...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
1523
1486
  });
1524
1487
  return textJson(
1525
1488
  "site_analysis_completed",
@@ -1540,8 +1503,9 @@ Next action: ${analysis.suggestedNextAction}`,
1540
1503
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
1541
1504
  inputSchema: {
1542
1505
  outputDir: z2.string().optional().describe("Output directory relative to the project root (overrides detection)."),
1543
- spaFallback: z2.boolean().optional().describe("Enable SPA fallback (requires spaFallbackConfirmed: true)."),
1544
- spaFallbackConfirmed: z2.boolean().optional().describe("User explicitly confirmed the SPA fallback 404-behavior change."),
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
+ ),
1545
1509
  publicConfirmed: z2.boolean().optional().describe(
1546
1510
  "Required only for the first deployment: user explicitly confirmed creation of a public 24-hour URL."
1547
1511
  ),
@@ -1551,13 +1515,8 @@ Next action: ${analysis.suggestedNextAction}`,
1551
1515
  async (args) => {
1552
1516
  try {
1553
1517
  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 } : {}
1518
+ ...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
1557
1519
  });
1558
- if (analysis.spa.confirmationRequired && args.spaFallbackConfirmed !== true) {
1559
- return spaConfirmationResult(analysis);
1560
- }
1561
1520
  if (!analysis.deployable || !analysis.files) {
1562
1521
  return notDeployableResult(analysis);
1563
1522
  }
@@ -1589,8 +1548,7 @@ Next action: ${analysis.suggestedNextAction}`,
1589
1548
  const created = await ctx.client.createSite({
1590
1549
  manifest,
1591
1550
  ...args.lang !== void 0 ? { lang: args.lang } : {},
1592
- spaFallback: args.spaFallback === true,
1593
- ...args.spaFallbackConfirmed !== void 0 ? { spaFallbackConfirmed: args.spaFallbackConfirmed } : {}
1551
+ ...args.spaFallback !== void 0 ? { spaFallback: args.spaFallback } : {}
1594
1552
  });
1595
1553
  const uploaded2 = await uploadAll(ctx, created.uploadTargets, files, outputAbs);
1596
1554
  const finalized2 = await ctx.client.finalizeDeployment(
@@ -1634,8 +1592,7 @@ ${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
1634
1592
  const req = {
1635
1593
  manifest,
1636
1594
  ...args.lang !== void 0 ? { lang: args.lang } : {},
1637
- spaFallback: args.spaFallback === true,
1638
- ...args.spaFallbackConfirmed !== void 0 ? { spaFallbackConfirmed: args.spaFallbackConfirmed } : {},
1595
+ ...args.spaFallback !== void 0 ? { spaFallback: args.spaFallback } : {},
1639
1596
  ...forceFullUpload ? { forceFullUpload: true } : {}
1640
1597
  };
1641
1598
  const deployment = await ctx.client.createDeployment(
@@ -1710,7 +1667,7 @@ ${JSON.stringify(finalized.warnings, null, 2)}` : ""),
1710
1667
  return text(
1711
1668
  "site_refreshed",
1712
1669
  `Site validity refreshed. New expiry: ${res.expiresAt}
1713
- 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.`,
1714
1671
  { siteId: site.siteId, expiresAt: res.expiresAt }
1715
1672
  );
1716
1673
  } catch (e) {
@@ -1761,7 +1718,7 @@ Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refre
1761
1718
  );
1762
1719
  return text(
1763
1720
  "subscription_checkout_ready",
1764
- `Stripe Checkout link \u2014 Sakupa Hosting for this site: ${res.plan} plan, \xA5${res.monthlyPriceJpy}/month
1721
+ `Stripe Checkout link \u2014 Sakupa Hosting for this site: ${res.plan} plan, JPY ${res.monthlyPriceJpy}/month (Japanese yen)
1765
1722
  ${res.checkoutUrl}
1766
1723
 
1767
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.
@@ -1873,7 +1830,7 @@ Then run bind_domain again with verificationId: "${res.verificationId}" to check
1873
1830
  const lines = [
1874
1831
  `Billing status for site ${res.siteId} (mode: ${res.mode})`,
1875
1832
  res.permanentUrl ? `Permanent URL: ${res.permanentUrl}` : void 0,
1876
- res.plan ? `Plan: ${res.plan} (\xA5${res.monthlyPriceJpy ?? tierPriceJpy(res.plan)}/month)` : "Plan: (no subscription yet)",
1833
+ res.plan ? `Plan: ${res.plan} (JPY ${res.monthlyPriceJpy ?? tierPriceJpy(res.plan)}/month)` : "Plan: (no subscription yet)",
1877
1834
  res.subscriptionStatus ? `Subscription payment state: ${res.subscriptionStatus}` : void 0,
1878
1835
  res.cancelAtPeriodEnd ? "Renewal: CANCELED \u2014 the site reverts to free at the end of the already-paid month" : void 0,
1879
1836
  res.currentPeriodStart ? `Current paid period: ${res.currentPeriodStart} -> ${res.currentPeriodEnd ?? "?"}` : void 0,
@@ -2387,6 +2344,16 @@ Safety boundaries:
2387
2344
  - Never upload source projects, secrets, .env files, private keys, archives, videos or audio.
2388
2345
  - Payment card data is entered only on Stripe-hosted pages \u2014 never through the AI tool.
2389
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.
2390
2357
  - The management credential lives only in .sakupa/site.json; never share or upload it. Without
2391
2358
  a bound custom domain, a lost credential is unrecoverable by design. manage_billing then opens
2392
2359
  Stripe's public no-code portal login, where the customer verifies the checkout email with a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sakupa/mcp",
3
- "version": "0.7.6",
3
+ "version": "0.7.7",
4
4
  "description": "Sakupa MCP server: publish AI-made static sites from your AI tool. AI-made pages, live in seconds.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",