Skip to content
This repository was archived by the owner on Sep 18, 2025. It is now read-only.

Commit 904061c

Browse files
committed
additional tools
1 parent 005b8ac commit 904061c

File tree

33 files changed

+3258
-236
lines changed

33 files changed

+3258
-236
lines changed

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# TermAI
2+
3+
**⚠️ WORK IN PROGRESS ⚠️**
4+
5+
This project is currently under active development.
6+
7+
## Current Progress
8+
9+
- Initial CLI setup
10+
- Basic functionality implementation
11+
- Working on core features
12+
13+
More details coming soon.

cmd/root.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/kujtimiihoxha/termai/internal/db"
1111
"github.com/kujtimiihoxha/termai/internal/llm/models"
1212
"github.com/kujtimiihoxha/termai/internal/tui"
13+
zone "github.com/lrstanley/bubblezone"
1314
"github.com/spf13/cobra"
1415
"github.com/spf13/viper"
1516
)
@@ -37,9 +38,11 @@ var rootCmd = &cobra.Command{
3738

3839
app := app.New(ctx, conn)
3940
app.Logger.Info("Starting termai...")
41+
zone.NewGlobal()
4042
tui := tea.NewProgram(
4143
tui.New(app),
4244
tea.WithAltScreen(),
45+
tea.WithMouseCellMotion(),
4346
)
4447
app.Logger.Info("Setting up subscriptions...")
4548
ch, unsub := setupSubscriptions(app)
@@ -102,6 +105,16 @@ func setupSubscriptions(app *app.App) (chan tea.Msg, func()) {
102105
wg.Done()
103106
}()
104107
}
108+
{
109+
sub := app.Permissions.Subscribe(ctx)
110+
wg.Add(1)
111+
go func() {
112+
for ev := range sub {
113+
ch <- ev
114+
}
115+
wg.Done()
116+
}()
117+
}
105118
return ch, func() {
106119
cancel()
107120
wg.Wait()
@@ -130,6 +143,7 @@ func loadConfig() {
130143
// LLM
131144
viper.SetDefault("models.big", string(models.DefaultBigModel))
132145
viper.SetDefault("models.small", string(models.DefaultLittleModel))
146+
133147
viper.SetDefault("providers.openai.key", os.Getenv("OPENAI_API_KEY"))
134148
viper.SetDefault("providers.anthropic.key", os.Getenv("ANTHROPIC_API_KEY"))
135149
viper.SetDefault("providers.groq.key", os.Getenv("GROQ_API_KEY"))

go.mod

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/kujtimiihoxha/termai
33
go 1.23.5
44

55
require (
6+
github.com/bmatcuk/doublestar/v4 v4.8.1
67
github.com/catppuccin/go v0.3.0
78
github.com/charmbracelet/bubbles v0.20.0
89
github.com/charmbracelet/bubbletea v1.3.4
@@ -16,11 +17,13 @@ require (
1617
github.com/golang-migrate/migrate/v4 v4.18.2
1718
github.com/google/uuid v1.6.0
1819
github.com/kujtimiihoxha/vimtea v0.0.3-0.20250317175717-9d8ba9c69840
20+
github.com/lrstanley/bubblezone v0.0.0-20250315020633-c249a3fe1231
1921
github.com/mattn/go-runewidth v0.0.16
2022
github.com/mattn/go-sqlite3 v1.14.24
2123
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6
2224
github.com/muesli/reflow v0.3.0
2325
github.com/muesli/termenv v0.16.0
26+
github.com/sergi/go-diff v1.3.1
2427
github.com/spf13/cobra v1.9.1
2528
github.com/spf13/viper v1.20.0
2629
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1
@@ -112,7 +115,8 @@ require (
112115
golang.org/x/arch v0.11.0 // indirect
113116
golang.org/x/net v0.33.0 // indirect
114117
golang.org/x/sync v0.12.0 // indirect
115-
golang.org/x/sys v0.30.0 // indirect
118+
golang.org/x/sys v0.31.0 // indirect
119+
golang.org/x/term v0.30.0 // indirect
116120
golang.org/x/text v0.23.0 // indirect
117121
gopkg.in/yaml.v2 v2.4.0 // indirect
118122
gopkg.in/yaml.v3 v3.0.1 // indirect

go.sum

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/
4646
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
4747
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
4848
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
49+
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
50+
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
4951
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
5052
github.com/bugsnag/bugsnag-go v1.4.0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
5153
github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
@@ -175,6 +177,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
175177
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
176178
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
177179
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
180+
github.com/lrstanley/bubblezone v0.0.0-20250315020633-c249a3fe1231 h1:9rjt7AfnrXKNSZhp36A3/4QAZAwGGCGD/p8Bse26zms=
181+
github.com/lrstanley/bubblezone v0.0.0-20250315020633-c249a3fe1231/go.mod h1:S5etECMx+sZnW0Gm100Ma9J1PgVCTgNyFaqGu2b08b4=
178182
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
179183
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
180184
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
@@ -241,6 +245,8 @@ github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
241245
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
242246
github.com/sashabaranov/go-openai v1.32.5 h1:/eNVa8KzlE7mJdKPZDj6886MUzZQjoVHyn0sLvIt5qA=
243247
github.com/sashabaranov/go-openai v1.32.5/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
248+
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
249+
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
244250
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
245251
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
246252
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
@@ -268,6 +274,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
268274
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
269275
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
270276
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
277+
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
271278
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
272279
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
273280
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@@ -327,8 +334,8 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h
327334
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
328335
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
329336
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
330-
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
331-
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
337+
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
338+
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
332339
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
333340
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
334341
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

internal/app/services.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,18 @@ import (
88
"github.com/kujtimiihoxha/termai/internal/llm"
99
"github.com/kujtimiihoxha/termai/internal/logging"
1010
"github.com/kujtimiihoxha/termai/internal/message"
11+
"github.com/kujtimiihoxha/termai/internal/permission"
1112
"github.com/kujtimiihoxha/termai/internal/session"
1213
"github.com/spf13/viper"
1314
)
1415

1516
type App struct {
1617
Context context.Context
1718

18-
Sessions session.Service
19-
Messages message.Service
20-
LLM llm.Service
19+
Sessions session.Service
20+
Messages message.Service
21+
Permissions permission.Service
22+
LLM llm.Service
2123

2224
Logger logging.Interface
2325
}
@@ -32,10 +34,11 @@ func New(ctx context.Context, conn *sql.DB) *App {
3234
llm := llm.NewService(ctx, log, sessions, messages)
3335

3436
return &App{
35-
Context: ctx,
36-
Sessions: sessions,
37-
Messages: messages,
38-
LLM: llm,
39-
Logger: log,
37+
Context: ctx,
38+
Sessions: sessions,
39+
Messages: messages,
40+
Permissions: permission.Default,
41+
LLM: llm,
42+
Logger: log,
4043
}
4144
}

internal/llm/agent/coder.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,15 @@ import (
1717
)
1818

1919
func coderTools() []tool.BaseTool {
20+
wd := viper.GetString("wd")
2021
return []tool.BaseTool{
21-
tools.NewBashTool(viper.GetString("wd")),
22+
tools.NewAgentTool(wd),
23+
tools.NewBashTool(wd),
24+
tools.NewLsTool(wd),
25+
tools.NewGlobTool(wd),
26+
tools.NewViewTool(wd),
27+
tools.NewWriteTool(wd),
28+
tools.NewEditTool(wd),
2229
}
2330
}
2431

internal/llm/llm.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ func (s *service) handleRequest(id string, sessionID string, content string) {
8888
return
8989
}
9090

91-
log.Printf("Request: %s", content)
9291
currentAgent, systemMessage, err := agent.GetAgent(s.ctx, viper.GetString("agents.default"))
9392
if err != nil {
9493
s.Publish(AgentErrorEvent, AgentEvent{
@@ -172,7 +171,6 @@ func (s *service) handleRequest(id string, sessionID string, content string) {
172171
}
173172
session.PromptTokens += int64(usage.PromptTokens)
174173
session.CompletionTokens += int64(usage.CompletionTokens)
175-
// TODO: calculate cost
176174
model := models.SupportedModels[models.ModelID(viper.GetString("models.big"))]
177175
session.Cost += float64(usage.PromptTokens)*(model.CostPer1MIn/1_000_000) +
178176
float64(usage.CompletionTokens)*(model.CostPer1MOut/1_000_000)

internal/llm/models/models.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ type Model struct {
2626
}
2727

2828
const (
29-
DefaultBigModel = GPT4oMini
30-
DefaultLittleModel = GPT4oMini
29+
DefaultBigModel = Claude37Sonnet
30+
DefaultLittleModel = Claude37Sonnet
3131
)
3232

3333
// Model IDs
@@ -118,10 +118,12 @@ var SupportedModels = map[ModelID]Model{
118118
APIModel: "claude-3-haiku",
119119
},
120120
Claude37Sonnet: {
121-
ID: Claude37Sonnet,
122-
Name: "Claude 3.7 Sonnet",
123-
Provider: ProviderAnthropic,
124-
APIModel: "claude-3-7-sonnet-20250219",
121+
ID: Claude37Sonnet,
122+
Name: "Claude 3.7 Sonnet",
123+
Provider: ProviderAnthropic,
124+
APIModel: "claude-3-7-sonnet-20250219",
125+
CostPer1MIn: 3.0,
126+
CostPer1MOut: 15.0,
125127
},
126128
// Google
127129
Gemini20Pro: {

internal/llm/tools/agent.go

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package tools
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"os"
8+
"runtime"
9+
"time"
10+
11+
"github.com/cloudwego/eino/components/tool"
12+
"github.com/cloudwego/eino/schema"
13+
14+
"github.com/cloudwego/eino/compose"
15+
"github.com/cloudwego/eino/flow/agent/react"
16+
"github.com/kujtimiihoxha/termai/internal/llm/models"
17+
"github.com/spf13/viper"
18+
)
19+
20+
type agentTool struct {
21+
workingDir string
22+
}
23+
24+
const (
25+
AgentToolName = "agent"
26+
)
27+
28+
type AgentParams struct {
29+
Prompt string `json:"prompt"`
30+
}
31+
32+
func taskAgentTools() []tool.BaseTool {
33+
wd := viper.GetString("wd")
34+
return []tool.BaseTool{
35+
NewBashTool(wd),
36+
NewLsTool(wd),
37+
NewGlobTool(wd),
38+
NewViewTool(wd),
39+
NewWriteTool(wd),
40+
NewEditTool(wd),
41+
}
42+
}
43+
44+
func NewTaskAgent(ctx context.Context) (*react.Agent, error) {
45+
model, err := models.GetModel(ctx, models.ModelID(viper.GetString("models.big")))
46+
if err != nil {
47+
return nil, err
48+
}
49+
reactAgent, err := react.NewAgent(ctx, &react.AgentConfig{
50+
Model: model,
51+
ToolsConfig: compose.ToolsNodeConfig{
52+
Tools: taskAgentTools(),
53+
},
54+
MaxStep: 1000,
55+
})
56+
if err != nil {
57+
return nil, err
58+
}
59+
60+
return reactAgent, nil
61+
}
62+
63+
func TaskAgentSystemPrompt() string {
64+
agentPrompt := `You are an agent for Orbitowl. Given the user's prompt, you should use the tools available to you to answer the user's question.
65+
66+
Notes:
67+
1. IMPORTANT: You should be concise, direct, and to the point, since your responses will be displayed on a command line interface. 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...".
68+
2. When relevant, share file names and code snippets relevant to the query
69+
3. Any file paths you return in your final response MUST be absolute. DO NOT use relative paths.
70+
71+
Here is useful information about the environment you are running in:
72+
<env>
73+
Working directory: %s
74+
Platform: %s
75+
Today's date: %s
76+
</env>`
77+
78+
cwd, err := os.Getwd()
79+
if err != nil {
80+
cwd = "unknown"
81+
}
82+
83+
platform := runtime.GOOS
84+
85+
switch platform {
86+
case "darwin":
87+
platform = "macos"
88+
case "windows":
89+
platform = "windows"
90+
case "linux":
91+
platform = "linux"
92+
}
93+
return fmt.Sprintf(agentPrompt, cwd, platform, time.Now().Format("1/2/2006"))
94+
}
95+
96+
func (b *agentTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
97+
return &schema.ToolInfo{
98+
Name: AgentToolName,
99+
Desc: "Launch a new agent that has access to the following tools: GlobTool, GrepTool, LS, View, ReadNotebook. When you are searching for a keyword or file and are not confident that you will find the right match on the first try, use the Agent tool to perform the search for you. For example:\n\n- If you are searching for a keyword like \"config\" or \"logger\", or for questions like \"which file does X?\", the Agent tool is strongly recommended\n- If you want to read a specific file path, use the View or GlobTool tool instead of the Agent tool, to find the match more quickly\n- If you are searching for a specific class definition like \"class Foo\", use the GlobTool tool instead, to find the match more quickly\n\nUsage notes:\n1. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses\n2. When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.\n3. Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.\n4. The agent's outputs should generally be trusted\n5. IMPORTANT: The agent can not use Bash, Replace, Edit, NotebookEditCell, so can not modify files. If you want to use these tools, use them directly instead of going through the agent.",
100+
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
101+
"prompt": {
102+
Type: "string",
103+
Desc: "The task for the agent to perform",
104+
Required: true,
105+
},
106+
}),
107+
}, nil
108+
}
109+
110+
func (b *agentTool) InvokableRun(ctx context.Context, args string, opts ...tool.Option) (string, error) {
111+
var params AgentParams
112+
if err := json.Unmarshal([]byte(args), &params); err != nil {
113+
return "", err
114+
}
115+
if params.Prompt == "" {
116+
return "prompt is required", nil
117+
}
118+
119+
a, err := NewTaskAgent(ctx)
120+
if err != nil {
121+
return "", err
122+
}
123+
out, err := a.Generate(
124+
ctx,
125+
[]*schema.Message{
126+
schema.SystemMessage(TaskAgentSystemPrompt()),
127+
schema.UserMessage(params.Prompt),
128+
},
129+
)
130+
if err != nil {
131+
return "", err
132+
}
133+
134+
return out.Content, nil
135+
}
136+
137+
func NewAgentTool(wd string) tool.InvokableTool {
138+
return &agentTool{
139+
workingDir: wd,
140+
}
141+
}

0 commit comments

Comments
 (0)