@xalia/agent 0.5.0

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 (66) hide show
  1. package/.prettierrc.json +11 -0
  2. package/README.md +56 -0
  3. package/dist/agent.js +238 -0
  4. package/dist/agentUtils.js +106 -0
  5. package/dist/chat.js +296 -0
  6. package/dist/dummyLLM.js +38 -0
  7. package/dist/files.js +115 -0
  8. package/dist/iplatform.js +2 -0
  9. package/dist/llm.js +2 -0
  10. package/dist/main.js +147 -0
  11. package/dist/mcpServerManager.js +278 -0
  12. package/dist/nodePlatform.js +61 -0
  13. package/dist/openAILLM.js +38 -0
  14. package/dist/openAILLMStreaming.js +431 -0
  15. package/dist/options.js +79 -0
  16. package/dist/prompt.js +83 -0
  17. package/dist/sudoMcpServerManager.js +183 -0
  18. package/dist/test/imageLoad.test.js +14 -0
  19. package/dist/test/mcpServerManager.test.js +71 -0
  20. package/dist/test/prompt.test.js +26 -0
  21. package/dist/test/sudoMcpServerManager.test.js +49 -0
  22. package/dist/tokenAuth.js +39 -0
  23. package/dist/tools.js +44 -0
  24. package/eslint.config.mjs +25 -0
  25. package/frog.png +0 -0
  26. package/package.json +42 -0
  27. package/scripts/git_message +31 -0
  28. package/scripts/git_wip +21 -0
  29. package/scripts/pr_message +18 -0
  30. package/scripts/pr_review +16 -0
  31. package/scripts/sudomcp_import +23 -0
  32. package/scripts/test_script +60 -0
  33. package/src/agent.ts +283 -0
  34. package/src/agentUtils.ts +198 -0
  35. package/src/chat.ts +346 -0
  36. package/src/dummyLLM.ts +50 -0
  37. package/src/files.ts +95 -0
  38. package/src/iplatform.ts +17 -0
  39. package/src/llm.ts +15 -0
  40. package/src/main.ts +187 -0
  41. package/src/mcpServerManager.ts +371 -0
  42. package/src/nodePlatform.ts +24 -0
  43. package/src/openAILLM.ts +51 -0
  44. package/src/openAILLMStreaming.ts +528 -0
  45. package/src/options.ts +103 -0
  46. package/src/prompt.ts +93 -0
  47. package/src/sudoMcpServerManager.ts +278 -0
  48. package/src/test/imageLoad.test.ts +14 -0
  49. package/src/test/mcpServerManager.test.ts +98 -0
  50. package/src/test/prompt.test.src +0 -0
  51. package/src/test/prompt.test.ts +26 -0
  52. package/src/test/sudoMcpServerManager.test.ts +65 -0
  53. package/src/tokenAuth.ts +50 -0
  54. package/src/tools.ts +57 -0
  55. package/test_data/background_test_profile.json +6 -0
  56. package/test_data/background_test_script.json +11 -0
  57. package/test_data/dummyllm_script_simplecalc.json +28 -0
  58. package/test_data/git_message_profile.json +4 -0
  59. package/test_data/git_wip_system.txt +5 -0
  60. package/test_data/pr_message_profile.json +4 -0
  61. package/test_data/pr_review_profile.json +4 -0
  62. package/test_data/prompt_simplecalc.txt +1 -0
  63. package/test_data/simplecalc_profile.json +4 -0
  64. package/test_data/sudomcp_import_profile.json +4 -0
  65. package/test_data/test_script_profile.json +8 -0
  66. package/tsconfig.json +13 -0
package/src/chat.ts ADDED
@@ -0,0 +1,346 @@
1
+ import yocto from "yocto-spinner";
2
+ import { Spinner } from "yocto-spinner";
3
+ import * as fs from "fs";
4
+ import { Agent, AgentProfile } from "./agent";
5
+ import { displayToolCall } from "./tools";
6
+ import { McpServerManager } from "./mcpServerManager";
7
+ import OpenAI from "openai";
8
+ import { loadImageB64OrUndefined } from "./files";
9
+ import { Prompt, parsePrompt } from "./prompt";
10
+ import { SudoMcpServerManager } from "./sudoMcpServerManager";
11
+ import chalk from "chalk";
12
+ import { configuration, utils } from "@xalia/xmcp/tool";
13
+ import { getLogger, InvalidConfiguration } from "@xalia/xmcp/sdk";
14
+ import { createAgentAndSudoMcpServerManager } from "./agentUtils";
15
+ import { NODE_PLATFORM } from "./nodePlatform";
16
+
17
+ const logger = getLogger();
18
+
19
+ async function write(msg: string): Promise<void> {
20
+ return new Promise((resolve, err) => {
21
+ process.stdout.write(msg, (e) => {
22
+ if (e) {
23
+ err(e);
24
+ } else {
25
+ resolve();
26
+ }
27
+ });
28
+ });
29
+ }
30
+
31
+ export async function runChat(
32
+ llmUrl: string,
33
+ agentProfile: AgentProfile,
34
+ conversation: OpenAI.ChatCompletionMessageParam[] | undefined,
35
+ prompt: string | undefined,
36
+ image: string | undefined,
37
+ llmApiKey: string | undefined,
38
+ sudomcpConfig: configuration.Configuration,
39
+ approveToolsUpTo: number,
40
+ stream: boolean
41
+ ) {
42
+ // In chat mode, just print the messages. Ask for tool confirmation
43
+ // unless approveTools is set.
44
+
45
+ const spinner: Spinner = yocto();
46
+ let first = true;
47
+ const onMessage = async (msg: string, msgEnd: boolean) => {
48
+ if (first) {
49
+ first = false;
50
+ await write("AGENT: ");
51
+ if (spinner) {
52
+ spinner.stop().clear();
53
+ }
54
+ }
55
+
56
+ if (msg) {
57
+ await write(msg);
58
+ }
59
+
60
+ if (msgEnd) {
61
+ await write("\n");
62
+ first = true;
63
+ }
64
+ };
65
+
66
+ let remainingApprovedToolCalls = approveToolsUpTo;
67
+ const onToolCall = async (toolCall: OpenAI.ChatCompletionMessageToolCall) => {
68
+ if (remainingApprovedToolCalls !== 0) {
69
+ --remainingApprovedToolCalls;
70
+ return true;
71
+ }
72
+
73
+ spinner.stop().clear();
74
+ const result = await promptToolCall(repl, toolCall);
75
+ spinner.start();
76
+ return result;
77
+ };
78
+
79
+ // Create agent
80
+
81
+ const [agent, sudoMcpServerManager] =
82
+ await createAgentAndSudoMcpServerManager(
83
+ llmUrl,
84
+ agentProfile,
85
+ onMessage,
86
+ onToolCall,
87
+ NODE_PLATFORM,
88
+ llmApiKey,
89
+ sudomcpConfig,
90
+ utils.FRONTEND_PROD_AUTHORIZED_URL,
91
+ undefined,
92
+ stream
93
+ );
94
+ if (conversation) {
95
+ agent.setConversation(conversation);
96
+ }
97
+
98
+ // Opening banner
99
+ console.log(`${chalk.green("SudoMCP Agent CLI")}`);
100
+ console.log(
101
+ `(Type ${chalk.yellow("/h")} for help, ${chalk.yellow("/q")} to quit.)`
102
+ );
103
+
104
+ // Display first prompt if supplied
105
+ if (prompt) {
106
+ console.log(`USER: ${prompt}`);
107
+ }
108
+
109
+ const repl = new Prompt();
110
+
111
+ // Conversation loop
112
+ while (true) {
113
+ if (!prompt) {
114
+ const [msg, img] = await getNextPrompt(repl, agent, sudoMcpServerManager);
115
+ if (!msg && !img) {
116
+ break;
117
+ }
118
+ prompt = msg;
119
+ image = img;
120
+ }
121
+
122
+ image = loadImageB64OrUndefined(image);
123
+
124
+ // Pass the prompt and image to the Agent
125
+ try {
126
+ spinner.start();
127
+ await agent.userMessage(prompt, image);
128
+ } catch (e) {
129
+ console.log(`ERROR: ${e}`);
130
+ console.log(` STACK: ${(e as unknown as Error).stack}`);
131
+ }
132
+ prompt = undefined;
133
+ image = undefined;
134
+ }
135
+
136
+ // Shutdown the agent
137
+
138
+ repl.shutdown();
139
+ await agent.shutdown();
140
+
141
+ logger.debug("shutdown done");
142
+ }
143
+
144
+ /**
145
+ * Read lines from the prompt, parsing any commands, and return once there is
146
+ * a prompt and/or image for the llm. Both undefined means exit.
147
+ */
148
+ async function getNextPrompt(
149
+ repl: Prompt,
150
+ agent: Agent,
151
+ sudoMcpServerManager: SudoMcpServerManager
152
+ ): Promise<[string | undefined, string | undefined]> {
153
+ while (true) {
154
+ // Get a line, detecting the EOF signal.
155
+ const line = await repl.run();
156
+ if (typeof line === "undefined") {
157
+ console.log("closing ...");
158
+ return [undefined, undefined];
159
+ }
160
+ if (line.length === 0) {
161
+ continue;
162
+ }
163
+
164
+ // Extract prompt or commands
165
+ const { msg, cmds } = parsePrompt(line);
166
+
167
+ // If there are no commands, this must be a prompt only
168
+ if (!cmds) {
169
+ return [msg, undefined];
170
+ }
171
+
172
+ // There are commands. If it's image, return [prompt, image]. If it's
173
+ // quit, return [undefined, undefined], otherwise it must be a command.
174
+ // Execute it and prompt again.
175
+ switch (cmds[0]) {
176
+ case "i": // image
177
+ return [msg, cmds[1]];
178
+ case "q":
179
+ case "quit":
180
+ case "exit":
181
+ return [undefined, undefined];
182
+ default:
183
+ break;
184
+ }
185
+
186
+ try {
187
+ await runCommand(
188
+ agent,
189
+ agent.getMcpServerManager(),
190
+ sudoMcpServerManager,
191
+ cmds
192
+ );
193
+ } catch (e) {
194
+ console.log(`ERROR: ${e}`);
195
+ }
196
+ }
197
+ }
198
+
199
+ async function runCommand(
200
+ agent: Agent,
201
+ mcpServerManager: McpServerManager,
202
+ sudoMcpServerManager: SudoMcpServerManager,
203
+ cmds: string[]
204
+ ) {
205
+ switch (cmds[0]) {
206
+ case "lt":
207
+ listTools(mcpServerManager);
208
+ break;
209
+ case "ls":
210
+ listServers(sudoMcpServerManager);
211
+ break;
212
+ case "as":
213
+ await addServer(sudoMcpServerManager, cmds[1]);
214
+ break;
215
+ case "rs":
216
+ await mcpServerManager.removeMcpServer(cmds[1]);
217
+ break;
218
+ case "e":
219
+ mcpServerManager.enableTool(cmds[1], cmds[2]);
220
+ console.log(`Enabled tool ${cmds[2]} for server ${cmds[1]}`);
221
+ break;
222
+ case "d":
223
+ mcpServerManager.disableTool(cmds[1], cmds[2]);
224
+ console.log(`Disabled tool ${cmds[2]} for server ${cmds[1]}`);
225
+ break;
226
+ case "ea":
227
+ mcpServerManager.enableAllTools(cmds[1]);
228
+ console.log(`Enabled all tools for server ${cmds[1]}`);
229
+ break;
230
+ case "da":
231
+ mcpServerManager.disableAllTools(cmds[1]);
232
+ console.log(`Disabled all tools for server ${cmds[1]}`);
233
+ break;
234
+ case "wc":
235
+ writeConversation(agent, cmds[1]);
236
+ break;
237
+ case "wa":
238
+ writeAgentProfile(agent, cmds[1]);
239
+ break;
240
+ case "h":
241
+ case "help":
242
+ case "?":
243
+ helpMenu();
244
+ break;
245
+ default:
246
+ console.log(`error: Unknown command ${cmds[0]}`);
247
+ }
248
+ }
249
+
250
+ function helpMenu() {
251
+ console.log(`Tool management commands:`);
252
+ console.log(` ${chalk.yellow("/lt")} List tools: `);
253
+ console.log(` ${chalk.yellow("/ls")} List servers`);
254
+ console.log(` ${chalk.yellow("/as <server-name>")} Add server`);
255
+ console.log(
256
+ ` ${chalk.yellow("/rs <server-name>")} Remove server`
257
+ );
258
+ console.log(` ${chalk.yellow("/e <server-name> <tool-name>")} Enable tool`);
259
+ console.log(` ${chalk.yellow("/d <server-name> <tool-name>")} Disable tool`);
260
+ console.log(
261
+ ` ${chalk.yellow("/ea <server-name>")} Enable all tools`
262
+ );
263
+ console.log(
264
+ ` ${chalk.yellow("/da <server-name>")} Disable all tools`
265
+ );
266
+ console.log(
267
+ ` ${chalk.yellow("/wc <file-name>")} Write conversation file`
268
+ );
269
+ console.log(
270
+ ` ${chalk.yellow("/wa <file-name>")} Write agent profile file`
271
+ );
272
+
273
+ console.log(` ${chalk.yellow("/q")} Quit`);
274
+ }
275
+
276
+ function listTools(mcpServerManager: McpServerManager) {
277
+ console.log("Mcp servers and tools (* - enabled):");
278
+
279
+ const serverNames = mcpServerManager.getMcpServerNames();
280
+ for (const serverName of serverNames) {
281
+ const server = mcpServerManager.getMcpServer(serverName);
282
+ console.log(` ${chalk.green(serverName)}`);
283
+ const tools = server.getTools();
284
+ const enabled = server.getEnabledTools();
285
+ for (const tool of tools) {
286
+ const isEnabled = enabled[tool.name] ? "*" : " ";
287
+ console.log(` [${isEnabled}] ${tool.name}`);
288
+ }
289
+ }
290
+ }
291
+
292
+ function listServers(sudoMcpServerManager: SudoMcpServerManager) {
293
+ console.log(`Available MCP Servers:`);
294
+
295
+ const serverBriefs = sudoMcpServerManager.getServerBriefs();
296
+ serverBriefs.sort((a, b) => a.name.localeCompare(b.name));
297
+ for (const brief of serverBriefs) {
298
+ console.log(`- ${chalk.green(brief.name)}: ${brief.description}`);
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Adds server and enables all tools.
304
+ */
305
+ async function addServer(
306
+ sudoMcpServerManager: SudoMcpServerManager,
307
+ serverName: string
308
+ ) {
309
+ try {
310
+ await sudoMcpServerManager.addMcpServer(serverName);
311
+ } catch (e) {
312
+ if (e instanceof InvalidConfiguration) {
313
+ throw `${e}. \n
314
+ Have you installed server ${serverName} using the SudoMCP CLI tool?`;
315
+ } else {
316
+ throw e;
317
+ }
318
+ }
319
+ sudoMcpServerManager.getMcpServerManager().enableAllTools(serverName);
320
+ console.log(`Added server: ${serverName} and enabled all tools.`);
321
+ console.log(`Current tool list:`);
322
+ listTools(sudoMcpServerManager.getMcpServerManager());
323
+ }
324
+
325
+ function writeConversation(agent: Agent, fileName: string) {
326
+ const conversation = agent.getConversation();
327
+ fs.writeFileSync(fileName, JSON.stringify(conversation), {
328
+ encoding: "utf8",
329
+ });
330
+ console.log(`Conversation written to ${fileName}`);
331
+ }
332
+
333
+ function writeAgentProfile(agent: Agent, fileName: string) {
334
+ const profile = agent.getAgentProfile();
335
+ fs.writeFileSync(fileName, JSON.stringify(profile));
336
+ console.log(`AgentProfile written to ${fileName}`);
337
+ }
338
+
339
+ async function promptToolCall(
340
+ repl: Prompt,
341
+ toolCall: OpenAI.ChatCompletionMessageToolCall
342
+ ): Promise<boolean> {
343
+ displayToolCall(toolCall);
344
+ const response = await repl.run("Approve tool call? (Y/n) ");
345
+ return response === "y" || response === "yes" || response == "";
346
+ }
@@ -0,0 +1,50 @@
1
+ import { ILLM } from "./llm";
2
+ import { OpenAI } from "openai";
3
+ import { strict as assert } from "assert";
4
+
5
+ export class DummyLLM implements ILLM {
6
+ private readonly responses: OpenAI.Chat.Completions.ChatCompletion.Choice[];
7
+ private idx: number;
8
+
9
+ constructor(responses: OpenAI.Chat.Completions.ChatCompletion.Choice[]) {
10
+ this.responses = responses;
11
+ this.idx = 0;
12
+ }
13
+
14
+ public getModel(): string {
15
+ return "dummy";
16
+ }
17
+
18
+ public getUrl(): string {
19
+ throw "cannot get url for DummyLLM";
20
+ }
21
+
22
+ public async getConversationResponse(
23
+ _messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[],
24
+ _tools?: OpenAI.Chat.Completions.ChatCompletionTool[],
25
+ onMessage?: (msg: string, msgEnd: boolean) => Promise<void>
26
+ ): Promise<OpenAI.Chat.Completions.ChatCompletion> {
27
+ await new Promise((r) => setTimeout(r, 0));
28
+ assert(this.idx < this.responses.length);
29
+
30
+ const response = this.responses[this.idx++];
31
+ if (onMessage) {
32
+ const message = response.message;
33
+ if (message.content) {
34
+ onMessage(message.content, true);
35
+ }
36
+ }
37
+
38
+ return {
39
+ id: "" + this.idx,
40
+ choices: [response],
41
+ created: Date.now(),
42
+ model: "dummyLlmModel",
43
+ object: "chat.completion",
44
+ };
45
+ }
46
+
47
+ public setModel(_model: string): void {
48
+ assert(false, "unexpected call to setModel");
49
+ }
50
+ }
package/src/files.ts ADDED
@@ -0,0 +1,95 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { McpServerUrls } from "./agent";
4
+
5
+ export function loadFileOrUndefined(
6
+ file: string | undefined
7
+ ): string | undefined {
8
+ if (file) {
9
+ return fs.readFileSync(file, { encoding: "utf8" });
10
+ }
11
+ return undefined;
12
+ }
13
+
14
+ /**
15
+ * If filename is `-` read everything from stdin and return it. Otherwise,
16
+ * load the file and return the content.
17
+ */
18
+ export async function loadFileOrStdin(file: string): Promise<string> {
19
+ if (file === "-") {
20
+ return new Promise((resolve, reject) => {
21
+ let data = "";
22
+
23
+ process.stdin.setEncoding("utf8");
24
+
25
+ process.stdin.on("data", (chunk) => {
26
+ data += chunk;
27
+ });
28
+
29
+ process.stdin.on("end", () => {
30
+ resolve(data);
31
+ });
32
+
33
+ process.stdin.on("error", (err) => {
34
+ reject(err);
35
+ });
36
+ });
37
+ }
38
+
39
+ return fs.readFileSync(file, { encoding: "utf8" });
40
+ }
41
+
42
+ export function loadImageB64OrUndefined(
43
+ file: string | undefined
44
+ ): string | undefined {
45
+ if (file) {
46
+ const ext = path.extname(file).slice(1).toLowerCase();
47
+ const mimeType = {
48
+ jpg: "image/jpeg",
49
+ jpeg: "image/jpeg",
50
+ png: "image/png",
51
+ gif: "image/gif",
52
+ svg: "image/svg+xml",
53
+ webp: "image/webp",
54
+ }[ext];
55
+ if (!mimeType) {
56
+ throw Error(`invalid file extension: ${ext}`);
57
+ }
58
+
59
+ const fileBuffer = fs.readFileSync(file);
60
+ const imgB64 = fileBuffer.toString("base64");
61
+ return `data:${mimeType};base64,${imgB64}`;
62
+ }
63
+ return undefined;
64
+ }
65
+
66
+ export function loadServerUrls(path: string): McpServerUrls {
67
+ try {
68
+ const file = fs.readFileSync(path, "utf-8");
69
+ const urls = JSON.parse(file);
70
+
71
+ // Validate the structure
72
+ if (typeof urls !== "object" || urls === null) {
73
+ throw new Error("Invalid server URLs format: must be an object");
74
+ }
75
+
76
+ // Validate each URL is a string
77
+ for (const [key, value] of Object.entries(urls)) {
78
+ if (typeof value !== "string") {
79
+ throw new Error(
80
+ `Invalid URL format for server ${key}: must be a string`
81
+ );
82
+ }
83
+ }
84
+
85
+ return urls as McpServerUrls;
86
+ } catch (error) {
87
+ if (error instanceof SyntaxError) {
88
+ throw new Error(`Invalid JSON in server URLs file: ${error.message}`);
89
+ }
90
+ if (error instanceof Error) {
91
+ throw new Error(`Failed to load server URLs: ${error.message}`);
92
+ }
93
+ throw error;
94
+ }
95
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Per-platform services that the agent and related classes can use.
3
+ */
4
+ export interface IPlatform {
5
+ /**
6
+ * Open the browser (or a new tab) at some URL.
7
+ * Optional displayName to allow frontend messages
8
+ * to display server name.
9
+ */
10
+ openUrl(
11
+ url: string,
12
+ authResultP: Promise<boolean>,
13
+ displayName: string
14
+ ): void;
15
+
16
+ load(filename: string): Promise<string>;
17
+ }
package/src/llm.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { OpenAI } from "openai";
2
+
3
+ export interface ILLM {
4
+ getModel(): string;
5
+
6
+ getUrl(): string;
7
+
8
+ getConversationResponse(
9
+ messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[],
10
+ tools?: OpenAI.Chat.Completions.ChatCompletionTool[],
11
+ onMessage?: (msg: string, end: boolean) => Promise<void>
12
+ ): Promise<OpenAI.Chat.Completions.ChatCompletion>;
13
+
14
+ setModel(model: string): void;
15
+ }
package/src/main.ts ADDED
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+ // -*- typescript -*-
3
+
4
+ import * as fs from "fs";
5
+ import * as dotenv from "dotenv";
6
+ import { run, command, option, flag, optional, string } from "cmd-ts";
7
+ import { AgentProfile } from "./agent";
8
+ import {
9
+ llmUrl,
10
+ llmApiKey,
11
+ promptFile,
12
+ oneShot,
13
+ approveTools,
14
+ approveToolsUpTo,
15
+ imageFile,
16
+ } from "./options";
17
+ import OpenAI from "openai";
18
+ import {
19
+ loadFileOrUndefined,
20
+ loadFileOrStdin,
21
+ loadImageB64OrUndefined,
22
+ } from "./files";
23
+ import { configuration, utils } from "@xalia/xmcp/tool";
24
+ import { getLogger } from "@xalia/xmcp/sdk";
25
+ import { runOneShot } from "./agentUtils";
26
+ import { runChat } from "./chat";
27
+ import { NODE_PLATFORM } from "./nodePlatform";
28
+ import { strict as assert } from "assert";
29
+
30
+ dotenv.config();
31
+
32
+ const logger = getLogger();
33
+
34
+ const main = command({
35
+ name: "main",
36
+ args: {
37
+ promptFile,
38
+ imageFile,
39
+ llmUrl,
40
+ llmApiKey,
41
+ oneShot,
42
+ approveTools,
43
+ approveToolsUpTo,
44
+ sudomcpConfigFile: option({
45
+ type: optional(string),
46
+ long: "sudomcp-config-file",
47
+ description:
48
+ "SudoMCP config file (content or file, default: ~/config/..)",
49
+ env: "SUDOMCP_CONFIG",
50
+ }),
51
+ conversationFile: option({
52
+ type: optional(string),
53
+ long: "conversation",
54
+ description: "Restore conversation (content or file) (see /wc)",
55
+ env: "CONVERSATION",
56
+ }),
57
+ conversationOutputFile: option({
58
+ type: optional(string),
59
+ long: "conversation-output",
60
+ description: "Save final conversation to file (--one-shot mode)",
61
+ env: "CONVERSATION_OUTPUT",
62
+ }),
63
+ agentProfileFile: option({
64
+ type: optional(string),
65
+ long: "agent-profile",
66
+ description: "Agent profile (content or filename)",
67
+ env: "AGENT_PROFILE",
68
+ }),
69
+ noStreaming: flag({
70
+ long: "no-stream",
71
+ description: "Disable streaming (chat mode only)",
72
+ }),
73
+ },
74
+ handler: async ({
75
+ promptFile,
76
+ imageFile,
77
+ llmUrl,
78
+ llmApiKey,
79
+ oneShot,
80
+ approveTools,
81
+ approveToolsUpTo,
82
+ sudomcpConfigFile,
83
+ conversationFile,
84
+ conversationOutputFile,
85
+ agentProfileFile,
86
+ noStreaming,
87
+ }): Promise<void> => {
88
+ approveToolsUpTo = (() => {
89
+ if (typeof approveToolsUpTo === "undefined") {
90
+ // For the non-interactive case, `--approve-tools` is ignored.
91
+ if (oneShot) {
92
+ return -1;
93
+ }
94
+ return approveTools ? -1 : 0;
95
+ }
96
+ return approveToolsUpTo;
97
+ })();
98
+ assert(typeof approveToolsUpTo === "number");
99
+
100
+ // Load the AgentProfile or use a default
101
+
102
+ const agentProfile: AgentProfile = (() => {
103
+ let agentProfile = utils.loadContentOrFileOrUndefined(
104
+ agentProfileFile,
105
+ AgentProfile
106
+ );
107
+ if (!agentProfile) {
108
+ agentProfile = new AgentProfile(
109
+ undefined,
110
+ "You are a helpful agent",
111
+ {}
112
+ );
113
+ }
114
+
115
+ return agentProfile;
116
+ })();
117
+ logger.debug(`agent config: ${JSON.stringify(agentProfile)}`);
118
+
119
+ // Read sudomcp config file. This will be used for SudoMcpServerManager,
120
+ // and also to fall back to the sudomcp api key if an explicit llmApiKey
121
+ // was not given.
122
+
123
+ const sudomcpConfig = configuration.loadEnsureConfig(sudomcpConfigFile);
124
+ if (!llmApiKey) {
125
+ llmApiKey = sudomcpConfig.api_key;
126
+ logger.debug(`using xmcp api key: ${llmApiKey}`);
127
+ }
128
+
129
+ // Restore conversation from value or file.
130
+
131
+ const startingConversation:
132
+ | OpenAI.ChatCompletionMessageParam[]
133
+ | undefined = utils.loadContentOrFileOrUndefined(conversationFile);
134
+ logger.debug(
135
+ `startingConversation: ${JSON.stringify(startingConversation)}`
136
+ );
137
+
138
+ // Run in one-shot mode or chat-mode
139
+
140
+ if (oneShot) {
141
+ if (!promptFile) {
142
+ throw "one-shot mode requires a prompt";
143
+ }
144
+ const prompt = await loadFileOrStdin(promptFile);
145
+
146
+ const { response, conversation } = await runOneShot(
147
+ llmUrl,
148
+ agentProfile,
149
+ startingConversation,
150
+ NODE_PLATFORM,
151
+ prompt,
152
+ loadImageB64OrUndefined(imageFile),
153
+ llmApiKey,
154
+ sudomcpConfig,
155
+ approveToolsUpTo
156
+ );
157
+ console.log(response);
158
+
159
+ if (conversationOutputFile) {
160
+ logger.debug(`writing conversation to ${conversationOutputFile}:`);
161
+ logger.debug(` conversation: ${JSON.stringify(conversation)}`);
162
+
163
+ fs.writeFileSync(conversationOutputFile, JSON.stringify(conversation));
164
+ }
165
+ } else {
166
+ const prompt = loadFileOrUndefined(promptFile);
167
+ return runChat(
168
+ llmUrl,
169
+ agentProfile,
170
+ startingConversation,
171
+ prompt,
172
+ imageFile,
173
+ llmApiKey,
174
+ sudomcpConfig,
175
+ approveToolsUpTo,
176
+ !noStreaming
177
+ );
178
+ }
179
+ },
180
+ });
181
+
182
+ function handleError(msg: string) {
183
+ console.error(msg);
184
+ process.exit(1);
185
+ }
186
+
187
+ run(main, process.argv.slice(2)).catch(handleError);