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,150 @@
1
+ /**
2
+ * @fileoverview Rate limit monitoring and protection.
3
+ * @module rate/shield
4
+ */
5
+
6
+ import { EventEmitter } from 'node:events';
7
+ import { TokenBucket, SlidingWindow } from './limiter.mjs';
8
+
9
+ /**
10
+ * @typedef {Object} ProviderLimits
11
+ * @property {number} [requestsPerMinute] - RPM limit
12
+ * @property {number} [tokensPerMinute] - TPM limit
13
+ * @property {number} [requestsPerDay] - Daily request limit
14
+ */
15
+
16
+ export class RateShield extends EventEmitter {
17
+ /**
18
+ * @param {Object} [options]
19
+ * @param {Object<string, ProviderLimits>} [options.providers] - Per-provider limits
20
+ * @param {number} [options.warningThreshold=0.8] - Warn at this % of limit
21
+ */
22
+ constructor(options = {}) {
23
+ super();
24
+ this._warningThreshold = options.warningThreshold ?? 0.8;
25
+ /** @type {Map<string, { bucket: TokenBucket, window: SlidingWindow, limits: ProviderLimits, usage: Object }>} */
26
+ this._providers = new Map();
27
+
28
+ if (options.providers) {
29
+ for (const [name, limits] of Object.entries(options.providers)) {
30
+ this.addProvider(name, limits);
31
+ }
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Add a provider with rate limits.
37
+ * @param {string} name - Provider name
38
+ * @param {ProviderLimits} limits
39
+ */
40
+ addProvider(name, limits) {
41
+ const rpm = limits.requestsPerMinute ?? 60;
42
+ const bucket = new TokenBucket({ capacity: rpm, refillRate: rpm / 60 });
43
+ const window = new SlidingWindow({ maxRequests: rpm, windowMs: 60000 });
44
+ this._providers.set(name, {
45
+ bucket,
46
+ window,
47
+ limits,
48
+ usage: { requests: 0, tokens: 0, rejected: 0, lastReset: Date.now() },
49
+ });
50
+ }
51
+
52
+ /**
53
+ * Check if a request is allowed for a provider.
54
+ * @param {string} provider - Provider name
55
+ * @param {number} [tokens=0] - Token count for this request
56
+ * @returns {Object} { allowed: boolean, reason?: string, waitMs?: number }
57
+ */
58
+ check(provider, tokens = 0) {
59
+ const entry = this._providers.get(provider);
60
+ if (!entry) {
61
+ return { allowed: true };
62
+ }
63
+
64
+ // Check sliding window
65
+ if (!entry.window.consume()) {
66
+ entry.usage.rejected++;
67
+ const waitMs = entry.window.waitFor();
68
+ this.emit('rate:rejected', { provider, waitMs });
69
+ return { allowed: false, reason: 'Rate limit exceeded', waitMs };
70
+ }
71
+
72
+ // Check token bucket
73
+ if (!entry.bucket.consume()) {
74
+ entry.usage.rejected++;
75
+ const waitMs = entry.bucket.waitFor();
76
+ this.emit('rate:rejected', { provider, waitMs });
77
+ return { allowed: false, reason: 'Token bucket empty', waitMs };
78
+ }
79
+
80
+ entry.usage.requests++;
81
+ entry.usage.tokens += tokens;
82
+
83
+ // Check warning threshold
84
+ const usageRatio = entry.usage.requests / (entry.limits.requestsPerMinute ?? 60);
85
+ if (usageRatio >= this._warningThreshold) {
86
+ this.emit('rate:warning', { provider, usageRatio });
87
+ }
88
+
89
+ return { allowed: true };
90
+ }
91
+
92
+ /**
93
+ * Record token usage for a provider.
94
+ * @param {string} provider
95
+ * @param {number} tokens
96
+ */
97
+ recordUsage(provider, tokens) {
98
+ const entry = this._providers.get(provider);
99
+ if (entry) {
100
+ entry.usage.tokens += tokens;
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Get usage stats for a provider.
106
+ * @param {string} provider
107
+ * @returns {Object|null}
108
+ */
109
+ getUsage(provider) {
110
+ const entry = this._providers.get(provider);
111
+ if (!entry) return null;
112
+ return { ...entry.usage };
113
+ }
114
+
115
+ /**
116
+ * Get all provider stats.
117
+ * @returns {Object}
118
+ */
119
+ getAllUsage() {
120
+ const result = {};
121
+ for (const [name, entry] of this._providers) {
122
+ result[name] = { ...entry.usage };
123
+ }
124
+ return result;
125
+ }
126
+
127
+ /**
128
+ * Reset usage counters for a provider.
129
+ * @param {string} provider
130
+ */
131
+ reset(provider) {
132
+ const entry = this._providers.get(provider);
133
+ if (entry) {
134
+ entry.usage = { requests: 0, tokens: 0, rejected: 0, lastReset: Date.now() };
135
+ entry.bucket.reset();
136
+ entry.window.reset();
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Reset all providers.
142
+ */
143
+ resetAll() {
144
+ for (const name of this._providers.keys()) {
145
+ this.reset(name);
146
+ }
147
+ }
148
+ }
149
+
150
+ export default RateShield;
@@ -0,0 +1,164 @@
1
+ /**
2
+ * @fileoverview PrepiScript executor - runs parsed scripts.
3
+ * @module script/executor
4
+ */
5
+
6
+ import { EventEmitter } from 'node:events';
7
+
8
+ export class ScriptExecutor extends EventEmitter {
9
+ /**
10
+ * @param {Object} [options]
11
+ * @param {Object} [options.tools] - Tool instances for execution
12
+ */
13
+ constructor(options = {}) {
14
+ super();
15
+ this._tools = options.tools || {};
16
+ this._results = new Map();
17
+ }
18
+
19
+ /**
20
+ * Execute a parsed PrepiScript.
21
+ * @param {import('./parser.mjs').ParsedScript} script - Parsed script
22
+ * @param {Object} [context] - Execution context
23
+ * @returns {Promise<Object>} Execution result
24
+ */
25
+ async execute(script, context = {}) {
26
+ const variables = { ...script.variables, ...context };
27
+ const stepResults = [];
28
+ let lastResult = null;
29
+
30
+ for (const step of script.steps) {
31
+ this.emit('step:start', { keyword: step.keyword, line: step.line });
32
+
33
+ try {
34
+ const result = await this._executeStep(step, variables, lastResult);
35
+ stepResults.push({ step, result, success: true });
36
+ lastResult = result;
37
+
38
+ // Store in variables if argument looks like a variable assignment
39
+ if (step.argument?.includes('->')) {
40
+ const [_, varName] = step.argument.split('->').map(s => s.trim());
41
+ if (varName) variables[varName] = result;
42
+ }
43
+
44
+ this.emit('step:complete', { keyword: step.keyword, line: step.line, result });
45
+ } catch (err) {
46
+ stepResults.push({ step, error: err.message, success: false });
47
+ this.emit('step:error', { keyword: step.keyword, line: step.line, error: err.message });
48
+
49
+ // Stop on error unless it's a non-critical step
50
+ if (step.keyword !== 'CACHE' && step.keyword !== 'FILTER') {
51
+ break;
52
+ }
53
+ }
54
+ }
55
+
56
+ return {
57
+ scriptName: script.name,
58
+ steps: stepResults,
59
+ variables,
60
+ success: stepResults.every(r => r.success),
61
+ output: lastResult,
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Execute a single step.
67
+ * @param {Object} step
68
+ * @param {Object} variables
69
+ * @param {*} lastResult
70
+ * @returns {Promise<*>}
71
+ * @private
72
+ */
73
+ async _executeStep(step, variables, lastResult) {
74
+ const arg = this._resolveVariables(step.argument, variables);
75
+
76
+ switch (step.keyword) {
77
+ case 'SEARCH':
78
+ return this._executeSearch(arg, step.options);
79
+ case 'EXTRACT':
80
+ return this._executeExtract(arg, lastResult, step.options);
81
+ case 'FORMAT':
82
+ return this._executeFormat(arg, lastResult, step.options);
83
+ case 'DELIVER':
84
+ return this._executeDeliver(arg, lastResult, step.options);
85
+ case 'CACHE':
86
+ return { cached: true, key: arg };
87
+ case 'FILTER':
88
+ return this._executeFilter(arg, lastResult, step.options);
89
+ case 'MERGE':
90
+ return this._executeMerge(lastResult, step.options);
91
+ case 'IF':
92
+ return this._executeIf(arg, lastResult, step.options);
93
+ case 'OUTPUT':
94
+ return lastResult;
95
+ default:
96
+ throw new Error(`Unknown step keyword: ${step.keyword}`);
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Resolve variable references in a string.
102
+ * @param {string} str
103
+ * @param {Object} variables
104
+ * @returns {string}
105
+ * @private
106
+ */
107
+ _resolveVariables(str, variables) {
108
+ if (!str) return str;
109
+ return str.replace(/\$\{(\w+)\}/g, (_, name) => {
110
+ return variables[name] !== undefined ? String(variables[name]) : `\${${name}}`;
111
+ });
112
+ }
113
+
114
+ async _executeSearch(query, options) {
115
+ if (this._tools.search) {
116
+ return this._tools.search(query, options);
117
+ }
118
+ return { query, results: [], source: 'no-search-tool' };
119
+ }
120
+
121
+ async _executeExtract(target, source, options) {
122
+ if (this._tools.extract && source) {
123
+ return this._tools.extract(target, source, options);
124
+ }
125
+ return { target, extracted: source };
126
+ }
127
+
128
+ async _executeFormat(format, data, options) {
129
+ if (format === 'json') return JSON.stringify(data, null, 2);
130
+ if (format === 'text') return typeof data === 'string' ? data : JSON.stringify(data);
131
+ if (format === 'markdown') return typeof data === 'object' ? JSON.stringify(data, null, 2) : String(data);
132
+ return data;
133
+ }
134
+
135
+ async _executeDeliver(target, data, options) {
136
+ return { target, data, delivered: true };
137
+ }
138
+
139
+ async _executeFilter(filter, data, options) {
140
+ if (!data || !Array.isArray(data)) return data;
141
+ return data; // In a real implementation, apply filter criteria
142
+ }
143
+
144
+ async _executeMerge(data, options) {
145
+ return { merged: true, data };
146
+ }
147
+
148
+ async _executeIf(condition, data, options) {
149
+ // Simple condition evaluation
150
+ if (condition && data) return data;
151
+ return null;
152
+ }
153
+
154
+ /**
155
+ * Register a tool for script execution.
156
+ * @param {string} name
157
+ * @param {Function} fn
158
+ */
159
+ registerTool(name, fn) {
160
+ this._tools[name] = fn;
161
+ }
162
+ }
163
+
164
+ export default ScriptExecutor;
@@ -0,0 +1,134 @@
1
+ /**
2
+ * @fileoverview PrepiScript parser - custom task definition language.
3
+ * @module script/parser
4
+ */
5
+
6
+ /**
7
+ * @typedef {Object} ParsedScript
8
+ * @property {string} name - Script name
9
+ * @property {Object[]} steps - Parsed steps
10
+ * @property {Object} variables - Variable declarations
11
+ * @property {Object[]} errors - Parse errors
12
+ */
13
+
14
+ /**
15
+ * @typedef {Object} ScriptStep
16
+ * @property {string} keyword - Step keyword (SEARCH, EXTRACT, FORMAT, DELIVER, etc.)
17
+ * @property {string} argument - Step argument
18
+ * @property {Object} options - Step options
19
+ * @property {number} line - Source line number
20
+ */
21
+
22
+ /** Valid keywords */
23
+ const KEYWORDS = ['TASK', 'SEARCH', 'EXTRACT', 'FORMAT', 'DELIVER', 'CACHE', 'FILTER', 'MERGE', 'IF', 'SET', 'OUTPUT'];
24
+
25
+ /**
26
+ * Parse a PrepiScript string.
27
+ * @param {string} script - Script source
28
+ * @returns {ParsedScript}
29
+ */
30
+ export function parse(script) {
31
+ if (!script || typeof script !== 'string') {
32
+ return { name: '', steps: [], variables: {}, errors: [{ line: 0, message: 'Empty script' }] };
33
+ }
34
+
35
+ const lines = script.split('\n');
36
+ const steps = [];
37
+ const variables = {};
38
+ const errors = [];
39
+ let name = '';
40
+
41
+ for (let i = 0; i < lines.length; i++) {
42
+ const lineNum = i + 1;
43
+ const line = lines[i].trim();
44
+
45
+ // Skip empty lines and comments
46
+ if (!line || line.startsWith('#') || line.startsWith('//')) continue;
47
+
48
+ // Parse keyword and argument
49
+ const match = line.match(/^(\w+)\s*(?:["']([^"']*)["']|\s+(.+))?$/);
50
+ if (!match) {
51
+ if (line.length > 0) {
52
+ errors.push({ line: lineNum, message: `Invalid syntax: ${line.substring(0, 50)}` });
53
+ }
54
+ continue;
55
+ }
56
+
57
+ const keyword = match[1].toUpperCase();
58
+ const argument = (match[2] || match[3] || '').trim();
59
+
60
+ if (!KEYWORDS.includes(keyword)) {
61
+ errors.push({ line: lineNum, message: `Unknown keyword: ${keyword}` });
62
+ continue;
63
+ }
64
+
65
+ if (keyword === 'TASK') {
66
+ name = argument;
67
+ continue;
68
+ }
69
+
70
+ // Parse options (key=value pairs)
71
+ const options = {};
72
+ const optPattern = /(\w+)=["']?([^"'\s]+)["']?/g;
73
+ let optMatch;
74
+ const remaining = line.substring(match[0].indexOf(argument || '') + (argument || '').length);
75
+ while ((optMatch = optPattern.exec(remaining)) !== null) {
76
+ options[optMatch[1]] = optMatch[2];
77
+ }
78
+
79
+ if (keyword === 'SET') {
80
+ const varMatch = argument.match(/^(\w+)\s*=\s*(.+)$/);
81
+ if (varMatch) {
82
+ variables[varMatch[1]] = varMatch[2];
83
+ } else {
84
+ errors.push({ line: lineNum, message: `Invalid SET syntax: ${argument}` });
85
+ }
86
+ continue;
87
+ }
88
+
89
+ steps.push({
90
+ keyword,
91
+ argument,
92
+ options,
93
+ line: lineNum,
94
+ });
95
+ }
96
+
97
+ if (!name && steps.length > 0) {
98
+ errors.unshift({ line: 0, message: 'Missing TASK declaration' });
99
+ }
100
+
101
+ return { name, steps, variables, errors };
102
+ }
103
+
104
+ /**
105
+ * Validate a parsed script.
106
+ * @param {ParsedScript} parsed
107
+ * @returns {{ valid: boolean, errors: string[] }}
108
+ */
109
+ export function validate(parsed) {
110
+ const errors = [];
111
+
112
+ if (parsed.errors.length > 0) {
113
+ errors.push(...parsed.errors.map(e => `Line ${e.line}: ${e.message}`));
114
+ }
115
+
116
+ if (!parsed.name) {
117
+ errors.push('Script must have a TASK name');
118
+ }
119
+
120
+ if (parsed.steps.length === 0) {
121
+ errors.push('Script must have at least one step');
122
+ }
123
+
124
+ // Check that DELIVER has a target
125
+ for (const step of parsed.steps) {
126
+ if (step.keyword === 'DELIVER' && !step.argument) {
127
+ errors.push(`Line ${step.line}: DELIVER requires a target`);
128
+ }
129
+ }
130
+
131
+ return { valid: errors.length === 0, errors };
132
+ }
133
+
134
+ export default { parse, validate };
@@ -0,0 +1,108 @@
1
+ /**
2
+ * @fileoverview PII detection and redaction.
3
+ * @module security/privacy
4
+ */
5
+
6
+ /**
7
+ * @typedef {Object} PIIMatch
8
+ * @property {string} type - PII type
9
+ * @property {string} value - Matched value
10
+ * @property {number} start - Start index
11
+ * @property {number} end - End index
12
+ */
13
+
14
+ /** PII detection patterns */
15
+ const PII_PATTERNS = {
16
+ email: {
17
+ pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
18
+ replacement: '[EMAIL]',
19
+ },
20
+ phone: {
21
+ pattern: /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g,
22
+ replacement: '[PHONE]',
23
+ },
24
+ ssn: {
25
+ pattern: /\b\d{3}[-.\s]?\d{2}[-.\s]?\d{4}\b/g,
26
+ replacement: '[SSN]',
27
+ },
28
+ creditCard: {
29
+ pattern: /\b(?:\d{4}[-.\s]?){3}\d{4}\b/g,
30
+ replacement: '[CREDIT_CARD]',
31
+ },
32
+ ipAddress: {
33
+ pattern: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g,
34
+ replacement: '[IP]',
35
+ },
36
+ // US addresses (simplified)
37
+ address: {
38
+ pattern: /\b\d{1,5}\s+[\w\s]+(?:street|st|avenue|ave|road|rd|boulevard|blvd|drive|dr|lane|ln|way|court|ct)\b/gi,
39
+ replacement: '[ADDRESS]',
40
+ },
41
+ };
42
+
43
+ /**
44
+ * Detect PII in text.
45
+ * @param {string} text
46
+ * @returns {PIIMatch[]}
47
+ */
48
+ export function detectPII(text) {
49
+ if (!text || typeof text !== 'string') return [];
50
+
51
+ const matches = [];
52
+ for (const [type, { pattern }] of Object.entries(PII_PATTERNS)) {
53
+ const regex = new RegExp(pattern.source, pattern.flags);
54
+ let match;
55
+ while ((match = regex.exec(text)) !== null) {
56
+ matches.push({
57
+ type,
58
+ value: match[0],
59
+ start: match.index,
60
+ end: match.index + match[0].length,
61
+ });
62
+ }
63
+ }
64
+
65
+ return matches.sort((a, b) => a.start - b.start);
66
+ }
67
+
68
+ /**
69
+ * Redact PII from text.
70
+ * @param {string} text
71
+ * @param {Object} [options]
72
+ * @param {string[]} [options.types] - PII types to redact (default: all)
73
+ * @param {string} [options.replacement] - Custom replacement marker
74
+ * @returns {{ text: string, redacted: number }}
75
+ */
76
+ export function redactPII(text, options = {}) {
77
+ if (!text || typeof text !== 'string') {
78
+ return { text: '', redacted: 0 };
79
+ }
80
+
81
+ const types = options.types || Object.keys(PII_PATTERNS);
82
+ let result = text;
83
+ let redacted = 0;
84
+
85
+ for (const type of types) {
86
+ const config = PII_PATTERNS[type];
87
+ if (!config) continue;
88
+ const replacement = options.replacement || config.replacement;
89
+ const regex = new RegExp(config.pattern.source, config.pattern.flags);
90
+ const before = result;
91
+ result = result.replace(regex, replacement);
92
+ const matches = before.match(regex);
93
+ if (matches) redacted += matches.length;
94
+ }
95
+
96
+ return { text: result, redacted };
97
+ }
98
+
99
+ /**
100
+ * Check if text contains PII.
101
+ * @param {string} text
102
+ * @returns {boolean}
103
+ */
104
+ export function containsPII(text) {
105
+ return detectPII(text).length > 0;
106
+ }
107
+
108
+ export default { detectPII, redactPII, containsPII };
@@ -0,0 +1,133 @@
1
+ /**
2
+ * @fileoverview Input/output sanitization.
3
+ * @module security/sanitizer
4
+ */
5
+
6
+ /**
7
+ * @typedef {Object} SanitizeResult
8
+ * @property {string} text - Sanitized text
9
+ * @property {boolean} modified - Whether text was modified
10
+ * @property {string[]} issues - Issues found
11
+ */
12
+
13
+ /** Injection patterns to detect and strip */
14
+ const INJECTION_PATTERNS = [
15
+ // Prompt injection attempts
16
+ /ignore\s+(?:all\s+)?(?:previous|prior|above)\s+(?:instructions?|prompts?|rules?)/gi,
17
+ /you\s+are\s+now\s+(?:a|an|the)/gi,
18
+ /system\s*:\s*/gi,
19
+ /assistant\s*:\s*/gi,
20
+ /user\s*:\s*/gi,
21
+ /\[INST\]/gi,
22
+ /\[\/INST\]/gi,
23
+ /<\|im_start\|>/gi,
24
+ /<\|im_end\|>/gi,
25
+ /###\s*(?:system|instruction)/gi,
26
+ // Code injection
27
+ /```(?:bash|sh|shell)\s*\n[\s\S]*?rm\s+-rf/gi,
28
+ /```(?:bash|sh|shell)\s*\n[\s\S]*?curl\s+.*\|\s*sh/gi,
29
+ ];
30
+
31
+ /**
32
+ * Sanitize user input.
33
+ * @param {string} input - Raw user input
34
+ * @param {Object} [options]
35
+ * @param {boolean} [options.stripInjection=true] - Strip injection patterns
36
+ * @param {number} [options.maxLength=100000] - Max input length
37
+ * @returns {SanitizeResult}
38
+ */
39
+ export function sanitize(input, options = {}) {
40
+ if (!input || typeof input !== 'string') {
41
+ return { text: '', modified: false, issues: ['Empty or invalid input'] };
42
+ }
43
+
44
+ const { stripInjection = true, maxLength = 100000 } = options;
45
+ let text = input;
46
+ let modified = false;
47
+ const issues = [];
48
+
49
+ // Truncate if too long
50
+ if (text.length > maxLength) {
51
+ text = text.substring(0, maxLength);
52
+ modified = true;
53
+ issues.push(`Input truncated to ${maxLength} characters`);
54
+ }
55
+
56
+ // Strip null bytes
57
+ if (text.includes('\0')) {
58
+ text = text.replace(/\0/g, '');
59
+ modified = true;
60
+ issues.push('Null bytes removed');
61
+ }
62
+
63
+ // Strip injection patterns
64
+ if (stripInjection) {
65
+ for (const pattern of INJECTION_PATTERNS) {
66
+ if (pattern.test(text)) {
67
+ text = text.replace(pattern, '[REDACTED]');
68
+ modified = true;
69
+ issues.push(`Injection pattern detected and redacted: ${pattern.source.substring(0, 40)}`);
70
+ }
71
+ }
72
+ }
73
+
74
+ return { text, modified, issues };
75
+ }
76
+
77
+ /**
78
+ * Validate task parameters.
79
+ * @param {Object} params - Task parameters
80
+ * @returns {{ valid: boolean, errors: string[] }}
81
+ */
82
+ export function validateParams(params) {
83
+ const errors = [];
84
+
85
+ if (!params || typeof params !== 'object') {
86
+ return { valid: false, errors: ['Parameters must be an object'] };
87
+ }
88
+
89
+ if (params.query && typeof params.query !== 'string') {
90
+ errors.push('Query must be a string');
91
+ }
92
+ if (params.query && params.query.length > 100000) {
93
+ errors.push('Query exceeds maximum length');
94
+ }
95
+ if (params.type && typeof params.type !== 'string') {
96
+ errors.push('Type must be a string');
97
+ }
98
+
99
+ return { valid: errors.length === 0, errors };
100
+ }
101
+
102
+ /**
103
+ * Sanitize output before returning to user.
104
+ * @param {string} output
105
+ * @returns {SanitizeResult}
106
+ */
107
+ export function sanitizeOutput(output) {
108
+ if (!output || typeof output !== 'string') {
109
+ return { text: '', modified: false, issues: ['Empty output'] };
110
+ }
111
+
112
+ let text = output;
113
+ let modified = false;
114
+ const issues = [];
115
+
116
+ // Remove any system-level markers that might have leaked
117
+ const leakPatterns = [
118
+ /system\s*:\s*.*$/gim,
119
+ /\[INST\][\s\S]*?\[\/INST\]/gi,
120
+ ];
121
+
122
+ for (const pattern of leakPatterns) {
123
+ if (pattern.test(text)) {
124
+ text = text.replace(pattern, '');
125
+ modified = true;
126
+ issues.push('System-level content leaked in output');
127
+ }
128
+ }
129
+
130
+ return { text: text.trim(), modified, issues };
131
+ }
132
+
133
+ export default { sanitize, validateParams, sanitizeOutput };