@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/config.js
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Paths
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
/** Package root (parent of dist/). Holds .env, skills/. */
|
|
10
|
+
export const PROJECT_DIR = path.resolve(__dirname, "..");
|
|
11
|
+
/** Where user data lives. Defaults to ~/.oxe so it survives reinstalls. */
|
|
12
|
+
export const DATA_DIR = path.resolve(process.env.OXE_DATA_DIR || path.join(os.homedir(), ".oxe"));
|
|
13
|
+
export const SESSION_DIR = path.join(DATA_DIR, "sessions");
|
|
14
|
+
export const SKILLS_DIR = path.join(PROJECT_DIR, "skills");
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Tunables
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
export const max_output_chars = 8000;
|
|
19
|
+
export const max_read_lines = 1500;
|
|
20
|
+
export const max_read_line_chars = 500;
|
|
21
|
+
export const max_read_file_bytes = 50 * 1024 * 1024;
|
|
22
|
+
export const max_diff_lines = 16;
|
|
23
|
+
export const max_diff_context_lines = 6;
|
|
24
|
+
export const max_diff_line_chars = 400;
|
|
25
|
+
export const max_agent_steps = 50;
|
|
26
|
+
export const max_context_tokens = 256_000;
|
|
27
|
+
export const context_overhead_margin = 32_000;
|
|
28
|
+
export const compact_keep_recent_turns = 4;
|
|
29
|
+
export const max_summary_source_chars = 200_000;
|
|
30
|
+
export const max_output_tokens = 16384;
|
|
31
|
+
export const max_empty_retries = 2;
|
|
32
|
+
export const max_diff_source_chars = 1_000_000;
|
|
33
|
+
export const max_grep_file_bytes = 20 * 1024 * 1024;
|
|
34
|
+
export const max_read_file_stored_chars = 40_000;
|
|
35
|
+
export const max_command_chars = 400;
|
|
36
|
+
export const max_action_chars = 500;
|
|
37
|
+
export const max_resume_history_items = 24;
|
|
38
|
+
export const max_bash_timeout_seconds = 300;
|
|
39
|
+
export const strict_max_properties = 2;
|
|
40
|
+
export const ai_style = "grey62";
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// .env loading
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
function loadEnvFile() {
|
|
45
|
+
const envPath = path.join(PROJECT_DIR, ".env");
|
|
46
|
+
if (!fs.existsSync(envPath))
|
|
47
|
+
return;
|
|
48
|
+
try {
|
|
49
|
+
const text = fs.readFileSync(envPath, "utf-8");
|
|
50
|
+
for (const rawLine of text.split("\n")) {
|
|
51
|
+
const line = rawLine.trim();
|
|
52
|
+
if (!line || line.startsWith("#") || !line.includes("="))
|
|
53
|
+
continue;
|
|
54
|
+
const idx = line.indexOf("=");
|
|
55
|
+
const key = line.slice(0, idx).trim();
|
|
56
|
+
let value = line.slice(idx + 1).trim();
|
|
57
|
+
if (value.length >= 2 &&
|
|
58
|
+
value[0] === value[value.length - 1] &&
|
|
59
|
+
(value[0] === '"' || value[0] === "'")) {
|
|
60
|
+
value = value.slice(1, -1);
|
|
61
|
+
}
|
|
62
|
+
if (process.env[key] === undefined)
|
|
63
|
+
process.env[key] = value;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
/* ignore */
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
loadEnvFile();
|
|
71
|
+
const OXE_SUPABASE_URL = process.env.OXE_SUPABASE_URL;
|
|
72
|
+
const OXE_SUPABASE_ANON_KEY = process.env.OXE_SUPABASE_ANON_KEY;
|
|
73
|
+
if (!OXE_SUPABASE_URL || !OXE_SUPABASE_ANON_KEY) {
|
|
74
|
+
console.error("Error: missing required environment variable OXE_SUPABASE_URL/OXE_SUPABASE_ANON_KEY.\n" +
|
|
75
|
+
"Set it in your shell environment, or add it to a .env file next to this package.\n");
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
export const OXE_SUPABASE_URL_EXPORT = OXE_SUPABASE_URL;
|
|
79
|
+
export const OXE_SUPABASE_ANON_KEY_EXPORT = OXE_SUPABASE_ANON_KEY;
|
|
80
|
+
export const OXE_KEY_PREFIX = "oxe_live_";
|
|
81
|
+
export const default_base_url = process.env.OXE_BASE_URL || `${OXE_SUPABASE_URL}/functions/v1/chat-proxy`;
|
|
82
|
+
export const default_model = process.env.OXE_MODEL_NAME || "deepseek-v4-flash";
|
|
83
|
+
export const default_reasoning_effort = "low";
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// OS / system context
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
export function runtimeOsSummary() {
|
|
88
|
+
return `${os.platform()} ${os.release()} (${os.arch()}, Node ${process.version})`;
|
|
89
|
+
}
|
|
90
|
+
export function osPrefix() {
|
|
91
|
+
const isWin = process.platform === "win32";
|
|
92
|
+
const shell = isWin ? "cmd.exe" : "POSIX sh";
|
|
93
|
+
return (`Running environment: ${runtimeOsSummary()}. ` +
|
|
94
|
+
`Shell commands run via ${shell} ` +
|
|
95
|
+
"with `shell=True`; use commands, quoting, and path separators " +
|
|
96
|
+
"appropriate to this OS.\n\n");
|
|
97
|
+
}
|
|
98
|
+
export const IGNORED_DIRS_BY_CATEGORY = [
|
|
99
|
+
["Version control", [".git", ".svn"]],
|
|
100
|
+
["Dependencies & environments", ["node_modules", "__pycache__", ".venv", "venv"]],
|
|
101
|
+
["Build output", ["dist", "build", "target", ".tox"]],
|
|
102
|
+
["Tool caches & IDE files", [".mypy_cache", ".pytest_cache", ".next", ".idea"]],
|
|
103
|
+
];
|
|
104
|
+
export const ignoredDirs = new Set(IGNORED_DIRS_BY_CATEGORY.flatMap(([, dirs]) => dirs));
|
|
105
|
+
export const IGNORED_DIRS_SYSTEM_TEXT = IGNORED_DIRS_BY_CATEGORY.map(([cat, dirs]) => `- ${dirs.map((d) => `\`${d}\``).join(", ")} — ${cat}`).join("\n");
|
|
106
|
+
export const SYSTEM_PROMPT_BODY = "You are a terminal-based coding agent named Oxe. You help with software engineering " +
|
|
107
|
+
"tasks by reading and editing files, running commands, and searching code, directly on the " +
|
|
108
|
+
"user's machine. You are a concise agent.\n\n" +
|
|
109
|
+
"CRITICAL: APPLY CHANGES, DON'T JUST DESCRIBE THEM:\n" +
|
|
110
|
+
"When the user asks you to make, fix, add, or refactor something, you MUST call write_file or " +
|
|
111
|
+
"edit_file to actually apply that change to the files on disk. Never respond with a plan, a " +
|
|
112
|
+
"code snippet, or instructions telling the user what to change instead of changing it yourself. " +
|
|
113
|
+
"Only describe a change in prose without applying it if you genuinely cannot proceed (e.g. the " +
|
|
114
|
+
"request is ambiguous and you need one clarifying answer first) — and say so explicitly rather " +
|
|
115
|
+
"than silently defaulting to a description.\n\n" +
|
|
116
|
+
"CRITICAL HISTORY TRACKING RULE:\n" +
|
|
117
|
+
"You must strictly keep track of your previous tool outputs during this conversation loop. If a " +
|
|
118
|
+
"tool call returns an empty string, error, or doesn't yield what you expected, DO NOT execute " +
|
|
119
|
+
"the exact same tool call with the exact same arguments again. Analyze why it failed, explicitly " +
|
|
120
|
+
"acknowledge the strategy change in your text response block, and try a completely different approach " +
|
|
121
|
+
"(e.g., changing search terms, looking at alternative files, or utilizing `bash`).\n\n" +
|
|
122
|
+
"You have these tools:\n" +
|
|
123
|
+
"- read_file: inspect a file's contents before changing it.\n" +
|
|
124
|
+
"- write_file: create a file, or replace one entirely.\n" +
|
|
125
|
+
"- edit_file: make a small, precise change by replacing one unique snippet of text.\n" +
|
|
126
|
+
"- bash: run shell commands (tests, git, package managers, build tools, etc).\n" +
|
|
127
|
+
"- glob: find files by name pattern.\n" +
|
|
128
|
+
"- grep: find text/regex matches across files.\n\n" +
|
|
129
|
+
"Search scope:\n" +
|
|
130
|
+
"- `glob` and `grep` automatically skip the following directories, so their\n" +
|
|
131
|
+
" contents never appear in results and are never searched:\n" +
|
|
132
|
+
IGNORED_DIRS_SYSTEM_TEXT +
|
|
133
|
+
"\n\n" +
|
|
134
|
+
"Guidelines:\n" +
|
|
135
|
+
"1. Always read a file with read_file before editing it with edit_file, so your old_string " +
|
|
136
|
+
"matches exactly.\n" +
|
|
137
|
+
"2. Prefer edit_file for targeted changes and write_file only for new files or full rewrites.\n" +
|
|
138
|
+
"3. Use glob/grep to explore an unfamiliar codebase before making changes, instead of guessing " +
|
|
139
|
+
"at file locations.\n" +
|
|
140
|
+
"4. Verify your work when possible (e.g. run the relevant tests or linter with bash) before " +
|
|
141
|
+
"declaring a task done.\n" +
|
|
142
|
+
"5. Keep responses concise. Explain what you did and why, not step-by-step narration of tool calls.\n" +
|
|
143
|
+
"6. Never run destructive commands (e.g. rm -rf, force pushes) without first explaining the " +
|
|
144
|
+
"consequence in your response.\n";
|
|
145
|
+
export const API_KEY_MAX = 64;
|
|
146
|
+
import { validateOxeApiKey } from "./api.js";
|
|
147
|
+
import { renderPanel, markupToAnsi } from "./ui.js";
|
|
148
|
+
export async function loadOrPrompt() {
|
|
149
|
+
let api_key = "";
|
|
150
|
+
let key_data = {};
|
|
151
|
+
for (;;) {
|
|
152
|
+
if (!api_key) {
|
|
153
|
+
renderPanel("[bold white]Oxe Desktop Authentication[/bold white]\n\n" +
|
|
154
|
+
`[dim]Only valid Oxe API keys (starting with [bold]${OXE_KEY_PREFIX}[/bold]) generated on your\n` +
|
|
155
|
+
"Oxe web dashboard are authorized to run this agent.\n\n" +
|
|
156
|
+
"Get your key at: [bold underline]http://localhost:5173/dashboard/api-keys[/bold underline][/dim]", "Oxe Cloud Access");
|
|
157
|
+
process.stdout.write("\n");
|
|
158
|
+
api_key = await promptApiKey("Enter Oxe API Key: ");
|
|
159
|
+
if (!api_key) {
|
|
160
|
+
process.stdout.write("\n" + markupToAnsi("[red]Error: Key cannot be blank. Exiting application.[/red]") + "\n");
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
process.stdout.write("\n");
|
|
165
|
+
process.stdout.write(markupToAnsi("[dim]Authenticating key with Oxe Cloud…[/dim]") + "\n");
|
|
166
|
+
const validation = await validateOxeApiKey(api_key);
|
|
167
|
+
if (validation.valid) {
|
|
168
|
+
key_data = validation.key_data || {};
|
|
169
|
+
process.stdout.write(markupToAnsi(`[green]✓ Oxe API Key verified[/green] [dim](${String(key_data["name"] ?? "Desktop")})[/dim]`) + "\n");
|
|
170
|
+
process.env.OXE_API_KEY = api_key;
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
process.stdout.write(`\x1b[31mAuthentication Error:\x1b[0m ${validation.error}\n`);
|
|
175
|
+
process.stdout.write("\n");
|
|
176
|
+
api_key = "";
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
let timeout = 120.0;
|
|
180
|
+
try {
|
|
181
|
+
timeout = parseFloat((process.env.OXE_TIMEOUT || "120").trim() || "120");
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
timeout = 120.0;
|
|
185
|
+
}
|
|
186
|
+
let max_retries = 2;
|
|
187
|
+
try {
|
|
188
|
+
max_retries = parseInt((process.env.OXE_MAX_RETRIES || "2").trim() || "2", 10);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
max_retries = 2;
|
|
192
|
+
}
|
|
193
|
+
let temperature = null;
|
|
194
|
+
const temp = (process.env.OXE_TEMPERATURE || "").trim();
|
|
195
|
+
if (temp) {
|
|
196
|
+
const t = parseFloat(temp);
|
|
197
|
+
if (!Number.isNaN(t))
|
|
198
|
+
temperature = t;
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
api_key,
|
|
202
|
+
base_url: (process.env.OXE_BASE_URL || default_base_url).trim(),
|
|
203
|
+
model_name: (process.env.OXE_MODEL_NAME || default_model).trim(),
|
|
204
|
+
reasoning_effort: (process.env.OXE_REASONING_EFFORT || default_reasoning_effort).trim(),
|
|
205
|
+
timeout,
|
|
206
|
+
max_retries,
|
|
207
|
+
temperature,
|
|
208
|
+
key_data,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Prompt for a secret (API key) without echoing it to the terminal.
|
|
213
|
+
* Falls back to a plain readline read when stdin isn't a TTY (e.g. piped input).
|
|
214
|
+
*/
|
|
215
|
+
export async function promptApiKey(prompt) {
|
|
216
|
+
// Non-interactive (piped) input: just read a line, no masking possible.
|
|
217
|
+
if (!process.stdin.isTTY) {
|
|
218
|
+
const { default: readline } = await import("node:readline/promises");
|
|
219
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
220
|
+
try {
|
|
221
|
+
const answer = await rl.question(prompt);
|
|
222
|
+
return answer.replace(/\r?\n$/, "").trim();
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
rl.close();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return new Promise((resolve, reject) => {
|
|
229
|
+
process.stdin.setRawMode(true);
|
|
230
|
+
process.stdin.resume();
|
|
231
|
+
process.stdout.write(prompt);
|
|
232
|
+
let input = "";
|
|
233
|
+
const onData = (buf) => {
|
|
234
|
+
for (const b of buf) {
|
|
235
|
+
if (b === 3) {
|
|
236
|
+
// Ctrl+C
|
|
237
|
+
cleanup();
|
|
238
|
+
process.stdout.write("\n");
|
|
239
|
+
reject(new Error("interrupt"));
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (b === 4 || b === 26) {
|
|
243
|
+
// Ctrl+D / Ctrl+Z
|
|
244
|
+
cleanup();
|
|
245
|
+
resolve(input);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (b === 13 || b === 10) {
|
|
249
|
+
// Enter
|
|
250
|
+
cleanup();
|
|
251
|
+
resolve(input);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (b === 8 || b === 127) {
|
|
255
|
+
// Backspace
|
|
256
|
+
if (input.length) {
|
|
257
|
+
input = input.slice(0, -1);
|
|
258
|
+
process.stdout.write("\b \b");
|
|
259
|
+
}
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (b < 32)
|
|
263
|
+
continue;
|
|
264
|
+
input += String.fromCharCode(b);
|
|
265
|
+
process.stdout.write("*");
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
const cleanup = () => {
|
|
269
|
+
process.stdin.removeListener("data", onData);
|
|
270
|
+
try {
|
|
271
|
+
process.stdin.setRawMode(false);
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
/* ignore */
|
|
275
|
+
}
|
|
276
|
+
process.stdout.write("\n");
|
|
277
|
+
};
|
|
278
|
+
process.stdin.on("data", onData);
|
|
279
|
+
});
|
|
280
|
+
}
|