System prompt gets a one-line summary (~200 chars): OS, CPU, RAM, GPU, top runtimes, package count, PATH command count. Full details available on demand via system_info tool with sections: runtimes, packages, tools, hardware, all. LLM calls the tool when it needs specifics — saves thousands of tokens per request. Hardware detection: CPU model, core count, total RAM, GPU via lspci. Package manager: pacman/apt/dnf/brew with dev package filtering. PATH scan: 5541 executables. Runtime probing: 22 detected.
455 lines
12 KiB
Go
455 lines
12 KiB
Go
package bash
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const inventoryTimeout = 15 * time.Second
|
|
|
|
// SystemInventory holds dynamically discovered system information.
|
|
type SystemInventory struct {
|
|
// Core
|
|
OS string
|
|
Shell string
|
|
Hardware HardwareInfo
|
|
|
|
// Discovered
|
|
Tools []string // all executables found in PATH
|
|
Runtimes []Runtime // detected runtimes with versions
|
|
PackageCount int // total installed packages
|
|
PackageMgr string // detected package manager
|
|
DevPackages []string // development-relevant packages
|
|
}
|
|
|
|
type HardwareInfo struct {
|
|
CPU string
|
|
Cores int
|
|
MemTotal string
|
|
GPU string
|
|
}
|
|
|
|
type Runtime struct {
|
|
Name string
|
|
Version string
|
|
}
|
|
|
|
// HarvestInventory dynamically discovers system info.
|
|
func HarvestInventory(ctx context.Context) *SystemInventory {
|
|
ctx, cancel := context.WithTimeout(ctx, inventoryTimeout)
|
|
defer cancel()
|
|
|
|
inv := &SystemInventory{
|
|
OS: detectOS(ctx),
|
|
Shell: os.Getenv("SHELL"),
|
|
Hardware: detectHardware(ctx),
|
|
}
|
|
|
|
inv.Tools = scanPATH()
|
|
inv.Runtimes = probeRuntimes(ctx, inv.Tools)
|
|
inv.PackageMgr, inv.PackageCount, inv.DevPackages = queryPackageManager(ctx)
|
|
|
|
return inv
|
|
}
|
|
|
|
// Summary returns a compact one-paragraph description for the system prompt.
|
|
// Minimal tokens — just enough for the LLM to know the system's capabilities.
|
|
func (inv *SystemInventory) Summary() string {
|
|
if inv == nil {
|
|
return ""
|
|
}
|
|
|
|
var b strings.Builder
|
|
b.WriteString("System: ")
|
|
|
|
if inv.OS != "" {
|
|
b.WriteString(inv.OS)
|
|
}
|
|
if inv.Shell != "" {
|
|
fmt.Fprintf(&b, ", %s", filepath.Base(inv.Shell))
|
|
}
|
|
if inv.Hardware.CPU != "" {
|
|
fmt.Fprintf(&b, ". CPU: %s (%d cores)", inv.Hardware.CPU, inv.Hardware.Cores)
|
|
}
|
|
if inv.Hardware.MemTotal != "" {
|
|
fmt.Fprintf(&b, ", RAM: %s", inv.Hardware.MemTotal)
|
|
}
|
|
if inv.Hardware.GPU != "" {
|
|
fmt.Fprintf(&b, ", GPU: %s", inv.Hardware.GPU)
|
|
}
|
|
|
|
// Top runtimes (max 6)
|
|
if len(inv.Runtimes) > 0 {
|
|
top := inv.Runtimes
|
|
if len(top) > 6 {
|
|
top = top[:6]
|
|
}
|
|
names := make([]string, len(top))
|
|
for i, rt := range top {
|
|
names[i] = rt.Name
|
|
}
|
|
fmt.Fprintf(&b, ". Runtimes: %s", strings.Join(names, ", "))
|
|
if len(inv.Runtimes) > 6 {
|
|
fmt.Fprintf(&b, " +%d more", len(inv.Runtimes)-6)
|
|
}
|
|
}
|
|
|
|
if inv.PackageCount > 0 {
|
|
fmt.Fprintf(&b, ". %d packages (%s)", inv.PackageCount, inv.PackageMgr)
|
|
}
|
|
fmt.Fprintf(&b, ", %d commands in PATH.", len(inv.Tools))
|
|
b.WriteString(" Use system_info tool for full details.")
|
|
|
|
return b.String()
|
|
}
|
|
|
|
// QuerySection returns detailed info for a specific section.
|
|
// Sections: "all", "runtimes", "packages", "tools", "hardware"
|
|
func (inv *SystemInventory) QuerySection(section string) string {
|
|
if inv == nil {
|
|
return "no inventory available"
|
|
}
|
|
|
|
switch strings.ToLower(section) {
|
|
case "runtimes":
|
|
return inv.formatRuntimes()
|
|
case "packages", "dev":
|
|
return inv.formatPackages()
|
|
case "tools", "commands":
|
|
return inv.formatTools()
|
|
case "hardware", "hw":
|
|
return inv.formatHardware()
|
|
case "all", "":
|
|
return inv.formatAll()
|
|
default:
|
|
return fmt.Sprintf("unknown section %q. Available: runtimes, packages, tools, hardware, all", section)
|
|
}
|
|
}
|
|
|
|
func (inv *SystemInventory) formatRuntimes() string {
|
|
if len(inv.Runtimes) == 0 {
|
|
return "no runtimes detected"
|
|
}
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "Detected runtimes (%d):\n", len(inv.Runtimes))
|
|
for _, rt := range inv.Runtimes {
|
|
fmt.Fprintf(&b, " %s: %s\n", rt.Name, rt.Version)
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func (inv *SystemInventory) formatPackages() string {
|
|
var b strings.Builder
|
|
if inv.PackageMgr != "" {
|
|
fmt.Fprintf(&b, "Package manager: %s (%d total packages)\n", inv.PackageMgr, inv.PackageCount)
|
|
}
|
|
if len(inv.DevPackages) > 0 {
|
|
fmt.Fprintf(&b, "Dev packages (%d):\n %s\n", len(inv.DevPackages), strings.Join(inv.DevPackages, ", "))
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func (inv *SystemInventory) formatTools() string {
|
|
if len(inv.Tools) == 0 {
|
|
return "no tools found in PATH"
|
|
}
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "Executables in PATH (%d):\n %s\n", len(inv.Tools), strings.Join(inv.Tools, ", "))
|
|
return b.String()
|
|
}
|
|
|
|
func (inv *SystemInventory) formatHardware() string {
|
|
var b strings.Builder
|
|
b.WriteString("Hardware:\n")
|
|
fmt.Fprintf(&b, " OS: %s\n", inv.OS)
|
|
fmt.Fprintf(&b, " Shell: %s\n", inv.Shell)
|
|
hw := inv.Hardware
|
|
if hw.CPU != "" {
|
|
fmt.Fprintf(&b, " CPU: %s\n", hw.CPU)
|
|
}
|
|
fmt.Fprintf(&b, " Cores: %d\n", hw.Cores)
|
|
if hw.MemTotal != "" {
|
|
fmt.Fprintf(&b, " Memory: %s\n", hw.MemTotal)
|
|
}
|
|
if hw.GPU != "" {
|
|
fmt.Fprintf(&b, " GPU: %s\n", hw.GPU)
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func (inv *SystemInventory) formatAll() string {
|
|
var b strings.Builder
|
|
b.WriteString(inv.formatHardware())
|
|
b.WriteString("\n")
|
|
b.WriteString(inv.formatRuntimes())
|
|
b.WriteString("\n")
|
|
b.WriteString(inv.formatPackages())
|
|
return b.String()
|
|
}
|
|
|
|
// --- Hardware Detection ---
|
|
|
|
func detectHardware(ctx context.Context) HardwareInfo {
|
|
hw := HardwareInfo{
|
|
Cores: runtime.NumCPU(),
|
|
}
|
|
|
|
// CPU model
|
|
if cpuInfo := runQuiet(ctx, "sh", "-c", `command grep -m1 'model name' /proc/cpuinfo 2>/dev/null | cut -d: -f2`); cpuInfo != "" {
|
|
hw.CPU = strings.TrimSpace(cpuInfo)
|
|
}
|
|
|
|
// Total memory
|
|
if memInfo := runQuiet(ctx, "sh", "-c", `command grep MemTotal /proc/meminfo 2>/dev/null | awk '{printf "%.0f GB", $2/1024/1024}'`); memInfo != "" {
|
|
hw.MemTotal = memInfo
|
|
}
|
|
|
|
// GPU (try lspci first, then nvidia-smi)
|
|
if gpu := runQuiet(ctx, "sh", "-c", `lspci 2>/dev/null | command grep -i 'vga\|3d\|display' | head -1 | sed 's/.*: //'`); gpu != "" {
|
|
hw.GPU = gpu
|
|
}
|
|
|
|
return hw
|
|
}
|
|
|
|
// --- PATH Scanning ---
|
|
|
|
func scanPATH() []string {
|
|
seen := make(map[string]bool)
|
|
var names []string
|
|
|
|
for _, dir := range filepath.SplitList(os.Getenv("PATH")) {
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
continue
|
|
}
|
|
name := entry.Name()
|
|
if seen[name] {
|
|
continue
|
|
}
|
|
info, err := entry.Info()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if info.Mode()&0o111 != 0 {
|
|
seen[name] = true
|
|
names = append(names, name)
|
|
}
|
|
}
|
|
}
|
|
|
|
sort.Strings(names)
|
|
return names
|
|
}
|
|
|
|
// --- Runtime Probing ---
|
|
|
|
var runtimePatterns = []struct {
|
|
name string
|
|
binary string
|
|
args []string
|
|
}{
|
|
{"go", "go", []string{"version"}},
|
|
{"rust", "rustc", []string{"--version"}},
|
|
{"zig", "zig", []string{"version"}},
|
|
{"nim", "nim", []string{"--version"}},
|
|
{"crystal", "crystal", []string{"--version"}},
|
|
{"gcc", "gcc", []string{"--version"}},
|
|
{"clang", "clang", []string{"--version"}},
|
|
{"nasm", "nasm", []string{"-v"}},
|
|
{"python3", "python3", []string{"--version"}},
|
|
{"python2", "python2", []string{"--version"}},
|
|
{"perl", "perl", []string{"--version"}},
|
|
{"ruby", "ruby", []string{"--version"}},
|
|
{"lua", "lua", []string{"-v"}},
|
|
{"luajit", "luajit", []string{"-v"}},
|
|
{"guile", "guile", []string{"--version"}},
|
|
{"php", "php", []string{"--version"}},
|
|
{"r", "R", []string{"--version"}},
|
|
{"node", "node", []string{"--version"}},
|
|
{"deno", "deno", []string{"--version"}},
|
|
{"bun", "bun", []string{"--version"}},
|
|
{"java", "java", []string{"-version"}},
|
|
{"kotlin", "kotlin", []string{"-version"}},
|
|
{"scala", "scala", []string{"-version"}},
|
|
{"groovy", "groovy", []string{"--version"}},
|
|
{"clojure", "clj", []string{"--version"}},
|
|
{"haskell", "ghc", []string{"--version"}},
|
|
{"ocaml", "ocaml", []string{"-version"}},
|
|
{"elixir", "elixir", []string{"--version"}},
|
|
{"racket", "racket", []string{"--version"}},
|
|
{"dart", "dart", []string{"--version"}},
|
|
{"julia", "julia", []string{"--version"}},
|
|
{"swift", "swift", []string{"--version"}},
|
|
{"dotnet", "dotnet", []string{"--version"}},
|
|
{"mono", "mono", []string{"--version"}},
|
|
{"cargo", "cargo", []string{"--version"}},
|
|
{"npm", "npm", []string{"--version"}},
|
|
{"yarn", "yarn", []string{"--version"}},
|
|
{"pnpm", "pnpm", []string{"--version"}},
|
|
{"pip", "pip3", []string{"--version"}},
|
|
{"gem", "gem", []string{"--version"}},
|
|
}
|
|
|
|
func probeRuntimes(ctx context.Context, available []string) []Runtime {
|
|
availSet := make(map[string]bool, len(available))
|
|
for _, name := range available {
|
|
availSet[name] = true
|
|
}
|
|
|
|
var mu sync.Mutex
|
|
var runtimes []Runtime
|
|
var wg sync.WaitGroup
|
|
sem := make(chan struct{}, 10)
|
|
|
|
for _, p := range runtimePatterns {
|
|
if !availSet[p.binary] {
|
|
continue
|
|
}
|
|
wg.Add(1)
|
|
sem <- struct{}{}
|
|
go func(name, binary string, args []string) {
|
|
defer wg.Done()
|
|
defer func() { <-sem }()
|
|
if v := probeVersion(ctx, binary, args); v != "" {
|
|
mu.Lock()
|
|
runtimes = append(runtimes, Runtime{Name: name, Version: v})
|
|
mu.Unlock()
|
|
}
|
|
}(p.name, p.binary, p.args)
|
|
}
|
|
wg.Wait()
|
|
|
|
sort.Slice(runtimes, func(i, j int) bool { return runtimes[i].Name < runtimes[j].Name })
|
|
return runtimes
|
|
}
|
|
|
|
func probeVersion(ctx context.Context, binary string, args []string) string {
|
|
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
|
defer cancel()
|
|
cmd := exec.CommandContext(ctx, binary, args...)
|
|
cmd.Stdin = nil
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil && len(output) == 0 {
|
|
return ""
|
|
}
|
|
for _, line := range strings.Split(string(output), "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line != "" {
|
|
return line
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// --- Package Manager ---
|
|
|
|
func queryPackageManager(ctx context.Context) (mgr string, total int, devPkgs []string) {
|
|
managers := []struct {
|
|
name, binary string
|
|
args []string
|
|
}{
|
|
{"pacman", "pacman", []string{"-Qq"}},
|
|
{"apt", "dpkg", []string{"--get-selections"}},
|
|
{"dnf", "rpm", []string{"-qa", "--qf", "%{NAME}\\n"}},
|
|
{"brew", "brew", []string{"list", "--formula", "-1"}},
|
|
{"nix", "nix-env", []string{"-q"}},
|
|
{"apk", "apk", []string{"list", "--installed", "-q"}},
|
|
}
|
|
|
|
for _, pm := range managers {
|
|
if _, err := exec.LookPath(pm.binary); err != nil {
|
|
continue
|
|
}
|
|
cmd := exec.CommandContext(ctx, pm.binary, pm.args...)
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
|
|
return pm.name, len(lines), filterDevPackages(lines)
|
|
}
|
|
return "", 0, nil
|
|
}
|
|
|
|
var devPrefixes = []string{
|
|
"python", "ruby", "nodejs", "go", "rustup", "lua", "luajit",
|
|
"perl", "dart", "deno", "bun", "zig", "nim", "crystal",
|
|
"elixir", "erlang", "ghc", "ocaml", "swift", "kotlin", "scala",
|
|
"julia", "php", "dotnet", "mono", "clojure", "racket", "guile",
|
|
"openjdk", "jdk", "gcc", "clang", "llvm", "cmake", "make",
|
|
"ninja", "meson", "nasm", "gdb", "valgrind", "strace",
|
|
"docker", "podman", "kubectl", "helm", "terraform", "ansible",
|
|
"npm", "yarn", "pnpm", "cargo", "rubygems", "composer",
|
|
"sqlite", "postgresql", "mysql", "redis", "mongodb",
|
|
"git", "mercurial", "subversion", "neovim", "vim", "emacs",
|
|
}
|
|
|
|
func filterDevPackages(packages []string) []string {
|
|
var devPkgs []string
|
|
seen := make(map[string]bool)
|
|
|
|
for _, pkg := range packages {
|
|
pkg = strings.TrimSpace(pkg)
|
|
if f := strings.Fields(pkg); len(f) > 0 {
|
|
pkg = f[0]
|
|
}
|
|
if pkg == "" || seen[pkg] {
|
|
continue
|
|
}
|
|
lower := strings.ToLower(pkg)
|
|
|
|
// Skip sub-packages
|
|
if strings.HasPrefix(lower, "python-") || strings.HasPrefix(lower, "haskell-") ||
|
|
strings.HasPrefix(lower, "perl-") || strings.HasPrefix(lower, "ruby-") ||
|
|
strings.HasPrefix(lower, "lua-") || strings.HasPrefix(lower, "lua51-") ||
|
|
strings.HasPrefix(lower, "lua54-") || strings.HasPrefix(lower, "lib32-") ||
|
|
strings.HasPrefix(lower, "lib") || strings.HasPrefix(lower, "ttf-") ||
|
|
strings.HasPrefix(lower, "otf-") || strings.HasPrefix(lower, "qt5-") ||
|
|
strings.HasPrefix(lower, "qt6-") || strings.HasPrefix(lower, "gtk") ||
|
|
strings.HasPrefix(lower, "gst-") {
|
|
continue
|
|
}
|
|
|
|
for _, prefix := range devPrefixes {
|
|
if lower == prefix || strings.HasPrefix(lower, prefix+"-") ||
|
|
strings.HasPrefix(lower, prefix+"1") || strings.HasPrefix(lower, prefix+"2") ||
|
|
strings.HasPrefix(lower, prefix+"3") {
|
|
seen[pkg] = true
|
|
devPkgs = append(devPkgs, pkg)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
sort.Strings(devPkgs)
|
|
return devPkgs
|
|
}
|
|
|
|
func detectOS(ctx context.Context) string {
|
|
if out := runQuiet(ctx, "uname", "-srm"); out != "" {
|
|
return strings.TrimSpace(out)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func runQuiet(ctx context.Context, name string, args ...string) string {
|
|
cmd := exec.CommandContext(ctx, name, args...)
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(string(output))
|
|
}
|