@emblemvault/hustle-react 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -276,35 +276,52 @@ var pluginRegistry = new PluginRegistry();
276
276
  function getStorageKey(instanceId) {
277
277
  return `hustle-plugins-${instanceId}`;
278
278
  }
279
- function usePlugins(instanceId = "default") {
279
+ function getInstanceId(providedId) {
280
+ if (providedId) return providedId;
281
+ if (typeof window !== "undefined") {
282
+ const globalId = window.__hustleInstanceId;
283
+ if (globalId) return globalId;
284
+ }
285
+ return "default";
286
+ }
287
+ function usePlugins(instanceId) {
288
+ const [resolvedInstanceId] = useState(() => getInstanceId(instanceId));
280
289
  const [plugins, setPlugins] = useState([]);
281
290
  useEffect(() => {
282
- setPlugins(pluginRegistry.loadFromStorage(instanceId));
283
- const unsubscribe = pluginRegistry.onChange(setPlugins, instanceId);
284
- const storageKey = getStorageKey(instanceId);
291
+ setPlugins(pluginRegistry.loadFromStorage(resolvedInstanceId));
292
+ const unsubscribe = pluginRegistry.onChange(setPlugins, resolvedInstanceId);
293
+ const storageKey = getStorageKey(resolvedInstanceId);
285
294
  const handleStorage = (e) => {
286
295
  if (e.key === storageKey) {
287
- setPlugins(pluginRegistry.loadFromStorage(instanceId));
296
+ setPlugins(pluginRegistry.loadFromStorage(resolvedInstanceId));
288
297
  }
289
298
  };
290
299
  window.addEventListener("storage", handleStorage);
300
+ const handlePluginInstalled = (e) => {
301
+ const customEvent = e;
302
+ if (customEvent.detail.instanceId === resolvedInstanceId) {
303
+ setPlugins(pluginRegistry.loadFromStorage(resolvedInstanceId));
304
+ }
305
+ };
306
+ window.addEventListener("hustle-plugin-installed", handlePluginInstalled);
291
307
  return () => {
292
308
  unsubscribe();
293
309
  window.removeEventListener("storage", handleStorage);
310
+ window.removeEventListener("hustle-plugin-installed", handlePluginInstalled);
294
311
  };
295
- }, [instanceId]);
312
+ }, [resolvedInstanceId]);
296
313
  const registerPlugin = useCallback((plugin) => {
297
- pluginRegistry.register(plugin, true, instanceId);
298
- }, [instanceId]);
314
+ pluginRegistry.register(plugin, true, resolvedInstanceId);
315
+ }, [resolvedInstanceId]);
299
316
  const unregisterPlugin = useCallback((name) => {
300
- pluginRegistry.unregister(name, instanceId);
301
- }, [instanceId]);
317
+ pluginRegistry.unregister(name, resolvedInstanceId);
318
+ }, [resolvedInstanceId]);
302
319
  const enablePlugin = useCallback((name) => {
303
- pluginRegistry.setEnabled(name, true, instanceId);
304
- }, [instanceId]);
320
+ pluginRegistry.setEnabled(name, true, resolvedInstanceId);
321
+ }, [resolvedInstanceId]);
305
322
  const disablePlugin = useCallback((name) => {
306
- pluginRegistry.setEnabled(name, false, instanceId);
307
- }, [instanceId]);
323
+ pluginRegistry.setEnabled(name, false, resolvedInstanceId);
324
+ }, [resolvedInstanceId]);
308
325
  const isRegistered = useCallback(
309
326
  (name) => plugins.some((p) => p.name === name),
310
327
  [plugins]
@@ -359,6 +376,42 @@ function HustleProvider({
359
376
  }
360
377
  };
361
378
  }, [isAutoInstance, resolvedInstanceId]);
379
+ useEffect(() => {
380
+ if (typeof window !== "undefined") {
381
+ const win = window;
382
+ win.__hustleInstanceId = resolvedInstanceId;
383
+ win.__hustleRegisterPlugin = async (plugin, enabled = true) => {
384
+ const hydrated = hydratePlugin(plugin);
385
+ pluginRegistry.register(hydrated, enabled, resolvedInstanceId);
386
+ };
387
+ win.__hustleUnregisterPlugin = async (name) => {
388
+ pluginRegistry.unregister(name, resolvedInstanceId);
389
+ };
390
+ win.__hustleListPlugins = () => {
391
+ const plugins = pluginRegistry.loadFromStorage(resolvedInstanceId);
392
+ return plugins.map((p) => ({
393
+ name: p.name,
394
+ version: p.version,
395
+ description: p.description || "",
396
+ enabled: p.enabled
397
+ }));
398
+ };
399
+ win.__hustleGetPlugin = (name) => {
400
+ const plugins = pluginRegistry.loadFromStorage(resolvedInstanceId);
401
+ return plugins.find((p) => p.name === name) || null;
402
+ };
403
+ }
404
+ return () => {
405
+ if (typeof window !== "undefined") {
406
+ const win = window;
407
+ delete win.__hustleInstanceId;
408
+ delete win.__hustleRegisterPlugin;
409
+ delete win.__hustleUnregisterPlugin;
410
+ delete win.__hustleListPlugins;
411
+ delete win.__hustleGetPlugin;
412
+ }
413
+ };
414
+ }, [resolvedInstanceId]);
362
415
  const isApiKeyMode = Boolean(apiKey && vaultId);
363
416
  const authContext = useEmblemAuthOptional();
364
417
  const authSDK = isApiKeyMode ? null : authContext?.authSDK ?? null;
@@ -506,6 +559,19 @@ function HustleProvider({
506
559
  };
507
560
  registerPlugins();
508
561
  }, [client, enabledPlugins, log]);
562
+ useEffect(() => {
563
+ if (typeof window !== "undefined" && client) {
564
+ const win = window;
565
+ win.__hustleUploadFile = async (file, fileName) => {
566
+ return await client.uploadFile(file, fileName);
567
+ };
568
+ }
569
+ return () => {
570
+ if (typeof window !== "undefined") {
571
+ delete window.__hustleUploadFile;
572
+ }
573
+ };
574
+ }, [client]);
509
575
  const loadModels = useCallback(async () => {
510
576
  if (!client) {
511
577
  log("Cannot load models - client not ready");
@@ -1386,12 +1452,16 @@ function ensureModalStyles() {
1386
1452
  left: 0;
1387
1453
  right: 0;
1388
1454
  bottom: 0;
1389
- background: rgba(0, 0, 0, 0.6);
1455
+ background: transparent;
1390
1456
  display: flex;
1391
1457
  align-items: center;
1392
1458
  justify-content: center;
1393
1459
  z-index: 10000;
1394
- animation: uqFadeIn 0.2s ease-out;
1460
+ pointer-events: none;
1461
+ }
1462
+
1463
+ .user-question-modal {
1464
+ pointer-events: auto;
1395
1465
  }
1396
1466
 
1397
1467
  @keyframes uqFadeIn {
@@ -1504,6 +1574,27 @@ function ensureModalStyles() {
1504
1574
  color: #666;
1505
1575
  cursor: not-allowed;
1506
1576
  }
1577
+
1578
+ .user-question-custom-input {
1579
+ width: 100%;
1580
+ padding: 8px 12px;
1581
+ margin-top: 8px;
1582
+ background: #1a1a2e;
1583
+ border: 1px solid #444;
1584
+ border-radius: 6px;
1585
+ color: #e0e0e0;
1586
+ font-size: 14px;
1587
+ box-sizing: border-box;
1588
+ }
1589
+
1590
+ .user-question-custom-input:focus {
1591
+ outline: none;
1592
+ border-color: #4a7aff;
1593
+ }
1594
+
1595
+ .user-question-custom-input::placeholder {
1596
+ color: #666;
1597
+ }
1507
1598
  `;
1508
1599
  document.head.appendChild(styles2);
1509
1600
  }
@@ -1525,13 +1616,17 @@ var askUserTool = {
1525
1616
  allowMultiple: {
1526
1617
  type: "boolean",
1527
1618
  description: "If true, user can select multiple choices. Default: false"
1619
+ },
1620
+ allowCustom: {
1621
+ type: "boolean",
1622
+ description: 'If true, adds an "Other" option where user can type a custom response. Default: false'
1528
1623
  }
1529
1624
  },
1530
1625
  required: ["question", "choices"]
1531
1626
  }
1532
1627
  };
1533
1628
  var askUserExecutor = async (args2) => {
1534
- const { question, choices, allowMultiple = false } = args2;
1629
+ const { question, choices, allowMultiple = false, allowCustom = false } = args2;
1535
1630
  if (!question || !choices || !Array.isArray(choices) || choices.length === 0) {
1536
1631
  return {
1537
1632
  question: question || "",
@@ -1563,6 +1658,17 @@ var askUserExecutor = async (args2) => {
1563
1658
  choicesDiv.className = "user-question-choices";
1564
1659
  const inputType = allowMultiple ? "checkbox" : "radio";
1565
1660
  const inputName = `uq-${Date.now()}`;
1661
+ let customInput = null;
1662
+ let isCustomSelected = false;
1663
+ const submitBtn = document.createElement("button");
1664
+ submitBtn.className = "user-question-btn user-question-btn-submit";
1665
+ submitBtn.textContent = "Submit";
1666
+ submitBtn.disabled = true;
1667
+ const updateSubmitButton = () => {
1668
+ const hasSelection = selected.size > 0;
1669
+ const hasCustomValue = isCustomSelected && customInput && customInput.value.trim().length > 0;
1670
+ submitBtn.disabled = !hasSelection && !hasCustomValue;
1671
+ };
1566
1672
  choices.forEach((choice, index) => {
1567
1673
  const choiceDiv = document.createElement("div");
1568
1674
  choiceDiv.className = "user-question-choice";
@@ -1587,6 +1693,8 @@ var askUserExecutor = async (args2) => {
1587
1693
  }
1588
1694
  } else {
1589
1695
  selected.clear();
1696
+ isCustomSelected = false;
1697
+ if (customInput) customInput.value = "";
1590
1698
  selected.add(choice);
1591
1699
  choicesDiv.querySelectorAll(".user-question-choice").forEach((c) => c.classList.remove("selected"));
1592
1700
  choiceDiv.classList.add("selected");
@@ -1602,9 +1710,62 @@ var askUserExecutor = async (args2) => {
1602
1710
  });
1603
1711
  choicesDiv.appendChild(choiceDiv);
1604
1712
  });
1713
+ if (allowCustom) {
1714
+ const customChoiceDiv = document.createElement("div");
1715
+ customChoiceDiv.className = "user-question-choice";
1716
+ const customRadio = document.createElement("input");
1717
+ customRadio.type = inputType;
1718
+ customRadio.name = inputName;
1719
+ customRadio.id = `${inputName}-custom`;
1720
+ customRadio.value = "__custom__";
1721
+ const customLabel = document.createElement("label");
1722
+ customLabel.htmlFor = customRadio.id;
1723
+ customLabel.textContent = "Other:";
1724
+ customLabel.style.flexShrink = "0";
1725
+ customInput = document.createElement("input");
1726
+ customInput.type = "text";
1727
+ customInput.className = "user-question-custom-input";
1728
+ customInput.placeholder = "Type your answer...";
1729
+ customInput.style.marginTop = "0";
1730
+ customInput.style.marginLeft = "8px";
1731
+ customInput.style.flex = "1";
1732
+ customChoiceDiv.appendChild(customRadio);
1733
+ customChoiceDiv.appendChild(customLabel);
1734
+ customChoiceDiv.appendChild(customInput);
1735
+ const handleCustomSelect = () => {
1736
+ if (!allowMultiple) {
1737
+ selected.clear();
1738
+ choicesDiv.querySelectorAll(".user-question-choice").forEach((c) => c.classList.remove("selected"));
1739
+ }
1740
+ isCustomSelected = true;
1741
+ customChoiceDiv.classList.add("selected");
1742
+ customInput?.focus();
1743
+ updateSubmitButton();
1744
+ };
1745
+ customRadio.addEventListener("change", handleCustomSelect);
1746
+ customChoiceDiv.addEventListener("click", (e) => {
1747
+ if (e.target !== customRadio && e.target !== customInput) {
1748
+ customRadio.checked = true;
1749
+ handleCustomSelect();
1750
+ }
1751
+ });
1752
+ customInput.addEventListener("focus", () => {
1753
+ if (!customRadio.checked) {
1754
+ customRadio.checked = true;
1755
+ handleCustomSelect();
1756
+ }
1757
+ });
1758
+ customInput.addEventListener("input", updateSubmitButton);
1759
+ choicesDiv.appendChild(customChoiceDiv);
1760
+ }
1605
1761
  modal.appendChild(choicesDiv);
1606
1762
  const actions = document.createElement("div");
1607
1763
  actions.className = "user-question-actions";
1764
+ overlay.appendChild(modal);
1765
+ document.body.appendChild(overlay);
1766
+ const cleanup = () => {
1767
+ overlay.remove();
1768
+ };
1608
1769
  const cancelBtn = document.createElement("button");
1609
1770
  cancelBtn.className = "user-question-btn user-question-btn-cancel";
1610
1771
  cancelBtn.textContent = "Skip";
@@ -1616,29 +1777,21 @@ var askUserExecutor = async (args2) => {
1616
1777
  answered: false
1617
1778
  });
1618
1779
  };
1619
- const submitBtn = document.createElement("button");
1620
- submitBtn.className = "user-question-btn user-question-btn-submit";
1621
- submitBtn.textContent = "Submit";
1622
- submitBtn.disabled = true;
1623
1780
  submitBtn.onclick = () => {
1624
1781
  cleanup();
1782
+ const results = Array.from(selected);
1783
+ if (isCustomSelected && customInput && customInput.value.trim()) {
1784
+ results.push(customInput.value.trim());
1785
+ }
1625
1786
  resolve({
1626
1787
  question,
1627
- selectedChoices: Array.from(selected),
1788
+ selectedChoices: results,
1628
1789
  answered: true
1629
1790
  });
1630
1791
  };
1631
- const updateSubmitButton = () => {
1632
- submitBtn.disabled = selected.size === 0;
1633
- };
1634
1792
  actions.appendChild(cancelBtn);
1635
1793
  actions.appendChild(submitBtn);
1636
1794
  modal.appendChild(actions);
1637
- overlay.appendChild(modal);
1638
- document.body.appendChild(overlay);
1639
- const cleanup = () => {
1640
- overlay.remove();
1641
- };
1642
1795
  const handleEscape = (e) => {
1643
1796
  if (e.key === "Escape") {
1644
1797
  document.removeEventListener("keydown", handleEscape);
@@ -1807,6 +1960,11 @@ var screenshotTool = {
1807
1960
 
1808
1961
  The screenshot captures the visible viewport of the page. The image is uploaded to the server and a permanent URL is returned.
1809
1962
 
1963
+ IMPORTANT: Before taking a screenshot, use the ask_user tool (if available) to ask which size they prefer:
1964
+ - "full" (100%) - highest quality, larger file
1965
+ - "half" (50%) - good balance of quality and size
1966
+ - "quarter" (25%) - smallest file, faster upload
1967
+
1810
1968
  Use this when:
1811
1969
  - User asks to see what's on their screen
1812
1970
  - You need to analyze the current page visually
@@ -1817,12 +1975,24 @@ Use this when:
1817
1975
  selector: {
1818
1976
  type: "string",
1819
1977
  description: "Optional CSS selector to capture a specific element instead of the full page. Leave empty for full page screenshot."
1978
+ },
1979
+ size: {
1980
+ type: "string",
1981
+ enum: ["full", "half", "quarter"],
1982
+ description: 'Image size: "full" (100%), "half" (50%), or "quarter" (25%). Ask the user which size they prefer before capturing.'
1820
1983
  }
1821
1984
  }
1822
1985
  }
1823
1986
  };
1824
1987
  var screenshotExecutor = async (args2) => {
1825
1988
  const selector = args2.selector;
1989
+ const size = args2.size || "full";
1990
+ const scaleMap = {
1991
+ full: 1,
1992
+ half: 0.5,
1993
+ quarter: 0.25
1994
+ };
1995
+ const scale = scaleMap[size] || 1;
1826
1996
  if (typeof window === "undefined" || typeof document === "undefined") {
1827
1997
  return {
1828
1998
  success: false,
@@ -1843,34 +2013,41 @@ var screenshotExecutor = async (args2) => {
1843
2013
  if (!target) {
1844
2014
  return { success: false, error: "Element not found: " + selector };
1845
2015
  }
1846
- const canvas = await window.html2canvas(target, {
2016
+ const fullCanvas = await window.html2canvas(target, {
1847
2017
  useCORS: true,
1848
2018
  allowTaint: true,
1849
2019
  backgroundColor: "#000000",
1850
2020
  scale: 1
1851
2021
  });
2022
+ let finalCanvas = fullCanvas;
2023
+ if (scale < 1) {
2024
+ const resizedCanvas = document.createElement("canvas");
2025
+ resizedCanvas.width = Math.round(fullCanvas.width * scale);
2026
+ resizedCanvas.height = Math.round(fullCanvas.height * scale);
2027
+ const ctx = resizedCanvas.getContext("2d");
2028
+ if (ctx) {
2029
+ ctx.drawImage(fullCanvas, 0, 0, resizedCanvas.width, resizedCanvas.height);
2030
+ finalCanvas = resizedCanvas;
2031
+ }
2032
+ }
1852
2033
  const blob = await new Promise((resolve) => {
1853
- canvas.toBlob(resolve, "image/png", 0.9);
2034
+ finalCanvas.toBlob(resolve, "image/png", 0.9);
1854
2035
  });
1855
2036
  if (!blob) {
1856
2037
  return { success: false, error: "Failed to create image blob" };
1857
2038
  }
1858
- const formData = new FormData();
1859
- formData.append("file", blob, "screenshot-" + Date.now() + ".png");
1860
- const response = await fetch("/api/files/upload", {
1861
- method: "POST",
1862
- body: formData
1863
- });
1864
- if (!response.ok) {
1865
- const errorData = await response.json().catch(() => ({}));
1866
- return { success: false, error: errorData.error || "Upload failed" };
2039
+ const uploadFn = window.__hustleUploadFile;
2040
+ if (!uploadFn) {
2041
+ return { success: false, error: "Upload not available. Make sure HustleProvider is mounted and client is ready." };
1867
2042
  }
1868
- const data = await response.json();
2043
+ const fileName = "screenshot-" + Date.now() + ".png";
2044
+ const attachment = await uploadFn(blob, fileName);
2045
+ const sizeLabel = size === "full" ? "100%" : size === "half" ? "50%" : "25%";
1869
2046
  return {
1870
2047
  success: true,
1871
- url: data.url,
1872
- contentType: data.contentType,
1873
- message: "Screenshot captured and uploaded successfully"
2048
+ url: attachment.url,
2049
+ contentType: attachment.contentType || "image/png",
2050
+ message: `Screenshot captured at ${sizeLabel} size (${finalCanvas.width}x${finalCanvas.height}) and uploaded successfully`
1874
2051
  };
1875
2052
  } catch (e) {
1876
2053
  const err = e;
@@ -1895,6 +2072,636 @@ var screenshotPlugin = {
1895
2072
  }
1896
2073
  };
1897
2074
 
2075
+ // src/plugins/pluginBuilder.ts
2076
+ var buildPluginTool = {
2077
+ name: "build_plugin",
2078
+ description: `Build a Hustle plugin definition. Use this tool to construct a plugin based on user requirements.
2079
+
2080
+ ## Plugin Structure
2081
+
2082
+ A plugin consists of:
2083
+ - **name**: Unique identifier (lowercase, no spaces, e.g., "my-plugin")
2084
+ - **version**: Semantic version (e.g., "1.0.0")
2085
+ - **description**: What the plugin does
2086
+ - **tools**: Array of tool definitions the AI can call
2087
+ - **executorCode**: Object mapping tool names to JavaScript function code strings
2088
+
2089
+ ## Tool Definition Format
2090
+
2091
+ Each tool needs:
2092
+ - **name**: Unique tool name (alphanumeric + underscore, e.g., "get_weather")
2093
+ - **description**: Clear description for the AI to understand when to use it
2094
+ - **parameters**: JSON Schema object defining the arguments
2095
+
2096
+ Example tool:
2097
+ {
2098
+ "name": "get_weather",
2099
+ "description": "Get current weather for a city",
2100
+ "parameters": {
2101
+ "type": "object",
2102
+ "properties": {
2103
+ "city": { "type": "string", "description": "City name" },
2104
+ "units": { "type": "string", "enum": ["celsius", "fahrenheit"], "description": "Temperature units" }
2105
+ },
2106
+ "required": ["city"]
2107
+ }
2108
+ }
2109
+
2110
+ ## Executor Code Format
2111
+
2112
+ Executors are async JavaScript functions that receive args and return a result.
2113
+ Write them as arrow function strings that will be eval'd:
2114
+
2115
+ "async (args) => { const { city } = args; return { weather: 'sunny', city }; }"
2116
+
2117
+ The function receives args as Record<string, unknown> and should return the result.
2118
+
2119
+ ## Available in Executor Scope
2120
+
2121
+ Executors run in the browser context with full access to:
2122
+
2123
+ ### Browser APIs
2124
+ - **fetch(url, options)** - HTTP requests (subject to CORS)
2125
+ - **localStorage / sessionStorage** - Persistent storage
2126
+ - **document** - Full DOM access (create elements, modals, forms, etc.)
2127
+ - **window** - Global window object
2128
+ - **console** - Logging (log, warn, error, etc.)
2129
+ - **setTimeout / setInterval / clearTimeout / clearInterval** - Timers
2130
+ - **JSON** - Parse and stringify
2131
+ - **Date** - Date/time operations
2132
+ - **URL / URLSearchParams** - URL manipulation
2133
+ - **FormData / Blob / File / FileReader** - File handling
2134
+ - **crypto** - Cryptographic operations (crypto.randomUUID(), etc.)
2135
+ - **navigator** - Browser info, clipboard, geolocation, etc.
2136
+ - **location** - Current URL info
2137
+ - **history** - Browser history navigation
2138
+ - **WebSocket** - Real-time bidirectional communication
2139
+ - **EventSource** - Server-sent events
2140
+ - **indexedDB** - Client-side database for large data
2141
+ - **Notification** - Browser notifications (requires permission)
2142
+ - **performance** - Performance timing
2143
+ - **atob / btoa** - Base64 encoding/decoding
2144
+ - **TextEncoder / TextDecoder** - Text encoding
2145
+ - **AbortController** - Cancel fetch requests
2146
+ - **IntersectionObserver / MutationObserver / ResizeObserver** - DOM observers
2147
+ - **requestAnimationFrame** - Animation timing
2148
+ - **speechSynthesis** - Text-to-speech
2149
+ - **Audio / Image / Canvas** - Media APIs
2150
+
2151
+ ### Hustle Plugin System Globals
2152
+ - **window.__hustleInstanceId** - Current Hustle instance ID
2153
+ - **window.__hustleRegisterPlugin(plugin, enabled)** - Install another plugin dynamically
2154
+ - **window.__hustleUnregisterPlugin(name)** - Uninstall a plugin by name
2155
+ - **window.__hustleUploadFile(file, fileName?)** - Upload a File/Blob to the server, returns { url, contentType }
2156
+ - **window.__hustleListPlugins()** - List all installed plugins
2157
+ - **window.__hustleGetPlugin(name)** - Get a specific plugin by name
2158
+
2159
+ ### DOM Manipulation Examples
2160
+ Create a modal: document.createElement('div'), style it, append to document.body
2161
+ Add event listeners: element.addEventListener('click', handler)
2162
+ Query elements: document.querySelector(), document.querySelectorAll()
2163
+
2164
+ ### Storage Patterns
2165
+ Store data: localStorage.setItem('key', JSON.stringify(data))
2166
+ Retrieve data: JSON.parse(localStorage.getItem('key') || '{}')
2167
+ Namespace your keys: Use plugin name prefix like "myplugin-settings"
2168
+
2169
+ ### Async Patterns
2170
+ All executors should be async. Use await for promises:
2171
+ "async (args) => { const res = await fetch(url); return await res.json(); }"
2172
+
2173
+ ## Lifecycle Hooks (Optional)
2174
+
2175
+ Hooks also have full access to the browser scope described above.
2176
+
2177
+ - **onRegisterCode**: Called once when plugin is registered/enabled. Good for initialization.
2178
+ **IMPORTANT: Always log when your plugin registers so users know it's active!**
2179
+ Example: "async () => { console.log('[MyPlugin] v1.0.0 registered'); }"
2180
+
2181
+ - **beforeRequestCode**: Modify messages before sending. Receives request object with { messages, model, ... }. Must return the modified request.
2182
+ Example: "async (req) => { req.messages = req.messages.map(m => ({...m, content: m.content.toUpperCase()})); return req; }"
2183
+
2184
+ - **afterResponseCode**: Process/modify response after receiving. Receives response object with { content, ... }.
2185
+ Example: "async (res) => { console.log('Response received:', res.content.substring(0, 100)); }"
2186
+
2187
+ - **onErrorCode**: Called on errors. Receives (error, context) where context has { phase: 'beforeRequest'|'execute'|'afterResponse' }.
2188
+ Example: "async (error, ctx) => { console.error('[MyPlugin] Error in', ctx.phase, ':', error.message); }"
2189
+
2190
+ ## Best Practices
2191
+
2192
+ 1. **Always add onRegisterCode** that logs the plugin name and version
2193
+ 2. **Namespace console logs** with [PluginName] prefix for easy identification
2194
+ 3. **Handle errors gracefully** in executors - return { error: message } instead of throwing
2195
+
2196
+ ## Security Notes
2197
+ - Code runs in browser sandbox with same-origin policy
2198
+ - fetch() is subject to CORS restrictions
2199
+ - No direct filesystem access (use File API with user interaction)
2200
+ - Be careful with eval() on user input`,
2201
+ parameters: {
2202
+ type: "object",
2203
+ properties: {
2204
+ name: {
2205
+ type: "string",
2206
+ description: "Unique plugin identifier (lowercase, no spaces)"
2207
+ },
2208
+ version: {
2209
+ type: "string",
2210
+ description: 'Semantic version (e.g., "1.0.0")'
2211
+ },
2212
+ description: {
2213
+ type: "string",
2214
+ description: "What the plugin does"
2215
+ },
2216
+ tools: {
2217
+ type: "array",
2218
+ description: "Array of tool definitions",
2219
+ items: {
2220
+ type: "object",
2221
+ properties: {
2222
+ name: { type: "string", description: "Tool name" },
2223
+ description: { type: "string", description: "Tool description for AI" },
2224
+ parameters: { type: "object", description: "JSON Schema for arguments" }
2225
+ },
2226
+ required: ["name", "description", "parameters"]
2227
+ }
2228
+ },
2229
+ executorCode: {
2230
+ type: "object",
2231
+ description: "Object mapping tool names to executor function code strings"
2232
+ },
2233
+ beforeRequestCode: {
2234
+ type: "string",
2235
+ description: "Optional: Code for beforeRequest hook"
2236
+ },
2237
+ afterResponseCode: {
2238
+ type: "string",
2239
+ description: "Optional: Code for afterResponse hook"
2240
+ },
2241
+ onRegisterCode: {
2242
+ type: "string",
2243
+ description: "Optional: Code for onRegister hook"
2244
+ },
2245
+ onErrorCode: {
2246
+ type: "string",
2247
+ description: "Optional: Code for onError hook"
2248
+ }
2249
+ },
2250
+ required: ["name", "version", "description", "tools", "executorCode"]
2251
+ }
2252
+ };
2253
+ var savePluginTool = {
2254
+ name: "save_plugin",
2255
+ description: "Save a built plugin as a JSON file. Opens a download dialog for the user.",
2256
+ parameters: {
2257
+ type: "object",
2258
+ properties: {
2259
+ plugin: {
2260
+ type: "object",
2261
+ description: "The plugin object to save (from build_plugin result)"
2262
+ },
2263
+ filename: {
2264
+ type: "string",
2265
+ description: "Filename without extension (defaults to plugin name)"
2266
+ }
2267
+ },
2268
+ required: ["plugin"]
2269
+ }
2270
+ };
2271
+ var installPluginTool = {
2272
+ name: "install_plugin",
2273
+ description: "Install a built plugin to browser storage so it persists and can be used.",
2274
+ parameters: {
2275
+ type: "object",
2276
+ properties: {
2277
+ plugin: {
2278
+ type: "object",
2279
+ description: "The plugin object to install (from build_plugin result)"
2280
+ },
2281
+ enabled: {
2282
+ type: "boolean",
2283
+ description: "Whether to enable the plugin immediately (default: true)"
2284
+ }
2285
+ },
2286
+ required: ["plugin"]
2287
+ }
2288
+ };
2289
+ var uninstallPluginTool = {
2290
+ name: "uninstall_plugin",
2291
+ description: "Uninstall a plugin by name, removing it from browser storage.",
2292
+ parameters: {
2293
+ type: "object",
2294
+ properties: {
2295
+ name: {
2296
+ type: "string",
2297
+ description: "The name of the plugin to uninstall"
2298
+ }
2299
+ },
2300
+ required: ["name"]
2301
+ }
2302
+ };
2303
+ var listPluginsTool = {
2304
+ name: "list_plugins",
2305
+ description: "List all installed plugins with their enabled/disabled status.",
2306
+ parameters: {
2307
+ type: "object",
2308
+ properties: {},
2309
+ required: []
2310
+ }
2311
+ };
2312
+ var modifyPluginTool = {
2313
+ name: "modify_plugin",
2314
+ description: `Modify an existing installed plugin. Can update version, description, tools, executors, and hooks.
2315
+
2316
+ Use list_plugins first to see installed plugins, then modify by name.
2317
+
2318
+ You can:
2319
+ - Add new tools (provide tools array with new tools to add)
2320
+ - Update existing tools (provide tool with same name)
2321
+ - Remove tools (set removeTool to the tool name)
2322
+ - Update hooks (provide hook code)
2323
+ - Update version/description
2324
+
2325
+ Example: Add a new tool to existing plugin:
2326
+ {
2327
+ "name": "my-plugin",
2328
+ "addTools": [{ "name": "new_tool", "description": "...", "parameters": {...} }],
2329
+ "addExecutorCode": { "new_tool": "async (args) => { ... }" }
2330
+ }`,
2331
+ parameters: {
2332
+ type: "object",
2333
+ properties: {
2334
+ name: {
2335
+ type: "string",
2336
+ description: "Name of the plugin to modify (required)"
2337
+ },
2338
+ version: {
2339
+ type: "string",
2340
+ description: "New version string"
2341
+ },
2342
+ description: {
2343
+ type: "string",
2344
+ description: "New description"
2345
+ },
2346
+ addTools: {
2347
+ type: "array",
2348
+ description: "Tools to add or update",
2349
+ items: {
2350
+ type: "object",
2351
+ properties: {
2352
+ name: { type: "string" },
2353
+ description: { type: "string" },
2354
+ parameters: { type: "object" }
2355
+ }
2356
+ }
2357
+ },
2358
+ addExecutorCode: {
2359
+ type: "object",
2360
+ description: "Executor code to add/update (tool name -> code string)"
2361
+ },
2362
+ removeTools: {
2363
+ type: "array",
2364
+ description: "Names of tools to remove",
2365
+ items: { type: "string" }
2366
+ },
2367
+ onRegisterCode: {
2368
+ type: "string",
2369
+ description: "New onRegister hook code"
2370
+ },
2371
+ beforeRequestCode: {
2372
+ type: "string",
2373
+ description: "New beforeRequest hook code"
2374
+ },
2375
+ afterResponseCode: {
2376
+ type: "string",
2377
+ description: "New afterResponse hook code"
2378
+ },
2379
+ onErrorCode: {
2380
+ type: "string",
2381
+ description: "New onError hook code"
2382
+ }
2383
+ },
2384
+ required: ["name"]
2385
+ }
2386
+ };
2387
+ var buildPluginExecutor = async (args2) => {
2388
+ const {
2389
+ name,
2390
+ version,
2391
+ description,
2392
+ tools,
2393
+ executorCode,
2394
+ beforeRequestCode,
2395
+ afterResponseCode,
2396
+ onRegisterCode,
2397
+ onErrorCode
2398
+ } = args2;
2399
+ if (!/^[a-z][a-z0-9-]*$/.test(name)) {
2400
+ return {
2401
+ success: false,
2402
+ error: "Plugin name must be lowercase, start with a letter, and contain only letters, numbers, and hyphens"
2403
+ };
2404
+ }
2405
+ if (!/^\d+\.\d+\.\d+/.test(version)) {
2406
+ return {
2407
+ success: false,
2408
+ error: 'Version must be in semver format (e.g., "1.0.0")'
2409
+ };
2410
+ }
2411
+ for (const tool of tools) {
2412
+ if (!executorCode[tool.name]) {
2413
+ return {
2414
+ success: false,
2415
+ error: `Missing executor code for tool: ${tool.name}`
2416
+ };
2417
+ }
2418
+ }
2419
+ const storedPlugin = {
2420
+ name,
2421
+ version,
2422
+ description,
2423
+ tools: tools.map((tool) => ({
2424
+ ...tool,
2425
+ executorCode: executorCode[tool.name]
2426
+ })),
2427
+ enabled: true,
2428
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
2429
+ };
2430
+ if (beforeRequestCode || afterResponseCode || onRegisterCode || onErrorCode) {
2431
+ storedPlugin.hooksCode = {};
2432
+ if (beforeRequestCode) storedPlugin.hooksCode.beforeRequestCode = beforeRequestCode;
2433
+ if (afterResponseCode) storedPlugin.hooksCode.afterResponseCode = afterResponseCode;
2434
+ if (onRegisterCode) storedPlugin.hooksCode.onRegisterCode = onRegisterCode;
2435
+ if (onErrorCode) storedPlugin.hooksCode.onErrorCode = onErrorCode;
2436
+ }
2437
+ return {
2438
+ success: true,
2439
+ plugin: storedPlugin,
2440
+ 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.`
2441
+ };
2442
+ };
2443
+ function normalizePlugin(input) {
2444
+ const plugin = input;
2445
+ const tools = plugin.tools;
2446
+ const hasEmbeddedExecutors = tools?.[0]?.executorCode !== void 0;
2447
+ if (hasEmbeddedExecutors) {
2448
+ return {
2449
+ ...plugin,
2450
+ enabled: plugin.enabled ?? true,
2451
+ installedAt: plugin.installedAt ?? (/* @__PURE__ */ new Date()).toISOString()
2452
+ };
2453
+ }
2454
+ const executorCode = plugin.executorCode;
2455
+ const rawTools = tools || [];
2456
+ const hookKeys = ["onRegisterCode", "beforeRequestCode", "afterResponseCode", "onErrorCode"];
2457
+ const hooksCode = {};
2458
+ for (const key of hookKeys) {
2459
+ if (executorCode?.[key]) {
2460
+ hooksCode[key] = executorCode[key];
2461
+ }
2462
+ if (plugin[key]) {
2463
+ hooksCode[key] = plugin[key];
2464
+ }
2465
+ }
2466
+ return {
2467
+ name: plugin.name,
2468
+ version: plugin.version,
2469
+ description: plugin.description,
2470
+ tools: rawTools.map((tool) => ({
2471
+ name: tool.name,
2472
+ description: tool.description,
2473
+ parameters: tool.parameters,
2474
+ executorCode: executorCode?.[tool.name]
2475
+ })),
2476
+ hooksCode: Object.keys(hooksCode).length > 0 ? hooksCode : void 0,
2477
+ enabled: true,
2478
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
2479
+ };
2480
+ }
2481
+ var savePluginExecutor = async (args2) => {
2482
+ const { plugin: rawPlugin, filename } = args2;
2483
+ if (typeof window === "undefined") {
2484
+ return { success: false, error: "Cannot save files in server environment" };
2485
+ }
2486
+ try {
2487
+ const plugin = normalizePlugin(rawPlugin);
2488
+ const json2 = JSON.stringify(plugin, null, 2);
2489
+ const blob = new Blob([json2], { type: "application/json" });
2490
+ const url = URL.createObjectURL(blob);
2491
+ const a = document.createElement("a");
2492
+ a.href = url;
2493
+ a.download = `${filename || plugin.name}.json`;
2494
+ document.body.appendChild(a);
2495
+ a.click();
2496
+ document.body.removeChild(a);
2497
+ URL.revokeObjectURL(url);
2498
+ return {
2499
+ success: true,
2500
+ message: `Plugin saved as ${filename || plugin.name}.json`
2501
+ };
2502
+ } catch (error2) {
2503
+ return {
2504
+ success: false,
2505
+ error: `Failed to save: ${error2 instanceof Error ? error2.message : "Unknown error"}`
2506
+ };
2507
+ }
2508
+ };
2509
+ var installPluginExecutor = async (args2) => {
2510
+ const { plugin: rawPlugin, enabled = true } = args2;
2511
+ if (typeof window === "undefined") {
2512
+ return { success: false, error: "Cannot install plugins in server environment" };
2513
+ }
2514
+ try {
2515
+ const plugin = normalizePlugin(rawPlugin);
2516
+ const win = window;
2517
+ if (!win.__hustleRegisterPlugin) {
2518
+ return {
2519
+ success: false,
2520
+ error: "Plugin registration not available. Make sure HustleProvider is mounted."
2521
+ };
2522
+ }
2523
+ await win.__hustleRegisterPlugin(plugin, enabled);
2524
+ return {
2525
+ success: true,
2526
+ message: `Plugin "${plugin.name}" installed${enabled ? " and enabled" : ""}.`
2527
+ };
2528
+ } catch (error2) {
2529
+ return {
2530
+ success: false,
2531
+ error: `Failed to install: ${error2 instanceof Error ? error2.message : "Unknown error"}`
2532
+ };
2533
+ }
2534
+ };
2535
+ var uninstallPluginExecutor = async (args2) => {
2536
+ const { name } = args2;
2537
+ if (typeof window === "undefined") {
2538
+ return { success: false, error: "Cannot uninstall plugins in server environment" };
2539
+ }
2540
+ try {
2541
+ const win = window;
2542
+ if (!win.__hustleUnregisterPlugin) {
2543
+ return {
2544
+ success: false,
2545
+ error: "Plugin unregistration not available. Make sure HustleProvider is mounted."
2546
+ };
2547
+ }
2548
+ await win.__hustleUnregisterPlugin(name);
2549
+ return {
2550
+ success: true,
2551
+ message: `Plugin "${name}" has been uninstalled.`
2552
+ };
2553
+ } catch (error2) {
2554
+ return {
2555
+ success: false,
2556
+ error: `Failed to uninstall: ${error2 instanceof Error ? error2.message : "Unknown error"}`
2557
+ };
2558
+ }
2559
+ };
2560
+ var listPluginsExecutor = async () => {
2561
+ if (typeof window === "undefined") {
2562
+ return { success: false, error: "Cannot list plugins in server environment" };
2563
+ }
2564
+ try {
2565
+ const win = window;
2566
+ if (!win.__hustleListPlugins) {
2567
+ return {
2568
+ success: false,
2569
+ error: "Plugin listing not available. Make sure HustleProvider is mounted."
2570
+ };
2571
+ }
2572
+ const plugins = win.__hustleListPlugins();
2573
+ return {
2574
+ success: true,
2575
+ plugins,
2576
+ 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(", ")}`
2577
+ };
2578
+ } catch (error2) {
2579
+ return {
2580
+ success: false,
2581
+ error: `Failed to list plugins: ${error2 instanceof Error ? error2.message : "Unknown error"}`
2582
+ };
2583
+ }
2584
+ };
2585
+ var modifyPluginExecutor = async (args2) => {
2586
+ const {
2587
+ name,
2588
+ version,
2589
+ description,
2590
+ addTools,
2591
+ addExecutorCode,
2592
+ removeTools,
2593
+ onRegisterCode,
2594
+ beforeRequestCode,
2595
+ afterResponseCode,
2596
+ onErrorCode
2597
+ } = args2;
2598
+ if (typeof window === "undefined") {
2599
+ return { success: false, error: "Cannot modify plugins in server environment" };
2600
+ }
2601
+ try {
2602
+ const win = window;
2603
+ if (!win.__hustleGetPlugin || !win.__hustleRegisterPlugin) {
2604
+ return {
2605
+ success: false,
2606
+ error: "Plugin modification not available. Make sure HustleProvider is mounted."
2607
+ };
2608
+ }
2609
+ const existing = win.__hustleGetPlugin(name);
2610
+ if (!existing) {
2611
+ return {
2612
+ success: false,
2613
+ error: `Plugin "${name}" not found. Use list_plugins to see installed plugins.`
2614
+ };
2615
+ }
2616
+ let tools = existing.tools ? [...existing.tools] : [];
2617
+ if (removeTools && removeTools.length > 0) {
2618
+ tools = tools.filter((t) => !removeTools.includes(t.name));
2619
+ }
2620
+ if (addTools && addTools.length > 0) {
2621
+ for (const newTool of addTools) {
2622
+ const existingIndex = tools.findIndex((t) => t.name === newTool.name);
2623
+ const toolWithExecutor = {
2624
+ ...newTool,
2625
+ executorCode: addExecutorCode?.[newTool.name] || tools.find((t) => t.name === newTool.name)?.executorCode
2626
+ };
2627
+ if (existingIndex >= 0) {
2628
+ tools[existingIndex] = toolWithExecutor;
2629
+ } else {
2630
+ tools.push(toolWithExecutor);
2631
+ }
2632
+ }
2633
+ }
2634
+ if (addExecutorCode) {
2635
+ for (const [toolName, code2] of Object.entries(addExecutorCode)) {
2636
+ const tool = tools.find((t) => t.name === toolName);
2637
+ if (tool) {
2638
+ tool.executorCode = code2;
2639
+ }
2640
+ }
2641
+ }
2642
+ const modified = {
2643
+ ...existing,
2644
+ tools
2645
+ };
2646
+ if (version) modified.version = version;
2647
+ if (description) modified.description = description;
2648
+ if (onRegisterCode || beforeRequestCode || afterResponseCode || onErrorCode) {
2649
+ modified.hooksCode = modified.hooksCode || {};
2650
+ if (onRegisterCode) modified.hooksCode.onRegisterCode = onRegisterCode;
2651
+ if (beforeRequestCode) modified.hooksCode.beforeRequestCode = beforeRequestCode;
2652
+ if (afterResponseCode) modified.hooksCode.afterResponseCode = afterResponseCode;
2653
+ if (onErrorCode) modified.hooksCode.onErrorCode = onErrorCode;
2654
+ }
2655
+ const unregisterFn = window.__hustleUnregisterPlugin;
2656
+ if (unregisterFn) {
2657
+ await unregisterFn(name);
2658
+ }
2659
+ await win.__hustleRegisterPlugin(modified, existing.enabled);
2660
+ const changes = [];
2661
+ if (version) changes.push(`version \u2192 ${version}`);
2662
+ if (description) changes.push("description updated");
2663
+ if (removeTools?.length) changes.push(`removed ${removeTools.length} tool(s)`);
2664
+ if (addTools?.length) changes.push(`added/updated ${addTools.length} tool(s)`);
2665
+ if (onRegisterCode) changes.push("onRegister hook updated");
2666
+ if (beforeRequestCode) changes.push("beforeRequest hook updated");
2667
+ if (afterResponseCode) changes.push("afterResponse hook updated");
2668
+ if (onErrorCode) changes.push("onError hook updated");
2669
+ return {
2670
+ success: true,
2671
+ message: `Plugin "${name}" modified: ${changes.join(", ")}`,
2672
+ plugin: {
2673
+ name: modified.name,
2674
+ version: modified.version,
2675
+ toolCount: tools.length
2676
+ }
2677
+ };
2678
+ } catch (error2) {
2679
+ return {
2680
+ success: false,
2681
+ error: `Failed to modify plugin: ${error2 instanceof Error ? error2.message : "Unknown error"}`
2682
+ };
2683
+ }
2684
+ };
2685
+ var pluginBuilderPlugin = {
2686
+ name: "plugin-builder",
2687
+ version: "1.2.0",
2688
+ description: "Build custom plugins through conversation",
2689
+ tools: [buildPluginTool, savePluginTool, installPluginTool, uninstallPluginTool, listPluginsTool, modifyPluginTool],
2690
+ executors: {
2691
+ build_plugin: buildPluginExecutor,
2692
+ save_plugin: savePluginExecutor,
2693
+ install_plugin: installPluginExecutor,
2694
+ uninstall_plugin: uninstallPluginExecutor,
2695
+ list_plugins: listPluginsExecutor,
2696
+ modify_plugin: modifyPluginExecutor
2697
+ },
2698
+ hooks: {
2699
+ onRegister: () => {
2700
+ console.log("[Plugin Builder] Ready to help build custom plugins");
2701
+ }
2702
+ }
2703
+ };
2704
+
1898
2705
  // src/plugins/index.ts
1899
2706
  var availablePlugins = [
1900
2707
  {
@@ -1924,6 +2731,10 @@ var availablePlugins = [
1924
2731
  {
1925
2732
  ...screenshotPlugin,
1926
2733
  description: "Take screenshots of the current page"
2734
+ },
2735
+ {
2736
+ ...pluginBuilderPlugin,
2737
+ description: "Build custom plugins through conversation with AI"
1927
2738
  }
1928
2739
  ];
1929
2740
  function getAvailablePlugin(name) {