loki-mode 5.51.0 → 5.52.1
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/README.md +4 -56
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/hooks/validate-bash.sh +5 -2
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/docs/alternative-installations.md +3 -3
- package/docs/certification/01-core-concepts/lab.md +174 -0
- package/docs/certification/01-core-concepts/lesson.md +182 -0
- package/docs/certification/01-core-concepts/quiz.md +93 -0
- package/docs/certification/02-enterprise-features/lab.md +154 -0
- package/docs/certification/02-enterprise-features/lesson.md +202 -0
- package/docs/certification/02-enterprise-features/quiz.md +93 -0
- package/docs/certification/03-advanced-patterns/lab.md +138 -0
- package/docs/certification/03-advanced-patterns/lesson.md +199 -0
- package/docs/certification/03-advanced-patterns/quiz.md +93 -0
- package/docs/certification/04-production-deployment/lab.md +160 -0
- package/docs/certification/04-production-deployment/lesson.md +261 -0
- package/docs/certification/04-production-deployment/quiz.md +93 -0
- package/docs/certification/05-troubleshooting/lab.md +254 -0
- package/docs/certification/05-troubleshooting/lesson.md +266 -0
- package/docs/certification/05-troubleshooting/quiz.md +93 -0
- package/docs/certification/README.md +80 -0
- package/docs/certification/answer-key.md +117 -0
- package/docs/certification/certification-exam.md +471 -0
- package/docs/certification/sample-prds/microservices-platform.md +100 -0
- package/docs/certification/sample-prds/saas-dashboard.md +60 -0
- package/docs/certification/sample-prds/todo-app.md +44 -0
- package/mcp/__init__.py +1 -1
- package/mcp/server.py +230 -0
- package/package.json +1 -1
- package/src/plugins/agent-plugin.js +123 -0
- package/src/plugins/gate-plugin.js +153 -0
- package/src/plugins/index.js +116 -0
- package/src/plugins/integration-plugin.js +174 -0
- package/src/plugins/loader.js +275 -0
- package/src/plugins/mcp-plugin.js +190 -0
- package/src/plugins/schemas/agent.json +59 -0
- package/src/plugins/schemas/integration.json +62 -0
- package/src/plugins/schemas/mcp_tool.json +73 -0
- package/src/plugins/schemas/quality_gate.json +52 -0
- package/src/plugins/validator.js +297 -0
- /package/dashboard/{secrets.py → app_secrets.py} +0 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { readFileSync, readdirSync, existsSync, statSync, watch } = require('fs');
|
|
4
|
+
const { join, extname } = require('path');
|
|
5
|
+
const { PluginValidator } = require('./validator');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Simple YAML parser for plugin configs.
|
|
9
|
+
* Handles: key-value pairs, arrays (- item), multiline strings (|), booleans, numbers.
|
|
10
|
+
* This avoids requiring the js-yaml dependency.
|
|
11
|
+
*/
|
|
12
|
+
function parseSimpleYAML(content) {
|
|
13
|
+
const result = {};
|
|
14
|
+
const lines = content.split('\n');
|
|
15
|
+
let currentKey = null;
|
|
16
|
+
let multilineValue = null;
|
|
17
|
+
let multilineIndent = 0;
|
|
18
|
+
let arrayKey = null;
|
|
19
|
+
|
|
20
|
+
for (let i = 0; i < lines.length; i++) {
|
|
21
|
+
const line = lines[i];
|
|
22
|
+
|
|
23
|
+
// Skip empty lines and comments (unless in multiline mode)
|
|
24
|
+
if (!multilineValue && (line.trim() === '' || line.trim().startsWith('#'))) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Handle multiline string continuation
|
|
29
|
+
if (multilineValue !== null) {
|
|
30
|
+
const stripped = line;
|
|
31
|
+
const indent = stripped.length - stripped.trimStart().length;
|
|
32
|
+
if (indent >= multilineIndent && stripped.trim() !== '') {
|
|
33
|
+
if (result[currentKey] === '') {
|
|
34
|
+
result[currentKey] = stripped.trimStart();
|
|
35
|
+
} else {
|
|
36
|
+
result[currentKey] += '\n' + stripped.trimStart();
|
|
37
|
+
}
|
|
38
|
+
continue;
|
|
39
|
+
} else {
|
|
40
|
+
// End of multiline
|
|
41
|
+
multilineValue = null;
|
|
42
|
+
// Fall through to process this line normally
|
|
43
|
+
if (line.trim() === '') continue;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Handle array items
|
|
48
|
+
const arrayMatch = line.match(/^(\s+)- (.+)$/);
|
|
49
|
+
if (arrayMatch && arrayKey) {
|
|
50
|
+
if (!Array.isArray(result[arrayKey])) {
|
|
51
|
+
result[arrayKey] = [];
|
|
52
|
+
}
|
|
53
|
+
result[arrayKey].push(parseYAMLValue(arrayMatch[2].trim()));
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Handle key-value pairs
|
|
58
|
+
const kvMatch = line.match(/^([a-z_][a-z0-9_]*)\s*:\s*(.*)$/i);
|
|
59
|
+
if (kvMatch) {
|
|
60
|
+
const key = kvMatch[1];
|
|
61
|
+
const rawValue = kvMatch[2].trim();
|
|
62
|
+
|
|
63
|
+
// Check for multiline indicator
|
|
64
|
+
if (rawValue === '|' || rawValue === '>') {
|
|
65
|
+
currentKey = key;
|
|
66
|
+
multilineValue = '';
|
|
67
|
+
multilineIndent = 2; // expect at least 2-space indent
|
|
68
|
+
result[key] = '';
|
|
69
|
+
arrayKey = null;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Check if next line starts an array
|
|
74
|
+
if (rawValue === '' && i + 1 < lines.length && lines[i + 1].match(/^\s+- /)) {
|
|
75
|
+
arrayKey = key;
|
|
76
|
+
result[key] = [];
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
result[key] = parseYAMLValue(rawValue);
|
|
81
|
+
currentKey = key;
|
|
82
|
+
arrayKey = null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Parse a single YAML value (string, number, boolean, null).
|
|
91
|
+
*/
|
|
92
|
+
function parseYAMLValue(raw) {
|
|
93
|
+
if (raw === '' || raw === 'null' || raw === '~') return null;
|
|
94
|
+
if (raw === 'true') return true;
|
|
95
|
+
if (raw === 'false') return false;
|
|
96
|
+
|
|
97
|
+
// Quoted strings
|
|
98
|
+
if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))) {
|
|
99
|
+
return raw.slice(1, -1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Numbers
|
|
103
|
+
if (/^-?\d+$/.test(raw)) return parseInt(raw, 10);
|
|
104
|
+
if (/^-?\d+\.\d+$/.test(raw)) return parseFloat(raw);
|
|
105
|
+
|
|
106
|
+
return raw;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
class PluginLoader {
|
|
110
|
+
/**
|
|
111
|
+
* Create a new PluginLoader.
|
|
112
|
+
* @param {string} pluginsDir - Path to plugins directory (default: .loki/plugins)
|
|
113
|
+
* @param {string} [schemasDir] - Path to schemas directory
|
|
114
|
+
*/
|
|
115
|
+
constructor(pluginsDir, schemasDir) {
|
|
116
|
+
this.pluginsDir = pluginsDir || '.loki/plugins';
|
|
117
|
+
this.validator = new PluginValidator(schemasDir);
|
|
118
|
+
this._watchers = [];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Discover plugin files in the plugins directory.
|
|
123
|
+
* @returns {string[]} Array of file paths
|
|
124
|
+
*/
|
|
125
|
+
discover() {
|
|
126
|
+
if (!existsSync(this.pluginsDir)) {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const stat = statSync(this.pluginsDir);
|
|
132
|
+
if (!stat.isDirectory()) {
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
} catch {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const entries = readdirSync(this.pluginsDir);
|
|
141
|
+
const pluginFiles = [];
|
|
142
|
+
|
|
143
|
+
for (const entry of entries) {
|
|
144
|
+
const ext = extname(entry).toLowerCase();
|
|
145
|
+
if (ext === '.yaml' || ext === '.yml' || ext === '.json') {
|
|
146
|
+
pluginFiles.push(join(this.pluginsDir, entry));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return pluginFiles.sort();
|
|
151
|
+
} catch {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Parse a plugin file (YAML or JSON).
|
|
158
|
+
* @param {string} filePath - Path to the plugin file
|
|
159
|
+
* @returns {object|null} Parsed config or null on error
|
|
160
|
+
*/
|
|
161
|
+
_parseFile(filePath) {
|
|
162
|
+
try {
|
|
163
|
+
const content = readFileSync(filePath, 'utf8');
|
|
164
|
+
const ext = extname(filePath).toLowerCase();
|
|
165
|
+
|
|
166
|
+
if (ext === '.json') {
|
|
167
|
+
return JSON.parse(content);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// YAML parsing
|
|
171
|
+
return parseSimpleYAML(content);
|
|
172
|
+
} catch (err) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Load all plugins from the plugins directory.
|
|
179
|
+
* @returns {{ loaded: Array<{path: string, config: object}>, failed: Array<{path: string, errors: string[]}> }}
|
|
180
|
+
*/
|
|
181
|
+
loadAll() {
|
|
182
|
+
const files = this.discover();
|
|
183
|
+
const loaded = [];
|
|
184
|
+
const failed = [];
|
|
185
|
+
|
|
186
|
+
for (const filePath of files) {
|
|
187
|
+
try {
|
|
188
|
+
const config = this._parseFile(filePath);
|
|
189
|
+
|
|
190
|
+
if (!config) {
|
|
191
|
+
failed.push({ path: filePath, errors: ['Failed to parse plugin file'] });
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const result = this.validator.validate(config);
|
|
196
|
+
|
|
197
|
+
if (result.valid) {
|
|
198
|
+
loaded.push({ path: filePath, config });
|
|
199
|
+
} else {
|
|
200
|
+
failed.push({ path: filePath, errors: result.errors });
|
|
201
|
+
}
|
|
202
|
+
} catch (err) {
|
|
203
|
+
failed.push({ path: filePath, errors: [err.message || 'Unknown error'] });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return { loaded, failed };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Load a single plugin file.
|
|
212
|
+
* @param {string} filePath - Path to the plugin file
|
|
213
|
+
* @returns {{ config: object|null, errors: string[] }}
|
|
214
|
+
*/
|
|
215
|
+
loadOne(filePath) {
|
|
216
|
+
const config = this._parseFile(filePath);
|
|
217
|
+
|
|
218
|
+
if (!config) {
|
|
219
|
+
return { config: null, errors: ['Failed to parse plugin file'] };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const result = this.validator.validate(config);
|
|
223
|
+
|
|
224
|
+
if (result.valid) {
|
|
225
|
+
return { config, errors: [] };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return { config: null, errors: result.errors };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Watch the plugins directory for changes.
|
|
233
|
+
* @param {function} callback - Called with (eventType, filePath) on changes
|
|
234
|
+
* @returns {function} Cleanup function to stop watching
|
|
235
|
+
*/
|
|
236
|
+
watchForChanges(callback) {
|
|
237
|
+
if (!existsSync(this.pluginsDir)) {
|
|
238
|
+
return () => {};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
const watcher = watch(this.pluginsDir, (eventType, filename) => {
|
|
243
|
+
if (!filename) return;
|
|
244
|
+
|
|
245
|
+
const ext = extname(filename).toLowerCase();
|
|
246
|
+
if (ext === '.yaml' || ext === '.yml' || ext === '.json') {
|
|
247
|
+
const filePath = join(this.pluginsDir, filename);
|
|
248
|
+
callback(eventType, filePath);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
this._watchers.push(watcher);
|
|
253
|
+
|
|
254
|
+
return () => {
|
|
255
|
+
watcher.close();
|
|
256
|
+
const idx = this._watchers.indexOf(watcher);
|
|
257
|
+
if (idx >= 0) this._watchers.splice(idx, 1);
|
|
258
|
+
};
|
|
259
|
+
} catch {
|
|
260
|
+
return () => {};
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Stop all file watchers.
|
|
266
|
+
*/
|
|
267
|
+
stopWatching() {
|
|
268
|
+
for (const watcher of this._watchers) {
|
|
269
|
+
try { watcher.close(); } catch { /* ignore */ }
|
|
270
|
+
}
|
|
271
|
+
this._watchers = [];
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
module.exports = { PluginLoader, parseSimpleYAML, parseYAMLValue };
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execFile } = require('child_process');
|
|
4
|
+
|
|
5
|
+
// In-memory registry for custom MCP tool plugins
|
|
6
|
+
const _registeredTools = new Map();
|
|
7
|
+
|
|
8
|
+
class MCPPlugin {
|
|
9
|
+
/**
|
|
10
|
+
* Register a custom MCP tool plugin.
|
|
11
|
+
*
|
|
12
|
+
* @param {object} pluginConfig - Validated MCP tool plugin config
|
|
13
|
+
* @returns {{ success: boolean, error?: string }}
|
|
14
|
+
*/
|
|
15
|
+
static register(pluginConfig) {
|
|
16
|
+
if (!pluginConfig || pluginConfig.type !== 'mcp_tool') {
|
|
17
|
+
return { success: false, error: 'Invalid plugin config: type must be "mcp_tool"' };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const name = pluginConfig.name;
|
|
21
|
+
|
|
22
|
+
if (_registeredTools.has(name)) {
|
|
23
|
+
return { success: false, error: `MCP tool plugin "${name}" is already registered` };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const toolDef = {
|
|
27
|
+
name: name,
|
|
28
|
+
description: pluginConfig.description,
|
|
29
|
+
command: pluginConfig.command,
|
|
30
|
+
parameters: pluginConfig.parameters || [],
|
|
31
|
+
timeout_ms: pluginConfig.timeout_ms || 30000,
|
|
32
|
+
working_directory: pluginConfig.working_directory || 'project',
|
|
33
|
+
registered_at: new Date().toISOString(),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
_registeredTools.set(name, toolDef);
|
|
37
|
+
return { success: true };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Unregister a custom MCP tool plugin.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} pluginName - Name of the tool to remove
|
|
44
|
+
* @returns {{ success: boolean, error?: string }}
|
|
45
|
+
*/
|
|
46
|
+
static unregister(pluginName) {
|
|
47
|
+
if (!_registeredTools.has(pluginName)) {
|
|
48
|
+
return { success: false, error: `MCP tool plugin "${pluginName}" is not registered` };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
_registeredTools.delete(pluginName);
|
|
52
|
+
return { success: true };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Execute an MCP tool command with parameter substitution.
|
|
57
|
+
*
|
|
58
|
+
* @param {object} pluginConfig - The MCP tool plugin config
|
|
59
|
+
* @param {object} params - Input parameters
|
|
60
|
+
* @param {string} [projectDir] - Project directory for sandbox
|
|
61
|
+
* @returns {Promise<{ success: boolean, output: string, duration_ms: number }>}
|
|
62
|
+
*/
|
|
63
|
+
static async execute(pluginConfig, params, projectDir) {
|
|
64
|
+
const command = pluginConfig.command;
|
|
65
|
+
const timeoutMs = pluginConfig.timeout_ms || 30000;
|
|
66
|
+
const startTime = Date.now();
|
|
67
|
+
|
|
68
|
+
if (!command) {
|
|
69
|
+
return {
|
|
70
|
+
success: false,
|
|
71
|
+
output: 'Error: no command specified for MCP tool',
|
|
72
|
+
duration_ms: 0,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Substitute parameters into command with shell-safe quoting
|
|
77
|
+
let resolvedCommand = command;
|
|
78
|
+
if (params && typeof params === 'object') {
|
|
79
|
+
for (const [key, value] of Object.entries(params)) {
|
|
80
|
+
const safeValue = MCPPlugin._sanitizeValue(value);
|
|
81
|
+
resolvedCommand = resolvedCommand.replace(
|
|
82
|
+
new RegExp(`\\{\\{params\\.${key}\\}\\}`, 'g'),
|
|
83
|
+
safeValue
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return new Promise((resolve) => {
|
|
89
|
+
const cwd = projectDir || process.cwd();
|
|
90
|
+
|
|
91
|
+
execFile('/bin/sh', ['-c', resolvedCommand], {
|
|
92
|
+
cwd,
|
|
93
|
+
timeout: timeoutMs,
|
|
94
|
+
maxBuffer: 1024 * 1024, // 1MB
|
|
95
|
+
env: { ...process.env, LOKI_MCP_TOOL: pluginConfig.name || 'unknown' },
|
|
96
|
+
}, (error, stdout, stderr) => {
|
|
97
|
+
const durationMs = Date.now() - startTime;
|
|
98
|
+
const output = (stdout || '') + (stderr ? '\n' + stderr : '');
|
|
99
|
+
|
|
100
|
+
if (error) {
|
|
101
|
+
if (error.killed) {
|
|
102
|
+
resolve({
|
|
103
|
+
success: false,
|
|
104
|
+
output: `Timeout: command exceeded ${timeoutMs}ms limit`,
|
|
105
|
+
duration_ms: durationMs,
|
|
106
|
+
});
|
|
107
|
+
} else {
|
|
108
|
+
resolve({
|
|
109
|
+
success: false,
|
|
110
|
+
output: output.trim() || error.message || 'Command failed',
|
|
111
|
+
duration_ms: durationMs,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
resolve({
|
|
116
|
+
success: true,
|
|
117
|
+
output: output.trim(),
|
|
118
|
+
duration_ms: durationMs,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get the MCP tool definition in a format suitable for MCP protocol.
|
|
127
|
+
*
|
|
128
|
+
* @param {string} name - Tool name
|
|
129
|
+
* @returns {object|null} MCP-compatible tool definition
|
|
130
|
+
*/
|
|
131
|
+
static getMCPDefinition(name) {
|
|
132
|
+
const tool = _registeredTools.get(name);
|
|
133
|
+
if (!tool) return null;
|
|
134
|
+
|
|
135
|
+
const inputSchema = {
|
|
136
|
+
type: 'object',
|
|
137
|
+
properties: {},
|
|
138
|
+
required: [],
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
for (const param of tool.parameters) {
|
|
142
|
+
inputSchema.properties[param.name] = {
|
|
143
|
+
type: param.type || 'string',
|
|
144
|
+
description: param.description || '',
|
|
145
|
+
};
|
|
146
|
+
if (param.default !== undefined) {
|
|
147
|
+
inputSchema.properties[param.name].default = param.default;
|
|
148
|
+
}
|
|
149
|
+
if (param.required) {
|
|
150
|
+
inputSchema.required.push(param.name);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
name: tool.name,
|
|
156
|
+
description: tool.description,
|
|
157
|
+
inputSchema,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* List all registered MCP tool plugins.
|
|
163
|
+
*
|
|
164
|
+
* @returns {object[]}
|
|
165
|
+
*/
|
|
166
|
+
static listRegistered() {
|
|
167
|
+
return Array.from(_registeredTools.values());
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Shell-safe value sanitization using POSIX single-quote escaping.
|
|
172
|
+
* Wraps value in single quotes with internal single quotes escaped.
|
|
173
|
+
*
|
|
174
|
+
* @param {*} value - The value to sanitize
|
|
175
|
+
* @returns {string} Shell-safe quoted string
|
|
176
|
+
*/
|
|
177
|
+
static _sanitizeValue(value) {
|
|
178
|
+
const str = String(value);
|
|
179
|
+
return "'" + str.replace(/'/g, "'\\''") + "'";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Clear all registered tools (primarily for testing).
|
|
184
|
+
*/
|
|
185
|
+
static _clearAll() {
|
|
186
|
+
_registeredTools.clear();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
module.exports = { MCPPlugin };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "Agent Plugin Schema",
|
|
4
|
+
"description": "Schema for custom agent plugins in Loki Mode",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["type", "name", "description", "prompt_template"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"type": {
|
|
9
|
+
"const": "agent"
|
|
10
|
+
},
|
|
11
|
+
"name": {
|
|
12
|
+
"type": "string",
|
|
13
|
+
"pattern": "^[a-z][a-z0-9-]{2,49}$",
|
|
14
|
+
"description": "Unique agent name (lowercase, 3-50 chars, alphanumeric and hyphens)"
|
|
15
|
+
},
|
|
16
|
+
"description": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"maxLength": 500,
|
|
19
|
+
"description": "Human-readable description of the agent"
|
|
20
|
+
},
|
|
21
|
+
"trigger": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"description": "Regex pattern for when to activate this agent"
|
|
24
|
+
},
|
|
25
|
+
"prompt_template": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"maxLength": 10000,
|
|
28
|
+
"description": "Prompt template for the agent (supports {{event.*}} variables)"
|
|
29
|
+
},
|
|
30
|
+
"quality_gate": {
|
|
31
|
+
"type": "boolean",
|
|
32
|
+
"default": false,
|
|
33
|
+
"description": "Whether this agent participates in quality gate reviews"
|
|
34
|
+
},
|
|
35
|
+
"category": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"enum": [
|
|
38
|
+
"engineering",
|
|
39
|
+
"operations",
|
|
40
|
+
"business",
|
|
41
|
+
"data",
|
|
42
|
+
"product",
|
|
43
|
+
"growth",
|
|
44
|
+
"review",
|
|
45
|
+
"orchestration",
|
|
46
|
+
"custom"
|
|
47
|
+
],
|
|
48
|
+
"description": "Swarm category for this agent"
|
|
49
|
+
},
|
|
50
|
+
"capabilities": {
|
|
51
|
+
"type": "array",
|
|
52
|
+
"items": {
|
|
53
|
+
"type": "string"
|
|
54
|
+
},
|
|
55
|
+
"description": "List of capabilities this agent provides"
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"additionalProperties": false
|
|
59
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "Integration Plugin Schema",
|
|
4
|
+
"description": "Schema for custom integration plugins in Loki Mode",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["type", "name", "description", "webhook_url", "events"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"type": {
|
|
9
|
+
"const": "integration"
|
|
10
|
+
},
|
|
11
|
+
"name": {
|
|
12
|
+
"type": "string",
|
|
13
|
+
"pattern": "^[a-z][a-z0-9-]{2,49}$",
|
|
14
|
+
"description": "Unique integration name (lowercase, 3-50 chars, alphanumeric and hyphens)"
|
|
15
|
+
},
|
|
16
|
+
"description": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"maxLength": 500,
|
|
19
|
+
"description": "Human-readable description of the integration"
|
|
20
|
+
},
|
|
21
|
+
"webhook_url": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"format": "uri",
|
|
24
|
+
"description": "URL to POST event payloads to"
|
|
25
|
+
},
|
|
26
|
+
"events": {
|
|
27
|
+
"type": "array",
|
|
28
|
+
"items": {
|
|
29
|
+
"type": "string"
|
|
30
|
+
},
|
|
31
|
+
"minItems": 1,
|
|
32
|
+
"description": "List of event types to subscribe to"
|
|
33
|
+
},
|
|
34
|
+
"payload_template": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"maxLength": 5000,
|
|
37
|
+
"description": "JSON template for the webhook payload (supports {{event.*}} variables)"
|
|
38
|
+
},
|
|
39
|
+
"headers": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"additionalProperties": {
|
|
42
|
+
"type": "string"
|
|
43
|
+
},
|
|
44
|
+
"description": "Additional HTTP headers to send with webhook requests"
|
|
45
|
+
},
|
|
46
|
+
"timeout_ms": {
|
|
47
|
+
"type": "integer",
|
|
48
|
+
"minimum": 1000,
|
|
49
|
+
"maximum": 30000,
|
|
50
|
+
"default": 5000,
|
|
51
|
+
"description": "HTTP request timeout in milliseconds"
|
|
52
|
+
},
|
|
53
|
+
"retry_count": {
|
|
54
|
+
"type": "integer",
|
|
55
|
+
"minimum": 0,
|
|
56
|
+
"maximum": 3,
|
|
57
|
+
"default": 1,
|
|
58
|
+
"description": "Number of retry attempts on failure"
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"additionalProperties": false
|
|
62
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "MCP Tool Plugin Schema",
|
|
4
|
+
"description": "Schema for custom MCP tool plugins in Loki Mode",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["type", "name", "description", "command"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"type": {
|
|
9
|
+
"const": "mcp_tool"
|
|
10
|
+
},
|
|
11
|
+
"name": {
|
|
12
|
+
"type": "string",
|
|
13
|
+
"pattern": "^[a-z][a-z0-9-]{2,49}$",
|
|
14
|
+
"description": "Unique tool name (lowercase, 3-50 chars, alphanumeric and hyphens)"
|
|
15
|
+
},
|
|
16
|
+
"description": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"maxLength": 500,
|
|
19
|
+
"description": "Human-readable description of the MCP tool"
|
|
20
|
+
},
|
|
21
|
+
"command": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"maxLength": 1000,
|
|
24
|
+
"description": "Command to execute when tool is invoked"
|
|
25
|
+
},
|
|
26
|
+
"parameters": {
|
|
27
|
+
"type": "array",
|
|
28
|
+
"items": {
|
|
29
|
+
"type": "object",
|
|
30
|
+
"required": ["name", "type"],
|
|
31
|
+
"properties": {
|
|
32
|
+
"name": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"description": "Parameter name"
|
|
35
|
+
},
|
|
36
|
+
"type": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"enum": ["string", "number", "boolean"],
|
|
39
|
+
"description": "Parameter type"
|
|
40
|
+
},
|
|
41
|
+
"description": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"description": "Parameter description"
|
|
44
|
+
},
|
|
45
|
+
"required": {
|
|
46
|
+
"type": "boolean",
|
|
47
|
+
"default": false,
|
|
48
|
+
"description": "Whether the parameter is required"
|
|
49
|
+
},
|
|
50
|
+
"default": {
|
|
51
|
+
"description": "Default value for the parameter"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"additionalProperties": false
|
|
55
|
+
},
|
|
56
|
+
"description": "Tool input parameters"
|
|
57
|
+
},
|
|
58
|
+
"timeout_ms": {
|
|
59
|
+
"type": "integer",
|
|
60
|
+
"minimum": 1000,
|
|
61
|
+
"maximum": 120000,
|
|
62
|
+
"default": 30000,
|
|
63
|
+
"description": "Maximum execution time in milliseconds"
|
|
64
|
+
},
|
|
65
|
+
"working_directory": {
|
|
66
|
+
"type": "string",
|
|
67
|
+
"enum": ["project", "plugin"],
|
|
68
|
+
"default": "project",
|
|
69
|
+
"description": "Working directory for command execution"
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"additionalProperties": false
|
|
73
|
+
}
|