@sakupa/mcp 0.7.33 → 0.7.34
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 +145 -224
- package/dist/index.js +120 -215
- package/package.json +1 -1
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.34";
|
|
128
128
|
|
|
129
129
|
// ../core/dist/domain/errors.js
|
|
130
130
|
var HTTP_STATUS = {
|
|
@@ -1364,9 +1364,6 @@ async function analyzeProject(projectDir, opts = {}) {
|
|
|
1364
1364
|
};
|
|
1365
1365
|
}
|
|
1366
1366
|
|
|
1367
|
-
// src/tools/context.ts
|
|
1368
|
-
import { z as z2 } from "zod";
|
|
1369
|
-
|
|
1370
1367
|
// src/project-root.ts
|
|
1371
1368
|
import { randomUUID } from "node:crypto";
|
|
1372
1369
|
import {
|
|
@@ -1382,11 +1379,9 @@ import {
|
|
|
1382
1379
|
writeFileSync as writeFileSync2
|
|
1383
1380
|
} from "node:fs";
|
|
1384
1381
|
import { homedir } from "node:os";
|
|
1385
|
-
import {
|
|
1382
|
+
import { isAbsolute, join as join3, parse, relative, resolve as resolve2, sep as sep2 } from "node:path";
|
|
1386
1383
|
var SAKUPA_DIR = ".sakupa";
|
|
1387
1384
|
var PROJECT_FILE = "project.json";
|
|
1388
|
-
var SITE_FILE2 = "site.json";
|
|
1389
|
-
var RECOVERY_FILE2 = "recovery.json";
|
|
1390
1385
|
var PROJECT_SCHEMA_VERSION = 1;
|
|
1391
1386
|
var ProjectRootError = class extends Error {
|
|
1392
1387
|
code;
|
|
@@ -1440,96 +1435,27 @@ function loadProjectMarker(projectDir) {
|
|
|
1440
1435
|
}
|
|
1441
1436
|
};
|
|
1442
1437
|
}
|
|
1443
|
-
function
|
|
1438
|
+
function resolveLockedProjectRoot(projectDir) {
|
|
1444
1439
|
const canonical = canonicalProjectDirectory(projectDir);
|
|
1445
1440
|
assertSafeProjectRoot(canonical);
|
|
1446
|
-
const
|
|
1447
|
-
if (
|
|
1441
|
+
const markerState = loadProjectMarker(canonical);
|
|
1442
|
+
if (markerState.kind === "corrupted") {
|
|
1448
1443
|
throw new ProjectRootError(
|
|
1449
1444
|
"corrupted_marker",
|
|
1450
|
-
`
|
|
1451
|
-
);
|
|
1452
|
-
}
|
|
1453
|
-
if (current.kind === "ok") {
|
|
1454
|
-
return {
|
|
1455
|
-
projectDir: canonical,
|
|
1456
|
-
requestedPath: canonical,
|
|
1457
|
-
markerKind: "project",
|
|
1458
|
-
marker: current.marker
|
|
1459
|
-
};
|
|
1460
|
-
}
|
|
1461
|
-
const marker = {
|
|
1462
|
-
schemaVersion: PROJECT_SCHEMA_VERSION,
|
|
1463
|
-
projectId: randomUUID(),
|
|
1464
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1465
|
-
};
|
|
1466
|
-
writeMarkerAtomically(canonical, marker);
|
|
1467
|
-
return {
|
|
1468
|
-
projectDir: canonical,
|
|
1469
|
-
requestedPath: canonical,
|
|
1470
|
-
markerKind: "project",
|
|
1471
|
-
marker
|
|
1472
|
-
};
|
|
1473
|
-
}
|
|
1474
|
-
function resolveProjectRoot(requestedPath) {
|
|
1475
|
-
if (!isAbsolute(requestedPath)) {
|
|
1476
|
-
throw new ProjectRootError(
|
|
1477
|
-
"invalid_path",
|
|
1478
|
-
`Project path must be absolute (got "${requestedPath}").`
|
|
1445
|
+
`Sakupa project marker ${projectMarkerPath(canonical)} is damaged: ${markerState.problem}.`
|
|
1479
1446
|
);
|
|
1480
1447
|
}
|
|
1481
|
-
|
|
1482
|
-
const requestedStat = statSync(canonicalRequested);
|
|
1483
|
-
let cursor = requestedStat.isDirectory() ? canonicalRequested : dirname2(canonicalRequested);
|
|
1484
|
-
const startDevice = statSync(cursor).dev;
|
|
1485
|
-
const candidates = [];
|
|
1486
|
-
while (true) {
|
|
1487
|
-
if (statSync(cursor).dev !== startDevice) break;
|
|
1488
|
-
const markerState = loadProjectMarker(cursor);
|
|
1489
|
-
const hasSite = existsSync2(join3(cursor, SAKUPA_DIR, SITE_FILE2));
|
|
1490
|
-
const hasRecovery = existsSync2(join3(cursor, SAKUPA_DIR, RECOVERY_FILE2));
|
|
1491
|
-
if (markerState.kind !== "absent" || hasSite || hasRecovery) {
|
|
1492
|
-
candidates.push({ dir: cursor, markerState, hasSite, hasRecovery });
|
|
1493
|
-
}
|
|
1494
|
-
const parent = dirname2(cursor);
|
|
1495
|
-
if (parent === cursor) break;
|
|
1496
|
-
cursor = parent;
|
|
1497
|
-
}
|
|
1498
|
-
if (candidates.length === 0) {
|
|
1448
|
+
if (markerState.kind === "absent") {
|
|
1499
1449
|
throw new ProjectRootError(
|
|
1500
1450
|
"not_initialized",
|
|
1501
|
-
`
|
|
1502
|
-
);
|
|
1503
|
-
}
|
|
1504
|
-
const nearest = candidates[0];
|
|
1505
|
-
if (nearest.markerState.kind === "corrupted") {
|
|
1506
|
-
throw new ProjectRootError(
|
|
1507
|
-
"corrupted_marker",
|
|
1508
|
-
`Sakupa project marker ${projectMarkerPath(nearest.dir)} is damaged: ${nearest.markerState.problem}.`
|
|
1451
|
+
`The MCP working directory ${canonical} is not initialized. Run \`npx -y @sakupa/mcp@latest init\` in that directory; do not pass a path argument.`
|
|
1509
1452
|
);
|
|
1510
1453
|
}
|
|
1511
|
-
if (nearest.markerState.kind === "absent") {
|
|
1512
|
-
const explicitAncestor = candidates.find((candidate) => candidate.markerState.kind === "ok");
|
|
1513
|
-
if (explicitAncestor) {
|
|
1514
|
-
throw new ProjectRootError(
|
|
1515
|
-
"ambiguous_binding",
|
|
1516
|
-
`A legacy .sakupa binding exists at ${nearest.dir}, below the initialized Sakupa project ${explicitAncestor.dir}. Start the operation from ${explicitAncestor.dir}; deploy can then validate and relocate the same credential without creating a site.`
|
|
1517
|
-
);
|
|
1518
|
-
}
|
|
1519
|
-
}
|
|
1520
|
-
assertSafeProjectRoot(nearest.dir);
|
|
1521
|
-
if (nearest.markerState.kind === "ok") {
|
|
1522
|
-
return {
|
|
1523
|
-
projectDir: nearest.dir,
|
|
1524
|
-
requestedPath: canonicalRequested,
|
|
1525
|
-
markerKind: "project",
|
|
1526
|
-
marker: nearest.markerState.marker
|
|
1527
|
-
};
|
|
1528
|
-
}
|
|
1529
1454
|
return {
|
|
1530
|
-
projectDir:
|
|
1531
|
-
requestedPath:
|
|
1532
|
-
markerKind:
|
|
1455
|
+
projectDir: canonical,
|
|
1456
|
+
requestedPath: canonical,
|
|
1457
|
+
markerKind: "project",
|
|
1458
|
+
marker: markerState.marker
|
|
1533
1459
|
};
|
|
1534
1460
|
}
|
|
1535
1461
|
function updateProjectOutputDir(projectDir, outputDir) {
|
|
@@ -1666,24 +1592,15 @@ var LocalGuidanceError = class extends SakupaError {
|
|
|
1666
1592
|
super(code, message);
|
|
1667
1593
|
}
|
|
1668
1594
|
};
|
|
1669
|
-
|
|
1670
|
-
"REQUIRED: an absolute existing path anywhere inside the user's initialized Sakupa project (the root itself or a child such as dist/html/src). Sakupa resolves upward to its own .sakupa marker and never trusts this argument as the root. If no marker exists, run `npx -y @sakupa/mcp@latest init` once from the intended project root."
|
|
1671
|
-
);
|
|
1672
|
-
function withProjectDir(ctx, projectDirArg) {
|
|
1673
|
-
if (projectDirArg === void 0) {
|
|
1674
|
-
throw new LocalGuidanceError(
|
|
1675
|
-
"invalid_request",
|
|
1676
|
-
"projectDir is REQUIRED as a path locator: pass an absolute existing path anywhere inside the current Sakupa project. The server resolves its own .sakupa marker and never treats the supplied path as an authoritative root."
|
|
1677
|
-
);
|
|
1678
|
-
}
|
|
1595
|
+
function withProjectDir(ctx) {
|
|
1679
1596
|
try {
|
|
1680
|
-
const resolved =
|
|
1597
|
+
const resolved = resolveLockedProjectRoot(ctx.projectDir);
|
|
1681
1598
|
return {
|
|
1682
1599
|
...ctx,
|
|
1683
1600
|
projectDir: resolved.projectDir,
|
|
1684
1601
|
requestedPath: resolved.requestedPath,
|
|
1685
1602
|
markerKind: resolved.markerKind,
|
|
1686
|
-
|
|
1603
|
+
projectMarker: resolved.marker
|
|
1687
1604
|
};
|
|
1688
1605
|
} catch (error) {
|
|
1689
1606
|
if (error instanceof ProjectRootError) {
|
|
@@ -1706,7 +1623,7 @@ function requireSiteFile(ctx) {
|
|
|
1706
1623
|
if (state.kind === "absent") {
|
|
1707
1624
|
throw new LocalGuidanceError(
|
|
1708
1625
|
"not_found",
|
|
1709
|
-
`No .sakupa/site.json found in ${ctx.projectDir} \u2014 this directory has no Sakupa site binding. If you meant to manage
|
|
1626
|
+
`No .sakupa/site.json found in ${ctx.projectDir} \u2014 this directory has no Sakupa site binding. If you meant to manage a different existing site, open that project as the AI tool workspace and start its Sakupa MCP process there. To publish THIS directory as a new site, run deploy. If this was a paid custom-domain site whose project file was lost, use recover.`
|
|
1710
1627
|
);
|
|
1711
1628
|
}
|
|
1712
1629
|
return state.file;
|
|
@@ -1753,12 +1670,12 @@ function toolError(e) {
|
|
|
1753
1670
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1754
1671
|
import { promises as fs2 } from "node:fs";
|
|
1755
1672
|
import { join as join6, resolve as resolve4 } from "node:path";
|
|
1756
|
-
import { z as
|
|
1673
|
+
import { z as z2 } from "zod";
|
|
1757
1674
|
|
|
1758
1675
|
// src/recovery-archive.ts
|
|
1759
1676
|
import { existsSync as existsSync3, realpathSync as realpathSync2 } from "node:fs";
|
|
1760
1677
|
import { mkdtemp, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
1761
|
-
import { dirname as
|
|
1678
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, join as join4, relative as relative2, resolve as resolve3, sep as sep3 } from "node:path";
|
|
1762
1679
|
|
|
1763
1680
|
// ../../node_modules/fflate/esm/index.mjs
|
|
1764
1681
|
import { createRequire } from "module";
|
|
@@ -2177,15 +2094,15 @@ function strFromU8(dat, latin1) {
|
|
|
2177
2094
|
var slzh = function(d, b) {
|
|
2178
2095
|
return b + 30 + b2(d, b + 26) + b2(d, b + 28);
|
|
2179
2096
|
};
|
|
2180
|
-
var zh = function(d, b,
|
|
2097
|
+
var zh = function(d, b, z5) {
|
|
2181
2098
|
var fnl = b2(d, b + 28), efl = b2(d, b + 30), fn = strFromU8(d.subarray(b + 46, b + 46 + fnl), !(b2(d, b + 8) & 2048)), es = b + 46 + fnl;
|
|
2182
|
-
var _a2 = z64hs(d, es, efl,
|
|
2099
|
+
var _a2 = z64hs(d, es, efl, z5, b4(d, b + 20), b4(d, b + 24), b4(d, b + 42)), sc = _a2[0], su = _a2[1], off = _a2[2];
|
|
2183
2100
|
return [b2(d, b + 10), sc, su, fn, es + efl + b2(d, b + 32), off];
|
|
2184
2101
|
};
|
|
2185
|
-
var z64hs = function(d, b, l,
|
|
2102
|
+
var z64hs = function(d, b, l, z5, sc, su, off) {
|
|
2186
2103
|
var nsc = sc == 4294967295, nsu = su == 4294967295, noff = off == 4294967295, e = b + l;
|
|
2187
2104
|
var nf = nsc + nsu + noff;
|
|
2188
|
-
if (
|
|
2105
|
+
if (z5 && nf) {
|
|
2189
2106
|
for (; b + 4 < e; b += 4 + b2(d, b + 2)) {
|
|
2190
2107
|
if (b2(d, b) == 1) {
|
|
2191
2108
|
return [
|
|
@@ -2196,7 +2113,7 @@ var z64hs = function(d, b, l, z6, sc, su, off) {
|
|
|
2196
2113
|
];
|
|
2197
2114
|
}
|
|
2198
2115
|
}
|
|
2199
|
-
if (
|
|
2116
|
+
if (z5 < 2)
|
|
2200
2117
|
err(13);
|
|
2201
2118
|
}
|
|
2202
2119
|
return [sc, su, off, 0];
|
|
@@ -2213,18 +2130,18 @@ function unzipSync(data, opts) {
|
|
|
2213
2130
|
if (!c)
|
|
2214
2131
|
return {};
|
|
2215
2132
|
var o = b4(data, e + 16);
|
|
2216
|
-
var
|
|
2217
|
-
if (
|
|
2133
|
+
var z5 = b4(data, e - 20) == 117853008;
|
|
2134
|
+
if (z5) {
|
|
2218
2135
|
var ze = b4(data, e - 12);
|
|
2219
|
-
|
|
2220
|
-
if (
|
|
2136
|
+
z5 = b4(data, ze) == 101075792;
|
|
2137
|
+
if (z5) {
|
|
2221
2138
|
c = b4(data, ze + 32);
|
|
2222
2139
|
o = b4(data, ze + 48);
|
|
2223
2140
|
}
|
|
2224
2141
|
}
|
|
2225
2142
|
var fltr = opts && opts.filter;
|
|
2226
2143
|
for (var i = 0; i < c; ++i) {
|
|
2227
|
-
var _a2 = zh(data, o,
|
|
2144
|
+
var _a2 = zh(data, o, z5), c_2 = _a2[0], sc = _a2[1], su = _a2[2], fn = _a2[3], no = _a2[4], off = _a2[5], b = slzh(data, off);
|
|
2228
2145
|
o = no;
|
|
2229
2146
|
if (!fltr || fltr({
|
|
2230
2147
|
name: fn,
|
|
@@ -2259,7 +2176,7 @@ function safeOutputPath(projectDir, outputDir) {
|
|
|
2259
2176
|
}
|
|
2260
2177
|
let existingAncestor = target;
|
|
2261
2178
|
while (!existsSync3(existingAncestor)) {
|
|
2262
|
-
const parent =
|
|
2179
|
+
const parent = dirname2(existingAncestor);
|
|
2263
2180
|
if (parent === existingAncestor) break;
|
|
2264
2181
|
existingAncestor = parent;
|
|
2265
2182
|
}
|
|
@@ -2376,7 +2293,7 @@ async function extractRecoveryArchive(input) {
|
|
|
2376
2293
|
for (const [rawName, data] of entries) {
|
|
2377
2294
|
const name = safeEntryName(rawName);
|
|
2378
2295
|
const destination = join4(tempDir, ...name.split("/"));
|
|
2379
|
-
await mkdir(
|
|
2296
|
+
await mkdir(dirname2(destination), { recursive: true });
|
|
2380
2297
|
await writeFile(destination, data, { flag: "wx" });
|
|
2381
2298
|
writtenBytes += data.byteLength;
|
|
2382
2299
|
}
|
|
@@ -2386,7 +2303,7 @@ async function extractRecoveryArchive(input) {
|
|
|
2386
2303
|
"Extracted recovery data does not match site metadata"
|
|
2387
2304
|
);
|
|
2388
2305
|
}
|
|
2389
|
-
await mkdir(
|
|
2306
|
+
await mkdir(dirname2(outputDir), { recursive: true });
|
|
2390
2307
|
await rename(tempDir, outputDir);
|
|
2391
2308
|
return {
|
|
2392
2309
|
outputDir,
|
|
@@ -2403,7 +2320,7 @@ async function extractRecoveryArchive(input) {
|
|
|
2403
2320
|
// src/creation-registry.ts
|
|
2404
2321
|
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
2405
2322
|
import { homedir as homedir2 } from "node:os";
|
|
2406
|
-
import { dirname as
|
|
2323
|
+
import { dirname as dirname3, join as join5 } from "node:path";
|
|
2407
2324
|
var RECENT_WINDOW_MS = FREE_SITE_TTL_HOURS * 60 * 60 * 1e3;
|
|
2408
2325
|
function creationRegistryPath() {
|
|
2409
2326
|
const base = process.env["SAKUPA_STATE_DIR"] ?? homedir2();
|
|
@@ -2424,7 +2341,7 @@ function readAll() {
|
|
|
2424
2341
|
}
|
|
2425
2342
|
function writeAll(records) {
|
|
2426
2343
|
const path = creationRegistryPath();
|
|
2427
|
-
mkdirSync3(
|
|
2344
|
+
mkdirSync3(dirname3(path), { recursive: true });
|
|
2428
2345
|
writeFileSync3(path, `${JSON.stringify(records, null, 2)}
|
|
2429
2346
|
`, "utf-8");
|
|
2430
2347
|
}
|
|
@@ -2600,12 +2517,12 @@ ${JSON.stringify(obj, null, 2)}`;
|
|
|
2600
2517
|
nextActions: []
|
|
2601
2518
|
});
|
|
2602
2519
|
}
|
|
2603
|
-
var planEnum =
|
|
2604
|
-
var severityEnum =
|
|
2520
|
+
var planEnum = z2.enum(["water", "personal", "share", "business"]);
|
|
2521
|
+
var severityEnum = z2.enum(["low", "medium", "high", "critical"]);
|
|
2605
2522
|
function planCatalog() {
|
|
2606
2523
|
return TIER_ORDER.map((p) => `${p} JPY ${tierPriceJpy(p)}/month`).join(", ");
|
|
2607
2524
|
}
|
|
2608
|
-
var ticketCategoryEnum =
|
|
2525
|
+
var ticketCategoryEnum = z2.enum([
|
|
2609
2526
|
"billing",
|
|
2610
2527
|
"payment",
|
|
2611
2528
|
"refund_review",
|
|
@@ -2725,7 +2642,7 @@ function freeSiteCreationBarrier(apiBaseUrl) {
|
|
|
2725
2642
|
|
|
2726
2643
|
` + recent.map((r) => `- ${r.url} (project: ${r.projectDir}, created: ${r.createdAt})`).join("\n") + `
|
|
2727
2644
|
|
|
2728
|
-
How a slot frees up: (1) delete one of the sites above \u2014 run delete with that site's
|
|
2645
|
+
How a slot frees up: (1) delete one of the sites above \u2014 run delete with that site opened as the AI tool's current project; its slot frees immediately; (2) every record expires on its own 24 hours after creation; (3) a site that upgrades to a paid plan stops counting the next time any tool sees it. Deleting a project's .sakupa folder does NOT free a slot: this registry lives in the home directory and the server still counts the live site.
|
|
2729
2646
|
|
|
2730
2647
|
If this list is stale (sites deleted or subscribed from another machine), remove the local registry file at ${registryPath} and retry \u2014 that only skips this local precheck; the server still enforces the same per-IP limit and is the final authority.`,
|
|
2731
2648
|
{ recentCreations: recent, limit: FREE_ACTIVE_SITES_PER_IP, registryPath },
|
|
@@ -2741,13 +2658,12 @@ function registerTools(server, baseCtx) {
|
|
|
2741
2658
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
2742
2659
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
2743
2660
|
inputSchema: {
|
|
2744
|
-
|
|
2745
|
-
outputDir: z3.string().optional().describe("Output directory relative to the project root (overrides detection).")
|
|
2661
|
+
outputDir: z2.string().optional().describe("Output directory relative to the project root (overrides detection).")
|
|
2746
2662
|
}
|
|
2747
2663
|
},
|
|
2748
2664
|
async (args) => {
|
|
2749
2665
|
try {
|
|
2750
|
-
const ctx = withProjectDir(baseCtx
|
|
2666
|
+
const ctx = withProjectDir(baseCtx);
|
|
2751
2667
|
const analysis = await analyzeProject(ctx.projectDir, {
|
|
2752
2668
|
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
|
|
2753
2669
|
});
|
|
@@ -2765,32 +2681,31 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
2765
2681
|
server.registerTool(
|
|
2766
2682
|
"deploy",
|
|
2767
2683
|
{
|
|
2768
|
-
description: `Deploy the local static output to Sakupa. First deploy creates a free temporary site (valid ${FREE_SITE_TTL_HOURS}h, public URL like https://${previewHostPattern}) and stores the management credential in .sakupa/site.json. Later runs update the existing site (free sites also refresh their validity; subscribed sites are permanent). Runs analyze first and refuses to upload source projects, secrets, .env files, archives, media or server code.
|
|
2684
|
+
description: `Deploy the local static output to Sakupa. First deploy creates a free temporary site (valid ${FREE_SITE_TTL_HOURS}h, public URL like https://${previewHostPattern}) and stores the management credential in .sakupa/site.json. Later runs update the existing site (free sites also refresh their validity; subscribed sites are permanent). Runs analyze first and refuses to upload source projects, secrets, .env files, archives, media or server code. The MCP process is locked to the current directory initialized by \`npx -y @sakupa/mcp@latest init\`; no tool argument can change that root. outputDir is a separate REQUIRED relative path supplied from the current project inspection. Never uploads anything when analysis says the project is not deployable.`,
|
|
2769
2685
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
2770
2686
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
2771
2687
|
inputSchema: {
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
'REQUIRED: exact publish directory relative to the initialized project root, supplied by the AI after inspecting this project (for example ".", "dist", "html", or any custom build directory). Sakupa resolves projectDir upward before applying this path.'
|
|
2688
|
+
outputDir: z2.string().min(1).describe(
|
|
2689
|
+
'REQUIRED: exact publish directory relative to the initialized project root, supplied by the AI after inspecting this project (for example ".", "dist", "html", or any custom build directory). Sakupa applies it only inside the cwd-locked project.'
|
|
2775
2690
|
),
|
|
2776
|
-
outputDirChangeConfirmed:
|
|
2691
|
+
outputDirChangeConfirmed: z2.boolean().optional().describe(
|
|
2777
2692
|
"Required only when changing the previously successful publish directory. Confirm only after showing the old and new directories to the user."
|
|
2778
2693
|
),
|
|
2779
|
-
spaFallback:
|
|
2694
|
+
spaFallback: z2.boolean().optional().describe(
|
|
2780
2695
|
"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."
|
|
2781
2696
|
),
|
|
2782
|
-
publicConfirmed:
|
|
2697
|
+
publicConfirmed: z2.boolean().optional().describe(
|
|
2783
2698
|
"Required only for the first deployment: user explicitly confirmed creation of a public 24-hour URL."
|
|
2784
2699
|
),
|
|
2785
|
-
subprojectConfirmed:
|
|
2700
|
+
subprojectConfirmed: z2.boolean().optional().describe(
|
|
2786
2701
|
"Deprecated compatibility field. Project independence is established only by `sakupa-mcp init`, never inferred from package.json or folder names."
|
|
2787
2702
|
),
|
|
2788
|
-
lang:
|
|
2703
|
+
lang: z2.string().optional().describe("Site language override (en | ja | zh-CN); defaults to the html lang.")
|
|
2789
2704
|
}
|
|
2790
2705
|
},
|
|
2791
2706
|
async (args) => {
|
|
2792
2707
|
try {
|
|
2793
|
-
const ctx = withProjectDir(baseCtx
|
|
2708
|
+
const ctx = withProjectDir(baseCtx);
|
|
2794
2709
|
const analysis = await analyzeProject(ctx.projectDir, { outputDir: args.outputDir });
|
|
2795
2710
|
if (!analysis.deployable || !analysis.files) {
|
|
2796
2711
|
return notDeployableResult(analysis);
|
|
@@ -2842,7 +2757,7 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
2842
2757
|
if (outputSiteState.kind === "corrupted") {
|
|
2843
2758
|
return text(
|
|
2844
2759
|
"output_site_file_corrupted",
|
|
2845
|
-
`A misplaced .sakupa/site.json exists in output directory ${outputAbs}, but it is damaged: ${outputSiteState.problem}. Repair that file before retrying with
|
|
2760
|
+
`A misplaced .sakupa/site.json exists in output directory ${outputAbs}, but it is damaged: ${outputSiteState.problem}. Repair that file before retrying with the MCP still opened at ${ctx.projectDir}. Nothing was deployed and no site was created.`,
|
|
2846
2761
|
{ projectRoot: ctx.projectDir, outputDir: analysis.recommendedOutputDir },
|
|
2847
2762
|
"blocked"
|
|
2848
2763
|
);
|
|
@@ -2962,7 +2877,6 @@ ${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
|
|
|
2962
2877
|
const { uploaded, finalized } = update;
|
|
2963
2878
|
writeSiteFile(ctx.projectDir, { ...existing, url: finalized.url });
|
|
2964
2879
|
if (credentialRelocatedFrom !== null) deleteSiteFile(credentialRelocatedFrom);
|
|
2965
|
-
if (ctx.projectMarker === void 0) initializeProject(ctx.projectDir);
|
|
2966
2880
|
updateProjectOutputDir(ctx.projectDir, effectiveOutputDir);
|
|
2967
2881
|
noteSiteMode(existing.siteId, finalized.mode);
|
|
2968
2882
|
return text(
|
|
@@ -3002,11 +2916,11 @@ ${JSON.stringify(finalized.warnings, null, 2)}` : ""),
|
|
|
3002
2916
|
description: "Refresh the validity of the free temporary site WITHOUT uploading content. Uses the local credential in .sakupa/site.json. Subscribed sites are permanent and need no refresh.",
|
|
3003
2917
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
3004
2918
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
3005
|
-
inputSchema: {
|
|
2919
|
+
inputSchema: {}
|
|
3006
2920
|
},
|
|
3007
|
-
async (
|
|
2921
|
+
async () => {
|
|
3008
2922
|
try {
|
|
3009
|
-
const ctx = withProjectDir(baseCtx
|
|
2923
|
+
const ctx = withProjectDir(baseCtx);
|
|
3010
2924
|
const site = requireSiteFile(ctx);
|
|
3011
2925
|
const res = await ctx.client.refreshSite(site.siteId, site.credential);
|
|
3012
2926
|
return text(
|
|
@@ -3026,11 +2940,11 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
|
|
|
3026
2940
|
description: "Show the current status of this project's Sakupa site: URL, mode (free/paid), expiry, custom domains, size, last deployment and warnings. For a paid site this tool also automatically returns the complete authoritative billing snapshot; users never need to know or name a separate billing tool to get accurate subscription information.",
|
|
3027
2941
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
3028
2942
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
3029
|
-
inputSchema: {
|
|
2943
|
+
inputSchema: {}
|
|
3030
2944
|
},
|
|
3031
|
-
async (
|
|
2945
|
+
async () => {
|
|
3032
2946
|
try {
|
|
3033
|
-
const ctx = withProjectDir(baseCtx
|
|
2947
|
+
const ctx = withProjectDir(baseCtx);
|
|
3034
2948
|
const site = requireSiteFile(ctx);
|
|
3035
2949
|
const res = await ctx.client.getSiteStatus(site.siteId, site.credential);
|
|
3036
2950
|
noteSiteMode(res.siteId, res.mode);
|
|
@@ -3058,7 +2972,6 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
|
|
|
3058
2972
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
3059
2973
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
3060
2974
|
inputSchema: {
|
|
3061
|
-
projectDir: projectDirInput,
|
|
3062
2975
|
plan: planEnum.describe(
|
|
3063
2976
|
"Monthly plan: water (very light personal pages), personal (personal brand / small shop), share (small-business site), business (steadier traffic, more headroom)."
|
|
3064
2977
|
)
|
|
@@ -3066,7 +2979,7 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
|
|
|
3066
2979
|
},
|
|
3067
2980
|
async (args) => {
|
|
3068
2981
|
try {
|
|
3069
|
-
const ctx = withProjectDir(baseCtx
|
|
2982
|
+
const ctx = withProjectDir(baseCtx);
|
|
3070
2983
|
const site = requireSiteFile(ctx);
|
|
3071
2984
|
const res = await ctx.client.createPlanCheckout(
|
|
3072
2985
|
{
|
|
@@ -3105,17 +3018,16 @@ Once payment confirms, the site becomes permanent on its current URL. Binding a
|
|
|
3105
3018
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
3106
3019
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
3107
3020
|
inputSchema: {
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
verificationId: z3.string().optional().describe(
|
|
3021
|
+
action: z2.enum(["start", "status"]),
|
|
3022
|
+
hostname: z2.string().optional().describe("Required for start."),
|
|
3023
|
+
verificationId: z2.string().optional().describe(
|
|
3112
3024
|
"Optional for status: when omitted, the server finds this site's latest binding verification \u2014 a NEW session can resume without it."
|
|
3113
3025
|
)
|
|
3114
3026
|
}
|
|
3115
3027
|
},
|
|
3116
3028
|
async (args) => {
|
|
3117
3029
|
try {
|
|
3118
|
-
const ctx = withProjectDir(baseCtx
|
|
3030
|
+
const ctx = withProjectDir(baseCtx);
|
|
3119
3031
|
const site = requireSiteFile(ctx);
|
|
3120
3032
|
if (args.action === "status") {
|
|
3121
3033
|
const res2 = args.verificationId ? await ctx.client.checkVerification(args.verificationId, site.credential) : await ctx.client.checkVerification("latest", site.credential, site.siteId);
|
|
@@ -3220,11 +3132,11 @@ When the user says the TXT is set, run bind "status". It verifies ownership and
|
|
|
3220
3132
|
description: "Return the sole authoritative source for this site's hosting subscription: current plan, next renewal plan or cancellation, effective time, payment state, current paid entitlement, reconciled usage, estimated usage tier, bound custom domains and risks. Owner-only (uses the credential in .sakupa/site.json).",
|
|
3221
3133
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
3222
3134
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
3223
|
-
inputSchema: {
|
|
3135
|
+
inputSchema: {}
|
|
3224
3136
|
},
|
|
3225
|
-
async (
|
|
3137
|
+
async () => {
|
|
3226
3138
|
try {
|
|
3227
|
-
const ctx = withProjectDir(baseCtx
|
|
3139
|
+
const ctx = withProjectDir(baseCtx);
|
|
3228
3140
|
const site = requireSiteFile(ctx);
|
|
3229
3141
|
const res = await ctx.client.getBillingStatus(site.siteId, site.credential);
|
|
3230
3142
|
noteSiteMode(res.siteId, res.mode);
|
|
@@ -3262,14 +3174,13 @@ Full status:`, res);
|
|
|
3262
3174
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
3263
3175
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
3264
3176
|
inputSchema: {
|
|
3265
|
-
|
|
3266
|
-
scope: z3.enum(["site", "public_recovery"])
|
|
3177
|
+
scope: z2.enum(["site", "public_recovery"])
|
|
3267
3178
|
}
|
|
3268
3179
|
},
|
|
3269
3180
|
async (args) => {
|
|
3270
3181
|
try {
|
|
3271
3182
|
if (args.scope === "site") {
|
|
3272
|
-
const ctx = withProjectDir(baseCtx
|
|
3183
|
+
const ctx = withProjectDir(baseCtx);
|
|
3273
3184
|
const site = requireSiteFile(ctx);
|
|
3274
3185
|
const res2 = await ctx.client.createBillingPortal(site.siteId, site.credential);
|
|
3275
3186
|
return structuredToolResult({
|
|
@@ -3319,19 +3230,18 @@ Full status:`, res);
|
|
|
3319
3230
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
3320
3231
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
3321
3232
|
inputSchema: {
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
outputDir: z3.string().optional().describe(
|
|
3233
|
+
action: z2.enum(["start", "status", "complete", "download"]),
|
|
3234
|
+
hostname: z2.string().optional().describe("Required for start."),
|
|
3235
|
+
verificationId: z2.string().optional().describe("For status or complete; inferred from local recovery state when omitted."),
|
|
3236
|
+
outputDir: z2.string().optional().describe(
|
|
3327
3237
|
"REQUIRED for complete/download: exact extraction directory relative to the initialized project root. Inspect the current project; Sakupa never guesses a name."
|
|
3328
3238
|
),
|
|
3329
|
-
preserveExistingCredentials:
|
|
3239
|
+
preserveExistingCredentials: z2.boolean().optional().describe("Explicitly keep old local credentials working (default: revoke them all).")
|
|
3330
3240
|
}
|
|
3331
3241
|
},
|
|
3332
3242
|
async (args) => {
|
|
3333
3243
|
try {
|
|
3334
|
-
const ctx = withProjectDir(baseCtx
|
|
3244
|
+
const ctx = withProjectDir(baseCtx);
|
|
3335
3245
|
if ((args.action === "complete" || args.action === "download") && args.outputDir === void 0) {
|
|
3336
3246
|
throw new LocalGuidanceError(
|
|
3337
3247
|
"invalid_request",
|
|
@@ -3404,7 +3314,6 @@ No DNS verification was started or repeated.`,
|
|
|
3404
3314
|
{
|
|
3405
3315
|
tool: "recover",
|
|
3406
3316
|
arguments: {
|
|
3407
|
-
projectDir: ctx.projectDir,
|
|
3408
3317
|
action: "download",
|
|
3409
3318
|
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
|
|
3410
3319
|
},
|
|
@@ -3431,7 +3340,6 @@ No DNS verification was started or repeated.`,
|
|
|
3431
3340
|
{
|
|
3432
3341
|
tool: "recover",
|
|
3433
3342
|
arguments: {
|
|
3434
|
-
projectDir: ctx.projectDir,
|
|
3435
3343
|
action: "status",
|
|
3436
3344
|
verificationId: pending2.verificationId
|
|
3437
3345
|
},
|
|
@@ -3504,7 +3412,6 @@ After the DNS record resolves, re-run recover with verificationId: "${res2.verif
|
|
|
3504
3412
|
{
|
|
3505
3413
|
tool: "recover",
|
|
3506
3414
|
arguments: {
|
|
3507
|
-
projectDir: ctx.projectDir,
|
|
3508
3415
|
action: "download",
|
|
3509
3416
|
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
|
|
3510
3417
|
},
|
|
@@ -3539,7 +3446,6 @@ After the DNS record resolves, re-run recover with verificationId: "${res2.verif
|
|
|
3539
3446
|
{
|
|
3540
3447
|
tool: "recover",
|
|
3541
3448
|
arguments: {
|
|
3542
|
-
projectDir: ctx.projectDir,
|
|
3543
3449
|
action: "complete",
|
|
3544
3450
|
verificationId,
|
|
3545
3451
|
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
|
|
@@ -3616,7 +3522,6 @@ Files: ${extracted.fileCount}; bytes: ${extracted.totalBytes}
|
|
|
3616
3522
|
{
|
|
3617
3523
|
tool: "recover",
|
|
3618
3524
|
arguments: {
|
|
3619
|
-
projectDir: ctx.projectDir,
|
|
3620
3525
|
action: "download",
|
|
3621
3526
|
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
|
|
3622
3527
|
},
|
|
@@ -3637,16 +3542,15 @@ Files: ${extracted.fileCount}; bytes: ${extracted.totalBytes}
|
|
|
3637
3542
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
3638
3543
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
3639
3544
|
inputSchema: {
|
|
3640
|
-
projectDir: projectDirInput,
|
|
3641
3545
|
category: ticketCategoryEnum,
|
|
3642
|
-
subject:
|
|
3643
|
-
description:
|
|
3644
|
-
contactEmail:
|
|
3546
|
+
subject: z2.string().describe("Short subject line."),
|
|
3547
|
+
description: z2.string().describe("Problem description (no secrets, no card data)."),
|
|
3548
|
+
contactEmail: z2.string().optional().describe("Optional contact email for follow-up.")
|
|
3645
3549
|
}
|
|
3646
3550
|
},
|
|
3647
3551
|
async (args) => {
|
|
3648
3552
|
try {
|
|
3649
|
-
const ctx = withProjectDir(baseCtx
|
|
3553
|
+
const ctx = withProjectDir(baseCtx);
|
|
3650
3554
|
const site = requireSiteFile(ctx);
|
|
3651
3555
|
const res = await ctx.client.createTicket(site.credential, {
|
|
3652
3556
|
siteId: site.siteId,
|
|
@@ -3672,26 +3576,25 @@ Files: ${extracted.fileCount}; bytes: ${extracted.totalBytes}
|
|
|
3672
3576
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
3673
3577
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
3674
3578
|
inputSchema: {
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
deploymentId: z3.string().optional(),
|
|
3579
|
+
toolName: z2.string().describe('The Sakupa tool that failed, e.g. "deploy".'),
|
|
3580
|
+
errorCode: z2.string().optional(),
|
|
3581
|
+
errorMessage: z2.string().optional().describe("Sanitized error message (no secrets)."),
|
|
3582
|
+
requestId: z2.string().optional(),
|
|
3583
|
+
deploymentId: z2.string().optional(),
|
|
3681
3584
|
severity: severityEnum.optional(),
|
|
3682
|
-
description:
|
|
3683
|
-
agentContext:
|
|
3585
|
+
description: z2.string().optional().describe("What happened, in the user's words (no secrets)."),
|
|
3586
|
+
agentContext: z2.string().optional().describe(
|
|
3684
3587
|
"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."
|
|
3685
3588
|
),
|
|
3686
|
-
contactEmail:
|
|
3589
|
+
contactEmail: z2.string().optional().describe(
|
|
3687
3590
|
"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."
|
|
3688
3591
|
),
|
|
3689
|
-
confirmSubmit:
|
|
3592
|
+
confirmSubmit: z2.boolean().optional().describe("User reviewed the report payload and approved submission.")
|
|
3690
3593
|
}
|
|
3691
3594
|
},
|
|
3692
3595
|
async (args) => {
|
|
3693
3596
|
try {
|
|
3694
|
-
const ctx = withProjectDir(baseCtx
|
|
3597
|
+
const ctx = withProjectDir(baseCtx);
|
|
3695
3598
|
const siteState = loadSiteFile(ctx.projectDir);
|
|
3696
3599
|
const site = siteState.kind === "ok" ? siteState.file : null;
|
|
3697
3600
|
const diagnostics = {
|
|
@@ -3739,21 +3642,21 @@ Summary: ${res.sanitizedSummary}`,
|
|
|
3739
3642
|
|
|
3740
3643
|
// src/tools/lifecycle.ts
|
|
3741
3644
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
3742
|
-
import { z as
|
|
3743
|
-
var deleteConfirmation =
|
|
3744
|
-
siteId:
|
|
3745
|
-
expectedSiteUpdatedAt:
|
|
3746
|
-
expectedStatus:
|
|
3747
|
-
expectedMode:
|
|
3748
|
-
expectedServingMode:
|
|
3749
|
-
expectedShortId:
|
|
3750
|
-
expectedSubscriptionStatus:
|
|
3751
|
-
expectedPlan:
|
|
3752
|
-
expectedCancelAtPeriodEnd:
|
|
3753
|
-
expectedCurrentPeriodEnd:
|
|
3754
|
-
expectedLastDeploymentId:
|
|
3755
|
-
expectedBoundHostnames:
|
|
3756
|
-
acknowledge:
|
|
3645
|
+
import { z as z3 } from "zod";
|
|
3646
|
+
var deleteConfirmation = z3.object({
|
|
3647
|
+
siteId: z3.string().min(1),
|
|
3648
|
+
expectedSiteUpdatedAt: z3.string().datetime(),
|
|
3649
|
+
expectedStatus: z3.enum(["active", "expired", "deleted"]),
|
|
3650
|
+
expectedMode: z3.enum(["free", "paid"]),
|
|
3651
|
+
expectedServingMode: z3.enum(["normal", "over_limit_notice", "risk_notice", "stopped"]),
|
|
3652
|
+
expectedShortId: z3.string().optional(),
|
|
3653
|
+
expectedSubscriptionStatus: z3.enum(["incomplete", "active", "past_due", "canceled"]).optional(),
|
|
3654
|
+
expectedPlan: z3.enum(["water", "personal", "share", "business"]).optional(),
|
|
3655
|
+
expectedCancelAtPeriodEnd: z3.boolean().optional(),
|
|
3656
|
+
expectedCurrentPeriodEnd: z3.string().datetime().optional(),
|
|
3657
|
+
expectedLastDeploymentId: z3.string().optional(),
|
|
3658
|
+
expectedBoundHostnames: z3.array(z3.string()),
|
|
3659
|
+
acknowledge: z3.literal("delete_and_cancel_renewal")
|
|
3757
3660
|
});
|
|
3758
3661
|
function registerLifecycleTools(server, baseCtx) {
|
|
3759
3662
|
server.registerTool(
|
|
@@ -3761,9 +3664,8 @@ function registerLifecycleTools(server, baseCtx) {
|
|
|
3761
3664
|
{
|
|
3762
3665
|
description: "Preview or execute deletion of this Sakupa site. Execution requires an exact server-validated confirmation bound to the current site state. Paid sites must first cancel renewal through portal and return to free mode. For a temporary pause, publish a pause notice as index.html with deploy instead of deleting the site.",
|
|
3763
3666
|
inputSchema: {
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
operationId: z4.string().min(1).optional(),
|
|
3667
|
+
action: z3.enum(["preview", "confirm"]),
|
|
3668
|
+
operationId: z3.string().min(1).optional(),
|
|
3767
3669
|
confirmation: deleteConfirmation.optional()
|
|
3768
3670
|
},
|
|
3769
3671
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
@@ -3771,7 +3673,7 @@ function registerLifecycleTools(server, baseCtx) {
|
|
|
3771
3673
|
},
|
|
3772
3674
|
async (args) => {
|
|
3773
3675
|
try {
|
|
3774
|
-
const ctx = withProjectDir(baseCtx
|
|
3676
|
+
const ctx = withProjectDir(baseCtx);
|
|
3775
3677
|
const site = requireSiteFile(ctx);
|
|
3776
3678
|
const operationId = args.operationId ?? randomUUID3();
|
|
3777
3679
|
if (args.action === "preview") {
|
|
@@ -3842,7 +3744,7 @@ function registerLifecycleTools(server, baseCtx) {
|
|
|
3842
3744
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3843
3745
|
|
|
3844
3746
|
// src/tools/billing.ts
|
|
3845
|
-
import { z as
|
|
3747
|
+
import { z as z4 } from "zod";
|
|
3846
3748
|
function registerBillingTools(server, baseCtx) {
|
|
3847
3749
|
server.registerTool(
|
|
3848
3750
|
"plans",
|
|
@@ -3873,15 +3775,14 @@ function registerBillingTools(server, baseCtx) {
|
|
|
3873
3775
|
{
|
|
3874
3776
|
description: "Create one Stripe-hosted subscription-management link. The user chooses the plan or period-end cancellation on Stripe; Sakupa never infers intent from the conversation. Creating the link does not change billing.",
|
|
3875
3777
|
inputSchema: {
|
|
3876
|
-
|
|
3877
|
-
operationId: z5.string().min(1)
|
|
3778
|
+
operationId: z4.string().min(1)
|
|
3878
3779
|
},
|
|
3879
3780
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
3880
3781
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
|
|
3881
3782
|
},
|
|
3882
3783
|
async (args) => {
|
|
3883
3784
|
try {
|
|
3884
|
-
const ctx = withProjectDir(baseCtx
|
|
3785
|
+
const ctx = withProjectDir(baseCtx);
|
|
3885
3786
|
const site = requireSiteFile(ctx);
|
|
3886
3787
|
const result = await ctx.client.changeSubscriptionPlan(site.credential, {
|
|
3887
3788
|
siteId: site.siteId,
|
|
@@ -3943,21 +3844,21 @@ Workflow:
|
|
|
3943
3844
|
sanitized diagnostic report after the user explicitly confirms it.
|
|
3944
3845
|
|
|
3945
3846
|
Project directory contract: before the first deploy or a new recovery, initialize the intended
|
|
3946
|
-
project
|
|
3947
|
-
|
|
3948
|
-
ONE
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
.
|
|
3952
|
-
|
|
3953
|
-
|
|
3847
|
+
project once by running "npx -y @sakupa/mcp@latest init" with NO path argument from the AI
|
|
3848
|
+
tool's current project directory. This immediately creates the non-secret .sakupa/project.json
|
|
3849
|
+
there. ONE MCP process = ONE cwd-locked project = ONE site. Site tools do not accept projectDir
|
|
3850
|
+
and cannot select another root; plans and public_recovery portal remain project-independent.
|
|
3851
|
+
Sakupa stores .sakupa/site.json and recovery state only in the locked directory; it never uses
|
|
3852
|
+
package.json, .git, framework names or output-directory names to guess. For deploy, ALWAYS pass
|
|
3853
|
+
outputDir separately as the exact path RELATIVE to the locked directory (use "." when publishing
|
|
3854
|
+
the root); outputDir is
|
|
3954
3855
|
required and may have ANY name, so inspect the current project. If it differs from the last
|
|
3955
3856
|
successful publish directory, show the old and new paths and obtain explicit confirmation
|
|
3956
3857
|
before retrying with outputDirChangeConfirmed: true.
|
|
3957
3858
|
After every deploy, TELL the user which environment it went to (deploy results carry an
|
|
3958
3859
|
Explicit Environment line: TEST vs PRODUCTION). analyze, deploy, status,
|
|
3959
3860
|
refresh and delete echo
|
|
3960
|
-
the directory they acted on
|
|
3861
|
+
the cwd-locked directory they acted on.
|
|
3961
3862
|
|
|
3962
3863
|
When the same operation fails twice in a row, or the user is clearly stuck or
|
|
3963
3864
|
frustrated, proactively offer report: it files the problem into Sakupa's ticket and
|
|
@@ -3995,7 +3896,11 @@ function createSakupaMcpServer(opts) {
|
|
|
3995
3896
|
{ name: "sakupa", version: MCP_VERSION },
|
|
3996
3897
|
{ instructions: instructionsFor(previewHostPatternFor(opts.apiBaseUrl)) }
|
|
3997
3898
|
);
|
|
3998
|
-
const ctx = {
|
|
3899
|
+
const ctx = {
|
|
3900
|
+
client,
|
|
3901
|
+
apiBaseUrl: opts.apiBaseUrl,
|
|
3902
|
+
projectDir: canonicalProjectDirectory(opts.projectDir ?? process.cwd())
|
|
3903
|
+
};
|
|
3999
3904
|
registerTools(server, ctx);
|
|
4000
3905
|
registerBillingTools(server, ctx);
|
|
4001
3906
|
registerLifecycleTools(server, ctx);
|