@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.
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  'use strict';
2
3
 
3
4
  // src/plugins/predictionMarket.ts
@@ -678,12 +679,16 @@ function ensureModalStyles() {
678
679
  left: 0;
679
680
  right: 0;
680
681
  bottom: 0;
681
- background: rgba(0, 0, 0, 0.6);
682
+ background: transparent;
682
683
  display: flex;
683
684
  align-items: center;
684
685
  justify-content: center;
685
686
  z-index: 10000;
686
- animation: uqFadeIn 0.2s ease-out;
687
+ pointer-events: none;
688
+ }
689
+
690
+ .user-question-modal {
691
+ pointer-events: auto;
687
692
  }
688
693
 
689
694
  @keyframes uqFadeIn {
@@ -796,6 +801,27 @@ function ensureModalStyles() {
796
801
  color: #666;
797
802
  cursor: not-allowed;
798
803
  }
804
+
805
+ .user-question-custom-input {
806
+ width: 100%;
807
+ padding: 8px 12px;
808
+ margin-top: 8px;
809
+ background: #1a1a2e;
810
+ border: 1px solid #444;
811
+ border-radius: 6px;
812
+ color: #e0e0e0;
813
+ font-size: 14px;
814
+ box-sizing: border-box;
815
+ }
816
+
817
+ .user-question-custom-input:focus {
818
+ outline: none;
819
+ border-color: #4a7aff;
820
+ }
821
+
822
+ .user-question-custom-input::placeholder {
823
+ color: #666;
824
+ }
799
825
  `;
800
826
  document.head.appendChild(styles);
801
827
  }
@@ -817,13 +843,17 @@ var askUserTool = {
817
843
  allowMultiple: {
818
844
  type: "boolean",
819
845
  description: "If true, user can select multiple choices. Default: false"
846
+ },
847
+ allowCustom: {
848
+ type: "boolean",
849
+ description: 'If true, adds an "Other" option where user can type a custom response. Default: false'
820
850
  }
821
851
  },
822
852
  required: ["question", "choices"]
823
853
  }
824
854
  };
825
855
  var askUserExecutor = async (args2) => {
826
- const { question, choices, allowMultiple = false } = args2;
856
+ const { question, choices, allowMultiple = false, allowCustom = false } = args2;
827
857
  if (!question || !choices || !Array.isArray(choices) || choices.length === 0) {
828
858
  return {
829
859
  question: question || "",
@@ -855,6 +885,17 @@ var askUserExecutor = async (args2) => {
855
885
  choicesDiv.className = "user-question-choices";
856
886
  const inputType = allowMultiple ? "checkbox" : "radio";
857
887
  const inputName = `uq-${Date.now()}`;
888
+ let customInput = null;
889
+ let isCustomSelected = false;
890
+ const submitBtn = document.createElement("button");
891
+ submitBtn.className = "user-question-btn user-question-btn-submit";
892
+ submitBtn.textContent = "Submit";
893
+ submitBtn.disabled = true;
894
+ const updateSubmitButton = () => {
895
+ const hasSelection = selected.size > 0;
896
+ const hasCustomValue = isCustomSelected && customInput && customInput.value.trim().length > 0;
897
+ submitBtn.disabled = !hasSelection && !hasCustomValue;
898
+ };
858
899
  choices.forEach((choice, index) => {
859
900
  const choiceDiv = document.createElement("div");
860
901
  choiceDiv.className = "user-question-choice";
@@ -879,6 +920,8 @@ var askUserExecutor = async (args2) => {
879
920
  }
880
921
  } else {
881
922
  selected.clear();
923
+ isCustomSelected = false;
924
+ if (customInput) customInput.value = "";
882
925
  selected.add(choice);
883
926
  choicesDiv.querySelectorAll(".user-question-choice").forEach((c) => c.classList.remove("selected"));
884
927
  choiceDiv.classList.add("selected");
@@ -894,9 +937,62 @@ var askUserExecutor = async (args2) => {
894
937
  });
895
938
  choicesDiv.appendChild(choiceDiv);
896
939
  });
940
+ if (allowCustom) {
941
+ const customChoiceDiv = document.createElement("div");
942
+ customChoiceDiv.className = "user-question-choice";
943
+ const customRadio = document.createElement("input");
944
+ customRadio.type = inputType;
945
+ customRadio.name = inputName;
946
+ customRadio.id = `${inputName}-custom`;
947
+ customRadio.value = "__custom__";
948
+ const customLabel = document.createElement("label");
949
+ customLabel.htmlFor = customRadio.id;
950
+ customLabel.textContent = "Other:";
951
+ customLabel.style.flexShrink = "0";
952
+ customInput = document.createElement("input");
953
+ customInput.type = "text";
954
+ customInput.className = "user-question-custom-input";
955
+ customInput.placeholder = "Type your answer...";
956
+ customInput.style.marginTop = "0";
957
+ customInput.style.marginLeft = "8px";
958
+ customInput.style.flex = "1";
959
+ customChoiceDiv.appendChild(customRadio);
960
+ customChoiceDiv.appendChild(customLabel);
961
+ customChoiceDiv.appendChild(customInput);
962
+ const handleCustomSelect = () => {
963
+ if (!allowMultiple) {
964
+ selected.clear();
965
+ choicesDiv.querySelectorAll(".user-question-choice").forEach((c) => c.classList.remove("selected"));
966
+ }
967
+ isCustomSelected = true;
968
+ customChoiceDiv.classList.add("selected");
969
+ customInput?.focus();
970
+ updateSubmitButton();
971
+ };
972
+ customRadio.addEventListener("change", handleCustomSelect);
973
+ customChoiceDiv.addEventListener("click", (e) => {
974
+ if (e.target !== customRadio && e.target !== customInput) {
975
+ customRadio.checked = true;
976
+ handleCustomSelect();
977
+ }
978
+ });
979
+ customInput.addEventListener("focus", () => {
980
+ if (!customRadio.checked) {
981
+ customRadio.checked = true;
982
+ handleCustomSelect();
983
+ }
984
+ });
985
+ customInput.addEventListener("input", updateSubmitButton);
986
+ choicesDiv.appendChild(customChoiceDiv);
987
+ }
897
988
  modal.appendChild(choicesDiv);
898
989
  const actions = document.createElement("div");
899
990
  actions.className = "user-question-actions";
991
+ overlay.appendChild(modal);
992
+ document.body.appendChild(overlay);
993
+ const cleanup = () => {
994
+ overlay.remove();
995
+ };
900
996
  const cancelBtn = document.createElement("button");
901
997
  cancelBtn.className = "user-question-btn user-question-btn-cancel";
902
998
  cancelBtn.textContent = "Skip";
@@ -908,29 +1004,21 @@ var askUserExecutor = async (args2) => {
908
1004
  answered: false
909
1005
  });
910
1006
  };
911
- const submitBtn = document.createElement("button");
912
- submitBtn.className = "user-question-btn user-question-btn-submit";
913
- submitBtn.textContent = "Submit";
914
- submitBtn.disabled = true;
915
1007
  submitBtn.onclick = () => {
916
1008
  cleanup();
1009
+ const results = Array.from(selected);
1010
+ if (isCustomSelected && customInput && customInput.value.trim()) {
1011
+ results.push(customInput.value.trim());
1012
+ }
917
1013
  resolve({
918
1014
  question,
919
- selectedChoices: Array.from(selected),
1015
+ selectedChoices: results,
920
1016
  answered: true
921
1017
  });
922
1018
  };
923
- const updateSubmitButton = () => {
924
- submitBtn.disabled = selected.size === 0;
925
- };
926
1019
  actions.appendChild(cancelBtn);
927
1020
  actions.appendChild(submitBtn);
928
1021
  modal.appendChild(actions);
929
- overlay.appendChild(modal);
930
- document.body.appendChild(overlay);
931
- const cleanup = () => {
932
- overlay.remove();
933
- };
934
1022
  const handleEscape = (e) => {
935
1023
  if (e.key === "Escape") {
936
1024
  document.removeEventListener("keydown", handleEscape);
@@ -1099,6 +1187,11 @@ var screenshotTool = {
1099
1187
 
1100
1188
  The screenshot captures the visible viewport of the page. The image is uploaded to the server and a permanent URL is returned.
1101
1189
 
1190
+ IMPORTANT: Before taking a screenshot, use the ask_user tool (if available) to ask which size they prefer:
1191
+ - "full" (100%) - highest quality, larger file
1192
+ - "half" (50%) - good balance of quality and size
1193
+ - "quarter" (25%) - smallest file, faster upload
1194
+
1102
1195
  Use this when:
1103
1196
  - User asks to see what's on their screen
1104
1197
  - You need to analyze the current page visually
@@ -1109,12 +1202,24 @@ Use this when:
1109
1202
  selector: {
1110
1203
  type: "string",
1111
1204
  description: "Optional CSS selector to capture a specific element instead of the full page. Leave empty for full page screenshot."
1205
+ },
1206
+ size: {
1207
+ type: "string",
1208
+ enum: ["full", "half", "quarter"],
1209
+ description: 'Image size: "full" (100%), "half" (50%), or "quarter" (25%). Ask the user which size they prefer before capturing.'
1112
1210
  }
1113
1211
  }
1114
1212
  }
1115
1213
  };
1116
1214
  var screenshotExecutor = async (args2) => {
1117
1215
  const selector = args2.selector;
1216
+ const size = args2.size || "full";
1217
+ const scaleMap = {
1218
+ full: 1,
1219
+ half: 0.5,
1220
+ quarter: 0.25
1221
+ };
1222
+ const scale = scaleMap[size] || 1;
1118
1223
  if (typeof window === "undefined" || typeof document === "undefined") {
1119
1224
  return {
1120
1225
  success: false,
@@ -1135,34 +1240,41 @@ var screenshotExecutor = async (args2) => {
1135
1240
  if (!target) {
1136
1241
  return { success: false, error: "Element not found: " + selector };
1137
1242
  }
1138
- const canvas = await window.html2canvas(target, {
1243
+ const fullCanvas = await window.html2canvas(target, {
1139
1244
  useCORS: true,
1140
1245
  allowTaint: true,
1141
1246
  backgroundColor: "#000000",
1142
1247
  scale: 1
1143
1248
  });
1249
+ let finalCanvas = fullCanvas;
1250
+ if (scale < 1) {
1251
+ const resizedCanvas = document.createElement("canvas");
1252
+ resizedCanvas.width = Math.round(fullCanvas.width * scale);
1253
+ resizedCanvas.height = Math.round(fullCanvas.height * scale);
1254
+ const ctx = resizedCanvas.getContext("2d");
1255
+ if (ctx) {
1256
+ ctx.drawImage(fullCanvas, 0, 0, resizedCanvas.width, resizedCanvas.height);
1257
+ finalCanvas = resizedCanvas;
1258
+ }
1259
+ }
1144
1260
  const blob = await new Promise((resolve) => {
1145
- canvas.toBlob(resolve, "image/png", 0.9);
1261
+ finalCanvas.toBlob(resolve, "image/png", 0.9);
1146
1262
  });
1147
1263
  if (!blob) {
1148
1264
  return { success: false, error: "Failed to create image blob" };
1149
1265
  }
1150
- const formData = new FormData();
1151
- formData.append("file", blob, "screenshot-" + Date.now() + ".png");
1152
- const response = await fetch("/api/files/upload", {
1153
- method: "POST",
1154
- body: formData
1155
- });
1156
- if (!response.ok) {
1157
- const errorData = await response.json().catch(() => ({}));
1158
- return { success: false, error: errorData.error || "Upload failed" };
1266
+ const uploadFn = window.__hustleUploadFile;
1267
+ if (!uploadFn) {
1268
+ return { success: false, error: "Upload not available. Make sure HustleProvider is mounted and client is ready." };
1159
1269
  }
1160
- const data = await response.json();
1270
+ const fileName = "screenshot-" + Date.now() + ".png";
1271
+ const attachment = await uploadFn(blob, fileName);
1272
+ const sizeLabel = size === "full" ? "100%" : size === "half" ? "50%" : "25%";
1161
1273
  return {
1162
1274
  success: true,
1163
- url: data.url,
1164
- contentType: data.contentType,
1165
- message: "Screenshot captured and uploaded successfully"
1275
+ url: attachment.url,
1276
+ contentType: attachment.contentType || "image/png",
1277
+ message: `Screenshot captured at ${sizeLabel} size (${finalCanvas.width}x${finalCanvas.height}) and uploaded successfully`
1166
1278
  };
1167
1279
  } catch (e) {
1168
1280
  const err = e;
@@ -1192,6 +1304,21 @@ var buildPluginTool = {
1192
1304
  name: "build_plugin",
1193
1305
  description: `Build a Hustle plugin definition. Use this tool to construct a plugin based on user requirements.
1194
1306
 
1307
+ ## Testing Before Building (IMPORTANT)
1308
+
1309
+ 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.
1310
+
1311
+ **Workflow:**
1312
+ 1. When a user describes what they want, first test the core logic using execute_javascript
1313
+ 2. Iterate on the code until it works correctly
1314
+ 3. Ask the user: "Would you like to test this further, or should I build it into a plugin?"
1315
+ 4. Only call build_plugin once the code is validated and the user confirms
1316
+
1317
+ **Example:** If building a weather plugin, first test the API call:
1318
+ execute_javascript({ code: "fetch('https://api.example.com/weather?city=London').then(r => r.json()).then(console.log)" })
1319
+
1320
+ This catches errors early and lets users see results before committing to a plugin.
1321
+
1195
1322
  ## Plugin Structure
1196
1323
 
1197
1324
  A plugin consists of:
@@ -1230,21 +1357,127 @@ Write them as arrow function strings that will be eval'd:
1230
1357
  "async (args) => { const { city } = args; return { weather: 'sunny', city }; }"
1231
1358
 
1232
1359
  The function receives args as Record<string, unknown> and should return the result.
1233
- You can use fetch(), standard browser APIs, and async/await.
1360
+
1361
+ ## Available in Executor Scope
1362
+
1363
+ Executors run in the browser context with full access to:
1364
+
1365
+ ### Browser APIs
1366
+ - **fetch(url, options)** - HTTP requests (subject to CORS)
1367
+ - **localStorage / sessionStorage** - Persistent storage
1368
+ - **document** - Full DOM access (create elements, modals, forms, etc.)
1369
+ - **window** - Global window object
1370
+ - **console** - Logging (log, warn, error, etc.)
1371
+ - **setTimeout / setInterval / clearTimeout / clearInterval** - Timers
1372
+ - **JSON** - Parse and stringify
1373
+ - **Date** - Date/time operations
1374
+ - **URL / URLSearchParams** - URL manipulation
1375
+ - **FormData / Blob / File / FileReader** - File handling
1376
+ - **crypto** - Cryptographic operations (crypto.randomUUID(), etc.)
1377
+ - **navigator** - Browser info, clipboard, geolocation, etc.
1378
+ - **location** - Current URL info
1379
+ - **history** - Browser history navigation
1380
+ - **WebSocket** - Real-time bidirectional communication
1381
+ - **EventSource** - Server-sent events
1382
+ - **indexedDB** - Client-side database for large data
1383
+ - **Notification** - Browser notifications (requires permission)
1384
+ - **performance** - Performance timing
1385
+ - **atob / btoa** - Base64 encoding/decoding
1386
+ - **TextEncoder / TextDecoder** - Text encoding
1387
+ - **AbortController** - Cancel fetch requests
1388
+ - **IntersectionObserver / MutationObserver / ResizeObserver** - DOM observers
1389
+ - **requestAnimationFrame** - Animation timing
1390
+ - **speechSynthesis** - Text-to-speech
1391
+ - **Audio / Image / Canvas** - Media APIs
1392
+
1393
+ ### Hustle Plugin System Globals
1394
+ - **window.__hustleInstanceId** - Current Hustle instance ID
1395
+ - **window.__hustleRegisterPlugin(plugin, enabled)** - Install another plugin dynamically
1396
+ - **window.__hustleUnregisterPlugin(name)** - Uninstall a plugin by name
1397
+ - **window.__hustleUploadFile(file, fileName?)** - Upload a File/Blob to the server, returns { url, contentType }
1398
+ - **window.__hustleListPlugins()** - List all installed plugins
1399
+ - **window.__hustleGetPlugin(name)** - Get a specific plugin by name
1400
+
1401
+ ### DOM Manipulation Examples
1402
+ Create a modal: document.createElement('div'), style it, append to document.body
1403
+ Add event listeners: element.addEventListener('click', handler)
1404
+ Query elements: document.querySelector(), document.querySelectorAll()
1405
+
1406
+ ### Storage Patterns
1407
+ Store data: localStorage.setItem('key', JSON.stringify(data))
1408
+ Retrieve data: JSON.parse(localStorage.getItem('key') || '{}')
1409
+ Namespace your keys: Use plugin name prefix like "myplugin-settings"
1410
+
1411
+ ### Async Patterns
1412
+ All executors should be async. Use await for promises:
1413
+ "async (args) => { const res = await fetch(url); return await res.json(); }"
1234
1414
 
1235
1415
  ## Lifecycle Hooks (Optional)
1236
1416
 
1237
- - **beforeRequestCode**: Modify messages before sending. Receives request object, must return modified request.
1417
+ Hooks also have full access to the browser scope described above.
1418
+
1419
+ - **onRegisterCode**: Called once when plugin is registered/enabled. Good for initialization.
1420
+ **IMPORTANT: Always log when your plugin registers so users know it's active!**
1421
+ Example: "async () => { console.log('[MyPlugin] v1.0.0 registered'); }"
1422
+
1423
+ - **beforeRequestCode**: Modify messages before sending. Receives request object with { messages, model, ... }. Must return the modified request.
1238
1424
  Example: "async (req) => { req.messages = req.messages.map(m => ({...m, content: m.content.toUpperCase()})); return req; }"
1239
1425
 
1240
- - **afterResponseCode**: Process response after receiving. Receives response object.
1241
- Example: "async (res) => { console.log('Response:', res.content); }"
1426
+ - **afterResponseCode**: Process/modify response after receiving. Receives response object with { content, ... }.
1427
+ Example: "async (res) => { console.log('Response received:', res.content.substring(0, 100)); }"
1242
1428
 
1243
- - **onRegisterCode**: Called when plugin is registered.
1244
- Example: "async () => { console.log('Plugin loaded!'); }"
1429
+ - **onErrorCode**: Called on errors. Receives (error, context) where context has { phase: 'beforeRequest'|'execute'|'afterResponse' }.
1430
+ Example: "async (error, ctx) => { console.error('[MyPlugin] Error in', ctx.phase, ':', error.message); }"
1245
1431
 
1246
- - **onErrorCode**: Called on errors. Receives (error, context).
1247
- Example: "async (error, ctx) => { console.error('Error in', ctx.phase, error); }"`,
1432
+ ## Best Practices
1433
+
1434
+ 1. **Always add onRegisterCode** that logs the plugin name and version
1435
+ 2. **Namespace console logs** with [PluginName] prefix for easy identification
1436
+ 3. **Handle errors gracefully** in executors - return { error: message } instead of throwing
1437
+
1438
+ ## Building Plugin UI
1439
+
1440
+ Plugins can embed custom UI elements in two ways:
1441
+
1442
+ ### Persistent UI (via onRegister hook)
1443
+ 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:
1444
+
1445
+ "async () => {
1446
+ console.log('[MyPlugin] v1.0.0 registered');
1447
+ if (document.getElementById('my-plugin-panel')) return; // Already exists
1448
+
1449
+ const panel = document.createElement('div');
1450
+ panel.id = 'my-plugin-panel';
1451
+ Object.assign(panel.style, { position: 'fixed', bottom: '20px', right: '20px', padding: '10px', background: '#333', color: '#fff' });
1452
+ panel.textContent = 'My Panel';
1453
+ document.body.appendChild(panel);
1454
+ }"
1455
+
1456
+ ### On-Demand UI (via tool executors)
1457
+ Embed UI just-in-time when a tool is invoked. Useful for tools like show_clock, display_chart, etc.:
1458
+
1459
+ "async (args) => {
1460
+ const modal = document.createElement('div');
1461
+ modal.id = 'clock-modal';
1462
+ Object.assign(modal.style, { position: 'fixed', inset: '0', display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'rgba(0,0,0,0.5)' });
1463
+ modal.textContent = 'Current time: ' + new Date().toLocaleTimeString();
1464
+ modal.onclick = () => modal.remove(); // Click to dismiss
1465
+ document.body.appendChild(modal);
1466
+ return { success: true, message: 'Clock displayed' };
1467
+ }"
1468
+
1469
+ ### UI Best Practices
1470
+ - Always use unique IDs with your plugin name prefix (e.g., "myplugin-modal")
1471
+ - For persistent UI, check existence in onRegister before creating
1472
+ - Provide a way to dismiss/close UI elements (click handler, close button)
1473
+ - Use Object.assign(el.style, {...}) for inline styles or inject a <style> tag
1474
+ - Clean up UI in tool executors if appropriate (e.g., remove after timeout)
1475
+
1476
+ ## Security Notes
1477
+ - Code runs in browser sandbox with same-origin policy
1478
+ - fetch() is subject to CORS restrictions
1479
+ - No direct filesystem access (use File API with user interaction)
1480
+ - Sanitize any user-provided content before inserting into the DOM`,
1248
1481
  parameters: {
1249
1482
  type: "object",
1250
1483
  properties: {
@@ -1333,6 +1566,104 @@ var installPluginTool = {
1333
1566
  required: ["plugin"]
1334
1567
  }
1335
1568
  };
1569
+ var uninstallPluginTool = {
1570
+ name: "uninstall_plugin",
1571
+ description: "Uninstall a plugin by name, removing it from browser storage.",
1572
+ parameters: {
1573
+ type: "object",
1574
+ properties: {
1575
+ name: {
1576
+ type: "string",
1577
+ description: "The name of the plugin to uninstall"
1578
+ }
1579
+ },
1580
+ required: ["name"]
1581
+ }
1582
+ };
1583
+ var listPluginsTool = {
1584
+ name: "list_plugins",
1585
+ description: "List all installed plugins with their enabled/disabled status.",
1586
+ parameters: {
1587
+ type: "object",
1588
+ properties: {},
1589
+ required: []
1590
+ }
1591
+ };
1592
+ var modifyPluginTool = {
1593
+ name: "modify_plugin",
1594
+ description: `Modify an existing installed plugin. Can update version, description, tools, executors, and hooks.
1595
+
1596
+ Use list_plugins first to see installed plugins, then modify by name.
1597
+
1598
+ You can:
1599
+ - Add new tools (provide tools array with new tools to add)
1600
+ - Update existing tools (provide tool with same name)
1601
+ - Remove tools (set removeTool to the tool name)
1602
+ - Update hooks (provide hook code)
1603
+ - Update version/description
1604
+
1605
+ Example: Add a new tool to existing plugin:
1606
+ {
1607
+ "name": "my-plugin",
1608
+ "addTools": [{ "name": "new_tool", "description": "...", "parameters": {...} }],
1609
+ "addExecutorCode": { "new_tool": "async (args) => { ... }" }
1610
+ }`,
1611
+ parameters: {
1612
+ type: "object",
1613
+ properties: {
1614
+ name: {
1615
+ type: "string",
1616
+ description: "Name of the plugin to modify (required)"
1617
+ },
1618
+ version: {
1619
+ type: "string",
1620
+ description: "New version string"
1621
+ },
1622
+ description: {
1623
+ type: "string",
1624
+ description: "New description"
1625
+ },
1626
+ addTools: {
1627
+ type: "array",
1628
+ description: "Tools to add or update",
1629
+ items: {
1630
+ type: "object",
1631
+ properties: {
1632
+ name: { type: "string" },
1633
+ description: { type: "string" },
1634
+ parameters: { type: "object" }
1635
+ }
1636
+ }
1637
+ },
1638
+ addExecutorCode: {
1639
+ type: "object",
1640
+ description: "Executor code to add/update (tool name -> code string)"
1641
+ },
1642
+ removeTools: {
1643
+ type: "array",
1644
+ description: "Names of tools to remove",
1645
+ items: { type: "string" }
1646
+ },
1647
+ onRegisterCode: {
1648
+ type: "string",
1649
+ description: "New onRegister hook code"
1650
+ },
1651
+ beforeRequestCode: {
1652
+ type: "string",
1653
+ description: "New beforeRequest hook code"
1654
+ },
1655
+ afterResponseCode: {
1656
+ type: "string",
1657
+ description: "New afterResponse hook code"
1658
+ },
1659
+ onErrorCode: {
1660
+ type: "string",
1661
+ description: "New onError hook code"
1662
+ }
1663
+ },
1664
+ required: ["name"]
1665
+ }
1666
+ };
1336
1667
  var buildPluginExecutor = async (args2) => {
1337
1668
  const {
1338
1669
  name,
@@ -1389,12 +1720,51 @@ var buildPluginExecutor = async (args2) => {
1389
1720
  message: `Plugin "${name}" v${version} built successfully with ${tools.length} tool(s). Use save_plugin to download or install_plugin to add to browser storage.`
1390
1721
  };
1391
1722
  };
1723
+ function normalizePlugin(input) {
1724
+ const plugin = input;
1725
+ const tools = plugin.tools;
1726
+ const hasEmbeddedExecutors = tools?.[0]?.executorCode !== void 0;
1727
+ if (hasEmbeddedExecutors) {
1728
+ return {
1729
+ ...plugin,
1730
+ enabled: plugin.enabled ?? true,
1731
+ installedAt: plugin.installedAt ?? (/* @__PURE__ */ new Date()).toISOString()
1732
+ };
1733
+ }
1734
+ const executorCode = plugin.executorCode;
1735
+ const rawTools = tools || [];
1736
+ const hookKeys = ["onRegisterCode", "beforeRequestCode", "afterResponseCode", "onErrorCode"];
1737
+ const hooksCode = {};
1738
+ for (const key of hookKeys) {
1739
+ if (executorCode?.[key]) {
1740
+ hooksCode[key] = executorCode[key];
1741
+ }
1742
+ if (plugin[key]) {
1743
+ hooksCode[key] = plugin[key];
1744
+ }
1745
+ }
1746
+ return {
1747
+ name: plugin.name,
1748
+ version: plugin.version,
1749
+ description: plugin.description,
1750
+ tools: rawTools.map((tool) => ({
1751
+ name: tool.name,
1752
+ description: tool.description,
1753
+ parameters: tool.parameters,
1754
+ executorCode: executorCode?.[tool.name]
1755
+ })),
1756
+ hooksCode: Object.keys(hooksCode).length > 0 ? hooksCode : void 0,
1757
+ enabled: true,
1758
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
1759
+ };
1760
+ }
1392
1761
  var savePluginExecutor = async (args2) => {
1393
- const { plugin, filename } = args2;
1762
+ const { plugin: rawPlugin, filename } = args2;
1394
1763
  if (typeof window === "undefined") {
1395
1764
  return { success: false, error: "Cannot save files in server environment" };
1396
1765
  }
1397
1766
  try {
1767
+ const plugin = normalizePlugin(rawPlugin);
1398
1768
  const json = JSON.stringify(plugin, null, 2);
1399
1769
  const blob = new Blob([json], { type: "application/json" });
1400
1770
  const url = URL.createObjectURL(blob);
@@ -1417,31 +1787,23 @@ var savePluginExecutor = async (args2) => {
1417
1787
  }
1418
1788
  };
1419
1789
  var installPluginExecutor = async (args2) => {
1420
- const { plugin, enabled = true } = args2;
1790
+ const { plugin: rawPlugin, enabled = true } = args2;
1421
1791
  if (typeof window === "undefined") {
1422
1792
  return { success: false, error: "Cannot install plugins in server environment" };
1423
1793
  }
1424
1794
  try {
1425
- const instanceId = window.__hustleInstanceId || "global-demo";
1426
- const STORAGE_KEY = `hustle-plugins-${instanceId}`;
1427
- const stored = localStorage.getItem(STORAGE_KEY);
1428
- const plugins = stored ? JSON.parse(stored) : [];
1429
- const existingIndex = plugins.findIndex((p) => p.name === plugin.name);
1430
- const pluginToStore = {
1431
- ...plugin,
1432
- enabled,
1433
- installedAt: (/* @__PURE__ */ new Date()).toISOString()
1434
- };
1435
- if (existingIndex >= 0) {
1436
- plugins[existingIndex] = pluginToStore;
1437
- } else {
1438
- plugins.push(pluginToStore);
1795
+ const plugin = normalizePlugin(rawPlugin);
1796
+ const win = window;
1797
+ if (!win.__hustleRegisterPlugin) {
1798
+ return {
1799
+ success: false,
1800
+ error: "Plugin registration not available. Make sure HustleProvider is mounted."
1801
+ };
1439
1802
  }
1440
- localStorage.setItem(STORAGE_KEY, JSON.stringify(plugins));
1803
+ await win.__hustleRegisterPlugin(plugin, enabled);
1441
1804
  return {
1442
1805
  success: true,
1443
- message: `Plugin "${plugin.name}" installed${enabled ? " and enabled" : ""}. Refresh the page or re-register plugins to activate.`,
1444
- action: existingIndex >= 0 ? "updated" : "installed"
1806
+ message: `Plugin "${plugin.name}" installed${enabled ? " and enabled" : ""}.`
1445
1807
  };
1446
1808
  } catch (error2) {
1447
1809
  return {
@@ -1450,15 +1812,168 @@ var installPluginExecutor = async (args2) => {
1450
1812
  };
1451
1813
  }
1452
1814
  };
1815
+ var uninstallPluginExecutor = async (args2) => {
1816
+ const { name } = args2;
1817
+ if (typeof window === "undefined") {
1818
+ return { success: false, error: "Cannot uninstall plugins in server environment" };
1819
+ }
1820
+ try {
1821
+ const win = window;
1822
+ if (!win.__hustleUnregisterPlugin) {
1823
+ return {
1824
+ success: false,
1825
+ error: "Plugin unregistration not available. Make sure HustleProvider is mounted."
1826
+ };
1827
+ }
1828
+ await win.__hustleUnregisterPlugin(name);
1829
+ return {
1830
+ success: true,
1831
+ message: `Plugin "${name}" has been uninstalled.`
1832
+ };
1833
+ } catch (error2) {
1834
+ return {
1835
+ success: false,
1836
+ error: `Failed to uninstall: ${error2 instanceof Error ? error2.message : "Unknown error"}`
1837
+ };
1838
+ }
1839
+ };
1840
+ var listPluginsExecutor = async () => {
1841
+ if (typeof window === "undefined") {
1842
+ return { success: false, error: "Cannot list plugins in server environment" };
1843
+ }
1844
+ try {
1845
+ const win = window;
1846
+ if (!win.__hustleListPlugins) {
1847
+ return {
1848
+ success: false,
1849
+ error: "Plugin listing not available. Make sure HustleProvider is mounted."
1850
+ };
1851
+ }
1852
+ const plugins = win.__hustleListPlugins();
1853
+ return {
1854
+ success: true,
1855
+ plugins,
1856
+ 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(", ")}`
1857
+ };
1858
+ } catch (error2) {
1859
+ return {
1860
+ success: false,
1861
+ error: `Failed to list plugins: ${error2 instanceof Error ? error2.message : "Unknown error"}`
1862
+ };
1863
+ }
1864
+ };
1865
+ var modifyPluginExecutor = async (args2) => {
1866
+ const {
1867
+ name,
1868
+ version,
1869
+ description,
1870
+ addTools,
1871
+ addExecutorCode,
1872
+ removeTools,
1873
+ onRegisterCode,
1874
+ beforeRequestCode,
1875
+ afterResponseCode,
1876
+ onErrorCode
1877
+ } = args2;
1878
+ if (typeof window === "undefined") {
1879
+ return { success: false, error: "Cannot modify plugins in server environment" };
1880
+ }
1881
+ try {
1882
+ const win = window;
1883
+ if (!win.__hustleGetPlugin || !win.__hustleRegisterPlugin) {
1884
+ return {
1885
+ success: false,
1886
+ error: "Plugin modification not available. Make sure HustleProvider is mounted."
1887
+ };
1888
+ }
1889
+ const existing = win.__hustleGetPlugin(name);
1890
+ if (!existing) {
1891
+ return {
1892
+ success: false,
1893
+ error: `Plugin "${name}" not found. Use list_plugins to see installed plugins.`
1894
+ };
1895
+ }
1896
+ let tools = existing.tools ? [...existing.tools] : [];
1897
+ if (removeTools && removeTools.length > 0) {
1898
+ tools = tools.filter((t) => !removeTools.includes(t.name));
1899
+ }
1900
+ if (addTools && addTools.length > 0) {
1901
+ for (const newTool of addTools) {
1902
+ const existingIndex = tools.findIndex((t) => t.name === newTool.name);
1903
+ const toolWithExecutor = {
1904
+ ...newTool,
1905
+ executorCode: addExecutorCode?.[newTool.name] || tools.find((t) => t.name === newTool.name)?.executorCode
1906
+ };
1907
+ if (existingIndex >= 0) {
1908
+ tools[existingIndex] = toolWithExecutor;
1909
+ } else {
1910
+ tools.push(toolWithExecutor);
1911
+ }
1912
+ }
1913
+ }
1914
+ if (addExecutorCode) {
1915
+ for (const [toolName, code2] of Object.entries(addExecutorCode)) {
1916
+ const tool = tools.find((t) => t.name === toolName);
1917
+ if (tool) {
1918
+ tool.executorCode = code2;
1919
+ }
1920
+ }
1921
+ }
1922
+ const modified = {
1923
+ ...existing,
1924
+ tools
1925
+ };
1926
+ if (version) modified.version = version;
1927
+ if (description) modified.description = description;
1928
+ if (onRegisterCode || beforeRequestCode || afterResponseCode || onErrorCode) {
1929
+ modified.hooksCode = modified.hooksCode || {};
1930
+ if (onRegisterCode) modified.hooksCode.onRegisterCode = onRegisterCode;
1931
+ if (beforeRequestCode) modified.hooksCode.beforeRequestCode = beforeRequestCode;
1932
+ if (afterResponseCode) modified.hooksCode.afterResponseCode = afterResponseCode;
1933
+ if (onErrorCode) modified.hooksCode.onErrorCode = onErrorCode;
1934
+ }
1935
+ const unregisterFn = window.__hustleUnregisterPlugin;
1936
+ if (unregisterFn) {
1937
+ await unregisterFn(name);
1938
+ }
1939
+ await win.__hustleRegisterPlugin(modified, existing.enabled);
1940
+ const changes = [];
1941
+ if (version) changes.push(`version \u2192 ${version}`);
1942
+ if (description) changes.push("description updated");
1943
+ if (removeTools?.length) changes.push(`removed ${removeTools.length} tool(s)`);
1944
+ if (addTools?.length) changes.push(`added/updated ${addTools.length} tool(s)`);
1945
+ if (onRegisterCode) changes.push("onRegister hook updated");
1946
+ if (beforeRequestCode) changes.push("beforeRequest hook updated");
1947
+ if (afterResponseCode) changes.push("afterResponse hook updated");
1948
+ if (onErrorCode) changes.push("onError hook updated");
1949
+ return {
1950
+ success: true,
1951
+ message: `Plugin "${name}" modified: ${changes.join(", ")}`,
1952
+ plugin: {
1953
+ name: modified.name,
1954
+ version: modified.version,
1955
+ toolCount: tools.length
1956
+ }
1957
+ };
1958
+ } catch (error2) {
1959
+ return {
1960
+ success: false,
1961
+ error: `Failed to modify plugin: ${error2 instanceof Error ? error2.message : "Unknown error"}`
1962
+ };
1963
+ }
1964
+ };
1453
1965
  var pluginBuilderPlugin = {
1454
1966
  name: "plugin-builder",
1455
- version: "1.0.0",
1967
+ version: "1.2.0",
1456
1968
  description: "Build custom plugins through conversation",
1457
- tools: [buildPluginTool, savePluginTool, installPluginTool],
1969
+ tools: [buildPluginTool, savePluginTool, installPluginTool, uninstallPluginTool, listPluginsTool, modifyPluginTool],
1458
1970
  executors: {
1459
1971
  build_plugin: buildPluginExecutor,
1460
1972
  save_plugin: savePluginExecutor,
1461
- install_plugin: installPluginExecutor
1973
+ install_plugin: installPluginExecutor,
1974
+ uninstall_plugin: uninstallPluginExecutor,
1975
+ list_plugins: listPluginsExecutor,
1976
+ modify_plugin: modifyPluginExecutor
1462
1977
  },
1463
1978
  hooks: {
1464
1979
  onRegister: () => {