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.
Files changed (35) hide show
  1. package/README.md +3 -3
  2. package/dist/api/handlers/agents.js +82 -0
  3. package/dist/api/handlers/debug.js +69 -0
  4. package/dist/api/handlers/devices.js +79 -0
  5. package/dist/api/handlers/jobs.js +99 -0
  6. package/dist/api/handlers/status.js +149 -0
  7. package/dist/api/router.js +272 -2
  8. package/dist/api/runtime-event-producer.js +20 -0
  9. package/dist/api/shared.js +34 -0
  10. package/dist/api/websocket-hub.js +18 -7
  11. package/dist/config/json-config.js +393 -1
  12. package/dist/core/heartbeat.js +304 -8
  13. package/dist/core/lane-executor.js +18 -0
  14. package/dist/core/onboarding.js +2 -2
  15. package/dist/core/self-improve-cli.js +212 -0
  16. package/dist/core/simplified-onboarding.js +158 -16
  17. package/dist/index.js +61 -33
  18. package/dist/interfaces/dispatcher.js +4 -2
  19. package/dist/interfaces/whatsapp_handler.js +243 -2
  20. package/dist/services/auto-configurer.js +591 -0
  21. package/dist/services/device-pairing.js +378 -0
  22. package/dist/services/hooks.js +130 -0
  23. package/dist/services/job-scheduler.js +133 -1
  24. package/dist/services/learning-system.js +226 -0
  25. package/dist/services/self-healing.js +267 -0
  26. package/dist/services/skill-acquisition/intent-parser.js +115 -0
  27. package/dist/services/skill-acquisition/research-engine.js +319 -0
  28. package/dist/services/skill-builder.js +235 -0
  29. package/dist/services/sub-agent-service.js +215 -0
  30. package/dist/services/web-service.js +70 -0
  31. package/dist/services/webhook-service.js +127 -0
  32. package/dist/skills/builtin.js +827 -0
  33. package/dist/tools/agent-improvement.js +208 -0
  34. package/dist/types/skill-acquisition.js +4 -0
  35. package/package.json +3 -1
@@ -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
- * Now delegates to the centralized {@link JobScheduler} so heartbeat intervals
11
- * are managed alongside other repeating background jobs. The external API
12
- * remains identical to the original implementation for backward compatibility.
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 HeartbeatService {
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
  }
@@ -162,7 +162,7 @@ const ONBOARD_FIELDS = [
162
162
  {
163
163
  key: 'API_PORT',
164
164
  label: 'API Port',
165
- hint: 'Workspace default. Integer from 1 to 65535.',
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
+ }