on main help sign in
initial agent setup
commite7258e38aeb46281fda474b8b7fcc3eee35edd9f authorKujtim Hoxha <kujtimii.h@gmail.com> dateSun 23 Mar 2025, 22:25 (2 years ago) parent8daa6e7
29 files changed+2206−108
.sqlfluff−2 LICENSE cmd/root.go+36−7 go.mod+51−2 go.sum+184−2 internal/app/services.go+11−1 internal/db/db.go+64−14 internal/db/messages.sql.go+117 internal/db/migrations/000001_initial.down.sql+5−1 internal/db/migrations/000001_initial.up.sql+38−2 internal/db/models.go+16−7 internal/db/querier.go+5−1 internal/db/sessions.sql.go+32−22 internal/db/sql/messages.sql+30 internal/db/sql/sessions.sql+5−3 internal/llm/agent/agent.go+17 internal/llm/agent/coder.go+175 internal/llm/llm.go+206 internal/llm/models/models.go+196 internal/llm/tools/bash.go+454 internal/llm/tools/shell/shell.go+294 internal/message/message.go+122 internal/session/session.go+21−18 internal/tui/components/repl/editor.go+38−14 internal/tui/components/repl/messages.go+26−6 internal/tui/components/repl/sessions.go+41−4 internal/tui/page/repl.go+1 internal/tui/tui.go+7 main.go+14−2
.sqlfluffdeleted−2
@@ -1,2 +0,0 @@
1−[sqlfluff:rules]
2−exclude_rules = AM04
LICENSEdeleted

No changes to the content.

cmd/root.go+36−7
@@ -1,6 +1,3 @@
1−/*
2−Copyright © 2025 NAME HERE <EMAIL ADDRESS>
3−*/
41package cmd
52
63import (
@@ -11,6 +8,7 @@ import (
118 tea "github.com/charmbracelet/bubbletea"
129 "github.com/kujtimiihoxha/termai/internal/app"
1310 "github.com/kujtimiihoxha/termai/internal/db"
11+ "github.com/kujtimiihoxha/termai/internal/llm/models"
1412 "github.com/kujtimiihoxha/termai/internal/tui"
1513 "github.com/spf13/cobra"
1614 "github.com/spf13/viper"
@@ -64,7 +62,7 @@ func setupSubscriptions(app *app.App) (chan tea.Msg, func()) {
6462 wg := sync.WaitGroup{}
6563 ctx, cancel := context.WithCancel(app.Context)
6664
67− if viper.GetBool("debug") {
65+ {
6866 sub := app.Logger.Subscribe(ctx)
6967 wg.Add(1)
7068 go func() {
@@ -84,6 +82,26 @@ func setupSubscriptions(app *app.App) (chan tea.Msg, func()) {
8482 wg.Done()
8583 }()
8684 }
85+ {
86+ sub := app.Messages.Subscribe(ctx)
87+ wg.Add(1)
88+ go func() {
89+ for ev := range sub {
90+ ch <- ev
91+ }
92+ wg.Done()
93+ }()
94+ }
95+ {
96+ sub := app.LLM.Subscribe(ctx)
97+ wg.Add(1)
98+ go func() {
99+ for ev := range sub {
100+ ch <- ev
101+ }
102+ wg.Done()
103+ }()
104+ }
87105 return ch, func() {
88106 cancel()
89107 wg.Wait()
@@ -111,15 +129,26 @@ func loadConfig() {
111129 viper.SetDefault("log.level", "info")
112130 viper.SetDefault("data.dir", ".termai")
113131
132+ // LLM
133+ viper.SetDefault("models.big", string(models.DefaultBigModel))
134+ viper.SetDefault("models.little", string(models.DefaultLittleModel))
135+ viper.SetDefault("providers.openai.key", os.Getenv("OPENAI_API_KEY"))
136+ viper.SetDefault("providers.anthropic.key", os.Getenv("ANTHROPIC_API_KEY"))
137+ viper.SetDefault("providers.common.max_tokens", 4000)
138+
139+ viper.SetDefault("agents.default", "coder")
114140 //
115141 viper.ReadInConfig()
142+
143+ workdir, err := os.Getwd()
144+ if err != nil {
145+ panic(err)
146+ }
147+ viper.Set("wd", workdir)
116148}
117149
118150func init() {
119151 loadConfig()
120− // Here you will define your flags and configuration settings.
121− // Cobra supports persistent flags, which, if defined here,
122− // will be global for your application.
123152
124153 rootCmd.Flags().BoolP("help", "h", false, "Help")
125154 rootCmd.Flags().BoolP("debug", "d", false, "Help")
go.mod+51−2
@@ -9,9 +9,13 @@ require (
99 github.com/charmbracelet/glamour v0.9.1
1010 github.com/charmbracelet/huh v0.6.0
1111 github.com/charmbracelet/lipgloss v1.1.0
12+ github.com/cloudwego/eino v0.3.17
13+ github.com/cloudwego/eino-ext/components/model/claude v0.0.0-20250320062631-616205c32186
14+ github.com/cloudwego/eino-ext/components/model/openai v0.0.0-20250320062631-616205c32186
1215 github.com/go-logfmt/logfmt v0.6.0
1316 github.com/golang-migrate/migrate/v4 v4.18.2
1417 github.com/google/uuid v1.6.0
18+ github.com/kujtimiihoxha/vimtea v0.0.3-0.20250317175717-9d8ba9c69840
1519 github.com/mattn/go-runewidth v0.0.16
1620 github.com/mattn/go-sqlite3 v1.14.24
1721 github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6
@@ -19,52 +23,97 @@ require (
1923 github.com/muesli/termenv v0.16.0
2024 github.com/spf13/cobra v1.9.1
2125 github.com/spf13/viper v1.20.0
22− golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0
26+ golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1
2327)
2428
2529require (
2630 github.com/alecthomas/chroma/v2 v2.15.0 // indirect
31+ github.com/anthropics/anthropic-sdk-go v0.2.0-alpha.8 // indirect
2732 github.com/atotto/clipboard v0.1.4 // indirect
33+ github.com/aws/aws-sdk-go-v2 v1.33.0 // indirect
34+ github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect
35+ github.com/aws/aws-sdk-go-v2/config v1.29.1 // indirect
36+ github.com/aws/aws-sdk-go-v2/credentials v1.17.54 // indirect
37+ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.24 // indirect
38+ github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.28 // indirect
39+ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.28 // indirect
40+ github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
41+ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect
42+ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.9 // indirect
43+ github.com/aws/aws-sdk-go-v2/service/sso v1.24.11 // indirect
44+ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.10 // indirect
45+ github.com/aws/aws-sdk-go-v2/service/sts v1.33.9 // indirect
46+ github.com/aws/smithy-go v1.22.1 // indirect
2847 github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
2948 github.com/aymerick/douceur v0.2.0 // indirect
49+ github.com/bytedance/sonic v1.12.2 // indirect
50+ github.com/bytedance/sonic/loader v0.2.0 // indirect
3051 github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
3152 github.com/charmbracelet/x/ansi v0.8.0 // indirect
3253 github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
3354 github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
3455 github.com/charmbracelet/x/term v0.2.1 // indirect
56+ github.com/cloudwego/base64x v0.1.4 // indirect
57+ github.com/cloudwego/eino-ext/libs/acl/openai v0.0.0-20250305023926-469de0301955 // indirect
58+ github.com/cloudwego/iasm v0.2.0 // indirect
3559 github.com/dlclark/regexp2 v1.11.4 // indirect
3660 github.com/dustin/go-humanize v1.0.1 // indirect
3761 github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
3862 github.com/fsnotify/fsnotify v1.8.0 // indirect
63+ github.com/getkin/kin-openapi v0.118.0 // indirect
64+ github.com/go-openapi/jsonpointer v0.19.5 // indirect
65+ github.com/go-openapi/swag v0.19.5 // indirect
3966 github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
67+ github.com/goph/emperror v0.17.2 // indirect
4068 github.com/gorilla/css v1.0.1 // indirect
4169 github.com/hashicorp/errwrap v1.1.0 // indirect
4270 github.com/hashicorp/go-multierror v1.1.1 // indirect
4371 github.com/inconshreveable/mousetrap v1.1.0 // indirect
44− github.com/kujtimiihoxha/vimtea v0.0.3-0.20250317175717-9d8ba9c69840 // indirect
72+ github.com/invopop/yaml v0.1.0 // indirect
73+ github.com/josharian/intern v1.0.0 // indirect
74+ github.com/json-iterator/go v1.1.12 // indirect
75+ github.com/klauspost/cpuid/v2 v2.0.9 // indirect
4576 github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
77+ github.com/mailru/easyjson v0.7.7 // indirect
4678 github.com/mattn/go-isatty v0.0.20 // indirect
4779 github.com/mattn/go-localereader v0.0.1 // indirect
4880 github.com/microcosm-cc/bluemonday v1.0.27 // indirect
4981 github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
82+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
83+ github.com/modern-go/reflect2 v1.0.2 // indirect
84+ github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
5085 github.com/muesli/cancelreader v0.2.2 // indirect
86+ github.com/nikolalohinski/gonja v1.5.3 // indirect
5187 github.com/pelletier/go-toml/v2 v2.2.3 // indirect
88+ github.com/perimeterx/marshmallow v1.1.4 // indirect
89+ github.com/pkg/errors v0.9.1 // indirect
5290 github.com/rivo/uniseg v0.4.7 // indirect
5391 github.com/sagikazarmark/locafero v0.7.0 // indirect
5492 github.com/sahilm/fuzzy v0.1.1 // indirect
93+ github.com/sashabaranov/go-openai v1.32.5 // indirect
94+ github.com/sirupsen/logrus v1.9.3 // indirect
95+ github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f // indirect
5596 github.com/sourcegraph/conc v0.3.0 // indirect
5697 github.com/spf13/afero v1.12.0 // indirect
5798 github.com/spf13/cast v1.7.1 // indirect
5899 github.com/spf13/pflag v1.0.6 // indirect
59100 github.com/subosito/gotenv v1.6.0 // indirect
101+ github.com/tidwall/gjson v1.18.0 // indirect
102+ github.com/tidwall/match v1.1.1 // indirect
103+ github.com/tidwall/pretty v1.2.1 // indirect
104+ github.com/tidwall/sjson v1.2.5 // indirect
105+ github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
60106 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
107+ github.com/yargevad/filepathx v1.0.0 // indirect
61108 github.com/yuin/goldmark v1.7.8 // indirect
62109 github.com/yuin/goldmark-emoji v1.0.5 // indirect
63110 go.uber.org/atomic v1.9.0 // indirect
64111 go.uber.org/multierr v1.9.0 // indirect
112+ golang.org/x/arch v0.11.0 // indirect
65113 golang.org/x/net v0.33.0 // indirect
66114 golang.org/x/sync v0.12.0 // indirect
67115 golang.org/x/sys v0.30.0 // indirect
68116 golang.org/x/text v0.23.0 // indirect
117+ gopkg.in/yaml.v2 v2.4.0 // indirect
69118 gopkg.in/yaml.v3 v3.0.1 // indirect
70119)
go.sum+184−2
@@ -1,21 +1,64 @@
11github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
22github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
3+github.com/airbrake/gobrake v3.6.1+incompatible/go.mod h1:wM4gu3Cn0W0K7GUuVWnlXZU11AGBXMILnrdOU8Kn00o=
34github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
45github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
56github.com/alecthomas/chroma/v2 v2.15.0 h1:LxXTQHFoYrstG2nnV9y2X5O94sOBzf0CIUpSTbpxvMc=
67github.com/alecthomas/chroma/v2 v2.15.0/go.mod h1:gUhVLrPDXPtp/f+L1jo9xepo9gL4eLwRuGAunSZMkio=
78github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
89github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
10+github.com/anthropics/anthropic-sdk-go v0.2.0-alpha.8 h1:ss/c/eeyILgoK2sMsTJdcdLdhY3wZSt//+nanM41B9w=
11+github.com/anthropics/anthropic-sdk-go v0.2.0-alpha.8/go.mod h1:GJxtdOs9K4neo8Gg65CjJ7jNautmldGli5/OFNabOoo=
912github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
1013github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
14+github.com/aws/aws-sdk-go-v2 v1.33.0 h1:Evgm4DI9imD81V0WwD+TN4DCwjUMdc94TrduMLbgZJs=
15+github.com/aws/aws-sdk-go-v2 v1.33.0/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U=
16+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg=
17+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3/go.mod h1:UbnqO+zjqk3uIt9yCACHJ9IVNhyhOCnYk8yA19SAWrM=
18+github.com/aws/aws-sdk-go-v2/config v1.29.1 h1:JZhGawAyZ/EuJeBtbQYnaoftczcb2drR2Iq36Wgz4sQ=
19+github.com/aws/aws-sdk-go-v2/config v1.29.1/go.mod h1:7bR2YD5euaxBhzt2y/oDkt3uNRb6tjFp98GlTFueRwk=
20+github.com/aws/aws-sdk-go-v2/credentials v1.17.54 h1:4UmqeOqJPvdvASZWrKlhzpRahAulBfyTJQUaYy4+hEI=
21+github.com/aws/aws-sdk-go-v2/credentials v1.17.54/go.mod h1:RTdfo0P0hbbTxIhmQrOsC/PquBZGabEPnCaxxKRPSnI=
22+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.24 h1:5grmdTdMsovn9kPZPI23Hhvp0ZyNm5cRO+IZFIYiAfw=
23+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.24/go.mod h1:zqi7TVKTswH3Ozq28PkmBmgzG1tona7mo9G2IJg4Cis=
24+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.28 h1:igORFSiH3bfq4lxKFkTSYDhJEUCYo6C8VKiWJjYwQuQ=
25+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.28/go.mod h1:3So8EA/aAYm36L7XIvCVwLa0s5N0P7o2b1oqnx/2R4g=
26+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.28 h1:1mOW9zAUMhTSrMDssEHS/ajx8JcAj/IcftzcmNlmVLI=
27+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.28/go.mod h1:kGlXVIWDfvt2Ox5zEaNglmq0hXPHgQFNMix33Tw22jA=
28+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
29+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
30+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y=
31+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE=
32+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.9 h1:TQmKDyETFGiXVhZfQ/I0cCFziqqX58pi4tKJGYGFSz0=
33+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.9/go.mod h1:HVLPK2iHQBUx7HfZeOQSEu3v2ubZaAY2YPbAm5/WUyY=
34+github.com/aws/aws-sdk-go-v2/service/sso v1.24.11 h1:kuIyu4fTT38Kj7YCC7ouNbVZSSpqkZ+LzIfhCr6Dg+I=
35+github.com/aws/aws-sdk-go-v2/service/sso v1.24.11/go.mod h1:Ro744S4fKiCCuZECXgOi760TiYylUM8ZBf6OGiZzJtY=
36+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.10 h1:l+dgv/64iVlQ3WsBbnn+JSbkj01jIi+SM0wYsj3y/hY=
37+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.10/go.mod h1:Fzsj6lZEb8AkTE5S68OhcbBqeWPsR8RnGuKPr8Todl8=
38+github.com/aws/aws-sdk-go-v2/service/sts v1.33.9 h1:BRVDbewN6VZcwr+FBOszDKvYeXY1kJ+GGMCcpghlw0U=
39+github.com/aws/aws-sdk-go-v2/service/sts v1.33.9/go.mod h1:f6vjfZER1M17Fokn0IzssOTMT2N8ZSq+7jnNF0tArvw=
40+github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro=
41+github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
1142github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
1243github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
1344github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
1445github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
1546github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
1647github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
48+github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
49+github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
50+github.com/bugsnag/bugsnag-go v1.4.0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
51+github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
52+github.com/bytedance/mockey v1.2.13 h1:jokWZAm/pUEbD939Rhznz615MKUCZNuvCFQlJ2+ntoo=
53+github.com/bytedance/mockey v1.2.13/go.mod h1:1BPHF9sol5R1ud/+0VEHGQq/+i2lN+GTsr3O2Q9IENY=
54+github.com/bytedance/sonic v1.12.2 h1:oaMFuRTpMHYLpCntGca65YWt5ny+wAceDERTkT2L9lg=
55+github.com/bytedance/sonic v1.12.2/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
56+github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
57+github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM=
58+github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
1759github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
1860github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
61+github.com/certifi/gocertifi v0.0.0-20190105021004-abcd57078448/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4=
1962github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
2063github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
2164github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI=
@@ -38,6 +81,18 @@ github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko
3881github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
3982github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
4083github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
84+github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
85+github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
86+github.com/cloudwego/eino v0.3.17 h1:cRQUCLU6897cautWe1u3les1H3OILasUIhnHzxr2QcE=
87+github.com/cloudwego/eino v0.3.17/go.mod h1:+kmJimGEcKuSI6OKhet7kBedkm1WUZS3H1QRazxgWUo=
88+github.com/cloudwego/eino-ext/components/model/claude v0.0.0-20250320062631-616205c32186 h1:GGneAI4dIuQaxXfgsaaytmFuIi57cFTQ0R7b8PDUOxg=
89+github.com/cloudwego/eino-ext/components/model/claude v0.0.0-20250320062631-616205c32186/go.mod h1:ABAc+C6D9zs6KZZKfuHUhsCi+YWFogbWlqsMkQ9wJYY=
90+github.com/cloudwego/eino-ext/components/model/openai v0.0.0-20250320062631-616205c32186 h1:4f7KLAI2/177oZZ2iJO2bpzeJU9KPg3TduVTR0ulzQk=
91+github.com/cloudwego/eino-ext/components/model/openai v0.0.0-20250320062631-616205c32186/go.mod h1:vs6irdysecD6QxymjsCTzAgN2JceOTPYPjTXfJCNcgQ=
92+github.com/cloudwego/eino-ext/libs/acl/openai v0.0.0-20250305023926-469de0301955 h1:fgvkmTqAalDfjdy3b6Ur2mh/KEwB9L2uvqS4MFgTOqc=
93+github.com/cloudwego/eino-ext/libs/acl/openai v0.0.0-20250305023926-469de0301955/go.mod h1:6CThw1XQx/ASXNt31yuvp0X4Yp4GprknQuIvP9VKDpw=
94+github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
95+github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
4196github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
4297github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4398github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -50,20 +105,40 @@ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6
50105github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
51106github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
52107github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
108+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
53109github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
54110github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
111+github.com/getkin/kin-openapi v0.118.0 h1:z43njxPmJ7TaPpMSCQb7PN0dEYno4tyBPQcrFdHoLuM=
112+github.com/getkin/kin-openapi v0.118.0/go.mod h1:l5e9PaFUo9fyLJCPGQeXI2ML8c3P8BHOEV2VaAVf/pc=
113+github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
114+github.com/go-check/check v0.0.0-20180628173108-788fd7840127 h1:0gkP6mzaMqkmpcJYCFOLkIBwI7xFExG03bbkOkCvUPI=
115+github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
55116github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
56117github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
118+github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
119+github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
120+github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY=
121+github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
122+github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
123+github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
57124github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
58125github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
126+github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
59127github.com/golang-migrate/migrate/v4 v4.18.2 h1:2VSCMz7x7mjyTXx3m2zPokOY82LTRgxK1yQYKo6wWQ8=
60128github.com/golang-migrate/migrate/v4 v4.18.2/go.mod h1:2CM6tJvn2kqPXwnXO/d3rAQYiyoIm180VsO8PRX6Rpk=
129+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
61130github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
62131github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
132+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
63133github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
64134github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
135+github.com/goph/emperror v0.17.2 h1:yLapQcmEsO0ipe9p5TaN22djm3OFV/TfM/fcYP0/J18=
136+github.com/goph/emperror v0.17.2/go.mod h1:+ZbQ+fUNO/6FNiUo0ujtMjhgad9Xa6fQL9KhH4LNHic=
137+github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
138+github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
65139github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
66140github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
141+github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
67142github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
68143github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
69144github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -71,10 +146,27 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l
71146github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
72147github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
73148github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
149+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
74150github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
75151github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
152+github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc=
153+github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q=
154+github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
155+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
156+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
157+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
158+github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
159+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
160+github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
161+github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
162+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
163+github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
164+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
165+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
76166github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
77167github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
168+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
169+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
78170github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
79171github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
80172github.com/kujtimiihoxha/vimtea v0.0.3-0.20250317175717-9d8ba9c69840 h1:AORwYXTzap8hg0zmTA5RWB/0fxv9F19dF42dCY0IsRc=
@@ -85,6 +177,12 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
85177github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
86178github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
87179github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
180+github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
181+github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
182+github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
183+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
184+github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
185+github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
88186github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
89187github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
90188github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
@@ -94,10 +192,19 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T
94192github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
95193github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
96194github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
195+github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
196+github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
97197github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
98198github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
99199github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
100200github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
201+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
202+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
203+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
204+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
205+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
206+github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
207+github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
101208github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
102209github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
103210github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
@@ -106,8 +213,18 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
106213github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
107214github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
108215github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
216+github.com/nikolalohinski/gonja v1.5.3 h1:GsA+EEaZDZPGJ8JtpeGN78jidhOlxeJROpqMT9fTj9c=
217+github.com/nikolalohinski/gonja v1.5.3/go.mod h1:RmjwxNiXAEqcq1HeK5SSMmqFJvKOfTfXhkJv6YBtPa4=
218+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
219+github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
220+github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
109221github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
110222github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
223+github.com/perimeterx/marshmallow v1.1.4 h1:pZLDH9RjlLGGorbXhcaQLhfuV0pFMNfPO55FuFkxqLw=
224+github.com/perimeterx/marshmallow v1.1.4/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
225+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
226+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
227+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
111228github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
112229github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
113230github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -116,11 +233,23 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
116233github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
117234github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
118235github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
236+github.com/rollbar/rollbar-go v1.0.2/go.mod h1:AcFs5f0I+c71bpHlXNNDbOWJiKwjFDtISeXco0L5PKQ=
119237github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
120238github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
121239github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
122240github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
123241github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
242+github.com/sashabaranov/go-openai v1.32.5 h1:/eNVa8KzlE7mJdKPZDj6886MUzZQjoVHyn0sLvIt5qA=
243+github.com/sashabaranov/go-openai v1.32.5/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
244+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
245+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
246+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
247+github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f h1:Z2cODYsUxQPofhpYRMQVwWz4yUVpHF+vPi+eUdruUYI=
248+github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f/go.mod h1:JqzWyvTuI2X4+9wOHmKSQCYxybB/8j6Ko43qVmXDuZg=
249+github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY=
250+github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec=
251+github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
252+github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
124253github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
125254github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
126255github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
@@ -134,13 +263,41 @@ github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
134263github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY=
135264github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
136265github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
266+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
267+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
268+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
269+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
137270github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
271+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
272+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
273+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
274+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
138275github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
139276github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
140277github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
141278github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
279+github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
280+github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
281+github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
282+github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
283+github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
284+github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
285+github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
286+github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
287+github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
288+github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
289+github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
290+github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
291+github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo=
292+github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
293+github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
294+github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
295+github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
296+github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
142297github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
143298github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
299+github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc=
300+github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA=
144301github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
145302github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
146303github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
@@ -148,22 +305,47 @@ github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC
148305github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
149306go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
150307go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
308+go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
309+go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
151310go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
152311go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
153−golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 h1:pVgRXcIictcr+lBQIFeiwuwtDIs4eL21OuM9nyAADmo=
154−golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
312+golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4=
313+golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
314+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
315+golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
316+golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
317+golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw=
318+golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
319+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
155320golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
156321golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
322+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
157323golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
158324golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
325+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
326+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
159327golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
328+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
160329golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
161330golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
162331golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
332+golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
333+golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
334+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
163335golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
164336golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
165337gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
338+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
166339gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
167340gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
341+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
342+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
343+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
344+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
345+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
346+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
347+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
348+gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
168349gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
169350gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
351+nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
internal/app/services.go+11−1
@@ -5,7 +5,9 @@ import (
55 "database/sql"
66
77 "github.com/kujtimiihoxha/termai/internal/db"
8+ "github.com/kujtimiihoxha/termai/internal/llm"
89 "github.com/kujtimiihoxha/termai/internal/logging"
10+ "github.com/kujtimiihoxha/termai/internal/message"
911 "github.com/kujtimiihoxha/termai/internal/session"
1012 "github.com/spf13/viper"
1113)
@@ -14,6 +16,8 @@ type App struct {
1416 Context context.Context
1517
1618 Sessions session.Service
19+ Messages message.Service
20+ LLM llm.Service
1721
1822 Logger logging.Interface
1923}
@@ -23,9 +27,15 @@ func New(ctx context.Context, conn *sql.DB) *App {
2327 log := logging.NewLogger(logging.Options{
2428 Level: viper.GetString("log.level"),
2529 })
30+ sessions := session.NewService(ctx, q)
31+ messages := message.NewService(ctx, q)
32+ llm := llm.NewService(ctx, log, sessions, messages)
33+
2634 return &App{
2735 Context: ctx,
28− Sessions: session.NewService(ctx, q),
36+ Sessions: sessions,
37+ Messages: messages,
38+ LLM: llm,
2939 Logger: log,
3040 }
3141}
internal/db/db.go+64−14
@@ -24,15 +24,30 @@ func New(db DBTX) *Queries {
2424func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
2525 q := Queries{db: db}
2626 var err error
27+ if q.createMessageStmt, err = db.PrepareContext(ctx, createMessage); err != nil {
28+ return nil, fmt.Errorf("error preparing query CreateMessage: %w", err)
29+ }
2730 if q.createSessionStmt, err = db.PrepareContext(ctx, createSession); err != nil {
2831 return nil, fmt.Errorf("error preparing query CreateSession: %w", err)
2932 }
33+ if q.deleteMessageStmt, err = db.PrepareContext(ctx, deleteMessage); err != nil {
34+ return nil, fmt.Errorf("error preparing query DeleteMessage: %w", err)
35+ }
3036 if q.deleteSessionStmt, err = db.PrepareContext(ctx, deleteSession); err != nil {
3137 return nil, fmt.Errorf("error preparing query DeleteSession: %w", err)
3238 }
39+ if q.deleteSessionMessagesStmt, err = db.PrepareContext(ctx, deleteSessionMessages); err != nil {
40+ return nil, fmt.Errorf("error preparing query DeleteSessionMessages: %w", err)
41+ }
42+ if q.getMessageStmt, err = db.PrepareContext(ctx, getMessage); err != nil {
43+ return nil, fmt.Errorf("error preparing query GetMessage: %w", err)
44+ }
3345 if q.getSessionByIDStmt, err = db.PrepareContext(ctx, getSessionByID); err != nil {
3446 return nil, fmt.Errorf("error preparing query GetSessionByID: %w", err)
3547 }
48+ if q.listMessagesBySessionStmt, err = db.PrepareContext(ctx, listMessagesBySession); err != nil {
49+ return nil, fmt.Errorf("error preparing query ListMessagesBySession: %w", err)
50+ }
3651 if q.listSessionsStmt, err = db.PrepareContext(ctx, listSessions); err != nil {
3752 return nil, fmt.Errorf("error preparing query ListSessions: %w", err)
3853 }
@@ -44,21 +59,46 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
4459
4560func (q *Queries) Close() error {
4661 var err error
62+ if q.createMessageStmt != nil {
63+ if cerr := q.createMessageStmt.Close(); cerr != nil {
64+ err = fmt.Errorf("error closing createMessageStmt: %w", cerr)
65+ }
66+ }
4767 if q.createSessionStmt != nil {
4868 if cerr := q.createSessionStmt.Close(); cerr != nil {
4969 err = fmt.Errorf("error closing createSessionStmt: %w", cerr)
5070 }
5171 }
72+ if q.deleteMessageStmt != nil {
73+ if cerr := q.deleteMessageStmt.Close(); cerr != nil {
74+ err = fmt.Errorf("error closing deleteMessageStmt: %w", cerr)
75+ }
76+ }
5277 if q.deleteSessionStmt != nil {
5378 if cerr := q.deleteSessionStmt.Close(); cerr != nil {
5479 err = fmt.Errorf("error closing deleteSessionStmt: %w", cerr)
5580 }
5681 }
82+ if q.deleteSessionMessagesStmt != nil {
83+ if cerr := q.deleteSessionMessagesStmt.Close(); cerr != nil {
84+ err = fmt.Errorf("error closing deleteSessionMessagesStmt: %w", cerr)
85+ }
86+ }
87+ if q.getMessageStmt != nil {
88+ if cerr := q.getMessageStmt.Close(); cerr != nil {
89+ err = fmt.Errorf("error closing getMessageStmt: %w", cerr)
90+ }
91+ }
5792 if q.getSessionByIDStmt != nil {
5893 if cerr := q.getSessionByIDStmt.Close(); cerr != nil {
5994 err = fmt.Errorf("error closing getSessionByIDStmt: %w", cerr)
6095 }
6196 }
97+ if q.listMessagesBySessionStmt != nil {
98+ if cerr := q.listMessagesBySessionStmt.Close(); cerr != nil {
99+ err = fmt.Errorf("error closing listMessagesBySessionStmt: %w", cerr)
100+ }
101+ }
62102 if q.listSessionsStmt != nil {
63103 if cerr := q.listSessionsStmt.Close(); cerr != nil {
64104 err = fmt.Errorf("error closing listSessionsStmt: %w", cerr)
@@ -106,23 +146,33 @@ func (q *Queries) queryRow(ctx context.Context, stmt *sql.Stmt, query string, ar
106146}
107147
108148type Queries struct {
109− db DBTX
110− tx *sql.Tx
111− createSessionStmt *sql.Stmt
112− deleteSessionStmt *sql.Stmt
113− getSessionByIDStmt *sql.Stmt
114− listSessionsStmt *sql.Stmt
115− updateSessionStmt *sql.Stmt
149+ db DBTX
150+ tx *sql.Tx
151+ createMessageStmt *sql.Stmt
152+ createSessionStmt *sql.Stmt
153+ deleteMessageStmt *sql.Stmt
154+ deleteSessionStmt *sql.Stmt
155+ deleteSessionMessagesStmt *sql.Stmt
156+ getMessageStmt *sql.Stmt
157+ getSessionByIDStmt *sql.Stmt
158+ listMessagesBySessionStmt *sql.Stmt
159+ listSessionsStmt *sql.Stmt
160+ updateSessionStmt *sql.Stmt
116161}
117162
118163func (q *Queries) WithTx(tx *sql.Tx) *Queries {
119164 return &Queries{
120− db: tx,
121− tx: tx,
122− createSessionStmt: q.createSessionStmt,
123− deleteSessionStmt: q.deleteSessionStmt,
124− getSessionByIDStmt: q.getSessionByIDStmt,
125− listSessionsStmt: q.listSessionsStmt,
126− updateSessionStmt: q.updateSessionStmt,
165+ db: tx,
166+ tx: tx,
167+ createMessageStmt: q.createMessageStmt,
168+ createSessionStmt: q.createSessionStmt,
169+ deleteMessageStmt: q.deleteMessageStmt,
170+ deleteSessionStmt: q.deleteSessionStmt,
171+ deleteSessionMessagesStmt: q.deleteSessionMessagesStmt,
172+ getMessageStmt: q.getMessageStmt,
173+ getSessionByIDStmt: q.getSessionByIDStmt,
174+ listMessagesBySessionStmt: q.listMessagesBySessionStmt,
175+ listSessionsStmt: q.listSessionsStmt,
176+ updateSessionStmt: q.updateSessionStmt,
127177 }
128178}
internal/db/messages.sql.gonew file+117
@@ -0,0 +1,117 @@
1+// Code generated by sqlc. DO NOT EDIT.
2+// versions:
3+// sqlc v1.27.0
4+// source: messages.sql
5+
6+package db
7+
8+import (
9+ "context"
10+)
11+
12+const createMessage = `-- name: CreateMessage :one
13+INSERT INTO messages (
14+ id,
15+ session_id,
16+ message_data,
17+ created_at,
18+ updated_at
19+) VALUES (
20+ ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now')
21+)
22+RETURNING id, session_id, message_data, created_at, updated_at
23+`
24+
25+type CreateMessageParams struct {
26+ ID string `json:"id"`
27+ SessionID string `json:"session_id"`
28+ MessageData string `json:"message_data"`
29+}
30+
31+func (q *Queries) CreateMessage(ctx context.Context, arg CreateMessageParams) (Message, error) {
32+ row := q.queryRow(ctx, q.createMessageStmt, createMessage, arg.ID, arg.SessionID, arg.MessageData)
33+ var i Message
34+ err := row.Scan(
35+ &i.ID,
36+ &i.SessionID,
37+ &i.MessageData,
38+ &i.CreatedAt,
39+ &i.UpdatedAt,
40+ )
41+ return i, err
42+}
43+
44+const deleteMessage = `-- name: DeleteMessage :exec
45+DELETE FROM messages
46+WHERE id = ?
47+`
48+
49+func (q *Queries) DeleteMessage(ctx context.Context, id string) error {
50+ _, err := q.exec(ctx, q.deleteMessageStmt, deleteMessage, id)
51+ return err
52+}
53+
54+const deleteSessionMessages = `-- name: DeleteSessionMessages :exec
55+DELETE FROM messages
56+WHERE session_id = ?
57+`
58+
59+func (q *Queries) DeleteSessionMessages(ctx context.Context, sessionID string) error {
60+ _, err := q.exec(ctx, q.deleteSessionMessagesStmt, deleteSessionMessages, sessionID)
61+ return err
62+}
63+
64+const getMessage = `-- name: GetMessage :one
65+SELECT id, session_id, message_data, created_at, updated_at
66+FROM messages
67+WHERE id = ? LIMIT 1
68+`
69+
70+func (q *Queries) GetMessage(ctx context.Context, id string) (Message, error) {
71+ row := q.queryRow(ctx, q.getMessageStmt, getMessage, id)
72+ var i Message
73+ err := row.Scan(
74+ &i.ID,
75+ &i.SessionID,
76+ &i.MessageData,
77+ &i.CreatedAt,
78+ &i.UpdatedAt,
79+ )
80+ return i, err
81+}
82+
83+const listMessagesBySession = `-- name: ListMessagesBySession :many
84+SELECT id, session_id, message_data, created_at, updated_at
85+FROM messages
86+WHERE session_id = ?
87+ORDER BY created_at ASC
88+`
89+
90+func (q *Queries) ListMessagesBySession(ctx context.Context, sessionID string) ([]Message, error) {
91+ rows, err := q.query(ctx, q.listMessagesBySessionStmt, listMessagesBySession, sessionID)
92+ if err != nil {
93+ return nil, err
94+ }
95+ defer rows.Close()
96+ items := []Message{}
97+ for rows.Next() {
98+ var i Message
99+ if err := rows.Scan(
100+ &i.ID,
101+ &i.SessionID,
102+ &i.MessageData,
103+ &i.CreatedAt,
104+ &i.UpdatedAt,
105+ ); err != nil {
106+ return nil, err
107+ }
108+ items = append(items, i)
109+ }
110+ if err := rows.Close(); err != nil {
111+ return nil, err
112+ }
113+ if err := rows.Err(); err != nil {
114+ return nil, err
115+ }
116+ return items, nil
117+}
internal/db/migrations/000001_initial.down.sql+5−1
@@ -1,4 +1,8 @@
1−-- sqlfluff:dialect:sqlite
21DROP TRIGGER IF EXISTS update_sessions_updated_at;
2+DROP TRIGGER IF EXISTS update_messages_updated_at;
3+
4+DROP TRIGGER IF EXISTS update_session_message_count_on_delete;
5+DROP TRIGGER IF EXISTS update_session_message_count_on_insert;
36
47DROP TABLE IF EXISTS sessions;
8+DROP TABLE IF EXISTS messages;
internal/db/migrations/000001_initial.up.sql+38−2
@@ -1,9 +1,10 @@
1−-- sqlfluff:dialect:sqlite
1+-- Sessions
22CREATE TABLE IF NOT EXISTS sessions (
33 id TEXT PRIMARY KEY,
44 title TEXT NOT NULL,
55 message_count INTEGER NOT NULL DEFAULT 0 CHECK (message_count >= 0),
6− tokens INTEGER NOT NULL DEFAULT 0 CHECK (tokens >= 0),
6+ prompt_tokens INTEGER NOT NULL DEFAULT 0 CHECK (prompt_tokens >= 0),
7+ completion_tokens INTEGER NOT NULL DEFAULT 0 CHECK (completion_tokens>= 0),
78 cost REAL NOT NULL DEFAULT 0.0 CHECK (cost >= 0.0),
89 updated_at INTEGER NOT NULL, -- Unix timestamp in milliseconds
910 created_at INTEGER NOT NULL -- Unix timestamp in milliseconds
@@ -15,3 +16,38 @@ BEGIN
1516UPDATE sessions SET updated_at = strftime('%s', 'now')
1617WHERE id = new.id;
1718END;
19+
20+-- Messages
21+CREATE TABLE IF NOT EXISTS messages (
22+ id TEXT PRIMARY KEY,
23+ session_id TEXT NOT NULL,
24+ message_data TEXT NOT NULL, -- JSON string of message content
25+ created_at INTEGER NOT NULL, -- Unix timestamp in milliseconds
26+ updated_at INTEGER NOT NULL, -- Unix timestamp in milliseconds
27+ FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE CASCADE
28+);
29+
30+CREATE INDEX IF NOT EXISTS idx_messages_session_id ON messages (session_id);
31+
32+CREATE TRIGGER IF NOT EXISTS update_messages_updated_at
33+AFTER UPDATE ON messages
34+BEGIN
35+UPDATE messages SET updated_at = strftime('%s', 'now')
36+WHERE id = new.id;
37+END;
38+
39+CREATE TRIGGER IF NOT EXISTS update_session_message_count_on_insert
40+AFTER INSERT ON messages
41+BEGIN
42+UPDATE sessions SET
43+ message_count = message_count + 1
44+WHERE id = new.session_id;
45+END;
46+
47+CREATE TRIGGER IF NOT EXISTS update_session_message_count_on_delete
48+AFTER DELETE ON messages
49+BEGIN
50+UPDATE sessions SET
51+ message_count = message_count - 1
52+WHERE id = old.session_id;
53+END;
internal/db/models.go+16−7
@@ -4,12 +4,21 @@
44
55package db
66
7+type Message struct {
8+ ID string `json:"id"`
9+ SessionID string `json:"session_id"`
10+ MessageData string `json:"message_data"`
11+ CreatedAt int64 `json:"created_at"`
12+ UpdatedAt int64 `json:"updated_at"`
13+}
14+
715type Session struct {
8− ID string `json:"id"`
9− Title string `json:"title"`
10− MessageCount int64 `json:"message_count"`
11− Tokens int64 `json:"tokens"`
12− Cost float64 `json:"cost"`
13− UpdatedAt int64 `json:"updated_at"`
14− CreatedAt int64 `json:"created_at"`
16+ ID string `json:"id"`
17+ Title string `json:"title"`
18+ MessageCount int64 `json:"message_count"`
19+ PromptTokens int64 `json:"prompt_tokens"`
20+ CompletionTokens int64 `json:"completion_tokens"`
21+ Cost float64 `json:"cost"`
22+ UpdatedAt int64 `json:"updated_at"`
23+ CreatedAt int64 `json:"created_at"`
1524}
internal/db/querier.go+5−1
@@ -9,10 +9,14 @@ import (
99)
1010
1111type Querier interface {
12− // sqlfluff:dialect:sqlite
12+ CreateMessage(ctx context.Context, arg CreateMessageParams) (Message, error)
1313 CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error)
14+ DeleteMessage(ctx context.Context, id string) error
1415 DeleteSession(ctx context.Context, id string) error
16+ DeleteSessionMessages(ctx context.Context, sessionID string) error
17+ GetMessage(ctx context.Context, id string) (Message, error)
1518 GetSessionByID(ctx context.Context, id string) (Session, error)
19+ ListMessagesBySession(ctx context.Context, sessionID string) ([]Message, error)
1620 ListSessions(ctx context.Context) ([]Session, error)
1721 UpdateSession(ctx context.Context, arg UpdateSessionParams) (Session, error)
1822}
internal/db/sessions.sql.go+32−22
@@ -14,7 +14,8 @@ INSERT INTO sessions (
1414 id,
1515 title,
1616 message_count,
17− tokens,
17+ prompt_tokens,
18+ completion_tokens,
1819 cost,
1920 updated_at,
2021 created_at
@@ -24,26 +25,28 @@ INSERT INTO sessions (
2425 ?,
2526 ?,
2627 ?,
28+ ?,
2729 strftime('%s', 'now'),
2830 strftime('%s', 'now')
29−) RETURNING id, title, message_count, tokens, cost, updated_at, created_at
31+) RETURNING id, title, message_count, prompt_tokens, completion_tokens, cost, updated_at, created_at
3032`
3133
3234type CreateSessionParams struct {
33− ID string `json:"id"`
34− Title string `json:"title"`
35− MessageCount int64 `json:"message_count"`
36− Tokens int64 `json:"tokens"`
37− Cost float64 `json:"cost"`
35+ ID string `json:"id"`
36+ Title string `json:"title"`
37+ MessageCount int64 `json:"message_count"`
38+ PromptTokens int64 `json:"prompt_tokens"`
39+ CompletionTokens int64 `json:"completion_tokens"`
40+ Cost float64 `json:"cost"`
3841}
3942
40−// sqlfluff:dialect:sqlite
4143func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error) {
4244 row := q.queryRow(ctx, q.createSessionStmt, createSession,
4345 arg.ID,
4446 arg.Title,
4547 arg.MessageCount,
46− arg.Tokens,
48+ arg.PromptTokens,
49+ arg.CompletionTokens,
4750 arg.Cost,
4851 )
4952 var i Session
@@ -51,7 +54,8 @@ func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (S
5154 &i.ID,
5255 &i.Title,
5356 &i.MessageCount,
54− &i.Tokens,
57+ &i.PromptTokens,
58+ &i.CompletionTokens,
5559 &i.Cost,
5660 &i.UpdatedAt,
5761 &i.CreatedAt,
@@ -70,7 +74,7 @@ func (q *Queries) DeleteSession(ctx context.Context, id string) error {
7074}
7175
7276const getSessionByID = `-- name: GetSessionByID :one
73−SELECT id, title, message_count, tokens, cost, updated_at, created_at
77+SELECT id, title, message_count, prompt_tokens, completion_tokens, cost, updated_at, created_at
7478FROM sessions
7579WHERE id = ? LIMIT 1
7680`
@@ -82,7 +86,8 @@ func (q *Queries) GetSessionByID(ctx context.Context, id string) (Session, error
8286 &i.ID,
8387 &i.Title,
8488 &i.MessageCount,
85− &i.Tokens,
89+ &i.PromptTokens,
90+ &i.CompletionTokens,
8691 &i.Cost,
8792 &i.UpdatedAt,
8893 &i.CreatedAt,
@@ -91,7 +96,7 @@ func (q *Queries) GetSessionByID(ctx context.Context, id string) (Session, error
9196}
9297
9398const listSessions = `-- name: ListSessions :many
94−SELECT id, title, message_count, tokens, cost, updated_at, created_at
99+SELECT id, title, message_count, prompt_tokens, completion_tokens, cost, updated_at, created_at
95100FROM sessions
96101ORDER BY created_at DESC
97102`
@@ -109,7 +114,8 @@ func (q *Queries) ListSessions(ctx context.Context) ([]Session, error) {
109114 &i.ID,
110115 &i.Title,
111116 &i.MessageCount,
112− &i.Tokens,
117+ &i.PromptTokens,
118+ &i.CompletionTokens,
113119 &i.Cost,
114120 &i.UpdatedAt,
115121 &i.CreatedAt,
@@ -131,23 +137,26 @@ const updateSession = `-- name: UpdateSession :one
131137UPDATE sessions
132138SET
133139 title = ?,
134− tokens = ?,
140+ prompt_tokens = ?,
141+ completion_tokens = ?,
135142 cost = ?
136143WHERE id = ?
137−RETURNING id, title, message_count, tokens, cost, updated_at, created_at
144+RETURNING id, title, message_count, prompt_tokens, completion_tokens, cost, updated_at, created_at
138145`
139146
140147type UpdateSessionParams struct {
141− Title string `json:"title"`
142− Tokens int64 `json:"tokens"`
143− Cost float64 `json:"cost"`
144− ID string `json:"id"`
148+ Title string `json:"title"`
149+ PromptTokens int64 `json:"prompt_tokens"`
150+ CompletionTokens int64 `json:"completion_tokens"`
151+ Cost float64 `json:"cost"`
152+ ID string `json:"id"`
145153}
146154
147155func (q *Queries) UpdateSession(ctx context.Context, arg UpdateSessionParams) (Session, error) {
148156 row := q.queryRow(ctx, q.updateSessionStmt, updateSession,
149157 arg.Title,
150− arg.Tokens,
158+ arg.PromptTokens,
159+ arg.CompletionTokens,
151160 arg.Cost,
152161 arg.ID,
153162 )
@@ -156,7 +165,8 @@ func (q *Queries) UpdateSession(ctx context.Context, arg UpdateSessionParams) (S
156165 &i.ID,
157166 &i.Title,
158167 &i.MessageCount,
159− &i.Tokens,
168+ &i.PromptTokens,
169+ &i.CompletionTokens,
160170 &i.Cost,
161171 &i.UpdatedAt,
162172 &i.CreatedAt,
internal/db/sql/messages.sqlnew file+30
@@ -0,0 +1,30 @@
1+-- name: GetMessage :one
2+SELECT *
3+FROM messages
4+WHERE id = ? LIMIT 1;
5+
6+-- name: ListMessagesBySession :many
7+SELECT *
8+FROM messages
9+WHERE session_id = ?
10+ORDER BY created_at ASC;
11+
12+-- name: CreateMessage :one
13+INSERT INTO messages (
14+ id,
15+ session_id,
16+ message_data,
17+ created_at,
18+ updated_at
19+) VALUES (
20+ ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now')
21+)
22+RETURNING *;
23+
24+-- name: DeleteMessage :exec
25+DELETE FROM messages
26+WHERE id = ?;
27+
28+-- name: DeleteSessionMessages :exec
29+DELETE FROM messages
30+WHERE session_id = ?;
internal/db/sql/sessions.sql+5−3
@@ -1,10 +1,10 @@
1−-- sqlfluff:dialect:sqlite
21-- name: CreateSession :one
32INSERT INTO sessions (
43 id,
54 title,
65 message_count,
7− tokens,
6+ prompt_tokens,
7+ completion_tokens,
88 cost,
99 updated_at,
1010 created_at
@@ -14,6 +14,7 @@ INSERT INTO sessions (
1414 ?,
1515 ?,
1616 ?,
17+ ?,
1718 strftime('%s', 'now'),
1819 strftime('%s', 'now')
1920) RETURNING *;
@@ -32,7 +33,8 @@ ORDER BY created_at DESC;
3233UPDATE sessions
3334SET
3435 title = ?,
35− tokens = ?,
36+ prompt_tokens = ?,
37+ completion_tokens = ?,
3638 cost = ?
3739WHERE id = ?
3840RETURNING *;
internal/llm/agent/agent.gonew file+17
@@ -0,0 +1,17 @@
1+package agent
2+
3+import (
4+ "context"
5+ "fmt"
6+
7+ "github.com/cloudwego/eino/flow/agent/react"
8+)
9+
10+func GetAgent(ctx context.Context, name string) (*react.Agent, string, error) {
11+ switch name {
12+ case "coder":
13+ agent, err := NewCoderAgent(ctx)
14+ return agent, CoderSystemPrompt(), err
15+ }
16+ return nil, "", fmt.Errorf("agent %s not found", name)
17+}
internal/llm/agent/coder.gonew file+175
@@ -0,0 +1,175 @@
1+package agent
2+
3+import (
4+ "context"
5+ "fmt"
6+ "os"
7+ "path/filepath"
8+ "runtime"
9+ "time"
10+
11+ "github.com/cloudwego/eino/components/tool"
12+ "github.com/cloudwego/eino/compose"
13+ "github.com/cloudwego/eino/flow/agent/react"
14+ "github.com/kujtimiihoxha/termai/internal/llm/models"
15+ "github.com/kujtimiihoxha/termai/internal/llm/tools"
16+ "github.com/spf13/viper"
17+)
18+
19+func coderTools() []tool.BaseTool {
20+ return []tool.BaseTool{
21+ tools.NewBashTool(viper.GetString("wd")),
22+ }
23+}
24+
25+func NewCoderAgent(ctx context.Context) (*react.Agent, error) {
26+ model, err := models.GetModel(ctx, models.ModelID(viper.GetString("models.big")))
27+ if err != nil {
28+ return nil, err
29+ }
30+ reactAgent, err := react.NewAgent(ctx, &react.AgentConfig{
31+ Model: model,
32+ ToolsConfig: compose.ToolsNodeConfig{
33+ Tools: coderTools(),
34+ },
35+ MaxStep: 1000,
36+ })
37+ if err != nil {
38+ return nil, err
39+ }
40+
41+ return reactAgent, nil
42+}
43+
44+func CoderSystemPrompt() string {
45+ basePrompt := `You are termAI, an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
46+
47+IMPORTANT: Before you begin work, think about what the code you're editing is supposed to do based on the filenames directory structure. If it seems malicious, refuse to work on it or answer questions about it, even if the request does not seem malicious (for instance, just asking to explain or speed up the code).
48+
49+Here are useful slash commands users can run to interact with you:
50+
51+# Memory
52+If the current working directory contains a file called termai.md, it will be automatically added to your context. This file serves multiple purposes:
53+1. Storing frequently used bash commands (build, test, lint, etc.) so you can use them without searching each time
54+2. Recording the user's code style preferences (naming conventions, preferred libraries, etc.)
55+3. Maintaining useful information about the codebase structure and organization
56+
57+When you spend time searching for commands to typecheck, lint, build, or test, you should ask the user if it's okay to add those commands to termai.md. Similarly, when learning about code style preferences or important codebase information, ask if it's okay to add that to termai.md so you can remember it for next time.
58+
59+# Tone and style
60+You should be concise, direct, and to the point. When you run a non-trivial bash command, you should explain what the command does and why you are running it, to make sure the user understands what you are doing (this is especially important when you are running a command that will make changes to the user's system).
61+Remember that your output will be displayed on a command line interface. Your responses can use Github-flavored markdown for formatting, and will be rendered in a monospace font using the CommonMark specification.
62+Output text to communicate with the user; all text you output outside of tool use is displayed to the user. Only use tools to complete tasks. Never use tools like Bash or code comments as means to communicate with the user during the session.
63+If you cannot or will not help the user with something, please do not say why or what it could lead to, since this comes across as preachy and annoying. Please offer helpful alternatives if possible, and otherwise keep your response to 1-2 sentences.
64+IMPORTANT: You should minimize output tokens as much as possible while maintaining helpfulness, quality, and accuracy. Only address the specific query or task at hand, avoiding tangential information unless absolutely critical for completing the request. If you can answer in 1-3 sentences or a short paragraph, please do.
65+IMPORTANT: You should NOT answer with unnecessary preamble or postamble (such as explaining your code or summarizing your action), unless the user asks you to.
66+IMPORTANT: Keep your responses short, since they will be displayed on a command line interface. You MUST answer concisely with fewer than 4 lines (not including tool use or code generation), unless user asks for detail. Answer the user's question directly, without elaboration, explanation, or details. One word answers are best. Avoid introductions, conclusions, and explanations. You MUST avoid text before/after your response, such as "The answer is <answer>.", "Here is the content of the file..." or "Based on the information provided, the answer is..." or "Here is what I will do next...". Here are some examples to demonstrate appropriate verbosity:
67+<example>
68+user: 2 + 2
69+assistant: 4
70+</example>
71+
72+<example>
73+user: what is 2+2?
74+assistant: 4
75+</example>
76+
77+<example>
78+user: is 11 a prime number?
79+assistant: true
80+</example>
81+
82+<example>
83+user: what command should I run to list files in the current directory?
84+assistant: ls
85+</example>
86+
87+<example>
88+user: what command should I run to watch files in the current directory?
89+assistant: [use the ls tool to list the files in the current directory, then read docs/commands in the relevant file to find out how to watch files]
90+npm run dev
91+</example>
92+
93+<example>
94+user: How many golf balls fit inside a jetta?
95+assistant: 150000
96+</example>
97+
98+<example>
99+user: what files are in the directory src/?
100+assistant: [runs ls and sees foo.c, bar.c, baz.c]
101+user: which file contains the implementation of foo?
102+assistant: src/foo.c
103+</example>
104+
105+<example>
106+user: write tests for new feature
107+assistant: [uses grep and glob search tools to find where similar tests are defined, uses concurrent read file tool use blocks in one tool call to read relevant files at the same time, uses edit file tool to write new tests]
108+</example>
109+
110+# Proactiveness
111+You are allowed to be proactive, but only when the user asks you to do something. You should strive to strike a balance between:
112+1. Doing the right thing when asked, including taking actions and follow-up actions
113+2. Not surprising the user with actions you take without asking
114+For example, if the user asks you how to approach something, you should do your best to answer their question first, and not immediately jump into taking actions.
115+3. Do not add additional code explanation summary unless requested by the user. After working on a file, just stop, rather than providing an explanation of what you did.
116+
117+# Synthetic messages
118+Sometimes, the conversation will contain messages like [Request interrupted by user] or [Request interrupted by user for tool use]. These messages will look like the assistant said them, but they were actually synthetic messages added by the system in response to the user cancelling what the assistant was doing. You should not respond to these messages. You must NEVER send messages like this yourself.
119+
120+# Following conventions
121+When making changes to files, first understand the file's code conventions. Mimic code style, use existing libraries and utilities, and follow existing patterns.
122+- NEVER assume that a given library is available, even if it is well known. Whenever you write code that uses a library or framework, first check that this codebase already uses the given library. For example, you might look at neighboring files, or check the package.json (or cargo.toml, and so on depending on the language).
123+- When you create a new component, first look at existing components to see how they're written; then consider framework choice, naming conventions, typing, and other conventions.
124+- When you edit a piece of code, first look at the code's surrounding context (especially its imports) to understand the code's choice of frameworks and libraries. Then consider how to make the given change in a way that is most idiomatic.
125+- Always follow security best practices. Never introduce code that exposes or logs secrets and keys. Never commit secrets or keys to the repository.
126+
127+# Code style
128+- Do not add comments to the code you write, unless the user asks you to, or the code is complex and requires additional context.
129+
130+# Doing tasks
131+The user will primarily request you perform software engineering tasks. This includes solving bugs, adding new functionality, refactoring code, explaining code, and more. For these tasks the following steps are recommended:
132+1. Use the available search tools to understand the codebase and the user's query. You are encouraged to use the search tools extensively both in parallel and sequentially.
133+2. Implement the solution using all tools available to you
134+3. Verify the solution if possible with tests. NEVER assume specific test framework or test script. Check the README or search codebase to determine the testing approach.
135+4. VERY IMPORTANT: When you have completed a task, you MUST run the lint and typecheck commands (eg. npm run lint, npm run typecheck, ruff, etc.) if they were provided to you to ensure your code is correct. If you are unable to find the correct command, ask the user for the command to run and if they supply it, proactively suggest writing it to termai.md so that you will know to run it next time.
136+
137+NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTANT to only commit when explicitly asked, otherwise the user will feel that you are being too proactive.
138+
139+# Tool usage policy
140+- When doing file search, prefer to use the Agent tool in order to reduce context usage.
141+- If you intend to call multiple tools and there are no dependencies between the calls, make all of the independent calls in the same function_calls block.
142+
143+You MUST answer concisely with fewer than 4 lines of text (not including tool use or code generation), unless user asks for detail.`
144+
145+ envInfo := getEnvironmentInfo()
146+
147+ return fmt.Sprintf("%s\n\n%s", basePrompt, envInfo)
148+}
149+
150+func getEnvironmentInfo() string {
151+ cwd := viper.GetString("wd")
152+ isGit := isGitRepo(cwd)
153+ platform := runtime.GOOS
154+ date := time.Now().Format("1/2/2006")
155+
156+ return fmt.Sprintf(`Here is useful information about the environment you are running in:
157+<env>
158+Working directory: %s
159+Is directory a git repo: %s
160+Platform: %s
161+Today's date: %s
162+</env>`, cwd, boolToYesNo(isGit), platform, date)
163+}
164+
165+func isGitRepo(dir string) bool {
166+ _, err := os.Stat(filepath.Join(dir, ".git"))
167+ return err == nil
168+}
169+
170+func boolToYesNo(b bool) string {
171+ if b {
172+ return "Yes"
173+ }
174+ return "No"
175+}
internal/llm/llm.gonew file+206
@@ -0,0 +1,206 @@
1+package llm
2+
3+import (
4+ "context"
5+ "log"
6+ "sync"
7+ "time"
8+
9+ "github.com/cloudwego/eino/callbacks"
10+ "github.com/cloudwego/eino/compose"
11+ "github.com/cloudwego/eino/schema"
12+ "github.com/google/uuid"
13+ "github.com/kujtimiihoxha/termai/internal/llm/agent"
14+ "github.com/kujtimiihoxha/termai/internal/logging"
15+ "github.com/kujtimiihoxha/termai/internal/message"
16+ "github.com/kujtimiihoxha/termai/internal/pubsub"
17+ "github.com/kujtimiihoxha/termai/internal/session"
18+
19+ eModel "github.com/cloudwego/eino/components/model"
20+ enioAgent "github.com/cloudwego/eino/flow/agent"
21+ "github.com/spf13/viper"
22+)
23+
24+const (
25+ AgentRequestoEvent pubsub.EventType = "agent_request"
26+ AgentErrorEvent pubsub.EventType = "agent_error"
27+ AgentResponseEvent pubsub.EventType = "agent_response"
28+)
29+
30+type AgentMessageType int
31+
32+const (
33+ AgentMessageTypeNewUserMessage AgentMessageType = iota
34+ AgentMessageTypeAgentResponse
35+ AgentMessageTypeError
36+)
37+
38+type agentID string
39+
40+const (
41+ RootAgent agentID = "root"
42+ TaskAgent agentID = "task"
43+)
44+
45+type AgentEvent struct {
46+ ID string `json:"id"`
47+ Type AgentMessageType `json:"type"`
48+ AgentID agentID `json:"agent_id"`
49+ MessageID string `json:"message_id"`
50+ SessionID string `json:"session_id"`
51+ Content string `json:"content"`
52+}
53+
54+type Service interface {
55+ pubsub.Suscriber[AgentEvent]
56+
57+ SendRequest(sessionID string, content string)
58+}
59+type service struct {
60+ *pubsub.Broker[AgentEvent]
61+ Requests sync.Map
62+ ctx context.Context
63+ activeRequests sync.Map
64+ messages message.Service
65+ sessions session.Service
66+ logger logging.Interface
67+}
68+
69+func (s *service) handleRequest(id string, sessionID string, content string) {
70+ cancel, ok := s.activeRequests.Load(id)
71+ if !ok {
72+ return
73+ }
74+ defer cancel.(context.CancelFunc)()
75+ defer s.activeRequests.Delete(id)
76+
77+ history, err := s.messages.List(sessionID)
78+ if err != nil {
79+ s.Publish(AgentErrorEvent, AgentEvent{
80+ ID: id,
81+ Type: AgentMessageTypeError,
82+ AgentID: RootAgent,
83+ MessageID: "",
84+ SessionID: sessionID,
85+ Content: err.Error(),
86+ })
87+ return
88+ }
89+
90+ log.Printf("Request: %s", content)
91+ agent, systemMessage, err := agent.GetAgent(s.ctx, viper.GetString("agents.default"))
92+ if err != nil {
93+ s.Publish(AgentErrorEvent, AgentEvent{
94+ ID: id,
95+ Type: AgentMessageTypeError,
96+ AgentID: RootAgent,
97+ MessageID: "",
98+ SessionID: sessionID,
99+ Content: err.Error(),
100+ })
101+ return
102+ }
103+
104+ messages := []*schema.Message{
105+ {
106+ Role: schema.System,
107+ Content: systemMessage,
108+ },
109+ }
110+ for _, m := range history {
111+ messages = append(messages, &m.MessageData)
112+ }
113+ builder := callbacks.NewHandlerBuilder()
114+ builder.OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context {
115+ i, ok := input.(*eModel.CallbackInput)
116+ if info.Component == "ChatModel" && ok {
117+ if len(messages) < len(i.Messages) {
118+ // find new messages
119+ newMessages := i.Messages[len(messages):]
120+ for _, m := range newMessages {
121+ _, err = s.messages.Create(sessionID, *m)
122+ if err != nil {
123+ s.Publish(AgentErrorEvent, AgentEvent{
124+ ID: id,
125+ Type: AgentMessageTypeError,
126+ AgentID: RootAgent,
127+ MessageID: "",
128+ SessionID: sessionID,
129+ Content: err.Error(),
130+ })
131+ }
132+ messages = append(messages, m)
133+ }
134+ }
135+ }
136+
137+ return ctx
138+ })
139+ builder.OnEndFn(func(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context {
140+ return ctx
141+ })
142+
143+ out, err := agent.Generate(s.ctx, messages, enioAgent.WithComposeOptions(compose.WithCallbacks(builder.Build())))
144+ if err != nil {
145+ s.Publish(AgentErrorEvent, AgentEvent{
146+ ID: id,
147+ Type: AgentMessageTypeError,
148+ AgentID: RootAgent,
149+ MessageID: "",
150+ SessionID: sessionID,
151+ Content: err.Error(),
152+ })
153+ return
154+ }
155+ usage := out.ResponseMeta.Usage
156+ if usage != nil {
157+ log.Printf("Prompt Tokens: %d, Completion Tokens: %d, Total Tokens: %d", usage.PromptTokens, usage.CompletionTokens, usage.TotalTokens)
158+ session, err := s.sessions.Get(sessionID)
159+ if err != nil {
160+ s.Publish(AgentErrorEvent, AgentEvent{
161+ ID: id,
162+ Type: AgentMessageTypeError,
163+ AgentID: RootAgent,
164+ MessageID: "",
165+ SessionID: sessionID,
166+ Content: err.Error(),
167+ })
168+ return
169+ }
170+ session.PromptTokens += int64(usage.PromptTokens)
171+ session.CompletionTokens += int64(usage.CompletionTokens)
172+ // TODO: calculate cost
173+ _, err = s.sessions.Save(session)
174+ if err != nil {
175+ s.Publish(AgentErrorEvent, AgentEvent{
176+ ID: id,
177+ Type: AgentMessageTypeError,
178+ AgentID: RootAgent,
179+ MessageID: "",
180+ SessionID: sessionID,
181+ Content: err.Error(),
182+ })
183+ return
184+ }
185+ }
186+ s.messages.Create(sessionID, *out)
187+}
188+
189+func (s *service) SendRequest(sessionID string, content string) {
190+ id := uuid.New().String()
191+
192+ _, cancel := context.WithTimeout(s.ctx, 5*time.Minute)
193+ s.activeRequests.Store(id, cancel)
194+ log.Printf("Request: %s", content)
195+ go s.handleRequest(id, sessionID, content)
196+}
197+
198+func NewService(ctx context.Context, logger logging.Interface, sessions session.Service, messages message.Service) Service {
199+ return &service{
200+ Broker: pubsub.NewBroker[AgentEvent](),
201+ ctx: ctx,
202+ sessions: sessions,
203+ messages: messages,
204+ logger: logger,
205+ }
206+}
internal/llm/models/models.gonew file+196
@@ -0,0 +1,196 @@
1+package models
2+
3+import (
4+ "context"
5+ "errors"
6+
7+ "github.com/cloudwego/eino-ext/components/model/claude"
8+ "github.com/cloudwego/eino-ext/components/model/openai"
9+ "github.com/cloudwego/eino/components/model"
10+ "github.com/spf13/viper"
11+)
12+
13+type (
14+ ModelID string
15+ ModelProvider string
16+)
17+
18+type Model struct {
19+ ID ModelID `json:"id"`
20+ Name string `json:"name"`
21+ Provider ModelProvider `json:"provider"`
22+ APIModel string `json:"api_model"` // Actual value used when calling the API
23+}
24+
25+const (
26+ DefaultBigModel = GPT4oMini
27+ DefaultLittleModel = GPT4oMini
28+)
29+
30+// Model IDs
31+const (
32+ // OpenAI
33+ GPT4o ModelID = "gpt-4o"
34+ GPT4oMini ModelID = "gpt-4o-mini"
35+ GPT45 ModelID = "gpt-4.5"
36+ O1 ModelID = "o1"
37+ O1Mini ModelID = "o1-mini"
38+ // Anthropic
39+ Claude35Sonnet ModelID = "claude-3.5-sonnet"
40+ Claude3Haiku ModelID = "claude-3-haiku"
41+ Claude37Sonnet ModelID = "claude-3.7-sonnet"
42+ // Google
43+ Gemini20Pro ModelID = "gemini-2.0-pro"
44+ Gemini15Flash ModelID = "gemini-1.5-flash"
45+ Gemini20Flash ModelID = "gemini-2.0-flash"
46+ // xAI
47+ Grok3 ModelID = "grok-3"
48+ Grok2Mini ModelID = "grok-2-mini"
49+ // DeepSeek
50+ DeepSeekR1 ModelID = "deepseek-r1"
51+ DeepSeekCoder ModelID = "deepseek-coder"
52+ // Meta
53+ Llama3 ModelID = "llama-3"
54+ Llama270B ModelID = "llama-2-70b"
55+)
56+
57+const (
58+ ProviderOpenAI ModelProvider = "openai"
59+ ProviderAnthropic ModelProvider = "anthropic"
60+ ProviderGoogle ModelProvider = "google"
61+ ProviderXAI ModelProvider = "xai"
62+ ProviderDeepSeek ModelProvider = "deepseek"
63+ ProviderMeta ModelProvider = "meta"
64+)
65+
66+var SupportedModels = map[ModelID]Model{
67+ // OpenAI
68+ GPT4o: {
69+ ID: GPT4o,
70+ Name: "GPT-4o",
71+ Provider: ProviderOpenAI,
72+ APIModel: "gpt-4o",
73+ },
74+ GPT4oMini: {
75+ ID: GPT4oMini,
76+ Name: "GPT-4o Mini",
77+ Provider: ProviderOpenAI,
78+ APIModel: "gpt-4o-mini",
79+ },
80+ GPT45: {
81+ ID: GPT45,
82+ Name: "GPT-4.5",
83+ Provider: ProviderOpenAI,
84+ APIModel: "gpt-4.5",
85+ },
86+ O1: {
87+ ID: O1,
88+ Name: "o1",
89+ Provider: ProviderOpenAI,
90+ APIModel: "o1",
91+ },
92+ O1Mini: {
93+ ID: O1Mini,
94+ Name: "o1 Mini",
95+ Provider: ProviderOpenAI,
96+ APIModel: "o1-mini",
97+ },
98+ // Anthropic
99+ Claude35Sonnet: {
100+ ID: Claude35Sonnet,
101+ Name: "Claude 3.5 Sonnet",
102+ Provider: ProviderAnthropic,
103+ APIModel: "claude-3.5-sonnet",
104+ },
105+ Claude3Haiku: {
106+ ID: Claude3Haiku,
107+ Name: "Claude 3 Haiku",
108+ Provider: ProviderAnthropic,
109+ APIModel: "claude-3-haiku",
110+ },
111+ Claude37Sonnet: {
112+ ID: Claude37Sonnet,
113+ Name: "Claude 3.7 Sonnet",
114+ Provider: ProviderAnthropic,
115+ APIModel: "claude-3-7-sonnet-20250219",
116+ },
117+ // Google
118+ Gemini20Pro: {
119+ ID: Gemini20Pro,
120+ Name: "Gemini 2.0 Pro",
121+ Provider: ProviderGoogle,
122+ APIModel: "gemini-2.0-pro",
123+ },
124+ Gemini15Flash: {
125+ ID: Gemini15Flash,
126+ Name: "Gemini 1.5 Flash",
127+ Provider: ProviderGoogle,
128+ APIModel: "gemini-1.5-flash",
129+ },
130+ Gemini20Flash: {
131+ ID: Gemini20Flash,
132+ Name: "Gemini 2.0 Flash",
133+ Provider: ProviderGoogle,
134+ APIModel: "gemini-2.0-flash",
135+ },
136+ // xAI
137+ Grok3: {
138+ ID: Grok3,
139+ Name: "Grok 3",
140+ Provider: ProviderXAI,
141+ APIModel: "grok-3",
142+ },
143+ Grok2Mini: {
144+ ID: Grok2Mini,
145+ Name: "Grok 2 Mini",
146+ Provider: ProviderXAI,
147+ APIModel: "grok-2-mini",
148+ },
149+ // DeepSeek
150+ DeepSeekR1: {
151+ ID: DeepSeekR1,
152+ Name: "DeepSeek R1",
153+ Provider: ProviderDeepSeek,
154+ APIModel: "deepseek-r1",
155+ },
156+ DeepSeekCoder: {
157+ ID: DeepSeekCoder,
158+ Name: "DeepSeek Coder",
159+ Provider: ProviderDeepSeek,
160+ APIModel: "deepseek-coder",
161+ },
162+ // Meta
163+ Llama3: {
164+ ID: Llama3,
165+ Name: "LLaMA 3",
166+ Provider: ProviderMeta,
167+ APIModel: "llama-3",
168+ },
169+ Llama270B: {
170+ ID: Llama270B,
171+ Name: "LLaMA 2 70B",
172+ Provider: ProviderMeta,
173+ APIModel: "llama-2-70b",
174+ },
175+}
176+
177+func GetModel(ctx context.Context, model ModelID) (model.ChatModel, error) {
178+ provider := SupportedModels[model].Provider
179+ maxTokens := viper.GetInt("providers.common.max_tokens")
180+ switch provider {
181+ case ProviderOpenAI:
182+ return openai.NewChatModel(ctx, &openai.ChatModelConfig{
183+ APIKey: viper.GetString("providers.openai.key"),
184+ Model: string(SupportedModels[model].APIModel),
185+ MaxTokens: &maxTokens,
186+ })
187+ case ProviderAnthropic:
188+ return claude.NewChatModel(ctx, &claude.Config{
189+ APIKey: viper.GetString("providers.anthropic.key"),
190+ Model: string(SupportedModels[model].APIModel),
191+ MaxTokens: maxTokens,
192+ })
193+
194+ }
195+ return nil, errors.New("unsupported provider")
196+}
internal/llm/tools/bash.gonew file+454
@@ -0,0 +1,454 @@
1+package tools
2+
3+import (
4+ "context"
5+ "encoding/json"
6+ "errors"
7+ "fmt"
8+ "log"
9+ "strings"
10+
11+ "github.com/cloudwego/eino/components/tool"
12+ "github.com/cloudwego/eino/schema"
13+ "github.com/kujtimiihoxha/termai/internal/llm/tools/shell"
14+)
15+
16+type bashTool struct {
17+ workingDir string
18+}
19+
20+const (
21+ BashToolName = "bash"
22+
23+ DefaultTimeout = 30 * 60 * 1000 // 30 minutes in milliseconds
24+ MaxTimeout = 10 * 60 * 1000 // 10 minutes in milliseconds
25+ MaxOutputLength = 30000
26+)
27+
28+type BashParams struct {
29+ Command string `json:"command"`
30+ Timeout int `json:"timeout"`
31+}
32+
33+var BannedCommands = []string{
34+ "alias", "curl", "curlie", "wget", "axel", "aria2c",
35+ "nc", "telnet", "lynx", "w3m", "links", "httpie", "xh",
36+ "http-prompt", "chrome", "firefox", "safari",
37+}
38+
39+func (b *bashTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
40+ return &schema.ToolInfo{
41+ Name: BashToolName,
42+ Desc: bashDescription(),
43+ ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
44+ "command": {
45+ Type: "string",
46+ Desc: "The command to execute",
47+ Required: true,
48+ },
49+ "timeout": {
50+ Type: "number",
51+ Desc: "Optional timeout in milliseconds (max 600000)",
52+ },
53+ }),
54+ }, nil
55+}
56+
57+// Handle implements Tool.
58+func (b *bashTool) InvokableRun(ctx context.Context, args string, opts ...tool.Option) (string, error) {
59+ log.Printf("BashTool InvokableRun: %s", args)
60+ var params BashParams
61+ if err := json.Unmarshal([]byte(args), &params); err != nil {
62+ return "", err
63+ }
64+
65+ if params.Timeout > MaxTimeout {
66+ params.Timeout = MaxTimeout
67+ } else if params.Timeout <= 0 {
68+ params.Timeout = DefaultTimeout
69+ }
70+
71+ if params.Command == "" {
72+ return "", errors.New("missing command")
73+ }
74+
75+ baseCmd := strings.Fields(params.Command)[0]
76+ for _, banned := range BannedCommands {
77+ if strings.EqualFold(baseCmd, banned) {
78+ return "", fmt.Errorf("command '%s' is not allowed", baseCmd)
79+ }
80+ }
81+
82+ // p := b.permission.Request(permission.CreatePermissionRequest{
83+ // Path: b.workingDir,
84+ // ToolName: BashToolName,
85+ // Action: "execute",
86+ // Description: fmt.Sprintf("Execute command: %s", params.Command),
87+ // Params: map[string]any{
88+ // "command": params.Command,
89+ // "timeout": params.Timeout,
90+ // },
91+ // })
92+ // if !p {
93+ // return "", errors.New("permission denied")
94+ // }
95+
96+ shell := shell.GetPersistentShell(b.workingDir)
97+ stdout, stderr, exitCode, interrupted, err := shell.Exec(ctx, params.Command, params.Timeout)
98+ if err != nil {
99+ return "", err
100+ }
101+
102+ stdout = truncateOutput(stdout)
103+ stderr = truncateOutput(stderr)
104+
105+ errorMessage := stderr
106+ if interrupted {
107+ if errorMessage != "" {
108+ errorMessage += "\n"
109+ }
110+ errorMessage += "Command was aborted before completion"
111+ } else if exitCode != 0 {
112+ if errorMessage != "" {
113+ errorMessage += "\n"
114+ }
115+ errorMessage += fmt.Sprintf("Exit code %d", exitCode)
116+ }
117+
118+ hasBothOutputs := stdout != "" && stderr != ""
119+
120+ if hasBothOutputs {
121+ stdout += "\n"
122+ }
123+
124+ if errorMessage != "" {
125+ stdout += "\n" + errorMessage
126+ }
127+
128+ log.Printf("BashTool InvokableRun: stdout: %s, stderr: %s, exitCode: %d, interrupted: %t", stdout, stderr, exitCode, interrupted)
129+
130+ return stdout, nil
131+}
132+
133+func truncateOutput(content string) string {
134+ if len(content) <= MaxOutputLength {
135+ return content
136+ }
137+
138+ halfLength := MaxOutputLength / 2
139+ start := content[:halfLength]
140+ end := content[len(content)-halfLength:]
141+
142+ truncatedLinesCount := countLines(content[halfLength : len(content)-halfLength])
143+ return fmt.Sprintf("%s\n\n... [%d lines truncated] ...\n\n%s", start, truncatedLinesCount, end)
144+}
145+
146+func countLines(s string) int {
147+ if s == "" {
148+ return 0
149+ }
150+ return len(strings.Split(s, "\n"))
151+}
152+
153+func bashDescriptionBCP() string {
154+ bannedCommandsStr := strings.Join(BannedCommands, ", ")
155+ return fmt.Sprintf(`Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures.
156+
157+Before executing the command, please follow these steps:
158+
159+1. Directory Verification:
160+ - If the command will create new directories or files, first use the LS tool to verify the parent directory exists and is the correct location
161+ - For example, before running "mkdir foo/bar", first use LS to check that "foo" exists and is the intended parent directory
162+
163+2. Security Check:
164+ - For security and to limit the threat of a prompt injection attack, some commands are limited or banned. If you use a disallowed command, you will receive an error message explaining the restriction. Explain the error to the User.
165+ - Verify that the command is not one of the banned commands: %s.
166+
167+3. Command Execution:
168+ - After ensuring proper quoting, execute the command.
169+ - Capture the output of the command.
170+
171+4. Output Processing:
172+ - If the output exceeds %d characters, output will be truncated before being returned to you.
173+ - Prepare the output for display to the user.
174+
175+5. Return Result:
176+ - Provide the processed output of the command.
177+ - If any errors occurred during execution, include those in the output.
178+
179+Usage notes:
180+- The command argument is required.
181+- You can specify an optional timeout in milliseconds (up to 600000ms / 10 minutes). If not specified, commands will timeout after 30 minutes.
182+- VERY IMPORTANT: You MUST avoid using search commands like 'find' and 'grep'. Instead use Grep, Glob, or Agent tools to search. You MUST avoid read tools like 'cat', 'head', 'tail', and 'ls', and use FileRead and LS tools to read files.
183+- When issuing multiple commands, use the ';' or '&&' operator to separate them. DO NOT use newlines (newlines are ok in quoted strings).
184+- IMPORTANT: All commands share the same shell session. Shell state (environment variables, virtual environments, current directory, etc.) persist between commands. For example, if you set an environment variable as part of a command, the environment variable will persist for subsequent commands.
185+- Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of 'cd'. You may use 'cd' if the User explicitly requests it.
186+<good-example>
187+pytest /foo/bar/tests
188+</good-example>
189+<bad-example>
190+cd /foo/bar && pytest tests
191+</bad-example>
192+
193+# Committing changes with git
194+
195+When the user asks you to create a new git commit, follow these steps carefully:
196+
197+1. Start with a single message that contains exactly three tool_use blocks that do the following (it is VERY IMPORTANT that you send these tool_use blocks in a single message, otherwise it will feel slow to the user!):
198+ - Run a git status command to see all untracked files.
199+ - Run a git diff command to see both staged and unstaged changes that will be committed.
200+ - Run a git log command to see recent commit messages, so that you can follow this repository's commit message style.
201+
202+2. Use the git context at the start of this conversation to determine which files are relevant to your commit. Add relevant untracked files to the staging area. Do not commit files that were already modified at the start of this conversation, if they are not relevant to your commit.
203+
204+3. Analyze all staged changes (both previously staged and newly added) and draft a commit message. Wrap your analysis process in <commit_analysis> tags:
205+
206+<commit_analysis>
207+- List the files that have been changed or added
208+- Summarize the nature of the changes (eg. new feature, enhancement to an existing feature, bug fix, refactoring, test, docs, etc.)
209+- Brainstorm the purpose or motivation behind these changes
210+- Do not use tools to explore code, beyond what is available in the git context
211+- Assess the impact of these changes on the overall project
212+- Check for any sensitive information that shouldn't be committed
213+- Draft a concise (1-2 sentences) commit message that focuses on the "why" rather than the "what"
214+- Ensure your language is clear, concise, and to the point
215+- Ensure the message accurately reflects the changes and their purpose (i.e. "add" means a wholly new feature, "update" means an enhancement to an existing feature, "fix" means a bug fix, etc.)
216+- Ensure the message is not generic (avoid words like "Update" or "Fix" without context)
217+- Review the draft message to ensure it accurately reflects the changes and their purpose
218+</commit_analysis>
219+
220+4. Create the commit with a message ending with:
221+🤖 Generated with termai
222+Co-Authored-By: termai <noreply@termai.io>
223+
224+- In order to ensure good formatting, ALWAYS pass the commit message via a HEREDOC, a la this example:
225+<example>
226+git commit -m "$(cat <<'EOF'
227+ Commit message here.
228+
229+ 🤖 Generated with termai
230+ Co-Authored-By: termai <noreply@termai.io>
231+ EOF
232+ )"
233+</example>
234+
235+5. If the commit fails due to pre-commit hook changes, retry the commit ONCE to include these automated changes. If it fails again, it usually means a pre-commit hook is preventing the commit. If the commit succeeds but you notice that files were modified by the pre-commit hook, you MUST amend your commit to include them.
236+
237+6. Finally, run git status to make sure the commit succeeded.
238+
239+Important notes:
240+- When possible, combine the "git add" and "git commit" commands into a single "git commit -am" command, to speed things up
241+- However, be careful not to stage files (e.g. with 'git add .') for commits that aren't part of the change, they may have untracked files they want to keep around, but not commit.
242+- NEVER update the git config
243+- DO NOT push to the remote repository
244+- IMPORTANT: Never use git commands with the -i flag (like git rebase -i or git add -i) since they require interactive input which is not supported.
245+- If there are no changes to commit (i.e., no untracked files and no modifications), do not create an empty commit
246+- Ensure your commit message is meaningful and concise. It should explain the purpose of the changes, not just describe them.
247+- Return an empty response - the user will see the git output directly
248+
249+# Creating pull requests
250+Use the gh command via the Bash tool for ALL GitHub-related tasks including working with issues, pull requests, checks, and releases. If given a Github URL use the gh command to get the information needed.
251+
252+IMPORTANT: When the user asks you to create a pull request, follow these steps carefully:
253+
254+1. Understand the current state of the branch. Remember to send a single message that contains multiple tool_use blocks (it is VERY IMPORTANT that you do this in a single message, otherwise it will feel slow to the user!):
255+ - Run a git status command to see all untracked files.
256+ - Run a git diff command to see both staged and unstaged changes that will be committed.
257+ - Check if the current branch tracks a remote branch and is up to date with the remote, so you know if you need to push to the remote
258+ - Run a git log command and 'git diff main...HEAD' to understand the full commit history for the current branch (from the time it diverged from the 'main' branch.)
259+
260+2. Create new branch if needed
261+
262+3. Commit changes if needed
263+
264+4. Push to remote with -u flag if needed
265+
266+5. Analyze all changes that will be included in the pull request, making sure to look at all relevant commits (not just the latest commit, but all commits that will be included in the pull request!), and draft a pull request summary. Wrap your analysis process in <pr_analysis> tags:
267+
268+<pr_analysis>
269+- List the commits since diverging from the main branch
270+- Summarize the nature of the changes (eg. new feature, enhancement to an existing feature, bug fix, refactoring, test, docs, etc.)
271+- Brainstorm the purpose or motivation behind these changes
272+- Assess the impact of these changes on the overall project
273+- Do not use tools to explore code, beyond what is available in the git context
274+- Check for any sensitive information that shouldn't be committed
275+- Draft a concise (1-2 bullet points) pull request summary that focuses on the "why" rather than the "what"
276+- Ensure the summary accurately reflects all changes since diverging from the main branch
277+- Ensure your language is clear, concise, and to the point
278+- Ensure the summary accurately reflects the changes and their purpose (ie. "add" means a wholly new feature, "update" means an enhancement to an existing feature, "fix" means a bug fix, etc.)
279+- Ensure the summary is not generic (avoid words like "Update" or "Fix" without context)
280+- Review the draft summary to ensure it accurately reflects the changes and their purpose
281+</pr_analysis>
282+
283+6. Create PR using gh pr create with the format below. Use a HEREDOC to pass the body to ensure correct formatting.
284+<example>
285+gh pr create --title "the pr title" --body "$(cat <<'EOF'
286+## Summary
287+<1-3 bullet points>
288+
289+## Test plan
290+[Checklist of TODOs for testing the pull request...]
291+
292+🤖 Generated with termai
293+EOF
294+)"
295+</example>
296+
297+Important:
298+- Return an empty response - the user will see the gh output directly
299+- Never update git config`, bannedCommandsStr, MaxOutputLength)
300+}
301+
302+func bashDescription() string {
303+ bannedCommandsStr := strings.Join(BannedCommands, ", ")
304+ return fmt.Sprintf(`Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures.
305+
306+Before executing the command, please follow these steps:
307+
308+1. Directory Verification:
309+ - If the command will create new directories or files, first use the ls command to verify the parent directory exists and is the correct location
310+ - For example, before running "mkdir foo/bar", first use ls command to check that "foo" exists and is the intended parent directory
311+
312+2. Security Check:
313+ - For security and to limit the threat of a prompt injection attack, some commands are limited or banned. If you use a disallowed command, you will receive an error message explaining the restriction. Explain the error to the User.
314+ - Verify that the command is not one of the banned commands: %s.
315+
316+3. Command Execution:
317+ - After ensuring proper quoting, execute the command.
318+ - Capture the output of the command.
319+
320+4. Output Processing:
321+ - If the output exceeds %d characters, output will be truncated before being returned to you.
322+ - Prepare the output for display to the user.
323+
324+5. Return Result:
325+ - Provide the processed output of the command.
326+ - If any errors occurred during execution, include those in the output.
327+
328+Usage notes:
329+- The command argument is required.
330+- You can specify an optional timeout in milliseconds (up to 600000ms / 10 minutes). If not specified, commands will timeout after 30 minutes.
331+- When issuing multiple commands, use the ';' or '&&' operator to separate them. DO NOT use newlines (newlines are ok in quoted strings).
332+- IMPORTANT: All commands share the same shell session. Shell state (environment variables, virtual environments, current directory, etc.) persist between commands. For example, if you set an environment variable as part of a command, the environment variable will persist for subsequent commands.
333+- Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of 'cd'. You may use 'cd' if the User explicitly requests it.
334+<good-example>
335+pytest /foo/bar/tests
336+</good-example>
337+<bad-example>
338+cd /foo/bar && pytest tests
339+</bad-example>
340+
341+# Committing changes with git
342+
343+When the user asks you to create a new git commit, follow these steps carefully:
344+
345+1. Start with a single message that contains exactly three tool_use blocks that do the following (it is VERY IMPORTANT that you send these tool_use blocks in a single message, otherwise it will feel slow to the user!):
346+ - Run a git status command to see all untracked files.
347+ - Run a git diff command to see both staged and unstaged changes that will be committed.
348+ - Run a git log command to see recent commit messages, so that you can follow this repository's commit message style.
349+
350+2. Use the git context at the start of this conversation to determine which files are relevant to your commit. Add relevant untracked files to the staging area. Do not commit files that were already modified at the start of this conversation, if they are not relevant to your commit.
351+
352+3. Analyze all staged changes (both previously staged and newly added) and draft a commit message. Wrap your analysis process in <commit_analysis> tags:
353+
354+<commit_analysis>
355+- List the files that have been changed or added
356+- Summarize the nature of the changes (eg. new feature, enhancement to an existing feature, bug fix, refactoring, test, docs, etc.)
357+- Brainstorm the purpose or motivation behind these changes
358+- Do not use tools to explore code, beyond what is available in the git context
359+- Assess the impact of these changes on the overall project
360+- Check for any sensitive information that shouldn't be committed
361+- Draft a concise (1-2 sentences) commit message that focuses on the "why" rather than the "what"
362+- Ensure your language is clear, concise, and to the point
363+- Ensure the message accurately reflects the changes and their purpose (i.e. "add" means a wholly new feature, "update" means an enhancement to an existing feature, "fix" means a bug fix, etc.)
364+- Ensure the message is not generic (avoid words like "Update" or "Fix" without context)
365+- Review the draft message to ensure it accurately reflects the changes and their purpose
366+</commit_analysis>
367+
368+4. Create the commit with a message ending with:
369+🤖 Generated with termai
370+Co-Authored-By: termai <noreply@termai.io>
371+
372+- In order to ensure good formatting, ALWAYS pass the commit message via a HEREDOC, a la this example:
373+<example>
374+git commit -m "$(cat <<'EOF'
375+ Commit message here.
376+
377+ 🤖 Generated with termai
378+ Co-Authored-By: termai <noreply@termai.io>
379+ EOF
380+ )"
381+</example>
382+
383+5. If the commit fails due to pre-commit hook changes, retry the commit ONCE to include these automated changes. If it fails again, it usually means a pre-commit hook is preventing the commit. If the commit succeeds but you notice that files were modified by the pre-commit hook, you MUST amend your commit to include them.
384+
385+6. Finally, run git status to make sure the commit succeeded.
386+
387+Important notes:
388+- When possible, combine the "git add" and "git commit" commands into a single "git commit -am" command, to speed things up
389+- However, be careful not to stage files (e.g. with 'git add .') for commits that aren't part of the change, they may have untracked files they want to keep around, but not commit.
390+- NEVER update the git config
391+- DO NOT push to the remote repository
392+- IMPORTANT: Never use git commands with the -i flag (like git rebase -i or git add -i) since they require interactive input which is not supported.
393+- If there are no changes to commit (i.e., no untracked files and no modifications), do not create an empty commit
394+- Ensure your commit message is meaningful and concise. It should explain the purpose of the changes, not just describe them.
395+- Return an empty response - the user will see the git output directly
396+
397+# Creating pull requests
398+Use the gh command via the Bash tool for ALL GitHub-related tasks including working with issues, pull requests, checks, and releases. If given a Github URL use the gh command to get the information needed.
399+
400+IMPORTANT: When the user asks you to create a pull request, follow these steps carefully:
401+
402+1. Understand the current state of the branch. Remember to send a single message that contains multiple tool_use blocks (it is VERY IMPORTANT that you do this in a single message, otherwise it will feel slow to the user!):
403+ - Run a git status command to see all untracked files.
404+ - Run a git diff command to see both staged and unstaged changes that will be committed.
405+ - Check if the current branch tracks a remote branch and is up to date with the remote, so you know if you need to push to the remote
406+ - Run a git log command and 'git diff main...HEAD' to understand the full commit history for the current branch (from the time it diverged from the 'main' branch.)
407+
408+2. Create new branch if needed
409+
410+3. Commit changes if needed
411+
412+4. Push to remote with -u flag if needed
413+
414+5. Analyze all changes that will be included in the pull request, making sure to look at all relevant commits (not just the latest commit, but all commits that will be included in the pull request!), and draft a pull request summary. Wrap your analysis process in <pr_analysis> tags:
415+
416+<pr_analysis>
417+- List the commits since diverging from the main branch
418+- Summarize the nature of the changes (eg. new feature, enhancement to an existing feature, bug fix, refactoring, test, docs, etc.)
419+- Brainstorm the purpose or motivation behind these changes
420+- Assess the impact of these changes on the overall project
421+- Do not use tools to explore code, beyond what is available in the git context
422+- Check for any sensitive information that shouldn't be committed
423+- Draft a concise (1-2 bullet points) pull request summary that focuses on the "why" rather than the "what"
424+- Ensure the summary accurately reflects all changes since diverging from the main branch
425+- Ensure your language is clear, concise, and to the point
426+- Ensure the summary accurately reflects the changes and their purpose (ie. "add" means a wholly new feature, "update" means an enhancement to an existing feature, "fix" means a bug fix, etc.)
427+- Ensure the summary is not generic (avoid words like "Update" or "Fix" without context)
428+- Review the draft summary to ensure it accurately reflects the changes and their purpose
429+</pr_analysis>
430+
431+6. Create PR using gh pr create with the format below. Use a HEREDOC to pass the body to ensure correct formatting.
432+<example>
433+gh pr create --title "the pr title" --body "$(cat <<'EOF'
434+## Summary
435+<1-3 bullet points>
436+
437+## Test plan
438+[Checklist of TODOs for testing the pull request...]
439+
440+🤖 Generated with termai
441+EOF
442+)"
443+</example>
444+
445+Important:
446+- Return an empty response - the user will see the gh output directly
447+- Never update git config`, bannedCommandsStr, MaxOutputLength)
448+}
449+
450+func NewBashTool(workingDir string) tool.InvokableTool {
451+ return &bashTool{
452+ workingDir: workingDir,
453+ }
454+}
internal/llm/tools/shell/shell.gonew file+294
@@ -0,0 +1,294 @@
1+package shell
2+
3+import (
4+ "context"
5+ "errors"
6+ "fmt"
7+ "os"
8+ "os/exec"
9+ "path/filepath"
10+ "strings"
11+ "sync"
12+ "syscall"
13+ "time"
14+)
15+
16+type PersistentShell struct {
17+ cmd *exec.Cmd
18+ stdin *os.File
19+ isAlive bool
20+ cwd string
21+ mu sync.Mutex
22+ commandQueue chan *commandExecution
23+}
24+
25+type commandExecution struct {
26+ command string
27+ timeout time.Duration
28+ resultChan chan commandResult
29+ ctx context.Context
30+}
31+
32+type commandResult struct {
33+ stdout string
34+ stderr string
35+ exitCode int
36+ interrupted bool
37+ err error
38+}
39+
40+var (
41+ shellInstance *PersistentShell
42+ shellInstanceOnce sync.Once
43+)
44+
45+func GetPersistentShell(workingDir string) *PersistentShell {
46+ shellInstanceOnce.Do(func() {
47+ shellInstance = newPersistentShell(workingDir)
48+ })
49+
50+ if !shellInstance.isAlive {
51+ shellInstance = newPersistentShell(shellInstance.cwd)
52+ }
53+
54+ return shellInstance
55+}
56+
57+func newPersistentShell(cwd string) *PersistentShell {
58+ shellPath := os.Getenv("SHELL")
59+ if shellPath == "" {
60+ shellPath = "/bin/bash"
61+ }
62+
63+ cmd := exec.Command(shellPath, "-l")
64+ cmd.Dir = cwd
65+
66+ stdinPipe, err := cmd.StdinPipe()
67+ if err != nil {
68+ return nil
69+ }
70+
71+ cmd.Env = append(os.Environ(), "GIT_EDITOR=true")
72+
73+ err = cmd.Start()
74+ if err != nil {
75+ return nil
76+ }
77+
78+ shell := &PersistentShell{
79+ cmd: cmd,
80+ stdin: stdinPipe.(*os.File),
81+ isAlive: true,
82+ cwd: cwd,
83+ commandQueue: make(chan *commandExecution, 10),
84+ }
85+
86+ go shell.processCommands()
87+
88+ go func() {
89+ err := cmd.Wait()
90+ if err != nil {
91+ }
92+ shell.isAlive = false
93+ close(shell.commandQueue)
94+ }()
95+
96+ return shell
97+}
98+
99+func (s *PersistentShell) processCommands() {
100+ for cmd := range s.commandQueue {
101+ result := s.execCommand(cmd.command, cmd.timeout, cmd.ctx)
102+ cmd.resultChan <- result
103+ }
104+}
105+
106+func (s *PersistentShell) execCommand(command string, timeout time.Duration, ctx context.Context) commandResult {
107+ s.mu.Lock()
108+ defer s.mu.Unlock()
109+
110+ if !s.isAlive {
111+ return commandResult{
112+ stderr: "Shell is not alive",
113+ exitCode: 1,
114+ err: errors.New("shell is not alive"),
115+ }
116+ }
117+
118+ tempDir := os.TempDir()
119+ stdoutFile := filepath.Join(tempDir, fmt.Sprintf("orbitowl-stdout-%d", time.Now().UnixNano()))
120+ stderrFile := filepath.Join(tempDir, fmt.Sprintf("orbitowl-stderr-%d", time.Now().UnixNano()))
121+ statusFile := filepath.Join(tempDir, fmt.Sprintf("orbitowl-status-%d", time.Now().UnixNano()))
122+ cwdFile := filepath.Join(tempDir, fmt.Sprintf("orbitowl-cwd-%d", time.Now().UnixNano()))
123+
124+ defer func() {
125+ os.Remove(stdoutFile)
126+ os.Remove(stderrFile)
127+ os.Remove(statusFile)
128+ os.Remove(cwdFile)
129+ }()
130+
131+ fullCommand := fmt.Sprintf(`
132+eval %s < /dev/null > %s 2> %s
133+EXEC_EXIT_CODE=$?
134+pwd > %s
135+echo $EXEC_EXIT_CODE > %s
136+`,
137+ shellQuote(command),
138+ shellQuote(stdoutFile),
139+ shellQuote(stderrFile),
140+ shellQuote(cwdFile),
141+ shellQuote(statusFile),
142+ )
143+
144+ _, err := s.stdin.Write([]byte(fullCommand + "\n"))
145+ if err != nil {
146+ return commandResult{
147+ stderr: fmt.Sprintf("Failed to write command to shell: %v", err),
148+ exitCode: 1,
149+ err: err,
150+ }
151+ }
152+
153+ interrupted := false
154+
155+ startTime := time.Now()
156+
157+ done := make(chan bool)
158+ go func() {
159+ for {
160+ select {
161+ case <-ctx.Done():
162+ s.killChildren()
163+ interrupted = true
164+ done <- true
165+ return
166+
167+ case <-time.After(10 * time.Millisecond):
168+ if fileExists(statusFile) && fileSize(statusFile) > 0 {
169+ done <- true
170+ return
171+ }
172+
173+ if timeout > 0 {
174+ elapsed := time.Since(startTime)
175+ if elapsed > timeout {
176+ s.killChildren()
177+ interrupted = true
178+ done <- true
179+ return
180+ }
181+ }
182+ }
183+ }
184+ }()
185+
186+ <-done
187+
188+ stdout := readFileOrEmpty(stdoutFile)
189+ stderr := readFileOrEmpty(stderrFile)
190+ exitCodeStr := readFileOrEmpty(statusFile)
191+ newCwd := readFileOrEmpty(cwdFile)
192+
193+ exitCode := 0
194+ if exitCodeStr != "" {
195+ fmt.Sscanf(exitCodeStr, "%d", &exitCode)
196+ } else if interrupted {
197+ exitCode = 143
198+ stderr += "\nCommand execution timed out or was interrupted"
199+ }
200+
201+ if newCwd != "" {
202+ s.cwd = strings.TrimSpace(newCwd)
203+ }
204+
205+ return commandResult{
206+ stdout: stdout,
207+ stderr: stderr,
208+ exitCode: exitCode,
209+ interrupted: interrupted,
210+ }
211+}
212+
213+func (s *PersistentShell) killChildren() {
214+ if s.cmd == nil || s.cmd.Process == nil {
215+ return
216+ }
217+
218+ pgrepCmd := exec.Command("pgrep", "-P", fmt.Sprintf("%d", s.cmd.Process.Pid))
219+ output, err := pgrepCmd.Output()
220+ if err != nil {
221+ return
222+ }
223+
224+ for _, pidStr := range strings.Split(string(output), "\n") {
225+ if pidStr = strings.TrimSpace(pidStr); pidStr != "" {
226+ var pid int
227+ fmt.Sscanf(pidStr, "%d", &pid)
228+ if pid > 0 {
229+ proc, err := os.FindProcess(pid)
230+ if err == nil {
231+ proc.Signal(syscall.SIGTERM)
232+ }
233+ }
234+ }
235+ }
236+}
237+
238+func (s *PersistentShell) Exec(ctx context.Context, command string, timeoutMs int) (string, string, int, bool, error) {
239+ if !s.isAlive {
240+ return "", "Shell is not alive", 1, false, errors.New("shell is not alive")
241+ }
242+
243+ timeout := time.Duration(timeoutMs) * time.Millisecond
244+
245+ resultChan := make(chan commandResult)
246+ s.commandQueue <- &commandExecution{
247+ command: command,
248+ timeout: timeout,
249+ resultChan: resultChan,
250+ ctx: ctx,
251+ }
252+
253+ result := <-resultChan
254+ return result.stdout, result.stderr, result.exitCode, result.interrupted, result.err
255+}
256+
257+func (s *PersistentShell) Close() {
258+ s.mu.Lock()
259+ defer s.mu.Unlock()
260+
261+ if !s.isAlive {
262+ return
263+ }
264+
265+ s.stdin.Write([]byte("exit\n"))
266+
267+ s.cmd.Process.Kill()
268+ s.isAlive = false
269+}
270+
271+func shellQuote(s string) string {
272+ return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'"
273+}
274+
275+func readFileOrEmpty(path string) string {
276+ content, err := os.ReadFile(path)
277+ if err != nil {
278+ return ""
279+ }
280+ return string(content)
281+}
282+
283+func fileExists(path string) bool {
284+ _, err := os.Stat(path)
285+ return err == nil
286+}
287+
288+func fileSize(path string) int64 {
289+ info, err := os.Stat(path)
290+ if err != nil {
291+ return 0
292+ }
293+ return info.Size()
294+}
internal/message/message.gonew file+122
@@ -0,0 +1,122 @@
1+package message
2+
3+import (
4+ "context"
5+ "encoding/json"
6+
7+ "github.com/cloudwego/eino/schema"
8+ "github.com/google/uuid"
9+ "github.com/kujtimiihoxha/termai/internal/db"
10+ "github.com/kujtimiihoxha/termai/internal/pubsub"
11+)
12+
13+type Message struct {
14+ ID string
15+ SessionID string
16+ MessageData schema.Message
17+
18+ CreatedAt int64
19+ UpdatedAt int64
20+}
21+
22+type Service interface {
23+ pubsub.Suscriber[Message]
24+ Create(sessionID string, messageData schema.Message) (Message, error)
25+ Get(id string) (Message, error)
26+ List(sessionID string) ([]Message, error)
27+ Delete(id string) error
28+ DeleteSessionMessages(sessionID string) error
29+}
30+
31+type service struct {
32+ *pubsub.Broker[Message]
33+ q db.Querier
34+ ctx context.Context
35+}
36+
37+func (s *service) Create(sessionID string, messageData schema.Message) (Message, error) {
38+ messageDataJSON, err := json.Marshal(messageData)
39+ if err != nil {
40+ return Message{}, err
41+ }
42+ dbMessage, err := s.q.CreateMessage(s.ctx, db.CreateMessageParams{
43+ ID: uuid.New().String(),
44+ SessionID: sessionID,
45+ MessageData: string(messageDataJSON),
46+ })
47+ if err != nil {
48+ return Message{}, err
49+ }
50+ message := s.fromDBItem(dbMessage)
51+ s.Publish(pubsub.CreatedEvent, message)
52+ return message, nil
53+}
54+
55+func (s *service) Delete(id string) error {
56+ message, err := s.Get(id)
57+ if err != nil {
58+ return err
59+ }
60+ err = s.q.DeleteMessage(s.ctx, message.ID)
61+ if err != nil {
62+ return err
63+ }
64+ s.Publish(pubsub.DeletedEvent, message)
65+ return nil
66+}
67+
68+func (s *service) DeleteSessionMessages(sessionID string) error {
69+ messages, err := s.List(sessionID)
70+ if err != nil {
71+ return err
72+ }
73+ for _, message := range messages {
74+ if message.SessionID == sessionID {
75+ err = s.Delete(message.ID)
76+ if err != nil {
77+ return err
78+ }
79+ }
80+ }
81+ return nil
82+}
83+
84+func (s *service) Get(id string) (Message, error) {
85+ dbMessage, err := s.q.GetMessage(s.ctx, id)
86+ if err != nil {
87+ return Message{}, err
88+ }
89+ return s.fromDBItem(dbMessage), nil
90+}
91+
92+func (s *service) List(sessionID string) ([]Message, error) {
93+ dbMessages, err := s.q.ListMessagesBySession(s.ctx, sessionID)
94+ if err != nil {
95+ return nil, err
96+ }
97+ messages := make([]Message, len(dbMessages))
98+ for i, dbMessage := range dbMessages {
99+ messages[i] = s.fromDBItem(dbMessage)
100+ }
101+ return messages, nil
102+}
103+
104+func (s *service) fromDBItem(item db.Message) Message {
105+ var messageData schema.Message
106+ json.Unmarshal([]byte(item.MessageData), &messageData)
107+ return Message{
108+ ID: item.ID,
109+ SessionID: item.SessionID,
110+ MessageData: messageData,
111+ CreatedAt: item.CreatedAt,
112+ UpdatedAt: item.UpdatedAt,
113+ }
114+}
115+
116+func NewService(ctx context.Context, q db.Querier) Service {
117+ return &service{
118+ Broker: pubsub.NewBroker[Message](),
119+ q: q,
120+ ctx: ctx,
121+ }
122+}
internal/session/session.go+21−18
@@ -9,13 +9,14 @@ import (
99)
1010
1111type Session struct {
12− ID string
13− Title string
14− MessageCount int64
15− Tokens int64
16− Cost float64
17− CreatedAt int64
18− UpdatedAt int64
12+ ID string
13+ Title string
14+ MessageCount int64
15+ PromptTokens int64
16+ CompletionTokens int64
17+ Cost float64
18+ CreatedAt int64
19+ UpdatedAt int64
1920}
2021
2122type Service interface {
@@ -69,10 +70,11 @@ func (s *service) Get(id string) (Session, error) {
6970
7071func (s *service) Save(session Session) (Session, error) {
7172 dbSession, err := s.q.UpdateSession(s.ctx, db.UpdateSessionParams{
72− ID: session.ID,
73− Title: session.Title,
74− Tokens: session.Tokens,
75− Cost: session.Cost,
73+ ID: session.ID,
74+ Title: session.Title,
75+ PromptTokens: session.PromptTokens,
76+ CompletionTokens: session.CompletionTokens,
77+ Cost: session.Cost,
7678 })
7779 if err != nil {
7880 return Session{}, err
@@ -96,13 +98,14 @@ func (s *service) List() ([]Session, error) {
9698
9799func (s service) fromDBItem(item db.Session) Session {
98100 return Session{
99− ID: item.ID,
100− Title: item.Title,
101− MessageCount: item.MessageCount,
102− Tokens: item.Tokens,
103− Cost: item.Cost,
104− CreatedAt: item.CreatedAt,
105− UpdatedAt: item.UpdatedAt,
101+ ID: item.ID,
102+ Title: item.Title,
103+ MessageCount: item.MessageCount,
104+ PromptTokens: item.PromptTokens,
105+ CompletionTokens: item.CompletionTokens,
106+ Cost: item.Cost,
107+ CreatedAt: item.CreatedAt,
108+ UpdatedAt: item.UpdatedAt,
106109 }
107110}
108111
internal/tui/components/repl/editor.go+38−14
@@ -1,8 +1,11 @@
11package repl
22
33import (
4+ "strings"
5+
46 "github.com/charmbracelet/bubbles/key"
57 tea "github.com/charmbracelet/bubbletea"
8+ "github.com/cloudwego/eino/schema"
69 "github.com/kujtimiihoxha/termai/internal/app"
710 "github.com/kujtimiihoxha/termai/internal/tui/layout"
811 "github.com/kujtimiihoxha/vimtea"
@@ -13,6 +16,7 @@ type EditorCmp interface {
1316 layout.Focusable
1417 layout.Sizeable
1518 layout.Bordered
19+ layout.Bindings
1620}
1721
1822type editorCmp struct {
@@ -25,12 +29,16 @@ type editorCmp struct {
2529 height int
2630}
2731
28−type localKeyMap struct {
29− SendMessage key.Binding
30− SendMessageI key.Binding
32+type editorKeyMap struct {
33+ SendMessage key.Binding
34+ SendMessageI key.Binding
35+ InsertMode key.Binding
36+ NormaMode key.Binding
37+ VisualMode key.Binding
38+ VisualLineMode key.Binding
3139}
3240
33−var keyMap = localKeyMap{
41+var editorKeyMapValue = editorKeyMap{
3442 SendMessage: key.NewBinding(
3543 key.WithKeys("enter"),
3644 key.WithHelp("enter", "send message normal mode"),
@@ -39,6 +47,22 @@ var keyMap = localKeyMap{
3947 key.WithKeys("ctrl+s"),
4048 key.WithHelp("ctrl+s", "send message insert mode"),
4149 ),
50+ InsertMode: key.NewBinding(
51+ key.WithKeys("i"),
52+ key.WithHelp("i", "insert mode"),
53+ ),
54+ NormaMode: key.NewBinding(
55+ key.WithKeys("esc"),
56+ key.WithHelp("esc", "normal mode"),
57+ ),
58+ VisualMode: key.NewBinding(
59+ key.WithKeys("v"),
60+ key.WithHelp("v", "visual mode"),
61+ ),
62+ VisualLineMode: key.NewBinding(
63+ key.WithKeys("V"),
64+ key.WithHelp("V", "visual line mode"),
65+ ),
4266}
4367
4468func (m *editorCmp) Init() tea.Cmd {
@@ -58,11 +82,11 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
5882 switch msg := msg.(type) {
5983 case tea.KeyMsg:
6084 switch {
61− case key.Matches(msg, keyMap.SendMessage):
85+ case key.Matches(msg, editorKeyMapValue.SendMessage):
6286 if m.editorMode == vimtea.ModeNormal {
6387 return m, m.Send()
6488 }
65− case key.Matches(msg, keyMap.SendMessageI):
89+ case key.Matches(msg, editorKeyMapValue.SendMessageI):
6690 if m.editorMode == vimtea.ModeInsert {
6791 return m, m.Send()
6892 }
@@ -75,36 +99,30 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
7599 return m, nil
76100}
77101
78−// Blur implements EditorCmp.
79102func (m *editorCmp) Blur() tea.Cmd {
80103 m.focused = false
81104 return nil
82105}
83106
84−// BorderText implements EditorCmp.
85107func (m *editorCmp) BorderText() map[layout.BorderPosition]string {
86108 return map[layout.BorderPosition]string{
87109 layout.TopLeftBorder: "New Message",
88110 }
89111}
90112
91−// Focus implements EditorCmp.
92113func (m *editorCmp) Focus() tea.Cmd {
93114 m.focused = true
94115 return m.editor.Tick()
95116}
96117
97−// GetSize implements EditorCmp.
98118func (m *editorCmp) GetSize() (int, int) {
99119 return m.width, m.height
100120}
101121
102−// IsFocused implements EditorCmp.
103122func (m *editorCmp) IsFocused() bool {
104123 return m.focused
105124}
106125
107−// SetSize implements EditorCmp.
108126func (m *editorCmp) SetSize(width int, height int) {
109127 m.width = width
110128 m.height = height
@@ -113,8 +131,10 @@ func (m *editorCmp) SetSize(width int, height int) {
113131
114132func (m *editorCmp) Send() tea.Cmd {
115133 return func() tea.Msg {
116− // TODO: Send message
117− return nil
134+ content := strings.Join(m.editor.GetBuffer().Lines(), "\n")
135+ m.app.Messages.Create(m.sessionID, *schema.UserMessage(content))
136+ m.app.LLM.SendRequest(m.sessionID, content)
137+ return m.editor.Reset()
118138 }
119139}
120140
@@ -122,6 +142,10 @@ func (m *editorCmp) View() string {
122142 return m.editor.View()
123143}
124144
145+func (m *editorCmp) BindingKeys() []key.Binding {
146+ return layout.KeyMapToSlice(editorKeyMapValue)
147+}
148+
125149func NewEditorCmp(app *app.App) EditorCmp {
126150 return &editorCmp{
127151 app: app,
internal/tui/components/repl/messages.go+26−6
@@ -2,27 +2,47 @@ package repl
22
33import (
44 tea "github.com/charmbracelet/bubbletea"
5+ "github.com/charmbracelet/lipgloss"
56 "github.com/kujtimiihoxha/termai/internal/app"
7+ "github.com/kujtimiihoxha/termai/internal/message"
8+ "github.com/kujtimiihoxha/termai/internal/pubsub"
9+ "github.com/kujtimiihoxha/termai/internal/session"
610)
711
812type messagesCmp struct {
9− app *app.App
13+ app *app.App
14+ messages []message.Message
15+ session session.Session
1016}
1117
12−func (i *messagesCmp) Init() tea.Cmd {
18+func (m *messagesCmp) Init() tea.Cmd {
1319 return nil
1420}
1521
16−func (i *messagesCmp) Update(_ tea.Msg) (tea.Model, tea.Cmd) {
17− return i, nil
22+func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
23+ switch msg := msg.(type) {
24+ case pubsub.Event[message.Message]:
25+ if msg.Type == pubsub.CreatedEvent {
26+ m.messages = append(m.messages, msg.Payload)
27+ }
28+ case SelectedSessionMsg:
29+ m.session, _ = m.app.Sessions.Get(msg.SessionID)
30+ m.messages, _ = m.app.Messages.List(m.session.ID)
31+ }
32+ return m, nil
1833}
1934
2035func (i *messagesCmp) View() string {
21− return "Messages"
36+ stringMessages := make([]string, len(i.messages))
37+ for idx, msg := range i.messages {
38+ stringMessages[idx] = msg.MessageData.Content
39+ }
40+ return lipgloss.JoinVertical(lipgloss.Top, stringMessages...)
2241}
2342
2443func NewMessagesCmp(app *app.App) tea.Model {
2544 return &messagesCmp{
26− app,
45+ app: app,
46+ messages: []message.Message{},
2747 }
2848}
internal/tui/components/repl/sessions.go+41−4
@@ -7,6 +7,7 @@ import (
77 "github.com/charmbracelet/bubbles/list"
88 tea "github.com/charmbracelet/bubbletea"
99 "github.com/kujtimiihoxha/termai/internal/app"
10+ "github.com/kujtimiihoxha/termai/internal/pubsub"
1011 "github.com/kujtimiihoxha/termai/internal/session"
1112 "github.com/kujtimiihoxha/termai/internal/tui/layout"
1213 "github.com/kujtimiihoxha/termai/internal/tui/styles"
@@ -42,19 +43,30 @@ type SelectedSessionMsg struct {
4243 SessionID string
4344}
4445
46+type sessionsKeyMap struct {
47+ Select key.Binding
48+}
49+
50+var sessionKeyMapValue = sessionsKeyMap{
51+ Select: key.NewBinding(
52+ key.WithKeys("enter", " "),
53+ key.WithHelp("enter/space", "select session"),
54+ ),
55+}
56+
4557func (i *sessionsCmp) Init() tea.Cmd {
4658 existing, err := i.app.Sessions.List()
4759 if err != nil {
4860 return util.ReportError(err)
4961 }
5062 if len(existing) == 0 || existing[0].MessageCount > 0 {
51− session, err := i.app.Sessions.Create(
63+ newSession, err := i.app.Sessions.Create(
5264 "New Session",
5365 )
5466 if err != nil {
5567 return util.ReportError(err)
5668 }
57− existing = append(existing, session)
69+ existing = append([]session.Session{newSession}, existing...)
5870 }
5971 return tea.Batch(
6072 util.CmdHandler(InsertSessionsMsg{existing}),
@@ -70,10 +82,35 @@ func (i *sessionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
7082 items[i] = listItem{
7183 id: s.ID,
7284 title: s.Title,
73− desc: fmt.Sprintf("Tokens: %d, Cost: %.2f", s.Tokens, s.Cost),
85+ desc: fmt.Sprintf("Tokens: %d, Cost: %.2f", s.PromptTokens+s.CompletionTokens, s.Cost),
7486 }
7587 }
7688 return i, i.list.SetItems(items)
89+ case pubsub.Event[session.Session]:
90+ if msg.Type == pubsub.UpdatedEvent {
91+ // update the session in the list
92+ items := i.list.Items()
93+ for idx, item := range items {
94+ s := item.(listItem)
95+ if s.id == msg.Payload.ID {
96+ s.title = msg.Payload.Title
97+ s.desc = fmt.Sprintf("Tokens: %d, Cost: %.2f", msg.Payload.PromptTokens+msg.Payload.CompletionTokens, msg.Payload.Cost)
98+ items[idx] = s
99+ break
100+ }
101+ }
102+ return i, i.list.SetItems(items)
103+ }
104+
105+ case tea.KeyMsg:
106+ switch {
107+ case key.Matches(msg, sessionKeyMapValue.Select):
108+ selected := i.list.SelectedItem()
109+ if selected == nil {
110+ return i, nil
111+ }
112+ return i, util.CmdHandler(SelectedSessionMsg{selected.(listItem).id})
113+ }
77114 }
78115 if i.focused {
79116 u, cmd := i.list.Update(msg)
@@ -129,7 +166,7 @@ func (i *sessionsCmp) BorderText() map[layout.BorderPosition]string {
129166}
130167
131168func (i *sessionsCmp) BindingKeys() []key.Binding {
132− return layout.KeyMapToSlice(i.list.KeyMap)
169+ return append(layout.KeyMapToSlice(i.list.KeyMap), sessionKeyMapValue.Select)
133170}
134171
135172func NewSessionsCmp(app *app.App) SessionsCmp {
internal/tui/page/repl.go+1
@@ -16,5 +16,6 @@ func NewReplPage(app *app.App) tea.Model {
1616 layout.BentoRightTopPane: repl.NewMessagesCmp(app),
1717 layout.BentoRightBottomPane: repl.NewEditorCmp(app),
1818 },
19+ layout.WithBentoLayoutCurrentPane(layout.BentoRightBottomPane),
1920 )
2021}
internal/tui/tui.go+7
@@ -1,10 +1,14 @@
11package tui
22
33import (
4+ "log"
5+
46 "github.com/charmbracelet/bubbles/key"
57 tea "github.com/charmbracelet/bubbletea"
68 "github.com/charmbracelet/lipgloss"
79 "github.com/kujtimiihoxha/termai/internal/app"
10+ "github.com/kujtimiihoxha/termai/internal/llm"
11+ "github.com/kujtimiihoxha/termai/internal/pubsub"
812 "github.com/kujtimiihoxha/termai/internal/tui/components/core"
913 "github.com/kujtimiihoxha/termai/internal/tui/components/dialog"
1014 "github.com/kujtimiihoxha/termai/internal/tui/layout"
@@ -66,6 +70,9 @@ func (a appModel) Init() tea.Cmd {
6670
6771func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
6872 switch msg := msg.(type) {
73+ case pubsub.Event[llm.AgentEvent]:
74+ log.Println("Event received")
75+ log.Println(msg)
6976 case vimtea.EditorModeMsg:
7077 a.editorMode = msg.Mode
7178 case tea.WindowSizeMsg:
main.go+14−2
@@ -1,11 +1,23 @@
11/*
22Copyright © 2025 NAME HERE <EMAIL ADDRESS>
3−
43*/
54package main
65
7−import "github.com/kujtimiihoxha/termai/cmd"
6+import (
7+ "log"
8+ "os"
9+
10+ "github.com/kujtimiihoxha/termai/cmd"
11+)
812
913func main() {
14+ // Create a log file and make that the log output
15+ logfile, err := os.OpenFile("debug.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o666)
16+ if err != nil {
17+ panic(err)
18+ }
19+
20+ log.SetOutput(logfile)
21+
1022 cmd.Execute()
1123}