-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathscanner_opencode.go
More file actions
186 lines (159 loc) · 4.47 KB
/
scanner_opencode.go
File metadata and controls
186 lines (159 loc) · 4.47 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
package main
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
_ "modernc.org/sqlite"
)
// OpenCodeScanner scans OpenCode sessions from its SQLite database.
// DB path is obtained via `opencode db path`.
type OpenCodeScanner struct {
bin string
dbPath string
}
// opencodeModel represents the JSON model field in session table
type opencodeModel struct {
ID string `json:"id"`
ProviderID string `json:"providerID"`
}
func NewOpenCodeScanner(cfg Config) *OpenCodeScanner {
bin := cfg.BinFor(PlatformOpenCode)
dbPath := getOpenCodeDBPath(bin)
return &OpenCodeScanner{
bin: bin,
dbPath: dbPath,
}
}
// getOpenCodeDBPath runs `bin db path` to obtain the SQLite database path.
func getOpenCodeDBPath(bin string) string {
out, err := exec.CommandContext(context.Background(), bin, "db", "path").Output()
if err != nil {
// 仅在命令存在但执行失败时输出警告(命令不存在时静默)
if !errors.Is(err, exec.ErrNotFound) {
fmt.Fprintf(os.Stderr, "warning: failed to get OpenCode DB path: %v\n", err)
}
return ""
}
return strings.TrimSpace(string(out))
}
func (s *OpenCodeScanner) Platform() Platform { return PlatformOpenCode }
func (s *OpenCodeScanner) DataDir() string {
if s.dbPath == "" {
return ""
}
return filepath.Dir(s.dbPath)
}
func (s *OpenCodeScanner) ScanProjects() ([]Project, error) {
if s.dbPath == "" {
return nil, nil
}
db, err := sql.Open("sqlite", s.dbPath+"?mode=ro")
if err != nil {
return nil, nil
}
defer func() { _ = db.Close() }()
// Verify the database is accessible
if pingErr := db.PingContext(context.Background()); pingErr != nil {
return nil, nil
}
query := `
SELECT s.id, s.title, s.model, s.time_updated, s.time_created, p.worktree,
(SELECT COUNT(*) FROM session_message sm WHERE sm.session_id = s.id) AS msg_count
FROM session s
JOIN project p ON s.project_id = p.id
ORDER BY s.time_updated DESC
`
rows, err := db.QueryContext(context.Background(), query)
if err != nil {
return nil, nil
}
defer func() { _ = rows.Close() }()
// Group sessions by worktree
projectMap := make(map[string][]Session)
var worktreeOrder []string
for rows.Next() {
var id, title, modelJSON, worktree string
var timeUpdated, timeCreated int64
var msgCount int
if err := rows.Scan(&id, &title, &modelJSON, &timeUpdated, &timeCreated, &worktree, &msgCount); err != nil {
continue
}
model := parseOpenCodeModel(modelJSON)
lastActive := time.Unix(timeUpdated, 0)
if created := time.Unix(timeCreated, 0); created.After(lastActive) {
lastActive = created
}
if title == "" {
title = untitledTitle
}
sess := Session{
ID: id,
Platform: PlatformOpenCode,
Title: truncate(title, 50),
Model: model,
LastActive: lastActive,
MsgCount: msgCount,
ProjectDir: worktree,
FilePath: fmt.Sprintf("%s#%s", s.dbPath, id),
ResumeArg: id,
ProjectPath: worktree,
}
if _, exists := projectMap[worktree]; !exists {
worktreeOrder = append(worktreeOrder, worktree)
}
projectMap[worktree] = append(projectMap[worktree], sess)
}
var projects []Project
for _, wt := range worktreeOrder {
projects = append(projects, Project{
Name: projectShortName(wt),
FullPath: wt,
Sessions: projectMap[wt],
})
}
return projects, nil
}
// parseOpenCodeModel extracts the model ID from the JSON model field.
func parseOpenCodeModel(modelJSON string) string {
var m opencodeModel
if err := json.Unmarshal([]byte(modelJSON), &m); err == nil && m.ID != "" {
return m.ID
}
return modelJSON
}
func (s *OpenCodeScanner) DeleteSession(sess Session) error {
db, err := sql.Open("sqlite", s.dbPath+"?mode=rw")
if err != nil {
return err
}
defer func() { _ = db.Close() }()
tx, err := db.BeginTx(context.Background(), nil)
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
// Delete messages first (foreign key constraint)
if _, err := tx.ExecContext(context.Background(), "DELETE FROM session_message WHERE session_id = ?", sess.ID); err != nil {
return err
}
if _, err := tx.ExecContext(context.Background(), "DELETE FROM session WHERE id = ?", sess.ID); err != nil {
return err
}
return tx.Commit()
}
func (s *OpenCodeScanner) DeleteProject(p Project) error {
for _, sess := range p.Sessions {
_ = s.DeleteSession(sess)
}
return nil
}
func (s *OpenCodeScanner) ResumeCmd(sess Session) []string {
return []string{s.bin, "-s", sess.ResumeArg}
}