@amirhosseinnateghi/vibed-mcp 0.1.2 → 0.1.4

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 +536 -55
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1248,10 +1248,10 @@ var require_decode = __commonJS({
1248
1248
  this.state = EntityDecoderState.NumericDecimal;
1249
1249
  return this.stateNumericDecimal(str2, offset);
1250
1250
  };
1251
- EntityDecoder2.prototype.addToNumericResult = function(str2, start, end, base) {
1251
+ EntityDecoder2.prototype.addToNumericResult = function(str2, start, end, base2) {
1252
1252
  if (start !== end) {
1253
1253
  var digitCount = end - start;
1254
- this.result = this.result * Math.pow(base, digitCount) + parseInt(str2.substr(start, digitCount), base);
1254
+ this.result = this.result * Math.pow(base2, digitCount) + parseInt(str2.substr(start, digitCount), base2);
1255
1255
  this.consumed += digitCount;
1256
1256
  }
1257
1257
  };
@@ -5986,6 +5986,18 @@ import { dirname, extname, isAbsolute, join, resolve } from "node:path";
5986
5986
  // ../sandbox/src/index.ts
5987
5987
  var import_node_html_parser = __toESM(require_dist(), 1);
5988
5988
 
5989
+ // ../shared/src/terms.ts
5990
+ var ITEM = {
5991
+ /** lowercase singular — "vibe" */
5992
+ one: "vibe",
5993
+ /** lowercase plural — "vibes" */
5994
+ many: "vibes",
5995
+ /** Capitalized singular — "Vibe" */
5996
+ One: "Vibe",
5997
+ /** Capitalized plural — "Vibes" */
5998
+ Many: "Vibes"
5999
+ };
6000
+
5989
6001
  // ../shared/src/constants.ts
5990
6002
  var CATEGORIES = [
5991
6003
  "Game",
@@ -5998,10 +6010,13 @@ var CATEGORIES = [
5998
6010
  "Other"
5999
6011
  ];
6000
6012
  var VISIBILITY = ["public", "unlisted"];
6013
+ var COVER_MODES = ["image", "gif"];
6001
6014
  var EXPERIENCE_SOURCE = ["builder", "import", "remix"];
6015
+ var PUSH_PROVIDERS = ["webpush", "fcm", "apns"];
6016
+ var DEVICE_PLATFORMS = ["web", "ios", "android"];
6002
6017
  var config = {
6003
- ARTIFACT_MAX_BYTES: intEnv("ARTIFACT_MAX_BYTES", 3145728),
6004
- // 3 MB
6018
+ ARTIFACT_MAX_BYTES: intEnv("ARTIFACT_MAX_BYTES", 8388608),
6019
+ // 8 MB — headroom for an embedded audio track
6005
6020
  ARTIFACT_ALLOWED_ORIGINS: strEnv(
6006
6021
  "ARTIFACT_ALLOWED_ORIGINS",
6007
6022
  "fonts.googleapis.com,fonts.gstatic.com,cdnjs.cloudflare.com,cdn.jsdelivr.net,unpkg.com"
@@ -6022,8 +6037,33 @@ var config = {
6022
6037
  // Light model that auto-tags the experience category. Falls back to a keyword
6023
6038
  // heuristic if the call fails.
6024
6039
  BUILDER_CLASSIFIER_MODEL: strEnv("BUILDER_CLASSIFIER_MODEL", "gemini-2.5-flash"),
6025
- BUILDER_MAX_TOKENS: intEnv("BUILDER_MAX_TOKENS", 8192),
6040
+ // Speech-to-text model for the builder's voice input (docs/builder-architecture.md).
6041
+ // Gemini 2.5 Flash transcribes the recorded clip via GapGPT's OpenAI-compatible
6042
+ // chat API (an `input_audio` content part) — verified to accept the exact
6043
+ // formats browsers produce (Chrome webm/opus, Safari mp4). It's multilingual,
6044
+ // so Persian and English speech both work with no per-language config.
6045
+ BUILDER_TRANSCRIBE_MODEL: strEnv("BUILDER_TRANSCRIBE_MODEL", "gemini-2.5-flash"),
6046
+ // Upload cap for a voice clip. A minute of webm/opus is ~250KB; 8MB is ample
6047
+ // headroom while still bounding what one request can hand the model.
6048
+ BUILDER_TRANSCRIBE_MAX_BYTES: intEnv("BUILDER_TRANSCRIBE_MAX_BYTES", 8388608),
6049
+ // Output budget per model call. 8k proved too small in prod — a rich game is
6050
+ // ~7-8k tokens of file alone, and a truncated file used to slip through as a
6051
+ // text dump. 24576 was probed OK against every catalog model on GapGPT.
6052
+ BUILDER_MAX_TOKENS: intEnv("BUILDER_MAX_TOKENS", 24576),
6026
6053
  BUILDER_MAX_MESSAGES: intEnv("BUILDER_MAX_MESSAGES", 30),
6054
+ // Hard cap for a single build turn (across the model + one repair). The build
6055
+ // runs detached from the request, so this — not the browser — is what stops a
6056
+ // runaway/abandoned turn. The provider's own 90s per-stall timeout still applies.
6057
+ // Never cut a LIVE generation — these two guards only catch dead work:
6058
+ // stall = total silence (not even a keep-alive byte) long enough to mean the
6059
+ // connection is dead (TCP half-open never errors on its own); the turn cap is
6060
+ // a platform-cost safety ceiling for the whole turn incl. one repair pass.
6061
+ BUILDER_STALL_TIMEOUT_MS: intEnv("BUILDER_STALL_TIMEOUT_MS", 3e5),
6062
+ BUILDER_TURN_TIMEOUT_MS: intEnv("BUILDER_TURN_TIMEOUT_MS", 9e5),
6063
+ // Once a build has run this long, the UI nudges the user to go explore, and on
6064
+ // completion the server drops a notification (§7.1) — so a slow build never
6065
+ // traps them staring at a spinner.
6066
+ BUILDER_SLOW_MS: intEnv("BUILDER_SLOW_MS", 12e3),
6027
6067
  DEFAULT_LOCALE: strEnv("DEFAULT_LOCALE", "en")
6028
6068
  };
6029
6069
  function strEnv(key, fallback) {
@@ -6046,6 +6086,31 @@ function boolEnv(key, fallback) {
6046
6086
  return v === "true" || v === "1";
6047
6087
  }
6048
6088
 
6089
+ // ../shared/src/notifications.ts
6090
+ var NOTIF_VERB = {
6091
+ like_milestone: `liked your ${ITEM.one}`,
6092
+ comment: `commented on your ${ITEM.one}`,
6093
+ reply: "replied to your comment",
6094
+ follow: "started following you",
6095
+ mention: "mentioned you",
6096
+ remix: `remixed your ${ITEM.one}`
6097
+ };
6098
+ var SYSTEM_NOTIF = {
6099
+ build_ready: { icon: "\u2728", text: "Your build is ready \u2014 tap to see it", tone: "info" },
6100
+ build_failed: { icon: "\u26A0\uFE0F", text: "A build didn't finish \u2014 tap to try again", tone: "error" },
6101
+ // Moderation reject (spec §9): the vibe never went live (or was pulled), so
6102
+ // the inbox row must carry everything — title + reason arrive in the payload.
6103
+ post_rejected: { icon: "\u{1F6AB}", text: `Your ${ITEM.one} didn't pass moderation`, tone: "error" }
6104
+ };
6105
+ var NOTIF_PREF_GROUPS = ["follow", "comments", "likes", "remixes", "builder"];
6106
+ var NOTIF_GROUP_LABEL = {
6107
+ follow: { title: "New followers", hint: "when someone follows you" },
6108
+ comments: { title: "Comments", hint: "comments, replies & mentions" },
6109
+ likes: { title: "Likes", hint: `when your ${ITEM.many} hit like milestones` },
6110
+ remixes: { title: "Remixes", hint: `when someone remixes your ${ITEM.one}` },
6111
+ builder: { title: "Builder", hint: "when a build finishes or fails" }
6112
+ };
6113
+
6049
6114
  // ../shared/src/policy.ts
6050
6115
  function boolEnv2(key, fallback) {
6051
6116
  const v = typeof process !== "undefined" ? process.env[key] : void 0;
@@ -9726,23 +9791,23 @@ var ZodEffects = class extends ZodType {
9726
9791
  }
9727
9792
  if (effect.type === "transform") {
9728
9793
  if (ctx.common.async === false) {
9729
- const base = this._def.schema._parseSync({
9794
+ const base2 = this._def.schema._parseSync({
9730
9795
  data: ctx.data,
9731
9796
  path: ctx.path,
9732
9797
  parent: ctx
9733
9798
  });
9734
- if (!isValid(base))
9799
+ if (!isValid(base2))
9735
9800
  return INVALID;
9736
- const result = effect.transform(base.value, checkCtx);
9801
+ const result = effect.transform(base2.value, checkCtx);
9737
9802
  if (result instanceof Promise) {
9738
9803
  throw new Error(`Asynchronous transform encountered during synchronous parse operation. Use .parseAsync instead.`);
9739
9804
  }
9740
9805
  return { status: status.value, value: result };
9741
9806
  } else {
9742
- return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((base) => {
9743
- if (!isValid(base))
9807
+ return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((base2) => {
9808
+ if (!isValid(base2))
9744
9809
  return INVALID;
9745
- return Promise.resolve(effect.transform(base.value, checkCtx)).then((result) => ({
9810
+ return Promise.resolve(effect.transform(base2.value, checkCtx)).then((result) => ({
9746
9811
  status: status.value,
9747
9812
  value: result
9748
9813
  }));
@@ -10118,10 +10183,107 @@ var coerce = {
10118
10183
  };
10119
10184
  var NEVER = INVALID;
10120
10185
 
10186
+ // ../shared/src/storage.ts
10187
+ var STORAGE = {
10188
+ /** Total shared bytes per post (values, serialized). */
10189
+ SHARED_MAX_BYTES: intEnv("STORE_SHARED_MAX_BYTES", 1048576),
10190
+ // 1 MB
10191
+ /** Total shared records (rows) per post. */
10192
+ SHARED_MAX_RECORDS: intEnv("STORE_SHARED_MAX_RECORDS", 5e3),
10193
+ /** Serialized size cap for a single value. */
10194
+ VALUE_MAX_BYTES: intEnv("STORE_VALUE_MAX_BYTES", 16384),
10195
+ // 16 KB
10196
+ /** Records a single author may hold in one post (anti-flood / anti-grief). */
10197
+ AUTHOR_MAX_RECORDS: intEnv("STORE_AUTHOR_MAX_RECORDS", 100),
10198
+ /** Distinct collections per post. */
10199
+ MAX_COLLECTIONS: intEnv("STORE_MAX_COLLECTIONS", 32),
10200
+ /** Local-tier budget per (post, identity) — enforced by the host broker. */
10201
+ LOCAL_MAX_BYTES: intEnv("STORE_LOCAL_MAX_BYTES", 262144),
10202
+ // 256 KB
10203
+ /** Max records returnable in one query page. */
10204
+ QUERY_MAX_LIMIT: intEnv("STORE_QUERY_MAX_LIMIT", 100),
10205
+ /**
10206
+ * Platform kill-switch for anonymous shared writes. ON by default: the real
10207
+ * gate is per-post owner opt-in (`anon_writes` + writePolicy `anyone`), so a
10208
+ * post only accepts guest writes when its owner turns them on. Set the env to
10209
+ * `false` to disable anon writes platform-wide regardless of per-post config.
10210
+ */
10211
+ ANON_WRITES_AVAILABLE: boolEnv("STORE_ANON_WRITES_AVAILABLE", true)
10212
+ };
10213
+ var COLLECTION_RE = /^[a-z0-9][a-z0-9:_-]{0,63}$/;
10214
+ var collectionSchemaStore = external_exports.string().regex(COLLECTION_RE, "invalid collection name");
10215
+ var STORE_KEY_RE = /^[a-zA-Z0-9][a-zA-Z0-9:._-]{0,127}$/;
10216
+ var RESERVED_STORE_KEYS = ["submit", "rank", "increment"];
10217
+ var storeKeySchema = external_exports.string().regex(STORE_KEY_RE, "invalid key").refine((k) => !RESERVED_STORE_KEYS.includes(k), {
10218
+ message: "reserved key"
10219
+ });
10220
+ var STORE_WRITE_POLICIES = ["anyone", "logged_in", "owner_only"];
10221
+ var STORE_SORTS = ["num", "-num", "created", "-created"];
10222
+ var storeValueSchema = external_exports.unknown().refine(
10223
+ (v) => {
10224
+ try {
10225
+ const s = JSON.stringify(v);
10226
+ return s !== void 0 && byteLength(s) <= STORAGE.VALUE_MAX_BYTES;
10227
+ } catch {
10228
+ return false;
10229
+ }
10230
+ },
10231
+ { message: `value must be JSON-serializable and <= ${STORAGE.VALUE_MAX_BYTES} bytes` }
10232
+ );
10233
+ var storePutSchema = external_exports.object({
10234
+ value: storeValueSchema,
10235
+ /** Optimistic concurrency: fail with `conflict` unless current version matches. */
10236
+ ifVersion: external_exports.number().int().positive().optional()
10237
+ });
10238
+ var storeAppendSchema = external_exports.object({
10239
+ value: storeValueSchema,
10240
+ /** Optional numeric field to index (sorting/leaderboards). */
10241
+ num: external_exports.number().finite().optional()
10242
+ });
10243
+ var storeIncrementSchema = external_exports.object({
10244
+ delta: external_exports.number().int().min(-1e6).max(1e6).default(1)
10245
+ });
10246
+ var storeSubmitSchema = external_exports.object({
10247
+ /** The number that ranks this author (mapped into `num`). */
10248
+ score: external_exports.number().finite(),
10249
+ /** Conflict rule when the author already has a row. */
10250
+ keep: external_exports.enum(["max", "min", "last"]).default("max"),
10251
+ /** Small display payload stored alongside (same value-size gate). */
10252
+ meta: storeValueSchema.optional()
10253
+ });
10254
+ var storeQuerySchema = external_exports.object({
10255
+ sort: external_exports.enum(STORE_SORTS).default("-created"),
10256
+ limit: external_exports.coerce.number().int().min(1).max(STORAGE.QUERY_MAX_LIMIT).default(30),
10257
+ cursor: external_exports.string().max(200).optional(),
10258
+ /** Only the caller's records. */
10259
+ mine: external_exports.coerce.boolean().default(false)
10260
+ });
10261
+ var storeAdminSchema = external_exports.discriminatedUnion("action", [
10262
+ external_exports.object({ action: external_exports.literal("freeze") }),
10263
+ external_exports.object({ action: external_exports.literal("unfreeze") }),
10264
+ external_exports.object({ action: external_exports.literal("set_policy"), writePolicy: external_exports.enum(STORE_WRITE_POLICIES) }),
10265
+ external_exports.object({ action: external_exports.literal("set_anon_writes"), anonWrites: external_exports.boolean() }),
10266
+ external_exports.object({ action: external_exports.literal("reset"), collection: collectionSchemaStore.optional() })
10267
+ ]);
10268
+ var storageOptInSchema = external_exports.object({
10269
+ local: external_exports.boolean().default(false),
10270
+ shared: external_exports.boolean().default(false)
10271
+ });
10272
+ function byteLength(s) {
10273
+ if (typeof TextEncoder !== "undefined") return new TextEncoder().encode(s).length;
10274
+ let n = 0;
10275
+ for (let i = 0; i < s.length; i++) {
10276
+ const c = s.codePointAt(i);
10277
+ n += c <= 127 ? 1 : c <= 2047 ? 2 : c <= 65535 ? 3 : (i++, 4);
10278
+ }
10279
+ return n;
10280
+ }
10281
+
10121
10282
  // ../shared/src/schemas.ts
10122
10283
  var handleSchema = external_exports.string().regex(/^[a-z0-9_.]{3,24}$/, "3\u201324 chars: a\u2013z, 0\u20139, _ or .");
10123
10284
  var categorySchema = external_exports.enum(CATEGORIES);
10124
10285
  var visibilitySchema = external_exports.enum(VISIBILITY);
10286
+ var coverModeSchema = external_exports.enum(COVER_MODES);
10125
10287
  var sourceSchema = external_exports.enum(EXPERIENCE_SOURCE);
10126
10288
  var tagsSchema = external_exports.array(external_exports.string().min(1).max(40)).max(10).transform(
10127
10289
  (tags) => Array.from(
@@ -10135,8 +10297,12 @@ var publishSchema = external_exports.object({
10135
10297
  tags: tagsSchema,
10136
10298
  remixable: external_exports.boolean().default(true),
10137
10299
  visibility: visibilitySchema.default("public"),
10300
+ coverMode: coverModeSchema.default("gif"),
10138
10301
  draftArtifactKey: external_exports.string().min(1),
10139
- buildSessionId: external_exports.string().uuid().optional()
10302
+ buildSessionId: external_exports.string().uuid().optional(),
10303
+ /** Storage opt-in override; when omitted, publish auto-detects from the source
10304
+ * (docs/post-storage.md §9 — default off, enabled only when actually used). */
10305
+ storage: storageOptInSchema.optional()
10140
10306
  });
10141
10307
  var patchExperienceSchema = external_exports.object({
10142
10308
  title: external_exports.string().min(1).max(120).optional(),
@@ -10144,25 +10310,44 @@ var patchExperienceSchema = external_exports.object({
10144
10310
  tags: tagsSchema.optional(),
10145
10311
  category: categorySchema.optional(),
10146
10312
  remixable: external_exports.boolean().optional(),
10147
- visibility: visibilitySchema.optional()
10313
+ visibility: visibilitySchema.optional(),
10314
+ coverMode: coverModeSchema.optional()
10148
10315
  });
10149
10316
  var commentSchema = external_exports.object({
10150
10317
  body: external_exports.string().min(1).max(500),
10151
10318
  parentId: external_exports.string().uuid().optional()
10152
10319
  });
10153
10320
  var eventSchema = external_exports.object({
10154
- kind: external_exports.enum(["impression", "play", "play_time", "share"]),
10321
+ // "record"/"screenshot" fire when a viewer captures the vibe (see the in-iframe
10322
+ // bridge capture flow, docs/architecture.md §5.4). Plain-text `viewEvents.kind`
10323
+ // column, so new kinds need no migration.
10324
+ kind: external_exports.enum(["impression", "play", "play_time", "share", "record", "screenshot"]),
10155
10325
  ms: external_exports.number().int().nonnegative().optional()
10156
10326
  });
10157
10327
  var createBuildSchema = external_exports.object({
10158
10328
  mode: external_exports.enum(["ai", "remix"]),
10159
10329
  remixParentId: external_exports.string().uuid().optional()
10160
10330
  });
10331
+ var turnAttachmentSchema = external_exports.object({
10332
+ kind: external_exports.enum(["image", "audio", "file", "sketch"]),
10333
+ key: external_exports.string().max(300).default(""),
10334
+ name: external_exports.string().max(200),
10335
+ // NUL bytes break Postgres jsonb (and mean binary-not-text) — strip defensively.
10336
+ text: external_exports.string().max(5e4).transform((t) => t.replace(/\u0000/g, "")).optional()
10337
+ });
10338
+ var createTurnRequestSchema = external_exports.object({
10339
+ clientTurnId: external_exports.string().uuid(),
10340
+ content: external_exports.string().max(4e3).default(""),
10341
+ attachments: external_exports.array(turnAttachmentSchema).max(6).optional(),
10342
+ model: external_exports.string().max(100).optional()
10343
+ }).refine((v) => v.content.trim().length > 0 || (v.attachments?.length ?? 0) > 0, {
10344
+ message: "Send a message or an attachment."
10345
+ });
10161
10346
  var builderAttachmentSchema = external_exports.object({
10162
- kind: external_exports.enum(["image", "file"]),
10347
+ kind: external_exports.enum(["image", "file", "audio"]),
10163
10348
  name: external_exports.string().max(200),
10164
- dataUrl: external_exports.string().max(8e6).optional(),
10165
- // ~6MB image as base64 data URL
10349
+ // base64 data URL for image/audio (~8.7MB raw fits, covering a short song).
10350
+ dataUrl: external_exports.string().max(12e6).optional(),
10166
10351
  text: external_exports.string().max(5e4).optional()
10167
10352
  });
10168
10353
  var builderMessageSchema = external_exports.object({
@@ -10194,10 +10379,21 @@ var reportSchema = external_exports.object({
10194
10379
  var patchMeSchema = external_exports.object({
10195
10380
  displayName: external_exports.string().min(1).max(60).optional(),
10196
10381
  bio: external_exports.string().max(300).optional(),
10197
- avatar: external_exports.string().url().optional(),
10382
+ // A stored image URL, or "" to remove the photo and fall back to the color avatar.
10383
+ avatar: external_exports.string().url().or(external_exports.literal("")).optional(),
10384
+ // Fallback avatar colour (used when there's no photo). Hex like #6C4CF1.
10385
+ avatarBg: external_exports.string().regex(/^#[0-9a-fA-F]{6}$/, "hex colour, e.g. #6C4CF1").optional(),
10198
10386
  handle: handleSchema.optional()
10199
10387
  // once
10200
10388
  });
10389
+ var waitlistSchema = external_exports.object({
10390
+ email: external_exports.string().trim().email().max(200),
10391
+ note: external_exports.string().trim().max(500).optional()
10392
+ });
10393
+ var waitlistReviewSchema = external_exports.object({
10394
+ requestId: external_exports.string().uuid(),
10395
+ action: external_exports.enum(["approve", "reject"])
10396
+ });
10201
10397
  var aiConnectionSchema = external_exports.object({
10202
10398
  provider: external_exports.enum(AI_PROVIDERS),
10203
10399
  apiKey: external_exports.string().trim().min(12).max(300)
@@ -10211,28 +10407,225 @@ var feedQuerySchema = external_exports.object({
10211
10407
  var searchQuerySchema = external_exports.object({
10212
10408
  q: external_exports.string().default(""),
10213
10409
  tab: external_exports.enum(["experiences", "tags", "creators"]).default("experiences"),
10214
- remixableOnly: external_exports.coerce.boolean().default(false),
10410
+ // Query params arrive as strings; z.coerce.boolean() is just Boolean(x), so
10411
+ // "false"/"0" would (wrongly) become true and the filter could never be turned
10412
+ // off. Parse the string explicitly: only "true"/"1" (or a real boolean) is true.
10413
+ remixableOnly: external_exports.union([external_exports.boolean(), external_exports.string()]).optional().transform((v) => v === true || v === "true" || v === "1"),
10215
10414
  cursor: external_exports.string().optional(),
10216
10415
  limit: external_exports.coerce.number().int().min(1).max(30).default(15)
10217
10416
  });
10417
+ var registerPushDeviceSchema = external_exports.object({
10418
+ provider: external_exports.enum(PUSH_PROVIDERS),
10419
+ token: external_exports.string().trim().min(16).max(4096),
10420
+ keys: external_exports.record(external_exports.string().max(512)).default({}),
10421
+ platform: external_exports.enum(DEVICE_PLATFORMS).default("web"),
10422
+ userAgent: external_exports.string().trim().max(400).default("")
10423
+ }).superRefine((v, ctx) => {
10424
+ if (v.provider === "webpush" && (!v.keys.p256dh || !v.keys.auth)) {
10425
+ ctx.addIssue({
10426
+ code: external_exports.ZodIssueCode.custom,
10427
+ path: ["keys"],
10428
+ message: "webpush requires keys.p256dh and keys.auth"
10429
+ });
10430
+ }
10431
+ });
10432
+ var unregisterPushDeviceSchema = external_exports.object({
10433
+ token: external_exports.string().trim().min(16).max(4096)
10434
+ });
10435
+ var notificationsQuerySchema = external_exports.object({
10436
+ filter: external_exports.enum(["all", "mentions", "remixes"]).default("all"),
10437
+ // Keyset cursor: the `createdAt` ISO timestamp of the last item seen.
10438
+ cursor: external_exports.string().datetime({ offset: true }).optional(),
10439
+ limit: external_exports.coerce.number().int().min(1).max(80).default(30)
10440
+ });
10441
+ var notifPrefsPatchSchema = external_exports.object({
10442
+ enabled: external_exports.boolean().optional(),
10443
+ groups: external_exports.record(external_exports.enum(NOTIF_PREF_GROUPS), external_exports.boolean()).optional()
10444
+ });
10218
10445
 
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 }
10446
+ // ../shared/src/builderEvents.ts
10447
+ var BUILD_EVENT_VERSION = 1;
10448
+ var BUILD_PHASES = [
10449
+ "queued",
10450
+ // accepted, waiting for a worker slot
10451
+ "thinking",
10452
+ // model connected, no visible output yet (keep-alives flowing)
10453
+ "writing",
10454
+ // streaming the vibe's code/prose
10455
+ "validating",
10456
+ // sandbox pass on the produced artifact
10457
+ "repairing"
10458
+ // one automatic fix round after a validation failure
10234
10459
  ];
10235
- var FREE_IDS = new Set(BUILDER_MODELS.filter((m) => m.tier === "free").map((m) => m.id));
10460
+ var base = { v: external_exports.literal(BUILD_EVENT_VERSION), turnId: external_exports.string() };
10461
+ var buildEventSchema = external_exports.discriminatedUnion("type", [
10462
+ /** Assistant prose delta (never contains the HTML code block). */
10463
+ external_exports.object({ ...base, type: external_exports.literal("token"), text: external_exports.string() }),
10464
+ /**
10465
+ * Honest heartbeat, ~every 2s while the turn runs: real elapsed ms, real
10466
+ * output-token count, and the executor's actual phase. The UI renders this
10467
+ * as a live counter — a number that moves because work is happening.
10468
+ */
10469
+ external_exports.object({
10470
+ ...base,
10471
+ type: external_exports.literal("progress"),
10472
+ phase: external_exports.enum(BUILD_PHASES),
10473
+ elapsedMs: external_exports.number(),
10474
+ tokensOut: external_exports.number()
10475
+ }),
10476
+ /** Friendly status chip ("index.html", "Fixing a couple of things…"). */
10477
+ external_exports.object({ ...base, type: external_exports.literal("status"), summary: external_exports.string() }),
10478
+ /** First-message category classification. */
10479
+ external_exports.object({ ...base, type: external_exports.literal("category"), category: external_exports.string() }),
10480
+ /** The validated draft landed. Preview loads it from the draft URL. */
10481
+ external_exports.object({ ...base, type: external_exports.literal("artifact"), artifactKey: external_exports.string() }),
10482
+ /** Terminal: failure with taxonomy code + human message. */
10483
+ external_exports.object({
10484
+ ...base,
10485
+ type: external_exports.literal("error"),
10486
+ code: external_exports.string(),
10487
+ message: external_exports.string()
10488
+ }),
10489
+ /** Terminal: success. */
10490
+ external_exports.object({ ...base, type: external_exports.literal("done") })
10491
+ ]);
10492
+
10493
+ // ../shared/src/i18n.ts
10494
+ var en = {
10495
+ "nav.home": "Home",
10496
+ "nav.search": "Search",
10497
+ "nav.create": "Create",
10498
+ "nav.notifications": "Notifications",
10499
+ "nav.profile": "Profile",
10500
+ "feed.explore": "Explore",
10501
+ "feed.following": "Following",
10502
+ "feed.tapToInteract": "Tap to interact",
10503
+ "feed.loading": `Loading ${ITEM.one}`,
10504
+ "feed.exit": "Exit",
10505
+ "feed.remixedFrom": "Remixed from @{handle}",
10506
+ "feed.remixedFromRemoved": `Remixed from a removed ${ITEM.one}`,
10507
+ "feed.newVersionOf": "New version of {title}",
10508
+ "feed.end.title": "You're all caught up",
10509
+ "feed.end.body": `No new ${ITEM.many} right now \u2014 remix one you loved, or check back soon.`,
10510
+ "feed.end.remix": "Find something to remix",
10511
+ "feed.end.top": "Back to top",
10512
+ "rail.like": "Like",
10513
+ "rail.comment": "Comment",
10514
+ "rail.remix": "Remix",
10515
+ "rail.save": "Save",
10516
+ "rail.share": "Share",
10517
+ "comments.title": "Comments",
10518
+ "comments.placeholder": "Add a comment\u2026",
10519
+ "comments.reply": "Reply",
10520
+ "comments.empty": "No comments yet \u2014 be the first.",
10521
+ "comments.post": "Post",
10522
+ "share.title": "Share",
10523
+ "share.copyLink": "Copy link",
10524
+ "share.copied": "Link copied",
10525
+ "capture.record": "Record",
10526
+ "capture.screenshot": "Screenshot",
10527
+ "capture.recording": "Recording",
10528
+ "capture.stop": "Stop",
10529
+ "capture.preparing": "Preparing\u2026",
10530
+ "capture.processing": "Finishing\u2026",
10531
+ "capture.preview.videoTitle": "Your clip",
10532
+ "capture.preview.imageTitle": "Your screenshot",
10533
+ "capture.save": "Save",
10534
+ "capture.shareVia": "Share via\u2026",
10535
+ "capture.retry": "Try again",
10536
+ "capture.error": "Capture failed \u2014 try again.",
10537
+ "capture.allowShare": "Allow \u201CShare this tab\u201D to record \u2014 it captures just the vibe.",
10538
+ "capture.pickTab": "Choose \u201CThis Tab\u201D so it records only the vibe.",
10539
+ "capture.unsupported": `This ${ITEM.one} can't be captured yet. Try again in a moment.`,
10540
+ "create.title": "Create",
10541
+ "create.buildWithAI": "Build with AI",
10542
+ "create.import": "Import",
10543
+ "create.remix": "Remix something",
10544
+ "create.prompt.placeholder": "Describe what you want to build\u2026",
10545
+ "create.prompt.starters": "Start from a template",
10546
+ "builder.publish": "Publish",
10547
+ "builder.thinking": "Building\u2026",
10548
+ "builder.updated": "Updated \u2713",
10549
+ "builder.placeholder": "Ask for a change\u2026",
10550
+ "builder.mockNotice": "Preview builder \u2014 AI generation is mocked for now.",
10551
+ "import.title": "Import",
10552
+ "import.paste": "Paste HTML, or drop an .html file",
10553
+ "import.validate": "Validate",
10554
+ "import.warnings": "Warnings",
10555
+ "publish.title": "Publish",
10556
+ "publish.postTitle": "Title",
10557
+ "publish.caption": "Caption",
10558
+ "publish.category": "Category",
10559
+ "publish.remixable": "Allow remixes",
10560
+ "publish.public": "Public",
10561
+ "publish.unlisted": "Unlisted",
10562
+ "publish.cta": "Publish",
10563
+ "publish.remixCredit": "Remixing @{handle} \u2014 {title}",
10564
+ "publish.toast.published": "Published! \u{1F389}",
10565
+ "publish.toast.remix": "Remix published \u{1F501} @{handle} was credited",
10566
+ "remix.confirmTitle": "Remixing @{handle}'s {title}",
10567
+ "remix.confirmBody": `They'll be credited on your ${ITEM.one}. Attribution can't be removed.`,
10568
+ "remix.start": "Start remixing",
10569
+ "remix.galleryTitle": "Remixes",
10570
+ "remix.sortPopular": "Popular",
10571
+ "remix.sortRecent": "Recent",
10572
+ "remix.count": "{n} remixes",
10573
+ "remix.identicalWarn": "This looks identical to the original \u2014 publish anyway?",
10574
+ "search.placeholder": `Search ${ITEM.many}, tags, creators`,
10575
+ "search.tab.experiences": ITEM.Many,
10576
+ "search.tab.tags": "Hashtags",
10577
+ "search.tab.creators": "Creators",
10578
+ "search.remixableOnly": "Remixable only",
10579
+ "search.trending": "Trending",
10580
+ "profile.tab.experiences": ITEM.Many,
10581
+ "profile.tab.remixes": "Remixes",
10582
+ "profile.tab.saved": "Saved",
10583
+ "profile.followers": "followers",
10584
+ "profile.following": "following",
10585
+ "profile.likes": "likes",
10586
+ "profile.follow": "Follow",
10587
+ "profile.unfollow": "Following",
10588
+ "profile.edit": "Edit profile",
10589
+ "notifs.all": "All",
10590
+ "notifs.mentions": "Mentions",
10591
+ "notifs.remixes": "Remixes",
10592
+ "notifs.remixedYou": `@{handle} remixed your ${ITEM.one}`,
10593
+ "notifs.liked": `@{handle} liked your ${ITEM.one}`,
10594
+ "notifs.commented": "@{handle} commented",
10595
+ "notifs.followed": "@{handle} started following you",
10596
+ "notifs.empty": "Nothing here yet.",
10597
+ "collections.title": "Saved",
10598
+ "collections.new": "New collection",
10599
+ "analytics.title": "Analytics",
10600
+ "analytics.views": "Views",
10601
+ "analytics.plays": "Plays",
10602
+ "analytics.playRate": "Play rate",
10603
+ "analytics.avgTime": "Avg. time",
10604
+ "analytics.likes": "Likes",
10605
+ "analytics.comments": "Comments",
10606
+ "analytics.shares": "Shares",
10607
+ "analytics.saves": "Saves",
10608
+ "analytics.remixes": "Remixes",
10609
+ "post.options": `${ITEM.One} options`,
10610
+ "post.analytics": "View analytics",
10611
+ "post.edit": "Edit caption",
10612
+ "post.delete": `Delete ${ITEM.one}`,
10613
+ "post.deleteConfirm": `Delete this ${ITEM.one}? This can't be undone.`,
10614
+ "post.captionPlaceholder": "Write a caption\u2026",
10615
+ "auth.signIn": "Sign in",
10616
+ "auth.google": "Continue with Google",
10617
+ "auth.github": "Continue with GitHub",
10618
+ "auth.dev": "Dev login",
10619
+ "auth.needAccount": "Sign in to like, comment, and remix.",
10620
+ "common.cancel": "Cancel",
10621
+ "common.save": "Save",
10622
+ "common.back": "Back",
10623
+ "common.retry": "Retry",
10624
+ "common.loading": "Loading\u2026",
10625
+ "common.report": "Report",
10626
+ "common.delete": "Delete",
10627
+ "common.views": "{n} views"
10628
+ };
10236
10629
 
10237
10630
  // ../shared/src/tokens.ts
10238
10631
  var colors = {
@@ -10335,6 +10728,15 @@ function validate(html, opts) {
10335
10728
  errors.push({ code: "NOT_HTML", message: "Content is not parseable as HTML." });
10336
10729
  return { ok: false, errors, warnings };
10337
10730
  }
10731
+ const scriptOpens = (html.match(/<script\b/gi) ?? []).length;
10732
+ const scriptCloses = (html.match(/<\/script\s*>/gi) ?? []).length;
10733
+ if (scriptOpens > scriptCloses) {
10734
+ errors.push({
10735
+ code: "TRUNCATED_DOC",
10736
+ message: "The file ends mid-<script> \u2014 it looks cut off. Regenerate or paste the complete file."
10737
+ });
10738
+ return { ok: false, errors, warnings };
10739
+ }
10338
10740
  if (on("frames") && root.querySelector("iframe, frame, object, embed")) {
10339
10741
  errors.push({
10340
10742
  code: "FRAME_TAG",
@@ -10419,6 +10821,12 @@ function validate(html, opts) {
10419
10821
  }
10420
10822
  }
10421
10823
  }
10824
+ if (/(localStorage|sessionStorage|indexedDB|document\s*\.\s*cookie)/.test(html)) {
10825
+ warnings.push({
10826
+ code: "BROWSER_STORAGE",
10827
+ message: "Uses browser storage (localStorage/sessionStorage/indexedDB/cookies), which throws inside the vibed sandbox. Port persistence to window.vibed.storage (local per-viewer, shared collective)."
10828
+ });
10829
+ }
10422
10830
  const lower = html.toLowerCase();
10423
10831
  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
10832
  if (hasCredField) {
@@ -10932,22 +11340,38 @@ function normalizeCategory(input) {
10932
11340
  const match = CATEGORIES.find((c) => c.toLowerCase() === input.toLowerCase());
10933
11341
  return match ?? "Other";
10934
11342
  }
10935
- async function publishArtifact(client, html, meta, projectDir) {
10936
- const imported = await apiFetch(client, "POST", "/imports", { html });
11343
+ async function importDraft(client, html) {
11344
+ const r = await apiFetch(client, "POST", "/imports", { html });
11345
+ return { draftArtifactKey: r.draftArtifactKey, previewUrl: r.previewUrl };
11346
+ }
11347
+ async function publishDraft(client, draftArtifactKey, meta, html, projectDir) {
10937
11348
  const exp = await apiFetch(client, "POST", "/experiences", {
10938
- title: meta.title?.trim() || inferTitle(html, projectDir),
11349
+ title: meta.title?.trim() || (html ? inferTitle(html, projectDir ?? ".") : "Untitled"),
10939
11350
  caption: meta.caption ?? "",
10940
11351
  category: normalizeCategory(meta.category),
10941
11352
  tags: meta.tags ?? [],
10942
11353
  remixable: meta.remixable ?? true,
10943
11354
  visibility: meta.visibility ?? "public",
10944
- draftArtifactKey: imported.draftArtifactKey
11355
+ draftArtifactKey
10945
11356
  });
10946
11357
  return { id: exp.id, status: exp.status, url: `${client.apiBase}/e/${exp.id}` };
10947
11358
  }
11359
+ async function publishArtifact(client, html, meta, projectDir) {
11360
+ const { draftArtifactKey } = await importDraft(client, html);
11361
+ return publishDraft(client, draftArtifactKey, meta, html, projectDir);
11362
+ }
11363
+ function experienceIdFrom(input) {
11364
+ const m = input.match(/\/e\/([0-9a-f-]{36})/i);
11365
+ return (m?.[1] ?? input.trim()).toLowerCase();
11366
+ }
11367
+ async function deleteExperience(client, idOrUrl) {
11368
+ const id = experienceIdFrom(idOrUrl);
11369
+ await apiFetch(client, "DELETE", `/experiences/${id}`);
11370
+ return id;
11371
+ }
10948
11372
 
10949
11373
  // src/index.ts
10950
- var SERVER_INFO = { name: "vibed", version: "0.1.0" };
11374
+ var SERVER_INFO = { name: "vibed", version: "0.1.4" };
10951
11375
  var PROTOCOL_VERSION = "2024-11-05";
10952
11376
  var CATEGORIES2 = ["Game", "Invitation", "Story", "News", "Quiz", "Tool", "Art", "Other"];
10953
11377
  var pendingLogin = null;
@@ -11022,12 +11446,47 @@ After the user approves, call vibed_login_wait.`;
11022
11446
  }
11023
11447
  }
11024
11448
  },
11449
+ {
11450
+ name: "vibed_draft",
11451
+ 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.",
11452
+ inputSchema: {
11453
+ type: "object",
11454
+ properties: {
11455
+ path: { type: "string", description: "Project directory (default: current directory)" },
11456
+ entry: { type: "string" },
11457
+ build: { type: "boolean" },
11458
+ api: { type: "string", description: "API base URL (default: production)" }
11459
+ }
11460
+ },
11461
+ run: async (a) => {
11462
+ const dir2 = resolve2(str(a.path, "."));
11463
+ const cfg = await loadConfig({ api: str(a.api) || void 0 });
11464
+ if (!cfg.token) return "Not signed in. Use vibed_login first.";
11465
+ const result = await bundleProject(dir2, {
11466
+ entry: str(a.entry) || void 0,
11467
+ build: bool(a.build, true),
11468
+ onLog: (m) => process.stderr.write(m + "\n")
11469
+ });
11470
+ if (!result.ok || !result.html) {
11471
+ return `No preview \u2014 the project isn't vibeable yet.
11472
+
11473
+ ${formatReport(result)}`;
11474
+ }
11475
+ const d = await importDraft({ apiBase: cfg.apiBase, token: cfg.token }, result.html);
11476
+ return `\u2713 Private preview (not published): ${d.previewUrl ?? "(no preview URL)"}
11477
+ Show it to the user. To publish it, call vibed_publish with draftKey: ${d.draftArtifactKey}`;
11478
+ }
11479
+ },
11025
11480
  {
11026
11481
  name: "vibed_publish",
11027
- description: "Bundle the project and publish it as a vibed Experience. Requires sign-in (vibed_whoami / vibed_login). Confirm the title and category with the user first \u2014 publishing is public. Returns the published URL, or the blockers if it isn't vibeable.",
11482
+ 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 category with the user first \u2014 publishing is public. Returns the published URL, or the blockers if it isn't vibeable.",
11028
11483
  inputSchema: {
11029
11484
  type: "object",
11030
11485
  properties: {
11486
+ draftKey: {
11487
+ type: "string",
11488
+ description: "Promote this draft (from vibed_draft) instead of bundling path"
11489
+ },
11031
11490
  path: { type: "string", description: "Project directory (default: current directory)" },
11032
11491
  title: { type: "string", description: "Title (default: page <title>/<h1>/folder name)" },
11033
11492
  category: { type: "string", enum: CATEGORIES2, description: "Experience category" },
@@ -11041,9 +11500,23 @@ After the user approves, call vibed_login_wait.`;
11041
11500
  }
11042
11501
  },
11043
11502
  run: async (a) => {
11044
- const dir2 = resolve2(str(a.path, "."));
11045
11503
  const cfg = await loadConfig({ api: str(a.api) || void 0 });
11046
11504
  if (!cfg.token) return "Not signed in. Use vibed_login first.";
11505
+ const client = { apiBase: cfg.apiBase, token: cfg.token };
11506
+ const meta = {
11507
+ title: str(a.title) || void 0,
11508
+ category: str(a.category) || void 0,
11509
+ tags: Array.isArray(a.tags) ? a.tags : void 0,
11510
+ caption: str(a.caption) || void 0,
11511
+ remixable: bool(a.remixable, true),
11512
+ visibility: bool(a.unlisted, false) ? "unlisted" : "public"
11513
+ };
11514
+ const draftKey = str(a.draftKey);
11515
+ if (draftKey) {
11516
+ const outcome2 = await publishDraft(client, draftKey, meta);
11517
+ return `\u2713 Published (${outcome2.status}): ${outcome2.url}`;
11518
+ }
11519
+ const dir2 = resolve2(str(a.path, "."));
11047
11520
  const result = await bundleProject(dir2, {
11048
11521
  entry: str(a.entry) || void 0,
11049
11522
  build: bool(a.build, true),
@@ -11054,21 +11527,29 @@ After the user approves, call vibed_login_wait.`;
11054
11527
 
11055
11528
  ${formatReport(result)}`;
11056
11529
  }
11057
- const outcome = await publishArtifact(
11058
- { apiBase: cfg.apiBase, token: cfg.token },
11059
- result.html,
11060
- {
11061
- title: str(a.title) || void 0,
11062
- category: str(a.category) || void 0,
11063
- tags: Array.isArray(a.tags) ? a.tags : void 0,
11064
- caption: str(a.caption) || void 0,
11065
- remixable: bool(a.remixable, true),
11066
- visibility: bool(a.unlisted, false) ? "unlisted" : "public"
11067
- },
11068
- dir2
11069
- );
11530
+ const outcome = await publishArtifact(client, result.html, meta, dir2);
11070
11531
  return `\u2713 Published (${outcome.status}): ${outcome.url}`;
11071
11532
  }
11533
+ },
11534
+ {
11535
+ name: "vibed_delete",
11536
+ description: "Delete one of your vibed posts, by experience id or a \u2026/e/<id> share URL. Owner-only (the server rejects deleting someone else's post). Destructive \u2014 confirm the specific post with the user first. Requires sign-in.",
11537
+ inputSchema: {
11538
+ type: "object",
11539
+ properties: {
11540
+ target: { type: "string", description: "Experience id or https://\u2026/e/<id> URL" },
11541
+ api: { type: "string", description: "API base URL (default: production)" }
11542
+ },
11543
+ required: ["target"]
11544
+ },
11545
+ run: async (a) => {
11546
+ const target = str(a.target);
11547
+ if (!target) return "Provide the post's id or its \u2026/e/<id> URL.";
11548
+ const cfg = await loadConfig({ api: str(a.api) || void 0 });
11549
+ if (!cfg.token) return "Not signed in. Use vibed_login first.";
11550
+ const id = await deleteExperience({ apiBase: cfg.apiBase, token: cfg.token }, target);
11551
+ return `\u2713 Deleted post ${id}.`;
11552
+ }
11072
11553
  }
11073
11554
  ];
11074
11555
  function write(msg) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amirhosseinnateghi/vibed-mcp",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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",