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,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Registry
|
|
3
|
+
*
|
|
4
|
+
* Manages plugin storage and state in localStorage.
|
|
5
|
+
*
|
|
6
|
+
* Storage model:
|
|
7
|
+
* - Installed plugins are GLOBAL (hustle-plugins) - install once, available everywhere
|
|
8
|
+
* - Enabled/disabled state is INSTANCE-SCOPED (hustle-plugin-state-{instanceId})
|
|
9
|
+
*
|
|
10
|
+
* Executor functions are serialized as strings (executorCode) and
|
|
11
|
+
* reconstituted at runtime via new Function().
|
|
12
|
+
*
|
|
13
|
+
* SECURITY TODO: Add signature verification before executing stored code.
|
|
14
|
+
* Plugins should be signed by trusted publishers and verified before
|
|
15
|
+
* any eval/Function execution occurs.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type {
|
|
19
|
+
StoredPlugin,
|
|
20
|
+
HustlePlugin,
|
|
21
|
+
HydratedPlugin,
|
|
22
|
+
ToolExecutor,
|
|
23
|
+
PluginHooks,
|
|
24
|
+
SerializedToolDefinition,
|
|
25
|
+
SerializedHooks,
|
|
26
|
+
} from '../types';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Storage keys:
|
|
30
|
+
* - PLUGINS_KEY: Global list of installed plugins (not instance-scoped)
|
|
31
|
+
* - getEnabledStateKey: Per-instance enabled/disabled states
|
|
32
|
+
*/
|
|
33
|
+
const PLUGINS_KEY = 'hustle-plugins';
|
|
34
|
+
|
|
35
|
+
function getEnabledStateKey(instanceId: string): string {
|
|
36
|
+
return `hustle-plugin-state-${instanceId}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type PluginChangeCallback = (plugins: StoredPlugin[]) => void;
|
|
40
|
+
|
|
41
|
+
/** Stored enabled state per instance */
|
|
42
|
+
type EnabledState = Record<string, boolean>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Serialize a function to a string for storage
|
|
46
|
+
*/
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
48
|
+
function serializeFunction(fn: (...args: any[]) => any): string {
|
|
49
|
+
return fn.toString();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Deserialize a function string back to executable function
|
|
54
|
+
*
|
|
55
|
+
* FIXME: Add signature verification before execution
|
|
56
|
+
* This is a security-sensitive operation that executes stored code.
|
|
57
|
+
*/
|
|
58
|
+
function deserializeExecutor(code: string): ToolExecutor {
|
|
59
|
+
// Extract function body - handles arrow functions and regular functions
|
|
60
|
+
// The stored code is the full function: "(args) => { ... }" or "async (args) => { ... }"
|
|
61
|
+
// We wrap it in parentheses and eval to get the function reference
|
|
62
|
+
try {
|
|
63
|
+
// eslint-disable-next-line no-eval
|
|
64
|
+
return eval(`(${code})`) as ToolExecutor;
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.error('[Hustle] Failed to deserialize executor:', err);
|
|
67
|
+
// Return a no-op executor that reports the error
|
|
68
|
+
return async () => ({ error: 'Failed to deserialize executor', code });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Deserialize a hook function string
|
|
74
|
+
*
|
|
75
|
+
* FIXME: Add signature verification before execution
|
|
76
|
+
*/
|
|
77
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
78
|
+
function deserializeHook<T extends (...args: any[]) => any>(code: string): T {
|
|
79
|
+
try {
|
|
80
|
+
// eslint-disable-next-line no-eval
|
|
81
|
+
return eval(`(${code})`) as T;
|
|
82
|
+
} catch (err) {
|
|
83
|
+
console.error('[Hustle] Failed to deserialize hook:', err);
|
|
84
|
+
return (() => {}) as T;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Serialize a plugin's executors to executorCode strings
|
|
90
|
+
*/
|
|
91
|
+
function serializePluginTools(
|
|
92
|
+
tools: HustlePlugin['tools'],
|
|
93
|
+
executors: HustlePlugin['executors']
|
|
94
|
+
): SerializedToolDefinition[] {
|
|
95
|
+
if (!tools) return [];
|
|
96
|
+
|
|
97
|
+
return tools.map((tool) => ({
|
|
98
|
+
...tool,
|
|
99
|
+
executorCode: executors?.[tool.name]
|
|
100
|
+
? serializeFunction(executors[tool.name])
|
|
101
|
+
: undefined,
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Serialize plugin hooks to code strings
|
|
107
|
+
*/
|
|
108
|
+
function serializeHooks(hooks: PluginHooks | undefined): SerializedHooks | undefined {
|
|
109
|
+
if (!hooks) return undefined;
|
|
110
|
+
|
|
111
|
+
const serialized: SerializedHooks = {};
|
|
112
|
+
|
|
113
|
+
if (hooks.onRegister) {
|
|
114
|
+
serialized.onRegisterCode = serializeFunction(hooks.onRegister);
|
|
115
|
+
}
|
|
116
|
+
if (hooks.beforeRequest) {
|
|
117
|
+
serialized.beforeRequestCode = serializeFunction(hooks.beforeRequest);
|
|
118
|
+
}
|
|
119
|
+
if (hooks.afterResponse) {
|
|
120
|
+
serialized.afterResponseCode = serializeFunction(hooks.afterResponse);
|
|
121
|
+
}
|
|
122
|
+
if (hooks.onError) {
|
|
123
|
+
serialized.onErrorCode = serializeFunction(hooks.onError);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return Object.keys(serialized).length > 0 ? serialized : undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Hydrate a stored plugin - reconstitute executors from executorCode
|
|
131
|
+
*
|
|
132
|
+
* FIXME: Add signature verification before execution
|
|
133
|
+
*/
|
|
134
|
+
export function hydratePlugin(stored: StoredPlugin): HydratedPlugin {
|
|
135
|
+
// Reconstitute executors from executorCode strings
|
|
136
|
+
const executors: Record<string, ToolExecutor> = {};
|
|
137
|
+
|
|
138
|
+
if (stored.tools) {
|
|
139
|
+
for (const tool of stored.tools) {
|
|
140
|
+
if (tool.executorCode) {
|
|
141
|
+
executors[tool.name] = deserializeExecutor(tool.executorCode);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Reconstitute hooks from hooksCode strings
|
|
147
|
+
let hooks: PluginHooks | undefined;
|
|
148
|
+
|
|
149
|
+
if (stored.hooksCode) {
|
|
150
|
+
hooks = {};
|
|
151
|
+
if (stored.hooksCode.onRegisterCode) {
|
|
152
|
+
hooks.onRegister = deserializeHook(stored.hooksCode.onRegisterCode);
|
|
153
|
+
}
|
|
154
|
+
if (stored.hooksCode.beforeRequestCode) {
|
|
155
|
+
hooks.beforeRequest = deserializeHook(stored.hooksCode.beforeRequestCode);
|
|
156
|
+
}
|
|
157
|
+
if (stored.hooksCode.afterResponseCode) {
|
|
158
|
+
hooks.afterResponse = deserializeHook(stored.hooksCode.afterResponseCode);
|
|
159
|
+
}
|
|
160
|
+
if (stored.hooksCode.onErrorCode) {
|
|
161
|
+
hooks.onError = deserializeHook(stored.hooksCode.onErrorCode);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
...stored,
|
|
167
|
+
executors: Object.keys(executors).length > 0 ? executors : undefined,
|
|
168
|
+
hooks,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Plugin Registry class
|
|
174
|
+
*
|
|
175
|
+
* Manages plugin persistence with:
|
|
176
|
+
* - Global plugin installations (with serialized executorCode)
|
|
177
|
+
* - Instance-scoped enabled/disabled state
|
|
178
|
+
*/
|
|
179
|
+
class PluginRegistry {
|
|
180
|
+
private listeners: Map<string, Set<PluginChangeCallback>> = new Map();
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get listeners for a specific instance
|
|
184
|
+
*/
|
|
185
|
+
private getListeners(instanceId: string): Set<PluginChangeCallback> {
|
|
186
|
+
if (!this.listeners.has(instanceId)) {
|
|
187
|
+
this.listeners.set(instanceId, new Set());
|
|
188
|
+
}
|
|
189
|
+
return this.listeners.get(instanceId)!;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Load installed plugins (global)
|
|
194
|
+
*/
|
|
195
|
+
private loadInstalledPlugins(): Omit<StoredPlugin, 'enabled'>[] {
|
|
196
|
+
if (typeof window === 'undefined') return [];
|
|
197
|
+
try {
|
|
198
|
+
const stored = localStorage.getItem(PLUGINS_KEY);
|
|
199
|
+
return stored ? JSON.parse(stored) : [];
|
|
200
|
+
} catch {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Save installed plugins (global)
|
|
207
|
+
* Serializes executors as executorCode strings
|
|
208
|
+
*/
|
|
209
|
+
private saveInstalledPlugins(plugins: Omit<StoredPlugin, 'enabled'>[]): void {
|
|
210
|
+
if (typeof window === 'undefined') return;
|
|
211
|
+
localStorage.setItem(PLUGINS_KEY, JSON.stringify(plugins));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Load enabled state for an instance
|
|
216
|
+
*/
|
|
217
|
+
private loadEnabledState(instanceId: string): EnabledState {
|
|
218
|
+
if (typeof window === 'undefined') return {};
|
|
219
|
+
try {
|
|
220
|
+
const stored = localStorage.getItem(getEnabledStateKey(instanceId));
|
|
221
|
+
return stored ? JSON.parse(stored) : {};
|
|
222
|
+
} catch {
|
|
223
|
+
return {};
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Save enabled state for an instance
|
|
229
|
+
*/
|
|
230
|
+
private saveEnabledState(state: EnabledState, instanceId: string): void {
|
|
231
|
+
if (typeof window === 'undefined') return;
|
|
232
|
+
localStorage.setItem(getEnabledStateKey(instanceId), JSON.stringify(state));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Load plugins with instance-specific enabled state
|
|
237
|
+
* Combines global plugin list with per-instance enabled state
|
|
238
|
+
*/
|
|
239
|
+
loadFromStorage(instanceId: string = 'default'): StoredPlugin[] {
|
|
240
|
+
const installed = this.loadInstalledPlugins();
|
|
241
|
+
const enabledState = this.loadEnabledState(instanceId);
|
|
242
|
+
|
|
243
|
+
return installed.map((plugin) => ({
|
|
244
|
+
...plugin,
|
|
245
|
+
// Default to enabled if no state exists for this instance
|
|
246
|
+
enabled: enabledState[plugin.name] ?? true,
|
|
247
|
+
}));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Register a new plugin (global - available to all instances)
|
|
252
|
+
* Serializes executors as executorCode for persistence
|
|
253
|
+
*
|
|
254
|
+
* @param plugin The plugin to install
|
|
255
|
+
* @param enabled Initial enabled state for this instance (default: true)
|
|
256
|
+
* @param instanceId Instance to set initial enabled state for
|
|
257
|
+
*/
|
|
258
|
+
register(plugin: HustlePlugin, enabled = true, instanceId: string = 'default'): void {
|
|
259
|
+
// Add to global installed list with serialized executors
|
|
260
|
+
const installed = this.loadInstalledPlugins();
|
|
261
|
+
const existing = installed.findIndex((p) => p.name === plugin.name);
|
|
262
|
+
|
|
263
|
+
const storedPlugin: Omit<StoredPlugin, 'enabled'> = {
|
|
264
|
+
name: plugin.name,
|
|
265
|
+
version: plugin.version,
|
|
266
|
+
description: plugin.description,
|
|
267
|
+
tools: serializePluginTools(plugin.tools, plugin.executors),
|
|
268
|
+
hooksCode: serializeHooks(plugin.hooks),
|
|
269
|
+
installedAt: new Date().toISOString(),
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
if (existing >= 0) {
|
|
273
|
+
installed[existing] = storedPlugin;
|
|
274
|
+
} else {
|
|
275
|
+
installed.push(storedPlugin);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
this.saveInstalledPlugins(installed);
|
|
279
|
+
|
|
280
|
+
// Set initial enabled state for this instance
|
|
281
|
+
const enabledState = this.loadEnabledState(instanceId);
|
|
282
|
+
enabledState[plugin.name] = enabled;
|
|
283
|
+
this.saveEnabledState(enabledState, instanceId);
|
|
284
|
+
|
|
285
|
+
this.notifyListeners(instanceId);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Unregister a plugin (global - removes from all instances)
|
|
290
|
+
*/
|
|
291
|
+
unregister(pluginName: string, instanceId: string = 'default'): void {
|
|
292
|
+
// Remove from global list
|
|
293
|
+
const installed = this.loadInstalledPlugins().filter((p) => p.name !== pluginName);
|
|
294
|
+
this.saveInstalledPlugins(installed);
|
|
295
|
+
|
|
296
|
+
// Clean up enabled state for this instance
|
|
297
|
+
const enabledState = this.loadEnabledState(instanceId);
|
|
298
|
+
delete enabledState[pluginName];
|
|
299
|
+
this.saveEnabledState(enabledState, instanceId);
|
|
300
|
+
|
|
301
|
+
this.notifyListeners(instanceId);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Enable or disable a plugin (instance-scoped)
|
|
306
|
+
*/
|
|
307
|
+
setEnabled(pluginName: string, enabled: boolean, instanceId: string = 'default'): void {
|
|
308
|
+
const enabledState = this.loadEnabledState(instanceId);
|
|
309
|
+
enabledState[pluginName] = enabled;
|
|
310
|
+
this.saveEnabledState(enabledState, instanceId);
|
|
311
|
+
this.notifyListeners(instanceId);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Check if a plugin is installed (global)
|
|
316
|
+
*/
|
|
317
|
+
isRegistered(pluginName: string): boolean {
|
|
318
|
+
return this.loadInstalledPlugins().some((p) => p.name === pluginName);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Get a specific plugin with instance-specific enabled state
|
|
323
|
+
*/
|
|
324
|
+
getPlugin(pluginName: string, instanceId: string = 'default'): StoredPlugin | undefined {
|
|
325
|
+
return this.loadFromStorage(instanceId).find((p) => p.name === pluginName);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Get all enabled plugins for an instance (hydrated with executors)
|
|
330
|
+
*/
|
|
331
|
+
getEnabledPlugins(instanceId: string = 'default'): HydratedPlugin[] {
|
|
332
|
+
return this.loadFromStorage(instanceId).filter((p) => p.enabled).map(hydratePlugin);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Subscribe to plugin changes for a specific instance
|
|
337
|
+
*/
|
|
338
|
+
onChange(callback: PluginChangeCallback, instanceId: string = 'default'): () => void {
|
|
339
|
+
const listeners = this.getListeners(instanceId);
|
|
340
|
+
listeners.add(callback);
|
|
341
|
+
return () => listeners.delete(callback);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Notify all listeners for a specific instance
|
|
346
|
+
*/
|
|
347
|
+
private notifyListeners(instanceId: string = 'default'): void {
|
|
348
|
+
const plugins = this.loadFromStorage(instanceId);
|
|
349
|
+
const listeners = this.getListeners(instanceId);
|
|
350
|
+
listeners.forEach((cb) => cb(plugins));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Clear enabled state for an instance (plugins remain installed globally)
|
|
355
|
+
*/
|
|
356
|
+
clear(instanceId: string = 'default'): void {
|
|
357
|
+
if (typeof window === 'undefined') return;
|
|
358
|
+
localStorage.removeItem(getEnabledStateKey(instanceId));
|
|
359
|
+
this.notifyListeners(instanceId);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Clear all installed plugins globally
|
|
364
|
+
*/
|
|
365
|
+
clearAll(): void {
|
|
366
|
+
if (typeof window === 'undefined') return;
|
|
367
|
+
localStorage.removeItem(PLUGINS_KEY);
|
|
368
|
+
// Note: This doesn't clear instance-specific enabled states
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Singleton instance
|
|
373
|
+
export const pluginRegistry = new PluginRegistry();
|
|
374
|
+
|
|
375
|
+
export default pluginRegistry;
|