@sakupa/mcp 0.7.31 → 0.7.32

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 +66 -26
  2. package/dist/index.js +66 -26
  3. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -129,7 +129,7 @@ var FORBIDDEN_PATH_SEGMENTS = [
129
129
  var ALLOWED_HIDDEN_PATHS = [".well-known/"];
130
130
 
131
131
  // ../core/dist/domain/version.js
132
- var SAKUPA_MCP_VERSION = "0.7.31";
132
+ var SAKUPA_MCP_VERSION = "0.7.32";
133
133
 
134
134
  // ../core/dist/domain/errors.js
135
135
  var HTTP_STATUS = {
@@ -690,7 +690,7 @@ var HttpApiClient = class {
690
690
  // src/tools/definitions.ts
691
691
  import { randomUUID } from "node:crypto";
692
692
  import { existsSync as existsSync3, promises as fs2 } from "node:fs";
693
- import { join as join5, resolve as resolve4 } from "node:path";
693
+ import { join as join5, relative as relative2, resolve as resolve4, sep as sep3 } from "node:path";
694
694
  import { z as z3 } from "zod";
695
695
 
696
696
  // src/analyze/analyzer.ts
@@ -1113,7 +1113,15 @@ async function analyzeProject(projectDir, opts = {}) {
1113
1113
  }
1114
1114
 
1115
1115
  // src/project-file.ts
1116
- import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
1116
+ import {
1117
+ chmodSync,
1118
+ existsSync,
1119
+ mkdirSync,
1120
+ readFileSync,
1121
+ rmdirSync,
1122
+ rmSync,
1123
+ writeFileSync
1124
+ } from "node:fs";
1117
1125
  import { dirname, join as join2 } from "node:path";
1118
1126
  var SITE_DIR = ".sakupa";
1119
1127
  var SITE_FILE = "site.json";
@@ -1236,6 +1244,10 @@ function deleteSiteFile(projectDir) {
1236
1244
  if (existsSync(path)) {
1237
1245
  rmSync(path, { force: true });
1238
1246
  }
1247
+ try {
1248
+ rmdirSync(join2(projectDir, SITE_DIR));
1249
+ } catch {
1250
+ }
1239
1251
  }
1240
1252
  function findAncestor(startDir, predicate, maxLevels = Number.POSITIVE_INFINITY) {
1241
1253
  let cursor = startDir;
@@ -2344,18 +2356,19 @@ ${block}`, checklist: toDnsChecklist(diag.checks) };
2344
2356
  }
2345
2357
  function projectRootAbove(projectDir) {
2346
2358
  if (existsSync3(join5(projectDir, "package.json"))) return null;
2347
- return findAncestor(projectDir, (dir) => existsSync3(join5(dir, "package.json")), 4);
2348
- }
2349
- function findNeighborBinding(projectDir, outputRel) {
2350
- const bound = (dir) => loadSiteFile(dir).kind !== "absent";
2351
- const above = findAncestor(projectDir, bound, 3);
2352
- if (above) return above;
2353
- if (outputRel && outputRel !== ".") {
2354
- const outputAbs = resolve4(projectDir, outputRel);
2355
- if (bound(outputAbs)) return outputAbs;
2359
+ const packageRoot = findAncestor(projectDir, (dir) => existsSync3(join5(dir, "package.json")), 4);
2360
+ if (packageRoot) {
2361
+ return {
2362
+ projectRoot: packageRoot,
2363
+ outputDir: relative2(packageRoot, projectDir).split(sep3).join("/")
2364
+ };
2356
2365
  }
2357
2366
  return null;
2358
2367
  }
2368
+ function findNeighborBinding(projectDir) {
2369
+ const bound = (dir) => loadSiteFile(dir).kind !== "absent";
2370
+ return findAncestor(projectDir, bound, 3);
2371
+ }
2359
2372
  function freeSiteCreationBarrier(apiBaseUrl) {
2360
2373
  const recent = listRecentCreations(Date.now(), apiBaseUrl);
2361
2374
  if (recent.length < FREE_ACTIVE_SITES_PER_IP) return null;
@@ -2406,12 +2419,14 @@ Next action: ${analysis.suggestedNextAction}`,
2406
2419
  server.registerTool(
2407
2420
  "deploy",
2408
2421
  {
2409
- 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. Never uploads anything when the analysis says the project is not deployable.`,
2422
+ 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 is ALWAYS the project root where .sakupa belongs; outputDir is a separate REQUIRED relative path supplied from the current project inspection ("." when the root itself is published). NEVER pass the build/output folder as projectDir. Never uploads anything when analysis says the project is not deployable.`,
2410
2423
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
2411
2424
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
2412
2425
  inputSchema: {
2413
2426
  projectDir: projectDirInput,
2414
- outputDir: z3.string().optional().describe("Output directory relative to the project root (overrides detection)."),
2427
+ outputDir: z3.string().min(1).describe(
2428
+ 'REQUIRED: exact publish directory relative to projectDir, supplied by the AI after inspecting this project (for example ".", "dist", "html", or any custom build directory). NEVER put this path in projectDir; .sakupa belongs at projectDir.'
2429
+ ),
2415
2430
  spaFallback: z3.boolean().optional().describe(
2416
2431
  "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."
2417
2432
  ),
@@ -2427,9 +2442,7 @@ Next action: ${analysis.suggestedNextAction}`,
2427
2442
  async (args) => {
2428
2443
  try {
2429
2444
  const ctx = withProjectDir(baseCtx, args.projectDir);
2430
- const analysis = await analyzeProject(ctx.projectDir, {
2431
- ...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
2432
- });
2445
+ const analysis = await analyzeProject(ctx.projectDir, { outputDir: args.outputDir });
2433
2446
  if (!analysis.deployable || !analysis.files) {
2434
2447
  return notDeployableResult(analysis);
2435
2448
  }
@@ -2447,18 +2460,40 @@ Next action: ${analysis.suggestedNextAction}`,
2447
2460
  "blocked"
2448
2461
  );
2449
2462
  }
2450
- const existing = siteFileState.kind === "ok" ? siteFileState.file : null;
2463
+ let existing = siteFileState.kind === "ok" ? siteFileState.file : null;
2464
+ let credentialRelocatedFrom = null;
2465
+ if (!existing && analysis.recommendedOutputDir !== ".") {
2466
+ const outputSiteState = loadSiteFile(outputAbs);
2467
+ if (outputSiteState.kind === "corrupted") {
2468
+ return text(
2469
+ "output_site_file_corrupted",
2470
+ `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.`,
2471
+ { projectRoot: ctx.projectDir, outputDir: analysis.recommendedOutputDir },
2472
+ "blocked"
2473
+ );
2474
+ }
2475
+ if (outputSiteState.kind === "ok") {
2476
+ writeSiteFile(ctx.projectDir, outputSiteState.file);
2477
+ deleteSiteFile(outputAbs);
2478
+ existing = outputSiteState.file;
2479
+ credentialRelocatedFrom = outputAbs;
2480
+ }
2481
+ }
2451
2482
  if (!existing) {
2452
- const rootAbove = args.subprojectConfirmed === true ? null : projectRootAbove(ctx.projectDir);
2453
- if (rootAbove) {
2483
+ const rootHint = args.subprojectConfirmed === true ? null : projectRootAbove(ctx.projectDir);
2484
+ if (rootHint) {
2454
2485
  return text(
2455
2486
  "not_project_root",
2456
- `${ctx.projectDir} has no package.json but ${rootAbove} does \u2014 this directory is a SUBFOLDER of that project (typically its build output), and .sakupa must live at the project ROOT. Re-run deploy with projectDir: ${rootAbove}. Only if the user explicitly says this subfolder is an INDEPENDENT site (e.g. a docs/ site inside a repo), re-run with subprojectConfirmed: true. Nothing was deployed and no site was created.`,
2457
- { projectRoot: rootAbove, confirmationField: "subprojectConfirmed" },
2487
+ `${ctx.projectDir} is a SUBFOLDER of a package.json project, and .sakupa must live at the project ROOT. Re-run deploy with projectDir: ${rootHint.projectRoot} and outputDir: ${rootHint.outputDir}. Only if the user explicitly says this subfolder is an INDEPENDENT site (e.g. a docs/ site inside a repo), re-run with subprojectConfirmed: true. Nothing was deployed and no site was created.`,
2488
+ {
2489
+ projectRoot: rootHint.projectRoot,
2490
+ outputDir: rootHint.outputDir,
2491
+ confirmationField: "subprojectConfirmed"
2492
+ },
2458
2493
  "blocked"
2459
2494
  );
2460
2495
  }
2461
- const neighbor = findNeighborBinding(ctx.projectDir, analysis.recommendedOutputDir);
2496
+ const neighbor = findNeighborBinding(ctx.projectDir);
2462
2497
  if (neighbor) {
2463
2498
  return text(
2464
2499
  "neighbor_binding_found",
@@ -2582,6 +2617,7 @@ Environment: ${environmentFor(ctx.apiBaseUrl).toUpperCase()} (${ctx.apiBaseUrl})
2582
2617
  Project directory: ${ctx.projectDir}
2583
2618
  Files uploaded: ${uploaded} (${finalized.totalBytes} bytes)
2584
2619
  ` + (finalized.expiresAt ? `Validity refreshed \u2014 expires at: ${finalized.expiresAt}
2620
+ ` : "") + (credentialRelocatedFrom ? `Credential binding relocated from ${credentialRelocatedFrom}/.sakupa to ${ctx.projectDir}/.sakupa; the existing site was preserved.
2585
2621
  ` : "") + (finalized.mode === "free" ? `
2586
2622
  Reminder: free sites stay live for ${FREE_SITE_TTL_HOURS} hours after the last deploy or refresh call. Subscribing (subscribe) makes the site permanent.
2587
2623
  ` : "\nThis site is subscribed and permanent \u2014 no expiry.\n") + (finalized.warnings.length > 0 ? `
@@ -2596,7 +2632,8 @@ ${JSON.stringify(finalized.warnings, null, 2)}` : ""),
2596
2632
  expiresAt: finalized.expiresAt,
2597
2633
  filesUploaded: uploaded,
2598
2634
  totalBytes: finalized.totalBytes,
2599
- warnings: finalized.warnings
2635
+ warnings: finalized.warnings,
2636
+ ...credentialRelocatedFrom ? { credentialRelocatedFrom } : {}
2600
2637
  }
2601
2638
  );
2602
2639
  } catch (e) {
@@ -3635,8 +3672,11 @@ Project directory contract: ONE directory = ONE site (its .sakupa/site.json hold
3635
3672
  binding). projectDir is REQUIRED on EVERY tool call \u2014 always pass the absolute path of
3636
3673
  the user's PROJECT ROOT, the SAME directory every time for the same project: the folder
3637
3674
  the user opened (for framework projects, where package.json lives \u2014 never the dist/out
3638
- build folder; output is auto-detected). The server NEVER guesses a directory and refuses
3639
- calls without one: only you can see which directory the user is actually in. After every deploy, TELL the user which environment it went to (deploy results carry an
3675
+ build folder). For deploy, ALWAYS pass outputDir separately as the exact path RELATIVE to
3676
+ projectDir (use "." when publishing the root); outputDir is required and may have ANY name,
3677
+ so inspect the current project and never infer it from a conventional folder name. The server
3678
+ NEVER guesses either directory: only you can see the user's actual workspace and build output.
3679
+ After every deploy, TELL the user which environment it went to (deploy results carry an
3640
3680
  Explicit Environment line: TEST vs PRODUCTION). analyze, deploy, status,
3641
3681
  refresh and delete echo
3642
3682
  the directory they acted on \u2014 verify it matches the user's active project.
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.31";
127
+ var SAKUPA_MCP_VERSION = "0.7.32";
128
128
 
129
129
  // ../core/dist/domain/errors.js
130
130
  var HTTP_STATUS = {
@@ -781,7 +781,15 @@ var HttpApiClient = class {
781
781
  };
782
782
 
783
783
  // src/project-file.ts
784
- import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
784
+ import {
785
+ chmodSync,
786
+ existsSync,
787
+ mkdirSync,
788
+ readFileSync,
789
+ rmdirSync,
790
+ rmSync,
791
+ writeFileSync
792
+ } from "node:fs";
785
793
  import { dirname, join } from "node:path";
786
794
  var SITE_DIR = ".sakupa";
787
795
  var SITE_FILE = "site.json";
@@ -904,6 +912,10 @@ function deleteSiteFile(projectDir) {
904
912
  if (existsSync(path)) {
905
913
  rmSync(path, { force: true });
906
914
  }
915
+ try {
916
+ rmdirSync(join(projectDir, SITE_DIR));
917
+ } catch {
918
+ }
907
919
  }
908
920
  function findAncestor(startDir, predicate, maxLevels = Number.POSITIVE_INFINITY) {
909
921
  let cursor = startDir;
@@ -1484,7 +1496,7 @@ function toolError(e) {
1484
1496
  // src/tools/definitions.ts
1485
1497
  import { randomUUID } from "node:crypto";
1486
1498
  import { existsSync as existsSync3, promises as fs2 } from "node:fs";
1487
- import { join as join5, resolve as resolve4 } from "node:path";
1499
+ import { join as join5, relative as relative2, resolve as resolve4, sep as sep3 } from "node:path";
1488
1500
  import { z as z3 } from "zod";
1489
1501
 
1490
1502
  // src/recovery-archive.ts
@@ -2433,18 +2445,19 @@ ${block}`, checklist: toDnsChecklist(diag.checks) };
2433
2445
  }
2434
2446
  function projectRootAbove(projectDir) {
2435
2447
  if (existsSync3(join5(projectDir, "package.json"))) return null;
2436
- return findAncestor(projectDir, (dir) => existsSync3(join5(dir, "package.json")), 4);
2437
- }
2438
- function findNeighborBinding(projectDir, outputRel) {
2439
- const bound = (dir) => loadSiteFile(dir).kind !== "absent";
2440
- const above = findAncestor(projectDir, bound, 3);
2441
- if (above) return above;
2442
- if (outputRel && outputRel !== ".") {
2443
- const outputAbs = resolve4(projectDir, outputRel);
2444
- if (bound(outputAbs)) return outputAbs;
2448
+ const packageRoot = findAncestor(projectDir, (dir) => existsSync3(join5(dir, "package.json")), 4);
2449
+ if (packageRoot) {
2450
+ return {
2451
+ projectRoot: packageRoot,
2452
+ outputDir: relative2(packageRoot, projectDir).split(sep3).join("/")
2453
+ };
2445
2454
  }
2446
2455
  return null;
2447
2456
  }
2457
+ function findNeighborBinding(projectDir) {
2458
+ const bound = (dir) => loadSiteFile(dir).kind !== "absent";
2459
+ return findAncestor(projectDir, bound, 3);
2460
+ }
2448
2461
  function freeSiteCreationBarrier(apiBaseUrl) {
2449
2462
  const recent = listRecentCreations(Date.now(), apiBaseUrl);
2450
2463
  if (recent.length < FREE_ACTIVE_SITES_PER_IP) return null;
@@ -2495,12 +2508,14 @@ Next action: ${analysis.suggestedNextAction}`,
2495
2508
  server.registerTool(
2496
2509
  "deploy",
2497
2510
  {
2498
- 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. Never uploads anything when the analysis says the project is not deployable.`,
2511
+ 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 is ALWAYS the project root where .sakupa belongs; outputDir is a separate REQUIRED relative path supplied from the current project inspection ("." when the root itself is published). NEVER pass the build/output folder as projectDir. Never uploads anything when analysis says the project is not deployable.`,
2499
2512
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
2500
2513
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
2501
2514
  inputSchema: {
2502
2515
  projectDir: projectDirInput,
2503
- outputDir: z3.string().optional().describe("Output directory relative to the project root (overrides detection)."),
2516
+ outputDir: z3.string().min(1).describe(
2517
+ 'REQUIRED: exact publish directory relative to projectDir, supplied by the AI after inspecting this project (for example ".", "dist", "html", or any custom build directory). NEVER put this path in projectDir; .sakupa belongs at projectDir.'
2518
+ ),
2504
2519
  spaFallback: z3.boolean().optional().describe(
2505
2520
  "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."
2506
2521
  ),
@@ -2516,9 +2531,7 @@ Next action: ${analysis.suggestedNextAction}`,
2516
2531
  async (args) => {
2517
2532
  try {
2518
2533
  const ctx = withProjectDir(baseCtx, args.projectDir);
2519
- const analysis = await analyzeProject(ctx.projectDir, {
2520
- ...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
2521
- });
2534
+ const analysis = await analyzeProject(ctx.projectDir, { outputDir: args.outputDir });
2522
2535
  if (!analysis.deployable || !analysis.files) {
2523
2536
  return notDeployableResult(analysis);
2524
2537
  }
@@ -2536,18 +2549,40 @@ Next action: ${analysis.suggestedNextAction}`,
2536
2549
  "blocked"
2537
2550
  );
2538
2551
  }
2539
- const existing = siteFileState.kind === "ok" ? siteFileState.file : null;
2552
+ let existing = siteFileState.kind === "ok" ? siteFileState.file : null;
2553
+ let credentialRelocatedFrom = null;
2554
+ if (!existing && analysis.recommendedOutputDir !== ".") {
2555
+ const outputSiteState = loadSiteFile(outputAbs);
2556
+ if (outputSiteState.kind === "corrupted") {
2557
+ return text(
2558
+ "output_site_file_corrupted",
2559
+ `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.`,
2560
+ { projectRoot: ctx.projectDir, outputDir: analysis.recommendedOutputDir },
2561
+ "blocked"
2562
+ );
2563
+ }
2564
+ if (outputSiteState.kind === "ok") {
2565
+ writeSiteFile(ctx.projectDir, outputSiteState.file);
2566
+ deleteSiteFile(outputAbs);
2567
+ existing = outputSiteState.file;
2568
+ credentialRelocatedFrom = outputAbs;
2569
+ }
2570
+ }
2540
2571
  if (!existing) {
2541
- const rootAbove = args.subprojectConfirmed === true ? null : projectRootAbove(ctx.projectDir);
2542
- if (rootAbove) {
2572
+ const rootHint = args.subprojectConfirmed === true ? null : projectRootAbove(ctx.projectDir);
2573
+ if (rootHint) {
2543
2574
  return text(
2544
2575
  "not_project_root",
2545
- `${ctx.projectDir} has no package.json but ${rootAbove} does \u2014 this directory is a SUBFOLDER of that project (typically its build output), and .sakupa must live at the project ROOT. Re-run deploy with projectDir: ${rootAbove}. Only if the user explicitly says this subfolder is an INDEPENDENT site (e.g. a docs/ site inside a repo), re-run with subprojectConfirmed: true. Nothing was deployed and no site was created.`,
2546
- { projectRoot: rootAbove, confirmationField: "subprojectConfirmed" },
2576
+ `${ctx.projectDir} is a SUBFOLDER of a package.json project, and .sakupa must live at the project ROOT. Re-run deploy with projectDir: ${rootHint.projectRoot} and outputDir: ${rootHint.outputDir}. Only if the user explicitly says this subfolder is an INDEPENDENT site (e.g. a docs/ site inside a repo), re-run with subprojectConfirmed: true. Nothing was deployed and no site was created.`,
2577
+ {
2578
+ projectRoot: rootHint.projectRoot,
2579
+ outputDir: rootHint.outputDir,
2580
+ confirmationField: "subprojectConfirmed"
2581
+ },
2547
2582
  "blocked"
2548
2583
  );
2549
2584
  }
2550
- const neighbor = findNeighborBinding(ctx.projectDir, analysis.recommendedOutputDir);
2585
+ const neighbor = findNeighborBinding(ctx.projectDir);
2551
2586
  if (neighbor) {
2552
2587
  return text(
2553
2588
  "neighbor_binding_found",
@@ -2671,6 +2706,7 @@ Environment: ${environmentFor(ctx.apiBaseUrl).toUpperCase()} (${ctx.apiBaseUrl})
2671
2706
  Project directory: ${ctx.projectDir}
2672
2707
  Files uploaded: ${uploaded} (${finalized.totalBytes} bytes)
2673
2708
  ` + (finalized.expiresAt ? `Validity refreshed \u2014 expires at: ${finalized.expiresAt}
2709
+ ` : "") + (credentialRelocatedFrom ? `Credential binding relocated from ${credentialRelocatedFrom}/.sakupa to ${ctx.projectDir}/.sakupa; the existing site was preserved.
2674
2710
  ` : "") + (finalized.mode === "free" ? `
2675
2711
  Reminder: free sites stay live for ${FREE_SITE_TTL_HOURS} hours after the last deploy or refresh call. Subscribing (subscribe) makes the site permanent.
2676
2712
  ` : "\nThis site is subscribed and permanent \u2014 no expiry.\n") + (finalized.warnings.length > 0 ? `
@@ -2685,7 +2721,8 @@ ${JSON.stringify(finalized.warnings, null, 2)}` : ""),
2685
2721
  expiresAt: finalized.expiresAt,
2686
2722
  filesUploaded: uploaded,
2687
2723
  totalBytes: finalized.totalBytes,
2688
- warnings: finalized.warnings
2724
+ warnings: finalized.warnings,
2725
+ ...credentialRelocatedFrom ? { credentialRelocatedFrom } : {}
2689
2726
  }
2690
2727
  );
2691
2728
  } catch (e) {
@@ -3630,8 +3667,11 @@ Project directory contract: ONE directory = ONE site (its .sakupa/site.json hold
3630
3667
  binding). projectDir is REQUIRED on EVERY tool call \u2014 always pass the absolute path of
3631
3668
  the user's PROJECT ROOT, the SAME directory every time for the same project: the folder
3632
3669
  the user opened (for framework projects, where package.json lives \u2014 never the dist/out
3633
- build folder; output is auto-detected). The server NEVER guesses a directory and refuses
3634
- calls without one: only you can see which directory the user is actually in. After every deploy, TELL the user which environment it went to (deploy results carry an
3670
+ build folder). For deploy, ALWAYS pass outputDir separately as the exact path RELATIVE to
3671
+ projectDir (use "." when publishing the root); outputDir is required and may have ANY name,
3672
+ so inspect the current project and never infer it from a conventional folder name. The server
3673
+ NEVER guesses either directory: only you can see the user's actual workspace and build output.
3674
+ After every deploy, TELL the user which environment it went to (deploy results carry an
3635
3675
  Explicit Environment line: TEST vs PRODUCTION). analyze, deploy, status,
3636
3676
  refresh and delete echo
3637
3677
  the directory they acted on \u2014 verify it matches the user's active project.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sakupa/mcp",
3
- "version": "0.7.31",
3
+ "version": "0.7.32",
4
4
  "description": "Sakupa MCP server: publish AI-made static sites from your AI tool. AI-made pages, live in seconds.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",