dsh-deeptutor 0.2.0 → 0.3.0
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/README.md +17 -6
- package/.agents/skills/deeptutor/SKILL.md +91 -126
- package/.agents/skills/html-doc/scripts/md-to-html.js +16 -7
- package/README.md +86 -19
- package/README.zh.md +56 -10
- package/lib/cli-exec.d.ts +10 -4
- package/lib/cli-exec.js +9 -1
- package/lib/cli-policy.d.ts +23 -0
- package/lib/cli-policy.js +86 -0
- package/lib/config.d.ts +0 -1
- package/lib/config.js +0 -9
- package/lib/discovery.d.ts +45 -0
- package/lib/discovery.js +96 -0
- package/lib/index.d.ts +4 -3
- package/lib/index.js +21 -283
- package/lib/mcp-main.d.ts +2 -0
- package/lib/mcp-main.js +7 -0
- package/lib/mcp.d.ts +25 -0
- package/lib/mcp.js +133 -0
- package/lib/tools.d.ts +52 -0
- package/lib/tools.js +304 -0
- package/package.json +9 -5
- package/scripts/check-cli.ts +80 -0
- package/scripts/mcp-launch.mjs +68 -0
- package/scripts/md-to-html.js +16 -7
- package/src/cli-exec.ts +21 -9
- package/src/cli-policy.ts +99 -0
- package/src/config.ts +0 -10
- package/src/discovery.ts +112 -0
- package/src/index.ts +30 -317
- package/src/mcp-main.ts +8 -0
- package/src/mcp.ts +151 -0
- package/src/tools.ts +349 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepTutor bridge: safety tiers for the generic `deeptutor_cli` passthrough.
|
|
3
|
+
*
|
|
4
|
+
* The passthrough exists so the plugin never has to mirror DeepTutor's command
|
|
5
|
+
* surface: whatever the installed CLI offers is reachable, and the agent learns
|
|
6
|
+
* it from `--help`. The price is that the plugin cannot know what a command it
|
|
7
|
+
* has never seen does, so the default for anything unrecognised is "needs the
|
|
8
|
+
* user's explicit go-ahead" rather than "run".
|
|
9
|
+
*
|
|
10
|
+
* read run right away: help, and commands whose verb only reads
|
|
11
|
+
* confirm run only with confirm=true (writes, and every verb not listed here)
|
|
12
|
+
* blocked never run: destructive verbs, and interactive / long-running
|
|
13
|
+
* commands that would hang a non-interactive tool call
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export type CliTier = "read" | "confirm" | "blocked";
|
|
17
|
+
|
|
18
|
+
export interface CliVerdict {
|
|
19
|
+
tier: CliTier;
|
|
20
|
+
reason: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Top-level commands that start a REPL, a server, a launcher or a setup wizard. */
|
|
24
|
+
const INTERACTIVE_TOP = new Set(["chat", "start", "stop", "serve", "init"]);
|
|
25
|
+
|
|
26
|
+
/** Verbs that only read. Matched against the subcommand (or a single-command top level). */
|
|
27
|
+
const READ_VERBS = new Set([
|
|
28
|
+
"list",
|
|
29
|
+
"show",
|
|
30
|
+
"info",
|
|
31
|
+
"search",
|
|
32
|
+
"state",
|
|
33
|
+
"status",
|
|
34
|
+
"doctor",
|
|
35
|
+
"get",
|
|
36
|
+
"view",
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
/** Verbs that delete, reset or roll back state, or need an interactive login. */
|
|
40
|
+
const BLOCKED_VERB =
|
|
41
|
+
/^(delete|remove|remove-.+|clear|uninstall|rollback|purge|reset|drop|wipe|prune|login|logout)$/;
|
|
42
|
+
|
|
43
|
+
/** Commands the plugin's own tools run without confirmation; same rule here. */
|
|
44
|
+
const READ_TOP = new Set(["doctor", "run"]);
|
|
45
|
+
|
|
46
|
+
/** Leading positional words (the command path), stopping at the first option. */
|
|
47
|
+
export function commandPath(args: readonly string[]): string[] {
|
|
48
|
+
const path: string[] = [];
|
|
49
|
+
for (const a of args) {
|
|
50
|
+
if (a.startsWith("-")) break;
|
|
51
|
+
path.push(a);
|
|
52
|
+
}
|
|
53
|
+
return path;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Classify one `deeptutor <args>` invocation. Pure; unit-tested. */
|
|
57
|
+
export function classifyCliArgs(args: readonly string[]): CliVerdict {
|
|
58
|
+
if (args.length === 0) {
|
|
59
|
+
return { tier: "read", reason: "no arguments prints the top-level help" };
|
|
60
|
+
}
|
|
61
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
62
|
+
return { tier: "read", reason: "--help only prints usage" };
|
|
63
|
+
}
|
|
64
|
+
const path = commandPath(args);
|
|
65
|
+
const [top, sub] = path;
|
|
66
|
+
if (!top) {
|
|
67
|
+
return { tier: "confirm", reason: "options without a command" };
|
|
68
|
+
}
|
|
69
|
+
if (INTERACTIVE_TOP.has(top)) {
|
|
70
|
+
return {
|
|
71
|
+
tier: "blocked",
|
|
72
|
+
reason: `\`deeptutor ${top}\` is interactive or long-running and would hang a tool call; run it in a terminal yourself`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
for (const word of [top, sub]) {
|
|
76
|
+
if (word && BLOCKED_VERB.test(word)) {
|
|
77
|
+
return {
|
|
78
|
+
tier: "blocked",
|
|
79
|
+
reason: `\`${word}\` deletes, resets or logs in; run it in a terminal yourself if you really mean it`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (READ_TOP.has(top)) {
|
|
84
|
+
return { tier: "read", reason: `\`deeptutor ${top}\` is a read-only or learning command` };
|
|
85
|
+
}
|
|
86
|
+
if (sub && (READ_VERBS.has(sub) || sub.startsWith("list-"))) {
|
|
87
|
+
return { tier: "read", reason: `\`${sub}\` only reads` };
|
|
88
|
+
}
|
|
89
|
+
if (!sub && path.length === 1) {
|
|
90
|
+
return {
|
|
91
|
+
tier: "confirm",
|
|
92
|
+
reason: `\`deeptutor ${top}\` without a subcommand; use --help to see what it does`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
tier: "confirm",
|
|
97
|
+
reason: `\`${[top, sub].filter(Boolean).join(" ")}\` may change DeepTutor state`,
|
|
98
|
+
};
|
|
99
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -56,15 +56,5 @@ export function configGuide(): string {
|
|
|
56
56
|
);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
export const CAPABILITIES = [
|
|
60
|
-
"chat",
|
|
61
|
-
"deep_solve",
|
|
62
|
-
"deep_question",
|
|
63
|
-
"deep_research",
|
|
64
|
-
"visualize",
|
|
65
|
-
"math_animator",
|
|
66
|
-
"mastery_path",
|
|
67
|
-
] as const;
|
|
68
|
-
|
|
69
59
|
export const truncate = (s: string, n: number) =>
|
|
70
60
|
s.length > n ? s.slice(0, n) + `\n…[truncated, ${s.length} chars total]` : s;
|
package/src/discovery.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepTutor bridge: runtime discovery of the capabilities a deployment offers.
|
|
3
|
+
*
|
|
4
|
+
* DeepTutor 1.7 ships capabilities as optional modules, so the set differs per
|
|
5
|
+
* deployment and grows with every release. Instead of a hard-coded enum the
|
|
6
|
+
* plugin asks `deeptutor module list` (JSON) which modules are ready and what
|
|
7
|
+
* capabilities they provide, and caches the answer for a few minutes.
|
|
8
|
+
*
|
|
9
|
+
* Discovery is advisory: a capability that no module lists may still come from a
|
|
10
|
+
* DeepTutor plugin, so an unknown name is passed through to the CLI. Only a
|
|
11
|
+
* capability whose module is known but not ready is refused up front, because
|
|
12
|
+
* that run is certain to fail and the reason is worth telling the agent.
|
|
13
|
+
*/
|
|
14
|
+
import { runCli, type CliResult } from "./cli-exec.ts";
|
|
15
|
+
|
|
16
|
+
export interface ModuleInfo {
|
|
17
|
+
id: string;
|
|
18
|
+
ready: boolean;
|
|
19
|
+
capabilities: string[];
|
|
20
|
+
issues: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CapabilityIndex {
|
|
24
|
+
/** Capabilities whose module is installed, enabled and ready. */
|
|
25
|
+
ready: string[];
|
|
26
|
+
/** Capabilities whose module exists but is not ready → module id + reason. */
|
|
27
|
+
unavailable: Record<string, { module: string; reason: string }>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Parse `deeptutor module list` output (a JSON array). Tolerates leading noise. Pure. */
|
|
31
|
+
export function parseModuleList(stdout: string): ModuleInfo[] | undefined {
|
|
32
|
+
const start = stdout.indexOf("[");
|
|
33
|
+
if (start < 0) return undefined;
|
|
34
|
+
let raw: unknown;
|
|
35
|
+
try {
|
|
36
|
+
raw = JSON.parse(stdout.slice(start));
|
|
37
|
+
} catch {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
if (!Array.isArray(raw)) return undefined;
|
|
41
|
+
const out: ModuleInfo[] = [];
|
|
42
|
+
for (const m of raw) {
|
|
43
|
+
if (!m || typeof m !== "object" || typeof (m as any).id !== "string") continue;
|
|
44
|
+
const r = m as Record<string, unknown>;
|
|
45
|
+
out.push({
|
|
46
|
+
id: r.id as string,
|
|
47
|
+
ready: r.installed === true && r.enabled === true && r.ready === true,
|
|
48
|
+
capabilities: Array.isArray(r.capabilities)
|
|
49
|
+
? r.capabilities.filter((c): c is string => typeof c === "string")
|
|
50
|
+
: [],
|
|
51
|
+
issues: Array.isArray(r.issues) ? r.issues.map((i) => String(i)) : [],
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Fold module infos into a capability index. Pure. */
|
|
58
|
+
export function indexCapabilities(modules: ModuleInfo[]): CapabilityIndex {
|
|
59
|
+
const ready = new Set<string>();
|
|
60
|
+
const unavailable: CapabilityIndex["unavailable"] = {};
|
|
61
|
+
for (const m of modules) {
|
|
62
|
+
for (const cap of m.capabilities) {
|
|
63
|
+
if (m.ready) ready.add(cap);
|
|
64
|
+
else
|
|
65
|
+
unavailable[cap] = {
|
|
66
|
+
module: m.id,
|
|
67
|
+
reason: m.issues.length ? m.issues.join("; ") : "module not installed or not enabled",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
for (const cap of ready) delete unavailable[cap];
|
|
72
|
+
return { ready: [...ready].sort(), unavailable };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const CACHE_MS = 5 * 60_000;
|
|
76
|
+
let cache: { at: number; index: CapabilityIndex } | undefined;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Current capability index, cached for 5 minutes. Returns undefined when the
|
|
80
|
+
* deployment cannot tell (old DeepTutor without `module list`, CLI unreachable).
|
|
81
|
+
*/
|
|
82
|
+
export async function discoverCapabilities(
|
|
83
|
+
signal?: AbortSignal,
|
|
84
|
+
run: (args: string[], opts: { signal?: AbortSignal; timeoutMs?: number }) => Promise<CliResult> = runCli,
|
|
85
|
+
now: () => number = Date.now,
|
|
86
|
+
): Promise<CapabilityIndex | undefined> {
|
|
87
|
+
if (cache && now() - cache.at < CACHE_MS) return cache.index;
|
|
88
|
+
const res = await run(["module", "list"], { signal, timeoutMs: 60_000 });
|
|
89
|
+
if (res.code !== 0) return undefined;
|
|
90
|
+
const modules = parseModuleList(res.stdout);
|
|
91
|
+
if (!modules) return undefined;
|
|
92
|
+
const index = indexCapabilities(modules);
|
|
93
|
+
cache = { at: now(), index };
|
|
94
|
+
return index;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Test hook: forget the cached index. */
|
|
98
|
+
export function resetCapabilityCache() {
|
|
99
|
+
cache = undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** One line listing the ready capabilities, for tool results and errors. */
|
|
103
|
+
export function describeCapabilities(index: CapabilityIndex | undefined): string {
|
|
104
|
+
if (!index) return "Available capabilities: unknown (run `deeptutor module list` to check).";
|
|
105
|
+
const off = Object.entries(index.unavailable)
|
|
106
|
+
.map(([cap, u]) => `${cap} (module ${u.module}: ${u.reason})`)
|
|
107
|
+
.join(", ");
|
|
108
|
+
return (
|
|
109
|
+
`Available capabilities: ${index.ready.join(", ") || "(none)"}` +
|
|
110
|
+
(off ? `\nNot ready on this deployment: ${off}` : "")
|
|
111
|
+
);
|
|
112
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DeepTutor bridge plugin for DeepSeek Harness (dsh).
|
|
3
3
|
*
|
|
4
|
-
* Registers
|
|
4
|
+
* Registers the host-neutral tools from src/tools.ts:
|
|
5
5
|
* - deeptutor_run — execute a DeepTutor learning capability
|
|
6
6
|
* - deeptutor_kb — list / search / info knowledge bases
|
|
7
7
|
* - deeptutor_note — archive a Markdown learning note to a server notebook
|
|
8
|
+
* - deeptutor_cli — any other DeepTutor CLI command, behind safety tiers
|
|
9
|
+
*
|
|
10
|
+
* The same tools are served to MCP clients (Claude Code, …) by src/mcp.ts.
|
|
8
11
|
*
|
|
9
12
|
* Transport: every tool drives the deeptutor CLI.
|
|
10
13
|
* - Local: DEEPTUTOR_SSH_HOST unset → runs the local CLI (DEEPTUTOR_LOCAL_BIN).
|
|
11
14
|
* - Remote: DEEPTUTOR_SSH_HOST set → runs the server's CLI over SSH.
|
|
12
|
-
* The HTTP/WS API was dropped: DeepTutor 1.7 moved it off /api/v1 and put it behind
|
|
13
|
-
* a login, so the old probe always 404'd and cost ~30s per call before the CLI ran anyway.
|
|
14
15
|
*
|
|
15
16
|
* Env config (agent-agnostic): DEEPTUTOR_SSH_HOST / DEEPTUTOR_LOCAL_BIN /
|
|
16
17
|
* DEEPTUTOR_REMOTE_BIN / DEEPTUTOR_REMOTE_HOME — see src/config.ts and
|
|
@@ -18,329 +19,41 @@
|
|
|
18
19
|
*/
|
|
19
20
|
import type { Context } from "@deepseek-ai/cordis";
|
|
20
21
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
21
|
-
import { CAPABILITIES, configGuide, configHint, SSH_HOST, truncate } from "./config.ts";
|
|
22
|
-
import { cliNoteLocal, cliNoteRemote, runCli } from "./cli-exec.ts";
|
|
23
|
-
import { renderHtml } from "./html-render.ts";
|
|
24
|
-
import { cliRunTurn, fmtTurnResult } from "./turn.ts";
|
|
25
22
|
import { HTML_DOC_SCRIPT } from "./html-render.ts";
|
|
23
|
+
import { TOOLS } from "./tools.ts";
|
|
26
24
|
|
|
27
25
|
export const name = "dsh-deeptutor";
|
|
28
26
|
export const inject = ["tools"] as const;
|
|
29
27
|
|
|
30
|
-
/** Drop
|
|
31
|
-
|
|
32
|
-
[K in keyof T as T[K] extends undefined ? never : K]: Exclude<T[K], undefined>;
|
|
33
|
-
};
|
|
34
|
-
function json<T extends Record<string, unknown>>(o: T): Jsonify<T> {
|
|
28
|
+
/** Drop undefined values so the canonical JSON value stays lossless-JSON. */
|
|
29
|
+
function json(o: Record<string, unknown>): { text: string } & Record<string, any> {
|
|
35
30
|
const out: Record<string, unknown> = {};
|
|
36
31
|
for (const [k, v] of Object.entries(o)) if (v !== undefined) out[k] = v;
|
|
37
|
-
return out as
|
|
32
|
+
return out as { text: string };
|
|
38
33
|
}
|
|
39
34
|
|
|
35
|
+
const OUTPUT = {
|
|
36
|
+
schema: {
|
|
37
|
+
type: "object",
|
|
38
|
+
properties: { text: { type: "string" } },
|
|
39
|
+
additionalProperties: true,
|
|
40
|
+
},
|
|
41
|
+
render: (_args: unknown, value: { text?: string }) => [{ type: "text" as const, text: value.text ?? "" }],
|
|
42
|
+
} as const;
|
|
43
|
+
|
|
40
44
|
export function apply(ctx: Context) {
|
|
41
45
|
console.log(`[dsh-deeptutor] plugin loaded (html script: ${HTML_DOC_SCRIPT})`);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
},
|
|
56
|
-
prompt: {
|
|
57
|
-
type: "string",
|
|
58
|
-
required: true,
|
|
59
|
-
description:
|
|
60
|
-
"The learning question or instruction for this turn (e.g. explain the async/await state machine)",
|
|
61
|
-
},
|
|
62
|
-
kbs: {
|
|
63
|
-
type: "array",
|
|
64
|
-
items: { type: "string" },
|
|
65
|
-
description:
|
|
66
|
-
"Knowledge base names (check deeptutor_kb list for available ones, e.g. the user's dotnet/sqlserver bases)",
|
|
67
|
-
},
|
|
68
|
-
tools: {
|
|
69
|
-
type: "array",
|
|
70
|
-
items: { type: "string" },
|
|
71
|
-
description:
|
|
72
|
-
"Tools to enable: rag (KB retrieval), web_search, reason, brainstorm, paper_search, code_execution, read_source, etc.",
|
|
73
|
-
},
|
|
74
|
-
session_id: {
|
|
75
|
-
type: "string",
|
|
76
|
-
description:
|
|
77
|
-
"Session ID from a previous deeptutor_run call to continue that context",
|
|
78
|
-
},
|
|
79
|
-
config: {
|
|
80
|
-
type: "object",
|
|
81
|
-
additionalProperties: true,
|
|
82
|
-
description:
|
|
83
|
-
"Capability config key-values, e.g. num_questions=5 for deep_question, mode=report / depth=standard for deep_research",
|
|
84
|
-
},
|
|
85
|
-
language: {
|
|
86
|
-
type: "string",
|
|
87
|
-
description: "Response language code, default en; use zh for Chinese",
|
|
88
|
-
},
|
|
89
|
-
timeout: {
|
|
90
|
-
type: "number",
|
|
91
|
-
description: "Timeout in seconds, default 600; use 900+ for deep_research",
|
|
92
|
-
},
|
|
93
|
-
max_chars: {
|
|
94
|
-
type: "number",
|
|
95
|
-
description:
|
|
96
|
-
"Max characters of the answer returned to the model, default 30000",
|
|
97
|
-
},
|
|
98
|
-
html: {
|
|
99
|
-
type: "string",
|
|
100
|
-
description:
|
|
101
|
-
"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.",
|
|
102
|
-
},
|
|
103
|
-
html_title: {
|
|
104
|
-
type: "string",
|
|
105
|
-
description:
|
|
106
|
-
"HTML page title (default: derived from the html output file name)",
|
|
107
|
-
},
|
|
108
|
-
},
|
|
109
|
-
output: {
|
|
110
|
-
schema: {
|
|
111
|
-
type: "object",
|
|
112
|
-
properties: {
|
|
113
|
-
mode: { type: "string" },
|
|
114
|
-
sessionId: { type: "string" },
|
|
115
|
-
answer: { type: "string" },
|
|
116
|
-
toolCalls: { type: "array", items: { type: "string" } },
|
|
117
|
-
errors: { type: "array", items: { type: "string" } },
|
|
118
|
-
text: { type: "string" },
|
|
119
|
-
},
|
|
120
|
-
additionalProperties: true,
|
|
121
|
-
},
|
|
122
|
-
render: (_args, value) => [{ type: "text", text: value.text ?? "" }],
|
|
123
|
-
},
|
|
124
|
-
async execute(args, exec) {
|
|
125
|
-
const timeoutMs = (args.timeout ?? 600) * 1000;
|
|
126
|
-
const maxChars = args.max_chars ?? 30000;
|
|
127
|
-
try {
|
|
128
|
-
const r = await cliRunTurn(args, timeoutMs, exec.signal);
|
|
129
|
-
const htmlNote = await renderHtml(args, r.answer);
|
|
130
|
-
return json({
|
|
131
|
-
mode: "cli",
|
|
132
|
-
sessionId: r.sessionId,
|
|
133
|
-
toolCalls: r.toolCalls,
|
|
134
|
-
errors: r.errors,
|
|
135
|
-
text:
|
|
136
|
-
configHint() + fmtTurnResult(args.capability, r, maxChars) + htmlNote,
|
|
137
|
-
});
|
|
138
|
-
} catch (err: any) {
|
|
139
|
-
if (exec.signal.aborted) {
|
|
140
|
-
// user cancelled mid-CLI-run: report it as a cancellation, not a failure
|
|
141
|
-
throw new Error("[deeptutor_run cancelled]");
|
|
142
|
-
}
|
|
143
|
-
throw new Error(
|
|
144
|
-
configGuide() +
|
|
145
|
-
`[deeptutor_run failed] ${err?.message ?? String(err)}`,
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
},
|
|
149
|
-
}),
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
// ---------- Tool 2: knowledge base operations ----------
|
|
153
|
-
ctx.tools.register(
|
|
154
|
-
defineTool({
|
|
155
|
-
name: "deeptutor_kb",
|
|
156
|
-
description:
|
|
157
|
-
"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.",
|
|
158
|
-
parameters: {
|
|
159
|
-
action: {
|
|
160
|
-
type: "string",
|
|
161
|
-
enum: ["list", "search", "info"],
|
|
162
|
-
required: true,
|
|
163
|
-
description: "What to do with the knowledge bases",
|
|
164
|
-
},
|
|
165
|
-
kb: {
|
|
166
|
-
type: "string",
|
|
167
|
-
description: "Knowledge base name (required for search/info)",
|
|
168
|
-
},
|
|
169
|
-
query: {
|
|
170
|
-
type: "string",
|
|
171
|
-
description:
|
|
172
|
-
"Search question (required for search, e.g. C# generics covariance/contravariance)",
|
|
173
|
-
},
|
|
174
|
-
mode: {
|
|
175
|
-
type: "string",
|
|
176
|
-
enum: ["hybrid", "dense", "sparse"],
|
|
177
|
-
description: "Retrieval mode, default hybrid",
|
|
178
|
-
},
|
|
179
|
-
top_k: {
|
|
180
|
-
type: "number",
|
|
181
|
-
description:
|
|
182
|
-
"Number of hits to return, default 5 (applied locally to the CLI search result; the deeptutor CLI has no --top-k flag)",
|
|
183
|
-
},
|
|
184
|
-
max_chars: {
|
|
185
|
-
type: "number",
|
|
186
|
-
description:
|
|
187
|
-
"Max characters of the returned content, default 30000",
|
|
188
|
-
},
|
|
189
|
-
},
|
|
190
|
-
output: {
|
|
191
|
-
schema: {
|
|
192
|
-
type: "object",
|
|
193
|
-
properties: {
|
|
194
|
-
action: { type: "string" },
|
|
195
|
-
mode: { type: "string" },
|
|
196
|
-
kb: { type: "string" },
|
|
197
|
-
query: { type: "string" },
|
|
198
|
-
text: { type: "string" },
|
|
199
|
-
},
|
|
200
|
-
additionalProperties: true,
|
|
201
|
-
},
|
|
202
|
-
render: (_args, value) => [{ type: "text", text: value.text ?? "" }],
|
|
203
|
-
},
|
|
204
|
-
async execute(args, exec) {
|
|
205
|
-
const maxChars = args.max_chars ?? 30000;
|
|
206
|
-
// Local or SSH CLI. NOTE: `kb search` has no --top-k flag
|
|
207
|
-
// (verified on deeptutor CLI 1.5.x); top_k is applied locally below.
|
|
208
|
-
let argv: string[];
|
|
209
|
-
if (args.action === "list") {
|
|
210
|
-
argv = ["kb", "list", "--format", "json"];
|
|
211
|
-
} else if (args.action === "info") {
|
|
212
|
-
argv = ["kb", "info", args.kb ?? ""];
|
|
213
|
-
} else {
|
|
214
|
-
argv = [
|
|
215
|
-
"kb",
|
|
216
|
-
"search",
|
|
217
|
-
args.kb ?? "",
|
|
218
|
-
args.query ?? "",
|
|
219
|
-
"--format",
|
|
220
|
-
"json",
|
|
221
|
-
"--mode",
|
|
222
|
-
args.mode ?? "hybrid",
|
|
223
|
-
];
|
|
224
|
-
}
|
|
225
|
-
const { stdout, stderr, code, killed } = await runCli(argv, {
|
|
226
|
-
signal: exec.signal,
|
|
227
|
-
timeoutMs: 90_000,
|
|
228
|
-
});
|
|
229
|
-
if (killed) throw new Error(configGuide() + "[deeptutor_kb timed out]");
|
|
230
|
-
if (code !== 0) {
|
|
231
|
-
throw new Error(
|
|
232
|
-
`[deeptutor_kb failed] exit=${code}\nstderr: ${truncate(stderr || "(none)", 2000)}` +
|
|
233
|
-
configGuide(),
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
|
-
let body = stdout.trim() || "(empty result)";
|
|
237
|
-
if (args.action === "search") {
|
|
238
|
-
try {
|
|
239
|
-
const parsed = JSON.parse(stdout);
|
|
240
|
-
const fmtHit = (s: any, i: number) => {
|
|
241
|
-
const text = s.text ?? s.content ?? s.snippet ?? JSON.stringify(s);
|
|
242
|
-
const src = s.source ?? s.doc_name ?? s.file ?? "";
|
|
243
|
-
const title = s.title ? ` title: ${s.title}` : "";
|
|
244
|
-
const score = typeof s.score === "number" ? ` score: ${s.score}` : "";
|
|
245
|
-
return `[${i + 1}]${src ? ` source: ${src}` : ""}${title}${score}\n${typeof text === "string" ? text : JSON.stringify(text)}`;
|
|
246
|
-
};
|
|
247
|
-
// Current deeptutor CLI (1.5.x) returns an object
|
|
248
|
-
// { query, answer, content, sources, provider }; older builds
|
|
249
|
-
// returned a bare array of hits. Accept both.
|
|
250
|
-
if (Array.isArray(parsed)) {
|
|
251
|
-
const hits = args.top_k ? parsed.slice(0, args.top_k) : parsed;
|
|
252
|
-
body = `${hits.length} hits:\n\n` + hits.map(fmtHit).join("\n\n");
|
|
253
|
-
} else if (parsed && typeof parsed === "object") {
|
|
254
|
-
const parts: string[] = [];
|
|
255
|
-
if (parsed.query) parts.push(`Query: ${parsed.query}`);
|
|
256
|
-
if (typeof parsed.answer === "string" && parsed.answer.trim())
|
|
257
|
-
parts.push(`Answer: ${parsed.answer.trim()}`);
|
|
258
|
-
const sources = Array.isArray(parsed.sources) ? parsed.sources : [];
|
|
259
|
-
const hits = args.top_k ? sources.slice(0, args.top_k) : sources;
|
|
260
|
-
parts.push(`${hits.length} hits:`);
|
|
261
|
-
parts.push(hits.map(fmtHit).join("\n\n"));
|
|
262
|
-
body = parts.join("\n\n");
|
|
263
|
-
}
|
|
264
|
-
} catch {
|
|
265
|
-
/* non-JSON output returned as-is */
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
return json({
|
|
269
|
-
action: args.action,
|
|
270
|
-
mode: "cli",
|
|
271
|
-
kb: args.kb,
|
|
272
|
-
query: args.query,
|
|
273
|
-
text: truncate(body, maxChars),
|
|
274
|
-
});
|
|
275
|
-
},
|
|
276
|
-
}),
|
|
277
|
-
);
|
|
278
|
-
|
|
279
|
-
// ---------- Tool 3: archive learning notes to a server notebook ----------
|
|
280
|
-
ctx.tools.register(
|
|
281
|
-
defineTool({
|
|
282
|
-
name: "deeptutor_note",
|
|
283
|
-
description:
|
|
284
|
-
"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.",
|
|
285
|
-
parameters: {
|
|
286
|
-
notebook: {
|
|
287
|
-
type: "string",
|
|
288
|
-
required: true,
|
|
289
|
-
description:
|
|
290
|
-
"Notebook name, e.g. dotnet-learning (auto-created if missing)",
|
|
291
|
-
},
|
|
292
|
-
title: {
|
|
293
|
-
type: "string",
|
|
294
|
-
required: true,
|
|
295
|
-
description: "Record title, e.g. async/await state machine deep-dive notes",
|
|
296
|
-
},
|
|
297
|
-
type: {
|
|
298
|
-
type: "string",
|
|
299
|
-
enum: ["chat", "question", "research", "solve"],
|
|
300
|
-
description: "Record type, default chat",
|
|
301
|
-
},
|
|
302
|
-
content: {
|
|
303
|
-
type: "string",
|
|
304
|
-
required: true,
|
|
305
|
-
description: "Learning note content in Markdown",
|
|
306
|
-
},
|
|
307
|
-
},
|
|
308
|
-
output: {
|
|
309
|
-
schema: {
|
|
310
|
-
type: "object",
|
|
311
|
-
properties: {
|
|
312
|
-
notebook: { type: "string" },
|
|
313
|
-
nbId: { type: "string" },
|
|
314
|
-
title: { type: "string" },
|
|
315
|
-
mode: { type: "string" },
|
|
316
|
-
text: { type: "string" },
|
|
317
|
-
},
|
|
318
|
-
additionalProperties: true,
|
|
319
|
-
},
|
|
320
|
-
render: (_args, value) => [{ type: "text", text: value.text ?? "" }],
|
|
321
|
-
},
|
|
322
|
-
async execute(args, exec) {
|
|
323
|
-
const recordType = (args.type ?? "chat") as string;
|
|
324
|
-
const nbName = args.notebook.trim();
|
|
325
|
-
// Remote deployment runs the PY_NOTE script on the server; local deployment runs locally
|
|
326
|
-
const { stdout, stderr, code } = SSH_HOST
|
|
327
|
-
? await cliNoteRemote(nbName, args.title, recordType, args.content, exec.signal)
|
|
328
|
-
: await cliNoteLocal(nbName, args.title, recordType, args.content, exec.signal);
|
|
329
|
-
if (code !== 0) {
|
|
330
|
-
throw new Error(
|
|
331
|
-
`[deeptutor_note failed] exit=${code}\n${truncate(stderr || stdout, 2000)}` +
|
|
332
|
-
configGuide(),
|
|
333
|
-
);
|
|
334
|
-
}
|
|
335
|
-
return json({
|
|
336
|
-
notebook: nbName,
|
|
337
|
-
title: args.title,
|
|
338
|
-
mode: "cli",
|
|
339
|
-
text:
|
|
340
|
-
configHint() +
|
|
341
|
-
`[note archived] notebook=${nbName}, title=${args.title}, type=${recordType}\n${truncate((stdout ?? "").trim(), 2000)}`,
|
|
342
|
-
});
|
|
343
|
-
},
|
|
344
|
-
}),
|
|
345
|
-
);
|
|
46
|
+
for (const tool of TOOLS) {
|
|
47
|
+
ctx.tools.register(
|
|
48
|
+
defineTool({
|
|
49
|
+
name: tool.name,
|
|
50
|
+
description: tool.description,
|
|
51
|
+
parameters: tool.parameters,
|
|
52
|
+
output: OUTPUT,
|
|
53
|
+
async execute(args, exec) {
|
|
54
|
+
return json(await tool.execute(args as Record<string, any>, exec.signal)) as any;
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
346
59
|
}
|
package/src/mcp-main.ts
ADDED