diff --git a/src/channels/discord.ts b/src/channels/discord.ts index 05b8fbf..24c692a 100644 --- a/src/channels/discord.ts +++ b/src/channels/discord.ts @@ -415,12 +415,7 @@ export class DiscordChannel implements Channel { .trim(); // Convert @username mentions to Discord mention format - const mentionMap: Record = { - 눈쟁이: '216851709744513024', - }; - for (const [name, id] of Object.entries(mentionMap)) { - cleaned = cleaned.replace(new RegExp(`@${name}`, 'g'), `<@${id}>`); - } + cleaned = cleaned.replaceAll('@눈쟁이', '<@216851709744513024>'); cleaned = formatOutbound(cleaned); // Discord has a 2000 character limit per message and 10 attachments per message diff --git a/src/codex-config-features.ts b/src/codex-config-features.ts index 0b246fd..04cbd74 100644 --- a/src/codex-config-features.ts +++ b/src/codex-config-features.ts @@ -16,6 +16,7 @@ export function readCodexFeatureFromContent( feature: CodexConfigFeature, ): boolean { const lines = content.split('\n'); + const featureLinePattern = new RegExp(`^${feature}\\s*=\\s*(true|false)$`); let inFeatures = false; for (const line of lines) { @@ -28,9 +29,7 @@ export function readCodexFeatureFromContent( break; } if (!inFeatures) continue; - const match = trimmed.match( - new RegExp(`^${feature}\\s*=\\s*(true|false)$`), - ); + const match = trimmed.match(featureLinePattern); if (match) return match[1] === 'true'; } diff --git a/src/settings-store.ts b/src/settings-store.ts index 463fe4c..1cc9ddd 100644 --- a/src/settings-store.ts +++ b/src/settings-store.ts @@ -227,13 +227,7 @@ export function listClaudeAccounts(): ClaudeAccountSummary[] { if (def) out.push(def); const dir = path.join(settingsHomeDir(), '.claude-accounts'); if (fs.existsSync(dir)) { - const entries = fs.readdirSync(dir); - const indices = entries - .filter((e) => /^\d+$/.test(e)) - .map((e) => Number.parseInt(e, 10)) - .filter((n) => Number.isFinite(n) && n >= 1) - .sort((a, b) => a - b); - for (const i of indices) { + for (const i of readAccountIndices(dir)) { const acc = readClaudeAccount(i); if (acc) out.push(acc); } @@ -247,13 +241,7 @@ export function listCodexAccounts(): CodexAccountSummary[] { if (def) out.push(def); const dir = path.join(settingsHomeDir(), '.codex-accounts'); if (fs.existsSync(dir)) { - const entries = fs.readdirSync(dir); - const indices = entries - .filter((e) => /^\d+$/.test(e)) - .map((e) => Number.parseInt(e, 10)) - .filter((n) => Number.isFinite(n) && n >= 1) - .sort((a, b) => a - b); - for (const i of indices) { + for (const i of readAccountIndices(dir)) { const acc = readCodexAccount(i); if (acc) out.push(acc); } @@ -261,6 +249,16 @@ export function listCodexAccounts(): CodexAccountSummary[] { return out; } +function readAccountIndices(dir: string): number[] { + const indices: number[] = []; + for (const entry of fs.readdirSync(dir)) { + if (!/^\d+$/.test(entry)) continue; + const index = Number.parseInt(entry, 10); + if (Number.isFinite(index) && index >= 1) indices.push(index); + } + return indices.sort((a, b) => a - b); +} + function pickEnvValue(content: string, key: string): string | undefined { const re = new RegExp(`^${key}=(.*)$`, 'm'); const match = content.match(re); @@ -526,13 +524,7 @@ export function getActiveCodexSettingsIndex(): number | null { } const dir = path.join(settingsHomeDir(), '.codex-accounts'); if (fs.existsSync(dir)) { - const indices = fs - .readdirSync(dir) - .filter((e) => /^\d+$/.test(e)) - .map((e) => Number.parseInt(e, 10)) - .filter((n) => Number.isFinite(n) && n >= 1) - .sort((a, b) => a - b); - for (const i of indices) { + for (const i of readAccountIndices(dir)) { if (codexAuthPath(i) === activePath) return i; } } diff --git a/src/task-suspension.ts b/src/task-suspension.ts index b3f4555..b289aa0 100644 --- a/src/task-suspension.ts +++ b/src/task-suspension.ts @@ -116,19 +116,23 @@ export function suspendTask(taskId: string, suspendedUntil: string): void { /** * Format the suspension notification for Discord. */ +const SUSPENSION_RESUME_FORMATTER = new Intl.DateTimeFormat('ko-KR', { + month: 'numeric', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + hour12: false, + timeZone: TIMEZONE, +}); + export function formatSuspensionNotice( task: ScheduledTask, suspendedUntil: string, reason: string, ): string { - const resumeLabel = new Intl.DateTimeFormat('ko-KR', { - month: 'numeric', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - hour12: false, - timeZone: TIMEZONE, - }).format(new Date(suspendedUntil)); + const resumeLabel = SUSPENSION_RESUME_FORMATTER.format( + new Date(suspendedUntil), + ); const lines = [ `⏸ 태스크 일시 중단`, diff --git a/src/task-watch-status.ts b/src/task-watch-status.ts index d3c55bb..6f7dd97 100644 --- a/src/task-watch-status.ts +++ b/src/task-watch-status.ts @@ -13,6 +13,18 @@ export type WatcherStatusPhase = export const WATCH_CI_PREFIX = WATCH_CI_PROMPT_PREFIX; export const TASK_STATUS_MESSAGE_PREFIX = '\u2063\u2063\u2063'; export const DEFAULT_WATCH_CI_MAX_DURATION_MS = 24 * 60 * 60 * 1000; +let watchTimeFormatter: Intl.DateTimeFormat | null = null; + +function getWatchTimeFormatter(): Intl.DateTimeFormat { + watchTimeFormatter ??= new Intl.DateTimeFormat('ko-KR', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + timeZone: TIMEZONE, + }); + return watchTimeFormatter; +} export function isWatchCiTask(task: Pick): boolean { return task.prompt.startsWith(WATCH_CI_PREFIX); @@ -36,13 +48,7 @@ export function extractWatchCiTarget(prompt: string): string | null { } function formatTimeLabel(timestampIso: string): string { - return new Intl.DateTimeFormat('ko-KR', { - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - hour12: false, - timeZone: TIMEZONE, - }) + return getWatchTimeFormatter() .format(new Date(timestampIso)) .replace(/:/g, '시 ') .replace(/시 (\d{2})$/, '분 $1초');