Zion Boggan
repos/TreeTrace/src/discover.js
zionboggan.com ↗
81 lines · javascript
History for this file →
1
import { readdirSync, statSync, existsSync, openSync, readSync, closeSync } from 'node:fs';
2
import { homedir } from 'node:os';
3
import { join, resolve, sep } from 'node:path';
4
 
5
export function mungePath(absPath) {
6
  return absPath.replace(/[^A-Za-z0-9-]/g, '-');
7
}
8
 
9
const CWD_PROBE_BYTES = 65536;
10
const CWD_RE = /"cwd"\s*:\s*"((?:[^"\\]|\\.)*)"/;
11
 
12
export function recordedCwd(filePath) {
13
  let fd;
14
  try {
15
    fd = openSync(filePath, 'r');
16
    const buf = Buffer.alloc(CWD_PROBE_BYTES);
17
    const bytes = readSync(fd, buf, 0, CWD_PROBE_BYTES, 0);
18
    const head = buf.toString('utf8', 0, bytes);
19
    const m = head.match(CWD_RE);
20
    if (!m) return null;
21
    try {
22
      return JSON.parse(`"${m[1]}"`);
23
    } catch {
24
      return m[1];
25
    }
26
  } catch {
27
    return null;
28
  } finally {
29
    if (fd !== undefined) {
30
      try {
31
        closeSync(fd);
32
      } catch {
33
 
34
      }
35
    }
36
  }
37
}
38
 
39
export function claudeProjectsRoot() {
40
  return process.env.CLAUDE_CONFIG_DIR
41
    ? join(process.env.CLAUDE_CONFIG_DIR, 'projects')
42
    : join(homedir(), '.claude', 'projects');
43
}
44
 
45
export function discoverSessions(projectDir) {
46
  const root = claudeProjectsRoot();
47
  if (!existsSync(root)) return [];
48
 
49
  const abs = resolve(projectDir);
50
  const exact = mungePath(abs);
51
  const prefix = mungePath(abs + sep);
52
 
53
  const sessions = [];
54
  for (const entry of readdirSync(root, { withFileTypes: true })) {
55
    if (!entry.isDirectory()) continue;
56
    if (entry.name !== exact && !entry.name.startsWith(prefix)) continue;
57
    const dir = join(root, entry.name);
58
    for (const f of readdirSync(dir, { withFileTypes: true })) {
59
 
60
      if (!f.isFile() || !f.name.endsWith('.jsonl')) continue;
61
      const path = join(dir, f.name);
62
      let st;
63
      try {
64
        st = statSync(path);
65
      } catch {
66
        continue;
67
      }
68
      const cwd = recordedCwd(path);
69
      if (cwd && resolve(cwd) !== abs) continue;
70
      sessions.push({
71
        path,
72
        sessionId: f.name.replace(/\.jsonl$/, ''),
73
        sizeBytes: st.size,
74
        mtimeMs: st.mtimeMs,
75
        storageDir: entry.name,
76
      });
77
    }
78
  }
79
  sessions.sort((a, b) => a.mtimeMs - b.mtimeMs);
80
  return sessions;
81
}