openmates 0.12.0-alpha.20 → 0.12.0-alpha.22

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.
@@ -6108,7 +6108,7 @@ function printLogo() {
6108
6108
  import { createInterface as createInterface3 } from "readline/promises";
6109
6109
  import { realpathSync, writeFileSync as writeFileSync5 } from "fs";
6110
6110
  import { fileURLToPath as fileURLToPath2 } from "url";
6111
- import { basename as basename3, dirname as dirname2 } from "path";
6111
+ import { basename as basename3, dirname as dirname3 } from "path";
6112
6112
  import WebSocket2 from "ws";
6113
6113
 
6114
6114
  // ../secret-scanner/src/registry.ts
@@ -8577,8 +8577,9 @@ import { execSync, spawn as nodeSpawn } from "child_process";
8577
8577
  import { randomBytes as randomBytes2 } from "crypto";
8578
8578
  import { copyFileSync, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
8579
8579
  import { createInterface as createInterface2 } from "readline";
8580
+ import { createInterface as createPromptInterface } from "readline/promises";
8580
8581
  import { homedir as homedir5 } from "os";
8581
- import { join as join3, resolve as resolve3 } from "path";
8582
+ import { dirname, join as join3, resolve as resolve3 } from "path";
8582
8583
  var SOURCE_COMPOSE_FILE = join3("backend", "core", "docker-compose.yml");
8583
8584
  var IMAGE_COMPOSE_FILE = join3("backend", "core", "docker-compose.selfhost.yml");
8584
8585
  var COMPOSE_OVERRIDE = join3("backend", "core", "docker-compose.override.yml");
@@ -8596,12 +8597,42 @@ var IMAGE_CHANNEL_TAGS = {
8596
8597
  dev: DEV_BRANCH
8597
8598
  };
8598
8599
  var BACKEND_CONFIG_FILE = join3("backend", "config", "backend_config.yml");
8600
+ var IMAGE_RUNTIME_CONFIG_FILE = join3("config", "backend_config.yml");
8601
+ var LOCAL_AI_MODELS_FILE = "local-ai-models.yml";
8599
8602
  var OFF_BY_DEFAULT_FEATURES = /* @__PURE__ */ new Map([
8600
8603
  ["embed:code:application", "Application previews are still unstable"],
8601
8604
  ["platform:projects", "Projects workspace is not ready by default"],
8602
8605
  ["platform:workflows", "Workflows workspace is not implemented yet"],
8603
8606
  ["platform:tasks", "Tasks workspace is not implemented yet"]
8604
8607
  ]);
8608
+ var LOCAL_MODEL_RUNTIME_DEFAULTS = {
8609
+ ollama: {
8610
+ label: "Ollama",
8611
+ serverId: "ollama",
8612
+ baseUrl: "http://host.docker.internal:11434/v1",
8613
+ apiKey: "ollama"
8614
+ },
8615
+ "lm-studio": {
8616
+ label: "LM Studio",
8617
+ serverId: "lm_studio",
8618
+ baseUrl: "http://host.docker.internal:1234/v1",
8619
+ apiKey: "lm-studio"
8620
+ },
8621
+ custom: {
8622
+ label: "Custom OpenAI-compatible API",
8623
+ serverId: "custom_openai_compatible",
8624
+ baseUrl: "",
8625
+ apiKey: "local"
8626
+ }
8627
+ };
8628
+ var MODEL_CREATOR_OPTIONS = [
8629
+ { id: "alibaba", name: "Alibaba / Qwen", match: /(^|[-_:/])qwen/i },
8630
+ { id: "google", name: "Google / Gemma", match: /(^|[-_:/])gemma/i },
8631
+ { id: "mistral", name: "Mistral", match: /(^|[-_:/])(mistral|mixtral|ministral)/i },
8632
+ { id: "openai", name: "OpenAI", match: /(^|[-_:/])gpt-oss/i },
8633
+ { id: "deepseek", name: "DeepSeek", match: /(^|[-_:/])deepseek/i },
8634
+ { id: "custom", name: "Custom", match: null }
8635
+ ];
8605
8636
  var MINIMAL_ENV_TEMPLATE = `# OpenMates self-host image-mode environment
8606
8637
  SECRET__MISTRAL_AI__API_KEY=
8607
8638
  SECRET__CEREBRAS__API_KEY=
@@ -8898,8 +8929,10 @@ async function writeImageModeRuntimeFiles(installPath, imageTag) {
8898
8929
  const coreDir = join3(installPath, "backend", "core");
8899
8930
  const vaultConfigDir = join3(coreDir, "vault", "config");
8900
8931
  mkdirSync3(vaultConfigDir, { recursive: true });
8932
+ mkdirSync3(join3(installPath, "config", "providers"), { recursive: true });
8901
8933
  writeFileSync3(join3(coreDir, "docker-compose.selfhost.yml"), await loadSelfHostComposeTemplate(templateRefForImageTag(imageTag, getPackageVersion())));
8902
8934
  writeFileSync3(join3(vaultConfigDir, "vault.hcl"), VAULT_CONFIG_TEMPLATE);
8935
+ ensureImageRuntimeConfig(installPath);
8903
8936
  const envPath = join3(installPath, ".env");
8904
8937
  let envContent = existsSync5(envPath) ? readFileSync5(envPath, "utf-8") : MINIMAL_ENV_TEMPLATE;
8905
8938
  envContent = setEnvIfEmpty(envContent, "DATABASE_ADMIN_PASSWORD", randomHex(12));
@@ -8963,6 +8996,7 @@ function defaultCloneBranchForVersion(version) {
8963
8996
  }
8964
8997
  function hasLlmCredentials(envPath) {
8965
8998
  if (!existsSync5(envPath)) return false;
8999
+ if (hasLocalAiModels(dirname(envPath))) return true;
8966
9000
  const content = readFileSync5(envPath, "utf-8");
8967
9001
  for (const line of content.split("\n")) {
8968
9002
  const trimmed = line.trim();
@@ -8987,7 +9021,7 @@ function warnIfMissingLlmCredentials(installPath) {
8987
9021
  }
8988
9022
  if (!hasLlmCredentials(envPath)) {
8989
9023
  console.error(
8990
- "No LLM provider API key found in .env.\nOpenMates will start, but AI chat/model processing will stay unavailable until you add one.\n\nAdd at least one of these to your .env file:\n SECRET__OPENAI__API_KEY=sk-...\n SECRET__ANTHROPIC__API_KEY=sk-ant-...\n SECRET__GOOGLE_AI_STUDIO__API_KEY=...\n\nAfter updating .env, run 'openmates server restart'."
9024
+ "No LLM provider API key found in .env.\nOpenMates will start, but AI chat/model processing will stay unavailable until you add one.\n\nRun 'openmates server ai models add' to add a local Ollama/LM Studio model, or add at least one of these to your .env file:\n SECRET__OPENAI__API_KEY=sk-...\n SECRET__ANTHROPIC__API_KEY=sk-ant-...\n SECRET__GOOGLE_AI_STUDIO__API_KEY=...\n\nAfter updating .env, run 'openmates server restart'."
8991
9025
  );
8992
9026
  }
8993
9027
  }
@@ -9003,6 +9037,164 @@ async function confirmDestructive(phrase) {
9003
9037
  function printJson(data) {
9004
9038
  console.log(JSON.stringify(data, null, 2));
9005
9039
  }
9040
+ function localModelsOverlayPath(installPath, installMode = getInstallMode(installPath)) {
9041
+ if (installMode === "source") return join3(installPath, "backend", "providers", LOCAL_AI_MODELS_FILE);
9042
+ return join3(installPath, "config", "providers", LOCAL_AI_MODELS_FILE);
9043
+ }
9044
+ function imageBackendConfigPath(installPath) {
9045
+ return join3(installPath, IMAGE_RUNTIME_CONFIG_FILE);
9046
+ }
9047
+ function ensureImageRuntimeConfig(installPath) {
9048
+ const configPath = imageBackendConfigPath(installPath);
9049
+ if (existsSync5(configPath)) return;
9050
+ mkdirSync3(dirname(configPath), { recursive: true });
9051
+ writeFileSync3(configPath, renderFeatureOverrides({ enabled: [], disabled: [] }));
9052
+ }
9053
+ function readLocalModelOverlay(path) {
9054
+ if (!existsSync5(path)) return { providers: [] };
9055
+ try {
9056
+ const parsed = JSON.parse(readFileSync5(path, "utf-8"));
9057
+ return { providers: Array.isArray(parsed.providers) ? parsed.providers : [] };
9058
+ } catch (error) {
9059
+ throw new Error(
9060
+ `Could not parse ${path}. This file is managed by the OpenMates CLI and must remain JSON-compatible YAML. ${error instanceof Error ? error.message : String(error)}`
9061
+ );
9062
+ }
9063
+ }
9064
+ function writeLocalModelOverlay(path, overlay) {
9065
+ mkdirSync3(dirname(path), { recursive: true });
9066
+ writeFileSync3(path, `${JSON.stringify(overlay, null, 2)}
9067
+ `);
9068
+ }
9069
+ function hasLocalAiModels(installPath) {
9070
+ const imagePath = join3(installPath, "config", "providers", LOCAL_AI_MODELS_FILE);
9071
+ const sourcePath = join3(installPath, "backend", "providers", LOCAL_AI_MODELS_FILE);
9072
+ return [imagePath, sourcePath].some((path) => {
9073
+ if (!existsSync5(path)) return false;
9074
+ try {
9075
+ return readLocalModelOverlay(path).providers.some((provider) => provider.models.length > 0);
9076
+ } catch {
9077
+ return false;
9078
+ }
9079
+ });
9080
+ }
9081
+ function sanitizeModelId(raw) {
9082
+ return raw.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "local-model";
9083
+ }
9084
+ function inferCreatorId(modelId) {
9085
+ return MODEL_CREATOR_OPTIONS.find((option) => option.match?.test(modelId))?.id ?? "custom";
9086
+ }
9087
+ function creatorDisplayName(creatorId) {
9088
+ return MODEL_CREATOR_OPTIONS.find((option) => option.id === creatorId)?.name ?? creatorId;
9089
+ }
9090
+ function normalizeCreatorId(value) {
9091
+ const normalized = value.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "_").replace(/^_+|_+$/g, "");
9092
+ return normalized || "custom_local";
9093
+ }
9094
+ function normalizeRuntimeKey(value) {
9095
+ const normalized = value.trim().toLowerCase().replace(/_/g, "-");
9096
+ if (normalized === "lmstudio" || normalized === "lm-studio") return "lm-studio";
9097
+ if (normalized === "ollama") return "ollama";
9098
+ return "custom";
9099
+ }
9100
+ function boolFromFlag(value, defaultValue = false) {
9101
+ if (value === void 0) return defaultValue;
9102
+ if (value === true) return true;
9103
+ if (value === false) return false;
9104
+ const normalized = value.toLowerCase();
9105
+ return ["1", "true", "yes", "y", "on"].includes(normalized);
9106
+ }
9107
+ async function promptText(question, defaultValue = "") {
9108
+ const rl = createPromptInterface({ input: process.stdin, output: process.stderr });
9109
+ try {
9110
+ const suffix = defaultValue ? ` (${defaultValue})` : "";
9111
+ const answer = await rl.question(`${question}${suffix}: `);
9112
+ return answer.trim() || defaultValue;
9113
+ } finally {
9114
+ rl.close();
9115
+ }
9116
+ }
9117
+ async function promptChoice(question, choices, defaultValue) {
9118
+ console.error(question);
9119
+ choices.forEach((choice, index) => {
9120
+ const marker = choice.value === defaultValue ? " [default]" : "";
9121
+ console.error(` ${index + 1}. ${choice.label}${marker}`);
9122
+ });
9123
+ const answer = await promptText("Choose number or value", defaultValue);
9124
+ const numeric = Number.parseInt(answer, 10);
9125
+ if (Number.isInteger(numeric) && numeric >= 1 && numeric <= choices.length) {
9126
+ return choices[numeric - 1].value;
9127
+ }
9128
+ const direct = choices.find((choice) => choice.value === answer || choice.label.toLowerCase() === answer.toLowerCase());
9129
+ return direct?.value ?? answer;
9130
+ }
9131
+ async function fetchLocalModelIds(baseUrl, apiKey) {
9132
+ const response = await fetch(`${baseUrl.replace(/\/+$/, "")}/models`, {
9133
+ headers: { Authorization: `Bearer ${apiKey || "local"}` }
9134
+ });
9135
+ if (!response.ok) throw new Error(`Failed to fetch local models: HTTP ${response.status}`);
9136
+ const body = await response.json();
9137
+ return (body.data ?? []).map((model) => model.id).filter((id) => Boolean(id));
9138
+ }
9139
+ async function testLocalModel(baseUrl, apiKey, modelId) {
9140
+ const controller = new AbortController();
9141
+ const timeout = setTimeout(() => controller.abort(), 3e4);
9142
+ try {
9143
+ const response = await fetch(`${baseUrl.replace(/\/+$/, "")}/chat/completions`, {
9144
+ method: "POST",
9145
+ signal: controller.signal,
9146
+ headers: {
9147
+ Authorization: `Bearer ${apiKey || "local"}`,
9148
+ "Content-Type": "application/json"
9149
+ },
9150
+ body: JSON.stringify({
9151
+ model: modelId,
9152
+ messages: [
9153
+ { role: "system", content: "Answer with only the number." },
9154
+ { role: "user", content: "1+2?" }
9155
+ ],
9156
+ stream: false,
9157
+ temperature: 0
9158
+ })
9159
+ });
9160
+ if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
9161
+ const body = await response.json();
9162
+ return body.choices?.[0]?.message?.content?.trim() ?? "";
9163
+ } finally {
9164
+ clearTimeout(timeout);
9165
+ }
9166
+ }
9167
+ function upsertLocalModel(path, providerId, providerName, model) {
9168
+ const overlay = readLocalModelOverlay(path);
9169
+ let provider = overlay.providers.find((item) => item.provider_id === providerId);
9170
+ if (!provider) {
9171
+ provider = {
9172
+ provider_id: providerId,
9173
+ name: providerName,
9174
+ description: `Local self-hosted models for ${providerName}.`,
9175
+ models: []
9176
+ };
9177
+ overlay.providers.push(provider);
9178
+ }
9179
+ const modelId = model.id;
9180
+ provider.models = provider.models.filter((existing) => existing.id !== modelId);
9181
+ provider.models.push(model);
9182
+ writeLocalModelOverlay(path, overlay);
9183
+ }
9184
+ function removeLocalModel(path, fullModelId) {
9185
+ const [providerId, modelId] = fullModelId.split("/", 2);
9186
+ if (!providerId || !modelId) throw new Error("Use provider/model-id format.");
9187
+ const overlay = readLocalModelOverlay(path);
9188
+ const provider = overlay.providers.find((item) => item.provider_id === providerId);
9189
+ if (!provider) return false;
9190
+ const before = provider.models.length;
9191
+ provider.models = provider.models.filter((model) => model.id !== modelId);
9192
+ writeLocalModelOverlay(path, overlay);
9193
+ return provider.models.length !== before;
9194
+ }
9195
+ function localModelsFromOverlay(overlay) {
9196
+ return overlay.providers.flatMap((provider) => provider.models.map((model) => ({ providerId: provider.provider_id, model })));
9197
+ }
9006
9198
  async function serverStatus(flags) {
9007
9199
  requireDocker();
9008
9200
  const installPath = resolveServerPath(flags);
@@ -9461,7 +9653,9 @@ async function serverFeatures(rest, flags) {
9461
9653
  const action = rest[0] ?? "list";
9462
9654
  const featureId = rest[1];
9463
9655
  const installPath = resolveServerPath(flags);
9464
- const configPath = join3(installPath, BACKEND_CONFIG_FILE);
9656
+ const installMode = getInstallMode(installPath);
9657
+ if (installMode === "image") ensureImageRuntimeConfig(installPath);
9658
+ const configPath = installMode === "image" ? imageBackendConfigPath(installPath) : join3(installPath, BACKEND_CONFIG_FILE);
9465
9659
  if (!existsSync5(configPath)) {
9466
9660
  throw new Error(`Backend config not found at ${configPath}. Run 'openmates server install' first or pass --path <dir>.`);
9467
9661
  }
@@ -9521,6 +9715,179 @@ async function serverFeatures(rest, flags) {
9521
9715
  }
9522
9716
  throw new Error(`Unknown server features command '${action}'. Use list, enable, disable, reset, or explain.`);
9523
9717
  }
9718
+ async function serverAiModelsAdd(flags) {
9719
+ const installPath = resolveServerPath(flags);
9720
+ const installMode = getInstallMode(installPath);
9721
+ const overlayPath = localModelsOverlayPath(installPath, installMode);
9722
+ const runtimeInput = typeof flags.runtime === "string" ? flags.runtime : await promptChoice(
9723
+ "Which local runtime should OpenMates use?",
9724
+ [
9725
+ { value: "ollama", label: "Ollama" },
9726
+ { value: "lm-studio", label: "LM Studio" },
9727
+ { value: "custom", label: "Custom OpenAI-compatible API" }
9728
+ ],
9729
+ "ollama"
9730
+ );
9731
+ const runtimeKey = normalizeRuntimeKey(runtimeInput);
9732
+ const runtime = LOCAL_MODEL_RUNTIME_DEFAULTS[runtimeKey];
9733
+ const baseUrl = typeof flags["base-url"] === "string" ? flags["base-url"] : await promptText("OpenAI-compatible base URL", runtime.baseUrl);
9734
+ const apiKey = typeof flags["api-key"] === "string" ? flags["api-key"] : runtimeKey === "custom" ? await promptText("API key", runtime.apiKey) : runtime.apiKey;
9735
+ let availableModels = [];
9736
+ try {
9737
+ availableModels = await fetchLocalModelIds(baseUrl, apiKey);
9738
+ } catch (error) {
9739
+ console.error(`Could not fetch /v1/models: ${error instanceof Error ? error.message : String(error)}`);
9740
+ }
9741
+ const rawModelId = typeof flags.model === "string" ? flags.model : availableModels.length ? await promptChoice(
9742
+ "Which installed model should OpenMates add?",
9743
+ [...availableModels.map((id) => ({ value: id, label: id })), { value: "manual", label: "Enter manually" }],
9744
+ availableModels[0]
9745
+ ) : await promptText("Local model ID");
9746
+ const serverModelId = rawModelId === "manual" ? await promptText("Local model ID") : rawModelId;
9747
+ if (!serverModelId) throw new Error("Model ID is required.");
9748
+ const inferredCreator = inferCreatorId(serverModelId);
9749
+ const creatorInput = typeof flags.creator === "string" ? flags.creator : await promptChoice(
9750
+ "Who created the model?",
9751
+ MODEL_CREATOR_OPTIONS.map((option) => ({ value: option.id, label: option.name })),
9752
+ inferredCreator
9753
+ );
9754
+ const providerId = creatorInput === "custom" ? normalizeCreatorId(await promptText("Custom creator/provider ID", "custom_local")) : normalizeCreatorId(creatorInput);
9755
+ const providerName = providerId === creatorInput ? creatorDisplayName(providerId) : providerId;
9756
+ const internalModelId = typeof flags.id === "string" ? sanitizeModelId(flags.id) : `${sanitizeModelId(serverModelId)}-local`;
9757
+ const displayName = typeof flags.name === "string" ? flags.name : await promptText("Display name", serverModelId);
9758
+ const supportsImages = boolFromFlag(flags.images ?? flags.image ?? flags.vision, false);
9759
+ const supportsTools = boolFromFlag(flags.tools ?? flags["tool-use"], false);
9760
+ const contextWindowInput = typeof flags["context-window"] === "string" ? flags["context-window"] : await promptText("Context window tokens", "32768");
9761
+ const contextWindow = Number.parseInt(contextWindowInput, 10) || 32768;
9762
+ if (flags["skip-test"] !== true) {
9763
+ console.error(`Testing ${runtime.label} model '${serverModelId}'...`);
9764
+ const testOutput = await testLocalModel(baseUrl, apiKey, serverModelId);
9765
+ if (!testOutput) throw new Error("Local model test returned no content. Not saving model.");
9766
+ console.error(`Test response: ${testOutput}`);
9767
+ }
9768
+ const model = {
9769
+ id: internalModelId,
9770
+ name: displayName,
9771
+ description: `${displayName} served locally through ${runtime.label}.`,
9772
+ country_origin: "local",
9773
+ for_app_skill: "ai.ask",
9774
+ allow_auto_select: false,
9775
+ local: true,
9776
+ self_hosted: true,
9777
+ input_types: supportsImages ? ["text", "image"] : ["text"],
9778
+ output_types: ["text"],
9779
+ default_server: runtime.serverId,
9780
+ servers: [
9781
+ {
9782
+ id: runtime.serverId,
9783
+ name: runtime.label,
9784
+ model_id: serverModelId,
9785
+ region: "local",
9786
+ base_url: baseUrl.replace(/\/+$/, ""),
9787
+ api_key: apiKey,
9788
+ supports_tools: supportsTools
9789
+ }
9790
+ ],
9791
+ pricing: { fixed: { credits: 0 } },
9792
+ costs: {
9793
+ input_per_million_token: { price: 0, currency: "USD", max_context: contextWindow },
9794
+ output_per_million_token: { price: 0, currency: "USD", max_context: contextWindow }
9795
+ },
9796
+ features: {
9797
+ streaming: true,
9798
+ tool_use: supportsTools,
9799
+ max_context: contextWindow
9800
+ }
9801
+ };
9802
+ upsertLocalModel(overlayPath, providerId, providerName, model);
9803
+ if (installMode === "image") ensureImageRuntimeConfig(installPath);
9804
+ if (flags.json === true) {
9805
+ printJson({ command: "server ai models add", status: "success", model: `${providerId}/${internalModelId}`, overlayPath });
9806
+ } else {
9807
+ console.log(`Added local model: ${providerId}/${internalModelId}`);
9808
+ console.log(`Updated ${overlayPath}`);
9809
+ console.log("Restart the server for model changes to take effect: openmates server restart");
9810
+ console.log("Self-hosted local models charge 0 credits; token usage may still be recorded in usage history.");
9811
+ }
9812
+ }
9813
+ async function serverAiModelsList(flags) {
9814
+ const installPath = resolveServerPath(flags);
9815
+ const overlayPath = localModelsOverlayPath(installPath);
9816
+ const overlay = readLocalModelOverlay(overlayPath);
9817
+ const models = localModelsFromOverlay(overlay).map(({ providerId, model }) => ({
9818
+ id: `${providerId}/${String(model.id ?? "")}`,
9819
+ name: model.name ?? model.id,
9820
+ server: Array.isArray(model.servers) ? model.servers[0]?.id : void 0,
9821
+ serverModelId: Array.isArray(model.servers) ? model.servers[0]?.model_id : void 0
9822
+ }));
9823
+ if (flags.json === true) {
9824
+ printJson({ overlayPath, models });
9825
+ return;
9826
+ }
9827
+ if (!models.length) {
9828
+ console.log("No local AI models configured. Add one with: openmates server ai models add");
9829
+ return;
9830
+ }
9831
+ console.log("Local AI models:");
9832
+ for (const model of models) {
9833
+ console.log(` ${model.id} (${model.server}: ${model.serverModelId})`);
9834
+ }
9835
+ }
9836
+ async function serverAiModelsTest(rest, flags) {
9837
+ const installPath = resolveServerPath(flags);
9838
+ const overlayPath = localModelsOverlayPath(installPath);
9839
+ const fullModelId = rest[0];
9840
+ if (!fullModelId || !fullModelId.includes("/")) throw new Error("Usage: openmates server ai models test <provider/model-id>");
9841
+ const [providerId, modelId] = fullModelId.split("/", 2);
9842
+ const overlay = readLocalModelOverlay(overlayPath);
9843
+ const provider = overlay.providers.find((item) => item.provider_id === providerId);
9844
+ const model = provider?.models.find((item) => item.id === modelId);
9845
+ const server = Array.isArray(model?.servers) ? model.servers[0] : void 0;
9846
+ if (!server) throw new Error(`Local model not found in ${overlayPath}: ${fullModelId}`);
9847
+ const baseUrl = String(server.base_url ?? "");
9848
+ const apiKey = String(server.api_key ?? "local");
9849
+ const serverModelId = String(server.model_id ?? "");
9850
+ const output = await testLocalModel(baseUrl, apiKey, serverModelId);
9851
+ if (flags.json === true) {
9852
+ printJson({ model: fullModelId, status: "success", output });
9853
+ } else {
9854
+ console.log(`Model test succeeded: ${fullModelId}`);
9855
+ console.log(`Response: ${output}`);
9856
+ }
9857
+ }
9858
+ async function serverAiModelsRemove(rest, flags) {
9859
+ const installPath = resolveServerPath(flags);
9860
+ const overlayPath = localModelsOverlayPath(installPath);
9861
+ const fullModelId = rest[0];
9862
+ if (!fullModelId) throw new Error("Usage: openmates server ai models remove <provider/model-id>");
9863
+ const removed = removeLocalModel(overlayPath, fullModelId);
9864
+ if (flags.json === true) {
9865
+ printJson({ command: "server ai models remove", status: removed ? "success" : "not_found", model: fullModelId });
9866
+ } else if (removed) {
9867
+ console.log(`Removed local model: ${fullModelId}`);
9868
+ console.log("Restart the server for model changes to take effect: openmates server restart");
9869
+ } else {
9870
+ console.log(`Local model not found: ${fullModelId}`);
9871
+ }
9872
+ }
9873
+ async function serverAi(rest, flags) {
9874
+ const area = rest[0];
9875
+ const action = rest[1] ?? "list";
9876
+ const args = rest.slice(2);
9877
+ if (area !== "models") throw new Error("Usage: openmates server ai models <add|list|test|remove>");
9878
+ switch (action) {
9879
+ case "add":
9880
+ return serverAiModelsAdd(flags);
9881
+ case "list":
9882
+ return serverAiModelsList(flags);
9883
+ case "test":
9884
+ return serverAiModelsTest(args, flags);
9885
+ case "remove":
9886
+ return serverAiModelsRemove(args, flags);
9887
+ default:
9888
+ throw new Error(`Unknown server ai models command '${action}'. Use add, list, test, or remove.`);
9889
+ }
9890
+ }
9524
9891
  async function serverUninstall(flags) {
9525
9892
  requireDocker();
9526
9893
  const installPath = resolveServerPath(flags);
@@ -9588,6 +9955,7 @@ Commands:
9588
9955
  logs Display server logs
9589
9956
  update Update to latest version (pull images, or git pull + rebuild for source installs)
9590
9957
  make-admin Grant admin privileges to a user
9958
+ ai Manage self-hosted local AI models
9591
9959
  reset Reset server data (requires confirmation)
9592
9960
  uninstall Completely remove OpenMates (requires confirmation)
9593
9961
 
@@ -9639,12 +10007,19 @@ Command Options:
9639
10007
  openmates server features reset <feature-id>
9640
10008
  openmates server features explain <feature-id>
9641
10009
 
10010
+ ai models:
10011
+ openmates server ai models add
10012
+ openmates server ai models list
10013
+ openmates server ai models test <provider/model-id>
10014
+ openmates server ai models remove <provider/model-id>
10015
+
9642
10016
  Examples:
9643
10017
  openmates server install
9644
10018
  openmates server start --with-overrides
9645
10019
  openmates server logs --container api --follow
9646
10020
  openmates server make-admin user@example.com
9647
10021
  openmates server features disable app:videos
10022
+ openmates server ai models add
9648
10023
  openmates server features enable embed:code:application
9649
10024
  openmates server update
9650
10025
  openmates server update --dry-run
@@ -9677,6 +10052,8 @@ async function handleServer(subcommand, rest, flags) {
9677
10052
  return serverReset(flags);
9678
10053
  case "make-admin":
9679
10054
  return serverMakeAdmin(rest, flags);
10055
+ case "ai":
10056
+ return serverAi(rest, flags);
9680
10057
  case "features":
9681
10058
  return serverFeatures(rest, flags);
9682
10059
  case "uninstall":
@@ -41926,7 +42303,7 @@ function buildAssistantFeedbackDecision(rating) {
41926
42303
  import { randomUUID as randomUUID3 } from "crypto";
41927
42304
  import { existsSync as existsSync6, mkdtempSync, readFileSync as readFileSync6, readdirSync, writeFileSync as writeFileSync4 } from "fs";
41928
42305
  import { tmpdir } from "os";
41929
- import { dirname, join as join4, resolve as resolve5 } from "path";
42306
+ import { dirname as dirname2, join as join4, resolve as resolve5 } from "path";
41930
42307
  import { fileURLToPath } from "url";
41931
42308
  var DEFAULT_JUDGE_MODEL = "google/gemini-3-flash-preview";
41932
42309
  var DEFAULT_EXTENSIVE_SIZE = 10;
@@ -42778,13 +43155,13 @@ function normalizeModelKey(model) {
42778
43155
  }
42779
43156
  function findProvidersDir() {
42780
43157
  const currentFile = fileURLToPath(import.meta.url);
42781
- let current = dirname(currentFile);
43158
+ let current = dirname2(currentFile);
42782
43159
  for (let index = 0; index < 8; index += 1) {
42783
43160
  const candidate = join4(current, "backend", "providers");
42784
43161
  if (existsSync6(candidate)) return candidate;
42785
43162
  const parentCandidate = join4(current, "..", "..", "backend", "providers");
42786
43163
  if (existsSync6(parentCandidate)) return resolve5(parentCandidate);
42787
- const next = dirname(current);
43164
+ const next = dirname2(current);
42788
43165
  if (next === current) break;
42789
43166
  current = next;
42790
43167
  }
@@ -42905,7 +43282,7 @@ function round2(value) {
42905
43282
  return Math.round(value * 100) / 100;
42906
43283
  }
42907
43284
  function defaultImageFixturePath() {
42908
- const fixtureDir = join4(dirname(fileURLToPath(import.meta.url)), "..", "fixtures");
43285
+ const fixtureDir = join4(dirname2(fileURLToPath(import.meta.url)), "..", "fixtures");
42909
43286
  const fixturePath = join4(fixtureDir, "brandenburger-tor.png");
42910
43287
  if (existsSync6(fixturePath)) return fixturePath;
42911
43288
  const tempDir = mkdtempSync(join4(tmpdir(), "openmates-benchmark-"));
@@ -44528,6 +44905,25 @@ async function printSettingsMutationResult(resultPromise, flags) {
44528
44905
  process.stdout.write("\x1B[32m\u2713\x1B[0m Settings updated\n");
44529
44906
  if (result && typeof result === "object") printGenericObject(result);
44530
44907
  }
44908
+ function printReportIssueCreateResult(result, flags) {
44909
+ if (flags.json === true) {
44910
+ printJson2(result);
44911
+ return;
44912
+ }
44913
+ process.stdout.write("\x1B[32m\u2713\x1B[0m Issue reported\n");
44914
+ const obj = result && typeof result === "object" ? result : {};
44915
+ const issueId = typeof obj.issue_id === "string" ? obj.issue_id : "";
44916
+ const shortIssueId = typeof obj.short_issue_id === "string" ? obj.short_issue_id : "";
44917
+ if (shortIssueId || issueId) {
44918
+ console.log(`Issue reference: ${shortIssueId || issueId}`);
44919
+ }
44920
+ if (issueId && shortIssueId && issueId !== shortIssueId) {
44921
+ console.log(`Internal issue ID: ${issueId}`);
44922
+ }
44923
+ if (typeof obj.message === "string") {
44924
+ console.log(obj.message);
44925
+ }
44926
+ }
44531
44927
  function addQueryParam(params, key, value) {
44532
44928
  if (typeof value === "string" && value.length > 0) params.set(key, value);
44533
44929
  }
@@ -44671,11 +45067,11 @@ function parseYamlScalar(value) {
44671
45067
  }
44672
45068
  async function saveDownloadedDocument(document, output) {
44673
45069
  const { mkdir, writeFile } = await import("fs/promises");
44674
- const { join: join5, basename: basename4, dirname: dirname3 } = await import("path");
45070
+ const { join: join5, basename: basename4, dirname: dirname4 } = await import("path");
44675
45071
  const target = typeof output === "string" ? output : ".";
44676
45072
  const filename = basename4(document.filename || "document.pdf");
44677
45073
  const filePath = target.endsWith(".pdf") ? target : join5(target, filename);
44678
- await mkdir(dirname3(filePath), { recursive: true });
45074
+ await mkdir(dirname4(filePath), { recursive: true });
44679
45075
  await writeFile(filePath, document.data);
44680
45076
  return filePath;
44681
45077
  }
@@ -44754,7 +45150,7 @@ async function promptSecret(question) {
44754
45150
  }
44755
45151
  async function writeSecretFile(filePath, content, force = false) {
44756
45152
  const { mkdir, writeFile, stat: stat2 } = await import("fs/promises");
44757
- const { dirname: dirname3 } = await import("path");
45153
+ const { dirname: dirname4 } = await import("path");
44758
45154
  try {
44759
45155
  await stat2(filePath);
44760
45156
  if (!force) throw new Error(`${filePath} already exists. Use --force to overwrite.`);
@@ -44764,7 +45160,7 @@ async function writeSecretFile(filePath, content, force = false) {
44764
45160
  }
44765
45161
  if (error instanceof Error && !("code" in error)) throw error;
44766
45162
  }
44767
- await mkdir(dirname3(filePath), { recursive: true });
45163
+ await mkdir(dirname4(filePath), { recursive: true });
44768
45164
  await writeFile(filePath, content, { mode: 384 });
44769
45165
  return filePath;
44770
45166
  }
@@ -45391,7 +45787,8 @@ async function handleSettings(client, subcommand, rest, flags) {
45391
45787
  const title = typeof flags.title === "string" ? flags.title : void 0;
45392
45788
  const body = typeof flags.body === "string" ? flags.body : void 0;
45393
45789
  if (!title || !body) throw new Error("Provide --title and --body.");
45394
- await printSettingsMutationResult(client.settingsPost("issues", { title, description: body }), flags);
45790
+ const result = await client.settingsPost("issues", { title, description: body });
45791
+ printReportIssueCreateResult(result, flags);
45395
45792
  return;
45396
45793
  }
45397
45794
  if (matches(tokens, ["report-issue", "status"])) {
@@ -47829,7 +48226,7 @@ async function handleDocs(client, subcommand, rest, flags) {
47829
48226
  }
47830
48227
  if (subcommand === "download") {
47831
48228
  const { writeFile, mkdir } = await import("fs/promises");
47832
- const { join: join5, dirname: dirname3 } = await import("path");
48229
+ const { join: join5, dirname: dirname4 } = await import("path");
47833
48230
  if (flags.all === true) {
47834
48231
  const outputDir = typeof flags.output === "string" ? flags.output : "./openmates-docs";
47835
48232
  const tree = await client.listDocs();
@@ -47839,7 +48236,7 @@ async function handleDocs(client, subcommand, rest, flags) {
47839
48236
  for (const slug2 of slugs) {
47840
48237
  const content2 = await client.getDoc(slug2);
47841
48238
  const filePath = join5(outputDir, `${slug2}.md`);
47842
- await mkdir(dirname3(filePath), { recursive: true });
48239
+ await mkdir(dirname4(filePath), { recursive: true });
47843
48240
  await writeFile(filePath, content2, "utf-8");
47844
48241
  count++;
47845
48242
  process.stderr.write(`\r Downloaded ${count}/${slugs.length}`);
@@ -47912,7 +48309,7 @@ function isCliEntrypoint() {
47912
48309
  try {
47913
48310
  const invokedPath = realpathSync(entrypoint);
47914
48311
  const modulePath = realpathSync(fileURLToPath2(import.meta.url));
47915
- return invokedPath === modulePath || basename3(invokedPath) === "cli.js" && dirname2(invokedPath) === dirname2(modulePath);
48312
+ return invokedPath === modulePath || basename3(invokedPath) === "cli.js" && dirname3(invokedPath) === dirname3(modulePath);
47916
48313
  } catch {
47917
48314
  return false;
47918
48315
  }
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  getExtForLang,
4
4
  serializeToYaml
5
- } from "./chunk-OSH7SOFJ.js";
5
+ } from "./chunk-PHFCP5AM.js";
6
6
  import "./chunk-AXNRPVLE.js";
7
7
  export {
8
8
  getExtForLang,
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  deriveAppUrl,
10
10
  getExtForLang,
11
11
  serializeToYaml
12
- } from "./chunk-OSH7SOFJ.js";
12
+ } from "./chunk-PHFCP5AM.js";
13
13
  import "./chunk-AXNRPVLE.js";
14
14
  export {
15
15
  ASSISTANT_FEEDBACK_REPORT_TITLE,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openmates",
3
- "version": "0.12.0-alpha.20",
3
+ "version": "0.12.0-alpha.22",
4
4
  "description": "OpenMates CLI and SDK",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",