@sakupa/mcp 0.7.44 → 0.7.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/bin.js +72 -22
  2. package/dist/index.js +70 -20
  3. 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.44";
383
+ var SAKUPA_MCP_VERSION = "0.7.45";
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 randomUUID5 } from "node:crypto";
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 releaseDeviceLock(fd2) {
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(fd2);
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(fd2, JSON.stringify({ pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString() }));
2350
- return fd2;
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)) return { schemaVersion: 1, environments: {} };
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
- return { schemaVersion: 1, environments: parsed.environments };
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 (typeof lock !== "number") return lock;
2426
+ if (!("fd" in lock)) return lock;
2407
2427
  try {
2408
2428
  const afterLock = loadDeviceBinding(apiBaseUrl);
2409
2429
  if (afterLock) return afterLock;
2410
- const created = await client.registerDevice();
2411
- const registry = readRegistry();
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
- registry.environments[apiBaseUrl] = binding;
2418
- writeRegistry(registry);
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 randomUUID3 } from "node:crypto";
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-${randomUUID3()}.tmp`);
2836
+ const temporary = join8(directory, `.rotation-${randomUUID4()}.tmp`);
2798
2837
  writeFileSync6(temporary, `${JSON.stringify(file, null, 2)}
2799
2838
  `, {
2800
2839
  encoding: "utf8",
@@ -3193,7 +3232,7 @@ function structuredToolResult(envelope) {
3193
3232
  }
3194
3233
 
3195
3234
  // src/tools/context.ts
3196
- import { randomUUID as randomUUID4 } from "node:crypto";
3235
+ import { randomUUID as randomUUID5 } from "node:crypto";
3197
3236
  var LocalGuidanceError = class extends SakupaError {
3198
3237
  constructor(code, message) {
3199
3238
  super(code, message);
@@ -3272,7 +3311,7 @@ function reportAuthorizationStore(ctx) {
3272
3311
  return store;
3273
3312
  }
3274
3313
  function issueReportAuthorization(ctx, failedTool) {
3275
- const token = randomUUID4();
3314
+ const token = randomUUID5();
3276
3315
  reportAuthorizationStore(ctx).set(token, {
3277
3316
  failedTool,
3278
3317
  expiresAt: Date.now() + 10 * 60 * 1e3
@@ -3542,9 +3581,15 @@ function freeSiteCreationBarrier(sites, deployArguments) {
3542
3581
  });
3543
3582
  }
3544
3583
  async function discoverDeviceFreeSites(client, apiBaseUrl, device) {
3584
+ let cloudSites = (await client.listDeviceFreeSites(device.deviceId, device.credential)).sites;
3585
+ const alreadyOwned = new Set(cloudSites.map((site) => site.siteId));
3586
+ let claimedAny = false;
3545
3587
  for (const record of listRecentCreations(Date.now(), apiBaseUrl)) {
3546
3588
  const state = loadSiteFile(record.projectDir);
3547
3589
  if (state.kind !== "ok" || state.file.siteId !== record.siteId) continue;
3590
+ if (state.file.apiBaseUrl !== "" && state.file.apiBaseUrl !== apiBaseUrl) continue;
3591
+ if (alreadyOwned.has(record.siteId)) continue;
3592
+ const environmentIsKnown = record.apiBaseUrl === apiBaseUrl || state.file.apiBaseUrl === apiBaseUrl;
3548
3593
  try {
3549
3594
  await client.claimDeviceFreeSite(
3550
3595
  record.siteId,
@@ -3552,9 +3597,11 @@ async function discoverDeviceFreeSites(client, apiBaseUrl, device) {
3552
3597
  device.deviceId,
3553
3598
  device.credential
3554
3599
  );
3600
+ claimedAny = true;
3601
+ alreadyOwned.add(record.siteId);
3555
3602
  } catch (error) {
3556
3603
  if (isSakupaError(error) && ["not_found", "state_conflict"].includes(error.code)) {
3557
- removeCreation(record.siteId);
3604
+ if (environmentIsKnown) removeCreation(record.siteId);
3558
3605
  continue;
3559
3606
  }
3560
3607
  if (!isSakupaError(error) || error.code !== "unauthorized") {
@@ -3562,7 +3609,10 @@ async function discoverDeviceFreeSites(client, apiBaseUrl, device) {
3562
3609
  }
3563
3610
  }
3564
3611
  }
3565
- return (await client.listDeviceFreeSites(device.deviceId, device.credential)).sites;
3612
+ if (claimedAny) {
3613
+ cloudSites = (await client.listDeviceFreeSites(device.deviceId, device.credential)).sites;
3614
+ }
3615
+ return cloudSites;
3566
3616
  }
3567
3617
  function outputDirectoryChain(projectRoot, outputAbs) {
3568
3618
  const rel = relative3(projectRoot, outputAbs);
@@ -4279,7 +4329,7 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
4279
4329
  {
4280
4330
  siteId: site.siteId,
4281
4331
  plan: args.plan,
4282
- idempotencyKey: randomUUID5()
4332
+ idempotencyKey: randomUUID6()
4283
4333
  },
4284
4334
  site.credential
4285
4335
  );
package/dist/index.js CHANGED
@@ -125,7 +125,7 @@ var FORBIDDEN_PATH_SEGMENTS = [
125
125
  var ALLOWED_HIDDEN_PATHS = [".well-known/"];
126
126
 
127
127
  // ../core/dist/domain/version.js
128
- var SAKUPA_MCP_VERSION = "0.7.44";
128
+ var SAKUPA_MCP_VERSION = "0.7.45";
129
129
 
130
130
  // ../core/dist/domain/errors.js
131
131
  var HTTP_STATUS = {
@@ -736,8 +736,8 @@ var HttpApiClient = class {
736
736
  body: body?.slice(0, 200)
737
737
  });
738
738
  }
739
- async registerDevice() {
740
- return this.call("POST", "/v1/devices");
739
+ async registerDevice(req) {
740
+ return this.call("POST", "/v1/devices", { body: req });
741
741
  }
742
742
  async listDeviceFreeSites(deviceId, credential) {
743
743
  return this.call("GET", "/v1/devices/free-sites", {
@@ -2211,7 +2211,7 @@ function toolError(e) {
2211
2211
  }
2212
2212
 
2213
2213
  // src/tools/definitions.ts
2214
- import { randomUUID as randomUUID5 } from "node:crypto";
2214
+ import { randomUUID as randomUUID6 } from "node:crypto";
2215
2215
  import { promises as fs2 } from "node:fs";
2216
2216
  import { join as join9, relative as relative3, resolve as resolve5, sep as sep4 } from "node:path";
2217
2217
  import { z as z2 } from "zod";
@@ -2939,6 +2939,7 @@ import {
2939
2939
  unlinkSync as unlinkSync2,
2940
2940
  writeFileSync as writeFileSync4
2941
2941
  } from "node:fs";
2942
+ import { randomUUID as randomUUID4 } from "node:crypto";
2942
2943
  import { homedir as homedir3 } from "node:os";
2943
2944
  import { dirname as dirname4, join as join6 } from "node:path";
2944
2945
  var DEVICE_LOCK_STALE_MS = 3e4;
@@ -2948,10 +2949,22 @@ function deviceRegistryPath() {
2948
2949
  return join6(base, ".sakupa", "devices.json");
2949
2950
  }
2950
2951
  var deviceLockPath = () => join6(dirname4(deviceRegistryPath()), "devices.lock");
2951
- function releaseDeviceLock(fd2) {
2952
+ function lockTokenAt(path) {
2953
+ try {
2954
+ const parsed = JSON.parse(readFileSync4(path, "utf8"));
2955
+ return typeof parsed.token === "string" ? parsed.token : null;
2956
+ } catch {
2957
+ return null;
2958
+ }
2959
+ }
2960
+ function ownsDeviceLock(lock) {
2961
+ return lockTokenAt(deviceLockPath()) === lock.token;
2962
+ }
2963
+ function releaseDeviceLock(lock) {
2952
2964
  try {
2953
- closeSync(fd2);
2965
+ closeSync(lock.fd);
2954
2966
  } finally {
2967
+ if (!ownsDeviceLock(lock)) return;
2955
2968
  try {
2956
2969
  unlinkSync2(deviceLockPath());
2957
2970
  } catch {
@@ -2966,9 +2979,13 @@ async function acquireDeviceLock(apiBaseUrl) {
2966
2979
  const existing = loadDeviceBinding(apiBaseUrl);
2967
2980
  if (existing) return existing;
2968
2981
  try {
2982
+ const token = randomUUID4();
2969
2983
  const fd2 = openSync(path, "wx", 384);
2970
- writeFileSync4(fd2, JSON.stringify({ pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString() }));
2971
- return fd2;
2984
+ writeFileSync4(
2985
+ fd2,
2986
+ JSON.stringify({ token, pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString() })
2987
+ );
2988
+ return { fd: fd2, token };
2972
2989
  } catch {
2973
2990
  try {
2974
2991
  if (Date.now() - statSync2(path).mtimeMs >= DEVICE_LOCK_STALE_MS) {
@@ -2989,13 +3006,16 @@ async function acquireDeviceLock(apiBaseUrl) {
2989
3006
  }
2990
3007
  function readRegistry() {
2991
3008
  const path = deviceRegistryPath();
2992
- if (!existsSync5(path)) return { schemaVersion: 1, environments: {} };
3009
+ if (!existsSync5(path)) {
3010
+ return { schemaVersion: 1, environments: {}, pendingRegistrations: {} };
3011
+ }
2993
3012
  try {
2994
3013
  const parsed = JSON.parse(readFileSync4(path, "utf8"));
2995
3014
  if (parsed.schemaVersion !== 1 || !parsed.environments || typeof parsed.environments !== "object") {
2996
3015
  throw new Error("unsupported device registry schema");
2997
3016
  }
2998
- return { schemaVersion: 1, environments: parsed.environments };
3017
+ const pendingRegistrations = "pendingRegistrations" in parsed && parsed.pendingRegistrations && typeof parsed.pendingRegistrations === "object" ? parsed.pendingRegistrations : {};
3018
+ return { schemaVersion: 1, environments: parsed.environments, pendingRegistrations };
2999
3019
  } catch (error) {
3000
3020
  throw new Error(
3001
3021
  `Sakupa device registry is unreadable at ${path}: ${error instanceof Error ? error.message : String(error)}. Run help; do not search old project directories.`
@@ -3024,19 +3044,38 @@ async function ensureDeviceBinding(client, apiBaseUrl) {
3024
3044
  const existing = loadDeviceBinding(apiBaseUrl);
3025
3045
  if (existing) return existing;
3026
3046
  const lock = await acquireDeviceLock(apiBaseUrl);
3027
- if (typeof lock !== "number") return lock;
3047
+ if (!("fd" in lock)) return lock;
3028
3048
  try {
3029
3049
  const afterLock = loadDeviceBinding(apiBaseUrl);
3030
3050
  if (afterLock) return afterLock;
3031
- const created = await client.registerDevice();
3032
- const registry = readRegistry();
3051
+ let registry = readRegistry();
3052
+ let pending = registry.pendingRegistrations[apiBaseUrl];
3053
+ if (!pending) {
3054
+ pending = {
3055
+ operationId: randomUUID4(),
3056
+ deviceId: randomUUID4(),
3057
+ credential: generateCredential(),
3058
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
3059
+ };
3060
+ registry.pendingRegistrations[apiBaseUrl] = pending;
3061
+ writeRegistry(registry);
3062
+ }
3063
+ const created = await client.registerDevice({
3064
+ operationId: pending.operationId,
3065
+ deviceId: pending.deviceId,
3066
+ credential: pending.credential
3067
+ });
3033
3068
  const binding = {
3034
3069
  deviceId: created.deviceId,
3035
3070
  credential: created.credential,
3036
3071
  createdAt: created.createdAt
3037
3072
  };
3038
- registry.environments[apiBaseUrl] = binding;
3039
- writeRegistry(registry);
3073
+ if (ownsDeviceLock(lock)) {
3074
+ registry = readRegistry();
3075
+ registry.environments[apiBaseUrl] = binding;
3076
+ delete registry.pendingRegistrations[apiBaseUrl];
3077
+ writeRegistry(registry);
3078
+ }
3040
3079
  return binding;
3041
3080
  } finally {
3042
3081
  releaseDeviceLock(lock);
@@ -3321,7 +3360,7 @@ import {
3321
3360
  rmSync as rmSync2,
3322
3361
  writeFileSync as writeFileSync6
3323
3362
  } from "node:fs";
3324
- import { randomUUID as randomUUID4 } from "node:crypto";
3363
+ import { randomUUID as randomUUID5 } from "node:crypto";
3325
3364
  import { join as join8 } from "node:path";
3326
3365
  var ROTATION_FILE = "rotation.json";
3327
3366
  function credentialRotationPath(projectDir) {
@@ -3382,7 +3421,7 @@ function writeCredentialRotation(projectDir, file) {
3382
3421
  const directory = join8(projectDir, ".sakupa");
3383
3422
  mkdirSync6(directory, { recursive: true, mode: 448 });
3384
3423
  const target = credentialRotationPath(projectDir);
3385
- const temporary = join8(directory, `.rotation-${randomUUID4()}.tmp`);
3424
+ const temporary = join8(directory, `.rotation-${randomUUID5()}.tmp`);
3386
3425
  writeFileSync6(temporary, `${JSON.stringify(file, null, 2)}
3387
3426
  `, {
3388
3427
  encoding: "utf8",
@@ -3658,9 +3697,15 @@ function freeSiteCreationBarrier(sites, deployArguments) {
3658
3697
  });
3659
3698
  }
3660
3699
  async function discoverDeviceFreeSites(client, apiBaseUrl, device) {
3700
+ let cloudSites = (await client.listDeviceFreeSites(device.deviceId, device.credential)).sites;
3701
+ const alreadyOwned = new Set(cloudSites.map((site) => site.siteId));
3702
+ let claimedAny = false;
3661
3703
  for (const record of listRecentCreations(Date.now(), apiBaseUrl)) {
3662
3704
  const state = loadSiteFile(record.projectDir);
3663
3705
  if (state.kind !== "ok" || state.file.siteId !== record.siteId) continue;
3706
+ if (state.file.apiBaseUrl !== "" && state.file.apiBaseUrl !== apiBaseUrl) continue;
3707
+ if (alreadyOwned.has(record.siteId)) continue;
3708
+ const environmentIsKnown = record.apiBaseUrl === apiBaseUrl || state.file.apiBaseUrl === apiBaseUrl;
3664
3709
  try {
3665
3710
  await client.claimDeviceFreeSite(
3666
3711
  record.siteId,
@@ -3668,9 +3713,11 @@ async function discoverDeviceFreeSites(client, apiBaseUrl, device) {
3668
3713
  device.deviceId,
3669
3714
  device.credential
3670
3715
  );
3716
+ claimedAny = true;
3717
+ alreadyOwned.add(record.siteId);
3671
3718
  } catch (error) {
3672
3719
  if (isSakupaError(error) && ["not_found", "state_conflict"].includes(error.code)) {
3673
- removeCreation(record.siteId);
3720
+ if (environmentIsKnown) removeCreation(record.siteId);
3674
3721
  continue;
3675
3722
  }
3676
3723
  if (!isSakupaError(error) || error.code !== "unauthorized") {
@@ -3678,7 +3725,10 @@ async function discoverDeviceFreeSites(client, apiBaseUrl, device) {
3678
3725
  }
3679
3726
  }
3680
3727
  }
3681
- return (await client.listDeviceFreeSites(device.deviceId, device.credential)).sites;
3728
+ if (claimedAny) {
3729
+ cloudSites = (await client.listDeviceFreeSites(device.deviceId, device.credential)).sites;
3730
+ }
3731
+ return cloudSites;
3682
3732
  }
3683
3733
  function outputDirectoryChain(projectRoot, outputAbs) {
3684
3734
  const rel = relative3(projectRoot, outputAbs);
@@ -4395,7 +4445,7 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
4395
4445
  {
4396
4446
  siteId: site.siteId,
4397
4447
  plan: args.plan,
4398
- idempotencyKey: randomUUID5()
4448
+ idempotencyKey: randomUUID6()
4399
4449
  },
4400
4450
  site.credential
4401
4451
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sakupa/mcp",
3
- "version": "0.7.44",
3
+ "version": "0.7.45",
4
4
  "description": "Sakupa MCP server: publish AI-made static sites from your AI tool. AI-made pages, live in seconds.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",