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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +312 -0
  3. package/bin/prepia.mjs +119 -0
  4. package/package.json +53 -0
  5. package/skill/SKILL.md +148 -0
  6. package/skill/config.json +29 -0
  7. package/src/analytics/dashboard.mjs +84 -0
  8. package/src/analytics/tracker.mjs +131 -0
  9. package/src/api/middleware.mjs +219 -0
  10. package/src/api/routes.mjs +142 -0
  11. package/src/api/server.mjs +150 -0
  12. package/src/cache/disk-store.mjs +199 -0
  13. package/src/cache/manager.mjs +142 -0
  14. package/src/cache/memory-store.mjs +205 -0
  15. package/src/chain/dag.mjs +209 -0
  16. package/src/chain/executor.mjs +103 -0
  17. package/src/chain/scheduler.mjs +89 -0
  18. package/src/client/adapters.mjs +483 -0
  19. package/src/client/connector.mjs +391 -0
  20. package/src/client/index.mjs +483 -0
  21. package/src/client/websocket.mjs +353 -0
  22. package/src/core/context-packager.mjs +169 -0
  23. package/src/core/engine.mjs +338 -0
  24. package/src/core/event-bus.mjs +84 -0
  25. package/src/core/prepimshot.mjs +120 -0
  26. package/src/core/task-decomposer.mjs +158 -0
  27. package/src/edge/lite.mjs +90 -0
  28. package/src/guard/checker.mjs +123 -0
  29. package/src/guard/fact-checker.mjs +105 -0
  30. package/src/guard/hallucination.mjs +108 -0
  31. package/src/index.mjs +67 -0
  32. package/src/models/local-model.mjs +171 -0
  33. package/src/models/provider.mjs +192 -0
  34. package/src/models/router.mjs +156 -0
  35. package/src/morph/optimizer.mjs +142 -0
  36. package/src/network/p2p.mjs +146 -0
  37. package/src/persona/detector.mjs +118 -0
  38. package/src/plugins/loader.mjs +120 -0
  39. package/src/plugins/registry.mjs +164 -0
  40. package/src/plugins/sandbox.mjs +79 -0
  41. package/src/rate/limiter.mjs +145 -0
  42. package/src/rate/shield.mjs +150 -0
  43. package/src/script/executor.mjs +164 -0
  44. package/src/script/parser.mjs +134 -0
  45. package/src/security/privacy.mjs +108 -0
  46. package/src/security/sanitizer.mjs +133 -0
  47. package/src/shadow/daemon.mjs +128 -0
  48. package/src/stream/handler.mjs +204 -0
  49. package/src/tools/calculator.mjs +312 -0
  50. package/src/tools/file-ops.mjs +138 -0
  51. package/src/tools/http-client.mjs +127 -0
  52. package/src/tools/orchestrator.mjs +205 -0
  53. package/src/tools/web-scraper.mjs +159 -0
  54. package/src/tools/web-search.mjs +129 -0
  55. package/src/vault/knowledge-base.mjs +207 -0
  56. package/src/vault/pattern-learner.mjs +192 -0
  57. package/workflows/analyze.json +32 -0
  58. package/workflows/automate.json +32 -0
  59. package/workflows/research.json +37 -0
  60. 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 };