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.
- package/dist/assets/{ActivityPanel-WwnzdBwg.js → ActivityPanel-CrqSA80q.js} +1 -1
- package/dist/assets/{App-xm5evxQc.js → App-Bx6hl9Sg.js} +129 -129
- package/dist/assets/{AppEventEditor-D4R4y0yV.js → AppEventEditor-DLfkpP1d.js} +1 -1
- package/dist/assets/{BotSettingsDialog-B6Nl9Kqn.js → BotSettingsDialog-rHcw4pr0.js} +1 -1
- package/dist/assets/{BrowserPanel-D95FHyTZ.js → BrowserPanel-BgNsYU3s.js} +1 -1
- package/dist/assets/{BrowserProfilesManager-7KU0ynoV.js → BrowserProfilesManager-ByQtDUyM.js} +1 -1
- package/dist/assets/{CloudAddDialog-eJTjUT_W.js → CloudAddDialog-754PKKwo.js} +1 -1
- package/dist/assets/{ComputerFilesPane-CDm1oiQb.js → ComputerFilesPane-IP5ib-4w.js} +1 -1
- package/dist/assets/{ComputerPanel-DsSTgobP.js → ComputerPanel-CndWeumv.js} +2 -2
- package/dist/assets/{DataPanel-BtBpnpvo.js → DataPanel-BWlfa8L7.js} +1 -1
- package/dist/assets/{DesktopViewer-BRzNtGEm.js → DesktopViewer-tgbnWJ7S.js} +1 -1
- package/dist/assets/{FilesView-BIOj6w1e.js → FilesView-PiPhchvH.js} +1 -1
- package/dist/assets/{GrokMoveDialog-C-UmQzuZ.js → GrokMoveDialog-DGBkFh2_.js} +1 -1
- package/dist/assets/{InspectorPanel-C13p36PD.js → InspectorPanel-DMb9KlS1.js} +1 -1
- package/dist/assets/{KeyboardShortcutsModal-CWOIkT5q.js → KeyboardShortcutsModal-DcAnacT0.js} +1 -1
- package/dist/assets/{LiveDesktop-B-OYWCWR.js → LiveDesktop-pNuAQp7j.js} +1 -1
- package/dist/assets/{LocalVmWorkspace-X4vOLsJm.js → LocalVmWorkspace-rUDRW4sg.js} +1 -1
- package/dist/assets/{MacLocalControl-DMOrG-td.js → MacLocalControl-D2_QV-or.js} +1 -1
- package/dist/assets/{MarkdownFilePreview-B4WXm-Zz.js → MarkdownFilePreview-DNNn-HhY.js} +1 -1
- package/dist/assets/{MenuMotion-BDYr4RxL.js → MenuMotion-CIhkQGNX.js} +1 -1
- package/dist/assets/{NewBotDialog-D5FKUF9t.js → NewBotDialog-C1onlYty.js} +1 -1
- package/dist/assets/{PhoneAppDialog-DGaR3ZqK.js → PhoneAppDialog-DygQ7EJ1.js} +1 -1
- package/dist/assets/{PlacesRail-yZdo57pP.js → PlacesRail-DC3DtwQy.js} +1 -1
- package/dist/assets/{PluginsPanel-Rr0WYhLE.js → PluginsPanel-BZv6B1JK.js} +1 -1
- package/dist/assets/{ProIntroduction-Bma87PIr.js → ProIntroduction-DeMFeFxH.js} +1 -1
- package/dist/assets/{RemoteAgentSettingsPanel-BEygLori.js → RemoteAgentSettingsPanel-DyVQB35x.js} +1 -1
- package/dist/assets/{RoomPanel-xOsJAq9r.js → RoomPanel-DEH49als.js} +1 -1
- package/dist/assets/{RoutinesPage-ntwDQROW.js → RoutinesPage-CRI83T_I.js} +1 -1
- package/dist/assets/SettingsModal-Dcdk0Qvv.js +11 -0
- package/dist/assets/{ShareTeamDialog-C_2Klxsb.js → ShareTeamDialog-D5ML8qDY.js} +1 -1
- package/dist/assets/{SignedInSites-6gzIcTSf.js → SignedInSites-Bjc69Ty-.js} +1 -1
- package/dist/assets/{TeamLibraryPanel-B1t0-XZ-.js → TeamLibraryPanel-CVcmOn6k.js} +1 -1
- package/dist/assets/{TeamMapPage-Cb2qYz3_.js → TeamMapPage-C3-Cu90W.js} +1 -1
- package/dist/assets/{TriggersPage-wBKXT1VB.js → TriggersPage-DS2Ceion.js} +1 -1
- package/dist/assets/{VisibilitySection-Bd8MQdKE.js → VisibilitySection-DigpYJ5s.js} +1 -1
- package/dist/assets/{WebhookTrigger-Bvx80Ue5.js → WebhookTrigger-BP28l-Jy.js} +1 -1
- package/dist/assets/{WorksOnGrid-TTZtT4m-.js → WorksOnGrid-dVy5M1Fj.js} +1 -1
- package/dist/assets/{abnfDiagram-YUKMZFIV-Boj-Bkbs.js → abnfDiagram-YUKMZFIV-DE2rjoCc.js} +1 -1
- package/dist/assets/{activity-MpAxS_kw.js → activity-8Dgeq15H.js} +1 -1
- package/dist/assets/{architectureDiagram-47P4ROYG-Cl-ocqce.js → architectureDiagram-47P4ROYG-DrIn6OKQ.js} +1 -1
- package/dist/assets/{blockDiagram-E7TT5TSB-0nSQiv6g.js → blockDiagram-E7TT5TSB-BQdVPMCR.js} +1 -1
- package/dist/assets/{c4Diagram-CRA5TL53-Cx9iymkb.js → c4Diagram-CRA5TL53-DFb8MJmm.js} +1 -1
- package/dist/assets/{calendar-days-DXHDRm_4.js → calendar-days-Bs4Es4HT.js} +1 -1
- package/dist/assets/channel-1lG6Uyuf.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-0rIrumUG.js → chunk-2Q5K7J3B-BXaLRTO9.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-w9e6iKvs.js → chunk-5VM5RSS4-Cie3uAC2.js} +1 -1
- package/dist/assets/{chunk-7TKQ45FW-c4KxVKkf.js → chunk-7TKQ45FW-Bw5vFHu9.js} +1 -1
- package/dist/assets/{chunk-AW2ZBBNX-DmaBcuOP.js → chunk-AW2ZBBNX-CkxRWzwT.js} +1 -1
- package/dist/assets/{chunk-FVRAUYC3-BLzfqLHR.js → chunk-FVRAUYC3-7FMS_2yS.js} +2 -2
- package/dist/assets/{chunk-HTAEGDNF-BN48XtBm.js → chunk-HTAEGDNF-DQ_kiZYj.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-D2NACa6i.js → chunk-JWPE2WC7-DD4AjX_J.js} +1 -1
- package/dist/assets/{chunk-KQW6MTUR-CbAfnxxG.js → chunk-KQW6MTUR-DAs7Un1F.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-Bwg-DPP_.js → chunk-XXDRQBXY-qEAG8aNp.js} +1 -1
- package/dist/assets/{classDiagram-v2-K4WV4PDN-Dh3ohoZp.js → classDiagram-v2-K4WV4PDN-SFDos9_W.js} +1 -1
- package/dist/assets/{coins-Bv8-qll4.js → coins-ni7p9MMO.js} +1 -1
- package/dist/assets/{core-Dr_lGnx7.js → core-CMus8yJ9.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-DCHadn0L.js → cose-bilkent-JH36ORCC-BIvnumq9.js} +1 -1
- package/dist/assets/{cynefin-EF2NZ3EQ-ClpPPxUg.js → cynefin-EF2NZ3EQ-DEyZ973A.js} +1 -1
- package/dist/assets/{cynefinDiagram-3GCD6N5R-DZ5l7nkL.js → cynefinDiagram-3GCD6N5R-DIH2LA6q.js} +1 -1
- package/dist/assets/{dagre-W4DXFKR2-BagTvT62.js → dagre-W4DXFKR2-doItNion.js} +1 -1
- package/dist/assets/{diagram-2UJZ2QOL-DuByootb.js → diagram-2UJZ2QOL-DKfcZ3Fe.js} +1 -1
- package/dist/assets/{diagram-OVF4WLC6-BQ8Cvr0D.js → diagram-OVF4WLC6-BRnKWMs5.js} +1 -1
- package/dist/assets/{diagram-PFPMY2P6-D_pklaf9.js → diagram-PFPMY2P6-C3aLszxc.js} +1 -1
- package/dist/assets/{diagram-UMYRVEAY-1BuHq7tR.js → diagram-UMYRVEAY-DbMKM9Kw.js} +1 -1
- package/dist/assets/{diagram-YEKJPTXX-CK0cPx8n.js → diagram-YEKJPTXX-BqrhdTTX.js} +1 -1
- package/dist/assets/{diagram-ZIFT7M5P-DVzPho45.js → diagram-ZIFT7M5P-CYnUFm0_.js} +1 -1
- package/dist/assets/{ebnfDiagram-VR2GEFS7-D8UO_5sE.js → ebnfDiagram-VR2GEFS7-CnuOWBxY.js} +1 -1
- package/dist/assets/{elk-IJKZMXRS-BRigfR5V.js → elk-IJKZMXRS-BmtbqAAs.js} +1 -1
- package/dist/assets/{erDiagram-O2IAPWRE-CXHPobWj.js → erDiagram-O2IAPWRE-CL6oNmZD.js} +1 -1
- package/dist/assets/{file-up-CQezfdJk.js → file-up-DT7t5NXT.js} +1 -1
- package/dist/assets/{flowDiagram-OXPTDLAJ-CqKD6jh8.js → flowDiagram-OXPTDLAJ-CE9hfMoK.js} +1 -1
- package/dist/assets/{ganttDiagram-R7TSEDQI-C9FV3gsA.js → ganttDiagram-R7TSEDQI-DRU_A2cB.js} +1 -1
- package/dist/assets/{gitGraphDiagram-XJZIOB7I-CRu4dnr_.js → gitGraphDiagram-XJZIOB7I-BPkWYlKZ.js} +1 -1
- package/dist/assets/{import-name-CrmN5zUf.js → import-name-DutQXK90.js} +1 -1
- package/dist/assets/{index-aOwy9fcM.js → index-C6OVbzeZ.js} +2 -2
- package/dist/assets/{index-D8CUjCIJ.js → index-CiYcp06m.js} +1 -1
- package/dist/assets/{infoDiagram-5W2HQ5XZ-CCRjb2CQ.js → infoDiagram-5W2HQ5XZ-C4SBhDtT.js} +1 -1
- package/dist/assets/{ishikawaDiagram-K3B6WC7H-TaNPsGZS.js → ishikawaDiagram-K3B6WC7H-3TlhKRsV.js} +1 -1
- package/dist/assets/{journeyDiagram-COXZFDF6-Dz7YIIc2.js → journeyDiagram-COXZFDF6-K9dyCbpb.js} +1 -1
- package/dist/assets/{kanban-definition-P3RFRI5V-Cg69Fpp8.js → kanban-definition-P3RFRI5V-Df8i0B0H.js} +1 -1
- package/dist/assets/{mcp-servers-DwkY4xUh.js → mcp-servers-xuxWmmEV.js} +1 -1
- package/dist/assets/{mermaid.core-DiG3LoLz.js → mermaid.core-Dj_sBsXx.js} +4 -4
- package/dist/assets/{mindmap-definition-LZFPQGKD-nxP4f0fq.js → mindmap-definition-LZFPQGKD-CuBxqBw1.js} +1 -1
- package/dist/assets/{minimize-B_k_fmmo.js → minimize-CuDlETLH.js} +1 -1
- package/dist/assets/{monitor-BfmnPU-I.js → monitor-DqFADU6d.js} +1 -1
- package/dist/assets/{new-bot-D8qQ_VN7.js → new-bot-DoJRfwOC.js} +1 -1
- package/dist/assets/{org-library-DfX89pz8.js → org-library-B7nhJQxt.js} +1 -1
- package/dist/assets/{package-format-DLfgEUES.js → package-format-FvzPr9Xd.js} +1 -1
- package/dist/assets/{pegDiagram-BGZESJAR-gPjpGcNH.js → pegDiagram-BGZESJAR-BDWRqrLr.js} +1 -1
- package/dist/assets/{pieDiagram-CAPJLFHJ-Cgh25Z4_.js → pieDiagram-CAPJLFHJ-BByTBeRn.js} +1 -1
- package/dist/assets/{quadrantDiagram-GDMTTRAM-DD9luyqA.js → quadrantDiagram-GDMTTRAM-D0SI7j3Y.js} +1 -1
- package/dist/assets/{rail-B1mj4zlo.js → rail-BEOIK49V.js} +1 -1
- package/dist/assets/{railroadDiagram-SM67HX2B-CS_TYko7.js → railroadDiagram-SM67HX2B-Dz3tUGbx.js} +1 -1
- package/dist/assets/{remote-desktop-panel-Gb0B8rvx.js → remote-desktop-panel-DgZionIJ.js} +1 -1
- package/dist/assets/{requirementDiagram-X7JNWC4A-BePHia6E.js → requirementDiagram-X7JNWC4A-DrwqW7OV.js} +1 -1
- package/dist/assets/{sankeyDiagram-UM26HJRW-24ExCXGS.js → sankeyDiagram-UM26HJRW-95WqcM7l.js} +1 -1
- package/dist/assets/{sequenceDiagram-ZO4K6R2Y-Dtmx27I5.js → sequenceDiagram-ZO4K6R2Y-1zoh2EeE.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-2LvmoT1T.js → sizeCapture-INFHLROL-IzBNvZN8.js} +1 -1
- package/dist/assets/{stateDiagram-v2-TBUQTH76-moL2SIo9.js → stateDiagram-v2-TBUQTH76-CXdapkBV.js} +1 -1
- package/dist/assets/styles-CSArda2J.css +1 -0
- package/dist/assets/{swimlanes-N4OXWK64-D95Pk8qh.js → swimlanes-N4OXWK64-QK7_rFPB.js} +1 -1
- package/dist/assets/swimlanesDiagram-K3J5GTZL-B4Zj1xuq.js +8 -0
- package/dist/assets/{team-share-0GXKeixY.js → team-share-NJEAQGUX.js} +1 -1
- package/dist/assets/{timeline-definition-YOQAKGHF-HM-J25k2.js → timeline-definition-YOQAKGHF-MhSu-mD6.js} +1 -1
- package/dist/assets/{usecaseDiagram-VIAY4XPW-C6q6bCzJ.js → usecaseDiagram-VIAY4XPW-DdZIcDLN.js} +1 -1
- package/dist/assets/{vennDiagram-BLWOH2XV-hf0dh2yT.js → vennDiagram-BLWOH2XV-DDMi9wJz.js} +1 -1
- package/dist/assets/{wardleyDiagram-YMQ3BMBF-CpT_eRn9.js → wardleyDiagram-YMQ3BMBF-DrZpbJKY.js} +1 -1
- package/dist/assets/{xychartDiagram-TAQBALBS-D6g1V9DG.js → xychartDiagram-TAQBALBS-__UkZCKS.js} +1 -1
- package/dist/index.html +3 -3
- package/dist/rail.html +5 -5
- package/dist-server/index.js +307 -289
- package/dist-server/package.json +1 -1
- package/dist-server/server/index.js +2 -0
- package/dist-server/server/limix-skill-hub.js +264 -0
- package/dist-server/server/routes/bot-skills.js +11 -0
- package/package.json +1 -1
- package/dist/assets/SettingsModal-CZ_GMpWi.js +0 -11
- package/dist/assets/channel-BStWWrSt.js +0 -1
- package/dist/assets/styles-bUbK33f1.css +0 -1
- package/dist/assets/swimlanesDiagram-K3J5GTZL-DByi9G3i.js +0 -8
- /package/dist/assets/{styles-6RbfQIY7.js → styles-Btbbe7r3.js} +0 -0
package/dist-server/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"type":"module","version":"0.3.
|
|
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") {
|