@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,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Sessions Command Entry
|
|
3
|
+
*
|
|
4
|
+
* 命令入口:roy sessions [action]
|
|
5
|
+
*
|
|
6
|
+
* 注意:--quiet / -q 选项现在由 cli.ts 的全局 middleware 统一处理
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { CommandModule } from "yargs";
|
|
10
|
+
import { ListCommand } from "./list";
|
|
11
|
+
import { GetCommand } from "./get";
|
|
12
|
+
import { NewCommand } from "./new";
|
|
13
|
+
import { RenameCommand } from "./rename";
|
|
14
|
+
import { DeleteCommand } from "./delete";
|
|
15
|
+
import { MessagesCommand } from "./messages";
|
|
16
|
+
import { CompactCommand } from "./compact";
|
|
17
|
+
import { CheckpointsCommand } from "./checkpoints";
|
|
18
|
+
import { ActiveCommand } from "./active";
|
|
19
|
+
import { AddMessageCommand } from "./add-message";
|
|
20
|
+
import { MockCommand } from "./mock";
|
|
21
|
+
import { GrepCommand } from "./grep";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Sessions Command
|
|
25
|
+
*
|
|
26
|
+
* 会话管理命令,提供以下子命令:
|
|
27
|
+
* - list: 列出所有会话
|
|
28
|
+
* - get: 获取会话详情
|
|
29
|
+
* - new: 创建新会话
|
|
30
|
+
* - rename: 更新会话标题
|
|
31
|
+
* - delete: 删除会话(支持批量删除、全量删除、按时间删除)
|
|
32
|
+
* - messages: 查看消息
|
|
33
|
+
* - grep: 全文搜索消息
|
|
34
|
+
* - compact: 压缩会话 (new)
|
|
35
|
+
* - checkpoints: 查看检查点 (new)
|
|
36
|
+
* - active: 活跃会话管理
|
|
37
|
+
* - add-message: 添加单条消息
|
|
38
|
+
* - mock: 插入模拟消息(用于测试)
|
|
39
|
+
*
|
|
40
|
+
* 删除命令使用示例:
|
|
41
|
+
* roy sessions delete <id> # 删除单个会话
|
|
42
|
+
* roy sessions delete <id1> <id2> <id3> # 批量删除
|
|
43
|
+
* roy sessions delete --all # 删除所有会话
|
|
44
|
+
* roy sessions delete --all --keep-active # 删除所有非活跃会话
|
|
45
|
+
* roy sessions delete --older-than 30 # 删除30天前的会话
|
|
46
|
+
* roy sessions delete --dry-run # 预览模式
|
|
47
|
+
*
|
|
48
|
+
* 搜索命令使用示例:
|
|
49
|
+
* roy sessions grep K8s deployment error # 多关键字搜索
|
|
50
|
+
* roy sessions grep "exact phrase" # 精确短语
|
|
51
|
+
* roy sessions grep --session <id> query # 搜索指定会话
|
|
52
|
+
* roy sessions grep --limit 10 query # 限制结果数
|
|
53
|
+
* roy sessions grep --json query # JSON 输出
|
|
54
|
+
*/
|
|
55
|
+
export const SessionsCommand: CommandModule = {
|
|
56
|
+
command: "sessions",
|
|
57
|
+
describe: `会话管理 - 列出、创建、删除和管理会话
|
|
58
|
+
|
|
59
|
+
删除命令使用示例:
|
|
60
|
+
roy sessions delete <id> # 删除单个会话
|
|
61
|
+
roy sessions delete <id1> <id2> <id3> # 批量删除
|
|
62
|
+
roy sessions delete --all # 删除所有会话
|
|
63
|
+
roy sessions delete --all --keep-active # 删除所有非活跃会话
|
|
64
|
+
roy sessions delete --older-than 30 # 删除30天前的会话
|
|
65
|
+
roy sessions delete --dry-run # 预览模式
|
|
66
|
+
|
|
67
|
+
搜索命令使用示例:
|
|
68
|
+
roy sessions grep K8s deployment error # 多关键字搜索
|
|
69
|
+
roy sessions grep "exact phrase" # 精确短语
|
|
70
|
+
roy sessions grep --session <id> query # 搜索指定会话
|
|
71
|
+
roy sessions grep --limit 10 query # 限制结果数
|
|
72
|
+
roy sessions grep --json query # JSON 输出`,
|
|
73
|
+
builder: (yargs) => {
|
|
74
|
+
// 注意:--quiet / -q 选项现在由 cli.ts 的全局 middleware 统一处理
|
|
75
|
+
return yargs
|
|
76
|
+
.command(ListCommand)
|
|
77
|
+
.command(GetCommand)
|
|
78
|
+
.command(NewCommand)
|
|
79
|
+
.command(RenameCommand)
|
|
80
|
+
.command(DeleteCommand)
|
|
81
|
+
.command(MessagesCommand)
|
|
82
|
+
.command(GrepCommand)
|
|
83
|
+
.command(CompactCommand)
|
|
84
|
+
.command(CheckpointsCommand)
|
|
85
|
+
.command(ActiveCommand)
|
|
86
|
+
.command(AddMessageCommand)
|
|
87
|
+
.command(MockCommand)
|
|
88
|
+
.demandCommand()
|
|
89
|
+
.help();
|
|
90
|
+
},
|
|
91
|
+
handler: () => {
|
|
92
|
+
// Default handler - shouldn't be reached if demandCommand is true
|
|
93
|
+
console.log("Use 'roy sessions --help' for usage information");
|
|
94
|
+
},
|
|
95
|
+
};
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Sessions List Command
|
|
3
|
+
*
|
|
4
|
+
* 命令:roy sessions list
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { CommandModule } from "yargs";
|
|
8
|
+
import { EnvironmentService } from "../../services/environment.service";
|
|
9
|
+
import { OutputService } from "../../services/output.service";
|
|
10
|
+
import { CliQuietModeService } from "../../services/quiet-mode.service";
|
|
11
|
+
import chalk from "chalk";
|
|
12
|
+
import type {
|
|
13
|
+
SessionComponent,
|
|
14
|
+
Session,
|
|
15
|
+
ListSessionsOptions,
|
|
16
|
+
} from "@ai-setting/roy-agent-core";
|
|
17
|
+
|
|
18
|
+
// Inline type for WorkflowSessionMetadata to avoid import issue
|
|
19
|
+
interface WorkflowSessionMetadata {
|
|
20
|
+
type: 'workflow';
|
|
21
|
+
workflowId?: string;
|
|
22
|
+
workflowName: string;
|
|
23
|
+
status: 'running' | 'paused' | 'completed' | 'failed';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ListOptions {
|
|
27
|
+
limit?: number;
|
|
28
|
+
offset?: number;
|
|
29
|
+
sort?: string;
|
|
30
|
+
order?: string;
|
|
31
|
+
json?: boolean;
|
|
32
|
+
quiet?: boolean;
|
|
33
|
+
id?: boolean;
|
|
34
|
+
config?: string;
|
|
35
|
+
type?: string;
|
|
36
|
+
status?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const ListCommand: CommandModule<object, ListOptions> = {
|
|
40
|
+
command: "list [options]",
|
|
41
|
+
aliases: ["ls"],
|
|
42
|
+
describe: "列出所有会话",
|
|
43
|
+
|
|
44
|
+
builder: (yargs) =>
|
|
45
|
+
yargs
|
|
46
|
+
.option("limit", { alias: "n", type: "number", default: 20 })
|
|
47
|
+
.option("offset", { type: "number", default: 0 })
|
|
48
|
+
.option("sort", { type: "string", default: "updatedAt" })
|
|
49
|
+
.option("order", { type: "string", default: "desc" })
|
|
50
|
+
.option("json", { alias: "j", type: "boolean", default: false })
|
|
51
|
+
.option("quiet", { alias: "q", type: "boolean", default: false })
|
|
52
|
+
.option("id", { alias: "i", type: "boolean", default: false, description: "显示 Session ID" })
|
|
53
|
+
.option("type", { alias: "t", type: "string", description: "按 session 类型过滤 (如 workflow)" })
|
|
54
|
+
.option("status", { alias: "s", type: "string", description: "按状态过滤 (如 running, paused)" }),
|
|
55
|
+
|
|
56
|
+
async handler(args) {
|
|
57
|
+
const isQuiet = args.quiet === true;
|
|
58
|
+
if (isQuiet) {
|
|
59
|
+
CliQuietModeService.getInstance().setQuiet(true);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const output = new OutputService();
|
|
63
|
+
output.configure({ quiet: isQuiet });
|
|
64
|
+
const envService = new EnvironmentService(output);
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
await envService.create({ configPath: args.config });
|
|
68
|
+
const env = envService.getEnvironment();
|
|
69
|
+
if (!env) {
|
|
70
|
+
output.error("Failed to create environment");
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
const sessionComponent = env.getComponent("session") as SessionComponent;
|
|
74
|
+
|
|
75
|
+
if (!sessionComponent) {
|
|
76
|
+
output.error("SessionComponent not available");
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const listOptions: ListSessionsOptions = {
|
|
81
|
+
sort: { field: args.sort as any, order: args.order as any },
|
|
82
|
+
offset: args.offset,
|
|
83
|
+
limit: args.limit,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Add metadata filter if type or status is specified
|
|
87
|
+
if (args.type || args.status) {
|
|
88
|
+
const metadataFilter: Record<string, unknown> = {};
|
|
89
|
+
if (args.type) {
|
|
90
|
+
metadataFilter.type = args.type;
|
|
91
|
+
}
|
|
92
|
+
if (args.status) {
|
|
93
|
+
metadataFilter.status = args.status;
|
|
94
|
+
}
|
|
95
|
+
listOptions.filter = {
|
|
96
|
+
metadata: metadataFilter,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const sessions = await sessionComponent.list(listOptions);
|
|
101
|
+
const totalCount = await sessionComponent.getCount();
|
|
102
|
+
const activeSessionId = sessionComponent.getActiveSessionId();
|
|
103
|
+
|
|
104
|
+
if (args.json) {
|
|
105
|
+
output.json({
|
|
106
|
+
sessions: sessions.map((s: Session) => {
|
|
107
|
+
const workflowMeta = s.metadata?.type === 'workflow'
|
|
108
|
+
? s.metadata as unknown as WorkflowSessionMetadata
|
|
109
|
+
: null;
|
|
110
|
+
return {
|
|
111
|
+
id: s.id,
|
|
112
|
+
title: s.title,
|
|
113
|
+
directory: s.directory,
|
|
114
|
+
messageCount: s.messageCount,
|
|
115
|
+
type: workflowMeta?.type || 'chat',
|
|
116
|
+
status: workflowMeta?.status,
|
|
117
|
+
workflowName: workflowMeta?.workflowName,
|
|
118
|
+
hasCheckpoint: !!s.metadata?.checkpoints?.checkpoints?.length,
|
|
119
|
+
lastCheckpointAt: s.metadata?.checkpoints?.latestCheckpointId,
|
|
120
|
+
createdAt: new Date(s.createdAt).toISOString(),
|
|
121
|
+
updatedAt: new Date(s.updatedAt).toISOString(),
|
|
122
|
+
isActive: s.id === activeSessionId,
|
|
123
|
+
};
|
|
124
|
+
}),
|
|
125
|
+
total: sessions.length,
|
|
126
|
+
totalCount: totalCount,
|
|
127
|
+
});
|
|
128
|
+
} else if (args.quiet || args.id) {
|
|
129
|
+
// 简洁输出:只显示 ID
|
|
130
|
+
sessions.forEach((s: Session) => output.log(s.id));
|
|
131
|
+
} else {
|
|
132
|
+
// 表格输出
|
|
133
|
+
const hasTypeFilter = !!args.type;
|
|
134
|
+
const hasStatusFilter = !!args.status;
|
|
135
|
+
|
|
136
|
+
const header = [
|
|
137
|
+
chalk.bold("#"),
|
|
138
|
+
chalk.bold("Title"),
|
|
139
|
+
chalk.bold("ID"),
|
|
140
|
+
hasTypeFilter || hasStatusFilter ? chalk.bold("Status") : null,
|
|
141
|
+
chalk.bold("Msgs"),
|
|
142
|
+
chalk.bold("CP"),
|
|
143
|
+
].filter(Boolean).join(" │ ");
|
|
144
|
+
|
|
145
|
+
const rows = sessions.map((s: Session, i: number) => {
|
|
146
|
+
const marker = s.id === activeSessionId ? chalk.green("▶") : " ";
|
|
147
|
+
const hasCp = (s.metadata?.checkpoints?.checkpoints?.length ?? 0) > 0;
|
|
148
|
+
const title = s.title.length > 20 ? s.title.slice(0, 17) + "..." : s.title;
|
|
149
|
+
// 显示完整的 Session ID
|
|
150
|
+
const idStr = s.id;
|
|
151
|
+
|
|
152
|
+
// Get workflow metadata if present
|
|
153
|
+
const workflowMeta = s.metadata?.type === 'workflow'
|
|
154
|
+
? s.metadata as unknown as WorkflowSessionMetadata
|
|
155
|
+
: null;
|
|
156
|
+
const typeStr = workflowMeta?.type || '';
|
|
157
|
+
const statusStr = workflowMeta?.status || '';
|
|
158
|
+
|
|
159
|
+
// Format status for display
|
|
160
|
+
let statusDisplay = '';
|
|
161
|
+
if (hasTypeFilter || hasStatusFilter) {
|
|
162
|
+
if (workflowMeta) {
|
|
163
|
+
statusDisplay = workflowMeta.status === 'running' ? chalk.green('running')
|
|
164
|
+
: workflowMeta.status === 'paused' ? chalk.yellow('paused')
|
|
165
|
+
: workflowMeta.status === 'completed' ? chalk.gray('completed')
|
|
166
|
+
: workflowMeta.status === 'failed' ? chalk.red('failed')
|
|
167
|
+
: statusStr;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const cells = [
|
|
172
|
+
marker + " " + (args.offset! + i + 1),
|
|
173
|
+
title,
|
|
174
|
+
chalk.gray(idStr),
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
if (hasTypeFilter || hasStatusFilter) {
|
|
178
|
+
cells.push(statusDisplay || chalk.gray('-'));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
cells.push(
|
|
182
|
+
s.messageCount.toString(),
|
|
183
|
+
hasCp ? chalk.green("✓") : chalk.gray("-"),
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
return cells.join(" │ ");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const showingInfo = totalCount > sessions.length
|
|
190
|
+
? `Showing ${sessions.length} of ${totalCount} sessions`
|
|
191
|
+
: `${totalCount} sessions`;
|
|
192
|
+
|
|
193
|
+
output.log([
|
|
194
|
+
`┌─ Sessions ${"─".repeat(50)}┐`,
|
|
195
|
+
`│${header}│`,
|
|
196
|
+
"├" + "─".repeat(header.length + 2) + "┤",
|
|
197
|
+
...rows.map((r: string) => `│${r}│`),
|
|
198
|
+
"└" + "─".repeat(header.length + 2) + "┘",
|
|
199
|
+
"",
|
|
200
|
+
chalk.gray(showingInfo),
|
|
201
|
+
].join("\n"));
|
|
202
|
+
}
|
|
203
|
+
} catch (error) {
|
|
204
|
+
output.error(`Failed to list sessions: ${error instanceof Error ? error.message : String(error)}`);
|
|
205
|
+
process.exit(1);
|
|
206
|
+
} finally {
|
|
207
|
+
await envService.dispose();
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
};
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Messages Command Tests
|
|
3
|
+
*
|
|
4
|
+
* TDD: 测试 sessions messages 命令的分页和总数功能
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, test, expect, vi, beforeEach } from "bun:test";
|
|
8
|
+
import type { SessionMessage } from "@ai-setting/roy-agent-core";
|
|
9
|
+
|
|
10
|
+
describe("MessagesCommand MessagesOptions", () => {
|
|
11
|
+
test("should have required offset parameter", () => {
|
|
12
|
+
// Given: 期望 offset 是必填参数
|
|
13
|
+
const options = {
|
|
14
|
+
sessionId: "test-session",
|
|
15
|
+
offset: 10,
|
|
16
|
+
limit: 20,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Then: offset 和 limit 应该是必填的
|
|
20
|
+
expect(options.offset).toBeDefined();
|
|
21
|
+
expect(options.limit).toBeDefined();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("should have required limit parameter", () => {
|
|
25
|
+
const options = {
|
|
26
|
+
sessionId: "test-session",
|
|
27
|
+
offset: 0,
|
|
28
|
+
limit: 50,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
expect(options.offset).toBeDefined();
|
|
32
|
+
expect(options.limit).toBeDefined();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("MessagesCommand Pagination Logic", () => {
|
|
37
|
+
// Mock SessionComponent
|
|
38
|
+
let mockSessionComponent: {
|
|
39
|
+
get: ReturnType<typeof vi.fn>;
|
|
40
|
+
getMessages: ReturnType<typeof vi.fn>;
|
|
41
|
+
getMessageCount: ReturnType<typeof vi.fn>;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
vi.clearAllMocks();
|
|
46
|
+
|
|
47
|
+
mockSessionComponent = {
|
|
48
|
+
get: vi.fn(),
|
|
49
|
+
getMessages: vi.fn(),
|
|
50
|
+
getMessageCount: vi.fn(),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Setup default mocks
|
|
54
|
+
mockSessionComponent.get.mockResolvedValue({
|
|
55
|
+
id: "test-session",
|
|
56
|
+
title: "Test Session",
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
mockSessionComponent.getMessageCount.mockResolvedValue(100);
|
|
60
|
+
mockSessionComponent.getMessages.mockResolvedValue([
|
|
61
|
+
{
|
|
62
|
+
id: "msg-1",
|
|
63
|
+
sessionID: "test-session",
|
|
64
|
+
role: "user",
|
|
65
|
+
content: "Hello",
|
|
66
|
+
timestamp: Date.now(),
|
|
67
|
+
},
|
|
68
|
+
]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("should call getMessages with offset and limit", async () => {
|
|
72
|
+
// Given: offset=10, limit=20
|
|
73
|
+
const offset = 10;
|
|
74
|
+
const limit = 20;
|
|
75
|
+
|
|
76
|
+
// When: 调用 getMessages
|
|
77
|
+
const messages = await mockSessionComponent.getMessages("test-session", {
|
|
78
|
+
offset,
|
|
79
|
+
limit,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Then: 应该传入正确的 offset 和 limit
|
|
83
|
+
expect(mockSessionComponent.getMessages).toHaveBeenCalledWith(
|
|
84
|
+
"test-session",
|
|
85
|
+
{ offset: 10, limit: 20 }
|
|
86
|
+
);
|
|
87
|
+
expect(messages).toHaveLength(1);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("should call getMessageCount to get total", async () => {
|
|
91
|
+
// Given
|
|
92
|
+
const sessionId = "test-session";
|
|
93
|
+
|
|
94
|
+
// When: 获取消息总数
|
|
95
|
+
const totalCount = await mockSessionComponent.getMessageCount(sessionId);
|
|
96
|
+
|
|
97
|
+
// Then: 应该返回正确的总数
|
|
98
|
+
expect(mockSessionComponent.getMessageCount).toHaveBeenCalledWith(sessionId);
|
|
99
|
+
expect(totalCount).toBe(100);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("should return messages with pagination info", async () => {
|
|
103
|
+
// Given
|
|
104
|
+
const sessionId = "test-session";
|
|
105
|
+
const offset = 0;
|
|
106
|
+
const limit = 10;
|
|
107
|
+
|
|
108
|
+
// 模拟分页数据
|
|
109
|
+
const mockMessages: SessionMessage[] = Array.from({ length: 10 }, (_, i) => ({
|
|
110
|
+
id: `msg-${i}`,
|
|
111
|
+
sessionID: sessionId,
|
|
112
|
+
role: i % 2 === 0 ? "user" : "assistant",
|
|
113
|
+
content: `Message ${i}`,
|
|
114
|
+
timestamp: Date.now() + i,
|
|
115
|
+
}));
|
|
116
|
+
|
|
117
|
+
mockSessionComponent.getMessages.mockResolvedValue(mockMessages);
|
|
118
|
+
mockSessionComponent.getMessageCount.mockResolvedValue(100);
|
|
119
|
+
|
|
120
|
+
// When
|
|
121
|
+
const [messages, totalCount] = await Promise.all([
|
|
122
|
+
mockSessionComponent.getMessages(sessionId, { offset, limit }),
|
|
123
|
+
mockSessionComponent.getMessageCount(sessionId),
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
// Then
|
|
127
|
+
expect(messages).toHaveLength(10);
|
|
128
|
+
expect(totalCount).toBe(100);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("should handle edge case: offset equals total count", async () => {
|
|
132
|
+
// Given: offset 等于总数(最后一页之后)
|
|
133
|
+
const sessionId = "test-session";
|
|
134
|
+
mockSessionComponent.getMessages.mockResolvedValue([]);
|
|
135
|
+
mockSessionComponent.getMessageCount.mockResolvedValue(50);
|
|
136
|
+
|
|
137
|
+
// When
|
|
138
|
+
const [messages, totalCount] = await Promise.all([
|
|
139
|
+
mockSessionComponent.getMessages(sessionId, { offset: 50, limit: 10 }),
|
|
140
|
+
mockSessionComponent.getMessageCount(sessionId),
|
|
141
|
+
]);
|
|
142
|
+
|
|
143
|
+
// Then
|
|
144
|
+
expect(messages).toHaveLength(0);
|
|
145
|
+
expect(totalCount).toBe(50);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe("MessagesCommand JSON Output", () => {
|
|
150
|
+
test("should include total count in JSON output", () => {
|
|
151
|
+
// Given: JSON 响应应该包含总数
|
|
152
|
+
const jsonResponse = {
|
|
153
|
+
sessionId: "test-session",
|
|
154
|
+
messages: [
|
|
155
|
+
{
|
|
156
|
+
id: "msg-1",
|
|
157
|
+
role: "user",
|
|
158
|
+
content: "Hello",
|
|
159
|
+
timestamp: new Date().toISOString(),
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
total: 100, // 消息总数
|
|
163
|
+
pagination: {
|
|
164
|
+
offset: 0,
|
|
165
|
+
limit: 10,
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// Then: 应该包含 total 字段
|
|
170
|
+
expect(jsonResponse).toHaveProperty("total");
|
|
171
|
+
expect(jsonResponse.total).toBe(100);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("should include pagination info in JSON output", () => {
|
|
175
|
+
// Given
|
|
176
|
+
const jsonResponse = {
|
|
177
|
+
sessionId: "test-session",
|
|
178
|
+
messages: [],
|
|
179
|
+
total: 100,
|
|
180
|
+
pagination: {
|
|
181
|
+
offset: 10,
|
|
182
|
+
limit: 20,
|
|
183
|
+
hasMore: true, // 是否有更多消息
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// Then
|
|
188
|
+
expect(jsonResponse.pagination).toBeDefined();
|
|
189
|
+
expect(jsonResponse.pagination.offset).toBe(10);
|
|
190
|
+
expect(jsonResponse.pagination.limit).toBe(20);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe("MessagesCommand CLI Options", () => {
|
|
195
|
+
test("offset should be required in CLI builder", () => {
|
|
196
|
+
// Given: CLI 参数定义
|
|
197
|
+
const expectedOptions = {
|
|
198
|
+
offset: { alias: "o", type: "number", demandOption: true },
|
|
199
|
+
limit: { alias: "n", type: "number", demandOption: true },
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// Then: offset 和 limit 应该是必填的 (demandOption: true)
|
|
203
|
+
expect(expectedOptions.offset.demandOption).toBe(true);
|
|
204
|
+
expect(expectedOptions.limit.demandOption).toBe(true);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("should have default values for optional parameters", () => {
|
|
208
|
+
// Note: 虽然 offset 和 limit 是必填的,但 reverse 和 json 有默认值
|
|
209
|
+
const cliOptions = {
|
|
210
|
+
offset: { alias: "o", type: "number", demandOption: true },
|
|
211
|
+
limit: { alias: "n", type: "number", demandOption: true },
|
|
212
|
+
reverse: { alias: "r", type: "boolean", default: false },
|
|
213
|
+
json: { alias: "j", type: "boolean", default: false },
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
expect(cliOptions.reverse.default).toBe(false);
|
|
217
|
+
expect(cliOptions.json.default).toBe(false);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe("MessagesCommand Format Message Content", () => {
|
|
222
|
+
test("should handle tool-call with undefined arguments without error", () => {
|
|
223
|
+
// Given: tool-call part 的 arguments 是 undefined
|
|
224
|
+
const mockMessage = {
|
|
225
|
+
id: "msg-1",
|
|
226
|
+
role: "assistant",
|
|
227
|
+
content: "",
|
|
228
|
+
timestamp: Date.now(),
|
|
229
|
+
parts: [
|
|
230
|
+
{ type: "tool-call", toolCallId: "call_1", toolName: "bash", arguments: undefined, state: "pending" },
|
|
231
|
+
],
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// Then: 应该能安全处理 undefined arguments
|
|
235
|
+
const formatMessageContent = (m: any): string[] => {
|
|
236
|
+
const lines: string[] = [];
|
|
237
|
+
if (m.parts && m.parts.length > 0) {
|
|
238
|
+
for (const part of m.parts) {
|
|
239
|
+
if (part.type === "tool-call") {
|
|
240
|
+
lines.push(`[Tool Call] ${part.toolName}`);
|
|
241
|
+
// 修复后的代码逻辑
|
|
242
|
+
const argsStr = typeof part.arguments === 'string'
|
|
243
|
+
? part.arguments
|
|
244
|
+
: JSON.stringify(part.arguments ?? null, null, 2);
|
|
245
|
+
if (argsStr && argsStr.length > 200) {
|
|
246
|
+
lines.push(...argsStr.slice(0, 197).split("\n").map(() => "line"));
|
|
247
|
+
lines.push("...");
|
|
248
|
+
} else if (argsStr) {
|
|
249
|
+
lines.push(...argsStr.split("\n").map(() => "line"));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return lines;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// Should not throw
|
|
258
|
+
expect(() => formatMessageContent(mockMessage)).not.toThrow();
|
|
259
|
+
const result = formatMessageContent(mockMessage);
|
|
260
|
+
expect(result).toContain("[Tool Call] bash");
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
describe("MessagesCommand getMessageCount Integration", () => {
|
|
265
|
+
let mockSessionComponent: {
|
|
266
|
+
getMessageCount: ReturnType<typeof vi.fn>;
|
|
267
|
+
getMessages: ReturnType<typeof vi.fn>;
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
beforeEach(() => {
|
|
271
|
+
vi.clearAllMocks();
|
|
272
|
+
|
|
273
|
+
mockSessionComponent = {
|
|
274
|
+
getMessageCount: vi.fn(),
|
|
275
|
+
getMessages: vi.fn(),
|
|
276
|
+
};
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("should use getMessageCount to get total messages", async () => {
|
|
280
|
+
// Given
|
|
281
|
+
mockSessionComponent.getMessageCount.mockResolvedValue(150);
|
|
282
|
+
|
|
283
|
+
// When
|
|
284
|
+
const total = await mockSessionComponent.getMessageCount("session-123");
|
|
285
|
+
|
|
286
|
+
// Then
|
|
287
|
+
expect(total).toBe(150);
|
|
288
|
+
expect(mockSessionComponent.getMessageCount).toHaveBeenCalledWith("session-123");
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("should calculate hasMore based on offset, limit and total", async () => {
|
|
292
|
+
// Given
|
|
293
|
+
const offset = 90;
|
|
294
|
+
const limit = 20;
|
|
295
|
+
const total = 100;
|
|
296
|
+
|
|
297
|
+
// When: 计算是否还有更多消息
|
|
298
|
+
// offset + limit < total 表示还有下一页
|
|
299
|
+
const hasMore = offset + limit < total;
|
|
300
|
+
|
|
301
|
+
// Then: offset=90, limit=20, total=100
|
|
302
|
+
// offset + limit = 110, 110 < 100 = false
|
|
303
|
+
expect(hasMore).toBe(false);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test("should correctly determine hasMore when at last page", async () => {
|
|
307
|
+
// Given
|
|
308
|
+
const offset = 90;
|
|
309
|
+
const limit = 10;
|
|
310
|
+
const total = 100;
|
|
311
|
+
|
|
312
|
+
// When
|
|
313
|
+
const hasMore = offset + limit < total;
|
|
314
|
+
|
|
315
|
+
// Then: offset=90, limit=10, total=100
|
|
316
|
+
// offset + limit = 100, 100 < 100 = false
|
|
317
|
+
expect(hasMore).toBe(false);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("should correctly determine hasMore when more pages exist", async () => {
|
|
321
|
+
// Given
|
|
322
|
+
const offset = 0;
|
|
323
|
+
const limit = 10;
|
|
324
|
+
const total = 100;
|
|
325
|
+
|
|
326
|
+
// When
|
|
327
|
+
const hasMore = offset + limit < total;
|
|
328
|
+
|
|
329
|
+
// Then: offset=0, limit=10, total=100
|
|
330
|
+
// 0 + 10 = 10, 10 < 100 = true
|
|
331
|
+
expect(hasMore).toBe(true);
|
|
332
|
+
});
|
|
333
|
+
});
|