@sakupa/mcp 0.7.2 → 0.7.4
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 +106 -33
- package/dist/index.js +107 -34
- 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.4";
|
|
132
132
|
|
|
133
133
|
// ../core/dist/domain/errors.js
|
|
134
134
|
var HTTP_STATUS = {
|
|
@@ -444,6 +444,9 @@ function safeDecode(bytes) {
|
|
|
444
444
|
}
|
|
445
445
|
}
|
|
446
446
|
|
|
447
|
+
// ../core/dist/domain/credentials.js
|
|
448
|
+
var CREDENTIAL_PATTERN = /^sk_[A-Za-z0-9_-]{43}$/;
|
|
449
|
+
|
|
447
450
|
// ../core/dist/domain/hashing.js
|
|
448
451
|
async function sha256Hex(bytes) {
|
|
449
452
|
const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
|
|
@@ -458,10 +461,6 @@ var CREDENTIAL_HEADER = "x-sakupa-credential";
|
|
|
458
461
|
var IDEMPOTENCY_HEADER = "x-sakupa-idempotency-key";
|
|
459
462
|
var MCP_VERSION_HEADER = "x-sakupa-mcp-version";
|
|
460
463
|
|
|
461
|
-
// ../core/dist/services/sites.js
|
|
462
|
-
var utf8Decoder = new TextDecoder("utf-8", { fatal: false });
|
|
463
|
-
var utf8Encoder = new TextEncoder();
|
|
464
|
-
|
|
465
464
|
// ../core/dist/services/subscriptions.js
|
|
466
465
|
var WEBHOOK_PROCESSING_LEASE_MS = 5 * 60 * 1e3;
|
|
467
466
|
|
|
@@ -1128,20 +1127,42 @@ var SITE_FILE = "site.json";
|
|
|
1128
1127
|
function siteFilePath(projectDir) {
|
|
1129
1128
|
return join2(projectDir, SITE_DIR, SITE_FILE);
|
|
1130
1129
|
}
|
|
1131
|
-
function
|
|
1130
|
+
function loadSiteFile(projectDir) {
|
|
1132
1131
|
const path = siteFilePath(projectDir);
|
|
1132
|
+
if (!existsSync(path)) return { kind: "absent" };
|
|
1133
1133
|
let raw;
|
|
1134
1134
|
try {
|
|
1135
1135
|
raw = readFileSync(path, "utf8");
|
|
1136
|
-
} catch {
|
|
1137
|
-
return
|
|
1136
|
+
} catch (err) {
|
|
1137
|
+
return {
|
|
1138
|
+
kind: "corrupted",
|
|
1139
|
+
problem: `the file exists but could not be read (${err instanceof Error ? err.message : String(err)})`
|
|
1140
|
+
};
|
|
1138
1141
|
}
|
|
1142
|
+
let parsed;
|
|
1139
1143
|
try {
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
+
parsed = JSON.parse(raw);
|
|
1145
|
+
} catch {
|
|
1146
|
+
return { kind: "corrupted", problem: "the file exists but is not valid JSON" };
|
|
1147
|
+
}
|
|
1148
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
1149
|
+
return { kind: "corrupted", problem: "the file does not contain a JSON object" };
|
|
1150
|
+
}
|
|
1151
|
+
if (typeof parsed.siteId !== "string" || parsed.siteId.length === 0) {
|
|
1152
|
+
return { kind: "corrupted", problem: "the siteId field is missing or empty" };
|
|
1153
|
+
}
|
|
1154
|
+
if (typeof parsed.credential !== "string" || parsed.credential.length === 0) {
|
|
1155
|
+
return { kind: "corrupted", problem: "the credential field is missing or empty" };
|
|
1156
|
+
}
|
|
1157
|
+
if (!CREDENTIAL_PATTERN.test(parsed.credential)) {
|
|
1144
1158
|
return {
|
|
1159
|
+
kind: "corrupted",
|
|
1160
|
+
problem: "the credential does not match the shape Sakupa issues (sk_ followed by exactly 43 URL-safe base64 characters) \u2014 it looks locally altered or damaged"
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
return {
|
|
1164
|
+
kind: "ok",
|
|
1165
|
+
file: {
|
|
1145
1166
|
siteId: parsed.siteId,
|
|
1146
1167
|
credential: parsed.credential,
|
|
1147
1168
|
createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : "",
|
|
@@ -1149,12 +1170,26 @@ function readSiteFile(projectDir) {
|
|
|
1149
1170
|
...typeof parsed.shortId === "string" ? { shortId: parsed.shortId } : {},
|
|
1150
1171
|
...typeof parsed.url === "string" ? { url: parsed.url } : {},
|
|
1151
1172
|
...typeof parsed.boundDomain === "string" ? { boundDomain: parsed.boundDomain } : {}
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
return null;
|
|
1155
|
-
}
|
|
1173
|
+
}
|
|
1174
|
+
};
|
|
1156
1175
|
}
|
|
1157
|
-
function
|
|
1176
|
+
function siteFileRecoveryGuidance(projectDir) {
|
|
1177
|
+
return `The site itself is intact on the server; only the local binding file (${siteFilePath(projectDir)}) is the problem. Restore the file (from a backup or by undoing the local edit). Do NOT delete it to work around the error: the credential is unrecoverable by design, so abandoning it permanently orphans the existing site. Publishing this project as a brand-NEW site requires the user to manually delete the .sakupa directory first \u2014 the tool will never overwrite it.`;
|
|
1178
|
+
}
|
|
1179
|
+
function writeSiteFile(projectDir, file, opts = {}) {
|
|
1180
|
+
if (opts.allowReplace !== true) {
|
|
1181
|
+
const existing = loadSiteFile(projectDir);
|
|
1182
|
+
if (existing.kind === "corrupted") {
|
|
1183
|
+
throw new Error(
|
|
1184
|
+
`Refusing to overwrite ${siteFilePath(projectDir)}: ${existing.problem}. ` + siteFileRecoveryGuidance(projectDir)
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
if (existing.kind === "ok" && existing.file.siteId !== file.siteId) {
|
|
1188
|
+
throw new Error(
|
|
1189
|
+
`Refusing to overwrite ${siteFilePath(projectDir)}: it already binds this project to site ${existing.file.siteId}. ` + siteFileRecoveryGuidance(projectDir)
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1158
1193
|
const dir = join2(projectDir, SITE_DIR);
|
|
1159
1194
|
mkdirSync(dir, { recursive: true });
|
|
1160
1195
|
const path = join2(dir, SITE_FILE);
|
|
@@ -1232,14 +1267,20 @@ function structuredToolResult(envelope) {
|
|
|
1232
1267
|
|
|
1233
1268
|
// src/tools/context.ts
|
|
1234
1269
|
function requireSiteFile(ctx) {
|
|
1235
|
-
const
|
|
1236
|
-
if (
|
|
1270
|
+
const state = loadSiteFile(ctx.projectDir);
|
|
1271
|
+
if (state.kind === "corrupted") {
|
|
1272
|
+
throw new SakupaError(
|
|
1273
|
+
"invalid_request",
|
|
1274
|
+
`.sakupa/site.json in ${ctx.projectDir} is damaged: ${state.problem}. ` + siteFileRecoveryGuidance(ctx.projectDir)
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
if (state.kind === "absent") {
|
|
1237
1278
|
throw new SakupaError(
|
|
1238
1279
|
"not_found",
|
|
1239
1280
|
`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.`
|
|
1240
1281
|
);
|
|
1241
1282
|
}
|
|
1242
|
-
return file;
|
|
1283
|
+
return state.file;
|
|
1243
1284
|
}
|
|
1244
1285
|
function toolError(e) {
|
|
1245
1286
|
const errorCode = isSakupaError(e) ? e.code : "internal";
|
|
@@ -1460,7 +1501,18 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1460
1501
|
const files = analysis.files;
|
|
1461
1502
|
const outputAbs = resolve2(ctx.projectDir, analysis.recommendedOutputDir ?? ".");
|
|
1462
1503
|
const manifest = await buildHashedManifest(files, outputAbs);
|
|
1463
|
-
const
|
|
1504
|
+
const siteFileState = loadSiteFile(ctx.projectDir);
|
|
1505
|
+
if (siteFileState.kind === "corrupted") {
|
|
1506
|
+
return text(
|
|
1507
|
+
"site_file_corrupted",
|
|
1508
|
+
`This project's .sakupa/site.json is damaged: ${siteFileState.problem}.
|
|
1509
|
+
|
|
1510
|
+
` + siteFileRecoveryGuidance(ctx.projectDir) + "\n\nNothing was deployed and no new site was created.",
|
|
1511
|
+
{ problem: siteFileState.problem },
|
|
1512
|
+
"blocked"
|
|
1513
|
+
);
|
|
1514
|
+
}
|
|
1515
|
+
const existing = siteFileState.kind === "ok" ? siteFileState.file : null;
|
|
1464
1516
|
if (!existing && args.publicConfirmed !== true) {
|
|
1465
1517
|
return text(
|
|
1466
1518
|
"public_deployment_confirmation_required",
|
|
@@ -1540,6 +1592,16 @@ ${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
|
|
|
1540
1592
|
try {
|
|
1541
1593
|
update = await updateOnce(false);
|
|
1542
1594
|
} catch (e) {
|
|
1595
|
+
if (isSakupaError(e) && e.code === "unauthorized") {
|
|
1596
|
+
return text(
|
|
1597
|
+
"credential_mismatch",
|
|
1598
|
+
`The server rejected the credential in .sakupa/site.json for site ${existing.siteId} \u2014 the local file no longer matches the server-side verifier (most often a local edit or merge damage).
|
|
1599
|
+
|
|
1600
|
+
` + siteFileRecoveryGuidance(ctx.projectDir) + "\n\nNothing was deployed and no new site was created.",
|
|
1601
|
+
{ siteId: existing.siteId },
|
|
1602
|
+
"blocked"
|
|
1603
|
+
);
|
|
1604
|
+
}
|
|
1543
1605
|
if (!isIncrementalReuseFailure(e)) throw e;
|
|
1544
1606
|
update = await updateOnce(true);
|
|
1545
1607
|
}
|
|
@@ -1890,13 +1952,17 @@ After the DNS record resolves, re-run recover_domain_site with verificationId: "
|
|
|
1890
1952
|
const res = await ctx.client.completeRecovery(args.verificationId, {
|
|
1891
1953
|
...args.preserveExistingCredentials !== void 0 ? { preserveExistingCredentials: args.preserveExistingCredentials } : {}
|
|
1892
1954
|
});
|
|
1893
|
-
writeSiteFile(
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1955
|
+
writeSiteFile(
|
|
1956
|
+
ctx.projectDir,
|
|
1957
|
+
{
|
|
1958
|
+
siteId: res.siteId,
|
|
1959
|
+
...res.boundHostnames[0] !== void 0 ? { boundDomain: res.boundHostnames[0] } : {},
|
|
1960
|
+
credential: res.credential,
|
|
1961
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1962
|
+
apiBaseUrl: ctx.apiBaseUrl
|
|
1963
|
+
},
|
|
1964
|
+
{ allowReplace: true }
|
|
1965
|
+
);
|
|
1900
1966
|
return text(
|
|
1901
1967
|
"domain_recovery_completed",
|
|
1902
1968
|
`Recovery complete.
|
|
@@ -1973,7 +2039,8 @@ ${res.archiveUrl}`,
|
|
|
1973
2039
|
},
|
|
1974
2040
|
async (args) => {
|
|
1975
2041
|
try {
|
|
1976
|
-
const
|
|
2042
|
+
const siteState = loadSiteFile(ctx.projectDir);
|
|
2043
|
+
const site = siteState.kind === "ok" ? siteState.file : null;
|
|
1977
2044
|
const diagnostics = {
|
|
1978
2045
|
toolName: args.toolName,
|
|
1979
2046
|
...args.errorCode !== void 0 ? { errorCode: args.errorCode } : {},
|
|
@@ -2301,9 +2368,11 @@ Workflow:
|
|
|
2301
2368
|
2. deploy_site \u2014 uploads ONLY the built static output. The first deploy creates a free temporary
|
|
2302
2369
|
site (public URL ${hostPattern}, valid 24 hours, free banner shown) and stores the
|
|
2303
2370
|
management credential in .sakupa/site.json. Deploying again updates the site and refreshes
|
|
2304
|
-
its validity; refresh_site extends validity without uploading
|
|
2305
|
-
|
|
2306
|
-
|
|
2371
|
+
its validity; refresh_site extends validity without uploading; site_status shows the
|
|
2372
|
+
current deployment and serving state at any time.
|
|
2373
|
+
3. To make the site PERMANENT, subscribe it to a monthly hosting plan (list_billing_plans
|
|
2374
|
+
shows the catalog; subscribe_site -> Stripe-hosted checkout;
|
|
2375
|
+
water/personal/share/business). Paying makes the
|
|
2307
2376
|
${hostPattern} URL permanent \u2014 that is what payment buys. Usage over the chosen plan
|
|
2308
2377
|
shows an over-limit notice by default. An external AI may periodically query usage and
|
|
2309
2378
|
recommend a plan, but Sakupa never changes a subscription automatically.
|
|
@@ -2311,9 +2380,13 @@ Workflow:
|
|
|
2311
2380
|
serving surface alongside the permanent URL. Ownership is proven only by DNS control; the
|
|
2312
2381
|
first verified request wins; unverified requests expire after 72 hours. billing_status,
|
|
2313
2382
|
change_subscription_plan, manage_billing and recover_domain_site manage the paid
|
|
2314
|
-
lifecycle
|
|
2383
|
+
lifecycle; unbind_domain removes the custom domain (the permanent URL keeps serving) and
|
|
2384
|
+
delete_site tears the whole site down after explicit confirmation.
|
|
2385
|
+
Plan changes are confirmed only on Stripe and synchronized by Stripe webhook.
|
|
2315
2386
|
A cancellation keeps the site paid through the current period. Sakupa reverts it to a free
|
|
2316
2387
|
24h site and removes paid data after Stripe sends the signed final-cancellation webhook.
|
|
2388
|
+
5. create_support_ticket (subscribed sites) opens a support ticket; report_bug sends a
|
|
2389
|
+
sanitized diagnostic report after the user explicitly confirms it.
|
|
2317
2390
|
|
|
2318
2391
|
Safety boundaries:
|
|
2319
2392
|
- Static output only: no SSR, API routes, middleware, server actions, databases or online builds.
|
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.4";
|
|
127
127
|
|
|
128
128
|
// ../core/dist/domain/errors.js
|
|
129
129
|
var HTTP_STATUS = {
|
|
@@ -439,6 +439,9 @@ function safeDecode(bytes) {
|
|
|
439
439
|
}
|
|
440
440
|
}
|
|
441
441
|
|
|
442
|
+
// ../core/dist/domain/credentials.js
|
|
443
|
+
var CREDENTIAL_PATTERN = /^sk_[A-Za-z0-9_-]{43}$/;
|
|
444
|
+
|
|
442
445
|
// ../core/dist/domain/hashing.js
|
|
443
446
|
async function sha256Hex(bytes) {
|
|
444
447
|
const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
|
|
@@ -453,10 +456,6 @@ var CREDENTIAL_HEADER = "x-sakupa-credential";
|
|
|
453
456
|
var IDEMPOTENCY_HEADER = "x-sakupa-idempotency-key";
|
|
454
457
|
var MCP_VERSION_HEADER = "x-sakupa-mcp-version";
|
|
455
458
|
|
|
456
|
-
// ../core/dist/services/sites.js
|
|
457
|
-
var utf8Decoder = new TextDecoder("utf-8", { fatal: false });
|
|
458
|
-
var utf8Encoder = new TextEncoder();
|
|
459
|
-
|
|
460
459
|
// ../core/dist/services/subscriptions.js
|
|
461
460
|
var WEBHOOK_PROCESSING_LEASE_MS = 5 * 60 * 1e3;
|
|
462
461
|
|
|
@@ -764,20 +763,42 @@ var SITE_FILE = "site.json";
|
|
|
764
763
|
function siteFilePath(projectDir) {
|
|
765
764
|
return join(projectDir, SITE_DIR, SITE_FILE);
|
|
766
765
|
}
|
|
767
|
-
function
|
|
766
|
+
function loadSiteFile(projectDir) {
|
|
768
767
|
const path = siteFilePath(projectDir);
|
|
768
|
+
if (!existsSync(path)) return { kind: "absent" };
|
|
769
769
|
let raw;
|
|
770
770
|
try {
|
|
771
771
|
raw = readFileSync(path, "utf8");
|
|
772
|
-
} catch {
|
|
773
|
-
return
|
|
772
|
+
} catch (err) {
|
|
773
|
+
return {
|
|
774
|
+
kind: "corrupted",
|
|
775
|
+
problem: `the file exists but could not be read (${err instanceof Error ? err.message : String(err)})`
|
|
776
|
+
};
|
|
774
777
|
}
|
|
778
|
+
let parsed;
|
|
775
779
|
try {
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
+
parsed = JSON.parse(raw);
|
|
781
|
+
} catch {
|
|
782
|
+
return { kind: "corrupted", problem: "the file exists but is not valid JSON" };
|
|
783
|
+
}
|
|
784
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
785
|
+
return { kind: "corrupted", problem: "the file does not contain a JSON object" };
|
|
786
|
+
}
|
|
787
|
+
if (typeof parsed.siteId !== "string" || parsed.siteId.length === 0) {
|
|
788
|
+
return { kind: "corrupted", problem: "the siteId field is missing or empty" };
|
|
789
|
+
}
|
|
790
|
+
if (typeof parsed.credential !== "string" || parsed.credential.length === 0) {
|
|
791
|
+
return { kind: "corrupted", problem: "the credential field is missing or empty" };
|
|
792
|
+
}
|
|
793
|
+
if (!CREDENTIAL_PATTERN.test(parsed.credential)) {
|
|
780
794
|
return {
|
|
795
|
+
kind: "corrupted",
|
|
796
|
+
problem: "the credential does not match the shape Sakupa issues (sk_ followed by exactly 43 URL-safe base64 characters) \u2014 it looks locally altered or damaged"
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
return {
|
|
800
|
+
kind: "ok",
|
|
801
|
+
file: {
|
|
781
802
|
siteId: parsed.siteId,
|
|
782
803
|
credential: parsed.credential,
|
|
783
804
|
createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : "",
|
|
@@ -785,12 +806,26 @@ function readSiteFile(projectDir) {
|
|
|
785
806
|
...typeof parsed.shortId === "string" ? { shortId: parsed.shortId } : {},
|
|
786
807
|
...typeof parsed.url === "string" ? { url: parsed.url } : {},
|
|
787
808
|
...typeof parsed.boundDomain === "string" ? { boundDomain: parsed.boundDomain } : {}
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
return null;
|
|
791
|
-
}
|
|
809
|
+
}
|
|
810
|
+
};
|
|
792
811
|
}
|
|
793
|
-
function
|
|
812
|
+
function siteFileRecoveryGuidance(projectDir) {
|
|
813
|
+
return `The site itself is intact on the server; only the local binding file (${siteFilePath(projectDir)}) is the problem. Restore the file (from a backup or by undoing the local edit). Do NOT delete it to work around the error: the credential is unrecoverable by design, so abandoning it permanently orphans the existing site. Publishing this project as a brand-NEW site requires the user to manually delete the .sakupa directory first \u2014 the tool will never overwrite it.`;
|
|
814
|
+
}
|
|
815
|
+
function writeSiteFile(projectDir, file, opts = {}) {
|
|
816
|
+
if (opts.allowReplace !== true) {
|
|
817
|
+
const existing = loadSiteFile(projectDir);
|
|
818
|
+
if (existing.kind === "corrupted") {
|
|
819
|
+
throw new Error(
|
|
820
|
+
`Refusing to overwrite ${siteFilePath(projectDir)}: ${existing.problem}. ` + siteFileRecoveryGuidance(projectDir)
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
if (existing.kind === "ok" && existing.file.siteId !== file.siteId) {
|
|
824
|
+
throw new Error(
|
|
825
|
+
`Refusing to overwrite ${siteFilePath(projectDir)}: it already binds this project to site ${existing.file.siteId}. ` + siteFileRecoveryGuidance(projectDir)
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
794
829
|
const dir = join(projectDir, SITE_DIR);
|
|
795
830
|
mkdirSync(dir, { recursive: true });
|
|
796
831
|
const path = join(dir, SITE_FILE);
|
|
@@ -1290,14 +1325,20 @@ function structuredToolResult(envelope) {
|
|
|
1290
1325
|
|
|
1291
1326
|
// src/tools/context.ts
|
|
1292
1327
|
function requireSiteFile(ctx) {
|
|
1293
|
-
const
|
|
1294
|
-
if (
|
|
1328
|
+
const state = loadSiteFile(ctx.projectDir);
|
|
1329
|
+
if (state.kind === "corrupted") {
|
|
1330
|
+
throw new SakupaError(
|
|
1331
|
+
"invalid_request",
|
|
1332
|
+
`.sakupa/site.json in ${ctx.projectDir} is damaged: ${state.problem}. ` + siteFileRecoveryGuidance(ctx.projectDir)
|
|
1333
|
+
);
|
|
1334
|
+
}
|
|
1335
|
+
if (state.kind === "absent") {
|
|
1295
1336
|
throw new SakupaError(
|
|
1296
1337
|
"not_found",
|
|
1297
1338
|
`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.`
|
|
1298
1339
|
);
|
|
1299
1340
|
}
|
|
1300
|
-
return file;
|
|
1341
|
+
return state.file;
|
|
1301
1342
|
}
|
|
1302
1343
|
function toolError(e) {
|
|
1303
1344
|
const errorCode = isSakupaError(e) ? e.code : "internal";
|
|
@@ -1522,7 +1563,18 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1522
1563
|
const files = analysis.files;
|
|
1523
1564
|
const outputAbs = resolve2(ctx.projectDir, analysis.recommendedOutputDir ?? ".");
|
|
1524
1565
|
const manifest = await buildHashedManifest(files, outputAbs);
|
|
1525
|
-
const
|
|
1566
|
+
const siteFileState = loadSiteFile(ctx.projectDir);
|
|
1567
|
+
if (siteFileState.kind === "corrupted") {
|
|
1568
|
+
return text(
|
|
1569
|
+
"site_file_corrupted",
|
|
1570
|
+
`This project's .sakupa/site.json is damaged: ${siteFileState.problem}.
|
|
1571
|
+
|
|
1572
|
+
` + siteFileRecoveryGuidance(ctx.projectDir) + "\n\nNothing was deployed and no new site was created.",
|
|
1573
|
+
{ problem: siteFileState.problem },
|
|
1574
|
+
"blocked"
|
|
1575
|
+
);
|
|
1576
|
+
}
|
|
1577
|
+
const existing = siteFileState.kind === "ok" ? siteFileState.file : null;
|
|
1526
1578
|
if (!existing && args.publicConfirmed !== true) {
|
|
1527
1579
|
return text(
|
|
1528
1580
|
"public_deployment_confirmation_required",
|
|
@@ -1602,6 +1654,16 @@ ${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
|
|
|
1602
1654
|
try {
|
|
1603
1655
|
update = await updateOnce(false);
|
|
1604
1656
|
} catch (e) {
|
|
1657
|
+
if (isSakupaError(e) && e.code === "unauthorized") {
|
|
1658
|
+
return text(
|
|
1659
|
+
"credential_mismatch",
|
|
1660
|
+
`The server rejected the credential in .sakupa/site.json for site ${existing.siteId} \u2014 the local file no longer matches the server-side verifier (most often a local edit or merge damage).
|
|
1661
|
+
|
|
1662
|
+
` + siteFileRecoveryGuidance(ctx.projectDir) + "\n\nNothing was deployed and no new site was created.",
|
|
1663
|
+
{ siteId: existing.siteId },
|
|
1664
|
+
"blocked"
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1605
1667
|
if (!isIncrementalReuseFailure(e)) throw e;
|
|
1606
1668
|
update = await updateOnce(true);
|
|
1607
1669
|
}
|
|
@@ -1952,13 +2014,17 @@ After the DNS record resolves, re-run recover_domain_site with verificationId: "
|
|
|
1952
2014
|
const res = await ctx.client.completeRecovery(args.verificationId, {
|
|
1953
2015
|
...args.preserveExistingCredentials !== void 0 ? { preserveExistingCredentials: args.preserveExistingCredentials } : {}
|
|
1954
2016
|
});
|
|
1955
|
-
writeSiteFile(
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
2017
|
+
writeSiteFile(
|
|
2018
|
+
ctx.projectDir,
|
|
2019
|
+
{
|
|
2020
|
+
siteId: res.siteId,
|
|
2021
|
+
...res.boundHostnames[0] !== void 0 ? { boundDomain: res.boundHostnames[0] } : {},
|
|
2022
|
+
credential: res.credential,
|
|
2023
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2024
|
+
apiBaseUrl: ctx.apiBaseUrl
|
|
2025
|
+
},
|
|
2026
|
+
{ allowReplace: true }
|
|
2027
|
+
);
|
|
1962
2028
|
return text(
|
|
1963
2029
|
"domain_recovery_completed",
|
|
1964
2030
|
`Recovery complete.
|
|
@@ -2035,7 +2101,8 @@ ${res.archiveUrl}`,
|
|
|
2035
2101
|
},
|
|
2036
2102
|
async (args) => {
|
|
2037
2103
|
try {
|
|
2038
|
-
const
|
|
2104
|
+
const siteState = loadSiteFile(ctx.projectDir);
|
|
2105
|
+
const site = siteState.kind === "ok" ? siteState.file : null;
|
|
2039
2106
|
const diagnostics = {
|
|
2040
2107
|
toolName: args.toolName,
|
|
2041
2108
|
...args.errorCode !== void 0 ? { errorCode: args.errorCode } : {},
|
|
@@ -2294,9 +2361,11 @@ Workflow:
|
|
|
2294
2361
|
2. deploy_site \u2014 uploads ONLY the built static output. The first deploy creates a free temporary
|
|
2295
2362
|
site (public URL ${hostPattern}, valid 24 hours, free banner shown) and stores the
|
|
2296
2363
|
management credential in .sakupa/site.json. Deploying again updates the site and refreshes
|
|
2297
|
-
its validity; refresh_site extends validity without uploading
|
|
2298
|
-
|
|
2299
|
-
|
|
2364
|
+
its validity; refresh_site extends validity without uploading; site_status shows the
|
|
2365
|
+
current deployment and serving state at any time.
|
|
2366
|
+
3. To make the site PERMANENT, subscribe it to a monthly hosting plan (list_billing_plans
|
|
2367
|
+
shows the catalog; subscribe_site -> Stripe-hosted checkout;
|
|
2368
|
+
water/personal/share/business). Paying makes the
|
|
2300
2369
|
${hostPattern} URL permanent \u2014 that is what payment buys. Usage over the chosen plan
|
|
2301
2370
|
shows an over-limit notice by default. An external AI may periodically query usage and
|
|
2302
2371
|
recommend a plan, but Sakupa never changes a subscription automatically.
|
|
@@ -2304,9 +2373,13 @@ Workflow:
|
|
|
2304
2373
|
serving surface alongside the permanent URL. Ownership is proven only by DNS control; the
|
|
2305
2374
|
first verified request wins; unverified requests expire after 72 hours. billing_status,
|
|
2306
2375
|
change_subscription_plan, manage_billing and recover_domain_site manage the paid
|
|
2307
|
-
lifecycle
|
|
2376
|
+
lifecycle; unbind_domain removes the custom domain (the permanent URL keeps serving) and
|
|
2377
|
+
delete_site tears the whole site down after explicit confirmation.
|
|
2378
|
+
Plan changes are confirmed only on Stripe and synchronized by Stripe webhook.
|
|
2308
2379
|
A cancellation keeps the site paid through the current period. Sakupa reverts it to a free
|
|
2309
2380
|
24h site and removes paid data after Stripe sends the signed final-cancellation webhook.
|
|
2381
|
+
5. create_support_ticket (subscribed sites) opens a support ticket; report_bug sends a
|
|
2382
|
+
sanitized diagnostic report after the user explicitly confirms it.
|
|
2310
2383
|
|
|
2311
2384
|
Safety boundaries:
|
|
2312
2385
|
- Static output only: no SSR, API routes, middleware, server actions, databases or online builds.
|
|
@@ -2352,7 +2425,7 @@ export {
|
|
|
2352
2425
|
createSakupaMcpServer,
|
|
2353
2426
|
deleteSiteFile,
|
|
2354
2427
|
loadMcpRuntimeConfig,
|
|
2355
|
-
|
|
2428
|
+
loadSiteFile,
|
|
2356
2429
|
registerLifecycleTools,
|
|
2357
2430
|
registerTools,
|
|
2358
2431
|
requireSiteFile,
|