src / net / pinchtabManager.ts
src / net / pinchtabManager.ts
// src/net/pinchtabManager.ts
import * as fs from "node:fs";
import * as path from "node:path";
import * as os from "node:os";
import { spawn, execFileSync, ChildProcess } from "node:child_process";
const PINCHTAB_VERSION = "v0.15.2";
const PINCHTAB_PORT = 9867;
const PINCHTAB_BASE_URL = `http://127.0.0.1:${PINCHTAB_PORT}`;
export interface PinchtabHealth {
installed: boolean;
reachable: boolean;
attempts: number;
successes: number;
failures: number;
lastError: string | null;
}
class PinchtabSidecarManager {
private serverProc: ChildProcess | null = null;
private bearerToken: string | null = null;
private initPromise: Promise<boolean> | null = null;
private baseDir = path.join(os.homedir(), ".deep-swarm-research", "pinchtab");
private binDir = path.join(this.baseDir, "bin");
public health: PinchtabHealth = {
installed: false,
reachable: false,
attempts: 0,
successes: 0,
failures: 0,
lastError: null,
};
constructor() {
process.on("exit", () => this.stopServer());
process.on("SIGINT", () => this.stopServer());
process.on("SIGTERM", () => this.stopServer());
}
private getBinaryPath(): string {
const exeName = process.platform === "win32" ? "pinchtab.exe" : "pinchtab";
// 1. Check plugin's local node_modules/.bin
const localNodeBin = path.resolve(process.cwd(), "node_modules", ".bin", exeName);
if (fs.existsSync(localNodeBin)) return localNodeBin;
// 2. Check ~/.deep-swarm-research/pinchtab/bin/
return path.join(this.binDir, exeName);
}
/**
* Downloads the platform-matched Pinchtab Go binary on first use if not present.
*/
private async ensureBinary(log?: (msg: string) => void): Promise<string | null> {
const binPath = this.getBinaryPath();
if (fs.existsSync(binPath)) {
this.health.installed = true;
return binPath;
}
fs.mkdirSync(this.binDir, { recursive: true });
const platformMap: Record<string, string> = {
win32: "windows",
darwin: "darwin",
linux: "linux",
};
const archMap: Record<string, string> = {
x64: "amd64",
arm64: "arm64",
};
const plat = platformMap[process.platform];
const arch = archMap[process.arch];
if (!plat || !arch) {
this.health.lastError = `Unsupported platform: ${process.platform}-${process.arch}`;
return null;
}
const ext = process.platform === "win32" ? ".exe" : "";
const assetName = `pinchtab-${plat}-${arch}${ext}`;
const downloadUrl = `https://github.com/pinchtab/pinchtab/releases/download/${PINCHTAB_VERSION}/${assetName}`;
try {
log?.(`[PINCHTAB] First-run setup: downloading stealth browser bridge (${assetName})...`);
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 25_000);
const res = await fetch(downloadUrl, { signal: controller.signal });
clearTimeout(timer);
if (!res.ok) {
throw new Error(`HTTP ${res.status} fetching ${downloadUrl}`);
}
const buffer = Buffer.from(await res.arrayBuffer());
fs.writeFileSync(binPath, buffer);
if (process.platform !== "win32") {
fs.chmodSync(binPath, 0o755);
}
this.health.installed = true;
log?.(`[PINCHTAB] Binary installed at ${binPath}`);
return binPath;
} catch (err: any) {
this.health.lastError = `Binary bootstrap failed: ${err?.message ?? err}`;
log?.(`[WARN] [PINCHTAB] ${this.health.lastError}`);
return null;
}
}
/**
* Starts the local Pinchtab server daemon on 127.0.0.1:9867 and retrieves its Bearer token.
*/
public async ensureReady(log?: (msg: string) => void): Promise<boolean> {
if (this.health.reachable && this.bearerToken) return true;
if (this.initPromise) return this.initPromise;
this.initPromise = this._startInternal(log).finally(() => {
this.initPromise = null;
});
return this.initPromise;
}
private async _startInternal(log?: (msg: string) => void): Promise<boolean> {
const binPath = await this.ensureBinary(log);
if (!binPath) return false;
try {
// 1. Initialize config & retrieve local bearer token
try {
execFileSync(binPath, ["config", "init"], { stdio: "ignore", timeout: 4000 });
} catch {
// Already initialized
}
const tokenOut = execFileSync(binPath, ["config", "token", "--stdout"], {
encoding: "utf-8",
timeout: 4000,
}).trim();
if (tokenOut) {
this.bearerToken = tokenOut;
}
// 2. Check if already running on 127.0.0.1:9867
if (await this.pingHealth()) {
this.health.reachable = true;
return true;
}
// 3. Spawn background server bound to 127.0.0.1 with isolated profile
this.serverProc = spawn(binPath, ["server"], {
stdio: "ignore",
detached: false,
windowsHide: true,
env: {
...process.env,
PINCHTAB_BIND: "127.0.0.1",
PINCHTAB_PORT: String(PINCHTAB_PORT),
PINCHTAB_HEADLESS: "true",
},
});
this.serverProc.unref();
// Wait up to 4 seconds for server readiness
for (let i = 0; i < 8; i++) {
await new Promise((r) => setTimeout(r, 500));
if (await this.pingHealth()) {
this.health.reachable = true;
log?.(`[PINCHTAB] Local stealth browser active on ${PINCHTAB_BASE_URL}`);
return true;
}
}
this.health.lastError = "Server spawned but did not pass /health check in 4s";
return false;
} catch (err: any) {
this.health.lastError = err?.message ?? String(err);
return false;
}
}
private async pingHealth(): Promise<boolean> {
try {
const headers: Record<string, string> = {};
if (this.bearerToken) {
headers["Authorization"] = `Bearer ${this.bearerToken}`;
}
const res = await fetch(`${PINCHTAB_BASE_URL}/health`, {
headers,
signal: AbortSignal.timeout(1500),
});
return res.ok;
} catch {
return false;
}
}
/**
* Fetches and extracts clean ~800-token Readability text via Pinchtab's stealth Chrome instance.
*/
public async fetchReadablePage(
url: string,
timeoutMs = 8000,
log?: (msg: string) => void
): Promise<string | null> {
const ready = await this.ensureReady(log);
if (!ready) return null;
this.health.attempts++;
const headers: Record<string, string> = {
"Content-Type": "application/json",
...(this.bearerToken ? { Authorization: `Bearer ${this.bearerToken}` } : {}),
};
try {
// Step 1: Navigate in headless stealth tab
const navRes = await fetch(`${PINCHTAB_BASE_URL}/navigate`, {
method: "POST",
headers,
body: JSON.stringify({ url, headless: true }),
signal: AbortSignal.timeout(timeoutMs),
});
if (!navRes.ok) {
this.health.failures++;
this.health.lastError = `Navigate HTTP ${navRes.status}`;
return null;
}
// Step 2: Pull Readability text (~800 tokens/page)
const textRes = await fetch(`${PINCHTAB_BASE_URL}/text`, {
method: "GET",
headers,
signal: AbortSignal.timeout(4000),
});
if (!textRes.ok) {
this.health.failures++;
this.health.lastError = `Text extract HTTP ${textRes.status}`;
return null;
}
const content = (await textRes.text()).trim();
if (content.length < 120) {
this.health.failures++;
return null;
}
this.health.successes++;
return content;
} catch (err: any) {
this.health.failures++;
this.health.lastError = err?.message ?? "Pinchtab fetch timeout";
return null;
}
}
public stopServer(): void {
if (this.serverProc && !this.serverProc.killed) {
try {
this.serverProc.kill();
} catch {
// Ignore kill error on exit
}
this.serverProc = null;
}
}
}
export const pinchtabManager = new PinchtabSidecarManager();// src/net/pinchtabManager.ts
import * as fs from "node:fs";
import * as path from "node:path";
import * as os from "node:os";
import { spawn, execFileSync, ChildProcess } from "node:child_process";
const PINCHTAB_VERSION = "v0.15.2";
const PINCHTAB_PORT = 9867;
const PINCHTAB_BASE_URL = `http://127.0.0.1:${PINCHTAB_PORT}`;
export interface PinchtabHealth {
installed: boolean;
reachable: boolean;
attempts: number;
successes: number;
failures: number;
lastError: string | null;
}
class PinchtabSidecarManager {
private serverProc: ChildProcess | null = null;
private bearerToken: string | null = null;
private initPromise: Promise<boolean> | null = null;
private baseDir = path.join(os.homedir(), ".deep-swarm-research", "pinchtab");
private binDir = path.join(this.baseDir, "bin");
public health: PinchtabHealth = {
installed: false,
reachable: false,
attempts: 0,
successes: 0,
failures: 0,
lastError: null,
};
constructor() {
process.on("exit", () => this.stopServer());
process.on("SIGINT", () => this.stopServer());
process.on("SIGTERM", () => this.stopServer());
}
private getBinaryPath(): string {
const exeName = process.platform === "win32" ? "pinchtab.exe" : "pinchtab";
// 1. Check plugin's local node_modules/.bin
const localNodeBin = path.resolve(process.cwd(), "node_modules", ".bin", exeName);
if (fs.existsSync(localNodeBin)) return localNodeBin;
// 2. Check ~/.deep-swarm-research/pinchtab/bin/
return path.join(this.binDir, exeName);
}
/**
* Downloads the platform-matched Pinchtab Go binary on first use if not present.
*/
private async ensureBinary(log?: (msg: string) => void): Promise<string | null> {
const binPath = this.getBinaryPath();
if (fs.existsSync(binPath)) {
this.health.installed = true;
return binPath;
}
fs.mkdirSync(this.binDir, { recursive: true });
const platformMap: Record<string, string> = {
win32: "windows",
darwin: "darwin",
linux: "linux",
};
const archMap: Record<string, string> = {
x64: "amd64",
arm64: "arm64",
};
const plat = platformMap[process.platform];
const arch = archMap[process.arch];
if (!plat || !arch) {
this.health.lastError = `Unsupported platform: ${process.platform}-${process.arch}`;
return null;
}
const ext = process.platform === "win32" ? ".exe" : "";
const assetName = `pinchtab-${plat}-${arch}${ext}`;
const downloadUrl = `https://github.com/pinchtab/pinchtab/releases/download/${PINCHTAB_VERSION}/${assetName}`;
try {
log?.(`[PINCHTAB] First-run setup: downloading stealth browser bridge (${assetName})...`);
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 25_000);
const res = await fetch(downloadUrl, { signal: controller.signal });
clearTimeout(timer);
if (!res.ok) {
throw new Error(`HTTP ${res.status} fetching ${downloadUrl}`);
}
const buffer = Buffer.from(await res.arrayBuffer());
fs.writeFileSync(binPath, buffer);
if (process.platform !== "win32") {
fs.chmodSync(binPath, 0o755);
}
this.health.installed = true;
log?.(`[PINCHTAB] Binary installed at ${binPath}`);
return binPath;
} catch (err: any) {
this.health.lastError = `Binary bootstrap failed: ${err?.message ?? err}`;
log?.(`[WARN] [PINCHTAB] ${this.health.lastError}`);
return null;
}
}
/**
* Starts the local Pinchtab server daemon on 127.0.0.1:9867 and retrieves its Bearer token.
*/
public async ensureReady(log?: (msg: string) => void): Promise<boolean> {
if (this.health.reachable && this.bearerToken) return true;
if (this.initPromise) return this.initPromise;
this.initPromise = this._startInternal(log).finally(() => {
this.initPromise = null;
});
return this.initPromise;
}
private async _startInternal(log?: (msg: string) => void): Promise<boolean> {
const binPath = await this.ensureBinary(log);
if (!binPath) return false;
try {
// 1. Initialize config & retrieve local bearer token
try {
execFileSync(binPath, ["config", "init"], { stdio: "ignore", timeout: 4000 });
} catch {
// Already initialized
}
const tokenOut = execFileSync(binPath, ["config", "token", "--stdout"], {
encoding: "utf-8",
timeout: 4000,
}).trim();
if (tokenOut) {
this.bearerToken = tokenOut;
}
// 2. Check if already running on 127.0.0.1:9867
if (await this.pingHealth()) {
this.health.reachable = true;
return true;
}
// 3. Spawn background server bound to 127.0.0.1 with isolated profile
this.serverProc = spawn(binPath, ["server"], {
stdio: "ignore",
detached: false,
windowsHide: true,
env: {
...process.env,
PINCHTAB_BIND: "127.0.0.1",
PINCHTAB_PORT: String(PINCHTAB_PORT),
PINCHTAB_HEADLESS: "true",
},
});
this.serverProc.unref();
// Wait up to 4 seconds for server readiness
for (let i = 0; i < 8; i++) {
await new Promise((r) => setTimeout(r, 500));
if (await this.pingHealth()) {
this.health.reachable = true;
log?.(`[PINCHTAB] Local stealth browser active on ${PINCHTAB_BASE_URL}`);
return true;
}
}
this.health.lastError = "Server spawned but did not pass /health check in 4s";
return false;
} catch (err: any) {
this.health.lastError = err?.message ?? String(err);
return false;
}
}
private async pingHealth(): Promise<boolean> {
try {
const headers: Record<string, string> = {};
if (this.bearerToken) {
headers["Authorization"] = `Bearer ${this.bearerToken}`;
}
const res = await fetch(`${PINCHTAB_BASE_URL}/health`, {
headers,
signal: AbortSignal.timeout(1500),
});
return res.ok;
} catch {
return false;
}
}
/**
* Fetches and extracts clean ~800-token Readability text via Pinchtab's stealth Chrome instance.
*/
public async fetchReadablePage(
url: string,
timeoutMs = 8000,
log?: (msg: string) => void
): Promise<string | null> {
const ready = await this.ensureReady(log);
if (!ready) return null;
this.health.attempts++;
const headers: Record<string, string> = {
"Content-Type": "application/json",
...(this.bearerToken ? { Authorization: `Bearer ${this.bearerToken}` } : {}),
};
try {
// Step 1: Navigate in headless stealth tab
const navRes = await fetch(`${PINCHTAB_BASE_URL}/navigate`, {
method: "POST",
headers,
body: JSON.stringify({ url, headless: true }),
signal: AbortSignal.timeout(timeoutMs),
});
if (!navRes.ok) {
this.health.failures++;
this.health.lastError = `Navigate HTTP ${navRes.status}`;
return null;
}
// Step 2: Pull Readability text (~800 tokens/page)
const textRes = await fetch(`${PINCHTAB_BASE_URL}/text`, {
method: "GET",
headers,
signal: AbortSignal.timeout(4000),
});
if (!textRes.ok) {
this.health.failures++;
this.health.lastError = `Text extract HTTP ${textRes.status}`;
return null;
}
const content = (await textRes.text()).trim();
if (content.length < 120) {
this.health.failures++;
return null;
}
this.health.successes++;
return content;
} catch (err: any) {
this.health.failures++;
this.health.lastError = err?.message ?? "Pinchtab fetch timeout";
return null;
}
}
public stopServer(): void {
if (this.serverProc && !this.serverProc.killed) {
try {
this.serverProc.kill();
} catch {
// Ignore kill error on exit
}
this.serverProc = null;
}
}
}
export const pinchtabManager = new PinchtabSidecarManager();