twinclaw 1.1.5 → 1.1.7

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 (98) hide show
  1. package/dist/api/handlers/agents.js +1 -1
  2. package/dist/api/handlers/browser.js +47 -7
  3. package/dist/api/handlers/callback.js +3 -3
  4. package/dist/api/handlers/debug.js +20 -15
  5. package/dist/api/handlers/devices.js +2 -1
  6. package/dist/api/handlers/health-data.js +87 -0
  7. package/dist/api/handlers/health.js +2 -84
  8. package/dist/api/handlers/self-routes.js +144 -0
  9. package/dist/api/handlers/skill-acquisition-routes.js +90 -0
  10. package/dist/api/handlers/status.js +10 -87
  11. package/dist/api/router.js +32 -229
  12. package/dist/api/runtime-event-producer.js +1 -1
  13. package/dist/api/shared.js +10 -5
  14. package/dist/api/websocket-hub.js +26 -10
  15. package/dist/bootstrap/owner-bootstrap.js +23 -0
  16. package/dist/bootstrap/startup-health-probe.js +24 -0
  17. package/dist/config/config-schema.js +54 -0
  18. package/dist/config/env-schema.js +73 -1
  19. package/dist/config/env-validator.js +21 -5
  20. package/dist/config/json-config.js +71 -6
  21. package/dist/config/model-catalog.js +336 -0
  22. package/dist/config/prestart-validation.js +37 -0
  23. package/dist/config/validation-rules.js +16 -0
  24. package/dist/config/workspace.js +18 -1
  25. package/dist/core/channels-cli.js +120 -3
  26. package/dist/core/cli.js +225 -99
  27. package/dist/core/context-assembly.js +7 -1
  28. package/dist/core/doctor.js +4 -3
  29. package/dist/core/gateway-cli.js +30 -12
  30. package/dist/core/gateway.js +57 -12
  31. package/dist/core/heartbeat.js +8 -41
  32. package/dist/core/help-formatter.js +41 -0
  33. package/dist/core/lane-executor.js +3 -2
  34. package/dist/core/logs-cli.js +24 -1
  35. package/dist/core/model-picker.js +148 -0
  36. package/dist/core/onboarding-v2.js +65 -0
  37. package/dist/core/onboarding.js +460 -78
  38. package/dist/core/pairing-cli.js +33 -7
  39. package/dist/core/queue-cli.js +125 -0
  40. package/dist/core/queue-metrics.js +49 -0
  41. package/dist/core/secret-vault-cli.js +24 -7
  42. package/dist/core/self-improve-cli.js +58 -48
  43. package/dist/core/simplified-onboarding.js +5 -3
  44. package/dist/core/status-cli.js +152 -0
  45. package/dist/index.js +380 -357
  46. package/dist/interfaces/dispatcher.js +39 -16
  47. package/dist/interfaces/whatsapp_handler.js +50 -10
  48. package/dist/release/twinclaw-config-schema.js +9 -2
  49. package/dist/services/auto-configurer.js +41 -17
  50. package/dist/services/block-chunker.js +20 -3
  51. package/dist/services/browser-service.js +67 -4
  52. package/dist/services/context-lifecycle.js +15 -8
  53. package/dist/services/db-delivery-callback.js +110 -0
  54. package/dist/services/db-incidents.js +97 -0
  55. package/dist/services/db-local-state.js +97 -0
  56. package/dist/services/db-mcp-audit.js +15 -0
  57. package/dist/services/db-model-routing.js +53 -0
  58. package/dist/services/db-orchestration.js +78 -0
  59. package/dist/services/db-reasoning.js +159 -0
  60. package/dist/services/db-runtime-budget.js +125 -0
  61. package/dist/services/db.js +69 -721
  62. package/dist/services/device-pairing.js +12 -22
  63. package/dist/services/embedding-service.js +59 -6
  64. package/dist/services/github-copilot-service.js +232 -0
  65. package/dist/services/hooks.js +44 -8
  66. package/dist/services/incident-manager.js +3 -3
  67. package/dist/services/learning-system.js +15 -5
  68. package/dist/services/local-state-backup.js +9 -13
  69. package/dist/services/mcp-client-adapter.js +53 -8
  70. package/dist/services/mcp-server-manager.js +2 -1
  71. package/dist/services/model-catalog-service.js +154 -0
  72. package/dist/services/model-router.js +226 -55
  73. package/dist/services/mvp-gate.js +5 -11
  74. package/dist/services/orchestration-service.js +26 -5
  75. package/dist/services/persona-state.js +48 -5
  76. package/dist/services/policy-engine.js +1 -1
  77. package/dist/services/proactive-notifier.js +35 -1
  78. package/dist/services/queue-service.js +25 -6
  79. package/dist/services/release-pipeline.js +3 -11
  80. package/dist/services/runtime-budget-governor.js +19 -7
  81. package/dist/services/secret-vault.js +27 -7
  82. package/dist/services/self-healing.js +59 -12
  83. package/dist/services/semantic-memory.js +19 -12
  84. package/dist/services/skill-builder.js +57 -59
  85. package/dist/services/skill-package-manager.js +10 -4
  86. package/dist/services/streaming-output.js +30 -0
  87. package/dist/services/sub-agent-service.js +17 -4
  88. package/dist/services/web-service.js +1 -1
  89. package/dist/skills/builtin.js +10 -11
  90. package/dist/skills/shell.js +5 -2
  91. package/dist/types/model-catalog.js +20 -0
  92. package/dist/types/model-routing.js +5 -1
  93. package/dist/utils/config-utils.js +33 -0
  94. package/dist/utils/fs-utils.js +14 -0
  95. package/dist/utils/logger.js +27 -8
  96. package/dist/utils/retry.js +7 -4
  97. package/dist/utils/secret-scan.js +1 -1
  98. package/package.json +8 -4
@@ -37,7 +37,7 @@ export function handleAgentsGet(deps) {
37
37
  id: agent.id,
38
38
  name: agent.name,
39
39
  model: agent.model,
40
- systemPrompt: agent.systemPrompt,
40
+ hasSystemPrompt: Boolean(agent.systemPrompt && agent.systemPrompt.trim().length > 0),
41
41
  maxSteps: agent.maxSteps,
42
42
  timeoutMs: agent.timeoutMs,
43
43
  reportBackTo: agent.reportBackTo,
@@ -2,7 +2,18 @@ import { BrowserReferenceError } from '../../services/browser-service.js';
2
2
  import { sendOk, sendError, mapError } from '../shared.js';
3
3
  import { logThought } from '../../utils/logger.js';
4
4
  import path from 'node:path';
5
+ import { isIP } from 'node:net';
6
+ import { unlink } from 'node:fs/promises';
5
7
  const DEFAULT_BROWSER_ALLOWED_HOSTS = ['example.com'];
8
+ const SNAPSHOT_CLEANUP_DELAY_MS = 5 * 60 * 1000;
9
+ const BLOCKED_METADATA_HOSTS = new Set([
10
+ 'metadata.google.internal',
11
+ 'metadata.azure.internal',
12
+ 'metadata',
13
+ '169.254.169.254',
14
+ '169.254.170.2',
15
+ '100.100.100.200',
16
+ ]);
6
17
  function resolveAllowedBrowserHosts() {
7
18
  const configured = (process.env.BROWSER_ALLOWED_HOSTS ?? '')
8
19
  .split(',')
@@ -24,21 +35,37 @@ function isPrivateIpv4(hostname) {
24
35
  a === 127 ||
25
36
  a === 0 ||
26
37
  (a === 169 && b === 254) ||
38
+ (a === 100 && b >= 64 && b <= 127) ||
27
39
  (a === 172 && b >= 16 && b <= 31) ||
28
40
  (a === 192 && b === 168));
29
41
  }
42
+ function isBlockedIpv6(hostname) {
43
+ const normalized = hostname.toLowerCase();
44
+ if (normalized === '::1' || normalized === '::') {
45
+ return true;
46
+ }
47
+ if (normalized.startsWith('fe80:') || normalized.startsWith('fc') || normalized.startsWith('fd')) {
48
+ return true;
49
+ }
50
+ if (normalized.startsWith('::ffff:')) {
51
+ return isPrivateIpv4(normalized.slice('::ffff:'.length));
52
+ }
53
+ return false;
54
+ }
30
55
  function isPrivateOrLocalHost(hostname) {
31
56
  const normalized = hostname.toLowerCase();
57
+ if (BLOCKED_METADATA_HOSTS.has(normalized)) {
58
+ return true;
59
+ }
32
60
  if (normalized === 'localhost' ||
33
61
  normalized.endsWith('.localhost') ||
34
- normalized.endsWith('.local') ||
35
- normalized === '::1' ||
36
- normalized === '::' ||
37
- normalized.startsWith('fe80:') ||
38
- normalized.startsWith('fc') ||
39
- normalized.startsWith('fd')) {
62
+ normalized.endsWith('.local')) {
40
63
  return true;
41
64
  }
65
+ const ipKind = isIP(normalized);
66
+ if (ipKind === 6) {
67
+ return isBlockedIpv6(normalized);
68
+ }
42
69
  return isPrivateIpv4(normalized);
43
70
  }
44
71
  function hostMatchesAllowRule(hostname, rule) {
@@ -56,7 +83,7 @@ function validateNavigationUrl(inputUrl) {
56
83
  try {
57
84
  parsed = new URL(inputUrl);
58
85
  }
59
- catch {
86
+ catch (_error) {
60
87
  return { ok: false, status: 400, error: 'Field "url" must be a valid absolute URL.' };
61
88
  }
62
89
  if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
@@ -80,6 +107,18 @@ function validateNavigationUrl(inputUrl) {
80
107
  }
81
108
  return { ok: true, url: parsed.toString() };
82
109
  }
110
+ function scheduleSnapshotCleanup(screenshotPath) {
111
+ const timeout = setTimeout(() => {
112
+ void unlink(screenshotPath).catch((error) => {
113
+ const fsError = error;
114
+ if (fsError.code !== 'ENOENT') {
115
+ const message = fsError instanceof Error ? fsError.message : String(fsError);
116
+ void logThought(`[API] Snapshot cleanup failed for ${screenshotPath}: ${message}`);
117
+ }
118
+ });
119
+ }, SNAPSHOT_CLEANUP_DELAY_MS);
120
+ timeout.unref();
121
+ }
83
122
  /**
84
123
  * POST /browser/snapshot
85
124
  *
@@ -116,6 +155,7 @@ export function handleBrowserSnapshot(deps) {
116
155
  const result = await deps.browserService.takeScreenshotForVlm(screenshotPath, fullPage);
117
156
  const tree = await deps.browserService.getAccessibilityTree();
118
157
  const referenceContext = await deps.browserService.captureSnapshotReferenceContext();
158
+ scheduleSnapshotCleanup(result.path);
119
159
  const data = {
120
160
  snapshotId: referenceContext.snapshotId,
121
161
  screenshotPath: result.path,
@@ -1,6 +1,6 @@
1
1
  import { sendOk, sendError } from '../shared.js';
2
2
  import { logThought } from '../../utils/logger.js';
3
- import { recordCallbackReceipt, getCallbackReceipt, getDelivery, updateDeliveryState } from '../../services/db.js';
3
+ import { recordCallbackReceipt, getCallbackReceipt, getDelivery, updateDeliveryState } from '../../services/db-delivery-callback.js';
4
4
  const MAX_SANITIZED_STRING_LENGTH = 512;
5
5
  const MAX_SANITIZED_ARRAY_ITEMS = 25;
6
6
  const MAX_SANITIZED_OBJECT_KEYS = 40;
@@ -107,8 +107,8 @@ export function handleWebhookCallback(deps) {
107
107
  updateDeliveryState(body.taskId, newState, newState === 'sent' ? new Date().toISOString() : null);
108
108
  await logThought(`[API] Webhook reconciled delivery queue item: ${body.taskId} -> ${newState}`);
109
109
  }
110
- // Fire-and-forget: process the webhook payload as a conversation turn
111
- void deps.gateway.processText(sessionId, summaryText);
110
+ const gatewayResult = await deps.gateway.processText(sessionId, summaryText);
111
+ await logThought(`[API] Webhook payload processed by gateway for task ${body.taskId} (responseLength=${gatewayResult.length}).`);
112
112
  recordCallbackReceipt(idempotencyKey, 202, 'accepted');
113
113
  const data = {
114
114
  accepted: true,
@@ -45,25 +45,30 @@ export function handleDebug(deps) {
45
45
  };
46
46
  }
47
47
  async function getRecentLogs(limit) {
48
+ const dateIso = new Date().toISOString().slice(0, 10);
49
+ const logPath = path.resolve('memory', `${dateIso}.md`);
50
+ let content = '';
48
51
  try {
49
- const dateIso = new Date().toISOString().slice(0, 10);
50
- const logPath = path.resolve('memory', `${dateIso}.md`);
51
- const content = await readFile(logPath, 'utf8').catch(() => '');
52
- if (!content) {
52
+ content = await readFile(logPath, 'utf8');
53
+ }
54
+ catch (error) {
55
+ const fsError = error;
56
+ if (fsError.code === 'ENOENT') {
53
57
  return [];
54
58
  }
55
- const sections = content.split(/\n## /).filter(Boolean);
56
- return sections.slice(-limit).map((s) => {
57
- const [header, ...bodyLines] = s.split('\n');
58
- const [type, timestamp] = header.split(' @ ');
59
- return {
60
- timestamp: timestamp || new Date().toISOString(),
61
- level: type.toUpperCase(),
62
- message: bodyLines.join('\n').trim(),
63
- };
64
- }).reverse();
59
+ throw new Error(`Failed to read debug logs from ${logPath}: ${fsError.message}`);
65
60
  }
66
- catch {
61
+ if (!content.trim()) {
67
62
  return [];
68
63
  }
64
+ const sections = content.split(/\n## /).filter(Boolean);
65
+ return sections.slice(-limit).map((s) => {
66
+ const [header, ...bodyLines] = s.split('\n');
67
+ const [type, timestamp] = header.split(' @ ');
68
+ return {
69
+ timestamp: timestamp || new Date().toISOString(),
70
+ level: type.toUpperCase(),
71
+ message: bodyLines.join('\n').trim(),
72
+ };
73
+ }).reverse();
69
74
  }
@@ -68,7 +68,8 @@ export function handleDevicesCommand(deps) {
68
68
  });
69
69
  }
70
70
  else {
71
- sendError(res, result.error || 'Command execution failed', 400);
71
+ const status = result.error && /not implemented/i.test(result.error) ? 501 : 400;
72
+ sendError(res, result.error || 'Command execution failed', status);
72
73
  }
73
74
  }
74
75
  catch (err) {
@@ -0,0 +1,87 @@
1
+ import { getSecretVaultService } from '../../services/secret-vault.js';
2
+ export async function buildHealthData(deps, startTimeMs) {
3
+ const summary = deps.skillRegistry.summary();
4
+ const servers = deps.mcpManager.listServers();
5
+ const packageDiagnostics = await deps.mcpManager.getSkillPackageDiagnostics();
6
+ const secretDiagnostics = getSecretVaultService().getDiagnostics(['API_SECRET']);
7
+ const budgetSnapshot = deps.budgetGovernor?.getSnapshot('health');
8
+ const routingSnapshot = deps.modelRouter?.getHealthSnapshot();
9
+ const backupDiagnostics = deps.localStateBackup
10
+ ? await deps.localStateBackup.getDiagnostics(5)
11
+ : null;
12
+ const heartbeatRunning = deps.heartbeat.scheduler
13
+ .listJobs()
14
+ .some((j) => j.status === 'running');
15
+ return {
16
+ status: servers.some((s) => s.state === 'error') ||
17
+ packageDiagnostics.blockedPackageCount > 0 ||
18
+ secretDiagnostics.health.hasIssues ||
19
+ budgetSnapshot?.directive.severity === 'hard_limit' ||
20
+ (routingSnapshot?.consecutiveFailures ?? 0) >= 3 ||
21
+ backupDiagnostics?.status === 'degraded'
22
+ ? 'degraded'
23
+ : 'ok',
24
+ uptimeSec: Math.floor((Date.now() - startTimeMs) / 1000),
25
+ memoryUsageMb: Math.round(process.memoryUsage().rss / 1024 / 1024),
26
+ heartbeat: { running: heartbeatRunning },
27
+ skills: {
28
+ builtin: summary.builtin ?? 0,
29
+ mcp: summary.mcp ?? 0,
30
+ total: deps.skillRegistry.size,
31
+ },
32
+ skillPackages: {
33
+ installed: packageDiagnostics.installed.length,
34
+ active: packageDiagnostics.activePackageCount,
35
+ blocked: packageDiagnostics.blockedPackageCount,
36
+ warnings: packageDiagnostics.warnings,
37
+ violations: packageDiagnostics.violations.map((violation) => ({
38
+ packageName: violation.packageName,
39
+ version: violation.version,
40
+ code: violation.code,
41
+ message: violation.message,
42
+ remediation: violation.remediation,
43
+ })),
44
+ },
45
+ secrets: {
46
+ status: secretDiagnostics.health.hasIssues ? 'degraded' : 'ok',
47
+ missingRequired: secretDiagnostics.health.missingRequired,
48
+ expired: secretDiagnostics.health.expired,
49
+ warnings: secretDiagnostics.health.warnings,
50
+ total: secretDiagnostics.total,
51
+ active: secretDiagnostics.active,
52
+ dueForRotation: secretDiagnostics.dueForRotation,
53
+ },
54
+ budget: budgetSnapshot
55
+ ? {
56
+ severity: budgetSnapshot.directive.severity,
57
+ profile: budgetSnapshot.directive.profile,
58
+ pacingDelayMs: budgetSnapshot.directive.pacingDelayMs,
59
+ manualProfile: budgetSnapshot.manualProfile,
60
+ daily: budgetSnapshot.daily,
61
+ session: budgetSnapshot.session,
62
+ providers: budgetSnapshot.providers,
63
+ }
64
+ : undefined,
65
+ routing: routingSnapshot,
66
+ backups: backupDiagnostics
67
+ ? {
68
+ status: backupDiagnostics.status,
69
+ lastSnapshotAt: backupDiagnostics.lastSnapshotAt,
70
+ lastRestoreAt: backupDiagnostics.lastRestoreAt,
71
+ validationFailureCount: backupDiagnostics.validationFailureCount,
72
+ recommendationCount: backupDiagnostics.recommendations.length,
73
+ }
74
+ : undefined,
75
+ mcpServers: servers.map((s) => ({
76
+ id: s.id,
77
+ name: s.name,
78
+ state: s.state,
79
+ toolCount: s.toolCount,
80
+ health: {
81
+ circuit: s.health.state,
82
+ failureCount: s.health.metrics.failureCount,
83
+ remainingCooldownMs: s.health.remainingCooldownMs,
84
+ },
85
+ })),
86
+ };
87
+ }
@@ -1,93 +1,11 @@
1
1
  import { getSecretVaultService } from '../../services/secret-vault.js';
2
2
  import { sendOk } from '../shared.js';
3
+ import { buildHealthData } from './health-data.js';
3
4
  const startTime = Date.now();
4
5
  /** GET /health — Returns system health status and subsystem summaries. */
5
6
  export function handleHealth(deps) {
6
7
  return async (_req, res) => {
7
- const summary = deps.skillRegistry.summary();
8
- const servers = deps.mcpManager.listServers();
9
- const packageDiagnostics = await deps.mcpManager.getSkillPackageDiagnostics();
10
- const secretDiagnostics = getSecretVaultService().getDiagnostics(['API_SECRET']);
11
- const budgetSnapshot = deps.budgetGovernor?.getSnapshot('health');
12
- const routingSnapshot = deps.modelRouter?.getHealthSnapshot();
13
- const backupDiagnostics = deps.localStateBackup
14
- ? await deps.localStateBackup.getDiagnostics(5)
15
- : null;
16
- const heartbeatRunning = deps.heartbeat.scheduler
17
- .listJobs()
18
- .some((j) => j.status === 'running');
19
- const data = {
20
- status: servers.some((s) => s.state === 'error') ||
21
- packageDiagnostics.blockedPackageCount > 0 ||
22
- secretDiagnostics.health.hasIssues ||
23
- budgetSnapshot?.directive.severity === 'hard_limit' ||
24
- (routingSnapshot?.consecutiveFailures ?? 0) >= 3 ||
25
- backupDiagnostics?.status === 'degraded'
26
- ? 'degraded'
27
- : 'ok',
28
- uptimeSec: Math.floor((Date.now() - startTime) / 1000),
29
- memoryUsageMb: Math.round(process.memoryUsage().rss / 1024 / 1024),
30
- heartbeat: { running: heartbeatRunning },
31
- skills: {
32
- builtin: summary.builtin ?? 0,
33
- mcp: summary.mcp ?? 0,
34
- total: deps.skillRegistry.size,
35
- },
36
- skillPackages: {
37
- installed: packageDiagnostics.installed.length,
38
- active: packageDiagnostics.activePackageCount,
39
- blocked: packageDiagnostics.blockedPackageCount,
40
- warnings: packageDiagnostics.warnings,
41
- violations: packageDiagnostics.violations.map((violation) => ({
42
- packageName: violation.packageName,
43
- version: violation.version,
44
- code: violation.code,
45
- message: violation.message,
46
- remediation: violation.remediation,
47
- })),
48
- },
49
- secrets: {
50
- status: secretDiagnostics.health.hasIssues ? 'degraded' : 'ok',
51
- missingRequired: secretDiagnostics.health.missingRequired,
52
- expired: secretDiagnostics.health.expired,
53
- warnings: secretDiagnostics.health.warnings,
54
- total: secretDiagnostics.total,
55
- active: secretDiagnostics.active,
56
- dueForRotation: secretDiagnostics.dueForRotation,
57
- },
58
- budget: budgetSnapshot
59
- ? {
60
- severity: budgetSnapshot.directive.severity,
61
- profile: budgetSnapshot.directive.profile,
62
- pacingDelayMs: budgetSnapshot.directive.pacingDelayMs,
63
- manualProfile: budgetSnapshot.manualProfile,
64
- daily: budgetSnapshot.daily,
65
- session: budgetSnapshot.session,
66
- providers: budgetSnapshot.providers,
67
- }
68
- : undefined,
69
- routing: routingSnapshot,
70
- backups: backupDiagnostics
71
- ? {
72
- status: backupDiagnostics.status,
73
- lastSnapshotAt: backupDiagnostics.lastSnapshotAt,
74
- lastRestoreAt: backupDiagnostics.lastRestoreAt,
75
- validationFailureCount: backupDiagnostics.validationFailureCount,
76
- recommendationCount: backupDiagnostics.recommendations.length,
77
- }
78
- : undefined,
79
- mcpServers: servers.map((s) => ({
80
- id: s.id,
81
- name: s.name,
82
- state: s.state,
83
- toolCount: s.toolCount,
84
- health: {
85
- circuit: s.health.state,
86
- failureCount: s.health.metrics.failureCount,
87
- remainingCooldownMs: s.health.remainingCooldownMs,
88
- },
89
- })),
90
- };
8
+ const data = await buildHealthData(deps, startTime);
91
9
  sendOk(res, data);
92
10
  };
93
11
  }
@@ -0,0 +1,144 @@
1
+ import { sendError, sendOk } from '../shared.js';
2
+ import { getLearningSystem } from '../../services/learning-system.js';
3
+ import { getSkillBuilder } from '../../services/skill-builder.js';
4
+ import { getAutoConfigurer } from '../../services/auto-configurer.js';
5
+ import { getSelfHealingService } from '../../services/self-healing.js';
6
+ import { getAgentImprovementTool } from '../../tools/agent-improvement.js';
7
+ export function registerSelfRoutes(app) {
8
+ app.get('/self/stats', (_req, res) => {
9
+ try {
10
+ const learning = getLearningSystem();
11
+ const stats = learning.getStats();
12
+ sendOk(res, stats);
13
+ }
14
+ catch (err) {
15
+ sendError(res, `Failed to get stats: ${err instanceof Error ? err.message : String(err)}`, 500);
16
+ }
17
+ });
18
+ app.get('/self/health', async (_req, res) => {
19
+ try {
20
+ const healer = getSelfHealingService();
21
+ const metrics = await healer.performHealthCheck();
22
+ sendOk(res, { metrics });
23
+ }
24
+ catch (err) {
25
+ sendError(res, `Health check failed: ${err instanceof Error ? err.message : String(err)}`, 500);
26
+ }
27
+ });
28
+ app.get('/self/learn', (req, res) => {
29
+ const query = typeof req.query.q === 'string' ? req.query.q : '';
30
+ if (!query) {
31
+ sendError(res, 'Query parameter "q" is required.', 400);
32
+ return;
33
+ }
34
+ try {
35
+ const learning = getLearningSystem();
36
+ const result = learning.query(query);
37
+ sendOk(res, result);
38
+ }
39
+ catch (err) {
40
+ sendError(res, `Query failed: ${err instanceof Error ? err.message : String(err)}`, 500);
41
+ }
42
+ });
43
+ app.post('/self/learn', async (req, res) => {
44
+ const { pattern, solution, type } = req.body ?? {};
45
+ if (!pattern || !solution) {
46
+ sendError(res, 'Required fields: pattern, solution', 400);
47
+ return;
48
+ }
49
+ try {
50
+ let entryType = 'fix';
51
+ if (type === 'success' ||
52
+ type === 'failure' ||
53
+ type === 'pattern' ||
54
+ type === 'fix' ||
55
+ type === 'config') {
56
+ entryType = type;
57
+ }
58
+ const learning = getLearningSystem();
59
+ await learning.learn(entryType, pattern, {}, solution, 'success');
60
+ sendOk(res, { message: `Learned: ${pattern} -> ${solution}` });
61
+ }
62
+ catch (err) {
63
+ sendError(res, `Learn failed: ${err instanceof Error ? err.message : String(err)}`, 500);
64
+ }
65
+ });
66
+ app.get('/self/services', (_req, res) => {
67
+ try {
68
+ const configurer = getAutoConfigurer();
69
+ const services = configurer.listKnownServices();
70
+ sendOk(res, { services });
71
+ }
72
+ catch (err) {
73
+ sendError(res, `Failed to list services: ${err instanceof Error ? err.message : String(err)}`, 500);
74
+ }
75
+ });
76
+ app.get('/self/configure/:service', async (req, res) => {
77
+ const service = req.params.service;
78
+ try {
79
+ const configurer = getAutoConfigurer();
80
+ const { config, steps } = await configurer.fetchDocsAndConfig(service);
81
+ sendOk(res, { service, config, steps });
82
+ }
83
+ catch (err) {
84
+ sendError(res, `Configure failed: ${err instanceof Error ? err.message : String(err)}`, 500);
85
+ }
86
+ });
87
+ app.post('/self/configure/:service', async (req, res) => {
88
+ const service = req.params.service;
89
+ const config = req.body;
90
+ try {
91
+ const configurer = getAutoConfigurer();
92
+ const result = await configurer.saveServiceConfig(service, config);
93
+ sendOk(res, result);
94
+ }
95
+ catch (err) {
96
+ sendError(res, `Save config failed: ${err instanceof Error ? err.message : String(err)}`, 500);
97
+ }
98
+ });
99
+ app.post('/self/build-skill', async (req, res) => {
100
+ const { description, purpose } = req.body ?? {};
101
+ if (!description) {
102
+ sendError(res, 'Required field: description', 400);
103
+ return;
104
+ }
105
+ try {
106
+ const builder = getSkillBuilder();
107
+ const skill = await builder.buildFromRequest({ description, purpose });
108
+ sendOk(res, {
109
+ id: skill.id,
110
+ name: skill.name,
111
+ category: skill.category,
112
+ location: `${builder.getCustomSkillsDir()}/${skill.id}.ts`,
113
+ });
114
+ }
115
+ catch (err) {
116
+ sendError(res, `Build skill failed: ${err instanceof Error ? err.message : String(err)}`, 500);
117
+ }
118
+ });
119
+ app.get('/self/skills', (_req, res) => {
120
+ try {
121
+ const builder = getSkillBuilder();
122
+ const skills = builder.getCustomSkills();
123
+ sendOk(res, { skills });
124
+ }
125
+ catch (err) {
126
+ sendError(res, `Failed to list skills: ${err instanceof Error ? err.message : String(err)}`, 500);
127
+ }
128
+ });
129
+ app.post('/self/improve', async (req, res) => {
130
+ const { goal, context } = req.body ?? {};
131
+ if (!goal) {
132
+ sendError(res, 'Required field: goal', 400);
133
+ return;
134
+ }
135
+ try {
136
+ const tool = getAgentImprovementTool();
137
+ const result = await tool.handleImprovementRequest({ goal, context });
138
+ sendOk(res, result);
139
+ }
140
+ catch (err) {
141
+ sendError(res, `Improve request failed: ${err instanceof Error ? err.message : String(err)}`, 500);
142
+ }
143
+ });
144
+ }
@@ -0,0 +1,90 @@
1
+ import { sendError, sendOk } from '../shared.js';
2
+ import { getSkillBuilder } from '../../services/skill-builder.js';
3
+ import { getIntentParser } from '../../services/skill-acquisition/intent-parser.js';
4
+ import { getResearchEngine } from '../../services/skill-acquisition/research-engine.js';
5
+ export function registerSkillAcquisitionRoutes(app) {
6
+ // POST /api/skills/acquire - Start acquiring a new skill
7
+ app.post('/skills/acquire', async (req, res) => {
8
+ const { request } = req.body ?? {};
9
+ if (!request) {
10
+ sendError(res, 'Required field: request (natural language description)', 400);
11
+ return;
12
+ }
13
+ try {
14
+ const intentParser = getIntentParser();
15
+ const intent = await intentParser.parseIntent(request);
16
+ const researchEngine = getResearchEngine();
17
+ const research = await researchEngine.research(intent);
18
+ const skillBuilder = getSkillBuilder();
19
+ const skill = await skillBuilder.buildFromRequest({
20
+ description: request,
21
+ purpose: 'user requested',
22
+ });
23
+ sendOk(res, {
24
+ status: 'generated',
25
+ intent,
26
+ research,
27
+ skill: {
28
+ id: skill.id,
29
+ name: skill.name,
30
+ description: skill.description,
31
+ category: skill.category,
32
+ },
33
+ nextSteps: [
34
+ 'Review the generated skill',
35
+ 'Provide credentials if needed',
36
+ 'Approve to activate the skill',
37
+ ],
38
+ });
39
+ }
40
+ catch (err) {
41
+ sendError(res, `Skill acquisition failed: ${err instanceof Error ? err.message : String(err)}`, 500);
42
+ }
43
+ });
44
+ // GET /api/skills - List all custom skills
45
+ app.get('/skills', (_req, res) => {
46
+ try {
47
+ const builder = getSkillBuilder();
48
+ const customSkills = builder.getCustomSkills();
49
+ sendOk(res, {
50
+ customSkills,
51
+ total: customSkills.length,
52
+ });
53
+ }
54
+ catch (err) {
55
+ sendError(res, `Failed to list skills: ${err instanceof Error ? err.message : String(err)}`, 500);
56
+ }
57
+ });
58
+ // GET /api/skills/:id - Get skill details
59
+ app.get('/skills/:id', (req, res) => {
60
+ const { id } = req.params;
61
+ try {
62
+ const builder = getSkillBuilder();
63
+ const skill = builder.getSkill(id);
64
+ if (!skill) {
65
+ sendError(res, 'Skill not found', 404);
66
+ return;
67
+ }
68
+ sendOk(res, skill);
69
+ }
70
+ catch (err) {
71
+ sendError(res, `Failed to get skill: ${err instanceof Error ? err.message : String(err)}`, 500);
72
+ }
73
+ });
74
+ // DELETE /api/skills/:id - Delete a custom skill
75
+ app.delete('/skills/:id', async (req, res) => {
76
+ const { id } = req.params;
77
+ try {
78
+ const builder = getSkillBuilder();
79
+ const deleted = await builder.deleteSkill(id);
80
+ if (!deleted) {
81
+ sendError(res, 'Skill not found or could not be deleted', 404);
82
+ return;
83
+ }
84
+ sendOk(res, { message: 'Skill deleted successfully' });
85
+ }
86
+ catch (err) {
87
+ sendError(res, `Failed to delete skill: ${err instanceof Error ? err.message : String(err)}`, 500);
88
+ }
89
+ });
90
+ }