limixbot 0.3.13 → 0.3.15

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 (121) hide show
  1. package/dist/assets/{ActivityPanel-WwnzdBwg.js → ActivityPanel-CrqSA80q.js} +1 -1
  2. package/dist/assets/{App-xm5evxQc.js → App-Bx6hl9Sg.js} +129 -129
  3. package/dist/assets/{AppEventEditor-D4R4y0yV.js → AppEventEditor-DLfkpP1d.js} +1 -1
  4. package/dist/assets/{BotSettingsDialog-B6Nl9Kqn.js → BotSettingsDialog-rHcw4pr0.js} +1 -1
  5. package/dist/assets/{BrowserPanel-D95FHyTZ.js → BrowserPanel-BgNsYU3s.js} +1 -1
  6. package/dist/assets/{BrowserProfilesManager-7KU0ynoV.js → BrowserProfilesManager-ByQtDUyM.js} +1 -1
  7. package/dist/assets/{CloudAddDialog-eJTjUT_W.js → CloudAddDialog-754PKKwo.js} +1 -1
  8. package/dist/assets/{ComputerFilesPane-CDm1oiQb.js → ComputerFilesPane-IP5ib-4w.js} +1 -1
  9. package/dist/assets/{ComputerPanel-DsSTgobP.js → ComputerPanel-CndWeumv.js} +2 -2
  10. package/dist/assets/{DataPanel-BtBpnpvo.js → DataPanel-BWlfa8L7.js} +1 -1
  11. package/dist/assets/{DesktopViewer-BRzNtGEm.js → DesktopViewer-tgbnWJ7S.js} +1 -1
  12. package/dist/assets/{FilesView-BIOj6w1e.js → FilesView-PiPhchvH.js} +1 -1
  13. package/dist/assets/{GrokMoveDialog-C-UmQzuZ.js → GrokMoveDialog-DGBkFh2_.js} +1 -1
  14. package/dist/assets/{InspectorPanel-C13p36PD.js → InspectorPanel-DMb9KlS1.js} +1 -1
  15. package/dist/assets/{KeyboardShortcutsModal-CWOIkT5q.js → KeyboardShortcutsModal-DcAnacT0.js} +1 -1
  16. package/dist/assets/{LiveDesktop-B-OYWCWR.js → LiveDesktop-pNuAQp7j.js} +1 -1
  17. package/dist/assets/{LocalVmWorkspace-X4vOLsJm.js → LocalVmWorkspace-rUDRW4sg.js} +1 -1
  18. package/dist/assets/{MacLocalControl-DMOrG-td.js → MacLocalControl-D2_QV-or.js} +1 -1
  19. package/dist/assets/{MarkdownFilePreview-B4WXm-Zz.js → MarkdownFilePreview-DNNn-HhY.js} +1 -1
  20. package/dist/assets/{MenuMotion-BDYr4RxL.js → MenuMotion-CIhkQGNX.js} +1 -1
  21. package/dist/assets/{NewBotDialog-D5FKUF9t.js → NewBotDialog-C1onlYty.js} +1 -1
  22. package/dist/assets/{PhoneAppDialog-DGaR3ZqK.js → PhoneAppDialog-DygQ7EJ1.js} +1 -1
  23. package/dist/assets/{PlacesRail-yZdo57pP.js → PlacesRail-DC3DtwQy.js} +1 -1
  24. package/dist/assets/{PluginsPanel-Rr0WYhLE.js → PluginsPanel-BZv6B1JK.js} +1 -1
  25. package/dist/assets/{ProIntroduction-Bma87PIr.js → ProIntroduction-DeMFeFxH.js} +1 -1
  26. package/dist/assets/{RemoteAgentSettingsPanel-BEygLori.js → RemoteAgentSettingsPanel-DyVQB35x.js} +1 -1
  27. package/dist/assets/{RoomPanel-xOsJAq9r.js → RoomPanel-DEH49als.js} +1 -1
  28. package/dist/assets/{RoutinesPage-ntwDQROW.js → RoutinesPage-CRI83T_I.js} +1 -1
  29. package/dist/assets/SettingsModal-Dcdk0Qvv.js +11 -0
  30. package/dist/assets/{ShareTeamDialog-C_2Klxsb.js → ShareTeamDialog-D5ML8qDY.js} +1 -1
  31. package/dist/assets/{SignedInSites-6gzIcTSf.js → SignedInSites-Bjc69Ty-.js} +1 -1
  32. package/dist/assets/{TeamLibraryPanel-B1t0-XZ-.js → TeamLibraryPanel-CVcmOn6k.js} +1 -1
  33. package/dist/assets/{TeamMapPage-Cb2qYz3_.js → TeamMapPage-C3-Cu90W.js} +1 -1
  34. package/dist/assets/{TriggersPage-wBKXT1VB.js → TriggersPage-DS2Ceion.js} +1 -1
  35. package/dist/assets/{VisibilitySection-Bd8MQdKE.js → VisibilitySection-DigpYJ5s.js} +1 -1
  36. package/dist/assets/{WebhookTrigger-Bvx80Ue5.js → WebhookTrigger-BP28l-Jy.js} +1 -1
  37. package/dist/assets/{WorksOnGrid-TTZtT4m-.js → WorksOnGrid-dVy5M1Fj.js} +1 -1
  38. package/dist/assets/{abnfDiagram-YUKMZFIV-Boj-Bkbs.js → abnfDiagram-YUKMZFIV-DE2rjoCc.js} +1 -1
  39. package/dist/assets/{activity-MpAxS_kw.js → activity-8Dgeq15H.js} +1 -1
  40. package/dist/assets/{architectureDiagram-47P4ROYG-Cl-ocqce.js → architectureDiagram-47P4ROYG-DrIn6OKQ.js} +1 -1
  41. package/dist/assets/{blockDiagram-E7TT5TSB-0nSQiv6g.js → blockDiagram-E7TT5TSB-BQdVPMCR.js} +1 -1
  42. package/dist/assets/{c4Diagram-CRA5TL53-Cx9iymkb.js → c4Diagram-CRA5TL53-DFb8MJmm.js} +1 -1
  43. package/dist/assets/{calendar-days-DXHDRm_4.js → calendar-days-Bs4Es4HT.js} +1 -1
  44. package/dist/assets/channel-1lG6Uyuf.js +1 -0
  45. package/dist/assets/{chunk-2Q5K7J3B-0rIrumUG.js → chunk-2Q5K7J3B-BXaLRTO9.js} +1 -1
  46. package/dist/assets/{chunk-5VM5RSS4-w9e6iKvs.js → chunk-5VM5RSS4-Cie3uAC2.js} +1 -1
  47. package/dist/assets/{chunk-7TKQ45FW-c4KxVKkf.js → chunk-7TKQ45FW-Bw5vFHu9.js} +1 -1
  48. package/dist/assets/{chunk-AW2ZBBNX-DmaBcuOP.js → chunk-AW2ZBBNX-CkxRWzwT.js} +1 -1
  49. package/dist/assets/{chunk-FVRAUYC3-BLzfqLHR.js → chunk-FVRAUYC3-7FMS_2yS.js} +2 -2
  50. package/dist/assets/{chunk-HTAEGDNF-BN48XtBm.js → chunk-HTAEGDNF-DQ_kiZYj.js} +1 -1
  51. package/dist/assets/{chunk-JWPE2WC7-D2NACa6i.js → chunk-JWPE2WC7-DD4AjX_J.js} +1 -1
  52. package/dist/assets/{chunk-KQW6MTUR-CbAfnxxG.js → chunk-KQW6MTUR-DAs7Un1F.js} +1 -1
  53. package/dist/assets/{chunk-XXDRQBXY-Bwg-DPP_.js → chunk-XXDRQBXY-qEAG8aNp.js} +1 -1
  54. package/dist/assets/{classDiagram-v2-K4WV4PDN-Dh3ohoZp.js → classDiagram-v2-K4WV4PDN-SFDos9_W.js} +1 -1
  55. package/dist/assets/{coins-Bv8-qll4.js → coins-ni7p9MMO.js} +1 -1
  56. package/dist/assets/{core-Dr_lGnx7.js → core-CMus8yJ9.js} +1 -1
  57. package/dist/assets/{cose-bilkent-JH36ORCC-DCHadn0L.js → cose-bilkent-JH36ORCC-BIvnumq9.js} +1 -1
  58. package/dist/assets/{cynefin-EF2NZ3EQ-ClpPPxUg.js → cynefin-EF2NZ3EQ-DEyZ973A.js} +1 -1
  59. package/dist/assets/{cynefinDiagram-3GCD6N5R-DZ5l7nkL.js → cynefinDiagram-3GCD6N5R-DIH2LA6q.js} +1 -1
  60. package/dist/assets/{dagre-W4DXFKR2-BagTvT62.js → dagre-W4DXFKR2-doItNion.js} +1 -1
  61. package/dist/assets/{diagram-2UJZ2QOL-DuByootb.js → diagram-2UJZ2QOL-DKfcZ3Fe.js} +1 -1
  62. package/dist/assets/{diagram-OVF4WLC6-BQ8Cvr0D.js → diagram-OVF4WLC6-BRnKWMs5.js} +1 -1
  63. package/dist/assets/{diagram-PFPMY2P6-D_pklaf9.js → diagram-PFPMY2P6-C3aLszxc.js} +1 -1
  64. package/dist/assets/{diagram-UMYRVEAY-1BuHq7tR.js → diagram-UMYRVEAY-DbMKM9Kw.js} +1 -1
  65. package/dist/assets/{diagram-YEKJPTXX-CK0cPx8n.js → diagram-YEKJPTXX-BqrhdTTX.js} +1 -1
  66. package/dist/assets/{diagram-ZIFT7M5P-DVzPho45.js → diagram-ZIFT7M5P-CYnUFm0_.js} +1 -1
  67. package/dist/assets/{ebnfDiagram-VR2GEFS7-D8UO_5sE.js → ebnfDiagram-VR2GEFS7-CnuOWBxY.js} +1 -1
  68. package/dist/assets/{elk-IJKZMXRS-BRigfR5V.js → elk-IJKZMXRS-BmtbqAAs.js} +1 -1
  69. package/dist/assets/{erDiagram-O2IAPWRE-CXHPobWj.js → erDiagram-O2IAPWRE-CL6oNmZD.js} +1 -1
  70. package/dist/assets/{file-up-CQezfdJk.js → file-up-DT7t5NXT.js} +1 -1
  71. package/dist/assets/{flowDiagram-OXPTDLAJ-CqKD6jh8.js → flowDiagram-OXPTDLAJ-CE9hfMoK.js} +1 -1
  72. package/dist/assets/{ganttDiagram-R7TSEDQI-C9FV3gsA.js → ganttDiagram-R7TSEDQI-DRU_A2cB.js} +1 -1
  73. package/dist/assets/{gitGraphDiagram-XJZIOB7I-CRu4dnr_.js → gitGraphDiagram-XJZIOB7I-BPkWYlKZ.js} +1 -1
  74. package/dist/assets/{import-name-CrmN5zUf.js → import-name-DutQXK90.js} +1 -1
  75. package/dist/assets/{index-aOwy9fcM.js → index-C6OVbzeZ.js} +2 -2
  76. package/dist/assets/{index-D8CUjCIJ.js → index-CiYcp06m.js} +1 -1
  77. package/dist/assets/{infoDiagram-5W2HQ5XZ-CCRjb2CQ.js → infoDiagram-5W2HQ5XZ-C4SBhDtT.js} +1 -1
  78. package/dist/assets/{ishikawaDiagram-K3B6WC7H-TaNPsGZS.js → ishikawaDiagram-K3B6WC7H-3TlhKRsV.js} +1 -1
  79. package/dist/assets/{journeyDiagram-COXZFDF6-Dz7YIIc2.js → journeyDiagram-COXZFDF6-K9dyCbpb.js} +1 -1
  80. package/dist/assets/{kanban-definition-P3RFRI5V-Cg69Fpp8.js → kanban-definition-P3RFRI5V-Df8i0B0H.js} +1 -1
  81. package/dist/assets/{mcp-servers-DwkY4xUh.js → mcp-servers-xuxWmmEV.js} +1 -1
  82. package/dist/assets/{mermaid.core-DiG3LoLz.js → mermaid.core-Dj_sBsXx.js} +4 -4
  83. package/dist/assets/{mindmap-definition-LZFPQGKD-nxP4f0fq.js → mindmap-definition-LZFPQGKD-CuBxqBw1.js} +1 -1
  84. package/dist/assets/{minimize-B_k_fmmo.js → minimize-CuDlETLH.js} +1 -1
  85. package/dist/assets/{monitor-BfmnPU-I.js → monitor-DqFADU6d.js} +1 -1
  86. package/dist/assets/{new-bot-D8qQ_VN7.js → new-bot-DoJRfwOC.js} +1 -1
  87. package/dist/assets/{org-library-DfX89pz8.js → org-library-B7nhJQxt.js} +1 -1
  88. package/dist/assets/{package-format-DLfgEUES.js → package-format-FvzPr9Xd.js} +1 -1
  89. package/dist/assets/{pegDiagram-BGZESJAR-gPjpGcNH.js → pegDiagram-BGZESJAR-BDWRqrLr.js} +1 -1
  90. package/dist/assets/{pieDiagram-CAPJLFHJ-Cgh25Z4_.js → pieDiagram-CAPJLFHJ-BByTBeRn.js} +1 -1
  91. package/dist/assets/{quadrantDiagram-GDMTTRAM-DD9luyqA.js → quadrantDiagram-GDMTTRAM-D0SI7j3Y.js} +1 -1
  92. package/dist/assets/{rail-B1mj4zlo.js → rail-BEOIK49V.js} +1 -1
  93. package/dist/assets/{railroadDiagram-SM67HX2B-CS_TYko7.js → railroadDiagram-SM67HX2B-Dz3tUGbx.js} +1 -1
  94. package/dist/assets/{remote-desktop-panel-Gb0B8rvx.js → remote-desktop-panel-DgZionIJ.js} +1 -1
  95. package/dist/assets/{requirementDiagram-X7JNWC4A-BePHia6E.js → requirementDiagram-X7JNWC4A-DrwqW7OV.js} +1 -1
  96. package/dist/assets/{sankeyDiagram-UM26HJRW-24ExCXGS.js → sankeyDiagram-UM26HJRW-95WqcM7l.js} +1 -1
  97. package/dist/assets/{sequenceDiagram-ZO4K6R2Y-Dtmx27I5.js → sequenceDiagram-ZO4K6R2Y-1zoh2EeE.js} +1 -1
  98. package/dist/assets/{sizeCapture-INFHLROL-2LvmoT1T.js → sizeCapture-INFHLROL-IzBNvZN8.js} +1 -1
  99. package/dist/assets/{stateDiagram-v2-TBUQTH76-moL2SIo9.js → stateDiagram-v2-TBUQTH76-CXdapkBV.js} +1 -1
  100. package/dist/assets/styles-CSArda2J.css +1 -0
  101. package/dist/assets/{swimlanes-N4OXWK64-D95Pk8qh.js → swimlanes-N4OXWK64-QK7_rFPB.js} +1 -1
  102. package/dist/assets/swimlanesDiagram-K3J5GTZL-B4Zj1xuq.js +8 -0
  103. package/dist/assets/{team-share-0GXKeixY.js → team-share-NJEAQGUX.js} +1 -1
  104. package/dist/assets/{timeline-definition-YOQAKGHF-HM-J25k2.js → timeline-definition-YOQAKGHF-MhSu-mD6.js} +1 -1
  105. package/dist/assets/{usecaseDiagram-VIAY4XPW-C6q6bCzJ.js → usecaseDiagram-VIAY4XPW-DdZIcDLN.js} +1 -1
  106. package/dist/assets/{vennDiagram-BLWOH2XV-hf0dh2yT.js → vennDiagram-BLWOH2XV-DDMi9wJz.js} +1 -1
  107. package/dist/assets/{wardleyDiagram-YMQ3BMBF-CpT_eRn9.js → wardleyDiagram-YMQ3BMBF-DrZpbJKY.js} +1 -1
  108. package/dist/assets/{xychartDiagram-TAQBALBS-D6g1V9DG.js → xychartDiagram-TAQBALBS-__UkZCKS.js} +1 -1
  109. package/dist/index.html +3 -3
  110. package/dist/rail.html +5 -5
  111. package/dist-server/index.js +307 -289
  112. package/dist-server/package.json +1 -1
  113. package/dist-server/server/index.js +2 -0
  114. package/dist-server/server/limix-skill-hub.js +264 -0
  115. package/dist-server/server/routes/bot-skills.js +11 -0
  116. package/package.json +1 -1
  117. package/dist/assets/SettingsModal-CZ_GMpWi.js +0 -11
  118. package/dist/assets/channel-BStWWrSt.js +0 -1
  119. package/dist/assets/styles-bUbK33f1.css +0 -1
  120. package/dist/assets/swimlanesDiagram-K3J5GTZL-DByi9G3i.js +0 -8
  121. /package/dist/assets/{styles-6RbfQIY7.js → styles-Btbbe7r3.js} +0 -0
@@ -1 +1 @@
1
- {"type":"module","version":"0.3.13"}
1
+ {"type":"module","version":"0.3.15"}
@@ -511,6 +511,7 @@ import { createBotPatchRoutes } from "./routes/bot-patch.js";
511
511
  import { createBotOrderRoutes } from "./routes/bot-order.js";
512
512
  import { createBotLifecycleRoutes } from "./routes/bot-lifecycle.js";
513
513
  import { createBotSkillsRoutes } from "./routes/bot-skills.js";
514
+ import { startHubAutoSync } from "./limix-skill-hub.js";
514
515
  import { createSkillShareRoutes } from "./routes/skill-share.js";
515
516
  import { createTeamContextRoutes } from "./routes/team-context.js";
516
517
  import { createAppSuggestions } from "./host/app-suggestions.js";
@@ -12385,6 +12386,7 @@ ROUTES.push(createBotLifecycleRoutes({
12385
12386
  activeGroupTurnForBot, cancelGroupTurnOperations, runningTurnInstance, closeOpenApprovals, deleteBotWithLifecycle,
12386
12387
  }));
12387
12388
  ROUTES.push(createBotSkillsRoutes({ store, cfg, listBotSkills, readBotSkillFile, stagedSkillListing }));
12389
+ startHubAutoSync();
12388
12390
  ROUTES.push(createEmailRoutes({ service: emailService }));
12389
12391
  ROUTES.push(phones.routes);
12390
12392
  ROUTES.push(createSkillShareRoutes({ enabled: () => skillsHost.mode() !== "off", botExists: (id) => Boolean(store.bot(id)),
@@ -0,0 +1,264 @@
1
+ // Limix Skill Hub client: keeps a cached copy of the hub catalog, installs a skill only after every file matches the
2
+ // SHA-256 the catalog pins, and never installs scripts, binaries or MCP files unless the owner allows tools for that
3
+ // one skill. Installed skills land DISABLED; the owner turns them on. Auto Sync (catalog refresh) is on by default,
4
+ // Auto Install is off by default.
5
+ import { createHash } from "node:crypto";
6
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { isSkillName } from "../shared/skill-md.js";
9
+ import { writeFileAtomic } from "./atomic.js";
10
+ import { DATA_DIR } from "./config.js";
11
+ import { installLibrarySkill, readSkillLibraryIndex, retireLibrarySkill, setLibrarySkillReviewState } from "./skill-library.js";
12
+ export const HUB_REPO = "leomieo/limix-skill-hub";
13
+ const HUB_BRANCH = "main";
14
+ const SYNC_MIN_GAP_MS = 20 * 60 * 60 * 1000;
15
+ const FILE_MAX_BYTES = 1024 * 1024;
16
+ const DEFAULT_SETTINGS = { autoSync: true, autoInstall: false, lastSync: null, lastError: null, etag: null };
17
+ const hubDir = () => join(DATA_DIR, "skill-hub");
18
+ const settingsFile = () => join(hubDir(), "settings.json");
19
+ const catalogFile = () => join(hubDir(), "catalog.json");
20
+ function readJson(file, fallback) {
21
+ try {
22
+ return JSON.parse(readFileSync(file, "utf8"));
23
+ }
24
+ catch {
25
+ return fallback;
26
+ }
27
+ }
28
+ export function hubSettings() {
29
+ return { ...DEFAULT_SETTINGS, ...readJson(settingsFile(), {}) };
30
+ }
31
+ function saveSettings(next) {
32
+ mkdirSync(hubDir(), { recursive: true, mode: 0o700 });
33
+ writeFileAtomic(settingsFile(), JSON.stringify(next, null, 2), { mode: 0o600 });
34
+ }
35
+ export function hubCatalog() {
36
+ const body = readJson(catalogFile(), {});
37
+ return Array.isArray(body.skills) ? body.skills : [];
38
+ }
39
+ const rawUrl = (path) => `https://raw.githubusercontent.com/${HUB_REPO}/${HUB_BRANCH}/${path}`;
40
+ async function fetchText(url, headers = {}) {
41
+ return fetch(url, { signal: AbortSignal.timeout(30_000), headers: { "User-Agent": "limix-bot", ...headers } });
42
+ }
43
+ let syncing = null;
44
+ /** Refreshes the cached catalog (ETag, so an unchanged hub costs one tiny request). */
45
+ export function syncHub() {
46
+ if (syncing)
47
+ return syncing;
48
+ syncing = (async () => {
49
+ const settings = hubSettings();
50
+ try {
51
+ const response = await fetchText(rawUrl("registry/catalog.json"), settings.etag && existsSync(catalogFile()) ? { "If-None-Match": settings.etag } : {});
52
+ if (response.status === 304) {
53
+ saveSettings({ ...settings, lastSync: new Date().toISOString(), lastError: null });
54
+ return { changed: false, count: hubCatalog().length };
55
+ }
56
+ if (!response.ok)
57
+ throw new Error(`Hub trả về ${response.status}`);
58
+ const text = await response.text();
59
+ const parsed = JSON.parse(text);
60
+ if (!Array.isArray(parsed.skills))
61
+ throw new Error("Danh mục Hub không đúng định dạng");
62
+ mkdirSync(hubDir(), { recursive: true, mode: 0o700 });
63
+ writeFileAtomic(catalogFile(), text, { mode: 0o600 });
64
+ saveSettings({ ...settings, etag: response.headers.get("etag"), lastSync: new Date().toISOString(), lastError: null });
65
+ if (settings.autoInstall)
66
+ await autoInstallEligible();
67
+ return { changed: true, count: parsed.skills.length };
68
+ }
69
+ catch (cause) {
70
+ const message = cause instanceof Error ? cause.message : String(cause);
71
+ saveSettings({ ...settings, lastError: message });
72
+ return { error: message };
73
+ }
74
+ finally {
75
+ syncing = null;
76
+ }
77
+ })();
78
+ return syncing;
79
+ }
80
+ const SCRIPT = /\.(py|js|mjs|cjs|ts|sh|bash|zsh|ps1|bat|cmd|rb|pl|php|go|rs|exe|dll|so|dylib|bin|jar|class|scr|msi|com|apk|dex|node|wasm)$/i;
81
+ const MCP_FILE = /(^|\/)(\.mcp\.json|mcp\.json|mcp-servers?\.json|claude_desktop_config\.json)$/i;
82
+ const isToolFile = (path) => SCRIPT.test(path) || MCP_FILE.test(path);
83
+ const sha = (data) => createHash("sha256").update(data).digest("hex");
84
+ export function hubSkillBlocked(entry) {
85
+ if (entry.area === "quarantine" || entry.security_status === "quarantined")
86
+ return "Skill nằm trong khu cách ly vì có mã nguy hiểm.";
87
+ if (entry.approval_status === "quarantined" || entry.approval_status === "rejected")
88
+ return "Skill bị từ chối.";
89
+ return null;
90
+ }
91
+ /** installed state of every catalog entry that came from the hub */
92
+ function installedFromHub() {
93
+ const out = new Map();
94
+ for (const entry of Object.values(readSkillLibraryIndex())) {
95
+ const match = /^limix-hub:([a-z0-9-]+)@([0-9a-f]{64})/.exec(entry.source);
96
+ if (match)
97
+ out.set(match[1], { checksum: match[2], sha256: entry.sha256, reviewState: entry.reviewState });
98
+ }
99
+ return out;
100
+ }
101
+ export function hubSkills() {
102
+ const installed = installedFromHub();
103
+ const library = readSkillLibraryIndex();
104
+ return hubCatalog().map((entry) => {
105
+ const have = installed.get(entry.id);
106
+ return {
107
+ ...entry,
108
+ installed: Boolean(have),
109
+ // A same-named skill the owner imported by hand is not the hub's: never touched by sync.
110
+ enabled: have?.reviewState === "approved",
111
+ updateAvailable: Boolean(have && have.checksum !== entry.checksum),
112
+ blocked: hubSkillBlocked(entry) ?? (!have && library[entry.id] ? "Thư viện đã có kỹ năng cùng tên không đến từ Hub." : null),
113
+ };
114
+ });
115
+ }
116
+ export async function installHubSkill(id, options = {}) {
117
+ if (!isSkillName(id))
118
+ return { error: "Tên kỹ năng không hợp lệ" };
119
+ const entry = hubCatalog().find((e) => e.id === id);
120
+ if (!entry)
121
+ return { error: "Không thấy kỹ năng này trong danh mục Hub. Hãy đồng bộ lại." };
122
+ const blocked = hubSkillBlocked(entry);
123
+ if (blocked)
124
+ return { error: blocked };
125
+ const installed = installedFromHub().get(id);
126
+ const existing = readSkillLibraryIndex()[id];
127
+ if (existing && !installed)
128
+ return { error: "Thư viện đã có kỹ năng cùng tên không đến từ Hub." };
129
+ const wanted = Object.keys(entry.file_hashes).filter((path) => path !== "SKILL.md" && (options.allowTools || !isToolFile(path)));
130
+ const skipped = Object.keys(entry.file_hashes).filter((path) => path !== "SKILL.md" && !wanted.includes(path));
131
+ const files = [];
132
+ let skillMd = "";
133
+ for (const path of ["SKILL.md", ...wanted]) {
134
+ const expected = entry.file_hashes[path];
135
+ if (!expected)
136
+ return { error: `Danh mục thiếu mã kiểm tra cho ${path}` };
137
+ let response;
138
+ try {
139
+ response = await fetchText(rawUrl(`skills/${id}/${path}`));
140
+ }
141
+ catch (cause) {
142
+ return { error: `Không tải được ${path}: ${cause instanceof Error ? cause.message : String(cause)}` };
143
+ }
144
+ if (!response.ok)
145
+ return { error: `Không tải được ${path} (${response.status})` };
146
+ const bytes = new Uint8Array(await response.arrayBuffer());
147
+ if (bytes.byteLength > FILE_MAX_BYTES)
148
+ return { error: `${path} quá lớn` };
149
+ // Pinned content: a file the hub no longer serves as catalogued is refused, never installed.
150
+ if (sha(bytes) !== expected)
151
+ return { error: `${path} không khớp mã SHA-256 trong danh mục — đã dừng cài` };
152
+ if (path === "SKILL.md")
153
+ skillMd = new TextDecoder().decode(bytes);
154
+ else
155
+ files.push({ path, content: bytes });
156
+ }
157
+ if (installed) {
158
+ const retired = retireLibrarySkill(id, installed.sha256);
159
+ if ("error" in retired)
160
+ return { error: retired.error };
161
+ if ("stale" in retired)
162
+ return { error: "Kỹ năng đã bị sửa tay sau khi cài, không cập nhật đè." };
163
+ }
164
+ const result = installLibrarySkill({
165
+ name: id,
166
+ instructions: skillMd,
167
+ source: `limix-hub:${id}@${entry.checksum} (${entry.source.repository ?? "?"}@${(entry.source.commit ?? "").slice(0, 7)})`,
168
+ ...(entry.license ? { license: entry.license } : {}),
169
+ ...(entry.compatibility.text ? { compatibility: entry.compatibility.text } : {}),
170
+ ...(entry.tags.length ? { tags: entry.tags } : {}),
171
+ warnings: entry.notes,
172
+ skippedFiles: skipped,
173
+ reviewState: "disabled",
174
+ ...(files.length ? { files } : {}),
175
+ });
176
+ if ("error" in result)
177
+ return { error: result.error };
178
+ return { ok: true, skipped };
179
+ }
180
+ export function setHubSkillEnabled(id, enabled) {
181
+ if (!installedFromHub().has(id))
182
+ return { error: "Kỹ năng này chưa cài từ Hub" };
183
+ const entry = hubCatalog().find((e) => e.id === id);
184
+ if (enabled && entry && hubSkillBlocked(entry))
185
+ return { error: hubSkillBlocked(entry) };
186
+ const result = setLibrarySkillReviewState(id, enabled ? "approved" : "disabled");
187
+ return "error" in result ? { error: result.error } : { ok: true };
188
+ }
189
+ export function removeHubSkill(id) {
190
+ const installed = installedFromHub().get(id);
191
+ if (!installed)
192
+ return { error: "Kỹ năng này chưa cài từ Hub" };
193
+ const result = retireLibrarySkill(id, installed.sha256);
194
+ if ("error" in result)
195
+ return { error: result.error };
196
+ if ("stale" in result)
197
+ return { error: "Kỹ năng đã bị sửa tay sau khi cài, không gỡ tự động." };
198
+ return { ok: true }; // the bytes stay in the library archive, so a removal can be undone by hand
199
+ }
200
+ /** Auto Install (off by default): only excellent/recommended, approved, instructions-only skills; max 5 per sync. */
201
+ async function autoInstallEligible() {
202
+ let budget = 5;
203
+ for (const skill of hubSkills()) {
204
+ if (budget <= 0)
205
+ return;
206
+ const eligible = !skill.blocked && skill.kind === "instructions-only" && skill.approval_status === "approved" && (skill.grade === "excellent" || skill.grade === "recommended");
207
+ if (!eligible || (skill.installed && !skill.updateAvailable))
208
+ continue;
209
+ budget -= 1;
210
+ const done = await installHubSkill(skill.id);
211
+ if ("ok" in done && skill.updateAvailable && skill.enabled)
212
+ setHubSkillEnabled(skill.id, true);
213
+ }
214
+ }
215
+ export async function handleLimixHub(method, path, body) {
216
+ const record = (body && typeof body === "object" ? body : {});
217
+ if (method === "GET" && path === "/api/limix/hub") {
218
+ return { status: 200, body: { repo: HUB_REPO, settings: hubSettings(), skills: hubSkills() } };
219
+ }
220
+ if (method === "POST" && path === "/api/limix/hub/sync") {
221
+ const result = await syncHub();
222
+ return "error" in result ? { status: 502, body: result } : { status: 200, body: result };
223
+ }
224
+ if (method === "POST" && path === "/api/limix/hub/settings") {
225
+ const current = hubSettings();
226
+ saveSettings({
227
+ ...current,
228
+ autoSync: typeof record.autoSync === "boolean" ? record.autoSync : current.autoSync,
229
+ autoInstall: typeof record.autoInstall === "boolean" ? record.autoInstall : current.autoInstall,
230
+ });
231
+ return { status: 200, body: { settings: hubSettings() } };
232
+ }
233
+ const id = typeof record.id === "string" ? record.id : "";
234
+ if (method === "POST" && (path === "/api/limix/hub/install" || path === "/api/limix/hub/update")) {
235
+ const result = await installHubSkill(id, { allowTools: record.allowTools === true });
236
+ return "error" in result ? { status: 422, body: result } : { status: 200, body: result };
237
+ }
238
+ if (method === "POST" && path === "/api/limix/hub/enable") {
239
+ const result = setHubSkillEnabled(id, record.enabled === true);
240
+ return "error" in result ? { status: 422, body: result } : { status: 200, body: result };
241
+ }
242
+ if (method === "POST" && path === "/api/limix/hub/remove") {
243
+ const result = removeHubSkill(id);
244
+ return "error" in result ? { status: 422, body: result } : { status: 200, body: result };
245
+ }
246
+ return null;
247
+ }
248
+ let started = false;
249
+ /** Auto Sync: once soon after start, then checked every 30 minutes; it only runs when 20 h have passed. */
250
+ export function startHubAutoSync() {
251
+ if (started)
252
+ return;
253
+ started = true;
254
+ const tick = () => {
255
+ const settings = hubSettings();
256
+ if (!settings.autoSync)
257
+ return;
258
+ if (settings.lastSync && Date.now() - Date.parse(settings.lastSync) < SYNC_MIN_GAP_MS)
259
+ return;
260
+ void syncHub();
261
+ };
262
+ setTimeout(tick, 60_000).unref();
263
+ setInterval(tick, 30 * 60 * 1000).unref();
264
+ }
@@ -4,6 +4,8 @@ import { setNativeSkillEnabled } from "../native-skill-choices.js";
4
4
  import { botSkillTemplateSchema } from "../new-bot-defaults.js";
5
5
  import { fetchSkillFromSource } from "../skill-fetch.js";
6
6
  import { readLocalSkills } from "../limix-skill-folder.js";
7
+ import { loadBundledSkills } from "../skill-library.js";
8
+ import { handleLimixHub } from "../limix-skill-hub.js";
7
9
  import { skillImportSummary } from "../skill-folder.js";
8
10
  import { installLibrarySkill, librarySkillOptOuts, listLibrarySkills, readLibrarySkillFile, readSkillLibraryIndex, setLibrarySkillEveryBot, setLibrarySkillOptOut, setLibrarySkillReviewState, skillsLibraryEnabled, unassignLibrarySkill } from "../skill-library.js";
9
11
  import { installSkill, listBotSkillsWithOrigin, listStagedSkillWrites, parseSkillMd, removeSkill, scanSkillText, setSkillEnabled, } from "../skills.js";
@@ -12,6 +14,15 @@ export function createBotSkillsRoutes(deps) {
12
14
  const { store, cfg, listBotSkills, readBotSkillFile, stagedSkillListing } = deps;
13
15
  return async ({ req, res, url, path, method, json, readBody }) => {
14
16
  let m = null;
17
+ if (path.startsWith("/api/limix/hub")) {
18
+ const hub = await handleLimixHub(method, path, method === "POST" ? await readBody(req) : undefined);
19
+ if (hub)
20
+ return json(res, hub.status, hub.body);
21
+ }
22
+ if (method === "GET" && path === "/api/limix/bundled-skills") {
23
+ const skills = loadBundledSkills().map(({ manifest }) => ({ id: manifest.id, name: manifest.name, description: manifest.description, defaultEnabled: manifest.defaultEnabled, triggerTerms: manifest.triggerTerms }));
24
+ return json(res, 200, { skills });
25
+ }
15
26
  // Imports land DISABLED: a person enables one after reading SKILL.md and its scan warnings.
16
27
  m = path.match(/^\/api\/bots\/([\w-]+)\/skill-template$/);
17
28
  if (m && method === "POST") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "limixbot",
3
- "version": "0.3.13",
3
+ "version": "0.3.15",
4
4
  "description": "Run the Limix Bot server anywhere and pair your devices to it (Leox ৹ Limix AI)",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",