Skip to content

Commit 2d591cc

Browse files
feat: add OS-aware command suggestions for v0.5.0
BREAKING CHANGE: ContextEnvelope now includes OS detection fields This major feature makes LineSense truly cross-platform by detecting the user's operating system, Linux distribution, and package manager, then tailoring all command suggestions accordingly. ### Key Features **OS Detection:** - Detects operating system (Linux, macOS, Windows) - Detects Linux distribution from /etc/os-release (Ubuntu, Arch, Fedora, etc.) - Detects package manager (apt, yum, dnf, pacman, brew, choco, winget, etc.) - All detection happens automatically - zero configuration required **Smart Suggestions:** - Package installation commands use correct package manager - Ubuntu: `sudo apt install nginx` - Arch: `sudo pacman -S nginx` - macOS: `brew install nginx` - Windows: `choco install nginx` or `winget install nginx` - Native command preferences (e.g., `open` on macOS vs `xdg-open` on Linux) - OS-appropriate file paths and command syntax **Implementation:** - New `internal/core/osdetect.go` with detection utilities - Extended `ContextEnvelope` with OS, Distribution, PackageManager fields - Updated AI system prompt with OS-specific command guidelines - AI user prompts now include OS context - Comprehensive test coverage for all detection functions **Examples:** Input: "install docker" - Ubuntu → `sudo apt install docker.io` - Arch → `sudo pacman -S docker` - macOS → `brew install --cask docker` Input: "open file.txt" - macOS → `open file.txt` - Linux → `xdg-open file.txt` - Windows → `start file.txt` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 21edd7c commit 2d591cc

File tree

7 files changed

+320
-23
lines changed

7 files changed

+320
-23
lines changed

CHANGELOG.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.5.0] - 2025-11-15
11+
12+
### Added
13+
- **Operating System Aware Suggestions** 🎯
14+
- LineSense now detects your operating system (Linux, macOS, Windows)
15+
- Detects Linux distribution (Ubuntu, Arch, Fedora, etc.)
16+
- Automatically detects your package manager (apt, yum, dnf, pacman, brew, etc.)
17+
- AI suggestions are now tailored to your specific OS and package manager
18+
- Example: Typing "install nginx" on Ubuntu suggests `sudo apt install nginx`, on macOS suggests `brew install nginx`, on Arch suggests `sudo pacman -S nginx`
19+
20+
### Improved
21+
- **Smarter Package Management Commands**
22+
- No more suggesting apt commands on macOS or brew commands on Linux
23+
- Package installation commands use the correct package manager for your system
24+
- File operations use OS-appropriate paths and command syntax
25+
- Native commands preferred (e.g., `open` on macOS vs `xdg-open` on Linux)
26+
27+
### Technical
28+
- Added OS detection utility in `internal/core/osdetect.go`
29+
- New `DetectOS()` function using `runtime.GOOS`
30+
- New `DetectDistribution()` function parsing `/etc/os-release` on Linux
31+
- New `DetectPackageManager()` function checking for installed package managers
32+
- Extended `ContextEnvelope` with `OS`, `Distribution`, and `PackageManager` fields
33+
- Updated AI prompts to include OS context and OS-specific command guidelines
34+
- Comprehensive test suite for OS detection across platforms
35+
1036
## [0.4.4] - 2025-11-15
1137

1238
### Added

cmd/linesense/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515
"github.com/traves/linesense/internal/core"
1616
)
1717

18-
const version = "0.4.4"
18+
const version = "0.5.0"
1919

2020
func main() {
2121
// Try to load .env file (silently ignore if not present)

internal/ai/prompts.go

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,20 @@ IMPORTANT RULES:
1515
1. Provide 3-5 alternative command suggestions
1616
2. Order suggestions from most likely to least likely
1717
3. Make commands safe and appropriate
18-
4. Use the context (git info, history, cwd) to make intelligent suggestions
18+
4. Use the context (git info, history, cwd, OS, package manager) to make intelligent suggestions
1919
5. If the input is already complete, suggest improvements or alternatives
2020
6. For ambiguous or typo inputs, interpret user intent and suggest corrections
21-
7. Prefer standard Unix/Linux commands
22-
8. Keep commands concise but complete
21+
7. Keep commands concise but complete
22+
23+
OS-SPECIFIC COMMANDS:
24+
- ALWAYS use commands appropriate for the user's operating system and package manager
25+
- For package installation on Linux with apt: use "sudo apt install <package>"
26+
- For package installation on Linux with yum/dnf: use "sudo yum install <package>" or "sudo dnf install <package>"
27+
- For package installation on Linux with pacman: use "sudo pacman -S <package>"
28+
- For package installation on macOS with brew: use "brew install <package>"
29+
- For package installation on Windows: use appropriate Windows commands or PowerShell
30+
- Adjust file paths, command options, and syntax based on the OS
31+
- Use native commands when available (e.g., 'open' on macOS vs 'xdg-open' on Linux)
2332
2433
RESPONSE FORMAT:
2534
One suggestion per line in this exact format:
@@ -38,6 +47,15 @@ func buildSuggestUserPrompt(ctx *core.ContextEnvelope) string {
3847
// Add the current line (what the user is typing)
3948
parts = append(parts, fmt.Sprintf("Current input: %s", ctx.Line))
4049

50+
// Add system information
51+
parts = append(parts, fmt.Sprintf("\nOperating System: %s", ctx.OS))
52+
if ctx.Distribution != "" {
53+
parts = append(parts, fmt.Sprintf("Distribution: %s", ctx.Distribution))
54+
}
55+
if ctx.PackageManager != "" {
56+
parts = append(parts, fmt.Sprintf("Package Manager: %s", ctx.PackageManager))
57+
}
58+
4159
// Add shell and working directory
4260
parts = append(parts, fmt.Sprintf("\nShell: %s", ctx.Shell))
4361
parts = append(parts, fmt.Sprintf("Working directory: %s", ctx.CWD))
@@ -94,6 +112,12 @@ func buildExplainUserPrompt(ctx *core.ContextEnvelope) string {
94112
// Add the command to explain
95113
parts = append(parts, fmt.Sprintf("Explain this command: %s", ctx.Line))
96114

115+
// Add system information
116+
parts = append(parts, fmt.Sprintf("\nOperating System: %s", ctx.OS))
117+
if ctx.Distribution != "" {
118+
parts = append(parts, fmt.Sprintf("Distribution: %s", ctx.Distribution))
119+
}
120+
97121
// Add context
98122
parts = append(parts, fmt.Sprintf("\nShell: %s", ctx.Shell))
99123
parts = append(parts, fmt.Sprintf("Working directory: %s", ctx.CWD))

internal/ai/prompts_test.go

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,12 @@ func TestBuildSuggestSystemPrompt(t *testing.T) {
3232

3333
func TestBuildSuggestUserPrompt(t *testing.T) {
3434
ctx := &core.ContextEnvelope{
35-
Shell: "bash",
36-
Line: "git com",
37-
CWD: "/home/user/project",
35+
Shell: "bash",
36+
Line: "git com",
37+
CWD: "/home/user/project",
38+
OS: "linux",
39+
Distribution: "ubuntu",
40+
PackageManager: "apt",
3841
Git: &core.GitInfo{
3942
IsRepo: true,
4043
Branch: "main",
@@ -54,6 +57,15 @@ func TestBuildSuggestUserPrompt(t *testing.T) {
5457
if !strings.Contains(prompt, "git com") {
5558
t.Error("Should contain current input")
5659
}
60+
if !strings.Contains(prompt, "linux") {
61+
t.Error("Should contain OS")
62+
}
63+
if !strings.Contains(prompt, "ubuntu") {
64+
t.Error("Should contain distribution")
65+
}
66+
if !strings.Contains(prompt, "apt") {
67+
t.Error("Should contain package manager")
68+
}
5769
if !strings.Contains(prompt, "bash") {
5870
t.Error("Should contain shell")
5971
}
@@ -73,9 +85,11 @@ func TestBuildSuggestUserPrompt(t *testing.T) {
7385

7486
func TestBuildSuggestUserPrompt_NoGit(t *testing.T) {
7587
ctx := &core.ContextEnvelope{
76-
Shell: "zsh",
77-
Line: "ls",
78-
CWD: "/tmp",
88+
Shell: "zsh",
89+
Line: "ls",
90+
CWD: "/tmp",
91+
OS: "darwin",
92+
PackageManager: "brew",
7993
}
8094

8195
prompt := buildSuggestUserPrompt(ctx)
@@ -117,9 +131,11 @@ func TestBuildExplainSystemPrompt(t *testing.T) {
117131

118132
func TestBuildExplainUserPrompt(t *testing.T) {
119133
ctx := &core.ContextEnvelope{
120-
Shell: "bash",
121-
Line: "rm -rf /tmp/test",
122-
CWD: "/home/user",
134+
Shell: "bash",
135+
Line: "rm -rf /tmp/test",
136+
CWD: "/home/user",
137+
OS: "linux",
138+
Distribution: "arch",
123139
Git: &core.GitInfo{
124140
IsRepo: true,
125141
Branch: "develop",
@@ -133,6 +149,12 @@ func TestBuildExplainUserPrompt(t *testing.T) {
133149
if !strings.Contains(prompt, "rm -rf /tmp/test") {
134150
t.Error("Should contain command to explain")
135151
}
152+
if !strings.Contains(prompt, "linux") {
153+
t.Error("Should contain OS")
154+
}
155+
if !strings.Contains(prompt, "arch") {
156+
t.Error("Should contain distribution")
157+
}
136158
if !strings.Contains(prompt, "bash") {
137159
t.Error("Should contain shell")
138160
}

internal/core/context.go

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,16 @@ import (
99

1010
// ContextEnvelope is collected before each suggestion / explanation
1111
type ContextEnvelope struct {
12-
Shell string `json:"shell"` // "bash" | "zsh"
13-
Line string `json:"line"` // current input line
14-
CWD string `json:"cwd"`
15-
Git *GitInfo `json:"git,omitempty"`
16-
Env map[string]string `json:"env,omitempty"` // filtered env (if enabled)
17-
History []HistoryEntry `json:"history,omitempty"` // last N commands
18-
UsageSummary *UsageSummary `json:"usage_summary,omitempty"`
12+
Shell string `json:"shell"` // "bash" | "zsh"
13+
Line string `json:"line"` // current input line
14+
CWD string `json:"cwd"`
15+
OS string `json:"os"` // "linux" | "darwin" | "windows"
16+
Distribution string `json:"distribution,omitempty"` // Linux distro: "ubuntu", "arch", "fedora", etc.
17+
PackageManager string `json:"package_manager,omitempty"` // "apt", "yum", "brew", "pacman", etc.
18+
Git *GitInfo `json:"git,omitempty"`
19+
Env map[string]string `json:"env,omitempty"` // filtered env (if enabled)
20+
History []HistoryEntry `json:"history,omitempty"` // last N commands
21+
UsageSummary *UsageSummary `json:"usage_summary,omitempty"`
1922
}
2023

2124
// GitInfo contains git repository information
@@ -41,9 +44,12 @@ type UsageSummary struct {
4144
// BuildContext gathers all contextual information
4245
func BuildContext(shell, line, cwd string, cfg *config.Config) (*ContextEnvelope, error) {
4346
ctx := &ContextEnvelope{
44-
Shell: shell,
45-
Line: line,
46-
CWD: cwd,
47+
Shell: shell,
48+
Line: line,
49+
CWD: cwd,
50+
OS: DetectOS(),
51+
Distribution: DetectDistribution(),
52+
PackageManager: DetectPackageManager(),
4753
}
4854

4955
// Collect git context if enabled

internal/core/osdetect.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package core
2+
3+
import (
4+
"os"
5+
"os/exec"
6+
"runtime"
7+
"strings"
8+
)
9+
10+
// DetectOS returns the operating system type
11+
func DetectOS() string {
12+
switch runtime.GOOS {
13+
case "darwin":
14+
return "darwin"
15+
case "linux":
16+
return "linux"
17+
case "windows":
18+
return "windows"
19+
default:
20+
return runtime.GOOS
21+
}
22+
}
23+
24+
// DetectDistribution detects the Linux distribution
25+
// Returns empty string for non-Linux systems or if detection fails
26+
func DetectDistribution() string {
27+
if runtime.GOOS != "linux" {
28+
return ""
29+
}
30+
31+
// Try to read /etc/os-release (standard location)
32+
data, err := os.ReadFile("/etc/os-release")
33+
if err != nil {
34+
return ""
35+
}
36+
37+
// Parse os-release file
38+
lines := strings.Split(string(data), "\n")
39+
for _, line := range lines {
40+
if strings.HasPrefix(line, "ID=") {
41+
// Extract distro ID (e.g., ID=ubuntu)
42+
distro := strings.TrimPrefix(line, "ID=")
43+
distro = strings.Trim(distro, "\"")
44+
return strings.ToLower(distro)
45+
}
46+
}
47+
48+
return ""
49+
}
50+
51+
// DetectPackageManager detects the available package manager
52+
// Returns the most common package manager for the system
53+
func DetectPackageManager() string {
54+
osType := runtime.GOOS
55+
56+
switch osType {
57+
case "darwin":
58+
// macOS - check for brew
59+
if commandExists("brew") {
60+
return "brew"
61+
}
62+
return ""
63+
64+
case "linux":
65+
// Check for common Linux package managers in order of preference
66+
managers := []string{
67+
"apt", // Debian/Ubuntu
68+
"dnf", // Fedora/RHEL 8+
69+
"yum", // RHEL/CentOS 7
70+
"pacman", // Arch
71+
"zypper", // openSUSE
72+
"apk", // Alpine
73+
}
74+
75+
for _, manager := range managers {
76+
if commandExists(manager) {
77+
return manager
78+
}
79+
}
80+
return ""
81+
82+
case "windows":
83+
// Windows - check for common package managers
84+
if commandExists("choco") {
85+
return "choco"
86+
}
87+
if commandExists("winget") {
88+
return "winget"
89+
}
90+
if commandExists("scoop") {
91+
return "scoop"
92+
}
93+
return ""
94+
95+
default:
96+
return ""
97+
}
98+
}
99+
100+
// commandExists checks if a command is available in PATH
101+
func commandExists(cmd string) bool {
102+
_, err := exec.LookPath(cmd)
103+
return err == nil
104+
}

0 commit comments

Comments
 (0)