@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/engine.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import OpenAI from "openai";
2
2
  import { max_output_tokens, max_empty_retries, max_agent_steps, max_context_tokens, context_overhead_margin, compact_keep_recent_turns, max_summary_source_chars, max_read_file_stored_chars, } from "./config.js";
3
- import { SYSTEM_PROMPT } from "./system.js";
3
+ import { getSystemPrompt } from "./system.js";
4
4
  import { buildTools, truncateToolOutput, toolReadFile, toolWriteFile, toolEditFile, toolBash, toolGlob, toolGrep, toolLoadSkill, } from "./tools.js";
5
5
  import { mutedMarkdown, aiMarkdown, tickDuration, displayRows, safeCommitPoint, printAiChunk, formatToolAction, renderPanel, Spinner, hideCursor, } from "./ui.js";
6
6
  import { reportUsage } from "./api.js";
7
7
  import { stripOrphanCalls, persistCompactionSummary, toolOutputFailed, } from "./sessions.js";
8
8
  // ---------------------------------------------------------------------------
9
- // Token estimation (heuristic, mirrors Python fallback)
9
+ // Token estimation
10
10
  // ---------------------------------------------------------------------------
11
11
  const tokenEstCache = new Map();
12
12
  const tokenEstCacheMax = 4096;
@@ -42,8 +42,6 @@ export class InferenceEngine {
42
42
  reasoningEffort;
43
43
  keyData;
44
44
  client;
45
- // "Worked for Xs" line is overwritten in place across tool iterations
46
- // (mirrors Python's _work_active / _overwrite_work_line).
47
45
  workActive = false;
48
46
  workRows = 2;
49
47
  inQuery = false;
@@ -59,7 +57,6 @@ export class InferenceEngine {
59
57
  this.client = new OpenAI({
60
58
  apiKey: config["api_key"],
61
59
  baseURL: config["base_url"],
62
- // OpenAI SDK `timeout` is in MILLISECONDS; config stores seconds.
63
60
  timeout: (config["timeout"] ?? 120) * 1000,
64
61
  maxRetries: config["max_retries"] ?? 2,
65
62
  });
@@ -90,11 +87,6 @@ export class InferenceEngine {
90
87
  args = {};
91
88
  }
92
89
  try {
93
- // Dispatch by explicit named parameters. Do NOT use Object.values(args)
94
- // (order-dependent): JSON object key order is not guaranteed, and when a
95
- // model emits e.g. {content, path} for write_file the content would be
96
- // mistaken for the path (turning JSX "/" into "\" on Windows). Binding
97
- // each argument by name makes the tools robust to any key ordering.
98
90
  let result;
99
91
  switch (name) {
100
92
  case "read_file":
@@ -142,8 +134,9 @@ export class InferenceEngine {
142
134
  if (reason)
143
135
  return `(empty response: ${reason})`;
144
136
  const status = response?.status;
145
- if (status && status !== "completed")
137
+ if (status && status !== "completed") {
146
138
  return `(empty response: status=${status})`;
139
+ }
147
140
  if (reasoned)
148
141
  return "(model reasoned but returned no text or tool call)";
149
142
  return "(empty response from the model — no text, no tool calls)";
@@ -151,7 +144,6 @@ export class InferenceEngine {
151
144
  incompleteReason(response) {
152
145
  return response?.incomplete_details?.reason ?? null;
153
146
  }
154
- /** Replace the previously-printed "Worked for Xs" line in place. */
155
147
  overwriteWorkLine(text) {
156
148
  for (let i = 0; i < this.workRows; i++) {
157
149
  process.stdout.write("\x1b[1A\x1b[2K");
@@ -177,9 +169,9 @@ export class InferenceEngine {
177
169
  let thinkingStart = null;
178
170
  const workStatus = new Spinner();
179
171
  const workingStarted = Date.now();
180
- workStatus.start(`Working for ${tickDuration(0)}`);
172
+ workStatus.start(`Working (${tickDuration(0)})`);
181
173
  const workTimer = setInterval(() => {
182
- workStatus.update(`Working for ${tickDuration((Date.now() - workingStarted) / 1000)}`);
174
+ workStatus.update(`Working (${tickDuration((Date.now() - workingStarted) / 1000)})`);
183
175
  }, 500);
184
176
  let workingReported = false;
185
177
  let response = null;
@@ -187,8 +179,6 @@ export class InferenceEngine {
187
179
  const silenceWorking = () => {
188
180
  clearInterval(workTimer);
189
181
  workStatus.stop();
190
- // Reasoning took over; suppress any later "Worked for Xs" report for this
191
- // stream (mirrors Python's working_started = None in silence_working()).
192
182
  workingReported = true;
193
183
  };
194
184
  const reportWorking = () => {
@@ -213,6 +203,7 @@ export class InferenceEngine {
213
203
  if (thinkingStart === null)
214
204
  return;
215
205
  const elapsed = (Date.now() - thinkingStart) / 1000;
206
+ stats["thinking"] = (stats["thinking"] ?? 0) + elapsed;
216
207
  if (status) {
217
208
  status.stop();
218
209
  status = null;
@@ -228,7 +219,7 @@ export class InferenceEngine {
228
219
  try {
229
220
  const kwargs = {
230
221
  model: this.modelName,
231
- instructions: SYSTEM_PROMPT,
222
+ instructions: getSystemPrompt(),
232
223
  input: inputItems.map((item) => {
233
224
  const copy = { ...item };
234
225
  delete copy["footer"];
@@ -263,10 +254,10 @@ export class InferenceEngine {
263
254
  if (thinkingStart === null) {
264
255
  thinkingStart = Date.now();
265
256
  status = new Spinner();
266
- status.start("Thinking for `0s`");
257
+ status.start("Thinking `0s`");
267
258
  }
268
259
  else {
269
- status?.update(`Thinking for ${tickDuration((Date.now() - thinkingStart) / 1000)}`);
260
+ status?.update(`Thinking ${tickDuration((Date.now() - thinkingStart) / 1000)}`);
270
261
  }
271
262
  }
272
263
  else if (etype === "response.reasoning_text.done" ||
@@ -299,8 +290,6 @@ export class InferenceEngine {
299
290
  finally {
300
291
  clearInterval(workTimer);
301
292
  workStatus.stop();
302
- // If the stream ended while still reasoning (no *.done event), still report
303
- // the elapsed "Thought for Xs" (mirrors Python's `still_thinking` handling).
304
293
  const stillThinking = thinkingStart !== null;
305
294
  finishThinking(stillThinking);
306
295
  if (this.interrupted && !this.queryHasOutput)
@@ -314,15 +303,15 @@ export class InferenceEngine {
314
303
  throw new Error("Stream ended without a completed response");
315
304
  }
316
305
  if (etype === "response.failed") {
317
- throw new Error(`DeepSeek response failed: ${response?.error}`);
306
+ throw new Error(`Response failed: ${response?.error ?? "unknown error"}`);
318
307
  }
319
308
  const usage = response?.usage;
320
309
  if (usage) {
321
- stats["input"] = (stats["input"] ?? 0) + (usage?.input_tokens ?? 0);
322
- const outTok = usage?.output_tokens ??
323
- usage?.total_tokens ??
324
- 0;
325
- stats["tokens"] = (stats["tokens"] ?? 0) + outTok;
310
+ const inTok = usage?.input_tokens ?? 0;
311
+ const outTok = usage?.output_tokens ?? 0;
312
+ const totalTok = usage?.total_tokens ?? inTok + outTok;
313
+ stats["input"] = (stats["input"] ?? 0) + inTok;
314
+ stats["tokens"] = (stats["tokens"] ?? 0) + totalTok;
326
315
  const reasoningTok = usage?.output_tokens_details?.reasoning_tokens ?? 0;
327
316
  stats["reasoning"] = (stats["reasoning"] ?? 0) + reasoningTok;
328
317
  }
@@ -414,9 +403,9 @@ export class InferenceEngine {
414
403
  return [conversation, false];
415
404
  const started = Date.now();
416
405
  const comp = new Spinner();
417
- comp.start("Compacting conversation for `0s`");
406
+ comp.start("Compacting conversation `0s`");
418
407
  const compTimer = setInterval(() => {
419
- comp.update(`Compacting conversation for ${tickDuration((Date.now() - started) / 1000)}`);
408
+ comp.update(`Compacting conversation ${tickDuration((Date.now() - started) / 1000)}`);
420
409
  }, 500);
421
410
  let compacted;
422
411
  try {
@@ -432,14 +421,11 @@ export class InferenceEngine {
432
421
  const text = `Compacted conversation in ${tickDuration((Date.now() - started) / 1000)}`;
433
422
  story.push({ type: "compacted", text });
434
423
  this.workActive = false;
435
- process.stdout.write(mutedMarkdown(text) + "\n");
436
- process.stdout.write("\n");
424
+ process.stdout.write(mutedMarkdown(text) + "\n\n");
437
425
  return [compacted, true];
438
426
  }
439
427
  async executeQuery(userPrompt, inputItems, story, pasteSpans) {
440
428
  this.inQuery = true;
441
- // A completed status line belongs to the previous turn. Never let its
442
- // replacement state reach the next user prompt.
443
429
  this.workActive = false;
444
430
  this.workRows = 0;
445
431
  this.interrupted = false;
@@ -456,9 +442,25 @@ export class InferenceEngine {
456
442
  [conversation] = await this.compactIfNeeded(conversation, inputItems, story);
457
443
  let emptyRetries = 0;
458
444
  const retryPrompts = [];
459
- const stats = { thinking: 0, tokens: 0, reasoning: 0, input: 0 };
445
+ const stats = {
446
+ thinking: 0,
447
+ tokens: 0,
448
+ reasoning: 0,
449
+ input: 0,
450
+ };
460
451
  const queryStart = Date.now();
461
- const footerText = () => `\x1b[2m(Thought for ${tickDuration(stats["thinking"])} · Worked for ${tickDuration((Date.now() - queryStart) / 1000)} · Used \x1b[22;37m${stats["tokens"].toLocaleString()}\x1b[2m tokens)\x1b[0m`;
452
+ const footerText = () => {
453
+ const parts = [];
454
+ if (stats["thinking"] > 0) {
455
+ parts.push(`Thought for ${tickDuration(stats["thinking"])}`);
456
+ }
457
+ parts.push(`Worked for ${tickDuration((Date.now() - queryStart) / 1000)}`);
458
+ if (stats["tokens"] > 0) {
459
+ parts.push(`Used \x1b[22;37m${stats["tokens"].toLocaleString()}\x1b[2m tokens`);
460
+ }
461
+ parts.push(`Effort: \x1b[1;36m${this.reasoningEffort}\x1b[0m\x1b[2m`);
462
+ return `\x1b[2m(${parts.join(" · ")})\x1b[0m`;
463
+ };
462
464
  const summary = () => mutedMarkdown(footerText());
463
465
  const attachFooter = (items) => {
464
466
  const footer = footerText();
@@ -537,7 +539,8 @@ export class InferenceEngine {
537
539
  pending = conversation;
538
540
  }
539
541
  if (emptyMessage !== null) {
540
- if (incompleteReason === "max_output_tokens" && emptyRetries < max_empty_retries) {
542
+ if (incompleteReason === "max_output_tokens" &&
543
+ emptyRetries < max_empty_retries) {
541
544
  emptyRetries++;
542
545
  const retryPrompt = {
543
546
  role: "user",
@@ -561,7 +564,11 @@ export class InferenceEngine {
561
564
  return;
562
565
  }
563
566
  if (!calls.length) {
564
- const gap = text.endsWith("\n\n") ? "" : text.endsWith("\n") ? "\n" : "\n\n";
567
+ const gap = text.endsWith("\n\n")
568
+ ? ""
569
+ : text.endsWith("\n")
570
+ ? "\n"
571
+ : "\n\n";
565
572
  process.stdout.write(gap + summary() + "\n");
566
573
  story.push({ type: "footer", text: footerText() });
567
574
  attachFooter(conversation);
@@ -575,17 +582,23 @@ export class InferenceEngine {
575
582
  for (const c of calls) {
576
583
  this.workActive = false;
577
584
  const started = formatToolAction(c.name, c.arguments, "started");
578
- process.stdout.write(`\x1b[2m${started}\x1b[0m\n`);
579
- if (c.name === "edit_file" || c.name === "write_file")
580
- process.stdout.write("\n");
585
+ const toolSpinner = new Spinner();
586
+ toolSpinner.start(started);
581
587
  const rawResult = await this.runTool(c.name, c.arguments);
588
+ toolSpinner.stop();
582
589
  if (this.interrupted)
583
590
  throw new Error("interrupt");
584
591
  const failed = toolOutputFailed(c.name, rawResult);
585
592
  const action = formatToolAction(c.name, c.arguments, failed ? "failed" : "ok");
586
- story.push({ type: "tool", started, text: action, status: failed ? "failed" : "ok" });
587
- const style = failed ? "\x1b[31m" : "\x1b[32m";
588
- process.stdout.write(`${style}${action}\x1b[0m\n\n`);
593
+ story.push({
594
+ type: "tool",
595
+ started,
596
+ text: action,
597
+ status: failed ? "failed" : "ok",
598
+ });
599
+ const icon = failed ? "\x1b[31m✗\x1b[0m" : "\x1b[32m✓\x1b[0m";
600
+ const actionStyle = failed ? "\x1b[31m" : "\x1b[32m";
601
+ process.stdout.write(`${icon} ${actionStyle}${action}\x1b[0m\n\n`);
589
602
  const truncatedResult = c.name === "read_file"
590
603
  ? truncateToolOutput(rawResult, max_read_file_stored_chars)
591
604
  : truncateToolOutput(rawResult);
package/dist/skills.js CHANGED
@@ -2,7 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import YAML from "yaml";
4
4
  import { SKILLS_DIR } from "./config.js";
5
- const FRONTMATTER_RE = /^---[ \t]*\r?\n(.*?)\r?\n---[ \t]*\r?\n?([\s\S]*)$/;
5
+ const FRONTMATTER_RE = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n?([\s\S]*)$/;
6
6
  function naturalSortKey(s) {
7
7
  return s.replace(/\d+/g, (m) => m.padStart(12, "0")).toLowerCase();
8
8
  }
@@ -34,7 +34,8 @@ function scanSkills() {
34
34
  .filter((d) => d.isDirectory())
35
35
  .sort((a, b) => naturalSortKey(a.name).localeCompare(naturalSortKey(b.name)));
36
36
  for (const sub of subs) {
37
- const mdPath = path.join(SKILLS_DIR, sub.name, "SKILL.md");
37
+ const skillDirPath = path.join(SKILLS_DIR, sub.name);
38
+ const mdPath = path.join(skillDirPath, "SKILL.md");
38
39
  if (!fs.existsSync(mdPath))
39
40
  continue;
40
41
  let text;
@@ -51,7 +52,12 @@ function scanSkills() {
51
52
  if (seen.has(name))
52
53
  continue;
53
54
  seen.add(name);
54
- skills.push({ name, description: meta["description"] ?? "", path: mdPath, dir: sub.name });
55
+ skills.push({
56
+ name,
57
+ description: meta["description"] ?? "",
58
+ path: mdPath,
59
+ dir: skillDirPath,
60
+ });
55
61
  }
56
62
  return skills;
57
63
  }
@@ -111,7 +117,7 @@ export function toolLoadSkill(skillName) {
111
117
  return "Error: skill_name must be a non-empty string.";
112
118
  }
113
119
  const skills = getSkills();
114
- const skill = skills.find((s) => s.name === skillName);
120
+ const skill = skills.find((s) => s.name === skillName.trim());
115
121
  if (!skill) {
116
122
  const available = skills.map((s) => s.name).join(", ") || "none";
117
123
  return `Error: unknown skill '${skillName}'. Available skills: ${available}`;
package/dist/system.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { osPrefix, SYSTEM_PROMPT_BODY } from "./config.js";
2
2
  import { getSkills } from "./skills.js";
3
- function buildSystemPrompt() {
3
+ export function getSystemPrompt() {
4
4
  const skills = getSkills();
5
5
  let skillsText = "";
6
6
  if (skills.length) {
@@ -16,4 +16,4 @@ function buildSystemPrompt() {
16
16
  }
17
17
  return osPrefix() + SYSTEM_PROMPT_BODY + skillsText;
18
18
  }
19
- export const SYSTEM_PROMPT = buildSystemPrompt();
19
+ export const SYSTEM_PROMPT = getSystemPrompt();