@realtimex/folio 0.1.2
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/.env.example +20 -0
- package/README.md +63 -0
- package/api/server.ts +130 -0
- package/api/src/config/index.ts +96 -0
- package/api/src/middleware/auth.ts +128 -0
- package/api/src/middleware/errorHandler.ts +88 -0
- package/api/src/middleware/index.ts +4 -0
- package/api/src/middleware/rateLimit.ts +71 -0
- package/api/src/middleware/validation.ts +58 -0
- package/api/src/routes/accounts.ts +142 -0
- package/api/src/routes/baseline-config.ts +124 -0
- package/api/src/routes/chat.ts +154 -0
- package/api/src/routes/health.ts +61 -0
- package/api/src/routes/index.ts +35 -0
- package/api/src/routes/ingestions.ts +275 -0
- package/api/src/routes/migrate.ts +112 -0
- package/api/src/routes/policies.ts +121 -0
- package/api/src/routes/processing.ts +90 -0
- package/api/src/routes/rules.ts +11 -0
- package/api/src/routes/sdk.ts +100 -0
- package/api/src/routes/settings.ts +80 -0
- package/api/src/routes/setup.ts +389 -0
- package/api/src/routes/stats.ts +81 -0
- package/api/src/routes/tts.ts +190 -0
- package/api/src/services/BaselineConfigService.ts +208 -0
- package/api/src/services/ChatService.ts +204 -0
- package/api/src/services/GoogleDriveService.ts +331 -0
- package/api/src/services/GoogleSheetsService.ts +1107 -0
- package/api/src/services/IngestionService.ts +1187 -0
- package/api/src/services/ModelCapabilityService.ts +248 -0
- package/api/src/services/PolicyEngine.ts +1625 -0
- package/api/src/services/PolicyLearningService.ts +527 -0
- package/api/src/services/PolicyLoader.ts +249 -0
- package/api/src/services/RAGService.ts +391 -0
- package/api/src/services/SDKService.ts +249 -0
- package/api/src/services/supabase.ts +113 -0
- package/api/src/utils/Actuator.ts +284 -0
- package/api/src/utils/actions/ActionHandler.ts +34 -0
- package/api/src/utils/actions/AppendToGSheetAction.ts +260 -0
- package/api/src/utils/actions/AutoRenameAction.ts +58 -0
- package/api/src/utils/actions/CopyAction.ts +120 -0
- package/api/src/utils/actions/CopyToGDriveAction.ts +64 -0
- package/api/src/utils/actions/LogCsvAction.ts +48 -0
- package/api/src/utils/actions/NotifyAction.ts +39 -0
- package/api/src/utils/actions/RenameAction.ts +57 -0
- package/api/src/utils/actions/WebhookAction.ts +58 -0
- package/api/src/utils/actions/utils.ts +293 -0
- package/api/src/utils/llmResponse.ts +61 -0
- package/api/src/utils/logger.ts +67 -0
- package/bin/folio-deploy.js +12 -0
- package/bin/folio-setup.js +45 -0
- package/bin/folio.js +65 -0
- package/dist/api/server.js +106 -0
- package/dist/api/src/config/index.js +81 -0
- package/dist/api/src/middleware/auth.js +93 -0
- package/dist/api/src/middleware/errorHandler.js +73 -0
- package/dist/api/src/middleware/index.js +4 -0
- package/dist/api/src/middleware/rateLimit.js +43 -0
- package/dist/api/src/middleware/validation.js +54 -0
- package/dist/api/src/routes/accounts.js +110 -0
- package/dist/api/src/routes/baseline-config.js +91 -0
- package/dist/api/src/routes/chat.js +114 -0
- package/dist/api/src/routes/health.js +52 -0
- package/dist/api/src/routes/index.js +31 -0
- package/dist/api/src/routes/ingestions.js +207 -0
- package/dist/api/src/routes/migrate.js +91 -0
- package/dist/api/src/routes/policies.js +86 -0
- package/dist/api/src/routes/processing.js +75 -0
- package/dist/api/src/routes/rules.js +8 -0
- package/dist/api/src/routes/sdk.js +80 -0
- package/dist/api/src/routes/settings.js +68 -0
- package/dist/api/src/routes/setup.js +315 -0
- package/dist/api/src/routes/stats.js +62 -0
- package/dist/api/src/routes/tts.js +178 -0
- package/dist/api/src/services/BaselineConfigService.js +168 -0
- package/dist/api/src/services/ChatService.js +166 -0
- package/dist/api/src/services/GoogleDriveService.js +280 -0
- package/dist/api/src/services/GoogleSheetsService.js +795 -0
- package/dist/api/src/services/IngestionService.js +990 -0
- package/dist/api/src/services/ModelCapabilityService.js +179 -0
- package/dist/api/src/services/PolicyEngine.js +1353 -0
- package/dist/api/src/services/PolicyLearningService.js +397 -0
- package/dist/api/src/services/PolicyLoader.js +159 -0
- package/dist/api/src/services/RAGService.js +295 -0
- package/dist/api/src/services/SDKService.js +212 -0
- package/dist/api/src/services/supabase.js +72 -0
- package/dist/api/src/utils/Actuator.js +225 -0
- package/dist/api/src/utils/actions/ActionHandler.js +1 -0
- package/dist/api/src/utils/actions/AppendToGSheetAction.js +191 -0
- package/dist/api/src/utils/actions/AutoRenameAction.js +49 -0
- package/dist/api/src/utils/actions/CopyAction.js +112 -0
- package/dist/api/src/utils/actions/CopyToGDriveAction.js +55 -0
- package/dist/api/src/utils/actions/LogCsvAction.js +42 -0
- package/dist/api/src/utils/actions/NotifyAction.js +32 -0
- package/dist/api/src/utils/actions/RenameAction.js +51 -0
- package/dist/api/src/utils/actions/WebhookAction.js +51 -0
- package/dist/api/src/utils/actions/utils.js +237 -0
- package/dist/api/src/utils/llmResponse.js +63 -0
- package/dist/api/src/utils/logger.js +51 -0
- package/dist/assets/index-DzN8-j-e.css +1 -0
- package/dist/assets/index-Uy-ai3Dh.js +113 -0
- package/dist/favicon.svg +31 -0
- package/dist/folio-logo.svg +46 -0
- package/dist/index.html +14 -0
- package/docs-dev/FPE-spec.md +196 -0
- package/docs-dev/folio-prd.md +47 -0
- package/docs-dev/foundation-checklist.md +30 -0
- package/docs-dev/hybrid-routing-architecture.md +205 -0
- package/docs-dev/ingestion-engine.md +69 -0
- package/docs-dev/port-from-email-automator.md +32 -0
- package/docs-dev/tech-spec.md +98 -0
- package/index.html +13 -0
- package/package.json +101 -0
- package/public/favicon.svg +31 -0
- package/public/folio-logo.svg +46 -0
- package/scripts/dev-task.mjs +51 -0
- package/scripts/get-latest-migration-timestamp.mjs +34 -0
- package/scripts/migrate.sh +91 -0
- package/supabase/.temp/cli-latest +1 -0
- package/supabase/.temp/gotrue-version +1 -0
- package/supabase/.temp/pooler-url +1 -0
- package/supabase/.temp/postgres-version +1 -0
- package/supabase/.temp/project-ref +1 -0
- package/supabase/.temp/rest-version +1 -0
- package/supabase/.temp/storage-migration +1 -0
- package/supabase/.temp/storage-version +1 -0
- package/supabase/config.toml +64 -0
- package/supabase/functions/_shared/auth.ts +35 -0
- package/supabase/functions/_shared/cors.ts +12 -0
- package/supabase/functions/_shared/supabaseAdmin.ts +17 -0
- package/supabase/functions/api-v1-settings/index.ts +66 -0
- package/supabase/functions/setup/index.ts +91 -0
- package/supabase/migrations/20260223000000_initial_foundation.sql +136 -0
- package/supabase/migrations/20260223000001_add_migration_rpc.sql +10 -0
- package/supabase/migrations/20260224000002_add_init_state_view.sql +20 -0
- package/supabase/migrations/20260224000003_port_user_creation_parity.sql +139 -0
- package/supabase/migrations/20260224000004_add_avatars_storage.sql +26 -0
- package/supabase/migrations/20260224000005_add_tts_and_embed_settings.sql +24 -0
- package/supabase/migrations/20260224000006_add_policies_table.sql +48 -0
- package/supabase/migrations/20260224000007_fix_migration_rpc.sql +9 -0
- package/supabase/migrations/20260224000008_add_ingestions_table.sql +42 -0
- package/supabase/migrations/20260225000000_setup_compatible_mode.sql +119 -0
- package/supabase/migrations/20260225000001_restore_ingestions.sql +49 -0
- package/supabase/migrations/20260225000002_add_ingestion_trace.sql +2 -0
- package/supabase/migrations/20260225000003_add_baseline_configs.sql +35 -0
- package/supabase/migrations/20260226000000_add_processing_events.sql +26 -0
- package/supabase/migrations/20260226000001_add_ingestion_file_hash.sql +10 -0
- package/supabase/migrations/20260226000002_add_dynamic_rag.sql +150 -0
- package/supabase/migrations/20260226000003_add_ingestion_summary.sql +4 -0
- package/supabase/migrations/20260226000004_add_ingestion_tags.sql +7 -0
- package/supabase/migrations/20260226000005_add_chat_tables.sql +60 -0
- package/supabase/migrations/20260227000000_harden_chat_messages_rls.sql +25 -0
- package/supabase/migrations/20260228000000_add_vision_model_capabilities.sql +8 -0
- package/supabase/migrations/20260228000001_add_policy_match_feedback.sql +51 -0
- package/supabase/migrations/29991231235959_test_migration.sql +0 -0
- package/supabase/templates/confirmation.html +76 -0
- package/supabase/templates/email-change.html +76 -0
- package/supabase/templates/invite.html +72 -0
- package/supabase/templates/magic-link.html +68 -0
- package/supabase/templates/recovery.html +82 -0
- package/tsconfig.api.json +16 -0
- package/tsconfig.json +25 -0
- package/vite.config.ts +146 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { RealtimeXSDK, ProvidersResponse } from "@realtimex/sdk";
|
|
2
|
+
|
|
3
|
+
import { createLogger } from "../utils/logger.js";
|
|
4
|
+
|
|
5
|
+
const logger = createLogger("SDKService");
|
|
6
|
+
|
|
7
|
+
export interface ProviderResult {
|
|
8
|
+
provider: string;
|
|
9
|
+
model: string;
|
|
10
|
+
isDefaultFallback?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class SDKService {
|
|
14
|
+
private static instance: RealtimeXSDK | null = null;
|
|
15
|
+
private static initAttempted = false;
|
|
16
|
+
|
|
17
|
+
// Default provider/model configuration
|
|
18
|
+
// realtimexai routes through RealTimeX Desktop to user's configured providers
|
|
19
|
+
static readonly DEFAULT_LLM_PROVIDER = "realtimexai";
|
|
20
|
+
static readonly DEFAULT_LLM_MODEL = "gpt-4o-mini";
|
|
21
|
+
static readonly DEFAULT_EMBED_PROVIDER = "realtimexai";
|
|
22
|
+
static readonly DEFAULT_EMBED_MODEL = "text-embedding-3-small";
|
|
23
|
+
|
|
24
|
+
static initialize(): RealtimeXSDK {
|
|
25
|
+
if (!this.instance && !this.initAttempted) {
|
|
26
|
+
this.initAttempted = true;
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
this.instance = new RealtimeXSDK({
|
|
30
|
+
realtimex: {
|
|
31
|
+
// @ts-ignore Desktop dev bridge key
|
|
32
|
+
apiKey: "SXKX93J-QSWMB04-K9E0GRE-J5DA8J0"
|
|
33
|
+
},
|
|
34
|
+
permissions: [
|
|
35
|
+
"api.agents", // List agents
|
|
36
|
+
"api.workspaces", // List workspaces
|
|
37
|
+
"api.threads", // List threads
|
|
38
|
+
"webhook.trigger", // Trigger agents
|
|
39
|
+
"activities.read", // Read activities
|
|
40
|
+
"activities.write", // Write activities
|
|
41
|
+
"llm.chat", // Chat completion
|
|
42
|
+
"llm.embed", // Generate embeddings
|
|
43
|
+
"llm.providers", // List LLM providers (chat, embed)
|
|
44
|
+
"vectors.read", // Query vectors
|
|
45
|
+
"vectors.write", // Store vectors
|
|
46
|
+
]
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
logger.info("RealTimeX SDK initialized successfully");
|
|
50
|
+
|
|
51
|
+
// @ts-ignore ping available in desktop bridge
|
|
52
|
+
this.instance.ping?.().catch(() => {
|
|
53
|
+
logger.warn("Desktop ping failed during startup");
|
|
54
|
+
});
|
|
55
|
+
} catch (error) {
|
|
56
|
+
logger.error("Failed to initialize SDK", {
|
|
57
|
+
error: error instanceof Error ? error.message : String(error)
|
|
58
|
+
});
|
|
59
|
+
this.instance = null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return this.instance!;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
static getSDK(): RealtimeXSDK | null {
|
|
67
|
+
if (!this.instance && !this.initAttempted) {
|
|
68
|
+
this.initialize();
|
|
69
|
+
}
|
|
70
|
+
return this.instance;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
static async isAvailable(): Promise<boolean> {
|
|
74
|
+
try {
|
|
75
|
+
const sdk = this.getSDK();
|
|
76
|
+
if (!sdk) return false;
|
|
77
|
+
|
|
78
|
+
// Try to ping first (faster)
|
|
79
|
+
try {
|
|
80
|
+
// @ts-ignore ping available in desktop bridge
|
|
81
|
+
await sdk.ping();
|
|
82
|
+
return true;
|
|
83
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
84
|
+
} catch (e) {
|
|
85
|
+
// Fallback to providers check if ping not available/fails
|
|
86
|
+
await sdk.llm.chatProviders();
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
90
|
+
} catch (error: any) {
|
|
91
|
+
logger.warn("SDK not available", { error: error.message });
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Helper to wrap a promise with a timeout
|
|
98
|
+
*/
|
|
99
|
+
static async withTimeout<T>(promise: Promise<T>, timeoutMs: number, errorMessage: string): Promise<T> {
|
|
100
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
101
|
+
let timeoutHandle: any;
|
|
102
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
103
|
+
timeoutHandle = setTimeout(() => reject(new Error(errorMessage)), timeoutMs);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const result = await Promise.race([promise, timeoutPromise]);
|
|
108
|
+
return result as T;
|
|
109
|
+
} finally {
|
|
110
|
+
clearTimeout(timeoutHandle);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Cache for default providers (avoid repeated SDK calls)
|
|
115
|
+
private static defaultChatProvider: ProviderResult | null = null;
|
|
116
|
+
private static defaultEmbedProvider: ProviderResult | null = null;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get default chat provider/model from SDK dynamically
|
|
120
|
+
*/
|
|
121
|
+
static async getDefaultChatProvider(): Promise<ProviderResult> {
|
|
122
|
+
// Return cached if available
|
|
123
|
+
if (this.defaultChatProvider) {
|
|
124
|
+
return this.defaultChatProvider;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const sdk = this.getSDK();
|
|
128
|
+
if (!sdk) {
|
|
129
|
+
return {
|
|
130
|
+
provider: this.DEFAULT_LLM_PROVIDER,
|
|
131
|
+
model: this.DEFAULT_LLM_MODEL,
|
|
132
|
+
isDefaultFallback: true
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const { providers } = await this.withTimeout<ProvidersResponse>(
|
|
138
|
+
sdk.llm.chatProviders(),
|
|
139
|
+
30000,
|
|
140
|
+
"Chat providers fetch timed out"
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
if (!providers || providers.length === 0) {
|
|
144
|
+
throw new Error("No LLM providers available");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const preferred = providers.find((item) => item.provider === this.DEFAULT_LLM_PROVIDER);
|
|
148
|
+
const chosen = preferred || providers[0];
|
|
149
|
+
const model = chosen.models?.[0]?.id || this.DEFAULT_LLM_MODEL;
|
|
150
|
+
|
|
151
|
+
this.defaultChatProvider = {
|
|
152
|
+
provider: chosen.provider,
|
|
153
|
+
model,
|
|
154
|
+
isDefaultFallback: !preferred
|
|
155
|
+
};
|
|
156
|
+
return this.defaultChatProvider;
|
|
157
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
158
|
+
} catch (error: any) {
|
|
159
|
+
logger.warn("Failed to get default chat provider from SDK", error);
|
|
160
|
+
return {
|
|
161
|
+
provider: this.DEFAULT_LLM_PROVIDER,
|
|
162
|
+
model: this.DEFAULT_LLM_MODEL,
|
|
163
|
+
isDefaultFallback: true
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get default embedding provider/model from SDK dynamically
|
|
170
|
+
*/
|
|
171
|
+
static async getDefaultEmbedProvider(): Promise<ProviderResult> {
|
|
172
|
+
if (this.defaultEmbedProvider) {
|
|
173
|
+
return this.defaultEmbedProvider;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const sdk = this.getSDK();
|
|
177
|
+
if (!sdk) {
|
|
178
|
+
return {
|
|
179
|
+
provider: this.DEFAULT_EMBED_PROVIDER,
|
|
180
|
+
model: this.DEFAULT_EMBED_MODEL,
|
|
181
|
+
isDefaultFallback: true
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const { providers } = await this.withTimeout<ProvidersResponse>(
|
|
187
|
+
sdk.llm.embedProviders(),
|
|
188
|
+
30000,
|
|
189
|
+
"Embed providers fetch timed out"
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
if (!providers || providers.length === 0) {
|
|
193
|
+
throw new Error("No embedding providers available");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const preferred = providers.find((p) => p.provider === this.DEFAULT_EMBED_PROVIDER);
|
|
197
|
+
const chosen = preferred || providers[0];
|
|
198
|
+
const model = chosen.models?.[0]?.id || this.DEFAULT_EMBED_MODEL;
|
|
199
|
+
|
|
200
|
+
this.defaultEmbedProvider = {
|
|
201
|
+
provider: chosen.provider,
|
|
202
|
+
model,
|
|
203
|
+
isDefaultFallback: !preferred
|
|
204
|
+
};
|
|
205
|
+
return this.defaultEmbedProvider;
|
|
206
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
207
|
+
} catch (error: any) {
|
|
208
|
+
logger.warn("Failed to get default embed provider from SDK", error);
|
|
209
|
+
return {
|
|
210
|
+
provider: this.DEFAULT_EMBED_PROVIDER,
|
|
211
|
+
model: this.DEFAULT_EMBED_MODEL,
|
|
212
|
+
isDefaultFallback: true
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Resolve LLM provider/model - use settings if available, otherwise use defaults
|
|
219
|
+
*/
|
|
220
|
+
static async resolveChatProvider(settings: { llm_provider?: string; llm_model?: string }): Promise<ProviderResult> {
|
|
221
|
+
if (settings.llm_provider && settings.llm_model) {
|
|
222
|
+
return {
|
|
223
|
+
provider: settings.llm_provider,
|
|
224
|
+
model: settings.llm_model,
|
|
225
|
+
isDefaultFallback: false
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
return await this.getDefaultChatProvider();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Resolve embedding provider/model - use settings if available, otherwise use defaults
|
|
233
|
+
*/
|
|
234
|
+
static async resolveEmbedProvider(settings: { embedding_provider?: string; embedding_model?: string }): Promise<ProviderResult> {
|
|
235
|
+
if (settings.embedding_provider && settings.embedding_model) {
|
|
236
|
+
return {
|
|
237
|
+
provider: settings.embedding_provider,
|
|
238
|
+
model: settings.embedding_model,
|
|
239
|
+
isDefaultFallback: false
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
return await this.getDefaultEmbedProvider();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
static clearProviderCache(): void {
|
|
246
|
+
this.defaultChatProvider = null;
|
|
247
|
+
this.defaultEmbedProvider = null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { createClient, SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
+
|
|
3
|
+
import { config } from "../config/index.js";
|
|
4
|
+
import { createLogger } from "../utils/logger.js";
|
|
5
|
+
|
|
6
|
+
const logger = createLogger("SupabaseService");
|
|
7
|
+
|
|
8
|
+
let serverClient: SupabaseClient | null = null;
|
|
9
|
+
let lastConfigHash = "";
|
|
10
|
+
|
|
11
|
+
export function isValidUrl(url: string): boolean {
|
|
12
|
+
return url.startsWith("http://") || url.startsWith("https://");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isValidAnonKey(key: string): boolean {
|
|
16
|
+
return key.startsWith("eyJ") || key.startsWith("sb_publishable_");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getConfigHash() {
|
|
20
|
+
return `${config.supabase.url}_${config.supabase.anonKey}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getServerSupabase(forceRefresh = false): SupabaseClient | null {
|
|
24
|
+
const currentHash = getConfigHash();
|
|
25
|
+
|
|
26
|
+
if (serverClient && !forceRefresh && currentHash === lastConfigHash) {
|
|
27
|
+
return serverClient;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const url = config.supabase.url;
|
|
31
|
+
const key = config.supabase.anonKey;
|
|
32
|
+
|
|
33
|
+
if (!url || !key || !isValidUrl(url)) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
serverClient = createClient(url, key, {
|
|
39
|
+
auth: {
|
|
40
|
+
autoRefreshToken: false,
|
|
41
|
+
persistSession: false
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
lastConfigHash = currentHash;
|
|
45
|
+
logger.info("Server Supabase client initialized");
|
|
46
|
+
return serverClient;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
logger.error("Failed to initialize server Supabase client", {
|
|
49
|
+
error: error instanceof Error ? error.message : String(error)
|
|
50
|
+
});
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getServiceRoleSupabase(): SupabaseClient | null {
|
|
56
|
+
const url = config.supabase.url;
|
|
57
|
+
const key = config.supabase.serviceRoleKey;
|
|
58
|
+
|
|
59
|
+
if (!url || !key || !isValidUrl(url)) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
return createClient(url, key, {
|
|
65
|
+
auth: {
|
|
66
|
+
autoRefreshToken: false,
|
|
67
|
+
persistSession: false
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
} catch (error) {
|
|
71
|
+
logger.error("Failed to initialize service-role Supabase client", {
|
|
72
|
+
error: error instanceof Error ? error.message : String(error)
|
|
73
|
+
});
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getSupabaseConfigFromHeaders(headers: Record<string, unknown>): {
|
|
79
|
+
url: string;
|
|
80
|
+
anonKey: string;
|
|
81
|
+
} | null {
|
|
82
|
+
const url = String(headers["x-supabase-url"] || "");
|
|
83
|
+
const anonKey = String(headers["x-supabase-anon-key"] || "");
|
|
84
|
+
|
|
85
|
+
if (!url || !anonKey || !isValidUrl(url) || !isValidAnonKey(anonKey)) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { url, anonKey };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface UserSettings {
|
|
93
|
+
id: string;
|
|
94
|
+
user_id: string;
|
|
95
|
+
llm_provider: string | null;
|
|
96
|
+
llm_model: string | null;
|
|
97
|
+
vision_model_capabilities: Record<string, unknown> | null;
|
|
98
|
+
sync_interval_minutes: number;
|
|
99
|
+
created_at: string;
|
|
100
|
+
updated_at: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface ProcessingJob {
|
|
104
|
+
id: string;
|
|
105
|
+
user_id: string;
|
|
106
|
+
status: "queued" | "running" | "completed" | "failed";
|
|
107
|
+
source_type: string;
|
|
108
|
+
payload: Record<string, unknown>;
|
|
109
|
+
runtime_key: string | null;
|
|
110
|
+
error_message: string | null;
|
|
111
|
+
created_at: string;
|
|
112
|
+
updated_at: string;
|
|
113
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
+
import { createLogger } from "./logger.js";
|
|
3
|
+
import type { ExtractField } from "../services/PolicyLoader.js";
|
|
4
|
+
import { getServiceRoleSupabase } from "../services/supabase.js";
|
|
5
|
+
import { ActionHandler, ActionContext } from "./actions/ActionHandler.js";
|
|
6
|
+
import { ActionInput, ExtractedData, deriveVariables } from "./actions/utils.js";
|
|
7
|
+
|
|
8
|
+
import { RenameAction } from "./actions/RenameAction.js";
|
|
9
|
+
import { AutoRenameAction } from "./actions/AutoRenameAction.js";
|
|
10
|
+
import { CopyAction } from "./actions/CopyAction.js";
|
|
11
|
+
import { CopyToGDriveAction } from "./actions/CopyToGDriveAction.js";
|
|
12
|
+
import { AppendToGSheetAction } from "./actions/AppendToGSheetAction.js";
|
|
13
|
+
import { LogCsvAction } from "./actions/LogCsvAction.js";
|
|
14
|
+
import { NotifyAction } from "./actions/NotifyAction.js";
|
|
15
|
+
import { WebhookAction } from "./actions/WebhookAction.js";
|
|
16
|
+
|
|
17
|
+
const logger = createLogger("Actuator");
|
|
18
|
+
|
|
19
|
+
let warnedMissingServiceRole = false;
|
|
20
|
+
|
|
21
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
22
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function toVariableString(value: unknown): string | undefined {
|
|
26
|
+
if (value == null) return undefined;
|
|
27
|
+
if (typeof value === "string") return value;
|
|
28
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
29
|
+
try {
|
|
30
|
+
return JSON.stringify(value);
|
|
31
|
+
} catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function ensureRecord(container: ExtractedData, key: string): Record<string, unknown> {
|
|
37
|
+
const existing = container[key];
|
|
38
|
+
if (isRecord(existing)) {
|
|
39
|
+
return existing;
|
|
40
|
+
}
|
|
41
|
+
const next: Record<string, unknown> = {};
|
|
42
|
+
container[key] = next;
|
|
43
|
+
return next;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ActuatorResult {
|
|
47
|
+
success: boolean;
|
|
48
|
+
actionsExecuted: string[];
|
|
49
|
+
errors: string[];
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
51
|
+
trace: { timestamp: string; step: string; details?: any }[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class Actuator {
|
|
55
|
+
private static handlers: Map<string, ActionHandler> = new Map([
|
|
56
|
+
["rename", new RenameAction()],
|
|
57
|
+
["auto_rename", new AutoRenameAction()],
|
|
58
|
+
["copy", new CopyAction()],
|
|
59
|
+
["copy_to_gdrive", new CopyToGDriveAction()],
|
|
60
|
+
["append_to_google_sheet", new AppendToGSheetAction()],
|
|
61
|
+
["log_csv", new LogCsvAction()],
|
|
62
|
+
["notify", new NotifyAction()],
|
|
63
|
+
["webhook", new WebhookAction()],
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
static registerAction(type: string, handler: ActionHandler) {
|
|
67
|
+
this.handlers.set(type, handler);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private static mergeActionOutputs(
|
|
71
|
+
runtimeData: ExtractedData,
|
|
72
|
+
runtimeVariables: Record<string, string>,
|
|
73
|
+
actionType: string,
|
|
74
|
+
actionIndex: number,
|
|
75
|
+
outputs: Record<string, unknown>
|
|
76
|
+
): string[] {
|
|
77
|
+
const outputKeys = Object.keys(outputs);
|
|
78
|
+
if (outputKeys.length === 0) return outputKeys;
|
|
79
|
+
|
|
80
|
+
const actionsByType = ensureRecord(runtimeData, "_actions");
|
|
81
|
+
const previousByType = actionsByType[actionType];
|
|
82
|
+
const mergedByType = {
|
|
83
|
+
...(isRecord(previousByType) ? previousByType : {}),
|
|
84
|
+
...outputs,
|
|
85
|
+
};
|
|
86
|
+
actionsByType[actionType] = mergedByType;
|
|
87
|
+
|
|
88
|
+
const history = Array.isArray(runtimeData._action_history) ? [...runtimeData._action_history] : [];
|
|
89
|
+
const historyEntry: Record<string, unknown> = {
|
|
90
|
+
index: actionIndex,
|
|
91
|
+
type: actionType,
|
|
92
|
+
...outputs,
|
|
93
|
+
};
|
|
94
|
+
history.push(historyEntry);
|
|
95
|
+
runtimeData._action_history = history;
|
|
96
|
+
runtimeData._last = historyEntry;
|
|
97
|
+
runtimeData._last_action_type = actionType;
|
|
98
|
+
runtimeVariables._last_action_type = actionType;
|
|
99
|
+
|
|
100
|
+
for (const key of outputKeys) {
|
|
101
|
+
const value = outputs[key];
|
|
102
|
+
const serialized = toVariableString(value);
|
|
103
|
+
|
|
104
|
+
if (runtimeData[key] === undefined) {
|
|
105
|
+
runtimeData[key] = value;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (serialized === undefined) continue;
|
|
109
|
+
|
|
110
|
+
runtimeVariables[`${actionType}.${key}`] = serialized;
|
|
111
|
+
runtimeVariables[`_actions.${actionType}.${key}`] = serialized;
|
|
112
|
+
runtimeVariables[`_last.${key}`] = serialized;
|
|
113
|
+
|
|
114
|
+
if (runtimeVariables[key] === undefined) {
|
|
115
|
+
runtimeVariables[key] = serialized;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return outputKeys;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
static async logEvent(
|
|
123
|
+
ingestionId: string | null,
|
|
124
|
+
userId: string | null,
|
|
125
|
+
eventType: "info" | "action" | "error" | "analysis",
|
|
126
|
+
state: string,
|
|
127
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
128
|
+
details: any,
|
|
129
|
+
supabaseClient?: SupabaseClient | null
|
|
130
|
+
) {
|
|
131
|
+
// Fire and forget, don't await blocking execution
|
|
132
|
+
const supabase = supabaseClient ?? getServiceRoleSupabase();
|
|
133
|
+
if (!supabase) {
|
|
134
|
+
if (!warnedMissingServiceRole) {
|
|
135
|
+
logger.warn("Service-role Supabase client unavailable; skipping processing_events stream writes.");
|
|
136
|
+
warnedMissingServiceRole = true;
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
void supabase.from("processing_events").insert({
|
|
142
|
+
ingestion_id: ingestionId ?? null,
|
|
143
|
+
user_id: userId ?? null,
|
|
144
|
+
event_type: eventType,
|
|
145
|
+
agent_state: state,
|
|
146
|
+
details,
|
|
147
|
+
}).then(({ error }: { error: unknown }) => {
|
|
148
|
+
if (error) logger.warn("Failed to stream actuator log", { error });
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Executes an array of abstract action definitions.
|
|
154
|
+
*/
|
|
155
|
+
static async execute(
|
|
156
|
+
ingestionId: string,
|
|
157
|
+
userId: string,
|
|
158
|
+
actions: ActionInput[],
|
|
159
|
+
data: ExtractedData,
|
|
160
|
+
file: { path: string; name: string },
|
|
161
|
+
fields: ExtractField[] = [],
|
|
162
|
+
supabase?: SupabaseClient | null
|
|
163
|
+
): Promise<ActuatorResult> {
|
|
164
|
+
const result: ActuatorResult = {
|
|
165
|
+
success: true,
|
|
166
|
+
actionsExecuted: [],
|
|
167
|
+
errors: [],
|
|
168
|
+
trace: [],
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
result.trace.push({ timestamp: new Date().toISOString(), step: "Initializing Actuator", details: { actionsCount: actions.length } });
|
|
172
|
+
Actuator.logEvent(ingestionId, userId, "info", "Actuator Initialized", { actionsCount: actions.length }, supabase);
|
|
173
|
+
|
|
174
|
+
// Convert the 13-digit Dropzone timestamp into a human-readable local time
|
|
175
|
+
// (e.g. "1772148849046-bill.pdf" -> "2026-02-26_15-34-09_bill.pdf")
|
|
176
|
+
// Note: only `currentFile.name` is normalized; `file.path` (disk path) is unchanged.
|
|
177
|
+
const tsMatch = file.name.match(/^(\d{13})-(.*)$/);
|
|
178
|
+
let currentFile = { ...file };
|
|
179
|
+
if (tsMatch) {
|
|
180
|
+
const date = new Date(parseInt(tsMatch[1], 10));
|
|
181
|
+
const yyyy = date.getFullYear();
|
|
182
|
+
const MM = String(date.getMonth() + 1).padStart(2, "0");
|
|
183
|
+
const dd = String(date.getDate()).padStart(2, "0");
|
|
184
|
+
const HH = String(date.getHours()).padStart(2, "0");
|
|
185
|
+
const mm = String(date.getMinutes()).padStart(2, "0");
|
|
186
|
+
const ss = String(date.getSeconds()).padStart(2, "0");
|
|
187
|
+
currentFile.name = `${yyyy}-${MM}-${dd}_${HH}-${mm}-${ss}_${tsMatch[2]}`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const runtimeData: ExtractedData = { ...data };
|
|
191
|
+
const runtimeVariables = deriveVariables(runtimeData, fields);
|
|
192
|
+
runtimeData.current_file_name = currentFile.name;
|
|
193
|
+
runtimeData.current_file_path = currentFile.path;
|
|
194
|
+
runtimeVariables.current_file_name = currentFile.name;
|
|
195
|
+
runtimeVariables.current_file_path = currentFile.path;
|
|
196
|
+
|
|
197
|
+
for (const [actionIndex, action] of actions.entries()) {
|
|
198
|
+
const handler = this.handlers.get(action.type);
|
|
199
|
+
|
|
200
|
+
if (!handler) {
|
|
201
|
+
const msg = `Action failed: Unsupported action type '${action.type}'`;
|
|
202
|
+
logger.error(msg);
|
|
203
|
+
result.errors.push(msg);
|
|
204
|
+
result.success = false;
|
|
205
|
+
result.trace.push({ timestamp: new Date().toISOString(), step: `Unsupported action type`, details: { type: action.type } });
|
|
206
|
+
Actuator.logEvent(ingestionId, userId, "error", "Action Execution", { action: action.type, error: msg }, supabase);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
runtimeData.current_file_name = currentFile.name;
|
|
212
|
+
runtimeData.current_file_path = currentFile.path;
|
|
213
|
+
runtimeVariables.current_file_name = currentFile.name;
|
|
214
|
+
runtimeVariables.current_file_path = currentFile.path;
|
|
215
|
+
|
|
216
|
+
const context: ActionContext = {
|
|
217
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
218
|
+
action: action as any,
|
|
219
|
+
data: runtimeData,
|
|
220
|
+
file: currentFile,
|
|
221
|
+
variables: runtimeVariables,
|
|
222
|
+
userId,
|
|
223
|
+
ingestionId,
|
|
224
|
+
supabase,
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const handlerResult = await handler.execute(context);
|
|
228
|
+
|
|
229
|
+
result.trace.push(...handlerResult.trace);
|
|
230
|
+
|
|
231
|
+
if (handlerResult.success) {
|
|
232
|
+
if (handlerResult.newFileState) {
|
|
233
|
+
currentFile = handlerResult.newFileState;
|
|
234
|
+
runtimeData.current_file_name = currentFile.name;
|
|
235
|
+
runtimeData.current_file_path = currentFile.path;
|
|
236
|
+
runtimeVariables.current_file_name = currentFile.name;
|
|
237
|
+
runtimeVariables.current_file_path = currentFile.path;
|
|
238
|
+
}
|
|
239
|
+
if (handlerResult.outputs && Object.keys(handlerResult.outputs).length > 0) {
|
|
240
|
+
const outputKeys = this.mergeActionOutputs(
|
|
241
|
+
runtimeData,
|
|
242
|
+
runtimeVariables,
|
|
243
|
+
action.type,
|
|
244
|
+
actionIndex,
|
|
245
|
+
handlerResult.outputs
|
|
246
|
+
);
|
|
247
|
+
result.trace.push({
|
|
248
|
+
timestamp: new Date().toISOString(),
|
|
249
|
+
step: "Captured action outputs",
|
|
250
|
+
details: {
|
|
251
|
+
action: action.type,
|
|
252
|
+
outputKeys,
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
result.actionsExecuted.push(...handlerResult.logs);
|
|
257
|
+
} else {
|
|
258
|
+
const msg = `Action failed (${action.type}): ${handlerResult.error}`;
|
|
259
|
+
logger.error(msg);
|
|
260
|
+
result.errors.push(msg);
|
|
261
|
+
result.success = false;
|
|
262
|
+
const eventDetails: Record<string, unknown> = {
|
|
263
|
+
action: action.type,
|
|
264
|
+
error: handlerResult.error,
|
|
265
|
+
};
|
|
266
|
+
if (handlerResult.errorDetails && Object.keys(handlerResult.errorDetails).length > 0) {
|
|
267
|
+
Object.assign(eventDetails, handlerResult.errorDetails);
|
|
268
|
+
}
|
|
269
|
+
Actuator.logEvent(ingestionId, userId, "error", "Action Execution", eventDetails, supabase);
|
|
270
|
+
}
|
|
271
|
+
} catch (err: unknown) {
|
|
272
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
273
|
+
const msg = `Action failed (${action.type}): ${errMsg}`;
|
|
274
|
+
logger.error(msg);
|
|
275
|
+
result.errors.push(msg);
|
|
276
|
+
result.success = false;
|
|
277
|
+
result.trace.push({ timestamp: new Date().toISOString(), step: `Action execution error`, details: { type: action.type, error: errMsg } });
|
|
278
|
+
Actuator.logEvent(ingestionId, userId, "error", "Action Execution", { action: action.type, error: errMsg }, supabase);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return result;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
+
import { PolicyAction } from "../../services/PolicyLoader.js";
|
|
3
|
+
import type { ExtractedData } from "./utils.js";
|
|
4
|
+
|
|
5
|
+
export interface TraceLog {
|
|
6
|
+
timestamp: string;
|
|
7
|
+
step: string;
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
|
+
details?: any;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ActionContext {
|
|
13
|
+
action: PolicyAction;
|
|
14
|
+
data: ExtractedData;
|
|
15
|
+
file: { path: string; name: string };
|
|
16
|
+
variables: Record<string, string>;
|
|
17
|
+
userId: string;
|
|
18
|
+
ingestionId: string;
|
|
19
|
+
supabase?: SupabaseClient | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ActionResult {
|
|
23
|
+
success: boolean;
|
|
24
|
+
newFileState?: { path: string; name: string };
|
|
25
|
+
logs: string[];
|
|
26
|
+
trace: TraceLog[];
|
|
27
|
+
outputs?: Record<string, unknown>;
|
|
28
|
+
error?: string;
|
|
29
|
+
errorDetails?: Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ActionHandler {
|
|
33
|
+
execute(context: ActionContext): Promise<ActionResult>;
|
|
34
|
+
}
|