@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.
- package/README.md +15 -10
- package/bin/oracle-data-mcp.mjs +9 -0
- package/docs/architecture.md +3 -1
- package/docs/profiles.md +3 -6
- package/docs/public-surface.md +133 -0
- package/package.json +3 -2
- package/public/oracle-splash/assets/cli-chain-hyperliquid.jpg +0 -0
- package/public/oracle-splash/brand/aave.webp +0 -0
- package/public/oracle-splash/brand/aavev2.webp +0 -0
- package/public/oracle-splash/brand/aborean.webp +0 -0
- package/public/oracle-splash/brand/across.webp +0 -0
- package/public/oracle-splash/brand/aerodrome.webp +0 -0
- package/public/oracle-splash/brand/afi.webp +0 -0
- package/public/oracle-splash/brand/arcus.webp +0 -0
- package/public/oracle-splash/brand/aster.webp +0 -0
- package/public/oracle-splash/brand/avant.webp +0 -0
- package/public/oracle-splash/brand/balancer.webp +0 -0
- package/public/oracle-splash/brand/beefy.webp +0 -0
- package/public/oracle-splash/brand/benqi.webp +0 -0
- package/public/oracle-splash/brand/bestinslot.ico +0 -0
- package/public/oracle-splash/brand/bitfi.webp +0 -0
- package/public/oracle-splash/brand/bonkswap.webp +0 -0
- package/public/oracle-splash/brand/bouncebit.webp +0 -0
- package/public/oracle-splash/brand/camelot.webp +0 -0
- package/public/oracle-splash/brand/compound.webp +0 -0
- package/public/oracle-splash/brand/concrete.webp +0 -0
- package/public/oracle-splash/brand/cow.webp +0 -0
- package/public/oracle-splash/brand/curve.webp +0 -0
- package/public/oracle-splash/brand/derive.webp +0 -0
- package/public/oracle-splash/brand/dolomite.webp +0 -0
- package/public/oracle-splash/brand/eigenlayer.webp +0 -0
- package/public/oracle-splash/brand/ekubo.webp +0 -0
- package/public/oracle-splash/brand/ethena.webp +0 -0
- package/public/oracle-splash/brand/etherfi.webp +0 -0
- package/public/oracle-splash/brand/etherfiborrow.webp +0 -0
- package/public/oracle-splash/brand/felix.webp +0 -0
- package/public/oracle-splash/brand/fluid.webp +0 -0
- package/public/oracle-splash/brand/fluxbeam.webp +0 -0
- package/public/oracle-splash/brand/folks.webp +0 -0
- package/public/oracle-splash/brand/fortyacres.webp +0 -0
- package/public/oracle-splash/brand/gainsnetwork.webp +0 -0
- package/public/oracle-splash/brand/gassss.webp +0 -0
- package/public/oracle-splash/brand/gmx.webp +0 -0
- package/public/oracle-splash/brand/harmonix.webp +0 -0
- package/public/oracle-splash/brand/hlorderbook.webp +0 -0
- package/public/oracle-splash/brand/hlp.webp +0 -0
- package/public/oracle-splash/brand/hop.webp +0 -0
- package/public/oracle-splash/brand/hyperbeat.webp +0 -0
- package/public/oracle-splash/brand/hyperlend.webp +0 -0
- package/public/oracle-splash/brand/hyperliquid.webp +0 -0
- package/public/oracle-splash/brand/hyperliquidhpl.webp +0 -0
- package/public/oracle-splash/brand/hyperliquidspot.webp +0 -0
- package/public/oracle-splash/brand/hyperswap.webp +0 -0
- package/public/oracle-splash/brand/hypha.webp +0 -0
- package/public/oracle-splash/brand/joedex.webp +0 -0
- package/public/oracle-splash/brand/jupiter.webp +0 -0
- package/public/oracle-splash/brand/kelp.webp +0 -0
- package/public/oracle-splash/brand/kinetiq.webp +0 -0
- package/public/oracle-splash/brand/kmhype.webp +0 -0
- package/public/oracle-splash/brand/kona.webp +0 -0
- package/public/oracle-splash/brand/konalend.webp +0 -0
- package/public/oracle-splash/brand/lfj.webp +0 -0
- package/public/oracle-splash/brand/lido.webp +0 -0
- package/public/oracle-splash/brand/lifi.webp +0 -0
- package/public/oracle-splash/brand/lighter.webp +0 -0
- package/public/oracle-splash/brand/lista.webp +0 -0
- package/public/oracle-splash/brand/loopedhype.webp +0 -0
- package/public/oracle-splash/brand/lorenzo.webp +0 -0
- package/public/oracle-splash/brand/magic-eden.webp +0 -0
- package/public/oracle-splash/brand/magiceden.webp +0 -0
- package/public/oracle-splash/brand/mempool.png +0 -0
- package/public/oracle-splash/brand/meteora.webp +0 -0
- package/public/oracle-splash/brand/morpho.webp +0 -0
- package/public/oracle-splash/brand/myriad.webp +0 -0
- package/public/oracle-splash/brand/noxa.webp +0 -0
- package/public/oracle-splash/brand/odos.webp +0 -0
- package/public/oracle-splash/brand/oneinch.webp +0 -0
- package/public/oracle-splash/brand/openbook.webp +0 -0
- package/public/oracle-splash/brand/opensea.webp +0 -0
- package/public/oracle-splash/brand/orca.webp +0 -0
- package/public/oracle-splash/brand/ordinals.png +0 -0
- package/public/oracle-splash/brand/ostium.webp +0 -0
- package/public/oracle-splash/brand/pancakeswap.webp +0 -0
- package/public/oracle-splash/brand/pendle.webp +0 -0
- package/public/oracle-splash/brand/perena.webp +0 -0
- package/public/oracle-splash/brand/pharaoh.webp +0 -0
- package/public/oracle-splash/brand/phoenix.webp +0 -0
- package/public/oracle-splash/brand/polymarket.webp +0 -0
- package/public/oracle-splash/brand/polynomial.webp +0 -0
- package/public/oracle-splash/brand/projectx.webp +0 -0
- package/public/oracle-splash/brand/pumpfun.webp +0 -0
- package/public/oracle-splash/brand/quickswap.webp +0 -0
- package/public/oracle-splash/brand/rain.webp +0 -0
- package/public/oracle-splash/brand/raydium.webp +0 -0
- package/public/oracle-splash/brand/relay.webp +0 -0
- package/public/oracle-splash/brand/rocketpool.webp +0 -0
- package/public/oracle-splash/brand/saber.webp +0 -0
- package/public/oracle-splash/brand/sakuraswap.webp +0 -0
- package/public/oracle-splash/brand/sanctum.webp +0 -0
- package/public/oracle-splash/brand/satflow.png +0 -0
- package/public/oracle-splash/brand/sierra.webp +0 -0
- package/public/oracle-splash/brand/sky.webp +0 -0
- package/public/oracle-splash/brand/solv.webp +0 -0
- package/public/oracle-splash/brand/spark.webp +0 -0
- package/public/oracle-splash/brand/sparksavings.webp +0 -0
- package/public/oracle-splash/brand/sphere.webp +0 -0
- package/public/oracle-splash/brand/ssv.webp +0 -0
- package/public/oracle-splash/brand/stabble.webp +0 -0
- package/public/oracle-splash/brand/stableswap.webp +0 -0
- package/public/oracle-splash/brand/stakewise.webp +0 -0
- package/public/oracle-splash/brand/stargate.webp +0 -0
- package/public/oracle-splash/brand/sthype.webp +0 -0
- package/public/oracle-splash/brand/sushiswap.webp +0 -0
- package/public/oracle-splash/brand/unisat.png +0 -0
- package/public/oracle-splash/brand/uniswap.webp +0 -0
- package/public/oracle-splash/brand/uniswapv2.webp +0 -0
- package/public/oracle-splash/brand/uniswapv4.webp +0 -0
- package/public/oracle-splash/brand/unit.webp +0 -0
- package/public/oracle-splash/brand/velodrome.webp +0 -0
- package/public/oracle-splash/brand/velora.webp +0 -0
- package/public/oracle-splash/brand/venus.webp +0 -0
- package/public/oracle-splash/brand/wbeth.webp +0 -0
- package/public/oracle-splash/brand/xsy.webp +0 -0
- package/public/oracle-splash/brand/yieldyak.webp +0 -0
- package/public/oracle-splash/brand/zerobase.webp +0 -0
- package/public/oracle-splash/brand/zerox.webp +0 -0
- package/public/oracle-splash/index.html +337 -197
- package/scripts/check-doc-drift.mjs +11 -0
- package/scripts/e2e-solana-bitcoin.mjs +1 -1
- package/scripts/secret-scan.mjs +1 -1
- package/src/action-receipts.mjs +108 -11
- package/src/auth/oauth.mjs +672 -0
- package/src/cli/commands/auth.mjs +193 -0
- package/src/cli/commands/bootstrap.mjs +2 -2
- package/src/cli/commands/chat.mjs +97 -75
- package/src/cli/commands/data.mjs +3 -1
- package/src/cli/commands/doctor.mjs +11 -1
- package/src/cli/commands/model.mjs +79 -23
- package/src/cli/kernel.mjs +4 -3
- package/src/cli/model-config.mjs +70 -0
- package/src/data/catalog.mjs +38 -7
- package/src/data/desk-data.mjs +12 -0
- package/src/data/providers/evm-rpc.mjs +19 -16
- package/src/data/providers/jupiter-venues.mjs +119 -0
- package/src/data/providers/jupiter.mjs +132 -0
- package/src/data/providers/magiceden-sol.mjs +13 -0
- package/src/data/providers/morpho.mjs +6 -2
- package/src/data/providers/pendle.mjs +3 -2
- package/src/data/providers/satflow.mjs +4 -1
- package/src/data/providers/signed-material-guard.mjs +108 -0
- package/src/data/providers/uniswap-v3.mjs +34 -5
- package/src/data/providers/uniswap-v4.mjs +414 -0
- package/src/prepare-envelope.mjs +22 -4
- package/src/router/prepare-route.mjs +17 -0
- package/src/router/route-sources.mjs +35 -0
- package/src/tui/app.mjs +18 -7
- package/src/tui/backend.mjs +145 -0
- package/src/tui/standalone-client.mjs +666 -0
- package/src/cards.mjs +0 -369
- package/src/cli/__pycache__/oracle-harness.cpython-311.pyc +0 -0
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { EventEmitter } from "node:events";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
|
|
7
|
+
import { PACKAGE_ROOT } from "../cli/paths.mjs";
|
|
8
|
+
|
|
9
|
+
const ALLOWED_COMMANDS = new Set(["chain", "data", "scan", "route", "prepare"]);
|
|
10
|
+
const ALLOWED_CHAIN = new Set(["list", "show"]);
|
|
11
|
+
const ALLOWED_DATA = new Set(["call", "catalog", "health"]);
|
|
12
|
+
const ALLOWED_ROUTE = new Set(["swap", "bridge", "prepare", "prepare-bridge"]);
|
|
13
|
+
const OAUTH_BASE_URLS = Object.freeze({
|
|
14
|
+
"anthropic-oauth": "https://api.anthropic.com/v1",
|
|
15
|
+
"openai-codex": "https://chatgpt.com/backend-api/codex",
|
|
16
|
+
"xai-oauth": "https://api.x.ai/v1",
|
|
17
|
+
});
|
|
18
|
+
const MAX_TOOL_OUTPUT = 20_000;
|
|
19
|
+
const MAX_AGENT_STEPS = 6;
|
|
20
|
+
|
|
21
|
+
const ORACLE_TOOL = Object.freeze({
|
|
22
|
+
type: "function",
|
|
23
|
+
function: {
|
|
24
|
+
name: "oracle_cli",
|
|
25
|
+
description: "Run a bounded read or prepare-only Oracle CLI command. It cannot sign or broadcast transactions.",
|
|
26
|
+
parameters: {
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: {
|
|
29
|
+
argv: {
|
|
30
|
+
type: "array",
|
|
31
|
+
items: { type: "string" },
|
|
32
|
+
minItems: 1,
|
|
33
|
+
maxItems: 24,
|
|
34
|
+
description: "Arguments after the oracle executable, for example [\"scan\",\"chains\"] or [\"data\",\"health\"].",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
required: ["argv"],
|
|
38
|
+
additionalProperties: false,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
function trimOutput(value) {
|
|
44
|
+
const text = String(value || "");
|
|
45
|
+
if (text.length <= MAX_TOOL_OUTPUT) return text;
|
|
46
|
+
return `${text.slice(0, MAX_TOOL_OUTPUT)}\n[truncated ${text.length - MAX_TOOL_OUTPUT} chars]`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function validateCommand(argv) {
|
|
50
|
+
if (!Array.isArray(argv) || argv.length === 0 || argv.length > 24) {
|
|
51
|
+
return "argv must contain 1 to 24 arguments";
|
|
52
|
+
}
|
|
53
|
+
if (argv.some((arg) => typeof arg !== "string" || arg.length > 1_000)) {
|
|
54
|
+
return "every argument must be a string no longer than 1000 characters";
|
|
55
|
+
}
|
|
56
|
+
const command = argv[0];
|
|
57
|
+
if (!ALLOWED_COMMANDS.has(command)) return `command not allowed: ${command}`;
|
|
58
|
+
if (command === "chain" && !ALLOWED_CHAIN.has(argv[1] || "list")) {
|
|
59
|
+
return `command not allowed: chain${argv[1] ? ` ${argv[1]}` : ""}`;
|
|
60
|
+
}
|
|
61
|
+
if (command === "data" && !ALLOWED_DATA.has(argv[1] || "")) {
|
|
62
|
+
return `command not allowed: data${argv[1] ? ` ${argv[1]}` : ""}`;
|
|
63
|
+
}
|
|
64
|
+
if (command === "route" && !ALLOWED_ROUTE.has(argv[1] || "")) {
|
|
65
|
+
return `command not allowed: route${argv[1] ? ` ${argv[1]}` : ""}`;
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function runOracleCommand(argv, options = {}) {
|
|
71
|
+
const invalid = validateCommand(argv);
|
|
72
|
+
if (invalid) return { ok: false, error: invalid };
|
|
73
|
+
const spawnFn = options.spawnFn || spawnSync;
|
|
74
|
+
const root = options.packageRoot || PACKAGE_ROOT;
|
|
75
|
+
const result = spawnFn(
|
|
76
|
+
process.execPath,
|
|
77
|
+
[path.join(root, "bin", "oracle.mjs"), ...argv],
|
|
78
|
+
{
|
|
79
|
+
encoding: "utf8",
|
|
80
|
+
timeout: options.timeoutMs || 45_000,
|
|
81
|
+
maxBuffer: 1024 * 1024,
|
|
82
|
+
env: { ...process.env, ORACLE_CHAT_TOOL: "1" },
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
if (result.error) return { ok: false, error: result.error.message };
|
|
86
|
+
return {
|
|
87
|
+
ok: result.status === 0,
|
|
88
|
+
status: typeof result.status === "number" ? result.status : 1,
|
|
89
|
+
stdout: trimOutput(result.stdout),
|
|
90
|
+
stderr: trimOutput(result.stderr),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function defaultPersona() {
|
|
95
|
+
const file = path.join(PACKAGE_ROOT, "profiles", "oracle", "SOUL.md");
|
|
96
|
+
const soul = fs.readFileSync(file, "utf8");
|
|
97
|
+
return `${soul.trim()}\n\nYou have one bounded tool named oracle_cli. Use it for live chain facts, scans, route comparison, and unsigned preparation. Never claim a tool action succeeded unless its returned output proves it.`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function eventFrame(type, sessionId, payload = {}) {
|
|
101
|
+
return {
|
|
102
|
+
method: "event",
|
|
103
|
+
params: {
|
|
104
|
+
type,
|
|
105
|
+
session_id: sessionId,
|
|
106
|
+
payload,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function responseError(response) {
|
|
112
|
+
return new Error(`model request failed (HTTP ${response?.status || "unknown"})`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function* parseSse(response) {
|
|
116
|
+
if (!response?.ok) throw await responseError(response);
|
|
117
|
+
if (!response.body) throw new Error("model response had no body");
|
|
118
|
+
const reader = response.body.getReader();
|
|
119
|
+
const decoder = new TextDecoder();
|
|
120
|
+
let buffer = "";
|
|
121
|
+
while (true) {
|
|
122
|
+
const { value, done } = await reader.read();
|
|
123
|
+
buffer += decoder.decode(value || new Uint8Array(), { stream: !done });
|
|
124
|
+
const blocks = buffer.split(/\r?\n\r?\n/);
|
|
125
|
+
buffer = blocks.pop() || "";
|
|
126
|
+
for (const block of blocks) {
|
|
127
|
+
const data = block
|
|
128
|
+
.split(/\r?\n/)
|
|
129
|
+
.filter((line) => line.startsWith("data:"))
|
|
130
|
+
.map((line) => line.slice(5).trimStart())
|
|
131
|
+
.join("\n");
|
|
132
|
+
if (!data || data === "[DONE]") continue;
|
|
133
|
+
yield JSON.parse(data);
|
|
134
|
+
}
|
|
135
|
+
if (done) break;
|
|
136
|
+
}
|
|
137
|
+
const tail = buffer.trim();
|
|
138
|
+
if (tail.startsWith("data:")) {
|
|
139
|
+
const data = tail.slice(5).trim();
|
|
140
|
+
if (data && data !== "[DONE]") yield JSON.parse(data);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function mergeToolDelta(map, delta) {
|
|
145
|
+
const index = Number(delta.index || 0);
|
|
146
|
+
const current = map.get(index) || {
|
|
147
|
+
id: "",
|
|
148
|
+
type: "function",
|
|
149
|
+
function: { name: "", arguments: "" },
|
|
150
|
+
};
|
|
151
|
+
if (delta.id) current.id += delta.id;
|
|
152
|
+
if (delta.type) current.type = delta.type;
|
|
153
|
+
if (delta.function?.name) current.function.name += delta.function.name;
|
|
154
|
+
if (delta.function?.arguments) current.function.arguments += delta.function.arguments;
|
|
155
|
+
map.set(index, current);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function usagePayload(usage, contextLength) {
|
|
159
|
+
const input = Number(usage?.prompt_tokens || usage?.input_tokens || 0);
|
|
160
|
+
const output = Number(usage?.completion_tokens || usage?.output_tokens || 0);
|
|
161
|
+
return {
|
|
162
|
+
context_used: input,
|
|
163
|
+
context_max: contextLength,
|
|
164
|
+
context_percent: contextLength > 0 ? Math.round((input / contextLength) * 100) : 0,
|
|
165
|
+
input,
|
|
166
|
+
output,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function responseTool() {
|
|
171
|
+
return {
|
|
172
|
+
type: "function",
|
|
173
|
+
name: ORACLE_TOOL.function.name,
|
|
174
|
+
description: ORACLE_TOOL.function.description,
|
|
175
|
+
parameters: ORACLE_TOOL.function.parameters,
|
|
176
|
+
strict: false,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function anthropicTool() {
|
|
181
|
+
return {
|
|
182
|
+
name: ORACLE_TOOL.function.name,
|
|
183
|
+
description: ORACLE_TOOL.function.description,
|
|
184
|
+
input_schema: ORACLE_TOOL.function.parameters,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function appendAnthropicMessage(output, role, blocks) {
|
|
189
|
+
const normalized = Array.isArray(blocks) ? blocks : [{ type: "text", text: String(blocks || "") }];
|
|
190
|
+
const previous = output.at(-1);
|
|
191
|
+
if (previous?.role === role) previous.content.push(...normalized);
|
|
192
|
+
else output.push({ role, content: normalized });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function toAnthropicMessages(messages) {
|
|
196
|
+
const output = [];
|
|
197
|
+
for (const message of messages) {
|
|
198
|
+
if (message.role === "system") continue;
|
|
199
|
+
if (message.role === "user") {
|
|
200
|
+
appendAnthropicMessage(output, "user", [{ type: "text", text: String(message.content || "") }]);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (message.role === "assistant") {
|
|
204
|
+
const blocks = [];
|
|
205
|
+
if (message.content) blocks.push({ type: "text", text: String(message.content) });
|
|
206
|
+
for (const call of message.tool_calls || []) {
|
|
207
|
+
let input = {};
|
|
208
|
+
try { input = JSON.parse(call.function?.arguments || "{}"); } catch {}
|
|
209
|
+
blocks.push({
|
|
210
|
+
type: "tool_use",
|
|
211
|
+
id: call.id,
|
|
212
|
+
name: call.function?.name,
|
|
213
|
+
input,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
if (blocks.length) appendAnthropicMessage(output, "assistant", blocks);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (message.role === "tool") {
|
|
220
|
+
appendAnthropicMessage(output, "user", [{
|
|
221
|
+
type: "tool_result",
|
|
222
|
+
tool_use_id: message.tool_call_id,
|
|
223
|
+
content: String(message.content || ""),
|
|
224
|
+
}]);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return output;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function toResponsesInput(messages) {
|
|
231
|
+
const output = [];
|
|
232
|
+
for (const message of messages) {
|
|
233
|
+
if (message.role === "system") continue;
|
|
234
|
+
if (message.role === "user") {
|
|
235
|
+
output.push({ role: "user", content: [{ type: "input_text", text: String(message.content || "") }] });
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (message.role === "assistant") {
|
|
239
|
+
if (message.content) {
|
|
240
|
+
output.push({
|
|
241
|
+
role: "assistant",
|
|
242
|
+
status: "completed",
|
|
243
|
+
content: [{ type: "output_text", text: String(message.content) }],
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
for (const call of message.tool_calls || []) {
|
|
247
|
+
output.push({
|
|
248
|
+
type: "function_call",
|
|
249
|
+
id: call.response_item_id || undefined,
|
|
250
|
+
call_id: call.id,
|
|
251
|
+
name: call.function?.name,
|
|
252
|
+
arguments: call.function?.arguments || "{}",
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (message.role === "tool") {
|
|
258
|
+
output.push({
|
|
259
|
+
type: "function_call_output",
|
|
260
|
+
call_id: message.tool_call_id,
|
|
261
|
+
output: String(message.content || ""),
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return output.map((item) => Object.fromEntries(Object.entries(item).filter(([, value]) => value !== undefined)));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function accountIdFromJwt(token) {
|
|
269
|
+
try {
|
|
270
|
+
const payload = JSON.parse(Buffer.from(String(token).split(".")[1], "base64url").toString("utf8"));
|
|
271
|
+
return String(payload?.["https://api.openai.com/auth"]?.chatgpt_account_id || "");
|
|
272
|
+
} catch {
|
|
273
|
+
return "";
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function chatCompletionStep({ config, messages, fetchFn, signal, onText }) {
|
|
278
|
+
const response = await fetchFn(`${config.baseUrl}/chat/completions`, {
|
|
279
|
+
method: "POST",
|
|
280
|
+
headers: {
|
|
281
|
+
"content-type": "application/json",
|
|
282
|
+
...(config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {}),
|
|
283
|
+
...(config.provider === "openrouter" ? {
|
|
284
|
+
"HTTP-Referer": "https://oracle-agent.dev",
|
|
285
|
+
"X-Title": "Oracle CLI",
|
|
286
|
+
} : {}),
|
|
287
|
+
},
|
|
288
|
+
body: JSON.stringify({
|
|
289
|
+
model: config.model,
|
|
290
|
+
messages,
|
|
291
|
+
tools: [ORACLE_TOOL],
|
|
292
|
+
tool_choice: "auto",
|
|
293
|
+
stream: true,
|
|
294
|
+
stream_options: { include_usage: true },
|
|
295
|
+
}),
|
|
296
|
+
signal,
|
|
297
|
+
});
|
|
298
|
+
const toolDeltas = new Map();
|
|
299
|
+
let text = "";
|
|
300
|
+
let usage = null;
|
|
301
|
+
for await (const frame of parseSse(response)) {
|
|
302
|
+
if (frame.usage) usage = frame.usage;
|
|
303
|
+
const delta = frame.choices?.[0]?.delta || {};
|
|
304
|
+
if (typeof delta.content === "string" && delta.content) {
|
|
305
|
+
text += delta.content;
|
|
306
|
+
onText(delta.content);
|
|
307
|
+
}
|
|
308
|
+
for (const toolDelta of delta.tool_calls || []) mergeToolDelta(toolDeltas, toolDelta);
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
text,
|
|
312
|
+
usage,
|
|
313
|
+
toolCalls: [...toolDeltas.entries()].sort(([a], [b]) => a - b).map(([, value]) => value),
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function anthropicStep({ config, messages, persona, credential, fetchFn, signal, onText }) {
|
|
318
|
+
const response = await fetchFn(`${config.baseUrl}/messages`, {
|
|
319
|
+
method: "POST",
|
|
320
|
+
headers: {
|
|
321
|
+
"content-type": "application/json",
|
|
322
|
+
accept: "text/event-stream",
|
|
323
|
+
Authorization: `Bearer ${credential.accessToken}`,
|
|
324
|
+
"anthropic-version": "2023-06-01",
|
|
325
|
+
"anthropic-beta": "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14,claude-code-20250219,oauth-2025-04-20",
|
|
326
|
+
"user-agent": `claude-code/${process.env.ORACLE_CLAUDE_CODE_VERSION || "2.1.220"} (external, cli)`,
|
|
327
|
+
"x-app": "cli",
|
|
328
|
+
},
|
|
329
|
+
body: JSON.stringify({
|
|
330
|
+
model: config.model,
|
|
331
|
+
max_tokens: Number(config.maxOutputTokens || 8192),
|
|
332
|
+
system: [
|
|
333
|
+
{ type: "text", text: "You are Claude Code, Anthropic's official CLI for Claude." },
|
|
334
|
+
{ type: "text", text: persona },
|
|
335
|
+
],
|
|
336
|
+
messages: toAnthropicMessages(messages),
|
|
337
|
+
tools: [anthropicTool()],
|
|
338
|
+
tool_choice: { type: "auto" },
|
|
339
|
+
stream: true,
|
|
340
|
+
}),
|
|
341
|
+
signal,
|
|
342
|
+
});
|
|
343
|
+
const toolCalls = new Map();
|
|
344
|
+
let text = "";
|
|
345
|
+
let usage = null;
|
|
346
|
+
for await (const frame of parseSse(response)) {
|
|
347
|
+
if (frame.type === "message_start") usage = { ...(usage || {}), ...(frame.message?.usage || {}) };
|
|
348
|
+
if (frame.type === "message_delta") usage = { ...(usage || {}), ...(frame.usage || {}) };
|
|
349
|
+
if (frame.type === "content_block_start" && frame.content_block?.type === "tool_use") {
|
|
350
|
+
const block = frame.content_block;
|
|
351
|
+
toolCalls.set(Number(frame.index || 0), {
|
|
352
|
+
id: block.id,
|
|
353
|
+
type: "function",
|
|
354
|
+
function: {
|
|
355
|
+
name: block.name,
|
|
356
|
+
arguments: block.input && Object.keys(block.input).length ? JSON.stringify(block.input) : "",
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
if (frame.type === "content_block_delta" && frame.delta?.type === "text_delta") {
|
|
361
|
+
text += frame.delta.text || "";
|
|
362
|
+
if (frame.delta.text) onText(frame.delta.text);
|
|
363
|
+
}
|
|
364
|
+
if (frame.type === "content_block_delta" && frame.delta?.type === "input_json_delta") {
|
|
365
|
+
const index = Number(frame.index || 0);
|
|
366
|
+
const call = toolCalls.get(index);
|
|
367
|
+
if (call) call.function.arguments += frame.delta.partial_json || "";
|
|
368
|
+
}
|
|
369
|
+
if (frame.type === "error") throw new Error(frame.error?.message || "Claude stream failed");
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
text,
|
|
373
|
+
usage,
|
|
374
|
+
toolCalls: [...toolCalls.entries()].sort(([a], [b]) => a - b).map(([, value]) => {
|
|
375
|
+
if (!value.function.arguments) value.function.arguments = "{}";
|
|
376
|
+
return value;
|
|
377
|
+
}),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async function responsesStep({ config, messages, persona, sessionId, credential, fetchFn, signal, onText }) {
|
|
382
|
+
const isCodex = config.provider === "openai-codex";
|
|
383
|
+
const accountId = isCodex ? accountIdFromJwt(credential.accessToken) : "";
|
|
384
|
+
const headers = {
|
|
385
|
+
"content-type": "application/json",
|
|
386
|
+
accept: "text/event-stream",
|
|
387
|
+
Authorization: `Bearer ${credential.accessToken}`,
|
|
388
|
+
...(isCodex ? {
|
|
389
|
+
"user-agent": "codex_cli_rs/0.0.0 (Oracle CLI)",
|
|
390
|
+
originator: "codex_cli_rs",
|
|
391
|
+
...(accountId ? { "ChatGPT-Account-ID": accountId } : {}),
|
|
392
|
+
session_id: sessionId,
|
|
393
|
+
"x-client-request-id": sessionId,
|
|
394
|
+
} : {
|
|
395
|
+
"x-grok-conv-id": sessionId,
|
|
396
|
+
}),
|
|
397
|
+
};
|
|
398
|
+
const body = {
|
|
399
|
+
model: config.model,
|
|
400
|
+
instructions: persona,
|
|
401
|
+
input: toResponsesInput(messages),
|
|
402
|
+
tools: [responseTool()],
|
|
403
|
+
tool_choice: "auto",
|
|
404
|
+
parallel_tool_calls: true,
|
|
405
|
+
store: false,
|
|
406
|
+
stream: true,
|
|
407
|
+
...(isCodex ? { reasoning: { effort: config.reasoningEffort || "high", summary: "auto" } } : {}),
|
|
408
|
+
};
|
|
409
|
+
const response = await fetchFn(`${config.baseUrl}/responses`, {
|
|
410
|
+
method: "POST",
|
|
411
|
+
headers,
|
|
412
|
+
body: JSON.stringify(body),
|
|
413
|
+
signal,
|
|
414
|
+
});
|
|
415
|
+
const toolCalls = new Map();
|
|
416
|
+
let text = "";
|
|
417
|
+
let usage = null;
|
|
418
|
+
for await (const frame of parseSse(response)) {
|
|
419
|
+
if (frame.type === "response.output_text.delta" && typeof frame.delta === "string") {
|
|
420
|
+
text += frame.delta;
|
|
421
|
+
onText(frame.delta);
|
|
422
|
+
}
|
|
423
|
+
const item = frame.item;
|
|
424
|
+
if ((frame.type === "response.output_item.added" || frame.type === "response.output_item.done") && item?.type === "function_call") {
|
|
425
|
+
const key = String(item.call_id || item.id || frame.output_index || toolCalls.size);
|
|
426
|
+
toolCalls.set(key, {
|
|
427
|
+
id: item.call_id || item.id,
|
|
428
|
+
response_item_id: item.id,
|
|
429
|
+
type: "function",
|
|
430
|
+
function: { name: item.name, arguments: item.arguments || "{}" },
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
if (frame.type === "response.function_call_arguments.delta") {
|
|
434
|
+
const key = String(frame.call_id || frame.item_id || frame.output_index || "");
|
|
435
|
+
const call = toolCalls.get(key);
|
|
436
|
+
if (call) call.function.arguments += frame.delta || "";
|
|
437
|
+
}
|
|
438
|
+
if (frame.type === "response.completed") usage = frame.response?.usage || usage;
|
|
439
|
+
if (frame.type === "response.failed" || frame.type === "error") {
|
|
440
|
+
throw new Error(frame.response?.error?.message || frame.error?.message || "Responses stream failed");
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return { text, usage, toolCalls: [...toolCalls.values()] };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export function createStandaloneClient(options = {}) {
|
|
447
|
+
const config = options.config;
|
|
448
|
+
if (!config?.baseUrl || !config?.model) throw new Error("standalone model configuration is incomplete");
|
|
449
|
+
if (config.authType === "oauth") {
|
|
450
|
+
const expected = OAUTH_BASE_URLS[config.provider];
|
|
451
|
+
const actual = String(config.baseUrl).replace(/\/+$/, "");
|
|
452
|
+
if (!expected || actual !== expected) throw new Error(`${config.provider || "unknown"} OAuth endpoint is not trusted`);
|
|
453
|
+
}
|
|
454
|
+
const emitter = new EventEmitter();
|
|
455
|
+
const fetchFn = options.fetchFn || globalThis.fetch;
|
|
456
|
+
const toolRunner = options.toolRunner || ((argv) => runOracleCommand(argv, options));
|
|
457
|
+
const credentialProvider = options.credentialProvider || (async (provider) => {
|
|
458
|
+
const { resolveOAuthCredentials } = await import("../auth/oauth.mjs");
|
|
459
|
+
return resolveOAuthCredentials(provider);
|
|
460
|
+
});
|
|
461
|
+
const persona = options.persona || defaultPersona();
|
|
462
|
+
const sessionId = options.sessionId || `oracle-${randomUUID()}`;
|
|
463
|
+
let history = [];
|
|
464
|
+
let controller = null;
|
|
465
|
+
let stopped = false;
|
|
466
|
+
let active = false;
|
|
467
|
+
|
|
468
|
+
function emitEvent(type, payload = {}) {
|
|
469
|
+
emitter.emit("event", eventFrame(type, sessionId, payload));
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function emitError(error) {
|
|
473
|
+
emitEvent("error", { message: error?.message || String(error) });
|
|
474
|
+
if (emitter.listenerCount("error") > 0) emitter.emit("error", error);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async function performTurn(text) {
|
|
478
|
+
active = true;
|
|
479
|
+
controller = new AbortController();
|
|
480
|
+
const messages = [
|
|
481
|
+
{ role: "system", content: persona },
|
|
482
|
+
...history,
|
|
483
|
+
{ role: "user", content: text },
|
|
484
|
+
];
|
|
485
|
+
let finalText = "";
|
|
486
|
+
let usage = null;
|
|
487
|
+
let messageStarted = false;
|
|
488
|
+
|
|
489
|
+
try {
|
|
490
|
+
for (let step = 0; step < MAX_AGENT_STEPS; step += 1) {
|
|
491
|
+
const credential = config.authType === "oauth"
|
|
492
|
+
? await credentialProvider(config.provider)
|
|
493
|
+
: null;
|
|
494
|
+
const onText = (delta) => {
|
|
495
|
+
if (!messageStarted) {
|
|
496
|
+
emitEvent("message.start", {});
|
|
497
|
+
messageStarted = true;
|
|
498
|
+
}
|
|
499
|
+
finalText += delta;
|
|
500
|
+
emitEvent("message.delta", { text: delta });
|
|
501
|
+
};
|
|
502
|
+
const request = {
|
|
503
|
+
config,
|
|
504
|
+
messages,
|
|
505
|
+
persona,
|
|
506
|
+
sessionId,
|
|
507
|
+
credential,
|
|
508
|
+
fetchFn,
|
|
509
|
+
signal: controller.signal,
|
|
510
|
+
onText,
|
|
511
|
+
};
|
|
512
|
+
const result = config.protocol === "anthropic-messages"
|
|
513
|
+
? await anthropicStep(request)
|
|
514
|
+
: config.protocol === "responses"
|
|
515
|
+
? await responsesStep(request)
|
|
516
|
+
: await chatCompletionStep(request);
|
|
517
|
+
const stepText = result.text;
|
|
518
|
+
const toolCalls = result.toolCalls;
|
|
519
|
+
usage = result.usage || usage;
|
|
520
|
+
|
|
521
|
+
if (toolCalls.length === 0) {
|
|
522
|
+
messages.push({ role: "assistant", content: stepText });
|
|
523
|
+
history = messages.slice(1);
|
|
524
|
+
if (!messageStarted) emitEvent("message.start", {});
|
|
525
|
+
emitEvent("message.complete", {
|
|
526
|
+
text: finalText,
|
|
527
|
+
usage: usagePayload(usage, config.contextLength),
|
|
528
|
+
});
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
messages.push({ role: "assistant", content: stepText || null, tool_calls: toolCalls });
|
|
533
|
+
for (const call of toolCalls) {
|
|
534
|
+
let result;
|
|
535
|
+
try {
|
|
536
|
+
const parsed = JSON.parse(call.function.arguments || "{}");
|
|
537
|
+
result = call.function.name === "oracle_cli"
|
|
538
|
+
? await toolRunner(parsed.argv)
|
|
539
|
+
: { ok: false, error: `unknown tool: ${call.function.name}` };
|
|
540
|
+
} catch (error) {
|
|
541
|
+
result = { ok: false, error: `invalid tool arguments: ${error.message}` };
|
|
542
|
+
}
|
|
543
|
+
messages.push({
|
|
544
|
+
role: "tool",
|
|
545
|
+
tool_call_id: call.id || `call_${randomUUID()}`,
|
|
546
|
+
content: JSON.stringify(result),
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
throw new Error(`agent exceeded ${MAX_AGENT_STEPS} tool steps`);
|
|
551
|
+
} catch (error) {
|
|
552
|
+
if (error?.name !== "AbortError") emitError(error);
|
|
553
|
+
} finally {
|
|
554
|
+
active = false;
|
|
555
|
+
controller = null;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async function start() {
|
|
560
|
+
if (stopped) throw new Error("standalone client is stopped");
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async function request(method, params = {}) {
|
|
564
|
+
if (stopped) throw new Error("standalone client is stopped");
|
|
565
|
+
if (method === "session.create") {
|
|
566
|
+
emitEvent("session.info", {
|
|
567
|
+
model: params.model || config.model,
|
|
568
|
+
reasoning_effort: config.reasoningEffort,
|
|
569
|
+
usage: { context_max: config.contextLength },
|
|
570
|
+
backend: "standalone",
|
|
571
|
+
});
|
|
572
|
+
return {
|
|
573
|
+
session_id: sessionId,
|
|
574
|
+
info: {
|
|
575
|
+
model: params.model || config.model,
|
|
576
|
+
reasoning_effort: config.reasoningEffort,
|
|
577
|
+
usage: { context_max: config.contextLength },
|
|
578
|
+
backend: "standalone",
|
|
579
|
+
},
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
if (method === "prompt.submit") {
|
|
583
|
+
if (active) throw new Error("a turn is already active");
|
|
584
|
+
const text = String(params.text || "").trim();
|
|
585
|
+
if (!text) throw new Error("prompt text is required");
|
|
586
|
+
queueMicrotask(() => { performTurn(text).catch(emitError); });
|
|
587
|
+
return { accepted: true };
|
|
588
|
+
}
|
|
589
|
+
if (method === "session.interrupt") {
|
|
590
|
+
const interrupted = Boolean(controller && active);
|
|
591
|
+
controller?.abort();
|
|
592
|
+
return { interrupted };
|
|
593
|
+
}
|
|
594
|
+
if (method === "session.usage") {
|
|
595
|
+
return { calls: 0, input: 0, output: 0, total: 0 };
|
|
596
|
+
}
|
|
597
|
+
throw new Error(`unsupported standalone method: ${method}`);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
async function stop() {
|
|
601
|
+
if (stopped) return;
|
|
602
|
+
stopped = true;
|
|
603
|
+
controller?.abort();
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
return Object.freeze({
|
|
607
|
+
start,
|
|
608
|
+
request,
|
|
609
|
+
stop,
|
|
610
|
+
on: emitter.on.bind(emitter),
|
|
611
|
+
off: emitter.off.bind(emitter),
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
export async function runStandaloneQuery({
|
|
616
|
+
client,
|
|
617
|
+
text,
|
|
618
|
+
stdout = process.stdout,
|
|
619
|
+
stderr = process.stderr,
|
|
620
|
+
} = {}) {
|
|
621
|
+
if (!client) throw new Error("standalone client is required");
|
|
622
|
+
await client.start();
|
|
623
|
+
const session = await client.request("session.create", { source: "cli" });
|
|
624
|
+
let written = "";
|
|
625
|
+
let settled = false;
|
|
626
|
+
|
|
627
|
+
try {
|
|
628
|
+
return await new Promise((resolve, reject) => {
|
|
629
|
+
const handle = (frame) => {
|
|
630
|
+
const type = frame?.params?.type;
|
|
631
|
+
const payload = frame?.params?.payload || {};
|
|
632
|
+
if (type === "message.delta" && typeof payload.text === "string") {
|
|
633
|
+
written += payload.text;
|
|
634
|
+
stdout.write(payload.text);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
if (type === "message.complete") {
|
|
638
|
+
if (!written && payload.text) stdout.write(String(payload.text));
|
|
639
|
+
stdout.write("\n");
|
|
640
|
+
settled = true;
|
|
641
|
+
resolve(0);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
if (type === "error") {
|
|
645
|
+
const message = payload.message || payload.text || "standalone chat failed";
|
|
646
|
+
stderr.write(`oracle chat: ${message}\n`);
|
|
647
|
+
settled = true;
|
|
648
|
+
reject(new Error(message));
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
client.on("event", handle);
|
|
652
|
+
client.request("prompt.submit", {
|
|
653
|
+
session_id: session.session_id,
|
|
654
|
+
text,
|
|
655
|
+
}).catch((error) => {
|
|
656
|
+
if (settled) return;
|
|
657
|
+
settled = true;
|
|
658
|
+
reject(error);
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
} finally {
|
|
662
|
+
await client.stop();
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
export default { createStandaloneClient, runOracleCommand, runStandaloneQuery };
|