@oracle-agent/oracle 0.7.0 → 0.9.0

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 (36) hide show
  1. package/README.md +6 -4
  2. package/SETUP.md +1 -0
  3. package/bin/desk-server.mjs +2 -2
  4. package/docs/cli.md +2 -2
  5. package/package.json +1 -1
  6. package/profiles/oracle/SOUL.md +2 -2
  7. package/protocols/templates/safe-erc20/README.md +9 -0
  8. package/public/oracle-splash/assets/llms/codex-icon.svg +1 -0
  9. package/public/oracle-splash/assets/llms/gemini-icon.svg +1 -0
  10. package/public/oracle-splash/assets/wordmarks/across-icon.webp +0 -0
  11. package/public/oracle-splash/assets/wordmarks/hop-icon.webp +0 -0
  12. package/public/oracle-splash/assets/wordmarks/markets.svg +1 -0
  13. package/public/oracle-splash/assets/wordmarks/oneinch-icon.webp +0 -0
  14. package/public/oracle-splash/assets/wordmarks/opensea-icon.svg +17 -0
  15. package/public/oracle-splash/assets/wordmarks/satflow-icon.png +0 -0
  16. package/public/oracle-splash/assets/wordmarks/stargate-icon.webp +0 -0
  17. package/public/oracle-splash/assets/wordmarks/zerox-icon.webp +0 -0
  18. package/public/oracle-splash/favicon.ico +0 -0
  19. package/public/oracle-splash/favicon.svg +6 -0
  20. package/public/oracle-splash/index.html +153 -96
  21. package/scripts/check-doc-drift.mjs +1 -0
  22. package/skills/oracle-chat/setup.SKILL.md +2 -2
  23. package/skins/oracle.yaml +45 -41
  24. package/src/cli/commands/bootstrap.mjs +87 -0
  25. package/src/cli/commands/chat.mjs +140 -25
  26. package/src/cli/commands/doctor.mjs +5 -10
  27. package/src/cli/commands/model.mjs +19 -13
  28. package/src/cli/commands/setup.mjs +11 -19
  29. package/src/cli/first-run.mjs +2 -2
  30. package/src/cli/kernel.mjs +5 -4
  31. package/src/cli/messaging-platforms.mjs +1 -1
  32. package/src/cli/oracle-harness.py +226 -70
  33. package/src/cli/runtime.mjs +351 -0
  34. package/src/data/catalog.mjs +17 -2
  35. package/src/public-api/http.mjs +16 -1
  36. package/src/public-control/runtime-config.mjs +11 -1
@@ -1,23 +1,80 @@
1
+ #!/usr/bin/env python3
1
2
  import importlib
2
3
  import importlib.abc
3
4
  import importlib.machinery
4
- import re
5
+ import json
6
+ import os
7
+ import shutil
8
+ import subprocess
5
9
  import sys
6
10
 
7
- LOGO = (
8
- " _",
9
- " ___ _ _ __ _ __| |___",
10
- "/ _ \\ '_/ _` / _| / -_)",
11
- "\\___/_| \\__,_\\__|_\\___|",
11
+
12
+ WORDMARK = (
13
+ " ████ ",
14
+ " ░░███ ",
15
+ " ██████ ████████ ██████ ██████ ░███ ██████ ",
16
+ " ███░░███░░███░░███ ░░░░░███ ███░░███ ░███ ███░░███",
17
+ "░███ ░███ ░███ ░░░ ███████ ░███ ░░░ ░███ ░███████ ",
18
+ "░███ ░███ ░███ ███░░███ ░███ ███ ░███ ░███░░░ ",
19
+ "░░██████ █████ ░░████████░░██████ █████░░██████ ",
20
+ " ░░░░░░ ░░░░░ ░░░░░░░░ ░░░░░░ ░░░░░ ░░░░░░ ",
12
21
  )
13
22
 
14
- SILENT_STDERR = {
23
+ SILENT_OUTPUT = {
15
24
  "[anthropic_billing_bypass] Bypass installed",
16
25
  "[anthropic_billing_bypass] Transport unwrap hook installed",
17
26
  }
18
27
 
19
28
 
20
- class OracleStderr:
29
+ def _oracle_chain(command):
30
+ node = os.environ.get("ORACLE_NODE_BIN")
31
+ entry = os.environ.get("ORACLE_CLI_ENTRY")
32
+ if not node or not entry:
33
+ return False, "oracle chain is unavailable in this session"
34
+
35
+ parts = command.strip().split()[1:]
36
+ args = parts or ["list"]
37
+ try:
38
+ result = subprocess.run(
39
+ [node, entry, "chain", *args],
40
+ capture_output=True,
41
+ text=True,
42
+ timeout=15,
43
+ check=False,
44
+ )
45
+ except (OSError, subprocess.TimeoutExpired) as exc:
46
+ return False, f"oracle chain failed: {exc}"
47
+
48
+ output = "\n".join(
49
+ part.strip() for part in (result.stdout, result.stderr) if part.strip()
50
+ )
51
+ if result.returncode != 0:
52
+ return False, output or "oracle chain failed"
53
+
54
+ state = subprocess.run(
55
+ [node, entry, "chain", "show", "--json"],
56
+ capture_output=True,
57
+ text=True,
58
+ timeout=15,
59
+ check=False,
60
+ )
61
+ if state.returncode in (0, 1):
62
+ try:
63
+ active = json.loads(state.stdout).get("active") or {}
64
+ if active:
65
+ os.environ["ORACLE_ACTIVE_CHAIN"] = str(active.get("key", ""))
66
+ os.environ["ORACLE_ACTIVE_CHAIN_ID"] = str(active.get("chainId", ""))
67
+ os.environ["ORACLE_ACTIVE_AGENT"] = str(active.get("agent", ""))
68
+ else:
69
+ os.environ.pop("ORACLE_ACTIVE_CHAIN", None)
70
+ os.environ.pop("ORACLE_ACTIVE_CHAIN_ID", None)
71
+ os.environ.pop("ORACLE_ACTIVE_AGENT", None)
72
+ except (TypeError, ValueError):
73
+ pass
74
+ return True, output
75
+
76
+
77
+ class OracleOutput:
21
78
  def __init__(self, wrapped):
22
79
  self.wrapped = wrapped
23
80
 
@@ -25,8 +82,12 @@ class OracleStderr:
25
82
  kept = "".join(
26
83
  line
27
84
  for line in data.splitlines(keepends=True)
28
- if line.rstrip("\r\n") not in SILENT_STDERR
85
+ if line.rstrip("\r\n") not in SILENT_OUTPUT
29
86
  )
87
+ kept = kept.replace("hermes model", "oracle model")
88
+ kept = kept.replace("hermes --resume", "oracle --resume")
89
+ kept = kept.replace("hermes -c", "oracle -c")
90
+ kept = kept.replace(" in ~/.hermes/.env", " through oracle model")
30
91
  if kept:
31
92
  self.wrapped.write(kept)
32
93
  return len(data)
@@ -39,74 +100,167 @@ class OracleStderr:
39
100
 
40
101
 
41
102
  def patch_cli(module):
42
- hermes_cli = getattr(module, "HermesCLI")
43
- original_layout = hermes_cli._build_tui_layout_children
44
- original_emit = hermes_cli._emit_stream_text
45
- original_flush = hermes_cli._flush_stream
46
- original_cprint = getattr(module, "_cprint")
47
- frame_filter = [0]
48
-
49
- def cprint(text="", *args, **kwargs):
50
- if frame_filter[0] and isinstance(text, str):
51
- plain = re.sub(r"\x1b\[[0-9;]*m", "", text).lstrip("\n")
52
- if plain.startswith("\u256d\u2500oracle") or plain.startswith("\u2570"):
53
- return None
54
- return original_cprint(text, *args, **kwargs)
55
-
56
- setattr(module, "_cprint", cprint)
103
+ cls = getattr(module, "HermesCLI", None)
104
+ if cls is None or getattr(cls, "_oracle_patched", False):
105
+ return
106
+
107
+ original_style = getattr(cls, "_build_tui_style_dict", None)
108
+ original_process_command = cls.process_command
57
109
 
58
110
  def show_banner(self):
111
+ from rich.align import Align
112
+ from rich.panel import Panel
113
+ from rich.text import Text
114
+
59
115
  self.console.clear()
60
- self._console_print()
61
- for line in LOGO:
62
- self._console_print(f"[#e6edf3]{line}[/]")
63
- self._console_print()
116
+ width = shutil.get_terminal_size((100, 28)).columns
117
+ body = Text(justify="center")
118
+ if width >= 72:
119
+ colors = (
120
+ "#B8F0FF",
121
+ "#B8F0FF",
122
+ "#ACDEEF",
123
+ "#A5D9EB",
124
+ "#9FCBDD",
125
+ "#9FCBDD",
126
+ "#ACDEEF",
127
+ "#B8F0FF",
128
+ )
129
+ for index, line in enumerate(WORDMARK):
130
+ body.append(line.rstrip(), style=f"bold {colors[index]}")
131
+ if index != len(WORDMARK) - 1:
132
+ body.append("\n")
133
+ else:
134
+ body.append("oracle", style="bold #B8F0FF")
135
+
136
+ body.append("\n\nTHE FUTURE IS AGENTIC", style="bold #EAF2F8")
137
+ body.append(" / ", style="#52606D")
138
+ model = str(getattr(self, "model", "") or "choose model")
139
+ body.append(model, style="#91A2B1")
140
+ body.append("\n\n/model /chain /setup", style="#60717F")
141
+
142
+ panel_width = min(max(66, 66 if width >= 72 else 34), max(width - 4, 34))
143
+ panel = Panel(
144
+ Align.center(body),
145
+ width=panel_width,
146
+ padding=(0, 2),
147
+ border_style="#455867",
148
+ )
149
+ self.console.print(Align.center(panel))
150
+ self.console.print()
64
151
 
65
152
  def prompt_fragments(self):
66
- if self._approval_state or getattr(self, "_slash_confirm_state", None):
67
- marker = "!"
68
- elif self._clarify_state or self._clarify_freetext:
69
- marker = "?"
70
- elif self._command_running or self._agent_running:
71
- marker = "..."
72
- else:
73
- marker = ">"
74
- return [("class:prompt", f"oracle {marker} ")]
75
-
76
- def layout_children(self, **kwargs):
77
- kwargs["status_bar"] = None
78
- kwargs["input_rule_top"] = None
79
- kwargs["input_rule_bot"] = None
80
- return original_layout(self, **kwargs)
81
-
82
- def emit_stream_text(self, text):
83
- first = not self._stream_box_opened
84
- if first:
85
- original_cprint()
86
- frame_filter[0] += 1
87
- try:
88
- return original_emit(self, text)
89
- finally:
90
- frame_filter[0] -= 1
153
+ if getattr(self, "_voice_recording", False):
154
+ return [("class:voice-recording", "recording ")]
155
+ if getattr(self, "_voice_processing", False):
156
+ return [("class:voice-processing", "transcribing ")]
157
+ if getattr(self, "_sudo_state", None) or getattr(self, "_secret_state", None):
158
+ return [("class:sudo-prompt", "secure › ")]
159
+ if getattr(self, "_approval_state", None) or getattr(self, "_slash_confirm_state", None):
160
+ return [("class:prompt-working", "confirm › ")]
161
+ if getattr(self, "_clarify_freetext", False):
162
+ return [("class:clarify-selected", "answer › ")]
163
+ if getattr(self, "_clarify_state", None):
164
+ return [("class:prompt-working", "choose › ")]
165
+ if getattr(self, "_command_running", False):
166
+ return [("class:prompt-working", "working ")]
167
+ if getattr(self, "_agent_running", False):
168
+ return [("class:prompt-working", "thinking ")]
169
+ return [("class:prompt", "› ")]
91
170
 
92
- def flush_stream(self):
93
- frame_filter[0] += 1
94
- try:
95
- return original_flush(self)
96
- finally:
97
- frame_filter[0] -= 1
171
+ def layout_children(
172
+ self,
173
+ *,
174
+ sudo_widget,
175
+ secret_widget,
176
+ approval_widget,
177
+ slash_confirm_widget=None,
178
+ clarify_widget,
179
+ model_picker_widget=None,
180
+ spinner_widget=None,
181
+ spacer,
182
+ status_bar,
183
+ input_rule_top,
184
+ image_bar,
185
+ input_area,
186
+ input_rule_bot,
187
+ voice_status_bar,
188
+ completions_menu,
189
+ ):
190
+ from prompt_toolkit.layout.containers import HSplit, Window
191
+ from prompt_toolkit.widgets import Frame
192
+ from prompt_toolkit.widgets.base import Border
98
193
 
99
- hermes_cli.show_banner = show_banner
100
- hermes_cli._get_tui_prompt_fragments = prompt_fragments
101
- hermes_cli._build_tui_layout_children = layout_children
102
- hermes_cli._emit_stream_text = emit_stream_text
103
- hermes_cli._flush_stream = flush_stream
104
- tips = importlib.import_module("hermes_cli.tips")
194
+ Border.TOP_LEFT = "╭"
195
+ Border.TOP_RIGHT = "╮"
196
+ Border.BOTTOM_LEFT = "╰"
197
+ Border.BOTTOM_RIGHT = "╯"
105
198
 
106
- def no_tip():
107
- raise LookupError("oracle quiet startup")
199
+ composer = Frame(
200
+ HSplit([item for item in (image_bar, input_area) if item is not None]),
201
+ style="class:oracle-composer",
202
+ )
203
+ return [
204
+ item
205
+ for item in (
206
+ Window(height=0),
207
+ sudo_widget,
208
+ secret_widget,
209
+ approval_widget,
210
+ slash_confirm_widget,
211
+ clarify_widget,
212
+ model_picker_widget,
213
+ spinner_widget,
214
+ spacer,
215
+ composer,
216
+ completions_menu,
217
+ voice_status_bar,
218
+ )
219
+ if item is not None
220
+ ]
108
221
 
109
- setattr(tips, "get_random_tip", no_tip)
222
+ def build_style(self):
223
+ style = original_style(self) if original_style else {}
224
+ style.update(
225
+ {
226
+ "frame": "bg:#0B0E11 #EAF2F8",
227
+ "frame.border": "#455867",
228
+ "frame.label": "bold #B8F0FF",
229
+ "oracle-composer": "bg:#0B0E11 #EAF2F8",
230
+ "input-area": "bg:#0B0E11 #EAF2F8",
231
+ "prompt": "bold #B8F0FF",
232
+ "prompt-working": "bold #91C2D7",
233
+ }
234
+ )
235
+ return style
236
+
237
+ def process_command(self, command):
238
+ parts = command.strip().lower().split(maxsplit=1)
239
+ if parts and parts[0] == "/chain":
240
+ ok, output = _oracle_chain(command)
241
+ prefix = "" if ok else "error: "
242
+ lines = output.splitlines() or [""]
243
+ for index, line in enumerate(lines):
244
+ module._cprint(f"{prefix if index == 0 else ''}{line}")
245
+ return True
246
+ return original_process_command(self, command)
247
+
248
+ setattr(cls, "show_banner", show_banner)
249
+ setattr(cls, "_get_tui_prompt_fragments", prompt_fragments)
250
+ setattr(cls, "_build_tui_layout_children", layout_children)
251
+ setattr(cls, "_build_tui_style_dict", build_style)
252
+ setattr(cls, "process_command", process_command)
253
+ setattr(cls, "_oracle_patched", True)
254
+
255
+ try:
256
+ tips = importlib.import_module("hermes_cli.tips")
257
+
258
+ def no_tip():
259
+ raise LookupError("oracle quiet startup")
260
+
261
+ setattr(tips, "get_random_tip", no_tip)
262
+ except Exception:
263
+ pass
110
264
 
111
265
 
112
266
  class OracleLoader(importlib.abc.Loader):
@@ -138,9 +292,11 @@ class OracleFinder(importlib.abc.MetaPathFinder):
138
292
 
139
293
 
140
294
  def main():
141
- sys.stderr = OracleStderr(sys.stderr)
295
+ sys.stdout = OracleOutput(sys.stdout)
296
+ sys.stderr = OracleOutput(sys.stderr)
142
297
  sys.meta_path.insert(0, OracleFinder())
143
- hermes_main = getattr(importlib.import_module("hermes_cli.main"), "main")
298
+ from hermes_cli.main import main as hermes_main
299
+
144
300
  return hermes_main()
145
301
 
146
302
 
@@ -0,0 +1,351 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { spawnSync } from "node:child_process";
5
+ import { homeDir, oracleConfigDir, ensureDir } from "./paths.mjs";
6
+
7
+ export const HERMES_VERSION = "0.19.0";
8
+ export const HERMES_PYPI = `hermes-agent==${HERMES_VERSION}`;
9
+ const MIN_PY = [3, 11];
10
+ const MAX_PY_EXCLUSIVE = [3, 14];
11
+ export const UV_PYTHON = "3.13";
12
+ export const UV_VERSION = "0.12.1";
13
+ export const UV_INSTALLERS = Object.freeze({
14
+ posix: Object.freeze({
15
+ url: `https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-installer.sh`,
16
+ sha256: "d3f5412d38c99f9d024901843bf98206f0d2c6dbe64df40d0b740e2751ca62c1",
17
+ }),
18
+ windows: Object.freeze({
19
+ url: `https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-installer.ps1`,
20
+ sha256: "b9ec035151bfbb11d616dbd69886498d885551489eddf095ea3c0ad59f640eb0",
21
+ }),
22
+ });
23
+
24
+ export function verifyUvInstaller(bytes, { windows = process.platform === "win32" } = {}) {
25
+ const body = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
26
+ const spec = windows ? UV_INSTALLERS.windows : UV_INSTALLERS.posix;
27
+ const digest = createHash("sha256").update(body).digest("hex");
28
+ const text = body.toString("utf8");
29
+ return digest === spec.sha256 && text.length >= 1_000 && text.includes(UV_VERSION);
30
+ }
31
+
32
+ export function runtimeDir() {
33
+ return path.join(oracleConfigDir(), "runtime");
34
+ }
35
+
36
+ export function runtimeVenvDir() {
37
+ return path.join(runtimeDir(), "venv");
38
+ }
39
+
40
+ function venvBin(name) {
41
+ const dir = process.platform === "win32" ? "Scripts" : "bin";
42
+ const exe = process.platform === "win32" ? `${name}.exe` : name;
43
+ return path.join(runtimeVenvDir(), dir, exe);
44
+ }
45
+
46
+ export function managedHermesPath() {
47
+ return venvBin("hermes");
48
+ }
49
+
50
+ export function managedPythonPath() {
51
+ const py = venvBin("python3");
52
+ return fs.existsSync(py) ? py : venvBin("python");
53
+ }
54
+
55
+ export function managedUvPath() {
56
+ return path.join(runtimeDir(), "uv", process.platform === "win32" ? "uv.exe" : "uv");
57
+ }
58
+
59
+ export function whichBin(bin) {
60
+ const suffixes = process.platform === "win32" && !path.extname(bin)
61
+ ? ["", ".exe", ".cmd", ".bat"]
62
+ : [""];
63
+ for (const dir of (process.env.PATH || "").split(path.delimiter)) {
64
+ if (!dir) continue;
65
+ for (const suffix of suffixes) {
66
+ const c = path.join(dir, `${bin}${suffix}`);
67
+ try {
68
+ if (fs.existsSync(c)) return c;
69
+ } catch {}
70
+ }
71
+ }
72
+ return null;
73
+ }
74
+
75
+ function parsePyVersion(out) {
76
+ const m = /Python (\d+)\.(\d+)\.(\d+)/.exec(out || "");
77
+ if (!m) return null;
78
+ return [Number(m[1]), Number(m[2]), Number(m[3])];
79
+ }
80
+
81
+ function versionSupported(v) {
82
+ if (!v) return false;
83
+ const [maj, min] = v;
84
+ if (maj < MIN_PY[0] || (maj === MIN_PY[0] && min < MIN_PY[1])) return false;
85
+ if (maj > MAX_PY_EXCLUSIVE[0] || (maj === MAX_PY_EXCLUSIVE[0] && min >= MAX_PY_EXCLUSIVE[1])) {
86
+ return false;
87
+ }
88
+ return true;
89
+ }
90
+
91
+ export function findHostPython() {
92
+ const candidates = [
93
+ process.env.ORACLE_PYTHON,
94
+ "python3.13",
95
+ "python3.12",
96
+ "python3.11",
97
+ "python3",
98
+ "python",
99
+ ].filter(Boolean);
100
+ for (const cand of candidates) {
101
+ const bin = path.isAbsolute(cand) ? (fs.existsSync(cand) ? cand : null) : whichBin(cand);
102
+ if (!bin) continue;
103
+ const r = spawnSync(bin, ["--version"], { encoding: "utf8", timeout: 15_000 });
104
+ const v = parsePyVersion(`${r.stdout || ""}${r.stderr || ""}`);
105
+ if (versionSupported(v)) return { bin, version: v.join("."), source: "host" };
106
+ }
107
+ return null;
108
+ }
109
+
110
+ function uvBin() {
111
+ return (
112
+ process.env.ORACLE_UV_BIN ||
113
+ whichBin("uv") ||
114
+ (fs.existsSync(path.join(homeDir(), ".local", "bin", "uv"))
115
+ ? path.join(homeDir(), ".local", "bin", "uv")
116
+ : null) ||
117
+ (fs.existsSync(managedUvPath()) ? managedUvPath() : null)
118
+ );
119
+ }
120
+
121
+ function downloadFile(url, dest) {
122
+ const script = [
123
+ "const fs=require('node:fs');",
124
+ "const [url,dest]=process.argv.slice(1);",
125
+ "fetch(url).then(r=>{if(!r.ok)throw new Error('http '+r.status);return r.arrayBuffer()})",
126
+ ".then(b=>fs.writeFileSync(dest,Buffer.from(b)))",
127
+ ".catch(e=>{console.error(e.message);process.exit(1)});",
128
+ ].join("");
129
+ return spawnSync(process.execPath, ["-e", script, url, dest], {
130
+ encoding: "utf8",
131
+ timeout: 2 * 60_000,
132
+ });
133
+ }
134
+
135
+ export function installManagedUv({ quiet = false } = {}) {
136
+ const existing = uvBin();
137
+ if (existing) return { ok: true, bin: existing, reused: true };
138
+
139
+ ensureDir(runtimeDir());
140
+ const windows = process.platform === "win32";
141
+ const installer = path.join(runtimeDir(), windows ? "uv-install.ps1" : "uv-install.sh");
142
+ const spec = windows ? UV_INSTALLERS.windows : UV_INSTALLERS.posix;
143
+ const dl = downloadFile(spec.url, installer);
144
+ if (dl.status !== 0) {
145
+ return {
146
+ ok: false,
147
+ reason: `could not download the uv installer (${dl.status})`,
148
+ stderr: (dl.stderr || "").slice(-800),
149
+ };
150
+ }
151
+
152
+ const body = fs.readFileSync(installer);
153
+ if (!verifyUvInstaller(body, { windows })) {
154
+ return { ok: false, reason: `uv ${UV_VERSION} installer checksum mismatch` };
155
+ }
156
+
157
+ const installDir = path.dirname(managedUvPath());
158
+ ensureDir(installDir);
159
+ const command = windows
160
+ ? (whichBin("powershell.exe") || whichBin("powershell"))
161
+ : (whichBin("sh") || "/bin/sh");
162
+ if (!command) return { ok: false, reason: windows ? "PowerShell not found" : "sh not found" };
163
+ const args = windows
164
+ ? ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", installer]
165
+ : [installer];
166
+ const inst = spawnSync(command, args, {
167
+ stdio: quiet ? "pipe" : "inherit",
168
+ encoding: "utf8",
169
+ timeout: 5 * 60_000,
170
+ env: {
171
+ ...process.env,
172
+ UV_INSTALL_DIR: installDir,
173
+ UV_NO_MODIFY_PATH: "1",
174
+ },
175
+ });
176
+ if (inst.status !== 0 || !fs.existsSync(managedUvPath())) {
177
+ return {
178
+ ok: false,
179
+ reason: `uv install failed (${inst.status})`,
180
+ stderr: (inst.stderr || "").slice(-800),
181
+ };
182
+ }
183
+ return { ok: true, bin: managedUvPath(), reused: false };
184
+ }
185
+
186
+ export function provisionPythonViaUv({ quiet = false } = {}) {
187
+ const installed = installManagedUv({ quiet });
188
+ if (!installed.ok) return installed;
189
+ const uv = installed.bin;
190
+
191
+ const uvEnv = {
192
+ ...process.env,
193
+ UV_PYTHON_INSTALL_DIR: path.join(runtimeDir(), "python"),
194
+ UV_CACHE_DIR: path.join(runtimeDir(), "cache"),
195
+ };
196
+ const inst = spawnSync(uv, ["python", "install", "--no-bin", UV_PYTHON], {
197
+ stdio: quiet ? "pipe" : "inherit",
198
+ encoding: "utf8",
199
+ timeout: 15 * 60_000,
200
+ env: uvEnv,
201
+ });
202
+ if (inst.status !== 0) {
203
+ return {
204
+ ok: false,
205
+ reason: `uv python install ${UV_PYTHON} failed (${inst.status})`,
206
+ stderr: (inst.stderr || "").slice(-800),
207
+ };
208
+ }
209
+
210
+ const found = spawnSync(uv, ["python", "find", UV_PYTHON], {
211
+ encoding: "utf8",
212
+ timeout: 60_000,
213
+ env: uvEnv,
214
+ });
215
+ const bin = (found.stdout || "").trim().split(/\r?\n/)[0];
216
+ if (found.status !== 0 || !bin || !fs.existsSync(bin)) {
217
+ return { ok: false, reason: `uv could not locate python ${UV_PYTHON}` };
218
+ }
219
+ return { ok: true, bin, version: UV_PYTHON, source: "uv" };
220
+ }
221
+
222
+ /**
223
+ * Resolve the hermes runtime this Oracle install should use.
224
+ * Order: explicit override -> system PATH -> oracle-managed venv.
225
+ */
226
+ export function resolveHermes() {
227
+ if (process.env.ORACLE_HERMES_BIN) {
228
+ return { ok: true, bin: process.env.ORACLE_HERMES_BIN, source: "env" };
229
+ }
230
+ const onPath = whichBin("hermes");
231
+ if (onPath) return { ok: true, bin: onPath, source: "path" };
232
+ const managed = managedHermesPath();
233
+ if (fs.existsSync(managed)) return { ok: true, bin: managed, source: "managed" };
234
+ return { ok: false, bin: null, source: null };
235
+ }
236
+
237
+ export function runtimeStatus() {
238
+ const resolved = resolveHermes();
239
+ const host = findHostPython();
240
+ return {
241
+ hermes: resolved,
242
+ managedVenv: runtimeVenvDir(),
243
+ managedInstalled: fs.existsSync(managedHermesPath()),
244
+ hostPython: host,
245
+ uv: uvBin(),
246
+ };
247
+ }
248
+
249
+ function run(cmd, args, { quiet }) {
250
+ return spawnSync(cmd, args, {
251
+ stdio: quiet ? "pipe" : "inherit",
252
+ encoding: "utf8",
253
+ timeout: 20 * 60_000,
254
+ });
255
+ }
256
+
257
+ /**
258
+ * Create ~/.config/oracle/runtime/venv and install hermes-agent into it.
259
+ * Never touches system python or global site-packages.
260
+ */
261
+ export function installManagedHermes({ quiet = false, upgrade = false } = {}) {
262
+ let host = findHostPython();
263
+ if (!host) {
264
+ const viaUv = provisionPythonViaUv({ quiet });
265
+ if (viaUv.ok) {
266
+ host = { bin: viaUv.bin, version: viaUv.version, source: "uv" };
267
+ } else {
268
+ return {
269
+ ok: false,
270
+ reason:
271
+ `no supported python found (need 3.11-3.13) and uv could not provide one: ${viaUv.reason}. ` +
272
+ "install uv (https://docs.astral.sh/uv) or python 3.13, then re-run 'oracle bootstrap'",
273
+ };
274
+ }
275
+ }
276
+
277
+ ensureDir(runtimeDir());
278
+ const venv = runtimeVenvDir();
279
+ const alreadyInstalled = fs.existsSync(managedHermesPath());
280
+ if (alreadyInstalled && !upgrade) {
281
+ return { ok: true, bin: managedHermesPath(), reused: true };
282
+ }
283
+
284
+ if (!fs.existsSync(path.join(venv, "pyvenv.cfg"))) {
285
+ let mk = run(host.bin, ["-m", "venv", venv], { quiet });
286
+ if (mk.status !== 0 && host.source !== "uv") {
287
+ fs.rmSync(venv, { recursive: true, force: true });
288
+ const viaUv = provisionPythonViaUv({ quiet });
289
+ if (viaUv.ok) {
290
+ host = { bin: viaUv.bin, version: viaUv.version, source: "uv" };
291
+ mk = run(host.bin, ["-m", "venv", venv], { quiet });
292
+ } else {
293
+ return {
294
+ ok: false,
295
+ reason: `python -m venv failed (${mk.status}) and uv fallback failed: ${viaUv.reason}`,
296
+ stderr: (mk.stderr || "").slice(-800),
297
+ };
298
+ }
299
+ }
300
+ if (mk.status !== 0) {
301
+ return {
302
+ ok: false,
303
+ reason: `isolated python -m venv failed (${mk.status})`,
304
+ stderr: (mk.stderr || "").slice(-800),
305
+ };
306
+ }
307
+ }
308
+
309
+ const py = managedPythonPath();
310
+ if (!fs.existsSync(py)) {
311
+ return { ok: false, reason: `venv python missing at ${py}` };
312
+ }
313
+
314
+ const pipArgs = ["-m", "pip", "install", "--disable-pip-version-check"];
315
+ if (upgrade) pipArgs.push("--upgrade");
316
+ pipArgs.push(HERMES_PYPI);
317
+ const inst = run(py, pipArgs, { quiet });
318
+ if (inst.status !== 0) {
319
+ return {
320
+ ok: false,
321
+ reason: `pip install ${HERMES_PYPI} failed (${inst.status})`,
322
+ stderr: (inst.stderr || "").slice(-1200),
323
+ };
324
+ }
325
+
326
+ const bin = managedHermesPath();
327
+ if (!fs.existsSync(bin)) {
328
+ return { ok: false, reason: `hermes entrypoint not found after install (${bin})` };
329
+ }
330
+ return { ok: true, bin, reused: false, python: host.version };
331
+ }
332
+
333
+ export const runtime = {
334
+ HERMES_PYPI,
335
+ HERMES_VERSION,
336
+ UV_PYTHON,
337
+ runtimeDir,
338
+ runtimeVenvDir,
339
+ managedHermesPath,
340
+ managedPythonPath,
341
+ managedUvPath,
342
+ findHostPython,
343
+ installManagedUv,
344
+ provisionPythonViaUv,
345
+ resolveHermes,
346
+ runtimeStatus,
347
+ installManagedHermes,
348
+ whichBin,
349
+ };
350
+
351
+ export default runtime;