feat: Add Google Calendar integration, GitHub tools, and Email enhancements

This commit is contained in:
Udbhav 2026-02-17 11:22:21 +05:30
parent 7a6a973ef3
commit 4cf95a4276
12 changed files with 1254 additions and 390 deletions

View file

@ -14,7 +14,9 @@
"enabled": false, "enabled": false,
"token": "YOUR_TELEGRAM_BOT_TOKEN", "token": "YOUR_TELEGRAM_BOT_TOKEN",
"proxy": "", "proxy": "",
"allow_from": ["YOUR_USER_ID"] "allow_from": [
"YOUR_USER_ID"
]
}, },
"discord": { "discord": {
"enabled": false, "enabled": false,
@ -80,7 +82,10 @@
"smtp_user": "your-email@gmail.com", "smtp_user": "your-email@gmail.com",
"smtp_password": "your-app-password", "smtp_password": "your-app-password",
"poll_interval": 30, "poll_interval": 30,
"allow_from": ["allowed-sender@example.com"] "forward_to": "telegram:YOUR_CHAT_ID",
"allow_from": [
"allowed-sender@example.com"
]
} }
}, },
"providers": { "providers": {
@ -132,6 +137,10 @@
"api_key": "YOUR_BRAVE_API_KEY", "api_key": "YOUR_BRAVE_API_KEY",
"max_results": 5 "max_results": 5
} }
},
"github": {
"enabled": false,
"token": "YOUR_GITHUB_TOKEN"
} }
}, },
"heartbeat": { "heartbeat": {

29
go.mod
View file

@ -10,23 +10,52 @@ require (
github.com/chzyer/readline v1.5.1 github.com/chzyer/readline v1.5.1
github.com/emersion/go-imap v1.2.1 github.com/emersion/go-imap v1.2.1
github.com/emersion/go-message v0.18.2 github.com/emersion/go-message v0.18.2
github.com/google/go-github/v60 v60.0.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3 github.com/gorilla/websocket v1.5.3
github.com/larksuite/oapi-sdk-go/v3 v3.5.3 github.com/larksuite/oapi-sdk-go/v3 v3.5.3
github.com/mymmrac/telego v1.6.0 github.com/mymmrac/telego v1.6.0
github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1 github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1
github.com/openai/openai-go/v3 v3.22.0 github.com/openai/openai-go/v3 v3.22.0
github.com/shirou/gopsutil/v3 v3.24.5
github.com/slack-go/slack v0.17.3 github.com/slack-go/slack v0.17.3
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/tencent-connect/botgo v0.2.1 github.com/tencent-connect/botgo v0.2.1
golang.org/x/oauth2 v0.35.0 golang.org/x/oauth2 v0.35.0
google.golang.org/api v0.266.0
) )
require ( require (
cloud.google.com/go/auth v0.18.1 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
golang.org/x/text v0.34.0 // indirect golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect
google.golang.org/grpc v1.78.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

87
go.sum
View file

@ -1,4 +1,10 @@
cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs=
cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
github.com/adhocore/gronx v1.19.6 h1:5KNVcoR9ACgL9HhEqCm5QXsab/gI4QDIybTAWcXDKDc= github.com/adhocore/gronx v1.19.6 h1:5KNVcoR9ACgL9HhEqCm5QXsab/gI4QDIybTAWcXDKDc=
github.com/adhocore/gronx v1.19.6/go.mod h1:7oUY1WAU8rEJWmAxXR2DN0JaO4gi9khSgKjiRypqteg= github.com/adhocore/gronx v1.19.6/go.mod h1:7oUY1WAU8rEJWmAxXR2DN0JaO4gi9khSgKjiRypqteg=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
@ -17,6 +23,8 @@ github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5m
github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U= github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
@ -39,10 +47,19 @@ github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTe
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk= github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/github/copilot-sdk/go v0.1.23 h1:uExtO/inZQndCZMiSAA1hvXINiz9tqo/MZgQzFzurxw= github.com/github/copilot-sdk/go v0.1.23 h1:uExtO/inZQndCZMiSAA1hvXINiz9tqo/MZgQzFzurxw=
github.com/github/copilot-sdk/go v0.1.23/go.mod h1:GdwwBfMbm9AABLEM3x5IZKw4ZfwCYxZ1BgyytmZenQ0= github.com/github/copilot-sdk/go v0.1.23/go.mod h1:GdwwBfMbm9AABLEM3x5IZKw4ZfwCYxZ1BgyytmZenQ0=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w= github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w=
github.com/go-resty/resty/v2 v2.6.0/go.mod h1:PwvJS6hvaPkjtjNg9ph+VrSD92bi5Zq73w/BIH7cC3Q= github.com/go-resty/resty/v2 v2.6.0/go.mod h1:PwvJS6hvaPkjtjNg9ph+VrSD92bi5Zq73w/BIH7cC3Q=
github.com/go-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4= github.com/go-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4=
@ -61,19 +78,32 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github/v60 v60.0.0 h1:oLG98PsLauFvvu4D/YPxq374jhSxFYdzQGNCyONLfn8=
github.com/google/go-github/v60 v60.0.0/go.mod h1:ByhX2dP9XT9o/ll2yXAu2VD8l5eNVg8hD4Cr0S/LmQk=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao=
github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8=
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
@ -89,14 +119,17 @@ github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzh
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/larksuite/oapi-sdk-go/v3 v3.5.3 h1:xvf8Dv29kBXC5/DNDCLhHkAFW8l/0LlQJimO5Zn+JUk= github.com/larksuite/oapi-sdk-go/v3 v3.5.3 h1:xvf8Dv29kBXC5/DNDCLhHkAFW8l/0LlQJimO5Zn+JUk=
github.com/larksuite/oapi-sdk-go/v3 v3.5.3/go.mod h1:ZEplY+kwuIrj/nqw5uSCINNATcH3KdxSN7y+UxYY5fI= github.com/larksuite/oapi-sdk-go/v3 v3.5.3/go.mod h1:ZEplY+kwuIrj/nqw5uSCINNATcH3KdxSN7y+UxYY5fI=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/mymmrac/telego v1.6.0 h1:Zc8rgyHozvd/7ZgyrigyHdAF9koHYMfilYfyB6wlFC0= github.com/mymmrac/telego v1.6.0 h1:Zc8rgyHozvd/7ZgyrigyHdAF9koHYMfilYfyB6wlFC0=
github.com/mymmrac/telego v1.6.0/go.mod h1:xt6ZWA8zi8KmuzryE1ImEdl9JSwjHNpM4yhC7D8hU4Y= github.com/mymmrac/telego v1.6.0/go.mod h1:xt6ZWA8zi8KmuzryE1ImEdl9JSwjHNpM4yhC7D8hU4Y=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
@ -114,9 +147,18 @@ github.com/openai/openai-go/v3 v3.22.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbD
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/slack-go/slack v0.17.3 h1:zV5qO3Q+WJAQ/XwbGfNFrRMaJ5T/naqaonyPV/1TP4g= github.com/slack-go/slack v0.17.3 h1:zV5qO3Q+WJAQ/XwbGfNFrRMaJ5T/naqaonyPV/1TP4g=
github.com/slack-go/slack v0.17.3/go.mod h1:X+UqOufi3LYQHDnMG1vxf0J8asC6+WllXrVrhl8/Prk= github.com/slack-go/slack v0.17.3/go.mod h1:X+UqOufi3LYQHDnMG1vxf0J8asC6+WllXrVrhl8/Prk=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -145,6 +187,10 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
@ -158,6 +204,22 @@ github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3i
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y= golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y=
@ -204,11 +266,13 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -218,6 +282,7 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
@ -235,8 +300,8 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
@ -248,6 +313,18 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.266.0 h1:hco+oNCf9y7DmLeAtHJi/uBAY7n/7XC9mZPxu1ROiyk=
google.golang.org/api v0.266.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@ -256,6 +333,8 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=

View file

@ -104,7 +104,12 @@ func createToolRegistry(workspace string, restrict bool, cfg *config.Config, msg
// Email tools // Email tools
registry.Register(tools.NewReadEmailTool(cfg.Channels.Email)) registry.Register(tools.NewReadEmailTool(cfg.Channels.Email))
registry.Register(tools.NewSendEmailTool(cfg.Channels.Email)) registry.Register(tools.NewSendEmailTool(cfg.Channels.Email))
registry.Register(tools.NewCheckMailTool(msgBus)) // Keep for background sync trigger if needed registry.Register(tools.NewCheckMailTool(cfg.Channels.Email)) // Now fetches content
// Developer tools
registry.Register(tools.NewGitHubTool(cfg.Tools.GitHub))
registry.Register(tools.NewSystemTool())
registry.Register(tools.NewCalendarTool(cfg.Tools.Calendar))
return registry return registry
} }

View file

@ -7,6 +7,7 @@ import (
"io" "io"
"net/smtp" "net/smtp"
"strings" "strings"
"sync"
"time" "time"
"github.com/emersion/go-imap" "github.com/emersion/go-imap"
@ -21,33 +22,111 @@ import (
type EmailChannel struct { type EmailChannel struct {
*BaseChannel *BaseChannel
config config.EmailConfig config config.EmailConfig
imapClient *client.Client bus *bus.MessageBus
imapClients map[string]*client.Client // Map email address -> client
stopChan chan struct{}
manualCheck chan bool manualCheck chan bool
mu sync.Mutex
}
func NewEmailChannel(cfg config.EmailConfig, bus *bus.MessageBus) *EmailChannel {
// If Accounts is empty but single fields are set, populate Accounts with one entry
if len(cfg.Accounts) == 0 && cfg.IMAPServer != "" {
cfg.Accounts = []config.EmailAccountConfig{{
Email: cfg.IMAPUser, // Default to IMAP user as email
IMAPServer: cfg.IMAPServer,
IMAPPort: cfg.IMAPPort,
IMAPUser: cfg.IMAPUser,
IMAPPassword: cfg.IMAPPassword,
SMTPServer: cfg.SMTPServer,
SMTPPort: cfg.SMTPPort,
SMTPUser: cfg.SMTPUser,
SMTPPassword: cfg.SMTPPassword,
}}
} }
func NewEmailChannel(cfg config.EmailConfig, bus *bus.MessageBus) (*EmailChannel, error) {
base := NewBaseChannel("email", cfg, bus, cfg.AllowFrom) base := NewBaseChannel("email", cfg, bus, cfg.AllowFrom)
return &EmailChannel{ return &EmailChannel{
BaseChannel: base, BaseChannel: base,
config: cfg, config: cfg,
bus: bus,
imapClients: make(map[string]*client.Client),
stopChan: make(chan struct{}),
manualCheck: make(chan bool, 1), manualCheck: make(chan bool, 1),
}, nil }
} }
func (c *EmailChannel) Start(ctx context.Context) error { func (c *EmailChannel) Start(ctx context.Context) error {
logger.InfoC("email", "Starting Email channel polling...") logger.InfoC("email", "Starting Email channel polling...")
// Initial connection
c.connectAllIMAP()
c.setRunning(true) c.setRunning(true)
go c.pollLoop(ctx) go c.pollLoop(ctx)
return nil return nil
} }
func (c *EmailChannel) connectAllIMAP() {
c.mu.Lock()
defer c.mu.Unlock()
for _, acc := range c.config.Accounts {
if acc.Email == "" {
continue // Skip invalid config
}
// Skip if already connected
if client, ok := c.imapClients[acc.Email]; ok && client.State() == imap.AuthenticatedState {
continue
}
logger.DebugCF("email", "Connecting to IMAP", map[string]interface{}{"email": acc.Email, "server": acc.IMAPServer})
client, err := c.connectIMAPAccount(acc)
if err != nil {
logger.ErrorCF("email", "Failed to connect to IMAP account", map[string]interface{}{
"email": acc.Email,
"error": err.Error(),
})
continue
}
c.imapClients[acc.Email] = client
logger.InfoCF("email", "Connected to IMAP account", map[string]interface{}{"email": acc.Email})
}
}
func (c *EmailChannel) connectIMAPAccount(acc config.EmailAccountConfig) (*client.Client, error) {
addr := fmt.Sprintf("%s:%d", acc.IMAPServer, acc.IMAPPort)
var cClient *client.Client
var err error
if acc.IMAPPort == 993 {
cClient, err = client.DialTLS(addr, nil)
} else {
cClient, err = client.Dial(addr)
}
if err != nil {
return nil, err
}
if err := cClient.Login(acc.IMAPUser, acc.IMAPPassword); err != nil {
cClient.Logout()
return nil, err
}
return cClient, nil
}
func (c *EmailChannel) Stop(ctx context.Context) error { func (c *EmailChannel) Stop(ctx context.Context) error {
logger.InfoC("email", "Stopping Email channel...") logger.InfoC("email", "Stopping Email channel...")
c.setRunning(false) c.setRunning(false)
if c.imapClient != nil { close(c.stopChan)
c.imapClient.Logout()
c.mu.Lock()
defer c.mu.Unlock()
for _, client := range c.imapClients {
client.Logout()
} }
return nil return nil
} }
@ -56,205 +135,23 @@ func (c *EmailChannel) pollLoop(ctx context.Context) {
ticker := time.NewTicker(time.Duration(c.config.PollInterval) * time.Second) ticker := time.NewTicker(time.Duration(c.config.PollInterval) * time.Second)
defer ticker.Stop() defer ticker.Stop()
// Initial poll // Initial check
c.checkMail() c.checkAllMail()
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return return
case <-c.stopChan:
return
case <-ticker.C: case <-ticker.C:
c.checkMail() c.checkAllMail()
case <-c.manualCheck: case <-c.manualCheck:
c.checkMail() c.checkAllMail()
} }
} }
} }
func (c *EmailChannel) checkMail() {
// User feedback for manual check visibility
fmt.Println("📧 Checking for new emails...")
// Reconnect if needed
if c.imapClient == nil || c.imapClient.State() == imap.LogoutState {
fmt.Printf("📧 Connecting to IMAP server %s:%d...\n", c.config.IMAPServer, c.config.IMAPPort)
if err := c.connectIMAP(); err != nil {
logger.ErrorCF("email", "Failed to connect to IMAP", map[string]interface{}{"error": err.Error()})
fmt.Printf("❌ Failed to connect to IMAP: %v\n", err)
return
}
fmt.Println("✅ Connected to IMAP")
}
// Select INBOX
mbox, err := c.imapClient.Select("INBOX", false)
if err != nil {
logger.ErrorCF("email", "Failed to select INBOX", map[string]interface{}{"error": err.Error()})
fmt.Printf("❌ Failed to select INBOX: %v\n", err)
// Force reconnect next time
c.imapClient.Logout()
c.imapClient = nil
return
}
if mbox.Messages == 0 {
fmt.Println("📭 Inbox is empty.")
return
}
// Search for unread messages
criteria := imap.NewSearchCriteria()
criteria.WithoutFlags = []string{imap.SeenFlag}
uids, err := c.imapClient.Search(criteria)
if err != nil {
logger.ErrorCF("email", "Failed to search emails", map[string]interface{}{"error": err.Error()})
fmt.Printf("❌ Failed to search emails: %v\n", err)
return
}
if len(uids) == 0 {
fmt.Println("📭 No new unread emails.")
return
}
// Limit to last 10 emails to avoid overwhelming the system
const maxEmails = 10
if len(uids) > maxEmails {
fmt.Printf("⚠️ Too many emails (%d). Fetching last %d only.\n", len(uids), maxEmails)
uids = uids[len(uids)-maxEmails:]
}
fmt.Printf("📧 Fetching %d new emails...\n", len(uids))
seqset := new(imap.SeqSet)
seqset.AddNum(uids...)
section := &imap.BodySectionName{}
items := []imap.FetchItem{section.FetchItem(), imap.FetchEnvelope, imap.FetchFlags, imap.FetchUid}
messages := make(chan *imap.Message)
done := make(chan error, 1)
go func() {
done <- c.imapClient.Fetch(seqset, items, messages)
}()
for msg := range messages {
c.processMessage(msg, section)
// Mark as seen
item := imap.FormatFlagsOp(imap.AddFlags, true)
flags := []interface{}{imap.SeenFlag}
seq := new(imap.SeqSet)
seq.AddNum(msg.Uid)
if err := c.imapClient.Store(seq, item, flags, nil); err != nil {
logger.ErrorCF("email", "Failed to mark email as seen", map[string]interface{}{"uid": msg.Uid})
}
}
if err := <-done; err != nil {
logger.ErrorCF("email", "Fetch failed", map[string]interface{}{"error": err.Error()})
}
}
func (c *EmailChannel) connectIMAP() error {
addr := fmt.Sprintf("%s:%d", c.config.IMAPServer, c.config.IMAPPort)
logger.DebugCF("email", "Connecting to IMAP", map[string]interface{}{"addr": addr})
var err error
if c.config.IMAPPort == 993 {
c.imapClient, err = client.DialTLS(addr, nil)
} else {
c.imapClient, err = client.Dial(addr)
}
if err != nil {
return err
}
if err := c.imapClient.Login(c.config.IMAPUser, c.config.IMAPPassword); err != nil {
return err
}
return nil
}
func (c *EmailChannel) processMessage(msg *imap.Message, section *imap.BodySectionName) {
if msg == nil {
return
}
sender := "unknown"
if len(msg.Envelope.From) > 0 {
sender = fmt.Sprintf("%s@%s", msg.Envelope.From[0].MailboxName, msg.Envelope.From[0].HostName)
}
subject := msg.Envelope.Subject
// Check allowlist
if !c.IsAllowed(sender) {
logger.DebugCF("email", "Message rejected by allowlist", map[string]interface{}{"sender": sender})
return
}
r := msg.GetBody(section)
if r == nil {
logger.WarnC("email", "Message has no body")
return
}
// Create a new mail reader
mr, err := mail.CreateReader(r)
if err != nil {
logger.ErrorCF("email", "Failed to create mail reader", map[string]interface{}{"error": err.Error()})
return
}
body := ""
// Read each part
for {
p, err := mr.NextPart()
if err == io.EOF {
break
} else if err != nil {
logger.ErrorCF("email", "Failed to read email part", map[string]interface{}{"error": err.Error()})
break
}
switch h := p.Header.(type) {
case *mail.InlineHeader:
// checks explicitly for text/plain
contentType, _, _ := h.ContentType()
if contentType == "text/plain" {
b, _ := io.ReadAll(p.Body)
body += string(b)
}
case *mail.AttachmentHeader:
// Handle attachments if needed
}
}
if body == "" {
// Try to read simple body if multipart failed or wasn't multipart
// Reset reader if possible or handle non-multipart - simplified for this implementation
// For the MVP, we assume most emails have a text/plain part.
body = "[Content could not be parsed or was empty]"
}
content := fmt.Sprintf("Subject: %s\n\n%s", subject, body)
// Sender is the "User ID", ChatID is also the sender email for direct replies
chatID := sender // In email, the chat ID is effectively the sender's address
c.HandleMessage(sender, chatID, content, nil, map[string]string{
"subject": subject,
"email": sender,
})
fmt.Printf("✅ Processed email from %s: %s\n", sender, subject)
}
// HandleCustomCommand handles internal commands
// HandleCustomCommand handles internal commands
func (c *EmailChannel) CheckNow() { func (c *EmailChannel) CheckNow() {
select { select {
case c.manualCheck <- true: case c.manualCheck <- true:
@ -264,42 +161,235 @@ func (c *EmailChannel) CheckNow() {
} }
} }
func (c *EmailChannel) checkAllMail() {
fmt.Println("📧 Checking for new emails on all accounts...")
// Ensure connections
c.connectAllIMAP()
c.mu.Lock()
defer c.mu.Unlock()
for _, acc := range c.config.Accounts {
client, ok := c.imapClients[acc.Email]
if !ok || client.State() == imap.LogoutState {
continue
}
c.checkAccountMail(client, acc)
}
}
func (c *EmailChannel) checkAccountMail(imapClient *client.Client, acc config.EmailAccountConfig) {
// Select INBOX
mbox, err := imapClient.Select("INBOX", false)
if err != nil {
logger.ErrorCF("email", "Failed to select INBOX", map[string]interface{}{"email": acc.Email, "error": err.Error()})
// Invalidate connection
imapClient.Logout()
return
}
if mbox.Messages == 0 {
return
}
// Search for unread messages
criteria := imap.NewSearchCriteria()
criteria.WithoutFlags = []string{imap.SeenFlag}
uids, err := imapClient.Search(criteria)
if err != nil {
logger.ErrorCF("email", "Failed to search emails", map[string]interface{}{"email": acc.Email, "error": err.Error()})
return
}
if len(uids) == 0 {
return
}
// Limit to last 10 emails
const maxEmails = 10
if len(uids) > maxEmails {
// Take the newest ones (highest UIDs)
uids = uids[len(uids)-maxEmails:]
}
fmt.Printf("📧 [%s] Found %d unread emails, fetching details...\n", acc.Email, len(uids))
seqset := new(imap.SeqSet)
seqset.AddNum(uids...)
// Fetch envelope and body structure to check dates first?
// Actually just fetch everything for the small batch.
section := &imap.BodySectionName{}
items := []imap.FetchItem{section.FetchItem(), imap.FetchEnvelope, imap.FetchFlags, imap.FetchUid}
messages := make(chan *imap.Message)
done := make(chan error, 1)
go func() {
done <- imapClient.Fetch(seqset, items, messages)
}()
for msg := range messages {
// Time filter: Ignore emails older than 1 hour to prevent backlog flood on restart
if msg.Envelope != nil && time.Since(msg.Envelope.Date) > 1*time.Hour {
logger.DebugCF("email", "Skipping old unread email", map[string]interface{}{
"subject": msg.Envelope.Subject,
"date": msg.Envelope.Date,
})
continue
}
c.processMessage(msg, section, acc.Email) // Pass account email to know recipient context
// Mark as seen
item := imap.FormatFlagsOp(imap.AddFlags, true)
flags := []interface{}{imap.SeenFlag}
seq := new(imap.SeqSet)
seq.AddNum(msg.Uid)
if err := imapClient.Store(seq, item, flags, nil); err != nil {
logger.ErrorCF("email", "Failed to mark email as seen", map[string]interface{}{"uid": msg.Uid})
}
}
if err := <-done; err != nil {
logger.ErrorCF("email", "Fetch failed", map[string]interface{}{"error": err.Error()})
}
}
func (c *EmailChannel) processMessage(msg *imap.Message, section *imap.BodySectionName, accountEmail string) {
if msg == nil || msg.Envelope == nil {
return
}
subject := msg.Envelope.Subject
from := msg.Envelope.From
if len(from) == 0 {
return
}
sender := fmt.Sprintf("%s@%s", from[0].MailboxName, from[0].HostName)
// Check allowlist
if !c.IsAllowed(sender) {
logger.DebugCF("email", "Ignoring email from non-allowed sender", map[string]interface{}{"sender": sender})
return
}
// Get body
r := msg.GetBody(section)
if r == nil {
return
}
mr, err := mail.CreateReader(r)
if err != nil {
logger.ErrorCF("email", "Failed to create mail reader", map[string]interface{}{"error": err.Error()})
return
}
var body string
// Simple body extraction
for {
p, err := mr.NextPart()
if err == io.EOF {
break
} else if err != nil {
break
}
switch h := p.Header.(type) {
case *mail.InlineHeader:
contentType, _, _ := h.ContentType()
if contentType == "text/plain" {
b, _ := io.ReadAll(p.Body)
body = string(b)
} else if contentType == "text/html" && body == "" {
// Fallback to HTML if no text/plain yet, ideally strip tags
b, _ := io.ReadAll(p.Body)
body = string(b) // TODO: Strip HTML
}
}
}
// ChatID logic:
// For now, we treat the sender as the ChatID.
// To distinguish which account received it, we could prepend/append, but let's keep it simple.
// The User ID is the sender.
chatID := sender
// Forwarding logic
if c.config.ForwardTo != "" {
parts := strings.SplitN(c.config.ForwardTo, ":", 2)
if len(parts) == 2 {
channel, targetChatID := parts[0], parts[1]
forwardContent := fmt.Sprintf("📧 **New Email [%s]**\n**From:** %s\n**Subject:** %s\n\n%s", accountEmail, sender, subject, body)
// Truncate body
if len(body) > 500 {
forwardContent = fmt.Sprintf("📧 **New Email [%s]**\n**From:** %s\n**Subject:** %s\n\n%s...", accountEmail, sender, subject, body[:500])
}
c.bus.PublishOutbound(bus.OutboundMessage{
Channel: channel,
ChatID: targetChatID,
Content: forwardContent,
})
fmt.Printf("➡️ Forwarded email to %s:%s\n", channel, targetChatID)
}
}
c.HandleMessage(sender, chatID, contentWithContext(accountEmail, subject, body), nil, map[string]string{
"subject": subject,
"email": sender,
"to": accountEmail,
})
fmt.Printf("✅ Processed email from %s to %s: %s\n", sender, accountEmail, subject)
}
func contentWithContext(account, subject, body string) string {
return fmt.Sprintf("[Received at %s]\nSubject: %s\n\n%s", account, subject, body)
}
func (c *EmailChannel) Send(ctx context.Context, msg bus.OutboundMessage) error { func (c *EmailChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
logger.DebugCF("email", "Send received", map[string]interface{}{"content": msg.Content, "chat_id": msg.ChatID}) logger.DebugCF("email", "Send received", map[string]interface{}{"content": msg.Content, "chat_id": msg.ChatID})
// Check for command (handle potential whitespace) // Check for command
if strings.TrimSpace(msg.Content) == "CMD:CHECK" { if strings.TrimSpace(msg.Content) == "CMD:CHECK" {
c.CheckNow() c.CheckNow()
return nil return nil
} }
addr := fmt.Sprintf("%s:%d", c.config.SMTPServer, c.config.SMTPPort) // For simple replies, we don't know which account to send FROM unless we track state or infer.
auth := smtp.PlainAuth("", c.config.SMTPUser, c.config.SMTPPassword, c.config.SMTPServer) // We'll Default to the first account, or try to find one.
// Ideally, the tool `send_email` should be used which calls this.
// If this is a direct reply from Agent, it might lack context.
// However, we can use the first account as default.
account := c.config.Accounts[0]
// If msg.Metadata has "from", use it
// But OutboundMessage doesn't have arbitrary metadata map on struct usually?
// It does NOT.
// So we use default account for general replies.
addr := fmt.Sprintf("%s:%d", account.SMTPServer, account.SMTPPort)
auth := smtp.PlainAuth("", account.SMTPUser, account.SMTPPassword, account.SMTPServer)
to := []string{msg.ChatID} to := []string{msg.ChatID}
// Simple email construction
subject := "Re: PicoClaw Response" subject := "Re: PicoClaw Response"
// If we preserved the original subject in session metadata, we could use "Re: " + original_subject body := fmt.Sprintf("To: %s\r\nSubject: %s\r\n\r\n%s\r\n", msg.ChatID, subject, msg.Content)
body := fmt.Sprintf("To: %s\r\n"+ if account.SMTPPort == 465 {
"Subject: %s\r\n"+ tlsConfig := &tls.Config{ServerName: account.SMTPServer}
"\r\n"+
"%s\r\n", msg.ChatID, subject, msg.Content)
// Handle TLS for port 465 (SMTPS) vs 587 (STARTTLS)
if c.config.SMTPPort == 465 {
// Direct TLS
tlsConfig := &tls.Config{
ServerName: c.config.SMTPServer,
}
conn, err := tls.Dial("tcp", addr, tlsConfig) conn, err := tls.Dial("tcp", addr, tlsConfig)
if err != nil { if err != nil {
return err return err
} }
defer conn.Close() defer conn.Close()
client, err := smtp.NewClient(conn, c.config.SMTPServer) client, err := smtp.NewClient(conn, account.SMTPServer)
if err != nil { if err != nil {
return err return err
} }
@ -308,7 +398,7 @@ func (c *EmailChannel) Send(ctx context.Context, msg bus.OutboundMessage) error
if err = client.Auth(auth); err != nil { if err = client.Auth(auth); err != nil {
return err return err
} }
if err = client.Mail(c.config.SMTPUser); err != nil { if err = client.Mail(account.SMTPUser); err != nil {
return err return err
} }
if err = client.Rcpt(to[0]); err != nil { if err = client.Rcpt(to[0]); err != nil {
@ -324,7 +414,6 @@ func (c *EmailChannel) Send(ctx context.Context, msg bus.OutboundMessage) error
} }
return w.Close() return w.Close()
} else { } else {
// STARTTLS (Standard for 587) return smtp.SendMail(addr, auth, account.SMTPUser, to, []byte(body))
return smtp.SendMail(addr, auth, c.config.SMTPUser, to, []byte(body))
} }
} }

View file

@ -178,11 +178,9 @@ func (m *Manager) initChannels() error {
if m.config.Channels.Email.Enabled { if m.config.Channels.Email.Enabled {
logger.DebugC("channels", "Attempting to initialize Email channel") logger.DebugC("channels", "Attempting to initialize Email channel")
email, err := NewEmailChannel(m.config.Channels.Email, m.bus) email := NewEmailChannel(m.config.Channels.Email, m.bus)
if err != nil { if email == nil {
logger.ErrorCF("channels", "Failed to initialize Email channel", map[string]interface{}{ logger.ErrorC("channels", "Failed to initialize Email channel")
"error": err.Error(),
})
} else { } else {
m.channels["email"] = email m.channels["email"] = email
logger.InfoC("channels", "Email channel enabled successfully") logger.InfoC("channels", "Email channel enabled successfully")

View file

@ -157,8 +157,21 @@ type OneBotConfig struct {
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_ONEBOT_ALLOW_FROM"` AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_ONEBOT_ALLOW_FROM"`
} }
type EmailAccountConfig struct {
Email string `json:"email"`
IMAPServer string `json:"imap_server"`
IMAPPort int `json:"imap_port"`
IMAPUser string `json:"imap_user"`
IMAPPassword string `json:"imap_password"`
SMTPServer string `json:"smtp_server"`
SMTPPort int `json:"smtp_port"`
SMTPUser string `json:"smtp_user"`
SMTPPassword string `json:"smtp_password"`
}
type EmailConfig struct { type EmailConfig struct {
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_EMAIL_ENABLED"` Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_EMAIL_ENABLED"`
Accounts []EmailAccountConfig `json:"accounts"`
IMAPServer string `json:"imap_server" env:"PICOCLAW_CHANNELS_EMAIL_IMAP_SERVER"` IMAPServer string `json:"imap_server" env:"PICOCLAW_CHANNELS_EMAIL_IMAP_SERVER"`
IMAPPort int `json:"imap_port" env:"PICOCLAW_CHANNELS_EMAIL_IMAP_PORT"` IMAPPort int `json:"imap_port" env:"PICOCLAW_CHANNELS_EMAIL_IMAP_PORT"`
IMAPUser string `json:"imap_user" env:"PICOCLAW_CHANNELS_EMAIL_IMAP_USER"` IMAPUser string `json:"imap_user" env:"PICOCLAW_CHANNELS_EMAIL_IMAP_USER"`
@ -168,6 +181,7 @@ type EmailConfig struct {
SMTPUser string `json:"smtp_user" env:"PICOCLAW_CHANNELS_EMAIL_SMTP_USER"` SMTPUser string `json:"smtp_user" env:"PICOCLAW_CHANNELS_EMAIL_SMTP_USER"`
SMTPPassword string `json:"smtp_password" env:"PICOCLAW_CHANNELS_EMAIL_SMTP_PASSWORD"` SMTPPassword string `json:"smtp_password" env:"PICOCLAW_CHANNELS_EMAIL_SMTP_PASSWORD"`
PollInterval int `json:"poll_interval" env:"PICOCLAW_CHANNELS_EMAIL_POLL_INTERVAL"` // seconds PollInterval int `json:"poll_interval" env:"PICOCLAW_CHANNELS_EMAIL_POLL_INTERVAL"` // seconds
ForwardTo string `json:"forward_to" env:"PICOCLAW_CHANNELS_EMAIL_FORWARD_TO"` // e.g. "telegram:123456789"
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_EMAIL_ALLOW_FROM"` AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_EMAIL_ALLOW_FROM"`
} }
@ -228,6 +242,19 @@ type WebToolsConfig struct {
type ToolsConfig struct { type ToolsConfig struct {
Web WebToolsConfig `json:"web"` Web WebToolsConfig `json:"web"`
GitHub GitHubConfig `json:"github"`
Calendar CalendarConfig `json:"calendar"`
}
type CalendarConfig struct {
Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_CALENDAR_ENABLED"`
CredentialsJSON string `json:"credentials_json" env:"PICOCLAW_TOOLS_CALENDAR_CREDENTIALS"` // Path to credentials.json
CalendarID string `json:"calendar_id" env:"PICOCLAW_TOOLS_CALENDAR_ID"` // Target calendar ID (usually user's email)
}
type GitHubConfig struct {
Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_GITHUB_ENABLED"`
Token string `json:"token" env:"PICOCLAW_TOOLS_GITHUB_TOKEN"`
} }
func DefaultConfig() *Config { func DefaultConfig() *Config {

247
pkg/tools/calendar.go Normal file
View file

@ -0,0 +1,247 @@
package tools
import (
"context"
"fmt"
"os"
"strings"
"time"
"golang.org/x/oauth2/google"
"google.golang.org/api/calendar/v3"
"google.golang.org/api/option"
"github.com/sipeed/picoclaw/pkg/config"
)
type CalendarTool struct {
config config.CalendarConfig
service *calendar.Service
}
func NewCalendarTool(cfg config.CalendarConfig) *CalendarTool {
return &CalendarTool{
config: cfg,
}
}
func (t *CalendarTool) Name() string {
return "calendar"
}
func (t *CalendarTool) Description() string {
return "Manage Google Calendar events. Can list upcoming events and add new events."
}
func (t *CalendarTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"action": map[string]interface{}{
"type": "string",
"description": "Action to perform: list_events, add_event",
"enum": []string{"list_events", "add_event"},
},
"count": map[string]interface{}{
"type": "integer",
"description": "Number of events to list (default: 10). For list_events.",
},
"summary": map[string]interface{}{
"type": "string",
"description": "Event title/summary. For add_event.",
},
"description": map[string]interface{}{
"type": "string",
"description": "Event description. For add_event.",
},
"location": map[string]interface{}{
"type": "string",
"description": "Event location. For add_event.",
},
"start_time": map[string]interface{}{
"type": "string",
"description": "Start time in RFC3339 format (e.g. 2023-10-27T10:00:00Z). For add_event.",
},
"end_time": map[string]interface{}{
"type": "string",
"description": "End time in RFC3339 format. For add_event. If omitted, defaults to 1 hour after start.",
},
},
"required": []string{"action"},
}
}
func (t *CalendarTool) getService(ctx context.Context) (*calendar.Service, error) {
if t.service != nil {
return t.service, nil
}
if t.config.CredentialsJSON == "" {
return nil, fmt.Errorf("calendar credentials_json not configured")
}
// Expand home directory if needed
credPath := t.config.CredentialsJSON
if strings.HasPrefix(credPath, "~/") {
home, _ := os.UserHomeDir()
credPath = filepathJoin(home, credPath[2:])
}
b, err := os.ReadFile(credPath)
if err != nil {
return nil, fmt.Errorf("unable to read client secret file: %v", err)
}
// If using Service Account
// conf, err := google.JWTConfigFromJSON(b, calendar.CalendarScope)
// If using OAuth2 Client ID (more common for personal calendars)
// We need a token. For a CLI tool, we might need a stored token.
// Implementing robust OAuth flow in a tool is hard.
// Let's assume Service Account for now as it's easier for server-side,
// BUT Service Accounts can't access personal Gmail calendars without Domain-Wide Delegation (Workspace only).
// For personal Gmail, we need OAuth2 User Credentials.
// Strategy: Use "Application Default Credentials" or specific OAuth token if provided.
// Simplest for personal: User provides `token.json` generated elsewhere, or we use a Service Account shared with the personal email?
// Sharing personal calendar with Service Account email is the easiest way!
// 1. User creates Service Account.
// 2. User shares their calendar with Service Account email.
// 3. Tool uses Service Account credentials.
config, err := google.JWTConfigFromJSON(b, calendar.CalendarScope)
if err != nil {
// Try standard credentials (could be OAuth client secret)
// But for now let's stick to Service Account as primary recommendation for headless agents.
return nil, fmt.Errorf("unable to parse service account key file: %v. Please ensure you are using a Service Account key.", err)
}
client := config.Client(ctx)
srv, err := calendar.NewService(ctx, option.WithHTTPClient(client))
if err != nil {
return nil, fmt.Errorf("unable to retrieve Calendar client: %v", err)
}
t.service = srv
return srv, nil
}
func (t *CalendarTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
if !t.config.Enabled {
return &ToolResult{Err: fmt.Errorf("calendar tool is disabled in config")}
}
action, _ := args["action"].(string)
srv, err := t.getService(ctx)
if err != nil {
return &ToolResult{Err: err}
}
switch action {
case "list_events":
return t.listEvents(srv, args)
case "add_event":
return t.addEvent(srv, args)
default:
return &ToolResult{Err: fmt.Errorf("unknown action: %s", action)}
}
}
func (t *CalendarTool) listEvents(srv *calendar.Service, args map[string]interface{}) *ToolResult {
count := 10
if c, ok := args["count"].(float64); ok {
count = int(c)
}
tMin := time.Now().Format(time.RFC3339)
calendarId := "primary"
if t.config.CalendarID != "" {
calendarId = t.config.CalendarID
}
events, err := srv.Events.List(calendarId).ShowDeleted(false).
SingleEvents(true).TimeMin(tMin).MaxResults(int64(count)).OrderBy("startTime").Do()
if err != nil {
return &ToolResult{Err: fmt.Errorf("unable to retrieve next ten of the user's upcoming events: %v", err)}
}
if len(events.Items) == 0 {
return &ToolResult{ForLLM: "No upcoming events found."}
}
var sb strings.Builder
sb.WriteString("Upcoming events:\n")
for _, item := range events.Items {
date := item.Start.DateTime
if date == "" {
date = item.Start.Date
}
sb.WriteString(fmt.Sprintf("- %s (%s)\n", item.Summary, date))
}
return &ToolResult{ForLLM: sb.String(), ForUser: sb.String()}
}
func (t *CalendarTool) addEvent(srv *calendar.Service, args map[string]interface{}) *ToolResult {
summary, _ := args["summary"].(string)
description, _ := args["description"].(string)
location, _ := args["location"].(string)
startTimeStr, _ := args["start_time"].(string)
endTimeStr, _ := args["end_time"].(string)
if summary == "" || startTimeStr == "" {
return &ToolResult{Err: fmt.Errorf("summary and start_time are required")}
}
// Parse start time
// Try RFC3339 first
// If failed, maybe try other formats? LLM usually gives ISO/RFC.
event := &calendar.Event{
Summary: summary,
Location: location,
Description: description,
Start: &calendar.EventDateTime{
DateTime: startTimeStr,
TimeZone: "UTC", // Should ideally be user's timezone or inferred
},
End: &calendar.EventDateTime{
DateTime: endTimeStr,
TimeZone: "UTC",
},
}
if endTimeStr == "" {
// Default to 1 hour later
t, err := time.Parse(time.RFC3339, startTimeStr)
if err == nil {
event.End.DateTime = t.Add(1 * time.Hour).Format(time.RFC3339)
} else {
return &ToolResult{Err: fmt.Errorf("invalid start_time format, expected RFC3339: %v", err)}
}
}
calendarId := "primary"
if t.config.CalendarID != "" {
calendarId = t.config.CalendarID
}
event, err := srv.Events.Insert(calendarId, event).Do()
if err != nil {
return &ToolResult{Err: fmt.Errorf("unable to create event: %v", err)}
}
msg := fmt.Sprintf("Event created: %s (%s)", event.HtmlLink, event.Id)
return &ToolResult{ForLLM: msg, ForUser: fmt.Sprintf("✅ Created event '%s' at %s", summary, startTimeStr)}
}
// Helper to avoid circular dependency or just duplicating logic
func filepathJoin(elem ...string) string {
for i, e := range elem {
if e != "" {
return strings.Join(elem[i:], string(os.PathSeparator))
}
}
return ""
}

View file

@ -2,17 +2,18 @@ package tools
import ( import (
"context" "context"
"strings"
"github.com/sipeed/picoclaw/pkg/bus" "github.com/sipeed/picoclaw/pkg/config"
) )
type CheckMailTool struct { type CheckMailTool struct {
bus *bus.MessageBus config config.EmailConfig
} }
func NewCheckMailTool(bus *bus.MessageBus) *CheckMailTool { func NewCheckMailTool(cfg config.EmailConfig) *CheckMailTool {
return &CheckMailTool{ return &CheckMailTool{
bus: bus, config: cfg,
} }
} }
@ -21,27 +22,45 @@ func (t *CheckMailTool) Name() string {
} }
func (t *CheckMailTool) Description() string { func (t *CheckMailTool) Description() string {
return "Manually check for new emails immediately. Use this when the user asks to check email." return "Check for new emails and return them. This tool fetches recent unread emails from all accounts."
} }
func (t *CheckMailTool) Parameters() map[string]interface{} { func (t *CheckMailTool) Parameters() map[string]interface{} {
return map[string]interface{}{ return map[string]interface{}{
"type": "object", "type": "object",
"properties": map[string]interface{}{}, "properties": map[string]interface{}{
"required": []string{}, "count": map[string]interface{}{
"type": "integer",
"description": "Number of emails to fetch (default: 5)",
},
},
} }
} }
func (t *CheckMailTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult { func (t *CheckMailTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
// Publish a command to the email channel via outbound bus // Delegate to ReadEmailTool
t.bus.PublishOutbound(bus.OutboundMessage{ readTool := NewReadEmailTool(t.config)
Channel: "email",
ChatID: "system", // Dummy ID to prevent empty recipient error if it falls through
Content: "CMD:CHECK",
})
return &ToolResult{ // Default to unread only for "check"
ForLLM: "Initiated manual check for new emails. Any new emails will appear as messages shortly.", readArgs := map[string]interface{}{
ForUser: "Checking for new emails...", "unread_only": true,
"count": 5.0,
} }
if c, ok := args["count"]; ok {
readArgs["count"] = c
}
result := readTool.Execute(ctx, readArgs)
// If no unread, try fetching latest 3 just to show something (optional, but "check" usually implies new)
if result.ForLLM == "" || strings.Contains(result.ForLLM, "No unread emails") {
// Fallback to fetch recent 3 if no unread, to confirm connection?
// No, user said "no mails came through", implies they expected new ones or just wanted to see *something*.
// But "check mail" usually means "sync and show new".
// Let's stick to unread.
return result
}
return result
} }

View file

@ -29,7 +29,7 @@ func (t *ReadEmailTool) Name() string {
} }
func (t *ReadEmailTool) Description() string { func (t *ReadEmailTool) Description() string {
return "Fetch and read recent emails. Returns the sender, subject, and body of the last N emails." return "Fetch and read recent emails from the inbox. Use this when the user asks to 'check mail', 'read email', or sees a notification. Returns the actual email content."
} }
func (t *ReadEmailTool) Parameters() map[string]interface{} { func (t *ReadEmailTool) Parameters() map[string]interface{} {
@ -44,6 +44,10 @@ func (t *ReadEmailTool) Parameters() map[string]interface{} {
"type": "boolean", "type": "boolean",
"description": "If true, only fetch unread emails (default: false)", "description": "If true, only fetch unread emails (default: false)",
}, },
"account": map[string]interface{}{
"type": "string",
"description": "Optional: Specific email account to check (e.g. 'user@gmail.com'). If omitted, checks all.",
},
}, },
} }
} }
@ -66,72 +70,58 @@ func (t *ReadEmailTool) Execute(ctx context.Context, args map[string]interface{}
unreadOnly = u unreadOnly = u
} }
// Connect to IMAP targetAccount, _ := args["account"].(string)
addr := fmt.Sprintf("%s:%d", t.config.IMAPServer, t.config.IMAPPort)
var sb strings.Builder
totalFound := 0
// Helper function to check one account
checkAccount := func(acc config.EmailAccountConfig) error {
addr := fmt.Sprintf("%s:%d", acc.IMAPServer, acc.IMAPPort)
var c *client.Client var c *client.Client
var err error var err error
if t.config.IMAPPort == 993 { if acc.IMAPPort == 993 {
c, err = client.DialTLS(addr, nil) c, err = client.DialTLS(addr, nil)
} else { } else {
c, err = client.Dial(addr) c, err = client.Dial(addr)
} }
if err != nil { if err != nil {
return ErrorResult(fmt.Sprintf("Failed to connect to IMAP: %v", err)) return fmt.Errorf("failed to connect to %s: %v", acc.Email, err)
} }
defer c.Logout() defer c.Logout()
if err := c.Login(t.config.IMAPUser, t.config.IMAPPassword); err != nil { if err := c.Login(acc.IMAPUser, acc.IMAPPassword); err != nil {
return ErrorResult(fmt.Sprintf("Failed to login to IMAP: %v", err)) return fmt.Errorf("failed to login to %s: %v", acc.Email, err)
} }
mbox, err := c.Select("INBOX", false) mbox, err := c.Select("INBOX", false)
if err != nil { if err != nil {
return ErrorResult(fmt.Sprintf("Failed to select INBOX: %v", err)) return fmt.Errorf("failed to select INBOX for %s: %v", acc.Email, err)
} }
if mbox.Messages == 0 { if mbox.Messages == 0 {
return SilentResult("Inbox is empty.") return nil
} }
// Search criteria // Search
criteria := imap.NewSearchCriteria() criteria := imap.NewSearchCriteria()
if unreadOnly { if unreadOnly {
criteria.WithoutFlags = []string{imap.SeenFlag} criteria.WithoutFlags = []string{imap.SeenFlag}
} else {
// Fetch all (limited by range)
// imap.SearchCriteria doesn't have "ALL" by default, empty means all?
// Actually, we can just use sequence numbers if we want "recent"
} }
var uids []uint32 var uids []uint32
if unreadOnly { if unreadOnly {
uids, err = c.Search(criteria) uids, err = c.Search(criteria)
if err != nil { if err != nil {
return ErrorResult(fmt.Sprintf("Failed to search emails: %v", err)) return fmt.Errorf("failed to search %s: %v", acc.Email, err)
} }
} else {
// Just get the last N messages by sequence number
from := uint32(1)
if mbox.Messages > uint32(count) {
from = mbox.Messages - uint32(count) + 1
}
to := mbox.Messages
seqset := new(imap.SeqSet)
seqset.AddRange(from, to)
// We need to fetch UIDs for these sequence numbers or just fetch directly
// Let's fetch directly by SeqNum
// But the processing logic uses UIDs usually. Let's stick to UIDs for consistency if possible,
// but fetching by sequence is easier for "last N".
// Let's use Fetch directly with the seqset.
} }
// Reuse uids logic if unreadOnly, otherwise construct seqset
seqset := new(imap.SeqSet) seqset := new(imap.SeqSet)
if unreadOnly { if unreadOnly {
if len(uids) == 0 { if len(uids) == 0 {
return SilentResult("No unread emails.") return nil
} }
if len(uids) > count { if len(uids) > count {
uids = uids[len(uids)-count:] uids = uids[len(uids)-count:]
@ -156,18 +146,19 @@ func (t *ReadEmailTool) Execute(ctx context.Context, args map[string]interface{}
done <- c.Fetch(seqset, items, messages) done <- c.Fetch(seqset, items, messages)
}() }()
var sb strings.Builder accountData := fmt.Sprintf("\n📧 Account: %s\n", acc.Email)
sb.WriteString(fmt.Sprintf("Found %d emails (showing last %d):\n\n", mbox.Messages, count)) // Approx count hasEmails := false
for msg := range messages { for msg := range messages {
sb.WriteString("---\n") hasEmails = true
accountData += "---\n"
sender := "unknown" sender := "unknown"
if len(msg.Envelope.From) > 0 { if len(msg.Envelope.From) > 0 {
sender = fmt.Sprintf("%s@%s", msg.Envelope.From[0].MailboxName, msg.Envelope.From[0].HostName) sender = fmt.Sprintf("%s@%s", msg.Envelope.From[0].MailboxName, msg.Envelope.From[0].HostName)
} }
sb.WriteString(fmt.Sprintf("From: %s\n", sender)) accountData += fmt.Sprintf("From: %s\n", sender)
sb.WriteString(fmt.Sprintf("Subject: %s\n", msg.Envelope.Subject)) accountData += fmt.Sprintf("Subject: %s\n", msg.Envelope.Subject)
sb.WriteString(fmt.Sprintf("Date: %s\n", msg.Envelope.Date)) accountData += fmt.Sprintf("Date: %s\n", msg.Envelope.Date)
r := msg.GetBody(section) r := msg.GetBody(section)
if r != nil { if r != nil {
@ -185,23 +176,54 @@ func (t *ReadEmailTool) Execute(ctx context.Context, args map[string]interface{}
contentType, _, _ := h.ContentType() contentType, _, _ := h.ContentType()
if contentType == "text/plain" { if contentType == "text/plain" {
b, _ := io.ReadAll(p.Body) b, _ := io.ReadAll(p.Body)
sb.WriteString("\n") accountData += fmt.Sprintf("\n%s\n", string(b))
sb.WriteString(string(b))
} }
} }
} }
} }
} }
sb.WriteString("\n")
} }
if err := <-done; err != nil { if err := <-done; err != nil {
return ErrorResult(fmt.Sprintf("Failed to fetch emails: %v", err)) return fmt.Errorf("fetch failed for %s: %v", acc.Email, err)
}
if hasEmails {
sb.WriteString(accountData)
totalFound++
}
return nil
}
// Iterate over accounts
accountsToCheck := t.config.Accounts
// Fallback to legacy single account if Accounts is empty
if len(accountsToCheck) == 0 && t.config.IMAPServer != "" {
accountsToCheck = []config.EmailAccountConfig{{
Email: t.config.IMAPUser,
IMAPServer: t.config.IMAPServer,
IMAPPort: t.config.IMAPPort,
IMAPUser: t.config.IMAPUser,
IMAPPassword: t.config.IMAPPassword,
}}
}
for _, acc := range accountsToCheck {
if targetAccount != "" && !strings.EqualFold(acc.Email, targetAccount) {
continue
}
if err := checkAccount(acc); err != nil {
sb.WriteString(fmt.Sprintf("\n❌ Error fetching from %s: %v\n", acc.Email, err))
}
}
if sb.Len() == 0 {
return SilentResult("No recent emails found.")
} }
return &ToolResult{ return &ToolResult{
ForLLM: sb.String(), ForLLM: sb.String(),
ForUser: "Read recent emails.", ForUser: fmt.Sprintf("Checked emails for %d accounts.", len(accountsToCheck)),
} }
} }
@ -238,6 +260,10 @@ func (t *SendEmailTool) Parameters() map[string]interface{} {
"type": "string", "type": "string",
"description": "Email body content", "description": "Email body content",
}, },
"from_account": map[string]interface{}{
"type": "string",
"description": "Optional: Email address to send FROM. Must match a configured account.",
},
}, },
"required": []string{"to", "subject", "body"}, "required": []string{"to", "subject", "body"},
} }
@ -251,13 +277,54 @@ func (t *SendEmailTool) Execute(ctx context.Context, args map[string]interface{}
to, _ := args["to"].(string) to, _ := args["to"].(string)
subject, _ := args["subject"].(string) subject, _ := args["subject"].(string)
bodyContent, _ := args["body"].(string) bodyContent, _ := args["body"].(string)
fromAccount, _ := args["from_account"].(string)
if to == "" { if to == "" {
return ErrorResult("Recipient (to) is required.") return ErrorResult("Recipient (to) is required.")
} }
addr := fmt.Sprintf("%s:%d", t.config.SMTPServer, t.config.SMTPPort) // Select account
auth := smtp.PlainAuth("", t.config.SMTPUser, t.config.SMTPPassword, t.config.SMTPServer) var account config.EmailAccountConfig
found := false
// Fallback legacy
if len(t.config.Accounts) == 0 && t.config.SMTPServer != "" {
account = config.EmailAccountConfig{
Email: t.config.IMAPUser, // best guess
SMTPServer: t.config.SMTPServer,
SMTPPort: t.config.SMTPPort,
SMTPUser: t.config.SMTPUser,
SMTPPassword: t.config.SMTPPassword,
}
found = true
} else {
// Try to match requested account
if fromAccount != "" {
for _, acc := range t.config.Accounts {
if strings.EqualFold(acc.Email, fromAccount) {
account = acc
found = true
break
}
}
if !found {
return ErrorResult(fmt.Sprintf("Configured account not found for email: %s", fromAccount))
}
} else {
// Default to first account
if len(t.config.Accounts) > 0 {
account = t.config.Accounts[0]
found = true
}
}
}
if !found {
return ErrorResult("No valid email account configuration found.")
}
addr := fmt.Sprintf("%s:%d", account.SMTPServer, account.SMTPPort)
auth := smtp.PlainAuth("", account.SMTPUser, account.SMTPPassword, account.SMTPServer)
// RFC 822 format // RFC 822 format
msg := fmt.Sprintf("To: %s\r\n"+ msg := fmt.Sprintf("To: %s\r\n"+
@ -265,25 +332,28 @@ func (t *SendEmailTool) Execute(ctx context.Context, args map[string]interface{}
"\r\n"+ "\r\n"+
"%s\r\n", to, subject, bodyContent) "%s\r\n", to, subject, bodyContent)
logger.InfoCF("email", "Sending email via tool", map[string]interface{}{"to": to, "subject": subject}) logger.InfoCF("email", "Sending email via tool", map[string]interface{}{
"to": to,
"from": account.Email,
})
err := t.sendMail(addr, auth, t.config.SMTPUser, []string{to}, []byte(msg)) err := t.sendMail(addr, auth, account.SMTPUser, []string{to}, []byte(msg), account)
if err != nil { if err != nil {
return ErrorResult(fmt.Sprintf("Failed to send email: %v", err)) return ErrorResult(fmt.Sprintf("Failed to send email: %v", err))
} }
return &ToolResult{ return &ToolResult{
ForLLM: fmt.Sprintf("Email sent successfully to %s", to), ForLLM: fmt.Sprintf("Email sent successfully to %s using account %s", to, account.Email),
ForUser: fmt.Sprintf("Sent email to %s", to), ForUser: fmt.Sprintf("Sent email to %s (via %s)", to, account.Email),
} }
} }
func (t *SendEmailTool) sendMail(addr string, a smtp.Auth, from string, to []string, msg []byte) error { func (t *SendEmailTool) sendMail(addr string, a smtp.Auth, from string, to []string, msg []byte, acc config.EmailAccountConfig) error {
// Handle TLS logic similar to EmailChannel // Handle TLS logic
if t.config.SMTPPort == 465 { if acc.SMTPPort == 465 {
// Direct TLS // Direct TLS
tlsConfig := &tls.Config{ tlsConfig := &tls.Config{
ServerName: t.config.SMTPServer, ServerName: acc.SMTPServer,
} }
conn, err := tls.Dial("tcp", addr, tlsConfig) conn, err := tls.Dial("tcp", addr, tlsConfig)
if err != nil { if err != nil {
@ -291,7 +361,7 @@ func (t *SendEmailTool) sendMail(addr string, a smtp.Auth, from string, to []str
} }
defer conn.Close() defer conn.Close()
c, err := smtp.NewClient(conn, t.config.SMTPServer) c, err := smtp.NewClient(conn, acc.SMTPServer)
if err != nil { if err != nil {
return err return err
} }

219
pkg/tools/github.go Normal file
View file

@ -0,0 +1,219 @@
package tools
import (
"context"
"fmt"
"strings"
"github.com/google/go-github/v60/github"
"github.com/sipeed/picoclaw/pkg/config"
)
type GitHubTool struct {
config config.GitHubConfig
client *github.Client
}
func NewGitHubTool(cfg config.GitHubConfig) *GitHubTool {
t := &GitHubTool{
config: cfg,
}
if cfg.Enabled {
if cfg.Token != "" {
t.client = github.NewClient(nil).WithAuthToken(cfg.Token)
} else {
t.client = github.NewClient(nil)
}
}
return t
}
func (t *GitHubTool) Name() string {
return "github"
}
func (t *GitHubTool) Description() string {
return "Interact with GitHub to list issues, pull requests, or read files from repositories."
}
func (t *GitHubTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"action": map[string]interface{}{
"type": "string",
"description": "Action to perform: list_issues, get_pr, read_file, list_repos",
"enum": []string{"list_issues", "get_pr", "read_file", "list_repos"},
},
"owner": map[string]interface{}{
"type": "string",
"description": "Repository owner (user or organization)",
},
"repo": map[string]interface{}{
"type": "string",
"description": "Repository name",
},
"number": map[string]interface{}{
"type": "integer",
"description": "Issue or PR number (required for get_pr)",
},
"path": map[string]interface{}{
"type": "string",
"description": "File path in repository (required for read_file)",
},
"count": map[string]interface{}{
"type": "integer",
"description": "Number of items to list (default: 5, max: 20)",
},
},
"required": []string{"action"},
}
}
func (t *GitHubTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
if !t.config.Enabled {
return ErrorResult("GitHub tool is not enabled in configuration.")
}
if t.client == nil {
return ErrorResult("GitHub client not initialized.")
}
action, _ := args["action"].(string)
owner, _ := args["owner"].(string)
repo, _ := args["repo"].(string)
// Defaults
count := 5
if c, ok := args["count"].(float64); ok {
count = int(c)
}
if count > 20 {
count = 20
}
switch action {
case "list_issues":
if owner == "" || repo == "" {
return ErrorResult("Owner and repo are required for list_issues.")
}
return t.listIssues(ctx, owner, repo, count)
case "get_pr":
if owner == "" || repo == "" {
return ErrorResult("Owner and repo are required for get_pr.")
}
number, ok := args["number"].(float64)
if !ok {
return ErrorResult("Number is required for get_pr.")
}
return t.getPR(ctx, owner, repo, int(number))
case "read_file":
if owner == "" || repo == "" {
return ErrorResult("Owner and repo are required for read_file.")
}
path, _ := args["path"].(string)
if path == "" {
return ErrorResult("Path is required for read_file.")
}
return t.readFile(ctx, owner, repo, path)
case "list_repos":
// If owner is provided, list user's repos, else authenticated user's repos
return t.listRepos(ctx, owner, count)
default:
return ErrorResult(fmt.Sprintf("Unknown action: %s", action))
}
}
func (t *GitHubTool) listIssues(ctx context.Context, owner, repo string, count int) *ToolResult {
opts := &github.IssueListByRepoOptions{
State: "open",
ListOptions: github.ListOptions{PerPage: count},
}
issues, _, err := t.client.Issues.ListByRepo(ctx, owner, repo, opts)
if err != nil {
return ErrorResult(fmt.Sprintf("Failed to list issues: %v", err))
}
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Open issues in %s/%s:\n", owner, repo))
for _, issue := range issues {
sb.WriteString(fmt.Sprintf("- #%d: %s (by %s)\n", issue.GetNumber(), issue.GetTitle(), issue.User.GetLogin()))
}
return &ToolResult{
ForLLM: sb.String(),
ForUser: sb.String(),
}
}
func (t *GitHubTool) getPR(ctx context.Context, owner, repo string, number int) *ToolResult {
pr, _, err := t.client.PullRequests.Get(ctx, owner, repo, number)
if err != nil {
return ErrorResult(fmt.Sprintf("Failed to get PR #%d: %v", number, err))
}
var sb strings.Builder
sb.WriteString(fmt.Sprintf("PR #%d: %s\n", pr.GetNumber(), pr.GetTitle()))
sb.WriteString(fmt.Sprintf("State: %s\n", pr.GetState()))
sb.WriteString(fmt.Sprintf("User: %s\n", pr.User.GetLogin()))
if pr.Body != nil {
sb.WriteString(fmt.Sprintf("\nBody:\n%s\n", *pr.Body))
}
return &ToolResult{
ForLLM: sb.String(),
ForUser: sb.String(),
}
}
func (t *GitHubTool) readFile(ctx context.Context, owner, repo, path string) *ToolResult {
content, _, _, err := t.client.Repositories.GetContents(ctx, owner, repo, path, nil)
if err != nil {
return ErrorResult(fmt.Sprintf("Failed to read file: %v", err))
}
decoded, err := content.GetContent()
if err != nil {
return ErrorResult(fmt.Sprintf("Failed to decode file content: %v", err))
}
return &ToolResult{
ForLLM: decoded,
ForUser: fmt.Sprintf("Read file %s from %s/%s", path, owner, repo),
}
}
func (t *GitHubTool) listRepos(ctx context.Context, user string, count int) *ToolResult {
opts := &github.RepositoryListOptions{
ListOptions: github.ListOptions{PerPage: count},
Sort: "updated",
}
var repos []*github.Repository
var err error
if user != "" {
repos, _, err = t.client.Repositories.List(ctx, user, opts)
} else {
// Authenticated user
repos, _, err = t.client.Repositories.List(ctx, "", opts)
}
if err != nil {
return ErrorResult(fmt.Sprintf("Failed to list repos: %v", err))
}
var sb strings.Builder
if user != "" {
sb.WriteString(fmt.Sprintf("Repositories for %s:\n", user))
} else {
sb.WriteString("Your repositories:\n")
}
for _, repo := range repos {
sb.WriteString(fmt.Sprintf("- %s: %s (⭐ %d)\n", repo.GetName(), repo.GetDescription(), repo.GetStargazersCount()))
}
return &ToolResult{
ForLLM: sb.String(),
ForUser: sb.String(),
}
}

73
pkg/tools/system.go Normal file
View file

@ -0,0 +1,73 @@
package tools
import (
"context"
"fmt"
"strings"
"time"
"github.com/shirou/gopsutil/v3/cpu"
"github.com/shirou/gopsutil/v3/disk"
"github.com/shirou/gopsutil/v3/host"
"github.com/shirou/gopsutil/v3/mem"
)
type SystemTool struct{}
func NewSystemTool() *SystemTool {
return &SystemTool{}
}
func (t *SystemTool) Name() string {
return "system_stats"
}
func (t *SystemTool) Description() string {
return "Get current system statistics including CPU, Memory, Disk usage, and Host info."
}
func (t *SystemTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{},
"required": []string{},
}
}
func (t *SystemTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
var sb strings.Builder
// Host Info
hInfo, err := host.Info()
if err == nil {
sb.WriteString(fmt.Sprintf("Host: %s (%s %s)\n", hInfo.Hostname, hInfo.Platform, hInfo.PlatformVersion))
sb.WriteString(fmt.Sprintf("Uptime: %s\n", time.Duration(hInfo.Uptime)*time.Second))
}
// CPU
percent, err := cpu.Percent(time.Second, false)
if err == nil && len(percent) > 0 {
sb.WriteString(fmt.Sprintf("CPU Usage: %.2f%%\n", percent[0]))
}
// Memory
vMem, err := mem.VirtualMemory()
if err == nil {
usedGB := float64(vMem.Used) / 1024 / 1024 / 1024
totalGB := float64(vMem.Total) / 1024 / 1024 / 1024
sb.WriteString(fmt.Sprintf("Memory: %.2f GB / %.2f GB (%.2f%%)\n", usedGB, totalGB, vMem.UsedPercent))
}
// Disk (Root)
dUsage, err := disk.Usage("/")
if err == nil {
usedGB := float64(dUsage.Used) / 1024 / 1024 / 1024
totalGB := float64(dUsage.Total) / 1024 / 1024 / 1024
sb.WriteString(fmt.Sprintf("Disk (/): %.2f GB / %.2f GB (%.2f%%)\n", usedGB, totalGB, dUsage.UsedPercent))
}
return &ToolResult{
ForLLM: sb.String(),
ForUser: sb.String(),
}
}