Skip to content
Merged
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
34 changes: 34 additions & 0 deletions internal/parser/discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ var (
agent: AgentMiMoCode, dbName: "mimocode.db",
sessionSubdir: "session_diff",
}
icodemateFmt = openCodeFormat{
agent: AgentIcodemate, dbName: "icodemate.db",
sessionSubdir: "session_diff",
}
)

func resolveOpenCodeFormatSource(
Expand Down Expand Up @@ -400,6 +404,36 @@ func ParseKiloSQLiteVirtualPath(
return parseOpenCodeFormatVirtualPath(kiloFmt.dbName, sourcePath)
}

func ResolveIcodemateSource(root string) OpenCodeSource {
return resolveOpenCodeFormatSource(icodemateFmt, root)
}

func DiscoverIcodemateSessions(root string) []DiscoveredFile {
return discoverOpenCodeFormatSessions(icodemateFmt, root)
}

func FindIcodemateSourceFile(root, sessionID string) string {
return findOpenCodeFormatSourceFile(icodemateFmt, root, sessionID)
}

func IcodemateStorageSessionIDs(root string) map[string]struct{} {
return openCodeFormatStorageSessionIDs(icodemateFmt, root)
}

func ResolveIcodemateWatchRoots(root string) []string {
return resolveOpenCodeFormatWatchRoots(icodemateFmt, root)
}

func IcodemateSQLiteVirtualPath(dbPath, sessionID string) string {
return OpenCodeSQLiteVirtualPath(dbPath, sessionID)
}

func ParseIcodemateSQLiteVirtualPath(
sourcePath string,
) (dbPath, sessionID string, ok bool) {
return parseOpenCodeFormatVirtualPath(icodemateFmt.dbName, sourcePath)
}

// ResolveMiMoCodeSource detects whether a MiMoCode root is using
// file-backed storage (storage/session_diff) or SQLite storage.
func ResolveMiMoCodeSource(root string) OpenCodeSource {
Expand Down
65 changes: 65 additions & 0 deletions internal/parser/icodemate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package parser

import "strings"

// Icodemate uses OpenCode's storage format, and is exposed as a distinct
// agent with the icodemate: ID prefix.
func ParseIcodemateFile(
sessionPath, machine string,
) (*ParsedSession, []ParsedMessage, error) {
sess, msgs, err := ParseOpenCodeFile(sessionPath, machine)
if err != nil || sess == nil {
return sess, msgs, err
}
relabelOpenCodeSessionAsIcodemate(sess)
return sess, msgs, nil
}

func ParseIcodemateSession(
dbPath, sessionID, machine string,
) (*ParsedSession, []ParsedMessage, error) {
sess, msgs, err := ParseOpenCodeSession(dbPath, sessionID, machine)
if err != nil || sess == nil {
return sess, msgs, err
}
relabelOpenCodeSessionAsIcodemate(sess)
return sess, msgs, nil
}

func ListIcodemateSessionMeta(dbPath string) ([]OpenCodeSessionMeta, error) {
metas, err := ListOpenCodeSessionMeta(dbPath)
if err != nil {
return nil, err
}
for i := range metas {
metas[i].VirtualPath = IcodemateSQLiteVirtualPath(
dbPath, metas[i].SessionID,
)
}
return metas, nil
}

func IcodemateSourceMtime(sourcePath string) (int64, error) {
if sourcePath == "" {
return 0, nil
}
if dbPath, sessionID, ok := ParseIcodemateSQLiteVirtualPath(sourcePath); ok {
return openCodeSQLiteSessionMtime(dbPath, sessionID)
}
return openCodeStorageSessionMtime(sourcePath)
}

func relabelOpenCodeSessionAsIcodemate(sess *ParsedSession) {
sess.ID = strings.Replace(sess.ID, "opencode:", "icodemate:", 1)
if sess.ParentSessionID != "" {
sess.ParentSessionID = strings.Replace(
sess.ParentSessionID, "opencode:", "icodemate:", 1,
)
}
if sess.SourceSessionID != "" {
sess.SourceSessionID = strings.Replace(
sess.SourceSessionID, "opencode:", "icodemate:", 1,
)
}
sess.Agent = AgentIcodemate
}
111 changes: 111 additions & 0 deletions internal/parser/icodemate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package parser

import (
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestParseIcodemateFileRelabelsOpenCodeSession(t *testing.T) {
root := t.TempDir()
sessionPath := filepath.Join(
root, "storage", "session_diff", "global", "ses_icode.json",
)
writeOpenCodeStorageFile(t, sessionPath, map[string]any{
"id": "ses_icode",
"parentID": "ses_parent",
"directory": "/home/user/code/icodeapp",
"title": "IcodeMate Session",
"time": map[string]any{
"created": 1700000000000,
"updated": 1700000060000,
},
})
writeOpenCodeStorageFile(t, filepath.Join(
root, "storage", "message", "ses_icode", "msg_1.json",
), map[string]any{
"id": "msg_1",
"sessionID": "ses_icode",
"role": "user",
"time": map[string]any{
"created": 1700000000000,
},
})
writeOpenCodeStorageFile(t, filepath.Join(
root, "storage", "part", "msg_1", "prt_1.json",
), map[string]any{
"id": "prt_1",
"sessionID": "ses_icode",
"messageID": "msg_1",
"type": "text",
"text": "Hello from IcodeMate",
"time": map[string]any{
"created": 1700000000000,
},
})

sess, msgs, err := ParseIcodemateFile(sessionPath, "testmachine")
require.NoError(t, err)
require.NotNil(t, sess)
require.Len(t, msgs, 1)

assert.Equal(t, "icodemate:ses_icode", sess.ID)
assert.Equal(t, "icodemate:ses_parent", sess.ParentSessionID)
assert.Equal(t, AgentIcodemate, sess.Agent)
assert.Equal(t, "icodeapp", sess.Project)
assert.Equal(t, "Hello from IcodeMate", msgs[0].Content)
}

func TestDiscoverIcodemateSessions(t *testing.T) {
root := t.TempDir()
sessionPath := filepath.Join(
root, "storage", "session_diff", "global", "ses_icode.json",
)
writeOpenCodeStorageFile(t, sessionPath, map[string]any{
"id": "ses_icode",
"directory": "/home/user/code/icodeapp",
"time": map[string]any{
"created": 1700000000000,
"updated": 1700000060000,
},
})

files := DiscoverIcodemateSessions(root)
require.Len(t, files, 1)

assert.Equal(t, sessionPath, files[0].Path)
assert.Equal(t, "icodeapp", files[0].Project)
assert.Equal(t, AgentIcodemate, files[0].Agent)
}

func TestParseIcodemateSQLiteVirtualPath(t *testing.T) {
wantDBPath := filepath.Join(t.TempDir(), "icodemate.db")
virtual := wantDBPath + "#ses_icode"
dbPath, sessionID, ok := ParseIcodemateSQLiteVirtualPath(virtual)
require.True(t, ok)
assert.Equal(t, wantDBPath, dbPath)
assert.Equal(t, "ses_icode", sessionID)

_, _, ok = ParseIcodemateSQLiteVirtualPath(
filepath.Join(t.TempDir(), "opencode.db") + "#ses_icode",
)
assert.False(t, ok)
}

func TestDiscoverIcodemateSessionsEmptyDir(t *testing.T) {
root := t.TempDir()
files := DiscoverIcodemateSessions(root)
assert.Empty(t, files)
}

func TestDiscoverIcodemateSessionsNoSessionDiff(t *testing.T) {
root := t.TempDir()
writeOpenCodeStorageFile(t,
filepath.Join(root, "storage", "other", "x.json"),
map[string]any{"id": "x"},
)
files := DiscoverIcodemateSessions(root)
assert.Empty(t, files)
}
14 changes: 14 additions & 0 deletions internal/parser/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const (
AgentShelley AgentType = "shelley"
AgentAider AgentType = "aider"
AgentReasonix AgentType = "reasonix"
AgentIcodemate AgentType = "icodemate"
)

// AgentDef describes a supported coding agent's filesystem
Expand Down Expand Up @@ -655,6 +656,19 @@ var Registry = []AgentDef{
DiscoverFunc: DiscoverReasonixSessions,
FindSourceFunc: FindReasonixSourceFile,
},
{
Type: AgentIcodemate,
DisplayName: "IcodeMate",
EnvVar: "ICODEMATE_DIR",
ConfigKey: "icodemate_dirs",
DefaultDirs: []string{".local/share/icodemate"},
IDPrefix: "icodemate:",
WatchSubdirs: []string{"storage/session_diff"},
FileBased: true,
DiscoverFunc: DiscoverIcodemateSessions,
FindSourceFunc: FindIcodemateSourceFile,
WatchRootsFunc: ResolveIcodemateWatchRoots,
},
}

// NonFileBackedAgents returns agent types where FileBased is false.
Expand Down
1 change: 1 addition & 0 deletions internal/parser/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ func TestRegistryCompleteness(t *testing.T) {
AgentAntigravity,
AgentAntigravityCLI,
AgentIflow,
AgentIcodemate,
AgentWorkBuddy,
AgentZencoder,
AgentGptme,
Expand Down
Loading