relaybrain 0.0.0-stage → 0.2.1

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 (140) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +434 -3
  3. package/backend/coordinator/client.js +364 -0
  4. package/backend/coordinator/lockTable.js +649 -0
  5. package/backend/coordinator/server.js +179 -0
  6. package/backend/lib/centralAuth.js +62 -0
  7. package/backend/lib/centralStore.js +112 -0
  8. package/backend/lib/centralSync.js +88 -0
  9. package/backend/lib/cloneRepo.js +108 -0
  10. package/backend/lib/collisionMetrics.js +231 -0
  11. package/backend/lib/conflicts.js +35 -0
  12. package/backend/lib/dashboard.js +53 -0
  13. package/backend/lib/depGraph.js +393 -0
  14. package/backend/lib/deps.js +46 -0
  15. package/backend/lib/dirtyFiles.js +33 -0
  16. package/backend/lib/editMatchers.js +55 -0
  17. package/backend/lib/falkor/analytics.js +228 -0
  18. package/backend/lib/falkor/bus.js +16 -0
  19. package/backend/lib/falkor/central.js +281 -0
  20. package/backend/lib/falkor/client.js +165 -0
  21. package/backend/lib/falkor/config.js +73 -0
  22. package/backend/lib/falkor/coord.js +299 -0
  23. package/backend/lib/falkor/cursor.js +58 -0
  24. package/backend/lib/falkor/demo.js +221 -0
  25. package/backend/lib/falkor/fixtures.js +85 -0
  26. package/backend/lib/falkor/index.js +146 -0
  27. package/backend/lib/falkor/ingest.js +233 -0
  28. package/backend/lib/falkor/intents.js +32 -0
  29. package/backend/lib/falkor/keys.js +102 -0
  30. package/backend/lib/falkor/migrations.js +56 -0
  31. package/backend/lib/falkor/project.js +307 -0
  32. package/backend/lib/falkor/query.js +344 -0
  33. package/backend/lib/falkor/redact.js +30 -0
  34. package/backend/lib/falkor/routes.js +125 -0
  35. package/backend/lib/falkor/schema.js +108 -0
  36. package/backend/lib/falkor/write.js +281 -0
  37. package/backend/lib/githubLogin.js +108 -0
  38. package/backend/lib/githubOAuth.js +42 -0
  39. package/backend/lib/githubRepo.js +117 -0
  40. package/backend/lib/harvest.js +85 -0
  41. package/backend/lib/httpServer.js +27 -0
  42. package/backend/lib/installAgentRelay.js +235 -0
  43. package/backend/lib/installHooks.js +309 -0
  44. package/backend/lib/lamport.js +14 -0
  45. package/backend/lib/localBrowse.js +109 -0
  46. package/backend/lib/memorySync.js +63 -0
  47. package/backend/lib/notices.js +150 -0
  48. package/backend/lib/ot.js +74 -0
  49. package/backend/lib/patches.js +192 -0
  50. package/backend/lib/paths.js +85 -0
  51. package/backend/lib/peerClash.js +64 -0
  52. package/backend/lib/presence.js +35 -0
  53. package/backend/lib/relayOsBlock.js +84 -0
  54. package/backend/lib/repoSnapshot.js +125 -0
  55. package/backend/lib/room.js +638 -0
  56. package/backend/lib/roomAsk.js +148 -0
  57. package/backend/lib/roomAuth.js +215 -0
  58. package/backend/lib/roomControl.js +226 -0
  59. package/backend/lib/roomLocks.js +201 -0
  60. package/backend/lib/roomSignal.js +265 -0
  61. package/backend/lib/roomSync.js +194 -0
  62. package/backend/lib/seed.js +107 -0
  63. package/backend/lib/sseHub.js +40 -0
  64. package/backend/lib/store.js +79 -0
  65. package/backend/lib/supergraph.js +46 -0
  66. package/backend/lib/transcripts/discover.js +437 -0
  67. package/backend/lib/transcripts/parse.js +558 -0
  68. package/backend/lib/transcripts/sessionId.js +36 -0
  69. package/backend/lib/transcripts/sync.js +291 -0
  70. package/backend/lib/transcripts/util.js +299 -0
  71. package/backend/lib/treeSitterImports.js +219 -0
  72. package/backend/lib/watchWorkspace.js +42 -0
  73. package/backend/mcp/README.md +193 -0
  74. package/backend/mcp/server.js +42 -0
  75. package/backend/mcp/tools.js +484 -0
  76. package/backend/server.js +3894 -0
  77. package/cli/graph.js +286 -0
  78. package/cli/relay.js +475 -0
  79. package/docker-compose.falkor.yml +17 -0
  80. package/docs/MEMORY-GRAPH.md +314 -0
  81. package/hooks/relay-antigravity-post-tool.js +2 -0
  82. package/hooks/relay-antigravity-pre-read.js +2 -0
  83. package/hooks/relay-antigravity-pre-tool.js +2 -0
  84. package/hooks/relay-antigravity-stop.js +2 -0
  85. package/hooks/relay-claude-post-tool.js +2 -0
  86. package/hooks/relay-claude-pre-read.js +2 -0
  87. package/hooks/relay-claude-pre-tool.js +2 -0
  88. package/hooks/relay-claude-stop.js +2 -0
  89. package/hooks/relay-codex-post-tool.js +2 -0
  90. package/hooks/relay-codex-pre-read.js +2 -0
  91. package/hooks/relay-codex-pre-tool.js +2 -0
  92. package/hooks/relay-codex-stop.js +2 -0
  93. package/hooks/relay-copilot-post-tool.js +2 -0
  94. package/hooks/relay-copilot-pre-read.js +2 -0
  95. package/hooks/relay-copilot-pre-tool.js +2 -0
  96. package/hooks/relay-copilot-stop.js +2 -0
  97. package/hooks/relay-cursor-post-tool.js +2 -0
  98. package/hooks/relay-cursor-pre-read.js +2 -0
  99. package/hooks/relay-cursor-pre-tool.js +2 -0
  100. package/hooks/relay-cursor-stop.js +2 -0
  101. package/hooks/relay-hook-lib.js +621 -0
  102. package/mission-control/out/404.html +1 -0
  103. package/mission-control/out/__next.__PAGE__.txt +26 -0
  104. package/mission-control/out/__next._full.txt +21 -0
  105. package/mission-control/out/__next._tree.txt +4 -0
  106. package/mission-control/out/_next/static/BuhfCxd0WQE65yIzZJCOM/_buildManifest.js +11 -0
  107. package/mission-control/out/_next/static/BuhfCxd0WQE65yIzZJCOM/_clientMiddlewareManifest.js +1 -0
  108. package/mission-control/out/_next/static/BuhfCxd0WQE65yIzZJCOM/_ssgManifest.js +1 -0
  109. package/mission-control/out/_next/static/chunks/0-3l04fnym95-.js +22 -0
  110. package/mission-control/out/_next/static/chunks/064ntycm-fx5v.js +1 -0
  111. package/mission-control/out/_next/static/chunks/0cz1d0mv5g_q7.js +1 -0
  112. package/mission-control/out/_next/static/chunks/0ehjiuuxbbhq9.js +1 -0
  113. package/mission-control/out/_next/static/chunks/1-dy52ls2ntb_.css +4 -0
  114. package/mission-control/out/_next/static/chunks/1q0cmt0kqufow.js +2 -0
  115. package/mission-control/out/_next/static/chunks/1ya1u0izct4f6.js +1 -0
  116. package/mission-control/out/_next/static/chunks/2n66mlnb34vjq.js +1 -0
  117. package/mission-control/out/_next/static/chunks/2xar70jzx8f1y.css +1 -0
  118. package/mission-control/out/_next/static/chunks/31cgqj_-l15yt.js +31 -0
  119. package/mission-control/out/_next/static/chunks/31qgr-p-oulpb.js +1 -0
  120. package/mission-control/out/_next/static/chunks/3vq9c4hivvgn_.js +1 -0
  121. package/mission-control/out/_next/static/chunks/turbopack-3hsllzpnzz12p.js +1 -0
  122. package/mission-control/out/_next/static/media/icon.27otmh_fv3buh.svg +4 -0
  123. package/mission-control/out/_not-found/__next._full.txt +18 -0
  124. package/mission-control/out/_not-found/__next._not-found/__PAGE__.txt +24 -0
  125. package/mission-control/out/_not-found/__next._tree.txt +3 -0
  126. package/mission-control/out/_not-found.html +1 -0
  127. package/mission-control/out/_not-found.txt +18 -0
  128. package/mission-control/out/agents/antigravity.png +0 -0
  129. package/mission-control/out/agents/claude.png +0 -0
  130. package/mission-control/out/agents/codex.png +0 -0
  131. package/mission-control/out/agents/copilot.png +0 -0
  132. package/mission-control/out/agents/cursor.png +0 -0
  133. package/mission-control/out/icon.svg +4 -0
  134. package/mission-control/out/index.html +1 -0
  135. package/mission-control/out/index.txt +21 -0
  136. package/mission-control/out/logo.png +0 -0
  137. package/mission-control/out/logo_transparent.png +0 -0
  138. package/package.json +76 -6
  139. package/scripts/falkor-rebuild.js +38 -0
  140. package/scripts/falkor-seed-demo.js +29 -0
@@ -0,0 +1,364 @@
1
+ const fs = require("fs");
2
+ const http = require("http");
3
+ const path = require("path");
4
+ const { coordinatorStatePath } = require("../lib/paths");
5
+ const { loadRoom } = require("../lib/room");
6
+
7
+ const TOTAL_BUDGET_MS = 3000;
8
+ const HTTP_TIMEOUT_MS = 2000;
9
+
10
+ function readState(workspacePath) {
11
+ const files = [coordinatorStatePath(workspacePath), coordinatorStatePath(null)];
12
+ for (const file of files) {
13
+ try {
14
+ const state = JSON.parse(fs.readFileSync(file, "utf8"));
15
+ if (state.port || state.apiPort) return state;
16
+ } catch {
17
+ /* next */
18
+ }
19
+ }
20
+ return null;
21
+ }
22
+
23
+ function readPort(workspacePath) {
24
+ const state = readState(workspacePath);
25
+ return state?.port ? Number(state.port) : null;
26
+ }
27
+
28
+ // The API server owns the same in-memory lock table the dashboard renders, and
29
+ // its port is fixed, so it is the one target that cannot go stale. The
30
+ // coordinator's port is ephemeral and its state file is trusted blindly by
31
+ // readPort — a `relay serve` that dies after binding the coordinator but before
32
+ // binding the API leaves a pointer to a dead port behind.
33
+ function apiPort(workspacePath) {
34
+ if (process.env.RELAY_PORT) return Number(process.env.RELAY_PORT);
35
+ const state = readState(workspacePath);
36
+ return Number(state?.apiPort) || 3001;
37
+ }
38
+
39
+ // A room whose host has gone away (closed laptop, expired tunnel) otherwise
40
+ // costs every hook a full HTTP timeout on each of claim/heartbeat/release, which
41
+ // reads to the user as "the agent froze".
42
+ const ROOM_COOLDOWN_MS = 30_000;
43
+ const roomFailedAt = new Map();
44
+
45
+ // Only a guest talks to the room. The host *is* the coordinator: routing its own
46
+ // hooks out through its ngrok tunnel and back adds a full internet round trip to
47
+ // every claim, and stalls the agent whenever the tunnel hiccups.
48
+ function roomUsable(room) {
49
+ if (!room?.url || room.role !== "guest") return false;
50
+ const failed = roomFailedAt.get(room.url);
51
+ return !failed || Date.now() - failed > ROOM_COOLDOWN_MS;
52
+ }
53
+
54
+ function markRoomDown(room) {
55
+ if (room?.url) roomFailedAt.set(room.url, Date.now());
56
+ }
57
+
58
+ // The lock table namespaces every lock by absolute workspace path, so a guest
59
+ // claiming `src/auth.ts` under `D:\work\repo` and a host holding it under
60
+ // `/home/ana/repo` land in two different namespaces and never collide — the
61
+ // coordinator looks shared while arbitrating nothing. Rewriting the workspace to
62
+ // the host's path is what puts both users on one mutex.
63
+ // `path.resolve` alone is not enough on Windows: the same folder reaches us as
64
+ // both `C:\Users\PRAKRI~1\...` and `C:\Users\Prakrititz Borah\...` depending on
65
+ // who spawned the agent, and a mismatch here silently sends the claim to the
66
+ // guest's own namespace instead of the shared one.
67
+ function canonicalPath(value) {
68
+ if (!value) return "";
69
+ try {
70
+ return fs.realpathSync.native(value).replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
71
+ } catch {
72
+ return path.resolve(String(value)).replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
73
+ }
74
+ }
75
+
76
+ function roomPayload(room, workspacePath, payload) {
77
+ if (room?.role !== "guest" || !room.hostWorkspacePath) return payload;
78
+ const local = canonicalPath(room.projectPath);
79
+ const here = canonicalPath(workspacePath);
80
+ // Antigravity (and some other agents) report a cwd that is the same repo on
81
+ // a different drive letter than the clone Relay joined. Still rewrite onto
82
+ // the host path so both machines share one lock namespace. Skip only when
83
+ // this is clearly a different folder with no room of its own.
84
+ if (local && here && local !== here && !here.startsWith(`${local}/`)) {
85
+ const hereRoom = loadRoom(workspacePath);
86
+ if (!hereRoom?.url || hereRoom.url !== room.url) return payload;
87
+ }
88
+ return { ...payload, workspaceId: room.hostWorkspacePath, peerWorkspacePath: workspacePath };
89
+ }
90
+
91
+ function postJson(port, pathname, body, timeoutMs) {
92
+ return new Promise((resolve, reject) => {
93
+ const data = JSON.stringify(body || {});
94
+ const req = http.request(
95
+ {
96
+ host: "127.0.0.1",
97
+ port,
98
+ path: pathname,
99
+ method: "POST",
100
+ headers: {
101
+ "Content-Type": "application/json",
102
+ "Content-Length": Buffer.byteLength(data),
103
+ },
104
+ timeout: timeoutMs,
105
+ },
106
+ (res) => {
107
+ let raw = "";
108
+ res.on("data", (c) => (raw += c));
109
+ res.on("end", () => {
110
+ try {
111
+ resolve({ status: res.statusCode, body: JSON.parse(raw || "{}") });
112
+ } catch (err) {
113
+ reject(err);
114
+ }
115
+ });
116
+ }
117
+ );
118
+ req.on("timeout", () => {
119
+ req.destroy(new Error("timeout"));
120
+ });
121
+ req.on("error", reject);
122
+ req.write(data);
123
+ req.end();
124
+ });
125
+ }
126
+
127
+ function getJson(port, pathname, timeoutMs) {
128
+ return new Promise((resolve, reject) => {
129
+ const req = http.request(
130
+ {
131
+ host: "127.0.0.1",
132
+ port,
133
+ path: pathname,
134
+ method: "GET",
135
+ timeout: timeoutMs,
136
+ },
137
+ (res) => {
138
+ let raw = "";
139
+ res.on("data", (c) => (raw += c));
140
+ res.on("end", () => {
141
+ try {
142
+ resolve(JSON.parse(raw || "{}"));
143
+ } catch (err) {
144
+ reject(err);
145
+ }
146
+ });
147
+ }
148
+ );
149
+ req.on("timeout", () => req.destroy(new Error("timeout")));
150
+ req.on("error", reject);
151
+ req.end();
152
+ });
153
+ }
154
+
155
+ function lockfilePath(workspacePath, file) {
156
+ const crypto = require("crypto");
157
+ const { locksDirFor } = require("../lib/paths");
158
+ const hash = crypto.createHash("sha256").update(file).digest("hex").slice(0, 16);
159
+ return path.join(locksDirFor(workspacePath), `${hash}.lock`);
160
+ }
161
+
162
+ function filesystemClaim(workspacePath, agentId, file, ttlMs) {
163
+ const lockPath = lockfilePath(workspacePath, file);
164
+ const now = Date.now();
165
+ try {
166
+ fs.writeFileSync(
167
+ lockPath,
168
+ JSON.stringify({ filePath: file, agentId, claimedAt: now, ttlMs, source: "filesystem", workspaceId: workspacePath }),
169
+ { flag: "wx" }
170
+ );
171
+ return { allowed: true, source: "filesystem" };
172
+ } catch {
173
+ try {
174
+ const existing = JSON.parse(fs.readFileSync(lockPath, "utf8"));
175
+ const { sameOwner } = require("./lockTable");
176
+ if (sameOwner(existing.agentId, agentId) || now >= existing.claimedAt + (existing.ttlMs || 15000)) {
177
+ fs.writeFileSync(
178
+ lockPath,
179
+ JSON.stringify({ filePath: file, agentId, claimedAt: now, ttlMs, source: "filesystem", workspaceId: workspacePath })
180
+ );
181
+ return { allowed: true, source: "filesystem" };
182
+ }
183
+ return { allowed: false, holder: existing.agentId, source: "filesystem" };
184
+ } catch {
185
+ return { allowed: true, source: "fail-open" };
186
+ }
187
+ }
188
+ }
189
+
190
+ async function postRemote(url, pathname, body, timeoutMs, token) {
191
+ const ctrl = new AbortController();
192
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs);
193
+ try {
194
+ const res = await fetch(`${url.replace(/\/$/, "")}${pathname}`, {
195
+ method: "POST",
196
+ headers: {
197
+ "Content-Type": "application/json",
198
+ "ngrok-skip-browser-warning": "relay",
199
+ // An invite-only host rejects unauthenticated lock traffic; without the
200
+ // token every claim would silently fall back to local-only locking.
201
+ ...(token ? { "x-relay-room-token": token } : {}),
202
+ },
203
+ body: JSON.stringify(body || {}),
204
+ signal: ctrl.signal,
205
+ });
206
+ return { status: res.status, body: await res.json().catch(() => ({})) };
207
+ } finally {
208
+ clearTimeout(timer);
209
+ }
210
+ }
211
+
212
+ async function claimFile(workspacePath, agentId, file, ttlMs = 15000, opts = {}) {
213
+ const started = Date.now();
214
+ const room = loadRoom(workspacePath);
215
+ const payload = {
216
+ agentId,
217
+ file,
218
+ ttl: ttlMs,
219
+ workspaceId: workspacePath,
220
+ dependsOn: opts.dependsOn,
221
+ files: opts.files,
222
+ mode: opts.mode,
223
+ holder: opts.holder || null,
224
+ };
225
+ // Local API owns the control-plane socket. Guests must not wait on ngrok HTTP
226
+ // while chat snapshots are in flight — claims hop loopback, then a tiny WS.
227
+ // A `degraded` answer means the daemon could not reach the host, so it is not
228
+ // an answer at all — it must not short-circuit the ladder the way a real
229
+ // verdict does. Before this, the daemon's local-table grant came back as a
230
+ // plain `allowed: true` and stopped the search here, shadowing the room HTTP
231
+ // rung below: a dead control socket with a perfectly healthy tunnel never
232
+ // fell through to the wire that was still up.
233
+ let degraded = null;
234
+ try {
235
+ const remaining = Math.max(80, 800 - (Date.now() - started));
236
+ const { body } = await postJson(apiPort(workspacePath), "/api/coord/claim", roomPayload(room, workspacePath, payload), remaining);
237
+ if (body?.degraded) degraded = { ...body, source: body.source || "local_fallback" };
238
+ else if (body && typeof body.allowed === "boolean") return { ...body, source: body.source || "control" };
239
+ } catch {
240
+ /* coordinator / remote fallback */
241
+ }
242
+ if (roomUsable(room)) {
243
+ try {
244
+ const { body } = await postRemote(room.url, "/api/coord/claim", roomPayload(room, workspacePath, payload), HTTP_TIMEOUT_MS, room.memberToken);
245
+ return { ...body, source: body.source || "room" };
246
+ } catch {
247
+ markRoomDown(room);
248
+ }
249
+ }
250
+ // Every rung below this point is local to *this* machine: the coordinator
251
+ // process, the lock directory on this disk, and finally fail-open. None of
252
+ // them can adjudicate a lock that belongs to the room, so once we know we are
253
+ // a guest whose host is unreachable, consulting them would only launder an
254
+ // unanswered question into a confident yes. Refuse with the reason instead.
255
+ if (degraded) {
256
+ console.warn("[relay] HOST_UNREACHABLE — claim refused rather than granted locally");
257
+ return degraded;
258
+ }
259
+ const port = readPort(workspacePath);
260
+ if (port) {
261
+ try {
262
+ const remaining = Math.max(50, HTTP_TIMEOUT_MS - (Date.now() - started));
263
+ const { body } = await postJson(port, "/claim", payload, remaining);
264
+ return { ...body, source: body.source || "coordinator" };
265
+ } catch {
266
+ /* fall through */
267
+ }
268
+ }
269
+ const remaining = TOTAL_BUDGET_MS - (Date.now() - started);
270
+ if (remaining > 0) {
271
+ try {
272
+ return filesystemClaim(workspacePath, agentId, file, ttlMs);
273
+ } catch {
274
+ /* fail open */
275
+ }
276
+ }
277
+ console.warn("[relay] COORDINATOR_UNAVAILABLE — fail-open");
278
+ return { allowed: true, source: "fail-open" };
279
+ }
280
+
281
+ async function releaseFile(workspacePath, agentId, file) {
282
+ const room = loadRoom(workspacePath);
283
+ const payload = { agentId, file, workspaceId: workspacePath };
284
+ try {
285
+ const { body } = await postJson(apiPort(workspacePath), "/api/coord/release", roomPayload(room, workspacePath, payload), 600);
286
+ // A degraded release is already queued for replay by the daemon, but the
287
+ // tunnel may still be up even when the control socket is not — try it, and
288
+ // let the replay be a harmless no-op if this succeeds.
289
+ if (body && !body.degraded) return body;
290
+ } catch {
291
+ /* fallback */
292
+ }
293
+ if (roomUsable(room)) {
294
+ try {
295
+ const { body } = await postRemote(room.url, "/api/coord/release", roomPayload(room, workspacePath, payload), HTTP_TIMEOUT_MS, room.memberToken);
296
+ return body;
297
+ } catch {
298
+ markRoomDown(room);
299
+ }
300
+ }
301
+ return { ok: true };
302
+ }
303
+
304
+ async function status(workspacePath) {
305
+ const port = readPort(workspacePath);
306
+ if (!port) return { locks: {} };
307
+ try {
308
+ return await getJson(port, "/status", HTTP_TIMEOUT_MS);
309
+ } catch {
310
+ return { locks: {} };
311
+ }
312
+ }
313
+
314
+ async function releaseAll(workspacePath, agentId) {
315
+ const room = loadRoom(workspacePath);
316
+ const payload = { agentId, workspaceId: workspacePath };
317
+ if (roomUsable(room)) {
318
+ try {
319
+ const { body } = await postRemote(room.url, "/api/coord/release-all", roomPayload(room, workspacePath, payload), HTTP_TIMEOUT_MS, room.memberToken);
320
+ return body;
321
+ } catch {
322
+ markRoomDown(room);
323
+ /* local fallback */
324
+ }
325
+ }
326
+ const port = readPort(workspacePath);
327
+ if (port) {
328
+ try {
329
+ const { body } = await postJson(port, "/release-all", payload, HTTP_TIMEOUT_MS);
330
+ return body;
331
+ } catch {
332
+ /* ignore */
333
+ }
334
+ }
335
+ try {
336
+ const { body } = await postJson(apiPort(workspacePath), "/api/coord/release-all", payload, HTTP_TIMEOUT_MS);
337
+ return body;
338
+ } catch {
339
+ /* ignore */
340
+ }
341
+ return { released: [] };
342
+ }
343
+
344
+ async function heartbeat(workspacePath, agentId, file) {
345
+ const room = loadRoom(workspacePath);
346
+ const payload = { agentId, file, workspaceId: workspacePath };
347
+ try {
348
+ const { body } = await postJson(apiPort(workspacePath), "/api/coord/heartbeat", roomPayload(room, workspacePath, payload), 400);
349
+ if (body) return body;
350
+ } catch {
351
+ /* fallback */
352
+ }
353
+ if (roomUsable(room)) {
354
+ try {
355
+ const { body } = await postRemote(room.url, "/api/coord/heartbeat", roomPayload(room, workspacePath, payload), HTTP_TIMEOUT_MS, room.memberToken);
356
+ return body;
357
+ } catch {
358
+ markRoomDown(room);
359
+ }
360
+ }
361
+ return { ok: false };
362
+ }
363
+
364
+ module.exports = { claimFile, releaseFile, releaseAll, status, readPort, heartbeat, apiPort, postJson };