@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,385 @@
1
+ /**
2
+ * @fileoverview TDD Test for record --extract using env.handle_query
3
+ *
4
+ * 验证 record.ts 应该使用 env.handle_query() 执行 agent,
5
+ * 而不是直接调用 agentComponent.run()
6
+ *
7
+ * 参考 delegate_task 的实现模式
8
+ */
9
+
10
+ import { describe, expect, test, vi, beforeEach, afterEach } from "vitest";
11
+
12
+ // Mock chalk
13
+ vi.mock("chalk", () => ({
14
+ default: {
15
+ blue: (str: string) => str,
16
+ gray: (str: string) => str,
17
+ green: (str: string) => str,
18
+ red: (str: string) => str,
19
+ yellow: (str: string) => str,
20
+ cyan: (str: string) => str,
21
+ bold: (str: string) => str,
22
+ },
23
+ }));
24
+
25
+ describe("RecordCommand - Extract Mode with handle_query", () => {
26
+ let mockEnv: any;
27
+ let mockAgentComponent: any;
28
+ let mockMemoryComponent: any;
29
+ let mockSessionComponent: any;
30
+ let mockToolComponent: any;
31
+
32
+ beforeEach(() => {
33
+ // Mock components
34
+ mockMemoryComponent = {
35
+ recallMemory: vi.fn().mockResolvedValue("# 项目记忆\n\n暂无内容"),
36
+ recordMemory: vi.fn().mockResolvedValue({ path: "/test/memory.md", action: "overwrite" }),
37
+ getMemoryPaths: vi.fn().mockReturnValue([
38
+ { type: "project", path: ".roy/memory" },
39
+ { type: "user", path: "~/.config/roy-agent/memory" },
40
+ ]),
41
+ getMemoryFile: vi.fn().mockReturnValue("memory.md"),
42
+ };
43
+
44
+ mockSessionComponent = {
45
+ searchSessions: vi.fn().mockResolvedValue([
46
+ { sessionId: "test-1", title: "Test Session", preview: "Hello world" }
47
+ ]),
48
+ getSession: vi.fn().mockResolvedValue({
49
+ sessionId: "test-1",
50
+ messages: [
51
+ { role: "user", content: "Hello" },
52
+ { role: "assistant", content: "Hi there!" },
53
+ ],
54
+ }),
55
+ };
56
+
57
+ mockAgentComponent = {
58
+ registerAgent: vi.fn().mockReturnValue({
59
+ name: "memory-extract-project",
60
+ config: { type: "sub" },
61
+ status: "idle",
62
+ plugins: new Map(),
63
+ }),
64
+ unregisterAgent: vi.fn(),
65
+ setDefaultTools: vi.fn(),
66
+ run: vi.fn(), // Not used anymore with handle_query
67
+ getAgent: vi.fn().mockReturnValue({
68
+ name: "memory-extract-project",
69
+ config: { type: "sub" },
70
+ status: "idle",
71
+ plugins: new Map(),
72
+ }),
73
+ };
74
+
75
+ mockToolComponent = {
76
+ register: vi.fn(),
77
+ unregister: vi.fn(),
78
+ };
79
+
80
+ // Mock env with handle_query method
81
+ mockEnv = {
82
+ getComponent: vi.fn((name: string) => {
83
+ switch (name) {
84
+ case "memory":
85
+ return mockMemoryComponent;
86
+ case "agent":
87
+ return mockAgentComponent;
88
+ case "session":
89
+ return mockSessionComponent;
90
+ case "tool":
91
+ return mockToolComponent;
92
+ default:
93
+ return undefined;
94
+ }
95
+ }),
96
+ // handle_query returns string (finalText), not object
97
+ handle_query: vi.fn().mockResolvedValue("Memory extracted successfully via handle_query"),
98
+ };
99
+ });
100
+
101
+ afterEach(() => {
102
+ vi.clearAllMocks();
103
+ });
104
+
105
+ describe("TDD: record --extract should use env.handle_query", () => {
106
+
107
+ test("should call env.handle_query with agentType instead of agentComponent.run", async () => {
108
+ // This test verifies the NEW behavior: using handle_query
109
+ // Given: mock env and components
110
+ // When: runExtractMode is called
111
+ // Then: env.handle_query should be called with agentType
112
+
113
+ const { createMemoryAgentTools, getBuiltInPrompt } = await import("@ai-setting/roy-agent-core");
114
+
115
+ const memoryTools = createMemoryAgentTools({
116
+ sessionComponent: mockSessionComponent,
117
+ memoryComponent: mockMemoryComponent,
118
+ });
119
+
120
+ // Get prompt template
121
+ const promptTemplate = getBuiltInPrompt("project-memory");
122
+ expect(promptTemplate).toBeDefined();
123
+
124
+ // Simulate what runExtractMode does
125
+ const currentMemory = await mockMemoryComponent.recallMemory("project");
126
+ const agentName = "memory-extract-project";
127
+
128
+ const systemPrompt = promptTemplate!
129
+ .replace("{USER_REQUIREMENT}", "请从会话历史中提炼关键知识。")
130
+ .replace("{SCOPE}", "project")
131
+ .replace("{CURRENT_MEMORY}", currentMemory || "(无现有记忆)");
132
+
133
+ // Register agent
134
+ mockAgentComponent.registerAgent(agentName, {
135
+ type: "sub",
136
+ systemPrompt,
137
+ allowedTools: ["search_sessions", "get_session", "write_memory"],
138
+ deniedTools: [],
139
+ });
140
+
141
+ // NEW BEHAVIOR: Should call env.handle_query instead of agentComponent.run
142
+ const query = "请分析会话历史,提炼项目记忆并写入记忆文件。";
143
+
144
+ // This is what the new implementation does
145
+ let result: string;
146
+ try {
147
+ result = await mockEnv.handle_query(query, {
148
+ sessionId: "test-session",
149
+ agentType: agentName,
150
+ });
151
+ } catch (error) {
152
+ result = `执行失败: ${error instanceof Error ? error.message : String(error)}`;
153
+ }
154
+
155
+ // Verify handle_query was called
156
+ expect(mockEnv.handle_query).toHaveBeenCalledWith(
157
+ query,
158
+ expect.objectContaining({
159
+ agentType: agentName,
160
+ sessionId: "test-session",
161
+ })
162
+ );
163
+
164
+ // Verify agentComponent.run was NOT called
165
+ expect(mockAgentComponent.run).not.toHaveBeenCalled();
166
+
167
+ // Verify result is a string (not an object)
168
+ expect(typeof result).toBe("string");
169
+ expect(result).toBe("Memory extracted successfully via handle_query");
170
+ });
171
+
172
+ test("should register agent with allowedTools for tool filtering", async () => {
173
+ // Verify agent registration includes allowedTools
174
+ const { getBuiltInPrompt } = await import("@ai-setting/roy-agent-core");
175
+
176
+ const promptTemplate = getBuiltInPrompt("project-memory");
177
+ const agentName = "memory-extract-project";
178
+ const systemPrompt = promptTemplate!
179
+ .replace("{USER_REQUIREMENT}", "test")
180
+ .replace("{SCOPE}", "project")
181
+ .replace("{CURRENT_MEMORY}", "");
182
+
183
+ // Register agent with allowedTools (key for tool filtering)
184
+ mockAgentComponent.registerAgent(agentName, {
185
+ type: "sub",
186
+ systemPrompt,
187
+ allowedTools: ["search_sessions", "get_session", "write_memory"],
188
+ deniedTools: [],
189
+ });
190
+
191
+ // Verify registration was called with correct config
192
+ expect(mockAgentComponent.registerAgent).toHaveBeenCalledWith(
193
+ agentName,
194
+ expect.objectContaining({
195
+ type: "sub",
196
+ allowedTools: expect.arrayContaining(["search_sessions", "get_session", "write_memory"]),
197
+ })
198
+ );
199
+ });
200
+
201
+ test("should handle both project and global scope via handle_query", async () => {
202
+ const { getBuiltInPrompt } = await import("@ai-setting/roy-agent-core");
203
+
204
+ // Test project scope
205
+ const projectPrompt = getBuiltInPrompt("project-memory");
206
+ expect(projectPrompt).toContain("{USER_REQUIREMENT}");
207
+
208
+ // Test global scope
209
+ const globalPrompt = getBuiltInPrompt("global-memory");
210
+ expect(globalPrompt).toContain("{USER_REQUIREMENT}");
211
+
212
+ // Both scopes should work with handle_query
213
+ for (const [scope, prompt] of [["project", projectPrompt], ["global", globalPrompt]]) {
214
+ const agentName = `memory-extract-${scope}`;
215
+
216
+ await mockEnv.handle_query("test query", {
217
+ sessionId: "test-session",
218
+ agentType: agentName,
219
+ });
220
+
221
+ expect(mockEnv.handle_query).toHaveBeenCalledWith(
222
+ "test query",
223
+ expect.objectContaining({
224
+ agentType: agentName,
225
+ })
226
+ );
227
+ }
228
+ });
229
+
230
+ test("should cleanup agent after execution", async () => {
231
+ // After handle_query completes, agent should be unregistered
232
+ const agentName = "memory-extract-project";
233
+
234
+ await mockEnv.handle_query("test query", {
235
+ sessionId: "test-session",
236
+ agentType: agentName,
237
+ });
238
+
239
+ // Simulate cleanup (this happens in record.ts)
240
+ mockAgentComponent.unregisterAgent(agentName);
241
+
242
+ expect(mockAgentComponent.unregisterAgent).toHaveBeenCalledWith(agentName);
243
+ });
244
+
245
+ test("should cleanup toolComponent tools after execution", async () => {
246
+ // After handle_query completes, tools should be unregistered from ToolComponent
247
+ const agentName = "memory-extract-project";
248
+
249
+ await mockEnv.handle_query("test query", {
250
+ sessionId: "test-session",
251
+ agentType: agentName,
252
+ });
253
+
254
+ // Simulate tool cleanup
255
+ mockToolComponent.unregister("search_sessions");
256
+ mockToolComponent.unregister("get_session");
257
+ mockToolComponent.unregister("write_memory");
258
+
259
+ expect(mockToolComponent.unregister).toHaveBeenCalledTimes(3);
260
+ });
261
+
262
+ });
263
+
264
+ describe("Comparison: delegate_task vs record --extract pattern", () => {
265
+
266
+ test("delegate_task uses handle_query pattern (reference implementation)", async () => {
267
+ // Reference pattern from delegate_task (delegate-tool.ts)
268
+ const subagentType = "general";
269
+ const sessionId = "test-session";
270
+ const query = "Complete this task";
271
+
272
+ // Pattern:
273
+ // 1. Register agent (with deniedTools for nested delegation prevention)
274
+ // 2. Call env.handle_query with agentType
275
+ // 3. Return result
276
+
277
+ mockEnv.handle_query(query, {
278
+ sessionId,
279
+ deniedTools: ["delegate_task", "stop_task"],
280
+ agentType: subagentType,
281
+ });
282
+
283
+ expect(mockEnv.handle_query).toHaveBeenCalledWith(
284
+ query,
285
+ expect.objectContaining({
286
+ sessionId,
287
+ agentType: subagentType,
288
+ })
289
+ );
290
+ });
291
+
292
+ test("record --extract should follow same handle_query pattern", async () => {
293
+ // Expected pattern for record --extract after fix
294
+ const agentName = "memory-extract-project";
295
+ const sessionId = "test-session";
296
+ const query = "Extract memory from session";
297
+
298
+ // Pattern (same as delegate_task):
299
+ // 1. Register agent with allowedTools
300
+ // 2. Call env.handle_query with agentType (NOT agentComponent.run)
301
+ // 3. Return result
302
+
303
+ await mockEnv.handle_query(query, {
304
+ sessionId,
305
+ agentType: agentName,
306
+ });
307
+
308
+ expect(mockEnv.handle_query).toHaveBeenCalledWith(
309
+ query,
310
+ expect.objectContaining({
311
+ sessionId,
312
+ agentType: agentName,
313
+ })
314
+ );
315
+ });
316
+
317
+ });
318
+
319
+ describe("Error handling in handle_query execution", () => {
320
+
321
+ test("should handle handle_query errors gracefully", async () => {
322
+ // Mock handle_query to throw an error
323
+ mockEnv.handle_query.mockRejectedValueOnce(new Error("LLM error"));
324
+
325
+ let result: string;
326
+ try {
327
+ result = await mockEnv.handle_query("test query", { agentType: "memory-extract-project" });
328
+ } catch (error) {
329
+ result = `执行失败: ${error instanceof Error ? error.message : String(error)}`;
330
+ }
331
+
332
+ expect(result).toBe("执行失败: LLM error");
333
+ });
334
+
335
+ test("should handle empty handle_query result", async () => {
336
+ // Mock handle_query to return empty string
337
+ mockEnv.handle_query.mockResolvedValueOnce("");
338
+
339
+ const result = await mockEnv.handle_query("test query", { agentType: "memory-extract-project" });
340
+
341
+ expect(result).toBe("");
342
+ });
343
+
344
+ test("should handle long result from handle_query", async () => {
345
+ // Mock handle_query to return long result
346
+ const longResult = "A".repeat(500);
347
+ mockEnv.handle_query.mockResolvedValueOnce(longResult);
348
+
349
+ const result = await mockEnv.handle_query("test query", { agentType: "memory-extract-project" });
350
+
351
+ expect(result.length).toBe(500);
352
+ // record.ts truncates to 200 chars
353
+ expect(result.substring(0, 200).length).toBe(200);
354
+ });
355
+
356
+ });
357
+
358
+ describe("Result output handling", () => {
359
+
360
+ test("should output success message when handle_query succeeds", async () => {
361
+ const result = await mockEnv.handle_query("test query", { agentType: "memory-extract-project" });
362
+
363
+ // Success case: result is truthy and doesn't start with "执行失败"
364
+ if (result && !result.startsWith("执行失败")) {
365
+ // This is the success branch in record.ts
366
+ expect(true).toBe(true); // Would output success message
367
+ }
368
+ });
369
+
370
+ test("should output error message when handle_query fails", async () => {
371
+ mockEnv.handle_query.mockRejectedValueOnce(new Error("Connection timeout"));
372
+
373
+ let result: string;
374
+ try {
375
+ result = await mockEnv.handle_query("test query", { agentType: "memory-extract-project" });
376
+ } catch (error) {
377
+ result = `执行失败: ${error instanceof Error ? error.message : String(error)}`;
378
+ }
379
+
380
+ // Error case: result starts with "执行失败"
381
+ expect(result).toContain("执行失败");
382
+ });
383
+
384
+ });
385
+ });