@kimbho/kimbho-cli 0.1.2 → 0.1.3

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.3",
3350
3350
  description: "Kimbho CLI is a terminal-native coding agent for planning, execution, and verification.",
3351
3351
  type: "module",
3352
3352
  engines: {
@@ -8908,7 +8908,7 @@ function buildProviderHeaders(definition, apiKey, includeJsonContentType = true)
8908
8908
  }
8909
8909
  function filterModels(models, input = {}) {
8910
8910
  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;
8911
+ const filtered = normalized ? models.filter((model) => [model.id, model.name].filter((value) => Boolean(value)).some((value) => value.toLowerCase().includes(normalized))) : models;
8912
8912
  return typeof input.limit === "number" ? filtered.slice(0, input.limit) : filtered;
8913
8913
  }
8914
8914
  function mapOpenAIStyleModels(providerId, payload, input) {
@@ -9745,11 +9745,19 @@ function renderModelLine(model) {
9745
9745
  ].filter((value) => Boolean(value));
9746
9746
  return details.length > 0 ? `${model.id} | ${details.join(" | ")}` : model.id;
9747
9747
  }
9748
+ function filterCachedModels(models, options) {
9749
+ const normalizedSearch = options.search?.trim().toLowerCase();
9750
+ const filtered = normalizedSearch ? models.filter((model) => model.id.toLowerCase().includes(normalizedSearch)) : models;
9751
+ return typeof options.limit === "number" ? filtered.slice(0, options.limit) : filtered;
9752
+ }
9748
9753
  async function fetchProviderModels(provider, options) {
9749
- const cachedModels = provider.models.map((modelId) => ({
9750
- id: modelId,
9751
- providerId: provider.id
9752
- }));
9754
+ const cachedModels = filterCachedModels(
9755
+ provider.models.map((modelId) => ({
9756
+ id: modelId,
9757
+ providerId: provider.id
9758
+ })),
9759
+ options
9760
+ );
9753
9761
  if (options.cached) {
9754
9762
  return {
9755
9763
  source: "cache",
@@ -10559,6 +10567,7 @@ var DIM = "\x1B[2m";
10559
10567
  var RESET = "\x1B[0m";
10560
10568
  var TOP_LEVEL_COMMANDS = /* @__PURE__ */ new Set([
10561
10569
  "agents",
10570
+ "brain",
10562
10571
  "brains",
10563
10572
  "doctor",
10564
10573
  "fix",
@@ -10568,15 +10577,30 @@ var TOP_LEVEL_COMMANDS = /* @__PURE__ */ new Set([
10568
10577
  "models",
10569
10578
  "new",
10570
10579
  "plan",
10580
+ "provider",
10571
10581
  "providers",
10572
10582
  "quit",
10573
10583
  "resume",
10574
10584
  "review",
10575
10585
  "run",
10576
10586
  "scaffold",
10587
+ "select",
10577
10588
  "shell",
10578
- "status"
10589
+ "status",
10590
+ "use-model"
10579
10591
  ]);
10592
+ var MODEL_SUBCOMMANDS = /* @__PURE__ */ new Set([
10593
+ "help",
10594
+ "list",
10595
+ "sync",
10596
+ "use"
10597
+ ]);
10598
+ var BRAIN_ROLES = [
10599
+ "planner",
10600
+ "coder",
10601
+ "reviewer",
10602
+ "fast"
10603
+ ];
10580
10604
  function color(code, value) {
10581
10605
  return `${code}${value}${RESET}`;
10582
10606
  }
@@ -10589,12 +10613,14 @@ function renderBanner() {
10589
10613
  "|_|\\_\\_|_| |_|____/|____/|_| |_|\\___/ "
10590
10614
  ].map((line) => color(AMBER, line)).join("\n");
10591
10615
  }
10592
- async function getShellSessionState(cwd) {
10616
+ async function getShellSessionState(cwd, focusRole) {
10593
10617
  const config = await loadConfig(cwd);
10594
10618
  if (!config) {
10595
10619
  return {
10620
+ focusRole,
10596
10621
  providerLabel: "unconfigured",
10597
10622
  providerId: "-",
10623
+ focusModel: "not set",
10598
10624
  coderModel: "not set",
10599
10625
  plannerModel: "not set",
10600
10626
  approvalMode: "manual",
@@ -10603,11 +10629,13 @@ async function getShellSessionState(cwd) {
10603
10629
  configured: false
10604
10630
  };
10605
10631
  }
10606
- const coderSettings = config.brains.coder;
10607
- const provider = findProviderById(config, coderSettings.providerId);
10632
+ const focusSettings = config.brains[focusRole];
10633
+ const provider = findProviderById(config, focusSettings.providerId);
10608
10634
  return {
10635
+ focusRole,
10609
10636
  providerLabel: provider?.label ?? provider?.id ?? "unknown",
10610
- providerId: provider?.id ?? coderSettings.providerId,
10637
+ providerId: provider?.id ?? focusSettings.providerId,
10638
+ focusModel: resolveBrainModel(config, focusRole) ?? "not set",
10611
10639
  coderModel: resolveBrainModel(config, "coder") ?? "not set",
10612
10640
  plannerModel: resolveBrainModel(config, "planner") ?? "not set",
10613
10641
  approvalMode: config.approvalMode,
@@ -10640,20 +10668,24 @@ function renderBox(lines) {
10640
10668
  function renderHelp() {
10641
10669
  return [
10642
10670
  `${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.",
10671
+ "/status Show the active role, provider, and workspace state.",
10672
+ "/brain <role> Change shell focus to planner, coder, reviewer, or fast.",
10673
+ "/model Show current brain assignments.",
10674
+ "/providers List configured providers.",
10675
+ "/providers templates Show built-in provider templates.",
10676
+ "/providers add <tpl> [id] Add a provider from a built-in template.",
10677
+ "/providers use <id> [role] Use a provider for the active role.",
10678
+ "/providers check Run provider health checks.",
10679
+ "/models [search] Discover models for the active role's provider.",
10680
+ "/select <n> Select a model from the last numbered /models list.",
10681
+ "/use-model <id> Assign a model to the active role and provider.",
10682
+ "/plan <goal> Create a structured implementation plan.",
10683
+ "/run <goal> Start a Kimbho execution session for a goal.",
10684
+ "/resume Show the latest saved session.",
10685
+ "/agents Inspect agent roles and the active session.",
10686
+ "/doctor Check local environment and config.",
10687
+ "/clear Redraw the shell.",
10688
+ "/quit, /exit Leave the shell.",
10657
10689
  "",
10658
10690
  `${color(DIM, "Tip")}`,
10659
10691
  "Type a natural-language goal directly and Kimbho will treat it as /run <goal>."
@@ -10662,6 +10694,8 @@ function renderHelp() {
10662
10694
  function renderStartupCard(cwd, state) {
10663
10695
  const cardLines = [
10664
10696
  `${color(BOLD, "Kimbho CLI")} (v${KIMBHO_VERSION})`,
10697
+ renderCardLine("focus", state.focusRole),
10698
+ renderCardLine("model", state.focusModel),
10665
10699
  renderCardLine("coder", state.coderModel),
10666
10700
  renderCardLine("planner", state.plannerModel),
10667
10701
  renderCardLine("provider", `${state.providerLabel} [${state.providerId}]`),
@@ -10669,23 +10703,23 @@ function renderStartupCard(cwd, state) {
10669
10703
  renderCardLine("approval", state.approvalMode),
10670
10704
  renderCardLine("sandbox", state.sandboxMode),
10671
10705
  renderCardLine("preset", state.stackPreset),
10672
- renderCardLine("shortcuts", "/help /status /plan /run /models /brains /quit")
10706
+ renderCardLine("shortcuts", "/brain /providers /models /select /use-model /quit")
10673
10707
  ];
10674
10708
  if (!state.configured) {
10675
- cardLines.push("setup: run /init to create .kimbho/config.json");
10709
+ cardLines.push("setup: run /init or /providers add <template> to create .kimbho/config.json");
10676
10710
  }
10677
10711
  return renderBox(cardLines);
10678
10712
  }
10679
10713
  function formatPrompt(state) {
10680
- const coderModel = state.coderModel === "not set" ? "unconfigured" : shortenMiddle(state.coderModel, 18);
10681
- return `${color(AMBER, "kimbho")} ${color(DIM, `[${coderModel}]`)} > `;
10714
+ const model = state.focusModel === "not set" ? "unconfigured" : shortenMiddle(state.focusModel, 18);
10715
+ return `${color(AMBER, "kimbho")} ${color(DIM, `[${state.focusRole}:${model}]`)} > `;
10682
10716
  }
10683
10717
  function printHeader(cwd, state) {
10684
10718
  console.log(renderBanner());
10685
10719
  console.log(color(DIM, "Terminal-native coding agent"));
10686
10720
  console.log(renderStartupCard(cwd, state));
10687
10721
  console.log("");
10688
- console.log(color(DIM, "Tip: describe the app or repo change you want, and Kimbho will route it into /run."));
10722
+ console.log(color(DIM, "Tip: configure providers and models here, then describe the work and Kimbho will route it into /run."));
10689
10723
  console.log(renderHelp());
10690
10724
  console.log("");
10691
10725
  }
@@ -10736,21 +10770,399 @@ function tokenizeInput(input) {
10736
10770
  }
10737
10771
  return tokens;
10738
10772
  }
10739
- function toCommandTokens(input) {
10773
+ function normalizeInputTokens(input) {
10740
10774
  const trimmed = input.trim();
10741
10775
  if (!trimmed) {
10742
- return [];
10776
+ return {
10777
+ normalizedInput: "",
10778
+ tokens: [],
10779
+ head: null
10780
+ };
10743
10781
  }
10744
10782
  const normalizedInput = trimmed.startsWith("kimbho ") ? trimmed.slice("kimbho ".length).trim() : trimmed;
10745
10783
  const tokens = tokenizeInput(normalizedInput);
10746
- if (tokens.length === 0) {
10784
+ const firstToken = tokens[0];
10785
+ if (!firstToken) {
10786
+ return {
10787
+ normalizedInput,
10788
+ tokens,
10789
+ head: null
10790
+ };
10791
+ }
10792
+ return {
10793
+ normalizedInput,
10794
+ tokens,
10795
+ head: firstToken.startsWith("/") ? firstToken.slice(1) : firstToken
10796
+ };
10797
+ }
10798
+ function defaultProviderIdForTemplate(templateId) {
10799
+ switch (templateId) {
10800
+ case "openai":
10801
+ return "openai-main";
10802
+ case "anthropic":
10803
+ return "anthropic-main";
10804
+ case "openrouter":
10805
+ return "openrouter-main";
10806
+ case "ollama":
10807
+ return "ollama-local";
10808
+ case "lmstudio":
10809
+ return "lmstudio-local";
10810
+ default:
10811
+ return `${templateId}-main`;
10812
+ }
10813
+ }
10814
+ function buildCachedModels(provider, search, limit = 25) {
10815
+ const normalized = search?.trim().toLowerCase();
10816
+ const filtered = normalized ? provider.models.filter((modelId) => modelId.toLowerCase().includes(normalized)) : provider.models;
10817
+ return filtered.slice(0, limit).map((modelId) => ({
10818
+ id: modelId,
10819
+ providerId: provider.id
10820
+ }));
10821
+ }
10822
+ async function fetchModelsForProvider(cwd, config, provider, search, limit = 25) {
10823
+ const registry = createDefaultBrainProviderRegistry(cwd);
10824
+ const cachedModels = buildCachedModels(provider, search, limit);
10825
+ try {
10826
+ const models = await registry.listModels(provider, {
10827
+ ...search ? {
10828
+ search
10829
+ } : {},
10830
+ limit
10831
+ });
10832
+ const nextConfig = upsertProvider(config, {
10833
+ ...provider,
10834
+ models: Array.from(/* @__PURE__ */ new Set([
10835
+ ...provider.models,
10836
+ ...models.map((model) => model.id)
10837
+ ]))
10838
+ });
10839
+ if (nextConfig !== config) {
10840
+ await saveConfig(nextConfig, cwd);
10841
+ }
10842
+ return {
10843
+ config: nextConfig,
10844
+ source: "remote",
10845
+ models
10846
+ };
10847
+ } catch (error) {
10848
+ if (cachedModels.length > 0) {
10849
+ const message = error instanceof Error ? error.message : String(error);
10850
+ return {
10851
+ config,
10852
+ source: "cache",
10853
+ note: `Using cached models because remote discovery failed: ${message}`,
10854
+ models: cachedModels
10855
+ };
10856
+ }
10857
+ throw error;
10858
+ }
10859
+ }
10860
+ async function assignModelToRole(cwd, role, providerId, model) {
10861
+ const config = await loadConfig(cwd);
10862
+ if (!config) {
10863
+ throw new Error("No config found. Run /init or /providers add first.");
10864
+ }
10865
+ const provider = findProviderById(config, providerId);
10866
+ if (!provider) {
10867
+ throw new Error(`Unknown provider "${providerId}".`);
10868
+ }
10869
+ const currentSettings = resolveBrainSettings(config, role);
10870
+ const nextConfig = assignBrain(
10871
+ upsertProvider(config, {
10872
+ ...provider,
10873
+ defaultModel: model,
10874
+ models: Array.from(/* @__PURE__ */ new Set([
10875
+ ...provider.models,
10876
+ model
10877
+ ]))
10878
+ }),
10879
+ role,
10880
+ {
10881
+ providerId,
10882
+ model,
10883
+ ...typeof currentSettings.temperature === "number" ? {
10884
+ temperature: currentSettings.temperature
10885
+ } : {},
10886
+ ...typeof currentSettings.maxTokens === "number" ? {
10887
+ maxTokens: currentSettings.maxTokens
10888
+ } : {},
10889
+ ...currentSettings.promptPreamble ? {
10890
+ promptPreamble: currentSettings.promptPreamble
10891
+ } : {}
10892
+ }
10893
+ );
10894
+ const outputPath = await saveConfig(nextConfig, cwd);
10895
+ return outputPath;
10896
+ }
10897
+ async function useProviderForRole(cwd, role, providerId) {
10898
+ const config = await loadConfig(cwd);
10899
+ if (!config) {
10900
+ throw new Error("No config found. Run /init or /providers add first.");
10901
+ }
10902
+ const provider = findProviderById(config, providerId);
10903
+ if (!provider) {
10904
+ throw new Error(`Unknown provider "${providerId}".`);
10905
+ }
10906
+ const currentSettings = resolveBrainSettings(config, role);
10907
+ const resolvedModel = provider.defaultModel ?? provider.models[0] ?? null;
10908
+ const nextConfig = assignBrain(config, role, {
10909
+ providerId: provider.id,
10910
+ ...resolvedModel ? {
10911
+ model: resolvedModel
10912
+ } : {},
10913
+ ...typeof currentSettings.temperature === "number" ? {
10914
+ temperature: currentSettings.temperature
10915
+ } : {},
10916
+ ...typeof currentSettings.maxTokens === "number" ? {
10917
+ maxTokens: currentSettings.maxTokens
10918
+ } : {},
10919
+ ...currentSettings.promptPreamble ? {
10920
+ promptPreamble: currentSettings.promptPreamble
10921
+ } : {}
10922
+ });
10923
+ const outputPath = await saveConfig(nextConfig, cwd);
10924
+ return {
10925
+ outputPath,
10926
+ model: resolvedModel
10927
+ };
10928
+ }
10929
+ function renderModelLine2(model) {
10930
+ const details = [
10931
+ model.name,
10932
+ model.contextLength ? `ctx ${model.contextLength}` : null,
10933
+ model.promptPrice ? `in ${model.promptPrice}` : null,
10934
+ model.completionPrice ? `out ${model.completionPrice}` : null,
10935
+ model.modality
10936
+ ].filter((value) => Boolean(value));
10937
+ return details.length > 0 ? `${model.id} | ${details.join(" | ")}` : model.id;
10938
+ }
10939
+ async function printProviderList(cwd, focusRole) {
10940
+ const config = await loadConfig(cwd);
10941
+ if (!config) {
10942
+ console.log("No config found. Run /init or /providers add <template> first.");
10943
+ return;
10944
+ }
10945
+ const activeProviderId = config.brains[focusRole].providerId;
10946
+ for (const provider of config.providers) {
10947
+ const marker = provider.id === activeProviderId ? "*" : " ";
10948
+ const envState = provider.apiKeyEnv ? import_node_process10.default.env[provider.apiKeyEnv] ? "present" : `missing ${provider.apiKeyEnv}` : "no key required";
10949
+ console.log(`${marker} ${provider.id}`);
10950
+ console.log(` label: ${provider.label ?? "-"}`);
10951
+ console.log(` driver: ${provider.driver}`);
10952
+ console.log(` model: ${provider.defaultModel ?? "-"}`);
10953
+ console.log(` auth: ${envState}`);
10954
+ console.log(` cachedModels: ${provider.models.length}`);
10955
+ }
10956
+ }
10957
+ function printProviderTemplates() {
10958
+ for (const template of listProviderTemplates()) {
10959
+ console.log(`${template.id}`);
10960
+ console.log(` label: ${template.label}`);
10961
+ console.log(` driver: ${template.driver}`);
10962
+ console.log(` apiKeyEnv: ${template.defaultApiKeyEnv ?? "-"}`);
10963
+ console.log(` model: ${template.defaultModel ?? "-"}`);
10964
+ console.log(` notes: ${template.notes}`);
10965
+ }
10966
+ }
10967
+ async function addProviderFromTemplate(cwd, templateId, providerId) {
10968
+ const provider = buildProviderFromTemplate(templateId, {
10969
+ providerId: providerId ?? defaultProviderIdForTemplate(templateId)
10970
+ });
10971
+ const config = await loadConfig(cwd);
10972
+ if (!config) {
10973
+ const outputPath = await saveConfig(createDefaultConfig({ provider }), cwd);
10974
+ return outputPath;
10975
+ }
10976
+ return saveConfig(upsertProvider(config, provider), cwd);
10977
+ }
10978
+ async function printProviderHealth(cwd) {
10979
+ const config = await loadConfig(cwd);
10980
+ if (!config) {
10981
+ console.log("No config found. Run /init or /providers add <template> first.");
10982
+ return;
10983
+ }
10984
+ const registry = createDefaultBrainProviderRegistry(cwd);
10985
+ for (const provider of config.providers) {
10986
+ try {
10987
+ const result = await registry.healthCheck(provider);
10988
+ console.log(`${result.ok ? "PASS" : "FAIL"} ${provider.id}: ${provider.driver} | ${result.message}`);
10989
+ } catch (error) {
10990
+ const message = error instanceof Error ? error.message : String(error);
10991
+ console.log(`FAIL ${provider.id}: ${provider.driver} | ${message}`);
10992
+ }
10993
+ }
10994
+ }
10995
+ async function printBrainAssignments(cwd) {
10996
+ const config = await loadConfig(cwd);
10997
+ if (!config) {
10998
+ console.log("No config found. Run /init or /providers add <template> first.");
10999
+ return;
11000
+ }
11001
+ for (const role of BRAIN_ROLES) {
11002
+ const settings = resolveBrainSettings(config, role);
11003
+ console.log(`${role}`);
11004
+ console.log(` provider: ${settings.providerId}`);
11005
+ console.log(` model: ${resolveBrainModel(config, role) ?? "-"}`);
11006
+ console.log(` temperature: ${settings.temperature ?? "-"}`);
11007
+ console.log(` maxTokens: ${settings.maxTokens ?? "-"}`);
11008
+ }
11009
+ }
11010
+ function isBrainRole(value) {
11011
+ return BRAIN_ROLES.includes(value);
11012
+ }
11013
+ async function handleProvidersCommand(cwd, tokens, runtime) {
11014
+ const subcommand = tokens[1];
11015
+ if (!subcommand || subcommand === "list") {
11016
+ await printProviderList(cwd, runtime.focusRole);
11017
+ return;
11018
+ }
11019
+ if (subcommand === "templates") {
11020
+ printProviderTemplates();
11021
+ return;
11022
+ }
11023
+ if (subcommand === "check") {
11024
+ await printProviderHealth(cwd);
11025
+ return;
11026
+ }
11027
+ if (subcommand === "add") {
11028
+ const templateId = tokens[2];
11029
+ const providerId = tokens[3];
11030
+ if (!templateId) {
11031
+ throw new Error("Usage: /providers add <template> [provider-id]");
11032
+ }
11033
+ const outputPath = await addProviderFromTemplate(cwd, templateId, providerId);
11034
+ const resolvedProviderId = providerId ?? defaultProviderIdForTemplate(templateId);
11035
+ console.log(`Updated ${outputPath}`);
11036
+ console.log(`Added provider ${resolvedProviderId} from template ${templateId}`);
11037
+ console.log(`Next: /providers use ${resolvedProviderId}`);
11038
+ return;
11039
+ }
11040
+ if (subcommand === "use") {
11041
+ const providerId = tokens[2];
11042
+ const role = tokens[3];
11043
+ if (!providerId) {
11044
+ throw new Error("Usage: /providers use <provider-id> [role]");
11045
+ }
11046
+ const targetRole = role && isBrainRole(role) ? role : runtime.focusRole;
11047
+ const result = await useProviderForRole(cwd, targetRole, providerId);
11048
+ runtime.focusRole = targetRole;
11049
+ runtime.lastModels = null;
11050
+ console.log(`Updated ${result.outputPath}`);
11051
+ console.log(`Focus role ${targetRole} now uses provider ${providerId}${result.model ? ` (${result.model})` : ""}`);
11052
+ return;
11053
+ }
11054
+ throw new Error("Usage: /providers [list|templates|add|use|check]");
11055
+ }
11056
+ async function handleBrainCommand(cwd, tokens, runtime) {
11057
+ const subcommand = tokens[1];
11058
+ if (!subcommand || subcommand === "list") {
11059
+ await printBrainAssignments(cwd);
11060
+ return;
11061
+ }
11062
+ if (subcommand === "use" && tokens[2] && isBrainRole(tokens[2])) {
11063
+ runtime.focusRole = tokens[2];
11064
+ runtime.lastModels = null;
11065
+ console.log(`Shell focus role is now ${runtime.focusRole}.`);
11066
+ return;
11067
+ }
11068
+ if (isBrainRole(subcommand)) {
11069
+ runtime.focusRole = subcommand;
11070
+ runtime.lastModels = null;
11071
+ console.log(`Shell focus role is now ${runtime.focusRole}.`);
11072
+ return;
11073
+ }
11074
+ throw new Error("Usage: /brain [planner|coder|reviewer|fast]");
11075
+ }
11076
+ async function handleModelsCommand(cwd, tokens, runtime) {
11077
+ const config = await loadConfig(cwd);
11078
+ if (!config) {
11079
+ throw new Error("No config found. Run /init or /providers add <template> first.");
11080
+ }
11081
+ const providerId = config.brains[runtime.focusRole].providerId;
11082
+ const provider = findProviderById(config, providerId);
11083
+ if (!provider) {
11084
+ throw new Error(`Active role "${runtime.focusRole}" points to unknown provider "${providerId}".`);
11085
+ }
11086
+ const subcommand = tokens[1];
11087
+ if (subcommand === "use") {
11088
+ const modelId = tokens.slice(2).join(" ").trim();
11089
+ if (!modelId) {
11090
+ throw new Error("Usage: /models use <model-id>");
11091
+ }
11092
+ const outputPath = await assignModelToRole(cwd, runtime.focusRole, provider.id, modelId);
11093
+ console.log(`Updated ${outputPath}`);
11094
+ console.log(`Selected ${provider.id}/${modelId}`);
11095
+ console.log(`Assigned role ${runtime.focusRole}`);
11096
+ return;
11097
+ }
11098
+ const search = !subcommand || MODEL_SUBCOMMANDS.has(subcommand) ? void 0 : tokens.slice(1).join(" ");
11099
+ const result = await fetchModelsForProvider(cwd, config, provider, search, 25);
11100
+ runtime.lastModels = {
11101
+ providerId: provider.id,
11102
+ role: runtime.focusRole,
11103
+ source: result.source,
11104
+ ...search ? {
11105
+ search
11106
+ } : {},
11107
+ models: result.models
11108
+ };
11109
+ console.log(`${provider.id} (${result.source})`);
11110
+ if (result.note) {
11111
+ console.log(` note: ${result.note}`);
11112
+ }
11113
+ if (result.models.length === 0) {
11114
+ console.log(" no models found");
11115
+ return;
11116
+ }
11117
+ for (const [index, model] of result.models.entries()) {
11118
+ console.log(` ${index + 1}. ${renderModelLine2(model)}`);
11119
+ }
11120
+ console.log(``);
11121
+ console.log(`Use /select <number> or /use-model <model-id> to assign one to ${runtime.focusRole}.`);
11122
+ }
11123
+ async function handleModelSelection(cwd, modelId, runtime) {
11124
+ const config = await loadConfig(cwd);
11125
+ if (!config) {
11126
+ throw new Error("No config found. Run /init or /providers add <template> first.");
11127
+ }
11128
+ const providerId = config.brains[runtime.focusRole].providerId;
11129
+ const provider = findProviderById(config, providerId);
11130
+ if (!provider) {
11131
+ throw new Error(`Active role "${runtime.focusRole}" points to unknown provider "${providerId}".`);
11132
+ }
11133
+ const outputPath = await assignModelToRole(cwd, runtime.focusRole, provider.id, modelId);
11134
+ console.log(`Updated ${outputPath}`);
11135
+ console.log(`Selected ${provider.id}/${modelId}`);
11136
+ console.log(`Assigned role ${runtime.focusRole}`);
11137
+ }
11138
+ async function handleSelectCommand(cwd, tokens, runtime) {
11139
+ const rawIndex = tokens[1];
11140
+ if (!rawIndex) {
11141
+ throw new Error("Usage: /select <number>");
11142
+ }
11143
+ if (!runtime.lastModels || runtime.lastModels.models.length === 0) {
11144
+ throw new Error("No model list is active. Run /models first.");
11145
+ }
11146
+ const index = Number.parseInt(rawIndex, 10);
11147
+ if (!Number.isInteger(index) || index <= 0) {
11148
+ throw new Error(`Expected a positive model number, received "${rawIndex}".`);
11149
+ }
11150
+ const model = runtime.lastModels.models[index - 1];
11151
+ if (!model) {
11152
+ throw new Error(`Model number ${index} is out of range.`);
11153
+ }
11154
+ runtime.focusRole = runtime.lastModels.role;
11155
+ await handleModelSelection(cwd, model.id, runtime);
11156
+ }
11157
+ function toExternalCommandTokens(input, state) {
11158
+ const { normalizedInput, tokens, head } = normalizeInputTokens(input);
11159
+ if (!head || tokens.length === 0) {
10747
11160
  return [];
10748
11161
  }
10749
11162
  const firstToken = tokens[0];
10750
11163
  if (!firstToken) {
10751
11164
  return [];
10752
11165
  }
10753
- const head = firstToken.startsWith("/") ? firstToken.slice(1) : firstToken;
10754
11166
  if (head === "new") {
10755
11167
  return [
10756
11168
  "run",
@@ -10764,12 +11176,6 @@ function toCommandTokens(input) {
10764
11176
  `scaffold ${goal}`.trim()
10765
11177
  ];
10766
11178
  }
10767
- if (head === "model") {
10768
- return [
10769
- "brains",
10770
- "list"
10771
- ];
10772
- }
10773
11179
  if (!firstToken.startsWith("/") && !TOP_LEVEL_COMMANDS.has(head) && !head.startsWith("-")) {
10774
11180
  return [
10775
11181
  "run",
@@ -10779,62 +11185,122 @@ function toCommandTokens(input) {
10779
11185
  tokens[0] = head;
10780
11186
  return tokens;
10781
11187
  }
11188
+ async function handleShellCommand(cwd, input, state, runtime, execute) {
11189
+ const trimmed = input.trim();
11190
+ if (!trimmed) {
11191
+ return;
11192
+ }
11193
+ if (trimmed === "/exit" || trimmed === "exit" || trimmed === "quit" || trimmed === "/quit") {
11194
+ throw new Error("__kimbho_exit__");
11195
+ }
11196
+ if (trimmed === "/help" || trimmed === "help" || trimmed === "?") {
11197
+ console.log(renderHelp());
11198
+ return;
11199
+ }
11200
+ if (trimmed === "/status" || trimmed === "status") {
11201
+ console.log(renderStartupCard(cwd, state));
11202
+ return;
11203
+ }
11204
+ if (trimmed === "/clear" || trimmed === "clear") {
11205
+ if (import_node_process10.default.stdout.isTTY) {
11206
+ import_node_process10.default.stdout.write("\x1Bc");
11207
+ }
11208
+ const nextState = await getShellSessionState(cwd, runtime.focusRole);
11209
+ printHeader(cwd, nextState);
11210
+ return;
11211
+ }
11212
+ const { tokens, head } = normalizeInputTokens(trimmed);
11213
+ if (!head) {
11214
+ return;
11215
+ }
11216
+ if (head === "provider" || head === "providers") {
11217
+ const subcommand = tokens[1];
11218
+ const canHandleLocally = !subcommand || subcommand === "list" || subcommand === "templates" || subcommand === "check" || subcommand === "use" || subcommand === "add" && Boolean(tokens[2]) && !tokens[2]?.startsWith("-");
11219
+ if (canHandleLocally) {
11220
+ await handleProvidersCommand(cwd, [
11221
+ "providers",
11222
+ ...tokens.slice(1)
11223
+ ], runtime);
11224
+ return;
11225
+ }
11226
+ }
11227
+ if (head === "brain" || head === "brains") {
11228
+ const subcommand = tokens[1];
11229
+ const roleArg = tokens[2];
11230
+ const canHandleLocally = !subcommand || subcommand === "list" || isBrainRole(subcommand) || subcommand === "use" && (roleArg ? isBrainRole(roleArg) : false);
11231
+ if (canHandleLocally) {
11232
+ await handleBrainCommand(cwd, [
11233
+ "brain",
11234
+ ...tokens.slice(1)
11235
+ ], runtime);
11236
+ return;
11237
+ }
11238
+ }
11239
+ if (head === "model") {
11240
+ await printBrainAssignments(cwd);
11241
+ return;
11242
+ }
11243
+ if (head === "models") {
11244
+ await handleModelsCommand(cwd, [
11245
+ "models",
11246
+ ...tokens.slice(1)
11247
+ ], runtime);
11248
+ return;
11249
+ }
11250
+ if (head === "use-model") {
11251
+ const modelId = tokens.slice(1).join(" ").trim();
11252
+ if (!modelId) {
11253
+ throw new Error("Usage: /use-model <model-id>");
11254
+ }
11255
+ await handleModelSelection(cwd, modelId, runtime);
11256
+ return;
11257
+ }
11258
+ if (head === "select") {
11259
+ await handleSelectCommand(cwd, [
11260
+ "select",
11261
+ ...tokens.slice(1)
11262
+ ], runtime);
11263
+ return;
11264
+ }
11265
+ const externalTokens = toExternalCommandTokens(trimmed, state);
11266
+ if (externalTokens.length === 0) {
11267
+ return;
11268
+ }
11269
+ await execute(externalTokens);
11270
+ }
10782
11271
  async function runInteractiveShell(options) {
10783
11272
  const readline = (0, import_promises8.createInterface)({
10784
11273
  input: import_node_process10.default.stdin,
10785
11274
  output: import_node_process10.default.stdout,
10786
11275
  terminal: Boolean(import_node_process10.default.stdin.isTTY && import_node_process10.default.stdout.isTTY)
10787
11276
  });
11277
+ const runtime = {
11278
+ focusRole: "coder",
11279
+ lastModels: null
11280
+ };
10788
11281
  let closed = false;
10789
11282
  readline.on("SIGINT", () => {
10790
11283
  closed = true;
10791
11284
  readline.close();
10792
11285
  });
10793
- let state = await getShellSessionState(options.cwd);
11286
+ let state = await getShellSessionState(options.cwd, runtime.focusRole);
10794
11287
  printHeader(options.cwd, state);
10795
11288
  while (!closed) {
10796
11289
  let line;
10797
11290
  try {
10798
- state = await getShellSessionState(options.cwd);
11291
+ state = await getShellSessionState(options.cwd, runtime.focusRole);
10799
11292
  line = await readline.question(formatPrompt(state));
10800
11293
  } catch {
10801
11294
  break;
10802
11295
  }
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
11296
  try {
10831
- const tokens = toCommandTokens(trimmed);
10832
- if (tokens.length === 0) {
10833
- continue;
10834
- }
10835
- await options.execute(tokens);
11297
+ await handleShellCommand(options.cwd, line, state, runtime, options.execute);
10836
11298
  } catch (error) {
10837
11299
  const message = error instanceof Error ? error.message : String(error);
11300
+ if (message === "__kimbho_exit__") {
11301
+ closed = true;
11302
+ break;
11303
+ }
10838
11304
  console.error(message);
10839
11305
  } finally {
10840
11306
  import_node_process10.default.exitCode = 0;