agent-hustle-demo 1.0.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.
Files changed (60) hide show
  1. package/README.md +429 -0
  2. package/dist/HustleChat-BC9wvWVA.d.ts +90 -0
  3. package/dist/HustleChat-BcrKkkyn.d.cts +90 -0
  4. package/dist/browser/hustle-react.js +14854 -0
  5. package/dist/browser/hustle-react.js.map +1 -0
  6. package/dist/components/index.cjs +3141 -0
  7. package/dist/components/index.cjs.map +1 -0
  8. package/dist/components/index.d.cts +20 -0
  9. package/dist/components/index.d.ts +20 -0
  10. package/dist/components/index.js +3112 -0
  11. package/dist/components/index.js.map +1 -0
  12. package/dist/hooks/index.cjs +845 -0
  13. package/dist/hooks/index.cjs.map +1 -0
  14. package/dist/hooks/index.d.cts +6 -0
  15. package/dist/hooks/index.d.ts +6 -0
  16. package/dist/hooks/index.js +838 -0
  17. package/dist/hooks/index.js.map +1 -0
  18. package/dist/hustle-Kj0X8qXC.d.cts +193 -0
  19. package/dist/hustle-Kj0X8qXC.d.ts +193 -0
  20. package/dist/index-ChUsRBwL.d.ts +152 -0
  21. package/dist/index-DE1N7C3W.d.cts +152 -0
  22. package/dist/index-DuPFrMZy.d.cts +214 -0
  23. package/dist/index-kFIdHjNw.d.ts +214 -0
  24. package/dist/index.cjs +3746 -0
  25. package/dist/index.cjs.map +1 -0
  26. package/dist/index.d.cts +271 -0
  27. package/dist/index.d.ts +271 -0
  28. package/dist/index.js +3697 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/providers/index.cjs +844 -0
  31. package/dist/providers/index.cjs.map +1 -0
  32. package/dist/providers/index.d.cts +5 -0
  33. package/dist/providers/index.d.ts +5 -0
  34. package/dist/providers/index.js +838 -0
  35. package/dist/providers/index.js.map +1 -0
  36. package/package.json +80 -0
  37. package/src/components/AuthStatus.tsx +352 -0
  38. package/src/components/ConnectButton.tsx +421 -0
  39. package/src/components/HustleChat.tsx +1273 -0
  40. package/src/components/MarkdownContent.tsx +431 -0
  41. package/src/components/index.ts +15 -0
  42. package/src/hooks/index.ts +40 -0
  43. package/src/hooks/useEmblemAuth.ts +27 -0
  44. package/src/hooks/useHustle.ts +36 -0
  45. package/src/hooks/usePlugins.ts +135 -0
  46. package/src/index.ts +142 -0
  47. package/src/plugins/index.ts +48 -0
  48. package/src/plugins/migrateFun.ts +211 -0
  49. package/src/plugins/predictionMarket.ts +411 -0
  50. package/src/providers/EmblemAuthProvider.tsx +319 -0
  51. package/src/providers/HustleProvider.tsx +540 -0
  52. package/src/providers/index.ts +6 -0
  53. package/src/styles/index.ts +2 -0
  54. package/src/styles/tokens.ts +447 -0
  55. package/src/types/auth.ts +85 -0
  56. package/src/types/hustle.ts +217 -0
  57. package/src/types/index.ts +49 -0
  58. package/src/types/plugin.ts +180 -0
  59. package/src/utils/index.ts +122 -0
  60. package/src/utils/pluginRegistry.ts +375 -0
@@ -0,0 +1,540 @@
1
+ 'use client';
2
+
3
+ import React, {
4
+ createContext,
5
+ useContext,
6
+ useState,
7
+ useEffect,
8
+ useCallback,
9
+ useMemo,
10
+ useRef,
11
+ } from 'react';
12
+ import { HustleIncognitoClient } from 'hustle-incognito';
13
+ import { useEmblemAuth } from './EmblemAuthProvider';
14
+ import { usePlugins } from '../hooks/usePlugins';
15
+ import type {
16
+ Model,
17
+ ChatOptions,
18
+ StreamOptions,
19
+ StreamChunk,
20
+ ChatResponse,
21
+ Attachment,
22
+ HustleContextValue,
23
+ HustleProviderProps,
24
+ ChatMessage,
25
+ HydratedPlugin,
26
+ } from '../types';
27
+
28
+ /**
29
+ * Hustle context - undefined when not within provider
30
+ */
31
+ const HustleContext = createContext<HustleContextValue | undefined>(undefined);
32
+
33
+ /**
34
+ * Default Hustle API URL
35
+ */
36
+ const DEFAULT_HUSTLE_API_URL = 'https://agenthustle.ai';
37
+
38
+ /**
39
+ * Module-level counter for auto-generating instance IDs.
40
+ * Resets on page load, but render order is deterministic.
41
+ */
42
+ let instanceCounter = 0;
43
+
44
+ /**
45
+ * Track mounted instances without explicit IDs for dev warnings
46
+ */
47
+ const mountedAutoInstances = new Set<string>();
48
+
49
+ /**
50
+ * HustleProvider - Provides Hustle SDK functionality to the app
51
+ *
52
+ * IMPORTANT: This provider depends on EmblemAuthProvider and must be nested within it.
53
+ * It uses the modern pattern of passing the auth SDK instance to HustleIncognitoClient,
54
+ * NOT the deprecated api-key pattern.
55
+ *
56
+ * @example
57
+ * ```tsx
58
+ * <EmblemAuthProvider appId="your-app-id">
59
+ * <HustleProvider hustleApiUrl="https://dev.agenthustle.ai">
60
+ * <App />
61
+ * </HustleProvider>
62
+ * </EmblemAuthProvider>
63
+ * ```
64
+ */
65
+ export function HustleProvider({
66
+ children,
67
+ hustleApiUrl = DEFAULT_HUSTLE_API_URL,
68
+ debug = false,
69
+ instanceId: explicitInstanceId,
70
+ }: HustleProviderProps) {
71
+ // Generate stable instance ID - explicit or auto-generated based on mount order
72
+ const [resolvedInstanceId] = useState(() => {
73
+ if (explicitInstanceId) {
74
+ return explicitInstanceId;
75
+ }
76
+ // Auto-generate based on mount order
77
+ const autoId = `instance-${++instanceCounter}`;
78
+ mountedAutoInstances.add(autoId);
79
+ return autoId;
80
+ });
81
+
82
+ // Track if this is an auto-generated instance for cleanup
83
+ const isAutoInstance = !explicitInstanceId;
84
+
85
+ // Dev warning for multiple auto-generated instances
86
+ useEffect(() => {
87
+ if (isAutoInstance && mountedAutoInstances.size > 1 && process.env.NODE_ENV !== 'production') {
88
+ console.warn(
89
+ `[Hustle] Multiple HustleProviders detected without explicit instanceId. ` +
90
+ `For stable settings persistence, consider adding instanceId prop:\n` +
91
+ ` <HustleProvider instanceId="my-chat-name">`
92
+ );
93
+ }
94
+
95
+ // Cleanup on unmount
96
+ return () => {
97
+ if (isAutoInstance) {
98
+ mountedAutoInstances.delete(resolvedInstanceId);
99
+ }
100
+ };
101
+ }, [isAutoInstance, resolvedInstanceId]);
102
+
103
+ // Get auth context - this provider REQUIRES EmblemAuthProvider
104
+ const { authSDK, isAuthenticated } = useEmblemAuth();
105
+
106
+ // Get plugins with instance scoping
107
+ const { enabledPlugins } = usePlugins(resolvedInstanceId);
108
+
109
+ // State
110
+ const [isLoading, setIsLoading] = useState(false);
111
+ const [error, setError] = useState<Error | null>(null);
112
+ const [models, setModels] = useState<Model[]>([]);
113
+
114
+ // Track registered plugins to avoid re-registering
115
+ const registeredPluginsRef = useRef<Set<string>>(new Set());
116
+
117
+ // Settings storage key - scoped to instance
118
+ const SETTINGS_KEY = `hustle-settings-${resolvedInstanceId}`;
119
+
120
+ // Load initial settings from localStorage
121
+ const loadSettings = () => {
122
+ if (typeof window === 'undefined') return { selectedModel: '', systemPrompt: '', skipServerPrompt: false };
123
+ try {
124
+ const stored = localStorage.getItem(SETTINGS_KEY);
125
+ if (stored) {
126
+ return JSON.parse(stored);
127
+ }
128
+ } catch {
129
+ // Ignore parse errors
130
+ }
131
+ return { selectedModel: '', systemPrompt: '', skipServerPrompt: false };
132
+ };
133
+
134
+ const initialSettings = loadSettings();
135
+
136
+ // Settings state (initialized from localStorage)
137
+ const [selectedModel, setSelectedModelState] = useState<string>(initialSettings.selectedModel);
138
+ const [systemPrompt, setSystemPromptState] = useState<string>(initialSettings.systemPrompt);
139
+ const [skipServerPrompt, setSkipServerPromptState] = useState<boolean>(initialSettings.skipServerPrompt);
140
+
141
+ // Persist settings to localStorage
142
+ const saveSettings = useCallback((settings: { selectedModel: string; systemPrompt: string; skipServerPrompt: boolean }) => {
143
+ if (typeof window === 'undefined') return;
144
+ try {
145
+ localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
146
+ } catch {
147
+ // Ignore storage errors
148
+ }
149
+ }, []);
150
+
151
+ // Wrapped setters that also persist
152
+ const setSelectedModel = useCallback((value: string) => {
153
+ setSelectedModelState(value);
154
+ saveSettings({ selectedModel: value, systemPrompt, skipServerPrompt });
155
+ }, [systemPrompt, skipServerPrompt, saveSettings]);
156
+
157
+ const setSystemPrompt = useCallback((value: string) => {
158
+ setSystemPromptState(value);
159
+ saveSettings({ selectedModel, systemPrompt: value, skipServerPrompt });
160
+ }, [selectedModel, skipServerPrompt, saveSettings]);
161
+
162
+ const setSkipServerPrompt = useCallback((value: boolean) => {
163
+ setSkipServerPromptState(value);
164
+ saveSettings({ selectedModel, systemPrompt, skipServerPrompt: value });
165
+ }, [selectedModel, systemPrompt, saveSettings]);
166
+
167
+ // Debug logger
168
+ const log = useCallback(
169
+ (message: string, ...args: unknown[]) => {
170
+ if (debug) {
171
+ console.log(`[Hustle] ${message}`, ...args);
172
+ }
173
+ },
174
+ [debug]
175
+ );
176
+
177
+ /**
178
+ * Create the Hustle client with the auth SDK
179
+ * This is the CORRECT pattern - using sdk: authSDK, NOT apiKey
180
+ */
181
+ const client = useMemo(() => {
182
+ if (!authSDK || !isAuthenticated) {
183
+ log('Client not created - auth not ready');
184
+ return null;
185
+ }
186
+
187
+ log('Creating HustleIncognitoClient with auth SDK');
188
+
189
+ try {
190
+ const hustleClient = new HustleIncognitoClient({
191
+ sdk: authSDK, // CORRECT: Pass auth SDK instance, NOT apiKey
192
+ hustleApiUrl,
193
+ debug,
194
+ });
195
+
196
+ // Subscribe to events (use any for SDK event types)
197
+ hustleClient.on('tool_start', (event: unknown) => {
198
+ log('Tool start:', event);
199
+ });
200
+
201
+ hustleClient.on('tool_end', (event: unknown) => {
202
+ log('Tool end:', event);
203
+ });
204
+
205
+ hustleClient.on('stream_end', (event: unknown) => {
206
+ log('Stream end:', event);
207
+ });
208
+
209
+ return hustleClient;
210
+ } catch (err) {
211
+ log('Failed to create client:', err);
212
+ setError(err instanceof Error ? err : new Error('Failed to create Hustle client'));
213
+ return null;
214
+ }
215
+ }, [authSDK, isAuthenticated, hustleApiUrl, debug, log]);
216
+
217
+ // Is ready when client exists
218
+ const isReady = client !== null;
219
+
220
+ /**
221
+ * Register enabled plugins with the client
222
+ */
223
+ useEffect(() => {
224
+ if (!client) return;
225
+
226
+ const registerPlugins = async () => {
227
+ // Get the set of enabled plugin names
228
+ const enabledNames = new Set(enabledPlugins.map(p => p.name));
229
+
230
+ // Unregister plugins that were disabled
231
+ for (const name of registeredPluginsRef.current) {
232
+ if (!enabledNames.has(name)) {
233
+ log('Unregistering plugin:', name);
234
+ try {
235
+ await client.unuse(name);
236
+ registeredPluginsRef.current.delete(name);
237
+ log('Plugin unregistered:', name);
238
+ } catch (err) {
239
+ log('Failed to unregister plugin:', name, err);
240
+ }
241
+ }
242
+ }
243
+
244
+ // Register new plugins
245
+ for (const plugin of enabledPlugins) {
246
+ if (!registeredPluginsRef.current.has(plugin.name)) {
247
+ log('Registering plugin:', plugin.name);
248
+ try {
249
+ // The SDK's use() method registers the plugin
250
+ if (plugin.executors || plugin.hooks) {
251
+ // Cast to SDK's expected type (our types are compatible but TS is strict)
252
+ await client.use({
253
+ name: plugin.name,
254
+ version: plugin.version,
255
+ tools: plugin.tools,
256
+ executors: plugin.executors,
257
+ hooks: plugin.hooks,
258
+ } as Parameters<typeof client.use>[0]);
259
+ registeredPluginsRef.current.add(plugin.name);
260
+ log('Plugin registered:', plugin.name);
261
+ } else {
262
+ log('Plugin has no executors/hooks, skipping registration:', plugin.name);
263
+ }
264
+ } catch (err) {
265
+ log('Failed to register plugin:', plugin.name, err);
266
+ }
267
+ }
268
+ }
269
+ };
270
+
271
+ registerPlugins();
272
+ }, [client, enabledPlugins, log]);
273
+
274
+ /**
275
+ * Load available models
276
+ */
277
+ const loadModels = useCallback(async (): Promise<Model[]> => {
278
+ if (!client) {
279
+ log('Cannot load models - client not ready');
280
+ return [];
281
+ }
282
+
283
+ log('Loading models');
284
+ setIsLoading(true);
285
+
286
+ try {
287
+ const modelList = await client.getModels();
288
+ setModels(modelList as Model[]);
289
+ log('Loaded models:', modelList.length);
290
+ return modelList as Model[];
291
+ } catch (err) {
292
+ log('Failed to load models:', err);
293
+ setError(err instanceof Error ? err : new Error('Failed to load models'));
294
+ return [];
295
+ } finally {
296
+ setIsLoading(false);
297
+ }
298
+ }, [client, log]);
299
+
300
+ /**
301
+ * Load models when client becomes ready
302
+ */
303
+ useEffect(() => {
304
+ if (client) {
305
+ loadModels();
306
+ }
307
+ }, [client, loadModels]);
308
+
309
+ /**
310
+ * Send a chat message (non-streaming)
311
+ */
312
+ const chat = useCallback(
313
+ async (options: ChatOptions): Promise<ChatResponse> => {
314
+ if (!client) {
315
+ throw new Error('Hustle client not ready. Please authenticate first.');
316
+ }
317
+
318
+ log('Chat request:', options.messages.length, 'messages');
319
+ setIsLoading(true);
320
+ setError(null);
321
+
322
+ try {
323
+ // Build the messages array, prepending system prompt if provided
324
+ const effectiveSystemPrompt = options.systemPrompt || systemPrompt;
325
+ const messagesWithSystem: ChatMessage[] = [];
326
+
327
+ if (effectiveSystemPrompt) {
328
+ messagesWithSystem.push({ role: 'system', content: effectiveSystemPrompt });
329
+ }
330
+ messagesWithSystem.push(...options.messages);
331
+
332
+ // Build the options object for the SDK
333
+ const sdkOptions: Record<string, unknown> = {
334
+ messages: messagesWithSystem,
335
+ processChunks: true,
336
+ };
337
+
338
+ if (options.model || selectedModel) {
339
+ sdkOptions.model = options.model || selectedModel;
340
+ }
341
+ if (options.overrideSystemPrompt ?? skipServerPrompt) {
342
+ sdkOptions.overrideSystemPrompt = true;
343
+ }
344
+ if (options.attachments) {
345
+ sdkOptions.attachments = options.attachments;
346
+ }
347
+
348
+ // Call the SDK - it accepts an options object
349
+ const response = await (client as unknown as { chat: (opts: Record<string, unknown>) => Promise<unknown> }).chat(sdkOptions);
350
+ log('Chat response received');
351
+ return response as ChatResponse;
352
+ } catch (err) {
353
+ log('Chat error:', err);
354
+ const error = err instanceof Error ? err : new Error('Chat request failed');
355
+ setError(error);
356
+ throw error;
357
+ } finally {
358
+ setIsLoading(false);
359
+ }
360
+ },
361
+ [client, selectedModel, systemPrompt, skipServerPrompt, log]
362
+ );
363
+
364
+ /**
365
+ * Send a chat message with streaming response
366
+ */
367
+ const chatStreamImpl = useCallback(
368
+ (options: StreamOptions): AsyncIterable<StreamChunk> => {
369
+ if (!client) {
370
+ // Return an async iterable that yields an error
371
+ return {
372
+ [Symbol.asyncIterator]: async function* () {
373
+ yield { type: 'error', value: { message: 'Hustle client not ready. Please authenticate first.' } } as StreamChunk;
374
+ },
375
+ };
376
+ }
377
+
378
+ log('Chat stream request:', options.messages.length, 'messages');
379
+ setError(null);
380
+
381
+ // Build the messages array, prepending system prompt if provided
382
+ const effectiveSystemPrompt = options.systemPrompt || systemPrompt;
383
+ const messagesWithSystem: ChatMessage[] = [];
384
+
385
+ if (effectiveSystemPrompt) {
386
+ messagesWithSystem.push({ role: 'system', content: effectiveSystemPrompt });
387
+ }
388
+ messagesWithSystem.push(...options.messages);
389
+
390
+ // Build the options object for the SDK
391
+ const sdkOptions: Record<string, unknown> = {
392
+ messages: messagesWithSystem,
393
+ processChunks: options.processChunks ?? true,
394
+ };
395
+
396
+ if (options.model || selectedModel) {
397
+ sdkOptions.model = options.model || selectedModel;
398
+ }
399
+ if (options.overrideSystemPrompt ?? skipServerPrompt) {
400
+ sdkOptions.overrideSystemPrompt = true;
401
+ }
402
+ if (options.attachments) {
403
+ sdkOptions.attachments = options.attachments;
404
+ }
405
+
406
+ // Get the stream from the client (cast through unknown for SDK compatibility)
407
+ const stream = client.chatStream(sdkOptions as unknown as Parameters<typeof client.chatStream>[0]);
408
+
409
+ // Wrap to add logging and type conversion
410
+ return {
411
+ [Symbol.asyncIterator]: async function* () {
412
+ try {
413
+ for await (const chunk of stream) {
414
+ // Type guard for chunk with type property
415
+ const typedChunk = chunk as { type?: string; value?: unknown };
416
+
417
+ if (typedChunk.type === 'text') {
418
+ const textValue = typedChunk.value as string;
419
+ log('Stream text chunk:', textValue?.substring(0, 50));
420
+ yield { type: 'text', value: textValue } as StreamChunk;
421
+ } else if (typedChunk.type === 'tool_call') {
422
+ log('Stream tool call:', typedChunk.value);
423
+ yield { type: 'tool_call', value: typedChunk.value } as StreamChunk;
424
+ } else if (typedChunk.type === 'tool_result') {
425
+ log('Stream tool result');
426
+ yield { type: 'tool_result', value: typedChunk.value } as StreamChunk;
427
+ } else if (typedChunk.type === 'error') {
428
+ const errorValue = typedChunk.value as { message?: string };
429
+ log('Stream error:', errorValue);
430
+ setError(new Error(errorValue?.message || 'Stream error'));
431
+ yield { type: 'error', value: { message: errorValue?.message || 'Stream error' } } as StreamChunk;
432
+ } else {
433
+ // Pass through unknown chunk types
434
+ yield chunk as StreamChunk;
435
+ }
436
+ }
437
+ } catch (err) {
438
+ log('Stream error:', err);
439
+ const error = err instanceof Error ? err : new Error('Stream failed');
440
+ setError(error);
441
+ yield { type: 'error', value: { message: error.message } } as StreamChunk;
442
+ }
443
+ },
444
+ };
445
+ },
446
+ [client, selectedModel, systemPrompt, skipServerPrompt, log]
447
+ );
448
+
449
+ /**
450
+ * Upload a file
451
+ */
452
+ const uploadFile = useCallback(
453
+ async (file: File): Promise<Attachment> => {
454
+ if (!client) {
455
+ throw new Error('Hustle client not ready. Please authenticate first.');
456
+ }
457
+
458
+ log('Uploading file:', file.name);
459
+ setIsLoading(true);
460
+
461
+ try {
462
+ const attachment = await client.uploadFile(file);
463
+ log('File uploaded:', attachment);
464
+ return attachment as Attachment;
465
+ } catch (err) {
466
+ log('Upload error:', err);
467
+ const error = err instanceof Error ? err : new Error('File upload failed');
468
+ setError(error);
469
+ throw error;
470
+ } finally {
471
+ setIsLoading(false);
472
+ }
473
+ },
474
+ [client, log]
475
+ );
476
+
477
+ // Context value
478
+ const value: HustleContextValue = {
479
+ // Instance ID for scoped storage
480
+ instanceId: resolvedInstanceId,
481
+
482
+ // State
483
+ isReady,
484
+ isLoading,
485
+ error,
486
+ models,
487
+
488
+ // Client (for advanced use)
489
+ client,
490
+
491
+ // Chat methods
492
+ chat,
493
+ chatStream: chatStreamImpl,
494
+
495
+ // File upload
496
+ uploadFile,
497
+
498
+ // Data fetching
499
+ loadModels,
500
+
501
+ // Settings
502
+ selectedModel,
503
+ setSelectedModel,
504
+ systemPrompt,
505
+ setSystemPrompt,
506
+ skipServerPrompt,
507
+ setSkipServerPrompt,
508
+ };
509
+
510
+ return (
511
+ <HustleContext.Provider value={value}>
512
+ {children}
513
+ </HustleContext.Provider>
514
+ );
515
+ }
516
+
517
+ /**
518
+ * Hook to access Hustle context
519
+ * Must be used within HustleProvider (which must be within EmblemAuthProvider)
520
+ *
521
+ * @example
522
+ * ```tsx
523
+ * function ChatComponent() {
524
+ * const { isReady, chat, chatStream } = useHustle();
525
+ *
526
+ * if (!isReady) {
527
+ * return <div>Please connect to start chatting</div>;
528
+ * }
529
+ *
530
+ * // Use chat or chatStream...
531
+ * }
532
+ * ```
533
+ */
534
+ export function useHustle(): HustleContextValue {
535
+ const context = useContext(HustleContext);
536
+ if (context === undefined) {
537
+ throw new Error('useHustle must be used within a HustleProvider (which requires EmblemAuthProvider)');
538
+ }
539
+ return context;
540
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Provider exports
3
+ */
4
+
5
+ export { EmblemAuthProvider, useEmblemAuth, resetAuthSDK } from './EmblemAuthProvider';
6
+ export { HustleProvider, useHustle } from './HustleProvider';
@@ -0,0 +1,2 @@
1
+ export { tokens, presets, keyframes, animations, createStyle, getTokenValue, cssVariables, defaultTokens } from './tokens';
2
+ export type { Tokens } from './tokens';