UNPKG

naisys

Version:

NAISYS - Autonomous AI agent runner with built-in context management and cost tracking

137 lines 4.98 kB
import fs from "fs"; import http from "http"; import https from "https"; import path from "path"; /** Match the `<targetPath>.tmp.<pid>.<ts>` convention used by downloadToFile. */ const TMP_SUFFIX_RE = /\.tmp\.\d+\.\d+$/; /** Files younger than this might belong to a concurrent in-flight download * from another process; the age cap avoids deleting under their feet. */ const STALE_TMP_AGE_MS = 60 * 60 * 1000; /** Shared client for `GET /attachments/:publicId` against the hub. Used by * the startup-attachment stager (downloads to disk) and the resume-replay * image/audio fetcher (downloads to memory). */ export function createHubAttachmentClient(hubClient, naisysApiService) { function getCredentials() { if (!hubClient) { throw "Hub attachments not available in local mode."; } const apiKey = naisysApiService.getKey(); if (!apiKey) { throw "No API key configured for this user."; } return { hubUrl: hubClient.getHubUrl(), apiKey }; } /** Stream to a temp file and rename on success so a crashed download * doesn't leave a partial file at targetPath. */ async function downloadToFile(publicId, targetPath) { const { hubUrl, apiKey } = getCredentials(); const tmpPath = `${targetPath}.tmp.${process.pid}.${Date.now()}`; await new Promise((resolve, reject) => { const req = openRequest(hubUrl, apiKey, publicId, (res) => { if (res.statusCode !== 200) { drainError(res, reject); return; } const fileStream = fs.createWriteStream(tmpPath); res.pipe(fileStream); fileStream.on("error", (err) => { tryUnlink(tmpPath); reject(err); }); fileStream.on("finish", () => { try { fs.renameSync(tmpPath, targetPath); resolve(); } catch (err) { tryUnlink(tmpPath); reject(err); } }); }); req.on("error", reject); req.end(); }); } /** Remove leftover `*.tmp.<pid>.<ts>` files under rootDir from prior runs * that crashed mid-download. Walks recursively; errors are swallowed so * one unreadable entry doesn't block staging. */ async function sweepStaleTmpFiles(rootDir) { const cutoff = Date.now() - STALE_TMP_AGE_MS; await walkAndUnlinkStaleTmp(rootDir, cutoff); } async function downloadToBuffer(publicId) { const { hubUrl, apiKey } = getCredentials(); return new Promise((resolve, reject) => { const req = openRequest(hubUrl, apiKey, publicId, (res) => { if (res.statusCode !== 200) { drainError(res, reject); return; } const chunks = []; res.on("data", (chunk) => chunks.push(chunk)); res.on("end", () => resolve(Buffer.concat(chunks))); res.on("error", reject); }); req.on("error", reject); req.end(); }); } return { downloadToFile, downloadToBuffer, sweepStaleTmpFiles }; } function openRequest(hubUrl, apiKey, publicId, onResponse) { const url = new URL(`${hubUrl}/attachments/${publicId}`); const httpModule = url.protocol === "https:" ? https : http; return httpModule.request(url, { method: "GET", headers: { Authorization: `Bearer ${apiKey}`, "ngrok-skip-browser-warning": "true", }, }, onResponse); } function drainError(res, reject) { let body = ""; res.on("data", (chunk) => { body += chunk.toString(); }); res.on("end", () => { reject(`Download failed (${res.statusCode}): ${body}`); }); } function tryUnlink(filepath) { try { fs.unlinkSync(filepath); } catch { /* ignore */ } } async function walkAndUnlinkStaleTmp(dir, cutoffMs) { let entries; try { entries = await fs.promises.readdir(dir, { withFileTypes: true }); } catch { return; // dir may not exist yet on first start } for (const e of entries) { const full = path.join(dir, e.name); if (e.isDirectory()) { await walkAndUnlinkStaleTmp(full, cutoffMs); continue; } if (!TMP_SUFFIX_RE.test(e.name)) continue; try { const stat = await fs.promises.stat(full); if (stat.mtimeMs < cutoffMs) { await fs.promises.unlink(full); } } catch { /* ignore — file might have been removed concurrently */ } } } //# sourceMappingURL=hubAttachmentClient.js.map