@songsid/agend 2.1.4-beta.30 → 2.1.4-beta.31

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.
@@ -1021,6 +1021,10 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
1021
1021
  * Returns null on success (menu shown), or a fallback string to send.
1022
1022
  */
1023
1023
  promptModelMenu(instanceName: string, userId: string, channelId: string, adapter: ChannelAdapter, chatId: string, threadId?: string): Promise<string | null>;
1024
+ /** Cached-or-live account catalog behind "/model → more models" (claude). */
1025
+ private claudeApiModelOptions;
1026
+ /** Replace a consumed "/model" menu with the full account catalog tier. */
1027
+ private expandClaudeModelMenu;
1024
1028
  /** Consume a `/model` selection callback. Returns true for all model-select ids (incl. stale). */
1025
1029
  private handleModelSelection;
1026
1030
  /** Apply a model to an instance: runtime paste (claude-code) or persist + restart (others). */
@@ -7504,6 +7504,13 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
7504
7504
  if (previous?.models?.length)
7505
7505
  env.models = previous.models;
7506
7506
  }
7507
+ // Same protection for the extended catalog: one offline moment must not
7508
+ // blank a good account list for the whole cache TTL.
7509
+ if (!env.apiModels?.length) {
7510
+ const previous = this.readCliEnv(backend);
7511
+ if (previous?.apiModels?.length)
7512
+ env.apiModels = previous.apiModels;
7513
+ }
7507
7514
  const path = this.cliEnvPath(backend);
7508
7515
  mkdirSync(dirname(path), { recursive: true });
7509
7516
  writeFileSync(path, JSON.stringify(env, null, 2));
@@ -7839,13 +7846,17 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
7839
7846
  // Raw id for ✓-matching options; display resolves an inherited CLI default.
7840
7847
  const { model: currentModel, display: currentDisplay } = this.resolveInstanceModel(name);
7841
7848
  const nonce = randomBytes(6).toString("hex");
7842
- const choices = options.slice(0, 25).map(o => ({
7849
+ const isClaude = this.backendNameForInstance(name) === "claude-code";
7850
+ const choices = options.slice(0, isClaude ? 24 : 25).map(o => ({
7843
7851
  id: `${MODEL_SELECT_CALLBACK_PREFIX}${nonce}:${o.id}`,
7844
7852
  label: this.modelChoiceLabel(o, currentModel),
7845
7853
  }));
7854
+ if (isClaude) {
7855
+ choices.push({ id: `${MODEL_SELECT_CALLBACK_PREFIX}${nonce}:__more__`, label: t("model.more") });
7856
+ }
7846
7857
  const timer = setTimeout(() => this.pendingModelSelects.delete(nonce), CLASSIC_BACKEND_SELECTION_TIMEOUT_MS);
7847
7858
  timer.unref?.();
7848
- this.pendingModelSelects.set(nonce, { instanceName: name, model: "", userId: data.userId, channelId: data.channelId, timer, respond: data.respond });
7859
+ this.pendingModelSelects.set(nonce, { instanceName: name, model: "", userId: data.userId, channelId: data.channelId, timer, respond: data.respond, respondChoices: data.respondChoices });
7849
7860
  try {
7850
7861
  await data.respondChoices(t("model.menu", `**${currentDisplay}**`), choices);
7851
7862
  }
@@ -7868,10 +7879,15 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
7868
7879
  }
7869
7880
  const { model: currentModel, display: currentDisplay } = this.resolveInstanceModel(instanceName);
7870
7881
  const nonce = randomBytes(6).toString("hex");
7871
- const choices = options.slice(0, 25).map(o => ({
7882
+ const isClaude = this.backendNameForInstance(instanceName) === "claude-code";
7883
+ // Keep the more-models entry inside Discord's 25-option select cap.
7884
+ const choices = options.slice(0, isClaude ? 24 : 25).map(o => ({
7872
7885
  id: `${MODEL_SELECT_CALLBACK_PREFIX}${nonce}:${o.id}`,
7873
7886
  label: this.modelChoiceLabel(o, currentModel),
7874
7887
  }));
7888
+ if (isClaude) {
7889
+ choices.push({ id: `${MODEL_SELECT_CALLBACK_PREFIX}${nonce}:__more__`, label: t("model.more") });
7890
+ }
7875
7891
  const respond = async (text) => {
7876
7892
  await adapter.sendText(chatId, text, { threadId });
7877
7893
  return undefined;
@@ -7899,6 +7915,62 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
7899
7915
  return t("model.usage");
7900
7916
  }
7901
7917
  }
7918
+ /** Cached-or-live account catalog behind "/model → more models" (claude). */
7919
+ async claudeApiModelOptions() {
7920
+ const cached = this.readCliEnv("claude-code");
7921
+ if (cached?.apiModels?.length)
7922
+ return cached.apiModels;
7923
+ const env = await this.probeBackend("claude-code");
7924
+ return env?.apiModels ?? [];
7925
+ }
7926
+ /** Replace a consumed "/model" menu with the full account catalog tier. */
7927
+ async expandClaudeModelMenu(pending) {
7928
+ const expanded = await this.claudeApiModelOptions();
7929
+ if (!expanded.length) {
7930
+ await pending.respond(t("model.more_unavailable")).catch(() => { });
7931
+ return;
7932
+ }
7933
+ const { model: currentModel, display: currentDisplay } = this.resolveInstanceModel(pending.instanceName);
7934
+ const nonce = randomBytes(6).toString("hex");
7935
+ const choices = expanded.slice(0, 25).map(o => ({
7936
+ id: `${MODEL_SELECT_CALLBACK_PREFIX}${nonce}:${o.id}`,
7937
+ label: this.modelChoiceLabel(o, currentModel),
7938
+ }));
7939
+ const timer = setTimeout(() => {
7940
+ const p = this.pendingModelSelects.get(nonce);
7941
+ if (p) {
7942
+ this.pendingModelSelects.delete(nonce);
7943
+ p.respond(t("model.selection_expired")).catch(() => { });
7944
+ }
7945
+ }, CLASSIC_BACKEND_SELECTION_TIMEOUT_MS);
7946
+ timer.unref?.();
7947
+ this.pendingModelSelects.set(nonce, { ...pending, model: "", timer });
7948
+ try {
7949
+ if (pending.respondChoices) {
7950
+ // Discord select menu: edit the same interaction reply in place.
7951
+ await pending.respondChoices(t("model.menu", `**${currentDisplay}**`), choices);
7952
+ return;
7953
+ }
7954
+ if (pending.adapter && pending.adapterChatId) {
7955
+ // Telegram: retire the tier-1 keyboard, then post the expanded menu.
7956
+ if (pending.menuMessageId && pending.adapter.editMessageRemoveButtons) {
7957
+ await pending.adapter.editMessageRemoveButtons(pending.adapterChatId, pending.menuMessageId, t("model.more"), pending.adapterThreadId).catch(() => { });
7958
+ }
7959
+ const menuMessageId = await pending.adapter.promptUser(pending.adapterChatId, t("model.menu", currentDisplay), choices, { threadId: pending.adapterThreadId });
7960
+ const fresh = this.pendingModelSelects.get(nonce);
7961
+ if (fresh)
7962
+ fresh.menuMessageId = menuMessageId;
7963
+ return;
7964
+ }
7965
+ await pending.respond(t("model.more_unavailable")).catch(() => { });
7966
+ }
7967
+ catch (err) {
7968
+ this.pendingModelSelects.delete(nonce);
7969
+ clearTimeout(timer);
7970
+ this.logger.warn({ err, instanceName: pending.instanceName }, "Expanded model menu failed");
7971
+ await pending.respond(t("model.more_unavailable")).catch(() => { });
7972
+ }
7973
+ }
7902
7974
  /** Consume a `/model` selection callback. Returns true for all model-select ids (incl. stale). */
7903
7975
  async handleModelSelection(data) {
7904
7976
  if (!data.callbackData.startsWith(MODEL_SELECT_CALLBACK_PREFIX))
@@ -7918,6 +7990,12 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
7918
7990
  this.pendingModelSelects.delete(match[1]);
7919
7991
  clearTimeout(pending.timer);
7920
7992
  const model = match[2];
7993
+ // "More models…" is a navigation choice, not a model: swap the menu for the
7994
+ // full account catalog (live /v1/models with [1m] variants).
7995
+ if (model === "__more__") {
7996
+ await this.expandClaudeModelMenu(pending);
7997
+ return true;
7998
+ }
7921
7999
  // Send immediate "⏳ Switching..." feedback, then apply in background.
7922
8000
  const progressText = t("model.switching", pending.instanceName, model);
7923
8001
  let progressMsgId;