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,431 @@
1
+ 'use client';
2
+
3
+ import React, { useEffect, useState, useCallback, useRef } from 'react';
4
+ import { marked } from 'marked';
5
+ import hljs from 'highlight.js/lib/core';
6
+ // Import only common languages for AI chat
7
+ import javascript from 'highlight.js/lib/languages/javascript';
8
+ import typescript from 'highlight.js/lib/languages/typescript';
9
+ import python from 'highlight.js/lib/languages/python';
10
+ import json from 'highlight.js/lib/languages/json';
11
+ import bash from 'highlight.js/lib/languages/bash';
12
+ import shell from 'highlight.js/lib/languages/shell';
13
+ import css from 'highlight.js/lib/languages/css';
14
+ import xml from 'highlight.js/lib/languages/xml';
15
+ import markdown from 'highlight.js/lib/languages/markdown';
16
+ import sql from 'highlight.js/lib/languages/sql';
17
+ import yaml from 'highlight.js/lib/languages/yaml';
18
+ import rust from 'highlight.js/lib/languages/rust';
19
+ import go from 'highlight.js/lib/languages/go';
20
+ import java from 'highlight.js/lib/languages/java';
21
+ import cpp from 'highlight.js/lib/languages/cpp';
22
+ import csharp from 'highlight.js/lib/languages/csharp';
23
+ import php from 'highlight.js/lib/languages/php';
24
+ import ruby from 'highlight.js/lib/languages/ruby';
25
+ import swift from 'highlight.js/lib/languages/swift';
26
+ import kotlin from 'highlight.js/lib/languages/kotlin';
27
+ import { tokens } from '../styles';
28
+
29
+ // Register languages
30
+ hljs.registerLanguage('javascript', javascript);
31
+ hljs.registerLanguage('js', javascript);
32
+ hljs.registerLanguage('typescript', typescript);
33
+ hljs.registerLanguage('ts', typescript);
34
+ hljs.registerLanguage('python', python);
35
+ hljs.registerLanguage('py', python);
36
+ hljs.registerLanguage('json', json);
37
+ hljs.registerLanguage('bash', bash);
38
+ hljs.registerLanguage('sh', bash);
39
+ hljs.registerLanguage('shell', shell);
40
+ hljs.registerLanguage('css', css);
41
+ hljs.registerLanguage('xml', xml);
42
+ hljs.registerLanguage('html', xml);
43
+ hljs.registerLanguage('markdown', markdown);
44
+ hljs.registerLanguage('md', markdown);
45
+ hljs.registerLanguage('sql', sql);
46
+ hljs.registerLanguage('yaml', yaml);
47
+ hljs.registerLanguage('yml', yaml);
48
+ hljs.registerLanguage('rust', rust);
49
+ hljs.registerLanguage('go', go);
50
+ hljs.registerLanguage('java', java);
51
+ hljs.registerLanguage('cpp', cpp);
52
+ hljs.registerLanguage('c', cpp);
53
+ hljs.registerLanguage('csharp', csharp);
54
+ hljs.registerLanguage('cs', csharp);
55
+ hljs.registerLanguage('php', php);
56
+ hljs.registerLanguage('ruby', ruby);
57
+ hljs.registerLanguage('rb', ruby);
58
+ hljs.registerLanguage('swift', swift);
59
+ hljs.registerLanguage('kotlin', kotlin);
60
+ hljs.registerLanguage('kt', kotlin);
61
+
62
+ /**
63
+ * MarkdownContent - Renders markdown using marked + highlight.js
64
+ *
65
+ * Uses marked for markdown parsing (lightweight, reliable)
66
+ * Uses highlight.js for code block syntax highlighting
67
+ * Adds copy and "Open in Emblem AI" buttons to code blocks
68
+ */
69
+ export interface MarkdownContentProps {
70
+ content: string;
71
+ className?: string;
72
+ }
73
+
74
+ /**
75
+ * Container styles
76
+ */
77
+ const containerStyle: React.CSSProperties = {
78
+ fontFamily: tokens.typography.fontFamily,
79
+ fontSize: tokens.typography.fontSizeMd,
80
+ lineHeight: 1.5,
81
+ color: 'inherit',
82
+ wordBreak: 'break-word',
83
+ };
84
+
85
+ /**
86
+ * Scoped CSS for markdown content + highlight.js dark theme
87
+ */
88
+ const scopedStyles = `
89
+ .hljs-md p {
90
+ margin: 0 0 0.5em 0;
91
+ }
92
+ .hljs-md p:last-child {
93
+ margin-bottom: 0;
94
+ }
95
+ .hljs-md ul,
96
+ .hljs-md ol {
97
+ margin: 0.25em 0 0.5em 0;
98
+ padding-left: 1.5em;
99
+ }
100
+ .hljs-md li {
101
+ margin: 0.1em 0;
102
+ line-height: 1.4;
103
+ }
104
+ .hljs-md li > p {
105
+ margin: 0;
106
+ display: inline;
107
+ }
108
+ .hljs-md li > ul,
109
+ .hljs-md li > ol {
110
+ margin: 0.1em 0;
111
+ }
112
+ .hljs-md .code-block-wrapper {
113
+ position: relative;
114
+ margin: 0.5em 0;
115
+ }
116
+ .hljs-md .code-block-wrapper pre {
117
+ position: relative;
118
+ margin: 0;
119
+ padding: 0.75em 1em;
120
+ border-radius: 6px;
121
+ background: #1e1e1e;
122
+ overflow-x: auto;
123
+ }
124
+ .hljs-md .code-block-toolbar {
125
+ position: absolute;
126
+ top: 6px;
127
+ right: 6px;
128
+ display: flex;
129
+ gap: 4px;
130
+ z-index: 10;
131
+ }
132
+ .hljs-md .code-block-btn {
133
+ display: flex;
134
+ align-items: center;
135
+ justify-content: center;
136
+ width: 26px;
137
+ height: 26px;
138
+ padding: 0;
139
+ background: #2d2d2d;
140
+ border: 1px solid #404040;
141
+ border-radius: 4px;
142
+ color: #888;
143
+ cursor: pointer;
144
+ transition: all 0.15s ease;
145
+ opacity: 0.7;
146
+ }
147
+ .hljs-md .code-block-wrapper:hover .code-block-btn {
148
+ opacity: 1;
149
+ }
150
+ .hljs-md .code-block-btn:hover {
151
+ background: #3d3d3d;
152
+ border-color: #555;
153
+ color: #ccc;
154
+ }
155
+ .hljs-md .code-block-btn.copied {
156
+ color: ${tokens.colors.accentSuccess};
157
+ }
158
+ .hljs-md code {
159
+ font-family: ${tokens.typography.fontFamilyMono};
160
+ font-size: 0.9em;
161
+ }
162
+ .hljs-md :not(pre) > code {
163
+ padding: 0.15em 0.4em;
164
+ border-radius: 4px;
165
+ background: rgba(255, 255, 255, 0.1);
166
+ }
167
+ .hljs-md pre code {
168
+ padding: 0;
169
+ background: transparent;
170
+ font-size: 0.875em;
171
+ line-height: 1.5;
172
+ }
173
+ .hljs-md h1, .hljs-md h2, .hljs-md h3, .hljs-md h4 {
174
+ margin: 0.5em 0 0.25em 0;
175
+ font-weight: 600;
176
+ line-height: 1.3;
177
+ }
178
+ .hljs-md h1:first-child, .hljs-md h2:first-child, .hljs-md h3:first-child {
179
+ margin-top: 0;
180
+ }
181
+ .hljs-md h1 { font-size: 1.5em; }
182
+ .hljs-md h2 { font-size: 1.25em; }
183
+ .hljs-md h3 { font-size: 1.1em; }
184
+ .hljs-md h4 { font-size: 1em; }
185
+ .hljs-md blockquote {
186
+ margin: 0.5em 0;
187
+ padding: 0.5em 0 0.5em 1em;
188
+ border-left: 3px solid ${tokens.colors.borderSecondary};
189
+ color: ${tokens.colors.textSecondary};
190
+ }
191
+ .hljs-md a {
192
+ color: ${tokens.colors.accentPrimary};
193
+ text-decoration: underline;
194
+ }
195
+ .hljs-md hr {
196
+ margin: 1em 0;
197
+ border: none;
198
+ border-top: 1px solid ${tokens.colors.borderSecondary};
199
+ }
200
+ .hljs-md table {
201
+ width: 100%;
202
+ margin: 0.5em 0;
203
+ border-collapse: collapse;
204
+ }
205
+ .hljs-md th, .hljs-md td {
206
+ padding: 0.5em;
207
+ border: 1px solid ${tokens.colors.borderPrimary};
208
+ text-align: left;
209
+ }
210
+ .hljs-md th {
211
+ font-weight: 600;
212
+ background: rgba(255, 255, 255, 0.05);
213
+ }
214
+
215
+ /* highlight.js dark theme (VS Code Dark+ inspired) */
216
+ .hljs {
217
+ color: #d4d4d4;
218
+ background: #1e1e1e;
219
+ }
220
+ .hljs-keyword,
221
+ .hljs-selector-tag,
222
+ .hljs-title,
223
+ .hljs-section,
224
+ .hljs-doctag,
225
+ .hljs-name,
226
+ .hljs-strong {
227
+ color: #569cd6;
228
+ font-weight: normal;
229
+ }
230
+ .hljs-built_in,
231
+ .hljs-literal,
232
+ .hljs-type,
233
+ .hljs-params,
234
+ .hljs-meta,
235
+ .hljs-link {
236
+ color: #4ec9b0;
237
+ }
238
+ .hljs-string,
239
+ .hljs-symbol,
240
+ .hljs-bullet,
241
+ .hljs-addition {
242
+ color: #ce9178;
243
+ }
244
+ .hljs-number {
245
+ color: #b5cea8;
246
+ }
247
+ .hljs-comment,
248
+ .hljs-quote,
249
+ .hljs-deletion {
250
+ color: #6a9955;
251
+ }
252
+ .hljs-variable,
253
+ .hljs-template-variable,
254
+ .hljs-attr {
255
+ color: #9cdcfe;
256
+ }
257
+ .hljs-regexp,
258
+ .hljs-selector-id,
259
+ .hljs-selector-class {
260
+ color: #d7ba7d;
261
+ }
262
+ .hljs-emphasis {
263
+ font-style: italic;
264
+ }
265
+ `;
266
+
267
+ // Configure marked with highlight.js
268
+ marked.use({
269
+ gfm: true,
270
+ breaks: false,
271
+ });
272
+
273
+ /**
274
+ * Highlight code using highlight.js
275
+ */
276
+ function highlightCode(code: string, lang: string): string {
277
+ if (lang && hljs.getLanguage(lang)) {
278
+ try {
279
+ return hljs.highlight(code, { language: lang }).value;
280
+ } catch {
281
+ // Fall through to auto-detect
282
+ }
283
+ }
284
+
285
+ // Auto-detect language
286
+ try {
287
+ return hljs.highlightAuto(code).value;
288
+ } catch {
289
+ // Fallback to escaped plain text
290
+ return code
291
+ .replace(/&/g, '&')
292
+ .replace(/</g, '&lt;')
293
+ .replace(/>/g, '&gt;');
294
+ }
295
+ }
296
+
297
+ /**
298
+ * Generate unique ID for code blocks
299
+ */
300
+ let codeBlockCounter = 0;
301
+ function generateCodeBlockId(): string {
302
+ return `code-block-${++codeBlockCounter}`;
303
+ }
304
+
305
+ /**
306
+ * Process markdown and highlight code blocks
307
+ * Returns HTML with data attributes for code block content
308
+ */
309
+ function renderMarkdown(content: string): string {
310
+ // Parse markdown to HTML
311
+ const html = marked.parse(content, { async: false }) as string;
312
+
313
+ // Find and highlight code blocks with language
314
+ const codeBlockRegex = /<pre><code class="language-(\w+)">([\s\S]*?)<\/code><\/pre>/g;
315
+ let result = html.replace(codeBlockRegex, (_, lang, code) => {
316
+ const decoded = code
317
+ .replace(/&lt;/g, '<')
318
+ .replace(/&gt;/g, '>')
319
+ .replace(/&amp;/g, '&')
320
+ .replace(/&quot;/g, '"');
321
+ const highlighted = highlightCode(decoded, lang);
322
+ const id = generateCodeBlockId();
323
+ const encodedCode = encodeURIComponent(decoded);
324
+ return `<div class="code-block-wrapper" data-code-id="${id}" data-code="${encodedCode}"><pre><code class="hljs language-${lang}">${highlighted}</code></pre><div class="code-block-toolbar"><button class="code-block-btn" data-action="copy" data-code-id="${id}" title="Copy code"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg></button><button class="code-block-btn" data-action="emblem" data-code-id="${id}" title="Open in Emblem AI"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline></svg></button></div></div>`;
325
+ });
326
+
327
+ // Also handle code blocks without language
328
+ const plainCodeRegex = /<pre><code>([\s\S]*?)<\/code><\/pre>/g;
329
+ result = result.replace(plainCodeRegex, (_, code) => {
330
+ const decoded = code
331
+ .replace(/&lt;/g, '<')
332
+ .replace(/&gt;/g, '>')
333
+ .replace(/&amp;/g, '&')
334
+ .replace(/&quot;/g, '"');
335
+ const highlighted = highlightCode(decoded, '');
336
+ const id = generateCodeBlockId();
337
+ const encodedCode = encodeURIComponent(decoded);
338
+ return `<div class="code-block-wrapper" data-code-id="${id}" data-code="${encodedCode}"><pre><code class="hljs">${highlighted}</code></pre><div class="code-block-toolbar"><button class="code-block-btn" data-action="copy" data-code-id="${id}" title="Copy code"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg></button><button class="code-block-btn" data-action="emblem" data-code-id="${id}" title="Open in Emblem AI"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline></svg></button></div></div>`;
339
+ });
340
+
341
+ return result;
342
+ }
343
+
344
+ export function MarkdownContent({ content, className }: MarkdownContentProps) {
345
+ const [rendered, setRendered] = useState<string>('');
346
+ const containerRef = useRef<HTMLDivElement>(null);
347
+
348
+ useEffect(() => {
349
+ try {
350
+ const html = renderMarkdown(content);
351
+ setRendered(html);
352
+ } catch (err) {
353
+ console.error('[MarkdownContent] Render error:', err);
354
+ // Fallback to escaped plain text
355
+ setRendered(
356
+ content
357
+ .replace(/&/g, '&amp;')
358
+ .replace(/</g, '&lt;')
359
+ .replace(/>/g, '&gt;')
360
+ .replace(/\n/g, '<br>')
361
+ );
362
+ }
363
+ }, [content]);
364
+
365
+ // Attach event handlers after render
366
+ useEffect(() => {
367
+ const container = containerRef.current;
368
+ if (!container) return;
369
+
370
+ const handleClick = async (e: MouseEvent) => {
371
+ const target = e.target as HTMLElement;
372
+ const button = target.closest('[data-action]') as HTMLElement;
373
+ if (!button) return;
374
+
375
+ const action = button.dataset.action;
376
+ const codeId = button.dataset.codeId;
377
+ if (!codeId) return;
378
+
379
+ const wrapper = container.querySelector(`[data-code-id="${codeId}"]`) as HTMLElement;
380
+ if (!wrapper) return;
381
+
382
+ const encodedCode = wrapper.dataset.code;
383
+ if (!encodedCode) return;
384
+
385
+ const code = decodeURIComponent(encodedCode);
386
+
387
+ if (action === 'copy') {
388
+ e.preventDefault();
389
+ try {
390
+ await navigator.clipboard.writeText(code);
391
+ button.classList.add('copied');
392
+ // Change icon to checkmark temporarily
393
+ button.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
394
+ <polyline points="20 6 9 17 4 12"></polyline>
395
+ </svg>`;
396
+ setTimeout(() => {
397
+ button.classList.remove('copied');
398
+ button.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
399
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
400
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
401
+ </svg>`;
402
+ }, 2000);
403
+ } catch (err) {
404
+ console.error('Failed to copy:', err);
405
+ }
406
+ } else if (action === 'emblem') {
407
+ e.preventDefault();
408
+ // Open Emblem AI with code in URL (no auto-submit)
409
+ const url = `https://build.emblemvault.ai/?q=${encodeURIComponent(code)}`;
410
+ window.open(url, '_blank', 'noopener,noreferrer');
411
+ }
412
+ };
413
+
414
+ container.addEventListener('click', handleClick);
415
+ return () => container.removeEventListener('click', handleClick);
416
+ }, [rendered]);
417
+
418
+ return (
419
+ <>
420
+ <style>{scopedStyles}</style>
421
+ <div
422
+ ref={containerRef}
423
+ style={containerStyle}
424
+ className={`hljs-md ${className || ''}`}
425
+ dangerouslySetInnerHTML={{ __html: rendered }}
426
+ />
427
+ </>
428
+ );
429
+ }
430
+
431
+ export default MarkdownContent;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Component exports
3
+ */
4
+
5
+ export { ConnectButton } from './ConnectButton';
6
+ export type { ConnectButtonProps } from './ConnectButton';
7
+
8
+ export { AuthStatus } from './AuthStatus';
9
+ export type { AuthStatusProps } from './AuthStatus';
10
+
11
+ export { HustleChat } from './HustleChat';
12
+ export type { HustleChatProps } from './HustleChat';
13
+
14
+ export { MarkdownContent } from './MarkdownContent';
15
+ export type { MarkdownContentProps } from './MarkdownContent';
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Hook exports
3
+ */
4
+
5
+ export { useEmblemAuth, EmblemAuthProvider, resetAuthSDK } from './useEmblemAuth';
6
+ export { useHustle, HustleProvider } from './useHustle';
7
+ export { usePlugins } from './usePlugins';
8
+
9
+ // Re-export types for convenience
10
+ export type {
11
+ AuthSession,
12
+ AuthUser,
13
+ VaultInfo,
14
+ EmblemAuthConfig,
15
+ EmblemAuthContextValue,
16
+ EmblemAuthProviderProps,
17
+ ChatMessage,
18
+ ToolCall,
19
+ ToolResult,
20
+ Attachment,
21
+ Model,
22
+ ChatOptions,
23
+ StreamOptions,
24
+ StreamChunk,
25
+ ChatResponse,
26
+ ToolStartEvent,
27
+ ToolEndEvent,
28
+ StreamEndEvent,
29
+ HustleConfig,
30
+ HustleContextValue,
31
+ HustleProviderProps,
32
+ // Plugin types
33
+ HustlePlugin,
34
+ StoredPlugin,
35
+ HydratedPlugin,
36
+ ClientToolDefinition,
37
+ ToolExecutor,
38
+ } from '../types';
39
+
40
+ export type { UsePluginsReturn } from './usePlugins';
@@ -0,0 +1,27 @@
1
+ /**
2
+ * useEmblemAuth hook
3
+ *
4
+ * Re-exported from EmblemAuthProvider for convenience.
5
+ * Can be imported from either location:
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * import { useEmblemAuth } from '@/hooks/useEmblemAuth';
10
+ * // or
11
+ * import { useEmblemAuth } from '@/providers/EmblemAuthProvider';
12
+ * ```
13
+ */
14
+ export { useEmblemAuth } from '../providers/EmblemAuthProvider';
15
+
16
+ // Also export the provider and reset function for completeness
17
+ export { EmblemAuthProvider, resetAuthSDK } from '../providers/EmblemAuthProvider';
18
+
19
+ // Re-export auth types for convenience
20
+ export type {
21
+ AuthSession,
22
+ AuthUser,
23
+ VaultInfo,
24
+ EmblemAuthConfig,
25
+ EmblemAuthContextValue,
26
+ EmblemAuthProviderProps,
27
+ } from '../types';
@@ -0,0 +1,36 @@
1
+ /**
2
+ * useHustle hook
3
+ *
4
+ * Re-exported from HustleProvider for convenience.
5
+ * Can be imported from either location:
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * import { useHustle } from '@/hooks/useHustle';
10
+ * // or
11
+ * import { useHustle } from '@/providers/HustleProvider';
12
+ * ```
13
+ */
14
+ export { useHustle } from '../providers/HustleProvider';
15
+
16
+ // Also export the provider for completeness
17
+ export { HustleProvider } from '../providers/HustleProvider';
18
+
19
+ // Re-export hustle types for convenience
20
+ export type {
21
+ ChatMessage,
22
+ ToolCall,
23
+ ToolResult,
24
+ Attachment,
25
+ Model,
26
+ ChatOptions,
27
+ StreamOptions,
28
+ StreamChunk,
29
+ ChatResponse,
30
+ ToolStartEvent,
31
+ ToolEndEvent,
32
+ StreamEndEvent,
33
+ HustleConfig,
34
+ HustleContextValue,
35
+ HustleProviderProps,
36
+ } from '../types';
@@ -0,0 +1,135 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * usePlugins Hook
5
+ *
6
+ * Manages plugin state with localStorage persistence and cross-tab sync.
7
+ * Supports instance-scoped storage for multiple HustleProviders.
8
+ */
9
+
10
+ import { useState, useEffect, useCallback } from 'react';
11
+ import { pluginRegistry, hydratePlugin } from '../utils/pluginRegistry';
12
+ import type { StoredPlugin, HustlePlugin, HydratedPlugin } from '../types';
13
+
14
+ // Re-export hydratePlugin for convenience
15
+ export { hydratePlugin };
16
+
17
+ /**
18
+ * Get the storage key for a given instance
19
+ */
20
+ function getStorageKey(instanceId: string): string {
21
+ return `hustle-plugins-${instanceId}`;
22
+ }
23
+
24
+ /**
25
+ * Return type for usePlugins hook
26
+ */
27
+ export interface UsePluginsReturn {
28
+ /** All registered plugins (with enabled state) */
29
+ plugins: StoredPlugin[];
30
+ /** Only enabled plugins (hydrated with executors) */
31
+ enabledPlugins: HydratedPlugin[];
32
+ /** Register a new plugin */
33
+ registerPlugin: (plugin: HustlePlugin) => void;
34
+ /** Unregister a plugin by name */
35
+ unregisterPlugin: (name: string) => void;
36
+ /** Enable a plugin */
37
+ enablePlugin: (name: string) => void;
38
+ /** Disable a plugin */
39
+ disablePlugin: (name: string) => void;
40
+ /** Check if a plugin is registered */
41
+ isRegistered: (name: string) => boolean;
42
+ /** Check if a plugin is enabled */
43
+ isEnabled: (name: string) => boolean;
44
+ }
45
+
46
+ /**
47
+ * Hook for managing plugins
48
+ *
49
+ * @param instanceId - Optional instance ID for scoping plugin storage (defaults to 'default')
50
+ *
51
+ * @example
52
+ * ```tsx
53
+ * const { plugins, registerPlugin, enabledPlugins } = usePlugins();
54
+ *
55
+ * // Install a plugin
56
+ * registerPlugin(myPlugin);
57
+ *
58
+ * // Check enabled plugins
59
+ * console.log('Active tools:', enabledPlugins.flatMap(p => p.tools));
60
+ * ```
61
+ */
62
+ export function usePlugins(instanceId: string = 'default'): UsePluginsReturn {
63
+ const [plugins, setPlugins] = useState<StoredPlugin[]>([]);
64
+
65
+ // Load initial plugins and subscribe to changes
66
+ useEffect(() => {
67
+ // Load initial state
68
+ setPlugins(pluginRegistry.loadFromStorage(instanceId));
69
+
70
+ // Subscribe to registry changes for this instance
71
+ const unsubscribe = pluginRegistry.onChange(setPlugins, instanceId);
72
+
73
+ // Listen to storage events for cross-tab sync
74
+ const storageKey = getStorageKey(instanceId);
75
+ const handleStorage = (e: StorageEvent) => {
76
+ if (e.key === storageKey) {
77
+ setPlugins(pluginRegistry.loadFromStorage(instanceId));
78
+ }
79
+ };
80
+ window.addEventListener('storage', handleStorage);
81
+
82
+ return () => {
83
+ unsubscribe();
84
+ window.removeEventListener('storage', handleStorage);
85
+ };
86
+ }, [instanceId]);
87
+
88
+ // Register a new plugin
89
+ const registerPlugin = useCallback((plugin: HustlePlugin) => {
90
+ pluginRegistry.register(plugin, true, instanceId);
91
+ }, [instanceId]);
92
+
93
+ // Unregister a plugin
94
+ const unregisterPlugin = useCallback((name: string) => {
95
+ pluginRegistry.unregister(name, instanceId);
96
+ }, [instanceId]);
97
+
98
+ // Enable a plugin
99
+ const enablePlugin = useCallback((name: string) => {
100
+ pluginRegistry.setEnabled(name, true, instanceId);
101
+ }, [instanceId]);
102
+
103
+ // Disable a plugin
104
+ const disablePlugin = useCallback((name: string) => {
105
+ pluginRegistry.setEnabled(name, false, instanceId);
106
+ }, [instanceId]);
107
+
108
+ // Check if plugin is registered
109
+ const isRegistered = useCallback(
110
+ (name: string) => plugins.some(p => p.name === name),
111
+ [plugins]
112
+ );
113
+
114
+ // Check if plugin is enabled
115
+ const isEnabled = useCallback(
116
+ (name: string) => plugins.some(p => p.name === name && p.enabled),
117
+ [plugins]
118
+ );
119
+
120
+ // Get enabled plugins with hydrated executors
121
+ const enabledPlugins = plugins.filter(p => p.enabled).map(hydratePlugin);
122
+
123
+ return {
124
+ plugins,
125
+ enabledPlugins,
126
+ registerPlugin,
127
+ unregisterPlugin,
128
+ enablePlugin,
129
+ disablePlugin,
130
+ isRegistered,
131
+ isEnabled,
132
+ };
133
+ }
134
+
135
+ export default usePlugins;