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,153 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execFile } = require('child_process');
|
|
4
|
+
const { join } = require('path');
|
|
5
|
+
|
|
6
|
+
// In-memory registry for custom gate plugins
|
|
7
|
+
const _registeredGates = new Map();
|
|
8
|
+
|
|
9
|
+
class GatePlugin {
|
|
10
|
+
/**
|
|
11
|
+
* Register a custom quality gate plugin.
|
|
12
|
+
*
|
|
13
|
+
* @param {object} pluginConfig - Validated quality gate plugin config
|
|
14
|
+
* @returns {{ success: boolean, error?: string }}
|
|
15
|
+
*/
|
|
16
|
+
static register(pluginConfig) {
|
|
17
|
+
if (!pluginConfig || pluginConfig.type !== 'quality_gate') {
|
|
18
|
+
return { success: false, error: 'Invalid plugin config: type must be "quality_gate"' };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const name = pluginConfig.name;
|
|
22
|
+
|
|
23
|
+
if (_registeredGates.has(name)) {
|
|
24
|
+
return { success: false, error: `Gate plugin "${name}" is already registered` };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const gateDef = {
|
|
28
|
+
name: name,
|
|
29
|
+
description: pluginConfig.description,
|
|
30
|
+
phase: pluginConfig.phase || 'pre-commit',
|
|
31
|
+
command: pluginConfig.command,
|
|
32
|
+
timeout_ms: pluginConfig.timeout_ms || 30000,
|
|
33
|
+
blocking: pluginConfig.blocking !== undefined ? pluginConfig.blocking : true,
|
|
34
|
+
severity: pluginConfig.severity || 'high',
|
|
35
|
+
registered_at: new Date().toISOString(),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
_registeredGates.set(name, gateDef);
|
|
39
|
+
return { success: true };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Unregister a custom quality gate plugin.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} pluginName - Name of the gate plugin to remove
|
|
46
|
+
* @returns {{ success: boolean, error?: string }}
|
|
47
|
+
*/
|
|
48
|
+
static unregister(pluginName) {
|
|
49
|
+
if (!_registeredGates.has(pluginName)) {
|
|
50
|
+
return { success: false, error: `Gate plugin "${pluginName}" is not registered` };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_registeredGates.delete(pluginName);
|
|
54
|
+
return { success: true };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Execute a quality gate command.
|
|
59
|
+
*
|
|
60
|
+
* @param {object} pluginConfig - The gate plugin config
|
|
61
|
+
* @param {string} projectDir - Project directory to run the command in
|
|
62
|
+
* @returns {Promise<{ passed: boolean, output: string, duration_ms: number }>}
|
|
63
|
+
*/
|
|
64
|
+
static async execute(pluginConfig, projectDir) {
|
|
65
|
+
const command = pluginConfig.command;
|
|
66
|
+
const timeoutMs = pluginConfig.timeout_ms || 30000;
|
|
67
|
+
const startTime = Date.now();
|
|
68
|
+
|
|
69
|
+
if (!command) {
|
|
70
|
+
return {
|
|
71
|
+
passed: false,
|
|
72
|
+
output: 'Error: no command specified for quality gate',
|
|
73
|
+
duration_ms: 0,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
const cwd = projectDir || process.cwd();
|
|
79
|
+
|
|
80
|
+
// Split command into executable and args
|
|
81
|
+
// Use shell execution for simple commands
|
|
82
|
+
const child = execFile('/bin/sh', ['-c', command], {
|
|
83
|
+
cwd,
|
|
84
|
+
timeout: timeoutMs,
|
|
85
|
+
maxBuffer: 1024 * 1024, // 1MB
|
|
86
|
+
env: { ...process.env, LOKI_GATE: pluginConfig.name || 'unknown' },
|
|
87
|
+
}, (error, stdout, stderr) => {
|
|
88
|
+
const durationMs = Date.now() - startTime;
|
|
89
|
+
const output = (stdout || '') + (stderr ? '\n' + stderr : '');
|
|
90
|
+
|
|
91
|
+
if (error) {
|
|
92
|
+
if (error.killed || error.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER') {
|
|
93
|
+
resolve({
|
|
94
|
+
passed: false,
|
|
95
|
+
output: `Timeout: command exceeded ${timeoutMs}ms limit`,
|
|
96
|
+
duration_ms: durationMs,
|
|
97
|
+
});
|
|
98
|
+
} else {
|
|
99
|
+
resolve({
|
|
100
|
+
passed: false,
|
|
101
|
+
output: output.trim() || error.message || 'Command failed',
|
|
102
|
+
duration_ms: durationMs,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
resolve({
|
|
107
|
+
passed: true,
|
|
108
|
+
output: output.trim(),
|
|
109
|
+
duration_ms: durationMs,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get all gates registered for a specific phase.
|
|
118
|
+
*
|
|
119
|
+
* @param {string} phase - SDLC phase
|
|
120
|
+
* @returns {object[]} Array of gate definitions
|
|
121
|
+
*/
|
|
122
|
+
static getByPhase(phase) {
|
|
123
|
+
return Array.from(_registeredGates.values()).filter(g => g.phase === phase);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* List all registered gate plugins.
|
|
128
|
+
*
|
|
129
|
+
* @returns {object[]} Array of gate definitions
|
|
130
|
+
*/
|
|
131
|
+
static listRegistered() {
|
|
132
|
+
return Array.from(_registeredGates.values());
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Check if a gate is registered.
|
|
137
|
+
*
|
|
138
|
+
* @param {string} name - Gate name
|
|
139
|
+
* @returns {boolean}
|
|
140
|
+
*/
|
|
141
|
+
static isRegistered(name) {
|
|
142
|
+
return _registeredGates.has(name);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Clear all registered gates (primarily for testing).
|
|
147
|
+
*/
|
|
148
|
+
static _clearAll() {
|
|
149
|
+
_registeredGates.clear();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
module.exports = { GatePlugin };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { PluginValidator, BUILTIN_AGENT_NAMES, VALID_PLUGIN_TYPES } = require('./validator');
|
|
4
|
+
const { PluginLoader, parseSimpleYAML } = require('./loader');
|
|
5
|
+
const { AgentPlugin } = require('./agent-plugin');
|
|
6
|
+
const { GatePlugin } = require('./gate-plugin');
|
|
7
|
+
const { IntegrationPlugin } = require('./integration-plugin');
|
|
8
|
+
const { MCPPlugin } = require('./mcp-plugin');
|
|
9
|
+
|
|
10
|
+
// Plugin type to handler mapping
|
|
11
|
+
const PLUGIN_HANDLERS = {
|
|
12
|
+
agent: AgentPlugin,
|
|
13
|
+
quality_gate: GatePlugin,
|
|
14
|
+
integration: IntegrationPlugin,
|
|
15
|
+
mcp_tool: MCPPlugin,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Initialize the plugin system: discover, load, validate, and register all plugins.
|
|
20
|
+
*
|
|
21
|
+
* @param {string} [pluginsDir='.loki/plugins'] - Path to plugins directory
|
|
22
|
+
* @param {string} [schemasDir] - Path to schemas directory (defaults to built-in schemas)
|
|
23
|
+
* @param {object} [options] - Additional options
|
|
24
|
+
* @param {object} [options.agentRegistry] - External agent registry to register agents into
|
|
25
|
+
* @param {boolean} [options.watch=false] - Watch for file changes
|
|
26
|
+
* @returns {{ loaded: number, failed: number, details: { loaded: object[], failed: object[] } }}
|
|
27
|
+
*/
|
|
28
|
+
function initializePlugins(pluginsDir, schemasDir, options) {
|
|
29
|
+
const opts = options || {};
|
|
30
|
+
const loader = new PluginLoader(pluginsDir, schemasDir);
|
|
31
|
+
const { loaded, failed } = loader.loadAll();
|
|
32
|
+
|
|
33
|
+
const registered = [];
|
|
34
|
+
const registrationErrors = [];
|
|
35
|
+
|
|
36
|
+
for (const plugin of loaded) {
|
|
37
|
+
const handler = PLUGIN_HANDLERS[plugin.config.type];
|
|
38
|
+
if (!handler) {
|
|
39
|
+
registrationErrors.push({
|
|
40
|
+
path: plugin.path,
|
|
41
|
+
errors: [`No handler for plugin type: ${plugin.config.type}`],
|
|
42
|
+
});
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let result;
|
|
47
|
+
if (plugin.config.type === 'agent') {
|
|
48
|
+
result = handler.register(plugin.config, opts.agentRegistry);
|
|
49
|
+
} else {
|
|
50
|
+
result = handler.register(plugin.config);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (result.success) {
|
|
54
|
+
registered.push(plugin);
|
|
55
|
+
} else {
|
|
56
|
+
registrationErrors.push({
|
|
57
|
+
path: plugin.path,
|
|
58
|
+
errors: [result.error],
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Set up file watching if requested
|
|
64
|
+
let stopWatching = null;
|
|
65
|
+
if (opts.watch) {
|
|
66
|
+
stopWatching = loader.watchForChanges((eventType, filePath) => {
|
|
67
|
+
const fs = require('fs');
|
|
68
|
+
const path = require('path');
|
|
69
|
+
|
|
70
|
+
// Handle file deletion
|
|
71
|
+
if (!fs.existsSync(filePath)) {
|
|
72
|
+
const name = path.basename(filePath, path.extname(filePath));
|
|
73
|
+
for (const handler of Object.values(PLUGIN_HANDLERS)) {
|
|
74
|
+
if (handler.unregister) handler.unregister(name);
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const { config, errors } = loader.loadOne(filePath);
|
|
80
|
+
if (config) {
|
|
81
|
+
const handler = PLUGIN_HANDLERS[config.type];
|
|
82
|
+
if (handler) {
|
|
83
|
+
// Only unregister AFTER successful load and validation
|
|
84
|
+
if (handler.unregister) handler.unregister(config.name);
|
|
85
|
+
handler.register(config, opts.agentRegistry);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// If loadOne fails, keep old plugin registered (fail-safe)
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const allFailed = [...failed, ...registrationErrors];
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
loaded: registered.length,
|
|
96
|
+
failed: allFailed.length,
|
|
97
|
+
details: {
|
|
98
|
+
loaded: registered,
|
|
99
|
+
failed: allFailed,
|
|
100
|
+
},
|
|
101
|
+
stopWatching: stopWatching || (() => {}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = {
|
|
106
|
+
PluginValidator,
|
|
107
|
+
PluginLoader,
|
|
108
|
+
AgentPlugin,
|
|
109
|
+
GatePlugin,
|
|
110
|
+
IntegrationPlugin,
|
|
111
|
+
MCPPlugin,
|
|
112
|
+
initializePlugins,
|
|
113
|
+
parseSimpleYAML,
|
|
114
|
+
BUILTIN_AGENT_NAMES,
|
|
115
|
+
VALID_PLUGIN_TYPES,
|
|
116
|
+
};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { request } = require('https');
|
|
4
|
+
const { request: httpRequest } = require('http');
|
|
5
|
+
|
|
6
|
+
// In-memory registry for custom integration plugins
|
|
7
|
+
const _registeredIntegrations = new Map();
|
|
8
|
+
|
|
9
|
+
class IntegrationPlugin {
|
|
10
|
+
/**
|
|
11
|
+
* Register a custom integration plugin.
|
|
12
|
+
*
|
|
13
|
+
* @param {object} pluginConfig - Validated integration plugin config
|
|
14
|
+
* @returns {{ success: boolean, error?: string }}
|
|
15
|
+
*/
|
|
16
|
+
static register(pluginConfig) {
|
|
17
|
+
if (!pluginConfig || pluginConfig.type !== 'integration') {
|
|
18
|
+
return { success: false, error: 'Invalid plugin config: type must be "integration"' };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const name = pluginConfig.name;
|
|
22
|
+
|
|
23
|
+
if (_registeredIntegrations.has(name)) {
|
|
24
|
+
return { success: false, error: `Integration plugin "${name}" is already registered` };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const intDef = {
|
|
28
|
+
name: name,
|
|
29
|
+
description: pluginConfig.description,
|
|
30
|
+
webhook_url: pluginConfig.webhook_url,
|
|
31
|
+
events: pluginConfig.events || [],
|
|
32
|
+
payload_template: pluginConfig.payload_template || '{"event": "{{event.type}}", "message": "{{event.message}}"}',
|
|
33
|
+
headers: pluginConfig.headers || {},
|
|
34
|
+
timeout_ms: pluginConfig.timeout_ms || 5000,
|
|
35
|
+
retry_count: pluginConfig.retry_count || 1,
|
|
36
|
+
registered_at: new Date().toISOString(),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
_registeredIntegrations.set(name, intDef);
|
|
40
|
+
return { success: true };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Unregister a custom integration plugin.
|
|
45
|
+
*
|
|
46
|
+
* @param {string} pluginName - Name of the integration to remove
|
|
47
|
+
* @returns {{ success: boolean, error?: string }}
|
|
48
|
+
*/
|
|
49
|
+
static unregister(pluginName) {
|
|
50
|
+
if (!_registeredIntegrations.has(pluginName)) {
|
|
51
|
+
return { success: false, error: `Integration plugin "${pluginName}" is not registered` };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
_registeredIntegrations.delete(pluginName);
|
|
55
|
+
return { success: true };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Render a template string with event data.
|
|
60
|
+
* Replaces {{event.field}} patterns with actual event values.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} template - Template string
|
|
63
|
+
* @param {object} event - Event data
|
|
64
|
+
* @returns {string} Rendered string
|
|
65
|
+
*/
|
|
66
|
+
static renderTemplate(template, event) {
|
|
67
|
+
if (!template || typeof template !== 'string') return template || '';
|
|
68
|
+
|
|
69
|
+
return template.replace(/\{\{event\.(\w+(?:\.\w+)*)\}\}/g, (match, path) => {
|
|
70
|
+
const parts = path.split('.');
|
|
71
|
+
let value = event;
|
|
72
|
+
for (const part of parts) {
|
|
73
|
+
if (value === null || value === undefined) return '';
|
|
74
|
+
value = value[part];
|
|
75
|
+
}
|
|
76
|
+
if (value === undefined || value === null) return '';
|
|
77
|
+
if (typeof value === 'object') return JSON.stringify(value);
|
|
78
|
+
// JSON-safe escape: handles quotes, backslashes, control chars
|
|
79
|
+
return JSON.stringify(String(value)).slice(1, -1);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Handle an event by sending it to the integration webhook.
|
|
85
|
+
* Fire-and-forget with timeout.
|
|
86
|
+
*
|
|
87
|
+
* @param {object} pluginConfig - The integration plugin config
|
|
88
|
+
* @param {object} event - The event data
|
|
89
|
+
* @returns {Promise<{ sent: boolean, status?: number, error?: string }>}
|
|
90
|
+
*/
|
|
91
|
+
static async handleEvent(pluginConfig, event) {
|
|
92
|
+
const webhookUrl = pluginConfig.webhook_url;
|
|
93
|
+
const timeoutMs = pluginConfig.timeout_ms || 5000;
|
|
94
|
+
const headers = { ...pluginConfig.headers, 'Content-Type': 'application/json' };
|
|
95
|
+
|
|
96
|
+
// Render payload template
|
|
97
|
+
const payload = IntegrationPlugin.renderTemplate(
|
|
98
|
+
pluginConfig.payload_template || '{"event": "{{event.type}}", "message": "{{event.message}}"}',
|
|
99
|
+
event
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
return new Promise((resolve) => {
|
|
103
|
+
try {
|
|
104
|
+
const url = new URL(webhookUrl);
|
|
105
|
+
const isHttps = url.protocol === 'https:';
|
|
106
|
+
const reqFn = isHttps ? request : httpRequest;
|
|
107
|
+
|
|
108
|
+
const options = {
|
|
109
|
+
hostname: url.hostname,
|
|
110
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
111
|
+
path: url.pathname + url.search,
|
|
112
|
+
method: 'POST',
|
|
113
|
+
headers: {
|
|
114
|
+
...headers,
|
|
115
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
116
|
+
},
|
|
117
|
+
timeout: timeoutMs,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const req = reqFn(options, (res) => {
|
|
121
|
+
let body = '';
|
|
122
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
123
|
+
res.on('end', () => {
|
|
124
|
+
resolve({ sent: true, status: res.statusCode });
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
req.on('error', (err) => {
|
|
129
|
+
resolve({ sent: false, error: err.message });
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
req.on('timeout', () => {
|
|
133
|
+
req.destroy();
|
|
134
|
+
resolve({ sent: false, error: `Timeout after ${timeoutMs}ms` });
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
req.write(payload);
|
|
138
|
+
req.end();
|
|
139
|
+
} catch (err) {
|
|
140
|
+
resolve({ sent: false, error: err.message });
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get integrations subscribed to a specific event type.
|
|
147
|
+
*
|
|
148
|
+
* @param {string} eventType - The event type
|
|
149
|
+
* @returns {object[]} Matching integration definitions
|
|
150
|
+
*/
|
|
151
|
+
static getByEvent(eventType) {
|
|
152
|
+
return Array.from(_registeredIntegrations.values()).filter(
|
|
153
|
+
i => i.events.includes(eventType) || i.events.includes('*')
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* List all registered integration plugins.
|
|
159
|
+
*
|
|
160
|
+
* @returns {object[]}
|
|
161
|
+
*/
|
|
162
|
+
static listRegistered() {
|
|
163
|
+
return Array.from(_registeredIntegrations.values());
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Clear all registered integrations (primarily for testing).
|
|
168
|
+
*/
|
|
169
|
+
static _clearAll() {
|
|
170
|
+
_registeredIntegrations.clear();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = { IntegrationPlugin };
|