@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
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule Pack Routes - Zero-Configuration UX
|
|
3
|
+
*
|
|
4
|
+
* Endpoints for managing rule pack installations and user role selection
|
|
5
|
+
*/
|
|
6
|
+
import { Router } from 'express';
|
|
7
|
+
import { createLogger } from '../utils/logger.js';
|
|
8
|
+
import { RulePackService } from '../services/RulePackService.js';
|
|
9
|
+
import { getAllPacks, getPackById, getPacksForRole } from '../services/rulePacks/index.js';
|
|
10
|
+
const router = Router();
|
|
11
|
+
const logger = createLogger('RulePackRoutes');
|
|
12
|
+
/**
|
|
13
|
+
* GET /api/rule-packs
|
|
14
|
+
* List all available rule packs
|
|
15
|
+
*/
|
|
16
|
+
router.get('/', async (req, res) => {
|
|
17
|
+
try {
|
|
18
|
+
const packs = getAllPacks();
|
|
19
|
+
res.json({
|
|
20
|
+
success: true,
|
|
21
|
+
packs: packs.map(pack => ({
|
|
22
|
+
id: pack.id,
|
|
23
|
+
name: pack.name,
|
|
24
|
+
description: pack.description,
|
|
25
|
+
icon: pack.icon,
|
|
26
|
+
ruleCount: pack.rules.length,
|
|
27
|
+
targetRoles: pack.target_roles
|
|
28
|
+
}))
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
logger.error('Failed to list packs', error);
|
|
33
|
+
res.status(500).json({ success: false, error: error.message });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
/**
|
|
37
|
+
* GET /api/rule-packs/:packId
|
|
38
|
+
* Get details of a specific pack
|
|
39
|
+
*/
|
|
40
|
+
router.get('/:packId', async (req, res) => {
|
|
41
|
+
try {
|
|
42
|
+
const packId = req.params.packId;
|
|
43
|
+
const pack = getPackById(packId);
|
|
44
|
+
if (!pack) {
|
|
45
|
+
return res.status(404).json({ success: false, error: 'Pack not found' });
|
|
46
|
+
}
|
|
47
|
+
res.json({ success: true, pack });
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
logger.error('Failed to get pack', error);
|
|
51
|
+
res.status(500).json({ success: false, error: error.message });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
/**
|
|
55
|
+
* POST /api/rule-packs/install
|
|
56
|
+
* Install a rule pack for the authenticated user
|
|
57
|
+
* Body: { packId: string }
|
|
58
|
+
*/
|
|
59
|
+
router.post('/install', async (req, res) => {
|
|
60
|
+
try {
|
|
61
|
+
const { packId } = req.body;
|
|
62
|
+
const supabase = req.supabase;
|
|
63
|
+
const userId = req.userId;
|
|
64
|
+
if (!packId) {
|
|
65
|
+
return res.status(400).json({ success: false, error: 'packId is required' });
|
|
66
|
+
}
|
|
67
|
+
if (!userId) {
|
|
68
|
+
return res.status(401).json({ success: false, error: 'Unauthorized' });
|
|
69
|
+
}
|
|
70
|
+
const service = new RulePackService(supabase);
|
|
71
|
+
const result = await service.installPack(userId, packId);
|
|
72
|
+
res.json(result);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
logger.error('Failed to install pack', error);
|
|
76
|
+
res.status(500).json({ success: false, error: error.message });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
/**
|
|
80
|
+
* POST /api/rule-packs/uninstall
|
|
81
|
+
* Uninstall a rule pack
|
|
82
|
+
* Body: { packId: string }
|
|
83
|
+
*/
|
|
84
|
+
router.post('/uninstall', async (req, res) => {
|
|
85
|
+
try {
|
|
86
|
+
const { packId } = req.body;
|
|
87
|
+
const supabase = req.supabase;
|
|
88
|
+
const userId = req.userId;
|
|
89
|
+
if (!packId) {
|
|
90
|
+
return res.status(400).json({ success: false, error: 'packId is required' });
|
|
91
|
+
}
|
|
92
|
+
if (!userId) {
|
|
93
|
+
return res.status(401).json({ success: false, error: 'Unauthorized' });
|
|
94
|
+
}
|
|
95
|
+
const service = new RulePackService(supabase);
|
|
96
|
+
const result = await service.uninstallPack(userId, packId);
|
|
97
|
+
res.json(result);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
logger.error('Failed to uninstall pack', error);
|
|
101
|
+
res.status(500).json({ success: false, error: error.message });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
/**
|
|
105
|
+
* GET /api/rule-packs/installed
|
|
106
|
+
* Get user's installed packs
|
|
107
|
+
*/
|
|
108
|
+
router.get('/installed', async (req, res) => {
|
|
109
|
+
try {
|
|
110
|
+
const supabase = req.supabase;
|
|
111
|
+
const userId = req.userId;
|
|
112
|
+
if (!userId) {
|
|
113
|
+
return res.status(401).json({ success: false, error: 'Unauthorized' });
|
|
114
|
+
}
|
|
115
|
+
const service = new RulePackService(supabase);
|
|
116
|
+
const installations = await service.getInstalledPacks(userId);
|
|
117
|
+
res.json({ success: true, installations });
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
logger.error('Failed to get installed packs', error);
|
|
121
|
+
res.status(500).json({ success: false, error: error.message });
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
/**
|
|
125
|
+
* POST /api/rule-packs/set-role
|
|
126
|
+
* Set user role and install appropriate packs
|
|
127
|
+
* Body: { role: UserRole }
|
|
128
|
+
*/
|
|
129
|
+
router.post('/set-role', async (req, res) => {
|
|
130
|
+
try {
|
|
131
|
+
const { role } = req.body;
|
|
132
|
+
const supabase = req.supabase;
|
|
133
|
+
const userId = req.userId;
|
|
134
|
+
if (!role) {
|
|
135
|
+
return res.status(400).json({ success: false, error: 'role is required' });
|
|
136
|
+
}
|
|
137
|
+
if (!userId) {
|
|
138
|
+
return res.status(401).json({ success: false, error: 'Unauthorized' });
|
|
139
|
+
}
|
|
140
|
+
const validRoles = ['executive', 'developer', 'sales', 'operations', 'marketing', 'other'];
|
|
141
|
+
if (!validRoles.includes(role)) {
|
|
142
|
+
return res.status(400).json({ success: false, error: 'Invalid role' });
|
|
143
|
+
}
|
|
144
|
+
const service = new RulePackService(supabase);
|
|
145
|
+
const result = await service.updateUserRole(userId, role, 'onboarding');
|
|
146
|
+
logger.info(`User ${userId} set role to ${role}, installed ${result.packsInstalled.length} packs`);
|
|
147
|
+
res.json(result);
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
logger.error('Failed to set user role', error);
|
|
151
|
+
res.status(500).json({ success: false, error: error.message });
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
/**
|
|
155
|
+
* GET /api/rule-packs/for-role/:role
|
|
156
|
+
* Get recommended packs for a specific role
|
|
157
|
+
*/
|
|
158
|
+
router.get('/for-role/:role', async (req, res) => {
|
|
159
|
+
try {
|
|
160
|
+
const { role } = req.params;
|
|
161
|
+
const packs = getPacksForRole(role);
|
|
162
|
+
res.json({
|
|
163
|
+
success: true,
|
|
164
|
+
role,
|
|
165
|
+
packs: packs.map(pack => ({
|
|
166
|
+
id: pack.id,
|
|
167
|
+
name: pack.name,
|
|
168
|
+
description: pack.description,
|
|
169
|
+
icon: pack.icon,
|
|
170
|
+
ruleCount: pack.rules.length
|
|
171
|
+
}))
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
logger.error('Failed to get packs for role', error);
|
|
176
|
+
res.status(500).json({ success: false, error: error.message });
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
export default router;
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule Pack Service - Zero-Configuration Email Automation
|
|
3
|
+
*
|
|
4
|
+
* Handles installation, management, and tracking of rule packs for users.
|
|
5
|
+
*/
|
|
6
|
+
import { createLogger } from '../utils/logger.js';
|
|
7
|
+
import { getPackById, getPacksForRole } from './rulePacks/index.js';
|
|
8
|
+
const logger = createLogger('RulePackService');
|
|
9
|
+
export class RulePackService {
|
|
10
|
+
supabase;
|
|
11
|
+
constructor(supabase) {
|
|
12
|
+
this.supabase = supabase;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Install a rule pack for a user
|
|
16
|
+
* Creates all rules from the pack and tracks installation
|
|
17
|
+
*/
|
|
18
|
+
async installPack(userId, packId) {
|
|
19
|
+
try {
|
|
20
|
+
const pack = getPackById(packId);
|
|
21
|
+
if (!pack) {
|
|
22
|
+
return { success: false, rulesCreated: 0, error: `Pack '${packId}' not found` };
|
|
23
|
+
}
|
|
24
|
+
logger.info(`Installing pack '${packId}' for user ${userId}`);
|
|
25
|
+
// Check if pack is already installed
|
|
26
|
+
const { data: existing } = await this.supabase
|
|
27
|
+
.from('pack_installations')
|
|
28
|
+
.select('*')
|
|
29
|
+
.eq('user_id', userId)
|
|
30
|
+
.eq('pack_id', packId)
|
|
31
|
+
.is('uninstalled_at', null)
|
|
32
|
+
.single();
|
|
33
|
+
if (existing) {
|
|
34
|
+
logger.info(`Pack '${packId}' already installed for user ${userId}`);
|
|
35
|
+
return { success: true, rulesCreated: 0 }; // Already installed, no-op
|
|
36
|
+
}
|
|
37
|
+
// Create all rules from the pack
|
|
38
|
+
let rulesCreated = 0;
|
|
39
|
+
for (const template of pack.rules) {
|
|
40
|
+
// Check if rule already exists (by rule_template_id)
|
|
41
|
+
const { data: existingRule } = await this.supabase
|
|
42
|
+
.from('rules')
|
|
43
|
+
.select('id')
|
|
44
|
+
.eq('user_id', userId)
|
|
45
|
+
.eq('rule_template_id', template.id)
|
|
46
|
+
.single();
|
|
47
|
+
if (existingRule) {
|
|
48
|
+
logger.debug(`Rule '${template.id}' already exists, skipping`);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
// Create rule from template
|
|
52
|
+
const { error: ruleError } = await this.supabase
|
|
53
|
+
.from('rules')
|
|
54
|
+
.insert({
|
|
55
|
+
user_id: userId,
|
|
56
|
+
name: template.name,
|
|
57
|
+
description: template.description,
|
|
58
|
+
intent: template.intent,
|
|
59
|
+
priority: template.priority,
|
|
60
|
+
condition: template.condition,
|
|
61
|
+
actions: template.actions,
|
|
62
|
+
instructions: template.instructions,
|
|
63
|
+
is_enabled: template.is_enabled_by_default,
|
|
64
|
+
pack: packId,
|
|
65
|
+
rule_template_id: template.id,
|
|
66
|
+
is_system_managed: true
|
|
67
|
+
});
|
|
68
|
+
if (ruleError) {
|
|
69
|
+
logger.error(`Failed to create rule '${template.id}'`, ruleError);
|
|
70
|
+
continue; // Continue with other rules even if one fails
|
|
71
|
+
}
|
|
72
|
+
rulesCreated++;
|
|
73
|
+
}
|
|
74
|
+
// Track pack installation
|
|
75
|
+
const { error: installError } = await this.supabase
|
|
76
|
+
.from('pack_installations')
|
|
77
|
+
.insert({
|
|
78
|
+
user_id: userId,
|
|
79
|
+
pack_id: packId,
|
|
80
|
+
source: 'manual'
|
|
81
|
+
});
|
|
82
|
+
if (installError) {
|
|
83
|
+
logger.error(`Failed to track pack installation`, installError);
|
|
84
|
+
// Don't fail the whole operation if tracking fails
|
|
85
|
+
}
|
|
86
|
+
logger.info(`Successfully installed pack '${packId}' with ${rulesCreated} rules`);
|
|
87
|
+
return { success: true, rulesCreated };
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
logger.error(`Error installing pack '${packId}'`, error);
|
|
91
|
+
return { success: false, rulesCreated: 0, error: error.message };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Uninstall a rule pack for a user
|
|
96
|
+
* Disables all rules from the pack and marks it as uninstalled
|
|
97
|
+
*/
|
|
98
|
+
async uninstallPack(userId, packId) {
|
|
99
|
+
try {
|
|
100
|
+
logger.info(`Uninstalling pack '${packId}' for user ${userId}`);
|
|
101
|
+
// Disable all rules from this pack
|
|
102
|
+
const { error: disableError } = await this.supabase
|
|
103
|
+
.from('rules')
|
|
104
|
+
.update({ is_enabled: false })
|
|
105
|
+
.eq('user_id', userId)
|
|
106
|
+
.eq('pack', packId);
|
|
107
|
+
if (disableError) {
|
|
108
|
+
logger.error(`Failed to disable pack rules`, disableError);
|
|
109
|
+
return { success: false, error: disableError.message };
|
|
110
|
+
}
|
|
111
|
+
// Mark pack as uninstalled
|
|
112
|
+
const { error: uninstallError } = await this.supabase
|
|
113
|
+
.from('pack_installations')
|
|
114
|
+
.update({ uninstalled_at: new Date().toISOString() })
|
|
115
|
+
.eq('user_id', userId)
|
|
116
|
+
.eq('pack_id', packId)
|
|
117
|
+
.is('uninstalled_at', null);
|
|
118
|
+
if (uninstallError) {
|
|
119
|
+
logger.error(`Failed to mark pack as uninstalled`, uninstallError);
|
|
120
|
+
return { success: false, error: uninstallError.message };
|
|
121
|
+
}
|
|
122
|
+
logger.info(`Successfully uninstalled pack '${packId}'`);
|
|
123
|
+
return { success: true };
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
logger.error(`Error uninstalling pack '${packId}'`, error);
|
|
127
|
+
return { success: false, error: error.message };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Get all installed packs for a user
|
|
132
|
+
*/
|
|
133
|
+
async getInstalledPacks(userId) {
|
|
134
|
+
const { data, error } = await this.supabase
|
|
135
|
+
.from('pack_installations')
|
|
136
|
+
.select('*')
|
|
137
|
+
.eq('user_id', userId)
|
|
138
|
+
.is('uninstalled_at', null)
|
|
139
|
+
.order('installed_at', { ascending: false });
|
|
140
|
+
if (error) {
|
|
141
|
+
logger.error('Failed to fetch installed packs', error);
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
return data || [];
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Update user role and install appropriate packs
|
|
148
|
+
* This is the main entry point for onboarding
|
|
149
|
+
*/
|
|
150
|
+
async updateUserRole(userId, role, source = 'onboarding') {
|
|
151
|
+
try {
|
|
152
|
+
logger.info(`Updating user role to '${role}' for user ${userId}`);
|
|
153
|
+
// 1. Update user settings
|
|
154
|
+
const { error: settingsError } = await this.supabase
|
|
155
|
+
.from('user_settings')
|
|
156
|
+
.update({
|
|
157
|
+
user_role: role,
|
|
158
|
+
onboarding_completed: true
|
|
159
|
+
})
|
|
160
|
+
.eq('user_id', userId);
|
|
161
|
+
if (settingsError) {
|
|
162
|
+
logger.error('Failed to update user settings', settingsError);
|
|
163
|
+
return { success: false, packsInstalled: [], error: settingsError.message };
|
|
164
|
+
}
|
|
165
|
+
// 2. Get packs for this role
|
|
166
|
+
const packs = getPacksForRole(role);
|
|
167
|
+
const packsInstalled = [];
|
|
168
|
+
// 3. Install each pack
|
|
169
|
+
for (const pack of packs) {
|
|
170
|
+
const result = await this.installPackWithSource(userId, pack.id, source);
|
|
171
|
+
if (result.success) {
|
|
172
|
+
packsInstalled.push(pack.id);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
logger.info(`Successfully set up ${packsInstalled.length} packs for role '${role}'`);
|
|
176
|
+
return { success: true, packsInstalled };
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
logger.error(`Error updating user role`, error);
|
|
180
|
+
return { success: false, packsInstalled: [], error: error.message };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Install a pack with a specific source tracking
|
|
185
|
+
*/
|
|
186
|
+
async installPackWithSource(userId, packId, source) {
|
|
187
|
+
const pack = getPackById(packId);
|
|
188
|
+
if (!pack) {
|
|
189
|
+
return { success: false, rulesCreated: 0, error: `Pack '${packId}' not found` };
|
|
190
|
+
}
|
|
191
|
+
// Check if already installed
|
|
192
|
+
const { data: existing } = await this.supabase
|
|
193
|
+
.from('pack_installations')
|
|
194
|
+
.select('*')
|
|
195
|
+
.eq('user_id', userId)
|
|
196
|
+
.eq('pack_id', packId)
|
|
197
|
+
.is('uninstalled_at', null)
|
|
198
|
+
.single();
|
|
199
|
+
if (existing) {
|
|
200
|
+
return { success: true, rulesCreated: 0 }; // Already installed
|
|
201
|
+
}
|
|
202
|
+
// Create rules
|
|
203
|
+
let rulesCreated = 0;
|
|
204
|
+
for (const template of pack.rules) {
|
|
205
|
+
const { data: existingRule } = await this.supabase
|
|
206
|
+
.from('rules')
|
|
207
|
+
.select('id')
|
|
208
|
+
.eq('user_id', userId)
|
|
209
|
+
.eq('rule_template_id', template.id)
|
|
210
|
+
.single();
|
|
211
|
+
if (existingRule)
|
|
212
|
+
continue;
|
|
213
|
+
const { error } = await this.supabase
|
|
214
|
+
.from('rules')
|
|
215
|
+
.insert({
|
|
216
|
+
user_id: userId,
|
|
217
|
+
name: template.name,
|
|
218
|
+
description: template.description,
|
|
219
|
+
intent: template.intent,
|
|
220
|
+
priority: template.priority,
|
|
221
|
+
condition: template.condition,
|
|
222
|
+
actions: template.actions,
|
|
223
|
+
instructions: template.instructions,
|
|
224
|
+
is_enabled: template.is_enabled_by_default,
|
|
225
|
+
pack: packId,
|
|
226
|
+
rule_template_id: template.id,
|
|
227
|
+
is_system_managed: true
|
|
228
|
+
});
|
|
229
|
+
if (!error)
|
|
230
|
+
rulesCreated++;
|
|
231
|
+
}
|
|
232
|
+
// Track installation
|
|
233
|
+
await this.supabase
|
|
234
|
+
.from('pack_installations')
|
|
235
|
+
.insert({
|
|
236
|
+
user_id: userId,
|
|
237
|
+
pack_id: packId,
|
|
238
|
+
source
|
|
239
|
+
});
|
|
240
|
+
return { success: true, rulesCreated };
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Get pack metrics (how many users have it installed, etc.)
|
|
244
|
+
*/
|
|
245
|
+
async getPackMetrics(packId) {
|
|
246
|
+
const { data: all } = await this.supabase
|
|
247
|
+
.from('pack_installations')
|
|
248
|
+
.select('*')
|
|
249
|
+
.eq('pack_id', packId);
|
|
250
|
+
const total = all?.length || 0;
|
|
251
|
+
const active = all?.filter(p => !p.uninstalled_at).length || 0;
|
|
252
|
+
const uninstalled = all?.filter(p => p.uninstalled_at).length || 0;
|
|
253
|
+
return {
|
|
254
|
+
totalInstallations: total,
|
|
255
|
+
activeInstallations: active,
|
|
256
|
+
uninstallations: uninstalled
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Auto-install universal pack for new users
|
|
261
|
+
* Called during signup or first sync
|
|
262
|
+
*/
|
|
263
|
+
async ensureUniversalPack(userId) {
|
|
264
|
+
try {
|
|
265
|
+
// Check if user already has universal pack
|
|
266
|
+
const { data: existing } = await this.supabase
|
|
267
|
+
.from('pack_installations')
|
|
268
|
+
.select('*')
|
|
269
|
+
.eq('user_id', userId)
|
|
270
|
+
.eq('pack_id', 'universal')
|
|
271
|
+
.is('uninstalled_at', null)
|
|
272
|
+
.single();
|
|
273
|
+
if (existing) {
|
|
274
|
+
return { success: true, installed: false }; // Already has it
|
|
275
|
+
}
|
|
276
|
+
// Install universal pack
|
|
277
|
+
const result = await this.installPackWithSource(userId, 'universal', 'auto');
|
|
278
|
+
logger.info(`Auto-installed Universal Pack for new user ${userId}`);
|
|
279
|
+
return { success: result.success, installed: true };
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
logger.error('Failed to ensure universal pack', error);
|
|
283
|
+
return { success: false, installed: false };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Get singleton instance (convenience)
|
|
289
|
+
*/
|
|
290
|
+
let defaultInstance = null;
|
|
291
|
+
export function getRulePackService(supabase) {
|
|
292
|
+
if (!defaultInstance) {
|
|
293
|
+
defaultInstance = new RulePackService(supabase);
|
|
294
|
+
}
|
|
295
|
+
return defaultInstance;
|
|
296
|
+
}
|
|
@@ -133,9 +133,10 @@ export class SDKService {
|
|
|
133
133
|
const preferredModel = preferredProvider.models.find(m => m.id === this.DEFAULT_LLM_MODEL) || preferredProvider.models[0];
|
|
134
134
|
this.defaultChatProvider = {
|
|
135
135
|
provider: preferredProvider.provider,
|
|
136
|
-
model: preferredModel.id
|
|
136
|
+
model: preferredModel.id,
|
|
137
|
+
isDefaultFallback: false
|
|
137
138
|
};
|
|
138
|
-
logger.info(`Using preferred
|
|
139
|
+
logger.info(`Using preferred chat provider: ${this.defaultChatProvider.provider}/${this.defaultChatProvider.model}`);
|
|
139
140
|
return this.defaultChatProvider;
|
|
140
141
|
}
|
|
141
142
|
// 2. Fallback to the first provider with available models
|
|
@@ -143,7 +144,8 @@ export class SDKService {
|
|
|
143
144
|
if (p.models && p.models.length > 0) {
|
|
144
145
|
this.defaultChatProvider = {
|
|
145
146
|
provider: p.provider,
|
|
146
|
-
model: p.models[0].id
|
|
147
|
+
model: p.models[0].id,
|
|
148
|
+
isDefaultFallback: true
|
|
147
149
|
};
|
|
148
150
|
logger.info(`Defaulting to first available chat provider: ${this.defaultChatProvider.provider}/${this.defaultChatProvider.model}`);
|
|
149
151
|
return this.defaultChatProvider;
|
|
@@ -183,9 +185,10 @@ export class SDKService {
|
|
|
183
185
|
if (p.models && p.models.length > 0) {
|
|
184
186
|
this.defaultEmbedProvider = {
|
|
185
187
|
provider: p.provider,
|
|
186
|
-
model: p.models[0].id
|
|
188
|
+
model: p.models[0].id,
|
|
189
|
+
isDefaultFallback: true
|
|
187
190
|
};
|
|
188
|
-
logger.info(`
|
|
191
|
+
logger.info(`Selected embed provider: ${this.defaultEmbedProvider.provider}/${this.defaultEmbedProvider.model}`);
|
|
189
192
|
return this.defaultEmbedProvider;
|
|
190
193
|
}
|
|
191
194
|
}
|
|
@@ -206,7 +209,11 @@ export class SDKService {
|
|
|
206
209
|
static async resolveChatProvider(settings) {
|
|
207
210
|
// If both provider and model are set in settings, use them
|
|
208
211
|
if (settings.llm_provider && settings.llm_model) {
|
|
209
|
-
return {
|
|
212
|
+
return {
|
|
213
|
+
provider: settings.llm_provider,
|
|
214
|
+
model: settings.llm_model,
|
|
215
|
+
isDefaultFallback: false
|
|
216
|
+
};
|
|
210
217
|
}
|
|
211
218
|
// Try to get from SDK discovery first
|
|
212
219
|
return await this.getDefaultChatProvider();
|
|
@@ -217,7 +224,11 @@ export class SDKService {
|
|
|
217
224
|
static async resolveEmbedProvider(settings) {
|
|
218
225
|
// If both provider and model are set in settings, use them
|
|
219
226
|
if (settings.embedding_provider && settings.embedding_model) {
|
|
220
|
-
return {
|
|
227
|
+
return {
|
|
228
|
+
provider: settings.embedding_provider,
|
|
229
|
+
model: settings.embedding_model,
|
|
230
|
+
isDefaultFallback: false
|
|
231
|
+
};
|
|
221
232
|
}
|
|
222
233
|
// Try to get from SDK discovery first
|
|
223
234
|
return await this.getDefaultEmbedProvider();
|
|
@@ -333,6 +333,62 @@ export class GmailService {
|
|
|
333
333
|
await this.addLabel(account, messageId, ['STARRED']);
|
|
334
334
|
logger.debug('Message starred', { messageId });
|
|
335
335
|
}
|
|
336
|
+
/**
|
|
337
|
+
* Get or create a label by name (supports nested labels like "Finance/Receipts")
|
|
338
|
+
* Returns the label ID
|
|
339
|
+
*/
|
|
340
|
+
async getOrCreateLabel(account, labelPath) {
|
|
341
|
+
const gmail = await this.getAuthenticatedClient(account);
|
|
342
|
+
// List existing labels
|
|
343
|
+
const { data: labelsData } = await gmail.users.labels.list({ userId: 'me' });
|
|
344
|
+
const existingLabels = labelsData.labels || [];
|
|
345
|
+
// Check if label already exists
|
|
346
|
+
const existingLabel = existingLabels.find(l => l.name === labelPath);
|
|
347
|
+
if (existingLabel?.id) {
|
|
348
|
+
logger.debug('Label already exists', { labelPath, labelId: existingLabel.id });
|
|
349
|
+
return existingLabel.id;
|
|
350
|
+
}
|
|
351
|
+
// Create nested labels if needed (e.g., "Finance/Receipts" creates "Finance" then "Finance/Receipts")
|
|
352
|
+
const parts = labelPath.split('/');
|
|
353
|
+
let currentPath = '';
|
|
354
|
+
let parentId;
|
|
355
|
+
for (const part of parts) {
|
|
356
|
+
currentPath = currentPath ? `${currentPath}/${part}` : part;
|
|
357
|
+
// Check if this level exists
|
|
358
|
+
const existing = existingLabels.find(l => l.name === currentPath);
|
|
359
|
+
if (existing?.id) {
|
|
360
|
+
parentId = existing.id;
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
// Create this level
|
|
364
|
+
const { data: newLabel } = await gmail.users.labels.create({
|
|
365
|
+
userId: 'me',
|
|
366
|
+
requestBody: {
|
|
367
|
+
name: currentPath,
|
|
368
|
+
labelListVisibility: 'labelShow',
|
|
369
|
+
messageListVisibility: 'show'
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
if (!newLabel.id) {
|
|
373
|
+
throw new Error(`Failed to create label: ${currentPath}`);
|
|
374
|
+
}
|
|
375
|
+
logger.info('Created label', { labelPath: currentPath, labelId: newLabel.id });
|
|
376
|
+
parentId = newLabel.id;
|
|
377
|
+
}
|
|
378
|
+
if (!parentId) {
|
|
379
|
+
throw new Error(`Failed to get or create label: ${labelPath}`);
|
|
380
|
+
}
|
|
381
|
+
return parentId;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Apply a label to a message by label name (creates label if needed)
|
|
385
|
+
* Supports nested labels like "Finance/Receipts"
|
|
386
|
+
*/
|
|
387
|
+
async applyLabelByName(account, messageId, labelName) {
|
|
388
|
+
const labelId = await this.getOrCreateLabel(account, labelName);
|
|
389
|
+
await this.addLabel(account, messageId, [labelId]);
|
|
390
|
+
logger.debug('Applied label to message', { messageId, labelName, labelId });
|
|
391
|
+
}
|
|
336
392
|
async fetchAttachment(supabase, path) {
|
|
337
393
|
const { data, error } = await supabase.storage
|
|
338
394
|
.from('rule-attachments')
|