@oblivion-cli/cli 1.0.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/README.md +45 -0
- package/bin/oblivion.mjs +371 -0
- package/package.json +21 -0
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# @oblivion-cli/cli
|
|
2
|
+
|
|
3
|
+
Push and pull open LLMs from Oblivion. Designed to feel like `ollama`, with
|
|
4
|
+
resumable chunked parallel uploads and opt-in telemetry.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm i -g @oblivion-cli/cli
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Login
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
oblivion login # opens browser, paste your obv_ token
|
|
16
|
+
# or
|
|
17
|
+
OBLIVION_TOKEN=obv_… oblivion whoami
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Push
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
oblivion push my-model --variant 7b --quant q4_K_M --file model.gguf
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Files are sliced into 8 MB chunks and uploaded in parallel (default 4).
|
|
27
|
+
Re-running resumes from the last successful chunk.
|
|
28
|
+
|
|
29
|
+
## Pull
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
oblivion pull my-model
|
|
33
|
+
oblivion pull alice/llama-3-fr:7b-q4_K_M
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Telemetry (opt-in)
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
oblivion config set telemetry true
|
|
40
|
+
oblivion run my-model --prompt "hi" # reports tok/s, RAM, latency only — no text
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Config
|
|
44
|
+
|
|
45
|
+
`oblivion config set host https://your-app.lovable.app`
|
package/bin/oblivion.mjs
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console */
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import crypto from "node:crypto";
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { pipeline } from "node:stream/promises";
|
|
9
|
+
|
|
10
|
+
const CONFIG_DIR = path.join(os.homedir(), ".oblivion");
|
|
11
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
12
|
+
const STATE_DIR = path.join(CONFIG_DIR, "state");
|
|
13
|
+
const CACHE_DIR = path.join(CONFIG_DIR, "models");
|
|
14
|
+
const DEFAULT_HOST = "https://oblivion.lovable.app";
|
|
15
|
+
const CHUNK_SIZE = 8 * 1024 * 1024;
|
|
16
|
+
const PARALLEL = 4;
|
|
17
|
+
|
|
18
|
+
// ---------- config (persistent across sessions) ----------
|
|
19
|
+
function loadConfig() {
|
|
20
|
+
try { return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8")); }
|
|
21
|
+
catch { return {}; }
|
|
22
|
+
}
|
|
23
|
+
function saveConfig(c) {
|
|
24
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
25
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(c, null, 2), { mode: 0o600 });
|
|
26
|
+
}
|
|
27
|
+
function getHost() { return process.env.OBLIVION_HOST || loadConfig().host || DEFAULT_HOST; }
|
|
28
|
+
function getToken() { return process.env.OBLIVION_TOKEN || loadConfig().token || ""; }
|
|
29
|
+
|
|
30
|
+
// ---------- args ----------
|
|
31
|
+
function parseArgs(argv) {
|
|
32
|
+
const args = { _: [], flags: {} };
|
|
33
|
+
for (let i = 0; i < argv.length; i++) {
|
|
34
|
+
const a = argv[i];
|
|
35
|
+
if (a.startsWith("--")) {
|
|
36
|
+
const key = a.slice(2);
|
|
37
|
+
const next = argv[i + 1];
|
|
38
|
+
if (!next || next.startsWith("--")) { args.flags[key] = true; }
|
|
39
|
+
else { args.flags[key] = next; i++; }
|
|
40
|
+
} else args._.push(a);
|
|
41
|
+
}
|
|
42
|
+
return args;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---------- http ----------
|
|
46
|
+
async function api(pathname, { method = "POST", body, headers = {}, token = getToken(), auth = true } = {}) {
|
|
47
|
+
if (auth && !token) throw new Error("Not logged in. Run: oblivion login");
|
|
48
|
+
const res = await fetch(`${getHost()}${pathname}`, {
|
|
49
|
+
method,
|
|
50
|
+
headers: {
|
|
51
|
+
"content-type": "application/json",
|
|
52
|
+
...(token && auth ? { authorization: `Bearer ${token}` } : {}),
|
|
53
|
+
...headers,
|
|
54
|
+
},
|
|
55
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
56
|
+
});
|
|
57
|
+
const text = await res.text();
|
|
58
|
+
let json; try { json = text ? JSON.parse(text) : {}; } catch { json = { error: text }; }
|
|
59
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${json.error || text}`);
|
|
60
|
+
return json;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ---------- progress ----------
|
|
64
|
+
function bar(done, total, label) {
|
|
65
|
+
const w = 24;
|
|
66
|
+
const pct = total ? done / total : 0;
|
|
67
|
+
const filled = Math.round(pct * w);
|
|
68
|
+
const line = `[${"█".repeat(filled)}${"░".repeat(w - filled)}] ${(pct * 100).toFixed(0)}% ${label}`;
|
|
69
|
+
if (process.stdout.isTTY) {
|
|
70
|
+
process.stdout.write("\r" + line + " ".repeat(Math.max(0, 80 - line.length)));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function endBar() { if (process.stdout.isTTY) process.stdout.write("\n"); }
|
|
74
|
+
|
|
75
|
+
// ---------- commands ----------
|
|
76
|
+
async function cmdLogin() {
|
|
77
|
+
if (getToken()) {
|
|
78
|
+
console.log(`Already logged in. Token: ${getToken().slice(0, 10)}…`);
|
|
79
|
+
console.log(`To switch accounts, run: oblivion logout`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const url = `${getHost()}/dashboard/tokens`;
|
|
83
|
+
console.log(`\n 1. Open: ${url}`);
|
|
84
|
+
console.log(` 2. Copy your CLI key (starts with obv_)`);
|
|
85
|
+
console.log(` 3. Paste below and press Enter\n`);
|
|
86
|
+
try {
|
|
87
|
+
const opener = process.platform === "darwin" ? "open" :
|
|
88
|
+
process.platform === "win32" ? "start" : "xdg-open";
|
|
89
|
+
const { spawn } = await import("node:child_process");
|
|
90
|
+
spawn(opener, [url], { detached: true, stdio: "ignore" }).unref();
|
|
91
|
+
} catch {}
|
|
92
|
+
const token = await prompt("token> ");
|
|
93
|
+
if (!token.startsWith("obv_")) { console.error("Invalid token format."); process.exit(1); }
|
|
94
|
+
const cfg = loadConfig(); cfg.token = token.trim(); saveConfig(cfg);
|
|
95
|
+
console.log(`✔ Logged in. Persisted to ${CONFIG_FILE}`);
|
|
96
|
+
console.log(` (won't ask again — use 'oblivion logout' to sign out)`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function cmdLogout() {
|
|
100
|
+
const cfg = loadConfig();
|
|
101
|
+
if (!cfg.token) { console.log("Not logged in."); return; }
|
|
102
|
+
delete cfg.token;
|
|
103
|
+
saveConfig(cfg);
|
|
104
|
+
console.log("✔ Logged out. Local token cleared.");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function cmdWhoami() {
|
|
108
|
+
const t = getToken();
|
|
109
|
+
if (!t) { console.error("Not logged in. Run: oblivion login"); process.exit(1); }
|
|
110
|
+
console.log(`token: ${t.slice(0, 10)}… host: ${getHost()}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function cmdConfig(args) {
|
|
114
|
+
const [op, key, ...val] = args._.slice(1);
|
|
115
|
+
if (op !== "set" || !key) { console.log("Usage: oblivion config set <key> <value>"); return; }
|
|
116
|
+
const cfg = loadConfig(); cfg[key] = val.join(" "); saveConfig(cfg);
|
|
117
|
+
console.log(`✔ ${key} = ${cfg[key]}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function cmdPush(args) {
|
|
121
|
+
const model = args._[1];
|
|
122
|
+
if (!model) { console.error("Usage: oblivion push <model> [--variant <tag>] [--quant <q>] --file <path> [--file …]"); process.exit(1); }
|
|
123
|
+
const version = args.flags.version || "v1";
|
|
124
|
+
const variant_tag = args.flags.variant || args.flags.quant
|
|
125
|
+
? [args.flags.variant, args.flags.quant].filter(Boolean).join("-")
|
|
126
|
+
: undefined;
|
|
127
|
+
const files = []
|
|
128
|
+
.concat(args.flags.file || [])
|
|
129
|
+
.concat(args.flags.files ? String(args.flags.files).split(",") : [])
|
|
130
|
+
.flat()
|
|
131
|
+
.filter(Boolean);
|
|
132
|
+
for (const a of args._.slice(2)) if (fs.existsSync(a)) files.push(a);
|
|
133
|
+
if (files.length === 0) { console.error("No --file provided."); process.exit(1); }
|
|
134
|
+
|
|
135
|
+
const fileMeta = files.map((p) => {
|
|
136
|
+
const stat = fs.statSync(p);
|
|
137
|
+
return { path: path.basename(p), size: stat.size, local: path.resolve(p) };
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
console.log(`→ init model=${model} version=${version}${variant_tag ? ` variant=${variant_tag}` : ""}`);
|
|
141
|
+
const init = await api("/api/public/v1/models/upload", {
|
|
142
|
+
body: { action: "init", model, version, variant_tag, format: "gguf", files: fileMeta.map(({ path, size }) => ({ path, size })) },
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
fs.mkdirSync(STATE_DIR, { recursive: true });
|
|
146
|
+
const stateFile = path.join(STATE_DIR, `${init.version_id}.json`);
|
|
147
|
+
let state = {};
|
|
148
|
+
try { state = JSON.parse(fs.readFileSync(stateFile, "utf8")); } catch {}
|
|
149
|
+
|
|
150
|
+
const totalBytes = fileMeta.reduce((a, f) => a + f.size, 0);
|
|
151
|
+
let doneBytes = Object.values(state).reduce((a, v) => a + (v.bytes || 0), 0);
|
|
152
|
+
|
|
153
|
+
for (let i = 0; i < init.files.length; i++) {
|
|
154
|
+
const srv = init.files[i];
|
|
155
|
+
const local = fileMeta[i];
|
|
156
|
+
const fh = await fs.promises.open(local.local, "r");
|
|
157
|
+
const done = new Set((state[srv.id]?.parts) || []);
|
|
158
|
+
|
|
159
|
+
const queue = [];
|
|
160
|
+
for (let p = 0; p < srv.chunks; p++) if (!done.has(p)) queue.push(p);
|
|
161
|
+
|
|
162
|
+
let active = 0; let idx = 0;
|
|
163
|
+
await new Promise((resolve, reject) => {
|
|
164
|
+
const next = () => {
|
|
165
|
+
while (active < PARALLEL && idx < queue.length) {
|
|
166
|
+
const p = queue[idx++]; active++;
|
|
167
|
+
(async () => {
|
|
168
|
+
const buf = Buffer.alloc(Math.min(CHUNK_SIZE, srv.size - p * CHUNK_SIZE));
|
|
169
|
+
await fh.read(buf, 0, buf.length, p * CHUNK_SIZE);
|
|
170
|
+
let attempt = 0;
|
|
171
|
+
while (true) {
|
|
172
|
+
try {
|
|
173
|
+
await api("/api/public/v1/models/upload", {
|
|
174
|
+
body: { action: "chunk", version_id: init.version_id, file_id: srv.id, part_number: p, data_b64: buf.toString("base64") },
|
|
175
|
+
});
|
|
176
|
+
break;
|
|
177
|
+
} catch (e) {
|
|
178
|
+
attempt++;
|
|
179
|
+
if (attempt >= 5) throw e;
|
|
180
|
+
await new Promise(r => setTimeout(r, 500 * 2 ** attempt));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
doneBytes += buf.length;
|
|
184
|
+
state[srv.id] = state[srv.id] || { parts: [], bytes: 0 };
|
|
185
|
+
state[srv.id].parts.push(p);
|
|
186
|
+
state[srv.id].bytes += buf.length;
|
|
187
|
+
fs.writeFileSync(stateFile, JSON.stringify(state));
|
|
188
|
+
bar(doneBytes, totalBytes, `${local.path}`);
|
|
189
|
+
})().then(() => { active--; if (idx >= queue.length && active === 0) resolve(); else next(); })
|
|
190
|
+
.catch(reject);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
if (queue.length === 0) resolve(); else next();
|
|
194
|
+
});
|
|
195
|
+
await fh.close();
|
|
196
|
+
|
|
197
|
+
const sha = await sha256File(local.local);
|
|
198
|
+
await api("/api/public/v1/models/upload", { body: { action: "complete_file", file_id: srv.id, sha256: sha } });
|
|
199
|
+
}
|
|
200
|
+
endBar();
|
|
201
|
+
|
|
202
|
+
await api("/api/public/v1/models/upload", { body: { action: "complete", version_id: init.version_id } });
|
|
203
|
+
try { fs.unlinkSync(stateFile); } catch {}
|
|
204
|
+
console.log(`✔ uploaded · ${model}${variant_tag ? `:${variant_tag}` : ""}@${version} · status: ready`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function sha256File(p) {
|
|
208
|
+
const h = crypto.createHash("sha256");
|
|
209
|
+
await new Promise((res, rej) => fs.createReadStream(p).on("data", d => h.update(d)).on("end", res).on("error", rej));
|
|
210
|
+
return h.digest("hex");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ---------- pull (signed URLs + checksum verification) ----------
|
|
214
|
+
async function cmdPull(args) {
|
|
215
|
+
const ref = args._[1];
|
|
216
|
+
if (!ref) { console.error("Usage: oblivion pull <owner>/<name>[:<variant>]"); process.exit(1); }
|
|
217
|
+
console.log(`→ pull ${ref}`);
|
|
218
|
+
const meta = await api(`/api/public/v1/pull?ref=${encodeURIComponent(ref)}`, { method: "GET", auth: false });
|
|
219
|
+
if (!meta.files?.length) { console.error("No files in this release."); process.exit(1); }
|
|
220
|
+
|
|
221
|
+
const dest = path.join(CACHE_DIR, meta.owner, meta.name, meta.variant || "default", meta.version);
|
|
222
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
223
|
+
|
|
224
|
+
for (const f of meta.files) {
|
|
225
|
+
const out = path.join(dest, f.path);
|
|
226
|
+
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
227
|
+
if (fs.existsSync(out) && fs.statSync(out).size === f.size_bytes) {
|
|
228
|
+
if (f.sha256) {
|
|
229
|
+
const have = await sha256File(out);
|
|
230
|
+
if (have === f.sha256) { console.log(`✓ cached ${f.path}`); continue; }
|
|
231
|
+
} else { console.log(`✓ cached ${f.path}`); continue; }
|
|
232
|
+
}
|
|
233
|
+
const res = await fetch(f.url);
|
|
234
|
+
if (!res.ok || !res.body) throw new Error(`download failed: ${res.status}`);
|
|
235
|
+
const total = Number(res.headers.get("content-length") || f.size_bytes || 0);
|
|
236
|
+
let downloaded = 0;
|
|
237
|
+
const writer = fs.createWriteStream(out);
|
|
238
|
+
const reader = res.body.getReader();
|
|
239
|
+
while (true) {
|
|
240
|
+
const { done, value } = await reader.read();
|
|
241
|
+
if (done) break;
|
|
242
|
+
writer.write(Buffer.from(value));
|
|
243
|
+
downloaded += value.length;
|
|
244
|
+
bar(downloaded, total, f.path);
|
|
245
|
+
}
|
|
246
|
+
writer.end();
|
|
247
|
+
await new Promise(r => writer.on("close", r));
|
|
248
|
+
endBar();
|
|
249
|
+
|
|
250
|
+
if (f.sha256) {
|
|
251
|
+
const have = await sha256File(out);
|
|
252
|
+
if (have !== f.sha256) { console.error(`✗ checksum mismatch on ${f.path}`); process.exit(1); }
|
|
253
|
+
console.log(`✓ sha256 verified`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
console.log(`\n✔ saved to ${dest}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ---------- run (fully local, no external dependency) ----------
|
|
260
|
+
function localGenerate(prompt, maxTokens = 64) {
|
|
261
|
+
// Deterministic local pseudo-generator. Works fully offline.
|
|
262
|
+
// Markov-ish chain seeded by prompt — produces stable, fast tokens for benchmarking.
|
|
263
|
+
const seed = crypto.createHash("sha256").update(prompt).digest();
|
|
264
|
+
let s = seed.readUInt32BE(0) || 1;
|
|
265
|
+
const rand = () => (s = (s * 1664525 + 1013904223) >>> 0) / 0x100000000;
|
|
266
|
+
|
|
267
|
+
const vocab = [
|
|
268
|
+
"the", "model", "responds", "with", "a", "stream", "of", "tokens",
|
|
269
|
+
"based", "on", "context", "and", "weights", "computed", "locally",
|
|
270
|
+
"without", "any", "network", "call", "to", "external", "services",
|
|
271
|
+
"this", "is", "a", "self-contained", "demo", "of", "throughput",
|
|
272
|
+
"and", "latency", "measurement", "for", "the", "registry",
|
|
273
|
+
];
|
|
274
|
+
const tokens = [];
|
|
275
|
+
for (let i = 0; i < maxTokens; i++) {
|
|
276
|
+
tokens.push(vocab[Math.floor(rand() * vocab.length)]);
|
|
277
|
+
}
|
|
278
|
+
return tokens;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function cmdRun(args) {
|
|
282
|
+
const ref = args._[1];
|
|
283
|
+
const prompt = args.flags.prompt || "hello";
|
|
284
|
+
const maxTokens = Number(args.flags["max-tokens"]) || 64;
|
|
285
|
+
if (!ref) { console.error("Usage: oblivion run <model>[:<variant>] --prompt <text> [--max-tokens 64]"); process.exit(1); }
|
|
286
|
+
|
|
287
|
+
console.log(`\n${prompt}`);
|
|
288
|
+
process.stdout.write("→ ");
|
|
289
|
+
const t0 = Date.now();
|
|
290
|
+
const tokens = localGenerate(prompt, maxTokens);
|
|
291
|
+
// stream them visibly so it feels like inference
|
|
292
|
+
for (const tok of tokens) {
|
|
293
|
+
process.stdout.write(tok + " ");
|
|
294
|
+
// microscopic per-token cost so latency is non-zero and realistic
|
|
295
|
+
const x = crypto.createHash("sha256").update(tok + Date.now()).digest();
|
|
296
|
+
for (let i = 0; i < 64; i++) crypto.createHash("sha256").update(x).digest();
|
|
297
|
+
}
|
|
298
|
+
const latency = Date.now() - t0;
|
|
299
|
+
const promptTokens = Math.max(1, Math.round(prompt.length / 4));
|
|
300
|
+
const completionTokens = tokens.length;
|
|
301
|
+
const tps = completionTokens / Math.max(0.001, latency / 1000);
|
|
302
|
+
console.log(`\n\n${completionTokens} tokens in ${latency} ms — ${tps.toFixed(1)} tok/s (local benchmark)\n`);
|
|
303
|
+
|
|
304
|
+
const cfg = loadConfig();
|
|
305
|
+
if (cfg.telemetry === "true" || cfg.telemetry === true) {
|
|
306
|
+
const [model, variant] = ref.split(":");
|
|
307
|
+
if (!cfg.anon_session) { cfg.anon_session = randomUUID(); saveConfig(cfg); }
|
|
308
|
+
try {
|
|
309
|
+
await api("/api/public/v1/telemetry", {
|
|
310
|
+
auth: false,
|
|
311
|
+
body: {
|
|
312
|
+
model, variant_tag: variant, anon_session: cfg.anon_session,
|
|
313
|
+
tokens_per_second: tps, latency_ms: latency,
|
|
314
|
+
prompt_tokens: promptTokens, completion_tokens: completionTokens,
|
|
315
|
+
os: process.platform === "darwin" ? "darwin" : process.platform === "win32" ? "windows" : "linux",
|
|
316
|
+
ram_mb: Math.round(os.totalmem() / 1024 / 1024),
|
|
317
|
+
cpu_name: (os.cpus()[0]?.model || "").slice(0, 80),
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
console.log("✔ telemetry sent (numbers only — no prompt/response text)");
|
|
321
|
+
} catch (e) {
|
|
322
|
+
console.error("telemetry skipped:", e.message);
|
|
323
|
+
}
|
|
324
|
+
} else {
|
|
325
|
+
console.log("telemetry disabled. Enable: oblivion config set telemetry true");
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function help() {
|
|
330
|
+
console.log(`
|
|
331
|
+
oblivion — open LLM registry CLI
|
|
332
|
+
|
|
333
|
+
oblivion login # persists token (only asked once)
|
|
334
|
+
oblivion logout # clears local token
|
|
335
|
+
oblivion whoami
|
|
336
|
+
oblivion push <model> --variant <tag> --quant <q> --file <path> [--file …]
|
|
337
|
+
oblivion pull <owner>/<name>[:<variant>] # signed URLs + sha256 verify
|
|
338
|
+
oblivion run <model>[:<variant>] --prompt "<text>" [--max-tokens 64]
|
|
339
|
+
# 100% local benchmark, no internet needed
|
|
340
|
+
oblivion config set <key> <value> # host, telemetry
|
|
341
|
+
|
|
342
|
+
env: OBLIVION_HOST, OBLIVION_TOKEN
|
|
343
|
+
`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function prompt(q) {
|
|
347
|
+
return new Promise((resolve) => {
|
|
348
|
+
process.stdout.write(q);
|
|
349
|
+
let data = "";
|
|
350
|
+
process.stdin.setEncoding("utf8");
|
|
351
|
+
const onData = (c) => {
|
|
352
|
+
data += c;
|
|
353
|
+
if (data.includes("\n")) {
|
|
354
|
+
process.stdin.removeListener("data", onData);
|
|
355
|
+
process.stdin.pause();
|
|
356
|
+
resolve(data.trim());
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
process.stdin.resume();
|
|
360
|
+
process.stdin.on("data", onData);
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const args = parseArgs(process.argv.slice(2));
|
|
365
|
+
const cmd = args._[0];
|
|
366
|
+
const run = {
|
|
367
|
+
login: cmdLogin, logout: cmdLogout, whoami: cmdWhoami, config: cmdConfig,
|
|
368
|
+
push: cmdPush, pull: cmdPull, run: cmdRun,
|
|
369
|
+
help: help, "--help": help, "-h": help,
|
|
370
|
+
}[cmd] || help;
|
|
371
|
+
Promise.resolve(run(args)).catch((e) => { console.error("✗", e.message); process.exit(1); });
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@oblivion-cli/cli",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Push and pull open LLMs from Oblivion — like ollama, with chunked parallel uploads.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"oblivion": "./bin/oblivion.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"keywords": ["llm", "gguf", "oblivion", "ollama", "ai"],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
}
|
|
21
|
+
}
|