loclaude 0.0.1-alpha.2 → 0.0.1-alpha.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/CHANGELOG.md +12 -0
- package/README.md +93 -7
- package/docker/docker-compose.yml +124 -37
- package/libs/cli/CHANGELOG.md +59 -0
- package/libs/cli/dist/cac.d.ts.map +1 -1
- package/libs/cli/dist/commands/config.d.ts.map +1 -1
- package/libs/cli/dist/commands/docker.d.ts.map +1 -1
- package/libs/cli/dist/commands/doctor.d.ts +4 -0
- package/libs/cli/dist/commands/doctor.d.ts.map +1 -1
- package/libs/cli/dist/commands/init.d.ts +2 -0
- package/libs/cli/dist/commands/init.d.ts.map +1 -1
- package/libs/cli/dist/commands/models.d.ts.map +1 -1
- package/libs/cli/dist/index.bun.js +884 -340
- package/libs/cli/dist/index.bun.js.map +12 -10
- package/libs/cli/dist/index.js +884 -340
- package/libs/cli/dist/index.js.map +12 -10
- package/libs/cli/dist/output.d.ts +107 -0
- package/libs/cli/dist/output.d.ts.map +1 -0
- package/libs/cli/dist/types.d.ts +40 -0
- package/libs/cli/dist/types.d.ts.map +1 -1
- package/libs/cli/dist/utils.d.ts +19 -1
- package/libs/cli/dist/utils.d.ts.map +1 -1
- package/libs/cli/package.json +7 -5
- package/package.json +5 -4
package/libs/cli/dist/index.js
CHANGED
|
@@ -277,6 +277,76 @@ var require_bytes = __commonJS((exports, module) => {
|
|
|
277
277
|
}
|
|
278
278
|
});
|
|
279
279
|
|
|
280
|
+
// ../../node_modules/.bun/picocolors@1.1.1/node_modules/picocolors/picocolors.js
|
|
281
|
+
var require_picocolors = __commonJS((exports, module) => {
|
|
282
|
+
var p = process || {};
|
|
283
|
+
var argv = p.argv || [];
|
|
284
|
+
var env = p.env || {};
|
|
285
|
+
var isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI);
|
|
286
|
+
var formatter = (open, close, replace = open) => (input) => {
|
|
287
|
+
let string = "" + input, index = string.indexOf(close, open.length);
|
|
288
|
+
return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close;
|
|
289
|
+
};
|
|
290
|
+
var replaceClose = (string, close, replace, index) => {
|
|
291
|
+
let result = "", cursor = 0;
|
|
292
|
+
do {
|
|
293
|
+
result += string.substring(cursor, index) + replace;
|
|
294
|
+
cursor = index + close.length;
|
|
295
|
+
index = string.indexOf(close, cursor);
|
|
296
|
+
} while (~index);
|
|
297
|
+
return result + string.substring(cursor);
|
|
298
|
+
};
|
|
299
|
+
var createColors = (enabled = isColorSupported) => {
|
|
300
|
+
let f = enabled ? formatter : () => String;
|
|
301
|
+
return {
|
|
302
|
+
isColorSupported: enabled,
|
|
303
|
+
reset: f("\x1B[0m", "\x1B[0m"),
|
|
304
|
+
bold: f("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m"),
|
|
305
|
+
dim: f("\x1B[2m", "\x1B[22m", "\x1B[22m\x1B[2m"),
|
|
306
|
+
italic: f("\x1B[3m", "\x1B[23m"),
|
|
307
|
+
underline: f("\x1B[4m", "\x1B[24m"),
|
|
308
|
+
inverse: f("\x1B[7m", "\x1B[27m"),
|
|
309
|
+
hidden: f("\x1B[8m", "\x1B[28m"),
|
|
310
|
+
strikethrough: f("\x1B[9m", "\x1B[29m"),
|
|
311
|
+
black: f("\x1B[30m", "\x1B[39m"),
|
|
312
|
+
red: f("\x1B[31m", "\x1B[39m"),
|
|
313
|
+
green: f("\x1B[32m", "\x1B[39m"),
|
|
314
|
+
yellow: f("\x1B[33m", "\x1B[39m"),
|
|
315
|
+
blue: f("\x1B[34m", "\x1B[39m"),
|
|
316
|
+
magenta: f("\x1B[35m", "\x1B[39m"),
|
|
317
|
+
cyan: f("\x1B[36m", "\x1B[39m"),
|
|
318
|
+
white: f("\x1B[37m", "\x1B[39m"),
|
|
319
|
+
gray: f("\x1B[90m", "\x1B[39m"),
|
|
320
|
+
bgBlack: f("\x1B[40m", "\x1B[49m"),
|
|
321
|
+
bgRed: f("\x1B[41m", "\x1B[49m"),
|
|
322
|
+
bgGreen: f("\x1B[42m", "\x1B[49m"),
|
|
323
|
+
bgYellow: f("\x1B[43m", "\x1B[49m"),
|
|
324
|
+
bgBlue: f("\x1B[44m", "\x1B[49m"),
|
|
325
|
+
bgMagenta: f("\x1B[45m", "\x1B[49m"),
|
|
326
|
+
bgCyan: f("\x1B[46m", "\x1B[49m"),
|
|
327
|
+
bgWhite: f("\x1B[47m", "\x1B[49m"),
|
|
328
|
+
blackBright: f("\x1B[90m", "\x1B[39m"),
|
|
329
|
+
redBright: f("\x1B[91m", "\x1B[39m"),
|
|
330
|
+
greenBright: f("\x1B[92m", "\x1B[39m"),
|
|
331
|
+
yellowBright: f("\x1B[93m", "\x1B[39m"),
|
|
332
|
+
blueBright: f("\x1B[94m", "\x1B[39m"),
|
|
333
|
+
magentaBright: f("\x1B[95m", "\x1B[39m"),
|
|
334
|
+
cyanBright: f("\x1B[96m", "\x1B[39m"),
|
|
335
|
+
whiteBright: f("\x1B[97m", "\x1B[39m"),
|
|
336
|
+
bgBlackBright: f("\x1B[100m", "\x1B[49m"),
|
|
337
|
+
bgRedBright: f("\x1B[101m", "\x1B[49m"),
|
|
338
|
+
bgGreenBright: f("\x1B[102m", "\x1B[49m"),
|
|
339
|
+
bgYellowBright: f("\x1B[103m", "\x1B[49m"),
|
|
340
|
+
bgBlueBright: f("\x1B[104m", "\x1B[49m"),
|
|
341
|
+
bgMagentaBright: f("\x1B[105m", "\x1B[49m"),
|
|
342
|
+
bgCyanBright: f("\x1B[106m", "\x1B[49m"),
|
|
343
|
+
bgWhiteBright: f("\x1B[107m", "\x1B[49m")
|
|
344
|
+
};
|
|
345
|
+
};
|
|
346
|
+
module.exports = createColors();
|
|
347
|
+
module.exports.createColors = createColors;
|
|
348
|
+
});
|
|
349
|
+
|
|
280
350
|
// ../../node_modules/.bun/cac@6.7.14/node_modules/cac/dist/index.mjs
|
|
281
351
|
import { EventEmitter } from "events";
|
|
282
352
|
function toArr(any) {
|
|
@@ -2674,15 +2744,67 @@ var dist_default3 = createPrompt((config, done) => {
|
|
|
2674
2744
|
// lib/utils.ts
|
|
2675
2745
|
var import_bytes = __toESM(require_bytes(), 1);
|
|
2676
2746
|
|
|
2747
|
+
// lib/output.ts
|
|
2748
|
+
var import_picocolors = __toESM(require_picocolors(), 1);
|
|
2749
|
+
var brand = (text) => import_picocolors.default.cyan(import_picocolors.default.bold(text));
|
|
2750
|
+
var success = (text) => `${import_picocolors.default.green("✓")} ${text}`;
|
|
2751
|
+
var warn = (text) => `${import_picocolors.default.yellow("⚠")} ${text}`;
|
|
2752
|
+
var error = (text) => `${import_picocolors.default.red("✗")} ${text}`;
|
|
2753
|
+
var info = (text) => `${import_picocolors.default.cyan("ℹ")} ${text}`;
|
|
2754
|
+
var dim = (text) => import_picocolors.default.dim(text);
|
|
2755
|
+
var green = (text) => import_picocolors.default.green(text);
|
|
2756
|
+
var yellow = (text) => import_picocolors.default.yellow(text);
|
|
2757
|
+
var red = (text) => import_picocolors.default.red(text);
|
|
2758
|
+
var cyan = (text) => import_picocolors.default.cyan(text);
|
|
2759
|
+
var magenta = (text) => import_picocolors.default.magenta(text);
|
|
2760
|
+
function header(text) {
|
|
2761
|
+
console.log("");
|
|
2762
|
+
console.log(brand(` ${text}`));
|
|
2763
|
+
console.log(import_picocolors.default.dim(" " + "─".repeat(text.length + 2)));
|
|
2764
|
+
}
|
|
2765
|
+
function labelValue(label, value) {
|
|
2766
|
+
console.log(` ${import_picocolors.default.dim(label + ":")} ${value}`);
|
|
2767
|
+
}
|
|
2768
|
+
function statusLine(status, name, message, extra) {
|
|
2769
|
+
const icons = { ok: "✓", warning: "⚠", error: "✗" };
|
|
2770
|
+
const colors = { ok: import_picocolors.default.green, warning: import_picocolors.default.yellow, error: import_picocolors.default.red };
|
|
2771
|
+
let line = `${colors[status](icons[status])} ${name}: ${message}`;
|
|
2772
|
+
if (extra) {
|
|
2773
|
+
line += ` ${import_picocolors.default.dim(`(${extra})`)}`;
|
|
2774
|
+
}
|
|
2775
|
+
return line;
|
|
2776
|
+
}
|
|
2777
|
+
function tableRow(columns, widths) {
|
|
2778
|
+
return columns.map((col, i) => {
|
|
2779
|
+
const width = widths[i] || col.length;
|
|
2780
|
+
return col.padEnd(width);
|
|
2781
|
+
}).join(" ");
|
|
2782
|
+
}
|
|
2783
|
+
function tableHeader(columns, widths) {
|
|
2784
|
+
const headerRow = tableRow(columns.map((c) => import_picocolors.default.bold(c)), widths);
|
|
2785
|
+
const underlineRow = widths.map((w) => "─".repeat(w)).join(" ");
|
|
2786
|
+
console.log(headerRow);
|
|
2787
|
+
console.log(import_picocolors.default.dim(underlineRow));
|
|
2788
|
+
}
|
|
2789
|
+
function url(urlStr) {
|
|
2790
|
+
return import_picocolors.default.underline(import_picocolors.default.cyan(urlStr));
|
|
2791
|
+
}
|
|
2792
|
+
function cmd(command) {
|
|
2793
|
+
return import_picocolors.default.cyan(command);
|
|
2794
|
+
}
|
|
2795
|
+
function file(filePath) {
|
|
2796
|
+
return import_picocolors.default.magenta(filePath);
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2677
2799
|
// lib/spawn.ts
|
|
2678
|
-
async function spawn(
|
|
2679
|
-
const command =
|
|
2680
|
-
const args =
|
|
2800
|
+
async function spawn(cmd2, opts = {}) {
|
|
2801
|
+
const command = cmd2[0];
|
|
2802
|
+
const args = cmd2.slice(1);
|
|
2681
2803
|
if (command === undefined) {
|
|
2682
2804
|
throw new Error("No command provided");
|
|
2683
2805
|
}
|
|
2684
2806
|
if (typeof Bun !== "undefined") {
|
|
2685
|
-
const proc = Bun.spawn(
|
|
2807
|
+
const proc = Bun.spawn(cmd2, {
|
|
2686
2808
|
env: opts.env ?? process.env,
|
|
2687
2809
|
cwd: opts.cwd ?? process.cwd(),
|
|
2688
2810
|
stdin: opts.stdin ?? "inherit",
|
|
@@ -2702,14 +2824,14 @@ async function spawn(cmd, opts = {}) {
|
|
|
2702
2824
|
});
|
|
2703
2825
|
}
|
|
2704
2826
|
}
|
|
2705
|
-
async function spawnCapture(
|
|
2706
|
-
const command =
|
|
2707
|
-
const args =
|
|
2827
|
+
async function spawnCapture(cmd2, opts = {}) {
|
|
2828
|
+
const command = cmd2[0];
|
|
2829
|
+
const args = cmd2.slice(1);
|
|
2708
2830
|
if (command === undefined) {
|
|
2709
2831
|
throw new Error("No command provided");
|
|
2710
2832
|
}
|
|
2711
2833
|
if (typeof Bun !== "undefined") {
|
|
2712
|
-
const proc = Bun.spawn(
|
|
2834
|
+
const proc = Bun.spawn(cmd2, {
|
|
2713
2835
|
env: opts.env ?? process.env,
|
|
2714
2836
|
cwd: opts.cwd,
|
|
2715
2837
|
stdin: opts.stdin ?? "ignore",
|
|
@@ -2744,17 +2866,17 @@ async function spawnCapture(cmd, opts = {}) {
|
|
|
2744
2866
|
});
|
|
2745
2867
|
}
|
|
2746
2868
|
}
|
|
2747
|
-
async function commandExists(
|
|
2869
|
+
async function commandExists(cmd2) {
|
|
2748
2870
|
try {
|
|
2749
|
-
const result = await spawnCapture(process.platform === "win32" ? ["where",
|
|
2871
|
+
const result = await spawnCapture(process.platform === "win32" ? ["where", cmd2] : ["which", cmd2]);
|
|
2750
2872
|
return result.exitCode === 0;
|
|
2751
2873
|
} catch {
|
|
2752
2874
|
return false;
|
|
2753
2875
|
}
|
|
2754
2876
|
}
|
|
2755
|
-
async function getCommandVersion(
|
|
2877
|
+
async function getCommandVersion(cmd2) {
|
|
2756
2878
|
try {
|
|
2757
|
-
const result = await spawnCapture([
|
|
2879
|
+
const result = await spawnCapture([cmd2, "--version"]);
|
|
2758
2880
|
if (result.exitCode === 0 && result.stdout) {
|
|
2759
2881
|
return result.stdout.trim().split(`
|
|
2760
2882
|
`)[0] ?? null;
|
|
@@ -2775,33 +2897,100 @@ async function fetchOllamaModels() {
|
|
|
2775
2897
|
const data = await response.json();
|
|
2776
2898
|
return data.models ?? [];
|
|
2777
2899
|
}
|
|
2900
|
+
async function fetchRunningModels() {
|
|
2901
|
+
const ollamaUrl = getOllamaUrl();
|
|
2902
|
+
try {
|
|
2903
|
+
const response = await fetch(`${ollamaUrl}/api/ps`, {
|
|
2904
|
+
signal: AbortSignal.timeout(5000)
|
|
2905
|
+
});
|
|
2906
|
+
if (!response.ok) {
|
|
2907
|
+
return [];
|
|
2908
|
+
}
|
|
2909
|
+
const data = await response.json();
|
|
2910
|
+
return data.models ?? [];
|
|
2911
|
+
} catch (error2) {
|
|
2912
|
+
return [];
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
async function isModelLoaded(modelName) {
|
|
2916
|
+
const runningModels = await fetchRunningModels();
|
|
2917
|
+
return runningModels.some((m) => m.model === modelName || m.name === modelName || m.model.startsWith(modelName + ":") || modelName.startsWith(m.model));
|
|
2918
|
+
}
|
|
2919
|
+
async function loadModel(modelName, keepAlive = "10m") {
|
|
2920
|
+
const ollamaUrl = getOllamaUrl();
|
|
2921
|
+
const response = await fetch(`${ollamaUrl}/api/generate`, {
|
|
2922
|
+
method: "POST",
|
|
2923
|
+
headers: {
|
|
2924
|
+
"Content-Type": "application/json"
|
|
2925
|
+
},
|
|
2926
|
+
body: JSON.stringify({
|
|
2927
|
+
model: modelName,
|
|
2928
|
+
prompt: "",
|
|
2929
|
+
stream: false,
|
|
2930
|
+
keep_alive: keepAlive
|
|
2931
|
+
})
|
|
2932
|
+
});
|
|
2933
|
+
if (!response.ok) {
|
|
2934
|
+
throw new Error(`Failed to load model: ${response.statusText}`);
|
|
2935
|
+
}
|
|
2936
|
+
await response.json();
|
|
2937
|
+
}
|
|
2938
|
+
async function ensureModelLoaded(modelName) {
|
|
2939
|
+
const isLoaded = await isModelLoaded(modelName);
|
|
2940
|
+
if (isLoaded) {
|
|
2941
|
+
console.log(dim(` Model ${magenta(modelName)} is already loaded`));
|
|
2942
|
+
return;
|
|
2943
|
+
}
|
|
2944
|
+
console.log(info(`Loading model ${magenta(modelName)}...`));
|
|
2945
|
+
console.log(dim(" This may take a moment on first run"));
|
|
2946
|
+
try {
|
|
2947
|
+
await loadModel(modelName, "10m");
|
|
2948
|
+
console.log(success(`Model ${magenta(modelName)} loaded (keep_alive: 10m)`));
|
|
2949
|
+
} catch (error2) {
|
|
2950
|
+
console.log(warn(`Could not pre-load model (will load on first request)`));
|
|
2951
|
+
console.log(dim(` ${error2 instanceof Error ? error2.message : "Unknown error"}`));
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2778
2954
|
async function selectModelInteractively() {
|
|
2779
2955
|
const ollamaUrl = getOllamaUrl();
|
|
2780
2956
|
let models;
|
|
2781
2957
|
try {
|
|
2782
2958
|
models = await fetchOllamaModels();
|
|
2783
|
-
} catch (
|
|
2784
|
-
console.
|
|
2785
|
-
console.
|
|
2959
|
+
} catch (error2) {
|
|
2960
|
+
console.log(warn(`Could not connect to Ollama at ${ollamaUrl}`));
|
|
2961
|
+
console.log(dim(" Make sure Ollama is running: loclaude docker-up"));
|
|
2786
2962
|
process.exit(1);
|
|
2787
2963
|
}
|
|
2788
2964
|
if (models.length === 0) {
|
|
2789
|
-
console.
|
|
2790
|
-
console.
|
|
2965
|
+
console.log(warn("No models found in Ollama."));
|
|
2966
|
+
console.log(dim(" Pull a model first: loclaude models-pull <model-name>"));
|
|
2791
2967
|
process.exit(1);
|
|
2792
2968
|
}
|
|
2969
|
+
const runningModels = await fetchRunningModels();
|
|
2970
|
+
const loadedModelNames = new Set(runningModels.map((m) => m.model));
|
|
2793
2971
|
const selected = await dist_default3({
|
|
2794
2972
|
message: "Select a model",
|
|
2795
|
-
choices: models.map((model) =>
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2973
|
+
choices: models.map((model) => {
|
|
2974
|
+
const isLoaded = loadedModelNames.has(model.name);
|
|
2975
|
+
const loadedIndicator = isLoaded ? " [loaded]" : "";
|
|
2976
|
+
return {
|
|
2977
|
+
name: `${model.name} (${import_bytes.default(model.size)})${loadedIndicator}`,
|
|
2978
|
+
value: model.name
|
|
2979
|
+
};
|
|
2980
|
+
})
|
|
2799
2981
|
});
|
|
2800
2982
|
return selected;
|
|
2801
2983
|
}
|
|
2802
2984
|
async function launchClaude(model, passthroughArgs) {
|
|
2803
2985
|
const ollamaUrl = getOllamaUrl();
|
|
2804
2986
|
const extraArgs = getClaudeExtraArgs();
|
|
2987
|
+
console.log("");
|
|
2988
|
+
console.log(cyan("Launching Claude Code with Ollama"));
|
|
2989
|
+
console.log(dim(` Model: ${magenta(model)}`));
|
|
2990
|
+
console.log(dim(` API: ${ollamaUrl}`));
|
|
2991
|
+
console.log("");
|
|
2992
|
+
await ensureModelLoaded(model);
|
|
2993
|
+
console.log("");
|
|
2805
2994
|
const env = {
|
|
2806
2995
|
...process.env,
|
|
2807
2996
|
ANTHROPIC_AUTH_TOKEN: "ollama",
|
|
@@ -2815,51 +3004,351 @@ async function launchClaude(model, passthroughArgs) {
|
|
|
2815
3004
|
// lib/commands/init.ts
|
|
2816
3005
|
import { existsSync as existsSync2, mkdirSync, writeFileSync, readFileSync as readFileSync2 } from "fs";
|
|
2817
3006
|
import { join as join2 } from "path";
|
|
2818
|
-
|
|
3007
|
+
|
|
3008
|
+
// lib/commands/doctor.ts
|
|
3009
|
+
async function checkDocker() {
|
|
3010
|
+
const exists = await commandExists("docker");
|
|
3011
|
+
if (!exists) {
|
|
3012
|
+
return {
|
|
3013
|
+
name: "Docker",
|
|
3014
|
+
status: "error",
|
|
3015
|
+
message: "Not installed",
|
|
3016
|
+
hint: "Install Docker: https://docs.docker.com/get-docker/"
|
|
3017
|
+
};
|
|
3018
|
+
}
|
|
3019
|
+
const version = await getCommandVersion("docker");
|
|
3020
|
+
return {
|
|
3021
|
+
name: "Docker",
|
|
3022
|
+
status: "ok",
|
|
3023
|
+
message: "Installed",
|
|
3024
|
+
version: version ?? undefined
|
|
3025
|
+
};
|
|
3026
|
+
}
|
|
3027
|
+
async function checkDockerCompose() {
|
|
3028
|
+
const result = await spawnCapture(["docker", "compose", "version"]);
|
|
3029
|
+
if (result.exitCode === 0) {
|
|
3030
|
+
const version = result.stdout?.trim().split(`
|
|
3031
|
+
`)[0];
|
|
3032
|
+
return {
|
|
3033
|
+
name: "Docker Compose",
|
|
3034
|
+
status: "ok",
|
|
3035
|
+
message: "Installed (v2)",
|
|
3036
|
+
version: version ?? undefined
|
|
3037
|
+
};
|
|
3038
|
+
}
|
|
3039
|
+
const v1Exists = await commandExists("docker-compose");
|
|
3040
|
+
if (v1Exists) {
|
|
3041
|
+
const version = await getCommandVersion("docker-compose");
|
|
3042
|
+
return {
|
|
3043
|
+
name: "Docker Compose",
|
|
3044
|
+
status: "warning",
|
|
3045
|
+
message: "Using legacy v1",
|
|
3046
|
+
version: version ?? undefined,
|
|
3047
|
+
hint: "Consider upgrading to Docker Compose v2"
|
|
3048
|
+
};
|
|
3049
|
+
}
|
|
3050
|
+
return {
|
|
3051
|
+
name: "Docker Compose",
|
|
3052
|
+
status: "error",
|
|
3053
|
+
message: "Not installed",
|
|
3054
|
+
hint: "Docker Compose is included with Docker Desktop, or install separately"
|
|
3055
|
+
};
|
|
3056
|
+
}
|
|
3057
|
+
async function checkNvidiaSmi() {
|
|
3058
|
+
const exists = await commandExists("nvidia-smi");
|
|
3059
|
+
if (!exists) {
|
|
3060
|
+
return {
|
|
3061
|
+
name: "NVIDIA GPU",
|
|
3062
|
+
status: "warning",
|
|
3063
|
+
message: "nvidia-smi not found",
|
|
3064
|
+
hint: "GPU support requires NVIDIA drivers. CPU-only mode will be used."
|
|
3065
|
+
};
|
|
3066
|
+
}
|
|
3067
|
+
const result = await spawnCapture(["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"]);
|
|
3068
|
+
if (result.exitCode === 0 && result.stdout) {
|
|
3069
|
+
const gpus = result.stdout.trim().split(`
|
|
3070
|
+
`).filter(Boolean);
|
|
3071
|
+
return {
|
|
3072
|
+
name: "NVIDIA GPU",
|
|
3073
|
+
status: "ok",
|
|
3074
|
+
message: `${gpus.length} GPU(s) detected`,
|
|
3075
|
+
version: gpus[0]
|
|
3076
|
+
};
|
|
3077
|
+
}
|
|
3078
|
+
return {
|
|
3079
|
+
name: "NVIDIA GPU",
|
|
3080
|
+
status: "warning",
|
|
3081
|
+
message: "nvidia-smi failed",
|
|
3082
|
+
hint: "GPU may not be available. Check NVIDIA drivers."
|
|
3083
|
+
};
|
|
3084
|
+
}
|
|
3085
|
+
async function checkNvidiaContainerToolkit() {
|
|
3086
|
+
const result = await spawnCapture(["docker", "info", "--format", "{{.Runtimes}}"]);
|
|
3087
|
+
if (result.exitCode === 0 && result.stdout?.includes("nvidia")) {
|
|
3088
|
+
return {
|
|
3089
|
+
name: "NVIDIA Container Toolkit",
|
|
3090
|
+
status: "ok",
|
|
3091
|
+
message: "nvidia runtime available"
|
|
3092
|
+
};
|
|
3093
|
+
}
|
|
3094
|
+
return {
|
|
3095
|
+
name: "NVIDIA Container Toolkit",
|
|
3096
|
+
status: "warning",
|
|
3097
|
+
message: "nvidia runtime not found",
|
|
3098
|
+
hint: "Install: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html"
|
|
3099
|
+
};
|
|
3100
|
+
}
|
|
3101
|
+
async function checkClaude() {
|
|
3102
|
+
const exists = await commandExists("claude");
|
|
3103
|
+
if (!exists) {
|
|
3104
|
+
return {
|
|
3105
|
+
name: "Claude Code",
|
|
3106
|
+
status: "error",
|
|
3107
|
+
message: "Not installed",
|
|
3108
|
+
hint: "Install: npm install -g @anthropic-ai/claude-code"
|
|
3109
|
+
};
|
|
3110
|
+
}
|
|
3111
|
+
const version = await getCommandVersion("claude");
|
|
3112
|
+
return {
|
|
3113
|
+
name: "Claude Code",
|
|
3114
|
+
status: "ok",
|
|
3115
|
+
message: "Installed",
|
|
3116
|
+
version: version ?? undefined
|
|
3117
|
+
};
|
|
3118
|
+
}
|
|
3119
|
+
async function checkOllamaConnection() {
|
|
3120
|
+
const ollamaUrl = getOllamaUrl();
|
|
3121
|
+
try {
|
|
3122
|
+
const response = await fetch(`${ollamaUrl}/api/tags`, {
|
|
3123
|
+
signal: AbortSignal.timeout(5000)
|
|
3124
|
+
});
|
|
3125
|
+
if (response.ok) {
|
|
3126
|
+
const data = await response.json();
|
|
3127
|
+
const modelCount = data.models?.length ?? 0;
|
|
3128
|
+
return {
|
|
3129
|
+
name: "Ollama API",
|
|
3130
|
+
status: "ok",
|
|
3131
|
+
message: `Connected (${modelCount} model${modelCount === 1 ? "" : "s"})`,
|
|
3132
|
+
version: ollamaUrl
|
|
3133
|
+
};
|
|
3134
|
+
}
|
|
3135
|
+
return {
|
|
3136
|
+
name: "Ollama API",
|
|
3137
|
+
status: "warning",
|
|
3138
|
+
message: `HTTP ${response.status}`,
|
|
3139
|
+
hint: "Ollama may not be running. Try: loclaude docker-up"
|
|
3140
|
+
};
|
|
3141
|
+
} catch (error3) {
|
|
3142
|
+
return {
|
|
3143
|
+
name: "Ollama API",
|
|
3144
|
+
status: "warning",
|
|
3145
|
+
message: "Not reachable",
|
|
3146
|
+
hint: `Cannot connect to ${ollamaUrl}. Start Ollama: loclaude docker-up`
|
|
3147
|
+
};
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
function formatCheck(check) {
|
|
3151
|
+
let line = statusLine(check.status, check.name, check.message, check.version);
|
|
3152
|
+
if (check.hint) {
|
|
3153
|
+
line += `
|
|
3154
|
+
${dim("→")} ${dim(check.hint)}`;
|
|
3155
|
+
}
|
|
3156
|
+
return line;
|
|
3157
|
+
}
|
|
3158
|
+
async function doctor() {
|
|
3159
|
+
header("System Health Check");
|
|
3160
|
+
console.log("");
|
|
3161
|
+
const checks = await Promise.all([
|
|
3162
|
+
checkDocker(),
|
|
3163
|
+
checkDockerCompose(),
|
|
3164
|
+
checkNvidiaSmi(),
|
|
3165
|
+
checkNvidiaContainerToolkit(),
|
|
3166
|
+
checkClaude(),
|
|
3167
|
+
checkOllamaConnection()
|
|
3168
|
+
]);
|
|
3169
|
+
for (const check of checks) {
|
|
3170
|
+
console.log(formatCheck(check));
|
|
3171
|
+
}
|
|
3172
|
+
const errors2 = checks.filter((c) => c.status === "error");
|
|
3173
|
+
const warnings = checks.filter((c) => c.status === "warning");
|
|
3174
|
+
console.log("");
|
|
3175
|
+
if (errors2.length > 0) {
|
|
3176
|
+
console.log(red(`${errors2.length} error(s) found.`) + " Fix these before proceeding.");
|
|
3177
|
+
process.exit(1);
|
|
3178
|
+
} else if (warnings.length > 0) {
|
|
3179
|
+
console.log(yellow(`${warnings.length} warning(s).`) + " loclaude may work with limited functionality.");
|
|
3180
|
+
} else {
|
|
3181
|
+
console.log(green("All checks passed!") + " Ready to use loclaude.");
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
async function hasNvidiaGpu() {
|
|
3185
|
+
const exists = await commandExists("nvidia-smi");
|
|
3186
|
+
if (!exists)
|
|
3187
|
+
return false;
|
|
3188
|
+
const result = await spawnCapture(["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"]);
|
|
3189
|
+
return result.exitCode === 0 && Boolean(result.stdout?.trim());
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
// lib/commands/init.ts
|
|
3193
|
+
var DOCKER_COMPOSE_TEMPLATE_GPU = `# =============================================================================
|
|
3194
|
+
# LOCLAUDE DOCKER COMPOSE - GPU MODE
|
|
3195
|
+
# =============================================================================
|
|
3196
|
+
# This configuration runs Ollama with NVIDIA GPU acceleration for fast inference.
|
|
3197
|
+
# Generated by: loclaude init
|
|
3198
|
+
#
|
|
3199
|
+
# Prerequisites:
|
|
3200
|
+
# - NVIDIA GPU with CUDA support
|
|
3201
|
+
# - NVIDIA drivers installed on host
|
|
3202
|
+
# - NVIDIA Container Toolkit: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit
|
|
3203
|
+
#
|
|
3204
|
+
# Quick test for GPU support:
|
|
3205
|
+
# docker run --rm --gpus all nvidia/cuda:12.0-base nvidia-smi
|
|
3206
|
+
#
|
|
3207
|
+
# =============================================================================
|
|
3208
|
+
|
|
3209
|
+
services:
|
|
3210
|
+
# ===========================================================================
|
|
3211
|
+
# OLLAMA - Local LLM Inference Server
|
|
3212
|
+
# ===========================================================================
|
|
3213
|
+
# Ollama provides the AI backend that Claude Code connects to.
|
|
3214
|
+
# It runs large language models locally on your hardware.
|
|
3215
|
+
#
|
|
3216
|
+
# API Documentation: https://github.com/ollama/ollama/blob/main/docs/api.md
|
|
3217
|
+
# Model Library: https://ollama.com/library
|
|
3218
|
+
# ===========================================================================
|
|
2819
3219
|
ollama:
|
|
3220
|
+
# Official Ollama image - 'latest' ensures newest features and model support
|
|
2820
3221
|
image: ollama/ollama:latest
|
|
3222
|
+
|
|
3223
|
+
# Fixed container name for easy CLI access:
|
|
3224
|
+
# docker exec ollama ollama list
|
|
3225
|
+
# docker logs ollama
|
|
2821
3226
|
container_name: ollama
|
|
3227
|
+
|
|
3228
|
+
# NVIDIA Container Runtime - Required for GPU access
|
|
3229
|
+
# This makes CUDA libraries available inside the container
|
|
2822
3230
|
runtime: nvidia
|
|
3231
|
+
|
|
2823
3232
|
environment:
|
|
3233
|
+
# ---------------------------------------------------------------------------
|
|
3234
|
+
# GPU Configuration
|
|
3235
|
+
# ---------------------------------------------------------------------------
|
|
3236
|
+
# NVIDIA_VISIBLE_DEVICES: Which GPUs to expose to the container
|
|
3237
|
+
# - 'all': Use all available GPUs (recommended for most setups)
|
|
3238
|
+
# - '0': Use only GPU 0
|
|
3239
|
+
# - '0,1': Use GPUs 0 and 1
|
|
2824
3240
|
- NVIDIA_VISIBLE_DEVICES=all
|
|
3241
|
+
|
|
3242
|
+
# NVIDIA_DRIVER_CAPABILITIES: What GPU features to enable
|
|
3243
|
+
# - 'compute': CUDA compute (required for inference)
|
|
3244
|
+
# - 'utility': nvidia-smi and other tools
|
|
2825
3245
|
- NVIDIA_DRIVER_CAPABILITIES=compute,utility
|
|
3246
|
+
|
|
3247
|
+
# ---------------------------------------------------------------------------
|
|
3248
|
+
# Ollama Configuration (Optional)
|
|
3249
|
+
# ---------------------------------------------------------------------------
|
|
3250
|
+
# Uncomment these to customize Ollama behavior:
|
|
3251
|
+
|
|
3252
|
+
# Maximum number of models loaded in memory simultaneously
|
|
3253
|
+
# Lower this if you're running out of VRAM
|
|
3254
|
+
# - OLLAMA_MAX_LOADED_MODELS=1
|
|
3255
|
+
|
|
3256
|
+
# Maximum parallel inference requests per model
|
|
3257
|
+
# Higher values use more VRAM but handle more concurrent requests
|
|
3258
|
+
# - OLLAMA_NUM_PARALLEL=1
|
|
3259
|
+
|
|
3260
|
+
# Enable debug logging for troubleshooting
|
|
3261
|
+
# - OLLAMA_DEBUG=1
|
|
3262
|
+
|
|
3263
|
+
# Custom model storage location (inside container)
|
|
3264
|
+
# - OLLAMA_MODELS=/root/.ollama
|
|
3265
|
+
|
|
2826
3266
|
volumes:
|
|
3267
|
+
# ---------------------------------------------------------------------------
|
|
3268
|
+
# Model Storage
|
|
3269
|
+
# ---------------------------------------------------------------------------
|
|
3270
|
+
# Maps ./models on your host to /root/.ollama in the container
|
|
3271
|
+
# This persists downloaded models across container restarts
|
|
3272
|
+
#
|
|
3273
|
+
# Disk space requirements (approximate):
|
|
3274
|
+
# - 7B model: ~4GB
|
|
3275
|
+
# - 13B model: ~8GB
|
|
3276
|
+
# - 30B model: ~16GB
|
|
3277
|
+
# - 70B model: ~40GB
|
|
2827
3278
|
- ./models:/root/.ollama
|
|
3279
|
+
|
|
2828
3280
|
ports:
|
|
3281
|
+
# Ollama API port - access at http://localhost:11434
|
|
3282
|
+
# Used by Claude Code and other Ollama clients
|
|
2829
3283
|
- "11434:11434"
|
|
3284
|
+
|
|
3285
|
+
# Restart policy - keeps Ollama running unless manually stopped
|
|
2830
3286
|
restart: unless-stopped
|
|
3287
|
+
|
|
2831
3288
|
healthcheck:
|
|
3289
|
+
# Verify Ollama is responsive by listing models
|
|
2832
3290
|
test: ["CMD", "ollama", "list"]
|
|
2833
|
-
interval: 300s
|
|
2834
|
-
timeout: 2s
|
|
2835
|
-
retries: 3
|
|
2836
|
-
start_period: 40s
|
|
3291
|
+
interval: 300s # Check every 5 minutes
|
|
3292
|
+
timeout: 2s # Fail if no response in 2 seconds
|
|
3293
|
+
retries: 3 # Mark unhealthy after 3 consecutive failures
|
|
3294
|
+
start_period: 40s # Grace period for initial model loading
|
|
3295
|
+
|
|
2837
3296
|
deploy:
|
|
2838
3297
|
resources:
|
|
2839
3298
|
reservations:
|
|
2840
3299
|
devices:
|
|
3300
|
+
# Request GPU access from Docker
|
|
2841
3301
|
- driver: nvidia
|
|
2842
|
-
count: all
|
|
2843
|
-
capabilities: [gpu]
|
|
3302
|
+
count: all # Use all available GPUs
|
|
3303
|
+
capabilities: [gpu] # Request GPU compute capability
|
|
2844
3304
|
|
|
3305
|
+
# ===========================================================================
|
|
3306
|
+
# OPEN WEBUI - Chat Interface (Optional)
|
|
3307
|
+
# ===========================================================================
|
|
3308
|
+
# Open WebUI provides a ChatGPT-like interface for your local models.
|
|
3309
|
+
# Access at http://localhost:3000 after starting containers.
|
|
3310
|
+
#
|
|
3311
|
+
# Features:
|
|
3312
|
+
# - Multi-model chat interface
|
|
3313
|
+
# - Conversation history
|
|
3314
|
+
# - Model management UI
|
|
3315
|
+
# - RAG/document upload support
|
|
3316
|
+
#
|
|
3317
|
+
# Documentation: https://docs.openwebui.com/
|
|
3318
|
+
# ===========================================================================
|
|
2845
3319
|
open-webui:
|
|
3320
|
+
# CUDA-enabled image for GPU-accelerated features (embeddings, etc.)
|
|
3321
|
+
# Change to :main if you don't need GPU features in the UI
|
|
2846
3322
|
image: ghcr.io/open-webui/open-webui:cuda
|
|
3323
|
+
|
|
2847
3324
|
container_name: open-webui
|
|
3325
|
+
|
|
2848
3326
|
ports:
|
|
3327
|
+
# Web UI port - access at http://localhost:3000
|
|
2849
3328
|
- "3000:8080"
|
|
3329
|
+
|
|
2850
3330
|
environment:
|
|
3331
|
+
# Tell Open WebUI where to find Ollama
|
|
3332
|
+
# Uses Docker internal networking (service name as hostname)
|
|
2851
3333
|
- OLLAMA_BASE_URL=http://ollama:11434
|
|
3334
|
+
|
|
3335
|
+
# Wait for Ollama to be ready before starting
|
|
2852
3336
|
depends_on:
|
|
2853
3337
|
- ollama
|
|
3338
|
+
|
|
2854
3339
|
restart: unless-stopped
|
|
3340
|
+
|
|
2855
3341
|
healthcheck:
|
|
2856
3342
|
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
|
|
2857
3343
|
interval: 30s
|
|
2858
3344
|
timeout: 10s
|
|
2859
3345
|
retries: 3
|
|
2860
3346
|
start_period: 60s
|
|
3347
|
+
|
|
2861
3348
|
volumes:
|
|
3349
|
+
# Persistent storage for conversations, settings, and user data
|
|
2862
3350
|
- open-webui:/app/backend/data
|
|
3351
|
+
|
|
2863
3352
|
deploy:
|
|
2864
3353
|
resources:
|
|
2865
3354
|
reservations:
|
|
@@ -2868,32 +3357,174 @@ var DOCKER_COMPOSE_TEMPLATE = `services:
|
|
|
2868
3357
|
count: all
|
|
2869
3358
|
capabilities: [gpu]
|
|
2870
3359
|
|
|
3360
|
+
# =============================================================================
|
|
3361
|
+
# VOLUMES
|
|
3362
|
+
# =============================================================================
|
|
3363
|
+
# Named volumes for persistent data that survives container recreation
|
|
2871
3364
|
volumes:
|
|
2872
3365
|
open-webui:
|
|
3366
|
+
# Open WebUI data: conversations, user settings, uploads
|
|
3367
|
+
# Located at /var/lib/docker/volumes/open-webui/_data on host
|
|
2873
3368
|
`;
|
|
2874
|
-
var
|
|
2875
|
-
|
|
3369
|
+
var DOCKER_COMPOSE_TEMPLATE_CPU = `# =============================================================================
|
|
3370
|
+
# LOCLAUDE DOCKER COMPOSE - CPU MODE
|
|
3371
|
+
# =============================================================================
|
|
3372
|
+
# This configuration runs Ollama in CPU-only mode.
|
|
3373
|
+
# Inference will be slower than GPU mode but works on any system.
|
|
3374
|
+
# Generated by: loclaude init --no-gpu
|
|
3375
|
+
#
|
|
3376
|
+
# Performance notes:
|
|
3377
|
+
# - 7B models: ~10-20 tokens/sec on modern CPUs
|
|
3378
|
+
# - Larger models will be significantly slower
|
|
3379
|
+
# - Consider using quantized models (Q4_K_M, Q5_K_M) for better performance
|
|
3380
|
+
#
|
|
3381
|
+
# Recommended CPU-optimized models:
|
|
3382
|
+
# - llama3.2:3b (fast, good for simple tasks)
|
|
3383
|
+
# - qwen2.5-coder:7b (coding tasks)
|
|
3384
|
+
# - gemma2:9b (general purpose)
|
|
3385
|
+
#
|
|
3386
|
+
# =============================================================================
|
|
3387
|
+
|
|
3388
|
+
services:
|
|
3389
|
+
# ===========================================================================
|
|
3390
|
+
# OLLAMA - Local LLM Inference Server (CPU Mode)
|
|
3391
|
+
# ===========================================================================
|
|
3392
|
+
# Ollama provides the AI backend that Claude Code connects to.
|
|
3393
|
+
# Running in CPU mode - no GPU acceleration.
|
|
3394
|
+
#
|
|
3395
|
+
# API Documentation: https://github.com/ollama/ollama/blob/main/docs/api.md
|
|
3396
|
+
# Model Library: https://ollama.com/library
|
|
3397
|
+
# ===========================================================================
|
|
3398
|
+
ollama:
|
|
3399
|
+
# Official Ollama image - works for both CPU and GPU
|
|
3400
|
+
image: ollama/ollama:latest
|
|
3401
|
+
|
|
3402
|
+
# Fixed container name for easy CLI access
|
|
3403
|
+
container_name: ollama
|
|
3404
|
+
|
|
3405
|
+
# NOTE: No 'runtime: nvidia' - running in CPU mode
|
|
3406
|
+
|
|
3407
|
+
environment:
|
|
3408
|
+
# ---------------------------------------------------------------------------
|
|
3409
|
+
# Ollama Configuration (Optional)
|
|
3410
|
+
# ---------------------------------------------------------------------------
|
|
3411
|
+
# Uncomment these to customize Ollama behavior:
|
|
3412
|
+
|
|
3413
|
+
# Maximum number of models loaded in memory simultaneously
|
|
3414
|
+
# CPU mode uses system RAM instead of VRAM
|
|
3415
|
+
# - OLLAMA_MAX_LOADED_MODELS=1
|
|
3416
|
+
|
|
3417
|
+
# Number of CPU threads to use (default: auto-detect)
|
|
3418
|
+
# - OLLAMA_NUM_THREADS=8
|
|
3419
|
+
|
|
3420
|
+
# Enable debug logging for troubleshooting
|
|
3421
|
+
# - OLLAMA_DEBUG=1
|
|
3422
|
+
|
|
3423
|
+
volumes:
|
|
3424
|
+
# ---------------------------------------------------------------------------
|
|
3425
|
+
# Model Storage
|
|
3426
|
+
# ---------------------------------------------------------------------------
|
|
3427
|
+
# Maps ./models on your host to /root/.ollama in the container
|
|
3428
|
+
# This persists downloaded models across container restarts
|
|
3429
|
+
- ./models:/root/.ollama
|
|
3430
|
+
|
|
3431
|
+
ports:
|
|
3432
|
+
# Ollama API port - access at http://localhost:11434
|
|
3433
|
+
- "11434:11434"
|
|
3434
|
+
|
|
3435
|
+
restart: unless-stopped
|
|
3436
|
+
|
|
3437
|
+
healthcheck:
|
|
3438
|
+
test: ["CMD", "ollama", "list"]
|
|
3439
|
+
interval: 300s
|
|
3440
|
+
timeout: 2s
|
|
3441
|
+
retries: 3
|
|
3442
|
+
start_period: 40s
|
|
3443
|
+
|
|
3444
|
+
# CPU resource limits (optional - uncomment to constrain)
|
|
3445
|
+
# deploy:
|
|
3446
|
+
# resources:
|
|
3447
|
+
# limits:
|
|
3448
|
+
# cpus: '4' # Limit to 4 CPU cores
|
|
3449
|
+
# memory: 16G # Limit to 16GB RAM
|
|
3450
|
+
# reservations:
|
|
3451
|
+
# cpus: '2' # Reserve at least 2 cores
|
|
3452
|
+
# memory: 8G # Reserve at least 8GB RAM
|
|
3453
|
+
|
|
3454
|
+
# ===========================================================================
|
|
3455
|
+
# OPEN WEBUI - Chat Interface (Optional)
|
|
3456
|
+
# ===========================================================================
|
|
3457
|
+
# Open WebUI provides a ChatGPT-like interface for your local models.
|
|
3458
|
+
# Access at http://localhost:3000 after starting containers.
|
|
3459
|
+
#
|
|
3460
|
+
# Documentation: https://docs.openwebui.com/
|
|
3461
|
+
# ===========================================================================
|
|
3462
|
+
open-webui:
|
|
3463
|
+
# Standard image (no CUDA) - smaller download, CPU-only features
|
|
3464
|
+
image: ghcr.io/open-webui/open-webui:main
|
|
3465
|
+
|
|
3466
|
+
container_name: open-webui
|
|
3467
|
+
|
|
3468
|
+
ports:
|
|
3469
|
+
- "3000:8080"
|
|
3470
|
+
|
|
3471
|
+
environment:
|
|
3472
|
+
- OLLAMA_BASE_URL=http://ollama:11434
|
|
3473
|
+
|
|
3474
|
+
depends_on:
|
|
3475
|
+
- ollama
|
|
3476
|
+
|
|
3477
|
+
restart: unless-stopped
|
|
3478
|
+
|
|
3479
|
+
healthcheck:
|
|
3480
|
+
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
|
|
3481
|
+
interval: 30s
|
|
3482
|
+
timeout: 10s
|
|
3483
|
+
retries: 3
|
|
3484
|
+
start_period: 60s
|
|
3485
|
+
|
|
3486
|
+
volumes:
|
|
3487
|
+
- open-webui:/app/backend/data
|
|
3488
|
+
|
|
3489
|
+
# =============================================================================
|
|
3490
|
+
# VOLUMES
|
|
3491
|
+
# =============================================================================
|
|
3492
|
+
volumes:
|
|
3493
|
+
open-webui:
|
|
3494
|
+
`;
|
|
3495
|
+
function getConfigTemplate(gpu) {
|
|
3496
|
+
return `{
|
|
3497
|
+
"ollama": {
|
|
2876
3498
|
"url": "http://localhost:11434",
|
|
2877
|
-
"defaultModel": "qwen3-coder:30b"
|
|
3499
|
+
"defaultModel": "${gpu ? "qwen3-coder:30b" : "qwen2.5-coder:7b"}"
|
|
2878
3500
|
},
|
|
2879
3501
|
"docker": {
|
|
2880
3502
|
"composeFile": "./docker-compose.yml",
|
|
2881
|
-
"gpu":
|
|
3503
|
+
"gpu": ${gpu}
|
|
2882
3504
|
}
|
|
2883
3505
|
}
|
|
2884
3506
|
`;
|
|
3507
|
+
}
|
|
2885
3508
|
var GITIGNORE_TEMPLATE = `# Ollama models (large binary files)
|
|
3509
|
+
# These are downloaded by Ollama and can be re-pulled anytime
|
|
2886
3510
|
models/
|
|
2887
3511
|
`;
|
|
2888
|
-
var MISE_TOML_TEMPLATE = `#
|
|
2889
|
-
#
|
|
2890
|
-
#
|
|
3512
|
+
var MISE_TOML_TEMPLATE = `# =============================================================================
|
|
3513
|
+
# MISE TASK RUNNER CONFIGURATION
|
|
3514
|
+
# =============================================================================
|
|
3515
|
+
# Mise is a task runner that provides convenient shortcuts for common operations.
|
|
3516
|
+
# Run 'mise tasks' to see all available tasks.
|
|
3517
|
+
#
|
|
3518
|
+
# Documentation: https://mise.jdx.dev/
|
|
3519
|
+
# Install: curl https://mise.jdx.dev/install.sh | sh
|
|
3520
|
+
# =============================================================================
|
|
2891
3521
|
|
|
2892
3522
|
[tasks]
|
|
2893
3523
|
|
|
2894
3524
|
# =============================================================================
|
|
2895
3525
|
# Docker Management
|
|
2896
3526
|
# =============================================================================
|
|
3527
|
+
# Commands for managing the Ollama and Open WebUI containers
|
|
2897
3528
|
|
|
2898
3529
|
[tasks.up]
|
|
2899
3530
|
description = "Start Ollama and Open WebUI containers"
|
|
@@ -2918,6 +3549,7 @@ run = "loclaude docker-logs --follow"
|
|
|
2918
3549
|
# =============================================================================
|
|
2919
3550
|
# Model Management
|
|
2920
3551
|
# =============================================================================
|
|
3552
|
+
# Commands for managing Ollama models (download, remove, list)
|
|
2921
3553
|
|
|
2922
3554
|
[tasks.models]
|
|
2923
3555
|
description = "List installed models"
|
|
@@ -2927,9 +3559,14 @@ run = "loclaude models"
|
|
|
2927
3559
|
description = "Pull a model (usage: mise run pull <model-name>)"
|
|
2928
3560
|
run = "loclaude models-pull {{arg(name='model')}}"
|
|
2929
3561
|
|
|
3562
|
+
[tasks."pull:recommended"]
|
|
3563
|
+
description = "Pull the recommended coding model"
|
|
3564
|
+
run = "loclaude models-pull qwen3-coder:30b"
|
|
3565
|
+
|
|
2930
3566
|
# =============================================================================
|
|
2931
3567
|
# Claude Code
|
|
2932
3568
|
# =============================================================================
|
|
3569
|
+
# Commands for running Claude Code with local Ollama
|
|
2933
3570
|
|
|
2934
3571
|
[tasks.claude]
|
|
2935
3572
|
description = "Run Claude Code with local Ollama"
|
|
@@ -2942,14 +3579,19 @@ run = "loclaude run -m {{arg(name='model')}}"
|
|
|
2942
3579
|
# =============================================================================
|
|
2943
3580
|
# Diagnostics
|
|
2944
3581
|
# =============================================================================
|
|
3582
|
+
# Commands for checking system health and troubleshooting
|
|
2945
3583
|
|
|
2946
3584
|
[tasks.doctor]
|
|
2947
3585
|
description = "Check system requirements"
|
|
2948
3586
|
run = "loclaude doctor"
|
|
2949
3587
|
|
|
2950
3588
|
[tasks.gpu]
|
|
2951
|
-
description = "Check GPU status"
|
|
3589
|
+
description = "Check GPU status (requires NVIDIA GPU)"
|
|
2952
3590
|
run = "docker exec ollama nvidia-smi"
|
|
3591
|
+
|
|
3592
|
+
[tasks.config]
|
|
3593
|
+
description = "Show current configuration"
|
|
3594
|
+
run = "loclaude config"
|
|
2953
3595
|
`;
|
|
2954
3596
|
var README_TEMPLATE = `# Project Name
|
|
2955
3597
|
|
|
@@ -2958,18 +3600,24 @@ var README_TEMPLATE = `# Project Name
|
|
|
2958
3600
|
## Prerequisites
|
|
2959
3601
|
|
|
2960
3602
|
- [Docker](https://docs.docker.com/get-docker/) with Docker Compose v2
|
|
2961
|
-
- [NVIDIA GPU](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) with drivers and container toolkit
|
|
2962
3603
|
- [mise](https://mise.jdx.dev/) task runner (recommended)
|
|
2963
3604
|
- [loclaude](https://www.npmjs.com/package/loclaude) CLI (\`npm install -g loclaude\`)
|
|
2964
3605
|
|
|
3606
|
+
### For GPU Mode (Recommended)
|
|
3607
|
+
|
|
3608
|
+
- [NVIDIA GPU](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) with CUDA support
|
|
3609
|
+
- NVIDIA drivers installed on host
|
|
3610
|
+
- [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html)
|
|
3611
|
+
|
|
2965
3612
|
## Quick Start
|
|
2966
3613
|
|
|
2967
3614
|
\`\`\`bash
|
|
2968
3615
|
# Start the LLM backend (Ollama + Open WebUI)
|
|
2969
3616
|
mise run up
|
|
2970
3617
|
|
|
2971
|
-
# Pull a model
|
|
2972
|
-
mise run pull qwen3-coder:30b
|
|
3618
|
+
# Pull a model (adjust based on your hardware)
|
|
3619
|
+
mise run pull qwen3-coder:30b # GPU: 30B model (~16GB VRAM)
|
|
3620
|
+
mise run pull qwen2.5-coder:7b # CPU: 7B model (faster)
|
|
2973
3621
|
|
|
2974
3622
|
# Run Claude Code with local LLM
|
|
2975
3623
|
mise run claude
|
|
@@ -3004,7 +3652,7 @@ Run \`mise tasks\` to see all available commands.
|
|
|
3004
3652
|
\`\`\`
|
|
3005
3653
|
.
|
|
3006
3654
|
├── .claude/
|
|
3007
|
-
│ └── CLAUDE.md # Claude Code instructions
|
|
3655
|
+
│ └── CLAUDE.md # Claude Code project instructions
|
|
3008
3656
|
├── .loclaude/
|
|
3009
3657
|
│ └── config.json # Loclaude configuration
|
|
3010
3658
|
├── models/ # Ollama model storage (gitignored)
|
|
@@ -3036,6 +3684,25 @@ Run \`mise tasks\` to see all available commands.
|
|
|
3036
3684
|
|----------|-------------|---------|
|
|
3037
3685
|
| \`OLLAMA_URL\` | Ollama API endpoint | \`http://localhost:11434\` |
|
|
3038
3686
|
| \`OLLAMA_MODEL\` | Default model name | \`qwen3-coder:30b\` |
|
|
3687
|
+
| \`LOCLAUDE_GPU\` | Enable GPU mode | \`true\` |
|
|
3688
|
+
|
|
3689
|
+
## Recommended Models
|
|
3690
|
+
|
|
3691
|
+
### For GPU (NVIDIA with 16GB+ VRAM)
|
|
3692
|
+
|
|
3693
|
+
| Model | Size | Use Case |
|
|
3694
|
+
|-------|------|----------|
|
|
3695
|
+
| \`qwen3-coder:30b\` | ~16GB | Best coding performance |
|
|
3696
|
+
| \`gpt-oss:20b\` | ~12GB | General purpose |
|
|
3697
|
+
| \`glm-4.7:cloud\` | Cloud | No local storage needed |
|
|
3698
|
+
|
|
3699
|
+
### For CPU or Limited VRAM
|
|
3700
|
+
|
|
3701
|
+
| Model | Size | Use Case |
|
|
3702
|
+
|-------|------|----------|
|
|
3703
|
+
| \`qwen2.5-coder:7b\` | ~4GB | Coding on CPU |
|
|
3704
|
+
| \`llama3.2:3b\` | ~2GB | Fast, simple tasks |
|
|
3705
|
+
| \`gemma2:9b\` | ~5GB | General purpose |
|
|
3039
3706
|
|
|
3040
3707
|
## Troubleshooting
|
|
3041
3708
|
|
|
@@ -3057,6 +3724,12 @@ mise run logs
|
|
|
3057
3724
|
mise run down && mise run up
|
|
3058
3725
|
\`\`\`
|
|
3059
3726
|
|
|
3727
|
+
### GPU Not Detected
|
|
3728
|
+
|
|
3729
|
+
1. Verify NVIDIA drivers: \`nvidia-smi\`
|
|
3730
|
+
2. Check Docker GPU access: \`docker run --rm --gpus all nvidia/cuda:12.0-base nvidia-smi\`
|
|
3731
|
+
3. Install NVIDIA Container Toolkit if missing
|
|
3732
|
+
|
|
3060
3733
|
## License
|
|
3061
3734
|
|
|
3062
3735
|
MIT
|
|
@@ -3123,304 +3796,153 @@ async function init(options = {}) {
|
|
|
3123
3796
|
const claudeDir = join2(cwd, ".claude");
|
|
3124
3797
|
const claudeMdPath = join2(claudeDir, "CLAUDE.md");
|
|
3125
3798
|
const readmePath = join2(cwd, "README.md");
|
|
3126
|
-
|
|
3127
|
-
|
|
3799
|
+
header("Initializing loclaude project");
|
|
3800
|
+
console.log("");
|
|
3801
|
+
let gpuMode;
|
|
3802
|
+
if (options.gpu === false) {
|
|
3803
|
+
gpuMode = false;
|
|
3804
|
+
console.log(info("CPU-only mode (--no-gpu)"));
|
|
3805
|
+
} else if (options.gpu === true) {
|
|
3806
|
+
gpuMode = true;
|
|
3807
|
+
console.log(info("GPU mode enabled (--gpu)"));
|
|
3808
|
+
} else {
|
|
3809
|
+
console.log(dim(" Detecting GPU..."));
|
|
3810
|
+
gpuMode = await hasNvidiaGpu();
|
|
3811
|
+
if (gpuMode) {
|
|
3812
|
+
console.log(success("NVIDIA GPU detected - using GPU mode"));
|
|
3813
|
+
} else {
|
|
3814
|
+
console.log(warn("No NVIDIA GPU detected - using CPU mode"));
|
|
3815
|
+
console.log(dim(" Use --gpu to force GPU mode if you have an NVIDIA GPU"));
|
|
3816
|
+
}
|
|
3817
|
+
}
|
|
3818
|
+
console.log("");
|
|
3128
3819
|
if (existsSync2(readmePath) && !options.force) {
|
|
3129
|
-
console.log("
|
|
3820
|
+
console.log(warn(`${file("README.md")} already exists`));
|
|
3130
3821
|
} else {
|
|
3131
3822
|
writeFileSync(readmePath, README_TEMPLATE);
|
|
3132
|
-
console.log(
|
|
3823
|
+
console.log(success(`Created ${file("README.md")}`));
|
|
3133
3824
|
}
|
|
3134
3825
|
if (existsSync2(composePath) && !options.force) {
|
|
3135
|
-
console.log("
|
|
3136
|
-
console.log(
|
|
3137
|
-
`);
|
|
3826
|
+
console.log(warn(`${file("docker-compose.yml")} already exists`));
|
|
3827
|
+
console.log(dim(" Use --force to overwrite"));
|
|
3138
3828
|
} else {
|
|
3139
|
-
let composeContent =
|
|
3829
|
+
let composeContent = gpuMode ? DOCKER_COMPOSE_TEMPLATE_GPU : DOCKER_COMPOSE_TEMPLATE_CPU;
|
|
3140
3830
|
if (options.noWebui) {
|
|
3141
|
-
composeContent = composeContent.replace(/\n
|
|
3142
|
-
`).replace(/\
|
|
3831
|
+
composeContent = composeContent.replace(/\n # =+\n # OPEN WEBUI[\s\S]*?capabilities: \[gpu\]\n/m, `
|
|
3832
|
+
`).replace(/\n # =+\n # OPEN WEBUI[\s\S]*?open-webui:\/app\/backend\/data\n/m, `
|
|
3833
|
+
`).replace(/\nvolumes:\n open-webui:\n.*$/m, `
|
|
3143
3834
|
`);
|
|
3144
3835
|
}
|
|
3145
3836
|
writeFileSync(composePath, composeContent);
|
|
3146
|
-
|
|
3837
|
+
const modeLabel = gpuMode ? cyan("GPU") : cyan("CPU");
|
|
3838
|
+
console.log(success(`Created ${file("docker-compose.yml")} (${modeLabel} mode)`));
|
|
3147
3839
|
}
|
|
3148
3840
|
if (existsSync2(miseTomlPath) && !options.force) {
|
|
3149
|
-
console.log("
|
|
3841
|
+
console.log(warn(`${file("mise.toml")} already exists`));
|
|
3150
3842
|
} else {
|
|
3151
3843
|
writeFileSync(miseTomlPath, MISE_TOML_TEMPLATE);
|
|
3152
|
-
console.log(
|
|
3844
|
+
console.log(success(`Created ${file("mise.toml")}`));
|
|
3153
3845
|
}
|
|
3154
3846
|
if (!existsSync2(claudeDir)) {
|
|
3155
3847
|
mkdirSync(claudeDir, { recursive: true });
|
|
3156
3848
|
}
|
|
3157
3849
|
if (existsSync2(claudeMdPath) && !options.force) {
|
|
3158
|
-
console.log("
|
|
3850
|
+
console.log(warn(`${file(".claude/CLAUDE.md")} already exists`));
|
|
3159
3851
|
} else {
|
|
3160
3852
|
writeFileSync(claudeMdPath, CLAUDE_MD_TEMPLATE);
|
|
3161
|
-
console.log(
|
|
3853
|
+
console.log(success(`Created ${file(".claude/CLAUDE.md")}`));
|
|
3162
3854
|
}
|
|
3163
3855
|
if (!existsSync2(configDir)) {
|
|
3164
3856
|
mkdirSync(configDir, { recursive: true });
|
|
3165
|
-
console.log(
|
|
3857
|
+
console.log(success(`Created ${file(".loclaude/")} directory`));
|
|
3166
3858
|
}
|
|
3167
3859
|
if (existsSync2(configPath) && !options.force) {
|
|
3168
|
-
console.log("
|
|
3860
|
+
console.log(warn(`${file(".loclaude/config.json")} already exists`));
|
|
3169
3861
|
} else {
|
|
3170
|
-
writeFileSync(configPath,
|
|
3171
|
-
console.log(
|
|
3862
|
+
writeFileSync(configPath, getConfigTemplate(gpuMode));
|
|
3863
|
+
console.log(success(`Created ${file(".loclaude/config.json")}`));
|
|
3172
3864
|
}
|
|
3173
3865
|
if (!existsSync2(modelsDir)) {
|
|
3174
3866
|
mkdirSync(modelsDir, { recursive: true });
|
|
3175
|
-
console.log(
|
|
3867
|
+
console.log(success(`Created ${file("models/")} directory`));
|
|
3176
3868
|
}
|
|
3177
3869
|
if (existsSync2(gitignorePath)) {
|
|
3178
3870
|
const existing = readFileSync2(gitignorePath, "utf-8");
|
|
3179
3871
|
if (!existing.includes("models/")) {
|
|
3180
3872
|
writeFileSync(gitignorePath, existing + `
|
|
3181
3873
|
` + GITIGNORE_TEMPLATE);
|
|
3182
|
-
console.log(
|
|
3874
|
+
console.log(success(`Updated ${file(".gitignore")}`));
|
|
3183
3875
|
}
|
|
3184
3876
|
} else {
|
|
3185
3877
|
writeFileSync(gitignorePath, GITIGNORE_TEMPLATE);
|
|
3186
|
-
console.log(
|
|
3187
|
-
}
|
|
3188
|
-
console.log(`
|
|
3189
|
-
\uD83C\uDF89 Project initialized!
|
|
3190
|
-
`);
|
|
3191
|
-
console.log("Next steps:");
|
|
3192
|
-
console.log(" 1. Start containers: mise run up");
|
|
3193
|
-
console.log(" 2. Pull a model: mise run pull qwen3-coder:30b");
|
|
3194
|
-
console.log(" 3. Run Claude: mise run claude");
|
|
3195
|
-
console.log(`
|
|
3196
|
-
Service URLs:`);
|
|
3197
|
-
console.log(" Ollama API: http://localhost:11434");
|
|
3198
|
-
if (!options.noWebui) {
|
|
3199
|
-
console.log(" Open WebUI: http://localhost:3000");
|
|
3200
|
-
}
|
|
3201
|
-
}
|
|
3202
|
-
// lib/commands/doctor.ts
|
|
3203
|
-
async function checkDocker() {
|
|
3204
|
-
const exists = await commandExists("docker");
|
|
3205
|
-
if (!exists) {
|
|
3206
|
-
return {
|
|
3207
|
-
name: "Docker",
|
|
3208
|
-
status: "error",
|
|
3209
|
-
message: "Not installed",
|
|
3210
|
-
hint: "Install Docker: https://docs.docker.com/get-docker/"
|
|
3211
|
-
};
|
|
3212
|
-
}
|
|
3213
|
-
const version = await getCommandVersion("docker");
|
|
3214
|
-
return {
|
|
3215
|
-
name: "Docker",
|
|
3216
|
-
status: "ok",
|
|
3217
|
-
message: "Installed",
|
|
3218
|
-
version: version ?? undefined
|
|
3219
|
-
};
|
|
3220
|
-
}
|
|
3221
|
-
async function checkDockerCompose() {
|
|
3222
|
-
const result = await spawnCapture(["docker", "compose", "version"]);
|
|
3223
|
-
if (result.exitCode === 0) {
|
|
3224
|
-
const version = result.stdout?.trim().split(`
|
|
3225
|
-
`)[0];
|
|
3226
|
-
return {
|
|
3227
|
-
name: "Docker Compose",
|
|
3228
|
-
status: "ok",
|
|
3229
|
-
message: "Installed (v2)",
|
|
3230
|
-
version: version ?? undefined
|
|
3231
|
-
};
|
|
3232
|
-
}
|
|
3233
|
-
const v1Exists = await commandExists("docker-compose");
|
|
3234
|
-
if (v1Exists) {
|
|
3235
|
-
const version = await getCommandVersion("docker-compose");
|
|
3236
|
-
return {
|
|
3237
|
-
name: "Docker Compose",
|
|
3238
|
-
status: "warning",
|
|
3239
|
-
message: "Using legacy v1",
|
|
3240
|
-
version: version ?? undefined,
|
|
3241
|
-
hint: "Consider upgrading to Docker Compose v2"
|
|
3242
|
-
};
|
|
3243
|
-
}
|
|
3244
|
-
return {
|
|
3245
|
-
name: "Docker Compose",
|
|
3246
|
-
status: "error",
|
|
3247
|
-
message: "Not installed",
|
|
3248
|
-
hint: "Docker Compose is included with Docker Desktop, or install separately"
|
|
3249
|
-
};
|
|
3250
|
-
}
|
|
3251
|
-
async function checkNvidiaSmi() {
|
|
3252
|
-
const exists = await commandExists("nvidia-smi");
|
|
3253
|
-
if (!exists) {
|
|
3254
|
-
return {
|
|
3255
|
-
name: "NVIDIA GPU",
|
|
3256
|
-
status: "warning",
|
|
3257
|
-
message: "nvidia-smi not found",
|
|
3258
|
-
hint: "GPU support requires NVIDIA drivers. CPU-only mode will be used."
|
|
3259
|
-
};
|
|
3878
|
+
console.log(success(`Created ${file(".gitignore")}`));
|
|
3260
3879
|
}
|
|
3261
|
-
const
|
|
3262
|
-
if (result.exitCode === 0 && result.stdout) {
|
|
3263
|
-
const gpus = result.stdout.trim().split(`
|
|
3264
|
-
`).filter(Boolean);
|
|
3265
|
-
return {
|
|
3266
|
-
name: "NVIDIA GPU",
|
|
3267
|
-
status: "ok",
|
|
3268
|
-
message: `${gpus.length} GPU(s) detected`,
|
|
3269
|
-
version: gpus[0]
|
|
3270
|
-
};
|
|
3271
|
-
}
|
|
3272
|
-
return {
|
|
3273
|
-
name: "NVIDIA GPU",
|
|
3274
|
-
status: "warning",
|
|
3275
|
-
message: "nvidia-smi failed",
|
|
3276
|
-
hint: "GPU may not be available. Check NVIDIA drivers."
|
|
3277
|
-
};
|
|
3278
|
-
}
|
|
3279
|
-
async function checkNvidiaContainerToolkit() {
|
|
3280
|
-
const result = await spawnCapture(["docker", "info", "--format", "{{.Runtimes}}"]);
|
|
3281
|
-
if (result.exitCode === 0 && result.stdout?.includes("nvidia")) {
|
|
3282
|
-
return {
|
|
3283
|
-
name: "NVIDIA Container Toolkit",
|
|
3284
|
-
status: "ok",
|
|
3285
|
-
message: "nvidia runtime available"
|
|
3286
|
-
};
|
|
3287
|
-
}
|
|
3288
|
-
return {
|
|
3289
|
-
name: "NVIDIA Container Toolkit",
|
|
3290
|
-
status: "warning",
|
|
3291
|
-
message: "nvidia runtime not found",
|
|
3292
|
-
hint: "Install: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html"
|
|
3293
|
-
};
|
|
3294
|
-
}
|
|
3295
|
-
async function checkClaude() {
|
|
3296
|
-
const exists = await commandExists("claude");
|
|
3297
|
-
if (!exists) {
|
|
3298
|
-
return {
|
|
3299
|
-
name: "Claude Code",
|
|
3300
|
-
status: "error",
|
|
3301
|
-
message: "Not installed",
|
|
3302
|
-
hint: "Install: npm install -g @anthropic-ai/claude-code"
|
|
3303
|
-
};
|
|
3304
|
-
}
|
|
3305
|
-
const version = await getCommandVersion("claude");
|
|
3306
|
-
return {
|
|
3307
|
-
name: "Claude Code",
|
|
3308
|
-
status: "ok",
|
|
3309
|
-
message: "Installed",
|
|
3310
|
-
version: version ?? undefined
|
|
3311
|
-
};
|
|
3312
|
-
}
|
|
3313
|
-
async function checkOllamaConnection() {
|
|
3314
|
-
const ollamaUrl = getOllamaUrl();
|
|
3315
|
-
try {
|
|
3316
|
-
const response = await fetch(`${ollamaUrl}/api/tags`, {
|
|
3317
|
-
signal: AbortSignal.timeout(5000)
|
|
3318
|
-
});
|
|
3319
|
-
if (response.ok) {
|
|
3320
|
-
const data = await response.json();
|
|
3321
|
-
const modelCount = data.models?.length ?? 0;
|
|
3322
|
-
return {
|
|
3323
|
-
name: "Ollama API",
|
|
3324
|
-
status: "ok",
|
|
3325
|
-
message: `Connected (${modelCount} model${modelCount === 1 ? "" : "s"})`,
|
|
3326
|
-
version: ollamaUrl
|
|
3327
|
-
};
|
|
3328
|
-
}
|
|
3329
|
-
return {
|
|
3330
|
-
name: "Ollama API",
|
|
3331
|
-
status: "warning",
|
|
3332
|
-
message: `HTTP ${response.status}`,
|
|
3333
|
-
hint: "Ollama may not be running. Try: loclaude docker-up"
|
|
3334
|
-
};
|
|
3335
|
-
} catch (error) {
|
|
3336
|
-
return {
|
|
3337
|
-
name: "Ollama API",
|
|
3338
|
-
status: "warning",
|
|
3339
|
-
message: "Not reachable",
|
|
3340
|
-
hint: `Cannot connect to ${ollamaUrl}. Start Ollama: loclaude docker-up`
|
|
3341
|
-
};
|
|
3342
|
-
}
|
|
3343
|
-
}
|
|
3344
|
-
function formatCheck(check) {
|
|
3345
|
-
const icons = {
|
|
3346
|
-
ok: "✓",
|
|
3347
|
-
warning: "⚠",
|
|
3348
|
-
error: "✗"
|
|
3349
|
-
};
|
|
3350
|
-
const colors = {
|
|
3351
|
-
ok: "\x1B[32m",
|
|
3352
|
-
warning: "\x1B[33m",
|
|
3353
|
-
error: "\x1B[31m"
|
|
3354
|
-
};
|
|
3355
|
-
const reset = "\x1B[0m";
|
|
3356
|
-
const icon = icons[check.status];
|
|
3357
|
-
const color = colors[check.status];
|
|
3358
|
-
let line = `${color}${icon}${reset} ${check.name}: ${check.message}`;
|
|
3359
|
-
if (check.version) {
|
|
3360
|
-
line += ` (${check.version})`;
|
|
3361
|
-
}
|
|
3362
|
-
if (check.hint) {
|
|
3363
|
-
line += `
|
|
3364
|
-
${check.hint}`;
|
|
3365
|
-
}
|
|
3366
|
-
return line;
|
|
3367
|
-
}
|
|
3368
|
-
async function doctor() {
|
|
3369
|
-
console.log(`Checking system requirements...
|
|
3370
|
-
`);
|
|
3371
|
-
const checks = await Promise.all([
|
|
3372
|
-
checkDocker(),
|
|
3373
|
-
checkDockerCompose(),
|
|
3374
|
-
checkNvidiaSmi(),
|
|
3375
|
-
checkNvidiaContainerToolkit(),
|
|
3376
|
-
checkClaude(),
|
|
3377
|
-
checkOllamaConnection()
|
|
3378
|
-
]);
|
|
3379
|
-
for (const check of checks) {
|
|
3380
|
-
console.log(formatCheck(check));
|
|
3381
|
-
}
|
|
3382
|
-
const errors2 = checks.filter((c) => c.status === "error");
|
|
3383
|
-
const warnings = checks.filter((c) => c.status === "warning");
|
|
3880
|
+
const recommendedModel = gpuMode ? "qwen3-coder:30b" : "qwen2.5-coder:7b";
|
|
3384
3881
|
console.log("");
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3882
|
+
console.log(green("Project initialized!"));
|
|
3883
|
+
console.log("");
|
|
3884
|
+
console.log(cyan("Next steps:"));
|
|
3885
|
+
console.log(` 1. Start containers: ${cmd("mise run up")}`);
|
|
3886
|
+
console.log(` 2. Pull a model: ${cmd(`mise run pull ${recommendedModel}`)}`);
|
|
3887
|
+
console.log(` 3. Run Claude: ${cmd("mise run claude")}`);
|
|
3888
|
+
console.log("");
|
|
3889
|
+
console.log(cyan("Service URLs:"));
|
|
3890
|
+
console.log(` Ollama API: ${url("http://localhost:11434")}`);
|
|
3891
|
+
if (!options.noWebui) {
|
|
3892
|
+
console.log(` Open WebUI: ${url("http://localhost:3000")}`);
|
|
3392
3893
|
}
|
|
3393
3894
|
}
|
|
3394
3895
|
// lib/commands/config.ts
|
|
3395
|
-
import { inspect } from "util";
|
|
3396
3896
|
async function configShow() {
|
|
3397
3897
|
const config = loadConfig();
|
|
3398
3898
|
const activePath = getActiveConfigPath();
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
console.log(
|
|
3402
|
-
|
|
3403
|
-
|
|
3899
|
+
header("Current Configuration");
|
|
3900
|
+
console.log("");
|
|
3901
|
+
console.log(cyan("Ollama:"));
|
|
3902
|
+
labelValue(" URL", config.ollama.url);
|
|
3903
|
+
labelValue(" Default Model", magenta(config.ollama.defaultModel));
|
|
3904
|
+
console.log("");
|
|
3905
|
+
console.log(cyan("Docker:"));
|
|
3906
|
+
labelValue(" Compose File", config.docker.composeFile);
|
|
3907
|
+
labelValue(" GPU Mode", config.docker.gpu ? green("enabled") : dim("disabled"));
|
|
3908
|
+
console.log("");
|
|
3909
|
+
console.log(cyan("Claude:"));
|
|
3910
|
+
if (config.claude.extraArgs.length > 0) {
|
|
3911
|
+
labelValue(" Extra Args", config.claude.extraArgs.join(" "));
|
|
3912
|
+
} else {
|
|
3913
|
+
labelValue(" Extra Args", dim("none"));
|
|
3914
|
+
}
|
|
3915
|
+
console.log("");
|
|
3916
|
+
console.log(dim("─".repeat(40)));
|
|
3404
3917
|
if (activePath) {
|
|
3405
|
-
console.log(`Loaded from: ${activePath}`);
|
|
3918
|
+
console.log(dim(`Loaded from: ${file(activePath)}`));
|
|
3406
3919
|
} else {
|
|
3407
|
-
console.log("Using default configuration (no config file found)");
|
|
3920
|
+
console.log(dim("Using default configuration (no config file found)"));
|
|
3408
3921
|
}
|
|
3409
3922
|
}
|
|
3410
3923
|
async function configPaths() {
|
|
3411
3924
|
const paths = getConfigSearchPaths();
|
|
3412
3925
|
const activePath = getActiveConfigPath();
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3926
|
+
header("Config Search Paths");
|
|
3927
|
+
console.log("");
|
|
3928
|
+
console.log(dim("Files are checked in priority order (first found wins):"));
|
|
3929
|
+
console.log("");
|
|
3930
|
+
for (let i = 0;i < paths.length; i++) {
|
|
3931
|
+
const configPath = paths[i];
|
|
3932
|
+
if (!configPath)
|
|
3933
|
+
continue;
|
|
3934
|
+
const isActive = configPath === activePath;
|
|
3935
|
+
const num = `${i + 1}.`;
|
|
3936
|
+
if (isActive) {
|
|
3937
|
+
console.log(` ${num} ${file(configPath)} ${green("← active")}`);
|
|
3938
|
+
} else {
|
|
3939
|
+
console.log(` ${num} ${dim(configPath)}`);
|
|
3940
|
+
}
|
|
3419
3941
|
}
|
|
3942
|
+
console.log("");
|
|
3420
3943
|
if (!activePath) {
|
|
3421
|
-
console.log(
|
|
3422
|
-
|
|
3423
|
-
console.log("Run 'loclaude init' to create a project config.");
|
|
3944
|
+
console.log(info("No config file found. Using defaults."));
|
|
3945
|
+
console.log(dim(` Run ${cmd("loclaude init")} to create a project config.`));
|
|
3424
3946
|
}
|
|
3425
3947
|
}
|
|
3426
3948
|
// lib/commands/docker.ts
|
|
@@ -3459,42 +3981,44 @@ function getComposeCommand() {
|
|
|
3459
3981
|
async function runCompose(args, options = {}) {
|
|
3460
3982
|
const composeFile = options.file ?? findComposeFile();
|
|
3461
3983
|
if (!composeFile) {
|
|
3462
|
-
console.error("
|
|
3463
|
-
console.
|
|
3984
|
+
console.log(error("No docker-compose.yml found"));
|
|
3985
|
+
console.log(dim(` Run ${cmd("loclaude init")} to create one, or specify --file`));
|
|
3464
3986
|
return 1;
|
|
3465
3987
|
}
|
|
3466
|
-
const
|
|
3467
|
-
return spawn(
|
|
3988
|
+
const cmd_args = [...getComposeCommand(), "-f", composeFile, ...args];
|
|
3989
|
+
return spawn(cmd_args);
|
|
3468
3990
|
}
|
|
3469
3991
|
async function dockerUp(options = {}) {
|
|
3470
3992
|
const args = ["up"];
|
|
3471
3993
|
if (options.detach !== false) {
|
|
3472
3994
|
args.push("-d");
|
|
3473
3995
|
}
|
|
3474
|
-
console.log(
|
|
3475
|
-
|
|
3996
|
+
console.log(info("Starting containers..."));
|
|
3997
|
+
console.log("");
|
|
3476
3998
|
const exitCode = await runCompose(args, options);
|
|
3477
3999
|
if (exitCode === 0) {
|
|
3478
|
-
console.log(
|
|
3479
|
-
|
|
3480
|
-
console.log(
|
|
3481
|
-
Service URLs
|
|
3482
|
-
console.log(
|
|
3483
|
-
console.log(
|
|
4000
|
+
console.log("");
|
|
4001
|
+
console.log(success("Containers started"));
|
|
4002
|
+
console.log("");
|
|
4003
|
+
console.log(cyan("Service URLs:"));
|
|
4004
|
+
console.log(` Ollama API: ${url("http://localhost:11434")}`);
|
|
4005
|
+
console.log(` Open WebUI: ${url("http://localhost:3000")}`);
|
|
3484
4006
|
}
|
|
3485
4007
|
process.exit(exitCode);
|
|
3486
4008
|
}
|
|
3487
4009
|
async function dockerDown(options = {}) {
|
|
3488
|
-
console.log(
|
|
3489
|
-
|
|
4010
|
+
console.log(info("Stopping containers..."));
|
|
4011
|
+
console.log("");
|
|
3490
4012
|
const exitCode = await runCompose(["down"], options);
|
|
3491
4013
|
if (exitCode === 0) {
|
|
3492
|
-
console.log(
|
|
3493
|
-
|
|
4014
|
+
console.log("");
|
|
4015
|
+
console.log(success("Containers stopped"));
|
|
3494
4016
|
}
|
|
3495
4017
|
process.exit(exitCode);
|
|
3496
4018
|
}
|
|
3497
4019
|
async function dockerStatus(options = {}) {
|
|
4020
|
+
console.log(info("Container status:"));
|
|
4021
|
+
console.log("");
|
|
3498
4022
|
const exitCode = await runCompose(["ps"], options);
|
|
3499
4023
|
process.exit(exitCode);
|
|
3500
4024
|
}
|
|
@@ -3505,17 +4029,21 @@ async function dockerLogs(options = {}) {
|
|
|
3505
4029
|
}
|
|
3506
4030
|
if (options.service) {
|
|
3507
4031
|
args.push(options.service);
|
|
4032
|
+
console.log(info(`Logs for ${cyan(options.service)}:`));
|
|
4033
|
+
} else {
|
|
4034
|
+
console.log(info("Container logs:"));
|
|
3508
4035
|
}
|
|
4036
|
+
console.log("");
|
|
3509
4037
|
const exitCode = await runCompose(args, options);
|
|
3510
4038
|
process.exit(exitCode);
|
|
3511
4039
|
}
|
|
3512
4040
|
async function dockerRestart(options = {}) {
|
|
3513
|
-
console.log(
|
|
3514
|
-
|
|
4041
|
+
console.log(info("Restarting containers..."));
|
|
4042
|
+
console.log("");
|
|
3515
4043
|
const exitCode = await runCompose(["restart"], options);
|
|
3516
4044
|
if (exitCode === 0) {
|
|
3517
|
-
console.log(
|
|
3518
|
-
|
|
4045
|
+
console.log("");
|
|
4046
|
+
console.log(success("Containers restarted"));
|
|
3519
4047
|
}
|
|
3520
4048
|
process.exit(exitCode);
|
|
3521
4049
|
}
|
|
@@ -3532,11 +4060,11 @@ async function fetchModels() {
|
|
|
3532
4060
|
}
|
|
3533
4061
|
const data = await response.json();
|
|
3534
4062
|
return data.models ?? [];
|
|
3535
|
-
} catch (
|
|
3536
|
-
if (
|
|
4063
|
+
} catch (error3) {
|
|
4064
|
+
if (error3 instanceof Error && error3.name === "TimeoutError") {
|
|
3537
4065
|
throw new Error(`Connection to Ollama timed out (${ollamaUrl})`);
|
|
3538
4066
|
}
|
|
3539
|
-
throw
|
|
4067
|
+
throw error3;
|
|
3540
4068
|
}
|
|
3541
4069
|
}
|
|
3542
4070
|
async function isOllamaInDocker() {
|
|
@@ -3551,83 +4079,99 @@ async function runOllamaCommand(args) {
|
|
|
3551
4079
|
return spawn(["ollama", ...args]);
|
|
3552
4080
|
}
|
|
3553
4081
|
}
|
|
4082
|
+
function formatSize(sizeBytes) {
|
|
4083
|
+
const sizeStr = import_bytes2.default(sizeBytes) ?? "?";
|
|
4084
|
+
const sizeNum = sizeBytes / (1024 * 1024 * 1024);
|
|
4085
|
+
if (sizeNum > 20) {
|
|
4086
|
+
return yellow(sizeStr);
|
|
4087
|
+
} else if (sizeNum > 10) {
|
|
4088
|
+
return cyan(sizeStr);
|
|
4089
|
+
}
|
|
4090
|
+
return dim(sizeStr);
|
|
4091
|
+
}
|
|
3554
4092
|
async function modelsList() {
|
|
3555
4093
|
try {
|
|
3556
4094
|
const models = await fetchModels();
|
|
3557
4095
|
if (models.length === 0) {
|
|
3558
|
-
|
|
3559
|
-
console.log(
|
|
3560
|
-
|
|
3561
|
-
console.log("
|
|
4096
|
+
header("Installed Models");
|
|
4097
|
+
console.log("");
|
|
4098
|
+
console.log(info("No models installed."));
|
|
4099
|
+
console.log("");
|
|
4100
|
+
console.log(`Pull a model with: ${cmd("loclaude models-pull <model-name>")}`);
|
|
4101
|
+
console.log(`Example: ${cmd("loclaude models-pull llama3.2")}`);
|
|
3562
4102
|
return;
|
|
3563
4103
|
}
|
|
3564
|
-
|
|
3565
|
-
|
|
4104
|
+
header("Installed Models");
|
|
4105
|
+
console.log("");
|
|
3566
4106
|
const nameWidth = Math.max(...models.map((m) => m.name.length), "NAME".length);
|
|
3567
4107
|
const sizeWidth = 10;
|
|
3568
|
-
|
|
3569
|
-
|
|
4108
|
+
const modifiedWidth = 20;
|
|
4109
|
+
tableHeader(["NAME", "SIZE", "MODIFIED"], [nameWidth, sizeWidth, modifiedWidth]);
|
|
3570
4110
|
for (const model of models) {
|
|
3571
|
-
const name = model.name.padEnd(nameWidth);
|
|
3572
|
-
const size = (
|
|
3573
|
-
const modified = formatRelativeTime(model.modified_at);
|
|
4111
|
+
const name = magenta(model.name.padEnd(nameWidth));
|
|
4112
|
+
const size = formatSize(model.size).padStart(sizeWidth);
|
|
4113
|
+
const modified = dim(formatRelativeTime(model.modified_at));
|
|
3574
4114
|
console.log(`${name} ${size} ${modified}`);
|
|
3575
4115
|
}
|
|
3576
|
-
console.log(
|
|
3577
|
-
|
|
3578
|
-
} catch (
|
|
4116
|
+
console.log("");
|
|
4117
|
+
console.log(dim(`${models.length} model(s) installed`));
|
|
4118
|
+
} catch (err) {
|
|
3579
4119
|
const ollamaUrl = getOllamaUrl();
|
|
3580
|
-
console.error(
|
|
3581
|
-
console.
|
|
4120
|
+
console.log(error(`Could not connect to Ollama at ${ollamaUrl}`));
|
|
4121
|
+
console.log(dim(` Make sure Ollama is running: ${cmd("loclaude docker-up")}`));
|
|
3582
4122
|
process.exit(1);
|
|
3583
4123
|
}
|
|
3584
4124
|
}
|
|
3585
4125
|
async function modelsPull(modelName) {
|
|
3586
4126
|
if (!modelName) {
|
|
3587
|
-
console.error("
|
|
3588
|
-
console.
|
|
3589
|
-
console.
|
|
4127
|
+
console.log(error("Model name required"));
|
|
4128
|
+
console.log(dim(`Usage: ${cmd("loclaude models-pull <model-name>")}`));
|
|
4129
|
+
console.log(dim(`Example: ${cmd("loclaude models-pull llama3.2")}`));
|
|
3590
4130
|
process.exit(1);
|
|
3591
4131
|
}
|
|
3592
|
-
console.log(`Pulling model: ${modelName}
|
|
3593
|
-
|
|
4132
|
+
console.log(info(`Pulling model: ${magenta(modelName)}`));
|
|
4133
|
+
console.log("");
|
|
3594
4134
|
const exitCode = await runOllamaCommand(["pull", modelName]);
|
|
3595
4135
|
if (exitCode === 0) {
|
|
3596
|
-
console.log(
|
|
3597
|
-
|
|
4136
|
+
console.log("");
|
|
4137
|
+
console.log(success(`Model '${magenta(modelName)}' pulled successfully`));
|
|
3598
4138
|
}
|
|
3599
4139
|
process.exit(exitCode);
|
|
3600
4140
|
}
|
|
3601
4141
|
async function modelsRm(modelName) {
|
|
3602
4142
|
if (!modelName) {
|
|
3603
|
-
console.error("
|
|
3604
|
-
console.
|
|
4143
|
+
console.log(error("Model name required"));
|
|
4144
|
+
console.log(dim(`Usage: ${cmd("loclaude models-rm <model-name>")}`));
|
|
3605
4145
|
process.exit(1);
|
|
3606
4146
|
}
|
|
3607
|
-
console.log(`Removing model: ${modelName}
|
|
3608
|
-
|
|
4147
|
+
console.log(info(`Removing model: ${magenta(modelName)}`));
|
|
4148
|
+
console.log("");
|
|
3609
4149
|
const exitCode = await runOllamaCommand(["rm", modelName]);
|
|
3610
4150
|
if (exitCode === 0) {
|
|
3611
|
-
console.log(
|
|
3612
|
-
|
|
4151
|
+
console.log("");
|
|
4152
|
+
console.log(success(`Model '${magenta(modelName)}' removed`));
|
|
3613
4153
|
}
|
|
3614
4154
|
process.exit(exitCode);
|
|
3615
4155
|
}
|
|
3616
4156
|
async function modelsShow(modelName) {
|
|
3617
4157
|
if (!modelName) {
|
|
3618
|
-
console.error("
|
|
3619
|
-
console.
|
|
4158
|
+
console.log(error("Model name required"));
|
|
4159
|
+
console.log(dim(`Usage: ${cmd("loclaude models-show <model-name>")}`));
|
|
3620
4160
|
process.exit(1);
|
|
3621
4161
|
}
|
|
4162
|
+
console.log(info(`Model details: ${magenta(modelName)}`));
|
|
4163
|
+
console.log("");
|
|
3622
4164
|
const exitCode = await runOllamaCommand(["show", modelName]);
|
|
3623
4165
|
process.exit(exitCode);
|
|
3624
4166
|
}
|
|
3625
4167
|
async function modelsRun(modelName) {
|
|
3626
4168
|
if (!modelName) {
|
|
3627
|
-
console.error("
|
|
3628
|
-
console.
|
|
4169
|
+
console.log(error("Model name required"));
|
|
4170
|
+
console.log(dim(`Usage: ${cmd("loclaude models-run <model-name>")}`));
|
|
3629
4171
|
process.exit(1);
|
|
3630
4172
|
}
|
|
4173
|
+
console.log(info(`Running model: ${magenta(modelName)}`));
|
|
4174
|
+
console.log("");
|
|
3631
4175
|
const exitCode = await runOllamaCommand(["run", modelName]);
|
|
3632
4176
|
process.exit(exitCode);
|
|
3633
4177
|
}
|
|
@@ -3667,7 +4211,7 @@ cli.command("run [...args]", "Run Claude Code with local Ollama", {
|
|
|
3667
4211
|
}
|
|
3668
4212
|
await launchClaude(model, args);
|
|
3669
4213
|
});
|
|
3670
|
-
cli.command("init", "Initialize a new loclaude project").option("--force", "Overwrite existing files").option("--no-webui", "Skip Open WebUI in docker-compose").action(async (options) => {
|
|
4214
|
+
cli.command("init", "Initialize a new loclaude project").option("--force", "Overwrite existing files").option("--no-webui", "Skip Open WebUI in docker-compose").option("--gpu", "Force GPU mode (NVIDIA)").option("--no-gpu", "Force CPU-only mode").action(async (options) => {
|
|
3671
4215
|
await init(options);
|
|
3672
4216
|
});
|
|
3673
4217
|
cli.command("doctor", "Check system requirements and health").action(async () => {
|
|
@@ -3723,5 +4267,5 @@ export {
|
|
|
3723
4267
|
cli
|
|
3724
4268
|
};
|
|
3725
4269
|
|
|
3726
|
-
//# debugId=
|
|
4270
|
+
//# debugId=D27E5C5FE65A8CD164756E2164756E21
|
|
3727
4271
|
//# sourceMappingURL=index.js.map
|