offgrid-ai 0.6.7 → 0.6.9
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 +107 -63
- 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, printGgufModelDetails, printManagedModelDetails, printWorkspaceHeader, printBenchmarkLine, printTableHeader, printTableFooter, printProfileDetails } from "../model-presenters.mjs";
|
|
11
10
|
import { runProfile } from "./run.mjs";
|
|
12
11
|
|
|
13
12
|
export async function modelsCommand(argv) {
|
|
@@ -42,15 +41,17 @@ 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
|
+
printTableHeader();
|
|
47
47
|
|
|
48
48
|
const prompt = createPrompt();
|
|
49
49
|
try {
|
|
50
|
-
const selected = await prompt.choice("Select a model", allItems.map((item) => modelSelectOption(item, { runningProfilesNow
|
|
50
|
+
const selected = await prompt.choice("Select a model", allItems.map((item) => modelSelectOption(item, { runningProfilesNow })));
|
|
51
51
|
if (!selected) return;
|
|
52
52
|
const item = allItems.find((candidate) => itemKey(candidate) === selected);
|
|
53
53
|
if (!item) return;
|
|
54
|
+
printTableFooter();
|
|
54
55
|
|
|
55
56
|
const actions = actionsForItem(item);
|
|
56
57
|
const action = await prompt.choice(item.label, actions, actions[0].value);
|
|
@@ -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,36 @@ 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;
|
|
15
|
+
const OPTION_MODEL_WIDTH = 26;
|
|
16
|
+
const OPTION_CTX_WIDTH = 5;
|
|
14
17
|
|
|
15
|
-
|
|
18
|
+
const { stripVTControlCharacters } = await import("node:util");
|
|
19
|
+
|
|
20
|
+
function optionPad(text, color, width) {
|
|
21
|
+
const visible = stripVTControlCharacters(String(text)).length;
|
|
16
22
|
const padded = String(text).padEnd(width);
|
|
17
|
-
return color ? color(padded) : padded;
|
|
23
|
+
return color ? color(padded.slice(0, padded.length - Math.max(0, visible - width))) : padded;
|
|
18
24
|
}
|
|
19
25
|
|
|
20
26
|
function optionStatusTag(kind) {
|
|
21
27
|
const statuses = {
|
|
22
28
|
running: ["RUNNING", pc.green],
|
|
23
29
|
ready: ["READY", pc.green],
|
|
24
|
-
missing: ["
|
|
25
|
-
setup: ["
|
|
30
|
+
missing: ["MISSING", pc.red],
|
|
31
|
+
setup: ["SETUP", pc.yellow],
|
|
26
32
|
};
|
|
27
33
|
const [text, color] = statuses[kind] ?? [kind, pc.dim];
|
|
28
|
-
return
|
|
34
|
+
return optionPad(text, color, OPTION_STATUS_WIDTH);
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
function optionSourceTag(sourceId, label) {
|
|
@@ -36,11 +42,35 @@ function optionSourceTag(sourceId, label) {
|
|
|
36
42
|
omlx: pc.magenta,
|
|
37
43
|
gguf: pc.cyan,
|
|
38
44
|
};
|
|
39
|
-
return
|
|
45
|
+
return optionPad(label, colors[sourceId] ?? pc.dim, OPTION_SOURCE_WIDTH);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function optionCtxLabel(item) {
|
|
49
|
+
if (item.type === "profile" && item.profile.flags?.ctxSize) {
|
|
50
|
+
return `${(item.profile.flags.ctxSize / 1000).toFixed(0)}k`;
|
|
51
|
+
}
|
|
52
|
+
return "—";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function optionSizeLabel(item) {
|
|
56
|
+
if (item.type === "profile") {
|
|
57
|
+
if (item.fileMissing) return "—";
|
|
58
|
+
if (item.profile.modelPath && existsSync(item.profile.modelPath)) {
|
|
59
|
+
return formatBytes(statSync(item.profile.modelPath).size);
|
|
60
|
+
}
|
|
61
|
+
return "—";
|
|
62
|
+
}
|
|
63
|
+
if (item.type === "new") {
|
|
64
|
+
return formatBytes(item.model.sizeBytes);
|
|
65
|
+
}
|
|
66
|
+
// managed
|
|
67
|
+
if (item.model.quant) return item.model.quant;
|
|
68
|
+
if (item.model.sizeBytes) return formatBytes(item.model.sizeBytes);
|
|
69
|
+
return "—";
|
|
40
70
|
}
|
|
41
71
|
|
|
42
|
-
function optionLabel({ status, source, name,
|
|
43
|
-
return [status, source, pc.bold(name),
|
|
72
|
+
function optionLabel({ status, source, name, ctx, size }) {
|
|
73
|
+
return [status, source, pc.bold(optionPad(name, null, OPTION_MODEL_WIDTH)), ctx, pc.dim(size)].filter(Boolean).join(OPTION_SEPARATOR);
|
|
44
74
|
}
|
|
45
75
|
|
|
46
76
|
export function modelSelectOption(item, { runningProfilesNow }) {
|
|
@@ -53,74 +83,88 @@ export function modelSelectOption(item, { runningProfilesNow }) {
|
|
|
53
83
|
status: optionStatusTag(item.fileMissing ? "missing" : running ? "running" : "ready"),
|
|
54
84
|
source: optionSourceTag(item.profile.backend, backend.label),
|
|
55
85
|
name: item.profile.label,
|
|
86
|
+
ctx: optionCtxLabel(item),
|
|
87
|
+
size: optionSizeLabel(item),
|
|
56
88
|
}),
|
|
57
89
|
};
|
|
58
90
|
}
|
|
59
91
|
if (item.type === "new") {
|
|
60
92
|
return {
|
|
61
93
|
value: itemKey(item),
|
|
62
|
-
label: optionLabel({
|
|
94
|
+
label: optionLabel({
|
|
95
|
+
status: optionStatusTag("setup"),
|
|
96
|
+
source: optionSourceTag("gguf", "GGUF file"),
|
|
97
|
+
name: item.model.label,
|
|
98
|
+
ctx: optionCtxLabel(item),
|
|
99
|
+
size: optionSizeLabel(item),
|
|
100
|
+
}),
|
|
63
101
|
};
|
|
64
102
|
}
|
|
65
103
|
const backend = BACKENDS[item.backendId];
|
|
66
104
|
return {
|
|
67
105
|
value: itemKey(item),
|
|
68
|
-
label: optionLabel({
|
|
106
|
+
label: optionLabel({
|
|
107
|
+
status: optionStatusTag("setup"),
|
|
108
|
+
source: optionSourceTag(item.backendId, backend.label),
|
|
109
|
+
name: item.model.label,
|
|
110
|
+
ctx: optionCtxLabel(item),
|
|
111
|
+
size: optionSizeLabel(item),
|
|
112
|
+
}),
|
|
69
113
|
};
|
|
70
114
|
}
|
|
71
115
|
|
|
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
|
-
}
|
|
116
|
+
export function printWorkspaceHeader(normalized, runningProfilesNow) {
|
|
117
|
+
const profiles = normalized.profiles;
|
|
118
|
+
const readyCount = profiles.filter((p) => !isProfileFileMissing(p) && !runningProfilesNow.some((r) => r.id === p.id)).length;
|
|
119
|
+
const runningCount = runningProfilesNow.length;
|
|
120
|
+
const missingCount = profiles.filter((p) => isProfileFileMissing(p)).length;
|
|
121
|
+
const setupCount = normalized.newModels.length + normalized.managedItems.length;
|
|
122
|
+
|
|
123
|
+
const countParts = [];
|
|
124
|
+
if (readyCount > 0) countParts.push(`${readyCount} ready`);
|
|
125
|
+
if (runningCount > 0) countParts.push(`${runningCount} running`);
|
|
126
|
+
if (missingCount > 0) countParts.push(`${missingCount} missing`);
|
|
127
|
+
if (setupCount > 0) countParts.push(`${setupCount} need setup`);
|
|
128
|
+
|
|
129
|
+
console.log(pc.bold(" offgrid-ai"));
|
|
130
|
+
console.log(pc.dim(` ${countParts.join(" · ")} · ${DATA_DIR}`));
|
|
131
|
+
}
|
|
97
132
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
}
|
|
133
|
+
export async function printBenchmarkLine() {
|
|
134
|
+
const repoPath = await findBenchmarkRepo();
|
|
135
|
+
if (repoPath) {
|
|
136
|
+
console.log(pc.green(" ✓") + " local-llm-visual-benchmark linked");
|
|
137
|
+
} else {
|
|
138
|
+
console.log(pc.yellow(" ○") + " to run benchmarks, pair with " + pc.cyan("local-llm-visual-benchmark"));
|
|
111
139
|
}
|
|
140
|
+
}
|
|
112
141
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
142
|
+
function tableWidth() {
|
|
143
|
+
const header = [
|
|
144
|
+
optionPad("Status", null, OPTION_STATUS_WIDTH),
|
|
145
|
+
optionPad("Backend", null, OPTION_SOURCE_WIDTH),
|
|
146
|
+
optionPad("Model", null, OPTION_MODEL_WIDTH),
|
|
147
|
+
optionPad("Ctx", null, OPTION_CTX_WIDTH),
|
|
148
|
+
"Size",
|
|
149
|
+
].join(" \u2502 ");
|
|
150
|
+
return stripVTControlCharacters(header).length;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function printTableHeader() {
|
|
154
|
+
console.log("");
|
|
155
|
+
const header = [
|
|
156
|
+
optionPad("Status", pc.dim, OPTION_STATUS_WIDTH),
|
|
157
|
+
optionPad("Backend", pc.dim, OPTION_SOURCE_WIDTH),
|
|
158
|
+
optionPad("Model", pc.dim, OPTION_MODEL_WIDTH),
|
|
159
|
+
optionPad("Ctx", pc.dim, OPTION_CTX_WIDTH),
|
|
160
|
+
pc.dim("Size"),
|
|
161
|
+
].join(OPTION_SEPARATOR);
|
|
162
|
+
console.log(header);
|
|
163
|
+
console.log(pc.dim("─".repeat(tableWidth())));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function printTableFooter() {
|
|
167
|
+
console.log(pc.dim("─".repeat(tableWidth())));
|
|
124
168
|
}
|
|
125
169
|
|
|
126
170
|
export async function printProfileDetails(profile) {
|
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}` : "";
|