@oxecli/oxe 1.0.51 → 1.0.53

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/dist/cli.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import path from "node:path";
2
2
  import { createRequire } from "node:module";
3
3
  import { fileURLToPath } from "node:url";
4
- import { loadOrPrompt, default_reasoning_effort, default_model, max_action_chars, max_resume_history_items, } from "./config.js";
4
+ import { loadOrPrompt, default_reasoning_effort, default_model, max_action_chars, max_resume_history_items, runtimeOsSummary, } from "./config.js";
5
5
  import { InferenceEngine, estimateTokens } from "./engine.js";
6
- import { saveSession, loadSession, listSessions, nextSessionId, conversationLabel, COMMAND_HELP, toolOutputFailed, } from "./sessions.js";
6
+ import { saveSession, loadSession, listSessions, nextSessionId, conversationLabel, COMMAND_HELP, stripOrphanCalls, toolOutputFailed, } from "./sessions.js";
7
7
  import { clearScreen, enableAnsi, askBottomPrompt, userDisplayText, aiMarkdown, mutedMarkdown, messageText, formatToolAction, truncateEllipsis, renderPanel, renderTableString, enableRawStdin, disableRawStdin, waitRawKey, } from "./ui.js";
8
8
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
9
  const require = createRequire(import.meta.url);
@@ -26,7 +26,7 @@ export class CLI {
26
26
  }
27
27
  renderHeader(model = default_model) {
28
28
  const version = packageInfo.version ?? "unknown";
29
- const cellWidth = 23;
29
+ const cellWidth = 22;
30
30
  const centeredCell = (value, render) => {
31
31
  const left = Math.max(0, Math.floor((cellWidth - value.length) / 2));
32
32
  const right = Math.max(0, cellWidth - value.length - left);
@@ -39,10 +39,17 @@ export class CLI {
39
39
  return centeredCell(value, `[bold ${color}]${value}[/bold ${color}]`);
40
40
  };
41
41
  const description = "A focused terminal workspace for building and shipping software.";
42
- renderPanel("[bold white]Terminal coding agent[/bold white]\n" +
42
+ renderPanel("[bold white]Terminal Coding Agent[/bold white]\n" +
43
43
  `[dim]${description}[/dim]\n\n` +
44
- labelCell("Version") + labelCell("Model") + labelCell("Engine") + "\n" +
45
- valueCell(version, "cyan") + valueCell(model, "blue") + valueCell("Ready", "green"), "Oxe", "", false, "90", "left");
44
+ labelCell("Version") +
45
+ labelCell("Model") +
46
+ labelCell("Status") +
47
+ "\n" +
48
+ valueCell(version, "cyan") +
49
+ valueCell(model, "blue") +
50
+ valueCell("Ready", "green") +
51
+ "\n\n" +
52
+ `[dim]Environment: ${runtimeOsSummary()}[/dim]`, "Oxe", "Type /help for commands · Ctrl+C to interrupt", false, "90", "left");
46
53
  }
47
54
  saveSession(engine) {
48
55
  if (!this.inputItems.length)
@@ -59,11 +66,9 @@ export class CLI {
59
66
  });
60
67
  }
61
68
  printToolEntry(started, text, status) {
62
- if (started) {
63
- process.stdout.write(`\x1b[2m${truncateEllipsis(String(started), max_action_chars, "text")}\x1b[0m\n`);
64
- }
69
+ const icon = status === "failed" ? "\x1b[31m✗\x1b[0m" : "\x1b[32m✓\x1b[0m";
65
70
  const style = status === "failed" ? "\x1b[31m" : "\x1b[32m";
66
- process.stdout.write(`${style}${truncateEllipsis(String(text), max_action_chars, "text")}\x1b[0m\n`);
71
+ process.stdout.write(`${icon} ${style}${truncateEllipsis(String(text), max_action_chars, "text")}\x1b[0m\n`);
67
72
  }
68
73
  printAssistantBlock(text) {
69
74
  const rendered = aiMarkdown(text);
@@ -80,25 +85,21 @@ export class CLI {
80
85
  process.stdout.write("\n");
81
86
  }
82
87
  async showHelp() {
83
- // Mirror Python's `_show_help`: a bordered table panel with the command
84
- // column bold (with `<arg>` placeholders cyan) and descriptions dim, inside
85
- // a panel titled "Commands · Enter / Ctrl+C closes". In a TTY it stays on
86
- // screen until Enter/Ctrl+C/arrows/q closes it.
87
88
  const rows = COMMAND_HELP.map(([cmd, desc]) => {
88
89
  const styled = cmd.replace(/(<[^>]+>)/g, "\x1b[36m$1\x1b[0m");
89
- return { cells: [`\x1b[1m${styled}\x1b[0m`, `\x1b[2m${desc}\x1b[0m`] };
90
+ return {
91
+ cells: [`\x1b[1;36m${styled}\x1b[0m`, `\x1b[2m${desc}\x1b[0m`],
92
+ };
90
93
  });
91
94
  const panel = renderTableString(rows, {
92
- title: "Commands · Enter / Ctrl+C closes",
95
+ title: "Commands · Press Enter or Ctrl+C to close",
93
96
  borderStyle: "90",
94
97
  expand: false,
95
98
  titleAlign: "left",
96
- colGap: 2,
99
+ colGap: 3,
97
100
  });
98
- // Keep one blank row between the previous response/footer and the panel.
99
101
  process.stdout.write("\n" + panel + "\n");
100
102
  if (process.stdin.isTTY) {
101
- // Wait for a close key, then erase the panel (mirrors Python's Live).
102
103
  enableRawStdin();
103
104
  try {
104
105
  for (;;) {
@@ -109,22 +110,29 @@ export class CLI {
109
110
  finally {
110
111
  disableRawStdin();
111
112
  }
112
- // Cursor sits one line below the panel; move up to its top border and
113
- // clear downward so the whole panel disappears and the cursor lands back
114
- // on the fresh line before the prompt.
115
113
  const n = panel.split("\n").length;
116
- // Include the leading separator row in the cleanup so the next prompt
117
- // creates exactly one fresh separator instead of duplicating it.
118
114
  process.stdout.write(`\x1b[${n + 1}A\r\x1b[J`);
119
115
  }
120
116
  }
121
117
  async awaitRawCloseKey() {
122
118
  const k = await waitRawKey();
123
- const closeNames = ["escape", "return", "enter", "up", "down", "left", "right"];
119
+ const closeNames = [
120
+ "escape",
121
+ "return",
122
+ "enter",
123
+ "up",
124
+ "down",
125
+ "left",
126
+ "right",
127
+ ];
124
128
  if (k.ctrl && (k.name === "c" || k.name === "z"))
125
129
  return true;
126
- if (k.str === "q" || k.str === "Q" || k.str === "\x1b" || closeNames.includes(k.name))
130
+ if (k.str === "q" ||
131
+ k.str === "Q" ||
132
+ k.str === "\x1b" ||
133
+ closeNames.includes(k.name)) {
127
134
  return true;
135
+ }
128
136
  return false;
129
137
  }
130
138
  renderHistory(items, maxItems = max_resume_history_items) {
@@ -145,13 +153,18 @@ export class CLI {
145
153
  renders.push({ kind: "user", text: c, paste_spans: it["paste_spans"] });
146
154
  }
147
155
  else if (Array.isArray(c) && messageText(it).trim()) {
148
- renders.push({ kind: "user", text: messageText(it), paste_spans: it["paste_spans"] });
156
+ renders.push({
157
+ kind: "user",
158
+ text: messageText(it),
159
+ paste_spans: it["paste_spans"],
160
+ });
149
161
  }
150
162
  }
151
163
  else if (typ === "message") {
152
164
  const text = messageText(it);
153
- if (text.trim())
165
+ if (text.trim()) {
154
166
  renders.push({ kind: "assistant", text, footer: it["footer"] ?? "" });
167
+ }
155
168
  }
156
169
  else if (typ === "function_call") {
157
170
  const name = it["name"] ?? "tool";
@@ -177,7 +190,7 @@ export class CLI {
177
190
  const kind = r["kind"];
178
191
  const payload = r["text"];
179
192
  if (kind === "user") {
180
- process.stdout.write(`\x1b[1m❯\x1b[0m ${userDisplayText(payload, r["paste_spans"])}\n`);
193
+ process.stdout.write(`\x1b[1;36m❯\x1b[0m ${userDisplayText(payload, r["paste_spans"])}\n`);
181
194
  }
182
195
  else if (kind === "assistant") {
183
196
  this.printAssistantBlock(payload);
@@ -208,7 +221,7 @@ export class CLI {
208
221
  const typ = e["type"];
209
222
  const text = e["text"] ?? "";
210
223
  if (typ === "user") {
211
- process.stdout.write(`\x1b[1m❯\x1b[0m ${userDisplayText(text, e["paste_spans"])}\n`);
224
+ process.stdout.write(`\x1b[1;36m❯\x1b[0m ${userDisplayText(text, e["paste_spans"])}\n`);
212
225
  }
213
226
  else if (typ === "assistant") {
214
227
  this.printAssistantBlock(text);
@@ -217,8 +230,6 @@ export class CLI {
217
230
  this.printToolEntry(e["started"], text, e["status"] ?? "");
218
231
  }
219
232
  else if (typ === "footer") {
220
- // Footer metadata is muted, with dynamic duration and token values
221
- // restored to white by the embedded ANSI value styles.
222
233
  process.stdout.write(mutedMarkdown(text) + "\n");
223
234
  }
224
235
  else {
@@ -230,7 +241,9 @@ export class CLI {
230
241
  }
231
242
  conversationRow(rec) {
232
243
  const sid = String(rec["id"] ?? "?");
233
- const updated = String(rec["updated_at"] ?? "").slice(0, 16).replace("T", " ");
244
+ const updated = String(rec["updated_at"] ?? "")
245
+ .slice(0, 16)
246
+ .replace("T", " ");
234
247
  const label = truncateLabel(rec["label"]);
235
248
  const items = rec["input_items"] ?? [];
236
249
  const toks = estimateTokens(items).toLocaleString();
@@ -243,7 +256,6 @@ export class CLI {
243
256
  process.stdout.write(this.conversationRow(rec) + "\n");
244
257
  }
245
258
  }
246
- /** Build the interactive picker panel (mirrors Python `_render_picker`). */
247
259
  renderPickerPanel(recs, selected, visible = 8) {
248
260
  const total = recs.length;
249
261
  const half = Math.floor(visible / 2);
@@ -253,17 +265,19 @@ export class CLI {
253
265
  for (let idx = lo; idx < hi; idx++) {
254
266
  const rec = recs[idx];
255
267
  const sid = String(rec["id"] ?? "?");
256
- const updated = String(rec["updated_at"] ?? "").slice(0, 16).replace("T", " ");
268
+ const updated = String(rec["updated_at"] ?? "")
269
+ .slice(0, 16)
270
+ .replace("T", " ");
257
271
  const label = truncateLabel(rec["label"]);
258
272
  const items = rec["input_items"] ?? [];
259
273
  const toks = estimateTokens(items).toLocaleString();
260
274
  const selectedRow = idx === selected;
261
- const content = `${selectedRow ? "\x1b[1m\x1b[37m" : "\x1b[1m"}${sid.padStart(4)}\x1b[0m` +
262
- `${selectedRow ? "\x1b[2m\x1b[36m" : "\x1b[2m"} ${updated}\x1b[0m` +
263
- `${selectedRow ? "\x1b[37m" : ""} ${label}\x1b[0m` +
264
- `${selectedRow ? "\x1b[2m\x1b[37m" : "\x1b[2m"} ${items.length} items · ${toks} tok\x1b[0m`;
275
+ const content = `${selectedRow ? "\x1b[1;37m" : "\x1b[1m"}${sid.padStart(4)}\x1b[0m` +
276
+ `${selectedRow ? "\x1b[2;36m" : "\x1b[2m"} ${updated}\x1b[0m` +
277
+ `${selectedRow ? " \x1b[1;37m" : " "}${label}\x1b[0m` +
278
+ `${selectedRow ? "\x1b[2;36m" : "\x1b[2m"} ${items.length} items · ${toks} tok\x1b[0m`;
265
279
  rows.push({
266
- cells: [selectedRow ? "\x1b[1m\x1b[36m❯\x1b[0m" : "", content],
280
+ cells: [selectedRow ? "\x1b[1;36m❯\x1b[0m" : "", content],
267
281
  });
268
282
  }
269
283
  const below = total - hi;
@@ -276,7 +290,7 @@ export class CLI {
276
290
  footer = `\x1b[2m▼ ${below} more\x1b[0m`;
277
291
  if (footer)
278
292
  rows.push({ cells: ["", footer] });
279
- const title = `Conversations \x1b[1m\x1b[36m${lo + 1}-${hi}/${total}\x1b[0m (↑/↓ · Quit (q) · Enter)`;
293
+ const title = `Conversations \x1b[1;36m${lo + 1}-${hi}/${total}\x1b[0m (↑/↓ · Quit: q · Enter to select)`;
280
294
  return renderTableString(rows, {
281
295
  title,
282
296
  borderStyle: "90",
@@ -286,7 +300,6 @@ export class CLI {
286
300
  colWidths: [3, undefined],
287
301
  });
288
302
  }
289
- /** Interactive arrow up/down project picker (mirrors Python `_pick_conversation`). */
290
303
  async pickConversation() {
291
304
  const recs = listSessions();
292
305
  if (!recs.length) {
@@ -359,11 +372,9 @@ export class CLI {
359
372
  clearScreen();
360
373
  this.renderHeader(engine.modelName);
361
374
  process.stdout.write("\n");
362
- process.stdout.write(`\x1b[2mResumed:\x1b[0m ${truncateLabel(rec["label"])}\n`);
363
- process.stdout.write("\n");
364
- process.stdout.write(`\x1b[2m(${this.inputItems.length} items · ${estimateTokens(this.inputItems).toLocaleString()} tokens · Effort: \x1b[1;36m${engine.reasoningEffort}\x1b[0m\x1b[2m)\x1b[0m\n`);
365
- process.stdout.write("\n");
366
- renderPanel("Conversation history", "Restored");
375
+ process.stdout.write(`\x1b[2mResumed:\x1b[0m \x1b[1m${truncateLabel(rec["label"])}\x1b[0m\n`);
376
+ process.stdout.write(`\x1b[2m(${this.inputItems.length} items · ${estimateTokens(this.inputItems).toLocaleString()} tokens · Effort: \x1b[1;36m${engine.reasoningEffort}\x1b[0m\x1b[2m)\x1b[0m\n\n`);
377
+ renderPanel("Conversation history restored", "Restored");
367
378
  process.stdout.write("\n");
368
379
  if (this.story.length)
369
380
  this.renderStory(this.story);
@@ -373,7 +384,6 @@ export class CLI {
373
384
  async replLoop(engine) {
374
385
  for (;;) {
375
386
  engine.inQuery = false;
376
- let rawPrompt;
377
387
  let queryStarted = false;
378
388
  try {
379
389
  const [input, spans] = await askBottomPrompt("You", "❯", this.promptHistory);
@@ -420,7 +430,7 @@ export class CLI {
420
430
  const effort = parts[1]?.toLowerCase() ?? "";
421
431
  if (["none", "low", "high"].includes(effort)) {
422
432
  engine.reasoningEffort = effort;
423
- process.stdout.write(`\n\x1b[2mReasoning effort set to ${effort}.\x1b[0m\n`);
433
+ process.stdout.write(`\n\x1b[32m✓\x1b[0m \x1b[2mReasoning effort set to \x1b[1;36m${effort}\x1b[0m\x1b[2m.\x1b[0m\n`);
424
434
  }
425
435
  else {
426
436
  process.stdout.write("\n");
@@ -428,7 +438,7 @@ export class CLI {
428
438
  }
429
439
  continue;
430
440
  }
431
- process.stdout.write(`\n\x1b[1m❯\x1b[0m ${userDisplayText(input, spans)}\n\n`);
441
+ process.stdout.write(`\n\x1b[1;36m❯\x1b[0m ${userDisplayText(input, spans)}\n\n`);
432
442
  queryStarted = true;
433
443
  await engine.executeQuery(input, this.inputItems, this.story, spans);
434
444
  queryStarted = false;
@@ -438,20 +448,22 @@ export class CLI {
438
448
  if (err?.message === "eof") {
439
449
  this.saveSession(engine);
440
450
  await engine.cleanupStoredResponses();
441
- process.stdout.write("\nSession closing via exit interrupt hook.\n");
451
+ process.stdout.write("\nSession closing via exit interrupt hook.\n\n");
442
452
  break;
443
453
  }
444
454
  if (err?.message === "interrupt") {
445
455
  const wasActive = queryStarted || engine.inQuery;
446
456
  if (wasActive && !engine.queryHasOutput) {
447
- process.stdout.write(aiMarkdown("What else can I help with?") + "\n");
457
+ process.stdout.write(aiMarkdown("What else can I help you with?") + "\n");
448
458
  }
449
459
  engine.inQuery = false;
450
- // Drop the pending user message/story entry that was never run.
451
- if (this.inputItems.length && this.inputItems[this.inputItems.length - 1]["role"] === "user") {
460
+ stripOrphanCalls(this.inputItems);
461
+ if (this.inputItems.length &&
462
+ this.inputItems[this.inputItems.length - 1]["role"] === "user") {
452
463
  this.inputItems.pop();
453
464
  }
454
- if (this.story.length && this.story[this.story.length - 1]["type"] === "user") {
465
+ if (this.story.length &&
466
+ this.story[this.story.length - 1]["type"] === "user") {
455
467
  this.story.pop();
456
468
  }
457
469
  if (this.interruptPending) {
@@ -461,13 +473,10 @@ export class CLI {
461
473
  break;
462
474
  }
463
475
  this.interruptPending = true;
464
- // clearBox leaves the cursor on the leading blank row; the newline
465
- // below places the message one row lower, giving one blank above it
466
- // (mirrors Python's console.print()).
467
476
  process.stdout.write("\n");
468
477
  process.stdout.write(wasActive
469
- ? "\x1b[2mInterrupted agent. Press Ctrl+C again to close the app, or type a new prompt.\x1b[0m\n"
470
- : "\x1b[2mNo active response. Press Ctrl+C again to exit the app.\x1b[0m\n");
478
+ ? "\x1b[2mInterrupted agent. Press Ctrl+C again to exit, or type a new prompt.\x1b[0m\n"
479
+ : "\x1b[2mNo active query. Press Ctrl+C again to exit.\x1b[0m\n");
471
480
  continue;
472
481
  }
473
482
  throw err;
@@ -484,9 +493,6 @@ export async function main() {
484
493
  }
485
494
  catch (err) {
486
495
  if (err?.message === "interrupt" || err?.message === "eof") {
487
- // cleanup() already moved to a fresh line; the leading newline below
488
- // leaves that row as a blank (one under the prompt), and the trailing
489
- // double-newline leaves a blank row under the message too.
490
496
  process.stdout.write("\nClosing terminal session. Goodbye!\n\n");
491
497
  process.exit(0);
492
498
  return;
@@ -513,7 +519,5 @@ export async function main() {
513
519
  if (typeof client?.close === "function")
514
520
  await client.close();
515
521
  }
516
- // The prompt's readline keypress listener is never removed and stdin remains
517
- // resumed, so Node won't exit on its own after the loop ends. Exit explicitly.
518
522
  process.exit(0);
519
523
  }
package/dist/config.js CHANGED
@@ -2,7 +2,6 @@ import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { Spinner } from "./ui.js";
6
5
  // ---------------------------------------------------------------------------
7
6
  // Paths
8
7
  // ---------------------------------------------------------------------------
@@ -20,7 +19,7 @@ export const max_output_chars = 8000;
20
19
  export const max_read_lines = 1500;
21
20
  export const max_read_line_chars = 500;
22
21
  export const max_read_file_bytes = 50 * 1024 * 1024;
23
- export const max_diff_lines = 16;
22
+ export const max_diff_lines = 24;
24
23
  export const max_diff_context_lines = 6;
25
24
  export const max_diff_line_chars = 400;
26
25
  export const max_agent_steps = 50;
@@ -40,7 +39,7 @@ export const max_bash_timeout_seconds = 300;
40
39
  export const strict_max_properties = 2;
41
40
  export const ai_style = "grey62";
42
41
  // ---------------------------------------------------------------------------
43
- // .env loading
42
+ // .env loading & cloud defaults
44
43
  // ---------------------------------------------------------------------------
45
44
  function loadEnvFile() {
46
45
  const envPath = path.join(PROJECT_DIR, ".env");
@@ -69,17 +68,13 @@ function loadEnvFile() {
69
68
  }
70
69
  }
71
70
  loadEnvFile();
72
- const OXE_SUPABASE_URL = process.env.OXE_SUPABASE_URL;
73
- const OXE_SUPABASE_ANON_KEY = process.env.OXE_SUPABASE_ANON_KEY;
74
- if (!OXE_SUPABASE_URL || !OXE_SUPABASE_ANON_KEY) {
75
- console.error("Error: missing required environment variable OXE_SUPABASE_URL/OXE_SUPABASE_ANON_KEY.\n" +
76
- "Set it in your shell environment, or add it to a .env file next to this package.\n");
77
- process.exit(1);
78
- }
79
- export const OXE_SUPABASE_URL_EXPORT = OXE_SUPABASE_URL;
80
- export const OXE_SUPABASE_ANON_KEY_EXPORT = OXE_SUPABASE_ANON_KEY;
71
+ const DEFAULT_SUPABASE_URL = "https://ajmhaekffqzzrdfwsxfu.supabase.co";
72
+ const DEFAULT_SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImFqbWhhZWtmZnF6enJkZndzeGZ1Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODczMzU4MzQsImV4cCI6MjEwMjkxMTgzNH0.17sukij-R3L0kecR6x2MILZBO4M1ZZ4mkyqCU5yrVyM";
73
+ export const OXE_SUPABASE_URL_EXPORT = process.env.OXE_SUPABASE_URL || DEFAULT_SUPABASE_URL;
74
+ export const OXE_SUPABASE_ANON_KEY_EXPORT = process.env.OXE_SUPABASE_ANON_KEY || DEFAULT_SUPABASE_ANON_KEY;
81
75
  export const OXE_KEY_PREFIX = "oxe_live_";
82
- export const default_base_url = process.env.OXE_BASE_URL || `${OXE_SUPABASE_URL}/functions/v1/chat-proxy`;
76
+ export const default_base_url = process.env.OXE_BASE_URL ||
77
+ `${OXE_SUPABASE_URL_EXPORT}/functions/v1/chat-proxy`;
83
78
  export const default_model = process.env.OXE_MODEL_NAME || "deepseek-v4-flash";
84
79
  export const default_reasoning_effort = "low";
85
80
  // ---------------------------------------------------------------------------
@@ -90,17 +85,23 @@ export function runtimeOsSummary() {
90
85
  }
91
86
  export function osPrefix() {
92
87
  const isWin = process.platform === "win32";
93
- const shell = isWin ? "cmd.exe" : "POSIX sh";
88
+ const shell = isWin ? "PowerShell / cmd.exe" : "POSIX sh";
94
89
  return (`Running environment: ${runtimeOsSummary()}. ` +
95
90
  `Shell commands run via ${shell} ` +
96
91
  "with `shell=True`; use commands, quoting, and path separators " +
97
92
  "appropriate to this OS.\n\n");
98
93
  }
99
94
  export const IGNORED_DIRS_BY_CATEGORY = [
100
- ["Version control", [".git", ".svn"]],
101
- ["Dependencies & environments", ["node_modules", "__pycache__", ".venv", "venv"]],
102
- ["Build output", ["dist", "build", "target", ".tox"]],
103
- ["Tool caches & IDE files", [".mypy_cache", ".pytest_cache", ".next", ".idea"]],
95
+ ["Version control", [".git", ".svn", ".hg"]],
96
+ [
97
+ "Dependencies & environments",
98
+ ["node_modules", "__pycache__", ".venv", "venv", ".env"],
99
+ ],
100
+ ["Build output", ["dist", "build", "target", ".tox", "out", ".turbo"]],
101
+ [
102
+ "Tool caches & IDE files",
103
+ [".mypy_cache", ".pytest_cache", ".next", ".nuxt", ".idea", ".vscode"],
104
+ ],
104
105
  ];
105
106
  export const ignoredDirs = new Set(IGNORED_DIRS_BY_CATEGORY.flatMap(([, dirs]) => dirs));
106
107
  export const IGNORED_DIRS_SYSTEM_TEXT = IGNORED_DIRS_BY_CATEGORY.map(([cat, dirs]) => `- ${dirs.map((d) => `\`${d}\``).join(", ")} — ${cat}`).join("\n");
@@ -126,7 +127,8 @@ export const SYSTEM_PROMPT_BODY = "You are a terminal-based coding agent named O
126
127
  "- edit_file: make a small, precise change by replacing one unique snippet of text.\n" +
127
128
  "- bash: run shell commands (tests, git, package managers, build tools, etc).\n" +
128
129
  "- glob: find files by name pattern.\n" +
129
- "- grep: find text/regex matches across files.\n\n" +
130
+ "- grep: find text/regex matches across files.\n" +
131
+ "- load_skill: load domain instructions from an available skill.\n\n" +
130
132
  "Search scope:\n" +
131
133
  "- `glob` and `grep` automatically skip the following directories, so their\n" +
132
134
  " contents never appear in results and are never searched:\n" +
@@ -145,31 +147,28 @@ export const SYSTEM_PROMPT_BODY = "You are a terminal-based coding agent named O
145
147
  "consequence in your response.\n";
146
148
  export const API_KEY_MAX = 64;
147
149
  import { validateOxeApiKey } from "./api.js";
148
- import { renderPanel, markupToAnsi, hideCursor, showCursor } from "./ui.js";
150
+ import { renderPanel, markupToAnsi, hideCursor, showCursor, Spinner } from "./ui.js";
149
151
  export async function loadOrPrompt() {
150
- let api_key = "";
152
+ let api_key = (process.env.OXE_API_KEY || "").trim();
151
153
  let key_data = {};
152
154
  for (;;) {
153
155
  if (!api_key) {
154
- // The CLI banner ends with a trailing newline; write one more blank so
155
- // there is exactly one empty line between the banner and this panel
156
- // (mirrors Python's console.print() after the banner).
157
156
  process.stdout.write("\n");
158
157
  renderPanel("[bold white]Oxe Desktop Authentication[/bold white]\n\n" +
159
- `[dim]Only valid Oxe API keys (starting with [bold]${OXE_KEY_PREFIX}[/bold])\n` +
160
- "generated on your Oxe web dashboard are authorized to run this agent.\n\n" +
161
- "Get your key at: [bold underline]http://localhost:5173/dashboard/api-keys[/bold underline][/dim]", "Oxe Cloud Access", "", false);
158
+ `[dim]Only valid Oxe API keys (starting with [bold cyan]${OXE_KEY_PREFIX}[/bold cyan])\n` +
159
+ "generated on your Oxe dashboard are authorized to run this agent.\n\n" +
160
+ "Get your key at: [bold underline]http://localhost:5173/dashboard/api-keys[/bold underline][/dim]", "Oxe Cloud Access", "", false, "90", "left");
162
161
  process.stdout.write("\n");
163
162
  api_key = await promptApiKey("Enter Oxe API Key: ");
164
163
  if (!api_key) {
165
- process.stdout.write("\n" + markupToAnsi("[red]Error: Key cannot be blank. Exiting application.[/red]") + "\n");
164
+ process.stdout.write("\n" +
165
+ markupToAnsi("[red]Error: Key cannot be blank. Exiting application.[/red]") +
166
+ "\n\n");
166
167
  process.exit(1);
167
168
  }
168
169
  }
169
170
  process.stdout.write("\n");
170
171
  const authSpinner = new Spinner();
171
- // promptApiKey echoes input with the cursor visible; hide it while the
172
- // validating-key label runs so no stray block cursor sits beside it.
173
172
  hideCursor();
174
173
  authSpinner.start("Authenticating key with Oxe Cloud…");
175
174
  const validation = await validateOxeApiKey(api_key);
@@ -182,8 +181,7 @@ export async function loadOrPrompt() {
182
181
  break;
183
182
  }
184
183
  else {
185
- process.stdout.write(`\x1b[31mAuthentication Error:\x1b[0m ${validation.error}\n`);
186
- process.stdout.write("\n");
184
+ process.stdout.write(`\x1b[31mAuthentication Error:\x1b[0m ${validation.error}\n\n`);
187
185
  api_key = "";
188
186
  }
189
187
  }
@@ -224,10 +222,12 @@ export async function loadOrPrompt() {
224
222
  * Falls back to a plain readline read when stdin isn't a TTY (e.g. piped input).
225
223
  */
226
224
  export async function promptApiKey(prompt) {
227
- // Non-interactive (piped) input: just read a line, no masking possible.
228
225
  if (!process.stdin.isTTY) {
229
226
  const { default: readline } = await import("node:readline/promises");
230
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
227
+ const rl = readline.createInterface({
228
+ input: process.stdin,
229
+ output: process.stdout,
230
+ });
231
231
  try {
232
232
  const answer = await rl.question(prompt);
233
233
  return answer.replace(/\r?\n$/, "").trim();
@@ -244,7 +244,7 @@ export async function promptApiKey(prompt) {
244
244
  const onData = (buf) => {
245
245
  for (const b of buf) {
246
246
  if (b === 3) {
247
- // Ctrl+C: cleanup() already writes the terminating newline.
247
+ // Ctrl+C
248
248
  cleanup();
249
249
  reject(new Error("interrupt"));
250
250
  return;