@emblemvault/hustle-react 1.1.0 → 1.1.2
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 +655 -75
- package/dist/browser/hustle-react.js.map +1 -1
- package/dist/components/index.cjs +610 -76
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +610 -76
- package/dist/components/index.js.map +1 -1
- package/dist/hooks/index.cjs +83 -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 +83 -14
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.cjs +658 -75
- 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 +658 -75
- 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 +576 -61
- package/dist/plugins/index.cjs.map +1 -1
- package/dist/plugins/index.d.cts +1 -1
- package/dist/plugins/index.d.ts +1 -1
- package/dist/plugins/index.js +576 -61
- package/dist/plugins/index.js.map +1 -1
- package/dist/providers/index.cjs +83 -14
- package/dist/providers/index.cjs.map +1 -1
- package/dist/providers/index.js +83 -14
- package/dist/providers/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
'use client';
|
|
1
2
|
'use strict';
|
|
2
3
|
|
|
3
4
|
var react = require('react');
|
|
@@ -51,6 +52,8 @@ var ruby__default = /*#__PURE__*/_interopDefault(ruby);
|
|
|
51
52
|
var swift__default = /*#__PURE__*/_interopDefault(swift);
|
|
52
53
|
var kotlin__default = /*#__PURE__*/_interopDefault(kotlin);
|
|
53
54
|
|
|
55
|
+
// src/providers/HustleProvider.tsx
|
|
56
|
+
|
|
54
57
|
// src/utils/pluginRegistry.ts
|
|
55
58
|
var PLUGINS_KEY = "hustle-plugins";
|
|
56
59
|
function getEnabledStateKey(instanceId) {
|
|
@@ -302,35 +305,52 @@ var pluginRegistry = new PluginRegistry();
|
|
|
302
305
|
function getStorageKey(instanceId) {
|
|
303
306
|
return `hustle-plugins-${instanceId}`;
|
|
304
307
|
}
|
|
305
|
-
function
|
|
308
|
+
function getInstanceId(providedId) {
|
|
309
|
+
if (providedId) return providedId;
|
|
310
|
+
if (typeof window !== "undefined") {
|
|
311
|
+
const globalId = window.__hustleInstanceId;
|
|
312
|
+
if (globalId) return globalId;
|
|
313
|
+
}
|
|
314
|
+
return "default";
|
|
315
|
+
}
|
|
316
|
+
function usePlugins(instanceId) {
|
|
317
|
+
const [resolvedInstanceId] = react.useState(() => getInstanceId(instanceId));
|
|
306
318
|
const [plugins, setPlugins] = react.useState([]);
|
|
307
319
|
react.useEffect(() => {
|
|
308
|
-
setPlugins(pluginRegistry.loadFromStorage(
|
|
309
|
-
const unsubscribe = pluginRegistry.onChange(setPlugins,
|
|
310
|
-
const storageKey = getStorageKey(
|
|
320
|
+
setPlugins(pluginRegistry.loadFromStorage(resolvedInstanceId));
|
|
321
|
+
const unsubscribe = pluginRegistry.onChange(setPlugins, resolvedInstanceId);
|
|
322
|
+
const storageKey = getStorageKey(resolvedInstanceId);
|
|
311
323
|
const handleStorage = (e) => {
|
|
312
324
|
if (e.key === storageKey) {
|
|
313
|
-
setPlugins(pluginRegistry.loadFromStorage(
|
|
325
|
+
setPlugins(pluginRegistry.loadFromStorage(resolvedInstanceId));
|
|
314
326
|
}
|
|
315
327
|
};
|
|
316
328
|
window.addEventListener("storage", handleStorage);
|
|
329
|
+
const handlePluginInstalled = (e) => {
|
|
330
|
+
const customEvent = e;
|
|
331
|
+
if (customEvent.detail.instanceId === resolvedInstanceId) {
|
|
332
|
+
setPlugins(pluginRegistry.loadFromStorage(resolvedInstanceId));
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
window.addEventListener("hustle-plugin-installed", handlePluginInstalled);
|
|
317
336
|
return () => {
|
|
318
337
|
unsubscribe();
|
|
319
338
|
window.removeEventListener("storage", handleStorage);
|
|
339
|
+
window.removeEventListener("hustle-plugin-installed", handlePluginInstalled);
|
|
320
340
|
};
|
|
321
|
-
}, [
|
|
341
|
+
}, [resolvedInstanceId]);
|
|
322
342
|
const registerPlugin = react.useCallback((plugin) => {
|
|
323
|
-
pluginRegistry.register(plugin, true,
|
|
324
|
-
}, [
|
|
343
|
+
pluginRegistry.register(plugin, true, resolvedInstanceId);
|
|
344
|
+
}, [resolvedInstanceId]);
|
|
325
345
|
const unregisterPlugin = react.useCallback((name) => {
|
|
326
|
-
pluginRegistry.unregister(name,
|
|
327
|
-
}, [
|
|
346
|
+
pluginRegistry.unregister(name, resolvedInstanceId);
|
|
347
|
+
}, [resolvedInstanceId]);
|
|
328
348
|
const enablePlugin = react.useCallback((name) => {
|
|
329
|
-
pluginRegistry.setEnabled(name, true,
|
|
330
|
-
}, [
|
|
349
|
+
pluginRegistry.setEnabled(name, true, resolvedInstanceId);
|
|
350
|
+
}, [resolvedInstanceId]);
|
|
331
351
|
const disablePlugin = react.useCallback((name) => {
|
|
332
|
-
pluginRegistry.setEnabled(name, false,
|
|
333
|
-
}, [
|
|
352
|
+
pluginRegistry.setEnabled(name, false, resolvedInstanceId);
|
|
353
|
+
}, [resolvedInstanceId]);
|
|
334
354
|
const isRegistered = react.useCallback(
|
|
335
355
|
(name) => plugins.some((p) => p.name === name),
|
|
336
356
|
[plugins]
|
|
@@ -385,6 +405,42 @@ function HustleProvider({
|
|
|
385
405
|
}
|
|
386
406
|
};
|
|
387
407
|
}, [isAutoInstance, resolvedInstanceId]);
|
|
408
|
+
react.useEffect(() => {
|
|
409
|
+
if (typeof window !== "undefined") {
|
|
410
|
+
const win = window;
|
|
411
|
+
win.__hustleInstanceId = resolvedInstanceId;
|
|
412
|
+
win.__hustleRegisterPlugin = async (plugin, enabled = true) => {
|
|
413
|
+
const hydrated = hydratePlugin(plugin);
|
|
414
|
+
pluginRegistry.register(hydrated, enabled, resolvedInstanceId);
|
|
415
|
+
};
|
|
416
|
+
win.__hustleUnregisterPlugin = async (name) => {
|
|
417
|
+
pluginRegistry.unregister(name, resolvedInstanceId);
|
|
418
|
+
};
|
|
419
|
+
win.__hustleListPlugins = () => {
|
|
420
|
+
const plugins = pluginRegistry.loadFromStorage(resolvedInstanceId);
|
|
421
|
+
return plugins.map((p) => ({
|
|
422
|
+
name: p.name,
|
|
423
|
+
version: p.version,
|
|
424
|
+
description: p.description || "",
|
|
425
|
+
enabled: p.enabled
|
|
426
|
+
}));
|
|
427
|
+
};
|
|
428
|
+
win.__hustleGetPlugin = (name) => {
|
|
429
|
+
const plugins = pluginRegistry.loadFromStorage(resolvedInstanceId);
|
|
430
|
+
return plugins.find((p) => p.name === name) || null;
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
return () => {
|
|
434
|
+
if (typeof window !== "undefined") {
|
|
435
|
+
const win = window;
|
|
436
|
+
delete win.__hustleInstanceId;
|
|
437
|
+
delete win.__hustleRegisterPlugin;
|
|
438
|
+
delete win.__hustleUnregisterPlugin;
|
|
439
|
+
delete win.__hustleListPlugins;
|
|
440
|
+
delete win.__hustleGetPlugin;
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
}, [resolvedInstanceId]);
|
|
388
444
|
const isApiKeyMode = Boolean(apiKey && vaultId);
|
|
389
445
|
const authContext = emblemAuthReact.useEmblemAuthOptional();
|
|
390
446
|
const authSDK = isApiKeyMode ? null : authContext?.authSDK ?? null;
|
|
@@ -532,6 +588,19 @@ function HustleProvider({
|
|
|
532
588
|
};
|
|
533
589
|
registerPlugins();
|
|
534
590
|
}, [client, enabledPlugins, log]);
|
|
591
|
+
react.useEffect(() => {
|
|
592
|
+
if (typeof window !== "undefined" && client) {
|
|
593
|
+
const win = window;
|
|
594
|
+
win.__hustleUploadFile = async (file, fileName) => {
|
|
595
|
+
return await client.uploadFile(file, fileName);
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
return () => {
|
|
599
|
+
if (typeof window !== "undefined") {
|
|
600
|
+
delete window.__hustleUploadFile;
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
}, [client]);
|
|
535
604
|
const loadModels = react.useCallback(async () => {
|
|
536
605
|
if (!client) {
|
|
537
606
|
log("Cannot load models - client not ready");
|
|
@@ -1412,12 +1481,16 @@ function ensureModalStyles() {
|
|
|
1412
1481
|
left: 0;
|
|
1413
1482
|
right: 0;
|
|
1414
1483
|
bottom: 0;
|
|
1415
|
-
background:
|
|
1484
|
+
background: transparent;
|
|
1416
1485
|
display: flex;
|
|
1417
1486
|
align-items: center;
|
|
1418
1487
|
justify-content: center;
|
|
1419
1488
|
z-index: 10000;
|
|
1420
|
-
|
|
1489
|
+
pointer-events: none;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
.user-question-modal {
|
|
1493
|
+
pointer-events: auto;
|
|
1421
1494
|
}
|
|
1422
1495
|
|
|
1423
1496
|
@keyframes uqFadeIn {
|
|
@@ -1530,6 +1603,27 @@ function ensureModalStyles() {
|
|
|
1530
1603
|
color: #666;
|
|
1531
1604
|
cursor: not-allowed;
|
|
1532
1605
|
}
|
|
1606
|
+
|
|
1607
|
+
.user-question-custom-input {
|
|
1608
|
+
width: 100%;
|
|
1609
|
+
padding: 8px 12px;
|
|
1610
|
+
margin-top: 8px;
|
|
1611
|
+
background: #1a1a2e;
|
|
1612
|
+
border: 1px solid #444;
|
|
1613
|
+
border-radius: 6px;
|
|
1614
|
+
color: #e0e0e0;
|
|
1615
|
+
font-size: 14px;
|
|
1616
|
+
box-sizing: border-box;
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
.user-question-custom-input:focus {
|
|
1620
|
+
outline: none;
|
|
1621
|
+
border-color: #4a7aff;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
.user-question-custom-input::placeholder {
|
|
1625
|
+
color: #666;
|
|
1626
|
+
}
|
|
1533
1627
|
`;
|
|
1534
1628
|
document.head.appendChild(styles2);
|
|
1535
1629
|
}
|
|
@@ -1551,13 +1645,17 @@ var askUserTool = {
|
|
|
1551
1645
|
allowMultiple: {
|
|
1552
1646
|
type: "boolean",
|
|
1553
1647
|
description: "If true, user can select multiple choices. Default: false"
|
|
1648
|
+
},
|
|
1649
|
+
allowCustom: {
|
|
1650
|
+
type: "boolean",
|
|
1651
|
+
description: 'If true, adds an "Other" option where user can type a custom response. Default: false'
|
|
1554
1652
|
}
|
|
1555
1653
|
},
|
|
1556
1654
|
required: ["question", "choices"]
|
|
1557
1655
|
}
|
|
1558
1656
|
};
|
|
1559
1657
|
var askUserExecutor = async (args2) => {
|
|
1560
|
-
const { question, choices, allowMultiple = false } = args2;
|
|
1658
|
+
const { question, choices, allowMultiple = false, allowCustom = false } = args2;
|
|
1561
1659
|
if (!question || !choices || !Array.isArray(choices) || choices.length === 0) {
|
|
1562
1660
|
return {
|
|
1563
1661
|
question: question || "",
|
|
@@ -1589,6 +1687,17 @@ var askUserExecutor = async (args2) => {
|
|
|
1589
1687
|
choicesDiv.className = "user-question-choices";
|
|
1590
1688
|
const inputType = allowMultiple ? "checkbox" : "radio";
|
|
1591
1689
|
const inputName = `uq-${Date.now()}`;
|
|
1690
|
+
let customInput = null;
|
|
1691
|
+
let isCustomSelected = false;
|
|
1692
|
+
const submitBtn = document.createElement("button");
|
|
1693
|
+
submitBtn.className = "user-question-btn user-question-btn-submit";
|
|
1694
|
+
submitBtn.textContent = "Submit";
|
|
1695
|
+
submitBtn.disabled = true;
|
|
1696
|
+
const updateSubmitButton = () => {
|
|
1697
|
+
const hasSelection = selected.size > 0;
|
|
1698
|
+
const hasCustomValue = isCustomSelected && customInput && customInput.value.trim().length > 0;
|
|
1699
|
+
submitBtn.disabled = !hasSelection && !hasCustomValue;
|
|
1700
|
+
};
|
|
1592
1701
|
choices.forEach((choice, index) => {
|
|
1593
1702
|
const choiceDiv = document.createElement("div");
|
|
1594
1703
|
choiceDiv.className = "user-question-choice";
|
|
@@ -1613,6 +1722,8 @@ var askUserExecutor = async (args2) => {
|
|
|
1613
1722
|
}
|
|
1614
1723
|
} else {
|
|
1615
1724
|
selected.clear();
|
|
1725
|
+
isCustomSelected = false;
|
|
1726
|
+
if (customInput) customInput.value = "";
|
|
1616
1727
|
selected.add(choice);
|
|
1617
1728
|
choicesDiv.querySelectorAll(".user-question-choice").forEach((c) => c.classList.remove("selected"));
|
|
1618
1729
|
choiceDiv.classList.add("selected");
|
|
@@ -1628,9 +1739,62 @@ var askUserExecutor = async (args2) => {
|
|
|
1628
1739
|
});
|
|
1629
1740
|
choicesDiv.appendChild(choiceDiv);
|
|
1630
1741
|
});
|
|
1742
|
+
if (allowCustom) {
|
|
1743
|
+
const customChoiceDiv = document.createElement("div");
|
|
1744
|
+
customChoiceDiv.className = "user-question-choice";
|
|
1745
|
+
const customRadio = document.createElement("input");
|
|
1746
|
+
customRadio.type = inputType;
|
|
1747
|
+
customRadio.name = inputName;
|
|
1748
|
+
customRadio.id = `${inputName}-custom`;
|
|
1749
|
+
customRadio.value = "__custom__";
|
|
1750
|
+
const customLabel = document.createElement("label");
|
|
1751
|
+
customLabel.htmlFor = customRadio.id;
|
|
1752
|
+
customLabel.textContent = "Other:";
|
|
1753
|
+
customLabel.style.flexShrink = "0";
|
|
1754
|
+
customInput = document.createElement("input");
|
|
1755
|
+
customInput.type = "text";
|
|
1756
|
+
customInput.className = "user-question-custom-input";
|
|
1757
|
+
customInput.placeholder = "Type your answer...";
|
|
1758
|
+
customInput.style.marginTop = "0";
|
|
1759
|
+
customInput.style.marginLeft = "8px";
|
|
1760
|
+
customInput.style.flex = "1";
|
|
1761
|
+
customChoiceDiv.appendChild(customRadio);
|
|
1762
|
+
customChoiceDiv.appendChild(customLabel);
|
|
1763
|
+
customChoiceDiv.appendChild(customInput);
|
|
1764
|
+
const handleCustomSelect = () => {
|
|
1765
|
+
if (!allowMultiple) {
|
|
1766
|
+
selected.clear();
|
|
1767
|
+
choicesDiv.querySelectorAll(".user-question-choice").forEach((c) => c.classList.remove("selected"));
|
|
1768
|
+
}
|
|
1769
|
+
isCustomSelected = true;
|
|
1770
|
+
customChoiceDiv.classList.add("selected");
|
|
1771
|
+
customInput?.focus();
|
|
1772
|
+
updateSubmitButton();
|
|
1773
|
+
};
|
|
1774
|
+
customRadio.addEventListener("change", handleCustomSelect);
|
|
1775
|
+
customChoiceDiv.addEventListener("click", (e) => {
|
|
1776
|
+
if (e.target !== customRadio && e.target !== customInput) {
|
|
1777
|
+
customRadio.checked = true;
|
|
1778
|
+
handleCustomSelect();
|
|
1779
|
+
}
|
|
1780
|
+
});
|
|
1781
|
+
customInput.addEventListener("focus", () => {
|
|
1782
|
+
if (!customRadio.checked) {
|
|
1783
|
+
customRadio.checked = true;
|
|
1784
|
+
handleCustomSelect();
|
|
1785
|
+
}
|
|
1786
|
+
});
|
|
1787
|
+
customInput.addEventListener("input", updateSubmitButton);
|
|
1788
|
+
choicesDiv.appendChild(customChoiceDiv);
|
|
1789
|
+
}
|
|
1631
1790
|
modal.appendChild(choicesDiv);
|
|
1632
1791
|
const actions = document.createElement("div");
|
|
1633
1792
|
actions.className = "user-question-actions";
|
|
1793
|
+
overlay.appendChild(modal);
|
|
1794
|
+
document.body.appendChild(overlay);
|
|
1795
|
+
const cleanup = () => {
|
|
1796
|
+
overlay.remove();
|
|
1797
|
+
};
|
|
1634
1798
|
const cancelBtn = document.createElement("button");
|
|
1635
1799
|
cancelBtn.className = "user-question-btn user-question-btn-cancel";
|
|
1636
1800
|
cancelBtn.textContent = "Skip";
|
|
@@ -1642,29 +1806,21 @@ var askUserExecutor = async (args2) => {
|
|
|
1642
1806
|
answered: false
|
|
1643
1807
|
});
|
|
1644
1808
|
};
|
|
1645
|
-
const submitBtn = document.createElement("button");
|
|
1646
|
-
submitBtn.className = "user-question-btn user-question-btn-submit";
|
|
1647
|
-
submitBtn.textContent = "Submit";
|
|
1648
|
-
submitBtn.disabled = true;
|
|
1649
1809
|
submitBtn.onclick = () => {
|
|
1650
1810
|
cleanup();
|
|
1811
|
+
const results = Array.from(selected);
|
|
1812
|
+
if (isCustomSelected && customInput && customInput.value.trim()) {
|
|
1813
|
+
results.push(customInput.value.trim());
|
|
1814
|
+
}
|
|
1651
1815
|
resolve({
|
|
1652
1816
|
question,
|
|
1653
|
-
selectedChoices:
|
|
1817
|
+
selectedChoices: results,
|
|
1654
1818
|
answered: true
|
|
1655
1819
|
});
|
|
1656
1820
|
};
|
|
1657
|
-
const updateSubmitButton = () => {
|
|
1658
|
-
submitBtn.disabled = selected.size === 0;
|
|
1659
|
-
};
|
|
1660
1821
|
actions.appendChild(cancelBtn);
|
|
1661
1822
|
actions.appendChild(submitBtn);
|
|
1662
1823
|
modal.appendChild(actions);
|
|
1663
|
-
overlay.appendChild(modal);
|
|
1664
|
-
document.body.appendChild(overlay);
|
|
1665
|
-
const cleanup = () => {
|
|
1666
|
-
overlay.remove();
|
|
1667
|
-
};
|
|
1668
1824
|
const handleEscape = (e) => {
|
|
1669
1825
|
if (e.key === "Escape") {
|
|
1670
1826
|
document.removeEventListener("keydown", handleEscape);
|
|
@@ -1833,6 +1989,11 @@ var screenshotTool = {
|
|
|
1833
1989
|
|
|
1834
1990
|
The screenshot captures the visible viewport of the page. The image is uploaded to the server and a permanent URL is returned.
|
|
1835
1991
|
|
|
1992
|
+
IMPORTANT: Before taking a screenshot, use the ask_user tool (if available) to ask which size they prefer:
|
|
1993
|
+
- "full" (100%) - highest quality, larger file
|
|
1994
|
+
- "half" (50%) - good balance of quality and size
|
|
1995
|
+
- "quarter" (25%) - smallest file, faster upload
|
|
1996
|
+
|
|
1836
1997
|
Use this when:
|
|
1837
1998
|
- User asks to see what's on their screen
|
|
1838
1999
|
- You need to analyze the current page visually
|
|
@@ -1843,12 +2004,24 @@ Use this when:
|
|
|
1843
2004
|
selector: {
|
|
1844
2005
|
type: "string",
|
|
1845
2006
|
description: "Optional CSS selector to capture a specific element instead of the full page. Leave empty for full page screenshot."
|
|
2007
|
+
},
|
|
2008
|
+
size: {
|
|
2009
|
+
type: "string",
|
|
2010
|
+
enum: ["full", "half", "quarter"],
|
|
2011
|
+
description: 'Image size: "full" (100%), "half" (50%), or "quarter" (25%). Ask the user which size they prefer before capturing.'
|
|
1846
2012
|
}
|
|
1847
2013
|
}
|
|
1848
2014
|
}
|
|
1849
2015
|
};
|
|
1850
2016
|
var screenshotExecutor = async (args2) => {
|
|
1851
2017
|
const selector = args2.selector;
|
|
2018
|
+
const size = args2.size || "full";
|
|
2019
|
+
const scaleMap = {
|
|
2020
|
+
full: 1,
|
|
2021
|
+
half: 0.5,
|
|
2022
|
+
quarter: 0.25
|
|
2023
|
+
};
|
|
2024
|
+
const scale = scaleMap[size] || 1;
|
|
1852
2025
|
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
1853
2026
|
return {
|
|
1854
2027
|
success: false,
|
|
@@ -1869,34 +2042,41 @@ var screenshotExecutor = async (args2) => {
|
|
|
1869
2042
|
if (!target) {
|
|
1870
2043
|
return { success: false, error: "Element not found: " + selector };
|
|
1871
2044
|
}
|
|
1872
|
-
const
|
|
2045
|
+
const fullCanvas = await window.html2canvas(target, {
|
|
1873
2046
|
useCORS: true,
|
|
1874
2047
|
allowTaint: true,
|
|
1875
2048
|
backgroundColor: "#000000",
|
|
1876
2049
|
scale: 1
|
|
1877
2050
|
});
|
|
2051
|
+
let finalCanvas = fullCanvas;
|
|
2052
|
+
if (scale < 1) {
|
|
2053
|
+
const resizedCanvas = document.createElement("canvas");
|
|
2054
|
+
resizedCanvas.width = Math.round(fullCanvas.width * scale);
|
|
2055
|
+
resizedCanvas.height = Math.round(fullCanvas.height * scale);
|
|
2056
|
+
const ctx = resizedCanvas.getContext("2d");
|
|
2057
|
+
if (ctx) {
|
|
2058
|
+
ctx.drawImage(fullCanvas, 0, 0, resizedCanvas.width, resizedCanvas.height);
|
|
2059
|
+
finalCanvas = resizedCanvas;
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
1878
2062
|
const blob = await new Promise((resolve) => {
|
|
1879
|
-
|
|
2063
|
+
finalCanvas.toBlob(resolve, "image/png", 0.9);
|
|
1880
2064
|
});
|
|
1881
2065
|
if (!blob) {
|
|
1882
2066
|
return { success: false, error: "Failed to create image blob" };
|
|
1883
2067
|
}
|
|
1884
|
-
const
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
method: "POST",
|
|
1888
|
-
body: formData
|
|
1889
|
-
});
|
|
1890
|
-
if (!response.ok) {
|
|
1891
|
-
const errorData = await response.json().catch(() => ({}));
|
|
1892
|
-
return { success: false, error: errorData.error || "Upload failed" };
|
|
2068
|
+
const uploadFn = window.__hustleUploadFile;
|
|
2069
|
+
if (!uploadFn) {
|
|
2070
|
+
return { success: false, error: "Upload not available. Make sure HustleProvider is mounted and client is ready." };
|
|
1893
2071
|
}
|
|
1894
|
-
const
|
|
2072
|
+
const fileName = "screenshot-" + Date.now() + ".png";
|
|
2073
|
+
const attachment = await uploadFn(blob, fileName);
|
|
2074
|
+
const sizeLabel = size === "full" ? "100%" : size === "half" ? "50%" : "25%";
|
|
1895
2075
|
return {
|
|
1896
2076
|
success: true,
|
|
1897
|
-
url:
|
|
1898
|
-
contentType:
|
|
1899
|
-
message:
|
|
2077
|
+
url: attachment.url,
|
|
2078
|
+
contentType: attachment.contentType || "image/png",
|
|
2079
|
+
message: `Screenshot captured at ${sizeLabel} size (${finalCanvas.width}x${finalCanvas.height}) and uploaded successfully`
|
|
1900
2080
|
};
|
|
1901
2081
|
} catch (e) {
|
|
1902
2082
|
const err = e;
|
|
@@ -1926,6 +2106,21 @@ var buildPluginTool = {
|
|
|
1926
2106
|
name: "build_plugin",
|
|
1927
2107
|
description: `Build a Hustle plugin definition. Use this tool to construct a plugin based on user requirements.
|
|
1928
2108
|
|
|
2109
|
+
## Testing Before Building (IMPORTANT)
|
|
2110
|
+
|
|
2111
|
+
If the execute_javascript tool is available, use it to prototype and test code BEFORE building the plugin. This allows rapid iteration without the overhead of building/installing/uninstalling.
|
|
2112
|
+
|
|
2113
|
+
**Workflow:**
|
|
2114
|
+
1. When a user describes what they want, first test the core logic using execute_javascript
|
|
2115
|
+
2. Iterate on the code until it works correctly
|
|
2116
|
+
3. Ask the user: "Would you like to test this further, or should I build it into a plugin?"
|
|
2117
|
+
4. Only call build_plugin once the code is validated and the user confirms
|
|
2118
|
+
|
|
2119
|
+
**Example:** If building a weather plugin, first test the API call:
|
|
2120
|
+
execute_javascript({ code: "fetch('https://api.example.com/weather?city=London').then(r => r.json()).then(console.log)" })
|
|
2121
|
+
|
|
2122
|
+
This catches errors early and lets users see results before committing to a plugin.
|
|
2123
|
+
|
|
1929
2124
|
## Plugin Structure
|
|
1930
2125
|
|
|
1931
2126
|
A plugin consists of:
|
|
@@ -1964,21 +2159,127 @@ Write them as arrow function strings that will be eval'd:
|
|
|
1964
2159
|
"async (args) => { const { city } = args; return { weather: 'sunny', city }; }"
|
|
1965
2160
|
|
|
1966
2161
|
The function receives args as Record<string, unknown> and should return the result.
|
|
1967
|
-
|
|
2162
|
+
|
|
2163
|
+
## Available in Executor Scope
|
|
2164
|
+
|
|
2165
|
+
Executors run in the browser context with full access to:
|
|
2166
|
+
|
|
2167
|
+
### Browser APIs
|
|
2168
|
+
- **fetch(url, options)** - HTTP requests (subject to CORS)
|
|
2169
|
+
- **localStorage / sessionStorage** - Persistent storage
|
|
2170
|
+
- **document** - Full DOM access (create elements, modals, forms, etc.)
|
|
2171
|
+
- **window** - Global window object
|
|
2172
|
+
- **console** - Logging (log, warn, error, etc.)
|
|
2173
|
+
- **setTimeout / setInterval / clearTimeout / clearInterval** - Timers
|
|
2174
|
+
- **JSON** - Parse and stringify
|
|
2175
|
+
- **Date** - Date/time operations
|
|
2176
|
+
- **URL / URLSearchParams** - URL manipulation
|
|
2177
|
+
- **FormData / Blob / File / FileReader** - File handling
|
|
2178
|
+
- **crypto** - Cryptographic operations (crypto.randomUUID(), etc.)
|
|
2179
|
+
- **navigator** - Browser info, clipboard, geolocation, etc.
|
|
2180
|
+
- **location** - Current URL info
|
|
2181
|
+
- **history** - Browser history navigation
|
|
2182
|
+
- **WebSocket** - Real-time bidirectional communication
|
|
2183
|
+
- **EventSource** - Server-sent events
|
|
2184
|
+
- **indexedDB** - Client-side database for large data
|
|
2185
|
+
- **Notification** - Browser notifications (requires permission)
|
|
2186
|
+
- **performance** - Performance timing
|
|
2187
|
+
- **atob / btoa** - Base64 encoding/decoding
|
|
2188
|
+
- **TextEncoder / TextDecoder** - Text encoding
|
|
2189
|
+
- **AbortController** - Cancel fetch requests
|
|
2190
|
+
- **IntersectionObserver / MutationObserver / ResizeObserver** - DOM observers
|
|
2191
|
+
- **requestAnimationFrame** - Animation timing
|
|
2192
|
+
- **speechSynthesis** - Text-to-speech
|
|
2193
|
+
- **Audio / Image / Canvas** - Media APIs
|
|
2194
|
+
|
|
2195
|
+
### Hustle Plugin System Globals
|
|
2196
|
+
- **window.__hustleInstanceId** - Current Hustle instance ID
|
|
2197
|
+
- **window.__hustleRegisterPlugin(plugin, enabled)** - Install another plugin dynamically
|
|
2198
|
+
- **window.__hustleUnregisterPlugin(name)** - Uninstall a plugin by name
|
|
2199
|
+
- **window.__hustleUploadFile(file, fileName?)** - Upload a File/Blob to the server, returns { url, contentType }
|
|
2200
|
+
- **window.__hustleListPlugins()** - List all installed plugins
|
|
2201
|
+
- **window.__hustleGetPlugin(name)** - Get a specific plugin by name
|
|
2202
|
+
|
|
2203
|
+
### DOM Manipulation Examples
|
|
2204
|
+
Create a modal: document.createElement('div'), style it, append to document.body
|
|
2205
|
+
Add event listeners: element.addEventListener('click', handler)
|
|
2206
|
+
Query elements: document.querySelector(), document.querySelectorAll()
|
|
2207
|
+
|
|
2208
|
+
### Storage Patterns
|
|
2209
|
+
Store data: localStorage.setItem('key', JSON.stringify(data))
|
|
2210
|
+
Retrieve data: JSON.parse(localStorage.getItem('key') || '{}')
|
|
2211
|
+
Namespace your keys: Use plugin name prefix like "myplugin-settings"
|
|
2212
|
+
|
|
2213
|
+
### Async Patterns
|
|
2214
|
+
All executors should be async. Use await for promises:
|
|
2215
|
+
"async (args) => { const res = await fetch(url); return await res.json(); }"
|
|
1968
2216
|
|
|
1969
2217
|
## Lifecycle Hooks (Optional)
|
|
1970
2218
|
|
|
1971
|
-
|
|
2219
|
+
Hooks also have full access to the browser scope described above.
|
|
2220
|
+
|
|
2221
|
+
- **onRegisterCode**: Called once when plugin is registered/enabled. Good for initialization.
|
|
2222
|
+
**IMPORTANT: Always log when your plugin registers so users know it's active!**
|
|
2223
|
+
Example: "async () => { console.log('[MyPlugin] v1.0.0 registered'); }"
|
|
2224
|
+
|
|
2225
|
+
- **beforeRequestCode**: Modify messages before sending. Receives request object with { messages, model, ... }. Must return the modified request.
|
|
1972
2226
|
Example: "async (req) => { req.messages = req.messages.map(m => ({...m, content: m.content.toUpperCase()})); return req; }"
|
|
1973
2227
|
|
|
1974
|
-
- **afterResponseCode**: Process response after receiving. Receives response object.
|
|
1975
|
-
Example: "async (res) => { console.log('Response:', res.content); }"
|
|
2228
|
+
- **afterResponseCode**: Process/modify response after receiving. Receives response object with { content, ... }.
|
|
2229
|
+
Example: "async (res) => { console.log('Response received:', res.content.substring(0, 100)); }"
|
|
2230
|
+
|
|
2231
|
+
- **onErrorCode**: Called on errors. Receives (error, context) where context has { phase: 'beforeRequest'|'execute'|'afterResponse' }.
|
|
2232
|
+
Example: "async (error, ctx) => { console.error('[MyPlugin] Error in', ctx.phase, ':', error.message); }"
|
|
2233
|
+
|
|
2234
|
+
## Best Practices
|
|
2235
|
+
|
|
2236
|
+
1. **Always add onRegisterCode** that logs the plugin name and version
|
|
2237
|
+
2. **Namespace console logs** with [PluginName] prefix for easy identification
|
|
2238
|
+
3. **Handle errors gracefully** in executors - return { error: message } instead of throwing
|
|
1976
2239
|
|
|
1977
|
-
|
|
1978
|
-
Example: "async () => { console.log('Plugin loaded!'); }"
|
|
2240
|
+
## Building Plugin UI
|
|
1979
2241
|
|
|
1980
|
-
|
|
1981
|
-
|
|
2242
|
+
Plugins can embed custom UI elements in two ways:
|
|
2243
|
+
|
|
2244
|
+
### Persistent UI (via onRegister hook)
|
|
2245
|
+
Use the onRegister hook to embed UI that persists across the chat session. Check if your element already exists to avoid duplicates on re-registration:
|
|
2246
|
+
|
|
2247
|
+
"async () => {
|
|
2248
|
+
console.log('[MyPlugin] v1.0.0 registered');
|
|
2249
|
+
if (document.getElementById('my-plugin-panel')) return; // Already exists
|
|
2250
|
+
|
|
2251
|
+
const panel = document.createElement('div');
|
|
2252
|
+
panel.id = 'my-plugin-panel';
|
|
2253
|
+
Object.assign(panel.style, { position: 'fixed', bottom: '20px', right: '20px', padding: '10px', background: '#333', color: '#fff' });
|
|
2254
|
+
panel.textContent = 'My Panel';
|
|
2255
|
+
document.body.appendChild(panel);
|
|
2256
|
+
}"
|
|
2257
|
+
|
|
2258
|
+
### On-Demand UI (via tool executors)
|
|
2259
|
+
Embed UI just-in-time when a tool is invoked. Useful for tools like show_clock, display_chart, etc.:
|
|
2260
|
+
|
|
2261
|
+
"async (args) => {
|
|
2262
|
+
const modal = document.createElement('div');
|
|
2263
|
+
modal.id = 'clock-modal';
|
|
2264
|
+
Object.assign(modal.style, { position: 'fixed', inset: '0', display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'rgba(0,0,0,0.5)' });
|
|
2265
|
+
modal.textContent = 'Current time: ' + new Date().toLocaleTimeString();
|
|
2266
|
+
modal.onclick = () => modal.remove(); // Click to dismiss
|
|
2267
|
+
document.body.appendChild(modal);
|
|
2268
|
+
return { success: true, message: 'Clock displayed' };
|
|
2269
|
+
}"
|
|
2270
|
+
|
|
2271
|
+
### UI Best Practices
|
|
2272
|
+
- Always use unique IDs with your plugin name prefix (e.g., "myplugin-modal")
|
|
2273
|
+
- For persistent UI, check existence in onRegister before creating
|
|
2274
|
+
- Provide a way to dismiss/close UI elements (click handler, close button)
|
|
2275
|
+
- Use Object.assign(el.style, {...}) for inline styles or inject a <style> tag
|
|
2276
|
+
- Clean up UI in tool executors if appropriate (e.g., remove after timeout)
|
|
2277
|
+
|
|
2278
|
+
## Security Notes
|
|
2279
|
+
- Code runs in browser sandbox with same-origin policy
|
|
2280
|
+
- fetch() is subject to CORS restrictions
|
|
2281
|
+
- No direct filesystem access (use File API with user interaction)
|
|
2282
|
+
- Sanitize any user-provided content before inserting into the DOM`,
|
|
1982
2283
|
parameters: {
|
|
1983
2284
|
type: "object",
|
|
1984
2285
|
properties: {
|
|
@@ -2067,6 +2368,104 @@ var installPluginTool = {
|
|
|
2067
2368
|
required: ["plugin"]
|
|
2068
2369
|
}
|
|
2069
2370
|
};
|
|
2371
|
+
var uninstallPluginTool = {
|
|
2372
|
+
name: "uninstall_plugin",
|
|
2373
|
+
description: "Uninstall a plugin by name, removing it from browser storage.",
|
|
2374
|
+
parameters: {
|
|
2375
|
+
type: "object",
|
|
2376
|
+
properties: {
|
|
2377
|
+
name: {
|
|
2378
|
+
type: "string",
|
|
2379
|
+
description: "The name of the plugin to uninstall"
|
|
2380
|
+
}
|
|
2381
|
+
},
|
|
2382
|
+
required: ["name"]
|
|
2383
|
+
}
|
|
2384
|
+
};
|
|
2385
|
+
var listPluginsTool = {
|
|
2386
|
+
name: "list_plugins",
|
|
2387
|
+
description: "List all installed plugins with their enabled/disabled status.",
|
|
2388
|
+
parameters: {
|
|
2389
|
+
type: "object",
|
|
2390
|
+
properties: {},
|
|
2391
|
+
required: []
|
|
2392
|
+
}
|
|
2393
|
+
};
|
|
2394
|
+
var modifyPluginTool = {
|
|
2395
|
+
name: "modify_plugin",
|
|
2396
|
+
description: `Modify an existing installed plugin. Can update version, description, tools, executors, and hooks.
|
|
2397
|
+
|
|
2398
|
+
Use list_plugins first to see installed plugins, then modify by name.
|
|
2399
|
+
|
|
2400
|
+
You can:
|
|
2401
|
+
- Add new tools (provide tools array with new tools to add)
|
|
2402
|
+
- Update existing tools (provide tool with same name)
|
|
2403
|
+
- Remove tools (set removeTool to the tool name)
|
|
2404
|
+
- Update hooks (provide hook code)
|
|
2405
|
+
- Update version/description
|
|
2406
|
+
|
|
2407
|
+
Example: Add a new tool to existing plugin:
|
|
2408
|
+
{
|
|
2409
|
+
"name": "my-plugin",
|
|
2410
|
+
"addTools": [{ "name": "new_tool", "description": "...", "parameters": {...} }],
|
|
2411
|
+
"addExecutorCode": { "new_tool": "async (args) => { ... }" }
|
|
2412
|
+
}`,
|
|
2413
|
+
parameters: {
|
|
2414
|
+
type: "object",
|
|
2415
|
+
properties: {
|
|
2416
|
+
name: {
|
|
2417
|
+
type: "string",
|
|
2418
|
+
description: "Name of the plugin to modify (required)"
|
|
2419
|
+
},
|
|
2420
|
+
version: {
|
|
2421
|
+
type: "string",
|
|
2422
|
+
description: "New version string"
|
|
2423
|
+
},
|
|
2424
|
+
description: {
|
|
2425
|
+
type: "string",
|
|
2426
|
+
description: "New description"
|
|
2427
|
+
},
|
|
2428
|
+
addTools: {
|
|
2429
|
+
type: "array",
|
|
2430
|
+
description: "Tools to add or update",
|
|
2431
|
+
items: {
|
|
2432
|
+
type: "object",
|
|
2433
|
+
properties: {
|
|
2434
|
+
name: { type: "string" },
|
|
2435
|
+
description: { type: "string" },
|
|
2436
|
+
parameters: { type: "object" }
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
},
|
|
2440
|
+
addExecutorCode: {
|
|
2441
|
+
type: "object",
|
|
2442
|
+
description: "Executor code to add/update (tool name -> code string)"
|
|
2443
|
+
},
|
|
2444
|
+
removeTools: {
|
|
2445
|
+
type: "array",
|
|
2446
|
+
description: "Names of tools to remove",
|
|
2447
|
+
items: { type: "string" }
|
|
2448
|
+
},
|
|
2449
|
+
onRegisterCode: {
|
|
2450
|
+
type: "string",
|
|
2451
|
+
description: "New onRegister hook code"
|
|
2452
|
+
},
|
|
2453
|
+
beforeRequestCode: {
|
|
2454
|
+
type: "string",
|
|
2455
|
+
description: "New beforeRequest hook code"
|
|
2456
|
+
},
|
|
2457
|
+
afterResponseCode: {
|
|
2458
|
+
type: "string",
|
|
2459
|
+
description: "New afterResponse hook code"
|
|
2460
|
+
},
|
|
2461
|
+
onErrorCode: {
|
|
2462
|
+
type: "string",
|
|
2463
|
+
description: "New onError hook code"
|
|
2464
|
+
}
|
|
2465
|
+
},
|
|
2466
|
+
required: ["name"]
|
|
2467
|
+
}
|
|
2468
|
+
};
|
|
2070
2469
|
var buildPluginExecutor = async (args2) => {
|
|
2071
2470
|
const {
|
|
2072
2471
|
name,
|
|
@@ -2123,12 +2522,51 @@ var buildPluginExecutor = async (args2) => {
|
|
|
2123
2522
|
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.`
|
|
2124
2523
|
};
|
|
2125
2524
|
};
|
|
2525
|
+
function normalizePlugin(input) {
|
|
2526
|
+
const plugin = input;
|
|
2527
|
+
const tools = plugin.tools;
|
|
2528
|
+
const hasEmbeddedExecutors = tools?.[0]?.executorCode !== void 0;
|
|
2529
|
+
if (hasEmbeddedExecutors) {
|
|
2530
|
+
return {
|
|
2531
|
+
...plugin,
|
|
2532
|
+
enabled: plugin.enabled ?? true,
|
|
2533
|
+
installedAt: plugin.installedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
2534
|
+
};
|
|
2535
|
+
}
|
|
2536
|
+
const executorCode = plugin.executorCode;
|
|
2537
|
+
const rawTools = tools || [];
|
|
2538
|
+
const hookKeys = ["onRegisterCode", "beforeRequestCode", "afterResponseCode", "onErrorCode"];
|
|
2539
|
+
const hooksCode = {};
|
|
2540
|
+
for (const key of hookKeys) {
|
|
2541
|
+
if (executorCode?.[key]) {
|
|
2542
|
+
hooksCode[key] = executorCode[key];
|
|
2543
|
+
}
|
|
2544
|
+
if (plugin[key]) {
|
|
2545
|
+
hooksCode[key] = plugin[key];
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
return {
|
|
2549
|
+
name: plugin.name,
|
|
2550
|
+
version: plugin.version,
|
|
2551
|
+
description: plugin.description,
|
|
2552
|
+
tools: rawTools.map((tool) => ({
|
|
2553
|
+
name: tool.name,
|
|
2554
|
+
description: tool.description,
|
|
2555
|
+
parameters: tool.parameters,
|
|
2556
|
+
executorCode: executorCode?.[tool.name]
|
|
2557
|
+
})),
|
|
2558
|
+
hooksCode: Object.keys(hooksCode).length > 0 ? hooksCode : void 0,
|
|
2559
|
+
enabled: true,
|
|
2560
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2561
|
+
};
|
|
2562
|
+
}
|
|
2126
2563
|
var savePluginExecutor = async (args2) => {
|
|
2127
|
-
const { plugin, filename } = args2;
|
|
2564
|
+
const { plugin: rawPlugin, filename } = args2;
|
|
2128
2565
|
if (typeof window === "undefined") {
|
|
2129
2566
|
return { success: false, error: "Cannot save files in server environment" };
|
|
2130
2567
|
}
|
|
2131
2568
|
try {
|
|
2569
|
+
const plugin = normalizePlugin(rawPlugin);
|
|
2132
2570
|
const json2 = JSON.stringify(plugin, null, 2);
|
|
2133
2571
|
const blob = new Blob([json2], { type: "application/json" });
|
|
2134
2572
|
const url = URL.createObjectURL(blob);
|
|
@@ -2151,31 +2589,23 @@ var savePluginExecutor = async (args2) => {
|
|
|
2151
2589
|
}
|
|
2152
2590
|
};
|
|
2153
2591
|
var installPluginExecutor = async (args2) => {
|
|
2154
|
-
const { plugin, enabled = true } = args2;
|
|
2592
|
+
const { plugin: rawPlugin, enabled = true } = args2;
|
|
2155
2593
|
if (typeof window === "undefined") {
|
|
2156
2594
|
return { success: false, error: "Cannot install plugins in server environment" };
|
|
2157
2595
|
}
|
|
2158
2596
|
try {
|
|
2159
|
-
const
|
|
2160
|
-
const
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
enabled,
|
|
2167
|
-
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2168
|
-
};
|
|
2169
|
-
if (existingIndex >= 0) {
|
|
2170
|
-
plugins[existingIndex] = pluginToStore;
|
|
2171
|
-
} else {
|
|
2172
|
-
plugins.push(pluginToStore);
|
|
2597
|
+
const plugin = normalizePlugin(rawPlugin);
|
|
2598
|
+
const win = window;
|
|
2599
|
+
if (!win.__hustleRegisterPlugin) {
|
|
2600
|
+
return {
|
|
2601
|
+
success: false,
|
|
2602
|
+
error: "Plugin registration not available. Make sure HustleProvider is mounted."
|
|
2603
|
+
};
|
|
2173
2604
|
}
|
|
2174
|
-
|
|
2605
|
+
await win.__hustleRegisterPlugin(plugin, enabled);
|
|
2175
2606
|
return {
|
|
2176
2607
|
success: true,
|
|
2177
|
-
message: `Plugin "${plugin.name}" installed${enabled ? " and enabled" : ""}
|
|
2178
|
-
action: existingIndex >= 0 ? "updated" : "installed"
|
|
2608
|
+
message: `Plugin "${plugin.name}" installed${enabled ? " and enabled" : ""}.`
|
|
2179
2609
|
};
|
|
2180
2610
|
} catch (error2) {
|
|
2181
2611
|
return {
|
|
@@ -2184,15 +2614,168 @@ var installPluginExecutor = async (args2) => {
|
|
|
2184
2614
|
};
|
|
2185
2615
|
}
|
|
2186
2616
|
};
|
|
2617
|
+
var uninstallPluginExecutor = async (args2) => {
|
|
2618
|
+
const { name } = args2;
|
|
2619
|
+
if (typeof window === "undefined") {
|
|
2620
|
+
return { success: false, error: "Cannot uninstall plugins in server environment" };
|
|
2621
|
+
}
|
|
2622
|
+
try {
|
|
2623
|
+
const win = window;
|
|
2624
|
+
if (!win.__hustleUnregisterPlugin) {
|
|
2625
|
+
return {
|
|
2626
|
+
success: false,
|
|
2627
|
+
error: "Plugin unregistration not available. Make sure HustleProvider is mounted."
|
|
2628
|
+
};
|
|
2629
|
+
}
|
|
2630
|
+
await win.__hustleUnregisterPlugin(name);
|
|
2631
|
+
return {
|
|
2632
|
+
success: true,
|
|
2633
|
+
message: `Plugin "${name}" has been uninstalled.`
|
|
2634
|
+
};
|
|
2635
|
+
} catch (error2) {
|
|
2636
|
+
return {
|
|
2637
|
+
success: false,
|
|
2638
|
+
error: `Failed to uninstall: ${error2 instanceof Error ? error2.message : "Unknown error"}`
|
|
2639
|
+
};
|
|
2640
|
+
}
|
|
2641
|
+
};
|
|
2642
|
+
var listPluginsExecutor = async () => {
|
|
2643
|
+
if (typeof window === "undefined") {
|
|
2644
|
+
return { success: false, error: "Cannot list plugins in server environment" };
|
|
2645
|
+
}
|
|
2646
|
+
try {
|
|
2647
|
+
const win = window;
|
|
2648
|
+
if (!win.__hustleListPlugins) {
|
|
2649
|
+
return {
|
|
2650
|
+
success: false,
|
|
2651
|
+
error: "Plugin listing not available. Make sure HustleProvider is mounted."
|
|
2652
|
+
};
|
|
2653
|
+
}
|
|
2654
|
+
const plugins = win.__hustleListPlugins();
|
|
2655
|
+
return {
|
|
2656
|
+
success: true,
|
|
2657
|
+
plugins,
|
|
2658
|
+
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(", ")}`
|
|
2659
|
+
};
|
|
2660
|
+
} catch (error2) {
|
|
2661
|
+
return {
|
|
2662
|
+
success: false,
|
|
2663
|
+
error: `Failed to list plugins: ${error2 instanceof Error ? error2.message : "Unknown error"}`
|
|
2664
|
+
};
|
|
2665
|
+
}
|
|
2666
|
+
};
|
|
2667
|
+
var modifyPluginExecutor = async (args2) => {
|
|
2668
|
+
const {
|
|
2669
|
+
name,
|
|
2670
|
+
version,
|
|
2671
|
+
description,
|
|
2672
|
+
addTools,
|
|
2673
|
+
addExecutorCode,
|
|
2674
|
+
removeTools,
|
|
2675
|
+
onRegisterCode,
|
|
2676
|
+
beforeRequestCode,
|
|
2677
|
+
afterResponseCode,
|
|
2678
|
+
onErrorCode
|
|
2679
|
+
} = args2;
|
|
2680
|
+
if (typeof window === "undefined") {
|
|
2681
|
+
return { success: false, error: "Cannot modify plugins in server environment" };
|
|
2682
|
+
}
|
|
2683
|
+
try {
|
|
2684
|
+
const win = window;
|
|
2685
|
+
if (!win.__hustleGetPlugin || !win.__hustleRegisterPlugin) {
|
|
2686
|
+
return {
|
|
2687
|
+
success: false,
|
|
2688
|
+
error: "Plugin modification not available. Make sure HustleProvider is mounted."
|
|
2689
|
+
};
|
|
2690
|
+
}
|
|
2691
|
+
const existing = win.__hustleGetPlugin(name);
|
|
2692
|
+
if (!existing) {
|
|
2693
|
+
return {
|
|
2694
|
+
success: false,
|
|
2695
|
+
error: `Plugin "${name}" not found. Use list_plugins to see installed plugins.`
|
|
2696
|
+
};
|
|
2697
|
+
}
|
|
2698
|
+
let tools = existing.tools ? [...existing.tools] : [];
|
|
2699
|
+
if (removeTools && removeTools.length > 0) {
|
|
2700
|
+
tools = tools.filter((t) => !removeTools.includes(t.name));
|
|
2701
|
+
}
|
|
2702
|
+
if (addTools && addTools.length > 0) {
|
|
2703
|
+
for (const newTool of addTools) {
|
|
2704
|
+
const existingIndex = tools.findIndex((t) => t.name === newTool.name);
|
|
2705
|
+
const toolWithExecutor = {
|
|
2706
|
+
...newTool,
|
|
2707
|
+
executorCode: addExecutorCode?.[newTool.name] || tools.find((t) => t.name === newTool.name)?.executorCode
|
|
2708
|
+
};
|
|
2709
|
+
if (existingIndex >= 0) {
|
|
2710
|
+
tools[existingIndex] = toolWithExecutor;
|
|
2711
|
+
} else {
|
|
2712
|
+
tools.push(toolWithExecutor);
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
if (addExecutorCode) {
|
|
2717
|
+
for (const [toolName, code2] of Object.entries(addExecutorCode)) {
|
|
2718
|
+
const tool = tools.find((t) => t.name === toolName);
|
|
2719
|
+
if (tool) {
|
|
2720
|
+
tool.executorCode = code2;
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
const modified = {
|
|
2725
|
+
...existing,
|
|
2726
|
+
tools
|
|
2727
|
+
};
|
|
2728
|
+
if (version) modified.version = version;
|
|
2729
|
+
if (description) modified.description = description;
|
|
2730
|
+
if (onRegisterCode || beforeRequestCode || afterResponseCode || onErrorCode) {
|
|
2731
|
+
modified.hooksCode = modified.hooksCode || {};
|
|
2732
|
+
if (onRegisterCode) modified.hooksCode.onRegisterCode = onRegisterCode;
|
|
2733
|
+
if (beforeRequestCode) modified.hooksCode.beforeRequestCode = beforeRequestCode;
|
|
2734
|
+
if (afterResponseCode) modified.hooksCode.afterResponseCode = afterResponseCode;
|
|
2735
|
+
if (onErrorCode) modified.hooksCode.onErrorCode = onErrorCode;
|
|
2736
|
+
}
|
|
2737
|
+
const unregisterFn = window.__hustleUnregisterPlugin;
|
|
2738
|
+
if (unregisterFn) {
|
|
2739
|
+
await unregisterFn(name);
|
|
2740
|
+
}
|
|
2741
|
+
await win.__hustleRegisterPlugin(modified, existing.enabled);
|
|
2742
|
+
const changes = [];
|
|
2743
|
+
if (version) changes.push(`version \u2192 ${version}`);
|
|
2744
|
+
if (description) changes.push("description updated");
|
|
2745
|
+
if (removeTools?.length) changes.push(`removed ${removeTools.length} tool(s)`);
|
|
2746
|
+
if (addTools?.length) changes.push(`added/updated ${addTools.length} tool(s)`);
|
|
2747
|
+
if (onRegisterCode) changes.push("onRegister hook updated");
|
|
2748
|
+
if (beforeRequestCode) changes.push("beforeRequest hook updated");
|
|
2749
|
+
if (afterResponseCode) changes.push("afterResponse hook updated");
|
|
2750
|
+
if (onErrorCode) changes.push("onError hook updated");
|
|
2751
|
+
return {
|
|
2752
|
+
success: true,
|
|
2753
|
+
message: `Plugin "${name}" modified: ${changes.join(", ")}`,
|
|
2754
|
+
plugin: {
|
|
2755
|
+
name: modified.name,
|
|
2756
|
+
version: modified.version,
|
|
2757
|
+
toolCount: tools.length
|
|
2758
|
+
}
|
|
2759
|
+
};
|
|
2760
|
+
} catch (error2) {
|
|
2761
|
+
return {
|
|
2762
|
+
success: false,
|
|
2763
|
+
error: `Failed to modify plugin: ${error2 instanceof Error ? error2.message : "Unknown error"}`
|
|
2764
|
+
};
|
|
2765
|
+
}
|
|
2766
|
+
};
|
|
2187
2767
|
var pluginBuilderPlugin = {
|
|
2188
2768
|
name: "plugin-builder",
|
|
2189
|
-
version: "1.
|
|
2769
|
+
version: "1.2.0",
|
|
2190
2770
|
description: "Build custom plugins through conversation",
|
|
2191
|
-
tools: [buildPluginTool, savePluginTool, installPluginTool],
|
|
2771
|
+
tools: [buildPluginTool, savePluginTool, installPluginTool, uninstallPluginTool, listPluginsTool, modifyPluginTool],
|
|
2192
2772
|
executors: {
|
|
2193
2773
|
build_plugin: buildPluginExecutor,
|
|
2194
2774
|
save_plugin: savePluginExecutor,
|
|
2195
|
-
install_plugin: installPluginExecutor
|
|
2775
|
+
install_plugin: installPluginExecutor,
|
|
2776
|
+
uninstall_plugin: uninstallPluginExecutor,
|
|
2777
|
+
list_plugins: listPluginsExecutor,
|
|
2778
|
+
modify_plugin: modifyPluginExecutor
|
|
2196
2779
|
},
|
|
2197
2780
|
hooks: {
|
|
2198
2781
|
onRegister: () => {
|