@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.
@@ -1820,35 +1820,52 @@ var pluginRegistry = new PluginRegistry();
1820
1820
  function getStorageKey(instanceId) {
1821
1821
  return `hustle-plugins-${instanceId}`;
1822
1822
  }
1823
- function usePlugins(instanceId = "default") {
1823
+ function getInstanceId(providedId) {
1824
+ if (providedId) return providedId;
1825
+ if (typeof window !== "undefined") {
1826
+ const globalId = window.__hustleInstanceId;
1827
+ if (globalId) return globalId;
1828
+ }
1829
+ return "default";
1830
+ }
1831
+ function usePlugins(instanceId) {
1832
+ const [resolvedInstanceId] = useState(() => getInstanceId(instanceId));
1824
1833
  const [plugins, setPlugins] = useState([]);
1825
1834
  useEffect(() => {
1826
- setPlugins(pluginRegistry.loadFromStorage(instanceId));
1827
- const unsubscribe = pluginRegistry.onChange(setPlugins, instanceId);
1828
- const storageKey = getStorageKey(instanceId);
1835
+ setPlugins(pluginRegistry.loadFromStorage(resolvedInstanceId));
1836
+ const unsubscribe = pluginRegistry.onChange(setPlugins, resolvedInstanceId);
1837
+ const storageKey = getStorageKey(resolvedInstanceId);
1829
1838
  const handleStorage = (e) => {
1830
1839
  if (e.key === storageKey) {
1831
- setPlugins(pluginRegistry.loadFromStorage(instanceId));
1840
+ setPlugins(pluginRegistry.loadFromStorage(resolvedInstanceId));
1832
1841
  }
1833
1842
  };
1834
1843
  window.addEventListener("storage", handleStorage);
1844
+ const handlePluginInstalled = (e) => {
1845
+ const customEvent = e;
1846
+ if (customEvent.detail.instanceId === resolvedInstanceId) {
1847
+ setPlugins(pluginRegistry.loadFromStorage(resolvedInstanceId));
1848
+ }
1849
+ };
1850
+ window.addEventListener("hustle-plugin-installed", handlePluginInstalled);
1835
1851
  return () => {
1836
1852
  unsubscribe();
1837
1853
  window.removeEventListener("storage", handleStorage);
1854
+ window.removeEventListener("hustle-plugin-installed", handlePluginInstalled);
1838
1855
  };
1839
- }, [instanceId]);
1856
+ }, [resolvedInstanceId]);
1840
1857
  const registerPlugin = useCallback((plugin) => {
1841
- pluginRegistry.register(plugin, true, instanceId);
1842
- }, [instanceId]);
1858
+ pluginRegistry.register(plugin, true, resolvedInstanceId);
1859
+ }, [resolvedInstanceId]);
1843
1860
  const unregisterPlugin = useCallback((name) => {
1844
- pluginRegistry.unregister(name, instanceId);
1845
- }, [instanceId]);
1861
+ pluginRegistry.unregister(name, resolvedInstanceId);
1862
+ }, [resolvedInstanceId]);
1846
1863
  const enablePlugin = useCallback((name) => {
1847
- pluginRegistry.setEnabled(name, true, instanceId);
1848
- }, [instanceId]);
1864
+ pluginRegistry.setEnabled(name, true, resolvedInstanceId);
1865
+ }, [resolvedInstanceId]);
1849
1866
  const disablePlugin = useCallback((name) => {
1850
- pluginRegistry.setEnabled(name, false, instanceId);
1851
- }, [instanceId]);
1867
+ pluginRegistry.setEnabled(name, false, resolvedInstanceId);
1868
+ }, [resolvedInstanceId]);
1852
1869
  const isRegistered = useCallback(
1853
1870
  (name) => plugins.some((p) => p.name === name),
1854
1871
  [plugins]
@@ -1903,6 +1920,42 @@ function HustleProvider({
1903
1920
  }
1904
1921
  };
1905
1922
  }, [isAutoInstance, resolvedInstanceId]);
1923
+ useEffect(() => {
1924
+ if (typeof window !== "undefined") {
1925
+ const win = window;
1926
+ win.__hustleInstanceId = resolvedInstanceId;
1927
+ win.__hustleRegisterPlugin = async (plugin, enabled = true) => {
1928
+ const hydrated = hydratePlugin(plugin);
1929
+ pluginRegistry.register(hydrated, enabled, resolvedInstanceId);
1930
+ };
1931
+ win.__hustleUnregisterPlugin = async (name) => {
1932
+ pluginRegistry.unregister(name, resolvedInstanceId);
1933
+ };
1934
+ win.__hustleListPlugins = () => {
1935
+ const plugins = pluginRegistry.loadFromStorage(resolvedInstanceId);
1936
+ return plugins.map((p) => ({
1937
+ name: p.name,
1938
+ version: p.version,
1939
+ description: p.description || "",
1940
+ enabled: p.enabled
1941
+ }));
1942
+ };
1943
+ win.__hustleGetPlugin = (name) => {
1944
+ const plugins = pluginRegistry.loadFromStorage(resolvedInstanceId);
1945
+ return plugins.find((p) => p.name === name) || null;
1946
+ };
1947
+ }
1948
+ return () => {
1949
+ if (typeof window !== "undefined") {
1950
+ const win = window;
1951
+ delete win.__hustleInstanceId;
1952
+ delete win.__hustleRegisterPlugin;
1953
+ delete win.__hustleUnregisterPlugin;
1954
+ delete win.__hustleListPlugins;
1955
+ delete win.__hustleGetPlugin;
1956
+ }
1957
+ };
1958
+ }, [resolvedInstanceId]);
1906
1959
  const isApiKeyMode = Boolean(apiKey && vaultId);
1907
1960
  const authContext = useEmblemAuthOptional();
1908
1961
  const authSDK = isApiKeyMode ? null : authContext?.authSDK ?? null;
@@ -2050,6 +2103,19 @@ function HustleProvider({
2050
2103
  };
2051
2104
  registerPlugins();
2052
2105
  }, [client, enabledPlugins, log]);
2106
+ useEffect(() => {
2107
+ if (typeof window !== "undefined" && client) {
2108
+ const win = window;
2109
+ win.__hustleUploadFile = async (file, fileName) => {
2110
+ return await client.uploadFile(file, fileName);
2111
+ };
2112
+ }
2113
+ return () => {
2114
+ if (typeof window !== "undefined") {
2115
+ delete window.__hustleUploadFile;
2116
+ }
2117
+ };
2118
+ }, [client]);
2053
2119
  const loadModels = useCallback(async () => {
2054
2120
  if (!client) {
2055
2121
  log("Cannot load models - client not ready");
@@ -2930,12 +2996,16 @@ function ensureModalStyles() {
2930
2996
  left: 0;
2931
2997
  right: 0;
2932
2998
  bottom: 0;
2933
- background: rgba(0, 0, 0, 0.6);
2999
+ background: transparent;
2934
3000
  display: flex;
2935
3001
  align-items: center;
2936
3002
  justify-content: center;
2937
3003
  z-index: 10000;
2938
- animation: uqFadeIn 0.2s ease-out;
3004
+ pointer-events: none;
3005
+ }
3006
+
3007
+ .user-question-modal {
3008
+ pointer-events: auto;
2939
3009
  }
2940
3010
 
2941
3011
  @keyframes uqFadeIn {
@@ -3048,6 +3118,27 @@ function ensureModalStyles() {
3048
3118
  color: #666;
3049
3119
  cursor: not-allowed;
3050
3120
  }
3121
+
3122
+ .user-question-custom-input {
3123
+ width: 100%;
3124
+ padding: 8px 12px;
3125
+ margin-top: 8px;
3126
+ background: #1a1a2e;
3127
+ border: 1px solid #444;
3128
+ border-radius: 6px;
3129
+ color: #e0e0e0;
3130
+ font-size: 14px;
3131
+ box-sizing: border-box;
3132
+ }
3133
+
3134
+ .user-question-custom-input:focus {
3135
+ outline: none;
3136
+ border-color: #4a7aff;
3137
+ }
3138
+
3139
+ .user-question-custom-input::placeholder {
3140
+ color: #666;
3141
+ }
3051
3142
  `;
3052
3143
  document.head.appendChild(styles2);
3053
3144
  }
@@ -3069,13 +3160,17 @@ var askUserTool = {
3069
3160
  allowMultiple: {
3070
3161
  type: "boolean",
3071
3162
  description: "If true, user can select multiple choices. Default: false"
3163
+ },
3164
+ allowCustom: {
3165
+ type: "boolean",
3166
+ description: 'If true, adds an "Other" option where user can type a custom response. Default: false'
3072
3167
  }
3073
3168
  },
3074
3169
  required: ["question", "choices"]
3075
3170
  }
3076
3171
  };
3077
3172
  var askUserExecutor = async (args2) => {
3078
- const { question, choices, allowMultiple = false } = args2;
3173
+ const { question, choices, allowMultiple = false, allowCustom = false } = args2;
3079
3174
  if (!question || !choices || !Array.isArray(choices) || choices.length === 0) {
3080
3175
  return {
3081
3176
  question: question || "",
@@ -3107,6 +3202,17 @@ var askUserExecutor = async (args2) => {
3107
3202
  choicesDiv.className = "user-question-choices";
3108
3203
  const inputType = allowMultiple ? "checkbox" : "radio";
3109
3204
  const inputName = `uq-${Date.now()}`;
3205
+ let customInput = null;
3206
+ let isCustomSelected = false;
3207
+ const submitBtn = document.createElement("button");
3208
+ submitBtn.className = "user-question-btn user-question-btn-submit";
3209
+ submitBtn.textContent = "Submit";
3210
+ submitBtn.disabled = true;
3211
+ const updateSubmitButton = () => {
3212
+ const hasSelection = selected.size > 0;
3213
+ const hasCustomValue = isCustomSelected && customInput && customInput.value.trim().length > 0;
3214
+ submitBtn.disabled = !hasSelection && !hasCustomValue;
3215
+ };
3110
3216
  choices.forEach((choice, index) => {
3111
3217
  const choiceDiv = document.createElement("div");
3112
3218
  choiceDiv.className = "user-question-choice";
@@ -3131,6 +3237,8 @@ var askUserExecutor = async (args2) => {
3131
3237
  }
3132
3238
  } else {
3133
3239
  selected.clear();
3240
+ isCustomSelected = false;
3241
+ if (customInput) customInput.value = "";
3134
3242
  selected.add(choice);
3135
3243
  choicesDiv.querySelectorAll(".user-question-choice").forEach((c) => c.classList.remove("selected"));
3136
3244
  choiceDiv.classList.add("selected");
@@ -3146,9 +3254,62 @@ var askUserExecutor = async (args2) => {
3146
3254
  });
3147
3255
  choicesDiv.appendChild(choiceDiv);
3148
3256
  });
3257
+ if (allowCustom) {
3258
+ const customChoiceDiv = document.createElement("div");
3259
+ customChoiceDiv.className = "user-question-choice";
3260
+ const customRadio = document.createElement("input");
3261
+ customRadio.type = inputType;
3262
+ customRadio.name = inputName;
3263
+ customRadio.id = `${inputName}-custom`;
3264
+ customRadio.value = "__custom__";
3265
+ const customLabel = document.createElement("label");
3266
+ customLabel.htmlFor = customRadio.id;
3267
+ customLabel.textContent = "Other:";
3268
+ customLabel.style.flexShrink = "0";
3269
+ customInput = document.createElement("input");
3270
+ customInput.type = "text";
3271
+ customInput.className = "user-question-custom-input";
3272
+ customInput.placeholder = "Type your answer...";
3273
+ customInput.style.marginTop = "0";
3274
+ customInput.style.marginLeft = "8px";
3275
+ customInput.style.flex = "1";
3276
+ customChoiceDiv.appendChild(customRadio);
3277
+ customChoiceDiv.appendChild(customLabel);
3278
+ customChoiceDiv.appendChild(customInput);
3279
+ const handleCustomSelect = () => {
3280
+ if (!allowMultiple) {
3281
+ selected.clear();
3282
+ choicesDiv.querySelectorAll(".user-question-choice").forEach((c) => c.classList.remove("selected"));
3283
+ }
3284
+ isCustomSelected = true;
3285
+ customChoiceDiv.classList.add("selected");
3286
+ customInput?.focus();
3287
+ updateSubmitButton();
3288
+ };
3289
+ customRadio.addEventListener("change", handleCustomSelect);
3290
+ customChoiceDiv.addEventListener("click", (e) => {
3291
+ if (e.target !== customRadio && e.target !== customInput) {
3292
+ customRadio.checked = true;
3293
+ handleCustomSelect();
3294
+ }
3295
+ });
3296
+ customInput.addEventListener("focus", () => {
3297
+ if (!customRadio.checked) {
3298
+ customRadio.checked = true;
3299
+ handleCustomSelect();
3300
+ }
3301
+ });
3302
+ customInput.addEventListener("input", updateSubmitButton);
3303
+ choicesDiv.appendChild(customChoiceDiv);
3304
+ }
3149
3305
  modal.appendChild(choicesDiv);
3150
3306
  const actions = document.createElement("div");
3151
3307
  actions.className = "user-question-actions";
3308
+ overlay.appendChild(modal);
3309
+ document.body.appendChild(overlay);
3310
+ const cleanup = () => {
3311
+ overlay.remove();
3312
+ };
3152
3313
  const cancelBtn = document.createElement("button");
3153
3314
  cancelBtn.className = "user-question-btn user-question-btn-cancel";
3154
3315
  cancelBtn.textContent = "Skip";
@@ -3160,29 +3321,21 @@ var askUserExecutor = async (args2) => {
3160
3321
  answered: false
3161
3322
  });
3162
3323
  };
3163
- const submitBtn = document.createElement("button");
3164
- submitBtn.className = "user-question-btn user-question-btn-submit";
3165
- submitBtn.textContent = "Submit";
3166
- submitBtn.disabled = true;
3167
3324
  submitBtn.onclick = () => {
3168
3325
  cleanup();
3326
+ const results = Array.from(selected);
3327
+ if (isCustomSelected && customInput && customInput.value.trim()) {
3328
+ results.push(customInput.value.trim());
3329
+ }
3169
3330
  resolve({
3170
3331
  question,
3171
- selectedChoices: Array.from(selected),
3332
+ selectedChoices: results,
3172
3333
  answered: true
3173
3334
  });
3174
3335
  };
3175
- const updateSubmitButton = () => {
3176
- submitBtn.disabled = selected.size === 0;
3177
- };
3178
3336
  actions.appendChild(cancelBtn);
3179
3337
  actions.appendChild(submitBtn);
3180
3338
  modal.appendChild(actions);
3181
- overlay.appendChild(modal);
3182
- document.body.appendChild(overlay);
3183
- const cleanup = () => {
3184
- overlay.remove();
3185
- };
3186
3339
  const handleEscape = (e) => {
3187
3340
  if (e.key === "Escape") {
3188
3341
  document.removeEventListener("keydown", handleEscape);
@@ -3351,6 +3504,11 @@ var screenshotTool = {
3351
3504
 
3352
3505
  The screenshot captures the visible viewport of the page. The image is uploaded to the server and a permanent URL is returned.
3353
3506
 
3507
+ IMPORTANT: Before taking a screenshot, use the ask_user tool (if available) to ask which size they prefer:
3508
+ - "full" (100%) - highest quality, larger file
3509
+ - "half" (50%) - good balance of quality and size
3510
+ - "quarter" (25%) - smallest file, faster upload
3511
+
3354
3512
  Use this when:
3355
3513
  - User asks to see what's on their screen
3356
3514
  - You need to analyze the current page visually
@@ -3361,12 +3519,24 @@ Use this when:
3361
3519
  selector: {
3362
3520
  type: "string",
3363
3521
  description: "Optional CSS selector to capture a specific element instead of the full page. Leave empty for full page screenshot."
3522
+ },
3523
+ size: {
3524
+ type: "string",
3525
+ enum: ["full", "half", "quarter"],
3526
+ description: 'Image size: "full" (100%), "half" (50%), or "quarter" (25%). Ask the user which size they prefer before capturing.'
3364
3527
  }
3365
3528
  }
3366
3529
  }
3367
3530
  };
3368
3531
  var screenshotExecutor = async (args2) => {
3369
3532
  const selector = args2.selector;
3533
+ const size = args2.size || "full";
3534
+ const scaleMap = {
3535
+ full: 1,
3536
+ half: 0.5,
3537
+ quarter: 0.25
3538
+ };
3539
+ const scale = scaleMap[size] || 1;
3370
3540
  if (typeof window === "undefined" || typeof document === "undefined") {
3371
3541
  return {
3372
3542
  success: false,
@@ -3387,34 +3557,41 @@ var screenshotExecutor = async (args2) => {
3387
3557
  if (!target) {
3388
3558
  return { success: false, error: "Element not found: " + selector };
3389
3559
  }
3390
- const canvas = await window.html2canvas(target, {
3560
+ const fullCanvas = await window.html2canvas(target, {
3391
3561
  useCORS: true,
3392
3562
  allowTaint: true,
3393
3563
  backgroundColor: "#000000",
3394
3564
  scale: 1
3395
3565
  });
3566
+ let finalCanvas = fullCanvas;
3567
+ if (scale < 1) {
3568
+ const resizedCanvas = document.createElement("canvas");
3569
+ resizedCanvas.width = Math.round(fullCanvas.width * scale);
3570
+ resizedCanvas.height = Math.round(fullCanvas.height * scale);
3571
+ const ctx = resizedCanvas.getContext("2d");
3572
+ if (ctx) {
3573
+ ctx.drawImage(fullCanvas, 0, 0, resizedCanvas.width, resizedCanvas.height);
3574
+ finalCanvas = resizedCanvas;
3575
+ }
3576
+ }
3396
3577
  const blob = await new Promise((resolve) => {
3397
- canvas.toBlob(resolve, "image/png", 0.9);
3578
+ finalCanvas.toBlob(resolve, "image/png", 0.9);
3398
3579
  });
3399
3580
  if (!blob) {
3400
3581
  return { success: false, error: "Failed to create image blob" };
3401
3582
  }
3402
- const formData = new FormData();
3403
- formData.append("file", blob, "screenshot-" + Date.now() + ".png");
3404
- const response = await fetch("/api/files/upload", {
3405
- method: "POST",
3406
- body: formData
3407
- });
3408
- if (!response.ok) {
3409
- const errorData = await response.json().catch(() => ({}));
3410
- return { success: false, error: errorData.error || "Upload failed" };
3583
+ const uploadFn = window.__hustleUploadFile;
3584
+ if (!uploadFn) {
3585
+ return { success: false, error: "Upload not available. Make sure HustleProvider is mounted and client is ready." };
3411
3586
  }
3412
- const data = await response.json();
3587
+ const fileName = "screenshot-" + Date.now() + ".png";
3588
+ const attachment = await uploadFn(blob, fileName);
3589
+ const sizeLabel = size === "full" ? "100%" : size === "half" ? "50%" : "25%";
3413
3590
  return {
3414
3591
  success: true,
3415
- url: data.url,
3416
- contentType: data.contentType,
3417
- message: "Screenshot captured and uploaded successfully"
3592
+ url: attachment.url,
3593
+ contentType: attachment.contentType || "image/png",
3594
+ message: `Screenshot captured at ${sizeLabel} size (${finalCanvas.width}x${finalCanvas.height}) and uploaded successfully`
3418
3595
  };
3419
3596
  } catch (e) {
3420
3597
  const err = e;
@@ -3439,6 +3616,636 @@ var screenshotPlugin = {
3439
3616
  }
3440
3617
  };
3441
3618
 
3619
+ // src/plugins/pluginBuilder.ts
3620
+ var buildPluginTool = {
3621
+ name: "build_plugin",
3622
+ description: `Build a Hustle plugin definition. Use this tool to construct a plugin based on user requirements.
3623
+
3624
+ ## Plugin Structure
3625
+
3626
+ A plugin consists of:
3627
+ - **name**: Unique identifier (lowercase, no spaces, e.g., "my-plugin")
3628
+ - **version**: Semantic version (e.g., "1.0.0")
3629
+ - **description**: What the plugin does
3630
+ - **tools**: Array of tool definitions the AI can call
3631
+ - **executorCode**: Object mapping tool names to JavaScript function code strings
3632
+
3633
+ ## Tool Definition Format
3634
+
3635
+ Each tool needs:
3636
+ - **name**: Unique tool name (alphanumeric + underscore, e.g., "get_weather")
3637
+ - **description**: Clear description for the AI to understand when to use it
3638
+ - **parameters**: JSON Schema object defining the arguments
3639
+
3640
+ Example tool:
3641
+ {
3642
+ "name": "get_weather",
3643
+ "description": "Get current weather for a city",
3644
+ "parameters": {
3645
+ "type": "object",
3646
+ "properties": {
3647
+ "city": { "type": "string", "description": "City name" },
3648
+ "units": { "type": "string", "enum": ["celsius", "fahrenheit"], "description": "Temperature units" }
3649
+ },
3650
+ "required": ["city"]
3651
+ }
3652
+ }
3653
+
3654
+ ## Executor Code Format
3655
+
3656
+ Executors are async JavaScript functions that receive args and return a result.
3657
+ Write them as arrow function strings that will be eval'd:
3658
+
3659
+ "async (args) => { const { city } = args; return { weather: 'sunny', city }; }"
3660
+
3661
+ The function receives args as Record<string, unknown> and should return the result.
3662
+
3663
+ ## Available in Executor Scope
3664
+
3665
+ Executors run in the browser context with full access to:
3666
+
3667
+ ### Browser APIs
3668
+ - **fetch(url, options)** - HTTP requests (subject to CORS)
3669
+ - **localStorage / sessionStorage** - Persistent storage
3670
+ - **document** - Full DOM access (create elements, modals, forms, etc.)
3671
+ - **window** - Global window object
3672
+ - **console** - Logging (log, warn, error, etc.)
3673
+ - **setTimeout / setInterval / clearTimeout / clearInterval** - Timers
3674
+ - **JSON** - Parse and stringify
3675
+ - **Date** - Date/time operations
3676
+ - **URL / URLSearchParams** - URL manipulation
3677
+ - **FormData / Blob / File / FileReader** - File handling
3678
+ - **crypto** - Cryptographic operations (crypto.randomUUID(), etc.)
3679
+ - **navigator** - Browser info, clipboard, geolocation, etc.
3680
+ - **location** - Current URL info
3681
+ - **history** - Browser history navigation
3682
+ - **WebSocket** - Real-time bidirectional communication
3683
+ - **EventSource** - Server-sent events
3684
+ - **indexedDB** - Client-side database for large data
3685
+ - **Notification** - Browser notifications (requires permission)
3686
+ - **performance** - Performance timing
3687
+ - **atob / btoa** - Base64 encoding/decoding
3688
+ - **TextEncoder / TextDecoder** - Text encoding
3689
+ - **AbortController** - Cancel fetch requests
3690
+ - **IntersectionObserver / MutationObserver / ResizeObserver** - DOM observers
3691
+ - **requestAnimationFrame** - Animation timing
3692
+ - **speechSynthesis** - Text-to-speech
3693
+ - **Audio / Image / Canvas** - Media APIs
3694
+
3695
+ ### Hustle Plugin System Globals
3696
+ - **window.__hustleInstanceId** - Current Hustle instance ID
3697
+ - **window.__hustleRegisterPlugin(plugin, enabled)** - Install another plugin dynamically
3698
+ - **window.__hustleUnregisterPlugin(name)** - Uninstall a plugin by name
3699
+ - **window.__hustleUploadFile(file, fileName?)** - Upload a File/Blob to the server, returns { url, contentType }
3700
+ - **window.__hustleListPlugins()** - List all installed plugins
3701
+ - **window.__hustleGetPlugin(name)** - Get a specific plugin by name
3702
+
3703
+ ### DOM Manipulation Examples
3704
+ Create a modal: document.createElement('div'), style it, append to document.body
3705
+ Add event listeners: element.addEventListener('click', handler)
3706
+ Query elements: document.querySelector(), document.querySelectorAll()
3707
+
3708
+ ### Storage Patterns
3709
+ Store data: localStorage.setItem('key', JSON.stringify(data))
3710
+ Retrieve data: JSON.parse(localStorage.getItem('key') || '{}')
3711
+ Namespace your keys: Use plugin name prefix like "myplugin-settings"
3712
+
3713
+ ### Async Patterns
3714
+ All executors should be async. Use await for promises:
3715
+ "async (args) => { const res = await fetch(url); return await res.json(); }"
3716
+
3717
+ ## Lifecycle Hooks (Optional)
3718
+
3719
+ Hooks also have full access to the browser scope described above.
3720
+
3721
+ - **onRegisterCode**: Called once when plugin is registered/enabled. Good for initialization.
3722
+ **IMPORTANT: Always log when your plugin registers so users know it's active!**
3723
+ Example: "async () => { console.log('[MyPlugin] v1.0.0 registered'); }"
3724
+
3725
+ - **beforeRequestCode**: Modify messages before sending. Receives request object with { messages, model, ... }. Must return the modified request.
3726
+ Example: "async (req) => { req.messages = req.messages.map(m => ({...m, content: m.content.toUpperCase()})); return req; }"
3727
+
3728
+ - **afterResponseCode**: Process/modify response after receiving. Receives response object with { content, ... }.
3729
+ Example: "async (res) => { console.log('Response received:', res.content.substring(0, 100)); }"
3730
+
3731
+ - **onErrorCode**: Called on errors. Receives (error, context) where context has { phase: 'beforeRequest'|'execute'|'afterResponse' }.
3732
+ Example: "async (error, ctx) => { console.error('[MyPlugin] Error in', ctx.phase, ':', error.message); }"
3733
+
3734
+ ## Best Practices
3735
+
3736
+ 1. **Always add onRegisterCode** that logs the plugin name and version
3737
+ 2. **Namespace console logs** with [PluginName] prefix for easy identification
3738
+ 3. **Handle errors gracefully** in executors - return { error: message } instead of throwing
3739
+
3740
+ ## Security Notes
3741
+ - Code runs in browser sandbox with same-origin policy
3742
+ - fetch() is subject to CORS restrictions
3743
+ - No direct filesystem access (use File API with user interaction)
3744
+ - Be careful with eval() on user input`,
3745
+ parameters: {
3746
+ type: "object",
3747
+ properties: {
3748
+ name: {
3749
+ type: "string",
3750
+ description: "Unique plugin identifier (lowercase, no spaces)"
3751
+ },
3752
+ version: {
3753
+ type: "string",
3754
+ description: 'Semantic version (e.g., "1.0.0")'
3755
+ },
3756
+ description: {
3757
+ type: "string",
3758
+ description: "What the plugin does"
3759
+ },
3760
+ tools: {
3761
+ type: "array",
3762
+ description: "Array of tool definitions",
3763
+ items: {
3764
+ type: "object",
3765
+ properties: {
3766
+ name: { type: "string", description: "Tool name" },
3767
+ description: { type: "string", description: "Tool description for AI" },
3768
+ parameters: { type: "object", description: "JSON Schema for arguments" }
3769
+ },
3770
+ required: ["name", "description", "parameters"]
3771
+ }
3772
+ },
3773
+ executorCode: {
3774
+ type: "object",
3775
+ description: "Object mapping tool names to executor function code strings"
3776
+ },
3777
+ beforeRequestCode: {
3778
+ type: "string",
3779
+ description: "Optional: Code for beforeRequest hook"
3780
+ },
3781
+ afterResponseCode: {
3782
+ type: "string",
3783
+ description: "Optional: Code for afterResponse hook"
3784
+ },
3785
+ onRegisterCode: {
3786
+ type: "string",
3787
+ description: "Optional: Code for onRegister hook"
3788
+ },
3789
+ onErrorCode: {
3790
+ type: "string",
3791
+ description: "Optional: Code for onError hook"
3792
+ }
3793
+ },
3794
+ required: ["name", "version", "description", "tools", "executorCode"]
3795
+ }
3796
+ };
3797
+ var savePluginTool = {
3798
+ name: "save_plugin",
3799
+ description: "Save a built plugin as a JSON file. Opens a download dialog for the user.",
3800
+ parameters: {
3801
+ type: "object",
3802
+ properties: {
3803
+ plugin: {
3804
+ type: "object",
3805
+ description: "The plugin object to save (from build_plugin result)"
3806
+ },
3807
+ filename: {
3808
+ type: "string",
3809
+ description: "Filename without extension (defaults to plugin name)"
3810
+ }
3811
+ },
3812
+ required: ["plugin"]
3813
+ }
3814
+ };
3815
+ var installPluginTool = {
3816
+ name: "install_plugin",
3817
+ description: "Install a built plugin to browser storage so it persists and can be used.",
3818
+ parameters: {
3819
+ type: "object",
3820
+ properties: {
3821
+ plugin: {
3822
+ type: "object",
3823
+ description: "The plugin object to install (from build_plugin result)"
3824
+ },
3825
+ enabled: {
3826
+ type: "boolean",
3827
+ description: "Whether to enable the plugin immediately (default: true)"
3828
+ }
3829
+ },
3830
+ required: ["plugin"]
3831
+ }
3832
+ };
3833
+ var uninstallPluginTool = {
3834
+ name: "uninstall_plugin",
3835
+ description: "Uninstall a plugin by name, removing it from browser storage.",
3836
+ parameters: {
3837
+ type: "object",
3838
+ properties: {
3839
+ name: {
3840
+ type: "string",
3841
+ description: "The name of the plugin to uninstall"
3842
+ }
3843
+ },
3844
+ required: ["name"]
3845
+ }
3846
+ };
3847
+ var listPluginsTool = {
3848
+ name: "list_plugins",
3849
+ description: "List all installed plugins with their enabled/disabled status.",
3850
+ parameters: {
3851
+ type: "object",
3852
+ properties: {},
3853
+ required: []
3854
+ }
3855
+ };
3856
+ var modifyPluginTool = {
3857
+ name: "modify_plugin",
3858
+ description: `Modify an existing installed plugin. Can update version, description, tools, executors, and hooks.
3859
+
3860
+ Use list_plugins first to see installed plugins, then modify by name.
3861
+
3862
+ You can:
3863
+ - Add new tools (provide tools array with new tools to add)
3864
+ - Update existing tools (provide tool with same name)
3865
+ - Remove tools (set removeTool to the tool name)
3866
+ - Update hooks (provide hook code)
3867
+ - Update version/description
3868
+
3869
+ Example: Add a new tool to existing plugin:
3870
+ {
3871
+ "name": "my-plugin",
3872
+ "addTools": [{ "name": "new_tool", "description": "...", "parameters": {...} }],
3873
+ "addExecutorCode": { "new_tool": "async (args) => { ... }" }
3874
+ }`,
3875
+ parameters: {
3876
+ type: "object",
3877
+ properties: {
3878
+ name: {
3879
+ type: "string",
3880
+ description: "Name of the plugin to modify (required)"
3881
+ },
3882
+ version: {
3883
+ type: "string",
3884
+ description: "New version string"
3885
+ },
3886
+ description: {
3887
+ type: "string",
3888
+ description: "New description"
3889
+ },
3890
+ addTools: {
3891
+ type: "array",
3892
+ description: "Tools to add or update",
3893
+ items: {
3894
+ type: "object",
3895
+ properties: {
3896
+ name: { type: "string" },
3897
+ description: { type: "string" },
3898
+ parameters: { type: "object" }
3899
+ }
3900
+ }
3901
+ },
3902
+ addExecutorCode: {
3903
+ type: "object",
3904
+ description: "Executor code to add/update (tool name -> code string)"
3905
+ },
3906
+ removeTools: {
3907
+ type: "array",
3908
+ description: "Names of tools to remove",
3909
+ items: { type: "string" }
3910
+ },
3911
+ onRegisterCode: {
3912
+ type: "string",
3913
+ description: "New onRegister hook code"
3914
+ },
3915
+ beforeRequestCode: {
3916
+ type: "string",
3917
+ description: "New beforeRequest hook code"
3918
+ },
3919
+ afterResponseCode: {
3920
+ type: "string",
3921
+ description: "New afterResponse hook code"
3922
+ },
3923
+ onErrorCode: {
3924
+ type: "string",
3925
+ description: "New onError hook code"
3926
+ }
3927
+ },
3928
+ required: ["name"]
3929
+ }
3930
+ };
3931
+ var buildPluginExecutor = async (args2) => {
3932
+ const {
3933
+ name,
3934
+ version,
3935
+ description,
3936
+ tools,
3937
+ executorCode,
3938
+ beforeRequestCode,
3939
+ afterResponseCode,
3940
+ onRegisterCode,
3941
+ onErrorCode
3942
+ } = args2;
3943
+ if (!/^[a-z][a-z0-9-]*$/.test(name)) {
3944
+ return {
3945
+ success: false,
3946
+ error: "Plugin name must be lowercase, start with a letter, and contain only letters, numbers, and hyphens"
3947
+ };
3948
+ }
3949
+ if (!/^\d+\.\d+\.\d+/.test(version)) {
3950
+ return {
3951
+ success: false,
3952
+ error: 'Version must be in semver format (e.g., "1.0.0")'
3953
+ };
3954
+ }
3955
+ for (const tool of tools) {
3956
+ if (!executorCode[tool.name]) {
3957
+ return {
3958
+ success: false,
3959
+ error: `Missing executor code for tool: ${tool.name}`
3960
+ };
3961
+ }
3962
+ }
3963
+ const storedPlugin = {
3964
+ name,
3965
+ version,
3966
+ description,
3967
+ tools: tools.map((tool) => ({
3968
+ ...tool,
3969
+ executorCode: executorCode[tool.name]
3970
+ })),
3971
+ enabled: true,
3972
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
3973
+ };
3974
+ if (beforeRequestCode || afterResponseCode || onRegisterCode || onErrorCode) {
3975
+ storedPlugin.hooksCode = {};
3976
+ if (beforeRequestCode) storedPlugin.hooksCode.beforeRequestCode = beforeRequestCode;
3977
+ if (afterResponseCode) storedPlugin.hooksCode.afterResponseCode = afterResponseCode;
3978
+ if (onRegisterCode) storedPlugin.hooksCode.onRegisterCode = onRegisterCode;
3979
+ if (onErrorCode) storedPlugin.hooksCode.onErrorCode = onErrorCode;
3980
+ }
3981
+ return {
3982
+ success: true,
3983
+ plugin: storedPlugin,
3984
+ 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.`
3985
+ };
3986
+ };
3987
+ function normalizePlugin(input) {
3988
+ const plugin = input;
3989
+ const tools = plugin.tools;
3990
+ const hasEmbeddedExecutors = tools?.[0]?.executorCode !== void 0;
3991
+ if (hasEmbeddedExecutors) {
3992
+ return {
3993
+ ...plugin,
3994
+ enabled: plugin.enabled ?? true,
3995
+ installedAt: plugin.installedAt ?? (/* @__PURE__ */ new Date()).toISOString()
3996
+ };
3997
+ }
3998
+ const executorCode = plugin.executorCode;
3999
+ const rawTools = tools || [];
4000
+ const hookKeys = ["onRegisterCode", "beforeRequestCode", "afterResponseCode", "onErrorCode"];
4001
+ const hooksCode = {};
4002
+ for (const key of hookKeys) {
4003
+ if (executorCode?.[key]) {
4004
+ hooksCode[key] = executorCode[key];
4005
+ }
4006
+ if (plugin[key]) {
4007
+ hooksCode[key] = plugin[key];
4008
+ }
4009
+ }
4010
+ return {
4011
+ name: plugin.name,
4012
+ version: plugin.version,
4013
+ description: plugin.description,
4014
+ tools: rawTools.map((tool) => ({
4015
+ name: tool.name,
4016
+ description: tool.description,
4017
+ parameters: tool.parameters,
4018
+ executorCode: executorCode?.[tool.name]
4019
+ })),
4020
+ hooksCode: Object.keys(hooksCode).length > 0 ? hooksCode : void 0,
4021
+ enabled: true,
4022
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
4023
+ };
4024
+ }
4025
+ var savePluginExecutor = async (args2) => {
4026
+ const { plugin: rawPlugin, filename } = args2;
4027
+ if (typeof window === "undefined") {
4028
+ return { success: false, error: "Cannot save files in server environment" };
4029
+ }
4030
+ try {
4031
+ const plugin = normalizePlugin(rawPlugin);
4032
+ const json2 = JSON.stringify(plugin, null, 2);
4033
+ const blob = new Blob([json2], { type: "application/json" });
4034
+ const url = URL.createObjectURL(blob);
4035
+ const a = document.createElement("a");
4036
+ a.href = url;
4037
+ a.download = `${filename || plugin.name}.json`;
4038
+ document.body.appendChild(a);
4039
+ a.click();
4040
+ document.body.removeChild(a);
4041
+ URL.revokeObjectURL(url);
4042
+ return {
4043
+ success: true,
4044
+ message: `Plugin saved as ${filename || plugin.name}.json`
4045
+ };
4046
+ } catch (error2) {
4047
+ return {
4048
+ success: false,
4049
+ error: `Failed to save: ${error2 instanceof Error ? error2.message : "Unknown error"}`
4050
+ };
4051
+ }
4052
+ };
4053
+ var installPluginExecutor = async (args2) => {
4054
+ const { plugin: rawPlugin, enabled = true } = args2;
4055
+ if (typeof window === "undefined") {
4056
+ return { success: false, error: "Cannot install plugins in server environment" };
4057
+ }
4058
+ try {
4059
+ const plugin = normalizePlugin(rawPlugin);
4060
+ const win = window;
4061
+ if (!win.__hustleRegisterPlugin) {
4062
+ return {
4063
+ success: false,
4064
+ error: "Plugin registration not available. Make sure HustleProvider is mounted."
4065
+ };
4066
+ }
4067
+ await win.__hustleRegisterPlugin(plugin, enabled);
4068
+ return {
4069
+ success: true,
4070
+ message: `Plugin "${plugin.name}" installed${enabled ? " and enabled" : ""}.`
4071
+ };
4072
+ } catch (error2) {
4073
+ return {
4074
+ success: false,
4075
+ error: `Failed to install: ${error2 instanceof Error ? error2.message : "Unknown error"}`
4076
+ };
4077
+ }
4078
+ };
4079
+ var uninstallPluginExecutor = async (args2) => {
4080
+ const { name } = args2;
4081
+ if (typeof window === "undefined") {
4082
+ return { success: false, error: "Cannot uninstall plugins in server environment" };
4083
+ }
4084
+ try {
4085
+ const win = window;
4086
+ if (!win.__hustleUnregisterPlugin) {
4087
+ return {
4088
+ success: false,
4089
+ error: "Plugin unregistration not available. Make sure HustleProvider is mounted."
4090
+ };
4091
+ }
4092
+ await win.__hustleUnregisterPlugin(name);
4093
+ return {
4094
+ success: true,
4095
+ message: `Plugin "${name}" has been uninstalled.`
4096
+ };
4097
+ } catch (error2) {
4098
+ return {
4099
+ success: false,
4100
+ error: `Failed to uninstall: ${error2 instanceof Error ? error2.message : "Unknown error"}`
4101
+ };
4102
+ }
4103
+ };
4104
+ var listPluginsExecutor = async () => {
4105
+ if (typeof window === "undefined") {
4106
+ return { success: false, error: "Cannot list plugins in server environment" };
4107
+ }
4108
+ try {
4109
+ const win = window;
4110
+ if (!win.__hustleListPlugins) {
4111
+ return {
4112
+ success: false,
4113
+ error: "Plugin listing not available. Make sure HustleProvider is mounted."
4114
+ };
4115
+ }
4116
+ const plugins = win.__hustleListPlugins();
4117
+ return {
4118
+ success: true,
4119
+ plugins,
4120
+ 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(", ")}`
4121
+ };
4122
+ } catch (error2) {
4123
+ return {
4124
+ success: false,
4125
+ error: `Failed to list plugins: ${error2 instanceof Error ? error2.message : "Unknown error"}`
4126
+ };
4127
+ }
4128
+ };
4129
+ var modifyPluginExecutor = async (args2) => {
4130
+ const {
4131
+ name,
4132
+ version,
4133
+ description,
4134
+ addTools,
4135
+ addExecutorCode,
4136
+ removeTools,
4137
+ onRegisterCode,
4138
+ beforeRequestCode,
4139
+ afterResponseCode,
4140
+ onErrorCode
4141
+ } = args2;
4142
+ if (typeof window === "undefined") {
4143
+ return { success: false, error: "Cannot modify plugins in server environment" };
4144
+ }
4145
+ try {
4146
+ const win = window;
4147
+ if (!win.__hustleGetPlugin || !win.__hustleRegisterPlugin) {
4148
+ return {
4149
+ success: false,
4150
+ error: "Plugin modification not available. Make sure HustleProvider is mounted."
4151
+ };
4152
+ }
4153
+ const existing = win.__hustleGetPlugin(name);
4154
+ if (!existing) {
4155
+ return {
4156
+ success: false,
4157
+ error: `Plugin "${name}" not found. Use list_plugins to see installed plugins.`
4158
+ };
4159
+ }
4160
+ let tools = existing.tools ? [...existing.tools] : [];
4161
+ if (removeTools && removeTools.length > 0) {
4162
+ tools = tools.filter((t) => !removeTools.includes(t.name));
4163
+ }
4164
+ if (addTools && addTools.length > 0) {
4165
+ for (const newTool of addTools) {
4166
+ const existingIndex = tools.findIndex((t) => t.name === newTool.name);
4167
+ const toolWithExecutor = {
4168
+ ...newTool,
4169
+ executorCode: addExecutorCode?.[newTool.name] || tools.find((t) => t.name === newTool.name)?.executorCode
4170
+ };
4171
+ if (existingIndex >= 0) {
4172
+ tools[existingIndex] = toolWithExecutor;
4173
+ } else {
4174
+ tools.push(toolWithExecutor);
4175
+ }
4176
+ }
4177
+ }
4178
+ if (addExecutorCode) {
4179
+ for (const [toolName, code2] of Object.entries(addExecutorCode)) {
4180
+ const tool = tools.find((t) => t.name === toolName);
4181
+ if (tool) {
4182
+ tool.executorCode = code2;
4183
+ }
4184
+ }
4185
+ }
4186
+ const modified = {
4187
+ ...existing,
4188
+ tools
4189
+ };
4190
+ if (version) modified.version = version;
4191
+ if (description) modified.description = description;
4192
+ if (onRegisterCode || beforeRequestCode || afterResponseCode || onErrorCode) {
4193
+ modified.hooksCode = modified.hooksCode || {};
4194
+ if (onRegisterCode) modified.hooksCode.onRegisterCode = onRegisterCode;
4195
+ if (beforeRequestCode) modified.hooksCode.beforeRequestCode = beforeRequestCode;
4196
+ if (afterResponseCode) modified.hooksCode.afterResponseCode = afterResponseCode;
4197
+ if (onErrorCode) modified.hooksCode.onErrorCode = onErrorCode;
4198
+ }
4199
+ const unregisterFn = window.__hustleUnregisterPlugin;
4200
+ if (unregisterFn) {
4201
+ await unregisterFn(name);
4202
+ }
4203
+ await win.__hustleRegisterPlugin(modified, existing.enabled);
4204
+ const changes = [];
4205
+ if (version) changes.push(`version \u2192 ${version}`);
4206
+ if (description) changes.push("description updated");
4207
+ if (removeTools?.length) changes.push(`removed ${removeTools.length} tool(s)`);
4208
+ if (addTools?.length) changes.push(`added/updated ${addTools.length} tool(s)`);
4209
+ if (onRegisterCode) changes.push("onRegister hook updated");
4210
+ if (beforeRequestCode) changes.push("beforeRequest hook updated");
4211
+ if (afterResponseCode) changes.push("afterResponse hook updated");
4212
+ if (onErrorCode) changes.push("onError hook updated");
4213
+ return {
4214
+ success: true,
4215
+ message: `Plugin "${name}" modified: ${changes.join(", ")}`,
4216
+ plugin: {
4217
+ name: modified.name,
4218
+ version: modified.version,
4219
+ toolCount: tools.length
4220
+ }
4221
+ };
4222
+ } catch (error2) {
4223
+ return {
4224
+ success: false,
4225
+ error: `Failed to modify plugin: ${error2 instanceof Error ? error2.message : "Unknown error"}`
4226
+ };
4227
+ }
4228
+ };
4229
+ var pluginBuilderPlugin = {
4230
+ name: "plugin-builder",
4231
+ version: "1.2.0",
4232
+ description: "Build custom plugins through conversation",
4233
+ tools: [buildPluginTool, savePluginTool, installPluginTool, uninstallPluginTool, listPluginsTool, modifyPluginTool],
4234
+ executors: {
4235
+ build_plugin: buildPluginExecutor,
4236
+ save_plugin: savePluginExecutor,
4237
+ install_plugin: installPluginExecutor,
4238
+ uninstall_plugin: uninstallPluginExecutor,
4239
+ list_plugins: listPluginsExecutor,
4240
+ modify_plugin: modifyPluginExecutor
4241
+ },
4242
+ hooks: {
4243
+ onRegister: () => {
4244
+ console.log("[Plugin Builder] Ready to help build custom plugins");
4245
+ }
4246
+ }
4247
+ };
4248
+
3442
4249
  // src/plugins/index.ts
3443
4250
  var availablePlugins = [
3444
4251
  {
@@ -3468,6 +4275,10 @@ var availablePlugins = [
3468
4275
  {
3469
4276
  ...screenshotPlugin,
3470
4277
  description: "Take screenshots of the current page"
4278
+ },
4279
+ {
4280
+ ...pluginBuilderPlugin,
4281
+ description: "Build custom plugins through conversation with AI"
3471
4282
  }
3472
4283
  ];
3473
4284
  function getAvailablePlugin(name) {