@oracle-agent/oracle 0.9.6 → 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 +29 -8
- 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(
|
|
@@ -109,13 +109,25 @@ def patch_cli(module):
|
|
|
109
109
|
from rich.align import Align
|
|
110
110
|
from rich.console import Group
|
|
111
111
|
from rich.panel import Panel
|
|
112
|
-
from rich.table import Table
|
|
113
112
|
from rich.text import Text
|
|
114
113
|
from rich import box
|
|
115
114
|
|
|
116
115
|
self.console.clear()
|
|
117
116
|
width = shutil.get_terminal_size((100, 28)).columns
|
|
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
|
+
|
|
118
128
|
content = []
|
|
129
|
+
tagline = "THE FUTURE IS AGENTIC / by DEMI"
|
|
130
|
+
|
|
119
131
|
if width >= 72:
|
|
120
132
|
colors = (
|
|
121
133
|
"#B8F0FF",
|
|
@@ -125,18 +137,27 @@ def patch_cli(module):
|
|
|
125
137
|
"#9FCBDD",
|
|
126
138
|
"#B8F0FF",
|
|
127
139
|
)
|
|
128
|
-
|
|
129
|
-
|
|
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)
|
|
130
149
|
for index, line in enumerate(WORDMARK):
|
|
131
|
-
|
|
132
|
-
|
|
150
|
+
content.append(
|
|
151
|
+
Text(pad + line.rstrip(), style=f"bold {colors[index]}", no_wrap=True)
|
|
152
|
+
)
|
|
133
153
|
else:
|
|
134
|
-
content.append(
|
|
154
|
+
content.append(Text(" " * pad_for(6) + "oracle", style="bold #B8F0FF"))
|
|
135
155
|
|
|
136
156
|
content.append(Text(""))
|
|
137
|
-
content.append(
|
|
157
|
+
content.append(
|
|
158
|
+
Text(" " * pad_for(len(tagline)) + tagline, style="bold #EAF2F8", no_wrap=True)
|
|
159
|
+
)
|
|
138
160
|
|
|
139
|
-
panel_width = max(36, min(width - 4, 76))
|
|
140
161
|
panel = Panel(
|
|
141
162
|
Group(*content),
|
|
142
163
|
width=panel_width,
|
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
|
+
};
|