chatccc 0.2.4 → 0.2.6

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.
@@ -0,0 +1,258 @@
1
+ // =============================================================================
2
+ // claude-adapter.ts — Claude Agent SDK 适配器
3
+ // =============================================================================
4
+ // 实现 ToolAdapter 接口,将 @anthropic-ai/claude-agent-sdk 调用封装在内。
5
+ // =============================================================================
6
+
7
+ import {
8
+ unstable_v2_createSession,
9
+ unstable_v2_resumeSession,
10
+ getSessionInfo as sdkGetSessionInfo,
11
+ } from "@anthropic-ai/claude-agent-sdk";
12
+
13
+ import type {
14
+ ToolAdapter,
15
+ UnifiedBlock,
16
+ UnifiedStreamMessage,
17
+ CreateSessionResult,
18
+ SessionInfo,
19
+ } from "./adapter-interface.ts";
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // 类型别名:SDK 内部消息的形状(避免导入 sdk.d.ts 的大型联合类型)
23
+ // ---------------------------------------------------------------------------
24
+
25
+ interface SdkContentBlock {
26
+ type: string;
27
+ text?: string;
28
+ thinking?: string;
29
+ name?: string;
30
+ input?: unknown;
31
+ tool_use_id?: string;
32
+ content?: unknown;
33
+ is_error?: boolean;
34
+ query?: string;
35
+ [key: string]: unknown;
36
+ }
37
+
38
+ interface SdkMessageLike {
39
+ type?: string;
40
+ subtype?: string;
41
+ message?: { content?: SdkContentBlock[] };
42
+ compact_metadata?: {
43
+ trigger?: "manual" | "auto";
44
+ pre_tokens?: number;
45
+ post_tokens?: number;
46
+ };
47
+ session_id?: string;
48
+ }
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // ClaudeAdapterOptions
52
+ // ---------------------------------------------------------------------------
53
+
54
+ export interface ClaudeAdapterOptions {
55
+ model: string;
56
+ effort: string;
57
+ isDefault: (value: string) => boolean;
58
+ }
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // buildSessionOptions — 还原 claudeSdkSessionOptions 的精确行为
62
+ // ---------------------------------------------------------------------------
63
+
64
+ function buildSessionOptions(
65
+ cwd: string,
66
+ model: string,
67
+ effort: string,
68
+ isDefault: (value: string) => boolean,
69
+ ): Record<string, unknown> {
70
+ const o: Record<string, unknown> = {
71
+ cwd,
72
+ permissionMode: "bypassPermissions",
73
+ allowDangerouslySkipPermissions: true,
74
+ autoCompactEnabled: true,
75
+ settingSources: ["project", "local"],
76
+ };
77
+ if (!isDefault(model)) o.model = model;
78
+ if (!isDefault(effort)) o.effort = effort;
79
+ return o;
80
+ }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // normalizeSdkMessage — 关键映射:SDK 消息 → UnifiedStreamMessage | null
84
+ // ---------------------------------------------------------------------------
85
+
86
+ export function normalizeSdkMessage(msg: SdkMessageLike): UnifiedStreamMessage | null {
87
+ // 1) assistant / user 消息:遍历 content 块
88
+ if (
89
+ (msg.type === "assistant" || msg.type === "user") &&
90
+ msg.message?.content
91
+ ) {
92
+ const blocks: UnifiedBlock[] = [];
93
+ for (const block of msg.message.content) {
94
+ if (block.type === "thinking" && block.thinking) {
95
+ blocks.push({ type: "thinking", thinking: block.thinking });
96
+ } else if (block.type === "tool_use") {
97
+ blocks.push({
98
+ type: "tool_use",
99
+ name: block.name ?? "unknown",
100
+ input: block.input,
101
+ });
102
+ } else if (block.type === "tool_result") {
103
+ blocks.push({
104
+ type: "tool_result",
105
+ tool_use_id: block.tool_use_id ?? "",
106
+ content: block.content,
107
+ is_error: block.is_error,
108
+ });
109
+ } else if (block.type === "redacted_thinking") {
110
+ blocks.push({ type: "redacted_thinking" });
111
+ } else if (block.type === "search_result") {
112
+ blocks.push({
113
+ type: "search_result",
114
+ query: block.query ?? "",
115
+ });
116
+ } else if (block.type === "text" && block.text) {
117
+ blocks.push({ type: "text", text: block.text });
118
+ }
119
+ }
120
+ return { type: msg.type, blocks };
121
+ }
122
+
123
+ // 2) system / compact_boundary 消息:上下文压缩事件
124
+ if (msg.type === "system" && msg.subtype === "compact_boundary") {
125
+ const meta = msg.compact_metadata;
126
+ if (!meta) return null;
127
+ return {
128
+ type: "system",
129
+ blocks: [
130
+ {
131
+ type: "compact_boundary",
132
+ trigger: meta.trigger ?? "auto",
133
+ pre_tokens: meta.pre_tokens ?? 0,
134
+ post_tokens: meta.post_tokens,
135
+ },
136
+ ],
137
+ };
138
+ }
139
+
140
+ // 3) 其他消息类型:跳过
141
+ return null;
142
+ }
143
+
144
+ // ---------------------------------------------------------------------------
145
+ // 适配器实现(私有类,仅通过工厂函数暴露)
146
+ // ---------------------------------------------------------------------------
147
+
148
+ class ClaudeAdapter implements ToolAdapter {
149
+ readonly displayName = "Claude Code";
150
+ readonly sessionDescPrefix = "Claude Code Session:";
151
+ private model: string;
152
+ private effort: string;
153
+ private isDefault: (value: string) => boolean;
154
+
155
+ constructor(options: ClaudeAdapterOptions) {
156
+ this.model = options.model;
157
+ this.effort = options.effort;
158
+ this.isDefault = options.isDefault;
159
+ }
160
+
161
+ async createSession(cwd: string): Promise<CreateSessionResult> {
162
+ const sessionOpts = buildSessionOptions(
163
+ cwd,
164
+ this.model,
165
+ this.effort,
166
+ this.isDefault,
167
+ );
168
+ const session = unstable_v2_createSession(sessionOpts as any);
169
+
170
+ await session.send("ok");
171
+
172
+ const stream = session.stream();
173
+ const first = await stream.next();
174
+
175
+ if (first.done || !(first.value as SdkMessageLike)?.session_id) {
176
+ session.close();
177
+ throw new Error("No session ID in Claude init event");
178
+ }
179
+
180
+ const sessionId = (first.value as SdkMessageLike).session_id!;
181
+
182
+ // 后台消费剩余的 stream(必须:否则 SDK 内部缓冲可能阻塞)
183
+ (async () => {
184
+ try {
185
+ for await (const _msg of stream) {
186
+ // 静默消费
187
+ }
188
+ } catch {
189
+ // stream 异常不阻塞主流程
190
+ } finally {
191
+ session.close();
192
+ }
193
+ })();
194
+
195
+ return { sessionId };
196
+ }
197
+
198
+ async *prompt(
199
+ sessionId: string,
200
+ userText: string,
201
+ cwd: string,
202
+ signal?: AbortSignal,
203
+ ): AsyncIterable<UnifiedStreamMessage> {
204
+ const sessionOpts = buildSessionOptions(
205
+ cwd,
206
+ this.model,
207
+ this.effort,
208
+ this.isDefault,
209
+ );
210
+ const session = unstable_v2_resumeSession(
211
+ sessionId,
212
+ sessionOpts as any,
213
+ );
214
+
215
+ await session.send(userText);
216
+
217
+ const stream = session.stream();
218
+
219
+ try {
220
+ for await (const msg of stream) {
221
+ if (signal?.aborted) break;
222
+ const normalized = normalizeSdkMessage(
223
+ msg as unknown as SdkMessageLike,
224
+ );
225
+ if (normalized) yield normalized;
226
+ }
227
+ } finally {
228
+ session.close();
229
+ }
230
+ }
231
+
232
+ async getSessionInfo(
233
+ sessionId: string,
234
+ ): Promise<SessionInfo | undefined> {
235
+ const info = await sdkGetSessionInfo(sessionId);
236
+ if (!info) return undefined;
237
+ return {
238
+ sessionId: info.sessionId,
239
+ cwd: info.cwd,
240
+ summary: info.summary,
241
+ lastModified: info.lastModified,
242
+ };
243
+ }
244
+
245
+ async closeSession(_sessionId: string): Promise<void> {
246
+ // Claude SDK 在 stream 结束后自动关闭 session,此处为 no-op
247
+ }
248
+ }
249
+
250
+ // ---------------------------------------------------------------------------
251
+ // 工厂函数
252
+ // ---------------------------------------------------------------------------
253
+
254
+ export function createClaudeAdapter(
255
+ options: ClaudeAdapterOptions,
256
+ ): ToolAdapter {
257
+ return new ClaudeAdapter(options);
258
+ }
@@ -0,0 +1,229 @@
1
+ // =============================================================================
2
+ // cursor-adapter.ts — Cursor Agent CLI 适配器
3
+ // =============================================================================
4
+ // 通过 agent -p --output-format stream-json 与 Cursor agent 交互。
5
+ // 命令行可通过 CHATCCC_CURSOR_COMMAND / CHATCCC_CURSOR_ARGS 环境变量自定义。
6
+ // =============================================================================
7
+
8
+ import { spawn, type ChildProcess } from "node:child_process";
9
+ import { createInterface } from "node:readline";
10
+
11
+ import type {
12
+ ToolAdapter,
13
+ UnifiedBlock,
14
+ UnifiedStreamMessage,
15
+ CreateSessionResult,
16
+ SessionInfo,
17
+ } from "./adapter-interface.ts";
18
+ import { CURSOR_AGENT_COMMAND, CURSOR_AGENT_ARGS } from "../config.ts";
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // 类型:Cursor JSONL 消息行
22
+ // ---------------------------------------------------------------------------
23
+
24
+ interface CursorMessageLine {
25
+ type?: string;
26
+ subtype?: string;
27
+ session_id?: string;
28
+ cwd?: string;
29
+ model?: string;
30
+ message?: {
31
+ role?: string;
32
+ content?: CursorContentBlock[];
33
+ };
34
+ result?: string;
35
+ duration_ms?: number;
36
+ usage?: {
37
+ inputTokens?: number;
38
+ outputTokens?: number;
39
+ };
40
+ }
41
+
42
+ interface CursorContentBlock {
43
+ type?: string;
44
+ text?: string;
45
+ thinking?: string;
46
+ name?: string;
47
+ input?: unknown;
48
+ tool_use_id?: string;
49
+ content?: unknown;
50
+ is_error?: boolean;
51
+ query?: string;
52
+ [key: string]: unknown;
53
+ }
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // normalizeCursorMessage — Cursor 消息 → UnifiedStreamMessage | null
57
+ // ---------------------------------------------------------------------------
58
+
59
+ export function normalizeCursorMessage(
60
+ msg: CursorMessageLine,
61
+ ): UnifiedStreamMessage | null {
62
+ if (msg.type === "assistant" && msg.message?.content) {
63
+ const blocks: UnifiedBlock[] = [];
64
+ for (const block of msg.message.content) {
65
+ if (block.type === "thinking" && block.thinking) {
66
+ blocks.push({ type: "thinking", thinking: block.thinking });
67
+ } else if (block.type === "tool_use") {
68
+ blocks.push({
69
+ type: "tool_use",
70
+ name: block.name ?? "unknown",
71
+ input: block.input,
72
+ });
73
+ } else if (block.type === "tool_result") {
74
+ blocks.push({
75
+ type: "tool_result",
76
+ tool_use_id: block.tool_use_id ?? "",
77
+ content: block.content,
78
+ is_error: block.is_error,
79
+ });
80
+ } else if (block.type === "redacted_thinking") {
81
+ blocks.push({ type: "redacted_thinking" });
82
+ } else if (block.type === "search_result") {
83
+ blocks.push({
84
+ type: "search_result",
85
+ query: block.query ?? "",
86
+ });
87
+ } else if (block.type === "text" && block.text) {
88
+ blocks.push({ type: "text", text: block.text });
89
+ }
90
+ }
91
+ return { type: "assistant", blocks };
92
+ }
93
+
94
+ if (msg.type === "user" && msg.message?.content) {
95
+ const blocks: UnifiedBlock[] = [];
96
+ for (const block of msg.message.content) {
97
+ if (block.type === "tool_result") {
98
+ blocks.push({
99
+ type: "tool_result",
100
+ tool_use_id: block.tool_use_id ?? "",
101
+ content: block.content,
102
+ is_error: block.is_error,
103
+ });
104
+ } else if (block.type === "text" && block.text) {
105
+ blocks.push({ type: "text", text: block.text });
106
+ }
107
+ }
108
+ return { type: "user", blocks };
109
+ }
110
+
111
+ if (msg.type === "system" && msg.subtype === "compact_boundary") {
112
+ const meta = (msg as Record<string, unknown>).compact_metadata as
113
+ | { trigger?: "manual" | "auto"; pre_tokens?: number; post_tokens?: number }
114
+ | undefined;
115
+ if (!meta) return null;
116
+ return {
117
+ type: "system",
118
+ blocks: [
119
+ {
120
+ type: "compact_boundary",
121
+ trigger: meta.trigger ?? "auto",
122
+ pre_tokens: meta.pre_tokens ?? 0,
123
+ post_tokens: meta.post_tokens,
124
+ },
125
+ ],
126
+ };
127
+ }
128
+
129
+ return null;
130
+ }
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // 子进程辅助函数
134
+ // ---------------------------------------------------------------------------
135
+
136
+ function spawnAgent(
137
+ extraArgs: string[],
138
+ cwd?: string,
139
+ ): ChildProcess {
140
+ const allArgs = [...CURSOR_AGENT_ARGS, ...extraArgs];
141
+ return spawn(CURSOR_AGENT_COMMAND, allArgs, {
142
+ cwd,
143
+ stdio: ["ignore", "pipe", "pipe"],
144
+ windowsHide: true,
145
+ shell: true,
146
+ });
147
+ }
148
+
149
+ async function* readJsonLines(
150
+ proc: ChildProcess,
151
+ signal?: AbortSignal,
152
+ ): AsyncGenerator<CursorMessageLine> {
153
+ const rl = createInterface({ input: proc.stdout!, crlfDelay: Infinity });
154
+ for await (const line of rl) {
155
+ if (signal?.aborted) break;
156
+ const trimmed = line.trim();
157
+ if (!trimmed) continue;
158
+ try {
159
+ yield JSON.parse(trimmed) as CursorMessageLine;
160
+ } catch { /* 非 JSON 行静默跳过 */ }
161
+ }
162
+ }
163
+
164
+ // ---------------------------------------------------------------------------
165
+ // 适配器实现
166
+ // ---------------------------------------------------------------------------
167
+
168
+ class CursorAdapter implements ToolAdapter {
169
+ readonly displayName = "Cursor";
170
+ readonly sessionDescPrefix = "Cursor Session:";
171
+ private activeProcs = new Set<ChildProcess>();
172
+
173
+ async createSession(cwd: string): Promise<CreateSessionResult> {
174
+ const proc = spawnAgent(["ok"], cwd);
175
+ this.activeProcs.add(proc);
176
+
177
+ for await (const msg of readJsonLines(proc)) {
178
+ if (msg.type === "system" && msg.subtype === "init" && msg.session_id) {
179
+ const sessionId = msg.session_id;
180
+ this.activeProcs.delete(proc);
181
+ proc.kill();
182
+ return { sessionId };
183
+ }
184
+ }
185
+
186
+ proc.kill();
187
+ this.activeProcs.delete(proc);
188
+ throw new Error("No session ID in Cursor init event");
189
+ }
190
+
191
+ async *prompt(
192
+ sessionId: string,
193
+ userText: string,
194
+ cwd: string,
195
+ signal?: AbortSignal,
196
+ ): AsyncIterable<UnifiedStreamMessage> {
197
+ const proc = spawnAgent(["--resume", sessionId, userText], cwd);
198
+ this.activeProcs.add(proc);
199
+
200
+ try {
201
+ for await (const raw of readJsonLines(proc, signal)) {
202
+ if (signal?.aborted) break;
203
+ const normalized = normalizeCursorMessage(raw);
204
+ if (normalized) yield normalized;
205
+ }
206
+ } finally {
207
+ proc.kill();
208
+ this.activeProcs.delete(proc);
209
+ }
210
+ }
211
+
212
+ async getSessionInfo(
213
+ _sessionId: string,
214
+ ): Promise<SessionInfo | undefined> {
215
+ return { sessionId: _sessionId };
216
+ }
217
+
218
+ async closeSession(_sessionId: string): Promise<void> {
219
+ // 子进程由 prompt 的 finally 自动 kill
220
+ }
221
+ }
222
+
223
+ // ---------------------------------------------------------------------------
224
+ // 工厂函数
225
+ // ---------------------------------------------------------------------------
226
+
227
+ export function createCursorAdapter(): ToolAdapter {
228
+ return new CursorAdapter();
229
+ }
package/src/cards.ts CHANGED
@@ -114,10 +114,12 @@ export function buildHelpCard(userText: string): string {
114
114
  header: { template: "blue", title: { content: "ChatCCC", tag: "plain_text" } },
115
115
  elements: [
116
116
  { tag: "div", text: { tag: "lark_md", content: `你发送了: ${userText}` } },
117
+ { tag: "div", text: { tag: "lark_md", content: "使用 **/new**(默认 Claude Code)或 **/new claude** / **/new cursor** 创建新会话" } },
117
118
  buildButtons([
118
- { text: "新建会话(/new)", value: JSON.stringify({ cmd: "new" }), type: "primary" },
119
- { text: "重启Chat CCC(/restart)", value: JSON.stringify({ cmd: "restart" }), type: "danger" },
120
- { text: "查看/切换工作路径(/cd)", value: JSON.stringify({ cmd: "cd" }), type: "default" },
119
+ { text: "新建 Claude Code 会话(/new claude)", value: JSON.stringify({ cmd: "new" }), type: "primary" },
120
+ { text: "新建 Cursor 会话(/new cursor)", value: JSON.stringify({ cmd: "new cursor" }), type: "primary" },
121
+ { text: "重启 ChatCCC(/restart)", value: JSON.stringify({ cmd: "restart" }), type: "danger" },
122
+ { text: "查看/切换工作路径及最近使用(/cd)", value: JSON.stringify({ cmd: "cd" }), type: "default" },
121
123
  ]),
122
124
  ],
123
125
  });
@@ -159,37 +161,134 @@ export function buildCdContent(
159
161
  return lines.join("\n");
160
162
  }
161
163
 
162
- // 所有会话列表卡片
164
+ // 工作路径卡片(/cd 无参数时使用,含最近使用路径按钮)
165
+ export function buildCdCard(
166
+ dirPath: string,
167
+ entries: { name: string; isDir: boolean }[],
168
+ recentDirs: string[],
169
+ sessionCwd?: string,
170
+ maxFiles = 100,
171
+ ): string {
172
+ const display = entries.slice(0, maxFiles);
173
+ const overflow = entries.length > maxFiles ? `\n...(共 ${entries.length} 个条目,仅显示前 ${maxFiles} 个)` : "";
174
+ const listing = display.map(e => e.isDir ? `📁 ${e.name}/` : `📄 ${e.name}`).join("\n");
175
+
176
+ const currentLine = sessionCwd
177
+ ? `**当前会话工作路径:** \`${sessionCwd}\``
178
+ : "";
179
+
180
+ const elements: object[] = [];
181
+
182
+ if (currentLine) {
183
+ elements.push({ tag: "markdown", content: currentLine });
184
+ }
185
+ elements.push({ tag: "markdown", content: `**新会话默认工作路径:** \`${dirPath}\`` });
186
+
187
+ if (recentDirs.length > 0) {
188
+ elements.push({ tag: "hr" });
189
+ elements.push({ tag: "markdown", content: "**最近使用过的路径(点击切换):**" });
190
+ elements.push({
191
+ tag: "action",
192
+ actions: recentDirs.map(d => {
193
+ const name = d.split(/[\\/]/).filter(Boolean).pop() ?? d;
194
+ const label = d.length > 36 ? `...${d.slice(-33)}` : d;
195
+ return {
196
+ tag: "button",
197
+ text: { tag: "plain_text", content: label },
198
+ type: "default",
199
+ value: { action: "cd", path: d },
200
+ };
201
+ }),
202
+ });
203
+ }
204
+
205
+ elements.push({ tag: "hr" });
206
+ elements.push({
207
+ tag: "markdown",
208
+ content: [
209
+ `**目录内容** (最多 ${maxFiles} 个):`,
210
+ listing,
211
+ overflow,
212
+ ].join("\n"),
213
+ });
214
+
215
+ return JSON.stringify({
216
+ schema: "2.0",
217
+ config: { wide_screen_mode: true },
218
+ header: {
219
+ template: "blue",
220
+ title: { tag: "plain_text", content: "工作路径" },
221
+ },
222
+ body: {
223
+ direction: "vertical",
224
+ elements,
225
+ },
226
+ });
227
+ }
228
+
229
+ // 所有会话列表卡片(Claude Code 优先,然后 Cursor)
163
230
  export function buildSessionsCard(sessions: Array<{
164
231
  sessionId: string;
165
232
  active: boolean;
166
233
  turnCount: number;
167
234
  elapsedSeconds: number | null;
168
235
  model: string;
236
+ tool: string;
169
237
  }>): string {
170
- const text = sessions.length === 0
171
- ? "当前没有会话记录。"
172
- : [
173
- `共 **${sessions.length}** 个会话:`,
174
- ``,
175
- ...sessions.map((s, i) => {
176
- const status = s.active ? "🟢 活跃" : "⚪ 空闲";
177
- const shortId = s.sessionId.length > 16 ? s.sessionId.slice(0, 16) + "..." : s.sessionId;
178
- let extra = "";
179
- if (s.active && s.elapsedSeconds !== null) {
180
- const mins = Math.floor(s.elapsedSeconds / 60);
181
- const secs = s.elapsedSeconds % 60;
182
- extra = ` | 本轮: ${mins}分${secs}秒`;
183
- }
184
- return `**${i + 1}.** \`${shortId}\` ${status} | 轮数: ${s.turnCount} | ${s.model}${extra}`;
185
- }),
186
- ].join("\n");
238
+ // tool 分组排序:Claude Code 在前,Cursor 在后
239
+ const claudeCodeSessions = sessions.filter(s => s.tool !== "cursor");
240
+ const cursorSessions = sessions.filter(s => s.tool === "cursor");
241
+ const hasClaudeCode = claudeCodeSessions.length > 0;
242
+ const hasCursor = cursorSessions.length > 0;
243
+
244
+ if (sessions.length === 0) {
245
+ return JSON.stringify({
246
+ config: { wide_screen_mode: true },
247
+ header: { template: "blue", title: { content: "所有会话", tag: "plain_text" } },
248
+ elements: [
249
+ { tag: "div", text: { tag: "lark_md", content: "当前没有会话记录。\n\n使用 **/new**(默认 Claude Code)、**/new claude** 或 **/new cursor** 创建新会话。" } },
250
+ { tag: "hr" },
251
+ { tag: "action", actions: [{ tag: "button", text: { tag: "plain_text", content: "收起" }, type: "default", value: { action: "close" } }] },
252
+ ],
253
+ });
254
+ }
255
+
256
+ const formatSession = (s: typeof sessions[0], i: number) => {
257
+ const status = s.active ? "🟢 活跃" : "⚪ 空闲";
258
+ const shortId = s.sessionId.length > 16 ? s.sessionId.slice(0, 16) + "..." : s.sessionId;
259
+ let extra = "";
260
+ if (s.active && s.elapsedSeconds !== null) {
261
+ const mins = Math.floor(s.elapsedSeconds / 60);
262
+ const secs = s.elapsedSeconds % 60;
263
+ extra = ` | 本轮: ${mins}分${secs}秒`;
264
+ }
265
+ const toolLabel = s.tool === "cursor" ? "Cursor" : "Claude Code";
266
+ return `**${i + 1}.** \`${shortId}\` ${status} | 工具: ${toolLabel} | 轮数: ${s.turnCount} | ${s.model}${extra}`;
267
+ };
268
+
269
+ const lines: string[] = [`共 **${sessions.length}** 个会话:`, ""];
270
+ let idx = 0;
271
+
272
+ if (hasClaudeCode) {
273
+ lines.push("**Claude Code 会话:**", "");
274
+ for (const s of claudeCodeSessions) {
275
+ lines.push(formatSession(s, idx++));
276
+ }
277
+ }
278
+
279
+ if (hasCursor) {
280
+ if (hasClaudeCode) lines.push("", "**Cursor 会话:**", "");
281
+ else lines.push("**Cursor 会话:**", "");
282
+ for (const s of cursorSessions) {
283
+ lines.push(formatSession(s, idx++));
284
+ }
285
+ }
187
286
 
188
287
  return JSON.stringify({
189
288
  config: { wide_screen_mode: true },
190
289
  header: { template: "blue", title: { content: "所有会话", tag: "plain_text" } },
191
290
  elements: [
192
- { tag: "div", text: { tag: "lark_md", content: text } },
291
+ { tag: "div", text: { tag: "lark_md", content: lines.join("\n") } },
193
292
  { tag: "hr" },
194
293
  {
195
294
  tag: "action",