corex-cli 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/dist/index.js ADDED
@@ -0,0 +1,742 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/index.ts
27
+ var import_react7 = __toESM(require("react"));
28
+ var import_ink7 = require("ink");
29
+ var import_dotenv = __toESM(require("dotenv"));
30
+
31
+ // src/lib/config.ts
32
+ var import_conf = __toESM(require("conf"));
33
+ var readline = __toESM(require("readline"));
34
+ var fs = __toESM(require("fs"));
35
+ var path = __toESM(require("path"));
36
+ var FALLBACK_SYSTEM_PROMPT = `You are COREX, an elite AI assistant living inside a terminal.
37
+ You are direct, insightful, and technically brilliant.
38
+ Format your responses for terminal display using clean spacing.
39
+ When showing code, use markdown code blocks.
40
+ Keep responses focused and avoid unnecessary filler text.`;
41
+ function loadSystemPrompt() {
42
+ const filename = "COREX_SYSTEM_PROMPT.txt";
43
+ const possiblePaths = [
44
+ path.join(__dirname, "..", "assets", filename),
45
+ path.join(__dirname, "..", "..", "assets", filename),
46
+ path.join(process.cwd(), "assets", filename)
47
+ ];
48
+ for (const p of possiblePaths) {
49
+ try {
50
+ if (fs.existsSync(p)) {
51
+ return fs.readFileSync(p, "utf-8").trim();
52
+ }
53
+ } catch {
54
+ }
55
+ }
56
+ return FALLBACK_SYSTEM_PROMPT;
57
+ }
58
+ var DEFAULT_SYSTEM_PROMPT = loadSystemPrompt();
59
+ var defaults = {
60
+ apiKey: "",
61
+ model: "claude-3-5-sonnet-20241022",
62
+ theme: "dark",
63
+ systemPrompt: DEFAULT_SYSTEM_PROMPT,
64
+ maxTokens: 4096,
65
+ temperature: 0.7,
66
+ saveHistory: false,
67
+ userName: "You"
68
+ };
69
+ var config = new import_conf.default({
70
+ projectName: "corex",
71
+ defaults
72
+ });
73
+ function loadConfig() {
74
+ return {
75
+ apiKey: config.get("apiKey"),
76
+ model: config.get("model"),
77
+ theme: config.get("theme"),
78
+ systemPrompt: config.get("systemPrompt"),
79
+ maxTokens: config.get("maxTokens"),
80
+ temperature: config.get("temperature"),
81
+ saveHistory: config.get("saveHistory"),
82
+ userName: config.get("userName")
83
+ };
84
+ }
85
+ function saveConfig(partial) {
86
+ for (const [key, value] of Object.entries(partial)) {
87
+ config.set(key, value);
88
+ }
89
+ }
90
+ function isFirstRun() {
91
+ const apiKey = config.get("apiKey");
92
+ return !apiKey || apiKey.trim() === "";
93
+ }
94
+ function prompt(rl, question) {
95
+ return new Promise((resolve) => {
96
+ rl.question(question, (answer) => {
97
+ resolve(answer.trim());
98
+ });
99
+ });
100
+ }
101
+ function promptMasked(rl, question) {
102
+ return new Promise((resolve) => {
103
+ const stdout = process.stdout;
104
+ stdout.write(question);
105
+ const stdin = process.stdin;
106
+ const wasRaw = stdin.isRaw;
107
+ if (stdin.setRawMode) {
108
+ stdin.setRawMode(true);
109
+ }
110
+ stdin.resume();
111
+ let input = "";
112
+ const onData = (char) => {
113
+ const c = char.toString("utf8");
114
+ if (c === "\n" || c === "\r") {
115
+ stdin.removeListener("data", onData);
116
+ if (stdin.setRawMode) {
117
+ stdin.setRawMode(wasRaw || false);
118
+ }
119
+ stdout.write("\n");
120
+ resolve(input.trim());
121
+ } else if (c === "\x7F" || c === "\b") {
122
+ if (input.length > 0) {
123
+ input = input.slice(0, -1);
124
+ stdout.write("\b \b");
125
+ }
126
+ } else if (c === "") {
127
+ process.exit(0);
128
+ } else {
129
+ input += c;
130
+ stdout.write("*");
131
+ }
132
+ };
133
+ stdin.on("data", onData);
134
+ });
135
+ }
136
+ var MODELS = [
137
+ { id: "claude-3-5-sonnet-20241022", label: "claude-3-5-sonnet-20241022 Recommended" },
138
+ { id: "claude-3-opus-20240229", label: "claude-3-opus-20240229 Most Powerful" },
139
+ { id: "claude-3-haiku-20240307", label: "claude-3-haiku-20240307 Fastest" }
140
+ ];
141
+ async function runFirstRunWizard() {
142
+ const rl = readline.createInterface({
143
+ input: process.stdin,
144
+ output: process.stdout
145
+ });
146
+ console.log("");
147
+ console.log("+------------------------------------------+");
148
+ console.log("| Welcome to COREX - First Setup |");
149
+ console.log("+------------------------------------------+");
150
+ console.log("");
151
+ rl.close();
152
+ let apiKey = "";
153
+ while (true) {
154
+ apiKey = await promptMasked(
155
+ readline.createInterface({ input: process.stdin, output: process.stdout }),
156
+ "Enter your Anthropic API key: "
157
+ );
158
+ if (apiKey.startsWith("sk-ant-")) {
159
+ break;
160
+ }
161
+ console.log(' Error: API key must start with "sk-ant-". Please try again.');
162
+ }
163
+ const rl2 = readline.createInterface({
164
+ input: process.stdin,
165
+ output: process.stdout
166
+ });
167
+ console.log("");
168
+ console.log("Choose your default model:");
169
+ MODELS.forEach((m, i) => {
170
+ console.log(` ${i + 1}) ${m.label}`);
171
+ });
172
+ const modelChoice = await prompt(rl2, "Enter choice [1]: ");
173
+ const modelIndex = modelChoice ? parseInt(modelChoice, 10) - 1 : 0;
174
+ const model = MODELS[modelIndex >= 0 && modelIndex < MODELS.length ? modelIndex : 0].id;
175
+ console.log("");
176
+ const themeChoice = await prompt(rl2, "Choose your theme: dark / light / neon / retro [dark]: ");
177
+ const validThemes = ["dark", "light", "neon", "retro"];
178
+ const theme = validThemes.includes(themeChoice) ? themeChoice : "dark";
179
+ rl2.close();
180
+ saveConfig({ apiKey, model, theme });
181
+ console.log("");
182
+ console.log("Config saved to ~/.config/corex/config.json");
183
+ console.log("Launching COREX...");
184
+ console.log("");
185
+ }
186
+
187
+ // src/lib/ai.ts
188
+ var import_sdk = __toESM(require("@anthropic-ai/sdk"));
189
+ var client = null;
190
+ function initAI(apiKey) {
191
+ client = new import_sdk.default({ apiKey });
192
+ }
193
+ async function sendMessage(history2, userMessage, config2, onToken, onComplete, onError) {
194
+ if (!client) {
195
+ onError(new Error("No API key found. Run corex to set it up."));
196
+ return;
197
+ }
198
+ const messages = [
199
+ ...history2.map((m) => ({
200
+ role: m.role,
201
+ content: m.content
202
+ })),
203
+ { role: "user", content: userMessage }
204
+ ];
205
+ try {
206
+ const stream = client.messages.stream({
207
+ model: config2.model,
208
+ max_tokens: config2.maxTokens,
209
+ temperature: config2.temperature,
210
+ system: config2.systemPrompt,
211
+ messages
212
+ });
213
+ let fullText = "";
214
+ stream.on("text", (text) => {
215
+ fullText += text;
216
+ onToken(text);
217
+ });
218
+ const finalMessage = await stream.finalMessage();
219
+ const usage = {
220
+ inputTokens: finalMessage.usage?.input_tokens || 0,
221
+ outputTokens: finalMessage.usage?.output_tokens || 0,
222
+ totalTokens: (finalMessage.usage?.input_tokens || 0) + (finalMessage.usage?.output_tokens || 0)
223
+ };
224
+ onComplete(fullText, usage);
225
+ } catch (err) {
226
+ const statusCode = err?.status || err?.statusCode;
227
+ let message = "An unexpected error occurred.";
228
+ if (statusCode === 401) {
229
+ message = "Invalid API key. Use /key to update it.";
230
+ } else if (statusCode === 429) {
231
+ message = "Rate limit reached. Please wait a moment and try again.";
232
+ } else if (statusCode === 404 || err?.message && err.message.includes("model")) {
233
+ message = `Selected model unavailable. Try switching with /model.`;
234
+ } else if (err?.code === "ENOTFOUND" || err?.code === "ECONNREFUSED" || err?.code === "ETIMEDOUT" || err?.message?.includes("fetch failed") || err?.message?.includes("network")) {
235
+ message = "Connection failed. Check your internet connection.";
236
+ } else if (err?.message) {
237
+ message = err.message;
238
+ }
239
+ onError(new Error(message));
240
+ }
241
+ }
242
+
243
+ // src/app.tsx
244
+ var import_react6 = __toESM(require("react"));
245
+ var import_ink6 = require("ink");
246
+
247
+ // src/themes/themes.ts
248
+ var themes = {
249
+ dark: {
250
+ userText: "#00D9FF",
251
+ aiText: "#FFFFFF",
252
+ border: "#333333",
253
+ accent: "#00FF88",
254
+ statusBar: "#666666",
255
+ headerGradient: ["#00D9FF", "#007AFF"]
256
+ },
257
+ neon: {
258
+ userText: "#FF00FF",
259
+ aiText: "#00FFFF",
260
+ border: "#FF00FF",
261
+ accent: "#FFFF00",
262
+ statusBar: "#FF00FF",
263
+ headerGradient: ["#FF00FF", "#00FFFF"]
264
+ },
265
+ retro: {
266
+ userText: "#FFB000",
267
+ aiText: "#FF8C00",
268
+ border: "#FF8C00",
269
+ accent: "#FFB000",
270
+ statusBar: "#FF8C00",
271
+ headerGradient: ["#FFB000", "#FF4500"]
272
+ },
273
+ light: {
274
+ userText: "#0066CC",
275
+ aiText: "#222222",
276
+ border: "#CCCCCC",
277
+ accent: "#009900",
278
+ statusBar: "#888888",
279
+ headerGradient: ["#0066CC", "#0099FF"]
280
+ }
281
+ };
282
+ function getTheme(name) {
283
+ return themes[name] || themes.dark;
284
+ }
285
+ function getThemeNames() {
286
+ return Object.keys(themes);
287
+ }
288
+
289
+ // src/lib/history.ts
290
+ var fs2 = __toESM(require("fs"));
291
+ var path2 = __toESM(require("path"));
292
+ var os = __toESM(require("os"));
293
+ var history = [];
294
+ function addMessage(role, content) {
295
+ history.push({ role, content });
296
+ }
297
+ function getHistory() {
298
+ return [...history];
299
+ }
300
+ function clearHistory() {
301
+ history = [];
302
+ }
303
+ function saveSession() {
304
+ const sessionsDir = path2.join(os.homedir(), ".corex", "sessions");
305
+ if (!fs2.existsSync(sessionsDir)) {
306
+ fs2.mkdirSync(sessionsDir, { recursive: true });
307
+ }
308
+ const now = /* @__PURE__ */ new Date();
309
+ const pad = (n) => n.toString().padStart(2, "0");
310
+ const filename = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}_${pad(now.getHours())}-${pad(now.getMinutes())}.txt`;
311
+ const filepath = path2.join(sessionsDir, filename);
312
+ let content = "=== COREX Chat Session ===\n";
313
+ content += `Date: ${now.toLocaleString()}
314
+ `;
315
+ content += `Messages: ${history.length}
316
+ `;
317
+ content += "=".repeat(40) + "\n\n";
318
+ for (const msg of history) {
319
+ const label = msg.role === "user" ? "You" : "COREX";
320
+ content += `[${label}]
321
+ ${msg.content}
322
+
323
+ `;
324
+ }
325
+ fs2.writeFileSync(filepath, content, "utf-8");
326
+ return filepath;
327
+ }
328
+
329
+ // src/components/Header.tsx
330
+ var import_react = __toESM(require("react"));
331
+ var import_ink = require("ink");
332
+ var fs3 = __toESM(require("fs"));
333
+ var path3 = __toESM(require("path"));
334
+ var import_gradient_string = __toESM(require("gradient-string"));
335
+ var Header = ({ theme }) => {
336
+ const [logoLines, setLogoLines] = (0, import_react.useState)([]);
337
+ (0, import_react.useEffect)(() => {
338
+ try {
339
+ const possiblePaths = [
340
+ path3.join(__dirname, "..", "assets", "logo.txt"),
341
+ path3.join(__dirname, "..", "..", "assets", "logo.txt"),
342
+ path3.join(process.cwd(), "assets", "logo.txt")
343
+ ];
344
+ let logoText = "";
345
+ for (const p of possiblePaths) {
346
+ if (fs3.existsSync(p)) {
347
+ logoText = fs3.readFileSync(p, "utf-8");
348
+ break;
349
+ }
350
+ }
351
+ if (!logoText) {
352
+ logoText = `
353
+ \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557
354
+ \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u255A\u2588\u2588\u2557\u2588\u2588\u2554\u255D
355
+ \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2557 \u255A\u2588\u2588\u2588\u2554\u255D
356
+ \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2554\u2588\u2588\u2557
357
+ \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2554\u255D \u2588\u2588\u2557
358
+ \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D`;
359
+ }
360
+ const grad = (0, import_gradient_string.default)(theme.headerGradient);
361
+ const lines = logoText.split("\n").map((line) => grad(line));
362
+ setLogoLines(lines);
363
+ } catch {
364
+ setLogoLines([" COREX"]);
365
+ }
366
+ }, [theme]);
367
+ return /* @__PURE__ */ import_react.default.createElement(import_ink.Box, { flexDirection: "column", marginBottom: 1 }, logoLines.map((line, i) => /* @__PURE__ */ import_react.default.createElement(import_ink.Text, { key: i }, line)));
368
+ };
369
+ var Header_default = Header;
370
+
371
+ // src/components/ChatHistory.tsx
372
+ var import_react3 = __toESM(require("react"));
373
+ var import_ink3 = require("ink");
374
+
375
+ // src/lib/markdown.ts
376
+ var import_marked = require("marked");
377
+ var import_marked_terminal = __toESM(require("marked-terminal"));
378
+ import_marked.marked.setOptions({
379
+ renderer: new import_marked_terminal.default()
380
+ });
381
+ function renderMarkdown(text) {
382
+ try {
383
+ const rendered = import_marked.marked.parse(text);
384
+ if (typeof rendered === "string") {
385
+ return rendered.replace(/\n$/, "");
386
+ }
387
+ return text;
388
+ } catch {
389
+ return text;
390
+ }
391
+ }
392
+
393
+ // src/components/ThinkingDots.tsx
394
+ var import_react2 = __toESM(require("react"));
395
+ var import_ink2 = require("ink");
396
+ var import_ink_spinner = __toESM(require("ink-spinner"));
397
+ var ThinkingDots = ({ theme }) => {
398
+ return /* @__PURE__ */ import_react2.default.createElement(import_ink2.Box, { marginLeft: 2 }, /* @__PURE__ */ import_react2.default.createElement(import_ink2.Text, { color: theme.accent }, /* @__PURE__ */ import_react2.default.createElement(import_ink_spinner.default, { type: "dots" })), /* @__PURE__ */ import_react2.default.createElement(import_ink2.Text, { color: theme.statusBar }, " thinking..."));
399
+ };
400
+ var ThinkingDots_default = ThinkingDots;
401
+
402
+ // src/components/ChatHistory.tsx
403
+ var ChatHistory = ({
404
+ messages,
405
+ theme,
406
+ isThinking,
407
+ streamingText,
408
+ userName
409
+ }) => {
410
+ return /* @__PURE__ */ import_react3.default.createElement(import_ink3.Box, { flexDirection: "column", flexGrow: 1 }, messages.map((msg, i) => /* @__PURE__ */ import_react3.default.createElement(import_ink3.Box, { key: i, flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ import_react3.default.createElement(import_ink3.Text, { bold: true, color: msg.role === "user" ? theme.userText : theme.accent }, msg.role === "user" ? `${userName}` : "COREX"), /* @__PURE__ */ import_react3.default.createElement(import_ink3.Box, { marginLeft: 2 }, /* @__PURE__ */ import_react3.default.createElement(import_ink3.Text, { color: msg.role === "user" ? theme.userText : theme.aiText }, msg.role === "assistant" ? renderMarkdown(msg.content) : msg.content)))), streamingText && /* @__PURE__ */ import_react3.default.createElement(import_ink3.Box, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ import_react3.default.createElement(import_ink3.Text, { bold: true, color: theme.accent }, "COREX"), /* @__PURE__ */ import_react3.default.createElement(import_ink3.Box, { marginLeft: 2 }, /* @__PURE__ */ import_react3.default.createElement(import_ink3.Text, { color: theme.aiText }, renderMarkdown(streamingText)))), isThinking && !streamingText && /* @__PURE__ */ import_react3.default.createElement(ThinkingDots_default, { theme }));
411
+ };
412
+ var ChatHistory_default = ChatHistory;
413
+
414
+ // src/components/InputBar.tsx
415
+ var import_react4 = __toESM(require("react"));
416
+ var import_ink4 = require("ink");
417
+ var import_ink_text_input = __toESM(require("ink-text-input"));
418
+ var InputBar = ({
419
+ value,
420
+ onChange,
421
+ onSubmit,
422
+ theme,
423
+ isDisabled
424
+ }) => {
425
+ return /* @__PURE__ */ import_react4.default.createElement(import_ink4.Box, { flexDirection: "column" }, /* @__PURE__ */ import_react4.default.createElement(import_ink4.Box, null, /* @__PURE__ */ import_react4.default.createElement(import_ink4.Text, { color: theme.border }, "\u2500".repeat(process.stdout.columns || 80))), /* @__PURE__ */ import_react4.default.createElement(import_ink4.Box, null, /* @__PURE__ */ import_react4.default.createElement(import_ink4.Box, { flexGrow: 1 }, /* @__PURE__ */ import_react4.default.createElement(import_ink4.Text, { color: theme.accent, bold: true }, "\u276F "), /* @__PURE__ */ import_react4.default.createElement(
426
+ import_ink_text_input.default,
427
+ {
428
+ value,
429
+ onChange,
430
+ onSubmit,
431
+ placeholder: isDisabled ? "Waiting for response..." : "Type a message..."
432
+ }
433
+ )), /* @__PURE__ */ import_react4.default.createElement(import_ink4.Box, { marginLeft: 1 }, /* @__PURE__ */ import_react4.default.createElement(import_ink4.Text, { color: theme.statusBar, dimColor: true }, "[Enter to send]"))));
434
+ };
435
+ var InputBar_default = InputBar;
436
+
437
+ // src/components/StatusBar.tsx
438
+ var import_react5 = __toESM(require("react"));
439
+ var import_ink5 = require("ink");
440
+ var StatusBar = ({
441
+ model,
442
+ totalTokens,
443
+ themeName,
444
+ theme
445
+ }) => {
446
+ return /* @__PURE__ */ import_react5.default.createElement(import_ink5.Box, null, /* @__PURE__ */ import_react5.default.createElement(import_ink5.Text, { color: theme.statusBar }, model, " \u2502 tokens: ", totalTokens, " \u2502 theme: ", themeName));
447
+ };
448
+ var StatusBar_default = StatusBar;
449
+
450
+ // src/app.tsx
451
+ var MODELS2 = [
452
+ { id: "claude-3-5-sonnet-20241022", label: "claude-3-5-sonnet Recommended" },
453
+ { id: "claude-3-opus-20240229", label: "claude-3-opus Most Powerful" },
454
+ { id: "claude-3-haiku-20240307", label: "claude-3-haiku Fastest" }
455
+ ];
456
+ var HELP_TEXT = `
457
+ \u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E
458
+ \u2502 COREX Commands \u2502
459
+ \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524
460
+ \u2502 /clear Clear conversation \u2502
461
+ \u2502 /model Switch AI model \u2502
462
+ \u2502 /theme Change color theme \u2502
463
+ \u2502 /save Save chat to file \u2502
464
+ \u2502 /help Show this help \u2502
465
+ \u2502 /key Update API key \u2502
466
+ \u2502 /exit Quit COREX \u2502
467
+ \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524
468
+ \u2502 Ctrl+C Exit \u2502
469
+ \u2502 Ctrl+L Clear screen \u2502
470
+ \u2502 \u2191 / \u2193 Navigate input history \u2502
471
+ \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F`;
472
+ var App = ({ config: initialConfig }) => {
473
+ const { exit } = (0, import_ink6.useApp)();
474
+ const [config2, setConfig] = (0, import_react6.useState)(initialConfig);
475
+ const [messages, setMessages] = (0, import_react6.useState)([]);
476
+ const [inputValue, setInputValue] = (0, import_react6.useState)("");
477
+ const [isThinking, setIsThinking] = (0, import_react6.useState)(false);
478
+ const [streamingText, setStreamingText] = (0, import_react6.useState)("");
479
+ const [totalTokens, setTotalTokens] = (0, import_react6.useState)(0);
480
+ const [inputHistory, setInputHistory] = (0, import_react6.useState)([]);
481
+ const [historyIndex, setHistoryIndex] = (0, import_react6.useState)(-1);
482
+ const [welcomeShown, setWelcomeShown] = (0, import_react6.useState)(false);
483
+ const [systemMessages, setSystemMessages] = (0, import_react6.useState)([]);
484
+ const theme = getTheme(config2.theme);
485
+ (0, import_react6.useEffect)(() => {
486
+ if (!welcomeShown) {
487
+ setWelcomeShown(true);
488
+ const welcomeMsg = "Hello. I am COREX. Ask me anything.";
489
+ let i = 0;
490
+ const chars = [];
491
+ const interval = setInterval(() => {
492
+ if (i < welcomeMsg.length) {
493
+ chars.push(welcomeMsg[i]);
494
+ setMessages([{ role: "assistant", content: chars.join("") }]);
495
+ i++;
496
+ } else {
497
+ clearInterval(interval);
498
+ addMessage("assistant", welcomeMsg);
499
+ }
500
+ }, 30);
501
+ return () => clearInterval(interval);
502
+ }
503
+ }, []);
504
+ (0, import_ink6.useInput)((input, key) => {
505
+ if (key.ctrl && input === "l") {
506
+ setMessages([]);
507
+ return;
508
+ }
509
+ if (key.upArrow && inputHistory.length > 0) {
510
+ const newIndex = historyIndex < inputHistory.length - 1 ? historyIndex + 1 : historyIndex;
511
+ setHistoryIndex(newIndex);
512
+ setInputValue(inputHistory[inputHistory.length - 1 - newIndex]);
513
+ return;
514
+ }
515
+ if (key.downArrow) {
516
+ if (historyIndex > 0) {
517
+ const newIndex = historyIndex - 1;
518
+ setHistoryIndex(newIndex);
519
+ setInputValue(inputHistory[inputHistory.length - 1 - newIndex]);
520
+ } else {
521
+ setHistoryIndex(-1);
522
+ setInputValue("");
523
+ }
524
+ return;
525
+ }
526
+ });
527
+ const addSystemMessage = (0, import_react6.useCallback)((text) => {
528
+ setSystemMessages((prev) => [...prev, text]);
529
+ }, []);
530
+ const handleSubmit = (0, import_react6.useCallback)(
531
+ async (value) => {
532
+ const trimmed = value.trim();
533
+ if (!trimmed || isThinking) return;
534
+ setInputValue("");
535
+ setHistoryIndex(-1);
536
+ setInputHistory((prev) => {
537
+ const next = [...prev, trimmed];
538
+ return next.slice(-50);
539
+ });
540
+ if (trimmed.startsWith("/")) {
541
+ const cmd = trimmed.toLowerCase();
542
+ if (cmd === "/exit") {
543
+ setMessages((prev) => [
544
+ ...prev,
545
+ { role: "assistant", content: "\u{1F44B} Goodbye! See you next time." }
546
+ ]);
547
+ setTimeout(() => {
548
+ exit();
549
+ process.exit(0);
550
+ }, 500);
551
+ return;
552
+ }
553
+ if (cmd === "/clear") {
554
+ clearHistory();
555
+ setMessages([]);
556
+ setTotalTokens(0);
557
+ setSystemMessages([]);
558
+ addSystemMessage("Conversation cleared.");
559
+ return;
560
+ }
561
+ if (cmd === "/help") {
562
+ setMessages((prev) => [
563
+ ...prev,
564
+ { role: "assistant", content: HELP_TEXT }
565
+ ]);
566
+ return;
567
+ }
568
+ if (cmd === "/save") {
569
+ try {
570
+ const filepath = saveSession();
571
+ addSystemMessage(`Session saved to ${filepath}`);
572
+ } catch (err) {
573
+ addSystemMessage("Error: Failed to save session.");
574
+ }
575
+ return;
576
+ }
577
+ if (cmd === "/model") {
578
+ const modelList = MODELS2.map(
579
+ (m, i) => ` ${i + 1}) ${m.label}${m.id === config2.model ? " \u2713" : ""}`
580
+ ).join("\n");
581
+ setMessages((prev) => [
582
+ ...prev,
583
+ {
584
+ role: "assistant",
585
+ content: `Switch model:
586
+ ${modelList}
587
+
588
+ Type: /model 1, /model 2, or /model 3`
589
+ }
590
+ ]);
591
+ return;
592
+ }
593
+ if (cmd.startsWith("/model ")) {
594
+ const choice = parseInt(cmd.split(" ")[1], 10);
595
+ if (choice >= 1 && choice <= MODELS2.length) {
596
+ const newModel = MODELS2[choice - 1].id;
597
+ const newConfig = { ...config2, model: newModel };
598
+ setConfig(newConfig);
599
+ saveConfig({ model: newModel });
600
+ addSystemMessage(`Model switched to ${newModel}`);
601
+ } else {
602
+ addSystemMessage("Invalid choice. Use /model 1, 2, or 3.");
603
+ }
604
+ return;
605
+ }
606
+ if (cmd === "/theme") {
607
+ const themeNames = getThemeNames();
608
+ const themeList = themeNames.map((t) => ` ${t}${t === config2.theme ? " \u2713" : ""}`).join("\n");
609
+ setMessages((prev) => [
610
+ ...prev,
611
+ {
612
+ role: "assistant",
613
+ content: `Available themes:
614
+ ${themeList}
615
+
616
+ Type: /theme dark, /theme neon, /theme retro, or /theme light`
617
+ }
618
+ ]);
619
+ return;
620
+ }
621
+ if (cmd.startsWith("/theme ")) {
622
+ const choice = cmd.split(" ")[1];
623
+ const validThemes = getThemeNames();
624
+ if (validThemes.includes(choice)) {
625
+ const newConfig = { ...config2, theme: choice };
626
+ setConfig(newConfig);
627
+ saveConfig({ theme: choice });
628
+ addSystemMessage(`Theme switched to ${choice}`);
629
+ } else {
630
+ addSystemMessage(`Invalid theme. Options: ${validThemes.join(", ")}`);
631
+ }
632
+ return;
633
+ }
634
+ if (cmd === "/key") {
635
+ const maskedKey = config2.apiKey ? config2.apiKey.slice(0, 10) + "..." + config2.apiKey.slice(-4) : "Not set";
636
+ setMessages((prev) => [
637
+ ...prev,
638
+ {
639
+ role: "assistant",
640
+ content: `Current API key: ${maskedKey}
641
+
642
+ To change your key, edit ~/.config/corex/config.json or delete it and restart COREX to re-run the setup wizard.`
643
+ }
644
+ ]);
645
+ return;
646
+ }
647
+ addSystemMessage(`Unknown command: ${trimmed}. Type /help for available commands.`);
648
+ return;
649
+ }
650
+ const userMsg = { role: "user", content: trimmed };
651
+ setMessages((prev) => [...prev, userMsg]);
652
+ addMessage("user", trimmed);
653
+ setIsThinking(true);
654
+ setStreamingText("");
655
+ const history2 = getHistory().slice(0, -1);
656
+ await sendMessage(
657
+ history2,
658
+ trimmed,
659
+ config2,
660
+ // onToken
661
+ (token) => {
662
+ setIsThinking(false);
663
+ setStreamingText((prev) => prev + token);
664
+ },
665
+ // onComplete
666
+ (fullText, usage) => {
667
+ setIsThinking(false);
668
+ setStreamingText("");
669
+ const assistantMsg = { role: "assistant", content: fullText };
670
+ setMessages((prev) => [...prev, assistantMsg]);
671
+ addMessage("assistant", fullText);
672
+ setTotalTokens((prev) => prev + usage.totalTokens);
673
+ },
674
+ // onError
675
+ (error) => {
676
+ setIsThinking(false);
677
+ setStreamingText("");
678
+ setMessages((prev) => [
679
+ ...prev,
680
+ { role: "assistant", content: `\u26A0 ${error.message}` }
681
+ ]);
682
+ }
683
+ );
684
+ },
685
+ [config2, isThinking, inputHistory, exit]
686
+ );
687
+ return /* @__PURE__ */ import_react6.default.createElement(import_ink6.Box, { flexDirection: "column", height: process.stdout.rows || 24 }, /* @__PURE__ */ import_react6.default.createElement(Header_default, { theme }), /* @__PURE__ */ import_react6.default.createElement(
688
+ ChatHistory_default,
689
+ {
690
+ messages,
691
+ theme,
692
+ isThinking,
693
+ streamingText,
694
+ userName: config2.userName
695
+ }
696
+ ), systemMessages.length > 0 && /* @__PURE__ */ import_react6.default.createElement(import_ink6.Box, { flexDirection: "column", marginBottom: 0 }, systemMessages.slice(-3).map((msg, i) => /* @__PURE__ */ import_react6.default.createElement(import_ink6.Text, { key: i, color: theme.accent, dimColor: true }, "\u2139 ", msg))), /* @__PURE__ */ import_react6.default.createElement(
697
+ InputBar_default,
698
+ {
699
+ value: inputValue,
700
+ onChange: setInputValue,
701
+ onSubmit: handleSubmit,
702
+ theme,
703
+ isDisabled: isThinking
704
+ }
705
+ ), /* @__PURE__ */ import_react6.default.createElement(
706
+ StatusBar_default,
707
+ {
708
+ model: config2.model,
709
+ totalTokens,
710
+ themeName: config2.theme,
711
+ theme
712
+ }
713
+ ));
714
+ };
715
+ var app_default = App;
716
+
717
+ // src/index.ts
718
+ import_dotenv.default.config();
719
+ var nodeVersion = parseInt(process.version.slice(1).split(".")[0], 10);
720
+ if (nodeVersion < 18) {
721
+ console.error("COREX requires Node.js 18 or higher.");
722
+ process.exit(1);
723
+ }
724
+ async function main() {
725
+ try {
726
+ if (isFirstRun()) {
727
+ await runFirstRunWizard();
728
+ }
729
+ const config2 = loadConfig();
730
+ if (process.env.ANTHROPIC_API_KEY && !config2.apiKey) {
731
+ config2.apiKey = process.env.ANTHROPIC_API_KEY;
732
+ }
733
+ initAI(config2.apiKey);
734
+ process.stdout.write("\x1B[2J\x1B[0f");
735
+ const { waitUntilExit } = (0, import_ink7.render)(import_react7.default.createElement(app_default, { config: config2 }));
736
+ await waitUntilExit();
737
+ } catch (err) {
738
+ console.error("Failed to start COREX:", err.message);
739
+ process.exit(1);
740
+ }
741
+ }
742
+ main();