@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/bin.js
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/bin.ts
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
-
import {
|
|
6
|
-
import { stdin, stdout } from "node:process";
|
|
5
|
+
import { stdout } from "node:process";
|
|
7
6
|
|
|
8
7
|
// src/project-root.ts
|
|
9
8
|
import { randomUUID } from "node:crypto";
|
|
@@ -20,11 +19,9 @@ import {
|
|
|
20
19
|
writeFileSync
|
|
21
20
|
} from "node:fs";
|
|
22
21
|
import { homedir } from "node:os";
|
|
23
|
-
import {
|
|
22
|
+
import { isAbsolute, join, parse, relative, resolve, sep } from "node:path";
|
|
24
23
|
var SAKUPA_DIR = ".sakupa";
|
|
25
24
|
var PROJECT_FILE = "project.json";
|
|
26
|
-
var SITE_FILE = "site.json";
|
|
27
|
-
var RECOVERY_FILE = "recovery.json";
|
|
28
25
|
var PROJECT_SCHEMA_VERSION = 1;
|
|
29
26
|
var ProjectRootError = class extends Error {
|
|
30
27
|
code;
|
|
@@ -109,65 +106,27 @@ function initializeProject(projectDir) {
|
|
|
109
106
|
marker
|
|
110
107
|
};
|
|
111
108
|
}
|
|
112
|
-
function
|
|
113
|
-
|
|
109
|
+
function resolveLockedProjectRoot(projectDir) {
|
|
110
|
+
const canonical = canonicalProjectDirectory(projectDir);
|
|
111
|
+
assertSafeProjectRoot(canonical);
|
|
112
|
+
const markerState = loadProjectMarker(canonical);
|
|
113
|
+
if (markerState.kind === "corrupted") {
|
|
114
114
|
throw new ProjectRootError(
|
|
115
|
-
"
|
|
116
|
-
`
|
|
115
|
+
"corrupted_marker",
|
|
116
|
+
`Sakupa project marker ${projectMarkerPath(canonical)} is damaged: ${markerState.problem}.`
|
|
117
117
|
);
|
|
118
118
|
}
|
|
119
|
-
|
|
120
|
-
const requestedStat = statSync(canonicalRequested);
|
|
121
|
-
let cursor = requestedStat.isDirectory() ? canonicalRequested : dirname(canonicalRequested);
|
|
122
|
-
const startDevice = statSync(cursor).dev;
|
|
123
|
-
const candidates = [];
|
|
124
|
-
while (true) {
|
|
125
|
-
if (statSync(cursor).dev !== startDevice) break;
|
|
126
|
-
const markerState = loadProjectMarker(cursor);
|
|
127
|
-
const hasSite = existsSync(join(cursor, SAKUPA_DIR, SITE_FILE));
|
|
128
|
-
const hasRecovery = existsSync(join(cursor, SAKUPA_DIR, RECOVERY_FILE));
|
|
129
|
-
if (markerState.kind !== "absent" || hasSite || hasRecovery) {
|
|
130
|
-
candidates.push({ dir: cursor, markerState, hasSite, hasRecovery });
|
|
131
|
-
}
|
|
132
|
-
const parent = dirname(cursor);
|
|
133
|
-
if (parent === cursor) break;
|
|
134
|
-
cursor = parent;
|
|
135
|
-
}
|
|
136
|
-
if (candidates.length === 0) {
|
|
119
|
+
if (markerState.kind === "absent") {
|
|
137
120
|
throw new ProjectRootError(
|
|
138
121
|
"not_initialized",
|
|
139
|
-
`
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
const nearest = candidates[0];
|
|
143
|
-
if (nearest.markerState.kind === "corrupted") {
|
|
144
|
-
throw new ProjectRootError(
|
|
145
|
-
"corrupted_marker",
|
|
146
|
-
`Sakupa project marker ${projectMarkerPath(nearest.dir)} is damaged: ${nearest.markerState.problem}.`
|
|
122
|
+
`The MCP working directory ${canonical} is not initialized. Run \`npx -y @sakupa/mcp@latest init\` in that directory; do not pass a path argument.`
|
|
147
123
|
);
|
|
148
124
|
}
|
|
149
|
-
if (nearest.markerState.kind === "absent") {
|
|
150
|
-
const explicitAncestor = candidates.find((candidate) => candidate.markerState.kind === "ok");
|
|
151
|
-
if (explicitAncestor) {
|
|
152
|
-
throw new ProjectRootError(
|
|
153
|
-
"ambiguous_binding",
|
|
154
|
-
`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.`
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
assertSafeProjectRoot(nearest.dir);
|
|
159
|
-
if (nearest.markerState.kind === "ok") {
|
|
160
|
-
return {
|
|
161
|
-
projectDir: nearest.dir,
|
|
162
|
-
requestedPath: canonicalRequested,
|
|
163
|
-
markerKind: "project",
|
|
164
|
-
marker: nearest.markerState.marker
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
125
|
return {
|
|
168
|
-
projectDir:
|
|
169
|
-
requestedPath:
|
|
170
|
-
markerKind:
|
|
126
|
+
projectDir: canonical,
|
|
127
|
+
requestedPath: canonical,
|
|
128
|
+
markerKind: "project",
|
|
129
|
+
marker: markerState.marker
|
|
171
130
|
};
|
|
172
131
|
}
|
|
173
132
|
function updateProjectOutputDir(projectDir, outputDir) {
|
|
@@ -258,13 +217,13 @@ function writeMarkerAtomically(projectDir, marker) {
|
|
|
258
217
|
}
|
|
259
218
|
|
|
260
219
|
// src/cli-init.ts
|
|
261
|
-
async function runInitCommand(args, io) {
|
|
262
|
-
if (args.length >
|
|
263
|
-
io.write("Usage: sakupa-mcp init
|
|
220
|
+
async function runInitCommand(args, io, cwd = process.cwd()) {
|
|
221
|
+
if (args.length > 0) {
|
|
222
|
+
io.write("Usage: sakupa-mcp init");
|
|
264
223
|
return { exitCode: 2, initialized: false };
|
|
265
224
|
}
|
|
266
225
|
try {
|
|
267
|
-
const projectDir = canonicalProjectDirectory(
|
|
226
|
+
const projectDir = canonicalProjectDirectory(cwd);
|
|
268
227
|
const current = loadProjectMarker(projectDir);
|
|
269
228
|
if (current.kind === "corrupted") {
|
|
270
229
|
io.write(`Refusing to replace damaged Sakupa marker in ${projectDir}: ${current.problem}.`);
|
|
@@ -274,17 +233,9 @@ async function runInitCommand(args, io) {
|
|
|
274
233
|
io.write(`Sakupa project is already initialized: ${projectDir}`);
|
|
275
234
|
return { exitCode: 0, projectDir, initialized: false };
|
|
276
235
|
}
|
|
277
|
-
io.write(`Sakupa project directory: ${projectDir}`);
|
|
278
|
-
const confirmed = await io.confirm(
|
|
279
|
-
"Initialize exactly this directory as one Sakupa project? [y/N] "
|
|
280
|
-
);
|
|
281
|
-
if (!confirmed) {
|
|
282
|
-
io.write("Initialization cancelled; no files were changed.");
|
|
283
|
-
return { exitCode: 0, projectDir, initialized: false };
|
|
284
|
-
}
|
|
285
236
|
initializeProject(projectDir);
|
|
286
237
|
io.write(
|
|
287
|
-
`Initialized ${projectDir}.
|
|
238
|
+
`Initialized the current directory ${projectDir}. Created .sakupa/project.json here; site credentials and recovery state will stay in this .sakupa directory.`
|
|
288
239
|
);
|
|
289
240
|
return { exitCode: 0, projectDir, initialized: true };
|
|
290
241
|
} catch (error) {
|
|
@@ -419,7 +370,7 @@ var FORBIDDEN_PATH_SEGMENTS = [
|
|
|
419
370
|
var ALLOWED_HIDDEN_PATHS = [".well-known/"];
|
|
420
371
|
|
|
421
372
|
// ../core/dist/domain/version.js
|
|
422
|
-
var SAKUPA_MCP_VERSION = "0.7.
|
|
373
|
+
var SAKUPA_MCP_VERSION = "0.7.34";
|
|
423
374
|
|
|
424
375
|
// ../core/dist/domain/errors.js
|
|
425
376
|
var HTTP_STATUS = {
|
|
@@ -981,7 +932,7 @@ var HttpApiClient = class {
|
|
|
981
932
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
982
933
|
import { promises as fs2 } from "node:fs";
|
|
983
934
|
import { join as join6, resolve as resolve4 } from "node:path";
|
|
984
|
-
import { z as
|
|
935
|
+
import { z as z2 } from "zod";
|
|
985
936
|
|
|
986
937
|
// src/analyze/analyzer.ts
|
|
987
938
|
import { promises as fs } from "node:fs";
|
|
@@ -1422,15 +1373,15 @@ import {
|
|
|
1422
1373
|
rmSync,
|
|
1423
1374
|
writeFileSync as writeFileSync2
|
|
1424
1375
|
} from "node:fs";
|
|
1425
|
-
import { dirname
|
|
1376
|
+
import { dirname, join as join3 } from "node:path";
|
|
1426
1377
|
var SITE_DIR = ".sakupa";
|
|
1427
|
-
var
|
|
1428
|
-
var
|
|
1378
|
+
var SITE_FILE = "site.json";
|
|
1379
|
+
var RECOVERY_FILE = "recovery.json";
|
|
1429
1380
|
function siteFilePath(projectDir) {
|
|
1430
|
-
return join3(projectDir, SITE_DIR,
|
|
1381
|
+
return join3(projectDir, SITE_DIR, SITE_FILE);
|
|
1431
1382
|
}
|
|
1432
1383
|
function recoveryFilePath(projectDir) {
|
|
1433
|
-
return join3(projectDir, SITE_DIR,
|
|
1384
|
+
return join3(projectDir, SITE_DIR, RECOVERY_FILE);
|
|
1434
1385
|
}
|
|
1435
1386
|
function loadSiteFile(projectDir) {
|
|
1436
1387
|
const path = siteFilePath(projectDir);
|
|
@@ -1500,7 +1451,7 @@ function loadRecoveryFile(projectDir) {
|
|
|
1500
1451
|
function writeRecoveryFile(projectDir, file) {
|
|
1501
1452
|
const dir = join3(projectDir, SITE_DIR);
|
|
1502
1453
|
mkdirSync2(dir, { recursive: true });
|
|
1503
|
-
const path = join3(dir,
|
|
1454
|
+
const path = join3(dir, RECOVERY_FILE);
|
|
1504
1455
|
writeFileSync2(path, `${JSON.stringify(file, null, 2)}
|
|
1505
1456
|
`, "utf8");
|
|
1506
1457
|
try {
|
|
@@ -1531,7 +1482,7 @@ function writeSiteFile(projectDir, file, opts = {}) {
|
|
|
1531
1482
|
}
|
|
1532
1483
|
const dir = join3(projectDir, SITE_DIR);
|
|
1533
1484
|
mkdirSync2(dir, { recursive: true });
|
|
1534
|
-
const path = join3(dir,
|
|
1485
|
+
const path = join3(dir, SITE_FILE);
|
|
1535
1486
|
writeFileSync2(path, `${JSON.stringify(file, null, 2)}
|
|
1536
1487
|
`, "utf8");
|
|
1537
1488
|
try {
|
|
@@ -1552,7 +1503,7 @@ function deleteSiteFile(projectDir) {
|
|
|
1552
1503
|
function findAncestor(startDir, predicate, maxLevels = Number.POSITIVE_INFINITY) {
|
|
1553
1504
|
let cursor = startDir;
|
|
1554
1505
|
for (let i = 0; i < maxLevels; i += 1) {
|
|
1555
|
-
const parent =
|
|
1506
|
+
const parent = dirname(cursor);
|
|
1556
1507
|
if (parent === cursor) return null;
|
|
1557
1508
|
if (predicate(parent)) return parent;
|
|
1558
1509
|
cursor = parent;
|
|
@@ -1570,7 +1521,7 @@ function credentialGitReminder(projectDir) {
|
|
|
1570
1521
|
// src/recovery-archive.ts
|
|
1571
1522
|
import { existsSync as existsSync3, realpathSync as realpathSync2 } from "node:fs";
|
|
1572
1523
|
import { mkdtemp, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
1573
|
-
import { dirname as
|
|
1524
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, join as join4, relative as relative2, resolve as resolve3, sep as sep3 } from "node:path";
|
|
1574
1525
|
|
|
1575
1526
|
// ../../node_modules/fflate/esm/index.mjs
|
|
1576
1527
|
import { createRequire } from "module";
|
|
@@ -1989,15 +1940,15 @@ function strFromU8(dat, latin1) {
|
|
|
1989
1940
|
var slzh = function(d, b) {
|
|
1990
1941
|
return b + 30 + b2(d, b + 26) + b2(d, b + 28);
|
|
1991
1942
|
};
|
|
1992
|
-
var zh = function(d, b,
|
|
1943
|
+
var zh = function(d, b, z5) {
|
|
1993
1944
|
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;
|
|
1994
|
-
var _a2 = z64hs(d, es, efl,
|
|
1945
|
+
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];
|
|
1995
1946
|
return [b2(d, b + 10), sc, su, fn, es + efl + b2(d, b + 32), off];
|
|
1996
1947
|
};
|
|
1997
|
-
var z64hs = function(d, b, l,
|
|
1948
|
+
var z64hs = function(d, b, l, z5, sc, su, off) {
|
|
1998
1949
|
var nsc = sc == 4294967295, nsu = su == 4294967295, noff = off == 4294967295, e = b + l;
|
|
1999
1950
|
var nf = nsc + nsu + noff;
|
|
2000
|
-
if (
|
|
1951
|
+
if (z5 && nf) {
|
|
2001
1952
|
for (; b + 4 < e; b += 4 + b2(d, b + 2)) {
|
|
2002
1953
|
if (b2(d, b) == 1) {
|
|
2003
1954
|
return [
|
|
@@ -2008,7 +1959,7 @@ var z64hs = function(d, b, l, z6, sc, su, off) {
|
|
|
2008
1959
|
];
|
|
2009
1960
|
}
|
|
2010
1961
|
}
|
|
2011
|
-
if (
|
|
1962
|
+
if (z5 < 2)
|
|
2012
1963
|
err(13);
|
|
2013
1964
|
}
|
|
2014
1965
|
return [sc, su, off, 0];
|
|
@@ -2025,18 +1976,18 @@ function unzipSync(data, opts) {
|
|
|
2025
1976
|
if (!c)
|
|
2026
1977
|
return {};
|
|
2027
1978
|
var o = b4(data, e + 16);
|
|
2028
|
-
var
|
|
2029
|
-
if (
|
|
1979
|
+
var z5 = b4(data, e - 20) == 117853008;
|
|
1980
|
+
if (z5) {
|
|
2030
1981
|
var ze = b4(data, e - 12);
|
|
2031
|
-
|
|
2032
|
-
if (
|
|
1982
|
+
z5 = b4(data, ze) == 101075792;
|
|
1983
|
+
if (z5) {
|
|
2033
1984
|
c = b4(data, ze + 32);
|
|
2034
1985
|
o = b4(data, ze + 48);
|
|
2035
1986
|
}
|
|
2036
1987
|
}
|
|
2037
1988
|
var fltr = opts && opts.filter;
|
|
2038
1989
|
for (var i = 0; i < c; ++i) {
|
|
2039
|
-
var _a2 = zh(data, o,
|
|
1990
|
+
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);
|
|
2040
1991
|
o = no;
|
|
2041
1992
|
if (!fltr || fltr({
|
|
2042
1993
|
name: fn,
|
|
@@ -2071,7 +2022,7 @@ function safeOutputPath(projectDir, outputDir) {
|
|
|
2071
2022
|
}
|
|
2072
2023
|
let existingAncestor = target;
|
|
2073
2024
|
while (!existsSync3(existingAncestor)) {
|
|
2074
|
-
const parent =
|
|
2025
|
+
const parent = dirname2(existingAncestor);
|
|
2075
2026
|
if (parent === existingAncestor) break;
|
|
2076
2027
|
existingAncestor = parent;
|
|
2077
2028
|
}
|
|
@@ -2188,7 +2139,7 @@ async function extractRecoveryArchive(input) {
|
|
|
2188
2139
|
for (const [rawName, data] of entries) {
|
|
2189
2140
|
const name = safeEntryName(rawName);
|
|
2190
2141
|
const destination = join4(tempDir, ...name.split("/"));
|
|
2191
|
-
await mkdir(
|
|
2142
|
+
await mkdir(dirname2(destination), { recursive: true });
|
|
2192
2143
|
await writeFile(destination, data, { flag: "wx" });
|
|
2193
2144
|
writtenBytes += data.byteLength;
|
|
2194
2145
|
}
|
|
@@ -2198,7 +2149,7 @@ async function extractRecoveryArchive(input) {
|
|
|
2198
2149
|
"Extracted recovery data does not match site metadata"
|
|
2199
2150
|
);
|
|
2200
2151
|
}
|
|
2201
|
-
await mkdir(
|
|
2152
|
+
await mkdir(dirname2(outputDir), { recursive: true });
|
|
2202
2153
|
await rename(tempDir, outputDir);
|
|
2203
2154
|
return {
|
|
2204
2155
|
outputDir,
|
|
@@ -2215,7 +2166,7 @@ async function extractRecoveryArchive(input) {
|
|
|
2215
2166
|
// src/creation-registry.ts
|
|
2216
2167
|
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
2217
2168
|
import { homedir as homedir2 } from "node:os";
|
|
2218
|
-
import { dirname as
|
|
2169
|
+
import { dirname as dirname3, join as join5 } from "node:path";
|
|
2219
2170
|
var RECENT_WINDOW_MS = FREE_SITE_TTL_HOURS * 60 * 60 * 1e3;
|
|
2220
2171
|
function creationRegistryPath() {
|
|
2221
2172
|
const base = process.env["SAKUPA_STATE_DIR"] ?? homedir2();
|
|
@@ -2236,7 +2187,7 @@ function readAll() {
|
|
|
2236
2187
|
}
|
|
2237
2188
|
function writeAll(records) {
|
|
2238
2189
|
const path = creationRegistryPath();
|
|
2239
|
-
mkdirSync3(
|
|
2190
|
+
mkdirSync3(dirname3(path), { recursive: true });
|
|
2240
2191
|
writeFileSync3(path, `${JSON.stringify(records, null, 2)}
|
|
2241
2192
|
`, "utf-8");
|
|
2242
2193
|
}
|
|
@@ -2393,9 +2344,6 @@ ${diag.layers}
|
|
|
2393
2344
|
var MCP_VERSION = SAKUPA_MCP_VERSION;
|
|
2394
2345
|
var CLIENT_TYPE = "sakupa-mcp";
|
|
2395
2346
|
|
|
2396
|
-
// src/tools/context.ts
|
|
2397
|
-
import { z as z2 } from "zod";
|
|
2398
|
-
|
|
2399
2347
|
// src/tools/result.ts
|
|
2400
2348
|
import { z } from "zod";
|
|
2401
2349
|
var STRUCTURED_TOOL_OUTPUT_SCHEMA = {
|
|
@@ -2443,24 +2391,15 @@ var LocalGuidanceError = class extends SakupaError {
|
|
|
2443
2391
|
super(code, message);
|
|
2444
2392
|
}
|
|
2445
2393
|
};
|
|
2446
|
-
|
|
2447
|
-
"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."
|
|
2448
|
-
);
|
|
2449
|
-
function withProjectDir(ctx, projectDirArg) {
|
|
2450
|
-
if (projectDirArg === void 0) {
|
|
2451
|
-
throw new LocalGuidanceError(
|
|
2452
|
-
"invalid_request",
|
|
2453
|
-
"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."
|
|
2454
|
-
);
|
|
2455
|
-
}
|
|
2394
|
+
function withProjectDir(ctx) {
|
|
2456
2395
|
try {
|
|
2457
|
-
const resolved =
|
|
2396
|
+
const resolved = resolveLockedProjectRoot(ctx.projectDir);
|
|
2458
2397
|
return {
|
|
2459
2398
|
...ctx,
|
|
2460
2399
|
projectDir: resolved.projectDir,
|
|
2461
2400
|
requestedPath: resolved.requestedPath,
|
|
2462
2401
|
markerKind: resolved.markerKind,
|
|
2463
|
-
|
|
2402
|
+
projectMarker: resolved.marker
|
|
2464
2403
|
};
|
|
2465
2404
|
} catch (error) {
|
|
2466
2405
|
if (error instanceof ProjectRootError) {
|
|
@@ -2483,7 +2422,7 @@ function requireSiteFile(ctx) {
|
|
|
2483
2422
|
if (state.kind === "absent") {
|
|
2484
2423
|
throw new LocalGuidanceError(
|
|
2485
2424
|
"not_found",
|
|
2486
|
-
`No .sakupa/site.json found in ${ctx.projectDir} \u2014 this directory has no Sakupa site binding. If you meant to manage
|
|
2425
|
+
`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.`
|
|
2487
2426
|
);
|
|
2488
2427
|
}
|
|
2489
2428
|
return state.file;
|
|
@@ -2549,12 +2488,12 @@ ${JSON.stringify(obj, null, 2)}`;
|
|
|
2549
2488
|
nextActions: []
|
|
2550
2489
|
});
|
|
2551
2490
|
}
|
|
2552
|
-
var planEnum =
|
|
2553
|
-
var severityEnum =
|
|
2491
|
+
var planEnum = z2.enum(["water", "personal", "share", "business"]);
|
|
2492
|
+
var severityEnum = z2.enum(["low", "medium", "high", "critical"]);
|
|
2554
2493
|
function planCatalog() {
|
|
2555
2494
|
return TIER_ORDER.map((p) => `${p} JPY ${tierPriceJpy(p)}/month`).join(", ");
|
|
2556
2495
|
}
|
|
2557
|
-
var ticketCategoryEnum =
|
|
2496
|
+
var ticketCategoryEnum = z2.enum([
|
|
2558
2497
|
"billing",
|
|
2559
2498
|
"payment",
|
|
2560
2499
|
"refund_review",
|
|
@@ -2674,7 +2613,7 @@ function freeSiteCreationBarrier(apiBaseUrl) {
|
|
|
2674
2613
|
|
|
2675
2614
|
` + recent.map((r) => `- ${r.url} (project: ${r.projectDir}, created: ${r.createdAt})`).join("\n") + `
|
|
2676
2615
|
|
|
2677
|
-
How a slot frees up: (1) delete one of the sites above \u2014 run delete with that site's
|
|
2616
|
+
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.
|
|
2678
2617
|
|
|
2679
2618
|
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.`,
|
|
2680
2619
|
{ recentCreations: recent, limit: FREE_ACTIVE_SITES_PER_IP, registryPath },
|
|
@@ -2690,13 +2629,12 @@ function registerTools(server, baseCtx) {
|
|
|
2690
2629
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
2691
2630
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
2692
2631
|
inputSchema: {
|
|
2693
|
-
|
|
2694
|
-
outputDir: z3.string().optional().describe("Output directory relative to the project root (overrides detection).")
|
|
2632
|
+
outputDir: z2.string().optional().describe("Output directory relative to the project root (overrides detection).")
|
|
2695
2633
|
}
|
|
2696
2634
|
},
|
|
2697
2635
|
async (args) => {
|
|
2698
2636
|
try {
|
|
2699
|
-
const ctx = withProjectDir(baseCtx
|
|
2637
|
+
const ctx = withProjectDir(baseCtx);
|
|
2700
2638
|
const analysis = await analyzeProject(ctx.projectDir, {
|
|
2701
2639
|
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
|
|
2702
2640
|
});
|
|
@@ -2714,32 +2652,31 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
2714
2652
|
server.registerTool(
|
|
2715
2653
|
"deploy",
|
|
2716
2654
|
{
|
|
2717
|
-
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.
|
|
2655
|
+
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.`,
|
|
2718
2656
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
2719
2657
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
2720
2658
|
inputSchema: {
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
'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.'
|
|
2659
|
+
outputDir: z2.string().min(1).describe(
|
|
2660
|
+
'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.'
|
|
2724
2661
|
),
|
|
2725
|
-
outputDirChangeConfirmed:
|
|
2662
|
+
outputDirChangeConfirmed: z2.boolean().optional().describe(
|
|
2726
2663
|
"Required only when changing the previously successful publish directory. Confirm only after showing the old and new directories to the user."
|
|
2727
2664
|
),
|
|
2728
|
-
spaFallback:
|
|
2665
|
+
spaFallback: z2.boolean().optional().describe(
|
|
2729
2666
|
"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."
|
|
2730
2667
|
),
|
|
2731
|
-
publicConfirmed:
|
|
2668
|
+
publicConfirmed: z2.boolean().optional().describe(
|
|
2732
2669
|
"Required only for the first deployment: user explicitly confirmed creation of a public 24-hour URL."
|
|
2733
2670
|
),
|
|
2734
|
-
subprojectConfirmed:
|
|
2671
|
+
subprojectConfirmed: z2.boolean().optional().describe(
|
|
2735
2672
|
"Deprecated compatibility field. Project independence is established only by `sakupa-mcp init`, never inferred from package.json or folder names."
|
|
2736
2673
|
),
|
|
2737
|
-
lang:
|
|
2674
|
+
lang: z2.string().optional().describe("Site language override (en | ja | zh-CN); defaults to the html lang.")
|
|
2738
2675
|
}
|
|
2739
2676
|
},
|
|
2740
2677
|
async (args) => {
|
|
2741
2678
|
try {
|
|
2742
|
-
const ctx = withProjectDir(baseCtx
|
|
2679
|
+
const ctx = withProjectDir(baseCtx);
|
|
2743
2680
|
const analysis = await analyzeProject(ctx.projectDir, { outputDir: args.outputDir });
|
|
2744
2681
|
if (!analysis.deployable || !analysis.files) {
|
|
2745
2682
|
return notDeployableResult(analysis);
|
|
@@ -2791,7 +2728,7 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
2791
2728
|
if (outputSiteState.kind === "corrupted") {
|
|
2792
2729
|
return text(
|
|
2793
2730
|
"output_site_file_corrupted",
|
|
2794
|
-
`A misplaced .sakupa/site.json exists in output directory ${outputAbs}, but it is damaged: ${outputSiteState.problem}. Repair that file before retrying with
|
|
2731
|
+
`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.`,
|
|
2795
2732
|
{ projectRoot: ctx.projectDir, outputDir: analysis.recommendedOutputDir },
|
|
2796
2733
|
"blocked"
|
|
2797
2734
|
);
|
|
@@ -2911,7 +2848,6 @@ ${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
|
|
|
2911
2848
|
const { uploaded, finalized } = update;
|
|
2912
2849
|
writeSiteFile(ctx.projectDir, { ...existing, url: finalized.url });
|
|
2913
2850
|
if (credentialRelocatedFrom !== null) deleteSiteFile(credentialRelocatedFrom);
|
|
2914
|
-
if (ctx.projectMarker === void 0) initializeProject(ctx.projectDir);
|
|
2915
2851
|
updateProjectOutputDir(ctx.projectDir, effectiveOutputDir);
|
|
2916
2852
|
noteSiteMode(existing.siteId, finalized.mode);
|
|
2917
2853
|
return text(
|
|
@@ -2951,11 +2887,11 @@ ${JSON.stringify(finalized.warnings, null, 2)}` : ""),
|
|
|
2951
2887
|
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.",
|
|
2952
2888
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
2953
2889
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
2954
|
-
inputSchema: {
|
|
2890
|
+
inputSchema: {}
|
|
2955
2891
|
},
|
|
2956
|
-
async (
|
|
2892
|
+
async () => {
|
|
2957
2893
|
try {
|
|
2958
|
-
const ctx = withProjectDir(baseCtx
|
|
2894
|
+
const ctx = withProjectDir(baseCtx);
|
|
2959
2895
|
const site = requireSiteFile(ctx);
|
|
2960
2896
|
const res = await ctx.client.refreshSite(site.siteId, site.credential);
|
|
2961
2897
|
return text(
|
|
@@ -2975,11 +2911,11 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
|
|
|
2975
2911
|
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.",
|
|
2976
2912
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
2977
2913
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
2978
|
-
inputSchema: {
|
|
2914
|
+
inputSchema: {}
|
|
2979
2915
|
},
|
|
2980
|
-
async (
|
|
2916
|
+
async () => {
|
|
2981
2917
|
try {
|
|
2982
|
-
const ctx = withProjectDir(baseCtx
|
|
2918
|
+
const ctx = withProjectDir(baseCtx);
|
|
2983
2919
|
const site = requireSiteFile(ctx);
|
|
2984
2920
|
const res = await ctx.client.getSiteStatus(site.siteId, site.credential);
|
|
2985
2921
|
noteSiteMode(res.siteId, res.mode);
|
|
@@ -3007,7 +2943,6 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
|
|
|
3007
2943
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
3008
2944
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
3009
2945
|
inputSchema: {
|
|
3010
|
-
projectDir: projectDirInput,
|
|
3011
2946
|
plan: planEnum.describe(
|
|
3012
2947
|
"Monthly plan: water (very light personal pages), personal (personal brand / small shop), share (small-business site), business (steadier traffic, more headroom)."
|
|
3013
2948
|
)
|
|
@@ -3015,7 +2950,7 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
|
|
|
3015
2950
|
},
|
|
3016
2951
|
async (args) => {
|
|
3017
2952
|
try {
|
|
3018
|
-
const ctx = withProjectDir(baseCtx
|
|
2953
|
+
const ctx = withProjectDir(baseCtx);
|
|
3019
2954
|
const site = requireSiteFile(ctx);
|
|
3020
2955
|
const res = await ctx.client.createPlanCheckout(
|
|
3021
2956
|
{
|
|
@@ -3054,17 +2989,16 @@ Once payment confirms, the site becomes permanent on its current URL. Binding a
|
|
|
3054
2989
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
3055
2990
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
3056
2991
|
inputSchema: {
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
verificationId: z3.string().optional().describe(
|
|
2992
|
+
action: z2.enum(["start", "status"]),
|
|
2993
|
+
hostname: z2.string().optional().describe("Required for start."),
|
|
2994
|
+
verificationId: z2.string().optional().describe(
|
|
3061
2995
|
"Optional for status: when omitted, the server finds this site's latest binding verification \u2014 a NEW session can resume without it."
|
|
3062
2996
|
)
|
|
3063
2997
|
}
|
|
3064
2998
|
},
|
|
3065
2999
|
async (args) => {
|
|
3066
3000
|
try {
|
|
3067
|
-
const ctx = withProjectDir(baseCtx
|
|
3001
|
+
const ctx = withProjectDir(baseCtx);
|
|
3068
3002
|
const site = requireSiteFile(ctx);
|
|
3069
3003
|
if (args.action === "status") {
|
|
3070
3004
|
const res2 = args.verificationId ? await ctx.client.checkVerification(args.verificationId, site.credential) : await ctx.client.checkVerification("latest", site.credential, site.siteId);
|
|
@@ -3169,11 +3103,11 @@ When the user says the TXT is set, run bind "status". It verifies ownership and
|
|
|
3169
3103
|
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).",
|
|
3170
3104
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
3171
3105
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
3172
|
-
inputSchema: {
|
|
3106
|
+
inputSchema: {}
|
|
3173
3107
|
},
|
|
3174
|
-
async (
|
|
3108
|
+
async () => {
|
|
3175
3109
|
try {
|
|
3176
|
-
const ctx = withProjectDir(baseCtx
|
|
3110
|
+
const ctx = withProjectDir(baseCtx);
|
|
3177
3111
|
const site = requireSiteFile(ctx);
|
|
3178
3112
|
const res = await ctx.client.getBillingStatus(site.siteId, site.credential);
|
|
3179
3113
|
noteSiteMode(res.siteId, res.mode);
|
|
@@ -3211,14 +3145,13 @@ Full status:`, res);
|
|
|
3211
3145
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
3212
3146
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
3213
3147
|
inputSchema: {
|
|
3214
|
-
|
|
3215
|
-
scope: z3.enum(["site", "public_recovery"])
|
|
3148
|
+
scope: z2.enum(["site", "public_recovery"])
|
|
3216
3149
|
}
|
|
3217
3150
|
},
|
|
3218
3151
|
async (args) => {
|
|
3219
3152
|
try {
|
|
3220
3153
|
if (args.scope === "site") {
|
|
3221
|
-
const ctx = withProjectDir(baseCtx
|
|
3154
|
+
const ctx = withProjectDir(baseCtx);
|
|
3222
3155
|
const site = requireSiteFile(ctx);
|
|
3223
3156
|
const res2 = await ctx.client.createBillingPortal(site.siteId, site.credential);
|
|
3224
3157
|
return structuredToolResult({
|
|
@@ -3268,19 +3201,18 @@ Full status:`, res);
|
|
|
3268
3201
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
3269
3202
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
3270
3203
|
inputSchema: {
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
outputDir: z3.string().optional().describe(
|
|
3204
|
+
action: z2.enum(["start", "status", "complete", "download"]),
|
|
3205
|
+
hostname: z2.string().optional().describe("Required for start."),
|
|
3206
|
+
verificationId: z2.string().optional().describe("For status or complete; inferred from local recovery state when omitted."),
|
|
3207
|
+
outputDir: z2.string().optional().describe(
|
|
3276
3208
|
"REQUIRED for complete/download: exact extraction directory relative to the initialized project root. Inspect the current project; Sakupa never guesses a name."
|
|
3277
3209
|
),
|
|
3278
|
-
preserveExistingCredentials:
|
|
3210
|
+
preserveExistingCredentials: z2.boolean().optional().describe("Explicitly keep old local credentials working (default: revoke them all).")
|
|
3279
3211
|
}
|
|
3280
3212
|
},
|
|
3281
3213
|
async (args) => {
|
|
3282
3214
|
try {
|
|
3283
|
-
const ctx = withProjectDir(baseCtx
|
|
3215
|
+
const ctx = withProjectDir(baseCtx);
|
|
3284
3216
|
if ((args.action === "complete" || args.action === "download") && args.outputDir === void 0) {
|
|
3285
3217
|
throw new LocalGuidanceError(
|
|
3286
3218
|
"invalid_request",
|
|
@@ -3353,7 +3285,6 @@ No DNS verification was started or repeated.`,
|
|
|
3353
3285
|
{
|
|
3354
3286
|
tool: "recover",
|
|
3355
3287
|
arguments: {
|
|
3356
|
-
projectDir: ctx.projectDir,
|
|
3357
3288
|
action: "download",
|
|
3358
3289
|
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
|
|
3359
3290
|
},
|
|
@@ -3380,7 +3311,6 @@ No DNS verification was started or repeated.`,
|
|
|
3380
3311
|
{
|
|
3381
3312
|
tool: "recover",
|
|
3382
3313
|
arguments: {
|
|
3383
|
-
projectDir: ctx.projectDir,
|
|
3384
3314
|
action: "status",
|
|
3385
3315
|
verificationId: pending2.verificationId
|
|
3386
3316
|
},
|
|
@@ -3453,7 +3383,6 @@ After the DNS record resolves, re-run recover with verificationId: "${res2.verif
|
|
|
3453
3383
|
{
|
|
3454
3384
|
tool: "recover",
|
|
3455
3385
|
arguments: {
|
|
3456
|
-
projectDir: ctx.projectDir,
|
|
3457
3386
|
action: "download",
|
|
3458
3387
|
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
|
|
3459
3388
|
},
|
|
@@ -3488,7 +3417,6 @@ After the DNS record resolves, re-run recover with verificationId: "${res2.verif
|
|
|
3488
3417
|
{
|
|
3489
3418
|
tool: "recover",
|
|
3490
3419
|
arguments: {
|
|
3491
|
-
projectDir: ctx.projectDir,
|
|
3492
3420
|
action: "complete",
|
|
3493
3421
|
verificationId,
|
|
3494
3422
|
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
|
|
@@ -3565,7 +3493,6 @@ Files: ${extracted.fileCount}; bytes: ${extracted.totalBytes}
|
|
|
3565
3493
|
{
|
|
3566
3494
|
tool: "recover",
|
|
3567
3495
|
arguments: {
|
|
3568
|
-
projectDir: ctx.projectDir,
|
|
3569
3496
|
action: "download",
|
|
3570
3497
|
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
|
|
3571
3498
|
},
|
|
@@ -3586,16 +3513,15 @@ Files: ${extracted.fileCount}; bytes: ${extracted.totalBytes}
|
|
|
3586
3513
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
3587
3514
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
3588
3515
|
inputSchema: {
|
|
3589
|
-
projectDir: projectDirInput,
|
|
3590
3516
|
category: ticketCategoryEnum,
|
|
3591
|
-
subject:
|
|
3592
|
-
description:
|
|
3593
|
-
contactEmail:
|
|
3517
|
+
subject: z2.string().describe("Short subject line."),
|
|
3518
|
+
description: z2.string().describe("Problem description (no secrets, no card data)."),
|
|
3519
|
+
contactEmail: z2.string().optional().describe("Optional contact email for follow-up.")
|
|
3594
3520
|
}
|
|
3595
3521
|
},
|
|
3596
3522
|
async (args) => {
|
|
3597
3523
|
try {
|
|
3598
|
-
const ctx = withProjectDir(baseCtx
|
|
3524
|
+
const ctx = withProjectDir(baseCtx);
|
|
3599
3525
|
const site = requireSiteFile(ctx);
|
|
3600
3526
|
const res = await ctx.client.createTicket(site.credential, {
|
|
3601
3527
|
siteId: site.siteId,
|
|
@@ -3621,26 +3547,25 @@ Files: ${extracted.fileCount}; bytes: ${extracted.totalBytes}
|
|
|
3621
3547
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
3622
3548
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
3623
3549
|
inputSchema: {
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
deploymentId: z3.string().optional(),
|
|
3550
|
+
toolName: z2.string().describe('The Sakupa tool that failed, e.g. "deploy".'),
|
|
3551
|
+
errorCode: z2.string().optional(),
|
|
3552
|
+
errorMessage: z2.string().optional().describe("Sanitized error message (no secrets)."),
|
|
3553
|
+
requestId: z2.string().optional(),
|
|
3554
|
+
deploymentId: z2.string().optional(),
|
|
3630
3555
|
severity: severityEnum.optional(),
|
|
3631
|
-
description:
|
|
3632
|
-
agentContext:
|
|
3556
|
+
description: z2.string().optional().describe("What happened, in the user's words (no secrets)."),
|
|
3557
|
+
agentContext: z2.string().optional().describe(
|
|
3633
3558
|
"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."
|
|
3634
3559
|
),
|
|
3635
|
-
contactEmail:
|
|
3560
|
+
contactEmail: z2.string().optional().describe(
|
|
3636
3561
|
"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."
|
|
3637
3562
|
),
|
|
3638
|
-
confirmSubmit:
|
|
3563
|
+
confirmSubmit: z2.boolean().optional().describe("User reviewed the report payload and approved submission.")
|
|
3639
3564
|
}
|
|
3640
3565
|
},
|
|
3641
3566
|
async (args) => {
|
|
3642
3567
|
try {
|
|
3643
|
-
const ctx = withProjectDir(baseCtx
|
|
3568
|
+
const ctx = withProjectDir(baseCtx);
|
|
3644
3569
|
const siteState = loadSiteFile(ctx.projectDir);
|
|
3645
3570
|
const site = siteState.kind === "ok" ? siteState.file : null;
|
|
3646
3571
|
const diagnostics = {
|
|
@@ -3687,7 +3612,7 @@ Summary: ${res.sanitizedSummary}`,
|
|
|
3687
3612
|
}
|
|
3688
3613
|
|
|
3689
3614
|
// src/tools/billing.ts
|
|
3690
|
-
import { z as
|
|
3615
|
+
import { z as z3 } from "zod";
|
|
3691
3616
|
function registerBillingTools(server, baseCtx) {
|
|
3692
3617
|
server.registerTool(
|
|
3693
3618
|
"plans",
|
|
@@ -3718,15 +3643,14 @@ function registerBillingTools(server, baseCtx) {
|
|
|
3718
3643
|
{
|
|
3719
3644
|
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.",
|
|
3720
3645
|
inputSchema: {
|
|
3721
|
-
|
|
3722
|
-
operationId: z4.string().min(1)
|
|
3646
|
+
operationId: z3.string().min(1)
|
|
3723
3647
|
},
|
|
3724
3648
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
3725
3649
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
|
|
3726
3650
|
},
|
|
3727
3651
|
async (args) => {
|
|
3728
3652
|
try {
|
|
3729
|
-
const ctx = withProjectDir(baseCtx
|
|
3653
|
+
const ctx = withProjectDir(baseCtx);
|
|
3730
3654
|
const site = requireSiteFile(ctx);
|
|
3731
3655
|
const result = await ctx.client.changeSubscriptionPlan(site.credential, {
|
|
3732
3656
|
siteId: site.siteId,
|
|
@@ -3756,21 +3680,21 @@ function registerBillingTools(server, baseCtx) {
|
|
|
3756
3680
|
|
|
3757
3681
|
// src/tools/lifecycle.ts
|
|
3758
3682
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
3759
|
-
import { z as
|
|
3760
|
-
var deleteConfirmation =
|
|
3761
|
-
siteId:
|
|
3762
|
-
expectedSiteUpdatedAt:
|
|
3763
|
-
expectedStatus:
|
|
3764
|
-
expectedMode:
|
|
3765
|
-
expectedServingMode:
|
|
3766
|
-
expectedShortId:
|
|
3767
|
-
expectedSubscriptionStatus:
|
|
3768
|
-
expectedPlan:
|
|
3769
|
-
expectedCancelAtPeriodEnd:
|
|
3770
|
-
expectedCurrentPeriodEnd:
|
|
3771
|
-
expectedLastDeploymentId:
|
|
3772
|
-
expectedBoundHostnames:
|
|
3773
|
-
acknowledge:
|
|
3683
|
+
import { z as z4 } from "zod";
|
|
3684
|
+
var deleteConfirmation = z4.object({
|
|
3685
|
+
siteId: z4.string().min(1),
|
|
3686
|
+
expectedSiteUpdatedAt: z4.string().datetime(),
|
|
3687
|
+
expectedStatus: z4.enum(["active", "expired", "deleted"]),
|
|
3688
|
+
expectedMode: z4.enum(["free", "paid"]),
|
|
3689
|
+
expectedServingMode: z4.enum(["normal", "over_limit_notice", "risk_notice", "stopped"]),
|
|
3690
|
+
expectedShortId: z4.string().optional(),
|
|
3691
|
+
expectedSubscriptionStatus: z4.enum(["incomplete", "active", "past_due", "canceled"]).optional(),
|
|
3692
|
+
expectedPlan: z4.enum(["water", "personal", "share", "business"]).optional(),
|
|
3693
|
+
expectedCancelAtPeriodEnd: z4.boolean().optional(),
|
|
3694
|
+
expectedCurrentPeriodEnd: z4.string().datetime().optional(),
|
|
3695
|
+
expectedLastDeploymentId: z4.string().optional(),
|
|
3696
|
+
expectedBoundHostnames: z4.array(z4.string()),
|
|
3697
|
+
acknowledge: z4.literal("delete_and_cancel_renewal")
|
|
3774
3698
|
});
|
|
3775
3699
|
function registerLifecycleTools(server, baseCtx) {
|
|
3776
3700
|
server.registerTool(
|
|
@@ -3778,9 +3702,8 @@ function registerLifecycleTools(server, baseCtx) {
|
|
|
3778
3702
|
{
|
|
3779
3703
|
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.",
|
|
3780
3704
|
inputSchema: {
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
operationId: z5.string().min(1).optional(),
|
|
3705
|
+
action: z4.enum(["preview", "confirm"]),
|
|
3706
|
+
operationId: z4.string().min(1).optional(),
|
|
3784
3707
|
confirmation: deleteConfirmation.optional()
|
|
3785
3708
|
},
|
|
3786
3709
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
@@ -3788,7 +3711,7 @@ function registerLifecycleTools(server, baseCtx) {
|
|
|
3788
3711
|
},
|
|
3789
3712
|
async (args) => {
|
|
3790
3713
|
try {
|
|
3791
|
-
const ctx = withProjectDir(baseCtx
|
|
3714
|
+
const ctx = withProjectDir(baseCtx);
|
|
3792
3715
|
const site = requireSiteFile(ctx);
|
|
3793
3716
|
const operationId = args.operationId ?? randomUUID3();
|
|
3794
3717
|
if (args.action === "preview") {
|
|
@@ -3986,21 +3909,21 @@ Workflow:
|
|
|
3986
3909
|
sanitized diagnostic report after the user explicitly confirms it.
|
|
3987
3910
|
|
|
3988
3911
|
Project directory contract: before the first deploy or a new recovery, initialize the intended
|
|
3989
|
-
project
|
|
3990
|
-
|
|
3991
|
-
ONE
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
.
|
|
3995
|
-
|
|
3996
|
-
|
|
3912
|
+
project once by running "npx -y @sakupa/mcp@latest init" with NO path argument from the AI
|
|
3913
|
+
tool's current project directory. This immediately creates the non-secret .sakupa/project.json
|
|
3914
|
+
there. ONE MCP process = ONE cwd-locked project = ONE site. Site tools do not accept projectDir
|
|
3915
|
+
and cannot select another root; plans and public_recovery portal remain project-independent.
|
|
3916
|
+
Sakupa stores .sakupa/site.json and recovery state only in the locked directory; it never uses
|
|
3917
|
+
package.json, .git, framework names or output-directory names to guess. For deploy, ALWAYS pass
|
|
3918
|
+
outputDir separately as the exact path RELATIVE to the locked directory (use "." when publishing
|
|
3919
|
+
the root); outputDir is
|
|
3997
3920
|
required and may have ANY name, so inspect the current project. If it differs from the last
|
|
3998
3921
|
successful publish directory, show the old and new paths and obtain explicit confirmation
|
|
3999
3922
|
before retrying with outputDirChangeConfirmed: true.
|
|
4000
3923
|
After every deploy, TELL the user which environment it went to (deploy results carry an
|
|
4001
3924
|
Explicit Environment line: TEST vs PRODUCTION). analyze, deploy, status,
|
|
4002
3925
|
refresh and delete echo
|
|
4003
|
-
the directory they acted on
|
|
3926
|
+
the cwd-locked directory they acted on.
|
|
4004
3927
|
|
|
4005
3928
|
When the same operation fails twice in a row, or the user is clearly stuck or
|
|
4006
3929
|
frustrated, proactively offer report: it files the problem into Sakupa's ticket and
|
|
@@ -4038,7 +3961,11 @@ function createSakupaMcpServer(opts) {
|
|
|
4038
3961
|
{ name: "sakupa", version: MCP_VERSION },
|
|
4039
3962
|
{ instructions: instructionsFor(previewHostPatternFor(opts.apiBaseUrl)) }
|
|
4040
3963
|
);
|
|
4041
|
-
const ctx = {
|
|
3964
|
+
const ctx = {
|
|
3965
|
+
client,
|
|
3966
|
+
apiBaseUrl: opts.apiBaseUrl,
|
|
3967
|
+
projectDir: canonicalProjectDirectory(opts.projectDir ?? process.cwd())
|
|
3968
|
+
};
|
|
4042
3969
|
registerTools(server, ctx);
|
|
4043
3970
|
registerBillingTools(server, ctx);
|
|
4044
3971
|
registerLifecycleTools(server, ctx);
|
|
@@ -4049,28 +3976,22 @@ function createSakupaMcpServer(opts) {
|
|
|
4049
3976
|
async function main() {
|
|
4050
3977
|
const argv = process.argv.slice(2);
|
|
4051
3978
|
if (argv[0] === "init") {
|
|
4052
|
-
const
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
});
|
|
4059
|
-
process.exitCode = result.exitCode;
|
|
4060
|
-
return;
|
|
4061
|
-
} finally {
|
|
4062
|
-
readline.close();
|
|
4063
|
-
}
|
|
3979
|
+
const result = await runInitCommand(argv.slice(1), {
|
|
3980
|
+
write: (message) => stdout.write(`${message}
|
|
3981
|
+
`)
|
|
3982
|
+
});
|
|
3983
|
+
process.exitCode = result.exitCode;
|
|
3984
|
+
return;
|
|
4064
3985
|
}
|
|
4065
3986
|
if (argv.length > 0) {
|
|
4066
|
-
throw new Error("Usage: sakupa-mcp [init
|
|
3987
|
+
throw new Error("Usage: sakupa-mcp [init]");
|
|
4067
3988
|
}
|
|
4068
3989
|
const config = loadMcpRuntimeConfig();
|
|
4069
3990
|
const server = createSakupaMcpServer(config);
|
|
4070
3991
|
const transport = new StdioServerTransport();
|
|
4071
3992
|
await server.connect(transport);
|
|
4072
3993
|
console.error(
|
|
4073
|
-
`[sakupa-mcp] v${MCP_VERSION} connected (api: ${config.apiBaseUrl};
|
|
3994
|
+
`[sakupa-mcp] v${MCP_VERSION} connected (api: ${config.apiBaseUrl}; project: ${process.cwd()})`
|
|
4074
3995
|
);
|
|
4075
3996
|
}
|
|
4076
3997
|
main().catch((err2) => {
|