team-anya-cli 0.1.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 (163) hide show
  1. package/README.md +38 -0
  2. package/anya/prompts/execution-guides/git-delivery.md +38 -0
  3. package/anya/prompts/execution-guides/testing-and-self-heal.md +28 -0
  4. package/anya/prompts/protocols/brief-assembly.md +55 -0
  5. package/anya/prompts/protocols/report.md +175 -0
  6. package/anya/prompts/protocols/review.md +90 -0
  7. package/anya/prompts/task-claude-md.template.md +32 -0
  8. package/apps/server/dist/broker/cc-broker.js +257 -0
  9. package/apps/server/dist/cli.js +296 -0
  10. package/apps/server/dist/config.js +76 -0
  11. package/apps/server/dist/daemon.js +51 -0
  12. package/apps/server/dist/gateway/chat-sync.js +135 -0
  13. package/apps/server/dist/gateway/command-router.js +114 -0
  14. package/apps/server/dist/gateway/commands/cancel.js +32 -0
  15. package/apps/server/dist/gateway/commands/help.js +16 -0
  16. package/apps/server/dist/gateway/commands/index.js +26 -0
  17. package/apps/server/dist/gateway/commands/restart.js +34 -0
  18. package/apps/server/dist/gateway/commands/status.js +34 -0
  19. package/apps/server/dist/gateway/commands/tasks.js +33 -0
  20. package/apps/server/dist/gateway/feishu-sender.js +346 -0
  21. package/apps/server/dist/gateway/feishu-ws.js +254 -0
  22. package/apps/server/dist/gateway/http.js +994 -0
  23. package/apps/server/dist/gateway/media-downloader.js +149 -0
  24. package/apps/server/dist/gateway/message-events.js +10 -0
  25. package/apps/server/dist/gateway/message-intake.js +50 -0
  26. package/apps/server/dist/gateway/message-queue.js +104 -0
  27. package/apps/server/dist/gateway/session-reader.js +142 -0
  28. package/apps/server/dist/gateway/ws-push.js +115 -0
  29. package/apps/server/dist/loid/brain.js +104 -0
  30. package/apps/server/dist/loid/clarifier.js +162 -0
  31. package/apps/server/dist/loid/context-builder.js +413 -0
  32. package/apps/server/dist/loid/mcp-server.js +104 -0
  33. package/apps/server/dist/loid/memory-settler.js +189 -0
  34. package/apps/server/dist/loid/opportunity-manager.js +148 -0
  35. package/apps/server/dist/loid/profile-updater.js +179 -0
  36. package/apps/server/dist/loid/reporter.js +148 -0
  37. package/apps/server/dist/loid/schemas.js +117 -0
  38. package/apps/server/dist/loid/self-calibrator.js +314 -0
  39. package/apps/server/dist/loid/session-manager.js +217 -0
  40. package/apps/server/dist/loid/session.js +271 -0
  41. package/apps/server/dist/loid/worktree-manager.js +191 -0
  42. package/apps/server/dist/main.js +337 -0
  43. package/apps/server/dist/tracing/index.js +2 -0
  44. package/apps/server/dist/tracing/trace-context.js +92 -0
  45. package/apps/server/dist/types/message.js +2 -0
  46. package/apps/server/dist/yor/yor-mcp-server.js +104 -0
  47. package/apps/server/dist/yor/yor-orchestrator.js +233 -0
  48. package/apps/web/dist/assets/index-CHIT0Dya.css +1 -0
  49. package/apps/web/dist/assets/index-CJzAjoVH.js +798 -0
  50. package/apps/web/dist/index.html +13 -0
  51. package/package.json +42 -0
  52. package/packages/cc-client/dist/claude-code-backend.js +664 -0
  53. package/packages/cc-client/dist/index.js +2 -0
  54. package/packages/cc-client/package.json +11 -0
  55. package/packages/core/dist/constants.js +59 -0
  56. package/packages/core/dist/errors.js +35 -0
  57. package/packages/core/dist/index.js +7 -0
  58. package/packages/core/dist/office-init.js +97 -0
  59. package/packages/core/dist/scope/checker.js +114 -0
  60. package/packages/core/dist/scope/defaults.js +40 -0
  61. package/packages/core/dist/scope/index.js +3 -0
  62. package/packages/core/dist/state-machine.js +85 -0
  63. package/packages/core/dist/types/audit.js +12 -0
  64. package/packages/core/dist/types/backend.js +2 -0
  65. package/packages/core/dist/types/commitment.js +17 -0
  66. package/packages/core/dist/types/communication.js +18 -0
  67. package/packages/core/dist/types/index.js +8 -0
  68. package/packages/core/dist/types/opportunity.js +27 -0
  69. package/packages/core/dist/types/org.js +26 -0
  70. package/packages/core/dist/types/task.js +46 -0
  71. package/packages/core/package.json +10 -0
  72. package/packages/db/dist/client.js +69 -0
  73. package/packages/db/dist/index.js +603 -0
  74. package/packages/db/dist/schema/audit-events.js +13 -0
  75. package/packages/db/dist/schema/cc-sessions.js +14 -0
  76. package/packages/db/dist/schema/chats.js +33 -0
  77. package/packages/db/dist/schema/commitments.js +18 -0
  78. package/packages/db/dist/schema/communication-events.js +14 -0
  79. package/packages/db/dist/schema/index.js +12 -0
  80. package/packages/db/dist/schema/message-log.js +20 -0
  81. package/packages/db/dist/schema/opportunities.js +23 -0
  82. package/packages/db/dist/schema/org.js +36 -0
  83. package/packages/db/dist/schema/projects.js +23 -0
  84. package/packages/db/dist/schema/tasks.js +46 -0
  85. package/packages/db/dist/schema/trace-spans.js +19 -0
  86. package/packages/db/package.json +12 -0
  87. package/packages/db/src/migrations/0000_simple_magneto.sql +148 -0
  88. package/packages/db/src/migrations/0001_nifty_morph.sql +42 -0
  89. package/packages/db/src/migrations/0002_common_joshua_kane.sql +20 -0
  90. package/packages/db/src/migrations/0003_add_cc_sessions.sql +13 -0
  91. package/packages/db/src/migrations/0004_jittery_triathlon.sql +1 -0
  92. package/packages/db/src/migrations/meta/0000_snapshot.json +987 -0
  93. package/packages/db/src/migrations/meta/0001_snapshot.json +1280 -0
  94. package/packages/db/src/migrations/meta/0002_snapshot.json +1417 -0
  95. package/packages/db/src/migrations/meta/0004_snapshot.json +1505 -0
  96. package/packages/db/src/migrations/meta/_journal.json +41 -0
  97. package/packages/mcp-tools/dist/index.js +41 -0
  98. package/packages/mcp-tools/dist/layer1/audit-append.js +38 -0
  99. package/packages/mcp-tools/dist/layer1/audit-query.js +51 -0
  100. package/packages/mcp-tools/dist/layer1/memory-brief.js +168 -0
  101. package/packages/mcp-tools/dist/layer1/memory-context.js +124 -0
  102. package/packages/mcp-tools/dist/layer1/memory-digest.js +126 -0
  103. package/packages/mcp-tools/dist/layer1/memory-forget.js +108 -0
  104. package/packages/mcp-tools/dist/layer1/memory-learn.js +63 -0
  105. package/packages/mcp-tools/dist/layer1/memory-recall.js +287 -0
  106. package/packages/mcp-tools/dist/layer1/memory-reflect.js +80 -0
  107. package/packages/mcp-tools/dist/layer1/memory-remember.js +119 -0
  108. package/packages/mcp-tools/dist/layer1/memory-search.js +263 -0
  109. package/packages/mcp-tools/dist/layer1/memory-write.js +21 -0
  110. package/packages/mcp-tools/dist/layer1/org-lookup.js +47 -0
  111. package/packages/mcp-tools/dist/layer1/project-get.js +28 -0
  112. package/packages/mcp-tools/dist/layer1/project-list.js +20 -0
  113. package/packages/mcp-tools/dist/layer1/report-daily.js +68 -0
  114. package/packages/mcp-tools/dist/layer1/task-get.js +29 -0
  115. package/packages/mcp-tools/dist/layer1/task-update.js +34 -0
  116. package/packages/mcp-tools/dist/layer2/loid/decision-log.js +15 -0
  117. package/packages/mcp-tools/dist/layer2/loid/decision-no-action.js +15 -0
  118. package/packages/mcp-tools/dist/layer2/loid/delivery-create-pr.js +30 -0
  119. package/packages/mcp-tools/dist/layer2/loid/delivery-share.js +12 -0
  120. package/packages/mcp-tools/dist/layer2/loid/delivery-submit.js +77 -0
  121. package/packages/mcp-tools/dist/layer2/loid/delivery-upload.js +18 -0
  122. package/packages/mcp-tools/dist/layer2/loid/project-remove.js +16 -0
  123. package/packages/mcp-tools/dist/layer2/loid/project-upsert.js +33 -0
  124. package/packages/mcp-tools/dist/layer2/loid/task-dispatch.js +177 -0
  125. package/packages/mcp-tools/dist/layer2/loid/task-lookup.js +38 -0
  126. package/packages/mcp-tools/dist/layer2/loid/yor-approve.js +8 -0
  127. package/packages/mcp-tools/dist/layer2/loid/yor-kill.js +7 -0
  128. package/packages/mcp-tools/dist/layer2/loid/yor-rework.js +7 -0
  129. package/packages/mcp-tools/dist/layer2/loid/yor-spawn.js +15 -0
  130. package/packages/mcp-tools/dist/layer2/loid/yor-status.js +8 -0
  131. package/packages/mcp-tools/dist/layer2/yor/task-block.js +11 -0
  132. package/packages/mcp-tools/dist/layer2/yor/task-deliver.js +35 -0
  133. package/packages/mcp-tools/dist/layer2/yor/task-progress.js +21 -0
  134. package/packages/mcp-tools/dist/layer3/adapters/feishu-adapter.js +191 -0
  135. package/packages/mcp-tools/dist/layer3/adapters/types.js +28 -0
  136. package/packages/mcp-tools/dist/layer3/channel-receive.js +11 -0
  137. package/packages/mcp-tools/dist/layer3/channel-send.js +90 -0
  138. package/packages/mcp-tools/dist/layer3/file-upload.js +44 -0
  139. package/packages/mcp-tools/dist/registry.js +779 -0
  140. package/packages/mcp-tools/package.json +13 -0
  141. package/workspace/.claude/settings.local.json +9 -0
  142. package/workspace/.mcp.json +12 -0
  143. package/workspace/CHARTER.md +73 -0
  144. package/workspace/CLAUDE.md +49 -0
  145. package/workspace/PROTOCOL.md +126 -0
  146. package/workspace/TOOLS.md +464 -0
  147. package/workspace/audit/.gitkeep +0 -0
  148. package/workspace/loid/CLAUDE.md +12 -0
  149. package/workspace/loid/PLAYBOOK.md +198 -0
  150. package/workspace/loid/PROFILE.md +78 -0
  151. package/workspace/memory/commitments/.gitkeep +0 -0
  152. package/workspace/memory/execution/.gitkeep +0 -0
  153. package/workspace/memory/people/.gitkeep +0 -0
  154. package/workspace/memory/projects/.gitkeep +0 -0
  155. package/workspace/memory/self/.gitkeep +0 -0
  156. package/workspace/reference/identity/.gitkeep +0 -0
  157. package/workspace/reference/org/escalation.yaml +24 -0
  158. package/workspace/reference/org/ownership.yaml +28 -0
  159. package/workspace/reports/.gitkeep +0 -0
  160. package/workspace/yor/CLAUDE.md +22 -0
  161. package/workspace/yor/PLAYBOOK.md +73 -0
  162. package/workspace/yor/PROFILE.md +52 -0
  163. package/workspace/yor/SELF-HEAL.md +39 -0
@@ -0,0 +1,664 @@
1
+ import { spawn, execSync } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { readFileSync, writeFileSync, appendFileSync, unlinkSync, existsSync, mkdirSync, } from "node:fs";
4
+ import { createServer } from "node:http";
5
+ import { join } from "node:path";
6
+ import { WebSocketServer, WebSocket } from "ws";
7
+ import { ConnectionState } from "@team-anya/core";
8
+ export class ClaudeCodeBackend {
9
+ proc = null;
10
+ ws = null;
11
+ wsServer = null;
12
+ httpServer = null;
13
+ cliSessionId = null;
14
+ config = null;
15
+ verbose;
16
+ // 事件回调
17
+ toolRequestHandler = null;
18
+ resultHandler = null;
19
+ progressHandler = null;
20
+ stateChangeHandler = null;
21
+ sessionIdHandler = null;
22
+ // system/init 信息(异步更新,供外部查询)
23
+ initMessage = null;
24
+ // 消息队列:CLI WS 未连接时暂存待发消息
25
+ pendingMessages = [];
26
+ // .mcp.json 管理:记录原始内容用于恢复
27
+ mcpJsonPath = null;
28
+ mcpJsonBackup = null; // null = 文件原本不存在
29
+ mcpJsonCreated = false;
30
+ // 原始对话日志:收集每次 prompt 执行期间的所有 NDJSON 消息
31
+ rawConversationLog = [];
32
+ // 日志文件路径
33
+ logFile = null;
34
+ constructor(options) {
35
+ this.verbose = options?.verbose ?? false;
36
+ this.logFile = options?.logFile ?? null;
37
+ }
38
+ /**
39
+ * 日志输出:
40
+ * - error: 始终输出到控制台 stderr
41
+ * - warn/info: 如果配置了 logFile 则写文件,否则输出到控制台
42
+ */
43
+ log(level, ...args) {
44
+ if (level === "info" && !this.verbose)
45
+ return;
46
+ const ts = new Date().toISOString();
47
+ const prefix = `[cc-backend ${ts}]`;
48
+ if (level === "error") {
49
+ console.error(prefix, ...args);
50
+ this.appendToLogFile(prefix, ...args);
51
+ }
52
+ else if (this.logFile) {
53
+ // warn/info 日志写入文件,不输出到控制台
54
+ this.appendToLogFile(prefix, ...args);
55
+ }
56
+ else if (level === "warn") {
57
+ console.warn(prefix, ...args);
58
+ }
59
+ else {
60
+ console.log(prefix, ...args);
61
+ }
62
+ }
63
+ appendToLogFile(...parts) {
64
+ if (!this.logFile)
65
+ return;
66
+ try {
67
+ const line = parts.map(p => typeof p === 'string' ? p : JSON.stringify(p)).join(' ');
68
+ appendFileSync(this.logFile, line + '\n');
69
+ }
70
+ catch {
71
+ // 日志写入失败不影响主流程
72
+ }
73
+ }
74
+ async connect(config) {
75
+ this.config = config;
76
+ this.initMessage = null;
77
+ this.cliSessionId = null;
78
+ this.pendingMessages = [];
79
+ this.stateChangeHandler?.(ConnectionState.CONNECTING);
80
+ this.log("info", "开始连接, binary:", config.binary ?? "claude", "workingDir:", config.workingDir);
81
+ const port = await this.findFreePort();
82
+ this.log("info", `WS 服务监听端口: ${port}`);
83
+ // 1. 启动 WebSocket Server,等待 CLI 反连
84
+ const wsReady = new Promise((resolve, reject) => {
85
+ this.httpServer = createServer();
86
+ this.wsServer = new WebSocketServer({ server: this.httpServer });
87
+ this.wsServer.on("connection", (ws, req) => {
88
+ this.log("warn", `CLI WebSocket 已连接, url=${req.url}, headers=${JSON.stringify(req.headers)}`);
89
+ this.ws = ws;
90
+ this.setupMessageHandler(ws);
91
+ resolve(ws);
92
+ });
93
+ this.httpServer.on("listening", () => {
94
+ this.log("warn", `HTTP server 已启动, 监听 127.0.0.1:${port}`);
95
+ });
96
+ this.httpServer.on("request", (req) => {
97
+ this.log("warn", `[HTTP] 收到请求: ${req.method} ${req.url}`);
98
+ });
99
+ this.httpServer.on("upgrade", (req) => {
100
+ this.log("warn", `[HTTP] 收到 upgrade 请求: ${req.url}, headers=${JSON.stringify(req.headers)}`);
101
+ });
102
+ this.httpServer.listen(port, "127.0.0.1");
103
+ this.httpServer.on("error", (err) => {
104
+ this.log("error", "HTTP server 错误:", err.message);
105
+ reject(err);
106
+ });
107
+ });
108
+ // 2. 启动 Claude Code CLI 子进程
109
+ const sessionId = randomUUID();
110
+ const sdkUrl = `ws://127.0.0.1:${port}/ws/cli/${sessionId}`;
111
+ const args = [
112
+ "--sdk-url",
113
+ sdkUrl,
114
+ "--print",
115
+ "--output-format",
116
+ "stream-json",
117
+ "--input-format",
118
+ "stream-json",
119
+ "--verbose",
120
+ ];
121
+ if (config.model)
122
+ args.push("--model", config.model);
123
+ if (config.resumeSessionId)
124
+ args.push("--resume", config.resumeSessionId);
125
+ if (config.systemPrompt)
126
+ args.push("--system-prompt", config.systemPrompt);
127
+ if (config.maxTurns)
128
+ args.push("--max-turns", String(config.maxTurns));
129
+ if (config.dangerouslySkipPermissions) {
130
+ args.push("--dangerously-skip-permissions");
131
+ }
132
+ else if (config.permissionMode) {
133
+ args.push("--permission-mode", config.permissionMode);
134
+ }
135
+ args.push("-p", ""); // headless 模式占位(参照 companion)
136
+ // MCP servers:写入 .mcp.json 到 workingDir(CLI 从 cwd 读取项目级配置)
137
+ if (config.mcpServers && config.mcpServers.length > 0) {
138
+ this.writeMcpJson(config.workingDir, config.mcpServers);
139
+ }
140
+ // 参照 companion: 用 which 解析 CLI 绝对路径
141
+ let binary = config.binary ?? "claude";
142
+ if (!binary.startsWith("/")) {
143
+ try {
144
+ binary = execSync(`which ${binary}`, { encoding: "utf-8" }).trim();
145
+ this.log("info", `解析 CLI 路径: ${binary}`);
146
+ }
147
+ catch {
148
+ this.log("warn", `无法解析 ${binary} 绝对路径,使用原值`);
149
+ }
150
+ }
151
+ this.log("warn", `启动 CLI: ${binary}`, "sdkUrl:", sdkUrl);
152
+ // 参照 companion (cli-launcher.ts:285-289):
153
+ // companion 设置 CLAUDECODE=1 标识 SDK 模式,但它是独立进程。
154
+ // 当从 Claude Code 会话内启动时,需要清理所有 CLAUDECODE/CLAUDE_CODE_*
155
+ // 环境变量避免嵌套检测。--sdk-url 本身已足够标识 SDK 模式。
156
+ const cleanEnv = {};
157
+ for (const [key, val] of Object.entries(process.env)) {
158
+ if (key === "CLAUDECODE" || key.startsWith("CLAUDE_CODE_"))
159
+ continue;
160
+ cleanEnv[key] = val;
161
+ }
162
+ // 二次清洗:config.env 可能把 CLAUDECODE/CLAUDE_CODE_* 又注入回来
163
+ const mergedEnv = {
164
+ ...cleanEnv,
165
+ ...config.env,
166
+ };
167
+ for (const key of Object.keys(mergedEnv)) {
168
+ if (key === "CLAUDECODE" || key.startsWith("CLAUDE_CODE_")) {
169
+ delete mergedEnv[key];
170
+ }
171
+ }
172
+ this.proc = spawn(binary, args, {
173
+ cwd: config.workingDir,
174
+ env: mergedEnv,
175
+ stdio: ["pipe", "pipe", "pipe"],
176
+ });
177
+ // 收集 stdout/stderr 用于超时诊断
178
+ const stdoutChunks = [];
179
+ const stderrChunks = [];
180
+ this.proc.stdout?.on("data", (data) => {
181
+ const text = data.toString().trim();
182
+ if (text)
183
+ stdoutChunks.push(text);
184
+ });
185
+ this.proc.stderr?.on("data", (data) => {
186
+ const text = data.toString().trim();
187
+ if (text)
188
+ stderrChunks.push(text);
189
+ this.log("warn", "CLI stderr:", text);
190
+ });
191
+ this.proc.on("error", (err) => {
192
+ this.log("error", "CLI 进程错误:", err.message);
193
+ this.stateChangeHandler?.(ConnectionState.ERROR);
194
+ });
195
+ // 进程提前退出时,让 wsReady 立即 reject 而非等到超时
196
+ this.proc.on("exit", (code, signal) => {
197
+ this.log("info", `CLI 进程退出, code=${code}, signal=${signal}`);
198
+ });
199
+ const earlyExit = new Promise((_, reject) => {
200
+ this.proc.on("exit", (code, signal) => {
201
+ const stdout = stdoutChunks.join("\n");
202
+ const stderr = stderrChunks.join("\n");
203
+ reject(new Error(`CLI 进程在连接前退出 (code=${code}, signal=${signal})` +
204
+ (stdout ? `\nstdout:\n${stdout}` : "") +
205
+ (stderr ? `\nstderr:\n${stderr}` : "")));
206
+ });
207
+ });
208
+ // 3. 等待 CLI 反连(不等待 system/init,参照 companion 非阻塞模式)
209
+ const connectTimeout = config.timeout?.connect ?? 30_000;
210
+ this.log("warn", `等待 CLI 反连, 超时: ${connectTimeout}ms`);
211
+ // 超时时附带进程状态和 stdout/stderr 便于诊断
212
+ const timeoutWithDiag = new Promise((_, reject) => {
213
+ setTimeout(() => {
214
+ const alive = this.proc && this.proc.exitCode === null && !this.proc.killed;
215
+ const stdout = stdoutChunks.join("\n");
216
+ const stderr = stderrChunks.join("\n");
217
+ const diag = [
218
+ `CLI 连接超时 (${connectTimeout}ms)`,
219
+ `进程状态: ${alive ? "运行中" : `已退出 (code=${this.proc?.exitCode}, signal=${this.proc?.signalCode})`}`,
220
+ stdout ? `stdout (最后 500 字符):\n${stdout.slice(-500)}` : "stdout: (空)",
221
+ stderr ? `stderr:\n${stderr}` : "stderr: (空)",
222
+ ].join("\n");
223
+ reject(new Error(diag));
224
+ }, connectTimeout);
225
+ });
226
+ this.ws = await Promise.race([wsReady, earlyExit, timeoutWithDiag]);
227
+ // system/init 到达时会异步更新 cliSessionId(在 routeMessage 中处理)
228
+ this.log("warn", `连接就绪, sessionId=${sessionId} (system/init 将异步更新)`);
229
+ this.stateChangeHandler?.(ConnectionState.CONNECTED);
230
+ return {
231
+ sessionId,
232
+ backendType: "claude-code",
233
+ model: config.model ?? "unknown",
234
+ tools: [],
235
+ capabilities: {
236
+ supportsResume: true,
237
+ supportsStreaming: true,
238
+ supportsToolApproval: true,
239
+ supportsModelSwitch: true,
240
+ },
241
+ };
242
+ }
243
+ async sendPrompt(prompt, _options) {
244
+ this.stateChangeHandler?.(ConnectionState.EXECUTING);
245
+ // 重置原始对话日志(每次 sendPrompt 开始新的收集周期)
246
+ this.rawConversationLog = [];
247
+ // 参照 companion (ws-bridge.ts:848-854)
248
+ const msg = {
249
+ type: "user",
250
+ message: { role: "user", content: prompt },
251
+ parent_tool_use_id: null,
252
+ session_id: this.cliSessionId ?? "",
253
+ };
254
+ const ndjson = JSON.stringify(msg);
255
+ // 记录发出的消息
256
+ this.rawConversationLog.push({
257
+ direction: "sent",
258
+ timestamp: new Date().toISOString(),
259
+ message: msg,
260
+ });
261
+ // this.log(
262
+ // "info",
263
+ // `[WS 发] sendPrompt: ${ndjson.slice(0, 500)}${ndjson.length > 500 ? "...(截断)" : ""}`,
264
+ // );
265
+ this.sendToCLI(ndjson);
266
+ }
267
+ async interrupt() {
268
+ // 参照 companion (ws-bridge.ts:899-905)
269
+ const msg = {
270
+ type: "control_request",
271
+ request_id: randomUUID(),
272
+ request: { subtype: "interrupt" },
273
+ };
274
+ const ndjson = JSON.stringify(msg);
275
+ this.rawConversationLog.push({
276
+ direction: "sent",
277
+ timestamp: new Date().toISOString(),
278
+ message: msg,
279
+ });
280
+ // this.log("info", `[WS 发] interrupt: ${ndjson}`);
281
+ this.sendToCLI(ndjson);
282
+ }
283
+ async resume(sessionId) {
284
+ await this.dispose();
285
+ if (!this.config)
286
+ throw new Error("No config available for resume");
287
+ // 参照 companion (cli-launcher.ts:219-224): 用 cliSessionId 恢复会话
288
+ return this.connect({
289
+ ...this.config,
290
+ resumeSessionId: sessionId,
291
+ });
292
+ }
293
+ getCapabilities() {
294
+ return {
295
+ supportsResume: true,
296
+ supportsStreaming: true,
297
+ supportsToolApproval: true,
298
+ supportsModelSwitch: true,
299
+ };
300
+ }
301
+ async dispose() {
302
+ // 参照 companion (cli-launcher.ts:507-532): SIGTERM → 等待 → SIGKILL
303
+ if (this.proc) {
304
+ const proc = this.proc;
305
+ const alreadyExited = proc.exitCode !== null || proc.signalCode !== null;
306
+ if (!alreadyExited) {
307
+ proc.kill("SIGTERM");
308
+ await new Promise((resolve) => {
309
+ const timeout = setTimeout(() => {
310
+ proc.kill("SIGKILL");
311
+ resolve();
312
+ }, 5_000);
313
+ proc.on("exit", () => {
314
+ clearTimeout(timeout);
315
+ resolve();
316
+ });
317
+ });
318
+ }
319
+ this.proc = null;
320
+ }
321
+ this.ws?.close();
322
+ this.ws = null;
323
+ this.wsServer?.close();
324
+ this.wsServer = null;
325
+ this.httpServer?.close();
326
+ this.httpServer = null;
327
+ this.restoreMcpJson();
328
+ this.stateChangeHandler?.(ConnectionState.DISCONNECTED);
329
+ }
330
+ onToolRequest(handler) {
331
+ this.toolRequestHandler = handler;
332
+ }
333
+ onResult(handler) {
334
+ this.resultHandler = handler;
335
+ }
336
+ onProgress(handler) {
337
+ this.progressHandler = handler;
338
+ }
339
+ onStateChange(handler) {
340
+ this.stateChangeHandler = handler;
341
+ }
342
+ onSessionId(handler) {
343
+ this.sessionIdHandler = handler;
344
+ }
345
+ getCliSessionId() {
346
+ return this.cliSessionId;
347
+ }
348
+ // ── 私有方法 ──
349
+ /**
350
+ * 统一发送入口:WS 未就绪时消息入队,就绪时直接发送。
351
+ * 参照 companion 非阻塞队列模式。
352
+ */
353
+ sendToCLI(ndjson) {
354
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
355
+ this.log("warn", `[WS] CLI 未就绪 (ws=${this.ws ? `readyState=${this.ws.readyState}` : "null"}),消息入队 (queue=${this.pendingMessages.length + 1})`);
356
+ this.pendingMessages.push(ndjson);
357
+ return;
358
+ }
359
+ // this.log(
360
+ // "warn",
361
+ // `[WS 发] ${ndjson.slice(0, 500)}${ndjson.length > 500 ? "...(截断)" : ""}`,
362
+ // );
363
+ this.ws.send(ndjson + "\n");
364
+ }
365
+ setupMessageHandler(ws) {
366
+ // WS 连接建立后立即刷新排队消息
367
+ if (this.pendingMessages.length > 0) {
368
+ this.log("info", `[WS] 刷新 ${this.pendingMessages.length} 条排队消息`);
369
+ const queued = this.pendingMessages.splice(0);
370
+ for (const ndjson of queued) {
371
+ this.sendToCLI(ndjson);
372
+ }
373
+ }
374
+ let buffer = "";
375
+ ws.on("message", (data) => {
376
+ const raw = data.toString();
377
+ // this.log('warn', `[WS 收] ${raw.slice(0, 500)}${raw.length > 500 ? '...(截断)' : ''}`);
378
+ buffer += raw;
379
+ const lines = buffer.split("\n");
380
+ buffer = lines.pop() ?? "";
381
+ for (const line of lines) {
382
+ if (!line.trim())
383
+ continue;
384
+ try {
385
+ const msg = JSON.parse(line);
386
+ this.routeMessage(msg);
387
+ }
388
+ catch (err) {
389
+ this.log("warn", `[WS 收] JSON 解析失败: ${err.message}, raw=${line.slice(0, 200)}`);
390
+ }
391
+ }
392
+ });
393
+ ws.on("close", (code, reason) => {
394
+ this.log("warn", `[WS] 连接关闭, code=${code}, reason=${reason.toString()}`);
395
+ });
396
+ ws.on("error", (err) => {
397
+ this.log("error", `[WS] 连接错误:`, err.message);
398
+ });
399
+ ws.on("ping", () => {
400
+ // this.log("warn", "[WS] 收到 ping");
401
+ });
402
+ ws.on("pong", () => {
403
+ // this.log("warn", "[WS] 收到 pong");
404
+ });
405
+ }
406
+ /**
407
+ * 参照 companion (ws-bridge.ts:550-591): 按 type 路由 CLI 消息
408
+ */
409
+ routeMessage(msg) {
410
+ // 收到任何消息即证明连接存活,通知 BackendManager 重置心跳计数
411
+ this.progressHandler?.({ type: "heartbeat", timestamp: Date.now() });
412
+ // 记录收到的原始消息到对话日志
413
+ this.rawConversationLog.push({
414
+ direction: "received",
415
+ timestamp: new Date().toISOString(),
416
+ message: msg,
417
+ });
418
+ const msgType = `${msg.type}${msg.subtype ? "/" + msg.subtype : ""}`;
419
+ this.log("info", `[路由] type=${msgType}${msg.session_id ? ` sid=${msg.session_id}` : ""}${msg.hook_name ? ` hook=${msg.hook_name}` : ""}`);
420
+ switch (msg.type) {
421
+ case "system":
422
+ // 参照 companion (ws-bridge.ts:594-641): 只有 subtype=init 才是初始化消息
423
+ // 异步更新 cliSessionId 和 initMessage,不阻塞连接流程
424
+ if (msg.subtype === "init") {
425
+ this.log("info", `[路由] system/init 详情: model=${msg.model}, tools=${JSON.stringify(msg.tools ?? [])}`);
426
+ this.initMessage = msg;
427
+ this.cliSessionId = msg.session_id ?? this.cliSessionId;
428
+ if (this.cliSessionId) {
429
+ this.sessionIdHandler?.(this.cliSessionId);
430
+ }
431
+ }
432
+ break;
433
+ case "assistant":
434
+ this.log("info", `[路由] assistant 内容块数: ${msg.message?.content?.length ?? 0}`);
435
+ this.handleAssistant(msg);
436
+ break;
437
+ case "stream_event":
438
+ this.handleStreamEvent(msg.event);
439
+ break;
440
+ case "result": {
441
+ const preview = typeof msg.result === "string"
442
+ ? msg.result.slice(0, 200)
443
+ : JSON.stringify(msg.result ?? null).slice(0, 200);
444
+ this.log("info", `[路由] result: is_error=${msg.is_error}, turns=${msg.num_turns}, preview=${preview}`);
445
+ this.handleResult(msg);
446
+ break;
447
+ }
448
+ case "control_request":
449
+ this.log("info", `[路由] control_request: subtype=${msg.request?.subtype}, tool=${msg.request?.tool_name ?? "N/A"}`);
450
+ if (msg.request?.subtype === "can_use_tool") {
451
+ this.handleToolApproval(msg);
452
+ }
453
+ break;
454
+ case "tool_progress":
455
+ this.progressHandler?.({
456
+ type: "tool_progress",
457
+ toolName: msg.tool_name ?? "unknown",
458
+ output: msg.output ?? "",
459
+ });
460
+ break;
461
+ case "keep_alive":
462
+ case "user":
463
+ // 静默消费(user 是 CLI 回传的 tool_use_result)
464
+ break;
465
+ default:
466
+ this.log("warn", `[路由] 未知消息类型: ${msg.type}, keys=${Object.keys(msg).join(",")}`);
467
+ break;
468
+ }
469
+ }
470
+ handleAssistant(msg) {
471
+ if (msg.message?.content) {
472
+ for (const block of msg.message.content) {
473
+ if (block.type === "text") {
474
+ this.progressHandler?.({ type: "text_chunk", text: block.text });
475
+ }
476
+ else if (block.type === "thinking") {
477
+ this.progressHandler?.({ type: "thinking", text: block.thinking });
478
+ }
479
+ }
480
+ }
481
+ }
482
+ handleStreamEvent(event) {
483
+ if (!event)
484
+ return;
485
+ if (event.type === "content_block_delta") {
486
+ if (event.delta?.type === "text_delta") {
487
+ this.progressHandler?.({ type: "text_chunk", text: event.delta.text });
488
+ }
489
+ }
490
+ }
491
+ handleResult(msg) {
492
+ const result = {
493
+ status: msg.is_error ? "error" : "success",
494
+ resultText: msg.result,
495
+ errors: msg.errors,
496
+ metrics: {
497
+ durationMs: msg.duration_ms ?? 0,
498
+ numTurns: msg.num_turns ?? 0,
499
+ totalCostUsd: msg.total_cost_usd,
500
+ linesAdded: msg.total_lines_added,
501
+ linesRemoved: msg.total_lines_removed,
502
+ },
503
+ // 附加完整的原始对话日志
504
+ rawConversation: [...this.rawConversationLog],
505
+ };
506
+ // 特殊处理 max_turns
507
+ if (msg.subtype === "error_max_turns") {
508
+ result.status = "max_turns";
509
+ }
510
+ this.resultHandler?.(result);
511
+ this.stateChangeHandler?.(ConnectionState.CONNECTED);
512
+ }
513
+ /**
514
+ * 参照 companion (ws-bridge.ts:714-733 + 858-896): 工具审批流
515
+ */
516
+ async handleToolApproval(msg) {
517
+ if (!this.toolRequestHandler)
518
+ return;
519
+ const req = {
520
+ requestId: msg.request_id,
521
+ toolName: msg.request.tool_name ?? "unknown",
522
+ input: msg.request.input ?? {},
523
+ description: msg.request.description,
524
+ toolUseId: msg.request.tool_use_id ?? "",
525
+ };
526
+ const response = await this.toolRequestHandler(req);
527
+ const reply = JSON.stringify({
528
+ type: "control_response",
529
+ response: {
530
+ subtype: "success",
531
+ request_id: req.requestId,
532
+ response: {
533
+ behavior: response.behavior,
534
+ ...(response.behavior === "allow" && response.updatedInput
535
+ ? { updatedInput: response.updatedInput }
536
+ : {}),
537
+ },
538
+ },
539
+ });
540
+ // 记录发出的 tool approval 响应
541
+ this.rawConversationLog.push({
542
+ direction: "sent",
543
+ timestamp: new Date().toISOString(),
544
+ message: JSON.parse(reply),
545
+ });
546
+ // this.log(
547
+ // "info",
548
+ // `[WS 发] toolApproval: tool=${req.toolName}, behavior=${response.behavior}`,
549
+ // );
550
+ this.sendToCLI(reply);
551
+ }
552
+ findFreePort() {
553
+ return new Promise((resolve, reject) => {
554
+ const server = createServer();
555
+ server.listen(0, "127.0.0.1", () => {
556
+ const addr = server.address();
557
+ if (addr && typeof addr === "object") {
558
+ const port = addr.port;
559
+ server.close(() => resolve(port));
560
+ }
561
+ else {
562
+ reject(new Error("Failed to get port"));
563
+ }
564
+ });
565
+ });
566
+ }
567
+ withTimeout(promise, ms, message) {
568
+ return new Promise((resolve, reject) => {
569
+ const timer = setTimeout(() => reject(new Error(message)), ms);
570
+ promise.then((val) => {
571
+ clearTimeout(timer);
572
+ resolve(val);
573
+ }, (err) => {
574
+ clearTimeout(timer);
575
+ reject(err);
576
+ });
577
+ });
578
+ }
579
+ /**
580
+ * 从 workingDir 向上查找 git root
581
+ */
582
+ /**
583
+ * 将 MCP server 配置写入目标目录的 .mcp.json
584
+ * 如果文件已存在,合并我们的 server 条目并备份原始内容
585
+ */
586
+ writeMcpJson(workingDir, mcpServers) {
587
+ // 确保目录存在
588
+ if (!existsSync(workingDir)) {
589
+ mkdirSync(workingDir, { recursive: true });
590
+ }
591
+ this.mcpJsonPath = join(workingDir, ".mcp.json");
592
+ // 构建我们的 server 条目
593
+ const ourServers = {};
594
+ for (const s of mcpServers) {
595
+ if (s.url) {
596
+ ourServers[s.name] = { type: "http", url: s.url };
597
+ }
598
+ else if (s.command) {
599
+ ourServers[s.name] = {
600
+ type: "stdio",
601
+ command: s.command,
602
+ ...(s.args ? { args: s.args } : {}),
603
+ };
604
+ }
605
+ }
606
+ // 读取已有文件(如有)
607
+ let existing = {};
608
+ if (existsSync(this.mcpJsonPath)) {
609
+ try {
610
+ const raw = readFileSync(this.mcpJsonPath, "utf-8");
611
+ existing = JSON.parse(raw);
612
+ this.mcpJsonBackup = raw;
613
+ this.mcpJsonCreated = false;
614
+ this.log("info", `.mcp.json 已存在,合并 ${Object.keys(ourServers).length} 个 server`);
615
+ }
616
+ catch {
617
+ this.mcpJsonBackup = null;
618
+ this.mcpJsonCreated = true;
619
+ }
620
+ }
621
+ else {
622
+ this.mcpJsonBackup = null;
623
+ this.mcpJsonCreated = true;
624
+ }
625
+ // 合并写入
626
+ const merged = {
627
+ ...existing,
628
+ mcpServers: {
629
+ ...(existing.mcpServers ?? {}),
630
+ ...ourServers,
631
+ },
632
+ };
633
+ writeFileSync(this.mcpJsonPath, JSON.stringify(merged, null, 2), "utf-8");
634
+ this.log("info", `.mcp.json 已写入: ${this.mcpJsonPath}, servers: ${Object.keys(ourServers).join(", ")}`);
635
+ }
636
+ /**
637
+ * 恢复或删除 .mcp.json
638
+ */
639
+ restoreMcpJson() {
640
+ if (!this.mcpJsonPath)
641
+ return;
642
+ try {
643
+ if (this.mcpJsonCreated) {
644
+ // 文件是我们创建的 → 直接删除
645
+ if (existsSync(this.mcpJsonPath)) {
646
+ unlinkSync(this.mcpJsonPath);
647
+ this.log("info", `.mcp.json 已删除: ${this.mcpJsonPath}`);
648
+ }
649
+ }
650
+ else if (this.mcpJsonBackup !== null) {
651
+ // 文件原本存在 → 恢复原始内容
652
+ writeFileSync(this.mcpJsonPath, this.mcpJsonBackup, "utf-8");
653
+ this.log("info", `.mcp.json 已恢复: ${this.mcpJsonPath}`);
654
+ }
655
+ }
656
+ catch (err) {
657
+ this.log("warn", `.mcp.json 清理失败:`, err.message);
658
+ }
659
+ this.mcpJsonPath = null;
660
+ this.mcpJsonBackup = null;
661
+ this.mcpJsonCreated = false;
662
+ }
663
+ }
664
+ //# sourceMappingURL=claude-code-backend.js.map
@@ -0,0 +1,2 @@
1
+ export { ClaudeCodeBackend } from './claude-code-backend.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "@team-anya/cc-client",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "dependencies": {
8
+ "@team-anya/core": "file:../core",
9
+ "ws": "^8.18.0"
10
+ }
11
+ }