beast-agent 2.35.0 → 2.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "beast-agent",
3
3
  "productName": "Beast Agent",
4
- "version": "2.35.0",
4
+ "version": "2.36.0",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -40,6 +40,8 @@
40
40
  "dist": "electron-builder --win nsis portable",
41
41
  "test": "node --test \"tests/**/*.test.js\"",
42
42
  "release": "node scripts/release.js",
43
+ "skillsweb:sync": "node skillsweb/sync-builtins.js",
44
+ "skillsweb:deploy": "node skillsweb/sync-builtins.js && cd skillsweb && firebase deploy --only hosting:skills",
43
45
  "postinstall": "node scripts/fix-electron.js --soft",
44
46
  "prepublishOnly": "node scripts/swap-electron.js deps",
45
47
  "postpublish": "node scripts/swap-electron.js devdeps"
@@ -3,7 +3,8 @@
3
3
  /* SKILLS STORE: topluluk skill mağazası.
4
4
  - Yerel DB : %APPDATA%\beast\store\skills.json (yüklenen skill'ler, dosyalar gömülü)
5
5
  - İstatistik DB : %APPDATA%\beast\store\stats.json (installs/likes — makine başına)
6
- - Topluluk indeksi: GitHub repo /store/skills.json (raw fetch + offline cache)
6
+ - Veri kaynağı : beastagent-skills.web.app (builtins.json + community.json)
7
+ erişilemezse GitHub raw /store/skills.json, o da yoksa offline cache
7
8
  - Kurulum hedefi : %APPDATA%\beast\skills\<id>\ (skills.scan() otomatik görür)
8
9
  Trending/Stars sıralaması renderer'da skorla yapılır; burada ham veri döner.
9
10
  Beast Kodu ASLA paylaşılmaz — yalnız sha256 türevi parmak izi (beastId) kullanılır. */
@@ -14,6 +15,9 @@ const crypto = require('crypto');
14
15
  const { beastRoot } = require('./memory');
15
16
  const skills = require('./skills');
16
17
 
18
+ const SITE_URL = 'https://beastagent-skills.web.app';
19
+ const SITE_COMMUNITY_URL = SITE_URL + '/community.json';
20
+ const SITE_BUILTINS_URL = SITE_URL + '/builtins.json';
17
21
  const COMMUNITY_URL = 'https://raw.githubusercontent.com/algokodcom/beast-agent/main/store/skills.json';
18
22
  const MAX_FILES = 20;
19
23
  const MAX_FILE_BYTES = 200 * 1024;
@@ -33,6 +37,9 @@ function statsFile() {
33
37
  function communityCacheFile() {
34
38
  return path.join(storeDir(), 'community.json');
35
39
  }
40
+ function builtinsCacheFile() {
41
+ return path.join(storeDir(), 'builtins.json');
42
+ }
36
43
 
37
44
  function readJson(p, fallback) {
38
45
  try {
@@ -83,32 +90,64 @@ function slugify(s) {
83
90
  );
84
91
  }
85
92
 
86
- /* ---------- topluluk indeksi ---------- */
93
+ /* ---------- veri kaynağı (site → GitHub → cache) ---------- */
87
94
 
88
- async function fetchCommunity(timeoutMs = 6000) {
95
+ const MEMO_MS = 5 * 60 * 1000;
96
+ const memo = { community: null, communityAt: 0, builtins: null, builtinsAt: 0 };
97
+
98
+ async function fetchJson(url, timeoutMs = 6000) {
99
+ const ctl = typeof AbortController !== 'undefined' ? new AbortController() : null;
100
+ const t = setTimeout(() => {
101
+ try {
102
+ ctl && ctl.abort();
103
+ } catch {}
104
+ }, timeoutMs);
89
105
  try {
90
- const ctl = typeof AbortController !== 'undefined' ? new AbortController() : null;
91
- const t = setTimeout(() => {
92
- try {
93
- ctl && ctl.abort();
94
- } catch {}
95
- }, timeoutMs);
96
- const r = await fetch(COMMUNITY_URL, { signal: ctl ? ctl.signal : undefined });
97
- clearTimeout(t);
106
+ const r = await fetch(url, { signal: ctl ? ctl.signal : undefined });
98
107
  if (!r.ok) throw new Error('HTTP ' + r.status);
99
- const j = await r.json();
108
+ return await r.json();
109
+ } finally {
110
+ clearTimeout(t);
111
+ }
112
+ }
113
+
114
+ /* Topluluk indeksi: önce SİTE (beastagent-skills.web.app), sonra GitHub raw */
115
+ async function fetchCommunity(timeoutMs = 6000) {
116
+ if (memo.community && Date.now() - memo.communityAt < MEMO_MS) return memo.community;
117
+ for (const url of [SITE_COMMUNITY_URL, COMMUNITY_URL]) {
118
+ try {
119
+ const j = await fetchJson(url, timeoutMs);
120
+ if (j && Array.isArray(j.skills)) {
121
+ try {
122
+ writeJson(communityCacheFile(), j);
123
+ } catch {}
124
+ memo.community = j.skills;
125
+ memo.communityAt = Date.now();
126
+ return j.skills;
127
+ }
128
+ } catch {}
129
+ }
130
+ /* offline: son iyi kopyadan devam */
131
+ const cached = readJson(communityCacheFile(), { skills: [] });
132
+ return Array.isArray(cached.skills) ? cached.skills : [];
133
+ }
134
+
135
+ /* Yerleşik skill vitrini: sitedeki builtins.json (offline'da diske cache) */
136
+ async function fetchBuiltins(timeoutMs = 6000) {
137
+ if (memo.builtins && Date.now() - memo.builtinsAt < MEMO_MS) return memo.builtins;
138
+ try {
139
+ const j = await fetchJson(SITE_BUILTINS_URL, timeoutMs);
100
140
  if (j && Array.isArray(j.skills)) {
101
141
  try {
102
- writeJson(communityCacheFile(), j);
142
+ writeJson(builtinsCacheFile(), j);
103
143
  } catch {}
144
+ memo.builtins = j.skills;
145
+ memo.builtinsAt = Date.now();
104
146
  return j.skills;
105
147
  }
106
- return [];
107
- } catch {
108
- /* offline: son iyi kopyadan devam */
109
- const cached = readJson(communityCacheFile(), { skills: [] });
110
- return Array.isArray(cached.skills) ? cached.skills : [];
111
- }
148
+ } catch {}
149
+ const cached = readJson(builtinsCacheFile(), { skills: [] });
150
+ return Array.isArray(cached.skills) ? cached.skills : [];
112
151
  }
113
152
 
114
153
  function mergeStats(entry, stats) {
@@ -122,18 +161,24 @@ function mergeStats(entry, stats) {
122
161
  };
123
162
  }
124
163
 
125
- /* Birleşik liste: topluluk + yerel (aynı id'de yerel kazanır) */
164
+ /* Birleşik liste: site (yerleşikler + topluluk) + yerel; aynı id'de yerel kazanır.
165
+ Yerleşikler `builtin:true` taşır — kart "Kurulu" gösterir, Trending/Stars'ta görünür. */
126
166
  async function list(beastCode) {
127
- const [community] = await Promise.all([fetchCommunity()]);
167
+ const [community, builtins] = await Promise.all([fetchCommunity(), fetchBuiltins()]);
128
168
  const db = loadLocal();
129
169
  const stats = loadStats();
130
170
  const byId = new Map();
131
171
  for (const e of community) {
132
172
  if (e && e.id && e.files && e.files['SKILL.md']) byId.set(e.id, e);
133
173
  }
174
+ for (const b of builtins) {
175
+ if (!b || !b.id || !b.files || !b.files['SKILL.md']) continue;
176
+ byId.set(b.id, { ...b, builtin: true });
177
+ }
134
178
  for (const e of db.skills) if (e && e.id) byId.set(e.id, e);
135
179
  const entries = [...byId.values()].map((e) => mergeStats(e, stats));
136
180
  const installed = new Set(skills.scan().map((s) => path.basename(path.dirname(s.path))));
181
+ for (const e of entries) if (e.builtin) installed.add(e.id);
137
182
  return {
138
183
  ok: true,
139
184
  beastId: beastFingerprint(beastCode),
package/src/main.js CHANGED
@@ -317,6 +317,7 @@ let waHistory = new Map(); // jid -> [sid,...] bu sohbete ait tüm oturumlar
317
317
  let waBcMode = new Set(); // jid -> BeastCode modu AKTİF (WhatsApp'tan uzaktan kodlama)
318
318
  let waJidPn = new Map(); // jid -> gerçek telefon numarası (LID fallback için)
319
319
  let waLastActiveJid = ''; // en son mesaj gelen WA sohbeti — kanal tek oturumunda cevap/dosya hedefi
320
+ let desktopActiveSid = ''; // masaüstü UI'da AÇIK olan oturum — proaktif not buraya işlenir
320
321
  const WA_HISTORY_CAP = 20;
321
322
  let tg = null;
322
323
  let tgChats = new Map(); // telegram chatId -> aktif session id (hepsi KANAL TEK OTURUMUNA bağlı)
@@ -5081,11 +5082,34 @@ ipcMain.handle('sessions:create', () => {
5081
5082
  return v;
5082
5083
  });
5083
5084
  ipcMain.handle('sessions:open', (_e, id) => engine.openSession(id));
5084
- ipcMain.handle('sessions:delete', (_e, id) => engine.deleteSession(id));
5085
+ ipcMain.handle('sessions:delete', (_e, id) => {
5086
+ const sid = String(id || '');
5087
+ if (sid && sid === desktopActiveSid) desktopActiveSid = '';
5088
+ return engine.deleteSession(id);
5089
+ });
5090
+ /* masaüstü UI hangi sohbeti AÇIK tutuyorsa main'e bildirir: empati loop
5091
+ proaktif notu rastgele bir oturuma değil, kullanıcının gördüğü sohbete
5092
+ işler; meşgulse enjeksiyon kuyruğa girer, tur bitince geçmişe düşer */
5093
+ ipcMain.handle('sessions:active', (_e, id) => {
5094
+ const sid = String(id || '');
5095
+ if (!sid) {
5096
+ desktopActiveSid = '';
5097
+ return true;
5098
+ }
5099
+ try {
5100
+ if (sessionFileAlive(sid)) desktopActiveSid = sid;
5101
+ } catch {}
5102
+ return true;
5103
+ });
5085
5104
 
5086
5105
  ipcMain.handle('agent:send', (_e, { sessionId, text }) => {
5087
5106
  const raw = text && typeof text === 'object' ? String(text.text || '') : String(text ?? '');
5088
5107
  const t = raw.trim();
5108
+ /* yazan sohbet = kullanıcının gördüğü sohbet: proaktif enjeksiyon hedefi */
5109
+ try {
5110
+ const asid = String(sessionId || '');
5111
+ if (asid && sessionFileAlive(asid)) desktopActiveSid = asid;
5112
+ } catch {}
5089
5113
  /* BOT HAFIZA GARANTİSİ: aktif bot bir MÜŞTERİ botuysa, botId'siz (eskiden
5090
5114
  kalma) oturumlar bu bota bağlanır — bot konuşması asla Beast'in global
5091
5115
  hafızasıyla (SOUL/USER/MEMORY) yürümez. Var olan botId asla üstüne yazılmaz. */
@@ -7210,10 +7234,18 @@ function ensureDcSession(channelId) {
7210
7234
  return dcSingleSid;
7211
7235
  }
7212
7236
 
7213
- /* Masaüstü yedeği: en güncel (bg/bot-DM'siz, meşgul olmayan) sohbet — yoksa yeni */
7214
- function empatiDesktopSid() {
7237
+ /* Masaüstü hedefi: kullanıcının AÇIK sohbeti önceliklidir — proaktif not
7238
+ nereye bakıyorsa oraya işlenir; meşgulse empatiInjectToSession kuyruğa alır.
7239
+ Açık sohbet bilinmiyorsa en güncel meşgul olmayan sohbet (kanal oturumları
7240
+ hariç — onlara kanal yoluyla zaten enjekte edildi); o da yoksa yeni oturum. */
7241
+ function empatiDesktopSid(exclude) {
7242
+ const skip = exclude instanceof Set ? exclude : null;
7243
+ try {
7244
+ if (desktopActiveSid && sessionFileAlive(desktopActiveSid)) return String(desktopActiveSid);
7245
+ } catch {}
7215
7246
  try {
7216
7247
  for (const v of engine.listSessions()) {
7248
+ if (skip && skip.has(String(v.id))) continue;
7217
7249
  if (!engine.isBusy(v.id)) return String(v.id);
7218
7250
  }
7219
7251
  } catch {}
@@ -7234,14 +7266,17 @@ function empatiNotify(text, ev) {
7234
7266
  const outWa = PROACTIVE_MARK + '\n' + text + (srcWa ? '\n' + srcWa : '');
7235
7267
  const inject = empatiInjectText(ev, out);
7236
7268
  const senders = [];
7269
+ const channelSids = new Set(); /* bu tur enjeksiyon ALAN kanal oturumları */
7237
7270
  const tryWa = () => {
7238
7271
  try {
7239
7272
  const own = waOwnerNum();
7240
7273
  if (own && wa && wa.connected) {
7241
7274
  const jid = own + '@s.whatsapp.net';
7275
+ const sid = ensureWaSession(jid);
7276
+ channelSids.add(String(sid));
7242
7277
  senders.push(() =>
7243
7278
  Promise.resolve(sendWaSafe(jid, outWa))
7244
- .then(() => empatiInjectToSession(ensureWaSession(jid), inject))
7279
+ .then(() => empatiInjectToSession(sid, inject))
7245
7280
  .catch(() => {})
7246
7281
  );
7247
7282
  }
@@ -7251,9 +7286,11 @@ function empatiNotify(text, ev) {
7251
7286
  try {
7252
7287
  if (tg && tg.connected) {
7253
7288
  for (const id of tgOwnerIds()) {
7289
+ const sid = ensureTgSession(String(id));
7290
+ channelSids.add(String(sid));
7254
7291
  senders.push(() =>
7255
7292
  Promise.resolve(sendTgSafe(id, outPlain))
7256
- .then(() => empatiInjectToSession(ensureTgSession(String(id)), inject))
7293
+ .then(() => empatiInjectToSession(sid, inject))
7257
7294
  .catch(() => {})
7258
7295
  );
7259
7296
  }
@@ -7265,9 +7302,11 @@ function empatiNotify(text, ev) {
7265
7302
  if (dc && dc.connected) {
7266
7303
  const outDc = out.replace('🫡 *Beast proaktif:*', '🫡 **Beast proaktif:**');
7267
7304
  for (const id of dcOwnerIds()) {
7305
+ const sid = ensureDcSession(String(id));
7306
+ channelSids.add(String(sid));
7268
7307
  senders.push(() =>
7269
7308
  Promise.resolve(sendDcSafe(id, outDc))
7270
- .then(() => empatiInjectToSession(ensureDcSession(String(id)), inject))
7309
+ .then(() => empatiInjectToSession(sid, inject))
7271
7310
  .catch(() => {})
7272
7311
  );
7273
7312
  }
@@ -7289,8 +7328,9 @@ function empatiNotify(text, ev) {
7289
7328
  gider (kanal kullanıcısı ham BAĞLAM bloğunu görmez, ajan görür). */
7290
7329
  try {
7291
7330
  if (win && !win.isDestroyed()) {
7292
- const dsid = empatiDesktopSid();
7293
- empatiInjectToSession(dsid, out);
7331
+ const dsid = String(empatiDesktopSid(channelSids) || '');
7332
+ /* aynı oturuma kanal yoluyla zaten enjekte edildiyse TEKRAR yazma */
7333
+ if (dsid && !channelSids.has(dsid)) empatiInjectToSession(dsid, out);
7294
7334
  win.webContents.send('agent:event', { type: 'proactive', sessionId: dsid, id: ev.id, level: ev.level, title: ev.title, text: out });
7295
7335
  }
7296
7336
  } catch {}
package/src/preload.js CHANGED
@@ -10,6 +10,7 @@ contextBridge.exposeInMainWorld('beast', {
10
10
  listSessions: () => ipcRenderer.invoke('sessions:list'),
11
11
  createSession: () => ipcRenderer.invoke('sessions:create'),
12
12
  openSession: (id) => ipcRenderer.invoke('sessions:open', id),
13
+ setActiveSession: (id) => ipcRenderer.invoke('sessions:active', id),
13
14
  deleteSession: (id) => ipcRenderer.invoke('sessions:delete', id),
14
15
  send: (sessionId, text) => ipcRenderer.invoke('agent:send', { sessionId, text }),
15
16
  interrupt: (sessionId, reason) => ipcRenderer.invoke('agent:interrupt', sessionId, reason),
@@ -620,6 +620,8 @@
620
620
  store_update: 'Güncelle',
621
621
  store_installs: 'kurulum',
622
622
  store_likes: 'beğeni',
623
+ store_builtin: 'Yerleşik',
624
+ store_builtin_note: 'Ajanla birlikte gelir',
623
625
  store_empty_trending: 'Mağaza henüz boş — ilk skill\u2019i sen yükle!',
624
626
  store_empty_stars: 'Henüz yıldız skill yok — uzun süre popüler olanlar buraya yükselir.',
625
627
  store_offline: 'Topluluk indeksine ulaşılamadı (offline) — yerel kayıtlar gösteriliyor.',
@@ -1411,6 +1413,8 @@
1411
1413
  store_update: 'Update',
1412
1414
  store_installs: 'installs',
1413
1415
  store_likes: 'likes',
1416
+ store_builtin: 'Built-in',
1417
+ store_builtin_note: 'Ships with the agent',
1414
1418
  store_empty_trending: 'The store is empty — be the first to upload a skill!',
1415
1419
  store_empty_stars: 'No star skills yet — long-time popular ones rise here.',
1416
1420
  store_offline: 'Community index unreachable (offline) — showing local entries.',
@@ -615,7 +615,6 @@
615
615
  <nav id="storeTabs">
616
616
  <button class="stab active" data-stab="trending" data-i18n="store_tab_trending">Trending</button>
617
617
  <button class="stab" data-stab="stars" data-i18n="store_tab_stars">Stars</button>
618
- <button class="stab" data-stab="upload" data-i18n="store_tab_upload">Upload</button>
619
618
  </nav>
620
619
  <div id="storePane"></div>
621
620
  </div>
@@ -1077,6 +1077,7 @@ setInterval(() => {
1077
1077
 
1078
1078
  async function openSession(id) {
1079
1079
  activeId = id;
1080
+ try { beast.setActiveSession(id); } catch {}
1080
1081
  streamEl = null;
1081
1082
  const s = await beast.openSession(id);
1082
1083
  els.msgs.innerHTML = '';
@@ -7675,33 +7676,73 @@ function closeStore() {
7675
7676
  $('#storeOverlay').hidden = true;
7676
7677
  }
7677
7678
 
7679
+ /* Mağaza kart ikonları — kapağı olmayan skill'lere otomatik SVG ikon atanır
7680
+ (site builtins.json / community kayıtlarındaki `icon` alanı kullanılır). */
7681
+ const STORE_ICONS = {
7682
+ grid: '<rect x="3.5" y="3.5" width="7" height="7" rx="1.5"/><rect x="13.5" y="3.5" width="7" height="7" rx="1.5"/><rect x="3.5" y="13.5" width="7" height="7" rx="1.5"/><rect x="13.5" y="13.5" width="7" height="7" rx="1.5"/>',
7683
+ 'file-text': '<path d="M13 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V9z"/><path d="M13 3v6h6M9 13h6M9 17h4"/>',
7684
+ wrench: '<path d="M15.5 3.5a5.5 5.5 0 0 0-4.6 8.5L4 18.9a2 2 0 0 0 2.8 2.8l6.9-6.9a5.5 5.5 0 0 0 6.9-7.3l-3.2 3.2-2.8-.7-.7-2.8z"/>',
7685
+ zap: '<path d="M13 2 4.5 13.5H11L10 22l8.5-11.5H12z"/>',
7686
+ bar: '<path d="M5 20V11M12 20V5M19 20v-6"/>',
7687
+ shield: '<path d="M12 3l7.5 3v5.5c0 4.6-3.1 7.9-7.5 9.5-4.4-1.6-7.5-4.9-7.5-9.5V6z"/><path d="m9 12 2 2 4-4"/>',
7688
+ book: '<path d="M4 4.5A2.5 2.5 0 0 1 6.5 2H20v16H6.5A2.5 2.5 0 0 0 4 20.5z"/><path d="M4 20.5A2.5 2.5 0 0 1 6.5 18H20v4H6.5A2.5 2.5 0 0 1 4 19.5z"/>',
7689
+ cpu: '<rect x="6.5" y="6.5" width="11" height="11" rx="2"/><rect x="10" y="10" width="4" height="4" rx="1"/><path d="M9 3v2.5M15 3v2.5M9 18.5V21M15 18.5V21M3 9h2.5M3 15h2.5M18.5 9H21M18.5 15H21"/>',
7690
+ flask: '<path d="M9.5 3h5M10.5 3v6.2L5 18.5A2 2 0 0 0 6.8 21h10.4a2 2 0 0 0 1.8-2.5L13.5 9.2V3"/><path d="M7.5 15h9"/>',
7691
+ news: '<path d="M4 5h13a2 2 0 0 1 2 2v11a2 2 0 0 0 2 2H6a2 2 0 0 1-2-2z"/><path d="M7 9h7M7 13h7M7 17h4"/>',
7692
+ compass: '<circle cx="12" cy="12" r="8.5"/><path d="m15.5 8.5-2 5-5 2 2-5z"/>',
7693
+ mail: '<rect x="3" y="5.5" width="18" height="13" rx="2"/><path d="m3.5 7 8.5 6 8.5-6"/>',
7694
+ activity: '<path d="M3 12h4l2.5-6 4 12L16 12h5"/>',
7695
+ bulb: '<path d="M9 18h6M10 21h4"/><path d="M12 3a6 6 0 0 1 3.5 10.9c-.6.5-1 1.3-1 2.1h-5c0-.8-.4-1.6-1-2.1A6 6 0 0 1 12 3z"/>',
7696
+ clipboard: '<rect x="5" y="4.5" width="14" height="16" rx="2"/><path d="M9 4.5V3.8A1.8 1.8 0 0 1 10.8 2h2.4A1.8 1.8 0 0 1 15 3.8v.7M9 10h6M9 14h6M9 18h3"/>',
7697
+ play: '<path d="M7 4.5v15l12-7.5z"/>',
7698
+ bug: '<rect x="8" y="7" width="8" height="11" rx="4"/><path d="M9.5 7a2.5 2.5 0 0 1 5 0M3.5 11H8M16 11h4.5M4.5 17H8M16 17h3.5M6 6.5 8 9M18 6.5 16 9"/>',
7699
+ 'check-circle': '<circle cx="12" cy="12" r="8.5"/><path d="m8.5 12.2 2.4 2.4 4.6-4.8"/>',
7700
+ search: '<circle cx="11" cy="11" r="6.5"/><path d="m16.2 16.2 4.3 4.3"/>',
7701
+ trending: '<path d="m3.5 17 5.5-5.5 3.5 3.5 7-7"/><path d="M15 8h5v5"/>',
7702
+ download: '<path d="M12 3v11M7.5 10.5 12 15l4.5-4.5"/><path d="M4 18v1.5A1.5 1.5 0 0 0 5.5 21h13A1.5 1.5 0 0 0 20 19.5V18"/>',
7703
+ heart: '<path d="M12 20s-7-4.4-7-9.4A4.1 4.1 0 0 1 12 7.6a4.1 4.1 0 0 1 7 3c0 5-7 9.4-7 9.4z"/>',
7704
+ box: '<path d="M3.5 7.5 12 3l8.5 4.5v9L12 21l-8.5-4.5z"/><path d="M3.5 7.5 12 12l8.5-4.5M12 12v9"/>',
7705
+ user: '<circle cx="12" cy="8" r="3.6"/><path d="M5 20c.6-3.4 3.5-5.4 7-5.4s6.4 2 7 5.4"/>',
7706
+ paw: '<circle cx="7" cy="9" r="2.2"/><circle cx="12" cy="7" r="2.2"/><circle cx="17" cy="9" r="2.2"/><path d="M12 12c-3.2 0-5.5 2.4-5.5 4.6 0 1.6 1.3 2.4 2.9 2.4 1 0 1.8-.4 2.6-.4s1.6.4 2.6.4c1.6 0 2.9-.8 2.9-2.4C17.5 14.4 15.2 12 12 12Z"/>',
7707
+ };
7708
+
7709
+ function storeIconSvg(name, size) {
7710
+ const s = Number(size) || 20;
7711
+ const body = STORE_ICONS[name] || STORE_ICONS.grid;
7712
+ const filled = name === 'paw';
7713
+ return `<svg viewBox="0 0 24 24" width="${s}" height="${s}" fill="${filled ? 'currentColor' : 'none'}" stroke="${filled ? 'none' : 'currentColor'}" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${body}</svg>`;
7714
+ }
7715
+
7678
7716
  function storeCard(e, opts = {}) {
7679
- const installed = storeState.installed.has(e.id);
7680
- const hasUpdate = installed && e.updatedAt && e.installedAt && String(e.updatedAt) > String(e.installedAt);
7717
+ const builtin = !!e.builtin;
7718
+ const installed = builtin || storeState.installed.has(e.id);
7719
+ const hasUpdate = !builtin && installed && e.updatedAt && e.installedAt && String(e.updatedAt) > String(e.installedAt);
7681
7720
  const img = e.image
7682
7721
  ? `<span class="store-avatar"><img src="${e.image}" alt="" style="width:100%;height:100%;object-fit:cover;border-radius:8px"/></span>`
7683
- : `<span class="store-avatar">${e.author && e.author.avatar ? escapeHtml(e.author.avatar) : '🧩'}</span>`;
7722
+ : `<span class="store-avatar store-avatar-ic">${storeIconSvg(e.icon, 20)}</span>`;
7684
7723
  const author = e.author || {};
7685
- const btnLabel = !installed ? _t('store_install') : hasUpdate ? _t('store_update') : _t('store_installed');
7724
+ const btnLabel = builtin ? _t('store_installed') : !installed ? _t('store_install') : hasUpdate ? _t('store_update') : _t('store_installed');
7686
7725
  const btnDone = installed && !hasUpdate;
7687
7726
  return `
7688
7727
  <div class="store-card" data-id="${escapeHtml(e.id)}">
7689
7728
  <div class="store-card-top">
7690
7729
  ${img}
7691
7730
  <div style="min-width:0;flex:1">
7692
- <div class="store-name" title="${escapeHtml(e.name)}">${escapeHtml(e.name)} <span class="store-ver">v${escapeHtml(e.version || '1.0.0')}</span></div>
7731
+ <div class="store-name" title="${escapeHtml(e.name)}">${escapeHtml(e.name)} <span class="store-ver">v${escapeHtml(e.version || '1.0.0')}</span>${builtin ? ` <span class="store-builtin">${_t('store_builtin')}</span>` : ''}</div>
7693
7732
  <div class="store-author">
7694
- <span class="sa-avatar">${author.avatar ? escapeHtml(author.avatar) : '👤'}</span>
7695
- <span class="sa-name">${escapeHtml(author.username || '—')}</span>
7696
- ${author.beastId ? `<span class="sa-beast">${escapeHtml(author.beastId)}</span>` : ''}
7733
+ <span class="sa-avatar">${builtin ? storeIconSvg('paw', 13) : (author.avatar ? escapeHtml(author.avatar) : storeIconSvg('user', 13))}</span>
7734
+ <span class="sa-name">${escapeHtml(builtin ? 'beast' : (author.username || '—'))}</span>
7735
+ ${!builtin && author.beastId ? `<span class="sa-beast">${escapeHtml(author.beastId)}</span>` : ''}
7697
7736
  </div>
7698
7737
  </div>
7699
7738
  </div>
7700
7739
  <div class="store-desc">${escapeHtml(e.description || '')}</div>
7701
7740
  ${(e.tags || []).length ? `<div class="store-tags">${e.tags.map((t) => `<span class="store-tag">${escapeHtml(t)}</span>`).join('')}</div>` : ''}
7702
7741
  <div class="store-card-foot">
7703
- <span class="store-stats"><span>⬇ ${e.installs || 0} ${_t('store_installs')}</span><span>♥ ${e.likes || 0} ${_t('store_likes')}</span></span>
7704
- <button class="store-like ${e.liked ? 'liked' : ''}" data-like="${escapeHtml(e.id)}">♥</button>
7742
+ ${builtin
7743
+ ? `<span class="store-stats"><span>${storeIconSvg('box', 13)} ${_t('store_builtin_note')}</span></span>`
7744
+ : `<span class="store-stats"><span>${storeIconSvg('download', 13)} ${e.installs || 0} ${_t('store_installs')}</span><span>${storeIconSvg('heart', 13)} ${e.likes || 0} ${_t('store_likes')}</span></span>
7745
+ <button class="store-like ${e.liked ? 'liked' : ''}" data-like="${escapeHtml(e.id)}">${storeIconSvg('heart', 14)}</button>`}
7705
7746
  <button class="store-install ${btnDone ? 'done' : ''}" data-install="${escapeHtml(e.id)}">${btnLabel}</button>
7706
7747
  </div>
7707
7748
  </div>`;
@@ -7710,6 +7751,7 @@ function storeCard(e, opts = {}) {
7710
7751
  function bindStoreCardActions(pane) {
7711
7752
  pane.querySelectorAll('[data-install]').forEach((btn) =>
7712
7753
  btn.addEventListener('click', async () => {
7754
+ if (btn.classList.contains('done')) return; /* kurulu/yerleşik — tıklama yok */
7713
7755
  const id = btn.dataset.install;
7714
7756
  btn.disabled = true;
7715
7757
  const r = await beast.storeInstall(id).catch(() => null);
@@ -7738,7 +7780,7 @@ function bindStoreCardActions(pane) {
7738
7780
  function renderStoreList(mode) {
7739
7781
  const pane = $('#storePane');
7740
7782
  const list = storeState.entries
7741
- .filter((e) => (mode === 'stars' ? storeAgeDays(e.createdAt) >= 14 && storeStarsScore(e) > 0 : true))
7783
+ .filter((e) => (mode === 'stars' ? (e.builtin || (storeAgeDays(e.createdAt) >= 14 && storeStarsScore(e) > 0)) : true))
7742
7784
  .sort((a, b) => (mode === 'stars' ? storeStarsScore(b) - storeStarsScore(a) : storeTrendingScore(b) - storeTrendingScore(a)))
7743
7785
  .slice(0, 30);
7744
7786
  pane.innerHTML =
@@ -7956,8 +7998,7 @@ function renderStoreUpload() {
7956
7998
  }
7957
7999
 
7958
8000
  function renderStorePane() {
7959
- if (storeState.tab === 'upload') renderStoreUpload();
7960
- else renderStoreList(storeState.tab);
8001
+ renderStoreList(storeState.tab);
7961
8002
  const bid = $('#storeBeastId');
7962
8003
  if (bid) bid.textContent = storeState.beastId || '';
7963
8004
  }
@@ -3852,6 +3852,22 @@ body.browser-open #storeOverlay { right: var(--bw, 480px); }
3852
3852
  }
3853
3853
  .store-name { font-weight: 700; font-size: 13.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3854
3854
  .store-ver { font-size: 10.5px; color: var(--muted); }
3855
+ .store-builtin {
3856
+ font-size: 9.5px;
3857
+ font-weight: 700;
3858
+ letter-spacing: 0.5px;
3859
+ color: var(--muted);
3860
+ border: 1px solid var(--border);
3861
+ border-radius: 20px;
3862
+ padding: 1px 7px;
3863
+ background: var(--panel2);
3864
+ vertical-align: 1px;
3865
+ }
3866
+ .store-avatar-ic { color: var(--text); }
3867
+ .store-avatar-ic svg { display: block; }
3868
+ .store-author .sa-avatar svg { display: inline-block; vertical-align: -2px; }
3869
+ .store-stats svg { vertical-align: -2px; }
3870
+ .store-like svg { display: block; }
3855
3871
  .store-author { display: flex; align-items: center; gap: 5px; font-size: 11.5px; color: var(--muted); min-width: 0; }
3856
3872
  .store-author .sa-avatar { font-size: 13px; }
3857
3873
  .store-author .sa-name { font-weight: 700; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }