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
package/README.md CHANGED
@@ -37,11 +37,11 @@ When you first run TwinClaw, it will automatically start a **Guided Setup Wizard
37
37
  3. **Security**: Generates a master encryption key for your local vault.
38
38
  4. **Skills**: Auto-registers built-in skills for immediate use.
39
39
 
40
- TwinClaw now defaults to `dmPolicy: "pairing"` for Telegram/WhatsApp DMs. Unknown senders receive a pairing code and must be explicitly approved:
40
+ TwinClaw now defaults to `dmPolicy: "allowlist"` for Telegram/WhatsApp DMs. Only allowlisted users can message the bot. Unknown senders must be explicitly added:
41
41
 
42
42
  ```powershell
43
- node src/index.ts pairing list telegram
44
- node src/index.ts pairing approve telegram <CODE>
43
+ node src/index.ts allowlist add telegram <userId>
44
+ node src/index.ts allowlist list telegram
45
45
  ```
46
46
 
47
47
  ---
@@ -0,0 +1,82 @@
1
+ import { sendOk, sendError } from '../shared.js';
2
+ /** GET /agents — List all sub-agents */
3
+ export function handleAgentsList(deps) {
4
+ return (_req, res) => {
5
+ const agents = deps.subAgentService.list();
6
+ const data = {
7
+ agents: agents.map((agent) => ({
8
+ id: agent.id,
9
+ name: agent.name,
10
+ model: agent.model,
11
+ status: agent.status,
12
+ createdAt: agent.createdAt.toISOString(),
13
+ startedAt: agent.startedAt?.toISOString() ?? null,
14
+ completedAt: agent.completedAt?.toISOString() ?? null,
15
+ error: agent.error,
16
+ steps: agent.steps.length,
17
+ })),
18
+ total: agents.length,
19
+ running: agents.filter((a) => a.status === 'running').length,
20
+ completed: agents.filter((a) => a.status === 'completed').length,
21
+ failed: agents.filter((a) => a.status === 'failed').length,
22
+ cancelled: agents.filter((a) => a.status === 'cancelled').length,
23
+ };
24
+ sendOk(res, data);
25
+ };
26
+ }
27
+ /** GET /agents/:id — Get a specific agent */
28
+ export function handleAgentsGet(deps) {
29
+ return (req, res) => {
30
+ const id = req.params.id;
31
+ const agent = deps.subAgentService.get(id);
32
+ if (!agent) {
33
+ sendError(res, `Agent not found: ${id}`, 404);
34
+ return;
35
+ }
36
+ const data = {
37
+ id: agent.id,
38
+ name: agent.name,
39
+ model: agent.model,
40
+ systemPrompt: agent.systemPrompt,
41
+ maxSteps: agent.maxSteps,
42
+ timeoutMs: agent.timeoutMs,
43
+ reportBackTo: agent.reportBackTo,
44
+ status: agent.status,
45
+ createdAt: agent.createdAt.toISOString(),
46
+ startedAt: agent.startedAt?.toISOString() ?? null,
47
+ completedAt: agent.completedAt?.toISOString() ?? null,
48
+ result: agent.result,
49
+ error: agent.error,
50
+ steps: agent.steps.map((step) => ({
51
+ step: step.step,
52
+ action: step.action,
53
+ observation: step.observation,
54
+ startedAt: step.startedAt.toISOString(),
55
+ completedAt: step.completedAt?.toISOString() ?? null,
56
+ })),
57
+ };
58
+ sendOk(res, data);
59
+ };
60
+ }
61
+ /** POST /agents/:id/cancel — Cancel a running agent */
62
+ export function handleAgentsCancel(deps) {
63
+ return (req, res) => {
64
+ const id = req.params.id;
65
+ const agent = deps.subAgentService.get(id);
66
+ if (!agent) {
67
+ sendError(res, `Agent not found: ${id}`, 404);
68
+ return;
69
+ }
70
+ if (agent.status !== 'running') {
71
+ sendError(res, `Agent ${id} is not running (status: ${agent.status})`, 400);
72
+ return;
73
+ }
74
+ const cancelled = deps.subAgentService.cancel(id);
75
+ if (cancelled) {
76
+ sendOk(res, { message: `Agent ${id} cancelled`, agentId: id });
77
+ }
78
+ else {
79
+ sendError(res, `Failed to cancel agent ${id}`, 500);
80
+ }
81
+ };
82
+ }
@@ -0,0 +1,69 @@
1
+ import { sendOk, sendError } from '../shared.js';
2
+ import { readFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ /** GET /debug — Extended diagnostics including full context budget, recent logs, active connections, performance metrics */
6
+ export function handleDebug(deps) {
7
+ return async (req, res) => {
8
+ try {
9
+ const limit = typeof req.query.limit === 'string'
10
+ ? Math.min(500, Math.max(1, parseInt(req.query.limit, 10) || 50))
11
+ : 50;
12
+ const budgetFull = deps.budgetGovernor?.getSnapshot('health');
13
+ const routingTelemetry = deps.modelRouter?.getHealthSnapshot();
14
+ const logs = await getRecentLogs(limit);
15
+ const debugData = {
16
+ system: {
17
+ platform: os.platform(),
18
+ arch: os.arch(),
19
+ cpus: os.cpus().length,
20
+ totalMemoryMb: Math.round(os.totalmem() / 1024 / 1024),
21
+ freeMemoryMb: Math.round(os.freemem() / 1024 / 1024),
22
+ uptimeSec: os.uptime(),
23
+ nodeVersion: process.version,
24
+ },
25
+ runtime: {
26
+ memoryUsageMb: Math.round(process.memoryUsage().rss / 1024 / 1024),
27
+ heapUsedMb: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
28
+ heapTotalMb: Math.round(process.memoryUsage().heapTotal / 1024 / 1024),
29
+ externalMb: Math.round(process.memoryUsage().external / 1024 / 1024),
30
+ arrayBuffersMb: Math.round((process.memoryUsage().arrayBuffers ?? 0) / 1024 / 1024),
31
+ },
32
+ budget: budgetFull ? {
33
+ context: budgetFull,
34
+ } : undefined,
35
+ routing: routingTelemetry ? {
36
+ telemetry: routingTelemetry,
37
+ } : undefined,
38
+ recentLogs: logs,
39
+ };
40
+ sendOk(res, debugData);
41
+ }
42
+ catch (err) {
43
+ sendError(res, `Failed to get debug info: ${err instanceof Error ? err.message : String(err)}`, 500);
44
+ }
45
+ };
46
+ }
47
+ async function getRecentLogs(limit) {
48
+ 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) {
53
+ return [];
54
+ }
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();
65
+ }
66
+ catch {
67
+ return [];
68
+ }
69
+ }
@@ -0,0 +1,79 @@
1
+ import { sendOk, sendError } from '../shared.js';
2
+ /** GET /devices — List all paired devices */
3
+ export function handleDevicesList(deps) {
4
+ return (_req, res) => {
5
+ const devices = deps.devicePairingService.list();
6
+ const data = {
7
+ devices: devices.map((device) => ({
8
+ deviceId: device.deviceId,
9
+ displayName: device.displayName,
10
+ platform: device.platform,
11
+ clientMode: device.clientMode,
12
+ roles: device.roles,
13
+ status: device.status,
14
+ capabilities: device.capabilities,
15
+ lastSeenAt: device.lastSeenAt ?? null,
16
+ pairedAt: device.pairedAt ?? null,
17
+ })),
18
+ total: devices.length,
19
+ paired: devices.filter((d) => d.status === 'paired').length,
20
+ disconnected: devices.filter((d) => d.status === 'disconnected').length,
21
+ revoked: devices.filter((d) => d.status === 'revoked').length,
22
+ };
23
+ sendOk(res, data);
24
+ };
25
+ }
26
+ /** GET /devices/:id — Get a specific device */
27
+ export function handleDevicesGet(deps) {
28
+ return (req, res) => {
29
+ const deviceId = req.params.id;
30
+ const device = deps.devicePairingService.get(deviceId);
31
+ if (!device) {
32
+ sendError(res, `Device not found: ${deviceId}`, 404);
33
+ return;
34
+ }
35
+ const data = {
36
+ deviceId: device.deviceId,
37
+ displayName: device.displayName,
38
+ platform: device.platform,
39
+ clientId: device.clientId,
40
+ clientMode: device.clientMode,
41
+ roles: device.roles,
42
+ scopes: device.scopes,
43
+ capabilities: device.capabilities,
44
+ status: device.status,
45
+ lastSeenAt: device.lastSeenAt ?? null,
46
+ pairedAt: device.pairedAt ?? null,
47
+ createdAt: device.createdAt,
48
+ };
49
+ sendOk(res, data);
50
+ };
51
+ }
52
+ /** POST /devices/:id/command — Send a command to a device */
53
+ export function handleDevicesCommand(deps) {
54
+ return async (req, res) => {
55
+ const deviceId = req.params.id;
56
+ const { command, args } = req.body;
57
+ if (!command || typeof command !== 'string') {
58
+ sendError(res, 'Required field: command (string)', 400);
59
+ return;
60
+ }
61
+ try {
62
+ const result = await deps.devicePairingService.executeCommand(deviceId, command, args);
63
+ if (result.success) {
64
+ sendOk(res, {
65
+ deviceId,
66
+ command,
67
+ output: result.output,
68
+ });
69
+ }
70
+ else {
71
+ sendError(res, result.error || 'Command execution failed', 400);
72
+ }
73
+ }
74
+ catch (err) {
75
+ const message = err instanceof Error ? err.message : String(err);
76
+ sendError(res, `Command execution failed: ${message}`, 500);
77
+ }
78
+ };
79
+ }
@@ -0,0 +1,99 @@
1
+ import { sendOk, sendError } from '../shared.js';
2
+ /** GET /jobs — List all scheduled jobs */
3
+ export function handleJobsList(deps) {
4
+ return (_req, res) => {
5
+ const jobs = deps.scheduler.listJobs();
6
+ const data = {
7
+ jobs: jobs.map((job) => ({
8
+ id: job.id,
9
+ cronExpression: job.cronExpression,
10
+ description: job.description,
11
+ status: job.status,
12
+ lastRunAt: job.lastRunAt?.toISOString() ?? null,
13
+ lastError: job.lastError,
14
+ })),
15
+ total: jobs.length,
16
+ running: jobs.filter((j) => j.status === 'running').length,
17
+ idle: jobs.filter((j) => j.status === 'idle').length,
18
+ error: jobs.filter((j) => j.status === 'error').length,
19
+ };
20
+ sendOk(res, data);
21
+ };
22
+ }
23
+ /** GET /jobs/:id — Get a specific job */
24
+ export function handleJobsGet(deps) {
25
+ return (req, res) => {
26
+ const id = req.params.id;
27
+ const job = deps.scheduler.getJob(id);
28
+ if (!job) {
29
+ sendError(res, `Job not found: ${id}`, 404);
30
+ return;
31
+ }
32
+ const data = {
33
+ id: job.id,
34
+ cronExpression: job.cronExpression,
35
+ description: job.description,
36
+ status: job.status,
37
+ lastRunAt: job.lastRunAt?.toISOString() ?? null,
38
+ lastError: job.lastError,
39
+ };
40
+ sendOk(res, data);
41
+ };
42
+ }
43
+ /** POST /jobs/:id/run — Trigger a job manually */
44
+ export function handleJobsRun(deps) {
45
+ return async (req, res) => {
46
+ const id = req.params.id;
47
+ const job = deps.scheduler.getJob(id);
48
+ if (!job) {
49
+ sendError(res, `Job not found: ${id}`, 404);
50
+ return;
51
+ }
52
+ try {
53
+ deps.scheduler.start(id);
54
+ sendOk(res, { message: `Job ${id} triggered successfully`, jobId: id });
55
+ }
56
+ catch (err) {
57
+ const message = err instanceof Error ? err.message : String(err);
58
+ sendError(res, `Failed to run job: ${message}`, 500);
59
+ }
60
+ };
61
+ }
62
+ /** POST /jobs/:id/start — Start a stopped job */
63
+ export function handleJobsStart(deps) {
64
+ return (req, res) => {
65
+ const id = req.params.id;
66
+ const job = deps.scheduler.getJob(id);
67
+ if (!job) {
68
+ sendError(res, `Job not found: ${id}`, 404);
69
+ return;
70
+ }
71
+ try {
72
+ deps.scheduler.start(id);
73
+ sendOk(res, { message: `Job ${id} started`, jobId: id });
74
+ }
75
+ catch (err) {
76
+ const message = err instanceof Error ? err.message : String(err);
77
+ sendError(res, `Failed to start job: ${message}`, 500);
78
+ }
79
+ };
80
+ }
81
+ /** POST /jobs/:id/stop — Stop a running job */
82
+ export function handleJobsStop(deps) {
83
+ return (req, res) => {
84
+ const id = req.params.id;
85
+ const job = deps.scheduler.getJob(id);
86
+ if (!job) {
87
+ sendError(res, `Job not found: ${id}`, 404);
88
+ return;
89
+ }
90
+ try {
91
+ deps.scheduler.stop(id);
92
+ sendOk(res, { message: `Job ${id} stopped`, jobId: id });
93
+ }
94
+ catch (err) {
95
+ const message = err instanceof Error ? err.message : String(err);
96
+ sendError(res, `Failed to stop job: ${message}`, 500);
97
+ }
98
+ };
99
+ }
@@ -0,0 +1,149 @@
1
+ import { getSecretVaultService } from '../../services/secret-vault.js';
2
+ import { sendOk } from '../shared.js';
3
+ const startTime = Date.now();
4
+ function getVersion() {
5
+ try {
6
+ const pkg = JSON.parse(require.resolve('../../package.json').replace('file://', ''));
7
+ return pkg.version ?? '1.0.0';
8
+ }
9
+ catch {
10
+ return '1.0.0';
11
+ }
12
+ }
13
+ /** GET /status — Unified diagnostics view combining health + budget + routing + jobs + agents + devices */
14
+ export function handleStatus(deps) {
15
+ return async (_req, res) => {
16
+ const summary = deps.skillRegistry.summary();
17
+ const servers = deps.mcpManager.listServers();
18
+ const packageDiagnostics = await deps.mcpManager.getSkillPackageDiagnostics();
19
+ const secretDiagnostics = getSecretVaultService().getDiagnostics(['API_SECRET']);
20
+ const budgetSnapshot = deps.budgetGovernor?.getSnapshot('health');
21
+ const routingSnapshot = deps.modelRouter?.getHealthSnapshot();
22
+ const backupDiagnostics = deps.localStateBackup
23
+ ? await deps.localStateBackup.getDiagnostics(5)
24
+ : null;
25
+ const heartbeatRunning = deps.heartbeat.scheduler
26
+ .listJobs()
27
+ .some((j) => j.status === 'running');
28
+ const healthData = {
29
+ status: servers.some((s) => s.state === 'error') ||
30
+ packageDiagnostics.blockedPackageCount > 0 ||
31
+ secretDiagnostics.health.hasIssues ||
32
+ budgetSnapshot?.directive.severity === 'hard_limit' ||
33
+ (routingSnapshot?.consecutiveFailures ?? 0) >= 3 ||
34
+ backupDiagnostics?.status === 'degraded'
35
+ ? 'degraded'
36
+ : 'ok',
37
+ uptimeSec: Math.floor((Date.now() - startTime) / 1000),
38
+ memoryUsageMb: Math.round(process.memoryUsage().rss / 1024 / 1024),
39
+ heartbeat: { running: heartbeatRunning },
40
+ skills: {
41
+ builtin: summary.builtin ?? 0,
42
+ mcp: summary.mcp ?? 0,
43
+ total: deps.skillRegistry.size,
44
+ },
45
+ skillPackages: {
46
+ installed: packageDiagnostics.installed.length,
47
+ active: packageDiagnostics.activePackageCount,
48
+ blocked: packageDiagnostics.blockedPackageCount,
49
+ warnings: packageDiagnostics.warnings,
50
+ violations: packageDiagnostics.violations.map((violation) => ({
51
+ packageName: violation.packageName,
52
+ version: violation.version,
53
+ code: violation.code,
54
+ message: violation.message,
55
+ remediation: violation.remediation,
56
+ })),
57
+ },
58
+ secrets: {
59
+ status: secretDiagnostics.health.hasIssues ? 'degraded' : 'ok',
60
+ missingRequired: secretDiagnostics.health.missingRequired,
61
+ expired: secretDiagnostics.health.expired,
62
+ warnings: secretDiagnostics.health.warnings,
63
+ total: secretDiagnostics.total,
64
+ active: secretDiagnostics.active,
65
+ dueForRotation: secretDiagnostics.dueForRotation,
66
+ },
67
+ budget: budgetSnapshot
68
+ ? {
69
+ severity: budgetSnapshot.directive.severity,
70
+ profile: budgetSnapshot.directive.profile,
71
+ pacingDelayMs: budgetSnapshot.directive.pacingDelayMs,
72
+ manualProfile: budgetSnapshot.manualProfile,
73
+ daily: budgetSnapshot.daily,
74
+ session: budgetSnapshot.session,
75
+ providers: budgetSnapshot.providers,
76
+ }
77
+ : undefined,
78
+ routing: routingSnapshot,
79
+ backups: backupDiagnostics
80
+ ? {
81
+ status: backupDiagnostics.status,
82
+ lastSnapshotAt: backupDiagnostics.lastSnapshotAt,
83
+ lastRestoreAt: backupDiagnostics.lastRestoreAt,
84
+ validationFailureCount: backupDiagnostics.validationFailureCount,
85
+ recommendationCount: backupDiagnostics.recommendations.length,
86
+ }
87
+ : undefined,
88
+ mcpServers: servers.map((s) => ({
89
+ id: s.id,
90
+ name: s.name,
91
+ state: s.state,
92
+ toolCount: s.toolCount,
93
+ health: {
94
+ circuit: s.health.state,
95
+ failureCount: s.health.metrics.failureCount,
96
+ remainingCooldownMs: s.health.remainingCooldownMs,
97
+ },
98
+ })),
99
+ };
100
+ const jobList = deps.jobScheduler?.listJobs() ?? [];
101
+ const agentList = deps.subAgentService?.list() ?? [];
102
+ const deviceList = deps.devicePairingService?.list() ?? [];
103
+ const currentIncidents = deps.incidentManager?.getCurrentIncidents() ?? [];
104
+ const data = {
105
+ status: healthData.status,
106
+ uptimeSec: healthData.uptimeSec,
107
+ memoryUsageMb: healthData.memoryUsageMb,
108
+ version: getVersion(),
109
+ health: healthData,
110
+ jobs: {
111
+ count: jobList.length,
112
+ list: jobList.map((j) => ({
113
+ id: j.id,
114
+ cronExpression: j.cronExpression,
115
+ description: j.description,
116
+ status: j.status,
117
+ lastRunAt: j.lastRunAt?.toISOString() ?? null,
118
+ lastError: j.lastError,
119
+ })),
120
+ },
121
+ agents: {
122
+ count: agentList.length,
123
+ list: agentList.map((a) => ({
124
+ id: a.id,
125
+ name: a.name,
126
+ status: a.status,
127
+ createdAt: a.createdAt.toISOString(),
128
+ startedAt: a.startedAt?.toISOString(),
129
+ completedAt: a.completedAt?.toISOString(),
130
+ })),
131
+ },
132
+ devices: {
133
+ count: deviceList.length,
134
+ list: deviceList.map((d) => ({
135
+ deviceId: d.deviceId,
136
+ displayName: d.displayName,
137
+ platform: d.platform,
138
+ status: d.status,
139
+ lastSeenAt: d.lastSeenAt,
140
+ })),
141
+ },
142
+ incidents: {
143
+ safeMode: deps.incidentManager?.isSafeModeEnabled() ?? false,
144
+ count: currentIncidents.length,
145
+ },
146
+ };
147
+ sendOk(res, data);
148
+ };
149
+ }