dsh-deeptutor 0.1.5 → 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.
@@ -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 >= 22).
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 === "--out") opts.out = argv[++i];
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
- const templatePath = path.join(import.meta.dirname, "..", "assets", "template.html");
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
- opts.signal?.addEventListener("abort", onAbort, { once: true });
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)}`,
@@ -200,7 +204,8 @@ export function apply(ctx: Context) {
200
204
  },
201
205
  top_k: {
202
206
  type: "number",
203
- description: "Number of hits to return, default 5",
207
+ description:
208
+ "Number of hits to return, default 5 (applied locally to the CLI search result; the deeptutor CLI has no --top-k flag)",
204
209
  },
205
210
  max_chars: {
206
211
  type: "number",
@@ -267,7 +272,8 @@ export function apply(ctx: Context) {
267
272
  );
268
273
  }
269
274
  }
270
- // CLI fallback (local or SSH)
275
+ // CLI fallback (local or SSH). NOTE: `kb search` has no --top-k flag
276
+ // (verified on deeptutor CLI 1.5.x); top_k is applied locally below.
271
277
  let argv: string[];
272
278
  if (args.action === "list") {
273
279
  argv = ["kb", "list", "--format", "json"];
@@ -284,7 +290,6 @@ export function apply(ctx: Context) {
284
290
  "--mode",
285
291
  args.mode ?? "hybrid",
286
292
  ];
287
- if (args.top_k) argv.push("--top-k", String(args.top_k));
288
293
  }
289
294
  const { stdout, stderr, code, killed } = await runCli(argv, {
290
295
  signal: exec.signal,
@@ -300,16 +305,30 @@ export function apply(ctx: Context) {
300
305
  let body = stdout.trim() || "(empty result)";
301
306
  if (args.action === "search") {
302
307
  try {
303
- const arr = JSON.parse(stdout);
304
- if (Array.isArray(arr)) {
305
- body = arr
306
- .map((s: any, i: number) => {
307
- const text = s.text ?? s.content ?? s.snippet ?? JSON.stringify(s);
308
- const src = s.source ?? s.doc_name ?? s.file ?? "";
309
- return `[${i + 1}]${src ? ` source: ${src}` : ""}\n${typeof text === "string" ? text : JSON.stringify(text)}`;
310
- })
311
- .join("\n\n");
312
- body = `${arr.length} hits:\n\n` + body;
308
+ const parsed = JSON.parse(stdout);
309
+ const fmtHit = (s: any, i: number) => {
310
+ const text = s.text ?? s.content ?? s.snippet ?? JSON.stringify(s);
311
+ const src = s.source ?? s.doc_name ?? s.file ?? "";
312
+ const title = s.title ? ` title: ${s.title}` : "";
313
+ const score = typeof s.score === "number" ? ` score: ${s.score}` : "";
314
+ return `[${i + 1}]${src ? ` source: ${src}` : ""}${title}${score}\n${typeof text === "string" ? text : JSON.stringify(text)}`;
315
+ };
316
+ // Current deeptutor CLI (1.5.x) returns an object
317
+ // { query, answer, content, sources, provider }; older builds
318
+ // returned a bare array of hits. Accept both.
319
+ if (Array.isArray(parsed)) {
320
+ const hits = args.top_k ? parsed.slice(0, args.top_k) : parsed;
321
+ body = `${hits.length} hits:\n\n` + hits.map(fmtHit).join("\n\n");
322
+ } else if (parsed && typeof parsed === "object") {
323
+ const parts: string[] = [];
324
+ if (parsed.query) parts.push(`Query: ${parsed.query}`);
325
+ if (typeof parsed.answer === "string" && parsed.answer.trim())
326
+ parts.push(`Answer: ${parsed.answer.trim()}`);
327
+ const sources = Array.isArray(parsed.sources) ? parsed.sources : [];
328
+ const hits = args.top_k ? sources.slice(0, args.top_k) : sources;
329
+ parts.push(`${hits.length} hits:`);
330
+ parts.push(hits.map(fmtHit).join("\n\n"));
331
+ body = parts.join("\n\n");
313
332
  }
314
333
  } catch {
315
334
  /* non-JSON output returned as-is */
package/src/turn.ts CHANGED
@@ -128,21 +128,44 @@ export function wsRunTurn(
128
128
  });
129
129
  }
130
130
 
131
- /** Execute one turn via the CLI (local or SSH). */
132
- export async function cliRunTurn(
133
- params: RunParams,
134
- timeoutMs: number,
135
- signal?: AbortSignal,
136
- ): Promise<TurnResult> {
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
- for (const [k, v] of Object.entries(params.config ?? {}))
143
- argv.push("--config", `${k}=${typeof v === "object" ? JSON.stringify(v) : v}`);
144
- const { stdout, stderr, code, killed } = await runCli(argv, { signal, timeoutMs });
145
- if (killed) throw new Error(`CLI execution timed out (${Math.round(timeoutMs / 1000)}s)`);
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")) {