pi-unsloth-webtools 0.7.6 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/config.ts DELETED
@@ -1,207 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import { mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
3
- import { homedir } from "node:os";
4
- import { dirname, join } from "node:path";
5
-
6
- const CONFIG_DIR_NAME = "pi-unsloth-webtools";
7
- const TEMP_PREFIX = ".tmp-";
8
- const CONFIG_LOCK_DELAY_MS = 25;
9
- const CONFIG_LOCK_STALE_MS = 5000;
10
- const CONFIG_LOCK_RETRIES = Math.ceil(CONFIG_LOCK_STALE_MS / CONFIG_LOCK_DELAY_MS) * 2;
11
-
12
- export interface Config {
13
- webRenderEnabled: boolean;
14
- }
15
-
16
- const DEFAULT_CONFIG: Config = {
17
- webRenderEnabled: true,
18
- };
19
-
20
- function homeBase(): string {
21
- const envHome = process.env.HOME;
22
- return envHome && envHome.length > 0 ? envHome : homedir();
23
- }
24
-
25
- function configBase(): string {
26
- if (process.platform !== "win32") {
27
- const xdg = process.env.XDG_CONFIG_HOME;
28
- if (xdg && xdg.length > 0) return xdg;
29
- }
30
- return join(homeBase(), ".config");
31
- }
32
-
33
- export function configDir(): string {
34
- return join(configBase(), CONFIG_DIR_NAME);
35
- }
36
-
37
- export function configPath(): string {
38
- return join(configDir(), "config.json");
39
- }
40
-
41
- function isRecord(value: unknown): value is Record<string, unknown> {
42
- return typeof value === "object" && value !== null && !Array.isArray(value);
43
- }
44
-
45
- function errCode(error: unknown): string | undefined {
46
- if (typeof error !== "object" || error === null) return undefined;
47
- const code = (error as { code?: unknown }).code;
48
- return typeof code === "string" ? code : undefined;
49
- }
50
-
51
- function parseConfig(content: string): Config {
52
- const parsed = JSON.parse(content) as unknown;
53
- if (!isRecord(parsed) || (parsed.webRenderEnabled !== undefined && typeof parsed.webRenderEnabled !== "boolean")) {
54
- throw new Error("config.json must be an object with a boolean webRenderEnabled field");
55
- }
56
- return {
57
- webRenderEnabled:
58
- typeof parsed.webRenderEnabled === "boolean" ? parsed.webRenderEnabled : DEFAULT_CONFIG.webRenderEnabled,
59
- };
60
- }
61
-
62
- async function loadConfigFile(): Promise<{ config: Config; corrupted: boolean }> {
63
- let content: string;
64
- try {
65
- content = await readFile(configPath(), "utf-8");
66
- } catch (error: unknown) {
67
- if (errCode(error) === "ENOENT") return { config: { ...DEFAULT_CONFIG }, corrupted: false };
68
- console.error("Config file unreadable, using defaults:", error);
69
- return { config: { ...DEFAULT_CONFIG }, corrupted: false };
70
- }
71
- try {
72
- return { config: parseConfig(content), corrupted: false };
73
- } catch (error: unknown) {
74
- try {
75
- const badPath = configPath();
76
- await rename(badPath, `${badPath}.corrupt-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2)}`);
77
- } catch {}
78
- console.error("Config file corrupted, quarantined, using defaults:", error);
79
- return { config: { ...DEFAULT_CONFIG }, corrupted: true };
80
- }
81
- }
82
-
83
- export async function readConfig(): Promise<Config> {
84
- return (await loadConfigFile()).config;
85
- }
86
-
87
- export async function readConfigWithStatus(): Promise<{ config: Config; corrupted: boolean }> {
88
- return loadConfigFile();
89
- }
90
-
91
- async function syncDir(dir: string): Promise<void> {
92
- if (process.platform === "win32") return;
93
- try {
94
- const handle = await open(dir, "r");
95
- try {
96
- await handle.sync();
97
- } finally {
98
- await handle.close();
99
- }
100
- } catch {}
101
- }
102
-
103
- export async function writeConfig(config: Config): Promise<void> {
104
- const path = configPath();
105
- const dir = dirname(path);
106
- await mkdir(dir, { recursive: true, mode: 0o700 });
107
- const tempPath = join(dir, `${TEMP_PREFIX}${randomUUID()}`);
108
- const content = JSON.stringify(config, null, 2);
109
- await writeFile(tempPath, content, { mode: 0o600 });
110
- try {
111
- await rename(tempPath, path);
112
- await syncDir(dir);
113
- } catch (error: unknown) {
114
- if (process.platform === "win32" && errCode(error) === "EPERM") {
115
- try {
116
- await writeFile(path, content, "utf-8");
117
- return;
118
- } finally {
119
- try {
120
- await rm(tempPath, { force: true });
121
- } catch {}
122
- }
123
- }
124
- try {
125
- await rm(tempPath, { force: true });
126
- } catch {}
127
- throw error;
128
- }
129
- }
130
-
131
- interface ConfigLock {
132
- path: string;
133
- dev: number;
134
- ino: number;
135
- }
136
-
137
- async function acquireConfigLock(lockPath: string): Promise<ConfigLock> {
138
- try {
139
- await mkdir(dirname(lockPath), { recursive: true, mode: 0o700 });
140
- } catch {}
141
- for (let attempt = 0; attempt < CONFIG_LOCK_RETRIES; attempt++) {
142
- try {
143
- await mkdir(lockPath, { mode: 0o700 });
144
- } catch (error: unknown) {
145
- if (errCode(error) === "ENOENT") {
146
- try {
147
- await mkdir(dirname(lockPath), { recursive: true, mode: 0o700 });
148
- } catch {}
149
- continue;
150
- }
151
- if (errCode(error) !== "EEXIST") throw error;
152
- try {
153
- const lockStats = await stat(lockPath);
154
- if (Date.now() - lockStats.mtimeMs > CONFIG_LOCK_STALE_MS) {
155
- await rm(lockPath, { recursive: true, force: true });
156
- continue;
157
- }
158
- } catch {}
159
- await new Promise<void>((resolve) => setTimeout(resolve, CONFIG_LOCK_DELAY_MS));
160
- continue;
161
- }
162
- try {
163
- const lockStats = await stat(lockPath);
164
- return { path: lockPath, dev: lockStats.dev, ino: lockStats.ino };
165
- } catch (error: unknown) {
166
- if (errCode(error) !== "ENOENT") {
167
- try {
168
- await rm(lockPath, { recursive: true, force: true });
169
- } catch {}
170
- }
171
- continue;
172
- }
173
- }
174
- throw new Error(`[E_ACCESS] Could not acquire config lock: ${lockPath}`);
175
- }
176
-
177
- async function releaseConfigLock(lock: ConfigLock): Promise<void> {
178
- try {
179
- const lockStats = await stat(lock.path);
180
- if (lockStats.dev !== lock.dev || lockStats.ino !== lock.ino) return;
181
- } catch {
182
- return;
183
- }
184
- try {
185
- await rm(lock.path, { recursive: true, force: true });
186
- } catch {}
187
- }
188
-
189
- export async function updateConfig(mut: (config: Config) => void): Promise<Config> {
190
- const path = configPath();
191
- const lock = await acquireConfigLock(`${path}.lock`);
192
- try {
193
- const config = await readConfig();
194
- mut(config);
195
- await writeConfig(config);
196
- return config;
197
- } finally {
198
- await releaseConfigLock(lock);
199
- }
200
- }
201
-
202
- export async function toggleWebRender(): Promise<boolean> {
203
- const config = await updateConfig((current) => {
204
- current.webRenderEnabled = !(current.webRenderEnabled === true);
205
- });
206
- return config.webRenderEnabled === true;
207
- }
package/web-render.ts DELETED
@@ -1,100 +0,0 @@
1
- import { collapseWhitespace } from "./html-to-md.ts";
2
- import { checkUrlAccess, MAX_SIGNAL_TIMEOUT_MS, normalizeUrlScheme, type WebsitePolicy } from "./web-access.ts";
3
- import { parseLocalPath, resolveAndValidateHost, truncatePageText } from "./web-fetch.ts";
4
-
5
- const DEFAULT_RENDER_TIMEOUT_MS = 60_000;
6
- const JINA_READER_URL = "https://r.jina.ai/";
7
- const CANCELLED_MESSAGE = "Failed to render URL: cancelled.";
8
- const TIMED_OUT_MESSAGE = "Failed to render URL: timed out.";
9
- const LOCAL_FILE_MESSAGE = "Blocked: web_render cannot fetch local files.";
10
- const EMPTY_READER_MESSAGE = "Failed to render URL: the Jina Reader returned no content.";
11
-
12
- export interface RenderPageOptions {
13
- timeoutMs?: number;
14
- maxChars?: number;
15
- signal?: AbortSignal;
16
- websitePolicy?: WebsitePolicy | null;
17
- apiKey?: string;
18
- }
19
-
20
- interface JinaReaderData {
21
- title?: string;
22
- url?: string;
23
- content?: string;
24
- }
25
-
26
- interface JinaReaderPayload {
27
- data?: JinaReaderData;
28
- }
29
-
30
- function clampTimeout(value: number | undefined): number {
31
- if (value === undefined || !Number.isFinite(value)) return DEFAULT_RENDER_TIMEOUT_MS;
32
- return Math.min(MAX_SIGNAL_TIMEOUT_MS, Math.max(1, Math.floor(value)));
33
- }
34
-
35
- function budgetMessage(caller: AbortSignal | undefined, signal: AbortSignal): string | null {
36
- if (caller?.aborted) return CANCELLED_MESSAGE;
37
- if (signal.aborted) return TIMED_OUT_MESSAGE;
38
- return null;
39
- }
40
-
41
- async function readerErrorDetail(response: Response): Promise<string> {
42
- try {
43
- const body = (await response.json()) as { readableMessage?: unknown; message?: unknown; detail?: unknown };
44
- const detail = [body.readableMessage, body.message, body.detail].find(
45
- (value) => typeof value === "string" && value.trim().length > 0,
46
- );
47
- if (detail) return `${response.status} ${detail}`;
48
- } catch {}
49
- return `${response.status} ${response.statusText}`.trim();
50
- }
51
-
52
- function formatRenderedPage(data: JinaReaderData, fallbackUrl: string, maxChars: number | undefined): string {
53
- const title = collapseWhitespace(String(data.title ?? ""));
54
- const finalUrl = collapseWhitespace(String(data.url ?? "")) || fallbackUrl;
55
- const content = String(data.content ?? "").trim();
56
- if (!content) return truncatePageText("", maxChars);
57
- const header: string[] = [];
58
- if (title) header.push(`Title: ${title}`);
59
- header.push(`URL: ${finalUrl}`);
60
- header.push("Rendered via the Jina Reader (r.jina.ai).");
61
- return truncatePageText(`${header.join("\n")}\n\n${content}`, maxChars);
62
- }
63
-
64
- export async function renderPageText(url: string, options: RenderPageOptions = {}): Promise<string> {
65
- const target = typeof url === "string" ? url.trim() : "";
66
- const policy = options.websitePolicy ?? null;
67
- if (!target) return checkUrlAccess("", policy)[1];
68
- if (parseLocalPath(target) !== null) return LOCAL_FILE_MESSAGE;
69
- const normalized = normalizeUrlScheme(target);
70
- const [allowed, reason, hostname] = checkUrlAccess(normalized, policy);
71
- if (!allowed) return reason;
72
- const timeoutSignal = AbortSignal.timeout(clampTimeout(options.timeoutMs));
73
- const signal = options.signal ? AbortSignal.any([options.signal, timeoutSignal]) : timeoutSignal;
74
- const resolved = await resolveAndValidateHost(hostname, signal, false);
75
- const resolutionBudget = budgetMessage(options.signal, signal);
76
- if (resolutionBudget !== null) return resolutionBudget;
77
- if (!resolved.ok) return resolved.reason;
78
- const headers: Record<string, string> = { Accept: "application/json" };
79
- const apiKey = options.apiKey?.trim();
80
- if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
81
- let response: Response;
82
- try {
83
- response = await fetch(JINA_READER_URL + encodeURIComponent(normalized), { headers, signal });
84
- } catch (err) {
85
- const budget = budgetMessage(options.signal, signal);
86
- if (budget !== null) return budget;
87
- return `Failed to render URL: ${err instanceof Error ? err.message : String(err)}`;
88
- }
89
- if (!response.ok) return `Failed to render URL: ${await readerErrorDetail(response)}`;
90
- let payload: JinaReaderPayload;
91
- try {
92
- payload = (await response.json()) as JinaReaderPayload;
93
- } catch {
94
- const budget = budgetMessage(options.signal, signal);
95
- if (budget !== null) return budget;
96
- return `Failed to render URL: the Jina Reader returned a non-JSON response (HTTP ${response.status}).`;
97
- }
98
- if (!payload || !payload.data) return EMPTY_READER_MESSAGE;
99
- return formatRenderedPage(payload.data, normalized, options.maxChars);
100
- }