@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/plugins/index.cjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
'use client';
|
|
1
2
|
'use strict';
|
|
2
3
|
|
|
3
4
|
// src/plugins/predictionMarket.ts
|
|
@@ -678,12 +679,16 @@ function ensureModalStyles() {
|
|
|
678
679
|
left: 0;
|
|
679
680
|
right: 0;
|
|
680
681
|
bottom: 0;
|
|
681
|
-
background:
|
|
682
|
+
background: transparent;
|
|
682
683
|
display: flex;
|
|
683
684
|
align-items: center;
|
|
684
685
|
justify-content: center;
|
|
685
686
|
z-index: 10000;
|
|
686
|
-
|
|
687
|
+
pointer-events: none;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
.user-question-modal {
|
|
691
|
+
pointer-events: auto;
|
|
687
692
|
}
|
|
688
693
|
|
|
689
694
|
@keyframes uqFadeIn {
|
|
@@ -796,6 +801,27 @@ function ensureModalStyles() {
|
|
|
796
801
|
color: #666;
|
|
797
802
|
cursor: not-allowed;
|
|
798
803
|
}
|
|
804
|
+
|
|
805
|
+
.user-question-custom-input {
|
|
806
|
+
width: 100%;
|
|
807
|
+
padding: 8px 12px;
|
|
808
|
+
margin-top: 8px;
|
|
809
|
+
background: #1a1a2e;
|
|
810
|
+
border: 1px solid #444;
|
|
811
|
+
border-radius: 6px;
|
|
812
|
+
color: #e0e0e0;
|
|
813
|
+
font-size: 14px;
|
|
814
|
+
box-sizing: border-box;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
.user-question-custom-input:focus {
|
|
818
|
+
outline: none;
|
|
819
|
+
border-color: #4a7aff;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
.user-question-custom-input::placeholder {
|
|
823
|
+
color: #666;
|
|
824
|
+
}
|
|
799
825
|
`;
|
|
800
826
|
document.head.appendChild(styles);
|
|
801
827
|
}
|
|
@@ -817,13 +843,17 @@ var askUserTool = {
|
|
|
817
843
|
allowMultiple: {
|
|
818
844
|
type: "boolean",
|
|
819
845
|
description: "If true, user can select multiple choices. Default: false"
|
|
846
|
+
},
|
|
847
|
+
allowCustom: {
|
|
848
|
+
type: "boolean",
|
|
849
|
+
description: 'If true, adds an "Other" option where user can type a custom response. Default: false'
|
|
820
850
|
}
|
|
821
851
|
},
|
|
822
852
|
required: ["question", "choices"]
|
|
823
853
|
}
|
|
824
854
|
};
|
|
825
855
|
var askUserExecutor = async (args2) => {
|
|
826
|
-
const { question, choices, allowMultiple = false } = args2;
|
|
856
|
+
const { question, choices, allowMultiple = false, allowCustom = false } = args2;
|
|
827
857
|
if (!question || !choices || !Array.isArray(choices) || choices.length === 0) {
|
|
828
858
|
return {
|
|
829
859
|
question: question || "",
|
|
@@ -855,6 +885,17 @@ var askUserExecutor = async (args2) => {
|
|
|
855
885
|
choicesDiv.className = "user-question-choices";
|
|
856
886
|
const inputType = allowMultiple ? "checkbox" : "radio";
|
|
857
887
|
const inputName = `uq-${Date.now()}`;
|
|
888
|
+
let customInput = null;
|
|
889
|
+
let isCustomSelected = false;
|
|
890
|
+
const submitBtn = document.createElement("button");
|
|
891
|
+
submitBtn.className = "user-question-btn user-question-btn-submit";
|
|
892
|
+
submitBtn.textContent = "Submit";
|
|
893
|
+
submitBtn.disabled = true;
|
|
894
|
+
const updateSubmitButton = () => {
|
|
895
|
+
const hasSelection = selected.size > 0;
|
|
896
|
+
const hasCustomValue = isCustomSelected && customInput && customInput.value.trim().length > 0;
|
|
897
|
+
submitBtn.disabled = !hasSelection && !hasCustomValue;
|
|
898
|
+
};
|
|
858
899
|
choices.forEach((choice, index) => {
|
|
859
900
|
const choiceDiv = document.createElement("div");
|
|
860
901
|
choiceDiv.className = "user-question-choice";
|
|
@@ -879,6 +920,8 @@ var askUserExecutor = async (args2) => {
|
|
|
879
920
|
}
|
|
880
921
|
} else {
|
|
881
922
|
selected.clear();
|
|
923
|
+
isCustomSelected = false;
|
|
924
|
+
if (customInput) customInput.value = "";
|
|
882
925
|
selected.add(choice);
|
|
883
926
|
choicesDiv.querySelectorAll(".user-question-choice").forEach((c) => c.classList.remove("selected"));
|
|
884
927
|
choiceDiv.classList.add("selected");
|
|
@@ -894,9 +937,62 @@ var askUserExecutor = async (args2) => {
|
|
|
894
937
|
});
|
|
895
938
|
choicesDiv.appendChild(choiceDiv);
|
|
896
939
|
});
|
|
940
|
+
if (allowCustom) {
|
|
941
|
+
const customChoiceDiv = document.createElement("div");
|
|
942
|
+
customChoiceDiv.className = "user-question-choice";
|
|
943
|
+
const customRadio = document.createElement("input");
|
|
944
|
+
customRadio.type = inputType;
|
|
945
|
+
customRadio.name = inputName;
|
|
946
|
+
customRadio.id = `${inputName}-custom`;
|
|
947
|
+
customRadio.value = "__custom__";
|
|
948
|
+
const customLabel = document.createElement("label");
|
|
949
|
+
customLabel.htmlFor = customRadio.id;
|
|
950
|
+
customLabel.textContent = "Other:";
|
|
951
|
+
customLabel.style.flexShrink = "0";
|
|
952
|
+
customInput = document.createElement("input");
|
|
953
|
+
customInput.type = "text";
|
|
954
|
+
customInput.className = "user-question-custom-input";
|
|
955
|
+
customInput.placeholder = "Type your answer...";
|
|
956
|
+
customInput.style.marginTop = "0";
|
|
957
|
+
customInput.style.marginLeft = "8px";
|
|
958
|
+
customInput.style.flex = "1";
|
|
959
|
+
customChoiceDiv.appendChild(customRadio);
|
|
960
|
+
customChoiceDiv.appendChild(customLabel);
|
|
961
|
+
customChoiceDiv.appendChild(customInput);
|
|
962
|
+
const handleCustomSelect = () => {
|
|
963
|
+
if (!allowMultiple) {
|
|
964
|
+
selected.clear();
|
|
965
|
+
choicesDiv.querySelectorAll(".user-question-choice").forEach((c) => c.classList.remove("selected"));
|
|
966
|
+
}
|
|
967
|
+
isCustomSelected = true;
|
|
968
|
+
customChoiceDiv.classList.add("selected");
|
|
969
|
+
customInput?.focus();
|
|
970
|
+
updateSubmitButton();
|
|
971
|
+
};
|
|
972
|
+
customRadio.addEventListener("change", handleCustomSelect);
|
|
973
|
+
customChoiceDiv.addEventListener("click", (e) => {
|
|
974
|
+
if (e.target !== customRadio && e.target !== customInput) {
|
|
975
|
+
customRadio.checked = true;
|
|
976
|
+
handleCustomSelect();
|
|
977
|
+
}
|
|
978
|
+
});
|
|
979
|
+
customInput.addEventListener("focus", () => {
|
|
980
|
+
if (!customRadio.checked) {
|
|
981
|
+
customRadio.checked = true;
|
|
982
|
+
handleCustomSelect();
|
|
983
|
+
}
|
|
984
|
+
});
|
|
985
|
+
customInput.addEventListener("input", updateSubmitButton);
|
|
986
|
+
choicesDiv.appendChild(customChoiceDiv);
|
|
987
|
+
}
|
|
897
988
|
modal.appendChild(choicesDiv);
|
|
898
989
|
const actions = document.createElement("div");
|
|
899
990
|
actions.className = "user-question-actions";
|
|
991
|
+
overlay.appendChild(modal);
|
|
992
|
+
document.body.appendChild(overlay);
|
|
993
|
+
const cleanup = () => {
|
|
994
|
+
overlay.remove();
|
|
995
|
+
};
|
|
900
996
|
const cancelBtn = document.createElement("button");
|
|
901
997
|
cancelBtn.className = "user-question-btn user-question-btn-cancel";
|
|
902
998
|
cancelBtn.textContent = "Skip";
|
|
@@ -908,29 +1004,21 @@ var askUserExecutor = async (args2) => {
|
|
|
908
1004
|
answered: false
|
|
909
1005
|
});
|
|
910
1006
|
};
|
|
911
|
-
const submitBtn = document.createElement("button");
|
|
912
|
-
submitBtn.className = "user-question-btn user-question-btn-submit";
|
|
913
|
-
submitBtn.textContent = "Submit";
|
|
914
|
-
submitBtn.disabled = true;
|
|
915
1007
|
submitBtn.onclick = () => {
|
|
916
1008
|
cleanup();
|
|
1009
|
+
const results = Array.from(selected);
|
|
1010
|
+
if (isCustomSelected && customInput && customInput.value.trim()) {
|
|
1011
|
+
results.push(customInput.value.trim());
|
|
1012
|
+
}
|
|
917
1013
|
resolve({
|
|
918
1014
|
question,
|
|
919
|
-
selectedChoices:
|
|
1015
|
+
selectedChoices: results,
|
|
920
1016
|
answered: true
|
|
921
1017
|
});
|
|
922
1018
|
};
|
|
923
|
-
const updateSubmitButton = () => {
|
|
924
|
-
submitBtn.disabled = selected.size === 0;
|
|
925
|
-
};
|
|
926
1019
|
actions.appendChild(cancelBtn);
|
|
927
1020
|
actions.appendChild(submitBtn);
|
|
928
1021
|
modal.appendChild(actions);
|
|
929
|
-
overlay.appendChild(modal);
|
|
930
|
-
document.body.appendChild(overlay);
|
|
931
|
-
const cleanup = () => {
|
|
932
|
-
overlay.remove();
|
|
933
|
-
};
|
|
934
1022
|
const handleEscape = (e) => {
|
|
935
1023
|
if (e.key === "Escape") {
|
|
936
1024
|
document.removeEventListener("keydown", handleEscape);
|
|
@@ -1099,6 +1187,11 @@ var screenshotTool = {
|
|
|
1099
1187
|
|
|
1100
1188
|
The screenshot captures the visible viewport of the page. The image is uploaded to the server and a permanent URL is returned.
|
|
1101
1189
|
|
|
1190
|
+
IMPORTANT: Before taking a screenshot, use the ask_user tool (if available) to ask which size they prefer:
|
|
1191
|
+
- "full" (100%) - highest quality, larger file
|
|
1192
|
+
- "half" (50%) - good balance of quality and size
|
|
1193
|
+
- "quarter" (25%) - smallest file, faster upload
|
|
1194
|
+
|
|
1102
1195
|
Use this when:
|
|
1103
1196
|
- User asks to see what's on their screen
|
|
1104
1197
|
- You need to analyze the current page visually
|
|
@@ -1109,12 +1202,24 @@ Use this when:
|
|
|
1109
1202
|
selector: {
|
|
1110
1203
|
type: "string",
|
|
1111
1204
|
description: "Optional CSS selector to capture a specific element instead of the full page. Leave empty for full page screenshot."
|
|
1205
|
+
},
|
|
1206
|
+
size: {
|
|
1207
|
+
type: "string",
|
|
1208
|
+
enum: ["full", "half", "quarter"],
|
|
1209
|
+
description: 'Image size: "full" (100%), "half" (50%), or "quarter" (25%). Ask the user which size they prefer before capturing.'
|
|
1112
1210
|
}
|
|
1113
1211
|
}
|
|
1114
1212
|
}
|
|
1115
1213
|
};
|
|
1116
1214
|
var screenshotExecutor = async (args2) => {
|
|
1117
1215
|
const selector = args2.selector;
|
|
1216
|
+
const size = args2.size || "full";
|
|
1217
|
+
const scaleMap = {
|
|
1218
|
+
full: 1,
|
|
1219
|
+
half: 0.5,
|
|
1220
|
+
quarter: 0.25
|
|
1221
|
+
};
|
|
1222
|
+
const scale = scaleMap[size] || 1;
|
|
1118
1223
|
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
1119
1224
|
return {
|
|
1120
1225
|
success: false,
|
|
@@ -1135,34 +1240,41 @@ var screenshotExecutor = async (args2) => {
|
|
|
1135
1240
|
if (!target) {
|
|
1136
1241
|
return { success: false, error: "Element not found: " + selector };
|
|
1137
1242
|
}
|
|
1138
|
-
const
|
|
1243
|
+
const fullCanvas = await window.html2canvas(target, {
|
|
1139
1244
|
useCORS: true,
|
|
1140
1245
|
allowTaint: true,
|
|
1141
1246
|
backgroundColor: "#000000",
|
|
1142
1247
|
scale: 1
|
|
1143
1248
|
});
|
|
1249
|
+
let finalCanvas = fullCanvas;
|
|
1250
|
+
if (scale < 1) {
|
|
1251
|
+
const resizedCanvas = document.createElement("canvas");
|
|
1252
|
+
resizedCanvas.width = Math.round(fullCanvas.width * scale);
|
|
1253
|
+
resizedCanvas.height = Math.round(fullCanvas.height * scale);
|
|
1254
|
+
const ctx = resizedCanvas.getContext("2d");
|
|
1255
|
+
if (ctx) {
|
|
1256
|
+
ctx.drawImage(fullCanvas, 0, 0, resizedCanvas.width, resizedCanvas.height);
|
|
1257
|
+
finalCanvas = resizedCanvas;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1144
1260
|
const blob = await new Promise((resolve) => {
|
|
1145
|
-
|
|
1261
|
+
finalCanvas.toBlob(resolve, "image/png", 0.9);
|
|
1146
1262
|
});
|
|
1147
1263
|
if (!blob) {
|
|
1148
1264
|
return { success: false, error: "Failed to create image blob" };
|
|
1149
1265
|
}
|
|
1150
|
-
const
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
method: "POST",
|
|
1154
|
-
body: formData
|
|
1155
|
-
});
|
|
1156
|
-
if (!response.ok) {
|
|
1157
|
-
const errorData = await response.json().catch(() => ({}));
|
|
1158
|
-
return { success: false, error: errorData.error || "Upload failed" };
|
|
1266
|
+
const uploadFn = window.__hustleUploadFile;
|
|
1267
|
+
if (!uploadFn) {
|
|
1268
|
+
return { success: false, error: "Upload not available. Make sure HustleProvider is mounted and client is ready." };
|
|
1159
1269
|
}
|
|
1160
|
-
const
|
|
1270
|
+
const fileName = "screenshot-" + Date.now() + ".png";
|
|
1271
|
+
const attachment = await uploadFn(blob, fileName);
|
|
1272
|
+
const sizeLabel = size === "full" ? "100%" : size === "half" ? "50%" : "25%";
|
|
1161
1273
|
return {
|
|
1162
1274
|
success: true,
|
|
1163
|
-
url:
|
|
1164
|
-
contentType:
|
|
1165
|
-
message:
|
|
1275
|
+
url: attachment.url,
|
|
1276
|
+
contentType: attachment.contentType || "image/png",
|
|
1277
|
+
message: `Screenshot captured at ${sizeLabel} size (${finalCanvas.width}x${finalCanvas.height}) and uploaded successfully`
|
|
1166
1278
|
};
|
|
1167
1279
|
} catch (e) {
|
|
1168
1280
|
const err = e;
|
|
@@ -1192,6 +1304,21 @@ var buildPluginTool = {
|
|
|
1192
1304
|
name: "build_plugin",
|
|
1193
1305
|
description: `Build a Hustle plugin definition. Use this tool to construct a plugin based on user requirements.
|
|
1194
1306
|
|
|
1307
|
+
## Testing Before Building (IMPORTANT)
|
|
1308
|
+
|
|
1309
|
+
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.
|
|
1310
|
+
|
|
1311
|
+
**Workflow:**
|
|
1312
|
+
1. When a user describes what they want, first test the core logic using execute_javascript
|
|
1313
|
+
2. Iterate on the code until it works correctly
|
|
1314
|
+
3. Ask the user: "Would you like to test this further, or should I build it into a plugin?"
|
|
1315
|
+
4. Only call build_plugin once the code is validated and the user confirms
|
|
1316
|
+
|
|
1317
|
+
**Example:** If building a weather plugin, first test the API call:
|
|
1318
|
+
execute_javascript({ code: "fetch('https://api.example.com/weather?city=London').then(r => r.json()).then(console.log)" })
|
|
1319
|
+
|
|
1320
|
+
This catches errors early and lets users see results before committing to a plugin.
|
|
1321
|
+
|
|
1195
1322
|
## Plugin Structure
|
|
1196
1323
|
|
|
1197
1324
|
A plugin consists of:
|
|
@@ -1230,21 +1357,127 @@ Write them as arrow function strings that will be eval'd:
|
|
|
1230
1357
|
"async (args) => { const { city } = args; return { weather: 'sunny', city }; }"
|
|
1231
1358
|
|
|
1232
1359
|
The function receives args as Record<string, unknown> and should return the result.
|
|
1233
|
-
|
|
1360
|
+
|
|
1361
|
+
## Available in Executor Scope
|
|
1362
|
+
|
|
1363
|
+
Executors run in the browser context with full access to:
|
|
1364
|
+
|
|
1365
|
+
### Browser APIs
|
|
1366
|
+
- **fetch(url, options)** - HTTP requests (subject to CORS)
|
|
1367
|
+
- **localStorage / sessionStorage** - Persistent storage
|
|
1368
|
+
- **document** - Full DOM access (create elements, modals, forms, etc.)
|
|
1369
|
+
- **window** - Global window object
|
|
1370
|
+
- **console** - Logging (log, warn, error, etc.)
|
|
1371
|
+
- **setTimeout / setInterval / clearTimeout / clearInterval** - Timers
|
|
1372
|
+
- **JSON** - Parse and stringify
|
|
1373
|
+
- **Date** - Date/time operations
|
|
1374
|
+
- **URL / URLSearchParams** - URL manipulation
|
|
1375
|
+
- **FormData / Blob / File / FileReader** - File handling
|
|
1376
|
+
- **crypto** - Cryptographic operations (crypto.randomUUID(), etc.)
|
|
1377
|
+
- **navigator** - Browser info, clipboard, geolocation, etc.
|
|
1378
|
+
- **location** - Current URL info
|
|
1379
|
+
- **history** - Browser history navigation
|
|
1380
|
+
- **WebSocket** - Real-time bidirectional communication
|
|
1381
|
+
- **EventSource** - Server-sent events
|
|
1382
|
+
- **indexedDB** - Client-side database for large data
|
|
1383
|
+
- **Notification** - Browser notifications (requires permission)
|
|
1384
|
+
- **performance** - Performance timing
|
|
1385
|
+
- **atob / btoa** - Base64 encoding/decoding
|
|
1386
|
+
- **TextEncoder / TextDecoder** - Text encoding
|
|
1387
|
+
- **AbortController** - Cancel fetch requests
|
|
1388
|
+
- **IntersectionObserver / MutationObserver / ResizeObserver** - DOM observers
|
|
1389
|
+
- **requestAnimationFrame** - Animation timing
|
|
1390
|
+
- **speechSynthesis** - Text-to-speech
|
|
1391
|
+
- **Audio / Image / Canvas** - Media APIs
|
|
1392
|
+
|
|
1393
|
+
### Hustle Plugin System Globals
|
|
1394
|
+
- **window.__hustleInstanceId** - Current Hustle instance ID
|
|
1395
|
+
- **window.__hustleRegisterPlugin(plugin, enabled)** - Install another plugin dynamically
|
|
1396
|
+
- **window.__hustleUnregisterPlugin(name)** - Uninstall a plugin by name
|
|
1397
|
+
- **window.__hustleUploadFile(file, fileName?)** - Upload a File/Blob to the server, returns { url, contentType }
|
|
1398
|
+
- **window.__hustleListPlugins()** - List all installed plugins
|
|
1399
|
+
- **window.__hustleGetPlugin(name)** - Get a specific plugin by name
|
|
1400
|
+
|
|
1401
|
+
### DOM Manipulation Examples
|
|
1402
|
+
Create a modal: document.createElement('div'), style it, append to document.body
|
|
1403
|
+
Add event listeners: element.addEventListener('click', handler)
|
|
1404
|
+
Query elements: document.querySelector(), document.querySelectorAll()
|
|
1405
|
+
|
|
1406
|
+
### Storage Patterns
|
|
1407
|
+
Store data: localStorage.setItem('key', JSON.stringify(data))
|
|
1408
|
+
Retrieve data: JSON.parse(localStorage.getItem('key') || '{}')
|
|
1409
|
+
Namespace your keys: Use plugin name prefix like "myplugin-settings"
|
|
1410
|
+
|
|
1411
|
+
### Async Patterns
|
|
1412
|
+
All executors should be async. Use await for promises:
|
|
1413
|
+
"async (args) => { const res = await fetch(url); return await res.json(); }"
|
|
1234
1414
|
|
|
1235
1415
|
## Lifecycle Hooks (Optional)
|
|
1236
1416
|
|
|
1237
|
-
|
|
1417
|
+
Hooks also have full access to the browser scope described above.
|
|
1418
|
+
|
|
1419
|
+
- **onRegisterCode**: Called once when plugin is registered/enabled. Good for initialization.
|
|
1420
|
+
**IMPORTANT: Always log when your plugin registers so users know it's active!**
|
|
1421
|
+
Example: "async () => { console.log('[MyPlugin] v1.0.0 registered'); }"
|
|
1422
|
+
|
|
1423
|
+
- **beforeRequestCode**: Modify messages before sending. Receives request object with { messages, model, ... }. Must return the modified request.
|
|
1238
1424
|
Example: "async (req) => { req.messages = req.messages.map(m => ({...m, content: m.content.toUpperCase()})); return req; }"
|
|
1239
1425
|
|
|
1240
|
-
- **afterResponseCode**: Process response after receiving. Receives response object.
|
|
1241
|
-
Example: "async (res) => { console.log('Response:', res.content); }"
|
|
1426
|
+
- **afterResponseCode**: Process/modify response after receiving. Receives response object with { content, ... }.
|
|
1427
|
+
Example: "async (res) => { console.log('Response received:', res.content.substring(0, 100)); }"
|
|
1242
1428
|
|
|
1243
|
-
- **
|
|
1244
|
-
Example: "async () => { console.
|
|
1429
|
+
- **onErrorCode**: Called on errors. Receives (error, context) where context has { phase: 'beforeRequest'|'execute'|'afterResponse' }.
|
|
1430
|
+
Example: "async (error, ctx) => { console.error('[MyPlugin] Error in', ctx.phase, ':', error.message); }"
|
|
1245
1431
|
|
|
1246
|
-
|
|
1247
|
-
|
|
1432
|
+
## Best Practices
|
|
1433
|
+
|
|
1434
|
+
1. **Always add onRegisterCode** that logs the plugin name and version
|
|
1435
|
+
2. **Namespace console logs** with [PluginName] prefix for easy identification
|
|
1436
|
+
3. **Handle errors gracefully** in executors - return { error: message } instead of throwing
|
|
1437
|
+
|
|
1438
|
+
## Building Plugin UI
|
|
1439
|
+
|
|
1440
|
+
Plugins can embed custom UI elements in two ways:
|
|
1441
|
+
|
|
1442
|
+
### Persistent UI (via onRegister hook)
|
|
1443
|
+
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:
|
|
1444
|
+
|
|
1445
|
+
"async () => {
|
|
1446
|
+
console.log('[MyPlugin] v1.0.0 registered');
|
|
1447
|
+
if (document.getElementById('my-plugin-panel')) return; // Already exists
|
|
1448
|
+
|
|
1449
|
+
const panel = document.createElement('div');
|
|
1450
|
+
panel.id = 'my-plugin-panel';
|
|
1451
|
+
Object.assign(panel.style, { position: 'fixed', bottom: '20px', right: '20px', padding: '10px', background: '#333', color: '#fff' });
|
|
1452
|
+
panel.textContent = 'My Panel';
|
|
1453
|
+
document.body.appendChild(panel);
|
|
1454
|
+
}"
|
|
1455
|
+
|
|
1456
|
+
### On-Demand UI (via tool executors)
|
|
1457
|
+
Embed UI just-in-time when a tool is invoked. Useful for tools like show_clock, display_chart, etc.:
|
|
1458
|
+
|
|
1459
|
+
"async (args) => {
|
|
1460
|
+
const modal = document.createElement('div');
|
|
1461
|
+
modal.id = 'clock-modal';
|
|
1462
|
+
Object.assign(modal.style, { position: 'fixed', inset: '0', display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'rgba(0,0,0,0.5)' });
|
|
1463
|
+
modal.textContent = 'Current time: ' + new Date().toLocaleTimeString();
|
|
1464
|
+
modal.onclick = () => modal.remove(); // Click to dismiss
|
|
1465
|
+
document.body.appendChild(modal);
|
|
1466
|
+
return { success: true, message: 'Clock displayed' };
|
|
1467
|
+
}"
|
|
1468
|
+
|
|
1469
|
+
### UI Best Practices
|
|
1470
|
+
- Always use unique IDs with your plugin name prefix (e.g., "myplugin-modal")
|
|
1471
|
+
- For persistent UI, check existence in onRegister before creating
|
|
1472
|
+
- Provide a way to dismiss/close UI elements (click handler, close button)
|
|
1473
|
+
- Use Object.assign(el.style, {...}) for inline styles or inject a <style> tag
|
|
1474
|
+
- Clean up UI in tool executors if appropriate (e.g., remove after timeout)
|
|
1475
|
+
|
|
1476
|
+
## Security Notes
|
|
1477
|
+
- Code runs in browser sandbox with same-origin policy
|
|
1478
|
+
- fetch() is subject to CORS restrictions
|
|
1479
|
+
- No direct filesystem access (use File API with user interaction)
|
|
1480
|
+
- Sanitize any user-provided content before inserting into the DOM`,
|
|
1248
1481
|
parameters: {
|
|
1249
1482
|
type: "object",
|
|
1250
1483
|
properties: {
|
|
@@ -1333,6 +1566,104 @@ var installPluginTool = {
|
|
|
1333
1566
|
required: ["plugin"]
|
|
1334
1567
|
}
|
|
1335
1568
|
};
|
|
1569
|
+
var uninstallPluginTool = {
|
|
1570
|
+
name: "uninstall_plugin",
|
|
1571
|
+
description: "Uninstall a plugin by name, removing it from browser storage.",
|
|
1572
|
+
parameters: {
|
|
1573
|
+
type: "object",
|
|
1574
|
+
properties: {
|
|
1575
|
+
name: {
|
|
1576
|
+
type: "string",
|
|
1577
|
+
description: "The name of the plugin to uninstall"
|
|
1578
|
+
}
|
|
1579
|
+
},
|
|
1580
|
+
required: ["name"]
|
|
1581
|
+
}
|
|
1582
|
+
};
|
|
1583
|
+
var listPluginsTool = {
|
|
1584
|
+
name: "list_plugins",
|
|
1585
|
+
description: "List all installed plugins with their enabled/disabled status.",
|
|
1586
|
+
parameters: {
|
|
1587
|
+
type: "object",
|
|
1588
|
+
properties: {},
|
|
1589
|
+
required: []
|
|
1590
|
+
}
|
|
1591
|
+
};
|
|
1592
|
+
var modifyPluginTool = {
|
|
1593
|
+
name: "modify_plugin",
|
|
1594
|
+
description: `Modify an existing installed plugin. Can update version, description, tools, executors, and hooks.
|
|
1595
|
+
|
|
1596
|
+
Use list_plugins first to see installed plugins, then modify by name.
|
|
1597
|
+
|
|
1598
|
+
You can:
|
|
1599
|
+
- Add new tools (provide tools array with new tools to add)
|
|
1600
|
+
- Update existing tools (provide tool with same name)
|
|
1601
|
+
- Remove tools (set removeTool to the tool name)
|
|
1602
|
+
- Update hooks (provide hook code)
|
|
1603
|
+
- Update version/description
|
|
1604
|
+
|
|
1605
|
+
Example: Add a new tool to existing plugin:
|
|
1606
|
+
{
|
|
1607
|
+
"name": "my-plugin",
|
|
1608
|
+
"addTools": [{ "name": "new_tool", "description": "...", "parameters": {...} }],
|
|
1609
|
+
"addExecutorCode": { "new_tool": "async (args) => { ... }" }
|
|
1610
|
+
}`,
|
|
1611
|
+
parameters: {
|
|
1612
|
+
type: "object",
|
|
1613
|
+
properties: {
|
|
1614
|
+
name: {
|
|
1615
|
+
type: "string",
|
|
1616
|
+
description: "Name of the plugin to modify (required)"
|
|
1617
|
+
},
|
|
1618
|
+
version: {
|
|
1619
|
+
type: "string",
|
|
1620
|
+
description: "New version string"
|
|
1621
|
+
},
|
|
1622
|
+
description: {
|
|
1623
|
+
type: "string",
|
|
1624
|
+
description: "New description"
|
|
1625
|
+
},
|
|
1626
|
+
addTools: {
|
|
1627
|
+
type: "array",
|
|
1628
|
+
description: "Tools to add or update",
|
|
1629
|
+
items: {
|
|
1630
|
+
type: "object",
|
|
1631
|
+
properties: {
|
|
1632
|
+
name: { type: "string" },
|
|
1633
|
+
description: { type: "string" },
|
|
1634
|
+
parameters: { type: "object" }
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
},
|
|
1638
|
+
addExecutorCode: {
|
|
1639
|
+
type: "object",
|
|
1640
|
+
description: "Executor code to add/update (tool name -> code string)"
|
|
1641
|
+
},
|
|
1642
|
+
removeTools: {
|
|
1643
|
+
type: "array",
|
|
1644
|
+
description: "Names of tools to remove",
|
|
1645
|
+
items: { type: "string" }
|
|
1646
|
+
},
|
|
1647
|
+
onRegisterCode: {
|
|
1648
|
+
type: "string",
|
|
1649
|
+
description: "New onRegister hook code"
|
|
1650
|
+
},
|
|
1651
|
+
beforeRequestCode: {
|
|
1652
|
+
type: "string",
|
|
1653
|
+
description: "New beforeRequest hook code"
|
|
1654
|
+
},
|
|
1655
|
+
afterResponseCode: {
|
|
1656
|
+
type: "string",
|
|
1657
|
+
description: "New afterResponse hook code"
|
|
1658
|
+
},
|
|
1659
|
+
onErrorCode: {
|
|
1660
|
+
type: "string",
|
|
1661
|
+
description: "New onError hook code"
|
|
1662
|
+
}
|
|
1663
|
+
},
|
|
1664
|
+
required: ["name"]
|
|
1665
|
+
}
|
|
1666
|
+
};
|
|
1336
1667
|
var buildPluginExecutor = async (args2) => {
|
|
1337
1668
|
const {
|
|
1338
1669
|
name,
|
|
@@ -1389,12 +1720,51 @@ var buildPluginExecutor = async (args2) => {
|
|
|
1389
1720
|
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.`
|
|
1390
1721
|
};
|
|
1391
1722
|
};
|
|
1723
|
+
function normalizePlugin(input) {
|
|
1724
|
+
const plugin = input;
|
|
1725
|
+
const tools = plugin.tools;
|
|
1726
|
+
const hasEmbeddedExecutors = tools?.[0]?.executorCode !== void 0;
|
|
1727
|
+
if (hasEmbeddedExecutors) {
|
|
1728
|
+
return {
|
|
1729
|
+
...plugin,
|
|
1730
|
+
enabled: plugin.enabled ?? true,
|
|
1731
|
+
installedAt: plugin.installedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
1732
|
+
};
|
|
1733
|
+
}
|
|
1734
|
+
const executorCode = plugin.executorCode;
|
|
1735
|
+
const rawTools = tools || [];
|
|
1736
|
+
const hookKeys = ["onRegisterCode", "beforeRequestCode", "afterResponseCode", "onErrorCode"];
|
|
1737
|
+
const hooksCode = {};
|
|
1738
|
+
for (const key of hookKeys) {
|
|
1739
|
+
if (executorCode?.[key]) {
|
|
1740
|
+
hooksCode[key] = executorCode[key];
|
|
1741
|
+
}
|
|
1742
|
+
if (plugin[key]) {
|
|
1743
|
+
hooksCode[key] = plugin[key];
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
return {
|
|
1747
|
+
name: plugin.name,
|
|
1748
|
+
version: plugin.version,
|
|
1749
|
+
description: plugin.description,
|
|
1750
|
+
tools: rawTools.map((tool) => ({
|
|
1751
|
+
name: tool.name,
|
|
1752
|
+
description: tool.description,
|
|
1753
|
+
parameters: tool.parameters,
|
|
1754
|
+
executorCode: executorCode?.[tool.name]
|
|
1755
|
+
})),
|
|
1756
|
+
hooksCode: Object.keys(hooksCode).length > 0 ? hooksCode : void 0,
|
|
1757
|
+
enabled: true,
|
|
1758
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1392
1761
|
var savePluginExecutor = async (args2) => {
|
|
1393
|
-
const { plugin, filename } = args2;
|
|
1762
|
+
const { plugin: rawPlugin, filename } = args2;
|
|
1394
1763
|
if (typeof window === "undefined") {
|
|
1395
1764
|
return { success: false, error: "Cannot save files in server environment" };
|
|
1396
1765
|
}
|
|
1397
1766
|
try {
|
|
1767
|
+
const plugin = normalizePlugin(rawPlugin);
|
|
1398
1768
|
const json = JSON.stringify(plugin, null, 2);
|
|
1399
1769
|
const blob = new Blob([json], { type: "application/json" });
|
|
1400
1770
|
const url = URL.createObjectURL(blob);
|
|
@@ -1417,31 +1787,23 @@ var savePluginExecutor = async (args2) => {
|
|
|
1417
1787
|
}
|
|
1418
1788
|
};
|
|
1419
1789
|
var installPluginExecutor = async (args2) => {
|
|
1420
|
-
const { plugin, enabled = true } = args2;
|
|
1790
|
+
const { plugin: rawPlugin, enabled = true } = args2;
|
|
1421
1791
|
if (typeof window === "undefined") {
|
|
1422
1792
|
return { success: false, error: "Cannot install plugins in server environment" };
|
|
1423
1793
|
}
|
|
1424
1794
|
try {
|
|
1425
|
-
const
|
|
1426
|
-
const
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
enabled,
|
|
1433
|
-
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1434
|
-
};
|
|
1435
|
-
if (existingIndex >= 0) {
|
|
1436
|
-
plugins[existingIndex] = pluginToStore;
|
|
1437
|
-
} else {
|
|
1438
|
-
plugins.push(pluginToStore);
|
|
1795
|
+
const plugin = normalizePlugin(rawPlugin);
|
|
1796
|
+
const win = window;
|
|
1797
|
+
if (!win.__hustleRegisterPlugin) {
|
|
1798
|
+
return {
|
|
1799
|
+
success: false,
|
|
1800
|
+
error: "Plugin registration not available. Make sure HustleProvider is mounted."
|
|
1801
|
+
};
|
|
1439
1802
|
}
|
|
1440
|
-
|
|
1803
|
+
await win.__hustleRegisterPlugin(plugin, enabled);
|
|
1441
1804
|
return {
|
|
1442
1805
|
success: true,
|
|
1443
|
-
message: `Plugin "${plugin.name}" installed${enabled ? " and enabled" : ""}
|
|
1444
|
-
action: existingIndex >= 0 ? "updated" : "installed"
|
|
1806
|
+
message: `Plugin "${plugin.name}" installed${enabled ? " and enabled" : ""}.`
|
|
1445
1807
|
};
|
|
1446
1808
|
} catch (error2) {
|
|
1447
1809
|
return {
|
|
@@ -1450,15 +1812,168 @@ var installPluginExecutor = async (args2) => {
|
|
|
1450
1812
|
};
|
|
1451
1813
|
}
|
|
1452
1814
|
};
|
|
1815
|
+
var uninstallPluginExecutor = async (args2) => {
|
|
1816
|
+
const { name } = args2;
|
|
1817
|
+
if (typeof window === "undefined") {
|
|
1818
|
+
return { success: false, error: "Cannot uninstall plugins in server environment" };
|
|
1819
|
+
}
|
|
1820
|
+
try {
|
|
1821
|
+
const win = window;
|
|
1822
|
+
if (!win.__hustleUnregisterPlugin) {
|
|
1823
|
+
return {
|
|
1824
|
+
success: false,
|
|
1825
|
+
error: "Plugin unregistration not available. Make sure HustleProvider is mounted."
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
await win.__hustleUnregisterPlugin(name);
|
|
1829
|
+
return {
|
|
1830
|
+
success: true,
|
|
1831
|
+
message: `Plugin "${name}" has been uninstalled.`
|
|
1832
|
+
};
|
|
1833
|
+
} catch (error2) {
|
|
1834
|
+
return {
|
|
1835
|
+
success: false,
|
|
1836
|
+
error: `Failed to uninstall: ${error2 instanceof Error ? error2.message : "Unknown error"}`
|
|
1837
|
+
};
|
|
1838
|
+
}
|
|
1839
|
+
};
|
|
1840
|
+
var listPluginsExecutor = async () => {
|
|
1841
|
+
if (typeof window === "undefined") {
|
|
1842
|
+
return { success: false, error: "Cannot list plugins in server environment" };
|
|
1843
|
+
}
|
|
1844
|
+
try {
|
|
1845
|
+
const win = window;
|
|
1846
|
+
if (!win.__hustleListPlugins) {
|
|
1847
|
+
return {
|
|
1848
|
+
success: false,
|
|
1849
|
+
error: "Plugin listing not available. Make sure HustleProvider is mounted."
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
const plugins = win.__hustleListPlugins();
|
|
1853
|
+
return {
|
|
1854
|
+
success: true,
|
|
1855
|
+
plugins,
|
|
1856
|
+
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(", ")}`
|
|
1857
|
+
};
|
|
1858
|
+
} catch (error2) {
|
|
1859
|
+
return {
|
|
1860
|
+
success: false,
|
|
1861
|
+
error: `Failed to list plugins: ${error2 instanceof Error ? error2.message : "Unknown error"}`
|
|
1862
|
+
};
|
|
1863
|
+
}
|
|
1864
|
+
};
|
|
1865
|
+
var modifyPluginExecutor = async (args2) => {
|
|
1866
|
+
const {
|
|
1867
|
+
name,
|
|
1868
|
+
version,
|
|
1869
|
+
description,
|
|
1870
|
+
addTools,
|
|
1871
|
+
addExecutorCode,
|
|
1872
|
+
removeTools,
|
|
1873
|
+
onRegisterCode,
|
|
1874
|
+
beforeRequestCode,
|
|
1875
|
+
afterResponseCode,
|
|
1876
|
+
onErrorCode
|
|
1877
|
+
} = args2;
|
|
1878
|
+
if (typeof window === "undefined") {
|
|
1879
|
+
return { success: false, error: "Cannot modify plugins in server environment" };
|
|
1880
|
+
}
|
|
1881
|
+
try {
|
|
1882
|
+
const win = window;
|
|
1883
|
+
if (!win.__hustleGetPlugin || !win.__hustleRegisterPlugin) {
|
|
1884
|
+
return {
|
|
1885
|
+
success: false,
|
|
1886
|
+
error: "Plugin modification not available. Make sure HustleProvider is mounted."
|
|
1887
|
+
};
|
|
1888
|
+
}
|
|
1889
|
+
const existing = win.__hustleGetPlugin(name);
|
|
1890
|
+
if (!existing) {
|
|
1891
|
+
return {
|
|
1892
|
+
success: false,
|
|
1893
|
+
error: `Plugin "${name}" not found. Use list_plugins to see installed plugins.`
|
|
1894
|
+
};
|
|
1895
|
+
}
|
|
1896
|
+
let tools = existing.tools ? [...existing.tools] : [];
|
|
1897
|
+
if (removeTools && removeTools.length > 0) {
|
|
1898
|
+
tools = tools.filter((t) => !removeTools.includes(t.name));
|
|
1899
|
+
}
|
|
1900
|
+
if (addTools && addTools.length > 0) {
|
|
1901
|
+
for (const newTool of addTools) {
|
|
1902
|
+
const existingIndex = tools.findIndex((t) => t.name === newTool.name);
|
|
1903
|
+
const toolWithExecutor = {
|
|
1904
|
+
...newTool,
|
|
1905
|
+
executorCode: addExecutorCode?.[newTool.name] || tools.find((t) => t.name === newTool.name)?.executorCode
|
|
1906
|
+
};
|
|
1907
|
+
if (existingIndex >= 0) {
|
|
1908
|
+
tools[existingIndex] = toolWithExecutor;
|
|
1909
|
+
} else {
|
|
1910
|
+
tools.push(toolWithExecutor);
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
if (addExecutorCode) {
|
|
1915
|
+
for (const [toolName, code2] of Object.entries(addExecutorCode)) {
|
|
1916
|
+
const tool = tools.find((t) => t.name === toolName);
|
|
1917
|
+
if (tool) {
|
|
1918
|
+
tool.executorCode = code2;
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
const modified = {
|
|
1923
|
+
...existing,
|
|
1924
|
+
tools
|
|
1925
|
+
};
|
|
1926
|
+
if (version) modified.version = version;
|
|
1927
|
+
if (description) modified.description = description;
|
|
1928
|
+
if (onRegisterCode || beforeRequestCode || afterResponseCode || onErrorCode) {
|
|
1929
|
+
modified.hooksCode = modified.hooksCode || {};
|
|
1930
|
+
if (onRegisterCode) modified.hooksCode.onRegisterCode = onRegisterCode;
|
|
1931
|
+
if (beforeRequestCode) modified.hooksCode.beforeRequestCode = beforeRequestCode;
|
|
1932
|
+
if (afterResponseCode) modified.hooksCode.afterResponseCode = afterResponseCode;
|
|
1933
|
+
if (onErrorCode) modified.hooksCode.onErrorCode = onErrorCode;
|
|
1934
|
+
}
|
|
1935
|
+
const unregisterFn = window.__hustleUnregisterPlugin;
|
|
1936
|
+
if (unregisterFn) {
|
|
1937
|
+
await unregisterFn(name);
|
|
1938
|
+
}
|
|
1939
|
+
await win.__hustleRegisterPlugin(modified, existing.enabled);
|
|
1940
|
+
const changes = [];
|
|
1941
|
+
if (version) changes.push(`version \u2192 ${version}`);
|
|
1942
|
+
if (description) changes.push("description updated");
|
|
1943
|
+
if (removeTools?.length) changes.push(`removed ${removeTools.length} tool(s)`);
|
|
1944
|
+
if (addTools?.length) changes.push(`added/updated ${addTools.length} tool(s)`);
|
|
1945
|
+
if (onRegisterCode) changes.push("onRegister hook updated");
|
|
1946
|
+
if (beforeRequestCode) changes.push("beforeRequest hook updated");
|
|
1947
|
+
if (afterResponseCode) changes.push("afterResponse hook updated");
|
|
1948
|
+
if (onErrorCode) changes.push("onError hook updated");
|
|
1949
|
+
return {
|
|
1950
|
+
success: true,
|
|
1951
|
+
message: `Plugin "${name}" modified: ${changes.join(", ")}`,
|
|
1952
|
+
plugin: {
|
|
1953
|
+
name: modified.name,
|
|
1954
|
+
version: modified.version,
|
|
1955
|
+
toolCount: tools.length
|
|
1956
|
+
}
|
|
1957
|
+
};
|
|
1958
|
+
} catch (error2) {
|
|
1959
|
+
return {
|
|
1960
|
+
success: false,
|
|
1961
|
+
error: `Failed to modify plugin: ${error2 instanceof Error ? error2.message : "Unknown error"}`
|
|
1962
|
+
};
|
|
1963
|
+
}
|
|
1964
|
+
};
|
|
1453
1965
|
var pluginBuilderPlugin = {
|
|
1454
1966
|
name: "plugin-builder",
|
|
1455
|
-
version: "1.
|
|
1967
|
+
version: "1.2.0",
|
|
1456
1968
|
description: "Build custom plugins through conversation",
|
|
1457
|
-
tools: [buildPluginTool, savePluginTool, installPluginTool],
|
|
1969
|
+
tools: [buildPluginTool, savePluginTool, installPluginTool, uninstallPluginTool, listPluginsTool, modifyPluginTool],
|
|
1458
1970
|
executors: {
|
|
1459
1971
|
build_plugin: buildPluginExecutor,
|
|
1460
1972
|
save_plugin: savePluginExecutor,
|
|
1461
|
-
install_plugin: installPluginExecutor
|
|
1973
|
+
install_plugin: installPluginExecutor,
|
|
1974
|
+
uninstall_plugin: uninstallPluginExecutor,
|
|
1975
|
+
list_plugins: listPluginsExecutor,
|
|
1976
|
+
modify_plugin: modifyPluginExecutor
|
|
1462
1977
|
},
|
|
1463
1978
|
hooks: {
|
|
1464
1979
|
onRegister: () => {
|