openhorizon-cli 1.0.4 → 1.0.6
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 +113 -48
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import
|
|
5
|
+
import chalk4 from "chalk";
|
|
6
6
|
import dotenv from "dotenv";
|
|
7
7
|
|
|
8
8
|
// src/config.ts
|
|
@@ -37,7 +37,7 @@ function saveConfig(updates) {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
// src/chat.ts
|
|
40
|
-
import
|
|
40
|
+
import chalk3 from "chalk";
|
|
41
41
|
import { input } from "@inquirer/prompts";
|
|
42
42
|
import { createHash } from "crypto";
|
|
43
43
|
import { marked } from "marked";
|
|
@@ -47,6 +47,7 @@ import TerminalRenderer from "marked-terminal";
|
|
|
47
47
|
import fs2 from "fs";
|
|
48
48
|
import path2 from "path";
|
|
49
49
|
import os2 from "os";
|
|
50
|
+
import chalk from "chalk";
|
|
50
51
|
var HISTORY_DIR = path2.join(os2.homedir(), ".openhorizon");
|
|
51
52
|
var HISTORY_FILE = path2.join(HISTORY_DIR, "history.jsonl");
|
|
52
53
|
function ensureDir() {
|
|
@@ -64,9 +65,59 @@ function appendHistory(entry) {
|
|
|
64
65
|
function getHistoryPath() {
|
|
65
66
|
return HISTORY_FILE;
|
|
66
67
|
}
|
|
68
|
+
function getHistoryEntries() {
|
|
69
|
+
if (!fs2.existsSync(HISTORY_FILE)) {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const raw = fs2.readFileSync(HISTORY_FILE, "utf-8");
|
|
74
|
+
return raw.split("\n").filter((line) => line.trim().length > 0).map((line) => {
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(line);
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}).filter((entry) => entry !== null);
|
|
81
|
+
} catch {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function printHistoryEntries(entries, limit, isRaw = false) {
|
|
86
|
+
if (entries.length === 0) {
|
|
87
|
+
console.log(chalk.gray("\n No history found.\n"));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (isRaw) {
|
|
91
|
+
entries.slice(-limit).forEach((e) => console.log(JSON.stringify(e)));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
console.log(chalk.bold.blue(`
|
|
95
|
+
Recent Chat History (showing ${Math.min(entries.length, limit)})
|
|
96
|
+
`));
|
|
97
|
+
const subset = entries.slice(-limit);
|
|
98
|
+
subset.forEach((entry, i) => {
|
|
99
|
+
const date = new Date(entry.ts).toLocaleString([], {
|
|
100
|
+
month: "short",
|
|
101
|
+
day: "numeric",
|
|
102
|
+
hour: "2-digit",
|
|
103
|
+
minute: "2-digit"
|
|
104
|
+
});
|
|
105
|
+
const roleStr = entry.role === "user" ? chalk.green("\u276F You") : chalk.blue("\u276F Assistant");
|
|
106
|
+
const prevEntry = subset[i - 1];
|
|
107
|
+
if (!prevEntry || prevEntry.session !== entry.session) {
|
|
108
|
+
console.log(chalk.dim(` \u2500\u2500 Session: ${entry.session.slice(0, 8)}\u2026 \u2500\u2500`));
|
|
109
|
+
}
|
|
110
|
+
console.log(`${chalk.dim(date)} ${roleStr}`);
|
|
111
|
+
console.log(` ${entry.content.replace(/\n/g, "\n ")}`);
|
|
112
|
+
if (entry.usage) {
|
|
113
|
+
console.log(chalk.dim(` tokens: \u2191${entry.usage.promptTokens} \u2193${entry.usage.completionTokens} \xB7 ${entry.latencyMs}ms`));
|
|
114
|
+
}
|
|
115
|
+
console.log("");
|
|
116
|
+
});
|
|
117
|
+
}
|
|
67
118
|
|
|
68
119
|
// src/update.ts
|
|
69
|
-
import
|
|
120
|
+
import chalk2 from "chalk";
|
|
70
121
|
async function getLatestVersion() {
|
|
71
122
|
try {
|
|
72
123
|
const response = await fetch("https://registry.npmjs.org/openhorizon-cli/latest", {
|
|
@@ -85,8 +136,8 @@ async function checkForUpdate(currentVersion) {
|
|
|
85
136
|
if (latest !== currentVersion) {
|
|
86
137
|
console.log(
|
|
87
138
|
boxen(
|
|
88
|
-
`${
|
|
89
|
-
Run ${
|
|
139
|
+
`${chalk2.yellow("Update available!")} ${chalk2.dim(currentVersion)} \u2192 ${chalk2.green(latest)}
|
|
140
|
+
Run ${chalk2.cyan("openhorizon update")} to update.`,
|
|
90
141
|
{ padding: 1, margin: 1, borderColor: "yellow", borderStyle: "round" }
|
|
91
142
|
)
|
|
92
143
|
);
|
|
@@ -109,26 +160,26 @@ function boxen(text, options) {
|
|
|
109
160
|
return res;
|
|
110
161
|
}
|
|
111
162
|
async function runUpdate(currentVersion) {
|
|
112
|
-
console.log(
|
|
163
|
+
console.log(chalk2.blue(`
|
|
113
164
|
Checking for updates...`));
|
|
114
165
|
const latest = await getLatestVersion();
|
|
115
166
|
if (!latest) {
|
|
116
|
-
console.error(
|
|
167
|
+
console.error(chalk2.red(" \u2717 Could not fetch the latest version from npm registry."));
|
|
117
168
|
return;
|
|
118
169
|
}
|
|
119
170
|
if (latest === currentVersion) {
|
|
120
|
-
console.log(
|
|
171
|
+
console.log(chalk2.green(` \u2713 You are already on the latest version (${chalk2.bold(currentVersion)}).
|
|
121
172
|
`));
|
|
122
173
|
return;
|
|
123
174
|
}
|
|
124
|
-
console.log(
|
|
125
|
-
console.log(
|
|
175
|
+
console.log(chalk2.yellow(` ! A new version is available: ${chalk2.bold(latest)}`));
|
|
176
|
+
console.log(chalk2.gray(` Current version: ${currentVersion}
|
|
126
177
|
`));
|
|
127
|
-
console.log(
|
|
128
|
-
console.log(
|
|
178
|
+
console.log(chalk2.white(` To update, run the following command:`));
|
|
179
|
+
console.log(chalk2.cyan(`
|
|
129
180
|
npm install -g openhorizon-cli
|
|
130
181
|
`));
|
|
131
|
-
console.log(
|
|
182
|
+
console.log(chalk2.gray(` (Or use your preferred package manager like bun or pnpm)
|
|
132
183
|
`));
|
|
133
184
|
}
|
|
134
185
|
|
|
@@ -143,7 +194,7 @@ function createSpinner(label) {
|
|
|
143
194
|
const interval = setInterval(() => {
|
|
144
195
|
if (stopped) return;
|
|
145
196
|
process.stdout.write(
|
|
146
|
-
`\r${
|
|
197
|
+
`\r${chalk3.cyan(SPINNER_FRAMES[i % SPINNER_FRAMES.length])} ${chalk3.dim(label)} `
|
|
147
198
|
);
|
|
148
199
|
i++;
|
|
149
200
|
}, 80);
|
|
@@ -171,7 +222,7 @@ async function* streamCompletion(baseUrl, apiKey, model, messages) {
|
|
|
171
222
|
Authorization: `Bearer ${apiKey}`,
|
|
172
223
|
"x-api-key": apiKey,
|
|
173
224
|
"x-client": "openhorizon-cli",
|
|
174
|
-
"x-client-version": "1.0.
|
|
225
|
+
"x-client-version": "1.0.6"
|
|
175
226
|
},
|
|
176
227
|
body: JSON.stringify({ model, messages, stream: true })
|
|
177
228
|
});
|
|
@@ -215,13 +266,13 @@ async function runChatLoop(options) {
|
|
|
215
266
|
const keyFingerprint = createHash("sha256").update(apiKey).digest("hex").slice(0, 8);
|
|
216
267
|
const session = `${date}_${keyFingerprint}_${Math.random().toString(36).slice(2, 7)}`;
|
|
217
268
|
console.log(
|
|
218
|
-
"\n" +
|
|
269
|
+
"\n" + chalk3.green("\u2713 Connected") + chalk3.gray(" \xB7 model ") + chalk3.bold.white(model)
|
|
219
270
|
);
|
|
220
|
-
console.log(
|
|
271
|
+
console.log(chalk3.dim(` History \u2192 ${getHistoryPath()}`));
|
|
221
272
|
console.log(
|
|
222
|
-
|
|
273
|
+
chalk3.gray(
|
|
223
274
|
`
|
|
224
|
-
Commands: ${
|
|
275
|
+
Commands: ${chalk3.white("/model")} ${chalk3.white("/clear")} ${chalk3.white("/help")} ${chalk3.white("/version")} ${chalk3.white("/update")} ${chalk3.white("exit")}
|
|
225
276
|
`
|
|
226
277
|
)
|
|
227
278
|
);
|
|
@@ -229,10 +280,10 @@ async function runChatLoop(options) {
|
|
|
229
280
|
while (true) {
|
|
230
281
|
let userInput;
|
|
231
282
|
try {
|
|
232
|
-
userInput = await input({ message:
|
|
283
|
+
userInput = await input({ message: chalk3.cyan("\u276F") });
|
|
233
284
|
} catch (err) {
|
|
234
285
|
if (err?.name === "ExitPromptError") {
|
|
235
|
-
console.log(
|
|
286
|
+
console.log(chalk3.gray("\nGoodbye!\n"));
|
|
236
287
|
process.exit(0);
|
|
237
288
|
}
|
|
238
289
|
throw err;
|
|
@@ -240,36 +291,46 @@ async function runChatLoop(options) {
|
|
|
240
291
|
const trimmed = userInput.trim();
|
|
241
292
|
if (!trimmed) continue;
|
|
242
293
|
if (["exit", "quit", "q"].includes(trimmed.toLowerCase())) {
|
|
243
|
-
console.log(
|
|
294
|
+
console.log(chalk3.gray("\nGoodbye!\n"));
|
|
244
295
|
process.exit(0);
|
|
245
296
|
}
|
|
246
297
|
if (trimmed === "/help") {
|
|
247
|
-
console.log(
|
|
248
|
-
console.log(
|
|
249
|
-
console.log(
|
|
250
|
-
console.log(
|
|
251
|
-
console.log(
|
|
298
|
+
console.log(chalk3.gray("\n /model \u2013 show current model"));
|
|
299
|
+
console.log(chalk3.gray(" /version \u2013 show current CLI version"));
|
|
300
|
+
console.log(chalk3.gray(" /update \u2013 check for and install updates"));
|
|
301
|
+
console.log(chalk3.gray(" /history \u2013 show recent chat history (e.g. /history 10)"));
|
|
302
|
+
console.log(chalk3.gray(" /clear \u2013 clear conversation history"));
|
|
303
|
+
console.log(chalk3.gray(" exit \u2013 quit\n"));
|
|
304
|
+
console.log(chalk3.dim(" Tip: Use 'openhorizon history --help' for full history viewer.\n"));
|
|
252
305
|
continue;
|
|
253
306
|
}
|
|
254
307
|
if (trimmed === "/version") {
|
|
255
|
-
console.log(
|
|
256
|
-
OpenHorizon CLI version: ${
|
|
308
|
+
console.log(chalk3.blue(`
|
|
309
|
+
OpenHorizon CLI version: ${chalk3.bold("1.0.6")}
|
|
257
310
|
`));
|
|
258
311
|
continue;
|
|
259
312
|
}
|
|
260
313
|
if (trimmed === "/update") {
|
|
261
|
-
await runUpdate("1.0.
|
|
314
|
+
await runUpdate("1.0.6");
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (trimmed.startsWith("/history")) {
|
|
318
|
+
const args = trimmed.split(" ").slice(1);
|
|
319
|
+
const limit = args.find((a) => !isNaN(Number(a))) || 5;
|
|
320
|
+
const isRaw = args.includes("--raw");
|
|
321
|
+
const entries = getHistoryEntries();
|
|
322
|
+
printHistoryEntries(entries, Number(limit), isRaw);
|
|
262
323
|
continue;
|
|
263
324
|
}
|
|
264
325
|
if (trimmed === "/model") {
|
|
265
|
-
console.log(
|
|
266
|
-
Model: ${
|
|
326
|
+
console.log(chalk3.gray(`
|
|
327
|
+
Model: ${chalk3.bold.white(model)}
|
|
267
328
|
`));
|
|
268
329
|
continue;
|
|
269
330
|
}
|
|
270
331
|
if (trimmed === "/clear") {
|
|
271
332
|
messages.length = 0;
|
|
272
|
-
console.log(
|
|
333
|
+
console.log(chalk3.gray("\n \u2713 Conversation cleared.\n"));
|
|
273
334
|
continue;
|
|
274
335
|
}
|
|
275
336
|
messages.push({ role: "user", content: trimmed });
|
|
@@ -286,12 +347,12 @@ async function runChatLoop(options) {
|
|
|
286
347
|
const { promptTokens, completionTokens } = event.usage;
|
|
287
348
|
spinner.stop();
|
|
288
349
|
if (!fullResponse) {
|
|
289
|
-
console.log(
|
|
350
|
+
console.log(chalk3.red(" \u276F No response received.\n"));
|
|
290
351
|
break;
|
|
291
352
|
}
|
|
292
353
|
const rendered = String(await marked(fullResponse)).trim();
|
|
293
|
-
process.stdout.write(
|
|
294
|
-
const usageStr = promptTokens || completionTokens ?
|
|
354
|
+
process.stdout.write(chalk3.green("\u276F ") + rendered + "\n");
|
|
355
|
+
const usageStr = promptTokens || completionTokens ? chalk3.dim(` \u2191${promptTokens} \u2193${completionTokens} tok \xB7 ${elapsed}s`) : chalk3.dim(` ${elapsed}s`);
|
|
295
356
|
process.stdout.write(usageStr + "\n\n");
|
|
296
357
|
appendHistory({
|
|
297
358
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -314,15 +375,15 @@ async function runChatLoop(options) {
|
|
|
314
375
|
spinner.stop();
|
|
315
376
|
const msg = err?.message ?? String(err);
|
|
316
377
|
if (msg.includes("404")) {
|
|
317
|
-
console.log(
|
|
378
|
+
console.log(chalk3.red(`
|
|
318
379
|
\u276F 404 \u2014 check your API URL (${baseUrl})
|
|
319
380
|
`));
|
|
320
381
|
} else if (msg.includes("401") || msg.includes("403")) {
|
|
321
|
-
console.log(
|
|
382
|
+
console.log(chalk3.red("\n \u276F Authentication failed \u2014 check your API key.\n"));
|
|
322
383
|
} else if (msg.includes("429")) {
|
|
323
|
-
console.log(
|
|
384
|
+
console.log(chalk3.red("\n \u276F Rate limit reached \u2014 try again shortly.\n"));
|
|
324
385
|
} else {
|
|
325
|
-
console.log(
|
|
386
|
+
console.log(chalk3.red(`
|
|
326
387
|
\u276F ${msg}
|
|
327
388
|
`));
|
|
328
389
|
}
|
|
@@ -334,23 +395,27 @@ async function runChatLoop(options) {
|
|
|
334
395
|
// src/index.ts
|
|
335
396
|
dotenv.config();
|
|
336
397
|
var program = new Command();
|
|
337
|
-
var version = "1.0.
|
|
398
|
+
var version = "1.0.6";
|
|
338
399
|
program.name("openhorizon").description("CLI to interact with OpenHorizon AI Models").version(version, "-v, --version", "Output the current version");
|
|
339
400
|
program.command("version").description("Show the current CLI version").action(() => {
|
|
340
|
-
console.log(
|
|
401
|
+
console.log(chalk4.blue(`OpenHorizon CLI version: ${chalk4.bold(version)}`));
|
|
341
402
|
});
|
|
342
403
|
program.command("update").description("Check for and perform CLI updates").action(async () => {
|
|
343
404
|
await runUpdate(version);
|
|
344
405
|
});
|
|
406
|
+
program.command("history").description("Show local chat history").option("-l, --limit <number>", "Limit the number of messages to show", "50").option("--raw", "Show raw JSONL history").action((options) => {
|
|
407
|
+
const entries = getHistoryEntries();
|
|
408
|
+
printHistoryEntries(entries, Number(options.limit), options.raw);
|
|
409
|
+
});
|
|
345
410
|
program.command("chat", { isDefault: true }).description("Start an interactive chat session with the AI").option("-m, --model <model>", "Specify a model to use for this session").option("-b, --base-url <url>", "Base API URL (e.g. https://api.openhorizon.devwtf.in/v1)").action(async (options) => {
|
|
346
411
|
const config = getConfig();
|
|
347
412
|
await checkForUpdate(version);
|
|
348
413
|
const apiKey = process.env.OPENHORIZON_API_KEY || config.apiKey;
|
|
349
414
|
if (!apiKey) {
|
|
350
|
-
console.error(
|
|
415
|
+
console.error(chalk4.red("\n\u2717 Missing API key."));
|
|
351
416
|
console.log(
|
|
352
|
-
|
|
353
|
-
" Run '" +
|
|
417
|
+
chalk4.gray(
|
|
418
|
+
" Run '" + chalk4.white("openhorizon login <api_key>") + chalk4.gray("' or set ") + chalk4.white("OPENHORIZON_API_KEY") + chalk4.gray(".\n")
|
|
354
419
|
)
|
|
355
420
|
);
|
|
356
421
|
process.exit(1);
|
|
@@ -363,9 +428,9 @@ program.command("model").description("Get or set the default AI model").argument
|
|
|
363
428
|
const config = getConfig();
|
|
364
429
|
if (modelName) {
|
|
365
430
|
saveConfig({ defaultModel: modelName });
|
|
366
|
-
console.log(
|
|
431
|
+
console.log(chalk4.green(`\u2713 Default model set to: ${chalk4.bold(modelName)}`));
|
|
367
432
|
} else {
|
|
368
|
-
console.log(
|
|
433
|
+
console.log(chalk4.blue(`Current default model: ${chalk4.bold(config.defaultModel ?? "(not set)")}`));
|
|
369
434
|
}
|
|
370
435
|
});
|
|
371
436
|
program.command("login").description("Save your OpenHorizon API Key securely to local config").argument("[apiKey]", "Your OpenHorizon API Key").action(async (apiKeyInput) => {
|
|
@@ -378,6 +443,6 @@ program.command("login").description("Save your OpenHorizon API Key securely to
|
|
|
378
443
|
});
|
|
379
444
|
}
|
|
380
445
|
saveConfig({ apiKey: key });
|
|
381
|
-
console.log(
|
|
446
|
+
console.log(chalk4.green("\u2713 API Key saved successfully."));
|
|
382
447
|
});
|
|
383
448
|
program.parse(process.argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openhorizon-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "Official CLI for OpenHorizon — chat with AI models directly from your terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"chalk": "^5.6.2",
|
|
51
51
|
"commander": "^14.0.3",
|
|
52
52
|
"dotenv": "^17.4.0",
|
|
53
|
-
"marked": "^
|
|
53
|
+
"marked": "^15.0.0",
|
|
54
54
|
"marked-terminal": "^7.3.0"
|
|
55
55
|
}
|
|
56
56
|
}
|