@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,449 @@
1
+ /**
2
+ * @fileoverview ConfigService 测试
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
6
+ import * as fsSync from "fs";
7
+ import * as fsPromises from "fs/promises";
8
+ import * as path from "path";
9
+ import * as os from "os";
10
+ import { ConfigService } from "./config-service";
11
+
12
+ // Mock OutputService
13
+ const mockOutput = {
14
+ log: vi.fn(),
15
+ info: vi.fn(),
16
+ error: vi.fn(),
17
+ warn: vi.fn(),
18
+ debug: vi.fn(),
19
+ };
20
+
21
+ // Mock ConfigComponent
22
+ function createMockConfigComponent(memoryData: Record<string, unknown> = {}) {
23
+ const memorySource = {
24
+ name: "memory" as const,
25
+ priority: 0,
26
+ read: (key: string) => memoryData[key],
27
+ list: () =>
28
+ Object.entries(memoryData).map(([key, value]) => ({ key, value })),
29
+ };
30
+
31
+ return {
32
+ getSources: () => [memorySource],
33
+ get: (key: string) => memoryData[key],
34
+ };
35
+ }
36
+
37
+ describe("ConfigService", () => {
38
+ let tempDir: string;
39
+ let tempFilePath: string;
40
+
41
+ beforeEach(() => {
42
+ tempDir = fsSync.mkdtempSync(path.join(os.tmpdir(), "config-test-"));
43
+ tempFilePath = path.join(tempDir, "config.json");
44
+ vi.clearAllMocks();
45
+ });
46
+
47
+ afterEach(() => {
48
+ // 清理临时目录
49
+ if (fsSync.existsSync(tempDir)) {
50
+ fsSync.rmSync(tempDir, { recursive: true });
51
+ }
52
+ });
53
+
54
+ describe("getComponentConfig", () => {
55
+ it("should return empty config when no memory source", () => {
56
+ const mockConfig = {
57
+ getSources: () => [],
58
+ };
59
+ const service = new ConfigService(
60
+ mockConfig as any,
61
+ mockOutput as any
62
+ );
63
+
64
+ const config = service.getComponentConfig("agent");
65
+ expect(config).toEqual({});
66
+ });
67
+
68
+ it("should extract component config with prefix", () => {
69
+ const mockConfig = createMockConfigComponent({
70
+ "agent.maxIterations": 100,
71
+ "agent.maxErrorRetries": 3,
72
+ "agent.doomLoopThreshold": 5,
73
+ "llm.provider": "openai",
74
+ });
75
+
76
+ const service = new ConfigService(
77
+ mockConfig as any,
78
+ mockOutput as any
79
+ );
80
+
81
+ const config = service.getComponentConfig("agent");
82
+ expect(config).toEqual({
83
+ maxIterations: 100,
84
+ maxErrorRetries: 3,
85
+ doomLoopThreshold: 5,
86
+ });
87
+ });
88
+
89
+ it("should preserve nested structure", () => {
90
+ const mockConfig = createMockConfigComponent({
91
+ "agent.defaultAgent.maxIterations": 100,
92
+ "agent.defaultAgent.toolTimeout": 30000,
93
+ });
94
+
95
+ const service = new ConfigService(
96
+ mockConfig as any,
97
+ mockOutput as any
98
+ );
99
+
100
+ const config = service.getComponentConfig("agent");
101
+ expect(config).toEqual({
102
+ defaultAgent: {
103
+ maxIterations: 100,
104
+ toolTimeout: 30000,
105
+ },
106
+ });
107
+ });
108
+ });
109
+
110
+ describe("getComponentFilePath", () => {
111
+ it("should return file path from file source", () => {
112
+ const mockConfig = {
113
+ getSources: () => [
114
+ {
115
+ name: "file",
116
+ filePath: "/path/to/config.json",
117
+ },
118
+ ],
119
+ };
120
+ const service = new ConfigService(
121
+ mockConfig as any,
122
+ mockOutput as any
123
+ );
124
+
125
+ const filePath = service.getComponentFilePath("agent");
126
+ expect(filePath).toBe("/path/to/config.json");
127
+ });
128
+
129
+ it("should return undefined when no file source", () => {
130
+ const mockConfig = {
131
+ getSources: () => [
132
+ {
133
+ name: "memory",
134
+ priority: 0,
135
+ },
136
+ ],
137
+ };
138
+ const service = new ConfigService(
139
+ mockConfig as any,
140
+ mockOutput as any
141
+ );
142
+
143
+ const filePath = service.getComponentFilePath("agent");
144
+ expect(filePath).toBeUndefined();
145
+ });
146
+ });
147
+
148
+ describe("exportToFile", () => {
149
+ it("should export config to file", async () => {
150
+ const mockConfig = createMockConfigComponent({
151
+ "agent.maxIterations": 100,
152
+ });
153
+ const service = new ConfigService(
154
+ mockConfig as any,
155
+ mockOutput as any
156
+ );
157
+
158
+ await service.exportToFile("agent", {
159
+ filePath: tempFilePath,
160
+ });
161
+
162
+ const content = fsSync.readFileSync(tempFilePath, "utf-8");
163
+ const data = JSON.parse(content);
164
+ expect(data).toEqual({
165
+ agent: {
166
+ maxIterations: 100,
167
+ },
168
+ });
169
+ });
170
+
171
+ it("should return content when no file path", async () => {
172
+ const mockConfig = createMockConfigComponent({
173
+ "agent.maxIterations": 100,
174
+ });
175
+ const service = new ConfigService(
176
+ mockConfig as any,
177
+ mockOutput as any
178
+ );
179
+
180
+ const content = await service.exportToFile("agent", {});
181
+
182
+ expect(content).toContain("agent");
183
+ expect(content).toContain("maxIterations");
184
+ expect(content).toContain("100");
185
+ });
186
+
187
+ it("should create parent directories if not exist", async () => {
188
+ const mockConfig = createMockConfigComponent({
189
+ "agent.maxIterations": 100,
190
+ });
191
+ const service = new ConfigService(
192
+ mockConfig as any,
193
+ mockOutput as any
194
+ );
195
+
196
+ const nestedPath = path.join(tempDir, "nested", "dir", "config.json");
197
+ await service.exportToFile("agent", { filePath: nestedPath });
198
+
199
+ expect(fsSync.existsSync(nestedPath)).toBe(true);
200
+ });
201
+ });
202
+
203
+ describe("importFromFile", () => {
204
+ it("should import config from file and merge", async () => {
205
+ // 创建源文件
206
+ const sourceFile = path.join(tempDir, "source.json");
207
+ fsSync.writeFileSync(
208
+ sourceFile,
209
+ JSON.stringify({
210
+ agent: {
211
+ maxIterations: 200,
212
+ doomLoopThreshold: 5,
213
+ },
214
+ })
215
+ );
216
+
217
+ // 创建目标文件(已有部分配置)
218
+ fsSync.writeFileSync(
219
+ tempFilePath,
220
+ JSON.stringify({
221
+ agent: {
222
+ maxIterations: 100,
223
+ maxErrorRetries: 3,
224
+ },
225
+ llm: {
226
+ provider: "openai",
227
+ },
228
+ })
229
+ );
230
+
231
+ const mockConfig = {
232
+ getSources: () => [
233
+ {
234
+ name: "file",
235
+ filePath: tempFilePath,
236
+ },
237
+ ],
238
+ };
239
+ const service = new ConfigService(
240
+ mockConfig as any,
241
+ mockOutput as any
242
+ );
243
+
244
+ const result = await service.importFromFile("agent", {
245
+ sourceFile,
246
+ });
247
+
248
+ expect(result.success).toBe(true);
249
+ expect(result.merged).toBe(true);
250
+ expect(result.filePath).toBe(tempFilePath);
251
+ expect(result.changes.length).toBeGreaterThan(0);
252
+
253
+ // 验证文件内容
254
+ const content = fsSync.readFileSync(tempFilePath, "utf-8");
255
+ const data = JSON.parse(content);
256
+ expect(data.agent.maxIterations).toBe(200); // 被覆盖
257
+ expect(data.agent.maxErrorRetries).toBe(3); // 保留
258
+ expect(data.agent.doomLoopThreshold).toBe(5); // 新增
259
+ expect(data.llm.provider).toBe("openai"); // 保留
260
+ });
261
+
262
+ it("should support dry-run mode", async () => {
263
+ const sourceFile = path.join(tempDir, "source.json");
264
+ fsSync.writeFileSync(
265
+ sourceFile,
266
+ JSON.stringify({
267
+ agent: { maxIterations: 200 },
268
+ })
269
+ );
270
+
271
+ fsSync.writeFileSync(
272
+ tempFilePath,
273
+ JSON.stringify({
274
+ agent: { maxIterations: 100 },
275
+ })
276
+ );
277
+
278
+ const mockConfig = {
279
+ getSources: () => [
280
+ {
281
+ name: "file",
282
+ filePath: tempFilePath,
283
+ },
284
+ ],
285
+ };
286
+ const service = new ConfigService(
287
+ mockConfig as any,
288
+ mockOutput as any
289
+ );
290
+
291
+ const result = await service.importFromFile("agent", {
292
+ sourceFile,
293
+ dryRun: true,
294
+ });
295
+
296
+ expect(result.success).toBe(true);
297
+ expect(result.filePath).toBe(tempFilePath);
298
+
299
+ // 文件内容不应被修改
300
+ const content = fsSync.readFileSync(tempFilePath, "utf-8");
301
+ const data = JSON.parse(content);
302
+ expect(data.agent.maxIterations).toBe(100); // 未被修改
303
+ });
304
+
305
+ it("should create new file if target does not exist", async () => {
306
+ const sourceFile = path.join(tempDir, "source.json");
307
+ fsSync.writeFileSync(
308
+ sourceFile,
309
+ JSON.stringify({
310
+ agent: { maxIterations: 200 },
311
+ })
312
+ );
313
+
314
+ const mockConfig = {
315
+ getSources: () => [
316
+ {
317
+ name: "file",
318
+ filePath: tempFilePath,
319
+ },
320
+ ],
321
+ };
322
+ const service = new ConfigService(
323
+ mockConfig as any,
324
+ mockOutput as any
325
+ );
326
+
327
+ const result = await service.importFromFile("agent", {
328
+ sourceFile,
329
+ });
330
+
331
+ expect(result.success).toBe(true);
332
+ expect(fsSync.existsSync(tempFilePath)).toBe(true);
333
+ });
334
+
335
+ it("should return error when source file not found", async () => {
336
+ const mockConfig = {
337
+ getSources: () => [
338
+ {
339
+ name: "file",
340
+ filePath: tempFilePath,
341
+ },
342
+ ],
343
+ };
344
+ const service = new ConfigService(
345
+ mockConfig as any,
346
+ mockOutput as any
347
+ );
348
+
349
+ const result = await service.importFromFile("agent", {
350
+ sourceFile: "/non/existent/file.json",
351
+ });
352
+
353
+ expect(result.success).toBe(false);
354
+ expect(result.message).toContain("无法读取源文件");
355
+ });
356
+
357
+ it("should return error when component config not found in source", async () => {
358
+ const sourceFile = path.join(tempDir, "source.json");
359
+ fsSync.writeFileSync(
360
+ sourceFile,
361
+ JSON.stringify({
362
+ llm: { provider: "openai" },
363
+ })
364
+ );
365
+
366
+ const mockConfig = {
367
+ getSources: () => [
368
+ {
369
+ name: "file",
370
+ filePath: tempFilePath,
371
+ },
372
+ ],
373
+ };
374
+ const service = new ConfigService(
375
+ mockConfig as any,
376
+ mockOutput as any
377
+ );
378
+
379
+ const result = await service.importFromFile("agent", {
380
+ sourceFile,
381
+ });
382
+
383
+ expect(result.success).toBe(false);
384
+ expect(result.message).toContain("未找到 agent 配置");
385
+ });
386
+
387
+ it("should preserve other components when importing JSONC file with comments", async () => {
388
+ // 创建源文件
389
+ const sourceFile = path.join(tempDir, "source.json");
390
+ fsSync.writeFileSync(
391
+ sourceFile,
392
+ JSON.stringify({
393
+ agent: {
394
+ maxIterations: 200,
395
+ doomLoopThreshold: 5,
396
+ },
397
+ })
398
+ );
399
+
400
+ // 创建目标文件(JSONC 格式,包含注释和其他组件配置)
401
+ const targetFilePath = path.join(tempDir, "target.jsonc");
402
+ fsSync.writeFileSync(
403
+ targetFilePath,
404
+ `{
405
+ // LLM 配置
406
+ "llm": {
407
+ "provider": "openai",
408
+ "model": "gpt-4"
409
+ },
410
+ "agent": {
411
+ "maxIterations": 100,
412
+ "maxErrorRetries": 3
413
+ }
414
+ }`
415
+ );
416
+
417
+ const mockConfig = {
418
+ getSources: () => [
419
+ {
420
+ name: "file",
421
+ filePath: targetFilePath,
422
+ },
423
+ ],
424
+ };
425
+ const service = new ConfigService(
426
+ mockConfig as any,
427
+ mockOutput as any
428
+ );
429
+
430
+ const result = await service.importFromFile("agent", {
431
+ sourceFile,
432
+ });
433
+
434
+ expect(result.success).toBe(true);
435
+ expect(result.merged).toBe(true);
436
+
437
+ // 验证文件内容 - llm 配置应该被保留
438
+ const content = fsSync.readFileSync(targetFilePath, "utf-8");
439
+ const data = JSON.parse(content);
440
+ expect(data.llm).toBeDefined();
441
+ expect(data.llm.provider).toBe("openai");
442
+ expect(data.llm.model).toBe("gpt-4");
443
+ // agent 配置应该被合并
444
+ expect(data.agent.maxIterations).toBe(200); // 被覆盖
445
+ expect(data.agent.maxErrorRetries).toBe(3); // 保留
446
+ expect(data.agent.doomLoopThreshold).toBe(5); // 新增
447
+ });
448
+ });
449
+ });