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