@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.
Files changed (200) hide show
  1. package/README.md +124 -0
  2. package/dist/index.d.ts +10 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +165 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/llm/client.d.ts +32 -0
  7. package/dist/llm/client.d.ts.map +1 -0
  8. package/dist/llm/client.js +280 -0
  9. package/dist/llm/client.js.map +1 -0
  10. package/dist/runtime/compaction.d.ts +49 -0
  11. package/dist/runtime/compaction.d.ts.map +1 -0
  12. package/dist/runtime/compaction.js +118 -0
  13. package/dist/runtime/compaction.js.map +1 -0
  14. package/dist/runtime/compaction.test.d.ts +7 -0
  15. package/dist/runtime/compaction.test.d.ts.map +1 -0
  16. package/dist/runtime/compaction.test.js +70 -0
  17. package/dist/runtime/compaction.test.js.map +1 -0
  18. package/dist/runtime/history.d.ts +34 -0
  19. package/dist/runtime/history.d.ts.map +1 -0
  20. package/dist/runtime/history.js +63 -0
  21. package/dist/runtime/history.js.map +1 -0
  22. package/dist/runtime/hooks.d.ts +54 -0
  23. package/dist/runtime/hooks.d.ts.map +1 -0
  24. package/dist/runtime/hooks.js +113 -0
  25. package/dist/runtime/hooks.js.map +1 -0
  26. package/dist/runtime/hooks.test.d.ts +7 -0
  27. package/dist/runtime/hooks.test.d.ts.map +1 -0
  28. package/dist/runtime/hooks.test.js +73 -0
  29. package/dist/runtime/hooks.test.js.map +1 -0
  30. package/dist/runtime/model-profile.d.ts +42 -0
  31. package/dist/runtime/model-profile.d.ts.map +1 -0
  32. package/dist/runtime/model-profile.js +84 -0
  33. package/dist/runtime/model-profile.js.map +1 -0
  34. package/dist/runtime/prompt.d.ts +38 -0
  35. package/dist/runtime/prompt.d.ts.map +1 -0
  36. package/dist/runtime/prompt.js +152 -0
  37. package/dist/runtime/prompt.js.map +1 -0
  38. package/dist/runtime/prompt.md +64 -0
  39. package/dist/runtime/prompt.test.d.ts +7 -0
  40. package/dist/runtime/prompt.test.d.ts.map +1 -0
  41. package/dist/runtime/prompt.test.js +38 -0
  42. package/dist/runtime/prompt.test.js.map +1 -0
  43. package/dist/runtime/react-loop.d.ts +82 -0
  44. package/dist/runtime/react-loop.d.ts.map +1 -0
  45. package/dist/runtime/react-loop.js +311 -0
  46. package/dist/runtime/react-loop.js.map +1 -0
  47. package/dist/runtime/react-loop.test.d.ts +7 -0
  48. package/dist/runtime/react-loop.test.d.ts.map +1 -0
  49. package/dist/runtime/react-loop.test.js +78 -0
  50. package/dist/runtime/react-loop.test.js.map +1 -0
  51. package/dist/runtime/session.d.ts +109 -0
  52. package/dist/runtime/session.d.ts.map +1 -0
  53. package/dist/runtime/session.js +252 -0
  54. package/dist/runtime/session.js.map +1 -0
  55. package/dist/runtime/skills.d.ts +36 -0
  56. package/dist/runtime/skills.d.ts.map +1 -0
  57. package/dist/runtime/skills.js +187 -0
  58. package/dist/runtime/skills.js.map +1 -0
  59. package/dist/runtime/skills.test.d.ts +7 -0
  60. package/dist/runtime/skills.test.d.ts.map +1 -0
  61. package/dist/runtime/skills.test.js +92 -0
  62. package/dist/runtime/skills.test.js.map +1 -0
  63. package/dist/tools/approval.d.ts +61 -0
  64. package/dist/tools/approval.d.ts.map +1 -0
  65. package/dist/tools/approval.js +119 -0
  66. package/dist/tools/approval.js.map +1 -0
  67. package/dist/tools/approval.test.d.ts +9 -0
  68. package/dist/tools/approval.test.d.ts.map +1 -0
  69. package/dist/tools/approval.test.js +112 -0
  70. package/dist/tools/approval.test.js.map +1 -0
  71. package/dist/tools/bash.d.ts +6 -0
  72. package/dist/tools/bash.d.ts.map +1 -0
  73. package/dist/tools/bash.js +58 -0
  74. package/dist/tools/bash.js.map +1 -0
  75. package/dist/tools/edit-file.d.ts +6 -0
  76. package/dist/tools/edit-file.d.ts.map +1 -0
  77. package/dist/tools/edit-file.js +58 -0
  78. package/dist/tools/edit-file.js.map +1 -0
  79. package/dist/tools/get-memory.d.ts +9 -0
  80. package/dist/tools/get-memory.d.ts.map +1 -0
  81. package/dist/tools/get-memory.js +56 -0
  82. package/dist/tools/get-memory.js.map +1 -0
  83. package/dist/tools/list-directory.d.ts +6 -0
  84. package/dist/tools/list-directory.d.ts.map +1 -0
  85. package/dist/tools/list-directory.js +68 -0
  86. package/dist/tools/list-directory.js.map +1 -0
  87. package/dist/tools/mcp-client.d.ts +74 -0
  88. package/dist/tools/mcp-client.d.ts.map +1 -0
  89. package/dist/tools/mcp-client.js +129 -0
  90. package/dist/tools/mcp-client.js.map +1 -0
  91. package/dist/tools/mcp-config.d.ts +31 -0
  92. package/dist/tools/mcp-config.d.ts.map +1 -0
  93. package/dist/tools/mcp-config.js +55 -0
  94. package/dist/tools/mcp-config.js.map +1 -0
  95. package/dist/tools/mcp-registry.d.ts +39 -0
  96. package/dist/tools/mcp-registry.d.ts.map +1 -0
  97. package/dist/tools/mcp-registry.js +88 -0
  98. package/dist/tools/mcp-registry.js.map +1 -0
  99. package/dist/tools/orchestrator.d.ts +52 -0
  100. package/dist/tools/orchestrator.d.ts.map +1 -0
  101. package/dist/tools/orchestrator.js +190 -0
  102. package/dist/tools/orchestrator.js.map +1 -0
  103. package/dist/tools/orchestrator.test.d.ts +8 -0
  104. package/dist/tools/orchestrator.test.d.ts.map +1 -0
  105. package/dist/tools/orchestrator.test.js +122 -0
  106. package/dist/tools/orchestrator.test.js.map +1 -0
  107. package/dist/tools/read-file.d.ts +6 -0
  108. package/dist/tools/read-file.d.ts.map +1 -0
  109. package/dist/tools/read-file.js +50 -0
  110. package/dist/tools/read-file.js.map +1 -0
  111. package/dist/tools/registry.d.ts +55 -0
  112. package/dist/tools/registry.d.ts.map +1 -0
  113. package/dist/tools/registry.js +75 -0
  114. package/dist/tools/registry.js.map +1 -0
  115. package/dist/tools/registry.test.d.ts +8 -0
  116. package/dist/tools/registry.test.d.ts.map +1 -0
  117. package/dist/tools/registry.test.js +100 -0
  118. package/dist/tools/registry.test.js.map +1 -0
  119. package/dist/tools/router.d.ts +62 -0
  120. package/dist/tools/router.d.ts.map +1 -0
  121. package/dist/tools/router.js +119 -0
  122. package/dist/tools/router.js.map +1 -0
  123. package/dist/tools/router.test.d.ts +7 -0
  124. package/dist/tools/router.test.d.ts.map +1 -0
  125. package/dist/tools/router.test.js +102 -0
  126. package/dist/tools/router.test.js.map +1 -0
  127. package/dist/tools/safety.d.ts +16 -0
  128. package/dist/tools/safety.d.ts.map +1 -0
  129. package/dist/tools/safety.js +81 -0
  130. package/dist/tools/safety.js.map +1 -0
  131. package/dist/tools/safety.test.d.ts +7 -0
  132. package/dist/tools/safety.test.d.ts.map +1 -0
  133. package/dist/tools/safety.test.js +104 -0
  134. package/dist/tools/safety.test.js.map +1 -0
  135. package/dist/tools/search-files.d.ts +9 -0
  136. package/dist/tools/search-files.d.ts.map +1 -0
  137. package/dist/tools/search-files.js +114 -0
  138. package/dist/tools/search-files.js.map +1 -0
  139. package/dist/tools/update-plan.d.ts +9 -0
  140. package/dist/tools/update-plan.d.ts.map +1 -0
  141. package/dist/tools/update-plan.js +99 -0
  142. package/dist/tools/update-plan.js.map +1 -0
  143. package/dist/tools/write-file.d.ts +6 -0
  144. package/dist/tools/write-file.d.ts.map +1 -0
  145. package/dist/tools/write-file.js +41 -0
  146. package/dist/tools/write-file.js.map +1 -0
  147. package/dist/tui/app.d.ts +31 -0
  148. package/dist/tui/app.d.ts.map +1 -0
  149. package/dist/tui/app.js +121 -0
  150. package/dist/tui/app.js.map +1 -0
  151. package/dist/tui/chatwidget/markdown_renderer.d.ts +20 -0
  152. package/dist/tui/chatwidget/markdown_renderer.d.ts.map +1 -0
  153. package/dist/tui/chatwidget/markdown_renderer.js +188 -0
  154. package/dist/tui/chatwidget/markdown_renderer.js.map +1 -0
  155. package/dist/tui/cjk_text.d.ts +25 -0
  156. package/dist/tui/cjk_text.d.ts.map +1 -0
  157. package/dist/tui/cjk_text.js +84 -0
  158. package/dist/tui/cjk_text.js.map +1 -0
  159. package/dist/tui/cjk_text.test.d.ts +2 -0
  160. package/dist/tui/cjk_text.test.d.ts.map +1 -0
  161. package/dist/tui/cjk_text.test.js +62 -0
  162. package/dist/tui/cjk_text.test.js.map +1 -0
  163. package/dist/tui/composer_input.d.ts +31 -0
  164. package/dist/tui/composer_input.d.ts.map +1 -0
  165. package/dist/tui/composer_input.js +184 -0
  166. package/dist/tui/composer_input.js.map +1 -0
  167. package/dist/tui/composer_input.test.d.ts +2 -0
  168. package/dist/tui/composer_input.test.d.ts.map +1 -0
  169. package/dist/tui/composer_input.test.js +87 -0
  170. package/dist/tui/composer_input.test.js.map +1 -0
  171. package/dist/tui/input.d.ts +21 -0
  172. package/dist/tui/input.d.ts.map +1 -0
  173. package/dist/tui/input.js +166 -0
  174. package/dist/tui/input.js.map +1 -0
  175. package/dist/tui/output.d.ts +17 -0
  176. package/dist/tui/output.d.ts.map +1 -0
  177. package/dist/tui/output.js +104 -0
  178. package/dist/tui/output.js.map +1 -0
  179. package/dist/tui/state/chat_timeline.d.ts +50 -0
  180. package/dist/tui/state/chat_timeline.d.ts.map +1 -0
  181. package/dist/tui/state/chat_timeline.js +129 -0
  182. package/dist/tui/state/chat_timeline.js.map +1 -0
  183. package/dist/tui/types.d.ts +45 -0
  184. package/dist/tui/types.d.ts.map +1 -0
  185. package/dist/tui/types.js +14 -0
  186. package/dist/tui/types.js.map +1 -0
  187. package/dist/types.d.ts +435 -0
  188. package/dist/types.d.ts.map +1 -0
  189. package/dist/types.js +8 -0
  190. package/dist/types.js.map +1 -0
  191. package/dist/utils/tokenizer.d.ts +21 -0
  192. package/dist/utils/tokenizer.d.ts.map +1 -0
  193. package/dist/utils/tokenizer.js +71 -0
  194. package/dist/utils/tokenizer.js.map +1 -0
  195. package/dist/utils/tokenizer.test.d.ts +7 -0
  196. package/dist/utils/tokenizer.test.d.ts.map +1 -0
  197. package/dist/utils/tokenizer.test.js +51 -0
  198. package/dist/utils/tokenizer.test.js.map +1 -0
  199. package/package.json +41 -0
  200. 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,7 @@
1
+ /**
2
+ * @file Unit tests for ToolRouter (Phase 9).
3
+ *
4
+ * Tests: Native priority, get, has, toOpenAITools, toMarkdown
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=router.test.d.ts.map
@@ -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,7 @@
1
+ /**
2
+ * @file Unit tests for safety module (Phase 3).
3
+ *
4
+ * Tests: validatePath, classifyCommand, isSensitiveFile
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=safety.test.d.ts.map
@@ -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