offgrid-ai 0.7.1 → 0.7.3
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/cli.mjs +2 -2
- package/src/commands/models.mjs +51 -18
- package/src/model-presenters.mjs +6 -5
- package/src/updates.mjs +9 -2
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { uninstallCommand } from "./commands/uninstall.mjs";
|
|
|
10
10
|
|
|
11
11
|
async function offerUpdate(argv) {
|
|
12
12
|
const invocation = detectInvocation();
|
|
13
|
-
const update = await checkForUpdate({ force:
|
|
13
|
+
const update = await checkForUpdate({ force: false });
|
|
14
14
|
if (!update) return false;
|
|
15
15
|
|
|
16
16
|
const plan = updateCommand(invocation, argv);
|
|
@@ -56,7 +56,7 @@ async function printVersion() {
|
|
|
56
56
|
const version = currentPackageVersion();
|
|
57
57
|
console.log(`offgrid-ai v${version}`);
|
|
58
58
|
const invocation = detectInvocation();
|
|
59
|
-
const update = await checkForUpdate({ force:
|
|
59
|
+
const update = await checkForUpdate({ force: false });
|
|
60
60
|
if (update) {
|
|
61
61
|
const plan = updateCommand(invocation, ["version"]);
|
|
62
62
|
console.log(pc.yellow(`Update available: v${update.latest}. Run: ${plan.display}`));
|
package/src/commands/models.mjs
CHANGED
|
@@ -9,6 +9,8 @@ import { buildCatalogItems, createManagedProfile, itemKey, loadModelCatalog, nor
|
|
|
9
9
|
import { modelSelectOption, modelNameWidth, printGgufModelDetails, printManagedModelDetails, printWorkspaceHeader, printBenchmarkLine, printProfileDetails } from "../model-presenters.mjs";
|
|
10
10
|
import { runProfile } from "./run.mjs";
|
|
11
11
|
|
|
12
|
+
const { stripVTControlCharacters } = await import("node:util");
|
|
13
|
+
|
|
12
14
|
export async function modelsCommand(argv) {
|
|
13
15
|
await ensureDirs();
|
|
14
16
|
const catalog = await loadModelCatalog();
|
|
@@ -62,33 +64,64 @@ export async function modelCommandCenter(initialCatalog) {
|
|
|
62
64
|
}
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
|
|
67
|
+
function formatActions(rawActions) {
|
|
68
|
+
const sep = pc.dim(" │ ");
|
|
69
|
+
const maxName = Math.max(...rawActions.map((a) => stripVTControlCharacters(a.name).length));
|
|
70
|
+
const width = Math.max(17, maxName + 2);
|
|
71
|
+
return rawActions.map((a) => {
|
|
72
|
+
const name = a.dimmed ? pc.dim(pc.strikethrough(a.name.padEnd(width).slice(0, width))) : pc.bold(a.name.padEnd(width).slice(0, width));
|
|
73
|
+
const desc = a.dimmed ? pc.red("file not found") : pc.dim(a.desc);
|
|
74
|
+
return { value: a.value, label: name + sep + desc };
|
|
75
|
+
});
|
|
76
|
+
}
|
|
66
77
|
|
|
67
78
|
function actionsForItem(item) {
|
|
79
|
+
const missing = item.type === "profile" && item.fileMissing;
|
|
68
80
|
if (item.type === "profile") {
|
|
69
|
-
const
|
|
70
|
-
{ value: "
|
|
71
|
-
{ value: "reconfigure", label: "Reconfigure", hint: "Change context, MTP, settings" },
|
|
72
|
-
{ value: "inspect", label: "Details", hint: "Paths, ports, flags" },
|
|
81
|
+
const available = [
|
|
82
|
+
{ value: "inspect", name: "Details", desc: "Paths, ports, flags" },
|
|
73
83
|
];
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
84
|
+
if (!missing) {
|
|
85
|
+
available.unshift(
|
|
86
|
+
{ value: "run", name: "Start chatting", desc: "Launch and open Pi" },
|
|
87
|
+
{ value: "reconfigure", name: "Reconfigure", desc: "Change context, MTP, settings" },
|
|
88
|
+
);
|
|
89
|
+
const backend = backendFor(item.profile.backend);
|
|
90
|
+
if (backend.type === "local-server" || backend.type === "managed-server") {
|
|
91
|
+
available.push({ value: "benchmark", name: "Benchmark", desc: "Prepare a benchmark run" });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
available.push({ value: "remove", name: "Remove", desc: missing ? "Delete this broken setup" : "Delete this setup" });
|
|
95
|
+
if (missing) {
|
|
96
|
+
available.unshift(
|
|
97
|
+
{ value: "run", name: "Start chatting", desc: "Launch and open Pi", dimmed: true },
|
|
98
|
+
{ value: "reconfigure", name: "Reconfigure", desc: "Change context, MTP, settings", dimmed: true },
|
|
99
|
+
);
|
|
100
|
+
const backend = backendFor(item.profile.backend);
|
|
101
|
+
if (backend.type === "local-server" || backend.type === "managed-server") {
|
|
102
|
+
available.push({ value: "benchmark", name: "Benchmark", desc: "Prepare a benchmark run", dimmed: true });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return formatActions(available);
|
|
78
106
|
}
|
|
79
107
|
if (item.type === "new") {
|
|
80
|
-
return [
|
|
81
|
-
{ value: "setup",
|
|
82
|
-
{ value: "inspect",
|
|
83
|
-
];
|
|
108
|
+
return formatActions([
|
|
109
|
+
{ value: "setup", name: "Set up", desc: "Configure and save" },
|
|
110
|
+
{ value: "inspect", name: "Details", desc: "Model info" },
|
|
111
|
+
]);
|
|
84
112
|
}
|
|
85
|
-
return [
|
|
86
|
-
{ value: "setup",
|
|
87
|
-
{ value: "inspect",
|
|
88
|
-
];
|
|
113
|
+
return formatActions([
|
|
114
|
+
{ value: "setup", name: "Set up", desc: `Connect via ${BACKENDS[item.backendId].label}` },
|
|
115
|
+
{ value: "inspect", name: "Details", desc: "Model info" },
|
|
116
|
+
]);
|
|
89
117
|
}
|
|
90
118
|
|
|
91
119
|
async function performAction(prompt, action, item) {
|
|
120
|
+
const missing = item.type === "profile" && item.fileMissing;
|
|
121
|
+
if (missing && ["run", "reconfigure", "benchmark"].includes(action)) {
|
|
122
|
+
console.log(pc.red("This model's file is no longer on disk. Remove the setup or move the file back."));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
92
125
|
if (action === "inspect") {
|
|
93
126
|
if (item.type === "profile") return await printProfileDetails(await readProfile(item.profile.id));
|
|
94
127
|
if (item.type === "managed") return printManagedModelDetails(item.model, BACKENDS[item.backendId]);
|
|
@@ -169,4 +202,4 @@ async function removeProfileInteractive(id) {
|
|
|
169
202
|
await removeFromPiConfig(profile);
|
|
170
203
|
await deleteProfile(id);
|
|
171
204
|
console.log(pc.green(`Removed ${profile.label} (${profile.id})`));
|
|
172
|
-
}
|
|
205
|
+
}
|
package/src/model-presenters.mjs
CHANGED
|
@@ -25,7 +25,7 @@ function optionPad(text, color, width) {
|
|
|
25
25
|
function optionStatusTag(kind) {
|
|
26
26
|
const statuses = {
|
|
27
27
|
running: ["RUNNING", pc.green],
|
|
28
|
-
ready: ["READY", pc.
|
|
28
|
+
ready: ["READY", pc.blue],
|
|
29
29
|
missing: ["MISSING", pc.red],
|
|
30
30
|
setup: ["SETUP", pc.yellow],
|
|
31
31
|
};
|
|
@@ -130,13 +130,14 @@ export function printWorkspaceHeader(normalized, runningProfilesNow) {
|
|
|
130
130
|
const setupCount = normalized.newModels.length + normalized.managedItems.length;
|
|
131
131
|
|
|
132
132
|
const countParts = [];
|
|
133
|
-
if (runningCount > 0) countParts.push(`${runningCount} running`);
|
|
134
|
-
if (readyCount > 0) countParts.push(pc.
|
|
133
|
+
if (runningCount > 0) countParts.push(pc.green(`${runningCount} running`));
|
|
134
|
+
if (readyCount > 0) countParts.push(pc.blue(`${readyCount} model${readyCount === 1 ? "" : "s"} ready`));
|
|
135
135
|
if (missingCount > 0) countParts.push(pc.red(`${missingCount} model${missingCount === 1 ? "" : "s"} missing`));
|
|
136
136
|
if (setupCount > 0) countParts.push(pc.yellow(`${setupCount} model${setupCount === 1 ? "" : "s"} need${setupCount === 1 ? "s" : ""} setup`));
|
|
137
137
|
|
|
138
|
-
console.log(
|
|
138
|
+
console.log(` ${countParts.join(pc.dim(" · "))}`);
|
|
139
139
|
console.log(pc.dim(` Profiles: ${DATA_DIR}`));
|
|
140
|
+
console.log(pc.dim(" ─────────────────────────────────────────────────────────"));
|
|
140
141
|
}
|
|
141
142
|
|
|
142
143
|
export async function printBenchmarkLine() {
|
|
@@ -155,7 +156,7 @@ export async function printProfileDetails(profile) {
|
|
|
155
156
|
const fileMissing = !isManaged && isProfileFileMissing(profile);
|
|
156
157
|
console.log("\n" + renderSection("Model overview", renderRows([
|
|
157
158
|
["Name", pc.bold(profile.label)],
|
|
158
|
-
["Status", fileMissing ? pc.red("File missing") : running ? pc.green("Running now") : "Ready"],
|
|
159
|
+
["Status", fileMissing ? pc.red("File missing") : running ? pc.green("Running now") : pc.blue("Ready")],
|
|
159
160
|
["Details", profileDetailParts(profile, { fileMissing }).join(pc.dim(" · "))],
|
|
160
161
|
["Server", fileMissing ? pc.red(profile.baseUrl) : profile.baseUrl],
|
|
161
162
|
])));
|
package/src/updates.mjs
CHANGED
|
@@ -15,8 +15,15 @@ export async function checkForUpdate({ now = Date.now(), fetchImpl = globalThis.
|
|
|
15
15
|
const cacheFile = join(DATA_DIR, "update-cache.json");
|
|
16
16
|
const cached = await readUpdateCache(cacheFile);
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
// Use cache if fresh (within 24h) and still applies to current version
|
|
19
|
+
if (!force && cached?.lastChecked && now - cached.lastChecked < UPDATE_CHECK_INTERVAL) {
|
|
20
|
+
if (cached.currentVersion === currentVersion) {
|
|
21
|
+
return updateResult(currentVersion, cached.latestVersion);
|
|
22
|
+
}
|
|
23
|
+
// Cache is from a different version — if latest isn't newer than current, no update
|
|
24
|
+
if (cached.latestVersion && !isNewerVersion(cached.latestVersion, currentVersion)) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
try {
|