@emblemvault/hustle-react 1.1.0 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  'use strict';
2
3
 
3
4
  var react = require('react');
@@ -51,6 +52,8 @@ var ruby__default = /*#__PURE__*/_interopDefault(ruby);
51
52
  var swift__default = /*#__PURE__*/_interopDefault(swift);
52
53
  var kotlin__default = /*#__PURE__*/_interopDefault(kotlin);
53
54
 
55
+ // src/providers/HustleProvider.tsx
56
+
54
57
  // src/utils/pluginRegistry.ts
55
58
  var PLUGINS_KEY = "hustle-plugins";
56
59
  function getEnabledStateKey(instanceId) {
@@ -302,35 +305,52 @@ var pluginRegistry = new PluginRegistry();
302
305
  function getStorageKey(instanceId) {
303
306
  return `hustle-plugins-${instanceId}`;
304
307
  }
305
- function usePlugins(instanceId = "default") {
308
+ function getInstanceId(providedId) {
309
+ if (providedId) return providedId;
310
+ if (typeof window !== "undefined") {
311
+ const globalId = window.__hustleInstanceId;
312
+ if (globalId) return globalId;
313
+ }
314
+ return "default";
315
+ }
316
+ function usePlugins(instanceId) {
317
+ const [resolvedInstanceId] = react.useState(() => getInstanceId(instanceId));
306
318
  const [plugins, setPlugins] = react.useState([]);
307
319
  react.useEffect(() => {
308
- setPlugins(pluginRegistry.loadFromStorage(instanceId));
309
- const unsubscribe = pluginRegistry.onChange(setPlugins, instanceId);
310
- const storageKey = getStorageKey(instanceId);
320
+ setPlugins(pluginRegistry.loadFromStorage(resolvedInstanceId));
321
+ const unsubscribe = pluginRegistry.onChange(setPlugins, resolvedInstanceId);
322
+ const storageKey = getStorageKey(resolvedInstanceId);
311
323
  const handleStorage = (e) => {
312
324
  if (e.key === storageKey) {
313
- setPlugins(pluginRegistry.loadFromStorage(instanceId));
325
+ setPlugins(pluginRegistry.loadFromStorage(resolvedInstanceId));
314
326
  }
315
327
  };
316
328
  window.addEventListener("storage", handleStorage);
329
+ const handlePluginInstalled = (e) => {
330
+ const customEvent = e;
331
+ if (customEvent.detail.instanceId === resolvedInstanceId) {
332
+ setPlugins(pluginRegistry.loadFromStorage(resolvedInstanceId));
333
+ }
334
+ };
335
+ window.addEventListener("hustle-plugin-installed", handlePluginInstalled);
317
336
  return () => {
318
337
  unsubscribe();
319
338
  window.removeEventListener("storage", handleStorage);
339
+ window.removeEventListener("hustle-plugin-installed", handlePluginInstalled);
320
340
  };
321
- }, [instanceId]);
341
+ }, [resolvedInstanceId]);
322
342
  const registerPlugin = react.useCallback((plugin) => {
323
- pluginRegistry.register(plugin, true, instanceId);
324
- }, [instanceId]);
343
+ pluginRegistry.register(plugin, true, resolvedInstanceId);
344
+ }, [resolvedInstanceId]);
325
345
  const unregisterPlugin = react.useCallback((name) => {
326
- pluginRegistry.unregister(name, instanceId);
327
- }, [instanceId]);
346
+ pluginRegistry.unregister(name, resolvedInstanceId);
347
+ }, [resolvedInstanceId]);
328
348
  const enablePlugin = react.useCallback((name) => {
329
- pluginRegistry.setEnabled(name, true, instanceId);
330
- }, [instanceId]);
349
+ pluginRegistry.setEnabled(name, true, resolvedInstanceId);
350
+ }, [resolvedInstanceId]);
331
351
  const disablePlugin = react.useCallback((name) => {
332
- pluginRegistry.setEnabled(name, false, instanceId);
333
- }, [instanceId]);
352
+ pluginRegistry.setEnabled(name, false, resolvedInstanceId);
353
+ }, [resolvedInstanceId]);
334
354
  const isRegistered = react.useCallback(
335
355
  (name) => plugins.some((p) => p.name === name),
336
356
  [plugins]
@@ -385,6 +405,42 @@ function HustleProvider({
385
405
  }
386
406
  };
387
407
  }, [isAutoInstance, resolvedInstanceId]);
408
+ react.useEffect(() => {
409
+ if (typeof window !== "undefined") {
410
+ const win = window;
411
+ win.__hustleInstanceId = resolvedInstanceId;
412
+ win.__hustleRegisterPlugin = async (plugin, enabled = true) => {
413
+ const hydrated = hydratePlugin(plugin);
414
+ pluginRegistry.register(hydrated, enabled, resolvedInstanceId);
415
+ };
416
+ win.__hustleUnregisterPlugin = async (name) => {
417
+ pluginRegistry.unregister(name, resolvedInstanceId);
418
+ };
419
+ win.__hustleListPlugins = () => {
420
+ const plugins = pluginRegistry.loadFromStorage(resolvedInstanceId);
421
+ return plugins.map((p) => ({
422
+ name: p.name,
423
+ version: p.version,
424
+ description: p.description || "",
425
+ enabled: p.enabled
426
+ }));
427
+ };
428
+ win.__hustleGetPlugin = (name) => {
429
+ const plugins = pluginRegistry.loadFromStorage(resolvedInstanceId);
430
+ return plugins.find((p) => p.name === name) || null;
431
+ };
432
+ }
433
+ return () => {
434
+ if (typeof window !== "undefined") {
435
+ const win = window;
436
+ delete win.__hustleInstanceId;
437
+ delete win.__hustleRegisterPlugin;
438
+ delete win.__hustleUnregisterPlugin;
439
+ delete win.__hustleListPlugins;
440
+ delete win.__hustleGetPlugin;
441
+ }
442
+ };
443
+ }, [resolvedInstanceId]);
388
444
  const isApiKeyMode = Boolean(apiKey && vaultId);
389
445
  const authContext = emblemAuthReact.useEmblemAuthOptional();
390
446
  const authSDK = isApiKeyMode ? null : authContext?.authSDK ?? null;
@@ -532,6 +588,19 @@ function HustleProvider({
532
588
  };
533
589
  registerPlugins();
534
590
  }, [client, enabledPlugins, log]);
591
+ react.useEffect(() => {
592
+ if (typeof window !== "undefined" && client) {
593
+ const win = window;
594
+ win.__hustleUploadFile = async (file, fileName) => {
595
+ return await client.uploadFile(file, fileName);
596
+ };
597
+ }
598
+ return () => {
599
+ if (typeof window !== "undefined") {
600
+ delete window.__hustleUploadFile;
601
+ }
602
+ };
603
+ }, [client]);
535
604
  const loadModels = react.useCallback(async () => {
536
605
  if (!client) {
537
606
  log("Cannot load models - client not ready");
@@ -1412,12 +1481,16 @@ function ensureModalStyles() {
1412
1481
  left: 0;
1413
1482
  right: 0;
1414
1483
  bottom: 0;
1415
- background: rgba(0, 0, 0, 0.6);
1484
+ background: transparent;
1416
1485
  display: flex;
1417
1486
  align-items: center;
1418
1487
  justify-content: center;
1419
1488
  z-index: 10000;
1420
- animation: uqFadeIn 0.2s ease-out;
1489
+ pointer-events: none;
1490
+ }
1491
+
1492
+ .user-question-modal {
1493
+ pointer-events: auto;
1421
1494
  }
1422
1495
 
1423
1496
  @keyframes uqFadeIn {
@@ -1530,6 +1603,27 @@ function ensureModalStyles() {
1530
1603
  color: #666;
1531
1604
  cursor: not-allowed;
1532
1605
  }
1606
+
1607
+ .user-question-custom-input {
1608
+ width: 100%;
1609
+ padding: 8px 12px;
1610
+ margin-top: 8px;
1611
+ background: #1a1a2e;
1612
+ border: 1px solid #444;
1613
+ border-radius: 6px;
1614
+ color: #e0e0e0;
1615
+ font-size: 14px;
1616
+ box-sizing: border-box;
1617
+ }
1618
+
1619
+ .user-question-custom-input:focus {
1620
+ outline: none;
1621
+ border-color: #4a7aff;
1622
+ }
1623
+
1624
+ .user-question-custom-input::placeholder {
1625
+ color: #666;
1626
+ }
1533
1627
  `;
1534
1628
  document.head.appendChild(styles2);
1535
1629
  }
@@ -1551,13 +1645,17 @@ var askUserTool = {
1551
1645
  allowMultiple: {
1552
1646
  type: "boolean",
1553
1647
  description: "If true, user can select multiple choices. Default: false"
1648
+ },
1649
+ allowCustom: {
1650
+ type: "boolean",
1651
+ description: 'If true, adds an "Other" option where user can type a custom response. Default: false'
1554
1652
  }
1555
1653
  },
1556
1654
  required: ["question", "choices"]
1557
1655
  }
1558
1656
  };
1559
1657
  var askUserExecutor = async (args2) => {
1560
- const { question, choices, allowMultiple = false } = args2;
1658
+ const { question, choices, allowMultiple = false, allowCustom = false } = args2;
1561
1659
  if (!question || !choices || !Array.isArray(choices) || choices.length === 0) {
1562
1660
  return {
1563
1661
  question: question || "",
@@ -1589,6 +1687,17 @@ var askUserExecutor = async (args2) => {
1589
1687
  choicesDiv.className = "user-question-choices";
1590
1688
  const inputType = allowMultiple ? "checkbox" : "radio";
1591
1689
  const inputName = `uq-${Date.now()}`;
1690
+ let customInput = null;
1691
+ let isCustomSelected = false;
1692
+ const submitBtn = document.createElement("button");
1693
+ submitBtn.className = "user-question-btn user-question-btn-submit";
1694
+ submitBtn.textContent = "Submit";
1695
+ submitBtn.disabled = true;
1696
+ const updateSubmitButton = () => {
1697
+ const hasSelection = selected.size > 0;
1698
+ const hasCustomValue = isCustomSelected && customInput && customInput.value.trim().length > 0;
1699
+ submitBtn.disabled = !hasSelection && !hasCustomValue;
1700
+ };
1592
1701
  choices.forEach((choice, index) => {
1593
1702
  const choiceDiv = document.createElement("div");
1594
1703
  choiceDiv.className = "user-question-choice";
@@ -1613,6 +1722,8 @@ var askUserExecutor = async (args2) => {
1613
1722
  }
1614
1723
  } else {
1615
1724
  selected.clear();
1725
+ isCustomSelected = false;
1726
+ if (customInput) customInput.value = "";
1616
1727
  selected.add(choice);
1617
1728
  choicesDiv.querySelectorAll(".user-question-choice").forEach((c) => c.classList.remove("selected"));
1618
1729
  choiceDiv.classList.add("selected");
@@ -1628,9 +1739,62 @@ var askUserExecutor = async (args2) => {
1628
1739
  });
1629
1740
  choicesDiv.appendChild(choiceDiv);
1630
1741
  });
1742
+ if (allowCustom) {
1743
+ const customChoiceDiv = document.createElement("div");
1744
+ customChoiceDiv.className = "user-question-choice";
1745
+ const customRadio = document.createElement("input");
1746
+ customRadio.type = inputType;
1747
+ customRadio.name = inputName;
1748
+ customRadio.id = `${inputName}-custom`;
1749
+ customRadio.value = "__custom__";
1750
+ const customLabel = document.createElement("label");
1751
+ customLabel.htmlFor = customRadio.id;
1752
+ customLabel.textContent = "Other:";
1753
+ customLabel.style.flexShrink = "0";
1754
+ customInput = document.createElement("input");
1755
+ customInput.type = "text";
1756
+ customInput.className = "user-question-custom-input";
1757
+ customInput.placeholder = "Type your answer...";
1758
+ customInput.style.marginTop = "0";
1759
+ customInput.style.marginLeft = "8px";
1760
+ customInput.style.flex = "1";
1761
+ customChoiceDiv.appendChild(customRadio);
1762
+ customChoiceDiv.appendChild(customLabel);
1763
+ customChoiceDiv.appendChild(customInput);
1764
+ const handleCustomSelect = () => {
1765
+ if (!allowMultiple) {
1766
+ selected.clear();
1767
+ choicesDiv.querySelectorAll(".user-question-choice").forEach((c) => c.classList.remove("selected"));
1768
+ }
1769
+ isCustomSelected = true;
1770
+ customChoiceDiv.classList.add("selected");
1771
+ customInput?.focus();
1772
+ updateSubmitButton();
1773
+ };
1774
+ customRadio.addEventListener("change", handleCustomSelect);
1775
+ customChoiceDiv.addEventListener("click", (e) => {
1776
+ if (e.target !== customRadio && e.target !== customInput) {
1777
+ customRadio.checked = true;
1778
+ handleCustomSelect();
1779
+ }
1780
+ });
1781
+ customInput.addEventListener("focus", () => {
1782
+ if (!customRadio.checked) {
1783
+ customRadio.checked = true;
1784
+ handleCustomSelect();
1785
+ }
1786
+ });
1787
+ customInput.addEventListener("input", updateSubmitButton);
1788
+ choicesDiv.appendChild(customChoiceDiv);
1789
+ }
1631
1790
  modal.appendChild(choicesDiv);
1632
1791
  const actions = document.createElement("div");
1633
1792
  actions.className = "user-question-actions";
1793
+ overlay.appendChild(modal);
1794
+ document.body.appendChild(overlay);
1795
+ const cleanup = () => {
1796
+ overlay.remove();
1797
+ };
1634
1798
  const cancelBtn = document.createElement("button");
1635
1799
  cancelBtn.className = "user-question-btn user-question-btn-cancel";
1636
1800
  cancelBtn.textContent = "Skip";
@@ -1642,29 +1806,21 @@ var askUserExecutor = async (args2) => {
1642
1806
  answered: false
1643
1807
  });
1644
1808
  };
1645
- const submitBtn = document.createElement("button");
1646
- submitBtn.className = "user-question-btn user-question-btn-submit";
1647
- submitBtn.textContent = "Submit";
1648
- submitBtn.disabled = true;
1649
1809
  submitBtn.onclick = () => {
1650
1810
  cleanup();
1811
+ const results = Array.from(selected);
1812
+ if (isCustomSelected && customInput && customInput.value.trim()) {
1813
+ results.push(customInput.value.trim());
1814
+ }
1651
1815
  resolve({
1652
1816
  question,
1653
- selectedChoices: Array.from(selected),
1817
+ selectedChoices: results,
1654
1818
  answered: true
1655
1819
  });
1656
1820
  };
1657
- const updateSubmitButton = () => {
1658
- submitBtn.disabled = selected.size === 0;
1659
- };
1660
1821
  actions.appendChild(cancelBtn);
1661
1822
  actions.appendChild(submitBtn);
1662
1823
  modal.appendChild(actions);
1663
- overlay.appendChild(modal);
1664
- document.body.appendChild(overlay);
1665
- const cleanup = () => {
1666
- overlay.remove();
1667
- };
1668
1824
  const handleEscape = (e) => {
1669
1825
  if (e.key === "Escape") {
1670
1826
  document.removeEventListener("keydown", handleEscape);
@@ -1833,6 +1989,11 @@ var screenshotTool = {
1833
1989
 
1834
1990
  The screenshot captures the visible viewport of the page. The image is uploaded to the server and a permanent URL is returned.
1835
1991
 
1992
+ IMPORTANT: Before taking a screenshot, use the ask_user tool (if available) to ask which size they prefer:
1993
+ - "full" (100%) - highest quality, larger file
1994
+ - "half" (50%) - good balance of quality and size
1995
+ - "quarter" (25%) - smallest file, faster upload
1996
+
1836
1997
  Use this when:
1837
1998
  - User asks to see what's on their screen
1838
1999
  - You need to analyze the current page visually
@@ -1843,12 +2004,24 @@ Use this when:
1843
2004
  selector: {
1844
2005
  type: "string",
1845
2006
  description: "Optional CSS selector to capture a specific element instead of the full page. Leave empty for full page screenshot."
2007
+ },
2008
+ size: {
2009
+ type: "string",
2010
+ enum: ["full", "half", "quarter"],
2011
+ description: 'Image size: "full" (100%), "half" (50%), or "quarter" (25%). Ask the user which size they prefer before capturing.'
1846
2012
  }
1847
2013
  }
1848
2014
  }
1849
2015
  };
1850
2016
  var screenshotExecutor = async (args2) => {
1851
2017
  const selector = args2.selector;
2018
+ const size = args2.size || "full";
2019
+ const scaleMap = {
2020
+ full: 1,
2021
+ half: 0.5,
2022
+ quarter: 0.25
2023
+ };
2024
+ const scale = scaleMap[size] || 1;
1852
2025
  if (typeof window === "undefined" || typeof document === "undefined") {
1853
2026
  return {
1854
2027
  success: false,
@@ -1869,34 +2042,41 @@ var screenshotExecutor = async (args2) => {
1869
2042
  if (!target) {
1870
2043
  return { success: false, error: "Element not found: " + selector };
1871
2044
  }
1872
- const canvas = await window.html2canvas(target, {
2045
+ const fullCanvas = await window.html2canvas(target, {
1873
2046
  useCORS: true,
1874
2047
  allowTaint: true,
1875
2048
  backgroundColor: "#000000",
1876
2049
  scale: 1
1877
2050
  });
2051
+ let finalCanvas = fullCanvas;
2052
+ if (scale < 1) {
2053
+ const resizedCanvas = document.createElement("canvas");
2054
+ resizedCanvas.width = Math.round(fullCanvas.width * scale);
2055
+ resizedCanvas.height = Math.round(fullCanvas.height * scale);
2056
+ const ctx = resizedCanvas.getContext("2d");
2057
+ if (ctx) {
2058
+ ctx.drawImage(fullCanvas, 0, 0, resizedCanvas.width, resizedCanvas.height);
2059
+ finalCanvas = resizedCanvas;
2060
+ }
2061
+ }
1878
2062
  const blob = await new Promise((resolve) => {
1879
- canvas.toBlob(resolve, "image/png", 0.9);
2063
+ finalCanvas.toBlob(resolve, "image/png", 0.9);
1880
2064
  });
1881
2065
  if (!blob) {
1882
2066
  return { success: false, error: "Failed to create image blob" };
1883
2067
  }
1884
- const formData = new FormData();
1885
- formData.append("file", blob, "screenshot-" + Date.now() + ".png");
1886
- const response = await fetch("/api/files/upload", {
1887
- method: "POST",
1888
- body: formData
1889
- });
1890
- if (!response.ok) {
1891
- const errorData = await response.json().catch(() => ({}));
1892
- return { success: false, error: errorData.error || "Upload failed" };
2068
+ const uploadFn = window.__hustleUploadFile;
2069
+ if (!uploadFn) {
2070
+ return { success: false, error: "Upload not available. Make sure HustleProvider is mounted and client is ready." };
1893
2071
  }
1894
- const data = await response.json();
2072
+ const fileName = "screenshot-" + Date.now() + ".png";
2073
+ const attachment = await uploadFn(blob, fileName);
2074
+ const sizeLabel = size === "full" ? "100%" : size === "half" ? "50%" : "25%";
1895
2075
  return {
1896
2076
  success: true,
1897
- url: data.url,
1898
- contentType: data.contentType,
1899
- message: "Screenshot captured and uploaded successfully"
2077
+ url: attachment.url,
2078
+ contentType: attachment.contentType || "image/png",
2079
+ message: `Screenshot captured at ${sizeLabel} size (${finalCanvas.width}x${finalCanvas.height}) and uploaded successfully`
1900
2080
  };
1901
2081
  } catch (e) {
1902
2082
  const err = e;
@@ -1926,6 +2106,21 @@ var buildPluginTool = {
1926
2106
  name: "build_plugin",
1927
2107
  description: `Build a Hustle plugin definition. Use this tool to construct a plugin based on user requirements.
1928
2108
 
2109
+ ## Testing Before Building (IMPORTANT)
2110
+
2111
+ If the execute_javascript tool is available, use it to prototype and test code BEFORE building the plugin. This allows rapid iteration without the overhead of building/installing/uninstalling.
2112
+
2113
+ **Workflow:**
2114
+ 1. When a user describes what they want, first test the core logic using execute_javascript
2115
+ 2. Iterate on the code until it works correctly
2116
+ 3. Ask the user: "Would you like to test this further, or should I build it into a plugin?"
2117
+ 4. Only call build_plugin once the code is validated and the user confirms
2118
+
2119
+ **Example:** If building a weather plugin, first test the API call:
2120
+ execute_javascript({ code: "fetch('https://api.example.com/weather?city=London').then(r => r.json()).then(console.log)" })
2121
+
2122
+ This catches errors early and lets users see results before committing to a plugin.
2123
+
1929
2124
  ## Plugin Structure
1930
2125
 
1931
2126
  A plugin consists of:
@@ -1964,21 +2159,127 @@ Write them as arrow function strings that will be eval'd:
1964
2159
  "async (args) => { const { city } = args; return { weather: 'sunny', city }; }"
1965
2160
 
1966
2161
  The function receives args as Record<string, unknown> and should return the result.
1967
- You can use fetch(), standard browser APIs, and async/await.
2162
+
2163
+ ## Available in Executor Scope
2164
+
2165
+ Executors run in the browser context with full access to:
2166
+
2167
+ ### Browser APIs
2168
+ - **fetch(url, options)** - HTTP requests (subject to CORS)
2169
+ - **localStorage / sessionStorage** - Persistent storage
2170
+ - **document** - Full DOM access (create elements, modals, forms, etc.)
2171
+ - **window** - Global window object
2172
+ - **console** - Logging (log, warn, error, etc.)
2173
+ - **setTimeout / setInterval / clearTimeout / clearInterval** - Timers
2174
+ - **JSON** - Parse and stringify
2175
+ - **Date** - Date/time operations
2176
+ - **URL / URLSearchParams** - URL manipulation
2177
+ - **FormData / Blob / File / FileReader** - File handling
2178
+ - **crypto** - Cryptographic operations (crypto.randomUUID(), etc.)
2179
+ - **navigator** - Browser info, clipboard, geolocation, etc.
2180
+ - **location** - Current URL info
2181
+ - **history** - Browser history navigation
2182
+ - **WebSocket** - Real-time bidirectional communication
2183
+ - **EventSource** - Server-sent events
2184
+ - **indexedDB** - Client-side database for large data
2185
+ - **Notification** - Browser notifications (requires permission)
2186
+ - **performance** - Performance timing
2187
+ - **atob / btoa** - Base64 encoding/decoding
2188
+ - **TextEncoder / TextDecoder** - Text encoding
2189
+ - **AbortController** - Cancel fetch requests
2190
+ - **IntersectionObserver / MutationObserver / ResizeObserver** - DOM observers
2191
+ - **requestAnimationFrame** - Animation timing
2192
+ - **speechSynthesis** - Text-to-speech
2193
+ - **Audio / Image / Canvas** - Media APIs
2194
+
2195
+ ### Hustle Plugin System Globals
2196
+ - **window.__hustleInstanceId** - Current Hustle instance ID
2197
+ - **window.__hustleRegisterPlugin(plugin, enabled)** - Install another plugin dynamically
2198
+ - **window.__hustleUnregisterPlugin(name)** - Uninstall a plugin by name
2199
+ - **window.__hustleUploadFile(file, fileName?)** - Upload a File/Blob to the server, returns { url, contentType }
2200
+ - **window.__hustleListPlugins()** - List all installed plugins
2201
+ - **window.__hustleGetPlugin(name)** - Get a specific plugin by name
2202
+
2203
+ ### DOM Manipulation Examples
2204
+ Create a modal: document.createElement('div'), style it, append to document.body
2205
+ Add event listeners: element.addEventListener('click', handler)
2206
+ Query elements: document.querySelector(), document.querySelectorAll()
2207
+
2208
+ ### Storage Patterns
2209
+ Store data: localStorage.setItem('key', JSON.stringify(data))
2210
+ Retrieve data: JSON.parse(localStorage.getItem('key') || '{}')
2211
+ Namespace your keys: Use plugin name prefix like "myplugin-settings"
2212
+
2213
+ ### Async Patterns
2214
+ All executors should be async. Use await for promises:
2215
+ "async (args) => { const res = await fetch(url); return await res.json(); }"
1968
2216
 
1969
2217
  ## Lifecycle Hooks (Optional)
1970
2218
 
1971
- - **beforeRequestCode**: Modify messages before sending. Receives request object, must return modified request.
2219
+ Hooks also have full access to the browser scope described above.
2220
+
2221
+ - **onRegisterCode**: Called once when plugin is registered/enabled. Good for initialization.
2222
+ **IMPORTANT: Always log when your plugin registers so users know it's active!**
2223
+ Example: "async () => { console.log('[MyPlugin] v1.0.0 registered'); }"
2224
+
2225
+ - **beforeRequestCode**: Modify messages before sending. Receives request object with { messages, model, ... }. Must return the modified request.
1972
2226
  Example: "async (req) => { req.messages = req.messages.map(m => ({...m, content: m.content.toUpperCase()})); return req; }"
1973
2227
 
1974
- - **afterResponseCode**: Process response after receiving. Receives response object.
1975
- Example: "async (res) => { console.log('Response:', res.content); }"
2228
+ - **afterResponseCode**: Process/modify response after receiving. Receives response object with { content, ... }.
2229
+ Example: "async (res) => { console.log('Response received:', res.content.substring(0, 100)); }"
2230
+
2231
+ - **onErrorCode**: Called on errors. Receives (error, context) where context has { phase: 'beforeRequest'|'execute'|'afterResponse' }.
2232
+ Example: "async (error, ctx) => { console.error('[MyPlugin] Error in', ctx.phase, ':', error.message); }"
2233
+
2234
+ ## Best Practices
2235
+
2236
+ 1. **Always add onRegisterCode** that logs the plugin name and version
2237
+ 2. **Namespace console logs** with [PluginName] prefix for easy identification
2238
+ 3. **Handle errors gracefully** in executors - return { error: message } instead of throwing
1976
2239
 
1977
- - **onRegisterCode**: Called when plugin is registered.
1978
- Example: "async () => { console.log('Plugin loaded!'); }"
2240
+ ## Building Plugin UI
1979
2241
 
1980
- - **onErrorCode**: Called on errors. Receives (error, context).
1981
- Example: "async (error, ctx) => { console.error('Error in', ctx.phase, error); }"`,
2242
+ Plugins can embed custom UI elements in two ways:
2243
+
2244
+ ### Persistent UI (via onRegister hook)
2245
+ Use the onRegister hook to embed UI that persists across the chat session. Check if your element already exists to avoid duplicates on re-registration:
2246
+
2247
+ "async () => {
2248
+ console.log('[MyPlugin] v1.0.0 registered');
2249
+ if (document.getElementById('my-plugin-panel')) return; // Already exists
2250
+
2251
+ const panel = document.createElement('div');
2252
+ panel.id = 'my-plugin-panel';
2253
+ Object.assign(panel.style, { position: 'fixed', bottom: '20px', right: '20px', padding: '10px', background: '#333', color: '#fff' });
2254
+ panel.textContent = 'My Panel';
2255
+ document.body.appendChild(panel);
2256
+ }"
2257
+
2258
+ ### On-Demand UI (via tool executors)
2259
+ Embed UI just-in-time when a tool is invoked. Useful for tools like show_clock, display_chart, etc.:
2260
+
2261
+ "async (args) => {
2262
+ const modal = document.createElement('div');
2263
+ modal.id = 'clock-modal';
2264
+ Object.assign(modal.style, { position: 'fixed', inset: '0', display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'rgba(0,0,0,0.5)' });
2265
+ modal.textContent = 'Current time: ' + new Date().toLocaleTimeString();
2266
+ modal.onclick = () => modal.remove(); // Click to dismiss
2267
+ document.body.appendChild(modal);
2268
+ return { success: true, message: 'Clock displayed' };
2269
+ }"
2270
+
2271
+ ### UI Best Practices
2272
+ - Always use unique IDs with your plugin name prefix (e.g., "myplugin-modal")
2273
+ - For persistent UI, check existence in onRegister before creating
2274
+ - Provide a way to dismiss/close UI elements (click handler, close button)
2275
+ - Use Object.assign(el.style, {...}) for inline styles or inject a <style> tag
2276
+ - Clean up UI in tool executors if appropriate (e.g., remove after timeout)
2277
+
2278
+ ## Security Notes
2279
+ - Code runs in browser sandbox with same-origin policy
2280
+ - fetch() is subject to CORS restrictions
2281
+ - No direct filesystem access (use File API with user interaction)
2282
+ - Sanitize any user-provided content before inserting into the DOM`,
1982
2283
  parameters: {
1983
2284
  type: "object",
1984
2285
  properties: {
@@ -2067,6 +2368,104 @@ var installPluginTool = {
2067
2368
  required: ["plugin"]
2068
2369
  }
2069
2370
  };
2371
+ var uninstallPluginTool = {
2372
+ name: "uninstall_plugin",
2373
+ description: "Uninstall a plugin by name, removing it from browser storage.",
2374
+ parameters: {
2375
+ type: "object",
2376
+ properties: {
2377
+ name: {
2378
+ type: "string",
2379
+ description: "The name of the plugin to uninstall"
2380
+ }
2381
+ },
2382
+ required: ["name"]
2383
+ }
2384
+ };
2385
+ var listPluginsTool = {
2386
+ name: "list_plugins",
2387
+ description: "List all installed plugins with their enabled/disabled status.",
2388
+ parameters: {
2389
+ type: "object",
2390
+ properties: {},
2391
+ required: []
2392
+ }
2393
+ };
2394
+ var modifyPluginTool = {
2395
+ name: "modify_plugin",
2396
+ description: `Modify an existing installed plugin. Can update version, description, tools, executors, and hooks.
2397
+
2398
+ Use list_plugins first to see installed plugins, then modify by name.
2399
+
2400
+ You can:
2401
+ - Add new tools (provide tools array with new tools to add)
2402
+ - Update existing tools (provide tool with same name)
2403
+ - Remove tools (set removeTool to the tool name)
2404
+ - Update hooks (provide hook code)
2405
+ - Update version/description
2406
+
2407
+ Example: Add a new tool to existing plugin:
2408
+ {
2409
+ "name": "my-plugin",
2410
+ "addTools": [{ "name": "new_tool", "description": "...", "parameters": {...} }],
2411
+ "addExecutorCode": { "new_tool": "async (args) => { ... }" }
2412
+ }`,
2413
+ parameters: {
2414
+ type: "object",
2415
+ properties: {
2416
+ name: {
2417
+ type: "string",
2418
+ description: "Name of the plugin to modify (required)"
2419
+ },
2420
+ version: {
2421
+ type: "string",
2422
+ description: "New version string"
2423
+ },
2424
+ description: {
2425
+ type: "string",
2426
+ description: "New description"
2427
+ },
2428
+ addTools: {
2429
+ type: "array",
2430
+ description: "Tools to add or update",
2431
+ items: {
2432
+ type: "object",
2433
+ properties: {
2434
+ name: { type: "string" },
2435
+ description: { type: "string" },
2436
+ parameters: { type: "object" }
2437
+ }
2438
+ }
2439
+ },
2440
+ addExecutorCode: {
2441
+ type: "object",
2442
+ description: "Executor code to add/update (tool name -> code string)"
2443
+ },
2444
+ removeTools: {
2445
+ type: "array",
2446
+ description: "Names of tools to remove",
2447
+ items: { type: "string" }
2448
+ },
2449
+ onRegisterCode: {
2450
+ type: "string",
2451
+ description: "New onRegister hook code"
2452
+ },
2453
+ beforeRequestCode: {
2454
+ type: "string",
2455
+ description: "New beforeRequest hook code"
2456
+ },
2457
+ afterResponseCode: {
2458
+ type: "string",
2459
+ description: "New afterResponse hook code"
2460
+ },
2461
+ onErrorCode: {
2462
+ type: "string",
2463
+ description: "New onError hook code"
2464
+ }
2465
+ },
2466
+ required: ["name"]
2467
+ }
2468
+ };
2070
2469
  var buildPluginExecutor = async (args2) => {
2071
2470
  const {
2072
2471
  name,
@@ -2123,12 +2522,51 @@ var buildPluginExecutor = async (args2) => {
2123
2522
  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.`
2124
2523
  };
2125
2524
  };
2525
+ function normalizePlugin(input) {
2526
+ const plugin = input;
2527
+ const tools = plugin.tools;
2528
+ const hasEmbeddedExecutors = tools?.[0]?.executorCode !== void 0;
2529
+ if (hasEmbeddedExecutors) {
2530
+ return {
2531
+ ...plugin,
2532
+ enabled: plugin.enabled ?? true,
2533
+ installedAt: plugin.installedAt ?? (/* @__PURE__ */ new Date()).toISOString()
2534
+ };
2535
+ }
2536
+ const executorCode = plugin.executorCode;
2537
+ const rawTools = tools || [];
2538
+ const hookKeys = ["onRegisterCode", "beforeRequestCode", "afterResponseCode", "onErrorCode"];
2539
+ const hooksCode = {};
2540
+ for (const key of hookKeys) {
2541
+ if (executorCode?.[key]) {
2542
+ hooksCode[key] = executorCode[key];
2543
+ }
2544
+ if (plugin[key]) {
2545
+ hooksCode[key] = plugin[key];
2546
+ }
2547
+ }
2548
+ return {
2549
+ name: plugin.name,
2550
+ version: plugin.version,
2551
+ description: plugin.description,
2552
+ tools: rawTools.map((tool) => ({
2553
+ name: tool.name,
2554
+ description: tool.description,
2555
+ parameters: tool.parameters,
2556
+ executorCode: executorCode?.[tool.name]
2557
+ })),
2558
+ hooksCode: Object.keys(hooksCode).length > 0 ? hooksCode : void 0,
2559
+ enabled: true,
2560
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
2561
+ };
2562
+ }
2126
2563
  var savePluginExecutor = async (args2) => {
2127
- const { plugin, filename } = args2;
2564
+ const { plugin: rawPlugin, filename } = args2;
2128
2565
  if (typeof window === "undefined") {
2129
2566
  return { success: false, error: "Cannot save files in server environment" };
2130
2567
  }
2131
2568
  try {
2569
+ const plugin = normalizePlugin(rawPlugin);
2132
2570
  const json2 = JSON.stringify(plugin, null, 2);
2133
2571
  const blob = new Blob([json2], { type: "application/json" });
2134
2572
  const url = URL.createObjectURL(blob);
@@ -2151,31 +2589,23 @@ var savePluginExecutor = async (args2) => {
2151
2589
  }
2152
2590
  };
2153
2591
  var installPluginExecutor = async (args2) => {
2154
- const { plugin, enabled = true } = args2;
2592
+ const { plugin: rawPlugin, enabled = true } = args2;
2155
2593
  if (typeof window === "undefined") {
2156
2594
  return { success: false, error: "Cannot install plugins in server environment" };
2157
2595
  }
2158
2596
  try {
2159
- const instanceId = window.__hustleInstanceId || "global-demo";
2160
- const STORAGE_KEY = `hustle-plugins-${instanceId}`;
2161
- const stored = localStorage.getItem(STORAGE_KEY);
2162
- const plugins = stored ? JSON.parse(stored) : [];
2163
- const existingIndex = plugins.findIndex((p) => p.name === plugin.name);
2164
- const pluginToStore = {
2165
- ...plugin,
2166
- enabled,
2167
- installedAt: (/* @__PURE__ */ new Date()).toISOString()
2168
- };
2169
- if (existingIndex >= 0) {
2170
- plugins[existingIndex] = pluginToStore;
2171
- } else {
2172
- plugins.push(pluginToStore);
2597
+ const plugin = normalizePlugin(rawPlugin);
2598
+ const win = window;
2599
+ if (!win.__hustleRegisterPlugin) {
2600
+ return {
2601
+ success: false,
2602
+ error: "Plugin registration not available. Make sure HustleProvider is mounted."
2603
+ };
2173
2604
  }
2174
- localStorage.setItem(STORAGE_KEY, JSON.stringify(plugins));
2605
+ await win.__hustleRegisterPlugin(plugin, enabled);
2175
2606
  return {
2176
2607
  success: true,
2177
- message: `Plugin "${plugin.name}" installed${enabled ? " and enabled" : ""}. Refresh the page or re-register plugins to activate.`,
2178
- action: existingIndex >= 0 ? "updated" : "installed"
2608
+ message: `Plugin "${plugin.name}" installed${enabled ? " and enabled" : ""}.`
2179
2609
  };
2180
2610
  } catch (error2) {
2181
2611
  return {
@@ -2184,15 +2614,168 @@ var installPluginExecutor = async (args2) => {
2184
2614
  };
2185
2615
  }
2186
2616
  };
2617
+ var uninstallPluginExecutor = async (args2) => {
2618
+ const { name } = args2;
2619
+ if (typeof window === "undefined") {
2620
+ return { success: false, error: "Cannot uninstall plugins in server environment" };
2621
+ }
2622
+ try {
2623
+ const win = window;
2624
+ if (!win.__hustleUnregisterPlugin) {
2625
+ return {
2626
+ success: false,
2627
+ error: "Plugin unregistration not available. Make sure HustleProvider is mounted."
2628
+ };
2629
+ }
2630
+ await win.__hustleUnregisterPlugin(name);
2631
+ return {
2632
+ success: true,
2633
+ message: `Plugin "${name}" has been uninstalled.`
2634
+ };
2635
+ } catch (error2) {
2636
+ return {
2637
+ success: false,
2638
+ error: `Failed to uninstall: ${error2 instanceof Error ? error2.message : "Unknown error"}`
2639
+ };
2640
+ }
2641
+ };
2642
+ var listPluginsExecutor = async () => {
2643
+ if (typeof window === "undefined") {
2644
+ return { success: false, error: "Cannot list plugins in server environment" };
2645
+ }
2646
+ try {
2647
+ const win = window;
2648
+ if (!win.__hustleListPlugins) {
2649
+ return {
2650
+ success: false,
2651
+ error: "Plugin listing not available. Make sure HustleProvider is mounted."
2652
+ };
2653
+ }
2654
+ const plugins = win.__hustleListPlugins();
2655
+ return {
2656
+ success: true,
2657
+ plugins,
2658
+ 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(", ")}`
2659
+ };
2660
+ } catch (error2) {
2661
+ return {
2662
+ success: false,
2663
+ error: `Failed to list plugins: ${error2 instanceof Error ? error2.message : "Unknown error"}`
2664
+ };
2665
+ }
2666
+ };
2667
+ var modifyPluginExecutor = async (args2) => {
2668
+ const {
2669
+ name,
2670
+ version,
2671
+ description,
2672
+ addTools,
2673
+ addExecutorCode,
2674
+ removeTools,
2675
+ onRegisterCode,
2676
+ beforeRequestCode,
2677
+ afterResponseCode,
2678
+ onErrorCode
2679
+ } = args2;
2680
+ if (typeof window === "undefined") {
2681
+ return { success: false, error: "Cannot modify plugins in server environment" };
2682
+ }
2683
+ try {
2684
+ const win = window;
2685
+ if (!win.__hustleGetPlugin || !win.__hustleRegisterPlugin) {
2686
+ return {
2687
+ success: false,
2688
+ error: "Plugin modification not available. Make sure HustleProvider is mounted."
2689
+ };
2690
+ }
2691
+ const existing = win.__hustleGetPlugin(name);
2692
+ if (!existing) {
2693
+ return {
2694
+ success: false,
2695
+ error: `Plugin "${name}" not found. Use list_plugins to see installed plugins.`
2696
+ };
2697
+ }
2698
+ let tools = existing.tools ? [...existing.tools] : [];
2699
+ if (removeTools && removeTools.length > 0) {
2700
+ tools = tools.filter((t) => !removeTools.includes(t.name));
2701
+ }
2702
+ if (addTools && addTools.length > 0) {
2703
+ for (const newTool of addTools) {
2704
+ const existingIndex = tools.findIndex((t) => t.name === newTool.name);
2705
+ const toolWithExecutor = {
2706
+ ...newTool,
2707
+ executorCode: addExecutorCode?.[newTool.name] || tools.find((t) => t.name === newTool.name)?.executorCode
2708
+ };
2709
+ if (existingIndex >= 0) {
2710
+ tools[existingIndex] = toolWithExecutor;
2711
+ } else {
2712
+ tools.push(toolWithExecutor);
2713
+ }
2714
+ }
2715
+ }
2716
+ if (addExecutorCode) {
2717
+ for (const [toolName, code2] of Object.entries(addExecutorCode)) {
2718
+ const tool = tools.find((t) => t.name === toolName);
2719
+ if (tool) {
2720
+ tool.executorCode = code2;
2721
+ }
2722
+ }
2723
+ }
2724
+ const modified = {
2725
+ ...existing,
2726
+ tools
2727
+ };
2728
+ if (version) modified.version = version;
2729
+ if (description) modified.description = description;
2730
+ if (onRegisterCode || beforeRequestCode || afterResponseCode || onErrorCode) {
2731
+ modified.hooksCode = modified.hooksCode || {};
2732
+ if (onRegisterCode) modified.hooksCode.onRegisterCode = onRegisterCode;
2733
+ if (beforeRequestCode) modified.hooksCode.beforeRequestCode = beforeRequestCode;
2734
+ if (afterResponseCode) modified.hooksCode.afterResponseCode = afterResponseCode;
2735
+ if (onErrorCode) modified.hooksCode.onErrorCode = onErrorCode;
2736
+ }
2737
+ const unregisterFn = window.__hustleUnregisterPlugin;
2738
+ if (unregisterFn) {
2739
+ await unregisterFn(name);
2740
+ }
2741
+ await win.__hustleRegisterPlugin(modified, existing.enabled);
2742
+ const changes = [];
2743
+ if (version) changes.push(`version \u2192 ${version}`);
2744
+ if (description) changes.push("description updated");
2745
+ if (removeTools?.length) changes.push(`removed ${removeTools.length} tool(s)`);
2746
+ if (addTools?.length) changes.push(`added/updated ${addTools.length} tool(s)`);
2747
+ if (onRegisterCode) changes.push("onRegister hook updated");
2748
+ if (beforeRequestCode) changes.push("beforeRequest hook updated");
2749
+ if (afterResponseCode) changes.push("afterResponse hook updated");
2750
+ if (onErrorCode) changes.push("onError hook updated");
2751
+ return {
2752
+ success: true,
2753
+ message: `Plugin "${name}" modified: ${changes.join(", ")}`,
2754
+ plugin: {
2755
+ name: modified.name,
2756
+ version: modified.version,
2757
+ toolCount: tools.length
2758
+ }
2759
+ };
2760
+ } catch (error2) {
2761
+ return {
2762
+ success: false,
2763
+ error: `Failed to modify plugin: ${error2 instanceof Error ? error2.message : "Unknown error"}`
2764
+ };
2765
+ }
2766
+ };
2187
2767
  var pluginBuilderPlugin = {
2188
2768
  name: "plugin-builder",
2189
- version: "1.0.0",
2769
+ version: "1.2.0",
2190
2770
  description: "Build custom plugins through conversation",
2191
- tools: [buildPluginTool, savePluginTool, installPluginTool],
2771
+ tools: [buildPluginTool, savePluginTool, installPluginTool, uninstallPluginTool, listPluginsTool, modifyPluginTool],
2192
2772
  executors: {
2193
2773
  build_plugin: buildPluginExecutor,
2194
2774
  save_plugin: savePluginExecutor,
2195
- install_plugin: installPluginExecutor
2775
+ install_plugin: installPluginExecutor,
2776
+ uninstall_plugin: uninstallPluginExecutor,
2777
+ list_plugins: listPluginsExecutor,
2778
+ modify_plugin: modifyPluginExecutor
2196
2779
  },
2197
2780
  hooks: {
2198
2781
  onRegister: () => {