@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,209 @@
|
|
|
1
|
+
// Hermes messaging platforms Oracle can configure.
|
|
2
|
+
// Values stay lowercase. Secrets stay in Hermes profile .env, never printed.
|
|
3
|
+
|
|
4
|
+
export const MESSAGING_PLATFORMS = Object.freeze([
|
|
5
|
+
{
|
|
6
|
+
key: "telegram",
|
|
7
|
+
name: "telegram",
|
|
8
|
+
hermes: ["gateway", "setup"],
|
|
9
|
+
required: ["TELEGRAM_BOT_TOKEN"],
|
|
10
|
+
optional: ["TELEGRAM_ALLOWED_USERS", "TELEGRAM_PROXY"],
|
|
11
|
+
note: "dms · groups · topics",
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
key: "discord",
|
|
15
|
+
name: "discord",
|
|
16
|
+
hermes: ["gateway", "setup"],
|
|
17
|
+
required: ["DISCORD_BOT_TOKEN"],
|
|
18
|
+
optional: ["DISCORD_ALLOWED_USERS"],
|
|
19
|
+
note: "dms · channels · threads",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
key: "slack",
|
|
23
|
+
name: "slack",
|
|
24
|
+
hermes: ["gateway", "setup"],
|
|
25
|
+
required: ["SLACK_BOT_TOKEN", "SLACK_APP_TOKEN"],
|
|
26
|
+
optional: ["SLACK_ALLOWED_USERS"],
|
|
27
|
+
note: "socket mode",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
key: "whatsapp",
|
|
31
|
+
name: "whatsapp",
|
|
32
|
+
hermes: ["whatsapp"],
|
|
33
|
+
required: [],
|
|
34
|
+
optional: ["WHATSAPP_ALLOWED_USERS", "WHATSAPP_MODE"],
|
|
35
|
+
note: "qr bridge",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
key: "whatsapp-cloud",
|
|
39
|
+
name: "whatsapp cloud",
|
|
40
|
+
hermes: ["whatsapp-cloud"],
|
|
41
|
+
required: [],
|
|
42
|
+
optional: [],
|
|
43
|
+
note: "business cloud api",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
key: "signal",
|
|
47
|
+
name: "signal",
|
|
48
|
+
hermes: ["gateway", "setup"],
|
|
49
|
+
required: ["SIGNAL_HTTP_URL", "SIGNAL_ACCOUNT"],
|
|
50
|
+
optional: ["SIGNAL_ALLOWED_USERS"],
|
|
51
|
+
note: "signal-cli rest bridge",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
key: "matrix",
|
|
55
|
+
name: "matrix",
|
|
56
|
+
hermes: ["gateway", "setup"],
|
|
57
|
+
required: ["MATRIX_HOMESERVER", "MATRIX_ACCESS_TOKEN", "MATRIX_USER_ID"],
|
|
58
|
+
optional: ["MATRIX_ALLOWED_USERS"],
|
|
59
|
+
note: "rooms · dms",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
key: "mattermost",
|
|
63
|
+
name: "mattermost",
|
|
64
|
+
hermes: ["gateway", "setup"],
|
|
65
|
+
required: ["MATTERMOST_URL", "MATTERMOST_TOKEN"],
|
|
66
|
+
optional: ["MATTERMOST_ALLOWED_USERS"],
|
|
67
|
+
note: "channels · dms",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
key: "email",
|
|
71
|
+
name: "email",
|
|
72
|
+
hermes: ["gateway", "setup"],
|
|
73
|
+
required: ["EMAIL_ADDRESS", "EMAIL_PASSWORD", "EMAIL_IMAP_HOST", "EMAIL_SMTP_HOST"],
|
|
74
|
+
optional: [],
|
|
75
|
+
note: "imap/smtp mailbox",
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
key: "bluebubbles",
|
|
79
|
+
name: "bluebubbles",
|
|
80
|
+
hermes: ["gateway", "setup"],
|
|
81
|
+
required: [],
|
|
82
|
+
optional: [],
|
|
83
|
+
note: "imessage via bluebubbles",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
key: "homeassistant",
|
|
87
|
+
name: "homeassistant",
|
|
88
|
+
hermes: ["gateway", "setup"],
|
|
89
|
+
required: ["HASS_URL", "HASS_TOKEN"],
|
|
90
|
+
optional: [],
|
|
91
|
+
note: "smart home",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
key: "webhook",
|
|
95
|
+
name: "webhook",
|
|
96
|
+
hermes: ["gateway", "setup"],
|
|
97
|
+
required: [],
|
|
98
|
+
optional: [],
|
|
99
|
+
note: "http ingress",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
key: "api-server",
|
|
103
|
+
name: "api server",
|
|
104
|
+
hermes: ["gateway", "setup"],
|
|
105
|
+
required: [],
|
|
106
|
+
optional: [],
|
|
107
|
+
note: "local http api",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
key: "dingtalk",
|
|
111
|
+
name: "dingtalk",
|
|
112
|
+
hermes: ["gateway", "setup"],
|
|
113
|
+
required: [],
|
|
114
|
+
optional: [],
|
|
115
|
+
note: "dingtalk bot",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
key: "feishu",
|
|
119
|
+
name: "feishu",
|
|
120
|
+
hermes: ["gateway", "setup"],
|
|
121
|
+
required: [],
|
|
122
|
+
optional: [],
|
|
123
|
+
note: "feishu / lark",
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
key: "wecom",
|
|
127
|
+
name: "wecom",
|
|
128
|
+
hermes: ["gateway", "setup"],
|
|
129
|
+
required: [],
|
|
130
|
+
optional: [],
|
|
131
|
+
note: "wecom",
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
key: "weixin",
|
|
135
|
+
name: "weixin",
|
|
136
|
+
hermes: ["gateway", "setup"],
|
|
137
|
+
required: [],
|
|
138
|
+
optional: [],
|
|
139
|
+
note: "weixin",
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
key: "qqbot",
|
|
143
|
+
name: "qqbot",
|
|
144
|
+
hermes: ["gateway", "setup"],
|
|
145
|
+
required: [],
|
|
146
|
+
optional: [],
|
|
147
|
+
note: "qq bot",
|
|
148
|
+
},
|
|
149
|
+
]);
|
|
150
|
+
|
|
151
|
+
function normalize(raw) {
|
|
152
|
+
return String(raw || "")
|
|
153
|
+
.trim()
|
|
154
|
+
.toLowerCase()
|
|
155
|
+
.replace(/^\/+/, "")
|
|
156
|
+
.replace(/\s+/g, "-");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function listMessagingPlatforms() {
|
|
160
|
+
return MESSAGING_PLATFORMS.slice();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function findMessagingPlatform(query) {
|
|
164
|
+
const q = normalize(query);
|
|
165
|
+
if (!q) return null;
|
|
166
|
+
for (const p of MESSAGING_PLATFORMS) {
|
|
167
|
+
if (p.key === q) return p;
|
|
168
|
+
if (normalize(p.name) === q) return p;
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function renderSetupMenu({ profile = "oracle", statuses = {} } = {}) {
|
|
174
|
+
const lines = [
|
|
175
|
+
"oracle setup",
|
|
176
|
+
"",
|
|
177
|
+
"one oracle. many models. hermes messaging underneath.",
|
|
178
|
+
`profile: ${profile}`,
|
|
179
|
+
"",
|
|
180
|
+
"messaging platforms:",
|
|
181
|
+
"",
|
|
182
|
+
];
|
|
183
|
+
for (const p of MESSAGING_PLATFORMS) {
|
|
184
|
+
const st = statuses[p.key] || "unset";
|
|
185
|
+
lines.push(` ${p.key.padEnd(16)} ${String(st).padEnd(8)} ${p.note}`);
|
|
186
|
+
}
|
|
187
|
+
lines.push("");
|
|
188
|
+
lines.push("commands:");
|
|
189
|
+
lines.push(" oracle setup this menu");
|
|
190
|
+
lines.push(" oracle setup messaging open hermes gateway setup");
|
|
191
|
+
lines.push(" oracle setup telegram configure telegram bot token");
|
|
192
|
+
lines.push(" oracle setup discord configure discord bot token");
|
|
193
|
+
lines.push(" oracle setup status show configured platforms (no secrets)");
|
|
194
|
+
lines.push(" oracle setup gateway start/status hermes gateway");
|
|
195
|
+
lines.push(" /setup same menu inside chat");
|
|
196
|
+
lines.push("");
|
|
197
|
+
lines.push("notes:");
|
|
198
|
+
lines.push(" tokens stay in the hermes profile .env on this machine");
|
|
199
|
+
lines.push(" oracle never prints bot tokens or app secrets");
|
|
200
|
+
lines.push(" after setup: hermes -p oracle gateway restart");
|
|
201
|
+
return lines.join("\n") + "\n";
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export default {
|
|
205
|
+
MESSAGING_PLATFORMS,
|
|
206
|
+
listMessagingPlatforms,
|
|
207
|
+
findMessagingPlatform,
|
|
208
|
+
renderSetupMenu,
|
|
209
|
+
};
|
|
@@ -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())
|
package/src/cli/paths.mjs
CHANGED
|
@@ -50,6 +50,10 @@ export function chatgptOpenApiPath() {
|
|
|
50
50
|
return path.join(chatgptConnectorDir(), "chatgpt-openapi.json");
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
export function activeChainPath() {
|
|
54
|
+
return path.join(oracleConfigDir(), "active-chain.json");
|
|
55
|
+
}
|
|
56
|
+
|
|
53
57
|
export function packageBin(name) {
|
|
54
58
|
return path.join(PACKAGE_ROOT, "bin", name);
|
|
55
59
|
}
|
|
@@ -64,6 +68,7 @@ export const paths = {
|
|
|
64
68
|
oracleConfigDir,
|
|
65
69
|
oracleExecEnvPath,
|
|
66
70
|
hermesRoot,
|
|
71
|
+
activeChainPath,
|
|
67
72
|
claudeCodeUserConfigPath,
|
|
68
73
|
claudeCodeProjectMcpPath,
|
|
69
74
|
claudeDesktopConfigPath,
|
|
@@ -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
|
+
};
|