@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,142 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import axios from "axios";
|
|
3
|
+
import { asyncHandler } from "../middleware/errorHandler.js";
|
|
4
|
+
import { optionalAuth } from "../middleware/auth.js";
|
|
5
|
+
|
|
6
|
+
const router = Router();
|
|
7
|
+
|
|
8
|
+
router.use(optionalAuth);
|
|
9
|
+
|
|
10
|
+
// GET /api/accounts
|
|
11
|
+
router.get(
|
|
12
|
+
"/",
|
|
13
|
+
asyncHandler(async (req, res) => {
|
|
14
|
+
if (!req.user || !req.supabase) {
|
|
15
|
+
res.status(401).json({ error: "Authentication required" });
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { data, error } = await req.supabase
|
|
20
|
+
.from("integrations")
|
|
21
|
+
.select("id, provider, is_enabled, created_at, updated_at")
|
|
22
|
+
.eq("user_id", req.user.id);
|
|
23
|
+
|
|
24
|
+
if (error) {
|
|
25
|
+
res.status(500).json({ error: error.message });
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Map to expected frontend format
|
|
30
|
+
const accounts = data?.map(integration => ({
|
|
31
|
+
id: integration.id,
|
|
32
|
+
email_address: integration.provider, // Just for UI display
|
|
33
|
+
provider: integration.provider,
|
|
34
|
+
is_connected: integration.is_enabled,
|
|
35
|
+
sync_enabled: integration.is_enabled,
|
|
36
|
+
created_at: integration.created_at,
|
|
37
|
+
updated_at: integration.updated_at
|
|
38
|
+
})) || [];
|
|
39
|
+
|
|
40
|
+
res.json({ accounts });
|
|
41
|
+
})
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// POST /api/accounts/google-drive/auth-url
|
|
45
|
+
router.post(
|
|
46
|
+
"/google-drive/auth-url",
|
|
47
|
+
asyncHandler(async (req, res) => {
|
|
48
|
+
const { clientId } = req.body;
|
|
49
|
+
if (!clientId) {
|
|
50
|
+
res.status(400).json({ error: "Missing clientId" });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const redirectUri = "urn:ietf:wg:oauth:2.0:oob"; // Desktop/local app flow
|
|
55
|
+
|
|
56
|
+
// We request drive (full access) to allow downloading and uploading (moving) files
|
|
57
|
+
const params = new URLSearchParams({
|
|
58
|
+
client_id: clientId,
|
|
59
|
+
redirect_uri: redirectUri,
|
|
60
|
+
response_type: "code",
|
|
61
|
+
scope: "https://www.googleapis.com/auth/drive",
|
|
62
|
+
access_type: "offline",
|
|
63
|
+
prompt: "consent",
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
|
|
67
|
+
res.json({ authUrl });
|
|
68
|
+
})
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// POST /api/accounts/google-drive/connect
|
|
72
|
+
router.post(
|
|
73
|
+
"/google-drive/connect",
|
|
74
|
+
asyncHandler(async (req, res) => {
|
|
75
|
+
if (!req.supabase || !req.user) {
|
|
76
|
+
res.status(401).json({ error: "Authentication required" });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const { authCode, clientId, clientSecret } = req.body;
|
|
81
|
+
if (!authCode || !clientId || !clientSecret) {
|
|
82
|
+
res.status(400).json({ error: "Missing authCode, clientId, or clientSecret" });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
// Exchange code for tokens
|
|
88
|
+
const tokenResponse = await axios.post("https://oauth2.googleapis.com/token", null, {
|
|
89
|
+
params: {
|
|
90
|
+
client_id: clientId,
|
|
91
|
+
client_secret: clientSecret,
|
|
92
|
+
code: authCode,
|
|
93
|
+
grant_type: "authorization_code",
|
|
94
|
+
redirect_uri: "urn:ietf:wg:oauth:2.0:oob",
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const { access_token, refresh_token, expires_in } = tokenResponse.data;
|
|
99
|
+
|
|
100
|
+
const credentials = {
|
|
101
|
+
access_token,
|
|
102
|
+
refresh_token,
|
|
103
|
+
expires_at: Date.now() + expires_in * 1000,
|
|
104
|
+
client_id: clientId,
|
|
105
|
+
client_secret: clientSecret
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// Save to integrations
|
|
109
|
+
const { data: integration, error } = await req.supabase
|
|
110
|
+
.from("integrations")
|
|
111
|
+
.upsert(
|
|
112
|
+
{
|
|
113
|
+
user_id: req.user.id,
|
|
114
|
+
provider: "google_drive",
|
|
115
|
+
credentials,
|
|
116
|
+
is_enabled: true
|
|
117
|
+
},
|
|
118
|
+
{ onConflict: "user_id,provider" }
|
|
119
|
+
)
|
|
120
|
+
.select()
|
|
121
|
+
.single();
|
|
122
|
+
|
|
123
|
+
if (error) {
|
|
124
|
+
throw new Error(`Database error: ${error.message}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
res.json({ success: true, account: integration });
|
|
128
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
129
|
+
} catch (error: any) {
|
|
130
|
+
const errorMessage = error.response?.data?.error_description
|
|
131
|
+
|| error.response?.data?.error
|
|
132
|
+
|| error.message
|
|
133
|
+
|| "Failed to connect Google Drive";
|
|
134
|
+
|
|
135
|
+
res.status(500).json({
|
|
136
|
+
error: errorMessage
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
export default router;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { asyncHandler } from "../middleware/errorHandler.js";
|
|
3
|
+
import { optionalAuth } from "../middleware/auth.js";
|
|
4
|
+
import { BaselineConfigService, DEFAULT_BASELINE_FIELDS } from "../services/BaselineConfigService.js";
|
|
5
|
+
import { PolicyEngine } from "../services/PolicyEngine.js";
|
|
6
|
+
|
|
7
|
+
const router = Router();
|
|
8
|
+
router.use(optionalAuth);
|
|
9
|
+
|
|
10
|
+
// GET /api/baseline-config
|
|
11
|
+
// Returns the active config. If none exists, returns the built-in defaults
|
|
12
|
+
// with id: null so the UI can distinguish "never saved" from "saved and active".
|
|
13
|
+
router.get(
|
|
14
|
+
"/",
|
|
15
|
+
asyncHandler(async (req, res) => {
|
|
16
|
+
if (!req.supabase || !req.user) {
|
|
17
|
+
res.status(401).json({ success: false, error: "Authentication required" });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const config = await BaselineConfigService.getActive(req.supabase, req.user.id);
|
|
21
|
+
res.json({
|
|
22
|
+
success: true,
|
|
23
|
+
config,
|
|
24
|
+
defaults: DEFAULT_BASELINE_FIELDS,
|
|
25
|
+
});
|
|
26
|
+
})
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
// GET /api/baseline-config/history
|
|
30
|
+
// Returns all saved versions for the user, newest first.
|
|
31
|
+
router.get(
|
|
32
|
+
"/history",
|
|
33
|
+
asyncHandler(async (req, res) => {
|
|
34
|
+
if (!req.supabase || !req.user) {
|
|
35
|
+
res.status(401).json({ success: false, error: "Authentication required" });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const history = await BaselineConfigService.list(req.supabase, req.user.id);
|
|
39
|
+
res.json({ success: true, history });
|
|
40
|
+
})
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// POST /api/baseline-config
|
|
44
|
+
// Save a new version. Body: { context?, fields[], activate? }
|
|
45
|
+
// Always creates a new row — never mutates an existing version.
|
|
46
|
+
router.post(
|
|
47
|
+
"/",
|
|
48
|
+
asyncHandler(async (req, res) => {
|
|
49
|
+
if (!req.supabase || !req.user) {
|
|
50
|
+
res.status(401).json({ success: false, error: "Authentication required" });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const { context, fields, activate = true } = req.body;
|
|
54
|
+
if (!Array.isArray(fields) || fields.length === 0) {
|
|
55
|
+
res.status(400).json({ success: false, error: "fields array is required and must not be empty" });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const config = await BaselineConfigService.save(
|
|
59
|
+
req.supabase,
|
|
60
|
+
req.user.id,
|
|
61
|
+
{ context, fields },
|
|
62
|
+
activate
|
|
63
|
+
);
|
|
64
|
+
res.status(201).json({ success: true, config });
|
|
65
|
+
})
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// POST /api/baseline-config/:id/activate
|
|
69
|
+
// Activate a previously saved version.
|
|
70
|
+
router.post(
|
|
71
|
+
"/:id/activate",
|
|
72
|
+
asyncHandler(async (req, res) => {
|
|
73
|
+
if (!req.supabase || !req.user) {
|
|
74
|
+
res.status(401).json({ success: false, error: "Authentication required" });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const ok = await BaselineConfigService.activate(
|
|
78
|
+
req.supabase,
|
|
79
|
+
req.user.id,
|
|
80
|
+
req.params.id as string
|
|
81
|
+
);
|
|
82
|
+
if (!ok) {
|
|
83
|
+
res.status(404).json({ success: false, error: "Config version not found" });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
res.json({ success: true });
|
|
87
|
+
})
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// POST /api/baseline-config/suggest
|
|
91
|
+
// Body: { description, provider?, model? }
|
|
92
|
+
// Returns a draft { context, fields[] } for the user to review before saving.
|
|
93
|
+
router.post(
|
|
94
|
+
"/suggest",
|
|
95
|
+
asyncHandler(async (req, res) => {
|
|
96
|
+
if (!req.supabase || !req.user) {
|
|
97
|
+
res.status(401).json({ success: false, error: "Authentication required" });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const { description, provider, model } = req.body;
|
|
101
|
+
if (!description || typeof description !== "string") {
|
|
102
|
+
res.status(400).json({ success: false, error: "description is required" });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Pass current active fields so the LLM avoids duplicating them
|
|
107
|
+
const activeConfig = await BaselineConfigService.getActive(req.supabase, req.user.id);
|
|
108
|
+
const currentFields = activeConfig?.fields ?? DEFAULT_BASELINE_FIELDS;
|
|
109
|
+
|
|
110
|
+
const result = await PolicyEngine.suggestBaseline(description, currentFields, {
|
|
111
|
+
provider,
|
|
112
|
+
model,
|
|
113
|
+
userId: req.user.id,
|
|
114
|
+
supabase: req.supabase,
|
|
115
|
+
});
|
|
116
|
+
if (!result.suggestion) {
|
|
117
|
+
res.status(503).json({ success: false, error: result.error ?? "Suggestion failed. SDK may be unavailable." });
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
res.json({ success: true, suggestion: result.suggestion });
|
|
121
|
+
})
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
export default router;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { asyncHandler } from "../middleware/errorHandler.js";
|
|
3
|
+
import { optionalAuth } from "../middleware/auth.js";
|
|
4
|
+
import { ChatService } from "../services/ChatService.js";
|
|
5
|
+
|
|
6
|
+
const router = Router();
|
|
7
|
+
|
|
8
|
+
interface PostMessageBody {
|
|
9
|
+
sessionId?: string;
|
|
10
|
+
content?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// All chat routes require authentication
|
|
14
|
+
router.use(optionalAuth);
|
|
15
|
+
|
|
16
|
+
// GET /api/chat/sessions
|
|
17
|
+
router.get(
|
|
18
|
+
"/sessions",
|
|
19
|
+
asyncHandler(async (req, res) => {
|
|
20
|
+
if (!req.supabase || !req.user) {
|
|
21
|
+
res.status(401).json({ success: false, error: "Authentication required" });
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const { data: sessions, error } = await req.supabase
|
|
26
|
+
.from("chat_sessions")
|
|
27
|
+
.select("*")
|
|
28
|
+
.eq("user_id", req.user.id)
|
|
29
|
+
.order("updated_at", { ascending: false });
|
|
30
|
+
|
|
31
|
+
if (error) {
|
|
32
|
+
res.status(500).json({ success: false, error: error.message });
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
res.json({ success: true, sessions });
|
|
37
|
+
})
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// POST /api/chat/sessions
|
|
41
|
+
router.post(
|
|
42
|
+
"/sessions",
|
|
43
|
+
asyncHandler(async (req, res) => {
|
|
44
|
+
if (!req.supabase || !req.user) {
|
|
45
|
+
res.status(401).json({ success: false, error: "Authentication required" });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const { data: session, error } = await req.supabase
|
|
50
|
+
.from("chat_sessions")
|
|
51
|
+
.insert({ user_id: req.user.id, title: "New Conversation" })
|
|
52
|
+
.select("*")
|
|
53
|
+
.single();
|
|
54
|
+
|
|
55
|
+
if (error) {
|
|
56
|
+
res.status(500).json({ success: false, error: error.message });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
res.status(201).json({ success: true, session });
|
|
61
|
+
})
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// GET /api/chat/sessions/:id/messages
|
|
65
|
+
router.get(
|
|
66
|
+
"/sessions/:id/messages",
|
|
67
|
+
asyncHandler(async (req, res) => {
|
|
68
|
+
if (!req.supabase || !req.user) {
|
|
69
|
+
res.status(401).json({ success: false, error: "Authentication required" });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Verify session belongs to user
|
|
74
|
+
const { data: sessionData } = await req.supabase
|
|
75
|
+
.from("chat_sessions")
|
|
76
|
+
.select("id")
|
|
77
|
+
.eq("id", req.params["id"] as string)
|
|
78
|
+
.eq("user_id", req.user.id)
|
|
79
|
+
.maybeSingle();
|
|
80
|
+
|
|
81
|
+
if (!sessionData) {
|
|
82
|
+
res.status(404).json({ success: false, error: "Session not found" });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const { data: messages, error } = await req.supabase
|
|
87
|
+
.from("chat_messages")
|
|
88
|
+
.select("*")
|
|
89
|
+
.eq("session_id", req.params["id"] as string)
|
|
90
|
+
.eq("user_id", req.user.id)
|
|
91
|
+
.order("created_at", { ascending: true });
|
|
92
|
+
|
|
93
|
+
if (error) {
|
|
94
|
+
res.status(500).json({ success: false, error: error.message });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
res.json({ success: true, messages });
|
|
99
|
+
})
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// POST /api/chat/message
|
|
103
|
+
router.post(
|
|
104
|
+
"/message",
|
|
105
|
+
asyncHandler(async (req, res) => {
|
|
106
|
+
if (!req.supabase || !req.user) {
|
|
107
|
+
res.status(401).json({ success: false, error: "Authentication required" });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const { sessionId, content } = (req.body ?? {}) as PostMessageBody;
|
|
112
|
+
const normalizedSessionId = typeof sessionId === "string" ? sessionId : "";
|
|
113
|
+
const trimmedContent = typeof content === "string" ? content.trim() : "";
|
|
114
|
+
if (!normalizedSessionId || !trimmedContent) {
|
|
115
|
+
res.status(400).json({ success: false, error: "Missing sessionId or content" });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Verify session belongs to user
|
|
120
|
+
const { data: sessionData } = await req.supabase
|
|
121
|
+
.from("chat_sessions")
|
|
122
|
+
.select("id")
|
|
123
|
+
.eq("id", normalizedSessionId)
|
|
124
|
+
.eq("user_id", req.user.id)
|
|
125
|
+
.maybeSingle();
|
|
126
|
+
|
|
127
|
+
if (!sessionData) {
|
|
128
|
+
res.status(404).json({ success: false, error: "Session not found" });
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Dynamically name session if it's the first message
|
|
133
|
+
const { count } = await req.supabase
|
|
134
|
+
.from("chat_messages")
|
|
135
|
+
.select("*", { count: 'exact', head: true })
|
|
136
|
+
.eq("session_id", normalizedSessionId)
|
|
137
|
+
.eq("user_id", req.user.id);
|
|
138
|
+
|
|
139
|
+
if (count === 0 && trimmedContent.length > 3) {
|
|
140
|
+
const title = trimmedContent.substring(0, 30) + (trimmedContent.length > 30 ? "..." : "");
|
|
141
|
+
await req.supabase.from("chat_sessions").update({ title }).eq("id", normalizedSessionId);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const aiMessage = await ChatService.handleMessage(normalizedSessionId, req.user.id, trimmedContent, req.supabase);
|
|
146
|
+
res.json({ success: true, message: aiMessage });
|
|
147
|
+
} catch (error) {
|
|
148
|
+
const message = error instanceof Error ? error.message : "Failed to process message";
|
|
149
|
+
res.status(500).json({ success: false, error: message });
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
export default router;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { createClient } from "@supabase/supabase-js";
|
|
5
|
+
import { Router } from "express";
|
|
6
|
+
|
|
7
|
+
import { config } from "../config/index.js";
|
|
8
|
+
import { SDKService } from "../services/SDKService.js";
|
|
9
|
+
import { getServerSupabase, getSupabaseConfigFromHeaders } from "../services/supabase.js";
|
|
10
|
+
|
|
11
|
+
const router = Router();
|
|
12
|
+
|
|
13
|
+
let version = "0.0.0";
|
|
14
|
+
try {
|
|
15
|
+
const pkgPath = join(config.packageRoot, "package.json");
|
|
16
|
+
version = JSON.parse(readFileSync(pkgPath, "utf8")).version;
|
|
17
|
+
} catch {
|
|
18
|
+
// fallback stays at default
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
router.get("/", async (req, res) => {
|
|
22
|
+
let supabase = getServerSupabase();
|
|
23
|
+
|
|
24
|
+
if (!supabase) {
|
|
25
|
+
const headerConfig = getSupabaseConfigFromHeaders(req.headers as Record<string, unknown>);
|
|
26
|
+
if (headerConfig) {
|
|
27
|
+
supabase = createClient(headerConfig.url, headerConfig.anonKey, {
|
|
28
|
+
auth: {
|
|
29
|
+
autoRefreshToken: false,
|
|
30
|
+
persistSession: false
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let databaseStatus = "not_configured";
|
|
37
|
+
|
|
38
|
+
if (supabase) {
|
|
39
|
+
try {
|
|
40
|
+
const { error } = await supabase.from("user_settings").select("id").limit(1);
|
|
41
|
+
databaseStatus = error ? "error" : "connected";
|
|
42
|
+
} catch {
|
|
43
|
+
databaseStatus = "error";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const sdkAvailable = await SDKService.isAvailable();
|
|
48
|
+
|
|
49
|
+
res.json({
|
|
50
|
+
status: "healthy",
|
|
51
|
+
timestamp: new Date().toISOString(),
|
|
52
|
+
version,
|
|
53
|
+
environment: config.nodeEnv,
|
|
54
|
+
services: {
|
|
55
|
+
database: databaseStatus,
|
|
56
|
+
realtimeXSdk: sdkAvailable ? "available" : "unavailable"
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export default router;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
|
|
3
|
+
import healthRoutes from "./health.js";
|
|
4
|
+
import migrateRoutes from "./migrate.js";
|
|
5
|
+
import processingRoutes from "./processing.js";
|
|
6
|
+
import setupRoutes from "./setup.js";
|
|
7
|
+
import ttsRoutes from "./tts.js";
|
|
8
|
+
import sdkRoutes from "./sdk.js";
|
|
9
|
+
import policiesRoutes from "./policies.js";
|
|
10
|
+
import ingestionsRoutes from "./ingestions.js";
|
|
11
|
+
import baselineConfigRoutes from "./baseline-config.js";
|
|
12
|
+
import accountsRoutes from "./accounts.js";
|
|
13
|
+
import settingsRoutes from "./settings.js";
|
|
14
|
+
import rulesRoutes from "./rules.js";
|
|
15
|
+
import chatRoutes from "./chat.js";
|
|
16
|
+
import statsRoutes from "./stats.js";
|
|
17
|
+
|
|
18
|
+
const router = Router();
|
|
19
|
+
|
|
20
|
+
router.use("/health", healthRoutes);
|
|
21
|
+
router.use("/migrate", migrateRoutes);
|
|
22
|
+
router.use("/setup", setupRoutes);
|
|
23
|
+
router.use("/processing", processingRoutes);
|
|
24
|
+
router.use("/tts", ttsRoutes);
|
|
25
|
+
router.use("/sdk", sdkRoutes);
|
|
26
|
+
router.use("/policies", policiesRoutes);
|
|
27
|
+
router.use("/ingestions", ingestionsRoutes);
|
|
28
|
+
router.use("/baseline-config", baselineConfigRoutes);
|
|
29
|
+
router.use("/accounts", accountsRoutes);
|
|
30
|
+
router.use("/settings", settingsRoutes);
|
|
31
|
+
router.use("/rules", rulesRoutes);
|
|
32
|
+
router.use("/chat", chatRoutes);
|
|
33
|
+
router.use("/stats", statsRoutes);
|
|
34
|
+
|
|
35
|
+
export default router;
|