@emblemvault/hustle-react 1.1.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.
@@ -678,12 +678,16 @@ function ensureModalStyles() {
678
678
  left: 0;
679
679
  right: 0;
680
680
  bottom: 0;
681
- background: rgba(0, 0, 0, 0.6);
681
+ background: transparent;
682
682
  display: flex;
683
683
  align-items: center;
684
684
  justify-content: center;
685
685
  z-index: 10000;
686
- animation: uqFadeIn 0.2s ease-out;
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: Array.from(selected),
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 canvas = await window.html2canvas(target, {
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
- canvas.toBlob(resolve, "image/png", 0.9);
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 formData = new FormData();
1151
- formData.append("file", blob, "screenshot-" + Date.now() + ".png");
1152
- const response = await fetch("/api/files/upload", {
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 data = await response.json();
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: data.url,
1164
- contentType: data.contentType,
1165
- message: "Screenshot captured and uploaded successfully"
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;
@@ -1230,21 +1341,89 @@ Write them as arrow function strings that will be eval'd:
1230
1341
  "async (args) => { const { city } = args; return { weather: 'sunny', city }; }"
1231
1342
 
1232
1343
  The function receives args as Record<string, unknown> and should return the result.
1233
- You can use fetch(), standard browser APIs, and async/await.
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(); }"
1234
1398
 
1235
1399
  ## Lifecycle Hooks (Optional)
1236
1400
 
1237
- - **beforeRequestCode**: Modify messages before sending. Receives request object, must return modified request.
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.
1238
1408
  Example: "async (req) => { req.messages = req.messages.map(m => ({...m, content: m.content.toUpperCase()})); return req; }"
1239
1409
 
1240
- - **afterResponseCode**: Process response after receiving. Receives response object.
1241
- Example: "async (res) => { console.log('Response:', res.content); }"
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); }"
1242
1415
 
1243
- - **onRegisterCode**: Called when plugin is registered.
1244
- Example: "async () => { console.log('Plugin loaded!'); }"
1416
+ ## Best Practices
1245
1417
 
1246
- - **onErrorCode**: Called on errors. Receives (error, context).
1247
- Example: "async (error, ctx) => { console.error('Error in', ctx.phase, error); }"`,
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`,
1248
1427
  parameters: {
1249
1428
  type: "object",
1250
1429
  properties: {
@@ -1333,6 +1512,104 @@ var installPluginTool = {
1333
1512
  required: ["plugin"]
1334
1513
  }
1335
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
+ };
1336
1613
  var buildPluginExecutor = async (args2) => {
1337
1614
  const {
1338
1615
  name,
@@ -1389,12 +1666,51 @@ var buildPluginExecutor = async (args2) => {
1389
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.`
1390
1667
  };
1391
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
+ }
1392
1707
  var savePluginExecutor = async (args2) => {
1393
- const { plugin, filename } = args2;
1708
+ const { plugin: rawPlugin, filename } = args2;
1394
1709
  if (typeof window === "undefined") {
1395
1710
  return { success: false, error: "Cannot save files in server environment" };
1396
1711
  }
1397
1712
  try {
1713
+ const plugin = normalizePlugin(rawPlugin);
1398
1714
  const json = JSON.stringify(plugin, null, 2);
1399
1715
  const blob = new Blob([json], { type: "application/json" });
1400
1716
  const url = URL.createObjectURL(blob);
@@ -1417,31 +1733,23 @@ var savePluginExecutor = async (args2) => {
1417
1733
  }
1418
1734
  };
1419
1735
  var installPluginExecutor = async (args2) => {
1420
- const { plugin, enabled = true } = args2;
1736
+ const { plugin: rawPlugin, enabled = true } = args2;
1421
1737
  if (typeof window === "undefined") {
1422
1738
  return { success: false, error: "Cannot install plugins in server environment" };
1423
1739
  }
1424
1740
  try {
1425
- const instanceId = window.__hustleInstanceId || "global-demo";
1426
- const STORAGE_KEY = `hustle-plugins-${instanceId}`;
1427
- const stored = localStorage.getItem(STORAGE_KEY);
1428
- const plugins = stored ? JSON.parse(stored) : [];
1429
- const existingIndex = plugins.findIndex((p) => p.name === plugin.name);
1430
- const pluginToStore = {
1431
- ...plugin,
1432
- enabled,
1433
- installedAt: (/* @__PURE__ */ new Date()).toISOString()
1434
- };
1435
- if (existingIndex >= 0) {
1436
- plugins[existingIndex] = pluginToStore;
1437
- } else {
1438
- plugins.push(pluginToStore);
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
+ };
1439
1748
  }
1440
- localStorage.setItem(STORAGE_KEY, JSON.stringify(plugins));
1749
+ await win.__hustleRegisterPlugin(plugin, enabled);
1441
1750
  return {
1442
1751
  success: true,
1443
- message: `Plugin "${plugin.name}" installed${enabled ? " and enabled" : ""}. Refresh the page or re-register plugins to activate.`,
1444
- action: existingIndex >= 0 ? "updated" : "installed"
1752
+ message: `Plugin "${plugin.name}" installed${enabled ? " and enabled" : ""}.`
1445
1753
  };
1446
1754
  } catch (error2) {
1447
1755
  return {
@@ -1450,15 +1758,168 @@ var installPluginExecutor = async (args2) => {
1450
1758
  };
1451
1759
  }
1452
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
+ };
1453
1911
  var pluginBuilderPlugin = {
1454
1912
  name: "plugin-builder",
1455
- version: "1.0.0",
1913
+ version: "1.2.0",
1456
1914
  description: "Build custom plugins through conversation",
1457
- tools: [buildPluginTool, savePluginTool, installPluginTool],
1915
+ tools: [buildPluginTool, savePluginTool, installPluginTool, uninstallPluginTool, listPluginsTool, modifyPluginTool],
1458
1916
  executors: {
1459
1917
  build_plugin: buildPluginExecutor,
1460
1918
  save_plugin: savePluginExecutor,
1461
- install_plugin: installPluginExecutor
1919
+ install_plugin: installPluginExecutor,
1920
+ uninstall_plugin: uninstallPluginExecutor,
1921
+ list_plugins: listPluginsExecutor,
1922
+ modify_plugin: modifyPluginExecutor
1462
1923
  },
1463
1924
  hooks: {
1464
1925
  onRegister: () => {