orch-code 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/LICENSE +21 -0
- package/README.md +624 -0
- package/cmd/apply.go +111 -0
- package/cmd/auth.go +393 -0
- package/cmd/auth_test.go +100 -0
- package/cmd/diff.go +57 -0
- package/cmd/doctor.go +149 -0
- package/cmd/explain.go +192 -0
- package/cmd/explain_test.go +62 -0
- package/cmd/init.go +100 -0
- package/cmd/interactive.go +1372 -0
- package/cmd/interactive_input.go +45 -0
- package/cmd/interactive_input_test.go +55 -0
- package/cmd/logs.go +72 -0
- package/cmd/model.go +84 -0
- package/cmd/plan.go +149 -0
- package/cmd/provider.go +189 -0
- package/cmd/provider_model_doctor_test.go +91 -0
- package/cmd/root.go +67 -0
- package/cmd/run.go +123 -0
- package/cmd/run_engine.go +208 -0
- package/cmd/run_engine_test.go +30 -0
- package/cmd/session.go +589 -0
- package/cmd/session_helpers.go +54 -0
- package/cmd/session_integration_test.go +30 -0
- package/cmd/session_list_current_test.go +87 -0
- package/cmd/session_messages_test.go +163 -0
- package/cmd/session_runs_test.go +68 -0
- package/cmd/sprint1_integration_test.go +119 -0
- package/cmd/stats.go +173 -0
- package/cmd/stats_test.go +71 -0
- package/cmd/version.go +4 -0
- package/go.mod +45 -0
- package/go.sum +108 -0
- package/internal/agents/agent.go +31 -0
- package/internal/agents/coder.go +167 -0
- package/internal/agents/planner.go +155 -0
- package/internal/agents/reviewer.go +118 -0
- package/internal/agents/runtime.go +25 -0
- package/internal/agents/runtime_test.go +77 -0
- package/internal/auth/account.go +78 -0
- package/internal/auth/oauth.go +523 -0
- package/internal/auth/store.go +287 -0
- package/internal/confidence/policy.go +174 -0
- package/internal/confidence/policy_test.go +71 -0
- package/internal/confidence/scorer.go +253 -0
- package/internal/confidence/scorer_test.go +83 -0
- package/internal/config/config.go +331 -0
- package/internal/config/config_defaults_test.go +138 -0
- package/internal/execution/contract_builder.go +160 -0
- package/internal/execution/contract_builder_test.go +68 -0
- package/internal/execution/plan_compliance.go +161 -0
- package/internal/execution/plan_compliance_test.go +71 -0
- package/internal/execution/retry_directive.go +132 -0
- package/internal/execution/scope_guard.go +69 -0
- package/internal/logger/logger.go +120 -0
- package/internal/models/contracts_test.go +100 -0
- package/internal/models/models.go +269 -0
- package/internal/orchestrator/orchestrator.go +701 -0
- package/internal/orchestrator/orchestrator_retry_test.go +135 -0
- package/internal/orchestrator/review_engine_test.go +50 -0
- package/internal/orchestrator/state.go +42 -0
- package/internal/orchestrator/test_classifier_test.go +68 -0
- package/internal/patch/applier.go +131 -0
- package/internal/patch/applier_test.go +25 -0
- package/internal/patch/parser.go +89 -0
- package/internal/patch/patch.go +60 -0
- package/internal/patch/summary.go +30 -0
- package/internal/patch/validator.go +104 -0
- package/internal/planning/normalizer.go +416 -0
- package/internal/planning/normalizer_test.go +64 -0
- package/internal/providers/errors.go +35 -0
- package/internal/providers/openai/client.go +498 -0
- package/internal/providers/openai/client_test.go +187 -0
- package/internal/providers/provider.go +47 -0
- package/internal/providers/registry.go +32 -0
- package/internal/providers/registry_test.go +57 -0
- package/internal/providers/router.go +52 -0
- package/internal/providers/state.go +114 -0
- package/internal/providers/state_test.go +64 -0
- package/internal/repo/analyzer.go +188 -0
- package/internal/repo/context.go +83 -0
- package/internal/review/engine.go +267 -0
- package/internal/review/engine_test.go +103 -0
- package/internal/runstore/store.go +137 -0
- package/internal/runstore/store_test.go +59 -0
- package/internal/runtime/lock.go +150 -0
- package/internal/runtime/lock_test.go +57 -0
- package/internal/session/compaction.go +260 -0
- package/internal/session/compaction_test.go +36 -0
- package/internal/session/service.go +117 -0
- package/internal/session/service_test.go +113 -0
- package/internal/storage/storage.go +1498 -0
- package/internal/storage/storage_test.go +413 -0
- package/internal/testing/classifier.go +80 -0
- package/internal/testing/classifier_test.go +36 -0
- package/internal/tools/command.go +160 -0
- package/internal/tools/command_test.go +56 -0
- package/internal/tools/file.go +111 -0
- package/internal/tools/git.go +77 -0
- package/internal/tools/invalid_params_test.go +36 -0
- package/internal/tools/policy.go +98 -0
- package/internal/tools/policy_test.go +36 -0
- package/internal/tools/registry_test.go +52 -0
- package/internal/tools/result.go +30 -0
- package/internal/tools/search.go +86 -0
- package/internal/tools/tool.go +94 -0
- package/main.go +9 -0
- package/npm/orch.js +25 -0
- package/package.json +41 -0
- package/scripts/changelog.js +20 -0
- package/scripts/check-release-version.js +21 -0
- package/scripts/lib/release-utils.js +223 -0
- package/scripts/postinstall.js +157 -0
- package/scripts/release.js +52 -0
package/cmd/session.go
ADDED
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"errors"
|
|
6
|
+
"fmt"
|
|
7
|
+
"os"
|
|
8
|
+
"strings"
|
|
9
|
+
"time"
|
|
10
|
+
|
|
11
|
+
"github.com/furkanbeydemir/orch/internal/session"
|
|
12
|
+
"github.com/furkanbeydemir/orch/internal/storage"
|
|
13
|
+
"github.com/spf13/cobra"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
var sessionCmd = &cobra.Command{
|
|
17
|
+
Use: "session",
|
|
18
|
+
Short: "Manage run sessions",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
var sessionListCmd = &cobra.Command{
|
|
22
|
+
Use: "list",
|
|
23
|
+
Short: "List sessions",
|
|
24
|
+
RunE: runSessionList,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
var sessionCreateCmd = &cobra.Command{
|
|
28
|
+
Use: "create [name]",
|
|
29
|
+
Short: "Create a session",
|
|
30
|
+
Args: cobra.ExactArgs(1),
|
|
31
|
+
RunE: runSessionCreate,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
var sessionRunsCmd = &cobra.Command{
|
|
35
|
+
Use: "runs [name-or-id]",
|
|
36
|
+
Short: "List recent runs for a session",
|
|
37
|
+
Args: cobra.MaximumNArgs(1),
|
|
38
|
+
RunE: runSessionRuns,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
var sessionMessagesCmd = &cobra.Command{
|
|
42
|
+
Use: "messages [name-or-id]",
|
|
43
|
+
Short: "List recent messages for a session",
|
|
44
|
+
Args: cobra.MaximumNArgs(1),
|
|
45
|
+
RunE: runSessionMessages,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
var (
|
|
49
|
+
sessionCreateWorktree string
|
|
50
|
+
sessionRunsLimit int
|
|
51
|
+
sessionRunsStatus string
|
|
52
|
+
sessionRunsContains string
|
|
53
|
+
sessionRunsJSON bool
|
|
54
|
+
sessionMessagesLimit int
|
|
55
|
+
sessionMessagesJSON bool
|
|
56
|
+
sessionListJSON bool
|
|
57
|
+
sessionCurrentJSON bool
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
var sessionSelectCmd = &cobra.Command{
|
|
61
|
+
Use: "select [name-or-id]",
|
|
62
|
+
Short: "Select active session",
|
|
63
|
+
Args: cobra.ExactArgs(1),
|
|
64
|
+
RunE: runSessionSelect,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
var sessionCloseCmd = &cobra.Command{
|
|
68
|
+
Use: "close [name-or-id]",
|
|
69
|
+
Short: "Close a session",
|
|
70
|
+
Args: cobra.ExactArgs(1),
|
|
71
|
+
RunE: runSessionClose,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
var sessionCurrentCmd = &cobra.Command{
|
|
75
|
+
Use: "current",
|
|
76
|
+
Short: "Show active session",
|
|
77
|
+
RunE: runSessionCurrent,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
func init() {
|
|
81
|
+
rootCmd.AddCommand(sessionCmd)
|
|
82
|
+
sessionCmd.AddCommand(sessionListCmd)
|
|
83
|
+
sessionCmd.AddCommand(sessionCreateCmd)
|
|
84
|
+
sessionCmd.AddCommand(sessionSelectCmd)
|
|
85
|
+
sessionCmd.AddCommand(sessionCloseCmd)
|
|
86
|
+
sessionCmd.AddCommand(sessionCurrentCmd)
|
|
87
|
+
sessionCmd.AddCommand(sessionRunsCmd)
|
|
88
|
+
sessionCmd.AddCommand(sessionMessagesCmd)
|
|
89
|
+
|
|
90
|
+
sessionCreateCmd.Flags().StringVar(&sessionCreateWorktree, "worktree-path", "", "Optional worktree path for session execution")
|
|
91
|
+
sessionListCmd.Flags().BoolVar(&sessionListJSON, "json", false, "Output sessions as JSON")
|
|
92
|
+
sessionCurrentCmd.Flags().BoolVar(&sessionCurrentJSON, "json", false, "Output active session as JSON")
|
|
93
|
+
sessionRunsCmd.Flags().IntVar(&sessionRunsLimit, "limit", 20, "Maximum number of runs to show")
|
|
94
|
+
sessionRunsCmd.Flags().StringVar(&sessionRunsStatus, "status", "", "Filter runs by status")
|
|
95
|
+
sessionRunsCmd.Flags().StringVar(&sessionRunsContains, "contains", "", "Filter runs by task text")
|
|
96
|
+
sessionRunsCmd.Flags().BoolVar(&sessionRunsJSON, "json", false, "Output runs as JSON")
|
|
97
|
+
sessionMessagesCmd.Flags().IntVar(&sessionMessagesLimit, "limit", 40, "Maximum number of messages to show")
|
|
98
|
+
sessionMessagesCmd.Flags().BoolVar(&sessionMessagesJSON, "json", false, "Output messages as JSON")
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
func runSessionList(cmd *cobra.Command, args []string) error {
|
|
102
|
+
cwd, err := os.Getwd()
|
|
103
|
+
if err != nil {
|
|
104
|
+
return fmt.Errorf("failed to get working directory: %w", err)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
ctx, err := loadSessionContext(cwd)
|
|
108
|
+
if err != nil {
|
|
109
|
+
return err
|
|
110
|
+
}
|
|
111
|
+
defer ctx.Store.Close()
|
|
112
|
+
|
|
113
|
+
sessions, err := ctx.Store.ListSessions(ctx.ProjectID)
|
|
114
|
+
if err != nil {
|
|
115
|
+
return err
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if len(sessions) == 0 {
|
|
119
|
+
if sessionListJSON {
|
|
120
|
+
fmt.Println("[]")
|
|
121
|
+
return nil
|
|
122
|
+
}
|
|
123
|
+
fmt.Println("No sessions found.")
|
|
124
|
+
return nil
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if sessionListJSON {
|
|
128
|
+
type jsonSession struct {
|
|
129
|
+
ID string `json:"id"`
|
|
130
|
+
ProjectID string `json:"project_id"`
|
|
131
|
+
Name string `json:"name"`
|
|
132
|
+
Status string `json:"status"`
|
|
133
|
+
Worktree string `json:"worktree,omitempty"`
|
|
134
|
+
CreatedAt time.Time `json:"created_at"`
|
|
135
|
+
ClosedAt *time.Time `json:"closed_at,omitempty"`
|
|
136
|
+
IsActive bool `json:"is_active"`
|
|
137
|
+
}
|
|
138
|
+
payload := make([]jsonSession, 0, len(sessions))
|
|
139
|
+
for _, s := range sessions {
|
|
140
|
+
payload = append(payload, jsonSession{
|
|
141
|
+
ID: s.ID,
|
|
142
|
+
ProjectID: s.ProjectID,
|
|
143
|
+
Name: s.Name,
|
|
144
|
+
Status: s.Status,
|
|
145
|
+
Worktree: s.Worktree,
|
|
146
|
+
CreatedAt: s.CreatedAt,
|
|
147
|
+
ClosedAt: s.ClosedAt,
|
|
148
|
+
IsActive: s.IsActive,
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
encoded, marshalErr := json.MarshalIndent(payload, "", " ")
|
|
152
|
+
if marshalErr != nil {
|
|
153
|
+
return fmt.Errorf("failed to encode sessions json: %w", marshalErr)
|
|
154
|
+
}
|
|
155
|
+
fmt.Println(string(encoded))
|
|
156
|
+
return nil
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
for _, s := range sessions {
|
|
160
|
+
marker := " "
|
|
161
|
+
if s.IsActive {
|
|
162
|
+
marker = "*"
|
|
163
|
+
}
|
|
164
|
+
if s.Worktree != "" {
|
|
165
|
+
fmt.Printf("%s %s (%s) status=%s worktree=%s\n", marker, s.Name, s.ID, s.Status, s.Worktree)
|
|
166
|
+
} else {
|
|
167
|
+
fmt.Printf("%s %s (%s) status=%s\n", marker, s.Name, s.ID, s.Status)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return nil
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
func runSessionCreate(cmd *cobra.Command, args []string) error {
|
|
175
|
+
cwd, err := os.Getwd()
|
|
176
|
+
if err != nil {
|
|
177
|
+
return fmt.Errorf("failed to get working directory: %w", err)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
ctx, err := loadSessionContext(cwd)
|
|
181
|
+
if err != nil {
|
|
182
|
+
return err
|
|
183
|
+
}
|
|
184
|
+
defer ctx.Store.Close()
|
|
185
|
+
|
|
186
|
+
created, err := ctx.Store.CreateSessionWithWorktree(ctx.ProjectID, args[0], sessionCreateWorktree)
|
|
187
|
+
if err != nil {
|
|
188
|
+
if errors.Is(err, storage.ErrNameConflict) {
|
|
189
|
+
return fmt.Errorf("session already exists: %s", args[0])
|
|
190
|
+
}
|
|
191
|
+
return err
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if err := ctx.Store.SetActiveSession(ctx.ProjectID, created.ID); err != nil {
|
|
195
|
+
return err
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if created.Worktree != "" {
|
|
199
|
+
fmt.Printf("Created and selected session: %s (%s) worktree=%s\n", created.Name, created.ID, created.Worktree)
|
|
200
|
+
} else {
|
|
201
|
+
fmt.Printf("Created and selected session: %s (%s)\n", created.Name, created.ID)
|
|
202
|
+
}
|
|
203
|
+
return nil
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
func runSessionSelect(cmd *cobra.Command, args []string) error {
|
|
207
|
+
cwd, err := os.Getwd()
|
|
208
|
+
if err != nil {
|
|
209
|
+
return fmt.Errorf("failed to get working directory: %w", err)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
ctx, err := loadSessionContext(cwd)
|
|
213
|
+
if err != nil {
|
|
214
|
+
return err
|
|
215
|
+
}
|
|
216
|
+
defer ctx.Store.Close()
|
|
217
|
+
|
|
218
|
+
selected, err := ctx.Store.SelectSession(ctx.ProjectID, args[0])
|
|
219
|
+
if err != nil {
|
|
220
|
+
switch {
|
|
221
|
+
case errors.Is(err, storage.ErrSessionNotFound):
|
|
222
|
+
return fmt.Errorf("session not found: %s", args[0])
|
|
223
|
+
case errors.Is(err, storage.ErrSessionClosed):
|
|
224
|
+
return fmt.Errorf("cannot select closed session: %s", args[0])
|
|
225
|
+
default:
|
|
226
|
+
return err
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
fmt.Printf("Active session: %s (%s)\n", selected.Name, selected.ID)
|
|
231
|
+
return nil
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
func runSessionClose(cmd *cobra.Command, args []string) error {
|
|
235
|
+
cwd, err := os.Getwd()
|
|
236
|
+
if err != nil {
|
|
237
|
+
return fmt.Errorf("failed to get working directory: %w", err)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
ctx, err := loadSessionContext(cwd)
|
|
241
|
+
if err != nil {
|
|
242
|
+
return err
|
|
243
|
+
}
|
|
244
|
+
defer ctx.Store.Close()
|
|
245
|
+
|
|
246
|
+
if err := ctx.Store.CloseSession(ctx.ProjectID, args[0]); err != nil {
|
|
247
|
+
if errors.Is(err, storage.ErrSessionNotFound) {
|
|
248
|
+
return fmt.Errorf("session not found: %s", args[0])
|
|
249
|
+
}
|
|
250
|
+
return err
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
fmt.Printf("Closed session: %s\n", args[0])
|
|
254
|
+
return nil
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
func runSessionCurrent(cmd *cobra.Command, args []string) error {
|
|
258
|
+
cwd, err := os.Getwd()
|
|
259
|
+
if err != nil {
|
|
260
|
+
return fmt.Errorf("failed to get working directory: %w", err)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
ctx, err := loadSessionContext(cwd)
|
|
264
|
+
if err != nil {
|
|
265
|
+
return err
|
|
266
|
+
}
|
|
267
|
+
defer ctx.Store.Close()
|
|
268
|
+
|
|
269
|
+
if sessionCurrentJSON {
|
|
270
|
+
payload := map[string]any{
|
|
271
|
+
"id": ctx.Session.ID,
|
|
272
|
+
"project_id": ctx.ProjectID,
|
|
273
|
+
"name": ctx.Session.Name,
|
|
274
|
+
"status": ctx.Session.Status,
|
|
275
|
+
"worktree": ctx.Session.Worktree,
|
|
276
|
+
"is_active": true,
|
|
277
|
+
}
|
|
278
|
+
encoded, err := json.MarshalIndent(payload, "", " ")
|
|
279
|
+
if err != nil {
|
|
280
|
+
return fmt.Errorf("failed to encode current session json: %w", err)
|
|
281
|
+
}
|
|
282
|
+
fmt.Println(string(encoded))
|
|
283
|
+
return nil
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
fmt.Printf("Active session: %s (%s)\n", ctx.Session.Name, ctx.Session.ID)
|
|
287
|
+
if ctx.Session.Worktree != "" {
|
|
288
|
+
fmt.Printf("Worktree: %s\n", ctx.Session.Worktree)
|
|
289
|
+
}
|
|
290
|
+
return nil
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
func runSessionRuns(cmd *cobra.Command, args []string) error {
|
|
294
|
+
cwd, err := os.Getwd()
|
|
295
|
+
if err != nil {
|
|
296
|
+
return fmt.Errorf("failed to get working directory: %w", err)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
ctx, err := loadSessionContext(cwd)
|
|
300
|
+
if err != nil {
|
|
301
|
+
return err
|
|
302
|
+
}
|
|
303
|
+
defer ctx.Store.Close()
|
|
304
|
+
|
|
305
|
+
target := ctx.Session
|
|
306
|
+
if len(args) == 1 {
|
|
307
|
+
target, err = ctx.Store.GetSession(ctx.ProjectID, args[0])
|
|
308
|
+
if err != nil {
|
|
309
|
+
switch {
|
|
310
|
+
case errors.Is(err, storage.ErrSessionNotFound):
|
|
311
|
+
return fmt.Errorf("session not found: %s", args[0])
|
|
312
|
+
default:
|
|
313
|
+
return err
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
runs, err := ctx.Store.ListRunsBySessionFiltered(target.ID, sessionRunsLimit, sessionRunsStatus, sessionRunsContains)
|
|
319
|
+
if err != nil {
|
|
320
|
+
return err
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if len(runs) == 0 {
|
|
324
|
+
if sessionRunsJSON {
|
|
325
|
+
fmt.Println("[]")
|
|
326
|
+
return nil
|
|
327
|
+
}
|
|
328
|
+
fmt.Printf("No runs found for session %s (%s).\n", target.Name, target.ID)
|
|
329
|
+
return nil
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if sessionRunsJSON {
|
|
333
|
+
type jsonRun struct {
|
|
334
|
+
ID string `json:"id"`
|
|
335
|
+
SessionID string `json:"session_id"`
|
|
336
|
+
Status string `json:"status"`
|
|
337
|
+
Task string `json:"task"`
|
|
338
|
+
StartedAt time.Time `json:"started_at"`
|
|
339
|
+
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
|
340
|
+
Error string `json:"error,omitempty"`
|
|
341
|
+
}
|
|
342
|
+
payload := make([]jsonRun, 0, len(runs))
|
|
343
|
+
for _, run := range runs {
|
|
344
|
+
payload = append(payload, jsonRun{
|
|
345
|
+
ID: run.ID,
|
|
346
|
+
SessionID: run.SessionID,
|
|
347
|
+
Status: run.Status,
|
|
348
|
+
Task: run.Task,
|
|
349
|
+
StartedAt: run.StartedAt,
|
|
350
|
+
CompletedAt: run.CompletedAt,
|
|
351
|
+
Error: run.Error,
|
|
352
|
+
})
|
|
353
|
+
}
|
|
354
|
+
encoded, marshalErr := json.MarshalIndent(payload, "", " ")
|
|
355
|
+
if marshalErr != nil {
|
|
356
|
+
return fmt.Errorf("failed to encode runs json: %w", marshalErr)
|
|
357
|
+
}
|
|
358
|
+
fmt.Println(string(encoded))
|
|
359
|
+
return nil
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
for _, run := range runs {
|
|
363
|
+
line := fmt.Sprintf("- %s status=%s task=%q started=%s", run.ID, run.Status, run.Task, run.StartedAt.Format(time.RFC3339))
|
|
364
|
+
if run.CompletedAt != nil {
|
|
365
|
+
line += fmt.Sprintf(" completed=%s", run.CompletedAt.Format(time.RFC3339))
|
|
366
|
+
}
|
|
367
|
+
if run.Error != "" {
|
|
368
|
+
line += fmt.Sprintf(" error=%q", run.Error)
|
|
369
|
+
}
|
|
370
|
+
fmt.Println(line)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return nil
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
func runSessionMessages(cmd *cobra.Command, args []string) error {
|
|
377
|
+
cwd, err := os.Getwd()
|
|
378
|
+
if err != nil {
|
|
379
|
+
return fmt.Errorf("failed to get working directory: %w", err)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
ctx, err := loadSessionContext(cwd)
|
|
383
|
+
if err != nil {
|
|
384
|
+
return err
|
|
385
|
+
}
|
|
386
|
+
defer ctx.Store.Close()
|
|
387
|
+
|
|
388
|
+
target := ctx.Session
|
|
389
|
+
if len(args) == 1 {
|
|
390
|
+
target, err = ctx.Store.GetSession(ctx.ProjectID, args[0])
|
|
391
|
+
if err != nil {
|
|
392
|
+
switch {
|
|
393
|
+
case errors.Is(err, storage.ErrSessionNotFound):
|
|
394
|
+
return fmt.Errorf("session not found: %s", args[0])
|
|
395
|
+
default:
|
|
396
|
+
return err
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
svc := session.NewService(ctx.Store)
|
|
402
|
+
messages, err := svc.ListMessagesWithParts(target.ID, sessionMessagesLimit)
|
|
403
|
+
if err != nil {
|
|
404
|
+
return err
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if len(messages) == 0 {
|
|
408
|
+
if sessionMessagesJSON {
|
|
409
|
+
fmt.Println("[]")
|
|
410
|
+
return nil
|
|
411
|
+
}
|
|
412
|
+
fmt.Printf("No messages found for session %s (%s).\n", target.Name, target.ID)
|
|
413
|
+
return nil
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if sessionMessagesJSON {
|
|
417
|
+
type jsonPart struct {
|
|
418
|
+
ID string `json:"id"`
|
|
419
|
+
Type string `json:"type"`
|
|
420
|
+
Compacted bool `json:"compacted"`
|
|
421
|
+
Rendered string `json:"rendered"`
|
|
422
|
+
Payload string `json:"payload"`
|
|
423
|
+
}
|
|
424
|
+
type jsonMessage struct {
|
|
425
|
+
ID string `json:"id"`
|
|
426
|
+
SessionID string `json:"session_id"`
|
|
427
|
+
Role string `json:"role"`
|
|
428
|
+
ParentID string `json:"parent_id,omitempty"`
|
|
429
|
+
ProviderID string `json:"provider_id,omitempty"`
|
|
430
|
+
ModelID string `json:"model_id,omitempty"`
|
|
431
|
+
FinishReason string `json:"finish_reason,omitempty"`
|
|
432
|
+
Error string `json:"error,omitempty"`
|
|
433
|
+
CreatedAt time.Time `json:"created_at"`
|
|
434
|
+
Parts []jsonPart `json:"parts"`
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
payload := make([]jsonMessage, 0, len(messages))
|
|
438
|
+
for _, item := range messages {
|
|
439
|
+
parts := make([]jsonPart, 0, len(item.Parts))
|
|
440
|
+
for _, part := range item.Parts {
|
|
441
|
+
parts = append(parts, jsonPart{
|
|
442
|
+
ID: part.ID,
|
|
443
|
+
Type: part.Type,
|
|
444
|
+
Compacted: part.Compacted,
|
|
445
|
+
Rendered: renderSessionPart(part),
|
|
446
|
+
Payload: part.Payload,
|
|
447
|
+
})
|
|
448
|
+
}
|
|
449
|
+
payload = append(payload, jsonMessage{
|
|
450
|
+
ID: item.Message.ID,
|
|
451
|
+
SessionID: item.Message.SessionID,
|
|
452
|
+
Role: item.Message.Role,
|
|
453
|
+
ParentID: item.Message.ParentID,
|
|
454
|
+
ProviderID: item.Message.ProviderID,
|
|
455
|
+
ModelID: item.Message.ModelID,
|
|
456
|
+
FinishReason: item.Message.FinishReason,
|
|
457
|
+
Error: item.Message.Error,
|
|
458
|
+
CreatedAt: item.Message.CreatedAt,
|
|
459
|
+
Parts: parts,
|
|
460
|
+
})
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
encoded, marshalErr := json.MarshalIndent(payload, "", " ")
|
|
464
|
+
if marshalErr != nil {
|
|
465
|
+
return fmt.Errorf("failed to encode session messages json: %w", marshalErr)
|
|
466
|
+
}
|
|
467
|
+
fmt.Println(string(encoded))
|
|
468
|
+
return nil
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
for _, item := range messages {
|
|
472
|
+
line := fmt.Sprintf("- %s role=%s at=%s", item.Message.ID, item.Message.Role, item.Message.CreatedAt.Format(time.RFC3339))
|
|
473
|
+
if strings.TrimSpace(item.Message.ProviderID) != "" || strings.TrimSpace(item.Message.ModelID) != "" {
|
|
474
|
+
line += fmt.Sprintf(" model=%s/%s", item.Message.ProviderID, item.Message.ModelID)
|
|
475
|
+
}
|
|
476
|
+
if strings.TrimSpace(item.Message.FinishReason) != "" {
|
|
477
|
+
line += fmt.Sprintf(" finish=%s", item.Message.FinishReason)
|
|
478
|
+
}
|
|
479
|
+
if strings.TrimSpace(item.Message.Error) != "" {
|
|
480
|
+
line += fmt.Sprintf(" error=%q", item.Message.Error)
|
|
481
|
+
}
|
|
482
|
+
fmt.Println(line)
|
|
483
|
+
|
|
484
|
+
for _, part := range item.Parts {
|
|
485
|
+
fmt.Printf(" - %s\n", renderSessionPart(part))
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return nil
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
func renderSessionPart(part storage.SessionPart) string {
|
|
493
|
+
partType := strings.ToLower(strings.TrimSpace(part.Type))
|
|
494
|
+
if partType == "" {
|
|
495
|
+
partType = "unknown"
|
|
496
|
+
}
|
|
497
|
+
compactedSuffix := ""
|
|
498
|
+
if part.Compacted {
|
|
499
|
+
compactedSuffix = " compacted=true"
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
switch partType {
|
|
503
|
+
case "text":
|
|
504
|
+
text := strings.TrimSpace(session.ExtractTextPart(part))
|
|
505
|
+
if text == "" {
|
|
506
|
+
text = compactPayload(part.Payload, 140)
|
|
507
|
+
}
|
|
508
|
+
return fmt.Sprintf("part=text%s text=%q", compactedSuffix, text)
|
|
509
|
+
case "stage":
|
|
510
|
+
var payload map[string]any
|
|
511
|
+
if err := json.Unmarshal([]byte(strings.TrimSpace(part.Payload)), &payload); err != nil {
|
|
512
|
+
return fmt.Sprintf("part=stage%s payload=%q", compactedSuffix, compactPayload(part.Payload, 140))
|
|
513
|
+
}
|
|
514
|
+
actor := extractString(payload, "actor")
|
|
515
|
+
step := extractString(payload, "step")
|
|
516
|
+
message := extractString(payload, "message")
|
|
517
|
+
timestamp := extractString(payload, "timestamp")
|
|
518
|
+
if actor == "" && step == "" && message == "" {
|
|
519
|
+
if status := extractString(payload, "status"); status != "" {
|
|
520
|
+
runID := extractString(payload, "run_id")
|
|
521
|
+
return fmt.Sprintf("part=stage%s run_id=%q status=%q", compactedSuffix, runID, status)
|
|
522
|
+
}
|
|
523
|
+
return fmt.Sprintf("part=stage%s payload=%q", compactedSuffix, compactPayload(part.Payload, 140))
|
|
524
|
+
}
|
|
525
|
+
if len(message) > 140 {
|
|
526
|
+
message = message[:140] + "..."
|
|
527
|
+
}
|
|
528
|
+
return fmt.Sprintf("part=stage%s actor=%q step=%q message=%q at=%q", compactedSuffix, actor, step, message, timestamp)
|
|
529
|
+
case "compaction":
|
|
530
|
+
var payload map[string]any
|
|
531
|
+
if err := json.Unmarshal([]byte(strings.TrimSpace(part.Payload)), &payload); err != nil {
|
|
532
|
+
return fmt.Sprintf("part=compaction%s payload=%q", compactedSuffix, compactPayload(part.Payload, 140))
|
|
533
|
+
}
|
|
534
|
+
estimated, _ := payload["estimated_tokens"].(float64)
|
|
535
|
+
usable, _ := payload["usable_input"].(float64)
|
|
536
|
+
summary := extractString(payload, "summary")
|
|
537
|
+
if len(summary) > 120 {
|
|
538
|
+
summary = summary[:120] + "..."
|
|
539
|
+
}
|
|
540
|
+
return fmt.Sprintf("part=compaction%s estimated_tokens=%.0f usable_input=%.0f summary=%q", compactedSuffix, estimated, usable, summary)
|
|
541
|
+
case "error":
|
|
542
|
+
var payload map[string]any
|
|
543
|
+
if err := json.Unmarshal([]byte(strings.TrimSpace(part.Payload)), &payload); err == nil {
|
|
544
|
+
message := extractString(payload, "message")
|
|
545
|
+
if message != "" {
|
|
546
|
+
return fmt.Sprintf("part=error%s message=%q", compactedSuffix, message)
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return fmt.Sprintf("part=error%s payload=%q", compactedSuffix, compactPayload(part.Payload, 140))
|
|
550
|
+
default:
|
|
551
|
+
return fmt.Sprintf("part=%s%s payload=%q", partType, compactedSuffix, compactPayload(part.Payload, 140))
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
func extractString(payload map[string]any, key string) string {
|
|
556
|
+
if payload == nil {
|
|
557
|
+
return ""
|
|
558
|
+
}
|
|
559
|
+
if value, ok := payload[key].(string); ok {
|
|
560
|
+
return strings.TrimSpace(value)
|
|
561
|
+
}
|
|
562
|
+
return ""
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
func compactPayload(payload string, maxLen int) string {
|
|
566
|
+
trimmed := strings.TrimSpace(payload)
|
|
567
|
+
if trimmed == "" {
|
|
568
|
+
return ""
|
|
569
|
+
}
|
|
570
|
+
if maxLen <= 0 {
|
|
571
|
+
maxLen = 120
|
|
572
|
+
}
|
|
573
|
+
if len(trimmed) <= maxLen {
|
|
574
|
+
return trimmed
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
var parsed map[string]any
|
|
578
|
+
if err := json.Unmarshal([]byte(trimmed), &parsed); err == nil {
|
|
579
|
+
if text, ok := parsed["text"].(string); ok {
|
|
580
|
+
text = strings.TrimSpace(text)
|
|
581
|
+
if len(text) > maxLen {
|
|
582
|
+
return text[:maxLen] + "..."
|
|
583
|
+
}
|
|
584
|
+
return text
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return trimmed[:maxLen] + "..."
|
|
589
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"strings"
|
|
7
|
+
|
|
8
|
+
"github.com/furkanbeydemir/orch/internal/storage"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
type sessionContext struct {
|
|
12
|
+
Store *storage.Store
|
|
13
|
+
ProjectID string
|
|
14
|
+
Session storage.Session
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
func (c *sessionContext) ExecutionRoot(defaultRoot string) string {
|
|
18
|
+
if c == nil {
|
|
19
|
+
return defaultRoot
|
|
20
|
+
}
|
|
21
|
+
worktree := strings.TrimSpace(c.Session.Worktree)
|
|
22
|
+
if worktree == "" {
|
|
23
|
+
return defaultRoot
|
|
24
|
+
}
|
|
25
|
+
if filepath.IsAbs(worktree) {
|
|
26
|
+
return worktree
|
|
27
|
+
}
|
|
28
|
+
return filepath.Join(defaultRoot, worktree)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func loadSessionContext(repoRoot string) (*sessionContext, error) {
|
|
32
|
+
store, err := storage.Open(repoRoot)
|
|
33
|
+
if err != nil {
|
|
34
|
+
return nil, fmt.Errorf("failed to open session storage: %w", err)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
projectID, err := store.GetOrCreateProject()
|
|
38
|
+
if err != nil {
|
|
39
|
+
_ = store.Close()
|
|
40
|
+
return nil, fmt.Errorf("failed to resolve project: %w", err)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
session, err := store.EnsureDefaultSession(projectID)
|
|
44
|
+
if err != nil {
|
|
45
|
+
_ = store.Close()
|
|
46
|
+
return nil, fmt.Errorf("failed to resolve active session: %w", err)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return &sessionContext{
|
|
50
|
+
Store: store,
|
|
51
|
+
ProjectID: projectID,
|
|
52
|
+
Session: session,
|
|
53
|
+
}, nil
|
|
54
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"testing"
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
func TestSessionCommandsLifecycle(t *testing.T) {
|
|
8
|
+
repoRoot := t.TempDir()
|
|
9
|
+
t.Chdir(repoRoot)
|
|
10
|
+
|
|
11
|
+
if err := runSessionCreate(nil, []string{"feature-a"}); err != nil {
|
|
12
|
+
t.Fatalf("create session: %v", err)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if err := runSessionCurrent(nil, nil); err != nil {
|
|
16
|
+
t.Fatalf("current session: %v", err)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if err := runSessionSelect(nil, []string{"default"}); err != nil {
|
|
20
|
+
t.Fatalf("select default session: %v", err)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if err := runSessionClose(nil, []string{"feature-a"}); err != nil {
|
|
24
|
+
t.Fatalf("close session: %v", err)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if err := runSessionRuns(nil, []string{"default"}); err != nil {
|
|
28
|
+
t.Fatalf("session runs command: %v", err)
|
|
29
|
+
}
|
|
30
|
+
}
|