@oracle-agent/oracle 0.4.2 → 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.
Files changed (129) hide show
  1. package/README.md +76 -18
  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 +47 -0
  6. package/docs/connectors.md +13 -0
  7. package/package.json +6 -2
  8. package/profiles/oracle/SOUL.md +10 -4
  9. package/profiles/robinhood-agent/SOUL.md +5 -3
  10. package/protocols/templates/safe-erc20/README.md +9 -0
  11. package/public/agent-connect/app.js +16 -13
  12. package/public/oracle-splash/assets/hero/orb.avif +0 -0
  13. package/public/oracle-splash/assets/hero/orb.webm +0 -0
  14. package/public/oracle-splash/assets/llms/claude.svg +1 -0
  15. package/public/oracle-splash/assets/llms/codex.svg +1 -0
  16. package/public/oracle-splash/assets/llms/cursor.svg +17 -0
  17. package/public/oracle-splash/assets/llms/deepseek.svg +1 -0
  18. package/public/oracle-splash/assets/llms/gemini.svg +1 -0
  19. package/public/oracle-splash/assets/llms/grok.svg +8 -0
  20. package/public/oracle-splash/assets/llms/hermes.png +0 -0
  21. package/public/oracle-splash/assets/llms/kimi.svg +1 -0
  22. package/public/oracle-splash/assets/llms/manus.svg +18 -0
  23. package/public/oracle-splash/assets/llms/openclaw.svg +22 -0
  24. package/public/oracle-splash/assets/llms/perplexity.svg +1 -0
  25. package/public/oracle-splash/assets/llms/qwen.svg +1 -0
  26. package/public/oracle-splash/assets/llms/windsurf.svg +1 -0
  27. package/public/oracle-splash/assets/protocols/abcdex.png +0 -0
  28. package/public/oracle-splash/assets/protocols/across.png +0 -0
  29. package/public/oracle-splash/assets/protocols/aerodrome.png +0 -0
  30. package/public/oracle-splash/assets/protocols/balancer.png +0 -0
  31. package/public/oracle-splash/assets/protocols/cowswap.png +0 -0
  32. package/public/oracle-splash/assets/protocols/curve.png +0 -0
  33. package/public/oracle-splash/assets/protocols/dreamcash.png +0 -0
  34. package/public/oracle-splash/assets/protocols/felix.png +0 -0
  35. package/public/oracle-splash/assets/protocols/gmx.png +0 -0
  36. package/public/oracle-splash/assets/protocols/hyena.png +0 -0
  37. package/public/oracle-splash/assets/protocols/hyperbeat.png +0 -0
  38. package/public/oracle-splash/assets/protocols/hyperliquid.png +0 -0
  39. package/public/oracle-splash/assets/protocols/hyperswap.png +0 -0
  40. package/public/oracle-splash/assets/protocols/jupiter.png +0 -0
  41. package/public/oracle-splash/assets/protocols/kinetiq.png +0 -0
  42. package/public/oracle-splash/assets/protocols/lfj.png +0 -0
  43. package/public/oracle-splash/assets/protocols/lifi.png +0 -0
  44. package/public/oracle-splash/assets/protocols/magic-eden.png +0 -0
  45. package/public/oracle-splash/assets/protocols/morpho.png +0 -0
  46. package/public/oracle-splash/assets/protocols/odos.png +0 -0
  47. package/public/oracle-splash/assets/protocols/oneinch.png +0 -0
  48. package/public/oracle-splash/assets/protocols/opensea.png +0 -0
  49. package/public/oracle-splash/assets/protocols/pancakeswap.png +0 -0
  50. package/public/oracle-splash/assets/protocols/paragon.png +0 -0
  51. package/public/oracle-splash/assets/protocols/paraswap.png +0 -0
  52. package/public/oracle-splash/assets/protocols/pendle.png +0 -0
  53. package/public/oracle-splash/assets/protocols/polymarket.png +0 -0
  54. package/public/oracle-splash/assets/protocols/quickswap.png +0 -0
  55. package/public/oracle-splash/assets/protocols/relay.png +0 -0
  56. package/public/oracle-splash/assets/protocols/stargate.png +0 -0
  57. package/public/oracle-splash/assets/protocols/tradexyz.png +0 -0
  58. package/public/oracle-splash/assets/protocols/uniswap.png +0 -0
  59. package/public/oracle-splash/assets/protocols/velodrome.png +0 -0
  60. package/public/oracle-splash/assets/protocols/ventuals.png +0 -0
  61. package/public/oracle-splash/assets/wordmarks/across.svg +4 -0
  62. package/public/oracle-splash/assets/wordmarks/aerodrome.svg +23 -0
  63. package/public/oracle-splash/assets/wordmarks/balancer.svg +3 -0
  64. package/public/oracle-splash/assets/wordmarks/cowswap.svg +1 -0
  65. package/public/oracle-splash/assets/wordmarks/gmx.svg +13 -0
  66. package/public/oracle-splash/assets/wordmarks/hyena.svg +7 -0
  67. package/public/oracle-splash/assets/wordmarks/hyperliquid.svg +21 -0
  68. package/public/oracle-splash/assets/wordmarks/hyperswap.svg +13 -0
  69. package/public/oracle-splash/assets/wordmarks/jupiter.svg +35 -0
  70. package/public/oracle-splash/assets/wordmarks/lifi.png +0 -0
  71. package/public/oracle-splash/assets/wordmarks/magic-eden.svg +1 -0
  72. package/public/oracle-splash/assets/wordmarks/morpho.svg +1 -0
  73. package/public/oracle-splash/assets/wordmarks/odos.svg +3 -0
  74. package/public/oracle-splash/assets/wordmarks/oneinch.svg +6 -0
  75. package/public/oracle-splash/assets/wordmarks/opensea.svg +1 -0
  76. package/public/oracle-splash/assets/wordmarks/outcome.svg +16 -0
  77. package/public/oracle-splash/assets/wordmarks/pancakeswap.svg +18 -0
  78. package/public/oracle-splash/assets/wordmarks/paraswap.svg +16 -0
  79. package/public/oracle-splash/assets/wordmarks/pendle.png +0 -0
  80. package/public/oracle-splash/assets/wordmarks/polymarket.png +0 -0
  81. package/public/oracle-splash/assets/wordmarks/quickswap.png +0 -0
  82. package/public/oracle-splash/assets/wordmarks/relay.svg +8 -0
  83. package/public/oracle-splash/assets/wordmarks/stargate.svg +15 -0
  84. package/public/oracle-splash/assets/wordmarks/uniswap.svg +18 -0
  85. package/public/oracle-splash/assets/wordmarks/velodrome.svg +84 -0
  86. package/public/oracle-splash/assets/wordmarks/xyz.svg +12 -0
  87. package/public/oracle-splash/index.html +784 -498
  88. package/scripts/check-doc-drift.mjs +111 -0
  89. package/skills/oracle-chat/SKILL.md +61 -0
  90. package/skills/oracle-chat/chain.SKILL.md +31 -0
  91. package/skills/oracle-chat/setup.SKILL.md +37 -0
  92. package/skins/oracle.yaml +52 -0
  93. package/src/cli/chain-catalog.mjs +215 -0
  94. package/src/cli/chain-state.mjs +74 -0
  95. package/src/cli/commands/chain.mjs +143 -0
  96. package/src/cli/commands/chat.mjs +268 -0
  97. package/src/cli/commands/credential.mjs +9 -0
  98. package/src/cli/commands/data-mcp.mjs +18 -0
  99. package/src/cli/commands/data.mjs +73 -0
  100. package/src/cli/commands/doctor.mjs +131 -0
  101. package/src/cli/commands/help.mjs +7 -0
  102. package/src/cli/commands/init.mjs +27 -0
  103. package/src/cli/commands/mcp.mjs +142 -0
  104. package/src/cli/commands/model.mjs +37 -0
  105. package/src/cli/commands/prepare.mjs +12 -0
  106. package/src/cli/commands/public.mjs +20 -0
  107. package/src/cli/commands/route.mjs +12 -0
  108. package/src/cli/commands/runner.mjs +9 -0
  109. package/src/cli/commands/scan.mjs +12 -0
  110. package/src/cli/commands/setup.mjs +283 -0
  111. package/src/cli/commands/sign.mjs +21 -0
  112. package/src/cli/commands/signer.mjs +9 -0
  113. package/src/cli/commands/upgrade.mjs +12 -0
  114. package/src/cli/commands/vault.mjs +9 -0
  115. package/src/cli/commands/version.mjs +22 -0
  116. package/src/cli/first-run.mjs +23 -0
  117. package/src/cli/kernel.mjs +237 -0
  118. package/src/cli/mcp-targets/chatgpt.mjs +51 -0
  119. package/src/cli/mcp-targets/claude-code.mjs +39 -0
  120. package/src/cli/mcp-targets/claude-desktop.mjs +24 -0
  121. package/src/cli/mcp-targets/codex.mjs +41 -0
  122. package/src/cli/mcp-targets/shared.mjs +61 -0
  123. package/src/cli/messaging-platforms.mjs +209 -0
  124. package/src/cli/operator-dispatch.mjs +258 -0
  125. package/src/cli/oracle-harness.py +148 -0
  126. package/src/cli/paths.mjs +83 -0
  127. package/src/cli/setup-state.mjs +168 -0
  128. package/src/cli/spawn-child.mjs +43 -0
  129. 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,148 @@
1
+ import importlib
2
+ import importlib.abc
3
+ import importlib.machinery
4
+ import re
5
+ import sys
6
+
7
+ LOGO = (
8
+ " _",
9
+ " ___ _ _ __ _ __| |___",
10
+ "/ _ \\ '_/ _` / _| / -_)",
11
+ "\\___/_| \\__,_\\__|_\\___|",
12
+ )
13
+
14
+ SILENT_STDERR = {
15
+ "[anthropic_billing_bypass] Bypass installed",
16
+ "[anthropic_billing_bypass] Transport unwrap hook installed",
17
+ }
18
+
19
+
20
+ class OracleStderr:
21
+ def __init__(self, wrapped):
22
+ self.wrapped = wrapped
23
+
24
+ def write(self, data):
25
+ kept = "".join(
26
+ line
27
+ for line in data.splitlines(keepends=True)
28
+ if line.rstrip("\r\n") not in SILENT_STDERR
29
+ )
30
+ if kept:
31
+ self.wrapped.write(kept)
32
+ return len(data)
33
+
34
+ def flush(self):
35
+ return self.wrapped.flush()
36
+
37
+ def __getattr__(self, name):
38
+ return getattr(self.wrapped, name)
39
+
40
+
41
+ def patch_cli(module):
42
+ hermes_cli = getattr(module, "HermesCLI")
43
+ original_layout = hermes_cli._build_tui_layout_children
44
+ original_emit = hermes_cli._emit_stream_text
45
+ original_flush = hermes_cli._flush_stream
46
+ original_cprint = getattr(module, "_cprint")
47
+ frame_filter = [0]
48
+
49
+ def cprint(text="", *args, **kwargs):
50
+ if frame_filter[0] and isinstance(text, str):
51
+ plain = re.sub(r"\x1b\[[0-9;]*m", "", text).lstrip("\n")
52
+ if plain.startswith("\u256d\u2500oracle") or plain.startswith("\u2570"):
53
+ return None
54
+ return original_cprint(text, *args, **kwargs)
55
+
56
+ setattr(module, "_cprint", cprint)
57
+
58
+ def show_banner(self):
59
+ self.console.clear()
60
+ self._console_print()
61
+ for line in LOGO:
62
+ self._console_print(f"[#e6edf3]{line}[/]")
63
+ self._console_print()
64
+
65
+ def prompt_fragments(self):
66
+ if self._approval_state or getattr(self, "_slash_confirm_state", None):
67
+ marker = "!"
68
+ elif self._clarify_state or self._clarify_freetext:
69
+ marker = "?"
70
+ elif self._command_running or self._agent_running:
71
+ marker = "..."
72
+ else:
73
+ marker = ">"
74
+ return [("class:prompt", f"oracle {marker} ")]
75
+
76
+ def layout_children(self, **kwargs):
77
+ kwargs["status_bar"] = None
78
+ kwargs["input_rule_top"] = None
79
+ kwargs["input_rule_bot"] = None
80
+ return original_layout(self, **kwargs)
81
+
82
+ def emit_stream_text(self, text):
83
+ first = not self._stream_box_opened
84
+ if first:
85
+ original_cprint()
86
+ frame_filter[0] += 1
87
+ try:
88
+ return original_emit(self, text)
89
+ finally:
90
+ frame_filter[0] -= 1
91
+
92
+ def flush_stream(self):
93
+ frame_filter[0] += 1
94
+ try:
95
+ return original_flush(self)
96
+ finally:
97
+ frame_filter[0] -= 1
98
+
99
+ hermes_cli.show_banner = show_banner
100
+ hermes_cli._get_tui_prompt_fragments = prompt_fragments
101
+ hermes_cli._build_tui_layout_children = layout_children
102
+ hermes_cli._emit_stream_text = emit_stream_text
103
+ hermes_cli._flush_stream = flush_stream
104
+ tips = importlib.import_module("hermes_cli.tips")
105
+
106
+ def no_tip():
107
+ raise LookupError("oracle quiet startup")
108
+
109
+ setattr(tips, "get_random_tip", no_tip)
110
+
111
+
112
+ class OracleLoader(importlib.abc.Loader):
113
+ def __init__(self, wrapped):
114
+ self.wrapped = wrapped
115
+
116
+ def create_module(self, spec):
117
+ if hasattr(self.wrapped, "create_module"):
118
+ return self.wrapped.create_module(spec)
119
+ return None
120
+
121
+ def exec_module(self, module):
122
+ self.wrapped.exec_module(module)
123
+ patch_cli(module)
124
+
125
+
126
+ class OracleFinder(importlib.abc.MetaPathFinder):
127
+ def find_spec(self, fullname, path, target=None):
128
+ if fullname != "cli":
129
+ return None
130
+ try:
131
+ sys.meta_path.remove(self)
132
+ except ValueError:
133
+ pass
134
+ spec = importlib.machinery.PathFinder.find_spec(fullname, path)
135
+ if spec and spec.loader:
136
+ spec.loader = OracleLoader(spec.loader)
137
+ return spec
138
+
139
+
140
+ def main():
141
+ sys.stderr = OracleStderr(sys.stderr)
142
+ sys.meta_path.insert(0, OracleFinder())
143
+ hermes_main = getattr(importlib.import_module("hermes_cli.main"), "main")
144
+ return hermes_main()
145
+
146
+
147
+ if __name__ == "__main__":
148
+ raise SystemExit(main())
@@ -0,0 +1,83 @@
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 activeChainPath() {
54
+ return path.join(oracleConfigDir(), "active-chain.json");
55
+ }
56
+
57
+ export function packageBin(name) {
58
+ return path.join(PACKAGE_ROOT, "bin", name);
59
+ }
60
+
61
+ export function ensureDir(dir) {
62
+ fs.mkdirSync(dir, { recursive: true });
63
+ return dir;
64
+ }
65
+
66
+ export const paths = {
67
+ homeDir,
68
+ oracleConfigDir,
69
+ oracleExecEnvPath,
70
+ hermesRoot,
71
+ activeChainPath,
72
+ claudeCodeUserConfigPath,
73
+ claudeCodeProjectMcpPath,
74
+ claudeDesktopConfigPath,
75
+ codexConfigPath,
76
+ chatgptConnectorDir,
77
+ chatgptOpenApiPath,
78
+ packageBin,
79
+ ensureDir,
80
+ PACKAGE_ROOT,
81
+ };
82
+
83
+ export default paths;
@@ -0,0 +1,168 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import {
5
+ listMessagingPlatforms,
6
+ findMessagingPlatform,
7
+ renderSetupMenu,
8
+ } from "./messaging-platforms.mjs";
9
+ import { hermesRoot, ensureDir, oracleConfigDir } from "./paths.mjs";
10
+
11
+ const SECRETISH = /(TOKEN|SECRET|PASSWORD|KEY|AUTH|PRIVATE|MNEMONIC|SEED)/i;
12
+
13
+ export function profileEnvPath(profile = "oracle") {
14
+ return path.join(hermesRoot(), "profiles", profile, ".env");
15
+ }
16
+
17
+ export function parseDotEnv(text) {
18
+ const out = {};
19
+ for (const line of String(text || "").split(/\r?\n/)) {
20
+ const trimmed = line.trim();
21
+ if (!trimmed || trimmed.startsWith("#")) continue;
22
+ const eq = trimmed.indexOf("=");
23
+ if (eq <= 0) continue;
24
+ const key = trimmed.slice(0, eq).trim();
25
+ let val = trimmed.slice(eq + 1).trim();
26
+ if (
27
+ (val.startsWith('"') && val.endsWith('"')) ||
28
+ (val.startsWith("'") && val.endsWith("'"))
29
+ ) {
30
+ val = val.slice(1, -1);
31
+ }
32
+ out[key] = val;
33
+ }
34
+ return out;
35
+ }
36
+
37
+ export function readProfileEnv(profile = "oracle") {
38
+ const p = profileEnvPath(profile);
39
+ if (!fs.existsSync(p)) return {};
40
+ return parseDotEnv(fs.readFileSync(p, "utf8"));
41
+ }
42
+
43
+ export function writeProfileEnvKey(profile, key, value) {
44
+ if (!key || SECRETISH.test(key) === false && /[^A-Z0-9_]/.test(key)) {
45
+ // keep simple env keys
46
+ }
47
+ if (!/^[A-Z][A-Z0-9_]*$/.test(key)) {
48
+ const err = new Error(`invalid env key '${key}'`);
49
+ err.code = "BAD_KEY";
50
+ throw err;
51
+ }
52
+ if (value == null || String(value).trim() === "") {
53
+ const err = new Error(`empty value for ${key}`);
54
+ err.code = "EMPTY";
55
+ throw err;
56
+ }
57
+ const p = profileEnvPath(profile);
58
+ ensureDir(path.dirname(p));
59
+ let text = fs.existsSync(p) ? fs.readFileSync(p, "utf8") : "";
60
+ const lines = text ? text.split(/\r?\n/) : [];
61
+ const next = [];
62
+ let replaced = false;
63
+ const assignment = `${key}=${String(value)}`;
64
+ for (const line of lines) {
65
+ if (line.startsWith(`${key}=`)) {
66
+ next.push(assignment);
67
+ replaced = true;
68
+ } else if (line.length || next.length) {
69
+ next.push(line);
70
+ }
71
+ }
72
+ if (!replaced) {
73
+ if (next.length && next[next.length - 1] !== "") next.push("");
74
+ next.push(assignment);
75
+ }
76
+ while (next.length && next[next.length - 1] === "") next.pop();
77
+ next.push("");
78
+ fs.writeFileSync(p, next.join("\n"), { mode: 0o600 });
79
+ try {
80
+ fs.chmodSync(p, 0o600);
81
+ } catch {
82
+ // best effort on platforms that ignore mode
83
+ }
84
+ return { path: p, key, set: true };
85
+ }
86
+
87
+ export function platformStatus(profile = "oracle") {
88
+ const env = readProfileEnv(profile);
89
+ const statuses = {};
90
+ for (const p of listMessagingPlatforms()) {
91
+ if (!p.required.length) {
92
+ statuses[p.key] = "manual";
93
+ continue;
94
+ }
95
+ const missing = p.required.filter((k) => !String(env[k] || "").trim());
96
+ statuses[p.key] = missing.length ? "unset" : "ready";
97
+ }
98
+ return { profile, statuses, envPath: profileEnvPath(profile) };
99
+ }
100
+
101
+ export function renderStatus(profile = "oracle") {
102
+ const { statuses } = platformStatus(profile);
103
+ return renderSetupMenu({ profile, statuses });
104
+ }
105
+
106
+ export function setPlatformToken(profile, platformKey, token, extra = {}) {
107
+ const platform = findMessagingPlatform(platformKey);
108
+ if (!platform) {
109
+ const err = new Error(`unknown platform '${platformKey}'`);
110
+ err.code = "UNKNOWN_PLATFORM";
111
+ throw err;
112
+ }
113
+ if (!platform.required.length) {
114
+ const err = new Error(
115
+ `${platform.key} uses hermes interactive setup (oracle setup messaging)`,
116
+ );
117
+ err.code = "NEEDS_INTERACTIVE";
118
+ throw err;
119
+ }
120
+ const primary = platform.required[0];
121
+ writeProfileEnvKey(profile, primary, token);
122
+ for (const [k, v] of Object.entries(extra || {})) {
123
+ if (v != null && String(v).trim()) writeProfileEnvKey(profile, k, v);
124
+ }
125
+ return {
126
+ platform: platform.key,
127
+ set: platform.required,
128
+ profile,
129
+ path: profileEnvPath(profile),
130
+ };
131
+ }
132
+
133
+ export function setupStatePath() {
134
+ return path.join(oracleConfigDir(), "setup-state.json");
135
+ }
136
+
137
+ export function touchSetupState(patch = {}) {
138
+ ensureDir(oracleConfigDir());
139
+ const p = setupStatePath();
140
+ let cur = {};
141
+ if (fs.existsSync(p)) {
142
+ try {
143
+ cur = JSON.parse(fs.readFileSync(p, "utf8"));
144
+ } catch {
145
+ cur = {};
146
+ }
147
+ }
148
+ const next = {
149
+ ...cur,
150
+ ...patch,
151
+ updatedAt: new Date().toISOString(),
152
+ host: os.hostname(),
153
+ };
154
+ fs.writeFileSync(p, JSON.stringify(next, null, 2) + "\n", { mode: 0o600 });
155
+ return next;
156
+ }
157
+
158
+ export default {
159
+ profileEnvPath,
160
+ parseDotEnv,
161
+ readProfileEnv,
162
+ writeProfileEnvKey,
163
+ platformStatus,
164
+ renderStatus,
165
+ setPlatformToken,
166
+ setupStatePath,
167
+ touchSetupState,
168
+ };
@@ -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
+ }