@datalabrotterdam/nova-ai-cli 1.2.0-alpha.2 → 1.2.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +15 -3
- package/dist/chunk-37PMUZ34.js +147 -0
- package/dist/{chunk-SKAW2KXC.js → chunk-FMHWW3E6.js} +1 -0
- package/dist/index.js +32 -4
- package/dist/{tui-NJWDD5X7.js → tui-LNFSHL4Y.js} +51 -86
- package/dist/update-check-OX6WAPNN.js +19 -0
- package/package.json +1 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [@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)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **agent:** never overwrite an unreadable settings file when switching a skill ([ca989ef](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/ca989ef1810fc903cea069cdadf3041fe164e364))
|
|
7
|
+
* **cli:** install the command as nova-ai only ([eda8064](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/eda806411af1738ff6f6b1404d23d4e84a74ced8))
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* **agent,cli:** Nova skill folders, switching skills off, a budgeted skill list ([0891c79](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/0891c79248d3a645d638f12f46928a54652b0941))
|
|
13
|
+
* **cli:** update notice from npm for every channel, cached daily ([c3a0014](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/c3a001473336aac14d216cdeec0bedde4df34427))
|
|
14
|
+
|
|
1
15
|
# [@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
16
|
|
|
3
17
|
|
package/README.md
CHANGED
|
@@ -44,6 +44,11 @@ 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, the terminal UI says so, and so do `-p` and
|
|
48
|
+
`--version` (on stderr, only in a terminal). npm is asked at most once a day;
|
|
49
|
+
an alpha install also follows the `alpha` channel. `NOVA_NO_UPDATE_CHECK=1`
|
|
50
|
+
turns it off (it is off in CI).
|
|
51
|
+
|
|
47
52
|
```
|
|
48
53
|
nova-ai [options] Interactive terminal UI
|
|
49
54
|
nova-ai -p [options] [prompt] Run one request headless (see nova-ai -p --help)
|
|
@@ -131,7 +136,7 @@ always options, client file system and terminals, form questions, plans, usage
|
|
|
131
136
|
and title updates, MCP servers (stdio, HTTP, SSE), image prompts, and three
|
|
132
137
|
login methods (browser, `NOVA_API_KEY`, or `nova-ai login` in a terminal).
|
|
133
138
|
Nova-specific features (prompt queue, rewind, compaction, context usage,
|
|
134
|
-
background jobs) are `_nova/…` extension methods. The agent is also published
|
|
139
|
+
per-session settings, background jobs) are `_nova/…` extension methods. The agent is also published
|
|
135
140
|
on its own as
|
|
136
141
|
[`@datalabrotterdam/nova-ai-agent`](https://www.npmjs.com/package/@datalabrotterdam/nova-ai-agent)
|
|
137
142
|
(command `nova-ai-agent`), without the terminal UI, for integrations that only
|
|
@@ -153,6 +158,9 @@ need ACP. Everything is documented in
|
|
|
153
158
|
private `~/.nova-ai/projects/<key>/settings.json`, outside the repository.
|
|
154
159
|
Compound commands (`a && b`, pipes) are checked part by part; substitutions
|
|
155
160
|
and nested shells need an exact rule.
|
|
161
|
+
- **Web pages are fetched only with approval.** `fetch_url` asks every time
|
|
162
|
+
unless a rule allows that URL (a URL can carry data out), and a redirect to
|
|
163
|
+
another site needs its own approval.
|
|
156
164
|
- **File access stays in the workspace** (symlinks resolved); commands run in
|
|
157
165
|
their own process group with a timeout, and the whole tree is stopped on
|
|
158
166
|
cancel.
|
|
@@ -172,8 +180,11 @@ VS Code extension: settings, per-project trust, sessions and memory.
|
|
|
172
180
|
- **Instructions**: `NOVA.md` and `AGENTS.md` in the workspace.
|
|
173
181
|
- **Memory**: a `MEMORY.md` index plus notes, globally and per project,
|
|
174
182
|
through the `memory_read` and `memory_write` tools (writes need approval).
|
|
175
|
-
- **Skills**: `SKILL.md` files under `.
|
|
176
|
-
|
|
183
|
+
- **Skills**: `SKILL.md` files under `.nova-ai/skills` in the workspace or
|
|
184
|
+
`~/.nova-ai/skills` for every project (`.agents`, `.claude` and `.codex`
|
|
185
|
+
skills folders are read too). `/skills` lists them; `/skills off <name>`
|
|
186
|
+
switches one off in this project (`--global`: everywhere). Only names and
|
|
187
|
+
short descriptions go into the prompt; Nova loads a skill when it fits.
|
|
177
188
|
- **MCP servers**: `.mcp.json` or `mcpServers` in `.nova-ai/settings.json`.
|
|
178
189
|
|
|
179
190
|
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 +206,7 @@ hand).
|
|
|
195
206
|
| `NOVA_TOOL_PROTOCOL` | Force `native` or `text` tool calling |
|
|
196
207
|
| `NOVA_NES_MODEL` | Separate model for next-edit suggestions (ACP) |
|
|
197
208
|
| `NOVA_KITTY_KEYBOARD` | `1`/`0`: force the kitty keyboard protocol on or off |
|
|
209
|
+
| `NOVA_NO_UPDATE_CHECK` | Don't ask npm for a newer version (also `NO_UPDATE_NOTIFIER`, or `CI`) |
|
|
198
210
|
|
|
199
211
|
## Development
|
|
200
212
|
|
|
@@ -0,0 +1,147 @@
|
|
|
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
|
+
command: `npm install -g ${CLI_PACKAGE_NAME}@${best.tag}`
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
async function readDistTags(options) {
|
|
48
|
+
const now = (options.now ?? Date.now)();
|
|
49
|
+
const cachePath = options.cachePath === void 0 ? updateCachePath() : options.cachePath;
|
|
50
|
+
const cached = cachePath ? readCache(cachePath) : null;
|
|
51
|
+
if (cached && now - cached.checkedAt >= 0 && now - cached.checkedAt < CACHE_MAX_AGE_MS) {
|
|
52
|
+
return cached.distTags;
|
|
53
|
+
}
|
|
54
|
+
const response = await (options.fetchImpl ?? fetch)(DIST_TAGS_URL, {
|
|
55
|
+
headers: { accept: "application/json" },
|
|
56
|
+
signal: AbortSignal.any([
|
|
57
|
+
AbortSignal.timeout(options.timeoutMs ?? DEFAULT_TIMEOUT_MS),
|
|
58
|
+
...options.signal ? [options.signal] : []
|
|
59
|
+
])
|
|
60
|
+
});
|
|
61
|
+
if (!response.ok) return null;
|
|
62
|
+
const payload = await response.json();
|
|
63
|
+
if (!isDistTags(payload)) return null;
|
|
64
|
+
if (cachePath) writeCache(cachePath, { checkedAt: now, distTags: payload });
|
|
65
|
+
return payload;
|
|
66
|
+
}
|
|
67
|
+
function readCache(path) {
|
|
68
|
+
try {
|
|
69
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
70
|
+
if (value && typeof value === "object" && typeof value.checkedAt === "number" && isDistTags(value.distTags)) {
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
function writeCache(path, value) {
|
|
78
|
+
try {
|
|
79
|
+
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
80
|
+
writeFileSync(path, `${JSON.stringify(value)}
|
|
81
|
+
`, { mode: 384 });
|
|
82
|
+
} catch {
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function isDistTags(value) {
|
|
86
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((version) => typeof version === "string");
|
|
87
|
+
}
|
|
88
|
+
function prereleaseChannel(version) {
|
|
89
|
+
const parsed = parseVersion(version);
|
|
90
|
+
const first = parsed?.prerelease[0];
|
|
91
|
+
return first && !/^\d+$/.test(first) ? first : null;
|
|
92
|
+
}
|
|
93
|
+
function readInstalledVersion() {
|
|
94
|
+
return cliVersion();
|
|
95
|
+
}
|
|
96
|
+
function isNewerVersion(candidate, current) {
|
|
97
|
+
const next = parseVersion(candidate);
|
|
98
|
+
const installed = parseVersion(current);
|
|
99
|
+
if (!next || !installed) return false;
|
|
100
|
+
for (let index = 0; index < 3; index++) {
|
|
101
|
+
const difference = next.core[index] - installed.core[index];
|
|
102
|
+
if (difference !== 0) return difference > 0;
|
|
103
|
+
}
|
|
104
|
+
return comparePrerelease(next.prerelease, installed.prerelease) > 0;
|
|
105
|
+
}
|
|
106
|
+
function parseVersion(value) {
|
|
107
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(value);
|
|
108
|
+
if (!match) return null;
|
|
109
|
+
return {
|
|
110
|
+
core: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
111
|
+
prerelease: match[4]?.split(".") ?? []
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function comparePrerelease(left, right) {
|
|
115
|
+
if (left.length === 0 || right.length === 0) {
|
|
116
|
+
if (left.length === right.length) return 0;
|
|
117
|
+
return left.length === 0 ? 1 : -1;
|
|
118
|
+
}
|
|
119
|
+
const length = Math.max(left.length, right.length);
|
|
120
|
+
for (let index = 0; index < length; index++) {
|
|
121
|
+
const leftPart = left[index];
|
|
122
|
+
const rightPart = right[index];
|
|
123
|
+
if (leftPart === void 0 || rightPart === void 0) {
|
|
124
|
+
return leftPart === rightPart ? 0 : leftPart === void 0 ? -1 : 1;
|
|
125
|
+
}
|
|
126
|
+
if (leftPart === rightPart) continue;
|
|
127
|
+
const leftNumber = /^\d+$/.test(leftPart) ? Number(leftPart) : null;
|
|
128
|
+
const rightNumber = /^\d+$/.test(rightPart) ? Number(rightPart) : null;
|
|
129
|
+
if (leftNumber !== null && rightNumber !== null) return leftNumber - rightNumber;
|
|
130
|
+
if (leftNumber !== null || rightNumber !== null) return leftNumber !== null ? -1 : 1;
|
|
131
|
+
return leftPart < rightPart ? -1 : 1;
|
|
132
|
+
}
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
function formatUpdateNotice(update) {
|
|
136
|
+
return `Update available: nova-ai ${update.currentVersion} \u2192 ${update.latestVersion}. Run: ${update.command}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export {
|
|
140
|
+
updateCachePath,
|
|
141
|
+
updateChecksDisabled,
|
|
142
|
+
checkForUpdate,
|
|
143
|
+
chooseUpdate,
|
|
144
|
+
readInstalledVersion,
|
|
145
|
+
isNewerVersion,
|
|
146
|
+
formatUpdateNotice
|
|
147
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
cliVersion
|
|
4
|
-
} from "./chunk-
|
|
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-OX6WAPNN.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-OX6WAPNN.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
|
-
|
|
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-
|
|
178
|
+
const { default: runChat } = await import("./tui-LNFSHL4Y.js");
|
|
151
179
|
return runChat(options);
|
|
152
180
|
}
|
|
153
181
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
} from "./chunk-
|
|
2
|
+
checkForUpdate
|
|
3
|
+
} from "./chunk-37PMUZ34.js";
|
|
4
|
+
import "./chunk-FMHWW3E6.js";
|
|
4
5
|
import {
|
|
5
6
|
TuiAcpClient,
|
|
6
7
|
createStore,
|
|
@@ -1012,82 +1013,6 @@ function fromAgentPermissionMode(mode) {
|
|
|
1012
1013
|
return mode === "bypassPermissions" ? "bypassAll" : mode === "acceptEdits" ? "acceptEdits" : "ask";
|
|
1013
1014
|
}
|
|
1014
1015
|
|
|
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
1016
|
// src/tui/ink/terminal-keyboard.ts
|
|
1092
1017
|
var KITTY_QUERY = "\x1B[?u";
|
|
1093
1018
|
var DEVICE_ATTRIBUTES_QUERY = "\x1B[c";
|
|
@@ -1808,6 +1733,11 @@ function PlanView({
|
|
|
1808
1733
|
|
|
1809
1734
|
// src/tui/ink/controller.ts
|
|
1810
1735
|
import { setWorkspaceTrusted } from "@datalabrotterdam/nova-ai-agent/core/nova-home.js";
|
|
1736
|
+
import {
|
|
1737
|
+
disabledSkillNames,
|
|
1738
|
+
discoverSkills as discoverSkills2,
|
|
1739
|
+
setSkillEnabled
|
|
1740
|
+
} from "@datalabrotterdam/nova-ai-agent/core/skills.js";
|
|
1811
1741
|
import { INTERACTION_MODES } from "@datalabrotterdam/nova-ai-agent/core/interaction-modes.js";
|
|
1812
1742
|
|
|
1813
1743
|
// src/tui/commands/builtins.ts
|
|
@@ -1884,7 +1814,7 @@ var builtinCommands = [
|
|
|
1884
1814
|
},
|
|
1885
1815
|
{
|
|
1886
1816
|
name: "exit",
|
|
1887
|
-
description: "Exit nova-ai
|
|
1817
|
+
description: "Exit nova-ai",
|
|
1888
1818
|
run(ctx) {
|
|
1889
1819
|
ctx.exit();
|
|
1890
1820
|
}
|
|
@@ -1957,9 +1887,20 @@ var builtinCommands = [
|
|
|
1957
1887
|
},
|
|
1958
1888
|
{
|
|
1959
1889
|
name: "skills",
|
|
1960
|
-
description: "
|
|
1961
|
-
run(ctx) {
|
|
1962
|
-
|
|
1890
|
+
description: "List skills, or switch one: /skills on|off <name> [--global]",
|
|
1891
|
+
run(ctx, args) {
|
|
1892
|
+
const words = args.trim().split(/\s+/).filter(Boolean);
|
|
1893
|
+
if (!words.length) {
|
|
1894
|
+
ctx.openSkillInspector();
|
|
1895
|
+
return;
|
|
1896
|
+
}
|
|
1897
|
+
const global = words.includes("--global");
|
|
1898
|
+
const [action, name] = words.filter((word) => word !== "--global");
|
|
1899
|
+
if (action !== "on" && action !== "off" || !name) {
|
|
1900
|
+
ctx.print("Usage: /skills, or /skills on|off <name> [--global]");
|
|
1901
|
+
return;
|
|
1902
|
+
}
|
|
1903
|
+
ctx.print(ctx.switchSkill(name, action === "on", global));
|
|
1963
1904
|
}
|
|
1964
1905
|
},
|
|
1965
1906
|
{
|
|
@@ -4024,15 +3965,38 @@ status: failed - ${failure.message}`);
|
|
|
4024
3965
|
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
3966
|
);
|
|
4026
3967
|
};
|
|
3968
|
+
/** Every discovered skill with its state; switched-off skills stay listed. */
|
|
4027
3969
|
openSkillInspector = () => {
|
|
4028
|
-
const
|
|
3970
|
+
const cwd = this.runner.cwd;
|
|
3971
|
+
const skills = discoverSkills2(cwd);
|
|
3972
|
+
const disabled = disabledSkillNames(cwd);
|
|
3973
|
+
const on = skills.filter((skill) => !disabled.has(skill.name)).length;
|
|
4029
3974
|
this.showText(
|
|
4030
|
-
`Skills (${skills.length})`,
|
|
4031
|
-
skills.length ?
|
|
3975
|
+
`Skills (${on} on, ${skills.length - on} off)`,
|
|
3976
|
+
skills.length ? [
|
|
3977
|
+
...skills.map(
|
|
3978
|
+
(skill) => `${skill.name} [${skill.source}]${disabled.has(skill.name) ? " (off)" : ""}
|
|
4032
3979
|
${skill.description}
|
|
4033
|
-
${skill.path}`
|
|
3980
|
+
${skill.path}`
|
|
3981
|
+
),
|
|
3982
|
+
"/skills off <name> switches one off in this project (--global: everywhere); /skills on <name> switches it back on."
|
|
3983
|
+
].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
3984
|
);
|
|
4035
3985
|
};
|
|
3986
|
+
/** /skills on|off <name> [--global]: applies from the next message. */
|
|
3987
|
+
switchSkill = (name, enabled, global) => {
|
|
3988
|
+
const cwd = this.runner.cwd;
|
|
3989
|
+
if (!discoverSkills2(cwd).some((skill) => skill.name === name)) {
|
|
3990
|
+
return `No skill named "${name}". /skills lists them.`;
|
|
3991
|
+
}
|
|
3992
|
+
try {
|
|
3993
|
+
setSkillEnabled(cwd, global ? "global" : "project", name, enabled);
|
|
3994
|
+
} catch (error) {
|
|
3995
|
+
return errorMessage(error, "Could not change the skill.");
|
|
3996
|
+
}
|
|
3997
|
+
const stillOff = enabled && disabledSkillNames(cwd).has(name);
|
|
3998
|
+
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.`;
|
|
3999
|
+
};
|
|
4036
4000
|
openUsageInspector = async () => {
|
|
4037
4001
|
this.showText("Context usage", "Calculating context usage...");
|
|
4038
4002
|
try {
|
|
@@ -4119,6 +4083,7 @@ ${skill.path}`).join("\n\n") : "No skills discovered. Add SKILL.md under .agents
|
|
|
4119
4083
|
openToolInspector: this.openToolInspector,
|
|
4120
4084
|
openMcpInspector: this.openMcpInspector,
|
|
4121
4085
|
openSkillInspector: this.openSkillInspector,
|
|
4086
|
+
switchSkill: this.switchSkill,
|
|
4122
4087
|
openUsageInspector: this.openUsageInspector,
|
|
4123
4088
|
steerMessage: (message) => this.runner.steer(message),
|
|
4124
4089
|
queuedMessages: () => this.runner.queuedMessages(),
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {
|
|
2
|
+
checkForUpdate,
|
|
3
|
+
chooseUpdate,
|
|
4
|
+
formatUpdateNotice,
|
|
5
|
+
isNewerVersion,
|
|
6
|
+
readInstalledVersion,
|
|
7
|
+
updateCachePath,
|
|
8
|
+
updateChecksDisabled
|
|
9
|
+
} from "./chunk-37PMUZ34.js";
|
|
10
|
+
import "./chunk-FMHWW3E6.js";
|
|
11
|
+
export {
|
|
12
|
+
checkForUpdate,
|
|
13
|
+
chooseUpdate,
|
|
14
|
+
formatUpdateNotice,
|
|
15
|
+
isNewerVersion,
|
|
16
|
+
readInstalledVersion,
|
|
17
|
+
updateCachePath,
|
|
18
|
+
updateChecksDisabled
|
|
19
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datalabrotterdam/nova-ai-cli",
|
|
3
|
-
"version": "1.2.0-alpha.
|
|
3
|
+
"version": "1.2.0-alpha.3",
|
|
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": [
|