opencrush 0.3.14 → 0.3.16

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 +127 -52
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -41872,6 +41872,7 @@ var init_llm_direct = __esm({
41872
41872
  "src/llm-direct.ts"() {
41873
41873
  PROVIDER_META = {
41874
41874
  openai: { baseURL: "", defaultModel: "gpt-4o-mini" },
41875
+ xai: { baseURL: "https://api.x.ai/v1", defaultModel: "grok-4-1-fast-non-reasoning" },
41875
41876
  deepseek: { baseURL: "https://api.deepseek.com", defaultModel: "deepseek-chat" },
41876
41877
  qwen: { baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1", defaultModel: "qwen-max" },
41877
41878
  kimi: { baseURL: "https://api.moonshot.cn/v1", defaultModel: "moonshot-v1-8k" },
@@ -41895,7 +41896,7 @@ var init_llm_direct = __esm({
41895
41896
  },
41896
41897
  {
41897
41898
  id: "qwen",
41898
- name: "\u901A\u4E49\u5343\u95EE Qwen",
41899
+ name: "Qwen (Alibaba)",
41899
41900
  emoji: "\u{1F31F}",
41900
41901
  tagline: "Alibaba \u2014 stable, multilingual, strong Chinese",
41901
41902
  taglineCN: "\u963F\u91CC\u51FA\u54C1\uFF0C\u7A33\u5B9A\u53EF\u9760\uFF0C\u4E2D\u82F1\u53CC\u5F3A",
@@ -41919,7 +41920,7 @@ var init_llm_direct = __esm({
41919
41920
  },
41920
41921
  {
41921
41922
  id: "zhipu",
41922
- name: "\u667A\u8C31 GLM",
41923
+ name: "GLM (Zhipu AI)",
41923
41924
  emoji: "\u{1F535}",
41924
41925
  tagline: "Tsinghua-backed, bilingual, free tier available",
41925
41926
  taglineCN: "\u6E05\u534E\u7CFB\uFF0C\u4E2D\u82F1\u53CC\u5F3A\uFF0C\u6709\u514D\u8D39\u989D\u5EA6",
@@ -41931,9 +41932,9 @@ var init_llm_direct = __esm({
41931
41932
  },
41932
41933
  {
41933
41934
  id: "minimax",
41934
- name: "MiniMax",
41935
+ name: "MiniMax (China)",
41935
41936
  emoji: "\u{1F49C}",
41936
- tagline: "Roleplay-optimized, character voice support",
41937
+ tagline: "Roleplay-optimized, character voice support \u2014 China API",
41937
41938
  taglineCN: "\u89D2\u8272\u626E\u6F14\u4F18\u5316\uFF0C\u652F\u6301\u89D2\u8272\u97F3\u8272",
41938
41939
  keyUrl: "https://platform.minimaxi.com/user-center/basic-information/interface-key",
41939
41940
  keyUrlCN: "https://platform.minimaxi.com/user-center/basic-information/interface-key",
@@ -41943,7 +41944,7 @@ var init_llm_direct = __esm({
41943
41944
  },
41944
41945
  {
41945
41946
  id: "minimax-global",
41946
- name: "MiniMax (Hailuo Global)",
41947
+ name: "MiniMax (Global)",
41947
41948
  emoji: "\u{1F49C}",
41948
41949
  tagline: "MiniMax global API \u2014 roleplay-optimized, English-first",
41949
41950
  taglineCN: "MiniMax \u6D77\u5916\u7248\uFF0C\u89D2\u8272\u626E\u6F14\u4F18\u5316",
@@ -41992,10 +41993,22 @@ var init_llm_direct = __esm({
41992
41993
  requiresVPN: true,
41993
41994
  isLocal: false
41994
41995
  },
41996
+ {
41997
+ id: "xai",
41998
+ name: "xAI (Grok)",
41999
+ emoji: "\u26A1",
42000
+ tagline: "$25 free credit on signup, fast reasoning",
42001
+ taglineCN: "$25\u514D\u8D39\u989D\u5EA6\uFF0C\u63A8\u7406\u5FEB",
42002
+ keyUrl: "https://console.x.ai",
42003
+ keyUrlCN: "https://console.x.ai",
42004
+ envKey: "XAI_API_KEY",
42005
+ requiresVPN: true,
42006
+ isLocal: false
42007
+ },
41995
42008
  // ── Local ──
41996
42009
  {
41997
42010
  id: "ollama",
41998
- name: "Ollama (\u672C\u5730)",
42011
+ name: "Ollama (Local)",
41999
42012
  emoji: "\u{1F3E0}",
42000
42013
  tagline: "Completely free, runs on your computer",
42001
42014
  taglineCN: "\u5B8C\u5168\u514D\u8D39\uFF0C\u672C\u5730\u8FD0\u884C\uFF0C\u9700\u8981\u9AD8\u6027\u80FD\u7535\u8111",
@@ -127539,6 +127552,7 @@ RULES:
127539
127552
  ];
127540
127553
  OPENAI_COMPAT_PROVIDERS = {
127541
127554
  openai: { baseURL: "", defaultModel: "gpt-4o-mini" },
127555
+ xai: { baseURL: "https://api.x.ai/v1", defaultModel: "grok-4-1-fast-non-reasoning" },
127542
127556
  deepseek: { baseURL: "https://api.deepseek.com", defaultModel: "deepseek-chat" },
127543
127557
  qwen: { baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1", defaultModel: "qwen-max" },
127544
127558
  kimi: { baseURL: "https://api.moonshot.cn/v1", defaultModel: "moonshot-v1-8k" },
@@ -127710,6 +127724,7 @@ RULES:
127710
127724
  resolveApiKey(config) {
127711
127725
  const map4 = {
127712
127726
  openai: config.openaiApiKey,
127727
+ xai: config.xaiApiKey,
127713
127728
  deepseek: config.deepseekApiKey,
127714
127729
  qwen: config.qwenApiKey,
127715
127730
  kimi: config.kimiApiKey,
@@ -244763,6 +244778,7 @@ async function startOpencrush() {
244763
244778
  // International
244764
244779
  anthropicApiKey: config.ANTHROPIC_API_KEY,
244765
244780
  openaiApiKey: config.OPENAI_API_KEY,
244781
+ xaiApiKey: config.XAI_API_KEY,
244766
244782
  // Chinese providers
244767
244783
  deepseekApiKey: config.DEEPSEEK_API_KEY,
244768
244784
  qwenApiKey: config.DASHSCOPE_API_KEY,
@@ -244948,6 +244964,7 @@ function loadConfig() {
244948
244964
  // International
244949
244965
  ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
244950
244966
  OPENAI_API_KEY: process.env.OPENAI_API_KEY,
244967
+ XAI_API_KEY: process.env.XAI_API_KEY,
244951
244968
  // Chinese providers
244952
244969
  DEEPSEEK_API_KEY: process.env.DEEPSEEK_API_KEY,
244953
244970
  DASHSCOPE_API_KEY: process.env.DASHSCOPE_API_KEY,
@@ -245012,11 +245029,11 @@ function validateConfig(config) {
245012
245029
  console.log(source_default.gray(' Run "npx opencrush@latest setup" to configure, or edit .env directly'));
245013
245030
  process.exit(1);
245014
245031
  }
245015
- const hasLLM = config.ANTHROPIC_API_KEY || config.OPENAI_API_KEY || config.DEEPSEEK_API_KEY || config.DASHSCOPE_API_KEY || config.MOONSHOT_API_KEY || config.ZHIPU_API_KEY || config.MINIMAX_API_KEY || config.LLM_PROVIDER === "ollama";
245032
+ const hasLLM = config.ANTHROPIC_API_KEY || config.OPENAI_API_KEY || config.XAI_API_KEY || config.DEEPSEEK_API_KEY || config.DASHSCOPE_API_KEY || config.MOONSHOT_API_KEY || config.ZHIPU_API_KEY || config.MINIMAX_API_KEY || config.LLM_PROVIDER === "ollama";
245016
245033
  if (!hasLLM) {
245017
245034
  console.log(source_default.red("\n \u274C No LLM API key configured"));
245018
245035
  console.log(source_default.gray(" Add one of these to .env:"));
245019
- console.log(source_default.gray(" ANTHROPIC_API_KEY / OPENAI_API_KEY / DEEPSEEK_API_KEY"));
245036
+ console.log(source_default.gray(" ANTHROPIC_API_KEY / OPENAI_API_KEY / XAI_API_KEY / DEEPSEEK_API_KEY"));
245020
245037
  console.log(source_default.gray(" DASHSCOPE_API_KEY / MOONSHOT_API_KEY / ZHIPU_API_KEY / MINIMAX_API_KEY"));
245021
245038
  console.log(source_default.gray(" Or set LLM_PROVIDER=ollama for local inference"));
245022
245039
  process.exit(1);
@@ -245115,7 +245132,7 @@ function maybeOpenCard(name) {
245115
245132
  }
245116
245133
  function generateEnvFile(values) {
245117
245134
  const sections = {
245118
- "# \u2500\u2500 AI Provider \u2500\u2500": ["LLM_PROVIDER", "LLM_MODEL", "ANTHROPIC_API_KEY", "OPENAI_API_KEY", "DEEPSEEK_API_KEY", "DASHSCOPE_API_KEY", "MOONSHOT_API_KEY", "ZHIPU_API_KEY", "MINIMAX_API_KEY", "OLLAMA_BASE_URL", "OLLAMA_MODEL", "JINA_API_KEY"],
245135
+ "# \u2500\u2500 AI Provider \u2500\u2500": ["LLM_PROVIDER", "LLM_MODEL", "ANTHROPIC_API_KEY", "OPENAI_API_KEY", "XAI_API_KEY", "DEEPSEEK_API_KEY", "DASHSCOPE_API_KEY", "MOONSHOT_API_KEY", "ZHIPU_API_KEY", "MINIMAX_API_KEY", "OLLAMA_BASE_URL", "OLLAMA_MODEL", "JINA_API_KEY"],
245119
245136
  "# \u2500\u2500 Character \u2500\u2500": ["CHARACTER_NAME"],
245120
245137
  "# \u2500\u2500 Messaging Platforms \u2500\u2500": ["DISCORD_BOT_TOKEN", "DISCORD_OWNER_ID", "DISCORD_CLIENT_ID", "TELEGRAM_BOT_TOKEN", "TELEGRAM_OWNER_ID", "WHATSAPP_ENABLED"],
245121
245138
  "# \u2500\u2500 Media (Selfies, Voice, Video) \u2500\u2500": ["FAL_KEY", "IMAGE_MODEL", "IMAGE_REFERENCE_MODEL", "TTS_PROVIDER", "ELEVENLABS_API_KEY", "ELEVENLABS_VOICE_ID", "FISH_AUDIO_API_KEY", "FISH_AUDIO_VOICE_ID"],
@@ -245185,6 +245202,7 @@ async function runSetupWizard() {
245185
245202
  const providerChoices = [
245186
245203
  PROVIDER_INFO.find((p2) => p2.id === "anthropic"),
245187
245204
  PROVIDER_INFO.find((p2) => p2.id === "openai"),
245205
+ PROVIDER_INFO.find((p2) => p2.id === "xai"),
245188
245206
  PROVIDER_INFO.find((p2) => p2.id === "deepseek"),
245189
245207
  PROVIDER_INFO.find((p2) => p2.id === "qwen"),
245190
245208
  PROVIDER_INFO.find((p2) => p2.id === "kimi"),
@@ -245217,6 +245235,8 @@ async function runSetupWizard() {
245217
245235
  console.log(source_default.gray(' Sign up \u2192 API Keys \u2192 Create Key \u2192 copy the key (starts with "sk-ant-")\n'));
245218
245236
  } else if (llmProvider === "openai") {
245219
245237
  console.log(source_default.gray(" Sign up \u2192 API Keys \u2192 Create new secret key\n"));
245238
+ } else if (llmProvider === "xai") {
245239
+ console.log(source_default.gray(" Sign up \u2192 get $25 free credit \u2192 copy API key\n"));
245220
245240
  } else if (llmProvider === "deepseek") {
245221
245241
  console.log(source_default.gray(" Sign up \u2192 API Keys \u2192 Create key (new users get free credits)\n"));
245222
245242
  }
@@ -245591,74 +245611,122 @@ __export(card_exports, {
245591
245611
  generateCard: () => generateCard
245592
245612
  });
245593
245613
  function parseIdentity(identityPath) {
245594
- var _a3, _b2, _c, _d;
245614
+ var _a3, _b2, _c, _d, _e2;
245595
245615
  const raw = (0, import_fs18.readFileSync)(identityPath, "utf-8");
245596
245616
  const { content } = (0, import_gray_matter3.default)(raw);
245597
245617
  const nameMatch = content.match(/^#\s+(.+)$/m);
245598
245618
  const name = ((_a3 = nameMatch == null ? void 0 : nameMatch[1]) == null ? void 0 : _a3.trim()) ?? "Unknown";
245599
245619
  const ageMatch = content.match(/\*\*Age:\*\*\s*(\d+)/i);
245600
- const age = (ageMatch == null ? void 0 : ageMatch[1]) ?? "??";
245620
+ const age = (ageMatch == null ? void 0 : ageMatch[1]) ?? "";
245601
245621
  const fromMatch = content.match(/\*\*From:\*\*\s*(.+)/i);
245602
- const locationRaw = ((_b2 = fromMatch == null ? void 0 : fromMatch[1]) == null ? void 0 : _b2.trim()) ?? "Unknown";
245603
- const location = locationRaw.replace(/\s*\(.*\)/, "").split(" \u2014 ")[0].trim();
245622
+ const locationRaw = ((_b2 = fromMatch == null ? void 0 : fromMatch[1]) == null ? void 0 : _b2.trim()) ?? "";
245623
+ const location = locationRaw.replace(/\s*\(.*\)/, "").split(" \u2014 ")[0].split(" - ")[0].trim();
245624
+ const cleanLocation = location.length > 40 || location.toLowerCase().startsWith("says ") ? "" : location;
245604
245625
  const hobbiesMatch = content.match(/\*\*Hobbies:\*\*\s*(.+)/i);
245605
245626
  const hobbiesRaw = (hobbiesMatch == null ? void 0 : hobbiesMatch[1]) ?? "";
245606
- const tags = hobbiesRaw.split(",").map((t2) => t2.trim()).filter(Boolean).slice(0, 5);
245607
- const bgMatch = content.match(/## (?:Background|Appearance)\s*\n+(.+)/i);
245608
- const description = ((_d = (_c = bgMatch == null ? void 0 : bgMatch[1]) == null ? void 0 : _c.trim().split(".")[0]) == null ? void 0 : _d.trim()) ?? name;
245627
+ const tags = splitRespectingParens(hobbiesRaw).map((t2) => t2.trim()).filter(Boolean).map((t2) => t2.length > 25 ? t2.split(/[,(]/)[0].trim() : t2).filter((t2) => t2.length > 1 && t2.length <= 25).slice(0, 6);
245628
+ const soulPath = identityPath.replace("IDENTITY.md", "SOUL.md");
245629
+ let description = "";
245630
+ if ((0, import_fs18.existsSync)(soulPath)) {
245631
+ const soul = (0, import_fs18.readFileSync)(soulPath, "utf-8");
245632
+ const vibeLine = soul.split("\n").find((l2) => l2.trim() && !l2.startsWith("#") && !l2.startsWith("-"));
245633
+ if (vibeLine) {
245634
+ description = ((_c = vibeLine.trim().split(".")[0]) == null ? void 0 : _c.trim()) ?? "";
245635
+ }
245636
+ }
245637
+ if (!description) {
245638
+ const bgMatch = content.match(/## (?:Background|Appearance)\s*\n+(.+)/i);
245639
+ description = ((_e2 = (_d = bgMatch == null ? void 0 : bgMatch[1]) == null ? void 0 : _d.trim().split(".")[0]) == null ? void 0 : _e2.trim()) ?? "";
245640
+ }
245609
245641
  const { data: meta } = (0, import_gray_matter3.default)(raw);
245610
245642
  const gender = meta.gender ?? "female";
245611
- return { name, age, location, tags, description, gender };
245643
+ return { name, age, location: cleanLocation, tags, description, gender };
245644
+ }
245645
+ function splitRespectingParens(str2) {
245646
+ const result = [];
245647
+ let depth = 0;
245648
+ let current = "";
245649
+ for (const ch of str2) {
245650
+ if (ch === "(") depth++;
245651
+ else if (ch === ")") depth--;
245652
+ if (ch === "," && depth === 0) {
245653
+ result.push(current);
245654
+ current = "";
245655
+ } else {
245656
+ current += ch;
245657
+ }
245658
+ }
245659
+ if (current) result.push(current);
245660
+ return result;
245661
+ }
245662
+ async function sampleGradientFromImage(imagePath) {
245663
+ try {
245664
+ const sharp = (await import("sharp")).default;
245665
+ const { data, info } = await sharp(imagePath).resize(1, 1, { fit: "cover" }).raw().toBuffer({ resolveWithObject: true });
245666
+ const r2 = data[0], g2 = data[1], b2 = data[2];
245667
+ const darken = (v2, f2) => Math.round(v2 * f2);
245668
+ const from2 = `rgb(${darken(r2, 0.25)}, ${darken(g2, 0.25)}, ${darken(b2, 0.25)})`;
245669
+ const to = `rgb(${darken(r2, 0.12)}, ${darken(g2, 0.12)}, ${darken(b2, 0.12)})`;
245670
+ return { from: from2, to };
245671
+ } catch {
245672
+ return { from: "#1a1a2e", to: "#0f0f1a" };
245673
+ }
245612
245674
  }
245613
245675
  function pickGradient(name) {
245614
245676
  const gradients = [
245615
- { from: "#1a1a2e", to: "#16213e" },
245677
+ { from: "#1a1a2e", to: "#0f0f1a" },
245616
245678
  // deep navy
245617
- { from: "#2d1b3d", to: "#1a1a2e" },
245679
+ { from: "#2d1b3d", to: "#150d1e" },
245618
245680
  // purple-dark
245619
- { from: "#1b2d2d", to: "#0f1f1f" },
245681
+ { from: "#1b2d2d", to: "#0a1414" },
245620
245682
  // teal-dark
245621
- { from: "#2d1b1b", to: "#1a1a1a" },
245683
+ { from: "#2d1b1b", to: "#140d0d" },
245622
245684
  // warm dark
245623
- { from: "#1b2d1b", to: "#0f1f0f" },
245624
- // forest dark
245625
- { from: "#2d2d1b", to: "#1f1f0f" }
245685
+ { from: "#1a1a30", to: "#0d0d18" },
245686
+ // midnight
245687
+ { from: "#2d241b", to: "#14120d" }
245626
245688
  // amber dark
245627
245689
  ];
245628
245690
  const hash = name.split("").reduce((acc, ch) => acc + ch.charCodeAt(0), 0);
245629
245691
  return gradients[hash % gradients.length];
245630
245692
  }
245631
245693
  function createSvgOverlay(data, gradient) {
245632
- const textX = PORTRAIT_X + PORTRAIT_SIZE + 50;
245633
- const maxTagX = WIDTH - 30;
245634
- const nameY = 210;
245635
- const metaY = nameY + 45;
245636
- const tagStartY = metaY + 50;
245694
+ const S2 = SCALE;
245695
+ const textX = PORTRAIT_X + PORTRAIT_SIZE + 50 * S2;
245696
+ const maxTagX = WIDTH - 40 * S2;
245697
+ const nameY = 200 * S2;
245698
+ const metaY = nameY + 44 * S2;
245699
+ const tagStartY = metaY + 48 * S2;
245700
+ const metaParts = [data.age, data.location].filter(Boolean);
245701
+ const metaText = metaParts.length > 0 ? metaParts.join(" \xB7 ") : "";
245637
245702
  let inlineTags = "";
245638
245703
  let offsetX = textX;
245639
245704
  let rowY = tagStartY;
245640
- const rowHeight = 30;
245705
+ const rowHeight = 28 * S2;
245641
245706
  const maxRows = 2;
245642
245707
  let currentRow = 1;
245643
245708
  for (const tag of data.tags) {
245644
- const label = tag.length > 18 ? tag.slice(0, 17) + "\u2026" : tag;
245645
- const w2 = label.length * 7.5 + 16;
245709
+ const label = tag.length > 20 ? tag.slice(0, 19) + "\u2026" : tag;
245710
+ const w2 = label.length * 7.2 * S2 + 14 * S2;
245646
245711
  if (offsetX + w2 > maxTagX) {
245647
245712
  if (currentRow >= maxRows) break;
245648
245713
  currentRow++;
245649
245714
  offsetX = textX;
245650
245715
  rowY += rowHeight;
245651
245716
  }
245717
+ const bubbleH = 22 * S2;
245718
+ const bubbleR = 11 * S2;
245652
245719
  inlineTags += `
245653
- <rect x="${offsetX}" y="${rowY - 14}" width="${w2}" height="22" rx="11" fill="rgba(255,255,255,0.10)" />
245654
- <text x="${offsetX + 8}" y="${rowY + 1}" font-family="system-ui, -apple-system, sans-serif" font-size="12" fill="#c0c0c0">${escapeXml(label)}</text>
245720
+ <rect x="${offsetX}" y="${rowY - 13 * S2}" width="${w2}" height="${bubbleH}" rx="${bubbleR}" fill="rgba(255,255,255,0.08)" />
245721
+ <text x="${offsetX + 7 * S2}" y="${rowY + 2 * S2}" font-family="system-ui, -apple-system, sans-serif" font-size="${11 * S2}" fill="#b0b0b0">${escapeXml(label)}</text>
245655
245722
  `;
245656
- offsetX += w2 + 6;
245723
+ offsetX += w2 + 6 * S2;
245657
245724
  }
245658
- const descriptionY = tagStartY + currentRow * rowHeight + 14;
245725
+ const descriptionY = tagStartY + currentRow * rowHeight + 16 * S2;
245726
+ const barH = 40 * S2;
245659
245727
  return `<svg width="${WIDTH}" height="${HEIGHT}" xmlns="http://www.w3.org/2000/svg">
245660
245728
  <defs>
245661
- <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
245729
+ <linearGradient id="bg" x1="0" y1="0" x2="0.5" y2="1">
245662
245730
  <stop offset="0%" stop-color="${gradient.from}" />
245663
245731
  <stop offset="100%" stop-color="${gradient.to}" />
245664
245732
  </linearGradient>
@@ -245669,19 +245737,25 @@ function createSvgOverlay(data, gradient) {
245669
245737
 
245670
245738
  <rect width="${WIDTH}" height="${HEIGHT}" fill="url(#bg)" />
245671
245739
 
245672
- <circle cx="${PORTRAIT_X + PORTRAIT_SIZE / 2}" cy="${PORTRAIT_Y + PORTRAIT_SIZE / 2}" r="${PORTRAIT_SIZE / 2 + 2}" fill="none" stroke="rgba(255,255,255,0.12)" stroke-width="2" />
245740
+ <!-- Portrait ring -->
245741
+ <circle cx="${PORTRAIT_X + PORTRAIT_SIZE / 2}" cy="${PORTRAIT_Y + PORTRAIT_SIZE / 2}" r="${PORTRAIT_SIZE / 2 + 2 * S2}" fill="none" stroke="rgba(255,255,255,0.10)" stroke-width="${2 * S2}" />
245673
245742
 
245674
- <text x="${textX}" y="${nameY}" font-family="system-ui, -apple-system, sans-serif" font-size="44" font-weight="bold" fill="white">${escapeXml(data.name)}</text>
245743
+ <!-- Name -->
245744
+ <text x="${textX}" y="${nameY}" font-family="system-ui, -apple-system, sans-serif" font-size="${42 * S2}" font-weight="bold" fill="white">${escapeXml(data.name)}</text>
245675
245745
 
245676
- <text x="${textX}" y="${metaY}" font-family="system-ui, -apple-system, sans-serif" font-size="16" fill="#a0a0a0">${escapeXml(data.age)} \xB7 ${escapeXml(truncate(data.location, 35))}</text>
245746
+ <!-- Age \xB7 Location -->
245747
+ ${metaText ? `<text x="${textX}" y="${metaY}" font-family="system-ui, -apple-system, sans-serif" font-size="${15 * S2}" fill="#999">${escapeXml(truncate(metaText, 40))}</text>` : ""}
245677
245748
 
245749
+ <!-- Tags -->
245678
245750
  ${inlineTags}
245679
245751
 
245680
- <text x="${textX}" y="${descriptionY}" font-family="system-ui, -apple-system, sans-serif" font-size="14" fill="#b0b0b0" font-style="italic">${escapeXml(truncate(data.description, 60))}</text>
245752
+ <!-- Description -->
245753
+ ${data.description ? `<text x="${textX}" y="${descriptionY}" font-family="system-ui, -apple-system, sans-serif" font-size="${13 * S2}" fill="#888" font-style="italic">${escapeXml(truncate(data.description, 55))}</text>` : ""}
245681
245754
 
245682
- <rect x="0" y="${HEIGHT - 44}" width="${WIDTH}" height="44" fill="rgba(0,0,0,0.3)" />
245683
- <text x="36" y="${HEIGHT - 17}" font-family="system-ui, -apple-system, sans-serif" font-size="14" font-weight="bold" fill="#ff69b4">Opencrush</text>
245684
- <text x="${WIDTH - 36}" y="${HEIGHT - 17}" font-family="system-ui, -apple-system, sans-serif" font-size="12" fill="#888" text-anchor="end">github.com/Hollandchirs/Opencrush</text>
245755
+ <!-- Bottom bar -->
245756
+ <rect x="0" y="${HEIGHT - barH}" width="${WIDTH}" height="${barH}" fill="rgba(0,0,0,0.25)" />
245757
+ <text x="${36 * S2}" y="${HEIGHT - 14 * S2}" font-family="system-ui, -apple-system, sans-serif" font-size="${13 * S2}" font-weight="bold" fill="#ff69b4">Opencrush</text>
245758
+ <text x="${WIDTH - 36 * S2}" y="${HEIGHT - 14 * S2}" font-family="system-ui, -apple-system, sans-serif" font-size="${11 * S2}" fill="#666" text-anchor="end">github.com/Hollandchirs/Opencrush</text>
245685
245759
  </svg>`;
245686
245760
  }
245687
245761
  function escapeXml(str2) {
@@ -245702,7 +245776,6 @@ async function generateCard(characterName) {
245702
245776
  throw new Error(`Missing IDENTITY.md for character "${characterName}"`);
245703
245777
  }
245704
245778
  const data = parseIdentity(identityPath);
245705
- const gradient = pickGradient(data.name);
245706
245779
  const imageExts = [".jpeg", ".jpg", ".png", ".webp"];
245707
245780
  let refImagePath;
245708
245781
  for (const ext of imageExts) {
@@ -245712,6 +245785,7 @@ async function generateCard(characterName) {
245712
245785
  break;
245713
245786
  }
245714
245787
  }
245788
+ const gradient = refImagePath ? await sampleGradientFromImage(refImagePath) : pickGradient(data.name);
245715
245789
  const svgOverlay = createSvgOverlay(data, gradient);
245716
245790
  const svgBuffer = Buffer.from(svgOverlay);
245717
245791
  let card = sharp(svgBuffer, { density: 300 }).resize(WIDTH, HEIGHT);
@@ -245734,22 +245808,23 @@ async function generateCard(characterName) {
245734
245808
  await card.png().toFile(outputPath);
245735
245809
  console.log(source_default.green(`
245736
245810
  Card generated: characters/${characterName}/card.png`));
245737
- console.log(source_default.gray(` ${WIDTH}x${HEIGHT} PNG
245811
+ console.log(source_default.gray(` ${WIDTH}x${HEIGHT} PNG (2x retina)
245738
245812
  `));
245739
245813
  return outputPath;
245740
245814
  }
245741
- var import_fs18, import_path12, import_gray_matter3, WIDTH, HEIGHT, PORTRAIT_SIZE, PORTRAIT_X, PORTRAIT_Y;
245815
+ var import_fs18, import_path12, import_gray_matter3, SCALE, WIDTH, HEIGHT, PORTRAIT_SIZE, PORTRAIT_X, PORTRAIT_Y;
245742
245816
  var init_card = __esm({
245743
245817
  "src/card.ts"() {
245744
245818
  import_fs18 = require("fs");
245745
245819
  import_path12 = require("path");
245746
245820
  import_gray_matter3 = __toESM(require_gray_matter());
245747
245821
  init_source();
245748
- WIDTH = 1200;
245749
- HEIGHT = 630;
245750
- PORTRAIT_SIZE = 260;
245751
- PORTRAIT_X = 80;
245752
- PORTRAIT_Y = (HEIGHT - PORTRAIT_SIZE) / 2 - 10;
245822
+ SCALE = 2;
245823
+ WIDTH = 1200 * SCALE;
245824
+ HEIGHT = 630 * SCALE;
245825
+ PORTRAIT_SIZE = 260 * SCALE;
245826
+ PORTRAIT_X = 80 * SCALE;
245827
+ PORTRAIT_Y = Math.round((HEIGHT - PORTRAIT_SIZE) / 2) - 10 * SCALE;
245753
245828
  }
245754
245829
  });
245755
245830
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencrush",
3
- "version": "0.3.14",
3
+ "version": "0.3.16",
4
4
  "description": "Your AI companion lives on your device. She watches dramas, listens to music, and thinks of you.",
5
5
  "bin": {
6
6
  "opencrush": "dist/index.js"