@oracle-agent/oracle 0.9.5 → 0.9.7
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/package.json +1 -1
- package/src/cli/__pycache__/oracle-harness.cpython-311.pyc +0 -0
- package/src/cli/commands/chat.mjs +31 -2
- package/src/cli/oracle-harness.py +36 -12
- package/src/tui/app.mjs +427 -0
- package/src/tui/format.mjs +272 -0
- package/src/tui/gateway-client.mjs +177 -0
- package/src/tui/input.mjs +501 -0
- package/src/tui/renderer.mjs +443 -0
- package/src/tui/theme.mjs +81 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oracle-agent/oracle",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.7",
|
|
4
4
|
"description": "Oracle: prepare-only multichain agent control plane. Policy-bounded intents for a user-signed wallet. Self-custody by default — the public package never takes your key. Built for Hermes; no model key required.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
Binary file
|
|
@@ -5,6 +5,7 @@ import { PACKAGE_ROOT, hermesRoot, ensureDir } from "../paths.mjs";
|
|
|
5
5
|
import { activeChainEnv } from "../chain-state.mjs";
|
|
6
6
|
import { spawnChild } from "../spawn-child.mjs";
|
|
7
7
|
import { installManagedHermes, resolveHermes } from "../runtime.mjs";
|
|
8
|
+
import { runOracleTui } from "../../tui/app.mjs";
|
|
8
9
|
|
|
9
10
|
const PROFILE = "oracle";
|
|
10
11
|
const SKIN_NAME = "oracle";
|
|
@@ -241,7 +242,7 @@ function modelReady(profile = PROFILE) {
|
|
|
241
242
|
|
|
242
243
|
function usage() {
|
|
243
244
|
return [
|
|
244
|
-
"oracle chat
|
|
245
|
+
"oracle chat, filled lowercase oracle chat",
|
|
245
246
|
"",
|
|
246
247
|
" oracle chat open interactive oracle chat",
|
|
247
248
|
" oracle chat -q \"...\" one-shot query",
|
|
@@ -257,6 +258,23 @@ function usage() {
|
|
|
257
258
|
].join("\n");
|
|
258
259
|
}
|
|
259
260
|
|
|
261
|
+
function terminalColumns() {
|
|
262
|
+
const columns = Number(process.stdout?.columns || process.env.COLUMNS || 80);
|
|
263
|
+
return Number.isFinite(columns) && columns > 0 ? Math.floor(columns) : 80;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function visibleWidth(text) {
|
|
267
|
+
return Array.from(String(text)).length;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function centerBlock(lines, width) {
|
|
271
|
+
const clean = lines.map((line) => line.trimEnd());
|
|
272
|
+
const blockWidth = Math.max(0, ...clean.map((line) => visibleWidth(line)));
|
|
273
|
+
const left = Math.max(0, Math.floor((width - blockWidth) / 2));
|
|
274
|
+
const pad = " ".repeat(left);
|
|
275
|
+
return clean.map((line) => (line ? `${pad}${line}` : ""));
|
|
276
|
+
}
|
|
277
|
+
|
|
260
278
|
function printBanner() {
|
|
261
279
|
const lines = [
|
|
262
280
|
"",
|
|
@@ -270,7 +288,7 @@ function printBanner() {
|
|
|
270
288
|
" ░░░░░░ ░░░░░ ░░░░░░░░ ░░░░░░ ░░░░░ ░░░░░░",
|
|
271
289
|
"",
|
|
272
290
|
];
|
|
273
|
-
process.stdout.write(lines.join("\n") + "\n");
|
|
291
|
+
process.stdout.write(centerBlock(lines, terminalColumns()).join("\n") + "\n");
|
|
274
292
|
}
|
|
275
293
|
|
|
276
294
|
export default {
|
|
@@ -368,6 +386,17 @@ export default {
|
|
|
368
386
|
ORACLE_CLI_ENTRY: path.join(PACKAGE_ROOT, "bin", "oracle.mjs"),
|
|
369
387
|
});
|
|
370
388
|
|
|
389
|
+
const hermesPython = resolveHermesPython(hermes);
|
|
390
|
+
const native = await runOracleTui({
|
|
391
|
+
hermesPython,
|
|
392
|
+
args,
|
|
393
|
+
env: cleanEnv,
|
|
394
|
+
cwd: process.cwd(),
|
|
395
|
+
stdin: process.stdin,
|
|
396
|
+
stdout: process.stdout,
|
|
397
|
+
});
|
|
398
|
+
if (native.native) return native.code;
|
|
399
|
+
|
|
371
400
|
const launch = oracleHarnessInvocation(hermes, [...hermesArgs, ...pass]);
|
|
372
401
|
|
|
373
402
|
return spawnChild(
|
|
@@ -12,10 +12,10 @@ import sys
|
|
|
12
12
|
WORDMARK = (
|
|
13
13
|
" ██████ ████████ ██████ ██████ ░███ ██████ ",
|
|
14
14
|
" ███░░███░░███░░███ ░░░░░███ ███░░███ ░███ ███░░███ ",
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
15
|
+
" ░███ ░███ ░███ ░░░ ███████ ░███ ░░░ ░███ ░███████ ",
|
|
16
|
+
" ░███ ░███ ░███ ███░░███ ░███ ███ ░███ ░███░░░ ",
|
|
17
|
+
" ░░██████ █████ ░░████████░░██████ █████░░██████ ",
|
|
18
|
+
" ░░░░░░ ░░░░░ ░░░░░░░░ ░░░░░░ ░░░░░ ░░░░░░ ",
|
|
19
19
|
)
|
|
20
20
|
|
|
21
21
|
SILENT_OUTPUT = {
|
|
@@ -114,7 +114,20 @@ def patch_cli(module):
|
|
|
114
114
|
|
|
115
115
|
self.console.clear()
|
|
116
116
|
width = shutil.get_terminal_size((100, 28)).columns
|
|
117
|
-
|
|
117
|
+
|
|
118
|
+
panel_width = max(36, min(width - 4, 76))
|
|
119
|
+
panel_left = (width - panel_width) // 2
|
|
120
|
+
content_left = panel_left + 3
|
|
121
|
+
inner = panel_width - 6
|
|
122
|
+
|
|
123
|
+
def pad_for(ink_width, ink_offset=0):
|
|
124
|
+
target = int(round((width - ink_width) / 2.0))
|
|
125
|
+
pad = target - ink_offset - content_left
|
|
126
|
+
return max(0, min(max(0, inner - ink_width - ink_offset), pad))
|
|
127
|
+
|
|
128
|
+
content = []
|
|
129
|
+
tagline = "THE FUTURE IS AGENTIC / by DEMI"
|
|
130
|
+
|
|
118
131
|
if width >= 72:
|
|
119
132
|
colors = (
|
|
120
133
|
"#B8F0FF",
|
|
@@ -124,18 +137,29 @@ def patch_cli(module):
|
|
|
124
137
|
"#9FCBDD",
|
|
125
138
|
"#B8F0FF",
|
|
126
139
|
)
|
|
140
|
+
span = max(len(row) for row in WORDMARK)
|
|
141
|
+
ink_cols = [
|
|
142
|
+
index
|
|
143
|
+
for index in range(span)
|
|
144
|
+
if any(index < len(row) and row[index] != " " for row in WORDMARK)
|
|
145
|
+
]
|
|
146
|
+
ink_offset = ink_cols[0]
|
|
147
|
+
ink_width = ink_cols[-1] - ink_cols[0] + 1
|
|
148
|
+
pad = " " * pad_for(ink_width, ink_offset)
|
|
127
149
|
for index, line in enumerate(WORDMARK):
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
150
|
+
content.append(
|
|
151
|
+
Text(pad + line.rstrip(), style=f"bold {colors[index]}", no_wrap=True)
|
|
152
|
+
)
|
|
131
153
|
else:
|
|
132
|
-
|
|
154
|
+
content.append(Text(" " * pad_for(6) + "oracle", style="bold #B8F0FF"))
|
|
133
155
|
|
|
134
|
-
|
|
156
|
+
content.append(Text(""))
|
|
157
|
+
content.append(
|
|
158
|
+
Text(" " * pad_for(len(tagline)) + tagline, style="bold #EAF2F8", no_wrap=True)
|
|
159
|
+
)
|
|
135
160
|
|
|
136
|
-
panel_width = max(36, min(width - 4, 76))
|
|
137
161
|
panel = Panel(
|
|
138
|
-
Group(
|
|
162
|
+
Group(*content),
|
|
139
163
|
width=panel_width,
|
|
140
164
|
padding=(1, 2),
|
|
141
165
|
border_style="#3B4A56",
|
package/src/tui/app.mjs
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { activeChainEnv } from "../cli/chain-state.mjs";
|
|
3
|
+
import { createGatewayClient } from "./gateway-client.mjs";
|
|
4
|
+
import { createInput } from "./input.mjs";
|
|
5
|
+
import { createRenderer } from "./renderer.mjs";
|
|
6
|
+
import {
|
|
7
|
+
formatElapsed,
|
|
8
|
+
renderBanner,
|
|
9
|
+
renderBox,
|
|
10
|
+
renderStatusBar,
|
|
11
|
+
visibleWidth,
|
|
12
|
+
wrapText,
|
|
13
|
+
} from "./format.mjs";
|
|
14
|
+
import { createPalette, THEME } from "./theme.mjs";
|
|
15
|
+
|
|
16
|
+
const LIVE_TAIL_LINES = 6;
|
|
17
|
+
|
|
18
|
+
function parseChatArgs(args = []) {
|
|
19
|
+
const out = { pass: [], query: null, model: null, provider: null, quiet: false, cont: false };
|
|
20
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
21
|
+
const a = args[i];
|
|
22
|
+
if ((a === "-q" || a === "--query") && args[i + 1]) {
|
|
23
|
+
out.query = args[++i];
|
|
24
|
+
out.pass.push("-q", out.query);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if ((a === "--model" || a === "-m") && args[i + 1]) {
|
|
28
|
+
out.model = args[++i];
|
|
29
|
+
out.pass.push("--model", out.model);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (a === "--provider" && args[i + 1]) {
|
|
33
|
+
out.provider = args[++i];
|
|
34
|
+
out.pass.push("--provider", out.provider);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (a === "--quiet" || a === "-Q") {
|
|
38
|
+
out.quiet = true;
|
|
39
|
+
out.pass.push(a);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (a === "--continue" || a === "-c") {
|
|
43
|
+
out.cont = true;
|
|
44
|
+
out.pass.push("--continue");
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
out.pass.push(a);
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function applyUsage(state, usage) {
|
|
53
|
+
if (!usage || typeof usage !== "object") return;
|
|
54
|
+
const used = Number(usage.context_used ?? usage.context_tokens ?? usage.input ?? usage.prompt);
|
|
55
|
+
const max = Number(usage.context_max ?? usage.context_length);
|
|
56
|
+
if (Number.isFinite(used) && used > 0) state.contextTokens = used;
|
|
57
|
+
if (Number.isFinite(max) && max > 0) state.contextLength = max;
|
|
58
|
+
const percent = Number(usage.context_percent);
|
|
59
|
+
if (Number.isFinite(percent)) state.contextPercent = percent;
|
|
60
|
+
else if (state.contextLength > 0) {
|
|
61
|
+
state.contextPercent = Math.round((state.contextTokens / state.contextLength) * 100);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function composerLines(state) {
|
|
66
|
+
const width = state.width;
|
|
67
|
+
const label = `${state.promptLabel} `;
|
|
68
|
+
const room = Math.max(1, width - 2 - visibleWidth(label));
|
|
69
|
+
const buffer = state.buffer || "";
|
|
70
|
+
const cursor = Math.max(0, Math.min(state.cursor ?? buffer.length, buffer.length));
|
|
71
|
+
let start = 0;
|
|
72
|
+
if (cursor > room) start = cursor - room;
|
|
73
|
+
const view = buffer.slice(start, start + room);
|
|
74
|
+
const rel = cursor - start;
|
|
75
|
+
const head = view.slice(0, rel);
|
|
76
|
+
const at = view.slice(rel, rel + 1) || " ";
|
|
77
|
+
const tail = view.slice(rel + 1);
|
|
78
|
+
const palette = state.palette;
|
|
79
|
+
const painted = `${palette.fg(THEME.accent, label)}${head}${palette.bg(THEME.selection_bg, at)}${tail}`;
|
|
80
|
+
return [painted];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function fixedLines(state) {
|
|
84
|
+
const width = state.width;
|
|
85
|
+
const out = [];
|
|
86
|
+
|
|
87
|
+
if (state.liveLines.length > 0) {
|
|
88
|
+
for (const line of state.liveLines.slice(-LIVE_TAIL_LINES)) out.push(line);
|
|
89
|
+
out.push("");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
out.push(...renderBox({
|
|
93
|
+
lines: composerLines(state),
|
|
94
|
+
width,
|
|
95
|
+
palette: state.palette,
|
|
96
|
+
title: "Ask Oracle ›",
|
|
97
|
+
}));
|
|
98
|
+
|
|
99
|
+
out.push(renderStatusBar({
|
|
100
|
+
model: state.model,
|
|
101
|
+
contextTokens: state.contextTokens,
|
|
102
|
+
contextLength: state.contextLength,
|
|
103
|
+
percent: state.contextPercent,
|
|
104
|
+
effort: state.effort,
|
|
105
|
+
thinking: state.thinking,
|
|
106
|
+
chain: state.chain,
|
|
107
|
+
width,
|
|
108
|
+
palette: state.palette,
|
|
109
|
+
}));
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function eventShape(frame) {
|
|
114
|
+
const params = frame?.params || frame || {};
|
|
115
|
+
return {
|
|
116
|
+
type: params.type || frame?.type || "",
|
|
117
|
+
sessionId: params.session_id || frame?.session_id || "",
|
|
118
|
+
payload: params.payload || frame?.payload || {},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function textPayload(payload = {}) {
|
|
123
|
+
if (typeof payload.text === "string") return payload.text;
|
|
124
|
+
if (typeof payload.rendered === "string") return payload.rendered;
|
|
125
|
+
if (typeof payload.message === "string") return payload.message;
|
|
126
|
+
return "";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function createOracleTui(options = {}) {
|
|
130
|
+
const stdout = options.stdout || process.stdout;
|
|
131
|
+
const stdin = options.stdin || process.stdin;
|
|
132
|
+
const width = Math.max(20, Number(stdout.columns || options.columns || 80));
|
|
133
|
+
const palette = options.palette || createPalette({ color: stdout.isTTY !== false });
|
|
134
|
+
const renderer = options.renderer || createRenderer({ stdout, rows: stdout.rows, columns: width });
|
|
135
|
+
const client = options.client || createGatewayClient({
|
|
136
|
+
python: options.python,
|
|
137
|
+
pythonArgs: options.pythonArgs || [],
|
|
138
|
+
cwd: options.cwd || process.cwd(),
|
|
139
|
+
env: options.env || process.env,
|
|
140
|
+
spawnFn: options.spawnFn,
|
|
141
|
+
startupTimeoutMs: options.startupTimeoutMs,
|
|
142
|
+
requestTimeoutMs: options.requestTimeoutMs,
|
|
143
|
+
});
|
|
144
|
+
const emitter = new EventEmitter();
|
|
145
|
+
const now = options.now || (() => Date.now());
|
|
146
|
+
const state = {
|
|
147
|
+
palette,
|
|
148
|
+
width,
|
|
149
|
+
promptLabel: "oracle ›",
|
|
150
|
+
buffer: "",
|
|
151
|
+
cursor: 0,
|
|
152
|
+
model: options.model || "model",
|
|
153
|
+
contextTokens: 0,
|
|
154
|
+
contextLength: 0,
|
|
155
|
+
contextPercent: 0,
|
|
156
|
+
effort: options.effort || "",
|
|
157
|
+
thinking: "0s",
|
|
158
|
+
chain: (options.env || process.env).ORACLE_ACTIVE_CHAIN || "",
|
|
159
|
+
busy: false,
|
|
160
|
+
cancelled: false,
|
|
161
|
+
sessionId: null,
|
|
162
|
+
assistantBuffer: "",
|
|
163
|
+
flushedLines: 0,
|
|
164
|
+
liveLines: [],
|
|
165
|
+
startedAt: 0,
|
|
166
|
+
};
|
|
167
|
+
let input = null;
|
|
168
|
+
let ticker = null;
|
|
169
|
+
|
|
170
|
+
const ui = { renderer, state };
|
|
171
|
+
|
|
172
|
+
function renderNow() {
|
|
173
|
+
renderer.render(fixedLines(state));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function emitLine(text) {
|
|
177
|
+
renderer.writeAbove(`${text}\n`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function recomputeLive() {
|
|
181
|
+
const parts = state.assistantBuffer.split("\n");
|
|
182
|
+
const partial = parts[parts.length - 1] || "";
|
|
183
|
+
state.liveLines = partial ? wrapText(partial, state.width) : [];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function flushAssistant({ final = false } = {}) {
|
|
187
|
+
const parts = state.assistantBuffer.split("\n");
|
|
188
|
+
const ready = final ? parts.length : parts.length - 1;
|
|
189
|
+
let wrote = false;
|
|
190
|
+
while (state.flushedLines < ready) {
|
|
191
|
+
const line = parts[state.flushedLines];
|
|
192
|
+
for (const wrapped of wrapText(line, state.width)) emitLine(wrapped);
|
|
193
|
+
state.flushedLines += 1;
|
|
194
|
+
wrote = true;
|
|
195
|
+
}
|
|
196
|
+
if (final) state.liveLines = [];
|
|
197
|
+
else recomputeLive();
|
|
198
|
+
return wrote;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function startTicker() {
|
|
202
|
+
if (ticker) return;
|
|
203
|
+
ticker = setInterval(() => {
|
|
204
|
+
if (!state.busy) return;
|
|
205
|
+
state.thinking = formatElapsed(now() - state.startedAt);
|
|
206
|
+
renderNow();
|
|
207
|
+
}, 250);
|
|
208
|
+
if (typeof ticker.unref === "function") ticker.unref();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function stopTicker() {
|
|
212
|
+
if (!ticker) return;
|
|
213
|
+
clearInterval(ticker);
|
|
214
|
+
ticker = null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function submit(text) {
|
|
218
|
+
const value = String(text || "").trim();
|
|
219
|
+
if (!value) return;
|
|
220
|
+
if (value === "/exit" || value === "/quit") {
|
|
221
|
+
await stop();
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (value === "/clear") {
|
|
225
|
+
renderer.clear();
|
|
226
|
+
for (const line of renderBanner({ width: state.width, palette })) emitLine(line);
|
|
227
|
+
renderNow();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
state.busy = true;
|
|
231
|
+
state.assistantBuffer = "";
|
|
232
|
+
state.flushedLines = 0;
|
|
233
|
+
state.liveLines = [];
|
|
234
|
+
state.startedAt = now();
|
|
235
|
+
state.thinking = "0s";
|
|
236
|
+
emitLine(`${palette.bold(palette.fg(THEME.prompt, "you"))} ${value}`);
|
|
237
|
+
emitLine("");
|
|
238
|
+
startTicker();
|
|
239
|
+
renderNow();
|
|
240
|
+
await client.request("prompt.submit", { session_id: state.sessionId, text: value });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function handleEvent(frame) {
|
|
244
|
+
const event = eventShape(frame);
|
|
245
|
+
if (event.type === "gateway.ready") return;
|
|
246
|
+
|
|
247
|
+
if (event.type === "session.info" && event.payload) {
|
|
248
|
+
if (event.payload.model) state.model = event.payload.model;
|
|
249
|
+
if (event.payload.reasoning_effort) state.effort = event.payload.reasoning_effort;
|
|
250
|
+
applyUsage(state, event.payload.usage);
|
|
251
|
+
renderNow();
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (event.type === "message.start") {
|
|
255
|
+
state.busy = true;
|
|
256
|
+
state.cancelled = false;
|
|
257
|
+
state.assistantBuffer = "";
|
|
258
|
+
state.flushedLines = 0;
|
|
259
|
+
state.liveLines = [];
|
|
260
|
+
emitLine(palette.bold(palette.fg(THEME.accent, "oracle")));
|
|
261
|
+
renderNow();
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (event.type === "thinking.delta" || event.type === "reasoning.delta") {
|
|
265
|
+
if (state.cancelled) return;
|
|
266
|
+
const text = textPayload(event.payload).trim();
|
|
267
|
+
if (text) state.thinking = `${formatElapsed(now() - state.startedAt)}`;
|
|
268
|
+
renderNow();
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (event.type === "message.delta") {
|
|
272
|
+
if (state.cancelled) return;
|
|
273
|
+
state.assistantBuffer += textPayload(event.payload);
|
|
274
|
+
flushAssistant();
|
|
275
|
+
renderNow();
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (event.type === "message.complete") {
|
|
279
|
+
if (state.cancelled) {
|
|
280
|
+
state.cancelled = false;
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const finalText = textPayload(event.payload);
|
|
284
|
+
if (finalText && finalText.length >= state.assistantBuffer.length) {
|
|
285
|
+
state.assistantBuffer = finalText;
|
|
286
|
+
}
|
|
287
|
+
flushAssistant({ final: true });
|
|
288
|
+
emitLine("");
|
|
289
|
+
state.busy = false;
|
|
290
|
+
stopTicker();
|
|
291
|
+
applyUsage(state, event.payload?.usage || event.payload?.context);
|
|
292
|
+
state.thinking = formatElapsed(now() - state.startedAt);
|
|
293
|
+
renderNow();
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (event.type === "error") {
|
|
297
|
+
state.busy = false;
|
|
298
|
+
stopTicker();
|
|
299
|
+
emitLine(palette.fg(THEME.error, `error: ${textPayload(event.payload) || "unknown"}`));
|
|
300
|
+
renderNow();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function start() {
|
|
305
|
+
await client.start();
|
|
306
|
+
client.on("event", handleEvent);
|
|
307
|
+
client.on("log", (line) => emitter.emit("log", line));
|
|
308
|
+
client.on("error", (error) => emitter.emit("error", error));
|
|
309
|
+
const session = await client.request("session.create", {
|
|
310
|
+
cols: state.width,
|
|
311
|
+
cwd: options.cwd || process.cwd(),
|
|
312
|
+
source: "cli",
|
|
313
|
+
profile: options.profile || "oracle",
|
|
314
|
+
close_on_disconnect: true,
|
|
315
|
+
...(options.model ? { model: options.model } : {}),
|
|
316
|
+
...(options.provider ? { provider: options.provider } : {}),
|
|
317
|
+
});
|
|
318
|
+
state.sessionId = session.session_id;
|
|
319
|
+
state.model = session.info?.model || state.model;
|
|
320
|
+
if (session.info?.reasoning_effort) state.effort = session.info.reasoning_effort;
|
|
321
|
+
applyUsage(state, session.info?.usage);
|
|
322
|
+
for (const line of renderBanner({ width: state.width, palette })) emitLine(line);
|
|
323
|
+
emitLine("");
|
|
324
|
+
renderNow();
|
|
325
|
+
input = createInput({
|
|
326
|
+
stdin,
|
|
327
|
+
stdout,
|
|
328
|
+
onSubmit: (value) => { submit(value).catch((error) => emitter.emit("error", error)); },
|
|
329
|
+
onCancel: () => {
|
|
330
|
+
if (!state.busy) return;
|
|
331
|
+
state.busy = false;
|
|
332
|
+
state.cancelled = true;
|
|
333
|
+
stopTicker();
|
|
334
|
+
flushAssistant({ final: true });
|
|
335
|
+
emitLine(palette.fg(THEME.warn, "interrupted"));
|
|
336
|
+
emitLine("");
|
|
337
|
+
renderNow();
|
|
338
|
+
client.request("session.interrupt", { session_id: state.sessionId })
|
|
339
|
+
.catch((error) => emitter.emit("error", error));
|
|
340
|
+
},
|
|
341
|
+
onEof: () => { stop().catch((error) => emitter.emit("error", error)); },
|
|
342
|
+
onClear: () => {
|
|
343
|
+
renderer.clear();
|
|
344
|
+
renderNow();
|
|
345
|
+
},
|
|
346
|
+
onRender: (editorState) => {
|
|
347
|
+
state.buffer = editorState.buffer || "";
|
|
348
|
+
state.cursor = editorState.cursor ?? state.buffer.length;
|
|
349
|
+
renderNow();
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
input.start();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function stop() {
|
|
356
|
+
stopTicker();
|
|
357
|
+
if (input) input.stop();
|
|
358
|
+
renderer.dispose();
|
|
359
|
+
await client.stop();
|
|
360
|
+
emitter.emit("stop");
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return Object.freeze({
|
|
364
|
+
start,
|
|
365
|
+
stop,
|
|
366
|
+
on: emitter.on.bind(emitter),
|
|
367
|
+
off: emitter.off.bind(emitter),
|
|
368
|
+
state,
|
|
369
|
+
handleEvent,
|
|
370
|
+
submit,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export async function runOracleTui({ hermesPython, args = [], env = process.env, cwd = process.cwd(), stdout = process.stdout, stdin = process.stdin } = {}) {
|
|
375
|
+
const parsed = parseChatArgs(args);
|
|
376
|
+
if (parsed.query) return { native: false, pass: parsed.pass };
|
|
377
|
+
if (!stdin.isTTY || !stdout.isTTY) return { native: false, pass: parsed.pass };
|
|
378
|
+
// Native TUI is intentionally opt-in while the stable oracle shim remains pinned.
|
|
379
|
+
if (env.ORACLE_NATIVE_TUI !== "1") return { native: false, pass: parsed.pass };
|
|
380
|
+
const invocation = hermesPython;
|
|
381
|
+
if (!invocation) return { native: false, pass: parsed.pass };
|
|
382
|
+
const cleanEnv = activeChainEnv({
|
|
383
|
+
...env,
|
|
384
|
+
ORACLE_CHAT_SURFACE: "1",
|
|
385
|
+
ORACLE_PROFILE: "oracle",
|
|
386
|
+
ORACLE_NODE_BIN: process.execPath,
|
|
387
|
+
});
|
|
388
|
+
const tui = createOracleTui({
|
|
389
|
+
python: invocation.command,
|
|
390
|
+
pythonArgs: invocation.prefix || [],
|
|
391
|
+
cwd,
|
|
392
|
+
env: cleanEnv,
|
|
393
|
+
stdin,
|
|
394
|
+
stdout,
|
|
395
|
+
model: parsed.model,
|
|
396
|
+
provider: parsed.provider,
|
|
397
|
+
profile: "oracle",
|
|
398
|
+
});
|
|
399
|
+
tui.on("log", (line) => {
|
|
400
|
+
if (env.ORACLE_CLI_DEBUG) process.stderr.write(`${line}\n`);
|
|
401
|
+
});
|
|
402
|
+
tui.on("error", (error) => {
|
|
403
|
+
process.stderr.write(`oracle chat: ${error.message}\n`);
|
|
404
|
+
});
|
|
405
|
+
await tui.start();
|
|
406
|
+
return new Promise((resolve) => {
|
|
407
|
+
let done = false;
|
|
408
|
+
const finish = async () => {
|
|
409
|
+
if (done) return;
|
|
410
|
+
done = true;
|
|
411
|
+
await tui.stop();
|
|
412
|
+
resolve({ native: true, code: 0 });
|
|
413
|
+
};
|
|
414
|
+
tui.on("stop", () => {
|
|
415
|
+
if (done) return;
|
|
416
|
+
done = true;
|
|
417
|
+
resolve({ native: true, code: 0 });
|
|
418
|
+
});
|
|
419
|
+
process.once("SIGINT", finish);
|
|
420
|
+
process.once("SIGTERM", finish);
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export default {
|
|
425
|
+
createOracleTui,
|
|
426
|
+
runOracleTui,
|
|
427
|
+
};
|