@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,58 @@
1
+ import { ActionHandler, ActionContext, ActionResult } from "./ActionHandler.js";
2
+ import { pickString, interpolate } from "./utils.js";
3
+ import { Actuator } from "../Actuator.js";
4
+
5
+ export class WebhookAction implements ActionHandler {
6
+ async execute(context: ActionContext): Promise<ActionResult> {
7
+ const { action, variables, data, userId, ingestionId, supabase } = context;
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
+ const webhookUrlTemplate = pickString(action as any, "url");
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ const webhookPayloadTemplate = pickString(action as any, "payload");
12
+
13
+ if (!webhookUrlTemplate || !webhookPayloadTemplate) {
14
+ return {
15
+ success: false,
16
+ logs: [],
17
+ trace: [{ timestamp: new Date().toISOString(), step: "Webhook failed: missing url or payload" }],
18
+ error: "Webhook action requires 'url' and 'payload' configs"
19
+ };
20
+ }
21
+
22
+ const url = interpolate(webhookUrlTemplate, variables, data);
23
+ const payloadStr = interpolate(webhookPayloadTemplate, variables, data);
24
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
+ let payload: any;
26
+
27
+ try {
28
+ payload = JSON.parse(payloadStr);
29
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
30
+ } catch (e) {
31
+ return {
32
+ success: false,
33
+ logs: [],
34
+ trace: [{ timestamp: new Date().toISOString(), step: "Webhook failed: invalid JSON payload" }],
35
+ error: "Webhook payload must be valid JSON"
36
+ };
37
+ }
38
+
39
+ await fetch(url, {
40
+ method: "POST",
41
+ headers: { "Content-Type": "application/json" },
42
+ body: JSON.stringify(payload),
43
+ });
44
+
45
+ const trace = [{
46
+ timestamp: new Date().toISOString(),
47
+ step: `Webhook payload sent to ${url}`,
48
+ details: { url, payload }
49
+ }];
50
+ Actuator.logEvent(ingestionId, userId, "action", "Action Execution", { action: "webhook", url }, supabase);
51
+
52
+ return {
53
+ success: true,
54
+ logs: [`Logged via webhook`],
55
+ trace
56
+ };
57
+ }
58
+ }
@@ -0,0 +1,293 @@
1
+ import type { ExtractField } from "../../services/PolicyLoader.js";
2
+ import { createLogger } from "../logger.js";
3
+
4
+ const logger = createLogger("ActionUtils");
5
+
6
+ export type ExtractedData = Record<string, unknown>;
7
+
8
+ export type ActionInput = {
9
+ type: string;
10
+ config?: Record<string, unknown>;
11
+ pattern?: string;
12
+ destination?: string;
13
+ path?: string;
14
+ columns?: string[] | string;
15
+ message?: string;
16
+ url?: string;
17
+ payload?: string;
18
+ };
19
+
20
+ // โ”€โ”€โ”€ Variable Interpolation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
21
+
22
+ function isRecord(value: unknown): value is Record<string, unknown> {
23
+ return value !== null && typeof value === "object" && !Array.isArray(value);
24
+ }
25
+
26
+ function maybeParseJson(value: unknown): unknown {
27
+ if (typeof value !== "string") return undefined;
28
+ const trimmed = value.trim();
29
+ if (!trimmed) return undefined;
30
+ if (!((trimmed.startsWith("{") && trimmed.endsWith("}")) || (trimmed.startsWith("[") && trimmed.endsWith("]")))) {
31
+ return undefined;
32
+ }
33
+ try {
34
+ return JSON.parse(trimmed);
35
+ } catch {
36
+ return undefined;
37
+ }
38
+ }
39
+
40
+ function tokenizePath(path: string): Array<string | number> {
41
+ const tokens: Array<string | number> = [];
42
+ let i = 0;
43
+
44
+ while (i < path.length) {
45
+ const ch = path[i];
46
+ if (ch === ".") {
47
+ i += 1;
48
+ continue;
49
+ }
50
+
51
+ if (ch === "[") {
52
+ const end = path.indexOf("]", i + 1);
53
+ if (end < 0) break;
54
+ const raw = path.slice(i + 1, end).trim();
55
+ if (/^\d+$/.test(raw)) {
56
+ tokens.push(Number(raw));
57
+ } else {
58
+ const unquoted = raw.replace(/^["']|["']$/g, "");
59
+ if (unquoted) tokens.push(unquoted);
60
+ }
61
+ i = end + 1;
62
+ continue;
63
+ }
64
+
65
+ let j = i;
66
+ while (j < path.length && path[j] !== "." && path[j] !== "[") {
67
+ j += 1;
68
+ }
69
+ const token = path.slice(i, j).trim();
70
+ if (token) tokens.push(token);
71
+ i = j;
72
+ }
73
+
74
+ return tokens;
75
+ }
76
+
77
+ function toTemplateString(value: unknown): string | undefined {
78
+ if (value == null) return undefined;
79
+ if (typeof value === "string") return value;
80
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
81
+ try {
82
+ return JSON.stringify(value);
83
+ } catch {
84
+ return String(value);
85
+ }
86
+ }
87
+
88
+ export function getNestedVariable(
89
+ keyPath: string,
90
+ vars: Record<string, string>,
91
+ data?: ExtractedData
92
+ ): string | undefined {
93
+ const key = keyPath.trim();
94
+ if (!key) return undefined;
95
+
96
+ if (vars[key] !== undefined) {
97
+ return vars[key];
98
+ }
99
+
100
+ const root: Record<string, unknown> = {
101
+ ...(data ?? {}),
102
+ ...vars,
103
+ };
104
+
105
+ const tokens = tokenizePath(key);
106
+ if (tokens.length === 0) return undefined;
107
+
108
+ let current: unknown = root;
109
+
110
+ for (let i = 0; i < tokens.length; i += 1) {
111
+ if (typeof current === "string") {
112
+ const parsed = maybeParseJson(current);
113
+ if (parsed !== undefined) {
114
+ current = parsed;
115
+ }
116
+ }
117
+
118
+ const token = tokens[i];
119
+ if (Array.isArray(current)) {
120
+ if (typeof token !== "number") return undefined;
121
+ current = current[token];
122
+ continue;
123
+ }
124
+
125
+ if (!isRecord(current)) {
126
+ return undefined;
127
+ }
128
+
129
+ if (typeof token === "number") {
130
+ current = current[String(token)];
131
+ continue;
132
+ }
133
+
134
+ const lookupToken =
135
+ i === 0 && token === "enrichment" && current["_enrichment"] !== undefined
136
+ ? "_enrichment"
137
+ : token;
138
+ current = current[lookupToken];
139
+ }
140
+
141
+ return toTemplateString(current);
142
+ }
143
+
144
+ export function interpolate(
145
+ template: string,
146
+ vars: Record<string, string>,
147
+ data?: ExtractedData
148
+ ): string {
149
+ return template.replace(/\{([^{}]+)\}/g, (match, rawKey: string) => {
150
+ const resolved = getNestedVariable(rawKey, vars, data);
151
+ return resolved ?? match;
152
+ });
153
+ }
154
+
155
+ /**
156
+ * Derive computed variables from extracted data using transformer definitions.
157
+ */
158
+ export function deriveVariables(
159
+ data: ExtractedData,
160
+ fields: ExtractField[]
161
+ ): Record<string, string> {
162
+ const vars: Record<string, string> = {};
163
+
164
+ // Populate raw extracted values as strings
165
+ for (const [k, v] of Object.entries(data)) {
166
+ const serialized = toTemplateString(v);
167
+ if (serialized !== undefined) vars[k] = serialized;
168
+ }
169
+
170
+ // Run transformers
171
+ for (const field of fields) {
172
+ if (!field.transformers) continue;
173
+ const rawValue = vars[field.key];
174
+ if (!rawValue) continue;
175
+
176
+ for (const t of field.transformers) {
177
+ try {
178
+ if (t.name === "get_year") {
179
+ vars[t.as] = new Date(rawValue).getFullYear().toString();
180
+ } else if (t.name === "get_month_name") {
181
+ vars[t.as] = new Date(rawValue).toLocaleString("en-US", { month: "long" });
182
+ } else if (t.name === "get_month") {
183
+ vars[t.as] = String(new Date(rawValue).getMonth() + 1).padStart(2, "0");
184
+ }
185
+ } catch {
186
+ logger.warn(`Transformer '${t.name}' failed for key '${field.key}'`);
187
+ }
188
+ }
189
+ }
190
+
191
+ return vars;
192
+ }
193
+
194
+ export function pickString(action: ActionInput, key: string): string | undefined {
195
+ const value = action.config?.[key];
196
+ if (typeof value === "string" && value.trim().length > 0) {
197
+ return value;
198
+ }
199
+
200
+ const legacyValue = (action as Record<string, unknown>)[key];
201
+ if (typeof legacyValue === "string" && legacyValue.trim().length > 0) {
202
+ return legacyValue;
203
+ }
204
+
205
+ return undefined;
206
+ }
207
+
208
+ // โ”€โ”€โ”€ Filename Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
209
+
210
+ /**
211
+ * Build a filename stem from extracted metadata when suggested_filename is unavailable.
212
+ * Format: YYYY-MM-DD_Issuer_DocType (any missing parts are simply omitted)
213
+ */
214
+ export function deriveNameFromVariables(variables: Record<string, string>): string | null {
215
+ const parts: string[] = [];
216
+
217
+ if (variables.date) {
218
+ const d = new Date(variables.date);
219
+ if (!isNaN(d.getTime())) {
220
+ const yyyy = d.getFullYear();
221
+ const MM = String(d.getMonth() + 1).padStart(2, "0");
222
+ const dd = String(d.getDate()).padStart(2, "0");
223
+ parts.push(`${yyyy}-${MM}-${dd}`);
224
+ }
225
+ }
226
+
227
+ if (variables.issuer) {
228
+ parts.push(variables.issuer.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, ""));
229
+ }
230
+
231
+ if (variables.document_type) {
232
+ parts.push(variables.document_type.replace(/\s+/g, "-").replace(/[^a-zA-Z0-9-]+/g, ""));
233
+ }
234
+
235
+ if (variables.amount || variables.total_amount) {
236
+ const raw = (variables.amount ?? variables.total_amount).replace(/[^0-9.$โ‚ฌยฃ]/g, "");
237
+ if (raw) parts.push(raw);
238
+ }
239
+
240
+ return parts.length > 0 ? parts.join("_") : null;
241
+ }
242
+
243
+ /**
244
+ * Resolve a final filename given the action's `filename` config field.
245
+ *
246
+ * Modes:
247
+ * undefined / "" / "original" โ†’ keep original stem + ext
248
+ * "auto" โ†’ AI suggested_filename โ†’ derived โ†’ originalStem, then + ext
249
+ * any other string โ†’ treat as a {variable} interpolation pattern
250
+ */
251
+ export function resolveFilename(
252
+ filenameConfig: string | undefined,
253
+ variables: Record<string, string>,
254
+ originalStem: string,
255
+ ext: string,
256
+ data?: ExtractedData
257
+ ): string {
258
+ if (!filenameConfig || filenameConfig === "original") {
259
+ return originalStem + ext;
260
+ }
261
+
262
+ if (filenameConfig === "auto") {
263
+ const smart =
264
+ deriveNameFromVariables(variables) ||
265
+ variables.suggested_filename?.trim() ||
266
+ originalStem;
267
+ return smart.endsWith(ext) ? smart : smart + ext;
268
+ }
269
+
270
+ // Custom interpolation pattern
271
+ const interpolated = interpolate(filenameConfig, variables, data);
272
+ return interpolated.endsWith(ext) ? interpolated : interpolated + ext;
273
+ }
274
+
275
+ export function pickColumns(action: ActionInput, fallback: string[]): string[] {
276
+ const value = action.config?.columns;
277
+ if (Array.isArray(value)) {
278
+ return value.map((item) => String(item).trim()).filter(Boolean);
279
+ }
280
+ if (typeof value === "string") {
281
+ return value.split(",").map((item) => item.trim()).filter(Boolean);
282
+ }
283
+
284
+ const legacyColumns = action.columns;
285
+ if (Array.isArray(legacyColumns)) {
286
+ return legacyColumns.map((item) => String(item).trim()).filter(Boolean);
287
+ }
288
+ if (typeof legacyColumns === "string") {
289
+ return legacyColumns.split(",").map((item) => item.trim()).filter(Boolean);
290
+ }
291
+
292
+ return fallback;
293
+ }
@@ -0,0 +1,61 @@
1
+ export function normalizeLlmContent(content: unknown): string {
2
+ if (typeof content === "string") return content;
3
+ if (typeof content === "number" || typeof content === "boolean") return String(content);
4
+
5
+ if (Array.isArray(content)) {
6
+ return content
7
+ .map((part) => {
8
+ if (part && typeof part === "object") {
9
+ const obj = part as Record<string, unknown>;
10
+ if (typeof obj.text === "string") return obj.text;
11
+ if (typeof obj.content === "string") return obj.content;
12
+ }
13
+ return normalizeLlmContent(part);
14
+ })
15
+ .filter(Boolean)
16
+ .join("\n");
17
+ }
18
+
19
+ if (content && typeof content === "object") {
20
+ const obj = content as Record<string, unknown>;
21
+ if (typeof obj.text === "string") return obj.text;
22
+ if (typeof obj.content === "string") return obj.content;
23
+ if ("content" in obj) return normalizeLlmContent(obj.content);
24
+ try {
25
+ return JSON.stringify(obj);
26
+ } catch {
27
+ return String(obj);
28
+ }
29
+ }
30
+
31
+ return "";
32
+ }
33
+
34
+ /**
35
+ * Robustly extracts the text payload from various SDK adapter response shapes.
36
+ */
37
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
+ export function extractLlmResponse(result: any): string {
39
+ if (!result) return "";
40
+
41
+ const candidates = [
42
+ result.response?.content,
43
+ result.message?.content,
44
+ result.content,
45
+ result.text,
46
+ result.choices?.[0]?.message?.content,
47
+ result.result,
48
+ result.output,
49
+ ];
50
+
51
+ for (const candidate of candidates) {
52
+ const normalized = normalizeLlmContent(candidate);
53
+ if (normalized) return normalized;
54
+ }
55
+
56
+ return "";
57
+ }
58
+
59
+ export function previewLlmText(raw: string, maxChars = 240): string {
60
+ return raw.replace(/\s+/g, " ").trim().slice(0, maxChars);
61
+ }
@@ -0,0 +1,67 @@
1
+ import type { SupabaseClient } from "@supabase/supabase-js";
2
+
3
+ interface LogData {
4
+ [key: string]: unknown;
5
+ }
6
+
7
+ class LoggerCore {
8
+ private static persistence:
9
+ | {
10
+ supabase: SupabaseClient;
11
+ userId: string;
12
+ }
13
+ | null = null;
14
+
15
+ static setPersistence(supabase: SupabaseClient, userId: string) {
16
+ this.persistence = { supabase, userId };
17
+ }
18
+
19
+ static clearPersistence() {
20
+ this.persistence = null;
21
+ }
22
+
23
+ static async persist(level: string, scope: string, message: string, data?: LogData) {
24
+ if (!this.persistence) {
25
+ return;
26
+ }
27
+
28
+ try {
29
+ await this.persistence.supabase.from("system_logs").insert({
30
+ user_id: this.persistence.userId,
31
+ level,
32
+ scope,
33
+ message,
34
+ metadata: data || {}
35
+ });
36
+ } catch {
37
+ // persistence is best-effort and should never crash request flow
38
+ }
39
+ }
40
+ }
41
+
42
+ export const Logger = LoggerCore;
43
+
44
+ export function createLogger(scope: string) {
45
+ function write(level: "debug" | "info" | "warn" | "error", message: string, data?: LogData) {
46
+ const line = `[${scope}] ${message}`;
47
+
48
+ if (level === "error") {
49
+ console.error(line, data || "");
50
+ } else if (level === "warn") {
51
+ console.warn(line, data || "");
52
+ } else if (level === "info") {
53
+ console.info(line, data || "");
54
+ } else {
55
+ console.debug(line, data || "");
56
+ }
57
+
58
+ void Logger.persist(level, scope, message, data);
59
+ }
60
+
61
+ return {
62
+ debug: (message: string, data?: LogData) => write("debug", message, data),
63
+ info: (message: string, data?: LogData) => write("info", message, data),
64
+ warn: (message: string, data?: LogData) => write("warn", message, data),
65
+ error: (message: string, data?: LogData) => write("error", message, data)
66
+ };
67
+ }
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from "node:child_process";
4
+
5
+ const child = spawn("bash", ["./scripts/migrate.sh"], {
6
+ stdio: "inherit",
7
+ env: process.env
8
+ });
9
+
10
+ child.on("close", (code) => {
11
+ process.exit(code || 0);
12
+ });
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, writeFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import readline from "node:readline";
6
+
7
+ const rl = readline.createInterface({
8
+ input: process.stdin,
9
+ output: process.stdout
10
+ });
11
+
12
+ function question(prompt) {
13
+ return new Promise((resolve) => rl.question(prompt, resolve));
14
+ }
15
+
16
+ async function setup() {
17
+ const envPath = join(process.cwd(), ".env");
18
+
19
+ if (existsSync(envPath)) {
20
+ const overwrite = await question("โš ๏ธ .env exists. Overwrite? (y/N): ");
21
+ if (overwrite.toLowerCase() !== "y") {
22
+ console.log("Setup cancelled.");
23
+ rl.close();
24
+ return;
25
+ }
26
+ }
27
+
28
+ const supabaseUrl = await question("Supabase URL: ");
29
+ const supabaseAnonKey = await question("Supabase anon key: ");
30
+ const port = (await question("API port [3006]: ")) || "3006";
31
+
32
+ const env = `VITE_SUPABASE_URL=${supabaseUrl}\nVITE_SUPABASE_ANON_KEY=${supabaseAnonKey}\nVITE_API_URL=http://localhost:${port}\nPORT=${port}\nDISABLE_AUTH=true\nJWT_SECRET=dev-secret-change-in-production\n`;
33
+
34
+ writeFileSync(envPath, env);
35
+
36
+ console.log("โœ… .env written");
37
+ console.log("Next:\n 1) npm install\n 2) npm run migrate\n 3) npm run dev:api\n 4) npm run dev");
38
+ rl.close();
39
+ }
40
+
41
+ setup().catch((error) => {
42
+ console.error("โŒ Setup failed:", error.message);
43
+ rl.close();
44
+ process.exit(1);
45
+ });
package/bin/folio.js ADDED
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from "node:child_process";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { existsSync } from "node:fs";
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+
11
+ const args = process.argv.slice(2);
12
+
13
+ let port = "5176";
14
+ const portIndex = args.indexOf("--port");
15
+ if (portIndex !== -1 && args[portIndex + 1]) {
16
+ port = args[portIndex + 1];
17
+ }
18
+
19
+ const noUi = args.includes("--no-ui");
20
+
21
+ console.log("๐Ÿš€ Folio starting...");
22
+ console.log(`๐Ÿ“ก Port: ${port}`);
23
+ if (noUi) console.log("๐Ÿ–ฅ๏ธ Mode: No-UI");
24
+
25
+ const distServerPath = join(__dirname, "..", "dist", "api", "server.js");
26
+ const sourceServerPath = join(__dirname, "..", "api", "server.ts");
27
+ const distPath = join(__dirname, "..", "dist");
28
+
29
+ let execPath = process.execPath;
30
+ let execArgs = [];
31
+
32
+ if (existsSync(distServerPath)) {
33
+ execArgs = [distServerPath, ...args];
34
+ } else if (existsSync(sourceServerPath)) {
35
+ execPath = "npx";
36
+ execArgs = ["tsx", sourceServerPath, ...args];
37
+ console.log("๐Ÿงช Compiled server not found. Falling back to source (tsx).");
38
+ if (!existsSync(join(distPath, "index.html")) && !noUi) {
39
+ console.log("โš ๏ธ dist/index.html not found. UI will not be served in single-port mode until you run `npm run build`.");
40
+ }
41
+ } else {
42
+ console.error("โŒ Could not find Folio server entry point.");
43
+ process.exit(1);
44
+ }
45
+
46
+ const child = spawn(execPath, execArgs, {
47
+ stdio: "inherit",
48
+ env: {
49
+ ...process.env,
50
+ PORT: port,
51
+ ELECTRON_STATIC_PATH: distPath
52
+ }
53
+ });
54
+
55
+ child.on("error", (error) => {
56
+ console.error("โŒ Failed to start Folio:", error.message);
57
+ process.exit(1);
58
+ });
59
+
60
+ child.on("close", (code) => {
61
+ process.exit(code || 0);
62
+ });
63
+
64
+ process.on("SIGINT", () => child.kill("SIGINT"));
65
+ process.on("SIGTERM", () => child.kill("SIGTERM"));