@sakupa/mcp 0.7.44 → 0.7.46
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 +540 -114
- package/dist/index.js +538 -112
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -380,7 +380,7 @@ var FORBIDDEN_PATH_SEGMENTS = [
|
|
|
380
380
|
var ALLOWED_HIDDEN_PATHS = [".well-known/"];
|
|
381
381
|
|
|
382
382
|
// ../core/dist/domain/version.js
|
|
383
|
-
var SAKUPA_MCP_VERSION = "0.7.
|
|
383
|
+
var SAKUPA_MCP_VERSION = "0.7.46";
|
|
384
384
|
|
|
385
385
|
// ../core/dist/domain/errors.js
|
|
386
386
|
var HTTP_STATUS = {
|
|
@@ -810,8 +810,8 @@ var HttpApiClient = class {
|
|
|
810
810
|
body: body?.slice(0, 200)
|
|
811
811
|
});
|
|
812
812
|
}
|
|
813
|
-
async registerDevice() {
|
|
814
|
-
return this.call("POST", "/v1/devices");
|
|
813
|
+
async registerDevice(req) {
|
|
814
|
+
return this.call("POST", "/v1/devices", { body: req });
|
|
815
815
|
}
|
|
816
816
|
async listDeviceFreeSites(deviceId, credential) {
|
|
817
817
|
return this.call("GET", "/v1/devices/free-sites", {
|
|
@@ -984,7 +984,7 @@ var HttpApiClient = class {
|
|
|
984
984
|
};
|
|
985
985
|
|
|
986
986
|
// src/tools/definitions.ts
|
|
987
|
-
import { randomUUID as
|
|
987
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
988
988
|
import { promises as fs2 } from "node:fs";
|
|
989
989
|
import { join as join9, relative as relative3, resolve as resolve5, sep as sep4 } from "node:path";
|
|
990
990
|
import { z as z2 } from "zod";
|
|
@@ -2318,6 +2318,7 @@ import {
|
|
|
2318
2318
|
unlinkSync as unlinkSync2,
|
|
2319
2319
|
writeFileSync as writeFileSync4
|
|
2320
2320
|
} from "node:fs";
|
|
2321
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
2321
2322
|
import { homedir as homedir3 } from "node:os";
|
|
2322
2323
|
import { dirname as dirname4, join as join6 } from "node:path";
|
|
2323
2324
|
var DEVICE_LOCK_STALE_MS = 3e4;
|
|
@@ -2327,10 +2328,22 @@ function deviceRegistryPath() {
|
|
|
2327
2328
|
return join6(base, ".sakupa", "devices.json");
|
|
2328
2329
|
}
|
|
2329
2330
|
var deviceLockPath = () => join6(dirname4(deviceRegistryPath()), "devices.lock");
|
|
2330
|
-
function
|
|
2331
|
+
function lockTokenAt(path) {
|
|
2332
|
+
try {
|
|
2333
|
+
const parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
2334
|
+
return typeof parsed.token === "string" ? parsed.token : null;
|
|
2335
|
+
} catch {
|
|
2336
|
+
return null;
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
function ownsDeviceLock(lock) {
|
|
2340
|
+
return lockTokenAt(deviceLockPath()) === lock.token;
|
|
2341
|
+
}
|
|
2342
|
+
function releaseDeviceLock(lock) {
|
|
2331
2343
|
try {
|
|
2332
|
-
closeSync(
|
|
2344
|
+
closeSync(lock.fd);
|
|
2333
2345
|
} finally {
|
|
2346
|
+
if (!ownsDeviceLock(lock)) return;
|
|
2334
2347
|
try {
|
|
2335
2348
|
unlinkSync2(deviceLockPath());
|
|
2336
2349
|
} catch {
|
|
@@ -2345,9 +2358,13 @@ async function acquireDeviceLock(apiBaseUrl) {
|
|
|
2345
2358
|
const existing = loadDeviceBinding(apiBaseUrl);
|
|
2346
2359
|
if (existing) return existing;
|
|
2347
2360
|
try {
|
|
2361
|
+
const token = randomUUID3();
|
|
2348
2362
|
const fd2 = openSync(path, "wx", 384);
|
|
2349
|
-
writeFileSync4(
|
|
2350
|
-
|
|
2363
|
+
writeFileSync4(
|
|
2364
|
+
fd2,
|
|
2365
|
+
JSON.stringify({ token, pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString() })
|
|
2366
|
+
);
|
|
2367
|
+
return { fd: fd2, token };
|
|
2351
2368
|
} catch {
|
|
2352
2369
|
try {
|
|
2353
2370
|
if (Date.now() - statSync2(path).mtimeMs >= DEVICE_LOCK_STALE_MS) {
|
|
@@ -2368,13 +2385,16 @@ async function acquireDeviceLock(apiBaseUrl) {
|
|
|
2368
2385
|
}
|
|
2369
2386
|
function readRegistry() {
|
|
2370
2387
|
const path = deviceRegistryPath();
|
|
2371
|
-
if (!existsSync5(path))
|
|
2388
|
+
if (!existsSync5(path)) {
|
|
2389
|
+
return { schemaVersion: 1, environments: {}, pendingRegistrations: {} };
|
|
2390
|
+
}
|
|
2372
2391
|
try {
|
|
2373
2392
|
const parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
2374
2393
|
if (parsed.schemaVersion !== 1 || !parsed.environments || typeof parsed.environments !== "object") {
|
|
2375
2394
|
throw new Error("unsupported device registry schema");
|
|
2376
2395
|
}
|
|
2377
|
-
|
|
2396
|
+
const pendingRegistrations = "pendingRegistrations" in parsed && parsed.pendingRegistrations && typeof parsed.pendingRegistrations === "object" ? parsed.pendingRegistrations : {};
|
|
2397
|
+
return { schemaVersion: 1, environments: parsed.environments, pendingRegistrations };
|
|
2378
2398
|
} catch (error) {
|
|
2379
2399
|
throw new Error(
|
|
2380
2400
|
`Sakupa device registry is unreadable at ${path}: ${error instanceof Error ? error.message : String(error)}. Run help; do not search old project directories.`
|
|
@@ -2403,19 +2423,38 @@ async function ensureDeviceBinding(client, apiBaseUrl) {
|
|
|
2403
2423
|
const existing = loadDeviceBinding(apiBaseUrl);
|
|
2404
2424
|
if (existing) return existing;
|
|
2405
2425
|
const lock = await acquireDeviceLock(apiBaseUrl);
|
|
2406
|
-
if (
|
|
2426
|
+
if (!("fd" in lock)) return lock;
|
|
2407
2427
|
try {
|
|
2408
2428
|
const afterLock = loadDeviceBinding(apiBaseUrl);
|
|
2409
2429
|
if (afterLock) return afterLock;
|
|
2410
|
-
|
|
2411
|
-
|
|
2430
|
+
let registry = readRegistry();
|
|
2431
|
+
let pending = registry.pendingRegistrations[apiBaseUrl];
|
|
2432
|
+
if (!pending) {
|
|
2433
|
+
pending = {
|
|
2434
|
+
operationId: randomUUID3(),
|
|
2435
|
+
deviceId: randomUUID3(),
|
|
2436
|
+
credential: generateCredential(),
|
|
2437
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2438
|
+
};
|
|
2439
|
+
registry.pendingRegistrations[apiBaseUrl] = pending;
|
|
2440
|
+
writeRegistry(registry);
|
|
2441
|
+
}
|
|
2442
|
+
const created = await client.registerDevice({
|
|
2443
|
+
operationId: pending.operationId,
|
|
2444
|
+
deviceId: pending.deviceId,
|
|
2445
|
+
credential: pending.credential
|
|
2446
|
+
});
|
|
2412
2447
|
const binding = {
|
|
2413
2448
|
deviceId: created.deviceId,
|
|
2414
2449
|
credential: created.credential,
|
|
2415
2450
|
createdAt: created.createdAt
|
|
2416
2451
|
};
|
|
2417
|
-
|
|
2418
|
-
|
|
2452
|
+
if (ownsDeviceLock(lock)) {
|
|
2453
|
+
registry = readRegistry();
|
|
2454
|
+
registry.environments[apiBaseUrl] = binding;
|
|
2455
|
+
delete registry.pendingRegistrations[apiBaseUrl];
|
|
2456
|
+
writeRegistry(registry);
|
|
2457
|
+
}
|
|
2419
2458
|
return binding;
|
|
2420
2459
|
} finally {
|
|
2421
2460
|
releaseDeviceLock(lock);
|
|
@@ -2733,7 +2772,7 @@ import {
|
|
|
2733
2772
|
rmSync as rmSync2,
|
|
2734
2773
|
writeFileSync as writeFileSync6
|
|
2735
2774
|
} from "node:fs";
|
|
2736
|
-
import { randomUUID as
|
|
2775
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
2737
2776
|
import { join as join8 } from "node:path";
|
|
2738
2777
|
var ROTATION_FILE = "rotation.json";
|
|
2739
2778
|
function credentialRotationPath(projectDir) {
|
|
@@ -2794,7 +2833,7 @@ function writeCredentialRotation(projectDir, file) {
|
|
|
2794
2833
|
const directory = join8(projectDir, ".sakupa");
|
|
2795
2834
|
mkdirSync6(directory, { recursive: true, mode: 448 });
|
|
2796
2835
|
const target = credentialRotationPath(projectDir);
|
|
2797
|
-
const temporary = join8(directory, `.rotation-${
|
|
2836
|
+
const temporary = join8(directory, `.rotation-${randomUUID4()}.tmp`);
|
|
2798
2837
|
writeFileSync6(temporary, `${JSON.stringify(file, null, 2)}
|
|
2799
2838
|
`, {
|
|
2800
2839
|
encoding: "utf8",
|
|
@@ -3146,6 +3185,24 @@ function boundDiagnostics(processCwd, selected, snapshot = { supported: false, r
|
|
|
3146
3185
|
|
|
3147
3186
|
// src/tools/result.ts
|
|
3148
3187
|
import { z } from "zod";
|
|
3188
|
+
var TARGET_MCP_TOOL_NAMES = [
|
|
3189
|
+
"init",
|
|
3190
|
+
"help",
|
|
3191
|
+
"analyze",
|
|
3192
|
+
"deploy",
|
|
3193
|
+
"refresh",
|
|
3194
|
+
"status",
|
|
3195
|
+
"rotate",
|
|
3196
|
+
"plans",
|
|
3197
|
+
"subscribe",
|
|
3198
|
+
"bind",
|
|
3199
|
+
"billing",
|
|
3200
|
+
"portal",
|
|
3201
|
+
"recover",
|
|
3202
|
+
"change",
|
|
3203
|
+
"support",
|
|
3204
|
+
"report"
|
|
3205
|
+
];
|
|
3149
3206
|
var STRUCTURED_TOOL_OUTPUT_SCHEMA = {
|
|
3150
3207
|
schemaVersion: z.literal(1),
|
|
3151
3208
|
outcome: z.enum([
|
|
@@ -3161,13 +3218,41 @@ var STRUCTURED_TOOL_OUTPUT_SCHEMA = {
|
|
|
3161
3218
|
operationId: z.string().optional(),
|
|
3162
3219
|
summary: z.string(),
|
|
3163
3220
|
data: z.record(z.string(), z.unknown()),
|
|
3221
|
+
decision: z.object({
|
|
3222
|
+
decisionVersion: z.literal(1),
|
|
3223
|
+
prompt: z.string(),
|
|
3224
|
+
selectionMode: z.literal("single"),
|
|
3225
|
+
selectionRequired: z.literal(true),
|
|
3226
|
+
defaultOptionId: z.null(),
|
|
3227
|
+
options: z.array(
|
|
3228
|
+
z.object({
|
|
3229
|
+
id: z.string(),
|
|
3230
|
+
label: z.string(),
|
|
3231
|
+
description: z.string(),
|
|
3232
|
+
consequences: z.array(z.string()),
|
|
3233
|
+
nextAction: z.discriminatedUnion("type", [
|
|
3234
|
+
z.object({
|
|
3235
|
+
type: z.literal("call_tool"),
|
|
3236
|
+
tool: z.enum(TARGET_MCP_TOOL_NAMES),
|
|
3237
|
+
arguments: z.record(z.string(), z.unknown()),
|
|
3238
|
+
reasonCode: z.string().optional()
|
|
3239
|
+
}),
|
|
3240
|
+
z.object({ type: z.literal("open_url"), url: z.string() }),
|
|
3241
|
+
z.object({ type: z.literal("none") })
|
|
3242
|
+
])
|
|
3243
|
+
})
|
|
3244
|
+
)
|
|
3245
|
+
}).optional(),
|
|
3164
3246
|
userAction: z.object({
|
|
3165
3247
|
type: z.enum(["open_url", "confirm_in_mcp", "configure_dns", "select_site"]),
|
|
3166
3248
|
provider: z.enum(["stripe", "sakupa"]).optional(),
|
|
3167
3249
|
url: z.string().optional(),
|
|
3168
3250
|
expiresAt: z.string().optional(),
|
|
3169
3251
|
expectedOutcome: z.string(),
|
|
3170
|
-
resumeWith: z.object({
|
|
3252
|
+
resumeWith: z.object({
|
|
3253
|
+
tool: z.enum(TARGET_MCP_TOOL_NAMES),
|
|
3254
|
+
arguments: z.record(z.string(), z.unknown())
|
|
3255
|
+
}).optional(),
|
|
3171
3256
|
options: z.array(
|
|
3172
3257
|
z.object({
|
|
3173
3258
|
label: z.string(),
|
|
@@ -3178,7 +3263,7 @@ var STRUCTURED_TOOL_OUTPUT_SCHEMA = {
|
|
|
3178
3263
|
}).optional(),
|
|
3179
3264
|
nextActions: z.array(
|
|
3180
3265
|
z.object({
|
|
3181
|
-
tool: z.
|
|
3266
|
+
tool: z.enum(TARGET_MCP_TOOL_NAMES),
|
|
3182
3267
|
arguments: z.record(z.string(), z.unknown()).optional(),
|
|
3183
3268
|
allowed: z.boolean(),
|
|
3184
3269
|
reasonCode: z.string().optional()
|
|
@@ -3193,7 +3278,7 @@ function structuredToolResult(envelope) {
|
|
|
3193
3278
|
}
|
|
3194
3279
|
|
|
3195
3280
|
// src/tools/context.ts
|
|
3196
|
-
import { randomUUID as
|
|
3281
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
3197
3282
|
var LocalGuidanceError = class extends SakupaError {
|
|
3198
3283
|
constructor(code, message) {
|
|
3199
3284
|
super(code, message);
|
|
@@ -3272,7 +3357,7 @@ function reportAuthorizationStore(ctx) {
|
|
|
3272
3357
|
return store;
|
|
3273
3358
|
}
|
|
3274
3359
|
function issueReportAuthorization(ctx, failedTool) {
|
|
3275
|
-
const token =
|
|
3360
|
+
const token = randomUUID5();
|
|
3276
3361
|
reportAuthorizationStore(ctx).set(token, {
|
|
3277
3362
|
failedTool,
|
|
3278
3363
|
expiresAt: Date.now() + 10 * 60 * 1e3
|
|
@@ -3361,6 +3446,176 @@ function toolError(e) {
|
|
|
3361
3446
|
return { ...result, isError: true };
|
|
3362
3447
|
}
|
|
3363
3448
|
|
|
3449
|
+
// src/tools/decision.ts
|
|
3450
|
+
var FORBIDDEN_DECISION_ARGUMENT_KEYS = /* @__PURE__ */ new Set([
|
|
3451
|
+
"credential",
|
|
3452
|
+
"candidatecredential",
|
|
3453
|
+
"devicecredential",
|
|
3454
|
+
"password",
|
|
3455
|
+
"secret",
|
|
3456
|
+
"privatekey",
|
|
3457
|
+
"accesstoken",
|
|
3458
|
+
"refreshtoken"
|
|
3459
|
+
]);
|
|
3460
|
+
function assertDecisionArgumentsSafe(value, path = "arguments") {
|
|
3461
|
+
if (typeof value === "string") {
|
|
3462
|
+
if (/^sk_[A-Za-z0-9_-]{20,}$/.test(value)) {
|
|
3463
|
+
throw new Error(`Decision ${path} contains a credential-like value.`);
|
|
3464
|
+
}
|
|
3465
|
+
return;
|
|
3466
|
+
}
|
|
3467
|
+
if (Array.isArray(value)) {
|
|
3468
|
+
value.forEach((entry, index) => assertDecisionArgumentsSafe(entry, `${path}[${index}]`));
|
|
3469
|
+
return;
|
|
3470
|
+
}
|
|
3471
|
+
if (typeof value !== "object" || value === null) return;
|
|
3472
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
3473
|
+
const normalizedKey = key.replace(/[_-]/g, "").toLowerCase();
|
|
3474
|
+
if (FORBIDDEN_DECISION_ARGUMENT_KEYS.has(normalizedKey)) {
|
|
3475
|
+
throw new Error(`Decision ${path}.${key} contains a forbidden secret field.`);
|
|
3476
|
+
}
|
|
3477
|
+
assertDecisionArgumentsSafe(entry, `${path}.${key}`);
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
function buildDecisionContract(prompt, options) {
|
|
3481
|
+
const normalizedPrompt = prompt.trim();
|
|
3482
|
+
if (!normalizedPrompt) throw new Error("Decision prompt must not be empty.");
|
|
3483
|
+
if (options.length < 2) throw new Error("A decision must contain at least two options.");
|
|
3484
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3485
|
+
let actionableOptions = 0;
|
|
3486
|
+
let noActionOptions = 0;
|
|
3487
|
+
for (const option of options) {
|
|
3488
|
+
if (!/^[a-z][a-z0-9_]*$/.test(option.id)) {
|
|
3489
|
+
throw new Error(`Invalid decision option id: ${option.id}`);
|
|
3490
|
+
}
|
|
3491
|
+
if (ids.has(option.id)) throw new Error(`Duplicate decision option id: ${option.id}`);
|
|
3492
|
+
ids.add(option.id);
|
|
3493
|
+
if (!option.label.trim() || !option.description.trim()) {
|
|
3494
|
+
throw new Error(`Decision option ${option.id} requires a label and description.`);
|
|
3495
|
+
}
|
|
3496
|
+
if (option.nextAction.type === "none") {
|
|
3497
|
+
noActionOptions += 1;
|
|
3498
|
+
continue;
|
|
3499
|
+
}
|
|
3500
|
+
actionableOptions += 1;
|
|
3501
|
+
if (option.nextAction.type === "call_tool" && !TARGET_MCP_TOOL_NAMES.includes(option.nextAction.tool)) {
|
|
3502
|
+
throw new Error(`Unknown MCP decision tool: ${String(option.nextAction.tool)}`);
|
|
3503
|
+
}
|
|
3504
|
+
if (option.nextAction.type === "call_tool") {
|
|
3505
|
+
assertDecisionArgumentsSafe(option.nextAction.arguments);
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
if (actionableOptions === 0) throw new Error("A decision requires an actionable option.");
|
|
3509
|
+
if (noActionOptions === 0) throw new Error("A decision requires a no-action exit option.");
|
|
3510
|
+
return {
|
|
3511
|
+
decisionVersion: 1,
|
|
3512
|
+
prompt: normalizedPrompt,
|
|
3513
|
+
selectionMode: "single",
|
|
3514
|
+
selectionRequired: true,
|
|
3515
|
+
defaultOptionId: null,
|
|
3516
|
+
options
|
|
3517
|
+
};
|
|
3518
|
+
}
|
|
3519
|
+
function formatDecisionFallback(decision) {
|
|
3520
|
+
const options = decision.options.map((option, index) => {
|
|
3521
|
+
const consequences = option.consequences.length === 0 ? "" : `
|
|
3522
|
+
Consequences: ${option.consequences.join(" ")}`;
|
|
3523
|
+
let exactAction;
|
|
3524
|
+
switch (option.nextAction.type) {
|
|
3525
|
+
case "call_tool":
|
|
3526
|
+
exactAction = `If the user selects this option, call ${option.nextAction.tool} with these exact arguments: ${JSON.stringify(option.nextAction.arguments)}.`;
|
|
3527
|
+
break;
|
|
3528
|
+
case "open_url":
|
|
3529
|
+
exactAction = `If the user selects this option, present this exact URL: ${option.nextAction.url}.`;
|
|
3530
|
+
break;
|
|
3531
|
+
case "none":
|
|
3532
|
+
exactAction = "If the user selects this option, call no tool and make no change.";
|
|
3533
|
+
break;
|
|
3534
|
+
}
|
|
3535
|
+
return `${index + 1}. [${option.id}] ${option.label}
|
|
3536
|
+
${option.description}${consequences}
|
|
3537
|
+
${exactAction}`;
|
|
3538
|
+
});
|
|
3539
|
+
return `USER DECISION REQUIRED: ${decision.prompt}
|
|
3540
|
+
No option is selected by default. Present every option to the user, do not choose on their behalf, and never reconstruct or guess tool arguments.
|
|
3541
|
+
|
|
3542
|
+
` + options.join("\n\n");
|
|
3543
|
+
}
|
|
3544
|
+
function decisionToolResult(input) {
|
|
3545
|
+
const decision = buildDecisionContract(input.prompt, input.options);
|
|
3546
|
+
const actionable = decision.options.filter(
|
|
3547
|
+
(option) => option.nextAction.type === "call_tool"
|
|
3548
|
+
);
|
|
3549
|
+
const nextActions = actionable.map((option) => ({
|
|
3550
|
+
tool: option.nextAction.tool,
|
|
3551
|
+
arguments: option.nextAction.arguments,
|
|
3552
|
+
allowed: true,
|
|
3553
|
+
...option.nextAction.reasonCode === void 0 ? {} : { reasonCode: option.nextAction.reasonCode }
|
|
3554
|
+
}));
|
|
3555
|
+
let userAction;
|
|
3556
|
+
if (input.legacyUserAction?.type === "confirm_in_mcp" && actionable.length === 1) {
|
|
3557
|
+
const option = actionable[0];
|
|
3558
|
+
if (option !== void 0) {
|
|
3559
|
+
userAction = {
|
|
3560
|
+
type: "confirm_in_mcp",
|
|
3561
|
+
provider: input.legacyUserAction.provider,
|
|
3562
|
+
expectedOutcome: option.description,
|
|
3563
|
+
resumeWith: {
|
|
3564
|
+
tool: option.nextAction.tool,
|
|
3565
|
+
arguments: option.nextAction.arguments
|
|
3566
|
+
}
|
|
3567
|
+
};
|
|
3568
|
+
}
|
|
3569
|
+
} else if (input.legacyUserAction?.type === "select_site") {
|
|
3570
|
+
userAction = {
|
|
3571
|
+
type: "select_site",
|
|
3572
|
+
provider: input.legacyUserAction.provider,
|
|
3573
|
+
expectedOutcome: "Apply only the option explicitly selected by the user.",
|
|
3574
|
+
options: actionable.map((option) => ({
|
|
3575
|
+
label: option.label,
|
|
3576
|
+
value: typeof option.nextAction.arguments["reuseSiteUrl"] === "string" ? option.nextAction.arguments["reuseSiteUrl"] : option.id,
|
|
3577
|
+
expectedOutcome: option.description
|
|
3578
|
+
}))
|
|
3579
|
+
};
|
|
3580
|
+
}
|
|
3581
|
+
return structuredToolResult({
|
|
3582
|
+
schemaVersion: 1,
|
|
3583
|
+
outcome: input.outcome ?? "waiting_user",
|
|
3584
|
+
resultCode: input.resultCode,
|
|
3585
|
+
...input.operationId === void 0 ? {} : { operationId: input.operationId },
|
|
3586
|
+
summary: `${input.summary}
|
|
3587
|
+
|
|
3588
|
+
${formatDecisionFallback(decision)}`,
|
|
3589
|
+
data: input.data,
|
|
3590
|
+
decision,
|
|
3591
|
+
...userAction === void 0 ? {} : { userAction },
|
|
3592
|
+
nextActions
|
|
3593
|
+
});
|
|
3594
|
+
}
|
|
3595
|
+
function callToolDecisionOption(input) {
|
|
3596
|
+
return {
|
|
3597
|
+
id: input.id,
|
|
3598
|
+
label: input.label,
|
|
3599
|
+
description: input.description,
|
|
3600
|
+
consequences: input.consequences ?? [],
|
|
3601
|
+
nextAction: {
|
|
3602
|
+
type: "call_tool",
|
|
3603
|
+
tool: input.tool,
|
|
3604
|
+
arguments: input.arguments,
|
|
3605
|
+
...input.reasonCode === void 0 ? {} : { reasonCode: input.reasonCode }
|
|
3606
|
+
}
|
|
3607
|
+
};
|
|
3608
|
+
}
|
|
3609
|
+
function noActionDecisionOption(input) {
|
|
3610
|
+
return {
|
|
3611
|
+
id: input?.id ?? "cancel",
|
|
3612
|
+
label: input?.label ?? "Do not continue",
|
|
3613
|
+
description: input?.description ?? "Keep the current local and cloud state unchanged.",
|
|
3614
|
+
consequences: [],
|
|
3615
|
+
nextAction: { type: "none" }
|
|
3616
|
+
};
|
|
3617
|
+
}
|
|
3618
|
+
|
|
3364
3619
|
// src/tools/definitions.ts
|
|
3365
3620
|
function text(resultCode, t, data = {}, outcome = "completed", nextActions = []) {
|
|
3366
3621
|
return structuredToolResult({
|
|
@@ -3500,17 +3755,10 @@ ${block}`, checklist: toDnsChecklist(diag.checks) };
|
|
|
3500
3755
|
}
|
|
3501
3756
|
}
|
|
3502
3757
|
function freeSiteCreationBarrier(sites, deployArguments) {
|
|
3503
|
-
const userSiteOptions = sites.map((site) => ({
|
|
3504
|
-
label: `Replace content at ${site.url}`,
|
|
3505
|
-
value: site.url,
|
|
3506
|
-
expectedOutcome: "A site handoff keeps this existing free-site URL, replaces its online content, issues a fresh project credential, and revokes every previous credential."
|
|
3507
|
-
}));
|
|
3508
3758
|
const summary = `Sakupa cloud confirmed that this network already has ${FREE_ACTIVE_SITES_PER_IP} active free sites, so no new site was created. Authenticated device discovery found ${sites.length} free site(s) this device can hand off.
|
|
3509
3759
|
|
|
3510
3760
|
` + sites.map((site) => `- ${site.url} (expires ${site.expiresAt})`).join("\n") + "\n\nThe free-site allowance is full. Ask the user which existing free URL may have its content REPLACED by the current project. Selecting one authorizes a site handoff: deploy keeps that URL, overwrites its online content with the current files, issues a fresh project credential, and revokes every previous credential. The cloud site is NOT deleted. No prior project directory, browser history, workspace switch, or user-run command is required. YOU then call deploy with the exact nextAction arguments. Never ask the user to locate an old directory or run a CLI, and never recommend another hosting provider.";
|
|
3511
|
-
return
|
|
3512
|
-
schemaVersion: 1,
|
|
3513
|
-
outcome: "waiting_user",
|
|
3761
|
+
return decisionToolResult({
|
|
3514
3762
|
resultCode: "free_site_slot_selection_required",
|
|
3515
3763
|
summary,
|
|
3516
3764
|
data: {
|
|
@@ -3522,29 +3770,44 @@ function freeSiteCreationBarrier(sites, deployArguments) {
|
|
|
3522
3770
|
cloudSiteWillBeDeleted: false,
|
|
3523
3771
|
previousProjectWillBeUnbound: true
|
|
3524
3772
|
},
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3773
|
+
prompt: "Choose exactly one existing free URL that the current project may take over.",
|
|
3774
|
+
options: [
|
|
3775
|
+
...sites.map(
|
|
3776
|
+
(site, index) => callToolDecisionOption({
|
|
3777
|
+
id: `handoff_site_${index + 1}`,
|
|
3778
|
+
label: `Replace content at ${site.url}`,
|
|
3779
|
+
description: "Keep this existing free-site URL and replace its online content with the current project.",
|
|
3780
|
+
consequences: [
|
|
3781
|
+
"A fresh project credential is issued and every previous credential is revoked.",
|
|
3782
|
+
"The cloud site is not deleted and no previous project directory is needed."
|
|
3783
|
+
],
|
|
3784
|
+
tool: "deploy",
|
|
3785
|
+
arguments: {
|
|
3786
|
+
...deployArguments,
|
|
3787
|
+
publicConfirmed: true,
|
|
3788
|
+
reuseSiteUrl: site.url,
|
|
3789
|
+
reuseConfirmed: true
|
|
3790
|
+
},
|
|
3791
|
+
reasonCode: "user_selected_reusable_free_site"
|
|
3792
|
+
})
|
|
3793
|
+
),
|
|
3794
|
+
noActionDecisionOption({
|
|
3795
|
+
description: "Do not take over any existing free site and create no new site."
|
|
3796
|
+
})
|
|
3797
|
+
],
|
|
3798
|
+
legacyUserAction: { type: "select_site", provider: "sakupa" }
|
|
3542
3799
|
});
|
|
3543
3800
|
}
|
|
3544
3801
|
async function discoverDeviceFreeSites(client, apiBaseUrl, device) {
|
|
3802
|
+
let cloudSites = (await client.listDeviceFreeSites(device.deviceId, device.credential)).sites;
|
|
3803
|
+
const alreadyOwned = new Set(cloudSites.map((site) => site.siteId));
|
|
3804
|
+
let claimedAny = false;
|
|
3545
3805
|
for (const record of listRecentCreations(Date.now(), apiBaseUrl)) {
|
|
3546
3806
|
const state = loadSiteFile(record.projectDir);
|
|
3547
3807
|
if (state.kind !== "ok" || state.file.siteId !== record.siteId) continue;
|
|
3808
|
+
if (state.file.apiBaseUrl !== "" && state.file.apiBaseUrl !== apiBaseUrl) continue;
|
|
3809
|
+
if (alreadyOwned.has(record.siteId)) continue;
|
|
3810
|
+
const environmentIsKnown = record.apiBaseUrl === apiBaseUrl || state.file.apiBaseUrl === apiBaseUrl;
|
|
3548
3811
|
try {
|
|
3549
3812
|
await client.claimDeviceFreeSite(
|
|
3550
3813
|
record.siteId,
|
|
@@ -3552,9 +3815,11 @@ async function discoverDeviceFreeSites(client, apiBaseUrl, device) {
|
|
|
3552
3815
|
device.deviceId,
|
|
3553
3816
|
device.credential
|
|
3554
3817
|
);
|
|
3818
|
+
claimedAny = true;
|
|
3819
|
+
alreadyOwned.add(record.siteId);
|
|
3555
3820
|
} catch (error) {
|
|
3556
3821
|
if (isSakupaError(error) && ["not_found", "state_conflict"].includes(error.code)) {
|
|
3557
|
-
removeCreation(record.siteId);
|
|
3822
|
+
if (environmentIsKnown) removeCreation(record.siteId);
|
|
3558
3823
|
continue;
|
|
3559
3824
|
}
|
|
3560
3825
|
if (!isSakupaError(error) || error.code !== "unauthorized") {
|
|
@@ -3562,7 +3827,10 @@ async function discoverDeviceFreeSites(client, apiBaseUrl, device) {
|
|
|
3562
3827
|
}
|
|
3563
3828
|
}
|
|
3564
3829
|
}
|
|
3565
|
-
|
|
3830
|
+
if (claimedAny) {
|
|
3831
|
+
cloudSites = (await client.listDeviceFreeSites(device.deviceId, device.credential)).sites;
|
|
3832
|
+
}
|
|
3833
|
+
return cloudSites;
|
|
3566
3834
|
}
|
|
3567
3835
|
function outputDirectoryChain(projectRoot, outputAbs) {
|
|
3568
3836
|
const rel = relative3(projectRoot, outputAbs);
|
|
@@ -3659,18 +3927,39 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
3659
3927
|
const effectiveOutputDir = analysis.recommendedOutputDir ?? ".";
|
|
3660
3928
|
const recordedOutputDir = ctx.projectMarker?.outputDir;
|
|
3661
3929
|
if (recordedOutputDir !== void 0 && resolve5(ctx.projectDir, recordedOutputDir) !== resolve5(ctx.projectDir, effectiveOutputDir) && args.outputDirChangeConfirmed !== true) {
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3930
|
+
const confirmation = { outputDirChangeConfirmed: true };
|
|
3931
|
+
const confirmArguments = {
|
|
3932
|
+
...args,
|
|
3933
|
+
outputDir: effectiveOutputDir,
|
|
3934
|
+
...confirmation
|
|
3935
|
+
};
|
|
3936
|
+
return decisionToolResult({
|
|
3665
3937
|
resultCode: "publish_directory_change_confirmation_required",
|
|
3666
3938
|
summary: `This initialized project last published from "${recordedOutputDir}", but this request selected "${effectiveOutputDir}". Nothing was uploaded and the site was not changed. Show both paths to the user; only after explicit confirmation call deploy again with outputDirChangeConfirmed: true.`,
|
|
3667
3939
|
data: {
|
|
3668
3940
|
projectDir: ctx.projectDir,
|
|
3669
3941
|
previousOutputDir: recordedOutputDir,
|
|
3670
3942
|
requestedOutputDir: effectiveOutputDir,
|
|
3671
|
-
confirmationField: "outputDirChangeConfirmed"
|
|
3943
|
+
confirmationField: "outputDirChangeConfirmed",
|
|
3944
|
+
confirmation,
|
|
3945
|
+
confirmArguments
|
|
3672
3946
|
},
|
|
3673
|
-
|
|
3947
|
+
prompt: `Use the newly selected publish directory "${effectiveOutputDir}"?`,
|
|
3948
|
+
options: [
|
|
3949
|
+
callToolDecisionOption({
|
|
3950
|
+
id: "use_new_publish_directory",
|
|
3951
|
+
label: `Use ${effectiveOutputDir}`,
|
|
3952
|
+
description: "Publish this project from the newly selected directory.",
|
|
3953
|
+
consequences: [`The recorded publish directory changes from ${recordedOutputDir}.`],
|
|
3954
|
+
tool: "deploy",
|
|
3955
|
+
arguments: confirmArguments,
|
|
3956
|
+
reasonCode: "explicit_confirmation"
|
|
3957
|
+
}),
|
|
3958
|
+
noActionDecisionOption({
|
|
3959
|
+
description: "Keep the recorded publish directory and upload nothing."
|
|
3960
|
+
})
|
|
3961
|
+
],
|
|
3962
|
+
legacyUserAction: { type: "confirm_in_mcp", provider: "sakupa" }
|
|
3674
3963
|
});
|
|
3675
3964
|
}
|
|
3676
3965
|
const files = analysis.files;
|
|
@@ -3725,24 +4014,37 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
3725
4014
|
}
|
|
3726
4015
|
if (nestedMarker.kind === "ok") {
|
|
3727
4016
|
if (args.sakupaRelocationConfirmed !== true) {
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
4017
|
+
const confirmation = { sakupaRelocationConfirmed: true };
|
|
4018
|
+
const confirmArguments = { ...args, ...confirmation };
|
|
4019
|
+
return decisionToolResult({
|
|
3731
4020
|
resultCode: "sakupa_relocation_confirmation_required",
|
|
3732
4021
|
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.`,
|
|
3733
4022
|
data: {
|
|
3734
4023
|
projectRoot: ctx.projectDir,
|
|
3735
4024
|
misplacedSakupaDirectory: join9(candidateDir, ".sakupa"),
|
|
3736
4025
|
targetSakupaDirectory: join9(ctx.projectDir, ".sakupa"),
|
|
3737
|
-
confirmationField: "sakupaRelocationConfirmed"
|
|
4026
|
+
confirmationField: "sakupaRelocationConfirmed",
|
|
4027
|
+
confirmation,
|
|
4028
|
+
confirmArguments
|
|
3738
4029
|
},
|
|
3739
|
-
|
|
3740
|
-
|
|
4030
|
+
prompt: "Move the nested Sakupa project binding to the active workspace Root?",
|
|
4031
|
+
options: [
|
|
4032
|
+
callToolDecisionOption({
|
|
4033
|
+
id: "relocate_sakupa_binding",
|
|
4034
|
+
label: "Move the Sakupa binding to the workspace Root",
|
|
4035
|
+
description: "Validate and relocate the nested Sakupa binding, then continue this deployment.",
|
|
4036
|
+
consequences: [
|
|
4037
|
+
"Sakupa preserves valid credentials and refuses conflicting bindings."
|
|
4038
|
+
],
|
|
3741
4039
|
tool: "deploy",
|
|
3742
|
-
|
|
4040
|
+
arguments: confirmArguments,
|
|
3743
4041
|
reasonCode: "explicit_sakupa_relocation_confirmation"
|
|
3744
|
-
}
|
|
3745
|
-
|
|
4042
|
+
}),
|
|
4043
|
+
noActionDecisionOption({
|
|
4044
|
+
description: "Leave both directories unchanged and upload nothing."
|
|
4045
|
+
})
|
|
4046
|
+
],
|
|
4047
|
+
legacyUserAction: { type: "confirm_in_mcp", provider: "sakupa" }
|
|
3746
4048
|
});
|
|
3747
4049
|
}
|
|
3748
4050
|
markerRelocatedFrom.push(candidateDir);
|
|
@@ -3865,19 +4167,41 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
3865
4167
|
deleteProjectMarker(dir);
|
|
3866
4168
|
}
|
|
3867
4169
|
if (!existing && args.reuseSiteUrl === void 0 && args.publicConfirmed !== true) {
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
4170
|
+
const confirmation = { publicConfirmed: true };
|
|
4171
|
+
const confirmArguments = { ...args, ...confirmation };
|
|
4172
|
+
return decisionToolResult({
|
|
4173
|
+
resultCode: "public_deployment_confirmation_required",
|
|
4174
|
+
summary: `First deployment creates a public URL that anyone with the link can open. The free preview stays live for ${FREE_SITE_TTL_HOURS} hours. Nothing has been uploaded or made public yet. The exact confirmation field is publicConfirmed: true.`,
|
|
4175
|
+
data: {
|
|
4176
|
+
publicUrlLifetimeHours: FREE_SITE_TTL_HOURS,
|
|
4177
|
+
confirmationField: "publicConfirmed",
|
|
4178
|
+
confirmation,
|
|
4179
|
+
confirmArguments
|
|
4180
|
+
},
|
|
4181
|
+
prompt: "Create the first public free-site preview for this project?",
|
|
4182
|
+
options: [
|
|
4183
|
+
callToolDecisionOption({
|
|
4184
|
+
id: "create_public_preview",
|
|
4185
|
+
label: "Create the public preview",
|
|
4186
|
+
description: `Publish the selected files at a public URL for ${FREE_SITE_TTL_HOURS} hours.`,
|
|
4187
|
+
consequences: ["Anyone with the generated URL can open the site."],
|
|
4188
|
+
tool: "deploy",
|
|
4189
|
+
arguments: confirmArguments,
|
|
4190
|
+
reasonCode: "explicit_public_deployment_confirmation"
|
|
4191
|
+
}),
|
|
4192
|
+
noActionDecisionOption({
|
|
4193
|
+
description: "Keep the project private and upload nothing."
|
|
4194
|
+
})
|
|
4195
|
+
],
|
|
4196
|
+
legacyUserAction: { type: "confirm_in_mcp", provider: "sakupa" }
|
|
4197
|
+
});
|
|
3874
4198
|
}
|
|
3875
4199
|
if (!existing) {
|
|
3876
4200
|
if (args.reuseSiteUrl !== void 0) {
|
|
3877
4201
|
if (args.reuseConfirmed !== true) {
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
4202
|
+
const confirmation = { reuseConfirmed: true };
|
|
4203
|
+
const confirmArguments = { ...args, publicConfirmed: true, ...confirmation };
|
|
4204
|
+
return decisionToolResult({
|
|
3881
4205
|
resultCode: "free_site_reuse_confirmation_required",
|
|
3882
4206
|
summary: `Nothing was changed. Reusing ${args.reuseSiteUrl} will replace all online content at that URL with the current project, issue a fresh credential here, and revoke every previous credential automatically. No old directory is needed. Show these consequences and call deploy with reuseConfirmed:true only after the user explicitly selects this URL.`,
|
|
3883
4207
|
data: {
|
|
@@ -3885,25 +4209,29 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
3885
4209
|
cloudSiteWillBeDeleted: false,
|
|
3886
4210
|
onlineContentWillBeReplaced: true,
|
|
3887
4211
|
previousProjectWillBeUnbound: true,
|
|
3888
|
-
confirmationField: "reuseConfirmed"
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
type: "confirm_in_mcp",
|
|
3892
|
-
provider: "sakupa",
|
|
3893
|
-
expectedOutcome: "Replace the selected free URL content and move its local project binding.",
|
|
3894
|
-
resumeWith: {
|
|
3895
|
-
tool: "deploy",
|
|
3896
|
-
arguments: { ...args, publicConfirmed: true, reuseConfirmed: true }
|
|
3897
|
-
}
|
|
4212
|
+
confirmationField: "reuseConfirmed",
|
|
4213
|
+
confirmation,
|
|
4214
|
+
confirmArguments
|
|
3898
4215
|
},
|
|
3899
|
-
|
|
3900
|
-
|
|
4216
|
+
prompt: `Take over ${args.reuseSiteUrl} with the current project?`,
|
|
4217
|
+
options: [
|
|
4218
|
+
callToolDecisionOption({
|
|
4219
|
+
id: "confirm_site_handoff",
|
|
4220
|
+
label: `Take over ${args.reuseSiteUrl}`,
|
|
4221
|
+
description: "Keep the selected URL and replace all online content with the current project.",
|
|
4222
|
+
consequences: [
|
|
4223
|
+
"A fresh credential is issued here and every previous credential is revoked.",
|
|
4224
|
+
"The previous project becomes unbound; the cloud site is not deleted."
|
|
4225
|
+
],
|
|
3901
4226
|
tool: "deploy",
|
|
3902
|
-
arguments:
|
|
3903
|
-
allowed: true,
|
|
4227
|
+
arguments: confirmArguments,
|
|
3904
4228
|
reasonCode: "explicit_free_site_reuse_confirmation"
|
|
3905
|
-
}
|
|
3906
|
-
|
|
4229
|
+
}),
|
|
4230
|
+
noActionDecisionOption({
|
|
4231
|
+
description: "Keep the selected site and current project unchanged."
|
|
4232
|
+
})
|
|
4233
|
+
],
|
|
4234
|
+
legacyUserAction: { type: "confirm_in_mcp", provider: "sakupa" }
|
|
3907
4235
|
});
|
|
3908
4236
|
}
|
|
3909
4237
|
}
|
|
@@ -4279,7 +4607,7 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
|
|
|
4279
4607
|
{
|
|
4280
4608
|
siteId: site.siteId,
|
|
4281
4609
|
plan: args.plan,
|
|
4282
|
-
idempotencyKey:
|
|
4610
|
+
idempotencyKey: randomUUID6()
|
|
4283
4611
|
},
|
|
4284
4612
|
site.credential
|
|
4285
4613
|
);
|
|
@@ -4730,10 +5058,59 @@ After the DNS record resolves, re-run recover with verificationId: "${res2.verif
|
|
|
4730
5058
|
}
|
|
4731
5059
|
if (args.action === "status") {
|
|
4732
5060
|
const res2 = await ctx.client.getRecoveryStatus(verificationId);
|
|
5061
|
+
if (res2.readyToComplete) {
|
|
5062
|
+
const revokeArguments = {
|
|
5063
|
+
action: "complete",
|
|
5064
|
+
verificationId,
|
|
5065
|
+
preserveExistingCredentials: false,
|
|
5066
|
+
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
|
|
5067
|
+
};
|
|
5068
|
+
const preserveArguments = {
|
|
5069
|
+
...revokeArguments,
|
|
5070
|
+
preserveExistingCredentials: true
|
|
5071
|
+
};
|
|
5072
|
+
return decisionToolResult({
|
|
5073
|
+
resultCode: "domain_recovery_ready",
|
|
5074
|
+
summary: "DNS control is verified and recovery is ready to complete. Nothing was completed yet. The user must choose whether previous site credentials remain valid.",
|
|
5075
|
+
data: {
|
|
5076
|
+
recovery: res2,
|
|
5077
|
+
credentialPolicyChoices: ["revoke_previous", "preserve_previous"]
|
|
5078
|
+
},
|
|
5079
|
+
prompt: "How should Sakupa handle the site credentials that existed before recovery?",
|
|
5080
|
+
options: [
|
|
5081
|
+
callToolDecisionOption({
|
|
5082
|
+
id: "revoke_previous_credentials",
|
|
5083
|
+
label: "Revoke all previous credentials",
|
|
5084
|
+
description: "Complete recovery with the new local credential and revoke every previous credential.",
|
|
5085
|
+
consequences: [
|
|
5086
|
+
"Old project folders and credential backups can no longer manage the site."
|
|
5087
|
+
],
|
|
5088
|
+
tool: "recover",
|
|
5089
|
+
arguments: revokeArguments,
|
|
5090
|
+
reasonCode: "user_selected_secure_recovery"
|
|
5091
|
+
}),
|
|
5092
|
+
callToolDecisionOption({
|
|
5093
|
+
id: "preserve_previous_credentials",
|
|
5094
|
+
label: "Keep previous credentials valid",
|
|
5095
|
+
description: "Complete recovery with the new local credential without revoking existing credentials.",
|
|
5096
|
+
consequences: [
|
|
5097
|
+
"Any old project folder or leaked credential that still works retains site authority."
|
|
5098
|
+
],
|
|
5099
|
+
tool: "recover",
|
|
5100
|
+
arguments: preserveArguments,
|
|
5101
|
+
reasonCode: "user_selected_credential_preservation"
|
|
5102
|
+
}),
|
|
5103
|
+
noActionDecisionOption({
|
|
5104
|
+
label: "Do not complete recovery yet",
|
|
5105
|
+
description: "Keep the verified recovery pending and change no credential."
|
|
5106
|
+
})
|
|
5107
|
+
]
|
|
5108
|
+
});
|
|
5109
|
+
}
|
|
4733
5110
|
return structuredToolResult({
|
|
4734
5111
|
schemaVersion: 1,
|
|
4735
|
-
outcome: res2.status === "expired" ? "expired" :
|
|
4736
|
-
resultCode: res2.status === "expired" ? "domain_recovery_expired" :
|
|
5112
|
+
outcome: res2.status === "expired" ? "expired" : "pending_provider",
|
|
5113
|
+
resultCode: res2.status === "expired" ? "domain_recovery_expired" : "domain_recovery_pending_dns",
|
|
4737
5114
|
summary: `DNS recovery verification status: ${res2.status}`,
|
|
4738
5115
|
data: { recovery: res2 },
|
|
4739
5116
|
nextActions: [
|
|
@@ -4744,8 +5121,8 @@ After the DNS record resolves, re-run recover with verificationId: "${res2.verif
|
|
|
4744
5121
|
verificationId,
|
|
4745
5122
|
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
|
|
4746
5123
|
},
|
|
4747
|
-
allowed:
|
|
4748
|
-
|
|
5124
|
+
allowed: false,
|
|
5125
|
+
reasonCode: res2.status
|
|
4749
5126
|
}
|
|
4750
5127
|
]
|
|
4751
5128
|
});
|
|
@@ -4915,12 +5292,39 @@ Files: ${extracted.fileCount}; bytes: ${extracted.totalBytes}
|
|
|
4915
5292
|
};
|
|
4916
5293
|
if (args.confirmSubmit !== true) {
|
|
4917
5294
|
const contactNote = args.contactEmail !== void 0 ? "note that their contact email is attached for follow-up. " : "ASK THEM ONCE whether they want to attach a contact email for follow-up (optional \u2014 omit if declined; include it as contactEmail when they do). ";
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
"preview"
|
|
4923
|
-
|
|
5295
|
+
const confirmation = { confirmSubmit: true };
|
|
5296
|
+
const confirmArguments = { ...args, ...confirmation };
|
|
5297
|
+
return decisionToolResult({
|
|
5298
|
+
resultCode: "bug_report_preview_ready",
|
|
5299
|
+
outcome: "preview",
|
|
5300
|
+
summary: `Bug report prepared but NOT submitted. This is the exact payload that would be sent (structured diagnostics only \u2014 no file contents, source code or secrets). Show it to the user, and ${contactNote}Exact payload:
|
|
5301
|
+
${JSON.stringify(payload, null, 2)}`,
|
|
5302
|
+
data: {
|
|
5303
|
+
result: payload,
|
|
5304
|
+
confirmation,
|
|
5305
|
+
confirmArguments,
|
|
5306
|
+
submitted: false
|
|
5307
|
+
},
|
|
5308
|
+
prompt: "Submit this exact sanitized bug report?",
|
|
5309
|
+
options: [
|
|
5310
|
+
callToolDecisionOption({
|
|
5311
|
+
id: "submit_bug_report",
|
|
5312
|
+
label: "Submit the reviewed report",
|
|
5313
|
+
description: "Submit exactly the sanitized payload shown above.",
|
|
5314
|
+
consequences: [
|
|
5315
|
+
args.contactEmail === void 0 ? "No contact email is attached." : "The provided contact email is attached for follow-up."
|
|
5316
|
+
],
|
|
5317
|
+
tool: "report",
|
|
5318
|
+
arguments: confirmArguments,
|
|
5319
|
+
reasonCode: "explicit_bug_report_submission_confirmation"
|
|
5320
|
+
}),
|
|
5321
|
+
noActionDecisionOption({
|
|
5322
|
+
label: "Do not submit the report",
|
|
5323
|
+
description: "Keep the report local and send nothing to Sakupa."
|
|
5324
|
+
})
|
|
5325
|
+
],
|
|
5326
|
+
legacyUserAction: { type: "confirm_in_mcp", provider: "sakupa" }
|
|
5327
|
+
});
|
|
4924
5328
|
}
|
|
4925
5329
|
const res = await baseCtx.client.reportBug(payload, site?.credential);
|
|
4926
5330
|
return text(
|
|
@@ -5317,8 +5721,18 @@ function registerHelpTools(server, baseCtx) {
|
|
|
5317
5721
|
schemaVersion: 1,
|
|
5318
5722
|
outcome: "completed",
|
|
5319
5723
|
resultCode: "help_overview",
|
|
5320
|
-
summary:
|
|
5321
|
-
data: {
|
|
5724
|
+
summary: `Sakupa tool overview and parameter names returned. Site handoff moves an existing free URL to the current project and replaces its credential as a safety consequence; credential rotation changes the credential in place solely for security. Both revoke prior values. When a result contains decision, present every option, select none by default, and copy only the user's selected option nextAction exactly. Use help topic:"terminology" for every site/credential distinction. On any failure call help with topic:"diagnose" before retrying, support or report.`,
|
|
5725
|
+
data: {
|
|
5726
|
+
tools: catalog,
|
|
5727
|
+
toolOrder: TOOL_TOPICS,
|
|
5728
|
+
terminology: HELP_TERMINOLOGY,
|
|
5729
|
+
decisionOptions: {
|
|
5730
|
+
selectionMode: "single",
|
|
5731
|
+
defaultOptionId: null,
|
|
5732
|
+
presentEveryOption: true,
|
|
5733
|
+
exactNextActionRequired: true
|
|
5734
|
+
}
|
|
5735
|
+
},
|
|
5322
5736
|
nextActions: []
|
|
5323
5737
|
});
|
|
5324
5738
|
}
|
|
@@ -5477,9 +5891,7 @@ function registerCredentialTools(server, baseCtx) {
|
|
|
5477
5891
|
const status = await ctx.client.getCredentialStatus(site.siteId, site.credential);
|
|
5478
5892
|
const confirmation = { confirmed: true };
|
|
5479
5893
|
if (args.confirmed !== true) {
|
|
5480
|
-
return
|
|
5481
|
-
schemaVersion: 1,
|
|
5482
|
-
outcome: "waiting_user",
|
|
5894
|
+
return decisionToolResult({
|
|
5483
5895
|
resultCode: "credential_rotation_confirmation_required",
|
|
5484
5896
|
summary: `Nothing was changed. Rotating the management credential for ${site.url ?? site.siteId} will generate a new credential locally, save it as the current credential in this project .sakupa/site.json, and revoke EVERY previous credential for this site\u2014including copies in old folders and backups. Rotation is optional and deploy remains available. Current credential created at: ${status.credentialCreatedAt}. Exact confirm arguments: ${JSON.stringify(confirmation)}. Ask the user for explicit approval; never expose credential values.`,
|
|
5485
5897
|
data: {
|
|
@@ -5492,20 +5904,26 @@ function registerCredentialTools(server, baseCtx) {
|
|
|
5492
5904
|
previousCredentialsWillBeRevoked: true,
|
|
5493
5905
|
optional: true
|
|
5494
5906
|
},
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5907
|
+
prompt: `Rotate the management credential for ${site.url ?? site.siteId}?`,
|
|
5908
|
+
options: [
|
|
5909
|
+
callToolDecisionOption({
|
|
5910
|
+
id: "rotate_credential",
|
|
5911
|
+
label: "Rotate the management credential",
|
|
5912
|
+
description: "Generate one new local credential and make it the only valid credential for this site.",
|
|
5913
|
+
consequences: [
|
|
5914
|
+
"Every previous credential is revoked, including copies in old folders and backups.",
|
|
5915
|
+
"The site URL and online content do not change."
|
|
5916
|
+
],
|
|
5503
5917
|
tool: "rotate",
|
|
5504
5918
|
arguments: confirmation,
|
|
5505
|
-
allowed: true,
|
|
5506
5919
|
reasonCode: "explicit_credential_rotation_confirmation"
|
|
5507
|
-
}
|
|
5508
|
-
|
|
5920
|
+
}),
|
|
5921
|
+
noActionDecisionOption({
|
|
5922
|
+
label: "Keep the current credential",
|
|
5923
|
+
description: "Do not rotate; deployment remains available with the current credential."
|
|
5924
|
+
})
|
|
5925
|
+
],
|
|
5926
|
+
legacyUserAction: { type: "confirm_in_mcp", provider: "sakupa" }
|
|
5509
5927
|
});
|
|
5510
5928
|
}
|
|
5511
5929
|
writeCredentialRotation(ctx.projectDir, {
|
|
@@ -5734,6 +6152,14 @@ Workflow:
|
|
|
5734
6152
|
payment, refund and other customer-service requests. report is the LAST resort only when
|
|
5735
6153
|
help explicitly recommends a product bug report, and submission still requires user review.
|
|
5736
6154
|
|
|
6155
|
+
Decision-options contract: when a result contains decision, present EVERY numbered option from the
|
|
6156
|
+
tool to the user and wait for their selection. No option is selected by default. Never choose from
|
|
6157
|
+
context, paraphrase a selection into different arguments, or invent another option. After the user
|
|
6158
|
+
selects, copy that option's exact nextAction. Legacy userAction and nextActions mirror the same
|
|
6159
|
+
choice for older clients; decision is the authoritative choice set. A type "none" option means call
|
|
6160
|
+
no tool and change nothing. Stripe-hosted links remain direct links because Stripe itself owns plan
|
|
6161
|
+
selection and confirmation.
|
|
6162
|
+
|
|
5737
6163
|
Project directory contract: before the first deploy or a new recovery, initialize the intended
|
|
5738
6164
|
project by calling init with NO path argument. init uses the IDE's exact MCP Root and creates the
|
|
5739
6165
|
non-secret .sakupa/project.json directly there. The CLI command
|