@sakupa/mcp 0.7.33 → 0.7.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/bin.js +164 -227
  2. package/dist/index.js +139 -218
  3. 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 { createInterface } from "node:readline/promises";
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 { dirname, isAbsolute, join, parse, relative, resolve, sep } from "node:path";
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 resolveProjectRoot(requestedPath) {
113
- if (!isAbsolute(requestedPath)) {
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
- "invalid_path",
116
- `Project path must be absolute (got "${requestedPath}").`
115
+ "corrupted_marker",
116
+ `Sakupa project marker ${projectMarkerPath(canonical)} is damaged: ${markerState.problem}.`
117
117
  );
118
118
  }
119
- const canonicalRequested = canonicalExistingPath(requestedPath);
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
- `No Sakupa project marker was found at or above ${canonicalRequested}. Run \`npx -y @sakupa/mcp@latest init\` once from the intended project root; no site was changed.`
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: nearest.dir,
169
- requestedPath: canonicalRequested,
170
- markerKind: nearest.hasSite ? "legacy_site" : "legacy_recovery"
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 > 1) {
263
- io.write("Usage: sakupa-mcp init [project-directory]");
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(args[0] ?? process.cwd());
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}. The .sakupa directory stays here; publish output may be any child directory.`
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.33";
373
+ var SAKUPA_MCP_VERSION = "0.7.35";
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 z3 } from "zod";
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 as dirname2, join as join3 } from "node:path";
1376
+ import { dirname, join as join3 } from "node:path";
1426
1377
  var SITE_DIR = ".sakupa";
1427
- var SITE_FILE2 = "site.json";
1428
- var RECOVERY_FILE2 = "recovery.json";
1378
+ var SITE_FILE = "site.json";
1379
+ var RECOVERY_FILE = "recovery.json";
1429
1380
  function siteFilePath(projectDir) {
1430
- return join3(projectDir, SITE_DIR, SITE_FILE2);
1381
+ return join3(projectDir, SITE_DIR, SITE_FILE);
1431
1382
  }
1432
1383
  function recoveryFilePath(projectDir) {
1433
- return join3(projectDir, SITE_DIR, RECOVERY_FILE2);
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, RECOVERY_FILE2);
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, SITE_FILE2);
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 = dirname2(cursor);
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 dirname3, isAbsolute as isAbsolute2, join as join4, relative as relative2, resolve as resolve3, sep as sep3 } from "node:path";
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, z6) {
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, z6, b4(d, b + 20), b4(d, b + 24), b4(d, b + 42)), sc = _a2[0], su = _a2[1], off = _a2[2];
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, z6, sc, su, off) {
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 (z6 && nf) {
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 (z6 < 2)
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 z6 = b4(data, e - 20) == 117853008;
2029
- if (z6) {
1979
+ var z5 = b4(data, e - 20) == 117853008;
1980
+ if (z5) {
2030
1981
  var ze = b4(data, e - 12);
2031
- z6 = b4(data, ze) == 101075792;
2032
- if (z6) {
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, z6), c_2 = _a2[0], sc = _a2[1], su = _a2[2], fn = _a2[3], no = _a2[4], off = _a2[5], b = slzh(data, off);
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 = dirname3(existingAncestor);
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(dirname3(destination), { recursive: true });
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(dirname3(outputDir), { recursive: true });
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 dirname4, join as join5 } from "node:path";
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(dirname4(path), { recursive: true });
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
- var projectDirInput = z2.string().describe(
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 = resolveProjectRoot(projectDirArg);
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
- ...resolved.marker !== void 0 ? { projectMarker: resolved.marker } : {}
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 (delete/status/bind) an EXISTING site, re-run this tool with projectDir set to THAT site's own directory (each site's binding lives in its own folder). 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.`
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 = z3.enum(["water", "personal", "share", "business"]);
2553
- var severityEnum = z3.enum(["low", "medium", "high", "critical"]);
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 = z3.enum([
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 projectDir; 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.
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
- projectDir: projectDirInput,
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, args.projectDir);
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. projectDir may point anywhere inside a project already initialized with \`npx -y @sakupa/mcp@latest init\`; Sakupa resolves its own marker upward and stores credentials only at 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.`,
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
- projectDir: projectDirInput,
2722
- outputDir: z3.string().min(1).describe(
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: z3.boolean().optional().describe(
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: z3.boolean().optional().describe(
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: z3.boolean().optional().describe(
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: z3.boolean().optional().describe(
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: z3.string().optional().describe("Site language override (en | ja | zh-CN); defaults to the html 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, args.projectDir);
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 projectDir: ${ctx.projectDir}. Nothing was deployed and no site was created.`,
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: { projectDir: projectDirInput }
2890
+ inputSchema: {}
2955
2891
  },
2956
- async (args) => {
2892
+ async () => {
2957
2893
  try {
2958
- const ctx = withProjectDir(baseCtx, args.projectDir);
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: { projectDir: projectDirInput }
2914
+ inputSchema: {}
2979
2915
  },
2980
- async (args) => {
2916
+ async () => {
2981
2917
  try {
2982
- const ctx = withProjectDir(baseCtx, args.projectDir);
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, args.projectDir);
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
- projectDir: projectDirInput,
3058
- action: z3.enum(["start", "status"]),
3059
- hostname: z3.string().optional().describe("Required for start."),
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, args.projectDir);
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: { projectDir: projectDirInput }
3106
+ inputSchema: {}
3173
3107
  },
3174
- async (args) => {
3108
+ async () => {
3175
3109
  try {
3176
- const ctx = withProjectDir(baseCtx, args.projectDir);
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
- projectDir: projectDirInput.optional(),
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, args.projectDir);
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
- projectDir: projectDirInput,
3272
- action: z3.enum(["start", "status", "complete", "download"]),
3273
- hostname: z3.string().optional().describe("Required for start."),
3274
- verificationId: z3.string().optional().describe("For status or complete; inferred from local recovery state when omitted."),
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: z3.boolean().optional().describe("Explicitly keep old local credentials working (default: revoke them all).")
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, args.projectDir);
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: z3.string().describe("Short subject line."),
3592
- description: z3.string().describe("Problem description (no secrets, no card data)."),
3593
- contactEmail: z3.string().optional().describe("Optional contact email for follow-up.")
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, args.projectDir);
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
- projectDir: projectDirInput,
3625
- toolName: z3.string().describe('The Sakupa tool that failed, e.g. "deploy".'),
3626
- errorCode: z3.string().optional(),
3627
- errorMessage: z3.string().optional().describe("Sanitized error message (no secrets)."),
3628
- requestId: z3.string().optional(),
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: z3.string().optional().describe("What happened, in the user's words (no secrets)."),
3632
- agentContext: z3.string().optional().describe(
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: z3.string().optional().describe(
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: z3.boolean().optional().describe("User reviewed the report payload and approved submission.")
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, args.projectDir);
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 z4 } from "zod";
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
- projectDir: projectDirInput,
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, args.projectDir);
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 z5 } from "zod";
3760
- var deleteConfirmation = z5.object({
3761
- siteId: z5.string().min(1),
3762
- expectedSiteUpdatedAt: z5.string().datetime(),
3763
- expectedStatus: z5.enum(["active", "expired", "deleted"]),
3764
- expectedMode: z5.enum(["free", "paid"]),
3765
- expectedServingMode: z5.enum(["normal", "over_limit_notice", "risk_notice", "stopped"]),
3766
- expectedShortId: z5.string().optional(),
3767
- expectedSubscriptionStatus: z5.enum(["incomplete", "active", "past_due", "canceled"]).optional(),
3768
- expectedPlan: z5.enum(["water", "personal", "share", "business"]).optional(),
3769
- expectedCancelAtPeriodEnd: z5.boolean().optional(),
3770
- expectedCurrentPeriodEnd: z5.string().datetime().optional(),
3771
- expectedLastDeploymentId: z5.string().optional(),
3772
- expectedBoundHostnames: z5.array(z5.string()),
3773
- acknowledge: z5.literal("delete_and_cancel_renewal")
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
- projectDir: projectDirInput,
3782
- action: z5.enum(["preview", "confirm"]),
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, args.projectDir);
3714
+ const ctx = withProjectDir(baseCtx);
3792
3715
  const site = requireSiteFile(ctx);
3793
3716
  const operationId = args.operationId ?? randomUUID3();
3794
3717
  if (args.action === "preview") {
@@ -3809,15 +3732,31 @@ function registerLifecycleTools(server, baseCtx) {
3809
3732
  ]
3810
3733
  });
3811
3734
  }
3735
+ const confirmArguments = {
3736
+ action: "confirm",
3737
+ operationId,
3738
+ confirmation: preview.confirmation
3739
+ };
3740
+ const confirmationJson = JSON.stringify(preview.confirmation);
3812
3741
  return structuredToolResult({
3813
3742
  schemaVersion: 1,
3814
3743
  outcome: "waiting_user",
3815
3744
  resultCode: "delete_confirmation_required",
3816
3745
  operationId,
3817
- summary: `Deletion consequences returned, bound to the current site and billing state; after confirmation the content and the permanent URL are unrecoverable. To proceed, call delete again with action:"confirm", operationId:"${operationId}", and the confirmation object from data.preview.confirmation.`,
3818
- data: { preview },
3746
+ summary: `Deletion consequences returned, bound to the current site and billing state; after confirmation the content and the permanent URL are unrecoverable. To proceed, call delete exactly once with these arguments (copy them verbatim; do not infer values): ${JSON.stringify(confirmArguments)}. Exact confirmation object: ${confirmationJson}`,
3747
+ data: { preview, confirmation: preview.confirmation, confirmArguments },
3748
+ userAction: {
3749
+ type: "confirm_in_mcp",
3750
+ expectedOutcome: "Permanently delete this site if its cloud state is unchanged.",
3751
+ resumeWith: { tool: "delete", arguments: confirmArguments }
3752
+ },
3819
3753
  nextActions: [
3820
- { tool: "delete", allowed: true, reasonCode: "exact_confirmation_required" }
3754
+ {
3755
+ tool: "delete",
3756
+ arguments: confirmArguments,
3757
+ allowed: true,
3758
+ reasonCode: "exact_confirmation_required"
3759
+ }
3821
3760
  ]
3822
3761
  });
3823
3762
  }
@@ -3986,21 +3925,21 @@ Workflow:
3986
3925
  sanitized diagnostic report after the user explicitly confirms it.
3987
3926
 
3988
3927
  Project directory contract: before the first deploy or a new recovery, initialize the intended
3989
- project root once with "npx -y @sakupa/mcp@latest init"; this creates a non-secret
3990
- .sakupa/project.json marker after the user confirms the canonical path. ONE initialized root =
3991
- ONE site. Project-bound tools require projectDir, but it is only a path locator: it may point to
3992
- the root or any existing child directory. The site-independent plans tool and public_recovery
3993
- portal do not require a local project. Sakupa resolves upward to its own marker and stores
3994
- .sakupa/site.json only at that authoritative root; it never uses package.json, .git, framework
3995
- names or output-directory names to guess. For deploy, ALWAYS pass outputDir separately as the
3996
- exact path RELATIVE to the resolved root (use "." when publishing the root); outputDir is
3928
+ project once by running "npx -y @sakupa/mcp@latest init" with NO path argument from the AI
3929
+ tool's current project directory. This immediately creates the non-secret .sakupa/project.json
3930
+ there. ONE MCP process = ONE cwd-locked project = ONE site. Site tools do not accept projectDir
3931
+ and cannot select another root; plans and public_recovery portal remain project-independent.
3932
+ Sakupa stores .sakupa/site.json and recovery state only in the locked directory; it never uses
3933
+ package.json, .git, framework names or output-directory names to guess. For deploy, ALWAYS pass
3934
+ outputDir separately as the exact path RELATIVE to the locked directory (use "." when publishing
3935
+ the root); outputDir is
3997
3936
  required and may have ANY name, so inspect the current project. If it differs from the last
3998
3937
  successful publish directory, show the old and new paths and obtain explicit confirmation
3999
3938
  before retrying with outputDirChangeConfirmed: true.
4000
3939
  After every deploy, TELL the user which environment it went to (deploy results carry an
4001
3940
  Explicit Environment line: TEST vs PRODUCTION). analyze, deploy, status,
4002
3941
  refresh and delete echo
4003
- the directory they acted on \u2014 verify it matches the user's active project.
3942
+ the cwd-locked directory they acted on.
4004
3943
 
4005
3944
  When the same operation fails twice in a row, or the user is clearly stuck or
4006
3945
  frustrated, proactively offer report: it files the problem into Sakupa's ticket and
@@ -4038,7 +3977,11 @@ function createSakupaMcpServer(opts) {
4038
3977
  { name: "sakupa", version: MCP_VERSION },
4039
3978
  { instructions: instructionsFor(previewHostPatternFor(opts.apiBaseUrl)) }
4040
3979
  );
4041
- const ctx = { client, apiBaseUrl: opts.apiBaseUrl };
3980
+ const ctx = {
3981
+ client,
3982
+ apiBaseUrl: opts.apiBaseUrl,
3983
+ projectDir: canonicalProjectDirectory(opts.projectDir ?? process.cwd())
3984
+ };
4042
3985
  registerTools(server, ctx);
4043
3986
  registerBillingTools(server, ctx);
4044
3987
  registerLifecycleTools(server, ctx);
@@ -4049,28 +3992,22 @@ function createSakupaMcpServer(opts) {
4049
3992
  async function main() {
4050
3993
  const argv = process.argv.slice(2);
4051
3994
  if (argv[0] === "init") {
4052
- const readline = createInterface({ input: stdin, output: stdout });
4053
- try {
4054
- const result = await runInitCommand(argv.slice(1), {
4055
- write: (message) => stdout.write(`${message}
4056
- `),
4057
- confirm: async (question) => /^(?:y|yes)$/i.test((await readline.question(question)).trim())
4058
- });
4059
- process.exitCode = result.exitCode;
4060
- return;
4061
- } finally {
4062
- readline.close();
4063
- }
3995
+ const result = await runInitCommand(argv.slice(1), {
3996
+ write: (message) => stdout.write(`${message}
3997
+ `)
3998
+ });
3999
+ process.exitCode = result.exitCode;
4000
+ return;
4064
4001
  }
4065
4002
  if (argv.length > 0) {
4066
- throw new Error("Usage: sakupa-mcp [init [project-directory]]");
4003
+ throw new Error("Usage: sakupa-mcp [init]");
4067
4004
  }
4068
4005
  const config = loadMcpRuntimeConfig();
4069
4006
  const server = createSakupaMcpServer(config);
4070
4007
  const transport = new StdioServerTransport();
4071
4008
  await server.connect(transport);
4072
4009
  console.error(
4073
- `[sakupa-mcp] v${MCP_VERSION} connected (api: ${config.apiBaseUrl}; projectDir resolves through Sakupa markers)`
4010
+ `[sakupa-mcp] v${MCP_VERSION} connected (api: ${config.apiBaseUrl}; project: ${process.cwd()})`
4074
4011
  );
4075
4012
  }
4076
4013
  main().catch((err2) => {