@kimbho/kimbho-cli 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -3346,7 +3346,7 @@ var {
3346
3346
  // package.json
3347
3347
  var package_default = {
3348
3348
  name: "@kimbho/kimbho-cli",
3349
- version: "0.1.2",
3349
+ version: "0.1.4",
3350
3350
  description: "Kimbho CLI is a terminal-native coding agent for planning, execution, and verification.",
3351
3351
  type: "module",
3352
3352
  engines: {
@@ -8072,6 +8072,34 @@ function assignBrain(config, role, settings) {
8072
8072
  }
8073
8073
  });
8074
8074
  }
8075
+ function buildRetainedBrainSettings(current, providerId, model) {
8076
+ return {
8077
+ providerId,
8078
+ ...model ? {
8079
+ model
8080
+ } : {},
8081
+ ...typeof current.temperature === "number" ? {
8082
+ temperature: current.temperature
8083
+ } : {},
8084
+ ...typeof current.maxTokens === "number" ? {
8085
+ maxTokens: current.maxTokens
8086
+ } : {},
8087
+ ...current.promptPreamble ? {
8088
+ promptPreamble: current.promptPreamble
8089
+ } : {}
8090
+ };
8091
+ }
8092
+ function assignProviderToAllBrains(config, providerId, model) {
8093
+ return KimbhoConfigSchema.parse({
8094
+ ...config,
8095
+ brains: {
8096
+ planner: buildRetainedBrainSettings(config.brains.planner, providerId, model),
8097
+ coder: buildRetainedBrainSettings(config.brains.coder, providerId, model),
8098
+ reviewer: buildRetainedBrainSettings(config.brains.reviewer, providerId, model),
8099
+ fast: buildRetainedBrainSettings(config.brains.fast, providerId, model)
8100
+ }
8101
+ });
8102
+ }
8075
8103
  function resolveBrainSettings(config, role) {
8076
8104
  return config.brains[role];
8077
8105
  }
@@ -8826,6 +8854,28 @@ function listProviderTemplates() {
8826
8854
  function findProviderTemplate(templateId) {
8827
8855
  return BUILTIN_PROVIDER_TEMPLATES.find((template) => template.id === templateId);
8828
8856
  }
8857
+ function trimTrailingSlash(value) {
8858
+ return value.replace(/\/+$/, "");
8859
+ }
8860
+ function normalizeTemplateBaseUrl(templateId, baseUrl) {
8861
+ if (!baseUrl) {
8862
+ return void 0;
8863
+ }
8864
+ const normalized = trimTrailingSlash(baseUrl);
8865
+ if (templateId !== "lmstudio") {
8866
+ return normalized;
8867
+ }
8868
+ try {
8869
+ const parsed = new URL(normalized);
8870
+ if (!parsed.pathname || parsed.pathname === "/") {
8871
+ parsed.pathname = "/v1";
8872
+ return trimTrailingSlash(parsed.toString());
8873
+ }
8874
+ } catch {
8875
+ return normalized;
8876
+ }
8877
+ return normalized;
8878
+ }
8829
8879
  function buildProviderFromTemplate(templateId, options = {}) {
8830
8880
  const template = findProviderTemplate(templateId);
8831
8881
  if (!template) {
@@ -8835,8 +8885,8 @@ function buildProviderFromTemplate(templateId, options = {}) {
8835
8885
  id: options.providerId ?? template.id,
8836
8886
  label: options.label ?? template.label,
8837
8887
  driver: template.driver,
8838
- ...options.baseUrl ?? template.defaultBaseUrl ? {
8839
- baseUrl: options.baseUrl ?? template.defaultBaseUrl
8888
+ ...normalizeTemplateBaseUrl(templateId, options.baseUrl) ?? template.defaultBaseUrl ? {
8889
+ baseUrl: normalizeTemplateBaseUrl(templateId, options.baseUrl) ?? template.defaultBaseUrl
8840
8890
  } : {},
8841
8891
  ...options.apiKeyEnv ?? template.defaultApiKeyEnv ? {
8842
8892
  apiKeyEnv: options.apiKeyEnv ?? template.defaultApiKeyEnv
@@ -8875,11 +8925,11 @@ async function requestJson(url, init, timeoutMs = 3e4) {
8875
8925
  }
8876
8926
  return response.json();
8877
8927
  }
8878
- function trimTrailingSlash(value) {
8928
+ function trimTrailingSlash2(value) {
8879
8929
  return value.replace(/\/+$/, "");
8880
8930
  }
8881
8931
  function joinUrl(baseUrl, suffix) {
8882
- const normalizedBase = trimTrailingSlash(baseUrl);
8932
+ const normalizedBase = trimTrailingSlash2(baseUrl);
8883
8933
  const normalizedSuffix = suffix.startsWith("/") ? suffix : `/${suffix}`;
8884
8934
  return `${normalizedBase}${normalizedSuffix}`;
8885
8935
  }
@@ -8908,7 +8958,7 @@ function buildProviderHeaders(definition, apiKey, includeJsonContentType = true)
8908
8958
  }
8909
8959
  function filterModels(models, input = {}) {
8910
8960
  const normalized = input.search?.trim().toLowerCase();
8911
- const filtered = normalized ? models.filter((model) => [model.id, model.name, model.description].filter((value) => Boolean(value)).some((value) => value.toLowerCase().includes(normalized))) : models;
8961
+ const filtered = normalized ? models.filter((model) => [model.id, model.name].filter((value) => Boolean(value)).some((value) => value.toLowerCase().includes(normalized))) : models;
8912
8962
  return typeof input.limit === "number" ? filtered.slice(0, input.limit) : filtered;
8913
8963
  }
8914
8964
  function mapOpenAIStyleModels(providerId, payload, input) {
@@ -9187,9 +9237,26 @@ var OpenAICompatibleProvider = class {
9187
9237
  };
9188
9238
  }
9189
9239
  const apiKey = resolveApiKey(this.definition);
9240
+ if (this.definition.apiKeyEnv && !apiKey) {
9241
+ return {
9242
+ ok: false,
9243
+ message: `${this.definition.baseUrl} (${this.definition.defaultModel ?? "model not set"}), missing ${this.definition.apiKeyEnv}`
9244
+ };
9245
+ }
9246
+ try {
9247
+ await requestJson(joinUrl(this.definition.baseUrl, "/models"), {
9248
+ method: "GET",
9249
+ headers: buildProviderHeaders(this.definition, apiKey, false)
9250
+ }, 2e3);
9251
+ } catch (error) {
9252
+ return {
9253
+ ok: false,
9254
+ message: error instanceof Error ? error.message : String(error)
9255
+ };
9256
+ }
9190
9257
  return {
9191
- ok: !this.definition.apiKeyEnv || Boolean(apiKey),
9192
- message: `${this.definition.baseUrl} (${this.definition.defaultModel ?? "model not set"})${this.definition.apiKeyEnv && !apiKey ? `, missing ${this.definition.apiKeyEnv}` : ""}`
9258
+ ok: true,
9259
+ message: `${this.definition.baseUrl} (${this.definition.defaultModel ?? "model not set"})`
9193
9260
  };
9194
9261
  }
9195
9262
  async listModels(input) {
@@ -9745,11 +9812,19 @@ function renderModelLine(model) {
9745
9812
  ].filter((value) => Boolean(value));
9746
9813
  return details.length > 0 ? `${model.id} | ${details.join(" | ")}` : model.id;
9747
9814
  }
9815
+ function filterCachedModels(models, options) {
9816
+ const normalizedSearch = options.search?.trim().toLowerCase();
9817
+ const filtered = normalizedSearch ? models.filter((model) => model.id.toLowerCase().includes(normalizedSearch)) : models;
9818
+ return typeof options.limit === "number" ? filtered.slice(0, options.limit) : filtered;
9819
+ }
9748
9820
  async function fetchProviderModels(provider, options) {
9749
- const cachedModels = provider.models.map((modelId) => ({
9750
- id: modelId,
9751
- providerId: provider.id
9752
- }));
9821
+ const cachedModels = filterCachedModels(
9822
+ provider.models.map((modelId) => ({
9823
+ id: modelId,
9824
+ providerId: provider.id
9825
+ })),
9826
+ options
9827
+ );
9753
9828
  if (options.cached) {
9754
9829
  return {
9755
9830
  source: "cache",
@@ -9852,7 +9927,7 @@ function createModelsCommand() {
9852
9927
  console.log(`Updated ${outputPath}`);
9853
9928
  console.log(`Synced ${models.length} models for ${provider.id}`);
9854
9929
  });
9855
- command.command("use").description("Select a model for a provider and optionally assign it to a brain role.").argument("<model>", "Model id to select").requiredOption("--provider <provider>", "Provider id to use").option("--role <role>", "Brain role to assign: planner, coder, reviewer, or fast").option("--set-default", "Also set this model as the provider default", false).option("--force", "Skip remote model validation", false).option("--temperature <value>", "Temperature override for the role", parseNumber2).option("--max-tokens <value>", "Max token override for the role", parseInteger).action(async (model, options) => {
9930
+ command.command("use").description("Select a model for a provider and assign it globally unless a specific role is requested.").argument("<model>", "Model id to select").requiredOption("--provider <provider>", "Provider id to use").option("--role <role>", "Brain role to assign: planner, coder, reviewer, or fast").option("--set-default", "Also set this model as the provider default", false).option("--force", "Skip remote model validation", false).option("--temperature <value>", "Temperature override for the role", parseNumber2).option("--max-tokens <value>", "Max token override for the role", parseInteger).action(async (model, options) => {
9856
9931
  let config = await loadConfig(import_node_process5.default.cwd());
9857
9932
  if (!config) {
9858
9933
  requireConfigMessage2();
@@ -9902,12 +9977,16 @@ function createModelsCommand() {
9902
9977
  maxTokens: options.maxTokens
9903
9978
  } : {}
9904
9979
  });
9980
+ } else {
9981
+ config = assignProviderToAllBrains(config, provider.id, model);
9905
9982
  }
9906
9983
  const outputPath = await saveConfig(config, import_node_process5.default.cwd());
9907
9984
  console.log(`Updated ${outputPath}`);
9908
9985
  console.log(`Selected ${provider.id}/${model}`);
9909
9986
  if (options.role) {
9910
9987
  console.log(`Assigned role ${options.role}`);
9988
+ } else {
9989
+ console.log("Assigned all roles");
9911
9990
  }
9912
9991
  if (shouldSetDefault) {
9913
9992
  console.log(`Set provider default model to ${model}`);
@@ -10559,6 +10638,7 @@ var DIM = "\x1B[2m";
10559
10638
  var RESET = "\x1B[0m";
10560
10639
  var TOP_LEVEL_COMMANDS = /* @__PURE__ */ new Set([
10561
10640
  "agents",
10641
+ "brain",
10562
10642
  "brains",
10563
10643
  "doctor",
10564
10644
  "fix",
@@ -10568,15 +10648,30 @@ var TOP_LEVEL_COMMANDS = /* @__PURE__ */ new Set([
10568
10648
  "models",
10569
10649
  "new",
10570
10650
  "plan",
10651
+ "provider",
10571
10652
  "providers",
10572
10653
  "quit",
10573
10654
  "resume",
10574
10655
  "review",
10575
10656
  "run",
10576
10657
  "scaffold",
10658
+ "select",
10577
10659
  "shell",
10578
- "status"
10660
+ "status",
10661
+ "use-model"
10579
10662
  ]);
10663
+ var MODEL_SUBCOMMANDS = /* @__PURE__ */ new Set([
10664
+ "help",
10665
+ "list",
10666
+ "sync",
10667
+ "use"
10668
+ ]);
10669
+ var BRAIN_ROLES = [
10670
+ "planner",
10671
+ "coder",
10672
+ "reviewer",
10673
+ "fast"
10674
+ ];
10580
10675
  function color(code, value) {
10581
10676
  return `${code}${value}${RESET}`;
10582
10677
  }
@@ -10589,12 +10684,14 @@ function renderBanner() {
10589
10684
  "|_|\\_\\_|_| |_|____/|____/|_| |_|\\___/ "
10590
10685
  ].map((line) => color(AMBER, line)).join("\n");
10591
10686
  }
10592
- async function getShellSessionState(cwd) {
10687
+ async function getShellSessionState(cwd, focusRole) {
10593
10688
  const config = await loadConfig(cwd);
10594
10689
  if (!config) {
10595
10690
  return {
10691
+ focusRole,
10596
10692
  providerLabel: "unconfigured",
10597
10693
  providerId: "-",
10694
+ focusModel: "not set",
10598
10695
  coderModel: "not set",
10599
10696
  plannerModel: "not set",
10600
10697
  approvalMode: "manual",
@@ -10603,11 +10700,13 @@ async function getShellSessionState(cwd) {
10603
10700
  configured: false
10604
10701
  };
10605
10702
  }
10606
- const coderSettings = config.brains.coder;
10607
- const provider = findProviderById(config, coderSettings.providerId);
10703
+ const focusSettings = config.brains[focusRole];
10704
+ const provider = findProviderById(config, focusSettings.providerId);
10608
10705
  return {
10706
+ focusRole,
10609
10707
  providerLabel: provider?.label ?? provider?.id ?? "unknown",
10610
- providerId: provider?.id ?? coderSettings.providerId,
10708
+ providerId: provider?.id ?? focusSettings.providerId,
10709
+ focusModel: resolveBrainModel(config, focusRole) ?? "not set",
10611
10710
  coderModel: resolveBrainModel(config, "coder") ?? "not set",
10612
10711
  plannerModel: resolveBrainModel(config, "planner") ?? "not set",
10613
10712
  approvalMode: config.approvalMode,
@@ -10640,20 +10739,25 @@ function renderBox(lines) {
10640
10739
  function renderHelp() {
10641
10740
  return [
10642
10741
  `${color(DIM, "Commands")}`,
10643
- "/status Show the active model, provider, and workspace state.",
10644
- "/plan <goal> Create a structured implementation plan.",
10645
- "/run <goal> Start a Kimbho execution session for a goal.",
10646
- "/new <goal> Alias for /run <goal>.",
10647
- "/scaffold <goal> Alias for /run <goal> with scaffolding intent.",
10648
- "/resume Show the latest saved session.",
10649
- "/agents Inspect agent roles and the active session.",
10650
- "/providers Manage model providers.",
10651
- "/model Show the active planner/coder model selection.",
10652
- "/models Discover and select provider models.",
10653
- "/brains Inspect or assign planner/coder/reviewer brains.",
10654
- "/doctor Check local environment and config.",
10655
- "/clear Redraw the shell.",
10656
- "/quit, /exit Leave the shell.",
10742
+ "/status Show the active role, provider, and workspace state.",
10743
+ "/brain <role> Change shell focus to planner, coder, reviewer, or fast.",
10744
+ "/model Show current brain assignments.",
10745
+ "/providers List configured providers.",
10746
+ "/providers templates Show built-in provider templates.",
10747
+ "/providers add <tpl> [id] [--base-url <url>] [--model <id>] [--api-key-env <env>]",
10748
+ " Add a provider from a built-in template.",
10749
+ "/providers use <id> Use a provider for all agent roles.",
10750
+ "/providers check Run provider health checks.",
10751
+ "/models [search] Discover models for the active role's provider.",
10752
+ "/select <n> Select a model from the last numbered /models list.",
10753
+ "/use-model <id> Assign a model to all agent roles.",
10754
+ "/plan <goal> Create a structured implementation plan.",
10755
+ "/run <goal> Start a Kimbho execution session for a goal.",
10756
+ "/resume Show the latest saved session.",
10757
+ "/agents Inspect agent roles and the active session.",
10758
+ "/doctor Check local environment and config.",
10759
+ "/clear Redraw the shell.",
10760
+ "/quit, /exit Leave the shell.",
10657
10761
  "",
10658
10762
  `${color(DIM, "Tip")}`,
10659
10763
  "Type a natural-language goal directly and Kimbho will treat it as /run <goal>."
@@ -10662,6 +10766,8 @@ function renderHelp() {
10662
10766
  function renderStartupCard(cwd, state) {
10663
10767
  const cardLines = [
10664
10768
  `${color(BOLD, "Kimbho CLI")} (v${KIMBHO_VERSION})`,
10769
+ renderCardLine("focus", state.focusRole),
10770
+ renderCardLine("model", state.focusModel),
10665
10771
  renderCardLine("coder", state.coderModel),
10666
10772
  renderCardLine("planner", state.plannerModel),
10667
10773
  renderCardLine("provider", `${state.providerLabel} [${state.providerId}]`),
@@ -10669,23 +10775,23 @@ function renderStartupCard(cwd, state) {
10669
10775
  renderCardLine("approval", state.approvalMode),
10670
10776
  renderCardLine("sandbox", state.sandboxMode),
10671
10777
  renderCardLine("preset", state.stackPreset),
10672
- renderCardLine("shortcuts", "/help /status /plan /run /models /brains /quit")
10778
+ renderCardLine("shortcuts", "/brain /providers /models /select /use-model /quit")
10673
10779
  ];
10674
10780
  if (!state.configured) {
10675
- cardLines.push("setup: run /init to create .kimbho/config.json");
10781
+ cardLines.push("setup: run /init or /providers add <template> to create .kimbho/config.json");
10676
10782
  }
10677
10783
  return renderBox(cardLines);
10678
10784
  }
10679
10785
  function formatPrompt(state) {
10680
- const coderModel = state.coderModel === "not set" ? "unconfigured" : shortenMiddle(state.coderModel, 18);
10681
- return `${color(AMBER, "kimbho")} ${color(DIM, `[${coderModel}]`)} > `;
10786
+ const model = state.focusModel === "not set" ? "unconfigured" : shortenMiddle(state.focusModel, 18);
10787
+ return `${color(AMBER, "kimbho")} ${color(DIM, `[${state.focusRole}:${model}]`)} > `;
10682
10788
  }
10683
10789
  function printHeader(cwd, state) {
10684
10790
  console.log(renderBanner());
10685
10791
  console.log(color(DIM, "Terminal-native coding agent"));
10686
10792
  console.log(renderStartupCard(cwd, state));
10687
10793
  console.log("");
10688
- console.log(color(DIM, "Tip: describe the app or repo change you want, and Kimbho will route it into /run."));
10794
+ console.log(color(DIM, "Tip: configure providers and models here, then describe the work and Kimbho will route it into /run."));
10689
10795
  console.log(renderHelp());
10690
10796
  console.log("");
10691
10797
  }
@@ -10736,21 +10842,426 @@ function tokenizeInput(input) {
10736
10842
  }
10737
10843
  return tokens;
10738
10844
  }
10739
- function toCommandTokens(input) {
10845
+ function normalizeInputTokens(input) {
10740
10846
  const trimmed = input.trim();
10741
10847
  if (!trimmed) {
10742
- return [];
10848
+ return {
10849
+ normalizedInput: "",
10850
+ tokens: [],
10851
+ head: null
10852
+ };
10743
10853
  }
10744
10854
  const normalizedInput = trimmed.startsWith("kimbho ") ? trimmed.slice("kimbho ".length).trim() : trimmed;
10745
10855
  const tokens = tokenizeInput(normalizedInput);
10746
- if (tokens.length === 0) {
10856
+ const firstToken = tokens[0];
10857
+ if (!firstToken) {
10858
+ return {
10859
+ normalizedInput,
10860
+ tokens,
10861
+ head: null
10862
+ };
10863
+ }
10864
+ return {
10865
+ normalizedInput,
10866
+ tokens,
10867
+ head: firstToken.startsWith("/") ? firstToken.slice(1) : firstToken
10868
+ };
10869
+ }
10870
+ function defaultProviderIdForTemplate(templateId) {
10871
+ switch (templateId) {
10872
+ case "openai":
10873
+ return "openai-main";
10874
+ case "anthropic":
10875
+ return "anthropic-main";
10876
+ case "openrouter":
10877
+ return "openrouter-main";
10878
+ case "ollama":
10879
+ return "ollama-local";
10880
+ case "lmstudio":
10881
+ return "lmstudio-local";
10882
+ default:
10883
+ return `${templateId}-main`;
10884
+ }
10885
+ }
10886
+ function parseProviderAddOptions(tokens) {
10887
+ const templateId = tokens[2];
10888
+ if (!templateId) {
10889
+ throw new Error("Usage: /providers add <template> [provider-id] [--base-url <url>] [--model <id>] [--api-key-env <env>]");
10890
+ }
10891
+ let index = 3;
10892
+ let providerId;
10893
+ if (tokens[index] && !tokens[index]?.startsWith("-")) {
10894
+ providerId = tokens[index];
10895
+ index += 1;
10896
+ }
10897
+ const options = {
10898
+ templateId,
10899
+ ...providerId ? {
10900
+ providerId
10901
+ } : {}
10902
+ };
10903
+ while (index < tokens.length) {
10904
+ const flag = tokens[index];
10905
+ if (!flag?.startsWith("--")) {
10906
+ throw new Error(`Unexpected token "${flag}".`);
10907
+ }
10908
+ const value = tokens[index + 1];
10909
+ if (!value || value.startsWith("--")) {
10910
+ throw new Error(`Missing value for ${flag}.`);
10911
+ }
10912
+ switch (flag) {
10913
+ case "--label":
10914
+ options.label = value;
10915
+ break;
10916
+ case "--base-url":
10917
+ options.baseUrl = normalizeTemplateBaseUrl(templateId, value) ?? value;
10918
+ break;
10919
+ case "--api-key-env":
10920
+ options.apiKeyEnv = value;
10921
+ break;
10922
+ case "--model":
10923
+ options.model = value;
10924
+ break;
10925
+ default:
10926
+ throw new Error(`Unknown option "${flag}". Supported: --label, --base-url, --api-key-env, --model.`);
10927
+ }
10928
+ index += 2;
10929
+ }
10930
+ return options;
10931
+ }
10932
+ function buildCachedModels(provider, search, limit = 25) {
10933
+ const normalized = search?.trim().toLowerCase();
10934
+ const filtered = normalized ? provider.models.filter((modelId) => modelId.toLowerCase().includes(normalized)) : provider.models;
10935
+ return filtered.slice(0, limit).map((modelId) => ({
10936
+ id: modelId,
10937
+ providerId: provider.id
10938
+ }));
10939
+ }
10940
+ async function fetchModelsForProvider(cwd, config, provider, search, limit = 25) {
10941
+ const registry = createDefaultBrainProviderRegistry(cwd);
10942
+ const cachedModels = buildCachedModels(provider, search, limit);
10943
+ try {
10944
+ const models = await registry.listModels(provider, {
10945
+ ...search ? {
10946
+ search
10947
+ } : {},
10948
+ limit
10949
+ });
10950
+ const nextConfig = upsertProvider(config, {
10951
+ ...provider,
10952
+ models: Array.from(/* @__PURE__ */ new Set([
10953
+ ...provider.models,
10954
+ ...models.map((model) => model.id)
10955
+ ]))
10956
+ });
10957
+ if (nextConfig !== config) {
10958
+ await saveConfig(nextConfig, cwd);
10959
+ }
10960
+ return {
10961
+ config: nextConfig,
10962
+ source: "remote",
10963
+ models
10964
+ };
10965
+ } catch (error) {
10966
+ if (cachedModels.length > 0) {
10967
+ const message = error instanceof Error ? error.message : String(error);
10968
+ return {
10969
+ config,
10970
+ source: "cache",
10971
+ note: `Using cached models because remote discovery failed: ${message}`,
10972
+ models: cachedModels
10973
+ };
10974
+ }
10975
+ throw error;
10976
+ }
10977
+ }
10978
+ async function assignModelGlobally(cwd, providerId, model) {
10979
+ const config = await loadConfig(cwd);
10980
+ if (!config) {
10981
+ throw new Error("No config found. Run /init or /providers add first.");
10982
+ }
10983
+ const provider = findProviderById(config, providerId);
10984
+ if (!provider) {
10985
+ throw new Error(`Unknown provider "${providerId}".`);
10986
+ }
10987
+ const nextConfig = assignProviderToAllBrains(
10988
+ upsertProvider(config, {
10989
+ ...provider,
10990
+ defaultModel: model,
10991
+ models: Array.from(/* @__PURE__ */ new Set([
10992
+ ...provider.models,
10993
+ model
10994
+ ]))
10995
+ }),
10996
+ providerId,
10997
+ model
10998
+ );
10999
+ const outputPath = await saveConfig(nextConfig, cwd);
11000
+ return outputPath;
11001
+ }
11002
+ async function useProviderGlobally(cwd, providerId) {
11003
+ const config = await loadConfig(cwd);
11004
+ if (!config) {
11005
+ throw new Error("No config found. Run /init or /providers add first.");
11006
+ }
11007
+ const provider = findProviderById(config, providerId);
11008
+ if (!provider) {
11009
+ throw new Error(`Unknown provider "${providerId}".`);
11010
+ }
11011
+ const resolvedModel = provider.defaultModel ?? provider.models[0] ?? null;
11012
+ const nextConfig = assignProviderToAllBrains(config, provider.id, resolvedModel);
11013
+ const outputPath = await saveConfig(nextConfig, cwd);
11014
+ return {
11015
+ outputPath,
11016
+ model: resolvedModel
11017
+ };
11018
+ }
11019
+ function renderModelLine2(model) {
11020
+ const details = [
11021
+ model.name,
11022
+ model.contextLength ? `ctx ${model.contextLength}` : null,
11023
+ model.promptPrice ? `in ${model.promptPrice}` : null,
11024
+ model.completionPrice ? `out ${model.completionPrice}` : null,
11025
+ model.modality
11026
+ ].filter((value) => Boolean(value));
11027
+ return details.length > 0 ? `${model.id} | ${details.join(" | ")}` : model.id;
11028
+ }
11029
+ async function printProviderList(cwd, focusRole) {
11030
+ const config = await loadConfig(cwd);
11031
+ if (!config) {
11032
+ console.log("No config found. Run /init or /providers add <template> first.");
11033
+ return;
11034
+ }
11035
+ const activeProviderId = config.brains[focusRole].providerId;
11036
+ for (const provider of config.providers) {
11037
+ const marker = provider.id === activeProviderId ? "*" : " ";
11038
+ const envState = provider.apiKeyEnv ? import_node_process10.default.env[provider.apiKeyEnv] ? "present" : `missing ${provider.apiKeyEnv}` : "no key required";
11039
+ console.log(`${marker} ${provider.id}`);
11040
+ console.log(` label: ${provider.label ?? "-"}`);
11041
+ console.log(` driver: ${provider.driver}`);
11042
+ console.log(` model: ${provider.defaultModel ?? "-"}`);
11043
+ console.log(` baseUrl: ${provider.baseUrl ?? "-"}`);
11044
+ console.log(` auth: ${envState}`);
11045
+ console.log(` cachedModels: ${provider.models.length}`);
11046
+ }
11047
+ }
11048
+ function printProviderTemplates() {
11049
+ for (const template of listProviderTemplates()) {
11050
+ console.log(`${template.id}`);
11051
+ console.log(` label: ${template.label}`);
11052
+ console.log(` driver: ${template.driver}`);
11053
+ console.log(` apiKeyEnv: ${template.defaultApiKeyEnv ?? "-"}`);
11054
+ console.log(` model: ${template.defaultModel ?? "-"}`);
11055
+ console.log(` notes: ${template.notes}`);
11056
+ }
11057
+ }
11058
+ async function addProviderFromTemplate(cwd, options) {
11059
+ const provider = buildProviderFromTemplate(options.templateId, {
11060
+ providerId: options.providerId ?? defaultProviderIdForTemplate(options.templateId),
11061
+ ...options.label ? {
11062
+ label: options.label
11063
+ } : {},
11064
+ ...options.baseUrl ? {
11065
+ baseUrl: options.baseUrl
11066
+ } : {},
11067
+ ...options.apiKeyEnv ? {
11068
+ apiKeyEnv: options.apiKeyEnv
11069
+ } : {},
11070
+ ...options.model ? {
11071
+ model: options.model
11072
+ } : {}
11073
+ });
11074
+ const config = await loadConfig(cwd);
11075
+ if (!config) {
11076
+ const outputPath = await saveConfig(createDefaultConfig({ provider }), cwd);
11077
+ return outputPath;
11078
+ }
11079
+ return saveConfig(upsertProvider(config, provider), cwd);
11080
+ }
11081
+ async function printProviderHealth(cwd) {
11082
+ const config = await loadConfig(cwd);
11083
+ if (!config) {
11084
+ console.log("No config found. Run /init or /providers add <template> first.");
11085
+ return;
11086
+ }
11087
+ const registry = createDefaultBrainProviderRegistry(cwd);
11088
+ for (const provider of config.providers) {
11089
+ try {
11090
+ const result = await registry.healthCheck(provider);
11091
+ console.log(`${result.ok ? "PASS" : "FAIL"} ${provider.id}: ${provider.driver} | ${result.message}`);
11092
+ } catch (error) {
11093
+ const message = error instanceof Error ? error.message : String(error);
11094
+ console.log(`FAIL ${provider.id}: ${provider.driver} | ${message}`);
11095
+ }
11096
+ }
11097
+ }
11098
+ async function printBrainAssignments(cwd) {
11099
+ const config = await loadConfig(cwd);
11100
+ if (!config) {
11101
+ console.log("No config found. Run /init or /providers add <template> first.");
11102
+ return;
11103
+ }
11104
+ for (const role of BRAIN_ROLES) {
11105
+ const settings = resolveBrainSettings(config, role);
11106
+ console.log(`${role}`);
11107
+ console.log(` provider: ${settings.providerId}`);
11108
+ console.log(` model: ${resolveBrainModel(config, role) ?? "-"}`);
11109
+ console.log(` temperature: ${settings.temperature ?? "-"}`);
11110
+ console.log(` maxTokens: ${settings.maxTokens ?? "-"}`);
11111
+ }
11112
+ }
11113
+ function isBrainRole(value) {
11114
+ return BRAIN_ROLES.includes(value);
11115
+ }
11116
+ async function handleProvidersCommand(cwd, tokens, runtime) {
11117
+ const subcommand = tokens[1];
11118
+ if (!subcommand || subcommand === "list") {
11119
+ await printProviderList(cwd, runtime.focusRole);
11120
+ return;
11121
+ }
11122
+ if (subcommand === "templates") {
11123
+ printProviderTemplates();
11124
+ return;
11125
+ }
11126
+ if (subcommand === "check") {
11127
+ await printProviderHealth(cwd);
11128
+ return;
11129
+ }
11130
+ if (subcommand === "add") {
11131
+ const options = parseProviderAddOptions(tokens);
11132
+ const outputPath = await addProviderFromTemplate(cwd, options);
11133
+ const resolvedProviderId = options.providerId ?? defaultProviderIdForTemplate(options.templateId);
11134
+ console.log(`Updated ${outputPath}`);
11135
+ console.log(`Added provider ${resolvedProviderId} from template ${options.templateId}`);
11136
+ if (options.baseUrl) {
11137
+ console.log(`Base URL ${options.baseUrl}`);
11138
+ }
11139
+ console.log(`Next: /providers use ${resolvedProviderId}`);
11140
+ return;
11141
+ }
11142
+ if (subcommand === "use") {
11143
+ const providerId = tokens[2];
11144
+ if (!providerId) {
11145
+ throw new Error("Usage: /providers use <provider-id>");
11146
+ }
11147
+ const result = await useProviderGlobally(cwd, providerId);
11148
+ runtime.lastModels = null;
11149
+ console.log(`Updated ${result.outputPath}`);
11150
+ console.log(`All roles now use provider ${providerId}${result.model ? ` (${result.model})` : ""}`);
11151
+ return;
11152
+ }
11153
+ throw new Error("Usage: /providers [list|templates|add|use|check]");
11154
+ }
11155
+ async function handleBrainCommand(cwd, tokens, runtime) {
11156
+ const subcommand = tokens[1];
11157
+ if (!subcommand || subcommand === "list") {
11158
+ await printBrainAssignments(cwd);
11159
+ return;
11160
+ }
11161
+ if (subcommand === "use" && tokens[2] && isBrainRole(tokens[2])) {
11162
+ runtime.focusRole = tokens[2];
11163
+ runtime.lastModels = null;
11164
+ console.log(`Shell focus role is now ${runtime.focusRole}.`);
11165
+ return;
11166
+ }
11167
+ if (isBrainRole(subcommand)) {
11168
+ runtime.focusRole = subcommand;
11169
+ runtime.lastModels = null;
11170
+ console.log(`Shell focus role is now ${runtime.focusRole}.`);
11171
+ return;
11172
+ }
11173
+ throw new Error("Usage: /brain [planner|coder|reviewer|fast]");
11174
+ }
11175
+ async function handleModelsCommand(cwd, tokens, runtime) {
11176
+ const config = await loadConfig(cwd);
11177
+ if (!config) {
11178
+ throw new Error("No config found. Run /init or /providers add <template> first.");
11179
+ }
11180
+ const providerId = config.brains[runtime.focusRole].providerId;
11181
+ const provider = findProviderById(config, providerId);
11182
+ if (!provider) {
11183
+ throw new Error(`Active role "${runtime.focusRole}" points to unknown provider "${providerId}".`);
11184
+ }
11185
+ const subcommand = tokens[1];
11186
+ if (subcommand === "use") {
11187
+ const modelId = tokens.slice(2).join(" ").trim();
11188
+ if (!modelId) {
11189
+ throw new Error("Usage: /models use <model-id>");
11190
+ }
11191
+ const outputPath = await assignModelGlobally(cwd, provider.id, modelId);
11192
+ console.log(`Updated ${outputPath}`);
11193
+ console.log(`Selected ${provider.id}/${modelId}`);
11194
+ console.log("Assigned all roles");
11195
+ return;
11196
+ }
11197
+ const search = !subcommand || MODEL_SUBCOMMANDS.has(subcommand) ? void 0 : tokens.slice(1).join(" ");
11198
+ const result = await fetchModelsForProvider(cwd, config, provider, search, 25);
11199
+ runtime.lastModels = {
11200
+ providerId: provider.id,
11201
+ role: runtime.focusRole,
11202
+ source: result.source,
11203
+ ...search ? {
11204
+ search
11205
+ } : {},
11206
+ models: result.models
11207
+ };
11208
+ console.log(`${provider.id} (${result.source})`);
11209
+ if (result.note) {
11210
+ console.log(` note: ${result.note}`);
11211
+ }
11212
+ if (result.models.length === 0) {
11213
+ console.log(" no models found");
11214
+ return;
11215
+ }
11216
+ for (const [index, model] of result.models.entries()) {
11217
+ console.log(` ${index + 1}. ${renderModelLine2(model)}`);
11218
+ }
11219
+ console.log(``);
11220
+ console.log("Use /select <number> or /use-model <model-id> to assign one to all roles.");
11221
+ }
11222
+ async function handleModelSelection(cwd, modelId, runtime) {
11223
+ const config = await loadConfig(cwd);
11224
+ if (!config) {
11225
+ throw new Error("No config found. Run /init or /providers add <template> first.");
11226
+ }
11227
+ const providerId = config.brains[runtime.focusRole].providerId;
11228
+ const provider = findProviderById(config, providerId);
11229
+ if (!provider) {
11230
+ throw new Error(`Active role "${runtime.focusRole}" points to unknown provider "${providerId}".`);
11231
+ }
11232
+ const outputPath = await assignModelGlobally(cwd, provider.id, modelId);
11233
+ console.log(`Updated ${outputPath}`);
11234
+ console.log(`Selected ${provider.id}/${modelId}`);
11235
+ console.log("Assigned all roles");
11236
+ }
11237
+ async function handleSelectCommand(cwd, tokens, runtime) {
11238
+ const rawIndex = tokens[1];
11239
+ if (!rawIndex) {
11240
+ throw new Error("Usage: /select <number>");
11241
+ }
11242
+ if (!runtime.lastModels || runtime.lastModels.models.length === 0) {
11243
+ throw new Error("No model list is active. Run /models first.");
11244
+ }
11245
+ const index = Number.parseInt(rawIndex, 10);
11246
+ if (!Number.isInteger(index) || index <= 0) {
11247
+ throw new Error(`Expected a positive model number, received "${rawIndex}".`);
11248
+ }
11249
+ const model = runtime.lastModels.models[index - 1];
11250
+ if (!model) {
11251
+ throw new Error(`Model number ${index} is out of range.`);
11252
+ }
11253
+ runtime.focusRole = runtime.lastModels.role;
11254
+ await handleModelSelection(cwd, model.id, runtime);
11255
+ }
11256
+ function toExternalCommandTokens(input, state) {
11257
+ const { normalizedInput, tokens, head } = normalizeInputTokens(input);
11258
+ if (!head || tokens.length === 0) {
10747
11259
  return [];
10748
11260
  }
10749
11261
  const firstToken = tokens[0];
10750
11262
  if (!firstToken) {
10751
11263
  return [];
10752
11264
  }
10753
- const head = firstToken.startsWith("/") ? firstToken.slice(1) : firstToken;
10754
11265
  if (head === "new") {
10755
11266
  return [
10756
11267
  "run",
@@ -10764,12 +11275,6 @@ function toCommandTokens(input) {
10764
11275
  `scaffold ${goal}`.trim()
10765
11276
  ];
10766
11277
  }
10767
- if (head === "model") {
10768
- return [
10769
- "brains",
10770
- "list"
10771
- ];
10772
- }
10773
11278
  if (!firstToken.startsWith("/") && !TOP_LEVEL_COMMANDS.has(head) && !head.startsWith("-")) {
10774
11279
  return [
10775
11280
  "run",
@@ -10779,62 +11284,122 @@ function toCommandTokens(input) {
10779
11284
  tokens[0] = head;
10780
11285
  return tokens;
10781
11286
  }
11287
+ async function handleShellCommand(cwd, input, state, runtime, execute) {
11288
+ const trimmed = input.trim();
11289
+ if (!trimmed) {
11290
+ return;
11291
+ }
11292
+ if (trimmed === "/exit" || trimmed === "exit" || trimmed === "quit" || trimmed === "/quit") {
11293
+ throw new Error("__kimbho_exit__");
11294
+ }
11295
+ if (trimmed === "/help" || trimmed === "help" || trimmed === "?") {
11296
+ console.log(renderHelp());
11297
+ return;
11298
+ }
11299
+ if (trimmed === "/status" || trimmed === "status") {
11300
+ console.log(renderStartupCard(cwd, state));
11301
+ return;
11302
+ }
11303
+ if (trimmed === "/clear" || trimmed === "clear") {
11304
+ if (import_node_process10.default.stdout.isTTY) {
11305
+ import_node_process10.default.stdout.write("\x1Bc");
11306
+ }
11307
+ const nextState = await getShellSessionState(cwd, runtime.focusRole);
11308
+ printHeader(cwd, nextState);
11309
+ return;
11310
+ }
11311
+ const { tokens, head } = normalizeInputTokens(trimmed);
11312
+ if (!head) {
11313
+ return;
11314
+ }
11315
+ if (head === "provider" || head === "providers") {
11316
+ const subcommand = tokens[1];
11317
+ const canHandleLocally = !subcommand || subcommand === "list" || subcommand === "templates" || subcommand === "check" || subcommand === "use" || subcommand === "add" && Boolean(tokens[2]) && !tokens[2]?.startsWith("-");
11318
+ if (canHandleLocally) {
11319
+ await handleProvidersCommand(cwd, [
11320
+ "providers",
11321
+ ...tokens.slice(1)
11322
+ ], runtime);
11323
+ return;
11324
+ }
11325
+ }
11326
+ if (head === "brain" || head === "brains") {
11327
+ const subcommand = tokens[1];
11328
+ const roleArg = tokens[2];
11329
+ const canHandleLocally = !subcommand || subcommand === "list" || isBrainRole(subcommand) || subcommand === "use" && (roleArg ? isBrainRole(roleArg) : false);
11330
+ if (canHandleLocally) {
11331
+ await handleBrainCommand(cwd, [
11332
+ "brain",
11333
+ ...tokens.slice(1)
11334
+ ], runtime);
11335
+ return;
11336
+ }
11337
+ }
11338
+ if (head === "model") {
11339
+ await printBrainAssignments(cwd);
11340
+ return;
11341
+ }
11342
+ if (head === "models") {
11343
+ await handleModelsCommand(cwd, [
11344
+ "models",
11345
+ ...tokens.slice(1)
11346
+ ], runtime);
11347
+ return;
11348
+ }
11349
+ if (head === "use-model") {
11350
+ const modelId = tokens.slice(1).join(" ").trim();
11351
+ if (!modelId) {
11352
+ throw new Error("Usage: /use-model <model-id>");
11353
+ }
11354
+ await handleModelSelection(cwd, modelId, runtime);
11355
+ return;
11356
+ }
11357
+ if (head === "select") {
11358
+ await handleSelectCommand(cwd, [
11359
+ "select",
11360
+ ...tokens.slice(1)
11361
+ ], runtime);
11362
+ return;
11363
+ }
11364
+ const externalTokens = toExternalCommandTokens(trimmed, state);
11365
+ if (externalTokens.length === 0) {
11366
+ return;
11367
+ }
11368
+ await execute(externalTokens);
11369
+ }
10782
11370
  async function runInteractiveShell(options) {
10783
11371
  const readline = (0, import_promises8.createInterface)({
10784
11372
  input: import_node_process10.default.stdin,
10785
11373
  output: import_node_process10.default.stdout,
10786
11374
  terminal: Boolean(import_node_process10.default.stdin.isTTY && import_node_process10.default.stdout.isTTY)
10787
11375
  });
11376
+ const runtime = {
11377
+ focusRole: "coder",
11378
+ lastModels: null
11379
+ };
10788
11380
  let closed = false;
10789
11381
  readline.on("SIGINT", () => {
10790
11382
  closed = true;
10791
11383
  readline.close();
10792
11384
  });
10793
- let state = await getShellSessionState(options.cwd);
11385
+ let state = await getShellSessionState(options.cwd, runtime.focusRole);
10794
11386
  printHeader(options.cwd, state);
10795
11387
  while (!closed) {
10796
11388
  let line;
10797
11389
  try {
10798
- state = await getShellSessionState(options.cwd);
11390
+ state = await getShellSessionState(options.cwd, runtime.focusRole);
10799
11391
  line = await readline.question(formatPrompt(state));
10800
11392
  } catch {
10801
11393
  break;
10802
11394
  }
10803
- const trimmed = line.trim();
10804
- if (!trimmed) {
10805
- continue;
10806
- }
10807
- if (trimmed === "/exit" || trimmed === "exit" || trimmed === "quit" || trimmed === "/quit") {
10808
- closed = true;
10809
- break;
10810
- }
10811
- if (trimmed === "/help" || trimmed === "help" || trimmed === "?") {
10812
- console.log(renderHelp());
10813
- console.log("");
10814
- continue;
10815
- }
10816
- if (trimmed === "/status" || trimmed === "status") {
10817
- state = await getShellSessionState(options.cwd);
10818
- console.log(renderStartupCard(options.cwd, state));
10819
- console.log("");
10820
- continue;
10821
- }
10822
- if (trimmed === "/clear" || trimmed === "clear") {
10823
- if (import_node_process10.default.stdout.isTTY) {
10824
- import_node_process10.default.stdout.write("\x1Bc");
10825
- }
10826
- state = await getShellSessionState(options.cwd);
10827
- printHeader(options.cwd, state);
10828
- continue;
10829
- }
10830
11395
  try {
10831
- const tokens = toCommandTokens(trimmed);
10832
- if (tokens.length === 0) {
10833
- continue;
10834
- }
10835
- await options.execute(tokens);
11396
+ await handleShellCommand(options.cwd, line, state, runtime, options.execute);
10836
11397
  } catch (error) {
10837
11398
  const message = error instanceof Error ? error.message : String(error);
11399
+ if (message === "__kimbho_exit__") {
11400
+ closed = true;
11401
+ break;
11402
+ }
10838
11403
  console.error(message);
10839
11404
  } finally {
10840
11405
  import_node_process10.default.exitCode = 0;