@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 +26 -0
- package/README.md +22 -3
- package/dist/{chunk-SKAW2KXC.js → chunk-FMHWW3E6.js} +1 -0
- package/dist/chunk-L77KQRGZ.js +170 -0
- package/dist/index.js +32 -4
- package/dist/{tui-NJWDD5X7.js → tui-OWU4YGTT.js} +212 -104
- package/dist/update-check-G3PYCRWM.js +23 -0
- package/package.json +2 -3
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 `.
|
|
176
|
-
|
|
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
|
|
|
@@ -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-
|
|
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
|
-
|
|
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-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-
|
|
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,
|
|
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(`..${
|
|
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(
|
|
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
|
|
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: "
|
|
1961
|
-
run(ctx) {
|
|
1962
|
-
|
|
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) =>
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
|
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("~/") ?
|
|
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 :
|
|
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 :
|
|
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
|
-
|
|
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
|
|
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 ?
|
|
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}`
|
|
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 =
|
|
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.
|
|
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.
|
|
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",
|