# import: share dir-resolution and timestamp parsing across importers

`c93af10`→[main](/content/gh/entireio/cli/commits/main/index.html)·

computermode·3w ago·9 files·+52 added/-82 removed

Factor the last cross-importer duplication into shared helpers:
- resolveDir (discover.go): the overridePath-or-GetSessionDir block every Discover repeated; each Discover now resolves the dir in one line.
- parseTimestamp (linesplit.go): the RFC3339-or-zero parse used by cursor, pi, copilot, claude, and codexLineTime.

With discoverSessionFiles, splitLineTurns, resolveDir, and parseTimestamp, all the agent-agnostic plumbing is now shared; what remains per importer is the genuinely format-specific part — prompt-line detection and which agent token/model parsing to call (per-turn import needs each prompt's native-space line offset, which no agent method exposes, so detection can't be reused from the agent layer). Pure refactor; behavior unchanged, verified against the importtest fixtures and existing tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

## Sessions

5041f405df33View transcript

## Changes

9

- cmd/entire/cli/agentimport

- Mclaude.go+4/-13
  
  - Mcodex.go+4/-13
  
  - Mcopilot.go+4/-13
  
  - Mcursor.go+4/-14
  
  - Mdiscover.go+13
  
  - Mfactory.go+3/-8
  
  - Mgemini.go+3/-8
  
  - Mlinesplit.go+13
  
  - Mpi.go+4/-13

```
24 unmodified lines

25
26
27
28
29
30
31
32
33
34
35
28
29
30
31
32
33
20 unmodified lines

54
55
56
62
63
64
65
57
58
59
60
70
61
62
63
64

24 unmodified lines

// dir; sessionFilter, when non-empty, keeps only matching session IDs (the
// file stem).
func (claudeImporter) Discover(repoRoot, overridePath string, now time.Time, sessionFilter []string) ([]SessionFile, error) {
	dir := overridePath
	if dir == "" {
		ag := &claudecode.ClaudeCodeAgent{}
		d, err := ag.GetSessionDir(repoRoot)
		if err != nil {
			return nil, fmt.Errorf("resolve claude session dir: %w", err)
		}
		dir = d
	}
	dir, err := resolveDir(repoRoot, overridePath, "claude", (&claudecode.ClaudeCodeAgent{}).GetSessionDir)
	if err != nil {
		return nil, err
	}
	return discoverSessionFiles(dir, now, sessionFilter, jsonlSessionResolver(".jsonl", identitySessionID))
}

//nolint:nilerr // skip defensively; the line already parsed in isUserPromptLine
return nil, nil

ts, parseErr := time.Parse(time.RFC3339, rec.Timestamp)
	if parseErr != nil {
		ts = time.Time{}
	}
	return &Turn{
		UUID:      rec.UUID,
		Prompt:    transcript.ExtractUserContent(rec.Message),
		Model:     modelInRange(rawLines, start, end),
		CreatedAt: ts,
		CreatedAt: parseTimestamp(rec.Timestamp),
		Tokens:    tokens,
	}, nil
}
```

Mcmd/entire/cli/agentimport/claude.go+4/-13

```
36 unmodified lines

// Discover walks the Codex sessions tree and returns transcripts belonging to
// this repo (by session_meta cwd) modified within the lookback window.
func (codexImporter) Discover(repoRoot, overridePath string, now time.Time, sessionFilter []string) ([]SessionFile, error) {
	dir := overridePath
	if dir == "" {
		ag := &codex.CodexAgent{}
		d, err := ag.GetSessionDir(repoRoot)
		if err != nil {
			return nil, fmt.Errorf("resolve codex session dir: %w", err)
		}
		dir = d
	}
	dir, err := resolveDir(repoRoot, overridePath, "codex", (&codex.CodexAgent{}).GetSessionDir)
	if err != nil {
		return nil, err
	}
	cutoff := now.AddDate(0, 0, -LookbackDays)
	var out []SessionFile
}
```

Mcmd/entire/cli/agentimport/codex.go+4/-13

```
32 unmodified lines

// session.start context) modified within the lookback window. The session ID is
// the session-state subdirectory name.
func (copilotImporter) Discover(repoRoot, overridePath string, now time.Time, sessionFilter []string) ([]SessionFile, error) {
	dir := overridePath
	if dir == "" {
		ag := &copilotcli.CopilotCLIAgent{}
		d, err := ag.GetSessionDir(repoRoot)
		if err != nil {
			return nil, fmt.Errorf("resolve copilot session dir: %w", err)
		}
		dir = d
	}
	dir, err := resolveDir(repoRoot, overridePath, "copilot", (&copilotcli.CopilotCLIAgent{}).GetSessionDir)
	if err != nil {
		return nil, err
	}
	...
}
```

Mcmd/entire/cli/agentimport/copilot.go+4/-13

``` 
// lookback window. Cursor stores sessions either flat (<dir>/<id>.jsonl) or
// nested (<dir>/<id>/<id>.jsonl, the IDE layout); both are discovered.
func (cursorImporter) Discover(repoRoot, overridePath string, now time.Time, sessionFilter []string) ([]SessionFile, error) {
	dir := overridePath
	if dir == "" {
		ag := &cursor.CursorAgent{}
		d, err := ag.GetSessionDir(repoRoot)
		if err != nil {
			return nil, fmt.Errorf("resolve cursor session dir: %w", err)
		}
		dir = d
	}
	dir, err := resolveDir(repoRoot, overridePath, "cursor", (&cursor.CursorAgent{}).GetSessionDir)
	if err != nil {
		return nil, err
	}
	return discoverSessionFiles(dir, now, sessionFilter, func(dir string, e os.DirEntry) (string, string, bool) {
		id, path := cursorSessionFile(dir, e)
	...
}
}
```

Mcmd/entire/cli/agentimport/cursor.go+4/-14

``` 
import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"
)

// parseTimestamp parses an RFC3339 timestamp, returning the zero time when the
// string is empty or unparseable. Shared by the importers that read a per-turn
// timestamp off the transcript line.
func parseTimestamp(s string) time.Time {
	t, err := time.Parse(time.RFC3339, s)
	if err != nil {
		return time.Time{}
	}
	return t
}

// splitLineTurns is the shared per-turn scaffolding for line-based (JSONL)
// importers. It finds the user-prompt turn starts with isPrompt, then for each
// turn spanning raw lines [start, end) calls build to fill the agent-specific
```
