@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.
@@ -302,35 +302,52 @@ var pluginRegistry = new PluginRegistry();
302
302
  function getStorageKey(instanceId) {
303
303
  return `hustle-plugins-${instanceId}`;
304
304
  }
305
- function usePlugins(instanceId = "default") {
305
+ function getInstanceId(providedId) {
306
+ if (providedId) return providedId;
307
+ if (typeof window !== "undefined") {
308
+ const globalId = window.__hustleInstanceId;
309
+ if (globalId) return globalId;
310
+ }
311
+ return "default";
312
+ }
313
+ function usePlugins(instanceId) {
314
+ const [resolvedInstanceId] = react.useState(() => getInstanceId(instanceId));
306
315
  const [plugins, setPlugins] = react.useState([]);
307
316
  react.useEffect(() => {
308
- setPlugins(pluginRegistry.loadFromStorage(instanceId));
309
- const unsubscribe = pluginRegistry.onChange(setPlugins, instanceId);
310
- const storageKey = getStorageKey(instanceId);
317
+ setPlugins(pluginRegistry.loadFromStorage(resolvedInstanceId));
318
+ const unsubscribe = pluginRegistry.onChange(setPlugins, resolvedInstanceId);
319
+ const storageKey = getStorageKey(resolvedInstanceId);
311
320
  const handleStorage = (e) => {
312
321
  if (e.key === storageKey) {
313
- setPlugins(pluginRegistry.loadFromStorage(instanceId));
322
+ setPlugins(pluginRegistry.loadFromStorage(resolvedInstanceId));
314
323
  }
315
324
  };
316
325
  window.addEventListener("storage", handleStorage);
326
+ const handlePluginInstalled = (e) => {
327
+ const customEvent = e;
328
+ if (customEvent.detail.instanceId === resolvedInstanceId) {
329
+ setPlugins(pluginRegistry.loadFromStorage(resolvedInstanceId));
330
+ }
331
+ };
332
+ window.addEventListener("hustle-plugin-installed", handlePluginInstalled);
317
333
  return () => {
318
334
  unsubscribe();
319
335
  window.removeEventListener("storage", handleStorage);
336
+ window.removeEventListener("hustle-plugin-installed", handlePluginInstalled);
320
337
  };
321
- }, [instanceId]);
338
+ }, [resolvedInstanceId]);
322
339
  const registerPlugin = react.useCallback((plugin) => {
323
- pluginRegistry.register(plugin, true, instanceId);
324
- }, [instanceId]);
340
+ pluginRegistry.register(plugin, true, resolvedInstanceId);
341
+ }, [resolvedInstanceId]);
325
342
  const unregisterPlugin = react.useCallback((name) => {
326
- pluginRegistry.unregister(name, instanceId);
327
- }, [instanceId]);
343
+ pluginRegistry.unregister(name, resolvedInstanceId);
344
+ }, [resolvedInstanceId]);
328
345
  const enablePlugin = react.useCallback((name) => {
329
- pluginRegistry.setEnabled(name, true, instanceId);
330
- }, [instanceId]);
346
+ pluginRegistry.setEnabled(name, true, resolvedInstanceId);
347
+ }, [resolvedInstanceId]);
331
348
  const disablePlugin = react.useCallback((name) => {
332
- pluginRegistry.setEnabled(name, false, instanceId);
333
- }, [instanceId]);
349
+ pluginRegistry.setEnabled(name, false, resolvedInstanceId);
350
+ }, [resolvedInstanceId]);
334
351
  const isRegistered = react.useCallback(
335
352
  (name) => plugins.some((p) => p.name === name),
336
353
  [plugins]
@@ -1038,12 +1055,16 @@ function ensureModalStyles() {
1038
1055
  left: 0;
1039
1056
  right: 0;
1040
1057
  bottom: 0;
1041
- background: rgba(0, 0, 0, 0.6);
1058
+ background: transparent;
1042
1059
  display: flex;
1043
1060
  align-items: center;
1044
1061
  justify-content: center;
1045
1062
  z-index: 10000;
1046
- animation: uqFadeIn 0.2s ease-out;
1063
+ pointer-events: none;
1064
+ }
1065
+
1066
+ .user-question-modal {
1067
+ pointer-events: auto;
1047
1068
  }
1048
1069
 
1049
1070
  @keyframes uqFadeIn {
@@ -1156,6 +1177,27 @@ function ensureModalStyles() {
1156
1177
  color: #666;
1157
1178
  cursor: not-allowed;
1158
1179
  }
1180
+
1181
+ .user-question-custom-input {
1182
+ width: 100%;
1183
+ padding: 8px 12px;
1184
+ margin-top: 8px;
1185
+ background: #1a1a2e;
1186
+ border: 1px solid #444;
1187
+ border-radius: 6px;
1188
+ color: #e0e0e0;
1189
+ font-size: 14px;
1190
+ box-sizing: border-box;
1191
+ }
1192
+
1193
+ .user-question-custom-input:focus {
1194
+ outline: none;
1195
+ border-color: #4a7aff;
1196
+ }
1197
+
1198
+ .user-question-custom-input::placeholder {
1199
+ color: #666;
1200
+ }
1159
1201
  `;
1160
1202
  document.head.appendChild(styles2);
1161
1203
  }
@@ -1177,13 +1219,17 @@ var askUserTool = {
1177
1219
  allowMultiple: {
1178
1220
  type: "boolean",
1179
1221
  description: "If true, user can select multiple choices. Default: false"
1222
+ },
1223
+ allowCustom: {
1224
+ type: "boolean",
1225
+ description: 'If true, adds an "Other" option where user can type a custom response. Default: false'
1180
1226
  }
1181
1227
  },
1182
1228
  required: ["question", "choices"]
1183
1229
  }
1184
1230
  };
1185
1231
  var askUserExecutor = async (args2) => {
1186
- const { question, choices, allowMultiple = false } = args2;
1232
+ const { question, choices, allowMultiple = false, allowCustom = false } = args2;
1187
1233
  if (!question || !choices || !Array.isArray(choices) || choices.length === 0) {
1188
1234
  return {
1189
1235
  question: question || "",
@@ -1215,6 +1261,17 @@ var askUserExecutor = async (args2) => {
1215
1261
  choicesDiv.className = "user-question-choices";
1216
1262
  const inputType = allowMultiple ? "checkbox" : "radio";
1217
1263
  const inputName = `uq-${Date.now()}`;
1264
+ let customInput = null;
1265
+ let isCustomSelected = false;
1266
+ const submitBtn = document.createElement("button");
1267
+ submitBtn.className = "user-question-btn user-question-btn-submit";
1268
+ submitBtn.textContent = "Submit";
1269
+ submitBtn.disabled = true;
1270
+ const updateSubmitButton = () => {
1271
+ const hasSelection = selected.size > 0;
1272
+ const hasCustomValue = isCustomSelected && customInput && customInput.value.trim().length > 0;
1273
+ submitBtn.disabled = !hasSelection && !hasCustomValue;
1274
+ };
1218
1275
  choices.forEach((choice, index) => {
1219
1276
  const choiceDiv = document.createElement("div");
1220
1277
  choiceDiv.className = "user-question-choice";
@@ -1239,6 +1296,8 @@ var askUserExecutor = async (args2) => {
1239
1296
  }
1240
1297
  } else {
1241
1298
  selected.clear();
1299
+ isCustomSelected = false;
1300
+ if (customInput) customInput.value = "";
1242
1301
  selected.add(choice);
1243
1302
  choicesDiv.querySelectorAll(".user-question-choice").forEach((c) => c.classList.remove("selected"));
1244
1303
  choiceDiv.classList.add("selected");
@@ -1254,9 +1313,62 @@ var askUserExecutor = async (args2) => {
1254
1313
  });
1255
1314
  choicesDiv.appendChild(choiceDiv);
1256
1315
  });
1316
+ if (allowCustom) {
1317
+ const customChoiceDiv = document.createElement("div");
1318
+ customChoiceDiv.className = "user-question-choice";
1319
+ const customRadio = document.createElement("input");
1320
+ customRadio.type = inputType;
1321
+ customRadio.name = inputName;
1322
+ customRadio.id = `${inputName}-custom`;
1323
+ customRadio.value = "__custom__";
1324
+ const customLabel = document.createElement("label");
1325
+ customLabel.htmlFor = customRadio.id;
1326
+ customLabel.textContent = "Other:";
1327
+ customLabel.style.flexShrink = "0";
1328
+ customInput = document.createElement("input");
1329
+ customInput.type = "text";
1330
+ customInput.className = "user-question-custom-input";
1331
+ customInput.placeholder = "Type your answer...";
1332
+ customInput.style.marginTop = "0";
1333
+ customInput.style.marginLeft = "8px";
1334
+ customInput.style.flex = "1";
1335
+ customChoiceDiv.appendChild(customRadio);
1336
+ customChoiceDiv.appendChild(customLabel);
1337
+ customChoiceDiv.appendChild(customInput);
1338
+ const handleCustomSelect = () => {
1339
+ if (!allowMultiple) {
1340
+ selected.clear();
1341
+ choicesDiv.querySelectorAll(".user-question-choice").forEach((c) => c.classList.remove("selected"));
1342
+ }
1343
+ isCustomSelected = true;
1344
+ customChoiceDiv.classList.add("selected");
1345
+ customInput?.focus();
1346
+ updateSubmitButton();
1347
+ };
1348
+ customRadio.addEventListener("change", handleCustomSelect);
1349
+ customChoiceDiv.addEventListener("click", (e) => {
1350
+ if (e.target !== customRadio && e.target !== customInput) {
1351
+ customRadio.checked = true;
1352
+ handleCustomSelect();
1353
+ }
1354
+ });
1355
+ customInput.addEventListener("focus", () => {
1356
+ if (!customRadio.checked) {
1357
+ customRadio.checked = true;
1358
+ handleCustomSelect();
1359
+ }
1360
+ });
1361
+ customInput.addEventListener("input", updateSubmitButton);
1362
+ choicesDiv.appendChild(customChoiceDiv);
1363
+ }
1257
1364
  modal.appendChild(choicesDiv);
1258
1365
  const actions = document.createElement("div");
1259
1366
  actions.className = "user-question-actions";
1367
+ overlay.appendChild(modal);
1368
+ document.body.appendChild(overlay);
1369
+ const cleanup = () => {
1370
+ overlay.remove();
1371
+ };
1260
1372
  const cancelBtn = document.createElement("button");
1261
1373
  cancelBtn.className = "user-question-btn user-question-btn-cancel";
1262
1374
  cancelBtn.textContent = "Skip";
@@ -1268,29 +1380,21 @@ var askUserExecutor = async (args2) => {
1268
1380
  answered: false
1269
1381
  });
1270
1382
  };
1271
- const submitBtn = document.createElement("button");
1272
- submitBtn.className = "user-question-btn user-question-btn-submit";
1273
- submitBtn.textContent = "Submit";
1274
- submitBtn.disabled = true;
1275
1383
  submitBtn.onclick = () => {
1276
1384
  cleanup();
1385
+ const results = Array.from(selected);
1386
+ if (isCustomSelected && customInput && customInput.value.trim()) {
1387
+ results.push(customInput.value.trim());
1388
+ }
1277
1389
  resolve({
1278
1390
  question,
1279
- selectedChoices: Array.from(selected),
1391
+ selectedChoices: results,
1280
1392
  answered: true
1281
1393
  });
1282
1394
  };
1283
- const updateSubmitButton = () => {
1284
- submitBtn.disabled = selected.size === 0;
1285
- };
1286
1395
  actions.appendChild(cancelBtn);
1287
1396
  actions.appendChild(submitBtn);
1288
1397
  modal.appendChild(actions);
1289
- overlay.appendChild(modal);
1290
- document.body.appendChild(overlay);
1291
- const cleanup = () => {
1292
- overlay.remove();
1293
- };
1294
1398
  const handleEscape = (e) => {
1295
1399
  if (e.key === "Escape") {
1296
1400
  document.removeEventListener("keydown", handleEscape);
@@ -1459,6 +1563,11 @@ var screenshotTool = {
1459
1563
 
1460
1564
  The screenshot captures the visible viewport of the page. The image is uploaded to the server and a permanent URL is returned.
1461
1565
 
1566
+ IMPORTANT: Before taking a screenshot, use the ask_user tool (if available) to ask which size they prefer:
1567
+ - "full" (100%) - highest quality, larger file
1568
+ - "half" (50%) - good balance of quality and size
1569
+ - "quarter" (25%) - smallest file, faster upload
1570
+
1462
1571
  Use this when:
1463
1572
  - User asks to see what's on their screen
1464
1573
  - You need to analyze the current page visually
@@ -1469,12 +1578,24 @@ Use this when:
1469
1578
  selector: {
1470
1579
  type: "string",
1471
1580
  description: "Optional CSS selector to capture a specific element instead of the full page. Leave empty for full page screenshot."
1581
+ },
1582
+ size: {
1583
+ type: "string",
1584
+ enum: ["full", "half", "quarter"],
1585
+ description: 'Image size: "full" (100%), "half" (50%), or "quarter" (25%). Ask the user which size they prefer before capturing.'
1472
1586
  }
1473
1587
  }
1474
1588
  }
1475
1589
  };
1476
1590
  var screenshotExecutor = async (args2) => {
1477
1591
  const selector = args2.selector;
1592
+ const size = args2.size || "full";
1593
+ const scaleMap = {
1594
+ full: 1,
1595
+ half: 0.5,
1596
+ quarter: 0.25
1597
+ };
1598
+ const scale = scaleMap[size] || 1;
1478
1599
  if (typeof window === "undefined" || typeof document === "undefined") {
1479
1600
  return {
1480
1601
  success: false,
@@ -1495,34 +1616,41 @@ var screenshotExecutor = async (args2) => {
1495
1616
  if (!target) {
1496
1617
  return { success: false, error: "Element not found: " + selector };
1497
1618
  }
1498
- const canvas = await window.html2canvas(target, {
1619
+ const fullCanvas = await window.html2canvas(target, {
1499
1620
  useCORS: true,
1500
1621
  allowTaint: true,
1501
1622
  backgroundColor: "#000000",
1502
1623
  scale: 1
1503
1624
  });
1625
+ let finalCanvas = fullCanvas;
1626
+ if (scale < 1) {
1627
+ const resizedCanvas = document.createElement("canvas");
1628
+ resizedCanvas.width = Math.round(fullCanvas.width * scale);
1629
+ resizedCanvas.height = Math.round(fullCanvas.height * scale);
1630
+ const ctx = resizedCanvas.getContext("2d");
1631
+ if (ctx) {
1632
+ ctx.drawImage(fullCanvas, 0, 0, resizedCanvas.width, resizedCanvas.height);
1633
+ finalCanvas = resizedCanvas;
1634
+ }
1635
+ }
1504
1636
  const blob = await new Promise((resolve) => {
1505
- canvas.toBlob(resolve, "image/png", 0.9);
1637
+ finalCanvas.toBlob(resolve, "image/png", 0.9);
1506
1638
  });
1507
1639
  if (!blob) {
1508
1640
  return { success: false, error: "Failed to create image blob" };
1509
1641
  }
1510
- const formData = new FormData();
1511
- formData.append("file", blob, "screenshot-" + Date.now() + ".png");
1512
- const response = await fetch("/api/files/upload", {
1513
- method: "POST",
1514
- body: formData
1515
- });
1516
- if (!response.ok) {
1517
- const errorData = await response.json().catch(() => ({}));
1518
- return { success: false, error: errorData.error || "Upload failed" };
1642
+ const uploadFn = window.__hustleUploadFile;
1643
+ if (!uploadFn) {
1644
+ return { success: false, error: "Upload not available. Make sure HustleProvider is mounted and client is ready." };
1519
1645
  }
1520
- const data = await response.json();
1646
+ const fileName = "screenshot-" + Date.now() + ".png";
1647
+ const attachment = await uploadFn(blob, fileName);
1648
+ const sizeLabel = size === "full" ? "100%" : size === "half" ? "50%" : "25%";
1521
1649
  return {
1522
1650
  success: true,
1523
- url: data.url,
1524
- contentType: data.contentType,
1525
- message: "Screenshot captured and uploaded successfully"
1651
+ url: attachment.url,
1652
+ contentType: attachment.contentType || "image/png",
1653
+ message: `Screenshot captured at ${sizeLabel} size (${finalCanvas.width}x${finalCanvas.height}) and uploaded successfully`
1526
1654
  };
1527
1655
  } catch (e) {
1528
1656
  const err = e;
@@ -1590,21 +1718,89 @@ Write them as arrow function strings that will be eval'd:
1590
1718
  "async (args) => { const { city } = args; return { weather: 'sunny', city }; }"
1591
1719
 
1592
1720
  The function receives args as Record<string, unknown> and should return the result.
1593
- You can use fetch(), standard browser APIs, and async/await.
1721
+
1722
+ ## Available in Executor Scope
1723
+
1724
+ Executors run in the browser context with full access to:
1725
+
1726
+ ### Browser APIs
1727
+ - **fetch(url, options)** - HTTP requests (subject to CORS)
1728
+ - **localStorage / sessionStorage** - Persistent storage
1729
+ - **document** - Full DOM access (create elements, modals, forms, etc.)
1730
+ - **window** - Global window object
1731
+ - **console** - Logging (log, warn, error, etc.)
1732
+ - **setTimeout / setInterval / clearTimeout / clearInterval** - Timers
1733
+ - **JSON** - Parse and stringify
1734
+ - **Date** - Date/time operations
1735
+ - **URL / URLSearchParams** - URL manipulation
1736
+ - **FormData / Blob / File / FileReader** - File handling
1737
+ - **crypto** - Cryptographic operations (crypto.randomUUID(), etc.)
1738
+ - **navigator** - Browser info, clipboard, geolocation, etc.
1739
+ - **location** - Current URL info
1740
+ - **history** - Browser history navigation
1741
+ - **WebSocket** - Real-time bidirectional communication
1742
+ - **EventSource** - Server-sent events
1743
+ - **indexedDB** - Client-side database for large data
1744
+ - **Notification** - Browser notifications (requires permission)
1745
+ - **performance** - Performance timing
1746
+ - **atob / btoa** - Base64 encoding/decoding
1747
+ - **TextEncoder / TextDecoder** - Text encoding
1748
+ - **AbortController** - Cancel fetch requests
1749
+ - **IntersectionObserver / MutationObserver / ResizeObserver** - DOM observers
1750
+ - **requestAnimationFrame** - Animation timing
1751
+ - **speechSynthesis** - Text-to-speech
1752
+ - **Audio / Image / Canvas** - Media APIs
1753
+
1754
+ ### Hustle Plugin System Globals
1755
+ - **window.__hustleInstanceId** - Current Hustle instance ID
1756
+ - **window.__hustleRegisterPlugin(plugin, enabled)** - Install another plugin dynamically
1757
+ - **window.__hustleUnregisterPlugin(name)** - Uninstall a plugin by name
1758
+ - **window.__hustleUploadFile(file, fileName?)** - Upload a File/Blob to the server, returns { url, contentType }
1759
+ - **window.__hustleListPlugins()** - List all installed plugins
1760
+ - **window.__hustleGetPlugin(name)** - Get a specific plugin by name
1761
+
1762
+ ### DOM Manipulation Examples
1763
+ Create a modal: document.createElement('div'), style it, append to document.body
1764
+ Add event listeners: element.addEventListener('click', handler)
1765
+ Query elements: document.querySelector(), document.querySelectorAll()
1766
+
1767
+ ### Storage Patterns
1768
+ Store data: localStorage.setItem('key', JSON.stringify(data))
1769
+ Retrieve data: JSON.parse(localStorage.getItem('key') || '{}')
1770
+ Namespace your keys: Use plugin name prefix like "myplugin-settings"
1771
+
1772
+ ### Async Patterns
1773
+ All executors should be async. Use await for promises:
1774
+ "async (args) => { const res = await fetch(url); return await res.json(); }"
1594
1775
 
1595
1776
  ## Lifecycle Hooks (Optional)
1596
1777
 
1597
- - **beforeRequestCode**: Modify messages before sending. Receives request object, must return modified request.
1778
+ Hooks also have full access to the browser scope described above.
1779
+
1780
+ - **onRegisterCode**: Called once when plugin is registered/enabled. Good for initialization.
1781
+ **IMPORTANT: Always log when your plugin registers so users know it's active!**
1782
+ Example: "async () => { console.log('[MyPlugin] v1.0.0 registered'); }"
1783
+
1784
+ - **beforeRequestCode**: Modify messages before sending. Receives request object with { messages, model, ... }. Must return the modified request.
1598
1785
  Example: "async (req) => { req.messages = req.messages.map(m => ({...m, content: m.content.toUpperCase()})); return req; }"
1599
1786
 
1600
- - **afterResponseCode**: Process response after receiving. Receives response object.
1601
- Example: "async (res) => { console.log('Response:', res.content); }"
1787
+ - **afterResponseCode**: Process/modify response after receiving. Receives response object with { content, ... }.
1788
+ Example: "async (res) => { console.log('Response received:', res.content.substring(0, 100)); }"
1602
1789
 
1603
- - **onRegisterCode**: Called when plugin is registered.
1604
- Example: "async () => { console.log('Plugin loaded!'); }"
1790
+ - **onErrorCode**: Called on errors. Receives (error, context) where context has { phase: 'beforeRequest'|'execute'|'afterResponse' }.
1791
+ Example: "async (error, ctx) => { console.error('[MyPlugin] Error in', ctx.phase, ':', error.message); }"
1605
1792
 
1606
- - **onErrorCode**: Called on errors. Receives (error, context).
1607
- Example: "async (error, ctx) => { console.error('Error in', ctx.phase, error); }"`,
1793
+ ## Best Practices
1794
+
1795
+ 1. **Always add onRegisterCode** that logs the plugin name and version
1796
+ 2. **Namespace console logs** with [PluginName] prefix for easy identification
1797
+ 3. **Handle errors gracefully** in executors - return { error: message } instead of throwing
1798
+
1799
+ ## Security Notes
1800
+ - Code runs in browser sandbox with same-origin policy
1801
+ - fetch() is subject to CORS restrictions
1802
+ - No direct filesystem access (use File API with user interaction)
1803
+ - Be careful with eval() on user input`,
1608
1804
  parameters: {
1609
1805
  type: "object",
1610
1806
  properties: {
@@ -1693,6 +1889,104 @@ var installPluginTool = {
1693
1889
  required: ["plugin"]
1694
1890
  }
1695
1891
  };
1892
+ var uninstallPluginTool = {
1893
+ name: "uninstall_plugin",
1894
+ description: "Uninstall a plugin by name, removing it from browser storage.",
1895
+ parameters: {
1896
+ type: "object",
1897
+ properties: {
1898
+ name: {
1899
+ type: "string",
1900
+ description: "The name of the plugin to uninstall"
1901
+ }
1902
+ },
1903
+ required: ["name"]
1904
+ }
1905
+ };
1906
+ var listPluginsTool = {
1907
+ name: "list_plugins",
1908
+ description: "List all installed plugins with their enabled/disabled status.",
1909
+ parameters: {
1910
+ type: "object",
1911
+ properties: {},
1912
+ required: []
1913
+ }
1914
+ };
1915
+ var modifyPluginTool = {
1916
+ name: "modify_plugin",
1917
+ description: `Modify an existing installed plugin. Can update version, description, tools, executors, and hooks.
1918
+
1919
+ Use list_plugins first to see installed plugins, then modify by name.
1920
+
1921
+ You can:
1922
+ - Add new tools (provide tools array with new tools to add)
1923
+ - Update existing tools (provide tool with same name)
1924
+ - Remove tools (set removeTool to the tool name)
1925
+ - Update hooks (provide hook code)
1926
+ - Update version/description
1927
+
1928
+ Example: Add a new tool to existing plugin:
1929
+ {
1930
+ "name": "my-plugin",
1931
+ "addTools": [{ "name": "new_tool", "description": "...", "parameters": {...} }],
1932
+ "addExecutorCode": { "new_tool": "async (args) => { ... }" }
1933
+ }`,
1934
+ parameters: {
1935
+ type: "object",
1936
+ properties: {
1937
+ name: {
1938
+ type: "string",
1939
+ description: "Name of the plugin to modify (required)"
1940
+ },
1941
+ version: {
1942
+ type: "string",
1943
+ description: "New version string"
1944
+ },
1945
+ description: {
1946
+ type: "string",
1947
+ description: "New description"
1948
+ },
1949
+ addTools: {
1950
+ type: "array",
1951
+ description: "Tools to add or update",
1952
+ items: {
1953
+ type: "object",
1954
+ properties: {
1955
+ name: { type: "string" },
1956
+ description: { type: "string" },
1957
+ parameters: { type: "object" }
1958
+ }
1959
+ }
1960
+ },
1961
+ addExecutorCode: {
1962
+ type: "object",
1963
+ description: "Executor code to add/update (tool name -> code string)"
1964
+ },
1965
+ removeTools: {
1966
+ type: "array",
1967
+ description: "Names of tools to remove",
1968
+ items: { type: "string" }
1969
+ },
1970
+ onRegisterCode: {
1971
+ type: "string",
1972
+ description: "New onRegister hook code"
1973
+ },
1974
+ beforeRequestCode: {
1975
+ type: "string",
1976
+ description: "New beforeRequest hook code"
1977
+ },
1978
+ afterResponseCode: {
1979
+ type: "string",
1980
+ description: "New afterResponse hook code"
1981
+ },
1982
+ onErrorCode: {
1983
+ type: "string",
1984
+ description: "New onError hook code"
1985
+ }
1986
+ },
1987
+ required: ["name"]
1988
+ }
1989
+ };
1696
1990
  var buildPluginExecutor = async (args2) => {
1697
1991
  const {
1698
1992
  name,
@@ -1749,12 +2043,51 @@ var buildPluginExecutor = async (args2) => {
1749
2043
  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.`
1750
2044
  };
1751
2045
  };
2046
+ function normalizePlugin(input) {
2047
+ const plugin = input;
2048
+ const tools = plugin.tools;
2049
+ const hasEmbeddedExecutors = tools?.[0]?.executorCode !== void 0;
2050
+ if (hasEmbeddedExecutors) {
2051
+ return {
2052
+ ...plugin,
2053
+ enabled: plugin.enabled ?? true,
2054
+ installedAt: plugin.installedAt ?? (/* @__PURE__ */ new Date()).toISOString()
2055
+ };
2056
+ }
2057
+ const executorCode = plugin.executorCode;
2058
+ const rawTools = tools || [];
2059
+ const hookKeys = ["onRegisterCode", "beforeRequestCode", "afterResponseCode", "onErrorCode"];
2060
+ const hooksCode = {};
2061
+ for (const key of hookKeys) {
2062
+ if (executorCode?.[key]) {
2063
+ hooksCode[key] = executorCode[key];
2064
+ }
2065
+ if (plugin[key]) {
2066
+ hooksCode[key] = plugin[key];
2067
+ }
2068
+ }
2069
+ return {
2070
+ name: plugin.name,
2071
+ version: plugin.version,
2072
+ description: plugin.description,
2073
+ tools: rawTools.map((tool) => ({
2074
+ name: tool.name,
2075
+ description: tool.description,
2076
+ parameters: tool.parameters,
2077
+ executorCode: executorCode?.[tool.name]
2078
+ })),
2079
+ hooksCode: Object.keys(hooksCode).length > 0 ? hooksCode : void 0,
2080
+ enabled: true,
2081
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
2082
+ };
2083
+ }
1752
2084
  var savePluginExecutor = async (args2) => {
1753
- const { plugin, filename } = args2;
2085
+ const { plugin: rawPlugin, filename } = args2;
1754
2086
  if (typeof window === "undefined") {
1755
2087
  return { success: false, error: "Cannot save files in server environment" };
1756
2088
  }
1757
2089
  try {
2090
+ const plugin = normalizePlugin(rawPlugin);
1758
2091
  const json2 = JSON.stringify(plugin, null, 2);
1759
2092
  const blob = new Blob([json2], { type: "application/json" });
1760
2093
  const url = URL.createObjectURL(blob);
@@ -1777,31 +2110,23 @@ var savePluginExecutor = async (args2) => {
1777
2110
  }
1778
2111
  };
1779
2112
  var installPluginExecutor = async (args2) => {
1780
- const { plugin, enabled = true } = args2;
2113
+ const { plugin: rawPlugin, enabled = true } = args2;
1781
2114
  if (typeof window === "undefined") {
1782
2115
  return { success: false, error: "Cannot install plugins in server environment" };
1783
2116
  }
1784
2117
  try {
1785
- const instanceId = window.__hustleInstanceId || "global-demo";
1786
- const STORAGE_KEY = `hustle-plugins-${instanceId}`;
1787
- const stored = localStorage.getItem(STORAGE_KEY);
1788
- const plugins = stored ? JSON.parse(stored) : [];
1789
- const existingIndex = plugins.findIndex((p) => p.name === plugin.name);
1790
- const pluginToStore = {
1791
- ...plugin,
1792
- enabled,
1793
- installedAt: (/* @__PURE__ */ new Date()).toISOString()
1794
- };
1795
- if (existingIndex >= 0) {
1796
- plugins[existingIndex] = pluginToStore;
1797
- } else {
1798
- plugins.push(pluginToStore);
2118
+ const plugin = normalizePlugin(rawPlugin);
2119
+ const win = window;
2120
+ if (!win.__hustleRegisterPlugin) {
2121
+ return {
2122
+ success: false,
2123
+ error: "Plugin registration not available. Make sure HustleProvider is mounted."
2124
+ };
1799
2125
  }
1800
- localStorage.setItem(STORAGE_KEY, JSON.stringify(plugins));
2126
+ await win.__hustleRegisterPlugin(plugin, enabled);
1801
2127
  return {
1802
2128
  success: true,
1803
- message: `Plugin "${plugin.name}" installed${enabled ? " and enabled" : ""}. Refresh the page or re-register plugins to activate.`,
1804
- action: existingIndex >= 0 ? "updated" : "installed"
2129
+ message: `Plugin "${plugin.name}" installed${enabled ? " and enabled" : ""}.`
1805
2130
  };
1806
2131
  } catch (error2) {
1807
2132
  return {
@@ -1810,15 +2135,168 @@ var installPluginExecutor = async (args2) => {
1810
2135
  };
1811
2136
  }
1812
2137
  };
2138
+ var uninstallPluginExecutor = async (args2) => {
2139
+ const { name } = args2;
2140
+ if (typeof window === "undefined") {
2141
+ return { success: false, error: "Cannot uninstall plugins in server environment" };
2142
+ }
2143
+ try {
2144
+ const win = window;
2145
+ if (!win.__hustleUnregisterPlugin) {
2146
+ return {
2147
+ success: false,
2148
+ error: "Plugin unregistration not available. Make sure HustleProvider is mounted."
2149
+ };
2150
+ }
2151
+ await win.__hustleUnregisterPlugin(name);
2152
+ return {
2153
+ success: true,
2154
+ message: `Plugin "${name}" has been uninstalled.`
2155
+ };
2156
+ } catch (error2) {
2157
+ return {
2158
+ success: false,
2159
+ error: `Failed to uninstall: ${error2 instanceof Error ? error2.message : "Unknown error"}`
2160
+ };
2161
+ }
2162
+ };
2163
+ var listPluginsExecutor = async () => {
2164
+ if (typeof window === "undefined") {
2165
+ return { success: false, error: "Cannot list plugins in server environment" };
2166
+ }
2167
+ try {
2168
+ const win = window;
2169
+ if (!win.__hustleListPlugins) {
2170
+ return {
2171
+ success: false,
2172
+ error: "Plugin listing not available. Make sure HustleProvider is mounted."
2173
+ };
2174
+ }
2175
+ const plugins = win.__hustleListPlugins();
2176
+ return {
2177
+ success: true,
2178
+ plugins,
2179
+ 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(", ")}`
2180
+ };
2181
+ } catch (error2) {
2182
+ return {
2183
+ success: false,
2184
+ error: `Failed to list plugins: ${error2 instanceof Error ? error2.message : "Unknown error"}`
2185
+ };
2186
+ }
2187
+ };
2188
+ var modifyPluginExecutor = async (args2) => {
2189
+ const {
2190
+ name,
2191
+ version,
2192
+ description,
2193
+ addTools,
2194
+ addExecutorCode,
2195
+ removeTools,
2196
+ onRegisterCode,
2197
+ beforeRequestCode,
2198
+ afterResponseCode,
2199
+ onErrorCode
2200
+ } = args2;
2201
+ if (typeof window === "undefined") {
2202
+ return { success: false, error: "Cannot modify plugins in server environment" };
2203
+ }
2204
+ try {
2205
+ const win = window;
2206
+ if (!win.__hustleGetPlugin || !win.__hustleRegisterPlugin) {
2207
+ return {
2208
+ success: false,
2209
+ error: "Plugin modification not available. Make sure HustleProvider is mounted."
2210
+ };
2211
+ }
2212
+ const existing = win.__hustleGetPlugin(name);
2213
+ if (!existing) {
2214
+ return {
2215
+ success: false,
2216
+ error: `Plugin "${name}" not found. Use list_plugins to see installed plugins.`
2217
+ };
2218
+ }
2219
+ let tools = existing.tools ? [...existing.tools] : [];
2220
+ if (removeTools && removeTools.length > 0) {
2221
+ tools = tools.filter((t) => !removeTools.includes(t.name));
2222
+ }
2223
+ if (addTools && addTools.length > 0) {
2224
+ for (const newTool of addTools) {
2225
+ const existingIndex = tools.findIndex((t) => t.name === newTool.name);
2226
+ const toolWithExecutor = {
2227
+ ...newTool,
2228
+ executorCode: addExecutorCode?.[newTool.name] || tools.find((t) => t.name === newTool.name)?.executorCode
2229
+ };
2230
+ if (existingIndex >= 0) {
2231
+ tools[existingIndex] = toolWithExecutor;
2232
+ } else {
2233
+ tools.push(toolWithExecutor);
2234
+ }
2235
+ }
2236
+ }
2237
+ if (addExecutorCode) {
2238
+ for (const [toolName, code2] of Object.entries(addExecutorCode)) {
2239
+ const tool = tools.find((t) => t.name === toolName);
2240
+ if (tool) {
2241
+ tool.executorCode = code2;
2242
+ }
2243
+ }
2244
+ }
2245
+ const modified = {
2246
+ ...existing,
2247
+ tools
2248
+ };
2249
+ if (version) modified.version = version;
2250
+ if (description) modified.description = description;
2251
+ if (onRegisterCode || beforeRequestCode || afterResponseCode || onErrorCode) {
2252
+ modified.hooksCode = modified.hooksCode || {};
2253
+ if (onRegisterCode) modified.hooksCode.onRegisterCode = onRegisterCode;
2254
+ if (beforeRequestCode) modified.hooksCode.beforeRequestCode = beforeRequestCode;
2255
+ if (afterResponseCode) modified.hooksCode.afterResponseCode = afterResponseCode;
2256
+ if (onErrorCode) modified.hooksCode.onErrorCode = onErrorCode;
2257
+ }
2258
+ const unregisterFn = window.__hustleUnregisterPlugin;
2259
+ if (unregisterFn) {
2260
+ await unregisterFn(name);
2261
+ }
2262
+ await win.__hustleRegisterPlugin(modified, existing.enabled);
2263
+ const changes = [];
2264
+ if (version) changes.push(`version \u2192 ${version}`);
2265
+ if (description) changes.push("description updated");
2266
+ if (removeTools?.length) changes.push(`removed ${removeTools.length} tool(s)`);
2267
+ if (addTools?.length) changes.push(`added/updated ${addTools.length} tool(s)`);
2268
+ if (onRegisterCode) changes.push("onRegister hook updated");
2269
+ if (beforeRequestCode) changes.push("beforeRequest hook updated");
2270
+ if (afterResponseCode) changes.push("afterResponse hook updated");
2271
+ if (onErrorCode) changes.push("onError hook updated");
2272
+ return {
2273
+ success: true,
2274
+ message: `Plugin "${name}" modified: ${changes.join(", ")}`,
2275
+ plugin: {
2276
+ name: modified.name,
2277
+ version: modified.version,
2278
+ toolCount: tools.length
2279
+ }
2280
+ };
2281
+ } catch (error2) {
2282
+ return {
2283
+ success: false,
2284
+ error: `Failed to modify plugin: ${error2 instanceof Error ? error2.message : "Unknown error"}`
2285
+ };
2286
+ }
2287
+ };
1813
2288
  var pluginBuilderPlugin = {
1814
2289
  name: "plugin-builder",
1815
- version: "1.0.0",
2290
+ version: "1.2.0",
1816
2291
  description: "Build custom plugins through conversation",
1817
- tools: [buildPluginTool, savePluginTool, installPluginTool],
2292
+ tools: [buildPluginTool, savePluginTool, installPluginTool, uninstallPluginTool, listPluginsTool, modifyPluginTool],
1818
2293
  executors: {
1819
2294
  build_plugin: buildPluginExecutor,
1820
2295
  save_plugin: savePluginExecutor,
1821
- install_plugin: installPluginExecutor
2296
+ install_plugin: installPluginExecutor,
2297
+ uninstall_plugin: uninstallPluginExecutor,
2298
+ list_plugins: listPluginsExecutor,
2299
+ modify_plugin: modifyPluginExecutor
1822
2300
  },
1823
2301
  hooks: {
1824
2302
  onRegister: () => {