@emblemvault/hustle-react 1.0.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/hustle-react.js +856 -45
- package/dist/browser/hustle-react.js.map +1 -1
- package/dist/components/index.cjs +807 -45
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +807 -45
- package/dist/components/index.js.map +1 -1
- package/dist/hooks/index.cjs +80 -14
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.d.cts +3 -2
- package/dist/hooks/index.d.ts +3 -2
- package/dist/hooks/index.js +80 -14
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.cjs +856 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +856 -45
- package/dist/index.js.map +1 -1
- package/dist/{plugin-BUg7vMxe.d.cts → plugin-COr42J6-.d.cts} +3 -1
- package/dist/{plugin-BUg7vMxe.d.ts → plugin-COr42J6-.d.ts} +3 -1
- package/dist/plugins/index.cjs +777 -31
- package/dist/plugins/index.cjs.map +1 -1
- package/dist/plugins/index.d.cts +14 -2
- package/dist/plugins/index.d.ts +14 -2
- package/dist/plugins/index.js +777 -32
- package/dist/plugins/index.js.map +1 -1
- package/dist/providers/index.cjs +80 -14
- package/dist/providers/index.cjs.map +1 -1
- package/dist/providers/index.js +80 -14
- package/dist/providers/index.js.map +1 -1
- package/package.json +2 -2
package/dist/plugins/index.cjs
CHANGED
|
@@ -678,12 +678,16 @@ function ensureModalStyles() {
|
|
|
678
678
|
left: 0;
|
|
679
679
|
right: 0;
|
|
680
680
|
bottom: 0;
|
|
681
|
-
background:
|
|
681
|
+
background: transparent;
|
|
682
682
|
display: flex;
|
|
683
683
|
align-items: center;
|
|
684
684
|
justify-content: center;
|
|
685
685
|
z-index: 10000;
|
|
686
|
-
|
|
686
|
+
pointer-events: none;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
.user-question-modal {
|
|
690
|
+
pointer-events: auto;
|
|
687
691
|
}
|
|
688
692
|
|
|
689
693
|
@keyframes uqFadeIn {
|
|
@@ -796,6 +800,27 @@ function ensureModalStyles() {
|
|
|
796
800
|
color: #666;
|
|
797
801
|
cursor: not-allowed;
|
|
798
802
|
}
|
|
803
|
+
|
|
804
|
+
.user-question-custom-input {
|
|
805
|
+
width: 100%;
|
|
806
|
+
padding: 8px 12px;
|
|
807
|
+
margin-top: 8px;
|
|
808
|
+
background: #1a1a2e;
|
|
809
|
+
border: 1px solid #444;
|
|
810
|
+
border-radius: 6px;
|
|
811
|
+
color: #e0e0e0;
|
|
812
|
+
font-size: 14px;
|
|
813
|
+
box-sizing: border-box;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
.user-question-custom-input:focus {
|
|
817
|
+
outline: none;
|
|
818
|
+
border-color: #4a7aff;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
.user-question-custom-input::placeholder {
|
|
822
|
+
color: #666;
|
|
823
|
+
}
|
|
799
824
|
`;
|
|
800
825
|
document.head.appendChild(styles);
|
|
801
826
|
}
|
|
@@ -817,13 +842,17 @@ var askUserTool = {
|
|
|
817
842
|
allowMultiple: {
|
|
818
843
|
type: "boolean",
|
|
819
844
|
description: "If true, user can select multiple choices. Default: false"
|
|
845
|
+
},
|
|
846
|
+
allowCustom: {
|
|
847
|
+
type: "boolean",
|
|
848
|
+
description: 'If true, adds an "Other" option where user can type a custom response. Default: false'
|
|
820
849
|
}
|
|
821
850
|
},
|
|
822
851
|
required: ["question", "choices"]
|
|
823
852
|
}
|
|
824
853
|
};
|
|
825
854
|
var askUserExecutor = async (args2) => {
|
|
826
|
-
const { question, choices, allowMultiple = false } = args2;
|
|
855
|
+
const { question, choices, allowMultiple = false, allowCustom = false } = args2;
|
|
827
856
|
if (!question || !choices || !Array.isArray(choices) || choices.length === 0) {
|
|
828
857
|
return {
|
|
829
858
|
question: question || "",
|
|
@@ -855,6 +884,17 @@ var askUserExecutor = async (args2) => {
|
|
|
855
884
|
choicesDiv.className = "user-question-choices";
|
|
856
885
|
const inputType = allowMultiple ? "checkbox" : "radio";
|
|
857
886
|
const inputName = `uq-${Date.now()}`;
|
|
887
|
+
let customInput = null;
|
|
888
|
+
let isCustomSelected = false;
|
|
889
|
+
const submitBtn = document.createElement("button");
|
|
890
|
+
submitBtn.className = "user-question-btn user-question-btn-submit";
|
|
891
|
+
submitBtn.textContent = "Submit";
|
|
892
|
+
submitBtn.disabled = true;
|
|
893
|
+
const updateSubmitButton = () => {
|
|
894
|
+
const hasSelection = selected.size > 0;
|
|
895
|
+
const hasCustomValue = isCustomSelected && customInput && customInput.value.trim().length > 0;
|
|
896
|
+
submitBtn.disabled = !hasSelection && !hasCustomValue;
|
|
897
|
+
};
|
|
858
898
|
choices.forEach((choice, index) => {
|
|
859
899
|
const choiceDiv = document.createElement("div");
|
|
860
900
|
choiceDiv.className = "user-question-choice";
|
|
@@ -879,6 +919,8 @@ var askUserExecutor = async (args2) => {
|
|
|
879
919
|
}
|
|
880
920
|
} else {
|
|
881
921
|
selected.clear();
|
|
922
|
+
isCustomSelected = false;
|
|
923
|
+
if (customInput) customInput.value = "";
|
|
882
924
|
selected.add(choice);
|
|
883
925
|
choicesDiv.querySelectorAll(".user-question-choice").forEach((c) => c.classList.remove("selected"));
|
|
884
926
|
choiceDiv.classList.add("selected");
|
|
@@ -894,9 +936,62 @@ var askUserExecutor = async (args2) => {
|
|
|
894
936
|
});
|
|
895
937
|
choicesDiv.appendChild(choiceDiv);
|
|
896
938
|
});
|
|
939
|
+
if (allowCustom) {
|
|
940
|
+
const customChoiceDiv = document.createElement("div");
|
|
941
|
+
customChoiceDiv.className = "user-question-choice";
|
|
942
|
+
const customRadio = document.createElement("input");
|
|
943
|
+
customRadio.type = inputType;
|
|
944
|
+
customRadio.name = inputName;
|
|
945
|
+
customRadio.id = `${inputName}-custom`;
|
|
946
|
+
customRadio.value = "__custom__";
|
|
947
|
+
const customLabel = document.createElement("label");
|
|
948
|
+
customLabel.htmlFor = customRadio.id;
|
|
949
|
+
customLabel.textContent = "Other:";
|
|
950
|
+
customLabel.style.flexShrink = "0";
|
|
951
|
+
customInput = document.createElement("input");
|
|
952
|
+
customInput.type = "text";
|
|
953
|
+
customInput.className = "user-question-custom-input";
|
|
954
|
+
customInput.placeholder = "Type your answer...";
|
|
955
|
+
customInput.style.marginTop = "0";
|
|
956
|
+
customInput.style.marginLeft = "8px";
|
|
957
|
+
customInput.style.flex = "1";
|
|
958
|
+
customChoiceDiv.appendChild(customRadio);
|
|
959
|
+
customChoiceDiv.appendChild(customLabel);
|
|
960
|
+
customChoiceDiv.appendChild(customInput);
|
|
961
|
+
const handleCustomSelect = () => {
|
|
962
|
+
if (!allowMultiple) {
|
|
963
|
+
selected.clear();
|
|
964
|
+
choicesDiv.querySelectorAll(".user-question-choice").forEach((c) => c.classList.remove("selected"));
|
|
965
|
+
}
|
|
966
|
+
isCustomSelected = true;
|
|
967
|
+
customChoiceDiv.classList.add("selected");
|
|
968
|
+
customInput?.focus();
|
|
969
|
+
updateSubmitButton();
|
|
970
|
+
};
|
|
971
|
+
customRadio.addEventListener("change", handleCustomSelect);
|
|
972
|
+
customChoiceDiv.addEventListener("click", (e) => {
|
|
973
|
+
if (e.target !== customRadio && e.target !== customInput) {
|
|
974
|
+
customRadio.checked = true;
|
|
975
|
+
handleCustomSelect();
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
customInput.addEventListener("focus", () => {
|
|
979
|
+
if (!customRadio.checked) {
|
|
980
|
+
customRadio.checked = true;
|
|
981
|
+
handleCustomSelect();
|
|
982
|
+
}
|
|
983
|
+
});
|
|
984
|
+
customInput.addEventListener("input", updateSubmitButton);
|
|
985
|
+
choicesDiv.appendChild(customChoiceDiv);
|
|
986
|
+
}
|
|
897
987
|
modal.appendChild(choicesDiv);
|
|
898
988
|
const actions = document.createElement("div");
|
|
899
989
|
actions.className = "user-question-actions";
|
|
990
|
+
overlay.appendChild(modal);
|
|
991
|
+
document.body.appendChild(overlay);
|
|
992
|
+
const cleanup = () => {
|
|
993
|
+
overlay.remove();
|
|
994
|
+
};
|
|
900
995
|
const cancelBtn = document.createElement("button");
|
|
901
996
|
cancelBtn.className = "user-question-btn user-question-btn-cancel";
|
|
902
997
|
cancelBtn.textContent = "Skip";
|
|
@@ -908,29 +1003,21 @@ var askUserExecutor = async (args2) => {
|
|
|
908
1003
|
answered: false
|
|
909
1004
|
});
|
|
910
1005
|
};
|
|
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
1006
|
submitBtn.onclick = () => {
|
|
916
1007
|
cleanup();
|
|
1008
|
+
const results = Array.from(selected);
|
|
1009
|
+
if (isCustomSelected && customInput && customInput.value.trim()) {
|
|
1010
|
+
results.push(customInput.value.trim());
|
|
1011
|
+
}
|
|
917
1012
|
resolve({
|
|
918
1013
|
question,
|
|
919
|
-
selectedChoices:
|
|
1014
|
+
selectedChoices: results,
|
|
920
1015
|
answered: true
|
|
921
1016
|
});
|
|
922
1017
|
};
|
|
923
|
-
const updateSubmitButton = () => {
|
|
924
|
-
submitBtn.disabled = selected.size === 0;
|
|
925
|
-
};
|
|
926
1018
|
actions.appendChild(cancelBtn);
|
|
927
1019
|
actions.appendChild(submitBtn);
|
|
928
1020
|
modal.appendChild(actions);
|
|
929
|
-
overlay.appendChild(modal);
|
|
930
|
-
document.body.appendChild(overlay);
|
|
931
|
-
const cleanup = () => {
|
|
932
|
-
overlay.remove();
|
|
933
|
-
};
|
|
934
1021
|
const handleEscape = (e) => {
|
|
935
1022
|
if (e.key === "Escape") {
|
|
936
1023
|
document.removeEventListener("keydown", handleEscape);
|
|
@@ -1099,6 +1186,11 @@ var screenshotTool = {
|
|
|
1099
1186
|
|
|
1100
1187
|
The screenshot captures the visible viewport of the page. The image is uploaded to the server and a permanent URL is returned.
|
|
1101
1188
|
|
|
1189
|
+
IMPORTANT: Before taking a screenshot, use the ask_user tool (if available) to ask which size they prefer:
|
|
1190
|
+
- "full" (100%) - highest quality, larger file
|
|
1191
|
+
- "half" (50%) - good balance of quality and size
|
|
1192
|
+
- "quarter" (25%) - smallest file, faster upload
|
|
1193
|
+
|
|
1102
1194
|
Use this when:
|
|
1103
1195
|
- User asks to see what's on their screen
|
|
1104
1196
|
- You need to analyze the current page visually
|
|
@@ -1109,12 +1201,24 @@ Use this when:
|
|
|
1109
1201
|
selector: {
|
|
1110
1202
|
type: "string",
|
|
1111
1203
|
description: "Optional CSS selector to capture a specific element instead of the full page. Leave empty for full page screenshot."
|
|
1204
|
+
},
|
|
1205
|
+
size: {
|
|
1206
|
+
type: "string",
|
|
1207
|
+
enum: ["full", "half", "quarter"],
|
|
1208
|
+
description: 'Image size: "full" (100%), "half" (50%), or "quarter" (25%). Ask the user which size they prefer before capturing.'
|
|
1112
1209
|
}
|
|
1113
1210
|
}
|
|
1114
1211
|
}
|
|
1115
1212
|
};
|
|
1116
1213
|
var screenshotExecutor = async (args2) => {
|
|
1117
1214
|
const selector = args2.selector;
|
|
1215
|
+
const size = args2.size || "full";
|
|
1216
|
+
const scaleMap = {
|
|
1217
|
+
full: 1,
|
|
1218
|
+
half: 0.5,
|
|
1219
|
+
quarter: 0.25
|
|
1220
|
+
};
|
|
1221
|
+
const scale = scaleMap[size] || 1;
|
|
1118
1222
|
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
1119
1223
|
return {
|
|
1120
1224
|
success: false,
|
|
@@ -1135,34 +1239,41 @@ var screenshotExecutor = async (args2) => {
|
|
|
1135
1239
|
if (!target) {
|
|
1136
1240
|
return { success: false, error: "Element not found: " + selector };
|
|
1137
1241
|
}
|
|
1138
|
-
const
|
|
1242
|
+
const fullCanvas = await window.html2canvas(target, {
|
|
1139
1243
|
useCORS: true,
|
|
1140
1244
|
allowTaint: true,
|
|
1141
1245
|
backgroundColor: "#000000",
|
|
1142
1246
|
scale: 1
|
|
1143
1247
|
});
|
|
1248
|
+
let finalCanvas = fullCanvas;
|
|
1249
|
+
if (scale < 1) {
|
|
1250
|
+
const resizedCanvas = document.createElement("canvas");
|
|
1251
|
+
resizedCanvas.width = Math.round(fullCanvas.width * scale);
|
|
1252
|
+
resizedCanvas.height = Math.round(fullCanvas.height * scale);
|
|
1253
|
+
const ctx = resizedCanvas.getContext("2d");
|
|
1254
|
+
if (ctx) {
|
|
1255
|
+
ctx.drawImage(fullCanvas, 0, 0, resizedCanvas.width, resizedCanvas.height);
|
|
1256
|
+
finalCanvas = resizedCanvas;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1144
1259
|
const blob = await new Promise((resolve) => {
|
|
1145
|
-
|
|
1260
|
+
finalCanvas.toBlob(resolve, "image/png", 0.9);
|
|
1146
1261
|
});
|
|
1147
1262
|
if (!blob) {
|
|
1148
1263
|
return { success: false, error: "Failed to create image blob" };
|
|
1149
1264
|
}
|
|
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" };
|
|
1265
|
+
const uploadFn = window.__hustleUploadFile;
|
|
1266
|
+
if (!uploadFn) {
|
|
1267
|
+
return { success: false, error: "Upload not available. Make sure HustleProvider is mounted and client is ready." };
|
|
1159
1268
|
}
|
|
1160
|
-
const
|
|
1269
|
+
const fileName = "screenshot-" + Date.now() + ".png";
|
|
1270
|
+
const attachment = await uploadFn(blob, fileName);
|
|
1271
|
+
const sizeLabel = size === "full" ? "100%" : size === "half" ? "50%" : "25%";
|
|
1161
1272
|
return {
|
|
1162
1273
|
success: true,
|
|
1163
|
-
url:
|
|
1164
|
-
contentType:
|
|
1165
|
-
message:
|
|
1274
|
+
url: attachment.url,
|
|
1275
|
+
contentType: attachment.contentType || "image/png",
|
|
1276
|
+
message: `Screenshot captured at ${sizeLabel} size (${finalCanvas.width}x${finalCanvas.height}) and uploaded successfully`
|
|
1166
1277
|
};
|
|
1167
1278
|
} catch (e) {
|
|
1168
1279
|
const err = e;
|
|
@@ -1187,6 +1298,636 @@ var screenshotPlugin = {
|
|
|
1187
1298
|
}
|
|
1188
1299
|
};
|
|
1189
1300
|
|
|
1301
|
+
// src/plugins/pluginBuilder.ts
|
|
1302
|
+
var buildPluginTool = {
|
|
1303
|
+
name: "build_plugin",
|
|
1304
|
+
description: `Build a Hustle plugin definition. Use this tool to construct a plugin based on user requirements.
|
|
1305
|
+
|
|
1306
|
+
## Plugin Structure
|
|
1307
|
+
|
|
1308
|
+
A plugin consists of:
|
|
1309
|
+
- **name**: Unique identifier (lowercase, no spaces, e.g., "my-plugin")
|
|
1310
|
+
- **version**: Semantic version (e.g., "1.0.0")
|
|
1311
|
+
- **description**: What the plugin does
|
|
1312
|
+
- **tools**: Array of tool definitions the AI can call
|
|
1313
|
+
- **executorCode**: Object mapping tool names to JavaScript function code strings
|
|
1314
|
+
|
|
1315
|
+
## Tool Definition Format
|
|
1316
|
+
|
|
1317
|
+
Each tool needs:
|
|
1318
|
+
- **name**: Unique tool name (alphanumeric + underscore, e.g., "get_weather")
|
|
1319
|
+
- **description**: Clear description for the AI to understand when to use it
|
|
1320
|
+
- **parameters**: JSON Schema object defining the arguments
|
|
1321
|
+
|
|
1322
|
+
Example tool:
|
|
1323
|
+
{
|
|
1324
|
+
"name": "get_weather",
|
|
1325
|
+
"description": "Get current weather for a city",
|
|
1326
|
+
"parameters": {
|
|
1327
|
+
"type": "object",
|
|
1328
|
+
"properties": {
|
|
1329
|
+
"city": { "type": "string", "description": "City name" },
|
|
1330
|
+
"units": { "type": "string", "enum": ["celsius", "fahrenheit"], "description": "Temperature units" }
|
|
1331
|
+
},
|
|
1332
|
+
"required": ["city"]
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
## Executor Code Format
|
|
1337
|
+
|
|
1338
|
+
Executors are async JavaScript functions that receive args and return a result.
|
|
1339
|
+
Write them as arrow function strings that will be eval'd:
|
|
1340
|
+
|
|
1341
|
+
"async (args) => { const { city } = args; return { weather: 'sunny', city }; }"
|
|
1342
|
+
|
|
1343
|
+
The function receives args as Record<string, unknown> and should return the result.
|
|
1344
|
+
|
|
1345
|
+
## Available in Executor Scope
|
|
1346
|
+
|
|
1347
|
+
Executors run in the browser context with full access to:
|
|
1348
|
+
|
|
1349
|
+
### Browser APIs
|
|
1350
|
+
- **fetch(url, options)** - HTTP requests (subject to CORS)
|
|
1351
|
+
- **localStorage / sessionStorage** - Persistent storage
|
|
1352
|
+
- **document** - Full DOM access (create elements, modals, forms, etc.)
|
|
1353
|
+
- **window** - Global window object
|
|
1354
|
+
- **console** - Logging (log, warn, error, etc.)
|
|
1355
|
+
- **setTimeout / setInterval / clearTimeout / clearInterval** - Timers
|
|
1356
|
+
- **JSON** - Parse and stringify
|
|
1357
|
+
- **Date** - Date/time operations
|
|
1358
|
+
- **URL / URLSearchParams** - URL manipulation
|
|
1359
|
+
- **FormData / Blob / File / FileReader** - File handling
|
|
1360
|
+
- **crypto** - Cryptographic operations (crypto.randomUUID(), etc.)
|
|
1361
|
+
- **navigator** - Browser info, clipboard, geolocation, etc.
|
|
1362
|
+
- **location** - Current URL info
|
|
1363
|
+
- **history** - Browser history navigation
|
|
1364
|
+
- **WebSocket** - Real-time bidirectional communication
|
|
1365
|
+
- **EventSource** - Server-sent events
|
|
1366
|
+
- **indexedDB** - Client-side database for large data
|
|
1367
|
+
- **Notification** - Browser notifications (requires permission)
|
|
1368
|
+
- **performance** - Performance timing
|
|
1369
|
+
- **atob / btoa** - Base64 encoding/decoding
|
|
1370
|
+
- **TextEncoder / TextDecoder** - Text encoding
|
|
1371
|
+
- **AbortController** - Cancel fetch requests
|
|
1372
|
+
- **IntersectionObserver / MutationObserver / ResizeObserver** - DOM observers
|
|
1373
|
+
- **requestAnimationFrame** - Animation timing
|
|
1374
|
+
- **speechSynthesis** - Text-to-speech
|
|
1375
|
+
- **Audio / Image / Canvas** - Media APIs
|
|
1376
|
+
|
|
1377
|
+
### Hustle Plugin System Globals
|
|
1378
|
+
- **window.__hustleInstanceId** - Current Hustle instance ID
|
|
1379
|
+
- **window.__hustleRegisterPlugin(plugin, enabled)** - Install another plugin dynamically
|
|
1380
|
+
- **window.__hustleUnregisterPlugin(name)** - Uninstall a plugin by name
|
|
1381
|
+
- **window.__hustleUploadFile(file, fileName?)** - Upload a File/Blob to the server, returns { url, contentType }
|
|
1382
|
+
- **window.__hustleListPlugins()** - List all installed plugins
|
|
1383
|
+
- **window.__hustleGetPlugin(name)** - Get a specific plugin by name
|
|
1384
|
+
|
|
1385
|
+
### DOM Manipulation Examples
|
|
1386
|
+
Create a modal: document.createElement('div'), style it, append to document.body
|
|
1387
|
+
Add event listeners: element.addEventListener('click', handler)
|
|
1388
|
+
Query elements: document.querySelector(), document.querySelectorAll()
|
|
1389
|
+
|
|
1390
|
+
### Storage Patterns
|
|
1391
|
+
Store data: localStorage.setItem('key', JSON.stringify(data))
|
|
1392
|
+
Retrieve data: JSON.parse(localStorage.getItem('key') || '{}')
|
|
1393
|
+
Namespace your keys: Use plugin name prefix like "myplugin-settings"
|
|
1394
|
+
|
|
1395
|
+
### Async Patterns
|
|
1396
|
+
All executors should be async. Use await for promises:
|
|
1397
|
+
"async (args) => { const res = await fetch(url); return await res.json(); }"
|
|
1398
|
+
|
|
1399
|
+
## Lifecycle Hooks (Optional)
|
|
1400
|
+
|
|
1401
|
+
Hooks also have full access to the browser scope described above.
|
|
1402
|
+
|
|
1403
|
+
- **onRegisterCode**: Called once when plugin is registered/enabled. Good for initialization.
|
|
1404
|
+
**IMPORTANT: Always log when your plugin registers so users know it's active!**
|
|
1405
|
+
Example: "async () => { console.log('[MyPlugin] v1.0.0 registered'); }"
|
|
1406
|
+
|
|
1407
|
+
- **beforeRequestCode**: Modify messages before sending. Receives request object with { messages, model, ... }. Must return the modified request.
|
|
1408
|
+
Example: "async (req) => { req.messages = req.messages.map(m => ({...m, content: m.content.toUpperCase()})); return req; }"
|
|
1409
|
+
|
|
1410
|
+
- **afterResponseCode**: Process/modify response after receiving. Receives response object with { content, ... }.
|
|
1411
|
+
Example: "async (res) => { console.log('Response received:', res.content.substring(0, 100)); }"
|
|
1412
|
+
|
|
1413
|
+
- **onErrorCode**: Called on errors. Receives (error, context) where context has { phase: 'beforeRequest'|'execute'|'afterResponse' }.
|
|
1414
|
+
Example: "async (error, ctx) => { console.error('[MyPlugin] Error in', ctx.phase, ':', error.message); }"
|
|
1415
|
+
|
|
1416
|
+
## Best Practices
|
|
1417
|
+
|
|
1418
|
+
1. **Always add onRegisterCode** that logs the plugin name and version
|
|
1419
|
+
2. **Namespace console logs** with [PluginName] prefix for easy identification
|
|
1420
|
+
3. **Handle errors gracefully** in executors - return { error: message } instead of throwing
|
|
1421
|
+
|
|
1422
|
+
## Security Notes
|
|
1423
|
+
- Code runs in browser sandbox with same-origin policy
|
|
1424
|
+
- fetch() is subject to CORS restrictions
|
|
1425
|
+
- No direct filesystem access (use File API with user interaction)
|
|
1426
|
+
- Be careful with eval() on user input`,
|
|
1427
|
+
parameters: {
|
|
1428
|
+
type: "object",
|
|
1429
|
+
properties: {
|
|
1430
|
+
name: {
|
|
1431
|
+
type: "string",
|
|
1432
|
+
description: "Unique plugin identifier (lowercase, no spaces)"
|
|
1433
|
+
},
|
|
1434
|
+
version: {
|
|
1435
|
+
type: "string",
|
|
1436
|
+
description: 'Semantic version (e.g., "1.0.0")'
|
|
1437
|
+
},
|
|
1438
|
+
description: {
|
|
1439
|
+
type: "string",
|
|
1440
|
+
description: "What the plugin does"
|
|
1441
|
+
},
|
|
1442
|
+
tools: {
|
|
1443
|
+
type: "array",
|
|
1444
|
+
description: "Array of tool definitions",
|
|
1445
|
+
items: {
|
|
1446
|
+
type: "object",
|
|
1447
|
+
properties: {
|
|
1448
|
+
name: { type: "string", description: "Tool name" },
|
|
1449
|
+
description: { type: "string", description: "Tool description for AI" },
|
|
1450
|
+
parameters: { type: "object", description: "JSON Schema for arguments" }
|
|
1451
|
+
},
|
|
1452
|
+
required: ["name", "description", "parameters"]
|
|
1453
|
+
}
|
|
1454
|
+
},
|
|
1455
|
+
executorCode: {
|
|
1456
|
+
type: "object",
|
|
1457
|
+
description: "Object mapping tool names to executor function code strings"
|
|
1458
|
+
},
|
|
1459
|
+
beforeRequestCode: {
|
|
1460
|
+
type: "string",
|
|
1461
|
+
description: "Optional: Code for beforeRequest hook"
|
|
1462
|
+
},
|
|
1463
|
+
afterResponseCode: {
|
|
1464
|
+
type: "string",
|
|
1465
|
+
description: "Optional: Code for afterResponse hook"
|
|
1466
|
+
},
|
|
1467
|
+
onRegisterCode: {
|
|
1468
|
+
type: "string",
|
|
1469
|
+
description: "Optional: Code for onRegister hook"
|
|
1470
|
+
},
|
|
1471
|
+
onErrorCode: {
|
|
1472
|
+
type: "string",
|
|
1473
|
+
description: "Optional: Code for onError hook"
|
|
1474
|
+
}
|
|
1475
|
+
},
|
|
1476
|
+
required: ["name", "version", "description", "tools", "executorCode"]
|
|
1477
|
+
}
|
|
1478
|
+
};
|
|
1479
|
+
var savePluginTool = {
|
|
1480
|
+
name: "save_plugin",
|
|
1481
|
+
description: "Save a built plugin as a JSON file. Opens a download dialog for the user.",
|
|
1482
|
+
parameters: {
|
|
1483
|
+
type: "object",
|
|
1484
|
+
properties: {
|
|
1485
|
+
plugin: {
|
|
1486
|
+
type: "object",
|
|
1487
|
+
description: "The plugin object to save (from build_plugin result)"
|
|
1488
|
+
},
|
|
1489
|
+
filename: {
|
|
1490
|
+
type: "string",
|
|
1491
|
+
description: "Filename without extension (defaults to plugin name)"
|
|
1492
|
+
}
|
|
1493
|
+
},
|
|
1494
|
+
required: ["plugin"]
|
|
1495
|
+
}
|
|
1496
|
+
};
|
|
1497
|
+
var installPluginTool = {
|
|
1498
|
+
name: "install_plugin",
|
|
1499
|
+
description: "Install a built plugin to browser storage so it persists and can be used.",
|
|
1500
|
+
parameters: {
|
|
1501
|
+
type: "object",
|
|
1502
|
+
properties: {
|
|
1503
|
+
plugin: {
|
|
1504
|
+
type: "object",
|
|
1505
|
+
description: "The plugin object to install (from build_plugin result)"
|
|
1506
|
+
},
|
|
1507
|
+
enabled: {
|
|
1508
|
+
type: "boolean",
|
|
1509
|
+
description: "Whether to enable the plugin immediately (default: true)"
|
|
1510
|
+
}
|
|
1511
|
+
},
|
|
1512
|
+
required: ["plugin"]
|
|
1513
|
+
}
|
|
1514
|
+
};
|
|
1515
|
+
var uninstallPluginTool = {
|
|
1516
|
+
name: "uninstall_plugin",
|
|
1517
|
+
description: "Uninstall a plugin by name, removing it from browser storage.",
|
|
1518
|
+
parameters: {
|
|
1519
|
+
type: "object",
|
|
1520
|
+
properties: {
|
|
1521
|
+
name: {
|
|
1522
|
+
type: "string",
|
|
1523
|
+
description: "The name of the plugin to uninstall"
|
|
1524
|
+
}
|
|
1525
|
+
},
|
|
1526
|
+
required: ["name"]
|
|
1527
|
+
}
|
|
1528
|
+
};
|
|
1529
|
+
var listPluginsTool = {
|
|
1530
|
+
name: "list_plugins",
|
|
1531
|
+
description: "List all installed plugins with their enabled/disabled status.",
|
|
1532
|
+
parameters: {
|
|
1533
|
+
type: "object",
|
|
1534
|
+
properties: {},
|
|
1535
|
+
required: []
|
|
1536
|
+
}
|
|
1537
|
+
};
|
|
1538
|
+
var modifyPluginTool = {
|
|
1539
|
+
name: "modify_plugin",
|
|
1540
|
+
description: `Modify an existing installed plugin. Can update version, description, tools, executors, and hooks.
|
|
1541
|
+
|
|
1542
|
+
Use list_plugins first to see installed plugins, then modify by name.
|
|
1543
|
+
|
|
1544
|
+
You can:
|
|
1545
|
+
- Add new tools (provide tools array with new tools to add)
|
|
1546
|
+
- Update existing tools (provide tool with same name)
|
|
1547
|
+
- Remove tools (set removeTool to the tool name)
|
|
1548
|
+
- Update hooks (provide hook code)
|
|
1549
|
+
- Update version/description
|
|
1550
|
+
|
|
1551
|
+
Example: Add a new tool to existing plugin:
|
|
1552
|
+
{
|
|
1553
|
+
"name": "my-plugin",
|
|
1554
|
+
"addTools": [{ "name": "new_tool", "description": "...", "parameters": {...} }],
|
|
1555
|
+
"addExecutorCode": { "new_tool": "async (args) => { ... }" }
|
|
1556
|
+
}`,
|
|
1557
|
+
parameters: {
|
|
1558
|
+
type: "object",
|
|
1559
|
+
properties: {
|
|
1560
|
+
name: {
|
|
1561
|
+
type: "string",
|
|
1562
|
+
description: "Name of the plugin to modify (required)"
|
|
1563
|
+
},
|
|
1564
|
+
version: {
|
|
1565
|
+
type: "string",
|
|
1566
|
+
description: "New version string"
|
|
1567
|
+
},
|
|
1568
|
+
description: {
|
|
1569
|
+
type: "string",
|
|
1570
|
+
description: "New description"
|
|
1571
|
+
},
|
|
1572
|
+
addTools: {
|
|
1573
|
+
type: "array",
|
|
1574
|
+
description: "Tools to add or update",
|
|
1575
|
+
items: {
|
|
1576
|
+
type: "object",
|
|
1577
|
+
properties: {
|
|
1578
|
+
name: { type: "string" },
|
|
1579
|
+
description: { type: "string" },
|
|
1580
|
+
parameters: { type: "object" }
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
},
|
|
1584
|
+
addExecutorCode: {
|
|
1585
|
+
type: "object",
|
|
1586
|
+
description: "Executor code to add/update (tool name -> code string)"
|
|
1587
|
+
},
|
|
1588
|
+
removeTools: {
|
|
1589
|
+
type: "array",
|
|
1590
|
+
description: "Names of tools to remove",
|
|
1591
|
+
items: { type: "string" }
|
|
1592
|
+
},
|
|
1593
|
+
onRegisterCode: {
|
|
1594
|
+
type: "string",
|
|
1595
|
+
description: "New onRegister hook code"
|
|
1596
|
+
},
|
|
1597
|
+
beforeRequestCode: {
|
|
1598
|
+
type: "string",
|
|
1599
|
+
description: "New beforeRequest hook code"
|
|
1600
|
+
},
|
|
1601
|
+
afterResponseCode: {
|
|
1602
|
+
type: "string",
|
|
1603
|
+
description: "New afterResponse hook code"
|
|
1604
|
+
},
|
|
1605
|
+
onErrorCode: {
|
|
1606
|
+
type: "string",
|
|
1607
|
+
description: "New onError hook code"
|
|
1608
|
+
}
|
|
1609
|
+
},
|
|
1610
|
+
required: ["name"]
|
|
1611
|
+
}
|
|
1612
|
+
};
|
|
1613
|
+
var buildPluginExecutor = async (args2) => {
|
|
1614
|
+
const {
|
|
1615
|
+
name,
|
|
1616
|
+
version,
|
|
1617
|
+
description,
|
|
1618
|
+
tools,
|
|
1619
|
+
executorCode,
|
|
1620
|
+
beforeRequestCode,
|
|
1621
|
+
afterResponseCode,
|
|
1622
|
+
onRegisterCode,
|
|
1623
|
+
onErrorCode
|
|
1624
|
+
} = args2;
|
|
1625
|
+
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
1626
|
+
return {
|
|
1627
|
+
success: false,
|
|
1628
|
+
error: "Plugin name must be lowercase, start with a letter, and contain only letters, numbers, and hyphens"
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
if (!/^\d+\.\d+\.\d+/.test(version)) {
|
|
1632
|
+
return {
|
|
1633
|
+
success: false,
|
|
1634
|
+
error: 'Version must be in semver format (e.g., "1.0.0")'
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1637
|
+
for (const tool of tools) {
|
|
1638
|
+
if (!executorCode[tool.name]) {
|
|
1639
|
+
return {
|
|
1640
|
+
success: false,
|
|
1641
|
+
error: `Missing executor code for tool: ${tool.name}`
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
const storedPlugin = {
|
|
1646
|
+
name,
|
|
1647
|
+
version,
|
|
1648
|
+
description,
|
|
1649
|
+
tools: tools.map((tool) => ({
|
|
1650
|
+
...tool,
|
|
1651
|
+
executorCode: executorCode[tool.name]
|
|
1652
|
+
})),
|
|
1653
|
+
enabled: true,
|
|
1654
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1655
|
+
};
|
|
1656
|
+
if (beforeRequestCode || afterResponseCode || onRegisterCode || onErrorCode) {
|
|
1657
|
+
storedPlugin.hooksCode = {};
|
|
1658
|
+
if (beforeRequestCode) storedPlugin.hooksCode.beforeRequestCode = beforeRequestCode;
|
|
1659
|
+
if (afterResponseCode) storedPlugin.hooksCode.afterResponseCode = afterResponseCode;
|
|
1660
|
+
if (onRegisterCode) storedPlugin.hooksCode.onRegisterCode = onRegisterCode;
|
|
1661
|
+
if (onErrorCode) storedPlugin.hooksCode.onErrorCode = onErrorCode;
|
|
1662
|
+
}
|
|
1663
|
+
return {
|
|
1664
|
+
success: true,
|
|
1665
|
+
plugin: storedPlugin,
|
|
1666
|
+
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.`
|
|
1667
|
+
};
|
|
1668
|
+
};
|
|
1669
|
+
function normalizePlugin(input) {
|
|
1670
|
+
const plugin = input;
|
|
1671
|
+
const tools = plugin.tools;
|
|
1672
|
+
const hasEmbeddedExecutors = tools?.[0]?.executorCode !== void 0;
|
|
1673
|
+
if (hasEmbeddedExecutors) {
|
|
1674
|
+
return {
|
|
1675
|
+
...plugin,
|
|
1676
|
+
enabled: plugin.enabled ?? true,
|
|
1677
|
+
installedAt: plugin.installedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
const executorCode = plugin.executorCode;
|
|
1681
|
+
const rawTools = tools || [];
|
|
1682
|
+
const hookKeys = ["onRegisterCode", "beforeRequestCode", "afterResponseCode", "onErrorCode"];
|
|
1683
|
+
const hooksCode = {};
|
|
1684
|
+
for (const key of hookKeys) {
|
|
1685
|
+
if (executorCode?.[key]) {
|
|
1686
|
+
hooksCode[key] = executorCode[key];
|
|
1687
|
+
}
|
|
1688
|
+
if (plugin[key]) {
|
|
1689
|
+
hooksCode[key] = plugin[key];
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
return {
|
|
1693
|
+
name: plugin.name,
|
|
1694
|
+
version: plugin.version,
|
|
1695
|
+
description: plugin.description,
|
|
1696
|
+
tools: rawTools.map((tool) => ({
|
|
1697
|
+
name: tool.name,
|
|
1698
|
+
description: tool.description,
|
|
1699
|
+
parameters: tool.parameters,
|
|
1700
|
+
executorCode: executorCode?.[tool.name]
|
|
1701
|
+
})),
|
|
1702
|
+
hooksCode: Object.keys(hooksCode).length > 0 ? hooksCode : void 0,
|
|
1703
|
+
enabled: true,
|
|
1704
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
var savePluginExecutor = async (args2) => {
|
|
1708
|
+
const { plugin: rawPlugin, filename } = args2;
|
|
1709
|
+
if (typeof window === "undefined") {
|
|
1710
|
+
return { success: false, error: "Cannot save files in server environment" };
|
|
1711
|
+
}
|
|
1712
|
+
try {
|
|
1713
|
+
const plugin = normalizePlugin(rawPlugin);
|
|
1714
|
+
const json = JSON.stringify(plugin, null, 2);
|
|
1715
|
+
const blob = new Blob([json], { type: "application/json" });
|
|
1716
|
+
const url = URL.createObjectURL(blob);
|
|
1717
|
+
const a = document.createElement("a");
|
|
1718
|
+
a.href = url;
|
|
1719
|
+
a.download = `${filename || plugin.name}.json`;
|
|
1720
|
+
document.body.appendChild(a);
|
|
1721
|
+
a.click();
|
|
1722
|
+
document.body.removeChild(a);
|
|
1723
|
+
URL.revokeObjectURL(url);
|
|
1724
|
+
return {
|
|
1725
|
+
success: true,
|
|
1726
|
+
message: `Plugin saved as ${filename || plugin.name}.json`
|
|
1727
|
+
};
|
|
1728
|
+
} catch (error2) {
|
|
1729
|
+
return {
|
|
1730
|
+
success: false,
|
|
1731
|
+
error: `Failed to save: ${error2 instanceof Error ? error2.message : "Unknown error"}`
|
|
1732
|
+
};
|
|
1733
|
+
}
|
|
1734
|
+
};
|
|
1735
|
+
var installPluginExecutor = async (args2) => {
|
|
1736
|
+
const { plugin: rawPlugin, enabled = true } = args2;
|
|
1737
|
+
if (typeof window === "undefined") {
|
|
1738
|
+
return { success: false, error: "Cannot install plugins in server environment" };
|
|
1739
|
+
}
|
|
1740
|
+
try {
|
|
1741
|
+
const plugin = normalizePlugin(rawPlugin);
|
|
1742
|
+
const win = window;
|
|
1743
|
+
if (!win.__hustleRegisterPlugin) {
|
|
1744
|
+
return {
|
|
1745
|
+
success: false,
|
|
1746
|
+
error: "Plugin registration not available. Make sure HustleProvider is mounted."
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
await win.__hustleRegisterPlugin(plugin, enabled);
|
|
1750
|
+
return {
|
|
1751
|
+
success: true,
|
|
1752
|
+
message: `Plugin "${plugin.name}" installed${enabled ? " and enabled" : ""}.`
|
|
1753
|
+
};
|
|
1754
|
+
} catch (error2) {
|
|
1755
|
+
return {
|
|
1756
|
+
success: false,
|
|
1757
|
+
error: `Failed to install: ${error2 instanceof Error ? error2.message : "Unknown error"}`
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
};
|
|
1761
|
+
var uninstallPluginExecutor = async (args2) => {
|
|
1762
|
+
const { name } = args2;
|
|
1763
|
+
if (typeof window === "undefined") {
|
|
1764
|
+
return { success: false, error: "Cannot uninstall plugins in server environment" };
|
|
1765
|
+
}
|
|
1766
|
+
try {
|
|
1767
|
+
const win = window;
|
|
1768
|
+
if (!win.__hustleUnregisterPlugin) {
|
|
1769
|
+
return {
|
|
1770
|
+
success: false,
|
|
1771
|
+
error: "Plugin unregistration not available. Make sure HustleProvider is mounted."
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
await win.__hustleUnregisterPlugin(name);
|
|
1775
|
+
return {
|
|
1776
|
+
success: true,
|
|
1777
|
+
message: `Plugin "${name}" has been uninstalled.`
|
|
1778
|
+
};
|
|
1779
|
+
} catch (error2) {
|
|
1780
|
+
return {
|
|
1781
|
+
success: false,
|
|
1782
|
+
error: `Failed to uninstall: ${error2 instanceof Error ? error2.message : "Unknown error"}`
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
};
|
|
1786
|
+
var listPluginsExecutor = async () => {
|
|
1787
|
+
if (typeof window === "undefined") {
|
|
1788
|
+
return { success: false, error: "Cannot list plugins in server environment" };
|
|
1789
|
+
}
|
|
1790
|
+
try {
|
|
1791
|
+
const win = window;
|
|
1792
|
+
if (!win.__hustleListPlugins) {
|
|
1793
|
+
return {
|
|
1794
|
+
success: false,
|
|
1795
|
+
error: "Plugin listing not available. Make sure HustleProvider is mounted."
|
|
1796
|
+
};
|
|
1797
|
+
}
|
|
1798
|
+
const plugins = win.__hustleListPlugins();
|
|
1799
|
+
return {
|
|
1800
|
+
success: true,
|
|
1801
|
+
plugins,
|
|
1802
|
+
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(", ")}`
|
|
1803
|
+
};
|
|
1804
|
+
} catch (error2) {
|
|
1805
|
+
return {
|
|
1806
|
+
success: false,
|
|
1807
|
+
error: `Failed to list plugins: ${error2 instanceof Error ? error2.message : "Unknown error"}`
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
};
|
|
1811
|
+
var modifyPluginExecutor = async (args2) => {
|
|
1812
|
+
const {
|
|
1813
|
+
name,
|
|
1814
|
+
version,
|
|
1815
|
+
description,
|
|
1816
|
+
addTools,
|
|
1817
|
+
addExecutorCode,
|
|
1818
|
+
removeTools,
|
|
1819
|
+
onRegisterCode,
|
|
1820
|
+
beforeRequestCode,
|
|
1821
|
+
afterResponseCode,
|
|
1822
|
+
onErrorCode
|
|
1823
|
+
} = args2;
|
|
1824
|
+
if (typeof window === "undefined") {
|
|
1825
|
+
return { success: false, error: "Cannot modify plugins in server environment" };
|
|
1826
|
+
}
|
|
1827
|
+
try {
|
|
1828
|
+
const win = window;
|
|
1829
|
+
if (!win.__hustleGetPlugin || !win.__hustleRegisterPlugin) {
|
|
1830
|
+
return {
|
|
1831
|
+
success: false,
|
|
1832
|
+
error: "Plugin modification not available. Make sure HustleProvider is mounted."
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1835
|
+
const existing = win.__hustleGetPlugin(name);
|
|
1836
|
+
if (!existing) {
|
|
1837
|
+
return {
|
|
1838
|
+
success: false,
|
|
1839
|
+
error: `Plugin "${name}" not found. Use list_plugins to see installed plugins.`
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
let tools = existing.tools ? [...existing.tools] : [];
|
|
1843
|
+
if (removeTools && removeTools.length > 0) {
|
|
1844
|
+
tools = tools.filter((t) => !removeTools.includes(t.name));
|
|
1845
|
+
}
|
|
1846
|
+
if (addTools && addTools.length > 0) {
|
|
1847
|
+
for (const newTool of addTools) {
|
|
1848
|
+
const existingIndex = tools.findIndex((t) => t.name === newTool.name);
|
|
1849
|
+
const toolWithExecutor = {
|
|
1850
|
+
...newTool,
|
|
1851
|
+
executorCode: addExecutorCode?.[newTool.name] || tools.find((t) => t.name === newTool.name)?.executorCode
|
|
1852
|
+
};
|
|
1853
|
+
if (existingIndex >= 0) {
|
|
1854
|
+
tools[existingIndex] = toolWithExecutor;
|
|
1855
|
+
} else {
|
|
1856
|
+
tools.push(toolWithExecutor);
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
if (addExecutorCode) {
|
|
1861
|
+
for (const [toolName, code2] of Object.entries(addExecutorCode)) {
|
|
1862
|
+
const tool = tools.find((t) => t.name === toolName);
|
|
1863
|
+
if (tool) {
|
|
1864
|
+
tool.executorCode = code2;
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
const modified = {
|
|
1869
|
+
...existing,
|
|
1870
|
+
tools
|
|
1871
|
+
};
|
|
1872
|
+
if (version) modified.version = version;
|
|
1873
|
+
if (description) modified.description = description;
|
|
1874
|
+
if (onRegisterCode || beforeRequestCode || afterResponseCode || onErrorCode) {
|
|
1875
|
+
modified.hooksCode = modified.hooksCode || {};
|
|
1876
|
+
if (onRegisterCode) modified.hooksCode.onRegisterCode = onRegisterCode;
|
|
1877
|
+
if (beforeRequestCode) modified.hooksCode.beforeRequestCode = beforeRequestCode;
|
|
1878
|
+
if (afterResponseCode) modified.hooksCode.afterResponseCode = afterResponseCode;
|
|
1879
|
+
if (onErrorCode) modified.hooksCode.onErrorCode = onErrorCode;
|
|
1880
|
+
}
|
|
1881
|
+
const unregisterFn = window.__hustleUnregisterPlugin;
|
|
1882
|
+
if (unregisterFn) {
|
|
1883
|
+
await unregisterFn(name);
|
|
1884
|
+
}
|
|
1885
|
+
await win.__hustleRegisterPlugin(modified, existing.enabled);
|
|
1886
|
+
const changes = [];
|
|
1887
|
+
if (version) changes.push(`version \u2192 ${version}`);
|
|
1888
|
+
if (description) changes.push("description updated");
|
|
1889
|
+
if (removeTools?.length) changes.push(`removed ${removeTools.length} tool(s)`);
|
|
1890
|
+
if (addTools?.length) changes.push(`added/updated ${addTools.length} tool(s)`);
|
|
1891
|
+
if (onRegisterCode) changes.push("onRegister hook updated");
|
|
1892
|
+
if (beforeRequestCode) changes.push("beforeRequest hook updated");
|
|
1893
|
+
if (afterResponseCode) changes.push("afterResponse hook updated");
|
|
1894
|
+
if (onErrorCode) changes.push("onError hook updated");
|
|
1895
|
+
return {
|
|
1896
|
+
success: true,
|
|
1897
|
+
message: `Plugin "${name}" modified: ${changes.join(", ")}`,
|
|
1898
|
+
plugin: {
|
|
1899
|
+
name: modified.name,
|
|
1900
|
+
version: modified.version,
|
|
1901
|
+
toolCount: tools.length
|
|
1902
|
+
}
|
|
1903
|
+
};
|
|
1904
|
+
} catch (error2) {
|
|
1905
|
+
return {
|
|
1906
|
+
success: false,
|
|
1907
|
+
error: `Failed to modify plugin: ${error2 instanceof Error ? error2.message : "Unknown error"}`
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
1910
|
+
};
|
|
1911
|
+
var pluginBuilderPlugin = {
|
|
1912
|
+
name: "plugin-builder",
|
|
1913
|
+
version: "1.2.0",
|
|
1914
|
+
description: "Build custom plugins through conversation",
|
|
1915
|
+
tools: [buildPluginTool, savePluginTool, installPluginTool, uninstallPluginTool, listPluginsTool, modifyPluginTool],
|
|
1916
|
+
executors: {
|
|
1917
|
+
build_plugin: buildPluginExecutor,
|
|
1918
|
+
save_plugin: savePluginExecutor,
|
|
1919
|
+
install_plugin: installPluginExecutor,
|
|
1920
|
+
uninstall_plugin: uninstallPluginExecutor,
|
|
1921
|
+
list_plugins: listPluginsExecutor,
|
|
1922
|
+
modify_plugin: modifyPluginExecutor
|
|
1923
|
+
},
|
|
1924
|
+
hooks: {
|
|
1925
|
+
onRegister: () => {
|
|
1926
|
+
console.log("[Plugin Builder] Ready to help build custom plugins");
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
};
|
|
1930
|
+
|
|
1190
1931
|
// src/plugins/index.ts
|
|
1191
1932
|
var availablePlugins = [
|
|
1192
1933
|
{
|
|
@@ -1216,6 +1957,10 @@ var availablePlugins = [
|
|
|
1216
1957
|
{
|
|
1217
1958
|
...screenshotPlugin,
|
|
1218
1959
|
description: "Take screenshots of the current page"
|
|
1960
|
+
},
|
|
1961
|
+
{
|
|
1962
|
+
...pluginBuilderPlugin,
|
|
1963
|
+
description: "Build custom plugins through conversation with AI"
|
|
1219
1964
|
}
|
|
1220
1965
|
];
|
|
1221
1966
|
function getAvailablePlugin(name) {
|
|
@@ -1228,6 +1973,7 @@ exports.getAvailablePlugin = getAvailablePlugin;
|
|
|
1228
1973
|
exports.jsExecutorPlugin = jsExecutorPlugin;
|
|
1229
1974
|
exports.migrateFunPlugin = migrateFunPlugin;
|
|
1230
1975
|
exports.piiProtectionPlugin = piiProtectionPlugin;
|
|
1976
|
+
exports.pluginBuilderPlugin = pluginBuilderPlugin;
|
|
1231
1977
|
exports.predictionMarketPlugin = predictionMarketPlugin;
|
|
1232
1978
|
exports.screenshotPlugin = screenshotPlugin;
|
|
1233
1979
|
exports.userQuestionPlugin = userQuestionPlugin;
|