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