@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,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,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"));
|