Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions internal/parser/kiro.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ type kiroMeta struct {
UpdatedAt string `json:"updated_at"`
}

// DiscoverKiroSessions finds all .jsonl session files under the
// Kiro CLI sessions directory. Layout:
// discoverLegacyJSONL finds all .jsonl session files under a Kiro
// CLI sessions directory. Layout:
// <sessionsDir>/<uuid>.jsonl (with companion <uuid>.json)
func DiscoverKiroSessions(sessionsDir string) []DiscoveredFile {
func (s kiroSourceSet) discoverLegacyJSONL(sessionsDir string) []DiscoveredFile {
entries, err := os.ReadDir(sessionsDir)
if err != nil {
return nil
Expand All @@ -58,9 +58,9 @@ func DiscoverKiroSessions(sessionsDir string) []DiscoveredFile {
return files
}

// FindKiroSourceFile locates a Kiro session file by its raw
// legacySourceFile locates a legacy Kiro JSONL session file by its raw
// session ID (without the "kiro:" prefix).
func FindKiroSourceFile(sessionsDir, rawID string) string {
func (s kiroSourceSet) legacySourceFile(sessionsDir, rawID string) string {
if sessionsDir == "" || !IsValidSessionID(rawID) {
return ""
}
Expand Down Expand Up @@ -100,10 +100,10 @@ func loadKiroMeta(jsonlPath string) *kiroMeta {
return &m
}

// ParseKiroSession parses a Kiro CLI session from its JSONL file.
// parseLegacySession parses a Kiro CLI session from its JSONL file.
// Returns (nil, nil, nil) if the file doesn't exist or contains
// no user/assistant messages.
func ParseKiroSession(
func (p *kiroProvider) parseLegacySession(
path, machine string,
) (*ParsedSession, []ParsedMessage, error) {
info, err := os.Stat(path)
Expand Down
125 changes: 2 additions & 123 deletions internal/parser/kiro_ide.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"

Expand Down Expand Up @@ -98,126 +97,6 @@ type kiroIDEActionOutput struct {
Message string `json:"message"`
}

// DiscoverKiroIDESessions finds all session files under the
// Kiro IDE globalStorage directory. It scans both:
// - <dir>/<workspace-hash>/<execution-hash>.chat (old format)
// - <dir>/workspace-sessions/<b64-path>/<uuid>.json (new format)
func DiscoverKiroIDESessions(dir string) []DiscoveredFile {
entries, err := os.ReadDir(dir)
if err != nil {
return nil
}

var files []DiscoveredFile

for _, wsEntry := range entries {
if !wsEntry.IsDir() {
continue
}
name := wsEntry.Name()
if name == "default" || name == "dev_data" ||
name == "index" || name == "workspace-sessions" ||
strings.HasPrefix(name, ".") {
continue
}

wsDir := filepath.Join(dir, name)
chatFiles, err := os.ReadDir(wsDir)
if err != nil {
continue
}
for _, cf := range chatFiles {
if cf.IsDir() ||
!strings.HasSuffix(cf.Name(), ".chat") {
continue
}
files = append(files, DiscoveredFile{
Path: filepath.Join(wsDir, cf.Name()),
Agent: AgentKiroIDE,
})
}
}

// Scan workspace-sessions for new-format session JSONs.
wsSessionsDir := filepath.Join(dir, "workspace-sessions")
wsDirs, err := os.ReadDir(wsSessionsDir)
if err == nil {
for _, wsEntry := range wsDirs {
if !wsEntry.IsDir() {
continue
}
wsDir := filepath.Join(wsSessionsDir, wsEntry.Name())
jsonFiles, err := os.ReadDir(wsDir)
if err != nil {
continue
}
for _, jf := range jsonFiles {
name := jf.Name()
if name == "sessions.json" ||
!strings.HasSuffix(name, ".json") {
continue
}
files = append(files, DiscoveredFile{
Path: filepath.Join(wsDir, name),
Agent: AgentKiroIDE,
})
}
}
}

sort.Slice(files, func(i, j int) bool {
return files[i].Path < files[j].Path
})
return files
}

// FindKiroIDESourceFile locates a Kiro IDE session file by
// raw session ID. Supports both formats:
// - Old: "<workspace-hash>:<filename-hash>" → .chat file
// - New: "<uuid>" → workspace-sessions/*/<uuid>.json
func FindKiroIDESourceFile(dir, rawID string) string {
cleanDir := filepath.Clean(dir)

// Old format: <workspace-hash>:<filename-hash>
wsHash, fileHash, ok := strings.Cut(rawID, ":")
if ok && IsValidSessionID(wsHash) && IsValidSessionID(fileHash) {
candidate := filepath.Join(dir, wsHash, fileHash+".chat")
if abs, err := filepath.Abs(candidate); err == nil &&
strings.HasPrefix(abs, cleanDir) {
if _, err := os.Stat(candidate); err == nil {
return candidate
}
}
}

// New format: rawID is a UUID, file is at
// workspace-sessions/<b64-path>/<uuid>.json
if !IsValidSessionID(rawID) {
return ""
}
wsSessionsDir := filepath.Join(dir, "workspace-sessions")
wsDirs, err := os.ReadDir(wsSessionsDir)
if err != nil {
return ""
}
for _, wsEntry := range wsDirs {
if !wsEntry.IsDir() {
continue
}
candidate := filepath.Join(
wsSessionsDir, wsEntry.Name(), rawID+".json",
)
abs, err := filepath.Abs(candidate)
if err != nil || !strings.HasPrefix(abs, cleanDir) {
continue
}
if _, err := os.Stat(candidate); err == nil {
return candidate
}
}
return ""
}

// isKiroIDESystemMessage returns true for system prompt and
// rules messages that should not be shown as user content.
func isKiroIDESystemMessage(content string) bool {
Expand All @@ -228,11 +107,11 @@ func isKiroIDESystemMessage(content string) bool {
strings.HasPrefix(content, "You are operating in a workspace")
}

// ParseKiroIDESession parses a Kiro IDE session file.
// parseSession parses a Kiro IDE session file.
// Supports both old (.chat) and new (.json) formats.
// Returns (nil, nil, nil) if the file doesn't exist or
// contains no meaningful messages.
func ParseKiroIDESession(
func parseKiroIDESession(
path, machine string,
) (*ParsedSession, []ParsedMessage, error) {
if strings.HasSuffix(path, ".json") {
Expand Down
151 changes: 151 additions & 0 deletions internal/parser/kiro_ide_provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package parser

import (
"context"
"os"
"path/filepath"
"strings"
)

// Kiro IDE stores sessions in two on-disk layouts: an old format keyed by a
// "<workspace-hash>:<filename-hash>" pair pointing at a <ws>/<file>.chat file,
// and a new format where a UUID names a workspace-sessions/<ws>/<uuid>.json
// file. It is a directory-of-files provider: discovery, watching, change
// classification, and fingerprinting come from JSONLSourceSet. The ParseFile
// option makes that source set a full SourceSet so it rides the generic
// factory; RawSessionIDSourceFiles reconstructs the file path for the old
// colon-joined IDs, which the filename-stem discovery scan cannot match.
func newKiroIDEProviderFactory(def AgentDef) ProviderFactory {
return newSourceSetFactory(
def,
kiroIDEProviderCapabilities(),
func(cfg ProviderConfig) SourceSet { return newKiroIDESourceSet(cfg.Roots) },
)
}

func newKiroIDESourceSet(roots []string) JSONLSourceSet {
return newJSONLSourceSet(AgentKiroIDE, roots,
withRecursive(),
withExtensions(".chat", ".json"),
withContentHashing(),
withIncludePath(isKiroIDESourcePath),
withSessionIDFromPath(kiroIDESessionIDFromPath),
withRawSessionIDSourceFiles(kiroIDERawSessionIDSourceFiles),
withParseFile(kiroIDEParseFile),
)
}

func kiroIDEParseFile(
_ context.Context, path string, req ParseRequest,
) ([]ParseResult, []string, error) {
sess, msgs, err := parseKiroIDESession(path, req.Machine)
if err != nil {
return nil, nil, err
}
if sess == nil {
return nil, nil, nil
}
if req.Fingerprint.Hash != "" {
sess.File.Hash = req.Fingerprint.Hash
}
return []ParseResult{{Session: *sess, Messages: msgs}}, nil, nil
}

// kiroIDERawSessionIDSourceFiles reconstructs candidate file paths from a raw
// session ID for both Kiro IDE layouts. The old format
// "<workspace-hash>:<filename-hash>" maps to <root>/<ws>/<file>.chat. The new
// format is a UUID whose file lives at workspace-sessions/<ws>/<uuid>.json, so
// every workspace-sessions subdirectory under each root yields a candidate.
// FindSource gates each candidate on existence via the shared path lookup.
func kiroIDERawSessionIDSourceFiles(roots []string, rawID string) []string {
wsHash, fileHash, hasColon := strings.Cut(rawID, ":")
oldFormat := hasColon && IsValidSessionID(wsHash) && IsValidSessionID(fileHash)
newFormat := IsValidSessionID(rawID)
if !oldFormat && !newFormat {
return nil
}
var candidates []string
for _, root := range roots {
if root == "" {
continue
}
if oldFormat {
candidates = append(
candidates,
filepath.Join(root, wsHash, fileHash+".chat"),
)
}
if newFormat {
wsSessionsDir := filepath.Join(root, "workspace-sessions")
entries, err := os.ReadDir(wsSessionsDir)
if err != nil {
continue
}
for _, entry := range entries {
if !entry.IsDir() {
continue
}
candidates = append(candidates, filepath.Join(
wsSessionsDir, entry.Name(), rawID+".json",
))
}
}
}
return candidates
}

func isKiroIDESourcePath(root, path string) bool {
rel, ok := relUnder(filepath.Clean(root), filepath.Clean(path))
if !ok {
return false
}
parts := strings.Split(rel, string(filepath.Separator))
for _, part := range parts {
if part == "" || part == "." || part == ".." {
return false
}
}
if len(parts) == 2 {
if parts[0] == "default" || parts[0] == "dev_data" ||
parts[0] == "index" || parts[0] == "workspace-sessions" ||
strings.HasPrefix(parts[0], ".") {
return false
}
return strings.HasSuffix(parts[1], ".chat")
}
return len(parts) == 3 &&
parts[0] == "workspace-sessions" &&
!strings.HasPrefix(parts[1], ".") &&
parts[2] != "sessions.json" &&
strings.HasSuffix(parts[2], ".json")
}

func kiroIDESessionIDFromPath(root, path string) string {
if !isKiroIDESourcePath(root, path) {
return ""
}
rel, ok := relUnder(filepath.Clean(root), filepath.Clean(path))
if !ok {
return ""
}
parts := strings.Split(rel, string(filepath.Separator))
if len(parts) == 2 {
return parts[0] + ":" + strings.TrimSuffix(parts[1], ".chat")
}
if len(parts) == 3 {
return strings.TrimSuffix(parts[2], ".json")
}
return ""
}

func kiroIDEProviderCapabilities() Capabilities {
return Capabilities{
Source: jsonlFileProviderSourceCapabilities(),
Content: ContentCapabilities{
FirstMessage: CapabilitySupported,
SessionName: CapabilitySupported,
ToolCalls: CapabilitySupported,
Model: CapabilitySupported,
},
}
}
Loading