ningzhi-agent-cli 0.0.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/CLAUDE.md +197 -0
- package/README.md +35 -0
- package/dist/agent/agent.d.ts +24 -0
- package/dist/agent/agent.js +432 -0
- package/dist/agent/cli.d.ts +1 -0
- package/dist/agent/cli.js +209 -0
- package/dist/agent/colors.d.ts +19 -0
- package/dist/agent/colors.js +92 -0
- package/dist/agent/commands.d.ts +7 -0
- package/dist/agent/commands.js +73 -0
- package/dist/agent/config.d.ts +38 -0
- package/dist/agent/config.js +119 -0
- package/dist/agent/context.d.ts +17 -0
- package/dist/agent/context.js +146 -0
- package/dist/agent/db.d.ts +25 -0
- package/dist/agent/db.js +185 -0
- package/dist/agent/hooks/engine.d.ts +27 -0
- package/dist/agent/hooks/engine.js +122 -0
- package/dist/agent/hooks/index.d.ts +1 -0
- package/dist/agent/hooks/index.js +11 -0
- package/dist/agent/mcp/client.d.ts +31 -0
- package/dist/agent/mcp/client.js +138 -0
- package/dist/agent/mcp/index.d.ts +4 -0
- package/dist/agent/mcp/index.js +27 -0
- package/dist/agent/mcp/wrapper.d.ts +7 -0
- package/dist/agent/mcp/wrapper.js +26 -0
- package/dist/agent/model.d.ts +5 -0
- package/dist/agent/model.js +35 -0
- package/dist/agent/permission/dangerous-path.json +115 -0
- package/dist/agent/permission/exec.d.ts +14 -0
- package/dist/agent/permission/exec.js +35 -0
- package/dist/agent/permission/is-dangerous-path.d.ts +8 -0
- package/dist/agent/permission/is-dangerous-path.js +96 -0
- package/dist/agent/permission/is-safe-domains.d.ts +1 -0
- package/dist/agent/permission/is-safe-domains.js +145 -0
- package/dist/agent/permission/network.d.ts +8 -0
- package/dist/agent/permission/network.js +14 -0
- package/dist/agent/permission/read.d.ts +9 -0
- package/dist/agent/permission/read.js +17 -0
- package/dist/agent/permission/util.d.ts +11 -0
- package/dist/agent/permission/util.js +347 -0
- package/dist/agent/permission/write.d.ts +11 -0
- package/dist/agent/permission/write.js +21 -0
- package/dist/agent/prompt.d.ts +1 -0
- package/dist/agent/prompt.js +52 -0
- package/dist/agent/skills/oppose/SKILL.md +100 -0
- package/dist/agent/skills/planner/SKILL.md +90 -0
- package/dist/agent/skills.d.ts +8 -0
- package/dist/agent/skills.js +103 -0
- package/dist/agent/tools/agent_tool.d.ts +3 -0
- package/dist/agent/tools/agent_tool.js +44 -0
- package/dist/agent/tools/exec_tool.d.ts +3 -0
- package/dist/agent/tools/exec_tool.js +33 -0
- package/dist/agent/tools/load_skill_tool.d.ts +3 -0
- package/dist/agent/tools/load_skill_tool.js +16 -0
- package/dist/agent/tools/memory_create_tool.d.ts +6 -0
- package/dist/agent/tools/memory_create_tool.js +39 -0
- package/dist/agent/tools/memory_delete_tool.d.ts +3 -0
- package/dist/agent/tools/memory_delete_tool.js +28 -0
- package/dist/agent/tools/memory_retrieve_tool.d.ts +4 -0
- package/dist/agent/tools/memory_retrieve_tool.js +26 -0
- package/dist/agent/tools/profile_update_tool.d.ts +3 -0
- package/dist/agent/tools/profile_update_tool.js +51 -0
- package/dist/agent/tools/read_file_tool.d.ts +3 -0
- package/dist/agent/tools/read_file_tool.js +16 -0
- package/dist/agent/tools/run_js_tool.d.ts +3 -0
- package/dist/agent/tools/run_js_tool.js +47 -0
- package/dist/agent/tools/run_py_tool.d.ts +3 -0
- package/dist/agent/tools/run_py_tool.js +37 -0
- package/dist/agent/tools/web_fetch_tool.d.ts +3 -0
- package/dist/agent/tools/web_fetch_tool.js +28 -0
- package/dist/agent/tools/web_search_tool.d.ts +3 -0
- package/dist/agent/tools/web_search_tool.js +38 -0
- package/dist/agent/tools/write_file_tool.d.ts +4 -0
- package/dist/agent/tools/write_file_tool.js +15 -0
- package/dist/agent/tools.d.ts +14 -0
- package/dist/agent/tools.js +218 -0
- package/dist/agent/utils.d.ts +11 -0
- package/dist/agent/utils.js +43 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +81 -0
- package/dist/install.d.ts +1 -0
- package/dist/install.js +150 -0
- package/package.json +56 -0
package/dist/agent/db.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DB_PATH = void 0;
|
|
7
|
+
exports.searchMemories = searchMemories;
|
|
8
|
+
exports.threadIdExists = threadIdExists;
|
|
9
|
+
exports.initDb = initDb;
|
|
10
|
+
exports.listRecentSessions = listRecentSessions;
|
|
11
|
+
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
12
|
+
const node_fs_1 = require("node:fs");
|
|
13
|
+
const node_os_1 = require("node:os");
|
|
14
|
+
const node_path_1 = require("node:path");
|
|
15
|
+
const utils_1 = require("./utils");
|
|
16
|
+
exports.DB_PATH = (0, node_path_1.join)((0, node_os_1.homedir)(), ".ningzhiAgentCli", ".dbData", "checkpointer.db");
|
|
17
|
+
// recursive 多层路径创建,且目录已存在不报错,幂等性
|
|
18
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(exports.DB_PATH), { recursive: true });
|
|
19
|
+
// 搜索记忆
|
|
20
|
+
function searchMemories(query, limit = 10) {
|
|
21
|
+
const trimmedQueries = query.map((q) => q.trim()).filter((q) => q.length > 0);
|
|
22
|
+
if (trimmedQueries.length === 0) {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
const queryStr = trimmedQueries.join(" OR ").replace(/-/g, " ");
|
|
26
|
+
const db = new better_sqlite3_1.default(exports.DB_PATH);
|
|
27
|
+
try {
|
|
28
|
+
const rows = db
|
|
29
|
+
.prepare(`
|
|
30
|
+
WITH ranked AS (
|
|
31
|
+
SELECT
|
|
32
|
+
m.*,
|
|
33
|
+
-bm25(memory_fts, 10.0, 5.0) AS relevance_score,
|
|
34
|
+
(m.importance * 0.3) AS importance_score,
|
|
35
|
+
(
|
|
36
|
+
1.0 / (
|
|
37
|
+
1.0 +
|
|
38
|
+
((strftime('%s','now') - strftime('%s', m.updated_at)) / 86400.0)
|
|
39
|
+
)
|
|
40
|
+
) AS time_score
|
|
41
|
+
FROM memory_fts
|
|
42
|
+
JOIN memory m ON m.id = memory_fts.rowid
|
|
43
|
+
WHERE memory_fts MATCH ?
|
|
44
|
+
)
|
|
45
|
+
SELECT *,
|
|
46
|
+
(
|
|
47
|
+
relevance_score * 0.6 +
|
|
48
|
+
importance_score * 0.3 +
|
|
49
|
+
time_score * 0.1
|
|
50
|
+
) AS final_score
|
|
51
|
+
FROM ranked
|
|
52
|
+
ORDER BY final_score DESC
|
|
53
|
+
LIMIT ?
|
|
54
|
+
`)
|
|
55
|
+
.all(queryStr, limit);
|
|
56
|
+
return rows.map((r) => ({
|
|
57
|
+
id: r.id,
|
|
58
|
+
type: r.type,
|
|
59
|
+
content: r.content,
|
|
60
|
+
keywords: r.keywords,
|
|
61
|
+
importance: r.importance,
|
|
62
|
+
session_id: r.session_id,
|
|
63
|
+
created_at: r.created_at,
|
|
64
|
+
updated_at: r.updated_at,
|
|
65
|
+
final_score: r.final_score,
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
db.close();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// 检查会话id是否存在
|
|
73
|
+
function threadIdExists(threadId) {
|
|
74
|
+
const db = new better_sqlite3_1.default(exports.DB_PATH);
|
|
75
|
+
try {
|
|
76
|
+
const row = db
|
|
77
|
+
.prepare(`
|
|
78
|
+
SELECT 1 FROM checkpoints WHERE thread_id = ? LIMIT 1
|
|
79
|
+
`)
|
|
80
|
+
.get(threadId);
|
|
81
|
+
return !!row;
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
db.close();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// 初始化数据库
|
|
88
|
+
function initDb() {
|
|
89
|
+
const db = new better_sqlite3_1.default(exports.DB_PATH);
|
|
90
|
+
try {
|
|
91
|
+
db.exec(`
|
|
92
|
+
CREATE TABLE IF NOT EXISTS memory (
|
|
93
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
94
|
+
type TEXT NOT NULL,
|
|
95
|
+
content TEXT NOT NULL,
|
|
96
|
+
keywords TEXT,
|
|
97
|
+
importance INTEGER DEFAULT 3,
|
|
98
|
+
session_id TEXT,
|
|
99
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
100
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
101
|
+
)
|
|
102
|
+
`);
|
|
103
|
+
// SQLite 的 FTS(Full-Text Search)模块是一种虚拟表模块,支持高效的全文搜索功能。
|
|
104
|
+
// FTS5 是 SQLite 最新的全文搜索引擎,提供了强大的功能和灵活性。
|
|
105
|
+
db.exec(`
|
|
106
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(
|
|
107
|
+
content,
|
|
108
|
+
keywords,
|
|
109
|
+
content='memory', content_rowid='id'
|
|
110
|
+
)
|
|
111
|
+
`);
|
|
112
|
+
db.exec(`
|
|
113
|
+
CREATE TRIGGER IF NOT EXISTS memory_fts_insert AFTER INSERT ON memory BEGIN
|
|
114
|
+
INSERT INTO memory_fts(rowid, content, keywords)
|
|
115
|
+
VALUES (new.id, new.content, new.keywords);
|
|
116
|
+
END;
|
|
117
|
+
|
|
118
|
+
CREATE TRIGGER IF NOT EXISTS memory_fts_delete AFTER DELETE ON memory BEGIN
|
|
119
|
+
INSERT INTO memory_fts(memory_fts, rowid, content, keywords)
|
|
120
|
+
VALUES ('delete', old.id, old.content, old.keywords);
|
|
121
|
+
END;
|
|
122
|
+
|
|
123
|
+
CREATE TRIGGER IF NOT EXISTS memory_fts_update AFTER UPDATE ON memory BEGIN
|
|
124
|
+
INSERT INTO memory_fts(memory_fts, rowid, content, keywords)
|
|
125
|
+
VALUES ('delete', old.id, old.content, old.keywords);
|
|
126
|
+
INSERT INTO memory_fts(rowid, content, keywords)
|
|
127
|
+
VALUES (new.id, new.content, new.keywords);
|
|
128
|
+
END;
|
|
129
|
+
`);
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
db.close();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* 列出最近 10 个会话(线程)
|
|
137
|
+
* @returns 会话列表
|
|
138
|
+
*/
|
|
139
|
+
function listRecentSessions() {
|
|
140
|
+
const db = new better_sqlite3_1.default(exports.DB_PATH);
|
|
141
|
+
try {
|
|
142
|
+
// 查询最近 10 个会话(线程):取每个线程的最后活跃时间,以及最近一条用户提问
|
|
143
|
+
const rows = db
|
|
144
|
+
.prepare(`
|
|
145
|
+
-- CTE:取每个线程最新的 checkpoint 及其 ts
|
|
146
|
+
WITH thread_last AS (
|
|
147
|
+
SELECT
|
|
148
|
+
thread_id,
|
|
149
|
+
MAX(json_extract(CAST(checkpoint AS TEXT), '$.ts')) AS last_ts,
|
|
150
|
+
(SELECT CAST(c2.checkpoint AS TEXT)
|
|
151
|
+
FROM checkpoints c2
|
|
152
|
+
WHERE c2.thread_id = c.thread_id
|
|
153
|
+
ORDER BY json_extract(CAST(c2.checkpoint AS TEXT), '$.ts') DESC
|
|
154
|
+
LIMIT 1) AS last_checkpoint
|
|
155
|
+
FROM checkpoints c
|
|
156
|
+
GROUP BY thread_id
|
|
157
|
+
)
|
|
158
|
+
SELECT
|
|
159
|
+
t.thread_id,
|
|
160
|
+
-- 从最新 checkpoint 的 messages 数组中,取最后一条用户消息的内容
|
|
161
|
+
(SELECT COALESCE(
|
|
162
|
+
json_extract(j.value, '$.kwargs.content'),
|
|
163
|
+
json_extract(j.value, '$.content')
|
|
164
|
+
)
|
|
165
|
+
FROM json_each(json_extract(t.last_checkpoint, '$.channel_values.messages')) j
|
|
166
|
+
WHERE json_extract(j.value, '$.id[2]') = 'HumanMessage'
|
|
167
|
+
OR json_extract(j.value, '$.type') = 'human'
|
|
168
|
+
ORDER BY CAST(j.key AS INTEGER) DESC
|
|
169
|
+
LIMIT 1) AS last_question,
|
|
170
|
+
t.last_ts
|
|
171
|
+
FROM thread_last t
|
|
172
|
+
ORDER BY t.last_ts DESC -- 按最后活跃时间倒序(最新的在前)
|
|
173
|
+
LIMIT 10 -- 只取最近 10 个会话
|
|
174
|
+
`)
|
|
175
|
+
.all();
|
|
176
|
+
return rows.map((r) => ({
|
|
177
|
+
thread_id: r.thread_id,
|
|
178
|
+
last_question: (0, utils_1.truncate)(r.last_question, 50),
|
|
179
|
+
last_ts: (0, utils_1.formatRelativeTime)(r.last_ts),
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
db.close();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type HookConfig, type HooksConfig } from "../config";
|
|
2
|
+
export type { HookConfig, HooksConfig };
|
|
3
|
+
export type HookResult = {
|
|
4
|
+
action: "continue";
|
|
5
|
+
} | {
|
|
6
|
+
action: "block";
|
|
7
|
+
reason: string;
|
|
8
|
+
} | {
|
|
9
|
+
action: "inject";
|
|
10
|
+
message: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function loadHooksConfig(): HooksConfig;
|
|
13
|
+
export declare function clearHooksCache(): void;
|
|
14
|
+
export declare function matchHooks(hooks: HookConfig[] | undefined, toolName: string): HookConfig[];
|
|
15
|
+
export declare function runHook(hook: HookConfig, env: Record<string, string>, timeout?: number): Promise<HookResult>;
|
|
16
|
+
export interface ToolHookContext {
|
|
17
|
+
toolName: string;
|
|
18
|
+
toolArgs: any;
|
|
19
|
+
toolCallId: string;
|
|
20
|
+
threadId: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function runPreToolUseHooks(context: ToolHookContext): Promise<HookResult>;
|
|
23
|
+
export interface PostToolHookContext extends ToolHookContext {
|
|
24
|
+
toolOutput: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function runPostToolUseHooks(context: PostToolHookContext): Promise<HookResult>;
|
|
27
|
+
export declare function runSessionStartHooks(threadId: string): Promise<void>;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.loadHooksConfig = loadHooksConfig;
|
|
7
|
+
exports.clearHooksCache = clearHooksCache;
|
|
8
|
+
exports.matchHooks = matchHooks;
|
|
9
|
+
exports.runHook = runHook;
|
|
10
|
+
exports.runPreToolUseHooks = runPreToolUseHooks;
|
|
11
|
+
exports.runPostToolUseHooks = runPostToolUseHooks;
|
|
12
|
+
exports.runSessionStartHooks = runSessionStartHooks;
|
|
13
|
+
const node_child_process_1 = require("node:child_process");
|
|
14
|
+
const node_util_1 = require("node:util");
|
|
15
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
16
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
17
|
+
// 配置目录,hook 命令中的相对路径(如 ./hooks/xxx)相对于此目录解析
|
|
18
|
+
const CONFIG_DIR = node_path_1.default.join(node_os_1.default.homedir(), ".ningzhiAgentCli");
|
|
19
|
+
const config_1 = require("../config");
|
|
20
|
+
const execAsync = (0, node_util_1.promisify)(node_child_process_1.exec);
|
|
21
|
+
function loadHooksConfig() {
|
|
22
|
+
return (0, config_1.getHooksConfig)();
|
|
23
|
+
}
|
|
24
|
+
function clearHooksCache() {
|
|
25
|
+
(0, config_1.clearConfigCache)();
|
|
26
|
+
}
|
|
27
|
+
function matchHooks(hooks, toolName) {
|
|
28
|
+
if (!hooks)
|
|
29
|
+
return [];
|
|
30
|
+
return hooks.filter((h) => h.matcher === "*" || toolName.includes(h.matcher));
|
|
31
|
+
}
|
|
32
|
+
async function runHook(hook, env, timeout = 30000) {
|
|
33
|
+
const hookType = env.NINGZHI_HOOK_TYPE || "hook";
|
|
34
|
+
console.log(`[Hook ${hookType}] ${hook.command}`);
|
|
35
|
+
try {
|
|
36
|
+
await execAsync(hook.command, {
|
|
37
|
+
cwd: CONFIG_DIR,
|
|
38
|
+
env: { ...process.env, ...env },
|
|
39
|
+
timeout,
|
|
40
|
+
});
|
|
41
|
+
return { action: "continue" };
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
const code = error.code;
|
|
45
|
+
const stderr = error.stderr || "";
|
|
46
|
+
if (code === 1) {
|
|
47
|
+
return { action: "block", reason: stderr }; // 工具不执行,stderr 作为错误返回 ===> 通用未知错误
|
|
48
|
+
}
|
|
49
|
+
else if (code === 2) {
|
|
50
|
+
return { action: "inject", message: stderr }; // stderr 注入对话,工具仍执行 ===> 非法参数 / 命令语法错误
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
action: "block",
|
|
54
|
+
reason: stderr || `Hook exited with code ${code}`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function runPreToolUseHooks(context) {
|
|
59
|
+
const config = loadHooksConfig();
|
|
60
|
+
const hooks = matchHooks(config.hooks.PreToolUse, context.toolName);
|
|
61
|
+
const injectMessages = [];
|
|
62
|
+
for (const hook of hooks) {
|
|
63
|
+
const result = await runHook(hook, {
|
|
64
|
+
NINGZHI_HOOK_TYPE: "PreToolUse",
|
|
65
|
+
NINGZHI_TOOL_NAME: context.toolName,
|
|
66
|
+
NINGZHI_TOOL_ARGS: JSON.stringify(context.toolArgs),
|
|
67
|
+
NINGZHI_TOOL_CALL_ID: context.toolCallId,
|
|
68
|
+
NINGZHI_THREAD_ID: context.threadId,
|
|
69
|
+
});
|
|
70
|
+
if (result.action === "block") {
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
if (result.action === "inject") {
|
|
74
|
+
injectMessages.push(result.message);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (injectMessages.length > 0) {
|
|
78
|
+
return { action: "inject", message: injectMessages.join("\n") };
|
|
79
|
+
}
|
|
80
|
+
return { action: "continue" };
|
|
81
|
+
}
|
|
82
|
+
async function runPostToolUseHooks(context) {
|
|
83
|
+
const config = loadHooksConfig();
|
|
84
|
+
const hooks = matchHooks(config.hooks.PostToolUse, context.toolName);
|
|
85
|
+
const injectMessages = [];
|
|
86
|
+
for (const hook of hooks) {
|
|
87
|
+
const result = await runHook(hook, {
|
|
88
|
+
NINGZHI_HOOK_TYPE: "PostToolUse",
|
|
89
|
+
NINGZHI_TOOL_NAME: context.toolName,
|
|
90
|
+
NINGZHI_TOOL_ARGS: JSON.stringify(context.toolArgs),
|
|
91
|
+
NINGZHI_TOOL_OUTPUT: context.toolOutput,
|
|
92
|
+
NINGZHI_TOOL_CALL_ID: context.toolCallId,
|
|
93
|
+
NINGZHI_THREAD_ID: context.threadId,
|
|
94
|
+
});
|
|
95
|
+
if (result.action === "block") {
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
if (result.action === "inject") {
|
|
99
|
+
injectMessages.push(result.message);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (injectMessages.length > 0) {
|
|
103
|
+
return { action: "inject", message: injectMessages.join("\n") };
|
|
104
|
+
}
|
|
105
|
+
return { action: "continue" };
|
|
106
|
+
}
|
|
107
|
+
async function runSessionStartHooks(threadId) {
|
|
108
|
+
const config = loadHooksConfig();
|
|
109
|
+
const hooks = config.hooks.SessionStart || [];
|
|
110
|
+
for (const hook of hooks) {
|
|
111
|
+
const result = await runHook(hook, {
|
|
112
|
+
NINGZHI_HOOK_TYPE: "SessionStart",
|
|
113
|
+
NINGZHI_THREAD_ID: threadId,
|
|
114
|
+
});
|
|
115
|
+
if (result.action === "block") {
|
|
116
|
+
console.error(`[SessionStart hook blocked] ${result.reason}`);
|
|
117
|
+
}
|
|
118
|
+
else if (result.action === "inject") {
|
|
119
|
+
console.log(result.message);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { loadHooksConfig, clearHooksCache, matchHooks, runHook, runPreToolUseHooks, runPostToolUseHooks, runSessionStartHooks, type HookConfig, type HooksConfig, type HookResult, type ToolHookContext, type PostToolHookContext, } from "./engine";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runSessionStartHooks = exports.runPostToolUseHooks = exports.runPreToolUseHooks = exports.runHook = exports.matchHooks = exports.clearHooksCache = exports.loadHooksConfig = void 0;
|
|
4
|
+
var engine_1 = require("./engine");
|
|
5
|
+
Object.defineProperty(exports, "loadHooksConfig", { enumerable: true, get: function () { return engine_1.loadHooksConfig; } });
|
|
6
|
+
Object.defineProperty(exports, "clearHooksCache", { enumerable: true, get: function () { return engine_1.clearHooksCache; } });
|
|
7
|
+
Object.defineProperty(exports, "matchHooks", { enumerable: true, get: function () { return engine_1.matchHooks; } });
|
|
8
|
+
Object.defineProperty(exports, "runHook", { enumerable: true, get: function () { return engine_1.runHook; } });
|
|
9
|
+
Object.defineProperty(exports, "runPreToolUseHooks", { enumerable: true, get: function () { return engine_1.runPreToolUseHooks; } });
|
|
10
|
+
Object.defineProperty(exports, "runPostToolUseHooks", { enumerable: true, get: function () { return engine_1.runPostToolUseHooks; } });
|
|
11
|
+
Object.defineProperty(exports, "runSessionStartHooks", { enumerable: true, get: function () { return engine_1.runSessionStartHooks; } });
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
3
|
+
export interface McpServerConfig {
|
|
4
|
+
command?: string;
|
|
5
|
+
args?: string[];
|
|
6
|
+
env?: Record<string, string>;
|
|
7
|
+
url?: string;
|
|
8
|
+
headers?: Record<string, string>;
|
|
9
|
+
}
|
|
10
|
+
export interface McpToolInfo {
|
|
11
|
+
name: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
inputSchema: object;
|
|
14
|
+
}
|
|
15
|
+
export interface McpServerConnection {
|
|
16
|
+
name: string;
|
|
17
|
+
client: Client;
|
|
18
|
+
transport: Transport;
|
|
19
|
+
tools: McpToolInfo[];
|
|
20
|
+
}
|
|
21
|
+
export declare function loadMcpConfig(): Record<string, McpServerConfig>;
|
|
22
|
+
export declare function connectMcpServer(name: string, config: McpServerConfig): Promise<McpServerConnection>;
|
|
23
|
+
export declare function initializeMcpClients(): Promise<McpServerConnection[]>;
|
|
24
|
+
/**
|
|
25
|
+
* 调用指定 MCP 服务器上的工具,返回工具输出的文本内容。
|
|
26
|
+
* @param client 已连接的 MCP 客户端
|
|
27
|
+
* @param name 工具名称
|
|
28
|
+
* @param args 传给工具的参数
|
|
29
|
+
*/
|
|
30
|
+
export declare function callMcpTool(client: Client, name: string, args: Record<string, unknown>): Promise<string>;
|
|
31
|
+
export declare function disconnectAllMcpClients(connections: McpServerConnection[]): Promise<void>;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loadMcpConfig = loadMcpConfig;
|
|
4
|
+
exports.connectMcpServer = connectMcpServer;
|
|
5
|
+
exports.initializeMcpClients = initializeMcpClients;
|
|
6
|
+
exports.callMcpTool = callMcpTool;
|
|
7
|
+
exports.disconnectAllMcpClients = disconnectAllMcpClients;
|
|
8
|
+
const index_js_1 = require("@modelcontextprotocol/sdk/client/index.js");
|
|
9
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/client/stdio.js");
|
|
10
|
+
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
|
|
11
|
+
const colors_1 = require("../colors");
|
|
12
|
+
const config_1 = require("../config");
|
|
13
|
+
const CONNECT_TIMEOUT_MS = 60000;
|
|
14
|
+
function loadMcpConfig() {
|
|
15
|
+
return (0, config_1.getMCPServerConfig)();
|
|
16
|
+
}
|
|
17
|
+
async function withTimeout(promise, ms, label) {
|
|
18
|
+
return Promise.race([
|
|
19
|
+
promise,
|
|
20
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms)),
|
|
21
|
+
]);
|
|
22
|
+
}
|
|
23
|
+
// 根据配置创建 MCP 服务器的传输层,根据配置是使用 HTTP 还是 Stdio 连接。
|
|
24
|
+
// @param config - MCP 服务器的配置对象,包含连接信息和工具列表。
|
|
25
|
+
// @returns - 创建的 MCP 服务器传输层实例
|
|
26
|
+
function createMcpTransport(config) {
|
|
27
|
+
if (config.url) {
|
|
28
|
+
const url = new URL(config.url);
|
|
29
|
+
// `RequestInit` 是 Fetch API 中描述`fetch()` 请求配置项的接口类型,
|
|
30
|
+
// 包含`method` 、`headers` 、`body` 、`credentials` 等字段
|
|
31
|
+
// `@types/node` 已经把它声明为全局类型,所以不用导入 ==》 tsconfig.json 中的 compilerOptions 的"types": ["node", "jest"],
|
|
32
|
+
const requestInit = config.headers
|
|
33
|
+
? { headers: config.headers }
|
|
34
|
+
: undefined;
|
|
35
|
+
return new streamableHttp_js_1.StreamableHTTPClientTransport(url, { requestInit });
|
|
36
|
+
}
|
|
37
|
+
// 如果配置了`command`,则使用`StdioClientTransport`
|
|
38
|
+
if (config.command) {
|
|
39
|
+
return new stdio_js_1.StdioClientTransport({
|
|
40
|
+
command: config.command,
|
|
41
|
+
args: config.args,
|
|
42
|
+
env: config.env,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
throw new Error('Invalid MCP server config: must have either "command" or "url"');
|
|
46
|
+
}
|
|
47
|
+
async function connectMcpServer(name, config) {
|
|
48
|
+
const transport = createMcpTransport(config);
|
|
49
|
+
const client = new index_js_1.Client({ name: "ningzhi", version: "0.0.1" }, { capabilities: {} });
|
|
50
|
+
await withTimeout(client.connect(transport), // 通过传输层连接到 MCP 服务器
|
|
51
|
+
CONNECT_TIMEOUT_MS, `MCP server "${name}" connect`);
|
|
52
|
+
const toolsResult = await withTimeout(client.listTools(), CONNECT_TIMEOUT_MS, `MCP server "${name}" listTools`);
|
|
53
|
+
const tools = (toolsResult.tools || []).map((t) => ({
|
|
54
|
+
name: t.name,
|
|
55
|
+
description: t.description,
|
|
56
|
+
inputSchema: t.inputSchema,
|
|
57
|
+
}));
|
|
58
|
+
return { name, client, transport, tools };
|
|
59
|
+
}
|
|
60
|
+
// 初始化 MCP 客户端连接,根据配置文件中的服务器列表连接到所有 MCP 服务器。
|
|
61
|
+
// @returns 已连接的 MCP 服务器连接列表。
|
|
62
|
+
async function initializeMcpClients() {
|
|
63
|
+
const config = loadMcpConfig();
|
|
64
|
+
const entries = Object.entries(config); // [ [ "playwright", {...} ], [ "key", {...} ] ]
|
|
65
|
+
if (entries.length === 0) {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
console.log(colors_1.color.info(`[MCP] Connecting to ${entries.length} server(s)...`));
|
|
69
|
+
// allSettled 它会 等待所有传入的 Promise 都完成 (无论成功还是失败),然后返回一个结果数组。
|
|
70
|
+
const results = await Promise.allSettled(entries.map(([name, cfg]) => connectMcpServer(name, cfg).catch((err) => {
|
|
71
|
+
const msg = err.message || "";
|
|
72
|
+
const hint = msg.includes("timed out")
|
|
73
|
+
? " (Hint: if using npx, the first run may need to download the package. Consider pre-installing with `npm install -g <package>` or increasing timeout.)"
|
|
74
|
+
: "";
|
|
75
|
+
console.error(colors_1.color.error(`[MCP] Failed to connect "${name}": ${msg}${hint}`));
|
|
76
|
+
throw err;
|
|
77
|
+
})));
|
|
78
|
+
const connections = [];
|
|
79
|
+
for (let i = 0; i < results.length; i++) {
|
|
80
|
+
const result = results[i];
|
|
81
|
+
if (result.status === "fulfilled") {
|
|
82
|
+
const conn = result.value;
|
|
83
|
+
console.log(colors_1.color.good(`[MCP] Connected "${conn.name}" with ${conn.tools.length} tool(s)`));
|
|
84
|
+
connections.push(conn);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
console.error(colors_1.color.error(`[MCP] Skipped "${entries[i][0]}" due to connection error`));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return connections;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 调用指定 MCP 服务器上的工具,返回工具输出的文本内容。
|
|
94
|
+
* @param client 已连接的 MCP 客户端
|
|
95
|
+
* @param name 工具名称
|
|
96
|
+
* @param args 传给工具的参数
|
|
97
|
+
*/
|
|
98
|
+
async function callMcpTool(client, name, args) {
|
|
99
|
+
// 向 MCP 服务器发起 tool call 请求
|
|
100
|
+
const result = await client.callTool({ name, arguments: args });
|
|
101
|
+
// MCP 协议中 isError 为 true 表示工具执行失败,需抛出错误让上层处理
|
|
102
|
+
if (result.isError) {
|
|
103
|
+
const text = extractTextFromResult(result);
|
|
104
|
+
throw new Error(text || `MCP tool "${name}" returned an error`);
|
|
105
|
+
}
|
|
106
|
+
// 成功时,从返回结果中提取文本内容(可能有多段 text,用换行拼接)
|
|
107
|
+
return extractTextFromResult(result);
|
|
108
|
+
}
|
|
109
|
+
// 从 MCP 服务器返回的工具调用结果中提取文本内容(可能有多段 text,用换行拼接)
|
|
110
|
+
function extractTextFromResult(result) {
|
|
111
|
+
// 如果结果中没有 content 字段,返回原始结果
|
|
112
|
+
if (!result.content || !Array.isArray(result.content)) {
|
|
113
|
+
return JSON.stringify(result);
|
|
114
|
+
}
|
|
115
|
+
// 从结果中提取所有 text 类型的项,将它们的文本内容用换行拼接起来
|
|
116
|
+
const texts = [];
|
|
117
|
+
for (const item of result.content) {
|
|
118
|
+
if (item.type === "text" && typeof item.text === "string") {
|
|
119
|
+
texts.push(item.text);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// 如果结果中没有 text 类型的项,返回原始结果
|
|
123
|
+
if (texts.length === 0) {
|
|
124
|
+
return JSON.stringify(result);
|
|
125
|
+
}
|
|
126
|
+
// 否则,返回所有 text 类型项的文本内容,用换行拼接
|
|
127
|
+
return texts.join("\n");
|
|
128
|
+
}
|
|
129
|
+
async function disconnectAllMcpClients(connections) {
|
|
130
|
+
await Promise.allSettled(connections.map(async (conn) => {
|
|
131
|
+
try {
|
|
132
|
+
await conn.transport.close();
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// ignore
|
|
136
|
+
}
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.initMcpTools = initMcpTools;
|
|
4
|
+
exports.getMcpTools = getMcpTools;
|
|
5
|
+
exports.shutdownMcp = shutdownMcp;
|
|
6
|
+
const client_1 = require("./client");
|
|
7
|
+
const wrapper_1 = require("./wrapper");
|
|
8
|
+
let mcpConnections = [];
|
|
9
|
+
let mcpTools = [];
|
|
10
|
+
async function initMcpTools() {
|
|
11
|
+
mcpConnections = await (0, client_1.initializeMcpClients)();
|
|
12
|
+
mcpTools = [];
|
|
13
|
+
for (const conn of mcpConnections) {
|
|
14
|
+
for (const mcpTool of conn.tools) {
|
|
15
|
+
mcpTools.push((0, wrapper_1.wrapMcpTool)(conn, mcpTool));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return mcpTools;
|
|
19
|
+
}
|
|
20
|
+
function getMcpTools() {
|
|
21
|
+
return mcpTools;
|
|
22
|
+
}
|
|
23
|
+
async function shutdownMcp() {
|
|
24
|
+
await (0, client_1.disconnectAllMcpClients)(mcpConnections);
|
|
25
|
+
mcpConnections = [];
|
|
26
|
+
mcpTools = [];
|
|
27
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.wrapMcpTool = wrapMcpTool;
|
|
4
|
+
const tools_1 = require("@langchain/core/tools");
|
|
5
|
+
const client_1 = require("./client");
|
|
6
|
+
const MCP_TOOL_PERMISSION_LEVEL = "mcp";
|
|
7
|
+
function wrapMcpTool(connection, mcpTool) {
|
|
8
|
+
const prefixedName = `${connection.name}_${mcpTool.name}`;
|
|
9
|
+
// 描述信息:前缀标记来源服务器 + 原始描述
|
|
10
|
+
const description = `[MCP:${connection.name}] ${mcpTool.description || "No description"}`;
|
|
11
|
+
// 直接把 MCP 工具的 inputSchema(JSON Schema)传给 LangChain 作为参数 schema。
|
|
12
|
+
// 这样 LLM 能看到完整的参数定义(如 url 字段),而不是空 schema。
|
|
13
|
+
// LangChain 支持 JSON Schema,会用 @cfworker/json-schema 做参数校验。
|
|
14
|
+
const schema = mcpTool.inputSchema;
|
|
15
|
+
const impl = async (args) => {
|
|
16
|
+
const result = await (0, client_1.callMcpTool)(connection.client, mcpTool.name, args);
|
|
17
|
+
return result;
|
|
18
|
+
};
|
|
19
|
+
const t = (0, tools_1.tool)(impl, {
|
|
20
|
+
name: prefixedName,
|
|
21
|
+
description,
|
|
22
|
+
schema,
|
|
23
|
+
});
|
|
24
|
+
t.permission_level = MCP_TOOL_PERMISSION_LEVEL;
|
|
25
|
+
return t;
|
|
26
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createModel = createModel;
|
|
4
|
+
exports.checkModel = checkModel;
|
|
5
|
+
const openai_1 = require("@langchain/openai");
|
|
6
|
+
const config_1 = require("./config");
|
|
7
|
+
const modelConfig = (0, config_1.getModelConfig)();
|
|
8
|
+
// 只要能支持 openai 的接口格式,都可以用。
|
|
9
|
+
function createModel(options) {
|
|
10
|
+
return new openai_1.ChatOpenAI({
|
|
11
|
+
model: modelConfig.model,
|
|
12
|
+
apiKey: modelConfig.apiKey,
|
|
13
|
+
configuration: {
|
|
14
|
+
baseURL: modelConfig.baseURL,
|
|
15
|
+
},
|
|
16
|
+
streaming: options?.streaming ?? true,
|
|
17
|
+
// 透传给模型 API 的额外参数:关闭模型的深度思考(thinking)模式
|
|
18
|
+
modelKwargs: {
|
|
19
|
+
thinking: { type: "disabled" },
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
async function checkModel() {
|
|
24
|
+
if (modelConfig.apiKey.length < 20) {
|
|
25
|
+
try {
|
|
26
|
+
const model = createModel({ streaming: false });
|
|
27
|
+
await model.invoke([{ role: "user", content: "hi" }]);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
console.log(`\n⚠️ API Key 验证失败,请检查配置是否正确\n\n请参考 https://chat.ningzhi2.site/getStart 修改配置\n`);
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
}
|