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