mirror of
https://github.com/profullstack/agentbbs.git
synced 2026-08-14 14:57:27 +00:00
Video calls (PairUX→ASCII), agent@ chat, and finger routes
ssh video-<code>@ joins a PairUX/LiveKit call rendered as truecolor ASCII (▀ half-blocks, 2 pixels per cell); video@ prompts for a code. Codes are minted by PairUX only. Pipeline: VP8 RTP → PLI keyframe requests → ivfwriter remux → ffmpeg decode/scale → RGB24 → ANSI → bubbletea over the SSH PTY. Subscriber-only, no audio in v1. ssh agent@ opens a persisted chat with the operator's agent — AGENTBBS_AGENT_CMD runs per message (stdin→stdout), e.g. `claude -p`. ssh <member>@ with someone else's name prints a classic finger card (.plan, member since, last seen) and disconnects; your own name still enters the hub. cmd/lkpublish: dev publisher for testing (explicit -fps pacing; lksdk IVF replay mispaces from file timebase alone, measured 1fps from a 15fps file; dimensions required or dynacast pauses the track). Verified end-to-end against livekit-server --dev: 128k truecolor cells / 20k distinct colors streamed over a real SSH session; chat round-trip and finger card verified over SSH. Go toolchain pinned to 1.26 via mise.toml (lksdk requirement). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
f3b085a08f
commit
9b0f465946
14 changed files with 1397 additions and 46 deletions
15
README.md
15
README.md
|
|
@ -4,15 +4,18 @@
|
||||||
by Profullstack, Inc.
|
by Profullstack, Inc.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ssh bbs@profullstack.com # the hub: arcade (DOOM, snake), leaderboards — guests welcome
|
ssh bbs@profullstack.com # the hub: arcade (DOOM, snake), leaderboards — guests welcome
|
||||||
ssh join@profullstack.com # register your SSH key (prints instructions, disconnects)
|
ssh join@profullstack.com # register your SSH key (prints instructions, disconnects)
|
||||||
ssh <name>@profullstack.com # the hub as a member: saves, your own WADs, leaderboards
|
ssh <name>@profullstack.com # the hub as a member — or finger someone else's name
|
||||||
ssh pod@profullstack.com # your own Linux pod — members, $1/mo via CoinPay
|
ssh pod@profullstack.com # your own Linux pod — members, $1/mo via CoinPay
|
||||||
|
ssh video-<code>@profullstack.com # join a PairUX video call as truecolor ASCII
|
||||||
|
ssh agent@profullstack.com # chat with the operator's AI agent
|
||||||
```
|
```
|
||||||
|
|
||||||
No browser, no install, no client download. The BBS is a hub of hot-swappable
|
No browser, no install, no client download. The BBS is a hub of hot-swappable
|
||||||
plugins around one shared account system; the full product plan is in
|
plugins around one shared account system; the full product plan is in
|
||||||
[`docs/PRD.md`](docs/PRD.md) and [`docs/pods.md`](docs/pods.md).
|
[`docs/PRD.md`](docs/PRD.md), [`docs/pods.md`](docs/pods.md),
|
||||||
|
[`docs/video.md`](docs/video.md), and [`docs/social.md`](docs/social.md).
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
|
|
@ -21,6 +24,8 @@ plugins around one shared account system; the full product plan is in
|
||||||
| M0 — core hub (wish server, auth, plugin contract, SQLite) | ✅ |
|
| M0 — core hub (wish server, auth, plugin contract, SQLite) | ✅ |
|
||||||
| M1 — arcade (doom-ascii + Freedoom, sandbox, saves, leaderboards) | ✅ |
|
| M1 — arcade (doom-ascii + Freedoom, sandbox, saves, leaderboards) | ✅ |
|
||||||
| Pods (`pod@`, rootless containers, CoinPay membership) | ✅ |
|
| Pods (`pod@`, rootless containers, CoinPay membership) | ✅ |
|
||||||
|
| Video (`video-<code>@`, PairUX/LiveKit → ASCII streaming) | ✅ |
|
||||||
|
| `agent@` chat (configurable agent backend) + finger | ✅ |
|
||||||
| M2 — admin console | ⬜ |
|
| M2 — admin console | ⬜ |
|
||||||
| M3 — AgentGames (agent-vs-agent ladder; spec on logicsrc.com) | ⬜ |
|
| M3 — AgentGames (agent-vs-agent ladder; spec on logicsrc.com) | ⬜ |
|
||||||
| M4 — Files (cl1.tech SFTP workspaces) | ⬜ |
|
| M4 — Files (cl1.tech SFTP workspaces) | ⬜ |
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,8 @@ import (
|
||||||
gossh "golang.org/x/crypto/ssh"
|
gossh "golang.org/x/crypto/ssh"
|
||||||
|
|
||||||
"github.com/profullstack/agentbbs/internal/auth"
|
"github.com/profullstack/agentbbs/internal/auth"
|
||||||
|
"github.com/profullstack/agentbbs/internal/calls"
|
||||||
|
"github.com/profullstack/agentbbs/internal/chat"
|
||||||
"github.com/profullstack/agentbbs/internal/hub"
|
"github.com/profullstack/agentbbs/internal/hub"
|
||||||
"github.com/profullstack/agentbbs/internal/payments"
|
"github.com/profullstack/agentbbs/internal/payments"
|
||||||
"github.com/profullstack/agentbbs/internal/plugin"
|
"github.com/profullstack/agentbbs/internal/plugin"
|
||||||
|
|
@ -138,11 +140,18 @@ func (a *app) router() wish.Middleware {
|
||||||
hubHandler := activeterm.Middleware()(btMw(next))
|
hubHandler := activeterm.Middleware()(btMw(next))
|
||||||
return func(s ssh.Session) {
|
return func(s ssh.Session) {
|
||||||
user := strings.ToLower(s.User())
|
user := strings.ToLower(s.User())
|
||||||
|
code, isVideo := calls.RouteCode(user)
|
||||||
switch {
|
switch {
|
||||||
case auth.IsJoinName(user):
|
case auth.IsJoinName(user):
|
||||||
a.handleJoin(s)
|
a.handleJoin(s)
|
||||||
case auth.IsPodName(user):
|
case auth.IsPodName(user):
|
||||||
a.handlePod(s)
|
a.handlePod(s)
|
||||||
|
case isVideo:
|
||||||
|
a.handleVideo(s, code)
|
||||||
|
case user == "agent":
|
||||||
|
a.handleChat(s)
|
||||||
|
case a.handleFinger(s, user):
|
||||||
|
// fingered an existing account that isn't the caller's; done.
|
||||||
default:
|
default:
|
||||||
hubHandler(s)
|
hubHandler(s)
|
||||||
}
|
}
|
||||||
|
|
@ -280,6 +289,80 @@ func (a *app) handlePod(s ssh.Session) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleVideo joins a PairUX call rendered as ASCII (docs/video.md).
|
||||||
|
// `video@` prompts for a code; `video-<code>@` joins directly. Codes are
|
||||||
|
// minted by PairUX — starting a call requires already having one.
|
||||||
|
func (a *app) handleVideo(s ssh.Session, code string) {
|
||||||
|
identity := "ssh-guest"
|
||||||
|
if fp := auth.Fingerprint(s.PublicKey()); fp != "" {
|
||||||
|
if u, found, _ := a.st.UserByFingerprint(fp); found {
|
||||||
|
identity = "ssh-" + u.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sessID, _ := a.st.RecordSession(0, s.User(), remoteIP(s), "video")
|
||||||
|
defer func() { _ = a.st.EndSession(sessID) }()
|
||||||
|
if err := calls.Handle(s, code, identity); err != nil {
|
||||||
|
wish.Println(s, "video: "+err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleChat is the agent@ surface: talk to the operator's agent.
|
||||||
|
func (a *app) handleChat(s ssh.Session) {
|
||||||
|
u := auth.User{Name: "guest-" + remoteIP(s), Kind: auth.Guest}
|
||||||
|
if fp := auth.Fingerprint(s.PublicKey()); fp != "" {
|
||||||
|
if su, found, _ := a.st.UserByFingerprint(fp); found {
|
||||||
|
u = auth.User{Name: su.Name, Kind: auth.Kind(su.Kind), PubKeyFP: fp, StoreID: su.ID}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sessID, _ := a.st.RecordSession(u.StoreID, s.User(), remoteIP(s), "agent")
|
||||||
|
defer func() { _ = a.st.EndSession(sessID) }()
|
||||||
|
if err := chat.Handle(s, a.st, u); err != nil {
|
||||||
|
wish.Println(s, "chat: "+err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleFinger prints a classic finger card when someone ssh's to an
|
||||||
|
// existing account name that isn't their own (e.g. ssh anthony@host).
|
||||||
|
// Returns false when the route should fall through to the hub.
|
||||||
|
func (a *app) handleFinger(s ssh.Session, username string) bool {
|
||||||
|
if auth.IsGuestName(username) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
u, found, err := a.st.UserByName(username)
|
||||||
|
if err != nil || !found {
|
||||||
|
return false // unclaimed name → hub (claim flow)
|
||||||
|
}
|
||||||
|
if fp := auth.Fingerprint(s.PublicKey()); fp != "" && fp == u.PubKeyFP {
|
||||||
|
return false // it's them → hub
|
||||||
|
}
|
||||||
|
|
||||||
|
lastSeen := "never"
|
||||||
|
if t, ok, _ := a.st.LastSeen(u.ID); ok {
|
||||||
|
lastSeen = t.Local().Format("2006-01-02 15:04 MST")
|
||||||
|
}
|
||||||
|
plan := "no plan."
|
||||||
|
for _, p := range []string{
|
||||||
|
filepath.Join(a.dataDir, "users", u.Name, ".plan"),
|
||||||
|
filepath.Join(a.dataDir, "users", u.Name, "plan.txt"),
|
||||||
|
} {
|
||||||
|
if b, err := os.ReadFile(p); err == nil {
|
||||||
|
plan = strings.TrimSpace(string(b))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, _ = a.st.RecordSession(0, s.User(), remoteIP(s), "finger")
|
||||||
|
wish.Println(s, strings.Join([]string{
|
||||||
|
"",
|
||||||
|
" Login: " + u.Name + " Kind: " + u.Kind,
|
||||||
|
" Member since: " + u.CreatedAt.Format("2006-01-02") + " Last seen: " + lastSeen,
|
||||||
|
" Plan:",
|
||||||
|
" " + strings.ReplaceAll(plan, "\n", "\n "),
|
||||||
|
"",
|
||||||
|
}, "\n"))
|
||||||
|
_ = s.Exit(0)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func grantPod(st store.Store, args []string) {
|
func grantPod(st store.Store, args []string) {
|
||||||
if len(args) < 2 {
|
if len(args) < 2 {
|
||||||
fmt.Fprintln(os.Stderr, "usage: agentbbs grant-pod <username> <months>")
|
fmt.Fprintln(os.Stderr, "usage: agentbbs grant-pod <username> <months>")
|
||||||
|
|
|
||||||
68
cmd/lkpublish/main.go
Normal file
68
cmd/lkpublish/main.go
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
// Command lkpublish is a dev tool: it publishes a VP8 IVF file into a
|
||||||
|
// LiveKit room so the video-<code>@ SSH route has something to render.
|
||||||
|
//
|
||||||
|
// ffmpeg -f lavfi -i testsrc=size=320x240:rate=15 -t 60 -c:v libvpx test.ivf
|
||||||
|
// go run ./cmd/lkpublish -room test123 -file test.ivf
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
|
"github.com/livekit/protocol/livekit"
|
||||||
|
lksdk "github.com/livekit/server-sdk-go/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func env(k, def string) string {
|
||||||
|
if v := os.Getenv(k); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
room := flag.String("room", "test123", "room/call code to publish into")
|
||||||
|
file := flag.String("file", "test.ivf", "VP8 IVF file to stream")
|
||||||
|
fps := flag.Int("fps", 15, "frame rate to pace the file at")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
url := env("LIVEKIT_URL", "ws://localhost:7880")
|
||||||
|
key := env("LIVEKIT_API_KEY", "devkey")
|
||||||
|
secret := env("LIVEKIT_API_SECRET", "secret")
|
||||||
|
|
||||||
|
r, err := lksdk.ConnectToRoom(url, lksdk.ConnectInfo{
|
||||||
|
APIKey: key,
|
||||||
|
APISecret: secret,
|
||||||
|
RoomName: *room,
|
||||||
|
ParticipantIdentity: "lkpublish",
|
||||||
|
}, &lksdk.RoomCallback{})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("connect", "err", err)
|
||||||
|
}
|
||||||
|
defer r.Disconnect()
|
||||||
|
|
||||||
|
// Explicit frame duration: don't trust IVF timebase interpretation.
|
||||||
|
track, err := lksdk.NewLocalFileTrack(*file,
|
||||||
|
lksdk.ReaderTrackWithFrameDuration(time.Second/time.Duration(*fps)))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("track", "err", err)
|
||||||
|
}
|
||||||
|
// Width/height/source matter: without dimensions the SFU's dynacast has
|
||||||
|
// no layer to subscribe to and pauses the track entirely.
|
||||||
|
if _, err := r.LocalParticipant.PublishTrack(track, &lksdk.TrackPublicationOptions{
|
||||||
|
Name: "test-pattern",
|
||||||
|
Source: livekit.TrackSource_CAMERA,
|
||||||
|
VideoWidth: 320,
|
||||||
|
VideoHeight: 240,
|
||||||
|
}); err != nil {
|
||||||
|
log.Fatal("publish", "err", err)
|
||||||
|
}
|
||||||
|
log.Info("publishing", "room", *room, "file", *file)
|
||||||
|
|
||||||
|
sig := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sig, os.Interrupt)
|
||||||
|
<-sig
|
||||||
|
}
|
||||||
41
docs/social.md
Normal file
41
docs/social.md
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Social Routes Addendum: agent@ and finger
|
||||||
|
|
||||||
|
## agent@ — talk to the operator
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh agent@profullstack.com
|
||||||
|
```
|
||||||
|
|
||||||
|
A chat TUI: visitors talk to the operator's AI agent. Every message (both
|
||||||
|
directions) is persisted to `chat_messages`, so the operator can read
|
||||||
|
conversations later; live operator takeover arrives with the M2 admin
|
||||||
|
console.
|
||||||
|
|
||||||
|
The agent backend is one command, configured by env:
|
||||||
|
|
||||||
|
| Var | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `AGENTBBS_AGENT_CMD` | command per message: user text on stdin, reply on stdout (120s timeout). e.g. `claude -p`, a logicsrc/commandboard agent, any script. Unset = "message saved for the operator" mode. |
|
||||||
|
|
||||||
|
Identity: if the visitor's SSH key matches a member, the transcript is tied
|
||||||
|
to their account; otherwise it's keyed by remote address as a guest.
|
||||||
|
|
||||||
|
## finger — `ssh <member>@`
|
||||||
|
|
||||||
|
SSH'ing to an account name that exists but **isn't yours** prints a classic
|
||||||
|
finger card and disconnects:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ ssh anthony@profullstack.com
|
||||||
|
|
||||||
|
Login: anthony Kind: member
|
||||||
|
Member since: 2026-06-11 Last seen: 2026-06-11 11:07 UTC
|
||||||
|
Plan:
|
||||||
|
building agentbbs.
|
||||||
|
```
|
||||||
|
|
||||||
|
- The plan is read from the member's `~/.plan` (or `plan.txt`) in their data
|
||||||
|
directory.
|
||||||
|
- Your own name (key matches) still lands you in the hub; unclaimed names
|
||||||
|
fall through to the hub's claim flow.
|
||||||
|
- Works keyless — like real finger.
|
||||||
64
docs/video.md
Normal file
64
docs/video.md
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
# Video Calls Addendum (PairUX over SSH)
|
||||||
|
|
||||||
|
Join a PairUX video call from a terminal: the platform subscribes to the
|
||||||
|
call's LiveKit room and renders participant video as **truecolor ASCII** —
|
||||||
|
each character cell is two pixels via the upper-half block (▀), the same
|
||||||
|
technique doom-ascii uses.
|
||||||
|
|
||||||
|
## SSH routes
|
||||||
|
|
||||||
|
| Command | What happens |
|
||||||
|
|---|---|
|
||||||
|
| `ssh video-<code>@profullstack.com` | join call `<code>` directly |
|
||||||
|
| `ssh video@profullstack.com` | prompted for a code |
|
||||||
|
|
||||||
|
**Codes are minted by PairUX.** The SSH surface never creates calls — to
|
||||||
|
start one you must already have a code (create the call in PairUX first).
|
||||||
|
|
||||||
|
## Pipeline
|
||||||
|
|
||||||
|
```
|
||||||
|
PairUX / LiveKit room
|
||||||
|
└─ VP8 RTP track ── PLI keyframe requests ──┐
|
||||||
|
└─ ivfwriter remux → ffmpeg (decode + scale) → RGB24 frames
|
||||||
|
└─ half-block ANSI (internal/ascii) → bubbletea → SSH PTY
|
||||||
|
```
|
||||||
|
|
||||||
|
- Subscriber-only: the terminal viewer publishes nothing.
|
||||||
|
- A PLI is sent on subscribe and every 2s — without it the SFU never starts
|
||||||
|
forwarding video to a fresh subscriber, and the periodic refresh bounds
|
||||||
|
packet-loss artifacts.
|
||||||
|
- Audio is not rendered (it's a terminal); v2 could downlink Opus → local
|
||||||
|
audio out for desktop SSH clients.
|
||||||
|
- Frame size locks to the terminal geometry at join (`scale=` in ffmpeg);
|
||||||
|
~10–15 fps at typical terminal sizes costs a few hundred KB/s of SSH
|
||||||
|
bandwidth.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Shares PairUX's env shape:
|
||||||
|
|
||||||
|
| Var | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `AGENTBBS_LIVEKIT_URL` (or `LIVEKIT_URL` / `NEXT_PUBLIC_LIVEKIT_URL`) | LiveKit ws URL |
|
||||||
|
| `AGENTBBS_LIVEKIT_KEY` (or `LIVEKIT_API_KEY`) | API key |
|
||||||
|
| `AGENTBBS_LIVEKIT_SECRET` (or `LIVEKIT_API_SECRET`) | API secret |
|
||||||
|
| `AGENTBBS_VIDEO_DEBUG` | dump the received IVF stream to this path |
|
||||||
|
|
||||||
|
Unconfigured hosts refuse with a clear message.
|
||||||
|
|
||||||
|
## Dev testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d -p 7880:7880 -p 7881:7881 -p 7882:7882/udp \
|
||||||
|
livekit/livekit-server --dev --bind 0.0.0.0 # devkey/secret
|
||||||
|
ffmpeg -f lavfi -i testsrc=size=320x240:rate=15 -t 900 \
|
||||||
|
-c:v libvpx -b:v 400k -g 15 -y test.ivf
|
||||||
|
go run ./cmd/lkpublish -room demo1 -fps 15 -file test.ivf
|
||||||
|
ssh -p 2222 video-demo1@localhost
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: `lkpublish` passes an explicit frame duration — lksdk's IVF replay
|
||||||
|
pacing can't be trusted from the file timebase alone (we measured 1fps from
|
||||||
|
a 15fps file without it). Real PairUX publishers are browsers, which pace
|
||||||
|
correctly on their own.
|
||||||
91
go.mod
91
go.mod
|
|
@ -1,6 +1,6 @@
|
||||||
module github.com/profullstack/agentbbs
|
module github.com/profullstack/agentbbs
|
||||||
|
|
||||||
go 1.25.0
|
go 1.26
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/charmbracelet/bubbletea v1.3.10
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
|
|
@ -9,41 +9,110 @@ require (
|
||||||
github.com/charmbracelet/ssh v0.0.0-20250826160808-ebfa259c7309
|
github.com/charmbracelet/ssh v0.0.0-20250826160808-ebfa259c7309
|
||||||
github.com/charmbracelet/wish v1.4.7
|
github.com/charmbracelet/wish v1.4.7
|
||||||
github.com/creack/pty v1.1.24
|
github.com/creack/pty v1.1.24
|
||||||
golang.org/x/crypto v0.37.0
|
github.com/livekit/server-sdk-go/v2 v2.16.6
|
||||||
|
github.com/pion/webrtc/v4 v4.2.15
|
||||||
|
golang.org/x/crypto v0.50.0
|
||||||
modernc.org/sqlite v1.52.0
|
modernc.org/sqlite v1.52.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1 // indirect
|
||||||
|
buf.build/go/protovalidate v1.1.2 // indirect
|
||||||
|
buf.build/go/protoyaml v0.6.0 // indirect
|
||||||
|
cel.dev/expr v0.25.1 // indirect
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
||||||
|
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
github.com/benbjohnson/clock v1.3.5 // indirect
|
||||||
|
github.com/bep/debounce v1.2.1 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/charmbracelet/colorprofile v0.4.1 // indirect
|
||||||
github.com/charmbracelet/keygen v0.5.3 // indirect
|
github.com/charmbracelet/keygen v0.5.3 // indirect
|
||||||
github.com/charmbracelet/x/ansi v0.10.1 // indirect
|
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
||||||
github.com/charmbracelet/x/conpty v0.1.0 // indirect
|
github.com/charmbracelet/x/conpty v0.1.0 // indirect
|
||||||
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 // indirect
|
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 // indirect
|
||||||
github.com/charmbracelet/x/input v0.3.4 // indirect
|
github.com/charmbracelet/x/input v0.3.4 // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||||
github.com/charmbracelet/x/termios v0.1.0 // indirect
|
github.com/charmbracelet/x/termios v0.1.0 // indirect
|
||||||
github.com/charmbracelet/x/windows v0.2.0 // indirect
|
github.com/charmbracelet/x/windows v0.2.0 // indirect
|
||||||
|
github.com/clipperhouse/displaywidth v0.9.0 // indirect
|
||||||
|
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
|
||||||
|
github.com/dennwc/iters v1.2.2 // indirect
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
|
github.com/frostbyte73/core v0.1.1 // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
|
github.com/gammazero/deque v1.2.1 // indirect
|
||||||
|
github.com/go-jose/go-jose/v3 v3.0.5 // indirect
|
||||||
github.com/go-logfmt/logfmt v0.6.1 // indirect
|
github.com/go-logfmt/logfmt v0.6.1 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/google/cel-go v0.27.0 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
|
||||||
|
github.com/jxskiss/base62 v1.1.0 // indirect
|
||||||
|
github.com/klauspost/compress v1.18.4 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
|
github.com/lithammer/shortuuid/v4 v4.2.0 // indirect
|
||||||
|
github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731 // indirect
|
||||||
|
github.com/livekit/mediatransportutil v0.0.0-20260521165806-8004f10ad0c5 // indirect
|
||||||
|
github.com/livekit/protocol v1.46.0 // indirect
|
||||||
|
github.com/livekit/psrpc v0.7.1 // indirect
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||||
|
github.com/magefile/mage v1.17.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
github.com/mattn/go-runewidth v0.0.19 // indirect
|
||||||
|
github.com/moby/sys/user v0.4.0 // indirect
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
github.com/muesli/termenv v0.16.0 // indirect
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
|
github.com/nats-io/nats.go v1.48.0 // indirect
|
||||||
|
github.com/nats-io/nkeys v0.4.15 // indirect
|
||||||
|
github.com/nats-io/nuid v1.0.1 // indirect
|
||||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
|
github.com/pion/datachannel v1.6.0 // indirect
|
||||||
|
github.com/pion/dtls/v3 v3.1.4 // indirect
|
||||||
|
github.com/pion/ice/v4 v4.2.7 // indirect
|
||||||
|
github.com/pion/interceptor v0.1.45 // indirect
|
||||||
|
github.com/pion/logging v0.2.4 // indirect
|
||||||
|
github.com/pion/mdns/v2 v2.1.0 // indirect
|
||||||
|
github.com/pion/randutil v0.1.0 // indirect
|
||||||
|
github.com/pion/rtcp v1.2.16 // indirect
|
||||||
|
github.com/pion/rtp v1.10.2 // indirect
|
||||||
|
github.com/pion/sctp v1.10.0 // indirect
|
||||||
|
github.com/pion/sdp/v3 v3.0.18 // indirect
|
||||||
|
github.com/pion/srtp/v3 v3.0.11 // indirect
|
||||||
|
github.com/pion/stun/v3 v3.1.5 // indirect
|
||||||
|
github.com/pion/transport/v4 v4.0.2 // indirect
|
||||||
|
github.com/pion/turn/v5 v5.0.9 // indirect
|
||||||
|
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
|
||||||
|
github.com/redis/go-redis/v9 v9.17.2 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/twitchtv/twirp v8.1.3+incompatible // indirect
|
||||||
|
github.com/wlynxg/anet v0.0.5 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
|
github.com/zeebo/xxh3 v1.1.0 // indirect
|
||||||
golang.org/x/sys v0.42.0 // indirect
|
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||||
golang.org/x/text v0.24.0 // indirect
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
|
go.uber.org/zap v1.27.1 // indirect
|
||||||
|
go.uber.org/zap/exp v0.3.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a // indirect
|
||||||
|
golang.org/x/net v0.53.0 // indirect
|
||||||
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
|
golang.org/x/sys v0.43.0 // indirect
|
||||||
|
golang.org/x/text v0.36.0 // indirect
|
||||||
|
golang.org/x/time v0.14.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260427160629-7cedc36a6bc4 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 // indirect
|
||||||
|
google.golang.org/grpc v1.80.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
modernc.org/libc v1.72.3 // indirect
|
modernc.org/libc v1.72.3 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
|
|
|
||||||
329
go.sum
329
go.sum
|
|
@ -1,11 +1,43 @@
|
||||||
|
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1 h1:PMmTMyvHScV9Mn8wc6ASge9uRcHy0jtqPd+fM35LmsQ=
|
||||||
|
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM=
|
||||||
|
buf.build/go/protovalidate v1.1.2 h1:83vYHoY8f34hB8MeitGaYE3CGVPFxwdEUuskh5qQpA0=
|
||||||
|
buf.build/go/protovalidate v1.1.2/go.mod h1:Ez3z+w4c+wG+EpW8ovgZaZPnPl2XVF6kaxgcv1NG/QE=
|
||||||
|
buf.build/go/protoyaml v0.6.0 h1:Nzz1lvcXF8YgNZXk+voPPwdU8FjDPTUV4ndNTXN0n2w=
|
||||||
|
buf.build/go/protoyaml v0.6.0/go.mod h1:RgUOsBu/GYKLDSIRgQXniXbNgFlGEZnQpRAUdLAFV2Q=
|
||||||
|
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
|
||||||
|
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
||||||
|
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||||
|
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
|
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
|
||||||
|
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||||
|
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
|
||||||
|
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
|
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
|
||||||
|
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||||
|
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||||
|
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||||
|
github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4=
|
||||||
|
github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs=
|
||||||
|
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||||
|
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||||
|
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||||
|
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
|
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/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||||
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
|
||||||
github.com/charmbracelet/keygen v0.5.3 h1:2MSDC62OUbDy6VmjIE2jM24LuXUvKywLCmaJDmr/Z/4=
|
github.com/charmbracelet/keygen v0.5.3 h1:2MSDC62OUbDy6VmjIE2jM24LuXUvKywLCmaJDmr/Z/4=
|
||||||
github.com/charmbracelet/keygen v0.5.3/go.mod h1:TcpNoMAO5GSmhx3SgcEMqCrtn8BahKhB8AlwnLjRUpk=
|
github.com/charmbracelet/keygen v0.5.3/go.mod h1:TcpNoMAO5GSmhx3SgcEMqCrtn8BahKhB8AlwnLjRUpk=
|
||||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||||
|
|
@ -16,85 +48,322 @@ github.com/charmbracelet/ssh v0.0.0-20250826160808-ebfa259c7309 h1:dCVbCRRtg9+ts
|
||||||
github.com/charmbracelet/ssh v0.0.0-20250826160808-ebfa259c7309/go.mod h1:R9cISUs5kAH4Cq/rguNbSwcR+slE5Dfm8FEs//uoIGE=
|
github.com/charmbracelet/ssh v0.0.0-20250826160808-ebfa259c7309/go.mod h1:R9cISUs5kAH4Cq/rguNbSwcR+slE5Dfm8FEs//uoIGE=
|
||||||
github.com/charmbracelet/wish v1.4.7 h1:O+jdLac3s6GaqkOHHSwezejNK04vl6VjO1A+hl8J8Yc=
|
github.com/charmbracelet/wish v1.4.7 h1:O+jdLac3s6GaqkOHHSwezejNK04vl6VjO1A+hl8J8Yc=
|
||||||
github.com/charmbracelet/wish v1.4.7/go.mod h1:OBZ8vC62JC5cvbxJLh+bIWtG7Ctmct+ewziuUWK+G14=
|
github.com/charmbracelet/wish v1.4.7/go.mod h1:OBZ8vC62JC5cvbxJLh+bIWtG7Ctmct+ewziuUWK+G14=
|
||||||
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
|
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
||||||
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
|
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
||||||
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
|
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
|
||||||
github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
|
github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
|
||||||
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
|
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
|
||||||
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
|
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
|
||||||
github.com/charmbracelet/x/input v0.3.4 h1:Mujmnv/4DaitU0p+kIsrlfZl/UlmeLKw1wAP3e1fMN0=
|
github.com/charmbracelet/x/input v0.3.4 h1:Mujmnv/4DaitU0p+kIsrlfZl/UlmeLKw1wAP3e1fMN0=
|
||||||
github.com/charmbracelet/x/input v0.3.4/go.mod h1:JI8RcvdZWQIhn09VzeK3hdp4lTz7+yhiEdpEQtZN+2c=
|
github.com/charmbracelet/x/input v0.3.4/go.mod h1:JI8RcvdZWQIhn09VzeK3hdp4lTz7+yhiEdpEQtZN+2c=
|
||||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||||
github.com/charmbracelet/x/termios v0.1.0 h1:y4rjAHeFksBAfGbkRDmVinMg7x7DELIGAFbdNvxg97k=
|
github.com/charmbracelet/x/termios v0.1.0 h1:y4rjAHeFksBAfGbkRDmVinMg7x7DELIGAFbdNvxg97k=
|
||||||
github.com/charmbracelet/x/termios v0.1.0/go.mod h1:H/EVv/KRnrYjz+fCYa9bsKdqF3S8ouDK0AZEbG7r+/U=
|
github.com/charmbracelet/x/termios v0.1.0/go.mod h1:H/EVv/KRnrYjz+fCYa9bsKdqF3S8ouDK0AZEbG7r+/U=
|
||||||
github.com/charmbracelet/x/windows v0.2.0 h1:ilXA1GJjTNkgOm94CLPeSz7rar54jtFatdmoiONPuEw=
|
github.com/charmbracelet/x/windows v0.2.0 h1:ilXA1GJjTNkgOm94CLPeSz7rar54jtFatdmoiONPuEw=
|
||||||
github.com/charmbracelet/x/windows v0.2.0/go.mod h1:ZibNFR49ZFqCXgP76sYanisxRyC+EYrBE7TTknD8s1s=
|
github.com/charmbracelet/x/windows v0.2.0/go.mod h1:ZibNFR49ZFqCXgP76sYanisxRyC+EYrBE7TTknD8s1s=
|
||||||
|
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
|
||||||
|
github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
|
||||||
|
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
||||||
|
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||||
|
github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=
|
||||||
|
github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
|
||||||
|
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||||
|
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dennwc/iters v1.2.2 h1:XH2/Etihiy9ZvPOVCR+icQXeYlhbvS7k0qro4x/2qQo=
|
||||||
|
github.com/dennwc/iters v1.2.2/go.mod h1:M9KuuMBeyEXYTmB7EnI9SCyALFCmPWOIxn5W1L0CjGg=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
|
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||||
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
|
github.com/docker/cli v29.0.0+incompatible h1:KgsN2RUFMNM8wChxryicn4p46BdQWpXOA1XLGBGPGAw=
|
||||||
|
github.com/docker/cli v29.0.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||||
|
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||||
|
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||||
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
|
github.com/frostbyte73/core v0.1.1 h1:ChhJOR7bAKOCPbA+lqDLE2cGKlCG5JXsDvvQr4YaJIA=
|
||||||
|
github.com/frostbyte73/core v0.1.1/go.mod h1:mhfOtR+xWAvwXiwor7jnqPMnu4fxbv1F2MwZ0BEpzZo=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
|
github.com/gammazero/deque v1.2.1 h1:9fnQVFCCZ9/NOc7ccTNqzoKd1tCWOqeI05/lPqFPMGQ=
|
||||||
|
github.com/gammazero/deque v1.2.1/go.mod h1:5nSFkzVm+afG9+gy0VIowlqVAW4N8zNcMne+CMQVD2g=
|
||||||
|
github.com/go-jose/go-jose/v3 v3.0.5 h1:BLLJWbC4nMZOfuPVxoZIxeYsn6Nl2r1fITaJ78UQlVQ=
|
||||||
|
github.com/go-jose/go-jose/v3 v3.0.5/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||||
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
|
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
|
||||||
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
|
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
|
||||||
|
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-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
github.com/google/cel-go v0.27.0 h1:e7ih85+4qVrBuqQWTW4FKSqZYokVuc3HnhH5keboFTo=
|
||||||
|
github.com/google/cel-go v0.27.0/go.mod h1:tTJ11FWqnhw5KKpnWpvW9CJC3Y9GK4EIS0WXnBbebzw=
|
||||||
|
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/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||||
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||||
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/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
|
||||||
|
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc=
|
||||||
|
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||||
|
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/lithammer/shortuuid/v4 v4.2.0 h1:LMFOzVB3996a7b8aBuEXxqOBflbfPQAiVzkIcHO0h8c=
|
||||||
|
github.com/lithammer/shortuuid/v4 v4.2.0/go.mod h1:D5noHZ2oFw/YaKCfGy0YxyE7M0wMbezmMjPdhyEFe6Y=
|
||||||
|
github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731 h1:9x+U2HGLrSw5ATTo469PQPkqzdoU7be46ryiCDO3boc=
|
||||||
|
github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731/go.mod h1:Rs3MhFwutWhGwmY1VQsygw28z5bWcnEYmS1OG9OxjOQ=
|
||||||
|
github.com/livekit/mediatransportutil v0.0.0-20260521165806-8004f10ad0c5 h1:kGSXZ2P6K0Tv/5675P4JEPZJFGHe9AecX5g2F1/+71E=
|
||||||
|
github.com/livekit/mediatransportutil v0.0.0-20260521165806-8004f10ad0c5/go.mod h1:RCd46PT+6sEztld6XpkCrG1xskb0u3SqxIjy4G897Ss=
|
||||||
|
github.com/livekit/protocol v1.46.0 h1:onBkn2UEIX4qboVRtbdR1KZYNUoHfxHbgg7nqJ76e5s=
|
||||||
|
github.com/livekit/protocol v1.46.0/go.mod h1:KEPIJ/ZdMFQ9tmmfv/uT9TjQEuEcZupCZBabuRGEC1k=
|
||||||
|
github.com/livekit/psrpc v0.7.1 h1:ms37az0QTD3UXIWuUC5D/SkmKOlRMVRsI261eBWu/Vw=
|
||||||
|
github.com/livekit/psrpc v0.7.1/go.mod h1:bZ4iHFQptTkbPnB0LasvRNu/OBYXEu1NA6O5BMFo9kk=
|
||||||
|
github.com/livekit/server-sdk-go/v2 v2.16.6 h1:NBKw5l1AAOHsHAZKzuzAGzILRmSm+4E+/YZ9ZiaqudI=
|
||||||
|
github.com/livekit/server-sdk-go/v2 v2.16.6/go.mod h1:1+duFCDFpAvHqZ6mHQe7IwjecLIBFv/keJcexuFhD+0=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
|
github.com/magefile/mage v1.17.0 h1:dS4tkq997Ism03akafC8509iqDjeE7TNTexI25Y7sXM=
|
||||||
|
github.com/magefile/mage v1.17.0/go.mod h1:Yj51kqllmsgFpvvSzgrZPK9WtluG3kUhFaBUVLo4feA=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
|
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||||
|
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||||
|
github.com/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg=
|
||||||
|
github.com/moby/moby/api v1.52.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
|
||||||
|
github.com/moby/moby/client v0.1.0 h1:nt+hn6O9cyJQqq5UWnFGqsZRTS/JirUqzPjEl0Bdc/8=
|
||||||
|
github.com/moby/moby/client v0.1.0/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE=
|
||||||
|
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
|
||||||
|
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
|
||||||
|
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||||
|
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
|
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
|
||||||
|
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||||
|
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
|
||||||
|
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
|
||||||
|
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||||
|
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
|
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||||
|
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||||
|
github.com/opencontainers/runc v1.3.3 h1:qlmBbbhu+yY0QM7jqfuat7M1H3/iXjju3VkP9lkFQr4=
|
||||||
|
github.com/opencontainers/runc v1.3.3/go.mod h1:D7rL72gfWxVs9cJ2/AayxB0Hlvn9g0gaF1R7uunumSI=
|
||||||
|
github.com/ory/dockertest/v3 v3.12.0 h1:3oV9d0sDzlSQfHtIaB5k6ghUCVMVLpAY8hwrqoCyRCw=
|
||||||
|
github.com/ory/dockertest/v3 v3.12.0/go.mod h1:aKNDTva3cp8dwOWwb9cWuX84aH5akkxXRvO7KCwWVjE=
|
||||||
|
github.com/pion/datachannel v1.6.0 h1:XecBlj+cvsxhAMZWFfFcPyUaDZtd7IJvrXqlXD/53i0=
|
||||||
|
github.com/pion/datachannel v1.6.0/go.mod h1:ur+wzYF8mWdC+Mkis5Thosk+u/VOL287apDNEbFpsIk=
|
||||||
|
github.com/pion/dtls/v3 v3.1.4 h1:QhvtMflMfu9Kf0RcDC5BJBle4caPskByrKQR6uuYqpY=
|
||||||
|
github.com/pion/dtls/v3 v3.1.4/go.mod h1:cr/qotLISUw/9C1m83ZPNZtj9WnXkYLpfCptPqbkInc=
|
||||||
|
github.com/pion/ice/v4 v4.2.7 h1:zDEbC6MiEdhQpF8TxBOTws+NU6ZgGpveHrQq4Lc1kao=
|
||||||
|
github.com/pion/ice/v4 v4.2.7/go.mod h1:9SNPaq0c7El/ki8leJzyCkK10zsskprR3zTNbO3monY=
|
||||||
|
github.com/pion/interceptor v0.1.45 h1:6PUo/5829bIfRFIPPJQzuDn8EjxRTSB/CSD7QVCOaqo=
|
||||||
|
github.com/pion/interceptor v0.1.45/go.mod h1:gNDYM/uFKcLe/B3gS2/7+aw6z+RDiMy2qKTnF1LO31w=
|
||||||
|
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||||
|
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||||
|
github.com/pion/mdns/v2 v2.1.0 h1:3IJ9+Xio6tWYjhN6WwuY142P/1jA0D5ERaIqawg/fOY=
|
||||||
|
github.com/pion/mdns/v2 v2.1.0/go.mod h1:pcez23GdynwcfRU1977qKU0mDxSeucttSHbCSfFOd9A=
|
||||||
|
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||||
|
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||||
|
github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo=
|
||||||
|
github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo=
|
||||||
|
github.com/pion/rtp v1.10.2 h1:l+f6tTDcAH6xwepaAoW791ddhuYsJlqRATOzirO04Mo=
|
||||||
|
github.com/pion/rtp v1.10.2/go.mod h1:Au8fc6cEByy8RLTwKTQTEeQqDB/SJDxwL4mZuxYA5Pk=
|
||||||
|
github.com/pion/sctp v1.10.0 h1:qeoD6swF/2M5bYRcAGayqSbTKX3m4AW29CiQxG1+Pfg=
|
||||||
|
github.com/pion/sctp v1.10.0/go.mod h1:N20Dq6LY+JvJDAh9VVh1JELngb2rQ8dPgds5yBWiPgw=
|
||||||
|
github.com/pion/sdp/v3 v3.0.18 h1:l0bAXazKHpepazVdp+tPYnrsy9dfh7ZbT8DxesH5ZnI=
|
||||||
|
github.com/pion/sdp/v3 v3.0.18/go.mod h1:ZREGo6A9ZygQ9XkqAj5xYCQtQpif0i6Pa81HOiAdqQ8=
|
||||||
|
github.com/pion/srtp/v3 v3.0.11 h1:GiESUr54/K4UuPigfq/CvWUed80JenQAHXn0C2MQQIQ=
|
||||||
|
github.com/pion/srtp/v3 v3.0.11/go.mod h1:EeZOi/sd6glM1EXapg051gdNWO9yWT1YSsgQ4SlJkns=
|
||||||
|
github.com/pion/stun/v3 v3.1.5 h1:Y1FHlhaI6+4UoC5i/zQf4F7JvdZtB24/05oyy/GF1x8=
|
||||||
|
github.com/pion/stun/v3 v3.1.5/go.mod h1:zRUghXSQU32Lx5orJsz3uYMkIihweXb3mu5gIns02fs=
|
||||||
|
github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM=
|
||||||
|
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
|
||||||
|
github.com/pion/transport/v4 v4.0.2 h1:ifYlPqNwsy6aKQ9y8yzxXlHae5431ZrH2avkD/Rn6Tk=
|
||||||
|
github.com/pion/transport/v4 v4.0.2/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM=
|
||||||
|
github.com/pion/turn/v5 v5.0.9 h1:zNeBfRyzGn7MPyUTvmvxeltLEjlFdSLPT1tlakoaOXM=
|
||||||
|
github.com/pion/turn/v5 v5.0.9/go.mod h1:u3XjBqy2Z4+NhCUpDoOSsNuQDrPLvKStlCGWk6sTQ1E=
|
||||||
|
github.com/pion/webrtc/v4 v4.2.15 h1:Ir/MauNFCfg+kgyBYPQLiGdVWFlzEcLxqtuzAkYkky0=
|
||||||
|
github.com/pion/webrtc/v4 v4.2.15/go.mod h1:CPTcyLfIzC4scOkQ4UY4pj6WvbUGhcNLIpK28cP5h6M=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
||||||
|
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||||
|
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
|
||||||
|
github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8=
|
||||||
|
github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0=
|
||||||
|
github.com/shoenig/test v1.7.0 h1:eWcHtTXa6QLnBvm0jgEabMRN/uJ4DMV3M8xUGgRkZmk=
|
||||||
|
github.com/shoenig/test v1.7.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI=
|
||||||
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/twitchtv/twirp v8.1.3+incompatible h1:+F4TdErPgSUbMZMwp13Q/KgDVuI7HJXP61mNV3/7iuU=
|
||||||
|
github.com/twitchtv/twirp v8.1.3+incompatible/go.mod h1:RRJoFSAmTEh2weEqWtpPE3vFK5YBhA6bqp2l1kfCC5A=
|
||||||
|
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||||
|
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||||
|
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
|
||||||
|
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||||
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
|
||||||
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||||
|
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
||||||
|
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
||||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
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.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||||
|
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||||
|
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||||
|
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||||
|
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||||
|
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||||
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
|
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||||
|
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
|
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
|
||||||
|
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
|
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||||
|
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||||
|
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o=
|
||||||
|
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||||
|
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
|
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||||
|
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
|
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||||
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
|
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
|
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
||||||
|
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
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.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||||
|
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||||
|
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
|
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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||||
|
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260427160629-7cedc36a6bc4 h1:yOzSCGPx+cp5VO7IxvZ9SBFF7j1tZVcNtlHR2iYKtVo=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260427160629-7cedc36a6bc4/go.mod h1:Q9HWtNeE7tM9npdIsEvqXj1QJIvVoeAV3rtXtS715Cw=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 h1:tEkOQcXgF6dH1G+MVKZrfpYvozGrzb91k6ha7jireSM=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
|
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
||||||
|
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||||
|
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 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
|
modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
|
||||||
|
|
|
||||||
52
internal/ascii/ascii.go
Normal file
52
internal/ascii/ascii.go
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
// Package ascii converts raw video frames into truecolor terminal art.
|
||||||
|
//
|
||||||
|
// Each character cell renders two vertical pixels using the upper-half block
|
||||||
|
// (▀): foreground colors the top pixel, background the bottom — the same
|
||||||
|
// technique doom-ascii uses. A WxH terminal therefore displays a Wx(2H)
|
||||||
|
// pixel image.
|
||||||
|
package ascii
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FrameRGB renders a packed RGB24 frame (w*h*3 bytes, as ffmpeg's rawvideo
|
||||||
|
// rgb24 emits) sized exactly for the target cell grid: w columns, h*2 rows
|
||||||
|
// of pixels. Rows are joined with \r\n so the output is PTY-safe.
|
||||||
|
func FrameRGB(buf []byte, w, h int) string {
|
||||||
|
if len(buf) < w*h*3 || w <= 0 || h <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
rows := h / 2
|
||||||
|
var b strings.Builder
|
||||||
|
b.Grow(rows * w * 40)
|
||||||
|
for row := 0; row < rows; row++ {
|
||||||
|
top := row * 2
|
||||||
|
bot := top + 1
|
||||||
|
for x := 0; x < w; x++ {
|
||||||
|
ti := (top*w + x) * 3
|
||||||
|
bi := (bot*w + x) * 3
|
||||||
|
fmt.Fprintf(&b, "\x1b[38;2;%d;%d;%dm\x1b[48;2;%d;%d;%dm▀",
|
||||||
|
buf[ti], buf[ti+1], buf[ti+2],
|
||||||
|
buf[bi], buf[bi+1], buf[bi+2])
|
||||||
|
}
|
||||||
|
b.WriteString("\x1b[0m")
|
||||||
|
if row != rows-1 {
|
||||||
|
b.WriteString("\r\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// FitEven clamps a terminal geometry to an even pixel height for the
|
||||||
|
// half-block renderer and returns pixel dimensions (pw, ph) for the decoder.
|
||||||
|
func FitEven(cols, rows int) (pw, ph int) {
|
||||||
|
if cols < 8 {
|
||||||
|
cols = 8
|
||||||
|
}
|
||||||
|
if rows < 4 {
|
||||||
|
rows = 4
|
||||||
|
}
|
||||||
|
return cols, (rows - 1) * 2 // leave one status line
|
||||||
|
}
|
||||||
213
internal/calls/calls.go
Normal file
213
internal/calls/calls.go
Normal file
|
|
@ -0,0 +1,213 @@
|
||||||
|
// Package calls renders PairUX video calls as truecolor ASCII in the
|
||||||
|
// terminal: `ssh video-<code>@host` joins directly, `ssh video@host` prompts
|
||||||
|
// for a code. Codes are minted by PairUX — the SSH surface never creates
|
||||||
|
// calls, it only joins existing ones.
|
||||||
|
package calls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/charmbracelet/ssh"
|
||||||
|
|
||||||
|
"github.com/profullstack/agentbbs/internal/ascii"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RouteCode extracts the call code from an SSH username: "video" → "",
|
||||||
|
// "video-abc123" → "abc123". Second return is false when the username is
|
||||||
|
// not a video route at all.
|
||||||
|
func RouteCode(username string) (string, bool) {
|
||||||
|
u := strings.ToLower(username)
|
||||||
|
if u == "video" {
|
||||||
|
return "", true
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(u, "video-") && len(u) > len("video-") {
|
||||||
|
return u[len("video-"):], true
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle runs the call UI on the session. Standalone surface: leaving the
|
||||||
|
// call ends the SSH session.
|
||||||
|
func Handle(s ssh.Session, code, identity string) error {
|
||||||
|
ptyReq, winCh, hasPty := s.Pty()
|
||||||
|
if !hasPty {
|
||||||
|
_, _ = s.Write([]byte("video calls need a terminal (ssh -t)\r\n"))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
w, h := ptyReq.Window.Width, ptyReq.Window.Height
|
||||||
|
if w <= 0 {
|
||||||
|
w = 80
|
||||||
|
}
|
||||||
|
if h <= 0 {
|
||||||
|
h = 24
|
||||||
|
}
|
||||||
|
m := &model{
|
||||||
|
code: code,
|
||||||
|
identity: identity,
|
||||||
|
cfg: ConfigFromEnv(),
|
||||||
|
width: w,
|
||||||
|
height: h,
|
||||||
|
}
|
||||||
|
p := tea.NewProgram(m,
|
||||||
|
tea.WithInput(s), tea.WithOutput(s), tea.WithAltScreen())
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for w := range winCh {
|
||||||
|
p.Send(tea.WindowSizeMsg{Width: w.Width, Height: w.Height})
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
_, err := p.Run()
|
||||||
|
if m.sess != nil {
|
||||||
|
m.sess.Close()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type frameMsg string
|
||||||
|
type statusMsg string
|
||||||
|
type joinedMsg struct {
|
||||||
|
sess *session
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type model struct {
|
||||||
|
code string
|
||||||
|
identity string
|
||||||
|
cfg Config
|
||||||
|
|
||||||
|
input string // code entry buffer
|
||||||
|
sess *session
|
||||||
|
frame string
|
||||||
|
status string
|
||||||
|
errMsg string
|
||||||
|
|
||||||
|
width, height int
|
||||||
|
pw int // pixel width locked at join time (decoder output width)
|
||||||
|
joining bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
vTitle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#60a5fa"))
|
||||||
|
vDim = lipgloss.NewStyle().Foreground(lipgloss.Color("241"))
|
||||||
|
vErr = lipgloss.NewStyle().Foreground(lipgloss.Color("203"))
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *model) Init() tea.Cmd {
|
||||||
|
if m.code != "" {
|
||||||
|
return m.join()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// join connects in the background and pumps frames/status into the program.
|
||||||
|
func (m *model) join() tea.Cmd {
|
||||||
|
m.joining = true
|
||||||
|
code := m.code
|
||||||
|
pw, ph := ascii.FitEven(m.width, m.height)
|
||||||
|
m.pw = pw // decoder output is locked to this width for the session
|
||||||
|
cfg, id := m.cfg, m.identity
|
||||||
|
return func() tea.Msg {
|
||||||
|
sess, err := join(cfg, code, id, pw, ph)
|
||||||
|
return joinedMsg{sess: sess, err: err}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// nextFrame renders one decoded frame at the locked decoder width.
|
||||||
|
func (m *model) nextFrame() tea.Cmd {
|
||||||
|
sess, pw := m.sess, m.pw
|
||||||
|
return func() tea.Msg {
|
||||||
|
buf, ok := <-sess.Frames
|
||||||
|
if !ok {
|
||||||
|
return statusMsg("stream ended")
|
||||||
|
}
|
||||||
|
ph := (len(buf) / 3) / pw
|
||||||
|
return frameMsg(ascii.FrameRGB(buf, pw, ph))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) pump() tea.Cmd {
|
||||||
|
sess := m.sess
|
||||||
|
return tea.Batch(
|
||||||
|
m.nextFrame(),
|
||||||
|
func() tea.Msg { return statusMsg(<-sess.Status) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
m.width, m.height = msg.Width, msg.Height
|
||||||
|
case joinedMsg:
|
||||||
|
m.joining = false
|
||||||
|
if msg.err != nil {
|
||||||
|
m.errMsg = msg.err.Error()
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
m.sess = msg.sess
|
||||||
|
return m, m.pump()
|
||||||
|
case frameMsg:
|
||||||
|
m.frame = string(msg)
|
||||||
|
return m, m.nextFrame()
|
||||||
|
case statusMsg:
|
||||||
|
m.status = string(msg)
|
||||||
|
if m.sess != nil {
|
||||||
|
return m, func() tea.Msg { return statusMsg(<-m.sess.Status) }
|
||||||
|
}
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch msg.String() {
|
||||||
|
case "ctrl+c", "q":
|
||||||
|
if m.sess == nil && m.code == "" && msg.String() == "q" {
|
||||||
|
break // let people type codes containing q
|
||||||
|
}
|
||||||
|
return m, tea.Quit
|
||||||
|
case "esc":
|
||||||
|
return m, tea.Quit
|
||||||
|
}
|
||||||
|
// Code entry mode.
|
||||||
|
if m.sess == nil && !m.joining && m.code == "" {
|
||||||
|
switch msg.String() {
|
||||||
|
case "enter":
|
||||||
|
if strings.TrimSpace(m.input) != "" {
|
||||||
|
m.code = strings.TrimSpace(strings.ToLower(m.input))
|
||||||
|
return m, m.join()
|
||||||
|
}
|
||||||
|
case "backspace":
|
||||||
|
if len(m.input) > 0 {
|
||||||
|
m.input = m.input[:len(m.input)-1]
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if len(msg.String()) == 1 && len(m.input) < 64 {
|
||||||
|
m.input += msg.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) View() string {
|
||||||
|
switch {
|
||||||
|
case m.errMsg != "":
|
||||||
|
return lipgloss.NewStyle().Padding(1, 2).Render(
|
||||||
|
vTitle.Render("PairUX video") + "\n\n" +
|
||||||
|
vErr.Render(m.errMsg) + "\n\n" +
|
||||||
|
vDim.Render("esc to leave"))
|
||||||
|
case m.sess == nil && !m.joining:
|
||||||
|
return lipgloss.NewStyle().Padding(1, 2).Render(
|
||||||
|
vTitle.Render("PairUX video") + "\n\n" +
|
||||||
|
"Enter your call code (from pairux.com):\n\n" +
|
||||||
|
" > " + m.input + "█\n\n" +
|
||||||
|
vDim.Render("enter join · esc leave — don't have a code? create the call in PairUX first"))
|
||||||
|
case m.joining:
|
||||||
|
return lipgloss.NewStyle().Padding(1, 2).Render(
|
||||||
|
vTitle.Render("PairUX video") + "\n\n joining " + m.code + "…")
|
||||||
|
case m.frame == "":
|
||||||
|
return lipgloss.NewStyle().Padding(1, 2).Render(
|
||||||
|
vTitle.Render("PairUX video") + "\n\n " + m.status + "\n\n" +
|
||||||
|
vDim.Render("esc to leave"))
|
||||||
|
default:
|
||||||
|
return m.frame + "\r\n" + vDim.Render(" "+m.code+" · "+m.status+" · esc to leave")
|
||||||
|
}
|
||||||
|
}
|
||||||
223
internal/calls/livekit.go
Normal file
223
internal/calls/livekit.go
Normal file
|
|
@ -0,0 +1,223 @@
|
||||||
|
// LiveKit subscriber: joins a PairUX room, takes the first remote VP8 video
|
||||||
|
// track, remuxes RTP into IVF, and has ffmpeg decode + scale it into raw
|
||||||
|
// RGB24 frames sized for the terminal grid.
|
||||||
|
package calls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
lksdk "github.com/livekit/server-sdk-go/v2"
|
||||||
|
"github.com/pion/webrtc/v4"
|
||||||
|
"github.com/pion/webrtc/v4/pkg/media/ivfwriter"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config is the LiveKit deployment shared with PairUX.
|
||||||
|
type Config struct {
|
||||||
|
URL, Key, Secret string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigFromEnv reads AGENTBBS_LIVEKIT_* with LIVEKIT_* fallbacks, matching
|
||||||
|
// PairUX's env shape (LIVEKIT_API_KEY / LIVEKIT_API_SECRET).
|
||||||
|
func ConfigFromEnv() Config {
|
||||||
|
pick := func(keys ...string) string {
|
||||||
|
for _, k := range keys {
|
||||||
|
if v := os.Getenv(k); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return Config{
|
||||||
|
URL: pick("AGENTBBS_LIVEKIT_URL", "LIVEKIT_URL", "NEXT_PUBLIC_LIVEKIT_URL"),
|
||||||
|
Key: pick("AGENTBBS_LIVEKIT_KEY", "LIVEKIT_API_KEY"),
|
||||||
|
Secret: pick("AGENTBBS_LIVEKIT_SECRET", "LIVEKIT_API_SECRET"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Config) ok() bool { return c.URL != "" && c.Key != "" && c.Secret != "" }
|
||||||
|
|
||||||
|
// session is one live subscription: frames arrive on Frames sized pw*ph*3.
|
||||||
|
type session struct {
|
||||||
|
Frames chan []byte
|
||||||
|
Status chan string
|
||||||
|
room *lksdk.Room
|
||||||
|
ffmpeg *exec.Cmd
|
||||||
|
ivf io.WriteCloser
|
||||||
|
done chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// join connects to room `code` as a hidden-ish subscriber and starts the
|
||||||
|
// decode pipeline targeting pw x ph pixels.
|
||||||
|
func join(cfg Config, code, identity string, pw, ph int) (*session, error) {
|
||||||
|
if !cfg.ok() {
|
||||||
|
return nil, fmt.Errorf("video calls are not configured on this host (LIVEKIT_URL/API_KEY/API_SECRET)")
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &session{
|
||||||
|
Frames: make(chan []byte, 2),
|
||||||
|
Status: make(chan string, 8),
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
// ffmpeg: IVF (VP8) on stdin → raw RGB24 frames on stdout.
|
||||||
|
ffin, ivfW := io.Pipe()
|
||||||
|
s.ivf = ivfW
|
||||||
|
if dump := os.Getenv("AGENTBBS_VIDEO_DEBUG"); dump != "" {
|
||||||
|
if f, err := os.Create(dump); err == nil {
|
||||||
|
s.ivf = teeWriteCloser{io.MultiWriter(ivfW, f), ivfW, f}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmd := exec.Command("ffmpeg",
|
||||||
|
"-hide_banner", "-loglevel", "error",
|
||||||
|
"-probesize", "32", "-analyzeduration", "0",
|
||||||
|
"-fflags", "nobuffer", "-flags", "low_delay",
|
||||||
|
"-f", "ivf", "-i", "pipe:0",
|
||||||
|
"-vf", fmt.Sprintf("scale=%d:%d", pw, ph),
|
||||||
|
"-f", "rawvideo", "-pix_fmt", "rgb24", "pipe:1",
|
||||||
|
)
|
||||||
|
cmd.Stdin = ffin
|
||||||
|
cmd.Stderr = os.Stderr // surfaces decode errors in the server log
|
||||||
|
out, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return nil, fmt.Errorf("ffmpeg: %w", err)
|
||||||
|
}
|
||||||
|
s.ffmpeg = cmd
|
||||||
|
|
||||||
|
go func() { // frame pump: drop frames rather than lag
|
||||||
|
size := pw * ph * 3
|
||||||
|
for {
|
||||||
|
buf := make([]byte, size)
|
||||||
|
if _, err := io.ReadFull(out, buf); err != nil {
|
||||||
|
close(s.Frames)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case s.Frames <- buf:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
gotTrack := false
|
||||||
|
cb := &lksdk.RoomCallback{
|
||||||
|
ParticipantCallback: lksdk.ParticipantCallback{
|
||||||
|
OnTrackSubscribed: func(track *webrtc.TrackRemote, pub *lksdk.RemoteTrackPublication, rp *lksdk.RemoteParticipant) {
|
||||||
|
if gotTrack || track.Kind() != webrtc.RTPCodecTypeVideo {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if track.Codec().MimeType != webrtc.MimeTypeVP8 {
|
||||||
|
s.status("skipping non-VP8 track from " + rp.Identity())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gotTrack = true
|
||||||
|
s.status("video from " + rp.Identity())
|
||||||
|
// PLI is what makes the SFU start forwarding from a
|
||||||
|
// keyframe; without it a fresh subscriber starves.
|
||||||
|
rp.WritePLI(track.SSRC())
|
||||||
|
go s.keyframeTicker(rp, track.SSRC())
|
||||||
|
go s.consume(track)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
OnDisconnected: func() { s.status("disconnected") },
|
||||||
|
}
|
||||||
|
|
||||||
|
room, err := lksdk.ConnectToRoom(cfg.URL, lksdk.ConnectInfo{
|
||||||
|
APIKey: cfg.Key,
|
||||||
|
APISecret: cfg.Secret,
|
||||||
|
RoomName: code,
|
||||||
|
ParticipantIdentity: identity,
|
||||||
|
}, cb)
|
||||||
|
if err != nil {
|
||||||
|
s.Close()
|
||||||
|
return nil, fmt.Errorf("livekit: %w", err)
|
||||||
|
}
|
||||||
|
s.room = room
|
||||||
|
s.status("joined " + code + " — waiting for video…")
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// keyframeTicker re-requests keyframes so late joins and packet loss recover
|
||||||
|
// quickly; a periodic full refresh is cheap at terminal resolutions.
|
||||||
|
func (s *session) keyframeTicker(rp *lksdk.RemoteParticipant, ssrc webrtc.SSRC) {
|
||||||
|
t := time.NewTicker(2 * time.Second)
|
||||||
|
defer t.Stop()
|
||||||
|
for range t.C {
|
||||||
|
select {
|
||||||
|
case <-s.done:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
rp.WritePLI(ssrc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// consume remuxes the track's RTP into the IVF pipe until it ends.
|
||||||
|
func (s *session) consume(track *webrtc.TrackRemote) {
|
||||||
|
w, err := ivfwriter.NewWith(s.ivf)
|
||||||
|
if err != nil {
|
||||||
|
s.status("ivf: " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
n := 0
|
||||||
|
for {
|
||||||
|
pkt, _, err := track.ReadRTP()
|
||||||
|
if err != nil {
|
||||||
|
s.status(fmt.Sprintf("track ended after %d packets: %v", n, err))
|
||||||
|
_ = w.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := w.WriteRTP(pkt); err != nil {
|
||||||
|
s.status("ivf write: " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
if n == 1 || n%500 == 0 {
|
||||||
|
s.status(fmt.Sprintf("receiving (%d rtp packets)", n))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *session) status(msg string) {
|
||||||
|
select {
|
||||||
|
case s.Status <- msg:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// teeWriteCloser mirrors the IVF stream to a debug file (AGENTBBS_VIDEO_DEBUG).
|
||||||
|
type teeWriteCloser struct {
|
||||||
|
io.Writer
|
||||||
|
pipe io.WriteCloser
|
||||||
|
file io.Closer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t teeWriteCloser) Close() error {
|
||||||
|
_ = t.file.Close()
|
||||||
|
return t.pipe.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close tears the whole pipeline down.
|
||||||
|
func (s *session) Close() {
|
||||||
|
select {
|
||||||
|
case <-s.done:
|
||||||
|
default:
|
||||||
|
close(s.done)
|
||||||
|
}
|
||||||
|
if s.room != nil {
|
||||||
|
s.room.Disconnect()
|
||||||
|
}
|
||||||
|
if s.ivf != nil {
|
||||||
|
_ = s.ivf.Close()
|
||||||
|
}
|
||||||
|
if s.ffmpeg != nil && s.ffmpeg.Process != nil {
|
||||||
|
_ = s.ffmpeg.Process.Kill()
|
||||||
|
_ = s.ffmpeg.Wait()
|
||||||
|
}
|
||||||
|
}
|
||||||
177
internal/chat/chat.go
Normal file
177
internal/chat/chat.go
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
// Package chat is the agent@ surface: talk to the operator's AI agent (or,
|
||||||
|
// once the M2 admin console lands, the operator live).
|
||||||
|
//
|
||||||
|
// The agent backend is one configurable command, AGENTBBS_AGENT_CMD: each
|
||||||
|
// user message is piped to its stdin, stdout comes back as the reply. Point
|
||||||
|
// it at anything — `claude -p`, a logicsrc/commandboard agent, a shell
|
||||||
|
// script. Unset = a polite "leave a message" mode (messages are persisted
|
||||||
|
// either way, so the operator can read them later).
|
||||||
|
package chat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/charmbracelet/ssh"
|
||||||
|
|
||||||
|
"github.com/profullstack/agentbbs/internal/auth"
|
||||||
|
"github.com/profullstack/agentbbs/internal/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
const historyLines = 200
|
||||||
|
|
||||||
|
// Handle runs the chat UI; leaving ends the SSH session.
|
||||||
|
func Handle(s ssh.Session, st store.Store, user auth.User) error {
|
||||||
|
ptyReq, winCh, hasPty := s.Pty()
|
||||||
|
if !hasPty {
|
||||||
|
_, _ = s.Write([]byte("agent chat needs a terminal (ssh -t)\r\n"))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
m := &model{
|
||||||
|
st: st,
|
||||||
|
user: user,
|
||||||
|
width: ptyReq.Window.Width,
|
||||||
|
height: ptyReq.Window.Height,
|
||||||
|
}
|
||||||
|
if msgs, err := st.RecentChats(user.Name, 20); err == nil {
|
||||||
|
for _, c := range msgs {
|
||||||
|
m.lines = append(m.lines, render(c.Role, c.Text))
|
||||||
|
}
|
||||||
|
if len(msgs) > 0 {
|
||||||
|
m.lines = append(m.lines, cDim.Render("— earlier conversation —"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p := tea.NewProgram(m, tea.WithInput(s), tea.WithOutput(s), tea.WithAltScreen())
|
||||||
|
go func() {
|
||||||
|
for w := range winCh {
|
||||||
|
p.Send(tea.WindowSizeMsg{Width: w.Width, Height: w.Height})
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
_, err := p.Run()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
cTitle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#c084fc"))
|
||||||
|
cYou = lipgloss.NewStyle().Foreground(lipgloss.Color("#4ade80"))
|
||||||
|
cAgent = lipgloss.NewStyle().Foreground(lipgloss.Color("#c084fc"))
|
||||||
|
cDim = lipgloss.NewStyle().Foreground(lipgloss.Color("241"))
|
||||||
|
cErr = lipgloss.NewStyle().Foreground(lipgloss.Color("203"))
|
||||||
|
)
|
||||||
|
|
||||||
|
func render(role, text string) string {
|
||||||
|
if role == "user" {
|
||||||
|
return cYou.Render("you ") + text
|
||||||
|
}
|
||||||
|
return cAgent.Render("agent ") + text
|
||||||
|
}
|
||||||
|
|
||||||
|
type replyMsg struct {
|
||||||
|
text string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type model struct {
|
||||||
|
st store.Store
|
||||||
|
user auth.User
|
||||||
|
|
||||||
|
lines []string
|
||||||
|
input string
|
||||||
|
waiting bool
|
||||||
|
|
||||||
|
width, height int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) Init() tea.Cmd { return nil }
|
||||||
|
|
||||||
|
// ask pipes the message to the configured agent command.
|
||||||
|
func (m *model) ask(text string) tea.Cmd {
|
||||||
|
return func() tea.Msg {
|
||||||
|
cmdline := strings.TrimSpace(os.Getenv("AGENTBBS_AGENT_CMD"))
|
||||||
|
if cmdline == "" {
|
||||||
|
return replyMsg{text: "The operator's agent isn't wired up right now — your message is saved and a human will read it."}
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
parts := strings.Fields(cmdline)
|
||||||
|
cmd := exec.CommandContext(ctx, parts[0], parts[1:]...)
|
||||||
|
cmd.Stdin = strings.NewReader(text)
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return replyMsg{err: err}
|
||||||
|
}
|
||||||
|
reply := strings.TrimSpace(string(out))
|
||||||
|
if reply == "" {
|
||||||
|
reply = "(no reply)"
|
||||||
|
}
|
||||||
|
return replyMsg{text: reply}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
m.width, m.height = msg.Width, msg.Height
|
||||||
|
case replyMsg:
|
||||||
|
m.waiting = false
|
||||||
|
if msg.err != nil {
|
||||||
|
m.lines = append(m.lines, cErr.Render("agent error: "+msg.err.Error()))
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
_ = m.st.AddChat(m.user.StoreID, m.user.Name, "agent", msg.text)
|
||||||
|
for _, l := range strings.Split(msg.text, "\n") {
|
||||||
|
m.lines = append(m.lines, render("agent", l))
|
||||||
|
}
|
||||||
|
if len(m.lines) > historyLines {
|
||||||
|
m.lines = m.lines[len(m.lines)-historyLines:]
|
||||||
|
}
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch msg.String() {
|
||||||
|
case "ctrl+c", "esc":
|
||||||
|
return m, tea.Quit
|
||||||
|
case "enter":
|
||||||
|
text := strings.TrimSpace(m.input)
|
||||||
|
if text == "" || m.waiting {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
m.input = ""
|
||||||
|
m.waiting = true
|
||||||
|
_ = m.st.AddChat(m.user.StoreID, m.user.Name, "user", text)
|
||||||
|
m.lines = append(m.lines, render("user", text))
|
||||||
|
return m, m.ask(text)
|
||||||
|
case "backspace":
|
||||||
|
if len(m.input) > 0 {
|
||||||
|
m.input = m.input[:len(m.input)-1]
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if msg.Type == tea.KeyRunes || msg.String() == " " {
|
||||||
|
m.input += string(msg.Runes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) View() string {
|
||||||
|
rows := m.height - 4
|
||||||
|
if rows < 3 {
|
||||||
|
rows = 3
|
||||||
|
}
|
||||||
|
start := 0
|
||||||
|
if len(m.lines) > rows {
|
||||||
|
start = len(m.lines) - rows
|
||||||
|
}
|
||||||
|
body := strings.Join(m.lines[start:], "\n")
|
||||||
|
prompt := "> " + m.input + "█"
|
||||||
|
if m.waiting {
|
||||||
|
prompt = cDim.Render("agent is thinking…")
|
||||||
|
}
|
||||||
|
return lipgloss.NewStyle().Padding(0, 1).Render(
|
||||||
|
cTitle.Render("agent@ — talk to profullstack") + cDim.Render(" (esc to leave)") + "\n" +
|
||||||
|
body + "\n\n" + prompt)
|
||||||
|
}
|
||||||
|
|
@ -35,6 +35,10 @@ type Store interface {
|
||||||
EnsureUser(name, kind, pubkeyFP string) (User, error)
|
EnsureUser(name, kind, pubkeyFP string) (User, error)
|
||||||
// UserByFingerprint finds an account by SSH key fingerprint.
|
// UserByFingerprint finds an account by SSH key fingerprint.
|
||||||
UserByFingerprint(fp string) (User, bool, error)
|
UserByFingerprint(fp string) (User, bool, error)
|
||||||
|
// UserByName finds an account by exact username (no creation).
|
||||||
|
UserByName(name string) (User, bool, error)
|
||||||
|
// LastSeen reports the start of the user's most recent session.
|
||||||
|
LastSeen(userID int64) (time.Time, bool, error)
|
||||||
|
|
||||||
RecordSession(userID int64, username, remote, route string) (int64, error)
|
RecordSession(userID int64, username, remote, route string) (int64, error)
|
||||||
EndSession(sessionID int64) error
|
EndSession(sessionID int64) error
|
||||||
|
|
@ -46,9 +50,20 @@ type Store interface {
|
||||||
PodPaidUntil(userID int64) (time.Time, bool, error)
|
PodPaidUntil(userID int64) (time.Time, bool, error)
|
||||||
GrantPod(userID int64, until time.Time, paymentRef string) error
|
GrantPod(userID int64, until time.Time, paymentRef string) error
|
||||||
|
|
||||||
|
// Chat transcripts for the agent@ surface.
|
||||||
|
AddChat(userID int64, username, role, text string) error
|
||||||
|
RecentChats(username string, n int) ([]ChatMessage, error)
|
||||||
|
|
||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ChatMessage is one line of an agent@ conversation.
|
||||||
|
type ChatMessage struct {
|
||||||
|
Role string // "user" or "agent"
|
||||||
|
Text string
|
||||||
|
At time.Time
|
||||||
|
}
|
||||||
|
|
||||||
// ErrKeyMismatch means a username is already registered with another key.
|
// ErrKeyMismatch means a username is already registered with another key.
|
||||||
var ErrKeyMismatch = errors.New("username registered with a different key")
|
var ErrKeyMismatch = errors.New("username registered with a different key")
|
||||||
|
|
||||||
|
|
@ -92,6 +107,15 @@ CREATE TABLE IF NOT EXISTS scores (
|
||||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS idx_scores_game ON scores(game, score DESC);
|
CREATE INDEX IF NOT EXISTS idx_scores_game ON scores(game, score DESC);
|
||||||
|
CREATE TABLE IF NOT EXISTS chat_messages (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
user_id INTEGER,
|
||||||
|
username TEXT NOT NULL,
|
||||||
|
role TEXT NOT NULL,
|
||||||
|
text TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_chat_user ON chat_messages(username, id);
|
||||||
CREATE TABLE IF NOT EXISTS pod_subscriptions (
|
CREATE TABLE IF NOT EXISTS pod_subscriptions (
|
||||||
user_id INTEGER PRIMARY KEY REFERENCES users(id),
|
user_id INTEGER PRIMARY KEY REFERENCES users(id),
|
||||||
paid_until TEXT NOT NULL,
|
paid_until TEXT NOT NULL,
|
||||||
|
|
@ -211,4 +235,65 @@ func (s *sqliteStore) GrantPod(userID int64, until time.Time, ref string) error
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *sqliteStore) UserByName(name string) (User, bool, error) {
|
||||||
|
var u User
|
||||||
|
var created string
|
||||||
|
err := s.db.QueryRow(`SELECT id, name, kind, pubkey_fp, created_at FROM users WHERE name = ?`, name).
|
||||||
|
Scan(&u.ID, &u.Name, &u.Kind, &u.PubKeyFP, &created)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return User{}, false, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return User{}, false, err
|
||||||
|
}
|
||||||
|
u.CreatedAt, _ = time.Parse(time.RFC3339, created)
|
||||||
|
return u, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sqliteStore) LastSeen(userID int64) (time.Time, bool, error) {
|
||||||
|
var at string
|
||||||
|
err := s.db.QueryRow(`SELECT started_at FROM sessions WHERE user_id = ? ORDER BY id DESC LIMIT 1`, userID).Scan(&at)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return time.Time{}, false, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, false, err
|
||||||
|
}
|
||||||
|
t, err := time.Parse(time.RFC3339, at)
|
||||||
|
return t, err == nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sqliteStore) AddChat(userID int64, username, role, text string) error {
|
||||||
|
var uid any
|
||||||
|
if userID > 0 {
|
||||||
|
uid = userID
|
||||||
|
}
|
||||||
|
_, err := s.db.Exec(`INSERT INTO chat_messages (user_id, username, role, text) VALUES (?,?,?,?)`,
|
||||||
|
uid, username, role, text)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sqliteStore) RecentChats(username string, n int) ([]ChatMessage, error) {
|
||||||
|
rows, err := s.db.Query(`
|
||||||
|
SELECT role, text, created_at FROM (
|
||||||
|
SELECT id, role, text, created_at FROM chat_messages
|
||||||
|
WHERE username = ? ORDER BY id DESC LIMIT ?
|
||||||
|
) ORDER BY id ASC`, username, n)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var out []ChatMessage
|
||||||
|
for rows.Next() {
|
||||||
|
var m ChatMessage
|
||||||
|
var at string
|
||||||
|
if err := rows.Scan(&m.Role, &m.Text, &at); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
m.At, _ = time.Parse(time.RFC3339, at)
|
||||||
|
out = append(out, m)
|
||||||
|
}
|
||||||
|
return out, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
func (s *sqliteStore) Close() error { return s.db.Close() }
|
func (s *sqliteStore) Close() error { return s.db.Close() }
|
||||||
|
|
|
||||||
BIN
lkpublish
Executable file
BIN
lkpublish
Executable file
Binary file not shown.
2
mise.toml
Normal file
2
mise.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
[tools]
|
||||||
|
go = "1.26"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue