offgrid-ai 0.8.11 → 0.8.12
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 +42 -5
- package/src/model-catalog.mjs +15 -1
- package/src/model-presenters.mjs +12 -5
- package/src/process.mjs +12 -1
package/package.json
CHANGED
package/src/commands/models.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ensureDirs } from "../config.mjs";
|
|
2
2
|
import { backendFor, BACKENDS } from "../backends.mjs";
|
|
3
3
|
import { createProfileFromModel, readProfile, saveProfile, deleteProfile, profileJsonPath } from "../profiles.mjs";
|
|
4
|
-
import { isProfileRunning, stopProfile } from "../process.mjs";
|
|
4
|
+
import { isProfileRunning, isProfileServerUp, stopProfile } from "../process.mjs";
|
|
5
5
|
import { syncPiConfig, removeFromPiConfig } from "../harness-pi.mjs";
|
|
6
6
|
import { configureLocalProfile } from "../profile-setup.mjs";
|
|
7
7
|
import { pc, startInteractive, createPrompt } from "../ui.mjs";
|
|
@@ -40,18 +40,55 @@ export async function modelCommandCenter(initialCatalog) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
const runningProfilesNow = [];
|
|
43
|
+
const serverUpIds = new Set();
|
|
43
44
|
for (const profile of normalized.profiles) {
|
|
44
|
-
if (await isProfileRunning(profile))
|
|
45
|
+
if (await isProfileRunning(profile)) {
|
|
46
|
+
runningProfilesNow.push(profile);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (await isProfileServerUp(profile)) serverUpIds.add(profile.id);
|
|
45
50
|
}
|
|
46
|
-
printWorkspaceHeader(normalized, runningProfilesNow);
|
|
51
|
+
printWorkspaceHeader(normalized, runningProfilesNow, serverUpIds);
|
|
47
52
|
await printBenchmarkLine();
|
|
48
53
|
|
|
49
54
|
const nameWidth = modelNameWidth(allItems);
|
|
50
55
|
|
|
56
|
+
const statusFor = (item) => {
|
|
57
|
+
if (item.type === "profile") {
|
|
58
|
+
if (item.fileMissing) return "missing";
|
|
59
|
+
if (runningProfilesNow.some((profile) => profile.id === item.profile.id)) return "running";
|
|
60
|
+
if (serverUpIds.has(item.profile.id)) return "serverup";
|
|
61
|
+
return "ready";
|
|
62
|
+
}
|
|
63
|
+
return "setup";
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const groupOrder = [
|
|
67
|
+
{ key: "running", label: pc.green(" Running") },
|
|
68
|
+
{ key: "serverup", label: pc.yellow(" Server up · model not loaded") },
|
|
69
|
+
{ key: "ready", label: pc.blue(" Ready to chat") },
|
|
70
|
+
{ key: "setup", label: pc.yellow(" Need setup") },
|
|
71
|
+
{ key: "missing", label: pc.red(" File missing") },
|
|
72
|
+
];
|
|
73
|
+
const grouped = new Map(groupOrder.map((g) => [g.key, []]));
|
|
74
|
+
for (const item of allItems) grouped.get(statusFor(item)).push(item);
|
|
75
|
+
|
|
76
|
+
const sectionSentinel = "__section__";
|
|
77
|
+
const choices = [];
|
|
78
|
+
for (const group of groupOrder) {
|
|
79
|
+
const bucket = grouped.get(group.key);
|
|
80
|
+
if (!bucket || bucket.length === 0) continue;
|
|
81
|
+
choices.push({ value: `${sectionSentinel}:${group.key}`, label: `── ${group.label} (${bucket.length}) ──`, disabled: true });
|
|
82
|
+
for (const item of bucket) {
|
|
83
|
+
const opt = modelSelectOption(item, { runningProfilesNow, serverUpIds, nameWidth });
|
|
84
|
+
choices.push({ value: opt.value, label: opt.label });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
51
88
|
const prompt = createPrompt();
|
|
52
89
|
try {
|
|
53
|
-
const selected = await prompt.choice("Select a model",
|
|
54
|
-
if (!selected) return;
|
|
90
|
+
const selected = await prompt.choice("Select a model", choices);
|
|
91
|
+
if (!selected || selected.startsWith(`${sectionSentinel}:`)) return;
|
|
55
92
|
const item = allItems.find((candidate) => itemKey(candidate) === selected);
|
|
56
93
|
if (!item) return;
|
|
57
94
|
|
package/src/model-catalog.mjs
CHANGED
|
@@ -37,10 +37,24 @@ export function itemKey(item) {
|
|
|
37
37
|
return `managed:${item.backendId}:${item.model.id}`;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
function profileRecency(item) {
|
|
41
|
+
const updated = item.profile?.updatedAt ?? item.profile?.createdAt;
|
|
42
|
+
const ts = updated ? Date.parse(updated) : NaN;
|
|
43
|
+
return Number.isFinite(ts) ? ts : 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function compareRecency(a, b) {
|
|
47
|
+
const diff = profileRecency(b) - profileRecency(a);
|
|
48
|
+
if (diff !== 0) return diff;
|
|
49
|
+
return String(a.label ?? "").localeCompare(String(b.label ?? ""));
|
|
50
|
+
}
|
|
51
|
+
|
|
40
52
|
export function buildCatalogItems(normalized) {
|
|
41
53
|
const { profiles, newModels, managedItems, drafters } = normalized;
|
|
54
|
+
const profileItems = profiles.map((profile) => ({ type: "profile", profile, label: profile.label, fileMissing: isProfileFileMissing(profile) }));
|
|
55
|
+
profileItems.sort(compareRecency);
|
|
42
56
|
return [
|
|
43
|
-
...
|
|
57
|
+
...profileItems,
|
|
44
58
|
...newModels.map((model) => ({ type: "new", model, label: model.label, drafter: matchDrafter(model.path, drafters) })),
|
|
45
59
|
...managedItems.map(({ model, backendId }) => ({ type: "managed", model, backendId, label: model.label })),
|
|
46
60
|
];
|
package/src/model-presenters.mjs
CHANGED
|
@@ -25,6 +25,7 @@ function optionPad(text, color, width) {
|
|
|
25
25
|
function optionStatusTag(kind) {
|
|
26
26
|
const statuses = {
|
|
27
27
|
running: ["RUNNING", pc.green],
|
|
28
|
+
serverup: ["SERVER UP", pc.yellow],
|
|
28
29
|
ready: ["READY", pc.blue],
|
|
29
30
|
missing: ["MISSING", pc.red],
|
|
30
31
|
setup: ["SETUP", pc.yellow],
|
|
@@ -79,14 +80,16 @@ function optionLabel({ status, source, name, ctx, size, nameWidth }) {
|
|
|
79
80
|
return [status, source, pc.bold(optionPad(name, null, nameWidth)), ctx, pc.dim(size)].join(OPTION_SEPARATOR);
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
export function modelSelectOption(item, { runningProfilesNow, nameWidth }) {
|
|
83
|
+
export function modelSelectOption(item, { runningProfilesNow, serverUpIds, nameWidth }) {
|
|
83
84
|
if (item.type === "profile") {
|
|
84
85
|
const backend = backendFor(item.profile.backend);
|
|
85
86
|
const running = runningProfilesNow.some((profile) => profile.id === item.profile.id);
|
|
87
|
+
const serverUp = !running && !item.fileMissing && serverUpIds?.has(item.profile.id);
|
|
88
|
+
const status = item.fileMissing ? "missing" : running ? "running" : serverUp ? "serverup" : "ready";
|
|
86
89
|
return {
|
|
87
90
|
value: itemKey(item),
|
|
88
91
|
label: optionLabel({
|
|
89
|
-
status: optionStatusTag(
|
|
92
|
+
status: optionStatusTag(status),
|
|
90
93
|
source: optionSourceTag(item.profile.backend, backend.label),
|
|
91
94
|
name: item.profile.label,
|
|
92
95
|
nameWidth,
|
|
@@ -122,15 +125,19 @@ export function modelSelectOption(item, { runningProfilesNow, nameWidth }) {
|
|
|
122
125
|
};
|
|
123
126
|
}
|
|
124
127
|
|
|
125
|
-
export function printWorkspaceHeader(normalized, runningProfilesNow) {
|
|
128
|
+
export function printWorkspaceHeader(normalized, runningProfilesNow, serverUpIds = new Set()) {
|
|
126
129
|
const profiles = normalized.profiles;
|
|
127
|
-
const
|
|
130
|
+
const isRunning = (p) => runningProfilesNow.some((r) => r.id === p.id);
|
|
131
|
+
const isMissing = (p) => isProfileFileMissing(p);
|
|
132
|
+
const readyCount = profiles.filter((p) => !isMissing(p) && !isRunning(p) && !serverUpIds.has(p.id)).length;
|
|
128
133
|
const runningCount = runningProfilesNow.length;
|
|
129
|
-
const
|
|
134
|
+
const serverUpCount = profiles.filter((p) => !isMissing(p) && serverUpIds.has(p.id) && !isRunning(p)).length;
|
|
135
|
+
const missingCount = profiles.filter(isMissing).length;
|
|
130
136
|
const setupCount = normalized.newModels.length + normalized.managedItems.length;
|
|
131
137
|
|
|
132
138
|
const countParts = [];
|
|
133
139
|
if (runningCount > 0) countParts.push(pc.green(`${runningCount} running`));
|
|
140
|
+
if (serverUpCount > 0) countParts.push(pc.yellow(`${serverUpCount} server up, model not loaded`));
|
|
134
141
|
if (readyCount > 0) countParts.push(pc.blue(`${readyCount} model${readyCount === 1 ? "" : "s"} ready`));
|
|
135
142
|
if (missingCount > 0) countParts.push(pc.red(`${missingCount} model${missingCount === 1 ? "" : "s"} missing`));
|
|
136
143
|
if (setupCount > 0) countParts.push(pc.yellow(`${setupCount} model${setupCount === 1 ? "" : "s"} need${setupCount === 1 ? "s" : ""} setup`));
|
package/src/process.mjs
CHANGED
|
@@ -113,11 +113,22 @@ export async function stopProfile(profile) {
|
|
|
113
113
|
|
|
114
114
|
export async function isProfileRunning(profile) {
|
|
115
115
|
const backend = backendFor(profile.backend);
|
|
116
|
-
if (backend.type === "managed-server")
|
|
116
|
+
if (backend.type === "managed-server") {
|
|
117
|
+
return await serverReady(profile.baseUrl) && (await modelLoadedOnServer(profile));
|
|
118
|
+
}
|
|
117
119
|
const state = await readState(profile.id);
|
|
118
120
|
return Boolean(state?.pid && pidAlive(state.pid));
|
|
119
121
|
}
|
|
120
122
|
|
|
123
|
+
export async function isProfileServerUp(profile) {
|
|
124
|
+
return await serverReady(profile.baseUrl);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function modelLoadedOnServer(profile) {
|
|
128
|
+
const { matches } = await serverMatchesProfile(profile);
|
|
129
|
+
return matches;
|
|
130
|
+
}
|
|
131
|
+
|
|
121
132
|
export async function profileRuntimeStatus(profile) {
|
|
122
133
|
const backend = backendFor(profile.backend);
|
|
123
134
|
if (backend.type === "managed-server") {
|