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.
Files changed (2) hide show
  1. package/dist/index.js +113 -48
  2. 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 chalk3 from "chalk";
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 chalk2 from "chalk";
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 chalk from "chalk";
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
- `${chalk.yellow("Update available!")} ${chalk.dim(currentVersion)} \u2192 ${chalk.green(latest)}
89
- Run ${chalk.cyan("openhorizon update")} to update.`,
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(chalk.blue(`
163
+ console.log(chalk2.blue(`
113
164
  Checking for updates...`));
114
165
  const latest = await getLatestVersion();
115
166
  if (!latest) {
116
- console.error(chalk.red(" \u2717 Could not fetch the latest version from npm registry."));
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(chalk.green(` \u2713 You are already on the latest version (${chalk.bold(currentVersion)}).
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(chalk.yellow(` ! A new version is available: ${chalk.bold(latest)}`));
125
- console.log(chalk.gray(` Current version: ${currentVersion}
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(chalk.white(` To update, run the following command:`));
128
- console.log(chalk.cyan(`
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(chalk.gray(` (Or use your preferred package manager like bun or pnpm)
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${chalk2.cyan(SPINNER_FRAMES[i % SPINNER_FRAMES.length])} ${chalk2.dim(label)} `
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.4"
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" + chalk2.green("\u2713 Connected") + chalk2.gray(" \xB7 model ") + chalk2.bold.white(model)
269
+ "\n" + chalk3.green("\u2713 Connected") + chalk3.gray(" \xB7 model ") + chalk3.bold.white(model)
219
270
  );
220
- console.log(chalk2.dim(` History \u2192 ${getHistoryPath()}`));
271
+ console.log(chalk3.dim(` History \u2192 ${getHistoryPath()}`));
221
272
  console.log(
222
- chalk2.gray(
273
+ chalk3.gray(
223
274
  `
224
- Commands: ${chalk2.white("/model")} ${chalk2.white("/clear")} ${chalk2.white("/help")} ${chalk2.white("/version")} ${chalk2.white("exit")}
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: chalk2.cyan("\u276F") });
283
+ userInput = await input({ message: chalk3.cyan("\u276F") });
233
284
  } catch (err) {
234
285
  if (err?.name === "ExitPromptError") {
235
- console.log(chalk2.gray("\nGoodbye!\n"));
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(chalk2.gray("\nGoodbye!\n"));
294
+ console.log(chalk3.gray("\nGoodbye!\n"));
244
295
  process.exit(0);
245
296
  }
246
297
  if (trimmed === "/help") {
247
- console.log(chalk2.gray("\n /model \u2013 show current model"));
248
- console.log(chalk2.gray(" /version \u2013 show current CLI version"));
249
- console.log(chalk2.gray(" /update \u2013 check for and install updates"));
250
- console.log(chalk2.gray(" /clear \u2013 clear conversation history"));
251
- console.log(chalk2.gray(" exit \u2013 quit\n"));
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(chalk2.blue(`
256
- OpenHorizon CLI version: ${chalk2.bold("1.0.4")}
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.4");
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(chalk2.gray(`
266
- Model: ${chalk2.bold.white(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(chalk2.gray("\n \u2713 Conversation cleared.\n"));
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(chalk2.red(" \u276F No response received.\n"));
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(chalk2.green("\u276F ") + rendered + "\n");
294
- const usageStr = promptTokens || completionTokens ? chalk2.dim(` \u2191${promptTokens} \u2193${completionTokens} tok \xB7 ${elapsed}s`) : chalk2.dim(` ${elapsed}s`);
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(chalk2.red(`
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(chalk2.red("\n \u276F Authentication failed \u2014 check your API key.\n"));
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(chalk2.red("\n \u276F Rate limit reached \u2014 try again shortly.\n"));
384
+ console.log(chalk3.red("\n \u276F Rate limit reached \u2014 try again shortly.\n"));
324
385
  } else {
325
- console.log(chalk2.red(`
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.4";
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(chalk3.blue(`OpenHorizon CLI version: ${chalk3.bold(version)}`));
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(chalk3.red("\n\u2717 Missing API key."));
415
+ console.error(chalk4.red("\n\u2717 Missing API key."));
351
416
  console.log(
352
- chalk3.gray(
353
- " Run '" + chalk3.white("openhorizon login <api_key>") + chalk3.gray("' or set ") + chalk3.white("OPENHORIZON_API_KEY") + chalk3.gray(".\n")
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(chalk3.green(`\u2713 Default model set to: ${chalk3.bold(modelName)}`));
431
+ console.log(chalk4.green(`\u2713 Default model set to: ${chalk4.bold(modelName)}`));
367
432
  } else {
368
- console.log(chalk3.blue(`Current default model: ${chalk3.bold(config.defaultModel ?? "(not set)")}`));
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(chalk3.green("\u2713 API Key saved successfully."));
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.4",
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": "^17.0.5",
53
+ "marked": "^15.0.0",
54
54
  "marked-terminal": "^7.3.0"
55
55
  }
56
56
  }