@sakupa/mcp 0.7.35 → 0.7.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +900 -71
- package/dist/index.js +935 -73
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
readFileSync,
|
|
15
15
|
realpathSync,
|
|
16
16
|
renameSync,
|
|
17
|
+
rmdirSync,
|
|
17
18
|
statSync,
|
|
18
19
|
unlinkSync,
|
|
19
20
|
writeFileSync
|
|
@@ -151,6 +152,14 @@ function updateProjectOutputDir(projectDir, outputDir) {
|
|
|
151
152
|
writeMarkerAtomically(canonical, marker);
|
|
152
153
|
return marker;
|
|
153
154
|
}
|
|
155
|
+
function deleteProjectMarker(projectDir) {
|
|
156
|
+
const path = projectMarkerPath(projectDir);
|
|
157
|
+
if (existsSync(path)) unlinkSync(path);
|
|
158
|
+
try {
|
|
159
|
+
rmdirSync(join(projectDir, SAKUPA_DIR));
|
|
160
|
+
} catch {
|
|
161
|
+
}
|
|
162
|
+
}
|
|
154
163
|
function canonicalProjectDirectory(path) {
|
|
155
164
|
const canonical = canonicalExistingPath(resolve(path));
|
|
156
165
|
if (!statSync(canonical).isDirectory()) {
|
|
@@ -370,7 +379,7 @@ var FORBIDDEN_PATH_SEGMENTS = [
|
|
|
370
379
|
var ALLOWED_HIDDEN_PATHS = [".well-known/"];
|
|
371
380
|
|
|
372
381
|
// ../core/dist/domain/version.js
|
|
373
|
-
var SAKUPA_MCP_VERSION = "0.7.
|
|
382
|
+
var SAKUPA_MCP_VERSION = "0.7.37";
|
|
374
383
|
|
|
375
384
|
// ../core/dist/domain/errors.js
|
|
376
385
|
var HTTP_STATUS = {
|
|
@@ -929,9 +938,9 @@ var HttpApiClient = class {
|
|
|
929
938
|
};
|
|
930
939
|
|
|
931
940
|
// src/tools/definitions.ts
|
|
932
|
-
import { randomUUID as
|
|
941
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
933
942
|
import { promises as fs2 } from "node:fs";
|
|
934
|
-
import { join as join6, resolve as
|
|
943
|
+
import { join as join6, relative as relative3, resolve as resolve5, sep as sep4 } from "node:path";
|
|
935
944
|
import { z as z2 } from "zod";
|
|
936
945
|
|
|
937
946
|
// src/analyze/analyzer.ts
|
|
@@ -1369,7 +1378,7 @@ import {
|
|
|
1369
1378
|
existsSync as existsSync2,
|
|
1370
1379
|
mkdirSync as mkdirSync2,
|
|
1371
1380
|
readFileSync as readFileSync2,
|
|
1372
|
-
rmdirSync,
|
|
1381
|
+
rmdirSync as rmdirSync2,
|
|
1373
1382
|
rmSync,
|
|
1374
1383
|
writeFileSync as writeFileSync2
|
|
1375
1384
|
} from "node:fs";
|
|
@@ -1496,7 +1505,7 @@ function deleteSiteFile(projectDir) {
|
|
|
1496
1505
|
rmSync(path, { force: true });
|
|
1497
1506
|
}
|
|
1498
1507
|
try {
|
|
1499
|
-
|
|
1508
|
+
rmdirSync2(join3(projectDir, SITE_DIR));
|
|
1500
1509
|
} catch {
|
|
1501
1510
|
}
|
|
1502
1511
|
}
|
|
@@ -1940,15 +1949,15 @@ function strFromU8(dat, latin1) {
|
|
|
1940
1949
|
var slzh = function(d, b) {
|
|
1941
1950
|
return b + 30 + b2(d, b + 26) + b2(d, b + 28);
|
|
1942
1951
|
};
|
|
1943
|
-
var zh = function(d, b,
|
|
1952
|
+
var zh = function(d, b, z6) {
|
|
1944
1953
|
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;
|
|
1945
|
-
var _a2 = z64hs(d, es, efl,
|
|
1954
|
+
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];
|
|
1946
1955
|
return [b2(d, b + 10), sc, su, fn, es + efl + b2(d, b + 32), off];
|
|
1947
1956
|
};
|
|
1948
|
-
var z64hs = function(d, b, l,
|
|
1957
|
+
var z64hs = function(d, b, l, z6, sc, su, off) {
|
|
1949
1958
|
var nsc = sc == 4294967295, nsu = su == 4294967295, noff = off == 4294967295, e = b + l;
|
|
1950
1959
|
var nf = nsc + nsu + noff;
|
|
1951
|
-
if (
|
|
1960
|
+
if (z6 && nf) {
|
|
1952
1961
|
for (; b + 4 < e; b += 4 + b2(d, b + 2)) {
|
|
1953
1962
|
if (b2(d, b) == 1) {
|
|
1954
1963
|
return [
|
|
@@ -1959,7 +1968,7 @@ var z64hs = function(d, b, l, z5, sc, su, off) {
|
|
|
1959
1968
|
];
|
|
1960
1969
|
}
|
|
1961
1970
|
}
|
|
1962
|
-
if (
|
|
1971
|
+
if (z6 < 2)
|
|
1963
1972
|
err(13);
|
|
1964
1973
|
}
|
|
1965
1974
|
return [sc, su, off, 0];
|
|
@@ -1976,18 +1985,18 @@ function unzipSync(data, opts) {
|
|
|
1976
1985
|
if (!c)
|
|
1977
1986
|
return {};
|
|
1978
1987
|
var o = b4(data, e + 16);
|
|
1979
|
-
var
|
|
1980
|
-
if (
|
|
1988
|
+
var z6 = b4(data, e - 20) == 117853008;
|
|
1989
|
+
if (z6) {
|
|
1981
1990
|
var ze = b4(data, e - 12);
|
|
1982
|
-
|
|
1983
|
-
if (
|
|
1991
|
+
z6 = b4(data, ze) == 101075792;
|
|
1992
|
+
if (z6) {
|
|
1984
1993
|
c = b4(data, ze + 32);
|
|
1985
1994
|
o = b4(data, ze + 48);
|
|
1986
1995
|
}
|
|
1987
1996
|
}
|
|
1988
1997
|
var fltr = opts && opts.filter;
|
|
1989
1998
|
for (var i = 0; i < c; ++i) {
|
|
1990
|
-
var _a2 = zh(data, o,
|
|
1999
|
+
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);
|
|
1991
2000
|
o = no;
|
|
1992
2001
|
if (!fltr || fltr({
|
|
1993
2002
|
name: fn,
|
|
@@ -2344,6 +2353,259 @@ ${diag.layers}
|
|
|
2344
2353
|
var MCP_VERSION = SAKUPA_MCP_VERSION;
|
|
2345
2354
|
var CLIENT_TYPE = "sakupa-mcp";
|
|
2346
2355
|
|
|
2356
|
+
// src/project-binding.ts
|
|
2357
|
+
import { fileURLToPath } from "node:url";
|
|
2358
|
+
import { resolve as resolve4 } from "node:path";
|
|
2359
|
+
var ProjectBindingError = class extends Error {
|
|
2360
|
+
diagnostics;
|
|
2361
|
+
constructor(diagnostics) {
|
|
2362
|
+
super(diagnostics.guidance);
|
|
2363
|
+
this.name = "ProjectBindingError";
|
|
2364
|
+
this.diagnostics = diagnostics;
|
|
2365
|
+
}
|
|
2366
|
+
};
|
|
2367
|
+
var ProjectBindingResolver = class {
|
|
2368
|
+
constructor(processCwd, rootsProvider) {
|
|
2369
|
+
this.processCwd = processCwd;
|
|
2370
|
+
this.rootsProvider = rootsProvider;
|
|
2371
|
+
}
|
|
2372
|
+
bound;
|
|
2373
|
+
boundState;
|
|
2374
|
+
resolving;
|
|
2375
|
+
async resolve() {
|
|
2376
|
+
if (this.bound) return this.bound;
|
|
2377
|
+
if (this.resolving) return this.resolving;
|
|
2378
|
+
this.resolving = this.inspect().then((inspection) => {
|
|
2379
|
+
if (!inspection.selected) throw new ProjectBindingError(inspection.diagnostics);
|
|
2380
|
+
this.bound = inspection.selected;
|
|
2381
|
+
this.boundState = inspection.diagnostics;
|
|
2382
|
+
return inspection.selected;
|
|
2383
|
+
}).finally(() => {
|
|
2384
|
+
this.resolving = void 0;
|
|
2385
|
+
});
|
|
2386
|
+
return this.resolving;
|
|
2387
|
+
}
|
|
2388
|
+
async diagnose() {
|
|
2389
|
+
if (this.bound) return this.boundState ?? boundDiagnostics(this.processCwd, this.bound);
|
|
2390
|
+
const inspection = await this.inspect();
|
|
2391
|
+
if (inspection.selected) {
|
|
2392
|
+
this.bound = inspection.selected;
|
|
2393
|
+
this.boundState = inspection.diagnostics;
|
|
2394
|
+
}
|
|
2395
|
+
return inspection.diagnostics;
|
|
2396
|
+
}
|
|
2397
|
+
async initialize() {
|
|
2398
|
+
if (this.bound) return this.bound;
|
|
2399
|
+
const inspection = await this.inspect(true);
|
|
2400
|
+
if (inspection.selected) {
|
|
2401
|
+
this.bound = inspection.selected;
|
|
2402
|
+
this.boundState = inspection.diagnostics;
|
|
2403
|
+
return inspection.selected;
|
|
2404
|
+
}
|
|
2405
|
+
if (!inspection.initializableRoot) {
|
|
2406
|
+
throw new ProjectBindingError(inspection.diagnostics);
|
|
2407
|
+
}
|
|
2408
|
+
const initialized = initializeProject(inspection.initializableRoot);
|
|
2409
|
+
this.bound = { ...initialized, bindingSource: "mcp_root" };
|
|
2410
|
+
this.boundState = boundDiagnostics(
|
|
2411
|
+
this.processCwd,
|
|
2412
|
+
this.bound,
|
|
2413
|
+
{ supported: true, roots: [] },
|
|
2414
|
+
inspection.diagnostics.rootCandidates
|
|
2415
|
+
);
|
|
2416
|
+
return this.bound;
|
|
2417
|
+
}
|
|
2418
|
+
async inspect(forInitialization = false) {
|
|
2419
|
+
const snapshot = await safeRootsSnapshot(this.rootsProvider);
|
|
2420
|
+
const rootCandidates = snapshot.roots.map(inspectRoot);
|
|
2421
|
+
const initializedRoots = rootCandidates.filter(
|
|
2422
|
+
(candidate) => candidate.initialized && candidate.path !== void 0
|
|
2423
|
+
);
|
|
2424
|
+
if (snapshot.supported && snapshot.error) {
|
|
2425
|
+
return {
|
|
2426
|
+
diagnostics: diagnostic(
|
|
2427
|
+
"roots_request_failed",
|
|
2428
|
+
snapshot,
|
|
2429
|
+
this.processCwd,
|
|
2430
|
+
rootCandidates,
|
|
2431
|
+
"The IDE advertised MCP Roots, but the Roots request failed. Retry help after the IDE finishes loading the workspace. If it persists, restart the MCP connection; do not initialize or deploy from the IDE installation directory."
|
|
2432
|
+
)
|
|
2433
|
+
};
|
|
2434
|
+
}
|
|
2435
|
+
if (initializedRoots.length === 1) {
|
|
2436
|
+
const initializedRoot = initializedRoots[0];
|
|
2437
|
+
if (!initializedRoot) throw new Error("initialized Root disappeared during resolution");
|
|
2438
|
+
const project = resolveLockedProjectRoot(initializedRoot.path);
|
|
2439
|
+
const selected = { ...project, bindingSource: "mcp_root" };
|
|
2440
|
+
return {
|
|
2441
|
+
selected,
|
|
2442
|
+
diagnostics: boundDiagnostics(this.processCwd, selected, snapshot, rootCandidates)
|
|
2443
|
+
};
|
|
2444
|
+
}
|
|
2445
|
+
if (initializedRoots.length > 1) {
|
|
2446
|
+
return {
|
|
2447
|
+
diagnostics: diagnostic(
|
|
2448
|
+
"multiple_initialized_roots",
|
|
2449
|
+
snapshot,
|
|
2450
|
+
this.processCwd,
|
|
2451
|
+
rootCandidates,
|
|
2452
|
+
"More than one IDE workspace Root is already initialized for Sakupa. Close the unrelated workspaces and retry help; Sakupa will not guess which site to manage."
|
|
2453
|
+
)
|
|
2454
|
+
};
|
|
2455
|
+
}
|
|
2456
|
+
const validRoots = rootCandidates.filter(
|
|
2457
|
+
(candidate) => candidate.path !== void 0 && candidate.problem === void 0
|
|
2458
|
+
);
|
|
2459
|
+
if (forInitialization && snapshot.supported) {
|
|
2460
|
+
if (validRoots.length === 1) {
|
|
2461
|
+
const validRoot = validRoots[0];
|
|
2462
|
+
if (!validRoot) throw new Error("workspace Root disappeared during initialization");
|
|
2463
|
+
return {
|
|
2464
|
+
initializableRoot: validRoot.path,
|
|
2465
|
+
diagnostics: diagnostic(
|
|
2466
|
+
"workspace_not_initialized",
|
|
2467
|
+
snapshot,
|
|
2468
|
+
this.processCwd,
|
|
2469
|
+
rootCandidates,
|
|
2470
|
+
`The active MCP workspace ${validRoot.path} is ready to initialize.`
|
|
2471
|
+
)
|
|
2472
|
+
};
|
|
2473
|
+
}
|
|
2474
|
+
if (validRoots.length > 1) {
|
|
2475
|
+
return {
|
|
2476
|
+
diagnostics: diagnostic(
|
|
2477
|
+
"multiple_uninitialized_roots",
|
|
2478
|
+
snapshot,
|
|
2479
|
+
this.processCwd,
|
|
2480
|
+
rootCandidates,
|
|
2481
|
+
"The IDE exposes multiple uninitialized workspace Roots. Open only the intended project before calling init; Sakupa will not choose a directory for the user."
|
|
2482
|
+
)
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
if (snapshot.supported && validRoots.length === 1) {
|
|
2487
|
+
const validRoot = validRoots[0];
|
|
2488
|
+
if (!validRoot) throw new Error("workspace Root disappeared during diagnosis");
|
|
2489
|
+
return {
|
|
2490
|
+
diagnostics: diagnostic(
|
|
2491
|
+
"workspace_not_initialized",
|
|
2492
|
+
snapshot,
|
|
2493
|
+
this.processCwd,
|
|
2494
|
+
rootCandidates,
|
|
2495
|
+
`The IDE workspace ${validRoot.path} is not initialized. Call init with no path arguments; it will create .sakupa directly in that workspace Root.`
|
|
2496
|
+
)
|
|
2497
|
+
};
|
|
2498
|
+
}
|
|
2499
|
+
if (snapshot.supported && validRoots.length > 1) {
|
|
2500
|
+
return {
|
|
2501
|
+
diagnostics: diagnostic(
|
|
2502
|
+
"multiple_uninitialized_roots",
|
|
2503
|
+
snapshot,
|
|
2504
|
+
this.processCwd,
|
|
2505
|
+
rootCandidates,
|
|
2506
|
+
"The IDE exposes multiple uninitialized workspace Roots. Open only the intended project, then call init. Sakupa will not guess a project directory."
|
|
2507
|
+
)
|
|
2508
|
+
};
|
|
2509
|
+
}
|
|
2510
|
+
if (snapshot.supported) {
|
|
2511
|
+
return {
|
|
2512
|
+
diagnostics: diagnostic(
|
|
2513
|
+
"workspace_not_initialized",
|
|
2514
|
+
snapshot,
|
|
2515
|
+
this.processCwd,
|
|
2516
|
+
rootCandidates,
|
|
2517
|
+
"The IDE did not expose one usable file workspace Root. Open exactly one local project workspace, then retry help before calling init or deploy."
|
|
2518
|
+
)
|
|
2519
|
+
};
|
|
2520
|
+
}
|
|
2521
|
+
try {
|
|
2522
|
+
const cwdProject = resolveLockedProjectRoot(this.processCwd);
|
|
2523
|
+
const selected = { ...cwdProject, bindingSource: "process_cwd" };
|
|
2524
|
+
return {
|
|
2525
|
+
selected,
|
|
2526
|
+
diagnostics: boundDiagnostics(this.processCwd, selected, snapshot, rootCandidates)
|
|
2527
|
+
};
|
|
2528
|
+
} catch {
|
|
2529
|
+
}
|
|
2530
|
+
const cwdProblem = inspectDirectory(this.processCwd);
|
|
2531
|
+
return {
|
|
2532
|
+
diagnostics: diagnostic(
|
|
2533
|
+
cwdProblem.problem ? "invalid_process_cwd" : "process_cwd_is_not_workspace",
|
|
2534
|
+
snapshot,
|
|
2535
|
+
this.processCwd,
|
|
2536
|
+
rootCandidates,
|
|
2537
|
+
"This IDE did not provide MCP Roots and the MCP process cwd is not an initialized project. Do not write into the IDE installation directory. Run `npx -y @sakupa/mcp@latest init` with no path arguments from the intended project terminal, then configure/restart the MCP process in that directory."
|
|
2538
|
+
)
|
|
2539
|
+
};
|
|
2540
|
+
}
|
|
2541
|
+
};
|
|
2542
|
+
function fileRootUriToPath(uri, windows = process.platform === "win32") {
|
|
2543
|
+
const parsed = new URL(uri);
|
|
2544
|
+
if (parsed.protocol !== "file:") throw new Error("Root URI is not a file URI");
|
|
2545
|
+
return fileURLToPath(parsed, { windows });
|
|
2546
|
+
}
|
|
2547
|
+
async function safeRootsSnapshot(provider) {
|
|
2548
|
+
if (!provider) return { supported: false, roots: [] };
|
|
2549
|
+
try {
|
|
2550
|
+
return await provider();
|
|
2551
|
+
} catch (error) {
|
|
2552
|
+
return {
|
|
2553
|
+
supported: true,
|
|
2554
|
+
roots: [],
|
|
2555
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2556
|
+
};
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
function inspectRoot(root) {
|
|
2560
|
+
try {
|
|
2561
|
+
const path = canonicalProjectDirectory(fileRootUriToPath(root.uri));
|
|
2562
|
+
const marker = loadProjectMarker(path);
|
|
2563
|
+
return {
|
|
2564
|
+
uri: root.uri,
|
|
2565
|
+
...root.name !== void 0 ? { name: root.name } : {},
|
|
2566
|
+
path,
|
|
2567
|
+
initialized: marker.kind === "ok",
|
|
2568
|
+
...marker.kind === "corrupted" ? { problem: marker.problem } : {}
|
|
2569
|
+
};
|
|
2570
|
+
} catch (error) {
|
|
2571
|
+
return {
|
|
2572
|
+
uri: root.uri,
|
|
2573
|
+
...root.name !== void 0 ? { name: root.name } : {},
|
|
2574
|
+
initialized: false,
|
|
2575
|
+
problem: error instanceof Error ? error.message : String(error)
|
|
2576
|
+
};
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
function inspectDirectory(path) {
|
|
2580
|
+
try {
|
|
2581
|
+
return { path: canonicalProjectDirectory(resolve4(path)) };
|
|
2582
|
+
} catch (error) {
|
|
2583
|
+
return { problem: error instanceof Error ? error.message : String(error) };
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
function diagnostic(diagnosisCode, snapshot, processCwd, rootCandidates, guidance) {
|
|
2587
|
+
return {
|
|
2588
|
+
diagnosisCode,
|
|
2589
|
+
mcpRootsSupported: snapshot.supported,
|
|
2590
|
+
processCwd,
|
|
2591
|
+
rootCandidates,
|
|
2592
|
+
guidance,
|
|
2593
|
+
reportRecommended: false
|
|
2594
|
+
};
|
|
2595
|
+
}
|
|
2596
|
+
function boundDiagnostics(processCwd, selected, snapshot = { supported: false, roots: [] }, rootCandidates = []) {
|
|
2597
|
+
return {
|
|
2598
|
+
diagnosisCode: "project_bound",
|
|
2599
|
+
mcpRootsSupported: snapshot.supported,
|
|
2600
|
+
processCwd,
|
|
2601
|
+
rootCandidates,
|
|
2602
|
+
selectedProjectDir: selected.projectDir,
|
|
2603
|
+
bindingSource: selected.bindingSource,
|
|
2604
|
+
guidance: `Sakupa is locked to ${selected.projectDir} from ${selected.bindingSource}.`,
|
|
2605
|
+
reportRecommended: false
|
|
2606
|
+
};
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2347
2609
|
// src/tools/result.ts
|
|
2348
2610
|
import { z } from "zod";
|
|
2349
2611
|
var STRUCTURED_TOOL_OUTPUT_SCHEMA = {
|
|
@@ -2386,22 +2648,39 @@ function structuredToolResult(envelope) {
|
|
|
2386
2648
|
}
|
|
2387
2649
|
|
|
2388
2650
|
// src/tools/context.ts
|
|
2651
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2389
2652
|
var LocalGuidanceError = class extends SakupaError {
|
|
2390
2653
|
constructor(code, message) {
|
|
2391
2654
|
super(code, message);
|
|
2392
2655
|
}
|
|
2393
2656
|
};
|
|
2394
|
-
|
|
2657
|
+
var fallbackResolvers = /* @__PURE__ */ new WeakMap();
|
|
2658
|
+
var reportAuthorizations = /* @__PURE__ */ new WeakMap();
|
|
2659
|
+
function resolverFor(ctx) {
|
|
2660
|
+
if (ctx.projectBinding) return ctx.projectBinding;
|
|
2661
|
+
let resolver = fallbackResolvers.get(ctx);
|
|
2662
|
+
if (!resolver) {
|
|
2663
|
+
resolver = new ProjectBindingResolver(ctx.projectDir, ctx.rootsProvider);
|
|
2664
|
+
fallbackResolvers.set(ctx, resolver);
|
|
2665
|
+
}
|
|
2666
|
+
return resolver;
|
|
2667
|
+
}
|
|
2668
|
+
async function withProjectDir(ctx) {
|
|
2395
2669
|
try {
|
|
2396
|
-
const
|
|
2670
|
+
const binding = await resolverFor(ctx).resolve();
|
|
2671
|
+
const resolved = resolveLockedProjectRoot(binding.projectDir);
|
|
2397
2672
|
return {
|
|
2398
2673
|
...ctx,
|
|
2399
2674
|
projectDir: resolved.projectDir,
|
|
2400
2675
|
requestedPath: resolved.requestedPath,
|
|
2401
2676
|
markerKind: resolved.markerKind,
|
|
2402
|
-
projectMarker: resolved.marker
|
|
2677
|
+
projectMarker: resolved.marker,
|
|
2678
|
+
bindingSource: binding.bindingSource
|
|
2403
2679
|
};
|
|
2404
2680
|
} catch (error) {
|
|
2681
|
+
if (error instanceof ProjectBindingError) {
|
|
2682
|
+
throw new LocalGuidanceError("not_found", error.message);
|
|
2683
|
+
}
|
|
2405
2684
|
if (error instanceof ProjectRootError) {
|
|
2406
2685
|
throw new LocalGuidanceError(
|
|
2407
2686
|
error.code === "not_initialized" ? "not_found" : "invalid_request",
|
|
@@ -2411,6 +2690,60 @@ function withProjectDir(ctx) {
|
|
|
2411
2690
|
throw error;
|
|
2412
2691
|
}
|
|
2413
2692
|
}
|
|
2693
|
+
async function diagnoseProjectBinding(ctx) {
|
|
2694
|
+
return resolverFor(ctx).diagnose();
|
|
2695
|
+
}
|
|
2696
|
+
async function initializeWorkspaceProject(ctx) {
|
|
2697
|
+
try {
|
|
2698
|
+
const resolved = await resolverFor(ctx).initialize();
|
|
2699
|
+
return {
|
|
2700
|
+
...ctx,
|
|
2701
|
+
projectDir: resolved.projectDir,
|
|
2702
|
+
requestedPath: resolved.requestedPath,
|
|
2703
|
+
markerKind: resolved.markerKind,
|
|
2704
|
+
projectMarker: resolved.marker,
|
|
2705
|
+
bindingSource: resolved.bindingSource
|
|
2706
|
+
};
|
|
2707
|
+
} catch (error) {
|
|
2708
|
+
if (error instanceof ProjectBindingError) {
|
|
2709
|
+
throw new LocalGuidanceError("invalid_request", error.message);
|
|
2710
|
+
}
|
|
2711
|
+
throw error;
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
async function optionalProjectContext(ctx) {
|
|
2715
|
+
try {
|
|
2716
|
+
return await withProjectDir(ctx);
|
|
2717
|
+
} catch {
|
|
2718
|
+
return null;
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
function reportAuthorizationStore(ctx) {
|
|
2722
|
+
let store = reportAuthorizations.get(ctx);
|
|
2723
|
+
if (!store) {
|
|
2724
|
+
store = /* @__PURE__ */ new Map();
|
|
2725
|
+
reportAuthorizations.set(ctx, store);
|
|
2726
|
+
}
|
|
2727
|
+
return store;
|
|
2728
|
+
}
|
|
2729
|
+
function issueReportAuthorization(ctx, failedTool) {
|
|
2730
|
+
const token = randomUUID2();
|
|
2731
|
+
reportAuthorizationStore(ctx).set(token, {
|
|
2732
|
+
failedTool,
|
|
2733
|
+
expiresAt: Date.now() + 10 * 60 * 1e3
|
|
2734
|
+
});
|
|
2735
|
+
return token;
|
|
2736
|
+
}
|
|
2737
|
+
function requireReportAuthorization(ctx, token, failedTool) {
|
|
2738
|
+
const authorization = token ? reportAuthorizationStore(ctx).get(token) : void 0;
|
|
2739
|
+
if (!authorization || authorization.expiresAt < Date.now() || authorization.failedTool !== failedTool) {
|
|
2740
|
+
if (token) reportAuthorizationStore(ctx).delete(token);
|
|
2741
|
+
throw new LocalGuidanceError(
|
|
2742
|
+
"invalid_request",
|
|
2743
|
+
'report is the last resort. Run help with topic:"diagnose", failedTool and the error code first. Only when help returns reportRecommended:true, copy its helpAuthorization into report.'
|
|
2744
|
+
);
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2414
2747
|
function requireSiteFile(ctx) {
|
|
2415
2748
|
const state = loadSiteFile(ctx.projectDir);
|
|
2416
2749
|
if (state.kind === "corrupted") {
|
|
@@ -2449,7 +2782,7 @@ function toolError(e) {
|
|
|
2449
2782
|
) : void 0;
|
|
2450
2783
|
const minimumVersion = rawDetails && typeof rawDetails["minimumVersion"] === "string" ? rawDetails["minimumVersion"] : void 0;
|
|
2451
2784
|
const serverGuidance = isSakupaError(e) && errorCode !== "internal" && errorCode !== "unauthorized" && errorCode !== "upgrade_required" && e.message.trim().length > 0 ? e.message : void 0;
|
|
2452
|
-
const safeSummary = e instanceof LocalGuidanceError ? e.message : errorCode === "upgrade_required" ? `This Sakupa MCP client is v${MCP_VERSION}, older than the server's minimum supported version${minimumVersion !== void 0 ? ` (v${minimumVersion})` : ""}, so the server refused the call. To fix it: ask the user to fully restart their MCP client session \u2014 "npx -y @sakupa/mcp@latest" setups fetch the current version on restart (run "npx clear-npx-cache" first if the old version persists); global installs need "npm install -g @sakupa/mcp@latest". After the restart, retry this exact tool call.` : errorCode === "unauthorized" ? UNAUTHORIZED_SUMMARY : serverGuidance ?? (retryable ? "An upstream service is temporarily unavailable or busy; retry shortly." : opaqueUnclassified ? "This failed with an error Sakupa could not classify, and retrying will not help.
|
|
2785
|
+
const safeSummary = e instanceof LocalGuidanceError ? e.message : errorCode === "upgrade_required" ? `This Sakupa MCP client is v${MCP_VERSION}, older than the server's minimum supported version${minimumVersion !== void 0 ? ` (v${minimumVersion})` : ""}, so the server refused the call. To fix it: ask the user to fully restart their MCP client session \u2014 "npx -y @sakupa/mcp@latest" setups fetch the current version on restart (run "npx clear-npx-cache" first if the old version persists); global installs need "npm install -g @sakupa/mcp@latest". After the restart, retry this exact tool call.` : errorCode === "unauthorized" ? UNAUTHORIZED_SUMMARY : serverGuidance ?? (retryable ? "An upstream service is temporarily unavailable or busy; retry shortly." : opaqueUnclassified ? "This failed with an error Sakupa could not classify, and retrying the same call will not help. Run help with the failed tool and error code first; only use report if help explicitly recommends it." : "The operation failed; no server-internal details are exposed.");
|
|
2453
2786
|
const result = structuredToolResult({
|
|
2454
2787
|
schemaVersion: 1,
|
|
2455
2788
|
outcome: "failed",
|
|
@@ -2460,7 +2793,14 @@ function toolError(e) {
|
|
|
2460
2793
|
retryable,
|
|
2461
2794
|
...safeDetails && Object.keys(safeDetails).length > 0 ? { details: safeDetails } : {}
|
|
2462
2795
|
},
|
|
2463
|
-
nextActions: [
|
|
2796
|
+
nextActions: [
|
|
2797
|
+
{
|
|
2798
|
+
tool: "help",
|
|
2799
|
+
arguments: { topic: "diagnose", failedTool: "unknown", errorCode },
|
|
2800
|
+
allowed: true,
|
|
2801
|
+
reasonCode: "diagnose_before_report"
|
|
2802
|
+
}
|
|
2803
|
+
]
|
|
2464
2804
|
});
|
|
2465
2805
|
return { ...result, isError: true };
|
|
2466
2806
|
}
|
|
@@ -2620,6 +2960,27 @@ If this list is stale (sites deleted or subscribed from another machine), remove
|
|
|
2620
2960
|
"blocked"
|
|
2621
2961
|
);
|
|
2622
2962
|
}
|
|
2963
|
+
function outputDirectoryChain(projectRoot, outputAbs) {
|
|
2964
|
+
const rel = relative3(projectRoot, outputAbs);
|
|
2965
|
+
if (rel === "" || rel === ".") return [];
|
|
2966
|
+
if (rel === ".." || rel.startsWith(`..${sep4}`)) return [];
|
|
2967
|
+
const chain = [];
|
|
2968
|
+
let cursor = projectRoot;
|
|
2969
|
+
for (const part of rel.split(sep4).filter(Boolean)) {
|
|
2970
|
+
cursor = join6(cursor, part);
|
|
2971
|
+
chain.push(cursor);
|
|
2972
|
+
}
|
|
2973
|
+
return chain;
|
|
2974
|
+
}
|
|
2975
|
+
async function sakupaDirectoryEntries(projectDir) {
|
|
2976
|
+
try {
|
|
2977
|
+
return await fs2.readdir(join6(projectDir, ".sakupa"));
|
|
2978
|
+
} catch (error) {
|
|
2979
|
+
const code = error.code;
|
|
2980
|
+
if (code === "ENOENT") return [];
|
|
2981
|
+
throw error;
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2623
2984
|
function registerTools(server, baseCtx) {
|
|
2624
2985
|
const previewHostPattern = previewHostPatternFor(baseCtx.apiBaseUrl);
|
|
2625
2986
|
server.registerTool(
|
|
@@ -2634,7 +2995,7 @@ function registerTools(server, baseCtx) {
|
|
|
2634
2995
|
},
|
|
2635
2996
|
async (args) => {
|
|
2636
2997
|
try {
|
|
2637
|
-
const ctx = withProjectDir(baseCtx);
|
|
2998
|
+
const ctx = await withProjectDir(baseCtx);
|
|
2638
2999
|
const analysis = await analyzeProject(ctx.projectDir, {
|
|
2639
3000
|
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
|
|
2640
3001
|
});
|
|
@@ -2662,6 +3023,9 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
2662
3023
|
outputDirChangeConfirmed: z2.boolean().optional().describe(
|
|
2663
3024
|
"Required only when changing the previously successful publish directory. Confirm only after showing the old and new directories to the user."
|
|
2664
3025
|
),
|
|
3026
|
+
sakupaRelocationConfirmed: z2.boolean().optional().describe(
|
|
3027
|
+
"Required only when a nested directory is itself initialized with .sakupa/project.json. Confirm only after showing the source and authoritative project Root; Sakupa then migrates non-conflicting state without exposing credentials."
|
|
3028
|
+
),
|
|
2665
3029
|
spaFallback: z2.boolean().optional().describe(
|
|
2666
3030
|
"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."
|
|
2667
3031
|
),
|
|
@@ -2676,14 +3040,14 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
2676
3040
|
},
|
|
2677
3041
|
async (args) => {
|
|
2678
3042
|
try {
|
|
2679
|
-
const ctx = withProjectDir(baseCtx);
|
|
3043
|
+
const ctx = await withProjectDir(baseCtx);
|
|
2680
3044
|
const analysis = await analyzeProject(ctx.projectDir, { outputDir: args.outputDir });
|
|
2681
3045
|
if (!analysis.deployable || !analysis.files) {
|
|
2682
3046
|
return notDeployableResult(analysis);
|
|
2683
3047
|
}
|
|
2684
3048
|
const effectiveOutputDir = analysis.recommendedOutputDir ?? ".";
|
|
2685
3049
|
const recordedOutputDir = ctx.projectMarker?.outputDir;
|
|
2686
|
-
if (recordedOutputDir !== void 0 &&
|
|
3050
|
+
if (recordedOutputDir !== void 0 && resolve5(ctx.projectDir, recordedOutputDir) !== resolve5(ctx.projectDir, effectiveOutputDir) && args.outputDirChangeConfirmed !== true) {
|
|
2687
3051
|
return structuredToolResult({
|
|
2688
3052
|
schemaVersion: 1,
|
|
2689
3053
|
outcome: "waiting_user",
|
|
@@ -2699,7 +3063,7 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
2699
3063
|
});
|
|
2700
3064
|
}
|
|
2701
3065
|
const files = analysis.files;
|
|
2702
|
-
const outputAbs =
|
|
3066
|
+
const outputAbs = resolve5(ctx.projectDir, effectiveOutputDir);
|
|
2703
3067
|
const manifest = await buildHashedManifest(files, outputAbs);
|
|
2704
3068
|
const siteFileState = loadSiteFile(ctx.projectDir);
|
|
2705
3069
|
if (siteFileState.kind === "corrupted") {
|
|
@@ -2713,30 +3077,157 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
2713
3077
|
);
|
|
2714
3078
|
}
|
|
2715
3079
|
let existing = siteFileState.kind === "ok" ? siteFileState.file : null;
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
3080
|
+
const credentialRelocatedFrom = [];
|
|
3081
|
+
const markerRelocatedFrom = [];
|
|
3082
|
+
const nestedSiteFiles = [];
|
|
3083
|
+
const nestedRecoveryFiles = [];
|
|
3084
|
+
for (const candidateDir of outputDirectoryChain(ctx.projectDir, outputAbs)) {
|
|
3085
|
+
const entries = await sakupaDirectoryEntries(candidateDir);
|
|
3086
|
+
if (entries.length === 0) continue;
|
|
3087
|
+
const unknownEntries = entries.filter(
|
|
3088
|
+
(entry) => !["project.json", "site.json", "recovery.json"].includes(entry)
|
|
3089
|
+
);
|
|
3090
|
+
if (unknownEntries.length > 0) {
|
|
2720
3091
|
return text(
|
|
2721
|
-
"
|
|
2722
|
-
|
|
2723
|
-
{ projectRoot: ctx.projectDir,
|
|
3092
|
+
"nested_sakupa_contains_unknown_files",
|
|
3093
|
+
`A nested .sakupa directory at ${candidateDir} contains unknown files ${JSON.stringify(unknownEntries)}. Nothing was moved or deployed. Run help; Sakupa will never delete unrecognized user files.`,
|
|
3094
|
+
{ projectRoot: ctx.projectDir, nestedDirectory: candidateDir, unknownEntries },
|
|
2724
3095
|
"blocked"
|
|
2725
3096
|
);
|
|
2726
3097
|
}
|
|
2727
|
-
const
|
|
2728
|
-
if (
|
|
3098
|
+
const nestedMarker = loadProjectMarker(candidateDir);
|
|
3099
|
+
if (nestedMarker.kind === "corrupted") {
|
|
2729
3100
|
return text(
|
|
2730
|
-
"
|
|
2731
|
-
`
|
|
2732
|
-
{ projectRoot: ctx.projectDir,
|
|
3101
|
+
"nested_project_marker_corrupted",
|
|
3102
|
+
`The nested .sakupa/project.json at ${candidateDir} is damaged: ${nestedMarker.problem}. Nothing was moved or deployed.`,
|
|
3103
|
+
{ projectRoot: ctx.projectDir, nestedDirectory: candidateDir },
|
|
2733
3104
|
"blocked"
|
|
2734
3105
|
);
|
|
2735
3106
|
}
|
|
2736
|
-
if (
|
|
2737
|
-
|
|
2738
|
-
|
|
3107
|
+
if (nestedMarker.kind === "ok") {
|
|
3108
|
+
if (args.sakupaRelocationConfirmed !== true) {
|
|
3109
|
+
return structuredToolResult({
|
|
3110
|
+
schemaVersion: 1,
|
|
3111
|
+
outcome: "waiting_user",
|
|
3112
|
+
resultCode: "sakupa_relocation_confirmation_required",
|
|
3113
|
+
summary: `A nested Sakupa project marker exists at ${candidateDir}/.sakupa, but the active MCP Root is ${ctx.projectDir}. Nothing was moved or deployed. Show both paths to the user; after confirmation retry deploy with sakupaRelocationConfirmed:true. Sakupa will preserve credentials and refuse conflicts.`,
|
|
3114
|
+
data: {
|
|
3115
|
+
projectRoot: ctx.projectDir,
|
|
3116
|
+
misplacedSakupaDirectory: join6(candidateDir, ".sakupa"),
|
|
3117
|
+
targetSakupaDirectory: join6(ctx.projectDir, ".sakupa"),
|
|
3118
|
+
confirmationField: "sakupaRelocationConfirmed"
|
|
3119
|
+
},
|
|
3120
|
+
nextActions: [
|
|
3121
|
+
{
|
|
3122
|
+
tool: "deploy",
|
|
3123
|
+
allowed: true,
|
|
3124
|
+
reasonCode: "explicit_sakupa_relocation_confirmation"
|
|
3125
|
+
}
|
|
3126
|
+
]
|
|
3127
|
+
});
|
|
3128
|
+
}
|
|
3129
|
+
markerRelocatedFrom.push(candidateDir);
|
|
3130
|
+
}
|
|
3131
|
+
const nestedSite = loadSiteFile(candidateDir);
|
|
3132
|
+
if (nestedSite.kind === "corrupted") {
|
|
3133
|
+
return text(
|
|
3134
|
+
"output_site_file_corrupted",
|
|
3135
|
+
`A misplaced .sakupa/site.json exists at ${candidateDir}, but it is damaged: ${nestedSite.problem}. Nothing was moved or deployed.`,
|
|
3136
|
+
{ projectRoot: ctx.projectDir, nestedDirectory: candidateDir },
|
|
3137
|
+
"blocked"
|
|
3138
|
+
);
|
|
2739
3139
|
}
|
|
3140
|
+
if (nestedSite.kind === "ok")
|
|
3141
|
+
nestedSiteFiles.push({ dir: candidateDir, file: nestedSite.file });
|
|
3142
|
+
const nestedRecovery = loadRecoveryFile(candidateDir);
|
|
3143
|
+
if (nestedRecovery) nestedRecoveryFiles.push({ dir: candidateDir, file: nestedRecovery });
|
|
3144
|
+
}
|
|
3145
|
+
const allSiteFiles = [
|
|
3146
|
+
...existing ? [{ dir: ctx.projectDir, file: existing }] : [],
|
|
3147
|
+
...nestedSiteFiles
|
|
3148
|
+
];
|
|
3149
|
+
const uniqueSiteBindings = new Set(
|
|
3150
|
+
allSiteFiles.map(({ file }) => `${file.siteId}\0${file.credential}`)
|
|
3151
|
+
);
|
|
3152
|
+
if (uniqueSiteBindings.size > 1) {
|
|
3153
|
+
return text(
|
|
3154
|
+
"sakupa_relocation_conflict",
|
|
3155
|
+
"The project Root and nested publish path contain different Sakupa site bindings. Nothing was moved or deployed; run help. Sakupa will never overwrite one site credential with another.",
|
|
3156
|
+
{
|
|
3157
|
+
projectRoot: ctx.projectDir,
|
|
3158
|
+
conflictingDirectories: allSiteFiles.map(({ dir }) => dir)
|
|
3159
|
+
},
|
|
3160
|
+
"blocked"
|
|
3161
|
+
);
|
|
3162
|
+
}
|
|
3163
|
+
if (!existing && nestedSiteFiles[0]) existing = nestedSiteFiles[0].file;
|
|
3164
|
+
credentialRelocatedFrom.push(...nestedSiteFiles.map(({ dir }) => dir));
|
|
3165
|
+
const rootRecovery = loadRecoveryFile(ctx.projectDir);
|
|
3166
|
+
const allRecoveries = [
|
|
3167
|
+
...rootRecovery ? [{ dir: ctx.projectDir, file: rootRecovery }] : [],
|
|
3168
|
+
...nestedRecoveryFiles
|
|
3169
|
+
];
|
|
3170
|
+
const uniqueRecoveries = new Set(
|
|
3171
|
+
allRecoveries.map(({ file }) => `${file.verificationId}\0${file.credential}`)
|
|
3172
|
+
);
|
|
3173
|
+
if (uniqueRecoveries.size > 1) {
|
|
3174
|
+
return text(
|
|
3175
|
+
"recovery_relocation_conflict",
|
|
3176
|
+
"The project Root and nested publish path contain different recovery credentials. Nothing was moved or deployed; run help. Sakupa will not restart DNS recovery.",
|
|
3177
|
+
{
|
|
3178
|
+
projectRoot: ctx.projectDir,
|
|
3179
|
+
conflictingDirectories: allRecoveries.map(({ dir }) => dir)
|
|
3180
|
+
},
|
|
3181
|
+
"blocked"
|
|
3182
|
+
);
|
|
3183
|
+
}
|
|
3184
|
+
const localCredentials = /* @__PURE__ */ new Set([
|
|
3185
|
+
...allSiteFiles.map(({ file }) => file.credential),
|
|
3186
|
+
...allRecoveries.map(({ file }) => file.credential)
|
|
3187
|
+
]);
|
|
3188
|
+
if (allSiteFiles.length > 0 && allRecoveries.length > 0 && localCredentials.size > 1) {
|
|
3189
|
+
return text(
|
|
3190
|
+
"site_recovery_relocation_conflict",
|
|
3191
|
+
"The project Root and nested publish path contain site and recovery credentials that do not belong to the same recovered site. Nothing was moved or deployed; run help. Sakupa will never merge unrelated credentials.",
|
|
3192
|
+
{
|
|
3193
|
+
projectRoot: ctx.projectDir,
|
|
3194
|
+
siteDirectories: allSiteFiles.map(({ dir }) => dir),
|
|
3195
|
+
recoveryDirectories: allRecoveries.map(({ dir }) => dir)
|
|
3196
|
+
},
|
|
3197
|
+
"blocked"
|
|
3198
|
+
);
|
|
3199
|
+
}
|
|
3200
|
+
if (nestedRecoveryFiles.length > 0) {
|
|
3201
|
+
const recovery = rootRecovery ?? nestedRecoveryFiles[0]?.file;
|
|
3202
|
+
if (!recovery) throw new Error("recovery relocation lost its source state");
|
|
3203
|
+
if (!rootRecovery) writeRecoveryFile(ctx.projectDir, recovery);
|
|
3204
|
+
for (const { dir } of nestedRecoveryFiles) deleteRecoveryFile(dir);
|
|
3205
|
+
for (const dir of markerRelocatedFrom) deleteProjectMarker(dir);
|
|
3206
|
+
return structuredToolResult({
|
|
3207
|
+
schemaVersion: 1,
|
|
3208
|
+
outcome: "blocked",
|
|
3209
|
+
resultCode: "recovery_relocated_resume_required",
|
|
3210
|
+
summary: `Recovery state was moved safely to ${ctx.projectDir}/.sakupa before any deploy. The credential remains intact. Do not repeat DNS verification; call recover with action:"download" and outputDir:"${effectiveOutputDir}".`,
|
|
3211
|
+
data: {
|
|
3212
|
+
projectRoot: ctx.projectDir,
|
|
3213
|
+
relocatedFrom: nestedRecoveryFiles.map(({ dir }) => dir),
|
|
3214
|
+
credentialStoredLocally: true,
|
|
3215
|
+
dnsVerificationRepeated: false
|
|
3216
|
+
},
|
|
3217
|
+
nextActions: [
|
|
3218
|
+
{
|
|
3219
|
+
tool: "recover",
|
|
3220
|
+
arguments: { action: "download", outputDir: effectiveOutputDir },
|
|
3221
|
+
allowed: true,
|
|
3222
|
+
reasonCode: "credential_first_recovery_resume"
|
|
3223
|
+
}
|
|
3224
|
+
]
|
|
3225
|
+
});
|
|
3226
|
+
}
|
|
3227
|
+
for (const dir of markerRelocatedFrom.filter(
|
|
3228
|
+
(candidate) => !credentialRelocatedFrom.includes(candidate)
|
|
3229
|
+
)) {
|
|
3230
|
+
deleteProjectMarker(dir);
|
|
2740
3231
|
}
|
|
2741
3232
|
if (!existing) {
|
|
2742
3233
|
const barrier = freeSiteCreationBarrier(ctx.apiBaseUrl);
|
|
@@ -2847,7 +3338,10 @@ ${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
|
|
|
2847
3338
|
}
|
|
2848
3339
|
const { uploaded, finalized } = update;
|
|
2849
3340
|
writeSiteFile(ctx.projectDir, { ...existing, url: finalized.url });
|
|
2850
|
-
|
|
3341
|
+
for (const source of credentialRelocatedFrom) {
|
|
3342
|
+
deleteSiteFile(source);
|
|
3343
|
+
if (markerRelocatedFrom.includes(source)) deleteProjectMarker(source);
|
|
3344
|
+
}
|
|
2851
3345
|
updateProjectOutputDir(ctx.projectDir, effectiveOutputDir);
|
|
2852
3346
|
noteSiteMode(existing.siteId, finalized.mode);
|
|
2853
3347
|
return text(
|
|
@@ -2857,7 +3351,7 @@ Environment: ${environmentFor(ctx.apiBaseUrl).toUpperCase()} (${ctx.apiBaseUrl})
|
|
|
2857
3351
|
Project directory: ${ctx.projectDir}
|
|
2858
3352
|
Files uploaded: ${uploaded} (${finalized.totalBytes} bytes)
|
|
2859
3353
|
` + (finalized.expiresAt ? `Validity refreshed \u2014 expires at: ${finalized.expiresAt}
|
|
2860
|
-
` : "") + (credentialRelocatedFrom ? `Credential binding relocated from ${credentialRelocatedFrom}
|
|
3354
|
+
` : "") + (credentialRelocatedFrom.length > 0 ? `Credential binding relocated from ${credentialRelocatedFrom.join(", ")} to ${ctx.projectDir}/.sakupa; the existing site was preserved.
|
|
2861
3355
|
` : "") + (finalized.mode === "free" ? `
|
|
2862
3356
|
Reminder: free sites stay live for ${FREE_SITE_TTL_HOURS} hours after the last deploy or refresh call. Subscribing (subscribe) makes the site permanent.
|
|
2863
3357
|
` : "\nThis site is subscribed and permanent \u2014 no expiry.\n") + (finalized.warnings.length > 0 ? `
|
|
@@ -2873,7 +3367,7 @@ ${JSON.stringify(finalized.warnings, null, 2)}` : ""),
|
|
|
2873
3367
|
filesUploaded: uploaded,
|
|
2874
3368
|
totalBytes: finalized.totalBytes,
|
|
2875
3369
|
warnings: finalized.warnings,
|
|
2876
|
-
...credentialRelocatedFrom ? { credentialRelocatedFrom } : {}
|
|
3370
|
+
...credentialRelocatedFrom.length > 0 ? { credentialRelocatedFrom } : {}
|
|
2877
3371
|
}
|
|
2878
3372
|
);
|
|
2879
3373
|
} catch (e) {
|
|
@@ -2891,7 +3385,7 @@ ${JSON.stringify(finalized.warnings, null, 2)}` : ""),
|
|
|
2891
3385
|
},
|
|
2892
3386
|
async () => {
|
|
2893
3387
|
try {
|
|
2894
|
-
const ctx = withProjectDir(baseCtx);
|
|
3388
|
+
const ctx = await withProjectDir(baseCtx);
|
|
2895
3389
|
const site = requireSiteFile(ctx);
|
|
2896
3390
|
const res = await ctx.client.refreshSite(site.siteId, site.credential);
|
|
2897
3391
|
return text(
|
|
@@ -2915,7 +3409,7 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
|
|
|
2915
3409
|
},
|
|
2916
3410
|
async () => {
|
|
2917
3411
|
try {
|
|
2918
|
-
const ctx = withProjectDir(baseCtx);
|
|
3412
|
+
const ctx = await withProjectDir(baseCtx);
|
|
2919
3413
|
const site = requireSiteFile(ctx);
|
|
2920
3414
|
const res = await ctx.client.getSiteStatus(site.siteId, site.credential);
|
|
2921
3415
|
noteSiteMode(res.siteId, res.mode);
|
|
@@ -2950,13 +3444,13 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
|
|
|
2950
3444
|
},
|
|
2951
3445
|
async (args) => {
|
|
2952
3446
|
try {
|
|
2953
|
-
const ctx = withProjectDir(baseCtx);
|
|
3447
|
+
const ctx = await withProjectDir(baseCtx);
|
|
2954
3448
|
const site = requireSiteFile(ctx);
|
|
2955
3449
|
const res = await ctx.client.createPlanCheckout(
|
|
2956
3450
|
{
|
|
2957
3451
|
siteId: site.siteId,
|
|
2958
3452
|
plan: args.plan,
|
|
2959
|
-
idempotencyKey:
|
|
3453
|
+
idempotencyKey: randomUUID3()
|
|
2960
3454
|
},
|
|
2961
3455
|
site.credential
|
|
2962
3456
|
);
|
|
@@ -2998,7 +3492,7 @@ Once payment confirms, the site becomes permanent on its current URL. Binding a
|
|
|
2998
3492
|
},
|
|
2999
3493
|
async (args) => {
|
|
3000
3494
|
try {
|
|
3001
|
-
const ctx = withProjectDir(baseCtx);
|
|
3495
|
+
const ctx = await withProjectDir(baseCtx);
|
|
3002
3496
|
const site = requireSiteFile(ctx);
|
|
3003
3497
|
if (args.action === "status") {
|
|
3004
3498
|
const res2 = args.verificationId ? await ctx.client.checkVerification(args.verificationId, site.credential) : await ctx.client.checkVerification("latest", site.credential, site.siteId);
|
|
@@ -3107,7 +3601,7 @@ When the user says the TXT is set, run bind "status". It verifies ownership and
|
|
|
3107
3601
|
},
|
|
3108
3602
|
async () => {
|
|
3109
3603
|
try {
|
|
3110
|
-
const ctx = withProjectDir(baseCtx);
|
|
3604
|
+
const ctx = await withProjectDir(baseCtx);
|
|
3111
3605
|
const site = requireSiteFile(ctx);
|
|
3112
3606
|
const res = await ctx.client.getBillingStatus(site.siteId, site.credential);
|
|
3113
3607
|
noteSiteMode(res.siteId, res.mode);
|
|
@@ -3151,7 +3645,7 @@ Full status:`, res);
|
|
|
3151
3645
|
async (args) => {
|
|
3152
3646
|
try {
|
|
3153
3647
|
if (args.scope === "site") {
|
|
3154
|
-
const ctx = withProjectDir(baseCtx);
|
|
3648
|
+
const ctx = await withProjectDir(baseCtx);
|
|
3155
3649
|
const site = requireSiteFile(ctx);
|
|
3156
3650
|
const res2 = await ctx.client.createBillingPortal(site.siteId, site.credential);
|
|
3157
3651
|
return structuredToolResult({
|
|
@@ -3212,7 +3706,7 @@ Full status:`, res);
|
|
|
3212
3706
|
},
|
|
3213
3707
|
async (args) => {
|
|
3214
3708
|
try {
|
|
3215
|
-
const ctx = withProjectDir(baseCtx);
|
|
3709
|
+
const ctx = await withProjectDir(baseCtx);
|
|
3216
3710
|
if ((args.action === "complete" || args.action === "download") && args.outputDir === void 0) {
|
|
3217
3711
|
throw new LocalGuidanceError(
|
|
3218
3712
|
"invalid_request",
|
|
@@ -3521,7 +4015,7 @@ Files: ${extracted.fileCount}; bytes: ${extracted.totalBytes}
|
|
|
3521
4015
|
},
|
|
3522
4016
|
async (args) => {
|
|
3523
4017
|
try {
|
|
3524
|
-
const ctx = withProjectDir(baseCtx);
|
|
4018
|
+
const ctx = await withProjectDir(baseCtx);
|
|
3525
4019
|
const site = requireSiteFile(ctx);
|
|
3526
4020
|
const res = await ctx.client.createTicket(site.credential, {
|
|
3527
4021
|
siteId: site.siteId,
|
|
@@ -3543,11 +4037,12 @@ Files: ${extracted.fileCount}; bytes: ${extracted.totalBytes}
|
|
|
3543
4037
|
server.registerTool(
|
|
3544
4038
|
"report",
|
|
3545
4039
|
{
|
|
3546
|
-
description: "
|
|
4040
|
+
description: "LAST RESORT after help explicitly returns reportRecommended:true. Prepare and submit a sanitized product bug report using helpAuthorization from that diagnosis. Only whitelisted structured diagnostics are sent (tool name, error code/message, site id, bound domain, deployment id, timestamps, client/MCP version, request id) \u2014 NEVER file contents, source code, secrets, .env values or credentials. Without confirmSubmit: true the exact payload is shown for user review and nothing is submitted.",
|
|
3547
4041
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
3548
4042
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
3549
4043
|
inputSchema: {
|
|
3550
4044
|
toolName: z2.string().describe('The Sakupa tool that failed, e.g. "deploy".'),
|
|
4045
|
+
helpAuthorization: z2.string().describe("Short-lived authorization returned only by help when report is recommended."),
|
|
3551
4046
|
errorCode: z2.string().optional(),
|
|
3552
4047
|
errorMessage: z2.string().optional().describe("Sanitized error message (no secrets)."),
|
|
3553
4048
|
requestId: z2.string().optional(),
|
|
@@ -3565,8 +4060,9 @@ Files: ${extracted.fileCount}; bytes: ${extracted.totalBytes}
|
|
|
3565
4060
|
},
|
|
3566
4061
|
async (args) => {
|
|
3567
4062
|
try {
|
|
3568
|
-
|
|
3569
|
-
const
|
|
4063
|
+
requireReportAuthorization(baseCtx, args.helpAuthorization, args.toolName);
|
|
4064
|
+
const ctx = await optionalProjectContext(baseCtx);
|
|
4065
|
+
const siteState = ctx ? loadSiteFile(ctx.projectDir) : { kind: "absent" };
|
|
3570
4066
|
const site = siteState.kind === "ok" ? siteState.file : null;
|
|
3571
4067
|
const diagnostics = {
|
|
3572
4068
|
toolName: args.toolName,
|
|
@@ -3597,7 +4093,7 @@ Files: ${extracted.fileCount}; bytes: ${extracted.totalBytes}
|
|
|
3597
4093
|
"preview"
|
|
3598
4094
|
);
|
|
3599
4095
|
}
|
|
3600
|
-
const res = await
|
|
4096
|
+
const res = await baseCtx.client.reportBug(payload, site?.credential);
|
|
3601
4097
|
return text(
|
|
3602
4098
|
"bug_report_submitted",
|
|
3603
4099
|
`Bug report submitted. Ticket: ${res.ticketId}
|
|
@@ -3650,7 +4146,7 @@ function registerBillingTools(server, baseCtx) {
|
|
|
3650
4146
|
},
|
|
3651
4147
|
async (args) => {
|
|
3652
4148
|
try {
|
|
3653
|
-
const ctx = withProjectDir(baseCtx);
|
|
4149
|
+
const ctx = await withProjectDir(baseCtx);
|
|
3654
4150
|
const site = requireSiteFile(ctx);
|
|
3655
4151
|
const result = await ctx.client.changeSubscriptionPlan(site.credential, {
|
|
3656
4152
|
siteId: site.siteId,
|
|
@@ -3679,7 +4175,7 @@ function registerBillingTools(server, baseCtx) {
|
|
|
3679
4175
|
}
|
|
3680
4176
|
|
|
3681
4177
|
// src/tools/lifecycle.ts
|
|
3682
|
-
import { randomUUID as
|
|
4178
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
3683
4179
|
import { z as z4 } from "zod";
|
|
3684
4180
|
var deleteConfirmation = z4.object({
|
|
3685
4181
|
siteId: z4.string().min(1),
|
|
@@ -3711,9 +4207,9 @@ function registerLifecycleTools(server, baseCtx) {
|
|
|
3711
4207
|
},
|
|
3712
4208
|
async (args) => {
|
|
3713
4209
|
try {
|
|
3714
|
-
const ctx = withProjectDir(baseCtx);
|
|
4210
|
+
const ctx = await withProjectDir(baseCtx);
|
|
3715
4211
|
const site = requireSiteFile(ctx);
|
|
3716
|
-
const operationId = args.operationId ??
|
|
4212
|
+
const operationId = args.operationId ?? randomUUID4();
|
|
3717
4213
|
if (args.action === "preview") {
|
|
3718
4214
|
const preview = await ctx.client.previewDeleteSite(site.siteId, site.credential, {
|
|
3719
4215
|
operationId
|
|
@@ -3794,6 +4290,314 @@ function registerLifecycleTools(server, baseCtx) {
|
|
|
3794
4290
|
);
|
|
3795
4291
|
}
|
|
3796
4292
|
|
|
4293
|
+
// src/tools/help.ts
|
|
4294
|
+
import { join as join7 } from "node:path";
|
|
4295
|
+
import { z as z5 } from "zod";
|
|
4296
|
+
var TOOL_TOPICS = [
|
|
4297
|
+
"init",
|
|
4298
|
+
"analyze",
|
|
4299
|
+
"deploy",
|
|
4300
|
+
"refresh",
|
|
4301
|
+
"status",
|
|
4302
|
+
"plans",
|
|
4303
|
+
"subscribe",
|
|
4304
|
+
"bind",
|
|
4305
|
+
"billing",
|
|
4306
|
+
"portal",
|
|
4307
|
+
"recover",
|
|
4308
|
+
"change",
|
|
4309
|
+
"delete",
|
|
4310
|
+
"support",
|
|
4311
|
+
"report",
|
|
4312
|
+
"help"
|
|
4313
|
+
];
|
|
4314
|
+
var HELP_TOPICS = ["diagnose", "overview", ...TOOL_TOPICS];
|
|
4315
|
+
var TOOL_MANUALS = {
|
|
4316
|
+
init: {
|
|
4317
|
+
purpose: "Initialize the active IDE workspace as one Sakupa project.",
|
|
4318
|
+
sideEffects: "Creates only .sakupa/project.json locally; no API call, site or charge.",
|
|
4319
|
+
preconditions: "Exactly one usable MCP workspace Root. Clients without Roots use CLI init.",
|
|
4320
|
+
parameters: "No parameters and no path argument.",
|
|
4321
|
+
warnings: [
|
|
4322
|
+
"Never initialize the IDE installation directory.",
|
|
4323
|
+
"Existing site.json and recovery.json are preserved."
|
|
4324
|
+
],
|
|
4325
|
+
nextStep: "Call analyze, then deploy with the exact relative outputDir."
|
|
4326
|
+
},
|
|
4327
|
+
analyze: {
|
|
4328
|
+
purpose: "Inspect a project or explicit output directory for safe static deployment.",
|
|
4329
|
+
sideEffects: "Read-only local file inspection; no API call.",
|
|
4330
|
+
preconditions: "An initialized, unambiguous project binding.",
|
|
4331
|
+
parameters: "Optional outputDir relative to the bound project Root.",
|
|
4332
|
+
warnings: ["Build locally first.", "Never publish source, secrets, server code or media."],
|
|
4333
|
+
nextStep: "Fix reported blockers, then call deploy with the exact outputDir."
|
|
4334
|
+
},
|
|
4335
|
+
deploy: {
|
|
4336
|
+
purpose: "Create or update the bound Sakupa static site.",
|
|
4337
|
+
sideEffects: "Reads local output, uploads files and may create a public free site.",
|
|
4338
|
+
preconditions: "Initialized project, exact outputDir and first-publication confirmation.",
|
|
4339
|
+
parameters: "outputDir is required and relative to the project Root.",
|
|
4340
|
+
warnings: [
|
|
4341
|
+
".sakupa must remain at the project Root and is never uploaded.",
|
|
4342
|
+
"A changed outputDir requires explicit confirmation."
|
|
4343
|
+
],
|
|
4344
|
+
nextStep: "Call status to verify the cloud result."
|
|
4345
|
+
},
|
|
4346
|
+
refresh: {
|
|
4347
|
+
purpose: "Extend a free site lifetime without uploading content.",
|
|
4348
|
+
sideEffects: "Updates the site expiry in Sakupa.",
|
|
4349
|
+
preconditions: "A valid local site credential.",
|
|
4350
|
+
parameters: "No parameters.",
|
|
4351
|
+
warnings: ["Subscribed sites are permanent and do not need refresh."],
|
|
4352
|
+
nextStep: "Call status to verify the new expiry."
|
|
4353
|
+
},
|
|
4354
|
+
status: {
|
|
4355
|
+
purpose: "Read the bound site, deployment, domain and serving state.",
|
|
4356
|
+
sideEffects: "Read-only API request.",
|
|
4357
|
+
preconditions: "A valid local site credential.",
|
|
4358
|
+
parameters: "No parameters.",
|
|
4359
|
+
warnings: ["Billing truth comes from billing, not inferred status text."],
|
|
4360
|
+
nextStep: "Follow only the returned real tool names."
|
|
4361
|
+
},
|
|
4362
|
+
plans: {
|
|
4363
|
+
purpose: "Read the authoritative hosting plan catalog and rules.",
|
|
4364
|
+
sideEffects: "Read-only public API request.",
|
|
4365
|
+
preconditions: "None; project initialization is not required.",
|
|
4366
|
+
parameters: "No parameters.",
|
|
4367
|
+
warnings: ["JPY prices and cloud plan order are authoritative."],
|
|
4368
|
+
nextStep: "Use subscribe for first payment or change for an existing subscription."
|
|
4369
|
+
},
|
|
4370
|
+
subscribe: {
|
|
4371
|
+
purpose: "Create Stripe Checkout for the first subscription.",
|
|
4372
|
+
sideEffects: "Creates a short-lived Stripe Checkout session; payment happens only on Stripe.",
|
|
4373
|
+
preconditions: "A free bound site with a valid credential.",
|
|
4374
|
+
parameters: "The selected plan from plans.",
|
|
4375
|
+
warnings: ["Creating a link does not subscribe or charge the user."],
|
|
4376
|
+
nextStep: "Show the complete URL, then query billing after Stripe confirmation."
|
|
4377
|
+
},
|
|
4378
|
+
bind: {
|
|
4379
|
+
purpose: "Start, check or inspect custom-domain binding.",
|
|
4380
|
+
sideEffects: "May create DNS verification and hostname provisioning state.",
|
|
4381
|
+
preconditions: "A subscribed site and DNS control.",
|
|
4382
|
+
parameters: "Action plus hostname or verificationId as returned by the prior step.",
|
|
4383
|
+
warnings: ["www is mandatory; the apex is optional.", "Copy DNS values verbatim."],
|
|
4384
|
+
nextStep: "Follow the returned DNS checklist and call bind status/check."
|
|
4385
|
+
},
|
|
4386
|
+
billing: {
|
|
4387
|
+
purpose: "Read the single authoritative subscription and usage snapshot.",
|
|
4388
|
+
sideEffects: "Read-only API reconciliation.",
|
|
4389
|
+
preconditions: "A valid bound site.",
|
|
4390
|
+
parameters: "No parameters.",
|
|
4391
|
+
warnings: ["Never infer renewal state from user wording or an old link."],
|
|
4392
|
+
nextStep: "Use change or portal only when the user wants billing management."
|
|
4393
|
+
},
|
|
4394
|
+
portal: {
|
|
4395
|
+
purpose: "Open Stripe billing/customer management or public recovery login.",
|
|
4396
|
+
sideEffects: "Creates or returns a Stripe-hosted management URL.",
|
|
4397
|
+
preconditions: "Site scope needs a credential; public recovery does not.",
|
|
4398
|
+
parameters: "Use the supported scope.",
|
|
4399
|
+
warnings: ["Opening a link does not change subscription state."],
|
|
4400
|
+
nextStep: "Query billing after the user confirms an operation in Stripe."
|
|
4401
|
+
},
|
|
4402
|
+
recover: {
|
|
4403
|
+
purpose: "Recover a paid custom-domain site credential and download its content.",
|
|
4404
|
+
sideEffects: "Creates DNS verification state and writes local credential/archive files.",
|
|
4405
|
+
preconditions: "DNS control of a domain bound to an active paid site.",
|
|
4406
|
+
parameters: "Use the returned action and verificationId; outputDir is relative to Root.",
|
|
4407
|
+
warnings: [
|
|
4408
|
+
"After site.json exists, resume download and never repeat DNS verification.",
|
|
4409
|
+
"Credential is saved before archive creation/download."
|
|
4410
|
+
],
|
|
4411
|
+
nextStep: "Call recover download when local credentials already exist."
|
|
4412
|
+
},
|
|
4413
|
+
change: {
|
|
4414
|
+
purpose: "Open the unified Stripe subscription-management page.",
|
|
4415
|
+
sideEffects: "Creates a short-lived Portal session and audit record only.",
|
|
4416
|
+
preconditions: "An active subscription.",
|
|
4417
|
+
parameters: "No plan direction or target is accepted from conversational intent.",
|
|
4418
|
+
warnings: ["Only Stripe confirmation changes the subscription."],
|
|
4419
|
+
nextStep: "Call billing after the user finishes on Stripe."
|
|
4420
|
+
},
|
|
4421
|
+
delete: {
|
|
4422
|
+
purpose: "Preview and permanently delete a free site.",
|
|
4423
|
+
sideEffects: "Confirm permanently removes content, URL and local site credential.",
|
|
4424
|
+
preconditions: "Paid subscriptions must fully end and return the site to free mode first.",
|
|
4425
|
+
parameters: "Preview first; confirm with the exact returned confirmArguments.",
|
|
4426
|
+
warnings: [
|
|
4427
|
+
"Never guess timestamps or confirmation fields from status.",
|
|
4428
|
+
"Deletion is irreversible and does not secretly cancel payment."
|
|
4429
|
+
],
|
|
4430
|
+
nextStep: "Use the preview userAction.resumeWith arguments verbatim."
|
|
4431
|
+
},
|
|
4432
|
+
support: {
|
|
4433
|
+
purpose: "Create a customer-service ticket for billing, refund, payment or domain assistance.",
|
|
4434
|
+
sideEffects: "Submits a support ticket.",
|
|
4435
|
+
preconditions: "A bound subscribed site and user-provided issue description.",
|
|
4436
|
+
parameters: "Category, subject, sanitized description and optional contact email.",
|
|
4437
|
+
warnings: ["Never include credentials, source, card data or secrets."],
|
|
4438
|
+
nextStep: "Wait for support follow-up."
|
|
4439
|
+
},
|
|
4440
|
+
report: {
|
|
4441
|
+
purpose: "Last-resort product bug report after help recommends it.",
|
|
4442
|
+
sideEffects: "Preview is local; confirmSubmit sends a sanitized diagnostic report.",
|
|
4443
|
+
preconditions: "Call help first and show the exact report preview to the user.",
|
|
4444
|
+
parameters: "Failed tool, helpAuthorization, sanitized diagnostics and explicit confirmSubmit.",
|
|
4445
|
+
warnings: [
|
|
4446
|
+
"Never report ordinary setup errors help can solve.",
|
|
4447
|
+
"Submission requires approval."
|
|
4448
|
+
],
|
|
4449
|
+
nextStep: "Submit only after the user reviews the preview."
|
|
4450
|
+
},
|
|
4451
|
+
help: {
|
|
4452
|
+
purpose: "Diagnose the current MCP/project state or explain any Sakupa tool.",
|
|
4453
|
+
sideEffects: "Read-only local diagnosis; no API call or file write.",
|
|
4454
|
+
preconditions: "None; works even when project binding is broken.",
|
|
4455
|
+
parameters: "topic defaults to diagnose; use overview or a tool name for its manual.",
|
|
4456
|
+
warnings: ["Use help before repeating failed calls or suggesting report."],
|
|
4457
|
+
nextStep: "Follow the returned diagnosis and nextActions."
|
|
4458
|
+
}
|
|
4459
|
+
};
|
|
4460
|
+
function registerHelpTools(server, baseCtx) {
|
|
4461
|
+
server.registerTool(
|
|
4462
|
+
"init",
|
|
4463
|
+
{
|
|
4464
|
+
description: "Initialize the active MCP workspace Root as a Sakupa project. Takes no path argument, creates only .sakupa/project.json at that exact Root, preserves site/recovery state, makes no API call and is idempotent. Clients without MCP Roots must use the no-argument CLI init.",
|
|
4465
|
+
inputSchema: {},
|
|
4466
|
+
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
4467
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
|
|
4468
|
+
},
|
|
4469
|
+
async () => {
|
|
4470
|
+
try {
|
|
4471
|
+
const ctx = await initializeWorkspaceProject(baseCtx);
|
|
4472
|
+
const marker = loadProjectMarker(ctx.projectDir);
|
|
4473
|
+
if (marker.kind !== "ok")
|
|
4474
|
+
throw new Error("init postcondition failed: project marker missing");
|
|
4475
|
+
const site = loadSiteFile(ctx.projectDir);
|
|
4476
|
+
const recovery = loadRecoveryFile(ctx.projectDir);
|
|
4477
|
+
const sakupaDirectory = join7(ctx.projectDir, ".sakupa");
|
|
4478
|
+
return structuredToolResult({
|
|
4479
|
+
schemaVersion: 1,
|
|
4480
|
+
outcome: "completed",
|
|
4481
|
+
resultCode: "project_initialized",
|
|
4482
|
+
summary: `Sakupa project initialized and verified at the active workspace Root: ${ctx.projectDir}. .sakupa is at ${sakupaDirectory}; no cloud site was created and no charge occurred.`,
|
|
4483
|
+
data: {
|
|
4484
|
+
projectRoot: ctx.projectDir,
|
|
4485
|
+
sakupaDirectory,
|
|
4486
|
+
projectId: marker.marker.projectId,
|
|
4487
|
+
bindingSource: ctx.bindingSource,
|
|
4488
|
+
sakupaAtProjectRoot: true,
|
|
4489
|
+
postconditionVerified: true,
|
|
4490
|
+
existingSitePreserved: site.kind !== "absent",
|
|
4491
|
+
existingRecoveryPreserved: recovery !== null,
|
|
4492
|
+
cloudApiCalled: false
|
|
4493
|
+
},
|
|
4494
|
+
nextActions: [{ tool: "analyze", allowed: true, reasonCode: "project_ready" }]
|
|
4495
|
+
});
|
|
4496
|
+
} catch (error) {
|
|
4497
|
+
return toolError(error);
|
|
4498
|
+
}
|
|
4499
|
+
}
|
|
4500
|
+
);
|
|
4501
|
+
server.registerTool(
|
|
4502
|
+
"help",
|
|
4503
|
+
{
|
|
4504
|
+
description: "FIRST troubleshooting tool for every Sakupa difficulty. With topic diagnose (default), inspect MCP Roots, cwd, binding and local state without requiring a project or calling the API. Use overview or a tool name for complete usage, side effects, parameters and warnings. Only recommend report when help explicitly returns reportRecommended:true.",
|
|
4505
|
+
inputSchema: {
|
|
4506
|
+
topic: z5.enum(HELP_TOPICS).optional().default("diagnose"),
|
|
4507
|
+
failedTool: z5.string().optional(),
|
|
4508
|
+
errorCode: z5.string().optional(),
|
|
4509
|
+
resultCode: z5.string().optional(),
|
|
4510
|
+
requestId: z5.string().optional()
|
|
4511
|
+
},
|
|
4512
|
+
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
4513
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
|
|
4514
|
+
},
|
|
4515
|
+
async (args) => {
|
|
4516
|
+
try {
|
|
4517
|
+
if (args.topic === "overview") {
|
|
4518
|
+
const catalog = Object.fromEntries(
|
|
4519
|
+
TOOL_TOPICS.map((tool) => [tool, { purpose: TOOL_MANUALS[tool].purpose }])
|
|
4520
|
+
);
|
|
4521
|
+
return structuredToolResult({
|
|
4522
|
+
schemaVersion: 1,
|
|
4523
|
+
outcome: "completed",
|
|
4524
|
+
resultCode: "help_overview",
|
|
4525
|
+
summary: 'Sakupa tool overview returned. On any failure call help with topic:"diagnose" before retrying, support or report.',
|
|
4526
|
+
data: { tools: catalog, toolOrder: TOOL_TOPICS },
|
|
4527
|
+
nextActions: []
|
|
4528
|
+
});
|
|
4529
|
+
}
|
|
4530
|
+
if (args.topic !== "diagnose") {
|
|
4531
|
+
const manual = TOOL_MANUALS[args.topic];
|
|
4532
|
+
return structuredToolResult({
|
|
4533
|
+
schemaVersion: 1,
|
|
4534
|
+
outcome: "completed",
|
|
4535
|
+
resultCode: "help_tool_manual",
|
|
4536
|
+
summary: `${args.topic}: ${manual.purpose}
|
|
4537
|
+
Side effects: ${manual.sideEffects}
|
|
4538
|
+
Preconditions: ${manual.preconditions}
|
|
4539
|
+
Parameters: ${manual.parameters}
|
|
4540
|
+
Warnings: ${manual.warnings.join(" ")}
|
|
4541
|
+
Next: ${manual.nextStep}`,
|
|
4542
|
+
data: { tool: args.topic, ...manual },
|
|
4543
|
+
nextActions: []
|
|
4544
|
+
});
|
|
4545
|
+
}
|
|
4546
|
+
const diagnosis = await diagnoseProjectBinding(baseCtx);
|
|
4547
|
+
const selected = diagnosis.selectedProjectDir;
|
|
4548
|
+
const marker = selected ? loadProjectMarker(selected) : { kind: "absent" };
|
|
4549
|
+
const site = selected ? loadSiteFile(selected) : { kind: "absent" };
|
|
4550
|
+
let recoveryState = "absent";
|
|
4551
|
+
if (selected) {
|
|
4552
|
+
try {
|
|
4553
|
+
recoveryState = loadRecoveryFile(selected) === null ? "absent" : "ok";
|
|
4554
|
+
} catch {
|
|
4555
|
+
recoveryState = "corrupted";
|
|
4556
|
+
}
|
|
4557
|
+
}
|
|
4558
|
+
const opaqueFailure = args.errorCode === "internal" || args.resultCode === "error_internal";
|
|
4559
|
+
const reportRecommended = opaqueFailure && (diagnosis.diagnosisCode === "project_bound" || diagnosis.diagnosisCode === "roots_request_failed");
|
|
4560
|
+
const helpAuthorization = reportRecommended ? issueReportAuthorization(baseCtx, args.failedTool ?? "unknown") : void 0;
|
|
4561
|
+
const nextActions = reportRecommended ? [
|
|
4562
|
+
{
|
|
4563
|
+
tool: "report",
|
|
4564
|
+
arguments: {
|
|
4565
|
+
toolName: args.failedTool ?? "unknown",
|
|
4566
|
+
helpAuthorization,
|
|
4567
|
+
...args.errorCode !== void 0 ? { errorCode: args.errorCode } : {},
|
|
4568
|
+
...args.requestId !== void 0 ? { requestId: args.requestId } : {}
|
|
4569
|
+
},
|
|
4570
|
+
allowed: true,
|
|
4571
|
+
reasonCode: "help_confirmed_last_resort"
|
|
4572
|
+
}
|
|
4573
|
+
] : diagnosis.diagnosisCode === "workspace_not_initialized" ? [{ tool: "init", allowed: true, reasonCode: "initialize_active_root" }] : [];
|
|
4574
|
+
const summary = `Help diagnosis: ${diagnosis.diagnosisCode}. ${diagnosis.guidance} MCP version: ${MCP_VERSION}. ` + (reportRecommended ? diagnosis.diagnosisCode === "project_bound" ? "Local project binding is healthy but the failure is an unclassified internal error. report is now available as the last resort; preview it before submission." : "The MCP Roots request itself failed with an unclassified internal error. report is now available as the last resort; preview it before submission." : "Do not submit report for this diagnosis; follow the guidance and retry help.");
|
|
4575
|
+
return structuredToolResult({
|
|
4576
|
+
schemaVersion: 1,
|
|
4577
|
+
outcome: diagnosis.diagnosisCode === "project_bound" ? "completed" : "blocked",
|
|
4578
|
+
resultCode: `help_${diagnosis.diagnosisCode}`,
|
|
4579
|
+
summary,
|
|
4580
|
+
data: {
|
|
4581
|
+
...diagnosis,
|
|
4582
|
+
mcpVersion: MCP_VERSION,
|
|
4583
|
+
projectMarkerState: marker.kind,
|
|
4584
|
+
siteState: site.kind,
|
|
4585
|
+
recoveryState,
|
|
4586
|
+
reportRecommended,
|
|
4587
|
+
...helpAuthorization !== void 0 ? { helpAuthorization } : {},
|
|
4588
|
+
...args.failedTool !== void 0 ? { failedTool: args.failedTool } : {},
|
|
4589
|
+
...args.errorCode !== void 0 ? { errorCode: args.errorCode } : {},
|
|
4590
|
+
...args.resultCode !== void 0 ? { failedResultCode: args.resultCode } : {}
|
|
4591
|
+
},
|
|
4592
|
+
nextActions
|
|
4593
|
+
});
|
|
4594
|
+
} catch (error) {
|
|
4595
|
+
return toolError(error);
|
|
4596
|
+
}
|
|
4597
|
+
}
|
|
4598
|
+
);
|
|
4599
|
+
}
|
|
4600
|
+
|
|
3797
4601
|
// src/transport.ts
|
|
3798
4602
|
var FetchTransport = class {
|
|
3799
4603
|
baseUrl;
|
|
@@ -3892,6 +4696,7 @@ var FetchTransport = class {
|
|
|
3892
4696
|
};
|
|
3893
4697
|
|
|
3894
4698
|
// src/server.ts
|
|
4699
|
+
import { resolve as resolve6 } from "node:path";
|
|
3895
4700
|
var instructionsFor = (hostPattern) => `Sakupa publishes AI-made static websites. AI-made pages, live in seconds.
|
|
3896
4701
|
|
|
3897
4702
|
Workflow:
|
|
@@ -3921,14 +4726,17 @@ Workflow:
|
|
|
3921
4726
|
24h site and removes paid data after Stripe sends the signed final-cancellation webhook.
|
|
3922
4727
|
Recovery writes the new local credential before downloading content. If a session stops after
|
|
3923
4728
|
.sakupa/site.json exists, NEVER repeat DNS recovery: resume with recover action "download".
|
|
3924
|
-
5.
|
|
3925
|
-
|
|
4729
|
+
5. If any Sakupa operation is difficult or fails, call help FIRST. support handles billing,
|
|
4730
|
+
payment, refund and other customer-service requests. report is the LAST resort only when
|
|
4731
|
+
help explicitly recommends a product bug report, and submission still requires user review.
|
|
3926
4732
|
|
|
3927
4733
|
Project directory contract: before the first deploy or a new recovery, initialize the intended
|
|
3928
|
-
project
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
4734
|
+
project by calling init with NO path argument. init uses the IDE's exact MCP Root and creates the
|
|
4735
|
+
non-secret .sakupa/project.json directly there. The CLI command
|
|
4736
|
+
"npx -y @sakupa/mcp@latest init" remains the safe fallback for clients without MCP Roots and
|
|
4737
|
+
also accepts NO path argument. ONE MCP process = ONE Roots-first locked project = ONE site.
|
|
4738
|
+
Site tools do not accept projectDir and cannot select another root; help, plans, report preview
|
|
4739
|
+
and public_recovery portal remain project-independent.
|
|
3932
4740
|
Sakupa stores .sakupa/site.json and recovery state only in the locked directory; it never uses
|
|
3933
4741
|
package.json, .git, framework names or output-directory names to guess. For deploy, ALWAYS pass
|
|
3934
4742
|
outputDir separately as the exact path RELATIVE to the locked directory (use "." when publishing
|
|
@@ -3936,14 +4744,17 @@ the root); outputDir is
|
|
|
3936
4744
|
required and may have ANY name, so inspect the current project. If it differs from the last
|
|
3937
4745
|
successful publish directory, show the old and new paths and obtain explicit confirmation
|
|
3938
4746
|
before retrying with outputDirChangeConfirmed: true.
|
|
4747
|
+
After init, require sakupaAtProjectRoot=true. Before deploy, Sakupa checks every directory segment
|
|
4748
|
+
between the project Root and outputDir for a misplaced .sakupa and safely relocates only validated,
|
|
4749
|
+
non-conflicting state; never copy, delete or overwrite site.json by shell command.
|
|
3939
4750
|
After every deploy, TELL the user which environment it went to (deploy results carry an
|
|
3940
4751
|
Explicit Environment line: TEST vs PRODUCTION). analyze, deploy, status,
|
|
3941
4752
|
refresh and delete echo
|
|
3942
|
-
the
|
|
4753
|
+
the Roots-first locked directory they acted on.
|
|
3943
4754
|
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
4755
|
+
On any difficulty, call help before retrying or escalating. Only offer report when help returns
|
|
4756
|
+
reportRecommended:true; attach your own factual account via agentContext and show the exact
|
|
4757
|
+
sanitized preview before asking the user to confirm submission.
|
|
3947
4758
|
|
|
3948
4759
|
Present every step as Sakupa's own: never attribute DNS, certificates or hosting to
|
|
3949
4760
|
underlying infrastructure vendors in front of the user. Relay DNS record values and full
|
|
@@ -3977,14 +4788,32 @@ function createSakupaMcpServer(opts) {
|
|
|
3977
4788
|
{ name: "sakupa", version: MCP_VERSION },
|
|
3978
4789
|
{ instructions: instructionsFor(previewHostPatternFor(opts.apiBaseUrl)) }
|
|
3979
4790
|
);
|
|
4791
|
+
const processCwd = resolve6(opts.projectDir ?? process.cwd());
|
|
4792
|
+
const rootsProvider = opts.rootsProvider ?? (async () => {
|
|
4793
|
+
const capabilities = server.server.getClientCapabilities();
|
|
4794
|
+
if (!capabilities?.roots) return { supported: false, roots: [] };
|
|
4795
|
+
try {
|
|
4796
|
+
const response = await server.server.listRoots();
|
|
4797
|
+
return { supported: true, roots: response.roots };
|
|
4798
|
+
} catch (error) {
|
|
4799
|
+
return {
|
|
4800
|
+
supported: true,
|
|
4801
|
+
roots: [],
|
|
4802
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4803
|
+
};
|
|
4804
|
+
}
|
|
4805
|
+
});
|
|
3980
4806
|
const ctx = {
|
|
3981
4807
|
client,
|
|
3982
4808
|
apiBaseUrl: opts.apiBaseUrl,
|
|
3983
|
-
projectDir:
|
|
4809
|
+
projectDir: processCwd,
|
|
4810
|
+
rootsProvider,
|
|
4811
|
+
projectBinding: new ProjectBindingResolver(processCwd, rootsProvider)
|
|
3984
4812
|
};
|
|
3985
4813
|
registerTools(server, ctx);
|
|
3986
4814
|
registerBillingTools(server, ctx);
|
|
3987
4815
|
registerLifecycleTools(server, ctx);
|
|
4816
|
+
registerHelpTools(server, ctx);
|
|
3988
4817
|
return server;
|
|
3989
4818
|
}
|
|
3990
4819
|
|
|
@@ -4007,7 +4836,7 @@ async function main() {
|
|
|
4007
4836
|
const transport = new StdioServerTransport();
|
|
4008
4837
|
await server.connect(transport);
|
|
4009
4838
|
console.error(
|
|
4010
|
-
`[sakupa-mcp] v${MCP_VERSION} connected (api: ${config.apiBaseUrl};
|
|
4839
|
+
`[sakupa-mcp] v${MCP_VERSION} connected (api: ${config.apiBaseUrl}; process cwd fallback: ${process.cwd()}; MCP Roots preferred)`
|
|
4011
4840
|
);
|
|
4012
4841
|
}
|
|
4013
4842
|
main().catch((err2) => {
|