relaybrain 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +290 -2
- package/backend/coordinator/client.js +364 -0
- package/backend/coordinator/lockTable.js +649 -0
- package/backend/coordinator/server.js +179 -0
- package/backend/lib/centralAuth.js +62 -0
- package/backend/lib/centralStore.js +112 -0
- package/backend/lib/centralSync.js +88 -0
- package/backend/lib/cloneRepo.js +108 -0
- package/backend/lib/collisionMetrics.js +231 -0
- package/backend/lib/conflicts.js +35 -0
- package/backend/lib/dashboard.js +53 -0
- package/backend/lib/depGraph.js +393 -0
- package/backend/lib/deps.js +46 -0
- package/backend/lib/dirtyFiles.js +33 -0
- package/backend/lib/editMatchers.js +55 -0
- package/backend/lib/falkor/analytics.js +228 -0
- package/backend/lib/falkor/bus.js +16 -0
- package/backend/lib/falkor/central.js +281 -0
- package/backend/lib/falkor/client.js +165 -0
- package/backend/lib/falkor/config.js +73 -0
- package/backend/lib/falkor/coord.js +299 -0
- package/backend/lib/falkor/cursor.js +58 -0
- package/backend/lib/falkor/fixtures.js +85 -0
- package/backend/lib/falkor/index.js +146 -0
- package/backend/lib/falkor/ingest.js +233 -0
- package/backend/lib/falkor/intents.js +32 -0
- package/backend/lib/falkor/keys.js +102 -0
- package/backend/lib/falkor/migrations.js +56 -0
- package/backend/lib/falkor/project.js +307 -0
- package/backend/lib/falkor/query.js +344 -0
- package/backend/lib/falkor/redact.js +30 -0
- package/backend/lib/falkor/routes.js +81 -0
- package/backend/lib/falkor/schema.js +108 -0
- package/backend/lib/falkor/write.js +281 -0
- package/backend/lib/githubLogin.js +108 -0
- package/backend/lib/githubOAuth.js +42 -0
- package/backend/lib/githubRepo.js +117 -0
- package/backend/lib/harvest.js +85 -0
- package/backend/lib/httpServer.js +27 -0
- package/backend/lib/installAgentRelay.js +235 -0
- package/backend/lib/installHooks.js +309 -0
- package/backend/lib/lamport.js +14 -0
- package/backend/lib/localBrowse.js +109 -0
- package/backend/lib/memorySync.js +63 -0
- package/backend/lib/notices.js +143 -0
- package/backend/lib/ot.js +74 -0
- package/backend/lib/patches.js +192 -0
- package/backend/lib/paths.js +85 -0
- package/backend/lib/peerClash.js +64 -0
- package/backend/lib/presence.js +35 -0
- package/backend/lib/relayOsBlock.js +84 -0
- package/backend/lib/repoSnapshot.js +125 -0
- package/backend/lib/room.js +498 -0
- package/backend/lib/roomAsk.js +148 -0
- package/backend/lib/roomAuth.js +215 -0
- package/backend/lib/roomControl.js +226 -0
- package/backend/lib/roomLocks.js +201 -0
- package/backend/lib/roomSignal.js +265 -0
- package/backend/lib/roomSync.js +194 -0
- package/backend/lib/seed.js +107 -0
- package/backend/lib/sseHub.js +40 -0
- package/backend/lib/store.js +79 -0
- package/backend/lib/supergraph.js +46 -0
- package/backend/lib/transcripts/discover.js +338 -0
- package/backend/lib/transcripts/parse.js +558 -0
- package/backend/lib/transcripts/sessionId.js +36 -0
- package/backend/lib/transcripts/sync.js +291 -0
- package/backend/lib/transcripts/util.js +299 -0
- package/backend/lib/treeSitterImports.js +219 -0
- package/backend/lib/watchWorkspace.js +42 -0
- package/backend/mcp/README.md +193 -0
- package/backend/mcp/server.js +42 -0
- package/backend/mcp/tools.js +484 -0
- package/backend/server.js +3839 -0
- package/cli/graph.js +278 -0
- package/cli/relay.js +419 -0
- package/docker-compose.falkor.yml +17 -0
- package/docs/MEMORY-GRAPH.md +269 -0
- package/hooks/relay-antigravity-post-tool.js +2 -0
- package/hooks/relay-antigravity-pre-read.js +2 -0
- package/hooks/relay-antigravity-pre-tool.js +2 -0
- package/hooks/relay-antigravity-stop.js +2 -0
- package/hooks/relay-claude-post-tool.js +2 -0
- package/hooks/relay-claude-pre-read.js +2 -0
- package/hooks/relay-claude-pre-tool.js +2 -0
- package/hooks/relay-claude-stop.js +2 -0
- package/hooks/relay-codex-post-tool.js +2 -0
- package/hooks/relay-codex-pre-read.js +2 -0
- package/hooks/relay-codex-pre-tool.js +2 -0
- package/hooks/relay-codex-stop.js +2 -0
- package/hooks/relay-copilot-post-tool.js +2 -0
- package/hooks/relay-copilot-pre-read.js +2 -0
- package/hooks/relay-copilot-pre-tool.js +2 -0
- package/hooks/relay-copilot-stop.js +2 -0
- package/hooks/relay-cursor-post-tool.js +2 -0
- package/hooks/relay-cursor-pre-read.js +2 -0
- package/hooks/relay-cursor-pre-tool.js +2 -0
- package/hooks/relay-cursor-stop.js +2 -0
- package/hooks/relay-hook-lib.js +621 -0
- package/mission-control/out/404.html +1 -0
- package/mission-control/out/__next.__PAGE__.txt +26 -0
- package/mission-control/out/__next._full.txt +21 -0
- package/mission-control/out/__next._tree.txt +4 -0
- package/mission-control/out/_next/static/chunks/0-3l04fnym95-.js +22 -0
- package/mission-control/out/_next/static/chunks/0cz1d0mv5g_q7.js +1 -0
- package/mission-control/out/_next/static/chunks/0ehjiuuxbbhq9.js +1 -0
- package/mission-control/out/_next/static/chunks/1q0cmt0kqufow.js +2 -0
- package/mission-control/out/_next/static/chunks/1ya1u0izct4f6.js +1 -0
- package/mission-control/out/_next/static/chunks/25rvg4bnew99f.css +4 -0
- package/mission-control/out/_next/static/chunks/2n66mlnb34vjq.js +1 -0
- package/mission-control/out/_next/static/chunks/2xar70jzx8f1y.css +1 -0
- package/mission-control/out/_next/static/chunks/31cgqj_-l15yt.js +31 -0
- package/mission-control/out/_next/static/chunks/3_5x4qn1_b4al.js +1 -0
- package/mission-control/out/_next/static/chunks/3o1tiwncjss_k.js +1 -0
- package/mission-control/out/_next/static/chunks/3vq9c4hivvgn_.js +1 -0
- package/mission-control/out/_next/static/chunks/turbopack-3hsllzpnzz12p.js +1 -0
- package/mission-control/out/_next/static/dglM39XRu0cmneClUkGYd/_buildManifest.js +11 -0
- package/mission-control/out/_next/static/dglM39XRu0cmneClUkGYd/_clientMiddlewareManifest.js +1 -0
- package/mission-control/out/_next/static/dglM39XRu0cmneClUkGYd/_ssgManifest.js +1 -0
- package/mission-control/out/_next/static/media/icon.27otmh_fv3buh.svg +4 -0
- package/mission-control/out/_not-found/__next._full.txt +18 -0
- package/mission-control/out/_not-found/__next._not-found/__PAGE__.txt +24 -0
- package/mission-control/out/_not-found/__next._tree.txt +3 -0
- package/mission-control/out/_not-found.html +1 -0
- package/mission-control/out/_not-found.txt +18 -0
- package/mission-control/out/agents/antigravity.png +0 -0
- package/mission-control/out/agents/claude.png +0 -0
- package/mission-control/out/agents/codex.png +0 -0
- package/mission-control/out/agents/copilot.png +0 -0
- package/mission-control/out/agents/cursor.png +0 -0
- package/mission-control/out/icon.svg +4 -0
- package/mission-control/out/index.html +1 -0
- package/mission-control/out/index.txt +21 -0
- package/mission-control/out/logo.png +0 -0
- package/mission-control/out/logo_transparent.png +0 -0
- package/package.json +74 -4
- package/scripts/falkor-rebuild.js +57 -0
- package/scripts/falkor-seed-demo.js +157 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const graphs = new Map();
|
|
2
|
+
|
|
3
|
+
function unionKey(workspaceId) {
|
|
4
|
+
return workspaceId || "default";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function ingest(workspaceId, { userId, edges }) {
|
|
8
|
+
const key = unionKey(workspaceId);
|
|
9
|
+
let g = graphs.get(key);
|
|
10
|
+
if (!g) {
|
|
11
|
+
g = { users: new Map(), edges: new Map() };
|
|
12
|
+
graphs.set(key, g);
|
|
13
|
+
}
|
|
14
|
+
g.users.set(userId || "anon", edges || []);
|
|
15
|
+
g.edges = new Map();
|
|
16
|
+
for (const list of g.users.values()) {
|
|
17
|
+
for (const e of list) {
|
|
18
|
+
const from = e.from || e[0];
|
|
19
|
+
const to = e.to || e[1];
|
|
20
|
+
if (!from || !to) continue;
|
|
21
|
+
if (!g.edges.has(from)) g.edges.set(from, new Set());
|
|
22
|
+
g.edges.get(from).add(to);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return snapshot(workspaceId);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function dependents(workspaceId, file) {
|
|
29
|
+
const g = graphs.get(unionKey(workspaceId));
|
|
30
|
+
if (!g) return [];
|
|
31
|
+
const out = [];
|
|
32
|
+
for (const [from, tos] of g.edges) {
|
|
33
|
+
if (tos.has(file)) out.push(from);
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function snapshot(workspaceId) {
|
|
39
|
+
const g = graphs.get(unionKey(workspaceId));
|
|
40
|
+
if (!g) return { users: 0, edges: [] };
|
|
41
|
+
const edges = [];
|
|
42
|
+
for (const [from, tos] of g.edges) for (const to of tos) edges.push({ from, to });
|
|
43
|
+
return { users: g.users.size, edges };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { ingest, dependents, snapshot };
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const {
|
|
4
|
+
HOME,
|
|
5
|
+
cursorSlug,
|
|
6
|
+
claudeSlug,
|
|
7
|
+
findJsonlFiles,
|
|
8
|
+
normalizeCompare,
|
|
9
|
+
readJsonl,
|
|
10
|
+
discoverWorkspaceStorageDir,
|
|
11
|
+
pathNeedle,
|
|
12
|
+
pathsMentionedIn,
|
|
13
|
+
pathsCoverWorkspace,
|
|
14
|
+
} = require("./util");
|
|
15
|
+
|
|
16
|
+
const AGENTS = ["Cursor", "Claude Code", "Codex", "Copilot", "Antigravity"];
|
|
17
|
+
|
|
18
|
+
function listCursorTranscriptFiles(transcriptDir) {
|
|
19
|
+
if (!transcriptDir || !fs.existsSync(transcriptDir)) return [];
|
|
20
|
+
const files = [];
|
|
21
|
+
for (const entry of fs.readdirSync(transcriptDir, { withFileTypes: true })) {
|
|
22
|
+
if (entry.isDirectory()) {
|
|
23
|
+
const jsonl = path.join(transcriptDir, entry.name, `${entry.name}.jsonl`);
|
|
24
|
+
if (fs.existsSync(jsonl)) files.push(jsonl);
|
|
25
|
+
} else if (entry.name.endsWith(".jsonl")) {
|
|
26
|
+
files.push(path.join(transcriptDir, entry.name));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return files.sort((a, b) => {
|
|
30
|
+
try {
|
|
31
|
+
return fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs;
|
|
32
|
+
} catch {
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Only the first bytes of a transcript are ever inspected here, but reading it
|
|
39
|
+
// with readFileSync pulled the whole file into memory first — and these files
|
|
40
|
+
// reach tens of megabytes. Done for several candidates per workspace, for every
|
|
41
|
+
// workspace, on a 3s sweep, that read alone was enough to stall the event loop.
|
|
42
|
+
function readHead(file, bytes) {
|
|
43
|
+
let fd;
|
|
44
|
+
try {
|
|
45
|
+
fd = fs.openSync(file, "r");
|
|
46
|
+
const buf = Buffer.allocUnsafe(bytes);
|
|
47
|
+
const read = fs.readSync(fd, buf, 0, bytes, 0);
|
|
48
|
+
return buf.slice(0, read).toString("utf8");
|
|
49
|
+
} catch {
|
|
50
|
+
return "";
|
|
51
|
+
} finally {
|
|
52
|
+
if (fd !== undefined) {
|
|
53
|
+
try {
|
|
54
|
+
fs.closeSync(fd);
|
|
55
|
+
} catch {
|
|
56
|
+
/* ignore */
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Resolving a workspace to its Cursor transcript directory means scanning every
|
|
63
|
+
// project Cursor knows about. The answer changes only when a directory appears,
|
|
64
|
+
// so a short cache keeps the sweep off that path entirely.
|
|
65
|
+
const CURSOR_DIR_TTL_MS = 30000;
|
|
66
|
+
const cursorDirCache = new Map();
|
|
67
|
+
|
|
68
|
+
function discoverCursorDir(workspacePath) {
|
|
69
|
+
const cached = cursorDirCache.get(workspacePath);
|
|
70
|
+
if (cached && Date.now() - cached.at < CURSOR_DIR_TTL_MS) {
|
|
71
|
+
// A cached directory that has since been removed must not be trusted.
|
|
72
|
+
if (!cached.dir || fs.existsSync(cached.dir)) return cached.dir;
|
|
73
|
+
}
|
|
74
|
+
const dir = findCursorDir(workspacePath);
|
|
75
|
+
cursorDirCache.set(workspacePath, { dir, at: Date.now() });
|
|
76
|
+
return dir;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function findCursorDir(workspacePath) {
|
|
80
|
+
const projectsRoot = path.join(HOME, ".cursor", "projects");
|
|
81
|
+
const direct = path.join(projectsRoot, cursorSlug(workspacePath), "agent-transcripts");
|
|
82
|
+
if (fs.existsSync(direct)) return direct;
|
|
83
|
+
if (!fs.existsSync(projectsRoot)) return null;
|
|
84
|
+
const target = normalizeCompare(workspacePath);
|
|
85
|
+
for (const entry of fs.readdirSync(projectsRoot, { withFileTypes: true })) {
|
|
86
|
+
if (!entry.isDirectory()) continue;
|
|
87
|
+
const candidate = path.join(projectsRoot, entry.name, "agent-transcripts");
|
|
88
|
+
if (!fs.existsSync(candidate)) continue;
|
|
89
|
+
for (const file of listCursorTranscriptFiles(candidate).slice(0, 4)) {
|
|
90
|
+
if (readHead(file, 12000).toLowerCase().includes(target)) return candidate;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function discoverCursor(workspacePath) {
|
|
97
|
+
const dir = discoverCursorDir(workspacePath);
|
|
98
|
+
if (!dir) return [];
|
|
99
|
+
return listCursorTranscriptFiles(dir);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// The `cwd` recorded on a Claude transcript row is the authoritative workspace
|
|
103
|
+
// for that session. The directory slug alone is not: it is lossy (every
|
|
104
|
+
// non-alphanumeric character collapses to "-"), so sibling paths can land in the
|
|
105
|
+
// same folder, and the folder can hold unrelated .jsonl files (e.g. `memory/`).
|
|
106
|
+
// Checking cwd keeps one workspace's dashboard from showing another's sessions.
|
|
107
|
+
function claudeTranscriptCwd(file) {
|
|
108
|
+
// `cwd` is stamped on the session's opening rows, so the head is enough; the
|
|
109
|
+
// tail of a long session can be tens of megabytes of no interest here.
|
|
110
|
+
const lines = readHead(file, 64000).replace(/^\uFEFF/, "").split("\n");
|
|
111
|
+
for (const line of lines) {
|
|
112
|
+
if (!line) continue;
|
|
113
|
+
let row = null;
|
|
114
|
+
try {
|
|
115
|
+
row = JSON.parse(line);
|
|
116
|
+
} catch {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (row?.cwd) return String(row.cwd);
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function discoverClaude(workspacePath) {
|
|
125
|
+
const root = path.join(HOME, ".claude", "projects");
|
|
126
|
+
const projectDir = path.join(root, claudeSlug(workspacePath));
|
|
127
|
+
if (!fs.existsSync(projectDir)) return [];
|
|
128
|
+
const target = normalizeCompare(workspacePath);
|
|
129
|
+
let files = [];
|
|
130
|
+
try {
|
|
131
|
+
files = fs
|
|
132
|
+
.readdirSync(projectDir, { withFileTypes: true })
|
|
133
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl"))
|
|
134
|
+
.map((entry) => path.join(projectDir, entry.name));
|
|
135
|
+
} catch {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
return files.filter((file) => {
|
|
139
|
+
const cwd = claudeTranscriptCwd(file);
|
|
140
|
+
return !cwd || normalizeCompare(cwd) === target;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function discoverCodex(workspacePath) {
|
|
145
|
+
const root = path.join(HOME, ".codex", "sessions");
|
|
146
|
+
if (!fs.existsSync(root)) return [];
|
|
147
|
+
const target = normalizeCompare(workspacePath);
|
|
148
|
+
const files = findJsonlFiles(root);
|
|
149
|
+
const matched = [];
|
|
150
|
+
for (const file of files) {
|
|
151
|
+
try {
|
|
152
|
+
const first = JSON.parse(readHead(file, 32000).split("\n")[0] || "{}");
|
|
153
|
+
if (first.type === "session_meta" && first.payload?.cwd && normalizeCompare(first.payload.cwd) === target) {
|
|
154
|
+
matched.push(file);
|
|
155
|
+
}
|
|
156
|
+
} catch {
|
|
157
|
+
/* skip */
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return matched;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function copilotWorkspaceStorageRoots() {
|
|
164
|
+
return [
|
|
165
|
+
path.join(HOME, "AppData", "Roaming", "Code", "User", "workspaceStorage"),
|
|
166
|
+
path.join(HOME, ".config", "Code", "User", "workspaceStorage"),
|
|
167
|
+
path.join(HOME, "Library", "Application Support", "Code", "User", "workspaceStorage"),
|
|
168
|
+
];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function discoverCopilot(workspacePath) {
|
|
172
|
+
const storage = discoverWorkspaceStorageDir(workspacePath, copilotWorkspaceStorageRoots());
|
|
173
|
+
if (!storage) return [];
|
|
174
|
+
const sessions = [];
|
|
175
|
+
const chatDir = path.join(storage, "chatSessions");
|
|
176
|
+
if (fs.existsSync(chatDir)) {
|
|
177
|
+
for (const entry of fs.readdirSync(chatDir, { withFileTypes: true })) {
|
|
178
|
+
if (entry.isFile() && entry.name.endsWith(".jsonl")) sessions.push(path.join(chatDir, entry.name));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const transcriptsDir = path.join(storage, "GitHub.copilot-chat", "transcripts");
|
|
182
|
+
if (fs.existsSync(transcriptsDir)) {
|
|
183
|
+
for (const entry of fs.readdirSync(transcriptsDir, { withFileTypes: true })) {
|
|
184
|
+
if (entry.isFile() && entry.name.endsWith(".jsonl")) sessions.push(path.join(transcriptsDir, entry.name));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return sessions;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Antigravity keeps one directory per conversation under `brain/<uuid>/`, with
|
|
191
|
+
// the chat log at `.system_generated/logs/transcript.jsonl`. There is no
|
|
192
|
+
// workspace slug anywhere in the layout, so the only way to tell which
|
|
193
|
+
// conversations belong to this repo is to read the paths each transcript talks
|
|
194
|
+
// about.
|
|
195
|
+
function antigravityRoots() {
|
|
196
|
+
return [
|
|
197
|
+
path.join(HOME, ".gemini", "antigravity-ide", "brain"),
|
|
198
|
+
path.join(HOME, ".antigravity", "brain"),
|
|
199
|
+
path.join(HOME, "AppData", "Roaming", "Antigravity IDE", "brain"),
|
|
200
|
+
path.join(HOME, "Library", "Application Support", "Antigravity IDE", "brain"),
|
|
201
|
+
].filter((dir) => fs.existsSync(dir));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Scanning every conversation on every dashboard poll would mean re-reading tens
|
|
205
|
+
// of megabytes every 8 seconds. The set of paths a transcript mentions can only
|
|
206
|
+
// change when the file does, so key the cache on size+mtime.
|
|
207
|
+
const MENTION_CACHE_MAX = 400;
|
|
208
|
+
const mentionCache = new Map();
|
|
209
|
+
|
|
210
|
+
function mentionedPaths(file) {
|
|
211
|
+
let stat;
|
|
212
|
+
try {
|
|
213
|
+
stat = fs.statSync(file);
|
|
214
|
+
} catch {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
const stamp = `${stat.size}:${stat.mtimeMs}`;
|
|
218
|
+
const hit = mentionCache.get(file);
|
|
219
|
+
if (hit && hit.stamp === stamp) return hit.paths;
|
|
220
|
+
let paths;
|
|
221
|
+
try {
|
|
222
|
+
paths = pathsMentionedIn(fs.readFileSync(file, "utf8"));
|
|
223
|
+
} catch {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
if (mentionCache.size >= MENTION_CACHE_MAX) mentionCache.delete(mentionCache.keys().next().value);
|
|
227
|
+
mentionCache.set(file, { stamp, paths });
|
|
228
|
+
return paths;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Walking every conversation directory and stat-sorting the result costs ~100ms.
|
|
232
|
+
// The dashboard polls each workspace every 8s and the transcript watcher fires
|
|
233
|
+
// on every keystroke Antigravity persists, so without a short cache this alone
|
|
234
|
+
// would keep a core busy.
|
|
235
|
+
const LISTING_TTL_MS = 3000;
|
|
236
|
+
let listingCache = { at: 0, files: [] };
|
|
237
|
+
|
|
238
|
+
function antigravityTranscripts() {
|
|
239
|
+
if (Date.now() - listingCache.at < LISTING_TTL_MS) return listingCache.files;
|
|
240
|
+
const rows = [];
|
|
241
|
+
for (const root of antigravityRoots()) {
|
|
242
|
+
for (const file of findJsonlFiles(root)) {
|
|
243
|
+
// `transcript_full.jsonl` is the same conversation with tool payloads
|
|
244
|
+
// inlined; parsing both would double every message.
|
|
245
|
+
if (path.basename(file) !== "transcript.jsonl") continue;
|
|
246
|
+
let mtimeMs = 0;
|
|
247
|
+
try {
|
|
248
|
+
mtimeMs = fs.statSync(file).mtimeMs;
|
|
249
|
+
} catch {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
rows.push({ file, mtimeMs });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
rows.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
256
|
+
listingCache = { at: Date.now(), files: rows.map((r) => r.file) };
|
|
257
|
+
return listingCache.files;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const ANTIGRAVITY_MAX_SESSIONS = 12;
|
|
261
|
+
|
|
262
|
+
function discoverAntigravity(workspacePath) {
|
|
263
|
+
const needle = pathNeedle(workspacePath);
|
|
264
|
+
if (!needle) return [];
|
|
265
|
+
const matched = [];
|
|
266
|
+
for (const file of antigravityTranscripts()) {
|
|
267
|
+
const paths = mentionedPaths(file);
|
|
268
|
+
if (!paths) continue;
|
|
269
|
+
if (pathsCoverWorkspace(paths, needle)) matched.push(file);
|
|
270
|
+
if (matched.length >= ANTIGRAVITY_MAX_SESSIONS) break;
|
|
271
|
+
}
|
|
272
|
+
return matched;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function discoverForAgent(agent, workspacePath) {
|
|
276
|
+
if (agent === "Cursor") return discoverCursor(workspacePath);
|
|
277
|
+
if (agent === "Claude Code") return discoverClaude(workspacePath);
|
|
278
|
+
if (agent === "Codex") return discoverCodex(workspacePath);
|
|
279
|
+
if (agent === "Copilot") return discoverCopilot(workspacePath);
|
|
280
|
+
if (agent === "Antigravity") return discoverAntigravity(workspacePath);
|
|
281
|
+
return [];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// A Stop hook's `transcript_path` is a hint about the session that just
|
|
285
|
+
// finished, not a filter. Returning only that file — which is what every
|
|
286
|
+
// discoverX used to do — meant an agent's other sessions vanished from the
|
|
287
|
+
// dashboard the moment one of its sessions ended. Treat the hint as one more
|
|
288
|
+
// file to be sure of, on top of everything normal discovery finds.
|
|
289
|
+
function discoverAgentFiles(agent, workspacePath, hint = {}) {
|
|
290
|
+
const files = discoverForAgent(agent, workspacePath);
|
|
291
|
+
const hinted = hint.transcript_path;
|
|
292
|
+
if (hinted && !files.some((f) => path.resolve(f) === path.resolve(hinted))) {
|
|
293
|
+
try {
|
|
294
|
+
if (fs.existsSync(hinted)) files.unshift(hinted);
|
|
295
|
+
} catch {
|
|
296
|
+
/* unreadable hint — normal discovery still stands */
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return files;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function discoverAll(workspacePath, hint = {}) {
|
|
303
|
+
const found = [];
|
|
304
|
+
for (const agent of AGENTS) {
|
|
305
|
+
const agentHint = hint.agent === agent ? hint : {};
|
|
306
|
+
for (const file of discoverAgentFiles(agent, workspacePath, agentHint)) {
|
|
307
|
+
found.push({ agent, file });
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return found;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Every agent whose transcripts we can find needs a root here, otherwise its
|
|
314
|
+
// chats only refresh on the dashboard's slow poll — which is what made
|
|
315
|
+
// Antigravity and Copilot look frozen while Cursor and Claude updated live.
|
|
316
|
+
function transcriptWatchRoots(workspacePath) {
|
|
317
|
+
const roots = [];
|
|
318
|
+
const cursorDir = discoverCursorDir(workspacePath);
|
|
319
|
+
if (cursorDir) roots.push(cursorDir);
|
|
320
|
+
const claudeDir = path.join(HOME, ".claude", "projects", claudeSlug(workspacePath));
|
|
321
|
+
if (fs.existsSync(claudeDir)) roots.push(claudeDir);
|
|
322
|
+
const codexDir = path.join(HOME, ".codex", "sessions");
|
|
323
|
+
if (fs.existsSync(codexDir)) roots.push(codexDir);
|
|
324
|
+
roots.push(...antigravityRoots());
|
|
325
|
+
const copilotStorage = discoverWorkspaceStorageDir(workspacePath, copilotWorkspaceStorageRoots());
|
|
326
|
+
if (copilotStorage) roots.push(copilotStorage);
|
|
327
|
+
return [...new Set(roots)];
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
module.exports = {
|
|
331
|
+
AGENTS,
|
|
332
|
+
discoverAll,
|
|
333
|
+
discoverAgentFiles,
|
|
334
|
+
listCursorTranscriptFiles,
|
|
335
|
+
discoverCursorDir,
|
|
336
|
+
transcriptWatchRoots,
|
|
337
|
+
copilotWorkspaceStorageRoots,
|
|
338
|
+
};
|