@neopress/mcp 1.3.0 → 1.4.0

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.cjs CHANGED
@@ -45,6 +45,8 @@ node_process = __toESM(node_process, 1);
45
45
  let node_fs = require("node:fs");
46
46
  let node_path = require("node:path");
47
47
  let node_os = require("node:os");
48
+ let node_dns_promises = require("node:dns/promises");
49
+ let node_net = require("node:net");
48
50
  Object.freeze({ status: "aborted" });
49
51
  function $constructor(name, initializer, params) {
50
52
  function init(inst, def) {
@@ -42291,7 +42293,7 @@ function shouldShowDeprecationWarning() {
42291
42293
  }
42292
42294
  if (shouldShowDeprecationWarning()) console.warn("⚠️ Node.js 18 and below are deprecated and will no longer be supported in future versions of @supabase/supabase-js. Please upgrade to Node.js 20 or later. For more information, visit: https://github.com/orgs/supabase/discussions/37217");
42293
42295
  //#endregion
42294
- //#region ../sdk/dist/errors.js
42296
+ //#region ../sdk/src/errors.ts
42295
42297
  /**
42296
42298
  * SDK error classes
42297
42299
  */
@@ -42308,13 +42310,7 @@ var NeopressApiError = class extends Error {
42308
42310
  }
42309
42311
  };
42310
42312
  //#endregion
42311
- //#region ../sdk/dist/serializers.js
42312
- /**
42313
- * SDK serializers — convert DB snake_case rows to SDK camelCase shapes.
42314
- *
42315
- * Ported 1:1 from apps/neopress/src/lib/api/v1/serializers.ts.
42316
- * Do NOT change field mappings without updating both files.
42317
- */
42313
+ //#region ../sdk/src/serializers.ts
42318
42314
  function serializePageFull(page) {
42319
42315
  return {
42320
42316
  id: page.id,
@@ -42475,6 +42471,7 @@ function serializeTemplate(t) {
42475
42471
  referenceUrl: t.reference_url,
42476
42472
  previewImgUrls: t.preview_img_urls,
42477
42473
  previewVideoUrl: t.preview_video_url ?? null,
42474
+ language: t.language ?? null,
42478
42475
  createdAt: t.created_at
42479
42476
  };
42480
42477
  }
@@ -42563,7 +42560,7 @@ function serializeSiteLayout(siteId, layout) {
42563
42560
  };
42564
42561
  }
42565
42562
  //#endregion
42566
- //#region ../sdk/dist/endpoints/pages.js
42563
+ //#region ../sdk/src/endpoints/pages.ts
42567
42564
  var PagesEndpoint = class {
42568
42565
  client;
42569
42566
  constructor(client) {
@@ -42897,7 +42894,7 @@ function getDefaultLogLevel() {
42897
42894
  }
42898
42895
  getDefaultLogLevel();
42899
42896
  //#endregion
42900
- //#region ../sdk/dist/endpoints/collections.js
42897
+ //#region ../sdk/src/endpoints/collections.ts
42901
42898
  var CollectionsEndpoint = class {
42902
42899
  client;
42903
42900
  constructor(client) {
@@ -42963,7 +42960,7 @@ var CollectionsEndpoint = class {
42963
42960
  }
42964
42961
  };
42965
42962
  //#endregion
42966
- //#region ../sdk/dist/endpoints/entries.js
42963
+ //#region ../sdk/src/endpoints/entries.ts
42967
42964
  var EntriesEndpoint = class {
42968
42965
  client;
42969
42966
  constructor(client) {
@@ -43036,7 +43033,7 @@ var EntriesEndpoint = class {
43036
43033
  }
43037
43034
  };
43038
43035
  //#endregion
43039
- //#region ../sdk/dist/endpoints/forms.js
43036
+ //#region ../sdk/src/endpoints/forms.ts
43040
43037
  var FormsEndpoint = class {
43041
43038
  client;
43042
43039
  constructor(client) {
@@ -43104,7 +43101,7 @@ var FormsEndpoint = class {
43104
43101
  }
43105
43102
  };
43106
43103
  //#endregion
43107
- //#region ../sdk/dist/endpoints/assets.js
43104
+ //#region ../sdk/src/endpoints/assets.ts
43108
43105
  var AssetsEndpoint = class {
43109
43106
  client;
43110
43107
  constructor(client) {
@@ -43165,7 +43162,7 @@ var AssetsEndpoint = class {
43165
43162
  }
43166
43163
  };
43167
43164
  //#endregion
43168
- //#region ../sdk/dist/endpoints/site.js
43165
+ //#region ../sdk/src/endpoints/site.ts
43169
43166
  var SiteEndpoint = class {
43170
43167
  client;
43171
43168
  constructor(client) {
@@ -43210,7 +43207,7 @@ var SiteEndpoint = class {
43210
43207
  }
43211
43208
  };
43212
43209
  //#endregion
43213
- //#region ../sdk/dist/endpoints/redirects.js
43210
+ //#region ../sdk/src/endpoints/redirects.ts
43214
43211
  const DASHBOARD_ONLY_REWRITE_MESSAGE = "Rewrite proxy rules can only be managed in the dashboard.";
43215
43212
  function rejectModeParam(data) {
43216
43213
  if (data && typeof data === "object" && Object.prototype.hasOwnProperty.call(data, "mode")) throw new NeopressApiError("REDIRECT_RULE_DASHBOARD_ONLY", DASHBOARD_ONLY_REWRITE_MESSAGE, 400);
@@ -43271,7 +43268,7 @@ var RedirectsEndpoint = class {
43271
43268
  }
43272
43269
  };
43273
43270
  //#endregion
43274
- //#region ../sdk/dist/endpoints/publish.js
43271
+ //#region ../sdk/src/endpoints/publish.ts
43275
43272
  var PublishEndpoint = class {
43276
43273
  client;
43277
43274
  constructor(client) {
@@ -43285,7 +43282,7 @@ var PublishEndpoint = class {
43285
43282
  }
43286
43283
  };
43287
43284
  //#endregion
43288
- //#region ../sdk/dist/endpoints/entry-references.js
43285
+ //#region ../sdk/src/endpoints/entry-references.ts
43289
43286
  var EntryReferencesEndpoint = class {
43290
43287
  client;
43291
43288
  constructor(client) {
@@ -43316,7 +43313,7 @@ var EntryReferencesEndpoint = class {
43316
43313
  }
43317
43314
  };
43318
43315
  //#endregion
43319
- //#region ../sdk/dist/endpoints/analytics.js
43316
+ //#region ../sdk/src/endpoints/analytics.ts
43320
43317
  const RESERVED_ANALYTICS_PARAMS = /* @__PURE__ */ new Set([
43321
43318
  "pipe",
43322
43319
  "site",
@@ -43401,7 +43398,7 @@ var AnalyticsEndpoint = class {
43401
43398
  }
43402
43399
  };
43403
43400
  //#endregion
43404
- //#region ../sdk/dist/endpoints/templates.js
43401
+ //#region ../sdk/src/endpoints/templates.ts
43405
43402
  var TemplatesEndpoint = class {
43406
43403
  client;
43407
43404
  constructor(client) {
@@ -43443,7 +43440,7 @@ var TemplatesEndpoint = class {
43443
43440
  if (siteIds.length === 0) return [];
43444
43441
  const namePrefix = params?.namePrefix?.trim();
43445
43442
  const limit = Math.min(Math.max(params?.limit ?? 100, 1), 500);
43446
- let query = this.client.db.from("templates").select("id, name, reference_url, preview_img_urls, description, created_at").in("id", siteIds);
43443
+ let query = this.client.db.from("templates").select("id, name, reference_url, preview_img_urls, description, language, created_at").in("id", siteIds);
43447
43444
  if (namePrefix) query = query.ilike("name", `${namePrefix}%`);
43448
43445
  if (params?.order) query = query.order("created_at", { ascending: params.order === "asc" });
43449
43446
  query = query.limit(limit);
@@ -43459,13 +43456,14 @@ var TemplatesEndpoint = class {
43459
43456
  referenceUrl: t.reference_url,
43460
43457
  previewImgUrls: t.preview_img_urls ?? null,
43461
43458
  description: t.description ?? null,
43459
+ language: t.language ?? null,
43462
43460
  createdAt: t.created_at
43463
43461
  };
43464
43462
  });
43465
43463
  }
43466
43464
  };
43467
43465
  //#endregion
43468
- //#region ../sdk/dist/endpoints/headshots.js
43466
+ //#region ../sdk/src/endpoints/headshots.ts
43469
43467
  /**
43470
43468
  * headshot_pool is a GLOBAL (not site-scoped) admin table. RLS exposes it only to
43471
43469
  * the super-admin user, so these direct-DB calls return empty/forbidden for any
@@ -43549,7 +43547,7 @@ var HeadshotsEndpoint = class {
43549
43547
  }
43550
43548
  };
43551
43549
  //#endregion
43552
- //#region ../sdk/dist/endpoints/logos.js
43550
+ //#region ../sdk/src/endpoints/logos.ts
43553
43551
  /**
43554
43552
  * logo_pool is a GLOBAL (not site-scoped) admin table. RLS exposes it only to the
43555
43553
  * super-admin user, so these direct-DB calls return empty/forbidden for any other
@@ -43639,7 +43637,7 @@ var LogosEndpoint = class {
43639
43637
  }
43640
43638
  };
43641
43639
  //#endregion
43642
- //#region ../sdk/dist/client.js
43640
+ //#region ../sdk/src/client.ts
43643
43641
  /**
43644
43642
  * @neopress/sdk — API Client
43645
43643
  *
@@ -43716,7 +43714,8 @@ var NeopressClient = class {
43716
43714
  * header so RLS applies. No session persistence/refresh — token freshness is the
43717
43715
  * caller's responsibility (CLI getClientAsync / MCP per-request).
43718
43716
  *
43719
- * `@internal` (with `stripInternal`) keeps this out of the emitted declarations:
43717
+ * `@internal` (with `stripInternal` in the cli/mcp dts configs — sdk itself no
43718
+ * longer emits) keeps this out of the published declarations:
43720
43719
  * it is plumbing for the endpoints in this package, and typing it publicly would
43721
43720
  * drag `SupabaseClient` + the generated `Database` into the published `.d.ts` of
43722
43721
  * `@neopress/cli` / `@neopress/mcp` — packages that declare no dependencies, so
@@ -43860,7 +43859,7 @@ function getSiteId(siteIdOverride) {
43860
43859
  if (projectConfig.siteId) return projectConfig.siteId;
43861
43860
  return loadSession().activeSiteId;
43862
43861
  }
43863
- function setActiveSiteId(siteId) {
43862
+ async function setActiveSiteId(siteId) {
43864
43863
  saveSession({
43865
43864
  ...loadSession(),
43866
43865
  activeSiteId: siteId
@@ -43974,8 +43973,9 @@ Use these rules before generating pages, collection schemas, entries, forms, or
43974
43973
  - Reusable templates are normal Neopress sites marked by a \`templates\` metadata row.
43975
43974
  - \`templates.id\` equals \`sites.id\`.
43976
43975
  - Build layout, pages, collections, entries, forms, assets, compile, publish, and verify before marking a finished template.
43977
- - Use \`neopress_template_mark\` to create or update the metadata row with \`name\`, required \`referenceUrl\`, required \`previewImgUrls\` (at least one), and optional \`description\` and \`previewVideoUrl\`. The gallery card renders \`previewImgUrls[0]\` at rest and cross-fades to \`previewVideoUrl\` on hover, so the first preview image must be that video's first frame.
43976
+ - Use \`neopress_template_mark\` to create or update the metadata row with \`name\`, required \`referenceUrl\`, required \`language\`, required \`previewImgUrls\` (at least one), and optional \`description\` and \`previewVideoUrl\`. The gallery card renders \`previewImgUrls[0]\` at rest and cross-fades to \`previewVideoUrl\` on hover, so the first preview image must be that video's first frame.
43978
43977
  - \`referenceUrl\` is a single http(s) provenance URL. \`description\` is an optional summary of the template (max 150 characters).
43978
+ - \`language\` is the language the template's own content is written in, as a two-letter lowercase ISO 639-1 code (\`ko\`, \`en\`, \`es\`). Region locales like \`en-US\` are rejected.
43979
43979
  - Template metadata does not replace page TSX, CMS content, forms, or publish state.
43980
43980
  - Use \`neopress_template_delete\` only for explicit template-site hard deletion. It removes the metadata row and connected site.
43981
43981
 
@@ -43984,6 +43984,7 @@ Use these rules before generating pages, collection schemas, entries, forms, or
43984
43984
  - Dynamic pages require a collection, a page path like \`/blog/[slug]\`, a numeric \`collectionId\`, matching entries, entry publish, page compile, and site publish.
43985
43985
  - \`queryConfig\` is stored in page \`draftMeta.queryConfig\`.
43986
43986
  - Query labels become React props. Entry query results are flattened, so use \`post.body\` instead of \`post.data.body\`.
43987
+ - A query may include \`collectionWindow\` on an \`entries\` query for a dynamic detail route. Its label receives \`{ before, after, items }\`; each is a flattened-entry array around the current entry, and \`items\` combines both directions. Windows use \`published_at DESC, id DESC\`, allow 0–20 entries per direction, and may only declare that exact order. Use the exact configured label, never a guessed prop name. Optional \`matchFields\` compare raw anchor-snapshot fields only when the source field has a value.
43987
43988
  - Initial CMS data arrives through those injected query props. \`entryClient\`, when injected in reader/preview for a site handle, is only a client-side auxiliary for load-more/filter/get/create interactions after the initial render; do not use it for initial render or server data loading.
43988
43989
  - Use numeric \`collection_id\` filters. Runtime controls site, status, locale, deleted state, and published snapshots.
43989
43990
 
@@ -44043,6 +44044,198 @@ function toToolError(error) {
44043
44044
  }]
44044
44045
  };
44045
44046
  }
44047
+ var AssetImportError = class extends Error {
44048
+ constructor(message) {
44049
+ super(message);
44050
+ this.name = "AssetImportError";
44051
+ }
44052
+ };
44053
+ function ipv4ToInt(address) {
44054
+ const parts = address.split(".");
44055
+ if (parts.length !== 4) return null;
44056
+ let value = 0;
44057
+ for (const part of parts) {
44058
+ const octet = Number(part);
44059
+ if (!Number.isInteger(octet) || octet < 0 || octet > 255) return null;
44060
+ value = value * 256 + octet;
44061
+ }
44062
+ return value;
44063
+ }
44064
+ /**
44065
+ * Blocks every address that is not routable on the public internet. Includes
44066
+ * the ranges that matter for cloud metadata (169.254.0.0/16) and for reaching
44067
+ * back into our own VPC.
44068
+ */
44069
+ function isBlockedAddress(address) {
44070
+ const family = (0, node_net.isIP)(address);
44071
+ if (family === 4) {
44072
+ const value = ipv4ToInt(address);
44073
+ if (value === null) return true;
44074
+ const inRange = (cidrBase, bits) => {
44075
+ const base = ipv4ToInt(cidrBase);
44076
+ if (base === null) return false;
44077
+ const mask = bits === 0 ? 0 : -1 << 32 - bits >>> 0;
44078
+ return (value & mask) === (base & mask);
44079
+ };
44080
+ return inRange("0.0.0.0", 8) || inRange("10.0.0.0", 8) || inRange("100.64.0.0", 10) || inRange("127.0.0.0", 8) || inRange("169.254.0.0", 16) || inRange("172.16.0.0", 12) || inRange("192.0.0.0", 24) || inRange("192.168.0.0", 16) || inRange("198.18.0.0", 15) || inRange("224.0.0.0", 4) || inRange("240.0.0.0", 4);
44081
+ }
44082
+ if (family === 6) {
44083
+ const normalized = address.toLowerCase().split("%")[0] ?? "";
44084
+ const mapped = normalized.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
44085
+ if (mapped?.[1]) return isBlockedAddress(mapped[1]);
44086
+ if (normalized === "::" || normalized === "::1") return true;
44087
+ return normalized.startsWith("fc") || normalized.startsWith("fd") || normalized.startsWith("fe8") || normalized.startsWith("fe9") || normalized.startsWith("fea") || normalized.startsWith("feb");
44088
+ }
44089
+ return true;
44090
+ }
44091
+ /**
44092
+ * Rejects non-public destinations. Resolving here (rather than trusting the
44093
+ * hostname) is what stops `metadata.internal`-style names and DNS entries that
44094
+ * deliberately point at private space.
44095
+ */
44096
+ async function assertPublicUrl(rawUrl, resolver = defaultResolver) {
44097
+ let url;
44098
+ try {
44099
+ url = new URL(rawUrl);
44100
+ } catch {
44101
+ throw new AssetImportError(`Not a valid URL: ${rawUrl}`);
44102
+ }
44103
+ if (url.protocol !== "http:" && url.protocol !== "https:") throw new AssetImportError(`Only http(s) URLs can be imported, got ${url.protocol}`);
44104
+ const hostname = url.hostname.replace(/^\[|\]$/g, "");
44105
+ const addresses = (0, node_net.isIP)(hostname) ? [hostname] : await resolver(hostname);
44106
+ if (addresses.length === 0) throw new AssetImportError(`Could not resolve ${url.hostname}`);
44107
+ for (const address of addresses) if (isBlockedAddress(address)) throw new AssetImportError(`Refusing to fetch ${url.hostname}: it resolves to the non-public address ${address}`);
44108
+ return url;
44109
+ }
44110
+ async function defaultResolver(hostname) {
44111
+ return (await (0, node_dns_promises.lookup)(hostname, { all: true }).catch(() => [])).map((record) => record.address);
44112
+ }
44113
+ /**
44114
+ * Fetches the URL with every hop validated, then reads the body under a hard
44115
+ * byte cap. `redirect: 'manual'` is deliberate: letting fetch follow redirects
44116
+ * would skip the address check on the hops in between.
44117
+ */
44118
+ async function fetchImportableAsset(rawUrl, options) {
44119
+ const maxBytes = options.maxBytes ?? 52428800;
44120
+ let currentUrl = rawUrl;
44121
+ for (let hop = 0; hop <= 3; hop += 1) {
44122
+ const url = await assertPublicUrl(currentUrl, options.resolver);
44123
+ const controller = new AbortController();
44124
+ const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? 3e4);
44125
+ let response;
44126
+ try {
44127
+ response = await options.fetch(url.toString(), {
44128
+ redirect: "manual",
44129
+ signal: controller.signal
44130
+ });
44131
+ } finally {
44132
+ clearTimeout(timer);
44133
+ }
44134
+ if (response.status >= 300 && response.status < 400) {
44135
+ const location = response.headers.get("location");
44136
+ if (!location) throw new AssetImportError(`Redirect from ${url.toString()} had no Location header`);
44137
+ currentUrl = new URL(location, url).toString();
44138
+ continue;
44139
+ }
44140
+ if (!response.ok) throw new AssetImportError(`Source responded ${response.status} ${response.statusText || ""}`.trim());
44141
+ const declaredLength = Number(response.headers.get("content-length"));
44142
+ if (Number.isFinite(declaredLength) && declaredLength > maxBytes) throw new AssetImportError(`Source is ${declaredLength} bytes, over the ${maxBytes} byte limit`);
44143
+ return {
44144
+ bytes: await readCapped(response, maxBytes),
44145
+ contentType: (response.headers.get("content-type") || "").split(";")[0]?.trim() || "",
44146
+ finalUrl: url.toString()
44147
+ };
44148
+ }
44149
+ throw new AssetImportError(`Too many redirects (max 3)`);
44150
+ }
44151
+ /** Reads the body incrementally so a lying Content-Length cannot blow the cap. */
44152
+ async function readCapped(response, maxBytes) {
44153
+ const reader = response.body?.getReader();
44154
+ if (!reader) {
44155
+ const buffer = new Uint8Array(await response.arrayBuffer());
44156
+ if (buffer.byteLength > maxBytes) throw new AssetImportError(`Source exceeds the ${maxBytes} byte limit`);
44157
+ return buffer;
44158
+ }
44159
+ const chunks = [];
44160
+ let total = 0;
44161
+ for (;;) {
44162
+ const { done, value } = await reader.read();
44163
+ if (done) break;
44164
+ if (!value) continue;
44165
+ total += value.byteLength;
44166
+ if (total > maxBytes) {
44167
+ await reader.cancel().catch(() => {});
44168
+ throw new AssetImportError(`Source exceeds the ${maxBytes} byte limit`);
44169
+ }
44170
+ chunks.push(value);
44171
+ }
44172
+ const merged = new Uint8Array(total);
44173
+ let offset = 0;
44174
+ for (const chunk of chunks) {
44175
+ merged.set(chunk, offset);
44176
+ offset += chunk.byteLength;
44177
+ }
44178
+ return merged;
44179
+ }
44180
+ /**
44181
+ * Derives a storage-safe file name. The source path is only a hint — anything
44182
+ * outside the allowed character set is dropped so a crafted URL cannot steer
44183
+ * the object key.
44184
+ */
44185
+ function deriveImportFileName(finalUrl, contentType) {
44186
+ const sanitized = decodeURIComponent(new URL(finalUrl).pathname.split("/").pop() || "").replace(/[^\w.-]/g, "").replace(/^\.+/, "");
44187
+ if (sanitized && /\.[a-z0-9]{1,5}$/i.test(sanitized)) return sanitized.slice(0, 120);
44188
+ const extension = extensionForContentType(contentType);
44189
+ const stem = (sanitized || "imported-asset").slice(0, 100);
44190
+ return extension ? `${stem}.${extension}` : stem;
44191
+ }
44192
+ function extensionForContentType(contentType) {
44193
+ return {
44194
+ "image/avif": "avif",
44195
+ "image/gif": "gif",
44196
+ "image/jpeg": "jpg",
44197
+ "image/png": "png",
44198
+ "image/svg+xml": "svg",
44199
+ "image/webp": "webp",
44200
+ "video/mp4": "mp4",
44201
+ "application/pdf": "pdf",
44202
+ "text/csv": "csv"
44203
+ }[contentType.toLowerCase()] ?? null;
44204
+ }
44205
+ /**
44206
+ * Ceiling for the inline (base64) upload fallback. Enforced on the *decoded*
44207
+ * bytes: capping the base64 string instead would let a caller slip ~33% more
44208
+ * through, and the number that matters downstream is the object size.
44209
+ *
44210
+ * 1MB is a deliberate cost ceiling rather than a technical one. Inline bytes
44211
+ * traverse the model's context, so 1MB costs roughly 90K tokens — already
44212
+ * enough that a caller should reach for presign or URL import when it can.
44213
+ */
44214
+ const MAX_INLINE_UPLOAD_BYTES = 1048576;
44215
+ /**
44216
+ * Decodes a base64 payload, tolerating a `data:` URL wrapper since models
44217
+ * routinely produce one. Rejects anything that is not valid base64: Buffer
44218
+ * silently drops invalid characters, so a corrupted payload would otherwise
44219
+ * upload as a truncated file instead of failing.
44220
+ */
44221
+ function decodeInlineUpload(payload, maxBytes = MAX_INLINE_UPLOAD_BYTES) {
44222
+ const dataUrl = payload.match(/^data:([^;,]*)(;base64)?,(.*)$/s);
44223
+ const contentTypeHint = dataUrl?.[1]?.trim() || null;
44224
+ const base64 = (dataUrl?.[3] ?? payload).replace(/\s/g, "");
44225
+ if (base64.length === 0) throw new AssetImportError("base64Content is empty.");
44226
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(base64)) throw new AssetImportError("base64Content is not valid base64. Send the raw base64 of the file (a data: URL prefix is also accepted).");
44227
+ const approximateBytes = Math.floor(base64.length * 3 / 4);
44228
+ if (approximateBytes > maxBytes) throw new AssetImportError(oversizeMessage(approximateBytes, maxBytes));
44229
+ const bytes = new Uint8Array(Buffer.from(base64, "base64"));
44230
+ if (bytes.byteLength > maxBytes) throw new AssetImportError(oversizeMessage(bytes.byteLength, maxBytes));
44231
+ return {
44232
+ bytes,
44233
+ contentTypeHint
44234
+ };
44235
+ }
44236
+ function oversizeMessage(actual, maxBytes) {
44237
+ return `Inline upload is ${Math.round(actual / 1024)}KB, over the ${Math.round(maxBytes / 1024)}KB limit. Inline bytes pass through the conversation and are expensive; use neopress_asset_presign (upload straight to storage) or neopress_asset_import_url (server fetches a public URL) instead.`;
44238
+ }
44046
44239
  //#endregion
44047
44240
  //#region src/tools.ts
44048
44241
  const siteIdShape = { siteId: number().int().positive().optional() };
@@ -44155,11 +44348,7 @@ function getToolMeta(tool) {
44155
44348
  return {
44156
44349
  securitySchemes: [{
44157
44350
  type: "oauth2",
44158
- scopes: [
44159
- "neopress:read",
44160
- "neopress:write",
44161
- "offline_access"
44162
- ]
44351
+ scopes: []
44163
44352
  }],
44164
44353
  "openai/toolInvocation/invoking": `Running ${tool.title}`,
44165
44354
  "openai/toolInvocation/invoked": `Finished ${tool.title}`
@@ -44237,7 +44426,7 @@ const toolDefinitions = [
44237
44426
  handler: async (input, context, extra) => {
44238
44427
  const selectedSiteId = asNumber(input, "siteId");
44239
44428
  const site = await (await context.createClient({ siteId: selectedSiteId }, extra)).site.get();
44240
- context.setActiveSiteId(selectedSiteId, extra);
44429
+ await context.setActiveSiteId(selectedSiteId, extra);
44241
44430
  return {
44242
44431
  siteId: selectedSiteId,
44243
44432
  baseUrl: context.getBaseUrl(),
@@ -44274,7 +44463,7 @@ const toolDefinitions = [
44274
44463
  language: asOptionalString(input, "language"),
44275
44464
  placeholder: asOptionalBoolean(input, "placeholder")
44276
44465
  });
44277
- if (input.select !== false) context.setActiveSiteId(site.id, extra);
44466
+ if (input.select !== false) await context.setActiveSiteId(site.id, extra);
44278
44467
  return site;
44279
44468
  }
44280
44469
  },
@@ -44312,12 +44501,13 @@ const toolDefinitions = [
44312
44501
  {
44313
44502
  name: "neopress_template_mark",
44314
44503
  title: "Mark Site As Template",
44315
- description: "Create or update the template metadata row for a finished template site. The template id is the site id. Requires at least one previewImgUrl. The gallery card shows previewImgUrls[0] at rest and plays previewVideoUrl on hover, so pass the video together with its own first frame as the first preview image.",
44504
+ description: "Create or update the template metadata row for a finished template site. The template id is the site id. Requires at least one previewImgUrl and a language. The gallery card shows previewImgUrls[0] at rest and plays previewVideoUrl on hover, so pass the video together with its own first frame as the first preview image.",
44316
44505
  inputSchema: {
44317
44506
  ...siteIdShape,
44318
44507
  name: string(),
44319
44508
  description: string().max(150).optional(),
44320
44509
  referenceUrl: httpUrlSchema,
44510
+ language: string().regex(/^[a-z]{2}$/, "must be a two-letter ISO 639-1 language code (e.g. ko, en, es)").describe("Primary language of the template's own content, as a two-letter lowercase ISO 639-1 code (ko, en, es). Not a region locale like en-US."),
44321
44511
  previewImgUrls: array(string()).min(1),
44322
44512
  previewVideoUrl: httpUrlSchema.optional()
44323
44513
  },
@@ -44326,6 +44516,7 @@ const toolDefinitions = [
44326
44516
  name: asString(input, "name"),
44327
44517
  description: asOptionalString(input, "description"),
44328
44518
  referenceUrl: asString(input, "referenceUrl"),
44519
+ language: asString(input, "language"),
44329
44520
  previewImgUrls: input.previewImgUrls,
44330
44521
  previewVideoUrl: asOptionalString(input, "previewVideoUrl")
44331
44522
  });
@@ -45026,6 +45217,118 @@ const toolDefinitions = [
45026
45217
  });
45027
45218
  }
45028
45219
  },
45220
+ {
45221
+ name: "neopress_asset_presign",
45222
+ title: "Get Presigned Asset Upload URL",
45223
+ description: "Preferred way to upload bytes you hold (a local file, a generated image): returns a short-lived URL to PUT the file straight to Neopress storage, then call neopress_asset_register with the returned publicUrl. Nothing passes through the conversation, so size and count cost you nothing. This requires your environment to reach the storage host directly — if that PUT is refused by a proxy or egress allowlist, fall back to neopress_asset_upload_base64. neopress_asset_upload_file does all three steps at once but only over a local (stdio) connection, since a remote server cannot read your filesystem.",
45224
+ inputSchema: {
45225
+ ...siteIdShape,
45226
+ fileName: string(),
45227
+ contentType: string().optional(),
45228
+ contentLength: number().int().positive(),
45229
+ folder: string().optional()
45230
+ },
45231
+ handler: async (input, context, extra) => {
45232
+ const client = await context.createClient({ siteId: siteId(input) }, extra);
45233
+ const fileName = (0, node_path.basename)(asString(input, "fileName"));
45234
+ const folder = asOptionalString(input, "folder") || "uploads";
45235
+ return client.assets.presign({
45236
+ fileKey: `${folder}/${Date.now()}-${fileName}`,
45237
+ contentType: asOptionalString(input, "contentType") || getContentType(fileName),
45238
+ contentLength: asNumber(input, "contentLength")
45239
+ });
45240
+ }
45241
+ },
45242
+ {
45243
+ name: "neopress_asset_import_url",
45244
+ title: "Import Asset From URL",
45245
+ description: "Copy a publicly reachable file into Neopress storage and register it as an asset. Unlike neopress_asset_register — which only records an external URL and breaks when the source moves — this stores our own copy. The server does the fetching, so no bytes pass through the conversation.",
45246
+ inputSchema: {
45247
+ ...siteIdShape,
45248
+ sourceUrl: string().url(),
45249
+ folder: string().optional(),
45250
+ displayName: string().optional(),
45251
+ altText: string().optional(),
45252
+ description: string().optional(),
45253
+ tags: array(string()).optional(),
45254
+ metadata: optionalJsonObjectSchema
45255
+ },
45256
+ handler: async (input, context, extra) => {
45257
+ const client = await context.createClient({ siteId: siteId(input) }, extra);
45258
+ const fetched = await fetchImportableAsset(asString(input, "sourceUrl"), { fetch: context.fetch });
45259
+ const fileName = deriveImportFileName(fetched.finalUrl, fetched.contentType);
45260
+ const contentType = fetched.contentType || getContentType(fileName);
45261
+ const folder = asOptionalString(input, "folder") || "uploads";
45262
+ const { presignedUrl, publicUrl } = await client.assets.presign({
45263
+ fileKey: `${folder}/${Date.now()}-${fileName}`,
45264
+ contentType,
45265
+ contentLength: fetched.bytes.byteLength
45266
+ });
45267
+ const uploadResponse = await context.fetch(presignedUrl, {
45268
+ method: "PUT",
45269
+ headers: { "Content-Type": contentType },
45270
+ body: fetched.bytes
45271
+ });
45272
+ if (!uploadResponse.ok) throw new Error(`Asset upload failed: ${uploadResponse.status} ${uploadResponse.statusText}`);
45273
+ return client.assets.register({
45274
+ assetUrl: publicUrl,
45275
+ displayName: asOptionalString(input, "displayName") || fileName,
45276
+ mimeType: contentType,
45277
+ size: fetched.bytes.byteLength,
45278
+ altText: asOptionalString(input, "altText"),
45279
+ description: asOptionalString(input, "description"),
45280
+ tags: input.tags,
45281
+ folderPath: folder,
45282
+ metadata: input.metadata
45283
+ });
45284
+ }
45285
+ },
45286
+ {
45287
+ name: "neopress_asset_upload_base64",
45288
+ title: "Upload Asset Inline (base64)",
45289
+ description: "Fallback upload for environments with no outbound network of their own: the bytes ride inside this tool call, so they travel over the connection your host already has instead of one you must open. Expensive — inline bytes pass through the conversation, so ~1MB costs roughly 90K tokens, and the limit is 1MB decoded. Reach for this only when neopress_asset_presign is refused by a proxy or egress allowlist, and prefer neopress_asset_import_url whenever the file already has a public URL.",
45290
+ inputSchema: {
45291
+ ...siteIdShape,
45292
+ fileName: string(),
45293
+ base64Content: string(),
45294
+ contentType: string().optional(),
45295
+ folder: string().optional(),
45296
+ displayName: string().optional(),
45297
+ altText: string().optional(),
45298
+ description: string().optional(),
45299
+ tags: array(string()).optional(),
45300
+ metadata: optionalJsonObjectSchema
45301
+ },
45302
+ handler: async (input, context, extra) => {
45303
+ const client = await context.createClient({ siteId: siteId(input) }, extra);
45304
+ const fileName = (0, node_path.basename)(asString(input, "fileName"));
45305
+ const { bytes, contentTypeHint } = decodeInlineUpload(asString(input, "base64Content"));
45306
+ const contentType = asOptionalString(input, "contentType") || contentTypeHint || getContentType(fileName);
45307
+ const folder = asOptionalString(input, "folder") || "uploads";
45308
+ const { presignedUrl, publicUrl } = await client.assets.presign({
45309
+ fileKey: `${folder}/${Date.now()}-${fileName}`,
45310
+ contentType,
45311
+ contentLength: bytes.byteLength
45312
+ });
45313
+ const uploadResponse = await context.fetch(presignedUrl, {
45314
+ method: "PUT",
45315
+ headers: { "Content-Type": contentType },
45316
+ body: bytes
45317
+ });
45318
+ if (!uploadResponse.ok) throw new Error(`Asset upload failed: ${uploadResponse.status} ${uploadResponse.statusText}`);
45319
+ return client.assets.register({
45320
+ assetUrl: publicUrl,
45321
+ displayName: asOptionalString(input, "displayName") || fileName,
45322
+ mimeType: contentType,
45323
+ size: bytes.byteLength,
45324
+ altText: asOptionalString(input, "altText"),
45325
+ description: asOptionalString(input, "description"),
45326
+ tags: input.tags,
45327
+ folderPath: folder,
45328
+ metadata: input.metadata
45329
+ });
45330
+ }
45331
+ },
45029
45332
  {
45030
45333
  name: "neopress_asset_upload_file",
45031
45334
  title: "Upload Local Asset File",