@oracle-agent/oracle 0.5.0 → 0.6.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 +10 -1
- package/docs/cli.md +28 -0
- package/package.json +3 -3
- package/profiles/oracle/SOUL.md +10 -4
- package/protocols/templates/safe-erc20/README.md +9 -0
- package/public/oracle-splash/assets/hero/orb.avif +0 -0
- package/public/oracle-splash/assets/hero/orb.webm +0 -0
- package/public/oracle-splash/assets/llms/claude.svg +1 -0
- package/public/oracle-splash/assets/llms/codex.svg +1 -0
- package/public/oracle-splash/assets/llms/cursor.svg +17 -0
- package/public/oracle-splash/assets/llms/deepseek.svg +1 -0
- package/public/oracle-splash/assets/llms/gemini.svg +1 -0
- package/public/oracle-splash/assets/llms/grok.svg +8 -0
- package/public/oracle-splash/assets/llms/hermes.png +0 -0
- package/public/oracle-splash/assets/llms/kimi.svg +1 -0
- package/public/oracle-splash/assets/llms/manus.svg +18 -0
- package/public/oracle-splash/assets/llms/openclaw.svg +22 -0
- package/public/oracle-splash/assets/llms/perplexity.svg +1 -0
- package/public/oracle-splash/assets/llms/qwen.svg +1 -0
- package/public/oracle-splash/assets/llms/windsurf.svg +1 -0
- package/public/oracle-splash/assets/protocols/abcdex.png +0 -0
- package/public/oracle-splash/assets/protocols/across.png +0 -0
- package/public/oracle-splash/assets/protocols/aerodrome.png +0 -0
- package/public/oracle-splash/assets/protocols/balancer.png +0 -0
- package/public/oracle-splash/assets/protocols/cowswap.png +0 -0
- package/public/oracle-splash/assets/protocols/curve.png +0 -0
- package/public/oracle-splash/assets/protocols/dreamcash.png +0 -0
- package/public/oracle-splash/assets/protocols/felix.png +0 -0
- package/public/oracle-splash/assets/protocols/gmx.png +0 -0
- package/public/oracle-splash/assets/protocols/hyena.png +0 -0
- package/public/oracle-splash/assets/protocols/hyperbeat.png +0 -0
- package/public/oracle-splash/assets/protocols/hyperliquid.png +0 -0
- package/public/oracle-splash/assets/protocols/hyperswap.png +0 -0
- package/public/oracle-splash/assets/protocols/jupiter.png +0 -0
- package/public/oracle-splash/assets/protocols/kinetiq.png +0 -0
- package/public/oracle-splash/assets/protocols/lfj.png +0 -0
- package/public/oracle-splash/assets/protocols/lifi.png +0 -0
- package/public/oracle-splash/assets/protocols/magic-eden.png +0 -0
- package/public/oracle-splash/assets/protocols/morpho.png +0 -0
- package/public/oracle-splash/assets/protocols/odos.png +0 -0
- package/public/oracle-splash/assets/protocols/oneinch.png +0 -0
- package/public/oracle-splash/assets/protocols/opensea.png +0 -0
- package/public/oracle-splash/assets/protocols/pancakeswap.png +0 -0
- package/public/oracle-splash/assets/protocols/paragon.png +0 -0
- package/public/oracle-splash/assets/protocols/paraswap.png +0 -0
- package/public/oracle-splash/assets/protocols/pendle.png +0 -0
- package/public/oracle-splash/assets/protocols/polymarket.png +0 -0
- package/public/oracle-splash/assets/protocols/quickswap.png +0 -0
- package/public/oracle-splash/assets/protocols/relay.png +0 -0
- package/public/oracle-splash/assets/protocols/stargate.png +0 -0
- package/public/oracle-splash/assets/protocols/tradexyz.png +0 -0
- package/public/oracle-splash/assets/protocols/uniswap.png +0 -0
- package/public/oracle-splash/assets/protocols/velodrome.png +0 -0
- package/public/oracle-splash/assets/protocols/ventuals.png +0 -0
- package/public/oracle-splash/assets/wordmarks/hyena.svg +7 -0
- package/public/oracle-splash/assets/wordmarks/opensea.svg +1 -1
- package/public/oracle-splash/assets/wordmarks/outcome.svg +16 -0
- package/public/oracle-splash/assets/wordmarks/xyz.svg +12 -0
- package/public/oracle-splash/index.html +121 -139
- package/skills/oracle-chat/SKILL.md +61 -0
- package/skills/oracle-chat/chain.SKILL.md +31 -0
- package/skills/oracle-chat/setup.SKILL.md +37 -0
- package/skins/oracle.yaml +52 -0
- package/src/cli/chain-catalog.mjs +215 -0
- package/src/cli/chain-state.mjs +74 -0
- package/src/cli/commands/chain.mjs +143 -0
- package/src/cli/commands/chat.mjs +268 -0
- package/src/cli/commands/model.mjs +37 -0
- package/src/cli/commands/setup.mjs +283 -0
- package/src/cli/first-run.mjs +3 -0
- package/src/cli/kernel.mjs +42 -3
- package/src/cli/messaging-platforms.mjs +209 -0
- package/src/cli/oracle-harness.py +148 -0
- package/src/cli/paths.mjs +5 -0
- package/src/cli/setup-state.mjs +168 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { PACKAGE_ROOT, hermesRoot, ensureDir } from "../paths.mjs";
|
|
5
|
+
import { activeChainEnv } from "../chain-state.mjs";
|
|
6
|
+
import { spawnChild } from "../spawn-child.mjs";
|
|
7
|
+
|
|
8
|
+
const PROFILE = "oracle";
|
|
9
|
+
const SKIN_NAME = "oracle";
|
|
10
|
+
|
|
11
|
+
function which(bin) {
|
|
12
|
+
if (bin === "hermes" && process.env.ORACLE_HERMES_BIN) {
|
|
13
|
+
return process.env.ORACLE_HERMES_BIN;
|
|
14
|
+
}
|
|
15
|
+
for (const dir of (process.env.PATH || "").split(path.delimiter)) {
|
|
16
|
+
const c = path.join(dir, bin);
|
|
17
|
+
try {
|
|
18
|
+
if (fs.existsSync(c)) return c;
|
|
19
|
+
} catch {}
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function pythonShebang(file) {
|
|
25
|
+
try {
|
|
26
|
+
const first = fs.readFileSync(file, "utf8").split(/\r?\n/, 1)[0];
|
|
27
|
+
if (!first.startsWith("#!")) return null;
|
|
28
|
+
const parts = first.slice(2).trim().split(/\s+/);
|
|
29
|
+
if (parts[0]?.endsWith("/env") && parts[1]?.startsWith("python")) {
|
|
30
|
+
return { command: parts[0], prefix: [parts[1]] };
|
|
31
|
+
}
|
|
32
|
+
if (path.basename(parts[0] || "").startsWith("python")) {
|
|
33
|
+
return { command: parts[0], prefix: parts.slice(1) };
|
|
34
|
+
}
|
|
35
|
+
} catch {}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function resolveHermesPython(hermes) {
|
|
40
|
+
const direct = pythonShebang(hermes);
|
|
41
|
+
if (direct) return direct;
|
|
42
|
+
try {
|
|
43
|
+
const body = fs.readFileSync(hermes, "utf8");
|
|
44
|
+
const match = body.match(/["']([^"'\r\n]+\/bin\/hermes)["']/);
|
|
45
|
+
if (match) return pythonShebang(match[1]);
|
|
46
|
+
} catch {}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function harnessSourcePath() {
|
|
51
|
+
return path.join(PACKAGE_ROOT, "src", "cli", "oracle-harness.py");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function skinSourcePath() {
|
|
55
|
+
return path.join(PACKAGE_ROOT, "skins", "oracle.yaml");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function ensureOracleSkin(profile = PROFILE) {
|
|
59
|
+
const src = skinSourcePath();
|
|
60
|
+
if (!fs.existsSync(src)) return { ok: false, reason: "skin missing from package" };
|
|
61
|
+
const rootSkins = path.join(hermesRoot(), "skins");
|
|
62
|
+
const profileSkins = path.join(hermesRoot(), "profiles", profile, "skins");
|
|
63
|
+
ensureDir(rootSkins);
|
|
64
|
+
ensureDir(profileSkins);
|
|
65
|
+
const body = fs.readFileSync(src, "utf8");
|
|
66
|
+
fs.writeFileSync(path.join(rootSkins, `${SKIN_NAME}.yaml`), body);
|
|
67
|
+
fs.writeFileSync(path.join(profileSkins, `${SKIN_NAME}.yaml`), body);
|
|
68
|
+
return { ok: true, skin: SKIN_NAME };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function ensureQuickCommands(profile = PROFILE) {
|
|
72
|
+
// Install /chain and /setup as hermes quick commands that call oracle.
|
|
73
|
+
const cfgPath = path.join(hermesRoot(), "profiles", profile, "config.yaml");
|
|
74
|
+
if (!fs.existsSync(cfgPath)) return { ok: false, reason: "profile config missing" };
|
|
75
|
+
|
|
76
|
+
// Prefer a tiny sidecar json the chat skill can also read; hermes quick_commands
|
|
77
|
+
// live in config.yaml which we must not corrupt with naive YAML mutation.
|
|
78
|
+
// We install a hermes-facing skill command instead (see skills/oracle-chat).
|
|
79
|
+
const oracleBin = which("oracle") || path.join(PACKAGE_ROOT, "bin", "oracle.mjs");
|
|
80
|
+
const markerDir = path.join(hermesRoot(), "profiles", profile, "oracle");
|
|
81
|
+
ensureDir(markerDir);
|
|
82
|
+
const marker = {
|
|
83
|
+
profile,
|
|
84
|
+
skin: SKIN_NAME,
|
|
85
|
+
oracleBin,
|
|
86
|
+
commands: {
|
|
87
|
+
chain: [oracleBin, "chain"],
|
|
88
|
+
setup: [oracleBin, "setup"],
|
|
89
|
+
},
|
|
90
|
+
updatedAt: new Date().toISOString(),
|
|
91
|
+
};
|
|
92
|
+
fs.writeFileSync(
|
|
93
|
+
path.join(markerDir, "chat-surface.json"),
|
|
94
|
+
JSON.stringify(marker, null, 2) + "\n",
|
|
95
|
+
{ mode: 0o600 },
|
|
96
|
+
);
|
|
97
|
+
return { ok: true, marker };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function ensureSkillCommands(profile = PROFILE) {
|
|
101
|
+
const srcDir = path.join(PACKAGE_ROOT, "skills", "oracle-chat");
|
|
102
|
+
if (!fs.existsSync(srcDir)) return { ok: false, reason: "oracle-chat skill missing" };
|
|
103
|
+
const destRoot = path.join(hermesRoot(), "profiles", profile, "skills", "oracle-chat");
|
|
104
|
+
ensureDir(destRoot);
|
|
105
|
+
for (const name of fs.readdirSync(srcDir)) {
|
|
106
|
+
const from = path.join(srcDir, name);
|
|
107
|
+
const to = path.join(destRoot, name);
|
|
108
|
+
if (fs.statSync(from).isFile()) {
|
|
109
|
+
fs.copyFileSync(from, to);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Also install under profile skills root aliases for /chain and /setup.
|
|
113
|
+
for (const slug of ["chain", "setup"]) {
|
|
114
|
+
const aliasDir = path.join(hermesRoot(), "profiles", profile, "skills", slug);
|
|
115
|
+
ensureDir(aliasDir);
|
|
116
|
+
const skillMd = path.join(srcDir, `${slug}.SKILL.md`);
|
|
117
|
+
const fallback = path.join(srcDir, "SKILL.md");
|
|
118
|
+
const body = fs.existsSync(skillMd)
|
|
119
|
+
? fs.readFileSync(skillMd, "utf8")
|
|
120
|
+
: fs.readFileSync(fallback, "utf8");
|
|
121
|
+
fs.writeFileSync(path.join(aliasDir, "SKILL.md"), body);
|
|
122
|
+
}
|
|
123
|
+
return { ok: true };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function setDisplaySkin(profile = PROFILE) {
|
|
127
|
+
const hermes = which("hermes");
|
|
128
|
+
if (!hermes) return { ok: false, reason: "hermes missing" };
|
|
129
|
+
// hermes config set is safe; avoids hand-editing yaml.
|
|
130
|
+
const r = spawnSync(
|
|
131
|
+
hermes,
|
|
132
|
+
["-p", profile, "config", "set", "display.skin", SKIN_NAME],
|
|
133
|
+
{ encoding: "utf8", timeout: 30_000 },
|
|
134
|
+
);
|
|
135
|
+
if (r.status !== 0) {
|
|
136
|
+
return {
|
|
137
|
+
ok: false,
|
|
138
|
+
reason: (r.stderr || r.stdout || "config set failed").trim(),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return { ok: true };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function usage() {
|
|
145
|
+
return [
|
|
146
|
+
"oracle chat — one lowercase oracle surface",
|
|
147
|
+
"",
|
|
148
|
+
" oracle chat open interactive oracle chat",
|
|
149
|
+
" oracle chat -q \"...\" one-shot query",
|
|
150
|
+
" oracle chat --model <m> pass model to hermes for this session",
|
|
151
|
+
" oracle chat --print-banner show plain ascii banner only",
|
|
152
|
+
"",
|
|
153
|
+
"inside chat:",
|
|
154
|
+
" /chain list/select chains",
|
|
155
|
+
" /setup messaging setup menu",
|
|
156
|
+
" /model switch models (still one oracle persona)",
|
|
157
|
+
"",
|
|
158
|
+
"brand: plain lowercase ascii oracle",
|
|
159
|
+
].join("\n");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function printBanner() {
|
|
163
|
+
const lines = [
|
|
164
|
+
"",
|
|
165
|
+
" _",
|
|
166
|
+
" ___ _ _ __ _ __| |___",
|
|
167
|
+
"/ _ \\ '_/ _` / _| / -_)",
|
|
168
|
+
"\\___/_| \\__,_\\__|_\\___|",
|
|
169
|
+
"",
|
|
170
|
+
];
|
|
171
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export default {
|
|
175
|
+
name: "chat",
|
|
176
|
+
summary: "open the one oracle chat surface (telegram-like, multi-model)",
|
|
177
|
+
group: "read",
|
|
178
|
+
usage: usage(),
|
|
179
|
+
async run(ctx) {
|
|
180
|
+
const args = [...ctx.argv];
|
|
181
|
+
if (args.includes("-h") || args.includes("--help")) {
|
|
182
|
+
process.stdout.write(usage() + "\n");
|
|
183
|
+
return 0;
|
|
184
|
+
}
|
|
185
|
+
if (args.includes("--print-banner")) {
|
|
186
|
+
printBanner();
|
|
187
|
+
return 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const hermes = which("hermes");
|
|
191
|
+
if (!hermes) {
|
|
192
|
+
process.stderr.write(
|
|
193
|
+
"oracle chat needs hermes on PATH\n" +
|
|
194
|
+
"install: https://hermes-agent.nousresearch.com/docs\n",
|
|
195
|
+
);
|
|
196
|
+
return 1;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
ensureOracleSkin(PROFILE);
|
|
200
|
+
ensureQuickCommands(PROFILE);
|
|
201
|
+
ensureSkillCommands(PROFILE);
|
|
202
|
+
const skinSet = setDisplaySkin(PROFILE);
|
|
203
|
+
if (!skinSet.ok && process.env.ORACLE_CLI_DEBUG) {
|
|
204
|
+
process.stderr.write(`oracle chat: skin note: ${skinSet.reason}\n`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Parse a few passthrough flags.
|
|
208
|
+
const hermesArgs = ["-p", PROFILE, "chat"];
|
|
209
|
+
const pass = [];
|
|
210
|
+
for (let i = 0; i < args.length; i++) {
|
|
211
|
+
const a = args[i];
|
|
212
|
+
if (a === "--model" || a === "-m") {
|
|
213
|
+
if (args[i + 1]) {
|
|
214
|
+
pass.push("--model", args[++i]);
|
|
215
|
+
}
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (a === "--provider") {
|
|
219
|
+
if (args[i + 1]) {
|
|
220
|
+
pass.push("--provider", args[++i]);
|
|
221
|
+
}
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (a === "-q" || a === "--query") {
|
|
225
|
+
if (args[i + 1]) {
|
|
226
|
+
pass.push("-q", args[++i]);
|
|
227
|
+
}
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (a === "--quiet" || a === "-Q") {
|
|
231
|
+
pass.push("--quiet");
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (a === "--continue" || a === "-c") {
|
|
235
|
+
pass.push("--continue");
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
// unknown flags go through
|
|
239
|
+
pass.push(a);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// hermes -p scopes the same profile used by Telegram. Do not replace
|
|
243
|
+
// HERMES_HOME with the profile dir; nested profile resolution would break.
|
|
244
|
+
const cleanEnv = activeChainEnv({
|
|
245
|
+
ORACLE_CHAT_SURFACE: "1",
|
|
246
|
+
ORACLE_PROFILE: PROFILE,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const harness = harnessSourcePath();
|
|
250
|
+
const runtime = process.env.ORACLE_PLAIN_HARNESS === "0"
|
|
251
|
+
? null
|
|
252
|
+
: resolveHermesPython(hermes);
|
|
253
|
+
const command = runtime && fs.existsSync(harness) ? runtime.command : hermes;
|
|
254
|
+
const launchArgs = runtime && fs.existsSync(harness)
|
|
255
|
+
? [...runtime.prefix, harness, ...hermesArgs, ...pass]
|
|
256
|
+
: [...hermesArgs, ...pass];
|
|
257
|
+
|
|
258
|
+
return spawnChild(
|
|
259
|
+
command,
|
|
260
|
+
launchArgs,
|
|
261
|
+
{
|
|
262
|
+
stdio: "inherit",
|
|
263
|
+
env: cleanEnv,
|
|
264
|
+
},
|
|
265
|
+
"oracle-chat",
|
|
266
|
+
);
|
|
267
|
+
},
|
|
268
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawnChild } from "../spawn-child.mjs";
|
|
4
|
+
|
|
5
|
+
function hermesBin() {
|
|
6
|
+
if (process.env.ORACLE_HERMES_BIN) return process.env.ORACLE_HERMES_BIN;
|
|
7
|
+
for (const dir of (process.env.PATH || "").split(path.delimiter)) {
|
|
8
|
+
const candidate = path.join(dir, "hermes");
|
|
9
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default {
|
|
15
|
+
name: "model",
|
|
16
|
+
summary: "choose the oracle profile's model/provider",
|
|
17
|
+
group: "read",
|
|
18
|
+
usage: [
|
|
19
|
+
"oracle model — interactive provider/model picker for the oracle profile",
|
|
20
|
+
"",
|
|
21
|
+
"inside chat: /model",
|
|
22
|
+
"one oracle persona stays loaded when the model changes.",
|
|
23
|
+
].join("\n"),
|
|
24
|
+
async run(ctx) {
|
|
25
|
+
const hermes = hermesBin();
|
|
26
|
+
if (!hermes) {
|
|
27
|
+
process.stderr.write("oracle model needs hermes on PATH\n");
|
|
28
|
+
return 1;
|
|
29
|
+
}
|
|
30
|
+
return spawnChild(
|
|
31
|
+
hermes,
|
|
32
|
+
["-p", "oracle", "model", ...ctx.argv],
|
|
33
|
+
{ stdio: "inherit", env: process.env },
|
|
34
|
+
"oracle-model",
|
|
35
|
+
);
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import readline from "node:readline/promises";
|
|
5
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
6
|
+
import {
|
|
7
|
+
findMessagingPlatform,
|
|
8
|
+
listMessagingPlatforms,
|
|
9
|
+
renderSetupMenu,
|
|
10
|
+
} from "../messaging-platforms.mjs";
|
|
11
|
+
import {
|
|
12
|
+
platformStatus,
|
|
13
|
+
renderStatus,
|
|
14
|
+
setPlatformToken,
|
|
15
|
+
touchSetupState,
|
|
16
|
+
} from "../setup-state.mjs";
|
|
17
|
+
import { hermesRoot } from "../paths.mjs";
|
|
18
|
+
|
|
19
|
+
function which(bin) {
|
|
20
|
+
for (const dir of (process.env.PATH || "").split(path.delimiter)) {
|
|
21
|
+
const c = path.join(dir, bin);
|
|
22
|
+
try {
|
|
23
|
+
if (fs.existsSync(c)) return c;
|
|
24
|
+
} catch {}
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function usage() {
|
|
30
|
+
return [
|
|
31
|
+
"oracle setup — wire messaging + local agent surface",
|
|
32
|
+
"",
|
|
33
|
+
" oracle setup menu + platform status",
|
|
34
|
+
" oracle setup status same, machine-friendly with --json",
|
|
35
|
+
" oracle setup messaging open hermes gateway setup wizard",
|
|
36
|
+
" oracle setup telegram set TELEGRAM_BOT_TOKEN for profile",
|
|
37
|
+
" oracle setup discord set DISCORD_BOT_TOKEN for profile",
|
|
38
|
+
" oracle setup slack set slack tokens",
|
|
39
|
+
" oracle setup <platform> any supported hermes platform",
|
|
40
|
+
" oracle setup gateway [status|restart|start|stop]",
|
|
41
|
+
" oracle setup profile [name] default: oracle",
|
|
42
|
+
"",
|
|
43
|
+
"chat:",
|
|
44
|
+
" /setup",
|
|
45
|
+
" /setup telegram",
|
|
46
|
+
" /setup status",
|
|
47
|
+
"",
|
|
48
|
+
"tokens stay local in ~/.hermes/profiles/<profile>/.env",
|
|
49
|
+
"oracle never prints secrets.",
|
|
50
|
+
].join("\n");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function profileFromArgs(args) {
|
|
54
|
+
const i = args.indexOf("--profile");
|
|
55
|
+
if (i >= 0 && args[i + 1]) return args[i + 1];
|
|
56
|
+
const j = args.indexOf("-p");
|
|
57
|
+
if (j >= 0 && args[j + 1]) return args[j + 1];
|
|
58
|
+
return process.env.ORACLE_PROFILE || "oracle";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function stripProfileArgs(args) {
|
|
62
|
+
const out = [];
|
|
63
|
+
for (let i = 0; i < args.length; i++) {
|
|
64
|
+
if (args[i] === "--profile" || args[i] === "-p") {
|
|
65
|
+
i += 1;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
out.push(args[i]);
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function runHermes(argv, { inherit = true, env = process.env } = {}) {
|
|
74
|
+
const hermes = which("hermes");
|
|
75
|
+
if (!hermes) {
|
|
76
|
+
process.stderr.write(
|
|
77
|
+
"oracle setup: hermes not on PATH\ninstall hermes, then re-run\n",
|
|
78
|
+
);
|
|
79
|
+
return 1;
|
|
80
|
+
}
|
|
81
|
+
const r = spawnSync(hermes, argv, {
|
|
82
|
+
stdio: inherit ? "inherit" : "pipe",
|
|
83
|
+
encoding: "utf8",
|
|
84
|
+
env,
|
|
85
|
+
});
|
|
86
|
+
if (r.error) {
|
|
87
|
+
process.stderr.write(`oracle setup: failed to run hermes: ${r.error.message}\n`);
|
|
88
|
+
return 1;
|
|
89
|
+
}
|
|
90
|
+
if (!inherit) {
|
|
91
|
+
if (r.stdout) process.stdout.write(r.stdout);
|
|
92
|
+
if (r.stderr) process.stderr.write(r.stderr);
|
|
93
|
+
}
|
|
94
|
+
return typeof r.status === "number" ? r.status : 1;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function promptSecret(label) {
|
|
98
|
+
if (!process.stdin.isTTY) {
|
|
99
|
+
const err = new Error("interactive token entry requires a tty");
|
|
100
|
+
err.code = "NO_TTY";
|
|
101
|
+
throw err;
|
|
102
|
+
}
|
|
103
|
+
const rl = readline.createInterface({ input, output });
|
|
104
|
+
try {
|
|
105
|
+
// readline cannot hide input portably without raw mode; still avoid echo by
|
|
106
|
+
// asking user to paste and not logging it ourselves.
|
|
107
|
+
const value = (await rl.question(`${label}: `)).trim();
|
|
108
|
+
return value;
|
|
109
|
+
} finally {
|
|
110
|
+
rl.close();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function configurePlatform(profile, platformKey, inlineArgs = []) {
|
|
115
|
+
const platform = findMessagingPlatform(platformKey);
|
|
116
|
+
if (!platform) {
|
|
117
|
+
process.stderr.write(`oracle setup: unknown platform '${platformKey}'\n`);
|
|
118
|
+
process.stderr.write(
|
|
119
|
+
`known: ${listMessagingPlatforms()
|
|
120
|
+
.map((p) => p.key)
|
|
121
|
+
.join(", ")}\n`,
|
|
122
|
+
);
|
|
123
|
+
return 1;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Platforms without simple token keys go through Hermes interactive setup.
|
|
127
|
+
if (!platform.required.length) {
|
|
128
|
+
process.stdout.write(
|
|
129
|
+
`opening hermes setup for ${platform.key} (interactive)\n`,
|
|
130
|
+
);
|
|
131
|
+
touchSetupState({ lastPlatform: platform.key, profile });
|
|
132
|
+
return runHermes(["-p", profile, ...(platform.hermes || ["gateway", "setup"])]);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const flags = {};
|
|
136
|
+
const positionals = [];
|
|
137
|
+
for (let i = 0; i < inlineArgs.length; i++) {
|
|
138
|
+
const a = inlineArgs[i];
|
|
139
|
+
if (a === "--token" && inlineArgs[i + 1]) {
|
|
140
|
+
flags.token = inlineArgs[++i];
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (a === "--allowed-users" && inlineArgs[i + 1]) {
|
|
144
|
+
flags.allowedUsers = inlineArgs[++i];
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (a === "--app-token" && inlineArgs[i + 1]) {
|
|
148
|
+
flags.appToken = inlineArgs[++i];
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (a.startsWith("-")) {
|
|
152
|
+
process.stderr.write(`oracle setup: unknown flag ${a}\n`);
|
|
153
|
+
return 1;
|
|
154
|
+
}
|
|
155
|
+
positionals.push(a);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let token = flags.token || positionals[0] || "";
|
|
159
|
+
if (!token) {
|
|
160
|
+
try {
|
|
161
|
+
token = await promptSecret(`${platform.key} bot token`);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
if (err?.code === "NO_TTY") {
|
|
164
|
+
process.stderr.write(
|
|
165
|
+
`oracle setup ${platform.key}: pass --token <value> in non-interactive mode\n`,
|
|
166
|
+
);
|
|
167
|
+
return 1;
|
|
168
|
+
}
|
|
169
|
+
throw err;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (!token) {
|
|
173
|
+
process.stderr.write("aborted: empty token\n");
|
|
174
|
+
return 1;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const extra = {};
|
|
178
|
+
if (flags.allowedUsers) {
|
|
179
|
+
if (platform.key === "telegram") extra.TELEGRAM_ALLOWED_USERS = flags.allowedUsers;
|
|
180
|
+
if (platform.key === "discord") extra.DISCORD_ALLOWED_USERS = flags.allowedUsers;
|
|
181
|
+
if (platform.key === "slack") extra.SLACK_ALLOWED_USERS = flags.allowedUsers;
|
|
182
|
+
}
|
|
183
|
+
if (flags.appToken && platform.key === "slack") {
|
|
184
|
+
extra.SLACK_APP_TOKEN = flags.appToken;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Slack needs two tokens.
|
|
188
|
+
if (platform.key === "slack" && !extra.SLACK_APP_TOKEN) {
|
|
189
|
+
let app = flags.appToken || positionals[1] || "";
|
|
190
|
+
if (!app && process.stdin.isTTY) {
|
|
191
|
+
app = await promptSecret("slack app token (xapp-...)");
|
|
192
|
+
}
|
|
193
|
+
if (!app) {
|
|
194
|
+
process.stderr.write("slack needs bot token + app token\n");
|
|
195
|
+
return 1;
|
|
196
|
+
}
|
|
197
|
+
extra.SLACK_APP_TOKEN = app;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
const result = setPlatformToken(profile, platform.key, token, extra);
|
|
202
|
+
touchSetupState({ lastPlatform: platform.key, profile, ready: true });
|
|
203
|
+
process.stdout.write(
|
|
204
|
+
`saved ${platform.key} credentials for profile '${profile}'\n` +
|
|
205
|
+
`path: ${result.path}\n` +
|
|
206
|
+
`next: oracle setup gateway restart\n`,
|
|
207
|
+
);
|
|
208
|
+
return 0;
|
|
209
|
+
} catch (err) {
|
|
210
|
+
process.stderr.write(`oracle setup: ${err.message}\n`);
|
|
211
|
+
return 1;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export default {
|
|
216
|
+
name: "setup",
|
|
217
|
+
summary: "configure telegram/discord/slack and other hermes messaging platforms",
|
|
218
|
+
group: "read",
|
|
219
|
+
usage: usage(),
|
|
220
|
+
async run(ctx) {
|
|
221
|
+
const args = [...ctx.argv];
|
|
222
|
+
const json = args.includes("--json");
|
|
223
|
+
const profile = profileFromArgs(args);
|
|
224
|
+
const clean = stripProfileArgs(args).filter((a) => a !== "--json");
|
|
225
|
+
const verb = (clean[0] || "menu").toLowerCase();
|
|
226
|
+
const rest = clean.slice(1);
|
|
227
|
+
|
|
228
|
+
if (["-h", "--help", "help"].includes(verb)) {
|
|
229
|
+
process.stdout.write(usage() + "\n");
|
|
230
|
+
return 0;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (["menu", "list", "ls"].includes(verb)) {
|
|
234
|
+
if (json) {
|
|
235
|
+
process.stdout.write(JSON.stringify(platformStatus(profile), null, 2) + "\n");
|
|
236
|
+
} else {
|
|
237
|
+
process.stdout.write(renderStatus(profile));
|
|
238
|
+
}
|
|
239
|
+
return 0;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (["status", "show"].includes(verb)) {
|
|
243
|
+
const st = platformStatus(profile);
|
|
244
|
+
if (json) {
|
|
245
|
+
process.stdout.write(JSON.stringify(st, null, 2) + "\n");
|
|
246
|
+
} else {
|
|
247
|
+
process.stdout.write(renderStatus(profile));
|
|
248
|
+
process.stdout.write(`hermes root: ${hermesRoot()}\n`);
|
|
249
|
+
}
|
|
250
|
+
return 0;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (["messaging", "gateway-setup", "platforms"].includes(verb)) {
|
|
254
|
+
touchSetupState({ lastAction: "messaging", profile });
|
|
255
|
+
process.stdout.write("opening hermes gateway setup wizard...\n");
|
|
256
|
+
return runHermes(["-p", profile, "gateway", "setup"]);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (verb === "gateway") {
|
|
260
|
+
const action = (rest[0] || "status").toLowerCase();
|
|
261
|
+
if (!["status", "start", "stop", "restart", "run", "install"].includes(action)) {
|
|
262
|
+
process.stderr.write("usage: oracle setup gateway [status|start|stop|restart|run|install]\n");
|
|
263
|
+
return 1;
|
|
264
|
+
}
|
|
265
|
+
return runHermes(["-p", profile, "gateway", action]);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (verb === "profile") {
|
|
269
|
+
const name = rest[0] || profile;
|
|
270
|
+
process.stdout.write(`profile: ${name}\nhermes: ${hermesRoot()}/profiles/${name}\n`);
|
|
271
|
+
process.stdout.write(renderStatus(name));
|
|
272
|
+
return 0;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// oracle setup telegram|discord|...
|
|
276
|
+
if (findMessagingPlatform(verb)) {
|
|
277
|
+
return configurePlatform(profile, verb, rest);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
process.stderr.write(`oracle setup: unknown verb '${verb}'\n${usage()}\n`);
|
|
281
|
+
return 1;
|
|
282
|
+
},
|
|
283
|
+
};
|
package/src/cli/first-run.mjs
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export const NEXT_STEPS_AFTER_INIT = [
|
|
2
2
|
"oracle data serve # loopback read/prepare plane on 127.0.0.1:8787",
|
|
3
3
|
"oracle doctor # verify",
|
|
4
|
+
"oracle chat # one lowercase oracle surface",
|
|
5
|
+
"oracle chain list # pick hyperliquid/base/solana/...",
|
|
6
|
+
"oracle setup # telegram/discord/slack + hermes messaging",
|
|
4
7
|
"oracle mcp install claude-code",
|
|
5
8
|
"# optional, explicit second step — signing stays a separate local package:",
|
|
6
9
|
"npm i -g @oracle-agent/operator",
|
package/src/cli/kernel.mjs
CHANGED
|
@@ -9,11 +9,16 @@ import {
|
|
|
9
9
|
SIGN_NOUNS,
|
|
10
10
|
} from "./operator-dispatch.mjs";
|
|
11
11
|
|
|
12
|
-
const HELP_HEADER = `oracle —
|
|
12
|
+
const HELP_HEADER = `oracle — self-custody multichain agent control plane`;
|
|
13
13
|
|
|
14
14
|
const STATIC_HELP = `READ / RESEARCH (no keys, this package)
|
|
15
|
+
oracle open the plain oracle chat harness (TTY)
|
|
15
16
|
oracle init install agent lanes + read plane (dry-run by default)
|
|
16
17
|
oracle doctor check read plane; checks signer too when installed
|
|
18
|
+
oracle chat one lowercase oracle chat surface (multi-model)
|
|
19
|
+
oracle model choose the profile's default model/provider
|
|
20
|
+
oracle chain list/select working chains (hyperliquid, base, ...)
|
|
21
|
+
oracle setup telegram/discord/slack + hermes messaging menu
|
|
17
22
|
oracle data serve|call|catalog|health
|
|
18
23
|
oracle data-mcp start the read-only MCP server
|
|
19
24
|
oracle scan chains|head|token|pools|risk|quote|sell
|
|
@@ -85,7 +90,7 @@ export function renderHelp(version, commands) {
|
|
|
85
90
|
for (const cmd of commands.values()) {
|
|
86
91
|
if (["help", "version"].includes(cmd.name)) continue;
|
|
87
92
|
const known = new Set([
|
|
88
|
-
"init","doctor","data","data-mcp","scan","route","prepare","public","mcp","upgrade",
|
|
93
|
+
"init","doctor","chat","model","chain","setup","data","data-mcp","scan","route","prepare","public","mcp","upgrade",
|
|
89
94
|
"sign","vault","signer","runner","credential",
|
|
90
95
|
]);
|
|
91
96
|
if (!known.has(cmd.name) && cmd.summary) {
|
|
@@ -122,12 +127,28 @@ export async function run(argv = []) {
|
|
|
122
127
|
if (argv.includes("--version") || argv.includes("-V")) {
|
|
123
128
|
return runVersion();
|
|
124
129
|
}
|
|
125
|
-
if (argv
|
|
130
|
+
if (argv[0] === "--help" || argv[0] === "-h") {
|
|
126
131
|
const commands = await discoverCommands();
|
|
127
132
|
process.stdout.write(renderHelp(version, commands));
|
|
128
133
|
return 0;
|
|
129
134
|
}
|
|
130
135
|
|
|
136
|
+
if (argv.length === 0) {
|
|
137
|
+
const commands = await discoverCommands();
|
|
138
|
+
if (shouldLaunchChat(argv)) {
|
|
139
|
+
const chat = commands.get("chat");
|
|
140
|
+
if (chat) return chat.run(buildCtx([]));
|
|
141
|
+
}
|
|
142
|
+
process.stdout.write(renderHelp(version, commands));
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (isRootChatFlag(argv[0])) {
|
|
147
|
+
const commands = await discoverCommands();
|
|
148
|
+
const chat = commands.get("chat");
|
|
149
|
+
if (chat) return chat.run(buildCtx(argv));
|
|
150
|
+
}
|
|
151
|
+
|
|
131
152
|
const noun = argv[0];
|
|
132
153
|
const rest = argv.slice(1);
|
|
133
154
|
|
|
@@ -196,3 +217,21 @@ async function runVersion() {
|
|
|
196
217
|
}
|
|
197
218
|
|
|
198
219
|
export default { run, discoverCommands, renderHelp };
|
|
220
|
+
|
|
221
|
+
export function shouldLaunchChat(
|
|
222
|
+
argv = [],
|
|
223
|
+
io = { stdin: process.stdin, stdout: process.stdout },
|
|
224
|
+
) {
|
|
225
|
+
if (argv.length !== 0) return false;
|
|
226
|
+
if (process.env.ORACLE_FORCE_CHAT === "1") return true;
|
|
227
|
+
return io.stdin?.isTTY === true && io.stdout?.isTTY === true;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function isRootChatFlag(value) {
|
|
231
|
+
return [
|
|
232
|
+
"-m", "--model", "--provider", "-q", "--query", "-Q", "--quiet",
|
|
233
|
+
"-c", "--continue", "-r", "--resume", "-s", "--skills", "-t",
|
|
234
|
+
"--toolsets", "--reasoning", "--tui", "--cli", "--yolo",
|
|
235
|
+
"--checkpoints", "--source", "--max-turns",
|
|
236
|
+
].includes(String(value || ""));
|
|
237
|
+
}
|