@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oracle-agent/oracle",
3
- "version": "0.9.5",
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",
@@ -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 filled lowercase 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
- body = Text(justify="center")
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
- body.append(line, style=f"bold {colors[index]}")
129
- if index != len(WORDMARK) - 1:
130
- body.append("\n")
150
+ content.append(
151
+ Text(pad + line.rstrip(), style=f"bold {colors[index]}", no_wrap=True)
152
+ )
131
153
  else:
132
- body.append("oracle", style="bold #B8F0FF")
154
+ content.append(Text(" " * pad_for(6) + "oracle", style="bold #B8F0FF"))
133
155
 
134
- body.append("\n\nTHE FUTURE IS AGENTIC / by DEMI", style="bold #EAF2F8")
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(Align.center(body)),
162
+ Group(*content),
139
163
  width=panel_width,
140
164
  padding=(1, 2),
141
165
  border_style="#3B4A56",
@@ -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
+ };