@emblemvault/hustle-react 1.0.0

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.
Files changed (38) hide show
  1. package/README.md +225 -0
  2. package/dist/browser/hustle-react.js +14705 -0
  3. package/dist/browser/hustle-react.js.map +1 -0
  4. package/dist/components/index.cjs +3170 -0
  5. package/dist/components/index.cjs.map +1 -0
  6. package/dist/components/index.d.cts +58 -0
  7. package/dist/components/index.d.ts +58 -0
  8. package/dist/components/index.js +3143 -0
  9. package/dist/components/index.js.map +1 -0
  10. package/dist/hooks/index.cjs +695 -0
  11. package/dist/hooks/index.cjs.map +1 -0
  12. package/dist/hooks/index.d.cts +46 -0
  13. package/dist/hooks/index.d.ts +46 -0
  14. package/dist/hooks/index.js +691 -0
  15. package/dist/hooks/index.js.map +1 -0
  16. package/dist/hustle-S48t4lTZ.d.cts +222 -0
  17. package/dist/hustle-S48t4lTZ.d.ts +222 -0
  18. package/dist/index.cjs +3588 -0
  19. package/dist/index.cjs.map +1 -0
  20. package/dist/index.d.cts +229 -0
  21. package/dist/index.d.ts +229 -0
  22. package/dist/index.js +3547 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/plugin-BUg7vMxe.d.cts +172 -0
  25. package/dist/plugin-BUg7vMxe.d.ts +172 -0
  26. package/dist/plugins/index.cjs +1235 -0
  27. package/dist/plugins/index.cjs.map +1 -0
  28. package/dist/plugins/index.d.cts +192 -0
  29. package/dist/plugins/index.d.ts +192 -0
  30. package/dist/plugins/index.js +1225 -0
  31. package/dist/plugins/index.js.map +1 -0
  32. package/dist/providers/index.cjs +694 -0
  33. package/dist/providers/index.cjs.map +1 -0
  34. package/dist/providers/index.d.cts +46 -0
  35. package/dist/providers/index.d.ts +46 -0
  36. package/dist/providers/index.js +691 -0
  37. package/dist/providers/index.js.map +1 -0
  38. package/package.json +87 -0
@@ -0,0 +1,695 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var hustleIncognito = require('hustle-incognito');
5
+ var emblemAuthReact = require('@emblemvault/emblem-auth-react');
6
+ var jsxRuntime = require('react/jsx-runtime');
7
+
8
+ // src/utils/pluginRegistry.ts
9
+ var PLUGINS_KEY = "hustle-plugins";
10
+ function getEnabledStateKey(instanceId) {
11
+ return `hustle-plugin-state-${instanceId}`;
12
+ }
13
+ function serializeFunction(fn) {
14
+ return fn.toString();
15
+ }
16
+ function deserializeExecutor(code) {
17
+ try {
18
+ return eval(`(${code})`);
19
+ } catch (err) {
20
+ console.error("[Hustle] Failed to deserialize executor:", err);
21
+ return async () => ({ error: "Failed to deserialize executor", code });
22
+ }
23
+ }
24
+ function deserializeHook(code) {
25
+ try {
26
+ return eval(`(${code})`);
27
+ } catch (err) {
28
+ console.error("[Hustle] Failed to deserialize hook:", err);
29
+ return (() => {
30
+ });
31
+ }
32
+ }
33
+ function serializePluginTools(tools, executors) {
34
+ if (!tools) return [];
35
+ return tools.map((tool) => ({
36
+ ...tool,
37
+ executorCode: executors?.[tool.name] ? serializeFunction(executors[tool.name]) : void 0
38
+ }));
39
+ }
40
+ function serializeHooks(hooks) {
41
+ if (!hooks) return void 0;
42
+ const serialized = {};
43
+ if (hooks.onRegister) {
44
+ serialized.onRegisterCode = serializeFunction(hooks.onRegister);
45
+ }
46
+ if (hooks.beforeRequest) {
47
+ serialized.beforeRequestCode = serializeFunction(hooks.beforeRequest);
48
+ }
49
+ if (hooks.afterResponse) {
50
+ serialized.afterResponseCode = serializeFunction(hooks.afterResponse);
51
+ }
52
+ if (hooks.onError) {
53
+ serialized.onErrorCode = serializeFunction(hooks.onError);
54
+ }
55
+ return Object.keys(serialized).length > 0 ? serialized : void 0;
56
+ }
57
+ function hydratePlugin(stored) {
58
+ const executors = {};
59
+ if (stored.tools) {
60
+ for (const tool of stored.tools) {
61
+ if (tool.executorCode) {
62
+ executors[tool.name] = deserializeExecutor(tool.executorCode);
63
+ }
64
+ }
65
+ }
66
+ let hooks;
67
+ if (stored.hooksCode) {
68
+ hooks = {};
69
+ if (stored.hooksCode.onRegisterCode) {
70
+ hooks.onRegister = deserializeHook(stored.hooksCode.onRegisterCode);
71
+ }
72
+ if (stored.hooksCode.beforeRequestCode) {
73
+ hooks.beforeRequest = deserializeHook(stored.hooksCode.beforeRequestCode);
74
+ }
75
+ if (stored.hooksCode.afterResponseCode) {
76
+ hooks.afterResponse = deserializeHook(stored.hooksCode.afterResponseCode);
77
+ }
78
+ if (stored.hooksCode.onErrorCode) {
79
+ hooks.onError = deserializeHook(stored.hooksCode.onErrorCode);
80
+ }
81
+ }
82
+ return {
83
+ ...stored,
84
+ executors: Object.keys(executors).length > 0 ? executors : void 0,
85
+ hooks
86
+ };
87
+ }
88
+ var PluginRegistry = class {
89
+ constructor() {
90
+ this.listeners = /* @__PURE__ */ new Map();
91
+ }
92
+ /**
93
+ * Get listeners for a specific instance
94
+ */
95
+ getListeners(instanceId) {
96
+ if (!this.listeners.has(instanceId)) {
97
+ this.listeners.set(instanceId, /* @__PURE__ */ new Set());
98
+ }
99
+ return this.listeners.get(instanceId);
100
+ }
101
+ /**
102
+ * Load installed plugins (global)
103
+ */
104
+ loadInstalledPlugins() {
105
+ if (typeof window === "undefined") return [];
106
+ try {
107
+ const stored = localStorage.getItem(PLUGINS_KEY);
108
+ return stored ? JSON.parse(stored) : [];
109
+ } catch {
110
+ return [];
111
+ }
112
+ }
113
+ /**
114
+ * Save installed plugins (global)
115
+ * Serializes executors as executorCode strings
116
+ */
117
+ saveInstalledPlugins(plugins) {
118
+ if (typeof window === "undefined") return;
119
+ localStorage.setItem(PLUGINS_KEY, JSON.stringify(plugins));
120
+ }
121
+ /**
122
+ * Load enabled state for an instance
123
+ */
124
+ loadEnabledState(instanceId) {
125
+ if (typeof window === "undefined") return {};
126
+ try {
127
+ const stored = localStorage.getItem(getEnabledStateKey(instanceId));
128
+ return stored ? JSON.parse(stored) : {};
129
+ } catch {
130
+ return {};
131
+ }
132
+ }
133
+ /**
134
+ * Save enabled state for an instance
135
+ */
136
+ saveEnabledState(state, instanceId) {
137
+ if (typeof window === "undefined") return;
138
+ localStorage.setItem(getEnabledStateKey(instanceId), JSON.stringify(state));
139
+ }
140
+ /**
141
+ * Load plugins with instance-specific enabled state
142
+ * Combines global plugin list with per-instance enabled state
143
+ */
144
+ loadFromStorage(instanceId = "default") {
145
+ const installed = this.loadInstalledPlugins();
146
+ const enabledState = this.loadEnabledState(instanceId);
147
+ return installed.map((plugin) => ({
148
+ ...plugin,
149
+ // Default to enabled if no state exists for this instance
150
+ enabled: enabledState[plugin.name] ?? true
151
+ }));
152
+ }
153
+ /**
154
+ * Register a new plugin (global - available to all instances)
155
+ * Serializes executors as executorCode for persistence
156
+ *
157
+ * @param plugin The plugin to install
158
+ * @param enabled Initial enabled state for this instance (default: true)
159
+ * @param instanceId Instance to set initial enabled state for
160
+ */
161
+ register(plugin, enabled = true, instanceId = "default") {
162
+ const installed = this.loadInstalledPlugins();
163
+ const existing = installed.findIndex((p) => p.name === plugin.name);
164
+ const storedPlugin = {
165
+ name: plugin.name,
166
+ version: plugin.version,
167
+ description: plugin.description,
168
+ tools: serializePluginTools(plugin.tools, plugin.executors),
169
+ hooksCode: serializeHooks(plugin.hooks),
170
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
171
+ };
172
+ if (existing >= 0) {
173
+ installed[existing] = storedPlugin;
174
+ } else {
175
+ installed.push(storedPlugin);
176
+ }
177
+ this.saveInstalledPlugins(installed);
178
+ const enabledState = this.loadEnabledState(instanceId);
179
+ enabledState[plugin.name] = enabled;
180
+ this.saveEnabledState(enabledState, instanceId);
181
+ this.notifyListeners(instanceId);
182
+ }
183
+ /**
184
+ * Unregister a plugin (global - removes from all instances)
185
+ */
186
+ unregister(pluginName, instanceId = "default") {
187
+ const installed = this.loadInstalledPlugins().filter((p) => p.name !== pluginName);
188
+ this.saveInstalledPlugins(installed);
189
+ const enabledState = this.loadEnabledState(instanceId);
190
+ delete enabledState[pluginName];
191
+ this.saveEnabledState(enabledState, instanceId);
192
+ this.notifyListeners(instanceId);
193
+ }
194
+ /**
195
+ * Enable or disable a plugin (instance-scoped)
196
+ */
197
+ setEnabled(pluginName, enabled, instanceId = "default") {
198
+ const enabledState = this.loadEnabledState(instanceId);
199
+ enabledState[pluginName] = enabled;
200
+ this.saveEnabledState(enabledState, instanceId);
201
+ this.notifyListeners(instanceId);
202
+ }
203
+ /**
204
+ * Check if a plugin is installed (global)
205
+ */
206
+ isRegistered(pluginName) {
207
+ return this.loadInstalledPlugins().some((p) => p.name === pluginName);
208
+ }
209
+ /**
210
+ * Get a specific plugin with instance-specific enabled state
211
+ */
212
+ getPlugin(pluginName, instanceId = "default") {
213
+ return this.loadFromStorage(instanceId).find((p) => p.name === pluginName);
214
+ }
215
+ /**
216
+ * Get all enabled plugins for an instance (hydrated with executors)
217
+ */
218
+ getEnabledPlugins(instanceId = "default") {
219
+ return this.loadFromStorage(instanceId).filter((p) => p.enabled).map(hydratePlugin);
220
+ }
221
+ /**
222
+ * Subscribe to plugin changes for a specific instance
223
+ */
224
+ onChange(callback, instanceId = "default") {
225
+ const listeners = this.getListeners(instanceId);
226
+ listeners.add(callback);
227
+ return () => listeners.delete(callback);
228
+ }
229
+ /**
230
+ * Notify all listeners for a specific instance
231
+ */
232
+ notifyListeners(instanceId = "default") {
233
+ const plugins = this.loadFromStorage(instanceId);
234
+ const listeners = this.getListeners(instanceId);
235
+ listeners.forEach((cb) => cb(plugins));
236
+ }
237
+ /**
238
+ * Clear enabled state for an instance (plugins remain installed globally)
239
+ */
240
+ clear(instanceId = "default") {
241
+ if (typeof window === "undefined") return;
242
+ localStorage.removeItem(getEnabledStateKey(instanceId));
243
+ this.notifyListeners(instanceId);
244
+ }
245
+ /**
246
+ * Clear all installed plugins globally
247
+ */
248
+ clearAll() {
249
+ if (typeof window === "undefined") return;
250
+ localStorage.removeItem(PLUGINS_KEY);
251
+ }
252
+ };
253
+ var pluginRegistry = new PluginRegistry();
254
+
255
+ // src/hooks/usePlugins.ts
256
+ function getStorageKey(instanceId) {
257
+ return `hustle-plugins-${instanceId}`;
258
+ }
259
+ function usePlugins(instanceId = "default") {
260
+ const [plugins, setPlugins] = react.useState([]);
261
+ react.useEffect(() => {
262
+ setPlugins(pluginRegistry.loadFromStorage(instanceId));
263
+ const unsubscribe = pluginRegistry.onChange(setPlugins, instanceId);
264
+ const storageKey = getStorageKey(instanceId);
265
+ const handleStorage = (e) => {
266
+ if (e.key === storageKey) {
267
+ setPlugins(pluginRegistry.loadFromStorage(instanceId));
268
+ }
269
+ };
270
+ window.addEventListener("storage", handleStorage);
271
+ return () => {
272
+ unsubscribe();
273
+ window.removeEventListener("storage", handleStorage);
274
+ };
275
+ }, [instanceId]);
276
+ const registerPlugin = react.useCallback((plugin) => {
277
+ pluginRegistry.register(plugin, true, instanceId);
278
+ }, [instanceId]);
279
+ const unregisterPlugin = react.useCallback((name) => {
280
+ pluginRegistry.unregister(name, instanceId);
281
+ }, [instanceId]);
282
+ const enablePlugin = react.useCallback((name) => {
283
+ pluginRegistry.setEnabled(name, true, instanceId);
284
+ }, [instanceId]);
285
+ const disablePlugin = react.useCallback((name) => {
286
+ pluginRegistry.setEnabled(name, false, instanceId);
287
+ }, [instanceId]);
288
+ const isRegistered = react.useCallback(
289
+ (name) => plugins.some((p) => p.name === name),
290
+ [plugins]
291
+ );
292
+ const isEnabled = react.useCallback(
293
+ (name) => plugins.some((p) => p.name === name && p.enabled),
294
+ [plugins]
295
+ );
296
+ const enabledPlugins = plugins.filter((p) => p.enabled).map(hydratePlugin);
297
+ return {
298
+ plugins,
299
+ enabledPlugins,
300
+ registerPlugin,
301
+ unregisterPlugin,
302
+ enablePlugin,
303
+ disablePlugin,
304
+ isRegistered,
305
+ isEnabled
306
+ };
307
+ }
308
+ var HustleContext = react.createContext(void 0);
309
+ var DEFAULT_HUSTLE_API_URL = "https://agenthustle.ai";
310
+ var instanceCounter = 0;
311
+ var mountedAutoInstances = /* @__PURE__ */ new Set();
312
+ function HustleProvider({
313
+ children,
314
+ hustleApiUrl = DEFAULT_HUSTLE_API_URL,
315
+ debug = false,
316
+ instanceId: explicitInstanceId,
317
+ apiKey,
318
+ vaultId
319
+ }) {
320
+ const [resolvedInstanceId] = react.useState(() => {
321
+ if (explicitInstanceId) {
322
+ return explicitInstanceId;
323
+ }
324
+ const autoId = `instance-${++instanceCounter}`;
325
+ mountedAutoInstances.add(autoId);
326
+ return autoId;
327
+ });
328
+ const isAutoInstance = !explicitInstanceId;
329
+ react.useEffect(() => {
330
+ if (isAutoInstance && mountedAutoInstances.size > 1 && process.env.NODE_ENV !== "production") {
331
+ console.warn(
332
+ `[Hustle] Multiple HustleProviders detected without explicit instanceId. For stable settings persistence, consider adding instanceId prop:
333
+ <HustleProvider instanceId="my-chat-name">`
334
+ );
335
+ }
336
+ return () => {
337
+ if (isAutoInstance) {
338
+ mountedAutoInstances.delete(resolvedInstanceId);
339
+ }
340
+ };
341
+ }, [isAutoInstance, resolvedInstanceId]);
342
+ const isApiKeyMode = Boolean(apiKey && vaultId);
343
+ const authContext = emblemAuthReact.useEmblemAuthOptional();
344
+ const authSDK = isApiKeyMode ? null : authContext?.authSDK ?? null;
345
+ const isAuthenticated = isApiKeyMode ? true : authContext?.isAuthenticated ?? false;
346
+ const { enabledPlugins } = usePlugins(resolvedInstanceId);
347
+ const [isLoading, setIsLoading] = react.useState(false);
348
+ const [error, setError] = react.useState(null);
349
+ const [models, setModels] = react.useState([]);
350
+ const registeredPluginsRef = react.useRef(/* @__PURE__ */ new Set());
351
+ const SETTINGS_KEY = `hustle-settings-${resolvedInstanceId}`;
352
+ const loadSettings = () => {
353
+ if (typeof window === "undefined") return { selectedModel: "", systemPrompt: "", skipServerPrompt: false };
354
+ try {
355
+ const stored = localStorage.getItem(SETTINGS_KEY);
356
+ if (stored) {
357
+ return JSON.parse(stored);
358
+ }
359
+ } catch {
360
+ }
361
+ return { selectedModel: "", systemPrompt: "", skipServerPrompt: false };
362
+ };
363
+ const initialSettings = loadSettings();
364
+ const [selectedModel, setSelectedModelState] = react.useState(initialSettings.selectedModel);
365
+ const [systemPrompt, setSystemPromptState] = react.useState(initialSettings.systemPrompt);
366
+ const [skipServerPrompt, setSkipServerPromptState] = react.useState(initialSettings.skipServerPrompt);
367
+ const saveSettings = react.useCallback((settings) => {
368
+ if (typeof window === "undefined") return;
369
+ try {
370
+ localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
371
+ } catch {
372
+ }
373
+ }, []);
374
+ const setSelectedModel = react.useCallback((value2) => {
375
+ setSelectedModelState(value2);
376
+ saveSettings({ selectedModel: value2, systemPrompt, skipServerPrompt });
377
+ }, [systemPrompt, skipServerPrompt, saveSettings]);
378
+ const setSystemPrompt = react.useCallback((value2) => {
379
+ setSystemPromptState(value2);
380
+ saveSettings({ selectedModel, systemPrompt: value2, skipServerPrompt });
381
+ }, [selectedModel, skipServerPrompt, saveSettings]);
382
+ const setSkipServerPrompt = react.useCallback((value2) => {
383
+ setSkipServerPromptState(value2);
384
+ saveSettings({ selectedModel, systemPrompt, skipServerPrompt: value2 });
385
+ }, [selectedModel, systemPrompt, saveSettings]);
386
+ const log = react.useCallback(
387
+ (message, ...args) => {
388
+ if (debug) {
389
+ console.log(`[Hustle] ${message}`, ...args);
390
+ }
391
+ },
392
+ [debug]
393
+ );
394
+ const client = react.useMemo(() => {
395
+ if (isApiKeyMode && apiKey && vaultId) {
396
+ log("Creating HustleIncognitoClient with API key");
397
+ try {
398
+ const hustleClient = new hustleIncognito.HustleIncognitoClient({
399
+ apiKey,
400
+ vaultId,
401
+ hustleApiUrl,
402
+ debug
403
+ });
404
+ hustleClient.on("tool_start", (event) => {
405
+ log("Tool start:", event);
406
+ });
407
+ hustleClient.on("tool_end", (event) => {
408
+ log("Tool end:", event);
409
+ });
410
+ hustleClient.on("stream_end", (event) => {
411
+ log("Stream end:", event);
412
+ });
413
+ return hustleClient;
414
+ } catch (err) {
415
+ log("Failed to create client:", err);
416
+ setError(err instanceof Error ? err : new Error("Failed to create Hustle client"));
417
+ return null;
418
+ }
419
+ }
420
+ if (!authSDK || !isAuthenticated) {
421
+ log("Client not created - auth not ready");
422
+ return null;
423
+ }
424
+ log("Creating HustleIncognitoClient with auth SDK");
425
+ try {
426
+ const hustleClient = new hustleIncognito.HustleIncognitoClient({
427
+ sdk: authSDK,
428
+ hustleApiUrl,
429
+ debug
430
+ });
431
+ hustleClient.on("tool_start", (event) => {
432
+ log("Tool start:", event);
433
+ });
434
+ hustleClient.on("tool_end", (event) => {
435
+ log("Tool end:", event);
436
+ });
437
+ hustleClient.on("stream_end", (event) => {
438
+ log("Stream end:", event);
439
+ });
440
+ return hustleClient;
441
+ } catch (err) {
442
+ log("Failed to create client:", err);
443
+ setError(err instanceof Error ? err : new Error("Failed to create Hustle client"));
444
+ return null;
445
+ }
446
+ }, [isApiKeyMode, apiKey, vaultId, authSDK, isAuthenticated, hustleApiUrl, debug, log]);
447
+ const isReady = client !== null;
448
+ react.useEffect(() => {
449
+ if (!client) return;
450
+ const registerPlugins = async () => {
451
+ const enabledNames = new Set(enabledPlugins.map((p) => p.name));
452
+ for (const name of registeredPluginsRef.current) {
453
+ if (!enabledNames.has(name)) {
454
+ log("Unregistering plugin:", name);
455
+ try {
456
+ await client.unuse(name);
457
+ registeredPluginsRef.current.delete(name);
458
+ log("Plugin unregistered:", name);
459
+ } catch (err) {
460
+ log("Failed to unregister plugin:", name, err);
461
+ }
462
+ }
463
+ }
464
+ for (const plugin of enabledPlugins) {
465
+ if (!registeredPluginsRef.current.has(plugin.name)) {
466
+ log("Registering plugin:", plugin.name);
467
+ try {
468
+ if (plugin.executors || plugin.hooks) {
469
+ await client.use({
470
+ name: plugin.name,
471
+ version: plugin.version,
472
+ tools: plugin.tools,
473
+ executors: plugin.executors,
474
+ hooks: plugin.hooks
475
+ });
476
+ registeredPluginsRef.current.add(plugin.name);
477
+ log("Plugin registered:", plugin.name);
478
+ } else {
479
+ log("Plugin has no executors/hooks, skipping registration:", plugin.name);
480
+ }
481
+ } catch (err) {
482
+ log("Failed to register plugin:", plugin.name, err);
483
+ }
484
+ }
485
+ }
486
+ };
487
+ registerPlugins();
488
+ }, [client, enabledPlugins, log]);
489
+ const loadModels = react.useCallback(async () => {
490
+ if (!client) {
491
+ log("Cannot load models - client not ready");
492
+ return [];
493
+ }
494
+ log("Loading models");
495
+ setIsLoading(true);
496
+ try {
497
+ const modelList = await client.getModels();
498
+ setModels(modelList);
499
+ log("Loaded models:", modelList.length);
500
+ return modelList;
501
+ } catch (err) {
502
+ log("Failed to load models:", err);
503
+ setError(err instanceof Error ? err : new Error("Failed to load models"));
504
+ return [];
505
+ } finally {
506
+ setIsLoading(false);
507
+ }
508
+ }, [client, log]);
509
+ react.useEffect(() => {
510
+ if (client) {
511
+ loadModels();
512
+ }
513
+ }, [client, loadModels]);
514
+ const chat = react.useCallback(
515
+ async (options) => {
516
+ if (!client) {
517
+ throw new Error("Hustle client not ready. Please authenticate first.");
518
+ }
519
+ log("Chat request:", options.messages.length, "messages");
520
+ setIsLoading(true);
521
+ setError(null);
522
+ try {
523
+ const effectiveSystemPrompt = options.systemPrompt || systemPrompt;
524
+ const messagesWithSystem = [];
525
+ if (effectiveSystemPrompt) {
526
+ messagesWithSystem.push({ role: "system", content: effectiveSystemPrompt });
527
+ }
528
+ messagesWithSystem.push(...options.messages);
529
+ const sdkOptions = {
530
+ messages: messagesWithSystem,
531
+ processChunks: true
532
+ };
533
+ if (isApiKeyMode && vaultId) {
534
+ sdkOptions.vaultId = vaultId;
535
+ }
536
+ if (options.model || selectedModel) {
537
+ sdkOptions.model = options.model || selectedModel;
538
+ }
539
+ if (options.overrideSystemPrompt ?? skipServerPrompt) {
540
+ sdkOptions.overrideSystemPrompt = true;
541
+ }
542
+ if (options.attachments) {
543
+ sdkOptions.attachments = options.attachments;
544
+ }
545
+ const response = await client.chat(sdkOptions);
546
+ log("Chat response received");
547
+ return response;
548
+ } catch (err) {
549
+ log("Chat error:", err);
550
+ const error2 = err instanceof Error ? err : new Error("Chat request failed");
551
+ setError(error2);
552
+ throw error2;
553
+ } finally {
554
+ setIsLoading(false);
555
+ }
556
+ },
557
+ [client, isApiKeyMode, vaultId, selectedModel, systemPrompt, skipServerPrompt, log]
558
+ );
559
+ const chatStreamImpl = react.useCallback(
560
+ (options) => {
561
+ if (!client) {
562
+ return {
563
+ [Symbol.asyncIterator]: async function* () {
564
+ yield { type: "error", value: { message: "Hustle client not ready. Please authenticate first." } };
565
+ },
566
+ response: Promise.resolve({ content: "", messageId: void 0 })
567
+ };
568
+ }
569
+ log("Chat stream request:", options.messages.length, "messages");
570
+ setError(null);
571
+ const effectiveSystemPrompt = options.systemPrompt || systemPrompt;
572
+ const messagesWithSystem = [];
573
+ if (effectiveSystemPrompt) {
574
+ messagesWithSystem.push({ role: "system", content: effectiveSystemPrompt });
575
+ }
576
+ messagesWithSystem.push(...options.messages);
577
+ const sdkOptions = {
578
+ messages: messagesWithSystem,
579
+ processChunks: options.processChunks ?? true
580
+ };
581
+ if (isApiKeyMode && vaultId) {
582
+ sdkOptions.vaultId = vaultId;
583
+ }
584
+ if (options.model || selectedModel) {
585
+ sdkOptions.model = options.model || selectedModel;
586
+ }
587
+ if (options.overrideSystemPrompt ?? skipServerPrompt) {
588
+ sdkOptions.overrideSystemPrompt = true;
589
+ }
590
+ if (options.attachments) {
591
+ sdkOptions.attachments = options.attachments;
592
+ }
593
+ const stream = client.chatStream(sdkOptions);
594
+ return {
595
+ [Symbol.asyncIterator]: async function* () {
596
+ try {
597
+ for await (const chunk of stream) {
598
+ const typedChunk = chunk;
599
+ if (typedChunk.type === "text") {
600
+ const textValue = typedChunk.value;
601
+ log("Stream text chunk:", textValue?.substring(0, 50));
602
+ yield { type: "text", value: textValue };
603
+ } else if (typedChunk.type === "tool_call") {
604
+ log("Stream tool call:", typedChunk.value);
605
+ yield { type: "tool_call", value: typedChunk.value };
606
+ } else if (typedChunk.type === "tool_result") {
607
+ log("Stream tool result");
608
+ yield { type: "tool_result", value: typedChunk.value };
609
+ } else if (typedChunk.type === "error") {
610
+ const errorValue = typedChunk.value;
611
+ log("Stream error:", errorValue);
612
+ setError(new Error(errorValue?.message || "Stream error"));
613
+ yield { type: "error", value: { message: errorValue?.message || "Stream error" } };
614
+ } else {
615
+ yield chunk;
616
+ }
617
+ }
618
+ } catch (err) {
619
+ log("Stream error:", err);
620
+ const error2 = err instanceof Error ? err : new Error("Stream failed");
621
+ setError(error2);
622
+ yield { type: "error", value: { message: error2.message } };
623
+ }
624
+ },
625
+ // Forward the response promise from the SDK stream (includes afterResponse hook modifications)
626
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
627
+ response: stream.response
628
+ };
629
+ },
630
+ [client, isApiKeyMode, vaultId, selectedModel, systemPrompt, skipServerPrompt, log]
631
+ );
632
+ const uploadFile = react.useCallback(
633
+ async (file) => {
634
+ if (!client) {
635
+ throw new Error("Hustle client not ready. Please authenticate first.");
636
+ }
637
+ log("Uploading file:", file.name);
638
+ setIsLoading(true);
639
+ try {
640
+ const attachment = await client.uploadFile(file);
641
+ log("File uploaded:", attachment);
642
+ return attachment;
643
+ } catch (err) {
644
+ log("Upload error:", err);
645
+ const error2 = err instanceof Error ? err : new Error("File upload failed");
646
+ setError(error2);
647
+ throw error2;
648
+ } finally {
649
+ setIsLoading(false);
650
+ }
651
+ },
652
+ [client, log]
653
+ );
654
+ const value = {
655
+ // Instance ID for scoped storage
656
+ instanceId: resolvedInstanceId,
657
+ // Auth mode
658
+ isApiKeyMode,
659
+ // State
660
+ isReady,
661
+ isLoading,
662
+ error,
663
+ models,
664
+ // Client (for advanced use)
665
+ client,
666
+ // Chat methods
667
+ chat,
668
+ chatStream: chatStreamImpl,
669
+ // File upload
670
+ uploadFile,
671
+ // Data fetching
672
+ loadModels,
673
+ // Settings
674
+ selectedModel,
675
+ setSelectedModel,
676
+ systemPrompt,
677
+ setSystemPrompt,
678
+ skipServerPrompt,
679
+ setSkipServerPrompt
680
+ };
681
+ return /* @__PURE__ */ jsxRuntime.jsx(HustleContext.Provider, { value, children });
682
+ }
683
+ function useHustle() {
684
+ const context = react.useContext(HustleContext);
685
+ if (context === void 0) {
686
+ throw new Error("useHustle must be used within a HustleProvider (which requires EmblemAuthProvider)");
687
+ }
688
+ return context;
689
+ }
690
+
691
+ exports.HustleProvider = HustleProvider;
692
+ exports.useHustle = useHustle;
693
+ exports.usePlugins = usePlugins;
694
+ //# sourceMappingURL=index.cjs.map
695
+ //# sourceMappingURL=index.cjs.map