@qodo/sdk 0.1.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 (303) hide show
  1. package/LICENSE +118 -0
  2. package/README.md +121 -0
  3. package/dist/api/agent.d.ts +69 -0
  4. package/dist/api/agent.d.ts.map +1 -0
  5. package/dist/api/agent.js +1034 -0
  6. package/dist/api/agent.js.map +1 -0
  7. package/dist/api/analytics.d.ts +43 -0
  8. package/dist/api/analytics.d.ts.map +1 -0
  9. package/dist/api/analytics.js +163 -0
  10. package/dist/api/analytics.js.map +1 -0
  11. package/dist/api/http.d.ts +5 -0
  12. package/dist/api/http.d.ts.map +1 -0
  13. package/dist/api/http.js +59 -0
  14. package/dist/api/http.js.map +1 -0
  15. package/dist/api/index.d.ts +12 -0
  16. package/dist/api/index.d.ts.map +1 -0
  17. package/dist/api/index.js +17 -0
  18. package/dist/api/index.js.map +1 -0
  19. package/dist/api/taskTracking.d.ts +54 -0
  20. package/dist/api/taskTracking.d.ts.map +1 -0
  21. package/dist/api/taskTracking.js +208 -0
  22. package/dist/api/taskTracking.js.map +1 -0
  23. package/dist/api/types.d.ts +92 -0
  24. package/dist/api/types.d.ts.map +1 -0
  25. package/dist/api/types.js +2 -0
  26. package/dist/api/types.js.map +1 -0
  27. package/dist/api/utils.d.ts +8 -0
  28. package/dist/api/utils.d.ts.map +1 -0
  29. package/dist/api/utils.js +54 -0
  30. package/dist/api/utils.js.map +1 -0
  31. package/dist/api/websocket.d.ts +74 -0
  32. package/dist/api/websocket.d.ts.map +1 -0
  33. package/dist/api/websocket.js +685 -0
  34. package/dist/api/websocket.js.map +1 -0
  35. package/dist/auth/index.d.ts +25 -0
  36. package/dist/auth/index.d.ts.map +1 -0
  37. package/dist/auth/index.js +85 -0
  38. package/dist/auth/index.js.map +1 -0
  39. package/dist/clients/index.d.ts +8 -0
  40. package/dist/clients/index.d.ts.map +1 -0
  41. package/dist/clients/index.js +7 -0
  42. package/dist/clients/index.js.map +1 -0
  43. package/dist/clients/info/InfoClient.d.ts +37 -0
  44. package/dist/clients/info/InfoClient.d.ts.map +1 -0
  45. package/dist/clients/info/InfoClient.js +69 -0
  46. package/dist/clients/info/InfoClient.js.map +1 -0
  47. package/dist/clients/info/index.d.ts +4 -0
  48. package/dist/clients/info/index.d.ts.map +1 -0
  49. package/dist/clients/info/index.js +2 -0
  50. package/dist/clients/info/index.js.map +1 -0
  51. package/dist/clients/info/types.d.ts +21 -0
  52. package/dist/clients/info/types.d.ts.map +1 -0
  53. package/dist/clients/info/types.js +2 -0
  54. package/dist/clients/info/types.js.map +1 -0
  55. package/dist/clients/sessions/SessionsClient.d.ts +34 -0
  56. package/dist/clients/sessions/SessionsClient.d.ts.map +1 -0
  57. package/dist/clients/sessions/SessionsClient.js +71 -0
  58. package/dist/clients/sessions/SessionsClient.js.map +1 -0
  59. package/dist/clients/sessions/index.d.ts +4 -0
  60. package/dist/clients/sessions/index.d.ts.map +1 -0
  61. package/dist/clients/sessions/index.js +2 -0
  62. package/dist/clients/sessions/index.js.map +1 -0
  63. package/dist/clients/sessions/types.d.ts +20 -0
  64. package/dist/clients/sessions/types.d.ts.map +1 -0
  65. package/dist/clients/sessions/types.js +2 -0
  66. package/dist/clients/sessions/types.js.map +1 -0
  67. package/dist/config/ConfigManager.d.ts +43 -0
  68. package/dist/config/ConfigManager.d.ts.map +1 -0
  69. package/dist/config/ConfigManager.js +472 -0
  70. package/dist/config/ConfigManager.js.map +1 -0
  71. package/dist/config/index.d.ts +6 -0
  72. package/dist/config/index.d.ts.map +1 -0
  73. package/dist/config/index.js +7 -0
  74. package/dist/config/index.js.map +1 -0
  75. package/dist/config/urlConfig.d.ts +15 -0
  76. package/dist/config/urlConfig.d.ts.map +1 -0
  77. package/dist/config/urlConfig.js +75 -0
  78. package/dist/config/urlConfig.js.map +1 -0
  79. package/dist/constants/errors.d.ts +2 -0
  80. package/dist/constants/errors.d.ts.map +1 -0
  81. package/dist/constants/errors.js +2 -0
  82. package/dist/constants/errors.js.map +1 -0
  83. package/dist/constants/index.d.ts +7 -0
  84. package/dist/constants/index.d.ts.map +1 -0
  85. package/dist/constants/index.js +11 -0
  86. package/dist/constants/index.js.map +1 -0
  87. package/dist/constants/tools.d.ts +4 -0
  88. package/dist/constants/tools.d.ts.map +1 -0
  89. package/dist/constants/tools.js +4 -0
  90. package/dist/constants/tools.js.map +1 -0
  91. package/dist/constants/versions.d.ts +2 -0
  92. package/dist/constants/versions.d.ts.map +1 -0
  93. package/dist/constants/versions.js +2 -0
  94. package/dist/constants/versions.js.map +1 -0
  95. package/dist/context/buildUserContext.d.ts +18 -0
  96. package/dist/context/buildUserContext.d.ts.map +1 -0
  97. package/dist/context/buildUserContext.js +34 -0
  98. package/dist/context/buildUserContext.js.map +1 -0
  99. package/dist/context/index.d.ts +9 -0
  100. package/dist/context/index.d.ts.map +1 -0
  101. package/dist/context/index.js +9 -0
  102. package/dist/context/index.js.map +1 -0
  103. package/dist/context/messageManager.d.ts +42 -0
  104. package/dist/context/messageManager.d.ts.map +1 -0
  105. package/dist/context/messageManager.js +322 -0
  106. package/dist/context/messageManager.js.map +1 -0
  107. package/dist/context/taskFocus.d.ts +2 -0
  108. package/dist/context/taskFocus.d.ts.map +1 -0
  109. package/dist/context/taskFocus.js +26 -0
  110. package/dist/context/taskFocus.js.map +1 -0
  111. package/dist/context/userInput.d.ts +3 -0
  112. package/dist/context/userInput.d.ts.map +1 -0
  113. package/dist/context/userInput.js +20 -0
  114. package/dist/context/userInput.js.map +1 -0
  115. package/dist/index.d.ts +18 -0
  116. package/dist/index.d.ts.map +1 -0
  117. package/dist/index.js +21 -0
  118. package/dist/index.js.map +1 -0
  119. package/dist/mcp/MCPManager.d.ts +125 -0
  120. package/dist/mcp/MCPManager.d.ts.map +1 -0
  121. package/dist/mcp/MCPManager.js +616 -0
  122. package/dist/mcp/MCPManager.js.map +1 -0
  123. package/dist/mcp/approvedTools.d.ts +4 -0
  124. package/dist/mcp/approvedTools.d.ts.map +1 -0
  125. package/dist/mcp/approvedTools.js +19 -0
  126. package/dist/mcp/approvedTools.js.map +1 -0
  127. package/dist/mcp/baseServer.d.ts +75 -0
  128. package/dist/mcp/baseServer.d.ts.map +1 -0
  129. package/dist/mcp/baseServer.js +107 -0
  130. package/dist/mcp/baseServer.js.map +1 -0
  131. package/dist/mcp/builtinServers.d.ts +15 -0
  132. package/dist/mcp/builtinServers.d.ts.map +1 -0
  133. package/dist/mcp/builtinServers.js +155 -0
  134. package/dist/mcp/builtinServers.js.map +1 -0
  135. package/dist/mcp/dynamicBEServer.d.ts +20 -0
  136. package/dist/mcp/dynamicBEServer.d.ts.map +1 -0
  137. package/dist/mcp/dynamicBEServer.js +52 -0
  138. package/dist/mcp/dynamicBEServer.js.map +1 -0
  139. package/dist/mcp/index.d.ts +19 -0
  140. package/dist/mcp/index.d.ts.map +1 -0
  141. package/dist/mcp/index.js +24 -0
  142. package/dist/mcp/index.js.map +1 -0
  143. package/dist/mcp/mcpInitialization.d.ts +2 -0
  144. package/dist/mcp/mcpInitialization.d.ts.map +1 -0
  145. package/dist/mcp/mcpInitialization.js +56 -0
  146. package/dist/mcp/mcpInitialization.js.map +1 -0
  147. package/dist/mcp/servers/filesystem.d.ts +75 -0
  148. package/dist/mcp/servers/filesystem.d.ts.map +1 -0
  149. package/dist/mcp/servers/filesystem.js +992 -0
  150. package/dist/mcp/servers/filesystem.js.map +1 -0
  151. package/dist/mcp/servers/gerrit.d.ts +19 -0
  152. package/dist/mcp/servers/gerrit.d.ts.map +1 -0
  153. package/dist/mcp/servers/gerrit.js +515 -0
  154. package/dist/mcp/servers/gerrit.js.map +1 -0
  155. package/dist/mcp/servers/git.d.ts +18 -0
  156. package/dist/mcp/servers/git.d.ts.map +1 -0
  157. package/dist/mcp/servers/git.js +441 -0
  158. package/dist/mcp/servers/git.js.map +1 -0
  159. package/dist/mcp/servers/ripgrep.d.ts +34 -0
  160. package/dist/mcp/servers/ripgrep.d.ts.map +1 -0
  161. package/dist/mcp/servers/ripgrep.js +517 -0
  162. package/dist/mcp/servers/ripgrep.js.map +1 -0
  163. package/dist/mcp/servers/shell.d.ts +20 -0
  164. package/dist/mcp/servers/shell.d.ts.map +1 -0
  165. package/dist/mcp/servers/shell.js +603 -0
  166. package/dist/mcp/servers/shell.js.map +1 -0
  167. package/dist/mcp/serversRegistry.d.ts +55 -0
  168. package/dist/mcp/serversRegistry.d.ts.map +1 -0
  169. package/dist/mcp/serversRegistry.js +410 -0
  170. package/dist/mcp/serversRegistry.js.map +1 -0
  171. package/dist/mcp/toolProcessor.d.ts +42 -0
  172. package/dist/mcp/toolProcessor.d.ts.map +1 -0
  173. package/dist/mcp/toolProcessor.js +200 -0
  174. package/dist/mcp/toolProcessor.js.map +1 -0
  175. package/dist/mcp/types.d.ts +29 -0
  176. package/dist/mcp/types.d.ts.map +1 -0
  177. package/dist/mcp/types.js +2 -0
  178. package/dist/mcp/types.js.map +1 -0
  179. package/dist/parser/index.d.ts +72 -0
  180. package/dist/parser/index.d.ts.map +1 -0
  181. package/dist/parser/index.js +967 -0
  182. package/dist/parser/index.js.map +1 -0
  183. package/dist/parser/types.d.ts +153 -0
  184. package/dist/parser/types.d.ts.map +1 -0
  185. package/dist/parser/types.js +6 -0
  186. package/dist/parser/types.js.map +1 -0
  187. package/dist/parser/utils.d.ts +18 -0
  188. package/dist/parser/utils.d.ts.map +1 -0
  189. package/dist/parser/utils.js +64 -0
  190. package/dist/parser/utils.js.map +1 -0
  191. package/dist/sdk/QodoSDK.d.ts +152 -0
  192. package/dist/sdk/QodoSDK.d.ts.map +1 -0
  193. package/dist/sdk/QodoSDK.js +786 -0
  194. package/dist/sdk/QodoSDK.js.map +1 -0
  195. package/dist/sdk/bootstrap.d.ts +16 -0
  196. package/dist/sdk/bootstrap.d.ts.map +1 -0
  197. package/dist/sdk/bootstrap.js +21 -0
  198. package/dist/sdk/bootstrap.js.map +1 -0
  199. package/dist/sdk/builders.d.ts +54 -0
  200. package/dist/sdk/builders.d.ts.map +1 -0
  201. package/dist/sdk/builders.js +117 -0
  202. package/dist/sdk/builders.js.map +1 -0
  203. package/dist/sdk/defaults.d.ts +11 -0
  204. package/dist/sdk/defaults.d.ts.map +1 -0
  205. package/dist/sdk/defaults.js +39 -0
  206. package/dist/sdk/defaults.js.map +1 -0
  207. package/dist/sdk/discovery.d.ts +2 -0
  208. package/dist/sdk/discovery.d.ts.map +1 -0
  209. package/dist/sdk/discovery.js +25 -0
  210. package/dist/sdk/discovery.js.map +1 -0
  211. package/dist/sdk/events.d.ts +168 -0
  212. package/dist/sdk/events.d.ts.map +1 -0
  213. package/dist/sdk/events.js +52 -0
  214. package/dist/sdk/events.js.map +1 -0
  215. package/dist/sdk/index.d.ts +17 -0
  216. package/dist/sdk/index.d.ts.map +1 -0
  217. package/dist/sdk/index.js +17 -0
  218. package/dist/sdk/index.js.map +1 -0
  219. package/dist/sdk/runner/AgentRunner.d.ts +22 -0
  220. package/dist/sdk/runner/AgentRunner.d.ts.map +1 -0
  221. package/dist/sdk/runner/AgentRunner.js +222 -0
  222. package/dist/sdk/runner/AgentRunner.js.map +1 -0
  223. package/dist/sdk/runner/finalize.d.ts +9 -0
  224. package/dist/sdk/runner/finalize.d.ts.map +1 -0
  225. package/dist/sdk/runner/finalize.js +115 -0
  226. package/dist/sdk/runner/finalize.js.map +1 -0
  227. package/dist/sdk/runner/formats.d.ts +7 -0
  228. package/dist/sdk/runner/formats.d.ts.map +1 -0
  229. package/dist/sdk/runner/formats.js +91 -0
  230. package/dist/sdk/runner/formats.js.map +1 -0
  231. package/dist/sdk/runner/index.d.ts +9 -0
  232. package/dist/sdk/runner/index.d.ts.map +1 -0
  233. package/dist/sdk/runner/index.js +9 -0
  234. package/dist/sdk/runner/index.js.map +1 -0
  235. package/dist/sdk/runner/progress.d.ts +3 -0
  236. package/dist/sdk/runner/progress.d.ts.map +1 -0
  237. package/dist/sdk/runner/progress.js +16 -0
  238. package/dist/sdk/runner/progress.js.map +1 -0
  239. package/dist/sdk/schemas.d.ts +50 -0
  240. package/dist/sdk/schemas.d.ts.map +1 -0
  241. package/dist/sdk/schemas.js +145 -0
  242. package/dist/sdk/schemas.js.map +1 -0
  243. package/dist/session/SessionContext.d.ts +86 -0
  244. package/dist/session/SessionContext.d.ts.map +1 -0
  245. package/dist/session/SessionContext.js +395 -0
  246. package/dist/session/SessionContext.js.map +1 -0
  247. package/dist/session/environment.d.ts +42 -0
  248. package/dist/session/environment.d.ts.map +1 -0
  249. package/dist/session/environment.js +27 -0
  250. package/dist/session/environment.js.map +1 -0
  251. package/dist/session/history.d.ts +3 -0
  252. package/dist/session/history.d.ts.map +1 -0
  253. package/dist/session/history.js +67 -0
  254. package/dist/session/history.js.map +1 -0
  255. package/dist/session/index.d.ts +10 -0
  256. package/dist/session/index.d.ts.map +1 -0
  257. package/dist/session/index.js +9 -0
  258. package/dist/session/index.js.map +1 -0
  259. package/dist/session/serverData.d.ts +38 -0
  260. package/dist/session/serverData.d.ts.map +1 -0
  261. package/dist/session/serverData.js +241 -0
  262. package/dist/session/serverData.js.map +1 -0
  263. package/dist/tracking/Tracker.d.ts +55 -0
  264. package/dist/tracking/Tracker.d.ts.map +1 -0
  265. package/dist/tracking/Tracker.js +217 -0
  266. package/dist/tracking/Tracker.js.map +1 -0
  267. package/dist/tracking/index.d.ts +8 -0
  268. package/dist/tracking/index.d.ts.map +1 -0
  269. package/dist/tracking/index.js +8 -0
  270. package/dist/tracking/index.js.map +1 -0
  271. package/dist/tracking/schemas.d.ts +292 -0
  272. package/dist/tracking/schemas.d.ts.map +1 -0
  273. package/dist/tracking/schemas.js +91 -0
  274. package/dist/tracking/schemas.js.map +1 -0
  275. package/dist/types.d.ts +4 -0
  276. package/dist/types.d.ts.map +1 -0
  277. package/dist/types.js +2 -0
  278. package/dist/types.js.map +1 -0
  279. package/dist/utils/extractSetFlags.d.ts +6 -0
  280. package/dist/utils/extractSetFlags.d.ts.map +1 -0
  281. package/dist/utils/extractSetFlags.js +16 -0
  282. package/dist/utils/extractSetFlags.js.map +1 -0
  283. package/dist/utils/formatTimeAgo.d.ts +2 -0
  284. package/dist/utils/formatTimeAgo.d.ts.map +1 -0
  285. package/dist/utils/formatTimeAgo.js +20 -0
  286. package/dist/utils/formatTimeAgo.js.map +1 -0
  287. package/dist/utils/index.d.ts +12 -0
  288. package/dist/utils/index.d.ts.map +1 -0
  289. package/dist/utils/index.js +12 -0
  290. package/dist/utils/index.js.map +1 -0
  291. package/dist/utils/machineId.d.ts +14 -0
  292. package/dist/utils/machineId.d.ts.map +1 -0
  293. package/dist/utils/machineId.js +66 -0
  294. package/dist/utils/machineId.js.map +1 -0
  295. package/dist/utils/pathUtils.d.ts +22 -0
  296. package/dist/utils/pathUtils.d.ts.map +1 -0
  297. package/dist/utils/pathUtils.js +54 -0
  298. package/dist/utils/pathUtils.js.map +1 -0
  299. package/dist/version.d.ts +2 -0
  300. package/dist/version.d.ts.map +1 -0
  301. package/dist/version.js +23 -0
  302. package/dist/version.js.map +1 -0
  303. package/package.json +93 -0
@@ -0,0 +1,967 @@
1
+ import * as toml from 'toml';
2
+ import * as yaml from 'yaml';
3
+ import jmespath from "jmespath";
4
+ import { MCPManager } from "../mcp/index.js";
5
+ import path from "path";
6
+ import { transformOutputSchema } from "./utils.js";
7
+ import { LATEST_CONFIG_VERSION } from "../constants/versions.js";
8
+ import { isUrl, loadConfigContent } from '../config/index.js';
9
+ /**
10
+ * Detects the configuration file format based on file extension
11
+ * @param filePath Path to the configuration file
12
+ * @returns 'toml' or 'yaml'
13
+ */
14
+ function detectConfigFormat(filePath) {
15
+ const ext = path.extname(filePath).toLowerCase();
16
+ if (ext === '.yaml' || ext === '.yml') {
17
+ return 'yaml';
18
+ }
19
+ return 'toml'; // Default to TOML for backward compatibility
20
+ }
21
+ /**
22
+ * Parses a configuration string based on format
23
+ * @returns Parsed configuration object
24
+ * @param value
25
+ */
26
+ function normalizeMcpServersValue(value) {
27
+ if (value == null)
28
+ return undefined;
29
+ const unwrap = (obj) => {
30
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj))
31
+ return undefined;
32
+ // Backward compatibility: allow embedding under { "mcpServers": { ... } }
33
+ if (Object.prototype.hasOwnProperty.call(obj, 'mcpServers')) {
34
+ const inner = obj.mcpServers;
35
+ if (inner && typeof inner === 'object' && !Array.isArray(inner)) {
36
+ return inner;
37
+ }
38
+ }
39
+ return obj;
40
+ };
41
+ if (typeof value === 'string') {
42
+ try {
43
+ const parsed = JSON.parse(value);
44
+ return unwrap(parsed);
45
+ }
46
+ catch (e) {
47
+ throw new Error(`Failed to parse mcpServers JSON: ${e?.message || String(e)}`);
48
+ }
49
+ }
50
+ return unwrap(value);
51
+ }
52
+ /**
53
+ * Normalize a config object the same way as file/string parsing.
54
+ *
55
+ * Important:
56
+ * - Does not mutate the user-provided object (clones first).
57
+ * - Supports aliases and legacy formats for smooth SDK embedding.
58
+ */
59
+ export function normalizeConfigObject(rawConfig) {
60
+ // SDK runtime metadata (Zod schemas, etc.) is stored under `command.__sdk`.
61
+ // That metadata is not necessarily cloneable/serializable.
62
+ //
63
+ // We intentionally preserve it *by reference* while still cloning the user config
64
+ // object, so we don't mutate the input but also don't lose SDK-only metadata.
65
+ const sdkCommandMeta = {};
66
+ try {
67
+ const cmds = rawConfig?.commands;
68
+ if (cmds && typeof cmds === 'object') {
69
+ for (const [name, cmd] of Object.entries(cmds)) {
70
+ const meta = cmd?.__sdk;
71
+ if (meta)
72
+ sdkCommandMeta[name] = meta;
73
+ }
74
+ }
75
+ }
76
+ catch { }
77
+ const clone = (obj) => {
78
+ if (obj == null)
79
+ return obj;
80
+ try {
81
+ // Node 18+ supports structuredClone; use it when available.
82
+ // eslint-disable-next-line no-undef
83
+ if (typeof structuredClone === 'function') {
84
+ // eslint-disable-next-line no-undef
85
+ return structuredClone(obj);
86
+ }
87
+ }
88
+ catch { }
89
+ // Fallback: JSON clone (configs are expected to be plain data objects).
90
+ return JSON.parse(JSON.stringify(obj));
91
+ };
92
+ const config = clone(rawConfig || {});
93
+ // Restore preserved SDK-only metadata on the cloned config.
94
+ try {
95
+ if (config?.commands && typeof config.commands === 'object') {
96
+ for (const [name, meta] of Object.entries(sdkCommandMeta)) {
97
+ if (config.commands[name]) {
98
+ config.commands[name].__sdk = meta;
99
+ }
100
+ }
101
+ }
102
+ }
103
+ catch { }
104
+ // Normalize top-level mcpServers if provided (stringified JSON or record)
105
+ if (Object.prototype.hasOwnProperty.call(config, 'mcpServers')) {
106
+ config.mcpServers = normalizeMcpServersValue(config.mcpServers);
107
+ }
108
+ // Normalize command-level mcpServers if provided
109
+ if (config.commands) {
110
+ Object.keys(config.commands).forEach((commandName) => {
111
+ const command = config.commands[commandName];
112
+ if (command && Object.prototype.hasOwnProperty.call(command, 'mcpServers')) {
113
+ command.mcpServers = normalizeMcpServersValue(command.mcpServers);
114
+ }
115
+ });
116
+ }
117
+ // Normalize mode-level mcpServers if provided
118
+ if (config.modes) {
119
+ Object.keys(config.modes).forEach((modeName) => {
120
+ const mode = config.modes[modeName];
121
+ if (mode && Object.prototype.hasOwnProperty.call(mode, 'mcpServers')) {
122
+ mode.mcpServers = normalizeMcpServersValue(mode.mcpServers);
123
+ }
124
+ });
125
+ }
126
+ // Handle "tools" as synonym for "available_tools" at top level
127
+ if (Object.prototype.hasOwnProperty.call(config, 'tools') && !config.available_tools) {
128
+ config.available_tools = config.tools;
129
+ // If tools is explicitly an empty array, disable MCP entirely for this config
130
+ if (Array.isArray(config.tools) && config.tools.length === 0) {
131
+ config.disable_mcp = true;
132
+ }
133
+ delete config.tools;
134
+ }
135
+ // Handle "tools" as synonym for "available_tools" in commands
136
+ if (config.commands) {
137
+ Object.keys(config.commands).forEach((commandName) => {
138
+ const command = config.commands[commandName];
139
+ if (command && Object.prototype.hasOwnProperty.call(command, 'tools') && !command.available_tools) {
140
+ command.available_tools = command.tools;
141
+ // If tools is explicitly an empty array at command level, disable MCP for that command
142
+ if (Array.isArray(command.tools) && command.tools.length === 0) {
143
+ command.disable_mcp = true;
144
+ }
145
+ delete command.tools;
146
+ }
147
+ });
148
+ }
149
+ // Handle "tools" as synonym for "available_tools" in modes
150
+ if (config.modes) {
151
+ Object.keys(config.modes).forEach((modeName) => {
152
+ const mode = config.modes[modeName];
153
+ if (mode && Object.prototype.hasOwnProperty.call(mode, 'tools') && !mode.available_tools) {
154
+ mode.available_tools = mode.tools;
155
+ // If tools is explicitly an empty array at mode level, disable MCP for that mode
156
+ if (Array.isArray(mode.tools) && mode.tools.length === 0) {
157
+ mode.disable_mcp = true;
158
+ }
159
+ delete mode.tools;
160
+ }
161
+ });
162
+ }
163
+ // Handle "no_tools" as synonym for "ignore_tools" at top level
164
+ if (config.no_tools && !config.ignore_tools) {
165
+ config.ignore_tools = config.no_tools;
166
+ delete config.no_tools;
167
+ }
168
+ // Handle "no_tools" as synonym for "ignore_tools" in commands
169
+ if (config.commands) {
170
+ Object.keys(config.commands).forEach((commandName) => {
171
+ const command = config.commands[commandName];
172
+ if (command?.no_tools && !command.ignore_tools) {
173
+ command.ignore_tools = command.no_tools;
174
+ delete command.no_tools;
175
+ }
176
+ });
177
+ }
178
+ // Handle "no_tools" as synonym for "ignore_tools" in modes
179
+ if (config.modes) {
180
+ Object.keys(config.modes).forEach((modeName) => {
181
+ const mode = config.modes[modeName];
182
+ if (mode?.no_tools && !mode.ignore_tools) {
183
+ mode.ignore_tools = mode.no_tools;
184
+ delete mode.no_tools;
185
+ }
186
+ });
187
+ }
188
+ // Default missing version for SDK-friendly usage.
189
+ // The validator will still warn when version is missing (checkConfigVersion).
190
+ if (!config.version) {
191
+ config.version = LATEST_CONFIG_VERSION;
192
+ }
193
+ return config;
194
+ }
195
+ function parseConfigByFormat(configString, format) {
196
+ try {
197
+ let parsed;
198
+ if (format === 'yaml') {
199
+ parsed = yaml.parse(configString);
200
+ }
201
+ else {
202
+ parsed = toml.parse(configString);
203
+ }
204
+ return normalizeConfigObject(parsed);
205
+ }
206
+ catch (error) {
207
+ throw new Error(`Failed to parse ${format.toUpperCase()} configuration: ${error.message}`);
208
+ }
209
+ }
210
+ /**
211
+ * Loads and parses a configuration file (TOML or YAML) with support for imports
212
+ *
213
+ * @param pathOrUrl Path to the TOML configuration file
214
+ * @returns Parsed configuration object
215
+ */
216
+ export async function loadConfigFromFile(pathOrUrl) {
217
+ try {
218
+ const fileContent = await loadConfigContent(pathOrUrl);
219
+ const configDir = isUrl(pathOrUrl) ? process.cwd() : path.dirname(pathOrUrl);
220
+ return await parseConfigStringWithImports(fileContent, configDir, pathOrUrl);
221
+ }
222
+ catch (error) {
223
+ throw new Error(`Failed to load configuration: ${error.message}`);
224
+ }
225
+ }
226
+ /**
227
+ * Loads and parses a configuration file (TOML or YAML) with support for imports
228
+ *
229
+ * @param fileContent Content of the TOML configuration file as a string
230
+ * @returns Parsed configuration object
231
+ */
232
+ export async function loadConfigFromString(fileContent) {
233
+ try {
234
+ const configDir = process.cwd();
235
+ return await parseConfigStringWithImports(fileContent, configDir);
236
+ }
237
+ catch (error) {
238
+ throw new Error(`Failed to load configuration: ${error.message}`);
239
+ }
240
+ }
241
+ /**
242
+ * Parses a version string in x.y format
243
+ */
244
+ function parseVersion(version) {
245
+ const versionStr = typeof version === 'number'
246
+ ? (Number.isInteger(version) ? `${version}.0` : version.toString())
247
+ : version;
248
+ const versionRegex = /^(\d+)\.(\d+)$/;
249
+ const match = RegExp(versionRegex).exec(versionStr);
250
+ if (!match) {
251
+ return null;
252
+ }
253
+ return {
254
+ major: parseInt(match[1], 10),
255
+ minor: parseInt(match[2], 10)
256
+ };
257
+ }
258
+ /**
259
+ * Compares two version objects
260
+ * Returns: -1 if v1 < v2, 0 if v1 === v2, 1 if v1 > v2
261
+ */
262
+ function compareVersions(v1, v2) {
263
+ if (v1.major !== v2.major) {
264
+ return v1.major < v2.major ? -1 : 1;
265
+ }
266
+ if (v1.minor !== v2.minor) {
267
+ return v1.minor < v2.minor ? -1 : 1;
268
+ }
269
+ return 0;
270
+ }
271
+ /**
272
+ * Resolves imports in a configuration string (TOML or YAML)
273
+ *
274
+ * @param configString Configuration string (TOML or YAML)
275
+ * @param baseDir Base directory for resolving relative import paths
276
+ * @param sourceFile Optional source file path for tracking
277
+ * @returns Parsed configuration object with resolved imports
278
+ */
279
+ export async function parseConfigStringWithImports(configString, baseDir = process.cwd(), sourceFile) {
280
+ try {
281
+ // Detect format and parse the base configuration
282
+ const format = sourceFile ? detectConfigFormat(sourceFile) : 'toml';
283
+ const baseConfig = parseConfigByFormat(configString, format);
284
+ // Track source file for this config
285
+ if (sourceFile) {
286
+ baseConfig._sourceFile = sourceFile;
287
+ }
288
+ // Check for import directive
289
+ if (baseConfig.imports && Array.isArray(baseConfig.imports)) {
290
+ const importPromises = baseConfig.imports.map(async (importPath) => {
291
+ try {
292
+ let fullPath;
293
+ let importBaseDir;
294
+ if (isUrl(importPath)) {
295
+ // For URL imports, use current directory as base for nested imports
296
+ fullPath = importPath;
297
+ importBaseDir = process.cwd();
298
+ }
299
+ else {
300
+ // For local imports, resolve relative to the current config's directory
301
+ fullPath = path.resolve(baseDir, importPath);
302
+ importBaseDir = path.dirname(fullPath);
303
+ }
304
+ // Load the content
305
+ const importContent = await loadConfigContent(fullPath);
306
+ // Recursively resolve nested imports
307
+ return await parseConfigStringWithImports(importContent, importBaseDir, fullPath);
308
+ }
309
+ catch (importError) {
310
+ console.warn(`⚠️ Warning: Failed to import ${importPath}: ${importError.message}`);
311
+ // Return null to indicate this import failed but should not crash the process
312
+ return null;
313
+ }
314
+ });
315
+ // Wait for all imports to be resolved
316
+ const importResults = await Promise.all(importPromises);
317
+ // Filter out failed imports (null values)
318
+ const importedConfigs = importResults.filter((config) => config !== null);
319
+ // Deep merge all successfully imported configurations with the base config
320
+ const mergedConfig = deepMergeConfigs([...importedConfigs, baseConfig]);
321
+ // Remove the imports property from the final config
322
+ if ('imports' in mergedConfig) {
323
+ delete mergedConfig.imports;
324
+ }
325
+ return mergedConfig;
326
+ }
327
+ return baseConfig;
328
+ }
329
+ catch (error) {
330
+ throw new Error(`Failed to parse configuration: ${error.message}`);
331
+ }
332
+ }
333
+ /**
334
+ * Parses a TOML configuration string (without imports)
335
+ *
336
+ * @param configString TOML configuration string
337
+ * @returns Parsed configuration object
338
+ */
339
+ export function parseConfigString(configString) {
340
+ try {
341
+ return toml.parse(configString);
342
+ }
343
+ catch (error) {
344
+ throw new Error(`Failed to parse TOML configuration: ${error.message}`);
345
+ }
346
+ }
347
+ /**
348
+ * Deep merges multiple configuration objects
349
+ * Later configs in the array take precedence over earlier ones
350
+ *
351
+ * @param configs Array of configuration objects to merge
352
+ * @returns Merged configuration
353
+ */
354
+ function deepMergeConfigs(configs) {
355
+ if (configs.length === 0) {
356
+ throw new Error('No configurations to merge');
357
+ }
358
+ const merged = {
359
+ version: configs[configs.length - 1].version || LATEST_CONFIG_VERSION,
360
+ commands: {},
361
+ modes: {},
362
+ mcpServers: {},
363
+ available_tools: [],
364
+ ignore_tools: [],
365
+ instructions: undefined,
366
+ disable_mcp: undefined,
367
+ };
368
+ // Track command names and their source files for duplicate detection
369
+ const commandSources = new Map();
370
+ // Track mode names and their source files for duplicate detection
371
+ const modeSources = new Map();
372
+ // Process commands from all configurations
373
+ configs.forEach(config => {
374
+ // load mcpServers (supports stringified JSON, object, and legacy wrapper { mcpServers: { ... } })
375
+ let configMCPServers = {};
376
+ if (config.mcpServers) {
377
+ if (typeof config.mcpServers === 'string') {
378
+ // parseConfigByFormat should already normalize this, but keep for backward-compatibility
379
+ try {
380
+ const parsed = JSON.parse(config.mcpServers);
381
+ configMCPServers = (parsed?.mcpServers ?? parsed);
382
+ }
383
+ catch {
384
+ configMCPServers = {};
385
+ }
386
+ }
387
+ else if (typeof config.mcpServers === 'object') {
388
+ const obj = config.mcpServers;
389
+ configMCPServers = (obj?.mcpServers ?? obj);
390
+ }
391
+ }
392
+ if (config.system_prompt) {
393
+ merged.system_prompt = config.system_prompt;
394
+ }
395
+ if (config.instructions) {
396
+ merged.instructions = config.instructions;
397
+ }
398
+ if (config.model) {
399
+ merged.model = config.model;
400
+ }
401
+ if (config.available_tools) {
402
+ merged.available_tools = config.available_tools;
403
+ }
404
+ if (config.ignore_tools) {
405
+ merged.ignore_tools = config.ignore_tools;
406
+ }
407
+ // If any imported config explicitly disabled MCP, preserve that on the merged config
408
+ if (config.disable_mcp === true) {
409
+ merged.disable_mcp = true;
410
+ }
411
+ if (config.output_schema) {
412
+ merged.output_schema = typeof config.output_schema === 'string' ? transformOutputSchema(config.output_schema, "QodoAgent") : config.output_schema;
413
+ }
414
+ if (config.execution_strategy) {
415
+ merged.execution_strategy = config.execution_strategy;
416
+ }
417
+ merged.mcpServers = { ...merged.mcpServers, ...configMCPServers };
418
+ merged.commands = { ...(merged.commands || {}), ...(config.commands || {}) };
419
+ // Track command names and detect duplicates
420
+ Object.entries(config.commands || {}).forEach(([commandName, command]) => {
421
+ const sourceFile = config._sourceFile || 'main config';
422
+ if (commandSources.has(commandName)) {
423
+ const sources = commandSources.get(commandName);
424
+ sources.push(sourceFile);
425
+ // Get unique file names for the warning (remove duplicates)
426
+ const uniqueFiles = [...new Set(sources)];
427
+ const fileNames = uniqueFiles.map(file => file === 'main config' ? 'main config' : path.basename(file));
428
+ console.warn(`⚠️ Warning: Duplicate command name '${commandName}' found in multiple files: ${fileNames.join(', ')}. Using command from ${path.basename(sourceFile)}.`);
429
+ }
430
+ else {
431
+ commandSources.set(commandName, [sourceFile]);
432
+ }
433
+ // mcpServers are normalized in parseConfigByFormat (stringified JSON and legacy wrappers).
434
+ // Preserve them here without overwriting.
435
+ if (!merged.commands)
436
+ merged.commands = {};
437
+ merged.commands[commandName] = command;
438
+ });
439
+ // Merge modes
440
+ if (config.modes) {
441
+ Object.entries(config.modes).forEach(([modeName, mode]) => {
442
+ const sourceFile = config._sourceFile || 'main config';
443
+ if (modeSources.has(modeName)) {
444
+ const sources = modeSources.get(modeName);
445
+ sources.push(sourceFile);
446
+ const uniqueFiles = [...new Set(sources)];
447
+ const fileNames = uniqueFiles.map(file => file === 'main config' ? 'main config' : path.basename(file));
448
+ console.warn(`⚠️ Warning: Duplicate mode name '${modeName}' found in multiple files: ${fileNames.join(', ')}. Using mode from ${path.basename(sourceFile)}.`);
449
+ }
450
+ else {
451
+ modeSources.set(modeName, [sourceFile]);
452
+ }
453
+ // Don't parse/transform here; keep raw and handle later in ConfigManager like commands
454
+ merged.modes[modeName] = mode;
455
+ });
456
+ }
457
+ });
458
+ return merged;
459
+ }
460
+ /**
461
+ * Validates a configuration object against the expected schema
462
+ *
463
+ * @param config Configuration object to validate
464
+ * @param userCommand
465
+ * @returns Validation result
466
+ */
467
+ export function validateConfig(config, userCommand) {
468
+ const errors = [];
469
+ // Check version
470
+ const versionCheck = checkConfigVersion(config.version);
471
+ if (!versionCheck.isLatest && versionCheck.message) {
472
+ console.warn(versionCheck.message);
473
+ }
474
+ if (config.version) {
475
+ const parsedVersion = parseVersion(config.version);
476
+ if (!parsedVersion) {
477
+ errors.push({
478
+ path: 'version',
479
+ message: `Invalid version format "${config.version}". Expected format is x.y (e.g., "1.0", "2.1").`
480
+ });
481
+ }
482
+ else {
483
+ const parsedLatest = parseVersion(LATEST_CONFIG_VERSION);
484
+ if (parsedLatest && compareVersions(parsedVersion, parsedLatest) > 0) {
485
+ errors.push({
486
+ path: 'version',
487
+ message: `Config version ${config.version} is newer than the latest supported version ${LATEST_CONFIG_VERSION}. Please update your CLI or downgrade your configuration.`
488
+ });
489
+ }
490
+ }
491
+ }
492
+ // Validate top-level exit_expression if present
493
+ if (config.exit_expression) {
494
+ if (!config.output_schema) {
495
+ errors.push({
496
+ path: 'exit_expression',
497
+ message: 'Exit expression requires an output schema to be defined'
498
+ });
499
+ }
500
+ else {
501
+ try {
502
+ //@ts-ignore
503
+ jmespath.compile(config.exit_expression);
504
+ }
505
+ catch (e) {
506
+ errors.push({
507
+ path: 'exit_expression',
508
+ message: `Invalid JMESPath expression: ${e.message}`
509
+ });
510
+ }
511
+ }
512
+ }
513
+ if (config.execution_strategy && !['plan', 'act'].includes(config.execution_strategy)) {
514
+ errors.push({
515
+ path: 'execution_strategy',
516
+ message: 'Execution strategy must be either "plan" or "act"'
517
+ });
518
+ }
519
+ // Check commands
520
+ if (config.commands) {
521
+ if (typeof config.commands !== 'object') {
522
+ errors.push({ path: 'commands', message: 'Commands must be an object' });
523
+ }
524
+ else {
525
+ Object.entries(config.commands).forEach(([commandName, command]) => {
526
+ if (commandName !== userCommand) {
527
+ return;
528
+ }
529
+ const commandPath = `commands.${commandName}`;
530
+ // Validate command properties
531
+ if (!command.instructions) {
532
+ errors.push({ path: `${commandPath}.instructions`, message: 'Instructions are required' });
533
+ }
534
+ if (config.execution_strategy && !['plan', 'act'].includes(config.execution_strategy)) {
535
+ errors.push({
536
+ path: `${commandPath}.execution_strategy`,
537
+ message: 'Execution strategy must be either "plan" or "act"'
538
+ });
539
+ }
540
+ if (command.available_tools) {
541
+ validateAvailableTools(command.available_tools, `${commandPath}.available_tools`, errors);
542
+ }
543
+ if (command.ignore_tools) {
544
+ validateAvailableTools(command.ignore_tools, `${commandPath}.ignore_tools`, errors);
545
+ }
546
+ // Validate command-level tools_config if present
547
+ if (command.mcpServers) {
548
+ const commandMCPServers = command?.mcpServers && typeof command.mcpServers === 'string' ? JSON.parse(command.mcpServers) : {};
549
+ Object.entries(commandMCPServers).forEach(([serverId, serverConfig]) => {
550
+ validateMCPServerConfig(serverConfig, `${commandPath}.mcpServers.${serverId}`, errors);
551
+ });
552
+ }
553
+ // Validate output_schema if present
554
+ if (command.output_schema) {
555
+ validateOutputSchema(command.output_schema, `${commandPath}.output_schema`, errors);
556
+ }
557
+ // Validate command-level exit_expression if present
558
+ if (command.exit_expression) {
559
+ if (!command.output_schema) {
560
+ errors.push({
561
+ path: `${commandPath}.exit_expression`,
562
+ message: 'Exit expression requires an output schema to be defined'
563
+ });
564
+ }
565
+ else {
566
+ try {
567
+ //@ts-ignore
568
+ jmespath.compile(command.exit_expression);
569
+ }
570
+ catch (e) {
571
+ errors.push({
572
+ path: `${commandPath}.exit_expression`,
573
+ message: `Invalid JMESPath expression: ${e.message}`
574
+ });
575
+ }
576
+ }
577
+ }
578
+ // Validate arguments
579
+ if (command.arguments) {
580
+ if (!Array.isArray(command.arguments)) {
581
+ errors.push({ path: `${commandPath}.arguments`, message: 'Arguments must be an array' });
582
+ }
583
+ else {
584
+ command.arguments.forEach((arg, index) => {
585
+ const argPath = `${commandPath}.arguments[${index}]`;
586
+ if (!arg.name) {
587
+ errors.push({ path: `${argPath}.name`, message: 'Argument name is required' });
588
+ }
589
+ const validTypes = ['string', 'number', 'boolean', 'array', 'object'];
590
+ if (!arg.type || !validTypes.includes(arg.type)) {
591
+ errors.push({
592
+ path: `${argPath}.type`,
593
+ message: `Argument type must be one of: ${validTypes.join(', ')}`
594
+ });
595
+ }
596
+ if (arg.required === undefined) {
597
+ errors.push({ path: `${argPath}.required`, message: 'Argument required flag must be specified' });
598
+ }
599
+ if (!arg.description) {
600
+ errors.push({ path: `${argPath}.description`, message: 'Argument description is required' });
601
+ }
602
+ });
603
+ }
604
+ }
605
+ });
606
+ }
607
+ }
608
+ return {
609
+ valid: errors.length === 0,
610
+ errors
611
+ };
612
+ }
613
+ /**
614
+ * Checks if the config version is valid and up to date
615
+ */
616
+ export function checkConfigVersion(configVersion) {
617
+ const latestVersion = LATEST_CONFIG_VERSION;
618
+ const parsedLatest = parseVersion(latestVersion);
619
+ if (!parsedLatest) {
620
+ throw new Error(`Invalid LATEST_CONFIG_VERSION format: ${latestVersion}. Expected x.y format.`);
621
+ }
622
+ if (!configVersion) {
623
+ return {
624
+ isLatest: false,
625
+ latestVersion,
626
+ message: `⚠️ No version specified in agent.toml. Using latest version ${latestVersion}. Consider adding 'version = "${latestVersion}"' to your agent.toml file.`
627
+ };
628
+ }
629
+ const parsedCurrent = parseVersion(configVersion);
630
+ if (!parsedCurrent) {
631
+ return {
632
+ isLatest: false,
633
+ currentVersion: configVersion,
634
+ latestVersion,
635
+ message: `❌ Invalid version format "${configVersion}". Expected format is x.y (e.g., "1.0", "2.1").`
636
+ };
637
+ }
638
+ const comparison = compareVersions(parsedCurrent, parsedLatest);
639
+ if (comparison > 0) {
640
+ return {
641
+ isLatest: false,
642
+ currentVersion: configVersion,
643
+ latestVersion,
644
+ message: `❌ Config version ${configVersion} is newer than the latest supported version ${latestVersion}. Please update your CLI or downgrade your config.`
645
+ };
646
+ }
647
+ if (comparison < 0) {
648
+ return {
649
+ isLatest: false,
650
+ currentVersion: configVersion,
651
+ latestVersion,
652
+ message: `⚠️ Your agent.toml version (${configVersion}) is outdated. Latest version is ${latestVersion}. Consider updating your configuration.`
653
+ };
654
+ }
655
+ // Version is current - no message needed
656
+ return {
657
+ isLatest: true,
658
+ currentVersion: configVersion,
659
+ latestVersion
660
+ };
661
+ }
662
+ /**
663
+ * Validates a tool configuration
664
+ *
665
+ * @param serverConfig Tool configuration to validate
666
+ * @param basePath Base path for error reporting
667
+ * @param errors Array to collect validation errors
668
+ */
669
+ function validateMCPServerConfig(serverConfig, basePath, errors) {
670
+ // Either command or url must be provided
671
+ if (!serverConfig.command && !serverConfig.url) {
672
+ errors.push({
673
+ path: basePath,
674
+ message: 'Either command or url must be provided for a tool configuration'
675
+ });
676
+ }
677
+ // Validate args if present
678
+ if (serverConfig.args && !Array.isArray(serverConfig.args)) {
679
+ errors.push({
680
+ path: `${basePath}.args`,
681
+ message: 'Tool args must be an array of strings'
682
+ });
683
+ }
684
+ // Validate env if present
685
+ if (serverConfig.env && typeof serverConfig.env !== 'object') {
686
+ errors.push({
687
+ path: `${basePath}.env`,
688
+ message: 'Tool env must be an object mapping environment variables to values'
689
+ });
690
+ }
691
+ }
692
+ // Add this function to validate the output schema
693
+ /**
694
+ * Validates an output schema string
695
+ *
696
+ * @param schema Output schema string to validate
697
+ * @param basePath Base path for error reporting
698
+ * @param errors Array to collect validation errors
699
+ */
700
+ export function validateOutputSchema(schema, basePath, errors) {
701
+ try {
702
+ // Parse the schema to make sure it's valid JSON
703
+ const parsedSchema = schema.json_schema.schema;
704
+ // Verify the schema has the expected structure
705
+ if (!parsedSchema.properties) {
706
+ errors.push({
707
+ path: basePath,
708
+ message: 'Output schema must have a properties object'
709
+ });
710
+ }
711
+ // Validate each property in the schema
712
+ if (parsedSchema.properties && typeof parsedSchema.properties === 'object') {
713
+ Object.entries(parsedSchema.properties).forEach(([propName, propDef]) => {
714
+ if (!propDef.description) {
715
+ errors.push({
716
+ path: `${basePath}.properties.${propName}`,
717
+ message: `Property '${propName}' must have a description`
718
+ });
719
+ }
720
+ if (!propDef.type) {
721
+ errors.push({
722
+ path: `${basePath}.properties.${propName}`,
723
+ message: `Property '${propName}' must have a type`
724
+ });
725
+ }
726
+ });
727
+ }
728
+ }
729
+ catch (error) {
730
+ errors.push({
731
+ path: basePath,
732
+ message: `Invalid JSON schema: ${error.message}`
733
+ });
734
+ }
735
+ }
736
+ /**
737
+ * Validates command arguments against the command configuration
738
+ * @param commandConfig The command configuration from agent.toml
739
+ * @param customArgs The custom arguments provided by the user
740
+ * @returns ValidationResult with valid flag and any errors
741
+ */
742
+ export function validateCommandArgs(commandConfig, customArgs) {
743
+ const validationErrors = [];
744
+ // If no arguments defined in config, any args are valid
745
+ if (!commandConfig.arguments || !Array.isArray(commandConfig.arguments)) {
746
+ return { valid: true, errors: [] };
747
+ }
748
+ // Check for required arguments
749
+ const requiredArgs = commandConfig.arguments.filter(arg => arg.required);
750
+ for (const arg of requiredArgs) {
751
+ if (!(arg.name in customArgs)) {
752
+ validationErrors.push({
753
+ path: `arguments.${arg.name}`,
754
+ message: `Required argument '${arg.name}' is missing`
755
+ });
756
+ }
757
+ }
758
+ // Check types of provided arguments
759
+ for (const [key, value] of Object.entries(customArgs)) {
760
+ const argConfig = commandConfig.arguments.find(arg => arg.name === key);
761
+ // Check if argument is defined in the config
762
+ if (!argConfig) {
763
+ continue;
764
+ }
765
+ // Check type
766
+ switch (argConfig.type) {
767
+ case 'number':
768
+ if (typeof value !== 'number' && !/^-?\d+(\.\d+)?$/.test(String(value))) {
769
+ validationErrors.push({
770
+ path: `arguments.${key}`,
771
+ message: `Argument '${key}' should be a number, got ${typeof value}`
772
+ });
773
+ }
774
+ break;
775
+ case 'string':
776
+ if (typeof value !== 'string') {
777
+ validationErrors.push({
778
+ path: `arguments.${key}`,
779
+ message: `Argument '${key}' should be a string, got ${typeof value}`
780
+ });
781
+ }
782
+ break;
783
+ case 'boolean':
784
+ // Handle string representation of booleans from CLI
785
+ if (typeof value !== 'boolean' &&
786
+ !(typeof value === 'string' && ['true', 'false'].includes(value.toLowerCase()))) {
787
+ validationErrors.push({
788
+ path: `arguments.${key}`,
789
+ message: `Argument '${key}' should be a boolean, got ${typeof value}`
790
+ });
791
+ }
792
+ break;
793
+ case 'array': {
794
+ // Accept either an actual array, or a stringified JSON array
795
+ if (Array.isArray(value)) {
796
+ break;
797
+ }
798
+ if (typeof value === 'string') {
799
+ try {
800
+ const parsed = JSON.parse(value);
801
+ if (!Array.isArray(parsed)) {
802
+ validationErrors.push({
803
+ path: `arguments.${key}`,
804
+ message: `Argument '${key}' should be a JSON array (e.g., "[1, 2, 3]"), got valid JSON but not an array`
805
+ });
806
+ }
807
+ }
808
+ catch {
809
+ validationErrors.push({
810
+ path: `arguments.${key}`,
811
+ message: `Argument '${key}' should be a JSON array (e.g., "[1, 2, 3]"), got invalid JSON string`
812
+ });
813
+ }
814
+ break;
815
+ }
816
+ // Any other type is invalid
817
+ validationErrors.push({
818
+ path: `arguments.${key}`,
819
+ message: `Argument '${key}' should be a JSON array, got ${typeof value}`
820
+ });
821
+ break;
822
+ }
823
+ case 'object': {
824
+ // Accept either a plain object (not null/array), or a stringified JSON object
825
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
826
+ break;
827
+ }
828
+ if (typeof value === 'string') {
829
+ try {
830
+ const parsed = JSON.parse(value);
831
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
832
+ validationErrors.push({
833
+ path: `arguments.${key}`,
834
+ message: `Argument '${key}' should be a JSON object (e.g., "{\"foo\": 1}"), got valid JSON but not an object`
835
+ });
836
+ }
837
+ }
838
+ catch {
839
+ validationErrors.push({
840
+ path: `arguments.${key}`,
841
+ message: `Argument '${key}' should be a JSON object (e.g., "{\"foo\": 1}"), got invalid JSON string`
842
+ });
843
+ }
844
+ break;
845
+ }
846
+ // Any other type is invalid
847
+ validationErrors.push({
848
+ path: `arguments.${key}`,
849
+ message: `Argument '${key}' should be a JSON object, got ${typeof value}`
850
+ });
851
+ break;
852
+ }
853
+ default:
854
+ validationErrors.push({
855
+ path: `arguments.${key}.type`,
856
+ message: `Unknown type '${argConfig.type}' for argument '${key}'`
857
+ });
858
+ }
859
+ }
860
+ return {
861
+ valid: validationErrors.length === 0,
862
+ errors: validationErrors
863
+ };
864
+ }
865
+ /**
866
+ * Shared validator for available_tools entries
867
+ */
868
+ function validateAvailableTools(available, basePath, errors) {
869
+ if (!Array.isArray(available)) {
870
+ errors.push({ path: basePath, message: 'Available tools must be an array' });
871
+ return;
872
+ }
873
+ // MCPManager may not be initialized yet during early validation (e.g., before mcpInitialization)
874
+ let manager = null;
875
+ try {
876
+ manager = MCPManager.getInstance();
877
+ }
878
+ catch {
879
+ // MCP may be disabled (tools = []), or not initialized yet.
880
+ // In that case, skip server/tool existence validation entirely.
881
+ return;
882
+ }
883
+ available.forEach((tool) => {
884
+ const [serverName, toolName] = String(tool).split('.');
885
+ const tools = manager.getTools(serverName);
886
+ if (tools.length === 0) {
887
+ errors.push({
888
+ path: `${basePath}[${serverName}]`,
889
+ message: `Could not initialize the server ${serverName}. Make sure the MCP server is configured correctly.`
890
+ });
891
+ }
892
+ else if (toolName && !tools.find((t) => t.name === toolName)) {
893
+ errors.push({
894
+ path: `${basePath}[${tool}]`,
895
+ message: `Tool ${toolName} not found in server ${serverName}`
896
+ });
897
+ }
898
+ });
899
+ }
900
+ /**
901
+ * Validate a mode by name
902
+ */
903
+ export function validateMode(config, modeName) {
904
+ const errors = [];
905
+ if (!config.modes || typeof config.modes !== 'object') {
906
+ errors.push({ path: 'modes', message: 'No modes defined in configuration' });
907
+ return { valid: false, errors };
908
+ }
909
+ const mode = config.modes[modeName];
910
+ const basePath = `modes.${modeName}`;
911
+ if (!mode) {
912
+ errors.push({ path: basePath, message: `Mode '${modeName}' not found` });
913
+ return { valid: false, errors };
914
+ }
915
+ // instructions required
916
+ if (!mode.instructions || typeof mode.instructions !== 'string' || mode.instructions.trim().length === 0) {
917
+ errors.push({ path: `${basePath}.instructions`, message: 'Instructions are required' });
918
+ }
919
+ // execution_strategy (optional) must be plan|act
920
+ if (mode.execution_strategy && !['plan', 'act'].includes(mode.execution_strategy)) {
921
+ errors.push({ path: `${basePath}.execution_strategy`, message: 'Execution strategy must be either "plan" or "act"' });
922
+ }
923
+ // available_tools validation (optional)
924
+ if (mode.available_tools) {
925
+ validateAvailableTools(mode.available_tools, `${basePath}.available_tools`, errors);
926
+ }
927
+ if (mode.ignore_tools) {
928
+ validateAvailableTools(mode.ignore_tools, `${basePath}.ignore_tools`, errors);
929
+ }
930
+ // mcpServers validation (optional)
931
+ if (mode.mcpServers) {
932
+ const modeMCPServers = typeof mode.mcpServers === 'string' ? (() => { try {
933
+ return JSON.parse(mode.mcpServers);
934
+ }
935
+ catch {
936
+ return {};
937
+ } })() : mode.mcpServers;
938
+ Object.entries(modeMCPServers || {}).forEach(([serverId, serverConfig]) => {
939
+ validateMCPServerConfig(serverConfig, `${basePath}.mcpServers.${serverId}`, errors);
940
+ });
941
+ }
942
+ // output_schema validation (optional)
943
+ if (mode.output_schema && typeof mode.output_schema !== 'string') {
944
+ validateOutputSchema(mode.output_schema, `${basePath}.output_schema`, errors);
945
+ }
946
+ // exit_expression validation (optional)
947
+ if (mode.exit_expression) {
948
+ if (!mode.output_schema) {
949
+ errors.push({ path: `${basePath}.exit_expression`, message: 'Exit expression requires an output schema to be defined' });
950
+ }
951
+ else {
952
+ try {
953
+ // @ts-ignore
954
+ jmespath.compile(mode.exit_expression);
955
+ }
956
+ catch (e) {
957
+ errors.push({ path: `${basePath}.exit_expression`, message: `Invalid JMESPath expression: ${e.message}` });
958
+ }
959
+ }
960
+ }
961
+ // permissions (optional) basic validation
962
+ if (mode.permissions && !/^(r?w?x?|-|rw|rx|wx|rwx)$/.test(mode.permissions)) {
963
+ errors.push({ path: `${basePath}.permissions`, message: 'Invalid permissions format. Use one of: r, rw, rx, wx, rwx, -' });
964
+ }
965
+ return { valid: errors.length === 0, errors };
966
+ }
967
+ //# sourceMappingURL=index.js.map