feat: compact system inventory with queryable system_info tool

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.
This commit is contained in:
2026-04-03 14:50:33 +02:00
parent d02b544e08
commit 11a7a51d9d
4 changed files with 412 additions and 176 deletions

View File

@@ -23,6 +23,7 @@ import (
"somegit.dev/Owlibou/gnoma/internal/tool"
"somegit.dev/Owlibou/gnoma/internal/tool/bash"
"somegit.dev/Owlibou/gnoma/internal/tool/fs"
"somegit.dev/Owlibou/gnoma/internal/tool/sysinfo"
)
func main() {
@@ -89,6 +90,9 @@ func main() {
// Re-register bash tool with aliases
reg.Register(bash.New(bash.WithAliases(aliases)))
// Register system_info tool backed by the inventory
reg.Register(sysinfo.New(inventory))
// Create router and register the provider as a single arm
// (M4 foundation: one provider from CLI. Multi-provider routing comes with config.)
rtr := router.New(router.Config{Logger: logger})
@@ -114,10 +118,10 @@ func main() {
Logger: logger,
})
// Build system prompt with inventory
// Build system prompt with compact inventory summary
systemPrompt := *system
if invStr := inventory.String(); invStr != "" {
systemPrompt = systemPrompt + "\n\n" + invStr
if summary := inventory.Summary(); summary != "" {
systemPrompt = systemPrompt + "\n\n" + summary
}
// Create engine

View File

@@ -6,6 +6,7 @@ import (
"os"
"os/exec"
"path/filepath"
"runtime"
"sort"
"strings"
"sync"
@@ -16,46 +17,216 @@ const inventoryTimeout = 15 * time.Second
// SystemInventory holds dynamically discovered system information.
type SystemInventory struct {
// Core
OS string
Shell string
Tools []string // all executables found in PATH
Runtimes []Runtime // detected runtimes with versions
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
}
// Runtime is a detected language runtime or package manager.
type Runtime struct {
Name string
Version string
}
// HarvestInventory dynamically discovers installed tools and runtimes.
// No hardcoded lists — scans $PATH and probes for version info.
// 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: detectShell(),
OS: detectOS(ctx),
Shell: os.Getenv("SHELL"),
Hardware: detectHardware(ctx),
}
// Scan all executables in PATH
allBinaries := scanPATH()
inv.Tools = allBinaries
// Probe for runtimes: try --version on known runtime name patterns
inv.Runtimes = probeRuntimes(ctx, allBinaries)
inv.Tools = scanPATH()
inv.Runtimes = probeRuntimes(ctx, inv.Tools)
inv.PackageMgr, inv.PackageCount, inv.DevPackages = queryPackageManager(ctx)
return inv
}
// scanPATH collects all unique executable names from $PATH directories.
// 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
pathDirs := filepath.SplitList(os.Getenv("PATH"))
for _, dir := range pathDirs {
for _, dir := range filepath.SplitList(os.Getenv("PATH")) {
entries, err := os.ReadDir(dir)
if err != nil {
continue
@@ -68,7 +239,6 @@ func scanPATH() []string {
if seen[name] {
continue
}
// Check it's actually executable
info, err := entry.Info()
if err != nil {
continue
@@ -84,18 +254,13 @@ func scanPATH() []string {
return names
}
// runtimeCandidate defines how to detect a runtime from a binary name.
type runtimeCandidate struct {
name string // display name
binary string // executable name to look for
args []string // version flag
}
// --- Runtime Probing ---
// knownRuntimePatterns returns binary names that are likely runtimes.
// We still need a pattern list to know WHICH binaries to try --version on,
// but the actual detection is dynamic (only probes what's in PATH).
var runtimePatterns = []runtimeCandidate{
// Systems languages
var runtimePatterns = []struct {
name string
binary string
args []string
}{
{"go", "go", []string{"version"}},
{"rust", "rustc", []string{"--version"}},
{"zig", "zig", []string{"version"}},
@@ -104,7 +269,6 @@ var runtimePatterns = []runtimeCandidate{
{"gcc", "gcc", []string{"--version"}},
{"clang", "clang", []string{"--version"}},
{"nasm", "nasm", []string{"-v"}},
// Scripting
{"python3", "python3", []string{"--version"}},
{"python2", "python2", []string{"--version"}},
{"perl", "perl", []string{"--version"}},
@@ -112,50 +276,33 @@ var runtimePatterns = []runtimeCandidate{
{"lua", "lua", []string{"-v"}},
{"luajit", "luajit", []string{"-v"}},
{"guile", "guile", []string{"--version"}},
// tcl detection is tricky (needs stdin), skipped
{"php", "php", []string{"--version"}},
{"r", "R", []string{"--version"}},
// JS/TS
{"node", "node", []string{"--version"}},
{"deno", "deno", []string{"--version"}},
{"bun", "bun", []string{"--version"}},
// JVM
{"java", "java", []string{"-version"}},
{"kotlin", "kotlin", []string{"-version"}},
{"scala", "scala", []string{"-version"}},
{"groovy", "groovy", []string{"--version"}},
{"clojure", "clj", []string{"--version"}},
// Functional
{"haskell", "ghc", []string{"--version"}},
{"ocaml", "ocaml", []string{"-version"}},
{"elixir", "elixir", []string{"--version"}},
{"erlang", "erl", []string{"-eval", "io:format(erlang:system_info(otp_release)),halt()."}},
{"racket", "racket", []string{"--version"}},
// Other
{"dart", "dart", []string{"--version"}},
{"julia", "julia", []string{"--version"}},
{"swift", "swift", []string{"--version"}},
// .NET
{"dotnet", "dotnet", []string{"--version"}},
{"mono", "mono", []string{"--version"}},
// Package managers
{"cargo", "cargo", []string{"--version"}},
{"npm", "npm", []string{"--version"}},
{"yarn", "yarn", []string{"--version"}},
{"pnpm", "pnpm", []string{"--version"}},
{"pip", "pip3", []string{"--version"}},
{"gem", "gem", []string{"--version"}},
{"composer", "composer", []string{"--version"}},
{"mix", "mix", []string{"--version"}},
{"cabal", "cabal", []string{"--version"}},
{"stack", "stack", []string{"--version"}},
{"opam", "opam", []string{"--version"}},
{"maven", "mvn", []string{"--version"}},
{"gradle", "gradle", []string{"--version"}},
}
// probeRuntimes checks which runtime candidates exist in the discovered binaries
// and gets their version info. Runs probes concurrently for speed.
func probeRuntimes(ctx context.Context, available []string) []Runtime {
availSet := make(map[string]bool, len(available))
for _, name := range available {
@@ -165,53 +312,39 @@ func probeRuntimes(ctx context.Context, available []string) []Runtime {
var mu sync.Mutex
var runtimes []Runtime
var wg sync.WaitGroup
// Semaphore to limit concurrent version probes
sem := make(chan struct{}, 10)
for _, candidate := range runtimePatterns {
if !availSet[candidate.binary] {
for _, p := range runtimePatterns {
if !availSet[p.binary] {
continue
}
wg.Add(1)
sem <- struct{}{}
go func(c runtimeCandidate) {
go func(name, binary string, args []string) {
defer wg.Done()
defer func() { <-sem }()
version := probeVersion(ctx, c.binary, c.args)
if version != "" {
if v := probeVersion(ctx, binary, args); v != "" {
mu.Lock()
runtimes = append(runtimes, Runtime{Name: c.name, Version: version})
runtimes = append(runtimes, Runtime{Name: name, Version: v})
mu.Unlock()
}
}(candidate)
}(p.name, p.binary, p.args)
}
wg.Wait()
sort.Slice(runtimes, func(i, j int) bool {
return runtimes[i].Name < runtimes[j].Name
})
sort.Slice(runtimes, func(i, j int) bool { return runtimes[i].Name < runtimes[j].Name })
return runtimes
}
// probeVersion runs a binary with version args and extracts the first line.
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
// Some tools print version to stderr (java -version)
output, err := cmd.CombinedOutput()
if err != nil && len(output) == 0 {
return ""
}
// First non-empty line
for _, line := range strings.Split(string(output), "\n") {
line = strings.TrimSpace(line)
if line != "" {
@@ -221,89 +354,92 @@ func probeVersion(ctx context.Context, binary string, args []string) string {
return ""
}
// String formats the inventory for inclusion in a system prompt.
func (inv *SystemInventory) String() string {
if inv == nil {
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"}},
}
var b strings.Builder
b.WriteString("System environment:\n")
if inv.OS != "" {
fmt.Fprintf(&b, "- OS: %s\n", inv.OS)
}
if inv.Shell != "" {
fmt.Fprintf(&b, "- Shell: %s\n", inv.Shell)
}
if len(inv.Runtimes) > 0 {
b.WriteString("- Runtimes & package managers:\n")
for _, rt := range inv.Runtimes {
fmt.Fprintf(&b, " - %s: %s\n", rt.Name, rt.Version)
for _, pm := range managers {
if _, err := exec.LookPath(pm.binary); err != nil {
continue
}
}
if len(inv.Tools) > 0 {
fmt.Fprintf(&b, "- Available commands: %d executables in PATH", len(inv.Tools))
// Include notable tools only (not the full list — saves tokens)
notable := filterNotable(inv.Tools)
if len(notable) > 0 {
fmt.Fprintf(&b, " (notable: %s)", strings.Join(notable, ", "))
cmd := exec.CommandContext(ctx, pm.binary, pm.args...)
output, err := cmd.Output()
if err != nil {
continue
}
b.WriteString("\n")
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
return pm.name, len(lines), filterDevPackages(lines)
}
return b.String()
return "", 0, nil
}
// notableTools are development-relevant tools worth highlighting in the system prompt.
var notableTools = map[string]bool{
// VCS
"git": true, "gh": true, "tea": true, "lazygit": true,
// Build
"make": true, "cmake": true, "just": true,
// Containers
"docker": true, "podman": true, "kubectl": true, "helm": true,
// Data
"jq": true, "yq": true, "rg": true, "fd": true, "fzf": true,
// Modern CLI
"bat": true, "eza": true, "delta": true, "sd": true, "dust": true,
"hyperfine": true, "tokei": true, "scc": true,
// Debug
"gdb": true, "strace": true, "perf": true, "valgrind": true,
// Database
"sqlite3": true, "psql": true, "mysql": true, "mongosh": true, "redis-cli": true,
// Media
"ffmpeg": true, "convert": true,
// Infra
"terraform": true, "ansible": true,
// Network
"curl": true, "wget": true, "ssh": true,
// Editors
"nvim": true, "vim": true,
// Multiplexer
"tmux": true,
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 filterNotable(tools []string) []string {
var out []string
for _, t := range tools {
if notableTools[t] {
out = append(out, t)
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
}
}
}
return out
sort.Strings(devPkgs)
return devPkgs
}
func detectOS(ctx context.Context) string {
if output := runQuiet(ctx, "uname", "-srm"); output != "" {
return strings.TrimSpace(output)
}
return ""
}
func detectShell() string {
if s := os.Getenv("SHELL"); s != "" {
return s
if out := runQuiet(ctx, "uname", "-srm"); out != "" {
return strings.TrimSpace(out)
}
return ""
}

View File

@@ -12,18 +12,6 @@ func TestScanPATH(t *testing.T) {
t.Fatal("should find at least some executables in PATH")
}
t.Logf("Found %d executables in PATH", len(binaries))
// Basic sanity — ls should be in PATH
hasLS := false
for _, b := range binaries {
if b == "ls" {
hasLS = true
break
}
}
if !hasLS {
t.Error("ls should be in PATH")
}
}
func TestHarvestInventory(t *testing.T) {
@@ -33,20 +21,23 @@ func TestHarvestInventory(t *testing.T) {
t.Error("OS should be detected")
}
t.Logf("OS: %s", inv.OS)
if inv.Shell == "" {
t.Error("Shell should be detected")
}
t.Logf("Shell: %s", inv.Shell)
t.Logf("CPU: %s (%d cores)", inv.Hardware.CPU, inv.Hardware.Cores)
t.Logf("Memory: %s", inv.Hardware.MemTotal)
t.Logf("GPU: %s", inv.Hardware.GPU)
t.Logf("Tools: %d in PATH", len(inv.Tools))
t.Logf("Tools: %d executables in PATH", len(inv.Tools))
if inv.PackageMgr != "" {
t.Logf("Packages: %d (%s)", inv.PackageCount, inv.PackageMgr)
t.Logf("Dev packages: %d", len(inv.DevPackages))
}
t.Logf("Runtimes (%d):", len(inv.Runtimes))
t.Logf("Runtimes: %d", len(inv.Runtimes))
for _, rt := range inv.Runtimes {
t.Logf(" %s: %s", rt.Name, rt.Version)
}
// Should find at least Go (we're running Go tests)
// Should detect Go (we're running Go tests)
hasGo := false
for _, rt := range inv.Runtimes {
if rt.Name == "go" {
@@ -55,39 +46,94 @@ func TestHarvestInventory(t *testing.T) {
}
}
if !hasGo {
t.Error("should detect Go runtime (we're running Go tests)")
t.Error("should detect Go runtime")
}
}
func TestInventory_String(t *testing.T) {
func TestInventory_Summary(t *testing.T) {
inv := &SystemInventory{
OS: "Linux 6.18 x86_64",
Shell: "/usr/bin/zsh",
Tools: []string{"git", "make", "docker"},
Runtimes: []Runtime{
{"go", "go version go1.26.1"},
{"python3", "Python 3.14.3"},
Hardware: HardwareInfo{
CPU: "AMD Ryzen 9", Cores: 16,
MemTotal: "32 GB", GPU: "NVIDIA RTX 4090",
},
Tools: make([]string, 100),
Runtimes: []Runtime{{"go", "1.26"}, {"python3", "3.14"}, {"node", "25.8"}},
PackageCount: 2239,
PackageMgr: "pacman",
}
s := inv.String()
if !strings.Contains(s, "Linux") {
s := inv.Summary()
if !strings.Contains(s, "Linux 6.18") {
t.Error("should contain OS")
}
if !strings.Contains(s, "git") {
t.Error("should contain tools")
if !strings.Contains(s, "zsh") {
t.Error("should contain shell")
}
if !strings.Contains(s, "go:") {
t.Error("should contain runtimes")
if !strings.Contains(s, "AMD Ryzen") {
t.Error("should contain CPU")
}
if !strings.Contains(s, "3 executables in PATH") {
t.Errorf("should show tool count, got: %s", s)
if !strings.Contains(s, "32 GB") {
t.Error("should contain RAM")
}
if !strings.Contains(s, "RTX 4090") {
t.Error("should contain GPU")
}
if !strings.Contains(s, "go, python3, node") {
t.Error("should contain top runtimes")
}
if !strings.Contains(s, "2239 packages") {
t.Error("should contain package count")
}
if !strings.Contains(s, "system_info") {
t.Error("should mention system_info tool")
}
// Should be compact — under 300 chars
if len(s) > 500 {
t.Errorf("summary too long (%d chars), should be compact: %s", len(s), s)
}
}
func TestInventory_NilString(t *testing.T) {
var inv *SystemInventory
if inv.String() != "" {
t.Error("nil inventory should return empty string")
func TestInventory_QuerySection(t *testing.T) {
inv := &SystemInventory{
OS: "Linux",
Shell: "/bin/zsh",
Hardware: HardwareInfo{CPU: "Test CPU", Cores: 4, MemTotal: "16 GB"},
Tools: []string{"git", "make"},
Runtimes: []Runtime{{"go", "1.26"}, {"rust", "1.91"}},
DevPackages: []string{"docker", "kubectl"},
PackageMgr: "pacman", PackageCount: 100,
}
// Runtimes
r := inv.QuerySection("runtimes")
if !strings.Contains(r, "go: 1.26") {
t.Errorf("runtimes section should contain go: %s", r)
}
// Hardware
h := inv.QuerySection("hardware")
if !strings.Contains(h, "Test CPU") {
t.Errorf("hardware section should contain CPU: %s", h)
}
// Tools
tools := inv.QuerySection("tools")
if !strings.Contains(tools, "git") {
t.Errorf("tools section should contain git: %s", tools)
}
// Unknown
u := inv.QuerySection("bogus")
if !strings.Contains(u, "unknown section") {
t.Errorf("unknown section should error: %s", u)
}
}
func TestInventory_NilSummary(t *testing.T) {
var inv *SystemInventory
if inv.Summary() != "" {
t.Error("nil inventory should return empty summary")
}
}

View File

@@ -0,0 +1,50 @@
package sysinfo
import (
"context"
"encoding/json"
"somegit.dev/Owlibou/gnoma/internal/tool"
"somegit.dev/Owlibou/gnoma/internal/tool/bash"
)
var paramSchema = json.RawMessage(`{
"type": "object",
"properties": {
"section": {
"type": "string",
"description": "Section to query: runtimes, packages, tools, hardware, all",
"enum": ["runtimes", "packages", "tools", "hardware", "all"]
}
}
}`)
// Tool provides queryable access to system inventory.
type Tool struct {
inventory *bash.SystemInventory
}
func New(inv *bash.SystemInventory) *Tool {
return &Tool{inventory: inv}
}
func (t *Tool) Name() string { return "system_info" }
func (t *Tool) Description() string { return "Query system information: installed runtimes, packages, tools, hardware" }
func (t *Tool) Parameters() json.RawMessage { return paramSchema }
func (t *Tool) IsReadOnly() bool { return true }
func (t *Tool) IsDestructive() bool { return false }
type sysInfoArgs struct {
Section string `json:"section,omitempty"`
}
func (t *Tool) Execute(_ context.Context, args json.RawMessage) (tool.Result, error) {
var a sysInfoArgs
if args != nil {
_ = json.Unmarshal(args, &a)
}
if a.Section == "" {
a.Section = "all"
}
return tool.Result{Output: t.inventory.QuerySection(a.Section)}, nil
}