@sakupa/mcp 0.7.9 → 0.7.11
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 +143 -44
- package/dist/index.js +143 -44
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -129,7 +129,7 @@ var FORBIDDEN_PATH_SEGMENTS = [
|
|
|
129
129
|
var ALLOWED_HIDDEN_PATHS = [".well-known/"];
|
|
130
130
|
|
|
131
131
|
// ../core/dist/domain/version.js
|
|
132
|
-
var SAKUPA_MCP_VERSION = "0.7.
|
|
132
|
+
var SAKUPA_MCP_VERSION = "0.7.11";
|
|
133
133
|
|
|
134
134
|
// ../core/dist/domain/errors.js
|
|
135
135
|
var HTTP_STATUS = {
|
|
@@ -686,7 +686,7 @@ var HttpApiClient = class {
|
|
|
686
686
|
|
|
687
687
|
// src/tools/definitions.ts
|
|
688
688
|
import { randomUUID } from "node:crypto";
|
|
689
|
-
import { promises as fs2 } from "node:fs";
|
|
689
|
+
import { existsSync as existsSync3, promises as fs2 } from "node:fs";
|
|
690
690
|
import { join as join4, resolve as resolve3 } from "node:path";
|
|
691
691
|
import { z as z3 } from "zod";
|
|
692
692
|
|
|
@@ -1196,14 +1196,18 @@ function deleteSiteFile(projectDir) {
|
|
|
1196
1196
|
rmSync(path, { force: true });
|
|
1197
1197
|
}
|
|
1198
1198
|
}
|
|
1199
|
-
function
|
|
1200
|
-
let
|
|
1201
|
-
for (; ; ) {
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
if (parent
|
|
1205
|
-
|
|
1199
|
+
function findAncestor(startDir, predicate, maxLevels = Number.POSITIVE_INFINITY) {
|
|
1200
|
+
let cursor = startDir;
|
|
1201
|
+
for (let i = 0; i < maxLevels; i += 1) {
|
|
1202
|
+
const parent = dirname(cursor);
|
|
1203
|
+
if (parent === cursor) return null;
|
|
1204
|
+
if (predicate(parent)) return parent;
|
|
1205
|
+
cursor = parent;
|
|
1206
1206
|
}
|
|
1207
|
+
return null;
|
|
1208
|
+
}
|
|
1209
|
+
function isInsideGitRepo(projectDir) {
|
|
1210
|
+
return existsSync(join2(projectDir, ".git")) || findAncestor(projectDir, (dir) => existsSync(join2(dir, ".git"))) !== null;
|
|
1207
1211
|
}
|
|
1208
1212
|
function credentialGitReminder(projectDir) {
|
|
1209
1213
|
if (!isInsideGitRepo(projectDir)) return "";
|
|
@@ -1354,10 +1358,30 @@ async function diagnoseBinding(input) {
|
|
|
1354
1358
|
const apexResolves = apexAnswers.length > 0;
|
|
1355
1359
|
const allOk = checks.every((c) => c.state === "ok");
|
|
1356
1360
|
const checklist = checks.map(renderCheck).join("\n") + `
|
|
1357
|
-
[${apexResolves ? "OK" : "MISSING"}] APEX ${apex} \u2014 ` + (apexResolves ? "resolves." : `does not resolve yet
|
|
1361
|
+
[${apexResolves ? "OK" : "MISSING"}] APEX ${apex} \u2014 ` + (apexResolves ? "resolves." : `does not resolve yet. Three-step fix, stop at the first that works: (1) try adding a plain record \u2014 type CNAME, host @, value ${input.servingTarget} (most panels and Cloudflare accept this directly; confirm past any MX-conflict warning if the domain sends no email). (2) If rejected, look for ALIAS / ANAME / CNAME-flattening in the record-type list \u2014 same host and value. (3) If the panel has neither, skip the apex: www alone works fine (certificates do not depend on the apex record); optionally add a URL redirect from @ to www.`);
|
|
1358
1362
|
const layers = `Pipeline: [1] public DNS (checked LIVE above) -> [2] Sakupa ownership verification: ${input.verificationStatus} -> [3] HTTPS certificate & serving: ` + (input.provisioning ? "provisioning (Cloudflare validates and issues within minutes once the records above are all OK; Sakupa retries automatically every ~5 minutes)." : "starts after verification.");
|
|
1359
1363
|
return { checks, apexResolves, allOk, checklist, layers };
|
|
1360
1364
|
}
|
|
1365
|
+
var DNS_RETRY_AFTER_SECONDS = 300;
|
|
1366
|
+
var DNS_MAX_ATTEMPTS = 10;
|
|
1367
|
+
function toDnsChecklist(checks) {
|
|
1368
|
+
return checks.map((c) => ({
|
|
1369
|
+
name: c.record.name,
|
|
1370
|
+
type: c.record.type,
|
|
1371
|
+
shortHost: c.shortHost,
|
|
1372
|
+
state: c.state,
|
|
1373
|
+
...c.fix ? { fix: c.fix } : {}
|
|
1374
|
+
}));
|
|
1375
|
+
}
|
|
1376
|
+
function renderChecklistBlock(diag, fixTail) {
|
|
1377
|
+
const cadence = `Re-check every ${DNS_RETRY_AFTER_SECONDS / 60} minutes, up to ${DNS_MAX_ATTEMPTS} times.`;
|
|
1378
|
+
return `Live DNS checklist (host values are the SHORT panel form):
|
|
1379
|
+
${diag.checklist}
|
|
1380
|
+
|
|
1381
|
+
${diag.layers}
|
|
1382
|
+
|
|
1383
|
+
` + (diag.allOk ? "All records are live; certificate issuance completes automatically \u2014 re-check in a few minutes until the binding is active." : `${fixTail} ${cadence}`);
|
|
1384
|
+
}
|
|
1361
1385
|
|
|
1362
1386
|
// src/version.ts
|
|
1363
1387
|
var MCP_VERSION = SAKUPA_MCP_VERSION;
|
|
@@ -1417,7 +1441,7 @@ var LocalGuidanceError = class extends SakupaError {
|
|
|
1417
1441
|
}
|
|
1418
1442
|
};
|
|
1419
1443
|
var projectDirInput = z2.string().optional().describe(
|
|
1420
|
-
"Absolute path of the
|
|
1444
|
+
"Absolute path of the user's PROJECT ROOT \u2014 the folder the user opened/works in (for framework projects: where package.json lives, NEVER the build-output subfolder like dist/out; the analyzer locates the output automatically). .sakupa/site.json lives here, so PASS THE SAME DIRECTORY EVERY TIME for the same project. When omitted the server falls back to its startup directory, which may not be where the user is working now."
|
|
1421
1445
|
);
|
|
1422
1446
|
function withProjectDir(ctx, projectDirArg) {
|
|
1423
1447
|
if (projectDirArg === void 0) return ctx;
|
|
@@ -1609,8 +1633,52 @@ async function uploadAll(ctx, targets, files, outputAbs) {
|
|
|
1609
1633
|
}
|
|
1610
1634
|
return targets.length;
|
|
1611
1635
|
}
|
|
1612
|
-
|
|
1613
|
-
|
|
1636
|
+
async function describePendingBinding(client, credential, pb) {
|
|
1637
|
+
const framing = `
|
|
1638
|
+
Domain binding IN PROGRESS: ${pb.apexDomain} \u2014 ` + (pb.phase === "provisioning" ? "ownership verified; certificates/serving are provisioning." : `awaiting DNS verification (challenge valid until ${pb.verificationExpiresAt}).`);
|
|
1639
|
+
let check;
|
|
1640
|
+
try {
|
|
1641
|
+
check = await client.checkVerification(pb.verificationId, credential);
|
|
1642
|
+
} catch {
|
|
1643
|
+
return {
|
|
1644
|
+
note: framing + "\n(Couldn't refresh binding progress from the server just now \u2014 try site_status again shortly.)"
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
try {
|
|
1648
|
+
const diag = await diagnoseBinding({
|
|
1649
|
+
apexDomain: check.apexDomain,
|
|
1650
|
+
servingTarget: check.servingTarget,
|
|
1651
|
+
...check.verificationRecord ? { verificationRecord: check.verificationRecord } : {},
|
|
1652
|
+
pendingDnsRecords: check.pendingDnsRecords,
|
|
1653
|
+
verificationStatus: check.status,
|
|
1654
|
+
provisioning: pb.phase === "provisioning"
|
|
1655
|
+
});
|
|
1656
|
+
const block = renderChecklistBlock(
|
|
1657
|
+
diag,
|
|
1658
|
+
"Relay every [MISSING]/[FIX] line to the user with its exact fix."
|
|
1659
|
+
);
|
|
1660
|
+
return { note: `${framing}
|
|
1661
|
+
${block}`, checklist: toDnsChecklist(diag.checks) };
|
|
1662
|
+
} catch {
|
|
1663
|
+
return {
|
|
1664
|
+
note: framing + '\n(Live DNS lookups are unavailable right now \u2014 run bind_domain with action "status" for the per-record checklist.)'
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
function projectRootAbove(projectDir) {
|
|
1669
|
+
if (existsSync3(join4(projectDir, "package.json"))) return null;
|
|
1670
|
+
return findAncestor(projectDir, (dir) => existsSync3(join4(dir, "package.json")), 4);
|
|
1671
|
+
}
|
|
1672
|
+
function findNeighborBinding(projectDir, outputRel) {
|
|
1673
|
+
const bound = (dir) => loadSiteFile(dir).kind !== "absent";
|
|
1674
|
+
const above = findAncestor(projectDir, bound, 3);
|
|
1675
|
+
if (above) return above;
|
|
1676
|
+
if (outputRel && outputRel !== ".") {
|
|
1677
|
+
const outputAbs = resolve3(projectDir, outputRel);
|
|
1678
|
+
if (bound(outputAbs)) return outputAbs;
|
|
1679
|
+
}
|
|
1680
|
+
return null;
|
|
1681
|
+
}
|
|
1614
1682
|
function freeSiteCreationBarrier() {
|
|
1615
1683
|
const recent = listRecentCreations(Date.now());
|
|
1616
1684
|
if (recent.length < FREE_ACTIVE_SITES_PER_IP) return null;
|
|
@@ -1671,6 +1739,9 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1671
1739
|
publicConfirmed: z3.boolean().optional().describe(
|
|
1672
1740
|
"Required only for the first deployment: user explicitly confirmed creation of a public 24-hour URL."
|
|
1673
1741
|
),
|
|
1742
|
+
subprojectConfirmed: z3.boolean().optional().describe(
|
|
1743
|
+
"Only when creating a NEW site in a subfolder of a package.json project: the user explicitly confirmed this subfolder is an INDEPENDENT site, not the project's build output."
|
|
1744
|
+
),
|
|
1674
1745
|
lang: z3.string().optional().describe("Site language override (en | ja | zh-CN); defaults to the html lang.")
|
|
1675
1746
|
}
|
|
1676
1747
|
},
|
|
@@ -1699,16 +1770,34 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1699
1770
|
}
|
|
1700
1771
|
const existing = siteFileState.kind === "ok" ? siteFileState.file : null;
|
|
1701
1772
|
if (!existing) {
|
|
1773
|
+
const rootAbove = args.subprojectConfirmed === true ? null : projectRootAbove(ctx.projectDir);
|
|
1774
|
+
if (rootAbove) {
|
|
1775
|
+
return text(
|
|
1776
|
+
"not_project_root",
|
|
1777
|
+
`${ctx.projectDir} has no package.json but ${rootAbove} does \u2014 this directory is a SUBFOLDER of that project (typically its build output), and .sakupa must live at the project ROOT. Re-run deploy_site with projectDir: ${rootAbove}. Only if the user explicitly says this subfolder is an INDEPENDENT site (e.g. a docs/ site inside a repo), re-run with subprojectConfirmed: true. Nothing was deployed and no site was created.`,
|
|
1778
|
+
{ projectRoot: rootAbove, confirmationField: "subprojectConfirmed" },
|
|
1779
|
+
"blocked"
|
|
1780
|
+
);
|
|
1781
|
+
}
|
|
1782
|
+
const neighbor = findNeighborBinding(ctx.projectDir, analysis.recommendedOutputDir);
|
|
1783
|
+
if (neighbor) {
|
|
1784
|
+
return text(
|
|
1785
|
+
"neighbor_binding_found",
|
|
1786
|
+
`No .sakupa binding in ${ctx.projectDir}, but one EXISTS at ${neighbor} \u2014 this looks like the same project addressed at a different directory level. To update that existing site, re-run deploy_site with projectDir: ${neighbor}. Only if the user explicitly wants a SEPARATE new site, move this deploy to a directory outside that project. Nothing was deployed and no site was created.`,
|
|
1787
|
+
{ neighborProjectDir: neighbor },
|
|
1788
|
+
"blocked"
|
|
1789
|
+
);
|
|
1790
|
+
}
|
|
1702
1791
|
const barrier = freeSiteCreationBarrier();
|
|
1703
1792
|
if (barrier) return barrier;
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1793
|
+
if (args.publicConfirmed !== true) {
|
|
1794
|
+
return text(
|
|
1795
|
+
"public_deployment_confirmation_required",
|
|
1796
|
+
`First deployment creates a public URL that anyone with the link can open. The free preview stays live for ${FREE_SITE_TTL_HOURS} hours. Explain this to the user and obtain explicit confirmation before retrying deploy_site with publicConfirmed: true.`,
|
|
1797
|
+
{ publicUrlLifetimeHours: FREE_SITE_TTL_HOURS, confirmationField: "publicConfirmed" },
|
|
1798
|
+
"waiting_user"
|
|
1799
|
+
);
|
|
1800
|
+
}
|
|
1712
1801
|
}
|
|
1713
1802
|
ensureUploadSizeWithinLimits(manifest, !existing);
|
|
1714
1803
|
if (!existing) {
|
|
@@ -1871,9 +1960,11 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
|
|
|
1871
1960
|
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1872
1961
|
const site = requireSiteFile(ctx);
|
|
1873
1962
|
const res = await ctx.client.getSiteStatus(site.siteId, site.credential);
|
|
1874
|
-
|
|
1963
|
+
const binding = res.pendingDomainBinding ? await describePendingBinding(ctx.client, site.credential, res.pendingDomainBinding) : void 0;
|
|
1964
|
+
return textJson("site_status_returned", `Site status:${binding?.note ?? ""}`, {
|
|
1875
1965
|
...res,
|
|
1876
|
-
projectDir: ctx.projectDir
|
|
1966
|
+
projectDir: ctx.projectDir,
|
|
1967
|
+
...binding?.checklist ? { dnsChecklist: binding.checklist } : {}
|
|
1877
1968
|
});
|
|
1878
1969
|
} catch (e) {
|
|
1879
1970
|
return toolError(e);
|
|
@@ -1952,7 +2043,7 @@ Once payment confirms, the site becomes permanent on its current URL. Binding a
|
|
|
1952
2043
|
writeSiteFile(ctx.projectDir, { ...site, boundDomain: res2.apexDomain });
|
|
1953
2044
|
}
|
|
1954
2045
|
const apex2 = res2.apexDomain;
|
|
1955
|
-
const
|
|
2046
|
+
const diag = await diagnoseBinding({
|
|
1956
2047
|
apexDomain: apex2,
|
|
1957
2048
|
servingTarget: res2.servingTarget,
|
|
1958
2049
|
...res2.verificationRecord ? { verificationRecord: res2.verificationRecord } : {},
|
|
@@ -1960,30 +2051,23 @@ Once payment confirms, the site becomes permanent on its current URL. Binding a
|
|
|
1960
2051
|
verificationStatus: res2.status,
|
|
1961
2052
|
provisioning: res2.provisioningJobId !== void 0
|
|
1962
2053
|
});
|
|
2054
|
+
const { apexResolves, allOk } = diag;
|
|
1963
2055
|
return text(
|
|
1964
2056
|
res2.status === "verified" ? "domain_verification_succeeded" : "domain_verification_pending",
|
|
1965
2057
|
`Domain binding status for ${apex2}: ${res2.status}
|
|
1966
2058
|
${res2.message}
|
|
1967
2059
|
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
` + (allOk && res2.status === "verified" ? "All records are live; certificate issuance completes automatically \u2014 check again in a few minutes until the binding is active." : "Fix any [MISSING]/[FIX] lines above, then re-run bind_domain status. Re-check every 5 minutes, up to 10 times; if still failing after that, show the user this checklist."),
|
|
2060
|
+
` + renderChecklistBlock(
|
|
2061
|
+
diag,
|
|
2062
|
+
"Fix any [MISSING]/[FIX] lines above, then re-run bind_domain status; if still failing after the attempts below, show the user this checklist."
|
|
2063
|
+
),
|
|
1974
2064
|
{
|
|
1975
2065
|
verificationId: res2.verificationId,
|
|
1976
2066
|
status: res2.status,
|
|
1977
2067
|
apexDomain: apex2,
|
|
1978
2068
|
provisioningJobId: res2.provisioningJobId,
|
|
1979
2069
|
servingTarget: res2.servingTarget,
|
|
1980
|
-
dnsChecklist: checks
|
|
1981
|
-
name: c.record.name,
|
|
1982
|
-
type: c.record.type,
|
|
1983
|
-
shortHost: c.shortHost,
|
|
1984
|
-
state: c.state,
|
|
1985
|
-
fix: c.fix || void 0
|
|
1986
|
-
})),
|
|
2070
|
+
dnsChecklist: toDnsChecklist(diag.checks),
|
|
1987
2071
|
apexResolves,
|
|
1988
2072
|
retryAfterSeconds: DNS_RETRY_AFTER_SECONDS,
|
|
1989
2073
|
maxAttempts: DNS_MAX_ATTEMPTS,
|
|
@@ -2006,17 +2090,19 @@ ${layers}
|
|
|
2006
2090
|
"domain_verification_started",
|
|
2007
2091
|
`Domain binding started for ${apex} (includes: ${res.includedHostnames.join(", ")} \u2014 both will serve this site).
|
|
2008
2092
|
|
|
2009
|
-
|
|
2093
|
+
STEP 1 of 2 \u2014 add ALL THREE records NOW:
|
|
2010
2094
|
|
|
2011
2095
|
1) TXT host: ${txtShort} value: ${res.verificationRecord.value}
|
|
2012
2096
|
2) CNAME host: www value: ${res.servingTarget}
|
|
2013
|
-
3) APEX host: @ -> ${res.servingTarget}
|
|
2097
|
+
3) APEX host: @ -> ${res.servingTarget}. Try a plain CNAME at host @ first (most panels accept it); if rejected use ALIAS / ANAME / CNAME-flattening; if the panel has neither, skip it \u2014 www alone works, and a URL redirect from @ to www covers bare-domain visitors.
|
|
2014
2098
|
|
|
2015
2099
|
Host fields above are the SHORT form: most DNS panels append the domain automatically. After saving, the record list must NOT show ${apex} twice in one name \u2014 that means the full name was pasted into an auto-appending field.
|
|
2016
2100
|
|
|
2017
2101
|
Ownership comes ONLY from DNS control; paying never grants it. The first verified request wins and this challenge expires after 72 hours.
|
|
2018
2102
|
|
|
2019
|
-
|
|
2103
|
+
STEP 2 (after ownership verifies): Cloudflare issues certificate-validation TXT records \u2014 the verification result and bind_domain "status" list them the moment they exist; relay each to the user and add them too. Everything then completes automatically.
|
|
2104
|
+
|
|
2105
|
+
Drive the whole flow with bind_domain "status": it live-checks every record and names the exact fix for anything wrong. Re-check every 5 minutes (up to 10 times). Any later session can resume with action "status" alone; the verificationId is optional.`,
|
|
2020
2106
|
{
|
|
2021
2107
|
verificationId: res.verificationId,
|
|
2022
2108
|
apexDomain: apex,
|
|
@@ -2291,6 +2377,12 @@ ${res.archiveUrl}`,
|
|
|
2291
2377
|
deploymentId: z3.string().optional(),
|
|
2292
2378
|
severity: severityEnum.optional(),
|
|
2293
2379
|
description: z3.string().optional().describe("What happened, in the user's words (no secrets)."),
|
|
2380
|
+
agentContext: z3.string().optional().describe(
|
|
2381
|
+
"YOUR OWN factual account of the session as the AI: which tools you called, what they returned, expected vs actual. Write it yourself from your observations \u2014 never ask the user to compose it, and do not read it back to them; it travels alongside the user's description as a second witness. No secrets, no file contents."
|
|
2382
|
+
),
|
|
2383
|
+
contactEmail: z3.string().optional().describe(
|
|
2384
|
+
"OPTIONAL. Before submitting, ask the user ONCE whether they want to leave a contact for follow-up. Omit entirely if they decline \u2014 never require it."
|
|
2385
|
+
),
|
|
2294
2386
|
confirmSubmit: z3.boolean().optional().describe("User reviewed the report payload and approved submission.")
|
|
2295
2387
|
}
|
|
2296
2388
|
},
|
|
@@ -2315,7 +2407,9 @@ ${res.archiveUrl}`,
|
|
|
2315
2407
|
...site ? { siteId: site.siteId } : {},
|
|
2316
2408
|
...args.severity !== void 0 ? { severity: args.severity } : {},
|
|
2317
2409
|
diagnostics,
|
|
2318
|
-
...args.description !== void 0 ? { description: args.description } : {}
|
|
2410
|
+
...args.description !== void 0 ? { description: args.description } : {},
|
|
2411
|
+
...args.agentContext !== void 0 ? { agentContext: args.agentContext } : {},
|
|
2412
|
+
...args.contactEmail !== void 0 ? { contactEmail: args.contactEmail } : {}
|
|
2319
2413
|
};
|
|
2320
2414
|
if (args.confirmSubmit !== true) {
|
|
2321
2415
|
return textJson(
|
|
@@ -2656,13 +2750,18 @@ Workflow:
|
|
|
2656
2750
|
|
|
2657
2751
|
Project directory contract: ONE directory = ONE site (its .sakupa/site.json holds the
|
|
2658
2752
|
binding). Every project-scoped tool accepts projectDir \u2014 ALWAYS pass the absolute path of
|
|
2659
|
-
the
|
|
2660
|
-
|
|
2661
|
-
|
|
2753
|
+
the user's PROJECT ROOT, the SAME directory every time for the same project: the folder
|
|
2754
|
+
the user opened (for framework projects, where package.json lives \u2014 never the dist/out
|
|
2755
|
+
build folder; output is auto-detected). Without it the server falls back to its startup
|
|
2756
|
+
directory, which may be a different project than the one the user is looking at. After every deploy, TELL the user which environment it went to (deploy results carry an
|
|
2662
2757
|
Explicit Environment line: TEST vs PRODUCTION). analyze_site, deploy_site, site_status,
|
|
2663
2758
|
refresh_site, delete_site and unbind_domain echo
|
|
2664
2759
|
the directory they acted on \u2014 verify it matches the user's active project.
|
|
2665
2760
|
|
|
2761
|
+
When the same operation fails twice in a row, or the user is clearly stuck or
|
|
2762
|
+
frustrated, proactively offer report_bug: it files the problem into Sakupa's ticket and
|
|
2763
|
+
alert stream, and you should attach your own factual account via agentContext.
|
|
2764
|
+
|
|
2666
2765
|
Safety boundaries:
|
|
2667
2766
|
- Static output only: no SSR, API routes, middleware, server actions, databases or online builds.
|
|
2668
2767
|
- Never upload source projects, secrets, .env files, private keys, archives, videos or audio.
|
package/dist/index.js
CHANGED
|
@@ -124,7 +124,7 @@ var FORBIDDEN_PATH_SEGMENTS = [
|
|
|
124
124
|
var ALLOWED_HIDDEN_PATHS = [".well-known/"];
|
|
125
125
|
|
|
126
126
|
// ../core/dist/domain/version.js
|
|
127
|
-
var SAKUPA_MCP_VERSION = "0.7.
|
|
127
|
+
var SAKUPA_MCP_VERSION = "0.7.11";
|
|
128
128
|
|
|
129
129
|
// ../core/dist/domain/errors.js
|
|
130
130
|
var HTTP_STATUS = {
|
|
@@ -839,14 +839,18 @@ function deleteSiteFile(projectDir) {
|
|
|
839
839
|
rmSync(path, { force: true });
|
|
840
840
|
}
|
|
841
841
|
}
|
|
842
|
-
function
|
|
843
|
-
let
|
|
844
|
-
for (; ; ) {
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
if (parent
|
|
848
|
-
|
|
842
|
+
function findAncestor(startDir, predicate, maxLevels = Number.POSITIVE_INFINITY) {
|
|
843
|
+
let cursor = startDir;
|
|
844
|
+
for (let i = 0; i < maxLevels; i += 1) {
|
|
845
|
+
const parent = dirname(cursor);
|
|
846
|
+
if (parent === cursor) return null;
|
|
847
|
+
if (predicate(parent)) return parent;
|
|
848
|
+
cursor = parent;
|
|
849
849
|
}
|
|
850
|
+
return null;
|
|
851
|
+
}
|
|
852
|
+
function isInsideGitRepo(projectDir) {
|
|
853
|
+
return existsSync(join(projectDir, ".git")) || findAncestor(projectDir, (dir) => existsSync(join(dir, ".git"))) !== null;
|
|
850
854
|
}
|
|
851
855
|
function credentialGitReminder(projectDir) {
|
|
852
856
|
if (!isInsideGitRepo(projectDir)) return "";
|
|
@@ -1326,7 +1330,7 @@ var LocalGuidanceError = class extends SakupaError {
|
|
|
1326
1330
|
}
|
|
1327
1331
|
};
|
|
1328
1332
|
var projectDirInput = z2.string().optional().describe(
|
|
1329
|
-
"Absolute path of the
|
|
1333
|
+
"Absolute path of the user's PROJECT ROOT \u2014 the folder the user opened/works in (for framework projects: where package.json lives, NEVER the build-output subfolder like dist/out; the analyzer locates the output automatically). .sakupa/site.json lives here, so PASS THE SAME DIRECTORY EVERY TIME for the same project. When omitted the server falls back to its startup directory, which may not be where the user is working now."
|
|
1330
1334
|
);
|
|
1331
1335
|
function withProjectDir(ctx, projectDirArg) {
|
|
1332
1336
|
if (projectDirArg === void 0) return ctx;
|
|
@@ -1414,7 +1418,7 @@ function toolError(e) {
|
|
|
1414
1418
|
|
|
1415
1419
|
// src/tools/definitions.ts
|
|
1416
1420
|
import { randomUUID } from "node:crypto";
|
|
1417
|
-
import { promises as fs2 } from "node:fs";
|
|
1421
|
+
import { existsSync as existsSync3, promises as fs2 } from "node:fs";
|
|
1418
1422
|
import { join as join4, resolve as resolve3 } from "node:path";
|
|
1419
1423
|
import { z as z3 } from "zod";
|
|
1420
1424
|
|
|
@@ -1562,10 +1566,30 @@ async function diagnoseBinding(input) {
|
|
|
1562
1566
|
const apexResolves = apexAnswers.length > 0;
|
|
1563
1567
|
const allOk = checks.every((c) => c.state === "ok");
|
|
1564
1568
|
const checklist = checks.map(renderCheck).join("\n") + `
|
|
1565
|
-
[${apexResolves ? "OK" : "MISSING"}] APEX ${apex} \u2014 ` + (apexResolves ? "resolves." : `does not resolve yet
|
|
1569
|
+
[${apexResolves ? "OK" : "MISSING"}] APEX ${apex} \u2014 ` + (apexResolves ? "resolves." : `does not resolve yet. Three-step fix, stop at the first that works: (1) try adding a plain record \u2014 type CNAME, host @, value ${input.servingTarget} (most panels and Cloudflare accept this directly; confirm past any MX-conflict warning if the domain sends no email). (2) If rejected, look for ALIAS / ANAME / CNAME-flattening in the record-type list \u2014 same host and value. (3) If the panel has neither, skip the apex: www alone works fine (certificates do not depend on the apex record); optionally add a URL redirect from @ to www.`);
|
|
1566
1570
|
const layers = `Pipeline: [1] public DNS (checked LIVE above) -> [2] Sakupa ownership verification: ${input.verificationStatus} -> [3] HTTPS certificate & serving: ` + (input.provisioning ? "provisioning (Cloudflare validates and issues within minutes once the records above are all OK; Sakupa retries automatically every ~5 minutes)." : "starts after verification.");
|
|
1567
1571
|
return { checks, apexResolves, allOk, checklist, layers };
|
|
1568
1572
|
}
|
|
1573
|
+
var DNS_RETRY_AFTER_SECONDS = 300;
|
|
1574
|
+
var DNS_MAX_ATTEMPTS = 10;
|
|
1575
|
+
function toDnsChecklist(checks) {
|
|
1576
|
+
return checks.map((c) => ({
|
|
1577
|
+
name: c.record.name,
|
|
1578
|
+
type: c.record.type,
|
|
1579
|
+
shortHost: c.shortHost,
|
|
1580
|
+
state: c.state,
|
|
1581
|
+
...c.fix ? { fix: c.fix } : {}
|
|
1582
|
+
}));
|
|
1583
|
+
}
|
|
1584
|
+
function renderChecklistBlock(diag, fixTail) {
|
|
1585
|
+
const cadence = `Re-check every ${DNS_RETRY_AFTER_SECONDS / 60} minutes, up to ${DNS_MAX_ATTEMPTS} times.`;
|
|
1586
|
+
return `Live DNS checklist (host values are the SHORT panel form):
|
|
1587
|
+
${diag.checklist}
|
|
1588
|
+
|
|
1589
|
+
${diag.layers}
|
|
1590
|
+
|
|
1591
|
+
` + (diag.allOk ? "All records are live; certificate issuance completes automatically \u2014 re-check in a few minutes until the binding is active." : `${fixTail} ${cadence}`);
|
|
1592
|
+
}
|
|
1569
1593
|
|
|
1570
1594
|
// src/tools/definitions.ts
|
|
1571
1595
|
function text(resultCode, t, data = {}, outcome = "completed") {
|
|
@@ -1673,8 +1697,52 @@ async function uploadAll(ctx, targets, files, outputAbs) {
|
|
|
1673
1697
|
}
|
|
1674
1698
|
return targets.length;
|
|
1675
1699
|
}
|
|
1676
|
-
|
|
1677
|
-
|
|
1700
|
+
async function describePendingBinding(client, credential, pb) {
|
|
1701
|
+
const framing = `
|
|
1702
|
+
Domain binding IN PROGRESS: ${pb.apexDomain} \u2014 ` + (pb.phase === "provisioning" ? "ownership verified; certificates/serving are provisioning." : `awaiting DNS verification (challenge valid until ${pb.verificationExpiresAt}).`);
|
|
1703
|
+
let check;
|
|
1704
|
+
try {
|
|
1705
|
+
check = await client.checkVerification(pb.verificationId, credential);
|
|
1706
|
+
} catch {
|
|
1707
|
+
return {
|
|
1708
|
+
note: framing + "\n(Couldn't refresh binding progress from the server just now \u2014 try site_status again shortly.)"
|
|
1709
|
+
};
|
|
1710
|
+
}
|
|
1711
|
+
try {
|
|
1712
|
+
const diag = await diagnoseBinding({
|
|
1713
|
+
apexDomain: check.apexDomain,
|
|
1714
|
+
servingTarget: check.servingTarget,
|
|
1715
|
+
...check.verificationRecord ? { verificationRecord: check.verificationRecord } : {},
|
|
1716
|
+
pendingDnsRecords: check.pendingDnsRecords,
|
|
1717
|
+
verificationStatus: check.status,
|
|
1718
|
+
provisioning: pb.phase === "provisioning"
|
|
1719
|
+
});
|
|
1720
|
+
const block = renderChecklistBlock(
|
|
1721
|
+
diag,
|
|
1722
|
+
"Relay every [MISSING]/[FIX] line to the user with its exact fix."
|
|
1723
|
+
);
|
|
1724
|
+
return { note: `${framing}
|
|
1725
|
+
${block}`, checklist: toDnsChecklist(diag.checks) };
|
|
1726
|
+
} catch {
|
|
1727
|
+
return {
|
|
1728
|
+
note: framing + '\n(Live DNS lookups are unavailable right now \u2014 run bind_domain with action "status" for the per-record checklist.)'
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
function projectRootAbove(projectDir) {
|
|
1733
|
+
if (existsSync3(join4(projectDir, "package.json"))) return null;
|
|
1734
|
+
return findAncestor(projectDir, (dir) => existsSync3(join4(dir, "package.json")), 4);
|
|
1735
|
+
}
|
|
1736
|
+
function findNeighborBinding(projectDir, outputRel) {
|
|
1737
|
+
const bound = (dir) => loadSiteFile(dir).kind !== "absent";
|
|
1738
|
+
const above = findAncestor(projectDir, bound, 3);
|
|
1739
|
+
if (above) return above;
|
|
1740
|
+
if (outputRel && outputRel !== ".") {
|
|
1741
|
+
const outputAbs = resolve3(projectDir, outputRel);
|
|
1742
|
+
if (bound(outputAbs)) return outputAbs;
|
|
1743
|
+
}
|
|
1744
|
+
return null;
|
|
1745
|
+
}
|
|
1678
1746
|
function freeSiteCreationBarrier() {
|
|
1679
1747
|
const recent = listRecentCreations(Date.now());
|
|
1680
1748
|
if (recent.length < FREE_ACTIVE_SITES_PER_IP) return null;
|
|
@@ -1735,6 +1803,9 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1735
1803
|
publicConfirmed: z3.boolean().optional().describe(
|
|
1736
1804
|
"Required only for the first deployment: user explicitly confirmed creation of a public 24-hour URL."
|
|
1737
1805
|
),
|
|
1806
|
+
subprojectConfirmed: z3.boolean().optional().describe(
|
|
1807
|
+
"Only when creating a NEW site in a subfolder of a package.json project: the user explicitly confirmed this subfolder is an INDEPENDENT site, not the project's build output."
|
|
1808
|
+
),
|
|
1738
1809
|
lang: z3.string().optional().describe("Site language override (en | ja | zh-CN); defaults to the html lang.")
|
|
1739
1810
|
}
|
|
1740
1811
|
},
|
|
@@ -1763,16 +1834,34 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1763
1834
|
}
|
|
1764
1835
|
const existing = siteFileState.kind === "ok" ? siteFileState.file : null;
|
|
1765
1836
|
if (!existing) {
|
|
1837
|
+
const rootAbove = args.subprojectConfirmed === true ? null : projectRootAbove(ctx.projectDir);
|
|
1838
|
+
if (rootAbove) {
|
|
1839
|
+
return text(
|
|
1840
|
+
"not_project_root",
|
|
1841
|
+
`${ctx.projectDir} has no package.json but ${rootAbove} does \u2014 this directory is a SUBFOLDER of that project (typically its build output), and .sakupa must live at the project ROOT. Re-run deploy_site with projectDir: ${rootAbove}. Only if the user explicitly says this subfolder is an INDEPENDENT site (e.g. a docs/ site inside a repo), re-run with subprojectConfirmed: true. Nothing was deployed and no site was created.`,
|
|
1842
|
+
{ projectRoot: rootAbove, confirmationField: "subprojectConfirmed" },
|
|
1843
|
+
"blocked"
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1846
|
+
const neighbor = findNeighborBinding(ctx.projectDir, analysis.recommendedOutputDir);
|
|
1847
|
+
if (neighbor) {
|
|
1848
|
+
return text(
|
|
1849
|
+
"neighbor_binding_found",
|
|
1850
|
+
`No .sakupa binding in ${ctx.projectDir}, but one EXISTS at ${neighbor} \u2014 this looks like the same project addressed at a different directory level. To update that existing site, re-run deploy_site with projectDir: ${neighbor}. Only if the user explicitly wants a SEPARATE new site, move this deploy to a directory outside that project. Nothing was deployed and no site was created.`,
|
|
1851
|
+
{ neighborProjectDir: neighbor },
|
|
1852
|
+
"blocked"
|
|
1853
|
+
);
|
|
1854
|
+
}
|
|
1766
1855
|
const barrier = freeSiteCreationBarrier();
|
|
1767
1856
|
if (barrier) return barrier;
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1857
|
+
if (args.publicConfirmed !== true) {
|
|
1858
|
+
return text(
|
|
1859
|
+
"public_deployment_confirmation_required",
|
|
1860
|
+
`First deployment creates a public URL that anyone with the link can open. The free preview stays live for ${FREE_SITE_TTL_HOURS} hours. Explain this to the user and obtain explicit confirmation before retrying deploy_site with publicConfirmed: true.`,
|
|
1861
|
+
{ publicUrlLifetimeHours: FREE_SITE_TTL_HOURS, confirmationField: "publicConfirmed" },
|
|
1862
|
+
"waiting_user"
|
|
1863
|
+
);
|
|
1864
|
+
}
|
|
1776
1865
|
}
|
|
1777
1866
|
ensureUploadSizeWithinLimits(manifest, !existing);
|
|
1778
1867
|
if (!existing) {
|
|
@@ -1935,9 +2024,11 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
|
|
|
1935
2024
|
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1936
2025
|
const site = requireSiteFile(ctx);
|
|
1937
2026
|
const res = await ctx.client.getSiteStatus(site.siteId, site.credential);
|
|
1938
|
-
|
|
2027
|
+
const binding = res.pendingDomainBinding ? await describePendingBinding(ctx.client, site.credential, res.pendingDomainBinding) : void 0;
|
|
2028
|
+
return textJson("site_status_returned", `Site status:${binding?.note ?? ""}`, {
|
|
1939
2029
|
...res,
|
|
1940
|
-
projectDir: ctx.projectDir
|
|
2030
|
+
projectDir: ctx.projectDir,
|
|
2031
|
+
...binding?.checklist ? { dnsChecklist: binding.checklist } : {}
|
|
1941
2032
|
});
|
|
1942
2033
|
} catch (e) {
|
|
1943
2034
|
return toolError(e);
|
|
@@ -2016,7 +2107,7 @@ Once payment confirms, the site becomes permanent on its current URL. Binding a
|
|
|
2016
2107
|
writeSiteFile(ctx.projectDir, { ...site, boundDomain: res2.apexDomain });
|
|
2017
2108
|
}
|
|
2018
2109
|
const apex2 = res2.apexDomain;
|
|
2019
|
-
const
|
|
2110
|
+
const diag = await diagnoseBinding({
|
|
2020
2111
|
apexDomain: apex2,
|
|
2021
2112
|
servingTarget: res2.servingTarget,
|
|
2022
2113
|
...res2.verificationRecord ? { verificationRecord: res2.verificationRecord } : {},
|
|
@@ -2024,30 +2115,23 @@ Once payment confirms, the site becomes permanent on its current URL. Binding a
|
|
|
2024
2115
|
verificationStatus: res2.status,
|
|
2025
2116
|
provisioning: res2.provisioningJobId !== void 0
|
|
2026
2117
|
});
|
|
2118
|
+
const { apexResolves, allOk } = diag;
|
|
2027
2119
|
return text(
|
|
2028
2120
|
res2.status === "verified" ? "domain_verification_succeeded" : "domain_verification_pending",
|
|
2029
2121
|
`Domain binding status for ${apex2}: ${res2.status}
|
|
2030
2122
|
${res2.message}
|
|
2031
2123
|
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
` + (allOk && res2.status === "verified" ? "All records are live; certificate issuance completes automatically \u2014 check again in a few minutes until the binding is active." : "Fix any [MISSING]/[FIX] lines above, then re-run bind_domain status. Re-check every 5 minutes, up to 10 times; if still failing after that, show the user this checklist."),
|
|
2124
|
+
` + renderChecklistBlock(
|
|
2125
|
+
diag,
|
|
2126
|
+
"Fix any [MISSING]/[FIX] lines above, then re-run bind_domain status; if still failing after the attempts below, show the user this checklist."
|
|
2127
|
+
),
|
|
2038
2128
|
{
|
|
2039
2129
|
verificationId: res2.verificationId,
|
|
2040
2130
|
status: res2.status,
|
|
2041
2131
|
apexDomain: apex2,
|
|
2042
2132
|
provisioningJobId: res2.provisioningJobId,
|
|
2043
2133
|
servingTarget: res2.servingTarget,
|
|
2044
|
-
dnsChecklist: checks
|
|
2045
|
-
name: c.record.name,
|
|
2046
|
-
type: c.record.type,
|
|
2047
|
-
shortHost: c.shortHost,
|
|
2048
|
-
state: c.state,
|
|
2049
|
-
fix: c.fix || void 0
|
|
2050
|
-
})),
|
|
2134
|
+
dnsChecklist: toDnsChecklist(diag.checks),
|
|
2051
2135
|
apexResolves,
|
|
2052
2136
|
retryAfterSeconds: DNS_RETRY_AFTER_SECONDS,
|
|
2053
2137
|
maxAttempts: DNS_MAX_ATTEMPTS,
|
|
@@ -2070,17 +2154,19 @@ ${layers}
|
|
|
2070
2154
|
"domain_verification_started",
|
|
2071
2155
|
`Domain binding started for ${apex} (includes: ${res.includedHostnames.join(", ")} \u2014 both will serve this site).
|
|
2072
2156
|
|
|
2073
|
-
|
|
2157
|
+
STEP 1 of 2 \u2014 add ALL THREE records NOW:
|
|
2074
2158
|
|
|
2075
2159
|
1) TXT host: ${txtShort} value: ${res.verificationRecord.value}
|
|
2076
2160
|
2) CNAME host: www value: ${res.servingTarget}
|
|
2077
|
-
3) APEX host: @ -> ${res.servingTarget}
|
|
2161
|
+
3) APEX host: @ -> ${res.servingTarget}. Try a plain CNAME at host @ first (most panels accept it); if rejected use ALIAS / ANAME / CNAME-flattening; if the panel has neither, skip it \u2014 www alone works, and a URL redirect from @ to www covers bare-domain visitors.
|
|
2078
2162
|
|
|
2079
2163
|
Host fields above are the SHORT form: most DNS panels append the domain automatically. After saving, the record list must NOT show ${apex} twice in one name \u2014 that means the full name was pasted into an auto-appending field.
|
|
2080
2164
|
|
|
2081
2165
|
Ownership comes ONLY from DNS control; paying never grants it. The first verified request wins and this challenge expires after 72 hours.
|
|
2082
2166
|
|
|
2083
|
-
|
|
2167
|
+
STEP 2 (after ownership verifies): Cloudflare issues certificate-validation TXT records \u2014 the verification result and bind_domain "status" list them the moment they exist; relay each to the user and add them too. Everything then completes automatically.
|
|
2168
|
+
|
|
2169
|
+
Drive the whole flow with bind_domain "status": it live-checks every record and names the exact fix for anything wrong. Re-check every 5 minutes (up to 10 times). Any later session can resume with action "status" alone; the verificationId is optional.`,
|
|
2084
2170
|
{
|
|
2085
2171
|
verificationId: res.verificationId,
|
|
2086
2172
|
apexDomain: apex,
|
|
@@ -2355,6 +2441,12 @@ ${res.archiveUrl}`,
|
|
|
2355
2441
|
deploymentId: z3.string().optional(),
|
|
2356
2442
|
severity: severityEnum.optional(),
|
|
2357
2443
|
description: z3.string().optional().describe("What happened, in the user's words (no secrets)."),
|
|
2444
|
+
agentContext: z3.string().optional().describe(
|
|
2445
|
+
"YOUR OWN factual account of the session as the AI: which tools you called, what they returned, expected vs actual. Write it yourself from your observations \u2014 never ask the user to compose it, and do not read it back to them; it travels alongside the user's description as a second witness. No secrets, no file contents."
|
|
2446
|
+
),
|
|
2447
|
+
contactEmail: z3.string().optional().describe(
|
|
2448
|
+
"OPTIONAL. Before submitting, ask the user ONCE whether they want to leave a contact for follow-up. Omit entirely if they decline \u2014 never require it."
|
|
2449
|
+
),
|
|
2358
2450
|
confirmSubmit: z3.boolean().optional().describe("User reviewed the report payload and approved submission.")
|
|
2359
2451
|
}
|
|
2360
2452
|
},
|
|
@@ -2379,7 +2471,9 @@ ${res.archiveUrl}`,
|
|
|
2379
2471
|
...site ? { siteId: site.siteId } : {},
|
|
2380
2472
|
...args.severity !== void 0 ? { severity: args.severity } : {},
|
|
2381
2473
|
diagnostics,
|
|
2382
|
-
...args.description !== void 0 ? { description: args.description } : {}
|
|
2474
|
+
...args.description !== void 0 ? { description: args.description } : {},
|
|
2475
|
+
...args.agentContext !== void 0 ? { agentContext: args.agentContext } : {},
|
|
2476
|
+
...args.contactEmail !== void 0 ? { contactEmail: args.contactEmail } : {}
|
|
2383
2477
|
};
|
|
2384
2478
|
if (args.confirmSubmit !== true) {
|
|
2385
2479
|
return textJson(
|
|
@@ -2651,13 +2745,18 @@ Workflow:
|
|
|
2651
2745
|
|
|
2652
2746
|
Project directory contract: ONE directory = ONE site (its .sakupa/site.json holds the
|
|
2653
2747
|
binding). Every project-scoped tool accepts projectDir \u2014 ALWAYS pass the absolute path of
|
|
2654
|
-
the
|
|
2655
|
-
|
|
2656
|
-
|
|
2748
|
+
the user's PROJECT ROOT, the SAME directory every time for the same project: the folder
|
|
2749
|
+
the user opened (for framework projects, where package.json lives \u2014 never the dist/out
|
|
2750
|
+
build folder; output is auto-detected). Without it the server falls back to its startup
|
|
2751
|
+
directory, which may be a different project than the one the user is looking at. After every deploy, TELL the user which environment it went to (deploy results carry an
|
|
2657
2752
|
Explicit Environment line: TEST vs PRODUCTION). analyze_site, deploy_site, site_status,
|
|
2658
2753
|
refresh_site, delete_site and unbind_domain echo
|
|
2659
2754
|
the directory they acted on \u2014 verify it matches the user's active project.
|
|
2660
2755
|
|
|
2756
|
+
When the same operation fails twice in a row, or the user is clearly stuck or
|
|
2757
|
+
frustrated, proactively offer report_bug: it files the problem into Sakupa's ticket and
|
|
2758
|
+
alert stream, and you should attach your own factual account via agentContext.
|
|
2759
|
+
|
|
2661
2760
|
Safety boundaries:
|
|
2662
2761
|
- Static output only: no SSR, API routes, middleware, server actions, databases or online builds.
|
|
2663
2762
|
- Never upload source projects, secrets, .env files, private keys, archives, videos or audio.
|