dsh-deeptutor 0.1.3 → 0.1.4

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.
@@ -1,53 +1,53 @@
1
- /**
2
- * DeepTutor bridge: render a learning answer to a self-contained local HTML file
3
- * via the bundled zero-dependency Node converter (scripts/md-to-html.js, from
4
- * the html-doc skill). Kept as a standalone module (node built-ins only).
5
- */
6
- import { execFile } from "node:child_process";
7
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
8
- import { basename, dirname, extname, join, resolve } from "node:path";
9
- import { fileURLToPath } from "node:url";
10
-
11
- /** Absolute path to the bundled md-to-html converter script. */
12
- export const HTML_DOC_SCRIPT = join(
13
- fileURLToPath(new URL("..", import.meta.url)),
14
- "scripts",
15
- "md-to-html.js",
16
- );
17
-
18
- /**
19
- * Render a learning answer to a self-contained local HTML file (dark/light toggle,
20
- * TOC, collapsible quiz answers, print-friendly). Also writes the companion .md
21
- * source next to the html file.
22
- *
23
- * Returns a short status note to append to the tool result ("" when no html path
24
- * given; "[html] ✗ …" note when rendering fails — never throws).
25
- */
26
- export async function renderHtml(
27
- params: { html?: string; html_title?: string },
28
- answer: string,
29
- ): Promise<string> {
30
- const out = params.html;
31
- if (!out) return "";
32
- const htmlPath = extname(out).toLowerCase() === ".html" ? resolve(out) : resolve(out + ".html");
33
- const mdPath = htmlPath.slice(0, -extname(htmlPath).length) + ".md";
34
- const title = params.html_title || basename(htmlPath, extname(htmlPath));
35
- try {
36
- if (!existsSync(HTML_DOC_SCRIPT)) {
37
- return `\n[html] ✗ html-doc converter not found: ${HTML_DOC_SCRIPT}`;
38
- }
39
- mkdirSync(dirname(htmlPath), { recursive: true });
40
- writeFileSync(mdPath, answer || "(no answer)", "utf8");
41
- await new Promise<void>((res, rej) => {
42
- execFile(
43
- process.execPath,
44
- [HTML_DOC_SCRIPT, mdPath, "--out", htmlPath, "--title", title, "--toc"],
45
- { windowsHide: true, timeout: 60_000 },
46
- (err, _stdout, stderr) => (err ? rej(new Error(stderr || err.message)) : res()),
47
- );
48
- });
49
- return `\n[html] ✓ rendered (html-doc skill)\n[html] page: ${htmlPath}\n[html] source: ${mdPath}`;
50
- } catch (err: any) {
51
- return `\n[html] ✗ rendering failed: ${err?.message ?? String(err)}`;
52
- }
53
- }
1
+ /**
2
+ * DeepTutor bridge: render a learning answer to a self-contained local HTML file
3
+ * via the bundled zero-dependency Node converter (scripts/md-to-html.js, from
4
+ * the html-doc skill). Kept as a standalone module (node built-ins only).
5
+ */
6
+ import { execFile } from "node:child_process";
7
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
8
+ import { basename, dirname, extname, join, resolve } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+
11
+ /** Absolute path to the bundled md-to-html converter script. */
12
+ export const HTML_DOC_SCRIPT = join(
13
+ fileURLToPath(new URL("..", import.meta.url)),
14
+ "scripts",
15
+ "md-to-html.js",
16
+ );
17
+
18
+ /**
19
+ * Render a learning answer to a self-contained local HTML file (dark/light toggle,
20
+ * TOC, collapsible quiz answers, print-friendly). Also writes the companion .md
21
+ * source next to the html file.
22
+ *
23
+ * Returns a short status note to append to the tool result ("" when no html path
24
+ * given; "[html] ✗ …" note when rendering fails — never throws).
25
+ */
26
+ export async function renderHtml(
27
+ params: { html?: string; html_title?: string },
28
+ answer: string,
29
+ ): Promise<string> {
30
+ const out = params.html;
31
+ if (!out) return "";
32
+ const htmlPath = extname(out).toLowerCase() === ".html" ? resolve(out) : resolve(out + ".html");
33
+ const mdPath = htmlPath.slice(0, -extname(htmlPath).length) + ".md";
34
+ const title = params.html_title || basename(htmlPath, extname(htmlPath));
35
+ try {
36
+ if (!existsSync(HTML_DOC_SCRIPT)) {
37
+ return `\n[html] ✗ html-doc converter not found: ${HTML_DOC_SCRIPT}`;
38
+ }
39
+ mkdirSync(dirname(htmlPath), { recursive: true });
40
+ writeFileSync(mdPath, answer || "(no answer)", "utf8");
41
+ await new Promise<void>((res, rej) => {
42
+ execFile(
43
+ process.execPath,
44
+ [HTML_DOC_SCRIPT, mdPath, "--out", htmlPath, "--title", title, "--toc"],
45
+ { windowsHide: true, timeout: 60_000 },
46
+ (err, _stdout, stderr) => (err ? rej(new Error(stderr || err.message)) : res()),
47
+ );
48
+ });
49
+ return `\n[html] ✓ rendered (html-doc skill)\n[html] page: ${htmlPath}\n[html] source: ${mdPath}`;
50
+ } catch (err: any) {
51
+ return `\n[html] ✗ rendering failed: ${err?.message ?? String(err)}`;
52
+ }
53
+ }
package/src/http-api.ts CHANGED
@@ -1,83 +1,83 @@
1
- /**
2
- * DeepTutor bridge: HTTP/WS transport (API probing, SSH tunnel management, JSON calls).
3
- */
4
- import { spawn } from "node:child_process";
5
- import { API_BASE, SSH_HOST, TUNNEL_PORT } from "./config.ts";
6
-
7
- let tunnelProc: ReturnType<typeof spawn> | null = null;
8
-
9
- /** Kill the SSH tunnel process, if one is running (call on plugin disposal). */
10
- export function killTunnel(): void {
11
- if (tunnelProc && tunnelProc.exitCode === null) {
12
- try {
13
- tunnelProc.kill();
14
- } catch {}
15
- tunnelProc = null;
16
- }
17
- }
18
-
19
- export async function apiProbe(timeoutMs = 1500): Promise<boolean> {
20
- try {
21
- const res = await fetch(`${API_BASE}/api/v1/system/status`, {
22
- signal: AbortSignal.timeout(timeoutMs),
23
- });
24
- return res.ok;
25
- } catch {
26
- return false;
27
- }
28
- }
29
-
30
- /** Ensure the local API is reachable: probe → start SSH tunnel if needed (remote only) → wait. */
31
- export async function ensureApi(): Promise<boolean> {
32
- if (await apiProbe(1200)) return true;
33
- if (!SSH_HOST) return false; // Local deployment: fall back to local CLI when serve is down, no tunnel needed
34
- if (!tunnelProc || tunnelProc.exitCode !== null) {
35
- tunnelProc = spawn(
36
- "ssh",
37
- [
38
- "-N",
39
- "-L",
40
- `${TUNNEL_PORT}:127.0.0.1:${TUNNEL_PORT}`,
41
- "-o",
42
- "BatchMode=yes",
43
- "-o",
44
- "ExitOnForwardFailure=yes",
45
- "-o",
46
- "ServerAliveInterval=30",
47
- "-o",
48
- "ServerAliveCountMax=3",
49
- "-o",
50
- "ConnectTimeout=10",
51
- SSH_HOST,
52
- ],
53
- { stdio: "ignore", windowsHide: true },
54
- );
55
- tunnelProc.unref();
56
- }
57
- for (let i = 0; i < 20; i++) {
58
- if (await apiProbe(1000)) return true;
59
- await new Promise((r) => setTimeout(r, 500));
60
- }
61
- return false;
62
- }
63
-
64
- /** JSON fetch against the DeepTutor API. Combines a fixed timeout with an optional caller-supplied abort signal. */
65
- export async function apiJson(
66
- path: string,
67
- init?: RequestInit,
68
- opts: { timeoutMs?: number; signal?: AbortSignal } = {},
69
- ): Promise<any> {
70
- const controller = new AbortController();
71
- const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 60_000);
72
- const onAbort = () => controller.abort();
73
- opts.signal?.addEventListener("abort", onAbort, { once: true });
74
- try {
75
- const res = await fetch(`${API_BASE}${path}`, { ...init, signal: controller.signal });
76
- if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
77
- const text = await res.text();
78
- return text ? JSON.parse(text) : null;
79
- } finally {
80
- clearTimeout(timer);
81
- opts.signal?.removeEventListener("abort", onAbort);
82
- }
83
- }
1
+ /**
2
+ * DeepTutor bridge: HTTP/WS transport (API probing, SSH tunnel management, JSON calls).
3
+ */
4
+ import { spawn } from "node:child_process";
5
+ import { API_BASE, SSH_HOST, TUNNEL_PORT } from "./config.ts";
6
+
7
+ let tunnelProc: ReturnType<typeof spawn> | null = null;
8
+
9
+ /** Kill the SSH tunnel process, if one is running (call on plugin disposal). */
10
+ export function killTunnel(): void {
11
+ if (tunnelProc && tunnelProc.exitCode === null) {
12
+ try {
13
+ tunnelProc.kill();
14
+ } catch {}
15
+ tunnelProc = null;
16
+ }
17
+ }
18
+
19
+ export async function apiProbe(timeoutMs = 1500): Promise<boolean> {
20
+ try {
21
+ const res = await fetch(`${API_BASE}/api/v1/system/status`, {
22
+ signal: AbortSignal.timeout(timeoutMs),
23
+ });
24
+ return res.ok;
25
+ } catch {
26
+ return false;
27
+ }
28
+ }
29
+
30
+ /** Ensure the local API is reachable: probe → start SSH tunnel if needed (remote only) → wait. */
31
+ export async function ensureApi(): Promise<boolean> {
32
+ if (await apiProbe(1200)) return true;
33
+ if (!SSH_HOST) return false; // Local deployment: fall back to local CLI when serve is down, no tunnel needed
34
+ if (!tunnelProc || tunnelProc.exitCode !== null) {
35
+ tunnelProc = spawn(
36
+ "ssh",
37
+ [
38
+ "-N",
39
+ "-L",
40
+ `${TUNNEL_PORT}:127.0.0.1:${TUNNEL_PORT}`,
41
+ "-o",
42
+ "BatchMode=yes",
43
+ "-o",
44
+ "ExitOnForwardFailure=yes",
45
+ "-o",
46
+ "ServerAliveInterval=30",
47
+ "-o",
48
+ "ServerAliveCountMax=3",
49
+ "-o",
50
+ "ConnectTimeout=10",
51
+ SSH_HOST,
52
+ ],
53
+ { stdio: "ignore", windowsHide: true },
54
+ );
55
+ tunnelProc.unref();
56
+ }
57
+ for (let i = 0; i < 20; i++) {
58
+ if (await apiProbe(1000)) return true;
59
+ await new Promise((r) => setTimeout(r, 500));
60
+ }
61
+ return false;
62
+ }
63
+
64
+ /** JSON fetch against the DeepTutor API. Combines a fixed timeout with an optional caller-supplied abort signal. */
65
+ export async function apiJson(
66
+ path: string,
67
+ init?: RequestInit,
68
+ opts: { timeoutMs?: number; signal?: AbortSignal } = {},
69
+ ): Promise<any> {
70
+ const controller = new AbortController();
71
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 60_000);
72
+ const onAbort = () => controller.abort();
73
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
74
+ try {
75
+ const res = await fetch(`${API_BASE}${path}`, { ...init, signal: controller.signal });
76
+ if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
77
+ const text = await res.text();
78
+ return text ? JSON.parse(text) : null;
79
+ } finally {
80
+ clearTimeout(timer);
81
+ opts.signal?.removeEventListener("abort", onAbort);
82
+ }
83
+ }