@hflin/cclin 0.1.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/README.md +124 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +165 -0
- package/dist/index.js.map +1 -0
- package/dist/llm/client.d.ts +32 -0
- package/dist/llm/client.d.ts.map +1 -0
- package/dist/llm/client.js +280 -0
- package/dist/llm/client.js.map +1 -0
- package/dist/runtime/compaction.d.ts +49 -0
- package/dist/runtime/compaction.d.ts.map +1 -0
- package/dist/runtime/compaction.js +118 -0
- package/dist/runtime/compaction.js.map +1 -0
- package/dist/runtime/compaction.test.d.ts +7 -0
- package/dist/runtime/compaction.test.d.ts.map +1 -0
- package/dist/runtime/compaction.test.js +70 -0
- package/dist/runtime/compaction.test.js.map +1 -0
- package/dist/runtime/history.d.ts +34 -0
- package/dist/runtime/history.d.ts.map +1 -0
- package/dist/runtime/history.js +63 -0
- package/dist/runtime/history.js.map +1 -0
- package/dist/runtime/hooks.d.ts +54 -0
- package/dist/runtime/hooks.d.ts.map +1 -0
- package/dist/runtime/hooks.js +113 -0
- package/dist/runtime/hooks.js.map +1 -0
- package/dist/runtime/hooks.test.d.ts +7 -0
- package/dist/runtime/hooks.test.d.ts.map +1 -0
- package/dist/runtime/hooks.test.js +73 -0
- package/dist/runtime/hooks.test.js.map +1 -0
- package/dist/runtime/model-profile.d.ts +42 -0
- package/dist/runtime/model-profile.d.ts.map +1 -0
- package/dist/runtime/model-profile.js +84 -0
- package/dist/runtime/model-profile.js.map +1 -0
- package/dist/runtime/prompt.d.ts +38 -0
- package/dist/runtime/prompt.d.ts.map +1 -0
- package/dist/runtime/prompt.js +152 -0
- package/dist/runtime/prompt.js.map +1 -0
- package/dist/runtime/prompt.md +64 -0
- package/dist/runtime/prompt.test.d.ts +7 -0
- package/dist/runtime/prompt.test.d.ts.map +1 -0
- package/dist/runtime/prompt.test.js +38 -0
- package/dist/runtime/prompt.test.js.map +1 -0
- package/dist/runtime/react-loop.d.ts +82 -0
- package/dist/runtime/react-loop.d.ts.map +1 -0
- package/dist/runtime/react-loop.js +311 -0
- package/dist/runtime/react-loop.js.map +1 -0
- package/dist/runtime/react-loop.test.d.ts +7 -0
- package/dist/runtime/react-loop.test.d.ts.map +1 -0
- package/dist/runtime/react-loop.test.js +78 -0
- package/dist/runtime/react-loop.test.js.map +1 -0
- package/dist/runtime/session.d.ts +109 -0
- package/dist/runtime/session.d.ts.map +1 -0
- package/dist/runtime/session.js +252 -0
- package/dist/runtime/session.js.map +1 -0
- package/dist/runtime/skills.d.ts +36 -0
- package/dist/runtime/skills.d.ts.map +1 -0
- package/dist/runtime/skills.js +187 -0
- package/dist/runtime/skills.js.map +1 -0
- package/dist/runtime/skills.test.d.ts +7 -0
- package/dist/runtime/skills.test.d.ts.map +1 -0
- package/dist/runtime/skills.test.js +92 -0
- package/dist/runtime/skills.test.js.map +1 -0
- package/dist/tools/approval.d.ts +61 -0
- package/dist/tools/approval.d.ts.map +1 -0
- package/dist/tools/approval.js +119 -0
- package/dist/tools/approval.js.map +1 -0
- package/dist/tools/approval.test.d.ts +9 -0
- package/dist/tools/approval.test.d.ts.map +1 -0
- package/dist/tools/approval.test.js +112 -0
- package/dist/tools/approval.test.js.map +1 -0
- package/dist/tools/bash.d.ts +6 -0
- package/dist/tools/bash.d.ts.map +1 -0
- package/dist/tools/bash.js +58 -0
- package/dist/tools/bash.js.map +1 -0
- package/dist/tools/edit-file.d.ts +6 -0
- package/dist/tools/edit-file.d.ts.map +1 -0
- package/dist/tools/edit-file.js +58 -0
- package/dist/tools/edit-file.js.map +1 -0
- package/dist/tools/get-memory.d.ts +9 -0
- package/dist/tools/get-memory.d.ts.map +1 -0
- package/dist/tools/get-memory.js +56 -0
- package/dist/tools/get-memory.js.map +1 -0
- package/dist/tools/list-directory.d.ts +6 -0
- package/dist/tools/list-directory.d.ts.map +1 -0
- package/dist/tools/list-directory.js +68 -0
- package/dist/tools/list-directory.js.map +1 -0
- package/dist/tools/mcp-client.d.ts +74 -0
- package/dist/tools/mcp-client.d.ts.map +1 -0
- package/dist/tools/mcp-client.js +129 -0
- package/dist/tools/mcp-client.js.map +1 -0
- package/dist/tools/mcp-config.d.ts +31 -0
- package/dist/tools/mcp-config.d.ts.map +1 -0
- package/dist/tools/mcp-config.js +55 -0
- package/dist/tools/mcp-config.js.map +1 -0
- package/dist/tools/mcp-registry.d.ts +39 -0
- package/dist/tools/mcp-registry.d.ts.map +1 -0
- package/dist/tools/mcp-registry.js +88 -0
- package/dist/tools/mcp-registry.js.map +1 -0
- package/dist/tools/orchestrator.d.ts +52 -0
- package/dist/tools/orchestrator.d.ts.map +1 -0
- package/dist/tools/orchestrator.js +190 -0
- package/dist/tools/orchestrator.js.map +1 -0
- package/dist/tools/orchestrator.test.d.ts +8 -0
- package/dist/tools/orchestrator.test.d.ts.map +1 -0
- package/dist/tools/orchestrator.test.js +122 -0
- package/dist/tools/orchestrator.test.js.map +1 -0
- package/dist/tools/read-file.d.ts +6 -0
- package/dist/tools/read-file.d.ts.map +1 -0
- package/dist/tools/read-file.js +50 -0
- package/dist/tools/read-file.js.map +1 -0
- package/dist/tools/registry.d.ts +55 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +75 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/registry.test.d.ts +8 -0
- package/dist/tools/registry.test.d.ts.map +1 -0
- package/dist/tools/registry.test.js +100 -0
- package/dist/tools/registry.test.js.map +1 -0
- package/dist/tools/router.d.ts +62 -0
- package/dist/tools/router.d.ts.map +1 -0
- package/dist/tools/router.js +119 -0
- package/dist/tools/router.js.map +1 -0
- package/dist/tools/router.test.d.ts +7 -0
- package/dist/tools/router.test.d.ts.map +1 -0
- package/dist/tools/router.test.js +102 -0
- package/dist/tools/router.test.js.map +1 -0
- package/dist/tools/safety.d.ts +16 -0
- package/dist/tools/safety.d.ts.map +1 -0
- package/dist/tools/safety.js +81 -0
- package/dist/tools/safety.js.map +1 -0
- package/dist/tools/safety.test.d.ts +7 -0
- package/dist/tools/safety.test.d.ts.map +1 -0
- package/dist/tools/safety.test.js +104 -0
- package/dist/tools/safety.test.js.map +1 -0
- package/dist/tools/search-files.d.ts +9 -0
- package/dist/tools/search-files.d.ts.map +1 -0
- package/dist/tools/search-files.js +114 -0
- package/dist/tools/search-files.js.map +1 -0
- package/dist/tools/update-plan.d.ts +9 -0
- package/dist/tools/update-plan.d.ts.map +1 -0
- package/dist/tools/update-plan.js +99 -0
- package/dist/tools/update-plan.js.map +1 -0
- package/dist/tools/write-file.d.ts +6 -0
- package/dist/tools/write-file.d.ts.map +1 -0
- package/dist/tools/write-file.js +41 -0
- package/dist/tools/write-file.js.map +1 -0
- package/dist/tui/app.d.ts +31 -0
- package/dist/tui/app.d.ts.map +1 -0
- package/dist/tui/app.js +121 -0
- package/dist/tui/app.js.map +1 -0
- package/dist/tui/chatwidget/markdown_renderer.d.ts +20 -0
- package/dist/tui/chatwidget/markdown_renderer.d.ts.map +1 -0
- package/dist/tui/chatwidget/markdown_renderer.js +188 -0
- package/dist/tui/chatwidget/markdown_renderer.js.map +1 -0
- package/dist/tui/cjk_text.d.ts +25 -0
- package/dist/tui/cjk_text.d.ts.map +1 -0
- package/dist/tui/cjk_text.js +84 -0
- package/dist/tui/cjk_text.js.map +1 -0
- package/dist/tui/cjk_text.test.d.ts +2 -0
- package/dist/tui/cjk_text.test.d.ts.map +1 -0
- package/dist/tui/cjk_text.test.js +62 -0
- package/dist/tui/cjk_text.test.js.map +1 -0
- package/dist/tui/composer_input.d.ts +31 -0
- package/dist/tui/composer_input.d.ts.map +1 -0
- package/dist/tui/composer_input.js +184 -0
- package/dist/tui/composer_input.js.map +1 -0
- package/dist/tui/composer_input.test.d.ts +2 -0
- package/dist/tui/composer_input.test.d.ts.map +1 -0
- package/dist/tui/composer_input.test.js +87 -0
- package/dist/tui/composer_input.test.js.map +1 -0
- package/dist/tui/input.d.ts +21 -0
- package/dist/tui/input.d.ts.map +1 -0
- package/dist/tui/input.js +166 -0
- package/dist/tui/input.js.map +1 -0
- package/dist/tui/output.d.ts +17 -0
- package/dist/tui/output.d.ts.map +1 -0
- package/dist/tui/output.js +104 -0
- package/dist/tui/output.js.map +1 -0
- package/dist/tui/state/chat_timeline.d.ts +50 -0
- package/dist/tui/state/chat_timeline.d.ts.map +1 -0
- package/dist/tui/state/chat_timeline.js +129 -0
- package/dist/tui/state/chat_timeline.js.map +1 -0
- package/dist/tui/types.d.ts +45 -0
- package/dist/tui/types.d.ts.map +1 -0
- package/dist/tui/types.js +14 -0
- package/dist/tui/types.js.map +1 -0
- package/dist/types.d.ts +435 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/tokenizer.d.ts +21 -0
- package/dist/utils/tokenizer.d.ts.map +1 -0
- package/dist/utils/tokenizer.js +71 -0
- package/dist/utils/tokenizer.js.map +1 -0
- package/dist/utils/tokenizer.test.d.ts +7 -0
- package/dist/utils/tokenizer.test.d.ts.map +1 -0
- package/dist/utils/tokenizer.test.js +51 -0
- package/dist/utils/tokenizer.test.js.map +1 -0
- package/package.json +41 -0
- package/src/runtime/prompt.md +64 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Unit tests for ToolRegistry (Phase 3).
|
|
3
|
+
*
|
|
4
|
+
* Tests: register, get, has, size, getAll, registerMany,
|
|
5
|
+
* toOpenAITools, toMarkdown
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
8
|
+
import { ToolRegistry } from './registry.js';
|
|
9
|
+
// ─── Test Fixtures ────────────────────────────────────────────────────────────
|
|
10
|
+
function makeTool(name, mutating = false) {
|
|
11
|
+
return {
|
|
12
|
+
name,
|
|
13
|
+
description: `${name} tool description`,
|
|
14
|
+
inputSchema: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {
|
|
17
|
+
path: { type: 'string', description: 'file path' },
|
|
18
|
+
},
|
|
19
|
+
required: ['path'],
|
|
20
|
+
},
|
|
21
|
+
isMutating: mutating,
|
|
22
|
+
execute: async () => ({ output: `${name} result` }),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
// ─── Tests ────────────────────────────────────────────────────────────────────
|
|
26
|
+
describe('ToolRegistry', () => {
|
|
27
|
+
let registry;
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
registry = new ToolRegistry();
|
|
30
|
+
});
|
|
31
|
+
it('should start empty', () => {
|
|
32
|
+
expect(registry.size).toBe(0);
|
|
33
|
+
expect(registry.getAll()).toEqual([]);
|
|
34
|
+
});
|
|
35
|
+
it('should register and retrieve a tool', () => {
|
|
36
|
+
const tool = makeTool('read_file');
|
|
37
|
+
registry.register(tool);
|
|
38
|
+
expect(registry.size).toBe(1);
|
|
39
|
+
expect(registry.has('read_file')).toBe(true);
|
|
40
|
+
expect(registry.get('read_file')).toBe(tool);
|
|
41
|
+
});
|
|
42
|
+
it('should return undefined for unknown tools', () => {
|
|
43
|
+
expect(registry.get('nonexistent')).toBeUndefined();
|
|
44
|
+
expect(registry.has('nonexistent')).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
it('should register many tools at once', () => {
|
|
47
|
+
const tools = [makeTool('a'), makeTool('b'), makeTool('c')];
|
|
48
|
+
registry.registerMany(tools);
|
|
49
|
+
expect(registry.size).toBe(3);
|
|
50
|
+
expect(registry.has('a')).toBe(true);
|
|
51
|
+
expect(registry.has('b')).toBe(true);
|
|
52
|
+
expect(registry.has('c')).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
it('should overwrite on duplicate name', () => {
|
|
55
|
+
const tool1 = makeTool('read_file');
|
|
56
|
+
const tool2 = makeTool('read_file');
|
|
57
|
+
tool2.description = 'updated description';
|
|
58
|
+
registry.register(tool1);
|
|
59
|
+
registry.register(tool2);
|
|
60
|
+
expect(registry.size).toBe(1);
|
|
61
|
+
expect(registry.get('read_file')?.description).toBe('updated description');
|
|
62
|
+
});
|
|
63
|
+
it('should convert to OpenAI tools format', () => {
|
|
64
|
+
registry.register(makeTool('bash'));
|
|
65
|
+
const openAITools = registry.toOpenAITools();
|
|
66
|
+
expect(openAITools).toHaveLength(1);
|
|
67
|
+
expect(openAITools[0]).toEqual({
|
|
68
|
+
type: 'function',
|
|
69
|
+
function: {
|
|
70
|
+
name: 'bash',
|
|
71
|
+
description: 'bash tool description',
|
|
72
|
+
parameters: {
|
|
73
|
+
type: 'object',
|
|
74
|
+
properties: {
|
|
75
|
+
path: {
|
|
76
|
+
type: 'string',
|
|
77
|
+
description: 'file path',
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
required: ['path'],
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
it('should generate markdown text', () => {
|
|
86
|
+
registry.register(makeTool('read_file'));
|
|
87
|
+
const md = registry.toMarkdown();
|
|
88
|
+
expect(md).toContain('### read_file');
|
|
89
|
+
expect(md).toContain('read_file tool description');
|
|
90
|
+
expect(md).toContain('Parameters Schema');
|
|
91
|
+
expect(md).toContain('"type": "object"');
|
|
92
|
+
});
|
|
93
|
+
it('should return all tools in insertion order', () => {
|
|
94
|
+
registry.register(makeTool('a'));
|
|
95
|
+
registry.register(makeTool('b'));
|
|
96
|
+
const all = registry.getAll();
|
|
97
|
+
expect(all.map((t) => t.name)).toEqual(['a', 'b']);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
//# sourceMappingURL=registry.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.test.js","sourceRoot":"","sources":["../../src/tools/registry.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file 工具路由器 — 统一管理内置工具和 MCP 工具。
|
|
3
|
+
*
|
|
4
|
+
* Phase 9:在 ToolRegistry(内置)和 McpToolRegistry(MCP)之上
|
|
5
|
+
* 提供统一的工具查询、执行描述生成接口。
|
|
6
|
+
*
|
|
7
|
+
* 设计思路:
|
|
8
|
+
* 1. ToolRouter 不替代 ToolRegistry/McpToolRegistry,而是组合它们
|
|
9
|
+
* 2. 工具查找优先内置,fallback 到 MCP
|
|
10
|
+
* 3. 实现 ToolQueryable 接口,可直接替换 ToolOrchestrator 的依赖
|
|
11
|
+
* 4. toOpenAITools() / toMarkdown() 合并两组工具的输出
|
|
12
|
+
*/
|
|
13
|
+
import type { ToolDefinition, MCPServerConfig } from '../types.js';
|
|
14
|
+
/**
|
|
15
|
+
* 统一工具路由器。
|
|
16
|
+
*
|
|
17
|
+
* 用法:
|
|
18
|
+
* ```ts
|
|
19
|
+
* const router = new ToolRouter()
|
|
20
|
+
* router.registerNativeTools([readFileTool, bashTool])
|
|
21
|
+
* await router.loadMcpServers(mcpConfig)
|
|
22
|
+
*
|
|
23
|
+
* const tools = router.toOpenAITools() // 传给 LLM
|
|
24
|
+
* const tool = router.get('bash') // 查找工具
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare class ToolRouter {
|
|
28
|
+
private nativeRegistry;
|
|
29
|
+
private mcpRegistry;
|
|
30
|
+
/** 注册单个内置工具。 */
|
|
31
|
+
registerNativeTool(tool: ToolDefinition): void;
|
|
32
|
+
/** 批量注册内置工具。 */
|
|
33
|
+
registerNativeTools(tools: ToolDefinition[]): void;
|
|
34
|
+
/** 连接并加载所有 MCP Server。 */
|
|
35
|
+
loadMcpServers(servers: Record<string, MCPServerConfig>): Promise<number>;
|
|
36
|
+
/** 获取指定工具(优先内置,fallback MCP)。 */
|
|
37
|
+
get(name: string): ToolDefinition | undefined;
|
|
38
|
+
/** 获取所有工具(内置 + MCP)。 */
|
|
39
|
+
getAllTools(): ToolDefinition[];
|
|
40
|
+
/** 检查工具是否存在。 */
|
|
41
|
+
has(name: string): boolean;
|
|
42
|
+
/** 获取工具数量统计。 */
|
|
43
|
+
getToolCount(): {
|
|
44
|
+
native: number;
|
|
45
|
+
mcp: number;
|
|
46
|
+
total: number;
|
|
47
|
+
};
|
|
48
|
+
/** 转换为 OpenAI function calling 的 tools 参数格式。 */
|
|
49
|
+
toOpenAITools(): Array<{
|
|
50
|
+
type: 'function';
|
|
51
|
+
function: {
|
|
52
|
+
name: string;
|
|
53
|
+
description: string;
|
|
54
|
+
parameters: Record<string, unknown>;
|
|
55
|
+
};
|
|
56
|
+
}>;
|
|
57
|
+
/** 转换为 Markdown 文本,供系统提示词注入。 */
|
|
58
|
+
toMarkdown(): string;
|
|
59
|
+
/** 清理所有 MCP 连接。 */
|
|
60
|
+
dispose(): Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=router.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/tools/router.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAMlE;;;;;;;;;;;;GAYG;AACH,qBAAa,UAAU;IACnB,OAAO,CAAC,cAAc,CAAqB;IAC3C,OAAO,CAAC,WAAW,CAAwB;IAI3C,gBAAgB;IAChB,kBAAkB,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI;IAI9C,gBAAgB;IAChB,mBAAmB,CAAC,KAAK,EAAE,cAAc,EAAE,GAAG,IAAI;IAIlD,0BAA0B;IACpB,cAAc,CAChB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,GACzC,OAAO,CAAC,MAAM,CAAC;IAMlB,iCAAiC;IACjC,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS;IAK7C,wBAAwB;IACxB,WAAW,IAAI,cAAc,EAAE;IAO/B,gBAAgB;IAChB,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAK1B,gBAAgB;IAChB,YAAY,IAAI;QACZ,MAAM,EAAE,MAAM,CAAA;QACd,GAAG,EAAE,MAAM,CAAA;QACX,KAAK,EAAE,MAAM,CAAA;KAChB;IAQD,gDAAgD;IAChD,aAAa,IAAI,KAAK,CAAC;QACnB,IAAI,EAAE,UAAU,CAAA;QAChB,QAAQ,EAAE;YACN,IAAI,EAAE,MAAM,CAAA;YACZ,WAAW,EAAE,MAAM,CAAA;YACnB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;SACtC,CAAA;KACJ,CAAC;IAWF,gCAAgC;IAChC,UAAU,IAAI,MAAM;IAyCpB,mBAAmB;IACb,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAGjC"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file 工具路由器 — 统一管理内置工具和 MCP 工具。
|
|
3
|
+
*
|
|
4
|
+
* Phase 9:在 ToolRegistry(内置)和 McpToolRegistry(MCP)之上
|
|
5
|
+
* 提供统一的工具查询、执行描述生成接口。
|
|
6
|
+
*
|
|
7
|
+
* 设计思路:
|
|
8
|
+
* 1. ToolRouter 不替代 ToolRegistry/McpToolRegistry,而是组合它们
|
|
9
|
+
* 2. 工具查找优先内置,fallback 到 MCP
|
|
10
|
+
* 3. 实现 ToolQueryable 接口,可直接替换 ToolOrchestrator 的依赖
|
|
11
|
+
* 4. toOpenAITools() / toMarkdown() 合并两组工具的输出
|
|
12
|
+
*/
|
|
13
|
+
import { ToolRegistry } from './registry.js';
|
|
14
|
+
import { McpToolRegistry } from './mcp-registry.js';
|
|
15
|
+
// ─── ToolRouter 类 ────────────────────────────────────────────────────────
|
|
16
|
+
/**
|
|
17
|
+
* 统一工具路由器。
|
|
18
|
+
*
|
|
19
|
+
* 用法:
|
|
20
|
+
* ```ts
|
|
21
|
+
* const router = new ToolRouter()
|
|
22
|
+
* router.registerNativeTools([readFileTool, bashTool])
|
|
23
|
+
* await router.loadMcpServers(mcpConfig)
|
|
24
|
+
*
|
|
25
|
+
* const tools = router.toOpenAITools() // 传给 LLM
|
|
26
|
+
* const tool = router.get('bash') // 查找工具
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export class ToolRouter {
|
|
30
|
+
nativeRegistry = new ToolRegistry();
|
|
31
|
+
mcpRegistry = new McpToolRegistry();
|
|
32
|
+
// ── 注册方法 ──────────────────────────────────────
|
|
33
|
+
/** 注册单个内置工具。 */
|
|
34
|
+
registerNativeTool(tool) {
|
|
35
|
+
this.nativeRegistry.register(tool);
|
|
36
|
+
}
|
|
37
|
+
/** 批量注册内置工具。 */
|
|
38
|
+
registerNativeTools(tools) {
|
|
39
|
+
this.nativeRegistry.registerMany(tools);
|
|
40
|
+
}
|
|
41
|
+
/** 连接并加载所有 MCP Server。 */
|
|
42
|
+
async loadMcpServers(servers) {
|
|
43
|
+
return this.mcpRegistry.loadServers(servers);
|
|
44
|
+
}
|
|
45
|
+
// ── 查询方法 ──────────────────────────────────────
|
|
46
|
+
/** 获取指定工具(优先内置,fallback MCP)。 */
|
|
47
|
+
get(name) {
|
|
48
|
+
return this.nativeRegistry.get(name)
|
|
49
|
+
?? this.mcpRegistry.get(name);
|
|
50
|
+
}
|
|
51
|
+
/** 获取所有工具(内置 + MCP)。 */
|
|
52
|
+
getAllTools() {
|
|
53
|
+
return [
|
|
54
|
+
...this.nativeRegistry.getAll(),
|
|
55
|
+
...this.mcpRegistry.getAll(),
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
/** 检查工具是否存在。 */
|
|
59
|
+
has(name) {
|
|
60
|
+
return this.nativeRegistry.has(name)
|
|
61
|
+
|| this.mcpRegistry.has(name);
|
|
62
|
+
}
|
|
63
|
+
/** 获取工具数量统计。 */
|
|
64
|
+
getToolCount() {
|
|
65
|
+
const native = this.nativeRegistry.size;
|
|
66
|
+
const mcp = this.mcpRegistry.size;
|
|
67
|
+
return { native, mcp, total: native + mcp };
|
|
68
|
+
}
|
|
69
|
+
// ── 格式转换 ──────────────────────────────────────
|
|
70
|
+
/** 转换为 OpenAI function calling 的 tools 参数格式。 */
|
|
71
|
+
toOpenAITools() {
|
|
72
|
+
return this.getAllTools().map((tool) => ({
|
|
73
|
+
type: 'function',
|
|
74
|
+
function: {
|
|
75
|
+
name: tool.name,
|
|
76
|
+
description: tool.description,
|
|
77
|
+
parameters: tool.inputSchema,
|
|
78
|
+
},
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
/** 转换为 Markdown 文本,供系统提示词注入。 */
|
|
82
|
+
toMarkdown() {
|
|
83
|
+
const sections = [];
|
|
84
|
+
const nativeTools = this.nativeRegistry.getAll();
|
|
85
|
+
if (nativeTools.length > 0) {
|
|
86
|
+
sections.push('## Built-in Tools\n');
|
|
87
|
+
for (const tool of nativeTools) {
|
|
88
|
+
sections.push(`### ${tool.name}\n${tool.description}\n\n` +
|
|
89
|
+
`**Parameters Schema**:\n\`\`\`json\n` +
|
|
90
|
+
`${JSON.stringify(tool.inputSchema, null, 2)}\n\`\`\``);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const mcpTools = this.mcpRegistry.getAll();
|
|
94
|
+
if (mcpTools.length > 0) {
|
|
95
|
+
sections.push('\n## External MCP Tools\n');
|
|
96
|
+
const grouped = new Map();
|
|
97
|
+
for (const tool of mcpTools) {
|
|
98
|
+
const list = grouped.get(tool.serverName) ?? [];
|
|
99
|
+
list.push(tool);
|
|
100
|
+
grouped.set(tool.serverName, list);
|
|
101
|
+
}
|
|
102
|
+
for (const [server, tools] of grouped) {
|
|
103
|
+
sections.push(`**Server: ${server}**\n`);
|
|
104
|
+
for (const tool of tools) {
|
|
105
|
+
sections.push(`### ${tool.name}\n${tool.description}\n\n` +
|
|
106
|
+
`**Parameters Schema**:\n\`\`\`json\n` +
|
|
107
|
+
`${JSON.stringify(tool.inputSchema, null, 2)}\n\`\`\``);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return sections.join('\n\n');
|
|
112
|
+
}
|
|
113
|
+
// ── 生命周期 ──────────────────────────────────────
|
|
114
|
+
/** 清理所有 MCP 连接。 */
|
|
115
|
+
async dispose() {
|
|
116
|
+
await this.mcpRegistry.dispose();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=router.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"router.js","sourceRoot":"","sources":["../../src/tools/router.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"router.test.d.ts","sourceRoot":"","sources":["../../src/tools/router.test.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Unit tests for ToolRouter (Phase 9).
|
|
3
|
+
*
|
|
4
|
+
* Tests: Native priority, get, has, toOpenAITools, toMarkdown
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
7
|
+
import { ToolRouter } from './router.js';
|
|
8
|
+
import { ToolRegistry } from './registry.js';
|
|
9
|
+
// Simple mock for McpToolRegistry to avoid full MCP client dependency
|
|
10
|
+
class MockMcpRegistry {
|
|
11
|
+
tools = new Map();
|
|
12
|
+
async getTools() {
|
|
13
|
+
return Array.from(this.tools.values());
|
|
14
|
+
}
|
|
15
|
+
get(name) {
|
|
16
|
+
return this.tools.get(name);
|
|
17
|
+
}
|
|
18
|
+
has(name) {
|
|
19
|
+
return this.tools.has(name);
|
|
20
|
+
}
|
|
21
|
+
getAll() {
|
|
22
|
+
return Array.from(this.tools.values());
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
describe('ToolRouter', () => {
|
|
26
|
+
let nativeReg;
|
|
27
|
+
let mcpReg;
|
|
28
|
+
let router;
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
nativeReg = new ToolRegistry();
|
|
31
|
+
mcpReg = new MockMcpRegistry();
|
|
32
|
+
router = new ToolRouter();
|
|
33
|
+
// Inject nativeRegistry and mcpRegistry manually for tests
|
|
34
|
+
// since they are internal and not passed via constructor
|
|
35
|
+
Object.assign(router, { nativeRegistry: nativeReg, mcpRegistry: mcpReg });
|
|
36
|
+
});
|
|
37
|
+
function makeTool(name) {
|
|
38
|
+
return {
|
|
39
|
+
name,
|
|
40
|
+
description: `${name} desc`,
|
|
41
|
+
inputSchema: { type: 'object', properties: {} },
|
|
42
|
+
isMutating: false,
|
|
43
|
+
execute: vi.fn(),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
it('should query both registries for has()', async () => {
|
|
47
|
+
nativeReg.register(makeTool('native_tool'));
|
|
48
|
+
mcpReg.tools.set('mcp_tool', makeTool('mcp_tool'));
|
|
49
|
+
expect(router.has('native_tool')).toBe(true);
|
|
50
|
+
expect(router.has('mcp_tool')).toBe(true);
|
|
51
|
+
expect(router.has('unknown')).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
it('should return undefined from get() if not found', async () => {
|
|
54
|
+
expect(router.get('unknown')).toBeUndefined();
|
|
55
|
+
});
|
|
56
|
+
it('should get from native registry', async () => {
|
|
57
|
+
nativeReg.register(makeTool('native_tool'));
|
|
58
|
+
const tool = router.get('native_tool');
|
|
59
|
+
expect(tool).toBeDefined();
|
|
60
|
+
expect(tool?.name).toBe('native_tool');
|
|
61
|
+
});
|
|
62
|
+
it('should get from mcp registry', async () => {
|
|
63
|
+
mcpReg.tools.set('mcp_tool', makeTool('mcp_tool'));
|
|
64
|
+
const tool = router.get('mcp_tool');
|
|
65
|
+
expect(tool).toBeDefined();
|
|
66
|
+
expect(tool?.name).toBe('mcp_tool');
|
|
67
|
+
});
|
|
68
|
+
it('should prioritize native tools over mcp tools on exact name match', async () => {
|
|
69
|
+
const nativeOne = makeTool('conflict_tool');
|
|
70
|
+
nativeOne.description = 'NATIVE';
|
|
71
|
+
const mcpOne = makeTool('conflict_tool');
|
|
72
|
+
mcpOne.description = 'MCP';
|
|
73
|
+
nativeReg.register(nativeOne);
|
|
74
|
+
mcpReg.tools.set('conflict_tool', mcpOne);
|
|
75
|
+
// `get` should return the native one
|
|
76
|
+
const tool = router.get('conflict_tool');
|
|
77
|
+
expect(tool?.description).toBe('NATIVE');
|
|
78
|
+
});
|
|
79
|
+
it('should combine tools from both in getAllTools()', async () => {
|
|
80
|
+
nativeReg.register(makeTool('n1'));
|
|
81
|
+
mcpReg.tools.set('m1', makeTool('m1'));
|
|
82
|
+
const all = router.getAllTools();
|
|
83
|
+
expect(all).toHaveLength(2);
|
|
84
|
+
expect(all.map(t => t.name).sort()).toEqual(['m1', 'n1']);
|
|
85
|
+
});
|
|
86
|
+
it('should correctly build markdown docs', async () => {
|
|
87
|
+
nativeReg.register(makeTool('sys_info'));
|
|
88
|
+
mcpReg.tools.set('web_search', makeTool('web_search'));
|
|
89
|
+
const md = router.toMarkdown();
|
|
90
|
+
expect(md).toContain('### sys_info');
|
|
91
|
+
expect(md).toContain('### web_search');
|
|
92
|
+
});
|
|
93
|
+
it('should correctly format combined list as OpenAI tools', async () => {
|
|
94
|
+
nativeReg.register(makeTool('sys_info'));
|
|
95
|
+
mcpReg.tools.set('web_search', makeTool('web_search'));
|
|
96
|
+
const schemas = router.toOpenAITools();
|
|
97
|
+
expect(schemas).toHaveLength(2);
|
|
98
|
+
const names = schemas.map(s => s.function.name).sort();
|
|
99
|
+
expect(names).toEqual(['sys_info', 'web_search']);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
//# sourceMappingURL=router.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"router.test.js","sourceRoot":"","sources":["../../src/tools/router.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file 安全工具函数 — 路径校验、命令分级、敏感文件检测。
|
|
3
|
+
*
|
|
4
|
+
* Phase 3:基础安全机制。
|
|
5
|
+
* Phase 4 将在此基础上增加完整审批流程。
|
|
6
|
+
*/
|
|
7
|
+
export declare function validatePath(filePath: string): {
|
|
8
|
+
ok: true;
|
|
9
|
+
} | {
|
|
10
|
+
ok: false;
|
|
11
|
+
error: string;
|
|
12
|
+
};
|
|
13
|
+
export type CommandSafety = 'safe' | 'confirm' | 'block';
|
|
14
|
+
export declare function classifyCommand(command: string): CommandSafety;
|
|
15
|
+
export declare function isSensitiveFile(filePath: string): boolean;
|
|
16
|
+
//# sourceMappingURL=safety.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safety.d.ts","sourceRoot":"","sources":["../../src/tools/safety.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAsDH,wBAAgB,YAAY,CACxB,QAAQ,EAAE,MAAM,GACjB;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAS7C;AAID,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAA;AAExD,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,CAU9D;AAID,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAQzD"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file 安全工具函数 — 路径校验、命令分级、敏感文件检测。
|
|
3
|
+
*
|
|
4
|
+
* Phase 3:基础安全机制。
|
|
5
|
+
* Phase 4 将在此基础上增加完整审批流程。
|
|
6
|
+
*/
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
// ─── 敏感文件列表 ─────────────────────────────────────────────────────────────
|
|
9
|
+
/** 不应被读写的敏感文件模式。 */
|
|
10
|
+
const SENSITIVE_PATTERNS = [
|
|
11
|
+
'.env',
|
|
12
|
+
'.env.local',
|
|
13
|
+
'.env.production',
|
|
14
|
+
'id_rsa',
|
|
15
|
+
'id_ed25519',
|
|
16
|
+
'id_ecdsa',
|
|
17
|
+
'.ssh/config',
|
|
18
|
+
'.npmrc',
|
|
19
|
+
'.pypirc',
|
|
20
|
+
'credentials',
|
|
21
|
+
'shadow',
|
|
22
|
+
'passwd',
|
|
23
|
+
];
|
|
24
|
+
// ─── 危险命令列表 ─────────────────────────────────────────────────────────────
|
|
25
|
+
/** 绝对禁止执行的命令(block 级别)。 */
|
|
26
|
+
const BLOCKED_COMMANDS = new Set([
|
|
27
|
+
'rm -rf /',
|
|
28
|
+
'rm -rf ~',
|
|
29
|
+
'rm -rf /*',
|
|
30
|
+
'mkfs',
|
|
31
|
+
'dd if=',
|
|
32
|
+
':(){:|:&};:',
|
|
33
|
+
'shutdown',
|
|
34
|
+
'reboot',
|
|
35
|
+
'halt',
|
|
36
|
+
'poweroff',
|
|
37
|
+
'format',
|
|
38
|
+
]);
|
|
39
|
+
/** 需要用户确认的命令前缀(confirm 级别)。 */
|
|
40
|
+
const CONFIRM_PREFIXES = [
|
|
41
|
+
'rm ',
|
|
42
|
+
'rmdir ',
|
|
43
|
+
'del ',
|
|
44
|
+
'rd ',
|
|
45
|
+
'mv ',
|
|
46
|
+
'chmod ',
|
|
47
|
+
'chown ',
|
|
48
|
+
'kill ',
|
|
49
|
+
'pkill ',
|
|
50
|
+
];
|
|
51
|
+
// ─── 路径校验 ─────────────────────────────────────────────────────────────────
|
|
52
|
+
export function validatePath(filePath) {
|
|
53
|
+
const normalized = path.normalize(filePath);
|
|
54
|
+
if (normalized.includes('..')) {
|
|
55
|
+
return { ok: false, error: `Path traversal detected: ${filePath}` };
|
|
56
|
+
}
|
|
57
|
+
if (isSensitiveFile(filePath)) {
|
|
58
|
+
return { ok: false, error: `Access to sensitive file denied: ${filePath}` };
|
|
59
|
+
}
|
|
60
|
+
return { ok: true };
|
|
61
|
+
}
|
|
62
|
+
export function classifyCommand(command) {
|
|
63
|
+
const trimmed = command.trim().toLowerCase();
|
|
64
|
+
for (const blocked of BLOCKED_COMMANDS) {
|
|
65
|
+
if (trimmed.includes(blocked))
|
|
66
|
+
return 'block';
|
|
67
|
+
}
|
|
68
|
+
for (const prefix of CONFIRM_PREFIXES) {
|
|
69
|
+
if (trimmed.startsWith(prefix))
|
|
70
|
+
return 'confirm';
|
|
71
|
+
}
|
|
72
|
+
return 'safe';
|
|
73
|
+
}
|
|
74
|
+
// ─── 敏感文件检测 ──────────────────────────────────────────────────────────────
|
|
75
|
+
export function isSensitiveFile(filePath) {
|
|
76
|
+
const basename = path.basename(filePath);
|
|
77
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
78
|
+
return SENSITIVE_PATTERNS.some((pattern) => basename === pattern ||
|
|
79
|
+
normalized.endsWith(`/${pattern}`));
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=safety.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safety.js","sourceRoot":"","sources":["../../src/tools/safety.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA;AAEjC,2EAA2E;AAE3E,oBAAoB;AACpB,MAAM,kBAAkB,GAAG;IACvB,MAAM;IACN,YAAY;IACZ,iBAAiB;IACjB,QAAQ;IACR,YAAY;IACZ,UAAU;IACV,aAAa;IACb,QAAQ;IACR,SAAS;IACT,aAAa;IACb,QAAQ;IACR,QAAQ;CACX,CAAA;AAED,2EAA2E;AAE3E,2BAA2B;AAC3B,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC;IAC7B,UAAU;IACV,UAAU;IACV,WAAW;IACX,MAAM;IACN,QAAQ;IACR,aAAa;IACb,UAAU;IACV,QAAQ;IACR,MAAM;IACN,UAAU;IACV,QAAQ;CACX,CAAC,CAAA;AAEF,+BAA+B;AAC/B,MAAM,gBAAgB,GAAG;IACrB,KAAK;IACL,QAAQ;IACR,MAAM;IACN,KAAK;IACL,KAAK;IACL,QAAQ;IACR,QAAQ;IACR,OAAO;IACP,QAAQ;CACX,CAAA;AAED,6EAA6E;AAE7E,MAAM,UAAU,YAAY,CACxB,QAAgB;IAEhB,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAA;IAC3C,IAAI,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QAC5B,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,4BAA4B,QAAQ,EAAE,EAAE,CAAA;IACvE,CAAC;IACD,IAAI,eAAe,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC5B,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,oCAAoC,QAAQ,EAAE,EAAE,CAAA;IAC/E,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAA;AACvB,CAAC;AAMD,MAAM,UAAU,eAAe,CAAC,OAAe;IAC3C,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;IAE5C,KAAK,MAAM,OAAO,IAAI,gBAAgB,EAAE,CAAC;QACrC,IAAI,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;YAAE,OAAO,OAAO,CAAA;IACjD,CAAC;IACD,KAAK,MAAM,MAAM,IAAI,gBAAgB,EAAE,CAAC;QACpC,IAAI,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC;YAAE,OAAO,SAAS,CAAA;IACpD,CAAC;IACD,OAAO,MAAM,CAAA;AACjB,CAAC;AAED,4EAA4E;AAE5E,MAAM,UAAU,eAAe,CAAC,QAAgB;IAC5C,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAA;IACxC,MAAM,UAAU,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;IAC/C,OAAO,kBAAkB,CAAC,IAAI,CAC1B,CAAC,OAAO,EAAE,EAAE,CACR,QAAQ,KAAK,OAAO;QACpB,UAAU,CAAC,QAAQ,CAAC,IAAI,OAAO,EAAE,CAAC,CACzC,CAAA;AACL,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safety.test.d.ts","sourceRoot":"","sources":["../../src/tools/safety.test.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Unit tests for safety module (Phase 3).
|
|
3
|
+
*
|
|
4
|
+
* Tests: validatePath, classifyCommand, isSensitiveFile
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect } from 'vitest';
|
|
7
|
+
import { validatePath, classifyCommand, isSensitiveFile } from './safety.js';
|
|
8
|
+
// ─── validatePath ─────────────────────────────────────────────────────────────
|
|
9
|
+
describe('validatePath', () => {
|
|
10
|
+
it('should accept a normal relative path', () => {
|
|
11
|
+
const result = validatePath('src/index.ts');
|
|
12
|
+
expect(result).toEqual({ ok: true });
|
|
13
|
+
});
|
|
14
|
+
it('should accept an absolute path without traversal', () => {
|
|
15
|
+
const result = validatePath('/home/user/project/file.ts');
|
|
16
|
+
expect(result).toEqual({ ok: true });
|
|
17
|
+
});
|
|
18
|
+
it('should reject path traversal with ..', () => {
|
|
19
|
+
const result = validatePath('../../../etc/passwd');
|
|
20
|
+
expect(result.ok).toBe(false);
|
|
21
|
+
if (!result.ok) {
|
|
22
|
+
expect(result.error).toContain('Path traversal');
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
it('should reject hidden traversal like foo/../../bar', () => {
|
|
26
|
+
const result = validatePath('foo/../../bar');
|
|
27
|
+
expect(result.ok).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
it('should reject access to sensitive files', () => {
|
|
30
|
+
const result = validatePath('.env');
|
|
31
|
+
expect(result.ok).toBe(false);
|
|
32
|
+
if (!result.ok) {
|
|
33
|
+
expect(result.error).toContain('sensitive');
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
it('should reject access to SSH keys', () => {
|
|
37
|
+
const result = validatePath('id_rsa');
|
|
38
|
+
expect(result.ok).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
// ─── classifyCommand ──────────────────────────────────────────────────────────
|
|
42
|
+
describe('classifyCommand', () => {
|
|
43
|
+
it('should block rm -rf /', () => {
|
|
44
|
+
expect(classifyCommand('rm -rf /')).toBe('block');
|
|
45
|
+
});
|
|
46
|
+
it('should block shutdown', () => {
|
|
47
|
+
expect(classifyCommand('shutdown')).toBe('block');
|
|
48
|
+
});
|
|
49
|
+
it('should block fork bomb', () => {
|
|
50
|
+
expect(classifyCommand(':(){:|:&};:')).toBe('block');
|
|
51
|
+
});
|
|
52
|
+
it('should require confirm for rm commands', () => {
|
|
53
|
+
expect(classifyCommand('rm foo.txt')).toBe('confirm');
|
|
54
|
+
});
|
|
55
|
+
it('should require confirm for mv commands', () => {
|
|
56
|
+
expect(classifyCommand('mv a.txt b.txt')).toBe('confirm');
|
|
57
|
+
});
|
|
58
|
+
it('should require confirm for kill commands', () => {
|
|
59
|
+
expect(classifyCommand('kill 1234')).toBe('confirm');
|
|
60
|
+
});
|
|
61
|
+
it('should mark ls as safe', () => {
|
|
62
|
+
expect(classifyCommand('ls -la')).toBe('safe');
|
|
63
|
+
});
|
|
64
|
+
it('should mark cat as safe', () => {
|
|
65
|
+
expect(classifyCommand('cat file.txt')).toBe('safe');
|
|
66
|
+
});
|
|
67
|
+
it('should mark echo as safe', () => {
|
|
68
|
+
expect(classifyCommand('echo hello')).toBe('safe');
|
|
69
|
+
});
|
|
70
|
+
it('should be case-insensitive', () => {
|
|
71
|
+
expect(classifyCommand('SHUTDOWN')).toBe('block');
|
|
72
|
+
});
|
|
73
|
+
it('should trim whitespace', () => {
|
|
74
|
+
expect(classifyCommand(' rm foo ')).toBe('confirm');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
// ─── isSensitiveFile ──────────────────────────────────────────────────────────
|
|
78
|
+
describe('isSensitiveFile', () => {
|
|
79
|
+
it('should detect .env', () => {
|
|
80
|
+
expect(isSensitiveFile('.env')).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
it('should detect .env.local', () => {
|
|
83
|
+
expect(isSensitiveFile('.env.local')).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
it('should detect id_rsa in any directory', () => {
|
|
86
|
+
expect(isSensitiveFile('/home/user/.ssh/id_rsa')).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
it('should detect id_ed25519', () => {
|
|
89
|
+
expect(isSensitiveFile('id_ed25519')).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
it('should detect .ssh/config via path suffix', () => {
|
|
92
|
+
expect(isSensitiveFile('/home/user/.ssh/config')).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
it('should detect .npmrc', () => {
|
|
95
|
+
expect(isSensitiveFile('.npmrc')).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
it('should not flag normal files', () => {
|
|
98
|
+
expect(isSensitiveFile('index.ts')).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
it('should not flag package.json', () => {
|
|
101
|
+
expect(isSensitiveFile('package.json')).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
//# sourceMappingURL=safety.test.js.map
|