twinclaw 1.1.1 → 1.1.2
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 +3 -3
- package/dist/api/handlers/agents.js +82 -0
- package/dist/api/handlers/debug.js +69 -0
- package/dist/api/handlers/devices.js +79 -0
- package/dist/api/handlers/jobs.js +99 -0
- package/dist/api/handlers/status.js +149 -0
- package/dist/api/router.js +272 -2
- package/dist/api/runtime-event-producer.js +20 -0
- package/dist/api/shared.js +34 -0
- package/dist/api/websocket-hub.js +18 -7
- package/dist/config/json-config.js +393 -1
- package/dist/core/heartbeat.js +304 -8
- package/dist/core/lane-executor.js +18 -0
- package/dist/core/onboarding.js +2 -2
- package/dist/core/self-improve-cli.js +212 -0
- package/dist/core/simplified-onboarding.js +158 -16
- package/dist/index.js +61 -33
- package/dist/interfaces/dispatcher.js +4 -2
- package/dist/interfaces/whatsapp_handler.js +243 -2
- package/dist/services/auto-configurer.js +591 -0
- package/dist/services/device-pairing.js +378 -0
- package/dist/services/hooks.js +130 -0
- package/dist/services/job-scheduler.js +133 -1
- package/dist/services/learning-system.js +226 -0
- package/dist/services/self-healing.js +267 -0
- package/dist/services/skill-acquisition/intent-parser.js +115 -0
- package/dist/services/skill-acquisition/research-engine.js +319 -0
- package/dist/services/skill-builder.js +235 -0
- package/dist/services/sub-agent-service.js +215 -0
- package/dist/services/web-service.js +70 -0
- package/dist/services/webhook-service.js +127 -0
- package/dist/skills/builtin.js +827 -0
- package/dist/tools/agent-improvement.js +208 -0
- package/dist/types/skill-acquisition.js +4 -0
- package/package.json +3 -1
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { logThought } from '../utils/logger.js';
|
|
2
|
+
import { writeFile, readFile, mkdir, readdir } from 'fs/promises';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { getWorkspaceDir } from '../config/workspace.js';
|
|
6
|
+
class LearningSystem {
|
|
7
|
+
#memoryDir;
|
|
8
|
+
#memories = new Map();
|
|
9
|
+
#maxMemories = 1000;
|
|
10
|
+
constructor() {
|
|
11
|
+
this.#memoryDir = '';
|
|
12
|
+
}
|
|
13
|
+
async initialize() {
|
|
14
|
+
this.#memoryDir = path.join(getWorkspaceDir(), 'memory');
|
|
15
|
+
if (!existsSync(this.#memoryDir)) {
|
|
16
|
+
await mkdir(this.#memoryDir, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
await this.#loadMemories();
|
|
19
|
+
void logThought(`[Learning] Initialized with ${this.#memories.size} memories`);
|
|
20
|
+
}
|
|
21
|
+
async #loadMemories() {
|
|
22
|
+
try {
|
|
23
|
+
const files = await readdir(this.#memoryDir);
|
|
24
|
+
for (const file of files) {
|
|
25
|
+
if (file.endsWith('.json')) {
|
|
26
|
+
const content = await readFile(path.join(this.#memoryDir, file), 'utf-8');
|
|
27
|
+
const memory = JSON.parse(content);
|
|
28
|
+
this.#memories.set(memory.id, memory);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
void logThought(`[Learning] No existing memories found`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async learn(type, description, context, solution, outcome) {
|
|
37
|
+
const existing = this.#findSimilar(description);
|
|
38
|
+
let entry;
|
|
39
|
+
if (existing && existing.description === description) {
|
|
40
|
+
existing.timesApplied++;
|
|
41
|
+
if (outcome) {
|
|
42
|
+
const newRate = (existing.successRate * (existing.timesApplied - 1) + (outcome === 'success' ? 1 : 0)) / existing.timesApplied;
|
|
43
|
+
existing.successRate = newRate;
|
|
44
|
+
}
|
|
45
|
+
existing.outcome = outcome;
|
|
46
|
+
existing.timestamp = new Date();
|
|
47
|
+
entry = existing;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
entry = {
|
|
51
|
+
id: `mem_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
52
|
+
type,
|
|
53
|
+
description,
|
|
54
|
+
context,
|
|
55
|
+
solution,
|
|
56
|
+
outcome,
|
|
57
|
+
timestamp: new Date(),
|
|
58
|
+
relevance: this.#calculateRelevance(context),
|
|
59
|
+
timesApplied: 1,
|
|
60
|
+
successRate: outcome === 'success' ? 1 : 0
|
|
61
|
+
};
|
|
62
|
+
this.#memories.set(entry.id, entry);
|
|
63
|
+
}
|
|
64
|
+
await this.#saveMemory(entry);
|
|
65
|
+
void logThought(`[Learning] Learned: ${type} - ${description} (success rate: ${entry.successRate.toFixed(1)})`);
|
|
66
|
+
if (this.#memories.size > this.#maxMemories) {
|
|
67
|
+
await this.#pruneMemories();
|
|
68
|
+
}
|
|
69
|
+
return entry;
|
|
70
|
+
}
|
|
71
|
+
#findSimilar(description) {
|
|
72
|
+
const searchTerms = description.toLowerCase().split(/\s+/);
|
|
73
|
+
let bestMatch;
|
|
74
|
+
let bestScore = 0;
|
|
75
|
+
for (const memory of this.#memories.values()) {
|
|
76
|
+
const memTerms = memory.description.toLowerCase().split(/\s+/);
|
|
77
|
+
let score = 0;
|
|
78
|
+
for (const term of searchTerms) {
|
|
79
|
+
if (memTerms.includes(term)) {
|
|
80
|
+
score++;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (score > bestScore) {
|
|
84
|
+
bestScore = score;
|
|
85
|
+
bestMatch = memory;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return bestScore > 2 ? bestMatch : undefined;
|
|
89
|
+
}
|
|
90
|
+
#calculateRelevance(context) {
|
|
91
|
+
let score = 0;
|
|
92
|
+
if (context.component)
|
|
93
|
+
score += 2;
|
|
94
|
+
if (context.action)
|
|
95
|
+
score += 1;
|
|
96
|
+
if (context.result)
|
|
97
|
+
score += 1;
|
|
98
|
+
return Math.min(score / 4, 1);
|
|
99
|
+
}
|
|
100
|
+
async #saveMemory(memory) {
|
|
101
|
+
const filePath = path.join(this.#memoryDir, `${memory.id}.json`);
|
|
102
|
+
await writeFile(filePath, JSON.stringify(memory, null, 2), 'utf-8');
|
|
103
|
+
}
|
|
104
|
+
async #pruneMemories() {
|
|
105
|
+
const sorted = Array.from(this.#memories.values())
|
|
106
|
+
.sort((a, b) => {
|
|
107
|
+
const aScore = a.relevance * (a.successRate + 0.5);
|
|
108
|
+
const bScore = b.relevance * (b.successRate + 0.5);
|
|
109
|
+
return bScore - aScore;
|
|
110
|
+
});
|
|
111
|
+
const toKeep = sorted.slice(0, this.#maxMemories / 2);
|
|
112
|
+
const toRemove = sorted.slice(this.#maxMemories / 2);
|
|
113
|
+
for (const memory of toRemove) {
|
|
114
|
+
this.#memories.delete(memory.id);
|
|
115
|
+
try {
|
|
116
|
+
const { unlink } = await import('fs/promises');
|
|
117
|
+
await unlink(path.join(this.#memoryDir, `${memory.id}.json`));
|
|
118
|
+
}
|
|
119
|
+
catch { }
|
|
120
|
+
}
|
|
121
|
+
void logThought(`[Learning] Pruned memories, kept ${toKeep.length} most relevant`);
|
|
122
|
+
}
|
|
123
|
+
query(query, context) {
|
|
124
|
+
const searchTerms = query.toLowerCase().split(/\s+/);
|
|
125
|
+
const scored = Array.from(this.#memories.values())
|
|
126
|
+
.map(memory => {
|
|
127
|
+
let score = 0;
|
|
128
|
+
const descTerms = memory.description.toLowerCase().split(/\s+/);
|
|
129
|
+
for (const term of searchTerms) {
|
|
130
|
+
if (descTerms.includes(term))
|
|
131
|
+
score += 3;
|
|
132
|
+
}
|
|
133
|
+
if (memory.type === 'fix')
|
|
134
|
+
score *= 1.5;
|
|
135
|
+
if (memory.successRate > 0.7)
|
|
136
|
+
score *= 1.2;
|
|
137
|
+
if (context?.component) {
|
|
138
|
+
if (memory.context.component === context.component) {
|
|
139
|
+
score *= 1.5;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return { memory, score };
|
|
143
|
+
})
|
|
144
|
+
.filter(r => r.score > 0)
|
|
145
|
+
.sort((a, b) => b.score - a.score)
|
|
146
|
+
.slice(0, 10);
|
|
147
|
+
const entries = scored.map(r => r.memory);
|
|
148
|
+
const suggestions = this.#generateSuggestions(entries, context);
|
|
149
|
+
return { entries, suggestions };
|
|
150
|
+
}
|
|
151
|
+
#generateSuggestions(entries, context) {
|
|
152
|
+
const suggestions = [];
|
|
153
|
+
const successfulFixes = entries.filter(e => e.type === 'fix' && e.successRate > 0.5);
|
|
154
|
+
if (successfulFixes.length > 0) {
|
|
155
|
+
suggestions.push(`Try: ${successfulFixes[0].solution || 'review previous solution'}`);
|
|
156
|
+
}
|
|
157
|
+
const patterns = entries.filter(e => e.type === 'pattern');
|
|
158
|
+
if (patterns.length > 0) {
|
|
159
|
+
suggestions.push(`Pattern detected: ${patterns[0].description}`);
|
|
160
|
+
}
|
|
161
|
+
if (entries.length === 0) {
|
|
162
|
+
suggestions.push('No similar experiences found. Consider:');
|
|
163
|
+
suggestions.push(' - Breaking down the problem into smaller parts');
|
|
164
|
+
suggestions.push(' - Checking service documentation');
|
|
165
|
+
suggestions.push(' - Starting with a simpler configuration');
|
|
166
|
+
}
|
|
167
|
+
return suggestions.slice(0, 5);
|
|
168
|
+
}
|
|
169
|
+
getStats() {
|
|
170
|
+
const byType = {};
|
|
171
|
+
let totalSuccess = 0;
|
|
172
|
+
let totalCount = 0;
|
|
173
|
+
for (const memory of this.#memories.values()) {
|
|
174
|
+
byType[memory.type] = (byType[memory.type] || 0) + 1;
|
|
175
|
+
totalSuccess += memory.successRate;
|
|
176
|
+
totalCount++;
|
|
177
|
+
}
|
|
178
|
+
const topPatterns = Array.from(this.#memories.values())
|
|
179
|
+
.filter(m => m.type === 'pattern')
|
|
180
|
+
.sort((a, b) => b.timesApplied - a.timesApplied)
|
|
181
|
+
.slice(0, 5)
|
|
182
|
+
.map(m => m.description);
|
|
183
|
+
return {
|
|
184
|
+
total: this.#memories.size,
|
|
185
|
+
byType,
|
|
186
|
+
successRate: totalCount > 0 ? totalSuccess / totalCount : 0,
|
|
187
|
+
topPatterns
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
getMemoriesByType(type) {
|
|
191
|
+
return Array.from(this.#memories.values())
|
|
192
|
+
.filter(m => m.type === type)
|
|
193
|
+
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
|
194
|
+
}
|
|
195
|
+
async clear(type) {
|
|
196
|
+
let count = 0;
|
|
197
|
+
const toDelete = [];
|
|
198
|
+
for (const [id, memory] of this.#memories.entries()) {
|
|
199
|
+
if (!type || memory.type === type) {
|
|
200
|
+
toDelete.push(id);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
for (const id of toDelete) {
|
|
204
|
+
this.#memories.delete(id);
|
|
205
|
+
try {
|
|
206
|
+
const { unlink } = await import('fs/promises');
|
|
207
|
+
await unlink(path.join(this.#memoryDir, `${id}.json`));
|
|
208
|
+
count++;
|
|
209
|
+
}
|
|
210
|
+
catch { }
|
|
211
|
+
}
|
|
212
|
+
void logThought(`[Learning] Cleared ${count} memories`);
|
|
213
|
+
return count;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
let learningSystem = null;
|
|
217
|
+
export function getLearningSystem() {
|
|
218
|
+
if (!learningSystem) {
|
|
219
|
+
learningSystem = new LearningSystem();
|
|
220
|
+
}
|
|
221
|
+
return learningSystem;
|
|
222
|
+
}
|
|
223
|
+
export async function initializeLearning() {
|
|
224
|
+
const system = getLearningSystem();
|
|
225
|
+
await system.initialize();
|
|
226
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { logThought } from '../utils/logger.js';
|
|
2
|
+
import { readConfig } from '../config/config-loader.js';
|
|
3
|
+
import { exec } from 'child_process';
|
|
4
|
+
import { promisify } from 'util';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
import { getLearningSystem } from './learning-system.js';
|
|
7
|
+
const execAsync = promisify(exec);
|
|
8
|
+
class SelfHealingService {
|
|
9
|
+
#healthMetrics = new Map();
|
|
10
|
+
#healingActions = [];
|
|
11
|
+
#learningHistory = [];
|
|
12
|
+
#healthCheckInterval = null;
|
|
13
|
+
#autoHealEnabled = true;
|
|
14
|
+
async initialize() {
|
|
15
|
+
void logThought('[SelfHealer] Initializing self-healing system...');
|
|
16
|
+
this.#startHealthChecks();
|
|
17
|
+
void logThought('[SelfHealer] Self-healing system initialized');
|
|
18
|
+
}
|
|
19
|
+
#startHealthChecks() {
|
|
20
|
+
this.#healthCheckInterval = setInterval(async () => {
|
|
21
|
+
await this.performHealthCheck();
|
|
22
|
+
}, 60000);
|
|
23
|
+
}
|
|
24
|
+
async performHealthCheck() {
|
|
25
|
+
const metrics = [];
|
|
26
|
+
metrics.push(await this.#checkSystemResources());
|
|
27
|
+
metrics.push(await this.#checkSkills());
|
|
28
|
+
metrics.push(await this.#checkConfig());
|
|
29
|
+
metrics.push(await this.#checkChannels());
|
|
30
|
+
for (const metric of metrics) {
|
|
31
|
+
this.#healthMetrics.set(metric.component, metric);
|
|
32
|
+
if (metric.status === 'failed' && this.#autoHealEnabled) {
|
|
33
|
+
await this.#attemptHealing(metric);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return metrics;
|
|
37
|
+
}
|
|
38
|
+
async #checkSystemResources() {
|
|
39
|
+
const start = Date.now();
|
|
40
|
+
try {
|
|
41
|
+
const cpuLoad = os.loadavg()[0];
|
|
42
|
+
const freeMem = os.freemem();
|
|
43
|
+
const totalMem = os.totalmem();
|
|
44
|
+
const memPercent = ((totalMem - freeMem) / totalMem) * 100;
|
|
45
|
+
let status = 'healthy';
|
|
46
|
+
if (cpuLoad > 8 || memPercent > 90)
|
|
47
|
+
status = 'degraded';
|
|
48
|
+
if (cpuLoad > 16 || memPercent > 95)
|
|
49
|
+
status = 'failed';
|
|
50
|
+
return {
|
|
51
|
+
component: 'system.resources',
|
|
52
|
+
status,
|
|
53
|
+
lastCheck: new Date(),
|
|
54
|
+
responseTime: Date.now() - start,
|
|
55
|
+
details: { cpuLoad, memPercent, freeMem, totalMem }
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
return {
|
|
60
|
+
component: 'system.resources',
|
|
61
|
+
status: 'failed',
|
|
62
|
+
lastCheck: new Date(),
|
|
63
|
+
message: err instanceof Error ? err.message : String(err)
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async #checkSkills() {
|
|
68
|
+
try {
|
|
69
|
+
const learning = getLearningSystem();
|
|
70
|
+
const stats = learning.getStats();
|
|
71
|
+
return {
|
|
72
|
+
component: 'skills',
|
|
73
|
+
status: 'healthy',
|
|
74
|
+
lastCheck: new Date(),
|
|
75
|
+
details: {
|
|
76
|
+
memories: stats.total,
|
|
77
|
+
successRate: stats.successRate
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
return {
|
|
83
|
+
component: 'skills',
|
|
84
|
+
status: 'failed',
|
|
85
|
+
lastCheck: new Date(),
|
|
86
|
+
message: err instanceof Error ? err.message : String(err)
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async #checkConfig() {
|
|
91
|
+
try {
|
|
92
|
+
const config = await readConfig();
|
|
93
|
+
const hasApiSecret = !!config.runtime?.apiSecret;
|
|
94
|
+
const hasModelKey = !!(config.models?.openRouterApiKey || config.models?.modalApiKey || config.models?.geminiApiKey);
|
|
95
|
+
const hasChannels = !!(config.messaging?.telegram?.botToken || config.messaging?.whatsapp?.phoneNumber);
|
|
96
|
+
let status = 'healthy';
|
|
97
|
+
if (!hasApiSecret)
|
|
98
|
+
status = 'failed';
|
|
99
|
+
else if (!hasModelKey || !hasChannels)
|
|
100
|
+
status = 'degraded';
|
|
101
|
+
return {
|
|
102
|
+
component: 'config',
|
|
103
|
+
status,
|
|
104
|
+
lastCheck: new Date(),
|
|
105
|
+
details: { hasApiSecret, hasModelKey, hasChannels }
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
return {
|
|
110
|
+
component: 'config',
|
|
111
|
+
status: 'failed',
|
|
112
|
+
lastCheck: new Date(),
|
|
113
|
+
message: err instanceof Error ? err.message : String(err)
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async #checkChannels() {
|
|
118
|
+
try {
|
|
119
|
+
const config = await readConfig();
|
|
120
|
+
const hasTelegram = !!(config.messaging?.telegram?.botToken);
|
|
121
|
+
const hasWhatsApp = !!(config.messaging?.whatsapp?.phoneNumber);
|
|
122
|
+
const hasAnyChannel = hasTelegram || hasWhatsApp;
|
|
123
|
+
return {
|
|
124
|
+
component: 'channels',
|
|
125
|
+
status: hasAnyChannel ? 'healthy' : 'degraded',
|
|
126
|
+
lastCheck: new Date(),
|
|
127
|
+
details: {
|
|
128
|
+
telegram: hasTelegram,
|
|
129
|
+
whatsapp: hasWhatsApp
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
return {
|
|
135
|
+
component: 'channels',
|
|
136
|
+
status: 'failed',
|
|
137
|
+
lastCheck: new Date(),
|
|
138
|
+
message: err instanceof Error ? err.message : String(err)
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async #attemptHealing(metric) {
|
|
143
|
+
void logThought(`[SelfHealer] Attempting to heal: ${metric.component}`);
|
|
144
|
+
const action = {
|
|
145
|
+
id: `${metric.component}_${Date.now()}`,
|
|
146
|
+
description: `Auto-heal ${metric.component}`,
|
|
147
|
+
executed: false,
|
|
148
|
+
success: false,
|
|
149
|
+
timestamp: new Date()
|
|
150
|
+
};
|
|
151
|
+
try {
|
|
152
|
+
switch (metric.component) {
|
|
153
|
+
case 'system.resources':
|
|
154
|
+
await this.#healSystemResources(metric);
|
|
155
|
+
break;
|
|
156
|
+
case 'skills':
|
|
157
|
+
await this.#healSkills(metric);
|
|
158
|
+
break;
|
|
159
|
+
case 'config':
|
|
160
|
+
await this.#healConfig(metric);
|
|
161
|
+
break;
|
|
162
|
+
default:
|
|
163
|
+
void logThought(`[SelfHealer] No healing action for: ${metric.component}`);
|
|
164
|
+
}
|
|
165
|
+
action.executed = true;
|
|
166
|
+
action.success = true;
|
|
167
|
+
action.result = 'Healing attempted';
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
action.success = false;
|
|
171
|
+
action.result = err instanceof Error ? err.message : String(err);
|
|
172
|
+
void logThought(`[SelfHealer] Healing failed: ${action.result}`);
|
|
173
|
+
}
|
|
174
|
+
this.#healingActions.push(action);
|
|
175
|
+
}
|
|
176
|
+
async #healSystemResources(metric) {
|
|
177
|
+
const details = metric.details;
|
|
178
|
+
if (details?.memPercent && details.memPercent > 90) {
|
|
179
|
+
void logThought('[SelfHealer] High memory detected, attempting cleanup...');
|
|
180
|
+
try {
|
|
181
|
+
await execAsync('powershell -Command "Get-Process | Sort-Object WorkingSet64 -Descending | Select-Object -First 10 | Stop-Process -Force"');
|
|
182
|
+
void logThought('[SelfHealer] Cleaned up top memory consumers');
|
|
183
|
+
}
|
|
184
|
+
catch { }
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async #healSkills(metric) {
|
|
188
|
+
void logThought('[SelfHealer] Checking learning system for skill issues...');
|
|
189
|
+
const learning = getLearningSystem();
|
|
190
|
+
const stats = learning.getStats();
|
|
191
|
+
if (stats.total > 0 && stats.successRate < 0.7) {
|
|
192
|
+
void logThought(`[SelfHealer] Low success rate detected: ${(stats.successRate * 100).toFixed(1)}%`);
|
|
193
|
+
const query = learning.query('skill failure');
|
|
194
|
+
if (query.suggestions.length > 0) {
|
|
195
|
+
void logThought(`[SelfHealer] Suggested fix: ${query.suggestions[0]}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
async #healConfig(metric) {
|
|
200
|
+
const details = metric.details;
|
|
201
|
+
if (!details?.hasApiSecret) {
|
|
202
|
+
void logThought('[SelfHealer] Missing API secret, user needs to configure');
|
|
203
|
+
}
|
|
204
|
+
const learning = getLearningSystem();
|
|
205
|
+
const fixes = this.suggestFixes('config');
|
|
206
|
+
if (fixes.length > 0) {
|
|
207
|
+
void logThought(`[SelfHealer] Known fixes: ${fixes[0]}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
learn(pattern, success, context, solution) {
|
|
211
|
+
this.#learningHistory.push({
|
|
212
|
+
pattern,
|
|
213
|
+
success,
|
|
214
|
+
timestamp: new Date(),
|
|
215
|
+
context,
|
|
216
|
+
solution
|
|
217
|
+
});
|
|
218
|
+
if (this.#learningHistory.length > 1000) {
|
|
219
|
+
this.#learningHistory = this.#learningHistory.slice(-500);
|
|
220
|
+
}
|
|
221
|
+
void logThought(`[SelfHealer] Learned: ${pattern} (success: ${success})`);
|
|
222
|
+
}
|
|
223
|
+
getSimilarPastIssues(currentIssue) {
|
|
224
|
+
return this.#learningHistory
|
|
225
|
+
.filter(l => l.pattern.toLowerCase().includes(currentIssue.toLowerCase()))
|
|
226
|
+
.slice(-5);
|
|
227
|
+
}
|
|
228
|
+
suggestFixes(issue) {
|
|
229
|
+
const similar = this.getSimilarPastIssues(issue);
|
|
230
|
+
const fixes = similar
|
|
231
|
+
.filter(l => l.success && l.solution)
|
|
232
|
+
.map(l => l.solution);
|
|
233
|
+
return [...new Set(fixes)];
|
|
234
|
+
}
|
|
235
|
+
getHealthMetrics() {
|
|
236
|
+
return Array.from(this.#healthMetrics.values());
|
|
237
|
+
}
|
|
238
|
+
getHealingHistory() {
|
|
239
|
+
return this.#healingActions.slice(-50);
|
|
240
|
+
}
|
|
241
|
+
enableAutoHeal(enabled) {
|
|
242
|
+
this.#autoHealEnabled = enabled;
|
|
243
|
+
void logThought(`[SelfHealer] Auto-heal ${enabled ? 'enabled' : 'disabled'}`);
|
|
244
|
+
}
|
|
245
|
+
shutdown() {
|
|
246
|
+
if (this.#healthCheckInterval) {
|
|
247
|
+
clearInterval(this.#healthCheckInterval);
|
|
248
|
+
}
|
|
249
|
+
void logThought('[SelfHealer] Shutdown complete');
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
let selfHealingService = null;
|
|
253
|
+
export function getSelfHealingService() {
|
|
254
|
+
if (!selfHealingService) {
|
|
255
|
+
selfHealingService = new SelfHealingService();
|
|
256
|
+
}
|
|
257
|
+
return selfHealingService;
|
|
258
|
+
}
|
|
259
|
+
export async function initializeSelfHealer() {
|
|
260
|
+
const service = getSelfHealingService();
|
|
261
|
+
await service.initialize();
|
|
262
|
+
}
|
|
263
|
+
export async function shutdownSelfHealer() {
|
|
264
|
+
if (selfHealingService) {
|
|
265
|
+
selfHealingService.shutdown();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intent Parser Service
|
|
3
|
+
* Converts natural language skill requests into structured requirements
|
|
4
|
+
* Simplified version without direct LLM dependency
|
|
5
|
+
*/
|
|
6
|
+
import { logThought } from '../../utils/logger.js';
|
|
7
|
+
// Known provider -> credentials mapping
|
|
8
|
+
const PROVIDER_CREDENTIALS = {
|
|
9
|
+
'sendgrid': [{ name: 'SENDGRID_API_KEY', type: 'api_key', description: 'SendGrid API Key', envVarHint: 'SENDGRID_API_KEY', isRequired: true }],
|
|
10
|
+
'twilio': [{ name: 'TWILIO_ACCOUNT_SID', type: 'api_key', description: 'Twilio Account SID', envVarHint: 'TWILIO_ACCOUNT_SID', isRequired: true }],
|
|
11
|
+
'slack': [{ name: 'SLACK_BOT_TOKEN', type: 'bearer_token', description: 'Slack Bot Token', envVarHint: 'SLACK_BOT_TOKEN', isRequired: true }],
|
|
12
|
+
'github': [{ name: 'GITHUB_PERSONAL_ACCESS_TOKEN', type: 'bearer_token', description: 'GitHub PAT', envVarHint: 'GITHUB_PERSONAL_ACCESS_TOKEN', isRequired: true }],
|
|
13
|
+
'notion': [{ name: 'NOTION_API_KEY', type: 'api_key', description: 'Notion API Key', envVarHint: 'NOTION_API_KEY', isRequired: true }],
|
|
14
|
+
'discord': [{ name: 'DISCORD_BOT_TOKEN', type: 'bearer_token', description: 'Discord Bot Token', envVarHint: 'DISCORD_BOT_TOKEN', isRequired: true }],
|
|
15
|
+
'openai': [{ name: 'OPENAI_API_KEY', type: 'api_key', description: 'OpenAI API Key', envVarHint: 'OPENAI_API_KEY', isRequired: true }],
|
|
16
|
+
'aws': [{ name: 'AWS_ACCESS_KEY_ID', type: 'api_key', description: 'AWS Access Key', envVarHint: 'AWS_ACCESS_KEY_ID', isRequired: true }],
|
|
17
|
+
};
|
|
18
|
+
// Capability keywords -> suggested approach
|
|
19
|
+
const CAPABILITY_APPROACHES = {
|
|
20
|
+
'file': [{ approach: 'mcp-bridge', confidence: 0.9, estimatedEffort: 'low' }],
|
|
21
|
+
'filesystem': [{ approach: 'mcp-bridge', confidence: 0.9, estimatedEffort: 'low' }],
|
|
22
|
+
'memory': [{ approach: 'mcp-bridge', confidence: 0.9, estimatedEffort: 'low' }],
|
|
23
|
+
'search': [{ approach: 'mcp-bridge', confidence: 0.8, estimatedEffort: 'low' }],
|
|
24
|
+
'database': [{ approach: 'mcp-bridge', confidence: 0.8, estimatedEffort: 'low' }],
|
|
25
|
+
'sql': [{ approach: 'mcp-bridge', confidence: 0.8, estimatedEffort: 'low' }],
|
|
26
|
+
};
|
|
27
|
+
export class IntentParser {
|
|
28
|
+
async parseIntent(userRequest) {
|
|
29
|
+
void logThought(`[IntentParser] Parsing: "${userRequest}"`);
|
|
30
|
+
const lower = userRequest.toLowerCase();
|
|
31
|
+
// Extract capability
|
|
32
|
+
const capability = this.#extractCapability(lower);
|
|
33
|
+
// Extract provider
|
|
34
|
+
const provider = this.#extractProvider(lower);
|
|
35
|
+
// Get required credentials
|
|
36
|
+
const requiredCredentials = provider ? (PROVIDER_CREDENTIALS[provider] || []) : [];
|
|
37
|
+
// Get suggested approaches
|
|
38
|
+
const suggestedApproaches = this.#getSuggestedApproaches(lower);
|
|
39
|
+
// Estimate complexity
|
|
40
|
+
const estimatedComplexity = this.#estimateComplexity(lower);
|
|
41
|
+
void logThought(`[IntentParser] Parsed: capability=${capability}, provider=${provider || 'none'}, complexity=${estimatedComplexity}`);
|
|
42
|
+
return {
|
|
43
|
+
capability,
|
|
44
|
+
provider,
|
|
45
|
+
description: userRequest,
|
|
46
|
+
requiredCredentials,
|
|
47
|
+
requiredScopes: [],
|
|
48
|
+
estimatedComplexity,
|
|
49
|
+
similarSkills: [],
|
|
50
|
+
suggestedApproaches,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
#extractCapability(request) {
|
|
54
|
+
// Common capability keywords
|
|
55
|
+
const keywords = {
|
|
56
|
+
'email-send': ['email', 'mail', 'sendgrid', 'smtp'],
|
|
57
|
+
'sms-send': ['sms', 'text message', 'twilio'],
|
|
58
|
+
'file-management': ['file', 'upload', 'download', 'read file', 'write file'],
|
|
59
|
+
'api-call': ['api', 'http', 'fetch', 'rest'],
|
|
60
|
+
'database-query': ['database', 'db', 'sql', 'query'],
|
|
61
|
+
'search': ['search', 'find', 'lookup'],
|
|
62
|
+
'calendar': ['calendar', 'schedule', 'event'],
|
|
63
|
+
'note-taking': ['note', 'notion', 'obsidian', 'notes'],
|
|
64
|
+
'messaging': ['message', 'slack', 'discord', 'telegram'],
|
|
65
|
+
'github': ['github', 'repo', 'git'],
|
|
66
|
+
'browser': ['browser', 'scrape', 'web'],
|
|
67
|
+
'ai': ['ai', 'openai', 'gpt', 'llm'],
|
|
68
|
+
};
|
|
69
|
+
for (const [cap, words] of Object.entries(keywords)) {
|
|
70
|
+
if (words.some(w => request.includes(w))) {
|
|
71
|
+
return cap;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Default: extract from words
|
|
75
|
+
const words = request.split(/\s+/).filter(w => w.length > 3).slice(0, 3);
|
|
76
|
+
return words.join('-') || 'custom';
|
|
77
|
+
}
|
|
78
|
+
#extractProvider(request) {
|
|
79
|
+
const providers = Object.keys(PROVIDER_CREDENTIALS);
|
|
80
|
+
for (const p of providers) {
|
|
81
|
+
if (request.includes(p)) {
|
|
82
|
+
return p;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
#estimateComplexity(request) {
|
|
88
|
+
const complexIndicators = ['workflow', 'multiple', 'chain', 'complex', 'orchestrate', 'parallel', 'multi-step'];
|
|
89
|
+
const mediumIndicators = ['api', 'fetch', 'database', 'transform', 'convert'];
|
|
90
|
+
if (complexIndicators.some(w => request.includes(w)))
|
|
91
|
+
return 'complex';
|
|
92
|
+
if (mediumIndicators.some(w => request.includes(w)))
|
|
93
|
+
return 'medium';
|
|
94
|
+
return 'simple';
|
|
95
|
+
}
|
|
96
|
+
#getSuggestedApproaches(request) {
|
|
97
|
+
for (const [keyword, approaches] of Object.entries(CAPABILITY_APPROACHES)) {
|
|
98
|
+
if (request.includes(keyword)) {
|
|
99
|
+
return approaches;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Default approach: function-based
|
|
103
|
+
return [
|
|
104
|
+
{ approach: 'function', confidence: 0.7, estimatedEffort: 'medium' },
|
|
105
|
+
];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Singleton
|
|
109
|
+
let intentParser = null;
|
|
110
|
+
export function getIntentParser() {
|
|
111
|
+
if (!intentParser) {
|
|
112
|
+
intentParser = new IntentParser();
|
|
113
|
+
}
|
|
114
|
+
return intentParser;
|
|
115
|
+
}
|