@realtimex/email-automator 2.11.2 → 2.12.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/api/src/routes/index.ts +2 -0
- package/api/src/routes/rulePacks.ts +204 -0
- package/api/src/services/RulePackService.ts +354 -0
- package/api/src/services/SDKService.ts +18 -7
- package/api/src/services/gmail.ts +68 -0
- package/api/src/services/intelligence.ts +160 -22
- package/api/src/services/microsoft.ts +99 -0
- package/api/src/services/processor.ts +146 -12
- package/api/src/services/rulePacks/developer.ts +179 -0
- package/api/src/services/rulePacks/executive.ts +143 -0
- package/api/src/services/rulePacks/index.ts +63 -0
- package/api/src/services/rulePacks/operations.ts +183 -0
- package/api/src/services/rulePacks/sales.ts +160 -0
- package/api/src/services/rulePacks/types.ts +116 -0
- package/api/src/services/rulePacks/universal.ts +83 -0
- package/api/src/services/supabase.ts +40 -0
- package/dist/api/src/routes/index.js +2 -0
- package/dist/api/src/routes/rulePacks.js +179 -0
- package/dist/api/src/services/RulePackService.js +296 -0
- package/dist/api/src/services/SDKService.js +18 -7
- package/dist/api/src/services/gmail.js +56 -0
- package/dist/api/src/services/intelligence.js +153 -21
- package/dist/api/src/services/microsoft.js +79 -0
- package/dist/api/src/services/processor.js +133 -12
- package/dist/api/src/services/rulePacks/developer.js +176 -0
- package/dist/api/src/services/rulePacks/executive.js +140 -0
- package/dist/api/src/services/rulePacks/index.js +58 -0
- package/dist/api/src/services/rulePacks/operations.js +180 -0
- package/dist/api/src/services/rulePacks/sales.js +157 -0
- package/dist/api/src/services/rulePacks/types.js +7 -0
- package/dist/api/src/services/rulePacks/universal.js +80 -0
- package/dist/assets/index-B5rXh3y8.css +1 -0
- package/dist/assets/index-B62ViZum.js +105 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/supabase/migrations/20260131000000_add_rule_packs.sql +176 -0
- package/supabase/migrations/20260131000001_set_zero_config_defaults.sql +51 -0
- package/supabase/migrations/20260131095000_backfill_user_settings.sql +36 -0
- package/supabase/migrations/20260131100000_rule_templates_table.sql +154 -0
- package/supabase/migrations/20260131110000_auto_init_user_data.sql +90 -0
- package/supabase/migrations/20260131120000_backfill_universal_pack.sql +84 -0
- package/supabase/migrations/20260131130000_simplify_rules_with_categories.sql +87 -0
- package/supabase/migrations/20260131140000_fix_action_constraint.sql +11 -0
- package/supabase/migrations/20260131150000_fix_trigger_error_handling.sql +71 -0
- package/supabase/migrations/20260131160000_enable_intelligent_rename_by_default.sql +14 -0
- package/dist/assets/index-BuWrl4UD.js +0 -105
- package/dist/assets/index-CtDzSy0n.css +0 -1
package/api/src/routes/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import settingsRoutes from './settings.js';
|
|
|
8
8
|
import emailsRoutes from './emails.js';
|
|
9
9
|
import migrateRoutes from './migrate.js';
|
|
10
10
|
import sdkRoutes from './sdk.js';
|
|
11
|
+
import rulePacksRoutes from './rulePacks.js';
|
|
11
12
|
|
|
12
13
|
const router = Router();
|
|
13
14
|
|
|
@@ -20,5 +21,6 @@ router.use('/settings', settingsRoutes);
|
|
|
20
21
|
router.use('/emails', emailsRoutes);
|
|
21
22
|
router.use('/migrate', migrateRoutes);
|
|
22
23
|
router.use('/sdk', sdkRoutes);
|
|
24
|
+
router.use('/rule-packs', rulePacksRoutes);
|
|
23
25
|
|
|
24
26
|
export default router;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule Pack Routes - Zero-Configuration UX
|
|
3
|
+
*
|
|
4
|
+
* Endpoints for managing rule pack installations and user role selection
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Router, Request, Response } from 'express';
|
|
8
|
+
import { createLogger } from '../utils/logger.js';
|
|
9
|
+
import { RulePackService } from '../services/RulePackService.js';
|
|
10
|
+
import { getAllPacks, getPackById, getPacksForRole } from '../services/rulePacks/index.js';
|
|
11
|
+
import type { UserRole } from '../services/rulePacks/types.js';
|
|
12
|
+
|
|
13
|
+
const router = Router();
|
|
14
|
+
const logger = createLogger('RulePackRoutes');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* GET /api/rule-packs
|
|
18
|
+
* List all available rule packs
|
|
19
|
+
*/
|
|
20
|
+
router.get('/', async (req: Request, res: Response) => {
|
|
21
|
+
try {
|
|
22
|
+
const packs = getAllPacks();
|
|
23
|
+
|
|
24
|
+
res.json({
|
|
25
|
+
success: true,
|
|
26
|
+
packs: packs.map(pack => ({
|
|
27
|
+
id: pack.id,
|
|
28
|
+
name: pack.name,
|
|
29
|
+
description: pack.description,
|
|
30
|
+
icon: pack.icon,
|
|
31
|
+
ruleCount: pack.rules.length,
|
|
32
|
+
targetRoles: pack.target_roles
|
|
33
|
+
}))
|
|
34
|
+
});
|
|
35
|
+
} catch (error: any) {
|
|
36
|
+
logger.error('Failed to list packs', error);
|
|
37
|
+
res.status(500).json({ success: false, error: error.message });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* GET /api/rule-packs/:packId
|
|
43
|
+
* Get details of a specific pack
|
|
44
|
+
*/
|
|
45
|
+
router.get('/:packId', async (req: Request, res: Response) => {
|
|
46
|
+
try {
|
|
47
|
+
const packId = req.params.packId as string;
|
|
48
|
+
const pack = getPackById(packId);
|
|
49
|
+
|
|
50
|
+
if (!pack) {
|
|
51
|
+
return res.status(404).json({ success: false, error: 'Pack not found' });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
res.json({ success: true, pack });
|
|
55
|
+
} catch (error: any) {
|
|
56
|
+
logger.error('Failed to get pack', error);
|
|
57
|
+
res.status(500).json({ success: false, error: error.message });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* POST /api/rule-packs/install
|
|
63
|
+
* Install a rule pack for the authenticated user
|
|
64
|
+
* Body: { packId: string }
|
|
65
|
+
*/
|
|
66
|
+
router.post('/install', async (req: Request, res: Response) => {
|
|
67
|
+
try {
|
|
68
|
+
const { packId } = req.body;
|
|
69
|
+
const supabase = (req as any).supabase;
|
|
70
|
+
const userId = (req as any).userId;
|
|
71
|
+
|
|
72
|
+
if (!packId) {
|
|
73
|
+
return res.status(400).json({ success: false, error: 'packId is required' });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!userId) {
|
|
77
|
+
return res.status(401).json({ success: false, error: 'Unauthorized' });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const service = new RulePackService(supabase);
|
|
81
|
+
const result = await service.installPack(userId, packId);
|
|
82
|
+
|
|
83
|
+
res.json(result);
|
|
84
|
+
} catch (error: any) {
|
|
85
|
+
logger.error('Failed to install pack', error);
|
|
86
|
+
res.status(500).json({ success: false, error: error.message });
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* POST /api/rule-packs/uninstall
|
|
92
|
+
* Uninstall a rule pack
|
|
93
|
+
* Body: { packId: string }
|
|
94
|
+
*/
|
|
95
|
+
router.post('/uninstall', async (req: Request, res: Response) => {
|
|
96
|
+
try {
|
|
97
|
+
const { packId } = req.body;
|
|
98
|
+
const supabase = (req as any).supabase;
|
|
99
|
+
const userId = (req as any).userId;
|
|
100
|
+
|
|
101
|
+
if (!packId) {
|
|
102
|
+
return res.status(400).json({ success: false, error: 'packId is required' });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!userId) {
|
|
106
|
+
return res.status(401).json({ success: false, error: 'Unauthorized' });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const service = new RulePackService(supabase);
|
|
110
|
+
const result = await service.uninstallPack(userId, packId);
|
|
111
|
+
|
|
112
|
+
res.json(result);
|
|
113
|
+
} catch (error: any) {
|
|
114
|
+
logger.error('Failed to uninstall pack', error);
|
|
115
|
+
res.status(500).json({ success: false, error: error.message });
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* GET /api/rule-packs/installed
|
|
121
|
+
* Get user's installed packs
|
|
122
|
+
*/
|
|
123
|
+
router.get('/installed', async (req: Request, res: Response) => {
|
|
124
|
+
try {
|
|
125
|
+
const supabase = (req as any).supabase;
|
|
126
|
+
const userId = (req as any).userId;
|
|
127
|
+
|
|
128
|
+
if (!userId) {
|
|
129
|
+
return res.status(401).json({ success: false, error: 'Unauthorized' });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const service = new RulePackService(supabase);
|
|
133
|
+
const installations = await service.getInstalledPacks(userId);
|
|
134
|
+
|
|
135
|
+
res.json({ success: true, installations });
|
|
136
|
+
} catch (error: any) {
|
|
137
|
+
logger.error('Failed to get installed packs', error);
|
|
138
|
+
res.status(500).json({ success: false, error: error.message });
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* POST /api/rule-packs/set-role
|
|
144
|
+
* Set user role and install appropriate packs
|
|
145
|
+
* Body: { role: UserRole }
|
|
146
|
+
*/
|
|
147
|
+
router.post('/set-role', async (req: Request, res: Response) => {
|
|
148
|
+
try {
|
|
149
|
+
const { role } = req.body;
|
|
150
|
+
const supabase = (req as any).supabase;
|
|
151
|
+
const userId = (req as any).userId;
|
|
152
|
+
|
|
153
|
+
if (!role) {
|
|
154
|
+
return res.status(400).json({ success: false, error: 'role is required' });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!userId) {
|
|
158
|
+
return res.status(401).json({ success: false, error: 'Unauthorized' });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const validRoles: UserRole[] = ['executive', 'developer', 'sales', 'operations', 'marketing', 'other'];
|
|
162
|
+
if (!validRoles.includes(role as UserRole)) {
|
|
163
|
+
return res.status(400).json({ success: false, error: 'Invalid role' });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const service = new RulePackService(supabase);
|
|
167
|
+
const result = await service.updateUserRole(userId, role as UserRole, 'onboarding');
|
|
168
|
+
|
|
169
|
+
logger.info(`User ${userId} set role to ${role}, installed ${result.packsInstalled.length} packs`);
|
|
170
|
+
|
|
171
|
+
res.json(result);
|
|
172
|
+
} catch (error: any) {
|
|
173
|
+
logger.error('Failed to set user role', error);
|
|
174
|
+
res.status(500).json({ success: false, error: error.message });
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* GET /api/rule-packs/for-role/:role
|
|
180
|
+
* Get recommended packs for a specific role
|
|
181
|
+
*/
|
|
182
|
+
router.get('/for-role/:role', async (req: Request, res: Response) => {
|
|
183
|
+
try {
|
|
184
|
+
const { role } = req.params;
|
|
185
|
+
const packs = getPacksForRole(role as UserRole);
|
|
186
|
+
|
|
187
|
+
res.json({
|
|
188
|
+
success: true,
|
|
189
|
+
role,
|
|
190
|
+
packs: packs.map(pack => ({
|
|
191
|
+
id: pack.id,
|
|
192
|
+
name: pack.name,
|
|
193
|
+
description: pack.description,
|
|
194
|
+
icon: pack.icon,
|
|
195
|
+
ruleCount: pack.rules.length
|
|
196
|
+
}))
|
|
197
|
+
});
|
|
198
|
+
} catch (error: any) {
|
|
199
|
+
logger.error('Failed to get packs for role', error);
|
|
200
|
+
res.status(500).json({ success: false, error: error.message });
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
export default router;
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule Pack Service - Zero-Configuration Email Automation
|
|
3
|
+
*
|
|
4
|
+
* Handles installation, management, and tracking of rule packs for users.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { SupabaseClient } from '@supabase/supabase-js';
|
|
8
|
+
import { createLogger } from '../utils/logger.js';
|
|
9
|
+
import { getPackById, getPacksForRole, ALL_PACKS } from './rulePacks/index.js';
|
|
10
|
+
import type { RulePack, RuleTemplate, UserRole } from './rulePacks/types.js';
|
|
11
|
+
import type { Rule, PackInstallation } from './supabase.js';
|
|
12
|
+
|
|
13
|
+
const logger = createLogger('RulePackService');
|
|
14
|
+
|
|
15
|
+
export class RulePackService {
|
|
16
|
+
constructor(private supabase: SupabaseClient) {}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Install a rule pack for a user
|
|
20
|
+
* Creates all rules from the pack and tracks installation
|
|
21
|
+
*/
|
|
22
|
+
async installPack(userId: string, packId: string): Promise<{ success: boolean; rulesCreated: number; error?: string }> {
|
|
23
|
+
try {
|
|
24
|
+
const pack = getPackById(packId);
|
|
25
|
+
if (!pack) {
|
|
26
|
+
return { success: false, rulesCreated: 0, error: `Pack '${packId}' not found` };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
logger.info(`Installing pack '${packId}' for user ${userId}`);
|
|
30
|
+
|
|
31
|
+
// Check if pack is already installed
|
|
32
|
+
const { data: existing } = await this.supabase
|
|
33
|
+
.from('pack_installations')
|
|
34
|
+
.select('*')
|
|
35
|
+
.eq('user_id', userId)
|
|
36
|
+
.eq('pack_id', packId)
|
|
37
|
+
.is('uninstalled_at', null)
|
|
38
|
+
.single();
|
|
39
|
+
|
|
40
|
+
if (existing) {
|
|
41
|
+
logger.info(`Pack '${packId}' already installed for user ${userId}`);
|
|
42
|
+
return { success: true, rulesCreated: 0 }; // Already installed, no-op
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Create all rules from the pack
|
|
46
|
+
let rulesCreated = 0;
|
|
47
|
+
for (const template of pack.rules) {
|
|
48
|
+
// Check if rule already exists (by rule_template_id)
|
|
49
|
+
const { data: existingRule } = await this.supabase
|
|
50
|
+
.from('rules')
|
|
51
|
+
.select('id')
|
|
52
|
+
.eq('user_id', userId)
|
|
53
|
+
.eq('rule_template_id', template.id)
|
|
54
|
+
.single();
|
|
55
|
+
|
|
56
|
+
if (existingRule) {
|
|
57
|
+
logger.debug(`Rule '${template.id}' already exists, skipping`);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Create rule from template
|
|
62
|
+
const { error: ruleError } = await this.supabase
|
|
63
|
+
.from('rules')
|
|
64
|
+
.insert({
|
|
65
|
+
user_id: userId,
|
|
66
|
+
name: template.name,
|
|
67
|
+
description: template.description,
|
|
68
|
+
intent: template.intent,
|
|
69
|
+
priority: template.priority,
|
|
70
|
+
condition: template.condition as any,
|
|
71
|
+
actions: template.actions as any,
|
|
72
|
+
instructions: template.instructions,
|
|
73
|
+
is_enabled: template.is_enabled_by_default,
|
|
74
|
+
pack: packId,
|
|
75
|
+
rule_template_id: template.id,
|
|
76
|
+
is_system_managed: true
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (ruleError) {
|
|
80
|
+
logger.error(`Failed to create rule '${template.id}'`, ruleError);
|
|
81
|
+
continue; // Continue with other rules even if one fails
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
rulesCreated++;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Track pack installation
|
|
88
|
+
const { error: installError } = await this.supabase
|
|
89
|
+
.from('pack_installations')
|
|
90
|
+
.insert({
|
|
91
|
+
user_id: userId,
|
|
92
|
+
pack_id: packId,
|
|
93
|
+
source: 'manual'
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (installError) {
|
|
97
|
+
logger.error(`Failed to track pack installation`, installError);
|
|
98
|
+
// Don't fail the whole operation if tracking fails
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
logger.info(`Successfully installed pack '${packId}' with ${rulesCreated} rules`);
|
|
102
|
+
return { success: true, rulesCreated };
|
|
103
|
+
|
|
104
|
+
} catch (error: any) {
|
|
105
|
+
logger.error(`Error installing pack '${packId}'`, error);
|
|
106
|
+
return { success: false, rulesCreated: 0, error: error.message };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Uninstall a rule pack for a user
|
|
112
|
+
* Disables all rules from the pack and marks it as uninstalled
|
|
113
|
+
*/
|
|
114
|
+
async uninstallPack(userId: string, packId: string): Promise<{ success: boolean; error?: string }> {
|
|
115
|
+
try {
|
|
116
|
+
logger.info(`Uninstalling pack '${packId}' for user ${userId}`);
|
|
117
|
+
|
|
118
|
+
// Disable all rules from this pack
|
|
119
|
+
const { error: disableError } = await this.supabase
|
|
120
|
+
.from('rules')
|
|
121
|
+
.update({ is_enabled: false })
|
|
122
|
+
.eq('user_id', userId)
|
|
123
|
+
.eq('pack', packId);
|
|
124
|
+
|
|
125
|
+
if (disableError) {
|
|
126
|
+
logger.error(`Failed to disable pack rules`, disableError);
|
|
127
|
+
return { success: false, error: disableError.message };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Mark pack as uninstalled
|
|
131
|
+
const { error: uninstallError } = await this.supabase
|
|
132
|
+
.from('pack_installations')
|
|
133
|
+
.update({ uninstalled_at: new Date().toISOString() })
|
|
134
|
+
.eq('user_id', userId)
|
|
135
|
+
.eq('pack_id', packId)
|
|
136
|
+
.is('uninstalled_at', null);
|
|
137
|
+
|
|
138
|
+
if (uninstallError) {
|
|
139
|
+
logger.error(`Failed to mark pack as uninstalled`, uninstallError);
|
|
140
|
+
return { success: false, error: uninstallError.message };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
logger.info(`Successfully uninstalled pack '${packId}'`);
|
|
144
|
+
return { success: true };
|
|
145
|
+
|
|
146
|
+
} catch (error: any) {
|
|
147
|
+
logger.error(`Error uninstalling pack '${packId}'`, error);
|
|
148
|
+
return { success: false, error: error.message };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get all installed packs for a user
|
|
154
|
+
*/
|
|
155
|
+
async getInstalledPacks(userId: string): Promise<PackInstallation[]> {
|
|
156
|
+
const { data, error } = await this.supabase
|
|
157
|
+
.from('pack_installations')
|
|
158
|
+
.select('*')
|
|
159
|
+
.eq('user_id', userId)
|
|
160
|
+
.is('uninstalled_at', null)
|
|
161
|
+
.order('installed_at', { ascending: false });
|
|
162
|
+
|
|
163
|
+
if (error) {
|
|
164
|
+
logger.error('Failed to fetch installed packs', error);
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return data || [];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Update user role and install appropriate packs
|
|
173
|
+
* This is the main entry point for onboarding
|
|
174
|
+
*/
|
|
175
|
+
async updateUserRole(
|
|
176
|
+
userId: string,
|
|
177
|
+
role: UserRole,
|
|
178
|
+
source: 'onboarding' | 'manual' = 'onboarding'
|
|
179
|
+
): Promise<{ success: boolean; packsInstalled: string[]; error?: string }> {
|
|
180
|
+
try {
|
|
181
|
+
logger.info(`Updating user role to '${role}' for user ${userId}`);
|
|
182
|
+
|
|
183
|
+
// 1. Update user settings
|
|
184
|
+
const { error: settingsError } = await this.supabase
|
|
185
|
+
.from('user_settings')
|
|
186
|
+
.update({
|
|
187
|
+
user_role: role,
|
|
188
|
+
onboarding_completed: true
|
|
189
|
+
})
|
|
190
|
+
.eq('user_id', userId);
|
|
191
|
+
|
|
192
|
+
if (settingsError) {
|
|
193
|
+
logger.error('Failed to update user settings', settingsError);
|
|
194
|
+
return { success: false, packsInstalled: [], error: settingsError.message };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 2. Get packs for this role
|
|
198
|
+
const packs = getPacksForRole(role);
|
|
199
|
+
const packsInstalled: string[] = [];
|
|
200
|
+
|
|
201
|
+
// 3. Install each pack
|
|
202
|
+
for (const pack of packs) {
|
|
203
|
+
const result = await this.installPackWithSource(userId, pack.id, source);
|
|
204
|
+
if (result.success) {
|
|
205
|
+
packsInstalled.push(pack.id);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
logger.info(`Successfully set up ${packsInstalled.length} packs for role '${role}'`);
|
|
210
|
+
return { success: true, packsInstalled };
|
|
211
|
+
|
|
212
|
+
} catch (error: any) {
|
|
213
|
+
logger.error(`Error updating user role`, error);
|
|
214
|
+
return { success: false, packsInstalled: [], error: error.message };
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Install a pack with a specific source tracking
|
|
220
|
+
*/
|
|
221
|
+
private async installPackWithSource(
|
|
222
|
+
userId: string,
|
|
223
|
+
packId: string,
|
|
224
|
+
source: 'onboarding' | 'manual' | 'auto'
|
|
225
|
+
): Promise<{ success: boolean; rulesCreated: number; error?: string }> {
|
|
226
|
+
const pack = getPackById(packId);
|
|
227
|
+
if (!pack) {
|
|
228
|
+
return { success: false, rulesCreated: 0, error: `Pack '${packId}' not found` };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Check if already installed
|
|
232
|
+
const { data: existing } = await this.supabase
|
|
233
|
+
.from('pack_installations')
|
|
234
|
+
.select('*')
|
|
235
|
+
.eq('user_id', userId)
|
|
236
|
+
.eq('pack_id', packId)
|
|
237
|
+
.is('uninstalled_at', null)
|
|
238
|
+
.single();
|
|
239
|
+
|
|
240
|
+
if (existing) {
|
|
241
|
+
return { success: true, rulesCreated: 0 }; // Already installed
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Create rules
|
|
245
|
+
let rulesCreated = 0;
|
|
246
|
+
for (const template of pack.rules) {
|
|
247
|
+
const { data: existingRule } = await this.supabase
|
|
248
|
+
.from('rules')
|
|
249
|
+
.select('id')
|
|
250
|
+
.eq('user_id', userId)
|
|
251
|
+
.eq('rule_template_id', template.id)
|
|
252
|
+
.single();
|
|
253
|
+
|
|
254
|
+
if (existingRule) continue;
|
|
255
|
+
|
|
256
|
+
const { error } = await this.supabase
|
|
257
|
+
.from('rules')
|
|
258
|
+
.insert({
|
|
259
|
+
user_id: userId,
|
|
260
|
+
name: template.name,
|
|
261
|
+
description: template.description,
|
|
262
|
+
intent: template.intent,
|
|
263
|
+
priority: template.priority,
|
|
264
|
+
condition: template.condition as any,
|
|
265
|
+
actions: template.actions as any,
|
|
266
|
+
instructions: template.instructions,
|
|
267
|
+
is_enabled: template.is_enabled_by_default,
|
|
268
|
+
pack: packId,
|
|
269
|
+
rule_template_id: template.id,
|
|
270
|
+
is_system_managed: true
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
if (!error) rulesCreated++;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Track installation
|
|
277
|
+
await this.supabase
|
|
278
|
+
.from('pack_installations')
|
|
279
|
+
.insert({
|
|
280
|
+
user_id: userId,
|
|
281
|
+
pack_id: packId,
|
|
282
|
+
source
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
return { success: true, rulesCreated };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Get pack metrics (how many users have it installed, etc.)
|
|
290
|
+
*/
|
|
291
|
+
async getPackMetrics(packId: string): Promise<{
|
|
292
|
+
totalInstallations: number;
|
|
293
|
+
activeInstallations: number;
|
|
294
|
+
uninstallations: number;
|
|
295
|
+
}> {
|
|
296
|
+
const { data: all } = await this.supabase
|
|
297
|
+
.from('pack_installations')
|
|
298
|
+
.select('*')
|
|
299
|
+
.eq('pack_id', packId);
|
|
300
|
+
|
|
301
|
+
const total = all?.length || 0;
|
|
302
|
+
const active = all?.filter(p => !p.uninstalled_at).length || 0;
|
|
303
|
+
const uninstalled = all?.filter(p => p.uninstalled_at).length || 0;
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
totalInstallations: total,
|
|
307
|
+
activeInstallations: active,
|
|
308
|
+
uninstallations: uninstalled
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Auto-install universal pack for new users
|
|
314
|
+
* Called during signup or first sync
|
|
315
|
+
*/
|
|
316
|
+
async ensureUniversalPack(userId: string): Promise<{ success: boolean; installed: boolean }> {
|
|
317
|
+
try {
|
|
318
|
+
// Check if user already has universal pack
|
|
319
|
+
const { data: existing } = await this.supabase
|
|
320
|
+
.from('pack_installations')
|
|
321
|
+
.select('*')
|
|
322
|
+
.eq('user_id', userId)
|
|
323
|
+
.eq('pack_id', 'universal')
|
|
324
|
+
.is('uninstalled_at', null)
|
|
325
|
+
.single();
|
|
326
|
+
|
|
327
|
+
if (existing) {
|
|
328
|
+
return { success: true, installed: false }; // Already has it
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Install universal pack
|
|
332
|
+
const result = await this.installPackWithSource(userId, 'universal', 'auto');
|
|
333
|
+
|
|
334
|
+
logger.info(`Auto-installed Universal Pack for new user ${userId}`);
|
|
335
|
+
return { success: result.success, installed: true };
|
|
336
|
+
|
|
337
|
+
} catch (error: any) {
|
|
338
|
+
logger.error('Failed to ensure universal pack', error);
|
|
339
|
+
return { success: false, installed: false };
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Get singleton instance (convenience)
|
|
346
|
+
*/
|
|
347
|
+
let defaultInstance: RulePackService | null = null;
|
|
348
|
+
|
|
349
|
+
export function getRulePackService(supabase: SupabaseClient): RulePackService {
|
|
350
|
+
if (!defaultInstance) {
|
|
351
|
+
defaultInstance = new RulePackService(supabase);
|
|
352
|
+
}
|
|
353
|
+
return defaultInstance;
|
|
354
|
+
}
|
|
@@ -161,9 +161,10 @@ export class SDKService {
|
|
|
161
161
|
|
|
162
162
|
this.defaultChatProvider = {
|
|
163
163
|
provider: preferredProvider.provider,
|
|
164
|
-
model: preferredModel.id
|
|
164
|
+
model: preferredModel.id,
|
|
165
|
+
isDefaultFallback: false
|
|
165
166
|
};
|
|
166
|
-
logger.info(`Using preferred
|
|
167
|
+
logger.info(`Using preferred chat provider: ${this.defaultChatProvider.provider}/${this.defaultChatProvider.model}`);
|
|
167
168
|
return this.defaultChatProvider;
|
|
168
169
|
}
|
|
169
170
|
|
|
@@ -172,7 +173,8 @@ export class SDKService {
|
|
|
172
173
|
if (p.models && p.models.length > 0) {
|
|
173
174
|
this.defaultChatProvider = {
|
|
174
175
|
provider: p.provider,
|
|
175
|
-
model: p.models[0].id
|
|
176
|
+
model: p.models[0].id,
|
|
177
|
+
isDefaultFallback: true
|
|
176
178
|
};
|
|
177
179
|
logger.info(`Defaulting to first available chat provider: ${this.defaultChatProvider.provider}/${this.defaultChatProvider.model}`);
|
|
178
180
|
return this.defaultChatProvider;
|
|
@@ -221,9 +223,10 @@ export class SDKService {
|
|
|
221
223
|
if (p.models && p.models.length > 0) {
|
|
222
224
|
this.defaultEmbedProvider = {
|
|
223
225
|
provider: p.provider,
|
|
224
|
-
model: p.models[0].id
|
|
226
|
+
model: p.models[0].id,
|
|
227
|
+
isDefaultFallback: true
|
|
225
228
|
};
|
|
226
|
-
logger.info(`
|
|
229
|
+
logger.info(`Selected embed provider: ${this.defaultEmbedProvider.provider}/${this.defaultEmbedProvider.model}`);
|
|
227
230
|
return this.defaultEmbedProvider;
|
|
228
231
|
}
|
|
229
232
|
}
|
|
@@ -245,7 +248,11 @@ export class SDKService {
|
|
|
245
248
|
static async resolveChatProvider(settings: { llm_provider?: string; llm_model?: string }): Promise<ProviderResult> {
|
|
246
249
|
// If both provider and model are set in settings, use them
|
|
247
250
|
if (settings.llm_provider && settings.llm_model) {
|
|
248
|
-
return {
|
|
251
|
+
return {
|
|
252
|
+
provider: settings.llm_provider,
|
|
253
|
+
model: settings.llm_model,
|
|
254
|
+
isDefaultFallback: false
|
|
255
|
+
};
|
|
249
256
|
}
|
|
250
257
|
|
|
251
258
|
// Try to get from SDK discovery first
|
|
@@ -258,7 +265,11 @@ export class SDKService {
|
|
|
258
265
|
static async resolveEmbedProvider(settings: { embedding_provider?: string; embedding_model?: string }): Promise<ProviderResult> {
|
|
259
266
|
// If both provider and model are set in settings, use them
|
|
260
267
|
if (settings.embedding_provider && settings.embedding_model) {
|
|
261
|
-
return {
|
|
268
|
+
return {
|
|
269
|
+
provider: settings.embedding_provider,
|
|
270
|
+
model: settings.embedding_model,
|
|
271
|
+
isDefaultFallback: false
|
|
272
|
+
};
|
|
262
273
|
}
|
|
263
274
|
|
|
264
275
|
// Try to get from SDK discovery first
|