dsh-deeptutor 0.2.1 → 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/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/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
package/src/tools.ts
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepTutor bridge: the agent-facing tools, defined once and host-neutral.
|
|
3
|
+
*
|
|
4
|
+
* Two hosts adapt these specs:
|
|
5
|
+
* - dsh (src/index.ts) registers them through `defineTool`;
|
|
6
|
+
* - MCP clients such as Claude Code (src/mcp.ts) list and call them over stdio.
|
|
7
|
+
*
|
|
8
|
+
* Parameter specs use the dsh-tools shape (JSON-Schema-like, `required: true`
|
|
9
|
+
* on the property) so dsh can infer argument types; `toJsonSchema` turns them
|
|
10
|
+
* into plain JSON Schema for MCP.
|
|
11
|
+
*
|
|
12
|
+
* The four tools split the work like this:
|
|
13
|
+
* deeptutor_run one learning turn (event folding, session reuse, HTML render)
|
|
14
|
+
* deeptutor_kb list / search / info knowledge bases (hit formatting)
|
|
15
|
+
* deeptutor_note archive Markdown to a notebook (find-or-create, file upload)
|
|
16
|
+
* deeptutor_cli any other CLI command, discovered from --help, behind the
|
|
17
|
+
* safety tiers in cli-policy.ts
|
|
18
|
+
*/
|
|
19
|
+
import { cliNoteLocal, cliNoteRemote, runCli } from "./cli-exec.ts";
|
|
20
|
+
import { classifyCliArgs } from "./cli-policy.ts";
|
|
21
|
+
import { configGuide, configHint, SSH_HOST, truncate } from "./config.ts";
|
|
22
|
+
import { describeCapabilities, discoverCapabilities } from "./discovery.ts";
|
|
23
|
+
import { renderHtml } from "./html-render.ts";
|
|
24
|
+
import { cliRunTurn, fmtTurnResult } from "./turn.ts";
|
|
25
|
+
|
|
26
|
+
interface Base {
|
|
27
|
+
description: string;
|
|
28
|
+
required?: true;
|
|
29
|
+
}
|
|
30
|
+
export type ParamSpec =
|
|
31
|
+
| (Base & { type: "string"; enum?: readonly string[] })
|
|
32
|
+
| (Base & { type: "number" })
|
|
33
|
+
| (Base & { type: "boolean" })
|
|
34
|
+
| (Base & { type: "array"; items: { type: "string" } })
|
|
35
|
+
| (Base & { type: "object"; additionalProperties: true });
|
|
36
|
+
|
|
37
|
+
export type ParamsSpec = Record<string, ParamSpec>;
|
|
38
|
+
|
|
39
|
+
export interface ToolResultData {
|
|
40
|
+
/** Text shown to the model. */
|
|
41
|
+
text: string;
|
|
42
|
+
/** Structured extras (session id, tool calls…); undefined values are dropped. */
|
|
43
|
+
[key: string]: unknown;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ToolSpec {
|
|
47
|
+
name: string;
|
|
48
|
+
description: string;
|
|
49
|
+
parameters: ParamsSpec;
|
|
50
|
+
/** Throws an Error whose message is shown to the model on failure. */
|
|
51
|
+
execute(args: Record<string, any>, signal: AbortSignal): Promise<ToolResultData>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Plain JSON Schema (object) for one tool's parameters. Pure. */
|
|
55
|
+
export function toJsonSchema(params: ParamsSpec): {
|
|
56
|
+
type: "object";
|
|
57
|
+
properties: Record<string, unknown>;
|
|
58
|
+
required: string[];
|
|
59
|
+
additionalProperties: false;
|
|
60
|
+
} {
|
|
61
|
+
const properties: Record<string, unknown> = {};
|
|
62
|
+
const required: string[] = [];
|
|
63
|
+
for (const [key, spec] of Object.entries(params)) {
|
|
64
|
+
const { required: isRequired, ...schema } = spec;
|
|
65
|
+
properties[key] = schema;
|
|
66
|
+
if (isRequired) required.push(key);
|
|
67
|
+
}
|
|
68
|
+
return { type: "object", properties, required, additionalProperties: false };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Required-parameter and basic type check (MCP clients may skip schema validation). Pure. */
|
|
72
|
+
export function validateArgs(params: ParamsSpec, args: Record<string, unknown>): string[] {
|
|
73
|
+
const problems: string[] = [];
|
|
74
|
+
for (const [key, spec] of Object.entries(params)) {
|
|
75
|
+
const v = args[key];
|
|
76
|
+
if (v === undefined || v === null) {
|
|
77
|
+
if (spec.required) problems.push(`missing required parameter "${key}"`);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const ok =
|
|
81
|
+
spec.type === "array"
|
|
82
|
+
? Array.isArray(v) && v.every((x) => typeof x === "string")
|
|
83
|
+
: spec.type === "object"
|
|
84
|
+
? typeof v === "object" && !Array.isArray(v)
|
|
85
|
+
: typeof v === spec.type;
|
|
86
|
+
if (!ok) problems.push(`parameter "${key}" must be ${spec.type === "array" ? "an array of strings" : `a ${spec.type}`}`);
|
|
87
|
+
else if (spec.type === "string" && spec.enum && !spec.enum.includes(v as string))
|
|
88
|
+
problems.push(`parameter "${key}" must be one of: ${spec.enum.join(", ")}`);
|
|
89
|
+
}
|
|
90
|
+
return problems;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---------- deeptutor_run ----------
|
|
94
|
+
|
|
95
|
+
const runTool: ToolSpec = {
|
|
96
|
+
name: "deeptutor_run",
|
|
97
|
+
description:
|
|
98
|
+
"Execute one learning call on the DeepTutor service, e.g. deep_solve (in-depth explanation), deep_question (self-test questions), deep_research, chat, mastery_path (learning path), visualize. The capability set depends on the deployment and its installed modules: list it with deeptutor_cli args=[\"module\",\"list\"] (capabilities of modules that are ready). Can mount knowledge bases (kbs) and tools (rag, web_search, …; list them with deeptutor_cli args=[\"plugin\",\"list\"]). Returns the final answer plus session_id; pass session_id in later turns to continue the context.",
|
|
99
|
+
parameters: {
|
|
100
|
+
capability: {
|
|
101
|
+
type: "string",
|
|
102
|
+
required: true,
|
|
103
|
+
description:
|
|
104
|
+
"Capability name, e.g. deep_solve / deep_question / deep_research / chat / mastery_path / visualize. Not a fixed list: whatever the deployment's ready modules provide.",
|
|
105
|
+
},
|
|
106
|
+
prompt: {
|
|
107
|
+
type: "string",
|
|
108
|
+
required: true,
|
|
109
|
+
description: "The learning question or instruction for this turn (e.g. explain the async/await state machine)",
|
|
110
|
+
},
|
|
111
|
+
kbs: {
|
|
112
|
+
type: "array",
|
|
113
|
+
items: { type: "string" },
|
|
114
|
+
description: "Knowledge base names (check deeptutor_kb action=list first; never guess names)",
|
|
115
|
+
},
|
|
116
|
+
tools: {
|
|
117
|
+
type: "array",
|
|
118
|
+
items: { type: "string" },
|
|
119
|
+
description: "Tools to enable, e.g. rag (KB retrieval), web_search, reason, brainstorm, code_execution",
|
|
120
|
+
},
|
|
121
|
+
session_id: {
|
|
122
|
+
type: "string",
|
|
123
|
+
description: "Session ID from a previous deeptutor_run call to continue that context",
|
|
124
|
+
},
|
|
125
|
+
config: {
|
|
126
|
+
type: "object",
|
|
127
|
+
additionalProperties: true,
|
|
128
|
+
description:
|
|
129
|
+
"Capability config key-values, e.g. num_questions=5 for deep_question, mode=report / depth=standard for deep_research",
|
|
130
|
+
},
|
|
131
|
+
language: { type: "string", description: "Response language code, default en; use zh for Chinese" },
|
|
132
|
+
timeout: { type: "number", description: "Timeout in seconds, default 600; use 900+ for deep_research" },
|
|
133
|
+
max_chars: { type: "number", description: "Max characters of the answer returned, default 30000" },
|
|
134
|
+
html: {
|
|
135
|
+
type: "string",
|
|
136
|
+
description:
|
|
137
|
+
"Optional output path for a self-contained local HTML render of the answer (also writes the companion .md next to it), e.g. data/study/abstract-class/knowledge.html. Relative paths resolve against the agent's working directory. Set it when the user wants browsable/printable study material.",
|
|
138
|
+
},
|
|
139
|
+
html_title: { type: "string", description: "HTML page title (default: derived from the html file name)" },
|
|
140
|
+
},
|
|
141
|
+
async execute(args, signal) {
|
|
142
|
+
const timeoutMs = (args.timeout ?? 600) * 1000;
|
|
143
|
+
const maxChars = args.max_chars ?? 30000;
|
|
144
|
+
const capability = String(args.capability).trim();
|
|
145
|
+
const index = await discoverCapabilities(signal).catch(() => undefined);
|
|
146
|
+
const off = index?.unavailable[capability];
|
|
147
|
+
if (off) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
`[deeptutor_run] capability "${capability}" is not ready on this deployment ` +
|
|
150
|
+
`(module ${off.module}: ${off.reason}).\n${describeCapabilities(index)}`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
const r = await cliRunTurn({ ...args, capability } as any, timeoutMs, signal);
|
|
155
|
+
const htmlNote = await renderHtml(args, r.answer);
|
|
156
|
+
return {
|
|
157
|
+
mode: "cli",
|
|
158
|
+
sessionId: r.sessionId,
|
|
159
|
+
toolCalls: r.toolCalls,
|
|
160
|
+
errors: r.errors,
|
|
161
|
+
text: configHint() + fmtTurnResult(capability, r, maxChars) + htmlNote,
|
|
162
|
+
};
|
|
163
|
+
} catch (err: any) {
|
|
164
|
+
if (signal.aborted) throw new Error("[deeptutor_run cancelled]");
|
|
165
|
+
const known = index && !index.ready.includes(capability) ? `\n${describeCapabilities(index)}` : "";
|
|
166
|
+
throw new Error(configGuide() + `[deeptutor_run failed] ${err?.message ?? String(err)}${known}`);
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// ---------- deeptutor_kb ----------
|
|
172
|
+
|
|
173
|
+
const kbTool: ToolSpec = {
|
|
174
|
+
name: "deeptutor_kb",
|
|
175
|
+
description:
|
|
176
|
+
"Search DeepTutor knowledge bases: list all bases, search inside a base (RAG hit snippets), info for details. Search the user's personal knowledge bases before generating learning content and combine the hits with authoritative sources. Other kb commands (create, add, sources, eval…) go through deeptutor_cli.",
|
|
177
|
+
parameters: {
|
|
178
|
+
action: {
|
|
179
|
+
type: "string",
|
|
180
|
+
enum: ["list", "search", "info"],
|
|
181
|
+
required: true,
|
|
182
|
+
description: "What to do with the knowledge bases",
|
|
183
|
+
},
|
|
184
|
+
kb: { type: "string", description: "Knowledge base name (required for search/info)" },
|
|
185
|
+
query: { type: "string", description: "Search question (required for search)" },
|
|
186
|
+
mode: { type: "string", enum: ["hybrid", "dense", "sparse"], description: "Retrieval mode, default hybrid" },
|
|
187
|
+
top_k: {
|
|
188
|
+
type: "number",
|
|
189
|
+
description: "Number of hits to return, default all (applied locally; the CLI has no --top-k flag)",
|
|
190
|
+
},
|
|
191
|
+
max_chars: { type: "number", description: "Max characters of the returned content, default 30000" },
|
|
192
|
+
},
|
|
193
|
+
async execute(args, signal) {
|
|
194
|
+
const maxChars = args.max_chars ?? 30000;
|
|
195
|
+
if ((args.action === "search" || args.action === "info") && !args.kb) {
|
|
196
|
+
throw new Error(`[deeptutor_kb] action=${args.action} needs kb`);
|
|
197
|
+
}
|
|
198
|
+
if (args.action === "search" && !args.query) throw new Error("[deeptutor_kb] action=search needs query");
|
|
199
|
+
let argv: string[];
|
|
200
|
+
if (args.action === "list") argv = ["kb", "list", "--format", "json"];
|
|
201
|
+
else if (args.action === "info") argv = ["kb", "info", args.kb];
|
|
202
|
+
else argv = ["kb", "search", args.kb, args.query, "--format", "json", "--mode", args.mode ?? "hybrid"];
|
|
203
|
+
const { stdout, stderr, code, killed } = await runCli(argv, { signal, timeoutMs: 90_000 });
|
|
204
|
+
if (killed) throw new Error(configGuide() + "[deeptutor_kb timed out]");
|
|
205
|
+
if (code !== 0) {
|
|
206
|
+
throw new Error(
|
|
207
|
+
`[deeptutor_kb failed] exit=${code}\nstderr: ${truncate(stderr || "(none)", 2000)}` + configGuide(),
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
let body = stdout.trim() || "(empty result)";
|
|
211
|
+
if (args.action === "search") body = formatSearch(stdout, args.top_k) ?? body;
|
|
212
|
+
return { action: args.action, mode: "cli", kb: args.kb, query: args.query, text: truncate(body, maxChars) };
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/** Format `kb search --format json` output; undefined when it is not JSON. Exported for tests. */
|
|
217
|
+
export function formatSearch(stdout: string, topK?: number): string | undefined {
|
|
218
|
+
let parsed: any;
|
|
219
|
+
try {
|
|
220
|
+
parsed = JSON.parse(stdout);
|
|
221
|
+
} catch {
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
const fmtHit = (s: any, i: number) => {
|
|
225
|
+
const text = s.text ?? s.content ?? s.snippet ?? JSON.stringify(s);
|
|
226
|
+
const src = s.source ?? s.doc_name ?? s.file ?? "";
|
|
227
|
+
const title = s.title ? ` title: ${s.title}` : "";
|
|
228
|
+
const score = typeof s.score === "number" ? ` score: ${s.score}` : "";
|
|
229
|
+
return `[${i + 1}]${src ? ` source: ${src}` : ""}${title}${score}\n${typeof text === "string" ? text : JSON.stringify(text)}`;
|
|
230
|
+
};
|
|
231
|
+
// The CLI returns { query, answer, content, sources, provider }; older builds a bare array.
|
|
232
|
+
if (Array.isArray(parsed)) {
|
|
233
|
+
const hits = topK ? parsed.slice(0, topK) : parsed;
|
|
234
|
+
return `${hits.length} hits:\n\n` + hits.map(fmtHit).join("\n\n");
|
|
235
|
+
}
|
|
236
|
+
if (parsed && typeof parsed === "object") {
|
|
237
|
+
const parts: string[] = [];
|
|
238
|
+
if (parsed.query) parts.push(`Query: ${parsed.query}`);
|
|
239
|
+
if (typeof parsed.answer === "string" && parsed.answer.trim()) parts.push(`Answer: ${parsed.answer.trim()}`);
|
|
240
|
+
const sources = Array.isArray(parsed.sources) ? parsed.sources : [];
|
|
241
|
+
const hits = topK ? sources.slice(0, topK) : sources;
|
|
242
|
+
parts.push(`${hits.length} hits:`);
|
|
243
|
+
parts.push(hits.map(fmtHit).join("\n\n"));
|
|
244
|
+
return parts.join("\n\n");
|
|
245
|
+
}
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ---------- deeptutor_note ----------
|
|
250
|
+
|
|
251
|
+
const noteTool: ToolSpec = {
|
|
252
|
+
name: "deeptutor_note",
|
|
253
|
+
description:
|
|
254
|
+
"Archive a Markdown learning note (study plan, deep-solve summary, wrong answers, etc.) to a DeepTutor notebook (auto-creates the notebook if missing). Only when the user asks to save/archive notes.",
|
|
255
|
+
parameters: {
|
|
256
|
+
notebook: { type: "string", required: true, description: "Notebook name, e.g. dotnet-learning (auto-created if missing)" },
|
|
257
|
+
title: { type: "string", required: true, description: "Record title" },
|
|
258
|
+
type: {
|
|
259
|
+
type: "string",
|
|
260
|
+
enum: ["chat", "question", "research", "solve"],
|
|
261
|
+
description: "Record type, default chat",
|
|
262
|
+
},
|
|
263
|
+
content: { type: "string", required: true, description: "Learning note content in Markdown" },
|
|
264
|
+
},
|
|
265
|
+
async execute(args, signal) {
|
|
266
|
+
const recordType = (args.type ?? "chat") as string;
|
|
267
|
+
const nbName = String(args.notebook).trim();
|
|
268
|
+
const { stdout, stderr, code } = SSH_HOST
|
|
269
|
+
? await cliNoteRemote(nbName, args.title, recordType, args.content, signal)
|
|
270
|
+
: await cliNoteLocal(nbName, args.title, recordType, args.content, signal);
|
|
271
|
+
if (code !== 0) {
|
|
272
|
+
throw new Error(`[deeptutor_note failed] exit=${code}\n${truncate(stderr || stdout, 2000)}` + configGuide());
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
notebook: nbName,
|
|
276
|
+
title: args.title,
|
|
277
|
+
mode: "cli",
|
|
278
|
+
text:
|
|
279
|
+
configHint() +
|
|
280
|
+
`[note archived] notebook=${nbName}, title=${args.title}, type=${recordType}\n${truncate((stdout ?? "").trim(), 2000)}`,
|
|
281
|
+
};
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// ---------- deeptutor_cli ----------
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Rich pads every line to the terminal width; drop that padding and blank-line
|
|
289
|
+
* runs so wide output does not waste the model's context. Pure; exported for tests.
|
|
290
|
+
*/
|
|
291
|
+
export function tidyOutput(s: string): string {
|
|
292
|
+
return s
|
|
293
|
+
.split(/\r?\n/)
|
|
294
|
+
.map((l) => l.trimEnd())
|
|
295
|
+
.join("\n")
|
|
296
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
297
|
+
.trim();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Wider than Rich's 80-column default so list tables keep more of each cell,
|
|
302
|
+
* but not so wide that help boxes (which always span the full width) bloat.
|
|
303
|
+
*/
|
|
304
|
+
const CLI_ENV = { COLUMNS: "120", NO_COLOR: "1", TERM: "dumb" };
|
|
305
|
+
|
|
306
|
+
const cliTool: ToolSpec = {
|
|
307
|
+
name: "deeptutor_cli",
|
|
308
|
+
description:
|
|
309
|
+
"Run any DeepTutor CLI command: `deeptutor <args>` (locally or over SSH, same as the other tools). Use it for everything the dedicated tools do not cover, and discover commands from the CLI itself instead of guessing: args=[\"--help\"], then [\"<command>\",\"--help\"]. Read-only commands (help, list/show/info/search/state/status, doctor, run) execute directly. Any other command may change DeepTutor state and needs confirm=true, which you may only set after the user explicitly asked for that change. Destructive commands (delete, remove, clear, uninstall, rollback…) and interactive ones (chat, start, serve, init, login) are refused; tell the user to run those in a terminal.",
|
|
310
|
+
parameters: {
|
|
311
|
+
args: {
|
|
312
|
+
type: "array",
|
|
313
|
+
items: { type: "string" },
|
|
314
|
+
required: true,
|
|
315
|
+
description: "Arguments after `deeptutor`, one per item, e.g. [\"kb\",\"list-sources\",\"dotnet\"] or [\"notebook\",\"show\",\"sqlserver-learning\"]",
|
|
316
|
+
},
|
|
317
|
+
confirm: {
|
|
318
|
+
type: "boolean",
|
|
319
|
+
description: "Set true only when the user explicitly asked for this state-changing command",
|
|
320
|
+
},
|
|
321
|
+
timeout: { type: "number", description: "Timeout in seconds, default 120" },
|
|
322
|
+
max_chars: { type: "number", description: "Max characters of output returned, default 30000" },
|
|
323
|
+
},
|
|
324
|
+
async execute(args, signal) {
|
|
325
|
+
const argv = (args.args as string[]).map(String);
|
|
326
|
+
const verdict = classifyCliArgs(argv);
|
|
327
|
+
const shown = `deeptutor ${argv.join(" ")}`.trim();
|
|
328
|
+
if (verdict.tier === "blocked") throw new Error(`[deeptutor_cli refused] ${shown}: ${verdict.reason}`);
|
|
329
|
+
if (verdict.tier === "confirm" && args.confirm !== true) {
|
|
330
|
+
throw new Error(
|
|
331
|
+
`[deeptutor_cli needs confirmation] ${shown}: ${verdict.reason}. ` +
|
|
332
|
+
"Ask the user; retry with confirm=true only after they explicitly approve this command.",
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
const maxChars = args.max_chars ?? 30000;
|
|
336
|
+
const { stdout, stderr, code, killed } = await runCli(argv, {
|
|
337
|
+
signal,
|
|
338
|
+
timeoutMs: (args.timeout ?? 120) * 1000,
|
|
339
|
+
env: CLI_ENV,
|
|
340
|
+
});
|
|
341
|
+
if (killed) throw new Error(signal.aborted ? "[deeptutor_cli cancelled]" : `[deeptutor_cli timed out] ${shown}`);
|
|
342
|
+
const out = tidyOutput([stdout, stderr ? `--- stderr ---\n${stderr}` : ""].filter(Boolean).join("\n"));
|
|
343
|
+
const text = `[${shown}] exit=${code}\n${truncate(out || "(no output)", maxChars)}`;
|
|
344
|
+
if (code !== 0) throw new Error(code === 127 ? text + configGuide() : text);
|
|
345
|
+
return { args: argv, tier: verdict.tier, exitCode: code, text };
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
export const TOOLS: readonly ToolSpec[] = [runTool, kbTool, noteTool, cliTool];
|