offgrid-ai 0.6.7 → 0.6.10
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/commands/models.mjs +22 -20
- package/src/model-presenters.mjs +92 -68
- package/src/profile-setup.mjs +1 -1
package/package.json
CHANGED
package/src/commands/models.mjs
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { ensureDirs } from "../config.mjs";
|
|
2
2
|
import { backendFor, BACKENDS } from "../backends.mjs";
|
|
3
|
-
import { createProfileFromModel, readProfile, saveProfile, deleteProfile } from "../profiles.mjs";
|
|
3
|
+
import { createProfileFromModel, readProfile, saveProfile, deleteProfile, profileJsonPath } from "../profiles.mjs";
|
|
4
4
|
import { isProfileRunning, stopProfile } from "../process.mjs";
|
|
5
5
|
import { syncPiConfig, removeFromPiConfig } from "../harness-pi.mjs";
|
|
6
6
|
import { configureLocalProfile } from "../profile-setup.mjs";
|
|
7
|
-
import { pc,
|
|
7
|
+
import { pc, startInteractive, createPrompt } from "../ui.mjs";
|
|
8
8
|
import { buildCatalogItems, createManagedProfile, itemKey, loadModelCatalog, normalizeCatalog } from "../model-catalog.mjs";
|
|
9
|
-
import {
|
|
10
|
-
import { modelSelectOption, printGgufModelDetails, printManagedModelDetails, printModelCards, printProfileDetails } from "../model-presenters.mjs";
|
|
9
|
+
import { modelSelectOption, modelNameWidth, printGgufModelDetails, printManagedModelDetails, printWorkspaceHeader, printBenchmarkLine, printProfileDetails } from "../model-presenters.mjs";
|
|
11
10
|
import { runProfile } from "./run.mjs";
|
|
12
11
|
|
|
13
12
|
export async function modelsCommand(argv) {
|
|
@@ -42,12 +41,14 @@ export async function modelCommandCenter(initialCatalog) {
|
|
|
42
41
|
for (const profile of normalized.profiles) {
|
|
43
42
|
if (await isProfileRunning(profile)) runningProfilesNow.push(profile);
|
|
44
43
|
}
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
printWorkspaceHeader(normalized, runningProfilesNow);
|
|
45
|
+
await printBenchmarkLine();
|
|
46
|
+
|
|
47
|
+
const nameWidth = modelNameWidth(allItems);
|
|
47
48
|
|
|
48
49
|
const prompt = createPrompt();
|
|
49
50
|
try {
|
|
50
|
-
const selected = await prompt.choice("Select a model", allItems.map((item) => modelSelectOption(item, { runningProfilesNow,
|
|
51
|
+
const selected = await prompt.choice("Select a model", allItems.map((item) => modelSelectOption(item, { runningProfilesNow, nameWidth })));
|
|
51
52
|
if (!selected) return;
|
|
52
53
|
const item = allItems.find((candidate) => itemKey(candidate) === selected);
|
|
53
54
|
if (!item) return;
|
|
@@ -61,16 +62,7 @@ export async function modelCommandCenter(initialCatalog) {
|
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
|
|
65
|
-
const fileMissingCount = normalized.profiles.filter((profile) => isProfileFileMissing(profile)).length;
|
|
66
|
-
const summaryBorder = fileMissingCount > 0 ? pc.red : normalized.newModels.length > 0 ? pc.yellow : pc.dim;
|
|
67
|
-
console.log("\n" + renderCard("Your local AI workspace", renderRows([
|
|
68
|
-
["Setups", `${normalized.profiles.length} saved`],
|
|
69
|
-
["Need setup", normalized.newModels.length > 0 ? pc.yellow(`${normalized.newModels.length} model${normalized.newModels.length === 1 ? "" : "s"}`) : pc.dim("none")],
|
|
70
|
-
["Running", runningProfilesNow.length > 0 ? pc.green(String(runningProfilesNow.length)) : pc.dim("none")],
|
|
71
|
-
["File missing", fileMissingCount > 0 ? pc.red(`${fileMissingCount} setup${fileMissingCount === 1 ? "" : "s"}`) : pc.dim("none")],
|
|
72
|
-
]), { formatBorder: summaryBorder }));
|
|
73
|
-
}
|
|
65
|
+
|
|
74
66
|
|
|
75
67
|
function actionsForItem(item) {
|
|
76
68
|
if (item.type === "profile") {
|
|
@@ -122,26 +114,36 @@ async function runItem(prompt, item) {
|
|
|
122
114
|
if (!configured) return;
|
|
123
115
|
await saveProfile(configured);
|
|
124
116
|
await syncPiConfig(configured);
|
|
117
|
+
printProfileSaved(configured.id);
|
|
125
118
|
return await runProfile(configured);
|
|
126
119
|
}
|
|
127
120
|
|
|
121
|
+
function printProfileSaved(id) {
|
|
122
|
+
console.log(pc.dim(` Profile: ${profileJsonPath(id)}`));
|
|
123
|
+
}
|
|
124
|
+
|
|
128
125
|
async function setupItem(prompt, item, action) {
|
|
129
126
|
if (item.type === "profile") {
|
|
130
127
|
const configured = await configureLocalProfile(prompt, await readProfile(item.profile.id));
|
|
131
128
|
if (!configured) return;
|
|
132
129
|
await saveProfile(configured, { writeCommand: true });
|
|
133
|
-
|
|
130
|
+
await syncPiConfig(configured);
|
|
131
|
+
printProfileSaved(configured.id);
|
|
132
|
+
return;
|
|
134
133
|
}
|
|
135
134
|
if (item.type === "managed") {
|
|
136
135
|
const profile = createManagedProfile(item.model, item.backendId);
|
|
137
136
|
await saveProfile(profile);
|
|
138
|
-
|
|
137
|
+
await syncPiConfig(profile);
|
|
138
|
+
printProfileSaved(profile.id);
|
|
139
|
+
return;
|
|
139
140
|
}
|
|
140
141
|
const profile = await createProfileFromModel(item.model, null, item.drafter?.path);
|
|
141
142
|
const configured = await configureLocalProfile(prompt, profile);
|
|
142
143
|
if (!configured) return;
|
|
143
144
|
await saveProfile(configured, { writeCommand: action === "reconfigure" });
|
|
144
|
-
|
|
145
|
+
await syncPiConfig(configured);
|
|
146
|
+
printProfileSaved(configured.id);
|
|
145
147
|
}
|
|
146
148
|
|
|
147
149
|
async function removeProfileInteractive(id) {
|
package/src/model-presenters.mjs
CHANGED
|
@@ -2,30 +2,34 @@ import { existsSync, statSync } from "node:fs";
|
|
|
2
2
|
import { BACKENDS, backendFor } from "./backends.mjs";
|
|
3
3
|
import { readCommandArgv } from "./profiles.mjs";
|
|
4
4
|
import { isProfileRunning } from "./process.mjs";
|
|
5
|
-
import { detectCapabilities } from "./autodetect.mjs";
|
|
6
5
|
import { buildPrettyCommand } from "./command.mjs";
|
|
7
|
-
import { pc, formatBytes, renderRows, renderSection
|
|
8
|
-
import { capabilitySummary, ggufDetailParts,
|
|
6
|
+
import { pc, formatBytes, renderRows, renderSection } from "./ui.mjs";
|
|
7
|
+
import { capabilitySummary, ggufDetailParts, isProfileFileMissing, profileDetailParts } from "./model-summary.mjs";
|
|
9
8
|
import { itemKey } from "./model-catalog.mjs";
|
|
9
|
+
import { DATA_DIR } from "./config.mjs";
|
|
10
|
+
import { findBenchmarkRepo } from "./benchmark.mjs";
|
|
10
11
|
|
|
11
12
|
const OPTION_SEPARATOR = pc.dim(" │ ");
|
|
12
|
-
const OPTION_STATUS_WIDTH =
|
|
13
|
+
const OPTION_STATUS_WIDTH = 10;
|
|
13
14
|
const OPTION_SOURCE_WIDTH = 14;
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
const { stripVTControlCharacters } = await import("node:util");
|
|
17
|
+
|
|
18
|
+
function optionPad(text, color, width) {
|
|
19
|
+
const visible = stripVTControlCharacters(String(text)).length;
|
|
20
|
+
const padding = Math.max(1, width - visible);
|
|
21
|
+
return (color ? color(String(text)) : String(text)) + " ".repeat(padding);
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
function optionStatusTag(kind) {
|
|
21
25
|
const statuses = {
|
|
22
26
|
running: ["RUNNING", pc.green],
|
|
23
27
|
ready: ["READY", pc.green],
|
|
24
|
-
missing: ["
|
|
25
|
-
setup: ["
|
|
28
|
+
missing: ["MISSING", pc.red],
|
|
29
|
+
setup: ["SETUP", pc.yellow],
|
|
26
30
|
};
|
|
27
31
|
const [text, color] = statuses[kind] ?? [kind, pc.dim];
|
|
28
|
-
return
|
|
32
|
+
return optionPad(text, color, OPTION_STATUS_WIDTH);
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
function optionSourceTag(sourceId, label) {
|
|
@@ -36,14 +40,45 @@ function optionSourceTag(sourceId, label) {
|
|
|
36
40
|
omlx: pc.magenta,
|
|
37
41
|
gguf: pc.cyan,
|
|
38
42
|
};
|
|
39
|
-
return
|
|
43
|
+
return optionPad(label, colors[sourceId] ?? pc.dim, OPTION_SOURCE_WIDTH);
|
|
40
44
|
}
|
|
41
45
|
|
|
42
|
-
function
|
|
43
|
-
|
|
46
|
+
function optionCtxLabel(item) {
|
|
47
|
+
if (item.type === "profile" && item.profile.flags?.ctxSize) {
|
|
48
|
+
return `${(item.profile.flags.ctxSize / 1000).toFixed(0)}k`;
|
|
49
|
+
}
|
|
50
|
+
return "—";
|
|
44
51
|
}
|
|
45
52
|
|
|
46
|
-
|
|
53
|
+
function optionSizeLabel(item) {
|
|
54
|
+
if (item.type === "profile") {
|
|
55
|
+
if (item.fileMissing) return "—";
|
|
56
|
+
if (item.profile.modelPath && existsSync(item.profile.modelPath)) {
|
|
57
|
+
return formatBytes(statSync(item.profile.modelPath).size);
|
|
58
|
+
}
|
|
59
|
+
return "—";
|
|
60
|
+
}
|
|
61
|
+
if (item.type === "new") {
|
|
62
|
+
return formatBytes(item.model.sizeBytes);
|
|
63
|
+
}
|
|
64
|
+
// managed
|
|
65
|
+
if (item.model.quant) return item.model.quant;
|
|
66
|
+
if (item.model.sizeBytes) return formatBytes(item.model.sizeBytes);
|
|
67
|
+
return "—";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function modelNameWidth(items) {
|
|
71
|
+
const maxName = Math.max(...items.map((item) =>
|
|
72
|
+
stripVTControlCharacters(item.label ?? item.model?.label ?? item.profile?.label ?? "").length,
|
|
73
|
+
));
|
|
74
|
+
return Math.max(20, maxName + 2);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function optionLabel({ status, source, name, ctx, size, nameWidth }) {
|
|
78
|
+
return [status, source, pc.bold(optionPad(name, null, nameWidth)), ctx, pc.dim(size)].filter(Boolean).join(OPTION_SEPARATOR);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function modelSelectOption(item, { runningProfilesNow, nameWidth }) {
|
|
47
82
|
if (item.type === "profile") {
|
|
48
83
|
const backend = backendFor(item.profile.backend);
|
|
49
84
|
const running = runningProfilesNow.some((profile) => profile.id === item.profile.id);
|
|
@@ -53,73 +88,62 @@ export function modelSelectOption(item, { runningProfilesNow }) {
|
|
|
53
88
|
status: optionStatusTag(item.fileMissing ? "missing" : running ? "running" : "ready"),
|
|
54
89
|
source: optionSourceTag(item.profile.backend, backend.label),
|
|
55
90
|
name: item.profile.label,
|
|
91
|
+
nameWidth,
|
|
92
|
+
ctx: optionCtxLabel(item),
|
|
93
|
+
size: optionSizeLabel(item),
|
|
56
94
|
}),
|
|
57
95
|
};
|
|
58
96
|
}
|
|
59
97
|
if (item.type === "new") {
|
|
60
98
|
return {
|
|
61
99
|
value: itemKey(item),
|
|
62
|
-
label: optionLabel({
|
|
100
|
+
label: optionLabel({
|
|
101
|
+
status: optionStatusTag("setup"),
|
|
102
|
+
source: optionSourceTag("gguf", "GGUF file"),
|
|
103
|
+
name: item.model.label,
|
|
104
|
+
nameWidth,
|
|
105
|
+
ctx: optionCtxLabel(item),
|
|
106
|
+
size: optionSizeLabel(item),
|
|
107
|
+
}),
|
|
63
108
|
};
|
|
64
109
|
}
|
|
65
110
|
const backend = BACKENDS[item.backendId];
|
|
66
111
|
return {
|
|
67
112
|
value: itemKey(item),
|
|
68
|
-
label: optionLabel({
|
|
113
|
+
label: optionLabel({
|
|
114
|
+
status: optionStatusTag("setup"),
|
|
115
|
+
source: optionSourceTag(item.backendId, backend.label),
|
|
116
|
+
name: item.model.label,
|
|
117
|
+
nameWidth,
|
|
118
|
+
ctx: optionCtxLabel(item),
|
|
119
|
+
size: optionSizeLabel(item),
|
|
120
|
+
}),
|
|
69
121
|
};
|
|
70
122
|
}
|
|
71
123
|
|
|
72
|
-
export function
|
|
73
|
-
const profiles =
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const ctxLabel = profile.flags?.ctxSize ? `${(profile.flags.ctxSize / 1000).toFixed(0)}k ctx` : null;
|
|
89
|
-
if (ctxLabel) detailParts.push(ctxLabel);
|
|
90
|
-
console.log(renderCard(profile.label, renderRows([
|
|
91
|
-
["Status", status],
|
|
92
|
-
["Details", detailParts.join(pc.dim(" · "))],
|
|
93
|
-
["Runs with", backend.label],
|
|
94
|
-
]), { formatBorder: border }));
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (newModels.length > 0) {
|
|
99
|
-
console.log("\n" + pc.bold("Needs setup"));
|
|
100
|
-
for (const item of newModels) {
|
|
101
|
-
const caps = detectCapabilities(item.model.path, item.model.mmprojPath);
|
|
102
|
-
const detailParts = [capabilitySummary(caps)];
|
|
103
|
-
const mtpLabel = ggufMtpLabel(item.model, item.drafter);
|
|
104
|
-
if (mtpLabel) detailParts.push(mtpLabel);
|
|
105
|
-
detailParts.push(formatBytes(item.model.sizeBytes));
|
|
106
|
-
console.log(renderCard(item.model.label, renderRows([
|
|
107
|
-
["Status", pc.yellow("Needs setup")],
|
|
108
|
-
["Details", detailParts.join(pc.dim(" · "))],
|
|
109
|
-
]), { formatBorder: pc.yellow }));
|
|
110
|
-
}
|
|
111
|
-
}
|
|
124
|
+
export function printWorkspaceHeader(normalized, runningProfilesNow) {
|
|
125
|
+
const profiles = normalized.profiles;
|
|
126
|
+
const readyCount = profiles.filter((p) => !isProfileFileMissing(p) && !runningProfilesNow.some((r) => r.id === p.id)).length;
|
|
127
|
+
const runningCount = runningProfilesNow.length;
|
|
128
|
+
const missingCount = profiles.filter((p) => isProfileFileMissing(p)).length;
|
|
129
|
+
const setupCount = normalized.newModels.length + normalized.managedItems.length;
|
|
130
|
+
|
|
131
|
+
const countParts = [];
|
|
132
|
+
if (runningCount > 0) countParts.push(`${runningCount} running`);
|
|
133
|
+
if (readyCount > 0) countParts.push(pc.green(`${readyCount} model${readyCount === 1 ? "" : "s"} ready`));
|
|
134
|
+
if (missingCount > 0) countParts.push(pc.red(`${missingCount} model${missingCount === 1 ? "" : "s"} missing`));
|
|
135
|
+
if (setupCount > 0) countParts.push(pc.yellow(`${setupCount} model${setupCount === 1 ? "" : "s"} need${setupCount === 1 ? "s" : ""} setup`));
|
|
136
|
+
|
|
137
|
+
console.log(pc.dim(` ${countParts.join(" · ")}`));
|
|
138
|
+
console.log(pc.dim(` Profiles: ${DATA_DIR}`));
|
|
139
|
+
}
|
|
112
140
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
console.log(renderCard(item.model.label, renderRows([
|
|
120
|
-
["Details", [item.model.id, item.model.quant].filter(Boolean).join(pc.dim(" · "))],
|
|
121
|
-
]), { formatBorder: pc.dim }));
|
|
122
|
-
}
|
|
141
|
+
export async function printBenchmarkLine() {
|
|
142
|
+
const repoPath = await findBenchmarkRepo();
|
|
143
|
+
if (repoPath) {
|
|
144
|
+
console.log(pc.green(" ✓") + " local-llm-visual-benchmark linked");
|
|
145
|
+
} else {
|
|
146
|
+
console.log(pc.yellow(" ○") + " to run benchmarks, pair with " + pc.cyan("local-llm-visual-benchmark"));
|
|
123
147
|
}
|
|
124
148
|
}
|
|
125
149
|
|
|
@@ -163,7 +187,7 @@ export async function printProfileDetails(profile) {
|
|
|
163
187
|
|
|
164
188
|
export function printGgufModelDetails(model, drafter) {
|
|
165
189
|
const { caps, parts } = ggufDetailParts(model, drafter);
|
|
166
|
-
parts.push(formatBytes(model.sizeBytes));
|
|
190
|
+
parts.push(formatBytes(model.model.sizeBytes));
|
|
167
191
|
console.log("\n" + renderSection("Downloaded model", renderRows([
|
|
168
192
|
["Name", pc.bold(model.label)],
|
|
169
193
|
["Status", pc.yellow("Needs one-time setup")],
|
|
@@ -187,4 +211,4 @@ export function printManagedModelDetails(model, backend) {
|
|
|
187
211
|
["Quant", model.quant ?? "unknown"],
|
|
188
212
|
["Family", model.family ?? "unknown"],
|
|
189
213
|
])));
|
|
190
|
-
}
|
|
214
|
+
}
|
package/src/profile-setup.mjs
CHANGED
|
@@ -64,7 +64,7 @@ export async function configureLocalProfile(prompt, profile) {
|
|
|
64
64
|
["Sampling", samplingSummary(profile.flags)],
|
|
65
65
|
])));
|
|
66
66
|
console.log(pc.dim("Larger context windows use more memory. KV cache precision controls memory used by attention history."));
|
|
67
|
-
console.log(pc.dim("Sampling defaults are shown for transparency; you can edit
|
|
67
|
+
console.log(pc.dim("Sampling defaults are shown for transparency; you can edit the profile later if needed.\n"));
|
|
68
68
|
|
|
69
69
|
if (caps.mtp) {
|
|
70
70
|
const drafterInfo = configured.drafterPath ? `\n Drafter: ${configured.drafterPath}` : "";
|