@xalia/agent 1.0.19

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 (64) hide show
  1. package/.prettierrc.json +11 -0
  2. package/README.md +57 -0
  3. package/dist/agent.js +278 -0
  4. package/dist/agentUtils.js +88 -0
  5. package/dist/chat.js +278 -0
  6. package/dist/dummyLLM.js +28 -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 +136 -0
  11. package/dist/mcpServerManager.js +269 -0
  12. package/dist/nodePlatform.js +61 -0
  13. package/dist/openAILLM.js +31 -0
  14. package/dist/options.js +79 -0
  15. package/dist/prompt.js +83 -0
  16. package/dist/sudoMcpServerManager.js +174 -0
  17. package/dist/test/imageLoad.test.js +14 -0
  18. package/dist/test/mcpServerManager.test.js +71 -0
  19. package/dist/test/prompt.test.js +26 -0
  20. package/dist/test/sudoMcpServerManager.test.js +49 -0
  21. package/dist/tokenAuth.js +39 -0
  22. package/dist/tools.js +44 -0
  23. package/eslint.config.mjs +25 -0
  24. package/frog.png +0 -0
  25. package/package.json +41 -0
  26. package/scripts/git_message +31 -0
  27. package/scripts/git_wip +21 -0
  28. package/scripts/pr_message +18 -0
  29. package/scripts/pr_review +16 -0
  30. package/scripts/sudomcp_import +23 -0
  31. package/scripts/test_script +60 -0
  32. package/src/agent.ts +357 -0
  33. package/src/agentUtils.ts +188 -0
  34. package/src/chat.ts +325 -0
  35. package/src/dummyLLM.ts +36 -0
  36. package/src/files.ts +95 -0
  37. package/src/iplatform.ts +11 -0
  38. package/src/llm.ts +12 -0
  39. package/src/main.ts +171 -0
  40. package/src/mcpServerManager.ts +365 -0
  41. package/src/nodePlatform.ts +24 -0
  42. package/src/openAILLM.ts +43 -0
  43. package/src/options.ts +103 -0
  44. package/src/prompt.ts +93 -0
  45. package/src/sudoMcpServerManager.ts +268 -0
  46. package/src/test/imageLoad.test.ts +14 -0
  47. package/src/test/mcpServerManager.test.ts +98 -0
  48. package/src/test/prompt.test.src +0 -0
  49. package/src/test/prompt.test.ts +26 -0
  50. package/src/test/sudoMcpServerManager.test.ts +63 -0
  51. package/src/tokenAuth.ts +50 -0
  52. package/src/tools.ts +57 -0
  53. package/test_data/background_test_profile.json +7 -0
  54. package/test_data/background_test_script.json +11 -0
  55. package/test_data/dummyllm_script_simplecalc.json +28 -0
  56. package/test_data/git_message_profile.json +4 -0
  57. package/test_data/git_wip_system.txt +5 -0
  58. package/test_data/pr_message_profile.json +4 -0
  59. package/test_data/pr_review_profile.json +4 -0
  60. package/test_data/prompt_simplecalc.txt +1 -0
  61. package/test_data/simplecalc_profile.json +4 -0
  62. package/test_data/sudomcp_import_profile.json +4 -0
  63. package/test_data/test_script_profile.json +9 -0
  64. package/tsconfig.json +13 -0
@@ -0,0 +1,365 @@
1
+ import { ChatCompletionTool } from "openai/resources.mjs";
2
+ import {
3
+ SSEClientTransport,
4
+ SSEClientTransportOptions,
5
+ } from "@modelcontextprotocol/sdk/client/sse.js";
6
+ import { Client as McpClient } from "@modelcontextprotocol/sdk/client/index.js";
7
+ import { TokenAuth } from "./tokenAuth";
8
+ import { Tool } from "@modelcontextprotocol/sdk/types.js";
9
+ import { strict as assert } from "assert";
10
+ import { getLogger } from "@xalia/xmcp/sdk";
11
+
12
+ const logger = getLogger();
13
+
14
+ /// Callback into an Mcp server
15
+ export type McpCallback = { (args: string): Promise<string> };
16
+
17
+ /// Map of tool name to callback
18
+ export type McpCallbacks = { [toolName: string]: McpCallback };
19
+
20
+ /// Map of tool name to enabled flag
21
+ export type EnabledTools = { [toolName: string]: boolean };
22
+
23
+ /// This represents the full configuration of McpServers and tools. It cannot
24
+ /// be used directly by this class (since this class does not currently handle
25
+ /// creating the transports), but should be usable by a catalogue
26
+ /// (e.g. SudoMcpServerManager) to reconstruct the state.
27
+ export type McpServerManagerSettings = {
28
+ [serverName: string]: EnabledTools;
29
+ };
30
+
31
+ /**
32
+ * The (read-only) McpServerInfo to expose to external classes. Callers
33
+ * should not modify this data directly. Only through the McpServerManager
34
+ * class.
35
+ */
36
+ export class McpServerInfo {
37
+ private readonly tools: Tool[]; // TODO: May not need both tools and toolsMap
38
+ private readonly toolsMap: { [toolName: string]: Tool };
39
+ protected enabledToolsMap: EnabledTools;
40
+
41
+ constructor(tools: Tool[]) {
42
+ const toolsMap: { [toolName: string]: Tool } = {};
43
+
44
+ for (const mcpTool of tools) {
45
+ const toolName = mcpTool.name;
46
+ toolsMap[toolName] = mcpTool;
47
+ }
48
+
49
+ this.tools = tools;
50
+ this.toolsMap = toolsMap;
51
+ this.enabledToolsMap = {};
52
+ }
53
+
54
+ public getEnabledTools(): EnabledTools {
55
+ return this.enabledToolsMap;
56
+ }
57
+
58
+ public getTools(): Tool[] {
59
+ return this.tools;
60
+ }
61
+
62
+ public getTool(toolName: string): Tool {
63
+ return this.toolsMap[toolName];
64
+ }
65
+ }
66
+
67
+ /**
68
+ * The internal class holds server info and allows it to be updated. Managed
69
+ * by McpServerManager. Do not access these methods except via the
70
+ * McpServerManager.
71
+ */
72
+ class McpServerInfoInternal extends McpServerInfo {
73
+ private readonly client: McpClient;
74
+ private readonly callbacks: McpCallbacks;
75
+
76
+ constructor(client: McpClient, tools: Tool[]) {
77
+ super(tools);
78
+
79
+ const callbacks: McpCallbacks = {};
80
+
81
+ for (const mcpTool of tools) {
82
+ const toolName = mcpTool.name;
83
+
84
+ // Create callback
85
+ const callback = async (argStr: string): Promise<string> => {
86
+ logger.debug(
87
+ `cb for ${toolName} invoked with args (${typeof argStr}): ` +
88
+ `${JSON.stringify(argStr)}`
89
+ );
90
+
91
+ const argsObj = JSON.parse(argStr);
92
+ const toolResult = await client.callTool({
93
+ name: toolName,
94
+ arguments: argsObj,
95
+ });
96
+ logger.debug(
97
+ `cb for ${toolName} returned: ${JSON.stringify(toolResult)}`
98
+ );
99
+
100
+ assert(typeof toolResult === "object");
101
+ const content = toolResult.content as { [a: number]: unknown };
102
+ assert(typeof content === "object");
103
+ assert(content);
104
+ const content0 = content[0] as { text: string };
105
+ assert(typeof content0 === "object");
106
+ const content0Text = content0.text;
107
+ assert(typeof content0Text === "string");
108
+ return content0Text;
109
+ };
110
+
111
+ callbacks[toolName] = callback;
112
+ }
113
+
114
+ this.client = client;
115
+ this.callbacks = callbacks;
116
+ }
117
+
118
+ public async shutdown(): Promise<void> {
119
+ await this.client.close();
120
+ }
121
+
122
+ public enableTool(toolName: string) {
123
+ this.enabledToolsMap[toolName] = true;
124
+ }
125
+
126
+ public disableTool(toolName: string) {
127
+ delete this.enabledToolsMap[toolName];
128
+ }
129
+
130
+ public getCallback(toolName: string) {
131
+ return this.callbacks[toolName];
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Manage a set of MCP servers, where the tools for each server have an
137
+ * 'enabled' flag. Tools are disabled by default. The set of enabled tools
138
+ * over all servers is exposed as a single list of OpenAI functions.
139
+ */
140
+ export class McpServerManager {
141
+ private mcpServers: { [serverName: string]: McpServerInfoInternal } = {};
142
+ private enabledToolsDirty: boolean = true;
143
+ private enabledOpenAITools: ChatCompletionTool[] = [];
144
+
145
+ public async shutdown() {
146
+ await Promise.all(
147
+ Object.keys(this.mcpServers).map((name) => {
148
+ logger.debug(`shutting down: ${name}...`);
149
+ this.mcpServers[name].shutdown();
150
+ })
151
+ );
152
+
153
+ this.mcpServers = {};
154
+ }
155
+
156
+ public getMcpServerNames(): string[] {
157
+ return Object.keys(this.mcpServers);
158
+ }
159
+
160
+ public getMcpServer(mcpServerName: string): McpServerInfo {
161
+ return this.getMcpServerInternal(mcpServerName);
162
+ }
163
+
164
+ public async addMcpServer(
165
+ mcpServerName: string,
166
+ url: string,
167
+ apiKey?: string,
168
+ tools?: Tool[]
169
+ ): Promise<void> {
170
+ logger.debug(`Adding mcp server ${mcpServerName}: ${url}`);
171
+ const sseTransportOptions: SSEClientTransportOptions = {};
172
+ if (apiKey) {
173
+ sseTransportOptions.authProvider = new TokenAuth(apiKey);
174
+ }
175
+ const urlO = new URL(url);
176
+ const transport = new SSEClientTransport(urlO, sseTransportOptions);
177
+ const client = new McpClient({
178
+ name: "@xalia/agent",
179
+ version: "1.0.0",
180
+ });
181
+
182
+ try {
183
+ await client.connect(transport);
184
+ } catch (e) {
185
+ // TODO: is this catch necessary?
186
+ await client.close();
187
+ throw e;
188
+ }
189
+ await this.addMcpServerWithClient(client, mcpServerName, tools);
190
+ }
191
+
192
+ /**
193
+ * Add MCP server from an already connected McpClient.
194
+ */
195
+ public async addMcpServerWithClient(
196
+ client: McpClient,
197
+ mcpServerName: string,
198
+ tools?: Tool[]
199
+ ): Promise<void> {
200
+ try {
201
+ // TODO; require the tools to be passed in.
202
+
203
+ if (!tools) {
204
+ const mcpTools = await client.listTools();
205
+ tools = mcpTools.tools;
206
+ }
207
+
208
+ this.mcpServers[mcpServerName] = new McpServerInfoInternal(client, tools);
209
+ } catch (e) {
210
+ await client.close();
211
+ throw e;
212
+ }
213
+ }
214
+
215
+ public async removeMcpServer(mcpServerName: string) {
216
+ const server = this.getMcpServerInternal(mcpServerName);
217
+ delete this.mcpServers[mcpServerName];
218
+ await server.shutdown();
219
+ this.enabledToolsDirty = true;
220
+ }
221
+
222
+ public enableAllTools(mcpServerName: string) {
223
+ logger.debug(`enableAllTools: ${mcpServerName}`);
224
+ const server = this.getMcpServerInternal(mcpServerName);
225
+ for (const tool of server.getTools()) {
226
+ logger.debug(`enable: ${tool.name}`);
227
+ server.enableTool(tool.name);
228
+ }
229
+ this.enabledToolsDirty = true;
230
+ }
231
+
232
+ public disableAllTools(mcpServerName: string) {
233
+ logger.debug(`disableAllTools: ${mcpServerName}`);
234
+ const server = this.getMcpServerInternal(mcpServerName);
235
+ for (const tool of server.getTools()) {
236
+ logger.debug(`disable: ${tool.name}`);
237
+ server.disableTool(tool.name);
238
+ }
239
+ this.enabledToolsDirty = true;
240
+ }
241
+
242
+ public enableTool(mcpServerName: string, toolName: string) {
243
+ logger.debug(`enableTool: ${mcpServerName} ${toolName}`);
244
+ const server = this.getMcpServerInternal(mcpServerName);
245
+ server.enableTool(toolName);
246
+ this.enabledToolsDirty = true;
247
+ }
248
+
249
+ public disableTool(mcpServerName: string, toolName: string) {
250
+ const server = this.getMcpServerInternal(mcpServerName);
251
+ server.disableTool(toolName);
252
+ this.enabledToolsDirty = true;
253
+ }
254
+
255
+ public getOpenAITools(): ChatCompletionTool[] {
256
+ if (this.enabledToolsDirty) {
257
+ this.enabledOpenAITools = computeOpenAIToolList(this.mcpServers);
258
+ this.enabledToolsDirty = false;
259
+ }
260
+
261
+ return this.enabledOpenAITools;
262
+ }
263
+
264
+ /**
265
+ * Note the `qualifiedToolName` is the full `{mcpServerName}/{toolName}` as
266
+ * in the openai spec.
267
+ */
268
+ public async invoke(
269
+ qualifiedToolName: string,
270
+ args: unknown
271
+ ): Promise<string> {
272
+ const [mcpServerName, toolName] = splitQualifiedName(qualifiedToolName);
273
+ logger.debug(`invoke: qualified: ${qualifiedToolName}`);
274
+ logger.debug(
275
+ `invoke: mcpServerName: ${mcpServerName}, toolName: ${toolName}`
276
+ );
277
+ logger.debug(`invoke: args: ${JSON.stringify(args)}`);
278
+
279
+ const server = this.getMcpServerInternal(mcpServerName);
280
+ const cb = server.getCallback(toolName);
281
+ if (!cb) {
282
+ throw `Unknown tool ${qualifiedToolName}`;
283
+ }
284
+
285
+ return cb(JSON.stringify(args));
286
+ }
287
+
288
+ /**
289
+ * "Settings" refers to the set of added servers and enabled tools.
290
+ */
291
+ public getMcpServerSettings(): McpServerManagerSettings {
292
+ const config: McpServerManagerSettings = {};
293
+ for (const [serverName, server] of Object.entries(this.mcpServers)) {
294
+ config[serverName] = structuredClone(server.getEnabledTools());
295
+ }
296
+
297
+ return config;
298
+ }
299
+
300
+ private getMcpServerInternal(mcpServerName: string): McpServerInfoInternal {
301
+ const server = this.mcpServers[mcpServerName];
302
+ if (server) {
303
+ return server;
304
+ }
305
+ throw Error(`unknown server ${mcpServerName}`);
306
+ }
307
+ }
308
+
309
+ export function computeQualifiedName(
310
+ mcpServerName: string,
311
+ toolName: string
312
+ ): string {
313
+ return `${mcpServerName}__${toolName}`;
314
+ }
315
+
316
+ export function splitQualifiedName(
317
+ qualifiedToolName: string
318
+ ): [string, string] {
319
+ const delimIdx = qualifiedToolName.indexOf("__");
320
+ if (delimIdx < 0) {
321
+ throw Error(`invalid qualified name: ${qualifiedToolName}`);
322
+ }
323
+
324
+ return [
325
+ qualifiedToolName.slice(0, delimIdx),
326
+ qualifiedToolName.slice(delimIdx + 2),
327
+ ];
328
+ }
329
+
330
+ export function computeOpenAIToolList(mcpServers: {
331
+ [mcpServer: string]: McpServerInfoInternal;
332
+ }): ChatCompletionTool[] {
333
+ const openaiTools: ChatCompletionTool[] = [];
334
+
335
+ for (const mcpServerName in mcpServers) {
336
+ const mcpServer = mcpServers[mcpServerName];
337
+ const tools = mcpServer.getTools();
338
+ const enabled = mcpServer.getEnabledTools();
339
+
340
+ for (const mcpTool of tools) {
341
+ const toolName = mcpTool.name;
342
+ if (enabled[toolName]) {
343
+ const qualifiedName = computeQualifiedName(mcpServerName, toolName);
344
+ const openaiTool = mcpToolToOpenAITool(mcpTool, qualifiedName);
345
+ openaiTools.push(openaiTool);
346
+ }
347
+ }
348
+ }
349
+
350
+ return openaiTools;
351
+ }
352
+
353
+ export function mcpToolToOpenAITool(
354
+ tool: Tool,
355
+ qualifiedName?: string
356
+ ): ChatCompletionTool {
357
+ return {
358
+ type: "function",
359
+ function: {
360
+ name: qualifiedName || tool.name,
361
+ description: tool.description,
362
+ parameters: tool.inputSchema,
363
+ },
364
+ };
365
+ }
@@ -0,0 +1,24 @@
1
+ import * as fs from "fs";
2
+ import { execSync } from "child_process";
3
+ import { IPlatform } from "./iplatform";
4
+
5
+ /**
6
+ * Implementation of the IPlatform interface for node.js
7
+ */
8
+ export const NODE_PLATFORM: IPlatform = {
9
+ openUrl: (url: string) => {
10
+ const platform = process.platform;
11
+ if (platform === "darwin") {
12
+ execSync(`open '${url}'`);
13
+ } else if (platform === "linux") {
14
+ execSync(`xdg-open '${url}'`);
15
+ } else if (platform === "win32") {
16
+ execSync(`start '${url}'`);
17
+ } else {
18
+ throw `Unknown platform ${platform}`;
19
+ }
20
+ },
21
+ load: async (filename: string): Promise<string> => {
22
+ return fs.readFileSync(filename, { encoding: "utf8" });
23
+ },
24
+ };
@@ -0,0 +1,43 @@
1
+ import { ILLM } from "./llm";
2
+ import { OpenAI } from "openai";
3
+
4
+ export class OpenAILLM implements ILLM {
5
+ private readonly openai: OpenAI;
6
+ private model: string;
7
+
8
+ constructor(
9
+ apiKey: string,
10
+ apiUrl: string | undefined,
11
+ model: string | undefined
12
+ ) {
13
+ this.openai = new OpenAI({
14
+ apiKey,
15
+ baseURL: apiUrl,
16
+ dangerouslyAllowBrowser: true,
17
+ });
18
+ this.model = model || "gpt-4o-mini";
19
+ }
20
+
21
+ public setModel(model: string) {
22
+ this.model = model;
23
+ }
24
+
25
+ getModel(): string {
26
+ return this.model;
27
+ }
28
+
29
+ getUrl(): string {
30
+ return this.openai.baseURL;
31
+ }
32
+
33
+ public async getConversationResponse(
34
+ messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[],
35
+ tools?: OpenAI.Chat.Completions.ChatCompletionTool[]
36
+ ): Promise<OpenAI.Chat.Completions.ChatCompletion> {
37
+ return this.openai.chat.completions.create({
38
+ model: this.model,
39
+ messages,
40
+ tools,
41
+ });
42
+ }
43
+ }
package/src/options.ts ADDED
@@ -0,0 +1,103 @@
1
+ import { boolean, option, optional, string, flag, number } from "cmd-ts";
2
+ import { ArgParser } from "cmd-ts/dist/cjs/argparser";
3
+ import { Descriptive, ProvidesHelp } from "cmd-ts/dist/cjs/helpdoc";
4
+
5
+ export type Option = ReturnType<typeof option>;
6
+ export type OptionalOption = ArgParser<string | undefined> &
7
+ ProvidesHelp &
8
+ Partial<Descriptive>;
9
+
10
+ /// Prevents env content from being displayed in the help text.
11
+ export function secretOption({
12
+ long,
13
+ short,
14
+ env,
15
+ description,
16
+ }: {
17
+ long: string;
18
+ short?: string;
19
+ env?: string;
20
+ description: string;
21
+ }): OptionalOption {
22
+ if (env) {
23
+ return option({
24
+ type: optional(string),
25
+ long,
26
+ short,
27
+ description: `${description} [env: ${env}]`,
28
+ defaultValue: () => process.env[env],
29
+ defaultValueIsSerializable: false, // hides the value from --help
30
+ });
31
+ }
32
+
33
+ return option({
34
+ type: optional(string),
35
+ long,
36
+ short,
37
+ description: `${description} (can also be set via ${env} env var)`,
38
+ });
39
+ }
40
+
41
+ export const promptFile = option({
42
+ type: optional(string),
43
+ long: "prompt",
44
+ short: "p",
45
+ description: "File containing user's first prompt to LLM",
46
+ });
47
+
48
+ export const imageFile = option({
49
+ type: optional(string),
50
+ long: "image",
51
+ short: "i",
52
+ description: "File containing image input",
53
+ });
54
+
55
+ export const systemPromptFile = option({
56
+ type: optional(string),
57
+ long: "sysprompt",
58
+ short: "s",
59
+ description: "File containing system prompt",
60
+ });
61
+
62
+ export const llmModel = option({
63
+ type: optional(string),
64
+ long: "model",
65
+ short: "m",
66
+ description: "LLM model",
67
+ env: "LLM_MODEL",
68
+ });
69
+
70
+ export const oneShot = flag({
71
+ type: boolean,
72
+ long: "one-shot",
73
+ short: "1",
74
+ description: "Exit after first reply (implies --approve-tools)",
75
+ });
76
+
77
+ export const approveTools = flag({
78
+ type: boolean,
79
+ long: "approve-tools",
80
+ short: "y",
81
+ description: "Automatically approve all tool calls",
82
+ });
83
+
84
+ export const approveToolsUpTo = option({
85
+ type: optional(number),
86
+ long: "approve-tools-up-to",
87
+ description: "Automatically approve all tool calls up to some number",
88
+ });
89
+
90
+ export const openaiApiKey = secretOption({
91
+ long: "openai-api-key",
92
+ short: "o",
93
+ description: "OpenAI (or compatible protocol) API Key",
94
+ env: "OPENAI_API_KEY",
95
+ });
96
+
97
+ export const openaiApiUrl = option({
98
+ type: optional(string),
99
+ long: "openai-api-url",
100
+ short: "u",
101
+ description: "OpenAI (or compatible protocol) RPC endpoint url",
102
+ env: "OPENAI_API_URL",
103
+ });
package/src/prompt.ts ADDED
@@ -0,0 +1,93 @@
1
+ import readline from "readline";
2
+
3
+ const DEFAULT_PROMPT: string = "USER: ";
4
+
5
+ export class Prompt {
6
+ private line: string | undefined;
7
+ private online: { (line: string | undefined): void } | undefined;
8
+ // private onerror: { (reason?: any): void } | undefined;
9
+ private prompt: readline.Interface;
10
+
11
+ constructor() {
12
+ this.prompt = readline.createInterface({
13
+ input: process.stdin,
14
+ output: process.stdout,
15
+ prompt: DEFAULT_PROMPT,
16
+ });
17
+
18
+ this.prompt.on("line", (line) => {
19
+ this.line = line;
20
+ this.resolve();
21
+ });
22
+ this.prompt.on("close", () => {
23
+ this.line = undefined;
24
+ this.resolve();
25
+ });
26
+ }
27
+
28
+ async run(prompt?: string): Promise<string | undefined> {
29
+ // Clear any line
30
+ this.line = "";
31
+
32
+ return new Promise<string | undefined>((r) => {
33
+ this.online = r;
34
+ if (prompt) {
35
+ this.prompt.setPrompt(prompt);
36
+ }
37
+ this.prompt.prompt();
38
+ if (prompt) {
39
+ this.prompt.setPrompt(DEFAULT_PROMPT);
40
+ }
41
+ });
42
+ }
43
+
44
+ shutdown(): void {
45
+ this.prompt.close();
46
+ }
47
+
48
+ private resolve(): void {
49
+ if (this.online) {
50
+ this.online(this.line);
51
+ }
52
+ this.online = undefined;
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Support prompts:
58
+ * - some text (msg: some text, cmds: undefined)
59
+ * - :i image.png some text (msg: some text, cmds: ["i", "image.png"])
60
+ * - :i image.png (msg: undefined, cmds: ["i", "image.png"])
61
+ * - :l (msg: undefined, cmds: ["l"])
62
+ * - :e toolName .. (msg: undefined, cmds: ["e", "toolName", ...])
63
+ * - :ea toolName .. (msg: undefined, cmds: ["ea", "toolName", ...])
64
+ */
65
+ export function parsePrompt(prompt: string): {
66
+ msg?: string;
67
+ cmds?: string[];
68
+ } {
69
+ prompt = prompt.trim();
70
+
71
+ let msg: string | undefined = undefined;
72
+ let cmds: string[] | undefined = undefined;
73
+
74
+ if (prompt.startsWith(":") || prompt.startsWith("/")) {
75
+ cmds = prompt.split(" ");
76
+ cmds[0] = cmds[0].slice(1);
77
+
78
+ if (cmds[0] == "i") {
79
+ // :i is special as it may have a trailing message
80
+ const fileDelim = prompt.indexOf(" ", 3);
81
+ if (fileDelim < 0) {
82
+ cmds = [cmds[0], prompt.slice(3)];
83
+ } else {
84
+ msg = prompt.slice(fileDelim + 1);
85
+ cmds = [cmds[0], prompt.slice(3, fileDelim)];
86
+ }
87
+ }
88
+ } else {
89
+ msg = prompt;
90
+ }
91
+
92
+ return { msg, cmds };
93
+ }