@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,211 @@
1
+ /**
2
+ * @fileoverview Debug Span Command
3
+ *
4
+ * Command: roy debug span <spanId>
5
+ *
6
+ * 通过 spanId 从 SQLite 中查询详细的 span 信息
7
+ */
8
+
9
+ import * as path from "path";
10
+ import * as os from "os";
11
+ import type { CommandModule } from "yargs";
12
+ import { SQLiteSpanStorage } from "@ai-setting/roy-agent-core";
13
+ import type { Span } from "@ai-setting/roy-agent-core";
14
+
15
+ interface SpanOptions {
16
+ spanId?: string;
17
+ db?: string;
18
+ }
19
+
20
+ export const SpanCommand: CommandModule<object, SpanOptions> = {
21
+ command: "span",
22
+ describe: "通过 spanId 查询详细的 span 信息",
23
+
24
+ builder: (yargs) =>
25
+ yargs
26
+ .positional("spanId", {
27
+ type: "string",
28
+ describe: "Span ID",
29
+ demandOption: true,
30
+ })
31
+ .option("db", {
32
+ type: "string",
33
+ describe: "SQLite 数据库路径(可选,默认使用配置路径)",
34
+ }),
35
+
36
+ async handler(args) {
37
+ const spanId = args._[2] as string || args.spanId;
38
+
39
+ if (!spanId) {
40
+ console.error("Error: spanId is required");
41
+ console.error("Usage: roy debug span <spanId>");
42
+ process.exit(1);
43
+ }
44
+
45
+ // 获取数据库路径
46
+ const dbPath = args.db || getDefaultSpanDbPath();
47
+
48
+ // 创建存储实例并读取数据
49
+ const storage = new SQLiteSpanStorage(dbPath);
50
+ await storage.initialize();
51
+
52
+ try {
53
+ // 查询 span
54
+ const span = storage.findBySpanId(spanId);
55
+
56
+ if (!span) {
57
+ console.log(`Span ${spanId} not found.`);
58
+ return;
59
+ }
60
+
61
+ // 格式化输出详细信息
62
+ const output = formatSpanDetail(span);
63
+ console.log(output);
64
+ } finally {
65
+ storage.close();
66
+ }
67
+ },
68
+ };
69
+
70
+ /**
71
+ * 获取默认的 span 数据库路径
72
+ */
73
+ export function getDefaultSpanDbPath(): string {
74
+ // 支持环境变量覆盖
75
+ if (process.env.LOG_TRACE_TRACING_DB_PATH) {
76
+ return process.env.LOG_TRACE_TRACING_DB_PATH;
77
+ }
78
+
79
+ // 使用 XDG 标准路径
80
+ const home = os.homedir();
81
+ const dataHome = process.env.XDG_DATA_HOME || path.join(home, ".local", "share");
82
+ return path.join(dataHome, "roy-agent", "traces.db");
83
+ }
84
+
85
+ /**
86
+ * 安全序列化对象,处理循环引用和其他异常
87
+ */
88
+ function safeStringify(obj: unknown, indent: number = 2): string {
89
+ // 处理非对象类型
90
+ if (obj === null) return "null";
91
+ if (obj === undefined) return "undefined";
92
+ if (typeof obj !== "object") return String(obj);
93
+
94
+ // 如果已经是字符串,可能是已经 stringify 过的
95
+ if (typeof obj === "string") {
96
+ // 尝试检测是否是 JSON 字符串
97
+ try {
98
+ const parsed = JSON.parse(obj);
99
+ return safeStringify(parsed, indent);
100
+ } catch {
101
+ return obj;
102
+ }
103
+ }
104
+
105
+ // 处理数组
106
+ if (Array.isArray(obj)) {
107
+ try {
108
+ const items = obj.map(item => safeStringify(item, 0));
109
+ return `[${items.join(", ")}]`;
110
+ } catch {
111
+ return "[Array - cannot stringify]";
112
+ }
113
+ }
114
+
115
+ // 使用 WeakSet 检测循环引用
116
+ const seen = new WeakSet<object>();
117
+
118
+ const stringify = (value: unknown, depth: number): string => {
119
+ if (value === null) return "null";
120
+ if (value === undefined) return "undefined";
121
+ if (typeof value !== "object") return JSON.stringify(value);
122
+
123
+ if (typeof value === "string") {
124
+ // 可能是已经 stringify 过的 JSON 字符串
125
+ try {
126
+ const parsed = JSON.parse(value);
127
+ return JSON.stringify(parsed);
128
+ } catch {
129
+ return JSON.stringify(value);
130
+ }
131
+ }
132
+
133
+ if (Array.isArray(value)) {
134
+ if (seen.has(value)) return '"[Circular]"';
135
+ seen.add(value);
136
+ const items = value.map(item => stringify(item, depth + 1));
137
+ return `[${items.join(", ")}]`;
138
+ }
139
+
140
+ if (seen.has(value)) return '"[Circular]"';
141
+ seen.add(value);
142
+
143
+ const entries = Object.entries(value as Record<string, unknown>);
144
+ const props = entries.map(([k, v]) => `"${k}": ${stringify(v, depth + 1)}`);
145
+ return `{${props.join(", ")}}`;
146
+ };
147
+
148
+ try {
149
+ return stringify(obj, 0);
150
+ } catch (e) {
151
+ return `[Object - cannot stringify: ${e instanceof Error ? e.message : String(e)}]`;
152
+ }
153
+ }
154
+
155
+ /**
156
+ * 格式化 span 为详细输出
157
+ */
158
+ export function formatSpanDetail(span: Span): string {
159
+ const lines: string[] = [];
160
+
161
+ // 基本信息
162
+ lines.push(`Span ID: ${span.spanId}`);
163
+ lines.push(`Trace ID: ${span.traceId}`);
164
+ lines.push(`Name: ${span.name}`);
165
+ lines.push(`Kind: ${span.kind}`);
166
+ lines.push(`Status: ${span.status}`);
167
+
168
+ // 时间信息
169
+ const duration = span.endTime
170
+ ? `${span.endTime - span.startTime}ms`
171
+ : "(running)";
172
+ lines.push(`Duration: ${duration}`);
173
+ lines.push(`Start: ${span.startTime} (${new Date(span.startTime).toISOString()})`);
174
+ if (span.endTime) {
175
+ lines.push(`End: ${span.endTime} (${new Date(span.endTime).toISOString()})`);
176
+ }
177
+
178
+ // 父子关系
179
+ if (span.parentSpanId) {
180
+ lines.push(`Parent: ${span.parentSpanId}`);
181
+ }
182
+
183
+ // 错误信息
184
+ if (span.error) {
185
+ lines.push(`Error: ${span.error}`);
186
+ }
187
+
188
+ // 属性
189
+ if (span.attributes && Object.keys(span.attributes).length > 0) {
190
+ lines.push("");
191
+ lines.push("Attributes:");
192
+ for (const [key, value] of Object.entries(span.attributes)) {
193
+ lines.push(` ${key}: ${safeStringify(value, 0)}`);
194
+ }
195
+ }
196
+
197
+ // 结果
198
+ if (span.result !== undefined) {
199
+ lines.push("");
200
+ lines.push("Result:");
201
+ // 如果是字符串,尝试解析为 JSON 显示;如果是已损坏的数据,显示提示
202
+ const resultStr = typeof span.result === "string"
203
+ ? (span.result.includes("[Object with circular reference")
204
+ ? "[Circular reference - original data contained circular structures]"
205
+ : span.result)
206
+ : safeStringify(span.result);
207
+ lines.push(` ${resultStr}`);
208
+ }
209
+
210
+ return lines.join("\n");
211
+ }
@@ -0,0 +1,254 @@
1
+ /**
2
+ * @fileoverview Debug Trace Command - TDD Tests
3
+ *
4
+ * TDD 开发流程:
5
+ * 1. RED - 编写失败的测试
6
+ * 2. GREEN - 编写最小代码让测试通过
7
+ * 3. REFACTOR - 重构
8
+ *
9
+ * 测试目标: debug trace <traceId>
10
+ * 功能: 以 tree 方式展示 traceId 的 spans
11
+ */
12
+
13
+ import { describe, expect, test, beforeEach, afterEach } from "bun:test";
14
+ import type { Span } from "@ai-setting/roy-agent-core";
15
+ import { SpanKind, SpanStatus } from "@ai-setting/roy-agent-core";
16
+
17
+ describe("Debug Trace Command - TDD", () => {
18
+ describe("RED: getDefaultSpanDbPath()", () => {
19
+ test("should return path in XDG_DATA_HOME/roy-agent/traces.db by default", async () => {
20
+ const { getDefaultSpanDbPath } = await import("./trace");
21
+
22
+ // Clear environment variables
23
+ delete process.env.LOG_TRACE_TRACING_DB_PATH;
24
+
25
+ const dbPath = getDefaultSpanDbPath();
26
+
27
+ expect(dbPath).toContain("roy-agent");
28
+ expect(dbPath).toContain("traces.db");
29
+ });
30
+
31
+ test("should use LOG_TRACE_TRACING_DB_PATH environment variable when set", async () => {
32
+ const { getDefaultSpanDbPath } = await import("./trace");
33
+
34
+ const customPath = "/custom/path/my-traces.db";
35
+ process.env.LOG_TRACE_TRACING_DB_PATH = customPath;
36
+
37
+ const dbPath = getDefaultSpanDbPath();
38
+
39
+ expect(dbPath).toBe(customPath);
40
+
41
+ delete process.env.LOG_TRACE_TRACING_DB_PATH;
42
+ });
43
+ });
44
+
45
+ describe("RED: formatSpanTree()", () => {
46
+ test("should format single root span", async () => {
47
+ const { formatSpanTree } = await import("./trace");
48
+
49
+ const spans: Span[] = [{
50
+ traceId: "trace_1",
51
+ spanId: "span_1",
52
+ name: "root",
53
+ kind: SpanKind.INTERNAL,
54
+ status: SpanStatus.OK,
55
+ startTime: 1000,
56
+ endTime: 2000,
57
+ attributes: {},
58
+ children: [],
59
+ }];
60
+
61
+ const output = formatSpanTree(spans);
62
+
63
+ expect(output).toContain("root");
64
+ expect(output).toContain("1000ms"); // duration
65
+ });
66
+
67
+ test("should format span with children showing indentation", async () => {
68
+ const { formatSpanTree } = await import("./trace");
69
+
70
+ // spans 是已经构建好的树结构(根节点数组,每个节点有 children)
71
+ const spans: Span[] = [
72
+ {
73
+ traceId: "trace_1",
74
+ spanId: "span_1",
75
+ name: "root",
76
+ kind: SpanKind.INTERNAL,
77
+ status: SpanStatus.OK,
78
+ startTime: 1000,
79
+ endTime: 2000,
80
+ attributes: {},
81
+ children: [
82
+ {
83
+ traceId: "trace_1",
84
+ spanId: "span_2",
85
+ parentSpanId: "span_1",
86
+ name: "child",
87
+ kind: SpanKind.INTERNAL,
88
+ status: SpanStatus.OK,
89
+ startTime: 1200,
90
+ endTime: 1500,
91
+ attributes: {},
92
+ children: [],
93
+ },
94
+ ],
95
+ },
96
+ ];
97
+
98
+ const output = formatSpanTree(spans);
99
+
100
+ expect(output).toContain("root");
101
+ expect(output).toContain("child");
102
+ // Should show child under parent with indentation indicator
103
+ expect(output).toContain("└──") || expect(output).toContain("child");
104
+ });
105
+
106
+ test("should calculate duration correctly", async () => {
107
+ const { formatSpanTree } = await import("./trace");
108
+
109
+ const spans: Span[] = [{
110
+ traceId: "trace_1",
111
+ spanId: "span_1",
112
+ name: "operation",
113
+ kind: SpanKind.INTERNAL,
114
+ status: SpanStatus.OK,
115
+ startTime: 1000,
116
+ endTime: 2500, // 1500ms duration
117
+ attributes: {},
118
+ children: [],
119
+ }];
120
+
121
+ const output = formatSpanTree(spans);
122
+
123
+ expect(output).toContain("1500ms");
124
+ });
125
+
126
+ test("should handle span without endTime (running)", async () => {
127
+ const { formatSpanTree } = await import("./trace");
128
+
129
+ const spans: Span[] = [{
130
+ traceId: "trace_1",
131
+ spanId: "span_1",
132
+ name: "running-operation",
133
+ kind: SpanKind.INTERNAL,
134
+ status: SpanStatus.OK,
135
+ startTime: 1000,
136
+ endTime: undefined,
137
+ attributes: {},
138
+ children: [],
139
+ }];
140
+
141
+ const output = formatSpanTree(spans);
142
+
143
+ expect(output).toContain("running-operation");
144
+ expect(output).toContain("(running)");
145
+ });
146
+
147
+ test("should show error indicator for failed spans", async () => {
148
+ const { formatSpanTree } = await import("./trace");
149
+
150
+ const spans: Span[] = [{
151
+ traceId: "trace_1",
152
+ spanId: "span_1",
153
+ name: "failed-operation",
154
+ kind: SpanKind.INTERNAL,
155
+ status: SpanStatus.ERROR,
156
+ startTime: 1000,
157
+ endTime: 2000,
158
+ attributes: {},
159
+ error: "Something went wrong",
160
+ children: [],
161
+ }];
162
+
163
+ const output = formatSpanTree(spans);
164
+
165
+ expect(output).toContain("failed-operation");
166
+ expect(output).toContain("Something went wrong");
167
+ });
168
+
169
+ test("should handle empty spans array", async () => {
170
+ const { formatSpanTree } = await import("./trace");
171
+
172
+ const output = formatSpanTree([]);
173
+
174
+ expect(output).toContain("(empty)");
175
+ });
176
+
177
+ test("should show spanId in output", async () => {
178
+ const { formatSpanTree } = await import("./trace");
179
+
180
+ const spans: Span[] = [{
181
+ traceId: "trace_1",
182
+ spanId: "span_1",
183
+ name: "operation",
184
+ kind: SpanKind.INTERNAL,
185
+ status: SpanStatus.OK,
186
+ startTime: 1000,
187
+ endTime: 2000,
188
+ attributes: { "http.method": "GET", "http.url": "/api/test" },
189
+ children: [],
190
+ }];
191
+
192
+ const output = formatSpanTree(spans);
193
+
194
+ // Should show spanId
195
+ expect(output).toContain("[spanId:span_1]");
196
+ // Should NOT show attributes (simplified output)
197
+ expect(output).not.toContain("http.method");
198
+ expect(output).not.toContain("GET");
199
+ });
200
+ });
201
+
202
+ describe("GREEN: TraceCommand structure", () => {
203
+ test("should have correct command name 'trace'", async () => {
204
+ const { TraceCommand } = await import("./index");
205
+
206
+ expect(TraceCommand.command).toBe("trace");
207
+ });
208
+
209
+ test("should have describe text mentioning tree visualization", async () => {
210
+ const { TraceCommand } = await import("./index");
211
+
212
+ expect(TraceCommand.describe).toBeDefined();
213
+ expect(typeof TraceCommand.describe).toBe("string");
214
+ });
215
+
216
+ test("should have builder that adds traceId positional argument", async () => {
217
+ const { TraceCommand } = await import("./index");
218
+
219
+ let positionalConfig: any = null;
220
+ const mockYargs = {
221
+ positional: (name: string, config: any) => {
222
+ if (name === "traceId") {
223
+ positionalConfig = config;
224
+ }
225
+ return mockYargs;
226
+ },
227
+ option: () => mockYargs,
228
+ };
229
+
230
+ TraceCommand.builder(mockYargs as any);
231
+
232
+ expect(positionalConfig).toBeDefined();
233
+ expect(positionalConfig.type).toBe("string");
234
+ });
235
+
236
+ test("should have --db option for custom db path", async () => {
237
+ const { TraceCommand } = await import("./index");
238
+
239
+ let capturedOptions: Record<string, any> = {};
240
+ const mockYargs = {
241
+ option: (name: string, config: any) => {
242
+ capturedOptions[name] = config;
243
+ return mockYargs;
244
+ },
245
+ positional: () => mockYargs,
246
+ };
247
+
248
+ TraceCommand.builder(mockYargs as any);
249
+
250
+ expect(capturedOptions.db).toBeDefined();
251
+ expect(capturedOptions.db.type).toBe("string");
252
+ });
253
+ });
254
+ });
@@ -0,0 +1,140 @@
1
+ /**
2
+ * @fileoverview Debug Trace Command
3
+ *
4
+ * Command: roy debug trace <traceId>
5
+ *
6
+ * 以 tree 方式展示 traceId 的 spans
7
+ */
8
+
9
+ import * as path from "path";
10
+ import * as os from "os";
11
+ import type { CommandModule } from "yargs";
12
+ import { SQLiteSpanStorage } from "@ai-setting/roy-agent-core";
13
+ import type { Span } from "@ai-setting/roy-agent-core";
14
+
15
+ interface TraceOptions {
16
+ traceId?: string;
17
+ db?: string;
18
+ }
19
+
20
+ export const TraceCommand: CommandModule<object, TraceOptions> = {
21
+ command: "trace",
22
+ describe: "以 tree 方式查看 trace 的 spans",
23
+
24
+ builder: (yargs) =>
25
+ yargs
26
+ .positional("traceId", {
27
+ type: "string",
28
+ describe: "Trace ID",
29
+ demandOption: true,
30
+ })
31
+ .option("db", {
32
+ type: "string",
33
+ describe: "SQLite 数据库路径(可选,默认使用配置路径)",
34
+ }),
35
+
36
+ async handler(args) {
37
+ const traceId = args._[2] as string || args.traceId;
38
+
39
+ if (!traceId) {
40
+ console.error("Error: traceId is required");
41
+ console.error("Usage: roy debug trace <traceId>");
42
+ process.exit(1);
43
+ }
44
+
45
+ // 获取数据库路径
46
+ const dbPath = args.db || getDefaultSpanDbPath();
47
+
48
+ // 创建存储实例并读取数据
49
+ const storage = new SQLiteSpanStorage(dbPath);
50
+ await storage.initialize();
51
+
52
+ try {
53
+ // 查询 spans
54
+ const spans = storage.findByTraceId(traceId);
55
+
56
+ if (spans.length === 0) {
57
+ console.log(`Trace ${traceId} not found.`);
58
+ return;
59
+ }
60
+
61
+ // 格式化输出为 tree
62
+ // 注意:spans 已经是 buildTree() 返回的根节点数组,每个节点有 children
63
+ const treeOutput = formatSpanTree(spans);
64
+
65
+ // 分段输出到 stderr,避免被 log-trace 的 maxOutput 截断
66
+ const chunkSize = 4000;
67
+ for (let i = 0; i < treeOutput.length; i += chunkSize) {
68
+ console.error(treeOutput.substring(i, i + chunkSize));
69
+ }
70
+ } finally {
71
+ storage.close();
72
+ }
73
+ },
74
+ };
75
+
76
+ /**
77
+ * 获取默认的 span 数据库路径
78
+ */
79
+ export function getDefaultSpanDbPath(): string {
80
+ // 支持环境变量覆盖
81
+ if (process.env.LOG_TRACE_TRACING_DB_PATH) {
82
+ return process.env.LOG_TRACE_TRACING_DB_PATH;
83
+ }
84
+
85
+ // 使用 XDG 标准路径
86
+ const home = os.homedir();
87
+ const dataHome = process.env.XDG_DATA_HOME || path.join(home, ".local", "share");
88
+ return path.join(dataHome, "roy-agent", "traces.db");
89
+ }
90
+
91
+ /**
92
+ * 格式化 spans 为 tree 视图
93
+ *
94
+ * spans 是已经构建好的树结构(根节点数组,每个节点有 children)
95
+ * 直接遍历输出即可
96
+ */
97
+ export function formatSpanTree(spans: Span[]): string {
98
+ if (spans.length === 0) {
99
+ return "(empty)";
100
+ }
101
+
102
+ // spans 已经是根节点数组,直接格式化
103
+ const lines: string[] = [];
104
+
105
+ for (const root of spans) {
106
+ formatSpanNode(root, 0, lines);
107
+ }
108
+
109
+ return lines.join("\n");
110
+ }
111
+
112
+ /**
113
+ * 递归格式化单个 span
114
+ *
115
+ * 简化输出:只显示 name、duration、error、spanId
116
+ */
117
+ function formatSpanNode(span: Span, depth: number, lines: string[]): void {
118
+ const indent = " ".repeat(depth);
119
+ const connector = depth === 0 ? "" : "└── ";
120
+
121
+ // 计算耗时
122
+ const duration = span.endTime
123
+ ? `${span.endTime - span.startTime}ms`
124
+ : "(running)";
125
+
126
+ // 构建错误信息
127
+ const errorInfo = span.error
128
+ ? ` ⚠ ${span.error}`
129
+ : "";
130
+
131
+ // 添加 spanId 显示
132
+ const spanIdInfo = `[spanId:${span.spanId}]`;
133
+
134
+ lines.push(`${indent}${connector}${span.name} (${duration})${errorInfo} ${spanIdInfo}`);
135
+
136
+ // 递归处理子 span
137
+ for (const child of span.children || []) {
138
+ formatSpanNode(child, depth + 1, lines);
139
+ }
140
+ }