dsh-deeptutor 0.1.6 → 0.1.7
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/README.md +4 -0
- package/README.zh.md +2 -0
- package/lib/config.js +1 -0
- package/lib/http-api.js +18 -1
- package/lib/index.js +4 -0
- package/lib/turn.d.ts +10 -2
- package/lib/turn.js +27 -7
- package/package.json +6 -2
- package/scripts/md-to-html.js +35 -7
- package/scripts/sync-skills.mjs +46 -0
- package/skills/deeptutor/README.md +37 -14
- package/skills/deeptutor/SKILL.md +12 -11
- package/skills/html-doc/SKILL.md +2 -2
- package/skills/html-doc/assets/template.html +420 -420
- package/skills/html-doc/scripts/md-to-html.js +35 -7
- package/src/config.ts +1 -0
- package/src/http-api.ts +16 -1
- package/src/index.ts +4 -0
- package/src/turn.ts +33 -10
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Features: headings, fenced code blocks, inline code/bold/italic/links, lists,
|
|
11
11
|
* task lists, blockquotes, pipe tables, hr, quiz answer collapsing (<details>),
|
|
12
|
-
* raw HTML passthrough, HTML escaping. Zero npm dependencies (Node >=
|
|
12
|
+
* raw HTML passthrough, HTML escaping. Zero npm dependencies (Node >= 20.11 — uses import.meta.dirname).
|
|
13
13
|
*/
|
|
14
14
|
"use strict";
|
|
15
15
|
|
|
@@ -29,22 +29,41 @@ function parseArgs(argv) {
|
|
|
29
29
|
collapse: true,
|
|
30
30
|
};
|
|
31
31
|
const positional = [];
|
|
32
|
+
const valueFlags = new Set(["--out", "--title", "--theme", "--lang"]);
|
|
32
33
|
for (let i = 0; i < argv.length; i++) {
|
|
33
34
|
const a = argv[i];
|
|
34
|
-
if (a === "--
|
|
35
|
-
else if (a === "--title") opts.title = argv[++i];
|
|
36
|
-
else if (a === "--theme") opts.theme = argv[++i] === "dark" ? "dark" : "light";
|
|
37
|
-
else if (a === "--toc") opts.toc = true;
|
|
38
|
-
else if (a === "--lang") opts.lang = argv[++i];
|
|
35
|
+
if (a === "--toc") opts.toc = true;
|
|
39
36
|
else if (a === "--no-collapse") opts.collapse = false;
|
|
40
37
|
else if (a === "--help" || a === "-h") {
|
|
41
38
|
printHelp();
|
|
42
39
|
process.exit(0);
|
|
40
|
+
} else if (valueFlags.has(a)) {
|
|
41
|
+
const value = argv[i + 1];
|
|
42
|
+
if (value === undefined || value.startsWith("--")) {
|
|
43
|
+
fail(`Missing value for ${a} (e.g. --out out.html)`);
|
|
44
|
+
}
|
|
45
|
+
i++;
|
|
46
|
+
if (a === "--out") opts.out = value;
|
|
47
|
+
else if (a === "--title") opts.title = value;
|
|
48
|
+
else if (a === "--theme") {
|
|
49
|
+
if (value !== "dark" && value !== "light") {
|
|
50
|
+
fail(`Invalid --theme value "${value}": expected dark or light`);
|
|
51
|
+
}
|
|
52
|
+
opts.theme = value;
|
|
53
|
+
} else if (a === "--lang") opts.lang = value;
|
|
54
|
+
} else if (a.startsWith("--")) {
|
|
55
|
+
fail(`Unknown option: ${a} (run with --help for usage)`);
|
|
43
56
|
} else positional.push(a);
|
|
44
57
|
}
|
|
45
58
|
return { opts, positional };
|
|
46
59
|
}
|
|
47
60
|
|
|
61
|
+
/** Print an error to stderr and exit non-zero (CLI contract: never silently swallow bad flags). */
|
|
62
|
+
function fail(message) {
|
|
63
|
+
console.error(`[html-doc] ${message}`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
48
67
|
function printHelp() {
|
|
49
68
|
console.log(`md-to-html.js — Markdown → self-contained HTML
|
|
50
69
|
|
|
@@ -330,7 +349,16 @@ function main() {
|
|
|
330
349
|
: "";
|
|
331
350
|
|
|
332
351
|
const generated = new Date().toISOString().slice(0, 10);
|
|
333
|
-
|
|
352
|
+
// The same converter ships in two locations (skills/html-doc/scripts/ and a
|
|
353
|
+
// package-root scripts/ copy); resolve the template wherever we are.
|
|
354
|
+
const templateCandidates = [
|
|
355
|
+
path.join(import.meta.dirname, "..", "assets", "template.html"),
|
|
356
|
+
path.join(import.meta.dirname, "..", "skills", "html-doc", "assets", "template.html"),
|
|
357
|
+
];
|
|
358
|
+
const templatePath = templateCandidates.find((p) => fs.existsSync(p));
|
|
359
|
+
if (!templatePath) {
|
|
360
|
+
fail(`template.html not found (looked in: ${templateCandidates.join(", ")})`);
|
|
361
|
+
}
|
|
334
362
|
let template = fs.readFileSync(templatePath, "utf8");
|
|
335
363
|
template = template
|
|
336
364
|
.replace(/\{\{TITLE\}\}/g, esc(title))
|
package/src/config.ts
CHANGED
|
@@ -52,6 +52,7 @@ export function configGuide(): string {
|
|
|
52
52
|
' export DEEPTUTOR_REMOTE_HOME="remote-DEEPTUTOR_HOME-workspace"\n' +
|
|
53
53
|
" Verify serve is running on the server (ssh to it: curl http://127.0.0.1:8001/api/v1/system/status)\n" +
|
|
54
54
|
" Verify passwordless SSH login works (ssh alias + BatchMode)\n" +
|
|
55
|
+
'PowerShell users: use $env:VAR="value" instead of export VAR=value.\n' +
|
|
55
56
|
"Restart the agent after configuring.\n"
|
|
56
57
|
);
|
|
57
58
|
}
|
package/src/http-api.ts
CHANGED
|
@@ -31,6 +31,7 @@ export async function apiProbe(timeoutMs = 1500): Promise<boolean> {
|
|
|
31
31
|
export async function ensureApi(): Promise<boolean> {
|
|
32
32
|
if (await apiProbe(1200)) return true;
|
|
33
33
|
if (!SSH_HOST) return false; // Local deployment: fall back to local CLI when serve is down, no tunnel needed
|
|
34
|
+
let tunnelDead = false;
|
|
34
35
|
if (!tunnelProc || tunnelProc.exitCode !== null) {
|
|
35
36
|
tunnelProc = spawn(
|
|
36
37
|
"ssh",
|
|
@@ -52,9 +53,16 @@ export async function ensureApi(): Promise<boolean> {
|
|
|
52
53
|
],
|
|
53
54
|
{ stdio: "ignore", windowsHide: true },
|
|
54
55
|
);
|
|
56
|
+
// Fast-fail: a tunnel that exits immediately (bad alias, port in use,
|
|
57
|
+
// refused connection) is not going to recover — stop waiting for it.
|
|
58
|
+
tunnelProc.once("exit", () => {
|
|
59
|
+
tunnelDead = true;
|
|
60
|
+
tunnelProc = null;
|
|
61
|
+
});
|
|
55
62
|
tunnelProc.unref();
|
|
56
63
|
}
|
|
57
64
|
for (let i = 0; i < 20; i++) {
|
|
65
|
+
if (tunnelDead) return false;
|
|
58
66
|
if (await apiProbe(1000)) return true;
|
|
59
67
|
await new Promise((r) => setTimeout(r, 500));
|
|
60
68
|
}
|
|
@@ -70,7 +78,14 @@ export async function apiJson(
|
|
|
70
78
|
const controller = new AbortController();
|
|
71
79
|
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 60_000);
|
|
72
80
|
const onAbort = () => controller.abort();
|
|
73
|
-
|
|
81
|
+
// An already-aborted signal never fires its "abort" event again — check the
|
|
82
|
+
// flag first so a pre-aborted call fails immediately instead of running to
|
|
83
|
+
// completion against a still-open controller.
|
|
84
|
+
if (opts.signal?.aborted) {
|
|
85
|
+
controller.abort();
|
|
86
|
+
} else {
|
|
87
|
+
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
88
|
+
}
|
|
74
89
|
try {
|
|
75
90
|
const res = await fetch(`${API_BASE}${path}`, { ...init, signal: controller.signal });
|
|
76
91
|
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
package/src/index.ts
CHANGED
|
@@ -162,6 +162,10 @@ export function apply(ctx: Context) {
|
|
|
162
162
|
configHint() + fmtTurnResult(args.capability, r, maxChars) + htmlNote,
|
|
163
163
|
});
|
|
164
164
|
} catch (err: any) {
|
|
165
|
+
if (exec.signal.aborted) {
|
|
166
|
+
// user cancelled mid-CLI-run: report it as a cancellation, not a failure
|
|
167
|
+
throw new Error("[deeptutor_run cancelled]");
|
|
168
|
+
}
|
|
165
169
|
throw new Error(
|
|
166
170
|
configGuide() +
|
|
167
171
|
`[deeptutor_run failed (HTTP and CLI both unavailable)] ${err?.message ?? String(err)}`,
|
package/src/turn.ts
CHANGED
|
@@ -128,21 +128,44 @@ export function wsRunTurn(
|
|
|
128
128
|
});
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
/**
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
131
|
+
/**
|
|
132
|
+
* Build the `deeptutor run` argv for a capability turn (pure, unit-tested).
|
|
133
|
+
* Scalar config values go as repeatable --config k=v; object/array values are
|
|
134
|
+
* collected into --config-json (the CLI's documented form for complex config)
|
|
135
|
+
* instead of being stringified into a k=<json> value that no parser expects.
|
|
136
|
+
*/
|
|
137
|
+
export function buildRunArgs(params: RunParams): string[] {
|
|
137
138
|
const argv = ["run", params.capability, params.prompt, "--format", "json"];
|
|
138
139
|
for (const kb of params.kbs ?? []) argv.push("--kb", kb);
|
|
139
140
|
for (const t of params.tools ?? []) argv.push("--tool", t);
|
|
140
141
|
if (params.session_id) argv.push("--session", params.session_id);
|
|
141
142
|
if (params.language) argv.push("--language", params.language);
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
143
|
+
const complexConfig: Record<string, unknown> = {};
|
|
144
|
+
for (const [k, v] of Object.entries(params.config ?? {})) {
|
|
145
|
+
if (typeof v === "object" && v !== null) complexConfig[k] = v;
|
|
146
|
+
else argv.push("--config", `${k}=${v}`);
|
|
147
|
+
}
|
|
148
|
+
if (Object.keys(complexConfig).length > 0) {
|
|
149
|
+
argv.push("--config-json", JSON.stringify(complexConfig));
|
|
150
|
+
}
|
|
151
|
+
return argv;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Execute one turn via the CLI (local or SSH). `run` is injectable for tests. */
|
|
155
|
+
export async function cliRunTurn(
|
|
156
|
+
params: RunParams,
|
|
157
|
+
timeoutMs: number,
|
|
158
|
+
signal?: AbortSignal,
|
|
159
|
+
run: typeof runCli = runCli,
|
|
160
|
+
): Promise<TurnResult> {
|
|
161
|
+
const { stdout, stderr, code, killed } = await run(buildRunArgs(params), { signal, timeoutMs });
|
|
162
|
+
if (killed) {
|
|
163
|
+
// A kill can come from the user cancelling (signal) or from the timeout;
|
|
164
|
+
// report the real reason instead of always blaming the timeout.
|
|
165
|
+
throw new Error(
|
|
166
|
+
signal?.aborted ? "Aborted" : `CLI execution timed out (${Math.round(timeoutMs / 1000)}s)`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
146
169
|
if (code !== 0) throw new Error(`CLI exit=${code}: ${stderr || "(no stderr)"}`);
|
|
147
170
|
const state: TurnResult = { answer: "", errors: [], toolCalls: [] };
|
|
148
171
|
for (const line of stdout.split("\n")) {
|