@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.
@@ -678,12 +678,16 @@ function ensureModalStyles() {
678
678
  left: 0;
679
679
  right: 0;
680
680
  bottom: 0;
681
- background: rgba(0, 0, 0, 0.6);
681
+ background: transparent;
682
682
  display: flex;
683
683
  align-items: center;
684
684
  justify-content: center;
685
685
  z-index: 10000;
686
- animation: uqFadeIn 0.2s ease-out;
686
+ pointer-events: none;
687
+ }
688
+
689
+ .user-question-modal {
690
+ pointer-events: auto;
687
691
  }
688
692
 
689
693
  @keyframes uqFadeIn {
@@ -796,6 +800,27 @@ function ensureModalStyles() {
796
800
  color: #666;
797
801
  cursor: not-allowed;
798
802
  }
803
+
804
+ .user-question-custom-input {
805
+ width: 100%;
806
+ padding: 8px 12px;
807
+ margin-top: 8px;
808
+ background: #1a1a2e;
809
+ border: 1px solid #444;
810
+ border-radius: 6px;
811
+ color: #e0e0e0;
812
+ font-size: 14px;
813
+ box-sizing: border-box;
814
+ }
815
+
816
+ .user-question-custom-input:focus {
817
+ outline: none;
818
+ border-color: #4a7aff;
819
+ }
820
+
821
+ .user-question-custom-input::placeholder {
822
+ color: #666;
823
+ }
799
824
  `;
800
825
  document.head.appendChild(styles);
801
826
  }
@@ -817,13 +842,17 @@ var askUserTool = {
817
842
  allowMultiple: {
818
843
  type: "boolean",
819
844
  description: "If true, user can select multiple choices. Default: false"
845
+ },
846
+ allowCustom: {
847
+ type: "boolean",
848
+ description: 'If true, adds an "Other" option where user can type a custom response. Default: false'
820
849
  }
821
850
  },
822
851
  required: ["question", "choices"]
823
852
  }
824
853
  };
825
854
  var askUserExecutor = async (args2) => {
826
- const { question, choices, allowMultiple = false } = args2;
855
+ const { question, choices, allowMultiple = false, allowCustom = false } = args2;
827
856
  if (!question || !choices || !Array.isArray(choices) || choices.length === 0) {
828
857
  return {
829
858
  question: question || "",
@@ -855,6 +884,17 @@ var askUserExecutor = async (args2) => {
855
884
  choicesDiv.className = "user-question-choices";
856
885
  const inputType = allowMultiple ? "checkbox" : "radio";
857
886
  const inputName = `uq-${Date.now()}`;
887
+ let customInput = null;
888
+ let isCustomSelected = false;
889
+ const submitBtn = document.createElement("button");
890
+ submitBtn.className = "user-question-btn user-question-btn-submit";
891
+ submitBtn.textContent = "Submit";
892
+ submitBtn.disabled = true;
893
+ const updateSubmitButton = () => {
894
+ const hasSelection = selected.size > 0;
895
+ const hasCustomValue = isCustomSelected && customInput && customInput.value.trim().length > 0;
896
+ submitBtn.disabled = !hasSelection && !hasCustomValue;
897
+ };
858
898
  choices.forEach((choice, index) => {
859
899
  const choiceDiv = document.createElement("div");
860
900
  choiceDiv.className = "user-question-choice";
@@ -879,6 +919,8 @@ var askUserExecutor = async (args2) => {
879
919
  }
880
920
  } else {
881
921
  selected.clear();
922
+ isCustomSelected = false;
923
+ if (customInput) customInput.value = "";
882
924
  selected.add(choice);
883
925
  choicesDiv.querySelectorAll(".user-question-choice").forEach((c) => c.classList.remove("selected"));
884
926
  choiceDiv.classList.add("selected");
@@ -894,9 +936,62 @@ var askUserExecutor = async (args2) => {
894
936
  });
895
937
  choicesDiv.appendChild(choiceDiv);
896
938
  });
939
+ if (allowCustom) {
940
+ const customChoiceDiv = document.createElement("div");
941
+ customChoiceDiv.className = "user-question-choice";
942
+ const customRadio = document.createElement("input");
943
+ customRadio.type = inputType;
944
+ customRadio.name = inputName;
945
+ customRadio.id = `${inputName}-custom`;
946
+ customRadio.value = "__custom__";
947
+ const customLabel = document.createElement("label");
948
+ customLabel.htmlFor = customRadio.id;
949
+ customLabel.textContent = "Other:";
950
+ customLabel.style.flexShrink = "0";
951
+ customInput = document.createElement("input");
952
+ customInput.type = "text";
953
+ customInput.className = "user-question-custom-input";
954
+ customInput.placeholder = "Type your answer...";
955
+ customInput.style.marginTop = "0";
956
+ customInput.style.marginLeft = "8px";
957
+ customInput.style.flex = "1";
958
+ customChoiceDiv.appendChild(customRadio);
959
+ customChoiceDiv.appendChild(customLabel);
960
+ customChoiceDiv.appendChild(customInput);
961
+ const handleCustomSelect = () => {
962
+ if (!allowMultiple) {
963
+ selected.clear();
964
+ choicesDiv.querySelectorAll(".user-question-choice").forEach((c) => c.classList.remove("selected"));
965
+ }
966
+ isCustomSelected = true;
967
+ customChoiceDiv.classList.add("selected");
968
+ customInput?.focus();
969
+ updateSubmitButton();
970
+ };
971
+ customRadio.addEventListener("change", handleCustomSelect);
972
+ customChoiceDiv.addEventListener("click", (e) => {
973
+ if (e.target !== customRadio && e.target !== customInput) {
974
+ customRadio.checked = true;
975
+ handleCustomSelect();
976
+ }
977
+ });
978
+ customInput.addEventListener("focus", () => {
979
+ if (!customRadio.checked) {
980
+ customRadio.checked = true;
981
+ handleCustomSelect();
982
+ }
983
+ });
984
+ customInput.addEventListener("input", updateSubmitButton);
985
+ choicesDiv.appendChild(customChoiceDiv);
986
+ }
897
987
  modal.appendChild(choicesDiv);
898
988
  const actions = document.createElement("div");
899
989
  actions.className = "user-question-actions";
990
+ overlay.appendChild(modal);
991
+ document.body.appendChild(overlay);
992
+ const cleanup = () => {
993
+ overlay.remove();
994
+ };
900
995
  const cancelBtn = document.createElement("button");
901
996
  cancelBtn.className = "user-question-btn user-question-btn-cancel";
902
997
  cancelBtn.textContent = "Skip";
@@ -908,29 +1003,21 @@ var askUserExecutor = async (args2) => {
908
1003
  answered: false
909
1004
  });
910
1005
  };
911
- const submitBtn = document.createElement("button");
912
- submitBtn.className = "user-question-btn user-question-btn-submit";
913
- submitBtn.textContent = "Submit";
914
- submitBtn.disabled = true;
915
1006
  submitBtn.onclick = () => {
916
1007
  cleanup();
1008
+ const results = Array.from(selected);
1009
+ if (isCustomSelected && customInput && customInput.value.trim()) {
1010
+ results.push(customInput.value.trim());
1011
+ }
917
1012
  resolve({
918
1013
  question,
919
- selectedChoices: Array.from(selected),
1014
+ selectedChoices: results,
920
1015
  answered: true
921
1016
  });
922
1017
  };
923
- const updateSubmitButton = () => {
924
- submitBtn.disabled = selected.size === 0;
925
- };
926
1018
  actions.appendChild(cancelBtn);
927
1019
  actions.appendChild(submitBtn);
928
1020
  modal.appendChild(actions);
929
- overlay.appendChild(modal);
930
- document.body.appendChild(overlay);
931
- const cleanup = () => {
932
- overlay.remove();
933
- };
934
1021
  const handleEscape = (e) => {
935
1022
  if (e.key === "Escape") {
936
1023
  document.removeEventListener("keydown", handleEscape);
@@ -1099,6 +1186,11 @@ var screenshotTool = {
1099
1186
 
1100
1187
  The screenshot captures the visible viewport of the page. The image is uploaded to the server and a permanent URL is returned.
1101
1188
 
1189
+ IMPORTANT: Before taking a screenshot, use the ask_user tool (if available) to ask which size they prefer:
1190
+ - "full" (100%) - highest quality, larger file
1191
+ - "half" (50%) - good balance of quality and size
1192
+ - "quarter" (25%) - smallest file, faster upload
1193
+
1102
1194
  Use this when:
1103
1195
  - User asks to see what's on their screen
1104
1196
  - You need to analyze the current page visually
@@ -1109,12 +1201,24 @@ Use this when:
1109
1201
  selector: {
1110
1202
  type: "string",
1111
1203
  description: "Optional CSS selector to capture a specific element instead of the full page. Leave empty for full page screenshot."
1204
+ },
1205
+ size: {
1206
+ type: "string",
1207
+ enum: ["full", "half", "quarter"],
1208
+ description: 'Image size: "full" (100%), "half" (50%), or "quarter" (25%). Ask the user which size they prefer before capturing.'
1112
1209
  }
1113
1210
  }
1114
1211
  }
1115
1212
  };
1116
1213
  var screenshotExecutor = async (args2) => {
1117
1214
  const selector = args2.selector;
1215
+ const size = args2.size || "full";
1216
+ const scaleMap = {
1217
+ full: 1,
1218
+ half: 0.5,
1219
+ quarter: 0.25
1220
+ };
1221
+ const scale = scaleMap[size] || 1;
1118
1222
  if (typeof window === "undefined" || typeof document === "undefined") {
1119
1223
  return {
1120
1224
  success: false,
@@ -1135,34 +1239,41 @@ var screenshotExecutor = async (args2) => {
1135
1239
  if (!target) {
1136
1240
  return { success: false, error: "Element not found: " + selector };
1137
1241
  }
1138
- const canvas = await window.html2canvas(target, {
1242
+ const fullCanvas = await window.html2canvas(target, {
1139
1243
  useCORS: true,
1140
1244
  allowTaint: true,
1141
1245
  backgroundColor: "#000000",
1142
1246
  scale: 1
1143
1247
  });
1248
+ let finalCanvas = fullCanvas;
1249
+ if (scale < 1) {
1250
+ const resizedCanvas = document.createElement("canvas");
1251
+ resizedCanvas.width = Math.round(fullCanvas.width * scale);
1252
+ resizedCanvas.height = Math.round(fullCanvas.height * scale);
1253
+ const ctx = resizedCanvas.getContext("2d");
1254
+ if (ctx) {
1255
+ ctx.drawImage(fullCanvas, 0, 0, resizedCanvas.width, resizedCanvas.height);
1256
+ finalCanvas = resizedCanvas;
1257
+ }
1258
+ }
1144
1259
  const blob = await new Promise((resolve) => {
1145
- canvas.toBlob(resolve, "image/png", 0.9);
1260
+ finalCanvas.toBlob(resolve, "image/png", 0.9);
1146
1261
  });
1147
1262
  if (!blob) {
1148
1263
  return { success: false, error: "Failed to create image blob" };
1149
1264
  }
1150
- const formData = new FormData();
1151
- formData.append("file", blob, "screenshot-" + Date.now() + ".png");
1152
- const response = await fetch("/api/files/upload", {
1153
- method: "POST",
1154
- body: formData
1155
- });
1156
- if (!response.ok) {
1157
- const errorData = await response.json().catch(() => ({}));
1158
- return { success: false, error: errorData.error || "Upload failed" };
1265
+ const uploadFn = window.__hustleUploadFile;
1266
+ if (!uploadFn) {
1267
+ return { success: false, error: "Upload not available. Make sure HustleProvider is mounted and client is ready." };
1159
1268
  }
1160
- const data = await response.json();
1269
+ const fileName = "screenshot-" + Date.now() + ".png";
1270
+ const attachment = await uploadFn(blob, fileName);
1271
+ const sizeLabel = size === "full" ? "100%" : size === "half" ? "50%" : "25%";
1161
1272
  return {
1162
1273
  success: true,
1163
- url: data.url,
1164
- contentType: data.contentType,
1165
- message: "Screenshot captured and uploaded successfully"
1274
+ url: attachment.url,
1275
+ contentType: attachment.contentType || "image/png",
1276
+ message: `Screenshot captured at ${sizeLabel} size (${finalCanvas.width}x${finalCanvas.height}) and uploaded successfully`
1166
1277
  };
1167
1278
  } catch (e) {
1168
1279
  const err = e;
@@ -1187,6 +1298,636 @@ var screenshotPlugin = {
1187
1298
  }
1188
1299
  };
1189
1300
 
1301
+ // src/plugins/pluginBuilder.ts
1302
+ var buildPluginTool = {
1303
+ name: "build_plugin",
1304
+ description: `Build a Hustle plugin definition. Use this tool to construct a plugin based on user requirements.
1305
+
1306
+ ## Plugin Structure
1307
+
1308
+ A plugin consists of:
1309
+ - **name**: Unique identifier (lowercase, no spaces, e.g., "my-plugin")
1310
+ - **version**: Semantic version (e.g., "1.0.0")
1311
+ - **description**: What the plugin does
1312
+ - **tools**: Array of tool definitions the AI can call
1313
+ - **executorCode**: Object mapping tool names to JavaScript function code strings
1314
+
1315
+ ## Tool Definition Format
1316
+
1317
+ Each tool needs:
1318
+ - **name**: Unique tool name (alphanumeric + underscore, e.g., "get_weather")
1319
+ - **description**: Clear description for the AI to understand when to use it
1320
+ - **parameters**: JSON Schema object defining the arguments
1321
+
1322
+ Example tool:
1323
+ {
1324
+ "name": "get_weather",
1325
+ "description": "Get current weather for a city",
1326
+ "parameters": {
1327
+ "type": "object",
1328
+ "properties": {
1329
+ "city": { "type": "string", "description": "City name" },
1330
+ "units": { "type": "string", "enum": ["celsius", "fahrenheit"], "description": "Temperature units" }
1331
+ },
1332
+ "required": ["city"]
1333
+ }
1334
+ }
1335
+
1336
+ ## Executor Code Format
1337
+
1338
+ Executors are async JavaScript functions that receive args and return a result.
1339
+ Write them as arrow function strings that will be eval'd:
1340
+
1341
+ "async (args) => { const { city } = args; return { weather: 'sunny', city }; }"
1342
+
1343
+ The function receives args as Record<string, unknown> and should return the result.
1344
+
1345
+ ## Available in Executor Scope
1346
+
1347
+ Executors run in the browser context with full access to:
1348
+
1349
+ ### Browser APIs
1350
+ - **fetch(url, options)** - HTTP requests (subject to CORS)
1351
+ - **localStorage / sessionStorage** - Persistent storage
1352
+ - **document** - Full DOM access (create elements, modals, forms, etc.)
1353
+ - **window** - Global window object
1354
+ - **console** - Logging (log, warn, error, etc.)
1355
+ - **setTimeout / setInterval / clearTimeout / clearInterval** - Timers
1356
+ - **JSON** - Parse and stringify
1357
+ - **Date** - Date/time operations
1358
+ - **URL / URLSearchParams** - URL manipulation
1359
+ - **FormData / Blob / File / FileReader** - File handling
1360
+ - **crypto** - Cryptographic operations (crypto.randomUUID(), etc.)
1361
+ - **navigator** - Browser info, clipboard, geolocation, etc.
1362
+ - **location** - Current URL info
1363
+ - **history** - Browser history navigation
1364
+ - **WebSocket** - Real-time bidirectional communication
1365
+ - **EventSource** - Server-sent events
1366
+ - **indexedDB** - Client-side database for large data
1367
+ - **Notification** - Browser notifications (requires permission)
1368
+ - **performance** - Performance timing
1369
+ - **atob / btoa** - Base64 encoding/decoding
1370
+ - **TextEncoder / TextDecoder** - Text encoding
1371
+ - **AbortController** - Cancel fetch requests
1372
+ - **IntersectionObserver / MutationObserver / ResizeObserver** - DOM observers
1373
+ - **requestAnimationFrame** - Animation timing
1374
+ - **speechSynthesis** - Text-to-speech
1375
+ - **Audio / Image / Canvas** - Media APIs
1376
+
1377
+ ### Hustle Plugin System Globals
1378
+ - **window.__hustleInstanceId** - Current Hustle instance ID
1379
+ - **window.__hustleRegisterPlugin(plugin, enabled)** - Install another plugin dynamically
1380
+ - **window.__hustleUnregisterPlugin(name)** - Uninstall a plugin by name
1381
+ - **window.__hustleUploadFile(file, fileName?)** - Upload a File/Blob to the server, returns { url, contentType }
1382
+ - **window.__hustleListPlugins()** - List all installed plugins
1383
+ - **window.__hustleGetPlugin(name)** - Get a specific plugin by name
1384
+
1385
+ ### DOM Manipulation Examples
1386
+ Create a modal: document.createElement('div'), style it, append to document.body
1387
+ Add event listeners: element.addEventListener('click', handler)
1388
+ Query elements: document.querySelector(), document.querySelectorAll()
1389
+
1390
+ ### Storage Patterns
1391
+ Store data: localStorage.setItem('key', JSON.stringify(data))
1392
+ Retrieve data: JSON.parse(localStorage.getItem('key') || '{}')
1393
+ Namespace your keys: Use plugin name prefix like "myplugin-settings"
1394
+
1395
+ ### Async Patterns
1396
+ All executors should be async. Use await for promises:
1397
+ "async (args) => { const res = await fetch(url); return await res.json(); }"
1398
+
1399
+ ## Lifecycle Hooks (Optional)
1400
+
1401
+ Hooks also have full access to the browser scope described above.
1402
+
1403
+ - **onRegisterCode**: Called once when plugin is registered/enabled. Good for initialization.
1404
+ **IMPORTANT: Always log when your plugin registers so users know it's active!**
1405
+ Example: "async () => { console.log('[MyPlugin] v1.0.0 registered'); }"
1406
+
1407
+ - **beforeRequestCode**: Modify messages before sending. Receives request object with { messages, model, ... }. Must return the modified request.
1408
+ Example: "async (req) => { req.messages = req.messages.map(m => ({...m, content: m.content.toUpperCase()})); return req; }"
1409
+
1410
+ - **afterResponseCode**: Process/modify response after receiving. Receives response object with { content, ... }.
1411
+ Example: "async (res) => { console.log('Response received:', res.content.substring(0, 100)); }"
1412
+
1413
+ - **onErrorCode**: Called on errors. Receives (error, context) where context has { phase: 'beforeRequest'|'execute'|'afterResponse' }.
1414
+ Example: "async (error, ctx) => { console.error('[MyPlugin] Error in', ctx.phase, ':', error.message); }"
1415
+
1416
+ ## Best Practices
1417
+
1418
+ 1. **Always add onRegisterCode** that logs the plugin name and version
1419
+ 2. **Namespace console logs** with [PluginName] prefix for easy identification
1420
+ 3. **Handle errors gracefully** in executors - return { error: message } instead of throwing
1421
+
1422
+ ## Security Notes
1423
+ - Code runs in browser sandbox with same-origin policy
1424
+ - fetch() is subject to CORS restrictions
1425
+ - No direct filesystem access (use File API with user interaction)
1426
+ - Be careful with eval() on user input`,
1427
+ parameters: {
1428
+ type: "object",
1429
+ properties: {
1430
+ name: {
1431
+ type: "string",
1432
+ description: "Unique plugin identifier (lowercase, no spaces)"
1433
+ },
1434
+ version: {
1435
+ type: "string",
1436
+ description: 'Semantic version (e.g., "1.0.0")'
1437
+ },
1438
+ description: {
1439
+ type: "string",
1440
+ description: "What the plugin does"
1441
+ },
1442
+ tools: {
1443
+ type: "array",
1444
+ description: "Array of tool definitions",
1445
+ items: {
1446
+ type: "object",
1447
+ properties: {
1448
+ name: { type: "string", description: "Tool name" },
1449
+ description: { type: "string", description: "Tool description for AI" },
1450
+ parameters: { type: "object", description: "JSON Schema for arguments" }
1451
+ },
1452
+ required: ["name", "description", "parameters"]
1453
+ }
1454
+ },
1455
+ executorCode: {
1456
+ type: "object",
1457
+ description: "Object mapping tool names to executor function code strings"
1458
+ },
1459
+ beforeRequestCode: {
1460
+ type: "string",
1461
+ description: "Optional: Code for beforeRequest hook"
1462
+ },
1463
+ afterResponseCode: {
1464
+ type: "string",
1465
+ description: "Optional: Code for afterResponse hook"
1466
+ },
1467
+ onRegisterCode: {
1468
+ type: "string",
1469
+ description: "Optional: Code for onRegister hook"
1470
+ },
1471
+ onErrorCode: {
1472
+ type: "string",
1473
+ description: "Optional: Code for onError hook"
1474
+ }
1475
+ },
1476
+ required: ["name", "version", "description", "tools", "executorCode"]
1477
+ }
1478
+ };
1479
+ var savePluginTool = {
1480
+ name: "save_plugin",
1481
+ description: "Save a built plugin as a JSON file. Opens a download dialog for the user.",
1482
+ parameters: {
1483
+ type: "object",
1484
+ properties: {
1485
+ plugin: {
1486
+ type: "object",
1487
+ description: "The plugin object to save (from build_plugin result)"
1488
+ },
1489
+ filename: {
1490
+ type: "string",
1491
+ description: "Filename without extension (defaults to plugin name)"
1492
+ }
1493
+ },
1494
+ required: ["plugin"]
1495
+ }
1496
+ };
1497
+ var installPluginTool = {
1498
+ name: "install_plugin",
1499
+ description: "Install a built plugin to browser storage so it persists and can be used.",
1500
+ parameters: {
1501
+ type: "object",
1502
+ properties: {
1503
+ plugin: {
1504
+ type: "object",
1505
+ description: "The plugin object to install (from build_plugin result)"
1506
+ },
1507
+ enabled: {
1508
+ type: "boolean",
1509
+ description: "Whether to enable the plugin immediately (default: true)"
1510
+ }
1511
+ },
1512
+ required: ["plugin"]
1513
+ }
1514
+ };
1515
+ var uninstallPluginTool = {
1516
+ name: "uninstall_plugin",
1517
+ description: "Uninstall a plugin by name, removing it from browser storage.",
1518
+ parameters: {
1519
+ type: "object",
1520
+ properties: {
1521
+ name: {
1522
+ type: "string",
1523
+ description: "The name of the plugin to uninstall"
1524
+ }
1525
+ },
1526
+ required: ["name"]
1527
+ }
1528
+ };
1529
+ var listPluginsTool = {
1530
+ name: "list_plugins",
1531
+ description: "List all installed plugins with their enabled/disabled status.",
1532
+ parameters: {
1533
+ type: "object",
1534
+ properties: {},
1535
+ required: []
1536
+ }
1537
+ };
1538
+ var modifyPluginTool = {
1539
+ name: "modify_plugin",
1540
+ description: `Modify an existing installed plugin. Can update version, description, tools, executors, and hooks.
1541
+
1542
+ Use list_plugins first to see installed plugins, then modify by name.
1543
+
1544
+ You can:
1545
+ - Add new tools (provide tools array with new tools to add)
1546
+ - Update existing tools (provide tool with same name)
1547
+ - Remove tools (set removeTool to the tool name)
1548
+ - Update hooks (provide hook code)
1549
+ - Update version/description
1550
+
1551
+ Example: Add a new tool to existing plugin:
1552
+ {
1553
+ "name": "my-plugin",
1554
+ "addTools": [{ "name": "new_tool", "description": "...", "parameters": {...} }],
1555
+ "addExecutorCode": { "new_tool": "async (args) => { ... }" }
1556
+ }`,
1557
+ parameters: {
1558
+ type: "object",
1559
+ properties: {
1560
+ name: {
1561
+ type: "string",
1562
+ description: "Name of the plugin to modify (required)"
1563
+ },
1564
+ version: {
1565
+ type: "string",
1566
+ description: "New version string"
1567
+ },
1568
+ description: {
1569
+ type: "string",
1570
+ description: "New description"
1571
+ },
1572
+ addTools: {
1573
+ type: "array",
1574
+ description: "Tools to add or update",
1575
+ items: {
1576
+ type: "object",
1577
+ properties: {
1578
+ name: { type: "string" },
1579
+ description: { type: "string" },
1580
+ parameters: { type: "object" }
1581
+ }
1582
+ }
1583
+ },
1584
+ addExecutorCode: {
1585
+ type: "object",
1586
+ description: "Executor code to add/update (tool name -> code string)"
1587
+ },
1588
+ removeTools: {
1589
+ type: "array",
1590
+ description: "Names of tools to remove",
1591
+ items: { type: "string" }
1592
+ },
1593
+ onRegisterCode: {
1594
+ type: "string",
1595
+ description: "New onRegister hook code"
1596
+ },
1597
+ beforeRequestCode: {
1598
+ type: "string",
1599
+ description: "New beforeRequest hook code"
1600
+ },
1601
+ afterResponseCode: {
1602
+ type: "string",
1603
+ description: "New afterResponse hook code"
1604
+ },
1605
+ onErrorCode: {
1606
+ type: "string",
1607
+ description: "New onError hook code"
1608
+ }
1609
+ },
1610
+ required: ["name"]
1611
+ }
1612
+ };
1613
+ var buildPluginExecutor = async (args2) => {
1614
+ const {
1615
+ name,
1616
+ version,
1617
+ description,
1618
+ tools,
1619
+ executorCode,
1620
+ beforeRequestCode,
1621
+ afterResponseCode,
1622
+ onRegisterCode,
1623
+ onErrorCode
1624
+ } = args2;
1625
+ if (!/^[a-z][a-z0-9-]*$/.test(name)) {
1626
+ return {
1627
+ success: false,
1628
+ error: "Plugin name must be lowercase, start with a letter, and contain only letters, numbers, and hyphens"
1629
+ };
1630
+ }
1631
+ if (!/^\d+\.\d+\.\d+/.test(version)) {
1632
+ return {
1633
+ success: false,
1634
+ error: 'Version must be in semver format (e.g., "1.0.0")'
1635
+ };
1636
+ }
1637
+ for (const tool of tools) {
1638
+ if (!executorCode[tool.name]) {
1639
+ return {
1640
+ success: false,
1641
+ error: `Missing executor code for tool: ${tool.name}`
1642
+ };
1643
+ }
1644
+ }
1645
+ const storedPlugin = {
1646
+ name,
1647
+ version,
1648
+ description,
1649
+ tools: tools.map((tool) => ({
1650
+ ...tool,
1651
+ executorCode: executorCode[tool.name]
1652
+ })),
1653
+ enabled: true,
1654
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
1655
+ };
1656
+ if (beforeRequestCode || afterResponseCode || onRegisterCode || onErrorCode) {
1657
+ storedPlugin.hooksCode = {};
1658
+ if (beforeRequestCode) storedPlugin.hooksCode.beforeRequestCode = beforeRequestCode;
1659
+ if (afterResponseCode) storedPlugin.hooksCode.afterResponseCode = afterResponseCode;
1660
+ if (onRegisterCode) storedPlugin.hooksCode.onRegisterCode = onRegisterCode;
1661
+ if (onErrorCode) storedPlugin.hooksCode.onErrorCode = onErrorCode;
1662
+ }
1663
+ return {
1664
+ success: true,
1665
+ plugin: storedPlugin,
1666
+ message: `Plugin "${name}" v${version} built successfully with ${tools.length} tool(s). Use save_plugin to download or install_plugin to add to browser storage.`
1667
+ };
1668
+ };
1669
+ function normalizePlugin(input) {
1670
+ const plugin = input;
1671
+ const tools = plugin.tools;
1672
+ const hasEmbeddedExecutors = tools?.[0]?.executorCode !== void 0;
1673
+ if (hasEmbeddedExecutors) {
1674
+ return {
1675
+ ...plugin,
1676
+ enabled: plugin.enabled ?? true,
1677
+ installedAt: plugin.installedAt ?? (/* @__PURE__ */ new Date()).toISOString()
1678
+ };
1679
+ }
1680
+ const executorCode = plugin.executorCode;
1681
+ const rawTools = tools || [];
1682
+ const hookKeys = ["onRegisterCode", "beforeRequestCode", "afterResponseCode", "onErrorCode"];
1683
+ const hooksCode = {};
1684
+ for (const key of hookKeys) {
1685
+ if (executorCode?.[key]) {
1686
+ hooksCode[key] = executorCode[key];
1687
+ }
1688
+ if (plugin[key]) {
1689
+ hooksCode[key] = plugin[key];
1690
+ }
1691
+ }
1692
+ return {
1693
+ name: plugin.name,
1694
+ version: plugin.version,
1695
+ description: plugin.description,
1696
+ tools: rawTools.map((tool) => ({
1697
+ name: tool.name,
1698
+ description: tool.description,
1699
+ parameters: tool.parameters,
1700
+ executorCode: executorCode?.[tool.name]
1701
+ })),
1702
+ hooksCode: Object.keys(hooksCode).length > 0 ? hooksCode : void 0,
1703
+ enabled: true,
1704
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
1705
+ };
1706
+ }
1707
+ var savePluginExecutor = async (args2) => {
1708
+ const { plugin: rawPlugin, filename } = args2;
1709
+ if (typeof window === "undefined") {
1710
+ return { success: false, error: "Cannot save files in server environment" };
1711
+ }
1712
+ try {
1713
+ const plugin = normalizePlugin(rawPlugin);
1714
+ const json = JSON.stringify(plugin, null, 2);
1715
+ const blob = new Blob([json], { type: "application/json" });
1716
+ const url = URL.createObjectURL(blob);
1717
+ const a = document.createElement("a");
1718
+ a.href = url;
1719
+ a.download = `${filename || plugin.name}.json`;
1720
+ document.body.appendChild(a);
1721
+ a.click();
1722
+ document.body.removeChild(a);
1723
+ URL.revokeObjectURL(url);
1724
+ return {
1725
+ success: true,
1726
+ message: `Plugin saved as ${filename || plugin.name}.json`
1727
+ };
1728
+ } catch (error2) {
1729
+ return {
1730
+ success: false,
1731
+ error: `Failed to save: ${error2 instanceof Error ? error2.message : "Unknown error"}`
1732
+ };
1733
+ }
1734
+ };
1735
+ var installPluginExecutor = async (args2) => {
1736
+ const { plugin: rawPlugin, enabled = true } = args2;
1737
+ if (typeof window === "undefined") {
1738
+ return { success: false, error: "Cannot install plugins in server environment" };
1739
+ }
1740
+ try {
1741
+ const plugin = normalizePlugin(rawPlugin);
1742
+ const win = window;
1743
+ if (!win.__hustleRegisterPlugin) {
1744
+ return {
1745
+ success: false,
1746
+ error: "Plugin registration not available. Make sure HustleProvider is mounted."
1747
+ };
1748
+ }
1749
+ await win.__hustleRegisterPlugin(plugin, enabled);
1750
+ return {
1751
+ success: true,
1752
+ message: `Plugin "${plugin.name}" installed${enabled ? " and enabled" : ""}.`
1753
+ };
1754
+ } catch (error2) {
1755
+ return {
1756
+ success: false,
1757
+ error: `Failed to install: ${error2 instanceof Error ? error2.message : "Unknown error"}`
1758
+ };
1759
+ }
1760
+ };
1761
+ var uninstallPluginExecutor = async (args2) => {
1762
+ const { name } = args2;
1763
+ if (typeof window === "undefined") {
1764
+ return { success: false, error: "Cannot uninstall plugins in server environment" };
1765
+ }
1766
+ try {
1767
+ const win = window;
1768
+ if (!win.__hustleUnregisterPlugin) {
1769
+ return {
1770
+ success: false,
1771
+ error: "Plugin unregistration not available. Make sure HustleProvider is mounted."
1772
+ };
1773
+ }
1774
+ await win.__hustleUnregisterPlugin(name);
1775
+ return {
1776
+ success: true,
1777
+ message: `Plugin "${name}" has been uninstalled.`
1778
+ };
1779
+ } catch (error2) {
1780
+ return {
1781
+ success: false,
1782
+ error: `Failed to uninstall: ${error2 instanceof Error ? error2.message : "Unknown error"}`
1783
+ };
1784
+ }
1785
+ };
1786
+ var listPluginsExecutor = async () => {
1787
+ if (typeof window === "undefined") {
1788
+ return { success: false, error: "Cannot list plugins in server environment" };
1789
+ }
1790
+ try {
1791
+ const win = window;
1792
+ if (!win.__hustleListPlugins) {
1793
+ return {
1794
+ success: false,
1795
+ error: "Plugin listing not available. Make sure HustleProvider is mounted."
1796
+ };
1797
+ }
1798
+ const plugins = win.__hustleListPlugins();
1799
+ return {
1800
+ success: true,
1801
+ plugins,
1802
+ summary: plugins.length === 0 ? "No plugins installed." : `${plugins.length} plugin(s) installed: ${plugins.map((p) => `${p.name} v${p.version} (${p.enabled ? "enabled" : "disabled"})`).join(", ")}`
1803
+ };
1804
+ } catch (error2) {
1805
+ return {
1806
+ success: false,
1807
+ error: `Failed to list plugins: ${error2 instanceof Error ? error2.message : "Unknown error"}`
1808
+ };
1809
+ }
1810
+ };
1811
+ var modifyPluginExecutor = async (args2) => {
1812
+ const {
1813
+ name,
1814
+ version,
1815
+ description,
1816
+ addTools,
1817
+ addExecutorCode,
1818
+ removeTools,
1819
+ onRegisterCode,
1820
+ beforeRequestCode,
1821
+ afterResponseCode,
1822
+ onErrorCode
1823
+ } = args2;
1824
+ if (typeof window === "undefined") {
1825
+ return { success: false, error: "Cannot modify plugins in server environment" };
1826
+ }
1827
+ try {
1828
+ const win = window;
1829
+ if (!win.__hustleGetPlugin || !win.__hustleRegisterPlugin) {
1830
+ return {
1831
+ success: false,
1832
+ error: "Plugin modification not available. Make sure HustleProvider is mounted."
1833
+ };
1834
+ }
1835
+ const existing = win.__hustleGetPlugin(name);
1836
+ if (!existing) {
1837
+ return {
1838
+ success: false,
1839
+ error: `Plugin "${name}" not found. Use list_plugins to see installed plugins.`
1840
+ };
1841
+ }
1842
+ let tools = existing.tools ? [...existing.tools] : [];
1843
+ if (removeTools && removeTools.length > 0) {
1844
+ tools = tools.filter((t) => !removeTools.includes(t.name));
1845
+ }
1846
+ if (addTools && addTools.length > 0) {
1847
+ for (const newTool of addTools) {
1848
+ const existingIndex = tools.findIndex((t) => t.name === newTool.name);
1849
+ const toolWithExecutor = {
1850
+ ...newTool,
1851
+ executorCode: addExecutorCode?.[newTool.name] || tools.find((t) => t.name === newTool.name)?.executorCode
1852
+ };
1853
+ if (existingIndex >= 0) {
1854
+ tools[existingIndex] = toolWithExecutor;
1855
+ } else {
1856
+ tools.push(toolWithExecutor);
1857
+ }
1858
+ }
1859
+ }
1860
+ if (addExecutorCode) {
1861
+ for (const [toolName, code2] of Object.entries(addExecutorCode)) {
1862
+ const tool = tools.find((t) => t.name === toolName);
1863
+ if (tool) {
1864
+ tool.executorCode = code2;
1865
+ }
1866
+ }
1867
+ }
1868
+ const modified = {
1869
+ ...existing,
1870
+ tools
1871
+ };
1872
+ if (version) modified.version = version;
1873
+ if (description) modified.description = description;
1874
+ if (onRegisterCode || beforeRequestCode || afterResponseCode || onErrorCode) {
1875
+ modified.hooksCode = modified.hooksCode || {};
1876
+ if (onRegisterCode) modified.hooksCode.onRegisterCode = onRegisterCode;
1877
+ if (beforeRequestCode) modified.hooksCode.beforeRequestCode = beforeRequestCode;
1878
+ if (afterResponseCode) modified.hooksCode.afterResponseCode = afterResponseCode;
1879
+ if (onErrorCode) modified.hooksCode.onErrorCode = onErrorCode;
1880
+ }
1881
+ const unregisterFn = window.__hustleUnregisterPlugin;
1882
+ if (unregisterFn) {
1883
+ await unregisterFn(name);
1884
+ }
1885
+ await win.__hustleRegisterPlugin(modified, existing.enabled);
1886
+ const changes = [];
1887
+ if (version) changes.push(`version \u2192 ${version}`);
1888
+ if (description) changes.push("description updated");
1889
+ if (removeTools?.length) changes.push(`removed ${removeTools.length} tool(s)`);
1890
+ if (addTools?.length) changes.push(`added/updated ${addTools.length} tool(s)`);
1891
+ if (onRegisterCode) changes.push("onRegister hook updated");
1892
+ if (beforeRequestCode) changes.push("beforeRequest hook updated");
1893
+ if (afterResponseCode) changes.push("afterResponse hook updated");
1894
+ if (onErrorCode) changes.push("onError hook updated");
1895
+ return {
1896
+ success: true,
1897
+ message: `Plugin "${name}" modified: ${changes.join(", ")}`,
1898
+ plugin: {
1899
+ name: modified.name,
1900
+ version: modified.version,
1901
+ toolCount: tools.length
1902
+ }
1903
+ };
1904
+ } catch (error2) {
1905
+ return {
1906
+ success: false,
1907
+ error: `Failed to modify plugin: ${error2 instanceof Error ? error2.message : "Unknown error"}`
1908
+ };
1909
+ }
1910
+ };
1911
+ var pluginBuilderPlugin = {
1912
+ name: "plugin-builder",
1913
+ version: "1.2.0",
1914
+ description: "Build custom plugins through conversation",
1915
+ tools: [buildPluginTool, savePluginTool, installPluginTool, uninstallPluginTool, listPluginsTool, modifyPluginTool],
1916
+ executors: {
1917
+ build_plugin: buildPluginExecutor,
1918
+ save_plugin: savePluginExecutor,
1919
+ install_plugin: installPluginExecutor,
1920
+ uninstall_plugin: uninstallPluginExecutor,
1921
+ list_plugins: listPluginsExecutor,
1922
+ modify_plugin: modifyPluginExecutor
1923
+ },
1924
+ hooks: {
1925
+ onRegister: () => {
1926
+ console.log("[Plugin Builder] Ready to help build custom plugins");
1927
+ }
1928
+ }
1929
+ };
1930
+
1190
1931
  // src/plugins/index.ts
1191
1932
  var availablePlugins = [
1192
1933
  {
@@ -1216,6 +1957,10 @@ var availablePlugins = [
1216
1957
  {
1217
1958
  ...screenshotPlugin,
1218
1959
  description: "Take screenshots of the current page"
1960
+ },
1961
+ {
1962
+ ...pluginBuilderPlugin,
1963
+ description: "Build custom plugins through conversation with AI"
1219
1964
  }
1220
1965
  ];
1221
1966
  function getAvailablePlugin(name) {
@@ -1228,6 +1973,7 @@ exports.getAvailablePlugin = getAvailablePlugin;
1228
1973
  exports.jsExecutorPlugin = jsExecutorPlugin;
1229
1974
  exports.migrateFunPlugin = migrateFunPlugin;
1230
1975
  exports.piiProtectionPlugin = piiProtectionPlugin;
1976
+ exports.pluginBuilderPlugin = pluginBuilderPlugin;
1231
1977
  exports.predictionMarketPlugin = predictionMarketPlugin;
1232
1978
  exports.screenshotPlugin = screenshotPlugin;
1233
1979
  exports.userQuestionPlugin = userQuestionPlugin;