silicon-cli 1.0.0__tar.gz → 1.0.4__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 (26) hide show
  1. silicon_cli-1.0.4/PKG-INFO +139 -0
  2. silicon_cli-1.0.4/README.md +122 -0
  3. {silicon_cli-1.0.0 → silicon_cli-1.0.4}/pyproject.toml +2 -2
  4. {silicon_cli-1.0.0 → silicon_cli-1.0.4}/silicon_cli/__init__.py +1 -1
  5. {silicon_cli-1.0.0 → silicon_cli-1.0.4}/silicon_cli/cli.py +10 -5
  6. {silicon_cli-1.0.0 → silicon_cli-1.0.4}/silicon_cli/config.py +20 -3
  7. silicon_cli-1.0.4/silicon_cli/interface_cli.py +182 -0
  8. {silicon_cli-1.0.0 → silicon_cli-1.0.4}/silicon_cli/stemcell.py +8 -30
  9. silicon_cli-1.0.4/silicon_cli/sync.py +466 -0
  10. {silicon_cli-1.0.0 → silicon_cli-1.0.4}/silicon_cli/update.py +1 -1
  11. silicon_cli-1.0.4/silicon_cli.egg-info/PKG-INFO +139 -0
  12. {silicon_cli-1.0.0 → silicon_cli-1.0.4}/silicon_cli.egg-info/SOURCES.txt +2 -0
  13. silicon_cli-1.0.4/silicon_cli.egg-info/requires.txt +1 -0
  14. silicon_cli-1.0.0/PKG-INFO +0 -74
  15. silicon_cli-1.0.0/README.md +0 -58
  16. silicon_cli-1.0.0/silicon_cli/sync.py +0 -216
  17. silicon_cli-1.0.0/silicon_cli.egg-info/PKG-INFO +0 -74
  18. {silicon_cli-1.0.0 → silicon_cli-1.0.4}/LICENSE +0 -0
  19. {silicon_cli-1.0.0 → silicon_cli-1.0.4}/setup.cfg +0 -0
  20. {silicon_cli-1.0.0 → silicon_cli-1.0.4}/silicon_cli/glassagent.py +0 -0
  21. {silicon_cli-1.0.0 → silicon_cli-1.0.4}/silicon_cli/process.py +0 -0
  22. {silicon_cli-1.0.0 → silicon_cli-1.0.4}/silicon_cli/registry.py +0 -0
  23. {silicon_cli-1.0.0 → silicon_cli-1.0.4}/silicon_cli/ui.py +0 -0
  24. {silicon_cli-1.0.0 → silicon_cli-1.0.4}/silicon_cli.egg-info/dependency_links.txt +0 -0
  25. {silicon_cli-1.0.0 → silicon_cli-1.0.4}/silicon_cli.egg-info/entry_points.txt +0 -0
  26. {silicon_cli-1.0.0 → silicon_cli-1.0.4}/silicon_cli.egg-info/top_level.txt +0 -0
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.4
2
+ Name: silicon-cli
3
+ Version: 1.0.4
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
+ Requires-Dist: certifi>=2024.2.2
16
+ Dynamic: license-file
17
+
18
+ # silicon-cli
19
+
20
+ This is the single source for the installable **`silicon`** command. The PyPI
21
+ package is named `silicon-cli`, and the code lives here in this `silicon-cli`
22
+ repo.
23
+
24
+ The command manages silicon instances on a machine: create them from the
25
+ [silicon-stemcell](https://github.com/unlikefraction/silicon-stemcell) base,
26
+ start/stop them under an auto-restart watchdog, stream logs, and back them up to
27
+ Glass. It reads the same `~/.silicon/registry.json`, so existing installs carry
28
+ over unchanged.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ pip install silicon-cli
34
+ ```
35
+
36
+ (Zero runtime dependencies — stdlib only.)
37
+
38
+ ## Commands
39
+
40
+ ```
41
+ silicon Show status or list instances
42
+ silicon new [dir] Create a new Silicon (hydrate from stemcell)
43
+ silicon new . Hydrate the current folder into a runnable silicon
44
+ silicon start <target> Start silicon(s). target = name, index, 1,2,4, or all
45
+ silicon stop [--full] <target> Stop silicon(s) (--full also stops the glass agent)
46
+ silicon restart <target> Restart silicon(s)
47
+ silicon agent <start|stop|status> [name] Manage the per-silicon glass agent
48
+ silicon status [name] Show instance status
49
+ silicon browser [name] Open a headed browser for login
50
+ silicon debug [name] Tail a running instance's logs
51
+ silicon attach [path] Register an existing silicon directory
52
+ silicon pull [api_token] Pull a Glass silicon into a new local folder
53
+ silicon push [name] [now|stop] Daily 23:59 GMT backups to Glass (now = one-shot, stop = end loop)
54
+ silicon backup [name] [now|stop] Alias for silicon push
55
+ silicon update <target> Update silicon(s) from the latest stemcell
56
+ silicon list List all instances
57
+ silicon script update Update this CLI itself
58
+ silicon help Show help
59
+ ```
60
+
61
+ ## Configuration (env vars)
62
+
63
+ | Var | Default | Purpose |
64
+ | --- | --- | --- |
65
+ | `SILICON_HOME` | `~/.silicon` | registry + CLI state |
66
+ | `GLASS_SERVER_URL` | `https://glass.teamofsilicons.com` | Glass sync server (pull/push) |
67
+ | `SILICON_STEMCELL_REPO` | `unlikefraction/silicon-stemcell` | base for `new` |
68
+ | `SILICON_GLASS_CLI_REPO` | `unlikefraction/glass` | glass backup CLI |
69
+ | `SILICON_PYTHON` | `python3` | interpreter used to run a silicon's `main.py` |
70
+ | `SILICON_INTERFACE_CLI_PACKAGE` | `@teamofsilicons/silicon-interface-cli` | npm package used to install the Silicon Interface CLI |
71
+ | `SILICON_INTERFACE_CLI_TARBALL` | versioned npm tarball | fallback package URL if registry metadata is briefly unavailable |
72
+ | `SILICON_INTERFACE_CLI_SOURCE` | *(empty)* | local package dir or `silicon-interface.mjs` path for dev installs |
73
+ | `SILICON_INTERFACE_CLI_SKIP` | *(empty)* | set to `1` to skip interface CLI setup |
74
+ | `SILICON_INTERFACE_DAEMON_SKIP` | *(empty)* | set to `1` to install the CLI without starting its listener daemon |
75
+
76
+ ## Silicon Interface CLI
77
+
78
+ `silicon new`, `silicon install`, and `silicon pull` also set up the
79
+ Silicon Interface CLI in the silicon folder when Node 22+ is available.
80
+ When a Glass `.glass.json` is present, setup also starts the background listener
81
+ daemon so the silicon receives live conversation frames without polling.
82
+
83
+ `silicon pull` is token-native. Generate an API token from the silicon detail
84
+ page in Glass, then run:
85
+
86
+ ```bash
87
+ silicon pull
88
+ # paste the token when prompted
89
+
90
+ # or, less private because it lands in shell history:
91
+ silicon pull scs_live_...
92
+ ```
93
+
94
+ The command validates the token with Glass, creates a folder named after the
95
+ silicon, hydrates the stemcell, writes `.glass.json`, `.env`, and `env.py`,
96
+ registers the instance, and starts the Silicon Interface daemon.
97
+
98
+ Provider API keys for voice, browser profiles, billing, and architecture
99
+ generation are configured on the Glass backend. A Glass-pulled silicon only
100
+ stores its Glass API token locally.
101
+
102
+ The local wrappers are written to:
103
+
104
+ ```bash
105
+ <silicon>/.silicon-interface/bin/si
106
+ <silicon>/.silicon-interface/bin/silicon-interface
107
+ <silicon>/.silicon-interface/inbox.jsonl
108
+ <silicon>/.silicon-interface/state.json
109
+ ```
110
+
111
+ For Glass-pulled silicons, those wrappers automatically use the folder's
112
+ `.glass.json` (`server_url` + `api_key`) for conversation API auth.
113
+
114
+ During local development, point `SILICON_INTERFACE_CLI_SOURCE` at the package:
115
+
116
+ ```bash
117
+ SILICON_INTERFACE_CLI_SOURCE=../silicon-interface/packages/silicon-interface-cli silicon new ./ada
118
+ ```
119
+
120
+ ## Backups
121
+
122
+ `silicon push <name> now` and `silicon backup <name> now` use the
123
+ `.backupsilicon` manifest when the instance has one. The CLI archives those
124
+ paths and uploads them to Glass via `/api/v1/silicon-backups/` with the
125
+ instance's `.glass.json` API key. If no manifest exists, the command falls back
126
+ to the legacy `glass push` snapshot flow.
127
+
128
+ `silicon push <name>` starts the background loop. Scheduled manifest backups run
129
+ daily at 23:59 GMT; `now` remains available for one-shot manual backups.
130
+
131
+ ## How it differs from the bash version
132
+
133
+ - Pure Python package with a `silicon` console entry point (installed in an
134
+ isolated venv), instead of a single bash script.
135
+ - The auto-restart watchdog runs as `silicon _watchdog` (a detached supervisor
136
+ process) rather than a backgrounded bash function — same crash-loop detection,
137
+ `.silicon.stop` sentinel, and `.silicon.pid`/`.silicon.log` behavior.
138
+ - `silicon script update` reinstalls the package via pip from its recorded source.
139
+ - Everything (server, stemcell repo) is env-overridable.
@@ -0,0 +1,122 @@
1
+ # silicon-cli
2
+
3
+ This is the single source for the installable **`silicon`** command. The PyPI
4
+ package is named `silicon-cli`, and the code lives here in this `silicon-cli`
5
+ repo.
6
+
7
+ The command manages silicon instances on a machine: create them from the
8
+ [silicon-stemcell](https://github.com/unlikefraction/silicon-stemcell) base,
9
+ start/stop them under an auto-restart watchdog, stream logs, and back them up to
10
+ Glass. It reads the same `~/.silicon/registry.json`, so existing installs carry
11
+ over unchanged.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ pip install silicon-cli
17
+ ```
18
+
19
+ (Zero runtime dependencies — stdlib only.)
20
+
21
+ ## Commands
22
+
23
+ ```
24
+ silicon Show status or list instances
25
+ silicon new [dir] Create a new Silicon (hydrate from stemcell)
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)
30
+ silicon agent <start|stop|status> [name] Manage the per-silicon glass agent
31
+ silicon status [name] Show instance status
32
+ silicon browser [name] Open a headed browser for login
33
+ silicon debug [name] Tail a running instance's logs
34
+ silicon attach [path] Register an existing silicon directory
35
+ silicon pull [api_token] Pull a Glass silicon into a new local folder
36
+ silicon push [name] [now|stop] Daily 23:59 GMT backups to Glass (now = one-shot, stop = end loop)
37
+ silicon backup [name] [now|stop] Alias for silicon push
38
+ silicon update <target> Update silicon(s) from the latest stemcell
39
+ silicon list List all instances
40
+ silicon script update Update this CLI itself
41
+ silicon help Show help
42
+ ```
43
+
44
+ ## Configuration (env vars)
45
+
46
+ | Var | Default | Purpose |
47
+ | --- | --- | --- |
48
+ | `SILICON_HOME` | `~/.silicon` | registry + CLI state |
49
+ | `GLASS_SERVER_URL` | `https://glass.teamofsilicons.com` | Glass sync server (pull/push) |
50
+ | `SILICON_STEMCELL_REPO` | `unlikefraction/silicon-stemcell` | base for `new` |
51
+ | `SILICON_GLASS_CLI_REPO` | `unlikefraction/glass` | glass backup CLI |
52
+ | `SILICON_PYTHON` | `python3` | interpreter used to run a silicon's `main.py` |
53
+ | `SILICON_INTERFACE_CLI_PACKAGE` | `@teamofsilicons/silicon-interface-cli` | npm package used to install the Silicon Interface CLI |
54
+ | `SILICON_INTERFACE_CLI_TARBALL` | versioned npm tarball | fallback package URL if registry metadata is briefly unavailable |
55
+ | `SILICON_INTERFACE_CLI_SOURCE` | *(empty)* | local package dir or `silicon-interface.mjs` path for dev installs |
56
+ | `SILICON_INTERFACE_CLI_SKIP` | *(empty)* | set to `1` to skip interface CLI setup |
57
+ | `SILICON_INTERFACE_DAEMON_SKIP` | *(empty)* | set to `1` to install the CLI without starting its listener daemon |
58
+
59
+ ## Silicon Interface CLI
60
+
61
+ `silicon new`, `silicon install`, and `silicon pull` also set up the
62
+ Silicon Interface CLI in the silicon folder when Node 22+ is available.
63
+ When a Glass `.glass.json` is present, setup also starts the background listener
64
+ daemon so the silicon receives live conversation frames without polling.
65
+
66
+ `silicon pull` is token-native. Generate an API token from the silicon detail
67
+ page in Glass, then run:
68
+
69
+ ```bash
70
+ silicon pull
71
+ # paste the token when prompted
72
+
73
+ # or, less private because it lands in shell history:
74
+ silicon pull scs_live_...
75
+ ```
76
+
77
+ The command validates the token with Glass, creates a folder named after the
78
+ silicon, hydrates the stemcell, writes `.glass.json`, `.env`, and `env.py`,
79
+ registers the instance, and starts the Silicon Interface daemon.
80
+
81
+ Provider API keys for voice, browser profiles, billing, and architecture
82
+ generation are configured on the Glass backend. A Glass-pulled silicon only
83
+ stores its Glass API token locally.
84
+
85
+ The local wrappers are written to:
86
+
87
+ ```bash
88
+ <silicon>/.silicon-interface/bin/si
89
+ <silicon>/.silicon-interface/bin/silicon-interface
90
+ <silicon>/.silicon-interface/inbox.jsonl
91
+ <silicon>/.silicon-interface/state.json
92
+ ```
93
+
94
+ For Glass-pulled silicons, those wrappers automatically use the folder's
95
+ `.glass.json` (`server_url` + `api_key`) for conversation API auth.
96
+
97
+ During local development, point `SILICON_INTERFACE_CLI_SOURCE` at the package:
98
+
99
+ ```bash
100
+ SILICON_INTERFACE_CLI_SOURCE=../silicon-interface/packages/silicon-interface-cli silicon new ./ada
101
+ ```
102
+
103
+ ## Backups
104
+
105
+ `silicon push <name> now` and `silicon backup <name> now` use the
106
+ `.backupsilicon` manifest when the instance has one. The CLI archives those
107
+ paths and uploads them to Glass via `/api/v1/silicon-backups/` with the
108
+ instance's `.glass.json` API key. If no manifest exists, the command falls back
109
+ to the legacy `glass push` snapshot flow.
110
+
111
+ `silicon push <name>` starts the background loop. Scheduled manifest backups run
112
+ daily at 23:59 GMT; `now` remains available for one-shot manual backups.
113
+
114
+ ## How it differs from the bash version
115
+
116
+ - Pure Python package with a `silicon` console entry point (installed in an
117
+ isolated venv), instead of a single bash script.
118
+ - The auto-restart watchdog runs as `silicon _watchdog` (a detached supervisor
119
+ process) rather than a backgrounded bash function — same crash-loop detection,
120
+ `.silicon.stop` sentinel, and `.silicon.pid`/`.silicon.log` behavior.
121
+ - `silicon script update` reinstalls the package via pip from its recorded source.
122
+ - Everything (server, stemcell repo) is env-overridable.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "silicon-cli"
7
- version = "1.0.0"
7
+ version = "1.0.4"
8
8
  description = "Silicon CLI — create, run, and manage your silicon instances."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -15,7 +15,7 @@ classifiers = [
15
15
  "License :: OSI Approved :: MIT License",
16
16
  "Operating System :: OS Independent",
17
17
  ]
18
- dependencies = [] # stdlib only — keeps the install bulletproof
18
+ dependencies = ["certifi>=2024.2.2"]
19
19
 
20
20
  [project.urls]
21
21
  Homepage = "https://github.com/saket1225/silicon-cli"
@@ -1,2 +1,2 @@
1
1
  """Silicon CLI — manage silicon instances. Python port of the original bash manager."""
2
- __version__ = "1.0.0"
2
+ __version__ = "1.0.1"
@@ -9,7 +9,7 @@ from . import glassagent, process, registry, stemcell, sync, ui, update
9
9
  from .config import python_run_cmd
10
10
 
11
11
  COMMANDS = ["start", "stop", "restart", "status", "browser", "debug", "attach",
12
- "pull", "push", "update", "list", "install", "new", "help", "script", "agent"]
12
+ "pull", "push", "backup", "update", "list", "install", "new", "help", "script", "agent"]
13
13
 
14
14
 
15
15
  # ----------------------------------------------------------------- commands
@@ -164,10 +164,11 @@ def cmd_help() -> None:
164
164
  silicon browser [name] Open headed browser for login
165
165
  silicon debug [name] Attach to running instance (live logs)
166
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
167
+ silicon pull [api_token] Pull a Glass silicon into a new local folder
168
+ silicon push [name] Start daily 23:59 GMT backup loop to Glass
169
169
  silicon push [name] now Push a one-time backup to Glass
170
- silicon push [name] stop Stop the hourly backup loop
170
+ silicon push [name] stop Stop the daily backup loop
171
+ silicon backup [name] [now|stop] Alias for silicon push
171
172
  silicon update <target> Update silicon(s) to latest. target = name, index, 1,2,4, or all
172
173
  silicon list List all instances
173
174
  silicon script update Update the silicon CLI itself
@@ -199,6 +200,10 @@ def main(argv: list[str] | None = None) -> None:
199
200
  process.watchdog_loop(name=a2 or "silicon", path=a1, pid_file=argv[3] if len(argv) > 3 else "")
200
201
  return
201
202
 
203
+ if cmd == "_backup_loop": # internal: the scheduled manifest backup loop
204
+ sync.backup_loop(a1 or ".", a2)
205
+ return
206
+
202
207
  if cmd == "start":
203
208
  process.start(a1)
204
209
  elif cmd == "stop":
@@ -218,7 +223,7 @@ def main(argv: list[str] | None = None) -> None:
218
223
  cmd_attach(a1)
219
224
  elif cmd == "pull":
220
225
  sync.pull(a1)
221
- elif cmd == "push":
226
+ elif cmd in ("push", "backup"):
222
227
  sync.push(a1, a2)
223
228
  elif cmd == "update":
224
229
  update.update_instance(a1)
@@ -11,9 +11,8 @@ REGISTRY_DIR = Path(os.environ.get("SILICON_HOME", HOME / ".silicon"))
11
11
  REGISTRY_FILE = REGISTRY_DIR / "registry.json"
12
12
  CLI_SOURCE_FILE = REGISTRY_DIR / "cli-source" # where `silicon script update` reinstalls from
13
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("/")
14
+ # Glass sync server (pull/push). Override with GLASS_SERVER_URL to point elsewhere.
15
+ GLASS_SERVER_URL = os.environ.get("GLASS_SERVER_URL", "https://glass.teamofsilicons.com").rstrip("/")
17
16
 
18
17
  # Stemcell — the base every new silicon is hydrated from.
19
18
  STEMCELL_REPO = os.environ.get("SILICON_STEMCELL_REPO", "unlikefraction/silicon-stemcell")
@@ -23,6 +22,24 @@ STEMCELL_ZIP_URL = f"https://github.com/{STEMCELL_REPO}/archive/refs/heads/main.
23
22
  # Glass CLI (used by pull/push for backups).
24
23
  GLASS_CLI_REPO = os.environ.get("SILICON_GLASS_CLI_REPO", "unlikefraction/glass")
25
24
 
25
+ # Silicon Interface CLI. During local development, silicon-cli will auto-detect
26
+ # a sibling silicon-interface checkout; in production this package spec is used.
27
+ SILICON_INTERFACE_CLI_PACKAGE = os.environ.get(
28
+ "SILICON_INTERFACE_CLI_PACKAGE",
29
+ "@teamofsilicons/silicon-interface-cli",
30
+ )
31
+ SILICON_INTERFACE_CLI_TARBALL = os.environ.get(
32
+ "SILICON_INTERFACE_CLI_TARBALL",
33
+ "https://registry.npmjs.org/@teamofsilicons/silicon-interface-cli/-/silicon-interface-cli-0.1.2.tgz",
34
+ )
35
+ SILICON_INTERFACE_CLI_SOURCE = os.environ.get("SILICON_INTERFACE_CLI_SOURCE", "")
36
+ SILICON_INTERFACE_CLI_SKIP = os.environ.get("SILICON_INTERFACE_CLI_SKIP", "").lower() in {
37
+ "1", "true", "yes", "on",
38
+ }
39
+ SILICON_INTERFACE_DAEMON_SKIP = os.environ.get("SILICON_INTERFACE_DAEMON_SKIP", "").lower() in {
40
+ "1", "true", "yes", "on",
41
+ }
42
+
26
43
 
27
44
  def python_run_cmd() -> str:
28
45
  """The interpreter used to RUN a silicon's main.py (not this CLI's venv)."""
@@ -0,0 +1,182 @@
1
+ """Install the Silicon Interface CLI into a silicon folder."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ import re
6
+ import shutil
7
+ import subprocess
8
+ from pathlib import Path
9
+
10
+ from . import ui
11
+ from .config import (
12
+ SILICON_INTERFACE_DAEMON_SKIP,
13
+ SILICON_INTERFACE_CLI_PACKAGE,
14
+ SILICON_INTERFACE_CLI_SKIP,
15
+ SILICON_INTERFACE_CLI_SOURCE,
16
+ SILICON_INTERFACE_CLI_TARBALL,
17
+ )
18
+
19
+
20
+ def _node_major() -> int | None:
21
+ node = shutil.which("node")
22
+ if not node:
23
+ return None
24
+ try:
25
+ out = subprocess.run(
26
+ [node, "--version"],
27
+ capture_output=True,
28
+ text=True,
29
+ check=False,
30
+ ).stdout.strip()
31
+ except Exception:
32
+ return None
33
+ m = re.match(r"^v?(\d+)", out)
34
+ return int(m.group(1)) if m else None
35
+
36
+
37
+ def _source_script() -> Path | None:
38
+ if SILICON_INTERFACE_CLI_SOURCE:
39
+ source = Path(SILICON_INTERFACE_CLI_SOURCE).expanduser().resolve()
40
+ if source.is_file():
41
+ return source
42
+ candidate = source / "bin" / "silicon-interface.mjs"
43
+ if candidate.exists():
44
+ return candidate
45
+ return None
46
+
47
+ # Local dev layout: ../silicon-interface/packages/silicon-interface-cli
48
+ repo_root = Path(__file__).resolve().parents[2]
49
+ candidate = (
50
+ repo_root
51
+ / "silicon-interface"
52
+ / "packages"
53
+ / "silicon-interface-cli"
54
+ / "bin"
55
+ / "silicon-interface.mjs"
56
+ )
57
+ return candidate if candidate.exists() else None
58
+
59
+
60
+ def _run(cmd: list[str], target: Path, *, warn: bool = True) -> bool:
61
+ try:
62
+ proc = subprocess.run(
63
+ cmd,
64
+ cwd=str(target),
65
+ stdout=subprocess.PIPE,
66
+ stderr=subprocess.PIPE,
67
+ text=True,
68
+ check=False,
69
+ )
70
+ except Exception as exc:
71
+ if warn:
72
+ ui.warn(f"Silicon Interface CLI setup skipped: {exc}")
73
+ return False
74
+ if proc.returncode == 0:
75
+ return True
76
+ if not warn:
77
+ return False
78
+ detail = (proc.stderr or proc.stdout or "").strip().splitlines()
79
+ suffix = f": {detail[-1]}" if detail else ""
80
+ ui.warn(f"Silicon Interface CLI setup skipped{suffix}")
81
+ return False
82
+
83
+
84
+ def _npm_install_command(target: Path, package_spec: str) -> list[str] | None:
85
+ npm = shutil.which("npm")
86
+ if not npm:
87
+ return None
88
+ return [
89
+ npm,
90
+ "exec",
91
+ "--yes",
92
+ "--package",
93
+ package_spec,
94
+ "--",
95
+ "silicon-interface",
96
+ "install",
97
+ str(target),
98
+ ]
99
+
100
+
101
+ def _npm_install_commands(target: Path) -> list[list[str]]:
102
+ package_specs = [SILICON_INTERFACE_CLI_PACKAGE]
103
+ if (
104
+ SILICON_INTERFACE_CLI_TARBALL
105
+ and SILICON_INTERFACE_CLI_TARBALL not in package_specs
106
+ ):
107
+ package_specs.append(SILICON_INTERFACE_CLI_TARBALL)
108
+
109
+ commands: list[list[str]] = []
110
+ for package_spec in package_specs:
111
+ cmd = _npm_install_command(target, package_spec)
112
+ if cmd:
113
+ commands.append(cmd)
114
+ return commands
115
+
116
+
117
+ def _start_daemon(target: Path) -> bool:
118
+ if SILICON_INTERFACE_DAEMON_SKIP:
119
+ return False
120
+ if not (target / ".glass.json").exists():
121
+ return False
122
+ si = target / ".silicon-interface" / "bin" / "si"
123
+ if not si.exists():
124
+ return False
125
+ ok = _run([str(si), "daemon", "start"], target, warn=False)
126
+ if ok:
127
+ ui.success("Silicon Interface daemon running.")
128
+ else:
129
+ ui.warn("Silicon Interface daemon was not started; run '.silicon-interface/bin/si daemon start'.")
130
+ return ok
131
+
132
+
133
+ def setup(target: str | Path) -> bool:
134
+ """Install local si/silicon-interface wrappers into ``target``.
135
+
136
+ This is intentionally best-effort. A missing Node runtime or unpublished npm
137
+ package should not break `silicon new` or `silicon pull`; the user can rerun
138
+ the setup later after installing Node or setting SILICON_INTERFACE_CLI_SOURCE.
139
+ """
140
+ if SILICON_INTERFACE_CLI_SKIP:
141
+ return False
142
+
143
+ target_path = Path(target).resolve()
144
+ major = _node_major()
145
+ if major is None:
146
+ ui.warn("Silicon Interface CLI setup skipped: node not found.")
147
+ return False
148
+ if major < 22:
149
+ ui.warn(
150
+ "Silicon Interface CLI setup skipped: Node 22+ is required "
151
+ f"(found Node {major})."
152
+ )
153
+ return False
154
+
155
+ script = _source_script()
156
+ ui.info("Setting up Silicon Interface CLI...")
157
+ if script:
158
+ ok = _run([shutil.which("node") or "node", str(script), "install", str(target_path)], target_path)
159
+ else:
160
+ commands = _npm_install_commands(target_path)
161
+ if not commands:
162
+ ui.warn("Silicon Interface CLI setup skipped: npm not found.")
163
+ return False
164
+ ok = False
165
+ for index, cmd in enumerate(commands):
166
+ final_attempt = index == len(commands) - 1
167
+ ok = _run(cmd, target_path, warn=final_attempt)
168
+ if ok:
169
+ break
170
+ if not final_attempt:
171
+ ui.warn(
172
+ "Silicon Interface CLI package lookup failed; "
173
+ "retrying with published tarball."
174
+ )
175
+
176
+ if ok:
177
+ ui.success(
178
+ "Silicon Interface CLI ready: "
179
+ f"{target_path}/.silicon-interface/bin/si"
180
+ )
181
+ _start_daemon(target_path)
182
+ return ok
@@ -2,8 +2,8 @@
2
2
 
3
3
  `silicon new <dir>` downloads the stemcell, copies in any files the target is
4
4
  missing (never clobbering env.py / silicon.json / .glass.json), seeds config +
5
- env keys, prompts for tokens + brain/worker providers, installs requirements,
6
- and registers the instance — same flow as the original bash CLI.
5
+ env keys, prompts for brain/worker providers, installs requirements, and
6
+ registers the instance.
7
7
  """
8
8
  from __future__ import annotations
9
9
 
@@ -16,7 +16,7 @@ import sys
16
16
  import tempfile
17
17
  from pathlib import Path
18
18
 
19
- from . import registry, ui
19
+ from . import interface_cli, registry, ui
20
20
  from .config import STEMCELL_GIT_URL, STEMCELL_ZIP_URL, python_run_cmd
21
21
 
22
22
  SKIP_NAMES = {".git", "__pycache__", ".DS_Store"}
@@ -144,9 +144,9 @@ def hydrate(target: str) -> None:
144
144
  silicon.pop("version", None)
145
145
  sj.write_text(json.dumps(silicon, indent=4) + "\n")
146
146
 
147
- # Seed env.py required keys
147
+ # Seed env.py required keys used by the current stemcell.
148
148
  env_path = dst / "env.py"
149
- for key, default in {"TELEGRAM_BOT_TOKEN": "", "OPENAI_API_KEY": "", "GEMINI_API_KEY": "", "BROWSER_PROFILE": name}.items():
149
+ for key, default in {"GLASS_API_KEY": "", "BROWSER_PROFILE": name}.items():
150
150
  if not _env_value(env_path, key):
151
151
  _env_upsert(env_path, key, default)
152
152
 
@@ -158,7 +158,7 @@ def hydrate(target: str) -> None:
158
158
 
159
159
  # Interactive setup
160
160
  if ui.interactive():
161
- _interactive_setup(dst, env_path, sj, name)
161
+ _interactive_setup(sj)
162
162
 
163
163
  # Install dependencies
164
164
  req = dst / "requirements.txt"
@@ -169,36 +169,14 @@ def hydrate(target: str) -> None:
169
169
  subprocess.run([python_run_cmd(), "-m", "pip", "install", "-r", str(req), "--quiet", "--user"])
170
170
 
171
171
  registry.register(name, abs_target)
172
+ interface_cli.setup(abs_target)
172
173
  ui.success(f"Hydrated '{name}' at {abs_target}")
173
174
  ui.info(f"Run 'silicon start {name}' when you're ready.")
174
175
  finally:
175
176
  shutil.rmtree(tmp_src, ignore_errors=True)
176
177
 
177
178
 
178
- def _interactive_setup(dst: Path, env_path: Path, sj: Path, name: str) -> None:
179
- if not _env_value(env_path, "TELEGRAM_BOT_TOKEN"):
180
- ui.info("You need a Telegram bot token to use Silicon.")
181
- sys.stderr.write(f"{ui.DIM} 1. Open Telegram and search for @BotFather{ui.RESET}\n")
182
- sys.stderr.write(f"{ui.DIM} 2. Send /newbot and follow the prompts{ui.RESET}\n")
183
- sys.stderr.write(f"{ui.DIM} 3. Copy the token BotFather gives you{ui.RESET}\n")
184
- token = ui.read_secret("Telegram bot token")
185
- if not token:
186
- ui.error("Telegram bot token is required.")
187
- sys.exit(1)
188
- _env_upsert(env_path, "TELEGRAM_BOT_TOKEN", token)
189
-
190
- if not _env_value(env_path, "OPENAI_API_KEY"):
191
- ui.info("OpenAI API key (for incoming voice transcription via Whisper). Enter to skip.")
192
- v = ui.read_secret("OpenAI API key (optional)")
193
- if v:
194
- _env_upsert(env_path, "OPENAI_API_KEY", v)
195
-
196
- if not _env_value(env_path, "GEMINI_API_KEY"):
197
- ui.info("Gemini API key (for outgoing text-to-speech). Enter to skip.")
198
- v = ui.read_secret("Gemini API key (optional)")
199
- if v:
200
- _env_upsert(env_path, "GEMINI_API_KEY", v)
201
-
179
+ def _interactive_setup(sj: Path) -> None:
202
180
  # Brain / worker providers — only ask when both claude + codex are present
203
181
  brain = "claude"
204
182
  workers = {"browser": ["claude"], "terminal": ["claude"], "writer": ["claude"]}