@datalabrotterdam/nova-ai-cli 1.2.0-alpha.1 → 1.2.0-alpha.10
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 +165 -0
- package/README.md +74 -32
- package/dist/browser-setup-7SWHUQEL.js +57 -0
- package/dist/chunk-FMHWW3E6.js +11 -0
- package/dist/{chunk-ZKRZIF3S.js → chunk-KDAUUQBE.js} +49 -360
- package/dist/chunk-L77KQRGZ.js +170 -0
- package/dist/{headless-ZYUSTZDL.js → headless-BQXK7TNC.js} +29 -46
- package/dist/index.js +57 -19
- package/dist/{tui-XOKXPXN2.js → tui-TJFV6NC6.js} +719 -335
- package/dist/update-check-G3PYCRWM.js +23 -0
- package/package.json +25 -39
- package/dist/acp-ZYOQ6PON.js +0 -65
- package/dist/chunk-GZZ7PY3S.js +0 -8878
- package/dist/chunk-JAEXUFAS.js +0 -451
- package/dist/chunk-Q7WL3DXP.js +0 -19
- package/dist/index.d.ts +0 -1
- package/dist/login-6U7QS7EA.js +0 -118
- package/dist/webui-L4GGMXJ3.js +0 -9
- package/src/acp/web/dist/assets/index-CSccI7lJ.css +0 -1
- package/src/acp/web/dist/assets/index-DlCz3fmq.js +0 -1
- package/src/acp/web/dist/favicon.svg +0 -46
- package/src/acp/web/dist/index.html +0 -32
|
@@ -1,362 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
NOVA_METHODS,
|
|
3
|
-
NOVA_NOTIFICATIONS,
|
|
4
|
-
NovaAgent,
|
|
5
|
-
createAgentApp,
|
|
6
|
-
fromElicitationRequest,
|
|
7
|
-
isInteractionMode,
|
|
8
|
-
readSettings,
|
|
9
|
-
resolveWorkspaceFile,
|
|
10
|
-
toElicitationResponse
|
|
11
|
-
} from "./chunk-GZZ7PY3S.js";
|
|
12
|
-
import {
|
|
13
|
-
isWorkspaceTrusted
|
|
14
|
-
} from "./chunk-JAEXUFAS.js";
|
|
15
|
-
|
|
16
|
-
// src/client/in-process.ts
|
|
17
|
-
import * as acp from "@agentclientprotocol/sdk";
|
|
18
|
-
|
|
19
|
-
// src/client/nova-agent-client.ts
|
|
20
|
-
var NovaAgentClient = class {
|
|
21
|
-
constructor(connection) {
|
|
22
|
-
this.connection = connection;
|
|
23
|
-
}
|
|
24
|
-
connection;
|
|
25
|
-
initialize(params) {
|
|
26
|
-
return this.connection.initialize(params);
|
|
27
|
-
}
|
|
28
|
-
newSession(params) {
|
|
29
|
-
return this.connection.newSession(params);
|
|
30
|
-
}
|
|
31
|
-
loadSession(params) {
|
|
32
|
-
return this.connection.loadSession(params);
|
|
33
|
-
}
|
|
34
|
-
resumeSession(params) {
|
|
35
|
-
return this.connection.resumeSession(params);
|
|
36
|
-
}
|
|
37
|
-
closeSession(params) {
|
|
38
|
-
return this.connection.closeSession(params);
|
|
39
|
-
}
|
|
40
|
-
prompt(params) {
|
|
41
|
-
return this.connection.prompt(params);
|
|
42
|
-
}
|
|
43
|
-
cancel(params) {
|
|
44
|
-
return this.connection.cancel(params);
|
|
45
|
-
}
|
|
46
|
-
setSessionMode(params) {
|
|
47
|
-
return this.connection.setSessionMode(params);
|
|
48
|
-
}
|
|
49
|
-
setSessionConfigOption(params) {
|
|
50
|
-
return this.connection.setSessionConfigOption(params);
|
|
51
|
-
}
|
|
52
|
-
queueEnqueue(params) {
|
|
53
|
-
return this.ext(NOVA_METHODS.queueEnqueue, params);
|
|
54
|
-
}
|
|
55
|
-
queueList(params) {
|
|
56
|
-
return this.ext(NOVA_METHODS.queueList, params);
|
|
57
|
-
}
|
|
58
|
-
queueEditBegin(params) {
|
|
59
|
-
return this.ext(NOVA_METHODS.queueEditBegin, params);
|
|
60
|
-
}
|
|
61
|
-
queueUpdate(params) {
|
|
62
|
-
return this.ext(NOVA_METHODS.queueUpdate, params);
|
|
63
|
-
}
|
|
64
|
-
queueRemove(params) {
|
|
65
|
-
return this.ext(NOVA_METHODS.queueRemove, params);
|
|
66
|
-
}
|
|
67
|
-
queueClear(params) {
|
|
68
|
-
return this.ext(NOVA_METHODS.queueClear, params);
|
|
69
|
-
}
|
|
70
|
-
queueTakeNext(params) {
|
|
71
|
-
return this.ext(NOVA_METHODS.queueTakeNext, params);
|
|
72
|
-
}
|
|
73
|
-
sessionCompact(params) {
|
|
74
|
-
return this.ext(
|
|
75
|
-
NOVA_METHODS.sessionCompact,
|
|
76
|
-
params
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
sessionContextUsage(params) {
|
|
80
|
-
return this.ext(NOVA_METHODS.sessionContextUsage, params);
|
|
81
|
-
}
|
|
82
|
-
sessionRewind(params) {
|
|
83
|
-
return this.ext(NOVA_METHODS.sessionRewind, params);
|
|
84
|
-
}
|
|
85
|
-
backgroundStartTerminal(params) {
|
|
86
|
-
return this.ext(NOVA_METHODS.backgroundStartTerminal, params);
|
|
87
|
-
}
|
|
88
|
-
backgroundStartPrompt(params) {
|
|
89
|
-
return this.ext(NOVA_METHODS.backgroundStartPrompt, params);
|
|
90
|
-
}
|
|
91
|
-
backgroundList(params) {
|
|
92
|
-
return this.ext(NOVA_METHODS.backgroundList, params);
|
|
93
|
-
}
|
|
94
|
-
backgroundOutput(params) {
|
|
95
|
-
return this.ext(NOVA_METHODS.backgroundOutput, params);
|
|
96
|
-
}
|
|
97
|
-
backgroundKill(params) {
|
|
98
|
-
return this.ext(NOVA_METHODS.backgroundKill, params);
|
|
99
|
-
}
|
|
100
|
-
backgroundRelease(params) {
|
|
101
|
-
return this.ext(NOVA_METHODS.backgroundRelease, params);
|
|
102
|
-
}
|
|
103
|
-
async ext(method, params) {
|
|
104
|
-
return await this.connection.extMethod(method, params);
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
// src/client/in-process.ts
|
|
109
|
-
function startInProcessAgent(toClient, agent = new NovaAgent()) {
|
|
110
|
-
const toAgent = new TransformStream();
|
|
111
|
-
const toClientStream = new TransformStream();
|
|
112
|
-
const agentConnection = createAgentApp(agent).connect(
|
|
113
|
-
acp.ndJsonStream(toClientStream.writable, toAgent.readable)
|
|
114
|
-
);
|
|
115
|
-
const connection = new acp.ClientSideConnection(
|
|
116
|
-
toClient,
|
|
117
|
-
acp.ndJsonStream(toAgent.writable, toClientStream.readable)
|
|
118
|
-
);
|
|
119
|
-
return {
|
|
120
|
-
agent,
|
|
121
|
-
client: new NovaAgentClient(connection),
|
|
122
|
-
async close() {
|
|
123
|
-
await agent.shutdown().catch(() => {
|
|
124
|
-
});
|
|
125
|
-
agentConnection.close();
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// src/tui/settings/workspace-mcp.ts
|
|
131
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
132
|
-
import { join as join2 } from "path";
|
|
133
|
-
|
|
134
|
-
// src/tui/settings/workspace-settings.ts
|
|
135
|
-
import { readFileSync } from "fs";
|
|
136
|
-
import { join } from "path";
|
|
137
|
-
function settingsPath(cwd) {
|
|
138
|
-
return join(cwd, ".nova-ai", "settings.json");
|
|
139
|
-
}
|
|
140
|
-
function readWorkspaceSettings(cwd) {
|
|
141
|
-
try {
|
|
142
|
-
const raw = readFileSync(settingsPath(cwd), "utf8");
|
|
143
|
-
return JSON.parse(raw);
|
|
144
|
-
} catch {
|
|
145
|
-
return {};
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// src/tui/settings/workspace-mcp.ts
|
|
150
|
-
var UNTRUSTED_MCP_MESSAGE = "Not started: MCP servers declared by this workspace only run once you trust it (/trust in the TUI, --trust-workspace headless).";
|
|
151
|
-
function readWorkspaceMcpConfiguration(cwd, options = {}) {
|
|
152
|
-
const configured = readWorkspaceSettings(cwd).mcpServers ?? [];
|
|
153
|
-
const project = readProjectMcpConfiguration(cwd);
|
|
154
|
-
const servers = new Map(configured.map((server) => [server.name, server]));
|
|
155
|
-
for (const server of project.servers) servers.set(server.name, server);
|
|
156
|
-
const declared = [...servers.values()];
|
|
157
|
-
if (declared.length && !(options.trusted ?? isWorkspaceTrusted(cwd))) {
|
|
158
|
-
return {
|
|
159
|
-
servers: [],
|
|
160
|
-
failures: [
|
|
161
|
-
...project.failures,
|
|
162
|
-
...declared.map((server) => ({ serverName: server.name, message: UNTRUSTED_MCP_MESSAGE }))
|
|
163
|
-
],
|
|
164
|
-
untrusted: declared.map((server) => server.name)
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
return { servers: declared, failures: project.failures };
|
|
168
|
-
}
|
|
169
|
-
function workspaceNeedsTrust(cwd) {
|
|
170
|
-
if (isWorkspaceTrusted(cwd)) return false;
|
|
171
|
-
if (readWorkspaceMcpConfiguration(cwd, { trusted: true }).servers.length) return true;
|
|
172
|
-
return loadWorkspaceAllowRules(cwd).length > 0;
|
|
173
|
-
}
|
|
174
|
-
function loadWorkspaceAllowRules(cwd) {
|
|
175
|
-
return ["settings.json", "settings.local.json"].flatMap((name) => {
|
|
176
|
-
const allow = readSettings(join2(cwd, ".nova-ai", name))?.permissions?.allow;
|
|
177
|
-
return Array.isArray(allow) ? allow : [];
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
function readProjectMcpConfiguration(cwd) {
|
|
181
|
-
const path = join2(cwd, ".mcp.json");
|
|
182
|
-
let raw;
|
|
183
|
-
try {
|
|
184
|
-
raw = readFileSync2(path, "utf8");
|
|
185
|
-
} catch (error) {
|
|
186
|
-
if (error.code === "ENOENT") return { servers: [], failures: [] };
|
|
187
|
-
return { servers: [], failures: [{ serverName: ".mcp.json", message: errorMessage(error, "Could not read file") }] };
|
|
188
|
-
}
|
|
189
|
-
let document;
|
|
190
|
-
try {
|
|
191
|
-
document = JSON.parse(raw);
|
|
192
|
-
} catch (error) {
|
|
193
|
-
return { servers: [], failures: [{ serverName: ".mcp.json", message: errorMessage(error, "Invalid JSON") }] };
|
|
194
|
-
}
|
|
195
|
-
const root = asRecord(document);
|
|
196
|
-
const definitions = root ? asRecord(root.mcpServers) : null;
|
|
197
|
-
if (!definitions) {
|
|
198
|
-
return {
|
|
199
|
-
servers: [],
|
|
200
|
-
failures: [{ serverName: ".mcp.json", message: 'Expected an "mcpServers" object.' }]
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
const servers = [];
|
|
204
|
-
const failures = [];
|
|
205
|
-
for (const [name, value] of Object.entries(definitions)) {
|
|
206
|
-
const config = asRecord(value);
|
|
207
|
-
if (config?.disabled === true || config?.enabled === false) continue;
|
|
208
|
-
try {
|
|
209
|
-
servers.push(normalizeProjectServer(name, config));
|
|
210
|
-
} catch (error) {
|
|
211
|
-
failures.push({ serverName: `.mcp.json:${name}`, message: errorMessage(error, "Invalid server configuration") });
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
return { servers, failures };
|
|
215
|
-
}
|
|
216
|
-
function normalizeProjectServer(name, config) {
|
|
217
|
-
if (!config) throw new Error("Server definition must be an object.");
|
|
218
|
-
const type = typeof config.type === "string" ? config.type : config.url ? "http" : "stdio";
|
|
219
|
-
if (type === "http" || type === "sse") {
|
|
220
|
-
if (typeof config.url !== "string" || !config.url) throw new Error(`${type} server requires a URL.`);
|
|
221
|
-
try {
|
|
222
|
-
new URL(config.url);
|
|
223
|
-
} catch {
|
|
224
|
-
throw new Error(`${type} server requires a valid URL.`);
|
|
225
|
-
}
|
|
226
|
-
return {
|
|
227
|
-
name,
|
|
228
|
-
type,
|
|
229
|
-
url: config.url,
|
|
230
|
-
headers: normalizePairs(config.headers, "headers")
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
if (type !== "stdio") throw new Error(`Unsupported transport type: ${type}.`);
|
|
234
|
-
if (typeof config.command !== "string" || !config.command) throw new Error("stdio server requires a command.");
|
|
235
|
-
if (config.args !== void 0 && (!Array.isArray(config.args) || !config.args.every((arg) => typeof arg === "string"))) {
|
|
236
|
-
throw new Error("args must be an array of strings.");
|
|
237
|
-
}
|
|
238
|
-
return {
|
|
239
|
-
name,
|
|
240
|
-
command: config.command,
|
|
241
|
-
args: config.args ?? [],
|
|
242
|
-
env: normalizePairs(config.env, "env")
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
function normalizePairs(value, label) {
|
|
246
|
-
if (value === void 0) return [];
|
|
247
|
-
if (Array.isArray(value)) {
|
|
248
|
-
if (!value.every((entry) => {
|
|
249
|
-
const pair = asRecord(entry);
|
|
250
|
-
return typeof pair?.name === "string" && typeof pair.value === "string";
|
|
251
|
-
})) {
|
|
252
|
-
throw new Error(`${label} must contain name/value strings.`);
|
|
253
|
-
}
|
|
254
|
-
return value.map((entry) => {
|
|
255
|
-
const pair = entry;
|
|
256
|
-
return { name: pair.name, value: pair.value };
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
const record = asRecord(value);
|
|
260
|
-
if (!record || !Object.values(record).every((entry) => typeof entry === "string")) {
|
|
261
|
-
throw new Error(`${label} must be an object of string values.`);
|
|
262
|
-
}
|
|
263
|
-
return Object.entries(record).map(([name, entry]) => ({ name, value: entry }));
|
|
264
|
-
}
|
|
265
|
-
function asRecord(value) {
|
|
266
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
267
|
-
}
|
|
268
|
-
function errorMessage(error, fallback) {
|
|
269
|
-
return error instanceof Error && error.message ? `${fallback}: ${error.message}` : fallback;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
1
|
// src/tui/session/tui-acp-client.ts
|
|
2
|
+
import { NOVA_NOTIFICATIONS } from "@datalabrotterdam/nova-ai-agent/acp/extensions.js";
|
|
3
|
+
import { openBrowser } from "@datalabrotterdam/nova-ai-agent/acp/auth-server.js";
|
|
273
4
|
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
274
5
|
import { dirname } from "path";
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
env: options.env ?? process.env,
|
|
286
|
-
detached: process.platform !== "win32",
|
|
287
|
-
windowsHide: true,
|
|
288
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
289
|
-
};
|
|
290
|
-
const child = options.args?.length ? spawn(command, options.args, common) : spawn(command, { ...common, shell: true });
|
|
291
|
-
liveChildren.add(child);
|
|
292
|
-
const forget = () => liveChildren.delete(child);
|
|
293
|
-
child.once("exit", forget);
|
|
294
|
-
child.once("error", forget);
|
|
295
|
-
return child;
|
|
296
|
-
}
|
|
297
|
-
function killProcessTree(child, signal = "SIGTERM") {
|
|
298
|
-
if (child.pid === void 0) return;
|
|
299
|
-
try {
|
|
300
|
-
if (process.platform === "win32") {
|
|
301
|
-
spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
|
|
302
|
-
stdio: "ignore",
|
|
303
|
-
windowsHide: true
|
|
304
|
-
}).on("error", () => {
|
|
305
|
-
});
|
|
306
|
-
} else {
|
|
307
|
-
process.kill(-child.pid, signal);
|
|
308
|
-
}
|
|
309
|
-
} catch {
|
|
310
|
-
try {
|
|
311
|
-
child.kill(signal);
|
|
312
|
-
} catch {
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
function terminateProcessTree(child, graceMs = KILL_GRACE_MS) {
|
|
317
|
-
if (hasExited(child)) return Promise.resolve();
|
|
318
|
-
return new Promise((resolve) => {
|
|
319
|
-
let forceTimer;
|
|
320
|
-
let giveUpTimer;
|
|
321
|
-
const done = () => {
|
|
322
|
-
clearTimeout(forceTimer);
|
|
323
|
-
clearTimeout(giveUpTimer);
|
|
324
|
-
resolve();
|
|
325
|
-
};
|
|
326
|
-
child.once("exit", done);
|
|
327
|
-
killProcessTree(child, "SIGTERM");
|
|
328
|
-
forceTimer = setTimeout(() => {
|
|
329
|
-
killProcessTree(child, "SIGKILL");
|
|
330
|
-
giveUpTimer = setTimeout(done, graceMs);
|
|
331
|
-
}, graceMs);
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
function hasExited(child) {
|
|
335
|
-
return child.exitCode !== null || child.signalCode !== null;
|
|
336
|
-
}
|
|
337
|
-
function installExitHook() {
|
|
338
|
-
if (exitHookInstalled) return;
|
|
339
|
-
exitHookInstalled = true;
|
|
340
|
-
process.once("exit", () => {
|
|
341
|
-
for (const child of liveChildren) {
|
|
342
|
-
if (process.platform === "win32" && child.pid !== void 0) {
|
|
343
|
-
try {
|
|
344
|
-
spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
|
|
345
|
-
stdio: "ignore",
|
|
346
|
-
windowsHide: true,
|
|
347
|
-
timeout: 2e3
|
|
348
|
-
});
|
|
349
|
-
} catch {
|
|
350
|
-
}
|
|
351
|
-
} else {
|
|
352
|
-
killProcessTree(child, "SIGKILL");
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
liveChildren.clear();
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// src/tui/session/tui-acp-client.ts
|
|
6
|
+
import {
|
|
7
|
+
spawnCommand,
|
|
8
|
+
terminateProcessTree
|
|
9
|
+
} from "@datalabrotterdam/nova-ai-agent/core/process-tree.js";
|
|
10
|
+
import { resolveWorkspaceFile } from "@datalabrotterdam/nova-ai-agent/core/tools/workspace-paths.js";
|
|
11
|
+
import {
|
|
12
|
+
fromElicitationRequest,
|
|
13
|
+
toElicitationResponse
|
|
14
|
+
} from "@datalabrotterdam/nova-ai-agent/acp/user-questions.js";
|
|
15
|
+
import { INTERACTION_MODES } from "@datalabrotterdam/nova-ai-agent/core/interaction-modes.js";
|
|
360
16
|
var DEFAULT_OUTPUT_LIMIT = 1e5;
|
|
361
17
|
var nextMessageId = 0;
|
|
362
18
|
function uid() {
|
|
@@ -374,6 +30,10 @@ var TuiAcpClient = class {
|
|
|
374
30
|
cwd;
|
|
375
31
|
onBackgroundLifecycle;
|
|
376
32
|
onInteractionModeChange;
|
|
33
|
+
/** The modes the agent offers (built-in, then the user's agent files); set by the runner. */
|
|
34
|
+
modeIds = [...INTERACTION_MODES];
|
|
35
|
+
/** Slash commands the agent understands, such as the user's command files. */
|
|
36
|
+
agentCommands = [];
|
|
377
37
|
capabilities = {
|
|
378
38
|
fs: { readTextFile: true, writeTextFile: true },
|
|
379
39
|
terminal: true,
|
|
@@ -434,6 +94,29 @@ var TuiAcpClient = class {
|
|
|
434
94
|
appendUserMessage(text) {
|
|
435
95
|
this.appendMessage({ id: uid(), role: "user", text });
|
|
436
96
|
}
|
|
97
|
+
/** An MCP server's sign-in page: opened in the browser, and printed in case that fails. */
|
|
98
|
+
mcpAuthUrl(params) {
|
|
99
|
+
const { serverName, url } = params ?? {};
|
|
100
|
+
if (typeof serverName !== "string" || typeof url !== "string") return;
|
|
101
|
+
let page;
|
|
102
|
+
try {
|
|
103
|
+
page = new URL(url);
|
|
104
|
+
} catch {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (page.protocol !== "https:" && page.protocol !== "http:") {
|
|
108
|
+
this.appendError(`MCP server "${serverName}" sent a sign-in address that is not a web page; not opened.`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
openBrowser(page.toString());
|
|
112
|
+
this.appendMessage({
|
|
113
|
+
id: uid(),
|
|
114
|
+
role: "assistant",
|
|
115
|
+
streaming: false,
|
|
116
|
+
text: `Sign in to MCP server "${serverName}" in your browser. If it did not open, visit:
|
|
117
|
+
${page}`
|
|
118
|
+
});
|
|
119
|
+
}
|
|
437
120
|
appendError(text) {
|
|
438
121
|
this.appendMessage({ id: uid(), role: "error", text });
|
|
439
122
|
}
|
|
@@ -637,8 +320,15 @@ ${err.message}`;
|
|
|
637
320
|
});
|
|
638
321
|
}
|
|
639
322
|
return;
|
|
323
|
+
case "available_commands_update":
|
|
324
|
+
this.agentCommands = update.availableCommands.map(({ name, description, input }) => ({
|
|
325
|
+
name,
|
|
326
|
+
description,
|
|
327
|
+
...input && "hint" in input && input.hint ? { hint: input.hint } : {}
|
|
328
|
+
}));
|
|
329
|
+
return;
|
|
640
330
|
case "current_mode_update":
|
|
641
|
-
if (
|
|
331
|
+
if (this.modeIds.includes(update.currentModeId)) {
|
|
642
332
|
this.onInteractionModeChange?.(update.currentModeId);
|
|
643
333
|
}
|
|
644
334
|
return;
|
|
@@ -758,6 +448,8 @@ ${err.message}`;
|
|
|
758
448
|
return this.backgroundUpdate(params);
|
|
759
449
|
case NOVA_NOTIFICATIONS.queueChanged:
|
|
760
450
|
return this.queueChanged(params);
|
|
451
|
+
case NOVA_NOTIFICATIONS.mcpAuthUrl:
|
|
452
|
+
return this.mcpAuthUrl(params);
|
|
761
453
|
default:
|
|
762
454
|
return;
|
|
763
455
|
}
|
|
@@ -945,9 +637,6 @@ function createStore(initial) {
|
|
|
945
637
|
}
|
|
946
638
|
|
|
947
639
|
export {
|
|
948
|
-
startInProcessAgent,
|
|
949
|
-
readWorkspaceMcpConfiguration,
|
|
950
|
-
workspaceNeedsTrust,
|
|
951
640
|
TuiAcpClient,
|
|
952
641
|
choosePermissionOption,
|
|
953
642
|
createStore
|
|
@@ -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
|
+
};
|