@xberg-io/liter-llm-cli 1.9.3 → 1.10.1

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/bin/liter-llm.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- // Launcher: exec the downloaded native liter-llm binary, forwarding argv and
3
- // inheriting stdio. If the binary is missing (postinstall failed), download it
4
- // on demand before exec.
2
+ // ~keep Launcher: exec the downloaded native liter-llm binary, forwarding argv and
3
+ // ~keep inheriting stdio. If the binary is missing (postinstall failed), download it
4
+ // ~keep on demand before exec.
5
5
  import fs from "node:fs";
6
6
  import os from "node:os";
7
7
  import path from "node:path";
@@ -15,11 +15,11 @@ function binaryName() {
15
15
  }
16
16
 
17
17
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
- // install.js extracts the binary into this same bin/ directory.
18
+ // ~keep install.js extracts the binary into this same bin/ directory.
19
19
  const binPath = path.join(__dirname, binaryName());
20
20
 
21
- // A cached binary is only usable if it is non-empty and (on non-Windows) has an
22
- // exec bit. A truncated or non-executable file means a corrupt cache: re-download.
21
+ // ~keep A cached binary is only usable if it is non-empty and (on non-Windows) has an
22
+ // ~keep exec bit. A truncated or non-executable file means a corrupt cache: re-download.
23
23
  function isHealthy(file) {
24
24
  try {
25
25
  const stat = fs.statSync(file);
@@ -34,9 +34,9 @@ function isHealthy(file) {
34
34
  async function ensureBinary() {
35
35
  if (fs.existsSync(binPath) && isHealthy(binPath)) return;
36
36
  process.stderr.write(`${BIN_NAME}: binary missing or corrupt, attempting download...\n`);
37
- // Call main() explicitly rather than relying on import side-effects: ESM
38
- // caches modules, so the installer's top-level run is gated to direct
39
- // invocation only and would not fire on import.
37
+ // ~keep Call main() explicitly rather than relying on import side-effects: ESM
38
+ // ~keep caches modules, so the installer's top-level run is gated to direct
39
+ // ~keep invocation only and would not fire on import.
40
40
  const { main } = await import("../install.js");
41
41
  await main();
42
42
  }
@@ -64,7 +64,7 @@ async function main() {
64
64
  }
65
65
 
66
66
  main().catch((err) => {
67
- // No standalone CLI for this platform: print the graceful install hint, not a stack.
67
+ // ~keep No standalone CLI for this platform: print the graceful install hint, not a stack.
68
68
  if (err && err.name === "CliUnavailableError") {
69
69
  printUnavailable();
70
70
  process.exit(1);
package/install.js CHANGED
@@ -1,5 +1,3 @@
1
- // postinstall: download, verify, and extract the native liter-llm binary
2
- // into ./bin so the launcher can exec it. All diagnostics go to stderr.
3
1
  import fs from "node:fs";
4
2
  import os from "node:os";
5
3
  import path from "node:path";
@@ -14,7 +12,6 @@ const PKG_NAME = "liter-llm-cli";
14
12
  const VERSION_ENV = "LITER_LLM_CLI_VERSION";
15
13
  const USER_AGENT = "liter-llm-cli-npm-proxy";
16
14
 
17
- // Map Node's platform/arch to the Rust target triple embedded in asset names.
18
15
  function targetTriple() {
19
16
  const type = os.type();
20
17
  const arch = os.arch();
@@ -40,9 +37,6 @@ function binaryName() {
40
37
  return os.type() === "Windows_NT" ? `${BIN_NAME}.exe` : BIN_NAME;
41
38
  }
42
39
 
43
- // GET a URL following redirects, returning the response body as a Buffer.
44
- // Every hop (initial request and every redirect target) MUST be https; any
45
- // other scheme is rejected to prevent downgrade/SSRF via a malicious Location.
46
40
  function httpGetBuffer(url, { headers = {} } = {}, maxRedirects = 5) {
47
41
  return new Promise((resolve, reject) => {
48
42
  if (maxRedirects < 0) return reject(new Error("too many redirects"));
@@ -80,9 +74,6 @@ async function httpGetJson(url) {
80
74
  return JSON.parse(buf.toString("utf8"));
81
75
  }
82
76
 
83
- // Signals that the release carries no standalone CLI for this platform (only
84
- // bindings/native-lib/brew-bottle artifacts). The launcher catches this by name
85
- // and prints a graceful install hint instead of a raw stack trace.
86
77
  export class CliUnavailableError extends Error {
87
78
  constructor(message) {
88
79
  super(message);
@@ -90,8 +81,6 @@ export class CliUnavailableError extends Error {
90
81
  }
91
82
  }
92
83
 
93
- // Substrings that mark an asset as a binding/native-lib/brew-bottle artifact —
94
- // never the standalone CLI. Matched case-insensitively anywhere in the name.
95
84
  const NON_CLI_PATTERNS = [
96
85
  "-ffi",
97
86
  "_ffi",
@@ -109,7 +98,6 @@ const NON_CLI_PATTERNS = [
109
98
  "napi",
110
99
  ];
111
100
 
112
- // True if the asset name matches any non-CLI artifact pattern.
113
101
  export function isNonCliArtifact(name) {
114
102
  const n = (name || "").toLowerCase();
115
103
  return NON_CLI_PATTERNS.some((pat) => n.includes(pat));
@@ -123,9 +111,6 @@ export function assetScore(name) {
123
111
  return score;
124
112
  }
125
113
 
126
- // Pure asset-selection core: from a list of asset names, keep only triple-matched
127
- // .tar.gz/.zip archives that are NOT binding/native-lib/bottle artifacts, then
128
- // return the best (cli/bin-name preferred). Returns null when none qualify.
129
114
  export function selectArchiveName(names, triple) {
130
115
  const survivors = (names || []).filter((name) => {
131
116
  const n = (name || "").toLowerCase();
@@ -138,13 +123,6 @@ export function selectArchiveName(names, triple) {
138
123
  return survivors[0];
139
124
  }
140
125
 
141
- // Resolve the release (honoring LITER_LLM_CLI_VERSION to pin a tag) and pick the
142
- // archive asset for this platform plus an optional SHA256SUMS asset.
143
- //
144
- // Selection: among assets whose name contains the target triple, ends in
145
- // .tar.gz/.zip, and is NOT a binding/native-lib/bottle artifact, prefer one
146
- // whose name contains "cli" or the bin name. If none survive, the release has
147
- // no standalone CLI for this platform (CliUnavailableError).
148
126
  async function resolveRelease() {
149
127
  const triple = targetTriple();
150
128
  const pinned = process.env[VERSION_ENV];
@@ -180,7 +158,6 @@ async function resolveRelease() {
180
158
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
181
159
  const BIN_DIR = path.join(__dirname, "bin");
182
160
 
183
- // Parse a `sha256<space>filename` checksums file; return the digest for name.
184
161
  function expectedDigest(text, assetName) {
185
162
  for (const raw of text.split(/\r?\n/)) {
186
163
  const line = raw.trim();
@@ -215,18 +192,15 @@ async function verifyOrWarn(archiveBuf, archiveName, checksums) {
215
192
  process.stderr.write(`Checksum verified for ${archiveName}.\n`);
216
193
  }
217
194
 
218
- // Reject archive entry names that could escape the extraction directory:
219
- // absolute paths (POSIX or Windows drive/UNC) or any component equal to "..".
220
195
  function isUnsafeEntry(name) {
221
196
  const entry = String(name).replace(/\\/g, "/").trim();
222
197
  if (!entry) return false;
223
198
  if (entry.startsWith("/")) return true;
224
- if (/^[a-zA-Z]:/.test(entry)) return true; // Windows drive letter
225
- if (entry.startsWith("//")) return true; // UNC
199
+ if (/^[a-zA-Z]:/.test(entry)) return true;
200
+ if (entry.startsWith("//")) return true;
226
201
  return entry.split("/").some((part) => part === "..");
227
202
  }
228
203
 
229
- // List the entries of a gzipped tar without extracting (`tar -tzf`).
230
204
  function listTarEntries(archivePath) {
231
205
  const result = spawnSync("tar", ["-tzf", archivePath]);
232
206
  if (result.status !== 0) {
@@ -248,7 +222,6 @@ function extractTarGz(archivePath, destDir) {
248
222
  }
249
223
  }
250
224
 
251
- // List the entries of a zip without extracting (`unzip -Z1`, or PowerShell on Windows).
252
225
  function listZipEntries(archivePath) {
253
226
  if (os.type() === "Windows_NT") {
254
227
  const script =
@@ -278,7 +251,6 @@ function listZipEntries(archivePath) {
278
251
 
279
252
  function extractZip(archivePath, destDir) {
280
253
  if (os.type() === "Windows_NT") {
281
- // No string interpolation into a -Command: all path data passed as literal args.
282
254
  const result = spawnSync("powershell", [
283
255
  "-NoProfile",
284
256
  "-NonInteractive",
@@ -303,7 +275,6 @@ function extractZip(archivePath, destDir) {
303
275
  }
304
276
  }
305
277
 
306
- // Locate the binary anywhere under dir (archives may nest it in a subdir).
307
278
  function findBinary(dir, name) {
308
279
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
309
280
  const full = path.join(dir, entry.name);
@@ -317,7 +288,6 @@ function findBinary(dir, name) {
317
288
  return null;
318
289
  }
319
290
 
320
- // Find a directory named `name` anywhere under dir.
321
291
  function findDir(dir, name) {
322
292
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
323
293
  if (!entry.isDirectory()) continue;
@@ -328,10 +298,6 @@ function findDir(dir, name) {
328
298
  return null;
329
299
  }
330
300
 
331
- // Validate every archive entry, extract into an isolated temp dir, then copy
332
- // out ONLY the expected binary (by basename) plus an optional sibling lib/ dir.
333
- // Nothing else from the archive is honored, so a malicious member can never
334
- // land outside dest even if extraction tooling mishandled it.
335
301
  function safeExtract(archivePath, archiveName, dest) {
336
302
  const isZip = archiveName.toLowerCase().endsWith(".zip");
337
303
  const entries = isZip ? listZipEntries(archivePath) : listTarEntries(archivePath);
@@ -352,15 +318,11 @@ function safeExtract(archivePath, archiveName, dest) {
352
318
  const binName = binaryName();
353
319
  const extractedBin = findBinary(tmpDir, binName);
354
320
  if (!extractedBin) {
355
- // The chosen asset did not actually contain the CLI binary — treat the
356
- // release as having no valid CLI for this platform rather than leaving a
357
- // bad/partial install behind.
358
321
  throw new CliUnavailableError(`archive ${archiveName} did not contain expected CLI binary ${binName}`);
359
322
  }
360
323
  const finalBin = path.join(dest, binName);
361
324
  fs.copyFileSync(extractedBin, finalBin);
362
325
 
363
- // Copy a sibling lib/ directory if present (some platforms ship shared libs).
364
326
  const libDir = findDir(tmpDir, "lib");
365
327
  if (libDir) {
366
328
  fs.cpSync(libDir, path.join(dest, "lib"), { recursive: true });
@@ -380,9 +342,7 @@ export async function main() {
380
342
  const sizeOk = stat.size > 0;
381
343
  const execOk = os.type() === "Windows_NT" || (stat.mode & 0o111) !== 0;
382
344
  if (sizeOk && execOk) return;
383
- } catch {
384
- // fall through and re-download
385
- }
345
+ } catch {}
386
346
  }
387
347
 
388
348
  fs.mkdirSync(BIN_DIR, { recursive: true });
@@ -393,7 +353,6 @@ export async function main() {
393
353
  const archiveBuf = await httpGetBuffer(archive.browser_download_url);
394
354
  await verifyOrWarn(archiveBuf, archive.name, checksums);
395
355
 
396
- // Stage the archive in an isolated temp dir; never extract straight into BIN_DIR.
397
356
  const stageDir = fs.mkdtempSync(path.join(os.tmpdir(), `${PKG_NAME}-dl-`));
398
357
  try {
399
358
  const archivePath = path.join(stageDir, path.basename(archive.name));
@@ -409,9 +368,6 @@ export async function main() {
409
368
  process.stderr.write(`${BIN_NAME} installed.\n`);
410
369
  }
411
370
 
412
- // Run automatically only when invoked directly (npm postinstall: `node install.js`).
413
- // When imported by the launcher, the launcher calls main() explicitly instead of
414
- // relying on import side-effects (ESM caches modules, so a second import is a no-op).
415
371
  if (import.meta.url === pathToFileURL(process.argv[1] || "").href) {
416
372
  main().catch((err) => {
417
373
  process.stderr.write(`Error installing ${BIN_NAME}: ${err.message}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xberg-io/liter-llm-cli",
3
- "version": "1.9.3",
3
+ "version": "1.10.1",
4
4
  "description": "CLI proxy for liter-llm — downloads and runs the native liter-llm binary from GitHub releases.",
5
5
  "license": "MIT",
6
6
  "author": "Na'aman Hirschfeld",