✨feature: First push to git
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { homedir } from 'node:os';
|
||||
import { resolve } from 'node:path';
|
||||
import YAML from 'yaml';
|
||||
import { z } from 'zod';
|
||||
|
||||
const outputSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
sheetsApi: z
|
||||
.object({
|
||||
enabled: z.boolean().default(false),
|
||||
credentials: z.string().optional(),
|
||||
token: z.string().optional(),
|
||||
spreadsheetId: z.string().optional()
|
||||
})
|
||||
.optional(),
|
||||
excel: z
|
||||
.object({
|
||||
enabled: z.boolean().default(false),
|
||||
path: z.string().default('./output/newsletter-catalog.xlsx')
|
||||
})
|
||||
.optional()
|
||||
});
|
||||
|
||||
const configSchema = z
|
||||
.object({
|
||||
gmail: z.object({
|
||||
folder: z.string().min(1),
|
||||
credentials: z.string().default('~/.nlc/gmail-credentials.json'),
|
||||
token: z.string().default('~/.nlc/gmail-token.json')
|
||||
}),
|
||||
output: outputSchema,
|
||||
newsletters: z.record(z.string(), z.any()).default({}),
|
||||
links: z
|
||||
.object({
|
||||
unwrapRedirects: z.boolean().default(true),
|
||||
stripUtm: z.boolean().default(true),
|
||||
trackingParams: z
|
||||
.array(z.string())
|
||||
.default(['utm_*', 'fbclid', 'gclid', 'mc_cid', 'mc_eid']),
|
||||
redirectLimit: z.number().int().positive().default(5),
|
||||
readMorePattern: z.string().default('(?i)^(read more|continue reading|learn more)$'),
|
||||
sharePatterns: z.array(z.string()).default(['(?i)share', '(?i)forward to a friend']),
|
||||
sponsorMarkers: z
|
||||
.array(z.string())
|
||||
.default(['(?i)sponsor', '(?i)sponsored', '(?i)advertisement', '(?i)partner']),
|
||||
filterUnsubscribe: z.boolean().default(true),
|
||||
filterSocialFooter: z.boolean().default(true),
|
||||
filterShareLinks: z.boolean().default(true),
|
||||
mergeReadMore: z.boolean().default(true)
|
||||
})
|
||||
.default({}),
|
||||
categories: z
|
||||
.object({
|
||||
custom: z.array(z.string()).default([]),
|
||||
llm: z
|
||||
.object({
|
||||
provider: z
|
||||
.enum(['anthropic', 'openai', 'local', 'openai-compatible'])
|
||||
.default('anthropic'),
|
||||
model: z.string().default('claude-sonnet-4-6'),
|
||||
apiKeyEnv: z.string().default('ANTHROPIC_API_KEY'),
|
||||
baseUrl: z.string().nullable().optional(),
|
||||
failureCategory: z.string().default('Uncategorized')
|
||||
})
|
||||
.default({})
|
||||
})
|
||||
.default({}),
|
||||
enrichment: z
|
||||
.object({
|
||||
enabled: z.boolean().default(true),
|
||||
concurrency: z.number().int().positive().default(3),
|
||||
delayMs: z.number().int().nonnegative().default(1500),
|
||||
retries: z.number().int().nonnegative().default(2),
|
||||
timeoutMs: z.number().int().positive().default(10000)
|
||||
})
|
||||
.default({}),
|
||||
rateLimit: z
|
||||
.object({
|
||||
gmailQps: z.number().positive().default(5),
|
||||
linkConcurrency: z.number().int().positive().default(3)
|
||||
})
|
||||
.default({}),
|
||||
stateFile: z.string().default('~/.nlc/state.json'),
|
||||
plugins: z.record(z.string(), z.any()).default({})
|
||||
})
|
||||
.transform((config) => ({
|
||||
...config,
|
||||
output: {
|
||||
...config.output,
|
||||
sheetsApi: config.output.sheetsApi ?? { enabled: false },
|
||||
excel: config.output.excel ?? { enabled: false, path: './output/newsletter-catalog.xlsx' }
|
||||
}
|
||||
}));
|
||||
|
||||
export type AppConfig = z.infer<typeof configSchema>;
|
||||
export type PartialConfig = Record<string, unknown>;
|
||||
|
||||
function camelize(value: unknown): unknown {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(camelize);
|
||||
}
|
||||
if (value && typeof value === 'object') {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value as Record<string, unknown>).map(([key, entry]) => [
|
||||
key.replace(/_([a-z])/g, (_, letter: string) => letter.toUpperCase()),
|
||||
camelize(entry)
|
||||
])
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function expandHome(path: string): string {
|
||||
return path.startsWith('~/') ? resolve(homedir(), path.slice(2)) : path;
|
||||
}
|
||||
|
||||
export function loadConfigFromString(source: string): AppConfig {
|
||||
const parsed = camelize(YAML.parse(source) ?? {});
|
||||
return configSchema.parse(parsed);
|
||||
}
|
||||
|
||||
export async function loadConfig(path: string): Promise<AppConfig> {
|
||||
return loadConfigFromString(await readFile(expandHome(path), 'utf8'));
|
||||
}
|
||||
|
||||
export function normalizeConfig(config: PartialConfig): AppConfig {
|
||||
return configSchema.parse(camelize(config));
|
||||
}
|
||||
Reference in New Issue
Block a user