@otto-code/brain 0.7.5

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.
Files changed (150) hide show
  1. package/bin/otto-brain +2 -0
  2. package/dist/bench/context-corpus.d.ts +58 -0
  3. package/dist/bench/context-corpus.js +210 -0
  4. package/dist/bench/corpus.d.ts +31 -0
  5. package/dist/bench/corpus.js +379 -0
  6. package/dist/bench/curated-repos.d.ts +35 -0
  7. package/dist/bench/curated-repos.js +41 -0
  8. package/dist/bench/health.d.ts +50 -0
  9. package/dist/bench/health.js +112 -0
  10. package/dist/bench/index.d.ts +103 -0
  11. package/dist/bench/index.js +209 -0
  12. package/dist/bench/mine.d.ts +32 -0
  13. package/dist/bench/mine.js +101 -0
  14. package/dist/bench/repo-task.d.ts +93 -0
  15. package/dist/bench/repo-task.js +380 -0
  16. package/dist/bench/repo.d.ts +125 -0
  17. package/dist/bench/repo.js +195 -0
  18. package/dist/bench/rescore.d.ts +41 -0
  19. package/dist/bench/rescore.js +127 -0
  20. package/dist/bench/tasks.d.ts +124 -0
  21. package/dist/bench/tasks.js +1249 -0
  22. package/dist/bench/verify.d.ts +142 -0
  23. package/dist/bench/verify.js +350 -0
  24. package/dist/cli.d.ts +13 -0
  25. package/dist/cli.js +72 -0
  26. package/dist/commands/bench.d.ts +20 -0
  27. package/dist/commands/bench.js +222 -0
  28. package/dist/commands/calibrate.d.ts +19 -0
  29. package/dist/commands/calibrate.js +75 -0
  30. package/dist/commands/catalog.d.ts +32 -0
  31. package/dist/commands/catalog.js +48 -0
  32. package/dist/commands/config.d.ts +13 -0
  33. package/dist/commands/config.js +90 -0
  34. package/dist/commands/lifecycle.d.ts +29 -0
  35. package/dist/commands/lifecycle.js +194 -0
  36. package/dist/commands/pull.d.ts +22 -0
  37. package/dist/commands/pull.js +135 -0
  38. package/dist/commands/report.d.ts +10 -0
  39. package/dist/commands/report.js +34 -0
  40. package/dist/commands/rescore.d.ts +11 -0
  41. package/dist/commands/rescore.js +48 -0
  42. package/dist/commands/runtime.d.ts +21 -0
  43. package/dist/commands/runtime.js +62 -0
  44. package/dist/commands/scan.d.ts +24 -0
  45. package/dist/commands/scan.js +55 -0
  46. package/dist/commands/search.d.ts +31 -0
  47. package/dist/commands/search.js +135 -0
  48. package/dist/commands/share.d.ts +26 -0
  49. package/dist/commands/share.js +148 -0
  50. package/dist/commands/sweep.d.ts +21 -0
  51. package/dist/commands/sweep.js +69 -0
  52. package/dist/commands/ui.d.ts +9 -0
  53. package/dist/commands/ui.js +28 -0
  54. package/dist/config/env.d.ts +10 -0
  55. package/dist/config/env.js +48 -0
  56. package/dist/config/index.d.ts +9 -0
  57. package/dist/config/index.js +9 -0
  58. package/dist/config/otto-home.d.ts +2 -0
  59. package/dist/config/otto-home.js +24 -0
  60. package/dist/config/paths.d.ts +20 -0
  61. package/dist/config/paths.js +33 -0
  62. package/dist/config/private-files.d.ts +3 -0
  63. package/dist/config/private-files.js +32 -0
  64. package/dist/config/profiles.d.ts +34 -0
  65. package/dist/config/profiles.js +116 -0
  66. package/dist/config/schema.d.ts +1035 -0
  67. package/dist/config/schema.js +191 -0
  68. package/dist/config/store.d.ts +11 -0
  69. package/dist/config/store.js +73 -0
  70. package/dist/gguf.d.ts +61 -0
  71. package/dist/gguf.js +231 -0
  72. package/dist/gpu.d.ts +12 -0
  73. package/dist/gpu.js +49 -0
  74. package/dist/index.d.ts +14 -0
  75. package/dist/index.js +13 -0
  76. package/dist/main.d.ts +2 -0
  77. package/dist/main.js +5 -0
  78. package/dist/models/dirs.d.ts +9 -0
  79. package/dist/models/dirs.js +21 -0
  80. package/dist/models/download.d.ts +31 -0
  81. package/dist/models/download.js +105 -0
  82. package/dist/models/enrich.d.ts +36 -0
  83. package/dist/models/enrich.js +68 -0
  84. package/dist/models/hf.d.ts +62 -0
  85. package/dist/models/hf.js +114 -0
  86. package/dist/models/index.d.ts +15 -0
  87. package/dist/models/index.js +49 -0
  88. package/dist/models/manage.d.ts +27 -0
  89. package/dist/models/manage.js +112 -0
  90. package/dist/models/pick.d.ts +3 -0
  91. package/dist/models/pick.js +36 -0
  92. package/dist/models/scan.d.ts +13 -0
  93. package/dist/models/scan.js +185 -0
  94. package/dist/ops/archive.d.ts +34 -0
  95. package/dist/ops/archive.js +105 -0
  96. package/dist/ops/calibrate.d.ts +49 -0
  97. package/dist/ops/calibrate.js +74 -0
  98. package/dist/ops/report.d.ts +26 -0
  99. package/dist/ops/report.js +532 -0
  100. package/dist/ops/results.d.ts +207 -0
  101. package/dist/ops/results.js +250 -0
  102. package/dist/ops/sweep.d.ts +77 -0
  103. package/dist/ops/sweep.js +126 -0
  104. package/dist/output/index.d.ts +5 -0
  105. package/dist/output/index.js +5 -0
  106. package/dist/output/render.d.ts +4 -0
  107. package/dist/output/render.js +96 -0
  108. package/dist/output/types.d.ts +52 -0
  109. package/dist/output/types.js +9 -0
  110. package/dist/output/with-output.d.ts +3 -0
  111. package/dist/output/with-output.js +42 -0
  112. package/dist/run.d.ts +5 -0
  113. package/dist/run.js +19 -0
  114. package/dist/runtime/args.d.ts +21 -0
  115. package/dist/runtime/args.js +71 -0
  116. package/dist/runtime/index.d.ts +13 -0
  117. package/dist/runtime/index.js +41 -0
  118. package/dist/runtime/lmstudio.d.ts +10 -0
  119. package/dist/runtime/lmstudio.js +88 -0
  120. package/dist/runtime/managed.d.ts +26 -0
  121. package/dist/runtime/managed.js +148 -0
  122. package/dist/service/model-selector.d.ts +36 -0
  123. package/dist/service/model-selector.js +123 -0
  124. package/dist/service/pid-lock.d.ts +18 -0
  125. package/dist/service/pid-lock.js +50 -0
  126. package/dist/service/router.d.ts +144 -0
  127. package/dist/service/router.js +746 -0
  128. package/dist/service/scheduler.d.ts +74 -0
  129. package/dist/service/scheduler.js +110 -0
  130. package/dist/service/serve.d.ts +41 -0
  131. package/dist/service/serve.js +278 -0
  132. package/dist/service/supervisor.d.ts +67 -0
  133. package/dist/service/supervisor.js +222 -0
  134. package/dist/service/tailscale.d.ts +19 -0
  135. package/dist/service/tailscale.js +62 -0
  136. package/dist/service/tls.d.ts +77 -0
  137. package/dist/service/tls.js +192 -0
  138. package/dist/sysmon.d.ts +50 -0
  139. package/dist/sysmon.js +103 -0
  140. package/dist/tui/app.d.ts +208 -0
  141. package/dist/tui/app.js +1801 -0
  142. package/dist/tui/screen.d.ts +72 -0
  143. package/dist/tui/screen.js +176 -0
  144. package/dist/types.d.ts +76 -0
  145. package/dist/types.js +7 -0
  146. package/dist/version.d.ts +2 -0
  147. package/dist/version.js +8 -0
  148. package/dist/vram.d.ts +67 -0
  149. package/dist/vram.js +132 -0
  150. package/package.json +58 -0
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Format renderers for the output layer. chalk lives here and nowhere else —
3
+ * command handlers express color declaratively via ColumnDef.color, and only
4
+ * these renderers turn a result into text. Table output is ANSI-aware so colored
5
+ * cells still align.
6
+ */
7
+ import chalk from "chalk";
8
+ import { stringify as toYaml } from "yaml";
9
+ const ANSI = /\[[0-9;]*m/g;
10
+ function stripAnsi(value) {
11
+ return value.replace(ANSI, "");
12
+ }
13
+ const COLORS = {
14
+ red: chalk.red,
15
+ green: chalk.green,
16
+ blue: chalk.blue,
17
+ yellow: chalk.yellow,
18
+ cyan: chalk.cyan,
19
+ magenta: chalk.magenta,
20
+ white: chalk.white,
21
+ gray: chalk.gray,
22
+ dim: chalk.dim,
23
+ bold: chalk.bold,
24
+ };
25
+ function fieldValue(item, column) {
26
+ return typeof column.field === "function" ? column.field(item) : item[column.field];
27
+ }
28
+ function cellText(value) {
29
+ if (value === null || value === undefined)
30
+ return "-";
31
+ return String(value);
32
+ }
33
+ function padCell(text, width, align) {
34
+ const visible = stripAnsi(text).length;
35
+ if (visible >= width)
36
+ return text;
37
+ const pad = " ".repeat(width - visible);
38
+ return align === "right" ? pad + text : text + pad;
39
+ }
40
+ function renderTable(rows, schema, options) {
41
+ const widths = schema.columns.map((col) => {
42
+ const cells = rows.map((r) => stripAnsi(cellText(fieldValue(r, col))).length);
43
+ return Math.max(col.width ?? 0, col.header.length, ...cells, 0);
44
+ });
45
+ const lines = [];
46
+ if (!options.noHeaders) {
47
+ const header = schema.columns
48
+ .map((col, i) => padCell(options.noColor ? col.header : chalk.bold(col.header), widths[i], col.align ?? "left"))
49
+ .join(" ");
50
+ lines.push(header);
51
+ }
52
+ for (const row of rows) {
53
+ const cells = schema.columns.map((col, i) => {
54
+ const raw = fieldValue(row, col);
55
+ let text = cellText(raw);
56
+ if (!options.noColor && col.color) {
57
+ const name = col.color(raw, row);
58
+ if (name)
59
+ text = COLORS[name](text);
60
+ }
61
+ return padCell(text, widths[i], col.align ?? "left");
62
+ });
63
+ lines.push(cells.join(" "));
64
+ }
65
+ return lines.join("\n");
66
+ }
67
+ export function renderResult(result, options) {
68
+ const rows = result.type === "list" ? result.data : [result.data];
69
+ if (options.quiet) {
70
+ const { idField } = result.schema;
71
+ return rows
72
+ .map((r) => (typeof idField === "function" ? idField(r) : String(r[idField])))
73
+ .join("\n");
74
+ }
75
+ if (options.format === "json") {
76
+ const data = result.schema.serialize ? result.schema.serialize(result.data) : result.data;
77
+ return JSON.stringify(data, null, 2);
78
+ }
79
+ if (options.format === "yaml") {
80
+ const data = result.schema.serialize ? result.schema.serialize(result.data) : result.data;
81
+ return toYaml(data).trimEnd();
82
+ }
83
+ if (result.schema.renderHuman)
84
+ return result.schema.renderHuman(result.data, options);
85
+ return renderTable(rows, result.schema, options);
86
+ }
87
+ export function renderError(error, options) {
88
+ if (options.format === "json")
89
+ return JSON.stringify({ error }, null, 2);
90
+ if (options.format === "yaml")
91
+ return toYaml({ error }).trimEnd();
92
+ const prefix = options.noColor ? "Error: " : chalk.red("Error: ");
93
+ const details = error.details ? `\n ${String(error.details)}` : "";
94
+ return `${prefix}${error.message}${details}`;
95
+ }
96
+ //# sourceMappingURL=render.js.map
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The output contract, mirroring @otto-code/cli's output layer: command handlers
3
+ * do not print — they return a typed result plus a schema, and a wrapper renders
4
+ * it in the user-selected format (table / json / yaml / quiet). This is what lets
5
+ * every `otto brain` command support `--format`, `--json`, and `--quiet` uniformly.
6
+ */
7
+ export type OutputFormat = "table" | "json" | "yaml";
8
+ export interface OutputOptions {
9
+ format: OutputFormat;
10
+ quiet: boolean;
11
+ noHeaders: boolean;
12
+ noColor: boolean;
13
+ }
14
+ export type ColorName = "red" | "green" | "blue" | "yellow" | "cyan" | "magenta" | "white" | "gray" | "dim" | "bold";
15
+ export interface ColumnDef<T> {
16
+ header: string;
17
+ field: keyof T | ((item: T) => unknown);
18
+ width?: number;
19
+ align?: "left" | "right";
20
+ color?: (value: unknown, item: T) => ColorName | undefined;
21
+ }
22
+ export interface OutputSchema<T> {
23
+ idField: keyof T | ((item: T) => string);
24
+ columns: ColumnDef<T>[];
25
+ /** Optional custom human renderer, used for the table format only. */
26
+ renderHuman?: (data: T | T[], options: OutputOptions) => string;
27
+ /** Optional serializer for json/yaml (defaults to the value itself). */
28
+ serialize?: (data: T | T[]) => unknown;
29
+ }
30
+ export interface SingleResult<T> {
31
+ type: "single";
32
+ data: T;
33
+ schema: OutputSchema<T>;
34
+ }
35
+ export interface ListResult<T> {
36
+ type: "list";
37
+ data: T[];
38
+ schema: OutputSchema<T>;
39
+ }
40
+ export type AnyCommandResult<T> = SingleResult<T> | ListResult<T>;
41
+ /** A structured, user-facing error. Thrown by handlers; rendered by withOutput. */
42
+ export interface CommandErrorShape {
43
+ code: string;
44
+ message: string;
45
+ details?: unknown;
46
+ }
47
+ export declare class CommandError extends Error implements CommandErrorShape {
48
+ code: string;
49
+ details?: unknown;
50
+ constructor(shape: CommandErrorShape);
51
+ }
52
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,9 @@
1
+ export class CommandError extends Error {
2
+ constructor(shape) {
3
+ super(shape.message);
4
+ this.name = "CommandError";
5
+ this.code = shape.code;
6
+ this.details = shape.details;
7
+ }
8
+ }
9
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,3 @@
1
+ import { type AnyCommandResult } from "./types.js";
2
+ export declare function withOutput<A extends unknown[], T>(handler: (...args: A) => Promise<AnyCommandResult<T>>): (...args: A) => Promise<void>;
3
+ //# sourceMappingURL=with-output.d.ts.map
@@ -0,0 +1,42 @@
1
+ import { renderError, renderResult } from "./render.js";
2
+ import { CommandError, } from "./types.js";
3
+ const FORMATS = new Set(["table", "json", "yaml"]);
4
+ function optionsFromCommand(command) {
5
+ const opts = command.optsWithGlobals();
6
+ let format = typeof opts.format === "string" ? opts.format : "table";
7
+ if (opts.json === true)
8
+ format = "json";
9
+ if (format === "cli")
10
+ format = "table";
11
+ if (!FORMATS.has(format)) {
12
+ throw new CommandError({
13
+ code: "INVALID_FORMAT",
14
+ message: `unknown format "${format}"`,
15
+ details: "Supported formats: table, json, yaml",
16
+ });
17
+ }
18
+ return {
19
+ format: format,
20
+ quiet: opts.quiet === true,
21
+ noHeaders: opts.headers === false,
22
+ noColor: opts.color === false,
23
+ };
24
+ }
25
+ export function withOutput(handler) {
26
+ return async (...args) => {
27
+ const command = args[args.length - 1];
28
+ const options = optionsFromCommand(command);
29
+ try {
30
+ const result = await handler(...args);
31
+ process.stdout.write(`${renderResult(result, options)}\n`);
32
+ }
33
+ catch (error) {
34
+ const shape = error instanceof CommandError
35
+ ? { code: error.code, message: error.message, details: error.details }
36
+ : { code: "ERROR", message: error instanceof Error ? error.message : String(error) };
37
+ process.stderr.write(`${renderError(shape, options)}\n`);
38
+ process.exitCode = 1;
39
+ }
40
+ };
41
+ }
42
+ //# sourceMappingURL=with-output.js.map
package/dist/run.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export interface RunOptions {
2
+ from?: "user" | "node";
3
+ }
4
+ export declare function runBrainCli(argv: string[], options?: RunOptions): Promise<number>;
5
+ //# sourceMappingURL=run.d.ts.map
package/dist/run.js ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Standalone CLI runner for `bin/otto-brain`. Builds a root program named
3
+ * `otto-brain`, mounts the brain verbs directly on it (so `otto-brain serve`, not
4
+ * `otto-brain brain serve`), and sets process.exitCode rather than exiting, so
5
+ * the same code path is testable.
6
+ */
7
+ import { Command } from "commander";
8
+ import { registerBrainCommands } from "./cli.js";
9
+ import { resolveVersion } from "./version.js";
10
+ export async function runBrainCli(argv, options = {}) {
11
+ const program = new Command()
12
+ .name("otto-brain")
13
+ .description("Otto Brain - host local GGUF models with measured VRAM budgeting")
14
+ .version(resolveVersion(), "-v, --version", "output the version number");
15
+ registerBrainCommands(program);
16
+ await program.parseAsync(argv, { from: options.from ?? "user" });
17
+ return typeof process.exitCode === "number" ? process.exitCode : 0;
18
+ }
19
+ //# sourceMappingURL=run.js.map
@@ -0,0 +1,21 @@
1
+ import type { Profile } from "../config/schema.js";
2
+ import type { Runtime } from "../types.js";
3
+ export interface ServeTarget {
4
+ port: number;
5
+ host?: string;
6
+ }
7
+ /**
8
+ * PATH value the child process needs so the stub can resolve its DLLs. Both the
9
+ * runtime dir and its vendor dir go first, ahead of the inherited PATH.
10
+ */
11
+ export declare function buildEnv(runtime: Runtime, baseEnv?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
12
+ /**
13
+ * Translate a profile into llama-server arguments.
14
+ *
15
+ * Only settings that demonstrably matter for stable local inference are emitted —
16
+ * no experimental sampler knobs.
17
+ */
18
+ export declare function buildArgs(profile: Profile, { port, host }: ServeTarget): string[];
19
+ /** The same command as a copy-pasteable shell line, for the TUI to display. */
20
+ export declare function formatCommand(runtime: Runtime, args: string[]): string;
21
+ //# sourceMappingURL=args.d.ts.map
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Translates a profile into llama-server arguments and builds the PATH the child
3
+ * needs. Runtime-source agnostic: works the same for an LM Studio runtime or a
4
+ * managed one, since both resolve to a `Runtime` (exe + optional vendorDir).
5
+ */
6
+ import path from "node:path";
7
+ /**
8
+ * PATH value the child process needs so the stub can resolve its DLLs. Both the
9
+ * runtime dir and its vendor dir go first, ahead of the inherited PATH.
10
+ */
11
+ export function buildEnv(runtime, baseEnv = process.env) {
12
+ const parts = [runtime.dir];
13
+ if (runtime.vendorDir)
14
+ parts.push(runtime.vendorDir);
15
+ return {
16
+ ...baseEnv,
17
+ PATH: `${parts.join(path.delimiter)}${path.delimiter}${baseEnv.PATH || ""}`,
18
+ };
19
+ }
20
+ /**
21
+ * Translate a profile into llama-server arguments.
22
+ *
23
+ * Only settings that demonstrably matter for stable local inference are emitted —
24
+ * no experimental sampler knobs.
25
+ */
26
+ export function buildArgs(profile, { port, host = "127.0.0.1" }) {
27
+ const args = [
28
+ "-m",
29
+ profile.modelPath ?? "",
30
+ "-c",
31
+ String(profile.contextSize),
32
+ "-ctk",
33
+ profile.cacheTypeK,
34
+ "-ctv",
35
+ profile.cacheTypeV,
36
+ "-fa",
37
+ profile.flashAttention ? "on" : "off",
38
+ "-ngl",
39
+ String(profile.gpuLayers),
40
+ "--host",
41
+ host,
42
+ "--port",
43
+ String(port),
44
+ "--no-webui",
45
+ ];
46
+ if (profile.vision && profile.mmprojPath) {
47
+ args.push("--mmproj", profile.mmprojPath);
48
+ }
49
+ // The setting that was actually breaking long agentic runs.
50
+ if (profile.reasoningBudget !== null && profile.reasoningBudget !== undefined) {
51
+ args.push("--reasoning-budget", String(profile.reasoningBudget));
52
+ if (profile.reasoningBudgetMessage) {
53
+ args.push("--reasoning-budget-message", profile.reasoningBudgetMessage);
54
+ }
55
+ }
56
+ if (profile.parallelSlots)
57
+ args.push("--parallel", String(profile.parallelSlots));
58
+ if (profile.batchSize)
59
+ args.push("-b", String(profile.batchSize));
60
+ if (profile.ubatchSize)
61
+ args.push("-ub", String(profile.ubatchSize));
62
+ if (profile.extraArgs && profile.extraArgs.length)
63
+ args.push(...profile.extraArgs);
64
+ return args;
65
+ }
66
+ /** The same command as a copy-pasteable shell line, for the TUI to display. */
67
+ export function formatCommand(runtime, args) {
68
+ const quote = (s) => (/\s/.test(s) ? `"${s}"` : s);
69
+ return `${quote(runtime.exe)} ${args.map(quote).join(" ")}`;
70
+ }
71
+ //# sourceMappingURL=args.js.map
@@ -0,0 +1,13 @@
1
+ import type { BrainConfig } from "../config/schema.js";
2
+ import type { Runtime } from "../types.js";
3
+ import { type InstallProgress } from "./managed.js";
4
+ export { BACKENDS_DIR, LMSTUDIO_ROOT, listRuntimes as listLmStudioRuntimes } from "./lmstudio.js";
5
+ export { buildArgs, buildEnv, formatCommand, type ServeTarget } from "./args.js";
6
+ export { installManagedRuntime, listManagedRuntimes, defaultRuntimeSpec, DEFAULT_LLAMA_BUILD, type RuntimeSpec, type InstallProgress, } from "./managed.js";
7
+ /** Every runtime available on this machine, managed first then LM Studio. */
8
+ export declare function listAllRuntimes(env?: NodeJS.ProcessEnv): Runtime[];
9
+ /** The runtime to use given config, or null when none is available. */
10
+ export declare function resolveRuntime(config: BrainConfig, env?: NodeJS.ProcessEnv): Runtime | null;
11
+ /** Ensure a runtime exists, downloading the default managed build if none does. */
12
+ export declare function ensureRuntime(config: BrainConfig, env?: NodeJS.ProcessEnv, onProgress?: (progress: InstallProgress) => void): Promise<Runtime>;
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Runtime resolution across sources. Two providers implement "where does
3
+ * llama-server come from": `managed` (downloaded by otto-brain, the self-contained
4
+ * path) and `lmstudio` (discovered from an existing LM Studio install, a
5
+ * zero-download fast path). Selection follows config: an explicit path override
6
+ * wins; otherwise `auto` prefers a managed runtime and falls back to LM Studio.
7
+ */
8
+ import { resolveBrainPaths } from "../config/paths.js";
9
+ import { listRuntimes as listLmStudioRuntimes, resolveOverride } from "./lmstudio.js";
10
+ import { defaultRuntimeSpec, installManagedRuntime, listManagedRuntimes, } from "./managed.js";
11
+ export { BACKENDS_DIR, LMSTUDIO_ROOT, listRuntimes as listLmStudioRuntimes } from "./lmstudio.js";
12
+ export { buildArgs, buildEnv, formatCommand } from "./args.js";
13
+ export { installManagedRuntime, listManagedRuntimes, defaultRuntimeSpec, DEFAULT_LLAMA_BUILD, } from "./managed.js";
14
+ /** Every runtime available on this machine, managed first then LM Studio. */
15
+ export function listAllRuntimes(env = process.env) {
16
+ const paths = resolveBrainPaths(env);
17
+ return [...listManagedRuntimes(paths.runtimesDir), ...listLmStudioRuntimes()];
18
+ }
19
+ /** The runtime to use given config, or null when none is available. */
20
+ export function resolveRuntime(config, env = process.env) {
21
+ const rc = config.runtime;
22
+ if (rc.path)
23
+ return resolveOverride(rc.path);
24
+ const paths = resolveBrainPaths(env);
25
+ const managed = listManagedRuntimes(paths.runtimesDir);
26
+ const lmstudio = listLmStudioRuntimes();
27
+ if (rc.source === "managed")
28
+ return managed[0] ?? null;
29
+ if (rc.source === "lmstudio")
30
+ return lmstudio[0] ?? null;
31
+ return managed[0] ?? lmstudio[0] ?? null; // auto
32
+ }
33
+ /** Ensure a runtime exists, downloading the default managed build if none does. */
34
+ export async function ensureRuntime(config, env = process.env, onProgress) {
35
+ const existing = resolveRuntime(config, env);
36
+ if (existing)
37
+ return existing;
38
+ const paths = resolveBrainPaths(env);
39
+ return installManagedRuntime(defaultRuntimeSpec(), paths.runtimesDir, onProgress);
40
+ }
41
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,10 @@
1
+ import type { Runtime } from "../types.js";
2
+ export declare const LMSTUDIO_ROOT: string;
3
+ export declare const BACKENDS_DIR: string;
4
+ export declare function extractVersion(dirName: string): string;
5
+ /** Compare dotted version strings numerically, descending. */
6
+ export declare function compareVersionsDesc(a: string, b: string): number;
7
+ export declare function listRuntimes(backendsDir?: string): Runtime[];
8
+ /** Resolve an explicit runtime directory or exe override into a Runtime. */
9
+ export declare function resolveOverride(override: string): Runtime;
10
+ //# sourceMappingURL=lmstudio.d.ts.map
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Locates the llama-server binary shipped inside LM Studio.
3
+ *
4
+ * Important gotcha: that executable is a ~20KB stub. Launching it without the
5
+ * matching `backends/vendor/...` directory on PATH fails with
6
+ * STATUS_DLL_NOT_FOUND (0xC0000135) and prints absolutely nothing, so we always
7
+ * pair a runtime with its vendor directory. This is one of two runtime sources
8
+ * (the other being the self-contained `managed` runtime); it stays as a
9
+ * zero-download fast path when LM Studio is already installed.
10
+ */
11
+ import fs from "node:fs";
12
+ import os from "node:os";
13
+ import path from "node:path";
14
+ export const LMSTUDIO_ROOT = path.join(os.homedir(), ".lmstudio");
15
+ export const BACKENDS_DIR = path.join(LMSTUDIO_ROOT, "extensions", "backends");
16
+ // Most preferred first. Each entry maps a runtime prefix to its vendor dir.
17
+ const RUNTIME_PREFERENCE = [
18
+ {
19
+ prefix: "llama.cpp-win-x86_64-nvidia-cuda12-",
20
+ vendor: "win-llama-cuda12-vendor-v2",
21
+ label: "CUDA 12",
22
+ },
23
+ {
24
+ prefix: "llama.cpp-win-x86_64-nvidia-cuda-",
25
+ vendor: "win-llama-cuda-vendor-v2",
26
+ label: "CUDA 11",
27
+ },
28
+ { prefix: "llama.cpp-win-x86_64-vulkan-", vendor: "win-llama-vulkan-vendor-v2", label: "Vulkan" },
29
+ { prefix: "llama.cpp-win-x86_64-avx2-", vendor: null, label: "CPU (AVX2)" },
30
+ ];
31
+ // Directory names carry an instruction-set segment before the version
32
+ // (…-cuda12-avx2-2.24.0), so take the trailing dotted number, not the remainder.
33
+ const VERSION_SUFFIX = /(\d+(?:\.\d+)+)$/;
34
+ export function extractVersion(dirName) {
35
+ const match = dirName.match(VERSION_SUFFIX);
36
+ return match ? match[1] : "0";
37
+ }
38
+ /** Compare dotted version strings numerically, descending. */
39
+ export function compareVersionsDesc(a, b) {
40
+ const pa = a.split(".").map(Number);
41
+ const pb = b.split(".").map(Number);
42
+ for (let i = 0; i < Math.max(pa.length, pb.length); i += 1) {
43
+ const diff = (pb[i] || 0) - (pa[i] || 0);
44
+ if (diff !== 0)
45
+ return diff;
46
+ }
47
+ return 0;
48
+ }
49
+ export function listRuntimes(backendsDir = BACKENDS_DIR) {
50
+ if (!fs.existsSync(backendsDir))
51
+ return [];
52
+ const entries = fs
53
+ .readdirSync(backendsDir, { withFileTypes: true })
54
+ .filter((e) => e.isDirectory())
55
+ .map((e) => e.name);
56
+ const found = [];
57
+ for (const pref of RUNTIME_PREFERENCE) {
58
+ const matches = entries
59
+ .filter((name) => name.startsWith(pref.prefix))
60
+ .map((name) => ({ name, version: extractVersion(name) }))
61
+ .filter(({ name }) => fs.existsSync(path.join(backendsDir, name, "llama-server.exe")))
62
+ .sort((a, b) => compareVersionsDesc(a.version, b.version));
63
+ for (const match of matches) {
64
+ const vendorDir = pref.vendor ? path.join(backendsDir, "vendor", pref.vendor) : null;
65
+ // A runtime whose vendor DLLs are missing cannot launch; skip it.
66
+ if (vendorDir && !fs.existsSync(vendorDir))
67
+ continue;
68
+ found.push({
69
+ label: pref.label,
70
+ version: match.version,
71
+ dir: path.join(backendsDir, match.name),
72
+ exe: path.join(backendsDir, match.name, "llama-server.exe"),
73
+ vendorDir,
74
+ source: "lmstudio",
75
+ });
76
+ }
77
+ }
78
+ return found;
79
+ }
80
+ /** Resolve an explicit runtime directory or exe override into a Runtime. */
81
+ export function resolveOverride(override) {
82
+ const exe = override.endsWith(".exe") ? override : path.join(override, "llama-server.exe");
83
+ if (!fs.existsSync(exe))
84
+ throw new Error(`llama-server not found at ${exe}`);
85
+ const dir = path.dirname(exe);
86
+ return { label: "override", version: "unknown", dir, exe, vendorDir: null, source: "lmstudio" };
87
+ }
88
+ //# sourceMappingURL=lmstudio.js.map
@@ -0,0 +1,26 @@
1
+ import type { Runtime } from "../types.js";
2
+ export interface RuntimeSpec {
3
+ label: string;
4
+ version: string;
5
+ /** One or more archive URLs, extracted in order into the same target dir. */
6
+ assets: string[];
7
+ }
8
+ export interface InstallProgress {
9
+ phase: "downloading" | "extracting" | "done";
10
+ asset?: string;
11
+ receivedBytes?: number;
12
+ totalBytes?: number;
13
+ }
14
+ /**
15
+ * The default runtime build. The llama.cpp release tag and CUDA asset names must
16
+ * be pinned per platform; verify the URLs against the current release before
17
+ * shipping. Overridable via config (`runtime.path`) or an explicit spec.
18
+ */
19
+ export declare const DEFAULT_LLAMA_BUILD = "b4600";
20
+ /** Build the default Windows CUDA 12 spec for a given llama.cpp build tag. */
21
+ export declare function defaultRuntimeSpec(build?: string): RuntimeSpec;
22
+ /** A managed runtime is any dir under runtimesDir that contains llama-server.exe. */
23
+ export declare function listManagedRuntimes(runtimesDir: string): Runtime[];
24
+ /** Download + extract a runtime spec into runtimesDir and return the Runtime. */
25
+ export declare function installManagedRuntime(spec: RuntimeSpec, runtimesDir: string, onProgress?: (progress: InstallProgress) => void): Promise<Runtime>;
26
+ //# sourceMappingURL=managed.d.ts.map
@@ -0,0 +1,148 @@
1
+ /**
2
+ * The self-contained runtime source: otto-brain downloads a pinned llama.cpp
3
+ * build into `$OTTO_HOME/otto-brain/runtimes/` and runs it directly, so the tool
4
+ * needs no other software installed. Downloaded runtimes keep their DLLs in the
5
+ * same directory as the exe (so `vendorDir` is null — buildEnv already puts the
6
+ * runtime dir on PATH, which is what the DLL-stub trap requires).
7
+ *
8
+ * Extraction uses only OS built-ins — PowerShell's Expand-Archive for .zip and
9
+ * the bundled `tar` for tarballs — to keep the "nothing else to install" promise.
10
+ */
11
+ import { spawn } from "node:child_process";
12
+ import fs from "node:fs";
13
+ import { createWriteStream } from "node:fs";
14
+ import path from "node:path";
15
+ import { Readable } from "node:stream";
16
+ import { pipeline } from "node:stream/promises";
17
+ /**
18
+ * The default runtime build. The llama.cpp release tag and CUDA asset names must
19
+ * be pinned per platform; verify the URLs against the current release before
20
+ * shipping. Overridable via config (`runtime.path`) or an explicit spec.
21
+ */
22
+ export const DEFAULT_LLAMA_BUILD = "b4600";
23
+ const LLAMA_RELEASE_BASE = "https://github.com/ggml-org/llama.cpp/releases/download";
24
+ /** Build the default Windows CUDA 12 spec for a given llama.cpp build tag. */
25
+ export function defaultRuntimeSpec(build = DEFAULT_LLAMA_BUILD) {
26
+ return {
27
+ label: "CUDA 12 (managed)",
28
+ version: build,
29
+ assets: [
30
+ `${LLAMA_RELEASE_BASE}/${build}/llama-${build}-bin-win-cuda-12.4-x64.zip`,
31
+ `${LLAMA_RELEASE_BASE}/${build}/cudart-llama-bin-win-cuda-12.4-x64.zip`,
32
+ ],
33
+ };
34
+ }
35
+ function slug(spec) {
36
+ return `${spec.label}-${spec.version}`
37
+ .toLowerCase()
38
+ .replace(/[^a-z0-9]+/g, "-")
39
+ .replace(/(^-|-$)/g, "");
40
+ }
41
+ /** Recursively find the first file named `name` under `dir`. */
42
+ function findFile(dir, name) {
43
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
44
+ for (const entry of entries) {
45
+ const full = path.join(dir, entry.name);
46
+ if (entry.isDirectory()) {
47
+ const nested = findFile(full, name);
48
+ if (nested)
49
+ return nested;
50
+ }
51
+ else if (entry.name.toLowerCase() === name.toLowerCase()) {
52
+ return full;
53
+ }
54
+ }
55
+ return null;
56
+ }
57
+ /** A managed runtime is any dir under runtimesDir that contains llama-server.exe. */
58
+ export function listManagedRuntimes(runtimesDir) {
59
+ if (!fs.existsSync(runtimesDir))
60
+ return [];
61
+ const found = [];
62
+ for (const entry of fs.readdirSync(runtimesDir, { withFileTypes: true })) {
63
+ if (!entry.isDirectory())
64
+ continue;
65
+ const root = path.join(runtimesDir, entry.name);
66
+ const exe = findFile(root, "llama-server.exe");
67
+ if (!exe)
68
+ continue;
69
+ found.push({
70
+ label: entry.name,
71
+ version: entry.name.replace(/^.*-/, ""),
72
+ dir: path.dirname(exe),
73
+ exe,
74
+ vendorDir: null,
75
+ source: "managed",
76
+ });
77
+ }
78
+ return found;
79
+ }
80
+ async function downloadFile(url, dest, onProgress) {
81
+ const response = await fetch(url);
82
+ if (!response.ok || !response.body) {
83
+ throw new Error(`download failed (${response.status}) for ${url}`);
84
+ }
85
+ const totalBytes = Number(response.headers.get("content-length")) || undefined;
86
+ let receivedBytes = 0;
87
+ const body = Readable.fromWeb(response.body);
88
+ body.on("data", (chunk) => {
89
+ receivedBytes += chunk.length;
90
+ onProgress?.({ phase: "downloading", asset: url, receivedBytes, totalBytes });
91
+ });
92
+ await pipeline(body, createWriteStream(dest));
93
+ }
94
+ function run(command, args) {
95
+ return new Promise((resolve, reject) => {
96
+ const child = spawn(command, args, { windowsHide: true });
97
+ let stderr = "";
98
+ child.stderr.on("data", (d) => (stderr += d.toString()));
99
+ child.on("error", reject);
100
+ child.on("close", (code) => code === 0 ? resolve() : reject(new Error(`${command} exited ${code}: ${stderr.trim()}`)));
101
+ });
102
+ }
103
+ async function extractArchive(archivePath, destDir) {
104
+ fs.mkdirSync(destDir, { recursive: true });
105
+ if (/\.zip$/i.test(archivePath)) {
106
+ // PowerShell ships with Windows; -Force overwrites an interrupted extract.
107
+ // Paths are rooted at $OTTO_HOME (under the user profile), so a username with
108
+ // an apostrophe would break — or inject into — a raw single-quoted string.
109
+ // Escape single quotes for PowerShell (a literal ' is written as '').
110
+ const psQuote = (value) => `'${value.replace(/'/g, "''")}'`;
111
+ await run("powershell", [
112
+ "-NoProfile",
113
+ "-Command",
114
+ `Expand-Archive -LiteralPath ${psQuote(archivePath)} -DestinationPath ${psQuote(destDir)} -Force`,
115
+ ]);
116
+ return;
117
+ }
118
+ if (/\.(tar\.gz|tgz|tar)$/i.test(archivePath)) {
119
+ await run("tar", ["-xf", archivePath, "-C", destDir]);
120
+ return;
121
+ }
122
+ throw new Error(`unknown archive type: ${archivePath}`);
123
+ }
124
+ /** Download + extract a runtime spec into runtimesDir and return the Runtime. */
125
+ export async function installManagedRuntime(spec, runtimesDir, onProgress) {
126
+ const targetDir = path.join(runtimesDir, slug(spec));
127
+ fs.mkdirSync(targetDir, { recursive: true });
128
+ for (const url of spec.assets) {
129
+ const archivePath = path.join(targetDir, path.basename(new URL(url).pathname));
130
+ await downloadFile(url, archivePath, onProgress);
131
+ onProgress?.({ phase: "extracting", asset: url });
132
+ await extractArchive(archivePath, targetDir);
133
+ fs.rmSync(archivePath, { force: true });
134
+ }
135
+ const exe = findFile(targetDir, "llama-server.exe");
136
+ if (!exe)
137
+ throw new Error(`installed runtime has no llama-server.exe under ${targetDir}`);
138
+ onProgress?.({ phase: "done" });
139
+ return {
140
+ label: spec.label,
141
+ version: spec.version,
142
+ dir: path.dirname(exe),
143
+ exe,
144
+ vendorDir: null,
145
+ source: "managed",
146
+ };
147
+ }
148
+ //# sourceMappingURL=managed.js.map