@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.
- package/bin/otto-brain +2 -0
- package/dist/bench/context-corpus.d.ts +58 -0
- package/dist/bench/context-corpus.js +210 -0
- package/dist/bench/corpus.d.ts +31 -0
- package/dist/bench/corpus.js +379 -0
- package/dist/bench/curated-repos.d.ts +35 -0
- package/dist/bench/curated-repos.js +41 -0
- package/dist/bench/health.d.ts +50 -0
- package/dist/bench/health.js +112 -0
- package/dist/bench/index.d.ts +103 -0
- package/dist/bench/index.js +209 -0
- package/dist/bench/mine.d.ts +32 -0
- package/dist/bench/mine.js +101 -0
- package/dist/bench/repo-task.d.ts +93 -0
- package/dist/bench/repo-task.js +380 -0
- package/dist/bench/repo.d.ts +125 -0
- package/dist/bench/repo.js +195 -0
- package/dist/bench/rescore.d.ts +41 -0
- package/dist/bench/rescore.js +127 -0
- package/dist/bench/tasks.d.ts +124 -0
- package/dist/bench/tasks.js +1249 -0
- package/dist/bench/verify.d.ts +142 -0
- package/dist/bench/verify.js +350 -0
- package/dist/cli.d.ts +13 -0
- package/dist/cli.js +72 -0
- package/dist/commands/bench.d.ts +20 -0
- package/dist/commands/bench.js +222 -0
- package/dist/commands/calibrate.d.ts +19 -0
- package/dist/commands/calibrate.js +75 -0
- package/dist/commands/catalog.d.ts +32 -0
- package/dist/commands/catalog.js +48 -0
- package/dist/commands/config.d.ts +13 -0
- package/dist/commands/config.js +90 -0
- package/dist/commands/lifecycle.d.ts +29 -0
- package/dist/commands/lifecycle.js +194 -0
- package/dist/commands/pull.d.ts +22 -0
- package/dist/commands/pull.js +135 -0
- package/dist/commands/report.d.ts +10 -0
- package/dist/commands/report.js +34 -0
- package/dist/commands/rescore.d.ts +11 -0
- package/dist/commands/rescore.js +48 -0
- package/dist/commands/runtime.d.ts +21 -0
- package/dist/commands/runtime.js +62 -0
- package/dist/commands/scan.d.ts +24 -0
- package/dist/commands/scan.js +55 -0
- package/dist/commands/search.d.ts +31 -0
- package/dist/commands/search.js +135 -0
- package/dist/commands/share.d.ts +26 -0
- package/dist/commands/share.js +148 -0
- package/dist/commands/sweep.d.ts +21 -0
- package/dist/commands/sweep.js +69 -0
- package/dist/commands/ui.d.ts +9 -0
- package/dist/commands/ui.js +28 -0
- package/dist/config/env.d.ts +10 -0
- package/dist/config/env.js +48 -0
- package/dist/config/index.d.ts +9 -0
- package/dist/config/index.js +9 -0
- package/dist/config/otto-home.d.ts +2 -0
- package/dist/config/otto-home.js +24 -0
- package/dist/config/paths.d.ts +20 -0
- package/dist/config/paths.js +33 -0
- package/dist/config/private-files.d.ts +3 -0
- package/dist/config/private-files.js +32 -0
- package/dist/config/profiles.d.ts +34 -0
- package/dist/config/profiles.js +116 -0
- package/dist/config/schema.d.ts +1035 -0
- package/dist/config/schema.js +191 -0
- package/dist/config/store.d.ts +11 -0
- package/dist/config/store.js +73 -0
- package/dist/gguf.d.ts +61 -0
- package/dist/gguf.js +231 -0
- package/dist/gpu.d.ts +12 -0
- package/dist/gpu.js +49 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +13 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +5 -0
- package/dist/models/dirs.d.ts +9 -0
- package/dist/models/dirs.js +21 -0
- package/dist/models/download.d.ts +31 -0
- package/dist/models/download.js +105 -0
- package/dist/models/enrich.d.ts +36 -0
- package/dist/models/enrich.js +68 -0
- package/dist/models/hf.d.ts +62 -0
- package/dist/models/hf.js +114 -0
- package/dist/models/index.d.ts +15 -0
- package/dist/models/index.js +49 -0
- package/dist/models/manage.d.ts +27 -0
- package/dist/models/manage.js +112 -0
- package/dist/models/pick.d.ts +3 -0
- package/dist/models/pick.js +36 -0
- package/dist/models/scan.d.ts +13 -0
- package/dist/models/scan.js +185 -0
- package/dist/ops/archive.d.ts +34 -0
- package/dist/ops/archive.js +105 -0
- package/dist/ops/calibrate.d.ts +49 -0
- package/dist/ops/calibrate.js +74 -0
- package/dist/ops/report.d.ts +26 -0
- package/dist/ops/report.js +532 -0
- package/dist/ops/results.d.ts +207 -0
- package/dist/ops/results.js +250 -0
- package/dist/ops/sweep.d.ts +77 -0
- package/dist/ops/sweep.js +126 -0
- package/dist/output/index.d.ts +5 -0
- package/dist/output/index.js +5 -0
- package/dist/output/render.d.ts +4 -0
- package/dist/output/render.js +96 -0
- package/dist/output/types.d.ts +52 -0
- package/dist/output/types.js +9 -0
- package/dist/output/with-output.d.ts +3 -0
- package/dist/output/with-output.js +42 -0
- package/dist/run.d.ts +5 -0
- package/dist/run.js +19 -0
- package/dist/runtime/args.d.ts +21 -0
- package/dist/runtime/args.js +71 -0
- package/dist/runtime/index.d.ts +13 -0
- package/dist/runtime/index.js +41 -0
- package/dist/runtime/lmstudio.d.ts +10 -0
- package/dist/runtime/lmstudio.js +88 -0
- package/dist/runtime/managed.d.ts +26 -0
- package/dist/runtime/managed.js +148 -0
- package/dist/service/model-selector.d.ts +36 -0
- package/dist/service/model-selector.js +123 -0
- package/dist/service/pid-lock.d.ts +18 -0
- package/dist/service/pid-lock.js +50 -0
- package/dist/service/router.d.ts +144 -0
- package/dist/service/router.js +746 -0
- package/dist/service/scheduler.d.ts +74 -0
- package/dist/service/scheduler.js +110 -0
- package/dist/service/serve.d.ts +41 -0
- package/dist/service/serve.js +278 -0
- package/dist/service/supervisor.d.ts +67 -0
- package/dist/service/supervisor.js +222 -0
- package/dist/service/tailscale.d.ts +19 -0
- package/dist/service/tailscale.js +62 -0
- package/dist/service/tls.d.ts +77 -0
- package/dist/service/tls.js +192 -0
- package/dist/sysmon.d.ts +50 -0
- package/dist/sysmon.js +103 -0
- package/dist/tui/app.d.ts +208 -0
- package/dist/tui/app.js +1801 -0
- package/dist/tui/screen.d.ts +72 -0
- package/dist/tui/screen.js +176 -0
- package/dist/types.d.ts +76 -0
- package/dist/types.js +7 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +8 -0
- package/dist/vram.d.ts +67 -0
- package/dist/vram.js +132 -0
- package/package.json +58 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { loadBrainConfig } from "../config/index.js";
|
|
2
|
+
import { downloadRepoFiles, listRepoQuants, managedModelsDir, repoOfModel, resolveHfToken, scanModels, searchModels, } from "../models/index.js";
|
|
3
|
+
import { formatBytes } from "../models/scan.js";
|
|
4
|
+
import { CommandError } from "../output/types.js";
|
|
5
|
+
/** What is already on disk, so search + quant listings can mark installed rows. */
|
|
6
|
+
function installedIndex(config) {
|
|
7
|
+
const repos = new Set();
|
|
8
|
+
const quants = new Set();
|
|
9
|
+
for (const model of scanModels(config)) {
|
|
10
|
+
const repo = repoOfModel(model);
|
|
11
|
+
if (!repo)
|
|
12
|
+
continue;
|
|
13
|
+
repos.add(repo.toLowerCase());
|
|
14
|
+
if (model.quant)
|
|
15
|
+
quants.add(`${repo.toLowerCase()} ${model.quant.toUpperCase()}`);
|
|
16
|
+
}
|
|
17
|
+
return { repos, quants };
|
|
18
|
+
}
|
|
19
|
+
const searchSchema = {
|
|
20
|
+
idField: "repo",
|
|
21
|
+
columns: [
|
|
22
|
+
{ header: "REPO", field: "repo", width: 48 },
|
|
23
|
+
{ header: "DOWNLOADS", field: "downloads", width: 11, align: "right" },
|
|
24
|
+
{ header: "LIKES", field: "likes", width: 6, align: "right" },
|
|
25
|
+
{ header: "GATED", field: "gated", width: 6 },
|
|
26
|
+
{ header: "HAVE", field: (r) => (r.installed ? "yes" : ""), width: 5 },
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
export function addSearchOptions(cmd) {
|
|
30
|
+
return cmd
|
|
31
|
+
.description("Search Hugging Face for GGUF models")
|
|
32
|
+
.argument("<query>", "search text")
|
|
33
|
+
.option("--limit <n>", "maximum results", "25");
|
|
34
|
+
}
|
|
35
|
+
export async function runSearchCommand(query, options, _command) {
|
|
36
|
+
const config = loadBrainConfig();
|
|
37
|
+
const token = resolveHfToken(config);
|
|
38
|
+
const limit = Math.max(1, Math.min(100, Number(options.limit) || 25));
|
|
39
|
+
const results = await searchModels(query, { limit, token });
|
|
40
|
+
const { repos } = installedIndex(config);
|
|
41
|
+
return {
|
|
42
|
+
type: "list",
|
|
43
|
+
data: results.map((r) => ({
|
|
44
|
+
repo: r.repo,
|
|
45
|
+
downloads: r.downloads,
|
|
46
|
+
likes: r.likes,
|
|
47
|
+
gated: r.gated ? "yes" : "",
|
|
48
|
+
installed: repos.has(r.repo.toLowerCase()),
|
|
49
|
+
})),
|
|
50
|
+
schema: searchSchema,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const addSchema = {
|
|
54
|
+
idField: "repo",
|
|
55
|
+
columns: [
|
|
56
|
+
{ header: "REPO", field: "repo", width: 40 },
|
|
57
|
+
{ header: "QUANT", field: "quant", width: 12 },
|
|
58
|
+
{ header: "PATH", field: "path", width: 40 },
|
|
59
|
+
{ header: "SIZE", field: "size", width: 10, align: "right" },
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
const addQuantSchema = {
|
|
63
|
+
idField: "quant",
|
|
64
|
+
columns: [
|
|
65
|
+
{ header: "QUANT", field: "quant", width: 12 },
|
|
66
|
+
{ header: "SIZE", field: "size", width: 10, align: "right" },
|
|
67
|
+
{ header: "FILES", field: "files", width: 6, align: "right" },
|
|
68
|
+
{ header: "HAVE", field: (r) => (r.installed ? "yes" : ""), width: 5 },
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
export function addAddOptions(cmd) {
|
|
72
|
+
return cmd
|
|
73
|
+
.description("Download a model from an arbitrary Hugging Face repo")
|
|
74
|
+
.argument("<repo>", "owner/repo on Hugging Face")
|
|
75
|
+
.option("--quant <label>", "quantization to download (e.g. Q5_K_M)")
|
|
76
|
+
.option("--list-quants", "list the quantizations the repo offers and exit");
|
|
77
|
+
}
|
|
78
|
+
export async function runAddCommand(repo, options, _command) {
|
|
79
|
+
const config = loadBrainConfig();
|
|
80
|
+
const token = resolveHfToken(config);
|
|
81
|
+
const { quants, mmproj } = await listRepoQuants(repo, token);
|
|
82
|
+
if (options.listQuants || !options.quant) {
|
|
83
|
+
const { quants: installedQuants } = installedIndex(config);
|
|
84
|
+
const listing = {
|
|
85
|
+
type: "list",
|
|
86
|
+
data: quants.map((q) => ({
|
|
87
|
+
quant: q.quant,
|
|
88
|
+
size: formatBytes(q.sizeBytes),
|
|
89
|
+
sizeBytes: q.sizeBytes,
|
|
90
|
+
files: q.files.length,
|
|
91
|
+
installed: installedQuants.has(`${repo.toLowerCase()} ${q.quant.toUpperCase()}`),
|
|
92
|
+
})),
|
|
93
|
+
schema: addQuantSchema,
|
|
94
|
+
};
|
|
95
|
+
// withOutput infers one row type; the renderer is schema-driven, so a
|
|
96
|
+
// differently-shaped result renders correctly at runtime.
|
|
97
|
+
return listing;
|
|
98
|
+
}
|
|
99
|
+
const wanted = options.quant.toLowerCase();
|
|
100
|
+
const choice = quants.find((q) => q.quant.toLowerCase() === wanted);
|
|
101
|
+
if (!choice) {
|
|
102
|
+
throw new CommandError({
|
|
103
|
+
code: "NO_QUANT",
|
|
104
|
+
message: `${repo} has no ${options.quant} - available: ${quants.map((q) => q.quant).join(", ") || "none"}`,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
const files = [...choice.files, ...(mmproj ? mmproj.files : [])];
|
|
108
|
+
const total = choice.sizeBytes + (mmproj?.sizeBytes ?? 0);
|
|
109
|
+
let lastPct = -1;
|
|
110
|
+
const written = await downloadRepoFiles({
|
|
111
|
+
repo,
|
|
112
|
+
files,
|
|
113
|
+
destRoot: managedModelsDir(config),
|
|
114
|
+
token,
|
|
115
|
+
onProgress: (p) => {
|
|
116
|
+
const pct = total ? Math.floor((p.receivedBytes / total) * 100) : 0;
|
|
117
|
+
if (pct !== lastPct && pct % 5 === 0) {
|
|
118
|
+
lastPct = pct;
|
|
119
|
+
process.stderr.write(` ${repo} ${choice.quant}: ${pct}%\r`);
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
process.stderr.write("\n");
|
|
124
|
+
return {
|
|
125
|
+
type: "single",
|
|
126
|
+
data: {
|
|
127
|
+
repo,
|
|
128
|
+
quant: choice.quant,
|
|
129
|
+
path: written[0] ?? "(already present)",
|
|
130
|
+
size: formatBytes(total),
|
|
131
|
+
},
|
|
132
|
+
schema: addSchema,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=search.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import type { AnyCommandResult } from "../output/index.js";
|
|
3
|
+
interface ShareStatus {
|
|
4
|
+
shared: boolean;
|
|
5
|
+
bind: string;
|
|
6
|
+
access: "open" | "key" | "none";
|
|
7
|
+
hasKey: boolean;
|
|
8
|
+
/** The access key, only when it was just generated (so it is shown once). */
|
|
9
|
+
key: string | null;
|
|
10
|
+
https: string;
|
|
11
|
+
allowRemoteConfig: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface ShareOptions {
|
|
14
|
+
on?: boolean;
|
|
15
|
+
off?: boolean;
|
|
16
|
+
bind?: string;
|
|
17
|
+
access?: string;
|
|
18
|
+
key?: string;
|
|
19
|
+
generate?: boolean;
|
|
20
|
+
allowConfig?: boolean;
|
|
21
|
+
https?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function addShareOptions(cmd: Command): Command;
|
|
24
|
+
export declare function runShareCommand(options: ShareOptions, _command: Command): Promise<AnyCommandResult<ShareStatus>>;
|
|
25
|
+
export {};
|
|
26
|
+
//# sourceMappingURL=share.d.ts.map
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `otto brain share` — opt a brain into being reachable (and optionally
|
|
3
|
+
* configurable) by other Otto hosts. Off by default: a brain binds loopback and
|
|
4
|
+
* is invisible to the network until its owner runs this. Sets the bind, the
|
|
5
|
+
* access level (open on a trusted network, or a bearer key), HTTPS, and whether
|
|
6
|
+
* key holders may reconfigure it. Writes $OTTO_HOME/otto-brain/config.json — the
|
|
7
|
+
* same fields the Otto app's Sharing UI sets, so the two stay in sync.
|
|
8
|
+
*/
|
|
9
|
+
import { randomBytes } from "node:crypto";
|
|
10
|
+
import { loadPersistedConfig, saveBrainConfig } from "../config/index.js";
|
|
11
|
+
import { CommandError } from "../output/types.js";
|
|
12
|
+
import * as tailscale from "../service/tailscale.js";
|
|
13
|
+
function isLoopback(host) {
|
|
14
|
+
return host === "127.0.0.1" || host === "::1" || host === "localhost" || host === "";
|
|
15
|
+
}
|
|
16
|
+
function toStatus(config, generatedKey) {
|
|
17
|
+
const shared = !isLoopback(config.listen.host);
|
|
18
|
+
const access = config.auth.mode === "token" ? "key" : shared ? "open" : "none";
|
|
19
|
+
return {
|
|
20
|
+
shared,
|
|
21
|
+
bind: config.listen.host,
|
|
22
|
+
access,
|
|
23
|
+
hasKey: Boolean(config.auth.token),
|
|
24
|
+
key: generatedKey,
|
|
25
|
+
https: config.tls.mode,
|
|
26
|
+
allowRemoteConfig: config.allowRemoteConfig,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const shareSchema = {
|
|
30
|
+
idField: () => "share",
|
|
31
|
+
columns: [{ header: "SHARE", field: () => "share" }],
|
|
32
|
+
renderHuman: (data) => {
|
|
33
|
+
const d = Array.isArray(data) ? data[0] : data;
|
|
34
|
+
if (!d)
|
|
35
|
+
return "";
|
|
36
|
+
const lines = [
|
|
37
|
+
`Sharing: ${d.shared ? "on" : "off (loopback only)"}`,
|
|
38
|
+
`Bind: ${d.bind}`,
|
|
39
|
+
`Access: ${d.access}${d.access === "key" && !d.hasKey ? " (no key set!)" : ""}`,
|
|
40
|
+
`HTTPS: ${d.https}`,
|
|
41
|
+
`Remote config: ${d.allowRemoteConfig ? "allowed" : "disabled"}`,
|
|
42
|
+
];
|
|
43
|
+
if (d.key) {
|
|
44
|
+
lines.push("", "Access key (copy this to the connecting host):", ` ${d.key}`);
|
|
45
|
+
}
|
|
46
|
+
return lines.join("\n");
|
|
47
|
+
},
|
|
48
|
+
serialize: (d) => d,
|
|
49
|
+
};
|
|
50
|
+
export function addShareOptions(cmd) {
|
|
51
|
+
return cmd
|
|
52
|
+
.description("Share this brain with other Otto hosts (off by default)")
|
|
53
|
+
.option("--on", "turn sharing on")
|
|
54
|
+
.option("--off", "turn sharing off (bind loopback only)")
|
|
55
|
+
.option("--bind <where>", "network to bind: tailscale, all, or an explicit IP/host")
|
|
56
|
+
.option("--access <mode>", "open (trusted network) or key (bearer token)")
|
|
57
|
+
.option("--key <token>", "use this access key")
|
|
58
|
+
.option("--generate", "generate a fresh access key")
|
|
59
|
+
.option("--allow-config", "let key holders change model/lock over the network")
|
|
60
|
+
.option("--no-allow-config", "forbid remote configuration")
|
|
61
|
+
.option("--https <mode>", "off, self-signed, or tailscale");
|
|
62
|
+
}
|
|
63
|
+
function resolveBind(bind) {
|
|
64
|
+
if (bind === "all")
|
|
65
|
+
return "0.0.0.0";
|
|
66
|
+
if (bind === "tailscale")
|
|
67
|
+
return "tailscale";
|
|
68
|
+
return bind;
|
|
69
|
+
}
|
|
70
|
+
function validateAccess(access) {
|
|
71
|
+
if (access !== "open" && access !== "key") {
|
|
72
|
+
throw new CommandError({ code: "BAD_ACCESS", message: `--access must be "open" or "key"` });
|
|
73
|
+
}
|
|
74
|
+
return access;
|
|
75
|
+
}
|
|
76
|
+
export async function runShareCommand(options, _command) {
|
|
77
|
+
const config = loadPersistedConfig();
|
|
78
|
+
const anyChange = options.on ||
|
|
79
|
+
options.off ||
|
|
80
|
+
options.bind !== undefined ||
|
|
81
|
+
options.access !== undefined ||
|
|
82
|
+
options.key !== undefined ||
|
|
83
|
+
options.generate ||
|
|
84
|
+
options.allowConfig !== undefined ||
|
|
85
|
+
options.https !== undefined;
|
|
86
|
+
if (!anyChange) {
|
|
87
|
+
return { type: "single", data: toStatus(config, null), schema: shareSchema };
|
|
88
|
+
}
|
|
89
|
+
if (options.off) {
|
|
90
|
+
config.listen.host = "127.0.0.1";
|
|
91
|
+
config.auth.mode = "none";
|
|
92
|
+
config.allowInsecureBind = false;
|
|
93
|
+
config.allowRemoteConfig = false;
|
|
94
|
+
saveBrainConfig(config);
|
|
95
|
+
return { type: "single", data: toStatus(config, null), schema: shareSchema };
|
|
96
|
+
}
|
|
97
|
+
// Bind: explicit wins; otherwise turning on a loopback brain defaults to the
|
|
98
|
+
// tailnet when Tailscale is present (private + encrypted), else all interfaces.
|
|
99
|
+
if (options.bind !== undefined) {
|
|
100
|
+
config.listen.host = resolveBind(options.bind);
|
|
101
|
+
}
|
|
102
|
+
else if (options.on && isLoopback(config.listen.host)) {
|
|
103
|
+
config.listen.host = (await tailscale.isAvailable()) ? "tailscale" : "0.0.0.0";
|
|
104
|
+
}
|
|
105
|
+
// Access: key by default when turning on (secure by default). "open" allows an
|
|
106
|
+
// unauthenticated non-loopback bind for a trusted network.
|
|
107
|
+
const access = options.access
|
|
108
|
+
? validateAccess(options.access)
|
|
109
|
+
: options.key !== undefined || options.generate
|
|
110
|
+
? "key"
|
|
111
|
+
: options.on
|
|
112
|
+
? "key"
|
|
113
|
+
: config.auth.mode === "token"
|
|
114
|
+
? "key"
|
|
115
|
+
: "open";
|
|
116
|
+
let generatedKey = null;
|
|
117
|
+
if (access === "open") {
|
|
118
|
+
config.auth.mode = "none";
|
|
119
|
+
config.allowInsecureBind = true;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
config.auth.mode = "token";
|
|
123
|
+
config.allowInsecureBind = false;
|
|
124
|
+
if (options.key !== undefined) {
|
|
125
|
+
config.auth.token = options.key;
|
|
126
|
+
}
|
|
127
|
+
else if (options.generate || !config.auth.token) {
|
|
128
|
+
generatedKey = randomBytes(24).toString("base64url");
|
|
129
|
+
config.auth.token = generatedKey;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (options.https !== undefined) {
|
|
133
|
+
const mode = options.https;
|
|
134
|
+
if (mode !== "off" && mode !== "self-signed" && mode !== "tailscale" && mode !== "files") {
|
|
135
|
+
throw new CommandError({
|
|
136
|
+
code: "BAD_HTTPS",
|
|
137
|
+
message: `--https must be off, self-signed, tailscale, or files`,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
config.tls.mode = mode;
|
|
141
|
+
}
|
|
142
|
+
if (options.allowConfig !== undefined) {
|
|
143
|
+
config.allowRemoteConfig = options.allowConfig;
|
|
144
|
+
}
|
|
145
|
+
saveBrainConfig(config);
|
|
146
|
+
return { type: "single", data: toStatus(config, generatedKey), schema: shareSchema };
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=share.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `otto brain sweep` — find the reasoning budget that yields the best output, and
|
|
3
|
+
* save it to the model's profile. Guards against the thinking-model failure where
|
|
4
|
+
* an unrestricted budget returns pure reasoning and no content.
|
|
5
|
+
*/
|
|
6
|
+
import type { Command } from "commander";
|
|
7
|
+
import type { AnyCommandResult } from "../output/index.js";
|
|
8
|
+
export interface SweepRow {
|
|
9
|
+
budget: number | string;
|
|
10
|
+
content: string;
|
|
11
|
+
files: string;
|
|
12
|
+
tokensPerSec: string;
|
|
13
|
+
seconds: string;
|
|
14
|
+
best: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function addSweepOptions(cmd: Command): Command;
|
|
17
|
+
export declare function runSweepCommand(options: {
|
|
18
|
+
model?: string;
|
|
19
|
+
budgets?: string;
|
|
20
|
+
}, _command: Command): Promise<AnyCommandResult<SweepRow>>;
|
|
21
|
+
//# sourceMappingURL=sweep.d.ts.map
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { forModel, loadBrainConfig, loadProfilesStore, put, saveProfilesStore, } from "../config/index.js";
|
|
2
|
+
import { pickModel, scanModels } from "../models/index.js";
|
|
3
|
+
import { CommandError } from "../output/types.js";
|
|
4
|
+
import { sweep } from "../ops/sweep.js";
|
|
5
|
+
import { resolveRuntime } from "../runtime/index.js";
|
|
6
|
+
const sweepSchema = {
|
|
7
|
+
idField: "budget",
|
|
8
|
+
columns: [
|
|
9
|
+
{ header: "BUDGET", field: "budget", width: 8, align: "right" },
|
|
10
|
+
{ header: "CONTENT", field: "content", width: 9, align: "right" },
|
|
11
|
+
{ header: "FILES", field: "files", width: 6, align: "right" },
|
|
12
|
+
{ header: "TOK/S", field: "tokensPerSec", width: 7, align: "right" },
|
|
13
|
+
{ header: "TIME", field: "seconds", width: 6, align: "right" },
|
|
14
|
+
{ header: "", field: "best", width: 8, color: () => "green" },
|
|
15
|
+
],
|
|
16
|
+
};
|
|
17
|
+
export function addSweepOptions(cmd) {
|
|
18
|
+
return cmd
|
|
19
|
+
.description("Find and save the best reasoning budget")
|
|
20
|
+
.option("--model <fragment>", "model name fragment or catalog id")
|
|
21
|
+
.option("--budgets <a,b,c>", "reasoning budgets to sweep");
|
|
22
|
+
}
|
|
23
|
+
export async function runSweepCommand(options, _command) {
|
|
24
|
+
const config = loadBrainConfig();
|
|
25
|
+
const runtime = resolveRuntime(config);
|
|
26
|
+
if (!runtime) {
|
|
27
|
+
throw new CommandError({
|
|
28
|
+
code: "NO_RUNTIME",
|
|
29
|
+
message: "no llama.cpp runtime available",
|
|
30
|
+
details: "run `otto brain runtime install`",
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const store = loadProfilesStore();
|
|
34
|
+
const catalog = scanModels(config);
|
|
35
|
+
const model = pickModel(catalog, options.model ?? store.lastModelId ?? undefined);
|
|
36
|
+
const profile = forModel(store, model, config.defaults);
|
|
37
|
+
const budgets = options.budgets
|
|
38
|
+
? options.budgets.split(",").map((s) => Number(s.trim()))
|
|
39
|
+
: undefined;
|
|
40
|
+
const report = await sweep({
|
|
41
|
+
runtime,
|
|
42
|
+
model,
|
|
43
|
+
profile,
|
|
44
|
+
budgets,
|
|
45
|
+
onProgress: (p) => {
|
|
46
|
+
if (p.phase === "loading")
|
|
47
|
+
process.stderr.write(` budget ${String(p.budget).padStart(5)}: loading…\n`);
|
|
48
|
+
if (p.phase === "done")
|
|
49
|
+
process.stderr.write(` budget ${String(p.budget).padStart(5)}: done\n`);
|
|
50
|
+
if (p.phase === "failed")
|
|
51
|
+
process.stderr.write(` budget ${String(p.budget).padStart(5)}: failed ${p.error}\n`);
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
if (report.recommended !== null && report.recommended !== undefined) {
|
|
55
|
+
profile.reasoningBudget = report.recommended;
|
|
56
|
+
put(store, model, profile);
|
|
57
|
+
saveProfilesStore(store);
|
|
58
|
+
}
|
|
59
|
+
const rows = report.results.map((r) => ({
|
|
60
|
+
budget: r.budget,
|
|
61
|
+
content: r.error ? "error" : `${(r.contentChars / 1024).toFixed(1)}KB`,
|
|
62
|
+
files: r.error ? "-" : `${r.filesDelivered}/4`,
|
|
63
|
+
tokensPerSec: (r.tokensPerSecond ?? 0).toFixed(1),
|
|
64
|
+
seconds: `${(r.elapsedSeconds ?? 0).toFixed(0)}s`,
|
|
65
|
+
best: r.budget === report.recommended ? "<- best" : "",
|
|
66
|
+
}));
|
|
67
|
+
return { type: "list", data: rows, schema: sweepSchema };
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=sweep.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `otto brain ui` (and the bare `otto brain`) — the full-screen interactive TUI,
|
|
3
|
+
* the tool's signature experience. Gated on a TTY; falls back with guidance when
|
|
4
|
+
* piped. Bypasses the output layer since it owns the whole screen.
|
|
5
|
+
*/
|
|
6
|
+
import type { Command } from "commander";
|
|
7
|
+
export declare function addUiOptions(cmd: Command): Command;
|
|
8
|
+
export declare function runUiCommand(_options: unknown, _command: Command): Promise<void>;
|
|
9
|
+
//# sourceMappingURL=ui.d.ts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { loadBrainConfig } from "../config/index.js";
|
|
2
|
+
import { CommandError } from "../output/types.js";
|
|
3
|
+
import { resolveRuntime } from "../runtime/index.js";
|
|
4
|
+
export function addUiOptions(cmd) {
|
|
5
|
+
return cmd.description("Launch the interactive full-screen UI");
|
|
6
|
+
}
|
|
7
|
+
export async function runUiCommand(_options, _command) {
|
|
8
|
+
if (!process.stdout.isTTY) {
|
|
9
|
+
throw new CommandError({
|
|
10
|
+
code: "NO_TTY",
|
|
11
|
+
message: "the interactive UI needs a TTY",
|
|
12
|
+
details: "use `scan`, `serve`, `calibrate`, or `sweep` instead",
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
const config = loadBrainConfig();
|
|
16
|
+
const runtime = resolveRuntime(config);
|
|
17
|
+
if (!runtime) {
|
|
18
|
+
throw new CommandError({
|
|
19
|
+
code: "NO_RUNTIME",
|
|
20
|
+
message: "no llama.cpp runtime available",
|
|
21
|
+
details: "run `otto brain runtime install`",
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
const { App } = await import("../tui/app.js");
|
|
25
|
+
const app = new App({ runtime, listenPort: config.listen.port, listenHost: config.listen.host });
|
|
26
|
+
await app.run();
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=ui.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Env-var overrides layered over persisted config, matching Otto's precedence
|
|
3
|
+
* (CLI → env → file → default). The brain keeps its own `OTTO_BRAIN_*` namespace
|
|
4
|
+
* (like `OTTO_RELAY_*`), plus it honors `OTTO_HOME` via the path resolver.
|
|
5
|
+
* Overrides are applied in memory only; they are never written back to the file.
|
|
6
|
+
*/
|
|
7
|
+
import type { BrainConfig } from "./schema.js";
|
|
8
|
+
export declare function parseBooleanEnv(value: string | undefined): boolean | undefined;
|
|
9
|
+
export declare function applyEnvOverrides(config: BrainConfig, env: NodeJS.ProcessEnv): BrainConfig;
|
|
10
|
+
//# sourceMappingURL=env.d.ts.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const TRUE = new Set(["1", "true", "yes", "on"]);
|
|
2
|
+
const FALSE = new Set(["0", "false", "no", "off"]);
|
|
3
|
+
export function parseBooleanEnv(value) {
|
|
4
|
+
if (value === undefined)
|
|
5
|
+
return undefined;
|
|
6
|
+
const v = value.trim().toLowerCase();
|
|
7
|
+
if (TRUE.has(v))
|
|
8
|
+
return true;
|
|
9
|
+
if (FALSE.has(v))
|
|
10
|
+
return false;
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
export function applyEnvOverrides(config, env) {
|
|
14
|
+
const next = {
|
|
15
|
+
...config,
|
|
16
|
+
listen: { ...config.listen },
|
|
17
|
+
auth: { ...config.auth },
|
|
18
|
+
tls: { ...config.tls },
|
|
19
|
+
runtime: { ...config.runtime },
|
|
20
|
+
defaults: { ...config.defaults },
|
|
21
|
+
};
|
|
22
|
+
const enabled = parseBooleanEnv(env.OTTO_BRAIN_ENABLED);
|
|
23
|
+
if (enabled !== undefined)
|
|
24
|
+
next.enabled = enabled;
|
|
25
|
+
const autoStart = parseBooleanEnv(env.OTTO_BRAIN_AUTOSTART);
|
|
26
|
+
if (autoStart !== undefined)
|
|
27
|
+
next.autoStart = autoStart;
|
|
28
|
+
if (env.OTTO_BRAIN_LISTEN)
|
|
29
|
+
next.listen.host = env.OTTO_BRAIN_LISTEN;
|
|
30
|
+
if (env.OTTO_BRAIN_PORT) {
|
|
31
|
+
const port = Number(env.OTTO_BRAIN_PORT);
|
|
32
|
+
if (Number.isFinite(port))
|
|
33
|
+
next.listen.port = port;
|
|
34
|
+
}
|
|
35
|
+
if (env.OTTO_BRAIN_TOKEN) {
|
|
36
|
+
next.auth = { mode: "token", token: env.OTTO_BRAIN_TOKEN };
|
|
37
|
+
}
|
|
38
|
+
// Back-compat: the old runtime override env var points at a runtime directory.
|
|
39
|
+
if (env.OTTO_BRAIN_LLAMA_SERVER) {
|
|
40
|
+
next.runtime = { ...next.runtime, path: env.OTTO_BRAIN_LLAMA_SERVER };
|
|
41
|
+
}
|
|
42
|
+
if (env.OTTO_BRAIN_MODELS_DIR)
|
|
43
|
+
next.modelsDir = env.OTTO_BRAIN_MODELS_DIR;
|
|
44
|
+
if (env.OTTO_BRAIN_MODEL)
|
|
45
|
+
next.defaultModel = env.OTTO_BRAIN_MODEL;
|
|
46
|
+
return next;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=env.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Barrel for the config subsystem: paths, schemas, stores, and profile logic. */
|
|
2
|
+
export { resolveOttoHome } from "./otto-home.js";
|
|
3
|
+
export { ensurePrivateDirectory, writePrivateFileAtomicSync } from "./private-files.js";
|
|
4
|
+
export { resolveBrainPaths, packageRoot, type BrainPaths } from "./paths.js";
|
|
5
|
+
export { parseBooleanEnv, applyEnvOverrides } from "./env.js";
|
|
6
|
+
export { loadBrainConfig, loadPersistedConfig, saveBrainConfig, loadProfilesStore, saveProfilesStore, loadCatalog, } from "./store.js";
|
|
7
|
+
export { defaultProfile, forModel, put, calibrationKey, geometryKey, getCalibration, putCalibration, } from "./profiles.js";
|
|
8
|
+
export * from "./schema.js";
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Barrel for the config subsystem: paths, schemas, stores, and profile logic. */
|
|
2
|
+
export { resolveOttoHome } from "./otto-home.js";
|
|
3
|
+
export { ensurePrivateDirectory, writePrivateFileAtomicSync } from "./private-files.js";
|
|
4
|
+
export { resolveBrainPaths, packageRoot } from "./paths.js";
|
|
5
|
+
export { parseBooleanEnv, applyEnvOverrides } from "./env.js";
|
|
6
|
+
export { loadBrainConfig, loadPersistedConfig, saveBrainConfig, loadProfilesStore, saveProfilesStore, loadCatalog, } from "./store.js";
|
|
7
|
+
export { defaultProfile, forModel, put, calibrationKey, geometryKey, getCalibration, putCalibration, } from "./profiles.js";
|
|
8
|
+
export * from "./schema.js";
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves $OTTO_HOME exactly as the Otto daemon does (see
|
|
3
|
+
* packages/server/src/server/otto-home.ts): env override, else `~/.otto`, tilde
|
|
4
|
+
* expanded, made private. Sharing this rule is what lets otto-brain's config sit
|
|
5
|
+
* next to Otto's — and follow it into the dev home whenever the dev scripts export
|
|
6
|
+
* OTTO_HOME. There is deliberately no dev-path logic here; that lives in shell.
|
|
7
|
+
*/
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { ensurePrivateDirectory } from "./private-files.js";
|
|
11
|
+
function expandHomeDir(input) {
|
|
12
|
+
if (input.startsWith("~/"))
|
|
13
|
+
return path.join(homedir(), input.slice(2));
|
|
14
|
+
if (input === "~")
|
|
15
|
+
return homedir();
|
|
16
|
+
return input;
|
|
17
|
+
}
|
|
18
|
+
export function resolveOttoHome(env = process.env) {
|
|
19
|
+
const raw = env.OTTO_HOME ?? "~/.otto";
|
|
20
|
+
const resolved = path.resolve(expandHomeDir(raw));
|
|
21
|
+
ensurePrivateDirectory(resolved);
|
|
22
|
+
return resolved;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=otto-home.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface BrainPaths {
|
|
2
|
+
home: string;
|
|
3
|
+
root: string;
|
|
4
|
+
configFile: string;
|
|
5
|
+
profilesFile: string;
|
|
6
|
+
catalogFile: string;
|
|
7
|
+
modelsDir: string;
|
|
8
|
+
runtimesDir: string;
|
|
9
|
+
pidFile: string;
|
|
10
|
+
logFile: string;
|
|
11
|
+
resultsDir: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function resolveBrainPaths(env?: NodeJS.ProcessEnv): BrainPaths;
|
|
14
|
+
/**
|
|
15
|
+
* The package root, resolved from this module's location whether running from
|
|
16
|
+
* `dist/config/` (built) or `src/config/` (tsx). Used only to find the legacy
|
|
17
|
+
* repo-local `config/` seed data during the one-time migration.
|
|
18
|
+
*/
|
|
19
|
+
export declare function packageRoot(): string;
|
|
20
|
+
//# sourceMappingURL=paths.d.ts.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The on-disk layout of otto-brain's home, a namespaced subdirectory of $OTTO_HOME
|
|
3
|
+
* (the dominant per-subsystem convention in Otto: projects/, chat/, loops/, …).
|
|
4
|
+
* Everything the brain persists lives under `$OTTO_HOME/otto-brain/`.
|
|
5
|
+
*/
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { resolveOttoHome } from "./otto-home.js";
|
|
9
|
+
export function resolveBrainPaths(env = process.env) {
|
|
10
|
+
const home = resolveOttoHome(env);
|
|
11
|
+
const root = path.join(home, "otto-brain");
|
|
12
|
+
return {
|
|
13
|
+
home,
|
|
14
|
+
root,
|
|
15
|
+
configFile: path.join(root, "config.json"),
|
|
16
|
+
profilesFile: path.join(root, "profiles.json"),
|
|
17
|
+
catalogFile: path.join(root, "catalog.json"),
|
|
18
|
+
modelsDir: path.join(root, "models"),
|
|
19
|
+
runtimesDir: path.join(root, "runtimes"),
|
|
20
|
+
pidFile: path.join(root, "otto-brain.pid"),
|
|
21
|
+
logFile: path.join(root, "otto-brain.log"),
|
|
22
|
+
resultsDir: path.join(root, "results"),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The package root, resolved from this module's location whether running from
|
|
27
|
+
* `dist/config/` (built) or `src/config/` (tsx). Used only to find the legacy
|
|
28
|
+
* repo-local `config/` seed data during the one-time migration.
|
|
29
|
+
*/
|
|
30
|
+
export function packageRoot() {
|
|
31
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=paths.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Private-file helpers mirroring the daemon's `private-files.ts`: directories are
|
|
3
|
+
* created `0700`, files written `0600`, and every write is atomic (temp + rename)
|
|
4
|
+
* so a crash mid-write never leaves a truncated config on disk. chmod is
|
|
5
|
+
* best-effort because Windows POSIX-mode support is partial.
|
|
6
|
+
*/
|
|
7
|
+
import { chmodSync, mkdirSync, renameSync, writeFileSync } from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
const DIR_MODE = 0o700;
|
|
10
|
+
const FILE_MODE = 0o600;
|
|
11
|
+
export function ensurePrivateDirectory(dir) {
|
|
12
|
+
mkdirSync(dir, { recursive: true, mode: DIR_MODE });
|
|
13
|
+
try {
|
|
14
|
+
chmodSync(dir, DIR_MODE);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// best effort: Windows filesystems may reject POSIX modes
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function writePrivateFileAtomicSync(filePath, contents) {
|
|
21
|
+
ensurePrivateDirectory(path.dirname(filePath));
|
|
22
|
+
const tmp = `${filePath}.${process.pid}.tmp`;
|
|
23
|
+
writeFileSync(tmp, contents, { mode: FILE_MODE });
|
|
24
|
+
renameSync(tmp, filePath);
|
|
25
|
+
try {
|
|
26
|
+
chmodSync(filePath, FILE_MODE);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// best effort
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=private-files.js.map
|