@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.
- package/README.md +126 -0
- package/dist/bin/roy.js +127297 -0
- package/dist/roy-agent-darwin-arm64/bin/roy.js +127297 -0
- package/dist/roy-agent-darwin-x64/bin/roy.js +127297 -0
- package/dist/roy-agent-linux-arm64/bin/roy.js +127297 -0
- package/dist/roy-agent-linux-x64/bin/roy.js +127297 -0
- package/dist/roy-agent-windows-x64/bin/roy.js +127297 -0
- package/package.json +91 -0
- package/src/bin/roy.ts +12 -0
- package/src/cli.ts +101 -0
- package/src/commands/act.ts +480 -0
- package/src/commands/commands-add.ts +110 -0
- package/src/commands/commands-dirs.ts +70 -0
- package/src/commands/commands-info.ts +90 -0
- package/src/commands/commands-list.ts +161 -0
- package/src/commands/commands-remove.ts +147 -0
- package/src/commands/commands.ts +55 -0
- package/src/commands/config/config-service.test.ts +449 -0
- package/src/commands/config/config-service.ts +312 -0
- package/src/commands/config/deep-merge.test.ts +168 -0
- package/src/commands/config/deep-merge.ts +63 -0
- package/src/commands/config/export.ts +97 -0
- package/src/commands/config/filter-history-e2e.test.ts +141 -0
- package/src/commands/config/import-preserve-refs.test.ts +212 -0
- package/src/commands/config/import.ts +119 -0
- package/src/commands/config/index.ts +35 -0
- package/src/commands/config/list.ts +281 -0
- package/src/commands/config/roy-config-e2e.test.ts +297 -0
- package/src/commands/config/types.ts +54 -0
- package/src/commands/debug/index.ts +38 -0
- package/src/commands/debug/log.test.ts +233 -0
- package/src/commands/debug/log.ts +123 -0
- package/src/commands/debug/span.test.ts +297 -0
- package/src/commands/debug/span.ts +211 -0
- package/src/commands/debug/trace.test.ts +254 -0
- package/src/commands/debug/trace.ts +140 -0
- package/src/commands/eventsource/add.ts +133 -0
- package/src/commands/eventsource/index.ts +48 -0
- package/src/commands/eventsource/list.ts +194 -0
- package/src/commands/eventsource/remove.ts +95 -0
- package/src/commands/eventsource/start.ts +103 -0
- package/src/commands/eventsource/status.ts +185 -0
- package/src/commands/eventsource/stop.ts +89 -0
- package/src/commands/index.ts +22 -0
- package/src/commands/input-handler.test.ts +76 -0
- package/src/commands/input-handler.ts +43 -0
- package/src/commands/interactive-esc.test.ts +254 -0
- package/src/commands/interactive.shutdown.test.ts +122 -0
- package/src/commands/interactive.test.ts +221 -0
- package/src/commands/interactive.ts +1015 -0
- package/src/commands/lsp/check.ts +92 -0
- package/src/commands/lsp/index.ts +32 -0
- package/src/commands/lsp/install.ts +126 -0
- package/src/commands/lsp/list.ts +64 -0
- package/src/commands/mcp/index.ts +27 -0
- package/src/commands/mcp/list.ts +116 -0
- package/src/commands/mcp/reload.ts +70 -0
- package/src/commands/mcp/tools.ts +121 -0
- package/src/commands/memory/extract-e2e.test.ts +388 -0
- package/src/commands/memory/index.ts +11 -0
- package/src/commands/memory/memory-simplified.test.ts +58 -0
- package/src/commands/memory/memory.ts +25 -0
- package/src/commands/memory/organize.ts +300 -0
- package/src/commands/memory/recall.test.ts +120 -0
- package/src/commands/memory/recall.ts +88 -0
- package/src/commands/memory/record-extract-handle-query.test.ts +385 -0
- package/src/commands/memory/record-prompt-component.test.ts +343 -0
- package/src/commands/memory/record.test.ts +92 -0
- package/src/commands/memory/record.ts +332 -0
- package/src/commands/plugin.test.ts +292 -0
- package/src/commands/plugin.ts +267 -0
- package/src/commands/sessions/active.ts +96 -0
- package/src/commands/sessions/add-message.ts +96 -0
- package/src/commands/sessions/checkpoints.ts +154 -0
- package/src/commands/sessions/compact.test.ts +215 -0
- package/src/commands/sessions/compact.ts +269 -0
- package/src/commands/sessions/delete.ts +236 -0
- package/src/commands/sessions/get.ts +165 -0
- package/src/commands/sessions/grep.ts +233 -0
- package/src/commands/sessions/index.ts +95 -0
- package/src/commands/sessions/list.ts +210 -0
- package/src/commands/sessions/messages.test.ts +333 -0
- package/src/commands/sessions/messages.ts +248 -0
- package/src/commands/sessions/mock.ts +194 -0
- package/src/commands/sessions/new.ts +82 -0
- package/src/commands/sessions/rename.ts +98 -0
- package/src/commands/shared/event-handler.ts +213 -0
- package/src/commands/shared/event-message-formatter.ts +295 -0
- package/src/commands/shared/index.ts +11 -0
- package/src/commands/shared/query-executor.test.ts +434 -0
- package/src/commands/shared/query-executor.ts +324 -0
- package/src/commands/shared/repl-engine.test.ts +354 -0
- package/src/commands/shared/session-manager.test.ts +212 -0
- package/src/commands/shared/session-manager.ts +114 -0
- package/src/commands/skills/get.ts +90 -0
- package/src/commands/skills/index.ts +39 -0
- package/src/commands/skills/list.ts +129 -0
- package/src/commands/skills/reload.ts +59 -0
- package/src/commands/skills/search.ts +132 -0
- package/src/commands/skills/show-config.ts +93 -0
- package/src/commands/tasks/complete.ts +92 -0
- package/src/commands/tasks/create.ts +118 -0
- package/src/commands/tasks/delete.ts +86 -0
- package/src/commands/tasks/get.ts +116 -0
- package/src/commands/tasks/index.ts +53 -0
- package/src/commands/tasks/list.ts +140 -0
- package/src/commands/tasks/operations.ts +120 -0
- package/src/commands/tasks/update.ts +122 -0
- package/src/commands/tools/exec-tool.ts +128 -0
- package/src/commands/tools/get.ts +114 -0
- package/src/commands/tools/index.ts +35 -0
- package/src/commands/tools/list.ts +107 -0
- package/src/commands/tools/shared/index.ts +7 -0
- package/src/commands/tools/shared/schema-helper.ts +111 -0
- package/src/commands/workflow/commands/add.ts +315 -0
- package/src/commands/workflow/commands/get.ts +193 -0
- package/src/commands/workflow/commands/list.ts +137 -0
- package/src/commands/workflow/commands/nodes.ts +528 -0
- package/src/commands/workflow/commands/remove.ts +94 -0
- package/src/commands/workflow/commands/run.ts +398 -0
- package/src/commands/workflow/commands/status.ts +147 -0
- package/src/commands/workflow/commands/stop.ts +91 -0
- package/src/commands/workflow/commands/update.ts +130 -0
- package/src/commands/workflow/commands/validate.ts +139 -0
- package/src/commands/workflow/commands/workflow-cli.test.ts +196 -0
- package/src/commands/workflow/index.ts +65 -0
- package/src/commands/workflow/renderers.ts +358 -0
- package/src/commands/workflow/validators/index.ts +8 -0
- package/src/commands/workflow/validators/node-validator-factory.ts +40 -0
- package/src/commands/workflow/validators/node-validator.ts +125 -0
- package/src/commands/workflow/validators/nodes/agent-node-validator.ts +58 -0
- package/src/commands/workflow/validators/nodes/condition-node-validator.ts +34 -0
- package/src/commands/workflow/validators/nodes/decorator-node-validator.ts +45 -0
- package/src/commands/workflow/validators/nodes/merge-node-validator.ts +46 -0
- package/src/commands/workflow/validators/nodes/skill-node-validator.ts +33 -0
- package/src/commands/workflow/validators/nodes/tool-node-validator.ts +54 -0
- package/src/commands/workflow/validators/nodes/workflow-node-validator.ts +33 -0
- package/src/commands/workflow/validators/types.ts +78 -0
- package/src/commands/workflow/validators/workflow-validator.test.ts +273 -0
- package/src/commands/workflow/validators/workflow-validator.ts +320 -0
- package/src/index.ts +19 -0
- package/src/plugin/apply.ts +103 -0
- package/src/plugin/discover.ts +219 -0
- package/src/plugin/index.ts +45 -0
- package/src/plugin/registry.ts +272 -0
- package/src/plugin/types.ts +165 -0
- package/src/services/context-handler.service.test.ts +501 -0
- package/src/services/context-handler.service.ts +372 -0
- package/src/services/environment.service.commands-prompt.test.ts +167 -0
- package/src/services/environment.service.ts +656 -0
- package/src/services/output.service.test.ts +92 -0
- package/src/services/output.service.ts +122 -0
- package/src/services/quiet-mode.service.test.ts +114 -0
- package/src/services/quiet-mode.service.ts +81 -0
- package/src/services/stream-output.service.test.ts +214 -0
- package/src/services/stream-output.service.ts +323 -0
- package/src/util/which.test.ts +101 -0
- 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
|
+
});
|