openhorizon-cli 1.0.5 → 1.0.7
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 +109 -80
- 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() {
|
|
@@ -81,9 +82,50 @@ function getHistoryEntries() {
|
|
|
81
82
|
return [];
|
|
82
83
|
}
|
|
83
84
|
}
|
|
85
|
+
function clearHistory() {
|
|
86
|
+
try {
|
|
87
|
+
if (fs2.existsSync(HISTORY_FILE)) {
|
|
88
|
+
fs2.unlinkSync(HISTORY_FILE);
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function printHistoryEntries(entries, limit, isRaw = false) {
|
|
94
|
+
if (entries.length === 0) {
|
|
95
|
+
console.log(chalk.gray("\n No history found.\n"));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (isRaw) {
|
|
99
|
+
entries.slice(-limit).forEach((e) => console.log(JSON.stringify(e)));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
console.log(chalk.bold.blue(`
|
|
103
|
+
Recent Chat History (showing ${Math.min(entries.length, limit)})
|
|
104
|
+
`));
|
|
105
|
+
const subset = entries.slice(-limit);
|
|
106
|
+
subset.forEach((entry, i) => {
|
|
107
|
+
const date = new Date(entry.ts).toLocaleString([], {
|
|
108
|
+
month: "short",
|
|
109
|
+
day: "numeric",
|
|
110
|
+
hour: "2-digit",
|
|
111
|
+
minute: "2-digit"
|
|
112
|
+
});
|
|
113
|
+
const roleStr = entry.role === "user" ? chalk.green("\u276F You") : chalk.blue("\u276F Assistant");
|
|
114
|
+
const prevEntry = subset[i - 1];
|
|
115
|
+
if (!prevEntry || prevEntry.session !== entry.session) {
|
|
116
|
+
console.log(chalk.dim(` \u2500\u2500 Session: ${entry.session.slice(0, 8)}\u2026 \u2500\u2500`));
|
|
117
|
+
}
|
|
118
|
+
console.log(`${chalk.dim(date)} ${roleStr}`);
|
|
119
|
+
console.log(` ${entry.content.replace(/\n/g, "\n ")}`);
|
|
120
|
+
if (entry.usage) {
|
|
121
|
+
console.log(chalk.dim(` tokens: \u2191${entry.usage.promptTokens} \u2193${entry.usage.completionTokens} \xB7 ${entry.latencyMs}ms`));
|
|
122
|
+
}
|
|
123
|
+
console.log("");
|
|
124
|
+
});
|
|
125
|
+
}
|
|
84
126
|
|
|
85
127
|
// src/update.ts
|
|
86
|
-
import
|
|
128
|
+
import chalk2 from "chalk";
|
|
87
129
|
async function getLatestVersion() {
|
|
88
130
|
try {
|
|
89
131
|
const response = await fetch("https://registry.npmjs.org/openhorizon-cli/latest", {
|
|
@@ -102,8 +144,8 @@ async function checkForUpdate(currentVersion) {
|
|
|
102
144
|
if (latest !== currentVersion) {
|
|
103
145
|
console.log(
|
|
104
146
|
boxen(
|
|
105
|
-
`${
|
|
106
|
-
Run ${
|
|
147
|
+
`${chalk2.yellow("Update available!")} ${chalk2.dim(currentVersion)} \u2192 ${chalk2.green(latest)}
|
|
148
|
+
Run ${chalk2.cyan("openhorizon update")} to update.`,
|
|
107
149
|
{ padding: 1, margin: 1, borderColor: "yellow", borderStyle: "round" }
|
|
108
150
|
)
|
|
109
151
|
);
|
|
@@ -126,26 +168,26 @@ function boxen(text, options) {
|
|
|
126
168
|
return res;
|
|
127
169
|
}
|
|
128
170
|
async function runUpdate(currentVersion) {
|
|
129
|
-
console.log(
|
|
171
|
+
console.log(chalk2.blue(`
|
|
130
172
|
Checking for updates...`));
|
|
131
173
|
const latest = await getLatestVersion();
|
|
132
174
|
if (!latest) {
|
|
133
|
-
console.error(
|
|
175
|
+
console.error(chalk2.red(" \u2717 Could not fetch the latest version from npm registry."));
|
|
134
176
|
return;
|
|
135
177
|
}
|
|
136
178
|
if (latest === currentVersion) {
|
|
137
|
-
console.log(
|
|
179
|
+
console.log(chalk2.green(` \u2713 You are already on the latest version (${chalk2.bold(currentVersion)}).
|
|
138
180
|
`));
|
|
139
181
|
return;
|
|
140
182
|
}
|
|
141
|
-
console.log(
|
|
142
|
-
console.log(
|
|
183
|
+
console.log(chalk2.yellow(` ! A new version is available: ${chalk2.bold(latest)}`));
|
|
184
|
+
console.log(chalk2.gray(` Current version: ${currentVersion}
|
|
143
185
|
`));
|
|
144
|
-
console.log(
|
|
145
|
-
console.log(
|
|
186
|
+
console.log(chalk2.white(` To update, run the following command:`));
|
|
187
|
+
console.log(chalk2.cyan(`
|
|
146
188
|
npm install -g openhorizon-cli
|
|
147
189
|
`));
|
|
148
|
-
console.log(
|
|
190
|
+
console.log(chalk2.gray(` (Or use your preferred package manager like bun or pnpm)
|
|
149
191
|
`));
|
|
150
192
|
}
|
|
151
193
|
|
|
@@ -160,7 +202,7 @@ function createSpinner(label) {
|
|
|
160
202
|
const interval = setInterval(() => {
|
|
161
203
|
if (stopped) return;
|
|
162
204
|
process.stdout.write(
|
|
163
|
-
`\r${
|
|
205
|
+
`\r${chalk3.cyan(SPINNER_FRAMES[i % SPINNER_FRAMES.length])} ${chalk3.dim(label)} `
|
|
164
206
|
);
|
|
165
207
|
i++;
|
|
166
208
|
}, 80);
|
|
@@ -188,7 +230,7 @@ async function* streamCompletion(baseUrl, apiKey, model, messages) {
|
|
|
188
230
|
Authorization: `Bearer ${apiKey}`,
|
|
189
231
|
"x-api-key": apiKey,
|
|
190
232
|
"x-client": "openhorizon-cli",
|
|
191
|
-
"x-client-version": "1.0.
|
|
233
|
+
"x-client-version": "1.0.7"
|
|
192
234
|
},
|
|
193
235
|
body: JSON.stringify({ model, messages, stream: true })
|
|
194
236
|
});
|
|
@@ -232,13 +274,13 @@ async function runChatLoop(options) {
|
|
|
232
274
|
const keyFingerprint = createHash("sha256").update(apiKey).digest("hex").slice(0, 8);
|
|
233
275
|
const session = `${date}_${keyFingerprint}_${Math.random().toString(36).slice(2, 7)}`;
|
|
234
276
|
console.log(
|
|
235
|
-
"\n" +
|
|
277
|
+
"\n" + chalk3.green("\u2713 Connected") + chalk3.gray(" \xB7 model ") + chalk3.bold.white(model)
|
|
236
278
|
);
|
|
237
|
-
console.log(
|
|
279
|
+
console.log(chalk3.dim(` History \u2192 ${getHistoryPath()}`));
|
|
238
280
|
console.log(
|
|
239
|
-
|
|
281
|
+
chalk3.gray(
|
|
240
282
|
`
|
|
241
|
-
Commands: ${
|
|
283
|
+
Commands: ${chalk3.white("/model")} ${chalk3.white("/clear")} ${chalk3.white("/help")} ${chalk3.white("/version")} ${chalk3.white("/update")} ${chalk3.white("exit")}
|
|
242
284
|
`
|
|
243
285
|
)
|
|
244
286
|
);
|
|
@@ -246,10 +288,10 @@ async function runChatLoop(options) {
|
|
|
246
288
|
while (true) {
|
|
247
289
|
let userInput;
|
|
248
290
|
try {
|
|
249
|
-
userInput = await input({ message:
|
|
291
|
+
userInput = await input({ message: chalk3.cyan("\u276F") });
|
|
250
292
|
} catch (err) {
|
|
251
293
|
if (err?.name === "ExitPromptError") {
|
|
252
|
-
console.log(
|
|
294
|
+
console.log(chalk3.gray("\nGoodbye!\n"));
|
|
253
295
|
process.exit(0);
|
|
254
296
|
}
|
|
255
297
|
throw err;
|
|
@@ -257,36 +299,54 @@ async function runChatLoop(options) {
|
|
|
257
299
|
const trimmed = userInput.trim();
|
|
258
300
|
if (!trimmed) continue;
|
|
259
301
|
if (["exit", "quit", "q"].includes(trimmed.toLowerCase())) {
|
|
260
|
-
console.log(
|
|
302
|
+
console.log(chalk3.gray("\nGoodbye!\n"));
|
|
261
303
|
process.exit(0);
|
|
262
304
|
}
|
|
263
305
|
if (trimmed === "/help") {
|
|
264
|
-
console.log(
|
|
265
|
-
console.log(
|
|
266
|
-
console.log(
|
|
267
|
-
console.log(
|
|
268
|
-
console.log(
|
|
306
|
+
console.log(chalk3.gray("\n /model \u2013 show current model"));
|
|
307
|
+
console.log(chalk3.gray(" /version \u2013 show current CLI version"));
|
|
308
|
+
console.log(chalk3.gray(" /update \u2013 check for and install updates"));
|
|
309
|
+
console.log(chalk3.gray(" /history \u2013 guide on how to view chat history"));
|
|
310
|
+
console.log(chalk3.gray(" /clear \u2013 permanently clear all chat history"));
|
|
311
|
+
console.log(chalk3.gray(" exit \u2013 quit\n"));
|
|
312
|
+
console.log(chalk3.dim(" Tip: Use 'openhorizon history --help' for full history viewer.\n"));
|
|
269
313
|
continue;
|
|
270
314
|
}
|
|
271
315
|
if (trimmed === "/version") {
|
|
272
|
-
console.log(
|
|
273
|
-
OpenHorizon CLI version: ${
|
|
316
|
+
console.log(chalk3.blue(`
|
|
317
|
+
OpenHorizon CLI version: ${chalk3.bold("1.0.7")}
|
|
274
318
|
`));
|
|
275
319
|
continue;
|
|
276
320
|
}
|
|
277
321
|
if (trimmed === "/update") {
|
|
278
|
-
await runUpdate("1.0.
|
|
322
|
+
await runUpdate("1.0.7");
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (trimmed.startsWith("/history")) {
|
|
326
|
+
const args = trimmed.split(" ").slice(1);
|
|
327
|
+
if (args.length === 0) {
|
|
328
|
+
console.log(chalk3.blue("\n History Usage Guide:"));
|
|
329
|
+
console.log(chalk3.gray(" /history <number> \u2013 show last N messages (e.g. /history 10)"));
|
|
330
|
+
console.log(chalk3.gray(" /history --raw \u2013 output history in raw JSONL format\n"));
|
|
331
|
+
console.log(chalk3.dim(" Tip: Use 'openhorizon history' outside as a standalone command.\n"));
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
const limit = args.find((a) => !isNaN(Number(a))) || 5;
|
|
335
|
+
const isRaw = args.includes("--raw");
|
|
336
|
+
const entries = getHistoryEntries();
|
|
337
|
+
printHistoryEntries(entries, Number(limit), isRaw);
|
|
279
338
|
continue;
|
|
280
339
|
}
|
|
281
340
|
if (trimmed === "/model") {
|
|
282
|
-
console.log(
|
|
283
|
-
Model: ${
|
|
341
|
+
console.log(chalk3.gray(`
|
|
342
|
+
Model: ${chalk3.bold.white(model)}
|
|
284
343
|
`));
|
|
285
344
|
continue;
|
|
286
345
|
}
|
|
287
346
|
if (trimmed === "/clear") {
|
|
288
347
|
messages.length = 0;
|
|
289
|
-
|
|
348
|
+
clearHistory();
|
|
349
|
+
console.log(chalk3.gray("\n \u2713 History and conversation persistently cleared.\n"));
|
|
290
350
|
continue;
|
|
291
351
|
}
|
|
292
352
|
messages.push({ role: "user", content: trimmed });
|
|
@@ -303,12 +363,12 @@ async function runChatLoop(options) {
|
|
|
303
363
|
const { promptTokens, completionTokens } = event.usage;
|
|
304
364
|
spinner.stop();
|
|
305
365
|
if (!fullResponse) {
|
|
306
|
-
console.log(
|
|
366
|
+
console.log(chalk3.red(" \u276F No response received.\n"));
|
|
307
367
|
break;
|
|
308
368
|
}
|
|
309
369
|
const rendered = String(await marked(fullResponse)).trim();
|
|
310
|
-
process.stdout.write(
|
|
311
|
-
const usageStr = promptTokens || completionTokens ?
|
|
370
|
+
process.stdout.write(chalk3.green("\u276F ") + rendered + "\n");
|
|
371
|
+
const usageStr = promptTokens || completionTokens ? chalk3.dim(` \u2191${promptTokens} \u2193${completionTokens} tok \xB7 ${elapsed}s`) : chalk3.dim(` ${elapsed}s`);
|
|
312
372
|
process.stdout.write(usageStr + "\n\n");
|
|
313
373
|
appendHistory({
|
|
314
374
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -331,15 +391,15 @@ async function runChatLoop(options) {
|
|
|
331
391
|
spinner.stop();
|
|
332
392
|
const msg = err?.message ?? String(err);
|
|
333
393
|
if (msg.includes("404")) {
|
|
334
|
-
console.log(
|
|
394
|
+
console.log(chalk3.red(`
|
|
335
395
|
\u276F 404 \u2014 check your API URL (${baseUrl})
|
|
336
396
|
`));
|
|
337
397
|
} else if (msg.includes("401") || msg.includes("403")) {
|
|
338
|
-
console.log(
|
|
398
|
+
console.log(chalk3.red("\n \u276F Authentication failed \u2014 check your API key.\n"));
|
|
339
399
|
} else if (msg.includes("429")) {
|
|
340
|
-
console.log(
|
|
400
|
+
console.log(chalk3.red("\n \u276F Rate limit reached \u2014 try again shortly.\n"));
|
|
341
401
|
} else {
|
|
342
|
-
console.log(
|
|
402
|
+
console.log(chalk3.red(`
|
|
343
403
|
\u276F ${msg}
|
|
344
404
|
`));
|
|
345
405
|
}
|
|
@@ -351,58 +411,27 @@ async function runChatLoop(options) {
|
|
|
351
411
|
// src/index.ts
|
|
352
412
|
dotenv.config();
|
|
353
413
|
var program = new Command();
|
|
354
|
-
var version = "1.0.
|
|
414
|
+
var version = "1.0.7";
|
|
355
415
|
program.name("openhorizon").description("CLI to interact with OpenHorizon AI Models").version(version, "-v, --version", "Output the current version");
|
|
356
416
|
program.command("version").description("Show the current CLI version").action(() => {
|
|
357
|
-
console.log(
|
|
417
|
+
console.log(chalk4.blue(`OpenHorizon CLI version: ${chalk4.bold(version)}`));
|
|
358
418
|
});
|
|
359
419
|
program.command("update").description("Check for and perform CLI updates").action(async () => {
|
|
360
420
|
await runUpdate(version);
|
|
361
421
|
});
|
|
362
422
|
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) => {
|
|
363
423
|
const entries = getHistoryEntries();
|
|
364
|
-
|
|
365
|
-
console.log(chalk3.gray("\n No history found.\n"));
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
if (options.raw) {
|
|
369
|
-
entries.slice(-Number(options.limit)).forEach((e) => console.log(JSON.stringify(e)));
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
console.log(chalk3.bold.blue(`
|
|
373
|
-
Recent Chat History (last ${options.limit})
|
|
374
|
-
`));
|
|
375
|
-
const limit = Number(options.limit);
|
|
376
|
-
const subset = entries.slice(-limit);
|
|
377
|
-
subset.forEach((entry, i) => {
|
|
378
|
-
const date = new Date(entry.ts).toLocaleString([], {
|
|
379
|
-
month: "short",
|
|
380
|
-
day: "numeric",
|
|
381
|
-
hour: "2-digit",
|
|
382
|
-
minute: "2-digit"
|
|
383
|
-
});
|
|
384
|
-
const roleStr = entry.role === "user" ? chalk3.green("\u276F You") : chalk3.blue("\u276F Assistant");
|
|
385
|
-
const prevEntry = subset[i - 1];
|
|
386
|
-
if (!prevEntry || prevEntry.session !== entry.session) {
|
|
387
|
-
console.log(chalk3.dim(` \u2500\u2500 Session: ${entry.session.slice(0, 8)}\u2026 \u2500\u2500`));
|
|
388
|
-
}
|
|
389
|
-
console.log(`${chalk3.dim(date)} ${roleStr}`);
|
|
390
|
-
console.log(` ${entry.content.replace(/\n/g, "\n ")}`);
|
|
391
|
-
if (entry.usage) {
|
|
392
|
-
console.log(chalk3.dim(` tokens: \u2191${entry.usage.promptTokens} \u2193${entry.usage.completionTokens} \xB7 ${entry.latencyMs}ms`));
|
|
393
|
-
}
|
|
394
|
-
console.log("");
|
|
395
|
-
});
|
|
424
|
+
printHistoryEntries(entries, Number(options.limit), options.raw);
|
|
396
425
|
});
|
|
397
426
|
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) => {
|
|
398
427
|
const config = getConfig();
|
|
399
428
|
await checkForUpdate(version);
|
|
400
429
|
const apiKey = process.env.OPENHORIZON_API_KEY || config.apiKey;
|
|
401
430
|
if (!apiKey) {
|
|
402
|
-
console.error(
|
|
431
|
+
console.error(chalk4.red("\n\u2717 Missing API key."));
|
|
403
432
|
console.log(
|
|
404
|
-
|
|
405
|
-
" Run '" +
|
|
433
|
+
chalk4.gray(
|
|
434
|
+
" Run '" + chalk4.white("openhorizon login <api_key>") + chalk4.gray("' or set ") + chalk4.white("OPENHORIZON_API_KEY") + chalk4.gray(".\n")
|
|
406
435
|
)
|
|
407
436
|
);
|
|
408
437
|
process.exit(1);
|
|
@@ -415,9 +444,9 @@ program.command("model").description("Get or set the default AI model").argument
|
|
|
415
444
|
const config = getConfig();
|
|
416
445
|
if (modelName) {
|
|
417
446
|
saveConfig({ defaultModel: modelName });
|
|
418
|
-
console.log(
|
|
447
|
+
console.log(chalk4.green(`\u2713 Default model set to: ${chalk4.bold(modelName)}`));
|
|
419
448
|
} else {
|
|
420
|
-
console.log(
|
|
449
|
+
console.log(chalk4.blue(`Current default model: ${chalk4.bold(config.defaultModel ?? "(not set)")}`));
|
|
421
450
|
}
|
|
422
451
|
});
|
|
423
452
|
program.command("login").description("Save your OpenHorizon API Key securely to local config").argument("[apiKey]", "Your OpenHorizon API Key").action(async (apiKeyInput) => {
|
|
@@ -430,6 +459,6 @@ program.command("login").description("Save your OpenHorizon API Key securely to
|
|
|
430
459
|
});
|
|
431
460
|
}
|
|
432
461
|
saveConfig({ apiKey: key });
|
|
433
|
-
console.log(
|
|
462
|
+
console.log(chalk4.green("\u2713 API Key saved successfully."));
|
|
434
463
|
});
|
|
435
464
|
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.7",
|
|
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
|
}
|