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