@emblemvault/hustle-react 1.0.0 → 1.1.1
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/browser/hustle-react.js +856 -45
- package/dist/browser/hustle-react.js.map +1 -1
- package/dist/components/index.cjs +807 -45
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +807 -45
- package/dist/components/index.js.map +1 -1
- package/dist/hooks/index.cjs +80 -14
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.d.cts +3 -2
- package/dist/hooks/index.d.ts +3 -2
- package/dist/hooks/index.js +80 -14
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.cjs +856 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +856 -45
- package/dist/index.js.map +1 -1
- package/dist/{plugin-BUg7vMxe.d.cts → plugin-COr42J6-.d.cts} +3 -1
- package/dist/{plugin-BUg7vMxe.d.ts → plugin-COr42J6-.d.ts} +3 -1
- package/dist/plugins/index.cjs +777 -31
- package/dist/plugins/index.cjs.map +1 -1
- package/dist/plugins/index.d.cts +14 -2
- package/dist/plugins/index.d.ts +14 -2
- package/dist/plugins/index.js +777 -32
- package/dist/plugins/index.js.map +1 -1
- package/dist/providers/index.cjs +80 -14
- package/dist/providers/index.cjs.map +1 -1
- package/dist/providers/index.js +80 -14
- package/dist/providers/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -276,35 +276,52 @@ var pluginRegistry = new PluginRegistry();
|
|
|
276
276
|
function getStorageKey(instanceId) {
|
|
277
277
|
return `hustle-plugins-${instanceId}`;
|
|
278
278
|
}
|
|
279
|
-
function
|
|
279
|
+
function getInstanceId(providedId) {
|
|
280
|
+
if (providedId) return providedId;
|
|
281
|
+
if (typeof window !== "undefined") {
|
|
282
|
+
const globalId = window.__hustleInstanceId;
|
|
283
|
+
if (globalId) return globalId;
|
|
284
|
+
}
|
|
285
|
+
return "default";
|
|
286
|
+
}
|
|
287
|
+
function usePlugins(instanceId) {
|
|
288
|
+
const [resolvedInstanceId] = useState(() => getInstanceId(instanceId));
|
|
280
289
|
const [plugins, setPlugins] = useState([]);
|
|
281
290
|
useEffect(() => {
|
|
282
|
-
setPlugins(pluginRegistry.loadFromStorage(
|
|
283
|
-
const unsubscribe = pluginRegistry.onChange(setPlugins,
|
|
284
|
-
const storageKey = getStorageKey(
|
|
291
|
+
setPlugins(pluginRegistry.loadFromStorage(resolvedInstanceId));
|
|
292
|
+
const unsubscribe = pluginRegistry.onChange(setPlugins, resolvedInstanceId);
|
|
293
|
+
const storageKey = getStorageKey(resolvedInstanceId);
|
|
285
294
|
const handleStorage = (e) => {
|
|
286
295
|
if (e.key === storageKey) {
|
|
287
|
-
setPlugins(pluginRegistry.loadFromStorage(
|
|
296
|
+
setPlugins(pluginRegistry.loadFromStorage(resolvedInstanceId));
|
|
288
297
|
}
|
|
289
298
|
};
|
|
290
299
|
window.addEventListener("storage", handleStorage);
|
|
300
|
+
const handlePluginInstalled = (e) => {
|
|
301
|
+
const customEvent = e;
|
|
302
|
+
if (customEvent.detail.instanceId === resolvedInstanceId) {
|
|
303
|
+
setPlugins(pluginRegistry.loadFromStorage(resolvedInstanceId));
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
window.addEventListener("hustle-plugin-installed", handlePluginInstalled);
|
|
291
307
|
return () => {
|
|
292
308
|
unsubscribe();
|
|
293
309
|
window.removeEventListener("storage", handleStorage);
|
|
310
|
+
window.removeEventListener("hustle-plugin-installed", handlePluginInstalled);
|
|
294
311
|
};
|
|
295
|
-
}, [
|
|
312
|
+
}, [resolvedInstanceId]);
|
|
296
313
|
const registerPlugin = useCallback((plugin) => {
|
|
297
|
-
pluginRegistry.register(plugin, true,
|
|
298
|
-
}, [
|
|
314
|
+
pluginRegistry.register(plugin, true, resolvedInstanceId);
|
|
315
|
+
}, [resolvedInstanceId]);
|
|
299
316
|
const unregisterPlugin = useCallback((name) => {
|
|
300
|
-
pluginRegistry.unregister(name,
|
|
301
|
-
}, [
|
|
317
|
+
pluginRegistry.unregister(name, resolvedInstanceId);
|
|
318
|
+
}, [resolvedInstanceId]);
|
|
302
319
|
const enablePlugin = useCallback((name) => {
|
|
303
|
-
pluginRegistry.setEnabled(name, true,
|
|
304
|
-
}, [
|
|
320
|
+
pluginRegistry.setEnabled(name, true, resolvedInstanceId);
|
|
321
|
+
}, [resolvedInstanceId]);
|
|
305
322
|
const disablePlugin = useCallback((name) => {
|
|
306
|
-
pluginRegistry.setEnabled(name, false,
|
|
307
|
-
}, [
|
|
323
|
+
pluginRegistry.setEnabled(name, false, resolvedInstanceId);
|
|
324
|
+
}, [resolvedInstanceId]);
|
|
308
325
|
const isRegistered = useCallback(
|
|
309
326
|
(name) => plugins.some((p) => p.name === name),
|
|
310
327
|
[plugins]
|
|
@@ -359,6 +376,42 @@ function HustleProvider({
|
|
|
359
376
|
}
|
|
360
377
|
};
|
|
361
378
|
}, [isAutoInstance, resolvedInstanceId]);
|
|
379
|
+
useEffect(() => {
|
|
380
|
+
if (typeof window !== "undefined") {
|
|
381
|
+
const win = window;
|
|
382
|
+
win.__hustleInstanceId = resolvedInstanceId;
|
|
383
|
+
win.__hustleRegisterPlugin = async (plugin, enabled = true) => {
|
|
384
|
+
const hydrated = hydratePlugin(plugin);
|
|
385
|
+
pluginRegistry.register(hydrated, enabled, resolvedInstanceId);
|
|
386
|
+
};
|
|
387
|
+
win.__hustleUnregisterPlugin = async (name) => {
|
|
388
|
+
pluginRegistry.unregister(name, resolvedInstanceId);
|
|
389
|
+
};
|
|
390
|
+
win.__hustleListPlugins = () => {
|
|
391
|
+
const plugins = pluginRegistry.loadFromStorage(resolvedInstanceId);
|
|
392
|
+
return plugins.map((p) => ({
|
|
393
|
+
name: p.name,
|
|
394
|
+
version: p.version,
|
|
395
|
+
description: p.description || "",
|
|
396
|
+
enabled: p.enabled
|
|
397
|
+
}));
|
|
398
|
+
};
|
|
399
|
+
win.__hustleGetPlugin = (name) => {
|
|
400
|
+
const plugins = pluginRegistry.loadFromStorage(resolvedInstanceId);
|
|
401
|
+
return plugins.find((p) => p.name === name) || null;
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
return () => {
|
|
405
|
+
if (typeof window !== "undefined") {
|
|
406
|
+
const win = window;
|
|
407
|
+
delete win.__hustleInstanceId;
|
|
408
|
+
delete win.__hustleRegisterPlugin;
|
|
409
|
+
delete win.__hustleUnregisterPlugin;
|
|
410
|
+
delete win.__hustleListPlugins;
|
|
411
|
+
delete win.__hustleGetPlugin;
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
}, [resolvedInstanceId]);
|
|
362
415
|
const isApiKeyMode = Boolean(apiKey && vaultId);
|
|
363
416
|
const authContext = useEmblemAuthOptional();
|
|
364
417
|
const authSDK = isApiKeyMode ? null : authContext?.authSDK ?? null;
|
|
@@ -506,6 +559,19 @@ function HustleProvider({
|
|
|
506
559
|
};
|
|
507
560
|
registerPlugins();
|
|
508
561
|
}, [client, enabledPlugins, log]);
|
|
562
|
+
useEffect(() => {
|
|
563
|
+
if (typeof window !== "undefined" && client) {
|
|
564
|
+
const win = window;
|
|
565
|
+
win.__hustleUploadFile = async (file, fileName) => {
|
|
566
|
+
return await client.uploadFile(file, fileName);
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
return () => {
|
|
570
|
+
if (typeof window !== "undefined") {
|
|
571
|
+
delete window.__hustleUploadFile;
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
}, [client]);
|
|
509
575
|
const loadModels = useCallback(async () => {
|
|
510
576
|
if (!client) {
|
|
511
577
|
log("Cannot load models - client not ready");
|
|
@@ -1386,12 +1452,16 @@ function ensureModalStyles() {
|
|
|
1386
1452
|
left: 0;
|
|
1387
1453
|
right: 0;
|
|
1388
1454
|
bottom: 0;
|
|
1389
|
-
background:
|
|
1455
|
+
background: transparent;
|
|
1390
1456
|
display: flex;
|
|
1391
1457
|
align-items: center;
|
|
1392
1458
|
justify-content: center;
|
|
1393
1459
|
z-index: 10000;
|
|
1394
|
-
|
|
1460
|
+
pointer-events: none;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
.user-question-modal {
|
|
1464
|
+
pointer-events: auto;
|
|
1395
1465
|
}
|
|
1396
1466
|
|
|
1397
1467
|
@keyframes uqFadeIn {
|
|
@@ -1504,6 +1574,27 @@ function ensureModalStyles() {
|
|
|
1504
1574
|
color: #666;
|
|
1505
1575
|
cursor: not-allowed;
|
|
1506
1576
|
}
|
|
1577
|
+
|
|
1578
|
+
.user-question-custom-input {
|
|
1579
|
+
width: 100%;
|
|
1580
|
+
padding: 8px 12px;
|
|
1581
|
+
margin-top: 8px;
|
|
1582
|
+
background: #1a1a2e;
|
|
1583
|
+
border: 1px solid #444;
|
|
1584
|
+
border-radius: 6px;
|
|
1585
|
+
color: #e0e0e0;
|
|
1586
|
+
font-size: 14px;
|
|
1587
|
+
box-sizing: border-box;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
.user-question-custom-input:focus {
|
|
1591
|
+
outline: none;
|
|
1592
|
+
border-color: #4a7aff;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
.user-question-custom-input::placeholder {
|
|
1596
|
+
color: #666;
|
|
1597
|
+
}
|
|
1507
1598
|
`;
|
|
1508
1599
|
document.head.appendChild(styles2);
|
|
1509
1600
|
}
|
|
@@ -1525,13 +1616,17 @@ var askUserTool = {
|
|
|
1525
1616
|
allowMultiple: {
|
|
1526
1617
|
type: "boolean",
|
|
1527
1618
|
description: "If true, user can select multiple choices. Default: false"
|
|
1619
|
+
},
|
|
1620
|
+
allowCustom: {
|
|
1621
|
+
type: "boolean",
|
|
1622
|
+
description: 'If true, adds an "Other" option where user can type a custom response. Default: false'
|
|
1528
1623
|
}
|
|
1529
1624
|
},
|
|
1530
1625
|
required: ["question", "choices"]
|
|
1531
1626
|
}
|
|
1532
1627
|
};
|
|
1533
1628
|
var askUserExecutor = async (args2) => {
|
|
1534
|
-
const { question, choices, allowMultiple = false } = args2;
|
|
1629
|
+
const { question, choices, allowMultiple = false, allowCustom = false } = args2;
|
|
1535
1630
|
if (!question || !choices || !Array.isArray(choices) || choices.length === 0) {
|
|
1536
1631
|
return {
|
|
1537
1632
|
question: question || "",
|
|
@@ -1563,6 +1658,17 @@ var askUserExecutor = async (args2) => {
|
|
|
1563
1658
|
choicesDiv.className = "user-question-choices";
|
|
1564
1659
|
const inputType = allowMultiple ? "checkbox" : "radio";
|
|
1565
1660
|
const inputName = `uq-${Date.now()}`;
|
|
1661
|
+
let customInput = null;
|
|
1662
|
+
let isCustomSelected = false;
|
|
1663
|
+
const submitBtn = document.createElement("button");
|
|
1664
|
+
submitBtn.className = "user-question-btn user-question-btn-submit";
|
|
1665
|
+
submitBtn.textContent = "Submit";
|
|
1666
|
+
submitBtn.disabled = true;
|
|
1667
|
+
const updateSubmitButton = () => {
|
|
1668
|
+
const hasSelection = selected.size > 0;
|
|
1669
|
+
const hasCustomValue = isCustomSelected && customInput && customInput.value.trim().length > 0;
|
|
1670
|
+
submitBtn.disabled = !hasSelection && !hasCustomValue;
|
|
1671
|
+
};
|
|
1566
1672
|
choices.forEach((choice, index) => {
|
|
1567
1673
|
const choiceDiv = document.createElement("div");
|
|
1568
1674
|
choiceDiv.className = "user-question-choice";
|
|
@@ -1587,6 +1693,8 @@ var askUserExecutor = async (args2) => {
|
|
|
1587
1693
|
}
|
|
1588
1694
|
} else {
|
|
1589
1695
|
selected.clear();
|
|
1696
|
+
isCustomSelected = false;
|
|
1697
|
+
if (customInput) customInput.value = "";
|
|
1590
1698
|
selected.add(choice);
|
|
1591
1699
|
choicesDiv.querySelectorAll(".user-question-choice").forEach((c) => c.classList.remove("selected"));
|
|
1592
1700
|
choiceDiv.classList.add("selected");
|
|
@@ -1602,9 +1710,62 @@ var askUserExecutor = async (args2) => {
|
|
|
1602
1710
|
});
|
|
1603
1711
|
choicesDiv.appendChild(choiceDiv);
|
|
1604
1712
|
});
|
|
1713
|
+
if (allowCustom) {
|
|
1714
|
+
const customChoiceDiv = document.createElement("div");
|
|
1715
|
+
customChoiceDiv.className = "user-question-choice";
|
|
1716
|
+
const customRadio = document.createElement("input");
|
|
1717
|
+
customRadio.type = inputType;
|
|
1718
|
+
customRadio.name = inputName;
|
|
1719
|
+
customRadio.id = `${inputName}-custom`;
|
|
1720
|
+
customRadio.value = "__custom__";
|
|
1721
|
+
const customLabel = document.createElement("label");
|
|
1722
|
+
customLabel.htmlFor = customRadio.id;
|
|
1723
|
+
customLabel.textContent = "Other:";
|
|
1724
|
+
customLabel.style.flexShrink = "0";
|
|
1725
|
+
customInput = document.createElement("input");
|
|
1726
|
+
customInput.type = "text";
|
|
1727
|
+
customInput.className = "user-question-custom-input";
|
|
1728
|
+
customInput.placeholder = "Type your answer...";
|
|
1729
|
+
customInput.style.marginTop = "0";
|
|
1730
|
+
customInput.style.marginLeft = "8px";
|
|
1731
|
+
customInput.style.flex = "1";
|
|
1732
|
+
customChoiceDiv.appendChild(customRadio);
|
|
1733
|
+
customChoiceDiv.appendChild(customLabel);
|
|
1734
|
+
customChoiceDiv.appendChild(customInput);
|
|
1735
|
+
const handleCustomSelect = () => {
|
|
1736
|
+
if (!allowMultiple) {
|
|
1737
|
+
selected.clear();
|
|
1738
|
+
choicesDiv.querySelectorAll(".user-question-choice").forEach((c) => c.classList.remove("selected"));
|
|
1739
|
+
}
|
|
1740
|
+
isCustomSelected = true;
|
|
1741
|
+
customChoiceDiv.classList.add("selected");
|
|
1742
|
+
customInput?.focus();
|
|
1743
|
+
updateSubmitButton();
|
|
1744
|
+
};
|
|
1745
|
+
customRadio.addEventListener("change", handleCustomSelect);
|
|
1746
|
+
customChoiceDiv.addEventListener("click", (e) => {
|
|
1747
|
+
if (e.target !== customRadio && e.target !== customInput) {
|
|
1748
|
+
customRadio.checked = true;
|
|
1749
|
+
handleCustomSelect();
|
|
1750
|
+
}
|
|
1751
|
+
});
|
|
1752
|
+
customInput.addEventListener("focus", () => {
|
|
1753
|
+
if (!customRadio.checked) {
|
|
1754
|
+
customRadio.checked = true;
|
|
1755
|
+
handleCustomSelect();
|
|
1756
|
+
}
|
|
1757
|
+
});
|
|
1758
|
+
customInput.addEventListener("input", updateSubmitButton);
|
|
1759
|
+
choicesDiv.appendChild(customChoiceDiv);
|
|
1760
|
+
}
|
|
1605
1761
|
modal.appendChild(choicesDiv);
|
|
1606
1762
|
const actions = document.createElement("div");
|
|
1607
1763
|
actions.className = "user-question-actions";
|
|
1764
|
+
overlay.appendChild(modal);
|
|
1765
|
+
document.body.appendChild(overlay);
|
|
1766
|
+
const cleanup = () => {
|
|
1767
|
+
overlay.remove();
|
|
1768
|
+
};
|
|
1608
1769
|
const cancelBtn = document.createElement("button");
|
|
1609
1770
|
cancelBtn.className = "user-question-btn user-question-btn-cancel";
|
|
1610
1771
|
cancelBtn.textContent = "Skip";
|
|
@@ -1616,29 +1777,21 @@ var askUserExecutor = async (args2) => {
|
|
|
1616
1777
|
answered: false
|
|
1617
1778
|
});
|
|
1618
1779
|
};
|
|
1619
|
-
const submitBtn = document.createElement("button");
|
|
1620
|
-
submitBtn.className = "user-question-btn user-question-btn-submit";
|
|
1621
|
-
submitBtn.textContent = "Submit";
|
|
1622
|
-
submitBtn.disabled = true;
|
|
1623
1780
|
submitBtn.onclick = () => {
|
|
1624
1781
|
cleanup();
|
|
1782
|
+
const results = Array.from(selected);
|
|
1783
|
+
if (isCustomSelected && customInput && customInput.value.trim()) {
|
|
1784
|
+
results.push(customInput.value.trim());
|
|
1785
|
+
}
|
|
1625
1786
|
resolve({
|
|
1626
1787
|
question,
|
|
1627
|
-
selectedChoices:
|
|
1788
|
+
selectedChoices: results,
|
|
1628
1789
|
answered: true
|
|
1629
1790
|
});
|
|
1630
1791
|
};
|
|
1631
|
-
const updateSubmitButton = () => {
|
|
1632
|
-
submitBtn.disabled = selected.size === 0;
|
|
1633
|
-
};
|
|
1634
1792
|
actions.appendChild(cancelBtn);
|
|
1635
1793
|
actions.appendChild(submitBtn);
|
|
1636
1794
|
modal.appendChild(actions);
|
|
1637
|
-
overlay.appendChild(modal);
|
|
1638
|
-
document.body.appendChild(overlay);
|
|
1639
|
-
const cleanup = () => {
|
|
1640
|
-
overlay.remove();
|
|
1641
|
-
};
|
|
1642
1795
|
const handleEscape = (e) => {
|
|
1643
1796
|
if (e.key === "Escape") {
|
|
1644
1797
|
document.removeEventListener("keydown", handleEscape);
|
|
@@ -1807,6 +1960,11 @@ var screenshotTool = {
|
|
|
1807
1960
|
|
|
1808
1961
|
The screenshot captures the visible viewport of the page. The image is uploaded to the server and a permanent URL is returned.
|
|
1809
1962
|
|
|
1963
|
+
IMPORTANT: Before taking a screenshot, use the ask_user tool (if available) to ask which size they prefer:
|
|
1964
|
+
- "full" (100%) - highest quality, larger file
|
|
1965
|
+
- "half" (50%) - good balance of quality and size
|
|
1966
|
+
- "quarter" (25%) - smallest file, faster upload
|
|
1967
|
+
|
|
1810
1968
|
Use this when:
|
|
1811
1969
|
- User asks to see what's on their screen
|
|
1812
1970
|
- You need to analyze the current page visually
|
|
@@ -1817,12 +1975,24 @@ Use this when:
|
|
|
1817
1975
|
selector: {
|
|
1818
1976
|
type: "string",
|
|
1819
1977
|
description: "Optional CSS selector to capture a specific element instead of the full page. Leave empty for full page screenshot."
|
|
1978
|
+
},
|
|
1979
|
+
size: {
|
|
1980
|
+
type: "string",
|
|
1981
|
+
enum: ["full", "half", "quarter"],
|
|
1982
|
+
description: 'Image size: "full" (100%), "half" (50%), or "quarter" (25%). Ask the user which size they prefer before capturing.'
|
|
1820
1983
|
}
|
|
1821
1984
|
}
|
|
1822
1985
|
}
|
|
1823
1986
|
};
|
|
1824
1987
|
var screenshotExecutor = async (args2) => {
|
|
1825
1988
|
const selector = args2.selector;
|
|
1989
|
+
const size = args2.size || "full";
|
|
1990
|
+
const scaleMap = {
|
|
1991
|
+
full: 1,
|
|
1992
|
+
half: 0.5,
|
|
1993
|
+
quarter: 0.25
|
|
1994
|
+
};
|
|
1995
|
+
const scale = scaleMap[size] || 1;
|
|
1826
1996
|
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
1827
1997
|
return {
|
|
1828
1998
|
success: false,
|
|
@@ -1843,34 +2013,41 @@ var screenshotExecutor = async (args2) => {
|
|
|
1843
2013
|
if (!target) {
|
|
1844
2014
|
return { success: false, error: "Element not found: " + selector };
|
|
1845
2015
|
}
|
|
1846
|
-
const
|
|
2016
|
+
const fullCanvas = await window.html2canvas(target, {
|
|
1847
2017
|
useCORS: true,
|
|
1848
2018
|
allowTaint: true,
|
|
1849
2019
|
backgroundColor: "#000000",
|
|
1850
2020
|
scale: 1
|
|
1851
2021
|
});
|
|
2022
|
+
let finalCanvas = fullCanvas;
|
|
2023
|
+
if (scale < 1) {
|
|
2024
|
+
const resizedCanvas = document.createElement("canvas");
|
|
2025
|
+
resizedCanvas.width = Math.round(fullCanvas.width * scale);
|
|
2026
|
+
resizedCanvas.height = Math.round(fullCanvas.height * scale);
|
|
2027
|
+
const ctx = resizedCanvas.getContext("2d");
|
|
2028
|
+
if (ctx) {
|
|
2029
|
+
ctx.drawImage(fullCanvas, 0, 0, resizedCanvas.width, resizedCanvas.height);
|
|
2030
|
+
finalCanvas = resizedCanvas;
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
1852
2033
|
const blob = await new Promise((resolve) => {
|
|
1853
|
-
|
|
2034
|
+
finalCanvas.toBlob(resolve, "image/png", 0.9);
|
|
1854
2035
|
});
|
|
1855
2036
|
if (!blob) {
|
|
1856
2037
|
return { success: false, error: "Failed to create image blob" };
|
|
1857
2038
|
}
|
|
1858
|
-
const
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
method: "POST",
|
|
1862
|
-
body: formData
|
|
1863
|
-
});
|
|
1864
|
-
if (!response.ok) {
|
|
1865
|
-
const errorData = await response.json().catch(() => ({}));
|
|
1866
|
-
return { success: false, error: errorData.error || "Upload failed" };
|
|
2039
|
+
const uploadFn = window.__hustleUploadFile;
|
|
2040
|
+
if (!uploadFn) {
|
|
2041
|
+
return { success: false, error: "Upload not available. Make sure HustleProvider is mounted and client is ready." };
|
|
1867
2042
|
}
|
|
1868
|
-
const
|
|
2043
|
+
const fileName = "screenshot-" + Date.now() + ".png";
|
|
2044
|
+
const attachment = await uploadFn(blob, fileName);
|
|
2045
|
+
const sizeLabel = size === "full" ? "100%" : size === "half" ? "50%" : "25%";
|
|
1869
2046
|
return {
|
|
1870
2047
|
success: true,
|
|
1871
|
-
url:
|
|
1872
|
-
contentType:
|
|
1873
|
-
message:
|
|
2048
|
+
url: attachment.url,
|
|
2049
|
+
contentType: attachment.contentType || "image/png",
|
|
2050
|
+
message: `Screenshot captured at ${sizeLabel} size (${finalCanvas.width}x${finalCanvas.height}) and uploaded successfully`
|
|
1874
2051
|
};
|
|
1875
2052
|
} catch (e) {
|
|
1876
2053
|
const err = e;
|
|
@@ -1895,6 +2072,636 @@ var screenshotPlugin = {
|
|
|
1895
2072
|
}
|
|
1896
2073
|
};
|
|
1897
2074
|
|
|
2075
|
+
// src/plugins/pluginBuilder.ts
|
|
2076
|
+
var buildPluginTool = {
|
|
2077
|
+
name: "build_plugin",
|
|
2078
|
+
description: `Build a Hustle plugin definition. Use this tool to construct a plugin based on user requirements.
|
|
2079
|
+
|
|
2080
|
+
## Plugin Structure
|
|
2081
|
+
|
|
2082
|
+
A plugin consists of:
|
|
2083
|
+
- **name**: Unique identifier (lowercase, no spaces, e.g., "my-plugin")
|
|
2084
|
+
- **version**: Semantic version (e.g., "1.0.0")
|
|
2085
|
+
- **description**: What the plugin does
|
|
2086
|
+
- **tools**: Array of tool definitions the AI can call
|
|
2087
|
+
- **executorCode**: Object mapping tool names to JavaScript function code strings
|
|
2088
|
+
|
|
2089
|
+
## Tool Definition Format
|
|
2090
|
+
|
|
2091
|
+
Each tool needs:
|
|
2092
|
+
- **name**: Unique tool name (alphanumeric + underscore, e.g., "get_weather")
|
|
2093
|
+
- **description**: Clear description for the AI to understand when to use it
|
|
2094
|
+
- **parameters**: JSON Schema object defining the arguments
|
|
2095
|
+
|
|
2096
|
+
Example tool:
|
|
2097
|
+
{
|
|
2098
|
+
"name": "get_weather",
|
|
2099
|
+
"description": "Get current weather for a city",
|
|
2100
|
+
"parameters": {
|
|
2101
|
+
"type": "object",
|
|
2102
|
+
"properties": {
|
|
2103
|
+
"city": { "type": "string", "description": "City name" },
|
|
2104
|
+
"units": { "type": "string", "enum": ["celsius", "fahrenheit"], "description": "Temperature units" }
|
|
2105
|
+
},
|
|
2106
|
+
"required": ["city"]
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
## Executor Code Format
|
|
2111
|
+
|
|
2112
|
+
Executors are async JavaScript functions that receive args and return a result.
|
|
2113
|
+
Write them as arrow function strings that will be eval'd:
|
|
2114
|
+
|
|
2115
|
+
"async (args) => { const { city } = args; return { weather: 'sunny', city }; }"
|
|
2116
|
+
|
|
2117
|
+
The function receives args as Record<string, unknown> and should return the result.
|
|
2118
|
+
|
|
2119
|
+
## Available in Executor Scope
|
|
2120
|
+
|
|
2121
|
+
Executors run in the browser context with full access to:
|
|
2122
|
+
|
|
2123
|
+
### Browser APIs
|
|
2124
|
+
- **fetch(url, options)** - HTTP requests (subject to CORS)
|
|
2125
|
+
- **localStorage / sessionStorage** - Persistent storage
|
|
2126
|
+
- **document** - Full DOM access (create elements, modals, forms, etc.)
|
|
2127
|
+
- **window** - Global window object
|
|
2128
|
+
- **console** - Logging (log, warn, error, etc.)
|
|
2129
|
+
- **setTimeout / setInterval / clearTimeout / clearInterval** - Timers
|
|
2130
|
+
- **JSON** - Parse and stringify
|
|
2131
|
+
- **Date** - Date/time operations
|
|
2132
|
+
- **URL / URLSearchParams** - URL manipulation
|
|
2133
|
+
- **FormData / Blob / File / FileReader** - File handling
|
|
2134
|
+
- **crypto** - Cryptographic operations (crypto.randomUUID(), etc.)
|
|
2135
|
+
- **navigator** - Browser info, clipboard, geolocation, etc.
|
|
2136
|
+
- **location** - Current URL info
|
|
2137
|
+
- **history** - Browser history navigation
|
|
2138
|
+
- **WebSocket** - Real-time bidirectional communication
|
|
2139
|
+
- **EventSource** - Server-sent events
|
|
2140
|
+
- **indexedDB** - Client-side database for large data
|
|
2141
|
+
- **Notification** - Browser notifications (requires permission)
|
|
2142
|
+
- **performance** - Performance timing
|
|
2143
|
+
- **atob / btoa** - Base64 encoding/decoding
|
|
2144
|
+
- **TextEncoder / TextDecoder** - Text encoding
|
|
2145
|
+
- **AbortController** - Cancel fetch requests
|
|
2146
|
+
- **IntersectionObserver / MutationObserver / ResizeObserver** - DOM observers
|
|
2147
|
+
- **requestAnimationFrame** - Animation timing
|
|
2148
|
+
- **speechSynthesis** - Text-to-speech
|
|
2149
|
+
- **Audio / Image / Canvas** - Media APIs
|
|
2150
|
+
|
|
2151
|
+
### Hustle Plugin System Globals
|
|
2152
|
+
- **window.__hustleInstanceId** - Current Hustle instance ID
|
|
2153
|
+
- **window.__hustleRegisterPlugin(plugin, enabled)** - Install another plugin dynamically
|
|
2154
|
+
- **window.__hustleUnregisterPlugin(name)** - Uninstall a plugin by name
|
|
2155
|
+
- **window.__hustleUploadFile(file, fileName?)** - Upload a File/Blob to the server, returns { url, contentType }
|
|
2156
|
+
- **window.__hustleListPlugins()** - List all installed plugins
|
|
2157
|
+
- **window.__hustleGetPlugin(name)** - Get a specific plugin by name
|
|
2158
|
+
|
|
2159
|
+
### DOM Manipulation Examples
|
|
2160
|
+
Create a modal: document.createElement('div'), style it, append to document.body
|
|
2161
|
+
Add event listeners: element.addEventListener('click', handler)
|
|
2162
|
+
Query elements: document.querySelector(), document.querySelectorAll()
|
|
2163
|
+
|
|
2164
|
+
### Storage Patterns
|
|
2165
|
+
Store data: localStorage.setItem('key', JSON.stringify(data))
|
|
2166
|
+
Retrieve data: JSON.parse(localStorage.getItem('key') || '{}')
|
|
2167
|
+
Namespace your keys: Use plugin name prefix like "myplugin-settings"
|
|
2168
|
+
|
|
2169
|
+
### Async Patterns
|
|
2170
|
+
All executors should be async. Use await for promises:
|
|
2171
|
+
"async (args) => { const res = await fetch(url); return await res.json(); }"
|
|
2172
|
+
|
|
2173
|
+
## Lifecycle Hooks (Optional)
|
|
2174
|
+
|
|
2175
|
+
Hooks also have full access to the browser scope described above.
|
|
2176
|
+
|
|
2177
|
+
- **onRegisterCode**: Called once when plugin is registered/enabled. Good for initialization.
|
|
2178
|
+
**IMPORTANT: Always log when your plugin registers so users know it's active!**
|
|
2179
|
+
Example: "async () => { console.log('[MyPlugin] v1.0.0 registered'); }"
|
|
2180
|
+
|
|
2181
|
+
- **beforeRequestCode**: Modify messages before sending. Receives request object with { messages, model, ... }. Must return the modified request.
|
|
2182
|
+
Example: "async (req) => { req.messages = req.messages.map(m => ({...m, content: m.content.toUpperCase()})); return req; }"
|
|
2183
|
+
|
|
2184
|
+
- **afterResponseCode**: Process/modify response after receiving. Receives response object with { content, ... }.
|
|
2185
|
+
Example: "async (res) => { console.log('Response received:', res.content.substring(0, 100)); }"
|
|
2186
|
+
|
|
2187
|
+
- **onErrorCode**: Called on errors. Receives (error, context) where context has { phase: 'beforeRequest'|'execute'|'afterResponse' }.
|
|
2188
|
+
Example: "async (error, ctx) => { console.error('[MyPlugin] Error in', ctx.phase, ':', error.message); }"
|
|
2189
|
+
|
|
2190
|
+
## Best Practices
|
|
2191
|
+
|
|
2192
|
+
1. **Always add onRegisterCode** that logs the plugin name and version
|
|
2193
|
+
2. **Namespace console logs** with [PluginName] prefix for easy identification
|
|
2194
|
+
3. **Handle errors gracefully** in executors - return { error: message } instead of throwing
|
|
2195
|
+
|
|
2196
|
+
## Security Notes
|
|
2197
|
+
- Code runs in browser sandbox with same-origin policy
|
|
2198
|
+
- fetch() is subject to CORS restrictions
|
|
2199
|
+
- No direct filesystem access (use File API with user interaction)
|
|
2200
|
+
- Be careful with eval() on user input`,
|
|
2201
|
+
parameters: {
|
|
2202
|
+
type: "object",
|
|
2203
|
+
properties: {
|
|
2204
|
+
name: {
|
|
2205
|
+
type: "string",
|
|
2206
|
+
description: "Unique plugin identifier (lowercase, no spaces)"
|
|
2207
|
+
},
|
|
2208
|
+
version: {
|
|
2209
|
+
type: "string",
|
|
2210
|
+
description: 'Semantic version (e.g., "1.0.0")'
|
|
2211
|
+
},
|
|
2212
|
+
description: {
|
|
2213
|
+
type: "string",
|
|
2214
|
+
description: "What the plugin does"
|
|
2215
|
+
},
|
|
2216
|
+
tools: {
|
|
2217
|
+
type: "array",
|
|
2218
|
+
description: "Array of tool definitions",
|
|
2219
|
+
items: {
|
|
2220
|
+
type: "object",
|
|
2221
|
+
properties: {
|
|
2222
|
+
name: { type: "string", description: "Tool name" },
|
|
2223
|
+
description: { type: "string", description: "Tool description for AI" },
|
|
2224
|
+
parameters: { type: "object", description: "JSON Schema for arguments" }
|
|
2225
|
+
},
|
|
2226
|
+
required: ["name", "description", "parameters"]
|
|
2227
|
+
}
|
|
2228
|
+
},
|
|
2229
|
+
executorCode: {
|
|
2230
|
+
type: "object",
|
|
2231
|
+
description: "Object mapping tool names to executor function code strings"
|
|
2232
|
+
},
|
|
2233
|
+
beforeRequestCode: {
|
|
2234
|
+
type: "string",
|
|
2235
|
+
description: "Optional: Code for beforeRequest hook"
|
|
2236
|
+
},
|
|
2237
|
+
afterResponseCode: {
|
|
2238
|
+
type: "string",
|
|
2239
|
+
description: "Optional: Code for afterResponse hook"
|
|
2240
|
+
},
|
|
2241
|
+
onRegisterCode: {
|
|
2242
|
+
type: "string",
|
|
2243
|
+
description: "Optional: Code for onRegister hook"
|
|
2244
|
+
},
|
|
2245
|
+
onErrorCode: {
|
|
2246
|
+
type: "string",
|
|
2247
|
+
description: "Optional: Code for onError hook"
|
|
2248
|
+
}
|
|
2249
|
+
},
|
|
2250
|
+
required: ["name", "version", "description", "tools", "executorCode"]
|
|
2251
|
+
}
|
|
2252
|
+
};
|
|
2253
|
+
var savePluginTool = {
|
|
2254
|
+
name: "save_plugin",
|
|
2255
|
+
description: "Save a built plugin as a JSON file. Opens a download dialog for the user.",
|
|
2256
|
+
parameters: {
|
|
2257
|
+
type: "object",
|
|
2258
|
+
properties: {
|
|
2259
|
+
plugin: {
|
|
2260
|
+
type: "object",
|
|
2261
|
+
description: "The plugin object to save (from build_plugin result)"
|
|
2262
|
+
},
|
|
2263
|
+
filename: {
|
|
2264
|
+
type: "string",
|
|
2265
|
+
description: "Filename without extension (defaults to plugin name)"
|
|
2266
|
+
}
|
|
2267
|
+
},
|
|
2268
|
+
required: ["plugin"]
|
|
2269
|
+
}
|
|
2270
|
+
};
|
|
2271
|
+
var installPluginTool = {
|
|
2272
|
+
name: "install_plugin",
|
|
2273
|
+
description: "Install a built plugin to browser storage so it persists and can be used.",
|
|
2274
|
+
parameters: {
|
|
2275
|
+
type: "object",
|
|
2276
|
+
properties: {
|
|
2277
|
+
plugin: {
|
|
2278
|
+
type: "object",
|
|
2279
|
+
description: "The plugin object to install (from build_plugin result)"
|
|
2280
|
+
},
|
|
2281
|
+
enabled: {
|
|
2282
|
+
type: "boolean",
|
|
2283
|
+
description: "Whether to enable the plugin immediately (default: true)"
|
|
2284
|
+
}
|
|
2285
|
+
},
|
|
2286
|
+
required: ["plugin"]
|
|
2287
|
+
}
|
|
2288
|
+
};
|
|
2289
|
+
var uninstallPluginTool = {
|
|
2290
|
+
name: "uninstall_plugin",
|
|
2291
|
+
description: "Uninstall a plugin by name, removing it from browser storage.",
|
|
2292
|
+
parameters: {
|
|
2293
|
+
type: "object",
|
|
2294
|
+
properties: {
|
|
2295
|
+
name: {
|
|
2296
|
+
type: "string",
|
|
2297
|
+
description: "The name of the plugin to uninstall"
|
|
2298
|
+
}
|
|
2299
|
+
},
|
|
2300
|
+
required: ["name"]
|
|
2301
|
+
}
|
|
2302
|
+
};
|
|
2303
|
+
var listPluginsTool = {
|
|
2304
|
+
name: "list_plugins",
|
|
2305
|
+
description: "List all installed plugins with their enabled/disabled status.",
|
|
2306
|
+
parameters: {
|
|
2307
|
+
type: "object",
|
|
2308
|
+
properties: {},
|
|
2309
|
+
required: []
|
|
2310
|
+
}
|
|
2311
|
+
};
|
|
2312
|
+
var modifyPluginTool = {
|
|
2313
|
+
name: "modify_plugin",
|
|
2314
|
+
description: `Modify an existing installed plugin. Can update version, description, tools, executors, and hooks.
|
|
2315
|
+
|
|
2316
|
+
Use list_plugins first to see installed plugins, then modify by name.
|
|
2317
|
+
|
|
2318
|
+
You can:
|
|
2319
|
+
- Add new tools (provide tools array with new tools to add)
|
|
2320
|
+
- Update existing tools (provide tool with same name)
|
|
2321
|
+
- Remove tools (set removeTool to the tool name)
|
|
2322
|
+
- Update hooks (provide hook code)
|
|
2323
|
+
- Update version/description
|
|
2324
|
+
|
|
2325
|
+
Example: Add a new tool to existing plugin:
|
|
2326
|
+
{
|
|
2327
|
+
"name": "my-plugin",
|
|
2328
|
+
"addTools": [{ "name": "new_tool", "description": "...", "parameters": {...} }],
|
|
2329
|
+
"addExecutorCode": { "new_tool": "async (args) => { ... }" }
|
|
2330
|
+
}`,
|
|
2331
|
+
parameters: {
|
|
2332
|
+
type: "object",
|
|
2333
|
+
properties: {
|
|
2334
|
+
name: {
|
|
2335
|
+
type: "string",
|
|
2336
|
+
description: "Name of the plugin to modify (required)"
|
|
2337
|
+
},
|
|
2338
|
+
version: {
|
|
2339
|
+
type: "string",
|
|
2340
|
+
description: "New version string"
|
|
2341
|
+
},
|
|
2342
|
+
description: {
|
|
2343
|
+
type: "string",
|
|
2344
|
+
description: "New description"
|
|
2345
|
+
},
|
|
2346
|
+
addTools: {
|
|
2347
|
+
type: "array",
|
|
2348
|
+
description: "Tools to add or update",
|
|
2349
|
+
items: {
|
|
2350
|
+
type: "object",
|
|
2351
|
+
properties: {
|
|
2352
|
+
name: { type: "string" },
|
|
2353
|
+
description: { type: "string" },
|
|
2354
|
+
parameters: { type: "object" }
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
},
|
|
2358
|
+
addExecutorCode: {
|
|
2359
|
+
type: "object",
|
|
2360
|
+
description: "Executor code to add/update (tool name -> code string)"
|
|
2361
|
+
},
|
|
2362
|
+
removeTools: {
|
|
2363
|
+
type: "array",
|
|
2364
|
+
description: "Names of tools to remove",
|
|
2365
|
+
items: { type: "string" }
|
|
2366
|
+
},
|
|
2367
|
+
onRegisterCode: {
|
|
2368
|
+
type: "string",
|
|
2369
|
+
description: "New onRegister hook code"
|
|
2370
|
+
},
|
|
2371
|
+
beforeRequestCode: {
|
|
2372
|
+
type: "string",
|
|
2373
|
+
description: "New beforeRequest hook code"
|
|
2374
|
+
},
|
|
2375
|
+
afterResponseCode: {
|
|
2376
|
+
type: "string",
|
|
2377
|
+
description: "New afterResponse hook code"
|
|
2378
|
+
},
|
|
2379
|
+
onErrorCode: {
|
|
2380
|
+
type: "string",
|
|
2381
|
+
description: "New onError hook code"
|
|
2382
|
+
}
|
|
2383
|
+
},
|
|
2384
|
+
required: ["name"]
|
|
2385
|
+
}
|
|
2386
|
+
};
|
|
2387
|
+
var buildPluginExecutor = async (args2) => {
|
|
2388
|
+
const {
|
|
2389
|
+
name,
|
|
2390
|
+
version,
|
|
2391
|
+
description,
|
|
2392
|
+
tools,
|
|
2393
|
+
executorCode,
|
|
2394
|
+
beforeRequestCode,
|
|
2395
|
+
afterResponseCode,
|
|
2396
|
+
onRegisterCode,
|
|
2397
|
+
onErrorCode
|
|
2398
|
+
} = args2;
|
|
2399
|
+
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
2400
|
+
return {
|
|
2401
|
+
success: false,
|
|
2402
|
+
error: "Plugin name must be lowercase, start with a letter, and contain only letters, numbers, and hyphens"
|
|
2403
|
+
};
|
|
2404
|
+
}
|
|
2405
|
+
if (!/^\d+\.\d+\.\d+/.test(version)) {
|
|
2406
|
+
return {
|
|
2407
|
+
success: false,
|
|
2408
|
+
error: 'Version must be in semver format (e.g., "1.0.0")'
|
|
2409
|
+
};
|
|
2410
|
+
}
|
|
2411
|
+
for (const tool of tools) {
|
|
2412
|
+
if (!executorCode[tool.name]) {
|
|
2413
|
+
return {
|
|
2414
|
+
success: false,
|
|
2415
|
+
error: `Missing executor code for tool: ${tool.name}`
|
|
2416
|
+
};
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
const storedPlugin = {
|
|
2420
|
+
name,
|
|
2421
|
+
version,
|
|
2422
|
+
description,
|
|
2423
|
+
tools: tools.map((tool) => ({
|
|
2424
|
+
...tool,
|
|
2425
|
+
executorCode: executorCode[tool.name]
|
|
2426
|
+
})),
|
|
2427
|
+
enabled: true,
|
|
2428
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2429
|
+
};
|
|
2430
|
+
if (beforeRequestCode || afterResponseCode || onRegisterCode || onErrorCode) {
|
|
2431
|
+
storedPlugin.hooksCode = {};
|
|
2432
|
+
if (beforeRequestCode) storedPlugin.hooksCode.beforeRequestCode = beforeRequestCode;
|
|
2433
|
+
if (afterResponseCode) storedPlugin.hooksCode.afterResponseCode = afterResponseCode;
|
|
2434
|
+
if (onRegisterCode) storedPlugin.hooksCode.onRegisterCode = onRegisterCode;
|
|
2435
|
+
if (onErrorCode) storedPlugin.hooksCode.onErrorCode = onErrorCode;
|
|
2436
|
+
}
|
|
2437
|
+
return {
|
|
2438
|
+
success: true,
|
|
2439
|
+
plugin: storedPlugin,
|
|
2440
|
+
message: `Plugin "${name}" v${version} built successfully with ${tools.length} tool(s). Use save_plugin to download or install_plugin to add to browser storage.`
|
|
2441
|
+
};
|
|
2442
|
+
};
|
|
2443
|
+
function normalizePlugin(input) {
|
|
2444
|
+
const plugin = input;
|
|
2445
|
+
const tools = plugin.tools;
|
|
2446
|
+
const hasEmbeddedExecutors = tools?.[0]?.executorCode !== void 0;
|
|
2447
|
+
if (hasEmbeddedExecutors) {
|
|
2448
|
+
return {
|
|
2449
|
+
...plugin,
|
|
2450
|
+
enabled: plugin.enabled ?? true,
|
|
2451
|
+
installedAt: plugin.installedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
2452
|
+
};
|
|
2453
|
+
}
|
|
2454
|
+
const executorCode = plugin.executorCode;
|
|
2455
|
+
const rawTools = tools || [];
|
|
2456
|
+
const hookKeys = ["onRegisterCode", "beforeRequestCode", "afterResponseCode", "onErrorCode"];
|
|
2457
|
+
const hooksCode = {};
|
|
2458
|
+
for (const key of hookKeys) {
|
|
2459
|
+
if (executorCode?.[key]) {
|
|
2460
|
+
hooksCode[key] = executorCode[key];
|
|
2461
|
+
}
|
|
2462
|
+
if (plugin[key]) {
|
|
2463
|
+
hooksCode[key] = plugin[key];
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
return {
|
|
2467
|
+
name: plugin.name,
|
|
2468
|
+
version: plugin.version,
|
|
2469
|
+
description: plugin.description,
|
|
2470
|
+
tools: rawTools.map((tool) => ({
|
|
2471
|
+
name: tool.name,
|
|
2472
|
+
description: tool.description,
|
|
2473
|
+
parameters: tool.parameters,
|
|
2474
|
+
executorCode: executorCode?.[tool.name]
|
|
2475
|
+
})),
|
|
2476
|
+
hooksCode: Object.keys(hooksCode).length > 0 ? hooksCode : void 0,
|
|
2477
|
+
enabled: true,
|
|
2478
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2479
|
+
};
|
|
2480
|
+
}
|
|
2481
|
+
var savePluginExecutor = async (args2) => {
|
|
2482
|
+
const { plugin: rawPlugin, filename } = args2;
|
|
2483
|
+
if (typeof window === "undefined") {
|
|
2484
|
+
return { success: false, error: "Cannot save files in server environment" };
|
|
2485
|
+
}
|
|
2486
|
+
try {
|
|
2487
|
+
const plugin = normalizePlugin(rawPlugin);
|
|
2488
|
+
const json2 = JSON.stringify(plugin, null, 2);
|
|
2489
|
+
const blob = new Blob([json2], { type: "application/json" });
|
|
2490
|
+
const url = URL.createObjectURL(blob);
|
|
2491
|
+
const a = document.createElement("a");
|
|
2492
|
+
a.href = url;
|
|
2493
|
+
a.download = `${filename || plugin.name}.json`;
|
|
2494
|
+
document.body.appendChild(a);
|
|
2495
|
+
a.click();
|
|
2496
|
+
document.body.removeChild(a);
|
|
2497
|
+
URL.revokeObjectURL(url);
|
|
2498
|
+
return {
|
|
2499
|
+
success: true,
|
|
2500
|
+
message: `Plugin saved as ${filename || plugin.name}.json`
|
|
2501
|
+
};
|
|
2502
|
+
} catch (error2) {
|
|
2503
|
+
return {
|
|
2504
|
+
success: false,
|
|
2505
|
+
error: `Failed to save: ${error2 instanceof Error ? error2.message : "Unknown error"}`
|
|
2506
|
+
};
|
|
2507
|
+
}
|
|
2508
|
+
};
|
|
2509
|
+
var installPluginExecutor = async (args2) => {
|
|
2510
|
+
const { plugin: rawPlugin, enabled = true } = args2;
|
|
2511
|
+
if (typeof window === "undefined") {
|
|
2512
|
+
return { success: false, error: "Cannot install plugins in server environment" };
|
|
2513
|
+
}
|
|
2514
|
+
try {
|
|
2515
|
+
const plugin = normalizePlugin(rawPlugin);
|
|
2516
|
+
const win = window;
|
|
2517
|
+
if (!win.__hustleRegisterPlugin) {
|
|
2518
|
+
return {
|
|
2519
|
+
success: false,
|
|
2520
|
+
error: "Plugin registration not available. Make sure HustleProvider is mounted."
|
|
2521
|
+
};
|
|
2522
|
+
}
|
|
2523
|
+
await win.__hustleRegisterPlugin(plugin, enabled);
|
|
2524
|
+
return {
|
|
2525
|
+
success: true,
|
|
2526
|
+
message: `Plugin "${plugin.name}" installed${enabled ? " and enabled" : ""}.`
|
|
2527
|
+
};
|
|
2528
|
+
} catch (error2) {
|
|
2529
|
+
return {
|
|
2530
|
+
success: false,
|
|
2531
|
+
error: `Failed to install: ${error2 instanceof Error ? error2.message : "Unknown error"}`
|
|
2532
|
+
};
|
|
2533
|
+
}
|
|
2534
|
+
};
|
|
2535
|
+
var uninstallPluginExecutor = async (args2) => {
|
|
2536
|
+
const { name } = args2;
|
|
2537
|
+
if (typeof window === "undefined") {
|
|
2538
|
+
return { success: false, error: "Cannot uninstall plugins in server environment" };
|
|
2539
|
+
}
|
|
2540
|
+
try {
|
|
2541
|
+
const win = window;
|
|
2542
|
+
if (!win.__hustleUnregisterPlugin) {
|
|
2543
|
+
return {
|
|
2544
|
+
success: false,
|
|
2545
|
+
error: "Plugin unregistration not available. Make sure HustleProvider is mounted."
|
|
2546
|
+
};
|
|
2547
|
+
}
|
|
2548
|
+
await win.__hustleUnregisterPlugin(name);
|
|
2549
|
+
return {
|
|
2550
|
+
success: true,
|
|
2551
|
+
message: `Plugin "${name}" has been uninstalled.`
|
|
2552
|
+
};
|
|
2553
|
+
} catch (error2) {
|
|
2554
|
+
return {
|
|
2555
|
+
success: false,
|
|
2556
|
+
error: `Failed to uninstall: ${error2 instanceof Error ? error2.message : "Unknown error"}`
|
|
2557
|
+
};
|
|
2558
|
+
}
|
|
2559
|
+
};
|
|
2560
|
+
var listPluginsExecutor = async () => {
|
|
2561
|
+
if (typeof window === "undefined") {
|
|
2562
|
+
return { success: false, error: "Cannot list plugins in server environment" };
|
|
2563
|
+
}
|
|
2564
|
+
try {
|
|
2565
|
+
const win = window;
|
|
2566
|
+
if (!win.__hustleListPlugins) {
|
|
2567
|
+
return {
|
|
2568
|
+
success: false,
|
|
2569
|
+
error: "Plugin listing not available. Make sure HustleProvider is mounted."
|
|
2570
|
+
};
|
|
2571
|
+
}
|
|
2572
|
+
const plugins = win.__hustleListPlugins();
|
|
2573
|
+
return {
|
|
2574
|
+
success: true,
|
|
2575
|
+
plugins,
|
|
2576
|
+
summary: plugins.length === 0 ? "No plugins installed." : `${plugins.length} plugin(s) installed: ${plugins.map((p) => `${p.name} v${p.version} (${p.enabled ? "enabled" : "disabled"})`).join(", ")}`
|
|
2577
|
+
};
|
|
2578
|
+
} catch (error2) {
|
|
2579
|
+
return {
|
|
2580
|
+
success: false,
|
|
2581
|
+
error: `Failed to list plugins: ${error2 instanceof Error ? error2.message : "Unknown error"}`
|
|
2582
|
+
};
|
|
2583
|
+
}
|
|
2584
|
+
};
|
|
2585
|
+
var modifyPluginExecutor = async (args2) => {
|
|
2586
|
+
const {
|
|
2587
|
+
name,
|
|
2588
|
+
version,
|
|
2589
|
+
description,
|
|
2590
|
+
addTools,
|
|
2591
|
+
addExecutorCode,
|
|
2592
|
+
removeTools,
|
|
2593
|
+
onRegisterCode,
|
|
2594
|
+
beforeRequestCode,
|
|
2595
|
+
afterResponseCode,
|
|
2596
|
+
onErrorCode
|
|
2597
|
+
} = args2;
|
|
2598
|
+
if (typeof window === "undefined") {
|
|
2599
|
+
return { success: false, error: "Cannot modify plugins in server environment" };
|
|
2600
|
+
}
|
|
2601
|
+
try {
|
|
2602
|
+
const win = window;
|
|
2603
|
+
if (!win.__hustleGetPlugin || !win.__hustleRegisterPlugin) {
|
|
2604
|
+
return {
|
|
2605
|
+
success: false,
|
|
2606
|
+
error: "Plugin modification not available. Make sure HustleProvider is mounted."
|
|
2607
|
+
};
|
|
2608
|
+
}
|
|
2609
|
+
const existing = win.__hustleGetPlugin(name);
|
|
2610
|
+
if (!existing) {
|
|
2611
|
+
return {
|
|
2612
|
+
success: false,
|
|
2613
|
+
error: `Plugin "${name}" not found. Use list_plugins to see installed plugins.`
|
|
2614
|
+
};
|
|
2615
|
+
}
|
|
2616
|
+
let tools = existing.tools ? [...existing.tools] : [];
|
|
2617
|
+
if (removeTools && removeTools.length > 0) {
|
|
2618
|
+
tools = tools.filter((t) => !removeTools.includes(t.name));
|
|
2619
|
+
}
|
|
2620
|
+
if (addTools && addTools.length > 0) {
|
|
2621
|
+
for (const newTool of addTools) {
|
|
2622
|
+
const existingIndex = tools.findIndex((t) => t.name === newTool.name);
|
|
2623
|
+
const toolWithExecutor = {
|
|
2624
|
+
...newTool,
|
|
2625
|
+
executorCode: addExecutorCode?.[newTool.name] || tools.find((t) => t.name === newTool.name)?.executorCode
|
|
2626
|
+
};
|
|
2627
|
+
if (existingIndex >= 0) {
|
|
2628
|
+
tools[existingIndex] = toolWithExecutor;
|
|
2629
|
+
} else {
|
|
2630
|
+
tools.push(toolWithExecutor);
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
if (addExecutorCode) {
|
|
2635
|
+
for (const [toolName, code2] of Object.entries(addExecutorCode)) {
|
|
2636
|
+
const tool = tools.find((t) => t.name === toolName);
|
|
2637
|
+
if (tool) {
|
|
2638
|
+
tool.executorCode = code2;
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
const modified = {
|
|
2643
|
+
...existing,
|
|
2644
|
+
tools
|
|
2645
|
+
};
|
|
2646
|
+
if (version) modified.version = version;
|
|
2647
|
+
if (description) modified.description = description;
|
|
2648
|
+
if (onRegisterCode || beforeRequestCode || afterResponseCode || onErrorCode) {
|
|
2649
|
+
modified.hooksCode = modified.hooksCode || {};
|
|
2650
|
+
if (onRegisterCode) modified.hooksCode.onRegisterCode = onRegisterCode;
|
|
2651
|
+
if (beforeRequestCode) modified.hooksCode.beforeRequestCode = beforeRequestCode;
|
|
2652
|
+
if (afterResponseCode) modified.hooksCode.afterResponseCode = afterResponseCode;
|
|
2653
|
+
if (onErrorCode) modified.hooksCode.onErrorCode = onErrorCode;
|
|
2654
|
+
}
|
|
2655
|
+
const unregisterFn = window.__hustleUnregisterPlugin;
|
|
2656
|
+
if (unregisterFn) {
|
|
2657
|
+
await unregisterFn(name);
|
|
2658
|
+
}
|
|
2659
|
+
await win.__hustleRegisterPlugin(modified, existing.enabled);
|
|
2660
|
+
const changes = [];
|
|
2661
|
+
if (version) changes.push(`version \u2192 ${version}`);
|
|
2662
|
+
if (description) changes.push("description updated");
|
|
2663
|
+
if (removeTools?.length) changes.push(`removed ${removeTools.length} tool(s)`);
|
|
2664
|
+
if (addTools?.length) changes.push(`added/updated ${addTools.length} tool(s)`);
|
|
2665
|
+
if (onRegisterCode) changes.push("onRegister hook updated");
|
|
2666
|
+
if (beforeRequestCode) changes.push("beforeRequest hook updated");
|
|
2667
|
+
if (afterResponseCode) changes.push("afterResponse hook updated");
|
|
2668
|
+
if (onErrorCode) changes.push("onError hook updated");
|
|
2669
|
+
return {
|
|
2670
|
+
success: true,
|
|
2671
|
+
message: `Plugin "${name}" modified: ${changes.join(", ")}`,
|
|
2672
|
+
plugin: {
|
|
2673
|
+
name: modified.name,
|
|
2674
|
+
version: modified.version,
|
|
2675
|
+
toolCount: tools.length
|
|
2676
|
+
}
|
|
2677
|
+
};
|
|
2678
|
+
} catch (error2) {
|
|
2679
|
+
return {
|
|
2680
|
+
success: false,
|
|
2681
|
+
error: `Failed to modify plugin: ${error2 instanceof Error ? error2.message : "Unknown error"}`
|
|
2682
|
+
};
|
|
2683
|
+
}
|
|
2684
|
+
};
|
|
2685
|
+
var pluginBuilderPlugin = {
|
|
2686
|
+
name: "plugin-builder",
|
|
2687
|
+
version: "1.2.0",
|
|
2688
|
+
description: "Build custom plugins through conversation",
|
|
2689
|
+
tools: [buildPluginTool, savePluginTool, installPluginTool, uninstallPluginTool, listPluginsTool, modifyPluginTool],
|
|
2690
|
+
executors: {
|
|
2691
|
+
build_plugin: buildPluginExecutor,
|
|
2692
|
+
save_plugin: savePluginExecutor,
|
|
2693
|
+
install_plugin: installPluginExecutor,
|
|
2694
|
+
uninstall_plugin: uninstallPluginExecutor,
|
|
2695
|
+
list_plugins: listPluginsExecutor,
|
|
2696
|
+
modify_plugin: modifyPluginExecutor
|
|
2697
|
+
},
|
|
2698
|
+
hooks: {
|
|
2699
|
+
onRegister: () => {
|
|
2700
|
+
console.log("[Plugin Builder] Ready to help build custom plugins");
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
};
|
|
2704
|
+
|
|
1898
2705
|
// src/plugins/index.ts
|
|
1899
2706
|
var availablePlugins = [
|
|
1900
2707
|
{
|
|
@@ -1924,6 +2731,10 @@ var availablePlugins = [
|
|
|
1924
2731
|
{
|
|
1925
2732
|
...screenshotPlugin,
|
|
1926
2733
|
description: "Take screenshots of the current page"
|
|
2734
|
+
},
|
|
2735
|
+
{
|
|
2736
|
+
...pluginBuilderPlugin,
|
|
2737
|
+
description: "Build custom plugins through conversation with AI"
|
|
1927
2738
|
}
|
|
1928
2739
|
];
|
|
1929
2740
|
function getAvailablePlugin(name) {
|