@oracle-agent/oracle 0.9.8 → 0.11.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 (160) hide show
  1. package/README.md +15 -10
  2. package/bin/oracle-data-mcp.mjs +9 -0
  3. package/docs/architecture.md +3 -1
  4. package/docs/profiles.md +3 -6
  5. package/docs/public-surface.md +133 -0
  6. package/package.json +3 -2
  7. package/public/oracle-splash/assets/cli-chain-hyperliquid.jpg +0 -0
  8. package/public/oracle-splash/brand/aave.webp +0 -0
  9. package/public/oracle-splash/brand/aavev2.webp +0 -0
  10. package/public/oracle-splash/brand/aborean.webp +0 -0
  11. package/public/oracle-splash/brand/across.webp +0 -0
  12. package/public/oracle-splash/brand/aerodrome.webp +0 -0
  13. package/public/oracle-splash/brand/afi.webp +0 -0
  14. package/public/oracle-splash/brand/arcus.webp +0 -0
  15. package/public/oracle-splash/brand/aster.webp +0 -0
  16. package/public/oracle-splash/brand/avant.webp +0 -0
  17. package/public/oracle-splash/brand/balancer.webp +0 -0
  18. package/public/oracle-splash/brand/beefy.webp +0 -0
  19. package/public/oracle-splash/brand/benqi.webp +0 -0
  20. package/public/oracle-splash/brand/bestinslot.ico +0 -0
  21. package/public/oracle-splash/brand/bitfi.webp +0 -0
  22. package/public/oracle-splash/brand/bonkswap.webp +0 -0
  23. package/public/oracle-splash/brand/bouncebit.webp +0 -0
  24. package/public/oracle-splash/brand/camelot.webp +0 -0
  25. package/public/oracle-splash/brand/compound.webp +0 -0
  26. package/public/oracle-splash/brand/concrete.webp +0 -0
  27. package/public/oracle-splash/brand/cow.webp +0 -0
  28. package/public/oracle-splash/brand/curve.webp +0 -0
  29. package/public/oracle-splash/brand/derive.webp +0 -0
  30. package/public/oracle-splash/brand/dolomite.webp +0 -0
  31. package/public/oracle-splash/brand/eigenlayer.webp +0 -0
  32. package/public/oracle-splash/brand/ekubo.webp +0 -0
  33. package/public/oracle-splash/brand/ethena.webp +0 -0
  34. package/public/oracle-splash/brand/etherfi.webp +0 -0
  35. package/public/oracle-splash/brand/etherfiborrow.webp +0 -0
  36. package/public/oracle-splash/brand/felix.webp +0 -0
  37. package/public/oracle-splash/brand/fluid.webp +0 -0
  38. package/public/oracle-splash/brand/fluxbeam.webp +0 -0
  39. package/public/oracle-splash/brand/folks.webp +0 -0
  40. package/public/oracle-splash/brand/fortyacres.webp +0 -0
  41. package/public/oracle-splash/brand/gainsnetwork.webp +0 -0
  42. package/public/oracle-splash/brand/gassss.webp +0 -0
  43. package/public/oracle-splash/brand/gmx.webp +0 -0
  44. package/public/oracle-splash/brand/harmonix.webp +0 -0
  45. package/public/oracle-splash/brand/hlorderbook.webp +0 -0
  46. package/public/oracle-splash/brand/hlp.webp +0 -0
  47. package/public/oracle-splash/brand/hop.webp +0 -0
  48. package/public/oracle-splash/brand/hyperbeat.webp +0 -0
  49. package/public/oracle-splash/brand/hyperlend.webp +0 -0
  50. package/public/oracle-splash/brand/hyperliquid.webp +0 -0
  51. package/public/oracle-splash/brand/hyperliquidhpl.webp +0 -0
  52. package/public/oracle-splash/brand/hyperliquidspot.webp +0 -0
  53. package/public/oracle-splash/brand/hyperswap.webp +0 -0
  54. package/public/oracle-splash/brand/hypha.webp +0 -0
  55. package/public/oracle-splash/brand/joedex.webp +0 -0
  56. package/public/oracle-splash/brand/jupiter.webp +0 -0
  57. package/public/oracle-splash/brand/kelp.webp +0 -0
  58. package/public/oracle-splash/brand/kinetiq.webp +0 -0
  59. package/public/oracle-splash/brand/kmhype.webp +0 -0
  60. package/public/oracle-splash/brand/kona.webp +0 -0
  61. package/public/oracle-splash/brand/konalend.webp +0 -0
  62. package/public/oracle-splash/brand/lfj.webp +0 -0
  63. package/public/oracle-splash/brand/lido.webp +0 -0
  64. package/public/oracle-splash/brand/lifi.webp +0 -0
  65. package/public/oracle-splash/brand/lighter.webp +0 -0
  66. package/public/oracle-splash/brand/lista.webp +0 -0
  67. package/public/oracle-splash/brand/loopedhype.webp +0 -0
  68. package/public/oracle-splash/brand/lorenzo.webp +0 -0
  69. package/public/oracle-splash/brand/magic-eden.webp +0 -0
  70. package/public/oracle-splash/brand/magiceden.webp +0 -0
  71. package/public/oracle-splash/brand/mempool.png +0 -0
  72. package/public/oracle-splash/brand/meteora.webp +0 -0
  73. package/public/oracle-splash/brand/morpho.webp +0 -0
  74. package/public/oracle-splash/brand/myriad.webp +0 -0
  75. package/public/oracle-splash/brand/noxa.webp +0 -0
  76. package/public/oracle-splash/brand/odos.webp +0 -0
  77. package/public/oracle-splash/brand/oneinch.webp +0 -0
  78. package/public/oracle-splash/brand/openbook.webp +0 -0
  79. package/public/oracle-splash/brand/opensea.webp +0 -0
  80. package/public/oracle-splash/brand/orca.webp +0 -0
  81. package/public/oracle-splash/brand/ordinals.png +0 -0
  82. package/public/oracle-splash/brand/ostium.webp +0 -0
  83. package/public/oracle-splash/brand/pancakeswap.webp +0 -0
  84. package/public/oracle-splash/brand/pendle.webp +0 -0
  85. package/public/oracle-splash/brand/perena.webp +0 -0
  86. package/public/oracle-splash/brand/pharaoh.webp +0 -0
  87. package/public/oracle-splash/brand/phoenix.webp +0 -0
  88. package/public/oracle-splash/brand/polymarket.webp +0 -0
  89. package/public/oracle-splash/brand/polynomial.webp +0 -0
  90. package/public/oracle-splash/brand/projectx.webp +0 -0
  91. package/public/oracle-splash/brand/pumpfun.webp +0 -0
  92. package/public/oracle-splash/brand/quickswap.webp +0 -0
  93. package/public/oracle-splash/brand/rain.webp +0 -0
  94. package/public/oracle-splash/brand/raydium.webp +0 -0
  95. package/public/oracle-splash/brand/relay.webp +0 -0
  96. package/public/oracle-splash/brand/rocketpool.webp +0 -0
  97. package/public/oracle-splash/brand/saber.webp +0 -0
  98. package/public/oracle-splash/brand/sakuraswap.webp +0 -0
  99. package/public/oracle-splash/brand/sanctum.webp +0 -0
  100. package/public/oracle-splash/brand/satflow.png +0 -0
  101. package/public/oracle-splash/brand/sierra.webp +0 -0
  102. package/public/oracle-splash/brand/sky.webp +0 -0
  103. package/public/oracle-splash/brand/solv.webp +0 -0
  104. package/public/oracle-splash/brand/spark.webp +0 -0
  105. package/public/oracle-splash/brand/sparksavings.webp +0 -0
  106. package/public/oracle-splash/brand/sphere.webp +0 -0
  107. package/public/oracle-splash/brand/ssv.webp +0 -0
  108. package/public/oracle-splash/brand/stabble.webp +0 -0
  109. package/public/oracle-splash/brand/stableswap.webp +0 -0
  110. package/public/oracle-splash/brand/stakewise.webp +0 -0
  111. package/public/oracle-splash/brand/stargate.webp +0 -0
  112. package/public/oracle-splash/brand/sthype.webp +0 -0
  113. package/public/oracle-splash/brand/sushiswap.webp +0 -0
  114. package/public/oracle-splash/brand/unisat.png +0 -0
  115. package/public/oracle-splash/brand/uniswap.webp +0 -0
  116. package/public/oracle-splash/brand/uniswapv2.webp +0 -0
  117. package/public/oracle-splash/brand/uniswapv4.webp +0 -0
  118. package/public/oracle-splash/brand/unit.webp +0 -0
  119. package/public/oracle-splash/brand/velodrome.webp +0 -0
  120. package/public/oracle-splash/brand/velora.webp +0 -0
  121. package/public/oracle-splash/brand/venus.webp +0 -0
  122. package/public/oracle-splash/brand/wbeth.webp +0 -0
  123. package/public/oracle-splash/brand/xsy.webp +0 -0
  124. package/public/oracle-splash/brand/yieldyak.webp +0 -0
  125. package/public/oracle-splash/brand/zerobase.webp +0 -0
  126. package/public/oracle-splash/brand/zerox.webp +0 -0
  127. package/public/oracle-splash/index.html +337 -197
  128. package/scripts/check-doc-drift.mjs +11 -0
  129. package/scripts/e2e-solana-bitcoin.mjs +1 -1
  130. package/scripts/secret-scan.mjs +1 -1
  131. package/src/action-receipts.mjs +108 -11
  132. package/src/auth/oauth.mjs +672 -0
  133. package/src/cli/commands/auth.mjs +193 -0
  134. package/src/cli/commands/bootstrap.mjs +2 -2
  135. package/src/cli/commands/chat.mjs +97 -75
  136. package/src/cli/commands/data.mjs +3 -1
  137. package/src/cli/commands/doctor.mjs +11 -1
  138. package/src/cli/commands/model.mjs +79 -23
  139. package/src/cli/kernel.mjs +4 -3
  140. package/src/cli/model-config.mjs +70 -0
  141. package/src/data/catalog.mjs +38 -7
  142. package/src/data/desk-data.mjs +12 -0
  143. package/src/data/providers/evm-rpc.mjs +19 -16
  144. package/src/data/providers/jupiter-venues.mjs +119 -0
  145. package/src/data/providers/jupiter.mjs +132 -0
  146. package/src/data/providers/magiceden-sol.mjs +13 -0
  147. package/src/data/providers/morpho.mjs +6 -2
  148. package/src/data/providers/pendle.mjs +3 -2
  149. package/src/data/providers/satflow.mjs +4 -1
  150. package/src/data/providers/signed-material-guard.mjs +108 -0
  151. package/src/data/providers/uniswap-v3.mjs +34 -5
  152. package/src/data/providers/uniswap-v4.mjs +414 -0
  153. package/src/prepare-envelope.mjs +22 -4
  154. package/src/router/prepare-route.mjs +17 -0
  155. package/src/router/route-sources.mjs +35 -0
  156. package/src/tui/app.mjs +18 -7
  157. package/src/tui/backend.mjs +145 -0
  158. package/src/tui/standalone-client.mjs +666 -0
  159. package/src/cards.mjs +0 -369
  160. package/src/cli/__pycache__/oracle-harness.cpython-311.pyc +0 -0
@@ -0,0 +1,193 @@
1
+ import { writeModelConfig } from "../model-config.mjs";
2
+ import {
3
+ getOAuthStore,
4
+ loginOAuth,
5
+ oauthProviderConfig,
6
+ oauthProviderId,
7
+ } from "../../auth/oauth.mjs";
8
+
9
+ const API_PROVIDERS = Object.freeze({
10
+ openrouter: { model: "openrouter/auto" },
11
+ openai: { model: "gpt-4.1-mini" },
12
+ xai: { model: "grok-4.5" },
13
+ deepseek: { model: "deepseek-chat" },
14
+ gemini: { model: "gemini-2.5-flash" },
15
+ custom: { model: "" },
16
+ });
17
+
18
+ function option(args, name) {
19
+ const index = args.indexOf(name);
20
+ return index >= 0 && args[index + 1] ? args[index + 1] : "";
21
+ }
22
+
23
+ async function readAllStdin() {
24
+ let value = "";
25
+ for await (const chunk of process.stdin) value += chunk;
26
+ return value.trim();
27
+ }
28
+
29
+ function readHidden(prompt = "API key: ") {
30
+ if (!process.stdin.isTTY || !process.stdin.setRawMode) {
31
+ throw new Error("non-interactive API-key input requires --stdin");
32
+ }
33
+ process.stdout.write(prompt);
34
+ return new Promise((resolve, reject) => {
35
+ let value = "";
36
+ const cleanup = () => {
37
+ process.stdin.off("data", onData);
38
+ process.stdin.setRawMode(false);
39
+ process.stdin.pause();
40
+ process.stdout.write("\n");
41
+ };
42
+ const onData = (chunk) => {
43
+ const text = String(chunk);
44
+ for (const character of text) {
45
+ if (character === "\r" || character === "\n") {
46
+ cleanup();
47
+ resolve(value.trim());
48
+ return;
49
+ }
50
+ if (character === "\u0003") {
51
+ cleanup();
52
+ reject(new Error("cancelled"));
53
+ return;
54
+ }
55
+ if (character === "\u007f" || character === "\b") value = value.slice(0, -1);
56
+ else value += character;
57
+ }
58
+ };
59
+ process.stdin.setRawMode(true);
60
+ process.stdin.resume();
61
+ process.stdin.on("data", onData);
62
+ });
63
+ }
64
+
65
+ function selection(provider, model, { baseUrl = "" } = {}) {
66
+ if (!model) throw new Error(`${provider} provider requires --model`);
67
+ const config = { backend: "standalone", provider, model };
68
+ if (provider === "custom") {
69
+ if (!baseUrl) throw new Error("custom provider requires --base-url");
70
+ config.baseUrl = baseUrl;
71
+ }
72
+ return config;
73
+ }
74
+
75
+ function saveSelection(provider, model, options = {}) {
76
+ return writeModelConfig(selection(provider, model, options));
77
+ }
78
+
79
+ function printStatus(status) {
80
+ process.stdout.write(`storage: ${status.storage}\n`);
81
+ const oauth = Object.entries(status.providers || {});
82
+ const apiKeys = Object.entries(status.apiKeys || {});
83
+ if (!oauth.length && !apiKeys.length) {
84
+ process.stdout.write("no credentials configured\n");
85
+ return;
86
+ }
87
+ for (const [provider, value] of oauth) {
88
+ const expiry = value.expiresAt ? new Date(value.expiresAt).toISOString() : "unknown";
89
+ process.stdout.write(`${provider}: ${value.loggedIn ? "logged in" : "missing"}, expires ${expiry}\n`);
90
+ }
91
+ for (const [provider, value] of apiKeys) {
92
+ process.stdout.write(`${provider}: API key ${value.configured ? "configured" : "missing"}\n`);
93
+ }
94
+ }
95
+
96
+ function usage() {
97
+ return [
98
+ "oracle auth, configure OAuth or API-key authentication",
99
+ "",
100
+ " oracle auth login claude|codex|grok [--model MODEL] [--no-browser]",
101
+ " oracle auth api-key PROVIDER [--model MODEL] [--base-url URL]",
102
+ " printf '%s\\n' \"$KEY\" | oracle auth api-key PROVIDER --stdin",
103
+ " oracle auth status [--json]",
104
+ " oracle auth logout PROVIDER",
105
+ "",
106
+ "OAuth and API keys are additive. Select any compatible model with oracle model --provider PROVIDER --model MODEL.",
107
+ ].join("\n");
108
+ }
109
+
110
+ export default {
111
+ name: "auth",
112
+ summary: "login with Claude, Codex, or Grok OAuth; store provider API keys",
113
+ group: "read",
114
+ usage: usage(),
115
+ async run(ctx) {
116
+ const args = [...ctx.argv];
117
+ const action = args.shift() || "status";
118
+ const store = getOAuthStore();
119
+ try {
120
+ if (action === "status") {
121
+ const status = store.status();
122
+ if (args.includes("--json")) process.stdout.write(`${JSON.stringify(status, null, 2)}\n`);
123
+ else printStatus(status);
124
+ return 0;
125
+ }
126
+
127
+ if (action === "login") {
128
+ const requested = args.find((value) => !value.startsWith("-"));
129
+ if (!requested) throw new Error("usage: oracle auth login claude|codex|grok");
130
+ const config = oauthProviderConfig(requested);
131
+ const model = option(args, "--model") || config.model;
132
+ selection(config.id, model);
133
+ const credential = await loginOAuth(config.id, {
134
+ openFn: args.includes("--no-browser") ? () => false : undefined,
135
+ });
136
+ await store.withLock(async () => {
137
+ const previous = store.get(config.id);
138
+ store.set(config.id, credential);
139
+ try {
140
+ saveSelection(config.id, model);
141
+ } catch (error) {
142
+ if (previous) store.set(config.id, previous);
143
+ else store.remove(config.id);
144
+ throw error;
145
+ }
146
+ });
147
+ process.stdout.write(`${config.id}: logged in, stored in ${store.storage}\n`);
148
+ return 0;
149
+ }
150
+
151
+ if (action === "api-key") {
152
+ const provider = String(args.find((value) => !value.startsWith("-")) || "").trim().toLowerCase();
153
+ if (!provider) throw new Error("usage: oracle auth api-key PROVIDER [--stdin]");
154
+ if (!Object.hasOwn(API_PROVIDERS, provider)) throw new Error(`unsupported API-key provider: ${provider}`);
155
+ const model = option(args, "--model") || API_PROVIDERS[provider].model;
156
+ const baseUrl = option(args, "--base-url");
157
+ selection(provider, model, { baseUrl });
158
+ const apiKey = args.includes("--stdin") ? await readAllStdin() : await readHidden();
159
+ await store.withLock(async () => {
160
+ const previous = store.getApiKey(provider);
161
+ store.setApiKey(provider, apiKey);
162
+ try {
163
+ saveSelection(provider, model, { baseUrl });
164
+ } catch (error) {
165
+ if (previous) store.setApiKey(provider, previous);
166
+ else store.removeApiKey(provider);
167
+ throw error;
168
+ }
169
+ });
170
+ process.stdout.write(`${provider}: API key configured in ${store.storage}\n`);
171
+ return 0;
172
+ }
173
+
174
+ if (action === "logout") {
175
+ const requested = String(args.find((value) => !value.startsWith("-")) || "").trim().toLowerCase();
176
+ if (!requested) throw new Error("usage: oracle auth logout PROVIDER");
177
+ let removedOAuth = false;
178
+ try {
179
+ removedOAuth = store.remove(oauthProviderId(requested));
180
+ } catch {}
181
+ const removedApiKey = store.removeApiKey(requested);
182
+ if (!removedOAuth && !removedApiKey) throw new Error(`${requested}: no local credential found`);
183
+ process.stdout.write(`${requested}: local credential removed\n`);
184
+ return 0;
185
+ }
186
+
187
+ throw new Error(`unknown auth action: ${action}`);
188
+ } catch (error) {
189
+ process.stderr.write(`oracle auth: ${error.message}\n`);
190
+ return 1;
191
+ }
192
+ },
193
+ };
@@ -7,7 +7,7 @@ import {
7
7
 
8
8
  function usage() {
9
9
  return [
10
- "oracle bootstrap - install the local agent runtime oracle chat needs",
10
+ "oracle bootstrap - optionally install an isolated Hermes compatibility runtime",
11
11
  "",
12
12
  " oracle bootstrap install into ~/.config/oracle/runtime/venv",
13
13
  " oracle bootstrap --upgrade upgrade the managed runtime",
@@ -22,7 +22,7 @@ function usage() {
22
22
 
23
23
  export default {
24
24
  name: "bootstrap",
25
- summary: "install the local agent runtime for oracle chat",
25
+ summary: "optionally install a Hermes compatibility runtime",
26
26
  group: "read",
27
27
  usage: usage(),
28
28
  async run(ctx) {
@@ -3,9 +3,13 @@ import path from "node:path";
3
3
  import { spawnSync } from "node:child_process";
4
4
  import { PACKAGE_ROOT, hermesRoot, ensureDir } from "../paths.mjs";
5
5
  import { activeChainEnv } from "../chain-state.mjs";
6
+ import { readModelConfig } from "../model-config.mjs";
6
7
  import { spawnChild } from "../spawn-child.mjs";
7
- import { installManagedHermes, resolveHermes } from "../runtime.mjs";
8
+ import { resolveHermes } from "../runtime.mjs";
8
9
  import { runOracleTui } from "../../tui/app.mjs";
10
+ import { resolveChatBackend } from "../../tui/backend.mjs";
11
+ import { createStandaloneClient, runStandaloneQuery } from "../../tui/standalone-client.mjs";
12
+ import { getOAuthStore } from "../../auth/oauth.mjs";
9
13
 
10
14
  const PROFILE = "oracle";
11
15
  const SKIN_NAME = "oracle";
@@ -24,30 +28,6 @@ function which(bin) {
24
28
  return null;
25
29
  }
26
30
 
27
- /**
28
- * First-run bootstrap. Only fires for a real interactive TTY so scripts,
29
- * pipes, and CI never trigger a multi-minute install.
30
- */
31
- function ensureRuntime() {
32
- const found = resolveHermes();
33
- if (found.ok) return found.bin;
34
- if (process.env.ORACLE_NO_BOOTSTRAP === "1") return null;
35
- if (!process.stdin.isTTY || !process.stdout.isTTY) return null;
36
-
37
- process.stdout.write(
38
- "oracle needs a local agent runtime for chat (one-time setup).\n" +
39
- "installing into ~/.config/oracle/runtime/venv, a few minutes.\n\n",
40
- );
41
- const r = installManagedHermes();
42
- if (!r.ok) {
43
- process.stderr.write(`oracle: runtime setup failed: ${r.reason}\n`);
44
- if (r.stderr) process.stderr.write(`${r.stderr}\n`);
45
- return null;
46
- }
47
- process.stdout.write("\nruntime ready.\n\n");
48
- return r.bin;
49
- }
50
-
51
31
  function pythonShebang(file) {
52
32
  try {
53
33
  const first = fs.readFileSync(file, "utf8").split(/\r?\n/, 1)[0];
@@ -291,6 +271,38 @@ function printBanner() {
291
271
  process.stdout.write(centerBlock(lines, terminalColumns()).join("\n") + "\n");
292
272
  }
293
273
 
274
+ function parseLaunchArgs(args) {
275
+ const parsed = { pass: [], query: null, model: null, provider: null };
276
+ for (let i = 0; i < args.length; i += 1) {
277
+ const arg = args[i];
278
+ if ((arg === "--model" || arg === "-m") && args[i + 1]) {
279
+ parsed.model = args[++i];
280
+ parsed.pass.push("--model", parsed.model);
281
+ continue;
282
+ }
283
+ if (arg === "--provider" && args[i + 1]) {
284
+ parsed.provider = args[++i];
285
+ parsed.pass.push("--provider", parsed.provider);
286
+ continue;
287
+ }
288
+ if ((arg === "-q" || arg === "--query") && args[i + 1]) {
289
+ parsed.query = args[++i];
290
+ parsed.pass.push("-q", parsed.query);
291
+ continue;
292
+ }
293
+ if (arg === "--quiet" || arg === "-Q") {
294
+ parsed.pass.push("--quiet");
295
+ continue;
296
+ }
297
+ if (arg === "--continue" || arg === "-c") {
298
+ parsed.pass.push("--continue");
299
+ continue;
300
+ }
301
+ parsed.pass.push(arg);
302
+ }
303
+ return parsed;
304
+ }
305
+
294
306
  export default {
295
307
  name: "chat",
296
308
  summary: "open the one oracle chat surface (telegram-like, multi-model)",
@@ -307,15 +319,69 @@ export default {
307
319
  return 0;
308
320
  }
309
321
 
310
- const hermes = which("hermes") || ensureRuntime();
311
- if (!hermes) {
322
+ const parsed = parseLaunchArgs(args);
323
+ const selectionEnv = {
324
+ ...process.env,
325
+ ...(parsed.model ? { ORACLE_MODEL: parsed.model } : {}),
326
+ ...(parsed.provider ? { ORACLE_PROVIDER: parsed.provider } : {}),
327
+ };
328
+ const authStore = getOAuthStore();
329
+ const backend = resolveChatBackend({
330
+ env: selectionEnv,
331
+ hermes: resolveHermes(),
332
+ storedConfig: readModelConfig(),
333
+ oauthAvailable: (provider) => Boolean(authStore.get(provider)),
334
+ apiKeyResolver: (provider) => authStore.getApiKey(provider),
335
+ });
336
+ if (backend.kind === "unconfigured") {
337
+ process.stderr.write(`${backend.reason}\n`);
338
+ return 1;
339
+ }
340
+
341
+ const cleanEnv = activeChainEnv({
342
+ ORACLE_CHAT_SURFACE: "1",
343
+ ORACLE_PROFILE: PROFILE,
344
+ ORACLE_NODE_BIN: process.execPath,
345
+ ORACLE_CLI_ENTRY: path.join(PACKAGE_ROOT, "bin", "oracle.mjs"),
346
+ });
347
+
348
+ if (backend.kind === "standalone") {
349
+ const client = createStandaloneClient({
350
+ config: {
351
+ ...backend.config,
352
+ ...(parsed.model ? { model: parsed.model } : {}),
353
+ },
354
+ });
355
+ if (parsed.query) {
356
+ try {
357
+ return await runStandaloneQuery({
358
+ client,
359
+ text: parsed.query,
360
+ stdout: process.stdout,
361
+ stderr: process.stderr,
362
+ });
363
+ } catch {
364
+ return 1;
365
+ }
366
+ }
367
+ const native = await runOracleTui({
368
+ client,
369
+ args,
370
+ env: cleanEnv,
371
+ cwd: process.cwd(),
372
+ stdin: process.stdin,
373
+ stdout: process.stdout,
374
+ });
375
+ if (native.native) return native.code;
312
376
  process.stderr.write(
313
- "oracle chat needs a local agent runtime\n" +
314
- "run: oracle bootstrap\n",
377
+ process.env.ORACLE_NATIVE_TUI === "0"
378
+ ? "oracle standalone chat requires the native TUI; remove ORACLE_NATIVE_TUI=0\n"
379
+ : "oracle standalone chat requires an interactive TTY or -q <query>\n",
315
380
  );
316
381
  return 1;
317
382
  }
318
383
 
384
+ const hermes = backend.bin;
319
385
  ensureOracleProfile(PROFILE);
320
386
  ensureOracleSkin(PROFILE);
321
387
  ensureSkillCommands(PROFILE);
@@ -325,7 +391,7 @@ export default {
325
391
  process.stderr.write(`oracle chat: skin note: ${skinSet.reason}\n`);
326
392
  }
327
393
 
328
- const explicitModel = args.includes("--model") || args.includes("-m") || args.includes("--provider");
394
+ const explicitModel = Boolean(parsed.model || parsed.provider);
329
395
  if (!modelReady(PROFILE) && !explicitModel) {
330
396
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
331
397
  process.stderr.write("oracle chat needs a configured model\nrun: oracle model\n");
@@ -342,50 +408,7 @@ export default {
342
408
  if (selected !== 0 || !modelReady(PROFILE)) return selected || 1;
343
409
  }
344
410
 
345
- // Parse a few passthrough flags.
346
411
  const hermesArgs = ["-p", PROFILE, "chat"];
347
- const pass = [];
348
- for (let i = 0; i < args.length; i++) {
349
- const a = args[i];
350
- if (a === "--model" || a === "-m") {
351
- if (args[i + 1]) {
352
- pass.push("--model", args[++i]);
353
- }
354
- continue;
355
- }
356
- if (a === "--provider") {
357
- if (args[i + 1]) {
358
- pass.push("--provider", args[++i]);
359
- }
360
- continue;
361
- }
362
- if (a === "-q" || a === "--query") {
363
- if (args[i + 1]) {
364
- pass.push("-q", args[++i]);
365
- }
366
- continue;
367
- }
368
- if (a === "--quiet" || a === "-Q") {
369
- pass.push("--quiet");
370
- continue;
371
- }
372
- if (a === "--continue" || a === "-c") {
373
- pass.push("--continue");
374
- continue;
375
- }
376
- // unknown flags go through
377
- pass.push(a);
378
- }
379
-
380
- // hermes -p scopes the same profile used by Telegram. Do not replace
381
- // HERMES_HOME with the profile dir; nested profile resolution would break.
382
- const cleanEnv = activeChainEnv({
383
- ORACLE_CHAT_SURFACE: "1",
384
- ORACLE_PROFILE: PROFILE,
385
- ORACLE_NODE_BIN: process.execPath,
386
- ORACLE_CLI_ENTRY: path.join(PACKAGE_ROOT, "bin", "oracle.mjs"),
387
- });
388
-
389
412
  const hermesPython = resolveHermesPython(hermes);
390
413
  const native = await runOracleTui({
391
414
  hermesPython,
@@ -397,8 +420,7 @@ export default {
397
420
  });
398
421
  if (native.native) return native.code;
399
422
 
400
- const launch = oracleHarnessInvocation(hermes, [...hermesArgs, ...pass]);
401
-
423
+ const launch = oracleHarnessInvocation(hermes, [...hermesArgs, ...parsed.pass]);
402
424
  return spawnChild(
403
425
  launch.command,
404
426
  launch.args,
@@ -53,7 +53,9 @@ export default {
53
53
  const bin = path.join(ctx.root, "bin", "desk-server.mjs");
54
54
  return spawnChild(process.execPath, [bin], { stdio: "inherit", env }, "desk-server");
55
55
  }
56
- if (verb === "health") return dataFetch("/");
56
+ // The server serves /health; / intentionally 404s with a route list. This
57
+ // probed / and so reported the package's own running server as down.
58
+ if (verb === "health") return dataFetch("/health");
57
59
  if (verb === "catalog") return dataFetch("/data/catalog");
58
60
  if (verb === "call") {
59
61
  const provider = ctx.argv[1];
@@ -21,7 +21,9 @@ function loadExecEnv(p) {
21
21
 
22
22
  async function probeDataServer(url) {
23
23
  try {
24
- const res = await fetch(url.replace(/\/$/, "") + "/");
24
+ // The server serves /health; / intentionally 404s with a route list, so
25
+ // probing / reported a healthy server as down.
26
+ const res = await fetch(url.replace(/\/$/, "") + "/health");
25
27
  return res.ok;
26
28
  } catch {
27
29
  return false;
@@ -47,9 +49,17 @@ export default {
47
49
 
48
50
  const execEnvPath = ctx.paths.oracleExecEnvPath();
49
51
  const envFile = loadExecEnv(execEnvPath);
52
+ // `oracle init --apply` writes ORACLE_ROUTE_ATTESTATION_SECRET (see
53
+ // bin/oracle-init.mjs), but doctor checked only ORACLE_ATTESTATION_SECRET
54
+ // and ORACLE_STAMP_HMAC_SECRET — so it told the user to rerun an init step
55
+ // that could never satisfy the check.
50
56
  const att =
57
+ process.env.ORACLE_ROUTE_ATTESTATION_SECRET ||
58
+ process.env.MAD_ROUTE_ATTESTATION_SECRET ||
51
59
  process.env.ORACLE_ATTESTATION_SECRET ||
52
60
  process.env.ORACLE_STAMP_HMAC_SECRET ||
61
+ envFile.ORACLE_ROUTE_ATTESTATION_SECRET ||
62
+ envFile.MAD_ROUTE_ATTESTATION_SECRET ||
53
63
  envFile.ORACLE_ATTESTATION_SECRET ||
54
64
  envFile.ORACLE_STAMP_HMAC_SECRET;
55
65
  push("attestation_secret", att ? "ok" : "warn", att ? "present" : "missing",
@@ -1,38 +1,94 @@
1
1
  import { spawnChild } from "../spawn-child.mjs";
2
- import { installManagedHermes, resolveHermes } from "../runtime.mjs";
2
+ import { resolveHermes } from "../runtime.mjs";
3
+ import { readModelConfig, writeModelConfig } from "../model-config.mjs";
3
4
  import { oracleHarnessInvocation } from "./chat.mjs";
4
5
 
5
- function runtimeBin() {
6
- const found = resolveHermes();
7
- if (found.ok) return found.bin;
8
- if (process.env.ORACLE_NO_BOOTSTRAP === "1") return null;
9
- if (!process.stdin.isTTY || !process.stdout.isTTY) return null;
10
- process.stdout.write("setting up oracle's local model runtime (one time)\n\n");
11
- const installed = installManagedHermes();
12
- if (!installed.ok) {
13
- process.stderr.write(`oracle model: runtime setup failed: ${installed.reason}\n`);
14
- return null;
6
+ function option(args, name) {
7
+ const index = args.indexOf(name);
8
+ return index >= 0 && args[index + 1] ? args[index + 1] : null;
9
+ }
10
+
11
+ function standalonePatch(args) {
12
+ const backend = option(args, "--backend");
13
+ const provider = option(args, "--provider");
14
+ const model = option(args, "--model") || option(args, "-m");
15
+ const baseUrl = option(args, "--base-url");
16
+ const apiKeyEnv = option(args, "--api-key-env");
17
+ const contextLength = option(args, "--context-length");
18
+ const reasoningEffort = option(args, "--reasoning-effort");
19
+ const hasStandaloneField = Boolean(provider || model || baseUrl || apiKeyEnv || contextLength || reasoningEffort);
20
+ if (!backend && !hasStandaloneField) return null;
21
+ const current = { ...(readModelConfig() || {}) };
22
+ if (provider) {
23
+ delete current.apiKeyEnv;
24
+ if (provider !== "custom" || provider !== current.provider) delete current.baseUrl;
15
25
  }
16
- return installed.bin;
26
+ return {
27
+ ...current,
28
+ backend: backend || "standalone",
29
+ ...(provider ? { provider } : {}),
30
+ ...(model ? { model } : {}),
31
+ ...(baseUrl ? { baseUrl } : {}),
32
+ ...(apiKeyEnv ? { apiKeyEnv } : {}),
33
+ ...(contextLength ? { contextLength } : {}),
34
+ ...(reasoningEffort ? { reasoningEffort } : {}),
35
+ };
36
+ }
37
+
38
+ function usage() {
39
+ return [
40
+ "oracle model, choose standalone model settings or open Hermes' picker",
41
+ "",
42
+ " oracle model",
43
+ " oracle model --show",
44
+ " oracle model --backend standalone --provider openrouter --model openrouter/auto",
45
+ " oracle model --backend auto",
46
+ "",
47
+ "standalone keys stay in environment variables, never model.json:",
48
+ " OPENROUTER_API_KEY, OPENAI_API_KEY, XAI_API_KEY, DEEPSEEK_API_KEY, GEMINI_API_KEY",
49
+ ].join("\n");
17
50
  }
18
51
 
19
52
  export default {
20
53
  name: "model",
21
- summary: "choose the oracle profile's model/provider",
54
+ summary: "choose Oracle's standalone model or existing Hermes provider",
22
55
  group: "read",
23
- usage: [
24
- "oracle model - interactive provider/model picker for the oracle profile",
25
- "",
26
- "inside chat: /model",
27
- "one oracle persona stays loaded when the model changes.",
28
- ].join("\n"),
56
+ usage: usage(),
29
57
  async run(ctx) {
30
- const hermes = runtimeBin();
31
- if (!hermes) {
32
- process.stderr.write("oracle model needs the local runtime\nrun: oracle bootstrap\n");
58
+ const args = [...ctx.argv];
59
+ if (args.includes("--show")) {
60
+ process.stdout.write(`${JSON.stringify(readModelConfig() || { backend: "auto" }, null, 2)}\n`);
61
+ return 0;
62
+ }
63
+
64
+ const patch = standalonePatch(args);
65
+ if (patch) {
66
+ try {
67
+ const saved = writeModelConfig(patch);
68
+ process.stdout.write(`oracle model: ${saved.backend}`);
69
+ if (saved.provider) process.stdout.write(` / ${saved.provider}`);
70
+ if (saved.model) process.stdout.write(` / ${saved.model}`);
71
+ process.stdout.write("\n");
72
+ if (saved.backend === "standalone" && saved.apiKeyEnv) {
73
+ process.stdout.write(`key source: ${saved.apiKeyEnv}\n`);
74
+ }
75
+ return 0;
76
+ } catch (error) {
77
+ process.stderr.write(`oracle model: ${error.message}\n`);
78
+ return 1;
79
+ }
80
+ }
81
+
82
+ const found = resolveHermes();
83
+ if (!found.ok) {
84
+ process.stderr.write(
85
+ "Oracle standalone chat does not require Hermes.\n" +
86
+ "Configure a provider with: oracle model --backend standalone --provider openrouter --model openrouter/auto\n" +
87
+ "Then set OPENROUTER_API_KEY.\n",
88
+ );
33
89
  return 1;
34
90
  }
35
- const launch = oracleHarnessInvocation(hermes, ["-p", "oracle", "model", ...ctx.argv]);
91
+ const launch = oracleHarnessInvocation(found.bin, ["-p", "oracle", "model", ...args]);
36
92
  return spawnChild(
37
93
  launch.command,
38
94
  launch.args,
@@ -12,12 +12,13 @@ import {
12
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)
16
- oracle bootstrap install the local agent runtime chat needs
15
+ oracle open the native Oracle chat (TTY)
16
+ oracle bootstrap optional: install an isolated Hermes compatibility runtime
17
17
  oracle init install agent lanes + read plane (dry-run by default)
18
18
  oracle doctor check read plane; checks signer too when installed
19
19
  oracle chat premium boxed oracle chat (multi-model)
20
- oracle model choose the profile's default model/provider
20
+ oracle model choose standalone/Hermes provider and any compatible model
21
+ oracle auth Claude/Codex/Grok OAuth or securely stored provider API keys
21
22
  oracle chain list/select working chains (hyperliquid, base, ...)
22
23
  oracle setup telegram/discord/slack messaging menu
23
24
  oracle data serve|call|catalog|health