openhorizon-cli 1.0.5 → 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 +93 -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,42 @@ function getHistoryEntries() {
|
|
|
81
82
|
return [];
|
|
82
83
|
}
|
|
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
|
+
}
|
|
84
118
|
|
|
85
119
|
// src/update.ts
|
|
86
|
-
import
|
|
120
|
+
import chalk2 from "chalk";
|
|
87
121
|
async function getLatestVersion() {
|
|
88
122
|
try {
|
|
89
123
|
const response = await fetch("https://registry.npmjs.org/openhorizon-cli/latest", {
|
|
@@ -102,8 +136,8 @@ async function checkForUpdate(currentVersion) {
|
|
|
102
136
|
if (latest !== currentVersion) {
|
|
103
137
|
console.log(
|
|
104
138
|
boxen(
|
|
105
|
-
`${
|
|
106
|
-
Run ${
|
|
139
|
+
`${chalk2.yellow("Update available!")} ${chalk2.dim(currentVersion)} \u2192 ${chalk2.green(latest)}
|
|
140
|
+
Run ${chalk2.cyan("openhorizon update")} to update.`,
|
|
107
141
|
{ padding: 1, margin: 1, borderColor: "yellow", borderStyle: "round" }
|
|
108
142
|
)
|
|
109
143
|
);
|
|
@@ -126,26 +160,26 @@ function boxen(text, options) {
|
|
|
126
160
|
return res;
|
|
127
161
|
}
|
|
128
162
|
async function runUpdate(currentVersion) {
|
|
129
|
-
console.log(
|
|
163
|
+
console.log(chalk2.blue(`
|
|
130
164
|
Checking for updates...`));
|
|
131
165
|
const latest = await getLatestVersion();
|
|
132
166
|
if (!latest) {
|
|
133
|
-
console.error(
|
|
167
|
+
console.error(chalk2.red(" \u2717 Could not fetch the latest version from npm registry."));
|
|
134
168
|
return;
|
|
135
169
|
}
|
|
136
170
|
if (latest === currentVersion) {
|
|
137
|
-
console.log(
|
|
171
|
+
console.log(chalk2.green(` \u2713 You are already on the latest version (${chalk2.bold(currentVersion)}).
|
|
138
172
|
`));
|
|
139
173
|
return;
|
|
140
174
|
}
|
|
141
|
-
console.log(
|
|
142
|
-
console.log(
|
|
175
|
+
console.log(chalk2.yellow(` ! A new version is available: ${chalk2.bold(latest)}`));
|
|
176
|
+
console.log(chalk2.gray(` Current version: ${currentVersion}
|
|
143
177
|
`));
|
|
144
|
-
console.log(
|
|
145
|
-
console.log(
|
|
178
|
+
console.log(chalk2.white(` To update, run the following command:`));
|
|
179
|
+
console.log(chalk2.cyan(`
|
|
146
180
|
npm install -g openhorizon-cli
|
|
147
181
|
`));
|
|
148
|
-
console.log(
|
|
182
|
+
console.log(chalk2.gray(` (Or use your preferred package manager like bun or pnpm)
|
|
149
183
|
`));
|
|
150
184
|
}
|
|
151
185
|
|
|
@@ -160,7 +194,7 @@ function createSpinner(label) {
|
|
|
160
194
|
const interval = setInterval(() => {
|
|
161
195
|
if (stopped) return;
|
|
162
196
|
process.stdout.write(
|
|
163
|
-
`\r${
|
|
197
|
+
`\r${chalk3.cyan(SPINNER_FRAMES[i % SPINNER_FRAMES.length])} ${chalk3.dim(label)} `
|
|
164
198
|
);
|
|
165
199
|
i++;
|
|
166
200
|
}, 80);
|
|
@@ -188,7 +222,7 @@ async function* streamCompletion(baseUrl, apiKey, model, messages) {
|
|
|
188
222
|
Authorization: `Bearer ${apiKey}`,
|
|
189
223
|
"x-api-key": apiKey,
|
|
190
224
|
"x-client": "openhorizon-cli",
|
|
191
|
-
"x-client-version": "1.0.
|
|
225
|
+
"x-client-version": "1.0.6"
|
|
192
226
|
},
|
|
193
227
|
body: JSON.stringify({ model, messages, stream: true })
|
|
194
228
|
});
|
|
@@ -232,13 +266,13 @@ async function runChatLoop(options) {
|
|
|
232
266
|
const keyFingerprint = createHash("sha256").update(apiKey).digest("hex").slice(0, 8);
|
|
233
267
|
const session = `${date}_${keyFingerprint}_${Math.random().toString(36).slice(2, 7)}`;
|
|
234
268
|
console.log(
|
|
235
|
-
"\n" +
|
|
269
|
+
"\n" + chalk3.green("\u2713 Connected") + chalk3.gray(" \xB7 model ") + chalk3.bold.white(model)
|
|
236
270
|
);
|
|
237
|
-
console.log(
|
|
271
|
+
console.log(chalk3.dim(` History \u2192 ${getHistoryPath()}`));
|
|
238
272
|
console.log(
|
|
239
|
-
|
|
273
|
+
chalk3.gray(
|
|
240
274
|
`
|
|
241
|
-
Commands: ${
|
|
275
|
+
Commands: ${chalk3.white("/model")} ${chalk3.white("/clear")} ${chalk3.white("/help")} ${chalk3.white("/version")} ${chalk3.white("/update")} ${chalk3.white("exit")}
|
|
242
276
|
`
|
|
243
277
|
)
|
|
244
278
|
);
|
|
@@ -246,10 +280,10 @@ async function runChatLoop(options) {
|
|
|
246
280
|
while (true) {
|
|
247
281
|
let userInput;
|
|
248
282
|
try {
|
|
249
|
-
userInput = await input({ message:
|
|
283
|
+
userInput = await input({ message: chalk3.cyan("\u276F") });
|
|
250
284
|
} catch (err) {
|
|
251
285
|
if (err?.name === "ExitPromptError") {
|
|
252
|
-
console.log(
|
|
286
|
+
console.log(chalk3.gray("\nGoodbye!\n"));
|
|
253
287
|
process.exit(0);
|
|
254
288
|
}
|
|
255
289
|
throw err;
|
|
@@ -257,36 +291,46 @@ async function runChatLoop(options) {
|
|
|
257
291
|
const trimmed = userInput.trim();
|
|
258
292
|
if (!trimmed) continue;
|
|
259
293
|
if (["exit", "quit", "q"].includes(trimmed.toLowerCase())) {
|
|
260
|
-
console.log(
|
|
294
|
+
console.log(chalk3.gray("\nGoodbye!\n"));
|
|
261
295
|
process.exit(0);
|
|
262
296
|
}
|
|
263
297
|
if (trimmed === "/help") {
|
|
264
|
-
console.log(
|
|
265
|
-
console.log(
|
|
266
|
-
console.log(
|
|
267
|
-
console.log(
|
|
268
|
-
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"));
|
|
269
305
|
continue;
|
|
270
306
|
}
|
|
271
307
|
if (trimmed === "/version") {
|
|
272
|
-
console.log(
|
|
273
|
-
OpenHorizon CLI version: ${
|
|
308
|
+
console.log(chalk3.blue(`
|
|
309
|
+
OpenHorizon CLI version: ${chalk3.bold("1.0.6")}
|
|
274
310
|
`));
|
|
275
311
|
continue;
|
|
276
312
|
}
|
|
277
313
|
if (trimmed === "/update") {
|
|
278
|
-
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);
|
|
279
323
|
continue;
|
|
280
324
|
}
|
|
281
325
|
if (trimmed === "/model") {
|
|
282
|
-
console.log(
|
|
283
|
-
Model: ${
|
|
326
|
+
console.log(chalk3.gray(`
|
|
327
|
+
Model: ${chalk3.bold.white(model)}
|
|
284
328
|
`));
|
|
285
329
|
continue;
|
|
286
330
|
}
|
|
287
331
|
if (trimmed === "/clear") {
|
|
288
332
|
messages.length = 0;
|
|
289
|
-
console.log(
|
|
333
|
+
console.log(chalk3.gray("\n \u2713 Conversation cleared.\n"));
|
|
290
334
|
continue;
|
|
291
335
|
}
|
|
292
336
|
messages.push({ role: "user", content: trimmed });
|
|
@@ -303,12 +347,12 @@ async function runChatLoop(options) {
|
|
|
303
347
|
const { promptTokens, completionTokens } = event.usage;
|
|
304
348
|
spinner.stop();
|
|
305
349
|
if (!fullResponse) {
|
|
306
|
-
console.log(
|
|
350
|
+
console.log(chalk3.red(" \u276F No response received.\n"));
|
|
307
351
|
break;
|
|
308
352
|
}
|
|
309
353
|
const rendered = String(await marked(fullResponse)).trim();
|
|
310
|
-
process.stdout.write(
|
|
311
|
-
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`);
|
|
312
356
|
process.stdout.write(usageStr + "\n\n");
|
|
313
357
|
appendHistory({
|
|
314
358
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -331,15 +375,15 @@ async function runChatLoop(options) {
|
|
|
331
375
|
spinner.stop();
|
|
332
376
|
const msg = err?.message ?? String(err);
|
|
333
377
|
if (msg.includes("404")) {
|
|
334
|
-
console.log(
|
|
378
|
+
console.log(chalk3.red(`
|
|
335
379
|
\u276F 404 \u2014 check your API URL (${baseUrl})
|
|
336
380
|
`));
|
|
337
381
|
} else if (msg.includes("401") || msg.includes("403")) {
|
|
338
|
-
console.log(
|
|
382
|
+
console.log(chalk3.red("\n \u276F Authentication failed \u2014 check your API key.\n"));
|
|
339
383
|
} else if (msg.includes("429")) {
|
|
340
|
-
console.log(
|
|
384
|
+
console.log(chalk3.red("\n \u276F Rate limit reached \u2014 try again shortly.\n"));
|
|
341
385
|
} else {
|
|
342
|
-
console.log(
|
|
386
|
+
console.log(chalk3.red(`
|
|
343
387
|
\u276F ${msg}
|
|
344
388
|
`));
|
|
345
389
|
}
|
|
@@ -351,58 +395,27 @@ async function runChatLoop(options) {
|
|
|
351
395
|
// src/index.ts
|
|
352
396
|
dotenv.config();
|
|
353
397
|
var program = new Command();
|
|
354
|
-
var version = "1.0.
|
|
398
|
+
var version = "1.0.6";
|
|
355
399
|
program.name("openhorizon").description("CLI to interact with OpenHorizon AI Models").version(version, "-v, --version", "Output the current version");
|
|
356
400
|
program.command("version").description("Show the current CLI version").action(() => {
|
|
357
|
-
console.log(
|
|
401
|
+
console.log(chalk4.blue(`OpenHorizon CLI version: ${chalk4.bold(version)}`));
|
|
358
402
|
});
|
|
359
403
|
program.command("update").description("Check for and perform CLI updates").action(async () => {
|
|
360
404
|
await runUpdate(version);
|
|
361
405
|
});
|
|
362
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) => {
|
|
363
407
|
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
|
-
});
|
|
408
|
+
printHistoryEntries(entries, Number(options.limit), options.raw);
|
|
396
409
|
});
|
|
397
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) => {
|
|
398
411
|
const config = getConfig();
|
|
399
412
|
await checkForUpdate(version);
|
|
400
413
|
const apiKey = process.env.OPENHORIZON_API_KEY || config.apiKey;
|
|
401
414
|
if (!apiKey) {
|
|
402
|
-
console.error(
|
|
415
|
+
console.error(chalk4.red("\n\u2717 Missing API key."));
|
|
403
416
|
console.log(
|
|
404
|
-
|
|
405
|
-
" Run '" +
|
|
417
|
+
chalk4.gray(
|
|
418
|
+
" Run '" + chalk4.white("openhorizon login <api_key>") + chalk4.gray("' or set ") + chalk4.white("OPENHORIZON_API_KEY") + chalk4.gray(".\n")
|
|
406
419
|
)
|
|
407
420
|
);
|
|
408
421
|
process.exit(1);
|
|
@@ -415,9 +428,9 @@ program.command("model").description("Get or set the default AI model").argument
|
|
|
415
428
|
const config = getConfig();
|
|
416
429
|
if (modelName) {
|
|
417
430
|
saveConfig({ defaultModel: modelName });
|
|
418
|
-
console.log(
|
|
431
|
+
console.log(chalk4.green(`\u2713 Default model set to: ${chalk4.bold(modelName)}`));
|
|
419
432
|
} else {
|
|
420
|
-
console.log(
|
|
433
|
+
console.log(chalk4.blue(`Current default model: ${chalk4.bold(config.defaultModel ?? "(not set)")}`));
|
|
421
434
|
}
|
|
422
435
|
});
|
|
423
436
|
program.command("login").description("Save your OpenHorizon API Key securely to local config").argument("[apiKey]", "Your OpenHorizon API Key").action(async (apiKeyInput) => {
|
|
@@ -430,6 +443,6 @@ program.command("login").description("Save your OpenHorizon API Key securely to
|
|
|
430
443
|
});
|
|
431
444
|
}
|
|
432
445
|
saveConfig({ apiKey: key });
|
|
433
|
-
console.log(
|
|
446
|
+
console.log(chalk4.green("\u2713 API Key saved successfully."));
|
|
434
447
|
});
|
|
435
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
|
}
|