prepia 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +312 -0
- package/bin/prepia.mjs +119 -0
- package/package.json +53 -0
- package/skill/SKILL.md +148 -0
- package/skill/config.json +29 -0
- package/src/analytics/dashboard.mjs +84 -0
- package/src/analytics/tracker.mjs +131 -0
- package/src/api/middleware.mjs +219 -0
- package/src/api/routes.mjs +142 -0
- package/src/api/server.mjs +150 -0
- package/src/cache/disk-store.mjs +199 -0
- package/src/cache/manager.mjs +142 -0
- package/src/cache/memory-store.mjs +205 -0
- package/src/chain/dag.mjs +209 -0
- package/src/chain/executor.mjs +103 -0
- package/src/chain/scheduler.mjs +89 -0
- package/src/client/adapters.mjs +483 -0
- package/src/client/connector.mjs +391 -0
- package/src/client/index.mjs +483 -0
- package/src/client/websocket.mjs +353 -0
- package/src/core/context-packager.mjs +169 -0
- package/src/core/engine.mjs +338 -0
- package/src/core/event-bus.mjs +84 -0
- package/src/core/prepimshot.mjs +120 -0
- package/src/core/task-decomposer.mjs +158 -0
- package/src/edge/lite.mjs +90 -0
- package/src/guard/checker.mjs +123 -0
- package/src/guard/fact-checker.mjs +105 -0
- package/src/guard/hallucination.mjs +108 -0
- package/src/index.mjs +67 -0
- package/src/models/local-model.mjs +171 -0
- package/src/models/provider.mjs +192 -0
- package/src/models/router.mjs +156 -0
- package/src/morph/optimizer.mjs +142 -0
- package/src/network/p2p.mjs +146 -0
- package/src/persona/detector.mjs +118 -0
- package/src/plugins/loader.mjs +120 -0
- package/src/plugins/registry.mjs +164 -0
- package/src/plugins/sandbox.mjs +79 -0
- package/src/rate/limiter.mjs +145 -0
- package/src/rate/shield.mjs +150 -0
- package/src/script/executor.mjs +164 -0
- package/src/script/parser.mjs +134 -0
- package/src/security/privacy.mjs +108 -0
- package/src/security/sanitizer.mjs +133 -0
- package/src/shadow/daemon.mjs +128 -0
- package/src/stream/handler.mjs +204 -0
- package/src/tools/calculator.mjs +312 -0
- package/src/tools/file-ops.mjs +138 -0
- package/src/tools/http-client.mjs +127 -0
- package/src/tools/orchestrator.mjs +205 -0
- package/src/tools/web-scraper.mjs +159 -0
- package/src/tools/web-search.mjs +129 -0
- package/src/vault/knowledge-base.mjs +207 -0
- package/src/vault/pattern-learner.mjs +192 -0
- package/workflows/analyze.json +32 -0
- package/workflows/automate.json +32 -0
- package/workflows/research.json +37 -0
- package/workflows/summarize.json +32 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Context-aware persona selection.
|
|
3
|
+
* @module persona/detector
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {Object} Persona
|
|
8
|
+
* @property {string} name - Persona name
|
|
9
|
+
* @property {string} type - Task type
|
|
10
|
+
* @property {string} style - Processing style
|
|
11
|
+
* @property {string} outputFormat - Preferred output format
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Available personas */
|
|
15
|
+
const PERSONAS = {
|
|
16
|
+
data: {
|
|
17
|
+
name: 'Analyst',
|
|
18
|
+
type: 'data',
|
|
19
|
+
style: 'precise',
|
|
20
|
+
outputFormat: 'structured',
|
|
21
|
+
systemPrompt: 'You are a data analyst. Provide precise, structured answers with numbers and evidence. Use tables and lists.',
|
|
22
|
+
},
|
|
23
|
+
writing: {
|
|
24
|
+
name: 'Writer',
|
|
25
|
+
type: 'writing',
|
|
26
|
+
style: 'creative',
|
|
27
|
+
outputFormat: 'narrative',
|
|
28
|
+
systemPrompt: 'You are a skilled writer. Produce clear, engaging, well-structured prose. Focus on clarity and readability.',
|
|
29
|
+
},
|
|
30
|
+
research: {
|
|
31
|
+
name: 'Researcher',
|
|
32
|
+
type: 'research',
|
|
33
|
+
style: 'thorough',
|
|
34
|
+
outputFormat: 'cited',
|
|
35
|
+
systemPrompt: 'You are a thorough researcher. Provide comprehensive answers with sources and citations. Cover multiple perspectives.',
|
|
36
|
+
},
|
|
37
|
+
automation: {
|
|
38
|
+
name: 'Engineer',
|
|
39
|
+
type: 'automation',
|
|
40
|
+
style: 'efficient',
|
|
41
|
+
outputFormat: 'procedural',
|
|
42
|
+
systemPrompt: 'You are a technical engineer. Provide clear, step-by-step instructions. Focus on efficiency and correctness.',
|
|
43
|
+
},
|
|
44
|
+
casual: {
|
|
45
|
+
name: 'Assistant',
|
|
46
|
+
type: 'casual',
|
|
47
|
+
style: 'friendly',
|
|
48
|
+
outputFormat: 'conversational',
|
|
49
|
+
systemPrompt: 'You are a helpful assistant. Provide clear, friendly answers. Keep it concise but thorough.',
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Detect the appropriate persona for a task.
|
|
55
|
+
* @param {string} query - User query
|
|
56
|
+
* @param {Object} [context] - Additional context
|
|
57
|
+
* @returns {Persona}
|
|
58
|
+
*/
|
|
59
|
+
export function detectPersona(query, context = {}) {
|
|
60
|
+
if (!query || typeof query !== 'string') {
|
|
61
|
+
return PERSONAS.casual;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const lower = query.toLowerCase();
|
|
65
|
+
|
|
66
|
+
// Data analysis patterns
|
|
67
|
+
if (
|
|
68
|
+
/\b(analyze|data|statistics|numbers|metrics|compare|trend|calculate|measure|percent|average|median)\b/i.test(lower) ||
|
|
69
|
+
/\b(chart|graph|table|report|dashboard)\b/i.test(lower)
|
|
70
|
+
) {
|
|
71
|
+
return PERSONAS.data;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Writing patterns
|
|
75
|
+
if (
|
|
76
|
+
/\b(write|draft|compose|essay|article|story|blog|email|letter|creative|poem|narrative)\b/i.test(lower) ||
|
|
77
|
+
/\b(summarize|rewrite|edit|proofread|paraphrase)\b/i.test(lower)
|
|
78
|
+
) {
|
|
79
|
+
return PERSONAS.writing;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Research patterns
|
|
83
|
+
if (
|
|
84
|
+
/\b(research|investigate|find|search|learn|explain|understand|what is|who is|history of|background)\b/i.test(lower) ||
|
|
85
|
+
/\b(compare|versus|pros and cons|differences)\b/i.test(lower)
|
|
86
|
+
) {
|
|
87
|
+
return PERSONAS.research;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Automation patterns
|
|
91
|
+
if (
|
|
92
|
+
/\b(automate|script|code|program|build|create|implement|deploy|configure|setup|install)\b/i.test(lower) ||
|
|
93
|
+
/\b(api|database|server|function|class|method)\b/i.test(lower)
|
|
94
|
+
) {
|
|
95
|
+
return PERSONAS.automation;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return PERSONAS.casual;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get all available personas.
|
|
103
|
+
* @returns {Object[]}
|
|
104
|
+
*/
|
|
105
|
+
export function getPersonas() {
|
|
106
|
+
return Object.values(PERSONAS);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get a specific persona by type.
|
|
111
|
+
* @param {string} type
|
|
112
|
+
* @returns {Persona|undefined}
|
|
113
|
+
*/
|
|
114
|
+
export function getPersona(type) {
|
|
115
|
+
return PERSONAS[type];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export default { detectPersona, getPersonas, getPersona };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Plugin discovery and loading.
|
|
3
|
+
* @module plugins/loader
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { promises as fs } from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
|
|
9
|
+
export class PluginLoader {
|
|
10
|
+
/**
|
|
11
|
+
* @param {Object} [options]
|
|
12
|
+
* @param {string[]} [options.directories=['.prepia/plugins']] - Plugin directories
|
|
13
|
+
*/
|
|
14
|
+
constructor(options = {}) {
|
|
15
|
+
this._directories = options.directories ?? ['.prepia/plugins'];
|
|
16
|
+
this._watchers = [];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Discover plugins from configured directories.
|
|
21
|
+
* @returns {Promise<Object[]>} Array of plugin manifests
|
|
22
|
+
*/
|
|
23
|
+
async discover() {
|
|
24
|
+
const plugins = [];
|
|
25
|
+
for (const dir of this._directories) {
|
|
26
|
+
try {
|
|
27
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
if (!entry.isDirectory()) continue;
|
|
30
|
+
const manifestPath = path.join(dir, entry.name, 'manifest.json');
|
|
31
|
+
try {
|
|
32
|
+
const data = await fs.readFile(manifestPath, 'utf-8');
|
|
33
|
+
const manifest = JSON.parse(data);
|
|
34
|
+
manifest._path = path.join(dir, entry.name);
|
|
35
|
+
manifest._entryPath = path.join(dir, entry.name, manifest.main || 'index.mjs');
|
|
36
|
+
plugins.push(manifest);
|
|
37
|
+
} catch {
|
|
38
|
+
// Skip directories without manifest
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
// Directory may not exist
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return plugins;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Load a plugin from its manifest.
|
|
50
|
+
* @param {Object} manifest - Plugin manifest
|
|
51
|
+
* @returns {Promise<Object>} Loaded plugin
|
|
52
|
+
*/
|
|
53
|
+
async load(manifest) {
|
|
54
|
+
if (!manifest._entryPath) {
|
|
55
|
+
throw new Error('Plugin manifest missing entry path');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const plugin = await import(manifest._entryPath);
|
|
60
|
+
return {
|
|
61
|
+
name: manifest.name,
|
|
62
|
+
version: manifest.version,
|
|
63
|
+
description: manifest.description || '',
|
|
64
|
+
manifest,
|
|
65
|
+
module: plugin,
|
|
66
|
+
loadedAt: Date.now(),
|
|
67
|
+
};
|
|
68
|
+
} catch (err) {
|
|
69
|
+
throw new Error(`Failed to load plugin ${manifest.name}: ${err.message}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Discover and load all plugins.
|
|
75
|
+
* @returns {Promise<Object[]>}
|
|
76
|
+
*/
|
|
77
|
+
async loadAll() {
|
|
78
|
+
const manifests = await this.discover();
|
|
79
|
+
const plugins = [];
|
|
80
|
+
for (const manifest of manifests) {
|
|
81
|
+
try {
|
|
82
|
+
const plugin = await this.load(manifest);
|
|
83
|
+
plugins.push(plugin);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
// Log but don't fail on individual plugin errors
|
|
86
|
+
plugins.push({ name: manifest.name, error: err.message, loadedAt: Date.now() });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return plugins;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Validate a plugin manifest.
|
|
94
|
+
* @param {Object} manifest
|
|
95
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
96
|
+
*/
|
|
97
|
+
validate(manifest) {
|
|
98
|
+
const errors = [];
|
|
99
|
+
if (!manifest.name || typeof manifest.name !== 'string') {
|
|
100
|
+
errors.push('Missing or invalid "name" field');
|
|
101
|
+
}
|
|
102
|
+
if (!manifest.version || typeof manifest.version !== 'string') {
|
|
103
|
+
errors.push('Missing or invalid "version" field');
|
|
104
|
+
}
|
|
105
|
+
if (!manifest.main || typeof manifest.main !== 'string') {
|
|
106
|
+
errors.push('Missing or invalid "main" field');
|
|
107
|
+
}
|
|
108
|
+
return { valid: errors.length === 0, errors };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get the list of configured plugin directories.
|
|
113
|
+
* @returns {string[]}
|
|
114
|
+
*/
|
|
115
|
+
get directories() {
|
|
116
|
+
return [...this._directories];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export default PluginLoader;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Plugin registry and lifecycle management.
|
|
3
|
+
* @module plugins/registry
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { EventEmitter } from 'node:events';
|
|
7
|
+
|
|
8
|
+
export class PluginRegistry extends EventEmitter {
|
|
9
|
+
constructor() {
|
|
10
|
+
super();
|
|
11
|
+
/** @type {Map<string, Object>} */
|
|
12
|
+
this._plugins = new Map();
|
|
13
|
+
/** @type {Map<string, string[]>} Plugin dependencies */
|
|
14
|
+
this._dependencies = new Map();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Register a plugin.
|
|
19
|
+
* @param {Object} plugin - Loaded plugin object
|
|
20
|
+
* @param {Object} plugin.name - Plugin name
|
|
21
|
+
* @param {Object} plugin.module - Plugin module
|
|
22
|
+
* @param {Object} [plugin.manifest] - Plugin manifest
|
|
23
|
+
*/
|
|
24
|
+
register(plugin) {
|
|
25
|
+
if (!plugin.name) throw new Error('Plugin must have a name');
|
|
26
|
+
if (this._plugins.has(plugin.name)) {
|
|
27
|
+
throw new Error(`Plugin "${plugin.name}" is already registered`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Check dependencies
|
|
31
|
+
const deps = plugin.manifest?.dependencies || [];
|
|
32
|
+
for (const dep of deps) {
|
|
33
|
+
if (!this._plugins.has(dep)) {
|
|
34
|
+
throw new Error(`Plugin "${plugin.name}" depends on "${dep}" which is not registered`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
this._plugins.set(plugin.name, {
|
|
39
|
+
...plugin,
|
|
40
|
+
status: 'registered',
|
|
41
|
+
registeredAt: Date.now(),
|
|
42
|
+
});
|
|
43
|
+
this._dependencies.set(plugin.name, deps);
|
|
44
|
+
this.emit('plugin:registered', { name: plugin.name });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Unregister a plugin.
|
|
49
|
+
* @param {string} name - Plugin name
|
|
50
|
+
*/
|
|
51
|
+
unregister(name) {
|
|
52
|
+
// Check if other plugins depend on this one
|
|
53
|
+
for (const [pluginName, deps] of this._dependencies) {
|
|
54
|
+
if (deps.includes(name)) {
|
|
55
|
+
throw new Error(`Cannot unregister "${name}": plugin "${pluginName}" depends on it`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
this._plugins.delete(name);
|
|
60
|
+
this._dependencies.delete(name);
|
|
61
|
+
this.emit('plugin:unregistered', { name });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Initialize a plugin (call its init lifecycle method).
|
|
66
|
+
* @param {string} name
|
|
67
|
+
* @param {Object} [context] - Init context
|
|
68
|
+
*/
|
|
69
|
+
async init(name, context = {}) {
|
|
70
|
+
const plugin = this._plugins.get(name);
|
|
71
|
+
if (!plugin) throw new Error(`Plugin "${name}" not found`);
|
|
72
|
+
|
|
73
|
+
if (plugin.module.init) {
|
|
74
|
+
await plugin.module.init(context);
|
|
75
|
+
}
|
|
76
|
+
plugin.status = 'initialized';
|
|
77
|
+
this.emit('plugin:initialized', { name });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Execute a plugin's main function.
|
|
82
|
+
* @param {string} name
|
|
83
|
+
* @param {Object} params
|
|
84
|
+
* @returns {Promise<*>}
|
|
85
|
+
*/
|
|
86
|
+
async execute(name, params = {}) {
|
|
87
|
+
const plugin = this._plugins.get(name);
|
|
88
|
+
if (!plugin) throw new Error(`Plugin "${name}" not found`);
|
|
89
|
+
if (plugin.status !== 'initialized') {
|
|
90
|
+
throw new Error(`Plugin "${name}" is not initialized`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!plugin.module.execute) {
|
|
94
|
+
throw new Error(`Plugin "${name}" has no execute function`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
this.emit('plugin:execute:start', { name });
|
|
98
|
+
try {
|
|
99
|
+
const result = await plugin.module.execute(params);
|
|
100
|
+
this.emit('plugin:execute:complete', { name });
|
|
101
|
+
return result;
|
|
102
|
+
} catch (err) {
|
|
103
|
+
this.emit('plugin:execute:error', { name, error: err.message });
|
|
104
|
+
throw err;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Cleanup a plugin (call its cleanup lifecycle method).
|
|
110
|
+
* @param {string} name
|
|
111
|
+
*/
|
|
112
|
+
async cleanup(name) {
|
|
113
|
+
const plugin = this._plugins.get(name);
|
|
114
|
+
if (!plugin) throw new Error(`Plugin "${name}" not found`);
|
|
115
|
+
|
|
116
|
+
if (plugin.module.cleanup) {
|
|
117
|
+
await plugin.module.cleanup();
|
|
118
|
+
}
|
|
119
|
+
plugin.status = 'cleaned';
|
|
120
|
+
this.emit('plugin:cleaned', { name });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get a plugin by name.
|
|
125
|
+
* @param {string} name
|
|
126
|
+
* @returns {Object|undefined}
|
|
127
|
+
*/
|
|
128
|
+
get(name) {
|
|
129
|
+
return this._plugins.get(name);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get all registered plugins.
|
|
134
|
+
* @returns {Object[]}
|
|
135
|
+
*/
|
|
136
|
+
getAll() {
|
|
137
|
+
return Array.from(this._plugins.values()).map(p => ({
|
|
138
|
+
name: p.name,
|
|
139
|
+
version: p.version,
|
|
140
|
+
description: p.description,
|
|
141
|
+
status: p.status,
|
|
142
|
+
registeredAt: p.registeredAt,
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Check if a plugin is registered.
|
|
148
|
+
* @param {string} name
|
|
149
|
+
* @returns {boolean}
|
|
150
|
+
*/
|
|
151
|
+
has(name) {
|
|
152
|
+
return this._plugins.has(name);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get the number of registered plugins.
|
|
157
|
+
* @returns {number}
|
|
158
|
+
*/
|
|
159
|
+
get size() {
|
|
160
|
+
return this._plugins.size;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export default PluginRegistry;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Plugin execution sandbox with resource limits.
|
|
3
|
+
* @module plugins/sandbox
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class Sandbox {
|
|
7
|
+
/**
|
|
8
|
+
* @param {Object} [options]
|
|
9
|
+
* @param {number} [options.timeout=10000] - Execution timeout in ms
|
|
10
|
+
* @param {number} [options.maxMemory=50*1024*1024] - Max memory (50MB)
|
|
11
|
+
*/
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
this._timeout = options.timeout ?? 10000;
|
|
14
|
+
this._maxMemory = options.maxMemory ?? 50 * 1024 * 1024;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Execute a function in a sandboxed context.
|
|
19
|
+
* @param {Function} fn - Async function to execute
|
|
20
|
+
* @param {Object} [context] - Context to pass to the function
|
|
21
|
+
* @returns {Promise<*>}
|
|
22
|
+
*/
|
|
23
|
+
async execute(fn, context = {}) {
|
|
24
|
+
const start = Date.now();
|
|
25
|
+
let result;
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
result = await Promise.race([
|
|
29
|
+
fn(context),
|
|
30
|
+
new Promise((_, reject) =>
|
|
31
|
+
setTimeout(() => reject(new Error('Sandbox execution timeout')), this._timeout)
|
|
32
|
+
),
|
|
33
|
+
]);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
return {
|
|
36
|
+
success: false,
|
|
37
|
+
error: err.message,
|
|
38
|
+
duration: Date.now() - start,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
success: true,
|
|
44
|
+
result,
|
|
45
|
+
duration: Date.now() - start,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Execute a plugin module function with validation.
|
|
51
|
+
* @param {Object} plugin - Plugin with module.execute
|
|
52
|
+
* @param {Object} params - Execution parameters
|
|
53
|
+
* @returns {Promise<Object>}
|
|
54
|
+
*/
|
|
55
|
+
async executePlugin(plugin, params = {}) {
|
|
56
|
+
if (!plugin?.module?.execute) {
|
|
57
|
+
return { success: false, error: 'Plugin has no execute function' };
|
|
58
|
+
}
|
|
59
|
+
return this.execute(plugin.module.execute, params);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Create a restricted context object for plugin execution.
|
|
64
|
+
* @param {Object} [allowedAPIs] - APIs to expose
|
|
65
|
+
* @returns {Object}
|
|
66
|
+
*/
|
|
67
|
+
createContext(allowedAPIs = {}) {
|
|
68
|
+
return {
|
|
69
|
+
console: {
|
|
70
|
+
log: (...args) => console.log(`[plugin]`, ...args),
|
|
71
|
+
warn: (...args) => console.warn(`[plugin]`, ...args),
|
|
72
|
+
error: (...args) => console.error(`[plugin]`, ...args),
|
|
73
|
+
},
|
|
74
|
+
...allowedAPIs,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export default Sandbox;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Rate limiter - token bucket and sliding window algorithms.
|
|
3
|
+
* @module rate/limiter
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Token bucket rate limiter.
|
|
8
|
+
*/
|
|
9
|
+
export class TokenBucket {
|
|
10
|
+
/**
|
|
11
|
+
* @param {Object} options
|
|
12
|
+
* @param {number} options.capacity - Max tokens
|
|
13
|
+
* @param {number} options.refillRate - Tokens per second
|
|
14
|
+
*/
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
this._capacity = options.capacity ?? 100;
|
|
17
|
+
this._refillRate = options.refillRate ?? 10;
|
|
18
|
+
this._tokens = this._capacity;
|
|
19
|
+
this._lastRefill = Date.now();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Try to consume tokens.
|
|
24
|
+
* @param {number} [count=1] - Tokens to consume
|
|
25
|
+
* @returns {boolean} Whether tokens were available
|
|
26
|
+
*/
|
|
27
|
+
consume(count = 1) {
|
|
28
|
+
this._refill();
|
|
29
|
+
if (this._tokens >= count) {
|
|
30
|
+
this._tokens -= count;
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get current token count.
|
|
38
|
+
* @returns {number}
|
|
39
|
+
*/
|
|
40
|
+
get tokens() {
|
|
41
|
+
this._refill();
|
|
42
|
+
return this._tokens;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get time until next token is available.
|
|
47
|
+
* @param {number} [count=1] - Tokens needed
|
|
48
|
+
* @returns {number} Milliseconds until available, 0 if available now
|
|
49
|
+
*/
|
|
50
|
+
waitFor(count = 1) {
|
|
51
|
+
this._refill();
|
|
52
|
+
if (this._tokens >= count) return 0;
|
|
53
|
+
const deficit = count - this._tokens;
|
|
54
|
+
return Math.ceil((deficit / this._refillRate) * 1000);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Refill tokens based on elapsed time.
|
|
59
|
+
* @private
|
|
60
|
+
*/
|
|
61
|
+
_refill() {
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
const elapsed = (now - this._lastRefill) / 1000;
|
|
64
|
+
this._tokens = Math.min(this._capacity, this._tokens + elapsed * this._refillRate);
|
|
65
|
+
this._lastRefill = now;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Reset to full capacity.
|
|
70
|
+
*/
|
|
71
|
+
reset() {
|
|
72
|
+
this._tokens = this._capacity;
|
|
73
|
+
this._lastRefill = Date.now();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Sliding window rate limiter.
|
|
79
|
+
*/
|
|
80
|
+
export class SlidingWindow {
|
|
81
|
+
/**
|
|
82
|
+
* @param {Object} options
|
|
83
|
+
* @param {number} options.maxRequests - Max requests per window
|
|
84
|
+
* @param {number} options.windowMs - Window duration in ms
|
|
85
|
+
*/
|
|
86
|
+
constructor(options = {}) {
|
|
87
|
+
this._maxRequests = options.maxRequests ?? 100;
|
|
88
|
+
this._windowMs = options.windowMs ?? 60000;
|
|
89
|
+
/** @type {number[]} */
|
|
90
|
+
this._timestamps = [];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Try to make a request.
|
|
95
|
+
* @returns {boolean} Whether request is allowed
|
|
96
|
+
*/
|
|
97
|
+
consume() {
|
|
98
|
+
this._cleanup();
|
|
99
|
+
if (this._timestamps.length < this._maxRequests) {
|
|
100
|
+
this._timestamps.push(Date.now());
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get remaining requests in current window.
|
|
108
|
+
* @returns {number}
|
|
109
|
+
*/
|
|
110
|
+
get remaining() {
|
|
111
|
+
this._cleanup();
|
|
112
|
+
return this._maxRequests - this._timestamps.length;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get time until next request is allowed.
|
|
117
|
+
* @returns {number} ms until available, 0 if available now
|
|
118
|
+
*/
|
|
119
|
+
waitFor() {
|
|
120
|
+
this._cleanup();
|
|
121
|
+
if (this._timestamps.length < this._maxRequests) return 0;
|
|
122
|
+
const oldest = this._timestamps[0];
|
|
123
|
+
return Math.max(0, oldest + this._windowMs - Date.now());
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Remove expired timestamps.
|
|
128
|
+
* @private
|
|
129
|
+
*/
|
|
130
|
+
_cleanup() {
|
|
131
|
+
const cutoff = Date.now() - this._windowMs;
|
|
132
|
+
while (this._timestamps.length > 0 && this._timestamps[0] <= cutoff) {
|
|
133
|
+
this._timestamps.shift();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Reset the window.
|
|
139
|
+
*/
|
|
140
|
+
reset() {
|
|
141
|
+
this._timestamps = [];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export default { TokenBucket, SlidingWindow };
|