@datalabrotterdam/nova-ai-cli 1.2.0-alpha.5 → 1.2.0-alpha.7

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.
@@ -9,21 +9,14 @@ import {
9
9
  } from "./chunk-FMHWW3E6.js";
10
10
  import {
11
11
  TuiAcpClient,
12
- createStore,
13
- readWorkspaceMcpConfiguration,
14
- workspaceNeedsTrust
15
- } from "./chunk-NHGIUEIR.js";
12
+ createStore
13
+ } from "./chunk-EOPM4RRE.js";
16
14
 
17
15
  // src/tui/index.ts
18
16
  import { createInterface as createInterface2 } from "readline/promises";
19
- import {
20
- readCredentials,
21
- saveDefaultModel as saveDefaultModel2
22
- } from "@datalabrotterdam/nova-ai-agent/core/credentials.js";
23
- import { resolveDefaultModel as resolveDefaultModel2 } from "@datalabrotterdam/nova-ai-agent/core/model-capabilities.js";
24
- import { createNovaClient as createNovaClient2 } from "@datalabrotterdam/nova-ai-agent/core/nova-client.js";
17
+ import { readCredentials } from "@datalabrotterdam/nova-ai-agent/core/credentials.js";
25
18
  import { listStoredSessions as listStoredSessions2, loadStoredSession as loadStoredSession2 } from "@datalabrotterdam/nova-ai-agent/core/sessions.js";
26
- import { setWorkspaceTrusted as setWorkspaceTrusted2 } from "@datalabrotterdam/nova-ai-agent/core/nova-home.js";
19
+ import { setWorkspaceTrusted } from "@datalabrotterdam/nova-ai-agent/core/nova-home.js";
27
20
 
28
21
  // src/update-install.ts
29
22
  import { spawnSync } from "child_process";
@@ -126,27 +119,18 @@ import { savedPermissionMode } from "@datalabrotterdam/nova-ai-agent/core/policy
126
119
 
127
120
  // src/tui/session/session-runner.ts
128
121
  import * as acp from "@agentclientprotocol/sdk";
129
- import {
130
- NovaAIError
131
- } from "@datalabrotterdam/nova-sdk";
132
- import { createNovaClient } from "@datalabrotterdam/nova-ai-agent/core/nova-client.js";
133
122
  import { NovaAgent } from "@datalabrotterdam/nova-ai-agent/acp/agent.js";
134
123
  import { startInProcessAgent } from "@datalabrotterdam/nova-ai-agent/client/in-process.js";
135
124
  import { replayHistory } from "@datalabrotterdam/nova-ai-agent/core/history.js";
136
- import { saveDefaultModel } from "@datalabrotterdam/nova-ai-agent/core/credentials.js";
125
+ import {
126
+ saveDefaultEffort,
127
+ saveDefaultModel
128
+ } from "@datalabrotterdam/nova-ai-agent/core/credentials.js";
137
129
  import {
138
130
  savePermissionMode
139
131
  } from "@datalabrotterdam/nova-ai-agent/core/policy/settings.js";
140
132
  import { loadStoredSession } from "@datalabrotterdam/nova-ai-agent/core/sessions.js";
141
- import { discoverSkills } from "@datalabrotterdam/nova-ai-agent/core/skills.js";
142
- import {
143
- resolveModelContextWindow
144
- } from "@datalabrotterdam/nova-ai-agent/core/context-compaction.js";
145
- import {
146
- chatModels,
147
- resolveDefaultModel,
148
- resolveModelSupportsImageInput
149
- } from "@datalabrotterdam/nova-ai-agent/core/model-capabilities.js";
133
+ import { resolveEffort } from "@datalabrotterdam/nova-ai-agent/core/model-capabilities.js";
150
134
 
151
135
  // src/tui/files/file-mentions.ts
152
136
  import { statSync } from "fs";
@@ -291,15 +275,6 @@ var SessionRunner = class {
291
275
  constructor(store, credentials, cwd) {
292
276
  this.store = store;
293
277
  this.credentials = credentials;
294
- this.loadMcpConfiguration(cwd);
295
- this.skills = discoverSkills(cwd).map(
296
- ({ name, description, source, path }) => ({
297
- name,
298
- description,
299
- source,
300
- path
301
- })
302
- );
303
278
  this.cwd = cwd;
304
279
  this.acpClient = new TuiAcpClient(
305
280
  store,
@@ -314,13 +289,8 @@ var SessionRunner = class {
314
289
  clientCapabilities: this.acpClient.capabilities,
315
290
  clientInfo: { name: "nova-ai-cli-tui", version: "1" }
316
291
  });
317
- this.novaClient = createNovaClient(credentials.apiKey);
318
- const model = credentials.defaultModel ?? process.env.NOVA_MODEL;
319
- if (!model)
320
- throw new Error(
321
- "No Nova model configured. Re-run authentication or set NOVA_MODEL."
322
- );
323
- this.model = model;
292
+ this.model = credentials.defaultModel ?? process.env.NOVA_MODEL ?? "";
293
+ this.effort = credentials.defaultEffort ?? null;
324
294
  this.sessionId = crypto.randomUUID();
325
295
  this.sessionReady = Promise.resolve();
326
296
  }
@@ -331,21 +301,22 @@ var SessionRunner = class {
331
301
  title = null;
332
302
  model;
333
303
  interactionMode = "agent";
304
+ /** The modes the agent offers: the built-in ones, then the user's agent files. */
305
+ get modeIds() {
306
+ return this.acpClient.modeIds;
307
+ }
334
308
  /** The agent, served in-process over a real ACP connection (tests may stub it). */
335
309
  agent = new NovaAgent();
336
310
  acpClient;
337
311
  connection;
338
312
  nova;
339
313
  initialized;
340
- novaClient;
341
- mcpServers = [];
342
- mcpConfigurationFailures = [];
343
314
  mcpStatus = {
344
315
  configured: [],
345
316
  connected: [],
346
317
  failures: []
347
318
  };
348
- skills;
319
+ skills = [];
349
320
  sessionReady;
350
321
  agentSessionLoaded = false;
351
322
  promptActive = false;
@@ -353,10 +324,10 @@ var SessionRunner = class {
353
324
  handoffActive = false;
354
325
  queuedAttachments = /* @__PURE__ */ new Map();
355
326
  editingQueuedMessageId = null;
356
- imageSupportByModel = /* @__PURE__ */ new Map();
357
- contextWindow = null;
358
- contextWindowModel = null;
359
- contextWindowRequest = 0;
327
+ /** From the agent's model list: image input, thinking levels. */
328
+ modelInfo = /* @__PURE__ */ new Map();
329
+ /** The thinking level the user chose (saved); null: each model's default. */
330
+ effort;
360
331
  contextUsageRequest = 0;
361
332
  modelCheck = null;
362
333
  resumeFrom(storedSessionId) {
@@ -369,15 +340,7 @@ var SessionRunner = class {
369
340
  this.sessionId = stored.sessionId;
370
341
  this.cwd = stored.cwd;
371
342
  this.title = stored.title;
372
- this.loadMcpConfiguration(stored.cwd);
373
- this.skills = discoverSkills(stored.cwd).map(
374
- ({ name, description, source, path }) => ({
375
- name,
376
- description,
377
- source,
378
- path
379
- })
380
- );
343
+ this.skills = [];
381
344
  const messages = restoreSessionMessages(stored.messages);
382
345
  this.store.setState({
383
346
  messages,
@@ -396,12 +359,13 @@ var SessionRunner = class {
396
359
  return await this.nova.loadSession({
397
360
  sessionId: stored.sessionId,
398
361
  cwd: stored.cwd,
399
- mcpServers: this.mcpServers
362
+ mcpServers: []
400
363
  });
401
364
  } finally {
402
365
  this.acpClient.muteSessionUpdates = false;
403
366
  }
404
367
  }).then((response) => {
368
+ this.applyModes(response.modes);
405
369
  this.applyMcpStatus(response._meta);
406
370
  this.applySkillStatus(response._meta);
407
371
  this.agentSessionLoaded = true;
@@ -409,6 +373,7 @@ var SessionRunner = class {
409
373
  void this.nova.setSessionMode({ sessionId: this.sessionId, modeId: this.interactionMode }).catch(() => {
410
374
  });
411
375
  this.applyPermissionMode(this.sessionId);
376
+ this.applyEffort(this.sessionId);
412
377
  void this.updateContextUsage();
413
378
  void this.refreshContextUsage().catch(() => {
414
379
  });
@@ -521,9 +486,6 @@ var SessionRunner = class {
521
486
  void this.enqueueQueuedPrompt(value, [], [], "steer", true);
522
487
  return true;
523
488
  }
524
- configuredMcpServers() {
525
- return [...this.mcpServers];
526
- }
527
489
  mcpSessionStatus() {
528
490
  return {
529
491
  configured: this.mcpStatus.configured.map((server) => ({ ...server })),
@@ -534,27 +496,37 @@ var SessionRunner = class {
534
496
  skillSessionStatus() {
535
497
  return this.skills.map((skill) => ({ ...skill }));
536
498
  }
499
+ /** Signs in to (or out of) an HTTP MCP server; the agent reconnects it and reports the new status. */
500
+ async mcpSignIn(serverName, signIn) {
501
+ await this.ensureSession();
502
+ const params = { sessionId: this.sessionId, serverName };
503
+ const meta = signIn ? await this.nova.mcpLogin(params) : await this.nova.mcpLogout(params);
504
+ this.applyMcpStatus(meta);
505
+ return this.mcpSessionStatus();
506
+ }
507
+ async trustWorkspace() {
508
+ await this.initialized;
509
+ await this.nova.workspaceTrust({ cwd: this.cwd, trusted: true });
510
+ }
511
+ /** Every skill of the workspace with its on/off state, from the agent. */
512
+ async listSkills() {
513
+ await this.initialized;
514
+ return (await this.nova.skillsList({ cwd: this.cwd })).skills;
515
+ }
516
+ async setSkillEnabled(name, enabled, global) {
517
+ await this.initialized;
518
+ return (await this.nova.skillsSetEnabled({
519
+ cwd: this.cwd,
520
+ name,
521
+ enabled,
522
+ scope: global ? "global" : "project"
523
+ })).skills;
524
+ }
537
525
  contextUsageSnapshot() {
538
526
  return this.store.getState().contextUsage;
539
527
  }
540
528
  async refreshContextUsage() {
541
529
  await this.ensureSession();
542
- const sessionId = this.sessionId;
543
- const model = this.model;
544
- if (this.contextWindowModel !== model) {
545
- const request = ++this.contextWindowRequest;
546
- const contextWindow = await resolveModelContextWindow(
547
- this.novaClient,
548
- model
549
- );
550
- if (request === this.contextWindowRequest && model === this.model) {
551
- this.contextWindow = contextWindow;
552
- this.contextWindowModel = model;
553
- }
554
- }
555
- if (sessionId !== this.sessionId || !this.agentSessionLoaded) {
556
- return this.refreshContextUsage();
557
- }
558
530
  const usage = await this.updateContextUsage();
559
531
  if (!usage)
560
532
  throw new Error(
@@ -570,13 +542,8 @@ var SessionRunner = class {
570
542
  }
571
543
  setModel(model) {
572
544
  this.model = model;
573
- this.contextWindow = null;
574
- this.contextWindowModel = null;
575
- this.contextWindowRequest++;
576
545
  saveDefaultModel(model);
577
546
  if (this.agentSessionLoaded) void this.updateContextUsage();
578
- void this.refreshContextUsage().catch(() => {
579
- });
580
547
  }
581
548
  /**
582
549
  * Hands the mode to the agent's permission policy (which decides) and
@@ -634,27 +601,34 @@ var SessionRunner = class {
634
601
  }
635
602
  /** Chat models only: embedding, speech and transcription models can't run a session. */
636
603
  async listModels() {
637
- const { data } = await this.novaClient.models.list({ limit: 100 });
638
- return chatModels(data).map((model) => ({
639
- id: model.id,
640
- name: model.name ?? null
641
- }));
604
+ await this.initialized;
605
+ const { models } = await this.nova.modelsList();
606
+ for (const model of models) this.modelInfo.set(model.id, model);
607
+ return models.map((model) => ({ id: model.id, name: model.name, thinking: model.thinking }));
608
+ }
609
+ /** Sync, from the loaded model list: false until it has loaded. */
610
+ hasThinkingLevels(model = this.model) {
611
+ return (this.modelInfo.get(model)?.efforts.length ?? 0) > 0;
642
612
  }
643
613
  /**
644
- * Replaces a saved model that cannot chat (older logins saved the first
645
- * model of the list, e.g. an embedding model) and saves the replacement.
614
+ * Takes the account's default model when none is set, and replaces a saved
615
+ * one that cannot chat (older logins saved the first model of the list,
616
+ * e.g. an embedding model); the agent decides, the choice is saved.
646
617
  * Runs once; a failed lookup keeps the model and the prompt reports errors.
647
618
  */
648
619
  ensureUsableModel() {
649
620
  this.modelCheck ??= (async () => {
650
621
  try {
651
- const { data } = await this.novaClient.models.list({ limit: 100 });
652
- const model = resolveDefaultModel(data, this.model);
653
- if (!model || model === this.model) return;
622
+ await this.initialized;
623
+ const { models, defaultModel } = await this.nova.modelsList(
624
+ this.model ? { preferred: this.model } : {}
625
+ );
626
+ for (const model of models) this.modelInfo.set(model.id, model);
627
+ if (!defaultModel || defaultModel === this.model) return;
654
628
  const previous = this.model;
655
- this.setModel(model);
629
+ this.setModel(defaultModel);
656
630
  this.store.setState({
657
- statusLine: `${previous} is not a chat model; switched to ${model} (Ctrl+P to pick another).`
631
+ statusLine: previous ? `${previous} is not a chat model; switched to ${defaultModel} (Ctrl+P to pick another).` : `Using model ${defaultModel} (Ctrl+P switches).`
658
632
  });
659
633
  } catch {
660
634
  }
@@ -662,14 +636,41 @@ var SessionRunner = class {
662
636
  return this.modelCheck;
663
637
  }
664
638
  async supportsImageInput(model = this.model) {
665
- const cached = this.imageSupportByModel.get(model);
666
- if (cached !== void 0) return cached;
667
- const supported = await resolveModelSupportsImageInput(
668
- this.novaClient,
669
- model
670
- );
671
- this.imageSupportByModel.set(model, supported);
672
- return supported;
639
+ return (await this.modelDetails(model))?.imageInput ?? false;
640
+ }
641
+ /** What the agent's model list says about `model`; undefined when it isn't listed. */
642
+ async modelDetails(model = this.model) {
643
+ if (!this.modelInfo.has(model)) await this.listModels();
644
+ return this.modelInfo.get(model);
645
+ }
646
+ /**
647
+ * The level the current model thinks at, as the agent picks it (the
648
+ * chosen one when the model has it); null for a model without levels
649
+ * or before the model list has loaded.
650
+ */
651
+ currentEffort() {
652
+ const info = this.modelInfo.get(this.model);
653
+ if (!info?.efforts.length) return null;
654
+ return resolveEffort(
655
+ { reasoning_efforts: info.efforts, reasoning_default: info.reasoningDefault },
656
+ this.effort
657
+ ) ?? null;
658
+ }
659
+ /** The current model's levels from the loaded list; empty when it has none. */
660
+ modelEffortLevels() {
661
+ return this.modelInfo.get(this.model)?.efforts ?? [];
662
+ }
663
+ /** Saved and kept across models and sessions; a model without the level uses its default. */
664
+ setEffort(level) {
665
+ this.effort = level;
666
+ saveDefaultEffort(level);
667
+ if (this.agentSessionLoaded) this.applyEffort(this.sessionId);
668
+ }
669
+ applyEffort(sessionId) {
670
+ if (!this.effort) return;
671
+ void this.nova.setSessionConfigOption({ sessionId, configId: "thought_level", value: this.effort }).catch(() => {
672
+ this.acpClient.appendError("Could not change the thinking level.");
673
+ });
673
674
  }
674
675
  async compactContext() {
675
676
  if (this.store.getState().busy || this.promptActive) {
@@ -793,7 +794,7 @@ var SessionRunner = class {
793
794
  });
794
795
  }
795
796
  } catch (err) {
796
- const message = err instanceof NovaAIError ? `Nova AI request failed (status ${err.status}, requestId ${err.requestId}): ${err.message}` : err instanceof Error ? err.message : "Unknown error.";
797
+ const message = err instanceof Error ? err.message : "Unknown error.";
797
798
  pendingToolFailure = `Request failed: ${message}`;
798
799
  this.acpClient.appendError(message);
799
800
  this.store.setState({ statusLine: "Request failed." });
@@ -947,13 +948,13 @@ var SessionRunner = class {
947
948
  ];
948
949
  }
949
950
  async createSession() {
950
- this.loadMcpConfiguration(this.cwd);
951
951
  await this.initialized;
952
- const { sessionId, _meta } = await this.nova.newSession({
952
+ const { sessionId, _meta, modes } = await this.nova.newSession({
953
953
  cwd: this.cwd,
954
- mcpServers: this.mcpServers
954
+ mcpServers: []
955
955
  });
956
956
  this.sessionId = sessionId;
957
+ this.applyModes(modes);
957
958
  this.applyMcpStatus(_meta);
958
959
  this.applySkillStatus(_meta);
959
960
  this.agentSessionLoaded = true;
@@ -961,6 +962,7 @@ var SessionRunner = class {
961
962
  void this.nova.setSessionMode({ sessionId, modeId: this.interactionMode }).catch(() => {
962
963
  });
963
964
  this.applyPermissionMode(sessionId);
965
+ this.applyEffort(sessionId);
964
966
  void this.updateContextUsage();
965
967
  void this.refreshContextUsage().catch(() => {
966
968
  });
@@ -975,10 +977,20 @@ var SessionRunner = class {
975
977
  const value = meta?.["nova-ai-cli/mcp"];
976
978
  if (!value || typeof value !== "object") return;
977
979
  const record = value;
980
+ const strings = (value2) => Array.isArray(value2) ? value2.filter((entry) => typeof entry === "string") : void 0;
978
981
  const configured = Array.isArray(record.configured) ? record.configured.flatMap((entry) => {
979
982
  if (!entry || typeof entry !== "object") return [];
980
983
  const item = entry;
981
- return typeof item.name === "string" && typeof item.transport === "string" ? [{ name: item.name, transport: item.transport }] : [];
984
+ return typeof item.name === "string" && typeof item.transport === "string" ? [
985
+ {
986
+ name: item.name,
987
+ transport: item.transport,
988
+ ...typeof item.url === "string" ? { url: item.url } : {},
989
+ ...typeof item.command === "string" ? { command: item.command } : {},
990
+ ...strings(item.args) ? { args: strings(item.args) } : {},
991
+ ...strings(item.env) ? { env: strings(item.env) } : {}
992
+ }
993
+ ] : [];
982
994
  }) : [];
983
995
  const connected = Array.isArray(record.connected) ? record.connected.filter(
984
996
  (entry) => typeof entry === "string"
@@ -986,26 +998,15 @@ var SessionRunner = class {
986
998
  const failures = Array.isArray(record.failures) ? record.failures.flatMap((entry) => {
987
999
  if (!entry || typeof entry !== "object") return [];
988
1000
  const item = entry;
989
- return typeof item.serverName === "string" && typeof item.message === "string" ? [{ serverName: item.serverName, message: item.message }] : [];
1001
+ return typeof item.serverName === "string" && typeof item.message === "string" ? [
1002
+ {
1003
+ serverName: item.serverName,
1004
+ message: item.message,
1005
+ ...item.needsAuth === true ? { needsAuth: true } : {}
1006
+ }
1007
+ ] : [];
990
1008
  }) : [];
991
- this.mcpStatus = {
992
- configured,
993
- connected,
994
- failures: [...this.mcpConfigurationFailures, ...failures]
995
- };
996
- }
997
- loadMcpConfiguration(cwd) {
998
- const configuration = readWorkspaceMcpConfiguration(cwd);
999
- this.mcpServers = configuration.servers;
1000
- this.mcpConfigurationFailures = configuration.failures;
1001
- this.mcpStatus = {
1002
- configured: this.mcpServers.map((server) => ({
1003
- name: server.name,
1004
- transport: "type" in server ? server.type : "stdio"
1005
- })),
1006
- connected: [],
1007
- failures: [...this.mcpConfigurationFailures]
1008
- };
1009
+ this.mcpStatus = { configured, connected, failures };
1009
1010
  }
1010
1011
  applySkillStatus(meta) {
1011
1012
  const value = meta?.["nova-ai-cli/skills"];
@@ -1023,14 +1024,34 @@ var SessionRunner = class {
1023
1024
  ] : [];
1024
1025
  });
1025
1026
  }
1027
+ /**
1028
+ * Starts the session ahead of the first prompt, so the agent's MCP and
1029
+ * skill status is there for /mcp and the status line. A failure is left to
1030
+ * the first prompt, which tries again and reports it.
1031
+ */
1032
+ async prepareSession() {
1033
+ try {
1034
+ await this.ensureSession();
1035
+ } catch {
1036
+ this.sessionReady = Promise.resolve();
1037
+ }
1038
+ }
1039
+ get sessionLoaded() {
1040
+ return this.agentSessionLoaded;
1041
+ }
1026
1042
  async ensureSession() {
1027
1043
  await this.ensureUsableModel();
1028
- await this.sessionReady;
1029
- if (!this.agentSessionLoaded) {
1030
- this.sessionReady = this.createSession();
1031
- await this.sessionReady;
1044
+ for (; ; ) {
1045
+ const ready = this.sessionReady;
1046
+ await ready;
1047
+ if (this.agentSessionLoaded) return;
1048
+ if (this.sessionReady === ready) this.sessionReady = this.createSession();
1032
1049
  }
1033
1050
  }
1051
+ applyModes(modes) {
1052
+ if (modes?.availableModes.length) this.acpClient.modeIds = modes.availableModes.map((mode) => mode.id);
1053
+ if (!this.modeIds.includes(this.interactionMode)) this.applyInteractionMode("agent", "Mode: agent");
1054
+ }
1034
1055
  applyInteractionMode(mode, statusLine) {
1035
1056
  this.interactionMode = mode;
1036
1057
  this.store.setState({ interactionMode: mode, statusLine });
@@ -1044,7 +1065,7 @@ var SessionRunner = class {
1044
1065
  try {
1045
1066
  const usage = await this.nova.sessionContextUsage({
1046
1067
  sessionId,
1047
- contextWindow: this.contextWindow,
1068
+ ...this.model ? { model: this.model } : {},
1048
1069
  mode: this.interactionMode
1049
1070
  });
1050
1071
  if (request === this.contextUsageRequest && sessionId === this.sessionId) {
@@ -1500,6 +1521,7 @@ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-run
1500
1521
  function StatusLine({
1501
1522
  ui,
1502
1523
  model,
1524
+ effort = null,
1503
1525
  mcp,
1504
1526
  scroll = 0
1505
1527
  }) {
@@ -1510,6 +1532,7 @@ function StatusLine({
1510
1532
  ui.statusLine ? /* @__PURE__ */ jsx2(Text2, { color: palette.muted, children: ui.statusLine }) : null,
1511
1533
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate-end", children: [
1512
1534
  /* @__PURE__ */ jsx2(Text2, { color: palette.muted, children: model }),
1535
+ effort ? /* @__PURE__ */ jsx2(Text2, { color: palette.muted, children: ` \xB7 thinking ${effort.toLowerCase()}` }) : null,
1513
1536
  separator,
1514
1537
  ui.interactionMode,
1515
1538
  separator,
@@ -1852,24 +1875,14 @@ function PlanView({
1852
1875
  ] });
1853
1876
  }
1854
1877
 
1855
- // src/tui/ink/controller.ts
1856
- import { setWorkspaceTrusted } from "@datalabrotterdam/nova-ai-agent/core/nova-home.js";
1857
- import {
1858
- disabledSkillNames,
1859
- discoverSkills as discoverSkills2,
1860
- setSkillEnabled
1861
- } from "@datalabrotterdam/nova-ai-agent/core/skills.js";
1862
- import { INTERACTION_MODES } from "@datalabrotterdam/nova-ai-agent/core/interaction-modes.js";
1863
-
1864
1878
  // src/tui/commands/builtins.ts
1865
- import { isInteractionMode } from "@datalabrotterdam/nova-ai-agent/core/interaction-modes.js";
1866
1879
  var permissionModes = ["ask", "acceptEdits", "bypassAll"];
1867
1880
  var builtinCommands = [
1868
1881
  {
1869
1882
  name: "help",
1870
1883
  description: "Show available commands",
1871
1884
  run(ctx) {
1872
- const lines = builtinCommands.map((c) => `/${c.name} \u2014 ${c.description}`);
1885
+ const lines = builtinCommands.filter((c) => c.available?.(ctx) ?? true).map((c) => `/${c.name} \u2014 ${c.description}`);
1873
1886
  ctx.print(lines.join("\n"));
1874
1887
  }
1875
1888
  },
@@ -1950,15 +1963,15 @@ var builtinCommands = [
1950
1963
  },
1951
1964
  {
1952
1965
  name: "mode",
1953
- description: "Switch interaction mode (/mode agent|ask|plan)",
1966
+ description: "Switch interaction mode (/mode agent|ask|plan|<agent file>)",
1954
1967
  run(ctx, args) {
1955
1968
  const mode = args.trim();
1956
1969
  if (!mode) {
1957
- ctx.print(`Current mode: ${ctx.getInteractionMode()}`);
1970
+ ctx.print(`Current mode: ${ctx.getInteractionMode()} (available: ${ctx.getInteractionModes().join(", ")})`);
1958
1971
  return;
1959
1972
  }
1960
- if (!isInteractionMode(mode)) {
1961
- ctx.print("Usage: /mode agent|ask|plan");
1973
+ if (!ctx.getInteractionModes().includes(mode)) {
1974
+ ctx.print(`Usage: /mode ${ctx.getInteractionModes().join("|")}`);
1962
1975
  return;
1963
1976
  }
1964
1977
  ctx.setInteractionMode(mode);
@@ -1983,8 +1996,8 @@ var builtinCommands = [
1983
1996
  {
1984
1997
  name: "trust",
1985
1998
  description: "Trust this workspace: start the MCP servers it declares and apply its .nova-ai allow rules",
1986
- run(ctx) {
1987
- ctx.print(ctx.trustWorkspace());
1999
+ async run(ctx) {
2000
+ ctx.print(await ctx.trustWorkspace());
1988
2001
  }
1989
2002
  },
1990
2003
  {
@@ -2000,6 +2013,16 @@ var builtinCommands = [
2000
2013
  ctx.openModelPicker();
2001
2014
  }
2002
2015
  },
2016
+ {
2017
+ name: "effort",
2018
+ description: "Set how hard the model thinks (/effort <level>, or no args for a slider)",
2019
+ available: (ctx) => ctx.hasThinkingLevels(),
2020
+ async run(ctx, args) {
2021
+ const level = args.trim();
2022
+ if (level) ctx.print(await ctx.setEffort(level));
2023
+ else await ctx.openEffortPicker();
2024
+ }
2025
+ },
2003
2026
  {
2004
2027
  name: "tools",
2005
2028
  description: "Inspect tool calls from this session",
@@ -2009,18 +2032,29 @@ var builtinCommands = [
2009
2032
  },
2010
2033
  {
2011
2034
  name: "mcp",
2012
- description: "Inspect configured MCP servers",
2013
- run(ctx) {
2014
- ctx.openMcpInspector();
2035
+ description: "Inspect MCP servers, or sign in to one: /mcp login|logout <server>",
2036
+ async run(ctx, args) {
2037
+ const [action, ...rest] = args.trim().split(/\s+/).filter(Boolean);
2038
+ const name = rest.join(" ");
2039
+ if (!action) {
2040
+ await ctx.openMcpInspector();
2041
+ return;
2042
+ }
2043
+ if (action !== "login" && action !== "logout" || !name) {
2044
+ ctx.print("Usage: /mcp, or /mcp login|logout <server>");
2045
+ return;
2046
+ }
2047
+ if (action === "login") ctx.print(`Signing in to "${name}"\u2026 (waits up to 5 minutes for the browser)`);
2048
+ ctx.print(await ctx.mcpSignIn(name, action === "login"));
2015
2049
  }
2016
2050
  },
2017
2051
  {
2018
2052
  name: "skills",
2019
2053
  description: "List skills, or switch one: /skills on|off <name> [--global]",
2020
- run(ctx, args) {
2054
+ async run(ctx, args) {
2021
2055
  const words = args.trim().split(/\s+/).filter(Boolean);
2022
2056
  if (!words.length) {
2023
- ctx.openSkillInspector();
2057
+ await ctx.openSkillInspector();
2024
2058
  return;
2025
2059
  }
2026
2060
  const global = words.includes("--global");
@@ -2029,7 +2063,7 @@ var builtinCommands = [
2029
2063
  ctx.print("Usage: /skills, or /skills on|off <name> [--global]");
2030
2064
  return;
2031
2065
  }
2032
- ctx.print(ctx.switchSkill(name, action === "on", global));
2066
+ ctx.print(await ctx.switchSkill(name, action === "on", global));
2033
2067
  }
2034
2068
  },
2035
2069
  {
@@ -2198,8 +2232,8 @@ function formatJob(job) {
2198
2232
 
2199
2233
  // src/tui/commands/index.ts
2200
2234
  var COMMANDS = [...builtinCommands];
2201
- function allCommands() {
2202
- return COMMANDS;
2235
+ function allCommands(ctx) {
2236
+ return ctx ? COMMANDS.filter((c) => c.available?.(ctx) ?? true) : COMMANDS;
2203
2237
  }
2204
2238
  function findCommand(name) {
2205
2239
  return COMMANDS.find((c) => c.name === name);
@@ -2417,7 +2451,8 @@ var WorkspaceAutocompleteProvider = class {
2417
2451
  const mention = extractMentionPrefix(before);
2418
2452
  if (mention) return this.mentionSuggestions(mention, options.signal);
2419
2453
  if (cursorLine === 0 && before.startsWith("/") && !/\s/.test(before)) {
2420
- const items2 = filterCommands(this.commands, before.slice(1));
2454
+ const commands = typeof this.commands === "function" ? this.commands() : this.commands;
2455
+ const items2 = filterCommands(commands, before.slice(1));
2421
2456
  return items2.length ? { items: items2, prefix: before } : null;
2422
2457
  }
2423
2458
  if (!options.force) return null;
@@ -2940,6 +2975,40 @@ var SelectionState = class {
2940
2975
  return "changed";
2941
2976
  }
2942
2977
  };
2978
+ var SliderState = class {
2979
+ constructor(levels, selected, onSelect, onCancel) {
2980
+ this.levels = levels;
2981
+ this.selected = selected;
2982
+ this.onSelect = onSelect;
2983
+ this.onCancel = onCancel;
2984
+ }
2985
+ levels;
2986
+ selected;
2987
+ onSelect;
2988
+ onCancel;
2989
+ handleKey(input, key) {
2990
+ if (isCancel(input, key)) {
2991
+ this.onCancel();
2992
+ return "closed";
2993
+ }
2994
+ if (key.return) {
2995
+ const level = this.levels[this.selected];
2996
+ if (!level) return "ignored";
2997
+ this.onSelect(level);
2998
+ return "closed";
2999
+ }
3000
+ const last = this.levels.length - 1;
3001
+ let next = this.selected;
3002
+ if (key.leftArrow || key.downArrow) next = Math.max(0, this.selected - 1);
3003
+ else if (key.rightArrow || key.upArrow) next = Math.min(last, this.selected + 1);
3004
+ else if (key.home) next = 0;
3005
+ else if (key.end) next = last;
3006
+ else return "ignored";
3007
+ if (next === this.selected) return "ignored";
3008
+ this.selected = next;
3009
+ return "changed";
3010
+ }
3011
+ };
2943
3012
  var ScrollState = class {
2944
3013
  constructor(onClose) {
2945
3014
  this.onClose = onClose;
@@ -3188,6 +3257,9 @@ function permissionKey(request, input, key) {
3188
3257
  return "closed";
3189
3258
  }
3190
3259
 
3260
+ // src/tui/ink/controller.ts
3261
+ import { effortLabel } from "@datalabrotterdam/nova-ai-agent/core/model-capabilities.js";
3262
+
3191
3263
  // src/tui/ink/editor/prompt-editor.ts
3192
3264
  var MAX_HISTORY = 200;
3193
3265
  var PromptEditor = class {
@@ -3874,7 +3946,7 @@ var TuiController = class {
3874
3946
  };
3875
3947
  getView = () => this.view;
3876
3948
  start() {
3877
- void this.runner.ensureUsableModel().then(() => this.refresh());
3949
+ void this.runner.prepareSession().then(() => this.refresh());
3878
3950
  this.syncWindowTitle();
3879
3951
  this.animationTimer = setInterval(() => this.syncWindowTitle(true), 120);
3880
3952
  this.animationTimer.unref?.();
@@ -4021,8 +4093,8 @@ var TuiController = class {
4021
4093
  "Models",
4022
4094
  models.map((model) => ({
4023
4095
  value: model.id,
4024
- label: model.id,
4025
- description: model.name ?? void 0
4096
+ label: model.thinking ? `${model.id} \u25C6` : model.id,
4097
+ description: [model.name, model.thinking ? "thinking" : null].filter(Boolean).join(" \xB7 ") || void 0
4026
4098
  })),
4027
4099
  "model-picker",
4028
4100
  (model) => this.setModel(model)
@@ -4031,6 +4103,58 @@ var TuiController = class {
4031
4103
  this.showText("Models", errorMessage(error, "Failed to list models."));
4032
4104
  }
4033
4105
  };
4106
+ effortName() {
4107
+ const level = this.runner.currentEffort();
4108
+ const efforts = level ? this.runner.modelEffortLevels() : [];
4109
+ return level ? effortLabel(level, efforts) : null;
4110
+ }
4111
+ /** The selected model's levels and current one, or why it has none. */
4112
+ async effortLevels() {
4113
+ let info;
4114
+ try {
4115
+ info = await this.runner.modelDetails();
4116
+ } catch (error) {
4117
+ return errorMessage(error, "Could not load the model list.");
4118
+ }
4119
+ const model = this.runner.model || "This model";
4120
+ if (!info?.efforts.length) {
4121
+ return info?.thinking ? `${model} thinks at a fixed depth; there is no level to choose.` : `${model} is not a thinking model; /model lists the ones marked \u25C6.`;
4122
+ }
4123
+ return { efforts: info.efforts, current: this.runner.currentEffort() ?? info.efforts[0] };
4124
+ }
4125
+ /** /effort: a slider over the selected model's levels. */
4126
+ openEffort = async () => {
4127
+ const levels = await this.effortLevels();
4128
+ if (typeof levels === "string") {
4129
+ this.showText("Thinking effort", levels);
4130
+ return;
4131
+ }
4132
+ const state = new SliderState(
4133
+ levels.efforts.map((value) => ({ value, label: effortLabel(value, levels.efforts) })),
4134
+ Math.max(0, levels.efforts.indexOf(levels.current)),
4135
+ (item) => {
4136
+ this.closeOverlay();
4137
+ this.runner.setEffort(item.value);
4138
+ this.store.setState({ statusLine: `Thinking effort: ${item.label}.` });
4139
+ },
4140
+ this.closeOverlay
4141
+ );
4142
+ this.showOverlay({ kind: "slider", title: `Thinking effort \xB7 ${this.runner.model}`, state });
4143
+ };
4144
+ /** /effort <level>: by id ("high") or name ("Extra high", "off"). */
4145
+ setEffort = async (level) => {
4146
+ const levels = await this.effortLevels();
4147
+ if (typeof levels === "string") return levels;
4148
+ const wanted = level.trim().toLowerCase();
4149
+ const value = levels.efforts.find(
4150
+ (candidate) => candidate === wanted || effortLabel(candidate, levels.efforts).toLowerCase() === wanted
4151
+ );
4152
+ const names = levels.efforts.map((candidate) => effortLabel(candidate, levels.efforts).toLowerCase());
4153
+ if (!value) return `${this.runner.model} has these levels: ${names.join(", ")}.`;
4154
+ this.runner.setEffort(value);
4155
+ this.refresh();
4156
+ return `Thinking effort: ${effortLabel(value, levels.efforts)}.`;
4157
+ };
4034
4158
  openPermissionModes = () => {
4035
4159
  const modes = [
4036
4160
  { value: "ask", label: "ask", description: "Prompt before tools make changes" },
@@ -4084,9 +4208,13 @@ ${result.output}${result.truncated ? "\n\n[output truncated]" : ""}`
4084
4208
  state: new ToolInspectorState(() => this.toolCalls(), this.closeOverlay)
4085
4209
  });
4086
4210
  };
4087
- openMcpInspector = () => {
4088
- const servers = this.runner.configuredMcpServers();
4211
+ openMcpInspector = async () => {
4212
+ if (!this.runner.sessionLoaded) {
4213
+ this.showText("MCP servers", "Connecting MCP servers...");
4214
+ await this.runner.prepareSession();
4215
+ }
4089
4216
  const connection = this.runner.mcpSessionStatus();
4217
+ const servers = connection.configured;
4090
4218
  const configuredNames = new Set(servers.map((server) => server.name));
4091
4219
  const configurationFailures = connection.failures.filter((failure) => !configuredNames.has(failure.serverName)).map((failure) => `${failure.serverName}
4092
4220
  status: failed - ${failure.message}`);
@@ -4100,16 +4228,20 @@ status: failed - ${failure.message}`);
4100
4228
  );
4101
4229
  };
4102
4230
  /** Every discovered skill with its state; switched-off skills stay listed. */
4103
- openSkillInspector = () => {
4104
- const cwd = this.runner.cwd;
4105
- const skills = discoverSkills2(cwd);
4106
- const disabled = disabledSkillNames(cwd);
4107
- const on = skills.filter((skill) => !disabled.has(skill.name)).length;
4231
+ openSkillInspector = async () => {
4232
+ let skills;
4233
+ try {
4234
+ skills = await this.runner.listSkills();
4235
+ } catch (error) {
4236
+ this.showText("Skills", errorMessage(error, "Could not list the skills."));
4237
+ return;
4238
+ }
4239
+ const on = skills.filter((skill) => skill.enabled).length;
4108
4240
  this.showText(
4109
4241
  `Skills (${on} on, ${skills.length - on} off)`,
4110
4242
  skills.length ? [
4111
4243
  ...skills.map(
4112
- (skill) => `${skill.name} [${skill.source}]${disabled.has(skill.name) ? " (off)" : ""}
4244
+ (skill) => `${skill.name} [${skill.bundledBy ? `bundled with ${skill.bundledBy}` : skill.source}]${skill.enabled ? "" : " (off)"}
4113
4245
  ${skill.description}
4114
4246
  ${skill.path}`
4115
4247
  ),
@@ -4118,17 +4250,15 @@ ${skill.path}`
4118
4250
  );
4119
4251
  };
4120
4252
  /** /skills on|off <name> [--global]: applies from the next message. */
4121
- switchSkill = (name, enabled, global) => {
4122
- const cwd = this.runner.cwd;
4123
- if (!discoverSkills2(cwd).some((skill) => skill.name === name)) {
4124
- return `No skill named "${name}". /skills lists them.`;
4125
- }
4253
+ switchSkill = async (name, enabled, global) => {
4254
+ let skills;
4126
4255
  try {
4127
- setSkillEnabled(cwd, global ? "global" : "project", name, enabled);
4256
+ skills = await this.runner.setSkillEnabled(name, enabled, global);
4128
4257
  } catch (error) {
4129
- return errorMessage(error, "Could not change the skill.");
4258
+ const message = errorMessage(error, "Could not change the skill.");
4259
+ return message.startsWith("No skill named") ? `${message} /skills lists them.` : message;
4130
4260
  }
4131
- const stillOff = enabled && disabledSkillNames(cwd).has(name);
4261
+ const stillOff = enabled && skills.some((skill) => skill.name === name && !skill.enabled);
4132
4262
  return stillOff ? `Switched "${name}" on ${global ? "globally" : "in this project"}, but it is still off ${global ? "in this project" : "globally"} (use ${global ? "/skills on " + name : "/skills on " + name + " --global"}).` : `Skill "${name}" is ${enabled ? "on" : "off"} ${global ? "everywhere" : "in this project"} from the next message.`;
4133
4263
  };
4134
4264
  openUsageInspector = async () => {
@@ -4182,8 +4312,8 @@ ${skill.path}`
4182
4312
  this.runner.setInteractionMode(mode);
4183
4313
  }
4184
4314
  cycleInteractionMode() {
4185
- const current = INTERACTION_MODES.indexOf(this.runner.interactionMode);
4186
- this.setInteractionMode(INTERACTION_MODES[(current + 1) % INTERACTION_MODES.length]);
4315
+ const modes = this.runner.modeIds;
4316
+ this.setInteractionMode(modes[(modes.indexOf(this.runner.interactionMode) + 1) % modes.length]);
4187
4317
  }
4188
4318
  appendAssistant(text) {
4189
4319
  this.store.setState((state) => ({
@@ -4225,15 +4355,34 @@ ${skill.path}`
4225
4355
  },
4226
4356
  openSessionSwitcher: this.openSessions,
4227
4357
  openModelPicker: () => void this.openModels(),
4358
+ hasThinkingLevels: () => this.runner.hasThinkingLevels(),
4359
+ openEffortPicker: () => this.openEffort(),
4360
+ setEffort: (level) => this.setEffort(level),
4228
4361
  getModel: () => this.runner.model,
4229
4362
  setModel: (model) => this.setModel(model),
4230
4363
  getInteractionMode: () => this.runner.interactionMode,
4364
+ getInteractionModes: () => this.runner.modeIds,
4231
4365
  setInteractionMode: (mode) => this.setInteractionMode(mode),
4232
4366
  getPermissionMode: () => this.store.getState().permissionMode,
4233
4367
  setPermissionMode: (mode) => this.setPermissionMode(mode),
4234
4368
  openPermissionPicker: this.openPermissionModes,
4235
- trustWorkspace: () => {
4236
- setWorkspaceTrusted(this.runner.cwd, true);
4369
+ mcpSignIn: async (serverName, signIn) => {
4370
+ try {
4371
+ const status = await this.runner.mcpSignIn(serverName, signIn);
4372
+ this.refresh();
4373
+ const failure = status.failures.find((item) => item.serverName === serverName);
4374
+ if (!signIn) return `Signed out of "${serverName}".`;
4375
+ return failure ? `Signed in, but "${serverName}" did not connect: ${failure.message}` : `Signed in to "${serverName}"; its tools are available from the next message.`;
4376
+ } catch (error) {
4377
+ return errorMessage(error, signIn ? "Sign-in failed." : "Sign-out failed.");
4378
+ }
4379
+ },
4380
+ trustWorkspace: async () => {
4381
+ try {
4382
+ await this.runner.trustWorkspace();
4383
+ } catch (error) {
4384
+ return errorMessage(error, "Could not trust the workspace.");
4385
+ }
4237
4386
  return "Workspace trusted: its allow rules apply now; MCP servers it declares start with the next session (/clear).";
4238
4387
  },
4239
4388
  openToolInspector: this.openToolInspector,
@@ -4334,7 +4483,7 @@ ${skill.path}`
4334
4483
  const fdPath = resolveFdPath();
4335
4484
  editor.setAutocompleteProvider(
4336
4485
  new WorkspaceAutocompleteProvider(
4337
- allCommands().map((command) => ({
4486
+ () => allCommands(this.commandContext).map((command) => ({
4338
4487
  name: command.name,
4339
4488
  description: command.description
4340
4489
  })),
@@ -4375,6 +4524,7 @@ ${skill.path}`
4375
4524
  overlay: this.overlay,
4376
4525
  question: this.questionState(ui.pendingQuestion),
4377
4526
  model: this.runner.model,
4527
+ effort: this.effortName(),
4378
4528
  mcp: mcpCounts(this.runner),
4379
4529
  expandedTools: this.expandedTools,
4380
4530
  exiting: this.exiting,
@@ -4410,18 +4560,16 @@ ${skill.path}`
4410
4560
  }
4411
4561
  };
4412
4562
  function formatMcpServer(server, status) {
4413
- const type = "type" in server ? server.type : "stdio";
4414
- const lines = [`${server.name} [${type}]`];
4415
- if ("url" in server) lines.push(`url: ${server.url}`);
4416
- if ("command" in server) {
4563
+ const lines = [`${server.name} [${server.transport}]`];
4564
+ if (server.url) lines.push(`url: ${server.url}`);
4565
+ if (server.command) {
4417
4566
  lines.push(`command: ${server.command}`);
4418
- if (server.args.length) lines.push(`args: ${server.args.join(" ")}`);
4419
- if (server.env.length)
4420
- lines.push(`env: ${server.env.map((entry) => entry.name).join(", ")} (values hidden)`);
4567
+ if (server.args?.length) lines.push(`args: ${server.args.join(" ")}`);
4568
+ if (server.env?.length) lines.push(`env: ${server.env.join(", ")} (values hidden)`);
4421
4569
  }
4422
4570
  const failure = status.failures.find((item) => item.serverName === server.name);
4423
4571
  lines.push(
4424
- failure ? `status: failed - ${failure.message}` : status.connected.includes(server.name) ? "status: connected" : "status: configured; connection starts with the ACP session"
4572
+ failure?.needsAuth ? `status: needs sign-in - /mcp login ${server.name}` : failure ? `status: failed - ${failure.message}` : status.connected.includes(server.name) ? "status: connected" : "status: configured; connection starts with the ACP session"
4425
4573
  );
4426
4574
  return lines.join("\n");
4427
4575
  }
@@ -4460,6 +4608,25 @@ function Frame({ title, children, footer }) {
4460
4608
  footer ? /* @__PURE__ */ jsx5(Text5, { color: palette.faint, wrap: "truncate-end", children: footer }) : null
4461
4609
  ] });
4462
4610
  }
4611
+ function SliderDialog({
4612
+ title,
4613
+ state,
4614
+ isActive = true
4615
+ }) {
4616
+ useDialogInput((input, key) => state.handleKey(input, key), isActive);
4617
+ const current = state.levels[state.selected];
4618
+ return /* @__PURE__ */ jsxs5(Frame, { title, footer: "\u2190\u2192 adjust \xB7 Enter apply \xB7 Esc cancel", children: [
4619
+ /* @__PURE__ */ jsx5(Text5, { wrap: "truncate-end", children: state.levels.map((level, index) => {
4620
+ const selected = index === state.selected;
4621
+ return /* @__PURE__ */ jsxs5(Text5, { children: [
4622
+ index > 0 ? /* @__PURE__ */ jsx5(Text5, { color: palette.faint, children: " \u2500\u2500 " }) : null,
4623
+ /* @__PURE__ */ jsx5(Text5, { color: selected ? palette.primary : palette.faint, children: selected ? "\u25CF " : "\u25CB " }),
4624
+ /* @__PURE__ */ jsx5(Text5, { bold: selected, color: selected ? void 0 : palette.muted, children: level.label })
4625
+ ] }, level.value);
4626
+ }) }),
4627
+ current?.description ? /* @__PURE__ */ jsx5(Text5, { color: palette.muted, children: current.description }) : null
4628
+ ] });
4629
+ }
4463
4630
  function SelectionDialog({
4464
4631
  title,
4465
4632
  state,
@@ -4848,7 +5015,7 @@ function App({ controller }) {
4848
5015
  ),
4849
5016
  /* @__PURE__ */ jsxs6(Box6, { ref: bottomRef, flexDirection: "column", flexShrink: 0, children: [
4850
5017
  /* @__PURE__ */ jsx6(Surface, { controller, view, rows: Math.max(6, rows - 4), columns }),
4851
- /* @__PURE__ */ jsx6(StatusLine, { ui: view.ui, model: view.model, mcp: view.mcp, scroll: view.scroll })
5018
+ /* @__PURE__ */ jsx6(StatusLine, { ui: view.ui, model: view.model, effort: view.effort, mcp: view.mcp, scroll: view.scroll })
4852
5019
  ] })
4853
5020
  ] });
4854
5021
  }
@@ -4891,6 +5058,7 @@ function Surface({
4891
5058
  const overlay = view.overlay;
4892
5059
  if (overlay?.kind === "selection")
4893
5060
  return /* @__PURE__ */ jsx6(SelectionDialog, { title: overlay.title, state: overlay.state, height: rows });
5061
+ if (overlay?.kind === "slider") return /* @__PURE__ */ jsx6(SliderDialog, { title: overlay.title, state: overlay.state });
4894
5062
  if (overlay?.kind === "text")
4895
5063
  return /* @__PURE__ */ jsx6(
4896
5064
  ScrollPanel,
@@ -4937,19 +5105,14 @@ function useWorking(view) {
4937
5105
  }
4938
5106
 
4939
5107
  // src/tui/index.ts
5108
+ import { workspaceNeedsTrust } from "@datalabrotterdam/nova-ai-agent/core/workspace-mcp.js";
4940
5109
  async function runChat(options) {
4941
5110
  const stored = readCredentials();
4942
5111
  if (!stored) {
4943
5112
  console.error("nova-ai isn't connected to a Nova AI account yet. Run `nova-ai login` first.");
4944
5113
  return 1;
4945
5114
  }
4946
- const model = options.model ?? stored.defaultModel ?? process.env.NOVA_MODEL ?? await pickDefaultModel(stored);
4947
- if (!model) {
4948
- console.error("nova-ai has no model to use yet. Start it with one, for example:");
4949
- console.error(" nova-ai --model <model-id> (or set NOVA_MODEL)");
4950
- console.error("Then Ctrl+P in nova-ai switches and remembers the model.");
4951
- return 1;
4952
- }
5115
+ const model = options.model ?? stored.defaultModel ?? process.env.NOVA_MODEL;
4953
5116
  const credentials = { ...stored, defaultModel: model };
4954
5117
  const updateCheck = checkForUpdate().then((update) => withInstallability(update));
4955
5118
  const updatedFrom = process.env[UPDATED_FROM_ENV];
@@ -4985,19 +5148,6 @@ async function runChat(options) {
4985
5148
  }
4986
5149
  return 0;
4987
5150
  }
4988
- async function pickDefaultModel(credentials) {
4989
- try {
4990
- const { data } = await createNovaClient2(credentials.apiKey).models.list({ limit: 100 });
4991
- const model = resolveDefaultModel2(data, void 0);
4992
- if (model) {
4993
- saveDefaultModel2(model);
4994
- console.log(`Using model ${model} (Ctrl+P switches).`);
4995
- }
4996
- return model;
4997
- } catch {
4998
- return void 0;
4999
- }
5000
- }
5001
5151
  async function askToTrust(cwd) {
5002
5152
  const prompt = createInterface2({ input: process.stdin, output: process.stdout });
5003
5153
  try {
@@ -5007,7 +5157,7 @@ ${cwd}`);
5007
5157
  console.log("Trusting it starts those servers and applies those rules. Only trust code you know.");
5008
5158
  const answer = (await prompt.question("Trust this workspace? [y/N] ")).trim().toLowerCase();
5009
5159
  if (answer === "y" || answer === "yes") {
5010
- setWorkspaceTrusted2(cwd, true);
5160
+ setWorkspaceTrusted(cwd, true);
5011
5161
  } else {
5012
5162
  console.log("Not trusted: its MCP servers stay off and its allow rules are ignored. Use /trust later to change this.");
5013
5163
  }