@mclawnet/swarm 0.1.4 → 0.1.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.
Files changed (240) hide show
  1. package/README.md +118 -0
  2. package/dist/__tests__/action-parser.test.js +29 -82
  3. package/dist/__tests__/action-parser.test.js.map +1 -1
  4. package/dist/__tests__/coordinator-create-tx.test.d.ts +2 -0
  5. package/dist/__tests__/coordinator-create-tx.test.d.ts.map +1 -0
  6. package/dist/__tests__/coordinator-create-tx.test.js +114 -0
  7. package/dist/__tests__/coordinator-create-tx.test.js.map +1 -0
  8. package/dist/__tests__/coordinator-inbox-migration.test.d.ts +2 -0
  9. package/dist/__tests__/coordinator-inbox-migration.test.d.ts.map +1 -0
  10. package/dist/__tests__/coordinator-inbox-migration.test.js +56 -0
  11. package/dist/__tests__/coordinator-inbox-migration.test.js.map +1 -0
  12. package/dist/__tests__/inbox-integration.test.d.ts +2 -0
  13. package/dist/__tests__/inbox-integration.test.d.ts.map +1 -0
  14. package/dist/__tests__/inbox-integration.test.js +120 -0
  15. package/dist/__tests__/inbox-integration.test.js.map +1 -0
  16. package/dist/__tests__/inbox-persistence-recovery.test.d.ts +2 -0
  17. package/dist/__tests__/inbox-persistence-recovery.test.d.ts.map +1 -0
  18. package/dist/__tests__/inbox-persistence-recovery.test.js +139 -0
  19. package/dist/__tests__/inbox-persistence-recovery.test.js.map +1 -0
  20. package/dist/__tests__/inbox-relay-interceptor.test.d.ts +2 -0
  21. package/dist/__tests__/inbox-relay-interceptor.test.d.ts.map +1 -0
  22. package/dist/__tests__/inbox-relay-interceptor.test.js +156 -0
  23. package/dist/__tests__/inbox-relay-interceptor.test.js.map +1 -0
  24. package/dist/__tests__/inbox-relay.test.d.ts +2 -0
  25. package/dist/__tests__/inbox-relay.test.d.ts.map +1 -0
  26. package/dist/__tests__/inbox-relay.test.js +318 -0
  27. package/dist/__tests__/inbox-relay.test.js.map +1 -0
  28. package/dist/__tests__/inbox-store.test.d.ts +2 -0
  29. package/dist/__tests__/inbox-store.test.d.ts.map +1 -0
  30. package/dist/__tests__/inbox-store.test.js +129 -0
  31. package/dist/__tests__/inbox-store.test.js.map +1 -0
  32. package/dist/__tests__/inbox-watcher.test.d.ts +2 -0
  33. package/dist/__tests__/inbox-watcher.test.d.ts.map +1 -0
  34. package/dist/__tests__/inbox-watcher.test.js +104 -0
  35. package/dist/__tests__/inbox-watcher.test.js.map +1 -0
  36. package/dist/__tests__/persistence-path.test.d.ts +2 -0
  37. package/dist/__tests__/persistence-path.test.d.ts.map +1 -0
  38. package/dist/__tests__/persistence-path.test.js +79 -0
  39. package/dist/__tests__/persistence-path.test.js.map +1 -0
  40. package/dist/__tests__/persistence-robust.test.d.ts +2 -0
  41. package/dist/__tests__/persistence-robust.test.d.ts.map +1 -0
  42. package/dist/__tests__/persistence-robust.test.js +125 -0
  43. package/dist/__tests__/persistence-robust.test.js.map +1 -0
  44. package/dist/__tests__/persistence.test.d.ts +2 -0
  45. package/dist/__tests__/persistence.test.d.ts.map +1 -0
  46. package/dist/__tests__/persistence.test.js +105 -0
  47. package/dist/__tests__/persistence.test.js.map +1 -0
  48. package/dist/__tests__/phase4-5-e2e.test.d.ts +2 -0
  49. package/dist/__tests__/phase4-5-e2e.test.d.ts.map +1 -0
  50. package/dist/__tests__/phase4-5-e2e.test.js +203 -0
  51. package/dist/__tests__/phase4-5-e2e.test.js.map +1 -0
  52. package/dist/__tests__/phase6-7-e2e.test.d.ts +2 -0
  53. package/dist/__tests__/phase6-7-e2e.test.d.ts.map +1 -0
  54. package/dist/__tests__/phase6-7-e2e.test.js +93 -0
  55. package/dist/__tests__/phase6-7-e2e.test.js.map +1 -0
  56. package/dist/__tests__/project-files.test.d.ts +2 -0
  57. package/dist/__tests__/project-files.test.d.ts.map +1 -0
  58. package/dist/__tests__/project-files.test.js +143 -0
  59. package/dist/__tests__/project-files.test.js.map +1 -0
  60. package/dist/__tests__/projects-fs.test.d.ts +2 -0
  61. package/dist/__tests__/projects-fs.test.d.ts.map +1 -0
  62. package/dist/__tests__/projects-fs.test.js +107 -0
  63. package/dist/__tests__/projects-fs.test.js.map +1 -0
  64. package/dist/__tests__/recovery-cross-project.test.d.ts +2 -0
  65. package/dist/__tests__/recovery-cross-project.test.d.ts.map +1 -0
  66. package/dist/__tests__/recovery-cross-project.test.js +87 -0
  67. package/dist/__tests__/recovery-cross-project.test.js.map +1 -0
  68. package/dist/__tests__/recovery-forwards-to-coordinator.test.d.ts +2 -0
  69. package/dist/__tests__/recovery-forwards-to-coordinator.test.d.ts.map +1 -0
  70. package/dist/__tests__/recovery-forwards-to-coordinator.test.js +59 -0
  71. package/dist/__tests__/recovery-forwards-to-coordinator.test.js.map +1 -0
  72. package/dist/__tests__/recovery-resume.test.d.ts +2 -0
  73. package/dist/__tests__/recovery-resume.test.d.ts.map +1 -0
  74. package/dist/__tests__/recovery-resume.test.js +132 -0
  75. package/dist/__tests__/recovery-resume.test.js.map +1 -0
  76. package/dist/__tests__/retrospective.test.js +1 -0
  77. package/dist/__tests__/retrospective.test.js.map +1 -1
  78. package/dist/__tests__/role-loader-preamble-all.test.d.ts +2 -0
  79. package/dist/__tests__/role-loader-preamble-all.test.d.ts.map +1 -0
  80. package/dist/__tests__/role-loader-preamble-all.test.js +38 -0
  81. package/dist/__tests__/role-loader-preamble-all.test.js.map +1 -0
  82. package/dist/__tests__/role-loader-tools.test.d.ts +2 -0
  83. package/dist/__tests__/role-loader-tools.test.d.ts.map +1 -0
  84. package/dist/__tests__/role-loader-tools.test.js +39 -0
  85. package/dist/__tests__/role-loader-tools.test.js.map +1 -0
  86. package/dist/__tests__/role-loader.test.js +116 -1
  87. package/dist/__tests__/role-loader.test.js.map +1 -1
  88. package/dist/__tests__/role-prompt-no-legacy-protocol.test.d.ts +2 -0
  89. package/dist/__tests__/role-prompt-no-legacy-protocol.test.d.ts.map +1 -0
  90. package/dist/__tests__/role-prompt-no-legacy-protocol.test.js +37 -0
  91. package/dist/__tests__/role-prompt-no-legacy-protocol.test.js.map +1 -0
  92. package/dist/__tests__/role-tools.test.d.ts +2 -0
  93. package/dist/__tests__/role-tools.test.d.ts.map +1 -0
  94. package/dist/__tests__/role-tools.test.js +80 -0
  95. package/dist/__tests__/role-tools.test.js.map +1 -0
  96. package/dist/__tests__/spawn-role-injects-briefings.test.d.ts +2 -0
  97. package/dist/__tests__/spawn-role-injects-briefings.test.d.ts.map +1 -0
  98. package/dist/__tests__/spawn-role-injects-briefings.test.js +182 -0
  99. package/dist/__tests__/spawn-role-injects-briefings.test.js.map +1 -0
  100. package/dist/__tests__/spawn-role-tool-policy.test.d.ts +2 -0
  101. package/dist/__tests__/spawn-role-tool-policy.test.d.ts.map +1 -0
  102. package/dist/__tests__/spawn-role-tool-policy.test.js +96 -0
  103. package/dist/__tests__/spawn-role-tool-policy.test.js.map +1 -0
  104. package/dist/__tests__/swarm-coordinator-inbox-watcher.test.d.ts +2 -0
  105. package/dist/__tests__/swarm-coordinator-inbox-watcher.test.d.ts.map +1 -0
  106. package/dist/__tests__/swarm-coordinator-inbox-watcher.test.js +61 -0
  107. package/dist/__tests__/swarm-coordinator-inbox-watcher.test.js.map +1 -0
  108. package/dist/__tests__/swarm-coordinator-inbox.test.d.ts +2 -0
  109. package/dist/__tests__/swarm-coordinator-inbox.test.d.ts.map +1 -0
  110. package/dist/__tests__/swarm-coordinator-inbox.test.js +182 -0
  111. package/dist/__tests__/swarm-coordinator-inbox.test.js.map +1 -0
  112. package/dist/__tests__/swarm-coordinator-init.test.js +36 -8
  113. package/dist/__tests__/swarm-coordinator-init.test.js.map +1 -1
  114. package/dist/__tests__/swarm-coordinator-legacy-plan-review-warn.test.d.ts +2 -0
  115. package/dist/__tests__/swarm-coordinator-legacy-plan-review-warn.test.d.ts.map +1 -0
  116. package/dist/__tests__/swarm-coordinator-legacy-plan-review-warn.test.js +113 -0
  117. package/dist/__tests__/swarm-coordinator-legacy-plan-review-warn.test.js.map +1 -0
  118. package/dist/__tests__/swarm-coordinator-plan-review-intercept.test.d.ts +2 -0
  119. package/dist/__tests__/swarm-coordinator-plan-review-intercept.test.d.ts.map +1 -0
  120. package/dist/__tests__/swarm-coordinator-plan-review-intercept.test.js +465 -0
  121. package/dist/__tests__/swarm-coordinator-plan-review-intercept.test.js.map +1 -0
  122. package/dist/__tests__/swarm-coordinator-plan-review-recovery.test.d.ts +2 -0
  123. package/dist/__tests__/swarm-coordinator-plan-review-recovery.test.d.ts.map +1 -0
  124. package/dist/__tests__/swarm-coordinator-plan-review-recovery.test.js +284 -0
  125. package/dist/__tests__/swarm-coordinator-plan-review-recovery.test.js.map +1 -0
  126. package/dist/__tests__/swarm-coordinator-plan-review.test.d.ts +2 -0
  127. package/dist/__tests__/swarm-coordinator-plan-review.test.d.ts.map +1 -0
  128. package/dist/__tests__/swarm-coordinator-plan-review.test.js +294 -0
  129. package/dist/__tests__/swarm-coordinator-plan-review.test.js.map +1 -0
  130. package/dist/__tests__/swarm-coordinator-resume.test.d.ts +2 -0
  131. package/dist/__tests__/swarm-coordinator-resume.test.d.ts.map +1 -0
  132. package/dist/__tests__/swarm-coordinator-resume.test.js +93 -0
  133. package/dist/__tests__/swarm-coordinator-resume.test.js.map +1 -0
  134. package/dist/__tests__/swarm-coordinator-roleId.test.js +2 -2
  135. package/dist/__tests__/swarm-coordinator-roleId.test.js.map +1 -1
  136. package/dist/__tests__/swarm-destroy-detach.test.d.ts +2 -0
  137. package/dist/__tests__/swarm-destroy-detach.test.d.ts.map +1 -0
  138. package/dist/__tests__/swarm-destroy-detach.test.js +135 -0
  139. package/dist/__tests__/swarm-destroy-detach.test.js.map +1 -0
  140. package/dist/action-parser.d.ts +0 -9
  141. package/dist/action-parser.d.ts.map +1 -1
  142. package/dist/action-parser.js +0 -114
  143. package/dist/action-parser.js.map +1 -1
  144. package/dist/inbox-relay.d.ts +50 -0
  145. package/dist/inbox-relay.d.ts.map +1 -0
  146. package/dist/inbox-relay.js +168 -0
  147. package/dist/inbox-relay.js.map +1 -0
  148. package/dist/inbox-store.d.ts +25 -0
  149. package/dist/inbox-store.d.ts.map +1 -0
  150. package/dist/inbox-store.js +95 -0
  151. package/dist/inbox-store.js.map +1 -0
  152. package/dist/inbox-watcher.d.ts +13 -0
  153. package/dist/inbox-watcher.d.ts.map +1 -0
  154. package/dist/inbox-watcher.js +89 -0
  155. package/dist/inbox-watcher.js.map +1 -0
  156. package/dist/index.d.ts +8 -3
  157. package/dist/index.d.ts.map +1 -1
  158. package/dist/index.js +5 -2
  159. package/dist/index.js.map +1 -1
  160. package/dist/persistence.d.ts +19 -5
  161. package/dist/persistence.d.ts.map +1 -1
  162. package/dist/persistence.js +97 -22
  163. package/dist/persistence.js.map +1 -1
  164. package/dist/project-files.d.ts +60 -0
  165. package/dist/project-files.d.ts.map +1 -0
  166. package/dist/project-files.js +214 -0
  167. package/dist/project-files.js.map +1 -0
  168. package/dist/projects-fs.d.ts +28 -0
  169. package/dist/projects-fs.d.ts.map +1 -0
  170. package/dist/projects-fs.js +111 -0
  171. package/dist/projects-fs.js.map +1 -0
  172. package/dist/recovery.d.ts +12 -0
  173. package/dist/recovery.d.ts.map +1 -1
  174. package/dist/recovery.js +14 -19
  175. package/dist/recovery.js.map +1 -1
  176. package/dist/roles/role-loader.d.ts +28 -1
  177. package/dist/roles/role-loader.d.ts.map +1 -1
  178. package/dist/roles/role-loader.js +73 -1
  179. package/dist/roles/role-loader.js.map +1 -1
  180. package/dist/roles/role-tools.d.ts +16 -0
  181. package/dist/roles/role-tools.d.ts.map +1 -0
  182. package/dist/roles/role-tools.js +25 -0
  183. package/dist/roles/role-tools.js.map +1 -0
  184. package/dist/roles/types.d.ts +4 -0
  185. package/dist/roles/types.d.ts.map +1 -1
  186. package/dist/swarm-coordinator.d.ts +176 -12
  187. package/dist/swarm-coordinator.d.ts.map +1 -1
  188. package/dist/swarm-coordinator.js +863 -370
  189. package/dist/swarm-coordinator.js.map +1 -1
  190. package/dist/types.d.ts +26 -0
  191. package/dist/types.d.ts.map +1 -1
  192. package/package.json +9 -6
  193. package/roles/analyst-livermore.md +6 -30
  194. package/roles/designer-rams.md +2 -30
  195. package/roles/dev-torvalds.md +8 -44
  196. package/roles/developer.md +5 -21
  197. package/roles/director-jia.md +20 -49
  198. package/roles/editor-boyong.md +8 -40
  199. package/roles/macro-dalio.md +6 -30
  200. package/roles/planner-maoni.md +24 -53
  201. package/roles/pm-jobs.md +20 -71
  202. package/roles/preset-analyst-simons.md +2 -18
  203. package/roles/preset-architect-knuth.md +2 -18
  204. package/roles/preset-designer-norman.md +2 -18
  205. package/roles/preset-designer.md +2 -18
  206. package/roles/preset-dev-carmack.md +2 -18
  207. package/roles/preset-dev-gosling.md +2 -18
  208. package/roles/preset-developer.md +7 -23
  209. package/roles/preset-manager-grove.md +2 -18
  210. package/roles/preset-manager-musk.md +2 -18
  211. package/roles/preset-pm.md +7 -34
  212. package/roles/preset-researcher-feynman.md +2 -18
  213. package/roles/preset-reviewer.md +5 -21
  214. package/roles/preset-strategist-buffett.md +2 -18
  215. package/roles/preset-strategist-munger.md +2 -18
  216. package/roles/preset-strategist-sunzi.md +2 -18
  217. package/roles/preset-tester-beck.md +2 -18
  218. package/roles/preset-tester.md +5 -21
  219. package/roles/preset-writer-orwell.md +2 -18
  220. package/roles/preset-writer.md +2 -18
  221. package/roles/quant-simons.md +5 -32
  222. package/roles/queen.md +25 -41
  223. package/roles/reviewer-martin.md +11 -37
  224. package/roles/reviewer.md +20 -21
  225. package/roles/rhythm-tangsan.md +5 -29
  226. package/roles/risk-taleb.md +4 -32
  227. package/roles/script-shitiesheng.md +8 -31
  228. package/roles/storyboard-xuke.md +9 -29
  229. package/roles/strategist-soros.md +16 -73
  230. package/roles/tester-beck.md +4 -40
  231. package/roles/tester.md +5 -21
  232. package/roles/trader-jones.md +4 -32
  233. package/roles/vfx-guchangwei.md +8 -27
  234. package/roles/writer-zhouzi.md +7 -39
  235. package/templates/dev-team-pro.md +4 -1
  236. package/templates/dev-team.md +3 -1
  237. package/templates/minimal.md +2 -1
  238. package/templates/trading-team.md +6 -1
  239. package/templates/video-team.md +4 -1
  240. package/templates/writing-team.md +4 -1
@@ -1,52 +1,89 @@
1
- import { writeFileSync, readFileSync, existsSync, mkdirSync, appendFileSync, rmSync, readdirSync, } from "node:fs";
1
+ import { writeFileSync, readFileSync, existsSync, mkdirSync, appendFileSync, rmSync, readdirSync, renameSync, } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { homedir } from "node:os";
4
- function getSwarmsDir() {
5
- return join(homedir(), ".clawnet", "swarms");
4
+ import { randomBytes } from "node:crypto";
5
+ import { projectRoot } from "@mclawnet/task";
6
+ import { createLogger } from "@mclawnet/logger";
7
+ const log = createLogger({ module: "swarm-persistence" });
8
+ function getHome() {
9
+ return process.env.CLAWNET_HOME ?? homedir();
6
10
  }
7
- function getSwarmDir(swarmId) {
8
- return join(getSwarmsDir(), swarmId);
11
+ function getSwarmsDir(workDir) {
12
+ return join(projectRoot(workDir, getHome()), "swarms");
13
+ }
14
+ function getSwarmDir(workDir, swarmId) {
15
+ return join(getSwarmsDir(workDir), swarmId);
9
16
  }
10
17
  export function saveSwarmSnapshot(swarm) {
11
- const dir = getSwarmDir(swarm.id);
18
+ if (!swarm.workDir) {
19
+ throw new Error("saveSwarmSnapshot requires swarm.workDir");
20
+ }
21
+ const dir = getSwarmDir(swarm.workDir, swarm.id);
12
22
  mkdirSync(dir, { recursive: true });
13
23
  const snapshot = {
14
24
  id: swarm.id,
15
25
  hubSessionId: swarm.hubSessionId,
16
26
  workDir: swarm.workDir,
27
+ teamName: swarm.teamName ?? "default",
17
28
  roles: Array.from(swarm.roles.values()).map((r) => ({
18
29
  instanceId: r.instanceId,
19
30
  roleName: r.roleName,
20
31
  status: r.status,
21
32
  currentTask: r.currentTask,
33
+ claudeSessionId: r.claudeSessionId,
34
+ type: r.definition?.type,
22
35
  })),
23
36
  plan: swarm.plan,
24
37
  nextInstanceSeq: Object.fromEntries(swarm.nextInstanceSeq),
25
38
  savedAt: Date.now(),
26
39
  status: swarm.status,
27
40
  planStatus: swarm.planStatus,
41
+ partialRecover: swarm.partialRecover,
28
42
  };
29
- writeFileSync(join(dir, "recovery.json"), JSON.stringify(snapshot, null, 2));
43
+ // Atomic write: write to a unique tmp file in the same dir, then rename
44
+ // over the target. rename(2) within a filesystem is atomic — readers either
45
+ // see the previous valid snapshot or the new one, never a half-written file.
46
+ const finalPath = join(dir, "recovery.json");
47
+ const tmpPath = join(dir, `recovery.${randomBytes(6).toString("hex")}.tmp`);
48
+ writeFileSync(tmpPath, JSON.stringify(snapshot, null, 2));
49
+ try {
50
+ renameSync(tmpPath, finalPath);
51
+ }
52
+ catch (err) {
53
+ // best-effort cleanup of the tmp file so we don't leak garbage on disk
54
+ try {
55
+ rmSync(tmpPath, { force: true });
56
+ }
57
+ catch {
58
+ // ignore
59
+ }
60
+ throw err;
61
+ }
30
62
  }
31
- export function loadSwarmSnapshot(swarmId) {
32
- const filePath = join(getSwarmDir(swarmId), "recovery.json");
63
+ export function loadSwarmSnapshot(workDir, swarmId) {
64
+ const filePath = join(getSwarmDir(workDir, swarmId), "recovery.json");
33
65
  if (!existsSync(filePath))
34
66
  return null;
35
67
  try {
36
- return JSON.parse(readFileSync(filePath, "utf-8"));
68
+ const raw = JSON.parse(readFileSync(filePath, "utf-8"));
69
+ return {
70
+ ...raw,
71
+ teamName: raw.teamName ?? "default",
72
+ };
37
73
  }
38
- catch {
74
+ catch (err) {
75
+ log.warn({ err: err instanceof Error ? err.message : String(err), filePath, swarmId, workDir }, "loadSwarmSnapshot: failed to parse recovery.json — treating as missing");
39
76
  return null;
40
77
  }
41
78
  }
42
- export function deleteSwarmSnapshot(swarmId) {
43
- const recoveryPath = join(getSwarmDir(swarmId), "recovery.json");
79
+ export function deleteSwarmSnapshot(workDir, swarmId) {
80
+ const recoveryPath = join(getSwarmDir(workDir, swarmId), "recovery.json");
44
81
  if (existsSync(recoveryPath)) {
45
82
  rmSync(recoveryPath);
46
83
  }
47
84
  }
48
- export function readMessageLog(swarmId) {
49
- const filePath = join(getSwarmDir(swarmId), "messages.jsonl");
85
+ export function readMessageLog(workDir, swarmId) {
86
+ const filePath = join(getSwarmDir(workDir, swarmId), "messages.jsonl");
50
87
  if (!existsSync(filePath))
51
88
  return [];
52
89
  try {
@@ -68,17 +105,55 @@ export function readMessageLog(swarmId) {
68
105
  return [];
69
106
  }
70
107
  }
71
- export function appendMessageLog(swarmId, entry) {
72
- const dir = getSwarmDir(swarmId);
108
+ export function appendMessageLog(workDir, swarmId, entry) {
109
+ const dir = getSwarmDir(workDir, swarmId);
73
110
  mkdirSync(dir, { recursive: true });
74
111
  appendFileSync(join(dir, "messages.jsonl"), JSON.stringify(entry) + "\n");
75
112
  }
113
+ /**
114
+ * List recoverable swarms across all per-project roots. The authoritative
115
+ * `workDir` is read from each snapshot's `workDir` field (snapshots written
116
+ * since 0.1.6 always include it). Falling back on the encoded directory
117
+ * name is lossy for paths whose segments contain `-`, so we deliberately
118
+ * skip entries whose snapshot can't be parsed or doesn't carry `workDir`.
119
+ */
76
120
  export function listRecoverableSwarmIds() {
77
- const dir = getSwarmsDir();
78
- if (!existsSync(dir))
121
+ const projectsDir = join(getHome(), ".clawnet", "projects");
122
+ if (!existsSync(projectsDir))
79
123
  return [];
80
- return readdirSync(dir, { withFileTypes: true })
81
- .filter((e) => e.isDirectory() && existsSync(join(dir, e.name, "recovery.json")))
82
- .map((e) => e.name);
124
+ const result = [];
125
+ for (const projEntry of readdirSync(projectsDir, { withFileTypes: true })) {
126
+ if (!projEntry.isDirectory())
127
+ continue;
128
+ const swarmsDir = join(projectsDir, projEntry.name, "swarms");
129
+ if (!existsSync(swarmsDir))
130
+ continue;
131
+ for (const swarmEntry of readdirSync(swarmsDir, { withFileTypes: true })) {
132
+ if (!swarmEntry.isDirectory())
133
+ continue;
134
+ const swarmDir = join(swarmsDir, swarmEntry.name);
135
+ const recoveryPath = join(swarmDir, "recovery.json");
136
+ if (!existsSync(recoveryPath)) {
137
+ // Orphan: a swarm directory exists but was never persisted (likely
138
+ // an aborted SwarmCoordinator.create()). Surface it for the operator
139
+ // — never auto-delete (could destroy in-progress user state).
140
+ log.warn({ swarmDir, swarmId: swarmEntry.name, projectDir: projEntry.name }, "listRecoverableSwarmIds: swarm directory has no recovery.json — skipping (manual cleanup may be needed)");
141
+ continue;
142
+ }
143
+ try {
144
+ const raw = JSON.parse(readFileSync(recoveryPath, "utf-8"));
145
+ if (typeof raw.workDir === "string" && raw.workDir.length > 0) {
146
+ result.push({ workDir: raw.workDir, swarmId: swarmEntry.name });
147
+ }
148
+ else {
149
+ log.warn({ recoveryPath, swarmId: swarmEntry.name }, "listRecoverableSwarmIds: snapshot missing workDir field — skipping");
150
+ }
151
+ }
152
+ catch (err) {
153
+ log.warn({ err: err instanceof Error ? err.message : String(err), recoveryPath, swarmId: swarmEntry.name }, "listRecoverableSwarmIds: failed to parse recovery.json — skipping");
154
+ }
155
+ }
156
+ }
157
+ return result;
83
158
  }
84
159
  //# sourceMappingURL=persistence.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"persistence.js","sourceRoot":"","sources":["../src/persistence.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"persistence.js","sourceRoot":"","sources":["../src/persistence.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,YAAY,EACZ,UAAU,EACV,SAAS,EACT,cAAc,EACd,MAAM,EACN,WAAW,EACX,UAAU,GACX,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAGhD,MAAM,GAAG,GAAG,YAAY,CAAC,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC,CAAC;AAE1D,SAAS,OAAO;IACd,OAAO,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,OAAO,EAAE,CAAC;AAC/C,CAAC;AAED,SAAS,YAAY,CAAC,OAAe;IACnC,OAAO,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,QAAQ,CAAC,CAAC;AACzD,CAAC;AAED,SAAS,WAAW,CAAC,OAAe,EAAE,OAAe;IACnD,OAAO,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,CAAC;AAC9C,CAAC;AAuBD,MAAM,UAAU,iBAAiB,CAAC,KAAoB;IACpD,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;QACnB,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC;IAC9D,CAAC;IACD,MAAM,GAAG,GAAG,WAAW,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC;IACjD,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACpC,MAAM,QAAQ,GAAkB;QAC9B,EAAE,EAAE,KAAK,CAAC,EAAE;QACZ,YAAY,EAAE,KAAK,CAAC,YAAY;QAChC,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,QAAQ,EAAE,KAAK,CAAC,QAAQ,IAAI,SAAS;QACrC,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAClD,UAAU,EAAE,CAAC,CAAC,UAAU;YACxB,QAAQ,EAAE,CAAC,CAAC,QAAQ;YACpB,MAAM,EAAE,CAAC,CAAC,MAAM;YAChB,WAAW,EAAE,CAAC,CAAC,WAAW;YAC1B,eAAe,EAAE,CAAC,CAAC,eAAe;YAClC,IAAI,EAAE,CAAC,CAAC,UAAU,EAAE,IAAI;SACzB,CAAC,CAAC;QACH,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,eAAe,EAAE,MAAM,CAAC,WAAW,CAAC,KAAK,CAAC,eAAe,CAAC;QAC1D,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE;QACnB,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,cAAc,EAAE,KAAK,CAAC,cAAc;KACrC,CAAC;IACF,wEAAwE;IACxE,4EAA4E;IAC5E,6EAA6E;IAC7E,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,eAAe,CAAC,CAAC;IAC7C,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,YAAY,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAC5E,aAAa,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAC1D,IAAI,CAAC;QACH,UAAU,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IACjC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,uEAAuE;QACvE,IAAI,CAAC;YACH,MAAM,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACnC,CAAC;QAAC,MAAM,CAAC;YACP,SAAS;QACX,CAAC;QACD,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,OAAe,EAAE,OAAe;IAChE,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,eAAe,CAAC,CAAC;IACtE,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAC;IACvC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAA2B,CAAC;QAClF,OAAO;YACL,GAAI,GAAqB;YACzB,QAAQ,EAAE,GAAG,CAAC,QAAQ,IAAI,SAAS;SACpC,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,GAAG,CAAC,IAAI,CACN,EAAE,GAAG,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,EACrF,wEAAwE,CACzE,CAAC;QACF,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,OAAe,EAAE,OAAe;IAClE,MAAM,YAAY,GAAG,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,eAAe,CAAC,CAAC;IAC1E,IAAI,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;QAC7B,MAAM,CAAC,YAAY,CAAC,CAAC;IACvB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,OAAe,EAAE,OAAe;IAC7D,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,gBAAgB,CAAC,CAAC;IACvE,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,EAAE,CAAC;IACrC,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;QACvD,IAAI,CAAC,OAAO;YAAE,OAAO,EAAE,CAAC;QACxB,MAAM,OAAO,GAAa,EAAE,CAAC;QAC7B,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YACvC,IAAI,CAAC;gBACH,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;YACjC,CAAC;YAAC,MAAM,CAAC;gBACP,uBAAuB;YACzB,CAAC;QACH,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,OAAe,EAAE,OAAe,EAAE,KAAa;IAC9E,MAAM,GAAG,GAAG,WAAW,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC1C,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACpC,cAAc,CAAC,IAAI,CAAC,GAAG,EAAE,gBAAgB,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC;AAC5E,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,uBAAuB;IACrC,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC;IAC5D,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC;QAAE,OAAO,EAAE,CAAC;IACxC,MAAM,MAAM,GAAgD,EAAE,CAAC;IAC/D,KAAK,MAAM,SAAS,IAAI,WAAW,CAAC,WAAW,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;QAC1E,IAAI,CAAC,SAAS,CAAC,WAAW,EAAE;YAAE,SAAS;QACvC,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,EAAE,SAAS,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;QAC9D,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC;YAAE,SAAS;QACrC,KAAK,MAAM,UAAU,IAAI,WAAW,CAAC,SAAS,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;YACzE,IAAI,CAAC,UAAU,CAAC,WAAW,EAAE;gBAAE,SAAS;YACxC,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC,IAAI,CAAC,CAAC;YAClD,MAAM,YAAY,GAAG,IAAI,CAAC,QAAQ,EAAE,eAAe,CAAC,CAAC;YACrD,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;gBAC9B,mEAAmE;gBACnE,qEAAqE;gBACrE,8DAA8D;gBAC9D,GAAG,CAAC,IAAI,CACN,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,CAAC,IAAI,EAAE,UAAU,EAAE,SAAS,CAAC,IAAI,EAAE,EAClE,yGAAyG,CAC1G,CAAC;gBACF,SAAS;YACX,CAAC;YACD,IAAI,CAAC;gBACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,CAA2B,CAAC;gBACtF,IAAI,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBAC9D,MAAM,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,UAAU,CAAC,IAAI,EAAE,CAAC,CAAC;gBAClE,CAAC;qBAAM,CAAC;oBACN,GAAG,CAAC,IAAI,CACN,EAAE,YAAY,EAAE,OAAO,EAAE,UAAU,CAAC,IAAI,EAAE,EAC1C,oEAAoE,CACrE,CAAC;gBACJ,CAAC;YACH,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,GAAG,CAAC,IAAI,CACN,EAAE,GAAG,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,YAAY,EAAE,OAAO,EAAE,UAAU,CAAC,IAAI,EAAE,EACjG,mEAAmE,CACpE,CAAC;YACJ,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Project files endpoint helpers.
3
+ *
4
+ * Surfaces a fixed whitelist of files inside a project's `workDir` (CLAUDE.md
5
+ * and .claude/settings*.json) so the UI can read/write them without giving
6
+ * blanket FS access.
7
+ *
8
+ * Why a hard-coded whitelist (no globs in P1):
9
+ * The Claude CLI auto-loads exactly these paths from cwd. Limiting to the
10
+ * exact set lets us audit every read/write and rules out mistakes like a
11
+ * user typing `relPath=../../etc/passwd` or `.git/config`. Phase 2 may add
12
+ * `.claude/rules/*.md`, which will require a careful prefix-glob design.
13
+ *
14
+ * Defense-in-depth path checks:
15
+ * 1. relPath must be in WHITELIST (exact string match)
16
+ * 2. Resolved absolute path must live under realpath(workDir)
17
+ * 3. realpath() resolves symlinks, so a symlink inside .claude/ that
18
+ * points outside workDir is still rejected by check (2).
19
+ */
20
+ export declare const PROJECT_FILE_WHITELIST: readonly ["CLAUDE.md", ".claude/CLAUDE.md", ".claude/settings.json", ".claude/settings.local.json"];
21
+ export type ProjectFileRelPath = (typeof PROJECT_FILE_WHITELIST)[number];
22
+ export interface ProjectFileEntry {
23
+ relPath: ProjectFileRelPath;
24
+ exists: boolean;
25
+ size?: number;
26
+ mtime?: number;
27
+ loadedByCli: true;
28
+ }
29
+ export interface ProjectFileContent {
30
+ relPath: ProjectFileRelPath;
31
+ content: string;
32
+ mtime: number;
33
+ etag: string;
34
+ }
35
+ export declare class ProjectFilesError extends Error {
36
+ status: number;
37
+ details?: Record<string, unknown> | undefined;
38
+ constructor(status: number, message: string, details?: Record<string, unknown> | undefined);
39
+ }
40
+ /** Type guard — narrows an arbitrary string to the whitelist union. */
41
+ export declare function isWhitelistedRelPath(rel: string): rel is ProjectFileRelPath;
42
+ /** List the whitelist with stat info for present files. Never throws. */
43
+ export declare function listProjectFiles(workDir: string): ProjectFileEntry[];
44
+ /**
45
+ * Read a whitelisted file. Returns content="" + etag of empty string when
46
+ * the file does not exist, so the editor UI can populate a blank textarea
47
+ * with a valid etag for first-write conflict detection.
48
+ */
49
+ export declare function readProjectFile(workDir: string, relPath: string): ProjectFileContent;
50
+ export interface WriteOptions {
51
+ ifMatchEtag?: string;
52
+ }
53
+ /**
54
+ * Atomic write: validates JSON syntax for `.json` files, ensures parent
55
+ * `.claude/` exists, writes to a tmp sibling, then renames. Returns the
56
+ * post-write content snapshot. On etag mismatch throws 409 with the current
57
+ * server-side content so the UI can render a 3-way diff.
58
+ */
59
+ export declare function writeProjectFile(workDir: string, relPath: string, newContent: string, opts?: WriteOptions): ProjectFileContent;
60
+ //# sourceMappingURL=project-files.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-files.d.ts","sourceRoot":"","sources":["../src/project-files.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAcH,eAAO,MAAM,sBAAsB,qGAKzB,CAAC;AAEX,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzE,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,kBAAkB,CAAC;IAC5B,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,IAAI,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,kBAAkB,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED,qBAAa,iBAAkB,SAAQ,KAAK;IAEjC,MAAM,EAAE,MAAM;IAEd,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;gBAFjC,MAAM,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM,EACR,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,YAAA;CAK3C;AAED,uEAAuE;AACvE,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,IAAI,kBAAkB,CAE3E;AA8CD,yEAAyE;AACzE,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAmCpE;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,kBAAkB,CAmBpF;AAED,MAAM,WAAW,YAAY;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,IAAI,GAAE,YAAiB,GACtB,kBAAkB,CA6DpB"}
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Project files endpoint helpers.
3
+ *
4
+ * Surfaces a fixed whitelist of files inside a project's `workDir` (CLAUDE.md
5
+ * and .claude/settings*.json) so the UI can read/write them without giving
6
+ * blanket FS access.
7
+ *
8
+ * Why a hard-coded whitelist (no globs in P1):
9
+ * The Claude CLI auto-loads exactly these paths from cwd. Limiting to the
10
+ * exact set lets us audit every read/write and rules out mistakes like a
11
+ * user typing `relPath=../../etc/passwd` or `.git/config`. Phase 2 may add
12
+ * `.claude/rules/*.md`, which will require a careful prefix-glob design.
13
+ *
14
+ * Defense-in-depth path checks:
15
+ * 1. relPath must be in WHITELIST (exact string match)
16
+ * 2. Resolved absolute path must live under realpath(workDir)
17
+ * 3. realpath() resolves symlinks, so a symlink inside .claude/ that
18
+ * points outside workDir is still rejected by check (2).
19
+ */
20
+ import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, statSync, writeFileSync, } from "node:fs";
21
+ import { createHash, randomBytes } from "node:crypto";
22
+ import { dirname, join, sep } from "node:path";
23
+ export const PROJECT_FILE_WHITELIST = [
24
+ "CLAUDE.md",
25
+ ".claude/CLAUDE.md",
26
+ ".claude/settings.json",
27
+ ".claude/settings.local.json",
28
+ ];
29
+ export class ProjectFilesError extends Error {
30
+ status;
31
+ details;
32
+ constructor(status, message, details) {
33
+ super(message);
34
+ this.status = status;
35
+ this.details = details;
36
+ this.name = "ProjectFilesError";
37
+ }
38
+ }
39
+ /** Type guard — narrows an arbitrary string to the whitelist union. */
40
+ export function isWhitelistedRelPath(rel) {
41
+ return PROJECT_FILE_WHITELIST.includes(rel);
42
+ }
43
+ /**
44
+ * Resolve `relPath` against `workDir` and verify the result is still inside
45
+ * workDir even after symlink resolution. Throws ProjectFilesError on any
46
+ * violation. Returns the absolute path (which may not exist yet — caller
47
+ * decides whether absence is OK).
48
+ */
49
+ function resolveSafe(workDir, relPath) {
50
+ // workDir itself must exist for any of this to make sense.
51
+ let workDirReal;
52
+ try {
53
+ workDirReal = realpathSync(workDir);
54
+ }
55
+ catch {
56
+ throw new ProjectFilesError(404, "workDir does not exist on disk");
57
+ }
58
+ const candidate = join(workDirReal, relPath);
59
+ // For an existing file, realpath the file. For a not-yet-existing file,
60
+ // realpath the *parent* (which we may need to mkdir later) — this still
61
+ // catches symlink escapes from intermediate components.
62
+ let probe;
63
+ if (existsSync(candidate)) {
64
+ probe = realpathSync(candidate);
65
+ }
66
+ else {
67
+ // Walk up to the first existing ancestor and realpath it; that's enough
68
+ // to ensure we won't end up writing outside workDir even after rename().
69
+ let parent = dirname(candidate);
70
+ while (!existsSync(parent))
71
+ parent = dirname(parent);
72
+ probe = realpathSync(parent);
73
+ }
74
+ if (probe !== workDirReal && !probe.startsWith(workDirReal + sep)) {
75
+ throw new ProjectFilesError(403, "Path resolves outside workDir", {
76
+ relPath,
77
+ });
78
+ }
79
+ return candidate;
80
+ }
81
+ function computeEtag(content) {
82
+ return createHash("sha256").update(content).digest("hex").slice(0, 12);
83
+ }
84
+ /** List the whitelist with stat info for present files. Never throws. */
85
+ export function listProjectFiles(workDir) {
86
+ const out = [];
87
+ for (const rel of PROJECT_FILE_WHITELIST) {
88
+ let abs;
89
+ try {
90
+ abs = resolveSafe(workDir, rel);
91
+ }
92
+ catch {
93
+ // workDir missing or path escape — surface as "not exists" rather than
94
+ // erroring the whole list call (e.g. a project whose workDir was deleted
95
+ // on disk should still be navigable).
96
+ out.push({ relPath: rel, exists: false, loadedByCli: true });
97
+ continue;
98
+ }
99
+ if (!existsSync(abs)) {
100
+ out.push({ relPath: rel, exists: false, loadedByCli: true });
101
+ continue;
102
+ }
103
+ try {
104
+ const st = statSync(abs);
105
+ if (!st.isFile()) {
106
+ out.push({ relPath: rel, exists: false, loadedByCli: true });
107
+ continue;
108
+ }
109
+ out.push({
110
+ relPath: rel,
111
+ exists: true,
112
+ size: st.size,
113
+ mtime: st.mtime.getTime(),
114
+ loadedByCli: true,
115
+ });
116
+ }
117
+ catch {
118
+ out.push({ relPath: rel, exists: false, loadedByCli: true });
119
+ }
120
+ }
121
+ return out;
122
+ }
123
+ /**
124
+ * Read a whitelisted file. Returns content="" + etag of empty string when
125
+ * the file does not exist, so the editor UI can populate a blank textarea
126
+ * with a valid etag for first-write conflict detection.
127
+ */
128
+ export function readProjectFile(workDir, relPath) {
129
+ if (!isWhitelistedRelPath(relPath)) {
130
+ throw new ProjectFilesError(400, "relPath not in whitelist", { relPath });
131
+ }
132
+ const abs = resolveSafe(workDir, relPath);
133
+ if (!existsSync(abs)) {
134
+ return { relPath, content: "", mtime: 0, etag: computeEtag("") };
135
+ }
136
+ const st = statSync(abs);
137
+ if (!st.isFile()) {
138
+ throw new ProjectFilesError(400, "Path is not a regular file", { relPath });
139
+ }
140
+ const content = readFileSync(abs, "utf-8");
141
+ return {
142
+ relPath,
143
+ content,
144
+ mtime: st.mtime.getTime(),
145
+ etag: computeEtag(content),
146
+ };
147
+ }
148
+ /**
149
+ * Atomic write: validates JSON syntax for `.json` files, ensures parent
150
+ * `.claude/` exists, writes to a tmp sibling, then renames. Returns the
151
+ * post-write content snapshot. On etag mismatch throws 409 with the current
152
+ * server-side content so the UI can render a 3-way diff.
153
+ */
154
+ export function writeProjectFile(workDir, relPath, newContent, opts = {}) {
155
+ if (!isWhitelistedRelPath(relPath)) {
156
+ throw new ProjectFilesError(400, "relPath not in whitelist", { relPath });
157
+ }
158
+ // Optimistic-concurrency check happens BEFORE any FS mutation.
159
+ if (opts.ifMatchEtag !== undefined) {
160
+ const current = readProjectFile(workDir, relPath);
161
+ if (current.etag !== opts.ifMatchEtag) {
162
+ throw new ProjectFilesError(409, "etag mismatch", {
163
+ relPath,
164
+ currentEtag: current.etag,
165
+ currentContent: current.content,
166
+ currentMtime: current.mtime,
167
+ });
168
+ }
169
+ }
170
+ // Validate JSON before touching disk so we never leave broken settings.json.
171
+ if (relPath.endsWith(".json")) {
172
+ if (newContent.trim().length === 0) {
173
+ // Empty JSON → store as `{}` so the file remains valid for CLI consumers.
174
+ newContent = "{}\n";
175
+ }
176
+ else {
177
+ try {
178
+ JSON.parse(newContent);
179
+ }
180
+ catch (err) {
181
+ throw new ProjectFilesError(400, "Invalid JSON", {
182
+ relPath,
183
+ parseError: err instanceof Error ? err.message : String(err),
184
+ });
185
+ }
186
+ }
187
+ }
188
+ else if (relPath.endsWith(".md")) {
189
+ // Ensure trailing newline — POSIX text-file convention; avoids editors
190
+ // appending a newline on next manual edit and producing spurious diffs.
191
+ if (newContent.length > 0 && !newContent.endsWith("\n")) {
192
+ newContent = newContent + "\n";
193
+ }
194
+ }
195
+ const abs = resolveSafe(workDir, relPath);
196
+ const parent = dirname(abs);
197
+ if (!existsSync(parent)) {
198
+ mkdirSync(parent, { recursive: true });
199
+ }
200
+ // tmp file lives in the same directory so rename() is atomic on the same FS.
201
+ // randomBytes avoids collisions if two writes race; the loser gets EEXIST on
202
+ // its own tmp (very unlikely) and retries via the API layer.
203
+ const tmp = join(parent, `.${randomBytes(6).toString("hex")}.tmp`);
204
+ writeFileSync(tmp, newContent, "utf-8");
205
+ renameSync(tmp, abs);
206
+ const st = statSync(abs);
207
+ return {
208
+ relPath,
209
+ content: newContent,
210
+ mtime: st.mtime.getTime(),
211
+ etag: computeEtag(newContent),
212
+ };
213
+ }
214
+ //# sourceMappingURL=project-files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-files.js","sourceRoot":"","sources":["../src/project-files.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ export type TaskStatus = "pending" | "in_progress" | "completed" | "cancelled";
2
+ export interface TaskCounts {
3
+ pending: number;
4
+ in_progress: number;
5
+ completed: number;
6
+ cancelled: number;
7
+ }
8
+ export interface ProjectSummary {
9
+ encodedCwd: string;
10
+ workDir: string;
11
+ createdAt: string | null;
12
+ swarmCount: number;
13
+ taskCount: number;
14
+ taskCounts: TaskCounts;
15
+ }
16
+ export interface SwarmSummary {
17
+ swarmId: string;
18
+ teamName: string;
19
+ status: string;
20
+ roleCount: number;
21
+ savedAt: number | null;
22
+ }
23
+ export declare function listProjectDirs(home?: string): string[];
24
+ export declare function resolveWorkDir(home: string, encodedCwd: string): string | null;
25
+ export declare function loadProjectSummary(home: string, encodedCwd: string): ProjectSummary | null;
26
+ export declare function loadSwarmSummaries(home: string, workDir: string): SwarmSummary[];
27
+ export declare function listAllProjectSummaries(home?: string): ProjectSummary[];
28
+ //# sourceMappingURL=projects-fs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projects-fs.d.ts","sourceRoot":"","sources":["../src/projects-fs.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,aAAa,GAAG,WAAW,GAAG,WAAW,CAAC;AAC/E,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AACD,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,UAAU,CAAC;CACxB;AACD,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAkBD,wBAAgB,eAAe,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAMvD;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAY9E;AAiBD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAkB1F;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,YAAY,EAAE,CAiBhF;AAED,wBAAgB,uBAAuB,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,cAAc,EAAE,CAKvE"}
@@ -0,0 +1,111 @@
1
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { TaskStore, projectRoot } from "@mclawnet/task";
5
+ import { loadSwarmSnapshot } from "./persistence.js";
6
+ function getHome(homeOverride) {
7
+ return homeOverride ?? process.env.CLAWNET_HOME ?? homedir();
8
+ }
9
+ function projectsDir(home) {
10
+ return join(home, ".clawnet", "projects");
11
+ }
12
+ function isSafeEncoded(encoded) {
13
+ if (!encoded)
14
+ return false;
15
+ if (encoded.includes("/") || encoded.includes("\\"))
16
+ return false;
17
+ if (encoded.includes("\0"))
18
+ return false;
19
+ if (encoded === "." || encoded === ".." || encoded.includes(".."))
20
+ return false;
21
+ return true;
22
+ }
23
+ export function listProjectDirs(home) {
24
+ const dir = projectsDir(getHome(home));
25
+ if (!existsSync(dir))
26
+ return [];
27
+ return readdirSync(dir, { withFileTypes: true })
28
+ .filter((e) => e.isDirectory())
29
+ .map((e) => e.name);
30
+ }
31
+ export function resolveWorkDir(home, encodedCwd) {
32
+ if (!isSafeEncoded(encodedCwd))
33
+ return null;
34
+ const dirs = listProjectDirs(home);
35
+ if (!dirs.includes(encodedCwd))
36
+ return null;
37
+ const metaPath = join(projectsDir(home), encodedCwd, "meta.json");
38
+ if (!existsSync(metaPath))
39
+ return null;
40
+ try {
41
+ const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
42
+ return typeof meta.workDir === "string" ? meta.workDir : null;
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ function readMetaCreatedAt(home, encodedCwd) {
49
+ const metaPath = join(projectsDir(home), encodedCwd, "meta.json");
50
+ if (!existsSync(metaPath))
51
+ return null;
52
+ try {
53
+ const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
54
+ return meta.createdAt ?? null;
55
+ }
56
+ catch {
57
+ return null;
58
+ }
59
+ }
60
+ function emptyCounts() {
61
+ return { pending: 0, in_progress: 0, completed: 0, cancelled: 0 };
62
+ }
63
+ export function loadProjectSummary(home, encodedCwd) {
64
+ const workDir = resolveWorkDir(home, encodedCwd);
65
+ if (!workDir)
66
+ return null;
67
+ const store = new TaskStore({ workDir, home });
68
+ const tasks = store.list();
69
+ const counts = emptyCounts();
70
+ for (const t of tasks) {
71
+ if (t.status in counts)
72
+ counts[t.status] += 1;
73
+ }
74
+ const swarms = loadSwarmSummaries(home, workDir);
75
+ return {
76
+ encodedCwd,
77
+ workDir,
78
+ createdAt: readMetaCreatedAt(home, encodedCwd),
79
+ swarmCount: swarms.length,
80
+ taskCount: tasks.length,
81
+ taskCounts: counts,
82
+ };
83
+ }
84
+ export function loadSwarmSummaries(home, workDir) {
85
+ const swarmsRoot = join(projectRoot(workDir, home), "swarms");
86
+ if (!existsSync(swarmsRoot))
87
+ return [];
88
+ const out = [];
89
+ for (const entry of readdirSync(swarmsRoot, { withFileTypes: true })) {
90
+ if (!entry.isDirectory())
91
+ continue;
92
+ const snap = loadSwarmSnapshot(workDir, entry.name);
93
+ if (!snap)
94
+ continue;
95
+ out.push({
96
+ swarmId: snap.id,
97
+ teamName: snap.teamName,
98
+ status: snap.status ?? "unknown",
99
+ roleCount: snap.roles?.length ?? 0,
100
+ savedAt: snap.savedAt ?? null,
101
+ });
102
+ }
103
+ return out;
104
+ }
105
+ export function listAllProjectSummaries(home) {
106
+ const h = getHome(home);
107
+ return listProjectDirs(h)
108
+ .map((e) => loadProjectSummary(h, e))
109
+ .filter((s) => !!s);
110
+ }
111
+ //# sourceMappingURL=projects-fs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projects-fs.js","sourceRoot":"","sources":["../src/projects-fs.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,17 @@
1
1
  import type { SwarmCoordinator } from "./swarm-coordinator.js";
2
2
  import { type SwarmSnapshot } from "./persistence.js";
3
3
  export declare function listRecoverableSwarms(): SwarmSnapshot[];
4
+ /**
5
+ * Backwards-compatible recovery helper.
6
+ *
7
+ * Delegates to {@link SwarmCoordinator.recover}, which:
8
+ * - respawns each role with its persisted instanceId,
9
+ * - passes `claudeSessionId` through as `--resume` so per-role conversations
10
+ * continue,
11
+ * - drains each role's offline inbox via `inboxRelay.deliver`,
12
+ * - leaves the recovered swarm in `paused` state.
13
+ *
14
+ * Older callers still pass a full snapshot — we only need its `id`.
15
+ */
4
16
  export declare function recoverSwarm(coordinator: SwarmCoordinator, snapshot: SwarmSnapshot): Promise<void>;
5
17
  //# sourceMappingURL=recovery.d.ts.map