@oxecli/oxe 1.0.27 → 1.0.29
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 +17 -6
- package/dist/engine.js +18 -1
- package/dist/ui.js +8 -7
- package/package.json +1 -1
- package/README.md +0 -58
package/dist/cli.js
CHANGED
|
@@ -58,9 +58,8 @@ export class CLI {
|
|
|
58
58
|
titleAlign: "left",
|
|
59
59
|
colGap: 2,
|
|
60
60
|
});
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
process.stdout.write(panel + "\n");
|
|
61
|
+
// Keep one blank row between the previous response/footer and the panel.
|
|
62
|
+
process.stdout.write("\n" + panel + "\n");
|
|
64
63
|
if (process.stdin.isTTY) {
|
|
65
64
|
// Wait for a close key, then erase the panel (mirrors Python's Live).
|
|
66
65
|
enableRawStdin();
|
|
@@ -336,6 +335,7 @@ export class CLI {
|
|
|
336
335
|
for (;;) {
|
|
337
336
|
engine.inQuery = false;
|
|
338
337
|
let rawPrompt;
|
|
338
|
+
let queryStarted = false;
|
|
339
339
|
try {
|
|
340
340
|
const [input, spans] = await askBottomPrompt("You", "❯", this.promptHistory);
|
|
341
341
|
if (!input.trim())
|
|
@@ -390,7 +390,9 @@ export class CLI {
|
|
|
390
390
|
continue;
|
|
391
391
|
}
|
|
392
392
|
process.stdout.write(`\n\x1b[1m❯\x1b[0m ${userDisplayText(input, spans)}\n\n`);
|
|
393
|
+
queryStarted = true;
|
|
393
394
|
await engine.executeQuery(input, this.inputItems, this.story, spans);
|
|
395
|
+
queryStarted = false;
|
|
394
396
|
this.saveSession(engine);
|
|
395
397
|
}
|
|
396
398
|
catch (err) {
|
|
@@ -401,7 +403,7 @@ export class CLI {
|
|
|
401
403
|
break;
|
|
402
404
|
}
|
|
403
405
|
if (err?.message === "interrupt") {
|
|
404
|
-
const wasActive = engine.inQuery;
|
|
406
|
+
const wasActive = queryStarted || engine.inQuery;
|
|
405
407
|
engine.inQuery = false;
|
|
406
408
|
// Drop the pending user message/story entry that was never run.
|
|
407
409
|
if (this.inputItems.length && this.inputItems[this.inputItems.length - 1]["role"] === "user") {
|
|
@@ -422,8 +424,8 @@ export class CLI {
|
|
|
422
424
|
// (mirrors Python's console.print()).
|
|
423
425
|
process.stdout.write("\n");
|
|
424
426
|
process.stdout.write(wasActive
|
|
425
|
-
? "\x1b[2mInterrupted. Press Ctrl+C again to
|
|
426
|
-
: "\x1b[2mNo active response. Press Ctrl+C again to exit.\x1b[0m\n");
|
|
427
|
+
? "\x1b[2mInterrupted agent. Press Ctrl+C again to close the app, or type a new prompt.\x1b[0m\n"
|
|
428
|
+
: "\x1b[2mNo active response. Press Ctrl+C again to exit the app.\x1b[0m\n");
|
|
427
429
|
continue;
|
|
428
430
|
}
|
|
429
431
|
throw err;
|
|
@@ -451,10 +453,19 @@ export async function main() {
|
|
|
451
453
|
}
|
|
452
454
|
app.config = config;
|
|
453
455
|
const engine = new InferenceEngine(config);
|
|
456
|
+
const onInterrupt = () => {
|
|
457
|
+
if (engine.inQuery) {
|
|
458
|
+
engine.interrupt();
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
process.exit(130);
|
|
462
|
+
};
|
|
463
|
+
process.on("SIGINT", onInterrupt);
|
|
454
464
|
try {
|
|
455
465
|
await app.replLoop(engine);
|
|
456
466
|
}
|
|
457
467
|
finally {
|
|
468
|
+
process.removeListener("SIGINT", onInterrupt);
|
|
458
469
|
await engine.cleanupStoredResponses();
|
|
459
470
|
const client = engine.client;
|
|
460
471
|
if (typeof client?.close === "function")
|
package/dist/engine.js
CHANGED
|
@@ -47,6 +47,8 @@ export class InferenceEngine {
|
|
|
47
47
|
workActive = false;
|
|
48
48
|
workRows = 2;
|
|
49
49
|
inQuery = false;
|
|
50
|
+
activeAbort = null;
|
|
51
|
+
interrupted = false;
|
|
50
52
|
temperature;
|
|
51
53
|
storedResponseIds = [];
|
|
52
54
|
constructor(config) {
|
|
@@ -62,6 +64,10 @@ export class InferenceEngine {
|
|
|
62
64
|
});
|
|
63
65
|
this.temperature = config["temperature"] ?? null;
|
|
64
66
|
}
|
|
67
|
+
interrupt() {
|
|
68
|
+
this.interrupted = true;
|
|
69
|
+
this.activeAbort?.abort();
|
|
70
|
+
}
|
|
65
71
|
async cleanupStoredResponses() {
|
|
66
72
|
const ids = this.storedResponseIds;
|
|
67
73
|
this.storedResponseIds = [];
|
|
@@ -233,8 +239,12 @@ export class InferenceEngine {
|
|
|
233
239
|
kwargs["temperature"] = this.temperature;
|
|
234
240
|
if (previousResponseId)
|
|
235
241
|
kwargs["previous_response_id"] = previousResponseId;
|
|
236
|
-
const stream = await this.client.responses.create(kwargs
|
|
242
|
+
const stream = await this.client.responses.create(kwargs, {
|
|
243
|
+
signal: this.activeAbort?.signal,
|
|
244
|
+
});
|
|
237
245
|
for await (const event of stream) {
|
|
246
|
+
if (this.interrupted)
|
|
247
|
+
throw new Error("interrupt");
|
|
238
248
|
etype = event.type;
|
|
239
249
|
if (etype === "response.reasoning_text.delta" ||
|
|
240
250
|
etype === "response.reasoning.summary.delta") {
|
|
@@ -413,6 +423,8 @@ export class InferenceEngine {
|
|
|
413
423
|
}
|
|
414
424
|
async executeQuery(userPrompt, inputItems, story, pasteSpans) {
|
|
415
425
|
this.inQuery = true;
|
|
426
|
+
this.interrupted = false;
|
|
427
|
+
this.activeAbort = new AbortController();
|
|
416
428
|
hideCursor();
|
|
417
429
|
inputItems.push({
|
|
418
430
|
role: "user",
|
|
@@ -538,6 +550,8 @@ export class InferenceEngine {
|
|
|
538
550
|
if (c.name === "edit_file" || c.name === "write_file")
|
|
539
551
|
process.stdout.write("\n");
|
|
540
552
|
const rawResult = await this.runTool(c.name, c.arguments);
|
|
553
|
+
if (this.interrupted)
|
|
554
|
+
throw new Error("interrupt");
|
|
541
555
|
const failed = toolOutputFailed(c.name, rawResult);
|
|
542
556
|
const action = formatToolAction(c.name, c.arguments, failed ? "failed" : "ok");
|
|
543
557
|
story.push({ type: "tool", started, text: action, status: failed ? "failed" : "ok" });
|
|
@@ -568,6 +582,8 @@ export class InferenceEngine {
|
|
|
568
582
|
stripOrphanCalls(inputItems);
|
|
569
583
|
}
|
|
570
584
|
catch (err) {
|
|
585
|
+
if (this.interrupted)
|
|
586
|
+
throw new Error("interrupt");
|
|
571
587
|
if (err?.message === "interrupt" || err?.message === "eof") {
|
|
572
588
|
throw err;
|
|
573
589
|
}
|
|
@@ -575,6 +591,7 @@ export class InferenceEngine {
|
|
|
575
591
|
}
|
|
576
592
|
finally {
|
|
577
593
|
this.inQuery = false;
|
|
594
|
+
this.activeAbort = null;
|
|
578
595
|
if (stats["tokens"] > 0 && this.keyData) {
|
|
579
596
|
const userId = this.keyData["user_id"];
|
|
580
597
|
if (userId) {
|
package/dist/ui.js
CHANGED
|
@@ -300,14 +300,15 @@ export function tickDuration(seconds) {
|
|
|
300
300
|
// ---------------------------------------------------------------------------
|
|
301
301
|
const FENCE_MARKER_RE = /`{3,}/g;
|
|
302
302
|
export function printAiChunk(chunk) {
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
const
|
|
307
|
-
|
|
303
|
+
// A streaming commit can leave the next chunk beginning with the newlines
|
|
304
|
+
// that separate two paragraphs. Keep one of them so the response retains
|
|
305
|
+
// its intentional empty row without accumulating multiple blank rows.
|
|
306
|
+
const leading = chunk.match(/^\n+/)?.[0].length ?? 0;
|
|
307
|
+
const body = chunk.slice(leading);
|
|
308
|
+
if (!body)
|
|
308
309
|
return;
|
|
309
|
-
process.stdout.write(markdownToAnsi(
|
|
310
|
-
if (/^```/m.test(
|
|
310
|
+
process.stdout.write((leading ? "\n" : "") + markdownToAnsi(body) + "\n");
|
|
311
|
+
if (/^```/m.test(body))
|
|
311
312
|
process.stdout.write("\n");
|
|
312
313
|
}
|
|
313
314
|
export function safeCommitPoint(text) {
|
package/package.json
CHANGED
package/README.md
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
# Oxe — terminal AI coding agent
|
|
2
|
-
|
|
3
|
-
A terminal-based AI coding agent. This is a **native TypeScript** implementation
|
|
4
|
-
(pure Node.js, no Python required).
|
|
5
|
-
|
|
6
|
-
## Install
|
|
7
|
-
|
|
8
|
-
```bash
|
|
9
|
-
npm install -g @oxecli/oxe
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
The `oxe` command is provided by this package's `bin`.
|
|
13
|
-
|
|
14
|
-
## Usage
|
|
15
|
-
|
|
16
|
-
```bash
|
|
17
|
-
oxe
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
On first launch you'll be prompted for an **Oxe API key** (must start with
|
|
21
|
-
`oxe_live_`), generated on your Oxe dashboard.
|
|
22
|
-
|
|
23
|
-
## Interactive commands
|
|
24
|
-
|
|
25
|
-
| Command | Description |
|
|
26
|
-
| --- | --- |
|
|
27
|
-
| `/help` | Show available commands |
|
|
28
|
-
| `/resume` | List saved conversations |
|
|
29
|
-
| `/resume <n>` | Resume conversation number `n` |
|
|
30
|
-
| `/effort <none\|low\|high>` | Set reasoning effort |
|
|
31
|
-
| `/clear` | Save current conversation and start a new one |
|
|
32
|
-
| `/exit`, `/quit` | Save and exit |
|
|
33
|
-
|
|
34
|
-
## Configuration
|
|
35
|
-
|
|
36
|
-
Environment variables:
|
|
37
|
-
|
|
38
|
-
| Variable | Description | Default |
|
|
39
|
-
| --- | --- | --- |
|
|
40
|
-
| `OXE_DATA_DIR` | Where sessions are stored | `~/.oxe` |
|
|
41
|
-
| `OXE_BASE_URL` | Chat proxy base URL | bundled backend |
|
|
42
|
-
| `OXE_MODEL_NAME` | Model to use | `deepseek-v4-flash` |
|
|
43
|
-
| `OXE_REASONING_EFFORT` | Default reasoning effort | `low` |
|
|
44
|
-
| `OXE_TIMEOUT` / `OXE_MAX_RETRIES` / `OXE_TEMPERATURE` | Request tuning | |
|
|
45
|
-
|
|
46
|
-
Sessions and user data live under `~/.oxe/sessions`, never in `node_modules`.
|
|
47
|
-
|
|
48
|
-
## Development
|
|
49
|
-
|
|
50
|
-
```bash
|
|
51
|
-
npm install
|
|
52
|
-
npm run build # tsc -> dist/
|
|
53
|
-
npm install -g . # link as `oxe`
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
## License
|
|
57
|
-
|
|
58
|
-
MIT
|