@amirhosseinnateghi/vibed-mcp 0.1.0 → 0.1.2

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/README.md +5 -5
  2. package/dist/index.js +255 -140
  3. package/package.json +5 -2
package/README.md CHANGED
@@ -21,28 +21,28 @@ needs no install.
21
21
 
22
22
  ## Configure your client
23
23
 
24
- The server runs over stdio. Point your client at `npx -y @amirhosseinnateghi/vibed-mcp`.
24
+ The server runs over stdio. Point your client at `npx -y @amirhosseinnateghi/vibed-mcp@latest`.
25
25
 
26
26
  **Codex** — `~/.codex/config.toml`:
27
27
  ```toml
28
28
  [mcp_servers.vibed]
29
29
  command = "npx"
30
- args = ["-y", "@amirhosseinnateghi/vibed-mcp"]
30
+ args = ["-y", "@amirhosseinnateghi/vibed-mcp@latest"]
31
31
  ```
32
32
 
33
33
  **Cursor** — `~/.cursor/mcp.json` (or project `.cursor/mcp.json`):
34
34
  ```json
35
- { "mcpServers": { "vibed": { "command": "npx", "args": ["-y", "@amirhosseinnateghi/vibed-mcp"] } } }
35
+ { "mcpServers": { "vibed": { "command": "npx", "args": ["-y", "@amirhosseinnateghi/vibed-mcp@latest"] } } }
36
36
  ```
37
37
 
38
38
  **Gemini CLI** — `~/.gemini/settings.json`:
39
39
  ```json
40
- { "mcpServers": { "vibed": { "command": "npx", "args": ["-y", "@amirhosseinnateghi/vibed-mcp"] } } }
40
+ { "mcpServers": { "vibed": { "command": "npx", "args": ["-y", "@amirhosseinnateghi/vibed-mcp@latest"] } } }
41
41
  ```
42
42
 
43
43
  **Claude Code** (the plugin is the primary path, but MCP works too):
44
44
  ```bash
45
- claude mcp add vibed -- npx -y @amirhosseinnateghi/vibed-mcp
45
+ claude mcp add vibed -- npx -y @amirhosseinnateghi/vibed-mcp@latest
46
46
  ```
47
47
 
48
48
  For a local/self-hosted vibed, set the API base via env in the same config, e.g.
package/dist/index.js CHANGED
@@ -5985,141 +5985,6 @@ import { dirname, extname, isAbsolute, join, resolve } from "node:path";
5985
5985
 
5986
5986
  // ../sandbox/src/index.ts
5987
5987
  var import_node_html_parser = __toESM(require_dist(), 1);
5988
- var FORBIDDEN_API_PATTERNS = [
5989
- { re: /getUserMedia/, name: "getUserMedia" },
5990
- { re: /navigator\s*\.\s*geolocation/, name: "geolocation" },
5991
- { re: /new\s+Notification|Notification\s*\.\s*requestPermission/, name: "Notification" },
5992
- { re: /serviceWorker/, name: "serviceWorker" },
5993
- { re: /openDatabase/, name: "openDatabase" },
5994
- { re: /PaymentRequest|navigator\s*\.\s*payments/, name: "PaymentRequest" },
5995
- { re: /navigator\s*\.\s*credentials|PublicKeyCredential|WebAuthn/, name: "WebAuthn" },
5996
- { re: /requestPointerLock/, name: "pointerLock" }
5997
- // allowed via gesture only; flag
5998
- ];
5999
- var TOP_NAV_PATTERNS = [
6000
- /window\s*\.\s*top/,
6001
- /window\s*\.\s*parent\s*\.\s*location/,
6002
- /\btop\s*\.\s*location/,
6003
- /\bparent\s*\.\s*location/
6004
- ];
6005
- var BRAND_NAMES = [
6006
- "google",
6007
- "gmail",
6008
- "apple",
6009
- "icloud",
6010
- "microsoft",
6011
- "outlook",
6012
- "paypal",
6013
- "facebook",
6014
- "instagram",
6015
- "telegram",
6016
- "whatsapp",
6017
- "metamask",
6018
- "binance",
6019
- "coinbase"
6020
- ];
6021
- function extractExternalHosts(html) {
6022
- const scrubbed = html.replace(/\bxmlns(:[a-z0-9-]+)?\s*=\s*("[^"]*"|'[^']*')/gi, "").replace(/<!DOCTYPE[^>]*>/gi, "");
6023
- const hosts = /* @__PURE__ */ new Set();
6024
- const re = /https?:\/\/([a-z0-9.-]+)/gi;
6025
- let m;
6026
- while (m = re.exec(scrubbed)) {
6027
- const host = m[1]?.toLowerCase();
6028
- if (host) hosts.add(host);
6029
- }
6030
- return [...hosts];
6031
- }
6032
- function looksLikeHtml(root) {
6033
- const hasEl = root.querySelector("*") != null;
6034
- return hasEl;
6035
- }
6036
- function validate(html, opts) {
6037
- const errors = [];
6038
- const warnings = [];
6039
- const size = Buffer.byteLength(html, "utf8");
6040
- if (size > opts.maxBytes) {
6041
- errors.push({
6042
- code: "SIZE_EXCEEDED",
6043
- message: `Artifact is ${size} bytes; limit is ${opts.maxBytes}.`
6044
- });
6045
- }
6046
- const root = (0, import_node_html_parser.parse)(html, {
6047
- lowerCaseTagName: true,
6048
- comment: true,
6049
- voidTag: { closingSlash: true }
6050
- });
6051
- if (!looksLikeHtml(root)) {
6052
- errors.push({ code: "NOT_HTML", message: "Content is not parseable as HTML." });
6053
- return { ok: false, errors, warnings };
6054
- }
6055
- if (root.querySelector("iframe, frame, object, embed")) {
6056
- errors.push({
6057
- code: "FRAME_TAG",
6058
- message: "Frames are not allowed (iframe/frame/object/embed)."
6059
- });
6060
- }
6061
- if (root.querySelector("base")) {
6062
- errors.push({ code: "BASE_TAG", message: "<base> tags are not allowed." });
6063
- }
6064
- for (const meta of root.querySelectorAll("meta")) {
6065
- if ((meta.getAttribute("http-equiv") || "").toLowerCase() === "refresh") {
6066
- errors.push({ code: "META_REFRESH", message: "meta-refresh redirects are not allowed." });
6067
- break;
6068
- }
6069
- }
6070
- for (const input of root.querySelectorAll("input")) {
6071
- if ((input.getAttribute("type") || "").toLowerCase() === "password") {
6072
- errors.push({
6073
- code: "PASSWORD_INPUT",
6074
- message: 'Password inputs are not allowed (<input type="password">).'
6075
- });
6076
- break;
6077
- }
6078
- }
6079
- for (const re of TOP_NAV_PATTERNS) {
6080
- if (re.test(html)) {
6081
- errors.push({
6082
- code: "TOP_NAVIGATION",
6083
- message: "Top-level navigation attempts are not allowed.",
6084
- detail: re.source
6085
- });
6086
- break;
6087
- }
6088
- }
6089
- for (const { re, name } of FORBIDDEN_API_PATTERNS) {
6090
- if (re.test(html)) {
6091
- errors.push({
6092
- code: "FORBIDDEN_API",
6093
- message: `Forbidden API used: ${name}.`,
6094
- detail: name
6095
- });
6096
- }
6097
- }
6098
- const hosts = extractExternalHosts(html);
6099
- const offending = hosts.filter(
6100
- (h) => !opts.allowedOrigins.some((allowed) => h === allowed || h.endsWith("." + allowed))
6101
- );
6102
- for (const host of offending) {
6103
- errors.push({
6104
- code: "EXTERNAL_URL",
6105
- message: `External URL not in allowlist: ${host}`,
6106
- detail: host
6107
- });
6108
- }
6109
- const lower = html.toLowerCase();
6110
- const hasCredField = root.querySelector('input[type="email"], input[type="text"], input[type="tel"]') != null && /(password|passcode|otp|verify|sign\s*in|log\s*in|account)/i.test(html);
6111
- if (hasCredField) {
6112
- const brand = BRAND_NAMES.find((b) => lower.includes(b));
6113
- if (brand) {
6114
- warnings.push({
6115
- code: "BRAND_CREDENTIAL",
6116
- message: `Possible phishing: brand "${brand}" near a credential field.`,
6117
- detail: brand
6118
- });
6119
- }
6120
- }
6121
- return { ok: errors.length === 0, errors, warnings };
6122
- }
6123
5988
 
6124
5989
  // ../shared/src/constants.ts
6125
5990
  var CATEGORIES = [
@@ -6148,6 +6013,17 @@ var config = {
6148
6013
  MODERATION_AUTOPUBLISH: boolEnv("MODERATION_AUTOPUBLISH", true),
6149
6014
  BUILDER_MODE: strEnv("BUILDER_MODE", "mock"),
6150
6015
  // 'mock' | 'live'
6016
+ // Builder LLM — non-secret routing only (this object ships to the client).
6017
+ // The platform API key is read server-side from process.env.BUILDER_API_KEY.
6018
+ // Which platform provider serves the builder: 'gapgpt' | 'openrouter'.
6019
+ BUILDER_PROVIDER: strEnv("BUILDER_PROVIDER", "gapgpt"),
6020
+ BUILDER_BASE_URL: strEnv("BUILDER_BASE_URL", "https://api.gapgpt.app/v1"),
6021
+ BUILDER_MODEL: strEnv("BUILDER_MODEL", "claude-sonnet-4-20250514"),
6022
+ // Light model that auto-tags the experience category. Falls back to a keyword
6023
+ // heuristic if the call fails.
6024
+ BUILDER_CLASSIFIER_MODEL: strEnv("BUILDER_CLASSIFIER_MODEL", "gemini-2.5-flash"),
6025
+ BUILDER_MAX_TOKENS: intEnv("BUILDER_MAX_TOKENS", 8192),
6026
+ BUILDER_MAX_MESSAGES: intEnv("BUILDER_MAX_MESSAGES", 30),
6151
6027
  DEFAULT_LOCALE: strEnv("DEFAULT_LOCALE", "en")
6152
6028
  };
6153
6029
  function strEnv(key, fallback) {
@@ -6170,6 +6046,37 @@ function boolEnv(key, fallback) {
6170
6046
  return v === "true" || v === "1";
6171
6047
  }
6172
6048
 
6049
+ // ../shared/src/policy.ts
6050
+ function boolEnv2(key, fallback) {
6051
+ const v = typeof process !== "undefined" ? process.env[key] : void 0;
6052
+ if (v == null || v === "") return fallback;
6053
+ return v === "true" || v === "1";
6054
+ }
6055
+ var POST_RULES = [
6056
+ { id: "size_limit", code: "SIZE_EXCEEDED", title: "Size limit", default: true, env: "RULE_SIZE_LIMIT" },
6057
+ { id: "frames", code: "FRAME_TAG", title: "No frames", default: true, env: "RULE_FRAMES" },
6058
+ { id: "base_tag", code: "BASE_TAG", title: "No <base> tag", default: true, env: "RULE_BASE_TAG" },
6059
+ { id: "meta_refresh", code: "META_REFRESH", title: "No meta-refresh", default: true, env: "RULE_META_REFRESH" },
6060
+ { id: "password_input", code: "PASSWORD_INPUT", title: "No password inputs", default: true, env: "RULE_PASSWORD_INPUT" },
6061
+ { id: "top_navigation", code: "TOP_NAVIGATION", title: "No top-navigation", default: true, env: "RULE_TOP_NAVIGATION" },
6062
+ { id: "forbidden_apis", code: "FORBIDDEN_API", title: "No forbidden APIs (payments, background, capture\u2026)", default: true, env: "RULE_FORBIDDEN_APIS" },
6063
+ { id: "external_urls", code: "EXTERNAL_URL", title: "No off-allowlist external URLs", default: true, env: "RULE_EXTERNAL_URLS" },
6064
+ { id: "external_scripts", code: "EXTERNAL_SCRIPT", title: "No creator-selected JS packages (external <script>)", default: false, env: "RULE_EXTERNAL_SCRIPTS" },
6065
+ { id: "external_links", code: "EXTERNAL_LINK", title: "No direct external links (<a> off-site)", default: false, env: "RULE_EXTERNAL_LINKS" },
6066
+ // Enforced by injecting a viewport clip at sanitize (not a reject) — a static
6067
+ // scan can't tell whether a post actually scrolls, so we make it can't.
6068
+ { id: "no_vertical_scroll", code: "SANITIZE_CLIP", title: "No vertical page scroll (fit the viewport)", default: true, env: "RULE_NO_VERTICAL_SCROLL" }
6069
+ ];
6070
+ function resolvePolicy() {
6071
+ const rules = {};
6072
+ for (const r of POST_RULES) rules[r.id] = boolEnv2(r.env, r.default);
6073
+ return { enabled: boolEnv2("POST_RESTRICTIONS_ENABLED", true), rules };
6074
+ }
6075
+ var postPolicy = resolvePolicy();
6076
+ function ruleActive(id, policy = postPolicy) {
6077
+ return policy.enabled && policy.rules[id] === true;
6078
+ }
6079
+
6173
6080
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
6174
6081
  var external_exports = {};
6175
6082
  __export(external_exports, {
@@ -10251,8 +10158,19 @@ var createBuildSchema = external_exports.object({
10251
10158
  mode: external_exports.enum(["ai", "remix"]),
10252
10159
  remixParentId: external_exports.string().uuid().optional()
10253
10160
  });
10161
+ var builderAttachmentSchema = external_exports.object({
10162
+ kind: external_exports.enum(["image", "file"]),
10163
+ name: external_exports.string().max(200),
10164
+ dataUrl: external_exports.string().max(8e6).optional(),
10165
+ // ~6MB image as base64 data URL
10166
+ text: external_exports.string().max(5e4).optional()
10167
+ });
10254
10168
  var builderMessageSchema = external_exports.object({
10255
- content: external_exports.string().min(1).max(4e3)
10169
+ content: external_exports.string().max(4e3).default(""),
10170
+ attachments: external_exports.array(builderAttachmentSchema).max(6).optional(),
10171
+ model: external_exports.string().max(100).optional()
10172
+ }).refine((v) => v.content.trim().length > 0 || (v.attachments?.length ?? 0) > 0, {
10173
+ message: "Send a message or an attachment."
10256
10174
  });
10257
10175
  var importSchema = external_exports.object({
10258
10176
  html: external_exports.string().min(1).optional(),
@@ -10263,6 +10181,7 @@ var importSchema = external_exports.object({
10263
10181
  var saveSchema = external_exports.object({
10264
10182
  collectionId: external_exports.string().uuid().optional()
10265
10183
  });
10184
+ var AI_PROVIDERS = ["gapgpt", "moonshot", "openai", "openrouter"];
10266
10185
  var collectionSchema = external_exports.object({
10267
10186
  name: external_exports.string().min(1).max(60)
10268
10187
  });
@@ -10280,8 +10199,8 @@ var patchMeSchema = external_exports.object({
10280
10199
  // once
10281
10200
  });
10282
10201
  var aiConnectionSchema = external_exports.object({
10283
- provider: external_exports.enum(["openai", "anthropic"]),
10284
- apiKey: external_exports.string().min(8)
10202
+ provider: external_exports.enum(AI_PROVIDERS),
10203
+ apiKey: external_exports.string().trim().min(12).max(300)
10285
10204
  });
10286
10205
  var feedQuerySchema = external_exports.object({
10287
10206
  tab: external_exports.enum(["explore", "following"]).default("explore"),
@@ -10297,6 +10216,24 @@ var searchQuerySchema = external_exports.object({
10297
10216
  limit: external_exports.coerce.number().int().min(1).max(30).default(15)
10298
10217
  });
10299
10218
 
10219
+ // ../shared/src/models.ts
10220
+ var BUILDER_MODELS = [
10221
+ // ── Free (selectable now) — good quality, reasonable price ─────────
10222
+ { id: "claude-sonnet-4-20250514", label: "Claude Sonnet 4", tier: "free", note: "Best for polished HTML", vision: true },
10223
+ { id: "gemini-2.5-flash", label: "Gemini 2.5 Flash", tier: "free", note: "Fast \xB7 great UI \xB7 cheap", vision: true },
10224
+ { id: "gpt-4o-mini", label: "GPT-4o mini", tier: "free", note: "Cheap & reliable", vision: true },
10225
+ { id: "gpt-5-mini", label: "GPT-5 mini", tier: "free", note: "Newer, capable", vision: true },
10226
+ { id: "gpt-5.1-codex", label: "GPT-5.1 Codex", tier: "free", note: "Code specialist" },
10227
+ { id: "deepseek-v4-pro", label: "DeepSeek V4 Pro", tier: "free", note: "Strong coder \xB7 great value" },
10228
+ { id: "deepseek-v4-flash", label: "DeepSeek V4 Flash", tier: "free", note: "Fast \xB7 very cheap" },
10229
+ { id: "gapgpt-qwen-3.6", label: "Qwen 3.6", tier: "free", note: "Good value" },
10230
+ // ── Subscription (locked until billing is enabled) — premium ──────
10231
+ { id: "claude-opus-4-8", label: "Claude Opus 4.8", tier: "subscription", note: "Top quality", vision: true },
10232
+ { id: "gpt-5.2-pro", label: "GPT-5.2 Pro", tier: "subscription", note: "Premium" },
10233
+ { id: "gemini-3-pro-preview", label: "Gemini 3 Pro", tier: "subscription", note: "Premium", vision: true }
10234
+ ];
10235
+ var FREE_IDS = new Set(BUILDER_MODELS.filter((m) => m.tier === "free").map((m) => m.id));
10236
+
10300
10237
  // ../shared/src/tokens.ts
10301
10238
  var colors = {
10302
10239
  brand: "#6C4CF1",
@@ -10328,6 +10265,175 @@ var toggle = {
10328
10265
  knobTo: "22px"
10329
10266
  };
10330
10267
 
10268
+ // ../sandbox/src/index.ts
10269
+ var FORBIDDEN_API_PATTERNS = [
10270
+ { re: /getUserMedia/, name: "getUserMedia" },
10271
+ { re: /navigator\s*\.\s*geolocation/, name: "geolocation" },
10272
+ { re: /new\s+Notification|Notification\s*\.\s*requestPermission/, name: "Notification" },
10273
+ { re: /serviceWorker/, name: "serviceWorker" },
10274
+ { re: /openDatabase/, name: "openDatabase" },
10275
+ { re: /PaymentRequest|navigator\s*\.\s*payments/, name: "PaymentRequest" },
10276
+ { re: /navigator\s*\.\s*credentials|PublicKeyCredential|WebAuthn/, name: "WebAuthn" },
10277
+ { re: /requestPointerLock/, name: "pointerLock" }
10278
+ // allowed via gesture only; flag
10279
+ ];
10280
+ var TOP_NAV_PATTERNS = [
10281
+ /window\s*\.\s*top/,
10282
+ /window\s*\.\s*parent\s*\.\s*location/,
10283
+ /\btop\s*\.\s*location/,
10284
+ /\bparent\s*\.\s*location/
10285
+ ];
10286
+ var BRAND_NAMES = [
10287
+ "google",
10288
+ "gmail",
10289
+ "apple",
10290
+ "icloud",
10291
+ "microsoft",
10292
+ "outlook",
10293
+ "paypal",
10294
+ "facebook",
10295
+ "instagram",
10296
+ "telegram",
10297
+ "whatsapp",
10298
+ "metamask",
10299
+ "binance",
10300
+ "coinbase"
10301
+ ];
10302
+ function extractExternalHosts(html) {
10303
+ const scrubbed = html.replace(/\bxmlns(:[a-z0-9-]+)?\s*=\s*("[^"]*"|'[^']*')/gi, "").replace(/<!DOCTYPE[^>]*>/gi, "");
10304
+ const hosts = /* @__PURE__ */ new Set();
10305
+ const re = /https?:\/\/([a-z0-9.-]+)/gi;
10306
+ let m;
10307
+ while (m = re.exec(scrubbed)) {
10308
+ const host = m[1]?.toLowerCase();
10309
+ if (host) hosts.add(host);
10310
+ }
10311
+ return [...hosts];
10312
+ }
10313
+ function looksLikeHtml(root) {
10314
+ const hasEl = root.querySelector("*") != null;
10315
+ return hasEl;
10316
+ }
10317
+ function validate(html, opts) {
10318
+ const errors = [];
10319
+ const warnings = [];
10320
+ const size = Buffer.byteLength(html, "utf8");
10321
+ const policy = opts.policy ?? postPolicy;
10322
+ const on = (id) => ruleActive(id, policy);
10323
+ if (on("size_limit") && size > opts.maxBytes) {
10324
+ errors.push({
10325
+ code: "SIZE_EXCEEDED",
10326
+ message: `Artifact is ${size} bytes; limit is ${opts.maxBytes}.`
10327
+ });
10328
+ }
10329
+ const root = (0, import_node_html_parser.parse)(html, {
10330
+ lowerCaseTagName: true,
10331
+ comment: true,
10332
+ voidTag: { closingSlash: true }
10333
+ });
10334
+ if (!looksLikeHtml(root)) {
10335
+ errors.push({ code: "NOT_HTML", message: "Content is not parseable as HTML." });
10336
+ return { ok: false, errors, warnings };
10337
+ }
10338
+ if (on("frames") && root.querySelector("iframe, frame, object, embed")) {
10339
+ errors.push({
10340
+ code: "FRAME_TAG",
10341
+ message: "Frames are not allowed (iframe/frame/object/embed)."
10342
+ });
10343
+ }
10344
+ if (on("base_tag") && root.querySelector("base")) {
10345
+ errors.push({ code: "BASE_TAG", message: "<base> tags are not allowed." });
10346
+ }
10347
+ if (on("meta_refresh")) {
10348
+ for (const meta of root.querySelectorAll("meta")) {
10349
+ if ((meta.getAttribute("http-equiv") || "").toLowerCase() === "refresh") {
10350
+ errors.push({ code: "META_REFRESH", message: "meta-refresh redirects are not allowed." });
10351
+ break;
10352
+ }
10353
+ }
10354
+ }
10355
+ if (on("password_input")) {
10356
+ for (const input of root.querySelectorAll("input")) {
10357
+ if ((input.getAttribute("type") || "").toLowerCase() === "password") {
10358
+ errors.push({
10359
+ code: "PASSWORD_INPUT",
10360
+ message: 'Password inputs are not allowed (<input type="password">).'
10361
+ });
10362
+ break;
10363
+ }
10364
+ }
10365
+ }
10366
+ if (on("top_navigation")) {
10367
+ for (const re of TOP_NAV_PATTERNS) {
10368
+ if (re.test(html)) {
10369
+ errors.push({
10370
+ code: "TOP_NAVIGATION",
10371
+ message: "Top-level navigation attempts are not allowed.",
10372
+ detail: re.source
10373
+ });
10374
+ break;
10375
+ }
10376
+ }
10377
+ }
10378
+ if (on("forbidden_apis")) {
10379
+ for (const { re, name } of FORBIDDEN_API_PATTERNS) {
10380
+ if (re.test(html)) {
10381
+ errors.push({ code: "FORBIDDEN_API", message: `Forbidden API used: ${name}.`, detail: name });
10382
+ }
10383
+ }
10384
+ }
10385
+ if (on("external_urls")) {
10386
+ const hosts = extractExternalHosts(html);
10387
+ const offending = hosts.filter(
10388
+ (h) => !opts.allowedOrigins.some((allowed) => h === allowed || h.endsWith("." + allowed))
10389
+ );
10390
+ for (const host of offending) {
10391
+ errors.push({
10392
+ code: "EXTERNAL_URL",
10393
+ message: `External URL not in allowlist: ${host}`,
10394
+ detail: host
10395
+ });
10396
+ }
10397
+ }
10398
+ if (on("external_scripts")) {
10399
+ for (const s of root.querySelectorAll("script")) {
10400
+ const src = s.getAttribute("src");
10401
+ if (src && /^(https?:)?\/\//i.test(src.trim())) {
10402
+ errors.push({
10403
+ code: "EXTERNAL_SCRIPT",
10404
+ message: `External script not allowed (inline your code): ${src}`,
10405
+ detail: src
10406
+ });
10407
+ }
10408
+ }
10409
+ }
10410
+ if (on("external_links")) {
10411
+ for (const a of root.querySelectorAll("a")) {
10412
+ const href = (a.getAttribute("href") || "").trim();
10413
+ if (/^(https?:)?\/\//i.test(href)) {
10414
+ errors.push({
10415
+ code: "EXTERNAL_LINK",
10416
+ message: `External link not allowed: ${href}`,
10417
+ detail: href
10418
+ });
10419
+ }
10420
+ }
10421
+ }
10422
+ const lower = html.toLowerCase();
10423
+ const hasCredField = root.querySelector('input[type="email"], input[type="text"], input[type="tel"]') != null && /(password|passcode|otp|verify|sign\s*in|log\s*in|account)/i.test(html);
10424
+ if (hasCredField) {
10425
+ const brand = BRAND_NAMES.find((b) => lower.includes(b));
10426
+ if (brand) {
10427
+ warnings.push({
10428
+ code: "BRAND_CREDENTIAL",
10429
+ message: `Possible phishing: brand "${brand}" near a credential field.`,
10430
+ detail: brand
10431
+ });
10432
+ }
10433
+ }
10434
+ return { ok: errors.length === 0, errors, warnings };
10435
+ }
10436
+
10331
10437
  // ../cli/src/bundle.ts
10332
10438
  var MIME = {
10333
10439
  ".css": "text/css",
@@ -10578,10 +10684,11 @@ async function bundleProject(projectDir, opts = {}) {
10578
10684
  rootDir: dirname(resolved.entry),
10579
10685
  allowedOrigins
10580
10686
  });
10581
- const result = validate(html, { maxBytes, allowedOrigins });
10687
+ const result = validate(html, { maxBytes, allowedOrigins, policy: opts.policy });
10582
10688
  return {
10583
10689
  ok: result.ok,
10584
10690
  html,
10691
+ policy: opts.policy,
10585
10692
  entry: resolved.entry,
10586
10693
  builtWith: resolved.builtWith,
10587
10694
  sizeBytes: Buffer.byteLength(html, "utf8"),
@@ -10601,7 +10708,9 @@ var FIXES = {
10601
10708
  TOP_NAVIGATION: "Remove top-level navigation (window.top / parent.location). The artifact runs sandboxed and can't drive the page it's embedded in.",
10602
10709
  FORBIDDEN_API: "This browser API is blocked in the sandbox (e.g. getUserMedia, geolocation, serviceWorker, Notification, PaymentRequest, WebAuthn). Remove it or gate it behind a no-op fallback.",
10603
10710
  PASSWORD_INPUT: `Password inputs aren't allowed (anti-phishing). Remove <input type="password">.`,
10604
- EXTERNAL_URL: "References a host that isn't on the allowlist \u2014 often a call to your own backend/API or a third-party script. vibed artifacts are a single static file: inline the asset, drop the call, or move it to an allowlisted CDN (fonts.googleapis.com, cdn.jsdelivr.net, unpkg.com, cdnjs.cloudflare.com, fonts.gstatic.com)."
10711
+ EXTERNAL_URL: "References a host that isn't on the allowlist \u2014 often a call to your own backend/API or a third-party script. vibed artifacts are a single static file: inline the asset, drop the call, or move it to an allowlisted CDN (fonts.googleapis.com, cdn.jsdelivr.net, unpkg.com, cdnjs.cloudflare.com, fonts.gstatic.com).",
10712
+ EXTERNAL_SCRIPT: "Loads JavaScript from an external <script src>. Creator-selected packages aren't allowed \u2014 inline the code you need directly into the file.",
10713
+ EXTERNAL_LINK: "Links off the platform (<a href> to another site). Posts can't send viewers elsewhere \u2014 remove the link or make it in-page (e.g. an anchor to a section)."
10605
10714
  };
10606
10715
  var KB = 1024;
10607
10716
  function fmtSize(bytes) {
@@ -10644,6 +10753,12 @@ function formatReport(result) {
10644
10753
  out.push(`\u2717 Not vibeable yet \u2014 ${result.blockers.length} blocker(s):`);
10645
10754
  out.push(...summarize(result).lines);
10646
10755
  }
10756
+ if (result.policy && ruleActive("no_vertical_scroll", result.policy)) {
10757
+ out.push(
10758
+ "",
10759
+ "\u2139 Single screen: the page can't scroll vertically (auto-clipped at publish) \u2014 design it to fit."
10760
+ );
10761
+ }
10647
10762
  if (result.warnings.length) {
10648
10763
  out.push("", "Flags (published but sent to moderation):");
10649
10764
  for (const w of result.warnings) out.push(` \u2022 ${w.message}`);
package/package.json CHANGED
@@ -1,13 +1,16 @@
1
1
  {
2
2
  "name": "@amirhosseinnateghi/vibed-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "vibed MCP server — make it vibed from any MCP client (Codex, Cursor, Gemini, …): check, login, and publish tools",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "vibed-mcp": "./dist/index.js"
9
9
  },
10
- "files": ["dist", "README.md"],
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
11
14
  "publishConfig": {
12
15
  "access": "public"
13
16
  },