@nextsparkjs/plugin-ai 0.1.0-beta.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.
Files changed (34) hide show
  1. package/.env.example +79 -0
  2. package/README.md +529 -0
  3. package/api/README.md +65 -0
  4. package/api/ai-history/[id]/route.ts +112 -0
  5. package/api/embeddings/route.ts +129 -0
  6. package/api/generate/route.ts +160 -0
  7. package/docs/01-getting-started/01-introduction.md +237 -0
  8. package/docs/01-getting-started/02-installation.md +447 -0
  9. package/docs/01-getting-started/03-configuration.md +416 -0
  10. package/docs/02-features/01-text-generation.md +523 -0
  11. package/docs/02-features/02-embeddings.md +241 -0
  12. package/docs/02-features/03-ai-history.md +549 -0
  13. package/docs/03-advanced-usage/01-core-utilities.md +500 -0
  14. package/docs/04-use-cases/01-content-generation.md +453 -0
  15. package/entities/ai-history/ai-history.config.ts +123 -0
  16. package/entities/ai-history/ai-history.fields.ts +330 -0
  17. package/entities/ai-history/messages/en.json +56 -0
  18. package/entities/ai-history/messages/es.json +56 -0
  19. package/entities/ai-history/migrations/001_ai_history_table.sql +167 -0
  20. package/entities/ai-history/migrations/002_ai_history_metas.sql +103 -0
  21. package/lib/ai-history-meta-service.ts +379 -0
  22. package/lib/ai-history-service.ts +391 -0
  23. package/lib/ai-sdk.ts +7 -0
  24. package/lib/core-utils.ts +217 -0
  25. package/lib/plugin-env.ts +252 -0
  26. package/lib/sanitize.ts +122 -0
  27. package/lib/save-example.ts +237 -0
  28. package/lib/server-env.ts +104 -0
  29. package/package.json +23 -0
  30. package/plugin.config.ts +55 -0
  31. package/public/docs/login-404-error.png +0 -0
  32. package/tsconfig.json +47 -0
  33. package/tsconfig.tsbuildinfo +1 -0
  34. package/types/ai.types.ts +51 -0
@@ -0,0 +1,252 @@
1
+ /**
2
+ * AI Plugin Environment Configuration (Server-Only)
3
+ *
4
+ * Uses centralized plugin environment loader from core
5
+ * Provides type-safe access to AI provider credentials and configuration
6
+ */
7
+
8
+ import { getPluginEnv } from '@nextsparkjs/core/lib/plugins/env-loader'
9
+
10
+ interface AIPluginEnvConfig {
11
+ // AI provider credentials
12
+ ANTHROPIC_API_KEY?: string
13
+ OPENAI_API_KEY?: string
14
+
15
+ // Ollama configuration
16
+ OLLAMA_BASE_URL?: string
17
+ OLLAMA_DEFAULT_MODEL?: string
18
+
19
+ // AI provider selection
20
+ USE_LOCAL_AI?: string
21
+ DEFAULT_CLOUD_MODEL?: string
22
+
23
+ // Plugin configuration
24
+ AI_PLUGIN_ENABLED?: string
25
+ AI_PLUGIN_DEBUG?: string
26
+ AI_PLUGIN_DEFAULT_PROVIDER?: string
27
+ AI_PLUGIN_MAX_TOKENS?: string
28
+ AI_PLUGIN_DEFAULT_TEMPERATURE?: string
29
+
30
+ // Cost tracking
31
+ AI_PLUGIN_COST_TRACKING_ENABLED?: string
32
+ AI_PLUGIN_DAILY_COST_LIMIT?: string
33
+ AI_PLUGIN_MONTHLY_COST_LIMIT?: string
34
+
35
+ // Rate limiting
36
+ AI_PLUGIN_RATE_LIMIT_REQUESTS_PER_MINUTE?: string
37
+ AI_PLUGIN_RATE_LIMIT_TOKENS_PER_MINUTE?: string
38
+ }
39
+
40
+ class PluginEnvironment {
41
+ private static instance: PluginEnvironment
42
+ private config: AIPluginEnvConfig = {}
43
+ private loaded = false
44
+
45
+ private constructor() {
46
+ this.loadEnvironment()
47
+ }
48
+
49
+ public static getInstance(): PluginEnvironment {
50
+ if (!PluginEnvironment.instance) {
51
+ PluginEnvironment.instance = new PluginEnvironment()
52
+ }
53
+ return PluginEnvironment.instance
54
+ }
55
+
56
+ private loadEnvironment(forceReload: boolean = false): void {
57
+ if (this.loaded && !forceReload) return
58
+
59
+ try {
60
+ // Use centralized plugin env loader
61
+ const env = getPluginEnv('ai')
62
+
63
+ this.config = {
64
+ // AI provider credentials
65
+ ANTHROPIC_API_KEY: env.ANTHROPIC_API_KEY,
66
+ OPENAI_API_KEY: env.OPENAI_API_KEY,
67
+
68
+ // Ollama configuration
69
+ OLLAMA_BASE_URL: env.OLLAMA_BASE_URL || 'http://localhost:11434',
70
+ OLLAMA_DEFAULT_MODEL: env.OLLAMA_DEFAULT_MODEL || 'llama3.2:3b',
71
+
72
+ // AI provider selection
73
+ USE_LOCAL_AI: env.USE_LOCAL_AI || 'false',
74
+ DEFAULT_CLOUD_MODEL: env.DEFAULT_CLOUD_MODEL || 'claude-sonnet-4-5-20250929',
75
+
76
+ // Plugin configuration
77
+ AI_PLUGIN_ENABLED: env.AI_PLUGIN_ENABLED || 'true',
78
+ AI_PLUGIN_DEBUG: env.AI_PLUGIN_DEBUG || 'false',
79
+ AI_PLUGIN_DEFAULT_PROVIDER: env.AI_PLUGIN_DEFAULT_PROVIDER || 'anthropic',
80
+ AI_PLUGIN_MAX_TOKENS: env.AI_PLUGIN_MAX_TOKENS || '4000',
81
+ AI_PLUGIN_DEFAULT_TEMPERATURE: env.AI_PLUGIN_DEFAULT_TEMPERATURE || '0.7',
82
+ AI_PLUGIN_COST_TRACKING_ENABLED: env.AI_PLUGIN_COST_TRACKING_ENABLED || 'true',
83
+ AI_PLUGIN_DAILY_COST_LIMIT: env.AI_PLUGIN_DAILY_COST_LIMIT || '10.00',
84
+ AI_PLUGIN_MONTHLY_COST_LIMIT: env.AI_PLUGIN_MONTHLY_COST_LIMIT || '100.00',
85
+ AI_PLUGIN_RATE_LIMIT_REQUESTS_PER_MINUTE: env.AI_PLUGIN_RATE_LIMIT_REQUESTS_PER_MINUTE || '60',
86
+ AI_PLUGIN_RATE_LIMIT_TOKENS_PER_MINUTE: env.AI_PLUGIN_RATE_LIMIT_TOKENS_PER_MINUTE || '50000'
87
+ }
88
+
89
+ this.logLoadedConfiguration()
90
+ this.loaded = true
91
+ } catch (error) {
92
+ console.error('[AI Plugin] Failed to load environment:', error)
93
+ this.loaded = true
94
+ }
95
+ }
96
+
97
+ private logLoadedConfiguration(): void {
98
+ if (process.env.NODE_ENV === 'development') {
99
+ console.log('[AI Plugin] ℹ️ Plugin Environment Configuration:')
100
+ console.log(' → AI Provider Credentials:')
101
+ console.log(` - ANTHROPIC_API_KEY: ${this.config.ANTHROPIC_API_KEY ? '✓ set' : '✗ not set'}`)
102
+ console.log(` - OPENAI_API_KEY: ${this.config.OPENAI_API_KEY ? '✓ set' : '✗ not set'}`)
103
+ console.log(' → AI Provider Selection:')
104
+ console.log(` - USE_LOCAL_AI: ${this.config.USE_LOCAL_AI}`)
105
+ console.log(` - DEFAULT_CLOUD_MODEL: ${this.config.DEFAULT_CLOUD_MODEL}`)
106
+ console.log(' → Ollama Configuration:')
107
+ console.log(` - OLLAMA_BASE_URL: ${this.config.OLLAMA_BASE_URL}`)
108
+ console.log(` - OLLAMA_DEFAULT_MODEL: ${this.config.OLLAMA_DEFAULT_MODEL}`)
109
+ console.log(' → Plugin Settings:')
110
+ const pluginVars = [
111
+ 'AI_PLUGIN_ENABLED', 'AI_PLUGIN_DEBUG', 'AI_PLUGIN_DEFAULT_PROVIDER',
112
+ 'AI_PLUGIN_MAX_TOKENS', 'AI_PLUGIN_DEFAULT_TEMPERATURE',
113
+ 'AI_PLUGIN_COST_TRACKING_ENABLED', 'AI_PLUGIN_DAILY_COST_LIMIT',
114
+ 'AI_PLUGIN_MONTHLY_COST_LIMIT', 'AI_PLUGIN_RATE_LIMIT_REQUESTS_PER_MINUTE',
115
+ 'AI_PLUGIN_RATE_LIMIT_TOKENS_PER_MINUTE'
116
+ ]
117
+ for (const v of pluginVars) {
118
+ const value = this.config[v as keyof AIPluginEnvConfig]
119
+ console.log(` - ${v}: ${value || 'default'}`)
120
+ }
121
+ console.log()
122
+ }
123
+ }
124
+
125
+ public getConfig(): AIPluginEnvConfig {
126
+ if (!this.loaded) {
127
+ this.loadEnvironment()
128
+ }
129
+ return this.config
130
+ }
131
+
132
+ // Helper methods
133
+ public getAnthropicApiKey(): string | undefined {
134
+ return this.getConfig().ANTHROPIC_API_KEY
135
+ }
136
+
137
+ public getOpenAiApiKey(): string | undefined {
138
+ return this.getConfig().OPENAI_API_KEY
139
+ }
140
+
141
+ public getOllamaBaseUrl(): string {
142
+ return this.getConfig().OLLAMA_BASE_URL || 'http://localhost:11434'
143
+ }
144
+
145
+ public getOllamaDefaultModel(): string {
146
+ return this.getConfig().OLLAMA_DEFAULT_MODEL || 'llama3.2:3b'
147
+ }
148
+
149
+ public isUseLocalAI(): boolean {
150
+ return this.getConfig().USE_LOCAL_AI === 'true'
151
+ }
152
+
153
+ public getDefaultCloudModel(): string {
154
+ return this.getConfig().DEFAULT_CLOUD_MODEL || 'claude-sonnet-4-5-20250929'
155
+ }
156
+
157
+ public getDefaultProvider(): string {
158
+ return this.getConfig().AI_PLUGIN_DEFAULT_PROVIDER || 'anthropic'
159
+ }
160
+
161
+ public getDefaultModel(): string {
162
+ const useLocal = this.isUseLocalAI()
163
+ const ollamaModel = this.getOllamaDefaultModel()
164
+ const cloudModel = this.getDefaultCloudModel()
165
+ console.log(`🔍 [getDefaultModel] USE_LOCAL_AI=${useLocal}, ollamaModel=${ollamaModel}, cloudModel=${cloudModel}`)
166
+ const selectedModel = useLocal ? ollamaModel : cloudModel
167
+ console.log(`🔍 [getDefaultModel] Returning: ${selectedModel}`)
168
+ return selectedModel
169
+ }
170
+
171
+ public getMaxTokens(): number {
172
+ return parseInt(this.getConfig().AI_PLUGIN_MAX_TOKENS || '4000', 10)
173
+ }
174
+
175
+ public getDefaultTemperature(): number {
176
+ return parseFloat(this.getConfig().AI_PLUGIN_DEFAULT_TEMPERATURE || '0.7')
177
+ }
178
+
179
+ public isCostTrackingEnabled(): boolean {
180
+ return this.getConfig().AI_PLUGIN_COST_TRACKING_ENABLED === 'true'
181
+ }
182
+
183
+ public getDailyCostLimit(): number {
184
+ return parseFloat(this.getConfig().AI_PLUGIN_DAILY_COST_LIMIT || '10.00')
185
+ }
186
+
187
+ public getMonthlyCostLimit(): number {
188
+ return parseFloat(this.getConfig().AI_PLUGIN_MONTHLY_COST_LIMIT || '100.00')
189
+ }
190
+
191
+ public getRateLimitRequestsPerMinute(): number {
192
+ return parseInt(this.getConfig().AI_PLUGIN_RATE_LIMIT_REQUESTS_PER_MINUTE || '60', 10)
193
+ }
194
+
195
+ public getRateLimitTokensPerMinute(): number {
196
+ return parseInt(this.getConfig().AI_PLUGIN_RATE_LIMIT_TOKENS_PER_MINUTE || '50000', 10)
197
+ }
198
+
199
+ public isPluginEnabled(): boolean {
200
+ return this.getConfig().AI_PLUGIN_ENABLED !== 'false'
201
+ }
202
+
203
+ public isDebugEnabled(): boolean {
204
+ return this.getConfig().AI_PLUGIN_DEBUG === 'true'
205
+ }
206
+
207
+ public validateEnvironment(): { valid: boolean; errors: string[] } {
208
+ const errors: string[] = []
209
+ const config = this.getConfig()
210
+
211
+ if (!config.USE_LOCAL_AI || config.USE_LOCAL_AI === 'false') {
212
+ if (!config.ANTHROPIC_API_KEY && !config.OPENAI_API_KEY) {
213
+ errors.push('Cloud AI is enabled but no API keys are configured (ANTHROPIC_API_KEY or OPENAI_API_KEY required)')
214
+ }
215
+ }
216
+
217
+ if (config.AI_PLUGIN_ENABLED === 'false') {
218
+ errors.push('AI Plugin is disabled (AI_PLUGIN_ENABLED=false)')
219
+ }
220
+
221
+ return {
222
+ valid: errors.length === 0,
223
+ errors
224
+ }
225
+ }
226
+
227
+ public reload(): void {
228
+ this.loaded = false
229
+ this.loadEnvironment(true)
230
+ }
231
+ }
232
+
233
+ export const pluginEnv = PluginEnvironment.getInstance()
234
+
235
+ // Convenience exports
236
+ export const getAnthropicApiKey = () => pluginEnv.getAnthropicApiKey()
237
+ export const getOpenAiApiKey = () => pluginEnv.getOpenAiApiKey()
238
+ export const getOllamaBaseUrl = () => pluginEnv.getOllamaBaseUrl()
239
+ export const getOllamaDefaultModel = () => pluginEnv.getOllamaDefaultModel()
240
+ export const isUseLocalAI = () => pluginEnv.isUseLocalAI()
241
+ export const getDefaultCloudModel = () => pluginEnv.getDefaultCloudModel()
242
+ export const getDefaultProvider = () => pluginEnv.getDefaultProvider()
243
+ export const getDefaultModel = () => pluginEnv.getDefaultModel()
244
+ export const getMaxTokens = () => pluginEnv.getMaxTokens()
245
+ export const getDefaultTemperature = () => pluginEnv.getDefaultTemperature()
246
+ export const isCostTrackingEnabled = () => pluginEnv.isCostTrackingEnabled()
247
+ export const getDailyCostLimit = () => pluginEnv.getDailyCostLimit()
248
+ export const getMonthlyCostLimit = () => pluginEnv.getMonthlyCostLimit()
249
+ export const getRateLimitRequestsPerMinute = () => pluginEnv.getRateLimitRequestsPerMinute()
250
+ export const getRateLimitTokensPerMinute = () => pluginEnv.getRateLimitTokensPerMinute()
251
+ export const isPluginEnabled = () => pluginEnv.isPluginEnabled()
252
+ export const isDebugEnabled = () => pluginEnv.isDebugEnabled()
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Data sanitization utilities for AI examples
3
+ * Removes sensitive information before storing examples
4
+ */
5
+
6
+ const SENSITIVE_PATTERNS = {
7
+ // Email addresses
8
+ email: /[\w.-]+@[\w.-]+\.\w+/gi,
9
+
10
+ // API keys and tokens
11
+ apiKey: /[a-zA-Z0-9]{20,}/g,
12
+ bearerToken: /Bearer\s+[\w-]+\.[\w-]+\.[\w-]+/gi,
13
+
14
+ // Common secret patterns
15
+ password: /password['":\s=]+[\w!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]+/gi,
16
+ secret: /secret['":\s=]+[\w!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]+/gi,
17
+
18
+ // Credit card numbers (basic pattern)
19
+ creditCard: /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g,
20
+
21
+ // Phone numbers (basic pattern)
22
+ phone: /\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g,
23
+
24
+ // URLs with credentials
25
+ urlWithCredentials: /https?:\/\/[^:]+:[^@]+@[^\s]+/gi,
26
+ }
27
+
28
+ const REPLACEMENTS = {
29
+ email: '[EMAIL_REDACTED]',
30
+ apiKey: '[API_KEY_REDACTED]',
31
+ bearerToken: 'Bearer [TOKEN_REDACTED]',
32
+ password: 'password: [REDACTED]',
33
+ secret: 'secret: [REDACTED]',
34
+ creditCard: '[CARD_REDACTED]',
35
+ phone: '[PHONE_REDACTED]',
36
+ urlWithCredentials: 'https://[CREDENTIALS_REDACTED]',
37
+ }
38
+
39
+ export interface SanitizationResult {
40
+ sanitized: string
41
+ redactedCount: number
42
+ redactedTypes: string[]
43
+ }
44
+
45
+ /**
46
+ * Sanitize text by removing sensitive information
47
+ */
48
+ export function sanitizeText(text: string): SanitizationResult {
49
+ if (!text || typeof text !== 'string') {
50
+ return {
51
+ sanitized: text || '',
52
+ redactedCount: 0,
53
+ redactedTypes: []
54
+ }
55
+ }
56
+
57
+ let sanitized = text
58
+ let redactedCount = 0
59
+ const redactedTypes: string[] = []
60
+
61
+ for (const [type, pattern] of Object.entries(SENSITIVE_PATTERNS)) {
62
+ const matches = sanitized.match(pattern)
63
+ if (matches && matches.length > 0) {
64
+ const replacement = REPLACEMENTS[type as keyof typeof REPLACEMENTS]
65
+ sanitized = sanitized.replace(pattern, replacement)
66
+ redactedCount += matches.length
67
+ redactedTypes.push(type)
68
+ }
69
+ }
70
+
71
+ return {
72
+ sanitized,
73
+ redactedCount,
74
+ redactedTypes
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Sanitize prompt before saving as example
80
+ */
81
+ export function sanitizePrompt(prompt: string): string {
82
+ return sanitizeText(prompt).sanitized
83
+ }
84
+
85
+ /**
86
+ * Sanitize response before saving as example
87
+ */
88
+ export function sanitizeResponse(response: string): string {
89
+ return sanitizeText(response).sanitized
90
+ }
91
+
92
+ /**
93
+ * Check if text contains sensitive information
94
+ */
95
+ export function containsSensitiveInfo(text: string): boolean {
96
+ if (!text || typeof text !== 'string') {
97
+ return false
98
+ }
99
+
100
+ for (const pattern of Object.values(SENSITIVE_PATTERNS)) {
101
+ if (pattern.test(text)) {
102
+ return true
103
+ }
104
+ }
105
+
106
+ return false
107
+ }
108
+
109
+ /**
110
+ * Get sanitization report for a text
111
+ */
112
+ export function getSanitizationReport(text: string): {
113
+ hasSensitiveInfo: boolean
114
+ details: SanitizationResult
115
+ } {
116
+ const details = sanitizeText(text)
117
+
118
+ return {
119
+ hasSensitiveInfo: details.redactedCount > 0,
120
+ details
121
+ }
122
+ }
@@ -0,0 +1,237 @@
1
+ /**
2
+ * AI Example Saving - PostgreSQL Implementation
3
+ *
4
+ * Uses project's PostgreSQL connection with RLS support.
5
+ * Self-contained plugin with direct DB access.
6
+ */
7
+
8
+ import { mutateWithRLS, queryWithRLS } from '@nextsparkjs/core/lib/db'
9
+ import { sanitizePrompt, sanitizeResponse } from './sanitize'
10
+
11
+ export interface AIExampleData {
12
+ title?: string
13
+ prompt: string
14
+ response: string
15
+ model: string
16
+ status: 'pending' | 'processing' | 'completed' | 'failed'
17
+ userId?: string
18
+ metadata?: {
19
+ tokens?: number
20
+ cost?: number
21
+ duration?: number
22
+ provider?: string
23
+ isLocal?: boolean
24
+ platforms?: number
25
+ imagesProcessed?: number
26
+ }
27
+ }
28
+
29
+ export interface SaveExampleResult {
30
+ success: boolean
31
+ id?: string
32
+ error?: string
33
+ sanitizationApplied?: boolean
34
+ }
35
+
36
+ /**
37
+ * Save AI example to PostgreSQL database
38
+ */
39
+ export async function saveAIExample(
40
+ data: AIExampleData,
41
+ userId: string
42
+ ): Promise<SaveExampleResult> {
43
+ try {
44
+ // 1. Sanitize sensitive data
45
+ const sanitizedPrompt = sanitizePrompt(data.prompt)
46
+ const sanitizedResponse = sanitizeResponse(data.response)
47
+
48
+ const sanitizationApplied =
49
+ sanitizedPrompt !== data.prompt ||
50
+ sanitizedResponse !== data.response
51
+
52
+ // 2. Generate title if not provided
53
+ const title = data.title || generateTitle(sanitizedPrompt)
54
+
55
+ // 3. Insert into database with RLS
56
+ const query = `
57
+ INSERT INTO ai_example (
58
+ title, prompt, response, model, status, "userId", metadata, "createdAt", "updatedAt"
59
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())
60
+ RETURNING id
61
+ `
62
+
63
+ const params = [
64
+ title,
65
+ sanitizedPrompt,
66
+ sanitizedResponse,
67
+ data.model,
68
+ data.status,
69
+ userId,
70
+ JSON.stringify(data.metadata || {})
71
+ ]
72
+
73
+ const result = await mutateWithRLS<{ id: string }>(query, params, userId)
74
+
75
+ if (result.rowCount === 0 || !result.rows[0]) {
76
+ return {
77
+ success: false,
78
+ error: 'Failed to insert example'
79
+ }
80
+ }
81
+
82
+ console.log('[AI Plugin] Example saved to database:', {
83
+ id: result.rows[0].id,
84
+ title,
85
+ sanitizationApplied
86
+ })
87
+
88
+ return {
89
+ success: true,
90
+ id: result.rows[0].id,
91
+ sanitizationApplied
92
+ }
93
+
94
+ } catch (error) {
95
+ console.error('[AI Plugin] Save example failed:', error)
96
+ return {
97
+ success: false,
98
+ error: error instanceof Error ? error.message : 'Unknown error'
99
+ }
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Get AI examples from PostgreSQL
105
+ */
106
+ export async function getAIExamples(params: {
107
+ userId?: string
108
+ limit?: number
109
+ offset?: number
110
+ status?: string
111
+ }) {
112
+ try {
113
+ const { userId, limit = 50, offset = 0, status } = params
114
+
115
+ let query = `
116
+ SELECT id, title, prompt, response, model, status, "userId", metadata, "createdAt", "updatedAt"
117
+ FROM ai_example
118
+ WHERE 1=1
119
+ `
120
+ const queryParams: (string | number)[] = []
121
+
122
+ if (status) {
123
+ queryParams.push(status)
124
+ query += ` AND status = $${queryParams.length}`
125
+ }
126
+
127
+ query += ` ORDER BY "createdAt" DESC LIMIT $${queryParams.length + 1} OFFSET $${queryParams.length + 2}`
128
+ queryParams.push(limit, offset)
129
+
130
+ const rows = await queryWithRLS(query, queryParams, userId)
131
+
132
+ return {
133
+ success: true,
134
+ data: rows
135
+ }
136
+ } catch (error) {
137
+ console.error('[AI Plugin] Get examples failed:', error)
138
+ return {
139
+ success: false,
140
+ error: error instanceof Error ? error.message : 'Unknown error',
141
+ data: []
142
+ }
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Get single AI example by ID from PostgreSQL
148
+ */
149
+ export async function getAIExampleById(id: string, userId?: string) {
150
+ try {
151
+ const query = `
152
+ SELECT id, title, prompt, response, model, status, "userId", metadata, "createdAt", "updatedAt"
153
+ FROM ai_example
154
+ WHERE id = $1
155
+ `
156
+
157
+ const rows = await queryWithRLS(query, [id], userId)
158
+
159
+ if (rows.length === 0) {
160
+ return {
161
+ success: false,
162
+ error: 'Example not found'
163
+ }
164
+ }
165
+
166
+ return {
167
+ success: true,
168
+ data: rows[0]
169
+ }
170
+ } catch (error) {
171
+ console.error('[AI Plugin] Get example by ID failed:', error)
172
+ return {
173
+ success: false,
174
+ error: error instanceof Error ? error.message : 'Unknown error'
175
+ }
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Delete AI example from PostgreSQL
181
+ */
182
+ export async function deleteAIExample(id: string, userId: string) {
183
+ try {
184
+ const query = `DELETE FROM ai_example WHERE id = $1 RETURNING id`
185
+
186
+ const result = await mutateWithRLS<{ id: string }>(query, [id], userId)
187
+
188
+ if (result.rowCount === 0) {
189
+ return {
190
+ success: false,
191
+ error: 'Example not found or unauthorized'
192
+ }
193
+ }
194
+
195
+ return {
196
+ success: true,
197
+ id: result.rows[0].id
198
+ }
199
+ } catch (error) {
200
+ console.error('[AI Plugin] Delete example failed:', error)
201
+ return {
202
+ success: false,
203
+ error: error instanceof Error ? error.message : 'Unknown error'
204
+ }
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Generate title from prompt (helper)
210
+ */
211
+ function generateTitle(prompt: string): string {
212
+ const cleaned = prompt.replace(/\n/g, ' ').trim()
213
+ return cleaned.length > 50
214
+ ? cleaned.substring(0, 50) + '...'
215
+ : cleaned
216
+ }
217
+
218
+ /**
219
+ * Save example with automatic error handling (fire-and-forget)
220
+ */
221
+ export async function saveExampleSafely(
222
+ data: AIExampleData,
223
+ userId: string
224
+ ): Promise<void> {
225
+ try {
226
+ const result = await saveAIExample(data, userId)
227
+
228
+ if (!result.success) {
229
+ console.error('[AI Plugin] Example save failed:', result.error)
230
+ } else if (result.sanitizationApplied) {
231
+ console.warn('[AI Plugin] Sanitization was applied to saved example')
232
+ }
233
+ } catch (error) {
234
+ // Silent fail - don't break the main request
235
+ console.error('[AI Plugin] Example save error:', error)
236
+ }
237
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Server-Only Environment Variable Access
3
+ *
4
+ * This module provides server-only access to plugin environment variables
5
+ * preventing client-side access to sensitive configuration
6
+ */
7
+
8
+ import { pluginEnv } from './plugin-env'
9
+
10
+ // Server-only environment getters
11
+ export async function getServerAnthropicApiKey(): Promise<string | undefined> {
12
+ return pluginEnv.getAnthropicApiKey()
13
+ }
14
+
15
+ export async function getServerOpenAiApiKey(): Promise<string | undefined> {
16
+ return pluginEnv.getOpenAiApiKey()
17
+ }
18
+
19
+ export async function getServerOllamaBaseUrl(): Promise<string> {
20
+ return pluginEnv.getOllamaBaseUrl()
21
+ }
22
+
23
+ export async function getServerOllamaDefaultModel(): Promise<string> {
24
+ return pluginEnv.getOllamaDefaultModel()
25
+ }
26
+
27
+ export async function isServerUseLocalAI(): Promise<boolean> {
28
+ return pluginEnv.isUseLocalAI()
29
+ }
30
+
31
+ export async function getServerDefaultCloudModel(): Promise<string> {
32
+ return pluginEnv.getDefaultCloudModel()
33
+ }
34
+
35
+ export async function getServerDefaultModel(): Promise<string> {
36
+ return pluginEnv.getDefaultModel()
37
+ }
38
+
39
+ export async function isServerPluginEnabled(): Promise<boolean> {
40
+ return pluginEnv.isPluginEnabled()
41
+ }
42
+
43
+ export async function isServerDebugEnabled(): Promise<boolean> {
44
+ return pluginEnv.isDebugEnabled()
45
+ }
46
+
47
+ export async function getServerDefaultProvider(): Promise<string> {
48
+ return pluginEnv.getDefaultProvider()
49
+ }
50
+
51
+ export async function getServerMaxTokens(): Promise<number> {
52
+ return pluginEnv.getMaxTokens()
53
+ }
54
+
55
+ export async function getServerDefaultTemperature(): Promise<number> {
56
+ return pluginEnv.getDefaultTemperature()
57
+ }
58
+
59
+ export async function isServerCostTrackingEnabled(): Promise<boolean> {
60
+ return pluginEnv.isCostTrackingEnabled()
61
+ }
62
+
63
+ export async function getServerDailyCostLimit(): Promise<number> {
64
+ return pluginEnv.getDailyCostLimit()
65
+ }
66
+
67
+ export async function getServerMonthlyCostLimit(): Promise<number> {
68
+ return pluginEnv.getMonthlyCostLimit()
69
+ }
70
+
71
+ export async function getServerRateLimitRequestsPerMinute(): Promise<number> {
72
+ return pluginEnv.getRateLimitRequestsPerMinute()
73
+ }
74
+
75
+ export async function getServerRateLimitTokensPerMinute(): Promise<number> {
76
+ return pluginEnv.getRateLimitTokensPerMinute()
77
+ }
78
+
79
+ export async function validateServerPluginEnvironment(): Promise<{ valid: boolean; errors: string[] }> {
80
+ return pluginEnv.validateEnvironment()
81
+ }
82
+
83
+ // Configuration object for API routes
84
+ export async function getServerPluginConfig() {
85
+ return {
86
+ anthropicApiKey: await getServerAnthropicApiKey(),
87
+ openaiApiKey: await getServerOpenAiApiKey(),
88
+ ollamaBaseUrl: await getServerOllamaBaseUrl(),
89
+ ollamaDefaultModel: await getServerOllamaDefaultModel(),
90
+ useLocalAI: await isServerUseLocalAI(),
91
+ defaultCloudModel: await getServerDefaultCloudModel(),
92
+ defaultModel: await getServerDefaultModel(),
93
+ pluginEnabled: await isServerPluginEnabled(),
94
+ debugEnabled: await isServerDebugEnabled(),
95
+ defaultProvider: await getServerDefaultProvider(),
96
+ maxTokens: await getServerMaxTokens(),
97
+ defaultTemperature: await getServerDefaultTemperature(),
98
+ costTrackingEnabled: await isServerCostTrackingEnabled(),
99
+ dailyCostLimit: await getServerDailyCostLimit(),
100
+ monthlyCostLimit: await getServerMonthlyCostLimit(),
101
+ rateLimitRequestsPerMinute: await getServerRateLimitRequestsPerMinute(),
102
+ rateLimitTokensPerMinute: await getServerRateLimitTokensPerMinute()
103
+ }
104
+ }