@oracle-agent/oracle 0.8.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.
- package/README.md +6 -4
- package/SETUP.md +1 -0
- package/docs/cli.md +2 -2
- package/package.json +1 -1
- package/protocols/templates/safe-erc20/README.md +9 -0
- package/public/oracle-splash/assets/llms/gemini-icon.svg +1 -0
- package/public/oracle-splash/assets/wordmarks/hop-icon.webp +0 -0
- package/public/oracle-splash/assets/wordmarks/satflow-icon.png +0 -0
- package/public/oracle-splash/assets/wordmarks/zerox-icon.webp +0 -0
- package/public/oracle-splash/favicon.ico +0 -0
- package/public/oracle-splash/favicon.svg +6 -0
- package/public/oracle-splash/index.html +105 -69
- package/skills/oracle-chat/setup.SKILL.md +2 -2
- package/skins/oracle.yaml +45 -41
- package/src/cli/commands/bootstrap.mjs +87 -0
- package/src/cli/commands/chat.mjs +140 -25
- package/src/cli/commands/doctor.mjs +5 -10
- package/src/cli/commands/model.mjs +19 -13
- package/src/cli/commands/setup.mjs +11 -19
- package/src/cli/first-run.mjs +2 -2
- package/src/cli/kernel.mjs +5 -4
- package/src/cli/messaging-platforms.mjs +1 -1
- package/src/cli/oracle-harness.py +226 -70
- package/src/cli/runtime.mjs +351 -0
|
@@ -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
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
5
9
|
import sys
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
11
|
+
|
|
12
|
+
WORDMARK = (
|
|
13
|
+
" ████ ",
|
|
14
|
+
" ░░███ ",
|
|
15
|
+
" ██████ ████████ ██████ ██████ ░███ ██████ ",
|
|
16
|
+
" ███░░███░░███░░███ ░░░░░███ ███░░███ ░███ ███░░███",
|
|
17
|
+
"░███ ░███ ░███ ░░░ ███████ ░███ ░░░ ░███ ░███████ ",
|
|
18
|
+
"░███ ░███ ░███ ███░░███ ░███ ███ ░███ ░███░░░ ",
|
|
19
|
+
"░░██████ █████ ░░████████░░██████ █████░░██████ ",
|
|
20
|
+
" ░░░░░░ ░░░░░ ░░░░░░░░ ░░░░░░ ░░░░░ ░░░░░░ ",
|
|
12
21
|
)
|
|
13
22
|
|
|
14
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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.
|
|
295
|
+
sys.stdout = OracleOutput(sys.stdout)
|
|
296
|
+
sys.stderr = OracleOutput(sys.stderr)
|
|
142
297
|
sys.meta_path.insert(0, OracleFinder())
|
|
143
|
-
|
|
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;
|