stratum-cli 0.7.0-beta.0 → 0.7.0

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.
@@ -16,6 +16,14 @@
16
16
  "model": "claude-sonnet-4-5",
17
17
  "apiKey": "${LITELLM_API_KEY}",
18
18
  "contextWindow": 200000
19
+ },
20
+ "hosted": {
21
+ "type": "openai-compatible",
22
+ "baseUrl": "https://api.example.com/v1",
23
+ "apiKey": "${EXAMPLE_API_KEY}",
24
+ "models": {
25
+ "big-model": { "contextWindow": 200000 }
26
+ }
19
27
  }
20
28
  }
21
29
  },
package/CHANGELOG.md CHANGED
@@ -6,6 +6,24 @@ El formato sigue [Keep a Changelog](https://keepachangelog.com/es-ES/1.1.0/) y l
6
6
 
7
7
  > Las versiones anteriores a la 0.2.1-beta.0 solo existen como tags de git: la primera publicación en npm fue la 0.2.1-beta.0.
8
8
 
9
+ ## [0.7.0] — 2026-10-03
10
+
11
+ Versión estable de la 0.7.0: incluye todo lo de la [0.7.0-beta.0](#070-beta0--2026-09-27) (endurecimiento de la persistencia, de la escritura de ficheros, de MCP, del cliente del LLM y de `web_fetch`) y, además, los **modelos descubiertos**: a un provider le basta la URL y la key.
12
+
13
+ ### Añadido
14
+ - **Providers sin `model`**: un provider ya no necesita `model` ni `contextWindow` en la config; con `baseUrl` y `apiKey` los modelos se piden a `GET /models`. `chat` abre el selector de modelos al arrancar si el provider no tiene ninguno y guarda la primera elección como modelo por defecto.
15
+ - `stratum run` y `stratum init` aceptan `--model`. Sin modelo fijado, usan el único que exponga el provider; con varios, fallan listándolos en vez de elegir uno.
16
+ - `stratum provider add <alias> --base-url … --api-key-env VAR` da de alta un provider sin pasar por el wizard, y `stratum provider models [alias] [--set <id>]` lista los modelos y fija el de por defecto.
17
+ - `/provider remove <alias>` en el chat.
18
+ - Core de Stratum Desktop: guardado atómico de conversaciones y sesiones, guardado al cerrar y avisos cuando un guardado falla. Sin cambios de comportamiento en la CLI.
19
+
20
+ ### Cambiado
21
+ - **Ventana de contexto por modelo**: `models.<id>.contextWindow` > la del provider si está escrita > la que declare `/models` > 32768. El wizard ya no escribe `contextWindow`.
22
+ - `stratum provider remove` quita el provider de todos los ficheros que lo definen (global y proyecto), no solo del primero.
23
+
24
+ ### Corregido
25
+ - **Wizard de providers**: con una `apiKey` escrita como `${VAR}`, la consulta de modelos enviaba el placeholder literal y caía siempre a la entrada manual. Ahora consulta con la variable expandida y guarda el placeholder.
26
+
9
27
  ## [0.7.0-beta.0] — 2026-09-27
10
28
 
11
29
  Pre-release para pruebas (`npm install -g stratum-cli@beta`); `latest` sigue en la 0.6.0.
package/dist/index.js CHANGED
@@ -177,19 +177,32 @@ var init_accelerator = __esm({
177
177
 
178
178
  // src/config/schema.ts
179
179
  import { z } from "zod";
180
- var ProviderConfigSchema, McpServerSchema, SSHHostSchema, SSHConfigSchema, EnvironmentSchema, SessionProfileSchema, StratumConfigSchema;
180
+ var DEFAULT_CONTEXT_WINDOW, ProviderConfigSchema, McpServerSchema, SSHHostSchema, SSHConfigSchema, EnvironmentSchema, SessionProfileSchema, StratumConfigSchema;
181
181
  var init_schema = __esm({
182
182
  "src/config/schema.ts"() {
183
183
  "use strict";
184
184
  init_esm_shims();
185
185
  init_secrets();
186
186
  init_accelerator();
187
+ DEFAULT_CONTEXT_WINDOW = 32768;
187
188
  ProviderConfigSchema = z.object({
188
189
  type: z.literal("openai-compatible"),
189
190
  baseUrl: z.string().url(),
190
- model: z.string(),
191
+ /**
192
+ * Modelo de arranque. Vacío (o ausente) = se resuelve contra `GET /models`:
193
+ * `chat` abre el selector, `run`/`init` exigen `--model` salvo que el
194
+ * provider exponga uno solo. Basta con `baseUrl` + `apiKey`.
195
+ */
196
+ model: z.string().default(""),
191
197
  apiKey: z.string().default(""),
192
- contextWindow: z.number().int().positive().default(32768),
198
+ /**
199
+ * Ventana de contexto del provider. Opcional: si falta se usa la que
200
+ * `/models` declare para el modelo activo y, si no declara ninguna,
201
+ * `DEFAULT_CONTEXT_WINDOW`. Explícita, gana a lo descubierto.
202
+ */
203
+ contextWindow: z.number().int().positive().optional(),
204
+ /** Ajustes por modelo (id exacto); ganan a los del provider. */
205
+ models: z.record(z.object({ contextWindow: z.number().int().positive().optional() }).strict()).optional(),
193
206
  /**
194
207
  * Timeouts de inactividad del cliente en ms (§12.3); `0` desactiva. Por
195
208
  * defecto 120 s hasta las cabeceras y 300 s sin recibir nada del cuerpo, que
@@ -2044,6 +2057,99 @@ var init_openai_compatible = __esm({
2044
2057
  }
2045
2058
  });
2046
2059
 
2060
+ // src/providers/utils.ts
2061
+ function contextWindowOf(entry) {
2062
+ for (const field of CONTEXT_WINDOW_FIELDS) {
2063
+ const value = entry[field];
2064
+ if (typeof value === "number" && Number.isInteger(value) && value > 0) return value;
2065
+ }
2066
+ return void 0;
2067
+ }
2068
+ function discoveredContextWindow(baseUrl, model) {
2069
+ return discoveredWindows.get(normalizeBaseUrl(baseUrl))?.get(model);
2070
+ }
2071
+ async function fetchModels(baseUrl, apiKey, opts = {}) {
2072
+ return (await fetchModelInfos(baseUrl, apiKey, opts)).map((m) => m.id);
2073
+ }
2074
+ async function fetchModelInfos(baseUrl, apiKey, opts = {}) {
2075
+ const { timeoutMs = 5e3, fetchFn = fetch } = opts;
2076
+ const url = `${normalizeBaseUrl(baseUrl)}/models`;
2077
+ const headers = {};
2078
+ if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
2079
+ const response = await fetchFn(url, {
2080
+ headers,
2081
+ signal: AbortSignal.timeout(timeoutMs)
2082
+ });
2083
+ if (!response.ok) {
2084
+ throw new Error(`GET ${url} \u2192 HTTP ${response.status}`);
2085
+ }
2086
+ const body = await response.json();
2087
+ if (!Array.isArray(body.data)) {
2088
+ throw new Error(`GET ${url} \u2192 respuesta sin campo "data" (no es OpenAI-compatible)`);
2089
+ }
2090
+ const byId = /* @__PURE__ */ new Map();
2091
+ for (const entry of body.data) {
2092
+ const id = entry?.id;
2093
+ if (typeof id !== "string" || id.length === 0 || byId.has(id)) continue;
2094
+ const contextWindow = contextWindowOf(entry);
2095
+ byId.set(id, contextWindow ? { id, contextWindow } : { id });
2096
+ }
2097
+ const infos = [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
2098
+ const windows = /* @__PURE__ */ new Map();
2099
+ for (const info of infos) {
2100
+ if (info.contextWindow) windows.set(info.id, info.contextWindow);
2101
+ }
2102
+ discoveredWindows.set(normalizeBaseUrl(baseUrl), windows);
2103
+ return infos;
2104
+ }
2105
+ function classifyBackendByUrl(baseUrl) {
2106
+ const u = baseUrl.toLowerCase();
2107
+ if (u.includes("11434")) return "ollama";
2108
+ if (u.includes("/ollama") || u.includes("ollama")) return "ollama";
2109
+ if (u.includes(":4000") || u.includes("litellm")) return "litellm";
2110
+ if (u.includes("api.openai.com")) return "openai";
2111
+ if (u.includes(":8000") || u.includes("vllm")) return "vllm";
2112
+ if (u.includes(":8080") || u.includes("llama")) return "llamacpp";
2113
+ return "unknown";
2114
+ }
2115
+ async function detectCapabilities(baseUrl, apiKey, opts = {}) {
2116
+ const backend = classifyBackendByUrl(baseUrl);
2117
+ try {
2118
+ const models = await fetchModels(baseUrl, apiKey, opts);
2119
+ if (models.length === 0) {
2120
+ return {
2121
+ backend,
2122
+ listsModels: false,
2123
+ models: [],
2124
+ note: `El endpoint ${baseUrl}/models respondi\xF3 pero no devolvi\xF3 modelos` + (backend === "llamacpp" ? " (llama.cpp server suele exponer solo el modelo cargado)." : ".")
2125
+ };
2126
+ }
2127
+ return { backend, listsModels: true, models };
2128
+ } catch (err) {
2129
+ return {
2130
+ backend,
2131
+ listsModels: false,
2132
+ models: [],
2133
+ note: `No se pudo listar modelos en ${baseUrl}/models: ${String(err)}` + (backend === "llamacpp" ? " \u2014 llama.cpp server puede no implementar /models; escribe el modelo a mano." : " \u2014 escribe el modelo a mano.")
2134
+ };
2135
+ }
2136
+ }
2137
+ var CONTEXT_WINDOW_FIELDS, discoveredWindows, normalizeBaseUrl;
2138
+ var init_utils = __esm({
2139
+ "src/providers/utils.ts"() {
2140
+ "use strict";
2141
+ init_esm_shims();
2142
+ CONTEXT_WINDOW_FIELDS = [
2143
+ "context_length",
2144
+ "context_window",
2145
+ "max_model_len",
2146
+ "max_context_length"
2147
+ ];
2148
+ discoveredWindows = /* @__PURE__ */ new Map();
2149
+ normalizeBaseUrl = (baseUrl) => baseUrl.replace(/\/$/, "");
2150
+ }
2151
+ });
2152
+
2047
2153
  // src/providers/router.ts
2048
2154
  function makeClient(cfg) {
2049
2155
  return new OpenAICompatible(cfg.baseUrl, cfg.apiKey, cfg.model, { timeouts: cfg.timeouts });
@@ -2053,7 +2159,9 @@ var init_router = __esm({
2053
2159
  "src/providers/router.ts"() {
2054
2160
  "use strict";
2055
2161
  init_esm_shims();
2162
+ init_schema();
2056
2163
  init_openai_compatible();
2164
+ init_utils();
2057
2165
  init_logging();
2058
2166
  log2 = getLogger("provider");
2059
2167
  ProviderRouter = class {
@@ -2096,6 +2204,8 @@ var init_router = __esm({
2096
2204
  */
2097
2205
  switchModel(model) {
2098
2206
  this.activeConfig = { ...this.activeConfig, model };
2207
+ const entry = this.providers[this.activeKey];
2208
+ if (entry && !entry.model) this.providers[this.activeKey] = { ...entry, model };
2099
2209
  }
2100
2210
  /**
2101
2211
  * Cambia el provider activo en caliente (comando `/provider <name>`, Hito 6).
@@ -2114,6 +2224,19 @@ var init_router = __esm({
2114
2224
  this.provider = makeClient(cfg);
2115
2225
  this.failedKeys.clear();
2116
2226
  }
2227
+ /**
2228
+ * Retira de la sesión un provider eliminado de la config (`/provider remove`):
2229
+ * deja de listarse y de servir de fallback. El activo no se puede retirar.
2230
+ */
2231
+ forgetProvider(name) {
2232
+ if (name === this.activeKey) {
2233
+ throw new Error(`"${name}" es el provider activo: cambia antes a otro.`);
2234
+ }
2235
+ delete this.providers[name];
2236
+ const index = this.fallbackOrder.indexOf(name);
2237
+ if (index !== -1) this.fallbackOrder.splice(index, 1);
2238
+ this.failedKeys.delete(name);
2239
+ }
2117
2240
  /**
2118
2241
  * Reaplica la configuración del provider activo en caliente
2119
2242
  * (comando `/config_provider`, Hito 3.5). Recrea el cliente HTTP.
@@ -2143,7 +2266,7 @@ var init_router = __esm({
2143
2266
  */
2144
2267
  advanceProvider() {
2145
2268
  this.failedKeys.add(this.activeKey);
2146
- const next = this.fallbackOrder.find((k) => !this.failedKeys.has(k));
2269
+ const next = this.fallbackOrder.find((k) => !this.failedKeys.has(k) && this.providers[k].model);
2147
2270
  if (!next) {
2148
2271
  log2.error("fallback exhausted", { tried: [...this.failedKeys] });
2149
2272
  return null;
@@ -2171,8 +2294,18 @@ var init_router = __esm({
2171
2294
  get model() {
2172
2295
  return this.activeConfig.model;
2173
2296
  }
2297
+ /**
2298
+ * Ventana del modelo activo: ajuste por modelo (`models.<id>`) > la del
2299
+ * provider si es explícita > la que declaró `/models` > default.
2300
+ */
2174
2301
  get contextWindow() {
2175
- return this.activeConfig.contextWindow;
2302
+ const cfg = this.activeConfig;
2303
+ return cfg.models?.[cfg.model]?.contextWindow ?? cfg.contextWindow ?? discoveredContextWindow(cfg.baseUrl, cfg.model) ?? DEFAULT_CONTEXT_WINDOW;
2304
+ }
2305
+ /** ¿Depende la ventana de lo que declare `/models`? (ni modelo ni provider la fijan). */
2306
+ get contextWindowIsDiscoverable() {
2307
+ const cfg = this.activeConfig;
2308
+ return cfg.models?.[cfg.model]?.contextWindow === void 0 && cfg.contextWindow === void 0;
2176
2309
  }
2177
2310
  async healthCheck() {
2178
2311
  return this.provider.healthCheck();
@@ -2181,6 +2314,56 @@ var init_router = __esm({
2181
2314
  }
2182
2315
  });
2183
2316
 
2317
+ // src/cli/startup-model.ts
2318
+ async function resolveStartupModel(config, opts = {}) {
2319
+ const alias = opts.provider ?? config.provider?.default;
2320
+ const entry = alias ? config.provider?.providers[alias] : void 0;
2321
+ if (!config.provider || !alias || !entry) return { ok: true, fetched: false };
2322
+ const requested = opts.model?.trim();
2323
+ if (requested) {
2324
+ config.provider.providers[alias] = { ...entry, model: requested };
2325
+ return { ok: true, fetched: false };
2326
+ }
2327
+ if (entry.model) return { ok: true, fetched: false };
2328
+ let ids;
2329
+ try {
2330
+ ids = (await fetchModelInfos(entry.baseUrl, entry.apiKey, opts)).map((m) => m.id);
2331
+ } catch (err) {
2332
+ return {
2333
+ ok: false,
2334
+ error: `El provider "${alias}" no tiene modelo configurado y no se pudo consultar ${entry.baseUrl}/models (${err instanceof Error ? err.message : String(err)}). Indica uno con --model <id>.`
2335
+ };
2336
+ }
2337
+ if (ids.length === 1) {
2338
+ config.provider.providers[alias] = { ...entry, model: ids[0] };
2339
+ return { ok: true, fetched: true };
2340
+ }
2341
+ if (ids.length === 0) {
2342
+ return {
2343
+ ok: false,
2344
+ error: `El provider "${alias}" no tiene modelo configurado y ${entry.baseUrl}/models no devolvi\xF3 ninguno. Indica uno con --model <id>.`
2345
+ };
2346
+ }
2347
+ return {
2348
+ ok: false,
2349
+ error: `El provider "${alias}" no tiene modelo configurado. Elige uno con --model <id>, o f\xEDjalo con \`stratum provider models ${alias} --set <id>\`.
2350
+ Modelos disponibles: ${ids.join(", ")}`
2351
+ };
2352
+ }
2353
+ async function discoverContextWindow(provider, opts = {}) {
2354
+ try {
2355
+ await fetchModelInfos(provider.baseUrl, provider.apiKey, opts);
2356
+ } catch {
2357
+ }
2358
+ }
2359
+ var init_startup_model = __esm({
2360
+ "src/cli/startup-model.ts"() {
2361
+ "use strict";
2362
+ init_esm_shims();
2363
+ init_utils();
2364
+ }
2365
+ });
2366
+
2184
2367
  // src/tools/truncate.ts
2185
2368
  function truncateToolOutput(text, maxChars = MAX_TOOL_OUTPUT_CHARS) {
2186
2369
  if (text.length <= maxChars) return text;
@@ -6031,6 +6214,16 @@ function removeProvider(name, configPath) {
6031
6214
  const backupPath = writeConfigWithBackup(path2, raw);
6032
6215
  return { configPath: path2, backupPath, newDefault };
6033
6216
  }
6217
+ function removeProviderEverywhere(name, startDir) {
6218
+ const projectPath = findConfigFile(startDir ?? process.cwd());
6219
+ const paths = [.../* @__PURE__ */ new Set([projectPath, GLOBAL_CONFIG_PATH])].filter(
6220
+ (p) => !!p && hasProvider(p, name)
6221
+ );
6222
+ if (paths.length === 0) {
6223
+ throw new Error(`Provider "${name}" no existe en ning\xFAn .stratumrc.json`);
6224
+ }
6225
+ return paths.map((path2) => removeProvider(name, path2));
6226
+ }
6034
6227
  function setDefaultProvider(name, configPath) {
6035
6228
  const path2 = configPath ?? resolveWritableConfigPath();
6036
6229
  const raw = readRaw(path2);
@@ -6050,6 +6243,26 @@ function readRawProvider(name, configPath) {
6050
6243
  const entry = providers?.[name];
6051
6244
  return entry && typeof entry === "object" ? entry : null;
6052
6245
  }
6246
+ function hasProvider(path2, name) {
6247
+ if (!existsSync3(path2)) return false;
6248
+ try {
6249
+ return readRawProvider(name, path2) !== null;
6250
+ } catch {
6251
+ return false;
6252
+ }
6253
+ }
6254
+ function setProviderModel(name, model, startDir) {
6255
+ const projectPath = findConfigFile(startDir ?? process.cwd());
6256
+ const path2 = [projectPath, GLOBAL_CONFIG_PATH].find(
6257
+ (p) => !!p && hasProvider(p, name)
6258
+ );
6259
+ if (!path2) return null;
6260
+ const raw = readRaw(path2);
6261
+ const providers = raw["provider"]["providers"];
6262
+ providers[name]["model"] = model;
6263
+ writeConfigWithBackup(path2, raw);
6264
+ return path2;
6265
+ }
6053
6266
  function setConfigValue(key, value, cwd = process.cwd()) {
6054
6267
  const path2 = findConfigFile(cwd) ?? join9(cwd, ".stratumrc.json");
6055
6268
  const raw = readRaw(path2);
@@ -6442,7 +6655,7 @@ var init_local = __esm({
6442
6655
  killedOnMaxBytes: false
6443
6656
  });
6444
6657
  };
6445
- const fail = (err) => {
6658
+ const fail2 = (err) => {
6446
6659
  if (settled) return;
6447
6660
  settled = true;
6448
6661
  cleanup();
@@ -6476,7 +6689,7 @@ var init_local = __esm({
6476
6689
  subprocess.then(
6477
6690
  (result) => {
6478
6691
  if (!killing && result.failed && result.exitCode === void 0 && !result.signal) {
6479
- fail(new ExecSpawnError(result.shortMessage ?? result.message ?? "spawn failed"));
6692
+ fail2(new ExecSpawnError(result.shortMessage ?? result.message ?? "spawn failed"));
6480
6693
  return;
6481
6694
  }
6482
6695
  settle2(killing ? null : result.exitCode ?? null);
@@ -6486,7 +6699,7 @@ var init_local = __esm({
6486
6699
  settle2(null);
6487
6700
  return;
6488
6701
  }
6489
- fail(new ExecSpawnError(err.message));
6702
+ fail2(new ExecSpawnError(err.message));
6490
6703
  }
6491
6704
  );
6492
6705
  });
@@ -6808,7 +7021,7 @@ function runRemote(client, alias, command, req) {
6808
7021
  killedOnMaxBytes: true
6809
7022
  });
6810
7023
  };
6811
- const fail = (err) => {
7024
+ const fail2 = (err) => {
6812
7025
  if (settled) return;
6813
7026
  settled = true;
6814
7027
  cleanup();
@@ -6855,7 +7068,7 @@ function runRemote(client, alias, command, req) {
6855
7068
  return;
6856
7069
  }
6857
7070
  if (err) {
6858
- fail(new ExecSpawnError(`Could not run the command on "${alias}": ${err.message}`));
7071
+ fail2(new ExecSpawnError(`Could not run the command on "${alias}": ${err.message}`));
6859
7072
  return;
6860
7073
  }
6861
7074
  stream = channel;
@@ -6887,7 +7100,7 @@ function runRemote(client, alias, command, req) {
6887
7100
  finish();
6888
7101
  return;
6889
7102
  }
6890
- fail(new ExecSpawnError(streamErr.message));
7103
+ fail2(new ExecSpawnError(streamErr.message));
6891
7104
  });
6892
7105
  if (req.stdin !== void 0) channel.end(req.stdin);
6893
7106
  else channel.end();
@@ -14738,6 +14951,10 @@ var init_core = __esm({
14738
14951
  this.router.switchProvider(name);
14739
14952
  this.reloadMemory();
14740
14953
  }
14954
+ /** Retira de la sesión un provider eliminado de la config (`/provider remove`). */
14955
+ forgetProvider(name) {
14956
+ this.router.forgetProvider(name);
14957
+ }
14741
14958
  /** Alias de los providers configurados (para `/provider` y su autocompletado). */
14742
14959
  get providerNames() {
14743
14960
  return this.router.providerNames;
@@ -16243,65 +16460,6 @@ var init_cleanup = __esm({
16243
16460
  }
16244
16461
  });
16245
16462
 
16246
- // src/providers/utils.ts
16247
- async function fetchModels(baseUrl, apiKey, opts = {}) {
16248
- const { timeoutMs = 5e3, fetchFn = fetch } = opts;
16249
- const url = `${baseUrl.replace(/\/$/, "")}/models`;
16250
- const headers = {};
16251
- if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
16252
- const response = await fetchFn(url, {
16253
- headers,
16254
- signal: AbortSignal.timeout(timeoutMs)
16255
- });
16256
- if (!response.ok) {
16257
- throw new Error(`GET ${url} \u2192 HTTP ${response.status}`);
16258
- }
16259
- const body = await response.json();
16260
- if (!Array.isArray(body.data)) {
16261
- throw new Error(`GET ${url} \u2192 respuesta sin campo "data" (no es OpenAI-compatible)`);
16262
- }
16263
- const ids = body.data.map((m) => m.id).filter((id) => typeof id === "string" && id.length > 0);
16264
- return [...new Set(ids)].sort((a, b) => a.localeCompare(b));
16265
- }
16266
- function classifyBackendByUrl(baseUrl) {
16267
- const u = baseUrl.toLowerCase();
16268
- if (u.includes("11434")) return "ollama";
16269
- if (u.includes("/ollama") || u.includes("ollama")) return "ollama";
16270
- if (u.includes(":4000") || u.includes("litellm")) return "litellm";
16271
- if (u.includes("api.openai.com")) return "openai";
16272
- if (u.includes(":8000") || u.includes("vllm")) return "vllm";
16273
- if (u.includes(":8080") || u.includes("llama")) return "llamacpp";
16274
- return "unknown";
16275
- }
16276
- async function detectCapabilities(baseUrl, apiKey, opts = {}) {
16277
- const backend = classifyBackendByUrl(baseUrl);
16278
- try {
16279
- const models = await fetchModels(baseUrl, apiKey, opts);
16280
- if (models.length === 0) {
16281
- return {
16282
- backend,
16283
- listsModels: false,
16284
- models: [],
16285
- note: `El endpoint ${baseUrl}/models respondi\xF3 pero no devolvi\xF3 modelos` + (backend === "llamacpp" ? " (llama.cpp server suele exponer solo el modelo cargado)." : ".")
16286
- };
16287
- }
16288
- return { backend, listsModels: true, models };
16289
- } catch (err) {
16290
- return {
16291
- backend,
16292
- listsModels: false,
16293
- models: [],
16294
- note: `No se pudo listar modelos en ${baseUrl}/models: ${String(err)}` + (backend === "llamacpp" ? " \u2014 llama.cpp server puede no implementar /models; escribe el modelo a mano." : " \u2014 escribe el modelo a mano.")
16295
- };
16296
- }
16297
- }
16298
- var init_utils = __esm({
16299
- "src/providers/utils.ts"() {
16300
- "use strict";
16301
- init_esm_shims();
16302
- }
16303
- });
16304
-
16305
16463
  // src/cli/ui/theme.ts
16306
16464
  var theme;
16307
16465
  var init_theme = __esm({
@@ -19003,9 +19161,22 @@ function validateBaseUrl(url) {
19003
19161
  return "URL inv\xE1lida (ej. http://localhost:11434/v1)";
19004
19162
  }
19005
19163
  }
19164
+ function resolveApiKey(raw) {
19165
+ const missing = [];
19166
+ const key = String(expandEnvVars(raw, (m) => missing.push(m.name)));
19167
+ return { key, missing };
19168
+ }
19006
19169
  async function discoverModels(baseUrl, apiKey, fetchFn) {
19170
+ const { key, missing } = resolveApiKey(apiKey);
19171
+ if (missing.length > 0) {
19172
+ return {
19173
+ models: [],
19174
+ manualFallback: true,
19175
+ error: `variable de entorno no definida: ${missing.join(", ")}`
19176
+ };
19177
+ }
19007
19178
  try {
19008
- const models = await fetchModels(baseUrl, apiKey, fetchFn ? { fetchFn } : {});
19179
+ const models = await fetchModels(baseUrl, key, fetchFn ? { fetchFn } : {});
19009
19180
  if (models.length === 0) {
19010
19181
  return { models: [], manualFallback: true, error: "El endpoint /models no devolvi\xF3 modelos" };
19011
19182
  }
@@ -19015,12 +19186,13 @@ async function discoverModels(baseUrl, apiKey, fetchFn) {
19015
19186
  }
19016
19187
  }
19017
19188
  function buildProviderEntry(params) {
19189
+ const model = params.model?.trim();
19018
19190
  return {
19019
19191
  type: "openai-compatible",
19020
19192
  baseUrl: params.baseUrl.trim().replace(/\/$/, ""),
19021
- model: params.model.trim(),
19193
+ ...model ? { model } : {},
19022
19194
  apiKey: params.apiKey,
19023
- contextWindow: params.contextWindow ?? 32768
19195
+ ...params.contextWindow !== void 0 ? { contextWindow: params.contextWindow } : {}
19024
19196
  };
19025
19197
  }
19026
19198
  var PROVIDER_TYPE_PRESETS;
@@ -19028,6 +19200,7 @@ var init_wizard_logic = __esm({
19028
19200
  "src/cli/ui/wizard-logic.ts"() {
19029
19201
  "use strict";
19030
19202
  init_esm_shims();
19203
+ init_loader();
19031
19204
  init_utils();
19032
19205
  PROVIDER_TYPE_PRESETS = [
19033
19206
  {
@@ -19189,9 +19362,11 @@ function ProviderWizard({
19189
19362
  ] }),
19190
19363
  /* @__PURE__ */ jsxs26(Text26, { color: theme.textFaint, children: [
19191
19364
  " ",
19192
- "Puedes usar ",
19365
+ "Escribe ",
19193
19366
  "${VAR}",
19194
- " editando el archivo despu\xE9s; aqu\xED se guarda literal"
19367
+ " (p. ej. ",
19368
+ "${NAN_API_KEY}",
19369
+ ") para no guardar la key en el archivo"
19195
19370
  ] })
19196
19371
  ] });
19197
19372
  case "name":
@@ -19421,7 +19596,7 @@ var init_session_commands = __esm({
19421
19596
  },
19422
19597
  {
19423
19598
  name: "/provider",
19424
- description: "Cambia el provider activo en esta sesi\xF3n (sin args: lista los configurados)",
19599
+ description: "Cambia el provider activo en esta sesi\xF3n (sin args: lista; remove <alias>: lo elimina)",
19425
19600
  hasArgs: true
19426
19601
  },
19427
19602
  {
@@ -20911,7 +21086,12 @@ function App({
20911
21086
  setOverlay({ kind: "model-loading" });
20912
21087
  const cfg = agent.getActiveProviderConfig();
20913
21088
  detectCapabilities(cfg.baseUrl, cfg.apiKey).then((caps) => {
20914
- if (caps.listsModels && caps.models.length > 0) {
21089
+ if (!agent.model && caps.models.length === 1) {
21090
+ setOverlay(null);
21091
+ agent.switchModel(caps.models[0]);
21092
+ refreshProviderHealth();
21093
+ dispatch({ type: "SYSTEM_MESSAGE", text: `Modelo: ${caps.models[0]}` });
21094
+ } else if (caps.listsModels && caps.models.length > 0) {
20915
21095
  setOverlay({ kind: "model-select", models: caps.models });
20916
21096
  } else {
20917
21097
  setModelManualValue(agent.model);
@@ -20921,21 +21101,46 @@ function App({
20921
21101
  setModelManualValue(agent.model);
20922
21102
  setOverlay({ kind: "model-manual", note: String(err) });
20923
21103
  });
20924
- }, [agent]);
21104
+ }, [agent, refreshProviderHealth]);
20925
21105
  const applyModel = useCallback4(
20926
21106
  (model) => {
20927
21107
  setOverlay(null);
20928
21108
  const trimmed = model.trim();
20929
21109
  if (!trimmed || trimmed === agent.model) return;
21110
+ const wasUnset = !agent.model;
20930
21111
  agent.switchModel(trimmed);
20931
21112
  refreshProviderHealth();
21113
+ if (!wasUnset) {
21114
+ dispatch({
21115
+ type: "SYSTEM_MESSAGE",
21116
+ text: `Modelo cambiado a ${trimmed} (solo esta sesi\xF3n; no se ha modificado .stratumrc.json).`
21117
+ });
21118
+ return;
21119
+ }
21120
+ let saved = null;
21121
+ let saveError;
21122
+ try {
21123
+ saved = setProviderModel(agent.providerName, trimmed);
21124
+ } catch (err) {
21125
+ saveError = err instanceof Error ? err.message : String(err);
21126
+ }
20932
21127
  dispatch({
20933
21128
  type: "SYSTEM_MESSAGE",
20934
- text: `Modelo cambiado a ${trimmed} (solo esta sesi\xF3n; no se ha modificado .stratumrc.json).`
21129
+ text: saved ? `Modelo: ${trimmed} (guardado como modelo por defecto de "${agent.providerName}" en ${saved}; /model lo cambia en la sesi\xF3n).` : `Modelo: ${trimmed} (solo esta sesi\xF3n${saveError ? `; no se pudo guardar: ${saveError}` : ""}).`
20935
21130
  });
20936
21131
  },
20937
21132
  [agent, refreshProviderHealth]
20938
21133
  );
21134
+ const promptForModel = useCallback4(() => {
21135
+ dispatch({
21136
+ type: "SYSTEM_MESSAGE",
21137
+ text: `El provider "${agent.providerName}" no tiene modelo fijado: elige uno de los que expone.`
21138
+ });
21139
+ openModelSelector();
21140
+ }, [agent, openModelSelector]);
21141
+ useEffect4(() => {
21142
+ if (!agent.model) promptForModel();
21143
+ }, []);
20939
21144
  const openProviderEditor = useCallback4(() => {
20940
21145
  const name = agent.providerName;
20941
21146
  const active = agent.getActiveProviderConfig();
@@ -20947,7 +21152,8 @@ function App({
20947
21152
  baseUrl: typeof raw?.["baseUrl"] === "string" ? raw["baseUrl"] : active.baseUrl,
20948
21153
  apiKey: typeof raw?.["apiKey"] === "string" ? raw["apiKey"] : active.apiKey,
20949
21154
  model: typeof raw?.["model"] === "string" ? raw["model"] : active.model,
20950
- contextWindow: typeof raw?.["contextWindow"] === "number" ? raw["contextWindow"] : active.contextWindow
21155
+ // Solo si está escrita: sin ella manda lo que declare `/models`.
21156
+ contextWindow: typeof raw?.["contextWindow"] === "number" ? raw["contextWindow"] : void 0
20951
21157
  }
20952
21158
  });
20953
21159
  }, [agent]);
@@ -20958,7 +21164,7 @@ function App({
20958
21164
  const { configPath, backupPath } = upsertProvider(result.name, result.config, false);
20959
21165
  if (result.name === agent.providerName) {
20960
21166
  const expanded = expandEnvVars(result.config);
20961
- agent.reconfigureProvider(expanded);
21167
+ agent.reconfigureProvider({ model: "", ...expanded });
20962
21168
  }
20963
21169
  dispatch({
20964
21170
  type: "SYSTEM_MESSAGE",
@@ -21471,6 +21677,33 @@ ${lines.join("\n")}`
21471
21677
  dispatch({ type: "INPUT_CHANGE", value: "" });
21472
21678
  const target = cmd.slice("/provider".length).trim();
21473
21679
  const names = agent.providerNames;
21680
+ const removal = target.match(/^(?:remove|rm)\s+(\S+)$/);
21681
+ if (removal) {
21682
+ const alias = removal[1];
21683
+ if (alias === agent.providerName) {
21684
+ dispatch({
21685
+ type: "SYSTEM_MESSAGE",
21686
+ text: `"${alias}" es el provider activo: cambia antes a otro con /provider <alias>.`
21687
+ });
21688
+ return;
21689
+ }
21690
+ try {
21691
+ const results = removeProviderEverywhere(alias);
21692
+ if (names.includes(alias)) agent.forgetProvider(alias);
21693
+ const files = results.map((r) => r.configPath).join(", ");
21694
+ const promoted = results.find((r) => r.newDefault)?.newDefault;
21695
+ dispatch({
21696
+ type: "SYSTEM_MESSAGE",
21697
+ text: `Provider "${alias}" eliminado de ${files} (copia en .bak).` + (promoted ? ` Era el provider por defecto: ahora lo es "${promoted}".` : "")
21698
+ });
21699
+ } catch (err) {
21700
+ dispatch({
21701
+ type: "SYSTEM_MESSAGE",
21702
+ text: `No se pudo eliminar "${alias}": ${err instanceof Error ? err.message : String(err)}`
21703
+ });
21704
+ }
21705
+ return;
21706
+ }
21474
21707
  if (!target) {
21475
21708
  const lines = names.map(
21476
21709
  (n) => n === agent.providerName ? ` \u25B6 ${n} (activo)` : ` ${n}`
@@ -21481,7 +21714,7 @@ ${lines.join("\n")}`
21481
21714
 
21482
21715
  ${lines.join("\n")}
21483
21716
 
21484
- Uso: /provider <alias> para cambiar en esta sesi\xF3n.`
21717
+ Uso: /provider <alias> para cambiar en esta sesi\xF3n \xB7 /provider remove <alias> para eliminarlo de la config.`
21485
21718
  });
21486
21719
  return;
21487
21720
  }
@@ -21499,6 +21732,10 @@ Uso: /provider <alias> para cambiar en esta sesi\xF3n.`
21499
21732
  try {
21500
21733
  agent.switchProvider(target);
21501
21734
  refreshProviderHealth();
21735
+ if (!agent.model) {
21736
+ promptForModel();
21737
+ return;
21738
+ }
21502
21739
  dispatch({
21503
21740
  type: "SYSTEM_MESSAGE",
21504
21741
  text: `Provider activo: ${target} (modelo ${agent.model}, solo esta sesi\xF3n; no se ha modificado .stratumrc.json).`
@@ -21603,6 +21840,10 @@ Uso: /provider <alias> para cambiar en esta sesi\xF3n.`
21603
21840
  return;
21604
21841
  }
21605
21842
  if (state.thinking) return;
21843
+ if (!agent.model && (!cmd.startsWith("/") || /^\/(init|plan|compact)(\s|$)/.test(cmd))) {
21844
+ promptForModel();
21845
+ return;
21846
+ }
21606
21847
  if (paletteItems.length > 0) {
21607
21848
  const sel = paletteItems[effPaletteIndex];
21608
21849
  if (sel && sel.name !== cmd) {
@@ -21622,7 +21863,9 @@ Uso: /provider <alias> para cambiar en esta sesi\xF3n.`
21622
21863
  paletteItems,
21623
21864
  effPaletteIndex,
21624
21865
  completePaletteSelection,
21625
- executeCommand
21866
+ executeCommand,
21867
+ agent,
21868
+ promptForModel
21626
21869
  ]
21627
21870
  );
21628
21871
  if (state.phase === "banner") {
@@ -21676,7 +21919,8 @@ Uso: /provider <alias> para cambiar en esta sesi\xF3n.`
21676
21919
  /* @__PURE__ */ jsxs27(Text27, { color: theme.textMuted, bold: false, children: [
21677
21920
  " \xB7 ",
21678
21921
  agent.providerName,
21679
- " \xB7 solo esta sesi\xF3n"
21922
+ " \xB7 ",
21923
+ agent.model ? "solo esta sesi\xF3n" : "modelo por defecto"
21680
21924
  ] })
21681
21925
  ] }),
21682
21926
  /* @__PURE__ */ jsx30(Text27, { children: " " }),
@@ -21721,7 +21965,8 @@ Uso: /provider <alias> para cambiar en esta sesi\xF3n.`
21721
21965
  /* @__PURE__ */ jsxs27(Text27, { color: theme.textMuted, bold: false, children: [
21722
21966
  " \xB7 ",
21723
21967
  agent.providerName,
21724
- " \xB7 solo esta sesi\xF3n"
21968
+ " \xB7 ",
21969
+ agent.model ? "solo esta sesi\xF3n" : "modelo por defecto"
21725
21970
  ] })
21726
21971
  ] }),
21727
21972
  overlay.note && /* @__PURE__ */ jsxs27(Text27, { color: theme.warning, children: [
@@ -22047,7 +22292,7 @@ async function settle(step, fn) {
22047
22292
  }
22048
22293
  }
22049
22294
  function resolveVersion() {
22050
- if (true) return "0.7.0-beta.0";
22295
+ if (true) return "0.7.0";
22051
22296
  try {
22052
22297
  const thisDir = dirname13(fileURLToPath2(import.meta.url));
22053
22298
  const pkg = JSON.parse(
@@ -22065,6 +22310,7 @@ var init_chat = __esm({
22065
22310
  init_esm_shims();
22066
22311
  init_loader();
22067
22312
  init_router();
22313
+ init_startup_model();
22068
22314
  init_registry();
22069
22315
  init_tools();
22070
22316
  init_manager();
@@ -22084,7 +22330,7 @@ var init_chat = __esm({
22084
22330
  init_session_flags();
22085
22331
  init_logging();
22086
22332
  CHECKPOINT_EVERY_MS = 3e4;
22087
- chatCommand = new Command("chat").description("Start an interactive REPL session with the agent").option("--provider <name>", "use a specific provider from config").option("--resume <session-id>", "resume a previous session").option("--read-only", "observation only: no file writes and only read-only commands").option("--profile <name>", "session profile: auto | code | infra | full | <custom>").option("--infra", "shortcut for --profile infra").option("--code", "shortcut for --profile code").option("--log-level <level>", "log level: trace|debug|info|warn|error|silent").option("--debug", "enable verbose debug logging (level debug + file sink)").action(
22333
+ chatCommand = new Command("chat").description("Start an interactive REPL session with the agent").option("--provider <name>", "use a specific provider from config").option("--model <id>", "start with a specific model of the provider (this session only)").option("--resume <session-id>", "resume a previous session").option("--read-only", "observation only: no file writes and only read-only commands").option("--profile <name>", "session profile: auto | code | infra | full | <custom>").option("--infra", "shortcut for --profile infra").option("--code", "shortcut for --profile code").option("--log-level <level>", "log level: trace|debug|info|warn|error|silent").option("--debug", "enable verbose debug logging (level debug + file sink)").action(
22088
22334
  async (opts) => {
22089
22335
  const profileFlag = sessionProfileFlag(opts);
22090
22336
  if (!profileFlag.ok) {
@@ -22121,6 +22367,10 @@ var init_chat = __esm({
22121
22367
  `);
22122
22368
  process.exit(1);
22123
22369
  }
22370
+ if (opts.model?.trim()) router.switchModel(opts.model.trim());
22371
+ if (router.model && router.contextWindowIsDiscoverable) {
22372
+ void discoverContextWindow(router.getActiveConfig());
22373
+ }
22124
22374
  const registry = new ToolRegistry();
22125
22375
  registerBuiltinTools(registry, config);
22126
22376
  const mcpManager = new McpManager(config);
@@ -22413,6 +22663,7 @@ init_subagent_store();
22413
22663
  init_store();
22414
22664
  init_loader();
22415
22665
  init_router();
22666
+ init_startup_model();
22416
22667
  init_registry();
22417
22668
  init_tools();
22418
22669
  init_manager();
@@ -22429,7 +22680,7 @@ function summarizeInput(input) {
22429
22680
  const val = String(input[keys[0]] ?? "");
22430
22681
  return val.length > 60 ? val.slice(0, 57) + "..." : val;
22431
22682
  }
22432
- var runCommand = new Command2("run").description("Run a one-shot task with the agent").argument("<task>", "task to execute").option("--provider <name>", "use a specific provider from config").option("--allow-destructive", "approve all destructive operations without prompting").option("--deny-destructive", "block all destructive operations automatically").option("--plan", "plan-and-execute mode: produce a plan, approve, then execute step by step").option("--yes, --approve-plan", "auto-approve the plan in --plan mode (no prompt)").option("--agent <profile>", "use an agent profile (mode primary/all) as the main agent").option(
22683
+ var runCommand = new Command2("run").description("Run a one-shot task with the agent").argument("<task>", "task to execute").option("--provider <name>", "use a specific provider from config").option("--model <id>", "use a specific model of the provider for this run").option("--allow-destructive", "approve all destructive operations without prompting").option("--deny-destructive", "block all destructive operations automatically").option("--plan", "plan-and-execute mode: produce a plan, approve, then execute step by step").option("--yes, --approve-plan", "auto-approve the plan in --plan mode (no prompt)").option("--agent <profile>", "use an agent profile (mode primary/all) as the main agent").option(
22433
22684
  "--delegate <profile>",
22434
22685
  "hand the task straight to a subagent profile (mode subagent/all), without the main agent"
22435
22686
  ).option("--read-only", "observation only: no file writes and only read-only commands").option("--profile <name>", "session profile: auto | code | infra | full | <custom>").option("--infra", "shortcut for --profile infra").option("--code", "shortcut for --profile code").option("--log-level <level>", "log level: trace|debug|info|warn|error|silent").option("--debug", "enable verbose debug logging (level debug + file sink)").action(
@@ -22469,6 +22720,12 @@ var runCommand = new Command2("run").description("Run a one-shot task with the a
22469
22720
  getLogger("cli").debug("run start", { task: task.slice(0, 120) });
22470
22721
  warnInheritedGitRouting();
22471
22722
  warnConfigDeprecations();
22723
+ const startupModel = await resolveStartupModel(config, opts);
22724
+ if (!startupModel.ok) {
22725
+ process.stderr.write(`[fatal] ${startupModel.error}
22726
+ `);
22727
+ process.exit(1);
22728
+ }
22472
22729
  let router;
22473
22730
  try {
22474
22731
  router = new ProviderRouter(config, opts.provider);
@@ -22477,6 +22734,9 @@ var runCommand = new Command2("run").description("Run a one-shot task with the a
22477
22734
  `);
22478
22735
  process.exit(1);
22479
22736
  }
22737
+ if (router.contextWindowIsDiscoverable && !startupModel.fetched) {
22738
+ await discoverContextWindow(router.getActiveConfig(), { timeoutMs: 3e3 });
22739
+ }
22480
22740
  const registry = new ToolRegistry();
22481
22741
  registerBuiltinTools(registry, config);
22482
22742
  const mcpManager = new McpManager(config);
@@ -22997,6 +23257,7 @@ var configCommand = new Command5("config").description("Get or set configuration
22997
23257
  init_esm_shims();
22998
23258
  init_loader();
22999
23259
  init_router();
23260
+ init_startup_model();
23000
23261
  init_registry();
23001
23262
  init_tools();
23002
23263
  init_core();
@@ -23023,7 +23284,7 @@ var DEFAULT_CONFIG = {
23023
23284
  var initCommand = new Command6("init").description("Explore the project and create or update STRATUM.md").argument(
23024
23285
  "[focus]",
23025
23286
  'Optional focus or constraints for the agent (e.g. "focus on the test setup")'
23026
- ).option("--provider <name>", "use a specific provider from config").option("--allow-destructive", "approve all destructive operations without prompting").action(
23287
+ ).option("--provider <name>", "use a specific provider from config").option("--model <id>", "use a specific model of the provider for this run").option("--allow-destructive", "approve all destructive operations without prompting").action(
23027
23288
  async (focus, opts) => {
23028
23289
  const cwd = process.cwd();
23029
23290
  process.stdout.write("\n Stratum \u2014 Inicializando proyecto\n\n");
@@ -23043,6 +23304,13 @@ var initCommand = new Command6("init").description("Explore the project and crea
23043
23304
  );
23044
23305
  process.exit(1);
23045
23306
  }
23307
+ const startupModel = await resolveStartupModel(config, opts);
23308
+ if (!startupModel.ok) {
23309
+ process.stderr.write(`
23310
+ [error] Provider: ${startupModel.error}
23311
+ `);
23312
+ process.exit(1);
23313
+ }
23046
23314
  let router;
23047
23315
  try {
23048
23316
  router = new ProviderRouter(config, opts.provider);
@@ -23174,8 +23442,10 @@ init_esm_shims();
23174
23442
  init_loader();
23175
23443
  init_openai_compatible();
23176
23444
  init_writer();
23445
+ init_utils();
23177
23446
  init_ProviderWizard();
23178
23447
  init_theme();
23448
+ init_wizard_logic();
23179
23449
  import { Command as Command7 } from "commander";
23180
23450
  import React12 from "react";
23181
23451
  import { render as render2, Box as Box28, Text as Text28 } from "ink";
@@ -23199,13 +23469,75 @@ function AddApp({ existingNames, onDone }) {
23199
23469
  }
23200
23470
  );
23201
23471
  }
23202
- var providerAdd = new Command7("add").description("Wizard interactivo para a\xF1adir un provider a .stratumrc.json").action(async () => {
23472
+ function fail(message) {
23473
+ process.stderr.write(`${message}
23474
+ `);
23475
+ process.exit(1);
23476
+ }
23477
+ var errorText = (err) => err instanceof Error ? err.message : String(err);
23478
+ async function addFromFlags(name, flags, existingNames) {
23479
+ if (!name) fail("Falta el alias: stratum provider add <alias> --base-url <url> \u2026");
23480
+ if (!flags.baseUrl) fail("Falta --base-url <url>.");
23481
+ if (flags.apiKey && flags.apiKeyEnv) fail("Usa --api-key o --api-key-env, no los dos.");
23482
+ const aliasError = validateAlias(name, existingNames);
23483
+ if (aliasError) fail(aliasError);
23484
+ const urlError = validateBaseUrl(flags.baseUrl);
23485
+ if (urlError) fail(urlError);
23486
+ let contextWindow;
23487
+ if (flags.contextWindow !== void 0) {
23488
+ contextWindow = Number(flags.contextWindow);
23489
+ if (!Number.isInteger(contextWindow) || contextWindow <= 0) {
23490
+ fail(`--context-window inv\xE1lido: ${flags.contextWindow}`);
23491
+ }
23492
+ }
23493
+ const storedKey = flags.apiKeyEnv ? "${" + flags.apiKeyEnv + "}" : flags.apiKey ?? "";
23494
+ const { key, missing } = resolveApiKey(storedKey);
23495
+ if (missing.length > 0) fail(`Variable de entorno no definida: ${missing.join(", ")}`);
23496
+ let models = [];
23497
+ try {
23498
+ models = await fetchModelInfos(flags.baseUrl, key);
23499
+ process.stdout.write(
23500
+ `${models.length} modelos en ${flags.baseUrl}: ${models.map((m) => m.id).join(", ")}
23501
+ `
23502
+ );
23503
+ } catch (err) {
23504
+ process.stderr.write(`Aviso: no se pudieron listar los modelos (${errorText(err)}).
23505
+ `);
23506
+ }
23507
+ const model = flags.model?.trim();
23508
+ if (model && models.length > 0 && !models.some((m) => m.id === model)) {
23509
+ fail(`El modelo "${model}" no est\xE1 entre los que expone el provider.`);
23510
+ }
23511
+ try {
23512
+ const entry = buildProviderEntry({
23513
+ baseUrl: flags.baseUrl,
23514
+ apiKey: storedKey,
23515
+ model,
23516
+ contextWindow
23517
+ });
23518
+ const { configPath, created } = upsertProvider(name, entry, flags.default === true);
23519
+ process.stdout.write(`${created ? "Creado" : "Actualizado"} ${configPath}
23520
+ `);
23521
+ process.stdout.write(
23522
+ model ? `Provider "${name}" configurado con el modelo ${model}.
23523
+ ` : `Provider "${name}" configurado sin modelo fijo: se elige al arrancar \`stratum chat\`, o con --model en \`stratum run\`.
23524
+ `
23525
+ );
23526
+ } catch (err) {
23527
+ fail(`Error al escribir la config: ${errorText(err)}`);
23528
+ }
23529
+ }
23530
+ var providerAdd = new Command7("add").description("A\xF1ade un provider a .stratumrc.json: wizard interactivo, o directo con --base-url").argument("[name]", "alias del provider (solo con --base-url)").option("--base-url <url>", "URL base de la API (con /v1): alta sin wizard").option("--api-key-env <VAR>", "variable de entorno con la API key (se guarda como ${VAR})").option("--api-key <key>", "API key literal (queda en claro en el archivo)").option("--model <id>", "modelo por defecto; sin \xE9l se elige al arrancar").option("--context-window <n>", "ventana de contexto en tokens").option("--default", "dejarlo como provider activo").action(async (name, flags) => {
23203
23531
  let existingNames = [];
23204
23532
  try {
23205
23533
  const config = loadConfig();
23206
23534
  existingNames = Object.keys(config.provider?.providers ?? {});
23207
23535
  } catch {
23208
23536
  }
23537
+ if (flags.baseUrl || name) {
23538
+ await addFromFlags(name, flags, existingNames);
23539
+ return;
23540
+ }
23209
23541
  let result = null;
23210
23542
  const { unmount, waitUntilExit } = render2(
23211
23543
  React12.createElement(AddApp, {
@@ -23229,7 +23561,7 @@ var providerAdd = new Command7("add").description("Wizard interactivo para a\xF1
23229
23561
  `${created ? "Creado" : "Actualizado"} ${configPath}` + (backupPath ? ` (backup: ${backupPath})` : "") + "\n"
23230
23562
  );
23231
23563
  if (r.makeDefault) {
23232
- process.stdout.write(`Provider activo: ${r.name} (${r.config.model})
23564
+ process.stdout.write(`Provider activo: ${r.name} (${r.config.model ?? ""})
23233
23565
  `);
23234
23566
  }
23235
23567
  } catch (err) {
@@ -23264,7 +23596,8 @@ var providerList = new Command7("list").description("Lista los providers configu
23264
23596
  const col = (s, w) => s.length > w ? s.slice(0, w - 1) + "\u2026" : s.padEnd(w);
23265
23597
  const wName = Math.max(...names.map((n) => n.length + 2), 8);
23266
23598
  const wUrl = Math.max(...names.map((n) => providers[n].baseUrl.length), 10);
23267
- const wModel = Math.max(...names.map((n) => providers[n].model.length), 8);
23599
+ const modelOf = (n) => providers[n].model || "(al arrancar)";
23600
+ const wModel = Math.max(...names.map((n) => modelOf(n).length), 8);
23268
23601
  process.stdout.write(
23269
23602
  chalk3.bold(
23270
23603
  ` ${col("ALIAS", wName)} ${col("TIPO", 18)} ${col("BASE URL", wUrl)} ${col("MODELO", wModel)} ESTADO
@@ -23277,12 +23610,55 @@ var providerList = new Command7("list").description("Lista los providers configu
23277
23610
  const mark = name === defaultName ? chalk3.hex("#F59E0B")("\u25B6 ") : " ";
23278
23611
  const alias = name === defaultName ? chalk3.bold(col(name, wName)) : col(name, wName);
23279
23612
  process.stdout.write(
23280
- `${mark}${alias} ${col(p.type, 18)} ${col(p.baseUrl, wUrl)} ${col(p.model, wModel)} ${dot}
23613
+ `${mark}${alias} ${col(p.type, 18)} ${col(p.baseUrl, wUrl)} ${col(modelOf(name), wModel)} ${dot}
23281
23614
  `
23282
23615
  );
23283
23616
  });
23284
23617
  process.stdout.write(chalk3.dim("\n \u25B6 = provider activo \xB7 \u25CF verde = /models responde\n"));
23285
23618
  });
23619
+ var providerModels = new Command7("models").description("Lista los modelos que expone un provider (GET /models)").argument("[name]", "alias del provider (por defecto, el activo)").option("--set <id>", "fija ese modelo como el por defecto del provider").action(async (name, opts) => {
23620
+ let config;
23621
+ try {
23622
+ config = loadConfig();
23623
+ } catch (err) {
23624
+ fail(`Config error: ${errorText(err)}`);
23625
+ }
23626
+ const alias = name ?? config.provider?.default;
23627
+ const entry = alias ? config.provider?.providers[alias] : void 0;
23628
+ if (!alias || !entry) fail(`Provider "${alias ?? ""}" no existe en la config.`);
23629
+ let models;
23630
+ try {
23631
+ models = await fetchModelInfos(entry.baseUrl, entry.apiKey);
23632
+ } catch (err) {
23633
+ fail(`No se pudieron listar los modelos: ${errorText(err)}`);
23634
+ }
23635
+ if (opts.set) {
23636
+ if (models.length > 0 && !models.some((m) => m.id === opts.set)) {
23637
+ fail(`El modelo "${opts.set}" no est\xE1 entre los que expone "${alias}".`);
23638
+ }
23639
+ let path2;
23640
+ try {
23641
+ path2 = setProviderModel(alias, opts.set);
23642
+ } catch (err) {
23643
+ fail(`Error al escribir la config: ${errorText(err)}`);
23644
+ }
23645
+ if (!path2) fail(`"${alias}" no est\xE1 definido en ning\xFAn .stratumrc.json escribible.`);
23646
+ process.stdout.write(`Modelo por defecto de "${alias}": ${opts.set} (${path2})
23647
+ `);
23648
+ return;
23649
+ }
23650
+ for (const m of models) {
23651
+ const mark = m.id === entry.model ? chalk3.hex("#F59E0B")("\u25B6 ") : " ";
23652
+ const ctx = entry.models?.[m.id]?.contextWindow ?? m.contextWindow;
23653
+ process.stdout.write(`${mark}${m.id}${ctx ? chalk3.dim(` ${ctx} tokens`) : ""}
23654
+ `);
23655
+ }
23656
+ process.stdout.write(
23657
+ chalk3.dim(`
23658
+ ${models.length} modelos en ${entry.baseUrl} \xB7 \u25B6 = modelo por defecto
23659
+ `)
23660
+ );
23661
+ });
23286
23662
  var providerUse = new Command7("use").description("Cambia el provider activo (provider.default)").argument("<name>", "alias del provider").action((name) => {
23287
23663
  try {
23288
23664
  const { configPath, backupPath } = setDefaultProvider(name);
@@ -23295,16 +23671,17 @@ var providerUse = new Command7("use").description("Cambia el provider activo (pr
23295
23671
  process.exit(1);
23296
23672
  }
23297
23673
  });
23298
- var providerRemove = new Command7("remove").description("Elimina un provider de la config").argument("<name>", "alias del provider").action((name) => {
23674
+ var providerRemove = new Command7("remove").alias("rm").description("Elimina un provider (con su modelo y sus ajustes por modelo) de la config").argument("<name>", "alias del provider").action((name) => {
23299
23675
  try {
23300
- const { configPath, backupPath, newDefault } = removeProvider(name);
23301
- process.stdout.write(`Eliminado "${name}" de ${configPath}
23676
+ for (const { configPath, backupPath, newDefault } of removeProviderEverywhere(name)) {
23677
+ process.stdout.write(`Eliminado "${name}" de ${configPath}
23302
23678
  `);
23303
- if (backupPath) process.stdout.write(`Backup: ${backupPath}
23679
+ if (backupPath) process.stdout.write(`Backup: ${backupPath}
23304
23680
  `);
23305
- if (newDefault) {
23306
- process.stdout.write(`El provider activo era "${name}" \u2192 nuevo activo: ${newDefault}
23681
+ if (newDefault) {
23682
+ process.stdout.write(`El provider activo era "${name}" \u2192 nuevo activo: ${newDefault}
23307
23683
  `);
23684
+ }
23308
23685
  }
23309
23686
  } catch (err) {
23310
23687
  process.stderr.write(`${err.message}
@@ -23312,7 +23689,7 @@ var providerRemove = new Command7("remove").description("Elimina un provider de
23312
23689
  process.exit(1);
23313
23690
  }
23314
23691
  });
23315
- var providerCommand = new Command7("provider").alias("providers").description("Gesti\xF3n de providers LLM (add/list/use/remove) sin editar .stratumrc.json a mano").addCommand(providerAdd).addCommand(providerList).addCommand(providerUse).addCommand(providerRemove);
23692
+ var providerCommand = new Command7("provider").alias("providers").description("Gesti\xF3n de providers LLM (add/list/use/remove) sin editar .stratumrc.json a mano").addCommand(providerAdd).addCommand(providerList).addCommand(providerModels).addCommand(providerUse).addCommand(providerRemove);
23316
23693
 
23317
23694
  // src/cli/commands/mcp.ts
23318
23695
  init_esm_shims();
@@ -23693,7 +24070,7 @@ var agentsCommand = new Command11("agents").description("Inspect agent profiles
23693
24070
  init_loader();
23694
24071
  init_schema_version();
23695
24072
  function resolveVersion2() {
23696
- if (true) return "0.7.0-beta.0";
24073
+ if (true) return "0.7.0";
23697
24074
  const thisDir = dirname14(fileURLToPath3(import.meta.url));
23698
24075
  const pkg = JSON.parse(readFileSync20(join26(thisDir, "..", "..", "package.json"), "utf-8"));
23699
24076
  return pkg.version;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stratum-cli",
3
- "version": "0.7.0-beta.0",
3
+ "version": "0.7.0",
4
4
  "description": "Agente CLI extensible sobre un loop ReAct — provider-agnostic (Ollama, llama.cpp, vLLM, LiteLLM, OpenAI), con plan-and-execute, subagentes, MCP y SSH nativo",
5
5
  "keywords": [
6
6
  "cli",