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/README.zh.md CHANGED
@@ -2,21 +2,35 @@
2
2
 
3
3
  [English](https://github.com/TecFancy/dsh-deeptutor/blob/main/README.md) | **简体中文**
4
4
 
5
- 面向 **DeepSeek Harness (dsh)** 的**学习辅助扩展**。它将 [HKUDS/DeepTutor](https://github.com/HKUDS/DeepTutor) 辅导服务接入你的 agent,让 dsh 会话能够深度讲解知识点、出题自测、规划学习路径、检索你的个人知识库,并把学习笔记归档到 notebook —— 全程不出 harness。
5
+ 面向 **DeepSeek Harness (dsh) 和 Claude Code** 的**学习辅助扩展**。它将 [HKUDS/DeepTutor](https://github.com/HKUDS/DeepTutor) 辅导服务接入你的 agent,让会话能够深度讲解知识点、出题自测、规划学习路径、检索你的个人知识库,并把学习笔记归档到 notebook,全程不出 agent。在 dsh 里它作为 bundle 加载;在 Claude Code(或任何 MCP 客户端)里,同一组工具由随包的 MCP server 提供,以 Claude Code 插件的形式安装。
6
6
 
7
7
  只要对 agent 说 *"给我讲讲 async/await"*,它就能跑一次 deep-solve,把答案渲染成可在浏览器打开的自包含 HTML 学习页,再把摘要归档进你的笔记本。
8
8
 
9
9
  ## 它带来什么
10
10
 
11
- 三个 agent 可见工具,当你请求学习帮助时由模型调用:
11
+ 四个 agent 可见工具,当你请求学习帮助时由模型调用:
12
12
 
13
13
  | 工具 | 作用 |
14
14
  |---|---|
15
- | `deeptutor_run` | 执行一次学习调用: `deep_solve`(深度讲解)/ `deep_question`(自测题)/ `deep_research` / `chat` / `mastery_path`(学习路径规划)/ `visualize` / `math_animator`(可视化)。可挂载你的知识库(`kbs`)与工具(`rag`、`web_search`、`reason`、`code_execution` 等);返回 `session_id`,后续轮次可延续同一上下文。 |
15
+ | `deeptutor_run` | 执行一次学习调用,例如 `deep_solve`(深度讲解)、`deep_question`(自测题)、`deep_research`、`chat`、`mastery_path`(学习路径规划)、`visualize`。可挂载你的知识库(`kbs`)与工具(`rag`、`web_search`、`reason`、`code_execution` 等);返回 `session_id`,后续轮次可延续同一上下文,还能把答案渲染成 HTML 学习页。 |
16
16
  | `deeptutor_kb` | 列出 / 搜索 / 查看你的个人知识库(RAG) |
17
17
  | `deeptutor_note` | 把 Markdown 学习笔记(计划、总结、错题等)归档到服务器 notebook |
18
+ | `deeptutor_cli` | 执行 DeepTutor CLI 的其他任意命令,受下文的安全分级约束 |
18
19
 
19
- 上表 7 个 capability 是 `deeptutor_run` 工具接受的固定枚举。底层 DeepTutor CLI 可能支持更多命令 —— 用 `deeptutor --help` / `deeptutor <cmd> --help` 可枚举完整命令集。`deeptutor` skill 会指示 agent 以这种方式动态发现命令;当工具的枚举未覆盖某能力时,agent 可直接调用 CLI(本地二进制或经 SSH)来驱动。
20
+ ### DeepTutor 升级时插件不用跟着发版
21
+
22
+ 插件不再照抄 DeepTutor 的命令面,所以 DeepTutor 新增命令、capability 或工具时,插件不用改:
23
+
24
+ - **capability 运行时发现。** `deeptutor_run` 接受任意 capability 名。插件读取 `deeptutor module list`(JSON,缓存 5 分钟),得知部署上已就绪的模块提供哪些 capability;所属模块没就绪的 capability 会立即失败,并给出原因和当前可用列表;没有任何模块列出的名字直接交给 CLI(可能来自 DeepTutor 插件)。
25
+ - **其余命令都走 `deeptutor_cli`。** agent 从 CLI 自己查命令(`args: ["--help"]`,再 `["kb", "--help"]` 等),不依赖插件里写死的清单。安全分级保证透传对没见过的命令也有分寸:
26
+
27
+ | 档位 | 命令 | 行为 |
28
+ |---|---|---|
29
+ | read | `--help`,动词为 `list` / `show` / `info` / `search` / `state` / `status` / `list-*` 的命令,`doctor`、`run` | 直接执行 |
30
+ | confirm | 其余所有命令,包括插件还不认识的动词 | 需要 `confirm: true`,agent 只有在你明确要求这项改动后才能设 |
31
+ | blocked | `delete`、`remove*`、`clear`、`uninstall`、`rollback`、`login` 等;交互式或常驻的 `chat` / `start` / `stop` / `serve` / `init` | 拒绝执行,请自己在终端里跑 |
32
+
33
+ - **漂移检查。** 能让插件失效的只有专用工具依赖的少数几处 CLI 形状(`run --format json` 的事件、`kb search`、`notebook add-md`、`module list`)。`npm run check:cli` 会对环境变量指向的部署逐项检查;升级 DeepTutor 后跑一次。
20
34
 
21
35
  ## 示例会话
22
36
 
@@ -37,14 +51,40 @@
37
51
 
38
52
  ## 环境要求
39
53
 
40
- - 可用的 DeepSeek Harness (dsh) 安装,0.1.0-rc.6 或更新版本。清单里只写下限不写上限,新版本 dsh 发布后不会再在安装时被拒绝;替代办法是定时拿 npm 上 `latest` 和 `next` 两个通道的宿主各冷启动一次,插件起不来就开 issue。`dsh plugin add` 自动登记 bundle 已在 dsh CLI 0.1.0-rc.6、0.2.1-rc.2 与 0.2.1-rc.3 上验证。
54
+ - 一个 agent 宿主:Claude Code(或其他 MCP 客户端)加 Node ≥ 20,或者可用的 DeepSeek Harness (dsh) 安装,0.1.0-rc.6 或更新版本。清单里只写下限不写上限,新版本 dsh 发布后不会再在安装时被拒绝;替代办法是定时拿 npm 上 `latest` 和 `next` 两个通道的宿主各冷启动一次,插件起不来就开 issue。`dsh plugin add` 自动登记 bundle 已在 dsh CLI 0.1.0-rc.6、0.2.1-rc.2 与 0.2.1-rc.3 上验证。
41
55
  - 一个 DeepTutor 部署,二选一:
42
56
  - **本地** —— 本机装有 `deeptutor` CLI(在 PATH 上,或由 `DEEPTUTOR_LOCAL_BIN` 指定);
43
57
  - **远程** —— DeepTutor 装在一台能免密 SSH 登录的服务器上,插件经 SSH 调用服务器上的 CLI。
44
58
 
45
59
  已在 DeepTutor 1.7.1 上验证。从 1.7 起,`kb`、`notebook` 命令和大部分 capability 由可选模块提供,用 `deeptutor module list` 确认要用的模块已安装并启用。
46
60
 
47
- ## 安装到 profile(bundle 方式)
61
+ ## 安装到 Claude Code(插件方式)
62
+
63
+ 本仓库同时是一个 Claude Code 插件 marketplace。在 Claude Code 里:
64
+
65
+ ```text
66
+ /plugin marketplace add TecFancy/dsh-deeptutor
67
+ /plugin install dsh-deeptutor@dsh-deeptutor
68
+ ```
69
+
70
+ 或者在 shell 里:
71
+
72
+ ```sh
73
+ claude plugin marketplace add TecFancy/dsh-deeptutor
74
+ claude plugin install dsh-deeptutor@dsh-deeptutor
75
+ ```
76
+
77
+ 插件带来两个 skill(`deeptutor`、`html-doc`)和一个名为 `deeptutor` 的 MCP server,提供上面四个工具(Claude Code 里显示为 `mcp__plugin_dsh-deeptutor_deeptutor__deeptutor_run` 等)。把 `DEEPTUTOR_*` 环境变量设在 Claude Code 能继承到的地方(见[配置](#配置环境变量与-agent-无关)),然后重启 Claude Code。
78
+
79
+ MCP server 由 `scripts/mcp-launch.mjs` 启动,按顺序取第一个可用的入口:编译好的 `lib/` → 插件自带的 `src/`,用 Node 的类型剥离直接运行(Node ≥ 22.6,不联网)→ 旧版 Node 时用 `npx dsh-deeptutor@<插件版本>`。无论哪条路,跑的都是已安装插件那个版本的代码。
80
+
81
+ **其他 MCP 客户端**(Codex、Cursor 等)可以直接运行发布出去的 bin:
82
+
83
+ ```json
84
+ { "command": "npx", "args": ["-y", "-p", "dsh-deeptutor", "dsh-deeptutor-mcp"] }
85
+ ```
86
+
87
+ ## 安装到 dsh profile(bundle 方式)
48
88
 
49
89
  包已发布到 npm(`dsh-deeptutor`)。推荐一条命令 —— 运行随包发布的安装器(`scripts/install-profile.mjs`,以 `dsh-deeptutor` 二进制暴露),它包装 `dsh plugin add` 并自动处理下文所述的 pnpm workspace-root 检查:
50
90
 
@@ -79,7 +119,7 @@ dsh --profile web --dump-config | grep dsh-deeptutor
79
119
 
80
120
  ```json
81
121
  {
82
- "dependencies": { "dsh-deeptutor": "^0.2.0" },
122
+ "dependencies": { "dsh-deeptutor": "^0.3.1" },
83
123
  "dsh": {
84
124
  "profile": { "bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app", "dsh-deeptutor"] }
85
125
  }
@@ -110,7 +150,7 @@ bundle 清单(`dsh.bundle.patch → cordis.patch.yml`)负责插入插件行;上
110
150
 
111
151
  升级包版本时会就地覆盖 skill 文件;包未携带的文件不会被删除。
112
152
 
113
- skill 就在本仓库的 `.agents/skills/` 下维护。
153
+ Claude Code 插件直接从 `.agents/skills/` 加载同一份 skill。skill 就在本仓库的 `.agents/skills/` 下维护。
114
154
 
115
155
  ## 配置(环境变量,与 agent 无关)
116
156
 
@@ -124,7 +164,7 @@ export DEEPTUTOR_REMOTE_HOME="/home/ubuntu/my-deeptutor"
124
164
  # export DEEPTUTOR_LOCAL_BIN="deeptutor" # 本地 CLI 路径(默认: PATH 上的 deeptutor)
125
165
  ```
126
166
 
127
- 修改环境变量后需重启 dsh。
167
+ 修改环境变量后需重启 dsh 或 Claude Code。
128
168
 
129
169
  **Windows 本地模式:** 把 `DEEPTUTOR_LOCAL_BIN` 指向 `deeptutor.exe`(pip/venv 安装时是 `<venv>\Scripts\deeptutor.exe`)。`.cmd`/`.bat` 包装脚本也能用:只把 `%*` 转给某个 `.exe` 的包装脚本会被绕过,直接运行那个 `.exe`;其他包装脚本经 `cmd.exe` 运行,而 `cmd.exe` 传不了多行提示词,这类调用会报错并提示改用 `.exe`。
130
170
 
@@ -147,6 +187,8 @@ npm install
147
187
  npm run build # tsc → lib/(相对 .ts import 改写为 .js)
148
188
  npm run typecheck
149
189
  npm test # node:test + type stripping
190
+ npm run check:cli # 对环境变量指向的 DeepTutor 做漂移检查
191
+ claude plugin validate . --strict # 校验 Claude Code 插件和 marketplace 清单
150
192
  npm pack # 检查 dsh-deeptutor-<version>.tgz
151
193
  npm publish
152
194
  ```
@@ -160,9 +202,12 @@ src/ # TypeScript 源码(开发加载、类型检查)
160
202
  lib/ # 编译产物(发布入口,由 npm run build 生成)
161
203
  scripts/md-to-html.js # 零依赖 Markdown → HTML 转换器
162
204
  scripts/install-profile.mjs # 一键 profile 安装器(以 `dsh-deeptutor` 二进制暴露)
205
+ scripts/mcp-launch.mjs # 为 Claude Code 插件启动 MCP server
206
+ scripts/check-cli.ts # 对真实 DeepTutor 的漂移检查(`npm run check:cli`)
163
207
  .agents/skills/ # 随包 skill:deeptutor/ + html-doc/(安装到 <DSH_HOME>/skills/)
208
+ .claude-plugin/ # Claude Code 的 plugin.json(skill 和 MCP server)和 marketplace.json
164
209
  cordis.yml # 开发 overlay:按绝对路径插入 src/index.ts
165
210
  cordis.patch.yml # bundle patch:按包名插入插件
166
211
  ```
167
212
 
168
- `src/index.ts` 注册工具;`config.ts` 环境配置;`cli-exec.ts` 本地/SSH CLI 执行;`win-cmd.ts` 在 Windows 上启动 `.cmd`/`.bat` 形式的 CLI;`turn.ts` 单次学习回合(CLI JSONL 事件折叠与格式化);`html-render.ts` 答案 → 自包含 HTML。
213
+ `src/tools.ts` 定义四个工具,与宿主无关;`index.ts` 在 dsh 里注册它们;`mcp.ts` 经 MCP stdio 提供它们(`mcp-main.ts` 是 `dsh-deeptutor-mcp` bin 的入口);`cli-policy.ts` 是 `deeptutor_cli` 的安全分级;`discovery.ts` 负责 capability 发现;`config.ts` 环境配置;`cli-exec.ts` 本地/SSH CLI 执行;`win-cmd.ts` 在 Windows 上启动 `.cmd`/`.bat` 形式的 CLI;`turn.ts` 单次学习回合(CLI JSONL 事件折叠与格式化);`html-render.ts` 答案 → 自包含 HTML。
package/lib/cli-exec.d.ts CHANGED
@@ -4,16 +4,22 @@ export interface CliResult {
4
4
  code: number;
5
5
  killed: boolean;
6
6
  }
7
+ /** Options shared by every CLI invocation. */
8
+ export interface CliOptions {
9
+ signal?: AbortSignal;
10
+ timeoutMs?: number;
11
+ /** Extra env vars for the deeptutor process (names must match [A-Z_][A-Z0-9_]*). */
12
+ env?: Record<string, string>;
13
+ }
14
+ /** `export K='v'` lines for the remote script; values are single-quote escaped. */
15
+ export declare function exportLines(env?: Record<string, string>): string[];
7
16
  /**
8
17
  * Unified CLI entry:
9
18
  * - Remote deployment (SSH_HOST set) → execute on the server over SSH;
10
19
  * - Local deployment (SSH_HOST empty) → execute DEEPTUTOR_LOCAL_BIN locally.
11
20
  * args are deeptutor subcommand args (without the program name).
12
21
  */
13
- export declare function runCli(args: string[], opts?: {
14
- signal?: AbortSignal;
15
- timeoutMs?: number;
16
- }): Promise<CliResult>;
22
+ export declare function runCli(args: string[], opts?: CliOptions): Promise<CliResult>;
17
23
  /** Local-deployment notebook archiver: list → parse ID → create → add-md */
18
24
  export declare function cliNoteLocal(nbName: string, title: string, recordType: string, content: string, signal?: AbortSignal): Promise<{
19
25
  stdout: string;
package/lib/cli-exec.js CHANGED
@@ -77,11 +77,19 @@ function execFileResolved(file, args, opts = {}) {
77
77
  }, { once: true });
78
78
  });
79
79
  }
80
+ const ENV_NAME = /^[A-Z_][A-Z0-9_]*$/;
81
+ /** `export K='v'` lines for the remote script; values are single-quote escaped. */
82
+ export function exportLines(env = {}) {
83
+ return Object.entries(env)
84
+ .filter(([k]) => ENV_NAME.test(k))
85
+ .map(([k, v]) => `export ${k}='${v.replace(/'/g, `'\\''`)}'`);
86
+ }
80
87
  async function sshExec(argv, opts = {}) {
81
88
  const payload = JSON.stringify({ argv, cwd: DEEPTUTOR_HOME });
82
89
  const script = [
83
90
  `export DEEPTUTOR_HOME=${DEEPTUTOR_HOME}`,
84
91
  "export DEEPTUTOR_SANDBOX_ALLOW_SUBPROCESS=1",
92
+ ...exportLines(opts.env),
85
93
  "F=/tmp/dt_pi_$$",
86
94
  `echo '${b64(payload)}' | base64 -d > "$F.json"`,
87
95
  `echo '${b64(PY_RUNNER)}' | base64 -d > "$F.py"`,
@@ -119,7 +127,7 @@ export async function runCli(args, opts = {}) {
119
127
  const res = await execFileResolved(cmd.file, cmd.args, {
120
128
  signal: opts.signal,
121
129
  timeoutMs: opts.timeoutMs,
122
- env: { PYTHONUTF8: "1", PYTHONIOENCODING: "utf-8" },
130
+ env: { ...opts.env, PYTHONUTF8: "1", PYTHONIOENCODING: "utf-8" },
123
131
  windowsVerbatimArguments: cmd.windowsVerbatimArguments,
124
132
  });
125
133
  const out = res.stdout ?? "";
@@ -0,0 +1,23 @@
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
+ export type CliTier = "read" | "confirm" | "blocked";
16
+ export interface CliVerdict {
17
+ tier: CliTier;
18
+ reason: string;
19
+ }
20
+ /** Leading positional words (the command path), stopping at the first option. */
21
+ export declare function commandPath(args: readonly string[]): string[];
22
+ /** Classify one `deeptutor <args>` invocation. Pure; unit-tested. */
23
+ export declare function classifyCliArgs(args: readonly string[]): CliVerdict;
@@ -0,0 +1,86 @@
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
+ /** Top-level commands that start a REPL, a server, a launcher or a setup wizard. */
16
+ const INTERACTIVE_TOP = new Set(["chat", "start", "stop", "serve", "init"]);
17
+ /** Verbs that only read. Matched against the subcommand (or a single-command top level). */
18
+ const READ_VERBS = new Set([
19
+ "list",
20
+ "show",
21
+ "info",
22
+ "search",
23
+ "state",
24
+ "status",
25
+ "doctor",
26
+ "get",
27
+ "view",
28
+ ]);
29
+ /** Verbs that delete, reset or roll back state, or need an interactive login. */
30
+ const BLOCKED_VERB = /^(delete|remove|remove-.+|clear|uninstall|rollback|purge|reset|drop|wipe|prune|login|logout)$/;
31
+ /** Commands the plugin's own tools run without confirmation; same rule here. */
32
+ const READ_TOP = new Set(["doctor", "run"]);
33
+ /** Leading positional words (the command path), stopping at the first option. */
34
+ export function commandPath(args) {
35
+ const path = [];
36
+ for (const a of args) {
37
+ if (a.startsWith("-"))
38
+ break;
39
+ path.push(a);
40
+ }
41
+ return path;
42
+ }
43
+ /** Classify one `deeptutor <args>` invocation. Pure; unit-tested. */
44
+ export function classifyCliArgs(args) {
45
+ if (args.length === 0) {
46
+ return { tier: "read", reason: "no arguments prints the top-level help" };
47
+ }
48
+ if (args.includes("--help") || args.includes("-h")) {
49
+ return { tier: "read", reason: "--help only prints usage" };
50
+ }
51
+ const path = commandPath(args);
52
+ const [top, sub] = path;
53
+ if (!top) {
54
+ return { tier: "confirm", reason: "options without a command" };
55
+ }
56
+ if (INTERACTIVE_TOP.has(top)) {
57
+ return {
58
+ tier: "blocked",
59
+ reason: `\`deeptutor ${top}\` is interactive or long-running and would hang a tool call; run it in a terminal yourself`,
60
+ };
61
+ }
62
+ for (const word of [top, sub]) {
63
+ if (word && BLOCKED_VERB.test(word)) {
64
+ return {
65
+ tier: "blocked",
66
+ reason: `\`${word}\` deletes, resets or logs in; run it in a terminal yourself if you really mean it`,
67
+ };
68
+ }
69
+ }
70
+ if (READ_TOP.has(top)) {
71
+ return { tier: "read", reason: `\`deeptutor ${top}\` is a read-only or learning command` };
72
+ }
73
+ if (sub && (READ_VERBS.has(sub) || sub.startsWith("list-"))) {
74
+ return { tier: "read", reason: `\`${sub}\` only reads` };
75
+ }
76
+ if (!sub && path.length === 1) {
77
+ return {
78
+ tier: "confirm",
79
+ reason: `\`deeptutor ${top}\` without a subcommand; use --help to see what it does`,
80
+ };
81
+ }
82
+ return {
83
+ tier: "confirm",
84
+ reason: `\`${[top, sub].filter(Boolean).join(" ")}\` may change DeepTutor state`,
85
+ };
86
+ }
package/lib/config.d.ts CHANGED
@@ -16,5 +16,4 @@ export declare const DEEPTUTOR_BIN: string;
16
16
  export declare const DEEPTUTOR_HOME: string;
17
17
  export declare function configHint(): string;
18
18
  export declare function configGuide(): string;
19
- export declare const CAPABILITIES: readonly ["chat", "deep_solve", "deep_question", "deep_research", "visualize", "math_animator", "mastery_path"];
20
19
  export declare const truncate: (s: string, n: number) => string;
package/lib/config.js CHANGED
@@ -46,13 +46,4 @@ export function configGuide() {
46
46
  'PowerShell users: use $env:VAR="value" instead of export VAR=value.\n' +
47
47
  "Restart the agent after configuring.\n");
48
48
  }
49
- export const CAPABILITIES = [
50
- "chat",
51
- "deep_solve",
52
- "deep_question",
53
- "deep_research",
54
- "visualize",
55
- "math_animator",
56
- "mastery_path",
57
- ];
58
49
  export const truncate = (s, n) => s.length > n ? s.slice(0, n) + `\n…[truncated, ${s.length} chars total]` : s;
@@ -0,0 +1,45 @@
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 { type CliResult } from "./cli-exec.ts";
15
+ export interface ModuleInfo {
16
+ id: string;
17
+ ready: boolean;
18
+ capabilities: string[];
19
+ issues: string[];
20
+ }
21
+ export interface CapabilityIndex {
22
+ /** Capabilities whose module is installed, enabled and ready. */
23
+ ready: string[];
24
+ /** Capabilities whose module exists but is not ready → module id + reason. */
25
+ unavailable: Record<string, {
26
+ module: string;
27
+ reason: string;
28
+ }>;
29
+ }
30
+ /** Parse `deeptutor module list` output (a JSON array). Tolerates leading noise. Pure. */
31
+ export declare function parseModuleList(stdout: string): ModuleInfo[] | undefined;
32
+ /** Fold module infos into a capability index. Pure. */
33
+ export declare function indexCapabilities(modules: ModuleInfo[]): CapabilityIndex;
34
+ /**
35
+ * Current capability index, cached for 5 minutes. Returns undefined when the
36
+ * deployment cannot tell (old DeepTutor without `module list`, CLI unreachable).
37
+ */
38
+ export declare function discoverCapabilities(signal?: AbortSignal, run?: (args: string[], opts: {
39
+ signal?: AbortSignal;
40
+ timeoutMs?: number;
41
+ }) => Promise<CliResult>, now?: () => number): Promise<CapabilityIndex | undefined>;
42
+ /** Test hook: forget the cached index. */
43
+ export declare function resetCapabilityCache(): void;
44
+ /** One line listing the ready capabilities, for tool results and errors. */
45
+ export declare function describeCapabilities(index: CapabilityIndex | undefined): string;
@@ -0,0 +1,96 @@
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 } from "./cli-exec.js";
15
+ /** Parse `deeptutor module list` output (a JSON array). Tolerates leading noise. Pure. */
16
+ export function parseModuleList(stdout) {
17
+ const start = stdout.indexOf("[");
18
+ if (start < 0)
19
+ return undefined;
20
+ let raw;
21
+ try {
22
+ raw = JSON.parse(stdout.slice(start));
23
+ }
24
+ catch {
25
+ return undefined;
26
+ }
27
+ if (!Array.isArray(raw))
28
+ return undefined;
29
+ const out = [];
30
+ for (const m of raw) {
31
+ if (!m || typeof m !== "object" || typeof m.id !== "string")
32
+ continue;
33
+ const r = m;
34
+ out.push({
35
+ id: r.id,
36
+ ready: r.installed === true && r.enabled === true && r.ready === true,
37
+ capabilities: Array.isArray(r.capabilities)
38
+ ? r.capabilities.filter((c) => typeof c === "string")
39
+ : [],
40
+ issues: Array.isArray(r.issues) ? r.issues.map((i) => String(i)) : [],
41
+ });
42
+ }
43
+ return out;
44
+ }
45
+ /** Fold module infos into a capability index. Pure. */
46
+ export function indexCapabilities(modules) {
47
+ const ready = new Set();
48
+ const unavailable = {};
49
+ for (const m of modules) {
50
+ for (const cap of m.capabilities) {
51
+ if (m.ready)
52
+ ready.add(cap);
53
+ else
54
+ unavailable[cap] = {
55
+ module: m.id,
56
+ reason: m.issues.length ? m.issues.join("; ") : "module not installed or not enabled",
57
+ };
58
+ }
59
+ }
60
+ for (const cap of ready)
61
+ delete unavailable[cap];
62
+ return { ready: [...ready].sort(), unavailable };
63
+ }
64
+ const CACHE_MS = 5 * 60_000;
65
+ let cache;
66
+ /**
67
+ * Current capability index, cached for 5 minutes. Returns undefined when the
68
+ * deployment cannot tell (old DeepTutor without `module list`, CLI unreachable).
69
+ */
70
+ export async function discoverCapabilities(signal, run = runCli, now = Date.now) {
71
+ if (cache && now() - cache.at < CACHE_MS)
72
+ return cache.index;
73
+ const res = await run(["module", "list"], { signal, timeoutMs: 60_000 });
74
+ if (res.code !== 0)
75
+ return undefined;
76
+ const modules = parseModuleList(res.stdout);
77
+ if (!modules)
78
+ return undefined;
79
+ const index = indexCapabilities(modules);
80
+ cache = { at: now(), index };
81
+ return index;
82
+ }
83
+ /** Test hook: forget the cached index. */
84
+ export function resetCapabilityCache() {
85
+ cache = undefined;
86
+ }
87
+ /** One line listing the ready capabilities, for tool results and errors. */
88
+ export function describeCapabilities(index) {
89
+ if (!index)
90
+ return "Available capabilities: unknown (run `deeptutor module list` to check).";
91
+ const off = Object.entries(index.unavailable)
92
+ .map(([cap, u]) => `${cap} (module ${u.module}: ${u.reason})`)
93
+ .join(", ");
94
+ return (`Available capabilities: ${index.ready.join(", ") || "(none)"}` +
95
+ (off ? `\nNot ready on this deployment: ${off}` : ""));
96
+ }
package/lib/index.d.ts CHANGED
@@ -1,16 +1,17 @@
1
1
  /**
2
2
  * DeepTutor bridge plugin for DeepSeek Harness (dsh).
3
3
  *
4
- * Registers three model-facing tools:
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