@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,194 @@
1
+ /**
2
+ * Service lifecycle commands. `serve` runs the brain in the foreground; `start`
3
+ * launches it detached (the shape the Otto daemon uses to supervise a managed
4
+ * child); `stop`/`status` operate on the pid file. All honor the opt-in config —
5
+ * they are always explicit user actions, never auto-started.
6
+ */
7
+ import { spawn } from "node:child_process";
8
+ import { openSync } from "node:fs";
9
+ import http from "node:http";
10
+ import https from "node:https";
11
+ import { loadBrainConfig } from "../config/index.js";
12
+ import { resolveBrainPaths } from "../config/paths.js";
13
+ import { CommandError } from "../output/types.js";
14
+ import { isProcessAlive, readRunningService, removePidFile, } from "../service/pid-lock.js";
15
+ import { startService } from "../service/serve.js";
16
+ import * as vram from "../vram.js";
17
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
18
+ // ------------------------------------------------------------------------ serve
19
+ export function addServeOptions(cmd) {
20
+ return cmd
21
+ .description("Host a model in the foreground (Ctrl+C to stop)")
22
+ .option("--model <fragment>", "model name fragment or catalog id");
23
+ }
24
+ export async function runServeCommand(options, _command) {
25
+ const config = loadBrainConfig();
26
+ const handle = await startService({
27
+ config,
28
+ modelNeedle: options.model,
29
+ onLog: (line) => process.stderr.write(` ${line}\n`),
30
+ });
31
+ const scheme = handle.secure ? "https" : "http";
32
+ process.stdout.write(`router listening on ${scheme}://${handle.displayHost}:${handle.port}\n`);
33
+ process.stdout.write(`ready: ${handle.model.displayName}, ${vram.formatGiB(handle.supervisor.vramAtReadyBytes ?? 0)} VRAM in use\n`);
34
+ process.stdout.write("press Ctrl+C to stop\n");
35
+ const shutdown = async () => {
36
+ process.stdout.write("\nstopping…\n");
37
+ await handle.stop();
38
+ process.exit(0);
39
+ };
40
+ process.on("SIGINT", shutdown);
41
+ process.on("SIGTERM", shutdown);
42
+ }
43
+ export const lifecycleSchema = {
44
+ idField: "pid",
45
+ columns: [
46
+ {
47
+ header: "STATUS",
48
+ field: "status",
49
+ width: 10,
50
+ color: (v) => (v === "running" ? "green" : "yellow"),
51
+ },
52
+ { header: "PID", field: "pid", width: 8, align: "right" },
53
+ { header: "HOST", field: "host", width: 16 },
54
+ { header: "PORT", field: "port", width: 6, align: "right" },
55
+ ],
56
+ };
57
+ export function addStartOptions(cmd) {
58
+ return cmd
59
+ .description("Start the brain service detached")
60
+ .option("--model <fragment>", "model name fragment or catalog id");
61
+ }
62
+ // Rebuild the sub-command path this command was invoked under (excluding the
63
+ // root program and the leaf verb) so the detached child is spawned with the same
64
+ // prefix. Standalone `otto-brain start` → [] (serve is a root verb); integrated
65
+ // `otto brain start` → ["brain"] (so the child runs `otto brain serve`, not the
66
+ // non-existent `otto serve`).
67
+ function invocationVerbPrefix(command) {
68
+ const names = [];
69
+ let ancestor = command?.parent;
70
+ while (ancestor && ancestor.parent) {
71
+ names.unshift(ancestor.name());
72
+ ancestor = ancestor.parent;
73
+ }
74
+ return names;
75
+ }
76
+ export async function runStartCommand(options, command) {
77
+ const existing = readRunningService();
78
+ if (existing) {
79
+ throw new CommandError({
80
+ code: "ALREADY_RUNNING",
81
+ message: `brain already running (pid ${existing.pid}) on ${existing.host}:${existing.port}`,
82
+ details: "stop it with `otto brain stop`",
83
+ });
84
+ }
85
+ const { logFile } = resolveBrainPaths();
86
+ const entry = process.argv[1];
87
+ const args = [...invocationVerbPrefix(command), "serve"];
88
+ if (options.model)
89
+ args.push("--model", options.model);
90
+ const out = openSync(logFile, "a");
91
+ const child = spawn(process.execPath, [entry, ...args], {
92
+ detached: true,
93
+ stdio: ["ignore", out, out],
94
+ windowsHide: true,
95
+ });
96
+ child.unref();
97
+ return {
98
+ type: "single",
99
+ data: { status: "starting", pid: child.pid ?? "-", host: "-", port: "-" },
100
+ schema: lifecycleSchema,
101
+ };
102
+ }
103
+ // ------------------------------------------------------------------------- stop
104
+ export function addStopOptions(cmd) {
105
+ return cmd.description("Stop the running brain service");
106
+ }
107
+ export async function runStopCommand(_options, _command) {
108
+ const running = readRunningService();
109
+ if (!running) {
110
+ throw new CommandError({ code: "NOT_RUNNING", message: "no brain service is running" });
111
+ }
112
+ process.kill(running.pid, "SIGTERM");
113
+ removePidFile();
114
+ return {
115
+ type: "single",
116
+ data: { status: "stopped", pid: running.pid, host: running.host, port: running.port },
117
+ schema: lifecycleSchema,
118
+ };
119
+ }
120
+ // ---------------------------------------------------------------------- restart
121
+ export function addRestartOptions(cmd) {
122
+ return cmd
123
+ .description("Restart the brain service detached")
124
+ .option("--model <fragment>", "model name fragment or catalog id");
125
+ }
126
+ export async function runRestartCommand(options, command) {
127
+ const running = readRunningService();
128
+ if (running) {
129
+ try {
130
+ process.kill(running.pid, "SIGTERM");
131
+ }
132
+ catch {
133
+ // already gone; fall through to a fresh start
134
+ }
135
+ // Wait for it to exit so the port is free before we respawn (up to ~3s).
136
+ for (let i = 0; i < 30 && isProcessAlive(running.pid); i++) {
137
+ await delay(100);
138
+ }
139
+ removePidFile();
140
+ }
141
+ return runStartCommand(options, command);
142
+ }
143
+ const statusSchema = {
144
+ idField: "pid",
145
+ columns: [
146
+ ...lifecycleSchema.columns,
147
+ { header: "HEALTH", field: "health", width: 10, color: (v) => (v === "ok" ? "green" : "red") },
148
+ ],
149
+ };
150
+ export function addStatusOptions(cmd) {
151
+ return cmd.description("Show whether the brain service is running");
152
+ }
153
+ function probeHealth(record) {
154
+ const secure = record.secure === true;
155
+ const lib = secure ? https : http;
156
+ // Probe the bind host (always locally reachable) and skip cert validation: a
157
+ // self-signed or MagicDNS cert would otherwise fail a local liveness check.
158
+ const options = {
159
+ host: record.host,
160
+ port: record.port,
161
+ path: "/health",
162
+ timeout: 3000,
163
+ ...(secure ? { rejectUnauthorized: false } : {}),
164
+ };
165
+ return new Promise((resolve) => {
166
+ const req = lib.get(options, (res) => {
167
+ res.resume();
168
+ const code = res.statusCode ?? 0;
169
+ resolve(code >= 200 && code < 300 ? "ok" : `http ${code}`);
170
+ });
171
+ req.on("timeout", () => {
172
+ req.destroy();
173
+ resolve("loading");
174
+ });
175
+ req.on("error", () => resolve("loading"));
176
+ });
177
+ }
178
+ export async function runStatusCommand(_options, _command) {
179
+ const running = readRunningService();
180
+ if (!running) {
181
+ return {
182
+ type: "single",
183
+ data: { status: "stopped", pid: "-", host: "-", port: "-", health: "-" },
184
+ schema: statusSchema,
185
+ };
186
+ }
187
+ const health = await probeHealth(running);
188
+ return {
189
+ type: "single",
190
+ data: { status: "running", pid: running.pid, host: running.host, port: running.port, health },
191
+ schema: statusSchema,
192
+ };
193
+ }
194
+ //# sourceMappingURL=lifecycle.js.map
@@ -0,0 +1,22 @@
1
+ /**
2
+ * `otto brain pull <model>` — download a model from the catalog into the managed
3
+ * models directory, using only Node's fetch (no external downloader). The catalog
4
+ * is the same one seeded from docs/candidate-models.md. `--list-quants` shows what
5
+ * quantizations the repo offers and `--quant <label>` downloads a specific one.
6
+ */
7
+ import type { Command } from "commander";
8
+ import type { AnyCommandResult } from "../output/index.js";
9
+ export interface PullRow {
10
+ model: string;
11
+ repo: string;
12
+ path: string;
13
+ size: string;
14
+ }
15
+ export declare function addPullOptions(cmd: Command): Command;
16
+ export interface PullOptionsInput {
17
+ file?: string;
18
+ quant?: string;
19
+ listQuants?: boolean;
20
+ }
21
+ export declare function runPullCommand(modelArg: string, options: PullOptionsInput, _command: Command): Promise<AnyCommandResult<PullRow>>;
22
+ //# sourceMappingURL=pull.d.ts.map
@@ -0,0 +1,135 @@
1
+ import { loadBrainConfig, loadCatalog } from "../config/index.js";
2
+ import { downloadRepoFiles, listRepoQuants, managedModelsDir, pullModel, resolveHfToken, } from "../models/index.js";
3
+ import { CommandError } from "../output/types.js";
4
+ import { formatBytes } from "../models/scan.js";
5
+ const pullSchema = {
6
+ idField: "model",
7
+ columns: [
8
+ { header: "MODEL", field: "model", width: 32 },
9
+ { header: "REPO", field: "repo", width: 40 },
10
+ { header: "PATH", field: "path", width: 40 },
11
+ { header: "SIZE", field: "size", width: 10, align: "right" },
12
+ ],
13
+ };
14
+ const quantSchema = {
15
+ idField: "quant",
16
+ columns: [
17
+ { header: "QUANT", field: "quant", width: 12 },
18
+ { header: "SIZE", field: "size", width: 10, align: "right" },
19
+ { header: "FILES", field: "files", width: 6, align: "right" },
20
+ ],
21
+ };
22
+ function findCatalogModel(models, needle) {
23
+ const lower = needle.toLowerCase();
24
+ const exact = models.find((m) => m.id === needle || m.name.toLowerCase() === lower);
25
+ if (exact)
26
+ return exact;
27
+ const matches = models.filter((m) => m.id.toLowerCase().includes(lower) || m.name.toLowerCase().includes(lower));
28
+ if (!matches.length) {
29
+ throw new CommandError({ code: "NO_MATCH", message: `no catalog model matches "${needle}"` });
30
+ }
31
+ if (matches.length > 1) {
32
+ throw new CommandError({
33
+ code: "AMBIGUOUS",
34
+ message: `"${needle}" matches ${matches.length} catalog models`,
35
+ details: matches.map((m) => m.name).join(", "),
36
+ });
37
+ }
38
+ return matches[0];
39
+ }
40
+ export function addPullOptions(cmd) {
41
+ return cmd
42
+ .description("Download a model from the catalog")
43
+ .argument("<model>", "catalog id or name fragment")
44
+ .option("--file <name.gguf>", "explicit GGUF file name in the repo")
45
+ .option("--quant <label>", "download a specific quantization (e.g. Q5_K_M)")
46
+ .option("--list-quants", "list the quantizations the repo offers and exit");
47
+ }
48
+ export async function runPullCommand(modelArg, options, _command) {
49
+ const config = loadBrainConfig();
50
+ const catalog = loadCatalog();
51
+ const model = findCatalogModel(catalog.models, modelArg);
52
+ const token = resolveHfToken(config);
53
+ // Discover-and-choose paths both need the repo's quant listing.
54
+ if (options.listQuants || options.quant) {
55
+ const { quants, mmproj } = await listRepoQuants(model.hfRepo, token);
56
+ if (options.listQuants) {
57
+ const listing = {
58
+ type: "list",
59
+ data: quants.map((q) => ({
60
+ quant: q.quant,
61
+ size: formatBytes(q.sizeBytes),
62
+ files: q.files.length,
63
+ })),
64
+ schema: quantSchema,
65
+ };
66
+ // withOutput infers one row type; the renderer is schema-driven, so a
67
+ // differently-shaped result renders correctly at runtime.
68
+ return listing;
69
+ }
70
+ const wanted = options.quant.toLowerCase();
71
+ const choice = quants.find((q) => q.quant.toLowerCase() === wanted);
72
+ if (!choice) {
73
+ throw new CommandError({
74
+ code: "NO_QUANT",
75
+ message: `${model.hfRepo} has no ${options.quant} - available: ${quants.map((q) => q.quant).join(", ") || "none"}`,
76
+ });
77
+ }
78
+ const files = [...choice.files, ...(mmproj ? mmproj.files : [])];
79
+ const total = choice.sizeBytes + (mmproj?.sizeBytes ?? 0);
80
+ let lastPct = -1;
81
+ const written = await downloadRepoFiles({
82
+ repo: model.hfRepo,
83
+ files,
84
+ destRoot: managedModelsDir(config),
85
+ token,
86
+ onProgress: (p) => {
87
+ const pct = total ? Math.floor((p.receivedBytes / total) * 100) : 0;
88
+ if (pct !== lastPct && pct % 5 === 0) {
89
+ lastPct = pct;
90
+ process.stderr.write(` ${model.name} ${choice.quant}: ${pct}%\r`);
91
+ }
92
+ },
93
+ });
94
+ process.stderr.write("\n");
95
+ return {
96
+ type: "single",
97
+ data: {
98
+ model: `${model.name} (${choice.quant})`,
99
+ repo: model.hfRepo,
100
+ path: written[0] ?? "(already present)",
101
+ size: formatBytes(total),
102
+ },
103
+ schema: pullSchema,
104
+ };
105
+ }
106
+ const destRoot = managedModelsDir(config);
107
+ let lastPct = -1;
108
+ const localPath = await pullModel({
109
+ model,
110
+ destRoot,
111
+ file: options.file,
112
+ token,
113
+ onProgress: (p) => {
114
+ if (!p.totalBytes)
115
+ return;
116
+ const pct = Math.floor((p.receivedBytes / p.totalBytes) * 100);
117
+ if (pct !== lastPct && pct % 5 === 0) {
118
+ lastPct = pct;
119
+ process.stderr.write(` ${model.name}: ${pct}%\r`);
120
+ }
121
+ },
122
+ });
123
+ process.stderr.write("\n");
124
+ return {
125
+ type: "single",
126
+ data: {
127
+ model: model.name,
128
+ repo: model.hfRepo,
129
+ path: localPath,
130
+ size: model.approxWeightsBytes ? formatBytes(model.approxWeightsBytes) : "-",
131
+ },
132
+ schema: pullSchema,
133
+ };
134
+ }
135
+ //# sourceMappingURL=pull.js.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `otto brain report` — write the HTML comparison report and print a per-model
3
+ * score table (newest run per model+config).
4
+ */
5
+ import type { Command } from "commander";
6
+ export declare function addReportOptions(cmd: Command): Command;
7
+ export declare function runReportCommand(options: {
8
+ out?: string;
9
+ }, _command: Command): Promise<void>;
10
+ //# sourceMappingURL=report.d.ts.map
@@ -0,0 +1,34 @@
1
+ import { CommandError } from "../output/types.js";
2
+ import * as report from "../ops/report.js";
3
+ import * as results from "../ops/results.js";
4
+ export function addReportOptions(cmd) {
5
+ return cmd
6
+ .description("Write the HTML comparison report")
7
+ .option("--out <path>", "output HTML path");
8
+ }
9
+ export async function runReportCommand(options, _command) {
10
+ const records = results.latestPerConfig();
11
+ if (!records.length) {
12
+ throw new CommandError({
13
+ code: "NO_RESULTS",
14
+ message: "no results yet",
15
+ details: "run `otto brain bench --model <name>` first",
16
+ });
17
+ }
18
+ const { file, count } = report.write(options.out);
19
+ process.stdout.write(`\nwrote ${file}\n ${count} run(s) charted, newest per model+config\n\n`);
20
+ const columns = results.taskColumns(records);
21
+ const pad = (s, n) => String(s).padEnd(n);
22
+ process.stdout.write(` ${pad("model", 40)}${columns.map((c) => c.category.slice(0, 8).padStart(9)).join("")}${"overall".padStart(9)}\n`);
23
+ for (const record of records) {
24
+ const cells = columns
25
+ .map((c) => {
26
+ const task = record.tasks.find((t) => t.id === c.id);
27
+ return `${task ? `${(task.score * 100).toFixed(0)}%` : "-"}`.padStart(9);
28
+ })
29
+ .join("");
30
+ process.stdout.write(` ${pad(record.model.displayName.slice(0, 39), 40)}${cells}${`${(record.overall * 100).toFixed(0)}%`.padStart(9)}\n`);
31
+ }
32
+ process.stdout.write("\n");
33
+ }
34
+ //# sourceMappingURL=report.js.map
@@ -0,0 +1,11 @@
1
+ /**
2
+ * `otto brain rescore` — re-grade archived benchmark transcripts with the current
3
+ * scorer, no GPU used. Surfaces scorer changes against what was recorded live.
4
+ */
5
+ import type { Command } from "commander";
6
+ export declare function addRescoreOptions(cmd: Command): Command;
7
+ export declare function runRescoreCommand(options: {
8
+ run?: string;
9
+ execute?: boolean;
10
+ }, _command: Command): Promise<void>;
11
+ //# sourceMappingURL=rescore.d.ts.map
@@ -0,0 +1,48 @@
1
+ import { CommandError } from "../output/types.js";
2
+ import { rescoreRun } from "../bench/rescore.js";
3
+ import * as archive from "../ops/archive.js";
4
+ import * as results from "../ops/results.js";
5
+ export function addRescoreOptions(cmd) {
6
+ return cmd
7
+ .description("Re-grade archived runs with the current scorer (no GPU)")
8
+ .option("--run <id>", "rescore a single archived run")
9
+ .option("--no-execute", "syntax-check generated code but do not run it");
10
+ }
11
+ export async function runRescoreCommand(options, _command) {
12
+ const runs = archive.list();
13
+ if (!runs.length) {
14
+ throw new CommandError({
15
+ code: "NO_ARCHIVE",
16
+ message: "no archived transcripts yet",
17
+ details: "benchmark runs are archived automatically from now on",
18
+ });
19
+ }
20
+ process.stdout.write(`\n${runs.length} archived run(s), ${(archive.size() / 1024 ** 2).toFixed(1)} MB\n\n`);
21
+ const execute = options.execute !== false;
22
+ const stored = results.loadAll();
23
+ const targets = options.run ? [options.run] : runs;
24
+ let changed = 0;
25
+ for (const id of targets) {
26
+ const result = await rescoreRun(id, { execute });
27
+ const record = stored.find((r) => r.archiveId === id) ||
28
+ stored.find((r) => id.includes(results.slugify(r.model.displayName)));
29
+ process.stdout.write(` ${id}\n`);
30
+ for (const task of result.tasks) {
31
+ if (task.score === null) {
32
+ process.stdout.write(` ${task.taskId.padEnd(16)} ${task.summary}\n`);
33
+ continue;
34
+ }
35
+ const before = record?.tasks.find((t) => t.id === task.taskId);
36
+ const now = `${(task.score * 100).toFixed(0)}%`;
37
+ const then = before ? `${(before.score * 100).toFixed(0)}%` : "—";
38
+ const moved = before && Math.abs(before.score - task.score) > 0.005;
39
+ if (moved)
40
+ changed += 1;
41
+ process.stdout.write(` ${task.taskId.padEnd(16)} was ${then.padStart(5)} now ${now.padStart(5)}` +
42
+ `${moved ? " <- CHANGED" : ""} ${task.summary}\n`);
43
+ }
44
+ process.stdout.write("\n");
45
+ }
46
+ process.stdout.write(` re-graded from stored transcripts, ${changed} task result(s) changed, no GPU used\n\n`);
47
+ }
48
+ //# sourceMappingURL=rescore.js.map
@@ -0,0 +1,21 @@
1
+ /**
2
+ * `otto brain runtime install|list` — manage the self-contained llama.cpp runtime.
3
+ * `install` downloads a pinned build into $OTTO_HOME/otto-brain/runtimes so the
4
+ * tool needs no other software; `list` shows every runtime found (managed first,
5
+ * then LM Studio).
6
+ */
7
+ import type { Command } from "commander";
8
+ import type { AnyCommandResult } from "../output/index.js";
9
+ export interface RuntimeRow {
10
+ label: string;
11
+ version: string;
12
+ source: string;
13
+ dir: string;
14
+ }
15
+ export declare function addRuntimeListOptions(cmd: Command): Command;
16
+ export declare function runRuntimeListCommand(_options: unknown, _command: Command): Promise<AnyCommandResult<RuntimeRow>>;
17
+ export declare function addRuntimeInstallOptions(cmd: Command): Command;
18
+ export declare function runRuntimeInstallCommand(options: {
19
+ build?: string;
20
+ }, _command: Command): Promise<AnyCommandResult<RuntimeRow>>;
21
+ //# sourceMappingURL=runtime.d.ts.map
@@ -0,0 +1,62 @@
1
+ import { loadBrainConfig } from "../config/index.js";
2
+ import { resolveBrainPaths } from "../config/paths.js";
3
+ import { defaultRuntimeSpec, installManagedRuntime, listAllRuntimes } from "../runtime/index.js";
4
+ const runtimeSchema = {
5
+ idField: "dir",
6
+ columns: [
7
+ { header: "LABEL", field: "label", width: 20 },
8
+ { header: "VERSION", field: "version", width: 10 },
9
+ {
10
+ header: "SOURCE",
11
+ field: "source",
12
+ width: 10,
13
+ color: (v) => (v === "managed" ? "green" : undefined),
14
+ },
15
+ { header: "DIR", field: "dir", width: 50 },
16
+ ],
17
+ };
18
+ function toRows() {
19
+ return listAllRuntimes().map((r) => ({
20
+ label: r.label,
21
+ version: r.version,
22
+ source: r.source,
23
+ dir: r.dir,
24
+ }));
25
+ }
26
+ export function addRuntimeListOptions(cmd) {
27
+ return cmd.description("List available llama.cpp runtimes");
28
+ }
29
+ export async function runRuntimeListCommand(_options, _command) {
30
+ return { type: "list", data: toRows(), schema: runtimeSchema };
31
+ }
32
+ export function addRuntimeInstallOptions(cmd) {
33
+ return cmd
34
+ .description("Download a self-contained llama.cpp runtime")
35
+ .option("--build <tag>", "llama.cpp release build tag");
36
+ }
37
+ export async function runRuntimeInstallCommand(options, _command) {
38
+ loadBrainConfig();
39
+ const { runtimesDir } = resolveBrainPaths();
40
+ const spec = defaultRuntimeSpec(options.build);
41
+ process.stderr.write(` installing ${spec.label} (${spec.version})…\n`);
42
+ const runtime = await installManagedRuntime(spec, runtimesDir, (p) => {
43
+ if (p.phase === "downloading" && p.totalBytes) {
44
+ const pct = Math.floor(((p.receivedBytes ?? 0) / p.totalBytes) * 100);
45
+ process.stderr.write(` ${pct}%\r`);
46
+ }
47
+ if (p.phase === "extracting")
48
+ process.stderr.write("\n extracting…\n");
49
+ });
50
+ process.stderr.write("\n");
51
+ return {
52
+ type: "single",
53
+ data: {
54
+ label: runtime.label,
55
+ version: runtime.version,
56
+ source: runtime.source,
57
+ dir: runtime.dir,
58
+ },
59
+ schema: runtimeSchema,
60
+ };
61
+ }
62
+ //# sourceMappingURL=runtime.js.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `otto brain scan` — list detected models with their arch, quant, size, native
3
+ * context, vision, calibration state, and source. Returns a typed list the output
4
+ * layer renders as a table (default), json, or yaml.
5
+ */
6
+ import type { Command } from "commander";
7
+ import type { AnyCommandResult, OutputSchema } from "../output/index.js";
8
+ export interface ScanRow {
9
+ model: string;
10
+ arch: string;
11
+ quant: string;
12
+ size: string;
13
+ ctx: string;
14
+ vision: string;
15
+ calibrated: string;
16
+ features: string;
17
+ source: string;
18
+ }
19
+ export declare const scanSchema: OutputSchema<ScanRow>;
20
+ export declare function addScanOptions(cmd: Command): Command;
21
+ export declare function runScanCommand(options: {
22
+ metadata?: boolean;
23
+ }, _command: Command): Promise<AnyCommandResult<ScanRow>>;
24
+ //# sourceMappingURL=scan.d.ts.map
@@ -0,0 +1,55 @@
1
+ import { getCalibration, forModel, loadBrainConfig, loadProfilesStore } from "../config/index.js";
2
+ import { formatBytes, scanModels } from "../models/index.js";
3
+ export const scanSchema = {
4
+ idField: "model",
5
+ columns: [
6
+ { header: "MODEL", field: "model", width: 40 },
7
+ { header: "ARCH", field: "arch", width: 10 },
8
+ { header: "QUANT", field: "quant", width: 8 },
9
+ { header: "SIZE", field: "size", width: 9, align: "right" },
10
+ { header: "CTX", field: "ctx", width: 9, align: "right" },
11
+ { header: "VIS", field: "vision", width: 4 },
12
+ {
13
+ header: "CAL",
14
+ field: "calibrated",
15
+ width: 4,
16
+ color: (v) => (v === "yes" ? "green" : undefined),
17
+ },
18
+ { header: "FEAT", field: "features", width: 10 },
19
+ { header: "SOURCE", field: "source", width: 8 },
20
+ ],
21
+ };
22
+ export function addScanOptions(cmd) {
23
+ return cmd
24
+ .description("List detected local models")
25
+ .option("--no-metadata", "skip reading GGUF headers (faster)");
26
+ }
27
+ export async function runScanCommand(options, _command) {
28
+ const config = loadBrainConfig();
29
+ const store = loadProfilesStore();
30
+ const catalog = scanModels(config, process.env, { withMetadata: options.metadata !== false });
31
+ const rows = catalog.map((model) => {
32
+ const profile = forModel(store, model, config.defaults);
33
+ const md = model.metadata ?? {};
34
+ const feat = [
35
+ model.features.mtp ? "MTP" : null,
36
+ md.reasoning || model.thinking ? "think" : null,
37
+ model.features.distilled ? "distill" : null,
38
+ ]
39
+ .filter(Boolean)
40
+ .join(",");
41
+ return {
42
+ model: model.displayName.slice(0, 45),
43
+ arch: String(md.arch ?? "-"),
44
+ quant: model.quant ?? "-",
45
+ size: formatBytes(model.sizeBytes),
46
+ ctx: md.contextLength ? String(md.contextLength) : "-",
47
+ vision: model.mmprojPath ? "yes" : "",
48
+ calibrated: getCalibration(store, model, profile) ? "yes" : "",
49
+ features: feat,
50
+ source: model.origin ?? "-",
51
+ };
52
+ });
53
+ return { type: "list", data: rows, schema: scanSchema };
54
+ }
55
+ //# sourceMappingURL=scan.js.map
@@ -0,0 +1,31 @@
1
+ /**
2
+ * `otto brain search <query>` and `otto brain repo-quants <repo>` — Hugging Face
3
+ * model discovery. These wrap the surface-agnostic primitives in models/hf.ts so
4
+ * the same code serves the TUI (in-process) and the Otto app (daemon shells out
5
+ * to `--json`, mirroring `catalog`/`scan`).
6
+ */
7
+ import type { Command } from "commander";
8
+ import type { AnyCommandResult } from "../output/index.js";
9
+ export interface SearchRow {
10
+ repo: string;
11
+ downloads: number;
12
+ likes: number;
13
+ gated: string;
14
+ installed: boolean;
15
+ }
16
+ export declare function addSearchOptions(cmd: Command): Command;
17
+ export declare function runSearchCommand(query: string, options: {
18
+ limit?: string;
19
+ }, _command: Command): Promise<AnyCommandResult<SearchRow>>;
20
+ export interface AddRow {
21
+ repo: string;
22
+ quant: string;
23
+ path: string;
24
+ size: string;
25
+ }
26
+ export declare function addAddOptions(cmd: Command): Command;
27
+ export declare function runAddCommand(repo: string, options: {
28
+ quant?: string;
29
+ listQuants?: boolean;
30
+ }, _command: Command): Promise<AnyCommandResult<AddRow>>;
31
+ //# sourceMappingURL=search.d.ts.map