@steerable/agent-shell 0.6.15
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/LICENSE +91 -0
- package/contracts/tool-contract.json +326 -0
- package/dist/attachments.d.ts +41 -0
- package/dist/attachments.js +147 -0
- package/dist/brand.d.ts +24 -0
- package/dist/brand.js +92 -0
- package/dist/host/http-routes.d.ts +21 -0
- package/dist/host/http-routes.js +55 -0
- package/dist/host/ipc.d.ts +11 -0
- package/dist/host/ipc.js +20 -0
- package/dist/host/pack-assembly.d.ts +86 -0
- package/dist/host/pack-assembly.js +32 -0
- package/dist/host/runtime.d.ts +69 -0
- package/dist/host/runtime.js +207 -0
- package/dist/host/visible-terminal-exec.d.ts +21 -0
- package/dist/host/visible-terminal-exec.js +151 -0
- package/dist/hosted-web-search.d.ts +13 -0
- package/dist/hosted-web-search.js +82 -0
- package/dist/image-attachment.d.ts +39 -0
- package/dist/image-attachment.js +133 -0
- package/dist/insights/flush.d.ts +10 -0
- package/dist/insights/flush.js +139 -0
- package/dist/insights/record.d.ts +17 -0
- package/dist/insights/record.js +44 -0
- package/dist/json-store.d.ts +9 -0
- package/dist/json-store.js +22 -0
- package/dist/llm/index.d.ts +23 -0
- package/dist/llm/index.js +105 -0
- package/dist/llm/ollama.d.ts +23 -0
- package/dist/llm/ollama.js +242 -0
- package/dist/llm/openai-compat.d.ts +20 -0
- package/dist/llm/openai-compat.js +199 -0
- package/dist/llm/sidecar-provider.d.ts +37 -0
- package/dist/llm/sidecar-provider.js +163 -0
- package/dist/llm/tool-choice.d.ts +35 -0
- package/dist/llm/tool-choice.js +85 -0
- package/dist/llm/types.d.ts +122 -0
- package/dist/llm/types.js +1 -0
- package/dist/local-backend/agent-capability.d.ts +101 -0
- package/dist/local-backend/agent-capability.js +174 -0
- package/dist/local-backend/ai-title.d.ts +43 -0
- package/dist/local-backend/ai-title.js +173 -0
- package/dist/local-backend/auto-continue-helper.d.ts +80 -0
- package/dist/local-backend/auto-continue-helper.js +83 -0
- package/dist/local-backend/branch-helper.d.ts +24 -0
- package/dist/local-backend/branch-helper.js +27 -0
- package/dist/local-backend/context-compactor.d.ts +81 -0
- package/dist/local-backend/context-compactor.js +213 -0
- package/dist/local-backend/coreloop-stream.d.ts +245 -0
- package/dist/local-backend/coreloop-stream.js +277 -0
- package/dist/local-backend/deferred-detector.d.ts +15 -0
- package/dist/local-backend/deferred-detector.js +124 -0
- package/dist/local-backend/history-helper.d.ts +30 -0
- package/dist/local-backend/history-helper.js +34 -0
- package/dist/local-backend/interrupted-helper.d.ts +29 -0
- package/dist/local-backend/interrupted-helper.js +25 -0
- package/dist/local-backend/live-stream.d.ts +36 -0
- package/dist/local-backend/live-stream.js +23 -0
- package/dist/local-backend/llm-diagnose.d.ts +37 -0
- package/dist/local-backend/llm-diagnose.js +284 -0
- package/dist/local-backend/message-triggers.d.ts +27 -0
- package/dist/local-backend/message-triggers.js +67 -0
- package/dist/local-backend/pack-backend-routes.d.ts +31 -0
- package/dist/local-backend/pack-backend-routes.js +62 -0
- package/dist/local-backend/pack-turn-hooks.d.ts +37 -0
- package/dist/local-backend/pack-turn-hooks.js +67 -0
- package/dist/local-backend/prompt-builder.d.ts +101 -0
- package/dist/local-backend/prompt-builder.js +246 -0
- package/dist/local-backend/regenerate-helper.d.ts +62 -0
- package/dist/local-backend/regenerate-helper.js +75 -0
- package/dist/local-backend/router.d.ts +175 -0
- package/dist/local-backend/router.js +3139 -0
- package/dist/local-backend/skill-install.d.ts +19 -0
- package/dist/local-backend/skill-install.js +71 -0
- package/dist/local-backend/skill-loader.d.ts +92 -0
- package/dist/local-backend/skill-loader.js +146 -0
- package/dist/local-backend/skills/00-identity/SKILL.md +32 -0
- package/dist/local-backend/skills/10-goal/SKILL.md +59 -0
- package/dist/local-backend/skills/11-loop/SKILL.md +71 -0
- package/dist/local-backend/skills/12-create-skill/SKILL.md +88 -0
- package/dist/local-backend/skills/70-plan-mode/SKILL.md +58 -0
- package/dist/local-backend/skills/80-tool-usage/SKILL.md +70 -0
- package/dist/local-backend/skills/81-anti-deferred/SKILL.md +53 -0
- package/dist/local-backend/skills/82-data-grounding/SKILL.md +56 -0
- package/dist/local-backend/skills/85-local-exec/SKILL.md +86 -0
- package/dist/local-backend/skills/86-proactive-coding/SKILL.md +51 -0
- package/dist/local-backend/subagent-profiles.d.ts +30 -0
- package/dist/local-backend/subagent-profiles.js +74 -0
- package/dist/local-backend/task-process.d.ts +12 -0
- package/dist/local-backend/task-process.js +176 -0
- package/dist/local-backend/task-service.d.ts +135 -0
- package/dist/local-backend/task-service.js +565 -0
- package/dist/local-backend/turn-duration.d.ts +2 -0
- package/dist/local-backend/turn-duration.js +9 -0
- package/dist/local-backend/turn-timeline.d.ts +16 -0
- package/dist/local-backend/turn-timeline.js +42 -0
- package/dist/local-backend/worktree-service.d.ts +84 -0
- package/dist/local-backend/worktree-service.js +243 -0
- package/dist/local-edit.d.ts +48 -0
- package/dist/local-edit.js +44 -0
- package/dist/local-executor.d.ts +255 -0
- package/dist/local-executor.js +881 -0
- package/dist/local-script-registry.d.ts +28 -0
- package/dist/local-script-registry.js +63 -0
- package/dist/log.d.ts +13 -0
- package/dist/log.js +12 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +855 -0
- package/dist/mcp-executor.d.ts +45 -0
- package/dist/mcp-executor.js +241 -0
- package/dist/mcp-server-registry.d.ts +104 -0
- package/dist/mcp-server-registry.js +234 -0
- package/dist/preload-default.d.ts +1 -0
- package/dist/preload-default.js +9 -0
- package/dist/preload.cjs +395 -0
- package/dist/preload.d.ts +20 -0
- package/dist/preload.js +411 -0
- package/dist/product-config.d.ts +43 -0
- package/dist/product-config.js +26 -0
- package/dist/project-registry.d.ts +55 -0
- package/dist/project-registry.js +106 -0
- package/dist/project-rules.d.ts +15 -0
- package/dist/project-rules.js +102 -0
- package/dist/runtime.d.ts +62 -0
- package/dist/runtime.js +217 -0
- package/dist/scenario/pack.d.ts +8 -0
- package/dist/scenario/pack.js +1 -0
- package/dist/scenario/registry.d.ts +24 -0
- package/dist/scenario/registry.js +31 -0
- package/dist/server/http-server.d.ts +39 -0
- package/dist/server/http-server.js +361 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +107 -0
- package/dist/server/sse-bus.d.ts +14 -0
- package/dist/server/sse-bus.js +31 -0
- package/dist/shell-adapt.d.ts +21 -0
- package/dist/shell-adapt.js +104 -0
- package/dist/sidecar/boot.d.ts +36 -0
- package/dist/sidecar/boot.js +343 -0
- package/dist/sidecar/egress-hint.d.ts +15 -0
- package/dist/sidecar/egress-hint.js +46 -0
- package/dist/sidecar/egress-proxy.d.ts +183 -0
- package/dist/sidecar/egress-proxy.js +419 -0
- package/dist/sidecar/errors.d.ts +22 -0
- package/dist/sidecar/errors.js +38 -0
- package/dist/sidecar/exec-sandbox.d.ts +48 -0
- package/dist/sidecar/exec-sandbox.js +94 -0
- package/dist/sidecar/handle.d.ts +32 -0
- package/dist/sidecar/handle.js +53 -0
- package/dist/sidecar/index.d.ts +14 -0
- package/dist/sidecar/index.js +13 -0
- package/dist/sidecar/proxy-detect.d.ts +50 -0
- package/dist/sidecar/proxy-detect.js +182 -0
- package/dist/sidecar/reverse-approval.d.ts +55 -0
- package/dist/sidecar/reverse-approval.js +86 -0
- package/dist/sidecar/reverse-ask-user.d.ts +34 -0
- package/dist/sidecar/reverse-ask-user.js +59 -0
- package/dist/sidecar/reverse-spawn.d.ts +19 -0
- package/dist/sidecar/reverse-spawn.js +161 -0
- package/dist/sidecar/reverse-tools.d.ts +29 -0
- package/dist/sidecar/reverse-tools.js +106 -0
- package/dist/sidecar/safety-patterns.d.ts +41 -0
- package/dist/sidecar/safety-patterns.js +157 -0
- package/dist/sidecar/storage-path.d.ts +14 -0
- package/dist/sidecar/storage-path.js +35 -0
- package/dist/sidecar/supervisor.d.ts +218 -0
- package/dist/sidecar/supervisor.js +932 -0
- package/dist/sidecar/types.d.ts +601 -0
- package/dist/sidecar/types.js +1 -0
- package/dist/single-instance.d.ts +11 -0
- package/dist/single-instance.js +21 -0
- package/dist/storage/empty-chats.d.ts +9 -0
- package/dist/storage/empty-chats.js +16 -0
- package/dist/storage/index.d.ts +373 -0
- package/dist/storage/index.js +1158 -0
- package/dist/storage/insights-redact.d.ts +2 -0
- package/dist/storage/insights-redact.js +30 -0
- package/dist/storage/insights-settings.d.ts +53 -0
- package/dist/storage/insights-settings.js +92 -0
- package/dist/storage/llm-settings.d.ts +120 -0
- package/dist/storage/llm-settings.js +233 -0
- package/dist/storage/local-store-singleton.d.ts +28 -0
- package/dist/storage/local-store-singleton.js +38 -0
- package/dist/storage/message-order.d.ts +25 -0
- package/dist/storage/message-order.js +27 -0
- package/dist/storage/pack-migrations.d.ts +22 -0
- package/dist/storage/pack-migrations.js +24 -0
- package/dist/storage/pack-seeds.d.ts +36 -0
- package/dist/storage/pack-seeds.js +42 -0
- package/dist/storage/telemetry-settings.d.ts +38 -0
- package/dist/storage/telemetry-settings.js +59 -0
- package/dist/storage/usage-summary.d.ts +55 -0
- package/dist/storage/usage-summary.js +38 -0
- package/dist/storage/web-search-settings.d.ts +38 -0
- package/dist/storage/web-search-settings.js +74 -0
- package/dist/storage/write-lease.d.ts +26 -0
- package/dist/storage/write-lease.js +74 -0
- package/dist/terminal-manager.d.ts +83 -0
- package/dist/terminal-manager.js +506 -0
- package/dist/tool-router.d.ts +228 -0
- package/dist/tool-router.js +930 -0
- package/dist/tool-search-rank.d.ts +42 -0
- package/dist/tool-search-rank.js +96 -0
- package/package.json +67 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 给超时类错误追加可操作的排查提示。独立成纯函数便于单测。
|
|
3
|
+
*/
|
|
4
|
+
export declare function withMcpTroubleshootHint(message: string): string;
|
|
5
|
+
export interface McpServerConfig {
|
|
6
|
+
command: string;
|
|
7
|
+
args?: string[];
|
|
8
|
+
env?: Record<string, string>;
|
|
9
|
+
cwd?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Pure eviction predicate, split out so it's unit-testable without spinning
|
|
13
|
+
* up a real MCP child process (`McpExecutor` isn't otherwise exported —
|
|
14
|
+
* everything else about it talks to a real stdio transport).
|
|
15
|
+
*/
|
|
16
|
+
export declare function shouldEvictMcpConnection(entry: {
|
|
17
|
+
lastUsed: number;
|
|
18
|
+
inflight: number;
|
|
19
|
+
}, now: number, idleTimeoutMs: number): boolean;
|
|
20
|
+
declare class McpExecutor {
|
|
21
|
+
private pool;
|
|
22
|
+
private cleanupTimer;
|
|
23
|
+
constructor();
|
|
24
|
+
executeTool(config: McpServerConfig, toolName: string, toolArgs: Record<string, unknown>): Promise<{
|
|
25
|
+
success: boolean;
|
|
26
|
+
text?: string;
|
|
27
|
+
error?: string;
|
|
28
|
+
}>;
|
|
29
|
+
listTools(config: McpServerConfig): Promise<{
|
|
30
|
+
success: boolean;
|
|
31
|
+
tools?: {
|
|
32
|
+
name: string;
|
|
33
|
+
description: string;
|
|
34
|
+
inputSchema?: unknown;
|
|
35
|
+
}[];
|
|
36
|
+
error?: string;
|
|
37
|
+
}>;
|
|
38
|
+
private getOrCreate;
|
|
39
|
+
private closeEntry;
|
|
40
|
+
/** 关闭连接池中的所有连接(应用退出 / 测试收尾时调用)。 */
|
|
41
|
+
shutdownAll(): Promise<void>;
|
|
42
|
+
private evictIdle;
|
|
43
|
+
}
|
|
44
|
+
export declare const mcpExecutor: McpExecutor;
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { createRequire } from 'module';
|
|
3
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
4
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
5
|
+
// In ESM there is no `__filename` global; use the module URL to seed a local
|
|
6
|
+
// CommonJS require() so we can `require.resolve()` MCP server packages that
|
|
7
|
+
// ship as CJS (npx -y <pkg> targets, e.g. apple-notes-mcp).
|
|
8
|
+
const localRequire = createRequire(import.meta.url);
|
|
9
|
+
const IDLE_TIMEOUT_MS = 5 * 60 * 1000;
|
|
10
|
+
/**
|
|
11
|
+
* 单次 MCP 请求(initialize 握手 / listTools / callTool)的超时。
|
|
12
|
+
* SDK 默认 60s 太短:`npx -y <pkg>` 首次运行要从 npm registry 下载包,
|
|
13
|
+
* 国内网络或走代理时 60s 内完不成握手,报 "MCP error -32001: Request
|
|
14
|
+
* timed out"(2026-07-31 用户实测遇到)。放宽到 3 分钟覆盖冷启动下载。
|
|
15
|
+
*/
|
|
16
|
+
const MCP_REQUEST_TIMEOUT_MS = 3 * 60 * 1000;
|
|
17
|
+
/**
|
|
18
|
+
* 给超时类错误追加可操作的排查提示。独立成纯函数便于单测。
|
|
19
|
+
*/
|
|
20
|
+
export function withMcpTroubleshootHint(message) {
|
|
21
|
+
if (!/timed?\s*out|超时|-32001/i.test(message))
|
|
22
|
+
return message;
|
|
23
|
+
return (`${message}。若为首次启动该服务:npx/uvx 需要现场下载包,可能较慢——` +
|
|
24
|
+
`可先在终端手动执行一次相同命令预热缓存,或为 npm 配置国内镜像源 ` +
|
|
25
|
+
`(npm config set registry https://registry.npmmirror.com),之后重试"测试连接"。`);
|
|
26
|
+
}
|
|
27
|
+
const COMMAND_HINTS = {
|
|
28
|
+
npx: '请安装 Node.js (https://nodejs.org)',
|
|
29
|
+
node: '请安装 Node.js (https://nodejs.org)',
|
|
30
|
+
uvx: '请安装 uv (https://docs.astral.sh/uv)',
|
|
31
|
+
uv: '请安装 uv (https://docs.astral.sh/uv)',
|
|
32
|
+
python: '请安装 Python (https://python.org)',
|
|
33
|
+
python3: '请安装 Python (https://python.org)',
|
|
34
|
+
};
|
|
35
|
+
function commandExists(cmd) {
|
|
36
|
+
try {
|
|
37
|
+
const which = process.platform === 'win32' ? 'where' : 'which';
|
|
38
|
+
execSync(`${which} ${cmd}`, { stdio: 'ignore' });
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function tryResolveBundled(config) {
|
|
46
|
+
if (config.command !== 'npx')
|
|
47
|
+
return null;
|
|
48
|
+
const args = config.args ?? [];
|
|
49
|
+
const yIdx = args.indexOf('-y');
|
|
50
|
+
if (yIdx === -1 || yIdx + 1 >= args.length)
|
|
51
|
+
return null;
|
|
52
|
+
const pkgName = args[yIdx + 1];
|
|
53
|
+
const extraArgs = args.slice(yIdx + 2);
|
|
54
|
+
try {
|
|
55
|
+
const entryPath = localRequire.resolve(pkgName);
|
|
56
|
+
return {
|
|
57
|
+
command: process.execPath,
|
|
58
|
+
args: [entryPath, ...extraArgs],
|
|
59
|
+
env: { ...config.env, ELECTRON_RUN_AS_NODE: '1' },
|
|
60
|
+
cwd: config.cwd,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function configKey(cfg) {
|
|
68
|
+
return `${cfg.command}::${(cfg.args ?? []).join(' ')}`;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Pure eviction predicate, split out so it's unit-testable without spinning
|
|
72
|
+
* up a real MCP child process (`McpExecutor` isn't otherwise exported —
|
|
73
|
+
* everything else about it talks to a real stdio transport).
|
|
74
|
+
*/
|
|
75
|
+
export function shouldEvictMcpConnection(entry, now, idleTimeoutMs) {
|
|
76
|
+
if (entry.inflight > 0)
|
|
77
|
+
return false;
|
|
78
|
+
return now - entry.lastUsed > idleTimeoutMs;
|
|
79
|
+
}
|
|
80
|
+
function coerceArgs(args, schema) {
|
|
81
|
+
if (!schema?.inputSchema?.properties)
|
|
82
|
+
return args;
|
|
83
|
+
const props = schema.inputSchema.properties;
|
|
84
|
+
const out = { ...args };
|
|
85
|
+
for (const [key, value] of Object.entries(args)) {
|
|
86
|
+
const expected = props[key]?.type;
|
|
87
|
+
if (expected === 'string' && typeof value !== 'string') {
|
|
88
|
+
out[key] = String(value);
|
|
89
|
+
}
|
|
90
|
+
else if (expected === 'integer' && typeof value === 'string') {
|
|
91
|
+
const n = parseInt(value, 10);
|
|
92
|
+
if (!isNaN(n))
|
|
93
|
+
out[key] = n;
|
|
94
|
+
}
|
|
95
|
+
else if (expected === 'number' && typeof value === 'string') {
|
|
96
|
+
const n = parseFloat(value);
|
|
97
|
+
if (!isNaN(n))
|
|
98
|
+
out[key] = n;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
class McpExecutor {
|
|
104
|
+
pool = new Map();
|
|
105
|
+
cleanupTimer = null;
|
|
106
|
+
constructor() {
|
|
107
|
+
this.cleanupTimer = setInterval(() => this.evictIdle(), 60_000);
|
|
108
|
+
}
|
|
109
|
+
async executeTool(config, toolName, toolArgs) {
|
|
110
|
+
let entry;
|
|
111
|
+
try {
|
|
112
|
+
entry = await this.getOrCreate(config);
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
116
|
+
return { success: false, error: withMcpTroubleshootHint(`MCP server startup failed: ${msg}`) };
|
|
117
|
+
}
|
|
118
|
+
entry.inflight += 1;
|
|
119
|
+
try {
|
|
120
|
+
const coerced = coerceArgs(toolArgs, entry.toolSchemas.get(toolName));
|
|
121
|
+
const result = await entry.client.callTool({ name: toolName, arguments: coerced }, undefined, {
|
|
122
|
+
timeout: MCP_REQUEST_TIMEOUT_MS,
|
|
123
|
+
});
|
|
124
|
+
const textParts = [];
|
|
125
|
+
if (Array.isArray(result.content)) {
|
|
126
|
+
for (const item of result.content) {
|
|
127
|
+
if (typeof item === 'object' && item !== null && 'text' in item) {
|
|
128
|
+
textParts.push(String(item.text));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const text = textParts.join('\n') || JSON.stringify(result.content);
|
|
133
|
+
if (result.isError)
|
|
134
|
+
return { success: false, error: text };
|
|
135
|
+
return { success: true, text };
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
139
|
+
const key = configKey(config);
|
|
140
|
+
await this.closeEntry(key);
|
|
141
|
+
return { success: false, error: withMcpTroubleshootHint(`Tool execution failed: ${msg}`) };
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
entry.inflight -= 1;
|
|
145
|
+
entry.lastUsed = Date.now();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async listTools(config) {
|
|
149
|
+
let entry;
|
|
150
|
+
try {
|
|
151
|
+
entry = await this.getOrCreate(config);
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
155
|
+
return { success: false, error: withMcpTroubleshootHint(`MCP server startup failed: ${msg}`) };
|
|
156
|
+
}
|
|
157
|
+
entry.inflight += 1;
|
|
158
|
+
try {
|
|
159
|
+
const listing = await entry.client.listTools(undefined, { timeout: MCP_REQUEST_TIMEOUT_MS });
|
|
160
|
+
return {
|
|
161
|
+
success: true,
|
|
162
|
+
tools: listing.tools.map(t => ({ name: t.name, description: t.description ?? '', inputSchema: t.inputSchema })),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
167
|
+
const key = configKey(config);
|
|
168
|
+
await this.closeEntry(key);
|
|
169
|
+
return { success: false, error: withMcpTroubleshootHint(`List tools failed: ${msg}`) };
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
entry.inflight -= 1;
|
|
173
|
+
entry.lastUsed = Date.now();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async getOrCreate(config) {
|
|
177
|
+
const key = configKey(config);
|
|
178
|
+
const existing = this.pool.get(key);
|
|
179
|
+
if (existing)
|
|
180
|
+
return existing;
|
|
181
|
+
const resolved = tryResolveBundled(config) ?? config;
|
|
182
|
+
if (!commandExists(resolved.command)) {
|
|
183
|
+
const hint = COMMAND_HINTS[config.command] || `请确保 ${config.command} 已安装并在 PATH 中`;
|
|
184
|
+
throw new Error(`命令 "${config.command}" 不可用。${hint}`);
|
|
185
|
+
}
|
|
186
|
+
const transport = new StdioClientTransport({
|
|
187
|
+
command: resolved.command,
|
|
188
|
+
args: resolved.args,
|
|
189
|
+
env: { ...process.env, ...(resolved.env ?? {}) },
|
|
190
|
+
cwd: resolved.cwd ?? undefined,
|
|
191
|
+
stderr: 'pipe',
|
|
192
|
+
});
|
|
193
|
+
const client = new Client({ name: 'agent-shell', version: '1.0.0' }, { capabilities: {} });
|
|
194
|
+
await client.connect(transport, { timeout: MCP_REQUEST_TIMEOUT_MS });
|
|
195
|
+
const toolSchemas = new Map();
|
|
196
|
+
try {
|
|
197
|
+
const listing = await client.listTools(undefined, { timeout: MCP_REQUEST_TIMEOUT_MS });
|
|
198
|
+
for (const t of listing.tools) {
|
|
199
|
+
toolSchemas.set(t.name, {
|
|
200
|
+
name: t.name,
|
|
201
|
+
inputSchema: t.inputSchema,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
// no-op
|
|
207
|
+
}
|
|
208
|
+
const entry = { client, transport, lastUsed: Date.now(), key, toolSchemas, inflight: 0 };
|
|
209
|
+
this.pool.set(key, entry);
|
|
210
|
+
transport.onclose = () => {
|
|
211
|
+
this.pool.delete(key);
|
|
212
|
+
};
|
|
213
|
+
return entry;
|
|
214
|
+
}
|
|
215
|
+
async closeEntry(key) {
|
|
216
|
+
const entry = this.pool.get(key);
|
|
217
|
+
if (!entry)
|
|
218
|
+
return;
|
|
219
|
+
this.pool.delete(key);
|
|
220
|
+
try {
|
|
221
|
+
await entry.client.close();
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
// ignore
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/** 关闭连接池中的所有连接(应用退出 / 测试收尾时调用)。 */
|
|
228
|
+
async shutdownAll() {
|
|
229
|
+
const keys = [...this.pool.keys()];
|
|
230
|
+
await Promise.all(keys.map((key) => this.closeEntry(key)));
|
|
231
|
+
}
|
|
232
|
+
evictIdle() {
|
|
233
|
+
const now = Date.now();
|
|
234
|
+
for (const [key, entry] of this.pool) {
|
|
235
|
+
if (shouldEvictMcpConnection(entry, now, IDLE_TIMEOUT_MS)) {
|
|
236
|
+
this.closeEntry(key).catch(() => { });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
export const mcpExecutor = new McpExecutor();
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP 外部服务注册表。
|
|
3
|
+
*
|
|
4
|
+
* 用户在「设置 → MCP 服务」里导入/登记的外部 MCP server(stdio 型),
|
|
5
|
+
* 持久化在 userData/agent-mcp-servers.json。已启用服务的工具会经
|
|
6
|
+
* ToolRouter 以 `mcp__<serverKey>__<toolName>` 一等工具身份暴露给模型。
|
|
7
|
+
*
|
|
8
|
+
* 存储通过 {@link McpServerKvStore} 接口注入:main.ts 用 electron-store
|
|
9
|
+
* 实现,单测用内存实现——本模块不 import electron,保持纯 Node 可测。
|
|
10
|
+
*/
|
|
11
|
+
import { type McpServerConfig } from './mcp-executor.js';
|
|
12
|
+
export interface McpServerEntry {
|
|
13
|
+
id: string;
|
|
14
|
+
/** 用户可见名称,也用于生成工具前缀(如 "filesystem")。 */
|
|
15
|
+
name: string;
|
|
16
|
+
command: string;
|
|
17
|
+
args: string[];
|
|
18
|
+
env: Record<string, string>;
|
|
19
|
+
cwd?: string;
|
|
20
|
+
enabled: boolean;
|
|
21
|
+
createdAt: string;
|
|
22
|
+
updatedAt: string;
|
|
23
|
+
}
|
|
24
|
+
export interface CreateMcpServerInput {
|
|
25
|
+
name: string;
|
|
26
|
+
command: string;
|
|
27
|
+
args?: string[];
|
|
28
|
+
env?: Record<string, string>;
|
|
29
|
+
cwd?: string;
|
|
30
|
+
enabled?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/** 最小 KV 存储接口,避免本模块直接依赖 electron-store。 */
|
|
33
|
+
export interface McpServerKvStore {
|
|
34
|
+
get(key: 'mcpServers'): McpServerEntry[] | undefined;
|
|
35
|
+
set(key: 'mcpServers', value: McpServerEntry[]): void;
|
|
36
|
+
}
|
|
37
|
+
export interface CachedToolList {
|
|
38
|
+
tools: Array<{
|
|
39
|
+
name: string;
|
|
40
|
+
description: string;
|
|
41
|
+
inputSchema?: unknown;
|
|
42
|
+
}>;
|
|
43
|
+
fetchedAt: string;
|
|
44
|
+
error: string | null;
|
|
45
|
+
}
|
|
46
|
+
export interface ImportResult {
|
|
47
|
+
added: McpServerEntry[];
|
|
48
|
+
/** 因重名或缺少 command 被跳过的服务名。 */
|
|
49
|
+
skipped: string[];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 一个可调用 MCP 工具的扁平视图(跨服务),供 "/" 选择器与消息触发解析用。
|
|
53
|
+
* `token` 即模型侧的一等工具名 `mcp__<serverKey>__<toolName>`(命名规则与
|
|
54
|
+
* tool-router.ts 的动态工具保持一致)。
|
|
55
|
+
*/
|
|
56
|
+
export interface McpToolEntry {
|
|
57
|
+
token: string;
|
|
58
|
+
toolName: string;
|
|
59
|
+
serverKey: string;
|
|
60
|
+
serverName: string;
|
|
61
|
+
description: string;
|
|
62
|
+
serverId: string;
|
|
63
|
+
}
|
|
64
|
+
/** 工具名只允许 [a-z0-9-];中文名会被剥光,退化为 srv-<id前6位>。 */
|
|
65
|
+
export declare function sanitizeServerKey(name: string, id: string): string;
|
|
66
|
+
export declare class McpServerRegistry {
|
|
67
|
+
private readonly store;
|
|
68
|
+
/** serverId → 最近一次 listTools 的结果(内存缓存,重启后由后台刷新重建)。 */
|
|
69
|
+
private readonly toolCache;
|
|
70
|
+
constructor(store: McpServerKvStore);
|
|
71
|
+
list(): McpServerEntry[];
|
|
72
|
+
get(idOrName: string): McpServerEntry | null;
|
|
73
|
+
create(input: CreateMcpServerInput): McpServerEntry;
|
|
74
|
+
update(id: string, updates: Partial<CreateMcpServerInput>): McpServerEntry;
|
|
75
|
+
delete(id: string): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* 解析 Claude Desktop 风格的配置并批量导入:
|
|
78
|
+
*
|
|
79
|
+
* ```json
|
|
80
|
+
* { "mcpServers": { "filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "C:/"] } } }
|
|
81
|
+
* ```
|
|
82
|
+
*
|
|
83
|
+
* 也兼容裸对象(不带 mcpServers 包裹)。重名或缺 command 的条目跳过。
|
|
84
|
+
*/
|
|
85
|
+
importClaudeConfig(raw: string | Record<string, unknown>): ImportResult;
|
|
86
|
+
/** 工具名前缀(同一名称多实例撞名时追加 id 后缀保证唯一)。 */
|
|
87
|
+
serverKey(entry: McpServerEntry): string;
|
|
88
|
+
toExecutorConfig(entry: McpServerEntry): McpServerConfig;
|
|
89
|
+
getCachedTools(serverId: string): CachedToolList | null;
|
|
90
|
+
/**
|
|
91
|
+
* 所有「已启用 + 工具缓存非空」服务的工具扁平清单("/" 选择器数据源)。
|
|
92
|
+
*/
|
|
93
|
+
listEnabledToolEntries(): McpToolEntry[];
|
|
94
|
+
/**
|
|
95
|
+
* 按完整 token(大小写不敏感)反查工具;命中时返回注册表里的规范 token
|
|
96
|
+
* (用户手敲可能大小写不符,tool_call 必须用规范名)。未启用 / 无缓存 /
|
|
97
|
+
* 不存在均返回 null。
|
|
98
|
+
*/
|
|
99
|
+
findToolByToken(token: string): McpToolEntry | null;
|
|
100
|
+
/** 连接服务并刷新工具缓存;失败时缓存错误信息(tools 为空)。 */
|
|
101
|
+
refreshTools(serverId: string): Promise<CachedToolList>;
|
|
102
|
+
/** 后台刷新所有已启用服务的工具列表(启动时调用,不阻塞、不抛错)。 */
|
|
103
|
+
refreshAllEnabled(): Promise<void>;
|
|
104
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP 外部服务注册表。
|
|
3
|
+
*
|
|
4
|
+
* 用户在「设置 → MCP 服务」里导入/登记的外部 MCP server(stdio 型),
|
|
5
|
+
* 持久化在 userData/agent-mcp-servers.json。已启用服务的工具会经
|
|
6
|
+
* ToolRouter 以 `mcp__<serverKey>__<toolName>` 一等工具身份暴露给模型。
|
|
7
|
+
*
|
|
8
|
+
* 存储通过 {@link McpServerKvStore} 接口注入:main.ts 用 electron-store
|
|
9
|
+
* 实现,单测用内存实现——本模块不 import electron,保持纯 Node 可测。
|
|
10
|
+
*/
|
|
11
|
+
import { randomUUID } from 'node:crypto';
|
|
12
|
+
import { mcpExecutor } from './mcp-executor.js';
|
|
13
|
+
const STORE_KEY = 'mcpServers';
|
|
14
|
+
/** 工具名只允许 [a-z0-9-];中文名会被剥光,退化为 srv-<id前6位>。 */
|
|
15
|
+
export function sanitizeServerKey(name, id) {
|
|
16
|
+
const cleaned = name
|
|
17
|
+
.toLowerCase()
|
|
18
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
19
|
+
.replace(/^-+|-+$/g, '');
|
|
20
|
+
return cleaned || `srv-${id.slice(0, 6)}`;
|
|
21
|
+
}
|
|
22
|
+
export class McpServerRegistry {
|
|
23
|
+
store;
|
|
24
|
+
/** serverId → 最近一次 listTools 的结果(内存缓存,重启后由后台刷新重建)。 */
|
|
25
|
+
toolCache = new Map();
|
|
26
|
+
constructor(store) {
|
|
27
|
+
this.store = store;
|
|
28
|
+
}
|
|
29
|
+
list() {
|
|
30
|
+
return this.store.get(STORE_KEY) ?? [];
|
|
31
|
+
}
|
|
32
|
+
get(idOrName) {
|
|
33
|
+
const needle = idOrName.trim().toLowerCase();
|
|
34
|
+
return (this.list().find((s) => s.id === idOrName || s.name.toLowerCase() === needle) ?? null);
|
|
35
|
+
}
|
|
36
|
+
create(input) {
|
|
37
|
+
const name = input.name.trim();
|
|
38
|
+
const command = input.command.trim();
|
|
39
|
+
if (!name)
|
|
40
|
+
throw new Error('服务名称不能为空');
|
|
41
|
+
if (!command)
|
|
42
|
+
throw new Error('启动命令不能为空');
|
|
43
|
+
if (this.get(name))
|
|
44
|
+
throw new Error(`已存在同名服务「${name}」`);
|
|
45
|
+
const now = new Date().toISOString();
|
|
46
|
+
const entry = {
|
|
47
|
+
id: randomUUID(),
|
|
48
|
+
name,
|
|
49
|
+
command,
|
|
50
|
+
args: (input.args ?? []).map((a) => String(a)),
|
|
51
|
+
env: { ...(input.env ?? {}) },
|
|
52
|
+
cwd: input.cwd?.trim() || undefined,
|
|
53
|
+
enabled: input.enabled ?? true,
|
|
54
|
+
createdAt: now,
|
|
55
|
+
updatedAt: now,
|
|
56
|
+
};
|
|
57
|
+
this.store.set(STORE_KEY, [...this.list(), entry]);
|
|
58
|
+
return entry;
|
|
59
|
+
}
|
|
60
|
+
update(id, updates) {
|
|
61
|
+
const servers = this.list();
|
|
62
|
+
const idx = servers.findIndex((s) => s.id === id);
|
|
63
|
+
if (idx === -1)
|
|
64
|
+
throw new Error('服务不存在');
|
|
65
|
+
const current = servers[idx];
|
|
66
|
+
const nextName = updates.name !== undefined ? updates.name.trim() : current.name;
|
|
67
|
+
if (!nextName)
|
|
68
|
+
throw new Error('服务名称不能为空');
|
|
69
|
+
const nameClash = servers.some((s) => s.id !== id && s.name.toLowerCase() === nextName.toLowerCase());
|
|
70
|
+
if (nameClash)
|
|
71
|
+
throw new Error(`已存在同名服务「${nextName}」`);
|
|
72
|
+
const next = {
|
|
73
|
+
...current,
|
|
74
|
+
name: nextName,
|
|
75
|
+
command: updates.command !== undefined ? updates.command.trim() : current.command,
|
|
76
|
+
args: updates.args !== undefined ? updates.args.map((a) => String(a)) : current.args,
|
|
77
|
+
env: updates.env !== undefined ? { ...updates.env } : current.env,
|
|
78
|
+
cwd: updates.cwd !== undefined ? updates.cwd.trim() || undefined : current.cwd,
|
|
79
|
+
enabled: updates.enabled !== undefined ? updates.enabled : current.enabled,
|
|
80
|
+
updatedAt: new Date().toISOString(),
|
|
81
|
+
};
|
|
82
|
+
if (!next.command)
|
|
83
|
+
throw new Error('启动命令不能为空');
|
|
84
|
+
servers[idx] = next;
|
|
85
|
+
this.store.set(STORE_KEY, servers);
|
|
86
|
+
// 配置变了,旧工具缓存不可信
|
|
87
|
+
this.toolCache.delete(id);
|
|
88
|
+
return next;
|
|
89
|
+
}
|
|
90
|
+
delete(id) {
|
|
91
|
+
const servers = this.list();
|
|
92
|
+
const next = servers.filter((s) => s.id !== id);
|
|
93
|
+
if (next.length === servers.length)
|
|
94
|
+
return false;
|
|
95
|
+
this.store.set(STORE_KEY, next);
|
|
96
|
+
this.toolCache.delete(id);
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* 解析 Claude Desktop 风格的配置并批量导入:
|
|
101
|
+
*
|
|
102
|
+
* ```json
|
|
103
|
+
* { "mcpServers": { "filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "C:/"] } } }
|
|
104
|
+
* ```
|
|
105
|
+
*
|
|
106
|
+
* 也兼容裸对象(不带 mcpServers 包裹)。重名或缺 command 的条目跳过。
|
|
107
|
+
*/
|
|
108
|
+
importClaudeConfig(raw) {
|
|
109
|
+
let parsed = raw;
|
|
110
|
+
if (typeof raw === 'string') {
|
|
111
|
+
parsed = JSON.parse(raw); // JSON 语法错误直接抛给调用方
|
|
112
|
+
}
|
|
113
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
114
|
+
throw new Error('配置必须是 JSON 对象');
|
|
115
|
+
}
|
|
116
|
+
const container = 'mcpServers' in parsed
|
|
117
|
+
? parsed.mcpServers
|
|
118
|
+
: parsed;
|
|
119
|
+
if (typeof container !== 'object' || container === null || Array.isArray(container)) {
|
|
120
|
+
throw new Error('mcpServers 必须是对象(服务名 → 配置)');
|
|
121
|
+
}
|
|
122
|
+
const result = { added: [], skipped: [] };
|
|
123
|
+
for (const [name, cfgRaw] of Object.entries(container)) {
|
|
124
|
+
const cfg = (cfgRaw ?? {});
|
|
125
|
+
const command = typeof cfg.command === 'string' ? cfg.command.trim() : '';
|
|
126
|
+
if (!command || this.get(name)) {
|
|
127
|
+
result.skipped.push(name);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const env = {};
|
|
131
|
+
if (cfg.env && typeof cfg.env === 'object' && !Array.isArray(cfg.env)) {
|
|
132
|
+
for (const [k, v] of Object.entries(cfg.env)) {
|
|
133
|
+
env[k] = String(v);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const entry = this.create({
|
|
137
|
+
name: name.trim(),
|
|
138
|
+
command,
|
|
139
|
+
args: Array.isArray(cfg.args) ? cfg.args.map((a) => String(a)) : [],
|
|
140
|
+
env,
|
|
141
|
+
cwd: typeof cfg.cwd === 'string' ? cfg.cwd : undefined,
|
|
142
|
+
enabled: true,
|
|
143
|
+
});
|
|
144
|
+
result.added.push(entry);
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
/** 工具名前缀(同一名称多实例撞名时追加 id 后缀保证唯一)。 */
|
|
149
|
+
serverKey(entry) {
|
|
150
|
+
const base = sanitizeServerKey(entry.name, entry.id);
|
|
151
|
+
const clash = this.list().some((s) => s.id !== entry.id && sanitizeServerKey(s.name, s.id) === base);
|
|
152
|
+
return clash ? `${base}-${entry.id.slice(0, 4)}` : base;
|
|
153
|
+
}
|
|
154
|
+
toExecutorConfig(entry) {
|
|
155
|
+
return {
|
|
156
|
+
command: entry.command,
|
|
157
|
+
args: entry.args,
|
|
158
|
+
env: entry.env,
|
|
159
|
+
cwd: entry.cwd,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
getCachedTools(serverId) {
|
|
163
|
+
return this.toolCache.get(serverId) ?? null;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* 所有「已启用 + 工具缓存非空」服务的工具扁平清单("/" 选择器数据源)。
|
|
167
|
+
*/
|
|
168
|
+
listEnabledToolEntries() {
|
|
169
|
+
const out = [];
|
|
170
|
+
for (const server of this.list()) {
|
|
171
|
+
if (!server.enabled)
|
|
172
|
+
continue;
|
|
173
|
+
const cached = this.getCachedTools(server.id);
|
|
174
|
+
if (!cached || cached.tools.length === 0)
|
|
175
|
+
continue;
|
|
176
|
+
const serverKey = this.serverKey(server);
|
|
177
|
+
for (const tool of cached.tools) {
|
|
178
|
+
out.push({
|
|
179
|
+
token: `mcp__${serverKey}__${tool.name}`,
|
|
180
|
+
toolName: tool.name,
|
|
181
|
+
serverKey,
|
|
182
|
+
serverName: server.name,
|
|
183
|
+
description: tool.description ?? '',
|
|
184
|
+
serverId: server.id,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* 按完整 token(大小写不敏感)反查工具;命中时返回注册表里的规范 token
|
|
192
|
+
* (用户手敲可能大小写不符,tool_call 必须用规范名)。未启用 / 无缓存 /
|
|
193
|
+
* 不存在均返回 null。
|
|
194
|
+
*/
|
|
195
|
+
findToolByToken(token) {
|
|
196
|
+
const needle = token.trim().toLowerCase();
|
|
197
|
+
if (!needle.startsWith('mcp__'))
|
|
198
|
+
return null;
|
|
199
|
+
return (this.listEnabledToolEntries().find((e) => e.token.toLowerCase() === needle) ?? null);
|
|
200
|
+
}
|
|
201
|
+
/** 连接服务并刷新工具缓存;失败时缓存错误信息(tools 为空)。 */
|
|
202
|
+
async refreshTools(serverId) {
|
|
203
|
+
const entry = this.list().find((s) => s.id === serverId);
|
|
204
|
+
if (!entry)
|
|
205
|
+
throw new Error('服务不存在');
|
|
206
|
+
const result = await mcpExecutor.listTools(this.toExecutorConfig(entry));
|
|
207
|
+
const cached = result.success
|
|
208
|
+
? {
|
|
209
|
+
tools: (result.tools ?? []).map((t) => ({
|
|
210
|
+
name: t.name,
|
|
211
|
+
description: t.description,
|
|
212
|
+
inputSchema: t.inputSchema,
|
|
213
|
+
})),
|
|
214
|
+
fetchedAt: new Date().toISOString(),
|
|
215
|
+
error: null,
|
|
216
|
+
}
|
|
217
|
+
: { tools: [], fetchedAt: new Date().toISOString(), error: result.error ?? '未知错误' };
|
|
218
|
+
this.toolCache.set(serverId, cached);
|
|
219
|
+
return cached;
|
|
220
|
+
}
|
|
221
|
+
/** 后台刷新所有已启用服务的工具列表(启动时调用,不阻塞、不抛错)。 */
|
|
222
|
+
async refreshAllEnabled() {
|
|
223
|
+
const jobs = this.list()
|
|
224
|
+
.filter((s) => s.enabled)
|
|
225
|
+
.map((s) => this.refreshTools(s.id).catch((err) => {
|
|
226
|
+
this.toolCache.set(s.id, {
|
|
227
|
+
tools: [],
|
|
228
|
+
fetchedAt: new Date().toISOString(),
|
|
229
|
+
error: err instanceof Error ? err.message : String(err),
|
|
230
|
+
});
|
|
231
|
+
}));
|
|
232
|
+
await Promise.allSettled(jobs);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* shell 默认 preload 入口:无包贡献的中性面(buildPreloadApi())。
|
|
3
|
+
* 由本包构建(esbuild → dist/preload.cjs)打包成单文件 CJS,供
|
|
4
|
+
* shell main.ts 的默认 preload 路径与纯 shell/demo 启动使用。
|
|
5
|
+
* 带 invoke 型包贡献的产品用自己的 preload 入口(产品构建打包到
|
|
6
|
+
* 产品 main.js 旁)并经 setPreloadPath 注入,不走本入口。
|
|
7
|
+
*/
|
|
8
|
+
import { buildPreloadApi } from './preload.js';
|
|
9
|
+
buildPreloadApi();
|