chatccc 0.2.252 → 0.2.254

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 (296) hide show
  1. package/README.md +10 -1
  2. package/bin/cccagent.mjs +12 -3
  3. package/bin/chatccc.mjs +15 -6
  4. package/config.sample.json +2 -1
  5. package/deepccc-agent/README.md +40 -35
  6. package/deepccc-agent/os-prompts/darwin.md +8 -8
  7. package/deepccc-agent/os-prompts/linux.md +8 -8
  8. package/deepccc-agent/os-prompts/win32.md +11 -11
  9. package/deepccc-agent/package.json +65 -65
  10. package/dist/deepccc-agent/src/cli.js +634 -0
  11. package/dist/deepccc-agent/src/config.js +81 -0
  12. package/dist/deepccc-agent/src/context.js +348 -0
  13. package/dist/deepccc-agent/src/file-log.js +34 -0
  14. package/dist/deepccc-agent/src/file-tools.js +1208 -0
  15. package/dist/deepccc-agent/src/index.js +601 -0
  16. package/dist/deepccc-agent/src/permissions.js +170 -0
  17. package/dist/deepccc-agent/src/privacy.js +124 -0
  18. package/dist/deepccc-agent/src/proc-tree-kill.js +60 -0
  19. package/dist/deepccc-agent/src/progress/cards-helpers.js +70 -0
  20. package/dist/deepccc-agent/src/progress/reducer.js +102 -0
  21. package/dist/deepccc-agent/src/progress/terminal-renderer.js +264 -0
  22. package/dist/deepccc-agent/src/progress/view.js +30 -0
  23. package/dist/deepccc-agent/src/raw-stream-log.js +106 -0
  24. package/dist/deepccc-agent/src/session-search.js +276 -0
  25. package/dist/deepccc-agent/src/session-select.js +23 -0
  26. package/dist/deepccc-agent/src/sigint.js +26 -0
  27. package/dist/deepccc-agent/src/skills.js +178 -0
  28. package/dist/deepccc-agent/src/web-tools.js +246 -0
  29. package/dist/src/adapters/adapter-interface.js +19 -0
  30. package/dist/src/adapters/ccc-adapter.js +112 -0
  31. package/dist/src/adapters/claude-adapter.js +497 -0
  32. package/dist/src/adapters/claude-session-meta-store.js +92 -0
  33. package/dist/src/adapters/codex-adapter.js +279 -0
  34. package/dist/src/adapters/codex-session-meta-store.js +94 -0
  35. package/dist/src/adapters/cursor-adapter.js +491 -0
  36. package/dist/src/adapters/cursor-session-meta-store.js +116 -0
  37. package/dist/src/adapters/jsonl-stream.js +104 -0
  38. package/{src/adapters/proc-tree-kill.ts → dist/src/adapters/proc-tree-kill.js} +94 -97
  39. package/dist/src/adapters/raw-stream-log.js +106 -0
  40. package/dist/src/adapters/resource-monitor.js +113 -0
  41. package/dist/src/agent-activity.js +133 -0
  42. package/dist/src/agent-delegate-task-rpc.js +129 -0
  43. package/dist/src/agent-delegate-task.js +48 -0
  44. package/dist/src/agent-file-rpc.js +152 -0
  45. package/dist/src/agent-image-rpc.js +148 -0
  46. package/dist/src/agent-platform-routing.js +13 -0
  47. package/dist/src/agent-reload-config-rpc.js +23 -0
  48. package/dist/src/agent-rpc-body.js +87 -0
  49. package/dist/src/agent-stop-stuck.js +110 -0
  50. package/dist/src/agent-team/application/board-service.js +142 -0
  51. package/dist/src/agent-team/application/main-agent-service.js +151 -0
  52. package/dist/src/agent-team/domain/board.js +69 -0
  53. package/dist/src/agent-team/http/board-routes.js +213 -0
  54. package/dist/src/agent-team/infrastructure/filesystem-browser.js +112 -0
  55. package/dist/src/agent-team/infrastructure/main-agent-session-runtime.js +30 -0
  56. package/dist/src/agent-team/main-agent-bootstrap.js +15 -0
  57. package/dist/src/agent-team/repositories/board-repository.js +10 -0
  58. package/dist/src/agent-team/repositories/feishu-p2p-contact-store.js +64 -0
  59. package/dist/src/agent-team/repositories/json-board-repository.js +209 -0
  60. package/dist/src/agent-team/repositories/main-agent-binding-repository.js +84 -0
  61. package/dist/src/agent-team/sync/board-sync-adapter.js +1 -0
  62. package/dist/src/agent-team/web/agent-team-page.js +219 -0
  63. package/dist/src/agent-tool.js +10 -0
  64. package/dist/src/card-action-routing.js +7 -0
  65. package/dist/src/card-plain-text.js +101 -0
  66. package/dist/src/cardkit.js +158 -0
  67. package/dist/src/cards.js +573 -0
  68. package/dist/src/chatgpt-subscription-rpc.js +18 -0
  69. package/dist/src/chatgpt-subscription.js +199 -0
  70. package/dist/src/chrome-devtools-guard.js +238 -0
  71. package/dist/src/claude-sdk-installer.js +249 -0
  72. package/dist/src/codex-reset-actions.js +143 -0
  73. package/dist/src/config-utils.js +149 -0
  74. package/dist/src/config.js +808 -0
  75. package/dist/src/cursor-usage.js +77 -0
  76. package/dist/src/exit-banner.js +28 -0
  77. package/dist/src/feishu-api.js +1404 -0
  78. package/dist/src/feishu-message-ingress.js +137 -0
  79. package/dist/src/feishu-platform.js +97 -0
  80. package/dist/src/format-message.js +252 -0
  81. package/dist/src/git-command.js +155 -0
  82. package/dist/src/im-skills.js +121 -0
  83. package/dist/src/index.js +835 -0
  84. package/dist/src/litellm-proxy.js +300 -0
  85. package/dist/src/orchestrator.js +2096 -0
  86. package/dist/src/package-root.js +26 -0
  87. package/dist/src/platform-adapter.js +7 -0
  88. package/dist/src/platform-startup.js +6 -0
  89. package/dist/src/privacy.js +100 -0
  90. package/dist/src/progress/reducer.js +102 -0
  91. package/dist/src/progress/terminal-renderer.js +264 -0
  92. package/dist/src/progress/view.js +30 -0
  93. package/dist/src/response-stall.js +14 -0
  94. package/dist/src/runtime-entry.js +13 -0
  95. package/dist/src/runtime-reload.js +19 -0
  96. package/dist/src/session-chat-binding.js +183 -0
  97. package/dist/src/session-name.js +7 -0
  98. package/dist/src/session.js +2147 -0
  99. package/dist/src/shared-prefix.js +16 -0
  100. package/dist/src/shared.js +493 -0
  101. package/dist/src/sim-agent.js +105 -0
  102. package/dist/src/sim-platform.js +142 -0
  103. package/dist/src/sim-store.js +231 -0
  104. package/dist/src/simplify.js +99 -0
  105. package/dist/src/startup-lifecycle.js +209 -0
  106. package/dist/src/stream-state.js +141 -0
  107. package/dist/src/terminal-error.js +100 -0
  108. package/dist/src/trace.js +50 -0
  109. package/dist/src/turn-cards.js +92 -0
  110. package/dist/src/update-command-guard.js +114 -0
  111. package/{src/web-ui.ts → dist/src/web-ui.js} +753 -839
  112. package/dist/src/wechat-platform.js +545 -0
  113. package/package.json +76 -74
  114. package/deepccc-agent/LICENSE +0 -201
  115. package/deepccc-agent/bin/deepccc.mjs +0 -26
  116. package/deepccc-agent/docs/cache-hit-rate-1.jpg +0 -0
  117. package/deepccc-agent/docs/cache-hit-rate-2.jpg +0 -0
  118. package/deepccc-agent/package-lock.json +0 -2027
  119. package/deepccc-agent/src/__tests__/chat-session.test.ts +0 -877
  120. package/deepccc-agent/src/__tests__/cli-json.test.ts +0 -49
  121. package/deepccc-agent/src/__tests__/config.test.ts +0 -34
  122. package/deepccc-agent/src/__tests__/context.test.ts +0 -341
  123. package/deepccc-agent/src/__tests__/file-tools.test.ts +0 -240
  124. package/deepccc-agent/src/__tests__/permissions.test.ts +0 -199
  125. package/deepccc-agent/src/__tests__/privacy.test.ts +0 -318
  126. package/deepccc-agent/src/__tests__/progress-reducer.test.ts +0 -121
  127. package/deepccc-agent/src/__tests__/session-search.test.ts +0 -262
  128. package/deepccc-agent/src/__tests__/session-select.test.ts +0 -116
  129. package/deepccc-agent/src/__tests__/sigint.test.ts +0 -56
  130. package/deepccc-agent/src/__tests__/skills.test.ts +0 -284
  131. package/deepccc-agent/src/__tests__/terminal-renderer.test.ts +0 -247
  132. package/deepccc-agent/src/__tests__/web-tools.test.ts +0 -220
  133. package/deepccc-agent/src/cli.ts +0 -682
  134. package/deepccc-agent/src/config.ts +0 -101
  135. package/deepccc-agent/src/context.ts +0 -465
  136. package/deepccc-agent/src/file-log.ts +0 -38
  137. package/deepccc-agent/src/file-tools.ts +0 -1493
  138. package/deepccc-agent/src/index.ts +0 -710
  139. package/deepccc-agent/src/permissions.ts +0 -226
  140. package/deepccc-agent/src/privacy.ts +0 -141
  141. package/deepccc-agent/src/proc-tree-kill.ts +0 -61
  142. package/deepccc-agent/src/progress/cards-helpers.ts +0 -76
  143. package/deepccc-agent/src/progress/reducer.ts +0 -113
  144. package/deepccc-agent/src/progress/terminal-renderer.ts +0 -294
  145. package/deepccc-agent/src/progress/view.ts +0 -77
  146. package/deepccc-agent/src/raw-stream-log.ts +0 -124
  147. package/deepccc-agent/src/session-search.ts +0 -370
  148. package/deepccc-agent/src/session-select.ts +0 -48
  149. package/deepccc-agent/src/sigint.ts +0 -50
  150. package/deepccc-agent/src/skills.ts +0 -205
  151. package/deepccc-agent/src/web-tools.ts +0 -313
  152. package/deepccc-agent/tsconfig.build.json +0 -13
  153. package/deepccc-agent/tsconfig.json +0 -13
  154. package/deepccc-agent/vitest.config.ts +0 -7
  155. package/src/__tests__/adapter-interface.test.ts +0 -152
  156. package/src/__tests__/agent-activity.test.ts +0 -86
  157. package/src/__tests__/agent-delegate-task-rpc.test.ts +0 -165
  158. package/src/__tests__/agent-image-rpc.test.ts +0 -34
  159. package/src/__tests__/agent-platform-routing.test.ts +0 -26
  160. package/src/__tests__/agent-reload-config-rpc.test.ts +0 -99
  161. package/src/__tests__/agent-rpc-body.test.ts +0 -42
  162. package/src/__tests__/builtin-chat-session.test.ts +0 -532
  163. package/src/__tests__/builtin-cli-json.test.ts +0 -39
  164. package/src/__tests__/builtin-config.test.ts +0 -26
  165. package/src/__tests__/builtin-context.test.ts +0 -319
  166. package/src/__tests__/builtin-file-tools.test.ts +0 -240
  167. package/src/__tests__/builtin-permissions.test.ts +0 -219
  168. package/src/__tests__/builtin-session-search.test.ts +0 -262
  169. package/src/__tests__/builtin-session-select.test.ts +0 -116
  170. package/src/__tests__/builtin-sigint.test.ts +0 -56
  171. package/src/__tests__/builtin-skills.test.ts +0 -284
  172. package/src/__tests__/builtin-web-tools.test.ts +0 -220
  173. package/src/__tests__/card-action-routing.test.ts +0 -18
  174. package/src/__tests__/card-plain-text.test.ts +0 -45
  175. package/src/__tests__/cardkit.test.ts +0 -60
  176. package/src/__tests__/cards.test.ts +0 -607
  177. package/src/__tests__/ccc-adapter.test.ts +0 -194
  178. package/src/__tests__/chatgpt-subscription-rpc.test.ts +0 -89
  179. package/src/__tests__/chatgpt-subscription.test.ts +0 -135
  180. package/src/__tests__/chrome-devtools-guard.test.ts +0 -165
  181. package/src/__tests__/claude-adapter.test.ts +0 -614
  182. package/src/__tests__/claude-raw-stream-log.test.ts +0 -96
  183. package/src/__tests__/claude-sdk-installer.test.ts +0 -285
  184. package/src/__tests__/codex-adapter.test.ts +0 -331
  185. package/src/__tests__/codex-raw-stream-log.test.ts +0 -170
  186. package/src/__tests__/codex-reset-actions.test.ts +0 -146
  187. package/src/__tests__/config-reload.test.ts +0 -284
  188. package/src/__tests__/config-sample.test.ts +0 -97
  189. package/src/__tests__/config-utils.test.ts +0 -40
  190. package/src/__tests__/config.test.ts +0 -395
  191. package/src/__tests__/crash-logging.test.ts +0 -360
  192. package/src/__tests__/cursor-adapter.test.ts +0 -890
  193. package/src/__tests__/cursor-session-meta-store.test.ts +0 -212
  194. package/src/__tests__/feishu-api.test.ts +0 -60
  195. package/src/__tests__/feishu-avatar.test.ts +0 -504
  196. package/src/__tests__/feishu-message-ingress.test.ts +0 -138
  197. package/src/__tests__/feishu-platform.test.ts +0 -75
  198. package/src/__tests__/fixtures/codex_simple_text.jsonl +0 -4
  199. package/src/__tests__/fixtures/codex_with_tool.jsonl +0 -6
  200. package/src/__tests__/fixtures/cursor_partial_only.jsonl +0 -5
  201. package/src/__tests__/fixtures/cursor_partial_with_final.jsonl +0 -13
  202. package/src/__tests__/fixtures/cursor_with_tool_call.jsonl +0 -12
  203. package/src/__tests__/format-message.test.ts +0 -316
  204. package/src/__tests__/git-command.test.ts +0 -288
  205. package/src/__tests__/im-skills.test.ts +0 -125
  206. package/src/__tests__/jsonl-stream.test.ts +0 -79
  207. package/src/__tests__/orchestrator.test.ts +0 -1268
  208. package/src/__tests__/package-files.test.ts +0 -24
  209. package/src/__tests__/platform-startup.test.ts +0 -19
  210. package/src/__tests__/privacy.test.ts +0 -198
  211. package/src/__tests__/proc-tree-kill.test.ts +0 -108
  212. package/src/__tests__/progress-reducer.test.ts +0 -121
  213. package/src/__tests__/raw-stream-log.test.ts +0 -106
  214. package/src/__tests__/response-stall.test.ts +0 -49
  215. package/src/__tests__/restart.test.ts +0 -232
  216. package/src/__tests__/session-ccc-config.test.ts +0 -66
  217. package/src/__tests__/session.test.ts +0 -3004
  218. package/src/__tests__/shared-prefix.test.ts +0 -36
  219. package/src/__tests__/sim-agent.test.ts +0 -174
  220. package/src/__tests__/sim-platform.test.ts +0 -93
  221. package/src/__tests__/sim-store.test.ts +0 -214
  222. package/src/__tests__/simplify.test.ts +0 -283
  223. package/src/__tests__/startup-lifecycle.test.ts +0 -231
  224. package/src/__tests__/stop-session.test.ts +0 -162
  225. package/src/__tests__/stream-state.test.ts +0 -164
  226. package/src/__tests__/terminal-error.test.ts +0 -54
  227. package/src/__tests__/terminal-renderer.test.ts +0 -247
  228. package/src/__tests__/update-command-guard.test.ts +0 -144
  229. package/src/__tests__/web-ui.test.ts +0 -438
  230. package/src/__tests__/wechat-platform.test.ts +0 -111
  231. package/src/adapters/adapter-interface.ts +0 -217
  232. package/src/adapters/ccc-adapter.ts +0 -150
  233. package/src/adapters/claude-adapter.ts +0 -673
  234. package/src/adapters/claude-session-meta-store.ts +0 -120
  235. package/src/adapters/codex-adapter.ts +0 -426
  236. package/src/adapters/codex-session-meta-store.ts +0 -131
  237. package/src/adapters/cursor-adapter.ts +0 -681
  238. package/src/adapters/cursor-session-meta-store.ts +0 -154
  239. package/src/adapters/jsonl-stream.ts +0 -157
  240. package/src/adapters/raw-stream-log.ts +0 -124
  241. package/src/adapters/resource-monitor.ts +0 -141
  242. package/src/agent-activity.ts +0 -175
  243. package/src/agent-delegate-task-rpc.ts +0 -153
  244. package/src/agent-delegate-task.ts +0 -91
  245. package/src/agent-file-rpc.ts +0 -172
  246. package/src/agent-image-rpc.ts +0 -168
  247. package/src/agent-platform-routing.ts +0 -28
  248. package/src/agent-reload-config-rpc.ts +0 -34
  249. package/src/agent-rpc-body.ts +0 -92
  250. package/src/agent-stop-stuck.ts +0 -129
  251. package/src/card-action-routing.ts +0 -14
  252. package/src/card-plain-text.ts +0 -108
  253. package/src/cardkit.ts +0 -179
  254. package/src/cards.ts +0 -684
  255. package/src/chatgpt-subscription-rpc.ts +0 -27
  256. package/src/chatgpt-subscription.ts +0 -299
  257. package/src/chrome-devtools-guard.ts +0 -318
  258. package/src/claude-sdk-installer.ts +0 -324
  259. package/src/codex-reset-actions.ts +0 -184
  260. package/src/config-utils.ts +0 -211
  261. package/src/config.ts +0 -1063
  262. package/src/cursor-usage.ts +0 -128
  263. package/src/exit-banner.ts +0 -33
  264. package/src/feishu-api.ts +0 -1616
  265. package/src/feishu-message-ingress.ts +0 -195
  266. package/src/feishu-platform.ts +0 -159
  267. package/src/format-message.ts +0 -293
  268. package/src/git-command.ts +0 -202
  269. package/src/im-skills.ts +0 -149
  270. package/src/index.ts +0 -1089
  271. package/src/litellm-proxy.ts +0 -374
  272. package/src/orchestrator.ts +0 -2543
  273. package/src/platform-adapter.ts +0 -70
  274. package/src/platform-startup.ts +0 -16
  275. package/src/privacy.ts +0 -118
  276. package/src/progress/reducer.ts +0 -113
  277. package/src/progress/terminal-renderer.ts +0 -294
  278. package/src/progress/view.ts +0 -77
  279. package/src/response-stall.ts +0 -28
  280. package/src/runtime-reload.ts +0 -34
  281. package/src/session-chat-binding.ts +0 -292
  282. package/src/session-name.ts +0 -8
  283. package/src/session.ts +0 -2659
  284. package/src/shared-prefix.ts +0 -29
  285. package/src/shared.ts +0 -552
  286. package/src/sim-agent.ts +0 -167
  287. package/src/sim-platform.ts +0 -177
  288. package/src/sim-store.ts +0 -317
  289. package/src/simplify.ts +0 -120
  290. package/src/startup-lifecycle.ts +0 -250
  291. package/src/stream-state.ts +0 -177
  292. package/src/terminal-error.ts +0 -129
  293. package/src/trace.ts +0 -51
  294. package/src/turn-cards.ts +0 -118
  295. package/src/update-command-guard.ts +0 -165
  296. package/src/wechat-platform.ts +0 -680
@@ -0,0 +1,213 @@
1
+ import { AGENT_TOOL_OPTIONS, isAgentTool } from "../../agent-tool.js";
2
+ import { readUtf8JsonBody } from "../../agent-rpc-body.js";
3
+ import { BoardService } from "../application/board-service.js";
4
+ import { NodeFilesystemBrowser } from "../infrastructure/filesystem-browser.js";
5
+ import { JsonBoardRepository } from "../repositories/json-board-repository.js";
6
+ import { BoardStoreError } from "../repositories/board-repository.js";
7
+ const API_PREFIX = "/api/agent-team";
8
+ const MAX_REQUEST_BYTES = 256 * 1024;
9
+ export function createAgentTeamRequestHandler(options) {
10
+ const defaultWorkspace = options.defaultWorkspace ?? process.cwd();
11
+ const filesystemBrowser = options.filesystemBrowser ?? new NodeFilesystemBrowser({ defaultDirectory: defaultWorkspace });
12
+ const mainAgentService = () => {
13
+ if (typeof options.mainAgentService === "function")
14
+ return options.mainAgentService();
15
+ return options.mainAgentService ?? null;
16
+ };
17
+ return async function handleAgentTeamRequest(req, res) {
18
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
19
+ const pathname = url.pathname;
20
+ const method = req.method ?? "GET";
21
+ if (pathname !== API_PREFIX && !pathname.startsWith(`${API_PREFIX}/`))
22
+ return false;
23
+ try {
24
+ if (pathname === `${API_PREFIX}/workspaces` && method === "GET") {
25
+ jsonReply(res, 200, {
26
+ ok: true,
27
+ defaultWorkspace,
28
+ agentOptions: AGENT_TOOL_OPTIONS,
29
+ workspaces: await options.service.listWorkspaces(),
30
+ });
31
+ return true;
32
+ }
33
+ if (pathname === `${API_PREFIX}/filesystem/locations` && method === "GET") {
34
+ jsonReply(res, 200, { ok: true, locations: await filesystemBrowser.listLocations() });
35
+ return true;
36
+ }
37
+ if (pathname === `${API_PREFIX}/filesystem/directories` && method === "POST") {
38
+ const body = await bodyJson(req);
39
+ const directory = await filesystemBrowser.listDirectories(stringField(body.path, "path"), optionalBooleanField(body.showHidden, "showHidden"));
40
+ jsonReply(res, 200, { ok: true, directory });
41
+ return true;
42
+ }
43
+ if (pathname === `${API_PREFIX}/filesystem/validate-directory` && method === "POST") {
44
+ const body = await bodyJson(req);
45
+ jsonReply(res, 200, { ok: true, path: await filesystemBrowser.validateDirectory(stringField(body.path, "path")) });
46
+ return true;
47
+ }
48
+ if (pathname === `${API_PREFIX}/lookup` && method === "POST") {
49
+ const body = await bodyJson(req);
50
+ const board = await options.service.findWorkspace(stringField(body.workspacePath, "workspacePath"));
51
+ jsonReply(res, 200, board
52
+ ? { ok: true, exists: true, boardId: board.boardId }
53
+ : { ok: true, exists: false });
54
+ return true;
55
+ }
56
+ if (pathname === `${API_PREFIX}/open` && method === "POST") {
57
+ const body = await bodyJson(req);
58
+ const board = await options.service.openWorkspace(stringField(body.workspacePath, "workspacePath"));
59
+ jsonReply(res, 200, { ok: true, board, binding: await mainAgentService()?.getBinding(board.boardId) ?? null });
60
+ return true;
61
+ }
62
+ if (pathname === `${API_PREFIX}/feishu-contact` && method === "GET") {
63
+ const manager = requireMainAgentService(mainAgentService());
64
+ jsonReply(res, 200, { ok: true, contact: await manager.getContact() });
65
+ return true;
66
+ }
67
+ const boardMatch = pathname.match(/^\/api\/agent-team\/boards\/([^/]+)$/);
68
+ if (boardMatch && method === "GET") {
69
+ const boardId = decodeURIComponent(boardMatch[1]);
70
+ jsonReply(res, 200, {
71
+ ok: true,
72
+ board: await options.service.getBoard(boardId),
73
+ binding: await mainAgentService()?.getBinding(boardId) ?? null,
74
+ });
75
+ return true;
76
+ }
77
+ const relinkMatch = pathname.match(/^\/api\/agent-team\/boards\/([^/]+)\/relink$/);
78
+ if (relinkMatch && method === "POST") {
79
+ const body = await bodyJson(req);
80
+ const boardId = decodeURIComponent(relinkMatch[1]);
81
+ const workspacePath = stringField(body.workspacePath, "workspacePath");
82
+ const revision = revisionField(body.expectedRevision);
83
+ const manager = mainAgentService();
84
+ const result = manager
85
+ ? await manager.relinkWorkspace(boardId, workspacePath, revision)
86
+ : { board: await options.service.relinkWorkspace(boardId, workspacePath, revision), binding: null };
87
+ jsonReply(res, 200, { ok: true, ...result });
88
+ return true;
89
+ }
90
+ const mainAgentMatch = pathname.match(/^\/api\/agent-team\/boards\/([^/]+)\/main-agent$/);
91
+ if (mainAgentMatch && method === "POST") {
92
+ const body = await bodyJson(req);
93
+ if (!isAgentTool(body.agentId)) {
94
+ throw new BoardStoreError("invalid_request", `Unsupported primary Agent: ${String(body.agentId)}`, 400);
95
+ }
96
+ const result = await requireMainAgentService(mainAgentService()).setPrimaryAgent(decodeURIComponent(mainAgentMatch[1]), body.agentId, revisionField(body.expectedRevision));
97
+ jsonReply(res, 200, { ok: true, ...result });
98
+ return true;
99
+ }
100
+ const tasksMatch = pathname.match(/^\/api\/agent-team\/boards\/([^/]+)\/tasks$/);
101
+ if (tasksMatch && method === "POST") {
102
+ const body = await bodyJson(req);
103
+ const board = await options.service.createTask(decodeURIComponent(tasksMatch[1]), {
104
+ expectedRevision: revisionField(body.expectedRevision),
105
+ title: stringField(body.title, "title"),
106
+ description: optionalString(body.description, "description"),
107
+ columnId: body.columnId,
108
+ });
109
+ jsonReply(res, 200, { ok: true, board });
110
+ return true;
111
+ }
112
+ const moveMatch = pathname.match(/^\/api\/agent-team\/boards\/([^/]+)\/tasks\/([^/]+)\/move$/);
113
+ if (moveMatch && method === "POST") {
114
+ const body = await bodyJson(req);
115
+ const board = await options.service.moveTask(decodeURIComponent(moveMatch[1]), decodeURIComponent(moveMatch[2]), {
116
+ expectedRevision: revisionField(body.expectedRevision),
117
+ columnId: body.columnId,
118
+ index: integerField(body.index, "index"),
119
+ });
120
+ jsonReply(res, 200, { ok: true, board });
121
+ return true;
122
+ }
123
+ const taskMatch = pathname.match(/^\/api\/agent-team\/boards\/([^/]+)\/tasks\/([^/]+)$/);
124
+ if (taskMatch && method === "PATCH") {
125
+ const body = await bodyJson(req);
126
+ const board = await options.service.updateTask(decodeURIComponent(taskMatch[1]), decodeURIComponent(taskMatch[2]), {
127
+ expectedRevision: revisionField(body.expectedRevision),
128
+ title: stringField(body.title, "title"),
129
+ description: optionalString(body.description, "description"),
130
+ });
131
+ jsonReply(res, 200, { ok: true, board });
132
+ return true;
133
+ }
134
+ if (taskMatch && method === "DELETE") {
135
+ const body = await bodyJson(req);
136
+ const board = await options.service.deleteTask(decodeURIComponent(taskMatch[1]), decodeURIComponent(taskMatch[2]), {
137
+ expectedRevision: revisionField(body.expectedRevision),
138
+ });
139
+ jsonReply(res, 200, { ok: true, board });
140
+ return true;
141
+ }
142
+ jsonReply(res, 404, { ok: false, code: "not_found", error: "Agent Team API route not found" });
143
+ return true;
144
+ }
145
+ catch (err) {
146
+ const error = normalizeError(err);
147
+ jsonReply(res, error.status, { ok: false, code: error.code, error: error.message });
148
+ return true;
149
+ }
150
+ };
151
+ }
152
+ export const defaultAgentTeamBoardService = new BoardService(new JsonBoardRepository());
153
+ let defaultMainAgentService = null;
154
+ export const handleAgentTeamRequest = createAgentTeamRequestHandler({
155
+ service: defaultAgentTeamBoardService,
156
+ mainAgentService: () => defaultMainAgentService,
157
+ });
158
+ export function setDefaultAgentTeamMainAgentService(service) {
159
+ defaultMainAgentService = service;
160
+ }
161
+ function requireMainAgentService(service) {
162
+ if (!service) {
163
+ throw new BoardStoreError("main_agent_unavailable", "飞书主 Agent 服务尚未启动", 503);
164
+ }
165
+ return service;
166
+ }
167
+ async function bodyJson(req) {
168
+ try {
169
+ return await readUtf8JsonBody(req, MAX_REQUEST_BYTES);
170
+ }
171
+ catch (err) {
172
+ throw new BoardStoreError("invalid_request", `Invalid request body: ${err.message}`, 400);
173
+ }
174
+ }
175
+ function jsonReply(res, status, value) {
176
+ res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
177
+ res.end(JSON.stringify(value));
178
+ }
179
+ function normalizeError(err) {
180
+ if (err instanceof BoardStoreError)
181
+ return err;
182
+ return new BoardStoreError("storage_error", err instanceof Error ? err.message : String(err), 500);
183
+ }
184
+ function stringField(value, name) {
185
+ if (typeof value !== "string" || !value.trim())
186
+ throw new BoardStoreError("invalid_request", `${name} must be a non-empty string`, 400);
187
+ return value;
188
+ }
189
+ function optionalString(value, name) {
190
+ if (value === undefined || value === null)
191
+ return "";
192
+ if (typeof value !== "string")
193
+ throw new BoardStoreError("invalid_request", `${name} must be a string`, 400);
194
+ return value;
195
+ }
196
+ function optionalBooleanField(value, name) {
197
+ if (value === undefined || value === null)
198
+ return false;
199
+ if (typeof value !== "boolean")
200
+ throw new BoardStoreError("invalid_request", `${name} must be a boolean`, 400);
201
+ return value;
202
+ }
203
+ function integerField(value, name) {
204
+ if (!Number.isInteger(value))
205
+ throw new BoardStoreError("invalid_request", `${name} must be an integer`, 400);
206
+ return value;
207
+ }
208
+ function revisionField(value) {
209
+ const revision = integerField(value, "expectedRevision");
210
+ if (revision < 0)
211
+ throw new BoardStoreError("invalid_request", "expectedRevision must be non-negative", 400);
212
+ return revision;
213
+ }
@@ -0,0 +1,112 @@
1
+ import { readdir, realpath, stat } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join, parse, resolve } from "node:path";
4
+ import { BoardStoreError } from "../repositories/board-repository.js";
5
+ export class NodeFilesystemBrowser {
6
+ defaultDirectory;
7
+ homeDirectory;
8
+ platform;
9
+ constructor(options = {}) {
10
+ this.defaultDirectory = options.defaultDirectory ?? process.cwd();
11
+ this.homeDirectory = options.homeDirectory ?? homedir();
12
+ this.platform = options.platform ?? process.platform;
13
+ }
14
+ async listLocations() {
15
+ const current = await this.validateDirectory(this.defaultDirectory);
16
+ const home = await this.validateDirectory(this.homeDirectory);
17
+ const locations = [
18
+ { label: "当前工作目录", path: current, kind: "current" },
19
+ { label: "用户目录", path: home, kind: "home" },
20
+ ];
21
+ if (this.platform === "win32") {
22
+ const drives = await availableWindowsDrives();
23
+ for (const drive of drives) {
24
+ if (!locations.some((location) => samePath(location.path, drive, this.platform))) {
25
+ locations.push({ label: drive, path: drive, kind: "drive" });
26
+ }
27
+ }
28
+ }
29
+ else if (!locations.some((location) => location.path === "/")) {
30
+ locations.push({ label: "根目录", path: "/", kind: "root" });
31
+ }
32
+ return locations;
33
+ }
34
+ async listDirectories(inputPath, showHidden) {
35
+ const canonical = await this.validateDirectory(inputPath);
36
+ let children;
37
+ try {
38
+ children = await readdir(canonical, { withFileTypes: true });
39
+ }
40
+ catch (err) {
41
+ throw filesystemError(err, `无法读取目录:${canonical}`);
42
+ }
43
+ const entries = (await Promise.all(children.map(async (child) => {
44
+ const hidden = child.name.startsWith(".");
45
+ if (hidden && !showHidden)
46
+ return null;
47
+ const childPath = join(canonical, child.name);
48
+ if (child.isDirectory())
49
+ return { name: child.name, path: childPath, hidden };
50
+ if (!child.isSymbolicLink())
51
+ return null;
52
+ try {
53
+ return (await stat(childPath)).isDirectory() ? { name: child.name, path: childPath, hidden } : null;
54
+ }
55
+ catch {
56
+ return null;
57
+ }
58
+ }))).filter((entry) => entry !== null);
59
+ entries.sort((a, b) => a.name.localeCompare(b.name, "zh-CN", { numeric: true, sensitivity: "base" }));
60
+ const parent = dirname(canonical);
61
+ return { path: canonical, parentPath: samePath(parent, canonical, this.platform) ? null : parent, entries };
62
+ }
63
+ async validateDirectory(inputPath) {
64
+ if (typeof inputPath !== "string" || !inputPath.trim()) {
65
+ throw new BoardStoreError("invalid_request", "目录路径不能为空", 400);
66
+ }
67
+ const absolute = resolve(inputPath.trim());
68
+ try {
69
+ const info = await stat(absolute);
70
+ if (!info.isDirectory())
71
+ throw new BoardStoreError("invalid_request", `不是目录:${absolute}`, 400);
72
+ return await realpath(absolute);
73
+ }
74
+ catch (err) {
75
+ if (err instanceof BoardStoreError)
76
+ throw err;
77
+ throw filesystemError(err, `目录不存在或无法访问:${absolute}`);
78
+ }
79
+ }
80
+ }
81
+ async function availableWindowsDrives() {
82
+ const candidates = Array.from({ length: 26 }, (_, index) => `${String.fromCharCode(65 + index)}:\\`);
83
+ const results = await Promise.all(candidates.map(async (drive) => await isDriveAvailable(drive) ? drive : null));
84
+ return results.filter((drive) => drive !== null);
85
+ }
86
+ function isDriveAvailable(drive) {
87
+ return new Promise((resolveAvailability) => {
88
+ let settled = false;
89
+ const finish = (available) => {
90
+ if (settled)
91
+ return;
92
+ settled = true;
93
+ clearTimeout(timeout);
94
+ resolveAvailability(available);
95
+ };
96
+ const timeout = setTimeout(() => finish(false), 350);
97
+ stat(drive).then((info) => finish(info.isDirectory())).catch(() => finish(false));
98
+ });
99
+ }
100
+ function samePath(left, right, platform) {
101
+ const normalize = (value) => {
102
+ const root = parse(value).root;
103
+ const trimmed = value === root ? value : value.replace(/[\\/]+$/, "");
104
+ return platform === "win32" ? trimmed.toLocaleLowerCase("en-US") : trimmed;
105
+ };
106
+ return normalize(left) === normalize(right);
107
+ }
108
+ function filesystemError(err, message) {
109
+ const code = err?.code;
110
+ const detail = code === "EACCES" || code === "EPERM" ? "(权限不足)" : "";
111
+ return new BoardStoreError("invalid_request", `${message}${detail}`, 400);
112
+ }
@@ -0,0 +1,30 @@
1
+ import { sessionPrefixForTool, setDefaultCwd } from "../../config.js";
2
+ import { isSessionRunning } from "../../session-chat-binding.js";
3
+ import { initClaudeSession, recordChatPlatform, switchChatBinding, } from "../../session.js";
4
+ export function createMainAgentSessionRuntime(platform) {
5
+ return {
6
+ createSession(agentId, cwd) {
7
+ return initClaudeSession(agentId, cwd);
8
+ },
9
+ isSessionRunning,
10
+ async bindSession(input) {
11
+ const description = `${sessionPrefixForTool(input.agentId)} ${input.newSessionId}`;
12
+ const result = await switchChatBinding({
13
+ chatId: input.chatId,
14
+ chatType: "group",
15
+ oldSessionId: input.oldSessionId,
16
+ newSessionId: input.newSessionId,
17
+ tool: input.agentId,
18
+ chatName: input.chatName,
19
+ namePolicy: "fixed",
20
+ newDescription: description,
21
+ updateChatInfoFn: (chatId, name, nextDescription) => platform.updateChatInfo(chatId, name, nextDescription),
22
+ });
23
+ if (!result.ok)
24
+ throw result.error ?? new Error("Failed to bind main Agent session");
25
+ await setDefaultCwd(input.cwd, input.chatId);
26
+ recordChatPlatform(input.chatId, platform);
27
+ await platform.setChatAvatar(input.chatId, input.agentId, "new").catch(() => { });
28
+ },
29
+ };
30
+ }
@@ -0,0 +1,15 @@
1
+ import { MainAgentService } from "./application/main-agent-service.js";
2
+ import { defaultAgentTeamBoardService, setDefaultAgentTeamMainAgentService, } from "./http/board-routes.js";
3
+ import { createMainAgentSessionRuntime } from "./infrastructure/main-agent-session-runtime.js";
4
+ import { feishuP2pContactStore } from "./repositories/feishu-p2p-contact-store.js";
5
+ import { JsonMainAgentBindingRepository } from "./repositories/main-agent-binding-repository.js";
6
+ /** Wire runtime dependencies only from index.ts, keeping the standalone Web UI import side-effect free. */
7
+ export function configureAgentTeamMainAgent(platform) {
8
+ setDefaultAgentTeamMainAgentService(new MainAgentService({
9
+ boardService: defaultAgentTeamBoardService,
10
+ bindingRepository: new JsonMainAgentBindingRepository(),
11
+ contactStore: feishuP2pContactStore,
12
+ platform,
13
+ runtime: createMainAgentSessionRuntime(platform),
14
+ }));
15
+ }
@@ -0,0 +1,10 @@
1
+ export class BoardStoreError extends Error {
2
+ code;
3
+ status;
4
+ constructor(code, message, status) {
5
+ super(message);
6
+ this.code = code;
7
+ this.status = status;
8
+ this.name = "BoardStoreError";
9
+ }
10
+ }
@@ -0,0 +1,64 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ export class FeishuP2pContactStore {
6
+ filePath;
7
+ operationQueue = Promise.resolve();
8
+ constructor(options = {}) {
9
+ this.filePath = options.filePath ?? join(homedir(), ".chatccc", "state", "last-feishu-p2p-contact.json");
10
+ }
11
+ async get() {
12
+ try {
13
+ return parseContact(JSON.parse(await readFile(this.filePath, "utf8")));
14
+ }
15
+ catch (err) {
16
+ if (err.code === "ENOENT")
17
+ return null;
18
+ throw err;
19
+ }
20
+ }
21
+ async record(contact) {
22
+ const parsed = parseContact(contact);
23
+ await this.exclusive(async () => {
24
+ await writeJsonAtomic(this.filePath, parsed);
25
+ });
26
+ }
27
+ async exclusive(operation) {
28
+ const previous = this.operationQueue;
29
+ let release;
30
+ this.operationQueue = new Promise((resolveQueue) => { release = resolveQueue; });
31
+ await previous;
32
+ try {
33
+ return await operation();
34
+ }
35
+ finally {
36
+ release();
37
+ }
38
+ }
39
+ }
40
+ function parseContact(value) {
41
+ if (!value || typeof value !== "object" || Array.isArray(value))
42
+ throw new Error("Invalid Feishu P2P contact");
43
+ const contact = value;
44
+ if (typeof contact.openId !== "string" || !contact.openId)
45
+ throw new Error("Feishu P2P contact is missing openId");
46
+ if (typeof contact.chatId !== "string" || !contact.chatId)
47
+ throw new Error("Feishu P2P contact is missing chatId");
48
+ if (typeof contact.receivedAt !== "string" || !contact.receivedAt)
49
+ throw new Error("Feishu P2P contact is missing receivedAt");
50
+ return { openId: contact.openId, chatId: contact.chatId, receivedAt: contact.receivedAt };
51
+ }
52
+ async function writeJsonAtomic(path, value) {
53
+ await mkdir(dirname(path), { recursive: true });
54
+ const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
55
+ await writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
56
+ try {
57
+ await rename(tempPath, path);
58
+ }
59
+ catch (err) {
60
+ await unlink(tempPath).catch(() => { });
61
+ throw err;
62
+ }
63
+ }
64
+ export const feishuP2pContactStore = new FeishuP2pContactStore();
@@ -0,0 +1,209 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { existsSync } from "node:fs";
3
+ import { mkdir, readFile, realpath, rename, stat, unlink, writeFile } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import { dirname, join, resolve } from "node:path";
6
+ import { createEmptyBoard, parseBoard } from "../domain/board.js";
7
+ import { BoardStoreError } from "./board-repository.js";
8
+ const EMPTY_INDEX = { schemaVersion: 1, workspaces: [] };
9
+ export class JsonBoardRepository {
10
+ rootDir;
11
+ boardsDir;
12
+ indexPath;
13
+ now;
14
+ idFactory;
15
+ operationQueue = Promise.resolve();
16
+ constructor(options = {}) {
17
+ this.rootDir = options.rootDir ?? join(homedir(), ".chatccc", "agent-team");
18
+ this.boardsDir = join(this.rootDir, "boards");
19
+ this.indexPath = join(this.rootDir, "workspaces.json");
20
+ this.now = options.now ?? (() => new Date());
21
+ this.idFactory = options.idFactory ?? randomUUID;
22
+ }
23
+ async findWorkspace(workspacePath) {
24
+ const canonical = await this.canonicalWorkspace(workspacePath);
25
+ const normalizedPath = normalizeWorkspaceKey(canonical);
26
+ const index = await this.readIndex();
27
+ const record = index.workspaces.find((item) => item.normalizedPath === normalizedPath);
28
+ return record ? this.readBoard(record.boardId) : null;
29
+ }
30
+ async openWorkspace(workspacePath) {
31
+ return this.exclusive(async () => {
32
+ const canonical = await this.canonicalWorkspace(workspacePath);
33
+ const normalizedPath = normalizeWorkspaceKey(canonical);
34
+ const index = await this.readIndex();
35
+ let record = index.workspaces.find((item) => item.normalizedPath === normalizedPath);
36
+ const now = this.now().toISOString();
37
+ if (!record) {
38
+ const board = createEmptyBoard({ boardId: this.idFactory(), workspacePath: canonical, now });
39
+ record = { boardId: board.boardId, workspacePath: canonical, normalizedPath, lastOpenedAt: now };
40
+ index.workspaces.unshift(record);
41
+ await this.writeBoard(board);
42
+ await this.writeIndex(index);
43
+ return board;
44
+ }
45
+ record.workspacePath = canonical;
46
+ record.lastOpenedAt = now;
47
+ await this.writeIndex(index);
48
+ const board = await this.readBoard(record.boardId);
49
+ if (board.workspacePath !== canonical) {
50
+ const updated = { ...board, workspacePath: canonical, revision: board.revision + 1, updatedAt: now };
51
+ await this.writeBoard(updated);
52
+ return updated;
53
+ }
54
+ return board;
55
+ });
56
+ }
57
+ async getBoard(boardId) {
58
+ return this.readBoard(boardId);
59
+ }
60
+ async saveBoard(board, expectedRevision) {
61
+ await this.exclusive(async () => {
62
+ const current = await this.readBoard(board.boardId);
63
+ if (current.revision !== expectedRevision)
64
+ throw revisionConflict(current.revision, expectedRevision);
65
+ if (board.revision !== expectedRevision + 1) {
66
+ throw new BoardStoreError("invalid_request", "Board revision must increase by exactly one", 400);
67
+ }
68
+ parseBoard(board);
69
+ await this.writeBoard(board);
70
+ });
71
+ }
72
+ async listWorkspaces() {
73
+ const index = await this.readIndex();
74
+ const sorted = [...index.workspaces].sort((a, b) => b.lastOpenedAt.localeCompare(a.lastOpenedAt)).slice(0, 20);
75
+ return Promise.all(sorted.map(async (record) => ({
76
+ boardId: record.boardId,
77
+ workspacePath: record.workspacePath,
78
+ lastOpenedAt: record.lastOpenedAt,
79
+ exists: await directoryExists(record.workspacePath),
80
+ })));
81
+ }
82
+ async relinkWorkspace(boardId, workspacePath, expectedRevision) {
83
+ return this.exclusive(async () => {
84
+ const canonical = await this.canonicalWorkspace(workspacePath);
85
+ const normalizedPath = normalizeWorkspaceKey(canonical);
86
+ const index = await this.readIndex();
87
+ const collision = index.workspaces.find((item) => item.normalizedPath === normalizedPath && item.boardId !== boardId);
88
+ if (collision) {
89
+ throw new BoardStoreError("workspace_conflict", "The selected directory already has another board", 409);
90
+ }
91
+ const current = await this.readBoard(boardId);
92
+ if (current.revision !== expectedRevision)
93
+ throw revisionConflict(current.revision, expectedRevision);
94
+ const now = this.now().toISOString();
95
+ const record = index.workspaces.find((item) => item.boardId === boardId);
96
+ if (!record)
97
+ throw new BoardStoreError("not_found", `Workspace record not found for board ${boardId}`, 404);
98
+ record.workspacePath = canonical;
99
+ record.normalizedPath = normalizedPath;
100
+ record.lastOpenedAt = now;
101
+ const updated = {
102
+ ...current,
103
+ workspacePath: canonical,
104
+ revision: current.revision + 1,
105
+ updatedAt: now,
106
+ };
107
+ await this.writeBoard(updated);
108
+ await this.writeIndex(index);
109
+ return updated;
110
+ });
111
+ }
112
+ async canonicalWorkspace(input) {
113
+ if (typeof input !== "string" || !input.trim()) {
114
+ throw new BoardStoreError("invalid_request", "workspacePath must be a non-empty string", 400);
115
+ }
116
+ const absolute = resolve(input.trim());
117
+ let info;
118
+ try {
119
+ info = await stat(absolute);
120
+ }
121
+ catch {
122
+ throw new BoardStoreError("invalid_request", `Working directory does not exist: ${absolute}`, 400);
123
+ }
124
+ if (!info.isDirectory())
125
+ throw new BoardStoreError("invalid_request", `Not a directory: ${absolute}`, 400);
126
+ return realpath(absolute);
127
+ }
128
+ boardPath(boardId) {
129
+ if (!/^[a-zA-Z0-9_-]+$/.test(boardId)) {
130
+ throw new BoardStoreError("invalid_request", "Invalid board id", 400);
131
+ }
132
+ return join(this.boardsDir, `${boardId}.json`);
133
+ }
134
+ async readBoard(boardId) {
135
+ const path = this.boardPath(boardId);
136
+ try {
137
+ return parseBoard(JSON.parse(await readFile(path, "utf8")));
138
+ }
139
+ catch (err) {
140
+ if (err.code === "ENOENT") {
141
+ throw new BoardStoreError("not_found", `Board not found: ${boardId}`, 404);
142
+ }
143
+ if (err instanceof BoardStoreError)
144
+ throw err;
145
+ throw new BoardStoreError("storage_error", `Failed to read board ${boardId}: ${err.message}`, 500);
146
+ }
147
+ }
148
+ async readIndex() {
149
+ if (!existsSync(this.indexPath))
150
+ return { ...EMPTY_INDEX, workspaces: [] };
151
+ try {
152
+ const parsed = JSON.parse(await readFile(this.indexPath, "utf8"));
153
+ if (parsed.schemaVersion !== 1 || !Array.isArray(parsed.workspaces))
154
+ throw new Error("Invalid workspace index");
155
+ return { schemaVersion: 1, workspaces: parsed.workspaces };
156
+ }
157
+ catch (err) {
158
+ throw new BoardStoreError("storage_error", `Failed to read workspace index: ${err.message}`, 500);
159
+ }
160
+ }
161
+ async writeBoard(board) {
162
+ await writeJsonAtomic(this.boardPath(board.boardId), board);
163
+ }
164
+ async writeIndex(index) {
165
+ const deduped = new Map();
166
+ for (const record of index.workspaces)
167
+ deduped.set(record.boardId, record);
168
+ await writeJsonAtomic(this.indexPath, { schemaVersion: 1, workspaces: [...deduped.values()] });
169
+ }
170
+ async exclusive(operation) {
171
+ const previous = this.operationQueue;
172
+ let release;
173
+ this.operationQueue = new Promise((resolveQueue) => { release = resolveQueue; });
174
+ await previous;
175
+ try {
176
+ return await operation();
177
+ }
178
+ finally {
179
+ release();
180
+ }
181
+ }
182
+ }
183
+ function normalizeWorkspaceKey(path) {
184
+ const normalized = resolve(path).replace(/[\\/]+$/, "");
185
+ return process.platform === "win32" ? normalized.toLocaleLowerCase("en-US") : normalized;
186
+ }
187
+ async function directoryExists(path) {
188
+ try {
189
+ return (await stat(path)).isDirectory();
190
+ }
191
+ catch {
192
+ return false;
193
+ }
194
+ }
195
+ async function writeJsonAtomic(path, value) {
196
+ await mkdir(dirname(path), { recursive: true });
197
+ const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
198
+ await writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
199
+ try {
200
+ await rename(tempPath, path);
201
+ }
202
+ catch (err) {
203
+ await unlink(tempPath).catch(() => { });
204
+ throw err;
205
+ }
206
+ }
207
+ function revisionConflict(actual, expected) {
208
+ return new BoardStoreError("revision_conflict", `Board changed in another page (expected revision ${expected}, current ${actual})`, 409);
209
+ }