@ai-setting/roy-agent-cli 1.0.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 (158) hide show
  1. package/README.md +126 -0
  2. package/dist/bin/roy.js +127297 -0
  3. package/dist/roy-agent-darwin-arm64/bin/roy.js +127297 -0
  4. package/dist/roy-agent-darwin-x64/bin/roy.js +127297 -0
  5. package/dist/roy-agent-linux-arm64/bin/roy.js +127297 -0
  6. package/dist/roy-agent-linux-x64/bin/roy.js +127297 -0
  7. package/dist/roy-agent-windows-x64/bin/roy.js +127297 -0
  8. package/package.json +91 -0
  9. package/src/bin/roy.ts +12 -0
  10. package/src/cli.ts +101 -0
  11. package/src/commands/act.ts +480 -0
  12. package/src/commands/commands-add.ts +110 -0
  13. package/src/commands/commands-dirs.ts +70 -0
  14. package/src/commands/commands-info.ts +90 -0
  15. package/src/commands/commands-list.ts +161 -0
  16. package/src/commands/commands-remove.ts +147 -0
  17. package/src/commands/commands.ts +55 -0
  18. package/src/commands/config/config-service.test.ts +449 -0
  19. package/src/commands/config/config-service.ts +312 -0
  20. package/src/commands/config/deep-merge.test.ts +168 -0
  21. package/src/commands/config/deep-merge.ts +63 -0
  22. package/src/commands/config/export.ts +97 -0
  23. package/src/commands/config/filter-history-e2e.test.ts +141 -0
  24. package/src/commands/config/import-preserve-refs.test.ts +212 -0
  25. package/src/commands/config/import.ts +119 -0
  26. package/src/commands/config/index.ts +35 -0
  27. package/src/commands/config/list.ts +281 -0
  28. package/src/commands/config/roy-config-e2e.test.ts +297 -0
  29. package/src/commands/config/types.ts +54 -0
  30. package/src/commands/debug/index.ts +38 -0
  31. package/src/commands/debug/log.test.ts +233 -0
  32. package/src/commands/debug/log.ts +123 -0
  33. package/src/commands/debug/span.test.ts +297 -0
  34. package/src/commands/debug/span.ts +211 -0
  35. package/src/commands/debug/trace.test.ts +254 -0
  36. package/src/commands/debug/trace.ts +140 -0
  37. package/src/commands/eventsource/add.ts +133 -0
  38. package/src/commands/eventsource/index.ts +48 -0
  39. package/src/commands/eventsource/list.ts +194 -0
  40. package/src/commands/eventsource/remove.ts +95 -0
  41. package/src/commands/eventsource/start.ts +103 -0
  42. package/src/commands/eventsource/status.ts +185 -0
  43. package/src/commands/eventsource/stop.ts +89 -0
  44. package/src/commands/index.ts +22 -0
  45. package/src/commands/input-handler.test.ts +76 -0
  46. package/src/commands/input-handler.ts +43 -0
  47. package/src/commands/interactive-esc.test.ts +254 -0
  48. package/src/commands/interactive.shutdown.test.ts +122 -0
  49. package/src/commands/interactive.test.ts +221 -0
  50. package/src/commands/interactive.ts +1015 -0
  51. package/src/commands/lsp/check.ts +92 -0
  52. package/src/commands/lsp/index.ts +32 -0
  53. package/src/commands/lsp/install.ts +126 -0
  54. package/src/commands/lsp/list.ts +64 -0
  55. package/src/commands/mcp/index.ts +27 -0
  56. package/src/commands/mcp/list.ts +116 -0
  57. package/src/commands/mcp/reload.ts +70 -0
  58. package/src/commands/mcp/tools.ts +121 -0
  59. package/src/commands/memory/extract-e2e.test.ts +388 -0
  60. package/src/commands/memory/index.ts +11 -0
  61. package/src/commands/memory/memory-simplified.test.ts +58 -0
  62. package/src/commands/memory/memory.ts +25 -0
  63. package/src/commands/memory/organize.ts +300 -0
  64. package/src/commands/memory/recall.test.ts +120 -0
  65. package/src/commands/memory/recall.ts +88 -0
  66. package/src/commands/memory/record-extract-handle-query.test.ts +385 -0
  67. package/src/commands/memory/record-prompt-component.test.ts +343 -0
  68. package/src/commands/memory/record.test.ts +92 -0
  69. package/src/commands/memory/record.ts +332 -0
  70. package/src/commands/plugin.test.ts +292 -0
  71. package/src/commands/plugin.ts +267 -0
  72. package/src/commands/sessions/active.ts +96 -0
  73. package/src/commands/sessions/add-message.ts +96 -0
  74. package/src/commands/sessions/checkpoints.ts +154 -0
  75. package/src/commands/sessions/compact.test.ts +215 -0
  76. package/src/commands/sessions/compact.ts +269 -0
  77. package/src/commands/sessions/delete.ts +236 -0
  78. package/src/commands/sessions/get.ts +165 -0
  79. package/src/commands/sessions/grep.ts +233 -0
  80. package/src/commands/sessions/index.ts +95 -0
  81. package/src/commands/sessions/list.ts +210 -0
  82. package/src/commands/sessions/messages.test.ts +333 -0
  83. package/src/commands/sessions/messages.ts +248 -0
  84. package/src/commands/sessions/mock.ts +194 -0
  85. package/src/commands/sessions/new.ts +82 -0
  86. package/src/commands/sessions/rename.ts +98 -0
  87. package/src/commands/shared/event-handler.ts +213 -0
  88. package/src/commands/shared/event-message-formatter.ts +295 -0
  89. package/src/commands/shared/index.ts +11 -0
  90. package/src/commands/shared/query-executor.test.ts +434 -0
  91. package/src/commands/shared/query-executor.ts +324 -0
  92. package/src/commands/shared/repl-engine.test.ts +354 -0
  93. package/src/commands/shared/session-manager.test.ts +212 -0
  94. package/src/commands/shared/session-manager.ts +114 -0
  95. package/src/commands/skills/get.ts +90 -0
  96. package/src/commands/skills/index.ts +39 -0
  97. package/src/commands/skills/list.ts +129 -0
  98. package/src/commands/skills/reload.ts +59 -0
  99. package/src/commands/skills/search.ts +132 -0
  100. package/src/commands/skills/show-config.ts +93 -0
  101. package/src/commands/tasks/complete.ts +92 -0
  102. package/src/commands/tasks/create.ts +118 -0
  103. package/src/commands/tasks/delete.ts +86 -0
  104. package/src/commands/tasks/get.ts +116 -0
  105. package/src/commands/tasks/index.ts +53 -0
  106. package/src/commands/tasks/list.ts +140 -0
  107. package/src/commands/tasks/operations.ts +120 -0
  108. package/src/commands/tasks/update.ts +122 -0
  109. package/src/commands/tools/exec-tool.ts +128 -0
  110. package/src/commands/tools/get.ts +114 -0
  111. package/src/commands/tools/index.ts +35 -0
  112. package/src/commands/tools/list.ts +107 -0
  113. package/src/commands/tools/shared/index.ts +7 -0
  114. package/src/commands/tools/shared/schema-helper.ts +111 -0
  115. package/src/commands/workflow/commands/add.ts +315 -0
  116. package/src/commands/workflow/commands/get.ts +193 -0
  117. package/src/commands/workflow/commands/list.ts +137 -0
  118. package/src/commands/workflow/commands/nodes.ts +528 -0
  119. package/src/commands/workflow/commands/remove.ts +94 -0
  120. package/src/commands/workflow/commands/run.ts +398 -0
  121. package/src/commands/workflow/commands/status.ts +147 -0
  122. package/src/commands/workflow/commands/stop.ts +91 -0
  123. package/src/commands/workflow/commands/update.ts +130 -0
  124. package/src/commands/workflow/commands/validate.ts +139 -0
  125. package/src/commands/workflow/commands/workflow-cli.test.ts +196 -0
  126. package/src/commands/workflow/index.ts +65 -0
  127. package/src/commands/workflow/renderers.ts +358 -0
  128. package/src/commands/workflow/validators/index.ts +8 -0
  129. package/src/commands/workflow/validators/node-validator-factory.ts +40 -0
  130. package/src/commands/workflow/validators/node-validator.ts +125 -0
  131. package/src/commands/workflow/validators/nodes/agent-node-validator.ts +58 -0
  132. package/src/commands/workflow/validators/nodes/condition-node-validator.ts +34 -0
  133. package/src/commands/workflow/validators/nodes/decorator-node-validator.ts +45 -0
  134. package/src/commands/workflow/validators/nodes/merge-node-validator.ts +46 -0
  135. package/src/commands/workflow/validators/nodes/skill-node-validator.ts +33 -0
  136. package/src/commands/workflow/validators/nodes/tool-node-validator.ts +54 -0
  137. package/src/commands/workflow/validators/nodes/workflow-node-validator.ts +33 -0
  138. package/src/commands/workflow/validators/types.ts +78 -0
  139. package/src/commands/workflow/validators/workflow-validator.test.ts +273 -0
  140. package/src/commands/workflow/validators/workflow-validator.ts +320 -0
  141. package/src/index.ts +19 -0
  142. package/src/plugin/apply.ts +103 -0
  143. package/src/plugin/discover.ts +219 -0
  144. package/src/plugin/index.ts +45 -0
  145. package/src/plugin/registry.ts +272 -0
  146. package/src/plugin/types.ts +165 -0
  147. package/src/services/context-handler.service.test.ts +501 -0
  148. package/src/services/context-handler.service.ts +372 -0
  149. package/src/services/environment.service.commands-prompt.test.ts +167 -0
  150. package/src/services/environment.service.ts +656 -0
  151. package/src/services/output.service.test.ts +92 -0
  152. package/src/services/output.service.ts +122 -0
  153. package/src/services/quiet-mode.service.test.ts +114 -0
  154. package/src/services/quiet-mode.service.ts +81 -0
  155. package/src/services/stream-output.service.test.ts +214 -0
  156. package/src/services/stream-output.service.ts +323 -0
  157. package/src/util/which.test.ts +101 -0
  158. package/src/util/which.ts +55 -0
@@ -0,0 +1,332 @@
1
+ /**
2
+ * @fileoverview Memory Record Command
3
+ *
4
+ * 命令:roy memory record
5
+ *
6
+ * 将记忆内容记录到 memory file
7
+ * 支持 --extract 模式:从会话历史中提取记忆并保存
8
+ */
9
+
10
+ import { CommandModule } from "yargs";
11
+ import { EnvironmentService } from "../../services/environment.service";
12
+ import { OutputService } from "../../services/output.service";
13
+ import chalk from "chalk";
14
+ import type {
15
+ MemoryComponent,
16
+ AgentComponent,
17
+ } from "@ai-setting/roy-agent-core";
18
+ import { createMemoryAgentTools, getBuiltInPrompt } from "@ai-setting/roy-agent-core";
19
+ import type { PromptComponent } from "@ai-setting/roy-agent-core";
20
+ import { bashTool, globTool, readFileTool } from "@ai-setting/roy-agent-core";
21
+
22
+ interface RecordOptions {
23
+ config?: string;
24
+ /** 作用域: project 或 global */
25
+ scope: "project" | "global";
26
+ /** 操作模式: append/prepend/overwrite/delete */
27
+ mode: "append" | "prepend" | "overwrite" | "delete";
28
+ /** 记忆内容 */
29
+ content?: string;
30
+ /** 章节标题 */
31
+ title?: string;
32
+ /** 触发提取模式 */
33
+ extract?: boolean;
34
+ /** 指定 session ID */
35
+ "session-id"?: string;
36
+ /** 自然语言提取指导 */
37
+ require?: string;
38
+ }
39
+
40
+ /**
41
+ * Memory Agent 提取模式 - 从会话历史生成记忆
42
+ */
43
+ async function runExtractMode(
44
+ output: OutputService,
45
+ memoryComponent: MemoryComponent,
46
+ agentComponent: AgentComponent,
47
+ sessionComponent: any,
48
+ env: any,
49
+ options: {
50
+ scope: "project" | "global";
51
+ sessionId?: string;
52
+ require?: string;
53
+ }
54
+ ): Promise<void> {
55
+ const { scope, sessionId, require: userRequirement } = options;
56
+
57
+ output.log(chalk.blue(`\n🔍 Memory Extract Mode (${scope})`));
58
+ output.log(chalk.gray("正在分析会话历史,生成记忆...\n"));
59
+
60
+ try {
61
+ // 1. 获取当前 memory
62
+ const currentMemory = await memoryComponent.recallMemory(scope) || "(无现有记忆)";
63
+
64
+ // 2. 获取 prompt(通过 PromptComponent 或 fallback 到 getBuiltInPrompt)
65
+ const promptName = scope === "project" ? "project-memory" : "global-memory";
66
+ let promptTemplate: string | undefined;
67
+
68
+ try {
69
+ const promptComponent = env?.getComponent?.("prompt") as PromptComponent | undefined;
70
+ if (promptComponent) {
71
+ promptTemplate = await promptComponent.getPrompt(promptName);
72
+ }
73
+ } catch (err) {
74
+ // PromptComponent 不可用或获取失败,fallback 到 getBuiltInPrompt
75
+ }
76
+
77
+ // Fallback:如果 PromptComponent 不可用或未找到,使用 getBuiltInPrompt
78
+ if (!promptTemplate) {
79
+ promptTemplate = getBuiltInPrompt(promptName);
80
+ }
81
+
82
+ if (!promptTemplate) {
83
+ output.error(`Prompt not found for scope: ${scope}`);
84
+ process.exit(1);
85
+ }
86
+
87
+ // 3. 替换变量
88
+ const systemPrompt = promptTemplate
89
+ .replace("{USER_REQUIREMENT}", userRequirement || "请从会话历史中提炼关键知识。")
90
+ .replace("{SCOPE}", scope)
91
+ .replace("{CURRENT_MEMORY}", currentMemory);
92
+
93
+ // 4. 创建 Memory Agent tools
94
+ const memoryTools = createMemoryAgentTools({
95
+ sessionComponent,
96
+ memoryComponent: memoryComponent as any,
97
+ });
98
+
99
+ // 5. 准备 Agent 工具列表
100
+ const agentTools = [
101
+ memoryTools.searchSessions,
102
+ memoryTools.getSession,
103
+ memoryTools.writeMemory,
104
+ ];
105
+
106
+ // 6. Project scope 额外添加文件工具(读取项目文档和代码)
107
+ if (scope === "project") {
108
+ agentTools.push(bashTool, globTool, readFileTool);
109
+ }
110
+
111
+ // 7. 获取允许的工具名称列表
112
+ const allowedToolNames = agentTools.map((t) => t.name);
113
+
114
+ // 8. 注册 Agent
115
+ const agentName = `memory-extract-${scope}`;
116
+ agentComponent.registerAgent(agentName, {
117
+ type: "sub",
118
+ systemPrompt,
119
+ allowedTools: allowedToolNames,
120
+ deniedTools: [],
121
+ });
122
+ agentComponent.setDefaultTools(agentTools);
123
+
124
+ // 9. 将 Agent tools 注册到 ToolComponent(用于 executeTool 时查找)
125
+ const toolComponent = (env as any).getComponent?.("tool");
126
+ if (toolComponent) {
127
+ for (const tool of agentTools) {
128
+ try {
129
+ toolComponent.register(tool);
130
+ output.log(chalk.gray(`Tool registered: ${tool.name}`));
131
+ } catch (err) {
132
+ output.log(chalk.gray(`Tool already registered: ${tool.name}`));
133
+ }
134
+ }
135
+ }
136
+
137
+ output.log(chalk.gray(`Agent "${agentName}" registered with ${agentTools.length} tools`));
138
+
139
+ // 6. 执行 Agent(使用 handle_query,参考 delegate_task 模式)
140
+ // 这确保与 delegate_task 使用相同的执行路径,包括 memory 注入等
141
+ const query = `请分析会话历史,提炼${scope === "project" ? "项目" : "全局"}记忆并写入记忆文件。`;
142
+
143
+ let result: string;
144
+ try {
145
+ // 使用 env.handle_query() 而不是 agentComponent.run()
146
+ // 这与 delegate_task 的实现模式一致
147
+ result = await env.handle_query(query, {
148
+ sessionId,
149
+ agentType: agentName,
150
+ });
151
+ } catch (error) {
152
+ result = `执行失败: ${error instanceof Error ? error.message : String(error)}`;
153
+ }
154
+
155
+ // 10. 输出结果
156
+ // handle_query 返回 string,如果有错误会抛出异常
157
+ if (result && result.startsWith("执行失败")) {
158
+ output.error(`提取失败: ${result}`);
159
+ } else if (result) {
160
+ output.log(chalk.green("\n✅ 记忆提取完成"));
161
+ output.log(chalk.gray("记忆已保存到 memory 文件。\n"));
162
+ // 可选:输出 agent 返回的结果摘要
163
+ if (result.length > 0 && result !== "✅ 记忆提取完成") {
164
+ output.log(chalk.gray(`执行摘要: ${result.substring(0, 200)}${result.length > 200 ? "..." : ""}`));
165
+ }
166
+ }
167
+
168
+ // 11. 清理 - 注销 Agent 和 Tools
169
+ agentComponent.unregisterAgent(agentName);
170
+ if (toolComponent) {
171
+ // 清理 memory tools
172
+ for (const tool of [memoryTools.searchSessions, memoryTools.getSession, memoryTools.writeMemory]) {
173
+ toolComponent.unregister(tool.name);
174
+ }
175
+ // 如果是 project scope,额外清理文件工具
176
+ if (scope === "project") {
177
+ for (const toolName of ["bash", "glob", "read_file"]) {
178
+ toolComponent.unregister(toolName);
179
+ }
180
+ }
181
+ }
182
+ } catch (error) {
183
+ output.error(`提取失败: ${error}`);
184
+ throw error;
185
+ }
186
+ }
187
+
188
+ export const RecordCommand: CommandModule<object, object> = {
189
+ command: "record",
190
+ aliases: ["save"],
191
+ describe: "记录记忆内容到 memory file",
192
+
193
+ builder: (yargs) =>
194
+ yargs
195
+ .option("mode", {
196
+ alias: "m",
197
+ type: "string",
198
+ choices: ["append", "prepend", "overwrite", "delete"],
199
+ default: "append",
200
+ description: "操作模式: append(尾部追加), prepend(头部插入), overwrite(覆盖), delete(删除)",
201
+ })
202
+ .option("content", {
203
+ alias: "c",
204
+ type: "string",
205
+ description: "记忆内容 (markdown 格式)",
206
+ })
207
+ .option("title", {
208
+ alias: "t",
209
+ type: "string",
210
+ description: "记忆标题 (用于生成章节标题)",
211
+ })
212
+ .option("filename", {
213
+ alias: "f",
214
+ type: "string",
215
+ description: "文件名 (默认使用 memory.memoryFile 配置)",
216
+ })
217
+ .option("config", {
218
+ type: "string",
219
+ describe: "Config file path",
220
+ })
221
+ // Extract 模式选项
222
+ .option("extract", {
223
+ alias: "e",
224
+ type: "boolean",
225
+ default: false,
226
+ description: "触发提取模式:从会话历史中提取记忆",
227
+ })
228
+ .option("scope", {
229
+ type: "string",
230
+ choices: ["project", "global"],
231
+ describe: "记忆作用域",
232
+ demandOption: true, // 必填
233
+ })
234
+ .option("session-id", {
235
+ type: "string",
236
+ describe: "指定 session ID(可选)",
237
+ })
238
+ .option("require", {
239
+ alias: "r",
240
+ type: "string",
241
+ describe: "自然语言提取指导",
242
+ })
243
+ .demandOption(
244
+ ["mode"],
245
+ "请指定操作模式 (--mode)"
246
+ )
247
+ .check((argv) => {
248
+ const mode = argv.mode as string;
249
+ // extract 模式不需要 content
250
+ if (argv.extract) {
251
+ return true;
252
+ }
253
+ if (mode !== "delete" && !argv.content) {
254
+ throw new Error("--content is required for all modes except 'delete' and 'extract'");
255
+ }
256
+ return true;
257
+ }),
258
+
259
+ async handler(args) {
260
+ const a = args as any;
261
+ const output = new OutputService();
262
+ const envService = new EnvironmentService(output);
263
+
264
+ try {
265
+ await envService.create({ configPath: a.config });
266
+ const env = envService.getEnvironment();
267
+ if (!env) {
268
+ output.error("Failed to create environment");
269
+ process.exit(1);
270
+ }
271
+ const memoryComponent = env.getComponent("memory") as MemoryComponent | undefined;
272
+
273
+ if (!memoryComponent) {
274
+ output.error("MemoryComponent not available");
275
+ process.exit(1);
276
+ }
277
+
278
+ // Extract 模式:从会话历史提取记忆
279
+ if (a.extract) {
280
+ const agentComponent = env.getComponent("agent") as AgentComponent | undefined;
281
+ const sessionComponent = env.getComponent("session");
282
+
283
+ if (!agentComponent) {
284
+ output.error("AgentComponent not available");
285
+ process.exit(1);
286
+ }
287
+
288
+ await runExtractMode(output, memoryComponent, agentComponent, sessionComponent, env, {
289
+ scope: a.scope || "project",
290
+ sessionId: args["session-id"] as string | undefined,
291
+ require: a.require,
292
+ });
293
+ return;
294
+ }
295
+
296
+ // 普通 record 模式
297
+ const result = await memoryComponent.recordMemory({
298
+ scope: a.scope,
299
+ mode: a.mode,
300
+ content: a.content,
301
+ title: a.title,
302
+ });
303
+
304
+ if (!result) {
305
+ output.error("Failed to record memory: no memory paths configured");
306
+ process.exit(1);
307
+ }
308
+
309
+ const actionMessages: Record<string, string> = {
310
+ created: "已创建记忆文件",
311
+ overwrite: "已覆盖记忆文件",
312
+ append: "已追加内容到记忆文件",
313
+ prepend: "已插入内容到记忆文件开头",
314
+ delete: "已删除记忆文件",
315
+ };
316
+
317
+ output.log(chalk.green(`✓ ${actionMessages[result.action]}`));
318
+ output.log(chalk.gray(`路径: ${result.path}`));
319
+
320
+ // 如果是 overwrite/append/prepend,显示写入的内容预览
321
+ if (result.action !== "delete" && a.content) {
322
+ const preview = a.content.substring(0, 100);
323
+ output.log(chalk.gray(`内容预览: ${preview}${a.content.length > 100 ? "..." : ""}`));
324
+ }
325
+ } catch (error) {
326
+ output.error(`Failed to record memory: ${error}`);
327
+ process.exit(1);
328
+ } finally {
329
+ await envService.dispose();
330
+ }
331
+ },
332
+ };
@@ -0,0 +1,292 @@
1
+ /**
2
+ * @fileoverview Plugin Module Tests
3
+ */
4
+ import { describe, it, expect, vi, beforeEach } from "vitest";
5
+ import {
6
+ PluginRegistry,
7
+ parseArgs,
8
+ ToolHookPoints,
9
+ BUILTIN_PLUGINS,
10
+ executePlugin,
11
+ type ToolPlugin,
12
+ type ToolHookContext,
13
+ type PluginConfig,
14
+ } from "./plugin.js";
15
+
16
+ describe("Plugin Module", () => {
17
+ // ========================================================================
18
+ // ToolHookPoints
19
+ // ========================================================================
20
+ describe("ToolHookPoints", () => {
21
+ it("should have AFTER_EXECUTE hook point", () => {
22
+ expect(ToolHookPoints.AFTER_EXECUTE).toBe("tool.after-execute");
23
+ });
24
+
25
+ it("should have BEFORE_EXECUTE hook point", () => {
26
+ expect(ToolHookPoints.BEFORE_EXECUTE).toBe("tool.before-execute");
27
+ });
28
+ });
29
+
30
+ // ========================================================================
31
+ // BUILTIN_PLUGINS
32
+ // ========================================================================
33
+ describe("BUILTIN_PLUGINS", () => {
34
+ it("should contain lsp plugin", () => {
35
+ expect(BUILTIN_PLUGINS).toHaveProperty("lsp");
36
+ expect(BUILTIN_PLUGINS.lsp).toContain("LSP");
37
+ });
38
+
39
+ it("should contain code-check plugin", () => {
40
+ expect(BUILTIN_PLUGINS).toHaveProperty("code-check");
41
+ expect(BUILTIN_PLUGINS["code-check"]).toContain("code checking");
42
+ });
43
+
44
+ it("should contain reminder plugin", () => {
45
+ expect(BUILTIN_PLUGINS).toHaveProperty("reminder");
46
+ expect(BUILTIN_PLUGINS.reminder).toContain("reminder");
47
+ });
48
+ });
49
+
50
+ // ========================================================================
51
+ // parseArgs
52
+ // ========================================================================
53
+ describe("parseArgs", () => {
54
+ it("should parse single plugin name", () => {
55
+ const result = parseArgs(["lsp"]);
56
+ expect(result.plugins).toContain("lsp");
57
+ });
58
+
59
+ it("should parse multiple plugin names", () => {
60
+ const result = parseArgs(["lsp", "code-check", "reminder"]);
61
+ expect(result.plugins).toEqual(["lsp", "code-check", "reminder"]);
62
+ });
63
+
64
+ it("should parse plugin with config", () => {
65
+ const result = parseArgs(["lsp:typescript"]);
66
+ expect(result.plugins).toEqual([
67
+ { name: "lsp", config: "typescript" },
68
+ ]);
69
+ });
70
+
71
+ it("should parse plugin with path", () => {
72
+ const result = parseArgs(["/path/to/my-plugin.js"]);
73
+ expect(result.plugins).toContain("/path/to/my-plugin.js");
74
+ });
75
+
76
+ it("should return empty plugins for empty input", () => {
77
+ const result = parseArgs([]);
78
+ expect(result.plugins).toEqual([]);
79
+ });
80
+
81
+ it("should handle mixed plugins and paths", () => {
82
+ const result = parseArgs([
83
+ "lsp",
84
+ "/custom/plugin.js",
85
+ "code-check",
86
+ ]);
87
+ expect(result.plugins).toHaveLength(3);
88
+ expect(result.plugins).toContain("lsp");
89
+ expect(result.plugins).toContain("/custom/plugin.js");
90
+ expect(result.plugins[2]).toBe("code-check");
91
+ });
92
+
93
+ it("should detect file type from path", () => {
94
+ const result = parseArgs(["/path/to/test.ts"]);
95
+ expect(result.fileTypes).toContain(".ts");
96
+ });
97
+
98
+ it("should extract file types from plugin configs", () => {
99
+ const result = parseArgs(["lsp:typescript", "lsp:python"]);
100
+ expect(result.fileTypes).toContain(".ts");
101
+ expect(result.fileTypes).toContain(".tsx");
102
+ expect(result.fileTypes).toContain(".py");
103
+ });
104
+
105
+ it("should return file types as unique set", () => {
106
+ const result = parseArgs(["lsp:typescript", "lsp:typescript"]);
107
+ expect(result.fileTypes).toEqual([".ts", ".tsx"]);
108
+ });
109
+
110
+ it("should handle empty config after colon", () => {
111
+ const result = parseArgs(["lsp:"]);
112
+ expect(result.plugins[0]).toEqual({ name: "lsp", config: "" });
113
+ });
114
+
115
+ it("should handle nested config paths", () => {
116
+ const result = parseArgs(["linter:/path/to/config.json"]);
117
+ expect(result.plugins[0]).toEqual({
118
+ name: "linter",
119
+ config: "/path/to/config.json",
120
+ });
121
+ });
122
+ });
123
+ });
124
+
125
+ describe("PluginRegistry", () => {
126
+ let registry: PluginRegistry;
127
+
128
+ beforeEach(() => {
129
+ registry = new PluginRegistry();
130
+ });
131
+
132
+ describe("register", () => {
133
+ it("should register a plugin", () => {
134
+ const plugin: ToolPlugin = {
135
+ name: "test-plugin",
136
+ getHookPoint: () => "tool.after-execute",
137
+ };
138
+ registry.register(plugin);
139
+ expect(registry.has("test-plugin")).toBe(true);
140
+ });
141
+
142
+ it("should not register duplicate plugins", () => {
143
+ const plugin: ToolPlugin = {
144
+ name: "test-plugin",
145
+ getHookPoint: () => "tool.after-execute",
146
+ };
147
+ registry.register(plugin);
148
+ expect(() => registry.register(plugin)).toThrow(
149
+ "already registered"
150
+ );
151
+ });
152
+ });
153
+
154
+ describe("unregister", () => {
155
+ it("should unregister a plugin", () => {
156
+ const plugin: ToolPlugin = {
157
+ name: "test-plugin",
158
+ getHookPoint: () => "tool.after-execute",
159
+ };
160
+ registry.register(plugin);
161
+ expect(registry.unregister("test-plugin")).toBe(true);
162
+ expect(registry.has("test-plugin")).toBe(false);
163
+ });
164
+
165
+ it("should return false for non-existent plugin", () => {
166
+ expect(registry.unregister("non-existent")).toBe(false);
167
+ });
168
+ });
169
+
170
+ describe("get", () => {
171
+ it("should get a registered plugin", () => {
172
+ const plugin: ToolPlugin = {
173
+ name: "test-plugin",
174
+ getHookPoint: () => "tool.after-execute",
175
+ };
176
+ registry.register(plugin);
177
+ expect(registry.get("test-plugin")).toBe(plugin);
178
+ });
179
+
180
+ it("should return undefined for non-existent plugin", () => {
181
+ expect(registry.get("non-existent")).toBeUndefined();
182
+ });
183
+ });
184
+
185
+ describe("getAll", () => {
186
+ it("should return all registered plugins", () => {
187
+ const plugin1: ToolPlugin = {
188
+ name: "plugin-1",
189
+ getHookPoint: () => "tool.after-execute",
190
+ };
191
+ const plugin2: ToolPlugin = {
192
+ name: "plugin-2",
193
+ getHookPoint: () => "tool.before-execute",
194
+ };
195
+ registry.register(plugin1);
196
+ registry.register(plugin2);
197
+ expect(registry.getAll()).toHaveLength(2);
198
+ });
199
+ });
200
+
201
+ describe("getByHookPoint", () => {
202
+ it("should return plugins for specific hook point", () => {
203
+ const plugin1: ToolPlugin = {
204
+ name: "after-hook-plugin",
205
+ getHookPoint: () => "tool.after-execute",
206
+ };
207
+ const plugin2: ToolPlugin = {
208
+ name: "before-hook-plugin",
209
+ getHookPoint: () => "tool.before-execute",
210
+ };
211
+ registry.register(plugin1);
212
+ registry.register(plugin2);
213
+
214
+ const afterPlugins = registry.getByHookPoint("tool.after-execute");
215
+ expect(afterPlugins).toHaveLength(1);
216
+ expect(afterPlugins[0].name).toBe("after-hook-plugin");
217
+ });
218
+ });
219
+
220
+ describe("clear", () => {
221
+ it("should clear all plugins", () => {
222
+ const plugin1: ToolPlugin = {
223
+ name: "plugin-1",
224
+ getHookPoint: () => "tool.after-execute",
225
+ };
226
+ const plugin2: ToolPlugin = {
227
+ name: "plugin-2",
228
+ getHookPoint: () => "tool.after-execute",
229
+ };
230
+ registry.register(plugin1);
231
+ registry.register(plugin2);
232
+ registry.clear();
233
+ expect(registry.getAll()).toHaveLength(0);
234
+ });
235
+ });
236
+ });
237
+
238
+ describe("executePlugin", () => {
239
+ it("should execute plugin onAfterExecute hook", async () => {
240
+ const onAfterExecute = vi.fn();
241
+ const plugin: ToolPlugin = {
242
+ name: "test-plugin",
243
+ getHookPoint: () => "tool.after-execute",
244
+ onAfterExecute,
245
+ };
246
+
247
+ const context: ToolHookContext = {
248
+ tool: { name: "write_file" },
249
+ args: { path: "/test.ts" },
250
+ result: { success: true },
251
+ };
252
+
253
+ await executePlugin(plugin, context);
254
+ expect(onAfterExecute).toHaveBeenCalledWith(context);
255
+ });
256
+
257
+ it("should skip execution when shouldHandle returns false", async () => {
258
+ const onAfterExecute = vi.fn();
259
+ const plugin: ToolPlugin = {
260
+ name: "ts-only",
261
+ getHookPoint: () => "tool.after-execute",
262
+ shouldHandle: (ctx) => {
263
+ const path = ctx.args?.path as string | undefined;
264
+ return path?.endsWith(".ts") || false;
265
+ },
266
+ onAfterExecute,
267
+ };
268
+
269
+ const context: ToolHookContext = {
270
+ args: { path: "/test.py" },
271
+ };
272
+
273
+ await executePlugin(plugin, context);
274
+ expect(onAfterExecute).not.toHaveBeenCalled();
275
+ });
276
+
277
+ it("should execute onBeforeExecute when hook point matches", async () => {
278
+ const onBeforeExecute = vi.fn();
279
+ const plugin: ToolPlugin = {
280
+ name: "before-hook-plugin",
281
+ getHookPoint: () => "tool.before-execute",
282
+ onBeforeExecute,
283
+ };
284
+
285
+ const context: ToolHookContext = {
286
+ tool: { name: "read_file" },
287
+ };
288
+
289
+ await executePlugin(plugin, context);
290
+ expect(onBeforeExecute).toHaveBeenCalledWith(context);
291
+ });
292
+ });