offgrid-ai 0.5.4 → 0.6.0
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/package.json +1 -1
- package/src/benchmark.mjs +277 -0
- package/src/cli.mjs +46 -34
package/package.json
CHANGED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { mkdir, writeFile, readFile, readdir } from "node:fs/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { execFile } from "node:child_process";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
import { ensureDirs, loadConfig, saveConfig } from "./config.mjs";
|
|
9
|
+
import { pc, createPrompt, renderRows, renderSection } from "./ui.mjs";
|
|
10
|
+
|
|
11
|
+
const execFileAsync = promisify(execFile);
|
|
12
|
+
|
|
13
|
+
// ── Shared utilities (matches local-llm-visual-benchmark/scripts/local-llm/shared/run-utils.mjs) ──
|
|
14
|
+
|
|
15
|
+
export function slugModelId(modelId, maxLength = 80) {
|
|
16
|
+
const hash = createHash("sha256").update(modelId).digest("hex").slice(0, 10);
|
|
17
|
+
const normalized = modelId.normalize("NFKD").replace(/[\u0300-\u036f]/gu, "").toLowerCase();
|
|
18
|
+
const slug = normalized.replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "").replace(/-{2,}/gu, "-");
|
|
19
|
+
if (slug.length > 0 && slug.length <= maxLength && slug === normalized) return slug;
|
|
20
|
+
const baseMaxLength = Math.max(1, maxLength - 11);
|
|
21
|
+
const base = slug.slice(0, baseMaxLength).replace(/^-+|-+$/gu, "") || "model";
|
|
22
|
+
return `${base}-${hash}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createRunId(date = new Date()) {
|
|
26
|
+
return date.toISOString().replace(/:/gu, "-").replace(/\./gu, "-");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function buildToolPrompt(benchmark, kind) {
|
|
30
|
+
if (kind === "data-science") return benchmark.prompt;
|
|
31
|
+
return [
|
|
32
|
+
"Create a complete, self-contained HTML file for the request below.",
|
|
33
|
+
"Write the file as `index.html` in the current working directory.",
|
|
34
|
+
"Do not create any folders, do not infer a filesystem path, and do not print the HTML in chat.",
|
|
35
|
+
"",
|
|
36
|
+
"The HTML must include all CSS and JavaScript inline and must not depend on external network assets.",
|
|
37
|
+
"After building the page, run a visual QA pass with agent-browser or Playwright: open the saved index.html, inspect the rendered result, and fix any obvious layout, animation, console, or viewport issues before you finish.",
|
|
38
|
+
"",
|
|
39
|
+
benchmark.prompt,
|
|
40
|
+
].join("\n");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function loadBenchmarks(benchDir) {
|
|
44
|
+
const entries = await readdir(benchDir);
|
|
45
|
+
const markdownFiles = entries.filter((f) => f.endsWith(".md")).sort();
|
|
46
|
+
const benchmarks = [];
|
|
47
|
+
for (const filename of markdownFiles) {
|
|
48
|
+
const raw = await readFile(join(benchDir, filename), "utf8");
|
|
49
|
+
// Simple frontmatter parser (no gray-matter dependency)
|
|
50
|
+
const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
51
|
+
const frontmatter = match ? match[1] : "";
|
|
52
|
+
const content = match ? match[2].trim() : raw.trim();
|
|
53
|
+
let id = filename.replace(/\.md$/u, "");
|
|
54
|
+
let title = id;
|
|
55
|
+
let description = "";
|
|
56
|
+
let kind = "visual";
|
|
57
|
+
for (const line of frontmatter.split("\n")) {
|
|
58
|
+
const kv = line.match(/^(\w+):\s*(.+)$/);
|
|
59
|
+
if (kv) {
|
|
60
|
+
const [, key, val] = kv;
|
|
61
|
+
if (key === "id") id = val.trim();
|
|
62
|
+
if (key === "title") title = val.trim();
|
|
63
|
+
if (key === "description") description = val.trim();
|
|
64
|
+
if (key === "kind") kind = val.trim();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (id === "ab-test-analysis") kind = "data-science";
|
|
68
|
+
benchmarks.push({ id, title, description, prompt: content, kind });
|
|
69
|
+
}
|
|
70
|
+
return benchmarks;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Benchmark repo linking ────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
const BENCHMARK_REPO = "https://github.com/eeshansrivastava89/local-llm-visual-benchmark.git";
|
|
76
|
+
|
|
77
|
+
export async function findBenchmarkRepo() {
|
|
78
|
+
const config = await loadConfig();
|
|
79
|
+
if (config.benchmarkRepoPath && existsSync(join(config.benchmarkRepoPath, "benchmarks"))) {
|
|
80
|
+
return config.benchmarkRepoPath;
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function linkBenchmarkRepo(prompt) {
|
|
86
|
+
const existing = await findBenchmarkRepo();
|
|
87
|
+
if (existing) return existing;
|
|
88
|
+
|
|
89
|
+
// Check common locations
|
|
90
|
+
const candidates = [
|
|
91
|
+
join(homedir(), "dev", "local-llm-visual-benchmark"),
|
|
92
|
+
join(homedir(), "projects", "local-llm-visual-benchmark"),
|
|
93
|
+
join(homedir(), "local-llm-visual-benchmark"),
|
|
94
|
+
];
|
|
95
|
+
for (const candidate of candidates) {
|
|
96
|
+
if (existsSync(join(candidate, "benchmarks"))) {
|
|
97
|
+
const config = await loadConfig();
|
|
98
|
+
config.benchmarkRepoPath = candidate;
|
|
99
|
+
await saveConfig(config);
|
|
100
|
+
return candidate;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
console.log(pc.dim("\nThe benchmark gallery needs to be linked to offgrid-ai."));
|
|
105
|
+
console.log(pc.dim("This is the local-llm-visual-benchmark repo that stores prompts and run results.\n"));
|
|
106
|
+
|
|
107
|
+
const choice = await prompt.choice("Link benchmark gallery", [
|
|
108
|
+
{ value: "clone", label: "Clone from GitHub", hint: "git clone into ~/dev" },
|
|
109
|
+
{ value: "manual", label: "Enter path manually", hint: "If you already have it cloned" },
|
|
110
|
+
], "clone");
|
|
111
|
+
|
|
112
|
+
if (choice === "clone") {
|
|
113
|
+
const targetDir = join(homedir(), "dev", "local-llm-visual-benchmark");
|
|
114
|
+
console.log(pc.dim(`\nCloning ${BENCHMARK_REPO}...`));
|
|
115
|
+
try {
|
|
116
|
+
await execFileAsync("git", ["clone", BENCHMARK_REPO, targetDir], { stdio: "pipe" });
|
|
117
|
+
const config = await loadConfig();
|
|
118
|
+
config.benchmarkRepoPath = targetDir;
|
|
119
|
+
await saveConfig(config);
|
|
120
|
+
console.log(pc.green(`✓ Cloned to ${targetDir}`));
|
|
121
|
+
return targetDir;
|
|
122
|
+
} catch (err) {
|
|
123
|
+
console.log(pc.red(`Clone failed: ${err.message}`));
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Manual path
|
|
129
|
+
const path = await prompt.text("Path to local-llm-visual-benchmark", "");
|
|
130
|
+
if (!path) return null;
|
|
131
|
+
const resolved = resolve(path.replace(/^~/, homedir()));
|
|
132
|
+
if (!existsSync(join(resolved, "benchmarks"))) {
|
|
133
|
+
console.log(pc.red(`No benchmarks/ directory found at ${resolved}`));
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
const config = await loadConfig();
|
|
137
|
+
config.benchmarkRepoPath = resolved;
|
|
138
|
+
await saveConfig(config);
|
|
139
|
+
console.log(pc.green(`✓ Linked to ${resolved}`));
|
|
140
|
+
return resolved;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── Prepare a benchmark run ────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
export async function benchmarkFlow() {
|
|
146
|
+
await ensureDirs();
|
|
147
|
+
|
|
148
|
+
const prompt = createPrompt();
|
|
149
|
+
try {
|
|
150
|
+
const repoPath = await linkBenchmarkRepo(prompt);
|
|
151
|
+
if (!repoPath) return;
|
|
152
|
+
|
|
153
|
+
// 1. Pick category
|
|
154
|
+
const kind = await prompt.choice("Benchmark category", [
|
|
155
|
+
{ value: "visual", label: "Visual Benchmark", hint: "HTML/CSS/JS animation benchmarks" },
|
|
156
|
+
{ value: "data-science", label: "Data Science", hint: "Analysis and charting benchmarks" },
|
|
157
|
+
], "visual");
|
|
158
|
+
|
|
159
|
+
// 2. Pick benchmark prompt
|
|
160
|
+
const benchDir = join(repoPath, "benchmarks");
|
|
161
|
+
const benchmarks = (await loadBenchmarks(benchDir)).filter((b) => b.kind === kind);
|
|
162
|
+
if (benchmarks.length === 0) {
|
|
163
|
+
console.log(pc.yellow(`No ${kind} benchmarks found in ${benchDir}`));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const benchmarkId = await prompt.choice("Prompt", benchmarks.map((b) => ({
|
|
167
|
+
value: b.id,
|
|
168
|
+
label: b.title,
|
|
169
|
+
hint: b.description || b.id,
|
|
170
|
+
})), benchmarks[0].id);
|
|
171
|
+
const selectedBenchmark = benchmarks.find((b) => b.id === benchmarkId);
|
|
172
|
+
if (!selectedBenchmark) return;
|
|
173
|
+
|
|
174
|
+
// 3. Pick model source
|
|
175
|
+
const { loadProfiles } = await import("./profiles.mjs");
|
|
176
|
+
const { backendFor } = await import("./backends.mjs");
|
|
177
|
+
|
|
178
|
+
const profiles = await loadProfiles();
|
|
179
|
+
const source = await prompt.choice("Model source", [
|
|
180
|
+
{ value: "profile", label: "Use existing profile", hint: "Pick a saved offgrid-ai profile" },
|
|
181
|
+
{ value: "cloud", label: "Custom / cloud", hint: "Free-form model label for cloud runs" },
|
|
182
|
+
], "profile");
|
|
183
|
+
|
|
184
|
+
let modelId, modelSource, backendLabel;
|
|
185
|
+
|
|
186
|
+
if (source === "profile") {
|
|
187
|
+
if (profiles.length === 0) {
|
|
188
|
+
console.log(pc.yellow("No profiles yet. Run: offgrid-ai models"));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const profileId = await prompt.choice("Profile", profiles.map((p) => ({
|
|
192
|
+
value: p.id,
|
|
193
|
+
label: p.label,
|
|
194
|
+
hint: `${backendFor(p.backend).label} · ${p.modelAlias}`,
|
|
195
|
+
})), profiles[0].id);
|
|
196
|
+
const profile = profiles.find((p) => p.id === profileId);
|
|
197
|
+
if (!profile) return;
|
|
198
|
+
modelId = profile.modelAlias;
|
|
199
|
+
modelSource = profile.providerId === "llama-cpp-mtp" ? "llama-cpp-mtp" : profile.backend === "ollama" ? "ollama" : profile.backend === "omlx" ? "omlx" : "llama-cpp";
|
|
200
|
+
backendLabel = backendFor(profile.backend).label;
|
|
201
|
+
} else {
|
|
202
|
+
backendLabel = await prompt.text("Backend label", "cloud");
|
|
203
|
+
modelId = await prompt.text("Model name", "");
|
|
204
|
+
if (!modelId) { console.log(pc.yellow("Model name is required.")); return; }
|
|
205
|
+
modelSource = "cloud";
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 4. Create the run directory
|
|
209
|
+
const toolPrompt = buildToolPrompt(selectedBenchmark, kind);
|
|
210
|
+
const now = new Date();
|
|
211
|
+
const runId = createRunId(now);
|
|
212
|
+
const modelSlug = slugModelId(modelId);
|
|
213
|
+
const runsDir = join(repoPath, "runs");
|
|
214
|
+
const benchmarkDirectory = join(runsDir, selectedBenchmark.id);
|
|
215
|
+
const modelDirectory = join(benchmarkDirectory, modelSlug);
|
|
216
|
+
const runDirectory = join(modelDirectory, runId);
|
|
217
|
+
|
|
218
|
+
await mkdir(runDirectory, { recursive: true });
|
|
219
|
+
|
|
220
|
+
const isDs = kind === "data-science";
|
|
221
|
+
const metadata = {
|
|
222
|
+
schemaVersion: 1,
|
|
223
|
+
kind,
|
|
224
|
+
runId,
|
|
225
|
+
benchmark: { id: selectedBenchmark.id, title: selectedBenchmark.title, description: selectedBenchmark.description, prompt: selectedBenchmark.prompt },
|
|
226
|
+
model: { id: modelId, slug: modelSlug },
|
|
227
|
+
status: "prepared",
|
|
228
|
+
createdAt: now.toISOString(),
|
|
229
|
+
updatedAt: now.toISOString(),
|
|
230
|
+
preparedAt: now.toISOString(),
|
|
231
|
+
runDirectory,
|
|
232
|
+
assets: isDs
|
|
233
|
+
? { metadata: "metadata.json", prompt: "prompt.md", rawResponse: "response.raw.txt", ds: { notebook: "analysis.ipynb", summary: "summary.json", chartDistribution: "chart-distribution.png", chartTreatmentEffect: "chart-treatment-effect.png", chartCompletionRates: "chart-completion-rates.png" } }
|
|
234
|
+
: { metadata: "metadata.json", prompt: "prompt.md", html: "index.html", preview: "preview.png", video: "preview.webm", rawResponse: "response.raw.txt" },
|
|
235
|
+
runner: {
|
|
236
|
+
mode: source === "profile" ? "external" : "manual",
|
|
237
|
+
...(modelSource ? { modelSource } : {}),
|
|
238
|
+
...(backendLabel ? { backendLabel } : {}),
|
|
239
|
+
model: modelId,
|
|
240
|
+
retries: 0,
|
|
241
|
+
tokenMetrics: { reported: false },
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// Clean up the baseUrl field — only include if we have a real one
|
|
246
|
+
if (!metadata.runner.baseUrl) delete metadata.runner.baseUrl;
|
|
247
|
+
|
|
248
|
+
await writeFile(join(runDirectory, "metadata.json"), JSON.stringify(metadata, null, 2) + "\n", "utf8");
|
|
249
|
+
await writeFile(join(runDirectory, "prompt.md"), toolPrompt + "\n", "utf8");
|
|
250
|
+
|
|
251
|
+
// 5. Print result
|
|
252
|
+
console.log("");
|
|
253
|
+
console.log(pc.green("✓ Run slot prepared"));
|
|
254
|
+
console.log(renderSection("Run", renderRows([
|
|
255
|
+
["Directory", pc.cyan(runDirectory)],
|
|
256
|
+
["Benchmark", selectedBenchmark.title],
|
|
257
|
+
["Kind", kind],
|
|
258
|
+
["Model", pc.bold(modelId)],
|
|
259
|
+
["Source", backendLabel || modelSource],
|
|
260
|
+
])));
|
|
261
|
+
|
|
262
|
+
console.log("");
|
|
263
|
+
console.log(pc.bold("Next steps"));
|
|
264
|
+
if (source === "profile") {
|
|
265
|
+
console.log(` 1. ${pc.cyan(`cd ${runDirectory}`)}`);
|
|
266
|
+
console.log(` 2. ${pc.cyan("offgrid-ai models")} → select the model → Run`);
|
|
267
|
+
console.log(` 3. In Pi, paste the prompt from ${pc.cyan("prompt.md")}`);
|
|
268
|
+
console.log(` 4. View results: ${pc.cyan(`cd ${repoPath} && npm run dev`)}`);
|
|
269
|
+
} else {
|
|
270
|
+
console.log(` 1. ${pc.cyan(`cd ${runDirectory}`)}`);
|
|
271
|
+
console.log(` 2. Copy the prompt from ${pc.cyan("prompt.md")} into your tool of choice`);
|
|
272
|
+
console.log(` 3. View results: ${pc.cyan(`cd ${repoPath} && npm run dev`)}`);
|
|
273
|
+
}
|
|
274
|
+
} finally {
|
|
275
|
+
prompt.close();
|
|
276
|
+
}
|
|
277
|
+
}
|
package/src/cli.mjs
CHANGED
|
@@ -59,6 +59,7 @@ export async function run(argv) {
|
|
|
59
59
|
if (command === "run") return runCommand(argv.slice(1));
|
|
60
60
|
if (command === "status") return statusCommand();
|
|
61
61
|
if (command === "stop") return stopCommand(argv.slice(1));
|
|
62
|
+
if (command === "benchmark") return benchmarkCommand();
|
|
62
63
|
if (command === "uninstall" || command === "--uninstall") return uninstallCommand(argv.slice(1));
|
|
63
64
|
if (command === "--verbose") return mainFlow(); // verbose flag handled inside onboardFlow
|
|
64
65
|
|
|
@@ -181,41 +182,37 @@ async function modelCommandCenter(initialCatalog) {
|
|
|
181
182
|
return;
|
|
182
183
|
}
|
|
183
184
|
|
|
185
|
+
const catalog = initialCatalog.newModels ? initialCatalog : await loadModelCatalog();
|
|
186
|
+
const normalized = normalizeCatalog(catalog);
|
|
187
|
+
const allItems = buildCatalogItems(normalized);
|
|
188
|
+
if (allItems.length === 0) { console.log(pc.dim("No models found.")); return; }
|
|
189
|
+
const runningProfilesNow = [];
|
|
190
|
+
for (const profile of normalized.profiles) {
|
|
191
|
+
if (await isProfileRunning(profile)) runningProfilesNow.push(profile);
|
|
192
|
+
}
|
|
193
|
+
const fileMissingCount = normalized.profiles.filter((p) => isProfileFileMissing(p)).length;
|
|
194
|
+
const summaryBorder = fileMissingCount > 0 ? pc.red : normalized.newModels.length > 0 ? pc.yellow : pc.dim;
|
|
195
|
+
console.log("\n" + renderCard("Your local AI workspace", renderRows([
|
|
196
|
+
["Setups", `${normalized.profiles.length} saved`],
|
|
197
|
+
["Need setup", normalized.newModels.length > 0 ? pc.yellow(`${normalized.newModels.length} model${normalized.newModels.length === 1 ? "" : "s"}`) : pc.dim("none")],
|
|
198
|
+
["Running", runningProfilesNow.length > 0 ? pc.green(String(runningProfilesNow.length)) : pc.dim("none")],
|
|
199
|
+
["File missing", fileMissingCount > 0 ? pc.red(`${fileMissingCount} setup${fileMissingCount === 1 ? "" : "s"}`) : pc.dim("none")],
|
|
200
|
+
]), { formatBorder: summaryBorder }));
|
|
201
|
+
|
|
202
|
+
printModelCards(allItems, { runningProfilesNow, drafters: normalized.drafters });
|
|
203
|
+
|
|
184
204
|
const prompt = createPrompt();
|
|
185
205
|
try {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const summaryBorder = fileMissingCount > 0 ? pc.red : normalized.newModels.length > 0 ? pc.yellow : pc.dim;
|
|
197
|
-
console.log("\n" + renderCard("Your local AI workspace", renderRows([
|
|
198
|
-
["Setups", `${normalized.profiles.length} saved`],
|
|
199
|
-
["Need setup", normalized.newModels.length > 0 ? pc.yellow(`${normalized.newModels.length} model${normalized.newModels.length === 1 ? "" : "s"}`) : pc.dim("none")],
|
|
200
|
-
["Running", runningProfilesNow.length > 0 ? pc.green(String(runningProfilesNow.length)) : pc.dim("none")],
|
|
201
|
-
["File missing", fileMissingCount > 0 ? pc.red(`${fileMissingCount} setup${fileMissingCount === 1 ? "" : "s"}`) : pc.dim("none")],
|
|
202
|
-
]), { formatBorder: summaryBorder }));
|
|
203
|
-
|
|
204
|
-
// Show model cards for all items
|
|
205
|
-
printModelCards(allItems, { runningProfilesNow, drafters: normalized.drafters });
|
|
206
|
-
|
|
207
|
-
const options = allItems.map((item) => modelSelectOption(item, { runningProfilesNow, drafters: normalized.drafters }));
|
|
208
|
-
options.push({ value: "__exit__", label: pc.dim("Exit"), hint: "Close offgrid-ai" });
|
|
209
|
-
const selected = await prompt.choice("Select a model", options);
|
|
210
|
-
if (!selected || selected === "__exit__") break;
|
|
211
|
-
const item = allItems.find((i) => itemKey(i) === selected);
|
|
212
|
-
if (!item) break;
|
|
213
|
-
|
|
214
|
-
const actions = actionsForItem(item);
|
|
215
|
-
const action = await prompt.choice(item.label, actions, actions[0].value);
|
|
216
|
-
if (!action) continue;
|
|
217
|
-
await performAction(prompt, action, item);
|
|
218
|
-
}
|
|
206
|
+
const options = allItems.map((item) => modelSelectOption(item, { runningProfilesNow, drafters: normalized.drafters }));
|
|
207
|
+
const selected = await prompt.choice("Select a model", options);
|
|
208
|
+
if (!selected) return;
|
|
209
|
+
const item = allItems.find((i) => itemKey(i) === selected);
|
|
210
|
+
if (!item) return;
|
|
211
|
+
|
|
212
|
+
const actions = actionsForItem(item);
|
|
213
|
+
const action = await prompt.choice(item.label, actions, actions[0].value);
|
|
214
|
+
if (!action) return;
|
|
215
|
+
await performAction(prompt, action, item);
|
|
219
216
|
} finally {
|
|
220
217
|
prompt.close();
|
|
221
218
|
}
|
|
@@ -315,6 +312,11 @@ function actionsForItem(item) {
|
|
|
315
312
|
{ value: "reconfigure", label: "Reconfigure", hint: "Change context, MTP, settings" },
|
|
316
313
|
{ value: "inspect", label: "Details", hint: "Paths, ports, flags" },
|
|
317
314
|
];
|
|
315
|
+
// Benchmark is offered if repo is linked and model is local
|
|
316
|
+
const backend = backendFor(item.profile.backend);
|
|
317
|
+
if (backend.type === "local-server" || backend.type === "managed-server") {
|
|
318
|
+
actions.push({ value: "benchmark", label: "Benchmark", hint: "Prepare a benchmark run" });
|
|
319
|
+
}
|
|
318
320
|
if (!item.fileMissing) {
|
|
319
321
|
actions.push({ value: "remove", label: "Remove", hint: "Delete this setup" });
|
|
320
322
|
}
|
|
@@ -416,6 +418,10 @@ async function performAction(prompt, action, item) {
|
|
|
416
418
|
if (item.type === "managed") return printManagedModelDetails(item.model, BACKENDS[item.backendId]);
|
|
417
419
|
return printGgufModelDetails(item.model, item.drafter);
|
|
418
420
|
}
|
|
421
|
+
if (action === "benchmark") {
|
|
422
|
+
const { benchmarkFlow } = await import("./benchmark.mjs");
|
|
423
|
+
return await benchmarkFlow();
|
|
424
|
+
}
|
|
419
425
|
if (action === "run") {
|
|
420
426
|
if (item.type === "profile") return await runProfile(await readProfile(item.profile.id));
|
|
421
427
|
// Shouldn't reach here for new/managed, but handle gracefully
|
|
@@ -714,6 +720,11 @@ async function removeProfileInteractive(id) {
|
|
|
714
720
|
|
|
715
721
|
// ── Status ──────────────────────────────────────────────────────────────────
|
|
716
722
|
|
|
723
|
+
async function benchmarkCommand() {
|
|
724
|
+
const { benchmarkFlow } = await import("./benchmark.mjs");
|
|
725
|
+
return await benchmarkFlow();
|
|
726
|
+
}
|
|
727
|
+
|
|
717
728
|
async function statusCommand() {
|
|
718
729
|
await ensureDirs();
|
|
719
730
|
const profiles = await loadProfiles();
|
|
@@ -1227,9 +1238,10 @@ function printHelp() {
|
|
|
1227
1238
|
["Start", pc.bold("offgrid-ai")],
|
|
1228
1239
|
["Status", "offgrid-ai status"],
|
|
1229
1240
|
["Stop", "offgrid-ai stop"],
|
|
1241
|
+
["Benchmark", "offgrid-ai benchmark"],
|
|
1230
1242
|
["Uninstall", "offgrid-ai uninstall"],
|
|
1231
1243
|
["Version", "offgrid-ai version"],
|
|
1232
1244
|
]), { formatBorder: pc.cyan }));
|
|
1233
|
-
console.log("\n" + renderCard("How it works", "Run offgrid-ai, choose a local model, and start chatting in Pi.\n\nFirst run walks you through missing tools. After that, offgrid-ai remembers your model setup.", { formatBorder: pc.magenta }));
|
|
1245
|
+
console.log("\n" + renderCard("How it works", "Run offgrid-ai, choose a local model, and start chatting in Pi.\n\nFirst run walks you through missing tools. After that, offgrid-ai remembers your model setup.\n\nFor benchmarks, run offgrid-ai benchmark to prepare a visual or data-science benchmark run.", { formatBorder: pc.magenta }));
|
|
1234
1246
|
console.log("\n" + pc.dim("Tip: use --verbose only when you want detailed install output."));
|
|
1235
1247
|
}
|