alinux 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
alinux-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,89 @@
1
+ Metadata-Version: 2.5
2
+ Name: alinux
3
+ Version: 0.1.0
4
+ Summary: Alinux AI orchestration shell for minimal Linux desktops
5
+ Author: Alinux contributors
6
+ License: MIT
7
+ Keywords: ai,automation,desktop,fluxbox,linux
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: End Users/Desktop
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Desktop Environment
15
+ Requires-Python: >=3.10
16
+ Requires-Dist: openai<2,>=1.40.0
17
+ Requires-Dist: pydantic<3,>=2.7.0
18
+ Provides-Extra: dev
19
+ Requires-Dist: build>=1.2.0; extra == 'dev'
20
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
21
+ Requires-Dist: ruff>=0.6.0; extra == 'dev'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # Alinux
25
+
26
+ Alinux is an AI orchestration shell for a minimal Debian 12 desktop running
27
+ Fluxbox. It converts natural-language requests into a small, validated set of
28
+ desktop actions.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ python3 -m venv .venv
34
+ . .venv/bin/activate
35
+ python -m pip install .
36
+ ```
37
+
38
+ For model access, export a key and optionally choose a provider/model. Any
39
+ OpenAI-compatible API can be configured with a custom endpoint:
40
+
41
+ ```bash
42
+ export ALINUX_API_KEY='your-key'
43
+ export ALINUX_MODEL='gpt-4o-mini'
44
+ export ALINUX_API_BASE_URL='https://your-provider.example/v1'
45
+ ```
46
+
47
+ Groq is supported directly, including Alinux's built-in open model preset:
48
+
49
+ ```bash
50
+ export ALINUX_PROVIDER='groq'
51
+ export ALINUX_API_KEY='your-groq-key'
52
+ alinux
53
+ ```
54
+
55
+ With `ALINUX_PROVIDER=groq` and no model override, Alinux uses
56
+ `llama-3.3-70b-versatile` at Groq's OpenAI-compatible endpoint. You can also
57
+ select it explicitly with `ALINUX_MODEL=llama-3.3-70b-versatile`.
58
+
59
+ The desktop hooks expect `xdotool`, `amixer`, `free`, and `uptime`. Missing
60
+ tools are reported as readable responses rather than terminating Alinux.
61
+
62
+ ## Run
63
+
64
+ ```bash
65
+ alinux
66
+ ```
67
+
68
+ Open the graphical Alinux window from a terminal with:
69
+
70
+ ```bash
71
+ alinux open
72
+ ```
73
+
74
+ To add **Open Alinux** to the Fluxbox desktop context menu, run this once
75
+ inside the Fluxbox session:
76
+
77
+ ```bash
78
+ alinux install-fluxbox-menu
79
+ ```
80
+
81
+ Then restart or reconfigure Fluxbox. The installer updates
82
+ `~/.fluxbox/menu` without replacing your existing entries. The GUI requires
83
+ Debian's `python3-tk` package and a valid `DISPLAY`.
84
+
85
+ Type requests such as `show system status`, `turn the volume down`, or
86
+ `open the terminal`. Enter `quit` or press `Ctrl-D` to exit.
87
+
88
+ Alinux never executes arbitrary shell text from the model. Only the operations
89
+ listed in its system prompt and `system_controls.py` are routable.
alinux-0.1.0/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # Alinux
2
+
3
+ Alinux is an AI orchestration shell for a minimal Debian 12 desktop running
4
+ Fluxbox. It converts natural-language requests into a small, validated set of
5
+ desktop actions.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ python3 -m venv .venv
11
+ . .venv/bin/activate
12
+ python -m pip install .
13
+ ```
14
+
15
+ For model access, export a key and optionally choose a provider/model. Any
16
+ OpenAI-compatible API can be configured with a custom endpoint:
17
+
18
+ ```bash
19
+ export ALINUX_API_KEY='your-key'
20
+ export ALINUX_MODEL='gpt-4o-mini'
21
+ export ALINUX_API_BASE_URL='https://your-provider.example/v1'
22
+ ```
23
+
24
+ Groq is supported directly, including Alinux's built-in open model preset:
25
+
26
+ ```bash
27
+ export ALINUX_PROVIDER='groq'
28
+ export ALINUX_API_KEY='your-groq-key'
29
+ alinux
30
+ ```
31
+
32
+ With `ALINUX_PROVIDER=groq` and no model override, Alinux uses
33
+ `llama-3.3-70b-versatile` at Groq's OpenAI-compatible endpoint. You can also
34
+ select it explicitly with `ALINUX_MODEL=llama-3.3-70b-versatile`.
35
+
36
+ The desktop hooks expect `xdotool`, `amixer`, `free`, and `uptime`. Missing
37
+ tools are reported as readable responses rather than terminating Alinux.
38
+
39
+ ## Run
40
+
41
+ ```bash
42
+ alinux
43
+ ```
44
+
45
+ Open the graphical Alinux window from a terminal with:
46
+
47
+ ```bash
48
+ alinux open
49
+ ```
50
+
51
+ To add **Open Alinux** to the Fluxbox desktop context menu, run this once
52
+ inside the Fluxbox session:
53
+
54
+ ```bash
55
+ alinux install-fluxbox-menu
56
+ ```
57
+
58
+ Then restart or reconfigure Fluxbox. The installer updates
59
+ `~/.fluxbox/menu` without replacing your existing entries. The GUI requires
60
+ Debian's `python3-tk` package and a valid `DISPLAY`.
61
+
62
+ Type requests such as `show system status`, `turn the volume down`, or
63
+ `open the terminal`. Enter `quit` or press `Ctrl-D` to exit.
64
+
65
+ Alinux never executes arbitrary shell text from the model. Only the operations
66
+ listed in its system prompt and `system_controls.py` are routable.
@@ -0,0 +1,3 @@
1
+ """Alinux AI orchestration shell for minimal Linux desktops."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,6 @@
1
+ """Allow Alinux to run with ``python -m alinux``."""
2
+
3
+ from .main import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
@@ -0,0 +1,98 @@
1
+ """LLM-backed intent parsing for the Alinux shell."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from typing import Literal
8
+
9
+ from pydantic import BaseModel, ConfigDict, Field, ValidationError
10
+
11
+ BUILT_IN_MODELS = {
12
+ "llama-3.3-70b-versatile": {
13
+ "provider": "groq",
14
+ "base_url": "https://api.groq.com/openai/v1",
15
+ },
16
+ }
17
+
18
+
19
+ class AgentAction(BaseModel):
20
+ """The only action contract accepted by the desktop loop."""
21
+
22
+ model_config = ConfigDict(extra="forbid")
23
+
24
+ action: Literal["system_command", "launch_app", "respond_to_user"]
25
+ parameter: str = Field(max_length=500)
26
+ thought: str = Field(default="", max_length=1000)
27
+
28
+
29
+ SYSTEM_PROMPT = """You are the core brain of Alinux, a minimal Debian 12 Linux desktop using Fluxbox.
30
+ Translate the user's request into exactly one safe structured action.
31
+ You may use system_command for status, windows, volume up, volume down, or volume mute.
32
+ Use launch_app only with terminal, browser, or file manager.
33
+ Use respond_to_user for questions, unsupported requests, or requests requiring explanation.
34
+ Never invent shell commands, never request arbitrary command execution, and never claim an action succeeded.
35
+ Return only valid JSON matching this schema:
36
+ {"action":"system_command|launch_app|respond_to_user","parameter":"...","thought":"..."}
37
+ """
38
+
39
+
40
+ class Agent:
41
+ """Convert natural language into validated actions using an OpenAI-compatible API."""
42
+
43
+ def __init__(
44
+ self,
45
+ model: str | None = None,
46
+ api_key: str | None = None,
47
+ base_url: str | None = None,
48
+ provider: str | None = None,
49
+ ) -> None:
50
+ configured_model = model or os.getenv("ALINUX_MODEL")
51
+ self.model = configured_model or "gpt-4o-mini"
52
+ model_config = BUILT_IN_MODELS.get(self.model, {})
53
+ self.provider = (provider or os.getenv("ALINUX_PROVIDER") or model_config.get("provider", "openai")).lower()
54
+ self.base_url = base_url or os.getenv("ALINUX_API_BASE_URL") or model_config.get("base_url")
55
+ self.api_key = api_key or os.getenv("ALINUX_API_KEY") or os.getenv("OPENAI_API_KEY")
56
+ if self.provider == "groq" and configured_model is None and model is None:
57
+ self.model = "llama-3.3-70b-versatile"
58
+ self.base_url = self.base_url or "https://api.groq.com/openai/v1"
59
+ self._client = None
60
+ if self.api_key:
61
+ try:
62
+ from openai import OpenAI
63
+
64
+ client_options = {"api_key": self.api_key}
65
+ if self.base_url:
66
+ client_options["base_url"] = self.base_url
67
+ self._client = OpenAI(**client_options)
68
+ except ImportError:
69
+ self._client = None
70
+
71
+ def decide(self, user_text: str) -> AgentAction:
72
+ """Return a validated action, falling back safely when the LLM is unavailable."""
73
+ if not user_text.strip():
74
+ return AgentAction(action="respond_to_user", parameter="Please enter a request.")
75
+ if self._client is None:
76
+ return AgentAction(
77
+ action="respond_to_user",
78
+ parameter="Alinux needs ALINUX_API_KEY (or OPENAI_API_KEY) and the openai package to interpret requests.",
79
+ )
80
+ try:
81
+ response = self._client.chat.completions.create(
82
+ model=self.model,
83
+ temperature=0,
84
+ response_format={"type": "json_object"},
85
+ messages=[
86
+ {"role": "system", "content": SYSTEM_PROMPT},
87
+ {"role": "user", "content": user_text.strip()},
88
+ ],
89
+ )
90
+ content = response.choices[0].message.content or ""
91
+ return AgentAction.model_validate(json.loads(content))
92
+ except (json.JSONDecodeError, ValidationError, IndexError, TypeError, ValueError) as exc:
93
+ return AgentAction(
94
+ action="respond_to_user",
95
+ parameter=f"Alinux could not understand the model response safely: {exc}.",
96
+ )
97
+ except Exception as exc: # noqa: BLE001 - model failures must not stop the shell
98
+ return AgentAction(action="respond_to_user", parameter=f"Alinux model error: {exc}")
@@ -0,0 +1,34 @@
1
+ """Fluxbox menu integration for Alinux."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import tempfile
7
+ from pathlib import Path
8
+
9
+ _MENU_ENTRY = "[exec] (Open Alinux) {alinux open}"
10
+
11
+
12
+ def install_fluxbox_menu(menu_path: str | os.PathLike[str] | None = None) -> str:
13
+ """Add an Open Alinux entry to the user's Fluxbox menu idempotently."""
14
+ path = Path(menu_path).expanduser() if menu_path else Path.home() / ".fluxbox" / "menu"
15
+ path.parent.mkdir(parents=True, exist_ok=True)
16
+ existing = path.read_text(encoding="utf-8") if path.exists() else "[begin] (Fluxbox)\n[end]\n"
17
+ if _MENU_ENTRY in existing:
18
+ return f"Alinux is already in the Fluxbox menu at {path}."
19
+ lines = existing.splitlines()
20
+ end_index = next((index for index, line in enumerate(lines) if line.strip() == "[end]"), len(lines))
21
+ lines.insert(end_index, _MENU_ENTRY)
22
+ content = "\n".join(lines).rstrip() + "\n"
23
+ fd, temporary_path = tempfile.mkstemp(prefix=".alinux-menu-", dir=path.parent, text=True)
24
+ try:
25
+ with os.fdopen(fd, "w", encoding="utf-8") as temporary_file:
26
+ temporary_file.write(content)
27
+ os.replace(temporary_path, path)
28
+ except Exception:
29
+ try:
30
+ os.unlink(temporary_path)
31
+ except OSError:
32
+ pass
33
+ raise
34
+ return f"Added Open Alinux to the Fluxbox menu at {path}. Restart or reconfigure Fluxbox to see it."
@@ -0,0 +1,70 @@
1
+ """Tkinter desktop interface for Alinux."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import tkinter as tk
6
+ from tkinter import ttk
7
+
8
+ from .agent import Agent
9
+ from .main import _dispatch
10
+
11
+
12
+ class AlinuxWindow:
13
+ """Small desktop chat window that routes requests through the Agent."""
14
+
15
+ def __init__(self, root: tk.Tk, agent: Agent | None = None) -> None:
16
+ self.root = root
17
+ self.agent = agent or Agent()
18
+ self.root.title("Alinux")
19
+ self.root.geometry("640x460")
20
+ self.root.minsize(480, 320)
21
+
22
+ frame = ttk.Frame(root, padding=16)
23
+ frame.pack(fill=tk.BOTH, expand=True)
24
+ ttk.Label(frame, text="Alinux", font=("TkDefaultFont", 18, "bold")).pack(anchor=tk.W)
25
+ ttk.Label(frame, text="Linux desktop control through natural language").pack(anchor=tk.W, pady=(0, 12))
26
+
27
+ self.output = tk.Text(frame, height=16, state=tk.DISABLED, wrap=tk.WORD)
28
+ self.output.pack(fill=tk.BOTH, expand=True)
29
+ self.output.tag_configure("user", foreground="#245b9e")
30
+ self.output.tag_configure("assistant", foreground="#1f6b45")
31
+
32
+ controls = ttk.Frame(frame)
33
+ controls.pack(fill=tk.X, pady=(12, 0))
34
+ self.input = ttk.Entry(controls)
35
+ self.input.pack(side=tk.LEFT, fill=tk.X, expand=True)
36
+ self.input.bind("<Return>", lambda _event: self.submit())
37
+ ttk.Button(controls, text="Send", command=self.submit).pack(side=tk.LEFT, padx=(8, 0))
38
+ self.input.focus_set()
39
+ self._write("Alinux is ready. Ask for system status, volume changes, or an app to open.\n", "assistant")
40
+
41
+ def _write(self, text: str, tag: str) -> None:
42
+ self.output.configure(state=tk.NORMAL)
43
+ self.output.insert(tk.END, text, tag)
44
+ self.output.see(tk.END)
45
+ self.output.configure(state=tk.DISABLED)
46
+
47
+ def submit(self) -> None:
48
+ request = self.input.get().strip()
49
+ if not request:
50
+ return
51
+ self.input.delete(0, tk.END)
52
+ self._write(f"You: {request}\n", "user")
53
+ try:
54
+ response = _dispatch(self.agent.decide(request))
55
+ except Exception as exc: # noqa: BLE001 - GUI must remain open after a request failure
56
+ response = f"Alinux recovered from an error: {exc}"
57
+ self._write(f"Alinux: {response}\n\n", "assistant")
58
+
59
+
60
+ def open_gui(agent: Agent | None = None) -> int:
61
+ """Open the Alinux desktop window and return a process exit status."""
62
+ try:
63
+ root = tk.Tk()
64
+ except (ImportError, tk.TclError) as exc:
65
+ print(f"Could not open the Alinux GUI: {exc}")
66
+ print("Install python3-tk and ensure DISPLAY points to the Fluxbox session.")
67
+ return 1
68
+ AlinuxWindow(root, agent)
69
+ root.mainloop()
70
+ return 0
@@ -0,0 +1,66 @@
1
+ """Command-line entry point for Alinux."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+
7
+ from . import system_controls
8
+ from .agent import Agent
9
+
10
+
11
+ def _dispatch(agent_action) -> str:
12
+ if agent_action.action == "launch_app":
13
+ return system_controls.launch_app(agent_action.parameter)
14
+ if agent_action.action == "system_command":
15
+ return system_controls.execute_system_command(agent_action.parameter)
16
+ return agent_action.parameter
17
+
18
+
19
+ def _interactive() -> None:
20
+ """Run the resilient interactive terminal loop."""
21
+ print("Alinux ready. Type a request, or 'quit' to exit.")
22
+ agent = Agent()
23
+ while True:
24
+ try:
25
+ user_text = input("alinux> ").strip()
26
+ except (EOFError, KeyboardInterrupt):
27
+ print("\nAlinux stopped.")
28
+ return
29
+ if user_text.lower() in {"quit", "exit"}:
30
+ print("Alinux stopped.")
31
+ return
32
+ try:
33
+ print(_dispatch(agent.decide(user_text)))
34
+ except Exception as exc: # noqa: BLE001 - the interactive loop must recover
35
+ print(f"Alinux recovered from an error: {exc}")
36
+
37
+
38
+ def main() -> int:
39
+ """Run the GUI, Fluxbox installer, or terminal interface."""
40
+ parser = argparse.ArgumentParser(prog="alinux", description="Alinux Linux desktop assistant")
41
+ parser.add_argument(
42
+ "command",
43
+ nargs="?",
44
+ choices=("open", "install-fluxbox-menu"),
45
+ help="open the GUI or install its Fluxbox context-menu entry",
46
+ )
47
+ args = parser.parse_args()
48
+ if args.command == "open":
49
+ try:
50
+ from .gui import open_gui
51
+ except ImportError:
52
+ print("Could not open the Alinux GUI: install Debian's python3-tk package.")
53
+ return 1
54
+
55
+ return open_gui()
56
+ if args.command == "install-fluxbox-menu":
57
+ from .fluxbox import install_fluxbox_menu
58
+
59
+ print(install_fluxbox_menu())
60
+ return 0
61
+ _interactive()
62
+ return 0
63
+
64
+
65
+ if __name__ == "__main__":
66
+ main()
@@ -0,0 +1,156 @@
1
+ """Small, predictable Linux desktop control functions for Alinux.
2
+
3
+ All subprocess calls use argument lists and timeouts. Operations intentionally
4
+ cover a narrow set of desktop actions so an LLM cannot directly execute an
5
+ arbitrary shell command.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import shutil
12
+ import subprocess
13
+ from collections.abc import Sequence
14
+
15
+ _DEFAULT_TIMEOUT = 8
16
+ _ALLOWED_APPS = {
17
+ "terminal": ("x-terminal-emulator",),
18
+ "browser": ("xdg-open", "https://www.google.com"),
19
+ "file manager": ("xdg-open", os.path.expanduser("~")),
20
+ }
21
+
22
+
23
+ def _run(command: Sequence[str], timeout: int = _DEFAULT_TIMEOUT) -> subprocess.CompletedProcess[str]:
24
+ """Run a desktop command without invoking a shell."""
25
+ return subprocess.run(
26
+ list(command),
27
+ check=False,
28
+ capture_output=True,
29
+ text=True,
30
+ timeout=timeout,
31
+ )
32
+
33
+
34
+ def _tool_available(tool: str) -> bool:
35
+ return shutil.which(tool) is not None
36
+
37
+
38
+ def launch_app(application: str) -> str:
39
+ """Launch a known desktop application by friendly name."""
40
+ name = application.strip().lower()
41
+ command = _ALLOWED_APPS.get(name)
42
+ if command is None:
43
+ return f"I cannot launch {application!r}; supported apps are terminal, browser, and file manager."
44
+ if not _tool_available(command[0]):
45
+ return f"Cannot launch {name}: required command {command[0]!r} is not installed."
46
+ try:
47
+ subprocess.Popen(list(command), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
48
+ except OSError as exc:
49
+ return f"Could not launch {name}: {exc}"
50
+ return f"Launched {name}."
51
+
52
+
53
+ def adjust_volume(change: str) -> str:
54
+ """Adjust ALSA master volume using amixer."""
55
+ if not _tool_available("amixer"):
56
+ return "Cannot adjust volume: amixer is not installed."
57
+ normalized = change.strip().lower()
58
+ if normalized in {"up", "increase", "louder", "+"}:
59
+ value = "5%+"
60
+ description = "increased"
61
+ elif normalized in {"down", "decrease", "quieter", "-"}:
62
+ value = "5%-"
63
+ description = "decreased"
64
+ elif normalized in {"mute", "muted"}:
65
+ value = "toggle"
66
+ description = "toggled"
67
+ else:
68
+ return "Volume parameter must be up, down, or mute."
69
+ try:
70
+ result = _run(("amixer", "-q", "sset", "Master", value))
71
+ except (OSError, subprocess.TimeoutExpired) as exc:
72
+ return f"Volume adjustment failed: {exc}"
73
+ if result.returncode != 0:
74
+ detail = result.stderr.strip() or "amixer returned an error"
75
+ return f"Volume adjustment failed: {detail}"
76
+ return f"Volume {description}."
77
+
78
+
79
+ def list_windows() -> str:
80
+ """Return active window IDs and titles from xdotool."""
81
+ if not _tool_available("xdotool"):
82
+ return "Cannot list windows: xdotool is not installed."
83
+ try:
84
+ result = _run(("xdotool", "search", "--name", "."))
85
+ except (OSError, subprocess.TimeoutExpired) as exc:
86
+ return f"Window listing failed: {exc}"
87
+ if result.returncode != 0 or not result.stdout.strip():
88
+ return "No active windows found."
89
+ windows: list[str] = []
90
+ for window_id in result.stdout.splitlines():
91
+ window_id = window_id.strip()
92
+ if not window_id:
93
+ continue
94
+ title_result = _run(("xdotool", "getwindowname", window_id))
95
+ title = title_result.stdout.strip() if title_result.returncode == 0 else "(untitled)"
96
+ windows.append(f"{window_id}: {title}")
97
+ return "Active windows:\n" + "\n".join(windows) if windows else "No active windows found."
98
+
99
+
100
+ def manage_window(action: str, window_id: str) -> str:
101
+ """Close or minimize one window by its xdotool ID."""
102
+ if not _tool_available("xdotool"):
103
+ return "Cannot manage windows: xdotool is not installed."
104
+ normalized = action.strip().lower()
105
+ command = {"close": "windowclose", "minimize": "windowminimize"}.get(normalized)
106
+ if command is None or not window_id.strip().isdigit():
107
+ return "Window action must be close or minimize, with a numeric window ID."
108
+ try:
109
+ result = _run(("xdotool", command, window_id.strip()))
110
+ except (OSError, subprocess.TimeoutExpired) as exc:
111
+ return f"Window action failed: {exc}"
112
+ if result.returncode != 0:
113
+ return f"Window action failed: {result.stderr.strip() or 'xdotool returned an error'}"
114
+ return f"Window {window_id.strip()} {normalized}d."
115
+
116
+
117
+ def system_status() -> str:
118
+ """Return concise CPU, memory, and battery information."""
119
+ status: list[str] = []
120
+ if _tool_available("uptime"):
121
+ result = _run(("uptime",))
122
+ if result.returncode == 0:
123
+ status.append(f"Load: {result.stdout.strip()}")
124
+ try:
125
+ memory = _run(("free", "-h"))
126
+ memory_line = next((line for line in memory.stdout.splitlines() if line.startswith("Mem:")), None)
127
+ if memory_line:
128
+ fields = memory_line.split()
129
+ status.append(f"Memory: {fields[2]} used of {fields[1]}" if len(fields) > 2 else f"Memory: {memory_line}")
130
+ except (OSError, subprocess.TimeoutExpired):
131
+ status.append("Memory: unavailable")
132
+ battery_path = "/sys/class/power_supply"
133
+ batteries = [entry for entry in os.listdir(battery_path)] if os.path.isdir(battery_path) else []
134
+ battery = next((entry for entry in batteries if entry.startswith("BAT")), None)
135
+ if battery:
136
+ capacity_file = os.path.join(battery_path, battery, "capacity")
137
+ try:
138
+ with open(capacity_file, encoding="ascii") as file:
139
+ status.append(f"Battery: {file.read().strip()}%")
140
+ except OSError:
141
+ status.append("Battery: unavailable")
142
+ else:
143
+ status.append("Battery: not detected")
144
+ return "System status:\n" + "\n".join(status)
145
+
146
+
147
+ def execute_system_command(command: str) -> str:
148
+ """Execute a named safe system operation, never an arbitrary shell string."""
149
+ normalized = command.strip().lower()
150
+ if normalized in {"status", "system status", "info"}:
151
+ return system_status()
152
+ if normalized in {"windows", "list windows", "active windows"}:
153
+ return list_windows()
154
+ if normalized in {"volume up", "volume down", "volume mute"}:
155
+ return adjust_volume(normalized.removeprefix("volume "))
156
+ return "Unknown system command. Supported commands: status, windows, volume up, volume down, volume mute."
@@ -0,0 +1,46 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.25"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "alinux"
7
+ version = "0.1.0"
8
+ description = "Alinux AI orchestration shell for minimal Linux desktops"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Alinux contributors" }]
13
+ keywords = ["ai", "linux", "desktop", "automation", "fluxbox"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Environment :: Console",
17
+ "Intended Audience :: End Users/Desktop",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: POSIX :: Linux",
20
+ "Programming Language :: Python :: 3",
21
+ "Topic :: Desktop Environment",
22
+ ]
23
+ dependencies = [
24
+ "openai>=1.40.0,<2",
25
+ "pydantic>=2.7.0,<3",
26
+ ]
27
+
28
+ [project.optional-dependencies]
29
+ dev = [
30
+ "build>=1.2.0",
31
+ "pytest>=8.0.0",
32
+ "ruff>=0.6.0",
33
+ ]
34
+
35
+ [project.scripts]
36
+ alinux = "alinux.main:main"
37
+
38
+ [tool.hatch.build.targets.wheel]
39
+ packages = ["alinux"]
40
+
41
+ [tool.ruff]
42
+ target-version = "py310"
43
+ line-length = 100
44
+
45
+ [tool.pytest.ini_options]
46
+ testpaths = ["tests"]
@@ -0,0 +1,19 @@
1
+ from alinux.agent import Agent
2
+
3
+
4
+ def test_empty_input_returns_safe_response(monkeypatch):
5
+ monkeypatch.delenv("OPENAI_API_KEY", raising=False)
6
+
7
+ result = Agent().decide(" ")
8
+
9
+ assert result.action == "respond_to_user"
10
+ assert "Please enter" in result.parameter
11
+
12
+
13
+ def test_without_api_key_does_not_call_model(monkeypatch):
14
+ monkeypatch.delenv("OPENAI_API_KEY", raising=False)
15
+
16
+ result = Agent().decide("show status")
17
+
18
+ assert result.action == "respond_to_user"
19
+ assert "OPENAI_API_KEY" in result.parameter
@@ -0,0 +1,24 @@
1
+ from alinux.agent import Agent
2
+
3
+
4
+ def test_groq_provider_selects_builtin_model_and_endpoint(monkeypatch):
5
+ monkeypatch.setenv("ALINUX_PROVIDER", "groq")
6
+ monkeypatch.setenv("ALINUX_API_KEY", "test-key")
7
+
8
+ agent = Agent()
9
+
10
+ assert agent.model == "llama-3.3-70b-versatile"
11
+ assert agent.base_url == "https://api.groq.com/openai/v1"
12
+ assert agent.api_key == "test-key"
13
+
14
+
15
+ def test_custom_openai_compatible_configuration(monkeypatch):
16
+ monkeypatch.delenv("ALINUX_PROVIDER", raising=False)
17
+ monkeypatch.setenv("ALINUX_API_KEY", "custom-key")
18
+ monkeypatch.setenv("ALINUX_API_BASE_URL", "https://example.test/v1")
19
+
20
+ agent = Agent(model="local-model")
21
+
22
+ assert agent.model == "local-model"
23
+ assert agent.base_url == "https://example.test/v1"
24
+ assert agent.api_key == "custom-key"
@@ -0,0 +1,14 @@
1
+ from pathlib import Path
2
+
3
+ from alinux.fluxbox import install_fluxbox_menu
4
+
5
+
6
+ def test_fluxbox_menu_entry_is_added_once(tmp_path: Path):
7
+ menu_path = tmp_path / "menu"
8
+ menu_path.write_text("[begin] (Fluxbox)\n[end]\n", encoding="utf-8")
9
+
10
+ install_fluxbox_menu(menu_path)
11
+ install_fluxbox_menu(menu_path)
12
+
13
+ content = menu_path.read_text(encoding="utf-8")
14
+ assert content.count("[exec] (Open Alinux) {alinux open}") == 1
@@ -0,0 +1,15 @@
1
+ from alinux import system_controls
2
+ from alinux.agent import AgentAction
3
+ from alinux.main import _dispatch
4
+
5
+
6
+ def test_arbitrary_system_commands_are_rejected():
7
+ result = system_controls.execute_system_command("rm -rf /")
8
+
9
+ assert result.startswith("Unknown system command")
10
+
11
+
12
+ def test_dispatch_responds_without_desktop_access():
13
+ result = _dispatch(AgentAction(action="respond_to_user", parameter="Hello"))
14
+
15
+ assert result == "Hello"