This commit is contained in:
2026-06-23 05:02:15 +08:00
commit e6f1776d4f
264 changed files with 54215 additions and 0 deletions
+110
View File
@@ -0,0 +1,110 @@
package cc
import (
"context"
"os"
"path/filepath"
"strings"
"time"
"github.com/fsnotify/fsnotify"
)
// EncodeCwdForProjectsDir converts an absolute path to claude's projects-dir
// encoding: both '/' and '.' are replaced with '-'.
func EncodeCwdForProjectsDir(absPath string) string {
return strings.NewReplacer("/", "-", ".", "-").Replace(absPath)
}
// ClaudeProjectsDir returns the claude transcript directory for a given cwd:
// <homeDir>/.claude/projects/<encoded-cwd>.
func ClaudeProjectsDir(homeDir, cwd string) string {
return filepath.Join(homeDir, ".claude", "projects", EncodeCwdForProjectsDir(cwd))
}
// SessionWatchOpts configures WatchForNewSession.
type SessionWatchOpts struct {
ProjectsDir string
Timeout time.Duration
}
// WatchForNewSession watches ProjectsDir for the first new .jsonl file that
// appears after the call, returning its basename without the .jsonl suffix
// (the claude session id). Returns ("", nil) on timeout or context cancellation,
// mirroring the TypeScript null-on-timeout semantics. MkdirAll ensures the
// target dir exists so a missing claude projects dir is handled symmetrically
// with the codex watcher — the first session is captured even if the dir was
// absent at call time.
func WatchForNewSession(ctx context.Context, opts SessionWatchOpts) (string, error) {
// Ensure the target dir exists before snapshotting and watching.
if err := os.MkdirAll(opts.ProjectsDir, 0o755); err != nil {
return "", nil
}
// Snapshot existing .jsonl files so we only react to truly new ones.
snapshot, err := snapshotJSONL(opts.ProjectsDir)
if err != nil {
return "", nil
}
watcher, err := fsnotify.NewWatcher()
if err != nil {
return "", nil
}
defer watcher.Close()
if err := watcher.Add(opts.ProjectsDir); err != nil {
return "", nil
}
timer := time.NewTimer(opts.Timeout)
defer timer.Stop()
for {
select {
case <-ctx.Done():
return "", nil
case <-timer.C:
return "", nil
case event, ok := <-watcher.Events:
if !ok {
return "", nil
}
if event.Op&(fsnotify.Create|fsnotify.Rename) == 0 {
continue
}
name := filepath.Base(event.Name)
if !strings.HasSuffix(name, ".jsonl") {
continue
}
if snapshot[name] {
continue
}
// Confirm the file actually exists (rename fires for deletes too).
if _, err := os.Stat(event.Name); err != nil {
continue
}
return strings.TrimSuffix(name, ".jsonl"), nil
case _, ok := <-watcher.Errors:
if !ok {
return "", nil
}
// Non-fatal watcher error; keep watching.
}
}
}
// snapshotJSONL returns the set of .jsonl filenames currently in dir.
func snapshotJSONL(dir string) (map[string]bool, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
snap := make(map[string]bool, len(entries))
for _, e := range entries {
if !e.IsDir() && strings.HasSuffix(e.Name(), ".jsonl") {
snap[e.Name()] = true
}
}
return snap, nil
}