@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,42 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { pickString, pickColumns, interpolate, getNestedVariable } from "./utils.js";
|
|
4
|
+
import { Actuator } from "../Actuator.js";
|
|
5
|
+
export class LogCsvAction {
|
|
6
|
+
async execute(context) {
|
|
7
|
+
const { action, variables, data, userId, ingestionId, supabase } = context;
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
|
+
const csvPathTemplate = pickString(action, "path");
|
|
10
|
+
if (!csvPathTemplate) {
|
|
11
|
+
return {
|
|
12
|
+
success: false,
|
|
13
|
+
logs: [],
|
|
14
|
+
trace: [{ timestamp: new Date().toISOString(), step: "Log CSV failed: missing path" }],
|
|
15
|
+
error: "Log CSV action requires a 'path' config"
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const csvPath = interpolate(csvPathTemplate, variables, data);
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
+
const cols = pickColumns(action, Object.keys(data));
|
|
21
|
+
const row = cols.map((c) => getNestedVariable(c, variables, data) ?? "").join(",") + "\n";
|
|
22
|
+
const header = cols.join(",") + "\n";
|
|
23
|
+
if (!fs.existsSync(csvPath)) {
|
|
24
|
+
fs.mkdirSync(path.dirname(csvPath), { recursive: true });
|
|
25
|
+
fs.writeFileSync(csvPath, header + row, "utf-8");
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
fs.appendFileSync(csvPath, row, "utf-8");
|
|
29
|
+
}
|
|
30
|
+
const trace = [{
|
|
31
|
+
timestamp: new Date().toISOString(),
|
|
32
|
+
step: "Executed log_csv action",
|
|
33
|
+
details: { csvPath, cols }
|
|
34
|
+
}];
|
|
35
|
+
Actuator.logEvent(ingestionId, userId, "action", "Action Execution", { action: "log_csv", csvPath, cols }, supabase);
|
|
36
|
+
return {
|
|
37
|
+
success: true,
|
|
38
|
+
logs: [`Logged CSV → ${csvPath}`],
|
|
39
|
+
trace
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { pickString, interpolate } from "./utils.js";
|
|
2
|
+
import { Actuator } from "../Actuator.js";
|
|
3
|
+
import { createLogger } from "../logger.js";
|
|
4
|
+
const logger = createLogger("NotifyAction");
|
|
5
|
+
export class NotifyAction {
|
|
6
|
+
async execute(context) {
|
|
7
|
+
const { action, variables, data, userId, ingestionId, supabase } = context;
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
|
+
const messageTemplate = pickString(action, "message");
|
|
10
|
+
if (!messageTemplate) {
|
|
11
|
+
return {
|
|
12
|
+
success: false,
|
|
13
|
+
logs: [],
|
|
14
|
+
trace: [{ timestamp: new Date().toISOString(), step: "Notify failed: missing message" }],
|
|
15
|
+
error: "Notify action requires a 'message' config"
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const msg = interpolate(messageTemplate, variables, data);
|
|
19
|
+
logger.info(`[NOTIFY] ${msg}`);
|
|
20
|
+
const trace = [{
|
|
21
|
+
timestamp: new Date().toISOString(),
|
|
22
|
+
step: "Executed notify action",
|
|
23
|
+
details: { message: msg }
|
|
24
|
+
}];
|
|
25
|
+
Actuator.logEvent(ingestionId, userId, "action", "Action Execution", { action: "notify", message: msg }, supabase);
|
|
26
|
+
return {
|
|
27
|
+
success: true,
|
|
28
|
+
logs: [`Notified: ${msg}`],
|
|
29
|
+
trace
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { pickString, interpolate } from "./utils.js";
|
|
4
|
+
import { Actuator } from "../Actuator.js";
|
|
5
|
+
import { getServiceRoleSupabase } from "../../services/supabase.js";
|
|
6
|
+
export class RenameAction {
|
|
7
|
+
async execute(context) {
|
|
8
|
+
const { action, file, variables, data, userId, ingestionId, supabase } = context;
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
|
+
const pattern = pickString(action, "pattern");
|
|
11
|
+
if (!pattern) {
|
|
12
|
+
return {
|
|
13
|
+
success: false,
|
|
14
|
+
logs: [],
|
|
15
|
+
trace: [{ timestamp: new Date().toISOString(), step: "Rename failed: missing pattern" }],
|
|
16
|
+
error: "Rename action requires a 'pattern' config"
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
const ext = path.extname(file.path);
|
|
20
|
+
const dir = path.dirname(file.path);
|
|
21
|
+
let newName = interpolate(pattern, variables, data);
|
|
22
|
+
if (!newName.endsWith(ext))
|
|
23
|
+
newName += ext;
|
|
24
|
+
const newPath = path.join(dir, newName);
|
|
25
|
+
await new Promise((resolve, reject) => {
|
|
26
|
+
fs.rename(file.path, newPath, (err) => {
|
|
27
|
+
if (err)
|
|
28
|
+
reject(err);
|
|
29
|
+
else
|
|
30
|
+
resolve();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
const trace = [{
|
|
34
|
+
timestamp: new Date().toISOString(),
|
|
35
|
+
step: `Renamed file to ${newName}`,
|
|
36
|
+
details: { original: file.name, new: newName }
|
|
37
|
+
}];
|
|
38
|
+
Actuator.logEvent(ingestionId, userId, "action", "Action Execution", { action: "rename", original: file.name, new: newName }, supabase);
|
|
39
|
+
// Update DB so re-runs don't break
|
|
40
|
+
const db = supabase ?? getServiceRoleSupabase();
|
|
41
|
+
if (db) {
|
|
42
|
+
await db.from("ingestions").update({ storage_path: newPath, filename: newName }).eq("id", ingestionId);
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
success: true,
|
|
46
|
+
newFileState: { path: newPath, name: newName },
|
|
47
|
+
logs: [`Renamed to '${newName}'`],
|
|
48
|
+
trace
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { pickString, interpolate } from "./utils.js";
|
|
2
|
+
import { Actuator } from "../Actuator.js";
|
|
3
|
+
export class WebhookAction {
|
|
4
|
+
async execute(context) {
|
|
5
|
+
const { action, variables, data, userId, ingestionId, supabase } = context;
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
7
|
+
const webhookUrlTemplate = pickString(action, "url");
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
|
+
const webhookPayloadTemplate = pickString(action, "payload");
|
|
10
|
+
if (!webhookUrlTemplate || !webhookPayloadTemplate) {
|
|
11
|
+
return {
|
|
12
|
+
success: false,
|
|
13
|
+
logs: [],
|
|
14
|
+
trace: [{ timestamp: new Date().toISOString(), step: "Webhook failed: missing url or payload" }],
|
|
15
|
+
error: "Webhook action requires 'url' and 'payload' configs"
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const url = interpolate(webhookUrlTemplate, variables, data);
|
|
19
|
+
const payloadStr = interpolate(webhookPayloadTemplate, variables, data);
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
21
|
+
let payload;
|
|
22
|
+
try {
|
|
23
|
+
payload = JSON.parse(payloadStr);
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
return {
|
|
28
|
+
success: false,
|
|
29
|
+
logs: [],
|
|
30
|
+
trace: [{ timestamp: new Date().toISOString(), step: "Webhook failed: invalid JSON payload" }],
|
|
31
|
+
error: "Webhook payload must be valid JSON"
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
await fetch(url, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: { "Content-Type": "application/json" },
|
|
37
|
+
body: JSON.stringify(payload),
|
|
38
|
+
});
|
|
39
|
+
const trace = [{
|
|
40
|
+
timestamp: new Date().toISOString(),
|
|
41
|
+
step: `Webhook payload sent to ${url}`,
|
|
42
|
+
details: { url, payload }
|
|
43
|
+
}];
|
|
44
|
+
Actuator.logEvent(ingestionId, userId, "action", "Action Execution", { action: "webhook", url }, supabase);
|
|
45
|
+
return {
|
|
46
|
+
success: true,
|
|
47
|
+
logs: [`Logged via webhook`],
|
|
48
|
+
trace
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { createLogger } from "../logger.js";
|
|
2
|
+
const logger = createLogger("ActionUtils");
|
|
3
|
+
// ─── Variable Interpolation ────────────────────────────────────────────────
|
|
4
|
+
function isRecord(value) {
|
|
5
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
function maybeParseJson(value) {
|
|
8
|
+
if (typeof value !== "string")
|
|
9
|
+
return undefined;
|
|
10
|
+
const trimmed = value.trim();
|
|
11
|
+
if (!trimmed)
|
|
12
|
+
return undefined;
|
|
13
|
+
if (!((trimmed.startsWith("{") && trimmed.endsWith("}")) || (trimmed.startsWith("[") && trimmed.endsWith("]")))) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(trimmed);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function tokenizePath(path) {
|
|
24
|
+
const tokens = [];
|
|
25
|
+
let i = 0;
|
|
26
|
+
while (i < path.length) {
|
|
27
|
+
const ch = path[i];
|
|
28
|
+
if (ch === ".") {
|
|
29
|
+
i += 1;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (ch === "[") {
|
|
33
|
+
const end = path.indexOf("]", i + 1);
|
|
34
|
+
if (end < 0)
|
|
35
|
+
break;
|
|
36
|
+
const raw = path.slice(i + 1, end).trim();
|
|
37
|
+
if (/^\d+$/.test(raw)) {
|
|
38
|
+
tokens.push(Number(raw));
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
const unquoted = raw.replace(/^["']|["']$/g, "");
|
|
42
|
+
if (unquoted)
|
|
43
|
+
tokens.push(unquoted);
|
|
44
|
+
}
|
|
45
|
+
i = end + 1;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
let j = i;
|
|
49
|
+
while (j < path.length && path[j] !== "." && path[j] !== "[") {
|
|
50
|
+
j += 1;
|
|
51
|
+
}
|
|
52
|
+
const token = path.slice(i, j).trim();
|
|
53
|
+
if (token)
|
|
54
|
+
tokens.push(token);
|
|
55
|
+
i = j;
|
|
56
|
+
}
|
|
57
|
+
return tokens;
|
|
58
|
+
}
|
|
59
|
+
function toTemplateString(value) {
|
|
60
|
+
if (value == null)
|
|
61
|
+
return undefined;
|
|
62
|
+
if (typeof value === "string")
|
|
63
|
+
return value;
|
|
64
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
65
|
+
return String(value);
|
|
66
|
+
try {
|
|
67
|
+
return JSON.stringify(value);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return String(value);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export function getNestedVariable(keyPath, vars, data) {
|
|
74
|
+
const key = keyPath.trim();
|
|
75
|
+
if (!key)
|
|
76
|
+
return undefined;
|
|
77
|
+
if (vars[key] !== undefined) {
|
|
78
|
+
return vars[key];
|
|
79
|
+
}
|
|
80
|
+
const root = {
|
|
81
|
+
...(data ?? {}),
|
|
82
|
+
...vars,
|
|
83
|
+
};
|
|
84
|
+
const tokens = tokenizePath(key);
|
|
85
|
+
if (tokens.length === 0)
|
|
86
|
+
return undefined;
|
|
87
|
+
let current = root;
|
|
88
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
89
|
+
if (typeof current === "string") {
|
|
90
|
+
const parsed = maybeParseJson(current);
|
|
91
|
+
if (parsed !== undefined) {
|
|
92
|
+
current = parsed;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const token = tokens[i];
|
|
96
|
+
if (Array.isArray(current)) {
|
|
97
|
+
if (typeof token !== "number")
|
|
98
|
+
return undefined;
|
|
99
|
+
current = current[token];
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (!isRecord(current)) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
if (typeof token === "number") {
|
|
106
|
+
current = current[String(token)];
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const lookupToken = i === 0 && token === "enrichment" && current["_enrichment"] !== undefined
|
|
110
|
+
? "_enrichment"
|
|
111
|
+
: token;
|
|
112
|
+
current = current[lookupToken];
|
|
113
|
+
}
|
|
114
|
+
return toTemplateString(current);
|
|
115
|
+
}
|
|
116
|
+
export function interpolate(template, vars, data) {
|
|
117
|
+
return template.replace(/\{([^{}]+)\}/g, (match, rawKey) => {
|
|
118
|
+
const resolved = getNestedVariable(rawKey, vars, data);
|
|
119
|
+
return resolved ?? match;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Derive computed variables from extracted data using transformer definitions.
|
|
124
|
+
*/
|
|
125
|
+
export function deriveVariables(data, fields) {
|
|
126
|
+
const vars = {};
|
|
127
|
+
// Populate raw extracted values as strings
|
|
128
|
+
for (const [k, v] of Object.entries(data)) {
|
|
129
|
+
const serialized = toTemplateString(v);
|
|
130
|
+
if (serialized !== undefined)
|
|
131
|
+
vars[k] = serialized;
|
|
132
|
+
}
|
|
133
|
+
// Run transformers
|
|
134
|
+
for (const field of fields) {
|
|
135
|
+
if (!field.transformers)
|
|
136
|
+
continue;
|
|
137
|
+
const rawValue = vars[field.key];
|
|
138
|
+
if (!rawValue)
|
|
139
|
+
continue;
|
|
140
|
+
for (const t of field.transformers) {
|
|
141
|
+
try {
|
|
142
|
+
if (t.name === "get_year") {
|
|
143
|
+
vars[t.as] = new Date(rawValue).getFullYear().toString();
|
|
144
|
+
}
|
|
145
|
+
else if (t.name === "get_month_name") {
|
|
146
|
+
vars[t.as] = new Date(rawValue).toLocaleString("en-US", { month: "long" });
|
|
147
|
+
}
|
|
148
|
+
else if (t.name === "get_month") {
|
|
149
|
+
vars[t.as] = String(new Date(rawValue).getMonth() + 1).padStart(2, "0");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
logger.warn(`Transformer '${t.name}' failed for key '${field.key}'`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return vars;
|
|
158
|
+
}
|
|
159
|
+
export function pickString(action, key) {
|
|
160
|
+
const value = action.config?.[key];
|
|
161
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
162
|
+
return value;
|
|
163
|
+
}
|
|
164
|
+
const legacyValue = action[key];
|
|
165
|
+
if (typeof legacyValue === "string" && legacyValue.trim().length > 0) {
|
|
166
|
+
return legacyValue;
|
|
167
|
+
}
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
// ─── Filename Helpers ──────────────────────────────────────────────────────
|
|
171
|
+
/**
|
|
172
|
+
* Build a filename stem from extracted metadata when suggested_filename is unavailable.
|
|
173
|
+
* Format: YYYY-MM-DD_Issuer_DocType (any missing parts are simply omitted)
|
|
174
|
+
*/
|
|
175
|
+
export function deriveNameFromVariables(variables) {
|
|
176
|
+
const parts = [];
|
|
177
|
+
if (variables.date) {
|
|
178
|
+
const d = new Date(variables.date);
|
|
179
|
+
if (!isNaN(d.getTime())) {
|
|
180
|
+
const yyyy = d.getFullYear();
|
|
181
|
+
const MM = String(d.getMonth() + 1).padStart(2, "0");
|
|
182
|
+
const dd = String(d.getDate()).padStart(2, "0");
|
|
183
|
+
parts.push(`${yyyy}-${MM}-${dd}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (variables.issuer) {
|
|
187
|
+
parts.push(variables.issuer.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, ""));
|
|
188
|
+
}
|
|
189
|
+
if (variables.document_type) {
|
|
190
|
+
parts.push(variables.document_type.replace(/\s+/g, "-").replace(/[^a-zA-Z0-9-]+/g, ""));
|
|
191
|
+
}
|
|
192
|
+
if (variables.amount || variables.total_amount) {
|
|
193
|
+
const raw = (variables.amount ?? variables.total_amount).replace(/[^0-9.$€£]/g, "");
|
|
194
|
+
if (raw)
|
|
195
|
+
parts.push(raw);
|
|
196
|
+
}
|
|
197
|
+
return parts.length > 0 ? parts.join("_") : null;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Resolve a final filename given the action's `filename` config field.
|
|
201
|
+
*
|
|
202
|
+
* Modes:
|
|
203
|
+
* undefined / "" / "original" → keep original stem + ext
|
|
204
|
+
* "auto" → AI suggested_filename → derived → originalStem, then + ext
|
|
205
|
+
* any other string → treat as a {variable} interpolation pattern
|
|
206
|
+
*/
|
|
207
|
+
export function resolveFilename(filenameConfig, variables, originalStem, ext, data) {
|
|
208
|
+
if (!filenameConfig || filenameConfig === "original") {
|
|
209
|
+
return originalStem + ext;
|
|
210
|
+
}
|
|
211
|
+
if (filenameConfig === "auto") {
|
|
212
|
+
const smart = deriveNameFromVariables(variables) ||
|
|
213
|
+
variables.suggested_filename?.trim() ||
|
|
214
|
+
originalStem;
|
|
215
|
+
return smart.endsWith(ext) ? smart : smart + ext;
|
|
216
|
+
}
|
|
217
|
+
// Custom interpolation pattern
|
|
218
|
+
const interpolated = interpolate(filenameConfig, variables, data);
|
|
219
|
+
return interpolated.endsWith(ext) ? interpolated : interpolated + ext;
|
|
220
|
+
}
|
|
221
|
+
export function pickColumns(action, fallback) {
|
|
222
|
+
const value = action.config?.columns;
|
|
223
|
+
if (Array.isArray(value)) {
|
|
224
|
+
return value.map((item) => String(item).trim()).filter(Boolean);
|
|
225
|
+
}
|
|
226
|
+
if (typeof value === "string") {
|
|
227
|
+
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
228
|
+
}
|
|
229
|
+
const legacyColumns = action.columns;
|
|
230
|
+
if (Array.isArray(legacyColumns)) {
|
|
231
|
+
return legacyColumns.map((item) => String(item).trim()).filter(Boolean);
|
|
232
|
+
}
|
|
233
|
+
if (typeof legacyColumns === "string") {
|
|
234
|
+
return legacyColumns.split(",").map((item) => item.trim()).filter(Boolean);
|
|
235
|
+
}
|
|
236
|
+
return fallback;
|
|
237
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export function normalizeLlmContent(content) {
|
|
2
|
+
if (typeof content === "string")
|
|
3
|
+
return content;
|
|
4
|
+
if (typeof content === "number" || typeof content === "boolean")
|
|
5
|
+
return String(content);
|
|
6
|
+
if (Array.isArray(content)) {
|
|
7
|
+
return content
|
|
8
|
+
.map((part) => {
|
|
9
|
+
if (part && typeof part === "object") {
|
|
10
|
+
const obj = part;
|
|
11
|
+
if (typeof obj.text === "string")
|
|
12
|
+
return obj.text;
|
|
13
|
+
if (typeof obj.content === "string")
|
|
14
|
+
return obj.content;
|
|
15
|
+
}
|
|
16
|
+
return normalizeLlmContent(part);
|
|
17
|
+
})
|
|
18
|
+
.filter(Boolean)
|
|
19
|
+
.join("\n");
|
|
20
|
+
}
|
|
21
|
+
if (content && typeof content === "object") {
|
|
22
|
+
const obj = content;
|
|
23
|
+
if (typeof obj.text === "string")
|
|
24
|
+
return obj.text;
|
|
25
|
+
if (typeof obj.content === "string")
|
|
26
|
+
return obj.content;
|
|
27
|
+
if ("content" in obj)
|
|
28
|
+
return normalizeLlmContent(obj.content);
|
|
29
|
+
try {
|
|
30
|
+
return JSON.stringify(obj);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return String(obj);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return "";
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Robustly extracts the text payload from various SDK adapter response shapes.
|
|
40
|
+
*/
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
42
|
+
export function extractLlmResponse(result) {
|
|
43
|
+
if (!result)
|
|
44
|
+
return "";
|
|
45
|
+
const candidates = [
|
|
46
|
+
result.response?.content,
|
|
47
|
+
result.message?.content,
|
|
48
|
+
result.content,
|
|
49
|
+
result.text,
|
|
50
|
+
result.choices?.[0]?.message?.content,
|
|
51
|
+
result.result,
|
|
52
|
+
result.output,
|
|
53
|
+
];
|
|
54
|
+
for (const candidate of candidates) {
|
|
55
|
+
const normalized = normalizeLlmContent(candidate);
|
|
56
|
+
if (normalized)
|
|
57
|
+
return normalized;
|
|
58
|
+
}
|
|
59
|
+
return "";
|
|
60
|
+
}
|
|
61
|
+
export function previewLlmText(raw, maxChars = 240) {
|
|
62
|
+
return raw.replace(/\s+/g, " ").trim().slice(0, maxChars);
|
|
63
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
class LoggerCore {
|
|
2
|
+
static persistence = null;
|
|
3
|
+
static setPersistence(supabase, userId) {
|
|
4
|
+
this.persistence = { supabase, userId };
|
|
5
|
+
}
|
|
6
|
+
static clearPersistence() {
|
|
7
|
+
this.persistence = null;
|
|
8
|
+
}
|
|
9
|
+
static async persist(level, scope, message, data) {
|
|
10
|
+
if (!this.persistence) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
await this.persistence.supabase.from("system_logs").insert({
|
|
15
|
+
user_id: this.persistence.userId,
|
|
16
|
+
level,
|
|
17
|
+
scope,
|
|
18
|
+
message,
|
|
19
|
+
metadata: data || {}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// persistence is best-effort and should never crash request flow
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export const Logger = LoggerCore;
|
|
28
|
+
export function createLogger(scope) {
|
|
29
|
+
function write(level, message, data) {
|
|
30
|
+
const line = `[${scope}] ${message}`;
|
|
31
|
+
if (level === "error") {
|
|
32
|
+
console.error(line, data || "");
|
|
33
|
+
}
|
|
34
|
+
else if (level === "warn") {
|
|
35
|
+
console.warn(line, data || "");
|
|
36
|
+
}
|
|
37
|
+
else if (level === "info") {
|
|
38
|
+
console.info(line, data || "");
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
console.debug(line, data || "");
|
|
42
|
+
}
|
|
43
|
+
void Logger.persist(level, scope, message, data);
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
debug: (message, data) => write("debug", message, data),
|
|
47
|
+
info: (message, data) => write("info", message, data),
|
|
48
|
+
warn: (message, data) => write("warn", message, data),
|
|
49
|
+
error: (message, data) => write("error", message, data)
|
|
50
|
+
};
|
|
51
|
+
}
|