dsh-deeptutor 0.3.0 → 0.3.2
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/.agents/skills/deeptutor/SKILL.md +21 -5
- package/.agents/skills/html-doc/SKILL.md +25 -0
- package/.agents/skills/html-doc/scripts/md-to-html.js +29 -5
- package/README.md +9 -6
- package/README.zh.md +4 -5
- package/lib/cli-exec.d.ts +24 -0
- package/lib/cli-exec.js +36 -4
- package/lib/html-render.d.ts +32 -1
- package/lib/html-render.js +122 -11
- package/lib/index.js +3 -2
- package/lib/tools.js +7 -2
- package/lib/turn.js +6 -3
- package/package.json +1 -1
- package/scripts/mcp-launch.mjs +4 -1
- package/scripts/md-to-html.js +29 -5
- package/src/cli-exec.ts +39 -4
- package/src/html-render.ts +131 -15
- package/src/index.ts +5 -2
- package/src/tools.ts +6 -2
- package/src/turn.ts +6 -3
|
@@ -120,16 +120,32 @@ acceptance criteria; references.
|
|
|
120
120
|
|
|
121
121
|
Pass `html` to `deeptutor_run` and the answer is rendered to a self-contained
|
|
122
122
|
page (dark/light toggle, TOC, collapsible quiz answers, print-friendly) with
|
|
123
|
-
the
|
|
123
|
+
the **html-doc** converter, plus the `.md` source next to it:
|
|
124
124
|
|
|
125
125
|
```
|
|
126
126
|
deeptutor_run capability=deep_solve prompt=<topic> kbs=[...] tools=[rag, web_search]
|
|
127
|
-
html="
|
|
127
|
+
html="<absolute path, e.g. /home/me/study/<topic>/knowledge.html>" html_title="<主题> · 知识点" language=zh
|
|
128
128
|
```
|
|
129
129
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
`html` must be an **absolute** path. A tool has no workspace root (dsh hands it
|
|
131
|
+
only the agent identity, and the MCP server has no session), so a relative path
|
|
132
|
+
cannot be resolved against your working directory — it is rejected instead of
|
|
133
|
+
silently landing under the host process cwd. Prefix it with your working
|
|
134
|
+
directory explicitly.
|
|
135
|
+
|
|
136
|
+
The converter and its template are resolved in this order, and the tool result
|
|
137
|
+
prints which layer won (`converter:` / `template:` lines):
|
|
138
|
+
|
|
139
|
+
1. `HTML_DOC_SCRIPT` / `HTML_DOC_TEMPLATE` env vars (explicit override; a
|
|
140
|
+
set-but-missing path is an error),
|
|
141
|
+
2. the installed skill at `<DSH_HOME>/skills/html-doc/` (`DSH_HOME` defaults to
|
|
142
|
+
`~/.dsh`) — edit the template there to customise the page, no plugin edit
|
|
143
|
+
needed,
|
|
144
|
+
3. the copy bundled with the plugin.
|
|
145
|
+
|
|
146
|
+
To render other Markdown, use the html-doc skill's converter
|
|
147
|
+
(`node <html-doc skill dir>/scripts/md-to-html.js <in.md> --out <out.html> --title "<t>" --toc`,
|
|
148
|
+
plus `--template <file>` to pick a specific template).
|
|
133
149
|
Tell the user the file paths.
|
|
134
150
|
|
|
135
151
|
### 6. Archive (only on request)
|
|
@@ -39,6 +39,8 @@ cat notes.md | node scripts/md-to-html.js --title "Quiz" --theme dark --out quiz
|
|
|
39
39
|
# --toc generate a table of contents from headings (h2/h3)
|
|
40
40
|
# --lang <code> html lang attribute (default: zh-CN)
|
|
41
41
|
# --no-collapse disable <details> wrapping for quiz answers (default: collapse enabled)
|
|
42
|
+
# --template <file> HTML template to use (default: the assets/ copy next to the
|
|
43
|
+
# script, or $HTML_DOC_TEMPLATE when that is set)
|
|
42
44
|
```
|
|
43
45
|
|
|
44
46
|
## Supported Markdown
|
|
@@ -84,6 +86,29 @@ Edit `assets/template.html` — it uses `{{TITLE}}`, `{{BODY}}`, `{{THEME}}`, `{
|
|
|
84
86
|
- Table styling, blockquote styling, task-list checkboxes
|
|
85
87
|
- Responsive layout (comfortable reading column)
|
|
86
88
|
|
|
89
|
+
### Which template wins
|
|
90
|
+
|
|
91
|
+
The converter resolves the template in this order (first hit wins); the `--out`
|
|
92
|
+
log line names the file it used, and `deeptutor_run`'s `html` note prints it too:
|
|
93
|
+
|
|
94
|
+
1. `--template <file>` on the command line,
|
|
95
|
+
2. `$HTML_DOC_TEMPLATE`,
|
|
96
|
+
3. `assets/template.html` next to the script (the copy shipped in this skill),
|
|
97
|
+
4. `../.agents/skills/html-doc/assets/template.html` from a package-root duplicate.
|
|
98
|
+
|
|
99
|
+
An explicitly requested template (1 or 2) that does not exist is an error
|
|
100
|
+
instead of a silent fallback, so a typo cannot produce pages from the default
|
|
101
|
+
template.
|
|
102
|
+
|
|
103
|
+
`deeptutor_run`'s `html` path resolves this in the **host**, not inside this
|
|
104
|
+
script: it prefers the **installed** skill at `<DSH_HOME>/skills/html-doc/` over
|
|
105
|
+
the copy bundled in the plugin package (with `$HTML_DOC_SCRIPT`/`$HTML_DOC_TEMPLATE`
|
|
106
|
+
as explicit overrides), then hands the winning template to the converter as
|
|
107
|
+
`--template`. Editing the installed template therefore changes the next render —
|
|
108
|
+
no plugin edit needed. (Re-running the installer copies the bundled skill files
|
|
109
|
+
back over the installed ones, so keep a copy of a heavily customised template
|
|
110
|
+
outside `<DSH_HOME>/skills/` and point `$HTML_DOC_TEMPLATE` at it.)
|
|
111
|
+
|
|
87
112
|
## Notes
|
|
88
113
|
|
|
89
114
|
- Zero dependencies — only `node` (≥ 20.11, uses `import.meta.dirname`) is required; no npm install needed
|
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
6
|
* node md-to-html.js <input.md> [--out out.html] [--title "T"] [--theme dark|light]
|
|
7
|
-
* [--toc] [--lang zh-CN] [--no-collapse]
|
|
7
|
+
* [--toc] [--lang zh-CN] [--no-collapse] [--template template.html]
|
|
8
8
|
* cat file.md | node md-to-html.js --title "T" --out out.html
|
|
9
9
|
*
|
|
10
|
+
* Template resolution order: --template, $HTML_DOC_TEMPLATE, the assets/ copy
|
|
11
|
+
* next to this script, then the package-root duplicate's skill copy.
|
|
12
|
+
*
|
|
10
13
|
* Features: headings, fenced code blocks, inline code/bold/italic/links, lists,
|
|
11
14
|
* task lists, blockquotes, pipe tables, hr, quiz answer collapsing (<details>),
|
|
12
15
|
* raw HTML passthrough, HTML escaping. Zero npm dependencies (Node >= 20.11 — uses import.meta.dirname).
|
|
@@ -27,9 +30,10 @@ function parseArgs(argv) {
|
|
|
27
30
|
toc: false,
|
|
28
31
|
lang: "zh-CN",
|
|
29
32
|
collapse: true,
|
|
33
|
+
template: null,
|
|
30
34
|
};
|
|
31
35
|
const positional = [];
|
|
32
|
-
const valueFlags = new Set(["--out", "--title", "--theme", "--lang"]);
|
|
36
|
+
const valueFlags = new Set(["--out", "--title", "--theme", "--lang", "--template"]);
|
|
33
37
|
for (let i = 0; i < argv.length; i++) {
|
|
34
38
|
const a = argv[i];
|
|
35
39
|
if (a === "--toc") opts.toc = true;
|
|
@@ -50,7 +54,11 @@ function parseArgs(argv) {
|
|
|
50
54
|
fail(`Invalid --theme value "${value}": expected dark or light`);
|
|
51
55
|
}
|
|
52
56
|
opts.theme = value;
|
|
53
|
-
} else if (a === "--lang")
|
|
57
|
+
} else if (a === "--lang") {
|
|
58
|
+
opts.lang = value;
|
|
59
|
+
} else if (a === "--template") {
|
|
60
|
+
opts.template = value;
|
|
61
|
+
}
|
|
54
62
|
} else if (a.startsWith("--")) {
|
|
55
63
|
fail(`Unknown option: ${a} (run with --help for usage)`);
|
|
56
64
|
} else positional.push(a);
|
|
@@ -77,6 +85,8 @@ Options:
|
|
|
77
85
|
--theme <mode> dark | light (default: light; toggle also available in page)
|
|
78
86
|
--toc generate a table of contents from h2/h3 headings
|
|
79
87
|
--lang <code> html lang attribute (default: zh-CN)
|
|
88
|
+
--template <file> HTML template to use (default: the assets/ copy next to this
|
|
89
|
+
script, or $HTML_DOC_TEMPLATE when set)
|
|
80
90
|
--no-collapse disable quiz-answer collapsing
|
|
81
91
|
--help show this help`);
|
|
82
92
|
}
|
|
@@ -351,11 +361,25 @@ function main() {
|
|
|
351
361
|
const generated = new Date().toISOString().slice(0, 10);
|
|
352
362
|
// The same converter ships in two locations (.agents/skills/html-doc/scripts/
|
|
353
363
|
// and a package-root scripts/ copy); resolve the template wherever we are.
|
|
364
|
+
// Order: --template, $HTML_DOC_TEMPLATE, then the copies next to this script.
|
|
365
|
+
// An explicit override that does not exist is an error: a typo must not
|
|
366
|
+
// silently fall through to a different template.
|
|
367
|
+
const explicitTemplate = opts.template || process.env.HTML_DOC_TEMPLATE || null;
|
|
354
368
|
const templateCandidates = [
|
|
355
369
|
path.join(import.meta.dirname, "..", "assets", "template.html"),
|
|
356
370
|
path.join(import.meta.dirname, "..", ".agents", "skills", "html-doc", "assets", "template.html"),
|
|
357
371
|
];
|
|
358
|
-
|
|
372
|
+
let templatePath;
|
|
373
|
+
if (explicitTemplate) {
|
|
374
|
+
if (!fs.existsSync(explicitTemplate)) {
|
|
375
|
+
fail(
|
|
376
|
+
`template not found: ${explicitTemplate} (from ${opts.template ? "--template" : "HTML_DOC_TEMPLATE"})`,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
templatePath = explicitTemplate;
|
|
380
|
+
} else {
|
|
381
|
+
templatePath = templateCandidates.find((p) => fs.existsSync(p));
|
|
382
|
+
}
|
|
359
383
|
if (!templatePath) {
|
|
360
384
|
fail(`template.html not found (looked in: ${templateCandidates.join(", ")})`);
|
|
361
385
|
}
|
|
@@ -380,7 +404,7 @@ function main() {
|
|
|
380
404
|
if (opts.out) {
|
|
381
405
|
fs.writeFileSync(opts.out, template, "utf8");
|
|
382
406
|
console.log(
|
|
383
|
-
`[html-doc] wrote ${path.resolve(opts.out)} (${Buffer.byteLength(template)} bytes)`,
|
|
407
|
+
`[html-doc] wrote ${path.resolve(opts.out)} (${Buffer.byteLength(template)} bytes, template: ${templatePath})`,
|
|
384
408
|
);
|
|
385
409
|
} else {
|
|
386
410
|
process.stdout.write(template);
|
package/README.md
CHANGED
|
@@ -58,7 +58,7 @@ A typical flow when you ask your agent for learning help:
|
|
|
58
58
|
|
|
59
59
|
1. **Ask** — *"Explain C# generics covariance using my dotnet knowledge base, and generate a study page."*
|
|
60
60
|
2. The agent grounds the answer in your own material: `deeptutor_kb` (`action=search`, `kb=dotnet`).
|
|
61
|
-
3. It runs a deep-solve: `deeptutor_run` (`capability=deep_solve`, `kbs=[dotnet]`, `html
|
|
61
|
+
3. It runs a deep-solve: `deeptutor_run` (`capability=deep_solve`, `kbs=[dotnet]`, `html=/home/me/study/csharp-covariance.html`) — the answer comes back and a self-contained HTML page (plus the `.md` source) is written to disk. The `html` path must be absolute; relative paths are rejected.
|
|
62
62
|
4. It files a summary: `deeptutor_note` (`notebook=dotnet-learning`, `type=solve`).
|
|
63
63
|
|
|
64
64
|
Nothing here is a fixed script — the agent picks the tools and parameters based
|
|
@@ -182,7 +182,7 @@ install`:
|
|
|
182
182
|
|
|
183
183
|
```json
|
|
184
184
|
{
|
|
185
|
-
"dependencies": { "dsh-deeptutor": "^0.3.
|
|
185
|
+
"dependencies": { "dsh-deeptutor": "^0.3.1" },
|
|
186
186
|
"dsh": {
|
|
187
187
|
"profile": { "bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app", "dsh-deeptutor"] }
|
|
188
188
|
}
|
|
@@ -215,8 +215,12 @@ Two skills ship inside this package (`.agents/skills/deeptutor` and
|
|
|
215
215
|
|
|
216
216
|
- `deeptutor` — the agent-facing learning workflow (`~/.dsh/skills/deeptutor/SKILL.md`)
|
|
217
217
|
- `html-doc` — renders study answers to self-contained HTML pages
|
|
218
|
-
(`~/.dsh/skills/html-doc/`);
|
|
219
|
-
|
|
218
|
+
(`~/.dsh/skills/html-doc/`); when `deeptutor_run` gets an `html` path, the
|
|
219
|
+
converter and template are resolved as `$HTML_DOC_SCRIPT` /
|
|
220
|
+
`$HTML_DOC_TEMPLATE` → the installed skill above → the copy bundled in this
|
|
221
|
+
package. Editing the installed template (say, wiring KaTeX in) changes the
|
|
222
|
+
next render without patching the plugin, and the tool result prints which
|
|
223
|
+
layer supplied each file. The `html` path itself must be absolute.
|
|
220
224
|
|
|
221
225
|
Installing a newer package version overwrites skill files in place; files not
|
|
222
226
|
shipped by the package are never deleted.
|
|
@@ -290,8 +294,7 @@ scripts/install-profile.mjs # one-shot profile installer (exposed as the `dsh-d
|
|
|
290
294
|
scripts/mcp-launch.mjs # starts the MCP server for the Claude Code plugin
|
|
291
295
|
scripts/check-cli.ts # drift check against a live DeepTutor (`npm run check:cli`)
|
|
292
296
|
.agents/skills/ # bundled skills: deeptutor/ + html-doc/ (installed to <DSH_HOME>/skills/)
|
|
293
|
-
.claude-plugin/ # Claude Code plugin.json + marketplace.json
|
|
294
|
-
.mcp.json # the plugin's MCP server (runs scripts/mcp-launch.mjs)
|
|
297
|
+
.claude-plugin/ # Claude Code plugin.json (skills + MCP server) + marketplace.json
|
|
295
298
|
cordis.yml # dev overlay: insert src/index.ts by absolute path
|
|
296
299
|
cordis.patch.yml # bundle patch: insert the package by name
|
|
297
300
|
```
|
package/README.zh.md
CHANGED
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
|
|
39
39
|
1. **提问** —— *"用我的 dotnet 知识库讲讲 C# 泛型协变,并生成学习页。"*
|
|
40
40
|
2. agent 先检索你的自有资料:`deeptutor_kb`(`action=search`,`kb=dotnet`)。
|
|
41
|
-
3. 再执行深度讲解:`deeptutor_run`(`capability=deep_solve`,`kbs=[dotnet]`,`html
|
|
41
|
+
3. 再执行深度讲解:`deeptutor_run`(`capability=deep_solve`,`kbs=[dotnet]`,`html=/home/me/study/csharp-covariance.html`)—— 答案返回的同时,自包含 HTML 学习页(及配套 `.md` 源文件)写入磁盘。`html` 必须是绝对路径,相对路径会被拒绝。
|
|
42
42
|
4. 最后归档摘要:`deeptutor_note`(`notebook=dotnet-learning`,`type=solve`)。
|
|
43
43
|
|
|
44
44
|
以上不是固定脚本 —— agent 会根据你的提问自由选择工具与参数。
|
|
@@ -119,7 +119,7 @@ dsh --profile web --dump-config | grep dsh-deeptutor
|
|
|
119
119
|
|
|
120
120
|
```json
|
|
121
121
|
{
|
|
122
|
-
"dependencies": { "dsh-deeptutor": "^0.3.
|
|
122
|
+
"dependencies": { "dsh-deeptutor": "^0.3.1" },
|
|
123
123
|
"dsh": {
|
|
124
124
|
"profile": { "bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app", "dsh-deeptutor"] }
|
|
125
125
|
}
|
|
@@ -146,7 +146,7 @@ bundle 清单(`dsh.bundle.patch → cordis.patch.yml`)负责插入插件行;上
|
|
|
146
146
|
本包携带两个 skill(`.agents/skills/deeptutor` 与 `.agents/skills/html-doc`),随包安装器每次运行时都会把它们复制到 `<DSH_HOME>/skills/<name>/`(dsh 自动发现)—— 所以 `pnpm dlx dsh-deeptutor` 一次装齐插件 **和** skill:
|
|
147
147
|
|
|
148
148
|
- `deeptutor` — 面向 agent 的学习工作流(`~/.dsh/skills/deeptutor/SKILL.md`)
|
|
149
|
-
- `html-doc` — 将学习内容渲染为自包含 HTML 页面(`~/.dsh/skills/html-doc/`);`deeptutor_run` 传入 `html`
|
|
149
|
+
- `html-doc` — 将学习内容渲染为自包含 HTML 页面(`~/.dsh/skills/html-doc/`);`deeptutor_run` 传入 `html` 时,转换器与模板按 `$HTML_DOC_SCRIPT` / `$HTML_DOC_TEMPLATE` → 上面这个已安装 skill → 包内副本的顺序解析。改已安装模板(例如真正接上 KaTeX)下次渲染即生效,不用改插件包;工具结果会打印每层实际用的文件。`html` 路径必须是绝对路径。
|
|
150
150
|
|
|
151
151
|
升级包版本时会就地覆盖 skill 文件;包未携带的文件不会被删除。
|
|
152
152
|
|
|
@@ -205,8 +205,7 @@ scripts/install-profile.mjs # 一键 profile 安装器(以 `dsh-deeptutor` 二
|
|
|
205
205
|
scripts/mcp-launch.mjs # 为 Claude Code 插件启动 MCP server
|
|
206
206
|
scripts/check-cli.ts # 对真实 DeepTutor 的漂移检查(`npm run check:cli`)
|
|
207
207
|
.agents/skills/ # 随包 skill:deeptutor/ + html-doc/(安装到 <DSH_HOME>/skills/)
|
|
208
|
-
.claude-plugin/ # Claude Code 的 plugin.json 和 marketplace.json
|
|
209
|
-
.mcp.json # 插件的 MCP server(运行 scripts/mcp-launch.mjs)
|
|
208
|
+
.claude-plugin/ # Claude Code 的 plugin.json(skill 和 MCP server)和 marketplace.json
|
|
210
209
|
cordis.yml # 开发 overlay:按绝对路径插入 src/index.ts
|
|
211
210
|
cordis.patch.yml # bundle patch:按包名插入插件
|
|
212
211
|
```
|
package/lib/cli-exec.d.ts
CHANGED
|
@@ -1,9 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upper bound for one CLI run's captured stdout/stderr.
|
|
3
|
+
*
|
|
4
|
+
* `deeptutor run --format json` writes one JSON line per streamed chunk, and a
|
|
5
|
+
* chunk is roughly one character of the answer once event metadata is included
|
|
6
|
+
* (~1.5 KB of JSON per output character measured on 1.7.1). A 1500-character
|
|
7
|
+
* answer is therefore ~2 MB, already past Node's 1 MB `execFile` default: the
|
|
8
|
+
* child is killed mid-turn and the only trace is a string `err.code`
|
|
9
|
+
* (`ERR_CHILD_PROCESS_STDIO_MAXBUFFER`) with empty stderr. 64 MB covers a
|
|
10
|
+
* ~40k-character answer, well beyond the CLI's own per-turn token ceiling.
|
|
11
|
+
*/
|
|
12
|
+
export declare const MAX_CLI_OUTPUT_BYTES: number;
|
|
1
13
|
export interface CliResult {
|
|
2
14
|
stdout: string;
|
|
3
15
|
stderr: string;
|
|
4
16
|
code: number;
|
|
5
17
|
killed: boolean;
|
|
18
|
+
/** Node-level failure code, kept when stderr is empty so callers can report it. */
|
|
19
|
+
error?: string;
|
|
6
20
|
}
|
|
21
|
+
/** Model-facing detail for a failed CLI run: stderr first, else the Node error code. */
|
|
22
|
+
export declare function describeCliFailure(res: Pick<CliResult, "stderr" | "error">): string;
|
|
23
|
+
/** Exec one command with optional abort + timeout, resolving instead of throwing.
|
|
24
|
+
* Exported for tests: the captured-output ceiling is part of the contract. */
|
|
25
|
+
export declare function execFileResolved(file: string, args: string[], opts?: {
|
|
26
|
+
signal?: AbortSignal;
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
env?: Record<string, string>;
|
|
29
|
+
windowsVerbatimArguments?: boolean;
|
|
30
|
+
}): Promise<CliResult>;
|
|
7
31
|
/** Options shared by every CLI invocation. */
|
|
8
32
|
export interface CliOptions {
|
|
9
33
|
signal?: AbortSignal;
|
package/lib/cli-exec.js
CHANGED
|
@@ -8,6 +8,27 @@ import { tmpdir } from "node:os";
|
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { DEEPTUTOR_BIN, DEEPTUTOR_HOME, LOCAL_BIN, SSH_HOST } from "./config.js";
|
|
10
10
|
import { resolveLocalCommand } from "./win-cmd.js";
|
|
11
|
+
/**
|
|
12
|
+
* Upper bound for one CLI run's captured stdout/stderr.
|
|
13
|
+
*
|
|
14
|
+
* `deeptutor run --format json` writes one JSON line per streamed chunk, and a
|
|
15
|
+
* chunk is roughly one character of the answer once event metadata is included
|
|
16
|
+
* (~1.5 KB of JSON per output character measured on 1.7.1). A 1500-character
|
|
17
|
+
* answer is therefore ~2 MB, already past Node's 1 MB `execFile` default: the
|
|
18
|
+
* child is killed mid-turn and the only trace is a string `err.code`
|
|
19
|
+
* (`ERR_CHILD_PROCESS_STDIO_MAXBUFFER`) with empty stderr. 64 MB covers a
|
|
20
|
+
* ~40k-character answer, well beyond the CLI's own per-turn token ceiling.
|
|
21
|
+
*/
|
|
22
|
+
export const MAX_CLI_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
23
|
+
/** Model-facing detail for a failed CLI run: stderr first, else the Node error code. */
|
|
24
|
+
export function describeCliFailure(res) {
|
|
25
|
+
const stderr = (res.stderr ?? "").trim();
|
|
26
|
+
if (stderr)
|
|
27
|
+
return stderr;
|
|
28
|
+
if (res.error === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER")
|
|
29
|
+
return `output exceeded ${MAX_CLI_OUTPUT_BYTES} bytes (see MAX_CLI_OUTPUT_BYTES)`;
|
|
30
|
+
return res.error || "(no stderr)";
|
|
31
|
+
}
|
|
11
32
|
// Remote runner: reads argv from a JSON file and executes without a shell (avoids all quoting/escaping).
|
|
12
33
|
const PY_RUNNER = [
|
|
13
34
|
"import json, subprocess, sys",
|
|
@@ -48,14 +69,16 @@ const PY_NOTE = [
|
|
|
48
69
|
const b64 = (s) => Buffer.from(s, "utf8").toString("base64");
|
|
49
70
|
/** Timeout for each notebook-archiver CLI step (list / create / add-md). */
|
|
50
71
|
const NOTE_STEP_TIMEOUT_MS = 30_000;
|
|
51
|
-
/** Exec one command with optional abort + timeout, resolving instead of throwing.
|
|
52
|
-
|
|
72
|
+
/** Exec one command with optional abort + timeout, resolving instead of throwing.
|
|
73
|
+
* Exported for tests: the captured-output ceiling is part of the contract. */
|
|
74
|
+
export function execFileResolved(file, args, opts = {}) {
|
|
53
75
|
return new Promise((resolve) => {
|
|
54
76
|
const child = execFile(file, args, {
|
|
55
77
|
env: opts.env ? { ...process.env, ...opts.env } : process.env,
|
|
56
78
|
windowsHide: true,
|
|
57
79
|
windowsVerbatimArguments: opts.windowsVerbatimArguments,
|
|
58
80
|
timeout: opts.timeoutMs,
|
|
81
|
+
maxBuffer: MAX_CLI_OUTPUT_BYTES,
|
|
59
82
|
}, (err, stdout, stderr) => {
|
|
60
83
|
const e = err;
|
|
61
84
|
resolve({
|
|
@@ -63,6 +86,7 @@ function execFileResolved(file, args, opts = {}) {
|
|
|
63
86
|
stderr: stderr ?? "",
|
|
64
87
|
code: e ? (typeof e.code === "number" ? e.code : e.code === "ENOENT" ? 127 : 1) : 0,
|
|
65
88
|
killed: e?.killed ?? false,
|
|
89
|
+
error: e && typeof e.code === "string" ? String(e.code) : undefined,
|
|
66
90
|
});
|
|
67
91
|
});
|
|
68
92
|
opts.signal?.addEventListener("abort", () => {
|
|
@@ -107,6 +131,7 @@ async function sshExec(argv, opts = {}) {
|
|
|
107
131
|
stderr: (stderr ?? "").trim(),
|
|
108
132
|
code: res.code ?? -1,
|
|
109
133
|
killed: res.killed,
|
|
134
|
+
error: res.error,
|
|
110
135
|
};
|
|
111
136
|
}
|
|
112
137
|
/**
|
|
@@ -131,12 +156,19 @@ export async function runCli(args, opts = {}) {
|
|
|
131
156
|
windowsVerbatimArguments: cmd.windowsVerbatimArguments,
|
|
132
157
|
});
|
|
133
158
|
const out = res.stdout ?? "";
|
|
134
|
-
|
|
159
|
+
// Local mode is a direct execFile: the child's real stderr lands in res.stderr, and
|
|
160
|
+
// the __DT_STDERR__ marker only exists on the ssh path (the remote Python runner
|
|
161
|
+
// folds stderr into stdout). Splitting stdout still strips the marker if some wrapper
|
|
162
|
+
// emits one, but res.stderr must win — reading only the split tail threw away every
|
|
163
|
+
// Python traceback and reported "(no stderr)" for local runs (issue #20).
|
|
164
|
+
const [stdout, wrapped] = out.split("__DT_STDERR__");
|
|
165
|
+
const captured = (res.stderr ?? "").trim();
|
|
135
166
|
return {
|
|
136
167
|
stdout: stdout ?? "",
|
|
137
|
-
stderr: (
|
|
168
|
+
stderr: captured || (wrapped ?? "").trim(),
|
|
138
169
|
code: res.code,
|
|
139
170
|
killed: res.killed,
|
|
171
|
+
error: res.error,
|
|
140
172
|
};
|
|
141
173
|
}
|
|
142
174
|
/** Local-deployment notebook archiver: list → parse ID → create → add-md */
|
package/lib/html-render.d.ts
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
|
-
/** Absolute path to the bundled md-to-html converter
|
|
1
|
+
/** Absolute path to the bundled md-to-html converter (the last-resort fallback). */
|
|
2
2
|
export declare const HTML_DOC_SCRIPT: string;
|
|
3
|
+
/** dsh home: `$DSH_HOME`, else `~/.dsh` (mirrors scripts/install-profile.mjs). */
|
|
4
|
+
export declare function dshHome(): string;
|
|
5
|
+
export type Resolution = {
|
|
6
|
+
ok: true;
|
|
7
|
+
path: string;
|
|
8
|
+
source: string;
|
|
9
|
+
} | {
|
|
10
|
+
ok: false;
|
|
11
|
+
error: string;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Resolve the converter: explicit `$HTML_DOC_SCRIPT`, the installed html-doc
|
|
15
|
+
* skill, then the package copy. A set-but-missing override is an error — a typo
|
|
16
|
+
* must not silently fall through to a different converter.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveHtmlDocScript(): Resolution;
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the HTML template: explicit `$HTML_DOC_TEMPLATE`, the installed
|
|
21
|
+
* html-doc skill, then the package copy. A set-but-missing override is an error.
|
|
22
|
+
*/
|
|
23
|
+
export declare function resolveHtmlDocTemplate(): Resolution;
|
|
24
|
+
/**
|
|
25
|
+
* Why `html` cannot be a relative path (null when it is acceptable).
|
|
26
|
+
*
|
|
27
|
+
* A tool has no workspace root: dsh hands `execute` only the agent identity
|
|
28
|
+
* (`exec.agent.id`) plus the call's signal, and the MCP server has no session at
|
|
29
|
+
* all, so a relative path could only resolve against the host process cwd — the
|
|
30
|
+
* dsh config directory, not the caller's project. Requiring an absolute path is
|
|
31
|
+
* the one contract that can never write somewhere unexpected.
|
|
32
|
+
*/
|
|
33
|
+
export declare function htmlTargetProblem(out: string): string | null;
|
|
3
34
|
/**
|
|
4
35
|
* Render a learning answer to a self-contained local HTML file (dark/light toggle,
|
|
5
36
|
* TOC, collapsible quiz answers, print-friendly). Also writes the companion .md
|
package/lib/html-render.js
CHANGED
|
@@ -1,14 +1,113 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DeepTutor bridge: render a learning answer to a self-contained local HTML file
|
|
3
|
-
* via the
|
|
4
|
-
*
|
|
3
|
+
* via the zero-dependency Node converter (scripts/md-to-html.js, from the
|
|
4
|
+
* html-doc skill). Kept as a standalone module (node built-ins only).
|
|
5
|
+
*
|
|
6
|
+
* Two things are resolved at call time, first hit wins, and both are reported in
|
|
7
|
+
* the tool note so the order is visible without reading this file:
|
|
8
|
+
*
|
|
9
|
+
* converter $HTML_DOC_SCRIPT
|
|
10
|
+
* → <DSH_HOME>/skills/html-doc/scripts/md-to-html.js (installed skill)
|
|
11
|
+
* → the bundled package copy
|
|
12
|
+
* template $HTML_DOC_TEMPLATE
|
|
13
|
+
* → <DSH_HOME>/skills/html-doc/assets/template.html (installed skill)
|
|
14
|
+
* → the bundled package template
|
|
15
|
+
*
|
|
16
|
+
* <DSH_HOME> is `$DSH_HOME` or `~/.dsh` (same rule as scripts/install-profile.mjs),
|
|
17
|
+
* and `<DSH_HOME>/skills/<name>/` is where that installer copies the bundled
|
|
18
|
+
* skills for dsh auto-discovery. Preferring the installed copy means editing the
|
|
19
|
+
* installed html-doc template (say, wiring KaTeX in) changes `deeptutor_run`'s
|
|
20
|
+
* HTML immediately, instead of requiring edits to two files inside the plugin
|
|
21
|
+
* package that the next upgrade overwrites.
|
|
5
22
|
*/
|
|
6
23
|
import { execFile } from "node:child_process";
|
|
7
24
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
8
|
-
import {
|
|
25
|
+
import { homedir } from "node:os";
|
|
26
|
+
import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
|
|
9
27
|
import { fileURLToPath } from "node:url";
|
|
10
|
-
|
|
11
|
-
|
|
28
|
+
const PKG_ROOT = fileURLToPath(new URL("..", import.meta.url));
|
|
29
|
+
/** Absolute path to the bundled md-to-html converter (the last-resort fallback). */
|
|
30
|
+
export const HTML_DOC_SCRIPT = join(PKG_ROOT, "scripts", "md-to-html.js");
|
|
31
|
+
/** Bundled html-doc template, shipped inside the package's copy of the skill. */
|
|
32
|
+
const BUNDLED_TEMPLATE = join(PKG_ROOT, ".agents", "skills", "html-doc", "assets", "template.html");
|
|
33
|
+
/** dsh home: `$DSH_HOME`, else `~/.dsh` (mirrors scripts/install-profile.mjs). */
|
|
34
|
+
export function dshHome() {
|
|
35
|
+
return process.env.DSH_HOME?.trim() || join(homedir(), ".dsh");
|
|
36
|
+
}
|
|
37
|
+
/** `<DSH_HOME>/skills/html-doc/<segments…>` — the installer's auto-discovered skill. */
|
|
38
|
+
function installedSkill(...segments) {
|
|
39
|
+
return join(dshHome(), "skills", "html-doc", ...segments);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Resolve the converter: explicit `$HTML_DOC_SCRIPT`, the installed html-doc
|
|
43
|
+
* skill, then the package copy. A set-but-missing override is an error — a typo
|
|
44
|
+
* must not silently fall through to a different converter.
|
|
45
|
+
*/
|
|
46
|
+
export function resolveHtmlDocScript() {
|
|
47
|
+
const explicit = process.env.HTML_DOC_SCRIPT?.trim();
|
|
48
|
+
if (explicit) {
|
|
49
|
+
return existsSync(explicit)
|
|
50
|
+
? { ok: true, path: explicit, source: "HTML_DOC_SCRIPT" }
|
|
51
|
+
: { ok: false, error: `HTML_DOC_SCRIPT points at a missing file: ${explicit}` };
|
|
52
|
+
}
|
|
53
|
+
const installed = installedSkill("scripts", "md-to-html.js");
|
|
54
|
+
if (existsSync(installed))
|
|
55
|
+
return { ok: true, path: installed, source: "installed skill" };
|
|
56
|
+
if (existsSync(HTML_DOC_SCRIPT))
|
|
57
|
+
return { ok: true, path: HTML_DOC_SCRIPT, source: "bundled package" };
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
error: `html-doc converter not found (looked in: HTML_DOC_SCRIPT, ${installed}, ${HTML_DOC_SCRIPT})`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Resolve the HTML template: explicit `$HTML_DOC_TEMPLATE`, the installed
|
|
65
|
+
* html-doc skill, then the package copy. A set-but-missing override is an error.
|
|
66
|
+
*/
|
|
67
|
+
export function resolveHtmlDocTemplate() {
|
|
68
|
+
const explicit = process.env.HTML_DOC_TEMPLATE?.trim();
|
|
69
|
+
if (explicit) {
|
|
70
|
+
return existsSync(explicit)
|
|
71
|
+
? { ok: true, path: explicit, source: "HTML_DOC_TEMPLATE" }
|
|
72
|
+
: { ok: false, error: `HTML_DOC_TEMPLATE points at a missing file: ${explicit}` };
|
|
73
|
+
}
|
|
74
|
+
const installed = installedSkill("assets", "template.html");
|
|
75
|
+
if (existsSync(installed))
|
|
76
|
+
return { ok: true, path: installed, source: "installed skill" };
|
|
77
|
+
if (existsSync(BUNDLED_TEMPLATE))
|
|
78
|
+
return { ok: true, path: BUNDLED_TEMPLATE, source: "bundled package" };
|
|
79
|
+
return {
|
|
80
|
+
ok: false,
|
|
81
|
+
error: `html-doc template not found (looked in: HTML_DOC_TEMPLATE, ${installed}, ${BUNDLED_TEMPLATE})`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Why `html` cannot be a relative path (null when it is acceptable).
|
|
86
|
+
*
|
|
87
|
+
* A tool has no workspace root: dsh hands `execute` only the agent identity
|
|
88
|
+
* (`exec.agent.id`) plus the call's signal, and the MCP server has no session at
|
|
89
|
+
* all, so a relative path could only resolve against the host process cwd — the
|
|
90
|
+
* dsh config directory, not the caller's project. Requiring an absolute path is
|
|
91
|
+
* the one contract that can never write somewhere unexpected.
|
|
92
|
+
*/
|
|
93
|
+
export function htmlTargetProblem(out) {
|
|
94
|
+
if (isAbsolute(out.trim()))
|
|
95
|
+
return null;
|
|
96
|
+
return (`html must be an absolute path, got "${out}". Nothing was rendered: this tool cannot ` +
|
|
97
|
+
`resolve a relative path against your working directory (it has no workspace root, so a ` +
|
|
98
|
+
`relative path would land under the host process cwd instead). Prefix it with your working ` +
|
|
99
|
+
`directory, e.g. "<workspace>/data/study/topic/knowledge.html".`);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Extra CLI flag for the converter. The converter already picks up the template
|
|
103
|
+
* sitting next to it, so only pass `--template` when the resolved template is
|
|
104
|
+
* not that sibling — a plain bundled or installed run stays flag-free, and older
|
|
105
|
+
* converter copies that predate `--template` keep working on their own layout.
|
|
106
|
+
*/
|
|
107
|
+
function templateArgs(scriptPath, templatePath) {
|
|
108
|
+
const native = resolve(join(dirname(scriptPath), "..", "assets", "template.html"));
|
|
109
|
+
return native === resolve(templatePath) ? [] : ["--template", templatePath];
|
|
110
|
+
}
|
|
12
111
|
/**
|
|
13
112
|
* Render a learning answer to a self-contained local HTML file (dark/light toggle,
|
|
14
113
|
* TOC, collapsible quiz answers, print-friendly). Also writes the companion .md
|
|
@@ -21,19 +120,31 @@ export async function renderHtml(params, answer) {
|
|
|
21
120
|
const out = params.html;
|
|
22
121
|
if (!out)
|
|
23
122
|
return "";
|
|
24
|
-
const
|
|
123
|
+
const problem = htmlTargetProblem(out);
|
|
124
|
+
if (problem)
|
|
125
|
+
return `\n[html] ✗ ${problem}`;
|
|
126
|
+
const target = out.trim();
|
|
127
|
+
const htmlPath = extname(target).toLowerCase() === ".html" ? resolve(target) : resolve(target + ".html");
|
|
25
128
|
const mdPath = htmlPath.slice(0, -extname(htmlPath).length) + ".md";
|
|
26
129
|
const title = params.html_title || basename(htmlPath, extname(htmlPath));
|
|
130
|
+
const script = resolveHtmlDocScript();
|
|
131
|
+
if (!script.ok)
|
|
132
|
+
return `\n[html] ✗ ${script.error}`;
|
|
133
|
+
const template = resolveHtmlDocTemplate();
|
|
134
|
+
if (!template.ok)
|
|
135
|
+
return `\n[html] ✗ ${template.error}`;
|
|
27
136
|
try {
|
|
28
|
-
if (!existsSync(HTML_DOC_SCRIPT)) {
|
|
29
|
-
return `\n[html] ✗ html-doc converter not found: ${HTML_DOC_SCRIPT}`;
|
|
30
|
-
}
|
|
31
137
|
mkdirSync(dirname(htmlPath), { recursive: true });
|
|
32
138
|
writeFileSync(mdPath, answer || "(no answer)", "utf8");
|
|
139
|
+
const argv = [script.path, mdPath, "--out", htmlPath, "--title", title, "--toc"];
|
|
33
140
|
await new Promise((res, rej) => {
|
|
34
|
-
execFile(process.execPath, [
|
|
141
|
+
execFile(process.execPath, [...argv, ...templateArgs(script.path, template.path)], { windowsHide: true, timeout: 60_000 }, (err, _stdout, stderr) => (err ? rej(new Error(stderr || err.message)) : res()));
|
|
35
142
|
});
|
|
36
|
-
return `\n[html] ✓ rendered (html-doc skill)
|
|
143
|
+
return (`\n[html] ✓ rendered (html-doc skill)` +
|
|
144
|
+
`\n[html] page: ${htmlPath}` +
|
|
145
|
+
`\n[html] source: ${mdPath}` +
|
|
146
|
+
`\n[html] converter: ${script.path} (${script.source})` +
|
|
147
|
+
`\n[html] template: ${template.path} (${template.source})`);
|
|
37
148
|
}
|
|
38
149
|
catch (err) {
|
|
39
150
|
return `\n[html] ✗ rendering failed: ${err?.message ?? String(err)}`;
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveHtmlDocScript } from "./html-render.js";
|
|
3
3
|
import { TOOLS } from "./tools.js";
|
|
4
4
|
export const name = "dsh-deeptutor";
|
|
5
5
|
export const inject = ["tools"];
|
|
@@ -20,7 +20,8 @@ const OUTPUT = {
|
|
|
20
20
|
render: (_args, value) => [{ type: "text", text: value.text ?? "" }],
|
|
21
21
|
};
|
|
22
22
|
export function apply(ctx) {
|
|
23
|
-
|
|
23
|
+
const converter = resolveHtmlDocScript();
|
|
24
|
+
console.log(`[dsh-deeptutor] plugin loaded (html-doc converter: ${converter.ok ? `${converter.path} (${converter.source})` : converter.error})`);
|
|
24
25
|
for (const tool of TOOLS) {
|
|
25
26
|
ctx.tools.register(defineTool({
|
|
26
27
|
name: tool.name,
|
package/lib/tools.js
CHANGED
|
@@ -20,7 +20,7 @@ import { cliNoteLocal, cliNoteRemote, runCli } from "./cli-exec.js";
|
|
|
20
20
|
import { classifyCliArgs } from "./cli-policy.js";
|
|
21
21
|
import { configGuide, configHint, SSH_HOST, truncate } from "./config.js";
|
|
22
22
|
import { describeCapabilities, discoverCapabilities } from "./discovery.js";
|
|
23
|
-
import { renderHtml } from "./html-render.js";
|
|
23
|
+
import { renderHtml, htmlTargetProblem } from "./html-render.js";
|
|
24
24
|
import { cliRunTurn, fmtTurnResult } from "./turn.js";
|
|
25
25
|
/** Plain JSON Schema (object) for one tool's parameters. Pure. */
|
|
26
26
|
export function toJsonSchema(params) {
|
|
@@ -95,11 +95,16 @@ const runTool = {
|
|
|
95
95
|
max_chars: { type: "number", description: "Max characters of the answer returned, default 30000" },
|
|
96
96
|
html: {
|
|
97
97
|
type: "string",
|
|
98
|
-
description: "Optional output path for a self-contained local HTML render of the answer (also writes the companion .md next to it), e.g.
|
|
98
|
+
description: "Optional absolute output path for a self-contained local HTML render of the answer (also writes the companion .md next to it), e.g. /home/me/study/abstract-class/knowledge.html. Must be absolute: this tool has no workspace root, so relative paths are rejected instead of silently landing under the host process cwd. The template comes from the installed html-doc skill when present ($HTML_DOC_TEMPLATE overrides it); see SKILL.md. Set it when the user wants browsable/printable study material.",
|
|
99
99
|
},
|
|
100
100
|
html_title: { type: "string", description: "HTML page title (default: derived from the html file name)" },
|
|
101
101
|
},
|
|
102
102
|
async execute(args, signal) {
|
|
103
|
+
// Fail before the (possibly minutes-long) DeepTutor turn: a relative html
|
|
104
|
+
// path cannot be resolved against the caller's workspace, so reject it here.
|
|
105
|
+
const htmlProblem = args.html ? htmlTargetProblem(String(args.html)) : null;
|
|
106
|
+
if (htmlProblem)
|
|
107
|
+
throw new Error(`[deeptutor_run] ${htmlProblem}`);
|
|
103
108
|
const timeoutMs = (args.timeout ?? 600) * 1000;
|
|
104
109
|
const maxChars = args.max_chars ?? 30000;
|
|
105
110
|
const capability = String(args.capability).trim();
|
package/lib/turn.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* (local or over SSH), JSONL event parsing and result formatting.
|
|
4
4
|
*/
|
|
5
5
|
import { truncate } from "./config.js";
|
|
6
|
-
import { runCli } from "./cli-exec.js";
|
|
6
|
+
import { describeCliFailure, runCli } from "./cli-exec.js";
|
|
7
7
|
/** Fold one CLI JSONL event into the running turn state. Exported for unit tests. */
|
|
8
8
|
export function collectEvent(state, e) {
|
|
9
9
|
if (e.type === "content" && typeof e.content === "string")
|
|
@@ -49,14 +49,17 @@ export function buildRunArgs(params) {
|
|
|
49
49
|
}
|
|
50
50
|
/** Execute one turn via the CLI (local or SSH). `run` is injectable for tests. */
|
|
51
51
|
export async function cliRunTurn(params, timeoutMs, signal, run = runCli) {
|
|
52
|
-
const { stdout, stderr, code, killed } = await run(buildRunArgs(params), {
|
|
52
|
+
const { stdout, stderr, code, killed, error } = await run(buildRunArgs(params), {
|
|
53
|
+
signal,
|
|
54
|
+
timeoutMs,
|
|
55
|
+
});
|
|
53
56
|
if (killed) {
|
|
54
57
|
// A kill can come from the user cancelling (signal) or from the timeout;
|
|
55
58
|
// report the real reason instead of always blaming the timeout.
|
|
56
59
|
throw new Error(signal?.aborted ? "Aborted" : `CLI execution timed out (${Math.round(timeoutMs / 1000)}s)`);
|
|
57
60
|
}
|
|
58
61
|
if (code !== 0)
|
|
59
|
-
throw new Error(`CLI exit=${code}: ${stderr
|
|
62
|
+
throw new Error(`CLI exit=${code}: ${describeCliFailure({ stderr, error })}`);
|
|
60
63
|
const state = { answer: "", errors: [], toolCalls: [] };
|
|
61
64
|
for (const line of stdout.split("\n")) {
|
|
62
65
|
if (!line.trim())
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-deeptutor",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Learning assistant extension for DeepSeek Harness (dsh) and Claude Code (MCP): DeepTutor tutoring for your agent: deep explanations, self-test questions, learning paths, personal knowledge-base search (RAG), note archiving, and any other DeepTutor CLI command (drives the DeepTutor CLI locally or over SSH).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
package/scripts/mcp-launch.mjs
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* Launcher for the DeepTutor MCP server, used by the Claude Code plugin
|
|
4
|
-
* (.
|
|
4
|
+
* (`mcpServers` in .claude-plugin/plugin.json → `node ${CLAUDE_PLUGIN_ROOT}/scripts/mcp-launch.mjs`).
|
|
5
|
+
* The config lives in plugin.json, not a root .mcp.json: Claude Code also reads
|
|
6
|
+
* a root .mcp.json as project-scope config whenever this repo is the working
|
|
7
|
+
* directory, where ${CLAUDE_PLUGIN_ROOT} is undefined.
|
|
5
8
|
*
|
|
6
9
|
* A plugin installed from the marketplace is a plain git checkout: it has the
|
|
7
10
|
* TypeScript sources but no compiled lib/ and no node_modules. The server has no
|
package/scripts/md-to-html.js
CHANGED
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
6
|
* node md-to-html.js <input.md> [--out out.html] [--title "T"] [--theme dark|light]
|
|
7
|
-
* [--toc] [--lang zh-CN] [--no-collapse]
|
|
7
|
+
* [--toc] [--lang zh-CN] [--no-collapse] [--template template.html]
|
|
8
8
|
* cat file.md | node md-to-html.js --title "T" --out out.html
|
|
9
9
|
*
|
|
10
|
+
* Template resolution order: --template, $HTML_DOC_TEMPLATE, the assets/ copy
|
|
11
|
+
* next to this script, then the package-root duplicate's skill copy.
|
|
12
|
+
*
|
|
10
13
|
* Features: headings, fenced code blocks, inline code/bold/italic/links, lists,
|
|
11
14
|
* task lists, blockquotes, pipe tables, hr, quiz answer collapsing (<details>),
|
|
12
15
|
* raw HTML passthrough, HTML escaping. Zero npm dependencies (Node >= 20.11 — uses import.meta.dirname).
|
|
@@ -27,9 +30,10 @@ function parseArgs(argv) {
|
|
|
27
30
|
toc: false,
|
|
28
31
|
lang: "zh-CN",
|
|
29
32
|
collapse: true,
|
|
33
|
+
template: null,
|
|
30
34
|
};
|
|
31
35
|
const positional = [];
|
|
32
|
-
const valueFlags = new Set(["--out", "--title", "--theme", "--lang"]);
|
|
36
|
+
const valueFlags = new Set(["--out", "--title", "--theme", "--lang", "--template"]);
|
|
33
37
|
for (let i = 0; i < argv.length; i++) {
|
|
34
38
|
const a = argv[i];
|
|
35
39
|
if (a === "--toc") opts.toc = true;
|
|
@@ -50,7 +54,11 @@ function parseArgs(argv) {
|
|
|
50
54
|
fail(`Invalid --theme value "${value}": expected dark or light`);
|
|
51
55
|
}
|
|
52
56
|
opts.theme = value;
|
|
53
|
-
} else if (a === "--lang")
|
|
57
|
+
} else if (a === "--lang") {
|
|
58
|
+
opts.lang = value;
|
|
59
|
+
} else if (a === "--template") {
|
|
60
|
+
opts.template = value;
|
|
61
|
+
}
|
|
54
62
|
} else if (a.startsWith("--")) {
|
|
55
63
|
fail(`Unknown option: ${a} (run with --help for usage)`);
|
|
56
64
|
} else positional.push(a);
|
|
@@ -77,6 +85,8 @@ Options:
|
|
|
77
85
|
--theme <mode> dark | light (default: light; toggle also available in page)
|
|
78
86
|
--toc generate a table of contents from h2/h3 headings
|
|
79
87
|
--lang <code> html lang attribute (default: zh-CN)
|
|
88
|
+
--template <file> HTML template to use (default: the assets/ copy next to this
|
|
89
|
+
script, or $HTML_DOC_TEMPLATE when set)
|
|
80
90
|
--no-collapse disable quiz-answer collapsing
|
|
81
91
|
--help show this help`);
|
|
82
92
|
}
|
|
@@ -351,11 +361,25 @@ function main() {
|
|
|
351
361
|
const generated = new Date().toISOString().slice(0, 10);
|
|
352
362
|
// The same converter ships in two locations (.agents/skills/html-doc/scripts/
|
|
353
363
|
// and a package-root scripts/ copy); resolve the template wherever we are.
|
|
364
|
+
// Order: --template, $HTML_DOC_TEMPLATE, then the copies next to this script.
|
|
365
|
+
// An explicit override that does not exist is an error: a typo must not
|
|
366
|
+
// silently fall through to a different template.
|
|
367
|
+
const explicitTemplate = opts.template || process.env.HTML_DOC_TEMPLATE || null;
|
|
354
368
|
const templateCandidates = [
|
|
355
369
|
path.join(import.meta.dirname, "..", "assets", "template.html"),
|
|
356
370
|
path.join(import.meta.dirname, "..", ".agents", "skills", "html-doc", "assets", "template.html"),
|
|
357
371
|
];
|
|
358
|
-
|
|
372
|
+
let templatePath;
|
|
373
|
+
if (explicitTemplate) {
|
|
374
|
+
if (!fs.existsSync(explicitTemplate)) {
|
|
375
|
+
fail(
|
|
376
|
+
`template not found: ${explicitTemplate} (from ${opts.template ? "--template" : "HTML_DOC_TEMPLATE"})`,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
templatePath = explicitTemplate;
|
|
380
|
+
} else {
|
|
381
|
+
templatePath = templateCandidates.find((p) => fs.existsSync(p));
|
|
382
|
+
}
|
|
359
383
|
if (!templatePath) {
|
|
360
384
|
fail(`template.html not found (looked in: ${templateCandidates.join(", ")})`);
|
|
361
385
|
}
|
|
@@ -380,7 +404,7 @@ function main() {
|
|
|
380
404
|
if (opts.out) {
|
|
381
405
|
fs.writeFileSync(opts.out, template, "utf8");
|
|
382
406
|
console.log(
|
|
383
|
-
`[html-doc] wrote ${path.resolve(opts.out)} (${Buffer.byteLength(template)} bytes)`,
|
|
407
|
+
`[html-doc] wrote ${path.resolve(opts.out)} (${Buffer.byteLength(template)} bytes, template: ${templatePath})`,
|
|
384
408
|
);
|
|
385
409
|
} else {
|
|
386
410
|
process.stdout.write(template);
|
package/src/cli-exec.ts
CHANGED
|
@@ -9,11 +9,35 @@ import { join } from "node:path";
|
|
|
9
9
|
import { DEEPTUTOR_BIN, DEEPTUTOR_HOME, LOCAL_BIN, SSH_HOST } from "./config.ts";
|
|
10
10
|
import { resolveLocalCommand } from "./win-cmd.ts";
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Upper bound for one CLI run's captured stdout/stderr.
|
|
14
|
+
*
|
|
15
|
+
* `deeptutor run --format json` writes one JSON line per streamed chunk, and a
|
|
16
|
+
* chunk is roughly one character of the answer once event metadata is included
|
|
17
|
+
* (~1.5 KB of JSON per output character measured on 1.7.1). A 1500-character
|
|
18
|
+
* answer is therefore ~2 MB, already past Node's 1 MB `execFile` default: the
|
|
19
|
+
* child is killed mid-turn and the only trace is a string `err.code`
|
|
20
|
+
* (`ERR_CHILD_PROCESS_STDIO_MAXBUFFER`) with empty stderr. 64 MB covers a
|
|
21
|
+
* ~40k-character answer, well beyond the CLI's own per-turn token ceiling.
|
|
22
|
+
*/
|
|
23
|
+
export const MAX_CLI_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
24
|
+
|
|
12
25
|
export interface CliResult {
|
|
13
26
|
stdout: string;
|
|
14
27
|
stderr: string;
|
|
15
28
|
code: number;
|
|
16
29
|
killed: boolean;
|
|
30
|
+
/** Node-level failure code, kept when stderr is empty so callers can report it. */
|
|
31
|
+
error?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Model-facing detail for a failed CLI run: stderr first, else the Node error code. */
|
|
35
|
+
export function describeCliFailure(res: Pick<CliResult, "stderr" | "error">): string {
|
|
36
|
+
const stderr = (res.stderr ?? "").trim();
|
|
37
|
+
if (stderr) return stderr;
|
|
38
|
+
if (res.error === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER")
|
|
39
|
+
return `output exceeded ${MAX_CLI_OUTPUT_BYTES} bytes (see MAX_CLI_OUTPUT_BYTES)`;
|
|
40
|
+
return res.error || "(no stderr)";
|
|
17
41
|
}
|
|
18
42
|
|
|
19
43
|
// Remote runner: reads argv from a JSON file and executes without a shell (avoids all quoting/escaping).
|
|
@@ -60,8 +84,9 @@ const b64 = (s: string) => Buffer.from(s, "utf8").toString("base64");
|
|
|
60
84
|
/** Timeout for each notebook-archiver CLI step (list / create / add-md). */
|
|
61
85
|
const NOTE_STEP_TIMEOUT_MS = 30_000;
|
|
62
86
|
|
|
63
|
-
/** Exec one command with optional abort + timeout, resolving instead of throwing.
|
|
64
|
-
|
|
87
|
+
/** Exec one command with optional abort + timeout, resolving instead of throwing.
|
|
88
|
+
* Exported for tests: the captured-output ceiling is part of the contract. */
|
|
89
|
+
export function execFileResolved(
|
|
65
90
|
file: string,
|
|
66
91
|
args: string[],
|
|
67
92
|
opts: {
|
|
@@ -80,6 +105,7 @@ function execFileResolved(
|
|
|
80
105
|
windowsHide: true,
|
|
81
106
|
windowsVerbatimArguments: opts.windowsVerbatimArguments,
|
|
82
107
|
timeout: opts.timeoutMs,
|
|
108
|
+
maxBuffer: MAX_CLI_OUTPUT_BYTES,
|
|
83
109
|
},
|
|
84
110
|
(err, stdout, stderr) => {
|
|
85
111
|
const e: any = err;
|
|
@@ -88,6 +114,7 @@ function execFileResolved(
|
|
|
88
114
|
stderr: stderr ?? "",
|
|
89
115
|
code: e ? (typeof e.code === "number" ? e.code : e.code === "ENOENT" ? 127 : 1) : 0,
|
|
90
116
|
killed: e?.killed ?? false,
|
|
117
|
+
error: e && typeof e.code === "string" ? String(e.code) : undefined,
|
|
91
118
|
});
|
|
92
119
|
},
|
|
93
120
|
);
|
|
@@ -151,6 +178,7 @@ async function sshExec(argv: string[], opts: CliOptions = {}): Promise<CliResult
|
|
|
151
178
|
stderr: (stderr ?? "").trim(),
|
|
152
179
|
code: res.code ?? -1,
|
|
153
180
|
killed: res.killed,
|
|
181
|
+
error: res.error,
|
|
154
182
|
};
|
|
155
183
|
}
|
|
156
184
|
|
|
@@ -175,12 +203,19 @@ export async function runCli(args: string[], opts: CliOptions = {}): Promise<Cli
|
|
|
175
203
|
windowsVerbatimArguments: cmd.windowsVerbatimArguments,
|
|
176
204
|
});
|
|
177
205
|
const out = res.stdout ?? "";
|
|
178
|
-
|
|
206
|
+
// Local mode is a direct execFile: the child's real stderr lands in res.stderr, and
|
|
207
|
+
// the __DT_STDERR__ marker only exists on the ssh path (the remote Python runner
|
|
208
|
+
// folds stderr into stdout). Splitting stdout still strips the marker if some wrapper
|
|
209
|
+
// emits one, but res.stderr must win — reading only the split tail threw away every
|
|
210
|
+
// Python traceback and reported "(no stderr)" for local runs (issue #20).
|
|
211
|
+
const [stdout, wrapped] = out.split("__DT_STDERR__");
|
|
212
|
+
const captured = (res.stderr ?? "").trim();
|
|
179
213
|
return {
|
|
180
214
|
stdout: stdout ?? "",
|
|
181
|
-
stderr: (
|
|
215
|
+
stderr: captured || (wrapped ?? "").trim(),
|
|
182
216
|
code: res.code,
|
|
183
217
|
killed: res.killed,
|
|
218
|
+
error: res.error,
|
|
184
219
|
};
|
|
185
220
|
}
|
|
186
221
|
|
package/src/html-render.ts
CHANGED
|
@@ -1,19 +1,123 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DeepTutor bridge: render a learning answer to a self-contained local HTML file
|
|
3
|
-
* via the
|
|
4
|
-
*
|
|
3
|
+
* via the zero-dependency Node converter (scripts/md-to-html.js, from the
|
|
4
|
+
* html-doc skill). Kept as a standalone module (node built-ins only).
|
|
5
|
+
*
|
|
6
|
+
* Two things are resolved at call time, first hit wins, and both are reported in
|
|
7
|
+
* the tool note so the order is visible without reading this file:
|
|
8
|
+
*
|
|
9
|
+
* converter $HTML_DOC_SCRIPT
|
|
10
|
+
* → <DSH_HOME>/skills/html-doc/scripts/md-to-html.js (installed skill)
|
|
11
|
+
* → the bundled package copy
|
|
12
|
+
* template $HTML_DOC_TEMPLATE
|
|
13
|
+
* → <DSH_HOME>/skills/html-doc/assets/template.html (installed skill)
|
|
14
|
+
* → the bundled package template
|
|
15
|
+
*
|
|
16
|
+
* <DSH_HOME> is `$DSH_HOME` or `~/.dsh` (same rule as scripts/install-profile.mjs),
|
|
17
|
+
* and `<DSH_HOME>/skills/<name>/` is where that installer copies the bundled
|
|
18
|
+
* skills for dsh auto-discovery. Preferring the installed copy means editing the
|
|
19
|
+
* installed html-doc template (say, wiring KaTeX in) changes `deeptutor_run`'s
|
|
20
|
+
* HTML immediately, instead of requiring edits to two files inside the plugin
|
|
21
|
+
* package that the next upgrade overwrites.
|
|
5
22
|
*/
|
|
6
23
|
import { execFile } from "node:child_process";
|
|
7
24
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
8
|
-
import {
|
|
25
|
+
import { homedir } from "node:os";
|
|
26
|
+
import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
|
|
9
27
|
import { fileURLToPath } from "node:url";
|
|
10
28
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
29
|
+
const PKG_ROOT = fileURLToPath(new URL("..", import.meta.url));
|
|
30
|
+
|
|
31
|
+
/** Absolute path to the bundled md-to-html converter (the last-resort fallback). */
|
|
32
|
+
export const HTML_DOC_SCRIPT = join(PKG_ROOT, "scripts", "md-to-html.js");
|
|
33
|
+
|
|
34
|
+
/** Bundled html-doc template, shipped inside the package's copy of the skill. */
|
|
35
|
+
const BUNDLED_TEMPLATE = join(PKG_ROOT, ".agents", "skills", "html-doc", "assets", "template.html");
|
|
36
|
+
|
|
37
|
+
/** dsh home: `$DSH_HOME`, else `~/.dsh` (mirrors scripts/install-profile.mjs). */
|
|
38
|
+
export function dshHome(): string {
|
|
39
|
+
return process.env.DSH_HOME?.trim() || join(homedir(), ".dsh");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** `<DSH_HOME>/skills/html-doc/<segments…>` — the installer's auto-discovered skill. */
|
|
43
|
+
function installedSkill(...segments: string[]): string {
|
|
44
|
+
return join(dshHome(), "skills", "html-doc", ...segments);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type Resolution =
|
|
48
|
+
| { ok: true; path: string; source: string }
|
|
49
|
+
| { ok: false; error: string };
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Resolve the converter: explicit `$HTML_DOC_SCRIPT`, the installed html-doc
|
|
53
|
+
* skill, then the package copy. A set-but-missing override is an error — a typo
|
|
54
|
+
* must not silently fall through to a different converter.
|
|
55
|
+
*/
|
|
56
|
+
export function resolveHtmlDocScript(): Resolution {
|
|
57
|
+
const explicit = process.env.HTML_DOC_SCRIPT?.trim();
|
|
58
|
+
if (explicit) {
|
|
59
|
+
return existsSync(explicit)
|
|
60
|
+
? { ok: true, path: explicit, source: "HTML_DOC_SCRIPT" }
|
|
61
|
+
: { ok: false, error: `HTML_DOC_SCRIPT points at a missing file: ${explicit}` };
|
|
62
|
+
}
|
|
63
|
+
const installed = installedSkill("scripts", "md-to-html.js");
|
|
64
|
+
if (existsSync(installed)) return { ok: true, path: installed, source: "installed skill" };
|
|
65
|
+
if (existsSync(HTML_DOC_SCRIPT)) return { ok: true, path: HTML_DOC_SCRIPT, source: "bundled package" };
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
error: `html-doc converter not found (looked in: HTML_DOC_SCRIPT, ${installed}, ${HTML_DOC_SCRIPT})`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Resolve the HTML template: explicit `$HTML_DOC_TEMPLATE`, the installed
|
|
74
|
+
* html-doc skill, then the package copy. A set-but-missing override is an error.
|
|
75
|
+
*/
|
|
76
|
+
export function resolveHtmlDocTemplate(): Resolution {
|
|
77
|
+
const explicit = process.env.HTML_DOC_TEMPLATE?.trim();
|
|
78
|
+
if (explicit) {
|
|
79
|
+
return existsSync(explicit)
|
|
80
|
+
? { ok: true, path: explicit, source: "HTML_DOC_TEMPLATE" }
|
|
81
|
+
: { ok: false, error: `HTML_DOC_TEMPLATE points at a missing file: ${explicit}` };
|
|
82
|
+
}
|
|
83
|
+
const installed = installedSkill("assets", "template.html");
|
|
84
|
+
if (existsSync(installed)) return { ok: true, path: installed, source: "installed skill" };
|
|
85
|
+
if (existsSync(BUNDLED_TEMPLATE)) return { ok: true, path: BUNDLED_TEMPLATE, source: "bundled package" };
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
error: `html-doc template not found (looked in: HTML_DOC_TEMPLATE, ${installed}, ${BUNDLED_TEMPLATE})`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Why `html` cannot be a relative path (null when it is acceptable).
|
|
94
|
+
*
|
|
95
|
+
* A tool has no workspace root: dsh hands `execute` only the agent identity
|
|
96
|
+
* (`exec.agent.id`) plus the call's signal, and the MCP server has no session at
|
|
97
|
+
* all, so a relative path could only resolve against the host process cwd — the
|
|
98
|
+
* dsh config directory, not the caller's project. Requiring an absolute path is
|
|
99
|
+
* the one contract that can never write somewhere unexpected.
|
|
100
|
+
*/
|
|
101
|
+
export function htmlTargetProblem(out: string): string | null {
|
|
102
|
+
if (isAbsolute(out.trim())) return null;
|
|
103
|
+
return (
|
|
104
|
+
`html must be an absolute path, got "${out}". Nothing was rendered: this tool cannot ` +
|
|
105
|
+
`resolve a relative path against your working directory (it has no workspace root, so a ` +
|
|
106
|
+
`relative path would land under the host process cwd instead). Prefix it with your working ` +
|
|
107
|
+
`directory, e.g. "<workspace>/data/study/topic/knowledge.html".`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Extra CLI flag for the converter. The converter already picks up the template
|
|
113
|
+
* sitting next to it, so only pass `--template` when the resolved template is
|
|
114
|
+
* not that sibling — a plain bundled or installed run stays flag-free, and older
|
|
115
|
+
* converter copies that predate `--template` keep working on their own layout.
|
|
116
|
+
*/
|
|
117
|
+
function templateArgs(scriptPath: string, templatePath: string): string[] {
|
|
118
|
+
const native = resolve(join(dirname(scriptPath), "..", "assets", "template.html"));
|
|
119
|
+
return native === resolve(templatePath) ? [] : ["--template", templatePath];
|
|
120
|
+
}
|
|
17
121
|
|
|
18
122
|
/**
|
|
19
123
|
* Render a learning answer to a self-contained local HTML file (dark/light toggle,
|
|
@@ -29,24 +133,36 @@ export async function renderHtml(
|
|
|
29
133
|
): Promise<string> {
|
|
30
134
|
const out = params.html;
|
|
31
135
|
if (!out) return "";
|
|
32
|
-
const
|
|
136
|
+
const problem = htmlTargetProblem(out);
|
|
137
|
+
if (problem) return `\n[html] ✗ ${problem}`;
|
|
138
|
+
const target = out.trim();
|
|
139
|
+
const htmlPath =
|
|
140
|
+
extname(target).toLowerCase() === ".html" ? resolve(target) : resolve(target + ".html");
|
|
33
141
|
const mdPath = htmlPath.slice(0, -extname(htmlPath).length) + ".md";
|
|
34
142
|
const title = params.html_title || basename(htmlPath, extname(htmlPath));
|
|
143
|
+
const script = resolveHtmlDocScript();
|
|
144
|
+
if (!script.ok) return `\n[html] ✗ ${script.error}`;
|
|
145
|
+
const template = resolveHtmlDocTemplate();
|
|
146
|
+
if (!template.ok) return `\n[html] ✗ ${template.error}`;
|
|
35
147
|
try {
|
|
36
|
-
if (!existsSync(HTML_DOC_SCRIPT)) {
|
|
37
|
-
return `\n[html] ✗ html-doc converter not found: ${HTML_DOC_SCRIPT}`;
|
|
38
|
-
}
|
|
39
148
|
mkdirSync(dirname(htmlPath), { recursive: true });
|
|
40
149
|
writeFileSync(mdPath, answer || "(no answer)", "utf8");
|
|
150
|
+
const argv = [script.path, mdPath, "--out", htmlPath, "--title", title, "--toc"];
|
|
41
151
|
await new Promise<void>((res, rej) => {
|
|
42
152
|
execFile(
|
|
43
153
|
process.execPath,
|
|
44
|
-
[
|
|
154
|
+
[...argv, ...templateArgs(script.path, template.path)],
|
|
45
155
|
{ windowsHide: true, timeout: 60_000 },
|
|
46
156
|
(err, _stdout, stderr) => (err ? rej(new Error(stderr || err.message)) : res()),
|
|
47
157
|
);
|
|
48
158
|
});
|
|
49
|
-
return
|
|
159
|
+
return (
|
|
160
|
+
`\n[html] ✓ rendered (html-doc skill)` +
|
|
161
|
+
`\n[html] page: ${htmlPath}` +
|
|
162
|
+
`\n[html] source: ${mdPath}` +
|
|
163
|
+
`\n[html] converter: ${script.path} (${script.source})` +
|
|
164
|
+
`\n[html] template: ${template.path} (${template.source})`
|
|
165
|
+
);
|
|
50
166
|
} catch (err: any) {
|
|
51
167
|
return `\n[html] ✗ rendering failed: ${err?.message ?? String(err)}`;
|
|
52
168
|
}
|
package/src/index.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import type { Context } from "@deepseek-ai/cordis";
|
|
21
21
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
22
|
-
import {
|
|
22
|
+
import { resolveHtmlDocScript } from "./html-render.ts";
|
|
23
23
|
import { TOOLS } from "./tools.ts";
|
|
24
24
|
|
|
25
25
|
export const name = "dsh-deeptutor";
|
|
@@ -42,7 +42,10 @@ const OUTPUT = {
|
|
|
42
42
|
} as const;
|
|
43
43
|
|
|
44
44
|
export function apply(ctx: Context) {
|
|
45
|
-
|
|
45
|
+
const converter = resolveHtmlDocScript();
|
|
46
|
+
console.log(
|
|
47
|
+
`[dsh-deeptutor] plugin loaded (html-doc converter: ${converter.ok ? `${converter.path} (${converter.source})` : converter.error})`,
|
|
48
|
+
);
|
|
46
49
|
for (const tool of TOOLS) {
|
|
47
50
|
ctx.tools.register(
|
|
48
51
|
defineTool({
|
package/src/tools.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { cliNoteLocal, cliNoteRemote, runCli } from "./cli-exec.ts";
|
|
|
20
20
|
import { classifyCliArgs } from "./cli-policy.ts";
|
|
21
21
|
import { configGuide, configHint, SSH_HOST, truncate } from "./config.ts";
|
|
22
22
|
import { describeCapabilities, discoverCapabilities } from "./discovery.ts";
|
|
23
|
-
import { renderHtml } from "./html-render.ts";
|
|
23
|
+
import { renderHtml, htmlTargetProblem } from "./html-render.ts";
|
|
24
24
|
import { cliRunTurn, fmtTurnResult } from "./turn.ts";
|
|
25
25
|
|
|
26
26
|
interface Base {
|
|
@@ -134,11 +134,15 @@ const runTool: ToolSpec = {
|
|
|
134
134
|
html: {
|
|
135
135
|
type: "string",
|
|
136
136
|
description:
|
|
137
|
-
"Optional output path for a self-contained local HTML render of the answer (also writes the companion .md next to it), e.g.
|
|
137
|
+
"Optional absolute output path for a self-contained local HTML render of the answer (also writes the companion .md next to it), e.g. /home/me/study/abstract-class/knowledge.html. Must be absolute: this tool has no workspace root, so relative paths are rejected instead of silently landing under the host process cwd. The template comes from the installed html-doc skill when present ($HTML_DOC_TEMPLATE overrides it); see SKILL.md. Set it when the user wants browsable/printable study material.",
|
|
138
138
|
},
|
|
139
139
|
html_title: { type: "string", description: "HTML page title (default: derived from the html file name)" },
|
|
140
140
|
},
|
|
141
141
|
async execute(args, signal) {
|
|
142
|
+
// Fail before the (possibly minutes-long) DeepTutor turn: a relative html
|
|
143
|
+
// path cannot be resolved against the caller's workspace, so reject it here.
|
|
144
|
+
const htmlProblem = args.html ? htmlTargetProblem(String(args.html)) : null;
|
|
145
|
+
if (htmlProblem) throw new Error(`[deeptutor_run] ${htmlProblem}`);
|
|
142
146
|
const timeoutMs = (args.timeout ?? 600) * 1000;
|
|
143
147
|
const maxChars = args.max_chars ?? 30000;
|
|
144
148
|
const capability = String(args.capability).trim();
|
package/src/turn.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* (local or over SSH), JSONL event parsing and result formatting.
|
|
4
4
|
*/
|
|
5
5
|
import { truncate } from "./config.ts";
|
|
6
|
-
import { runCli } from "./cli-exec.ts";
|
|
6
|
+
import { describeCliFailure, runCli } from "./cli-exec.ts";
|
|
7
7
|
|
|
8
8
|
export interface TurnResult {
|
|
9
9
|
answer: string;
|
|
@@ -69,7 +69,10 @@ export async function cliRunTurn(
|
|
|
69
69
|
signal?: AbortSignal,
|
|
70
70
|
run: typeof runCli = runCli,
|
|
71
71
|
): Promise<TurnResult> {
|
|
72
|
-
const { stdout, stderr, code, killed } = await run(buildRunArgs(params), {
|
|
72
|
+
const { stdout, stderr, code, killed, error } = await run(buildRunArgs(params), {
|
|
73
|
+
signal,
|
|
74
|
+
timeoutMs,
|
|
75
|
+
});
|
|
73
76
|
if (killed) {
|
|
74
77
|
// A kill can come from the user cancelling (signal) or from the timeout;
|
|
75
78
|
// report the real reason instead of always blaming the timeout.
|
|
@@ -77,7 +80,7 @@ export async function cliRunTurn(
|
|
|
77
80
|
signal?.aborted ? "Aborted" : `CLI execution timed out (${Math.round(timeoutMs / 1000)}s)`,
|
|
78
81
|
);
|
|
79
82
|
}
|
|
80
|
-
if (code !== 0) throw new Error(`CLI exit=${code}: ${stderr
|
|
83
|
+
if (code !== 0) throw new Error(`CLI exit=${code}: ${describeCliFailure({ stderr, error })}`);
|
|
81
84
|
const state: TurnResult = { answer: "", errors: [], toolCalls: [] };
|
|
82
85
|
for (const line of stdout.split("\n")) {
|
|
83
86
|
if (!line.trim()) continue;
|