@xalia/agent 0.5.8 → 0.6.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 (185) hide show
  1. package/README.md +23 -8
  2. package/dist/agent/src/agent/agent.js +173 -96
  3. package/dist/agent/src/agent/agentUtils.js +82 -53
  4. package/dist/agent/src/agent/compressingContextManager.js +102 -0
  5. package/dist/agent/src/agent/context.js +189 -0
  6. package/dist/agent/src/agent/dummyLLM.js +46 -5
  7. package/dist/agent/src/agent/iAgentEventHandler.js +2 -0
  8. package/dist/agent/src/agent/mcpServerManager.js +22 -23
  9. package/dist/agent/src/agent/nullAgentEventHandler.js +21 -0
  10. package/dist/agent/src/agent/nullPlatform.js +14 -0
  11. package/dist/agent/src/agent/openAILLMStreaming.js +12 -7
  12. package/dist/agent/src/agent/promptProvider.js +63 -0
  13. package/dist/agent/src/agent/repeatLLM.js +5 -5
  14. package/dist/agent/src/agent/sudoMcpServerManager.js +11 -9
  15. package/dist/agent/src/agent/tokenAuth.js +7 -7
  16. package/dist/agent/src/agent/tools.js +1 -1
  17. package/dist/agent/src/chat/client/chatClient.js +733 -0
  18. package/dist/agent/src/chat/client/connection.js +209 -0
  19. package/dist/agent/src/chat/client/connection.test.js +188 -0
  20. package/dist/agent/src/chat/client/constants.js +5 -0
  21. package/dist/agent/src/chat/client/index.js +15 -0
  22. package/dist/agent/src/chat/client/interfaces.js +2 -0
  23. package/dist/agent/src/chat/client/responseHandler.js +105 -0
  24. package/dist/agent/src/chat/client/sessionClient.js +331 -0
  25. package/dist/agent/src/chat/client/teamManager.js +2 -0
  26. package/dist/agent/src/chat/{apiKeyManager.js → data/apiKeyManager.js} +4 -0
  27. package/dist/agent/src/chat/data/dataModels.js +2 -0
  28. package/dist/agent/src/chat/data/database.js +749 -0
  29. package/dist/agent/src/chat/data/dbMcpServerConfigs.js +47 -0
  30. package/dist/agent/src/chat/protocol/connectionMessages.js +5 -0
  31. package/dist/agent/src/chat/protocol/constants.js +50 -0
  32. package/dist/agent/src/chat/protocol/errors.js +22 -0
  33. package/dist/agent/src/chat/protocol/messages.js +110 -0
  34. package/dist/agent/src/chat/server/chatContextManager.js +405 -0
  35. package/dist/agent/src/chat/server/connectionManager.js +352 -0
  36. package/dist/agent/src/chat/server/connectionManager.test.js +159 -0
  37. package/dist/agent/src/chat/server/conversation.js +198 -0
  38. package/dist/agent/src/chat/server/errorUtils.js +23 -0
  39. package/dist/agent/src/chat/server/openSession.js +869 -0
  40. package/dist/agent/src/chat/server/server.js +177 -0
  41. package/dist/agent/src/chat/server/sessionFileManager.js +161 -0
  42. package/dist/agent/src/chat/server/sessionRegistry.js +700 -0
  43. package/dist/agent/src/chat/server/sessionRegistry.test.js +97 -0
  44. package/dist/agent/src/chat/server/test-utils/mockFactories.js +307 -0
  45. package/dist/agent/src/chat/server/tools.js +243 -0
  46. package/dist/agent/src/chat/utils/agentSessionMap.js +66 -0
  47. package/dist/agent/src/chat/utils/approvalManager.js +85 -0
  48. package/dist/agent/src/{utils → chat/utils}/asyncLock.js +3 -3
  49. package/dist/agent/src/chat/{asyncQueue.js → utils/asyncQueue.js} +12 -2
  50. package/dist/agent/src/chat/utils/htmlToText.js +84 -0
  51. package/dist/agent/src/chat/utils/multiAsyncQueue.js +42 -0
  52. package/dist/agent/src/chat/utils/search.js +145 -0
  53. package/dist/agent/src/chat/utils/userResolver.js +46 -0
  54. package/dist/agent/src/chat/{websocket.js → utils/websocket.js} +2 -0
  55. package/dist/agent/src/test/agent.test.js +332 -0
  56. package/dist/agent/src/test/approvalManager.test.js +58 -0
  57. package/dist/agent/src/test/chatContextManager.test.js +392 -0
  58. package/dist/agent/src/test/clientServerConnection.test.js +158 -0
  59. package/dist/agent/src/test/compressingContextManager.test.js +65 -0
  60. package/dist/agent/src/test/context.test.js +83 -0
  61. package/dist/agent/src/test/conversation.test.js +89 -0
  62. package/dist/agent/src/test/db.test.js +262 -90
  63. package/dist/agent/src/test/dbMcpServerConfigs.test.js +72 -0
  64. package/dist/agent/src/test/dbTestTools.js +99 -0
  65. package/dist/agent/src/test/imageLoad.test.js +8 -7
  66. package/dist/agent/src/test/mcpServerManager.test.js +21 -18
  67. package/dist/agent/src/test/multiAsyncQueue.test.js +101 -0
  68. package/dist/agent/src/test/openaiStreaming.test.js +12 -11
  69. package/dist/agent/src/test/prompt.test.js +5 -4
  70. package/dist/agent/src/test/promptProvider.test.js +28 -0
  71. package/dist/agent/src/test/responseHandler.test.js +61 -0
  72. package/dist/agent/src/test/sudoMcpServerManager.test.js +14 -12
  73. package/dist/agent/src/test/testTools.js +109 -0
  74. package/dist/agent/src/test/tools.test.js +31 -0
  75. package/dist/agent/src/tool/agentChat.js +21 -10
  76. package/dist/agent/src/tool/agentMain.js +1 -1
  77. package/dist/agent/src/tool/chatMain.js +235 -58
  78. package/dist/agent/src/tool/commandPrompt.js +15 -9
  79. package/dist/agent/src/tool/files.js +20 -16
  80. package/dist/agent/src/tool/nodePlatform.js +47 -3
  81. package/dist/agent/src/tool/options.js +4 -4
  82. package/dist/agent/src/tool/prompt.js +19 -13
  83. package/eslint.config.mjs +14 -1
  84. package/package.json +14 -6
  85. package/scripts/chat_server +8 -0
  86. package/scripts/setup_chat +7 -2
  87. package/scripts/shutdown_chat_server +3 -0
  88. package/scripts/test_chat +135 -17
  89. package/src/agent/agent.ts +270 -135
  90. package/src/agent/agentUtils.ts +136 -95
  91. package/src/agent/compressingContextManager.ts +164 -0
  92. package/src/agent/context.ts +268 -0
  93. package/src/agent/dummyLLM.ts +76 -8
  94. package/src/agent/iAgentEventHandler.ts +54 -0
  95. package/src/agent/iplatform.ts +1 -0
  96. package/src/agent/mcpServerManager.ts +32 -30
  97. package/src/agent/nullAgentEventHandler.ts +20 -0
  98. package/src/agent/nullPlatform.ts +13 -0
  99. package/src/agent/openAILLMStreaming.ts +12 -6
  100. package/src/agent/promptProvider.ts +87 -0
  101. package/src/agent/repeatLLM.ts +5 -5
  102. package/src/agent/sudoMcpServerManager.ts +13 -11
  103. package/src/agent/tokenAuth.ts +7 -7
  104. package/src/agent/tools.ts +3 -1
  105. package/src/chat/client/chatClient.ts +900 -0
  106. package/src/chat/client/connection.test.ts +241 -0
  107. package/src/chat/client/connection.ts +276 -0
  108. package/src/chat/client/constants.ts +3 -0
  109. package/src/chat/client/index.ts +18 -0
  110. package/src/chat/client/interfaces.ts +34 -0
  111. package/src/chat/client/responseHandler.ts +131 -0
  112. package/src/chat/client/sessionClient.ts +443 -0
  113. package/src/chat/client/teamManager.ts +29 -0
  114. package/src/chat/{apiKeyManager.ts → data/apiKeyManager.ts} +6 -2
  115. package/src/chat/data/dataModels.ts +85 -0
  116. package/src/chat/data/database.ts +982 -0
  117. package/src/chat/data/dbMcpServerConfigs.ts +59 -0
  118. package/src/chat/protocol/connectionMessages.ts +49 -0
  119. package/src/chat/protocol/constants.ts +55 -0
  120. package/src/chat/protocol/errors.ts +16 -0
  121. package/src/chat/protocol/messages.ts +682 -0
  122. package/src/chat/server/README.md +127 -0
  123. package/src/chat/server/chatContextManager.ts +612 -0
  124. package/src/chat/server/connectionManager.test.ts +266 -0
  125. package/src/chat/server/connectionManager.ts +541 -0
  126. package/src/chat/server/conversation.ts +269 -0
  127. package/src/chat/server/errorUtils.ts +28 -0
  128. package/src/chat/server/openSession.ts +1332 -0
  129. package/src/chat/server/server.ts +177 -0
  130. package/src/chat/server/sessionFileManager.ts +239 -0
  131. package/src/chat/server/sessionRegistry.test.ts +138 -0
  132. package/src/chat/server/sessionRegistry.ts +1064 -0
  133. package/src/chat/server/test-utils/mockFactories.ts +422 -0
  134. package/src/chat/server/tools.ts +265 -0
  135. package/src/chat/utils/agentSessionMap.ts +76 -0
  136. package/src/chat/utils/approvalManager.ts +111 -0
  137. package/src/{utils → chat/utils}/asyncLock.ts +3 -3
  138. package/src/chat/{asyncQueue.ts → utils/asyncQueue.ts} +14 -3
  139. package/src/chat/utils/htmlToText.ts +61 -0
  140. package/src/chat/utils/multiAsyncQueue.ts +52 -0
  141. package/src/chat/utils/search.ts +139 -0
  142. package/src/chat/utils/userResolver.ts +48 -0
  143. package/src/chat/{websocket.ts → utils/websocket.ts} +2 -0
  144. package/src/test/agent.test.ts +487 -0
  145. package/src/test/approvalManager.test.ts +73 -0
  146. package/src/test/chatContextManager.test.ts +521 -0
  147. package/src/test/clientServerConnection.test.ts +207 -0
  148. package/src/test/compressingContextManager.test.ts +82 -0
  149. package/src/test/context.test.ts +105 -0
  150. package/src/test/conversation.test.ts +109 -0
  151. package/src/test/db.test.ts +351 -103
  152. package/src/test/dbMcpServerConfigs.test.ts +112 -0
  153. package/src/test/dbTestTools.ts +153 -0
  154. package/src/test/imageLoad.test.ts +7 -6
  155. package/src/test/mcpServerManager.test.ts +19 -14
  156. package/src/test/multiAsyncQueue.test.ts +125 -0
  157. package/src/test/openaiStreaming.test.ts +11 -10
  158. package/src/test/prompt.test.ts +4 -3
  159. package/src/test/promptProvider.test.ts +33 -0
  160. package/src/test/responseHandler.test.ts +78 -0
  161. package/src/test/sudoMcpServerManager.test.ts +22 -15
  162. package/src/test/testTools.ts +146 -0
  163. package/src/test/tools.test.ts +39 -0
  164. package/src/tool/agentChat.ts +26 -12
  165. package/src/tool/agentMain.ts +1 -1
  166. package/src/tool/chatMain.ts +283 -100
  167. package/src/tool/commandPrompt.ts +25 -9
  168. package/src/tool/files.ts +25 -19
  169. package/src/tool/nodePlatform.ts +52 -3
  170. package/src/tool/options.ts +4 -2
  171. package/src/tool/prompt.ts +22 -15
  172. package/test_data/dummyllm_script_crash.json +32 -0
  173. package/test_data/frog.png.b64 +1 -0
  174. package/vitest.config.ts +39 -0
  175. package/dist/agent/src/chat/client.js +0 -310
  176. package/dist/agent/src/chat/conversationManager.js +0 -502
  177. package/dist/agent/src/chat/db.js +0 -218
  178. package/dist/agent/src/chat/messages.js +0 -29
  179. package/dist/agent/src/chat/server.js +0 -158
  180. package/src/chat/client.ts +0 -445
  181. package/src/chat/conversationManager.ts +0 -730
  182. package/src/chat/db.ts +0 -304
  183. package/src/chat/messages.ts +0 -266
  184. package/src/chat/server.ts +0 -177
  185. /package/{frog.png → test_data/frog.png} +0 -0
@@ -1,12 +1,6 @@
1
- import {
2
- command,
3
- optional,
4
- option,
5
- flag,
6
- string,
7
- number,
8
- subcommands,
9
- } from "cmd-ts";
1
+ /* eslint-disable @typescript-eslint/only-throw-error */
2
+
3
+ import { command, optional, option, string, number, subcommands } from "cmd-ts";
10
4
  import { stdout } from "process";
11
5
  import * as fs from "fs";
12
6
  import { strict as assert } from "assert";
@@ -14,22 +8,105 @@ import { strict as assert } from "assert";
14
8
  import { configuration } from "@xalia/xmcp/tool";
15
9
  import { getLogger } from "@xalia/xmcp/sdk";
16
10
 
17
- import { IConversation } from "../agent/agent";
18
- import { ChatClient } from "../chat/client";
19
- import { runServer } from "../chat/server";
20
11
  import {
21
- ClientUserMessage,
12
+ ChatClient,
13
+ SessionClient,
14
+ IChatClientEventHandler,
15
+ } from "../chat/client";
16
+ import {
17
+ runServer,
18
+ resolveSessionIdFromIdentifier,
19
+ } from "../chat/server/server";
20
+ import {
22
21
  ServerToClient,
23
22
  decodeAssistantMessageParam,
24
- } from "../chat/messages";
23
+ } from "../chat/protocol/messages";
25
24
 
26
25
  import { IPrompt, Prompt, ScriptPrompt } from "./prompt";
27
26
  import * as options from "./options";
28
27
  import { CommandPrompt } from "./commandPrompt";
29
28
  import { NODE_PLATFORM } from "./nodePlatform";
29
+ import { Database, createSessionParticipantMap } from "../chat/data/database";
30
+ import { loadImageB64 } from "./files";
30
31
 
31
32
  const logger = getLogger();
32
33
 
34
+ const listSessions = command({
35
+ name: "delete-session",
36
+ args: {
37
+ supabaseUrl: options.supabaseUrl,
38
+ supabaseKey: options.supabaseKey,
39
+ },
40
+ handler: async ({ supabaseUrl, supabaseKey }): Promise<void> => {
41
+ const db = new Database(supabaseUrl, supabaseKey);
42
+ console.log(JSON.stringify(await db.sessionsGet()));
43
+ },
44
+ });
45
+
46
+ const clearSessions = command({
47
+ name: "clear-sessions",
48
+ args: {
49
+ supabaseUrl: options.supabaseUrl,
50
+ supabaseKey: options.supabaseKey,
51
+ },
52
+ handler: async ({ supabaseUrl, supabaseKey }): Promise<void> => {
53
+ const db = new Database(supabaseUrl, supabaseKey);
54
+ await db.clearSessions();
55
+ },
56
+ });
57
+
58
+ const deleteSession = command({
59
+ name: "delete-session",
60
+ args: {
61
+ supabaseUrl: options.supabaseUrl,
62
+ supabaseKey: options.supabaseKey,
63
+ session: option({
64
+ type: string,
65
+ long: "session",
66
+ description:
67
+ "Session identifier (id, name, user/name or name of new session)",
68
+ env: "SESSION",
69
+ }),
70
+ },
71
+ handler: async ({ supabaseUrl, supabaseKey, session }): Promise<void> => {
72
+ const db = new Database(supabaseUrl, supabaseKey);
73
+ const sessionId = await resolveSessionIdFromIdentifier(db, session);
74
+ if (sessionId) {
75
+ await db.sessionDeleteById(sessionId);
76
+ } else {
77
+ throw `No such session ${session}`;
78
+ }
79
+ },
80
+ });
81
+
82
+ const listParticipants = command({
83
+ name: "list-participants",
84
+ args: {
85
+ supabaseUrl: options.supabaseUrl,
86
+ supabaseKey: options.supabaseKey,
87
+ session: option({
88
+ type: string,
89
+ long: "session",
90
+ description:
91
+ "Session identifier (id, name, user/name or name of new session)",
92
+ env: "SESSION",
93
+ }),
94
+ },
95
+ handler: async ({ supabaseUrl, supabaseKey, session }): Promise<void> => {
96
+ const db = new Database(supabaseUrl, supabaseKey);
97
+ const sessionId = await resolveSessionIdFromIdentifier(db, session);
98
+ if (sessionId) {
99
+ const participants = await db.sessionGetParticipants(sessionId);
100
+
101
+ console.log(`participants: ${JSON.stringify(participants)}`);
102
+ const users = createSessionParticipantMap(participants);
103
+ console.log(JSON.stringify(users));
104
+ } else {
105
+ throw `No such session ${session}`;
106
+ }
107
+ },
108
+ });
109
+
33
110
  const server = command({
34
111
  name: "server",
35
112
  args: {
@@ -46,6 +123,7 @@ const server = command({
46
123
  xmcpUrl: options.xmcpUrl,
47
124
  pidFile: options.pidFile,
48
125
  },
126
+ // eslint-disable-next-line @typescript-eslint/require-await
49
127
  handler: async ({
50
128
  port,
51
129
  supabaseUrl,
@@ -58,58 +136,61 @@ const server = command({
58
136
  fs.writeFileSync(pidFile, process.pid.toString());
59
137
  }
60
138
 
61
- runServer(port, supabaseUrl, supabaseKey, llmUrl, xmcpUrl);
139
+ void runServer(port, supabaseUrl, supabaseKey, llmUrl, xmcpUrl);
62
140
  },
63
141
  });
64
142
 
65
143
  const client = command({
66
144
  name: "client",
67
145
  args: {
68
- host: option({
146
+ url: option({
69
147
  type: string,
70
- long: "host",
148
+ long: "url",
71
149
  short: "h",
72
- env: "CHAT_SERVER_HOST",
73
- defaultValue: () => "localhost",
74
- }),
75
- port: option({
76
- type: number,
77
- long: "port",
78
- short: "p",
79
- env: "CHAT_SERVER_PORT",
80
- defaultValue: () => 5003,
150
+ env: "CHAT_URL",
151
+ description: "Chat server url",
152
+ defaultValue: () => "ws://localhost:5003",
81
153
  }),
82
154
  apiKey: options.apiKey,
83
- session: option({
84
- type: string,
85
- long: "session",
86
- description: "Session identifier (id, name, user/name)",
87
- env: "SESSION",
155
+ sessionId: option({
156
+ type: optional(string),
157
+ long: "session-id",
158
+ description: "Join existing session with the given UUID",
159
+ env: "SESSION_UUID",
160
+ }),
161
+ teamId: option({
162
+ type: optional(string),
163
+ long: "team-id",
164
+ description:
165
+ "Team uuid, if not provided, a user session will be created.",
166
+ env: "TEAM_ID",
88
167
  }),
89
- agentProfile: option({
168
+ agentProfileId: option({
90
169
  type: optional(string),
91
- long: "agent-profile",
92
- description: "Create new session using agent profile (id, name)",
93
- env: "AGENT_PROFILE",
170
+ long: "agent-profile-id",
171
+ description: "Create new session with agent profile (uuid)",
172
+ env: "AGENT_PROFILE_ID",
173
+ }),
174
+ sessionTitle: option({
175
+ type: optional(string),
176
+ long: "session-title",
177
+ description: "Title for new session",
178
+ env: "SESSION_TITLE",
94
179
  }),
95
180
  script: option({
96
181
  type: optional(string),
97
182
  long: "script",
98
183
  description: "Script (file) to execute and then exit.",
99
184
  }),
100
- test: flag({
101
- long: "test",
102
- description: "Run a test client and disconnect",
103
- }),
104
185
  },
105
186
  handler: async ({
106
- host,
107
- port,
187
+ url,
108
188
  apiKey,
109
- session,
110
- agentProfile,
189
+ sessionId,
190
+ agentProfileId,
191
+ sessionTitle,
192
+ teamId,
111
193
  script,
112
- test,
113
194
  }): Promise<void> => {
114
195
  if (!apiKey) {
115
196
  // TODO: configFIle param list in ../main.ts
@@ -117,12 +198,6 @@ const client = command({
117
198
  apiKey = sudomcpConfig.api_key;
118
199
  }
119
200
 
120
- if (test) {
121
- await runClientTest(host, port, apiKey, session);
122
- return;
123
- }
124
-
125
- const onMessage = getCLIOnMessage();
126
201
  const repl: IPrompt = (() => {
127
202
  if (script) {
128
203
  const scriptLines = fs.readFileSync(script, "utf8").split("\n");
@@ -132,40 +207,87 @@ const client = command({
132
207
  })();
133
208
  const cmdPrompt = new CommandPrompt(repl);
134
209
 
135
- const client = await ChatClient.init(
136
- host,
137
- port,
138
- apiKey,
139
- onMessage,
140
- async () => await cmdPrompt.shutdown(),
141
- NODE_PLATFORM,
142
- session,
143
- agentProfile
144
- );
210
+ const eventHandler = getCLIEventHandler(cmdPrompt);
211
+
212
+ const [client, sessionClient] = await (async () => {
213
+ const newClient = await ChatClient.init(url, apiKey, eventHandler);
214
+ if (sessionId) {
215
+ if (agentProfileId || sessionTitle) {
216
+ throw "t agent-profile-id/session-title set joining existing session";
217
+ }
145
218
 
219
+ console.log(`joining existing session: ${sessionId} ...`);
220
+ const newSessionClient = await newClient.connectToSession(sessionId);
221
+ return [newClient, newSessionClient];
222
+ }
223
+
224
+ const newSessionClient = await newClient.createNewSession(
225
+ sessionTitle || "New Chat",
226
+ agentProfileId,
227
+ teamId
228
+ );
229
+ return [newClient, newSessionClient];
230
+ })();
146
231
  logger.debug("client created");
147
232
 
233
+ const onUnknownCommand = (cmds: string[]) => {
234
+ switch (cmds[0]) {
235
+ case "aa":
236
+ sessionClient.setAutoApproval(cmds[1], cmds[2], !!cmds[3]);
237
+ break;
238
+ case "error":
239
+ sessionClient.sendMessage({ type: "fatal_error", message: cmds[1] });
240
+ break;
241
+ case "ap": // Add Participant
242
+ case "au":
243
+ client.addTeamMember(cmds[1], cmds[2]);
244
+ break;
245
+ case "rp": // Remove Participant
246
+ case "ru":
247
+ client.removeTeamMember(cmds[1], cmds[2]);
248
+ break;
249
+ case "lp": // List Participants
250
+ case "lu":
251
+ console.log(JSON.stringify(sessionClient.getParticipants()));
252
+ break;
253
+ case "si": // Write Session id to file
254
+ fs.writeFileSync(cmds[1], sessionClient.getSessionUUID());
255
+ break;
256
+ case "ct": // Create Team
257
+ client.createNewTeam(cmds[1], []);
258
+ break;
259
+ case "ci": // Write team id to file
260
+ fs.writeFileSync(cmds[1], client.getCurrentTeamId() ?? "");
261
+ break;
262
+ case "sw": // Set workspace
263
+ setWorkspaceFromPrompt(sessionClient, cmds);
264
+ break;
265
+ default:
266
+ console.log(`error: Unknown command ${cmds[0]}`);
267
+ }
268
+ };
269
+
270
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
148
271
  while (true) {
149
272
  const [msgText, imageFile] = await cmdPrompt.getNextPrompt(
150
- undefined as unknown as IConversation, // TODO: client
151
- client.getSudoMcpServerManager()
273
+ sessionClient,
274
+ sessionClient.getSudoMcpServerManager(),
275
+ onUnknownCommand
152
276
  );
153
277
  assert(imageFile === undefined);
154
278
 
155
279
  if (msgText === undefined) {
156
280
  logger.debug("exiting...");
157
- client.close();
281
+ logger.info("exiting...");
282
+ client.shutdown();
158
283
  return;
159
284
  }
160
285
 
161
286
  // TODO: other prompts
162
287
 
163
288
  if (msgText.length > 0) {
164
- const msg: ClientUserMessage = {
165
- type: "msg",
166
- message: msgText,
167
- };
168
- client.sendMessage(msg);
289
+ logger.debug(`sending message: ${msgText}`);
290
+ sessionClient.userMessage(msgText);
169
291
  } else {
170
292
  logger.debug("(ignoring empty message)");
171
293
  }
@@ -173,12 +295,66 @@ const client = command({
173
295
  },
174
296
  });
175
297
 
176
- function getCLIOnMessage(): (msg: ServerToClient) => void {
298
+ function setWorkspaceFromPrompt(
299
+ sessionClient: SessionClient,
300
+ cmds: string[]
301
+ ): void {
302
+ let msg = "";
303
+ let image = undefined;
304
+ if (!cmds[1]) {
305
+ sessionClient.setWorkspace(undefined, undefined);
306
+ return;
307
+ }
308
+ if (cmds[1].startsWith("file:")) {
309
+ image = loadImageB64(cmds[1].slice(5));
310
+ msg = cmds.slice(2).join(" ");
311
+ console.log(`setting workspace: ${msg} (image: ${image})`);
312
+ } else {
313
+ msg = cmds.slice(1).join(" ");
314
+ console.log(`setting workspace: ${msg}`);
315
+ }
316
+
317
+ sessionClient.setWorkspace(msg, image);
318
+ }
319
+
320
+ function getCLIEventHandler(
321
+ cmdPrompt?: CommandPrompt
322
+ ): IChatClientEventHandler {
323
+ let numErrors = 0;
324
+
325
+ const onClosed = async () => {
326
+ logger.debug("[onClosed]");
327
+ if (cmdPrompt) {
328
+ // TODO: check if this fully shuts down
329
+ await cmdPrompt.shutdown();
330
+ }
331
+
332
+ // If there have been errors, exit with an error code.
333
+ process.exit(numErrors);
334
+ };
335
+
336
+ const onError = (message: string) => {
337
+ numErrors++;
338
+ logger.debug("[onError]");
339
+ console.error(message);
340
+ };
341
+
177
342
  let startMsg = true;
178
- return (msg: ServerToClient) => {
343
+ const onMessage = async (msg: ServerToClient, client: ChatClient) => {
344
+ let sessionClient: SessionClient | undefined = undefined;
345
+ try {
346
+ sessionClient = client.getCurrentSession("[chatMain.onMessage]");
347
+ } catch (error) {
348
+ logger.error(String(error));
349
+ return;
350
+ }
351
+
179
352
  switch (msg.type) {
180
353
  case "user_msg":
181
- stdout.write(`[${msg.from}]: ${msg.message}\n`);
354
+ stdout.write(`[${msg.user_uuid}]: ${msg.message || ""}\n`);
355
+ if (msg.imageB64) {
356
+ stdout.write(`[${msg.user_uuid}]: IMAGE: ${msg.imageB64}\n`);
357
+ }
182
358
  break;
183
359
  case "agent_msg":
184
360
  {
@@ -199,40 +375,43 @@ function getCLIOnMessage(): (msg: ServerToClient) => void {
199
375
  startMsg = true;
200
376
  }
201
377
  break;
378
+ case "approve_tool_call":
379
+ {
380
+ const result = cmdPrompt
381
+ ? await cmdPrompt.promptToolCall(msg.tool_call)
382
+ : true;
383
+ logger.debug(`cmdPrompt.promptToolCall returned: ${String(result)}`);
384
+ sessionClient.toolCallApprovalResult(
385
+ msg.id,
386
+ result,
387
+ false /* auto-approve */
388
+ );
389
+ }
390
+ break;
391
+ case "render_html":
392
+ await NODE_PLATFORM.renderHTML(msg.html);
393
+ break;
394
+ case "authenticate":
395
+ NODE_PLATFORM.openUrl(
396
+ msg.url,
397
+ new Promise<boolean>((r) => {
398
+ r(true);
399
+ }),
400
+ msg.display_name
401
+ );
402
+ break;
403
+
202
404
  default:
203
- stdout.write(`(unrecognised) ${JSON.stringify(msg)}\n`);
405
+ stdout.write(`(unrecognised) ${JSON.stringify(msg.type)}\n`);
204
406
  break;
205
407
  }
206
408
  };
207
- }
208
409
 
209
- async function runClientTest(
210
- host: string,
211
- port: number,
212
- apiKey: string,
213
- session: string
214
- ): Promise<void> {
215
- const client = await ChatClient.init(
216
- host,
217
- port,
218
- apiKey,
219
- getCLIOnMessage(),
220
- async () => {},
221
- NODE_PLATFORM,
222
- session
223
- );
224
-
225
- for (let i = 1; i <= 60; i++) {
226
- await new Promise((r) => setTimeout(r, 1000));
227
-
228
- const message: ClientUserMessage = {
229
- type: "msg",
230
- message: `add ${i} and ${i + 1}`,
231
- };
232
- client.sendMessage(message);
233
- }
234
-
235
- client.close();
410
+ return {
411
+ onMessage,
412
+ onError,
413
+ onClosed: onClosed,
414
+ };
236
415
  }
237
416
 
238
417
  export const chatMain = subcommands({
@@ -240,5 +419,9 @@ export const chatMain = subcommands({
240
419
  cmds: {
241
420
  server,
242
421
  client,
422
+ "clear-sessions": clearSessions,
423
+ "delete-session": deleteSession,
424
+ "list-sessions": listSessions,
425
+ "list-participants": listParticipants,
243
426
  },
244
427
  });
@@ -11,6 +11,8 @@ import { IMcpServerManager } from "../agent/mcpServerManager";
11
11
 
12
12
  import { IPrompt } from "./prompt";
13
13
 
14
+ type OnUnknownCommand = (cmds: string[]) => void;
15
+
14
16
  /**
15
17
  * A prompt parser which can accept commands or messages from an IPrompt.
16
18
  * Commands are sent to an IMcpServerManager, ISkillManager and IConversation.
@@ -28,8 +30,10 @@ export class CommandPrompt {
28
30
 
29
31
  async getNextPrompt(
30
32
  agent: IConversation,
31
- sudoMcpServerManager: ISkillManager
33
+ sudoMcpServerManager: ISkillManager,
34
+ onUnknownCommand?: OnUnknownCommand
32
35
  ): Promise<[string | undefined, string | undefined]> {
36
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
33
37
  while (true) {
34
38
  // Get a line, detecting the EOF signal.
35
39
  const line = await this.prompt.run();
@@ -64,9 +68,14 @@ export class CommandPrompt {
64
68
  }
65
69
 
66
70
  try {
67
- await this.runCommand(agent, sudoMcpServerManager, cmds);
71
+ await this.runCommand(
72
+ agent,
73
+ sudoMcpServerManager,
74
+ cmds,
75
+ onUnknownCommand
76
+ );
68
77
  } catch (e) {
69
- console.log(`ERROR: ${e}`);
78
+ console.log(`ERROR: ${String(e)}`);
70
79
  }
71
80
  }
72
81
  }
@@ -74,7 +83,8 @@ export class CommandPrompt {
74
83
  async runCommand(
75
84
  agent: IConversation,
76
85
  sudoMcpServerManager: ISkillManager,
77
- cmds: string[]
86
+ cmds: string[],
87
+ onUnknownCommand?: OnUnknownCommand
78
88
  ) {
79
89
  switch (cmds[0]) {
80
90
  case "lt":
@@ -117,7 +127,11 @@ export class CommandPrompt {
117
127
  this.helpMenu();
118
128
  break;
119
129
  default:
120
- console.log(`error: Unknown command ${cmds[0]}`);
130
+ if (onUnknownCommand) {
131
+ onUnknownCommand(cmds);
132
+ } else {
133
+ console.log(`error: Unknown command ${cmds[0]}`);
134
+ }
121
135
  }
122
136
  }
123
137
 
@@ -147,7 +161,7 @@ export class CommandPrompt {
147
161
  const tools = server.getTools();
148
162
  const enabled = server.getEnabledTools();
149
163
  for (const tool of tools) {
150
- const isEnabled = enabled[tool.name] ? "*" : " ";
164
+ const isEnabled = enabled.get(tool.name) ? "*" : " ";
151
165
  console.log(` [${isEnabled}] ${tool.name}`);
152
166
  }
153
167
  }
@@ -171,8 +185,8 @@ export class CommandPrompt {
171
185
  await sudoMcpServerManager.addMcpServer(serverName, true);
172
186
  } catch (e) {
173
187
  if (e instanceof InvalidConfiguration) {
174
- throw `${e}. \n
175
- Have you installed server ${serverName} using the SudoMCP CLI tool?`;
188
+ throw new Error(`${e.msg}. \n
189
+ Have you installed server ${serverName} using the SudoMCP CLI tool?`);
176
190
  } else {
177
191
  throw e;
178
192
  }
@@ -201,7 +215,9 @@ Have you installed server ${serverName} using the SudoMCP CLI tool?`;
201
215
  ): Promise<boolean> {
202
216
  displayToolCall(toolCall);
203
217
  const response = await this.prompt.run("Approve tool call? (Y/n) ");
204
- return response === "y" || response === "yes" || response == "";
218
+ return (
219
+ response?.toLowerCase() === "y" || response === "yes" || response == ""
220
+ );
205
221
  }
206
222
  }
207
223
 
package/src/tool/files.ts CHANGED
@@ -23,7 +23,7 @@ export async function loadFileOrStdin(file: string): Promise<string> {
23
23
  process.stdin.setEncoding("utf8");
24
24
 
25
25
  process.stdin.on("data", (chunk) => {
26
- data += chunk;
26
+ data += chunk.toString();
27
27
  });
28
28
 
29
29
  process.stdin.on("end", () => {
@@ -39,26 +39,30 @@ export async function loadFileOrStdin(file: string): Promise<string> {
39
39
  return fs.readFileSync(file, { encoding: "utf8" });
40
40
  }
41
41
 
42
+ export function loadImageB64(file: string): string {
43
+ const ext = path.extname(file).slice(1).toLowerCase();
44
+ const mimeType = {
45
+ jpg: "image/jpeg",
46
+ jpeg: "image/jpeg",
47
+ png: "image/png",
48
+ gif: "image/gif",
49
+ svg: "image/svg+xml",
50
+ webp: "image/webp",
51
+ }[ext];
52
+ if (!mimeType) {
53
+ throw Error(`invalid file extension: ${ext}`);
54
+ }
55
+
56
+ const fileBuffer = fs.readFileSync(file);
57
+ const imgB64 = fileBuffer.toString("base64");
58
+ return `data:${mimeType};base64,${imgB64}`;
59
+ }
60
+
42
61
  export function loadImageB64OrUndefined(
43
62
  file: string | undefined
44
63
  ): string | undefined {
45
64
  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}`;
65
+ return loadImageB64(file);
62
66
  }
63
67
  return undefined;
64
68
  }
@@ -66,7 +70,7 @@ export function loadImageB64OrUndefined(
66
70
  export function loadServerUrls(path: string): McpServerUrls {
67
71
  try {
68
72
  const file = fs.readFileSync(path, "utf-8");
69
- const urls = JSON.parse(file);
73
+ const urls: unknown = JSON.parse(file);
70
74
 
71
75
  // Validate the structure
72
76
  if (typeof urls !== "object" || urls === null) {
@@ -74,7 +78,9 @@ export function loadServerUrls(path: string): McpServerUrls {
74
78
  }
75
79
 
76
80
  // Validate each URL is a string
77
- for (const [key, value] of Object.entries(urls)) {
81
+ for (const [key, value] of Object.entries(
82
+ urls as Record<string, unknown>
83
+ )) {
78
84
  if (typeof value !== "string") {
79
85
  throw new Error(
80
86
  `Invalid URL format for server ${key}: must be a string`