@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.
- package/CHANGELOG.md +70 -0
- package/README.md +160 -101
- package/dist/chunk-NHGIUEIR.js +754 -0
- package/dist/chunk-SKAW2KXC.js +10 -0
- package/dist/headless-OFJFVJ3O.js +543 -0
- package/dist/index.js +155 -17
- package/dist/tui-NJWDD5X7.js +4810 -0
- package/dist/webui-L4GGMXJ3.js +9 -0
- package/package.json +32 -32
- package/dist/acp/agent.d.ts +0 -13
- package/dist/acp/agent.js +0 -242
- package/dist/acp/auth-server.d.ts +0 -10
- package/dist/acp/auth-server.js +0 -67
- package/dist/acp/credentials.d.ts +0 -11
- package/dist/acp/credentials.js +0 -30
- package/dist/acp/index.d.ts +0 -2
- package/dist/acp/index.js +0 -21
- package/dist/acp/mcp.d.ts +0 -11
- package/dist/acp/mcp.js +0 -90
- package/dist/acp/sessions.d.ts +0 -20
- package/dist/acp/sessions.js +0 -91
- package/dist/acp/tools/index.d.ts +0 -6
- package/dist/acp/tools/index.js +0 -5
- package/dist/acp/tools/marker.d.ts +0 -17
- package/dist/acp/tools/marker.js +0 -43
- package/dist/acp/tools/permission.d.ts +0 -2
- package/dist/acp/tools/permission.js +0 -20
- package/dist/acp/tools/read-file.d.ts +0 -2
- package/dist/acp/tools/read-file.js +0 -20
- package/dist/acp/tools/registry.d.ts +0 -4
- package/dist/acp/tools/registry.js +0 -10
- package/dist/acp/tools/reporting.d.ts +0 -3
- package/dist/acp/tools/reporting.js +0 -26
- package/dist/acp/tools/run-command.d.ts +0 -2
- package/dist/acp/tools/run-command.js +0 -29
- package/dist/acp/tools/system-prompt.d.ts +0 -2
- package/dist/acp/tools/system-prompt.js +0 -23
- package/dist/acp/tools/types.d.ts +0 -18
- package/dist/acp/tools/types.js +0 -1
- package/dist/acp/tools/write-file.d.ts +0 -2
- package/dist/acp/tools/write-file.js +0 -20
- package/dist/index.d.ts +0 -2
- package/dist/tui/index.d.ts +0 -7
- package/dist/tui/index.js +0 -11
- package/dist/webui/index.d.ts +0 -6
- package/dist/webui/index.js +0 -9
- package/src/acp/web/dist/assets/index-CSccI7lJ.css +0 -1
- package/src/acp/web/dist/assets/index-D2Z0sfLR.js +0 -1
- package/src/acp/web/dist/favicon.svg +0 -46
- package/src/acp/web/dist/index.html +0 -32
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// src/version.ts
|
|
2
|
+
import { readPackageVersion } from "@datalabrotterdam/nova-ai-agent/core/version.js";
|
|
3
|
+
var CLI_PACKAGE_NAME = "@datalabrotterdam/nova-ai-cli";
|
|
4
|
+
function cliVersion() {
|
|
5
|
+
return readPackageVersion(CLI_PACKAGE_NAME, import.meta.url);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
cliVersion
|
|
10
|
+
};
|
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TuiAcpClient,
|
|
3
|
+
choosePermissionOption,
|
|
4
|
+
createStore,
|
|
5
|
+
readWorkspaceMcpConfiguration
|
|
6
|
+
} from "./chunk-NHGIUEIR.js";
|
|
7
|
+
|
|
8
|
+
// src/headless/index.ts
|
|
9
|
+
import { resolve } from "path";
|
|
10
|
+
import { NOVA_NOTIFICATIONS } from "@datalabrotterdam/nova-ai-agent/acp/extensions.js";
|
|
11
|
+
import * as acp from "@agentclientprotocol/sdk";
|
|
12
|
+
import { NovaAgent } from "@datalabrotterdam/nova-ai-agent/acp/agent.js";
|
|
13
|
+
import { clientFromContext } from "@datalabrotterdam/nova-ai-agent/client/context-client.js";
|
|
14
|
+
import { startInProcessAgent } from "@datalabrotterdam/nova-ai-agent/client/in-process.js";
|
|
15
|
+
import { setWorkspaceTrusted } from "@datalabrotterdam/nova-ai-agent/core/nova-home.js";
|
|
16
|
+
import { listStoredSessions, loadStoredSession } from "@datalabrotterdam/nova-ai-agent/core/sessions.js";
|
|
17
|
+
|
|
18
|
+
// src/headless/client.ts
|
|
19
|
+
var HeadlessAcpClient = class {
|
|
20
|
+
constructor(cwd, permissionMode, emit) {
|
|
21
|
+
this.permissionMode = permissionMode;
|
|
22
|
+
this.emit = emit;
|
|
23
|
+
const store = createStore({
|
|
24
|
+
messages: [],
|
|
25
|
+
plan: [],
|
|
26
|
+
pendingPermission: null,
|
|
27
|
+
pendingQuestion: null,
|
|
28
|
+
inputHistory: [],
|
|
29
|
+
busy: false,
|
|
30
|
+
mode: "chat",
|
|
31
|
+
permissionMode: "ask",
|
|
32
|
+
interactionMode: "agent",
|
|
33
|
+
sessionId: "headless",
|
|
34
|
+
cwd,
|
|
35
|
+
statusLine: null,
|
|
36
|
+
queuedCount: 0,
|
|
37
|
+
contextUsage: null,
|
|
38
|
+
updateAvailable: null
|
|
39
|
+
});
|
|
40
|
+
this.base = new TuiAcpClient(store, cwd);
|
|
41
|
+
this.baseContext = this.base.context();
|
|
42
|
+
}
|
|
43
|
+
permissionMode;
|
|
44
|
+
emit;
|
|
45
|
+
capabilities = {
|
|
46
|
+
fs: { readTextFile: true, writeTextFile: true },
|
|
47
|
+
terminal: true
|
|
48
|
+
};
|
|
49
|
+
base;
|
|
50
|
+
baseContext;
|
|
51
|
+
toolNames = /* @__PURE__ */ new Map();
|
|
52
|
+
context() {
|
|
53
|
+
return {
|
|
54
|
+
request: (method, params, options) => this.handleRequest(method, params, options),
|
|
55
|
+
notify: (method, params) => this.handleNotification(method, params)
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
async close() {
|
|
59
|
+
await this.base.releaseAllTerminals();
|
|
60
|
+
}
|
|
61
|
+
async handleRequest(method, params, options) {
|
|
62
|
+
options?.cancellationSignal?.throwIfAborted();
|
|
63
|
+
if (method === "session/request_permission") {
|
|
64
|
+
return this.requestPermission(params);
|
|
65
|
+
}
|
|
66
|
+
if (method === "elicitation/create") {
|
|
67
|
+
return { action: "decline" };
|
|
68
|
+
}
|
|
69
|
+
return this.baseContext.request(method, params, options);
|
|
70
|
+
}
|
|
71
|
+
async handleNotification(method, params) {
|
|
72
|
+
await this.emit({ type: "notification", method, params });
|
|
73
|
+
if (method === "session/update") await this.reportAgentDecision(params);
|
|
74
|
+
await this.baseContext.notify(method, params);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The agent's policy already applied rules and the mode (set from
|
|
78
|
+
* --permission-mode); what still reaches us needs a person, and there is
|
|
79
|
+
* none: only bypass-all approves it.
|
|
80
|
+
*/
|
|
81
|
+
async requestPermission(params) {
|
|
82
|
+
const toolName = this.toolName(params.toolCall);
|
|
83
|
+
const allow = this.permissionMode === "bypass-all";
|
|
84
|
+
const optionId = choosePermissionOption(params.options, allow, "once") ?? (allow ? "allow_once" : "reject_once");
|
|
85
|
+
await this.emit({
|
|
86
|
+
type: "permission",
|
|
87
|
+
toolCallId: params.toolCall.toolCallId,
|
|
88
|
+
toolName,
|
|
89
|
+
decision: allow ? "allow" : "reject",
|
|
90
|
+
source: this.permissionMode
|
|
91
|
+
});
|
|
92
|
+
return { outcome: { outcome: "selected", optionId } };
|
|
93
|
+
}
|
|
94
|
+
/** Reports decisions the agent's policy made without asking. */
|
|
95
|
+
async reportAgentDecision(params) {
|
|
96
|
+
const update = params?.update;
|
|
97
|
+
if (!update) return;
|
|
98
|
+
const toolCallId = typeof update.toolCallId === "string" ? update.toolCallId : null;
|
|
99
|
+
if (!toolCallId) return;
|
|
100
|
+
if (update.sessionUpdate === "tool_call") {
|
|
101
|
+
this.toolNames.set(toolCallId, this.toolName(update));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const permission = update._meta?.["nova-ai-cli/permission"];
|
|
105
|
+
if (update.sessionUpdate !== "tool_call_update" || !permission) return;
|
|
106
|
+
await this.emit({
|
|
107
|
+
type: "permission",
|
|
108
|
+
toolCallId,
|
|
109
|
+
toolName: this.toolNames.get(toolCallId) ?? "tool",
|
|
110
|
+
decision: permission.decision === "allow" ? "allow" : "reject",
|
|
111
|
+
source: permission.reason === "mode" ? this.permissionMode : "rule"
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
toolName(call) {
|
|
115
|
+
const fromMeta = call._meta?.["nova-ai-cli/tool"];
|
|
116
|
+
return typeof fromMeta === "string" ? fromMeta : call.title ?? "tool";
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// src/headless/index.ts
|
|
121
|
+
var AGENT_PERMISSION_MODE = {
|
|
122
|
+
"read-only": "default",
|
|
123
|
+
"accept-edits": "acceptEdits",
|
|
124
|
+
"bypass-all": "bypassPermissions"
|
|
125
|
+
};
|
|
126
|
+
async function runHeadless(args, dependencies = {}) {
|
|
127
|
+
const stdout = dependencies.stdout ?? process.stdout;
|
|
128
|
+
const stderr = dependencies.stderr ?? process.stderr;
|
|
129
|
+
let options;
|
|
130
|
+
try {
|
|
131
|
+
options = parseHeadlessArgs(args, process.cwd());
|
|
132
|
+
} catch (error) {
|
|
133
|
+
const message = errorMessage(error);
|
|
134
|
+
if (args.includes("--json") || args.some((arg) => /^--output-format(=|$)/.test(arg) && !arg.endsWith("=text"))) {
|
|
135
|
+
return writeFailure(true, stdout, stderr, message);
|
|
136
|
+
}
|
|
137
|
+
stderr.write(`${message}
|
|
138
|
+
|
|
139
|
+
${HEADLESS_HELP}
|
|
140
|
+
`);
|
|
141
|
+
return 1;
|
|
142
|
+
}
|
|
143
|
+
if (options.help) {
|
|
144
|
+
stdout.write(`${HEADLESS_HELP}
|
|
145
|
+
`);
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
148
|
+
let prompt = options.prompt;
|
|
149
|
+
if (!prompt) {
|
|
150
|
+
const canReadStdin = dependencies.readStdin || !process.stdin.isTTY;
|
|
151
|
+
if (canReadStdin) {
|
|
152
|
+
prompt = (await (dependencies.readStdin ?? readStdin)()).trim();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (!prompt) {
|
|
156
|
+
return writeFailure(
|
|
157
|
+
options.outputFormat !== "text",
|
|
158
|
+
stdout,
|
|
159
|
+
stderr,
|
|
160
|
+
"Headless mode requires a prompt argument or piped stdin."
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
if (options.continueLast) {
|
|
164
|
+
const latest = listStoredSessions(options.cwd)[0];
|
|
165
|
+
if (!latest) {
|
|
166
|
+
return writeFailure(
|
|
167
|
+
options.outputFormat !== "text",
|
|
168
|
+
stdout,
|
|
169
|
+
stderr,
|
|
170
|
+
`No earlier session in ${options.cwd} to continue.`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
options.resume = latest.sessionId;
|
|
174
|
+
}
|
|
175
|
+
if (options.resume) {
|
|
176
|
+
const stored = loadStoredSession(options.resume);
|
|
177
|
+
if (!stored) {
|
|
178
|
+
return writeFailure(
|
|
179
|
+
options.outputFormat !== "text",
|
|
180
|
+
stdout,
|
|
181
|
+
stderr,
|
|
182
|
+
`Session ${options.resume} not found.`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
options.cwd = stored.cwd;
|
|
186
|
+
}
|
|
187
|
+
const output = createOutput(options.outputFormat, stdout, stderr);
|
|
188
|
+
const client = (dependencies.clientFactory ?? ((cwd, permissionMode, emit) => new HeadlessAcpClient(cwd, permissionMode, emit)))(options.cwd, options.permissionMode, output.event);
|
|
189
|
+
const connection = startInProcessAgent(
|
|
190
|
+
() => clientFromContext(client.context()),
|
|
191
|
+
dependencies.agent ?? new NovaAgent()
|
|
192
|
+
);
|
|
193
|
+
const agent = connection.client;
|
|
194
|
+
if (options.trustWorkspace) setWorkspaceTrusted(options.cwd, true);
|
|
195
|
+
const mcpConfiguration = options.mcp ? readWorkspaceMcpConfiguration(options.cwd) : { servers: [], failures: [] };
|
|
196
|
+
for (const failure of mcpConfiguration.failures) {
|
|
197
|
+
output.write({
|
|
198
|
+
type: "mcp.configuration_error",
|
|
199
|
+
serverName: failure.serverName,
|
|
200
|
+
message: failure.message
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
let sessionId = null;
|
|
204
|
+
let interrupted = false;
|
|
205
|
+
const onInterrupt = () => {
|
|
206
|
+
interrupted = true;
|
|
207
|
+
if (sessionId) void agent.cancel({ sessionId }).catch(() => {
|
|
208
|
+
});
|
|
209
|
+
};
|
|
210
|
+
if (dependencies.registerSignals !== false)
|
|
211
|
+
process.once("SIGINT", onInterrupt);
|
|
212
|
+
try {
|
|
213
|
+
await agent.initialize({
|
|
214
|
+
protocolVersion: acp.PROTOCOL_VERSION,
|
|
215
|
+
clientCapabilities: client.capabilities,
|
|
216
|
+
clientInfo: { name: "nova-ai-cli-headless", version: "1" }
|
|
217
|
+
});
|
|
218
|
+
if (options.resume) {
|
|
219
|
+
await agent.resumeSession({
|
|
220
|
+
sessionId: options.resume,
|
|
221
|
+
cwd: options.cwd,
|
|
222
|
+
mcpServers: mcpConfiguration.servers
|
|
223
|
+
});
|
|
224
|
+
sessionId = options.resume;
|
|
225
|
+
} else {
|
|
226
|
+
const created = await agent.newSession({
|
|
227
|
+
cwd: options.cwd,
|
|
228
|
+
mcpServers: mcpConfiguration.servers
|
|
229
|
+
});
|
|
230
|
+
sessionId = created.sessionId;
|
|
231
|
+
}
|
|
232
|
+
await agent.setSessionConfigOption({
|
|
233
|
+
sessionId,
|
|
234
|
+
configId: "permission_mode",
|
|
235
|
+
value: AGENT_PERMISSION_MODE[options.permissionMode]
|
|
236
|
+
});
|
|
237
|
+
output.write({
|
|
238
|
+
type: "session.started",
|
|
239
|
+
sessionId,
|
|
240
|
+
cwd: options.cwd,
|
|
241
|
+
model: options.model,
|
|
242
|
+
resumed: Boolean(options.resume),
|
|
243
|
+
permissionMode: options.permissionMode
|
|
244
|
+
});
|
|
245
|
+
output.write({ type: "message", role: "user", text: prompt });
|
|
246
|
+
const response = await agent.prompt({
|
|
247
|
+
sessionId,
|
|
248
|
+
prompt: [{ type: "text", text: prompt }],
|
|
249
|
+
...options.model ? { _meta: { "nova-ai-cli/model": options.model } } : {}
|
|
250
|
+
});
|
|
251
|
+
const exitCode = interrupted ? 130 : response.stopReason === "max_turn_requests" ? 3 : response.stopReason === "cancelled" ? 2 : 0;
|
|
252
|
+
output.finishText();
|
|
253
|
+
output.write({
|
|
254
|
+
type: "result",
|
|
255
|
+
sessionId,
|
|
256
|
+
stopReason: response.stopReason,
|
|
257
|
+
exitCode
|
|
258
|
+
});
|
|
259
|
+
return exitCode;
|
|
260
|
+
} catch (error) {
|
|
261
|
+
output.finishText();
|
|
262
|
+
output.error(errorMessage(error));
|
|
263
|
+
return 1;
|
|
264
|
+
} finally {
|
|
265
|
+
if (dependencies.registerSignals !== false)
|
|
266
|
+
process.removeListener("SIGINT", onInterrupt);
|
|
267
|
+
if (sessionId) await agent.closeSession({ sessionId }).catch(() => ({}));
|
|
268
|
+
await connection.close().catch(() => {
|
|
269
|
+
});
|
|
270
|
+
await client.close().catch(() => {
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function parseHeadlessArgs(args, defaultCwd) {
|
|
275
|
+
const options = {
|
|
276
|
+
prompt: null,
|
|
277
|
+
cwd: resolve(defaultCwd),
|
|
278
|
+
model: null,
|
|
279
|
+
resume: null,
|
|
280
|
+
continueLast: false,
|
|
281
|
+
outputFormat: "text",
|
|
282
|
+
mcp: true,
|
|
283
|
+
trustWorkspace: false,
|
|
284
|
+
permissionMode: "read-only",
|
|
285
|
+
help: false
|
|
286
|
+
};
|
|
287
|
+
const positional = [];
|
|
288
|
+
for (let index = 0; index < args.length; index++) {
|
|
289
|
+
const arg = args[index];
|
|
290
|
+
if (arg === "--") {
|
|
291
|
+
positional.push(...args.slice(index + 1));
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
if (arg === "--json") options.outputFormat = "stream-json";
|
|
295
|
+
else if (arg === "--continue" || arg === "-c") options.continueLast = true;
|
|
296
|
+
else if (arg === "--output-format")
|
|
297
|
+
options.outputFormat = parseOutputFormat(takeValue(args, ++index, arg));
|
|
298
|
+
else if (arg.startsWith("--output-format="))
|
|
299
|
+
options.outputFormat = parseOutputFormat(arg.slice("--output-format=".length));
|
|
300
|
+
else if (arg === "--print" || arg === "-p") {
|
|
301
|
+
const next = args[index + 1];
|
|
302
|
+
if (next !== void 0 && !next.startsWith("-")) {
|
|
303
|
+
options.prompt = next;
|
|
304
|
+
index++;
|
|
305
|
+
}
|
|
306
|
+
} else if (arg === "--no-mcp") options.mcp = false;
|
|
307
|
+
else if (arg === "--trust-workspace") options.trustWorkspace = true;
|
|
308
|
+
else if (arg === "--help" || arg === "-h") options.help = true;
|
|
309
|
+
else if (arg === "--prompt")
|
|
310
|
+
options.prompt = takeValue(args, ++index, arg);
|
|
311
|
+
else if (arg.startsWith("--prompt="))
|
|
312
|
+
options.prompt = arg.slice("--prompt=".length);
|
|
313
|
+
else if (arg === "--cwd")
|
|
314
|
+
options.cwd = resolve(takeValue(args, ++index, arg));
|
|
315
|
+
else if (arg.startsWith("--cwd="))
|
|
316
|
+
options.cwd = resolve(arg.slice("--cwd=".length));
|
|
317
|
+
else if (arg === "--model") options.model = takeValue(args, ++index, arg);
|
|
318
|
+
else if (arg.startsWith("--model="))
|
|
319
|
+
options.model = arg.slice("--model=".length);
|
|
320
|
+
else if (arg === "--resume") options.resume = takeValue(args, ++index, arg);
|
|
321
|
+
else if (arg.startsWith("--resume="))
|
|
322
|
+
options.resume = arg.slice("--resume=".length);
|
|
323
|
+
else if (arg === "--permission-mode")
|
|
324
|
+
options.permissionMode = parsePermissionMode(
|
|
325
|
+
takeValue(args, ++index, arg)
|
|
326
|
+
);
|
|
327
|
+
else if (arg.startsWith("--permission-mode="))
|
|
328
|
+
options.permissionMode = parsePermissionMode(
|
|
329
|
+
arg.slice("--permission-mode=".length)
|
|
330
|
+
);
|
|
331
|
+
else if (arg.startsWith("-")) throw new Error(`Unknown option: ${arg}`);
|
|
332
|
+
else positional.push(arg);
|
|
333
|
+
}
|
|
334
|
+
if (options.prompt && positional.length) {
|
|
335
|
+
throw new Error(
|
|
336
|
+
"Provide the prompt either with --prompt or positionally, not both."
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
if (!options.prompt && positional.length)
|
|
340
|
+
options.prompt = positional.join(" ");
|
|
341
|
+
if (options.continueLast && options.resume) {
|
|
342
|
+
throw new Error("Use either --continue or --resume, not both.");
|
|
343
|
+
}
|
|
344
|
+
if (options.prompt !== null && !options.prompt.trim()) {
|
|
345
|
+
throw new Error("Prompt cannot be empty.");
|
|
346
|
+
}
|
|
347
|
+
return options;
|
|
348
|
+
}
|
|
349
|
+
function createOutput(format, stdout, stderr) {
|
|
350
|
+
let wroteText = false;
|
|
351
|
+
const summary = {
|
|
352
|
+
text: "",
|
|
353
|
+
tools: [],
|
|
354
|
+
permissions: []
|
|
355
|
+
};
|
|
356
|
+
const write = (record) => {
|
|
357
|
+
if (format === "stream-json") {
|
|
358
|
+
stdout.write(`${JSON.stringify({ schemaVersion: 1, ...record })}
|
|
359
|
+
`);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if (format !== "json") return;
|
|
363
|
+
if (record.type === "result") {
|
|
364
|
+
stdout.write(
|
|
365
|
+
`${JSON.stringify({ schemaVersion: 1, ...record, text: summary.text, tools: summary.tools, permissions: summary.permissions })}
|
|
366
|
+
`
|
|
367
|
+
);
|
|
368
|
+
} else if (record.type === "error") {
|
|
369
|
+
stdout.write(`${JSON.stringify({ schemaVersion: 1, ...record })}
|
|
370
|
+
`);
|
|
371
|
+
} else if (record.type === "message.delta" && record.role === "assistant") {
|
|
372
|
+
summary.text += String(record.text ?? "");
|
|
373
|
+
} else if (record.type === "tool.finished") {
|
|
374
|
+
summary.tools.push({ toolCallId: record.toolCallId, status: record.status });
|
|
375
|
+
} else if (record.type === "permission") {
|
|
376
|
+
summary.permissions.push(record);
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
return {
|
|
380
|
+
write,
|
|
381
|
+
event: async (event) => {
|
|
382
|
+
if (event.type === "permission") {
|
|
383
|
+
write(event);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const record = notificationRecord(event.method, event.params);
|
|
387
|
+
if (record) write(record);
|
|
388
|
+
if (format === "text" && record?.type === "message.delta" && record.role === "assistant") {
|
|
389
|
+
stdout.write(String(record.text ?? ""));
|
|
390
|
+
wroteText = true;
|
|
391
|
+
}
|
|
392
|
+
},
|
|
393
|
+
finishText: () => {
|
|
394
|
+
if (format === "text" && wroteText) {
|
|
395
|
+
stdout.write("\n");
|
|
396
|
+
wroteText = false;
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
error: (message) => {
|
|
400
|
+
if (format === "text") stderr.write(`${message}
|
|
401
|
+
`);
|
|
402
|
+
else write({ type: "error", message });
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
function notificationRecord(method, params) {
|
|
407
|
+
if (method === NOVA_NOTIFICATIONS.backgroundUpdate) {
|
|
408
|
+
return { type: "background.update", ...toRecord(params) ?? {} };
|
|
409
|
+
}
|
|
410
|
+
if (method === NOVA_NOTIFICATIONS.queueChanged) {
|
|
411
|
+
return { type: "queue.changed", ...toRecord(params) ?? {} };
|
|
412
|
+
}
|
|
413
|
+
if (method !== "session/update") {
|
|
414
|
+
return { type: "acp.notification", method, params };
|
|
415
|
+
}
|
|
416
|
+
const root = toRecord(params);
|
|
417
|
+
const update = toRecord(root?.update);
|
|
418
|
+
const sessionId = root?.sessionId;
|
|
419
|
+
switch (update?.sessionUpdate) {
|
|
420
|
+
case "agent_message_chunk":
|
|
421
|
+
return {
|
|
422
|
+
type: "message.delta",
|
|
423
|
+
sessionId,
|
|
424
|
+
role: "assistant",
|
|
425
|
+
text: textContent(update.content)
|
|
426
|
+
};
|
|
427
|
+
case "user_message_chunk":
|
|
428
|
+
return {
|
|
429
|
+
type: "message.delta",
|
|
430
|
+
sessionId,
|
|
431
|
+
role: "user",
|
|
432
|
+
text: textContent(update.content)
|
|
433
|
+
};
|
|
434
|
+
case "agent_thought_chunk":
|
|
435
|
+
return {
|
|
436
|
+
type: "thought",
|
|
437
|
+
sessionId,
|
|
438
|
+
text: textContent(update.content)
|
|
439
|
+
};
|
|
440
|
+
case "tool_call": {
|
|
441
|
+
const toolName = toRecord(update._meta)?.["nova-ai-cli/tool"];
|
|
442
|
+
return {
|
|
443
|
+
type: "tool.started",
|
|
444
|
+
sessionId,
|
|
445
|
+
toolCallId: update.toolCallId,
|
|
446
|
+
name: typeof toolName === "string" ? toolName : update.title,
|
|
447
|
+
title: update.title,
|
|
448
|
+
kind: update.kind,
|
|
449
|
+
input: update.rawInput
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
case "tool_call_update":
|
|
453
|
+
return {
|
|
454
|
+
type: update.status === "completed" || update.status === "failed" ? "tool.finished" : "tool.updated",
|
|
455
|
+
sessionId,
|
|
456
|
+
toolCallId: update.toolCallId,
|
|
457
|
+
status: update.status,
|
|
458
|
+
output: toRecord(update.rawOutput)?.output,
|
|
459
|
+
content: update.content
|
|
460
|
+
};
|
|
461
|
+
case "current_mode_update":
|
|
462
|
+
return {
|
|
463
|
+
type: "mode.changed",
|
|
464
|
+
sessionId,
|
|
465
|
+
mode: update.currentModeId
|
|
466
|
+
};
|
|
467
|
+
default:
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
function textContent(value) {
|
|
472
|
+
const content = toRecord(value);
|
|
473
|
+
return content?.type === "text" && typeof content.text === "string" ? content.text : "";
|
|
474
|
+
}
|
|
475
|
+
function toRecord(value) {
|
|
476
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
477
|
+
}
|
|
478
|
+
function parseOutputFormat(value) {
|
|
479
|
+
if (value === "text" || value === "json" || value === "stream-json") return value;
|
|
480
|
+
throw new Error(`Unknown output format: ${value}. Expected text, json, or stream-json.`);
|
|
481
|
+
}
|
|
482
|
+
function parsePermissionMode(value) {
|
|
483
|
+
if (value === "read-only" || value === "accept-edits" || value === "bypass-all") {
|
|
484
|
+
return value;
|
|
485
|
+
}
|
|
486
|
+
throw new Error(
|
|
487
|
+
`Unknown permission mode: ${value}. Expected read-only, accept-edits, or bypass-all.`
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
function takeValue(args, index, option) {
|
|
491
|
+
const value = args[index];
|
|
492
|
+
if (!value || value.startsWith("--")) {
|
|
493
|
+
throw new Error(`${option} requires a value.`);
|
|
494
|
+
}
|
|
495
|
+
return value;
|
|
496
|
+
}
|
|
497
|
+
async function readStdin() {
|
|
498
|
+
const chunks = [];
|
|
499
|
+
for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
|
|
500
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
501
|
+
}
|
|
502
|
+
function writeFailure(json, stdout, stderr, message) {
|
|
503
|
+
if (json)
|
|
504
|
+
stdout.write(
|
|
505
|
+
`${JSON.stringify({ schemaVersion: 1, type: "error", message })}
|
|
506
|
+
`
|
|
507
|
+
);
|
|
508
|
+
else stderr.write(`${message}
|
|
509
|
+
`);
|
|
510
|
+
return 1;
|
|
511
|
+
}
|
|
512
|
+
function errorMessage(error) {
|
|
513
|
+
return error instanceof Error ? error.message : String(error);
|
|
514
|
+
}
|
|
515
|
+
var HEADLESS_HELP = `Usage: nova-ai -p [options] [prompt]
|
|
516
|
+
|
|
517
|
+
Run one Nova agent request without the interactive terminal UI. The prompt
|
|
518
|
+
can also be piped on stdin.
|
|
519
|
+
|
|
520
|
+
Options:
|
|
521
|
+
-p, --print [prompt] Run headless (the prompt may follow)
|
|
522
|
+
--output-format <format> text (default), json (one object at the end)
|
|
523
|
+
or stream-json (JSON Lines while it runs)
|
|
524
|
+
--json Same as --output-format stream-json
|
|
525
|
+
--cwd <path> Workspace directory (default: current directory)
|
|
526
|
+
--model <id> Override the configured model
|
|
527
|
+
--resume <session-id> Continue a persisted session
|
|
528
|
+
-c, --continue Continue the most recent session of the workspace
|
|
529
|
+
--no-mcp Ignore workspace MCP configuration
|
|
530
|
+
--trust-workspace Trust this workspace (remembered): start the MCP
|
|
531
|
+
servers and apply the allow rules it declares
|
|
532
|
+
--permission-mode <mode> read-only (default), accept-edits, or bypass-all
|
|
533
|
+
-h, --help Show this help
|
|
534
|
+
|
|
535
|
+
Exit codes: 0 success, 1 configuration/runtime error, 2 cancelled,
|
|
536
|
+
3 tool-turn safety limit, 130 interrupted.`;
|
|
537
|
+
var headless_default = runHeadless;
|
|
538
|
+
export {
|
|
539
|
+
HEADLESS_HELP,
|
|
540
|
+
headless_default as default,
|
|
541
|
+
parseHeadlessArgs,
|
|
542
|
+
runHeadless
|
|
543
|
+
};
|