silicon-cli 1.0.8__tar.gz → 1.0.11__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.
Files changed (22) hide show
  1. {silicon_cli-1.0.8/silicon_cli.egg-info → silicon_cli-1.0.11}/PKG-INFO +13 -10
  2. {silicon_cli-1.0.8 → silicon_cli-1.0.11}/README.md +10 -8
  3. {silicon_cli-1.0.8 → silicon_cli-1.0.11}/pyproject.toml +1 -1
  4. {silicon_cli-1.0.8 → silicon_cli-1.0.11}/silicon_cli/__init__.py +1 -1
  5. {silicon_cli-1.0.8 → silicon_cli-1.0.11}/silicon_cli/cli.py +20 -4
  6. {silicon_cli-1.0.8 → silicon_cli-1.0.11}/silicon_cli/config.py +25 -2
  7. {silicon_cli-1.0.8 → silicon_cli-1.0.11}/silicon_cli/glassagent.py +1 -1
  8. {silicon_cli-1.0.8 → silicon_cli-1.0.11}/silicon_cli/process.py +2 -2
  9. {silicon_cli-1.0.8 → silicon_cli-1.0.11}/silicon_cli/registry.py +21 -12
  10. {silicon_cli-1.0.8 → silicon_cli-1.0.11}/silicon_cli/stemcell.py +54 -5
  11. {silicon_cli-1.0.8 → silicon_cli-1.0.11}/silicon_cli/sync.py +130 -9
  12. {silicon_cli-1.0.8 → silicon_cli-1.0.11}/silicon_cli/update.py +6 -3
  13. {silicon_cli-1.0.8 → silicon_cli-1.0.11/silicon_cli.egg-info}/PKG-INFO +13 -10
  14. {silicon_cli-1.0.8 → silicon_cli-1.0.11}/LICENSE +0 -0
  15. {silicon_cli-1.0.8 → silicon_cli-1.0.11}/setup.cfg +0 -0
  16. {silicon_cli-1.0.8 → silicon_cli-1.0.11}/silicon_cli/interface_cli.py +0 -0
  17. {silicon_cli-1.0.8 → silicon_cli-1.0.11}/silicon_cli/ui.py +0 -0
  18. {silicon_cli-1.0.8 → silicon_cli-1.0.11}/silicon_cli.egg-info/SOURCES.txt +0 -0
  19. {silicon_cli-1.0.8 → silicon_cli-1.0.11}/silicon_cli.egg-info/dependency_links.txt +0 -0
  20. {silicon_cli-1.0.8 → silicon_cli-1.0.11}/silicon_cli.egg-info/entry_points.txt +0 -0
  21. {silicon_cli-1.0.8 → silicon_cli-1.0.11}/silicon_cli.egg-info/requires.txt +0 -0
  22. {silicon_cli-1.0.8 → silicon_cli-1.0.11}/silicon_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: silicon-cli
3
- Version: 1.0.8
3
+ Version: 1.0.11
4
4
  Summary: Silicon CLI — create, run, and manage your silicon instances.
5
5
  Author: Saket
6
6
  Project-URL: Homepage, https://github.com/saket1225/silicon-cli
@@ -13,6 +13,7 @@ Requires-Python: >=3.9
13
13
  Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
15
  Requires-Dist: certifi>=2024.2.2
16
+ Dynamic: license-file
16
17
 
17
18
  # silicon-cli
18
19
 
@@ -40,12 +41,14 @@ pip install silicon-cli
40
41
  silicon Show status or list instances
41
42
  silicon new [dir] Create a new Silicon (hydrate from stemcell)
42
43
  silicon new . Hydrate the current folder into a runnable silicon
43
- silicon start <target> Start silicon(s). target = name, index, 1,2,4, or all
44
- silicon stop [--full] <target> Stop silicon(s) (--full also stops the glass agent)
45
- silicon restart <target> Restart silicon(s)
44
+ silicon start <target> Start silicon(s). target = name, *, all, 1,2,4, or name,name
45
+ silicon stop [--full] <target> Stop silicon(s). target = name, *, all, 1,2,4, or name,name
46
+ silicon restart <target> Restart silicon(s). target = name, *, all, 1,2,4, or name,name
46
47
  silicon agent <start|stop|status> [name] Manage the per-silicon glass agent
47
48
  silicon status [name] Show instance status
48
49
  silicon browser [name] Open a headed browser for login
50
+ silicon browser-profile setup <token> Create a Steel browser profile through Glass
51
+ silicon browser-profile finish <token> <session_id> [before_ids_csv] Finish profile setup
49
52
  silicon debug [name] Tail a running instance's logs
50
53
  silicon attach [path] Register an existing silicon directory
51
54
  silicon pull [api_token] Pull a Glass team or silicon into local folders
@@ -92,8 +95,8 @@ silicon pull sct_live_...
92
95
 
93
96
  The command validates the token with Glass, mints one local silicon API key per
94
97
  team silicon, creates one folder per silicon, hydrates the stemcell, writes
95
- `.glass.json`, `.env`, and `env.py`, registers each instance, and starts each
96
- Silicon Interface daemon.
98
+ `.glass.json`, `.env`, and `env.py`, registers each instance, starts each
99
+ Silicon Interface daemon, and starts each silicon process.
97
100
 
98
101
  During team pull, setup asks for the default brain/fallback settings once. You
99
102
  can apply those settings to every silicon, or select specific silicons that
@@ -102,9 +105,9 @@ Older per-silicon `scs_live_...` tokens still pull just that one silicon.
102
105
 
103
106
  Provider API keys for voice, browser profiles, billing, and architecture
104
107
  generation are configured on the Glass backend. After the silicon API token is
105
- validated, `silicon pull` reads the team's provider-key metadata from Glass,
106
- shows each provider as saved/missing without returning plaintext secrets, and
107
- asks whether the pulled silicon(s) should use the Glass-managed keys. A
108
+ validated, `silicon pull` reads the team's provider-key metadata from Glass and
109
+ automatically marks the pulled silicon(s) to use Glass-managed keys when every
110
+ provider is available. Plaintext secrets are never returned to the CLI. A
108
111
  Glass-pulled silicon only stores its Glass API token plus a local marker that
109
112
  provider keys come from Glass.
110
113
 
@@ -24,12 +24,14 @@ pip install silicon-cli
24
24
  silicon Show status or list instances
25
25
  silicon new [dir] Create a new Silicon (hydrate from stemcell)
26
26
  silicon new . Hydrate the current folder into a runnable silicon
27
- silicon start <target> Start silicon(s). target = name, index, 1,2,4, or all
28
- silicon stop [--full] <target> Stop silicon(s) (--full also stops the glass agent)
29
- silicon restart <target> Restart silicon(s)
27
+ silicon start <target> Start silicon(s). target = name, *, all, 1,2,4, or name,name
28
+ silicon stop [--full] <target> Stop silicon(s). target = name, *, all, 1,2,4, or name,name
29
+ silicon restart <target> Restart silicon(s). target = name, *, all, 1,2,4, or name,name
30
30
  silicon agent <start|stop|status> [name] Manage the per-silicon glass agent
31
31
  silicon status [name] Show instance status
32
32
  silicon browser [name] Open a headed browser for login
33
+ silicon browser-profile setup <token> Create a Steel browser profile through Glass
34
+ silicon browser-profile finish <token> <session_id> [before_ids_csv] Finish profile setup
33
35
  silicon debug [name] Tail a running instance's logs
34
36
  silicon attach [path] Register an existing silicon directory
35
37
  silicon pull [api_token] Pull a Glass team or silicon into local folders
@@ -76,8 +78,8 @@ silicon pull sct_live_...
76
78
 
77
79
  The command validates the token with Glass, mints one local silicon API key per
78
80
  team silicon, creates one folder per silicon, hydrates the stemcell, writes
79
- `.glass.json`, `.env`, and `env.py`, registers each instance, and starts each
80
- Silicon Interface daemon.
81
+ `.glass.json`, `.env`, and `env.py`, registers each instance, starts each
82
+ Silicon Interface daemon, and starts each silicon process.
81
83
 
82
84
  During team pull, setup asks for the default brain/fallback settings once. You
83
85
  can apply those settings to every silicon, or select specific silicons that
@@ -86,9 +88,9 @@ Older per-silicon `scs_live_...` tokens still pull just that one silicon.
86
88
 
87
89
  Provider API keys for voice, browser profiles, billing, and architecture
88
90
  generation are configured on the Glass backend. After the silicon API token is
89
- validated, `silicon pull` reads the team's provider-key metadata from Glass,
90
- shows each provider as saved/missing without returning plaintext secrets, and
91
- asks whether the pulled silicon(s) should use the Glass-managed keys. A
91
+ validated, `silicon pull` reads the team's provider-key metadata from Glass and
92
+ automatically marks the pulled silicon(s) to use Glass-managed keys when every
93
+ provider is available. Plaintext secrets are never returned to the CLI. A
92
94
  Glass-pulled silicon only stores its Glass API token plus a local marker that
93
95
  provider keys come from Glass.
94
96
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "silicon-cli"
7
- version = "1.0.8"
7
+ version = "1.0.11"
8
8
  description = "Silicon CLI — create, run, and manage your silicon instances."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,2 +1,2 @@
1
1
  """Silicon CLI — manage silicon instances. Python port of the original bash manager."""
2
- __version__ = "1.0.8"
2
+ __version__ = "1.0.10"
@@ -10,7 +10,7 @@ from .config import python_run_cmd
10
10
 
11
11
  COMMANDS = ["start", "stop", "restart", "status", "browser", "debug", "attach",
12
12
  "pull", "push", "backup", "update", "update-check", "check-update",
13
- "list", "install", "new", "help", "script", "agent"]
13
+ "browser-profile", "list", "install", "new", "help", "script", "agent"]
14
14
 
15
15
 
16
16
  # ----------------------------------------------------------------- commands
@@ -61,7 +61,7 @@ def cmd_status(target: str | None) -> None:
61
61
  def cmd_browser(target: str | None) -> None:
62
62
  inst = registry.resolve_one(target)
63
63
  ui.info(f"Opening browser for '{inst.name}'...")
64
- subprocess.run([python_run_cmd(), "main.py", "browser"], cwd=inst.path)
64
+ subprocess.run([python_run_cmd(inst.path), "main.py", "browser"], cwd=inst.path)
65
65
 
66
66
 
67
67
  def cmd_debug(target: str | None) -> None:
@@ -156,13 +156,17 @@ def cmd_help() -> None:
156
156
  silicon Show status or list instances
157
157
  silicon new [dir] Create a new Silicon (hydrate from stemcell)
158
158
  silicon new . Hydrate the current folder into a runnable silicon
159
- silicon start <target> Start silicon(s). target = name, index, 1,2,4, or all
159
+ silicon start <target> Start silicon(s). target = name, *, all, 1,2,4, or name,name
160
160
  silicon stop <target> Stop silicon(s) (agent stays alive)
161
161
  silicon stop --full <target> Stop silicon(s) and glass agent
162
162
  silicon restart <target> Restart silicon(s)
163
163
  silicon agent <start|stop|status> [name] Manage glass agent
164
164
  silicon status [name] Show instance status
165
165
  silicon browser [name] Open headed browser for login
166
+ silicon browser-profile setup <token>
167
+ Create a Steel browser profile through Glass
168
+ silicon browser-profile finish <token> <session_id> [before_ids_csv]
169
+ Finish a browser profile setup session
166
170
  silicon debug [name] Attach to running instance (live logs)
167
171
  silicon attach [path] Register an existing silicon instance
168
172
  silicon pull [api_token] Pull a Glass team or silicon into local folders
@@ -170,7 +174,7 @@ def cmd_help() -> None:
170
174
  silicon push [name] now Push a one-time backup to Glass
171
175
  silicon push [name] stop Stop the daily backup loop
172
176
  silicon backup [name] [now|stop] Alias for silicon push
173
- silicon update <target> Update silicon(s) to latest. target = name, index, 1,2,4, or all
177
+ silicon update <target> Update silicon(s) to latest. target = name, *, all, 1,2,4, or name,name
174
178
  silicon update check [name] Trigger this silicon's system update check now
175
179
  silicon update-check [name] Trigger this silicon's system update check now
176
180
  silicon list List all instances
@@ -220,6 +224,18 @@ def main(argv: list[str] | None = None) -> None:
220
224
  cmd_status(a1)
221
225
  elif cmd == "browser":
222
226
  cmd_browser(a1)
227
+ elif cmd == "browser-profile":
228
+ if a1 == "setup":
229
+ sync.browser_profile_setup(a2)
230
+ elif a1 == "finish":
231
+ sync.browser_profile_finish(
232
+ a2,
233
+ argv[3] if len(argv) > 3 else None,
234
+ argv[4] if len(argv) > 4 else None,
235
+ )
236
+ else:
237
+ ui.error("Usage: silicon browser-profile <setup|finish> ...")
238
+ sys.exit(1)
223
239
  elif cmd == "debug":
224
240
  cmd_debug(a1)
225
241
  elif cmd == "attach":
@@ -41,6 +41,29 @@ SILICON_INTERFACE_DAEMON_SKIP = os.environ.get("SILICON_INTERFACE_DAEMON_SKIP",
41
41
  }
42
42
 
43
43
 
44
- def python_run_cmd() -> str:
45
- """The interpreter used to RUN a silicon's main.py (not this CLI's venv)."""
44
+ def venv_python(path: str | os.PathLike) -> str | None:
45
+ """The silicon's own .venv interpreter, if one exists."""
46
+ sub = "Scripts/python.exe" if os.name == "nt" else "bin/python"
47
+ cand = Path(path) / ".venv" / sub
48
+ return str(cand) if cand.exists() else None
49
+
50
+
51
+ def base_python_cmd() -> str:
52
+ """The interpreter used to CREATE a silicon's venv (not this CLI's venv)."""
46
53
  return os.environ.get("SILICON_PYTHON") or shutil.which("python3") or shutil.which("python") or "python3"
54
+
55
+
56
+ def python_run_cmd(path: str | os.PathLike | None = None) -> str:
57
+ """The interpreter used to RUN a silicon's code (not this CLI's venv).
58
+
59
+ SILICON_PYTHON always wins; otherwise prefer the silicon's own .venv —
60
+ system interpreters are often externally managed (PEP 668) and never
61
+ received the silicon's dependencies.
62
+ """
63
+ if os.environ.get("SILICON_PYTHON"):
64
+ return os.environ["SILICON_PYTHON"]
65
+ if path:
66
+ venv = venv_python(path)
67
+ if venv:
68
+ return venv
69
+ return shutil.which("python3") or shutil.which("python") or "python3"
@@ -45,7 +45,7 @@ def start(path: str) -> None:
45
45
  return
46
46
  log = open(Path(path) / ".glass_agent.log", "a")
47
47
  proc = subprocess.Popen(
48
- [python_run_cmd(), "-u", "glass_agent.py"], cwd=path,
48
+ [python_run_cmd(path), "-u", "glass_agent.py"], cwd=path,
49
49
  stdout=log, stderr=subprocess.STDOUT, start_new_session=True,
50
50
  )
51
51
  _pid_file(path).write_text(str(proc.pid))
@@ -92,7 +92,7 @@ def watchdog_loop(name: str, path: str, pid_file: str) -> None:
92
92
  log_file = Path(path) / ".silicon.log"
93
93
  main_py = str(Path(path) / "main.py")
94
94
  stop_file = Path(path) / ".silicon.stop"
95
- py = python_run_cmd()
95
+ py = python_run_cmd(path)
96
96
  child: subprocess.Popen | None = None
97
97
 
98
98
  def _terminate(signum=None, frame=None):
@@ -231,7 +231,7 @@ def _multi(target: str, verb: str, fn) -> bool:
231
231
  if not names:
232
232
  ui.error("No matching installations")
233
233
  sys.exit(1)
234
- if target == "all":
234
+ if target in {"all", "*"}:
235
235
  joined = ", ".join(names)
236
236
  if not ui.confirm(f"Are you sure you want to {verb} the following silicons: {joined}?"):
237
237
  return True
@@ -82,26 +82,35 @@ def find(search: str | None = None) -> Install | None:
82
82
 
83
83
 
84
84
  def is_multi_target(s: str) -> bool:
85
- if s == "all":
85
+ if s in {"all", "*"}:
86
86
  return True
87
- parts = s.split(",")
88
- return all(p.strip().isdigit() for p in parts) and bool(parts)
87
+ parts = [p.strip() for p in s.split(",")]
88
+ return len(parts) > 1 and all(bool(p) for p in parts)
89
89
 
90
90
 
91
91
  def resolve_targets(selector: str) -> list[str]:
92
- """'all' | '1,2,4' (1-based indices) list of install names."""
92
+ """'all' | '*' | '1,2,4' | 'api-dev,copywriter' → install names."""
93
93
  rows = installs()
94
- if selector == "all":
94
+ if selector in {"all", "*"}:
95
95
  return [i.name for i in rows]
96
+ by_name = {i.name: i.name for i in rows}
96
97
  out = []
97
- for num in selector.split(","):
98
- num = num.strip()
99
- if not num.isdigit():
98
+ seen = set()
99
+ for part in selector.split(","):
100
+ part = part.strip()
101
+ name = ""
102
+ if part.isdigit():
103
+ idx = int(part) - 1
104
+ for i in rows:
105
+ if i.index == idx:
106
+ name = i.name
107
+ break
108
+ else:
109
+ name = by_name.get(part, "")
110
+ if not name or name in seen:
100
111
  continue
101
- idx = int(num) - 1
102
- for i in rows:
103
- if i.index == idx:
104
- out.append(i.name)
112
+ seen.add(name)
113
+ out.append(name)
105
114
  return out
106
115
 
107
116
 
@@ -17,7 +17,7 @@ import tempfile
17
17
  from pathlib import Path
18
18
 
19
19
  from . import interface_cli, registry, ui
20
- from .config import STEMCELL_GIT_URL, STEMCELL_ZIP_URL, python_run_cmd
20
+ from .config import STEMCELL_GIT_URL, STEMCELL_ZIP_URL, base_python_cmd, python_run_cmd, venv_python
21
21
 
22
22
  SKIP_NAMES = {".git", "__pycache__", ".DS_Store"}
23
23
  PRESERVE_ROOT = {"env.py", "silicon.json", ".glass.json"}
@@ -90,6 +90,58 @@ def _choose_brain_order(primary: str) -> list[str]:
90
90
  return [primary]
91
91
 
92
92
 
93
+ def _ensure_venv(dst: Path) -> str | None:
94
+ """Create <dst>/.venv if missing. Returns its interpreter, or None.
95
+
96
+ Returns None when the venv can't be created — e.g. Debian/Ubuntu without
97
+ python3-venv, where ensurepip is stripped from the system interpreter.
98
+ """
99
+ existing = venv_python(dst)
100
+ if existing:
101
+ return existing
102
+ r = subprocess.run([base_python_cmd(), "-m", "venv", str(dst / ".venv")],
103
+ capture_output=True, text=True)
104
+ if r.returncode != 0:
105
+ shutil.rmtree(dst / ".venv", ignore_errors=True)
106
+ return None
107
+ return venv_python(dst)
108
+
109
+
110
+ def install_requirements(dst: Path, req: Path) -> None:
111
+ """Install the silicon's requirements, preferring an instance-local venv.
112
+
113
+ System interpreters are commonly externally managed (PEP 668), so a plain
114
+ `pip install` is refused. Try, in order: the silicon's own .venv, plain
115
+ pip, `--user`, `--break-system-packages`. A failure here must abort the
116
+ hydration — a silicon without its dependencies starts but never completes
117
+ its Glass handshake.
118
+ """
119
+ ui.info("Installing Python dependencies...")
120
+ vpy = _ensure_venv(dst)
121
+ if vpy:
122
+ r = subprocess.run([vpy, "-m", "pip", "install", "-r", str(req), "--quiet"])
123
+ if r.returncode == 0:
124
+ return
125
+ ui.warn("Install into the silicon's venv failed; falling back to the system interpreter.")
126
+ # A half-provisioned venv must not survive: python_run_cmd() would
127
+ # prefer it over the system interpreter that gets the deps below.
128
+ shutil.rmtree(dst / ".venv", ignore_errors=True)
129
+ py = base_python_cmd()
130
+ last = None
131
+ for extra in ([], ["--user"], ["--break-system-packages"]):
132
+ last = subprocess.run([py, "-m", "pip", "install", "-r", str(req), "--quiet", *extra],
133
+ capture_output=True, text=True)
134
+ if last.returncode == 0:
135
+ return
136
+ if last is not None and last.stderr:
137
+ sys.stderr.write(last.stderr)
138
+ ui.error("Could not install Python dependencies — this silicon would start but never "
139
+ "complete its Glass handshake, so hydration was aborted.")
140
+ ui.info("On Debian/Ubuntu, install venv support and retry: sudo apt install python3-venv")
141
+ ui.info("Or point SILICON_PYTHON at an interpreter that allows pip installs.")
142
+ sys.exit(1)
143
+
144
+
93
145
  def hydrate(target: str, setup_config=None) -> None:
94
146
  abs_target = str(Path(target).resolve())
95
147
  os.makedirs(abs_target, exist_ok=True)
@@ -166,10 +218,7 @@ def hydrate(target: str, setup_config=None) -> None:
166
218
  # Install dependencies
167
219
  req = dst / "requirements.txt"
168
220
  if req.exists():
169
- ui.info("Installing Python dependencies...")
170
- r = subprocess.run([python_run_cmd(), "-m", "pip", "install", "-r", str(req), "--quiet"])
171
- if r.returncode != 0:
172
- subprocess.run([python_run_cmd(), "-m", "pip", "install", "-r", str(req), "--quiet", "--user"])
221
+ install_requirements(dst, req)
173
222
 
174
223
  registry.register(name, abs_target)
175
224
  interface_cli.setup(abs_target)
@@ -13,10 +13,11 @@ import time
13
13
  import urllib.error
14
14
  import urllib.parse
15
15
  import urllib.request
16
+ import webbrowser
16
17
  from datetime import datetime, timedelta, timezone
17
18
  from pathlib import Path
18
19
 
19
- from . import registry, stemcell, ui
20
+ from . import process, registry, stemcell, ui
20
21
  from .config import GLASS_CLI_REPO, GLASS_SERVER_URL
21
22
 
22
23
  MANIFEST_NAME = ".backupsilicon"
@@ -226,6 +227,30 @@ def _post_json_with_team_key(url: str, team_key: str, body=None) -> tuple[int, d
226
227
  return 0, {"detail": str(e)}
227
228
 
228
229
 
230
+ def _post_json(url: str, body=None, *, timeout: int = 60) -> tuple[int, dict]:
231
+ data = json.dumps(body or {}).encode("utf-8")
232
+ req = urllib.request.Request(
233
+ url,
234
+ data=data,
235
+ method="POST",
236
+ headers={
237
+ "Content-Type": "application/json",
238
+ "Accept": "application/json",
239
+ "User-Agent": "silicon-cli",
240
+ },
241
+ )
242
+ try:
243
+ with _urlopen(req, timeout=timeout) as resp:
244
+ return resp.status, json.loads(resp.read().decode() or "{}")
245
+ except urllib.error.HTTPError as e:
246
+ try:
247
+ return e.code, json.loads(e.read().decode() or "{}")
248
+ except Exception:
249
+ return e.code, {}
250
+ except Exception as e:
251
+ return 0, {"detail": str(e)}
252
+
253
+
229
254
  def _team_slug_from_silicon(silicon: dict) -> str:
230
255
  return str(
231
256
  silicon.get("team")
@@ -263,7 +288,12 @@ def _configured_team_key_names(rows: list[dict]) -> list[str]:
263
288
  configured = []
264
289
  for key_name, _label in PROVIDER_API_KEYS:
265
290
  row = by_name.get(key_name, {})
266
- if row.get("configured"):
291
+ if (
292
+ row.get("configured")
293
+ or row.get("team_configured")
294
+ or row.get("center_configured")
295
+ or row.get("server_fallback_configured")
296
+ ):
267
297
  configured.append(key_name)
268
298
  return configured
269
299
 
@@ -273,8 +303,15 @@ def _display_team_api_keys(rows: list[dict]) -> None:
273
303
  ui.info("Provider API token status from Glass (secrets are not returned):")
274
304
  for key_name, label in PROVIDER_API_KEYS:
275
305
  row = by_name.get(key_name, {})
276
- if row.get("configured"):
277
- status = "saved in Glass"
306
+ source = str(row.get("source") or "").strip()
307
+ if row.get("team_configured") or source == "team":
308
+ status = "team override"
309
+ elif row.get("center_configured") or source == "center":
310
+ status = "center managed"
311
+ elif row.get("server_fallback_configured") or source == "server":
312
+ status = "server fallback"
313
+ elif row.get("configured"):
314
+ status = "Glass managed"
278
315
  elif row.get("server_fallback_configured"):
279
316
  status = "server fallback only"
280
317
  else:
@@ -282,7 +319,7 @@ def _display_team_api_keys(rows: list[dict]) -> None:
282
319
  ui.info(f" {label} ({key_name}): {status}")
283
320
 
284
321
 
285
- def _provider_key_env_from_rows(team_slug: str, rows: list[dict], prompt: str) -> dict[str, str]:
322
+ def _provider_key_env_from_rows(team_slug: str, rows: list[dict], prompt: str = "") -> dict[str, str]:
286
323
  if not rows:
287
324
  ui.warn("Glass did not return provider API token metadata.")
288
325
  return {}
@@ -295,10 +332,6 @@ def _provider_key_env_from_rows(team_slug: str, rows: list[dict], prompt: str) -
295
332
  ui.info("Set them in Glass > API keys, then rerun silicon pull if these silicons need them.")
296
333
  return {}
297
334
 
298
- if not ui.confirm(prompt, default_yes=True):
299
- ui.info("Skipping Glass-managed provider API token marker.")
300
- return {}
301
-
302
335
  return {
303
336
  "SILICON_PROVIDER_KEYS_SOURCE": "glass",
304
337
  "SILICON_PROVIDER_KEYS_TEAM": team_slug,
@@ -506,6 +539,89 @@ def _team_pull_url(server: str) -> str:
506
539
  return f"{server}/api/v1/teams/setup-pull"
507
540
 
508
541
 
542
+ def _browser_profile_setup_start_url(server: str) -> str:
543
+ return f"{server}/api/v1/browser-profiles/setup/start"
544
+
545
+
546
+ def _browser_profile_setup_finish_url(server: str) -> str:
547
+ return f"{server}/api/v1/browser-profiles/setup/finish"
548
+
549
+
550
+ def _browser_profile_finish_command(token: str, session_id: str, before_ids: list[str]) -> str:
551
+ before = ",".join(before_ids)
552
+ return f"silicon browser-profile finish '{token}' '{session_id}' '{before}'"
553
+
554
+
555
+ def browser_profile_finish(token: str | None, session_id: str | None, before_ids_csv: str | None = None) -> None:
556
+ token = (token or "").strip()
557
+ session_id = (session_id or "").strip()
558
+ if not token or not session_id:
559
+ ui.error("Usage: silicon browser-profile finish <setup_token> <session_id> [before_profile_ids_csv]")
560
+ sys.exit(1)
561
+ before_ids = [p.strip() for p in (before_ids_csv or "").split(",") if p.strip()]
562
+ server = GLASS_SERVER_URL.rstrip("/")
563
+ ui.info("Finishing browser profile setup with Glass...")
564
+ code, body = _post_json(
565
+ _browser_profile_setup_finish_url(server),
566
+ {"token": token, "session_id": session_id, "before_profile_ids": before_ids},
567
+ timeout=120,
568
+ )
569
+ if not (200 <= code < 300):
570
+ ui.error(body.get("detail") or body.get("error") or f"Glass could not finish the profile setup (HTTP {code}).")
571
+ sys.exit(1)
572
+ profile = body.get("profile") or {}
573
+ name = profile.get("name") or profile.get("id") or "browser profile"
574
+ assigned = body.get("assigned", 0)
575
+ ui.success(f"Saved Steel profile '{name}' and assigned it to {assigned} silicon(s).")
576
+
577
+
578
+ def browser_profile_setup(token: str | None) -> None:
579
+ token = (token or "").strip()
580
+ if not token:
581
+ ui.error("Usage: silicon browser-profile setup <setup_token>")
582
+ sys.exit(1)
583
+ server = GLASS_SERVER_URL.rstrip("/")
584
+ ui.info("Starting browser profile setup with Glass...")
585
+ code, body = _post_json(_browser_profile_setup_start_url(server), {"token": token}, timeout=120)
586
+ if not (200 <= code < 300):
587
+ ui.error(body.get("detail") or body.get("error") or f"Glass could not start the profile setup (HTTP {code}).")
588
+ sys.exit(1)
589
+
590
+ session_id = str(body.get("session_id") or "").strip()
591
+ viewer_url = str(body.get("viewer_url") or body.get("debug_url") or "").strip()
592
+ before_ids = [str(p) for p in (body.get("before_profile_ids") or []) if p]
593
+ if not session_id or not viewer_url:
594
+ ui.error("Glass returned an incomplete browser setup session.")
595
+ sys.exit(1)
596
+
597
+ ui.success("Steel setup session started.")
598
+ ui.info(f"Viewer URL: {viewer_url}")
599
+ try:
600
+ webbrowser.open(viewer_url)
601
+ except Exception:
602
+ pass
603
+
604
+ finish_cmd = _browser_profile_finish_command(token, session_id, before_ids)
605
+ if not ui.interactive():
606
+ ui.info("When the browser is configured, finish with:")
607
+ print(f" {finish_cmd}")
608
+ return
609
+
610
+ print()
611
+ ui.info("Use the browser window to log in or configure the profile.")
612
+ ui.info("Press Enter here when you're done. Ctrl+C leaves the session open; finish later with:")
613
+ print(f" {finish_cmd}")
614
+ try:
615
+ input()
616
+ except KeyboardInterrupt:
617
+ print()
618
+ ui.warn("Setup session left open.")
619
+ ui.info(f"Finish later with: {finish_cmd}")
620
+ return
621
+
622
+ browser_profile_finish(token, session_id, ",".join(before_ids))
623
+
624
+
509
625
  def _silicon_display(silicon: dict) -> str:
510
626
  name = str(silicon.get("name") or "").strip()
511
627
  sid = str(silicon.get("silicon_id") or "").strip()
@@ -622,6 +738,10 @@ def _pull_team(api_key: str, server: str) -> None:
622
738
  for instance_name, target in pulled:
623
739
  ui.info(f" {instance_name}: {target}")
624
740
 
741
+ ui.info("Starting pulled silicons...")
742
+ for instance_name, _target in pulled:
743
+ process.start_one(instance_name)
744
+
625
745
  if ui.interactive() and ui.confirm("Enable daily 23:59 UTC backups for all pulled silicons?"):
626
746
  for instance_name, target in pulled:
627
747
  ui.info(f"Running initial backup for '{instance_name}'...")
@@ -679,6 +799,7 @@ def pull(api_token: str | None) -> None:
679
799
 
680
800
  ui.success(f"Pulled Glass silicon '{silicon_name or silicon_id}' into {target}")
681
801
  ui.info(f"Registered as '{instance_name}'.")
802
+ process.start_one(instance_name)
682
803
 
683
804
  if ui.interactive() and ui.confirm("Enable daily 23:59 UTC backups for this silicon?"):
684
805
  ui.info("Running initial backup...")
@@ -50,8 +50,11 @@ def _update_one(target: str | None, tmp_src: str, updater: str, multi: bool) ->
50
50
  sys.exit(1)
51
51
 
52
52
  ui.info(f"Updating '{inst.name}' safely...")
53
- r = subprocess.run([python_run_cmd(), updater, "update", "--source", tmp_src, "--target", inst.path])
53
+ r = subprocess.run([python_run_cmd(inst.path), updater, "update", "--source", tmp_src, "--target", inst.path])
54
54
  if r.returncode == 0:
55
+ req = Path(inst.path) / "requirements.txt"
56
+ if req.exists():
57
+ stemcell.install_requirements(Path(inst.path), req)
55
58
  ui.success(f"'{inst.name}' updated successfully")
56
59
  elif r.returncode == 2:
57
60
  ui.error(f"Update aborted — merge conflicts detected in '{inst.name}'. No files overwritten.")
@@ -87,9 +90,9 @@ def trigger_update_check(target: str | None) -> None:
87
90
  main_py = root / "main.py"
88
91
 
89
92
  if updater.exists():
90
- cmd = [python_run_cmd(), str(updater)]
93
+ cmd = [python_run_cmd(inst.path), str(updater)]
91
94
  elif main_py.exists():
92
- cmd = [python_run_cmd(), "main.py", "update-check"]
95
+ cmd = [python_run_cmd(inst.path), "main.py", "update-check"]
93
96
  else:
94
97
  ui.error(f"'{inst.name}' does not look like a stemcell with update.py or main.py.")
95
98
  sys.exit(1)
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: silicon-cli
3
- Version: 1.0.8
3
+ Version: 1.0.11
4
4
  Summary: Silicon CLI — create, run, and manage your silicon instances.
5
5
  Author: Saket
6
6
  Project-URL: Homepage, https://github.com/saket1225/silicon-cli
@@ -13,6 +13,7 @@ Requires-Python: >=3.9
13
13
  Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
15
  Requires-Dist: certifi>=2024.2.2
16
+ Dynamic: license-file
16
17
 
17
18
  # silicon-cli
18
19
 
@@ -40,12 +41,14 @@ pip install silicon-cli
40
41
  silicon Show status or list instances
41
42
  silicon new [dir] Create a new Silicon (hydrate from stemcell)
42
43
  silicon new . Hydrate the current folder into a runnable silicon
43
- silicon start <target> Start silicon(s). target = name, index, 1,2,4, or all
44
- silicon stop [--full] <target> Stop silicon(s) (--full also stops the glass agent)
45
- silicon restart <target> Restart silicon(s)
44
+ silicon start <target> Start silicon(s). target = name, *, all, 1,2,4, or name,name
45
+ silicon stop [--full] <target> Stop silicon(s). target = name, *, all, 1,2,4, or name,name
46
+ silicon restart <target> Restart silicon(s). target = name, *, all, 1,2,4, or name,name
46
47
  silicon agent <start|stop|status> [name] Manage the per-silicon glass agent
47
48
  silicon status [name] Show instance status
48
49
  silicon browser [name] Open a headed browser for login
50
+ silicon browser-profile setup <token> Create a Steel browser profile through Glass
51
+ silicon browser-profile finish <token> <session_id> [before_ids_csv] Finish profile setup
49
52
  silicon debug [name] Tail a running instance's logs
50
53
  silicon attach [path] Register an existing silicon directory
51
54
  silicon pull [api_token] Pull a Glass team or silicon into local folders
@@ -92,8 +95,8 @@ silicon pull sct_live_...
92
95
 
93
96
  The command validates the token with Glass, mints one local silicon API key per
94
97
  team silicon, creates one folder per silicon, hydrates the stemcell, writes
95
- `.glass.json`, `.env`, and `env.py`, registers each instance, and starts each
96
- Silicon Interface daemon.
98
+ `.glass.json`, `.env`, and `env.py`, registers each instance, starts each
99
+ Silicon Interface daemon, and starts each silicon process.
97
100
 
98
101
  During team pull, setup asks for the default brain/fallback settings once. You
99
102
  can apply those settings to every silicon, or select specific silicons that
@@ -102,9 +105,9 @@ Older per-silicon `scs_live_...` tokens still pull just that one silicon.
102
105
 
103
106
  Provider API keys for voice, browser profiles, billing, and architecture
104
107
  generation are configured on the Glass backend. After the silicon API token is
105
- validated, `silicon pull` reads the team's provider-key metadata from Glass,
106
- shows each provider as saved/missing without returning plaintext secrets, and
107
- asks whether the pulled silicon(s) should use the Glass-managed keys. A
108
+ validated, `silicon pull` reads the team's provider-key metadata from Glass and
109
+ automatically marks the pulled silicon(s) to use Glass-managed keys when every
110
+ provider is available. Plaintext secrets are never returned to the CLI. A
108
111
  Glass-pulled silicon only stores its Glass API token plus a local marker that
109
112
  provider keys come from Glass.
110
113
 
File without changes
File without changes