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,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Cost/efficiency reporting dashboard.
|
|
3
|
+
* @module analytics/dashboard
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generate a formatted report from tracker metrics.
|
|
8
|
+
* @param {Object} metrics - From Tracker.getMetrics()
|
|
9
|
+
* @param {Object} [savings] - From Tracker.estimateSavings()
|
|
10
|
+
* @returns {string}
|
|
11
|
+
*/
|
|
12
|
+
export function generateReport(metrics, savings = {}) {
|
|
13
|
+
const uptimeStr = formatDuration(metrics.uptime || 0);
|
|
14
|
+
const efficiencyPct = ((metrics.efficiency || 0) * 100).toFixed(1);
|
|
15
|
+
|
|
16
|
+
return `
|
|
17
|
+
╔══════════════════════════════════════════════════════════╗
|
|
18
|
+
║ PREPIA ANALYTICS ║
|
|
19
|
+
╠══════════════════════════════════════════════════════════╣
|
|
20
|
+
║ ║
|
|
21
|
+
║ Uptime: ${uptimeStr.padEnd(38)}║
|
|
22
|
+
║ Tasks Processed: ${String(metrics.tasksProcessed).padEnd(38)}║
|
|
23
|
+
║ ║
|
|
24
|
+
║ ── LLM Usage ────────────────────────────────────────── ║
|
|
25
|
+
║ LLM Calls Made: ${String(metrics.llmCallsMade).padEnd(38)}║
|
|
26
|
+
║ LLM Calls Avoided:${String(metrics.llmCallsAvoided).padEnd(38)}║
|
|
27
|
+
║ Efficiency: ${(efficiencyPct + '%').padEnd(38)}║
|
|
28
|
+
║ ║
|
|
29
|
+
║ ── Token Usage ──────────────────────────────────────── ║
|
|
30
|
+
║ Tokens Used: ${String(metrics.tokensUsed).padEnd(38)}║
|
|
31
|
+
║ Tokens Saved: ${String(metrics.tokensSaved).padEnd(38)}║
|
|
32
|
+
║ ║
|
|
33
|
+
║ ── Cache ────────────────────────────────────────────── ║
|
|
34
|
+
║ Cache Hits: ${String(metrics.cacheHits).padEnd(38)}║
|
|
35
|
+
║ Cache Misses: ${String(metrics.cacheMisses).padEnd(38)}║
|
|
36
|
+
║ Hit Rate: ${formatHitRate(metrics.cacheHits, metrics.cacheMisses).padEnd(38)}║
|
|
37
|
+
║ ║
|
|
38
|
+
║ ── Performance ──────────────────────────────────────── ║
|
|
39
|
+
║ Avg Duration: ${(metrics.avgDuration + 'ms').padEnd(38)}║
|
|
40
|
+
║ Tasks/Minute: ${String(metrics.tasksPerMinute).padEnd(38)}║
|
|
41
|
+
║ Errors: ${String(metrics.errors).padEnd(38)}║
|
|
42
|
+
║ ║${savings.estimatedDollarsSaved !== undefined ? `
|
|
43
|
+
║ ── Cost Savings ─────────────────────────────────────── ║
|
|
44
|
+
║ Est. Saved: ${('$' + savings.estimatedDollarsSaved.toFixed(2)).padEnd(38)}║
|
|
45
|
+
║ Est. Spent: ${('$' + (savings.estimatedDollarsSpent || 0).toFixed(2)).padEnd(38)}║` : ''}
|
|
46
|
+
║ ║
|
|
47
|
+
╚══════════════════════════════════════════════════════════╝`.trim();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Generate a compact summary line.
|
|
52
|
+
* @param {Object} metrics
|
|
53
|
+
* @returns {string}
|
|
54
|
+
*/
|
|
55
|
+
export function generateSummary(metrics) {
|
|
56
|
+
const eff = ((metrics.efficiency || 0) * 100).toFixed(0);
|
|
57
|
+
return `Prepia: ${metrics.tasksProcessed} tasks | ${eff}% LLM avoided | ${metrics.tokensSaved} tokens saved | ${metrics.errors} errors`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Format duration in ms to human readable.
|
|
62
|
+
* @param {number} ms
|
|
63
|
+
* @returns {string}
|
|
64
|
+
*/
|
|
65
|
+
function formatDuration(ms) {
|
|
66
|
+
if (ms < 1000) return `${ms}ms`;
|
|
67
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
68
|
+
if (ms < 3600000) return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`;
|
|
69
|
+
return `${Math.floor(ms / 3600000)}h ${Math.floor((ms % 3600000) / 60000)}m`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Format cache hit rate.
|
|
74
|
+
* @param {number} hits
|
|
75
|
+
* @param {number} misses
|
|
76
|
+
* @returns {string}
|
|
77
|
+
*/
|
|
78
|
+
function formatHitRate(hits, misses) {
|
|
79
|
+
const total = hits + misses;
|
|
80
|
+
if (total === 0) return 'N/A';
|
|
81
|
+
return `${((hits / total) * 100).toFixed(1)}%`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export default { generateReport, generateSummary };
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Usage tracking and metrics.
|
|
3
|
+
* @module analytics/tracker
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { EventEmitter } from 'node:events';
|
|
7
|
+
|
|
8
|
+
export class Tracker extends EventEmitter {
|
|
9
|
+
constructor() {
|
|
10
|
+
super();
|
|
11
|
+
this._metrics = {
|
|
12
|
+
tasksProcessed: 0,
|
|
13
|
+
llmCallsMade: 0,
|
|
14
|
+
llmCallsAvoided: 0,
|
|
15
|
+
tokensUsed: 0,
|
|
16
|
+
tokensSaved: 0,
|
|
17
|
+
totalDuration: 0,
|
|
18
|
+
cacheHits: 0,
|
|
19
|
+
cacheMisses: 0,
|
|
20
|
+
errors: 0,
|
|
21
|
+
};
|
|
22
|
+
this._taskHistory = [];
|
|
23
|
+
this._maxHistory = 1000;
|
|
24
|
+
this._startTime = Date.now();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Record a completed task.
|
|
29
|
+
* @param {Object} task
|
|
30
|
+
* @param {string} task.id - Task ID
|
|
31
|
+
* @param {string} task.type - Task type
|
|
32
|
+
* @param {boolean} task.usedLLM - Whether LLM was called
|
|
33
|
+
* @param {number} [task.tokensUsed=0] - Tokens consumed
|
|
34
|
+
* @param {number} [task.tokensSaved=0] - Tokens saved by optimization
|
|
35
|
+
* @param {number} task.duration - Task duration in ms
|
|
36
|
+
* @param {boolean} [task.cached=false] - Whether result was cached
|
|
37
|
+
* @param {boolean} [task.success=true] - Whether task succeeded
|
|
38
|
+
*/
|
|
39
|
+
record(task) {
|
|
40
|
+
this._metrics.tasksProcessed++;
|
|
41
|
+
if (task.usedLLM) {
|
|
42
|
+
this._metrics.llmCallsMade++;
|
|
43
|
+
this._metrics.tokensUsed += task.tokensUsed || 0;
|
|
44
|
+
} else {
|
|
45
|
+
this._metrics.llmCallsAvoided++;
|
|
46
|
+
}
|
|
47
|
+
this._metrics.tokensSaved += task.tokensSaved || 0;
|
|
48
|
+
this._metrics.totalDuration += task.duration || 0;
|
|
49
|
+
if (task.cached) this._metrics.cacheHits++;
|
|
50
|
+
else this._metrics.cacheMisses++;
|
|
51
|
+
if (!task.success) this._metrics.errors++;
|
|
52
|
+
|
|
53
|
+
this._taskHistory.push({
|
|
54
|
+
...task,
|
|
55
|
+
timestamp: Date.now(),
|
|
56
|
+
});
|
|
57
|
+
if (this._taskHistory.length > this._maxHistory) {
|
|
58
|
+
this._taskHistory.shift();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.emit('task:recorded', task);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get current metrics.
|
|
66
|
+
* @returns {Object}
|
|
67
|
+
*/
|
|
68
|
+
getMetrics() {
|
|
69
|
+
const uptime = Date.now() - this._startTime;
|
|
70
|
+
const efficiency = this._metrics.tasksProcessed > 0
|
|
71
|
+
? this._metrics.llmCallsAvoided / this._metrics.tasksProcessed
|
|
72
|
+
: 0;
|
|
73
|
+
const avgDuration = this._metrics.tasksProcessed > 0
|
|
74
|
+
? this._metrics.totalDuration / this._metrics.tasksProcessed
|
|
75
|
+
: 0;
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
...this._metrics,
|
|
79
|
+
efficiency: Math.round(efficiency * 100) / 100,
|
|
80
|
+
avgDuration: Math.round(avgDuration),
|
|
81
|
+
uptime,
|
|
82
|
+
tasksPerMinute: uptime > 0 ? (this._metrics.tasksProcessed / (uptime / 60000)).toFixed(2) : 0,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get task history.
|
|
88
|
+
* @param {number} [limit=50]
|
|
89
|
+
* @returns {Object[]}
|
|
90
|
+
*/
|
|
91
|
+
getHistory(limit = 50) {
|
|
92
|
+
return this._taskHistory.slice(-limit);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Estimate cost savings.
|
|
97
|
+
* @param {number} [costPerToken=0.00001] - Cost per token in dollars
|
|
98
|
+
* @returns {Object}
|
|
99
|
+
*/
|
|
100
|
+
estimateSavings(costPerToken = 0.00001) {
|
|
101
|
+
const tokensSaved = this._metrics.tokensSaved;
|
|
102
|
+
const callsAvoided = this._metrics.llmCallsAvoided;
|
|
103
|
+
return {
|
|
104
|
+
tokensSaved,
|
|
105
|
+
callsAvoided,
|
|
106
|
+
estimatedDollarsSaved: Math.round(tokensSaved * costPerToken * 100) / 100,
|
|
107
|
+
estimatedDollarsSpent: Math.round(this._metrics.tokensUsed * costPerToken * 100) / 100,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Reset all metrics.
|
|
113
|
+
*/
|
|
114
|
+
reset() {
|
|
115
|
+
this._metrics = {
|
|
116
|
+
tasksProcessed: 0,
|
|
117
|
+
llmCallsMade: 0,
|
|
118
|
+
llmCallsAvoided: 0,
|
|
119
|
+
tokensUsed: 0,
|
|
120
|
+
tokensSaved: 0,
|
|
121
|
+
totalDuration: 0,
|
|
122
|
+
cacheHits: 0,
|
|
123
|
+
cacheMisses: 0,
|
|
124
|
+
errors: 0,
|
|
125
|
+
};
|
|
126
|
+
this._taskHistory = [];
|
|
127
|
+
this._startTime = Date.now();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export default Tracker;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview API middleware - request parsing, error handling, rate limiting, auth.
|
|
3
|
+
* @module api/middleware
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { SlidingWindow } from '../rate/limiter.mjs';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parse JSON request body.
|
|
10
|
+
* @param {import('node:http').IncomingMessage} req
|
|
11
|
+
* @returns {Promise<Object>}
|
|
12
|
+
*/
|
|
13
|
+
export function parseBody(req) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
if (req.method === 'GET' || req.method === 'HEAD') {
|
|
16
|
+
resolve(null);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let body = '';
|
|
21
|
+
req.on('data', chunk => {
|
|
22
|
+
body += chunk;
|
|
23
|
+
if (body.length > 1024 * 1024) { // 1MB limit
|
|
24
|
+
reject(new Error('Request body too large'));
|
|
25
|
+
req.destroy();
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
req.on('end', () => {
|
|
29
|
+
if (!body) {
|
|
30
|
+
resolve(null);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
resolve(JSON.parse(body));
|
|
35
|
+
} catch {
|
|
36
|
+
reject(new Error('Invalid JSON body'));
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
req.on('error', reject);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* CORS middleware.
|
|
45
|
+
* @param {import('node:http').IncomingMessage} req
|
|
46
|
+
* @param {import('node:http').ServerResponse} res
|
|
47
|
+
* @param {Object} [options]
|
|
48
|
+
*/
|
|
49
|
+
export function cors(req, res, options = {}) {
|
|
50
|
+
const origin = options.origin || '*';
|
|
51
|
+
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
52
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
53
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key');
|
|
54
|
+
res.setHeader('Access-Control-Max-Age', '86400');
|
|
55
|
+
|
|
56
|
+
if (req.method === 'OPTIONS') {
|
|
57
|
+
res.writeHead(204);
|
|
58
|
+
res.end();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Per-client rate limiting middleware.
|
|
64
|
+
*/
|
|
65
|
+
export class RateLimitMiddleware {
|
|
66
|
+
/**
|
|
67
|
+
* @param {Object} [options]
|
|
68
|
+
* @param {number} [options.maxRequests=100] - Max requests per window
|
|
69
|
+
* @param {number} [options.windowMs=60000] - Window duration
|
|
70
|
+
*/
|
|
71
|
+
constructor(options = {}) {
|
|
72
|
+
this._maxRequests = options.maxRequests ?? 100;
|
|
73
|
+
this._windowMs = options.windowMs ?? 60000;
|
|
74
|
+
/** @type {Map<string, SlidingWindow>} */
|
|
75
|
+
this._clients = new Map();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Check rate limit for a client.
|
|
80
|
+
* @param {string} clientId - Client identifier (IP or API key)
|
|
81
|
+
* @returns {{ allowed: boolean, remaining: number, retryAfter: number }}
|
|
82
|
+
*/
|
|
83
|
+
check(clientId) {
|
|
84
|
+
if (!this._clients.has(clientId)) {
|
|
85
|
+
this._clients.set(clientId, new SlidingWindow({
|
|
86
|
+
maxRequests: this._maxRequests,
|
|
87
|
+
windowMs: this._windowMs,
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const window = this._clients.get(clientId);
|
|
92
|
+
const allowed = window.consume();
|
|
93
|
+
return {
|
|
94
|
+
allowed,
|
|
95
|
+
remaining: window.remaining,
|
|
96
|
+
retryAfter: allowed ? 0 : window.waitFor(),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Clean up old client entries.
|
|
102
|
+
*/
|
|
103
|
+
cleanup() {
|
|
104
|
+
for (const [id, window] of this._clients) {
|
|
105
|
+
if (window.remaining === this._maxRequests) {
|
|
106
|
+
this._clients.delete(id);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Authentication middleware.
|
|
114
|
+
*/
|
|
115
|
+
export class AuthMiddleware {
|
|
116
|
+
/**
|
|
117
|
+
* @param {Object} [options]
|
|
118
|
+
* @param {string[]} [options.apiKeys] - Valid API keys
|
|
119
|
+
* @param {boolean} [options.required=false] - Whether auth is required
|
|
120
|
+
*/
|
|
121
|
+
constructor(options = {}) {
|
|
122
|
+
this._apiKeys = new Set(options.apiKeys || []);
|
|
123
|
+
this._required = options.required ?? false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Authenticate a request.
|
|
128
|
+
* @param {import('node:http').IncomingMessage} req
|
|
129
|
+
* @returns {{ authenticated: boolean, clientId: string }}
|
|
130
|
+
*/
|
|
131
|
+
authenticate(req) {
|
|
132
|
+
const apiKey = req.headers['x-api-key'] || this._extractBearerToken(req);
|
|
133
|
+
|
|
134
|
+
if (!apiKey) {
|
|
135
|
+
return { authenticated: !this._required, clientId: req.socket.remoteAddress || 'unknown' };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (this._apiKeys.size === 0) {
|
|
139
|
+
return { authenticated: true, clientId: apiKey };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const valid = this._apiKeys.has(apiKey);
|
|
143
|
+
return {
|
|
144
|
+
authenticated: valid,
|
|
145
|
+
clientId: valid ? apiKey : req.socket.remoteAddress || 'unknown',
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Add an API key.
|
|
151
|
+
* @param {string} key
|
|
152
|
+
*/
|
|
153
|
+
addKey(key) {
|
|
154
|
+
this._apiKeys.add(key);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Remove an API key.
|
|
159
|
+
* @param {string} key
|
|
160
|
+
*/
|
|
161
|
+
removeKey(key) {
|
|
162
|
+
this._apiKeys.delete(key);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Extract Bearer token from Authorization header.
|
|
167
|
+
* @param {import('node:http').IncomingMessage} req
|
|
168
|
+
* @returns {string|null}
|
|
169
|
+
* @private
|
|
170
|
+
*/
|
|
171
|
+
_extractBearerToken(req) {
|
|
172
|
+
const auth = req.headers.authorization;
|
|
173
|
+
if (auth?.startsWith('Bearer ')) {
|
|
174
|
+
return auth.substring(7);
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Error handler middleware.
|
|
182
|
+
* @param {Error} err
|
|
183
|
+
* @param {import('node:http').ServerResponse} res
|
|
184
|
+
*/
|
|
185
|
+
export function errorHandler(err, res) {
|
|
186
|
+
const status = err.statusCode || 500;
|
|
187
|
+
const message = err.message || 'Internal Server Error';
|
|
188
|
+
|
|
189
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
190
|
+
res.end(JSON.stringify({
|
|
191
|
+
error: {
|
|
192
|
+
message,
|
|
193
|
+
status,
|
|
194
|
+
timestamp: new Date().toISOString(),
|
|
195
|
+
},
|
|
196
|
+
}));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Request logger middleware.
|
|
201
|
+
* @param {import('node:http').IncomingMessage} req
|
|
202
|
+
* @param {import('node:http').ServerResponse} res
|
|
203
|
+
* @returns {Object} Logger with end() method
|
|
204
|
+
*/
|
|
205
|
+
export function requestLogger(req) {
|
|
206
|
+
const start = Date.now();
|
|
207
|
+
const method = req.method;
|
|
208
|
+
const url = req.url;
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
end(res) {
|
|
212
|
+
const duration = Date.now() - start;
|
|
213
|
+
const status = res.statusCode;
|
|
214
|
+
console.log(`${method} ${url} ${status} ${duration}ms`);
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export default { parseBody, cors, RateLimitMiddleware, AuthMiddleware, errorHandler, requestLogger };
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview API route handlers.
|
|
3
|
+
* @module api/routes
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create route handlers for the API.
|
|
8
|
+
* @param {Object} engine - Prepia engine instance
|
|
9
|
+
* @returns {Object} Route handlers
|
|
10
|
+
*/
|
|
11
|
+
export function createRoutes(engine) {
|
|
12
|
+
/**
|
|
13
|
+
* POST /task - Submit a task
|
|
14
|
+
*/
|
|
15
|
+
async function handleTask(req, res, body) {
|
|
16
|
+
if (!body || !body.query) {
|
|
17
|
+
return sendError(res, 400, 'Missing "query" in request body');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const result = await engine.process(body.query, {
|
|
22
|
+
mode: body.mode || 'shot',
|
|
23
|
+
workflow: body.workflow,
|
|
24
|
+
...body.options,
|
|
25
|
+
});
|
|
26
|
+
sendJSON(res, 200, result);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
sendError(res, 500, err.message);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* GET /status - Health check
|
|
34
|
+
*/
|
|
35
|
+
function handleStatus(req, res) {
|
|
36
|
+
sendJSON(res, 200, {
|
|
37
|
+
status: 'ok',
|
|
38
|
+
version: '1.0.0',
|
|
39
|
+
uptime: process.uptime(),
|
|
40
|
+
timestamp: new Date().toISOString(),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* GET /analytics - Usage stats
|
|
46
|
+
*/
|
|
47
|
+
function handleAnalytics(req, res) {
|
|
48
|
+
try {
|
|
49
|
+
const metrics = engine.getAnalytics();
|
|
50
|
+
sendJSON(res, 200, metrics);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
sendError(res, 500, err.message);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* POST /cache/clear - Clear cache
|
|
58
|
+
*/
|
|
59
|
+
async function handleCacheClear(req, res) {
|
|
60
|
+
try {
|
|
61
|
+
await engine.clearCache();
|
|
62
|
+
sendJSON(res, 200, { message: 'Cache cleared' });
|
|
63
|
+
} catch (err) {
|
|
64
|
+
sendError(res, 500, err.message);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* GET /plugins - List plugins
|
|
70
|
+
*/
|
|
71
|
+
function handlePlugins(req, res) {
|
|
72
|
+
try {
|
|
73
|
+
const plugins = engine.getPlugins();
|
|
74
|
+
sendJSON(res, 200, { plugins });
|
|
75
|
+
} catch (err) {
|
|
76
|
+
sendError(res, 500, err.message);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* POST /config - Update configuration
|
|
82
|
+
*/
|
|
83
|
+
function handleConfig(req, res, body) {
|
|
84
|
+
if (!body) {
|
|
85
|
+
return sendError(res, 400, 'Missing request body');
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
engine.updateConfig(body);
|
|
89
|
+
sendJSON(res, 200, { message: 'Configuration updated' });
|
|
90
|
+
} catch (err) {
|
|
91
|
+
sendError(res, 500, err.message);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Route a request to the appropriate handler.
|
|
97
|
+
* @param {import('node:http').IncomingMessage} req
|
|
98
|
+
* @param {import('node:http').ServerResponse} res
|
|
99
|
+
* @param {Object|null} body - Parsed request body
|
|
100
|
+
*/
|
|
101
|
+
async function route(req, res, body) {
|
|
102
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
103
|
+
const path = url.pathname;
|
|
104
|
+
const method = req.method;
|
|
105
|
+
|
|
106
|
+
// Match routes
|
|
107
|
+
if (path === '/task' && method === 'POST') return handleTask(req, res, body);
|
|
108
|
+
if (path === '/status' && method === 'GET') return handleStatus(req, res);
|
|
109
|
+
if (path === '/analytics' && method === 'GET') return handleAnalytics(req, res);
|
|
110
|
+
if (path === '/cache/clear' && method === 'POST') return handleCacheClear(req, res);
|
|
111
|
+
if (path === '/plugins' && method === 'GET') return handlePlugins(req, res);
|
|
112
|
+
if (path === '/config' && method === 'POST') return handleConfig(req, res);
|
|
113
|
+
|
|
114
|
+
// 404
|
|
115
|
+
sendError(res, 404, `Not found: ${method} ${path}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { route, handleTask, handleStatus, handleAnalytics, handleCacheClear, handlePlugins, handleConfig };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Send a JSON response.
|
|
123
|
+
* @param {import('node:http').ServerResponse} res
|
|
124
|
+
* @param {number} status
|
|
125
|
+
* @param {*} data
|
|
126
|
+
*/
|
|
127
|
+
function sendJSON(res, status, data) {
|
|
128
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
129
|
+
res.end(JSON.stringify(data, null, 2));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Send an error response.
|
|
134
|
+
* @param {import('node:http').ServerResponse} res
|
|
135
|
+
* @param {number} status
|
|
136
|
+
* @param {string} message
|
|
137
|
+
*/
|
|
138
|
+
function sendError(res, status, message) {
|
|
139
|
+
sendJSON(res, status, { error: { message, status } });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export default { createRoutes };
|