@oracle-agent/oracle 0.8.0 → 0.9.1
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 +271 -69
- package/src/cli/runtime.mjs +351 -0
|
@@ -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;
|