@amirhosseinnateghi/vibed-mcp 0.1.8 → 0.2.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.
Files changed (2) hide show
  1. package/dist/index.js +251 -12
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -6083,6 +6083,9 @@ var config = {
6083
6083
  // completion the server drops a notification (§7.1) — so a slow build never
6084
6084
  // traps them staring at a spinner.
6085
6085
  BUILDER_SLOW_MS: intEnv("BUILDER_SLOW_MS", 12e3),
6086
+ /** Lite edits (ف۷) kill switch: set to "off" to force every edit turn back to
6087
+ * whole-file regeneration without deploying code. */
6088
+ BUILDER_LITE_EDITS: strEnv("BUILDER_LITE_EDITS", "on"),
6086
6089
  DEFAULT_LOCALE: strEnv("DEFAULT_LOCALE", "en")
6087
6090
  };
6088
6091
  function strEnv(key, fallback) {
@@ -10222,10 +10225,11 @@ var STORAGE = {
10222
10225
  /** Max records returnable in one query page. */
10223
10226
  QUERY_MAX_LIMIT: intEnv("STORE_QUERY_MAX_LIMIT", 100),
10224
10227
  /**
10225
- * Platform kill-switch for anonymous shared writes. ON by default: the real
10226
- * gate is per-post owner opt-in (`anon_writes` + writePolicy `anyone`), so a
10227
- * post only accepts guest writes when its owner turns them on. Set the env to
10228
- * `false` to disable anon writes platform-wide regardless of per-post config.
10228
+ * Platform kill-switch for anonymous shared writes. ON by default. The real
10229
+ * gate is the per-post config (`anon_writes` + writePolicy `anyone`), which is
10230
+ * guest-writable BY DEFAULT (a shared vibe should work for whoever opens the
10231
+ * link) an owner can lock it down per post. Set the env to `false` to disable
10232
+ * anon writes platform-wide regardless of per-post config.
10229
10233
  */
10230
10234
  ANON_WRITES_AVAILABLE: boolEnv("STORE_ANON_WRITES_AVAILABLE", true)
10231
10235
  };
@@ -10339,9 +10343,39 @@ var patchExperienceSchema = external_exports.object({
10339
10343
  visibility: visibilitySchema.optional(),
10340
10344
  coverMode: coverModeSchema.optional()
10341
10345
  });
10346
+ var COMMENT_MEDIA = {
10347
+ quality: 0.82,
10348
+ maxEdge: 1600,
10349
+ maxBytes: 8 * 1024 * 1024,
10350
+ // video/* is the compressed comment clip (client-side MediaRecorder → mp4 on
10351
+ // iOS, webm on Chromium); the client never sends a raw camera video as-is.
10352
+ types: ["image/webp", "image/jpeg", "image/png", "image/gif", "video/mp4", "video/webm"]
10353
+ };
10354
+ var COMMENT_VIDEO = {
10355
+ maxDurationSec: 60,
10356
+ maxEdge: 720,
10357
+ // 1 Mbps → a 60s clip is ~7.5MB, comfortably under the 10MB request-body cap.
10358
+ bitsPerSecond: 1e6,
10359
+ crf: 30,
10360
+ maxUploadBytes: 120 * 1024 * 1024
10361
+ };
10362
+ var commentMediaSchema = external_exports.object({
10363
+ key: external_exports.string().min(1).max(300),
10364
+ type: external_exports.enum(COMMENT_MEDIA.types),
10365
+ w: external_exports.number().int().positive().max(2e4),
10366
+ h: external_exports.number().int().positive().max(2e4)
10367
+ });
10368
+ var commentVideoSchema = external_exports.object({ key: external_exports.string().min(1).max(300) });
10342
10369
  var commentSchema = external_exports.object({
10343
- body: external_exports.string().min(1).max(500),
10344
- parentId: external_exports.string().uuid().optional()
10370
+ body: external_exports.string().max(500),
10371
+ parentId: external_exports.string().uuid().optional(),
10372
+ media: commentMediaSchema.optional(),
10373
+ // ready image/GIF (compressed client-side)
10374
+ video: commentVideoSchema.optional()
10375
+ // raw video → transcoded async in the worker
10376
+ }).refine((d) => d.body.trim().length > 0 || !!d.media || !!d.video, {
10377
+ message: "Write something or attach a photo or video.",
10378
+ path: ["body"]
10345
10379
  });
10346
10380
  var eventSchema = external_exports.object({
10347
10381
  // "record"/"screenshot" fire when a viewer captures the vibe (see the in-iframe
@@ -10372,6 +10406,9 @@ var createTurnRequestSchema = external_exports.object({
10372
10406
  // A voice turn carries no text yet — the worker transcribes `voice` first (م۷).
10373
10407
  voice: voiceInputSchema.optional(),
10374
10408
  model: external_exports.string().max(100).optional()
10409
+ // No editMode: lite edits (ف۷) proved themselves, so every edit takes that
10410
+ // path and the server decides — a client can't ask for the slow rewrite.
10411
+ // BUILDER_LITE_EDITS=off remains the fleet-wide kill switch.
10375
10412
  }).refine((v) => v.content.trim().length > 0 || (v.attachments?.length ?? 0) > 0 || !!v.voice, {
10376
10413
  message: "Send a message, an attachment, or a voice recording."
10377
10414
  });
@@ -10476,6 +10513,183 @@ var notifPrefsPatchSchema = external_exports.object({
10476
10513
  groups: external_exports.record(external_exports.enum(NOTIF_PREF_GROUPS), external_exports.boolean()).optional()
10477
10514
  });
10478
10515
 
10516
+ // ../shared/src/remix.ts
10517
+ var REMIX_GUIDE = `# Make it remix-friendly
10518
+
10519
+ On vibed every published vibe is **remixable**: anyone can fork it and make it
10520
+ theirs, and the AI builder can be asked to change it. A vibe is only *really*
10521
+ remixable if a stranger \u2014 or a model editing it \u2014 can find the knobs without
10522
+ reading the whole file. Treat this as part of shipping, like mobile-first.
10523
+
10524
+ It pays off twice: edits to a well-structured file are applied as small precise
10525
+ patches instead of regenerating the whole thing, which is faster, cheaper, and
10526
+ far less likely to break what already worked.
10527
+
10528
+ ## 1. One CONFIG block, at the top
10529
+
10530
+ Every tunable lives in a single object at the top of the script \u2014 the first
10531
+ thing anyone sees:
10532
+
10533
+ \`\`\`js
10534
+ const CONFIG = {
10535
+ // --- Feel ---
10536
+ gravity: 0.6,
10537
+ jumpForce: -11,
10538
+ pipeGap: 160,
10539
+ pipeSpeed: 2.4,
10540
+ // --- Look ---
10541
+ skyColor: "#7ec8ff",
10542
+ birdColor: "#ffd166",
10543
+ // --- Words (all user-facing text) ---
10544
+ title: "Tap to Fly",
10545
+ gameOverText: "Ouch! Tap to try again",
10546
+ scoreLabel: "Score",
10547
+ };
10548
+ \`\`\`
10549
+
10550
+ Rules that make it useful:
10551
+ - **Literal values only** \u2014 numbers, strings, colors. No computed expressions.
10552
+ - **Everything reads from CONFIG.** If a number appears twice in the logic, it
10553
+ belongs in CONFIG once.
10554
+ - **All user-facing text goes here too**, so rewording or translating a vibe is
10555
+ one edit, not a hunt.
10556
+
10557
+ ## 2. Content separate from logic
10558
+
10559
+ Questions, levels, items, cards, prompts \u2014 a labelled array right after CONFIG:
10560
+
10561
+ \`\`\`js
10562
+ const QUESTIONS = [
10563
+ { q: "Pick a color", options: ["Red", "Blue"], answer: 0 },
10564
+ // add more here \u2014 same shape
10565
+ ];
10566
+ \`\`\`
10567
+
10568
+ Now "add five more questions" is a data edit anyone can make.
10569
+
10570
+ ## 3. Predictable section order
10571
+
10572
+ Lay the script out in this order, each with a one-line banner comment:
10573
+
10574
+ \`\`\`js
10575
+ // ============ CONFIG ============
10576
+ // ============ CONTENT ===========
10577
+ // ============ STATE =============
10578
+ // ============ HELPERS ===========
10579
+ // ============ RENDER ============
10580
+ // ============ INPUT =============
10581
+ // ============ INIT ==============
10582
+ \`\`\`
10583
+
10584
+ A remixer (and a patching model) then knows where a change belongs before
10585
+ reading a single line.
10586
+
10587
+ ## 4. Write it plainly
10588
+
10589
+ - Descriptive names: \`spawnEnemy()\`, not \`se()\`; \`playerSpeed\`, not \`ps\`.
10590
+ - Small functions that do one thing. One \`init()\` entry point, one game loop.
10591
+ - Two-space indent, no minification, no bundler output, no clever one-liners or
10592
+ chained ternaries.
10593
+ - A short comment at each seam someone would want to change:
10594
+ \`// Win condition \u2014 swap this for a timer, a score target, ...\`
10595
+ - Stable \`id\`/\`class\` names on the elements a remixer would target.
10596
+ - No dead code, and no copy-pasted near-identical blocks \u2014 factor them into one
10597
+ function. (Duplicated blocks also make an automated edit ambiguous, so it
10598
+ fails or lands in the wrong place.)
10599
+
10600
+ ## 5. Quick self-check before publishing
10601
+
10602
+ - Could someone change the colors, speed, and all the words by editing only
10603
+ CONFIG?
10604
+ - Could they add one more level/question/item by editing only the content array?
10605
+ - Is there any magic number or user-facing string outside those two blocks?
10606
+ - Would a stranger know where to add a feature within ten seconds of scrolling?
10607
+
10608
+ If the answer to any of those is no, fix it before you publish \u2014 that is the
10609
+ difference between a vibe people fork and one they scroll past.`;
10610
+ var SMALL_VIBE_CHARS = 1200;
10611
+ var SMALL_VIBE_LINES = 40;
10612
+ var COMMENT_EXPECTED_LINES = 60;
10613
+ var SECTIONS_EXPECTED_LINES = 120;
10614
+ var MINIFIED_LINE_CHARS = 400;
10615
+ var MINIFIED_AVG_LINE_CHARS = 160;
10616
+ function authorScript(html) {
10617
+ const parts = [];
10618
+ const re = /<script\b([^>]*)>([\s\S]*?)<\/script\s*>/gi;
10619
+ let m;
10620
+ while (m = re.exec(html)) {
10621
+ const attrs = m[1] ?? "";
10622
+ if (/data-vibed-bridge/i.test(attrs)) continue;
10623
+ if (/\bsrc=/i.test(attrs)) continue;
10624
+ parts.push(m[2] ?? "");
10625
+ }
10626
+ return parts.join("\n");
10627
+ }
10628
+ function remixAudit(html) {
10629
+ const script = authorScript(html ?? "");
10630
+ const lines = script.split(/\r?\n/);
10631
+ const codeLines = lines.filter((l) => l.trim() !== "");
10632
+ const scriptLines = codeLines.length;
10633
+ const scriptChars = codeLines.reduce((n, l) => n + l.trim().length, 0);
10634
+ const avgLine = scriptLines > 0 ? scriptChars / scriptLines : 0;
10635
+ const findings = [];
10636
+ const longest = codeLines.reduce((n, l) => Math.max(n, l.trim().length), 0);
10637
+ const denselyPacked = scriptChars > 600 && avgLine > MINIFIED_AVG_LINE_CHARS;
10638
+ if (longest > MINIFIED_LINE_CHARS || denselyPacked) {
10639
+ findings.push({
10640
+ code: "MINIFIED",
10641
+ blocking: true,
10642
+ message: `The script averages ${Math.round(avgLine)} characters per line (longest ${longest}) \u2014 this looks minified or bundled.`,
10643
+ fix: "Publish readable source: turn off minification for the vibe build, or inline the unminified code."
10644
+ });
10645
+ }
10646
+ if (scriptLines >= SMALL_VIBE_LINES || scriptChars >= SMALL_VIBE_CHARS) {
10647
+ if (!/\b(?:const|let|var)\s+CONFIG\s*=/.test(script)) {
10648
+ findings.push({
10649
+ code: "NO_CONFIG",
10650
+ blocking: true,
10651
+ message: "No `const CONFIG = { ... }` block \u2014 a remixer has to hunt through the logic to change anything.",
10652
+ fix: "Move every tunable (speeds, sizes, colors, counts, durations) AND all user-facing text into one CONFIG object at the top, and read from it everywhere."
10653
+ });
10654
+ }
10655
+ const cryptic = /* @__PURE__ */ new Set();
10656
+ const decl = /\b(?:const|let|var)\s+([A-Za-z_$])\b(?!\s*[,)])/g;
10657
+ let d;
10658
+ while (d = decl.exec(script)) {
10659
+ const name = d[1];
10660
+ if (!"ijknxyt_".includes(name)) cryptic.add(name);
10661
+ }
10662
+ if (cryptic.size >= 5) {
10663
+ findings.push({
10664
+ code: "CRYPTIC_NAMES",
10665
+ blocking: true,
10666
+ message: `${cryptic.size} single-letter variables (${[...cryptic].slice(0, 6).join(", ")}) \u2014 the code reads as machine output.`,
10667
+ fix: "Give variables and functions descriptive names (`playerSpeed`, not `p`)."
10668
+ });
10669
+ }
10670
+ if (scriptLines >= COMMENT_EXPECTED_LINES) {
10671
+ const comments = codeLines.filter((l) => /^\s*(\/\/|\/\*|\*)/.test(l)).length;
10672
+ if (comments === 0) {
10673
+ findings.push({
10674
+ code: "NO_COMMENTS",
10675
+ blocking: false,
10676
+ message: "The script has no comments at all.",
10677
+ fix: "Add a short note at each seam a remixer would change (the win condition, the spawn rule, the scoring)."
10678
+ });
10679
+ }
10680
+ }
10681
+ if (scriptLines >= SECTIONS_EXPECTED_LINES && !/CONFIG\s*=+\s*$|=== *CONFIG|-- *CONFIG/im.test(script)) {
10682
+ findings.push({
10683
+ code: "NO_SECTIONS",
10684
+ blocking: false,
10685
+ message: "No section banners in a long script.",
10686
+ fix: "Split it with banner comments: CONFIG \u2192 CONTENT \u2192 STATE \u2192 helpers \u2192 render \u2192 input \u2192 init."
10687
+ });
10688
+ }
10689
+ }
10690
+ return { ok: !findings.some((f) => f.blocking), findings, scriptLines };
10691
+ }
10692
+
10479
10693
  // ../shared/src/builderEvents.ts
10480
10694
  var BUILD_EVENT_VERSION = 1;
10481
10695
  var BUILD_PHASES = [
@@ -11133,8 +11347,10 @@ async function bundleProject(projectDir, opts = {}) {
11133
11347
  allowedOrigins
11134
11348
  });
11135
11349
  const result = validate(html, { maxBytes, allowedOrigins, policy: opts.policy });
11350
+ const remix = remixAudit(html);
11136
11351
  return {
11137
- ok: result.ok,
11352
+ ok: result.ok && remix.ok,
11353
+ remix,
11138
11354
  html,
11139
11355
  policy: opts.policy,
11140
11356
  entry: resolved.entry,
@@ -11195,12 +11411,28 @@ function formatReport(result) {
11195
11411
  result.sizeBytes != null ? `size: ${fmtSize(result.sizeBytes)}` : null
11196
11412
  ].filter(Boolean).join(" \xB7 ");
11197
11413
  if (where) out.push(where);
11414
+ const remixBlockers = (result.remix?.findings ?? []).filter((f) => f.blocking);
11198
11415
  if (result.ok) {
11199
11416
  out.push("\u2713 Vibeable \u2014 this project can be published to vibed.");
11200
- } else {
11417
+ } else if (result.blockers.length > 0) {
11201
11418
  out.push(`\u2717 Not vibeable yet \u2014 ${result.blockers.length} blocker(s):`);
11202
11419
  out.push(...summarize(result).lines);
11203
11420
  }
11421
+ if (remixBlockers.length > 0) {
11422
+ out.push("");
11423
+ out.push(`\u2717 Not remixable yet \u2014 ${remixBlockers.length} thing(s) to fix before publishing:`);
11424
+ for (const f of remixBlockers) {
11425
+ out.push(` \u2022 ${f.message}`);
11426
+ out.push(` \u2192 ${f.fix}`);
11427
+ }
11428
+ out.push(" Run `vibed remixable` for the full guide.");
11429
+ }
11430
+ const remixAdvice = (result.remix?.findings ?? []).filter((f) => !f.blocking);
11431
+ if (remixAdvice.length > 0) {
11432
+ out.push("");
11433
+ out.push("Remix polish (not blocking):");
11434
+ for (const f of remixAdvice) out.push(` \u2022 ${f.message} \u2192 ${f.fix}`);
11435
+ }
11204
11436
  if (result.policy && ruleActive("no_vertical_scroll", result.policy)) {
11205
11437
  out.push(
11206
11438
  "",
@@ -11500,7 +11732,8 @@ async function deleteExperience(client, idOrUrl) {
11500
11732
  }
11501
11733
 
11502
11734
  // src/index.ts
11503
- var SERVER_INFO = { name: "vibed", version: "0.1.8" };
11735
+ var MCP_VERSION = true ? "0.2.0" : "0.0.0-dev";
11736
+ var SERVER_INFO = { name: "vibed", version: MCP_VERSION };
11504
11737
  var PROTOCOL_VERSION = "2024-11-05";
11505
11738
  var pendingLogin = null;
11506
11739
  var str = (v, d = "") => typeof v === "string" ? v : d;
@@ -11512,9 +11745,15 @@ var TOOLS = [
11512
11745
  inputSchema: { type: "object", properties: {} },
11513
11746
  run: async () => STORAGE_GUIDE
11514
11747
  },
11748
+ {
11749
+ name: "vibed_remix_guide",
11750
+ description: "How to write a vibe that others can actually REMIX \u2014 vibed's signature loop, where every published vibe is someone else's starting point (and may later be edited by the AI builder). Returns the authoring rules: one CONFIG block at the top holding every tunable and all user-facing text, content/data separated from logic, a predictable section order, and plainly written code. Call this BEFORE writing a vibe, and follow it while building \u2014 the same structure is what lets later edits be applied as small precise patches instead of rewriting the whole file.",
11751
+ inputSchema: { type: "object", properties: {} },
11752
+ run: async () => REMIX_GUIDE
11753
+ },
11515
11754
  {
11516
11755
  name: "vibed_check",
11517
- description: "Bundle the project into a single HTML file and validate it against vibed's sandbox rules (no network/auth). Returns whether it's publishable and, if not, the blockers to fix. Run this before vibed_publish. If it warns BROWSER_STORAGE (localStorage etc. throw in the sandbox), call vibed_storage_guide and port to window.vibed.storage.",
11756
+ description: "Bundle the project into a single HTML file and validate it against vibed's sandbox rules (no network/auth). Returns whether it's publishable and, if not, the blockers to fix. Run this before vibed_publish. If it warns BROWSER_STORAGE (localStorage etc. throw in the sandbox), call vibed_storage_guide and port to window.vibed.storage. This also audits REMIX-FRIENDLINESS (readable code, a CONFIG block holding every tunable and all user-facing text) and REFUSES a file that fails \u2014 call vibed_remix_guide and fix it.",
11518
11757
  inputSchema: {
11519
11758
  type: "object",
11520
11759
  properties: {
@@ -11582,7 +11821,7 @@ After the user approves, call vibed_login_wait.`;
11582
11821
  },
11583
11822
  {
11584
11823
  name: "vibed_draft",
11585
- description: "Bundle the project and upload it as a PRIVATE draft, returning a hosted, sandboxed PREVIEW link (\u2026/p/<id>/) and a draftKey. Publishes nothing \u2014 the link is in no feed, profile, or search; only someone with it can open it. Show the preview to the user, then call vibed_publish with the returned draftKey once they approve. Requires sign-in.",
11824
+ description: "THE DEFAULT for 'make it vibed' / 'publish to vibed': bundle the project and upload it as a PRIVATE draft, returning a hosted, sandboxed PREVIEW link (\u2026/p/<id>/) and a draftKey. Publishes NOTHING \u2014 the link is in no feed, profile, or search; only someone with it can open it. Give the user the preview link, then STOP and let them review \u2014 'make it vibed' means preview first, NOT publish. Only call vibed_publish after the user explicitly tells you to publish this draft. Requires sign-in. BEFORE drafting, make the single HTML file remix-friendly \u2014 a file-top comment, tunable constants (speed/difficulty/colors/text) grouped and labeled near the top, clear names, comments on the non-obvious WHY, readable not minified \u2014 because remix is core to vibed and its AI builder edits THIS file to fork it.",
11586
11825
  inputSchema: {
11587
11826
  type: "object",
11588
11827
  properties: {
@@ -11613,7 +11852,7 @@ Show it to the user. To publish it, call vibed_publish with draftKey: ${d.draftA
11613
11852
  },
11614
11853
  {
11615
11854
  name: "vibed_publish",
11616
- description: "Publish a vibed Experience. Pass draftKey (from vibed_draft) to promote an already-previewed draft \u2014 the recommended flow \u2014 or pass path to bundle + publish in one shot. Requires sign-in (vibed_whoami / vibed_login). Confirm the title and tags with the user first \u2014 publishing is public. Returns the published URL, or the blockers if it isn't vibeable.",
11855
+ description: "Make a draft PUBLIC. Publishing is public and hard to undo \u2014 do NOT call this unless the user has EXPLICITLY told you to publish (e.g. 'publish it', 'post it', 'go ahead'). 'make it vibed' is NOT a publish instruction, and 'looks good' is not either \u2014 when unsure, ask and wait. Default flow: vibed_draft \u2192 user reviews the preview \u2192 user says publish \u2192 this. Pass draftKey (from vibed_draft) to promote the previewed draft (preferred); passing path bundles + publishes in one shot and SKIPS the preview \u2014 only do that if the user asked to skip it. Requires sign-in. Confirm the title and tags first. Returns the published URL.",
11617
11856
  inputSchema: {
11618
11857
  type: "object",
11619
11858
  properties: {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@amirhosseinnateghi/vibed-mcp",
3
- "version": "0.1.8",
4
- "description": "vibed MCP server make it vibed from any MCP client (Codex, Cursor, Gemini, ): check, login, and publish tools",
3
+ "version": "0.2.0",
4
+ "description": "vibed MCP server \u2014 make it vibed from any MCP client (Codex, Cursor, Gemini, \u2026): check, login, and publish tools",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "bin": {
@@ -15,7 +15,7 @@
15
15
  "access": "public"
16
16
  },
17
17
  "scripts": {
18
- "build": "esbuild src/index.ts --bundle --platform=node --target=node20 --format=esm --outfile=dist/index.js && chmod +x dist/index.js",
18
+ "build": "esbuild src/index.ts --bundle --platform=node --target=node20 --format=esm --define:__VIBED_MCP_VERSION__='\"0.2.0\"' --outfile=dist/index.js && chmod +x dist/index.js",
19
19
  "prepublishOnly": "pnpm build",
20
20
  "dev": "tsx src/index.ts",
21
21
  "typecheck": "tsc --noEmit",