@jc20231028/local-code-agent 0.1.0 → 0.1.1
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/LICENSE +21 -21
- package/README.md +274 -263
- package/bin/local-code.js +8 -8
- package/package.json +34 -32
- package/scripts/postinstall.js +14 -0
- package/src/checkpoint.js +125 -125
- package/src/cli.js +932 -932
- package/src/config.js +106 -106
- package/src/providers/lmstudio.js +49 -49
- package/src/providers/ollama.js +52 -52
- package/src/runtime.js +341 -341
- package/src/skills.js +130 -130
- package/src/syntaxCheck.js +62 -62
- package/src/tools.js +105 -105
- package/src/ui.js +281 -281
- package/src/workspace.js +224 -224
package/src/runtime.js
CHANGED
|
@@ -1,341 +1,341 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { execFile as execFileCallback } from "node:child_process";
|
|
5
|
-
import { promisify } from "node:util";
|
|
6
|
-
|
|
7
|
-
const execFile = promisify(execFileCallback);
|
|
8
|
-
|
|
9
|
-
export async function inspectProviders(config) {
|
|
10
|
-
const [ollama, lmstudio] = await Promise.all([
|
|
11
|
-
detectOllama(config.ollamaBaseUrl),
|
|
12
|
-
detectLmStudio(config.lmStudioBaseUrl)
|
|
13
|
-
]);
|
|
14
|
-
|
|
15
|
-
return {
|
|
16
|
-
ollama,
|
|
17
|
-
lmstudio
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function buildProviderDiagnostics(statusMap) {
|
|
22
|
-
return Object.values(statusMap).map((status) => ({
|
|
23
|
-
heading: `${status.label} (${status.provider})`,
|
|
24
|
-
lines: [
|
|
25
|
-
` Status: ${summarizeProvider(status)}`,
|
|
26
|
-
` API: ${status.baseUrl}`,
|
|
27
|
-
...formatLocations(status.locations),
|
|
28
|
-
...formatNextSteps(status)
|
|
29
|
-
]
|
|
30
|
-
}));
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function summarizeProvider(status) {
|
|
34
|
-
if (!status.installed) {
|
|
35
|
-
return `${status.label} - not installed`;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (!status.serverReachable) {
|
|
39
|
-
return `${status.label} - installed, local API offline`;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (status.models.length === 0) {
|
|
43
|
-
return `${status.label} - installed, API online, no local models`;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return `${status.label} - installed, API online, ${status.models.length} model(s)`;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function buildProviderProblemMessage(status) {
|
|
50
|
-
if (!status.installed) {
|
|
51
|
-
return [
|
|
52
|
-
`${status.label} is not installed.`,
|
|
53
|
-
...status.installHints
|
|
54
|
-
].join("\n");
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (!status.serverReachable) {
|
|
58
|
-
return [
|
|
59
|
-
`${status.label} is installed, but the local API is not reachable at ${status.baseUrl}.`,
|
|
60
|
-
...status.serverHints
|
|
61
|
-
].join("\n");
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (status.models.length === 0) {
|
|
65
|
-
return [
|
|
66
|
-
`${status.label} is reachable, but no local models are available.`,
|
|
67
|
-
...status.modelHints
|
|
68
|
-
].join("\n");
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return `${status.label} is ready.`;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function pickAutoProvider(statusMap) {
|
|
75
|
-
const readyProviders = Object.values(statusMap).filter(isProviderReady);
|
|
76
|
-
if (readyProviders.length === 1) {
|
|
77
|
-
return readyProviders[0].provider;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (readyProviders.length > 1) {
|
|
81
|
-
return readyProviders[0].provider;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const installedProviders = Object.values(statusMap).filter((item) => item.installed);
|
|
85
|
-
if (installedProviders.length === 1) {
|
|
86
|
-
return installedProviders[0].provider;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export function isProviderReady(status) {
|
|
93
|
-
return status.installed && status.serverReachable && status.models.length > 0;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function getProviderUnavailableReason(status) {
|
|
97
|
-
if (!status.installed) {
|
|
98
|
-
return "Install required";
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (!status.serverReachable) {
|
|
102
|
-
return "Start local API server";
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (status.models.length === 0) {
|
|
106
|
-
return "Download a local model";
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return "";
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async function detectOllama(baseUrl) {
|
|
113
|
-
const commandLocations = await findCommandLocations(["ollama"]);
|
|
114
|
-
const installLocations = await existingPaths(getOllamaInstallPaths());
|
|
115
|
-
const locations = uniqueValues([...commandLocations, ...installLocations]);
|
|
116
|
-
const installed = locations.length > 0;
|
|
117
|
-
const api = await probeOllamaApi(baseUrl);
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
provider: "ollama",
|
|
121
|
-
label: "Ollama",
|
|
122
|
-
baseUrl,
|
|
123
|
-
installed,
|
|
124
|
-
locations,
|
|
125
|
-
serverReachable: api.serverReachable,
|
|
126
|
-
models: api.models,
|
|
127
|
-
installHints: [
|
|
128
|
-
"Install Ollama from https://ollama.com/download",
|
|
129
|
-
"After installation, open Ollama once or run `ollama serve`."
|
|
130
|
-
],
|
|
131
|
-
serverHints: [
|
|
132
|
-
"Start Ollama and confirm the local server is running.",
|
|
133
|
-
"If needed, run `ollama serve` and retry."
|
|
134
|
-
],
|
|
135
|
-
modelHints: [
|
|
136
|
-
"Download at least one model first, for example: `ollama pull qwen2.5-coder:7b`.",
|
|
137
|
-
"Then run the CLI again."
|
|
138
|
-
]
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async function detectLmStudio(baseUrl) {
|
|
143
|
-
const commandLocations = await findCommandLocations(["lms", "lmstudio"]);
|
|
144
|
-
const installLocations = await existingPaths(getLmStudioInstallPaths());
|
|
145
|
-
const locations = uniqueValues([...commandLocations, ...installLocations]);
|
|
146
|
-
const installed = locations.length > 0;
|
|
147
|
-
const api = await probeOpenAiCompatibleModels(baseUrl);
|
|
148
|
-
|
|
149
|
-
return {
|
|
150
|
-
provider: "lmstudio",
|
|
151
|
-
label: "LM Studio",
|
|
152
|
-
baseUrl,
|
|
153
|
-
installed,
|
|
154
|
-
locations,
|
|
155
|
-
serverReachable: api.serverReachable,
|
|
156
|
-
models: api.models,
|
|
157
|
-
installHints: [
|
|
158
|
-
"Install LM Studio from https://lmstudio.ai/",
|
|
159
|
-
"After installation, open LM Studio and enable the local server."
|
|
160
|
-
],
|
|
161
|
-
serverHints: [
|
|
162
|
-
"Open LM Studio and start the local server on the configured port.",
|
|
163
|
-
"By default this CLI expects LM Studio at http://127.0.0.1:1234."
|
|
164
|
-
],
|
|
165
|
-
modelHints: [
|
|
166
|
-
"Download a local model in LM Studio.",
|
|
167
|
-
"Load the model or expose it through the local server, then retry."
|
|
168
|
-
]
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
async function findCommandLocations(commands) {
|
|
173
|
-
const results = [];
|
|
174
|
-
for (const command of commands) {
|
|
175
|
-
try {
|
|
176
|
-
const { stdout } = await execFile(getWhichCommand(), getWhichArgs(command), {
|
|
177
|
-
timeout: 1500,
|
|
178
|
-
windowsHide: true
|
|
179
|
-
});
|
|
180
|
-
const lines = stdout
|
|
181
|
-
.split(/\r?\n/)
|
|
182
|
-
.map((value) => value.trim())
|
|
183
|
-
.filter(Boolean);
|
|
184
|
-
results.push(...lines);
|
|
185
|
-
} catch {
|
|
186
|
-
// Command not available in PATH.
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return uniqueValues(results);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function getWhichCommand() {
|
|
194
|
-
return process.platform === "win32" ? "where.exe" : "which";
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function getWhichArgs(command) {
|
|
198
|
-
return process.platform === "win32" ? [command] : [command];
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
async function existingPaths(pathsToCheck) {
|
|
202
|
-
const results = [];
|
|
203
|
-
for (const candidate of pathsToCheck) {
|
|
204
|
-
if (!candidate) {
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
try {
|
|
209
|
-
await fs.access(candidate);
|
|
210
|
-
results.push(candidate);
|
|
211
|
-
} catch {
|
|
212
|
-
// Path not present.
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return results;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
async function probeOllamaApi(baseUrl) {
|
|
220
|
-
const response = await fetchJson(`${stripTrailingSlash(baseUrl)}/api/tags`);
|
|
221
|
-
if (!response.ok) {
|
|
222
|
-
return {
|
|
223
|
-
serverReachable: false,
|
|
224
|
-
models: []
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
return {
|
|
229
|
-
serverReachable: true,
|
|
230
|
-
models: (response.data.models ?? []).map((item) => item.name).filter(Boolean)
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
async function probeOpenAiCompatibleModels(baseUrl) {
|
|
235
|
-
const response = await fetchJson(`${stripTrailingSlash(baseUrl)}/v1/models`);
|
|
236
|
-
if (!response.ok) {
|
|
237
|
-
return {
|
|
238
|
-
serverReachable: false,
|
|
239
|
-
models: []
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return {
|
|
244
|
-
serverReachable: true,
|
|
245
|
-
models: (response.data.data ?? []).map((item) => item.id).filter(Boolean)
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
async function fetchJson(url) {
|
|
250
|
-
const controller = new AbortController();
|
|
251
|
-
const timer = setTimeout(() => controller.abort(), 2000);
|
|
252
|
-
|
|
253
|
-
try {
|
|
254
|
-
const response = await fetch(url, {
|
|
255
|
-
method: "GET",
|
|
256
|
-
signal: controller.signal,
|
|
257
|
-
headers: {
|
|
258
|
-
accept: "application/json"
|
|
259
|
-
}
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
if (!response.ok) {
|
|
263
|
-
return {
|
|
264
|
-
ok: false,
|
|
265
|
-
data: null
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
return {
|
|
270
|
-
ok: true,
|
|
271
|
-
data: await response.json()
|
|
272
|
-
};
|
|
273
|
-
} catch {
|
|
274
|
-
return {
|
|
275
|
-
ok: false,
|
|
276
|
-
data: null
|
|
277
|
-
};
|
|
278
|
-
} finally {
|
|
279
|
-
clearTimeout(timer);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function getOllamaInstallPaths() {
|
|
284
|
-
const home = os.homedir();
|
|
285
|
-
return uniqueValues([
|
|
286
|
-
process.env.LOCALAPPDATA && path.join(process.env.LOCALAPPDATA, "Programs", "Ollama", "ollama.exe"),
|
|
287
|
-
process.env["ProgramFiles"] && path.join(process.env["ProgramFiles"], "Ollama", "ollama.exe"),
|
|
288
|
-
process.env["ProgramFiles(x86)"] && path.join(process.env["ProgramFiles(x86)"], "Ollama", "ollama.exe"),
|
|
289
|
-
path.join(home, "AppData", "Local", "Programs", "Ollama", "ollama.exe"),
|
|
290
|
-
"/Applications/Ollama.app",
|
|
291
|
-
path.join(home, "Applications", "Ollama.app"),
|
|
292
|
-
"/usr/local/bin/ollama",
|
|
293
|
-
"/usr/bin/ollama"
|
|
294
|
-
]);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function getLmStudioInstallPaths() {
|
|
298
|
-
const home = os.homedir();
|
|
299
|
-
return uniqueValues([
|
|
300
|
-
process.env.LOCALAPPDATA && path.join(process.env.LOCALAPPDATA, "Programs", "LM Studio", "LM Studio.exe"),
|
|
301
|
-
process.env["ProgramFiles"] && path.join(process.env["ProgramFiles"], "LM Studio", "LM Studio.exe"),
|
|
302
|
-
process.env["ProgramFiles(x86)"] && path.join(process.env["ProgramFiles(x86)"], "LM Studio", "LM Studio.exe"),
|
|
303
|
-
path.join(home, "AppData", "Local", "Programs", "LM Studio", "LM Studio.exe"),
|
|
304
|
-
"/Applications/LM Studio.app",
|
|
305
|
-
path.join(home, "Applications", "LM Studio.app"),
|
|
306
|
-
"/opt/LM Studio",
|
|
307
|
-
"/usr/local/bin/lmstudio"
|
|
308
|
-
]);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function uniqueValues(values) {
|
|
312
|
-
return [...new Set(values.filter(Boolean))];
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function stripTrailingSlash(value) {
|
|
316
|
-
return value.endsWith("/") ? value.slice(0, -1) : value;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function formatLocations(locations) {
|
|
320
|
-
if (!locations || locations.length === 0) {
|
|
321
|
-
return [" Install path: not found"];
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
return [` Install path: ${locations[0]}`];
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
function formatNextSteps(status) {
|
|
328
|
-
if (isProviderReady(status)) {
|
|
329
|
-
return [` Models: ${status.models.slice(0, 4).join(", ")}`];
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
if (!status.installed) {
|
|
333
|
-
return status.installHints.map((line) => ` Next: ${line}`);
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
if (!status.serverReachable) {
|
|
337
|
-
return status.serverHints.map((line) => ` Next: ${line}`);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
return status.modelHints.map((line) => ` Next: ${line}`);
|
|
341
|
-
}
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { execFile as execFileCallback } from "node:child_process";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
|
|
7
|
+
const execFile = promisify(execFileCallback);
|
|
8
|
+
|
|
9
|
+
export async function inspectProviders(config) {
|
|
10
|
+
const [ollama, lmstudio] = await Promise.all([
|
|
11
|
+
detectOllama(config.ollamaBaseUrl),
|
|
12
|
+
detectLmStudio(config.lmStudioBaseUrl)
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
ollama,
|
|
17
|
+
lmstudio
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function buildProviderDiagnostics(statusMap) {
|
|
22
|
+
return Object.values(statusMap).map((status) => ({
|
|
23
|
+
heading: `${status.label} (${status.provider})`,
|
|
24
|
+
lines: [
|
|
25
|
+
` Status: ${summarizeProvider(status)}`,
|
|
26
|
+
` API: ${status.baseUrl}`,
|
|
27
|
+
...formatLocations(status.locations),
|
|
28
|
+
...formatNextSteps(status)
|
|
29
|
+
]
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function summarizeProvider(status) {
|
|
34
|
+
if (!status.installed) {
|
|
35
|
+
return `${status.label} - not installed`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!status.serverReachable) {
|
|
39
|
+
return `${status.label} - installed, local API offline`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (status.models.length === 0) {
|
|
43
|
+
return `${status.label} - installed, API online, no local models`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return `${status.label} - installed, API online, ${status.models.length} model(s)`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function buildProviderProblemMessage(status) {
|
|
50
|
+
if (!status.installed) {
|
|
51
|
+
return [
|
|
52
|
+
`${status.label} is not installed.`,
|
|
53
|
+
...status.installHints
|
|
54
|
+
].join("\n");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!status.serverReachable) {
|
|
58
|
+
return [
|
|
59
|
+
`${status.label} is installed, but the local API is not reachable at ${status.baseUrl}.`,
|
|
60
|
+
...status.serverHints
|
|
61
|
+
].join("\n");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (status.models.length === 0) {
|
|
65
|
+
return [
|
|
66
|
+
`${status.label} is reachable, but no local models are available.`,
|
|
67
|
+
...status.modelHints
|
|
68
|
+
].join("\n");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return `${status.label} is ready.`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function pickAutoProvider(statusMap) {
|
|
75
|
+
const readyProviders = Object.values(statusMap).filter(isProviderReady);
|
|
76
|
+
if (readyProviders.length === 1) {
|
|
77
|
+
return readyProviders[0].provider;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (readyProviders.length > 1) {
|
|
81
|
+
return readyProviders[0].provider;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const installedProviders = Object.values(statusMap).filter((item) => item.installed);
|
|
85
|
+
if (installedProviders.length === 1) {
|
|
86
|
+
return installedProviders[0].provider;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function isProviderReady(status) {
|
|
93
|
+
return status.installed && status.serverReachable && status.models.length > 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function getProviderUnavailableReason(status) {
|
|
97
|
+
if (!status.installed) {
|
|
98
|
+
return "Install required";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!status.serverReachable) {
|
|
102
|
+
return "Start local API server";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (status.models.length === 0) {
|
|
106
|
+
return "Download a local model";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return "";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function detectOllama(baseUrl) {
|
|
113
|
+
const commandLocations = await findCommandLocations(["ollama"]);
|
|
114
|
+
const installLocations = await existingPaths(getOllamaInstallPaths());
|
|
115
|
+
const locations = uniqueValues([...commandLocations, ...installLocations]);
|
|
116
|
+
const installed = locations.length > 0;
|
|
117
|
+
const api = await probeOllamaApi(baseUrl);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
provider: "ollama",
|
|
121
|
+
label: "Ollama",
|
|
122
|
+
baseUrl,
|
|
123
|
+
installed,
|
|
124
|
+
locations,
|
|
125
|
+
serverReachable: api.serverReachable,
|
|
126
|
+
models: api.models,
|
|
127
|
+
installHints: [
|
|
128
|
+
"Install Ollama from https://ollama.com/download",
|
|
129
|
+
"After installation, open Ollama once or run `ollama serve`."
|
|
130
|
+
],
|
|
131
|
+
serverHints: [
|
|
132
|
+
"Start Ollama and confirm the local server is running.",
|
|
133
|
+
"If needed, run `ollama serve` and retry."
|
|
134
|
+
],
|
|
135
|
+
modelHints: [
|
|
136
|
+
"Download at least one model first, for example: `ollama pull qwen2.5-coder:7b`.",
|
|
137
|
+
"Then run the CLI again."
|
|
138
|
+
]
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function detectLmStudio(baseUrl) {
|
|
143
|
+
const commandLocations = await findCommandLocations(["lms", "lmstudio"]);
|
|
144
|
+
const installLocations = await existingPaths(getLmStudioInstallPaths());
|
|
145
|
+
const locations = uniqueValues([...commandLocations, ...installLocations]);
|
|
146
|
+
const installed = locations.length > 0;
|
|
147
|
+
const api = await probeOpenAiCompatibleModels(baseUrl);
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
provider: "lmstudio",
|
|
151
|
+
label: "LM Studio",
|
|
152
|
+
baseUrl,
|
|
153
|
+
installed,
|
|
154
|
+
locations,
|
|
155
|
+
serverReachable: api.serverReachable,
|
|
156
|
+
models: api.models,
|
|
157
|
+
installHints: [
|
|
158
|
+
"Install LM Studio from https://lmstudio.ai/",
|
|
159
|
+
"After installation, open LM Studio and enable the local server."
|
|
160
|
+
],
|
|
161
|
+
serverHints: [
|
|
162
|
+
"Open LM Studio and start the local server on the configured port.",
|
|
163
|
+
"By default this CLI expects LM Studio at http://127.0.0.1:1234."
|
|
164
|
+
],
|
|
165
|
+
modelHints: [
|
|
166
|
+
"Download a local model in LM Studio.",
|
|
167
|
+
"Load the model or expose it through the local server, then retry."
|
|
168
|
+
]
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function findCommandLocations(commands) {
|
|
173
|
+
const results = [];
|
|
174
|
+
for (const command of commands) {
|
|
175
|
+
try {
|
|
176
|
+
const { stdout } = await execFile(getWhichCommand(), getWhichArgs(command), {
|
|
177
|
+
timeout: 1500,
|
|
178
|
+
windowsHide: true
|
|
179
|
+
});
|
|
180
|
+
const lines = stdout
|
|
181
|
+
.split(/\r?\n/)
|
|
182
|
+
.map((value) => value.trim())
|
|
183
|
+
.filter(Boolean);
|
|
184
|
+
results.push(...lines);
|
|
185
|
+
} catch {
|
|
186
|
+
// Command not available in PATH.
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return uniqueValues(results);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function getWhichCommand() {
|
|
194
|
+
return process.platform === "win32" ? "where.exe" : "which";
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function getWhichArgs(command) {
|
|
198
|
+
return process.platform === "win32" ? [command] : [command];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function existingPaths(pathsToCheck) {
|
|
202
|
+
const results = [];
|
|
203
|
+
for (const candidate of pathsToCheck) {
|
|
204
|
+
if (!candidate) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
await fs.access(candidate);
|
|
210
|
+
results.push(candidate);
|
|
211
|
+
} catch {
|
|
212
|
+
// Path not present.
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return results;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function probeOllamaApi(baseUrl) {
|
|
220
|
+
const response = await fetchJson(`${stripTrailingSlash(baseUrl)}/api/tags`);
|
|
221
|
+
if (!response.ok) {
|
|
222
|
+
return {
|
|
223
|
+
serverReachable: false,
|
|
224
|
+
models: []
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
serverReachable: true,
|
|
230
|
+
models: (response.data.models ?? []).map((item) => item.name).filter(Boolean)
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function probeOpenAiCompatibleModels(baseUrl) {
|
|
235
|
+
const response = await fetchJson(`${stripTrailingSlash(baseUrl)}/v1/models`);
|
|
236
|
+
if (!response.ok) {
|
|
237
|
+
return {
|
|
238
|
+
serverReachable: false,
|
|
239
|
+
models: []
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
serverReachable: true,
|
|
245
|
+
models: (response.data.data ?? []).map((item) => item.id).filter(Boolean)
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function fetchJson(url) {
|
|
250
|
+
const controller = new AbortController();
|
|
251
|
+
const timer = setTimeout(() => controller.abort(), 2000);
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
const response = await fetch(url, {
|
|
255
|
+
method: "GET",
|
|
256
|
+
signal: controller.signal,
|
|
257
|
+
headers: {
|
|
258
|
+
accept: "application/json"
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
if (!response.ok) {
|
|
263
|
+
return {
|
|
264
|
+
ok: false,
|
|
265
|
+
data: null
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
ok: true,
|
|
271
|
+
data: await response.json()
|
|
272
|
+
};
|
|
273
|
+
} catch {
|
|
274
|
+
return {
|
|
275
|
+
ok: false,
|
|
276
|
+
data: null
|
|
277
|
+
};
|
|
278
|
+
} finally {
|
|
279
|
+
clearTimeout(timer);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function getOllamaInstallPaths() {
|
|
284
|
+
const home = os.homedir();
|
|
285
|
+
return uniqueValues([
|
|
286
|
+
process.env.LOCALAPPDATA && path.join(process.env.LOCALAPPDATA, "Programs", "Ollama", "ollama.exe"),
|
|
287
|
+
process.env["ProgramFiles"] && path.join(process.env["ProgramFiles"], "Ollama", "ollama.exe"),
|
|
288
|
+
process.env["ProgramFiles(x86)"] && path.join(process.env["ProgramFiles(x86)"], "Ollama", "ollama.exe"),
|
|
289
|
+
path.join(home, "AppData", "Local", "Programs", "Ollama", "ollama.exe"),
|
|
290
|
+
"/Applications/Ollama.app",
|
|
291
|
+
path.join(home, "Applications", "Ollama.app"),
|
|
292
|
+
"/usr/local/bin/ollama",
|
|
293
|
+
"/usr/bin/ollama"
|
|
294
|
+
]);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function getLmStudioInstallPaths() {
|
|
298
|
+
const home = os.homedir();
|
|
299
|
+
return uniqueValues([
|
|
300
|
+
process.env.LOCALAPPDATA && path.join(process.env.LOCALAPPDATA, "Programs", "LM Studio", "LM Studio.exe"),
|
|
301
|
+
process.env["ProgramFiles"] && path.join(process.env["ProgramFiles"], "LM Studio", "LM Studio.exe"),
|
|
302
|
+
process.env["ProgramFiles(x86)"] && path.join(process.env["ProgramFiles(x86)"], "LM Studio", "LM Studio.exe"),
|
|
303
|
+
path.join(home, "AppData", "Local", "Programs", "LM Studio", "LM Studio.exe"),
|
|
304
|
+
"/Applications/LM Studio.app",
|
|
305
|
+
path.join(home, "Applications", "LM Studio.app"),
|
|
306
|
+
"/opt/LM Studio",
|
|
307
|
+
"/usr/local/bin/lmstudio"
|
|
308
|
+
]);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function uniqueValues(values) {
|
|
312
|
+
return [...new Set(values.filter(Boolean))];
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function stripTrailingSlash(value) {
|
|
316
|
+
return value.endsWith("/") ? value.slice(0, -1) : value;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function formatLocations(locations) {
|
|
320
|
+
if (!locations || locations.length === 0) {
|
|
321
|
+
return [" Install path: not found"];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return [` Install path: ${locations[0]}`];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function formatNextSteps(status) {
|
|
328
|
+
if (isProviderReady(status)) {
|
|
329
|
+
return [` Models: ${status.models.slice(0, 4).join(", ")}`];
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (!status.installed) {
|
|
333
|
+
return status.installHints.map((line) => ` Next: ${line}`);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (!status.serverReachable) {
|
|
337
|
+
return status.serverHints.map((line) => ` Next: ${line}`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return status.modelHints.map((line) => ` Next: ${line}`);
|
|
341
|
+
}
|