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

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/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ # [@datalabrotterdam/nova-ai-cli-v1.2.0-alpha.6](https://github.com/Datalab-Rotterdam/nova-ai-cli/compare/@datalabrotterdam/nova-ai-cli@1.2.0-alpha.5...@datalabrotterdam/nova-ai-cli@1.2.0-alpha.6) (2026-10-23)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **cli:** start the session before the first prompt ([0c26456](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/0c26456b8126e3b42a6de2e38212213ae5380e62))
7
+
8
+
9
+ ### Features
10
+
11
+ * **agent:** set workspace trust over ACP ([cec4a89](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/cec4a8949a4232f46448219e4d5363289651215d))
12
+
1
13
  # [@datalabrotterdam/nova-ai-cli-v1.2.0-alpha.5](https://github.com/Datalab-Rotterdam/nova-ai-cli/compare/@datalabrotterdam/nova-ai-cli@1.2.0-alpha.4...@datalabrotterdam/nova-ai-cli@1.2.0-alpha.5) (2026-10-23)
2
14
 
3
15
 
@@ -1,147 +1,3 @@
1
- // src/tui/settings/workspace-mcp.ts
2
- import { readFileSync as readFileSync2 } from "fs";
3
- import { join as join2 } from "path";
4
- import { isWorkspaceTrusted } from "@datalabrotterdam/nova-ai-agent/core/nova-home.js";
5
- import { readSettings } from "@datalabrotterdam/nova-ai-agent/core/policy/settings.js";
6
-
7
- // src/tui/settings/workspace-settings.ts
8
- import { readFileSync } from "fs";
9
- import { join } from "path";
10
- function settingsPath(cwd) {
11
- return join(cwd, ".nova-ai", "settings.json");
12
- }
13
- function readWorkspaceSettings(cwd) {
14
- try {
15
- const raw = readFileSync(settingsPath(cwd), "utf8");
16
- return JSON.parse(raw);
17
- } catch {
18
- return {};
19
- }
20
- }
21
-
22
- // src/tui/settings/workspace-mcp.ts
23
- var UNTRUSTED_MCP_MESSAGE = "Not started: MCP servers declared by this workspace only run once you trust it (/trust in the TUI, --trust-workspace headless).";
24
- function readWorkspaceMcpConfiguration(cwd, options = {}) {
25
- const configured = readWorkspaceSettings(cwd).mcpServers ?? [];
26
- const project = readProjectMcpConfiguration(cwd);
27
- const servers = new Map(configured.map((server) => [server.name, server]));
28
- for (const server of project.servers) servers.set(server.name, server);
29
- const declared = [...servers.values()];
30
- if (declared.length && !(options.trusted ?? isWorkspaceTrusted(cwd))) {
31
- return {
32
- servers: [],
33
- failures: [
34
- ...project.failures,
35
- ...declared.map((server) => ({ serverName: server.name, message: UNTRUSTED_MCP_MESSAGE }))
36
- ],
37
- untrusted: declared.map((server) => server.name)
38
- };
39
- }
40
- return { servers: declared, failures: project.failures };
41
- }
42
- function workspaceNeedsTrust(cwd) {
43
- if (isWorkspaceTrusted(cwd)) return false;
44
- if (readWorkspaceMcpConfiguration(cwd, { trusted: true }).servers.length) return true;
45
- return loadWorkspaceAllowRules(cwd).length > 0;
46
- }
47
- function loadWorkspaceAllowRules(cwd) {
48
- return ["settings.json", "settings.local.json"].flatMap((name) => {
49
- const allow = readSettings(join2(cwd, ".nova-ai", name))?.permissions?.allow;
50
- return Array.isArray(allow) ? allow : [];
51
- });
52
- }
53
- function readProjectMcpConfiguration(cwd) {
54
- const path = join2(cwd, ".mcp.json");
55
- let raw;
56
- try {
57
- raw = readFileSync2(path, "utf8");
58
- } catch (error) {
59
- if (error.code === "ENOENT") return { servers: [], failures: [] };
60
- return { servers: [], failures: [{ serverName: ".mcp.json", message: errorMessage(error, "Could not read file") }] };
61
- }
62
- let document;
63
- try {
64
- document = JSON.parse(raw);
65
- } catch (error) {
66
- return { servers: [], failures: [{ serverName: ".mcp.json", message: errorMessage(error, "Invalid JSON") }] };
67
- }
68
- const root = asRecord(document);
69
- const definitions = root ? asRecord(root.mcpServers) : null;
70
- if (!definitions) {
71
- return {
72
- servers: [],
73
- failures: [{ serverName: ".mcp.json", message: 'Expected an "mcpServers" object.' }]
74
- };
75
- }
76
- const servers = [];
77
- const failures = [];
78
- for (const [name, value] of Object.entries(definitions)) {
79
- const config = asRecord(value);
80
- if (config?.disabled === true || config?.enabled === false) continue;
81
- try {
82
- servers.push(normalizeProjectServer(name, config));
83
- } catch (error) {
84
- failures.push({ serverName: `.mcp.json:${name}`, message: errorMessage(error, "Invalid server configuration") });
85
- }
86
- }
87
- return { servers, failures };
88
- }
89
- function normalizeProjectServer(name, config) {
90
- if (!config) throw new Error("Server definition must be an object.");
91
- const type = typeof config.type === "string" ? config.type : config.url ? "http" : "stdio";
92
- if (type === "http" || type === "sse") {
93
- if (typeof config.url !== "string" || !config.url) throw new Error(`${type} server requires a URL.`);
94
- try {
95
- new URL(config.url);
96
- } catch {
97
- throw new Error(`${type} server requires a valid URL.`);
98
- }
99
- return {
100
- name,
101
- type,
102
- url: config.url,
103
- headers: normalizePairs(config.headers, "headers")
104
- };
105
- }
106
- if (type !== "stdio") throw new Error(`Unsupported transport type: ${type}.`);
107
- if (typeof config.command !== "string" || !config.command) throw new Error("stdio server requires a command.");
108
- if (config.args !== void 0 && (!Array.isArray(config.args) || !config.args.every((arg) => typeof arg === "string"))) {
109
- throw new Error("args must be an array of strings.");
110
- }
111
- return {
112
- name,
113
- command: config.command,
114
- args: config.args ?? [],
115
- env: normalizePairs(config.env, "env")
116
- };
117
- }
118
- function normalizePairs(value, label) {
119
- if (value === void 0) return [];
120
- if (Array.isArray(value)) {
121
- if (!value.every((entry) => {
122
- const pair = asRecord(entry);
123
- return typeof pair?.name === "string" && typeof pair.value === "string";
124
- })) {
125
- throw new Error(`${label} must contain name/value strings.`);
126
- }
127
- return value.map((entry) => {
128
- const pair = entry;
129
- return { name: pair.name, value: pair.value };
130
- });
131
- }
132
- const record = asRecord(value);
133
- if (!record || !Object.values(record).every((entry) => typeof entry === "string")) {
134
- throw new Error(`${label} must be an object of string values.`);
135
- }
136
- return Object.entries(record).map(([name, entry]) => ({ name, value: entry }));
137
- }
138
- function asRecord(value) {
139
- return value && typeof value === "object" && !Array.isArray(value) ? value : null;
140
- }
141
- function errorMessage(error, fallback) {
142
- return error instanceof Error && error.message ? `${fallback}: ${error.message}` : fallback;
143
- }
144
-
145
1
  // src/tui/session/tui-acp-client.ts
146
2
  import { NOVA_NOTIFICATIONS } from "@datalabrotterdam/nova-ai-agent/acp/extensions.js";
147
3
  import { readFile, writeFile, mkdir } from "fs/promises";
@@ -746,8 +602,6 @@ function createStore(initial) {
746
602
  }
747
603
 
748
604
  export {
749
- readWorkspaceMcpConfiguration,
750
- workspaceNeedsTrust,
751
605
  TuiAcpClient,
752
606
  choosePermissionOption,
753
607
  createStore
@@ -1,9 +1,8 @@
1
1
  import {
2
2
  TuiAcpClient,
3
3
  choosePermissionOption,
4
- createStore,
5
- readWorkspaceMcpConfiguration
6
- } from "./chunk-NHGIUEIR.js";
4
+ createStore
5
+ } from "./chunk-QSNU4H55.js";
7
6
 
8
7
  // src/headless/index.ts
9
8
  import { resolve } from "path";
@@ -12,7 +11,6 @@ import * as acp from "@agentclientprotocol/sdk";
12
11
  import { NovaAgent } from "@datalabrotterdam/nova-ai-agent/acp/agent.js";
13
12
  import { clientFromContext } from "@datalabrotterdam/nova-ai-agent/client/context-client.js";
14
13
  import { startInProcessAgent } from "@datalabrotterdam/nova-ai-agent/client/in-process.js";
15
- import { setWorkspaceTrusted } from "@datalabrotterdam/nova-ai-agent/core/nova-home.js";
16
14
  import { listStoredSessions, loadStoredSession } from "@datalabrotterdam/nova-ai-agent/core/sessions.js";
17
15
 
18
16
  // src/headless/client.ts
@@ -191,15 +189,19 @@ ${HEADLESS_HELP}
191
189
  dependencies.agent ?? new NovaAgent()
192
190
  );
193
191
  const agent = connection.client;
194
- if (options.trustWorkspace) setWorkspaceTrusted(options.cwd, true);
195
- const mcpConfiguration = options.mcp ? readWorkspaceMcpConfiguration(options.cwd) : { servers: [], failures: [] };
196
- for (const failure of mcpConfiguration.failures) {
197
- output.write({
198
- type: "mcp.configuration_error",
199
- serverName: failure.serverName,
200
- message: failure.message
201
- });
202
- }
192
+ const sessionMeta = { "nova-ai-cli/workspaceMcp": options.mcp };
193
+ const reportMcpFailures = (meta) => {
194
+ const failures = toRecord(toRecord(meta)?.["nova-ai-cli/mcp"])?.failures;
195
+ for (const failure of Array.isArray(failures) ? failures : []) {
196
+ const item = toRecord(failure);
197
+ if (typeof item?.serverName !== "string" || typeof item.message !== "string") continue;
198
+ output.write({
199
+ type: "mcp.configuration_error",
200
+ serverName: item.serverName,
201
+ message: item.message
202
+ });
203
+ }
204
+ };
203
205
  let sessionId = null;
204
206
  let interrupted = false;
205
207
  const onInterrupt = () => {
@@ -215,18 +217,23 @@ ${HEADLESS_HELP}
215
217
  clientCapabilities: client.capabilities,
216
218
  clientInfo: { name: "nova-ai-cli-headless", version: "1" }
217
219
  });
220
+ if (options.trustWorkspace) await agent.workspaceTrust({ cwd: options.cwd, trusted: true });
218
221
  if (options.resume) {
219
- await agent.resumeSession({
222
+ const resumed = await agent.resumeSession({
220
223
  sessionId: options.resume,
221
224
  cwd: options.cwd,
222
- mcpServers: mcpConfiguration.servers
225
+ mcpServers: [],
226
+ _meta: sessionMeta
223
227
  });
228
+ reportMcpFailures(resumed._meta);
224
229
  sessionId = options.resume;
225
230
  } else {
226
231
  const created = await agent.newSession({
227
232
  cwd: options.cwd,
228
- mcpServers: mcpConfiguration.servers
233
+ mcpServers: [],
234
+ _meta: sessionMeta
229
235
  });
236
+ reportMcpFailures(created._meta);
230
237
  sessionId = created.sessionId;
231
238
  }
232
239
  await agent.setSessionConfigOption({
package/dist/index.js CHANGED
@@ -140,7 +140,7 @@ ${MAIN_HELP}
140
140
  }
141
141
  case "print": {
142
142
  const update = startUpdateCheck();
143
- const { default: runHeadless } = await import("./headless-OFJFVJ3O.js");
143
+ const { default: runHeadless } = await import("./headless-C36E4XSQ.js");
144
144
  const code = await runHeadless(command.args);
145
145
  if (update) await noticeUpdate(update, 500);
146
146
  return code;
@@ -175,7 +175,7 @@ ${MAIN_HELP}
175
175
  );
176
176
  return 2;
177
177
  }
178
- const { default: runChat } = await import("./tui-4XTC6VJP.js");
178
+ const { default: runChat } = await import("./tui-TRADKND7.js");
179
179
  return runChat(options);
180
180
  }
181
181
  }
@@ -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-QSNU4H55.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,10 +119,6 @@ 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";
@@ -138,15 +127,6 @@ import {
138
127
  savePermissionMode
139
128
  } from "@datalabrotterdam/nova-ai-agent/core/policy/settings.js";
140
129
  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";
150
130
 
151
131
  // src/tui/files/file-mentions.ts
152
132
  import { statSync } from "fs";
@@ -291,15 +271,6 @@ var SessionRunner = class {
291
271
  constructor(store, credentials, cwd) {
292
272
  this.store = store;
293
273
  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
274
  this.cwd = cwd;
304
275
  this.acpClient = new TuiAcpClient(
305
276
  store,
@@ -314,13 +285,7 @@ var SessionRunner = class {
314
285
  clientCapabilities: this.acpClient.capabilities,
315
286
  clientInfo: { name: "nova-ai-cli-tui", version: "1" }
316
287
  });
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;
288
+ this.model = credentials.defaultModel ?? process.env.NOVA_MODEL ?? "";
324
289
  this.sessionId = crypto.randomUUID();
325
290
  this.sessionReady = Promise.resolve();
326
291
  }
@@ -337,15 +302,12 @@ var SessionRunner = class {
337
302
  connection;
338
303
  nova;
339
304
  initialized;
340
- novaClient;
341
- mcpServers = [];
342
- mcpConfigurationFailures = [];
343
305
  mcpStatus = {
344
306
  configured: [],
345
307
  connected: [],
346
308
  failures: []
347
309
  };
348
- skills;
310
+ skills = [];
349
311
  sessionReady;
350
312
  agentSessionLoaded = false;
351
313
  promptActive = false;
@@ -354,9 +316,6 @@ var SessionRunner = class {
354
316
  queuedAttachments = /* @__PURE__ */ new Map();
355
317
  editingQueuedMessageId = null;
356
318
  imageSupportByModel = /* @__PURE__ */ new Map();
357
- contextWindow = null;
358
- contextWindowModel = null;
359
- contextWindowRequest = 0;
360
319
  contextUsageRequest = 0;
361
320
  modelCheck = null;
362
321
  resumeFrom(storedSessionId) {
@@ -369,15 +328,7 @@ var SessionRunner = class {
369
328
  this.sessionId = stored.sessionId;
370
329
  this.cwd = stored.cwd;
371
330
  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
- );
331
+ this.skills = [];
381
332
  const messages = restoreSessionMessages(stored.messages);
382
333
  this.store.setState({
383
334
  messages,
@@ -396,7 +347,7 @@ var SessionRunner = class {
396
347
  return await this.nova.loadSession({
397
348
  sessionId: stored.sessionId,
398
349
  cwd: stored.cwd,
399
- mcpServers: this.mcpServers
350
+ mcpServers: []
400
351
  });
401
352
  } finally {
402
353
  this.acpClient.muteSessionUpdates = false;
@@ -521,9 +472,6 @@ var SessionRunner = class {
521
472
  void this.enqueueQueuedPrompt(value, [], [], "steer", true);
522
473
  return true;
523
474
  }
524
- configuredMcpServers() {
525
- return [...this.mcpServers];
526
- }
527
475
  mcpSessionStatus() {
528
476
  return {
529
477
  configured: this.mcpStatus.configured.map((server) => ({ ...server })),
@@ -534,27 +482,29 @@ var SessionRunner = class {
534
482
  skillSessionStatus() {
535
483
  return this.skills.map((skill) => ({ ...skill }));
536
484
  }
485
+ async trustWorkspace() {
486
+ await this.initialized;
487
+ await this.nova.workspaceTrust({ cwd: this.cwd, trusted: true });
488
+ }
489
+ /** Every skill of the workspace with its on/off state, from the agent. */
490
+ async listSkills() {
491
+ await this.initialized;
492
+ return (await this.nova.skillsList({ cwd: this.cwd })).skills;
493
+ }
494
+ async setSkillEnabled(name, enabled, global) {
495
+ await this.initialized;
496
+ return (await this.nova.skillsSetEnabled({
497
+ cwd: this.cwd,
498
+ name,
499
+ enabled,
500
+ scope: global ? "global" : "project"
501
+ })).skills;
502
+ }
537
503
  contextUsageSnapshot() {
538
504
  return this.store.getState().contextUsage;
539
505
  }
540
506
  async refreshContextUsage() {
541
507
  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
508
  const usage = await this.updateContextUsage();
559
509
  if (!usage)
560
510
  throw new Error(
@@ -570,13 +520,8 @@ var SessionRunner = class {
570
520
  }
571
521
  setModel(model) {
572
522
  this.model = model;
573
- this.contextWindow = null;
574
- this.contextWindowModel = null;
575
- this.contextWindowRequest++;
576
523
  saveDefaultModel(model);
577
524
  if (this.agentSessionLoaded) void this.updateContextUsage();
578
- void this.refreshContextUsage().catch(() => {
579
- });
580
525
  }
581
526
  /**
582
527
  * Hands the mode to the agent's permission policy (which decides) and
@@ -634,27 +579,30 @@ var SessionRunner = class {
634
579
  }
635
580
  /** Chat models only: embedding, speech and transcription models can't run a session. */
636
581
  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
- }));
582
+ await this.initialized;
583
+ const { models } = await this.nova.modelsList();
584
+ for (const model of models) this.imageSupportByModel.set(model.id, model.imageInput);
585
+ return models.map((model) => ({ id: model.id, name: model.name }));
642
586
  }
643
587
  /**
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.
588
+ * Takes the account's default model when none is set, and replaces a saved
589
+ * one that cannot chat (older logins saved the first model of the list,
590
+ * e.g. an embedding model); the agent decides, the choice is saved.
646
591
  * Runs once; a failed lookup keeps the model and the prompt reports errors.
647
592
  */
648
593
  ensureUsableModel() {
649
594
  this.modelCheck ??= (async () => {
650
595
  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;
596
+ await this.initialized;
597
+ const { models, defaultModel } = await this.nova.modelsList(
598
+ this.model ? { preferred: this.model } : {}
599
+ );
600
+ for (const model of models) this.imageSupportByModel.set(model.id, model.imageInput);
601
+ if (!defaultModel || defaultModel === this.model) return;
654
602
  const previous = this.model;
655
- this.setModel(model);
603
+ this.setModel(defaultModel);
656
604
  this.store.setState({
657
- statusLine: `${previous} is not a chat model; switched to ${model} (Ctrl+P to pick another).`
605
+ statusLine: previous ? `${previous} is not a chat model; switched to ${defaultModel} (Ctrl+P to pick another).` : `Using model ${defaultModel} (Ctrl+P switches).`
658
606
  });
659
607
  } catch {
660
608
  }
@@ -662,14 +610,8 @@ var SessionRunner = class {
662
610
  return this.modelCheck;
663
611
  }
664
612
  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;
613
+ if (!this.imageSupportByModel.has(model)) await this.listModels();
614
+ return this.imageSupportByModel.get(model) ?? false;
673
615
  }
674
616
  async compactContext() {
675
617
  if (this.store.getState().busy || this.promptActive) {
@@ -793,7 +735,7 @@ var SessionRunner = class {
793
735
  });
794
736
  }
795
737
  } 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.";
738
+ const message = err instanceof Error ? err.message : "Unknown error.";
797
739
  pendingToolFailure = `Request failed: ${message}`;
798
740
  this.acpClient.appendError(message);
799
741
  this.store.setState({ statusLine: "Request failed." });
@@ -947,11 +889,10 @@ var SessionRunner = class {
947
889
  ];
948
890
  }
949
891
  async createSession() {
950
- this.loadMcpConfiguration(this.cwd);
951
892
  await this.initialized;
952
893
  const { sessionId, _meta } = await this.nova.newSession({
953
894
  cwd: this.cwd,
954
- mcpServers: this.mcpServers
895
+ mcpServers: []
955
896
  });
956
897
  this.sessionId = sessionId;
957
898
  this.applyMcpStatus(_meta);
@@ -975,10 +916,20 @@ var SessionRunner = class {
975
916
  const value = meta?.["nova-ai-cli/mcp"];
976
917
  if (!value || typeof value !== "object") return;
977
918
  const record = value;
919
+ const strings = (value2) => Array.isArray(value2) ? value2.filter((entry) => typeof entry === "string") : void 0;
978
920
  const configured = Array.isArray(record.configured) ? record.configured.flatMap((entry) => {
979
921
  if (!entry || typeof entry !== "object") return [];
980
922
  const item = entry;
981
- return typeof item.name === "string" && typeof item.transport === "string" ? [{ name: item.name, transport: item.transport }] : [];
923
+ return typeof item.name === "string" && typeof item.transport === "string" ? [
924
+ {
925
+ name: item.name,
926
+ transport: item.transport,
927
+ ...typeof item.url === "string" ? { url: item.url } : {},
928
+ ...typeof item.command === "string" ? { command: item.command } : {},
929
+ ...strings(item.args) ? { args: strings(item.args) } : {},
930
+ ...strings(item.env) ? { env: strings(item.env) } : {}
931
+ }
932
+ ] : [];
982
933
  }) : [];
983
934
  const connected = Array.isArray(record.connected) ? record.connected.filter(
984
935
  (entry) => typeof entry === "string"
@@ -988,24 +939,7 @@ var SessionRunner = class {
988
939
  const item = entry;
989
940
  return typeof item.serverName === "string" && typeof item.message === "string" ? [{ serverName: item.serverName, message: item.message }] : [];
990
941
  }) : [];
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
- };
942
+ this.mcpStatus = { configured, connected, failures };
1009
943
  }
1010
944
  applySkillStatus(meta) {
1011
945
  const value = meta?.["nova-ai-cli/skills"];
@@ -1023,12 +957,28 @@ var SessionRunner = class {
1023
957
  ] : [];
1024
958
  });
1025
959
  }
960
+ /**
961
+ * Starts the session ahead of the first prompt, so the agent's MCP and
962
+ * skill status is there for /mcp and the status line. A failure is left to
963
+ * the first prompt, which tries again and reports it.
964
+ */
965
+ async prepareSession() {
966
+ try {
967
+ await this.ensureSession();
968
+ } catch {
969
+ this.sessionReady = Promise.resolve();
970
+ }
971
+ }
972
+ get sessionLoaded() {
973
+ return this.agentSessionLoaded;
974
+ }
1026
975
  async ensureSession() {
1027
976
  await this.ensureUsableModel();
1028
- await this.sessionReady;
1029
- if (!this.agentSessionLoaded) {
1030
- this.sessionReady = this.createSession();
1031
- await this.sessionReady;
977
+ for (; ; ) {
978
+ const ready = this.sessionReady;
979
+ await ready;
980
+ if (this.agentSessionLoaded) return;
981
+ if (this.sessionReady === ready) this.sessionReady = this.createSession();
1032
982
  }
1033
983
  }
1034
984
  applyInteractionMode(mode, statusLine) {
@@ -1044,7 +994,7 @@ var SessionRunner = class {
1044
994
  try {
1045
995
  const usage = await this.nova.sessionContextUsage({
1046
996
  sessionId,
1047
- contextWindow: this.contextWindow,
997
+ ...this.model ? { model: this.model } : {},
1048
998
  mode: this.interactionMode
1049
999
  });
1050
1000
  if (request === this.contextUsageRequest && sessionId === this.sessionId) {
@@ -1853,12 +1803,6 @@ function PlanView({
1853
1803
  }
1854
1804
 
1855
1805
  // 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
1806
  import { INTERACTION_MODES } from "@datalabrotterdam/nova-ai-agent/core/interaction-modes.js";
1863
1807
 
1864
1808
  // src/tui/commands/builtins.ts
@@ -1983,8 +1927,8 @@ var builtinCommands = [
1983
1927
  {
1984
1928
  name: "trust",
1985
1929
  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());
1930
+ async run(ctx) {
1931
+ ctx.print(await ctx.trustWorkspace());
1988
1932
  }
1989
1933
  },
1990
1934
  {
@@ -2011,16 +1955,16 @@ var builtinCommands = [
2011
1955
  name: "mcp",
2012
1956
  description: "Inspect configured MCP servers",
2013
1957
  run(ctx) {
2014
- ctx.openMcpInspector();
1958
+ return ctx.openMcpInspector();
2015
1959
  }
2016
1960
  },
2017
1961
  {
2018
1962
  name: "skills",
2019
1963
  description: "List skills, or switch one: /skills on|off <name> [--global]",
2020
- run(ctx, args) {
1964
+ async run(ctx, args) {
2021
1965
  const words = args.trim().split(/\s+/).filter(Boolean);
2022
1966
  if (!words.length) {
2023
- ctx.openSkillInspector();
1967
+ await ctx.openSkillInspector();
2024
1968
  return;
2025
1969
  }
2026
1970
  const global = words.includes("--global");
@@ -2029,7 +1973,7 @@ var builtinCommands = [
2029
1973
  ctx.print("Usage: /skills, or /skills on|off <name> [--global]");
2030
1974
  return;
2031
1975
  }
2032
- ctx.print(ctx.switchSkill(name, action === "on", global));
1976
+ ctx.print(await ctx.switchSkill(name, action === "on", global));
2033
1977
  }
2034
1978
  },
2035
1979
  {
@@ -3874,7 +3818,7 @@ var TuiController = class {
3874
3818
  };
3875
3819
  getView = () => this.view;
3876
3820
  start() {
3877
- void this.runner.ensureUsableModel().then(() => this.refresh());
3821
+ void this.runner.prepareSession().then(() => this.refresh());
3878
3822
  this.syncWindowTitle();
3879
3823
  this.animationTimer = setInterval(() => this.syncWindowTitle(true), 120);
3880
3824
  this.animationTimer.unref?.();
@@ -4084,9 +4028,13 @@ ${result.output}${result.truncated ? "\n\n[output truncated]" : ""}`
4084
4028
  state: new ToolInspectorState(() => this.toolCalls(), this.closeOverlay)
4085
4029
  });
4086
4030
  };
4087
- openMcpInspector = () => {
4088
- const servers = this.runner.configuredMcpServers();
4031
+ openMcpInspector = async () => {
4032
+ if (!this.runner.sessionLoaded) {
4033
+ this.showText("MCP servers", "Connecting MCP servers...");
4034
+ await this.runner.prepareSession();
4035
+ }
4089
4036
  const connection = this.runner.mcpSessionStatus();
4037
+ const servers = connection.configured;
4090
4038
  const configuredNames = new Set(servers.map((server) => server.name));
4091
4039
  const configurationFailures = connection.failures.filter((failure) => !configuredNames.has(failure.serverName)).map((failure) => `${failure.serverName}
4092
4040
  status: failed - ${failure.message}`);
@@ -4100,16 +4048,20 @@ status: failed - ${failure.message}`);
4100
4048
  );
4101
4049
  };
4102
4050
  /** 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;
4051
+ openSkillInspector = async () => {
4052
+ let skills;
4053
+ try {
4054
+ skills = await this.runner.listSkills();
4055
+ } catch (error) {
4056
+ this.showText("Skills", errorMessage(error, "Could not list the skills."));
4057
+ return;
4058
+ }
4059
+ const on = skills.filter((skill) => skill.enabled).length;
4108
4060
  this.showText(
4109
4061
  `Skills (${on} on, ${skills.length - on} off)`,
4110
4062
  skills.length ? [
4111
4063
  ...skills.map(
4112
- (skill) => `${skill.name} [${skill.source}]${disabled.has(skill.name) ? " (off)" : ""}
4064
+ (skill) => `${skill.name} [${skill.source}]${skill.enabled ? "" : " (off)"}
4113
4065
  ${skill.description}
4114
4066
  ${skill.path}`
4115
4067
  ),
@@ -4118,17 +4070,15 @@ ${skill.path}`
4118
4070
  );
4119
4071
  };
4120
4072
  /** /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
- }
4073
+ switchSkill = async (name, enabled, global) => {
4074
+ let skills;
4126
4075
  try {
4127
- setSkillEnabled(cwd, global ? "global" : "project", name, enabled);
4076
+ skills = await this.runner.setSkillEnabled(name, enabled, global);
4128
4077
  } catch (error) {
4129
- return errorMessage(error, "Could not change the skill.");
4078
+ const message = errorMessage(error, "Could not change the skill.");
4079
+ return message.startsWith("No skill named") ? `${message} /skills lists them.` : message;
4130
4080
  }
4131
- const stillOff = enabled && disabledSkillNames(cwd).has(name);
4081
+ const stillOff = enabled && skills.some((skill) => skill.name === name && !skill.enabled);
4132
4082
  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
4083
  };
4134
4084
  openUsageInspector = async () => {
@@ -4232,8 +4182,12 @@ ${skill.path}`
4232
4182
  getPermissionMode: () => this.store.getState().permissionMode,
4233
4183
  setPermissionMode: (mode) => this.setPermissionMode(mode),
4234
4184
  openPermissionPicker: this.openPermissionModes,
4235
- trustWorkspace: () => {
4236
- setWorkspaceTrusted(this.runner.cwd, true);
4185
+ trustWorkspace: async () => {
4186
+ try {
4187
+ await this.runner.trustWorkspace();
4188
+ } catch (error) {
4189
+ return errorMessage(error, "Could not trust the workspace.");
4190
+ }
4237
4191
  return "Workspace trusted: its allow rules apply now; MCP servers it declares start with the next session (/clear).";
4238
4192
  },
4239
4193
  openToolInspector: this.openToolInspector,
@@ -4410,14 +4364,12 @@ ${skill.path}`
4410
4364
  }
4411
4365
  };
4412
4366
  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) {
4367
+ const lines = [`${server.name} [${server.transport}]`];
4368
+ if (server.url) lines.push(`url: ${server.url}`);
4369
+ if (server.command) {
4417
4370
  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)`);
4371
+ if (server.args?.length) lines.push(`args: ${server.args.join(" ")}`);
4372
+ if (server.env?.length) lines.push(`env: ${server.env.join(", ")} (values hidden)`);
4421
4373
  }
4422
4374
  const failure = status.failures.find((item) => item.serverName === server.name);
4423
4375
  lines.push(
@@ -4937,19 +4889,14 @@ function useWorking(view) {
4937
4889
  }
4938
4890
 
4939
4891
  // src/tui/index.ts
4892
+ import { workspaceNeedsTrust } from "@datalabrotterdam/nova-ai-agent/core/workspace-mcp.js";
4940
4893
  async function runChat(options) {
4941
4894
  const stored = readCredentials();
4942
4895
  if (!stored) {
4943
4896
  console.error("nova-ai isn't connected to a Nova AI account yet. Run `nova-ai login` first.");
4944
4897
  return 1;
4945
4898
  }
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
- }
4899
+ const model = options.model ?? stored.defaultModel ?? process.env.NOVA_MODEL;
4953
4900
  const credentials = { ...stored, defaultModel: model };
4954
4901
  const updateCheck = checkForUpdate().then((update) => withInstallability(update));
4955
4902
  const updatedFrom = process.env[UPDATED_FROM_ENV];
@@ -4985,19 +4932,6 @@ async function runChat(options) {
4985
4932
  }
4986
4933
  return 0;
4987
4934
  }
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
4935
  async function askToTrust(cwd) {
5002
4936
  const prompt = createInterface2({ input: process.stdin, output: process.stdout });
5003
4937
  try {
@@ -5007,7 +4941,7 @@ ${cwd}`);
5007
4941
  console.log("Trusting it starts those servers and applies those rules. Only trust code you know.");
5008
4942
  const answer = (await prompt.question("Trust this workspace? [y/N] ")).trim().toLowerCase();
5009
4943
  if (answer === "y" || answer === "yes") {
5010
- setWorkspaceTrusted2(cwd, true);
4944
+ setWorkspaceTrusted(cwd, true);
5011
4945
  } else {
5012
4946
  console.log("Not trusted: its MCP servers stay off and its allow rules are ignored. Use /trust later to change this.");
5013
4947
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datalabrotterdam/nova-ai-cli",
3
- "version": "1.2.0-alpha.5",
3
+ "version": "1.2.0-alpha.6",
4
4
  "description": "Agentic coding with DataLab Rotterdam's Nova AI in your terminal: interactive TUI, headless mode, and an ACP agent for editors",
5
5
  "license": "Apache-2.0",
6
6
  "author": "DataLab Rotterdam",
@@ -39,8 +39,7 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@agentclientprotocol/sdk": "^0.29.0",
42
- "@datalabrotterdam/nova-ai-agent": "^1.0.0-alpha.2",
43
- "@datalabrotterdam/nova-sdk": "^1.3.2",
42
+ "@datalabrotterdam/nova-ai-agent": "^1.0.0-alpha.3",
44
43
  "chalk": "^5.3.0",
45
44
  "cli-highlight": "^2.1.11",
46
45
  "diff": "^9.0.0",
@@ -75,5 +74,8 @@
75
74
  }
76
75
  ]
77
76
  ]
77
+ },
78
+ "devDependencies": {
79
+ "@datalabrotterdam/nova-sdk": "^1.3.2"
78
80
  }
79
81
  }