@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.
Files changed (163) hide show
  1. package/.env.example +20 -0
  2. package/README.md +63 -0
  3. package/api/server.ts +130 -0
  4. package/api/src/config/index.ts +96 -0
  5. package/api/src/middleware/auth.ts +128 -0
  6. package/api/src/middleware/errorHandler.ts +88 -0
  7. package/api/src/middleware/index.ts +4 -0
  8. package/api/src/middleware/rateLimit.ts +71 -0
  9. package/api/src/middleware/validation.ts +58 -0
  10. package/api/src/routes/accounts.ts +142 -0
  11. package/api/src/routes/baseline-config.ts +124 -0
  12. package/api/src/routes/chat.ts +154 -0
  13. package/api/src/routes/health.ts +61 -0
  14. package/api/src/routes/index.ts +35 -0
  15. package/api/src/routes/ingestions.ts +275 -0
  16. package/api/src/routes/migrate.ts +112 -0
  17. package/api/src/routes/policies.ts +121 -0
  18. package/api/src/routes/processing.ts +90 -0
  19. package/api/src/routes/rules.ts +11 -0
  20. package/api/src/routes/sdk.ts +100 -0
  21. package/api/src/routes/settings.ts +80 -0
  22. package/api/src/routes/setup.ts +389 -0
  23. package/api/src/routes/stats.ts +81 -0
  24. package/api/src/routes/tts.ts +190 -0
  25. package/api/src/services/BaselineConfigService.ts +208 -0
  26. package/api/src/services/ChatService.ts +204 -0
  27. package/api/src/services/GoogleDriveService.ts +331 -0
  28. package/api/src/services/GoogleSheetsService.ts +1107 -0
  29. package/api/src/services/IngestionService.ts +1187 -0
  30. package/api/src/services/ModelCapabilityService.ts +248 -0
  31. package/api/src/services/PolicyEngine.ts +1625 -0
  32. package/api/src/services/PolicyLearningService.ts +527 -0
  33. package/api/src/services/PolicyLoader.ts +249 -0
  34. package/api/src/services/RAGService.ts +391 -0
  35. package/api/src/services/SDKService.ts +249 -0
  36. package/api/src/services/supabase.ts +113 -0
  37. package/api/src/utils/Actuator.ts +284 -0
  38. package/api/src/utils/actions/ActionHandler.ts +34 -0
  39. package/api/src/utils/actions/AppendToGSheetAction.ts +260 -0
  40. package/api/src/utils/actions/AutoRenameAction.ts +58 -0
  41. package/api/src/utils/actions/CopyAction.ts +120 -0
  42. package/api/src/utils/actions/CopyToGDriveAction.ts +64 -0
  43. package/api/src/utils/actions/LogCsvAction.ts +48 -0
  44. package/api/src/utils/actions/NotifyAction.ts +39 -0
  45. package/api/src/utils/actions/RenameAction.ts +57 -0
  46. package/api/src/utils/actions/WebhookAction.ts +58 -0
  47. package/api/src/utils/actions/utils.ts +293 -0
  48. package/api/src/utils/llmResponse.ts +61 -0
  49. package/api/src/utils/logger.ts +67 -0
  50. package/bin/folio-deploy.js +12 -0
  51. package/bin/folio-setup.js +45 -0
  52. package/bin/folio.js +65 -0
  53. package/dist/api/server.js +106 -0
  54. package/dist/api/src/config/index.js +81 -0
  55. package/dist/api/src/middleware/auth.js +93 -0
  56. package/dist/api/src/middleware/errorHandler.js +73 -0
  57. package/dist/api/src/middleware/index.js +4 -0
  58. package/dist/api/src/middleware/rateLimit.js +43 -0
  59. package/dist/api/src/middleware/validation.js +54 -0
  60. package/dist/api/src/routes/accounts.js +110 -0
  61. package/dist/api/src/routes/baseline-config.js +91 -0
  62. package/dist/api/src/routes/chat.js +114 -0
  63. package/dist/api/src/routes/health.js +52 -0
  64. package/dist/api/src/routes/index.js +31 -0
  65. package/dist/api/src/routes/ingestions.js +207 -0
  66. package/dist/api/src/routes/migrate.js +91 -0
  67. package/dist/api/src/routes/policies.js +86 -0
  68. package/dist/api/src/routes/processing.js +75 -0
  69. package/dist/api/src/routes/rules.js +8 -0
  70. package/dist/api/src/routes/sdk.js +80 -0
  71. package/dist/api/src/routes/settings.js +68 -0
  72. package/dist/api/src/routes/setup.js +315 -0
  73. package/dist/api/src/routes/stats.js +62 -0
  74. package/dist/api/src/routes/tts.js +178 -0
  75. package/dist/api/src/services/BaselineConfigService.js +168 -0
  76. package/dist/api/src/services/ChatService.js +166 -0
  77. package/dist/api/src/services/GoogleDriveService.js +280 -0
  78. package/dist/api/src/services/GoogleSheetsService.js +795 -0
  79. package/dist/api/src/services/IngestionService.js +990 -0
  80. package/dist/api/src/services/ModelCapabilityService.js +179 -0
  81. package/dist/api/src/services/PolicyEngine.js +1353 -0
  82. package/dist/api/src/services/PolicyLearningService.js +397 -0
  83. package/dist/api/src/services/PolicyLoader.js +159 -0
  84. package/dist/api/src/services/RAGService.js +295 -0
  85. package/dist/api/src/services/SDKService.js +212 -0
  86. package/dist/api/src/services/supabase.js +72 -0
  87. package/dist/api/src/utils/Actuator.js +225 -0
  88. package/dist/api/src/utils/actions/ActionHandler.js +1 -0
  89. package/dist/api/src/utils/actions/AppendToGSheetAction.js +191 -0
  90. package/dist/api/src/utils/actions/AutoRenameAction.js +49 -0
  91. package/dist/api/src/utils/actions/CopyAction.js +112 -0
  92. package/dist/api/src/utils/actions/CopyToGDriveAction.js +55 -0
  93. package/dist/api/src/utils/actions/LogCsvAction.js +42 -0
  94. package/dist/api/src/utils/actions/NotifyAction.js +32 -0
  95. package/dist/api/src/utils/actions/RenameAction.js +51 -0
  96. package/dist/api/src/utils/actions/WebhookAction.js +51 -0
  97. package/dist/api/src/utils/actions/utils.js +237 -0
  98. package/dist/api/src/utils/llmResponse.js +63 -0
  99. package/dist/api/src/utils/logger.js +51 -0
  100. package/dist/assets/index-DzN8-j-e.css +1 -0
  101. package/dist/assets/index-Uy-ai3Dh.js +113 -0
  102. package/dist/favicon.svg +31 -0
  103. package/dist/folio-logo.svg +46 -0
  104. package/dist/index.html +14 -0
  105. package/docs-dev/FPE-spec.md +196 -0
  106. package/docs-dev/folio-prd.md +47 -0
  107. package/docs-dev/foundation-checklist.md +30 -0
  108. package/docs-dev/hybrid-routing-architecture.md +205 -0
  109. package/docs-dev/ingestion-engine.md +69 -0
  110. package/docs-dev/port-from-email-automator.md +32 -0
  111. package/docs-dev/tech-spec.md +98 -0
  112. package/index.html +13 -0
  113. package/package.json +101 -0
  114. package/public/favicon.svg +31 -0
  115. package/public/folio-logo.svg +46 -0
  116. package/scripts/dev-task.mjs +51 -0
  117. package/scripts/get-latest-migration-timestamp.mjs +34 -0
  118. package/scripts/migrate.sh +91 -0
  119. package/supabase/.temp/cli-latest +1 -0
  120. package/supabase/.temp/gotrue-version +1 -0
  121. package/supabase/.temp/pooler-url +1 -0
  122. package/supabase/.temp/postgres-version +1 -0
  123. package/supabase/.temp/project-ref +1 -0
  124. package/supabase/.temp/rest-version +1 -0
  125. package/supabase/.temp/storage-migration +1 -0
  126. package/supabase/.temp/storage-version +1 -0
  127. package/supabase/config.toml +64 -0
  128. package/supabase/functions/_shared/auth.ts +35 -0
  129. package/supabase/functions/_shared/cors.ts +12 -0
  130. package/supabase/functions/_shared/supabaseAdmin.ts +17 -0
  131. package/supabase/functions/api-v1-settings/index.ts +66 -0
  132. package/supabase/functions/setup/index.ts +91 -0
  133. package/supabase/migrations/20260223000000_initial_foundation.sql +136 -0
  134. package/supabase/migrations/20260223000001_add_migration_rpc.sql +10 -0
  135. package/supabase/migrations/20260224000002_add_init_state_view.sql +20 -0
  136. package/supabase/migrations/20260224000003_port_user_creation_parity.sql +139 -0
  137. package/supabase/migrations/20260224000004_add_avatars_storage.sql +26 -0
  138. package/supabase/migrations/20260224000005_add_tts_and_embed_settings.sql +24 -0
  139. package/supabase/migrations/20260224000006_add_policies_table.sql +48 -0
  140. package/supabase/migrations/20260224000007_fix_migration_rpc.sql +9 -0
  141. package/supabase/migrations/20260224000008_add_ingestions_table.sql +42 -0
  142. package/supabase/migrations/20260225000000_setup_compatible_mode.sql +119 -0
  143. package/supabase/migrations/20260225000001_restore_ingestions.sql +49 -0
  144. package/supabase/migrations/20260225000002_add_ingestion_trace.sql +2 -0
  145. package/supabase/migrations/20260225000003_add_baseline_configs.sql +35 -0
  146. package/supabase/migrations/20260226000000_add_processing_events.sql +26 -0
  147. package/supabase/migrations/20260226000001_add_ingestion_file_hash.sql +10 -0
  148. package/supabase/migrations/20260226000002_add_dynamic_rag.sql +150 -0
  149. package/supabase/migrations/20260226000003_add_ingestion_summary.sql +4 -0
  150. package/supabase/migrations/20260226000004_add_ingestion_tags.sql +7 -0
  151. package/supabase/migrations/20260226000005_add_chat_tables.sql +60 -0
  152. package/supabase/migrations/20260227000000_harden_chat_messages_rls.sql +25 -0
  153. package/supabase/migrations/20260228000000_add_vision_model_capabilities.sql +8 -0
  154. package/supabase/migrations/20260228000001_add_policy_match_feedback.sql +51 -0
  155. package/supabase/migrations/29991231235959_test_migration.sql +0 -0
  156. package/supabase/templates/confirmation.html +76 -0
  157. package/supabase/templates/email-change.html +76 -0
  158. package/supabase/templates/invite.html +72 -0
  159. package/supabase/templates/magic-link.html +68 -0
  160. package/supabase/templates/recovery.html +82 -0
  161. package/tsconfig.api.json +16 -0
  162. package/tsconfig.json +25 -0
  163. 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
+ }