silicon-cli 1.0.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Saket
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,74 @@
1
+ Metadata-Version: 2.4
2
+ Name: silicon-cli
3
+ Version: 1.0.0
4
+ Summary: Silicon CLI — create, run, and manage your silicon instances.
5
+ Author: Saket
6
+ Project-URL: Homepage, https://github.com/saket1225/silicon-cli
7
+ Project-URL: Repository, https://github.com/saket1225/silicon-cli
8
+ Keywords: silicon,cli,agents
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.9
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Dynamic: license-file
16
+
17
+ # silicon-cli
18
+
19
+ Our own **`silicon`** CLI — a Python (pip-installable) port of the original bash
20
+ silicon manager. Manages silicon instances on a machine: create them from the
21
+ [silicon-stemcell](https://github.com/unlikefraction/silicon-stemcell) base,
22
+ start/stop them under an auto-restart watchdog, stream logs, and back them up to
23
+ Glass. It reads the same `~/.silicon/registry.json`, so existing installs carry
24
+ over unchanged.
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ pip install silicon-cli
30
+ ```
31
+
32
+ (Zero runtime dependencies — stdlib only.)
33
+
34
+ ## Commands
35
+
36
+ ```
37
+ silicon Show status or list instances
38
+ silicon new [dir] Create a new Silicon (hydrate from stemcell)
39
+ silicon new . Hydrate the current folder into a runnable silicon
40
+ silicon start <target> Start silicon(s). target = name, index, 1,2,4, or all
41
+ silicon stop [--full] <target> Stop silicon(s) (--full also stops the glass agent)
42
+ silicon restart <target> Restart silicon(s)
43
+ silicon agent <start|stop|status> [name] Manage the per-silicon glass agent
44
+ silicon status [name] Show instance status
45
+ silicon browser [name] Open a headed browser for login
46
+ silicon debug [name] Tail a running instance's logs
47
+ silicon attach [path] Register an existing silicon directory
48
+ silicon pull <username> Pull a silicon from Glass into a new folder
49
+ silicon push [name] [now|stop] Hourly backups to Glass (now = one-shot, stop = end loop)
50
+ silicon update <target> Update silicon(s) from the latest stemcell
51
+ silicon list List all instances
52
+ silicon script update Update this CLI itself
53
+ silicon help Show help
54
+ ```
55
+
56
+ ## Configuration (env vars)
57
+
58
+ | Var | Default | Purpose |
59
+ | --- | --- | --- |
60
+ | `SILICON_HOME` | `~/.silicon` | registry + CLI state |
61
+ | `GLASS_SERVER_URL` | `https://glass.unlikefraction.com` | Glass sync server (pull/push) |
62
+ | `SILICON_STEMCELL_REPO` | `unlikefraction/silicon-stemcell` | base for `new` |
63
+ | `SILICON_GLASS_CLI_REPO` | `unlikefraction/glass` | glass backup CLI |
64
+ | `SILICON_PYTHON` | `python3` | interpreter used to run a silicon's `main.py` |
65
+
66
+ ## How it differs from the bash version
67
+
68
+ - Pure Python package with a `silicon` console entry point (installed in an
69
+ isolated venv), instead of a single bash script.
70
+ - The auto-restart watchdog runs as `silicon _watchdog` (a detached supervisor
71
+ process) rather than a backgrounded bash function — same crash-loop detection,
72
+ `.silicon.stop` sentinel, and `.silicon.pid`/`.silicon.log` behavior.
73
+ - `silicon script update` reinstalls the package via pip from its recorded source.
74
+ - Everything (server, stemcell repo) is env-overridable.
@@ -0,0 +1,58 @@
1
+ # silicon-cli
2
+
3
+ Our own **`silicon`** CLI — a Python (pip-installable) port of the original bash
4
+ silicon manager. Manages silicon instances on a machine: create them from the
5
+ [silicon-stemcell](https://github.com/unlikefraction/silicon-stemcell) base,
6
+ start/stop them under an auto-restart watchdog, stream logs, and back them up to
7
+ Glass. It reads the same `~/.silicon/registry.json`, so existing installs carry
8
+ over unchanged.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ pip install silicon-cli
14
+ ```
15
+
16
+ (Zero runtime dependencies — stdlib only.)
17
+
18
+ ## Commands
19
+
20
+ ```
21
+ silicon Show status or list instances
22
+ silicon new [dir] Create a new Silicon (hydrate from stemcell)
23
+ silicon new . Hydrate the current folder into a runnable silicon
24
+ silicon start <target> Start silicon(s). target = name, index, 1,2,4, or all
25
+ silicon stop [--full] <target> Stop silicon(s) (--full also stops the glass agent)
26
+ silicon restart <target> Restart silicon(s)
27
+ silicon agent <start|stop|status> [name] Manage the per-silicon glass agent
28
+ silicon status [name] Show instance status
29
+ silicon browser [name] Open a headed browser for login
30
+ silicon debug [name] Tail a running instance's logs
31
+ silicon attach [path] Register an existing silicon directory
32
+ silicon pull <username> Pull a silicon from Glass into a new folder
33
+ silicon push [name] [now|stop] Hourly backups to Glass (now = one-shot, stop = end loop)
34
+ silicon update <target> Update silicon(s) from the latest stemcell
35
+ silicon list List all instances
36
+ silicon script update Update this CLI itself
37
+ silicon help Show help
38
+ ```
39
+
40
+ ## Configuration (env vars)
41
+
42
+ | Var | Default | Purpose |
43
+ | --- | --- | --- |
44
+ | `SILICON_HOME` | `~/.silicon` | registry + CLI state |
45
+ | `GLASS_SERVER_URL` | `https://glass.unlikefraction.com` | Glass sync server (pull/push) |
46
+ | `SILICON_STEMCELL_REPO` | `unlikefraction/silicon-stemcell` | base for `new` |
47
+ | `SILICON_GLASS_CLI_REPO` | `unlikefraction/glass` | glass backup CLI |
48
+ | `SILICON_PYTHON` | `python3` | interpreter used to run a silicon's `main.py` |
49
+
50
+ ## How it differs from the bash version
51
+
52
+ - Pure Python package with a `silicon` console entry point (installed in an
53
+ isolated venv), instead of a single bash script.
54
+ - The auto-restart watchdog runs as `silicon _watchdog` (a detached supervisor
55
+ process) rather than a backgrounded bash function — same crash-loop detection,
56
+ `.silicon.stop` sentinel, and `.silicon.pid`/`.silicon.log` behavior.
57
+ - `silicon script update` reinstalls the package via pip from its recorded source.
58
+ - Everything (server, stemcell repo) is env-overridable.
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "silicon-cli"
7
+ version = "1.0.0"
8
+ description = "Silicon CLI — create, run, and manage your silicon instances."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ authors = [{ name = "Saket" }]
12
+ keywords = ["silicon", "cli", "agents"]
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Operating System :: OS Independent",
17
+ ]
18
+ dependencies = [] # stdlib only — keeps the install bulletproof
19
+
20
+ [project.urls]
21
+ Homepage = "https://github.com/saket1225/silicon-cli"
22
+ Repository = "https://github.com/saket1225/silicon-cli"
23
+
24
+ [project.scripts]
25
+ silicon = "silicon_cli.cli:main"
26
+
27
+ [tool.setuptools]
28
+ packages = ["silicon_cli"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,2 @@
1
+ """Silicon CLI — manage silicon instances. Python port of the original bash manager."""
2
+ __version__ = "1.0.0"
@@ -0,0 +1,250 @@
1
+ """silicon — manage your silicon instances. Dispatch mirrors the original bash CLI."""
2
+ from __future__ import annotations
3
+
4
+ import subprocess
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from . import glassagent, process, registry, stemcell, sync, ui, update
9
+ from .config import python_run_cmd
10
+
11
+ COMMANDS = ["start", "stop", "restart", "status", "browser", "debug", "attach",
12
+ "pull", "push", "update", "list", "install", "new", "help", "script", "agent"]
13
+
14
+
15
+ # ----------------------------------------------------------------- commands
16
+ def cmd_list() -> None:
17
+ rows = registry.installs()
18
+ if not rows:
19
+ ui.info("No silicon installations found.")
20
+ ui.info("Run 'silicon install' to set up a new instance.")
21
+ return
22
+ print(f"\n{ui.BOLD}{ui.CYAN}Silicon Installations{ui.RESET}\n")
23
+ print(f" {ui.DIM}{'#':<4}{'NAME':<22}{'STATUS':<10}PATH{ui.RESET}")
24
+ print(f" {ui.DIM}{'---':<4}{'----':<22}{'------':<10}----{ui.RESET}")
25
+ for i in rows:
26
+ if process.is_running(i.pid_file):
27
+ pid = process.get_pid(i.pid_file)
28
+ status = f"{ui.GREEN}● running{ui.RESET}"
29
+ extra = f" {ui.DIM}(PID {pid}){ui.RESET}"
30
+ else:
31
+ status, extra = f"{ui.DIM}○ stopped{ui.RESET}", ""
32
+ print(f" {i.index + 1:<4}{i.name:<22}{status}{extra} {ui.DIM}{i.path}{ui.RESET}")
33
+ print()
34
+
35
+
36
+ def _print_status(inst) -> None:
37
+ if process.is_running(inst.pid_file):
38
+ pid = process.get_pid(inst.pid_file)
39
+ print(f"\n{ui.BOLD}{inst.name}{ui.RESET} {ui.GREEN}● running{ui.RESET} (PID {pid})")
40
+ else:
41
+ print(f"\n{ui.BOLD}{inst.name}{ui.RESET} {ui.DIM}○ stopped{ui.RESET}")
42
+ print(f"{ui.DIM} Path: {inst.path}{ui.RESET}\n")
43
+
44
+
45
+ def cmd_status(target: str | None) -> None:
46
+ if target:
47
+ inst = registry.find(target)
48
+ if not inst:
49
+ cmd_list()
50
+ return
51
+ _print_status(inst)
52
+ return
53
+ inst = registry.find()
54
+ if inst:
55
+ _print_status(inst)
56
+ else:
57
+ cmd_list()
58
+
59
+
60
+ def cmd_browser(target: str | None) -> None:
61
+ inst = registry.resolve_one(target)
62
+ ui.info(f"Opening browser for '{inst.name}'...")
63
+ subprocess.run([python_run_cmd(), "main.py", "browser"], cwd=inst.path)
64
+
65
+
66
+ def cmd_debug(target: str | None) -> None:
67
+ inst = registry.resolve_one(target)
68
+ if not process.is_running(inst.pid_file):
69
+ ui.error(f"'{inst.name}' is not running. Start it first with: silicon start {inst.name}")
70
+ sys.exit(1)
71
+ log_file = Path(inst.path) / ".silicon.log"
72
+ if not log_file.exists():
73
+ ui.error(f"No log file found at {log_file}")
74
+ sys.exit(1)
75
+ pid = process.get_pid(inst.pid_file)
76
+ print(f"\n{ui.BOLD}{ui.CYAN}Debugging '{inst.name}'{ui.RESET} (PID {pid})")
77
+ print(f"{ui.DIM} Log: {log_file}{ui.RESET}")
78
+ print(f"{ui.DIM} Press Ctrl+C to detach{ui.RESET}\n")
79
+ try:
80
+ subprocess.run(["tail", "-f", str(log_file)])
81
+ except KeyboardInterrupt:
82
+ pass
83
+
84
+
85
+ def cmd_attach(target_dir: str | None) -> None:
86
+ target = Path(target_dir or ".").resolve()
87
+ if not (target / "main.py").exists() or not (target / "config.py").exists():
88
+ ui.error("This doesn't look like a silicon directory.")
89
+ ui.info(f"Expected main.py and config.py in: {target}")
90
+ ui.info(" silicon attach /path/to/silicon")
91
+ sys.exit(1)
92
+ if not (target / "prompts").is_dir() or not (target / "core").is_dir():
93
+ ui.error("Missing prompts/ or core/ directory. Not a valid silicon.")
94
+ sys.exit(1)
95
+
96
+ for i in registry.installs():
97
+ if i.path == str(target):
98
+ ui.warn(f"This silicon is already registered as '{i.name}'")
99
+ return
100
+
101
+ ui.success(f"Found a silicon at: {target}")
102
+ name = ui.ask("Instance name", target.name)
103
+ if registry.name_taken(name):
104
+ ui.error(f"Name '{name}' is already taken. Pick a different one.")
105
+ sys.exit(1)
106
+
107
+ pid_file = target / ".silicon.pid"
108
+ running = process.is_running(str(pid_file))
109
+ registry.register(name, str(target), str(pid_file))
110
+ ui.success(f"Attached '{name}' at {target}")
111
+ if running:
112
+ print(f"\n {ui.BOLD}{name}{ui.RESET} {ui.GREEN}● running{ui.RESET} (PID {process.get_pid(str(pid_file))})\n")
113
+ else:
114
+ print(f"\n {ui.BOLD}{name}{ui.RESET} {ui.DIM}○ stopped{ui.RESET}")
115
+ print(f" Start it with: {ui.BOLD}silicon start {name}{ui.RESET}\n")
116
+
117
+
118
+ def cmd_agent(subcmd: str | None, target: str | None) -> None:
119
+ if not subcmd:
120
+ ui.error("Usage: silicon agent <start|stop|status> [name]")
121
+ sys.exit(1)
122
+ inst = registry.resolve_one(target)
123
+ if subcmd == "start":
124
+ glassagent.start(inst.path)
125
+ elif subcmd == "stop":
126
+ glassagent.stop(inst.path)
127
+ elif subcmd == "status":
128
+ if glassagent.status(inst.path):
129
+ pid = (Path(inst.path) / ".glass_agent.pid").read_text().strip()
130
+ print(f"{ui.GREEN}●{ui.RESET} Glass agent running (PID {pid})")
131
+ else:
132
+ print(f"{ui.DIM}○{ui.RESET} Glass agent stopped")
133
+ else:
134
+ ui.error(f"Unknown agent command: {subcmd}. Use start, stop, or status.")
135
+ sys.exit(1)
136
+
137
+
138
+ def cmd_new(target: str | None) -> None:
139
+ if target:
140
+ stemcell.hydrate(target)
141
+ return
142
+ # No target: ask for a folder to create (Python-native installer).
143
+ name = ui.ask("New silicon folder name", "silicon")
144
+ if not name:
145
+ ui.error("A folder name is required.")
146
+ sys.exit(1)
147
+ stemcell.hydrate(str(Path.cwd() / name))
148
+
149
+
150
+ def cmd_help() -> None:
151
+ print(f"""
152
+ {ui.BOLD}{ui.CYAN}silicon{ui.RESET} – manage your silicon instances
153
+
154
+ {ui.BOLD}Usage:{ui.RESET}
155
+ silicon Show status or list instances
156
+ silicon new [dir] Create a new Silicon (hydrate from stemcell)
157
+ silicon new . Hydrate the current folder into a runnable silicon
158
+ silicon start <target> Start silicon(s). target = name, index, 1,2,4, or all
159
+ silicon stop <target> Stop silicon(s) (agent stays alive)
160
+ silicon stop --full <target> Stop silicon(s) and glass agent
161
+ silicon restart <target> Restart silicon(s)
162
+ silicon agent <start|stop|status> [name] Manage glass agent
163
+ silicon status [name] Show instance status
164
+ silicon browser [name] Open headed browser for login
165
+ silicon debug [name] Attach to running instance (live logs)
166
+ silicon attach [path] Register an existing silicon instance
167
+ silicon pull <username> Pull a silicon from Glass into a new folder
168
+ silicon push [name] Start hourly backup loop to Glass
169
+ silicon push [name] now Push a one-time backup to Glass
170
+ silicon push [name] stop Stop the hourly backup loop
171
+ silicon update <target> Update silicon(s) to latest. target = name, index, 1,2,4, or all
172
+ silicon list List all instances
173
+ silicon script update Update the silicon CLI itself
174
+ silicon install Install a new instance
175
+ silicon help Show this help
176
+ """)
177
+
178
+
179
+ def suggest_command(inp: str) -> None:
180
+ def score(cmd: str) -> int:
181
+ ld = abs(len(inp) - len(cmd))
182
+ if cmd.startswith(inp) or inp.startswith(cmd):
183
+ return ld
184
+ common = sum(1 for a, b in zip(inp, cmd) if a == b)
185
+ return max(len(cmd), len(inp)) - common + ld
186
+ best = min(COMMANDS + ["ls"], key=score)
187
+ if score(best) <= 3:
188
+ print(f"\n{ui.YELLOW}Did you mean?{ui.RESET}\n silicon {best}\n")
189
+
190
+
191
+ # ----------------------------------------------------------------- dispatch
192
+ def main(argv: list[str] | None = None) -> None:
193
+ argv = argv if argv is not None else sys.argv[1:]
194
+ cmd = argv[0] if argv else ""
195
+ a1 = argv[1] if len(argv) > 1 else None
196
+ a2 = argv[2] if len(argv) > 2 else None
197
+
198
+ if cmd == "_watchdog": # internal: the supervised auto-restart loop
199
+ process.watchdog_loop(name=a2 or "silicon", path=a1, pid_file=argv[3] if len(argv) > 3 else "")
200
+ return
201
+
202
+ if cmd == "start":
203
+ process.start(a1)
204
+ elif cmd == "stop":
205
+ if a1 == "--full":
206
+ process.stop(a2, full=True)
207
+ else:
208
+ process.stop(a1, full=(a2 == "--full"))
209
+ elif cmd == "restart":
210
+ process.restart(a1)
211
+ elif cmd == "status":
212
+ cmd_status(a1)
213
+ elif cmd == "browser":
214
+ cmd_browser(a1)
215
+ elif cmd == "debug":
216
+ cmd_debug(a1)
217
+ elif cmd == "attach":
218
+ cmd_attach(a1)
219
+ elif cmd == "pull":
220
+ sync.pull(a1)
221
+ elif cmd == "push":
222
+ sync.push(a1, a2)
223
+ elif cmd == "update":
224
+ update.update_instance(a1)
225
+ elif cmd in ("list", "ls"):
226
+ cmd_list()
227
+ elif cmd == "agent":
228
+ cmd_agent(a1, a2)
229
+ elif cmd == "script":
230
+ if a1 == "update":
231
+ update.update_cli()
232
+ else:
233
+ ui.error(f"Unknown script command: {a1}. Did you mean: silicon script update?")
234
+ sys.exit(1)
235
+ elif cmd == "new":
236
+ cmd_new(a1)
237
+ elif cmd == "install":
238
+ cmd_new(None)
239
+ elif cmd in ("help", "-h", "--help"):
240
+ cmd_help()
241
+ elif cmd == "":
242
+ cmd_status(None)
243
+ else:
244
+ ui.error(f"Unknown command: {cmd}")
245
+ suggest_command(cmd)
246
+ sys.exit(1)
247
+
248
+
249
+ if __name__ == "__main__":
250
+ main()
@@ -0,0 +1,29 @@
1
+ """Paths + endpoints. Everything is env-overridable so this CLI can point at
2
+ either the original Glass or your own."""
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shutil
7
+ from pathlib import Path
8
+
9
+ HOME = Path.home()
10
+ REGISTRY_DIR = Path(os.environ.get("SILICON_HOME", HOME / ".silicon"))
11
+ REGISTRY_FILE = REGISTRY_DIR / "registry.json"
12
+ CLI_SOURCE_FILE = REGISTRY_DIR / "cli-source" # where `silicon script update` reinstalls from
13
+
14
+ # Glass sync server (pull/push). Kept as the original default for compatibility;
15
+ # override with GLASS_SERVER_URL to point at your own.
16
+ GLASS_SERVER_URL = os.environ.get("GLASS_SERVER_URL", "https://glass.unlikefraction.com").rstrip("/")
17
+
18
+ # Stemcell — the base every new silicon is hydrated from.
19
+ STEMCELL_REPO = os.environ.get("SILICON_STEMCELL_REPO", "unlikefraction/silicon-stemcell")
20
+ STEMCELL_GIT_URL = f"https://github.com/{STEMCELL_REPO}.git"
21
+ STEMCELL_ZIP_URL = f"https://github.com/{STEMCELL_REPO}/archive/refs/heads/main.zip"
22
+
23
+ # Glass CLI (used by pull/push for backups).
24
+ GLASS_CLI_REPO = os.environ.get("SILICON_GLASS_CLI_REPO", "unlikefraction/glass")
25
+
26
+
27
+ def python_run_cmd() -> str:
28
+ """The interpreter used to RUN a silicon's main.py (not this CLI's venv)."""
29
+ return os.environ.get("SILICON_PYTHON") or shutil.which("python3") or shutil.which("python") or "python3"
@@ -0,0 +1,66 @@
1
+ """The per-silicon Glass agent (glass_agent.py) — remote control / backups.
2
+
3
+ Only relevant when the silicon dir has a .glass.json. Tracked via .glass_agent.pid.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import os
8
+ import signal
9
+ import subprocess
10
+ import time
11
+ from pathlib import Path
12
+
13
+ from . import ui
14
+ from .config import python_run_cmd
15
+
16
+
17
+ def _pid_file(path: str) -> Path:
18
+ return Path(path) / ".glass_agent.pid"
19
+
20
+
21
+ def _read_pid(path: str) -> int | None:
22
+ try:
23
+ return int(_pid_file(path).read_text().strip())
24
+ except Exception:
25
+ return None
26
+
27
+
28
+ def _alive(pid: int) -> bool:
29
+ try:
30
+ os.kill(pid, 0)
31
+ return True
32
+ except (OSError, ProcessLookupError):
33
+ return False
34
+
35
+
36
+ def status(path: str) -> bool:
37
+ pid = _read_pid(path)
38
+ return bool(pid and _alive(pid))
39
+
40
+
41
+ def start(path: str) -> None:
42
+ if not (Path(path) / ".glass.json").exists():
43
+ return
44
+ if status(path):
45
+ return
46
+ log = open(Path(path) / ".glass_agent.log", "a")
47
+ proc = subprocess.Popen(
48
+ [python_run_cmd(), "-u", "glass_agent.py"], cwd=path,
49
+ stdout=log, stderr=subprocess.STDOUT, start_new_session=True,
50
+ )
51
+ _pid_file(path).write_text(str(proc.pid))
52
+ ui.info(f"Glass agent started (PID {proc.pid})")
53
+
54
+
55
+ def stop(path: str) -> None:
56
+ pid = _read_pid(path)
57
+ if pid and _alive(pid):
58
+ try:
59
+ os.kill(pid, signal.SIGTERM)
60
+ time.sleep(1)
61
+ if _alive(pid):
62
+ os.kill(pid, signal.SIGKILL)
63
+ except (OSError, ProcessLookupError):
64
+ pass
65
+ ui.info("Glass agent stopped")
66
+ _pid_file(path).unlink(missing_ok=True)