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

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.
Files changed (50) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/README.md +160 -101
  3. package/dist/chunk-NHGIUEIR.js +754 -0
  4. package/dist/chunk-SKAW2KXC.js +10 -0
  5. package/dist/headless-OFJFVJ3O.js +543 -0
  6. package/dist/index.js +155 -17
  7. package/dist/tui-NJWDD5X7.js +4810 -0
  8. package/dist/webui-L4GGMXJ3.js +9 -0
  9. package/package.json +32 -32
  10. package/dist/acp/agent.d.ts +0 -13
  11. package/dist/acp/agent.js +0 -242
  12. package/dist/acp/auth-server.d.ts +0 -10
  13. package/dist/acp/auth-server.js +0 -67
  14. package/dist/acp/credentials.d.ts +0 -11
  15. package/dist/acp/credentials.js +0 -30
  16. package/dist/acp/index.d.ts +0 -2
  17. package/dist/acp/index.js +0 -21
  18. package/dist/acp/mcp.d.ts +0 -11
  19. package/dist/acp/mcp.js +0 -90
  20. package/dist/acp/sessions.d.ts +0 -20
  21. package/dist/acp/sessions.js +0 -91
  22. package/dist/acp/tools/index.d.ts +0 -6
  23. package/dist/acp/tools/index.js +0 -5
  24. package/dist/acp/tools/marker.d.ts +0 -17
  25. package/dist/acp/tools/marker.js +0 -43
  26. package/dist/acp/tools/permission.d.ts +0 -2
  27. package/dist/acp/tools/permission.js +0 -20
  28. package/dist/acp/tools/read-file.d.ts +0 -2
  29. package/dist/acp/tools/read-file.js +0 -20
  30. package/dist/acp/tools/registry.d.ts +0 -4
  31. package/dist/acp/tools/registry.js +0 -10
  32. package/dist/acp/tools/reporting.d.ts +0 -3
  33. package/dist/acp/tools/reporting.js +0 -26
  34. package/dist/acp/tools/run-command.d.ts +0 -2
  35. package/dist/acp/tools/run-command.js +0 -29
  36. package/dist/acp/tools/system-prompt.d.ts +0 -2
  37. package/dist/acp/tools/system-prompt.js +0 -23
  38. package/dist/acp/tools/types.d.ts +0 -18
  39. package/dist/acp/tools/types.js +0 -1
  40. package/dist/acp/tools/write-file.d.ts +0 -2
  41. package/dist/acp/tools/write-file.js +0 -20
  42. package/dist/index.d.ts +0 -2
  43. package/dist/tui/index.d.ts +0 -7
  44. package/dist/tui/index.js +0 -11
  45. package/dist/webui/index.d.ts +0 -6
  46. package/dist/webui/index.js +0 -9
  47. package/src/acp/web/dist/assets/index-CSccI7lJ.css +0 -1
  48. package/src/acp/web/dist/assets/index-D2Z0sfLR.js +0 -1
  49. package/src/acp/web/dist/favicon.svg +0 -46
  50. package/src/acp/web/dist/index.html +0 -32
@@ -0,0 +1,9 @@
1
+ // src/webui/index.ts
2
+ async function runWebUi() {
3
+ console.log("nova-ai --web is a work in progress \u2014 the browser UI isn't built yet.");
4
+ console.log("Use `nova-ai` for the terminal UI, `nova-ai -p` headless, or `nova-ai --acp` for editors.");
5
+ }
6
+ var webui_default = runWebUi;
7
+ export {
8
+ webui_default as default
9
+ };
package/package.json CHANGED
@@ -1,58 +1,58 @@
1
1
  {
2
2
  "name": "@datalabrotterdam/nova-ai-cli",
3
- "version": "1.1.0",
4
- "description": "CLI for DataLab Rotterdam's Nova AI, with an Agent Client Protocol mode for editors/IDEs, backed by @datalabrotterdam/nova-sdk",
3
+ "version": "1.2.0-alpha.2",
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",
7
7
  "type": "module",
8
- "main": "dist/index.js",
9
- "types": "dist/index.d.ts",
10
- "bin": {
11
- "nova-ai-cli": "dist/index.js",
12
- "nova-ai": "dist/index.js"
13
- },
14
- "files": [
15
- "dist",
16
- "src/acp/web/dist",
17
- "LICENSE"
18
- ],
19
8
  "engines": {
20
- "node": ">=18"
9
+ "node": ">=22.19"
21
10
  },
22
11
  "repository": {
23
12
  "type": "git",
24
- "url": "git+https://github.com/Datalab-Rotterdam/nova-ai-cli.git"
13
+ "url": "git+https://github.com/Datalab-Rotterdam/nova-ai-cli.git",
14
+ "directory": "packages/cli"
25
15
  },
26
16
  "bugs": {
27
17
  "url": "https://github.com/Datalab-Rotterdam/nova-ai-cli/issues"
28
18
  },
29
- "homepage": "https://github.com/Datalab-Rotterdam/nova-ai-cli#readme",
30
19
  "publishConfig": {
31
20
  "access": "public"
32
21
  },
22
+ "homepage": "https://github.com/Datalab-Rotterdam/nova-ai-cli#readme",
23
+ "bin": {
24
+ "nova-ai-cli": "dist/index.js",
25
+ "nova-ai": "dist/index.js"
26
+ },
27
+ "files": [
28
+ "dist",
29
+ "LICENSE",
30
+ "README.md",
31
+ "CHANGELOG.md"
32
+ ],
33
33
  "scripts": {
34
- "build": "npm install --prefix src/acp/web && npm run build --prefix src/acp/web && tsc -p tsconfig.json",
35
- "dev": "tsc -p tsconfig.json --watch",
36
- "test": "tsc -p tsconfig.json --noEmit && npm run test:unit",
37
- "test:unit": "node -e \"require('fs').rmSync('dist-test',{recursive:true,force:true})\" && tsc -p tsconfig.test.json && node --test \"dist-test/tests/**/*.test.js\""
34
+ "build": "tsup",
35
+ "dev": "tsx watch src/index.ts",
36
+ "typecheck": "tsc -p tsconfig.json --noEmit",
37
+ "test": "npm run typecheck && npm run test:unit && npm run test:pty",
38
+ "test:unit": "node -e \"require('fs').rmSync('dist-test',{recursive:true,force:true})\" && tsc -p tsconfig.test.json && node --import ./dist-test/test-support/setup-env.js --test --test-timeout=30000 \"dist-test/packages/cli/tests/**/*.test.js\"",
39
+ "test:pty": "tsx tests/tui/pty-runtime-runner.ts"
38
40
  },
39
41
  "dependencies": {
40
42
  "@agentclientprotocol/sdk": "^0.29.0",
41
- "@datalabrotterdam/nova-ai-cli": "^1.0.1",
43
+ "@datalabrotterdam/nova-ai-agent": "^1.0.0-alpha.1",
42
44
  "@datalabrotterdam/nova-sdk": "^1.3.2",
43
- "@modelcontextprotocol/sdk": "^1.29.0",
44
- "@sourceregistry/node-webserver": "^1.8.0"
45
- },
46
- "devDependencies": {
47
- "@semantic-release/changelog": "^6.0.3",
48
- "@semantic-release/commit-analyzer": "^13.0.1",
49
- "@semantic-release/git": "^10.0.1",
50
- "@semantic-release/npm": "^13.1.5",
51
- "@semantic-release/release-notes-generator": "^14.1.0",
52
- "@types/node": "^22.20.0",
53
- "typescript": "^5.6.0"
45
+ "chalk": "^5.3.0",
46
+ "cli-highlight": "^2.1.11",
47
+ "diff": "^9.0.0",
48
+ "ink": "^7.1.1",
49
+ "marked": "^18.0.14",
50
+ "react": "^19.3.0",
51
+ "string-width": "^8.3.0"
54
52
  },
55
53
  "release": {
54
+ "extends": "semantic-release-monorepo",
55
+ "tagFormat": "@datalabrotterdam/nova-ai-cli@${version}",
56
56
  "branches": [
57
57
  "main",
58
58
  {
@@ -1,13 +0,0 @@
1
- import * as acp from "@agentclientprotocol/sdk";
2
- export declare class NovaAgent {
3
- private readonly sessions;
4
- private clientCapabilities;
5
- initialize(params: acp.InitializeRequest): acp.InitializeResponse;
6
- newSession(params: acp.NewSessionRequest): Promise<acp.NewSessionResponse>;
7
- loadSession(params: acp.LoadSessionRequest, client: acp.AgentContext): Promise<acp.LoadSessionResponse>;
8
- listSessions(params: acp.ListSessionsRequest): acp.ListSessionsResponse;
9
- authenticate(params: acp.AuthenticateRequest): Promise<acp.AuthenticateResponse>;
10
- prompt(params: acp.PromptRequest, client: acp.AgentContext): Promise<acp.PromptResponse>;
11
- cancel(params: acp.CancelNotification): void;
12
- closeSession(params: acp.CloseSessionRequest): Promise<acp.CloseSessionResponse>;
13
- }
package/dist/acp/agent.js DELETED
@@ -1,242 +0,0 @@
1
- import * as acp from "@agentclientprotocol/sdk";
2
- import { NovaAI, NovaAIError } from "@datalabrotterdam/nova-sdk";
3
- import { runBrowserAuth } from "./auth-server.js";
4
- import { readCredentials } from "./credentials.js";
5
- import { closeMcpConnections, connectMcpServers, listMcpTools } from "./mcp.js";
6
- import { appendSessionTurn, deriveTitle, listStoredSessions, loadStoredSession } from "./sessions.js";
7
- import { availableTools, buildToolsSystemPrompt, extractToolCall, hasPendingFence, requestPermissionIfNeeded, sendToolCallPending, sendToolCallUpdate, } from "./tools/index.js";
8
- const AUTH_METHOD_ID = "nova-api-key";
9
- const MAX_TOOL_ROUNDS = 10;
10
- export class NovaAgent {
11
- sessions = new Map();
12
- clientCapabilities;
13
- initialize(params) {
14
- this.clientCapabilities = params.clientCapabilities;
15
- return {
16
- protocolVersion: acp.PROTOCOL_VERSION,
17
- agentCapabilities: {
18
- loadSession: true,
19
- promptCapabilities: {
20
- // Image/audio content isn't forwarded to the model; only text and
21
- // embedded text resources are supported.
22
- embeddedContext: true,
23
- },
24
- mcpCapabilities: {
25
- http: true,
26
- sse: true,
27
- },
28
- sessionCapabilities: {
29
- list: {},
30
- close: {},
31
- },
32
- },
33
- authMethods: [
34
- {
35
- id: AUTH_METHOD_ID,
36
- name: "Nova API Key",
37
- description: "Opens a local page in your browser to connect your DataLab Rotterdam Nova AI account.",
38
- },
39
- ],
40
- };
41
- }
42
- async newSession(params) {
43
- const sessionId = crypto.randomUUID();
44
- const mcpConnections = await connectMcpServers(params.mcpServers);
45
- const mcpTools = await listMcpTools(mcpConnections);
46
- this.sessions.set(sessionId, {
47
- pendingPrompt: null,
48
- cwd: params.cwd,
49
- history: [],
50
- title: null,
51
- mcpConnections,
52
- mcpTools,
53
- });
54
- return { sessionId };
55
- }
56
- async loadSession(params, client) {
57
- const stored = loadStoredSession(params.sessionId);
58
- if (!stored) {
59
- throw acp.RequestError.internalError(undefined, `Session ${params.sessionId} not found`);
60
- }
61
- const mcpConnections = await connectMcpServers(params.mcpServers);
62
- const mcpTools = await listMcpTools(mcpConnections);
63
- this.sessions.set(params.sessionId, {
64
- pendingPrompt: null,
65
- cwd: params.cwd,
66
- history: stored.messages,
67
- title: stored.title,
68
- mcpConnections,
69
- mcpTools,
70
- });
71
- for (const message of stored.messages) {
72
- if (typeof message.content !== "string" || !message.content)
73
- continue;
74
- if (message.role === "user") {
75
- await client.notify("session/update", {
76
- sessionId: params.sessionId,
77
- update: { sessionUpdate: "user_message_chunk", content: { type: "text", text: message.content } },
78
- });
79
- }
80
- else if (message.role === "assistant") {
81
- await client.notify("session/update", {
82
- sessionId: params.sessionId,
83
- update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text: message.content } },
84
- });
85
- }
86
- }
87
- return {};
88
- }
89
- listSessions(params) {
90
- const sessions = listStoredSessions(params.cwd ?? undefined).map((s) => ({
91
- sessionId: s.sessionId,
92
- cwd: s.cwd,
93
- title: s.title,
94
- updatedAt: s.updatedAt,
95
- }));
96
- return { sessions };
97
- }
98
- async authenticate(params) {
99
- if (params.methodId !== AUTH_METHOD_ID) {
100
- throw new Error(`Unknown auth method: ${params.methodId}`);
101
- }
102
- await runBrowserAuth();
103
- return {};
104
- }
105
- async prompt(params, client) {
106
- const session = this.sessions.get(params.sessionId);
107
- if (!session) {
108
- throw new Error(`Session ${params.sessionId} not found`);
109
- }
110
- const credentials = readCredentials();
111
- if (!credentials) {
112
- throw acp.RequestError.authRequired();
113
- }
114
- session.pendingPrompt?.abort();
115
- const abortController = new AbortController();
116
- session.pendingPrompt = abortController;
117
- const novaClient = new NovaAI({ apiKey: credentials.apiKey });
118
- const model = credentials.defaultModel ?? process.env.NOVA_MODEL;
119
- if (!model) {
120
- throw new Error("No Nova model configured. Re-run authentication or set NOVA_MODEL.");
121
- }
122
- const tools = [...availableTools(this.clientCapabilities), ...session.mcpTools];
123
- const findSessionTool = (name) => tools.find((tool) => tool.name === name);
124
- const systemPrompt = buildToolsSystemPrompt(tools, session.cwd);
125
- const userMessage = { role: "user", content: contentBlocksToText(params.prompt) };
126
- session.history.push(userMessage);
127
- const turnMessages = [userMessage];
128
- const messages = [
129
- ...(systemPrompt ? [{ role: "system", content: systemPrompt }] : []),
130
- ...session.history,
131
- ];
132
- const pushTurn = (message) => {
133
- session.history.push(message);
134
- messages.push(message);
135
- turnMessages.push(message);
136
- };
137
- try {
138
- for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
139
- if (abortController.signal.aborted) {
140
- return { stopReason: "cancelled" };
141
- }
142
- let buffer = "";
143
- let flushed = 0;
144
- for await (const event of novaClient.chat.completions.stream({ model, messages }, { signal: abortController.signal })) {
145
- if (event.type !== "chunk")
146
- continue;
147
- const text = event.data.choices?.[0]?.delta?.content;
148
- if (typeof text !== "string" || text.length === 0)
149
- continue;
150
- buffer += text;
151
- if (!hasPendingFence(buffer)) {
152
- const toFlush = buffer.slice(flushed);
153
- if (toFlush) {
154
- await client.notify("session/update", {
155
- sessionId: params.sessionId,
156
- update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text: toFlush } },
157
- });
158
- flushed = buffer.length;
159
- }
160
- }
161
- }
162
- const toolCall = extractToolCall(buffer);
163
- if (!toolCall) {
164
- const remaining = buffer.slice(flushed);
165
- if (remaining) {
166
- await client.notify("session/update", {
167
- sessionId: params.sessionId,
168
- update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text: remaining } },
169
- });
170
- }
171
- pushTurn({ role: "assistant", content: buffer });
172
- return { stopReason: "end_turn" };
173
- }
174
- const tool = findSessionTool(toolCall.name);
175
- const toolCallId = crypto.randomUUID();
176
- const toolCtx = { client, sessionId: params.sessionId, signal: abortController.signal };
177
- pushTurn({ role: "assistant", content: buffer });
178
- if (!tool) {
179
- pushTurn({ role: "user", content: `Tool "${toolCall.name}" is not available.` });
180
- continue;
181
- }
182
- await sendToolCallPending(toolCtx, toolCallId, tool, toolCall.args);
183
- const allowed = await requestPermissionIfNeeded(toolCtx, toolCallId, tool, toolCall.args);
184
- if (!allowed) {
185
- await sendToolCallUpdate(toolCtx, toolCallId, "failed", "Permission denied by user.");
186
- pushTurn({ role: "user", content: "Tool call rejected by user." });
187
- continue;
188
- }
189
- const result = await tool.execute(toolCtx, toolCall.args);
190
- if ("error" in result) {
191
- await sendToolCallUpdate(toolCtx, toolCallId, "failed", result.error);
192
- pushTurn({ role: "user", content: `Tool error: ${result.error}` });
193
- }
194
- else {
195
- await sendToolCallUpdate(toolCtx, toolCallId, "completed", result.output);
196
- pushTurn({ role: "user", content: `Tool result: ${result.output}` });
197
- }
198
- }
199
- return { stopReason: "max_turn_requests" };
200
- }
201
- catch (err) {
202
- if (abortController.signal.aborted) {
203
- return { stopReason: "cancelled" };
204
- }
205
- if (err instanceof NovaAIError) {
206
- throw new Error(`Nova AI request failed (status ${err.status}, requestId ${err.requestId}): ${err.message}`);
207
- }
208
- throw err;
209
- }
210
- finally {
211
- session.pendingPrompt = null;
212
- session.title ??= deriveTitle(session.history);
213
- appendSessionTurn(params.sessionId, { cwd: session.cwd, title: session.title }, turnMessages);
214
- }
215
- }
216
- cancel(params) {
217
- this.sessions.get(params.sessionId)?.pendingPrompt?.abort();
218
- }
219
- async closeSession(params) {
220
- const session = this.sessions.get(params.sessionId);
221
- if (session) {
222
- session.pendingPrompt?.abort();
223
- await closeMcpConnections(session.mcpConnections);
224
- this.sessions.delete(params.sessionId);
225
- }
226
- return {};
227
- }
228
- }
229
- function contentBlocksToText(prompt) {
230
- return prompt
231
- .map((block) => {
232
- if (block.type === "text")
233
- return block.text;
234
- if (block.type === "resource_link")
235
- return block.uri;
236
- if (block.type === "resource" && "text" in block.resource)
237
- return block.resource.text;
238
- return "";
239
- })
240
- .filter(Boolean)
241
- .join("\n");
242
- }
@@ -1,10 +0,0 @@
1
- import { credentialsPath } from "./credentials.js";
2
- /**
3
- * Agent Auth flow (AUTHENTICATION.md, type "agent"): the agent itself runs a
4
- * local HTTP server and opens the browser. Today the page just collects a
5
- * Nova API key; swapping to real OAuth later means changing what this page
6
- * does (redirect to the provider's authorize URL, exchange the callback code)
7
- * without touching the server lifecycle or the rest of the agent.
8
- */
9
- export declare function runBrowserAuth(): Promise<void>;
10
- export { credentialsPath };
@@ -1,67 +0,0 @@
1
- import { exec } from "node:child_process";
2
- import { dirname, join } from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- import { NovaAI, NovaAIError } from "@datalabrotterdam/nova-sdk";
5
- import { dir, json, WebServer } from "@sourceregistry/node-webserver";
6
- import { credentialsPath, writeCredentials } from "./credentials.js";
7
- const AUTH_TIMEOUT_MS = 5 * 60 * 1000;
8
- const UI_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "src", "acp", "web", "dist");
9
- function openBrowser(url) {
10
- const cmd = process.platform === "darwin"
11
- ? `open "${url}"`
12
- : process.platform === "win32"
13
- ? `start "" "${url}"`
14
- : `xdg-open "${url}"`;
15
- exec(cmd, () => {
16
- // best-effort; the URL is always printed as a fallback
17
- });
18
- }
19
- /**
20
- * Agent Auth flow (AUTHENTICATION.md, type "agent"): the agent itself runs a
21
- * local HTTP server and opens the browser. Today the page just collects a
22
- * Nova API key; swapping to real OAuth later means changing what this page
23
- * does (redirect to the provider's authorize URL, exchange the callback code)
24
- * without touching the server lifecycle or the rest of the agent.
25
- */
26
- export function runBrowserAuth() {
27
- return new Promise((resolve, reject) => {
28
- const app = new WebServer();
29
- app.POST("/api/authenticate", async (event) => {
30
- const body = await event.request.json().catch(() => null);
31
- const apiKey = typeof body?.apiKey === "string" ? body.apiKey.trim() : "";
32
- if (!apiKey) {
33
- return json({ message: "API key is required." }, { status: 400 });
34
- }
35
- try {
36
- const client = new NovaAI({ apiKey });
37
- const { data: models } = await client.models.list();
38
- const defaultModel = (models.find((m) => m.enabled !== false) ?? models[0])?.id;
39
- writeCredentials({ apiKey, defaultModel });
40
- clearTimeout(timeout);
41
- const response = json({ ok: true });
42
- void app.shutdown().then(resolve);
43
- return response;
44
- }
45
- catch (err) {
46
- const message = err instanceof NovaAIError
47
- ? `Nova AI rejected this key (status ${err.status}): ${err.message}`
48
- : "Unexpected error validating the key.";
49
- return json({ message }, { status: 401 });
50
- }
51
- });
52
- app.GET("/", dir(UI_DIR, { spa: true }));
53
- app.GET("/[...path]", dir(UI_DIR, { spa: true }));
54
- const timeout = setTimeout(() => {
55
- void app.shutdown();
56
- reject(new Error("Nova AI authentication timed out after 5 minutes."));
57
- }, AUTH_TIMEOUT_MS);
58
- app.listen(0, "127.0.0.1", () => {
59
- const address = app.address();
60
- const port = typeof address === "object" && address ? address.port : 0;
61
- const url = `http://127.0.0.1:${port}`;
62
- console.error(`Opening browser to authenticate with Nova AI: ${url}`);
63
- openBrowser(url);
64
- });
65
- });
66
- }
67
- export { credentialsPath };
@@ -1,11 +0,0 @@
1
- export type StoredCredentials = {
2
- apiKey: string;
3
- defaultModel?: string;
4
- };
5
- /**
6
- * Single place that owns "how we get the Nova API key", so swapping this for
7
- * an OAuth-based Agent Auth flow later only touches this file.
8
- */
9
- export declare function readCredentials(): StoredCredentials | null;
10
- export declare function writeCredentials(credentials: StoredCredentials): void;
11
- export declare function credentialsPath(): string;
@@ -1,30 +0,0 @@
1
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { dirname, join } from "node:path";
4
- const CREDENTIALS_PATH = join(homedir(), ".nova-ai", "credentials.json");
5
- /**
6
- * Single place that owns "how we get the Nova API key", so swapping this for
7
- * an OAuth-based Agent Auth flow later only touches this file.
8
- */
9
- export function readCredentials() {
10
- const envApiKey = process.env.NOVA_API_KEY;
11
- if (envApiKey) {
12
- return { apiKey: envApiKey, defaultModel: process.env.NOVA_MODEL };
13
- }
14
- try {
15
- const raw = readFileSync(CREDENTIALS_PATH, "utf8");
16
- return JSON.parse(raw);
17
- }
18
- catch {
19
- return null;
20
- }
21
- }
22
- export function writeCredentials(credentials) {
23
- mkdirSync(dirname(CREDENTIALS_PATH), { recursive: true });
24
- writeFileSync(CREDENTIALS_PATH, JSON.stringify(credentials, null, 2), {
25
- mode: 0o600,
26
- });
27
- }
28
- export function credentialsPath() {
29
- return CREDENTIALS_PATH;
30
- }
@@ -1,2 +0,0 @@
1
- declare function runAcp(...args: string[]): Promise<void>;
2
- export default runAcp;
package/dist/acp/index.js DELETED
@@ -1,21 +0,0 @@
1
- import { Readable, Writable } from "node:stream";
2
- import * as acp from "@agentclientprotocol/sdk";
3
- import { NovaAgent } from "./agent.js";
4
- async function runAcp(...args) {
5
- const agentImpl = new NovaAgent();
6
- const output = Writable.toWeb(process.stdout);
7
- const input = Readable.toWeb(process.stdin);
8
- const stream = acp.ndJsonStream(output, input);
9
- acp
10
- .agent({ name: "nova-ai-cli" })
11
- .onRequest("initialize", (ctx) => agentImpl.initialize(ctx.params))
12
- .onRequest("session/new", (ctx) => agentImpl.newSession(ctx.params))
13
- .onRequest("session/load", (ctx) => agentImpl.loadSession(ctx.params, ctx.client))
14
- .onRequest("session/list", (ctx) => agentImpl.listSessions(ctx.params))
15
- .onRequest("session/close", (ctx) => agentImpl.closeSession(ctx.params))
16
- .onRequest("authenticate", (ctx) => agentImpl.authenticate(ctx.params))
17
- .onRequest("session/prompt", (ctx) => agentImpl.prompt(ctx.params, ctx.client))
18
- .onNotification("session/cancel", (ctx) => agentImpl.cancel(ctx.params))
19
- .connect(stream);
20
- }
21
- export default runAcp;
package/dist/acp/mcp.d.ts DELETED
@@ -1,11 +0,0 @@
1
- import type * as acp from "@agentclientprotocol/sdk";
2
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
- import type { ToolDefinition } from "./tools/types.js";
4
- export type McpConnection = {
5
- serverName: string;
6
- client: Client;
7
- close(): Promise<void>;
8
- };
9
- export declare function connectMcpServers(servers: acp.McpServer[]): Promise<McpConnection[]>;
10
- export declare function closeMcpConnections(connections: McpConnection[]): Promise<void>;
11
- export declare function listMcpTools(connections: McpConnection[]): Promise<ToolDefinition[]>;
package/dist/acp/mcp.js DELETED
@@ -1,90 +0,0 @@
1
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
- import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
3
- import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
4
- import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
5
- const TOOL_NAME_PREFIX = "mcp__";
6
- function sanitize(name) {
7
- return name.replace(/[^a-zA-Z0-9_-]/g, "_");
8
- }
9
- function headersToRecord(headers) {
10
- return Object.fromEntries(headers.map((h) => [h.name, h.value]));
11
- }
12
- function buildTransport(server) {
13
- if ("type" in server && server.type === "http") {
14
- return new StreamableHTTPClientTransport(new URL(server.url), {
15
- requestInit: { headers: headersToRecord(server.headers) },
16
- });
17
- }
18
- if ("type" in server && server.type === "sse") {
19
- return new SSEClientTransport(new URL(server.url), {
20
- requestInit: { headers: headersToRecord(server.headers) },
21
- });
22
- }
23
- const stdio = server;
24
- return new StdioClientTransport({
25
- command: stdio.command,
26
- args: stdio.args,
27
- env: Object.fromEntries(stdio.env.map((e) => [e.name, e.value])),
28
- });
29
- }
30
- export async function connectMcpServers(servers) {
31
- const connections = [];
32
- for (const server of servers) {
33
- if ("type" in server && server.type === "acp")
34
- continue; // experimental ACP-transport MCP, not yet supported
35
- const client = new Client({ name: "nova-ai-cli", version: "1.0.0" });
36
- const transport = buildTransport(server);
37
- try {
38
- await client.connect(transport);
39
- connections.push({ serverName: server.name, client, close: () => client.close() });
40
- }
41
- catch (err) {
42
- console.error(`Failed to connect MCP server "${server.name}": ${err.message}`);
43
- }
44
- }
45
- return connections;
46
- }
47
- export async function closeMcpConnections(connections) {
48
- await Promise.all(connections.map((c) => c.close().catch(() => { })));
49
- }
50
- function toToolResult(result) {
51
- const text = Array.isArray(result.content)
52
- ? result.content
53
- .map((block) => {
54
- if (typeof block === "object" && block !== null && "type" in block && block.type === "text") {
55
- return block.text;
56
- }
57
- return "";
58
- })
59
- .filter(Boolean)
60
- .join("\n")
61
- : "";
62
- if (result.isError)
63
- return { error: text || "MCP tool call failed." };
64
- return { output: text };
65
- }
66
- export async function listMcpTools(connections) {
67
- const tools = [];
68
- for (const connection of connections) {
69
- const { tools: serverTools } = await connection.client.listTools();
70
- for (const tool of serverTools) {
71
- const qualifiedName = `${TOOL_NAME_PREFIX}${sanitize(connection.serverName)}__${sanitize(tool.name)}`;
72
- tools.push({
73
- name: qualifiedName,
74
- description: `${qualifiedName}: ${tool.description ?? tool.name} (from MCP server "${connection.serverName}")`,
75
- requiredCapability: () => true,
76
- mutating: true,
77
- async execute(_ctx, args) {
78
- try {
79
- const result = await connection.client.callTool({ name: tool.name, arguments: args });
80
- return toToolResult(result);
81
- }
82
- catch (err) {
83
- return { error: err.message };
84
- }
85
- },
86
- });
87
- }
88
- }
89
- return tools;
90
- }
@@ -1,20 +0,0 @@
1
- import type { ChatMessage } from "@datalabrotterdam/nova-sdk";
2
- export type StoredSession = {
3
- sessionId: string;
4
- cwd: string;
5
- title: string | null;
6
- updatedAt: string;
7
- messages: ChatMessage[];
8
- };
9
- export declare function loadStoredSession(sessionId: string): StoredSession | null;
10
- /**
11
- * Each turn is appended as its own line rather than rewriting the whole
12
- * session file, so persisting after every prompt stays O(1) instead of
13
- * O(history length).
14
- */
15
- export declare function appendSessionTurn(sessionId: string, header: {
16
- cwd: string;
17
- title: string | null;
18
- }, newMessages: ChatMessage[]): void;
19
- export declare function listStoredSessions(cwd?: string): StoredSession[];
20
- export declare function deriveTitle(messages: ChatMessage[]): string | null;