@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
|
@@ -1820,35 +1820,52 @@ var pluginRegistry = new PluginRegistry();
|
|
|
1820
1820
|
function getStorageKey(instanceId) {
|
|
1821
1821
|
return `hustle-plugins-${instanceId}`;
|
|
1822
1822
|
}
|
|
1823
|
-
function
|
|
1823
|
+
function getInstanceId(providedId) {
|
|
1824
|
+
if (providedId) return providedId;
|
|
1825
|
+
if (typeof window !== "undefined") {
|
|
1826
|
+
const globalId = window.__hustleInstanceId;
|
|
1827
|
+
if (globalId) return globalId;
|
|
1828
|
+
}
|
|
1829
|
+
return "default";
|
|
1830
|
+
}
|
|
1831
|
+
function usePlugins(instanceId) {
|
|
1832
|
+
const [resolvedInstanceId] = useState(() => getInstanceId(instanceId));
|
|
1824
1833
|
const [plugins, setPlugins] = useState([]);
|
|
1825
1834
|
useEffect(() => {
|
|
1826
|
-
setPlugins(pluginRegistry.loadFromStorage(
|
|
1827
|
-
const unsubscribe = pluginRegistry.onChange(setPlugins,
|
|
1828
|
-
const storageKey = getStorageKey(
|
|
1835
|
+
setPlugins(pluginRegistry.loadFromStorage(resolvedInstanceId));
|
|
1836
|
+
const unsubscribe = pluginRegistry.onChange(setPlugins, resolvedInstanceId);
|
|
1837
|
+
const storageKey = getStorageKey(resolvedInstanceId);
|
|
1829
1838
|
const handleStorage = (e) => {
|
|
1830
1839
|
if (e.key === storageKey) {
|
|
1831
|
-
setPlugins(pluginRegistry.loadFromStorage(
|
|
1840
|
+
setPlugins(pluginRegistry.loadFromStorage(resolvedInstanceId));
|
|
1832
1841
|
}
|
|
1833
1842
|
};
|
|
1834
1843
|
window.addEventListener("storage", handleStorage);
|
|
1844
|
+
const handlePluginInstalled = (e) => {
|
|
1845
|
+
const customEvent = e;
|
|
1846
|
+
if (customEvent.detail.instanceId === resolvedInstanceId) {
|
|
1847
|
+
setPlugins(pluginRegistry.loadFromStorage(resolvedInstanceId));
|
|
1848
|
+
}
|
|
1849
|
+
};
|
|
1850
|
+
window.addEventListener("hustle-plugin-installed", handlePluginInstalled);
|
|
1835
1851
|
return () => {
|
|
1836
1852
|
unsubscribe();
|
|
1837
1853
|
window.removeEventListener("storage", handleStorage);
|
|
1854
|
+
window.removeEventListener("hustle-plugin-installed", handlePluginInstalled);
|
|
1838
1855
|
};
|
|
1839
|
-
}, [
|
|
1856
|
+
}, [resolvedInstanceId]);
|
|
1840
1857
|
const registerPlugin = useCallback((plugin) => {
|
|
1841
|
-
pluginRegistry.register(plugin, true,
|
|
1842
|
-
}, [
|
|
1858
|
+
pluginRegistry.register(plugin, true, resolvedInstanceId);
|
|
1859
|
+
}, [resolvedInstanceId]);
|
|
1843
1860
|
const unregisterPlugin = useCallback((name) => {
|
|
1844
|
-
pluginRegistry.unregister(name,
|
|
1845
|
-
}, [
|
|
1861
|
+
pluginRegistry.unregister(name, resolvedInstanceId);
|
|
1862
|
+
}, [resolvedInstanceId]);
|
|
1846
1863
|
const enablePlugin = useCallback((name) => {
|
|
1847
|
-
pluginRegistry.setEnabled(name, true,
|
|
1848
|
-
}, [
|
|
1864
|
+
pluginRegistry.setEnabled(name, true, resolvedInstanceId);
|
|
1865
|
+
}, [resolvedInstanceId]);
|
|
1849
1866
|
const disablePlugin = useCallback((name) => {
|
|
1850
|
-
pluginRegistry.setEnabled(name, false,
|
|
1851
|
-
}, [
|
|
1867
|
+
pluginRegistry.setEnabled(name, false, resolvedInstanceId);
|
|
1868
|
+
}, [resolvedInstanceId]);
|
|
1852
1869
|
const isRegistered = useCallback(
|
|
1853
1870
|
(name) => plugins.some((p) => p.name === name),
|
|
1854
1871
|
[plugins]
|
|
@@ -1903,6 +1920,42 @@ function HustleProvider({
|
|
|
1903
1920
|
}
|
|
1904
1921
|
};
|
|
1905
1922
|
}, [isAutoInstance, resolvedInstanceId]);
|
|
1923
|
+
useEffect(() => {
|
|
1924
|
+
if (typeof window !== "undefined") {
|
|
1925
|
+
const win = window;
|
|
1926
|
+
win.__hustleInstanceId = resolvedInstanceId;
|
|
1927
|
+
win.__hustleRegisterPlugin = async (plugin, enabled = true) => {
|
|
1928
|
+
const hydrated = hydratePlugin(plugin);
|
|
1929
|
+
pluginRegistry.register(hydrated, enabled, resolvedInstanceId);
|
|
1930
|
+
};
|
|
1931
|
+
win.__hustleUnregisterPlugin = async (name) => {
|
|
1932
|
+
pluginRegistry.unregister(name, resolvedInstanceId);
|
|
1933
|
+
};
|
|
1934
|
+
win.__hustleListPlugins = () => {
|
|
1935
|
+
const plugins = pluginRegistry.loadFromStorage(resolvedInstanceId);
|
|
1936
|
+
return plugins.map((p) => ({
|
|
1937
|
+
name: p.name,
|
|
1938
|
+
version: p.version,
|
|
1939
|
+
description: p.description || "",
|
|
1940
|
+
enabled: p.enabled
|
|
1941
|
+
}));
|
|
1942
|
+
};
|
|
1943
|
+
win.__hustleGetPlugin = (name) => {
|
|
1944
|
+
const plugins = pluginRegistry.loadFromStorage(resolvedInstanceId);
|
|
1945
|
+
return plugins.find((p) => p.name === name) || null;
|
|
1946
|
+
};
|
|
1947
|
+
}
|
|
1948
|
+
return () => {
|
|
1949
|
+
if (typeof window !== "undefined") {
|
|
1950
|
+
const win = window;
|
|
1951
|
+
delete win.__hustleInstanceId;
|
|
1952
|
+
delete win.__hustleRegisterPlugin;
|
|
1953
|
+
delete win.__hustleUnregisterPlugin;
|
|
1954
|
+
delete win.__hustleListPlugins;
|
|
1955
|
+
delete win.__hustleGetPlugin;
|
|
1956
|
+
}
|
|
1957
|
+
};
|
|
1958
|
+
}, [resolvedInstanceId]);
|
|
1906
1959
|
const isApiKeyMode = Boolean(apiKey && vaultId);
|
|
1907
1960
|
const authContext = useEmblemAuthOptional();
|
|
1908
1961
|
const authSDK = isApiKeyMode ? null : authContext?.authSDK ?? null;
|
|
@@ -2050,6 +2103,19 @@ function HustleProvider({
|
|
|
2050
2103
|
};
|
|
2051
2104
|
registerPlugins();
|
|
2052
2105
|
}, [client, enabledPlugins, log]);
|
|
2106
|
+
useEffect(() => {
|
|
2107
|
+
if (typeof window !== "undefined" && client) {
|
|
2108
|
+
const win = window;
|
|
2109
|
+
win.__hustleUploadFile = async (file, fileName) => {
|
|
2110
|
+
return await client.uploadFile(file, fileName);
|
|
2111
|
+
};
|
|
2112
|
+
}
|
|
2113
|
+
return () => {
|
|
2114
|
+
if (typeof window !== "undefined") {
|
|
2115
|
+
delete window.__hustleUploadFile;
|
|
2116
|
+
}
|
|
2117
|
+
};
|
|
2118
|
+
}, [client]);
|
|
2053
2119
|
const loadModels = useCallback(async () => {
|
|
2054
2120
|
if (!client) {
|
|
2055
2121
|
log("Cannot load models - client not ready");
|
|
@@ -2930,12 +2996,16 @@ function ensureModalStyles() {
|
|
|
2930
2996
|
left: 0;
|
|
2931
2997
|
right: 0;
|
|
2932
2998
|
bottom: 0;
|
|
2933
|
-
background:
|
|
2999
|
+
background: transparent;
|
|
2934
3000
|
display: flex;
|
|
2935
3001
|
align-items: center;
|
|
2936
3002
|
justify-content: center;
|
|
2937
3003
|
z-index: 10000;
|
|
2938
|
-
|
|
3004
|
+
pointer-events: none;
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
.user-question-modal {
|
|
3008
|
+
pointer-events: auto;
|
|
2939
3009
|
}
|
|
2940
3010
|
|
|
2941
3011
|
@keyframes uqFadeIn {
|
|
@@ -3048,6 +3118,27 @@ function ensureModalStyles() {
|
|
|
3048
3118
|
color: #666;
|
|
3049
3119
|
cursor: not-allowed;
|
|
3050
3120
|
}
|
|
3121
|
+
|
|
3122
|
+
.user-question-custom-input {
|
|
3123
|
+
width: 100%;
|
|
3124
|
+
padding: 8px 12px;
|
|
3125
|
+
margin-top: 8px;
|
|
3126
|
+
background: #1a1a2e;
|
|
3127
|
+
border: 1px solid #444;
|
|
3128
|
+
border-radius: 6px;
|
|
3129
|
+
color: #e0e0e0;
|
|
3130
|
+
font-size: 14px;
|
|
3131
|
+
box-sizing: border-box;
|
|
3132
|
+
}
|
|
3133
|
+
|
|
3134
|
+
.user-question-custom-input:focus {
|
|
3135
|
+
outline: none;
|
|
3136
|
+
border-color: #4a7aff;
|
|
3137
|
+
}
|
|
3138
|
+
|
|
3139
|
+
.user-question-custom-input::placeholder {
|
|
3140
|
+
color: #666;
|
|
3141
|
+
}
|
|
3051
3142
|
`;
|
|
3052
3143
|
document.head.appendChild(styles2);
|
|
3053
3144
|
}
|
|
@@ -3069,13 +3160,17 @@ var askUserTool = {
|
|
|
3069
3160
|
allowMultiple: {
|
|
3070
3161
|
type: "boolean",
|
|
3071
3162
|
description: "If true, user can select multiple choices. Default: false"
|
|
3163
|
+
},
|
|
3164
|
+
allowCustom: {
|
|
3165
|
+
type: "boolean",
|
|
3166
|
+
description: 'If true, adds an "Other" option where user can type a custom response. Default: false'
|
|
3072
3167
|
}
|
|
3073
3168
|
},
|
|
3074
3169
|
required: ["question", "choices"]
|
|
3075
3170
|
}
|
|
3076
3171
|
};
|
|
3077
3172
|
var askUserExecutor = async (args2) => {
|
|
3078
|
-
const { question, choices, allowMultiple = false } = args2;
|
|
3173
|
+
const { question, choices, allowMultiple = false, allowCustom = false } = args2;
|
|
3079
3174
|
if (!question || !choices || !Array.isArray(choices) || choices.length === 0) {
|
|
3080
3175
|
return {
|
|
3081
3176
|
question: question || "",
|
|
@@ -3107,6 +3202,17 @@ var askUserExecutor = async (args2) => {
|
|
|
3107
3202
|
choicesDiv.className = "user-question-choices";
|
|
3108
3203
|
const inputType = allowMultiple ? "checkbox" : "radio";
|
|
3109
3204
|
const inputName = `uq-${Date.now()}`;
|
|
3205
|
+
let customInput = null;
|
|
3206
|
+
let isCustomSelected = false;
|
|
3207
|
+
const submitBtn = document.createElement("button");
|
|
3208
|
+
submitBtn.className = "user-question-btn user-question-btn-submit";
|
|
3209
|
+
submitBtn.textContent = "Submit";
|
|
3210
|
+
submitBtn.disabled = true;
|
|
3211
|
+
const updateSubmitButton = () => {
|
|
3212
|
+
const hasSelection = selected.size > 0;
|
|
3213
|
+
const hasCustomValue = isCustomSelected && customInput && customInput.value.trim().length > 0;
|
|
3214
|
+
submitBtn.disabled = !hasSelection && !hasCustomValue;
|
|
3215
|
+
};
|
|
3110
3216
|
choices.forEach((choice, index) => {
|
|
3111
3217
|
const choiceDiv = document.createElement("div");
|
|
3112
3218
|
choiceDiv.className = "user-question-choice";
|
|
@@ -3131,6 +3237,8 @@ var askUserExecutor = async (args2) => {
|
|
|
3131
3237
|
}
|
|
3132
3238
|
} else {
|
|
3133
3239
|
selected.clear();
|
|
3240
|
+
isCustomSelected = false;
|
|
3241
|
+
if (customInput) customInput.value = "";
|
|
3134
3242
|
selected.add(choice);
|
|
3135
3243
|
choicesDiv.querySelectorAll(".user-question-choice").forEach((c) => c.classList.remove("selected"));
|
|
3136
3244
|
choiceDiv.classList.add("selected");
|
|
@@ -3146,9 +3254,62 @@ var askUserExecutor = async (args2) => {
|
|
|
3146
3254
|
});
|
|
3147
3255
|
choicesDiv.appendChild(choiceDiv);
|
|
3148
3256
|
});
|
|
3257
|
+
if (allowCustom) {
|
|
3258
|
+
const customChoiceDiv = document.createElement("div");
|
|
3259
|
+
customChoiceDiv.className = "user-question-choice";
|
|
3260
|
+
const customRadio = document.createElement("input");
|
|
3261
|
+
customRadio.type = inputType;
|
|
3262
|
+
customRadio.name = inputName;
|
|
3263
|
+
customRadio.id = `${inputName}-custom`;
|
|
3264
|
+
customRadio.value = "__custom__";
|
|
3265
|
+
const customLabel = document.createElement("label");
|
|
3266
|
+
customLabel.htmlFor = customRadio.id;
|
|
3267
|
+
customLabel.textContent = "Other:";
|
|
3268
|
+
customLabel.style.flexShrink = "0";
|
|
3269
|
+
customInput = document.createElement("input");
|
|
3270
|
+
customInput.type = "text";
|
|
3271
|
+
customInput.className = "user-question-custom-input";
|
|
3272
|
+
customInput.placeholder = "Type your answer...";
|
|
3273
|
+
customInput.style.marginTop = "0";
|
|
3274
|
+
customInput.style.marginLeft = "8px";
|
|
3275
|
+
customInput.style.flex = "1";
|
|
3276
|
+
customChoiceDiv.appendChild(customRadio);
|
|
3277
|
+
customChoiceDiv.appendChild(customLabel);
|
|
3278
|
+
customChoiceDiv.appendChild(customInput);
|
|
3279
|
+
const handleCustomSelect = () => {
|
|
3280
|
+
if (!allowMultiple) {
|
|
3281
|
+
selected.clear();
|
|
3282
|
+
choicesDiv.querySelectorAll(".user-question-choice").forEach((c) => c.classList.remove("selected"));
|
|
3283
|
+
}
|
|
3284
|
+
isCustomSelected = true;
|
|
3285
|
+
customChoiceDiv.classList.add("selected");
|
|
3286
|
+
customInput?.focus();
|
|
3287
|
+
updateSubmitButton();
|
|
3288
|
+
};
|
|
3289
|
+
customRadio.addEventListener("change", handleCustomSelect);
|
|
3290
|
+
customChoiceDiv.addEventListener("click", (e) => {
|
|
3291
|
+
if (e.target !== customRadio && e.target !== customInput) {
|
|
3292
|
+
customRadio.checked = true;
|
|
3293
|
+
handleCustomSelect();
|
|
3294
|
+
}
|
|
3295
|
+
});
|
|
3296
|
+
customInput.addEventListener("focus", () => {
|
|
3297
|
+
if (!customRadio.checked) {
|
|
3298
|
+
customRadio.checked = true;
|
|
3299
|
+
handleCustomSelect();
|
|
3300
|
+
}
|
|
3301
|
+
});
|
|
3302
|
+
customInput.addEventListener("input", updateSubmitButton);
|
|
3303
|
+
choicesDiv.appendChild(customChoiceDiv);
|
|
3304
|
+
}
|
|
3149
3305
|
modal.appendChild(choicesDiv);
|
|
3150
3306
|
const actions = document.createElement("div");
|
|
3151
3307
|
actions.className = "user-question-actions";
|
|
3308
|
+
overlay.appendChild(modal);
|
|
3309
|
+
document.body.appendChild(overlay);
|
|
3310
|
+
const cleanup = () => {
|
|
3311
|
+
overlay.remove();
|
|
3312
|
+
};
|
|
3152
3313
|
const cancelBtn = document.createElement("button");
|
|
3153
3314
|
cancelBtn.className = "user-question-btn user-question-btn-cancel";
|
|
3154
3315
|
cancelBtn.textContent = "Skip";
|
|
@@ -3160,29 +3321,21 @@ var askUserExecutor = async (args2) => {
|
|
|
3160
3321
|
answered: false
|
|
3161
3322
|
});
|
|
3162
3323
|
};
|
|
3163
|
-
const submitBtn = document.createElement("button");
|
|
3164
|
-
submitBtn.className = "user-question-btn user-question-btn-submit";
|
|
3165
|
-
submitBtn.textContent = "Submit";
|
|
3166
|
-
submitBtn.disabled = true;
|
|
3167
3324
|
submitBtn.onclick = () => {
|
|
3168
3325
|
cleanup();
|
|
3326
|
+
const results = Array.from(selected);
|
|
3327
|
+
if (isCustomSelected && customInput && customInput.value.trim()) {
|
|
3328
|
+
results.push(customInput.value.trim());
|
|
3329
|
+
}
|
|
3169
3330
|
resolve({
|
|
3170
3331
|
question,
|
|
3171
|
-
selectedChoices:
|
|
3332
|
+
selectedChoices: results,
|
|
3172
3333
|
answered: true
|
|
3173
3334
|
});
|
|
3174
3335
|
};
|
|
3175
|
-
const updateSubmitButton = () => {
|
|
3176
|
-
submitBtn.disabled = selected.size === 0;
|
|
3177
|
-
};
|
|
3178
3336
|
actions.appendChild(cancelBtn);
|
|
3179
3337
|
actions.appendChild(submitBtn);
|
|
3180
3338
|
modal.appendChild(actions);
|
|
3181
|
-
overlay.appendChild(modal);
|
|
3182
|
-
document.body.appendChild(overlay);
|
|
3183
|
-
const cleanup = () => {
|
|
3184
|
-
overlay.remove();
|
|
3185
|
-
};
|
|
3186
3339
|
const handleEscape = (e) => {
|
|
3187
3340
|
if (e.key === "Escape") {
|
|
3188
3341
|
document.removeEventListener("keydown", handleEscape);
|
|
@@ -3351,6 +3504,11 @@ var screenshotTool = {
|
|
|
3351
3504
|
|
|
3352
3505
|
The screenshot captures the visible viewport of the page. The image is uploaded to the server and a permanent URL is returned.
|
|
3353
3506
|
|
|
3507
|
+
IMPORTANT: Before taking a screenshot, use the ask_user tool (if available) to ask which size they prefer:
|
|
3508
|
+
- "full" (100%) - highest quality, larger file
|
|
3509
|
+
- "half" (50%) - good balance of quality and size
|
|
3510
|
+
- "quarter" (25%) - smallest file, faster upload
|
|
3511
|
+
|
|
3354
3512
|
Use this when:
|
|
3355
3513
|
- User asks to see what's on their screen
|
|
3356
3514
|
- You need to analyze the current page visually
|
|
@@ -3361,12 +3519,24 @@ Use this when:
|
|
|
3361
3519
|
selector: {
|
|
3362
3520
|
type: "string",
|
|
3363
3521
|
description: "Optional CSS selector to capture a specific element instead of the full page. Leave empty for full page screenshot."
|
|
3522
|
+
},
|
|
3523
|
+
size: {
|
|
3524
|
+
type: "string",
|
|
3525
|
+
enum: ["full", "half", "quarter"],
|
|
3526
|
+
description: 'Image size: "full" (100%), "half" (50%), or "quarter" (25%). Ask the user which size they prefer before capturing.'
|
|
3364
3527
|
}
|
|
3365
3528
|
}
|
|
3366
3529
|
}
|
|
3367
3530
|
};
|
|
3368
3531
|
var screenshotExecutor = async (args2) => {
|
|
3369
3532
|
const selector = args2.selector;
|
|
3533
|
+
const size = args2.size || "full";
|
|
3534
|
+
const scaleMap = {
|
|
3535
|
+
full: 1,
|
|
3536
|
+
half: 0.5,
|
|
3537
|
+
quarter: 0.25
|
|
3538
|
+
};
|
|
3539
|
+
const scale = scaleMap[size] || 1;
|
|
3370
3540
|
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
3371
3541
|
return {
|
|
3372
3542
|
success: false,
|
|
@@ -3387,34 +3557,41 @@ var screenshotExecutor = async (args2) => {
|
|
|
3387
3557
|
if (!target) {
|
|
3388
3558
|
return { success: false, error: "Element not found: " + selector };
|
|
3389
3559
|
}
|
|
3390
|
-
const
|
|
3560
|
+
const fullCanvas = await window.html2canvas(target, {
|
|
3391
3561
|
useCORS: true,
|
|
3392
3562
|
allowTaint: true,
|
|
3393
3563
|
backgroundColor: "#000000",
|
|
3394
3564
|
scale: 1
|
|
3395
3565
|
});
|
|
3566
|
+
let finalCanvas = fullCanvas;
|
|
3567
|
+
if (scale < 1) {
|
|
3568
|
+
const resizedCanvas = document.createElement("canvas");
|
|
3569
|
+
resizedCanvas.width = Math.round(fullCanvas.width * scale);
|
|
3570
|
+
resizedCanvas.height = Math.round(fullCanvas.height * scale);
|
|
3571
|
+
const ctx = resizedCanvas.getContext("2d");
|
|
3572
|
+
if (ctx) {
|
|
3573
|
+
ctx.drawImage(fullCanvas, 0, 0, resizedCanvas.width, resizedCanvas.height);
|
|
3574
|
+
finalCanvas = resizedCanvas;
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3396
3577
|
const blob = await new Promise((resolve) => {
|
|
3397
|
-
|
|
3578
|
+
finalCanvas.toBlob(resolve, "image/png", 0.9);
|
|
3398
3579
|
});
|
|
3399
3580
|
if (!blob) {
|
|
3400
3581
|
return { success: false, error: "Failed to create image blob" };
|
|
3401
3582
|
}
|
|
3402
|
-
const
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
method: "POST",
|
|
3406
|
-
body: formData
|
|
3407
|
-
});
|
|
3408
|
-
if (!response.ok) {
|
|
3409
|
-
const errorData = await response.json().catch(() => ({}));
|
|
3410
|
-
return { success: false, error: errorData.error || "Upload failed" };
|
|
3583
|
+
const uploadFn = window.__hustleUploadFile;
|
|
3584
|
+
if (!uploadFn) {
|
|
3585
|
+
return { success: false, error: "Upload not available. Make sure HustleProvider is mounted and client is ready." };
|
|
3411
3586
|
}
|
|
3412
|
-
const
|
|
3587
|
+
const fileName = "screenshot-" + Date.now() + ".png";
|
|
3588
|
+
const attachment = await uploadFn(blob, fileName);
|
|
3589
|
+
const sizeLabel = size === "full" ? "100%" : size === "half" ? "50%" : "25%";
|
|
3413
3590
|
return {
|
|
3414
3591
|
success: true,
|
|
3415
|
-
url:
|
|
3416
|
-
contentType:
|
|
3417
|
-
message:
|
|
3592
|
+
url: attachment.url,
|
|
3593
|
+
contentType: attachment.contentType || "image/png",
|
|
3594
|
+
message: `Screenshot captured at ${sizeLabel} size (${finalCanvas.width}x${finalCanvas.height}) and uploaded successfully`
|
|
3418
3595
|
};
|
|
3419
3596
|
} catch (e) {
|
|
3420
3597
|
const err = e;
|
|
@@ -3444,6 +3621,21 @@ var buildPluginTool = {
|
|
|
3444
3621
|
name: "build_plugin",
|
|
3445
3622
|
description: `Build a Hustle plugin definition. Use this tool to construct a plugin based on user requirements.
|
|
3446
3623
|
|
|
3624
|
+
## Testing Before Building (IMPORTANT)
|
|
3625
|
+
|
|
3626
|
+
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.
|
|
3627
|
+
|
|
3628
|
+
**Workflow:**
|
|
3629
|
+
1. When a user describes what they want, first test the core logic using execute_javascript
|
|
3630
|
+
2. Iterate on the code until it works correctly
|
|
3631
|
+
3. Ask the user: "Would you like to test this further, or should I build it into a plugin?"
|
|
3632
|
+
4. Only call build_plugin once the code is validated and the user confirms
|
|
3633
|
+
|
|
3634
|
+
**Example:** If building a weather plugin, first test the API call:
|
|
3635
|
+
execute_javascript({ code: "fetch('https://api.example.com/weather?city=London').then(r => r.json()).then(console.log)" })
|
|
3636
|
+
|
|
3637
|
+
This catches errors early and lets users see results before committing to a plugin.
|
|
3638
|
+
|
|
3447
3639
|
## Plugin Structure
|
|
3448
3640
|
|
|
3449
3641
|
A plugin consists of:
|
|
@@ -3482,21 +3674,127 @@ Write them as arrow function strings that will be eval'd:
|
|
|
3482
3674
|
"async (args) => { const { city } = args; return { weather: 'sunny', city }; }"
|
|
3483
3675
|
|
|
3484
3676
|
The function receives args as Record<string, unknown> and should return the result.
|
|
3485
|
-
|
|
3677
|
+
|
|
3678
|
+
## Available in Executor Scope
|
|
3679
|
+
|
|
3680
|
+
Executors run in the browser context with full access to:
|
|
3681
|
+
|
|
3682
|
+
### Browser APIs
|
|
3683
|
+
- **fetch(url, options)** - HTTP requests (subject to CORS)
|
|
3684
|
+
- **localStorage / sessionStorage** - Persistent storage
|
|
3685
|
+
- **document** - Full DOM access (create elements, modals, forms, etc.)
|
|
3686
|
+
- **window** - Global window object
|
|
3687
|
+
- **console** - Logging (log, warn, error, etc.)
|
|
3688
|
+
- **setTimeout / setInterval / clearTimeout / clearInterval** - Timers
|
|
3689
|
+
- **JSON** - Parse and stringify
|
|
3690
|
+
- **Date** - Date/time operations
|
|
3691
|
+
- **URL / URLSearchParams** - URL manipulation
|
|
3692
|
+
- **FormData / Blob / File / FileReader** - File handling
|
|
3693
|
+
- **crypto** - Cryptographic operations (crypto.randomUUID(), etc.)
|
|
3694
|
+
- **navigator** - Browser info, clipboard, geolocation, etc.
|
|
3695
|
+
- **location** - Current URL info
|
|
3696
|
+
- **history** - Browser history navigation
|
|
3697
|
+
- **WebSocket** - Real-time bidirectional communication
|
|
3698
|
+
- **EventSource** - Server-sent events
|
|
3699
|
+
- **indexedDB** - Client-side database for large data
|
|
3700
|
+
- **Notification** - Browser notifications (requires permission)
|
|
3701
|
+
- **performance** - Performance timing
|
|
3702
|
+
- **atob / btoa** - Base64 encoding/decoding
|
|
3703
|
+
- **TextEncoder / TextDecoder** - Text encoding
|
|
3704
|
+
- **AbortController** - Cancel fetch requests
|
|
3705
|
+
- **IntersectionObserver / MutationObserver / ResizeObserver** - DOM observers
|
|
3706
|
+
- **requestAnimationFrame** - Animation timing
|
|
3707
|
+
- **speechSynthesis** - Text-to-speech
|
|
3708
|
+
- **Audio / Image / Canvas** - Media APIs
|
|
3709
|
+
|
|
3710
|
+
### Hustle Plugin System Globals
|
|
3711
|
+
- **window.__hustleInstanceId** - Current Hustle instance ID
|
|
3712
|
+
- **window.__hustleRegisterPlugin(plugin, enabled)** - Install another plugin dynamically
|
|
3713
|
+
- **window.__hustleUnregisterPlugin(name)** - Uninstall a plugin by name
|
|
3714
|
+
- **window.__hustleUploadFile(file, fileName?)** - Upload a File/Blob to the server, returns { url, contentType }
|
|
3715
|
+
- **window.__hustleListPlugins()** - List all installed plugins
|
|
3716
|
+
- **window.__hustleGetPlugin(name)** - Get a specific plugin by name
|
|
3717
|
+
|
|
3718
|
+
### DOM Manipulation Examples
|
|
3719
|
+
Create a modal: document.createElement('div'), style it, append to document.body
|
|
3720
|
+
Add event listeners: element.addEventListener('click', handler)
|
|
3721
|
+
Query elements: document.querySelector(), document.querySelectorAll()
|
|
3722
|
+
|
|
3723
|
+
### Storage Patterns
|
|
3724
|
+
Store data: localStorage.setItem('key', JSON.stringify(data))
|
|
3725
|
+
Retrieve data: JSON.parse(localStorage.getItem('key') || '{}')
|
|
3726
|
+
Namespace your keys: Use plugin name prefix like "myplugin-settings"
|
|
3727
|
+
|
|
3728
|
+
### Async Patterns
|
|
3729
|
+
All executors should be async. Use await for promises:
|
|
3730
|
+
"async (args) => { const res = await fetch(url); return await res.json(); }"
|
|
3486
3731
|
|
|
3487
3732
|
## Lifecycle Hooks (Optional)
|
|
3488
3733
|
|
|
3489
|
-
|
|
3734
|
+
Hooks also have full access to the browser scope described above.
|
|
3735
|
+
|
|
3736
|
+
- **onRegisterCode**: Called once when plugin is registered/enabled. Good for initialization.
|
|
3737
|
+
**IMPORTANT: Always log when your plugin registers so users know it's active!**
|
|
3738
|
+
Example: "async () => { console.log('[MyPlugin] v1.0.0 registered'); }"
|
|
3739
|
+
|
|
3740
|
+
- **beforeRequestCode**: Modify messages before sending. Receives request object with { messages, model, ... }. Must return the modified request.
|
|
3490
3741
|
Example: "async (req) => { req.messages = req.messages.map(m => ({...m, content: m.content.toUpperCase()})); return req; }"
|
|
3491
3742
|
|
|
3492
|
-
- **afterResponseCode**: Process response after receiving. Receives response object.
|
|
3493
|
-
Example: "async (res) => { console.log('Response:', res.content); }"
|
|
3743
|
+
- **afterResponseCode**: Process/modify response after receiving. Receives response object with { content, ... }.
|
|
3744
|
+
Example: "async (res) => { console.log('Response received:', res.content.substring(0, 100)); }"
|
|
3745
|
+
|
|
3746
|
+
- **onErrorCode**: Called on errors. Receives (error, context) where context has { phase: 'beforeRequest'|'execute'|'afterResponse' }.
|
|
3747
|
+
Example: "async (error, ctx) => { console.error('[MyPlugin] Error in', ctx.phase, ':', error.message); }"
|
|
3748
|
+
|
|
3749
|
+
## Best Practices
|
|
3494
3750
|
|
|
3495
|
-
|
|
3496
|
-
|
|
3751
|
+
1. **Always add onRegisterCode** that logs the plugin name and version
|
|
3752
|
+
2. **Namespace console logs** with [PluginName] prefix for easy identification
|
|
3753
|
+
3. **Handle errors gracefully** in executors - return { error: message } instead of throwing
|
|
3497
3754
|
|
|
3498
|
-
|
|
3499
|
-
|
|
3755
|
+
## Building Plugin UI
|
|
3756
|
+
|
|
3757
|
+
Plugins can embed custom UI elements in two ways:
|
|
3758
|
+
|
|
3759
|
+
### Persistent UI (via onRegister hook)
|
|
3760
|
+
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:
|
|
3761
|
+
|
|
3762
|
+
"async () => {
|
|
3763
|
+
console.log('[MyPlugin] v1.0.0 registered');
|
|
3764
|
+
if (document.getElementById('my-plugin-panel')) return; // Already exists
|
|
3765
|
+
|
|
3766
|
+
const panel = document.createElement('div');
|
|
3767
|
+
panel.id = 'my-plugin-panel';
|
|
3768
|
+
Object.assign(panel.style, { position: 'fixed', bottom: '20px', right: '20px', padding: '10px', background: '#333', color: '#fff' });
|
|
3769
|
+
panel.textContent = 'My Panel';
|
|
3770
|
+
document.body.appendChild(panel);
|
|
3771
|
+
}"
|
|
3772
|
+
|
|
3773
|
+
### On-Demand UI (via tool executors)
|
|
3774
|
+
Embed UI just-in-time when a tool is invoked. Useful for tools like show_clock, display_chart, etc.:
|
|
3775
|
+
|
|
3776
|
+
"async (args) => {
|
|
3777
|
+
const modal = document.createElement('div');
|
|
3778
|
+
modal.id = 'clock-modal';
|
|
3779
|
+
Object.assign(modal.style, { position: 'fixed', inset: '0', display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'rgba(0,0,0,0.5)' });
|
|
3780
|
+
modal.textContent = 'Current time: ' + new Date().toLocaleTimeString();
|
|
3781
|
+
modal.onclick = () => modal.remove(); // Click to dismiss
|
|
3782
|
+
document.body.appendChild(modal);
|
|
3783
|
+
return { success: true, message: 'Clock displayed' };
|
|
3784
|
+
}"
|
|
3785
|
+
|
|
3786
|
+
### UI Best Practices
|
|
3787
|
+
- Always use unique IDs with your plugin name prefix (e.g., "myplugin-modal")
|
|
3788
|
+
- For persistent UI, check existence in onRegister before creating
|
|
3789
|
+
- Provide a way to dismiss/close UI elements (click handler, close button)
|
|
3790
|
+
- Use Object.assign(el.style, {...}) for inline styles or inject a <style> tag
|
|
3791
|
+
- Clean up UI in tool executors if appropriate (e.g., remove after timeout)
|
|
3792
|
+
|
|
3793
|
+
## Security Notes
|
|
3794
|
+
- Code runs in browser sandbox with same-origin policy
|
|
3795
|
+
- fetch() is subject to CORS restrictions
|
|
3796
|
+
- No direct filesystem access (use File API with user interaction)
|
|
3797
|
+
- Sanitize any user-provided content before inserting into the DOM`,
|
|
3500
3798
|
parameters: {
|
|
3501
3799
|
type: "object",
|
|
3502
3800
|
properties: {
|
|
@@ -3585,6 +3883,104 @@ var installPluginTool = {
|
|
|
3585
3883
|
required: ["plugin"]
|
|
3586
3884
|
}
|
|
3587
3885
|
};
|
|
3886
|
+
var uninstallPluginTool = {
|
|
3887
|
+
name: "uninstall_plugin",
|
|
3888
|
+
description: "Uninstall a plugin by name, removing it from browser storage.",
|
|
3889
|
+
parameters: {
|
|
3890
|
+
type: "object",
|
|
3891
|
+
properties: {
|
|
3892
|
+
name: {
|
|
3893
|
+
type: "string",
|
|
3894
|
+
description: "The name of the plugin to uninstall"
|
|
3895
|
+
}
|
|
3896
|
+
},
|
|
3897
|
+
required: ["name"]
|
|
3898
|
+
}
|
|
3899
|
+
};
|
|
3900
|
+
var listPluginsTool = {
|
|
3901
|
+
name: "list_plugins",
|
|
3902
|
+
description: "List all installed plugins with their enabled/disabled status.",
|
|
3903
|
+
parameters: {
|
|
3904
|
+
type: "object",
|
|
3905
|
+
properties: {},
|
|
3906
|
+
required: []
|
|
3907
|
+
}
|
|
3908
|
+
};
|
|
3909
|
+
var modifyPluginTool = {
|
|
3910
|
+
name: "modify_plugin",
|
|
3911
|
+
description: `Modify an existing installed plugin. Can update version, description, tools, executors, and hooks.
|
|
3912
|
+
|
|
3913
|
+
Use list_plugins first to see installed plugins, then modify by name.
|
|
3914
|
+
|
|
3915
|
+
You can:
|
|
3916
|
+
- Add new tools (provide tools array with new tools to add)
|
|
3917
|
+
- Update existing tools (provide tool with same name)
|
|
3918
|
+
- Remove tools (set removeTool to the tool name)
|
|
3919
|
+
- Update hooks (provide hook code)
|
|
3920
|
+
- Update version/description
|
|
3921
|
+
|
|
3922
|
+
Example: Add a new tool to existing plugin:
|
|
3923
|
+
{
|
|
3924
|
+
"name": "my-plugin",
|
|
3925
|
+
"addTools": [{ "name": "new_tool", "description": "...", "parameters": {...} }],
|
|
3926
|
+
"addExecutorCode": { "new_tool": "async (args) => { ... }" }
|
|
3927
|
+
}`,
|
|
3928
|
+
parameters: {
|
|
3929
|
+
type: "object",
|
|
3930
|
+
properties: {
|
|
3931
|
+
name: {
|
|
3932
|
+
type: "string",
|
|
3933
|
+
description: "Name of the plugin to modify (required)"
|
|
3934
|
+
},
|
|
3935
|
+
version: {
|
|
3936
|
+
type: "string",
|
|
3937
|
+
description: "New version string"
|
|
3938
|
+
},
|
|
3939
|
+
description: {
|
|
3940
|
+
type: "string",
|
|
3941
|
+
description: "New description"
|
|
3942
|
+
},
|
|
3943
|
+
addTools: {
|
|
3944
|
+
type: "array",
|
|
3945
|
+
description: "Tools to add or update",
|
|
3946
|
+
items: {
|
|
3947
|
+
type: "object",
|
|
3948
|
+
properties: {
|
|
3949
|
+
name: { type: "string" },
|
|
3950
|
+
description: { type: "string" },
|
|
3951
|
+
parameters: { type: "object" }
|
|
3952
|
+
}
|
|
3953
|
+
}
|
|
3954
|
+
},
|
|
3955
|
+
addExecutorCode: {
|
|
3956
|
+
type: "object",
|
|
3957
|
+
description: "Executor code to add/update (tool name -> code string)"
|
|
3958
|
+
},
|
|
3959
|
+
removeTools: {
|
|
3960
|
+
type: "array",
|
|
3961
|
+
description: "Names of tools to remove",
|
|
3962
|
+
items: { type: "string" }
|
|
3963
|
+
},
|
|
3964
|
+
onRegisterCode: {
|
|
3965
|
+
type: "string",
|
|
3966
|
+
description: "New onRegister hook code"
|
|
3967
|
+
},
|
|
3968
|
+
beforeRequestCode: {
|
|
3969
|
+
type: "string",
|
|
3970
|
+
description: "New beforeRequest hook code"
|
|
3971
|
+
},
|
|
3972
|
+
afterResponseCode: {
|
|
3973
|
+
type: "string",
|
|
3974
|
+
description: "New afterResponse hook code"
|
|
3975
|
+
},
|
|
3976
|
+
onErrorCode: {
|
|
3977
|
+
type: "string",
|
|
3978
|
+
description: "New onError hook code"
|
|
3979
|
+
}
|
|
3980
|
+
},
|
|
3981
|
+
required: ["name"]
|
|
3982
|
+
}
|
|
3983
|
+
};
|
|
3588
3984
|
var buildPluginExecutor = async (args2) => {
|
|
3589
3985
|
const {
|
|
3590
3986
|
name,
|
|
@@ -3641,12 +4037,51 @@ var buildPluginExecutor = async (args2) => {
|
|
|
3641
4037
|
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.`
|
|
3642
4038
|
};
|
|
3643
4039
|
};
|
|
4040
|
+
function normalizePlugin(input) {
|
|
4041
|
+
const plugin = input;
|
|
4042
|
+
const tools = plugin.tools;
|
|
4043
|
+
const hasEmbeddedExecutors = tools?.[0]?.executorCode !== void 0;
|
|
4044
|
+
if (hasEmbeddedExecutors) {
|
|
4045
|
+
return {
|
|
4046
|
+
...plugin,
|
|
4047
|
+
enabled: plugin.enabled ?? true,
|
|
4048
|
+
installedAt: plugin.installedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
4049
|
+
};
|
|
4050
|
+
}
|
|
4051
|
+
const executorCode = plugin.executorCode;
|
|
4052
|
+
const rawTools = tools || [];
|
|
4053
|
+
const hookKeys = ["onRegisterCode", "beforeRequestCode", "afterResponseCode", "onErrorCode"];
|
|
4054
|
+
const hooksCode = {};
|
|
4055
|
+
for (const key of hookKeys) {
|
|
4056
|
+
if (executorCode?.[key]) {
|
|
4057
|
+
hooksCode[key] = executorCode[key];
|
|
4058
|
+
}
|
|
4059
|
+
if (plugin[key]) {
|
|
4060
|
+
hooksCode[key] = plugin[key];
|
|
4061
|
+
}
|
|
4062
|
+
}
|
|
4063
|
+
return {
|
|
4064
|
+
name: plugin.name,
|
|
4065
|
+
version: plugin.version,
|
|
4066
|
+
description: plugin.description,
|
|
4067
|
+
tools: rawTools.map((tool) => ({
|
|
4068
|
+
name: tool.name,
|
|
4069
|
+
description: tool.description,
|
|
4070
|
+
parameters: tool.parameters,
|
|
4071
|
+
executorCode: executorCode?.[tool.name]
|
|
4072
|
+
})),
|
|
4073
|
+
hooksCode: Object.keys(hooksCode).length > 0 ? hooksCode : void 0,
|
|
4074
|
+
enabled: true,
|
|
4075
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4076
|
+
};
|
|
4077
|
+
}
|
|
3644
4078
|
var savePluginExecutor = async (args2) => {
|
|
3645
|
-
const { plugin, filename } = args2;
|
|
4079
|
+
const { plugin: rawPlugin, filename } = args2;
|
|
3646
4080
|
if (typeof window === "undefined") {
|
|
3647
4081
|
return { success: false, error: "Cannot save files in server environment" };
|
|
3648
4082
|
}
|
|
3649
4083
|
try {
|
|
4084
|
+
const plugin = normalizePlugin(rawPlugin);
|
|
3650
4085
|
const json2 = JSON.stringify(plugin, null, 2);
|
|
3651
4086
|
const blob = new Blob([json2], { type: "application/json" });
|
|
3652
4087
|
const url = URL.createObjectURL(blob);
|
|
@@ -3669,31 +4104,23 @@ var savePluginExecutor = async (args2) => {
|
|
|
3669
4104
|
}
|
|
3670
4105
|
};
|
|
3671
4106
|
var installPluginExecutor = async (args2) => {
|
|
3672
|
-
const { plugin, enabled = true } = args2;
|
|
4107
|
+
const { plugin: rawPlugin, enabled = true } = args2;
|
|
3673
4108
|
if (typeof window === "undefined") {
|
|
3674
4109
|
return { success: false, error: "Cannot install plugins in server environment" };
|
|
3675
4110
|
}
|
|
3676
4111
|
try {
|
|
3677
|
-
const
|
|
3678
|
-
const
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
enabled,
|
|
3685
|
-
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3686
|
-
};
|
|
3687
|
-
if (existingIndex >= 0) {
|
|
3688
|
-
plugins[existingIndex] = pluginToStore;
|
|
3689
|
-
} else {
|
|
3690
|
-
plugins.push(pluginToStore);
|
|
4112
|
+
const plugin = normalizePlugin(rawPlugin);
|
|
4113
|
+
const win = window;
|
|
4114
|
+
if (!win.__hustleRegisterPlugin) {
|
|
4115
|
+
return {
|
|
4116
|
+
success: false,
|
|
4117
|
+
error: "Plugin registration not available. Make sure HustleProvider is mounted."
|
|
4118
|
+
};
|
|
3691
4119
|
}
|
|
3692
|
-
|
|
4120
|
+
await win.__hustleRegisterPlugin(plugin, enabled);
|
|
3693
4121
|
return {
|
|
3694
4122
|
success: true,
|
|
3695
|
-
message: `Plugin "${plugin.name}" installed${enabled ? " and enabled" : ""}
|
|
3696
|
-
action: existingIndex >= 0 ? "updated" : "installed"
|
|
4123
|
+
message: `Plugin "${plugin.name}" installed${enabled ? " and enabled" : ""}.`
|
|
3697
4124
|
};
|
|
3698
4125
|
} catch (error2) {
|
|
3699
4126
|
return {
|
|
@@ -3702,15 +4129,168 @@ var installPluginExecutor = async (args2) => {
|
|
|
3702
4129
|
};
|
|
3703
4130
|
}
|
|
3704
4131
|
};
|
|
4132
|
+
var uninstallPluginExecutor = async (args2) => {
|
|
4133
|
+
const { name } = args2;
|
|
4134
|
+
if (typeof window === "undefined") {
|
|
4135
|
+
return { success: false, error: "Cannot uninstall plugins in server environment" };
|
|
4136
|
+
}
|
|
4137
|
+
try {
|
|
4138
|
+
const win = window;
|
|
4139
|
+
if (!win.__hustleUnregisterPlugin) {
|
|
4140
|
+
return {
|
|
4141
|
+
success: false,
|
|
4142
|
+
error: "Plugin unregistration not available. Make sure HustleProvider is mounted."
|
|
4143
|
+
};
|
|
4144
|
+
}
|
|
4145
|
+
await win.__hustleUnregisterPlugin(name);
|
|
4146
|
+
return {
|
|
4147
|
+
success: true,
|
|
4148
|
+
message: `Plugin "${name}" has been uninstalled.`
|
|
4149
|
+
};
|
|
4150
|
+
} catch (error2) {
|
|
4151
|
+
return {
|
|
4152
|
+
success: false,
|
|
4153
|
+
error: `Failed to uninstall: ${error2 instanceof Error ? error2.message : "Unknown error"}`
|
|
4154
|
+
};
|
|
4155
|
+
}
|
|
4156
|
+
};
|
|
4157
|
+
var listPluginsExecutor = async () => {
|
|
4158
|
+
if (typeof window === "undefined") {
|
|
4159
|
+
return { success: false, error: "Cannot list plugins in server environment" };
|
|
4160
|
+
}
|
|
4161
|
+
try {
|
|
4162
|
+
const win = window;
|
|
4163
|
+
if (!win.__hustleListPlugins) {
|
|
4164
|
+
return {
|
|
4165
|
+
success: false,
|
|
4166
|
+
error: "Plugin listing not available. Make sure HustleProvider is mounted."
|
|
4167
|
+
};
|
|
4168
|
+
}
|
|
4169
|
+
const plugins = win.__hustleListPlugins();
|
|
4170
|
+
return {
|
|
4171
|
+
success: true,
|
|
4172
|
+
plugins,
|
|
4173
|
+
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(", ")}`
|
|
4174
|
+
};
|
|
4175
|
+
} catch (error2) {
|
|
4176
|
+
return {
|
|
4177
|
+
success: false,
|
|
4178
|
+
error: `Failed to list plugins: ${error2 instanceof Error ? error2.message : "Unknown error"}`
|
|
4179
|
+
};
|
|
4180
|
+
}
|
|
4181
|
+
};
|
|
4182
|
+
var modifyPluginExecutor = async (args2) => {
|
|
4183
|
+
const {
|
|
4184
|
+
name,
|
|
4185
|
+
version,
|
|
4186
|
+
description,
|
|
4187
|
+
addTools,
|
|
4188
|
+
addExecutorCode,
|
|
4189
|
+
removeTools,
|
|
4190
|
+
onRegisterCode,
|
|
4191
|
+
beforeRequestCode,
|
|
4192
|
+
afterResponseCode,
|
|
4193
|
+
onErrorCode
|
|
4194
|
+
} = args2;
|
|
4195
|
+
if (typeof window === "undefined") {
|
|
4196
|
+
return { success: false, error: "Cannot modify plugins in server environment" };
|
|
4197
|
+
}
|
|
4198
|
+
try {
|
|
4199
|
+
const win = window;
|
|
4200
|
+
if (!win.__hustleGetPlugin || !win.__hustleRegisterPlugin) {
|
|
4201
|
+
return {
|
|
4202
|
+
success: false,
|
|
4203
|
+
error: "Plugin modification not available. Make sure HustleProvider is mounted."
|
|
4204
|
+
};
|
|
4205
|
+
}
|
|
4206
|
+
const existing = win.__hustleGetPlugin(name);
|
|
4207
|
+
if (!existing) {
|
|
4208
|
+
return {
|
|
4209
|
+
success: false,
|
|
4210
|
+
error: `Plugin "${name}" not found. Use list_plugins to see installed plugins.`
|
|
4211
|
+
};
|
|
4212
|
+
}
|
|
4213
|
+
let tools = existing.tools ? [...existing.tools] : [];
|
|
4214
|
+
if (removeTools && removeTools.length > 0) {
|
|
4215
|
+
tools = tools.filter((t) => !removeTools.includes(t.name));
|
|
4216
|
+
}
|
|
4217
|
+
if (addTools && addTools.length > 0) {
|
|
4218
|
+
for (const newTool of addTools) {
|
|
4219
|
+
const existingIndex = tools.findIndex((t) => t.name === newTool.name);
|
|
4220
|
+
const toolWithExecutor = {
|
|
4221
|
+
...newTool,
|
|
4222
|
+
executorCode: addExecutorCode?.[newTool.name] || tools.find((t) => t.name === newTool.name)?.executorCode
|
|
4223
|
+
};
|
|
4224
|
+
if (existingIndex >= 0) {
|
|
4225
|
+
tools[existingIndex] = toolWithExecutor;
|
|
4226
|
+
} else {
|
|
4227
|
+
tools.push(toolWithExecutor);
|
|
4228
|
+
}
|
|
4229
|
+
}
|
|
4230
|
+
}
|
|
4231
|
+
if (addExecutorCode) {
|
|
4232
|
+
for (const [toolName, code2] of Object.entries(addExecutorCode)) {
|
|
4233
|
+
const tool = tools.find((t) => t.name === toolName);
|
|
4234
|
+
if (tool) {
|
|
4235
|
+
tool.executorCode = code2;
|
|
4236
|
+
}
|
|
4237
|
+
}
|
|
4238
|
+
}
|
|
4239
|
+
const modified = {
|
|
4240
|
+
...existing,
|
|
4241
|
+
tools
|
|
4242
|
+
};
|
|
4243
|
+
if (version) modified.version = version;
|
|
4244
|
+
if (description) modified.description = description;
|
|
4245
|
+
if (onRegisterCode || beforeRequestCode || afterResponseCode || onErrorCode) {
|
|
4246
|
+
modified.hooksCode = modified.hooksCode || {};
|
|
4247
|
+
if (onRegisterCode) modified.hooksCode.onRegisterCode = onRegisterCode;
|
|
4248
|
+
if (beforeRequestCode) modified.hooksCode.beforeRequestCode = beforeRequestCode;
|
|
4249
|
+
if (afterResponseCode) modified.hooksCode.afterResponseCode = afterResponseCode;
|
|
4250
|
+
if (onErrorCode) modified.hooksCode.onErrorCode = onErrorCode;
|
|
4251
|
+
}
|
|
4252
|
+
const unregisterFn = window.__hustleUnregisterPlugin;
|
|
4253
|
+
if (unregisterFn) {
|
|
4254
|
+
await unregisterFn(name);
|
|
4255
|
+
}
|
|
4256
|
+
await win.__hustleRegisterPlugin(modified, existing.enabled);
|
|
4257
|
+
const changes = [];
|
|
4258
|
+
if (version) changes.push(`version \u2192 ${version}`);
|
|
4259
|
+
if (description) changes.push("description updated");
|
|
4260
|
+
if (removeTools?.length) changes.push(`removed ${removeTools.length} tool(s)`);
|
|
4261
|
+
if (addTools?.length) changes.push(`added/updated ${addTools.length} tool(s)`);
|
|
4262
|
+
if (onRegisterCode) changes.push("onRegister hook updated");
|
|
4263
|
+
if (beforeRequestCode) changes.push("beforeRequest hook updated");
|
|
4264
|
+
if (afterResponseCode) changes.push("afterResponse hook updated");
|
|
4265
|
+
if (onErrorCode) changes.push("onError hook updated");
|
|
4266
|
+
return {
|
|
4267
|
+
success: true,
|
|
4268
|
+
message: `Plugin "${name}" modified: ${changes.join(", ")}`,
|
|
4269
|
+
plugin: {
|
|
4270
|
+
name: modified.name,
|
|
4271
|
+
version: modified.version,
|
|
4272
|
+
toolCount: tools.length
|
|
4273
|
+
}
|
|
4274
|
+
};
|
|
4275
|
+
} catch (error2) {
|
|
4276
|
+
return {
|
|
4277
|
+
success: false,
|
|
4278
|
+
error: `Failed to modify plugin: ${error2 instanceof Error ? error2.message : "Unknown error"}`
|
|
4279
|
+
};
|
|
4280
|
+
}
|
|
4281
|
+
};
|
|
3705
4282
|
var pluginBuilderPlugin = {
|
|
3706
4283
|
name: "plugin-builder",
|
|
3707
|
-
version: "1.
|
|
4284
|
+
version: "1.2.0",
|
|
3708
4285
|
description: "Build custom plugins through conversation",
|
|
3709
|
-
tools: [buildPluginTool, savePluginTool, installPluginTool],
|
|
4286
|
+
tools: [buildPluginTool, savePluginTool, installPluginTool, uninstallPluginTool, listPluginsTool, modifyPluginTool],
|
|
3710
4287
|
executors: {
|
|
3711
4288
|
build_plugin: buildPluginExecutor,
|
|
3712
4289
|
save_plugin: savePluginExecutor,
|
|
3713
|
-
install_plugin: installPluginExecutor
|
|
4290
|
+
install_plugin: installPluginExecutor,
|
|
4291
|
+
uninstall_plugin: uninstallPluginExecutor,
|
|
4292
|
+
list_plugins: listPluginsExecutor,
|
|
4293
|
+
modify_plugin: modifyPluginExecutor
|
|
3714
4294
|
},
|
|
3715
4295
|
hooks: {
|
|
3716
4296
|
onRegister: () => {
|