@otto-code/brain 0.8.7 → 0.8.9

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 (61) hide show
  1. package/dist/cli.js +2 -1
  2. package/dist/commands/bench.js +19 -5
  3. package/dist/commands/catalog.d.ts +3 -0
  4. package/dist/commands/catalog.js +2 -0
  5. package/dist/commands/pull.d.ts +1 -0
  6. package/dist/commands/pull.js +47 -14
  7. package/dist/commands/repo-download.d.ts +14 -0
  8. package/dist/commands/repo-download.js +29 -0
  9. package/dist/commands/runtime.d.ts +3 -0
  10. package/dist/commands/runtime.js +65 -18
  11. package/dist/commands/search.d.ts +3 -0
  12. package/dist/commands/search.js +38 -17
  13. package/dist/config/builtin-hosting-profiles.d.ts +8 -0
  14. package/dist/config/builtin-hosting-profiles.js +32 -0
  15. package/dist/config/hosting-profiles.d.ts +33 -0
  16. package/dist/config/hosting-profiles.js +71 -0
  17. package/dist/config/index.d.ts +1 -0
  18. package/dist/config/index.js +1 -0
  19. package/dist/config/paths.d.ts +2 -0
  20. package/dist/config/paths.js +1 -0
  21. package/dist/config/profile-edit.d.ts +5 -3
  22. package/dist/config/profile-edit.js +134 -14
  23. package/dist/config/profiles.js +66 -3
  24. package/dist/config/schema.d.ts +998 -0
  25. package/dist/config/schema.js +79 -0
  26. package/dist/config/store.js +28 -16
  27. package/dist/gguf.d.ts +1 -0
  28. package/dist/gguf.js +1 -0
  29. package/dist/models/download.d.ts +7 -0
  30. package/dist/models/download.js +165 -17
  31. package/dist/models/enrich.d.ts +6 -19
  32. package/dist/models/enrich.js +138 -4
  33. package/dist/models/hf.d.ts +14 -1
  34. package/dist/models/hf.js +239 -6
  35. package/dist/models/index.d.ts +2 -2
  36. package/dist/models/index.js +8 -5
  37. package/dist/models/manage.d.ts +4 -0
  38. package/dist/models/manage.js +34 -4
  39. package/dist/models/scan.js +8 -42
  40. package/dist/ops/calibrate.d.ts +4 -1
  41. package/dist/ops/calibrate.js +10 -7
  42. package/dist/ops/sweep.d.ts +3 -1
  43. package/dist/ops/sweep.js +3 -3
  44. package/dist/runtime/args.d.ts +2 -2
  45. package/dist/runtime/args.js +18 -1
  46. package/dist/runtime/index.d.ts +8 -1
  47. package/dist/runtime/index.js +11 -1
  48. package/dist/runtime/managed.d.ts +40 -0
  49. package/dist/runtime/managed.js +146 -7
  50. package/dist/service/host-api.d.ts +20 -3
  51. package/dist/service/host-api.js +313 -20
  52. package/dist/service/router.d.ts +18 -0
  53. package/dist/service/router.js +89 -4
  54. package/dist/service/serve.js +221 -22
  55. package/dist/service/supervisor.d.ts +32 -4
  56. package/dist/service/supervisor.js +30 -6
  57. package/dist/tui/app.js +1 -1
  58. package/dist/types.d.ts +24 -0
  59. package/dist/vram.d.ts +3 -0
  60. package/dist/vram.js +24 -6
  61. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -15,7 +15,7 @@ import { addRestartOptions, addServeOptions, addStartOptions, addStatusOptions,
15
15
  import { addPullOptions, runPullCommand } from "./commands/pull.js";
16
16
  import { addReportOptions, runReportCommand } from "./commands/report.js";
17
17
  import { addRescoreOptions, runRescoreCommand } from "./commands/rescore.js";
18
- import { addRuntimeInstallOptions, addRuntimeListOptions, runRuntimeInstallCommand, runRuntimeListCommand, } from "./commands/runtime.js";
18
+ import { addRuntimeInstallOptions, addRuntimeListOptions, addRuntimeRemoveOptions, runRuntimeInstallCommand, runRuntimeListCommand, runRuntimeRemoveCommand, } from "./commands/runtime.js";
19
19
  import { addScanOptions, runScanCommand } from "./commands/scan.js";
20
20
  import { addAddOptions, addSearchOptions, runAddCommand, runSearchCommand, } from "./commands/search.js";
21
21
  import { addSweepOptions, runSweepCommand } from "./commands/sweep.js";
@@ -57,6 +57,7 @@ export function registerBrainCommands(program) {
57
57
  const runtime = program.command("runtime").description("Manage the llama.cpp runtime");
58
58
  addRuntimeListOptions(runtime.command("list")).action(withOutput(runRuntimeListCommand));
59
59
  addRuntimeInstallOptions(runtime.command("install")).action(withOutput(runRuntimeInstallCommand));
60
+ addRuntimeRemoveOptions(runtime.command("remove")).action(withOutput(runRuntimeRemoveCommand));
60
61
  // config subgroup.
61
62
  const config = program.command("config").description("Inspect and edit brain config");
62
63
  addConfigShowOptions(config.command("show")).action(withOutput(runConfigShowCommand));
@@ -1,12 +1,14 @@
1
1
  /**
2
2
  * `otto brain bench` - score a model's agentic coding ability on this machine.
3
3
  * Either benchmarks an endpoint that is already serving (`--endpoint`) or loads
4
- * each requested model itself. A long streaming run, so it prints a formatted
5
- * report directly rather than going through the output layer.
4
+ * each requested model itself. Locally loaded runs use the router scheduler, so
5
+ * their requests receive the same profile system addendum as normal serving. A
6
+ * long streaming run, so it prints a formatted report directly rather than going
7
+ * through the output layer.
6
8
  */
7
9
  import http from "node:http";
8
10
  import path from "node:path";
9
- import { forModel, getCalibration, loadBrainConfig, loadProfilesStore } from "../config/index.js";
11
+ import { forModel, getCalibration, loadBrainConfig, loadProfilesStore, resolveBrainPaths, } from "../config/index.js";
10
12
  import { query as queryGpu } from "../gpu.js";
11
13
  import { pickModel, scanModels } from "../models/index.js";
12
14
  import { CommandError } from "../output/types.js";
@@ -190,9 +192,21 @@ async function runBenchSuite(options, _command) {
190
192
  profile = fit.profile;
191
193
  }
192
194
  process.stdout.write(`\n${"=".repeat(74)}\n${model.displayName}\n${"=".repeat(74)}\n`);
193
- const supervisor = new Supervisor({ runtime, internalPort: port });
195
+ const supervisor = new Supervisor({
196
+ runtime,
197
+ internalPort: port,
198
+ paths: resolveBrainPaths(),
199
+ getProfilesStore: () => store,
200
+ });
194
201
  const telemetry = new Telemetry();
195
- const server = http.createServer(createRouter({ supervisor, telemetry, getCatalog: () => catalog }));
202
+ const server = http.createServer(createRouter({
203
+ supervisor,
204
+ telemetry,
205
+ getCatalog: () => catalog,
206
+ loadModel: async (target) => {
207
+ await supervisor.start(target, forModel(store, target, config.defaults));
208
+ },
209
+ }));
196
210
  await new Promise((resolve, reject) => {
197
211
  server.once("error", reject);
198
212
  server.listen(port + 1, "127.0.0.1", resolve);
@@ -8,10 +8,12 @@
8
8
  * params, tier, use-cases) that only the JSON output surfaces.
9
9
  */
10
10
  import type { Command } from "commander";
11
+ import type { CatalogModel } from "../config/schema.js";
11
12
  import type { AnyCommandResult, OutputSchema } from "../output/index.js";
12
13
  export interface CatalogRow {
13
14
  id: string;
14
15
  name: string;
16
+ family: string | null;
15
17
  installed: boolean;
16
18
  publisher: string;
17
19
  repo: string;
@@ -25,6 +27,7 @@ export interface CatalogRow {
25
27
  tier: string;
26
28
  useCases: string[];
27
29
  why: string;
30
+ components?: CatalogModel["components"];
28
31
  }
29
32
  export declare const catalogSchema: OutputSchema<CatalogRow>;
30
33
  export declare function addCatalogOptions(cmd: Command): Command;
@@ -29,6 +29,7 @@ export async function runCatalogCommand(_options, _command) {
29
29
  const rows = catalog.models.map((model) => ({
30
30
  id: model.id,
31
31
  name: model.name,
32
+ family: model.family ?? null,
32
33
  installed: installedCatalogIds.has(model.id),
33
34
  publisher: model.publisher ?? "",
34
35
  repo: model.hfRepo,
@@ -42,6 +43,7 @@ export async function runCatalogCommand(_options, _command) {
42
43
  tier: model.tier ?? "",
43
44
  useCases: model.useCases ?? [],
44
45
  why: model.why ?? "",
46
+ components: model.components,
45
47
  }));
46
48
  return { type: "list", data: rows, schema: catalogSchema };
47
49
  }
@@ -17,6 +17,7 @@ export interface PullOptionsInput {
17
17
  file?: string;
18
18
  quant?: string;
19
19
  listQuants?: boolean;
20
+ component?: string[];
20
21
  }
21
22
  export declare function runPullCommand(modelArg: string, options: PullOptionsInput, _command: Command): Promise<AnyCommandResult<PullRow>>;
22
23
  //# sourceMappingURL=pull.d.ts.map
@@ -1,8 +1,9 @@
1
1
  import { loadBrainConfig, loadCatalog } from "../config/index.js";
2
- import { downloadRepoFiles, listRepoQuants, managedModelsDir, pullModel, resolveHfToken, } from "../models/index.js";
2
+ import { bundleDownloadPlan, listRepoQuants, managedModelsDir, pullModel, resolveHfToken, } from "../models/index.js";
3
3
  import { CommandError } from "../output/types.js";
4
4
  import { formatBytes } from "../models/scan.js";
5
5
  import { withActivity } from "../service/activity.js";
6
+ import { downloadRepoFilesWithProgress } from "./repo-download.js";
6
7
  const pullSchema = {
7
8
  idField: "model",
8
9
  columns: [
@@ -44,6 +45,7 @@ export function addPullOptions(cmd) {
44
45
  .argument("<model>", "catalog id or name fragment")
45
46
  .option("--file <name.gguf>", "explicit GGUF file name in the repo")
46
47
  .option("--quant <label>", "download a specific quantization (e.g. Q5_K_M)")
48
+ .option("--component <id...>", "download optional bundle component ids")
47
49
  .option("--list-quants", "list the quantizations the repo offers and exit");
48
50
  }
49
51
  export async function runPullCommand(modelArg, options, _command) {
@@ -51,6 +53,42 @@ export async function runPullCommand(modelArg, options, _command) {
51
53
  const catalog = loadCatalog();
52
54
  const model = findCatalogModel(catalog.models, modelArg);
53
55
  const token = resolveHfToken(config);
56
+ // A bundle combines one selected primary quant and explicit optional
57
+ // companions. It must not pull the generic discovery projector as a side
58
+ // effect, because that makes the download plan ambiguous.
59
+ if (model.components && !options.listQuants && !options.file) {
60
+ const { quants } = await listRepoQuants(model.hfRepo, token);
61
+ const choice = options.quant
62
+ ? quants.find((quant) => quant.quant.toLowerCase() === options.quant.toLowerCase())
63
+ : undefined;
64
+ if (options.quant && !choice) {
65
+ throw new CommandError({
66
+ code: "NO_QUANT",
67
+ message: `${model.hfRepo} has no ${options.quant}`,
68
+ });
69
+ }
70
+ const plan = bundleDownloadPlan(model, options.component ?? [], choice?.files, choice?.sizeBytes);
71
+ const progressLabel = `${model.name}${choice ? ` ${choice.quant}` : ""}`;
72
+ const written = await downloadRepoFilesWithProgress({
73
+ activityTarget: model.name,
74
+ progressLabel,
75
+ totalBytes: plan.totalBytes,
76
+ repo: plan.repo,
77
+ files: plan.files,
78
+ destRoot: managedModelsDir(config),
79
+ token,
80
+ });
81
+ return {
82
+ type: "single",
83
+ data: {
84
+ model: choice ? `${model.name} (${choice.quant})` : model.name,
85
+ repo: model.hfRepo,
86
+ path: written[0] ?? "(already present)",
87
+ size: plan.totalBytes ? formatBytes(plan.totalBytes) : "-",
88
+ },
89
+ schema: pullSchema,
90
+ };
91
+ }
54
92
  // Discover-and-choose paths both need the repo's quant listing.
55
93
  if (options.listQuants || options.quant) {
56
94
  const { quants, mmproj } = await listRepoQuants(model.hfRepo, token);
@@ -61,6 +99,7 @@ export async function runPullCommand(modelArg, options, _command) {
61
99
  quant: q.quant,
62
100
  size: formatBytes(q.sizeBytes),
63
101
  files: q.files.length,
102
+ fileNames: q.files,
64
103
  })),
65
104
  schema: quantSchema,
66
105
  };
@@ -78,22 +117,15 @@ export async function runPullCommand(modelArg, options, _command) {
78
117
  }
79
118
  const files = [...choice.files, ...(mmproj ? mmproj.files : [])];
80
119
  const total = choice.sizeBytes + (mmproj?.sizeBytes ?? 0);
81
- let lastPct = -1;
82
- const written = await withActivity("download", { target: `${model.name} (${choice.quant})` }, (activity) => downloadRepoFiles({
120
+ const written = await downloadRepoFilesWithProgress({
121
+ activityTarget: `${model.name} (${choice.quant})`,
122
+ progressLabel: `${model.name} ${choice.quant}`,
123
+ totalBytes: total,
83
124
  repo: model.hfRepo,
84
125
  files,
85
126
  destRoot: managedModelsDir(config),
86
127
  token,
87
- onProgress: (p) => {
88
- activity.update(total ? p.receivedBytes / total : null);
89
- const pct = total ? Math.floor((p.receivedBytes / total) * 100) : 0;
90
- if (pct !== lastPct && pct % 5 === 0) {
91
- lastPct = pct;
92
- process.stderr.write(` ${model.name} ${choice.quant}: ${pct}%\r`);
93
- }
94
- },
95
- }));
96
- process.stderr.write("\n");
128
+ });
97
129
  return {
98
130
  type: "single",
99
131
  data: {
@@ -119,12 +151,13 @@ export async function runPullCommand(modelArg, options, _command) {
119
151
  return;
120
152
  activity.update(p.receivedBytes / p.totalBytes);
121
153
  const pct = Math.floor((p.receivedBytes / p.totalBytes) * 100);
122
- if (pct !== lastPct && pct % 5 === 0) {
154
+ if (pct > lastPct) {
123
155
  lastPct = pct;
124
156
  process.stderr.write(` ${model.name}: ${pct}%\r`);
125
157
  }
126
158
  },
127
159
  }));
160
+ process.stderr.write(` ${model.name}: 100%\r`);
128
161
  process.stderr.write("\n");
129
162
  return {
130
163
  type: "single",
@@ -0,0 +1,14 @@
1
+ /** Shared CLI progress and activity reporting for Hugging Face repo downloads. */
2
+ import { type DownloadFilesOptions } from "../models/index.js";
3
+ export interface RepoDownloadOptions extends DownloadFilesOptions {
4
+ activityTarget: string;
5
+ progressLabel: string;
6
+ totalBytes: number | null;
7
+ }
8
+ /**
9
+ * Download a repo selection while keeping the daemon job ring and host-status
10
+ * activity record in lockstep. Repository size metadata is advisory, so a
11
+ * successful transfer is the only authoritative completion signal.
12
+ */
13
+ export declare function downloadRepoFilesWithProgress({ activityTarget, progressLabel, totalBytes, ...options }: RepoDownloadOptions): Promise<string[]>;
14
+ //# sourceMappingURL=repo-download.d.ts.map
@@ -0,0 +1,29 @@
1
+ /** Shared CLI progress and activity reporting for Hugging Face repo downloads. */
2
+ import { downloadRepoFiles, } from "../models/index.js";
3
+ import { withActivity } from "../service/activity.js";
4
+ /**
5
+ * Download a repo selection while keeping the daemon job ring and host-status
6
+ * activity record in lockstep. Repository size metadata is advisory, so a
7
+ * successful transfer is the only authoritative completion signal.
8
+ */
9
+ export async function downloadRepoFilesWithProgress({ activityTarget, progressLabel, totalBytes, ...options }) {
10
+ let lastPct = -1;
11
+ const written = await withActivity("download", { target: activityTarget }, (activity) => downloadRepoFiles({
12
+ ...options,
13
+ onProgress: (progress) => {
14
+ activity.update(totalBytes ? progress.receivedBytes / totalBytes : null);
15
+ const pct = totalBytes ? Math.floor((progress.receivedBytes / totalBytes) * 100) : 0;
16
+ // A download chunk can jump straight over a five-percent boundary.
17
+ // Report each new integer percent so the UI ring never stalls until
18
+ // completion simply because no chunk hit an exact multiple of five.
19
+ if (pct > lastPct) {
20
+ lastPct = pct;
21
+ process.stderr.write(` ${progressLabel}: ${pct}%\r`);
22
+ }
23
+ },
24
+ }));
25
+ process.stderr.write(` ${progressLabel}: 100%\r`);
26
+ process.stderr.write("\n");
27
+ return written;
28
+ }
29
+ //# sourceMappingURL=repo-download.js.map
@@ -8,6 +8,7 @@ import type { Command } from "commander";
8
8
  import type { AnyCommandResult } from "../output/index.js";
9
9
  export interface RuntimeRow {
10
10
  label: string;
11
+ displayName: string;
11
12
  version: string;
12
13
  source: string;
13
14
  dir: string;
@@ -15,6 +16,8 @@ export interface RuntimeRow {
15
16
  export declare function addRuntimeListOptions(cmd: Command): Command;
16
17
  export declare function runRuntimeListCommand(_options: unknown, _command: Command): Promise<AnyCommandResult<RuntimeRow>>;
17
18
  export declare function addRuntimeInstallOptions(cmd: Command): Command;
19
+ export declare function addRuntimeRemoveOptions(cmd: Command): Command;
20
+ export declare function runRuntimeRemoveCommand(name: string, _options: unknown, _command: Command): Promise<AnyCommandResult<RuntimeRow>>;
18
21
  export declare function runRuntimeInstallCommand(options: {
19
22
  build?: string;
20
23
  variant?: string;
@@ -1,7 +1,7 @@
1
1
  import { loadBrainConfig } from "../config/index.js";
2
2
  import { resolveBrainPaths } from "../config/paths.js";
3
3
  import { CommandError } from "../output/types.js";
4
- import { defaultRuntimeSpec, installManagedRuntime, listAllRuntimes, listRuntimeDevices, probeNvidiaGpu, supportedVariants, } from "../runtime/index.js";
4
+ import { DEFAULT_LLAMA_BUILD, defaultRuntimeSpec, installManagedRuntime, MissingAssetError, removeManagedRuntime, resolveLatestBuildOrPin, listAllRuntimes, listRuntimeDevices, probeNvidiaGpu, supportedVariants, } from "../runtime/index.js";
5
5
  const runtimeSchema = {
6
6
  idField: "dir",
7
7
  columns: [
@@ -19,6 +19,7 @@ const runtimeSchema = {
19
19
  function toRows() {
20
20
  return listAllRuntimes().map((r) => ({
21
21
  label: r.label,
22
+ displayName: r.displayName ?? `${r.label} · ${r.version}`,
22
23
  version: r.version,
23
24
  source: r.source,
24
25
  dir: r.dir,
@@ -33,9 +34,40 @@ export async function runRuntimeListCommand(_options, _command) {
33
34
  export function addRuntimeInstallOptions(cmd) {
34
35
  return cmd
35
36
  .description("Download a self-contained llama.cpp runtime")
36
- .option("--build <tag>", "llama.cpp release build tag")
37
+ .option("--build <tag>", "llama.cpp release build tag (for example b10355)")
37
38
  .option("--variant <name>", `accelerator to install (${supportedVariants().join("|")}); defaults to the best one this machine can use`);
38
39
  }
40
+ export function addRuntimeRemoveOptions(cmd) {
41
+ return cmd.description("Remove one Otto-managed runtime").argument("<name>");
42
+ }
43
+ export async function runRuntimeRemoveCommand(name, _options, _command) {
44
+ loadBrainConfig();
45
+ removeManagedRuntime(resolveBrainPaths().runtimesDir, name);
46
+ return { type: "list", data: toRows(), schema: runtimeSchema };
47
+ }
48
+ /**
49
+ * Every warning this command emits is one line on purpose. The daemon's
50
+ * BrainOpsManager keeps the *last* stderr line as the job's message, so a
51
+ * multi-line warning would surface in the UI as a dangling fragment. As one line
52
+ * it survives intact on both the CLI and the GUI.
53
+ */
54
+ function warn(message) {
55
+ process.stderr.write(` warning: ${message.replace(/\s+/gu, " ").trim()}\n`);
56
+ }
57
+ /** Download + extract a spec, reporting percentage and phase on stderr. */
58
+ async function install(spec, runtimesDir) {
59
+ process.stderr.write(` installing ${spec.label} (${spec.version})…\n`);
60
+ const runtime = await installManagedRuntime(spec, runtimesDir, (p) => {
61
+ if (p.phase === "downloading" && p.totalBytes) {
62
+ const pct = Math.floor(((p.receivedBytes ?? 0) / p.totalBytes) * 100);
63
+ process.stderr.write(` ${pct}%\r`);
64
+ }
65
+ if (p.phase === "extracting")
66
+ process.stderr.write("\n extracting…\n");
67
+ });
68
+ process.stderr.write("\n");
69
+ return runtime;
70
+ }
39
71
  export async function runRuntimeInstallCommand(options, _command) {
40
72
  loadBrainConfig();
41
73
  const { runtimesDir } = resolveBrainPaths();
@@ -49,38 +81,53 @@ export async function runRuntimeInstallCommand(options, _command) {
49
81
  }
50
82
  // Only "auto" needs the GPU probe, and only to choose between CUDA and Vulkan
51
83
  // on the platforms that have both.
52
- const spec = defaultRuntimeSpec(options.build, {
84
+ const target = {
53
85
  variant: options.variant ?? "auto",
54
86
  hasNvidiaGpu: options.variant ? undefined : await probeNvidiaGpu(),
55
- });
56
- process.stderr.write(` installing ${spec.label} (${spec.version})…\n`);
57
- const runtime = await installManagedRuntime(spec, runtimesDir, (p) => {
58
- if (p.phase === "downloading" && p.totalBytes) {
59
- const pct = Math.floor(((p.receivedBytes ?? 0) / p.totalBytes) * 100);
60
- process.stderr.write(` ${pct}%\r`);
87
+ };
88
+ // "latest" is a request to update, not a promise upstream can always keep: the
89
+ // lookup is unauthenticated and rate limited, so it falls back to the pin.
90
+ const resolvedLatest = options.build === "latest" ? await resolveLatestBuildOrPin() : null;
91
+ if (resolvedLatest?.warning)
92
+ warn(resolvedLatest.warning);
93
+ let spec = defaultRuntimeSpec(resolvedLatest ? resolvedLatest.build : options.build, target);
94
+ let runtime;
95
+ try {
96
+ runtime = await install(spec, runtimesDir);
97
+ }
98
+ catch (error) {
99
+ // Asset names are not derivable from the build tag, and the scheme has
100
+ // already changed once (b4600 shipped `win-cuda-cu12.4-x64`, b10265 ships
101
+ // `win-cuda-12.4-x64`), so a tag resolved at run time can name assets that
102
+ // do not exist under it. The pin's names are the ones managed.test.ts holds,
103
+ // which makes it the only build this can fall back to. An explicit --build
104
+ // still fails loudly: the user named that tag on purpose.
105
+ if (!(error instanceof MissingAssetError) ||
106
+ !resolvedLatest ||
107
+ spec.version === DEFAULT_LLAMA_BUILD) {
108
+ throw error;
61
109
  }
62
- if (p.phase === "extracting")
63
- process.stderr.write("\n extracting…\n");
64
- });
65
- process.stderr.write("\n");
110
+ warn(`llama.cpp build ${spec.version} does not publish the asset this platform needs,` +
111
+ ` so Otto installed the pinned build ${DEFAULT_LLAMA_BUILD} instead.`);
112
+ spec = defaultRuntimeSpec(DEFAULT_LLAMA_BUILD, target);
113
+ runtime = await install(spec, runtimesDir);
114
+ }
66
115
  // A GPU variant that finds no device still runs - on the CPU, at roughly a
67
116
  // fortieth of the prefill throughput, and says nothing about it. Measured on
68
117
  // WSL2, which carries no NVIDIA Vulkan ICD. Warn rather than fail: the runtime
69
118
  // is genuinely installed and usable, just not accelerated.
70
- // One line on purpose. The daemon's BrainOpsManager keeps the *last* stderr line
71
- // as the job's message, so a multi-line warning would surface in the UI as a
72
- // dangling fragment. As one line it survives intact on both the CLI and the GUI.
73
119
  if (spec.variant !== "cpu") {
74
120
  const devices = await listRuntimeDevices(runtime);
75
121
  if (devices.length === 0) {
76
- process.stderr.write(` warning: this ${spec.variant} runtime reports no GPU device, so inference will run` +
77
- ` on the CPU until a working ${spec.variant} driver for this GPU is installed.\n`);
122
+ warn(`this ${spec.variant} runtime reports no GPU device, so inference will run` +
123
+ ` on the CPU until a working ${spec.variant} driver for this GPU is installed.`);
78
124
  }
79
125
  }
80
126
  return {
81
127
  type: "single",
82
128
  data: {
83
129
  label: runtime.label,
130
+ displayName: runtime.displayName ?? `${runtime.label} · ${runtime.version}`,
84
131
  version: runtime.version,
85
132
  source: runtime.source,
86
133
  dir: runtime.dir,
@@ -12,6 +12,7 @@ export interface SearchRow {
12
12
  likes: number;
13
13
  gated: string;
14
14
  installed: boolean;
15
+ summary: string | null;
15
16
  }
16
17
  export declare function addSearchOptions(cmd: Command): Command;
17
18
  export declare function runSearchCommand(query: string, options: {
@@ -27,5 +28,7 @@ export declare function addAddOptions(cmd: Command): Command;
27
28
  export declare function runAddCommand(repo: string, options: {
28
29
  quant?: string;
29
30
  listQuants?: boolean;
31
+ component?: string[];
32
+ primaryOnly?: boolean;
30
33
  }, _command: Command): Promise<AnyCommandResult<AddRow>>;
31
34
  //# sourceMappingURL=search.d.ts.map
@@ -1,20 +1,24 @@
1
1
  import { loadBrainConfig } from "../config/index.js";
2
- import { downloadRepoFiles, listRepoQuants, managedModelsDir, repoOfModel, resolveHfToken, scanModels, searchModels, } from "../models/index.js";
2
+ import { listRepoQuants, managedModelsDir, repoOfModel, resolveHfToken, scanModels, searchModels, } from "../models/index.js";
3
3
  import { formatBytes } from "../models/scan.js";
4
4
  import { CommandError } from "../output/types.js";
5
+ import { downloadRepoFilesWithProgress } from "./repo-download.js";
5
6
  /** What is already on disk, so search + quant listings can mark installed rows. */
6
7
  function installedIndex(config) {
7
8
  const repos = new Set();
8
- const quants = new Set();
9
+ const quants = new Map();
10
+ const projectorRepos = new Set();
9
11
  for (const model of scanModels(config)) {
10
12
  const repo = repoOfModel(model);
11
13
  if (!repo)
12
14
  continue;
13
15
  repos.add(repo.toLowerCase());
14
16
  if (model.quant)
15
- quants.add(`${repo.toLowerCase()} ${model.quant.toUpperCase()}`);
17
+ quants.set(`${repo.toLowerCase()} ${model.quant.toUpperCase()}`, model.id);
18
+ if (model.mmprojPath)
19
+ projectorRepos.add(repo.toLowerCase());
16
20
  }
17
- return { repos, quants };
21
+ return { repos, quants, projectorRepos };
18
22
  }
19
23
  const searchSchema = {
20
24
  idField: "repo",
@@ -46,6 +50,7 @@ export async function runSearchCommand(query, options, _command) {
46
50
  likes: r.likes,
47
51
  gated: r.gated ? "yes" : "",
48
52
  installed: repos.has(r.repo.toLowerCase()),
53
+ summary: r.summary,
49
54
  })),
50
55
  schema: searchSchema,
51
56
  };
@@ -73,6 +78,8 @@ export function addAddOptions(cmd) {
73
78
  .description("Download a model from an arbitrary Hugging Face repo")
74
79
  .argument("<repo>", "owner/repo on Hugging Face")
75
80
  .option("--quant <label>", "quantization to download (e.g. Q5_K_M)")
81
+ .option("--component <id...>", "download optional discovered component ids")
82
+ .option("--primary-only", "download only the selected primary quant")
76
83
  .option("--list-quants", "list the quantizations the repo offers and exit");
77
84
  }
78
85
  export async function runAddCommand(repo, options, _command) {
@@ -80,7 +87,7 @@ export async function runAddCommand(repo, options, _command) {
80
87
  const token = resolveHfToken(config);
81
88
  const { quants, mmproj } = await listRepoQuants(repo, token);
82
89
  if (options.listQuants || !options.quant) {
83
- const { quants: installedQuants } = installedIndex(config);
90
+ const { quants: installedQuants, projectorRepos } = installedIndex(config);
84
91
  const listing = {
85
92
  type: "list",
86
93
  data: quants.map((q) => ({
@@ -88,7 +95,17 @@ export async function runAddCommand(repo, options, _command) {
88
95
  size: formatBytes(q.sizeBytes),
89
96
  sizeBytes: q.sizeBytes,
90
97
  files: q.files.length,
98
+ modelId: installedQuants.get(`${repo.toLowerCase()} ${q.quant.toUpperCase()}`) ?? null,
91
99
  installed: installedQuants.has(`${repo.toLowerCase()} ${q.quant.toUpperCase()}`),
100
+ ...(mmproj
101
+ ? {
102
+ projector: {
103
+ file: mmproj.files[0],
104
+ sizeBytes: mmproj.sizeBytes,
105
+ installed: projectorRepos.has(repo.toLowerCase()),
106
+ },
107
+ }
108
+ : {}),
92
109
  })),
93
110
  schema: addQuantSchema,
94
111
  };
@@ -104,23 +121,27 @@ export async function runAddCommand(repo, options, _command) {
104
121
  message: `${repo} has no ${options.quant} - available: ${quants.map((q) => q.quant).join(", ") || "none"}`,
105
122
  });
106
123
  }
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({
124
+ const requested = new Set(options.component ?? []);
125
+ const unknown = [...requested].filter((id) => id !== "vision-projector");
126
+ if (unknown.length)
127
+ throw new CommandError({
128
+ code: "NO_COMPONENT",
129
+ message: `unknown bundle components: ${unknown.join(", ")}`,
130
+ });
131
+ const includeProjector = Boolean(mmproj) &&
132
+ (requested.has("vision-projector") ||
133
+ (!options.primaryOnly && options.component === undefined));
134
+ const files = [...choice.files, ...(includeProjector ? mmproj.files : [])];
135
+ const total = choice.sizeBytes + (includeProjector ? mmproj.sizeBytes : 0);
136
+ const written = await downloadRepoFilesWithProgress({
137
+ activityTarget: `${repo} (${choice.quant})`,
138
+ progressLabel: `${repo} ${choice.quant}`,
139
+ totalBytes: total,
111
140
  repo,
112
141
  files,
113
142
  destRoot: managedModelsDir(config),
114
143
  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
144
  });
123
- process.stderr.write("\n");
124
145
  return {
125
146
  type: "single",
126
147
  data: {
@@ -0,0 +1,8 @@
1
+ import type { ProfilesStore } from "./schema.js";
2
+ /**
3
+ * Product-owned ids are upgraded wholesale, while records with unknown ids remain
4
+ * user-owned. Do not merge profile fields: stale template bytes or prose must not
5
+ * survive an Otto upgrade.
6
+ */
7
+ export declare function seedBuiltinHostingProfiles(store: ProfilesStore): boolean;
8
+ //# sourceMappingURL=builtin-hosting-profiles.d.ts.map
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Curated Brain-owned hosting profiles. Source: Apache-2.0
3
+ * https://huggingface.co/peculiar-ragdoll/Qwen-Sharp-Chat-Templates
4
+ */
5
+ import { Buffer } from "node:buffer";
6
+ const QWEN_SHARP_TEMPLATE_BASE64 = "eyUtIHNldCB0ZW1wbGF0ZV92ZXJzaW9uID0gInF3ZW4zLjYtZnJvZ2dlcmljLXYyMS4zIiAlfQp7JS0gc2V0IF90b29sX2Zvcm1hdCA9IHRvb2xfY2FsbF9mb3JtYXQgaWYgdG9vbF9jYWxsX2Zvcm1hdCBpcyBkZWZpbmVkIGVsc2UgJ3htbCcgJX0KeyUtIHNldCBpbWFnZV9jb3VudCA9IG5hbWVzcGFjZSh2YWx1ZT0wKSAlfQp7JS0gc2V0IHZpZGVvX2NvdW50ID0gbmFtZXNwYWNlKHZhbHVlPTApICV9CnslLSBzZXQgYWRkX3Zpc2lvbl9pZCA9IGFkZF92aXNpb25faWQgaWYgYWRkX3Zpc2lvbl9pZCBpcyBkZWZpbmVkIGVsc2UgZmFsc2UgJX0KeyUtIHNldCBlbmFibGVfdGhpbmtpbmcgPSBlbmFibGVfdGhpbmtpbmcgaWYgZW5hYmxlX3RoaW5raW5nIGlzIGRlZmluZWQgZWxzZSB0cnVlICV9CnslLSBzZXQgYXV0b19kaXNhYmxlX3RoaW5raW5nX3dpdGhfdG9vbHMgPSBhdXRvX2Rpc2FibGVfdGhpbmtpbmdfd2l0aF90b29scyBpZiBhdXRvX2Rpc2FibGVfdGhpbmtpbmdfd2l0aF90b29scyBpcyBkZWZpbmVkIGVsc2UgZmFsc2UgJX0KeyUtIHNldCBfcHJlc2VydmVfdGhpbmtpbmcgPSBwcmVzZXJ2ZV90aGlua2luZyBpZiBwcmVzZXJ2ZV90aGlua2luZyBpcyBkZWZpbmVkIGVsc2UgdHJ1ZSAlfQp7JS0gc2V0IG1heF90b29sX2FyZ19jaGFycyA9IG1heF90b29sX2FyZ19jaGFycyBpZiBtYXhfdG9vbF9hcmdfY2hhcnMgaXMgZGVmaW5lZCBlbHNlIDAgJX0KeyUtIHNldCBtYXhfdG9vbF9yZXNwb25zZV9jaGFycyA9IG1heF90b29sX3Jlc3BvbnNlX2NoYXJzIGlmIG1heF90b29sX3Jlc3BvbnNlX2NoYXJzIGlzIGRlZmluZWQgZWxzZSAwICV9CnslLSBzZXQgX2hhc190b29scyA9ICh0b29scyBpcyBkZWZpbmVkIGFuZCB0b29scyBhbmQgdG9vbHMgaXMgaXRlcmFibGUgYW5kIHRvb2xzIGlzIG5vdCBtYXBwaW5nKSAlfQp7JS0gc2V0IG5zX3N0YXRlID0gbmFtZXNwYWNlKHRoaW5raW5nPWVuYWJsZV90aGlua2luZykgJX0KeyUtIGlmIGF1dG9fZGlzYWJsZV90aGlua2luZ193aXRoX3Rvb2xzIGFuZCBfaGFzX3Rvb2xzICV9CiAgICB7JS0gc2V0IG5zX3N0YXRlLnRoaW5raW5nID0gZmFsc2UgJX0KeyUtIGVuZGlmICV9CnslLSBtYWNybyByZW5kZXJfY29udGVudChjb250ZW50LCBkb192aXNpb25fY291bnQsIGlzX3N5c3RlbV9jb250ZW50PWZhbHNlKSAlfQogICAgeyUtIGlmIGNvbnRlbnQgaXMgc3RyaW5nICV9CiAgICAgICAge3stIGNvbnRlbnQgfX0KICAgIHslLSBlbGlmIGNvbnRlbnQgaXMgaXRlcmFibGUgYW5kIGNvbnRlbnQgaXMgbm90IG1hcHBpbmcgJX0KICAgICAgICB7JS0gZm9yIGl0ZW0gaW4gY29udGVudCAlfQogICAgICAgICAgICB7JS0gaWYgaXRlbSBpcyBtYXBwaW5nICV9CiAgICAgICAgICAgICAgICB7JS0gaWYgaXRlbS50eXBlID09ICdpbWFnZScgb3IgJ2ltYWdlJyBpbiBpdGVtIG9yICdpbWFnZV91cmwnIGluIGl0ZW0gJX0KICAgICAgICAgICAgICAgICAgICB7JS0gaWYgaXNfc3lzdGVtX2NvbnRlbnQgJX0KICAgICAgICAgICAgICAgICAgICAgICAge3stIHJhaXNlX2V4Y2VwdGlvbignU3lzdGVtIG1lc3NhZ2UgY2Fubm90IGNvbnRhaW4gaW1hZ2VzLicpIH19CiAgICAgICAgICAgICAgICAgICAgeyUtIGVuZGlmICV9CiAgICAgICAgICAgICAgICAgICAgeyUtIGlmIGRvX3Zpc2lvbl9jb3VudCAlfQogICAgICAgICAgICAgICAgICAgICAgICB7JS0gc2V0IGltYWdlX2NvdW50LnZhbHVlID0gaW1hZ2VfY291bnQudmFsdWUgKyAxICV9CiAgICAgICAgICAgICAgICAgICAgeyUtIGVuZGlmICV9CiAgICAgICAgICAgICAgICAgICAgeyUtIGlmIGFkZF92aXNpb25faWQgJX0KICAgICAgICAgICAgICAgICAgICAgICAge3stICdQaWN0dXJlICcgfiBpbWFnZV9jb3VudC52YWx1ZSB+ICc6ICcgfX0KICAgICAgICAgICAgICAgICAgICB7JS0gZW5kaWYgJX0KICAgICAgICAgICAgICAgICAgICB7ey0gJzx8dmlzaW9uX3N0YXJ0fD48fGltYWdlX3BhZHw+PHx2aXNpb25fZW5kfD4nIH19CiAgICAgICAgICAgICAgICB7JS0gZWxpZiBpdGVtLnR5cGUgPT0gJ3ZpZGVvJyBvciAndmlkZW8nIGluIGl0ZW0gJX0KICAgICAgICAgICAgICAgICAgICB7JS0gaWYgaXNfc3lzdGVtX2NvbnRlbnQgJX0KICAgICAgICAgICAgICAgICAgICAgICAge3stIHJhaXNlX2V4Y2VwdGlvbignU3lzdGVtIG1lc3NhZ2UgY2Fubm90IGNvbnRhaW4gdmlkZW9zLicpIH19CiAgICAgICAgICAgICAgICAgICAgeyUtIGVuZGlmICV9CiAgICAgICAgICAgICAgICAgICAgeyUtIGlmIGRvX3Zpc2lvbl9jb3VudCAlfQogICAgICAgICAgICAgICAgICAgICAgICB7JS0gc2V0IHZpZGVvX2NvdW50LnZhbHVlID0gdmlkZW9fY291bnQudmFsdWUgKyAxICV9CiAgICAgICAgICAgICAgICAgICAgeyUtIGVuZGlmICV9CiAgICAgICAgICAgICAgICAgICAgeyUtIGlmIGFkZF92aXNpb25faWQgJX0KICAgICAgICAgICAgICAgICAgICAgICAge3stICdWaWRlbyAnIH4gdmlkZW9fY291bnQudmFsdWUgfiAnOiAnIH19CiAgICAgICAgICAgICAgICAgICAgeyUtIGVuZGlmICV9CiAgICAgICAgICAgICAgICAgICAge3stICc8fHZpc2lvbl9zdGFydHw+PHx2aWRlb19wYWR8Pjx8dmlzaW9uX2VuZHw+JyB9fQogICAgICAgICAgICAgICAgeyUtIGVsaWYgJ3RleHQnIGluIGl0ZW0gJX0KICAgICAgICAgICAgICAgICAgICB7ey0gaXRlbS50ZXh0IH19CiAgICAgICAgICAgICAgICB7JS0gZWxzZSAlfQogICAgICAgICAgICAgICAgICAgIHt7LSByYWlzZV9leGNlcHRpb24oJ1VuZXhwZWN0ZWQgaXRlbSB0eXBlIGluIGNvbnRlbnQuJykgfX0KICAgICAgICAgICAgICAgIHslLSBlbmRpZiAlfQogICAgICAgICAgICB7JS0gZWxzZSAlfQogICAgICAgICAgICAgICAge3stIGl0ZW0gfCBzdHJpbmcgfX0KICAgICAgICAgICAgeyUtIGVuZGlmICV9CiAgICAgICAgeyUtIGVuZGZvciAlfQogICAgeyUtIGVsaWYgY29udGVudCBpcyBub25lIG9yIGNvbnRlbnQgaXMgdW5kZWZpbmVkICV9CiAgICAgICAge3stICcnIH19CiAgICB7JS0gZWxzZSAlfQogICAgICAgIHt7LSByYWlzZV9leGNlcHRpb24oJ1VuZXhwZWN0ZWQgY29udGVudCB0eXBlLicpIH19CiAgICB7JS0gZW5kaWYgJX0KeyUtIGVuZG1hY3JvICV9CnslLSBpZiBub3QgbWVzc2FnZXMgJX0KICAgIHt7LSByYWlzZV9leGNlcHRpb24oJ05vIG1lc3NhZ2VzIHByb3ZpZGVkLicpIH19CnslLSBlbmRpZiAlfQp7JS0gc2V0IF9maXJzdF9yb2xlID0gbWVzc2FnZXNbMF0ucm9sZSAlfQp7JS0gaWYgX2ZpcnN0X3JvbGUgPT0gJ3N5c3RlbScgb3IgX2ZpcnN0X3JvbGUgPT0gJ2RldmVsb3BlcicgJX0KICAgIHslLSBzZXQgX3N5c19tc2cgPSBtZXNzYWdlc1swXSAlfQogICAgeyUtIHNldCBfbXNncyA9IG1lc3NhZ2VzWzE6XSAlfQp7JS0gZWxzZSAlfQogICAgeyUtIHNldCBfc3lzX21zZyA9IG5vbmUgJX0KICAgIHslLSBzZXQgX21zZ3MgPSBtZXNzYWdlcyAlfQp7JS0gZW5kaWYgJX0KeyUtIHNldCBfc2MgPSAnJyAlfQp7JS0gaWYgX3N5c19tc2cgaXMgbm90IG5vbmUgJX0KICAgIHslLSBzZXQgX3NjID0gcmVuZGVyX2NvbnRlbnQoX3N5c19tc2cuY29udGVudCwgZmFsc2UsIHRydWUpIHwgdHJpbSAlfQogICAgeyUtIGlmICc8fHRoaW5rX29mZnw+JyBpbiBfc2MgJX0KICAgICAgICB7JS0gc2V0IG5zX3N0YXRlLnRoaW5raW5nID0gZmFsc2UgJX0KICAgICAgICB7JS0gc2V0IF9zYyA9IF9zYy5zcGxpdCgnPHx0aGlua19vZmZ8PicpIHwgam9pbignJykgfCB0cmltICV9CiAgICB7JS0gZWxpZiAnPHx0aGlua19vbnw+JyBpbiBfc2MgJX0KICAgICAgICB7JS0gc2V0IG5zX3N0YXRlLnRoaW5raW5nID0gdHJ1ZSAlfQogICAgICAgIHslLSBzZXQgX3NjID0gX3NjLnNwbGl0KCc8fHRoaW5rX29ufD4nKSB8IGpvaW4oJycpIHwgdHJpbSAlfQogICAgeyUtIGVuZGlmICV9CnslLSBlbmRpZiAlfQp7JS0gc2V0IF90ZXJzZSAlfQpBbnN3ZXIgZGlyZWN0bHksIGFmdGVyIHRoaW5raW5nLiBMZWFkIHdpdGggdGhlIGFuc3dlciwgdGhlbiBvbmx5IHdoYXQgaXQgbmVlZHMgdG8gYmUgY29ycmVjdCBhbmQgdXNhYmxlLgpOZXZlcjogb3BlbiB3aXRoIHByZWFtYmxlIG9yIHBsZWFzYW50cmllczsgcmVzdGF0ZSB0aGUgcXVlc3Rpb247IGFkZCBmaWxsZXIgdHJhbnNpdGlvbnM7IGhlZGdlIHdpdGggbmljZXRpZXM7IG9yIHJlcGVhdCBhIHBvaW50IHlvdSd2ZSBhbHJlYWR5IG1hZGUuCkFsd2F5czoga2VlcCBlc3NlbnRpYWwgc3RlcHMsIGNhdmVhdHMsIHVuY2VydGFpbnRpZXMsIGFuZCBzcGVjaWZpY3Mg4oCUIG5ldmVyIGRyb3AgY29ycmVjdG5lc3Mgb3IgYSBuZWVkZWQgd2FybmluZyBmb3IgYnJldml0eS4gS2VlcCB0aGUgZmluYWwgYW5zd2VyIGxlYW4uIFVzZSB0aGUgbGVhc3Qgc3RydWN0dXJlIHRoYXQgY29udmV5cyBpdCAocGxhaW4gcHJvc2Ugd2hlbiBzaG9ydDsgbGlzdHMgb3IgY29kZSBvbmx5IHdoZW4gdGhleSBlYXJuIHRoZWlyIHBsYWNlKS4gSWYgZ2VudWluZWx5IHVuY2VydGFpbiwgc2F5IHNvIGFuZCBleHBsYWluIHdoeSDigJQgbmV2ZXIgb21pdCB1bmNlcnRhaW50eSBmb3IgdGhlIHNha2Ugb2YgYnJldml0eS4KSWYgYSB1c2VyIHJlcXVlc3QgaXMgZ2VudWluZWx5IGFtYmlndW91cywgYXNrIGEgc2hhcnAgcXVlc3Rpb24sIGRvbid0IGd1ZXNzLgp7JS0gZW5kc2V0ICV9CnslLSBpZiBub3QgX3NjICV9CiAgICB7JS0gc2V0IF9zYyA9IF90ZXJzZSB8IHRyaW0gJX0KeyUtIGVsc2UgJX0KICAgIHslLSBzZXQgX3NjID0gKF9zYyB8IHRyaW0pIH4gJ1xuXG4nIH4gKF90ZXJzZSB8IHRyaW0pICV9CnslLSBlbmRpZiAlfQp7JS0gaWYgX2hhc190b29scyAlfQogICAge3stICc8fGltX3N0YXJ0fD5zeXN0ZW1cbicgfX0KICAgIHt7LSAnIyBUb29sc1xuXG5Zb3UgaGF2ZSBhY2Nlc3MgdG8gdGhlIGZvbGxvd2luZyBmdW5jdGlvbnM6XG5cbjx0b29scz4nIH19CiAgICB7JS0gZm9yIHRvb2wgaW4gdG9vbHMgJX0KICAgICAgICB7ey0gJ1xuJyB9fQogICAgICAgIHt7LSB0b29sIHwgdG9qc29uIH19CiAgICB7JS0gZW5kZm9yICV9CiAgICB7ey0gJ1xuPC90b29scz4nIH19CiAgICB7JS0gc2V0IHRvb2xfaW5zdHJ1Y3Rpb25zICV9CklmIHlvdSBjaG9vc2UgdG8gY2FsbCBhIGZ1bmN0aW9uIE9OTFkgcmVwbHkgaW4gdGhlIGZvbGxvd2luZyBmb3JtYXQgd2l0aCBOTyBzdWZmaXg6Cgp7JS0gaWYgX3Rvb2xfZm9ybWF0ID09ICdqc29uJyAlfQo8dGhpbms+CkJyaWVmIGV4cGxhbmF0aW9uIG9mIHRvb2wgY2FsbAo8L3RoaW5rPgo8dG9vbF9jYWxsPgp7Im5hbWUiOiAiZXhhbXBsZV9mdW5jdGlvbl9uYW1lIiwgImFyZ3VtZW50cyI6IHsiZXhhbXBsZV9wYXJhbWV0ZXJfMSI6ICJ2YWx1ZV8xIiwgImV4YW1wbGVfcGFyYW1ldGVyXzIiOiAiVGhpcyBpcyB0aGUgdmFsdWUgZm9yIHRoZSBzZWNvbmQgcGFyYW1ldGVyIn19CjwvdG9vbF9jYWxsPgp7JS0gZWxzZSAlfQo8dGhpbms+CkJyaWVmIGV4cGxhbmF0aW9uIG9mIHRvb2wgY2FsbAo8L3RoaW5rPgo8dG9vbF9jYWxsPgo8ZnVuY3Rpb249ZXhhbXBsZV9mdW5jdGlvbl9uYW1lPgo8cGFyYW1ldGVyPWV4YW1wbGVfcGFyYW1ldGVyXzE+CnZhbHVlXzEKPC9wYXJhbWV0ZXI+CjxwYXJhbWV0ZXI9ZXhhbXBsZV9wYXJhbWV0ZXJfMj4KVGhpcyBpcyB0aGUgdmFsdWUgZm9yIHRoZSBzZWNvbmQgcGFyYW1ldGVyCnRoYXQgY2FuIHNwYW4KbXVsdGlwbGUgbGluZXMKPC9wYXJhbWV0ZXI+CjwvZnVuY3Rpb24+CjwvdG9vbF9jYWxsPgp7JS0gZW5kaWYgJX0KCjxJTVBPUlRBTlQ+ClJlbWluZGVyOgotIFlvdSBjYW4gdXNlIHRoZSA8dGhpbms+PC90aGluaz4gYmxvY2sgdG8gcGxhbiB5b3VyIG5leHQgdG9vbCBjYWxsIE9SIHRvIHN5bnRoZXNpemUgZGF0YSBhbmQgZm9ybXVsYXRlIHlvdXIgZmluYWwgcmVzcG9uc2UgdG8gdGhlIHVzZXIuCi0gQUxMIGV4cGxhbmF0aW9uIGFuZCByZWFzb25pbmcgTVVTVCBiZSBwbGFjZWQgc3RyaWN0bHkgaW5zaWRlIHRoZSA8dGhpbms+PC90aGluaz4gYmxvY2suCnslLSBpZiBfdG9vbF9mb3JtYXQgPT0gJ2pzb24nICV9Ci0gRnVuY3Rpb24gY2FsbHMgTVVTVCBmb2xsb3cgdGhlIHNwZWNpZmllZCBmb3JtYXQ6IGEgc2luZ2xlIEpTT04gb2JqZWN0IHdpdGggIm5hbWUiIGFuZCAiYXJndW1lbnRzIiBrZXlzIGluc2lkZSA8dG9vbF9jYWxsPjwvdG9vbF9jYWxsPiBYTUwgdGFncy4KeyUtIGVsc2UgJX0KLSBGdW5jdGlvbiBjYWxscyBNVVNUIGZvbGxvdyB0aGUgc3BlY2lmaWVkIGZvcm1hdDogYW4gaW5uZXIgPGZ1bmN0aW9uPS4uLj48L2Z1bmN0aW9uPiBibG9jayBtdXN0IGJlIG5lc3RlZCB3aXRoaW4gPHRvb2xfY2FsbD48L3Rvb2xfY2FsbD4gWE1MIHRhZ3MuCnslLSBlbmRpZiAlfQotIElmIHlvdSBjaG9vc2UgdG8gY2FsbCBhIHRvb2wsIHlvdSBNVVNUIG91dHB1dCB0aGUgPHRvb2xfY2FsbD4gYmxvY2sgSU1NRURJQVRFTFkgYWZ0ZXIgdGhpbmtpbmcsIHdpdGggTk8gY29udmVyc2F0aW9uYWwgdGV4dCBiZWZvcmUgaXQuCnslLSBpZiBfdG9vbF9mb3JtYXQgPT0gJ2pzb24nICV9Ci0gVGhlIDx0b29sX2NhbGw+IHRhZyBNVVNUIGJlIGF0IHRoZSB2ZXJ5IGJlZ2lubmluZyBvZiBhIG5ldyBsaW5lLCB3aXRoIE5PIHNwYWNlcyBvciBpbmRlbnRhdGlvbiBiZWZvcmUgaXQuCnslLSBlbHNlICV9Ci0gVGhlIDx0b29sX2NhbGw+IGFuZCA8ZnVuY3Rpb24+IHRhZ3MgTVVTVCBiZSBhdCB0aGUgdmVyeSBiZWdpbm5pbmcgb2YgYSBuZXcgbGluZSwgd2l0aCBOTyBzcGFjZXMgb3IgaW5kZW50YXRpb24gYmVmb3JlIHRoZW0uCnslLSBlbmRpZiAlfQotIFRvIGNhbGwgbXVsdGlwbGUgZnVuY3Rpb25zLCBvdXRwdXQgYSBzZXBhcmF0ZSwgY29tcGxldGVseSBjbG9zZWQgPHRvb2xfY2FsbD48L3Rvb2xfY2FsbD4gYmxvY2sgZm9yIEVBQ0ggZnVuY3Rpb24uIERvIE5PVCBuZXN0IDx0b29sX2NhbGw+IGJsb2Nrcy4KLSBJZiB5b3UgaGF2ZSBhbGwgbmVjZXNzYXJ5IGRhdGEsIHByb3ZpZGUgeW91ciBmaW5hbCBhbnN3ZXIgZGlyZWN0bHkgdG8gdGhlIHVzZXIgd2l0aG91dCBhbnkgdG9vbCBjYWxsLgo8L0lNUE9SVEFOVD4KICAgIHslLSBlbmRzZXQgJX0KICAgIHt7LSAnXG5cbicgfiB0b29sX2luc3RydWN0aW9ucyB8IHRyaW0gfX0KICAgIHslLSBpZiBfc2MgJX0KICAgICAgICB7ey0gJ1xuXG4nICsgX3NjIH19CiAgICB7JS0gZW5kaWYgJX0KICAgIHt7LSAnPHxpbV9lbmR8PlxuJyB9fQp7JS0gZWxzZSAlfQogICAgeyUtIGlmIF9zYyAlfQogICAgICAgIHt7LSAnPHxpbV9zdGFydHw+c3lzdGVtXG4nICsgX3NjICsgJzx8aW1fZW5kfD5cbicgfX0KICAgIHslLSBlbmRpZiAlfQp7JS0gZW5kaWYgJX0KeyUtIHNldCBfbGFzdF9pZHggPSBfbXNncyB8IGxlbmd0aCAtIDEgJX0KeyUtIHNldCBucyA9IG5hbWVzcGFjZShtdWx0aV9zdGVwX3Rvb2w9dHJ1ZSwgbGFzdF9xdWVyeV9pbmRleD1fbGFzdF9pZHgpICV9CnslLSBmb3IgbWVzc2FnZSBpbiBfbXNnc1s6Oi0xXSAlfQogICAgeyUtIHNldCBpbmRleCA9IChfbXNncyB8IGxlbmd0aCAtIDEpIC0gbG9vcC5pbmRleDAgJX0KICAgIHslLSBpZiBucy5tdWx0aV9zdGVwX3Rvb2wgYW5kIG1lc3NhZ2Uucm9sZSA9PSAndXNlcicgJX0KICAgICAgICB7JS0gc2V0IF9yYyA9IHJlbmRlcl9jb250ZW50KG1lc3NhZ2UuY29udGVudCwgZmFsc2UpIHwgdHJpbSAlfQogICAgICAgIHslLSBpZiBub3QgKF9yYy5zdGFydHN3aXRoKCc8dG9vbF9yZXNwb25zZT4nKSBhbmQgX3JjLmVuZHN3aXRoKCc8L3Rvb2xfcmVzcG9uc2U+JykpICV9CiAgICAgICAgICAgIHslLSBzZXQgbnMubXVsdGlfc3RlcF90b29sID0gZmFsc2UgJX0KICAgICAgICAgICAgeyUtIHNldCBucy5sYXN0X3F1ZXJ5X2luZGV4ID0gaW5kZXggJX0KICAgICAgICB7JS0gZW5kaWYgJX0KICAgIHslLSBlbmRpZiAlfQp7JS0gZW5kZm9yICV9CnslLSBpZiBucy5tdWx0aV9zdGVwX3Rvb2wgJX0KICAgIHslLSBpZiBfbGFzdF9pZHggPiA1MCAlfQogICAgICAgIHslLSBzZXQgbnMubGFzdF9xdWVyeV9pbmRleCA9IF9sYXN0X2lkeCAlfQogICAgeyUtIGVsc2UgJX0KICAgICAgICB7JS0gc2V0IG5zLmxhc3RfcXVlcnlfaW5kZXggPSAwICV9CiAgICB7JS0gZW5kaWYgJX0KeyUtIGVuZGlmICV9CnslLSBzZXQgbnMyID0gbmFtZXNwYWNlKHByZXZfcm9sZT0nJywgY29uc2VjdXRpdmVfZmFpbHVyZXM9MCkgJX0KeyUtIGZvciBtZXNzYWdlIGluIF9tc2dzICV9CiAgICB7JS0gc2V0IGlzX3N5c3RlbSA9IChtZXNzYWdlLnJvbGUgPT0gInN5c3RlbSIgb3IgbWVzc2FnZS5yb2xlID09ICJkZXZlbG9wZXIiKSAlfQogICAgeyUtIHNldCBjb250ZW50ID0gcmVuZGVyX2NvbnRlbnQobWVzc2FnZS5jb250ZW50LCB0cnVlLCBpc19zeXN0ZW0pIHwgdHJpbSAlfQogICAgeyUtIGlmIGlzX3N5c3RlbSBvciBtZXNzYWdlLnJvbGUgPT0gJ3VzZXInICV9CiAgICAgICAgeyUtIGlmICc8fHRoaW5rX29mZnw+JyBpbiBjb250ZW50ICV9CiAgICAgICAgICAgIHslLSBzZXQgbnNfc3RhdGUudGhpbmtpbmcgPSBmYWxzZSAlfQogICAgICAgICAgICB7JS0gc2V0IGNvbnRlbnQgPSBjb250ZW50LnNwbGl0KCc8fHRoaW5rX29mZnw+JykgfCBqb2luKCcnKSB8IHRyaW0gJX0KICAgICAgICB7JS0gZWxpZiAnPHx0aGlua19vbnw+JyBpbiBjb250ZW50ICV9CiAgICAgICAgICAgIHslLSBzZXQgbnNfc3RhdGUudGhpbmtpbmcgPSB0cnVlICV9CiAgICAgICAgICAgIHslLSBzZXQgY29udGVudCA9IGNvbnRlbnQuc3BsaXQoJzx8dGhpbmtfb258PicpIHwgam9pbignJykgfCB0cmltICV9CiAgICAgICAgeyUtIGVuZGlmICV9CiAgICB7JS0gZW5kaWYgJX0KICAgIHslLSBpZiBpc19zeXN0ZW0gJX0KICAgICAgICB7ey0gJzx8aW1fc3RhcnR8PnN5c3RlbVxuJyArIGNvbnRlbnQgKyAnPHxpbV9lbmR8PlxuJyB9fQogICAgeyUtIGVsaWYgbWVzc2FnZS5yb2xlID09ICd1c2VyJyAlfQogICAgICAgIHslLSBzZXQgbnMyLmNvbnNlY3V0aXZlX2ZhaWx1cmVzID0gMCAlfQogICAgICAgIHt7LSAnPHxpbV9zdGFydHw+dXNlclxuJyArIGNvbnRlbnQgKyAnPHxpbV9lbmR8PlxuJyB9fQogICAgeyUtIGVsaWYgbWVzc2FnZS5yb2xlID09ICdhc3Npc3RhbnQnICV9CiAgICAgICAgeyUtIHNldCByZWFzb25pbmdfY29udGVudCA9ICcnICV9CiAgICAgICAgeyUtIGlmIG1lc3NhZ2UucmVhc29uaW5nX2NvbnRlbnQgaXMgZGVmaW5lZCBhbmQgbWVzc2FnZS5yZWFzb25pbmdfY29udGVudCBpcyBub3Qgbm9uZSAlfQogICAgICAgICAgICB7JS0gaWYgbWVzc2FnZS5yZWFzb25pbmdfY29udGVudCBpcyBzdHJpbmcgJX0KICAgICAgICAgICAgICAgIHslLSBzZXQgcmVhc29uaW5nX2NvbnRlbnQgPSBtZXNzYWdlLnJlYXNvbmluZ19jb250ZW50ICV9CiAgICAgICAgICAgIHslLSBlbHNlICV9CiAgICAgICAgICAgICAgICB7JS0gc2V0IHJlYXNvbmluZ19jb250ZW50ID0gbWVzc2FnZS5yZWFzb25pbmdfY29udGVudCB8IHN0cmluZyAlfQogICAgICAgICAgICB7JS0gZW5kaWYgJX0KICAgICAgICB7JS0gZWxpZiBtZXNzYWdlLnRoaW5raW5nIGlzIGRlZmluZWQgYW5kIG1lc3NhZ2UudGhpbmtpbmcgaXMgbm90IG5vbmUgJX0KICAgICAgICAgICAgeyUtIGlmIG1lc3NhZ2UudGhpbmtpbmcgaXMgc3RyaW5nICV9CiAgICAgICAgICAgICAgICB7JS0gc2V0IHJlYXNvbmluZ19jb250ZW50ID0gbWVzc2FnZS50aGlua2luZyAlfQogICAgICAgICAgICB7JS0gZWxzZSAlfQogICAgICAgICAgICAgICAgeyUtIHNldCByZWFzb25pbmdfY29udGVudCA9IG1lc3NhZ2UudGhpbmtpbmcgfCBzdHJpbmcgJX0KICAgICAgICAgICAgeyUtIGVuZGlmICV9CiAgICAgICAgeyUtIGVsc2UgJX0KICAgICAgICAgICAgeyUtIHNldCBfdGhpbmtfZW5kID0gJycgJX0KICAgICAgICAgICAgeyUtIGlmIGNvbnRlbnQuc3RhcnRzd2l0aCgnPC90aGluaz4nKSAlfQogICAgICAgICAgICAgICAgeyUtIHNldCBfdGhpbmtfZW5kID0gJzwvdGhpbms+JyAlfQogICAgICAgICAgICB7JS0gZWxpZiBjb250ZW50LnN0YXJ0c3dpdGgoJzwvdGhpbmtpbmc+JykgJX0KICAgICAgICAgICAgICAgIHslLSBzZXQgX3RoaW5rX2VuZCA9ICc8L3RoaW5raW5nPicgJX0KICAgICAgICAgICAgeyUtIGVsaWYgJ1xuPC90aGluaz4nIGluIGNvbnRlbnQgJX0KICAgICAgICAgICAgICAgIHslLSBzZXQgX3RoaW5rX2VuZCA9ICdcbjwvdGhpbms+JyAlfQogICAgICAgICAgICB7JS0gZWxpZiAnXG48L3RoaW5raW5nPicgaW4gY29udGVudCAlfQogICAgICAgICAgICAgICAgeyUtIHNldCBfdGhpbmtfZW5kID0gJ1xuPC90aGlua2luZz4nICV9CiAgICAgICAgICAgIHslLSBlbGlmICdcbjwvIHRoaW5rPicgaW4gY29udGVudCAlfQogICAgICAgICAgICAgICAgeyUtIHNldCBfdGhpbmtfZW5kID0gJ1xuPC8gdGhpbms+JyAlfQogICAgICAgICAgICB7JS0gZWxpZiAnXG48L3RoaW5rID4nIGluIGNvbnRlbnQgJX0KICAgICAgICAgICAgICAgIHslLSBzZXQgX3RoaW5rX2VuZCA9ICdcbjwvdGhpbmsgPicgJX0KICAgICAgICAgICAgeyUtIGVuZGlmICV9CiAgICAgICAgICAgIHslLSBpZiBfdGhpbmtfZW5kICV9CiAgICAgICAgICAgICAgICB7JS0gaWYgJ3RoaW5raW5nJyBpbiBfdGhpbmtfZW5kICV9CiAgICAgICAgICAgICAgICAgICAgeyUtIHNldCBfdGhpbmtfc3RhcnQgPSAnPHRoaW5raW5nPicgJX0KICAgICAgICAgICAgICAgIHslLSBlbHNlICV9CiAgICAgICAgICAgICAgICAgICAgeyUtIHNldCBfdGhpbmtfc3RhcnQgPSAnPHRoaW5rPicgJX0KICAgICAgICAgICAgICAgIHslLSBlbmRpZiAlfQogICAgICAgICAgICAgICAgeyUtIHNldCByZWFzb25pbmdfY29udGVudCA9IGNvbnRlbnQuc3BsaXQoX3RoaW5rX2VuZClbMF0ucnN0cmlwKCdcbicpICV9CiAgICAgICAgICAgICAgICB7JS0gaWYgX3RoaW5rX3N0YXJ0IGluIHJlYXNvbmluZ19jb250ZW50ICV9CiAgICAgICAgICAgICAgICAgICAgeyUtIHNldCByZWFzb25pbmdfY29udGVudCA9IHJlYXNvbmluZ19jb250ZW50LnNwbGl0KF90aGlua19zdGFydClbLTFdLmxzdHJpcCgnXG4nKSAlfQogICAgICAgICAgICAgICAgeyUtIGVuZGlmICV9CiAgICAgICAgICAgICAgICB7JS0gc2V0IGNvbnRlbnQgPSBjb250ZW50LnNwbGl0KF90aGlua19lbmQpWy0xXS5sc3RyaXAoJ1xuJykgJX0KICAgICAgICAgICAgeyUtIGVuZGlmICV9CiAgICAgICAgeyUtIGVuZGlmICV9CiAgICAgICAgeyUtIHNldCByZWFzb25pbmdfY29udGVudCA9IHJlYXNvbmluZ19jb250ZW50IHwgdHJpbSAlfQogICAgICAgIHslLSBpZiAoX3ByZXNlcnZlX3RoaW5raW5nIG9yIGxvb3AuaW5kZXgwID4gbnMubGFzdF9xdWVyeV9pbmRleCkgYW5kIHJlYXNvbmluZ19jb250ZW50ICV9CiAgICAgICAgICAgIHt7LSAnPHxpbV9zdGFydHw+YXNzaXN0YW50XG48dGhpbms+XG4nICsgcmVhc29uaW5nX2NvbnRlbnQgKyAnXG48L3RoaW5rPlxuXG4nICsgY29udGVudCB9fQogICAgICAgIHslLSBlbHNlICV9CiAgICAgICAgICAgIHt7LSAnPHxpbV9zdGFydHw+YXNzaXN0YW50XG4nICsgY29udGVudCB9fQogICAgICAgIHslLSBlbmRpZiAlfQogICAgICAgIHslLSBpZiBtZXNzYWdlLnRvb2xfY2FsbHMgaXMgZGVmaW5lZCBhbmQgbWVzc2FnZS50b29sX2NhbGxzIGFuZCBtZXNzYWdlLnRvb2xfY2FsbHMgaXMgaXRlcmFibGUgYW5kIG1lc3NhZ2UudG9vbF9jYWxscyBpcyBub3QgbWFwcGluZyAlfQogICAgICAgICAgICB7JS0gZm9yIHRvb2xfY2FsbCBpbiBtZXNzYWdlLnRvb2xfY2FsbHMgJX0KICAgICAgICAgICAgICAgIHslLSBpZiB0b29sX2NhbGwuZnVuY3Rpb24gaXMgZGVmaW5lZCBhbmQgdG9vbF9jYWxsLmZ1bmN0aW9uIGlzIG5vdCBub25lICV9CiAgICAgICAgICAgICAgICAgICAgeyUtIHNldCB0YyA9IHRvb2xfY2FsbC5mdW5jdGlvbiAlfQogICAgICAgICAgICAgICAgeyUtIGVsc2UgJX0KICAgICAgICAgICAgICAgICAgICB7JS0gc2V0IHRjID0gdG9vbF9jYWxsICV9CiAgICAgICAgICAgICAgICB7JS0gZW5kaWYgJX0KICAgICAgICAgICAgICAgIHslLSBpZiBfdG9vbF9mb3JtYXQgPT0gJ2pzb24nICV9CiAgICAgICAgICAgICAgICAgICAgeyUtIGlmIG5vdCBsb29wLmZpcnN0IG9yIGNvbnRlbnQgfCB0cmltICV9CiAgICAgICAgICAgICAgICAgICAgICAgIHt7LSAnXG5cbicgfX0KICAgICAgICAgICAgICAgICAgICB7JS0gZW5kaWYgJX0KICAgICAgICAgICAgICAgICAgICB7JS0gc2V0IF9hcmdzID0gJ3t9JyAlfQogICAgICAgICAgICAgICAgICAgIHslLSBpZiB0Yy5hcmd1bWVudHMgaXMgZGVmaW5lZCBhbmQgdGMuYXJndW1lbnRzIGlzIG5vdCBub25lICV9CiAgICAgICAgICAgICAgICAgICAgICAgIHslLSBpZiB0Yy5hcmd1bWVudHMgaXMgbWFwcGluZyAlfQogICAgICAgICAgICAgICAgICAgICAgICAgICAgeyUtIHNldCBfYXJncyA9IHRjLmFyZ3VtZW50cyB8IHRvanNvbiAlfQogICAgICAgICAgICAgICAgICAgICAgICB7JS0gZWxpZiB0Yy5hcmd1bWVudHMgaXMgc3RyaW5nIGFuZCB0Yy5hcmd1bWVudHMgJX0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIHslLSBzZXQgX2FyZ3MgPSB0Yy5hcmd1bWVudHMgJX0KICAgICAgICAgICAgICAgICAgICAgICAgeyUtIGVuZGlmICV9CiAgICAgICAgICAgICAgICAgICAgeyUtIGVuZGlmICV9CiAgICAgICAgICAgICAgICAgICAge3stICc8dG9vbF9jYWxsPlxueyJuYW1lIjogJyB9fXt7LSB0Yy5uYW1lIHwgdG9qc29uIH19e3stICcsICJhcmd1bWVudHMiOiAnIH19e3stIF9hcmdzIH19e3stICd9XG48L3Rvb2xfY2FsbD4nIH19CiAgICAgICAgICAgICAgICB7JS0gZWxzZSAlfQogICAgICAgICAgICAgICAgICAgIHslLSBpZiBsb29wLmZpcnN0ICV9CiAgICAgICAgICAgICAgICAgICAgICAgIHslLSBpZiBjb250ZW50IHwgdHJpbSAlfQogICAgICAgICAgICAgICAgICAgICAgICAgICAge3stICdcblxuPHRvb2xfY2FsbD5cbjxmdW5jdGlvbj0nICsgdGMubmFtZSArICc+XG4nIH19CiAgICAgICAgICAgICAgICAgICAgICAgIHslLSBlbHNlICV9CiAgICAgICAgICAgICAgICAgICAgICAgICAgICB7ey0gJzx0b29sX2NhbGw+XG48ZnVuY3Rpb249JyArIHRjLm5hbWUgKyAnPlxuJyB9fQogICAgICAgICAgICAgICAgICAgICAgICB7JS0gZW5kaWYgJX0KICAgICAgICAgICAgICAgICAgICB7JS0gZWxzZSAlfQogICAgICAgICAgICAgICAgICAgICAgICB7ey0gJ1xuXG48dG9vbF9jYWxsPlxuPGZ1bmN0aW9uPScgKyB0Yy5uYW1lICsgJz5cbicgfX0KICAgICAgICAgICAgICAgICAgICB7JS0gZW5kaWYgJX0KICAgICAgICAgICAgICAgICAgICB7JS0gaWYgdGMuYXJndW1lbnRzIGlzIGRlZmluZWQgYW5kIHRjLmFyZ3VtZW50cyBpcyBub3Qgbm9uZSAlfQogICAgICAgICAgICAgICAgICAgICAgICB7JS0gaWYgdGMuYXJndW1lbnRzIGlzIG1hcHBpbmcgJX0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIHslLSBmb3IgYXJnc19uYW1lLCBhcmdzX3ZhbHVlIGluIHRjLmFyZ3VtZW50cy5pdGVtcygpICV9CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAge3stICc8cGFyYW1ldGVyPScgKyBhcmdzX25hbWUgKyAnPlxuJyB9fQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHslLSBpZiBhcmdzX3ZhbHVlIGlzIG1hcHBpbmcgb3IgKGFyZ3NfdmFsdWUgaXMgc2VxdWVuY2UgYW5kIGFyZ3NfdmFsdWUgaXMgbm90IHN0cmluZykgJX0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgeyUtIHNldCBfYXYgPSBhcmdzX3ZhbHVlIHwgdG9qc29uICV9CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgeyUtIGVsc2UgJX0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgeyUtIHNldCBfYXYgPSBhcmdzX3ZhbHVlIHwgc3RyaW5nICV9CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgeyUtIGVuZGlmICV9CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgeyUtIGlmIG1heF90b29sX2FyZ19jaGFycyA+IDAgYW5kIF9hdiB8IGxlbmd0aCA+IG1heF90b29sX2FyZ19jaGFycyAlfQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB7ey0gX2F2WzptYXhfdG9vbF9hcmdfY2hhcnNdICsgJ1xuW1RSVU5DQVRFRCDigJQgb3JpZ2luYWwgbGVuZ3RoICcgfiAoX2F2IHwgbGVuZ3RoIHwgc3RyaW5nKSB+ICcgY2hhcnNdJyB9fQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHslLSBlbHNlICV9CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHt7LSBfYXYgfX0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB7JS0gZW5kaWYgJX0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB7ey0gJ1xuPC9wYXJhbWV0ZXI+XG4nIH19CiAgICAgICAgICAgICAgICAgICAgICAgICAgICB7JS0gZW5kZm9yICV9CiAgICAgICAgICAgICAgICAgICAgICAgIHslLSBlbGlmIHRjLmFyZ3VtZW50cyBpcyBzdHJpbmcgYW5kIHRjLmFyZ3VtZW50cyAlfQogICAgICAgICAgICAgICAgICAgICAgICAgICAge3stIHRjLmFyZ3VtZW50cyB9fQogICAgICAgICAgICAgICAgICAgICAgICB7JS0gZW5kaWYgJX0KICAgICAgICAgICAgICAgICAgICB7JS0gZW5kaWYgJX0KICAgICAgICAgICAgICAgICAgICB7ey0gJzwvZnVuY3Rpb24+XG48L3Rvb2xfY2FsbD4nIH19CiAgICAgICAgICAgICAgICB7JS0gZW5kaWYgJX0KICAgICAgICAgICAgeyUtIGVuZGZvciAlfQogICAgICAgIHslLSBlbmRpZiAlfQogICAgICAgIHt7LSAnPHxpbV9lbmR8PlxuJyB9fQogICAgeyUtIGVsaWYgbWVzc2FnZS5yb2xlID09ICd0b29sJyAlfQogICAgICAgIHslLSBzZXQgX2NvbnRlbnRfbG93ZXIgPSBjb250ZW50IHwgbG93ZXIgJX0KICAgICAgICB7JS0gc2V0IF9jb250ZW50X2hlYWQgPSBfY29udGVudF9sb3dlcls6ODBdICV9CiAgICAgICAgeyUtIGlmIGNvbnRlbnQgfCBsZW5ndGggPCA1MDAgYW5kICckICcgbm90IGluIGNvbnRlbnQgYW5kICd0b29rICcgbm90IGluIF9jb250ZW50X2xvd2VyIGFuZCAoJyJlcnJvciI6JyBpbiBfY29udGVudF9oZWFkIG9yICdlcnJvcjonIGluIF9jb250ZW50X2hlYWQgb3IgJ2VyciEnIGluIF9jb250ZW50X2hlYWQgb3IgJ2ZhdGFsOicgaW4gX2NvbnRlbnRfaGVhZCBvciAnZXhjZXB0aW9uOicgaW4gX2NvbnRlbnRfaGVhZCBvciAndHJhY2ViYWNrJyBpbiBfY29udGVudF9oZWFkIG9yICdjb21tYW5kIG5vdCBmb3VuZCcgaW4gX2NvbnRlbnRfaGVhZCBvciAnaW52YWxpZCBzeW50YXgnIGluIF9jb250ZW50X2hlYWQgb3IgJ2ZhaWxlZCB0bycgaW4gX2NvbnRlbnRfaGVhZCkgJX0KICAgICAgICAgICAgeyUtIHNldCBuczIuY29uc2VjdXRpdmVfZmFpbHVyZXMgPSBuczIuY29uc2VjdXRpdmVfZmFpbHVyZXMgKyAxICV9CiAgICAgICAgeyUtIGVsc2UgJX0KICAgICAgICAgICAgeyUtIHNldCBuczIuY29uc2VjdXRpdmVfZmFpbHVyZXMgPSAwICV9CiAgICAgICAgeyUtIGVuZGlmICV9CiAgICAgICAgeyUtIGlmIG5zMi5wcmV2X3JvbGUgIT0gJ3Rvb2wnICV9CiAgICAgICAgICAgIHt7LSAnPHxpbV9zdGFydHw+dXNlcicgfX0KICAgICAgICB7JS0gZW5kaWYgJX0KICAgICAgICB7JS0gaWYgbWF4X3Rvb2xfcmVzcG9uc2VfY2hhcnMgPiAwIGFuZCBjb250ZW50IHwgbGVuZ3RoID4gbWF4X3Rvb2xfcmVzcG9uc2VfY2hhcnMgJX0KICAgICAgICAgICAgeyUtIHNldCBjb250ZW50ID0gY29udGVudFs6bWF4X3Rvb2xfcmVzcG9uc2VfY2hhcnNdICsgJ1xuW1RSVU5DQVRFRCDigJQgb3JpZ2luYWwgbGVuZ3RoICcgfiAoY29udGVudCB8IGxlbmd0aCB8IHN0cmluZykgfiAnIGNoYXJzXScgJX0KICAgICAgICB7JS0gZW5kaWYgJX0KICAgICAgICB7ey0gJ1xuPHRvb2xfcmVzcG9uc2U+XG4nICsgY29udGVudCB9fQogICAgICAgIHslLSBpZiBuczIuY29uc2VjdXRpdmVfZmFpbHVyZXMgPj0gMiAlfQogICAgICAgICAgICB7ey0gJ1xuXG7imqDvuI8gU1lTVEVNIFdBUk5JTkc6ICcgfiBuczIuY29uc2VjdXRpdmVfZmFpbHVyZXMgfiAnIGNvbnNlY3V0aXZlIHRvb2wgZXJyb3JzIGRldGVjdGVkLiBZb3VyIHByZXZpb3VzIGFwcHJvYWNoIGlzIGluY29ycmVjdC4gWW91IE1VU1QgdXNlIGEgZnVuZGFtZW50YWxseSBkaWZmZXJlbnQgYXBwcm9hY2ggb3IgY29ycmVjdGVkIGFyZ3VtZW50cy4nIH19CiAgICAgICAgeyUtIGVsaWYgbnMyLmNvbnNlY3V0aXZlX2ZhaWx1cmVzID09IDEgJX0KICAgICAgICAgICAge3stICdcblxu4pqg77iPIFNZU1RFTSBXQVJOSU5HOiBUaGUgcHJldmlvdXMgdG9vbCBjYWxsIHJldHVybmVkIGFuIGVycm9yLiBEaWFnbm9zZSB0aGUgZmFpbHVyZSBhbmQgcmV0cnkgd2l0aCBjb21wbGV0ZWx5IGNvcnJlY3RlZCBhcmd1bWVudHMuJyB9fQogICAgICAgIHslLSBlbmRpZiAlfQogICAgICAgIHt7LSAnXG48L3Rvb2xfcmVzcG9uc2U+JyB9fQogICAgICAgIHslLSBpZiBsb29wLmxhc3QgJX0KICAgICAgICAgICAge3stICc8fGltX2VuZHw+XG4nIH19CiAgICAgICAgeyUtIGVsc2UgJX0KICAgICAgICAgICAgeyUtIHNldCBfbmV4dF9yb2xlID0gX21zZ3NbbG9vcC5pbmRleDAgKyAxXS5yb2xlICV9CiAgICAgICAgICAgIHslLSBpZiBfbmV4dF9yb2xlICE9ICd0b29sJyAlfQogICAgICAgICAgICAgICAge3stICc8fGltX2VuZHw+XG4nIH19CiAgICAgICAgICAgIHslLSBlbmRpZiAlfQogICAgICAgIHslLSBlbmRpZiAlfQogICAgeyUtIGVsc2UgJX0KICAgICAgICB7ey0gJzx8aW1fc3RhcnR8PnVzZXJcblsnICsgbWVzc2FnZS5yb2xlICsgJ106ICcgKyBjb250ZW50ICsgJzx8aW1fZW5kfD5cbicgfX0KICAgIHslLSBlbmRpZiAlfQogICAgeyUtIHNldCBuczIucHJldl9yb2xlID0gbWVzc2FnZS5yb2xlICV9CnslLSBlbmRmb3IgJX0KeyUtIGlmIGFkZF9nZW5lcmF0aW9uX3Byb21wdCAlfQogICAge3stICc8fGltX3N0YXJ0fD5hc3Npc3RhbnRcbicgfX0KICAgIHslLSBpZiBub3QgbnNfc3RhdGUudGhpbmtpbmcgJX0KICAgICAgICB7ey0gJzx0aGluaz5cblxuPC90aGluaz5cblxuJyB9fQogICAgeyUtIGVsaWYgbnMyLmNvbnNlY3V0aXZlX2ZhaWx1cmVzID49IDIgJX0KICAgICAgICB7ey0gJzx0aGluaz5cblxuPC90aGluaz5cblxuJyB9fQogICAgeyUtIGVsc2UgJX0KICAgICAgICB7ey0gJzx0aGluaz5cbicgfX0KICAgIHslLSBlbmRpZiAlfQp7JS0gZW5kaWYgJX0=";
7
+ const QWEN_SHARP = {
8
+ id: "qwen-sharp-v21.3",
9
+ name: "Qwen Sharp — concise, tool-safe",
10
+ family: "qwen",
11
+ description: "froggeric fixed Qwen v21.3 template with peculiar-ragdoll's terse system prompt. Agentic default disables retained thinking to avoid multi-turn context/prefill growth.",
12
+ template: Buffer.from(QWEN_SHARP_TEMPLATE_BASE64, "base64").toString("utf8"),
13
+ systemPromptAddendum: null,
14
+ templateKwargs: { preserve_thinking: false },
15
+ };
16
+ const BUILTIN_HOSTING_PROFILES = [QWEN_SHARP];
17
+ /**
18
+ * Product-owned ids are upgraded wholesale, while records with unknown ids remain
19
+ * user-owned. Do not merge profile fields: stale template bytes or prose must not
20
+ * survive an Otto upgrade.
21
+ */
22
+ export function seedBuiltinHostingProfiles(store) {
23
+ let changed = false;
24
+ for (const profile of BUILTIN_HOSTING_PROFILES) {
25
+ if (JSON.stringify(store.hostingProfiles[profile.id]) === JSON.stringify(profile))
26
+ continue;
27
+ store.hostingProfiles[profile.id] = profile;
28
+ changed = true;
29
+ }
30
+ return changed;
31
+ }
32
+ //# sourceMappingURL=builtin-hosting-profiles.js.map