refactor: deduplicate parse helpers into src/utils (#398)

This commit is contained in:
Cameron
2026-02-25 14:27:06 -08:00
committed by GitHub
parent 0d5afd6326
commit 07d6939ead
4 changed files with 78 additions and 32 deletions

View File

@@ -24,6 +24,7 @@ import {
} from './config/index.js';
import { isLettaApiUrl } from './utils/server.js';
import { getDataDir, getWorkingDir, hasRailwayVolume } from './utils/paths.js';
import { parseCsvList, parseNonNegativeNumber } from './utils/parse.js';
import { createLogger, setLogLevel } from './logger.js';
const log = createLogger('Config');
@@ -484,20 +485,6 @@ function createGroupBatcher(
// Skills are installed to agent-scoped directory when agent is created (see core/bot.ts)
function parseCsvList(raw: string): string[] {
return raw
.split(',')
.map((item) => item.trim())
.filter((item) => item.length > 0);
}
function parseNonNegativeNumber(raw: string | undefined): number | undefined {
if (!raw) return undefined;
const parsed = Number(raw);
if (!Number.isFinite(parsed) || parsed < 0) return undefined;
return parsed;
}
function ensureRequiredTools(tools: string[]): string[] {
const out = [...tools];
if (!out.includes('manage_todo')) {

View File

@@ -9,24 +9,19 @@ import * as p from '@clack/prompts';
import { saveConfig, syncProviders, isApiServerMode } from './config/index.js';
import type { AgentConfig, LettaBotConfig, ProviderConfig } from './config/types.js';
import { isLettaApiUrl } from './utils/server.js';
import { parseCsvList, parseOptionalInt } from './utils/parse.js';
import { CHANNELS, getChannelHint, isSignalCliInstalled, setupTelegram, setupSlack, setupDiscord, setupWhatsApp, setupSignal } from './channels/setup.js';
// ============================================================================
// Non-Interactive Helpers
// ============================================================================
function parseCsvList(value?: string): string[] | undefined {
function parseOptionalCsvList(value?: string): string[] | undefined {
if (!value) return undefined;
const items = value.split(',').map(s => s.trim()).filter(Boolean);
const items = parseCsvList(value);
return items.length > 0 ? items : undefined;
}
function parseOptionalInt(value?: string): number | undefined {
if (!value) return undefined;
const parsed = parseInt(value, 10);
return Number.isFinite(parsed) ? parsed : undefined;
}
function readConfigFromEnv(existingConfig: any): any {
return {
baseUrl: process.env.LETTA_BASE_URL || existingConfig.server?.baseUrl || 'https://api.letta.com',
@@ -43,9 +38,9 @@ function readConfigFromEnv(existingConfig: any): any {
?? existingConfig.channels?.telegram?.groupDebounceSec,
groupPollIntervalMin: parseOptionalInt(process.env.TELEGRAM_GROUP_POLL_INTERVAL_MIN)
?? existingConfig.channels?.telegram?.groupPollIntervalMin,
instantGroups: parseCsvList(process.env.TELEGRAM_INSTANT_GROUPS)
instantGroups: parseOptionalCsvList(process.env.TELEGRAM_INSTANT_GROUPS)
?? existingConfig.channels?.telegram?.instantGroups,
listeningGroups: parseCsvList(process.env.TELEGRAM_LISTENING_GROUPS)
listeningGroups: parseOptionalCsvList(process.env.TELEGRAM_LISTENING_GROUPS)
?? existingConfig.channels?.telegram?.listeningGroups,
},
@@ -59,9 +54,9 @@ function readConfigFromEnv(existingConfig: any): any {
?? existingConfig.channels?.slack?.groupDebounceSec,
groupPollIntervalMin: parseOptionalInt(process.env.SLACK_GROUP_POLL_INTERVAL_MIN)
?? existingConfig.channels?.slack?.groupPollIntervalMin,
instantGroups: parseCsvList(process.env.SLACK_INSTANT_GROUPS)
instantGroups: parseOptionalCsvList(process.env.SLACK_INSTANT_GROUPS)
?? existingConfig.channels?.slack?.instantGroups,
listeningGroups: parseCsvList(process.env.SLACK_LISTENING_GROUPS)
listeningGroups: parseOptionalCsvList(process.env.SLACK_LISTENING_GROUPS)
?? existingConfig.channels?.slack?.listeningGroups,
},
@@ -74,9 +69,9 @@ function readConfigFromEnv(existingConfig: any): any {
?? existingConfig.channels?.discord?.groupDebounceSec,
groupPollIntervalMin: parseOptionalInt(process.env.DISCORD_GROUP_POLL_INTERVAL_MIN)
?? existingConfig.channels?.discord?.groupPollIntervalMin,
instantGroups: parseCsvList(process.env.DISCORD_INSTANT_GROUPS)
instantGroups: parseOptionalCsvList(process.env.DISCORD_INSTANT_GROUPS)
?? existingConfig.channels?.discord?.instantGroups,
listeningGroups: parseCsvList(process.env.DISCORD_LISTENING_GROUPS)
listeningGroups: parseOptionalCsvList(process.env.DISCORD_LISTENING_GROUPS)
?? existingConfig.channels?.discord?.listeningGroups,
},
@@ -89,9 +84,9 @@ function readConfigFromEnv(existingConfig: any): any {
?? existingConfig.channels?.whatsapp?.groupDebounceSec,
groupPollIntervalMin: parseOptionalInt(process.env.WHATSAPP_GROUP_POLL_INTERVAL_MIN)
?? existingConfig.channels?.whatsapp?.groupPollIntervalMin,
instantGroups: parseCsvList(process.env.WHATSAPP_INSTANT_GROUPS)
instantGroups: parseOptionalCsvList(process.env.WHATSAPP_INSTANT_GROUPS)
?? existingConfig.channels?.whatsapp?.instantGroups,
listeningGroups: parseCsvList(process.env.WHATSAPP_LISTENING_GROUPS)
listeningGroups: parseOptionalCsvList(process.env.WHATSAPP_LISTENING_GROUPS)
?? existingConfig.channels?.whatsapp?.listeningGroups,
},
@@ -105,9 +100,9 @@ function readConfigFromEnv(existingConfig: any): any {
?? existingConfig.channels?.signal?.groupDebounceSec,
groupPollIntervalMin: parseOptionalInt(process.env.SIGNAL_GROUP_POLL_INTERVAL_MIN)
?? existingConfig.channels?.signal?.groupPollIntervalMin,
instantGroups: parseCsvList(process.env.SIGNAL_INSTANT_GROUPS)
instantGroups: parseOptionalCsvList(process.env.SIGNAL_INSTANT_GROUPS)
?? existingConfig.channels?.signal?.instantGroups,
listeningGroups: parseCsvList(process.env.SIGNAL_LISTENING_GROUPS)
listeningGroups: parseOptionalCsvList(process.env.SIGNAL_LISTENING_GROUPS)
?? existingConfig.channels?.signal?.listeningGroups,
},
};

41
src/utils/parse.test.ts Normal file
View File

@@ -0,0 +1,41 @@
import { describe, expect, it } from 'vitest';
import { parseCsvList, parseNonNegativeNumber, parseOptionalInt } from './parse.js';
describe('parseCsvList', () => {
it('splits and trims comma-separated values', () => {
expect(parseCsvList('one, two,three')).toEqual(['one', 'two', 'three']);
});
it('drops empty entries', () => {
expect(parseCsvList('one,, ,two,')).toEqual(['one', 'two']);
});
});
describe('parseOptionalInt', () => {
it('returns undefined for missing values', () => {
expect(parseOptionalInt()).toBeUndefined();
expect(parseOptionalInt('')).toBeUndefined();
});
it('parses valid integer prefixes', () => {
expect(parseOptionalInt('42')).toBe(42);
expect(parseOptionalInt('42px')).toBe(42);
});
it('returns undefined for invalid values', () => {
expect(parseOptionalInt('nope')).toBeUndefined();
});
});
describe('parseNonNegativeNumber', () => {
it('returns undefined for missing, invalid, or negative values', () => {
expect(parseNonNegativeNumber()).toBeUndefined();
expect(parseNonNegativeNumber('nope')).toBeUndefined();
expect(parseNonNegativeNumber('-1')).toBeUndefined();
});
it('parses zero and positive numbers', () => {
expect(parseNonNegativeNumber('0')).toBe(0);
expect(parseNonNegativeNumber('1.5')).toBe(1.5);
});
});

23
src/utils/parse.ts Normal file
View File

@@ -0,0 +1,23 @@
/**
* Shared parsing helpers for environment/config input.
*/
export function parseCsvList(raw: string): string[] {
return raw
.split(',')
.map((item) => item.trim())
.filter((item) => item.length > 0);
}
export function parseOptionalInt(raw?: string): number | undefined {
if (!raw) return undefined;
const parsed = parseInt(raw, 10);
return Number.isFinite(parsed) ? parsed : undefined;
}
export function parseNonNegativeNumber(raw?: string): number | undefined {
if (!raw) return undefined;
const parsed = Number(raw);
if (!Number.isFinite(parsed) || parsed < 0) return undefined;
return parsed;
}