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
package/dist/core/heartbeat.js
CHANGED
|
@@ -5,35 +5,331 @@ const DEFAULT_CRON = '0 9 * * *';
|
|
|
5
5
|
const DEFAULT_MESSAGE = 'TwinClaw heartbeat: daily proactive check-in.';
|
|
6
6
|
const HEARTBEAT_JOB_ID = 'twinclaw-heartbeat';
|
|
7
7
|
/**
|
|
8
|
-
* Proactive heartbeat service.
|
|
8
|
+
* Proactive heartbeat service with configurable checks.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* Extends the basic HeartbeatService with:
|
|
11
|
+
* - Configurable proactive checks (email, calendar, weather, notifications)
|
|
12
|
+
* - State tracking for each check type
|
|
13
|
+
* - Proactive notification via dispatcher
|
|
14
|
+
* - Persistent state across restarts
|
|
13
15
|
*/
|
|
14
|
-
export class
|
|
16
|
+
export class ProactiveHeartbeatService {
|
|
15
17
|
#scheduler;
|
|
16
18
|
#onHeartbeat;
|
|
17
19
|
#cronExpression;
|
|
18
20
|
#message;
|
|
21
|
+
// Proactive check configuration
|
|
22
|
+
#checks = new Map();
|
|
23
|
+
#customCheckSkills;
|
|
24
|
+
// Notification targets
|
|
25
|
+
#notifyPlatforms;
|
|
26
|
+
#notifyUserIds;
|
|
27
|
+
// Gateway for proactive messages
|
|
28
|
+
#gateway;
|
|
29
|
+
// State management
|
|
30
|
+
#state = {
|
|
31
|
+
checkStates: {},
|
|
32
|
+
totalHeartbeats: 0,
|
|
33
|
+
totalNotifications: 0,
|
|
34
|
+
};
|
|
35
|
+
// State file path
|
|
36
|
+
#statePath = './memory/heartbeat-state.json';
|
|
19
37
|
/**
|
|
20
38
|
* @param onHeartbeat - callback invoked on every heartbeat tick.
|
|
21
39
|
* @param config - optional cron and message overrides.
|
|
22
40
|
* @param scheduler - optional external scheduler instance (for shared management).
|
|
23
|
-
* If omitted, a private scheduler is created internally.
|
|
24
41
|
*/
|
|
42
|
+
constructor(onHeartbeat, options = {}) {
|
|
43
|
+
this.#onHeartbeat = onHeartbeat;
|
|
44
|
+
this.#cronExpression = options.cronExpression ?? getConfigValue('HEARTBEAT_CRON') ?? DEFAULT_CRON;
|
|
45
|
+
this.#message = options.message ?? getConfigValue('HEARTBEAT_MESSAGE') ?? DEFAULT_MESSAGE;
|
|
46
|
+
this.#scheduler = options.scheduler ?? new JobScheduler();
|
|
47
|
+
this.#customCheckSkills = options.customCheckSkills ?? {};
|
|
48
|
+
this.#notifyPlatforms = options.notifyPlatforms ?? ['whatsapp'];
|
|
49
|
+
this.#notifyUserIds = options.notifyUserIds ?? [];
|
|
50
|
+
this.#gateway = options.gateway;
|
|
51
|
+
// Initialize default checks
|
|
52
|
+
this.#initializeDefaultChecks(options.checks);
|
|
53
|
+
// Load persisted state
|
|
54
|
+
this.#loadState();
|
|
55
|
+
}
|
|
56
|
+
#initializeDefaultChecks(checkOptions) {
|
|
57
|
+
const defaultChecks = ['email', 'calendar', 'weather', 'notifications'];
|
|
58
|
+
for (const checkType of defaultChecks) {
|
|
59
|
+
const enabled = checkOptions?.[checkType] ?? getConfigValue(`HEARTBEAT_CHECK_${checkType.toUpperCase()}`) === 'true';
|
|
60
|
+
this.#checks.set(checkType, {
|
|
61
|
+
type: checkType,
|
|
62
|
+
enabled,
|
|
63
|
+
skillId: this.#customCheckSkills[checkType],
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Expose the underlying scheduler so callers can register additional jobs. */
|
|
68
|
+
get scheduler() {
|
|
69
|
+
return this.#scheduler;
|
|
70
|
+
}
|
|
71
|
+
/** Get current heartbeat state */
|
|
72
|
+
get state() {
|
|
73
|
+
return { ...this.#state };
|
|
74
|
+
}
|
|
75
|
+
/** Get check configuration */
|
|
76
|
+
getCheck(type) {
|
|
77
|
+
return this.#checks.get(type);
|
|
78
|
+
}
|
|
79
|
+
/** Enable or disable a specific check */
|
|
80
|
+
setCheckEnabled(type, enabled) {
|
|
81
|
+
const check = this.#checks.get(type);
|
|
82
|
+
if (check) {
|
|
83
|
+
check.enabled = enabled;
|
|
84
|
+
this.#saveState();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** Add a notification target */
|
|
88
|
+
addNotificationTarget(platform, userId) {
|
|
89
|
+
if (!this.#notifyPlatforms.includes(platform)) {
|
|
90
|
+
this.#notifyPlatforms.push(platform);
|
|
91
|
+
}
|
|
92
|
+
if (!this.#notifyUserIds.includes(userId)) {
|
|
93
|
+
this.#notifyUserIds.push(userId);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
start() {
|
|
97
|
+
// Guard against double-start
|
|
98
|
+
if (this.#scheduler.getJob(HEARTBEAT_JOB_ID)) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
this.#scheduler.register({
|
|
102
|
+
id: HEARTBEAT_JOB_ID,
|
|
103
|
+
cronExpression: this.#cronExpression,
|
|
104
|
+
description: this.#message,
|
|
105
|
+
handler: async () => {
|
|
106
|
+
await this.#executeHeartbeat();
|
|
107
|
+
},
|
|
108
|
+
autoStart: true,
|
|
109
|
+
});
|
|
110
|
+
logThought('[ProactiveHeartbeat] Service started with checks');
|
|
111
|
+
}
|
|
112
|
+
stop() {
|
|
113
|
+
this.#scheduler.unregister(HEARTBEAT_JOB_ID);
|
|
114
|
+
}
|
|
115
|
+
/** Execute a single heartbeat with all enabled checks */
|
|
116
|
+
async #executeHeartbeat() {
|
|
117
|
+
await logThought(`[ProactiveHeartbeat] Executing heartbeat...`);
|
|
118
|
+
this.#state.lastHeartbeatAt = new Date().toISOString();
|
|
119
|
+
this.#state.totalHeartbeats++;
|
|
120
|
+
const checkResults = [];
|
|
121
|
+
// Run all enabled checks
|
|
122
|
+
for (const [type, check] of this.#checks.entries()) {
|
|
123
|
+
if (!check.enabled)
|
|
124
|
+
continue;
|
|
125
|
+
try {
|
|
126
|
+
const result = await this.#runCheck(type);
|
|
127
|
+
check.lastResult = result;
|
|
128
|
+
check.lastCheckAt = new Date();
|
|
129
|
+
this.#state.checkStates[type] = {
|
|
130
|
+
lastCheckAt: new Date().toISOString(),
|
|
131
|
+
lastResult: result,
|
|
132
|
+
};
|
|
133
|
+
checkResults.push(result);
|
|
134
|
+
if (result.shouldNotify) {
|
|
135
|
+
await this.#sendNotification(result);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
const errorResult = {
|
|
140
|
+
success: false,
|
|
141
|
+
summary: `${type} check failed`,
|
|
142
|
+
details: err instanceof Error ? err.message : String(err),
|
|
143
|
+
shouldNotify: false,
|
|
144
|
+
};
|
|
145
|
+
checkResults.push(errorResult);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Generate summary
|
|
149
|
+
const summary = this.#generateSummary(checkResults);
|
|
150
|
+
// Call the original heartbeat callback
|
|
151
|
+
await this.#onHeartbeat(summary);
|
|
152
|
+
// Save state
|
|
153
|
+
this.#saveState();
|
|
154
|
+
}
|
|
155
|
+
/** Run a specific check */
|
|
156
|
+
async #runCheck(type) {
|
|
157
|
+
const check = this.#checks.get(type);
|
|
158
|
+
if (!check) {
|
|
159
|
+
return {
|
|
160
|
+
success: false,
|
|
161
|
+
summary: `Unknown check type: ${type}`,
|
|
162
|
+
shouldNotify: false,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
// If there's a custom skill for this check, run it
|
|
166
|
+
if (check.skillId && this.#gateway) {
|
|
167
|
+
try {
|
|
168
|
+
// Call the gateway directly with the check type
|
|
169
|
+
const response = await this.#gateway.processMessage({
|
|
170
|
+
platform: 'whatsapp',
|
|
171
|
+
senderId: 'heartbeat',
|
|
172
|
+
chatId: 'heartbeat',
|
|
173
|
+
text: `Run check: ${type}`,
|
|
174
|
+
rawPayload: { checkType: type, skillId: check.skillId },
|
|
175
|
+
});
|
|
176
|
+
return {
|
|
177
|
+
success: true,
|
|
178
|
+
summary: `${type} check completed`,
|
|
179
|
+
details: response,
|
|
180
|
+
shouldNotify: response.length > 0,
|
|
181
|
+
notificationMessage: response,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
return {
|
|
186
|
+
success: false,
|
|
187
|
+
summary: `${type} check failed`,
|
|
188
|
+
details: err instanceof Error ? err.message : String(err),
|
|
189
|
+
shouldNotify: true,
|
|
190
|
+
notificationMessage: `ā ļø ${type} check failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// Default check implementations
|
|
195
|
+
return this.#defaultCheckImplementation(type);
|
|
196
|
+
}
|
|
197
|
+
/** Default check implementations */
|
|
198
|
+
async #defaultCheckImplementation(type) {
|
|
199
|
+
// These are placeholder implementations
|
|
200
|
+
// In a real system, these would connect to actual services
|
|
201
|
+
switch (type) {
|
|
202
|
+
case 'email':
|
|
203
|
+
return {
|
|
204
|
+
success: true,
|
|
205
|
+
summary: 'Email check: No new important emails',
|
|
206
|
+
shouldNotify: false,
|
|
207
|
+
};
|
|
208
|
+
case 'calendar':
|
|
209
|
+
return {
|
|
210
|
+
success: true,
|
|
211
|
+
summary: 'Calendar check: No upcoming events in the next hour',
|
|
212
|
+
shouldNotify: false,
|
|
213
|
+
};
|
|
214
|
+
case 'weather':
|
|
215
|
+
return {
|
|
216
|
+
success: true,
|
|
217
|
+
summary: 'Weather check: Clear skies, 72°F',
|
|
218
|
+
shouldNotify: false,
|
|
219
|
+
};
|
|
220
|
+
case 'notifications':
|
|
221
|
+
return {
|
|
222
|
+
success: true,
|
|
223
|
+
summary: 'Notifications check: No new notifications',
|
|
224
|
+
shouldNotify: false,
|
|
225
|
+
};
|
|
226
|
+
default:
|
|
227
|
+
return {
|
|
228
|
+
success: false,
|
|
229
|
+
summary: `No implementation for check type: ${type}`,
|
|
230
|
+
shouldNotify: false,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/** Generate a summary of all check results */
|
|
235
|
+
#generateSummary(results) {
|
|
236
|
+
const successful = results.filter(r => r.success).length;
|
|
237
|
+
const failed = results.filter(r => !r.success).length;
|
|
238
|
+
const shouldNotify = results.filter(r => r.shouldNotify).length;
|
|
239
|
+
let summary = `š Heartbeat Summary:\n`;
|
|
240
|
+
summary += `⢠Total checks: ${results.length}\n`;
|
|
241
|
+
summary += `⢠Successful: ${successful}\n`;
|
|
242
|
+
summary += `⢠Failed: ${failed}\n`;
|
|
243
|
+
summary += `⢠Notifications sent: ${shouldNotify}\n`;
|
|
244
|
+
if (failed > 0) {
|
|
245
|
+
summary += `\nā ļø Failed checks:\n`;
|
|
246
|
+
for (const result of results.filter(r => !r.success)) {
|
|
247
|
+
summary += `⢠${result.summary}\n`;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return summary;
|
|
251
|
+
}
|
|
252
|
+
/** Send notification to configured targets */
|
|
253
|
+
async #sendNotification(result) {
|
|
254
|
+
if (!result.notificationMessage)
|
|
255
|
+
return;
|
|
256
|
+
this.#state.totalNotifications++;
|
|
257
|
+
// In a real implementation, this would use the dispatcher to send
|
|
258
|
+
// proactive messages to the configured platforms and users
|
|
259
|
+
await logThought(`[ProactiveHeartbeat] Would send notification: ${result.notificationMessage}`);
|
|
260
|
+
// TODO: Integrate with dispatcher for actual notification sending
|
|
261
|
+
// For now, just log the notification
|
|
262
|
+
}
|
|
263
|
+
/** Manually trigger a heartbeat */
|
|
264
|
+
async triggerManual() {
|
|
265
|
+
await this.#executeHeartbeat();
|
|
266
|
+
}
|
|
267
|
+
/** Manually trigger a specific check */
|
|
268
|
+
async triggerCheck(type) {
|
|
269
|
+
const result = await this.#runCheck(type);
|
|
270
|
+
const check = this.#checks.get(type);
|
|
271
|
+
if (check) {
|
|
272
|
+
check.lastResult = result;
|
|
273
|
+
check.lastCheckAt = new Date();
|
|
274
|
+
this.#saveState();
|
|
275
|
+
}
|
|
276
|
+
return result;
|
|
277
|
+
}
|
|
278
|
+
/** Load state from disk */
|
|
279
|
+
#loadState() {
|
|
280
|
+
try {
|
|
281
|
+
const fs = require('fs');
|
|
282
|
+
if (fs.existsSync(this.#statePath)) {
|
|
283
|
+
const data = fs.readFileSync(this.#statePath, 'utf8');
|
|
284
|
+
const loaded = JSON.parse(data);
|
|
285
|
+
this.#state = { ...this.#state, ...loaded };
|
|
286
|
+
// Restore check states
|
|
287
|
+
for (const [type, state] of Object.entries(this.#state.checkStates)) {
|
|
288
|
+
const check = this.#checks.get(type);
|
|
289
|
+
if (check && state) {
|
|
290
|
+
check.lastCheckAt = state.lastCheckAt ? new Date(state.lastCheckAt) : undefined;
|
|
291
|
+
check.lastResult = state.lastResult;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
catch (err) {
|
|
297
|
+
console.warn('[ProactiveHeartbeat] Failed to load state:', err);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/** Save state to disk */
|
|
301
|
+
#saveState() {
|
|
302
|
+
try {
|
|
303
|
+
const fs = require('fs');
|
|
304
|
+
const dir = require('path').dirname(this.#statePath);
|
|
305
|
+
if (!fs.existsSync(dir)) {
|
|
306
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
307
|
+
}
|
|
308
|
+
fs.writeFileSync(this.#statePath, JSON.stringify(this.#state, null, 2));
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
console.warn('[ProactiveHeartbeat] Failed to save state:', err);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Simple backward-compatible wrapper for the original HeartbeatService
|
|
317
|
+
*/
|
|
318
|
+
export class HeartbeatService {
|
|
319
|
+
#scheduler;
|
|
320
|
+
#onHeartbeat;
|
|
321
|
+
#cronExpression;
|
|
322
|
+
#message;
|
|
25
323
|
constructor(onHeartbeat, config = {}, scheduler) {
|
|
26
324
|
this.#onHeartbeat = onHeartbeat;
|
|
27
325
|
this.#cronExpression = config.cronExpression ?? getConfigValue('HEARTBEAT_CRON') ?? DEFAULT_CRON;
|
|
28
326
|
this.#message = config.message ?? getConfigValue('HEARTBEAT_MESSAGE') ?? DEFAULT_MESSAGE;
|
|
29
327
|
this.#scheduler = scheduler ?? new JobScheduler();
|
|
30
328
|
}
|
|
31
|
-
/** Expose the underlying scheduler so callers can register additional jobs. */
|
|
32
329
|
get scheduler() {
|
|
33
330
|
return this.#scheduler;
|
|
34
331
|
}
|
|
35
332
|
start() {
|
|
36
|
-
// Guard against double-start
|
|
37
333
|
if (this.#scheduler.getJob(HEARTBEAT_JOB_ID)) {
|
|
38
334
|
return;
|
|
39
335
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { logToolCall, scrubSensitiveText } from '../utils/logger.js';
|
|
2
|
+
import { getLearningSystem } from '../services/learning-system.js';
|
|
3
|
+
import { getSelfHealingService } from '../services/self-healing.js';
|
|
2
4
|
/** Convert a Skill (from the registry) into the internal Tool format used by LaneExecutor. */
|
|
3
5
|
function skillToTool(skill) {
|
|
4
6
|
return {
|
|
@@ -112,6 +114,8 @@ export class LaneExecutor {
|
|
|
112
114
|
const result = await tool.execute(args);
|
|
113
115
|
content = typeof result === 'string' ? result : JSON.stringify(result);
|
|
114
116
|
await logToolCall(toolName, args, content);
|
|
117
|
+
const success = !content.startsWith('Error');
|
|
118
|
+
await this.#recordSkillExecution(toolName, args, success, content);
|
|
115
119
|
}
|
|
116
120
|
catch (error) {
|
|
117
121
|
const rawMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -119,6 +123,7 @@ export class LaneExecutor {
|
|
|
119
123
|
console.error(`[LaneExecutor] Tool ${toolName} failed: ${sanitizedMessage}`);
|
|
120
124
|
content = `Error executing tool: ${sanitizedMessage}`;
|
|
121
125
|
await logToolCall(toolName, args, content);
|
|
126
|
+
await this.#recordSkillExecution(toolName, args, false, sanitizedMessage);
|
|
122
127
|
}
|
|
123
128
|
}
|
|
124
129
|
}
|
|
@@ -131,4 +136,17 @@ export class LaneExecutor {
|
|
|
131
136
|
}
|
|
132
137
|
return results;
|
|
133
138
|
}
|
|
139
|
+
async #recordSkillExecution(toolName, args, success, result) {
|
|
140
|
+
try {
|
|
141
|
+
const learning = getLearningSystem();
|
|
142
|
+
const selfHealer = getSelfHealingService();
|
|
143
|
+
const isError = result.startsWith('Error');
|
|
144
|
+
const outcome = isError ? 'failure' : 'success';
|
|
145
|
+
await learning.learn('success', `skill:${toolName}`, { toolName, args, result: result.slice(0, 100) }, success ? undefined : result.slice(0, 200), outcome);
|
|
146
|
+
selfHealer.learn(`skill:${toolName}`, success, { toolName, args, result: result.slice(0, 100) }, success ? undefined : result.slice(0, 200));
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
console.warn('[LaneExecutor] Failed to record skill execution:', err);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
134
152
|
}
|
package/dist/core/onboarding.js
CHANGED
|
@@ -162,7 +162,7 @@ const ONBOARD_FIELDS = [
|
|
|
162
162
|
{
|
|
163
163
|
key: 'API_PORT',
|
|
164
164
|
label: 'API Port',
|
|
165
|
-
hint: '
|
|
165
|
+
hint: 'HTTP port for TwinClaw control plane API (default: 18789). Required to connect external clients.',
|
|
166
166
|
validator: (value) => {
|
|
167
167
|
const parsed = Number(value);
|
|
168
168
|
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
|
|
@@ -218,7 +218,6 @@ class ReadlinePrompter {
|
|
|
218
218
|
this.#rl = readline.createInterface({
|
|
219
219
|
input: process.stdin,
|
|
220
220
|
output: this.#writer,
|
|
221
|
-
terminal: true,
|
|
222
221
|
});
|
|
223
222
|
this.#rl.on('SIGINT', () => {
|
|
224
223
|
const reject = this.#pendingReject;
|
|
@@ -669,3 +668,4 @@ export async function handleOnboardCli(argv) {
|
|
|
669
668
|
await runSimplifiedOnboarding();
|
|
670
669
|
return true;
|
|
671
670
|
}
|
|
671
|
+
export { runSimplifiedOnboarding } from './simplified-onboarding.js';
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { getLearningSystem, initializeLearning } from '../services/learning-system.js';
|
|
2
|
+
import { getSkillBuilder, initializeSkillBuilder } from '../services/skill-builder.js';
|
|
3
|
+
import { getAutoConfigurer, initializeAutoConfigurer } from '../services/auto-configurer.js';
|
|
4
|
+
import { getSelfHealingService, initializeSelfHealer } from '../services/self-healing.js';
|
|
5
|
+
export async function handleSelfImproveCli(argv) {
|
|
6
|
+
if (argv[0] !== 'self') {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
try {
|
|
10
|
+
await initializeLearning();
|
|
11
|
+
await initializeSkillBuilder();
|
|
12
|
+
await initializeAutoConfigurer();
|
|
13
|
+
await initializeSelfHealer();
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
console.warn('Note: Some services may not be fully initialized:', err instanceof Error ? err.message : String(err));
|
|
17
|
+
}
|
|
18
|
+
const command = argv[1];
|
|
19
|
+
if (!command) {
|
|
20
|
+
console.log(`
|
|
21
|
+
š¤ TwinClaw Self-Improving Agent Commands
|
|
22
|
+
|
|
23
|
+
Usage: twinclaw self <command> [options]
|
|
24
|
+
|
|
25
|
+
Commands:
|
|
26
|
+
learn <pattern> <solution> - Learn from a success
|
|
27
|
+
query <question> - Query past experiences
|
|
28
|
+
stats - Show learning statistics
|
|
29
|
+
health - Run health check
|
|
30
|
+
configure <service> - Auto-configure a service
|
|
31
|
+
services - List available services
|
|
32
|
+
build-skill <description> - Build a new skill
|
|
33
|
+
|
|
34
|
+
Examples:
|
|
35
|
+
twinclaw self learn "api timeout" "increase timeout to 30s"
|
|
36
|
+
twinclaw self query "github integration"
|
|
37
|
+
twinclaw self configure slack
|
|
38
|
+
twinclaw self services
|
|
39
|
+
`.trim());
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
switch (command) {
|
|
44
|
+
case 'learn':
|
|
45
|
+
return await handleLearn(argv.slice(2));
|
|
46
|
+
case 'query':
|
|
47
|
+
return await handleQuery(argv.slice(2));
|
|
48
|
+
case 'stats':
|
|
49
|
+
return await handleStats();
|
|
50
|
+
case 'health':
|
|
51
|
+
return await handleHealth();
|
|
52
|
+
case 'configure':
|
|
53
|
+
return await handleConfigure(argv.slice(2));
|
|
54
|
+
case 'services':
|
|
55
|
+
return await handleServices();
|
|
56
|
+
case 'build-skill':
|
|
57
|
+
return await handleBuildSkill(argv.slice(2));
|
|
58
|
+
case 'fix':
|
|
59
|
+
return await handleGetFix(argv.slice(2));
|
|
60
|
+
default:
|
|
61
|
+
console.error(`Unknown command: ${command}`);
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
console.error('Error:', err instanceof Error ? err.message : String(err));
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function handleLearn(args) {
|
|
71
|
+
if (args.length < 2) {
|
|
72
|
+
console.log('Usage: twinclaw self learn <pattern> <solution>');
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
const pattern = args[0];
|
|
76
|
+
const solution = args.slice(1).join(' ');
|
|
77
|
+
const learning = getLearningSystem();
|
|
78
|
+
await learning.learn('fix', pattern, {}, solution, 'success');
|
|
79
|
+
console.log(`ā
Learned: "${pattern}" -> "${solution}"`);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
async function handleQuery(args) {
|
|
83
|
+
if (args.length < 1) {
|
|
84
|
+
console.log('Usage: twinclaw self query <question>');
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
const query = args.join(' ');
|
|
88
|
+
const learning = getLearningSystem();
|
|
89
|
+
const result = learning.query(query);
|
|
90
|
+
console.log(`\nš Results for: "${query}"\n`);
|
|
91
|
+
if (result.entries.length === 0) {
|
|
92
|
+
console.log('No similar experiences found.');
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
console.log('Past experiences:');
|
|
96
|
+
for (const entry of result.entries.slice(0, 5)) {
|
|
97
|
+
console.log(` - ${entry.description} (${entry.successRate * 100}% success)`);
|
|
98
|
+
if (entry.solution) {
|
|
99
|
+
console.log(` Solution: ${entry.solution}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (result.suggestions.length > 0) {
|
|
104
|
+
console.log('\nš” Suggestions:');
|
|
105
|
+
for (const s of result.suggestions) {
|
|
106
|
+
console.log(` - ${s}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
async function handleStats() {
|
|
112
|
+
const learning = getLearningSystem();
|
|
113
|
+
const stats = learning.getStats();
|
|
114
|
+
console.log(`
|
|
115
|
+
š Learning Statistics
|
|
116
|
+
āāāāāāāāāāāāāāāāāāāā
|
|
117
|
+
Total memories: ${stats.total}
|
|
118
|
+
Success rate: ${(stats.successRate * 100).toFixed(1)}%
|
|
119
|
+
|
|
120
|
+
By type:
|
|
121
|
+
${Object.entries(stats.byType).map(([type, count]) => ` ${type}: ${count}`).join('\n')}
|
|
122
|
+
|
|
123
|
+
Top patterns:
|
|
124
|
+
${stats.topPatterns.map(p => ` - ${p}`).join('\n') || ' (none yet)'}
|
|
125
|
+
`.trim());
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
async function handleHealth() {
|
|
129
|
+
const healer = getSelfHealingService();
|
|
130
|
+
const metrics = await healer.performHealthCheck();
|
|
131
|
+
console.log(`
|
|
132
|
+
š Self-Healing Health Check
|
|
133
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
134
|
+
`);
|
|
135
|
+
for (const metric of metrics) {
|
|
136
|
+
const statusIcon = metric.status === 'healthy' ? 'ā
' : metric.status === 'degraded' ? 'ā ļø' : 'ā';
|
|
137
|
+
console.log(`${statusIcon} ${metric.component}: ${metric.status}`);
|
|
138
|
+
if (metric.message) {
|
|
139
|
+
console.log(` ${metric.message}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
console.log('\nRun "twinclaw doctor" for full diagnostics.');
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
async function handleConfigure(args) {
|
|
146
|
+
if (args.length < 1) {
|
|
147
|
+
console.log('Usage: twinclaw self configure <service>');
|
|
148
|
+
console.log('Available services: twinclaw self services');
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
const serviceName = args[0];
|
|
152
|
+
const configurer = getAutoConfigurer();
|
|
153
|
+
console.log(`\nš§ Configuring ${serviceName}...\n`);
|
|
154
|
+
const { config, steps } = await configurer.fetchDocsAndConfig(serviceName);
|
|
155
|
+
console.log('Setup steps:');
|
|
156
|
+
for (const step of steps) {
|
|
157
|
+
console.log(` ${step}`);
|
|
158
|
+
}
|
|
159
|
+
console.log('\nTo complete setup, edit your twinclaw.json with:');
|
|
160
|
+
console.log(JSON.stringify({ [serviceName]: config }, null, 2));
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
async function handleServices() {
|
|
164
|
+
const configurer = getAutoConfigurer();
|
|
165
|
+
const services = configurer.listKnownServices();
|
|
166
|
+
console.log(`
|
|
167
|
+
š¦ Available Auto-Configure Services
|
|
168
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
169
|
+
`);
|
|
170
|
+
for (const service of services) {
|
|
171
|
+
console.log(` ${service.name.padEnd(15)} - ${service.description}`);
|
|
172
|
+
}
|
|
173
|
+
console.log('\nUsage: twinclaw self configure <service>');
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
async function handleBuildSkill(args) {
|
|
177
|
+
if (args.length < 1) {
|
|
178
|
+
console.log('Usage: twinclaw self build-skill <description>');
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
const description = args.join(' ');
|
|
182
|
+
const builder = getSkillBuilder();
|
|
183
|
+
console.log(`\nš§± Building skill: "${description}"`);
|
|
184
|
+
const skill = await builder.buildFromRequest({
|
|
185
|
+
description,
|
|
186
|
+
purpose: 'user requested'
|
|
187
|
+
});
|
|
188
|
+
console.log(`\nā
Created skill: ${skill.name}`);
|
|
189
|
+
console.log(` Category: ${skill.category}`);
|
|
190
|
+
console.log(` ID: ${skill.id}`);
|
|
191
|
+
console.log(`\nSkill code saved to: ${builder.getCustomSkillsDir()}/${skill.id}.ts`);
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
async function handleGetFix(args) {
|
|
195
|
+
if (args.length < 1) {
|
|
196
|
+
console.log('Usage: twinclaw self fix <issue>');
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
const issue = args.join(' ');
|
|
200
|
+
const healer = getSelfHealingService();
|
|
201
|
+
const fixes = healer.suggestFixes(issue);
|
|
202
|
+
console.log(`\nš§ Suggested fixes for: "${issue}"\n`);
|
|
203
|
+
if (fixes.length === 0) {
|
|
204
|
+
console.log('No known fixes. Try: twinclaw self query <issue>');
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
for (const fix of fixes) {
|
|
208
|
+
console.log(` - ${fix}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return true;
|
|
212
|
+
}
|