@datalabrotterdam/nova-ai-cli 1.2.0-alpha.2 → 1.2.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,29 @@
1
+ # [@datalabrotterdam/nova-ai-cli-v1.2.0-alpha.4](https://github.com/Datalab-Rotterdam/nova-ai-cli/compare/@datalabrotterdam/nova-ai-cli@1.2.0-alpha.3...@datalabrotterdam/nova-ai-cli@1.2.0-alpha.4) (2026-10-05)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **cli:** depend on the agent version the CLI was released with ([4931439](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/4931439d106063b81e24367d6aa9c3f285f4ad15))
7
+
8
+
9
+ ### Features
10
+
11
+ * **cli:** offer npm updates at start-up, remind until installed, /update ([18defca](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/18defcaea988698f9d0909419bb2aa1fdedfa5a9))
12
+
13
+ # [@datalabrotterdam/nova-ai-cli-v1.2.0-alpha.3](https://github.com/Datalab-Rotterdam/nova-ai-cli/compare/@datalabrotterdam/nova-ai-cli@1.2.0-alpha.2...@datalabrotterdam/nova-ai-cli@1.2.0-alpha.3) (2026-10-04)
14
+
15
+
16
+ ### Bug Fixes
17
+
18
+ * **agent:** never overwrite an unreadable settings file when switching a skill ([ca989ef](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/ca989ef1810fc903cea069cdadf3041fe164e364))
19
+ * **cli:** install the command as nova-ai only ([eda8064](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/eda806411af1738ff6f6b1404d23d4e84a74ced8))
20
+
21
+
22
+ ### Features
23
+
24
+ * **agent,cli:** Nova skill folders, switching skills off, a budgeted skill list ([0891c79](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/0891c79248d3a645d638f12f46928a54652b0941))
25
+ * **cli:** update notice from npm for every channel, cached daily ([c3a0014](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/c3a001473336aac14d216cdeec0bedde4df34427))
26
+
1
27
  # [@datalabrotterdam/nova-ai-cli-v1.2.0-alpha.2](https://github.com/Datalab-Rotterdam/nova-ai-cli/compare/@datalabrotterdam/nova-ai-cli@1.2.0-alpha.1...@datalabrotterdam/nova-ai-cli@1.2.0-alpha.2) (2026-10-03)
2
28
 
3
29
 
package/README.md CHANGED
@@ -44,6 +44,18 @@ nova-ai # start the terminal UI in your project
44
44
  Or without installing: `npx @datalabrotterdam/nova-ai-cli login`, then
45
45
  `npx @datalabrotterdam/nova-ai-cli`.
46
46
 
47
+ When a newer version is on npm, `nova-ai` asks before the terminal UI opens:
48
+ Enter installs it (`npm install -g`) and restarts, `n` asks again tomorrow, `s`
49
+ skips that version. Until it is installed, the status line keeps reminding you,
50
+ and `/update` installs it and restarts in the same session. `-p` and
51
+ `--version` print a notice (on stderr, only in a terminal). npm is asked at
52
+ most once a day; an alpha install also follows the `alpha` channel.
53
+ `NOVA_NO_UPDATE_CHECK=1` turns it off (it is off in CI).
54
+
55
+ nova-ai installs updates itself only when it was installed with
56
+ `npm install -g`; with npx, another package manager or a project dependency it
57
+ shows the command to run instead.
58
+
47
59
  ```
48
60
  nova-ai [options] Interactive terminal UI
49
61
  nova-ai -p [options] [prompt] Run one request headless (see nova-ai -p --help)
@@ -131,7 +143,7 @@ always options, client file system and terminals, form questions, plans, usage
131
143
  and title updates, MCP servers (stdio, HTTP, SSE), image prompts, and three
132
144
  login methods (browser, `NOVA_API_KEY`, or `nova-ai login` in a terminal).
133
145
  Nova-specific features (prompt queue, rewind, compaction, context usage,
134
- background jobs) are `_nova/…` extension methods. The agent is also published
146
+ per-session settings, background jobs) are `_nova/…` extension methods. The agent is also published
135
147
  on its own as
136
148
  [`@datalabrotterdam/nova-ai-agent`](https://www.npmjs.com/package/@datalabrotterdam/nova-ai-agent)
137
149
  (command `nova-ai-agent`), without the terminal UI, for integrations that only
@@ -153,6 +165,9 @@ need ACP. Everything is documented in
153
165
  private `~/.nova-ai/projects/<key>/settings.json`, outside the repository.
154
166
  Compound commands (`a && b`, pipes) are checked part by part; substitutions
155
167
  and nested shells need an exact rule.
168
+ - **Web pages are fetched only with approval.** `fetch_url` asks every time
169
+ unless a rule allows that URL (a URL can carry data out), and a redirect to
170
+ another site needs its own approval.
156
171
  - **File access stays in the workspace** (symlinks resolved); commands run in
157
172
  their own process group with a timeout, and the whole tree is stopped on
158
173
  cancel.
@@ -172,8 +187,11 @@ VS Code extension: settings, per-project trust, sessions and memory.
172
187
  - **Instructions**: `NOVA.md` and `AGENTS.md` in the workspace.
173
188
  - **Memory**: a `MEMORY.md` index plus notes, globally and per project,
174
189
  through the `memory_read` and `memory_write` tools (writes need approval).
175
- - **Skills**: `SKILL.md` files under `.agents/skills`, `.claude/skills` or
176
- `.codex/skills`, in the workspace or your home folder.
190
+ - **Skills**: `SKILL.md` files under `.nova-ai/skills` in the workspace or
191
+ `~/.nova-ai/skills` for every project (`.agents`, `.claude` and `.codex`
192
+ skills folders are read too). `/skills` lists them; `/skills off <name>`
193
+ switches one off in this project (`--global`: everywhere). Only names and
194
+ short descriptions go into the prompt; Nova loads a skill when it fits.
177
195
  - **MCP servers**: `.mcp.json` or `mcpServers` in `.nova-ai/settings.json`.
178
196
 
179
197
  The complete layout and rule syntax: [docs/NOVA_HOME.md](https://github.com/Datalab-Rotterdam/nova-ai-cli/blob/main/docs/NOVA_HOME.md).
@@ -195,6 +213,7 @@ hand).
195
213
  | `NOVA_TOOL_PROTOCOL` | Force `native` or `text` tool calling |
196
214
  | `NOVA_NES_MODEL` | Separate model for next-edit suggestions (ACP) |
197
215
  | `NOVA_KITTY_KEYBOARD` | `1`/`0`: force the kitty keyboard protocol on or off |
216
+ | `NOVA_NO_UPDATE_CHECK` | Don't ask npm for a newer version (also `NO_UPDATE_NOTIFIER`, or `CI`) |
198
217
 
199
218
  ## Development
200
219
 
@@ -6,5 +6,6 @@ function cliVersion() {
6
6
  }
7
7
 
8
8
  export {
9
+ CLI_PACKAGE_NAME,
9
10
  cliVersion
10
11
  };
@@ -0,0 +1,170 @@
1
+ import {
2
+ CLI_PACKAGE_NAME,
3
+ cliVersion
4
+ } from "./chunk-FMHWW3E6.js";
5
+
6
+ // src/update-check.ts
7
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
8
+ import { dirname, join } from "path";
9
+ import { novaHomeRoot } from "@datalabrotterdam/nova-ai-agent/core/nova-home.js";
10
+ var DIST_TAGS_URL = `https://registry.npmjs.org/-/package/${CLI_PACKAGE_NAME.replace("/", "%2F")}/dist-tags`;
11
+ var DEFAULT_TIMEOUT_MS = 3e3;
12
+ var CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
13
+ function updateCachePath() {
14
+ return join(novaHomeRoot(), "update-check.json");
15
+ }
16
+ function updateChecksDisabled(env = process.env) {
17
+ return Boolean(env.NOVA_NO_UPDATE_CHECK || env.NO_UPDATE_NOTIFIER || env.CI);
18
+ }
19
+ async function checkForUpdate(options = {}) {
20
+ try {
21
+ if (updateChecksDisabled(options.env)) return null;
22
+ const currentVersion = options.currentVersion ?? cliVersion();
23
+ if (!currentVersion) return null;
24
+ const distTags = await readDistTags(options);
25
+ if (!distTags) return null;
26
+ return chooseUpdate(currentVersion, distTags);
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+ function chooseUpdate(currentVersion, distTags) {
32
+ const channel = prereleaseChannel(currentVersion);
33
+ const tags = channel && distTags[channel] ? [channel, "latest"] : ["latest"];
34
+ let best = null;
35
+ for (const tag of tags) {
36
+ const version = distTags[tag];
37
+ if (!version || !isNewerVersion(version, currentVersion)) continue;
38
+ if (!best || isNewerVersion(version, best.version)) best = { tag, version };
39
+ }
40
+ if (!best) return null;
41
+ return {
42
+ currentVersion,
43
+ latestVersion: best.version,
44
+ tag: best.tag,
45
+ command: `npm install -g ${CLI_PACKAGE_NAME}@${best.tag}`
46
+ };
47
+ }
48
+ async function readDistTags(options) {
49
+ const now = (options.now ?? Date.now)();
50
+ const cachePath = options.cachePath === void 0 ? updateCachePath() : options.cachePath;
51
+ const cached = cachePath ? readCache(cachePath) : null;
52
+ if (cached && now - cached.checkedAt >= 0 && now - cached.checkedAt < CACHE_MAX_AGE_MS) {
53
+ return cached.distTags;
54
+ }
55
+ const response = await (options.fetchImpl ?? fetch)(DIST_TAGS_URL, {
56
+ headers: { accept: "application/json" },
57
+ signal: AbortSignal.any([
58
+ AbortSignal.timeout(options.timeoutMs ?? DEFAULT_TIMEOUT_MS),
59
+ ...options.signal ? [options.signal] : []
60
+ ])
61
+ });
62
+ if (!response.ok) return null;
63
+ const payload = await response.json();
64
+ if (!isDistTags(payload)) return null;
65
+ if (cachePath) writeCache(cachePath, { ...cached, checkedAt: now, distTags: payload });
66
+ return payload;
67
+ }
68
+ function readCache(path) {
69
+ try {
70
+ const value = JSON.parse(readFileSync(path, "utf8"));
71
+ if (value && typeof value === "object" && typeof value.checkedAt === "number" && isDistTags(value.distTags)) {
72
+ return value;
73
+ }
74
+ } catch {
75
+ }
76
+ return null;
77
+ }
78
+ var SNOOZE_MS = 24 * 60 * 60 * 1e3;
79
+ function shouldAskAboutUpdate(update, options = {}) {
80
+ const cachePath = options.cachePath === void 0 ? updateCachePath() : options.cachePath;
81
+ const cached = cachePath ? readCache(cachePath) : null;
82
+ if (!cached) return true;
83
+ if (cached.skippedVersion === update.latestVersion) return false;
84
+ const now = (options.now ?? Date.now)();
85
+ return !(typeof cached.snoozedUntil === "number" && now < cached.snoozedUntil);
86
+ }
87
+ function rememberUpdateAnswer(answer, update, options = {}) {
88
+ const cachePath = options.cachePath === void 0 ? updateCachePath() : options.cachePath;
89
+ if (!cachePath) return;
90
+ const cached = readCache(cachePath);
91
+ const base = cached ?? { checkedAt: 0, distTags: {} };
92
+ const now = (options.now ?? Date.now)();
93
+ writeCache(
94
+ cachePath,
95
+ answer === "skip" ? { ...base, skippedVersion: update.latestVersion, snoozedUntil: void 0 } : { ...base, snoozedUntil: now + SNOOZE_MS }
96
+ );
97
+ }
98
+ function writeCache(path, value) {
99
+ try {
100
+ mkdirSync(dirname(path), { recursive: true, mode: 448 });
101
+ writeFileSync(path, `${JSON.stringify(value)}
102
+ `, { mode: 384 });
103
+ } catch {
104
+ }
105
+ }
106
+ function isDistTags(value) {
107
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((version) => typeof version === "string");
108
+ }
109
+ function prereleaseChannel(version) {
110
+ const parsed = parseVersion(version);
111
+ const first = parsed?.prerelease[0];
112
+ return first && !/^\d+$/.test(first) ? first : null;
113
+ }
114
+ function readInstalledVersion() {
115
+ return cliVersion();
116
+ }
117
+ function isNewerVersion(candidate, current) {
118
+ const next = parseVersion(candidate);
119
+ const installed = parseVersion(current);
120
+ if (!next || !installed) return false;
121
+ for (let index = 0; index < 3; index++) {
122
+ const difference = next.core[index] - installed.core[index];
123
+ if (difference !== 0) return difference > 0;
124
+ }
125
+ return comparePrerelease(next.prerelease, installed.prerelease) > 0;
126
+ }
127
+ function parseVersion(value) {
128
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(value);
129
+ if (!match) return null;
130
+ return {
131
+ core: [Number(match[1]), Number(match[2]), Number(match[3])],
132
+ prerelease: match[4]?.split(".") ?? []
133
+ };
134
+ }
135
+ function comparePrerelease(left, right) {
136
+ if (left.length === 0 || right.length === 0) {
137
+ if (left.length === right.length) return 0;
138
+ return left.length === 0 ? 1 : -1;
139
+ }
140
+ const length = Math.max(left.length, right.length);
141
+ for (let index = 0; index < length; index++) {
142
+ const leftPart = left[index];
143
+ const rightPart = right[index];
144
+ if (leftPart === void 0 || rightPart === void 0) {
145
+ return leftPart === rightPart ? 0 : leftPart === void 0 ? -1 : 1;
146
+ }
147
+ if (leftPart === rightPart) continue;
148
+ const leftNumber = /^\d+$/.test(leftPart) ? Number(leftPart) : null;
149
+ const rightNumber = /^\d+$/.test(rightPart) ? Number(rightPart) : null;
150
+ if (leftNumber !== null && rightNumber !== null) return leftNumber - rightNumber;
151
+ if (leftNumber !== null || rightNumber !== null) return leftNumber !== null ? -1 : 1;
152
+ return leftPart < rightPart ? -1 : 1;
153
+ }
154
+ return 0;
155
+ }
156
+ function formatUpdateNotice(update) {
157
+ return `Update available: nova-ai ${update.currentVersion} \u2192 ${update.latestVersion}. Run: ${update.command}`;
158
+ }
159
+
160
+ export {
161
+ updateCachePath,
162
+ updateChecksDisabled,
163
+ checkForUpdate,
164
+ chooseUpdate,
165
+ shouldAskAboutUpdate,
166
+ rememberUpdateAnswer,
167
+ readInstalledVersion,
168
+ isNewerVersion,
169
+ formatUpdateNotice
170
+ };
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  cliVersion
4
- } from "./chunk-SKAW2KXC.js";
4
+ } from "./chunk-FMHWW3E6.js";
5
5
 
6
6
  // src/cli.ts
7
7
  var SUBCOMMANDS = {
@@ -86,6 +86,28 @@ function parseTuiArgs(args) {
86
86
  }
87
87
 
88
88
  // src/index.ts
89
+ function startUpdateCheck() {
90
+ if (!process.stderr.isTTY) return null;
91
+ const abort = new AbortController();
92
+ const result = import("./update-check-G3PYCRWM.js").then(({ checkForUpdate }) => checkForUpdate({ signal: abort.signal })).catch(() => null);
93
+ return { result, cancel: () => abort.abort() };
94
+ }
95
+ async function noticeUpdate(check, waitMs) {
96
+ let timer;
97
+ const update = await Promise.race([
98
+ check.result,
99
+ new Promise((resolve) => {
100
+ timer = setTimeout(() => resolve(null), waitMs);
101
+ })
102
+ ]);
103
+ clearTimeout(timer);
104
+ check.cancel();
105
+ if (!update) return;
106
+ const { formatUpdateNotice } = await import("./update-check-G3PYCRWM.js");
107
+ process.stderr.write(`
108
+ ${formatUpdateNotice(update)}
109
+ `);
110
+ }
89
111
  async function main() {
90
112
  const command = parseCli(process.argv.slice(2));
91
113
  switch (command.mode) {
@@ -99,10 +121,13 @@ ${MAIN_HELP}
99
121
  process.stdout.write(`${MAIN_HELP}
100
122
  `);
101
123
  return 0;
102
- case "version":
124
+ case "version": {
125
+ const update = startUpdateCheck();
103
126
  process.stdout.write(`${versionText()}
104
127
  `);
128
+ if (update) await noticeUpdate(update, 800);
105
129
  return 0;
130
+ }
106
131
  case "acp": {
107
132
  if (command.args.length) {
108
133
  process.stderr.write(`--acp takes no other arguments (got ${command.args.join(" ")}).
@@ -114,8 +139,11 @@ ${MAIN_HELP}
114
139
  return 0;
115
140
  }
116
141
  case "print": {
142
+ const update = startUpdateCheck();
117
143
  const { default: runHeadless } = await import("./headless-OFJFVJ3O.js");
118
- return runHeadless(command.args);
144
+ const code = await runHeadless(command.args);
145
+ if (update) await noticeUpdate(update, 500);
146
+ return code;
119
147
  }
120
148
  case "login": {
121
149
  const { runLogin } = await import("@datalabrotterdam/nova-ai-agent/commands/login.js");
@@ -147,7 +175,7 @@ ${MAIN_HELP}
147
175
  );
148
176
  return 2;
149
177
  }
150
- const { default: runChat } = await import("./tui-NJWDD5X7.js");
178
+ const { default: runChat } = await import("./tui-OWU4YGTT.js");
151
179
  return runChat(options);
152
180
  }
153
181
  }
@@ -1,6 +1,12 @@
1
1
  import {
2
+ checkForUpdate,
3
+ rememberUpdateAnswer,
4
+ shouldAskAboutUpdate
5
+ } from "./chunk-L77KQRGZ.js";
6
+ import {
7
+ CLI_PACKAGE_NAME,
2
8
  cliVersion
3
- } from "./chunk-SKAW2KXC.js";
9
+ } from "./chunk-FMHWW3E6.js";
4
10
  import {
5
11
  TuiAcpClient,
6
12
  createStore,
@@ -9,11 +15,105 @@ import {
9
15
  } from "./chunk-NHGIUEIR.js";
10
16
 
11
17
  // src/tui/index.ts
12
- import { createInterface } from "readline/promises";
18
+ import { createInterface as createInterface2 } from "readline/promises";
13
19
  import { readCredentials } from "@datalabrotterdam/nova-ai-agent/core/credentials.js";
14
- import { listStoredSessions as listStoredSessions2 } from "@datalabrotterdam/nova-ai-agent/core/sessions.js";
20
+ import { listStoredSessions as listStoredSessions2, loadStoredSession as loadStoredSession2 } from "@datalabrotterdam/nova-ai-agent/core/sessions.js";
15
21
  import { setWorkspaceTrusted as setWorkspaceTrusted2 } from "@datalabrotterdam/nova-ai-agent/core/nova-home.js";
16
22
 
23
+ // src/update-install.ts
24
+ import { spawnSync } from "child_process";
25
+ import { existsSync, realpathSync } from "fs";
26
+ import { dirname, join, sep } from "path";
27
+ import { createInterface } from "readline/promises";
28
+ var START_WAIT_MS = 1500;
29
+ var UPDATED_FROM_ENV = "NOVA_UPDATED_FROM";
30
+ function globalNpmPrefix(options = {}) {
31
+ const script = options.scriptPath ?? process.argv[1];
32
+ if (!script) return null;
33
+ const windows = (options.platform ?? process.platform) === "win32";
34
+ const marker = `${sep}node_modules${sep}${CLI_PACKAGE_NAME.split("/").join(sep)}${sep}`;
35
+ const resolved = realPath(script);
36
+ const at = resolved.lastIndexOf(marker);
37
+ if (at < 0) return null;
38
+ const nodeModulesParent = resolved.slice(0, at);
39
+ const prefix = windows ? nodeModulesParent : dirname(nodeModulesParent);
40
+ if (!windows && nodeModulesParent !== join(prefix, "lib")) return null;
41
+ return existsSync(join(prefix, windows ? "nova-ai.cmd" : join("bin", "nova-ai"))) ? prefix : null;
42
+ }
43
+ async function withInstallability(update, options = {}) {
44
+ if (!update) return null;
45
+ return { ...update, installable: globalNpmPrefix(options) !== null };
46
+ }
47
+ function npmInvocation(args) {
48
+ return process.platform === "win32" ? [`npm ${args.map((arg) => /\s/.test(arg) ? `"${arg}"` : arg).join(" ")}`, [], true] : ["npm", args, false];
49
+ }
50
+ function realPath(path) {
51
+ try {
52
+ return realpathSync(path);
53
+ } catch {
54
+ return path;
55
+ }
56
+ }
57
+ function parseUpdateAnswer(answer) {
58
+ const value = answer.trim().toLowerCase();
59
+ if (value === "" || value === "y" || value === "yes") return "update";
60
+ if (value === "s" || value === "skip") return "skip";
61
+ return "later";
62
+ }
63
+ async function askToUpdate(update) {
64
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
65
+ try {
66
+ console.log(`
67
+ Update available: nova-ai ${update.currentVersion} \u2192 ${update.latestVersion}`);
68
+ return parseUpdateAnswer(
69
+ await prompt.question("Update now? [Y/n/s] (n: not today, s: skip this version) ")
70
+ );
71
+ } finally {
72
+ prompt.close();
73
+ }
74
+ }
75
+ async function offerUpdateAtStart(check, restartArgs) {
76
+ let timer;
77
+ const update = await Promise.race([
78
+ check,
79
+ new Promise((resolve2) => {
80
+ timer = setTimeout(() => resolve2(null), START_WAIT_MS);
81
+ })
82
+ ]);
83
+ clearTimeout(timer);
84
+ if (!update?.installable || !shouldAskAboutUpdate(update)) return null;
85
+ const answer = await askToUpdate(update);
86
+ if (answer !== "update") {
87
+ rememberUpdateAnswer(answer, update);
88
+ console.log(
89
+ answer === "skip" ? `Skipping ${update.latestVersion}; you'll be asked again for a newer version.` : "Not now. /update installs it any time."
90
+ );
91
+ return null;
92
+ }
93
+ return installAndRestart(update, restartArgs);
94
+ }
95
+ function installAndRestart(update, restartArgs) {
96
+ const prefix = globalNpmPrefix();
97
+ if (!prefix) return null;
98
+ console.log(`
99
+ Installing nova-ai ${update.latestVersion}\u2026`);
100
+ const [file, args, shell] = npmInvocation(["install", "-g", "--prefix", prefix, `${CLI_PACKAGE_NAME}@${update.tag}`]);
101
+ const install = spawnSync(file, args, { stdio: "inherit", shell });
102
+ if (install.status !== 0) {
103
+ console.error(`
104
+ The update did not install. Run it yourself: ${update.command}
105
+ `);
106
+ return null;
107
+ }
108
+ const script = process.argv[1];
109
+ if (!script) return null;
110
+ const restarted = spawnSync(process.execPath, [...process.execArgv, script, ...restartArgs], {
111
+ stdio: "inherit",
112
+ env: { ...process.env, [UPDATED_FROM_ENV]: update.currentVersion }
113
+ });
114
+ return restarted.status ?? 1;
115
+ }
116
+
17
117
  // src/tui/ink/app.tsx
18
118
  import { Box as Box6, render, Static, Text as Text6, useBoxMetrics, useWindowSize } from "ink";
19
119
  import { useEffect, useRef, useState as useState2, useSyncExternalStore as useSyncExternalStore2 } from "react";
@@ -45,7 +145,7 @@ import {
45
145
 
46
146
  // src/tui/files/file-mentions.ts
47
147
  import { statSync } from "fs";
48
- import { isAbsolute, relative, resolve, sep } from "path";
148
+ import { isAbsolute, relative, resolve, sep as sep2 } from "path";
49
149
  var MENTION_RE = /@(?:"([^"]+)"|'([^']+)'|([^\s]+))/g;
50
150
  var TRAILING_PUNCTUATION = /[.,;:!?\)\]\}]+$/;
51
151
  function findWorkspaceFileMentions(text, cwd) {
@@ -55,10 +155,10 @@ function findWorkspaceFileMentions(text, cwd) {
55
155
  const raw = match[1] ?? match[2] ?? match[3] ?? "";
56
156
  const candidates = [raw, raw.replace(TRAILING_PUNCTUATION, "")].filter((value, index, all) => value && all.indexOf(value) === index);
57
157
  for (const candidate of candidates) {
58
- const normalized = candidate.replace(/[\\/]/g, sep);
158
+ const normalized = candidate.replace(/[\\/]/g, sep2);
59
159
  const absolutePath = resolve(workspaceRoot, normalized);
60
160
  const relativePath = relative(workspaceRoot, absolutePath);
61
- if (relativePath.startsWith(`..${sep}`) || relativePath === ".." || isAbsolute(relativePath)) continue;
161
+ if (relativePath.startsWith(`..${sep2}`) || relativePath === ".." || isAbsolute(relativePath)) continue;
62
162
  try {
63
163
  if (!statSync(absolutePath).isFile()) continue;
64
164
  } catch {
@@ -66,7 +166,7 @@ function findWorkspaceFileMentions(text, cwd) {
66
166
  }
67
167
  mentions.set(absolutePath.toLowerCase(), {
68
168
  absolutePath,
69
- relativePath: relativePath.split(sep).join("/")
169
+ relativePath: relativePath.split(sep2).join("/")
70
170
  });
71
171
  break;
72
172
  }
@@ -1012,82 +1112,6 @@ function fromAgentPermissionMode(mode) {
1012
1112
  return mode === "bypassPermissions" ? "bypassAll" : mode === "acceptEdits" ? "acceptEdits" : "ask";
1013
1113
  }
1014
1114
 
1015
- // src/tui/update-check.ts
1016
- var PACKAGE_NAME = "@datalabrotterdam/nova-ai-cli";
1017
- var REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME.replace("/", "%2F")}/latest`;
1018
- var DEFAULT_TIMEOUT_MS = 3e3;
1019
- var UPDATE_COMMAND = "npm install -g @datalabrotterdam/nova-ai-cli@latest";
1020
- async function checkForUpdate(options = {}) {
1021
- try {
1022
- const currentVersion = options.currentVersion ?? readInstalledVersion();
1023
- if (!currentVersion) return null;
1024
- const response = await (options.fetchImpl ?? fetch)(REGISTRY_URL, {
1025
- headers: { accept: "application/json" },
1026
- signal: AbortSignal.timeout(options.timeoutMs ?? DEFAULT_TIMEOUT_MS)
1027
- });
1028
- if (!response.ok) return null;
1029
- const payload = await response.json();
1030
- if (!isVersionPayload(payload)) return null;
1031
- if (!isNewerVersion(payload.version, currentVersion)) return null;
1032
- return {
1033
- currentVersion,
1034
- latestVersion: payload.version,
1035
- command: UPDATE_COMMAND
1036
- };
1037
- } catch {
1038
- return null;
1039
- }
1040
- }
1041
- function readInstalledVersion() {
1042
- return cliVersion();
1043
- }
1044
- function isNewerVersion(candidate, current) {
1045
- const next = parseVersion(candidate);
1046
- const installed = parseVersion(current);
1047
- if (!next || !installed) return false;
1048
- for (let index = 0; index < 3; index++) {
1049
- const difference = next.core[index] - installed.core[index];
1050
- if (difference !== 0) return difference > 0;
1051
- }
1052
- return comparePrerelease(next.prerelease, installed.prerelease) > 0;
1053
- }
1054
- function isVersionPayload(value) {
1055
- return typeof value === "object" && value !== null && "version" in value && typeof value.version === "string";
1056
- }
1057
- function parseVersion(value) {
1058
- const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(
1059
- value
1060
- );
1061
- if (!match) return null;
1062
- return {
1063
- core: [Number(match[1]), Number(match[2]), Number(match[3])],
1064
- prerelease: match[4]?.split(".") ?? []
1065
- };
1066
- }
1067
- function comparePrerelease(left, right) {
1068
- if (left.length === 0 || right.length === 0) {
1069
- if (left.length === right.length) return 0;
1070
- return left.length === 0 ? 1 : -1;
1071
- }
1072
- const length = Math.max(left.length, right.length);
1073
- for (let index = 0; index < length; index++) {
1074
- const leftPart = left[index];
1075
- const rightPart = right[index];
1076
- if (leftPart === void 0 || rightPart === void 0) {
1077
- return leftPart === rightPart ? 0 : leftPart === void 0 ? -1 : 1;
1078
- }
1079
- if (leftPart === rightPart) continue;
1080
- const leftNumber = /^\d+$/.test(leftPart) ? Number(leftPart) : null;
1081
- const rightNumber = /^\d+$/.test(rightPart) ? Number(rightPart) : null;
1082
- if (leftNumber !== null && rightNumber !== null)
1083
- return leftNumber - rightNumber;
1084
- if (leftNumber !== null || rightNumber !== null)
1085
- return leftNumber !== null ? -1 : 1;
1086
- return leftPart < rightPart ? -1 : 1;
1087
- }
1088
- return 0;
1089
- }
1090
-
1091
1115
  // src/tui/ink/terminal-keyboard.ts
1092
1116
  var KITTY_QUERY = "\x1B[?u";
1093
1117
  var DEVICE_ATTRIBUTES_QUERY = "\x1B[c";
@@ -1464,7 +1488,7 @@ function StatusLine({
1464
1488
  const separator = /* @__PURE__ */ jsx2(Text2, { color: palette.faint, children: " | " });
1465
1489
  const usage = ui.contextUsage ? `ctx:${formatTokenCount(ui.contextUsage.totalTokens)}${ui.contextUsage.contextWindow ? `/${formatTokenCount(ui.contextUsage.contextWindow)}` : ""}` : null;
1466
1490
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
1467
- ui.updateAvailable ? /* @__PURE__ */ jsx2(Text2, { color: palette.warning, children: `Update ${ui.updateAvailable.latestVersion} available (current ${ui.updateAvailable.currentVersion}). Run: ${ui.updateAvailable.command}` }) : null,
1491
+ ui.updateAvailable ? /* @__PURE__ */ jsx2(Text2, { color: palette.warning, children: `Update ${ui.updateAvailable.latestVersion} available (current ${ui.updateAvailable.currentVersion}). ${ui.updateAvailable.installable ? "Type /update to install it." : `Run: ${ui.updateAvailable.command}`}` }) : null,
1468
1492
  ui.statusLine ? /* @__PURE__ */ jsx2(Text2, { color: palette.muted, children: ui.statusLine }) : null,
1469
1493
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate-end", children: [
1470
1494
  /* @__PURE__ */ jsx2(Text2, { color: palette.muted, children: model }),
@@ -1808,6 +1832,11 @@ function PlanView({
1808
1832
 
1809
1833
  // src/tui/ink/controller.ts
1810
1834
  import { setWorkspaceTrusted } from "@datalabrotterdam/nova-ai-agent/core/nova-home.js";
1835
+ import {
1836
+ disabledSkillNames,
1837
+ discoverSkills as discoverSkills2,
1838
+ setSkillEnabled
1839
+ } from "@datalabrotterdam/nova-ai-agent/core/skills.js";
1811
1840
  import { INTERACTION_MODES } from "@datalabrotterdam/nova-ai-agent/core/interaction-modes.js";
1812
1841
 
1813
1842
  // src/tui/commands/builtins.ts
@@ -1884,11 +1913,19 @@ var builtinCommands = [
1884
1913
  },
1885
1914
  {
1886
1915
  name: "exit",
1887
- description: "Exit nova-ai-cli",
1916
+ description: "Exit nova-ai",
1888
1917
  run(ctx) {
1889
1918
  ctx.exit();
1890
1919
  }
1891
1920
  },
1921
+ {
1922
+ name: "update",
1923
+ description: "Install the newer nova-ai from npm and restart in this session",
1924
+ run(ctx) {
1925
+ const message = ctx.requestUpdate();
1926
+ if (message) ctx.print(message);
1927
+ }
1928
+ },
1892
1929
  {
1893
1930
  name: "mode",
1894
1931
  description: "Switch interaction mode (/mode agent|ask|plan)",
@@ -1957,9 +1994,20 @@ var builtinCommands = [
1957
1994
  },
1958
1995
  {
1959
1996
  name: "skills",
1960
- description: "Inspect discovered user and workspace skills",
1961
- run(ctx) {
1962
- ctx.openSkillInspector();
1997
+ description: "List skills, or switch one: /skills on|off <name> [--global]",
1998
+ run(ctx, args) {
1999
+ const words = args.trim().split(/\s+/).filter(Boolean);
2000
+ if (!words.length) {
2001
+ ctx.openSkillInspector();
2002
+ return;
2003
+ }
2004
+ const global = words.includes("--global");
2005
+ const [action, name] = words.filter((word) => word !== "--global");
2006
+ if (action !== "on" && action !== "off" || !name) {
2007
+ ctx.print("Usage: /skills, or /skills on|off <name> [--global]");
2008
+ return;
2009
+ }
2010
+ ctx.print(ctx.switchSkill(name, action === "on", global));
1963
2011
  }
1964
2012
  },
1965
2013
  {
@@ -2257,10 +2305,10 @@ console.log(ObjC.unwrap(png.base64EncodedStringWithOptions(0)));
2257
2305
  `;
2258
2306
 
2259
2307
  // src/tui/files/find-fd.ts
2260
- import { spawnSync } from "child_process";
2308
+ import { spawnSync as spawnSync2 } from "child_process";
2261
2309
  var FD_CANDIDATES = ["fd", "fdfind"];
2262
2310
  var cachedPath;
2263
- function resolveFdPath(run = (command, args) => spawnSync(command, args, { stdio: "ignore" })) {
2311
+ function resolveFdPath(run = (command, args) => spawnSync2(command, args, { stdio: "ignore" })) {
2264
2312
  if (cachedPath !== void 0) return cachedPath;
2265
2313
  for (const candidate of FD_CANDIDATES) {
2266
2314
  const result = run(candidate, ["--version"]);
@@ -2275,7 +2323,7 @@ function resolveFdPath(run = (command, args) => spawnSync(command, args, { stdio
2275
2323
 
2276
2324
  // src/tui/files/list-workspace-files.ts
2277
2325
  import { readdirSync } from "fs";
2278
- import { join, relative as relative2, sep as sep2 } from "path";
2326
+ import { join as join2, relative as relative2, sep as sep3 } from "path";
2279
2327
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".nova-ai", "out", "coverage", ".next", ".turbo"]);
2280
2328
  function listWorkspaceFiles(cwd, max = 2e3) {
2281
2329
  const results = [];
@@ -2291,12 +2339,12 @@ function listWorkspaceFiles(cwd, max = 2e3) {
2291
2339
  for (const entry of entries) {
2292
2340
  if (results.length >= max) break;
2293
2341
  if (entry.isSymbolicLink()) continue;
2294
- const full = join(dir, entry.name);
2342
+ const full = join2(dir, entry.name);
2295
2343
  if (entry.isDirectory()) {
2296
2344
  if (SKIP_DIRS.has(entry.name)) continue;
2297
2345
  stack.push(full);
2298
2346
  } else if (entry.isFile()) {
2299
- results.push(relative2(cwd, full).split(sep2).join("/"));
2347
+ results.push(relative2(cwd, full).split(sep3).join("/"));
2300
2348
  }
2301
2349
  }
2302
2350
  }
@@ -2327,7 +2375,7 @@ var DraftImageAttachments = class {
2327
2375
  import { spawn } from "child_process";
2328
2376
  import { readdirSync as readdirSync2, statSync as statSync2 } from "fs";
2329
2377
  import { homedir } from "os";
2330
- import { basename, dirname, join as join2 } from "path";
2378
+ import { basename, dirname as dirname2, join as join3 } from "path";
2331
2379
  var PATH_DELIMITERS = /* @__PURE__ */ new Set([" ", " ", '"', "'", "="]);
2332
2380
  var MAX_SUGGESTIONS2 = 20;
2333
2381
  var WorkspaceAutocompleteProvider = class {
@@ -2408,14 +2456,14 @@ var WorkspaceAutocompleteProvider = class {
2408
2456
  pathSuggestions(prefix) {
2409
2457
  const quoted = prefix.startsWith('"');
2410
2458
  const raw = quoted ? prefix.slice(1) : prefix;
2411
- const expanded = raw === "~" || raw.startsWith("~/") ? join2(homedir(), raw.slice(1)) : raw;
2459
+ const expanded = raw === "~" || raw.startsWith("~/") ? join3(homedir(), raw.slice(1)) : raw;
2412
2460
  const absolute = expanded.startsWith("/") || /^[A-Za-z]:[\\/]/.test(expanded);
2413
2461
  const separator = Math.max(raw.lastIndexOf("/"), raw.lastIndexOf("\\"));
2414
2462
  const listsDirectory = raw === "" || raw === "~" || separator === raw.length - 1;
2415
- const directoryPart = listsDirectory ? expanded : dirname(expanded);
2463
+ const directoryPart = listsDirectory ? expanded : dirname2(expanded);
2416
2464
  const namePrefix = listsDirectory ? "" : basename(expanded);
2417
2465
  const displayDirectory = raw === "~" ? "~/" : separator >= 0 ? raw.slice(0, separator + 1) : "";
2418
- const searchDirectory = absolute ? directoryPart : join2(this.cwd, directoryPart);
2466
+ const searchDirectory = absolute ? directoryPart : join3(this.cwd, directoryPart);
2419
2467
  let entries;
2420
2468
  try {
2421
2469
  entries = readdirSync2(searchDirectory, { withFileTypes: true });
@@ -2429,7 +2477,7 @@ var WorkspaceAutocompleteProvider = class {
2429
2477
  if (!isDirectory && entry.isSymbolicLink()) {
2430
2478
  try {
2431
2479
  isDirectory = statSync2(
2432
- join2(searchDirectory, entry.name)
2480
+ join3(searchDirectory, entry.name)
2433
2481
  ).isDirectory();
2434
2482
  } catch {
2435
2483
  }
@@ -3798,6 +3846,8 @@ var TuiController = class {
3798
3846
  unsubscribeStore;
3799
3847
  resolveExit = () => {
3800
3848
  };
3849
+ /** Set by /update: the caller installs and restarts after the TUI exits. */
3850
+ updateRequested = false;
3801
3851
  /** Settles once the user asked to quit; `run` then tears the view down. */
3802
3852
  exitRequested = new Promise((resolve2) => {
3803
3853
  this.resolveExit = resolve2;
@@ -4024,15 +4074,38 @@ status: failed - ${failure.message}`);
4024
4074
  rows.length ? rows.join("\n\n") : "No MCP servers configured for this workspace.\n\nAdd a project .mcp.json or mcpServers to .nova-ai/settings.json; ACP connects them when the session starts."
4025
4075
  );
4026
4076
  };
4077
+ /** Every discovered skill with its state; switched-off skills stay listed. */
4027
4078
  openSkillInspector = () => {
4028
- const skills = this.runner.skillSessionStatus();
4079
+ const cwd = this.runner.cwd;
4080
+ const skills = discoverSkills2(cwd);
4081
+ const disabled = disabledSkillNames(cwd);
4082
+ const on = skills.filter((skill) => !disabled.has(skill.name)).length;
4029
4083
  this.showText(
4030
- `Skills (${skills.length})`,
4031
- skills.length ? skills.map((skill) => `${skill.name} [${skill.source}]
4084
+ `Skills (${on} on, ${skills.length - on} off)`,
4085
+ skills.length ? [
4086
+ ...skills.map(
4087
+ (skill) => `${skill.name} [${skill.source}]${disabled.has(skill.name) ? " (off)" : ""}
4032
4088
  ${skill.description}
4033
- ${skill.path}`).join("\n\n") : "No skills discovered. Add SKILL.md under .agents/skills, .claude/skills, or .codex/skills in the workspace or user profile."
4089
+ ${skill.path}`
4090
+ ),
4091
+ "/skills off <name> switches one off in this project (--global: everywhere); /skills on <name> switches it back on."
4092
+ ].join("\n\n") : "No skills discovered. Add SKILL.md under .nova-ai/skills (or .agents, .claude, .codex skills) in the workspace, or ~/.nova-ai/skills for all projects."
4034
4093
  );
4035
4094
  };
4095
+ /** /skills on|off <name> [--global]: applies from the next message. */
4096
+ switchSkill = (name, enabled, global) => {
4097
+ const cwd = this.runner.cwd;
4098
+ if (!discoverSkills2(cwd).some((skill) => skill.name === name)) {
4099
+ return `No skill named "${name}". /skills lists them.`;
4100
+ }
4101
+ try {
4102
+ setSkillEnabled(cwd, global ? "global" : "project", name, enabled);
4103
+ } catch (error) {
4104
+ return errorMessage(error, "Could not change the skill.");
4105
+ }
4106
+ const stillOff = enabled && disabledSkillNames(cwd).has(name);
4107
+ 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.`;
4108
+ };
4036
4109
  openUsageInspector = async () => {
4037
4110
  this.showText("Context usage", "Calculating context usage...");
4038
4111
  try {
@@ -4099,6 +4172,19 @@ ${skill.path}`).join("\n\n") : "No skills discovered. Add SKILL.md under .agents
4099
4172
  clear: () => void this.runner.startNewSession(),
4100
4173
  newSession: () => this.runner.startNewSession(),
4101
4174
  exit: () => this.requestExit(),
4175
+ requestUpdate: () => {
4176
+ const update = this.store.getState().updateAvailable;
4177
+ if (!update) return "No newer nova-ai found on npm.";
4178
+ if (!update.installable) {
4179
+ return `nova-ai ${update.latestVersion} is available, but this copy wasn't installed with npm -g, so it can't update itself. Run: ${update.command}`;
4180
+ }
4181
+ if (this.store.getState().busy) {
4182
+ return "Wait for the current request to finish (or press Esc), then run /update again.";
4183
+ }
4184
+ this.updateRequested = true;
4185
+ this.requestExit();
4186
+ return null;
4187
+ },
4102
4188
  resumeSession: (sessionId) => {
4103
4189
  const id = sessionId ?? listSessionsForCwd(this.runner.cwd)[0]?.sessionId;
4104
4190
  return id ? this.runner.resumeFrom(id) : false;
@@ -4119,6 +4205,7 @@ ${skill.path}`).join("\n\n") : "No skills discovered. Add SKILL.md under .agents
4119
4205
  openToolInspector: this.openToolInspector,
4120
4206
  openMcpInspector: this.openMcpInspector,
4121
4207
  openSkillInspector: this.openSkillInspector,
4208
+ switchSkill: this.switchSkill,
4122
4209
  openUsageInspector: this.openUsageInspector,
4123
4210
  steerMessage: (message) => this.runner.steer(message),
4124
4211
  queuedMessages: () => this.runner.queuedMessages(),
@@ -4674,6 +4761,7 @@ async function runInkTui(credentials, cwd, args, options = {}) {
4674
4761
  }
4675
4762
  stdout.write(`Resume this session with: nova-ai --resume ${runner.sessionId}
4676
4763
  `);
4764
+ return { sessionId: runner.sessionId, updateRequested: controller.updateRequested };
4677
4765
  }
4678
4766
  function App({ controller }) {
4679
4767
  const view = useSyncExternalStore2(controller.subscribe, controller.getView);
@@ -4772,6 +4860,15 @@ async function runChat(options) {
4772
4860
  return 1;
4773
4861
  }
4774
4862
  const credentials = options.model ? { ...stored, defaultModel: options.model } : stored;
4863
+ const updateCheck = checkForUpdate().then((update) => withInstallability(update));
4864
+ const updatedFrom = process.env[UPDATED_FROM_ENV];
4865
+ if (updatedFrom) {
4866
+ delete process.env[UPDATED_FROM_ENV];
4867
+ console.log(`Updated nova-ai ${updatedFrom} \u2192 ${cliVersion() ?? "?"}.`);
4868
+ } else {
4869
+ const restartedCode = await offerUpdateAtStart(updateCheck, process.argv.slice(2));
4870
+ if (restartedCode !== null) return restartedCode;
4871
+ }
4775
4872
  const cwd = process.cwd();
4776
4873
  let resume = options.resume;
4777
4874
  if (options.continueLast) {
@@ -4783,11 +4880,22 @@ async function runChat(options) {
4783
4880
  }
4784
4881
  if (workspaceNeedsTrust(cwd)) await askToTrust(cwd);
4785
4882
  const args = resume ? ["--resume", resume] : [];
4786
- await runInkTui(credentials, cwd, args);
4883
+ const result = await runInkTui(credentials, cwd, args, { checkForUpdate: () => updateCheck });
4884
+ if (result.updateRequested) {
4885
+ const update = await updateCheck;
4886
+ if (update) {
4887
+ const restartArgs = [
4888
+ ...loadStoredSession2(result.sessionId) ? ["--resume", result.sessionId] : [],
4889
+ ...options.model ? ["--model", options.model] : []
4890
+ ];
4891
+ const restartedCode = installAndRestart(update, restartArgs);
4892
+ if (restartedCode !== null) return restartedCode;
4893
+ }
4894
+ }
4787
4895
  return 0;
4788
4896
  }
4789
4897
  async function askToTrust(cwd) {
4790
- const prompt = createInterface({ input: process.stdin, output: process.stdout });
4898
+ const prompt = createInterface2({ input: process.stdin, output: process.stdout });
4791
4899
  try {
4792
4900
  console.log(`
4793
4901
  ${cwd}`);
@@ -0,0 +1,23 @@
1
+ import {
2
+ checkForUpdate,
3
+ chooseUpdate,
4
+ formatUpdateNotice,
5
+ isNewerVersion,
6
+ readInstalledVersion,
7
+ rememberUpdateAnswer,
8
+ shouldAskAboutUpdate,
9
+ updateCachePath,
10
+ updateChecksDisabled
11
+ } from "./chunk-L77KQRGZ.js";
12
+ import "./chunk-FMHWW3E6.js";
13
+ export {
14
+ checkForUpdate,
15
+ chooseUpdate,
16
+ formatUpdateNotice,
17
+ isNewerVersion,
18
+ readInstalledVersion,
19
+ rememberUpdateAnswer,
20
+ shouldAskAboutUpdate,
21
+ updateCachePath,
22
+ updateChecksDisabled
23
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datalabrotterdam/nova-ai-cli",
3
- "version": "1.2.0-alpha.2",
3
+ "version": "1.2.0-alpha.4",
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",
@@ -21,7 +21,6 @@
21
21
  },
22
22
  "homepage": "https://github.com/Datalab-Rotterdam/nova-ai-cli#readme",
23
23
  "bin": {
24
- "nova-ai-cli": "dist/index.js",
25
24
  "nova-ai": "dist/index.js"
26
25
  },
27
26
  "files": [
@@ -40,7 +39,7 @@
40
39
  },
41
40
  "dependencies": {
42
41
  "@agentclientprotocol/sdk": "^0.29.0",
43
- "@datalabrotterdam/nova-ai-agent": "^1.0.0-alpha.1",
42
+ "@datalabrotterdam/nova-ai-agent": "^1.0.0-alpha.2",
44
43
  "@datalabrotterdam/nova-sdk": "^1.3.2",
45
44
  "chalk": "^5.3.0",
46
45
  "cli-highlight": "^2.1.11",