@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 +69 -65
- package/dist/config.js +35 -35
- package/dist/engine.js +56 -43
- package/dist/skills.js +10 -4
- package/dist/system.js +2 -2
- package/dist/tools.js +134 -75
- package/dist/ui.js +396 -297
- package/package.json +1 -1
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 {
|
|
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
|
|
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
|
|
172
|
+
workStatus.start(`Working (${tickDuration(0)})`);
|
|
181
173
|
const workTimer = setInterval(() => {
|
|
182
|
-
workStatus.update(`Working
|
|
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:
|
|
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
|
|
257
|
+
status.start("Thinking `0s`");
|
|
267
258
|
}
|
|
268
259
|
else {
|
|
269
|
-
status?.update(`Thinking
|
|
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(`
|
|
306
|
+
throw new Error(`Response failed: ${response?.error ?? "unknown error"}`);
|
|
318
307
|
}
|
|
319
308
|
const usage = response?.usage;
|
|
320
309
|
if (usage) {
|
|
321
|
-
|
|
322
|
-
const outTok = usage?.output_tokens ??
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
stats["tokens"] = (stats["tokens"] ?? 0) +
|
|
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
|
|
406
|
+
comp.start("Compacting conversation `0s`");
|
|
418
407
|
const compTimer = setInterval(() => {
|
|
419
|
-
comp.update(`Compacting conversation
|
|
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 = {
|
|
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 = () =>
|
|
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" &&
|
|
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")
|
|
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
|
-
|
|
579
|
-
|
|
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({
|
|
587
|
-
|
|
588
|
-
|
|
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(
|
|
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
|
|
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({
|
|
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
|
|
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 =
|
|
19
|
+
export const SYSTEM_PROMPT = getSystemPrompt();
|