summarizely-cli
Version:
YouTube summarizer that respects your existing subscriptions. No API keys required.
341 lines • 13.6 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.httpRequest = exports.ProviderError = void 0;
exports.selectProvider = selectProvider;
exports.summarizeWithProvider = summarizeWithProvider;
exports.extractMarkdownFromCodexOutput = extractMarkdownFromCodexOutput;
exports.__setHttpRequest = __setHttpRequest;
exports.formatProviderError = formatProviderError;
const child_process_1 = require("child_process");
const http_1 = __importDefault(require("http"));
const https_1 = __importDefault(require("https"));
const url_1 = require("url");
const fs_1 = __importDefault(require("fs"));
const logger_1 = require("./logger");
function selectProvider(env = process.env) {
// Prefer local CLI providers if present; then local Ollama
if (hasCli('claude'))
return { provider: 'claude-cli', reason: 'Claude CLI detected' };
if (hasCli('codex'))
return { provider: 'codex-cli', reason: 'Codex CLI detected' };
if (env.OLLAMA_HOST || hasOllamaCli())
return { provider: 'ollama', reason: 'Ollama detected' };
(0, logger_1.logFail)('provider selection', 'no CLI tools found');
return { provider: null, reason: 'No CLI tools or local models detected' };
}
function hasOllamaCli() {
try {
const r = (0, child_process_1.spawnSync)('ollama', ['--version'], { encoding: 'utf8' });
return r.status === 0;
}
catch {
return false;
}
}
function hasCli(bin) {
try {
const cmd = process.platform === 'win32' ? 'where' : 'which';
const r = (0, child_process_1.spawnSync)(cmd, [bin], { encoding: 'utf8' });
return r.status === 0;
}
catch {
return false;
}
}
class ProviderError extends Error {
constructor(code, message) {
super(message);
this.code = code;
}
}
exports.ProviderError = ProviderError;
async function summarizeWithProvider(provider, _cap, prompt, opts) {
if (provider === 'claude-cli') {
return runClaudePrintFlagCapture(prompt, 5 * 60000);
}
if (provider === 'codex-cli') {
const rawOutput = await runCliCapture('codex', ['exec'], prompt, 5 * 60000);
return extractMarkdownFromCodexOutput(rawOutput);
}
if (provider === 'ollama') {
try {
const host = process.env.OLLAMA_HOST || 'http://127.0.0.1:11434';
const model = opts?.model || (await selectOllamaModel(host));
if (!model)
throw new ProviderError('no_models', 'No Ollama models installed');
const text = await ollamaGenerate(host, model, prompt, {
temperature: 0.2,
top_p: 0.9,
top_k: 40,
timeoutMs: 3 * 60000,
});
return text || null;
}
catch (e) {
if (e instanceof ProviderError)
throw e;
throw new ProviderError('unavailable', e?.message || 'Ollama unavailable');
}
}
// Other providers not implemented in v1
return null;
}
function extractMarkdownFromCodexOutput(output) {
if (!output)
return null;
const lines = output.split('\n');
let actualSummaryStart = -1;
let foundCodexMarker = false;
// Look for the [timestamp] codex marker followed by actual markdown summary
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Check if this line contains a codex timestamp marker
if (line.includes('] codex')) {
foundCodexMarker = true;
continue;
}
// After finding codex marker, look for the actual summary start
if (foundCodexMarker && line.trim().startsWith('#') && line.trim().length > 1) {
actualSummaryStart = i;
break;
}
}
if (actualSummaryStart !== -1) {
// Found the actual summary after codex marker
return lines.slice(actualSummaryStart).join('\n').trim();
}
// Fallback: look for a line that starts with # followed by proper metadata
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line.startsWith('#') && !line.includes('Now write the summary') && line.length > 1) {
// Check if next few lines contain URL metadata
const nextLines = lines.slice(i, Math.min(i + 10, lines.length)).join('\n');
if (nextLines.includes('**URL:**') && nextLines.includes('**Generated:**')) {
return lines.slice(i).join('\n').trim();
}
}
}
// If no clear summary found, return original
return output;
}
function runCliCapture(cmd, args, input, timeoutMs) {
return new Promise((resolve) => {
try {
const realArgs = (cmd === 'claude' && args.length === 0) ? ['code'] : args;
(0, logger_1.logStart)(`${cmd} CLI spawn`);
const child = (0, child_process_1.spawn)(cmd, realArgs, { stdio: ['pipe', 'pipe', 'pipe'] });
let out = '';
let err = '';
let timedOut = false;
const t = setTimeout(() => {
timedOut = true;
(0, logger_1.logFail)(`${cmd} CLI spawn`, 'timeout');
child.kill('SIGTERM');
}, timeoutMs);
child.stdout.on('data', (d) => { out += d.toString(); });
child.stderr.on('data', (d) => { const s = d.toString(); err += s; process.stderr.write(`[provider ${cmd}] ${s}`); });
child.on('close', (code) => {
clearTimeout(t);
if (!timedOut) {
if (code === 0 && out.trim().length > 0) {
(0, logger_1.logOk)(`${cmd} CLI spawn`);
resolve(out.trim());
}
else {
(0, logger_1.logFail)(`${cmd} CLI spawn`, `exit code: ${code}`);
resolve(null);
}
}
else {
resolve(null);
}
});
child.on('error', (error) => {
if (!timedOut) {
(0, logger_1.logFail)(`${cmd} CLI spawn`, `error: ${error.message}`);
}
resolve(null);
});
child.stdin.write(input);
child.stdin.end();
}
catch {
resolve(null);
}
});
}
async function selectOllamaModel(host) {
// Pick smallest instruct model, or smallest model overall
const models = await listOllamaModels(host);
if (!models.length)
return null;
const instruct = models.filter((m) => /instruct/i.test(m.name));
if (instruct.length === 0)
return models[0].name;
instruct.sort((a, b) => (a.size ?? Number.MAX_SAFE_INTEGER) - (b.size ?? Number.MAX_SAFE_INTEGER));
return instruct[0].name;
}
async function listOllamaModels(host) {
try {
const data = await (0, exports.httpRequest)(host + '/api/tags', { method: 'GET' }, undefined, 5000);
const json = JSON.parse(data || '{}');
const arr = Array.isArray(json.models) ? json.models : [];
return arr.map((m) => ({ name: m?.name, size: m?.size })).filter((m) => typeof m.name === 'string');
}
catch (e) {
throw new ProviderError('unavailable', `Cannot reach Ollama at ${host}`);
}
}
async function ollamaGenerate(host, model, prompt, opts) {
const body = {
model,
prompt,
stream: false,
options: {
temperature: opts.temperature,
top_p: opts.top_p,
top_k: opts.top_k,
},
};
const url = host + '/api/generate';
(0, logger_1.logStart)('Ollama HTTP request');
const doReq = () => (0, exports.httpRequest)(url, { method: 'POST', headers: { 'content-type': 'application/json' } }, JSON.stringify(body), opts.timeoutMs ?? 180000);
let res;
try {
res = await doReq();
}
catch (e) {
// Transient retry: timeout or 5xx once
const isTransient = (e && (e.message === 'timeout' || (typeof e.status === 'number' && e.status >= 500)));
if (!isTransient) {
(0, logger_1.logFail)('Ollama HTTP request', e.message);
throw e;
}
(0, logger_1.logFail)('Ollama HTTP request', `${e.message}, retrying`);
res = await doReq();
}
(0, logger_1.logOk)('Ollama HTTP request');
const json = JSON.parse(res || '{}');
return json?.response ?? '';
}
class HttpError extends Error {
constructor(status, message) { super(message); this.status = status; }
}
function _httpRequestImpl(urlStr, options, body, timeoutMs = 10000) {
return new Promise((resolve, reject) => {
try {
const u = new url_1.URL(urlStr);
const mod = u.protocol === 'https:' ? https_1.default : http_1.default;
const req = mod.request({
protocol: u.protocol,
hostname: u.hostname,
port: u.port,
path: u.pathname + (u.search || ''),
method: options.method || 'GET',
headers: options.headers || {},
}, (res) => {
let data = '';
res.setEncoding('utf8');
res.on('data', (chunk) => (data += chunk));
res.on('end', () => {
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300)
resolve(data);
else
reject(new HttpError(res.statusCode, `HTTP ${res.statusCode}`));
});
});
req.on('error', (e) => reject(e));
req.setTimeout(timeoutMs, () => {
req.destroy(new HttpError(undefined, 'timeout'));
});
if (body)
req.write(body);
req.end();
}
catch (e) {
reject(e);
}
});
}
// Test hook: allow overriding http requester
exports.httpRequest = _httpRequestImpl;
function __setHttpRequest(fn) { exports.httpRequest = fn; }
// Minimal Claude print-mode helpers: pass entire prompt via -p to avoid stdin EPIPE
function runClaudePrintFlagCapture(prompt, timeoutMs) {
return new Promise((resolve) => {
try {
(0, logger_1.logStart)('claude -p spawn');
const child = (0, child_process_1.spawn)('claude', ['-p', prompt], { stdio: ['ignore', 'pipe', 'pipe'] });
let out = '';
let err = '';
let timedOut = false;
const t = setTimeout(() => {
timedOut = true;
(0, logger_1.logFail)('claude -p spawn', 'timeout');
child.kill('SIGTERM');
}, timeoutMs);
child.stdout.on('data', (d) => { out += d.toString(); });
child.stderr.on('data', (d) => { err += d.toString(); });
child.on('close', (code) => {
clearTimeout(t);
if (!timedOut && code === 0 && out.trim().length > 0) {
(0, logger_1.logOk)('claude -p spawn');
const output = out.trim();
// Check if Claude Code created a file instead of outputting the summary
const fileMatch = output.match(/^Summary created at `(.+?)`$/);
if (fileMatch && fileMatch[1]) {
try {
// Read the actual summary from the file Claude Code created
const content = fs_1.default.readFileSync(fileMatch[1], 'utf8');
// Clean up the temporary file
fs_1.default.unlinkSync(fileMatch[1]);
resolve(content);
}
catch {
// If we can't read the file, fall back to the original output
resolve(output);
}
}
else {
resolve(output);
}
}
else {
if (!timedOut) {
(0, logger_1.logFail)('claude -p spawn', `exit code: ${code}`);
}
resolve(null);
}
});
child.on('error', (error) => {
if (!timedOut) {
(0, logger_1.logFail)('claude -p spawn', `error: ${error.message}`);
}
resolve(null);
});
}
catch {
resolve(null);
}
});
}
// Friendly error message mapper for CLI
function formatProviderError(provider, e) {
if (provider === 'ollama') {
if (e.code === 'no_models')
return 'Ollama has no models installed. Try: ollama pull llama3.2:1b';
if (e.code === 'unavailable')
return `Cannot reach Ollama at ${process.env.OLLAMA_HOST || 'http://127.0.0.1:11434'}. Is it running?`;
if (e.code === 'timeout')
return 'Ollama request timed out.';
}
if (provider === 'claude-cli' || provider === 'codex-cli') {
if (e.code === 'not_found')
return `${provider === 'claude-cli' ? 'Claude' : 'Codex'} CLI not found on PATH.`;
}
return `Provider error (${provider}): ${e.message}`;
}
//# sourceMappingURL=providers.js.map