@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.
- package/LICENSE +118 -0
- package/README.md +121 -0
- package/dist/api/agent.d.ts +69 -0
- package/dist/api/agent.d.ts.map +1 -0
- package/dist/api/agent.js +1034 -0
- package/dist/api/agent.js.map +1 -0
- package/dist/api/analytics.d.ts +43 -0
- package/dist/api/analytics.d.ts.map +1 -0
- package/dist/api/analytics.js +163 -0
- package/dist/api/analytics.js.map +1 -0
- package/dist/api/http.d.ts +5 -0
- package/dist/api/http.d.ts.map +1 -0
- package/dist/api/http.js +59 -0
- package/dist/api/http.js.map +1 -0
- package/dist/api/index.d.ts +12 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +17 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/taskTracking.d.ts +54 -0
- package/dist/api/taskTracking.d.ts.map +1 -0
- package/dist/api/taskTracking.js +208 -0
- package/dist/api/taskTracking.js.map +1 -0
- package/dist/api/types.d.ts +92 -0
- package/dist/api/types.d.ts.map +1 -0
- package/dist/api/types.js +2 -0
- package/dist/api/types.js.map +1 -0
- package/dist/api/utils.d.ts +8 -0
- package/dist/api/utils.d.ts.map +1 -0
- package/dist/api/utils.js +54 -0
- package/dist/api/utils.js.map +1 -0
- package/dist/api/websocket.d.ts +74 -0
- package/dist/api/websocket.d.ts.map +1 -0
- package/dist/api/websocket.js +685 -0
- package/dist/api/websocket.js.map +1 -0
- package/dist/auth/index.d.ts +25 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +85 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/clients/index.d.ts +8 -0
- package/dist/clients/index.d.ts.map +1 -0
- package/dist/clients/index.js +7 -0
- package/dist/clients/index.js.map +1 -0
- package/dist/clients/info/InfoClient.d.ts +37 -0
- package/dist/clients/info/InfoClient.d.ts.map +1 -0
- package/dist/clients/info/InfoClient.js +69 -0
- package/dist/clients/info/InfoClient.js.map +1 -0
- package/dist/clients/info/index.d.ts +4 -0
- package/dist/clients/info/index.d.ts.map +1 -0
- package/dist/clients/info/index.js +2 -0
- package/dist/clients/info/index.js.map +1 -0
- package/dist/clients/info/types.d.ts +21 -0
- package/dist/clients/info/types.d.ts.map +1 -0
- package/dist/clients/info/types.js +2 -0
- package/dist/clients/info/types.js.map +1 -0
- package/dist/clients/sessions/SessionsClient.d.ts +34 -0
- package/dist/clients/sessions/SessionsClient.d.ts.map +1 -0
- package/dist/clients/sessions/SessionsClient.js +71 -0
- package/dist/clients/sessions/SessionsClient.js.map +1 -0
- package/dist/clients/sessions/index.d.ts +4 -0
- package/dist/clients/sessions/index.d.ts.map +1 -0
- package/dist/clients/sessions/index.js +2 -0
- package/dist/clients/sessions/index.js.map +1 -0
- package/dist/clients/sessions/types.d.ts +20 -0
- package/dist/clients/sessions/types.d.ts.map +1 -0
- package/dist/clients/sessions/types.js +2 -0
- package/dist/clients/sessions/types.js.map +1 -0
- package/dist/config/ConfigManager.d.ts +43 -0
- package/dist/config/ConfigManager.d.ts.map +1 -0
- package/dist/config/ConfigManager.js +472 -0
- package/dist/config/ConfigManager.js.map +1 -0
- package/dist/config/index.d.ts +6 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +7 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/urlConfig.d.ts +15 -0
- package/dist/config/urlConfig.d.ts.map +1 -0
- package/dist/config/urlConfig.js +75 -0
- package/dist/config/urlConfig.js.map +1 -0
- package/dist/constants/errors.d.ts +2 -0
- package/dist/constants/errors.d.ts.map +1 -0
- package/dist/constants/errors.js +2 -0
- package/dist/constants/errors.js.map +1 -0
- package/dist/constants/index.d.ts +7 -0
- package/dist/constants/index.d.ts.map +1 -0
- package/dist/constants/index.js +11 -0
- package/dist/constants/index.js.map +1 -0
- package/dist/constants/tools.d.ts +4 -0
- package/dist/constants/tools.d.ts.map +1 -0
- package/dist/constants/tools.js +4 -0
- package/dist/constants/tools.js.map +1 -0
- package/dist/constants/versions.d.ts +2 -0
- package/dist/constants/versions.d.ts.map +1 -0
- package/dist/constants/versions.js +2 -0
- package/dist/constants/versions.js.map +1 -0
- package/dist/context/buildUserContext.d.ts +18 -0
- package/dist/context/buildUserContext.d.ts.map +1 -0
- package/dist/context/buildUserContext.js +34 -0
- package/dist/context/buildUserContext.js.map +1 -0
- package/dist/context/index.d.ts +9 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +9 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context/messageManager.d.ts +42 -0
- package/dist/context/messageManager.d.ts.map +1 -0
- package/dist/context/messageManager.js +322 -0
- package/dist/context/messageManager.js.map +1 -0
- package/dist/context/taskFocus.d.ts +2 -0
- package/dist/context/taskFocus.d.ts.map +1 -0
- package/dist/context/taskFocus.js +26 -0
- package/dist/context/taskFocus.js.map +1 -0
- package/dist/context/userInput.d.ts +3 -0
- package/dist/context/userInput.d.ts.map +1 -0
- package/dist/context/userInput.js +20 -0
- package/dist/context/userInput.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/MCPManager.d.ts +125 -0
- package/dist/mcp/MCPManager.d.ts.map +1 -0
- package/dist/mcp/MCPManager.js +616 -0
- package/dist/mcp/MCPManager.js.map +1 -0
- package/dist/mcp/approvedTools.d.ts +4 -0
- package/dist/mcp/approvedTools.d.ts.map +1 -0
- package/dist/mcp/approvedTools.js +19 -0
- package/dist/mcp/approvedTools.js.map +1 -0
- package/dist/mcp/baseServer.d.ts +75 -0
- package/dist/mcp/baseServer.d.ts.map +1 -0
- package/dist/mcp/baseServer.js +107 -0
- package/dist/mcp/baseServer.js.map +1 -0
- package/dist/mcp/builtinServers.d.ts +15 -0
- package/dist/mcp/builtinServers.d.ts.map +1 -0
- package/dist/mcp/builtinServers.js +155 -0
- package/dist/mcp/builtinServers.js.map +1 -0
- package/dist/mcp/dynamicBEServer.d.ts +20 -0
- package/dist/mcp/dynamicBEServer.d.ts.map +1 -0
- package/dist/mcp/dynamicBEServer.js +52 -0
- package/dist/mcp/dynamicBEServer.js.map +1 -0
- package/dist/mcp/index.d.ts +19 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +24 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/mcpInitialization.d.ts +2 -0
- package/dist/mcp/mcpInitialization.d.ts.map +1 -0
- package/dist/mcp/mcpInitialization.js +56 -0
- package/dist/mcp/mcpInitialization.js.map +1 -0
- package/dist/mcp/servers/filesystem.d.ts +75 -0
- package/dist/mcp/servers/filesystem.d.ts.map +1 -0
- package/dist/mcp/servers/filesystem.js +992 -0
- package/dist/mcp/servers/filesystem.js.map +1 -0
- package/dist/mcp/servers/gerrit.d.ts +19 -0
- package/dist/mcp/servers/gerrit.d.ts.map +1 -0
- package/dist/mcp/servers/gerrit.js +515 -0
- package/dist/mcp/servers/gerrit.js.map +1 -0
- package/dist/mcp/servers/git.d.ts +18 -0
- package/dist/mcp/servers/git.d.ts.map +1 -0
- package/dist/mcp/servers/git.js +441 -0
- package/dist/mcp/servers/git.js.map +1 -0
- package/dist/mcp/servers/ripgrep.d.ts +34 -0
- package/dist/mcp/servers/ripgrep.d.ts.map +1 -0
- package/dist/mcp/servers/ripgrep.js +517 -0
- package/dist/mcp/servers/ripgrep.js.map +1 -0
- package/dist/mcp/servers/shell.d.ts +20 -0
- package/dist/mcp/servers/shell.d.ts.map +1 -0
- package/dist/mcp/servers/shell.js +603 -0
- package/dist/mcp/servers/shell.js.map +1 -0
- package/dist/mcp/serversRegistry.d.ts +55 -0
- package/dist/mcp/serversRegistry.d.ts.map +1 -0
- package/dist/mcp/serversRegistry.js +410 -0
- package/dist/mcp/serversRegistry.js.map +1 -0
- package/dist/mcp/toolProcessor.d.ts +42 -0
- package/dist/mcp/toolProcessor.d.ts.map +1 -0
- package/dist/mcp/toolProcessor.js +200 -0
- package/dist/mcp/toolProcessor.js.map +1 -0
- package/dist/mcp/types.d.ts +29 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +2 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/parser/index.d.ts +72 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/index.js +967 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/parser/types.d.ts +153 -0
- package/dist/parser/types.d.ts.map +1 -0
- package/dist/parser/types.js +6 -0
- package/dist/parser/types.js.map +1 -0
- package/dist/parser/utils.d.ts +18 -0
- package/dist/parser/utils.d.ts.map +1 -0
- package/dist/parser/utils.js +64 -0
- package/dist/parser/utils.js.map +1 -0
- package/dist/sdk/QodoSDK.d.ts +152 -0
- package/dist/sdk/QodoSDK.d.ts.map +1 -0
- package/dist/sdk/QodoSDK.js +786 -0
- package/dist/sdk/QodoSDK.js.map +1 -0
- package/dist/sdk/bootstrap.d.ts +16 -0
- package/dist/sdk/bootstrap.d.ts.map +1 -0
- package/dist/sdk/bootstrap.js +21 -0
- package/dist/sdk/bootstrap.js.map +1 -0
- package/dist/sdk/builders.d.ts +54 -0
- package/dist/sdk/builders.d.ts.map +1 -0
- package/dist/sdk/builders.js +117 -0
- package/dist/sdk/builders.js.map +1 -0
- package/dist/sdk/defaults.d.ts +11 -0
- package/dist/sdk/defaults.d.ts.map +1 -0
- package/dist/sdk/defaults.js +39 -0
- package/dist/sdk/defaults.js.map +1 -0
- package/dist/sdk/discovery.d.ts +2 -0
- package/dist/sdk/discovery.d.ts.map +1 -0
- package/dist/sdk/discovery.js +25 -0
- package/dist/sdk/discovery.js.map +1 -0
- package/dist/sdk/events.d.ts +168 -0
- package/dist/sdk/events.d.ts.map +1 -0
- package/dist/sdk/events.js +52 -0
- package/dist/sdk/events.js.map +1 -0
- package/dist/sdk/index.d.ts +17 -0
- package/dist/sdk/index.d.ts.map +1 -0
- package/dist/sdk/index.js +17 -0
- package/dist/sdk/index.js.map +1 -0
- package/dist/sdk/runner/AgentRunner.d.ts +22 -0
- package/dist/sdk/runner/AgentRunner.d.ts.map +1 -0
- package/dist/sdk/runner/AgentRunner.js +222 -0
- package/dist/sdk/runner/AgentRunner.js.map +1 -0
- package/dist/sdk/runner/finalize.d.ts +9 -0
- package/dist/sdk/runner/finalize.d.ts.map +1 -0
- package/dist/sdk/runner/finalize.js +115 -0
- package/dist/sdk/runner/finalize.js.map +1 -0
- package/dist/sdk/runner/formats.d.ts +7 -0
- package/dist/sdk/runner/formats.d.ts.map +1 -0
- package/dist/sdk/runner/formats.js +91 -0
- package/dist/sdk/runner/formats.js.map +1 -0
- package/dist/sdk/runner/index.d.ts +9 -0
- package/dist/sdk/runner/index.d.ts.map +1 -0
- package/dist/sdk/runner/index.js +9 -0
- package/dist/sdk/runner/index.js.map +1 -0
- package/dist/sdk/runner/progress.d.ts +3 -0
- package/dist/sdk/runner/progress.d.ts.map +1 -0
- package/dist/sdk/runner/progress.js +16 -0
- package/dist/sdk/runner/progress.js.map +1 -0
- package/dist/sdk/schemas.d.ts +50 -0
- package/dist/sdk/schemas.d.ts.map +1 -0
- package/dist/sdk/schemas.js +145 -0
- package/dist/sdk/schemas.js.map +1 -0
- package/dist/session/SessionContext.d.ts +86 -0
- package/dist/session/SessionContext.d.ts.map +1 -0
- package/dist/session/SessionContext.js +395 -0
- package/dist/session/SessionContext.js.map +1 -0
- package/dist/session/environment.d.ts +42 -0
- package/dist/session/environment.d.ts.map +1 -0
- package/dist/session/environment.js +27 -0
- package/dist/session/environment.js.map +1 -0
- package/dist/session/history.d.ts +3 -0
- package/dist/session/history.d.ts.map +1 -0
- package/dist/session/history.js +67 -0
- package/dist/session/history.js.map +1 -0
- package/dist/session/index.d.ts +10 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +9 -0
- package/dist/session/index.js.map +1 -0
- package/dist/session/serverData.d.ts +38 -0
- package/dist/session/serverData.d.ts.map +1 -0
- package/dist/session/serverData.js +241 -0
- package/dist/session/serverData.js.map +1 -0
- package/dist/tracking/Tracker.d.ts +55 -0
- package/dist/tracking/Tracker.d.ts.map +1 -0
- package/dist/tracking/Tracker.js +217 -0
- package/dist/tracking/Tracker.js.map +1 -0
- package/dist/tracking/index.d.ts +8 -0
- package/dist/tracking/index.d.ts.map +1 -0
- package/dist/tracking/index.js +8 -0
- package/dist/tracking/index.js.map +1 -0
- package/dist/tracking/schemas.d.ts +292 -0
- package/dist/tracking/schemas.d.ts.map +1 -0
- package/dist/tracking/schemas.js +91 -0
- package/dist/tracking/schemas.js.map +1 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/extractSetFlags.d.ts +6 -0
- package/dist/utils/extractSetFlags.d.ts.map +1 -0
- package/dist/utils/extractSetFlags.js +16 -0
- package/dist/utils/extractSetFlags.js.map +1 -0
- package/dist/utils/formatTimeAgo.d.ts +2 -0
- package/dist/utils/formatTimeAgo.d.ts.map +1 -0
- package/dist/utils/formatTimeAgo.js +20 -0
- package/dist/utils/formatTimeAgo.js.map +1 -0
- package/dist/utils/index.d.ts +12 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +12 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/machineId.d.ts +14 -0
- package/dist/utils/machineId.d.ts.map +1 -0
- package/dist/utils/machineId.js +66 -0
- package/dist/utils/machineId.js.map +1 -0
- package/dist/utils/pathUtils.d.ts +22 -0
- package/dist/utils/pathUtils.d.ts.map +1 -0
- package/dist/utils/pathUtils.js +54 -0
- package/dist/utils/pathUtils.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +23 -0
- package/dist/version.js.map +1 -0
- 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
|