dsh-deeptutor 0.2.1 → 0.3.1

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/lib/index.js CHANGED
@@ -1,11 +1,9 @@
1
1
  import { defineTool } from "@deepseek-ai/dsh-tools";
2
- import { CAPABILITIES, configGuide, configHint, SSH_HOST, truncate } from "./config.js";
3
- import { cliNoteLocal, cliNoteRemote, runCli } from "./cli-exec.js";
4
- import { renderHtml } from "./html-render.js";
5
- import { cliRunTurn, fmtTurnResult } from "./turn.js";
6
2
  import { HTML_DOC_SCRIPT } from "./html-render.js";
3
+ import { TOOLS } from "./tools.js";
7
4
  export const name = "dsh-deeptutor";
8
5
  export const inject = ["tools"];
6
+ /** Drop undefined values so the canonical JSON value stays lossless-JSON. */
9
7
  function json(o) {
10
8
  const out = {};
11
9
  for (const [k, v] of Object.entries(o))
@@ -13,285 +11,25 @@ function json(o) {
13
11
  out[k] = v;
14
12
  return out;
15
13
  }
14
+ const OUTPUT = {
15
+ schema: {
16
+ type: "object",
17
+ properties: { text: { type: "string" } },
18
+ additionalProperties: true,
19
+ },
20
+ render: (_args, value) => [{ type: "text", text: value.text ?? "" }],
21
+ };
16
22
  export function apply(ctx) {
17
23
  console.log(`[dsh-deeptutor] plugin loaded (html script: ${HTML_DOC_SCRIPT})`);
18
- // ---------- Tool 1: run a DeepTutor learning capability ----------
19
- ctx.tools.register(defineTool({
20
- name: "deeptutor_run",
21
- description: "Execute one learning call on the DeepTutor service: deep_solve (in-depth explanation), deep_question (generate self-test questions), deep_research (deep research), chat (conversation), mastery_path (learning path planning), visualize / math_animator (visualization). Can mount knowledge bases (kbs) and tools (tools like rag / web_search). Returns the final answer plus session_id; pass session_id in later turns to continue the context (inherits chat history, knowledge bases and tools).",
22
- parameters: {
23
- capability: {
24
- type: "string",
25
- enum: [...CAPABILITIES],
26
- required: true,
27
- description: "The learning capability to run",
28
- },
29
- prompt: {
30
- type: "string",
31
- required: true,
32
- description: "The learning question or instruction for this turn (e.g. explain the async/await state machine)",
33
- },
34
- kbs: {
35
- type: "array",
36
- items: { type: "string" },
37
- description: "Knowledge base names (check deeptutor_kb list for available ones, e.g. the user's dotnet/sqlserver bases)",
38
- },
39
- tools: {
40
- type: "array",
41
- items: { type: "string" },
42
- description: "Tools to enable: rag (KB retrieval), web_search, reason, brainstorm, paper_search, code_execution, read_source, etc.",
43
- },
44
- session_id: {
45
- type: "string",
46
- description: "Session ID from a previous deeptutor_run call to continue that context",
47
- },
48
- config: {
49
- type: "object",
50
- additionalProperties: true,
51
- description: "Capability config key-values, e.g. num_questions=5 for deep_question, mode=report / depth=standard for deep_research",
52
- },
53
- language: {
54
- type: "string",
55
- description: "Response language code, default en; use zh for Chinese",
56
- },
57
- timeout: {
58
- type: "number",
59
- description: "Timeout in seconds, default 600; use 900+ for deep_research",
60
- },
61
- max_chars: {
62
- type: "number",
63
- description: "Max characters of the answer returned to the model, default 30000",
64
- },
65
- html: {
66
- type: "string",
67
- description: "Optional output path for a self-contained local HTML render of the answer (bundled md-to-html converter; also writes the companion .md source next to it). e.g. data/study/abstract-class/knowledge.html — open the HTML in a browser to read comfortably. Set it when the user wants browsable/printable study material.",
68
- },
69
- html_title: {
70
- type: "string",
71
- description: "HTML page title (default: derived from the html output file name)",
72
- },
73
- },
74
- output: {
75
- schema: {
76
- type: "object",
77
- properties: {
78
- mode: { type: "string" },
79
- sessionId: { type: "string" },
80
- answer: { type: "string" },
81
- toolCalls: { type: "array", items: { type: "string" } },
82
- errors: { type: "array", items: { type: "string" } },
83
- text: { type: "string" },
84
- },
85
- additionalProperties: true,
86
- },
87
- render: (_args, value) => [{ type: "text", text: value.text ?? "" }],
88
- },
89
- async execute(args, exec) {
90
- const timeoutMs = (args.timeout ?? 600) * 1000;
91
- const maxChars = args.max_chars ?? 30000;
92
- try {
93
- const r = await cliRunTurn(args, timeoutMs, exec.signal);
94
- const htmlNote = await renderHtml(args, r.answer);
95
- return json({
96
- mode: "cli",
97
- sessionId: r.sessionId,
98
- toolCalls: r.toolCalls,
99
- errors: r.errors,
100
- text: configHint() + fmtTurnResult(args.capability, r, maxChars) + htmlNote,
101
- });
102
- }
103
- catch (err) {
104
- if (exec.signal.aborted) {
105
- // user cancelled mid-CLI-run: report it as a cancellation, not a failure
106
- throw new Error("[deeptutor_run cancelled]");
107
- }
108
- throw new Error(configGuide() +
109
- `[deeptutor_run failed] ${err?.message ?? String(err)}`);
110
- }
111
- },
112
- }));
113
- // ---------- Tool 2: knowledge base operations ----------
114
- ctx.tools.register(defineTool({
115
- name: "deeptutor_kb",
116
- description: "Manage/search DeepTutor knowledge bases: list all bases, search inside a base (RAG hit snippets), info for details. Works in both local and remote deployments. Search the user's personal knowledge bases before generating learning content and combine the hits with authoritative sources.",
117
- parameters: {
118
- action: {
119
- type: "string",
120
- enum: ["list", "search", "info"],
121
- required: true,
122
- description: "What to do with the knowledge bases",
123
- },
124
- kb: {
125
- type: "string",
126
- description: "Knowledge base name (required for search/info)",
127
- },
128
- query: {
129
- type: "string",
130
- description: "Search question (required for search, e.g. C# generics covariance/contravariance)",
131
- },
132
- mode: {
133
- type: "string",
134
- enum: ["hybrid", "dense", "sparse"],
135
- description: "Retrieval mode, default hybrid",
136
- },
137
- top_k: {
138
- type: "number",
139
- description: "Number of hits to return, default 5 (applied locally to the CLI search result; the deeptutor CLI has no --top-k flag)",
140
- },
141
- max_chars: {
142
- type: "number",
143
- description: "Max characters of the returned content, default 30000",
144
- },
145
- },
146
- output: {
147
- schema: {
148
- type: "object",
149
- properties: {
150
- action: { type: "string" },
151
- mode: { type: "string" },
152
- kb: { type: "string" },
153
- query: { type: "string" },
154
- text: { type: "string" },
155
- },
156
- additionalProperties: true,
157
- },
158
- render: (_args, value) => [{ type: "text", text: value.text ?? "" }],
159
- },
160
- async execute(args, exec) {
161
- const maxChars = args.max_chars ?? 30000;
162
- // Local or SSH CLI. NOTE: `kb search` has no --top-k flag
163
- // (verified on deeptutor CLI 1.5.x); top_k is applied locally below.
164
- let argv;
165
- if (args.action === "list") {
166
- argv = ["kb", "list", "--format", "json"];
167
- }
168
- else if (args.action === "info") {
169
- argv = ["kb", "info", args.kb ?? ""];
170
- }
171
- else {
172
- argv = [
173
- "kb",
174
- "search",
175
- args.kb ?? "",
176
- args.query ?? "",
177
- "--format",
178
- "json",
179
- "--mode",
180
- args.mode ?? "hybrid",
181
- ];
182
- }
183
- const { stdout, stderr, code, killed } = await runCli(argv, {
184
- signal: exec.signal,
185
- timeoutMs: 90_000,
186
- });
187
- if (killed)
188
- throw new Error(configGuide() + "[deeptutor_kb timed out]");
189
- if (code !== 0) {
190
- throw new Error(`[deeptutor_kb failed] exit=${code}\nstderr: ${truncate(stderr || "(none)", 2000)}` +
191
- configGuide());
192
- }
193
- let body = stdout.trim() || "(empty result)";
194
- if (args.action === "search") {
195
- try {
196
- const parsed = JSON.parse(stdout);
197
- const fmtHit = (s, i) => {
198
- const text = s.text ?? s.content ?? s.snippet ?? JSON.stringify(s);
199
- const src = s.source ?? s.doc_name ?? s.file ?? "";
200
- const title = s.title ? ` title: ${s.title}` : "";
201
- const score = typeof s.score === "number" ? ` score: ${s.score}` : "";
202
- return `[${i + 1}]${src ? ` source: ${src}` : ""}${title}${score}\n${typeof text === "string" ? text : JSON.stringify(text)}`;
203
- };
204
- // Current deeptutor CLI (1.5.x) returns an object
205
- // { query, answer, content, sources, provider }; older builds
206
- // returned a bare array of hits. Accept both.
207
- if (Array.isArray(parsed)) {
208
- const hits = args.top_k ? parsed.slice(0, args.top_k) : parsed;
209
- body = `${hits.length} hits:\n\n` + hits.map(fmtHit).join("\n\n");
210
- }
211
- else if (parsed && typeof parsed === "object") {
212
- const parts = [];
213
- if (parsed.query)
214
- parts.push(`Query: ${parsed.query}`);
215
- if (typeof parsed.answer === "string" && parsed.answer.trim())
216
- parts.push(`Answer: ${parsed.answer.trim()}`);
217
- const sources = Array.isArray(parsed.sources) ? parsed.sources : [];
218
- const hits = args.top_k ? sources.slice(0, args.top_k) : sources;
219
- parts.push(`${hits.length} hits:`);
220
- parts.push(hits.map(fmtHit).join("\n\n"));
221
- body = parts.join("\n\n");
222
- }
223
- }
224
- catch {
225
- /* non-JSON output returned as-is */
226
- }
227
- }
228
- return json({
229
- action: args.action,
230
- mode: "cli",
231
- kb: args.kb,
232
- query: args.query,
233
- text: truncate(body, maxChars),
234
- });
235
- },
236
- }));
237
- // ---------- Tool 3: archive learning notes to a server notebook ----------
238
- ctx.tools.register(defineTool({
239
- name: "deeptutor_note",
240
- description: "Archive a Markdown learning note (study plan, deep-solve summary, wrong answers, etc.) to a DeepTutor notebook (auto-creates the notebook if missing). Record type: chat / question / research / solve.",
241
- parameters: {
242
- notebook: {
243
- type: "string",
244
- required: true,
245
- description: "Notebook name, e.g. dotnet-learning (auto-created if missing)",
246
- },
247
- title: {
248
- type: "string",
249
- required: true,
250
- description: "Record title, e.g. async/await state machine deep-dive notes",
251
- },
252
- type: {
253
- type: "string",
254
- enum: ["chat", "question", "research", "solve"],
255
- description: "Record type, default chat",
256
- },
257
- content: {
258
- type: "string",
259
- required: true,
260
- description: "Learning note content in Markdown",
261
- },
262
- },
263
- output: {
264
- schema: {
265
- type: "object",
266
- properties: {
267
- notebook: { type: "string" },
268
- nbId: { type: "string" },
269
- title: { type: "string" },
270
- mode: { type: "string" },
271
- text: { type: "string" },
272
- },
273
- additionalProperties: true,
274
- },
275
- render: (_args, value) => [{ type: "text", text: value.text ?? "" }],
276
- },
277
- async execute(args, exec) {
278
- const recordType = (args.type ?? "chat");
279
- const nbName = args.notebook.trim();
280
- // Remote deployment runs the PY_NOTE script on the server; local deployment runs locally
281
- const { stdout, stderr, code } = SSH_HOST
282
- ? await cliNoteRemote(nbName, args.title, recordType, args.content, exec.signal)
283
- : await cliNoteLocal(nbName, args.title, recordType, args.content, exec.signal);
284
- if (code !== 0) {
285
- throw new Error(`[deeptutor_note failed] exit=${code}\n${truncate(stderr || stdout, 2000)}` +
286
- configGuide());
287
- }
288
- return json({
289
- notebook: nbName,
290
- title: args.title,
291
- mode: "cli",
292
- text: configHint() +
293
- `[note archived] notebook=${nbName}, title=${args.title}, type=${recordType}\n${truncate((stdout ?? "").trim(), 2000)}`,
294
- });
295
- },
296
- }));
24
+ for (const tool of TOOLS) {
25
+ ctx.tools.register(defineTool({
26
+ name: tool.name,
27
+ description: tool.description,
28
+ parameters: tool.parameters,
29
+ output: OUTPUT,
30
+ async execute(args, exec) {
31
+ return json(await tool.execute(args, exec.signal));
32
+ },
33
+ }));
34
+ }
297
35
  }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Entry point of the `dsh-deeptutor-mcp` bin: the DeepTutor tools as an MCP
4
+ * stdio server (Claude Code, Codex and other MCP clients).
5
+ */
6
+ import { startStdioServer } from "./mcp.js";
7
+ startStdioServer();
package/lib/mcp.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ import { type ToolSpec } from "./tools.ts";
2
+ type Id = string | number;
3
+ interface Message {
4
+ jsonrpc?: string;
5
+ id?: Id | null;
6
+ method?: string;
7
+ params?: any;
8
+ }
9
+ export interface McpServerOptions {
10
+ tools?: readonly ToolSpec[];
11
+ /** Writes one serialized message (without the trailing newline). */
12
+ send: (line: string) => void;
13
+ log?: (msg: string) => void;
14
+ }
15
+ /**
16
+ * Transport-free MCP message handler. `handle` takes one parsed message and
17
+ * resolves once any response has been sent. Exported for tests.
18
+ */
19
+ export declare function createMcpHandler(opts: McpServerOptions): {
20
+ handle: (msg: Message) => Promise<void>;
21
+ inflight: Map<Id, AbortController>;
22
+ };
23
+ /** Run the server on this process's stdin/stdout until stdin closes. */
24
+ export declare function startStdioServer(): void;
25
+ export {};
package/lib/mcp.js ADDED
@@ -0,0 +1,133 @@
1
+ /**
2
+ * DeepTutor bridge: Model Context Protocol server over stdio.
3
+ *
4
+ * Serves the same tools as the dsh plugin (src/tools.ts) to MCP clients such as
5
+ * Claude Code. Hand-rolled on purpose: the protocol surface a tools-only server
6
+ * needs is small (initialize, ping, tools/list, tools/call, cancellation), and
7
+ * keeping it in-house preserves the package's zero runtime dependencies.
8
+ *
9
+ * Transport: newline-delimited JSON-RPC 2.0 on stdin/stdout. stdout carries
10
+ * protocol messages only; diagnostics go to stderr.
11
+ */
12
+ import { readFileSync } from "node:fs";
13
+ import { createInterface } from "node:readline";
14
+ import { TOOLS, toJsonSchema, validateArgs } from "./tools.js";
15
+ const SUPPORTED_PROTOCOLS = ["2024-11-05", "2025-03-26", "2025-06-18"];
16
+ const LATEST_PROTOCOL = SUPPORTED_PROTOCOLS[SUPPORTED_PROTOCOLS.length - 1];
17
+ const INSTRUCTIONS = "DeepTutor learning tools. Explain topics with deeptutor_run (deep_solve, deep_question, …), " +
18
+ "search the user's knowledge bases with deeptutor_kb before answering from memory, archive notes " +
19
+ "with deeptutor_note only on request, and reach every other DeepTutor CLI command through " +
20
+ "deeptutor_cli, discovering commands with args=[\"--help\"] rather than guessing.";
21
+ function packageVersion() {
22
+ try {
23
+ const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
24
+ return typeof pkg.version === "string" ? pkg.version : "0.0.0";
25
+ }
26
+ catch {
27
+ return "0.0.0";
28
+ }
29
+ }
30
+ /**
31
+ * Transport-free MCP message handler. `handle` takes one parsed message and
32
+ * resolves once any response has been sent. Exported for tests.
33
+ */
34
+ export function createMcpHandler(opts) {
35
+ const tools = opts.tools ?? TOOLS;
36
+ const byName = new Map(tools.map((t) => [t.name, t]));
37
+ const inflight = new Map();
38
+ const log = opts.log ?? (() => { });
39
+ const reply = (id, result) => opts.send(JSON.stringify({ jsonrpc: "2.0", id, result }));
40
+ const fail = (id, code, message) => opts.send(JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } }));
41
+ async function callTool(id, params) {
42
+ const tool = byName.get(params?.name);
43
+ if (!tool)
44
+ return fail(id, -32602, `Unknown tool: ${String(params?.name)}`);
45
+ const args = params?.arguments && typeof params.arguments === "object" ? params.arguments : {};
46
+ const problems = validateArgs(tool.parameters, args);
47
+ if (problems.length) {
48
+ return reply(id, { content: [{ type: "text", text: `[${tool.name}] ${problems.join("; ")}` }], isError: true });
49
+ }
50
+ const ac = new AbortController();
51
+ inflight.set(id, ac);
52
+ try {
53
+ const { text, ...data } = await tool.execute(args, ac.signal);
54
+ const structured = Object.fromEntries(Object.entries(data).filter(([, v]) => v !== undefined));
55
+ if (ac.signal.aborted)
56
+ return; // cancelled: the client no longer expects a response
57
+ reply(id, { content: [{ type: "text", text }], structuredContent: { text, ...structured } });
58
+ }
59
+ catch (err) {
60
+ if (ac.signal.aborted)
61
+ return;
62
+ reply(id, { content: [{ type: "text", text: err?.message ?? String(err) }], isError: true });
63
+ }
64
+ finally {
65
+ inflight.delete(id);
66
+ }
67
+ }
68
+ async function handle(msg) {
69
+ if (!msg || typeof msg !== "object" || typeof msg.method !== "string") {
70
+ // Responses to requests we never send, or garbage: ignore.
71
+ return;
72
+ }
73
+ const isRequest = msg.id !== undefined && msg.id !== null;
74
+ if (!isRequest) {
75
+ if (msg.method === "notifications/cancelled")
76
+ inflight.get(msg.params?.requestId)?.abort();
77
+ return;
78
+ }
79
+ const id = msg.id;
80
+ switch (msg.method) {
81
+ case "initialize": {
82
+ const asked = msg.params?.protocolVersion;
83
+ reply(id, {
84
+ protocolVersion: SUPPORTED_PROTOCOLS.includes(asked) ? asked : LATEST_PROTOCOL,
85
+ capabilities: { tools: { listChanged: false } },
86
+ serverInfo: { name: "dsh-deeptutor", version: packageVersion() },
87
+ instructions: INSTRUCTIONS,
88
+ });
89
+ return;
90
+ }
91
+ case "ping":
92
+ reply(id, {});
93
+ return;
94
+ case "tools/list":
95
+ reply(id, {
96
+ tools: tools.map((t) => ({
97
+ name: t.name,
98
+ description: t.description,
99
+ inputSchema: toJsonSchema(t.parameters),
100
+ })),
101
+ });
102
+ return;
103
+ case "tools/call":
104
+ await callTool(id, msg.params);
105
+ return;
106
+ default:
107
+ log(`unsupported method: ${msg.method}`);
108
+ fail(id, -32601, `Method not found: ${msg.method}`);
109
+ }
110
+ }
111
+ return { handle, inflight };
112
+ }
113
+ /** Run the server on this process's stdin/stdout until stdin closes. */
114
+ export function startStdioServer() {
115
+ const log = (m) => process.stderr.write(`[dsh-deeptutor mcp] ${m}\n`);
116
+ const { handle } = createMcpHandler({ send: (line) => process.stdout.write(line + "\n"), log });
117
+ const rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
118
+ rl.on("line", (line) => {
119
+ if (!line.trim())
120
+ return;
121
+ let msg;
122
+ try {
123
+ msg = JSON.parse(line);
124
+ }
125
+ catch {
126
+ process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } }) + "\n");
127
+ return;
128
+ }
129
+ handle(msg).catch((err) => log(`handler error: ${err?.stack ?? err}`));
130
+ });
131
+ rl.on("close", () => process.exit(0));
132
+ log(`ready (v${packageVersion()}, ${process.env.DEEPTUTOR_SSH_HOST ? `remote via ${process.env.DEEPTUTOR_SSH_HOST}` : "local CLI"})`);
133
+ }
package/lib/tools.d.ts ADDED
@@ -0,0 +1,52 @@
1
+ interface Base {
2
+ description: string;
3
+ required?: true;
4
+ }
5
+ export type ParamSpec = (Base & {
6
+ type: "string";
7
+ enum?: readonly string[];
8
+ }) | (Base & {
9
+ type: "number";
10
+ }) | (Base & {
11
+ type: "boolean";
12
+ }) | (Base & {
13
+ type: "array";
14
+ items: {
15
+ type: "string";
16
+ };
17
+ }) | (Base & {
18
+ type: "object";
19
+ additionalProperties: true;
20
+ });
21
+ export type ParamsSpec = Record<string, ParamSpec>;
22
+ export interface ToolResultData {
23
+ /** Text shown to the model. */
24
+ text: string;
25
+ /** Structured extras (session id, tool calls…); undefined values are dropped. */
26
+ [key: string]: unknown;
27
+ }
28
+ export interface ToolSpec {
29
+ name: string;
30
+ description: string;
31
+ parameters: ParamsSpec;
32
+ /** Throws an Error whose message is shown to the model on failure. */
33
+ execute(args: Record<string, any>, signal: AbortSignal): Promise<ToolResultData>;
34
+ }
35
+ /** Plain JSON Schema (object) for one tool's parameters. Pure. */
36
+ export declare function toJsonSchema(params: ParamsSpec): {
37
+ type: "object";
38
+ properties: Record<string, unknown>;
39
+ required: string[];
40
+ additionalProperties: false;
41
+ };
42
+ /** Required-parameter and basic type check (MCP clients may skip schema validation). Pure. */
43
+ export declare function validateArgs(params: ParamsSpec, args: Record<string, unknown>): string[];
44
+ /** Format `kb search --format json` output; undefined when it is not JSON. Exported for tests. */
45
+ export declare function formatSearch(stdout: string, topK?: number): string | undefined;
46
+ /**
47
+ * Rich pads every line to the terminal width; drop that padding and blank-line
48
+ * runs so wide output does not waste the model's context. Pure; exported for tests.
49
+ */
50
+ export declare function tidyOutput(s: string): string;
51
+ export declare const TOOLS: readonly ToolSpec[];
52
+ export {};