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