@oxecli/oxe 1.0.0
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/.env +2 -0
- package/LICENSE +21 -0
- package/README.md +58 -0
- package/bin/oxe.js +6 -0
- package/dist/api.js +64 -0
- package/dist/cli.js +284 -0
- package/dist/config.js +280 -0
- package/dist/engine.js +534 -0
- package/dist/oxe.js +6 -0
- package/dist/sessions.js +146 -0
- package/dist/skills.js +141 -0
- package/dist/system.js +19 -0
- package/dist/tools.js +856 -0
- package/dist/ui.js +569 -0
- package/package.json +49 -0
- package/skills/apple-design/SKILL.md +282 -0
- package/skills/react-native/SKILL.md +14 -0
- package/skills/react-native/references/structure.md +9 -0
- package/skills/react-native/scripts/scaffold.sh +3 -0
package/dist/ui.js
ADDED
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
import * as readline from "node:readline";
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// Terminal helpers
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
export function isWindows() {
|
|
6
|
+
return process.platform === "win32";
|
|
7
|
+
}
|
|
8
|
+
export function enableAnsi() {
|
|
9
|
+
// Node's tty on Windows Terminal / modern conhost supports ANSI automatically.
|
|
10
|
+
// No native SetConsoleMode available without a native module; rely on the host.
|
|
11
|
+
}
|
|
12
|
+
export function clearScreen() {
|
|
13
|
+
if (isWindows()) {
|
|
14
|
+
// crude but reliable clear
|
|
15
|
+
process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function terminalWidth() {
|
|
22
|
+
return process.stdout.columns || 80;
|
|
23
|
+
}
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Markup tag -> ANSI
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
const TAG_MAP = {
|
|
28
|
+
bold: "\x1b[1m",
|
|
29
|
+
dim: "\x1b[2m",
|
|
30
|
+
italic: "\x1b[3m",
|
|
31
|
+
underline: "\x1b[4m",
|
|
32
|
+
red: "\x1b[31m",
|
|
33
|
+
green: "\x1b[32m",
|
|
34
|
+
yellow: "\x1b[33m",
|
|
35
|
+
blue: "\x1b[34m",
|
|
36
|
+
magenta: "\x1b[35m",
|
|
37
|
+
cyan: "\x1b[36m",
|
|
38
|
+
white: "\x1b[37m",
|
|
39
|
+
};
|
|
40
|
+
const RESET = "\x1b[0m";
|
|
41
|
+
const MARKUP_RE = /\[(\/?)([a-z0-9 ]+)\]/gi;
|
|
42
|
+
export function escapeMarkup(text) {
|
|
43
|
+
return text.replace(/\[/g, "\\[").replace(/\]/g, "\\]");
|
|
44
|
+
}
|
|
45
|
+
export function markupToAnsi(text) {
|
|
46
|
+
let open = false;
|
|
47
|
+
return text.replace(MARKUP_RE, (m, slash, tag) => {
|
|
48
|
+
const tags = tag.trim().split(/\s+/);
|
|
49
|
+
if (slash) {
|
|
50
|
+
if (open) {
|
|
51
|
+
open = false;
|
|
52
|
+
return RESET;
|
|
53
|
+
}
|
|
54
|
+
return "";
|
|
55
|
+
}
|
|
56
|
+
let codes = "";
|
|
57
|
+
let known = false;
|
|
58
|
+
for (const t of tags) {
|
|
59
|
+
const code = TAG_MAP[t];
|
|
60
|
+
if (code) {
|
|
61
|
+
codes += code;
|
|
62
|
+
known = true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (!known)
|
|
66
|
+
return m;
|
|
67
|
+
open = true;
|
|
68
|
+
return codes;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Lightweight markdown -> ANSI
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
export function markdownToAnsi(text) {
|
|
75
|
+
const lines = text.split("\n");
|
|
76
|
+
const out = [];
|
|
77
|
+
let inCode = false;
|
|
78
|
+
let codeBuf = [];
|
|
79
|
+
const flushCode = () => {
|
|
80
|
+
if (codeBuf.length) {
|
|
81
|
+
out.push(`\x1b[2m${codeBuf.join("\n")}\x1b[0m`);
|
|
82
|
+
codeBuf = [];
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
for (const raw of lines) {
|
|
86
|
+
const line = raw;
|
|
87
|
+
const fence = line.match(/^(`{3,})/);
|
|
88
|
+
if (fence) {
|
|
89
|
+
if (!inCode) {
|
|
90
|
+
inCode = true;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
inCode = false;
|
|
95
|
+
flushCode();
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (inCode) {
|
|
100
|
+
codeBuf.push(line);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
let s = line;
|
|
104
|
+
// inline code
|
|
105
|
+
s = s.replace(/`([^`]+)`/g, (_, c) => `\x1b[2m${c}\x1b[0m`);
|
|
106
|
+
// bold
|
|
107
|
+
s = s.replace(/\*\*([^*]+)\*\*/g, "\x1b[1m$1\x1b[0m");
|
|
108
|
+
// italic
|
|
109
|
+
s = s.replace(/(^|[^\w*])\*([^*\n]+)\*(?=$|[^\w*])/g, "$1\x1b[3m$2\x1b[0m");
|
|
110
|
+
// headers
|
|
111
|
+
const h = s.match(/^(#{1,6})\s+(.*)$/);
|
|
112
|
+
if (h) {
|
|
113
|
+
s = `\x1b[1m${h[2]}\x1b[0m`;
|
|
114
|
+
}
|
|
115
|
+
// links
|
|
116
|
+
s = s.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
|
|
117
|
+
// leading list markers
|
|
118
|
+
s = s.replace(/^(\s*)([-*+]\s)/, "$1· ");
|
|
119
|
+
out.push(s);
|
|
120
|
+
}
|
|
121
|
+
flushCode();
|
|
122
|
+
return out.join("\n");
|
|
123
|
+
}
|
|
124
|
+
export function aiMarkdown(text) {
|
|
125
|
+
return markdownToAnsi(text);
|
|
126
|
+
}
|
|
127
|
+
export function mutedMarkdown(text) {
|
|
128
|
+
const dimmed = text
|
|
129
|
+
.split("\n")
|
|
130
|
+
.map((l) => `\x1b[2m${l}\x1b[0m`)
|
|
131
|
+
.join("\n");
|
|
132
|
+
return markdownToAnsi(dimmed);
|
|
133
|
+
}
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// Panel
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
export function renderPanel(content, title = "") {
|
|
138
|
+
const w = terminalWidth();
|
|
139
|
+
const innerW = Math.max(w - 4, 10);
|
|
140
|
+
const styled = markupToAnsi(content);
|
|
141
|
+
const plainLines = styled.replace(/\x1b\[[0-9;]*m/g, "").split("\n");
|
|
142
|
+
const border = "─".repeat(innerW);
|
|
143
|
+
console.log(`\x1b[90m+${border}+`);
|
|
144
|
+
if (title) {
|
|
145
|
+
const titleStr = ` ${title} `;
|
|
146
|
+
const pad = Math.max(innerW - titleStr.length, 0);
|
|
147
|
+
console.log(`\x1b[90m|${titleStr}${" ".repeat(pad)}|`);
|
|
148
|
+
}
|
|
149
|
+
const styledLines = styled.split("\n");
|
|
150
|
+
for (let i = 0; i < styledLines.length; i++) {
|
|
151
|
+
const line = styledLines[i];
|
|
152
|
+
const plain = plainLines[i] ?? "";
|
|
153
|
+
const pad = Math.max(innerW - plain.length, 0);
|
|
154
|
+
console.log(`\x1b[90m| ${line}${" ".repeat(pad)} |\x1b[0m`);
|
|
155
|
+
}
|
|
156
|
+
console.log(`\x1b[90m+${border}+\x1b[0m`);
|
|
157
|
+
}
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// Durations
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
export function formatDuration(seconds) {
|
|
162
|
+
const total = Math.max(0, Math.round(seconds));
|
|
163
|
+
const hours = Math.floor(total / 3600);
|
|
164
|
+
const rem = total % 3600;
|
|
165
|
+
const minutes = Math.floor(rem / 60);
|
|
166
|
+
const secs = rem % 60;
|
|
167
|
+
const parts = [];
|
|
168
|
+
if (hours)
|
|
169
|
+
parts.push(`${hours}h`);
|
|
170
|
+
if (minutes)
|
|
171
|
+
parts.push(`${minutes}min`);
|
|
172
|
+
if (secs || !parts.length)
|
|
173
|
+
parts.push(`${secs}s`);
|
|
174
|
+
return parts.join(" ");
|
|
175
|
+
}
|
|
176
|
+
export function tickDuration(seconds) {
|
|
177
|
+
return formatDuration(seconds).replace(/(\d+[a-z]*)/g, "`$1`");
|
|
178
|
+
}
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// Text helpers
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
const FENCE_MARKER_RE = /`{3,}/g;
|
|
183
|
+
export function printAiChunk(chunk) {
|
|
184
|
+
process.stdout.write(markdownToAnsi(chunk) + "\n");
|
|
185
|
+
if (/^```/m.test(chunk))
|
|
186
|
+
process.stdout.write("\n");
|
|
187
|
+
}
|
|
188
|
+
export function safeCommitPoint(text) {
|
|
189
|
+
let idx = text.lastIndexOf("\n\n");
|
|
190
|
+
while (idx !== -1) {
|
|
191
|
+
const fenceCount = (text.slice(0, idx).match(FENCE_MARKER_RE) || []).length;
|
|
192
|
+
if (fenceCount % 2 === 0)
|
|
193
|
+
return idx;
|
|
194
|
+
idx = text.lastIndexOf("\n\n", idx - 1);
|
|
195
|
+
}
|
|
196
|
+
return 0;
|
|
197
|
+
}
|
|
198
|
+
export function truncateEllipsis(text, maxChars, label = "text") {
|
|
199
|
+
if (text.length > maxChars) {
|
|
200
|
+
return text.slice(0, maxChars) + ` …(${label} truncated: ${text.length.toLocaleString()} chars total)`;
|
|
201
|
+
}
|
|
202
|
+
return text;
|
|
203
|
+
}
|
|
204
|
+
export function displayRows(text) {
|
|
205
|
+
if (!text)
|
|
206
|
+
return 1;
|
|
207
|
+
const plain = text.replace(/`/g, "");
|
|
208
|
+
const width = Math.max(terminalWidth() || 80, 1);
|
|
209
|
+
let rows = 0;
|
|
210
|
+
const lines = plain.split("\n");
|
|
211
|
+
if (!lines.length)
|
|
212
|
+
return 1;
|
|
213
|
+
for (const line of lines) {
|
|
214
|
+
rows += Math.max(1, Math.ceil(line.length / width));
|
|
215
|
+
}
|
|
216
|
+
return rows;
|
|
217
|
+
}
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
// Tool action formatter
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
export function formatToolAction(name, argumentsJson, status = "ok") {
|
|
222
|
+
let args = {};
|
|
223
|
+
try {
|
|
224
|
+
args = argumentsJson ? JSON.parse(argumentsJson) : {};
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
args = {};
|
|
228
|
+
}
|
|
229
|
+
const phrase = (verbOk, verbIng, verbFail, detail) => {
|
|
230
|
+
if (status === "started")
|
|
231
|
+
return `${verbIng} ${detail}`;
|
|
232
|
+
if (status === "failed")
|
|
233
|
+
return `${verbFail} ${detail}`;
|
|
234
|
+
return `${verbOk} ${detail}`;
|
|
235
|
+
};
|
|
236
|
+
const p = args["path"] ?? "";
|
|
237
|
+
switch (name) {
|
|
238
|
+
case "read_file": {
|
|
239
|
+
const start = args["start_line"];
|
|
240
|
+
const end = args["end_line"];
|
|
241
|
+
const span = start != null || end != null
|
|
242
|
+
? `, lines ${start ?? 1} to ${end ?? "end"}`
|
|
243
|
+
: "";
|
|
244
|
+
return phrase("Read", "Reading", "Failed to read", `${p}${span}`);
|
|
245
|
+
}
|
|
246
|
+
case "write_file":
|
|
247
|
+
return phrase("Wrote", "Writing", "Failed to write", `${p} (${String(args["content"] ?? "").length} chars)`);
|
|
248
|
+
case "edit_file": {
|
|
249
|
+
const suffix = args["replace_all"] ? " (replace all)" : "";
|
|
250
|
+
return phrase("Edited", "Editing", "Failed to edit", `${p}${suffix}`);
|
|
251
|
+
}
|
|
252
|
+
case "bash": {
|
|
253
|
+
const cwd = args["cwd"];
|
|
254
|
+
const location = cwd ? ` in ${cwd}` : "";
|
|
255
|
+
const command = truncateEllipsis(String(args["command"] ?? ""), 400, "command");
|
|
256
|
+
if (status === "started")
|
|
257
|
+
return `Running command${location}: ${command}`;
|
|
258
|
+
if (status === "failed")
|
|
259
|
+
return `Command failed${location}: ${command}`;
|
|
260
|
+
return `Ran command${location}: ${command}`;
|
|
261
|
+
}
|
|
262
|
+
case "glob": {
|
|
263
|
+
const detail = String(args["pattern"] ?? "");
|
|
264
|
+
if (status === "failed")
|
|
265
|
+
return `Search failed for ${detail}`;
|
|
266
|
+
return phrase("Searched for", "Searching for", "", detail);
|
|
267
|
+
}
|
|
268
|
+
case "grep": {
|
|
269
|
+
const detail = String(args["pattern"] ?? "");
|
|
270
|
+
if (status === "failed")
|
|
271
|
+
return `Grep failed for ${detail}`;
|
|
272
|
+
return phrase("Grepped for", "Grepping for", "", detail);
|
|
273
|
+
}
|
|
274
|
+
case "load_skill": {
|
|
275
|
+
const skill = String(args["skill_name"] ?? "");
|
|
276
|
+
return phrase("Loaded skill", "Loading skill", "Failed to load skill", skill);
|
|
277
|
+
}
|
|
278
|
+
default:
|
|
279
|
+
return phrase("Invoked internal routine", "Invoking internal routine", "Internal routine failed", name);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
// Prompt editing / interactive input
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
export const PROMPT_PLACEHOLDER = "Describe a coding task, or type /help to see available commands";
|
|
286
|
+
const MAX_PROMPT_DISPLAY_LINES = 12;
|
|
287
|
+
const MAX_PROMPT_PASTE_CHARS = 400;
|
|
288
|
+
function shouldCollapsePaste(text) {
|
|
289
|
+
return (text.split("\n").length > MAX_PROMPT_DISPLAY_LINES ||
|
|
290
|
+
text.length > MAX_PROMPT_PASTE_CHARS);
|
|
291
|
+
}
|
|
292
|
+
export function cursorLineCol(buffer, cursor) {
|
|
293
|
+
const before = buffer.slice(0, cursor);
|
|
294
|
+
const lines = before.split("\n");
|
|
295
|
+
return [lines.length - 1, lines[lines.length - 1].length];
|
|
296
|
+
}
|
|
297
|
+
function endsWithWs(s) {
|
|
298
|
+
return s.length > 0 && /[ \t\n]/.test(s[s.length - 1]);
|
|
299
|
+
}
|
|
300
|
+
export function splitBlocks(buffer, pasteSpans) {
|
|
301
|
+
if (!buffer)
|
|
302
|
+
return [["", "text", ""]];
|
|
303
|
+
const pts = new Set([0, buffer.length]);
|
|
304
|
+
for (const [s, e] of pasteSpans) {
|
|
305
|
+
pts.add(s);
|
|
306
|
+
pts.add(e);
|
|
307
|
+
}
|
|
308
|
+
const sorted = [...pts].sort((a, b) => a - b);
|
|
309
|
+
const segs = [];
|
|
310
|
+
const isPaste = (a, b) => pasteSpans.some(([s, e]) => s <= a && b <= e);
|
|
311
|
+
for (let i = 0; i < sorted.length - 1; i++) {
|
|
312
|
+
const a = sorted[i];
|
|
313
|
+
const b = sorted[i + 1];
|
|
314
|
+
if (a === b)
|
|
315
|
+
continue;
|
|
316
|
+
const text = buffer.slice(a, b);
|
|
317
|
+
if (isPaste(a, b) && shouldCollapsePaste(text)) {
|
|
318
|
+
const n = text.split("\n").length;
|
|
319
|
+
segs.push(["", "collapsed", `[Pasted text, ${n} lines]`]);
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
segs.push(["", "text", text]);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return segs;
|
|
326
|
+
}
|
|
327
|
+
function renderBufferWithCursor(buffer, pasteSpans, prefix, cursor) {
|
|
328
|
+
const [row, col] = cursorLineCol(buffer, cursor);
|
|
329
|
+
const segs = splitBlocks(buffer, pasteSpans);
|
|
330
|
+
let display = "";
|
|
331
|
+
for (let i = 0; i < segs.length; i++) {
|
|
332
|
+
const [, kind, disp] = segs[i];
|
|
333
|
+
if (i && !endsWithWs(segs[i - 1][2]))
|
|
334
|
+
display += " ";
|
|
335
|
+
display += kind === "collapsed" ? `\x1b[1m\x1b[36m${disp}\x1b[0m` : disp;
|
|
336
|
+
}
|
|
337
|
+
const lines = display.split("\n");
|
|
338
|
+
const prefixStr = `\x1b[1m${prefix}\x1b[0m `;
|
|
339
|
+
// Reconstruct lines, prepending prefix to first line
|
|
340
|
+
const rendered = [prefixStr + (lines[0] ?? "")];
|
|
341
|
+
for (let i = 1; i < lines.length; i++)
|
|
342
|
+
rendered.push(lines[i] ?? "");
|
|
343
|
+
// Place cursor on the correct line/col (prefix+1 offset on line 0)
|
|
344
|
+
let cursorRow = row;
|
|
345
|
+
let cursorCol = col + (row === 0 ? prefix.length + 1 : 0);
|
|
346
|
+
const result = rendered.join("\n");
|
|
347
|
+
const currentLines = result.split("\n").length;
|
|
348
|
+
// Move cursor up currentLines-1 then right cursorCol
|
|
349
|
+
let cmd = "";
|
|
350
|
+
if (currentLines > 1)
|
|
351
|
+
cmd += `\x1b[${currentLines - 1}A`;
|
|
352
|
+
cmd += `\r\x1b[${cursorCol}C`;
|
|
353
|
+
return result + cmd;
|
|
354
|
+
}
|
|
355
|
+
export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
|
|
356
|
+
return new Promise((resolve, reject) => {
|
|
357
|
+
const isTTY = process.stdin.isTTY;
|
|
358
|
+
if (!isTTY) {
|
|
359
|
+
// Non-interactive: read a line from stdin
|
|
360
|
+
const chunks = [];
|
|
361
|
+
process.stdin.setEncoding("utf-8");
|
|
362
|
+
process.stdin.on("data", (d) => chunks.push(Buffer.from(String(d))));
|
|
363
|
+
process.stdin.on("end", () => {
|
|
364
|
+
const text = Buffer.concat(chunks).toString();
|
|
365
|
+
resolve([text.trim(), []]);
|
|
366
|
+
});
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
let buffer = "";
|
|
370
|
+
let cursor = 0;
|
|
371
|
+
let pasteSpans = [];
|
|
372
|
+
const hist = history.slice();
|
|
373
|
+
let histIdx = hist.length;
|
|
374
|
+
let draft = null;
|
|
375
|
+
readline.emitKeypressEvents(process.stdin);
|
|
376
|
+
if (process.stdin.isTTY)
|
|
377
|
+
process.stdin.setRawMode(true);
|
|
378
|
+
process.stdin.resume();
|
|
379
|
+
let done = false;
|
|
380
|
+
const finish = (resolveVal) => {
|
|
381
|
+
if (done)
|
|
382
|
+
return;
|
|
383
|
+
done = true;
|
|
384
|
+
process.stdin.setRawMode(false);
|
|
385
|
+
process.stdout.write("\r\x1b[K");
|
|
386
|
+
process.stdout.write("\n");
|
|
387
|
+
resolve(resolveVal);
|
|
388
|
+
};
|
|
389
|
+
const repaint = () => {
|
|
390
|
+
const frame = renderBufferWithCursor(buffer, pasteSpans, prefix, cursor);
|
|
391
|
+
process.stdout.write("\r\x1b[K");
|
|
392
|
+
process.stdout.write(frame);
|
|
393
|
+
};
|
|
394
|
+
const insert = (text) => {
|
|
395
|
+
if (histIdx !== hist.length) {
|
|
396
|
+
draft = { buffer, cursor, spans: pasteSpans.slice() };
|
|
397
|
+
histIdx = hist.length;
|
|
398
|
+
}
|
|
399
|
+
buffer = buffer.slice(0, cursor) + text + buffer.slice(cursor);
|
|
400
|
+
cursor += text.length;
|
|
401
|
+
};
|
|
402
|
+
const backspace = () => {
|
|
403
|
+
if (cursor <= 0)
|
|
404
|
+
return;
|
|
405
|
+
if (histIdx !== hist.length) {
|
|
406
|
+
draft = { buffer, cursor, spans: pasteSpans.slice() };
|
|
407
|
+
histIdx = hist.length;
|
|
408
|
+
}
|
|
409
|
+
// remove one grapheme before cursor
|
|
410
|
+
const before = Array.from(buffer.slice(0, cursor));
|
|
411
|
+
before.pop();
|
|
412
|
+
buffer = before.join("") + buffer.slice(cursor);
|
|
413
|
+
cursor -= 1;
|
|
414
|
+
};
|
|
415
|
+
const moveLeft = () => {
|
|
416
|
+
if (cursor > 0)
|
|
417
|
+
cursor -= 1;
|
|
418
|
+
};
|
|
419
|
+
const moveRight = () => {
|
|
420
|
+
if (cursor < buffer.length)
|
|
421
|
+
cursor += 1;
|
|
422
|
+
};
|
|
423
|
+
const moveUp = () => {
|
|
424
|
+
if (!buffer.includes("\n")) {
|
|
425
|
+
if (!hist.length)
|
|
426
|
+
return;
|
|
427
|
+
if (histIdx === hist.length) {
|
|
428
|
+
draft = { buffer, cursor, spans: pasteSpans.slice() };
|
|
429
|
+
}
|
|
430
|
+
if (histIdx > 0) {
|
|
431
|
+
histIdx -= 1;
|
|
432
|
+
buffer = hist[histIdx];
|
|
433
|
+
cursor = buffer.length;
|
|
434
|
+
pasteSpans = [];
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
const moveDown = () => {
|
|
439
|
+
if (!buffer.includes("\n")) {
|
|
440
|
+
if (histIdx === hist.length || !draft)
|
|
441
|
+
return;
|
|
442
|
+
histIdx += 1;
|
|
443
|
+
if (histIdx === hist.length) {
|
|
444
|
+
buffer = draft.buffer;
|
|
445
|
+
cursor = draft.cursor;
|
|
446
|
+
pasteSpans = draft.spans.slice();
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
buffer = hist[histIdx];
|
|
450
|
+
cursor = buffer.length;
|
|
451
|
+
pasteSpans = [];
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
const onKeypress = (str, key) => {
|
|
456
|
+
if (key && key.ctrl && key.name === "c") {
|
|
457
|
+
finish(["", []]);
|
|
458
|
+
reject(new Error("interrupt"));
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (key && key.ctrl && (key.name === "d" || key.name === "z")) {
|
|
462
|
+
finish([buffer, []]);
|
|
463
|
+
reject(new Error("eof"));
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (key && key.name === "return") {
|
|
467
|
+
if (buffer.trim()) {
|
|
468
|
+
finish([buffer, pasteSpans]);
|
|
469
|
+
}
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
if (key && key.name === "backspace") {
|
|
473
|
+
backspace();
|
|
474
|
+
repaint();
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (key && key.name === "left") {
|
|
478
|
+
moveLeft();
|
|
479
|
+
repaint();
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
if (key && key.name === "right") {
|
|
483
|
+
moveRight();
|
|
484
|
+
repaint();
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
if (key && key.name === "up") {
|
|
488
|
+
moveUp();
|
|
489
|
+
repaint();
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
if (key && key.name === "down") {
|
|
493
|
+
moveDown();
|
|
494
|
+
repaint();
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
if (key && key.name === "tab") {
|
|
498
|
+
insert(" ");
|
|
499
|
+
repaint();
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
if (key && key.name === "space") {
|
|
503
|
+
insert(" ");
|
|
504
|
+
repaint();
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
if (str) {
|
|
508
|
+
// paste / multi-char sequence
|
|
509
|
+
insert(str);
|
|
510
|
+
repaint();
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
process.stdin.on("keypress", onKeypress);
|
|
514
|
+
repaint();
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
// ---------------------------------------------------------------------------
|
|
518
|
+
// User display + message text
|
|
519
|
+
// ---------------------------------------------------------------------------
|
|
520
|
+
export function userDisplayText(payload, pasteSpans) {
|
|
521
|
+
if (pasteSpans && pasteSpans.length) {
|
|
522
|
+
const segs = splitBlocks(payload, pasteSpans);
|
|
523
|
+
let out = "";
|
|
524
|
+
for (let i = 0; i < segs.length; i++) {
|
|
525
|
+
const [, kind, disp] = segs[i];
|
|
526
|
+
if (i && !endsWithWs(segs[i - 1][2]))
|
|
527
|
+
out += " ";
|
|
528
|
+
out += kind === "collapsed" ? `\x1b[1m\x1b[36m${disp}\x1b[0m` : disp;
|
|
529
|
+
}
|
|
530
|
+
return out;
|
|
531
|
+
}
|
|
532
|
+
if (shouldCollapsePaste(payload)) {
|
|
533
|
+
return `\x1b[1m\x1b[36m[Pasted text, ${payload.split("\n").length} lines]\x1b[0m`;
|
|
534
|
+
}
|
|
535
|
+
return payload;
|
|
536
|
+
}
|
|
537
|
+
export function collapseLabelText(text, spans) {
|
|
538
|
+
if (spans && spans.length) {
|
|
539
|
+
const segs = splitBlocks(text, spans);
|
|
540
|
+
const parts = [];
|
|
541
|
+
for (const [, kind, disp] of segs) {
|
|
542
|
+
if (kind === "collapsed") {
|
|
543
|
+
parts.push(disp.replace(/\[[^\]]+\]/g, "").trim());
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
parts.push(disp);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return parts.filter(Boolean).join(" ");
|
|
550
|
+
}
|
|
551
|
+
if (shouldCollapsePaste(text)) {
|
|
552
|
+
return `[Pasted text, ${text.split("\n").length} lines]`;
|
|
553
|
+
}
|
|
554
|
+
return text;
|
|
555
|
+
}
|
|
556
|
+
export function messageText(item) {
|
|
557
|
+
const content = item["content"];
|
|
558
|
+
if (typeof content === "string")
|
|
559
|
+
return content;
|
|
560
|
+
if (Array.isArray(content)) {
|
|
561
|
+
const parts = [];
|
|
562
|
+
for (const part of content) {
|
|
563
|
+
if (part && typeof part === "object" && part["text"])
|
|
564
|
+
parts.push(part["text"]);
|
|
565
|
+
}
|
|
566
|
+
return parts.join("\n");
|
|
567
|
+
}
|
|
568
|
+
return "";
|
|
569
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@oxecli/oxe",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"description": "Oxe - a terminal-based AI coding agent (native TypeScript CLI).",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"ai",
|
|
10
|
+
"coding-agent",
|
|
11
|
+
"cli",
|
|
12
|
+
"terminal",
|
|
13
|
+
"coding",
|
|
14
|
+
"agent",
|
|
15
|
+
"oxe"
|
|
16
|
+
],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"type": "module",
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=18"
|
|
21
|
+
},
|
|
22
|
+
"bin": {
|
|
23
|
+
"oxe": "bin/oxe.js"
|
|
24
|
+
},
|
|
25
|
+
"main": "dist/oxe.js",
|
|
26
|
+
"files": [
|
|
27
|
+
"bin/",
|
|
28
|
+
"dist/",
|
|
29
|
+
"skills/",
|
|
30
|
+
".env",
|
|
31
|
+
"README.md",
|
|
32
|
+
"LICENSE"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc -p tsconfig.json",
|
|
36
|
+
"prepare": "npm run build",
|
|
37
|
+
"start": "node dist/oxe.js"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"diff": "^5.2.0",
|
|
41
|
+
"openai": "^4.0.0",
|
|
42
|
+
"yaml": "^2.0.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/diff": "^7.0.2",
|
|
46
|
+
"@types/node": "^20.0.0",
|
|
47
|
+
"typescript": "^5.0.0"
|
|
48
|
+
}
|
|
49
|
+
}
|