feat(cli): add boxed help/error UI with no-color support
This commit is contained in:
parent
c7461f9e96
commit
48a1e979a4
13 changed files with 1043 additions and 65 deletions
147
cmd/picoclaw/internal/cliui/cliui.go
Normal file
147
cmd/picoclaw/internal/cliui/cliui.go
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
// Package cliui renders human-oriented CLI output: bordered panels and columns
|
||||||
|
// on wide interactive terminals. Layout (boxes/columns) is independent of ANSI
|
||||||
|
// color: use --no-color or NO_COLOR to disable colors only; narrow or non-TTY
|
||||||
|
// stdout falls back to plain line-oriented output.
|
||||||
|
package cliui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/muesli/termenv"
|
||||||
|
"golang.org/x/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Minimum terminal width (columns) for bordered / structured layout.
|
||||||
|
// Below this, plain line-oriented output is used so boxes do not wrap badly.
|
||||||
|
const minWidthFancy = 88
|
||||||
|
|
||||||
|
// Minimum width to lay out some views in two columns (e.g. status providers).
|
||||||
|
const minWidthColumns = 104
|
||||||
|
|
||||||
|
var initMu sync.Mutex
|
||||||
|
|
||||||
|
// Init configures lipgloss for this process. When disableAnsiColors is true
|
||||||
|
// (e.g. --no-color, NO_COLOR, or TERM=dumb), only color is turned off; Unicode
|
||||||
|
// borders still render when UseFancyLayout() is true.
|
||||||
|
func Init(disableAnsiColors bool) {
|
||||||
|
initMu.Lock()
|
||||||
|
defer initMu.Unlock()
|
||||||
|
if disableAnsiColors {
|
||||||
|
lipgloss.SetColorProfile(termenv.Ascii)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lipgloss.SetColorProfile(termenv.EnvColorProfile())
|
||||||
|
}
|
||||||
|
|
||||||
|
// StdoutWidth returns the terminal width or a sane default if unknown.
|
||||||
|
func StdoutWidth() int {
|
||||||
|
w, _, err := term.GetSize(int(os.Stdout.Fd()))
|
||||||
|
if err != nil || w < 20 {
|
||||||
|
return 80
|
||||||
|
}
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseFancyLayout is true when styled boxes/columns should be used.
|
||||||
|
func UseFancyLayout() bool {
|
||||||
|
if !term.IsTerminal(int(os.Stdout.Fd())) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return StdoutWidth() >= minWidthFancy
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseColumnLayout is true when a second content column is viable.
|
||||||
|
func UseColumnLayout() bool {
|
||||||
|
return UseFancyLayout() && StdoutWidth() >= minWidthColumns
|
||||||
|
}
|
||||||
|
|
||||||
|
// InnerWidth is the target content width inside borders/margins.
|
||||||
|
func InnerWidth() int {
|
||||||
|
w := StdoutWidth()
|
||||||
|
// Rounded border + horizontal padding (lipgloss borders ~= 2 cols each side + padding).
|
||||||
|
const borderBudget = 8
|
||||||
|
if w > borderBudget+48 {
|
||||||
|
return w - borderBudget
|
||||||
|
}
|
||||||
|
return 48
|
||||||
|
}
|
||||||
|
|
||||||
|
// StderrWidth returns stderr terminal width or a sane default.
|
||||||
|
func StderrWidth() int {
|
||||||
|
w, _, err := term.GetSize(int(os.Stderr.Fd()))
|
||||||
|
if err != nil || w < 20 {
|
||||||
|
return 80
|
||||||
|
}
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseFancyStderr is true when stderr can show boxed errors without ugly wraps.
|
||||||
|
func UseFancyStderr() bool {
|
||||||
|
if !term.IsTerminal(int(os.Stderr.Fd())) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return StderrWidth() >= minWidthFancy
|
||||||
|
}
|
||||||
|
|
||||||
|
// InnerStderrWidth mirrors InnerWidth but for stderr.
|
||||||
|
func InnerStderrWidth() int {
|
||||||
|
w := StderrWidth()
|
||||||
|
const borderBudget = 8
|
||||||
|
if w > borderBudget+48 {
|
||||||
|
return w - borderBudget
|
||||||
|
}
|
||||||
|
return 48
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
accentBlue = lipgloss.Color("#3E5DB9")
|
||||||
|
accentRed = lipgloss.Color("#D54646")
|
||||||
|
colorMuted = lipgloss.Color("#6B6B6B")
|
||||||
|
colorOK = lipgloss.Color("#2E7D32")
|
||||||
|
)
|
||||||
|
|
||||||
|
func borderStyle() lipgloss.Style {
|
||||||
|
return lipgloss.NewStyle().
|
||||||
|
Border(lipgloss.RoundedBorder()).
|
||||||
|
BorderForeground(accentBlue).
|
||||||
|
Padding(0, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func titleBarStyle() lipgloss.Style {
|
||||||
|
return lipgloss.NewStyle().
|
||||||
|
Foreground(accentRed).
|
||||||
|
Bold(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mutedStyle() lipgloss.Style {
|
||||||
|
return lipgloss.NewStyle().Foreground(colorMuted)
|
||||||
|
}
|
||||||
|
|
||||||
|
func bodyStyle() lipgloss.Style {
|
||||||
|
return lipgloss.NewStyle()
|
||||||
|
}
|
||||||
|
|
||||||
|
func kvKeyStyle() lipgloss.Style {
|
||||||
|
return lipgloss.NewStyle().Foreground(accentBlue).Bold(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func kvValStyle() lipgloss.Style {
|
||||||
|
return lipgloss.NewStyle()
|
||||||
|
}
|
||||||
|
|
||||||
|
// helpIntroStyle is the top tagline (PicoClaw blue, matches ASCII banner left side).
|
||||||
|
func helpIntroStyle() lipgloss.Style {
|
||||||
|
return lipgloss.NewStyle().Foreground(accentBlue).Bold(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// helpIdentStyle is the left column for commands and flags (blue identifiers).
|
||||||
|
func helpIdentStyle() lipgloss.Style {
|
||||||
|
return lipgloss.NewStyle().Foreground(accentBlue).Bold(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// helpPlaceholderStyle highlights <placeholders> in usage lines (red accent).
|
||||||
|
func helpPlaceholderStyle() lipgloss.Style {
|
||||||
|
return lipgloss.NewStyle().Foreground(accentRed).Bold(true)
|
||||||
|
}
|
||||||
298
cmd/picoclaw/internal/cliui/help_cmd.go
Normal file
298
cmd/picoclaw/internal/cliui/help_cmd.go
Normal file
|
|
@ -0,0 +1,298 @@
|
||||||
|
package cliui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
flag "github.com/spf13/pflag"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RenderCommandHelp builds Ruff-style sectioned, two-column help when
|
||||||
|
// UseFancyLayout(); otherwise plain Cobra-style text.
|
||||||
|
func RenderCommandHelp(c *cobra.Command) string {
|
||||||
|
if !UseFancyLayout() {
|
||||||
|
return plainCommandHelp(c)
|
||||||
|
}
|
||||||
|
syncFlags(c)
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
head, sub := helpIntro(c)
|
||||||
|
if head != "" {
|
||||||
|
b.WriteString(helpIntroStyle().Render(head))
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
if sub != "" {
|
||||||
|
b.WriteString(mutedStyle().Render(sub))
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
if head != "" || sub != "" {
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
inner := InnerWidth()
|
||||||
|
contentW := inner - 6
|
||||||
|
if contentW < 36 {
|
||||||
|
contentW = 36
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
usageBody := bodyStyle().MaxWidth(contentW).Render(styleUsageTokens(c.UseLine()))
|
||||||
|
b.WriteString(sectionPanel("Usage", usageBody, inner))
|
||||||
|
b.WriteString("\n")
|
||||||
|
|
||||||
|
// Examples
|
||||||
|
if ex := strings.TrimSpace(c.Example); ex != "" {
|
||||||
|
exBody := bodyStyle().Width(contentW).Render(ex)
|
||||||
|
b.WriteString(sectionPanel("Examples", exBody, inner))
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subcommands
|
||||||
|
subs := visibleSubcommands(c)
|
||||||
|
if len(subs) > 0 {
|
||||||
|
rows := make([][2]string, 0, len(subs))
|
||||||
|
for _, sub := range subs {
|
||||||
|
left := sub.Name()
|
||||||
|
if a := sub.Aliases; len(a) > 0 {
|
||||||
|
left += " (" + strings.Join(a, ", ") + ")"
|
||||||
|
}
|
||||||
|
rows = append(rows, [2]string{left, sub.Short})
|
||||||
|
}
|
||||||
|
b.WriteString(sectionPanel("Commands", renderTwoColPairs(rows, contentW), inner))
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Local options
|
||||||
|
local := c.LocalFlags()
|
||||||
|
opts := collectFlagRows(local)
|
||||||
|
if len(opts) > 0 {
|
||||||
|
title := "Options"
|
||||||
|
if !c.HasParent() {
|
||||||
|
title = "Flags"
|
||||||
|
}
|
||||||
|
b.WriteString(sectionPanel(title, renderTwoColPairs(opts, contentW), inner))
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global (inherited) options
|
||||||
|
if c.HasAvailableInheritedFlags() {
|
||||||
|
inh := collectFlagRows(c.InheritedFlags())
|
||||||
|
if len(inh) > 0 {
|
||||||
|
b.WriteString(sectionPanel("Global options", renderTwoColPairs(inh, contentW), inner))
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderCommandQuickRef prints the same Usage / Flags / Global sections as help,
|
||||||
|
// for embedding after errors (stderr). outerW is typically InnerStderrWidth().
|
||||||
|
func RenderCommandQuickRef(c *cobra.Command, outerW int) string {
|
||||||
|
if c == nil || outerW < 40 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
syncFlags(c)
|
||||||
|
contentW := outerW - 6
|
||||||
|
if contentW < 36 {
|
||||||
|
contentW = 36
|
||||||
|
}
|
||||||
|
var b strings.Builder
|
||||||
|
usageBody := bodyStyle().MaxWidth(contentW).Render(styleUsageTokens(c.UseLine()))
|
||||||
|
b.WriteString(sectionPanel("Usage", usageBody, outerW))
|
||||||
|
b.WriteString("\n")
|
||||||
|
if len(c.Aliases) > 0 {
|
||||||
|
al := "Aliases: " + strings.Join(c.Aliases, ", ")
|
||||||
|
alBody := mutedStyle().MaxWidth(contentW).Render(al)
|
||||||
|
b.WriteString(sectionPanel("Aliases", alBody, outerW))
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
opts := collectFlagRows(c.LocalFlags())
|
||||||
|
if len(opts) > 0 {
|
||||||
|
title := "Options"
|
||||||
|
if !c.HasParent() {
|
||||||
|
title = "Flags"
|
||||||
|
}
|
||||||
|
b.WriteString(sectionPanel(title, renderTwoColPairs(opts, contentW), outerW))
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
if c.HasAvailableInheritedFlags() {
|
||||||
|
inh := collectFlagRows(c.InheritedFlags())
|
||||||
|
if len(inh) > 0 {
|
||||||
|
b.WriteString(sectionPanel("Global options", renderTwoColPairs(inh, contentW), outerW))
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncFlags(c *cobra.Command) {
|
||||||
|
_ = c.LocalFlags()
|
||||||
|
if c.HasAvailableInheritedFlags() {
|
||||||
|
_ = c.InheritedFlags()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func plainCommandHelp(c *cobra.Command) string {
|
||||||
|
desc := c.Long
|
||||||
|
if desc == "" {
|
||||||
|
desc = c.Short
|
||||||
|
}
|
||||||
|
desc = strings.TrimRight(desc, " \t\n\r")
|
||||||
|
var b strings.Builder
|
||||||
|
if desc != "" {
|
||||||
|
fmt.Fprintln(&b, desc)
|
||||||
|
fmt.Fprintln(&b)
|
||||||
|
}
|
||||||
|
if c.Runnable() || c.HasSubCommands() {
|
||||||
|
b.WriteString(c.UsageString())
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func helpIntro(c *cobra.Command) (head, sub string) {
|
||||||
|
head = strings.TrimSpace(c.Short)
|
||||||
|
long := strings.TrimSpace(c.Long)
|
||||||
|
if long == "" || long == head {
|
||||||
|
return head, ""
|
||||||
|
}
|
||||||
|
lines := strings.Split(long, "\n")
|
||||||
|
var rest []string
|
||||||
|
for i, ln := range lines {
|
||||||
|
ln = strings.TrimSpace(ln)
|
||||||
|
if ln == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if i == 0 && ln == head {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rest = append(rest, ln)
|
||||||
|
}
|
||||||
|
sub = strings.Join(rest, "\n")
|
||||||
|
return head, sub
|
||||||
|
}
|
||||||
|
|
||||||
|
func visibleSubcommands(c *cobra.Command) []*cobra.Command {
|
||||||
|
var out []*cobra.Command
|
||||||
|
for _, sub := range c.Commands() {
|
||||||
|
if sub.Hidden {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, sub)
|
||||||
|
}
|
||||||
|
sort.Slice(out, func(i, j int) bool { return out[i].Name() < out[j].Name() })
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func sectionPanel(title, body string, width int) string {
|
||||||
|
head := titleBarStyle().Render(title) + "\n\n"
|
||||||
|
return borderStyle().Width(width).Render(head + body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// styleUsageTokens highlights PicoClaw-blue command tokens and red <placeholders>/[groups].
|
||||||
|
func styleUsageTokens(s string) string {
|
||||||
|
var b strings.Builder
|
||||||
|
for len(s) > 0 {
|
||||||
|
ia := strings.Index(s, "<")
|
||||||
|
ib := strings.Index(s, "[")
|
||||||
|
next, kind := -1, 0 // 1 = angle, 2 = bracket
|
||||||
|
switch {
|
||||||
|
case ia >= 0 && (ib < 0 || ia < ib):
|
||||||
|
next, kind = ia, 1
|
||||||
|
case ib >= 0:
|
||||||
|
next, kind = ib, 2
|
||||||
|
}
|
||||||
|
if next < 0 {
|
||||||
|
b.WriteString(helpIdentStyle().Render(s))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if next > 0 {
|
||||||
|
b.WriteString(helpIdentStyle().Render(s[:next]))
|
||||||
|
}
|
||||||
|
s = s[next:]
|
||||||
|
if kind == 1 {
|
||||||
|
j := strings.Index(s, ">")
|
||||||
|
if j < 0 {
|
||||||
|
b.WriteString(helpIdentStyle().Render(s))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
b.WriteString(helpPlaceholderStyle().Render(s[:j+1]))
|
||||||
|
s = s[j+1:]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
j := strings.Index(s, "]")
|
||||||
|
if j < 0 {
|
||||||
|
b.WriteString(helpIdentStyle().Render(s))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
b.WriteString(helpPlaceholderStyle().Render(s[:j+1]))
|
||||||
|
s = s[j+1:]
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectFlagRows(fs *flag.FlagSet) [][2]string {
|
||||||
|
var names []string
|
||||||
|
seen := map[string][2]string{}
|
||||||
|
fs.VisitAll(func(f *flag.Flag) {
|
||||||
|
if f.Hidden {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
left := formatFlagLeft(f)
|
||||||
|
right := f.Usage
|
||||||
|
if f.Deprecated != "" {
|
||||||
|
right += " (deprecated: " + f.Deprecated + ")"
|
||||||
|
}
|
||||||
|
names = append(names, f.Name)
|
||||||
|
seen[f.Name] = [2]string{left, right}
|
||||||
|
})
|
||||||
|
sort.Strings(names)
|
||||||
|
rows := make([][2]string, 0, len(names))
|
||||||
|
for _, n := range names {
|
||||||
|
rows = append(rows, seen[n])
|
||||||
|
}
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatFlagLeft(f *flag.Flag) string {
|
||||||
|
if len(f.Shorthand) > 0 {
|
||||||
|
return "-" + f.Shorthand + ", --" + f.Name
|
||||||
|
}
|
||||||
|
return "--" + f.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderTwoColPairs(rows [][2]string, contentW int) string {
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
leftW := 0
|
||||||
|
for _, r := range rows {
|
||||||
|
if w := lipgloss.Width(r[0]); w > leftW {
|
||||||
|
leftW = w
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const minLeft, maxLeft = 16, 34
|
||||||
|
if leftW < minLeft {
|
||||||
|
leftW = minLeft
|
||||||
|
}
|
||||||
|
if leftW > maxLeft {
|
||||||
|
leftW = maxLeft
|
||||||
|
}
|
||||||
|
gap := " "
|
||||||
|
rightW := contentW - leftW - lipgloss.Width(gap)
|
||||||
|
if rightW < 24 {
|
||||||
|
rightW = 24
|
||||||
|
}
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
for _, r := range rows {
|
||||||
|
left := helpIdentStyle().Width(leftW).Align(lipgloss.Left).Render(r[0])
|
||||||
|
right := bodyStyle().Width(rightW).Render(strings.TrimSpace(r[1]))
|
||||||
|
b.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, left, gap, right))
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
return strings.TrimRight(b.String(), "\n")
|
||||||
|
}
|
||||||
74
cmd/picoclaw/internal/cliui/help_error.go
Normal file
74
cmd/picoclaw/internal/cliui/help_error.go
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
package cliui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FormatCLIError formats errors with the same boxed sections as help. When ctx
|
||||||
|
// is the command that was running when the error occurred, Usage / Flags panels
|
||||||
|
// are appended so styling matches picoclaw -h.
|
||||||
|
func FormatCLIError(msg string, ctx *cobra.Command) string {
|
||||||
|
msg = strings.TrimRight(msg, "\n")
|
||||||
|
if !UseFancyStderr() {
|
||||||
|
s := "Error: " + msg + "\n"
|
||||||
|
if ctx != nil && showErrHint(msg) {
|
||||||
|
s += "\n" + plainCommandHelp(ctx)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
w := InnerStderrWidth()
|
||||||
|
contentW := w - 6
|
||||||
|
if contentW < 36 {
|
||||||
|
contentW = 36
|
||||||
|
}
|
||||||
|
|
||||||
|
title := titleBarStyle().Render("Error") + "\n\n"
|
||||||
|
|
||||||
|
paras := strings.Split(msg, "\n")
|
||||||
|
var body strings.Builder
|
||||||
|
for i, p := range paras {
|
||||||
|
p = strings.TrimRight(p, " ")
|
||||||
|
if p == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
st := bodyStyle().Width(contentW)
|
||||||
|
if i > 0 {
|
||||||
|
body.WriteString("\n")
|
||||||
|
}
|
||||||
|
if i == 0 {
|
||||||
|
body.WriteString(st.Render(p))
|
||||||
|
} else {
|
||||||
|
body.WriteString(mutedStyle().Width(contentW).Render(p))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foot := ""
|
||||||
|
if showErrHint(msg) {
|
||||||
|
if ctx != nil {
|
||||||
|
foot = "\n\n" + mutedStyle().Width(contentW).
|
||||||
|
Render("Full command help: "+ctx.CommandPath()+" --help")
|
||||||
|
} else {
|
||||||
|
foot = "\n\n" + mutedStyle().Width(contentW).
|
||||||
|
Render("Tip: picoclaw --help · picoclaw <command> --help")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out := borderStyle().Width(w).Render(title+body.String()+foot) + "\n"
|
||||||
|
if ctx != nil && showErrHint(msg) {
|
||||||
|
if ref := RenderCommandQuickRef(ctx, w); ref != "" {
|
||||||
|
out += "\n" + ref
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func showErrHint(msg string) bool {
|
||||||
|
m := strings.ToLower(msg)
|
||||||
|
return strings.Contains(m, "unknown flag") ||
|
||||||
|
strings.Contains(m, "flag") ||
|
||||||
|
strings.Contains(m, "invalid") ||
|
||||||
|
strings.Contains(m, "required") ||
|
||||||
|
strings.Contains(m, "usage:")
|
||||||
|
}
|
||||||
110
cmd/picoclaw/internal/cliui/onboard.go
Normal file
110
cmd/picoclaw/internal/cliui/onboard.go
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
package cliui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PrintOnboardComplete prints the post-onboard “ready” message and next steps.
|
||||||
|
func PrintOnboardComplete(logo string, encrypt bool, configPath string) {
|
||||||
|
if !UseFancyLayout() {
|
||||||
|
printOnboardPlain(logo, encrypt, configPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
printOnboardFancy(logo, encrypt, configPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printOnboardPlain(logo string, encrypt bool, configPath string) {
|
||||||
|
fmt.Printf("\n%s picoclaw is ready!\n", logo)
|
||||||
|
fmt.Println("\nNext steps:")
|
||||||
|
if encrypt {
|
||||||
|
fmt.Println(" 1. Set your encryption passphrase before starting picoclaw:")
|
||||||
|
fmt.Println(" export PICOCLAW_KEY_PASSPHRASE=<your-passphrase> # Linux/macOS")
|
||||||
|
fmt.Println(" set PICOCLAW_KEY_PASSPHRASE=<your-passphrase> # Windows cmd")
|
||||||
|
fmt.Println("")
|
||||||
|
fmt.Println(" 2. Add your API key to", configPath)
|
||||||
|
} else {
|
||||||
|
fmt.Println(" 1. Add your API key to", configPath)
|
||||||
|
}
|
||||||
|
fmt.Println("")
|
||||||
|
fmt.Println(" Recommended:")
|
||||||
|
fmt.Println(" - OpenRouter: https://openrouter.ai/keys (access 100+ models)")
|
||||||
|
fmt.Println(" - Ollama: https://ollama.com (local, free)")
|
||||||
|
fmt.Println("")
|
||||||
|
fmt.Println(" See README.md for 17+ supported providers.")
|
||||||
|
fmt.Println("")
|
||||||
|
if encrypt {
|
||||||
|
fmt.Println(" 3. Chat: picoclaw agent -m \"Hello!\"")
|
||||||
|
} else {
|
||||||
|
fmt.Println(" 2. Chat: picoclaw agent -m \"Hello!\"")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printOnboardFancy(logo string, encrypt bool, configPath string) {
|
||||||
|
inner := InnerWidth()
|
||||||
|
box := borderStyle().MaxWidth(inner + 8)
|
||||||
|
|
||||||
|
ready := titleBarStyle().Render(logo+" picoclaw is ready!") + "\n"
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println(box.Width(inner).Render(strings.TrimSpace(ready)))
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
steps := buildOnboardingSteps(encrypt, configPath)
|
||||||
|
rec := recommendedBlock()
|
||||||
|
chat := chatStep(encrypt)
|
||||||
|
|
||||||
|
if UseColumnLayout() {
|
||||||
|
leftW := min(inner/2-2, 52)
|
||||||
|
rightW := inner - leftW - 4
|
||||||
|
if rightW < 36 {
|
||||||
|
rightW = 36
|
||||||
|
}
|
||||||
|
leftBlock := borderStyle().MaxWidth(leftW + 8).Width(leftW).
|
||||||
|
Render(titleBarStyle().Render("Next steps") + "\n\n" + bodyStyle().Width(leftW).Render(steps))
|
||||||
|
rightBlock := borderStyle().MaxWidth(rightW + 8).Width(rightW).
|
||||||
|
Render(mutedStyle().Bold(true).Render("Recommended") + "\n\n" + bodyStyle().Width(rightW).Render(rec))
|
||||||
|
gap := strings.Repeat(" ", 2)
|
||||||
|
fmt.Println(lipgloss.JoinHorizontal(lipgloss.Top, leftBlock, gap, rightBlock))
|
||||||
|
fmt.Println()
|
||||||
|
full := borderStyle().Width(inner).Render(bodyStyle().Width(inner - 4).Render(chat))
|
||||||
|
fmt.Println(full)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same order as plain output: numbered steps → recommended → chat line.
|
||||||
|
next := titleBarStyle().Render("Next steps") + "\n\n" +
|
||||||
|
bodyStyle().Width(inner - 4).Render(steps+"\n\n"+rec+"\n\n"+chat)
|
||||||
|
fmt.Println(borderStyle().Width(inner).Render(next))
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildOnboardingSteps(encrypt bool, configPath string) string {
|
||||||
|
var b strings.Builder
|
||||||
|
if encrypt {
|
||||||
|
b.WriteString("1. Set your encryption passphrase before starting picoclaw:\n")
|
||||||
|
b.WriteString(" export PICOCLAW_KEY_PASSPHRASE=<your-passphrase> # Linux/macOS\n")
|
||||||
|
b.WriteString(" set PICOCLAW_KEY_PASSPHRASE=<your-passphrase> # Windows cmd\n\n")
|
||||||
|
b.WriteString("2. Add your API key to\n ")
|
||||||
|
b.WriteString(configPath)
|
||||||
|
b.WriteString("\n")
|
||||||
|
} else {
|
||||||
|
b.WriteString("1. Add your API key to\n ")
|
||||||
|
b.WriteString(configPath)
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func recommendedBlock() string {
|
||||||
|
return "• OpenRouter: https://openrouter.ai/keys\n (access 100+ models)\n\n" +
|
||||||
|
"• Ollama: https://ollama.com\n (local, free)\n\n" +
|
||||||
|
"See README.md for 17+ supported providers."
|
||||||
|
}
|
||||||
|
|
||||||
|
func chatStep(encrypt bool) string {
|
||||||
|
if encrypt {
|
||||||
|
return "3. Chat:\n picoclaw agent -m \"Hello!\""
|
||||||
|
}
|
||||||
|
return "2. Chat:\n picoclaw agent -m \"Hello!\""
|
||||||
|
}
|
||||||
164
cmd/picoclaw/internal/cliui/status.go
Normal file
164
cmd/picoclaw/internal/cliui/status.go
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
package cliui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StatusReport is a structured status view for PrintStatus.
|
||||||
|
type StatusReport struct {
|
||||||
|
Logo string
|
||||||
|
Version string
|
||||||
|
Build string
|
||||||
|
ConfigPath string
|
||||||
|
ConfigOK bool
|
||||||
|
WorkspacePath string
|
||||||
|
WorkspaceOK bool
|
||||||
|
Model string
|
||||||
|
// ProviderNames and ProviderVals same length
|
||||||
|
ProviderNames []string
|
||||||
|
ProviderVals []string
|
||||||
|
OAuthLines []string // each full line "provider (method): state"
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintStatus renders picoclaw status (plain or fancy).
|
||||||
|
func PrintStatus(r StatusReport) {
|
||||||
|
if !UseFancyLayout() {
|
||||||
|
printStatusPlain(r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
printStatusFancy(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printStatusPlain(r StatusReport) {
|
||||||
|
fmt.Printf("%s picoclaw Status\n", r.Logo)
|
||||||
|
fmt.Printf("Version: %s\n", r.Version)
|
||||||
|
if r.Build != "" {
|
||||||
|
fmt.Printf("Build: %s\n", r.Build)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
printPathLine("Config", r.ConfigPath, r.ConfigOK)
|
||||||
|
printPathLine("Workspace", r.WorkspacePath, r.WorkspaceOK)
|
||||||
|
|
||||||
|
if r.ConfigOK {
|
||||||
|
fmt.Printf("Model: %s\n", r.Model)
|
||||||
|
for i := range r.ProviderNames {
|
||||||
|
fmt.Printf("%s: %s\n", r.ProviderNames[i], r.ProviderVals[i])
|
||||||
|
}
|
||||||
|
if len(r.OAuthLines) > 0 {
|
||||||
|
fmt.Println("\nOAuth/Token Auth:")
|
||||||
|
for _, line := range r.OAuthLines {
|
||||||
|
fmt.Printf(" %s\n", line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printPathLine(label, path string, ok bool) {
|
||||||
|
mark := "✗"
|
||||||
|
if ok {
|
||||||
|
mark = "✓"
|
||||||
|
}
|
||||||
|
fmt.Println(label+":", path, mark)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printStatusFancy(r StatusReport) {
|
||||||
|
inner := InnerWidth()
|
||||||
|
topBox := borderStyle().Width(inner)
|
||||||
|
|
||||||
|
var head strings.Builder
|
||||||
|
head.WriteString(titleBarStyle().Render(r.Logo + " picoclaw Status"))
|
||||||
|
head.WriteString("\n\n")
|
||||||
|
head.WriteString(kvKeyStyle().Render("Version") + " " + kvValStyle().Render(r.Version))
|
||||||
|
if r.Build != "" {
|
||||||
|
head.WriteString("\n")
|
||||||
|
head.WriteString(kvKeyStyle().Render("Build") + " " + kvValStyle().Render(r.Build))
|
||||||
|
}
|
||||||
|
fmt.Println(topBox.Render(head.String()))
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
if UseColumnLayout() && len(r.ProviderNames) > 0 && r.ConfigOK {
|
||||||
|
leftW := (inner - 2) / 2
|
||||||
|
rightW := inner - leftW - 2
|
||||||
|
pathsNarrow := pathStatusPanel(r, leftW)
|
||||||
|
prov := providerTablePanel(r, rightW)
|
||||||
|
gap := strings.Repeat(" ", 2)
|
||||||
|
fmt.Println(lipgloss.JoinHorizontal(lipgloss.Top, pathsNarrow, gap, prov))
|
||||||
|
} else {
|
||||||
|
fmt.Println(pathStatusPanel(r, inner))
|
||||||
|
if len(r.ProviderNames) > 0 && r.ConfigOK {
|
||||||
|
fmt.Println(providerTablePanel(r, inner))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(r.OAuthLines) > 0 && r.ConfigOK {
|
||||||
|
var ob strings.Builder
|
||||||
|
ob.WriteString(titleBarStyle().Render("OAuth / token auth") + "\n\n")
|
||||||
|
for _, line := range r.OAuthLines {
|
||||||
|
ob.WriteString(" • " + line + "\n")
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println(borderStyle().Width(inner).Render(ob.String()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pathStatusPanel(r StatusReport, inner int) string {
|
||||||
|
cfgMark := statusMark(r.ConfigOK)
|
||||||
|
wsMark := statusMark(r.WorkspaceOK)
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString(kvKeyStyle().Render("Config") + "\n")
|
||||||
|
b.WriteString(mutedStyle().Render(r.ConfigPath))
|
||||||
|
b.WriteString(" " + cfgMark + "\n\n")
|
||||||
|
b.WriteString(kvKeyStyle().Render("Workspace") + "\n")
|
||||||
|
b.WriteString(mutedStyle().Render(r.WorkspacePath))
|
||||||
|
b.WriteString(" " + wsMark + "\n")
|
||||||
|
if r.ConfigOK {
|
||||||
|
b.WriteString("\n")
|
||||||
|
b.WriteString(kvKeyStyle().Render("Model") + " " + kvValStyle().Render(r.Model))
|
||||||
|
}
|
||||||
|
return borderStyle().Width(inner).Render(b.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func statusMark(ok bool) string {
|
||||||
|
if ok {
|
||||||
|
return lipgloss.NewStyle().Foreground(colorOK).Render("✓")
|
||||||
|
}
|
||||||
|
return lipgloss.NewStyle().Foreground(accentRed).Render("✗")
|
||||||
|
}
|
||||||
|
|
||||||
|
func providerTablePanel(r StatusReport, colW int) string {
|
||||||
|
if len(r.ProviderNames) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
keyW := min(22, colW/3)
|
||||||
|
if keyW < 14 {
|
||||||
|
keyW = 14
|
||||||
|
}
|
||||||
|
valW := colW - keyW - 3
|
||||||
|
if valW < 12 {
|
||||||
|
valW = 12
|
||||||
|
}
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString(titleBarStyle().Render("Providers & local") + "\n\n")
|
||||||
|
for i := range r.ProviderNames {
|
||||||
|
k := lipgloss.NewStyle().Foreground(accentBlue).Bold(true).Width(keyW).Render(r.ProviderNames[i])
|
||||||
|
v := styleProviderVal(r.ProviderVals[i]).Width(valW).Render(r.ProviderVals[i])
|
||||||
|
b.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, k, " ", v))
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
return borderStyle().Width(colW).Render(strings.TrimRight(b.String(), "\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func styleProviderVal(s string) lipgloss.Style {
|
||||||
|
if s == "✓" || strings.HasPrefix(s, "✓ ") {
|
||||||
|
return lipgloss.NewStyle().Foreground(colorOK)
|
||||||
|
}
|
||||||
|
if s == "not set" {
|
||||||
|
return mutedStyle()
|
||||||
|
}
|
||||||
|
return lipgloss.NewStyle()
|
||||||
|
}
|
||||||
61
cmd/picoclaw/internal/cliui/version.go
Normal file
61
cmd/picoclaw/internal/cliui/version.go
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
package cliui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PrintVersion prints version, optional build info, and Go toolchain line.
|
||||||
|
func PrintVersion(logo, versionLine string, build, goVer string) {
|
||||||
|
if !UseFancyLayout() {
|
||||||
|
fmt.Printf("%s %s\n", logo, versionLine)
|
||||||
|
if build != "" {
|
||||||
|
fmt.Printf(" Build: %s\n", build)
|
||||||
|
}
|
||||||
|
if goVer != "" {
|
||||||
|
fmt.Printf(" Go: %s\n", goVer)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
inner := InnerWidth()
|
||||||
|
box := borderStyle().Width(inner)
|
||||||
|
|
||||||
|
if UseColumnLayout() {
|
||||||
|
leftCol := kvKeyStyle().Width(12).Align(lipgloss.Right)
|
||||||
|
rightW := inner - 16
|
||||||
|
rightStyle := kvValStyle().Width(rightW)
|
||||||
|
|
||||||
|
rows := [][]string{
|
||||||
|
{leftCol.Render("Version"), rightStyle.Render(versionLine)},
|
||||||
|
}
|
||||||
|
if build != "" {
|
||||||
|
rows = append(rows, []string{leftCol.Render("Build"), rightStyle.Render(build)})
|
||||||
|
}
|
||||||
|
if goVer != "" {
|
||||||
|
rows = append(rows, []string{leftCol.Render("Go"), rightStyle.Render(goVer)})
|
||||||
|
}
|
||||||
|
var body strings.Builder
|
||||||
|
for _, r := range rows {
|
||||||
|
body.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, r[0], " ", r[1]))
|
||||||
|
body.WriteString("\n")
|
||||||
|
}
|
||||||
|
header := titleBarStyle().Render(logo+" picoclaw") + "\n\n"
|
||||||
|
fmt.Println(box.Render(header + body.String()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var lines []string
|
||||||
|
lines = append(lines, titleBarStyle().Render(logo+" picoclaw"))
|
||||||
|
lines = append(lines, "")
|
||||||
|
lines = append(lines, kvKeyStyle().Render("Version")+" "+kvValStyle().Render(versionLine))
|
||||||
|
if build != "" {
|
||||||
|
lines = append(lines, kvKeyStyle().Render("Build")+" "+kvValStyle().Render(build))
|
||||||
|
}
|
||||||
|
if goVer != "" {
|
||||||
|
lines = append(lines, kvKeyStyle().Render("Go")+" "+kvValStyle().Render(goVer))
|
||||||
|
}
|
||||||
|
fmt.Println(box.Render(strings.Join(lines, "\n")))
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
|
||||||
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/cliui"
|
||||||
"github.com/sipeed/picoclaw/pkg/config"
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
"github.com/sipeed/picoclaw/pkg/credential"
|
"github.com/sipeed/picoclaw/pkg/credential"
|
||||||
)
|
)
|
||||||
|
|
@ -79,29 +80,7 @@ func onboard(encrypt bool) {
|
||||||
workspace := cfg.WorkspacePath()
|
workspace := cfg.WorkspacePath()
|
||||||
createWorkspaceTemplates(workspace)
|
createWorkspaceTemplates(workspace)
|
||||||
|
|
||||||
fmt.Printf("\n%s picoclaw is ready!\n", internal.Logo)
|
cliui.PrintOnboardComplete(internal.Logo, encrypt, configPath)
|
||||||
fmt.Println("\nNext steps:")
|
|
||||||
if encrypt {
|
|
||||||
fmt.Println(" 1. Set your encryption passphrase before starting picoclaw:")
|
|
||||||
fmt.Println(" export PICOCLAW_KEY_PASSPHRASE=<your-passphrase> # Linux/macOS")
|
|
||||||
fmt.Println(" set PICOCLAW_KEY_PASSPHRASE=<your-passphrase> # Windows cmd")
|
|
||||||
fmt.Println("")
|
|
||||||
fmt.Println(" 2. Add your API key to", configPath)
|
|
||||||
} else {
|
|
||||||
fmt.Println(" 1. Add your API key to", configPath)
|
|
||||||
}
|
|
||||||
fmt.Println("")
|
|
||||||
fmt.Println(" Recommended:")
|
|
||||||
fmt.Println(" - OpenRouter: https://openrouter.ai/keys (access 100+ models)")
|
|
||||||
fmt.Println(" - Ollama: https://ollama.com (local, free)")
|
|
||||||
fmt.Println("")
|
|
||||||
fmt.Println(" See README.md for 17+ supported providers.")
|
|
||||||
fmt.Println("")
|
|
||||||
if encrypt {
|
|
||||||
fmt.Println(" 3. Chat: picoclaw agent -m \"Hello!\"")
|
|
||||||
} else {
|
|
||||||
fmt.Println(" 2. Chat: picoclaw agent -m \"Hello!\"")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// promptPassphrase reads the encryption passphrase twice from the terminal
|
// promptPassphrase reads the encryption passphrase twice from the terminal
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
|
||||||
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/cliui"
|
||||||
"github.com/sipeed/picoclaw/pkg/auth"
|
"github.com/sipeed/picoclaw/pkg/auth"
|
||||||
"github.com/sipeed/picoclaw/pkg/config"
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
)
|
)
|
||||||
|
|
@ -17,43 +18,104 @@ func statusCmd() {
|
||||||
}
|
}
|
||||||
|
|
||||||
configPath := internal.GetConfigPath()
|
configPath := internal.GetConfigPath()
|
||||||
|
|
||||||
fmt.Printf("%s picoclaw Status\n", internal.Logo)
|
|
||||||
fmt.Printf("Version: %s\n", config.FormatVersion())
|
|
||||||
build, _ := config.FormatBuildInfo()
|
build, _ := config.FormatBuildInfo()
|
||||||
if build != "" {
|
|
||||||
fmt.Printf("Build: %s\n", build)
|
|
||||||
}
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
if _, err := os.Stat(configPath); err == nil {
|
_, configStatErr := os.Stat(configPath)
|
||||||
fmt.Println("Config:", configPath, "✓")
|
configOK := configStatErr == nil
|
||||||
} else {
|
|
||||||
fmt.Println("Config:", configPath, "✗")
|
|
||||||
}
|
|
||||||
|
|
||||||
workspace := cfg.WorkspacePath()
|
workspace := cfg.WorkspacePath()
|
||||||
if _, err := os.Stat(workspace); err == nil {
|
_, wsErr := os.Stat(workspace)
|
||||||
fmt.Println("Workspace:", workspace, "✓")
|
wsOK := wsErr == nil
|
||||||
} else {
|
|
||||||
fmt.Println("Workspace:", workspace, "✗")
|
report := cliui.StatusReport{
|
||||||
|
Logo: internal.Logo,
|
||||||
|
Version: config.FormatVersion(),
|
||||||
|
Build: build,
|
||||||
|
ConfigPath: configPath,
|
||||||
|
ConfigOK: configOK,
|
||||||
|
WorkspacePath: workspace,
|
||||||
|
WorkspaceOK: wsOK,
|
||||||
|
Model: cfg.Agents.Defaults.GetModelName(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := os.Stat(configPath); err == nil {
|
if configOK {
|
||||||
fmt.Printf("Model: %s\n", cfg.Agents.Defaults.GetModelName())
|
hasOpenRouter := cfg.Providers.OpenRouter.APIKey != ""
|
||||||
|
hasAnthropic := cfg.Providers.Anthropic.APIKey != ""
|
||||||
|
hasOpenAI := cfg.Providers.OpenAI.APIKey != ""
|
||||||
|
hasGemini := cfg.Providers.Gemini.APIKey != ""
|
||||||
|
hasZhipu := cfg.Providers.Zhipu.APIKey != ""
|
||||||
|
hasQwen := cfg.Providers.Qwen.APIKey != ""
|
||||||
|
hasGroq := cfg.Providers.Groq.APIKey != ""
|
||||||
|
hasVLLM := cfg.Providers.VLLM.APIBase != ""
|
||||||
|
hasMoonshot := cfg.Providers.Moonshot.APIKey != ""
|
||||||
|
hasDeepSeek := cfg.Providers.DeepSeek.APIKey != ""
|
||||||
|
hasVolcEngine := cfg.Providers.VolcEngine.APIKey != ""
|
||||||
|
hasNvidia := cfg.Providers.Nvidia.APIKey != ""
|
||||||
|
hasOllama := cfg.Providers.Ollama.APIBase != ""
|
||||||
|
|
||||||
|
status := func(enabled bool) string {
|
||||||
|
if enabled {
|
||||||
|
return "✓"
|
||||||
|
}
|
||||||
|
return "not set"
|
||||||
|
}
|
||||||
|
|
||||||
|
report.ProviderNames = []string{
|
||||||
|
"OpenRouter API",
|
||||||
|
"Anthropic API",
|
||||||
|
"OpenAI API",
|
||||||
|
"Gemini API",
|
||||||
|
"Zhipu API",
|
||||||
|
"Qwen API",
|
||||||
|
"Groq API",
|
||||||
|
"Moonshot API",
|
||||||
|
"DeepSeek API",
|
||||||
|
"VolcEngine API",
|
||||||
|
"Nvidia API",
|
||||||
|
}
|
||||||
|
report.ProviderVals = []string{
|
||||||
|
status(hasOpenRouter),
|
||||||
|
status(hasAnthropic),
|
||||||
|
status(hasOpenAI),
|
||||||
|
status(hasGemini),
|
||||||
|
status(hasZhipu),
|
||||||
|
status(hasQwen),
|
||||||
|
status(hasGroq),
|
||||||
|
status(hasMoonshot),
|
||||||
|
status(hasDeepSeek),
|
||||||
|
status(hasVolcEngine),
|
||||||
|
status(hasNvidia),
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasVLLM {
|
||||||
|
report.ProviderNames = append(report.ProviderNames, "vLLM / local")
|
||||||
|
report.ProviderVals = append(report.ProviderVals, "✓ "+cfg.Providers.VLLM.APIBase)
|
||||||
|
} else {
|
||||||
|
report.ProviderNames = append(report.ProviderNames, "vLLM / local")
|
||||||
|
report.ProviderVals = append(report.ProviderVals, "not set")
|
||||||
|
}
|
||||||
|
if hasOllama {
|
||||||
|
report.ProviderNames = append(report.ProviderNames, "Ollama")
|
||||||
|
report.ProviderVals = append(report.ProviderVals, "✓ "+cfg.Providers.Ollama.APIBase)
|
||||||
|
} else {
|
||||||
|
report.ProviderNames = append(report.ProviderNames, "Ollama")
|
||||||
|
report.ProviderVals = append(report.ProviderVals, "not set")
|
||||||
|
}
|
||||||
|
|
||||||
store, _ := auth.LoadStore()
|
store, _ := auth.LoadStore()
|
||||||
if store != nil && len(store.Credentials) > 0 {
|
if store != nil && len(store.Credentials) > 0 {
|
||||||
fmt.Println("\nOAuth/Token Auth:")
|
|
||||||
for provider, cred := range store.Credentials {
|
for provider, cred := range store.Credentials {
|
||||||
status := "authenticated"
|
st := "authenticated"
|
||||||
if cred.IsExpired() {
|
if cred.IsExpired() {
|
||||||
status = "expired"
|
st = "expired"
|
||||||
} else if cred.NeedsRefresh() {
|
} else if cred.NeedsRefresh() {
|
||||||
status = "needs refresh"
|
st = "needs refresh"
|
||||||
}
|
}
|
||||||
fmt.Printf(" %s (%s): %s\n", provider, cred.AuthMethod, status)
|
report.OAuthLines = append(report.OAuthLines,
|
||||||
|
fmt.Sprintf("%s (%s): %s", provider, cred.AuthMethod, st))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cliui.PrintStatus(report)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
package version
|
package version
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
|
||||||
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/cliui"
|
||||||
"github.com/sipeed/picoclaw/pkg/config"
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -23,12 +22,6 @@ func NewVersionCommand() *cobra.Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
func printVersion() {
|
func printVersion() {
|
||||||
fmt.Printf("%s picoclaw %s\n", internal.Logo, config.FormatVersion())
|
|
||||||
build, goVer := config.FormatBuildInfo()
|
build, goVer := config.FormatBuildInfo()
|
||||||
if build != "" {
|
cliui.PrintVersion(internal.Logo, "picoclaw "+config.FormatVersion(), build, goVer)
|
||||||
fmt.Printf(" Build: %s\n", build)
|
|
||||||
}
|
|
||||||
if goVer != "" {
|
|
||||||
fmt.Printf(" Go: %s\n", goVer)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
|
||||||
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/cliui"
|
||||||
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/agent"
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/agent"
|
||||||
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/auth"
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/auth"
|
||||||
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/cron"
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/cron"
|
||||||
|
|
@ -26,15 +27,56 @@ import (
|
||||||
"github.com/sipeed/picoclaw/pkg/config"
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var rootNoColor bool
|
||||||
|
|
||||||
|
func syncCliUIColor(root *cobra.Command) {
|
||||||
|
no, _ := root.PersistentFlags().GetBool("no-color")
|
||||||
|
cliui.Init(no || os.Getenv("NO_COLOR") != "" || os.Getenv("TERM") == "dumb")
|
||||||
|
}
|
||||||
|
|
||||||
|
// earlyColorDisabled matches lipgloss/banner behavior from env and argv before Cobra parses flags.
|
||||||
|
func earlyColorDisabled() bool {
|
||||||
|
if os.Getenv("NO_COLOR") != "" || os.Getenv("TERM") == "dumb" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for i := 1; i < len(os.Args); i++ {
|
||||||
|
if os.Args[i] == "--no-color" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func NewPicoclawCommand() *cobra.Command {
|
func NewPicoclawCommand() *cobra.Command {
|
||||||
short := fmt.Sprintf("%s picoclaw - Personal AI Assistant v%s\n\n", internal.Logo, config.GetVersion())
|
short := fmt.Sprintf("%s PicoClaw — personal AI assistant", internal.Logo)
|
||||||
|
long := fmt.Sprintf(`%s PicoClaw is a lightweight personal AI assistant.
|
||||||
|
|
||||||
|
Version: %s`, internal.Logo, config.FormatVersion())
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "picoclaw",
|
Use: "picoclaw",
|
||||||
Short: short,
|
Short: short,
|
||||||
Example: "picoclaw version",
|
Long: long,
|
||||||
|
Example: `picoclaw version
|
||||||
|
picoclaw onboard
|
||||||
|
picoclaw --no-color status`,
|
||||||
|
SilenceErrors: true,
|
||||||
|
// Avoid plain UsageString() on stderr/stdout when a command fails; cliui
|
||||||
|
// renders matching panels on stderr instead.
|
||||||
|
SilenceUsage: true,
|
||||||
|
PersistentPreRun: func(c *cobra.Command, _ []string) {
|
||||||
|
syncCliUIColor(c.Root())
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cmd.PersistentFlags().BoolVar(&rootNoColor, "no-color", false,
|
||||||
|
"Disable colors (boxed layout unchanged)")
|
||||||
|
|
||||||
|
cmd.SetHelpFunc(func(c *cobra.Command, _ []string) {
|
||||||
|
syncCliUIColor(c.Root())
|
||||||
|
fmt.Fprint(c.OutOrStdout(), cliui.RenderCommandHelp(c))
|
||||||
|
})
|
||||||
|
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
onboard.NewOnboardCommand(),
|
onboard.NewOnboardCommand(),
|
||||||
agent.NewAgentCommand(),
|
agent.NewAgentCommand(),
|
||||||
|
|
@ -62,12 +104,29 @@ const (
|
||||||
colorBlue + "██║ ██║╚██████╗╚██████╔╝" + colorRed + "╚██████╗███████╗██║ ██║╚███╔███╔╝\n" +
|
colorBlue + "██║ ██║╚██████╗╚██████╔╝" + colorRed + "╚██████╗███████╗██║ ██║╚███╔███╔╝\n" +
|
||||||
colorBlue + "╚═╝ ╚═╝ ╚═════╝ ╚═════╝ " + colorRed + " ╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝\n " +
|
colorBlue + "╚═╝ ╚═╝ ╚═════╝ ╚═════╝ " + colorRed + " ╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝\n " +
|
||||||
"\033[0m\r\n"
|
"\033[0m\r\n"
|
||||||
|
plainBanner = "\r\n" +
|
||||||
|
"██████╗ ██╗ ██████╗ ██████╗ ██████╗██╗ █████╗ ██╗ ██╗\n" +
|
||||||
|
"██╔══██╗██║██╔════╝██╔═══██╗██╔════╝██║ ██╔══██╗██║ ██║\n" +
|
||||||
|
"██████╔╝██║██║ ██║ ██║██║ ██║ ███████║██║ █╗ ██║\n" +
|
||||||
|
"██╔═══╝ ██║██║ ██║ ██║██║ ██║ ██╔══██║██║███╗██║\n" +
|
||||||
|
"██║ ██║╚██████╗╚██████╔╝╚██████╗███████╗██║ ██║╚███╔███╔╝\n" +
|
||||||
|
"╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝\n " +
|
||||||
|
"\r\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
fmt.Printf("%s", banner)
|
cliui.Init(earlyColorDisabled())
|
||||||
|
|
||||||
|
if earlyColorDisabled() {
|
||||||
|
fmt.Print(plainBanner)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("%s", banner)
|
||||||
|
}
|
||||||
cmd := NewPicoclawCommand()
|
cmd := NewPicoclawCommand()
|
||||||
if err := cmd.Execute(); err != nil {
|
last, err := cmd.ExecuteC()
|
||||||
|
if err != nil {
|
||||||
|
syncCliUIColor(cmd)
|
||||||
|
fmt.Fprint(os.Stderr, cliui.FormatCLIError(err.Error(), last))
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package main
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
@ -17,20 +18,22 @@ func TestNewPicoclawCommand(t *testing.T) {
|
||||||
|
|
||||||
require.NotNil(t, cmd)
|
require.NotNil(t, cmd)
|
||||||
|
|
||||||
short := fmt.Sprintf("%s picoclaw - Personal AI Assistant v%s\n\n", internal.Logo, config.GetVersion())
|
short := fmt.Sprintf("%s PicoClaw — personal AI assistant", internal.Logo)
|
||||||
|
longHas := strings.Contains(cmd.Long, config.FormatVersion())
|
||||||
|
|
||||||
assert.Equal(t, "picoclaw", cmd.Use)
|
assert.Equal(t, "picoclaw", cmd.Use)
|
||||||
assert.Equal(t, short, cmd.Short)
|
assert.Equal(t, short, cmd.Short)
|
||||||
|
assert.True(t, longHas)
|
||||||
|
|
||||||
assert.True(t, cmd.HasSubCommands())
|
assert.True(t, cmd.HasSubCommands())
|
||||||
assert.True(t, cmd.HasAvailableSubCommands())
|
assert.True(t, cmd.HasAvailableSubCommands())
|
||||||
|
|
||||||
assert.False(t, cmd.HasFlags())
|
assert.True(t, cmd.PersistentFlags().Lookup("no-color") != nil)
|
||||||
|
|
||||||
assert.Nil(t, cmd.Run)
|
assert.Nil(t, cmd.Run)
|
||||||
assert.Nil(t, cmd.RunE)
|
assert.Nil(t, cmd.RunE)
|
||||||
|
|
||||||
assert.Nil(t, cmd.PersistentPreRun)
|
assert.NotNil(t, cmd.PersistentPreRun)
|
||||||
assert.Nil(t, cmd.PersistentPostRun)
|
assert.Nil(t, cmd.PersistentPostRun)
|
||||||
|
|
||||||
allowedCommands := []string{
|
allowedCommands := []string{
|
||||||
|
|
|
||||||
11
go.mod
11
go.mod
|
|
@ -13,6 +13,7 @@ require (
|
||||||
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.50.4
|
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.50.4
|
||||||
github.com/bwmarrin/discordgo v0.29.0
|
github.com/bwmarrin/discordgo v0.29.0
|
||||||
github.com/caarlos0/env/v11 v11.4.0
|
github.com/caarlos0/env/v11 v11.4.0
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
github.com/creack/pty v1.1.24
|
github.com/creack/pty v1.1.24
|
||||||
github.com/ergochat/irc-go v0.6.0
|
github.com/ergochat/irc-go v0.6.0
|
||||||
github.com/ergochat/readline v0.1.3
|
github.com/ergochat/readline v0.1.3
|
||||||
|
|
@ -24,6 +25,7 @@ require (
|
||||||
github.com/larksuite/oapi-sdk-go/v3 v3.5.3
|
github.com/larksuite/oapi-sdk-go/v3 v3.5.3
|
||||||
github.com/mdp/qrterminal/v3 v3.2.1
|
github.com/mdp/qrterminal/v3 v3.2.1
|
||||||
github.com/modelcontextprotocol/go-sdk v1.4.1
|
github.com/modelcontextprotocol/go-sdk v1.4.1
|
||||||
|
github.com/muesli/termenv v0.16.0
|
||||||
github.com/mymmrac/telego v1.7.0
|
github.com/mymmrac/telego v1.7.0
|
||||||
github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1
|
github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1
|
||||||
github.com/openai/openai-go/v3 v3.22.0
|
github.com/openai/openai-go/v3 v3.22.0
|
||||||
|
|
@ -31,6 +33,7 @@ require (
|
||||||
github.com/rs/zerolog v1.34.0
|
github.com/rs/zerolog v1.34.0
|
||||||
github.com/slack-go/slack v0.17.3
|
github.com/slack-go/slack v0.17.3
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
|
github.com/spf13/pflag v1.0.10
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/tencent-connect/botgo v0.2.1
|
github.com/tencent-connect/botgo v0.2.1
|
||||||
go.mau.fi/util v0.9.7
|
go.mau.fi/util v0.9.7
|
||||||
|
|
@ -60,7 +63,12 @@ require (
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 // indirect
|
||||||
github.com/aws/smithy-go v1.24.2 // indirect
|
github.com/aws/smithy-go v1.24.2 // indirect
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/beeper/argo-go v1.1.2 // indirect
|
github.com/beeper/argo-go v1.1.2 // indirect
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||||
|
github.com/charmbracelet/x/ansi v0.8.0 // indirect
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
||||||
|
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||||
github.com/coder/websocket v1.8.14 // indirect
|
github.com/coder/websocket v1.8.14 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
|
@ -73,6 +81,7 @@ require (
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.34 // indirect
|
github.com/mattn/go-sqlite3 v1.14.34 // indirect
|
||||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
github.com/petermattis/goid v0.0.0-20260226131333-17d1149c6ac6 // indirect
|
github.com/petermattis/goid v0.0.0-20260226131333-17d1149c6ac6 // indirect
|
||||||
|
|
@ -81,8 +90,8 @@ require (
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/segmentio/asm v1.1.3 // indirect
|
github.com/segmentio/asm v1.1.3 // indirect
|
||||||
github.com/segmentio/encoding v0.5.4 // indirect
|
github.com/segmentio/encoding v0.5.4 // indirect
|
||||||
github.com/spf13/pflag v1.0.10 // indirect
|
|
||||||
github.com/vektah/gqlparser/v2 v2.5.27 // indirect
|
github.com/vektah/gqlparser/v2 v2.5.27 // indirect
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
go.mau.fi/libsignal v0.2.1 // indirect
|
go.mau.fi/libsignal v0.2.1 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||||
|
|
|
||||||
19
go.sum
19
go.sum
|
|
@ -51,6 +51,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk=
|
||||||
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
|
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
|
||||||
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||||
|
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/beeper/argo-go v1.1.2 h1:UQI2G8F+NLfGTOmTUI0254pGKx/HUU/etbUGTJv91Fs=
|
github.com/beeper/argo-go v1.1.2 h1:UQI2G8F+NLfGTOmTUI0254pGKx/HUU/etbUGTJv91Fs=
|
||||||
github.com/beeper/argo-go v1.1.2/go.mod h1:M+LJAnyowKVQ6Rdj6XYGEn+qcVFkb3R/MUpqkGR0hM4=
|
github.com/beeper/argo-go v1.1.2/go.mod h1:M+LJAnyowKVQ6Rdj6XYGEn+qcVFkb3R/MUpqkGR0hM4=
|
||||||
github.com/bwmarrin/discordgo v0.29.0 h1:FmWeXFaKUwrcL3Cx65c20bTRW+vOb6k8AnaP+EgjDno=
|
github.com/bwmarrin/discordgo v0.29.0 h1:FmWeXFaKUwrcL3Cx65c20bTRW+vOb6k8AnaP+EgjDno=
|
||||||
|
|
@ -65,6 +67,16 @@ github.com/caarlos0/env/v11 v11.4.0 h1:Kcb6t5kIIr4XkoQC9AF2j+8E1Jsrl3Wz/hhm1LtoG
|
||||||
github.com/caarlos0/env/v11 v11.4.0/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
|
github.com/caarlos0/env/v11 v11.4.0/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
|
||||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||||
|
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
|
||||||
|
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||||
|
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||||
|
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
||||||
|
|
@ -180,12 +192,16 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
|
||||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
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-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
|
github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
|
||||||
github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4=
|
github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4=
|
||||||
github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU=
|
github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU=
|
||||||
github.com/modelcontextprotocol/go-sdk v1.4.1 h1:M4x9GyIPj+HoIlHNGpK2hq5o3BFhC+78PkEaldQRphc=
|
github.com/modelcontextprotocol/go-sdk v1.4.1 h1:M4x9GyIPj+HoIlHNGpK2hq5o3BFhC+78PkEaldQRphc=
|
||||||
github.com/modelcontextprotocol/go-sdk v1.4.1/go.mod h1:Bo/mS87hPQqHSRkMv4dQq1XCu6zv4INdXnFZabkNU6s=
|
github.com/modelcontextprotocol/go-sdk v1.4.1/go.mod h1:Bo/mS87hPQqHSRkMv4dQq1XCu6zv4INdXnFZabkNU6s=
|
||||||
|
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/mymmrac/telego v1.7.0 h1:yRO/l00tFGG4nY66ufUKb4ARqv7qx9+LsjQv/b0NEyo=
|
github.com/mymmrac/telego v1.7.0 h1:yRO/l00tFGG4nY66ufUKb4ARqv7qx9+LsjQv/b0NEyo=
|
||||||
github.com/mymmrac/telego v1.7.0/go.mod h1:pdLV346EgVuq7Xrh3kMggeBiazeHhsdEoK0RTEOPXRM=
|
github.com/mymmrac/telego v1.7.0/go.mod h1:pdLV346EgVuq7Xrh3kMggeBiazeHhsdEoK0RTEOPXRM=
|
||||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
|
|
@ -212,6 +228,7 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94
|
||||||
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/tview v0.42.0 h1:b/ftp+RxtDsHSaynXTbJb+/n/BxDEi+W3UfF5jILK6c=
|
github.com/rivo/tview v0.42.0 h1:b/ftp+RxtDsHSaynXTbJb+/n/BxDEi+W3UfF5jILK6c=
|
||||||
github.com/rivo/tview v0.42.0/go.mod h1:cSfIYfhpSGCjp3r/ECJb+GKS7cGJnqV8vfjQPwoXyfY=
|
github.com/rivo/tview v0.42.0/go.mod h1:cSfIYfhpSGCjp3r/ECJb+GKS7cGJnqV8vfjQPwoXyfY=
|
||||||
|
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/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
|
|
@ -271,6 +288,8 @@ github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADT
|
||||||
github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE=
|
github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE=
|
||||||
github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s=
|
github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s=
|
||||||
github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
|
github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
|
||||||
|
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/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue