@oracle-agent/oracle 0.4.2 → 0.5.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 (62) hide show
  1. package/README.md +66 -17
  2. package/SETUP.md +13 -0
  3. package/bin/oracle-data-mcp.mjs +1 -1
  4. package/bin/oracle.mjs +33 -0
  5. package/docs/cli.md +19 -0
  6. package/docs/connectors.md +13 -0
  7. package/package.json +7 -3
  8. package/profiles/robinhood-agent/SOUL.md +5 -3
  9. package/public/agent-connect/app.js +16 -13
  10. package/public/oracle-splash/assets/wordmarks/across.svg +4 -0
  11. package/public/oracle-splash/assets/wordmarks/aerodrome.svg +23 -0
  12. package/public/oracle-splash/assets/wordmarks/balancer.svg +3 -0
  13. package/public/oracle-splash/assets/wordmarks/cowswap.svg +1 -0
  14. package/public/oracle-splash/assets/wordmarks/gmx.svg +13 -0
  15. package/public/oracle-splash/assets/wordmarks/hyperliquid.svg +21 -0
  16. package/public/oracle-splash/assets/wordmarks/hyperswap.svg +13 -0
  17. package/public/oracle-splash/assets/wordmarks/jupiter.svg +35 -0
  18. package/public/oracle-splash/assets/wordmarks/lifi.png +0 -0
  19. package/public/oracle-splash/assets/wordmarks/magic-eden.svg +1 -0
  20. package/public/oracle-splash/assets/wordmarks/morpho.svg +1 -0
  21. package/public/oracle-splash/assets/wordmarks/odos.svg +3 -0
  22. package/public/oracle-splash/assets/wordmarks/oneinch.svg +6 -0
  23. package/public/oracle-splash/assets/wordmarks/opensea.svg +1 -0
  24. package/public/oracle-splash/assets/wordmarks/pancakeswap.svg +18 -0
  25. package/public/oracle-splash/assets/wordmarks/paraswap.svg +16 -0
  26. package/public/oracle-splash/assets/wordmarks/pendle.png +0 -0
  27. package/public/oracle-splash/assets/wordmarks/polymarket.png +0 -0
  28. package/public/oracle-splash/assets/wordmarks/quickswap.png +0 -0
  29. package/public/oracle-splash/assets/wordmarks/relay.svg +8 -0
  30. package/public/oracle-splash/assets/wordmarks/stargate.svg +15 -0
  31. package/public/oracle-splash/assets/wordmarks/uniswap.svg +18 -0
  32. package/public/oracle-splash/assets/wordmarks/velodrome.svg +84 -0
  33. package/public/oracle-splash/index.html +789 -485
  34. package/scripts/check-doc-drift.mjs +111 -0
  35. package/src/cli/commands/credential.mjs +9 -0
  36. package/src/cli/commands/data-mcp.mjs +18 -0
  37. package/src/cli/commands/data.mjs +73 -0
  38. package/src/cli/commands/doctor.mjs +131 -0
  39. package/src/cli/commands/help.mjs +7 -0
  40. package/src/cli/commands/init.mjs +27 -0
  41. package/src/cli/commands/mcp.mjs +142 -0
  42. package/src/cli/commands/prepare.mjs +12 -0
  43. package/src/cli/commands/public.mjs +20 -0
  44. package/src/cli/commands/route.mjs +12 -0
  45. package/src/cli/commands/runner.mjs +9 -0
  46. package/src/cli/commands/scan.mjs +12 -0
  47. package/src/cli/commands/sign.mjs +21 -0
  48. package/src/cli/commands/signer.mjs +9 -0
  49. package/src/cli/commands/upgrade.mjs +12 -0
  50. package/src/cli/commands/vault.mjs +9 -0
  51. package/src/cli/commands/version.mjs +22 -0
  52. package/src/cli/first-run.mjs +20 -0
  53. package/src/cli/kernel.mjs +198 -0
  54. package/src/cli/mcp-targets/chatgpt.mjs +51 -0
  55. package/src/cli/mcp-targets/claude-code.mjs +39 -0
  56. package/src/cli/mcp-targets/claude-desktop.mjs +24 -0
  57. package/src/cli/mcp-targets/codex.mjs +41 -0
  58. package/src/cli/mcp-targets/shared.mjs +61 -0
  59. package/src/cli/operator-dispatch.mjs +258 -0
  60. package/src/cli/paths.mjs +78 -0
  61. package/src/cli/spawn-child.mjs +43 -0
  62. package/src/public-api/connect-agent.mjs +29 -10
@@ -0,0 +1,258 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ import { createRequire } from "node:module";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
8
+ const PACKAGE_ROOT = path.resolve(HERE, "../..");
9
+
10
+ export const OPERATOR_BIN_NAMES = Object.freeze([
11
+ "oracle-vault",
12
+ "oracle-credential",
13
+ "oracle-signer",
14
+ "oracle-runner",
15
+ "oracle-agentic-init",
16
+ "oracle-agentic-doctor",
17
+ "oracle-control-mcp",
18
+ "oracle-operator-caps",
19
+ ]);
20
+
21
+ export const SIGN_NOUNS = Object.freeze([
22
+ "vault",
23
+ "credential",
24
+ "signer",
25
+ "runner",
26
+ "sign",
27
+ ]);
28
+
29
+ export const OPERATOR_NOT_INSTALLED_MESSAGE = `oracle: signing lives in @oracle-agent/operator, which is not installed.
30
+
31
+ The public 'oracle' package is prepare-only and never handles keys.
32
+ To enable local signing on THIS machine:
33
+
34
+ npm i -g @oracle-agent/operator # or add it to this project
35
+ oracle sign init # provision keys + policy (interactive)
36
+
37
+ Nothing about your read/research setup is affected.
38
+ `;
39
+
40
+ export const SIGN_HINT =
41
+ "hint: run 'oracle sign init' to provision keys, or 'oracle sign doctor --json' for detail.";
42
+
43
+ function whichOnPath(binName) {
44
+ const pathEnv = process.env.PATH || "";
45
+ const parts = pathEnv.split(path.delimiter).filter(Boolean);
46
+ const exts = process.platform === "win32" ? ["", ".cmd", ".exe", ".bat"] : [""];
47
+ for (const dir of parts) {
48
+ for (const ext of exts) {
49
+ const candidate = path.join(dir, binName + ext);
50
+ try {
51
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
52
+ return candidate;
53
+ }
54
+ } catch {
55
+ // ignore
56
+ }
57
+ }
58
+ }
59
+ return null;
60
+ }
61
+
62
+ function binsFromDir(binDir) {
63
+ const bins = {};
64
+ for (const name of OPERATOR_BIN_NAMES) {
65
+ const candidates = [
66
+ path.join(binDir, name),
67
+ path.join(binDir, `${name}.mjs`),
68
+ path.join(binDir, `${name}.js`),
69
+ ];
70
+ for (const c of candidates) {
71
+ if (fs.existsSync(c) && fs.statSync(c).isFile()) {
72
+ bins[name] = c;
73
+ break;
74
+ }
75
+ }
76
+ }
77
+ return bins;
78
+ }
79
+
80
+ function findPackageRoot(entryPath) {
81
+ let dir = path.dirname(entryPath);
82
+ while (dir !== path.dirname(dir)) {
83
+ const pkgJson = path.join(dir, "package.json");
84
+ if (fs.existsSync(pkgJson)) {
85
+ try {
86
+ const pkg = JSON.parse(fs.readFileSync(pkgJson, "utf8"));
87
+ if (pkg.name === "@oracle-agent/operator") return { root: dir, pkg };
88
+ } catch {
89
+ // continue upward
90
+ }
91
+ }
92
+ dir = path.dirname(dir);
93
+ }
94
+ return null;
95
+ }
96
+
97
+ function tryResolvePackage(fromPaths) {
98
+ for (const from of fromPaths) {
99
+ try {
100
+ const req = createRequire(path.join(from, "package.json"));
101
+ let found;
102
+ try {
103
+ const pkgJson = req.resolve("@oracle-agent/operator/package.json");
104
+ found = {
105
+ root: path.dirname(pkgJson),
106
+ pkg: JSON.parse(fs.readFileSync(pkgJson, "utf8")),
107
+ };
108
+ } catch {
109
+ const entry = req.resolve("@oracle-agent/operator");
110
+ found = findPackageRoot(entry);
111
+ }
112
+ if (!found) continue;
113
+ const { root, pkg } = found;
114
+ const binField = pkg.bin || {};
115
+ const bins = {};
116
+ for (const [name, rel] of Object.entries(binField)) {
117
+ bins[name] = path.resolve(root, rel);
118
+ }
119
+ const extra = binsFromDir(path.join(root, "bin"));
120
+ for (const [k, v] of Object.entries(extra)) {
121
+ if (!bins[k]) bins[k] = v;
122
+ }
123
+ return {
124
+ ok: true,
125
+ binDir: path.join(root, "bin"),
126
+ version: pkg.version || "unknown",
127
+ bins,
128
+ source: `require:${from}`,
129
+ };
130
+ } catch {
131
+ // continue
132
+ }
133
+ }
134
+ return null;
135
+ }
136
+
137
+ function tryCapsOnPath() {
138
+ const capsBin = whichOnPath("oracle-operator-caps");
139
+ if (!capsBin) return null;
140
+ const r = spawnSync(capsBin, [], {
141
+ encoding: "utf8",
142
+ timeout: 5000,
143
+ env: process.env,
144
+ });
145
+ if (r.status !== 0 || !r.stdout) return null;
146
+ try {
147
+ const report = JSON.parse(r.stdout);
148
+ if (!report || typeof report !== "object" || !report.bins) return null;
149
+ return {
150
+ ok: true,
151
+ binDir: path.dirname(Object.values(report.bins)[0] || capsBin),
152
+ version: report.version || "unknown",
153
+ bins: report.bins,
154
+ source: "caps-path",
155
+ controlMcp: report.controlMcp,
156
+ };
157
+ } catch {
158
+ return null;
159
+ }
160
+ }
161
+
162
+ function tryPathProbe() {
163
+ const bins = {};
164
+ for (const name of OPERATOR_BIN_NAMES) {
165
+ const hit = whichOnPath(name);
166
+ if (hit) bins[name] = hit;
167
+ }
168
+ if (!Object.keys(bins).length) return null;
169
+ return {
170
+ ok: true,
171
+ binDir: path.dirname(Object.values(bins)[0]),
172
+ version: "unknown",
173
+ bins,
174
+ source: "path-probe",
175
+ };
176
+ }
177
+
178
+ export function resolveOperator() {
179
+ const envDir = process.env.ORACLE_OPERATOR_BIN_DIR;
180
+ if (envDir) {
181
+ const abs = path.resolve(envDir);
182
+ if (!fs.existsSync(abs)) return { ok: false };
183
+ const bins = binsFromDir(abs);
184
+ if (!Object.keys(bins).length) return { ok: false };
185
+ let version = "unknown";
186
+ const pkgCandidate = path.resolve(abs, "../package.json");
187
+ if (fs.existsSync(pkgCandidate)) {
188
+ try {
189
+ version = JSON.parse(fs.readFileSync(pkgCandidate, "utf8")).version || version;
190
+ } catch {
191
+ // ignore
192
+ }
193
+ }
194
+ return { ok: true, binDir: abs, version, bins, source: "env:ORACLE_OPERATOR_BIN_DIR" };
195
+ }
196
+
197
+ const fromPkg =
198
+ tryResolvePackage([process.cwd(), PACKAGE_ROOT]) ||
199
+ tryCapsOnPath() ||
200
+ tryPathProbe();
201
+ return fromPkg || { ok: false };
202
+ }
203
+
204
+ export function printOperatorNotInstalled(stream = process.stderr) {
205
+ stream.write(OPERATOR_NOT_INSTALLED_MESSAGE);
206
+ if (!OPERATOR_NOT_INSTALLED_MESSAGE.endsWith("\n")) stream.write("\n");
207
+ }
208
+
209
+ export function dispatchOperator(binName, argv = [], opts = {}) {
210
+ const resolved = resolveOperator();
211
+ if (!resolved.ok) {
212
+ printOperatorNotInstalled(opts.stderr || process.stderr);
213
+ return 3;
214
+ }
215
+ const binPath = resolved.bins[binName];
216
+ if (!binPath) {
217
+ const err = opts.stderr || process.stderr;
218
+ err.write(`oracle: operator bin '${binName}' not found in resolved operator (${resolved.source})\n`);
219
+ return 3;
220
+ }
221
+
222
+ const stdio = opts.stdio || "inherit";
223
+ const isJs = /\.(mjs|cjs|js)$/.test(binPath);
224
+ const command = isJs ? process.execPath : binPath;
225
+ const args = isJs ? [binPath, ...argv] : [...argv];
226
+ const r = spawnSync(command, args, {
227
+ stdio,
228
+ env: opts.env || process.env,
229
+ cwd: opts.cwd || process.cwd(),
230
+ encoding: stdio === "pipe" ? "utf8" : undefined,
231
+ });
232
+
233
+ if (r.error) {
234
+ const err = opts.stderr || process.stderr;
235
+ err.write(`oracle: failed to spawn ${binName}: ${r.error.message}\n`);
236
+ return 1;
237
+ }
238
+
239
+ const code = typeof r.status === "number" ? r.status : 1;
240
+ if (code !== 0 && opts.appendHintOnError) {
241
+ const err = opts.stderr || process.stderr;
242
+ err.write(SIGN_HINT + "\n");
243
+ }
244
+ if (stdio === "pipe") {
245
+ return { code, stdout: r.stdout || "", stderr: r.stderr || "" };
246
+ }
247
+ return code;
248
+ }
249
+
250
+ export default {
251
+ resolveOperator,
252
+ dispatchOperator,
253
+ printOperatorNotInstalled,
254
+ OPERATOR_NOT_INSTALLED_MESSAGE,
255
+ SIGN_HINT,
256
+ SIGN_NOUNS,
257
+ OPERATOR_BIN_NAMES,
258
+ };
@@ -0,0 +1,78 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
7
+ export const PACKAGE_ROOT = path.resolve(HERE, "../..");
8
+
9
+ export function homeDir() {
10
+ return process.env.ORACLE_FAKE_HOME || os.homedir();
11
+ }
12
+
13
+ export function oracleConfigDir() {
14
+ return path.join(homeDir(), ".config", "oracle");
15
+ }
16
+
17
+ export function oracleExecEnvPath() {
18
+ return path.join(oracleConfigDir(), "exec.env");
19
+ }
20
+
21
+ export function hermesRoot() {
22
+ if (process.env.HERMES_HOME) return path.resolve(process.env.HERMES_HOME);
23
+ return path.join(homeDir(), ".hermes");
24
+ }
25
+
26
+ export function claudeCodeUserConfigPath() {
27
+ return path.join(homeDir(), ".claude.json");
28
+ }
29
+
30
+ export function claudeCodeProjectMcpPath(cwd = process.cwd()) {
31
+ return path.join(cwd, ".mcp.json");
32
+ }
33
+
34
+ export function claudeDesktopConfigPath() {
35
+ if (process.platform === "darwin") {
36
+ return path.join(homeDir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
37
+ }
38
+ return path.join(homeDir(), ".config", "Claude", "claude_desktop_config.json");
39
+ }
40
+
41
+ export function codexConfigPath() {
42
+ return path.join(homeDir(), ".codex", "config.toml");
43
+ }
44
+
45
+ export function chatgptConnectorDir() {
46
+ return path.join(oracleConfigDir(), "connectors");
47
+ }
48
+
49
+ export function chatgptOpenApiPath() {
50
+ return path.join(chatgptConnectorDir(), "chatgpt-openapi.json");
51
+ }
52
+
53
+ export function packageBin(name) {
54
+ return path.join(PACKAGE_ROOT, "bin", name);
55
+ }
56
+
57
+ export function ensureDir(dir) {
58
+ fs.mkdirSync(dir, { recursive: true });
59
+ return dir;
60
+ }
61
+
62
+ export const paths = {
63
+ homeDir,
64
+ oracleConfigDir,
65
+ oracleExecEnvPath,
66
+ hermesRoot,
67
+ claudeCodeUserConfigPath,
68
+ claudeCodeProjectMcpPath,
69
+ claudeDesktopConfigPath,
70
+ codexConfigPath,
71
+ chatgptConnectorDir,
72
+ chatgptOpenApiPath,
73
+ packageBin,
74
+ ensureDir,
75
+ PACKAGE_ROOT,
76
+ };
77
+
78
+ export default paths;
@@ -0,0 +1,43 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ const FORWARDED_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
4
+
5
+ export function spawnChild(command, args = [], options = {}, label = command) {
6
+ return new Promise((resolve) => {
7
+ const child = spawn(command, args, options);
8
+ let settled = false;
9
+ let forwardedSignal = null;
10
+
11
+ const handlers = new Map(
12
+ FORWARDED_SIGNALS.map((signal) => [signal, () => {
13
+ forwardedSignal ||= signal;
14
+ if (child.exitCode === null && child.signalCode === null) child.kill(signal);
15
+ }]),
16
+ );
17
+ for (const [signal, handler] of handlers) process.on(signal, handler);
18
+
19
+ const cleanup = () => {
20
+ for (const [signal, handler] of handlers) process.off(signal, handler);
21
+ };
22
+
23
+ child.once("error", (error) => {
24
+ if (settled) return;
25
+ settled = true;
26
+ cleanup();
27
+ process.stderr.write(`oracle: could not start ${label}: ${error.message}\n`);
28
+ resolve(1);
29
+ });
30
+
31
+ child.once("exit", (code, signal) => {
32
+ if (settled) return;
33
+ settled = true;
34
+ cleanup();
35
+ const propagated = forwardedSignal || signal;
36
+ if (propagated) {
37
+ process.kill(process.pid, propagated);
38
+ return;
39
+ }
40
+ resolve(code ?? 1);
41
+ });
42
+ });
43
+ }
@@ -53,6 +53,18 @@ export class SecretLeakError extends Error {
53
53
  const FORBIDDEN_KEY_RE =
54
54
  /(private[_-]?key|secret|bearer|keystore|mnemonic|seed[_-]?phrase|passphrase|password|api[_-]?key|access[_-]?token|auth[_-]?token|session[_-]?token|signer[_-]?url|signature)/i;
55
55
 
56
+ /** Short wallet-export aliases need exact normalized matching. Substring
57
+ * matching `wif`, for example, would incorrectly reject an unrelated key such
58
+ * as `swift`. Removing common separators catches snake/kebab/spaced variants
59
+ * without widening those short aliases into arbitrary words. */
60
+ const FORBIDDEN_KEY_ALIASES = Object.freeze(["privkey", "wif", "xprv", "seed"]);
61
+
62
+ function isForbiddenKeyName(key) {
63
+ const text = String(key);
64
+ const normalized = text.toLowerCase().replace(/[\s._-]+/g, "");
65
+ return FORBIDDEN_KEY_RE.test(text) || FORBIDDEN_KEY_ALIASES.includes(normalized);
66
+ }
67
+
56
68
  /** String-value shapes that must never appear in any object we return. */
57
69
  const FORBIDDEN_VALUE_RULES = Object.freeze([
58
70
  // 32-byte hex — the shape of a raw EVM private key / session secret. Public
@@ -69,8 +81,12 @@ const FORBIDDEN_VALUE_RULES = Object.freeze([
69
81
  // gets disabled. Smuggling a key inside a longer string is still caught at
70
82
  // the serialized layer for any non-allowlisted field.
71
83
  { rule: "raw-32-byte-hex-bare", re: /^[0-9a-fA-F]{64}$/ },
72
- // Bitcoin WIF (mainnet 5/K/L, testnet c) — base58, 51-52 chars.
73
- { rule: "bitcoin-wif", re: /\b[5KLc][1-9A-HJ-NP-Za-km-z]{50,51}\b/ },
84
+ // BIP-32 private extended keys. Public xpub/tpub/ypub/zpub identifiers are
85
+ // intentionally not matched; only their private-key counterparts fail.
86
+ { rule: "extended-private-key", re: /\b(?:xprv|tprv|yprv|zprv)[1-9A-HJ-NP-Za-km-z]{107}\b/ },
87
+ // Bitcoin WIF (mainnet 5/K/L, testnet uncompressed 9 or compressed c) —
88
+ // base58, 51-52 chars.
89
+ { rule: "bitcoin-wif", re: /\b[59KLc][1-9A-HJ-NP-Za-km-z]{50,51}\b/ },
74
90
  // BIP-39 mnemonic: 12/15/18/21/24 lowercase words. Detect a long run of
75
91
  // space-separated short alpha words rather than shipping the wordlist.
76
92
  { rule: "bip39-mnemonic", re: /\b(?:[a-z]{3,8}\s+){11,23}[a-z]{3,8}\b/ },
@@ -169,12 +185,15 @@ export function assertSerializedNoSecrets(value) {
169
185
  if (embedded.test(scanned)) {
170
186
  throw new SecretLeakError("$<serialized>", "raw-32-byte-hex-embedded");
171
187
  }
172
- // Key names are checked against the serialized text too, so a key smuggled
173
- // in via toJSON or a getter is still caught.
174
- const keyHit = scanned.match(
175
- /"([^"]*(?:private[_-]?key|secret|bearer|keystore|mnemonic|seed[_-]?phrase|passphrase|password|api[_-]?key|access[_-]?token|auth[_-]?token|session[_-]?token|signer[_-]?url)[^"]*)"\s*:/i
176
- );
177
- if (keyHit) throw new SecretLeakError(`$<serialized>.${keyHit[1]}`, "forbidden-key-name");
188
+ // Parse the exact serialized bytes with a reviver so escaped or toJSON-made
189
+ // key names receive the same normalization as real object properties. This
190
+ // avoids maintaining a second, inevitably drifting key-name regex.
191
+ JSON.parse(json, (key, item) => {
192
+ if (key && isForbiddenKeyName(key)) {
193
+ throw new SecretLeakError(`$<serialized>.${key}`, "forbidden-key-name");
194
+ }
195
+ return item;
196
+ });
178
197
  return json;
179
198
  }
180
199
 
@@ -207,7 +226,7 @@ export function assertNoSecretMaterial(value, path = "$", seen = new Set(), keyH
207
226
  if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) return value;
208
227
  if (value instanceof Map) {
209
228
  for (const [k, v] of value.entries()) {
210
- if (typeof k === "string" && FORBIDDEN_KEY_RE.test(k)) {
229
+ if (typeof k === "string" && isForbiddenKeyName(k)) {
211
230
  throw new SecretLeakError(`${path}[${JSON.stringify(k)}]`, "forbidden-key-name");
212
231
  }
213
232
  assertNoSecretMaterial(k, `${path}[key]`, seen);
@@ -225,7 +244,7 @@ export function assertNoSecretMaterial(value, path = "$", seen = new Set(), keyH
225
244
  return value;
226
245
  }
227
246
  for (const [k, v] of Object.entries(value)) {
228
- if (FORBIDDEN_KEY_RE.test(k)) {
247
+ if (isForbiddenKeyName(k)) {
229
248
  throw new SecretLeakError(`${path}.${k}`, "forbidden-key-name");
230
249
  }
231
250
  assertNoSecretMaterial(v, `${path}.${k}`, seen, k);