twinclaw 1.0.0

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 (132) hide show
  1. package/README.md +66 -0
  2. package/bin/npm-twinclaw.js +17 -0
  3. package/bin/run-twinbot-cli.js +36 -0
  4. package/bin/twinbot.js +4 -0
  5. package/bin/twinclaw.js +4 -0
  6. package/dist/api/handlers/browser.js +160 -0
  7. package/dist/api/handlers/callback.js +80 -0
  8. package/dist/api/handlers/config-validate.js +19 -0
  9. package/dist/api/handlers/health.js +117 -0
  10. package/dist/api/handlers/local-state-backup.js +118 -0
  11. package/dist/api/handlers/persona-state.js +59 -0
  12. package/dist/api/handlers/skill-packages.js +94 -0
  13. package/dist/api/router.js +278 -0
  14. package/dist/api/runtime-event-producer.js +99 -0
  15. package/dist/api/shared.js +82 -0
  16. package/dist/api/websocket-hub.js +305 -0
  17. package/dist/config/config-loader.js +2 -0
  18. package/dist/config/env-schema.js +202 -0
  19. package/dist/config/env-validator.js +223 -0
  20. package/dist/config/identity-bootstrap.js +115 -0
  21. package/dist/config/json-config.js +344 -0
  22. package/dist/config/workspace.js +186 -0
  23. package/dist/core/channels-cli.js +77 -0
  24. package/dist/core/cli.js +119 -0
  25. package/dist/core/context-assembly.js +33 -0
  26. package/dist/core/doctor.js +365 -0
  27. package/dist/core/gateway-cli.js +323 -0
  28. package/dist/core/gateway.js +416 -0
  29. package/dist/core/heartbeat.js +54 -0
  30. package/dist/core/install-cli.js +320 -0
  31. package/dist/core/lane-executor.js +134 -0
  32. package/dist/core/logs-cli.js +70 -0
  33. package/dist/core/onboarding.js +760 -0
  34. package/dist/core/pairing-cli.js +78 -0
  35. package/dist/core/secret-vault-cli.js +204 -0
  36. package/dist/core/types.js +1 -0
  37. package/dist/index.js +404 -0
  38. package/dist/interfaces/dispatcher.js +214 -0
  39. package/dist/interfaces/telegram_handler.js +82 -0
  40. package/dist/interfaces/tui-dashboard.js +53 -0
  41. package/dist/interfaces/whatsapp_handler.js +94 -0
  42. package/dist/release/cli.js +97 -0
  43. package/dist/release/mvp-gate-cli.js +118 -0
  44. package/dist/release/twinbot-config-schema.js +162 -0
  45. package/dist/release/twinclaw-config-schema.js +162 -0
  46. package/dist/services/block-chunker.js +174 -0
  47. package/dist/services/browser-service.js +334 -0
  48. package/dist/services/context-lifecycle.js +314 -0
  49. package/dist/services/db.js +1055 -0
  50. package/dist/services/delivery-tracker.js +110 -0
  51. package/dist/services/dm-pairing.js +245 -0
  52. package/dist/services/embedding-service.js +125 -0
  53. package/dist/services/file-watcher.js +125 -0
  54. package/dist/services/inbound-debounce.js +92 -0
  55. package/dist/services/incident-manager.js +516 -0
  56. package/dist/services/job-scheduler.js +176 -0
  57. package/dist/services/local-state-backup.js +682 -0
  58. package/dist/services/mcp-client-adapter.js +291 -0
  59. package/dist/services/mcp-server-manager.js +143 -0
  60. package/dist/services/model-router.js +927 -0
  61. package/dist/services/mvp-gate.js +845 -0
  62. package/dist/services/orchestration-service.js +422 -0
  63. package/dist/services/persona-state.js +256 -0
  64. package/dist/services/policy-engine.js +92 -0
  65. package/dist/services/proactive-notifier.js +94 -0
  66. package/dist/services/queue-service.js +146 -0
  67. package/dist/services/release-pipeline.js +652 -0
  68. package/dist/services/runtime-budget-governor.js +415 -0
  69. package/dist/services/secret-vault.js +704 -0
  70. package/dist/services/semantic-memory.js +249 -0
  71. package/dist/services/skill-package-manager.js +806 -0
  72. package/dist/services/skill-registry.js +122 -0
  73. package/dist/services/streaming-output.js +75 -0
  74. package/dist/services/stt-service.js +39 -0
  75. package/dist/services/tts-service.js +44 -0
  76. package/dist/skills/builtin.js +250 -0
  77. package/dist/skills/shell.js +87 -0
  78. package/dist/skills/types.js +1 -0
  79. package/dist/types/api.js +1 -0
  80. package/dist/types/context-budget.js +1 -0
  81. package/dist/types/doctor.js +1 -0
  82. package/dist/types/file-watcher.js +1 -0
  83. package/dist/types/incident.js +1 -0
  84. package/dist/types/local-state-backup.js +1 -0
  85. package/dist/types/mcp.js +1 -0
  86. package/dist/types/messaging.js +1 -0
  87. package/dist/types/model-routing.js +1 -0
  88. package/dist/types/mvp-gate.js +2 -0
  89. package/dist/types/orchestration.js +1 -0
  90. package/dist/types/persona-state.js +22 -0
  91. package/dist/types/policy.js +1 -0
  92. package/dist/types/reasoning-graph.js +1 -0
  93. package/dist/types/release.js +1 -0
  94. package/dist/types/reliability.js +1 -0
  95. package/dist/types/runtime-budget.js +1 -0
  96. package/dist/types/scheduler.js +1 -0
  97. package/dist/types/secret-vault.js +1 -0
  98. package/dist/types/skill-packages.js +1 -0
  99. package/dist/types/websocket.js +14 -0
  100. package/dist/utils/logger.js +57 -0
  101. package/dist/utils/retry.js +61 -0
  102. package/dist/utils/secret-scan.js +208 -0
  103. package/mcp-servers.json +179 -0
  104. package/package.json +81 -0
  105. package/skill-packages.json +92 -0
  106. package/skill-packages.lock.json +5 -0
  107. package/src/skills/builtin.ts +275 -0
  108. package/src/skills/shell.ts +118 -0
  109. package/src/skills/types.ts +30 -0
  110. package/src/types/api.ts +252 -0
  111. package/src/types/blessed-contrib.d.ts +4 -0
  112. package/src/types/context-budget.ts +76 -0
  113. package/src/types/doctor.ts +29 -0
  114. package/src/types/file-watcher.ts +26 -0
  115. package/src/types/incident.ts +57 -0
  116. package/src/types/local-state-backup.ts +121 -0
  117. package/src/types/mcp.ts +106 -0
  118. package/src/types/messaging.ts +35 -0
  119. package/src/types/model-routing.ts +61 -0
  120. package/src/types/mvp-gate.ts +99 -0
  121. package/src/types/orchestration.ts +65 -0
  122. package/src/types/persona-state.ts +61 -0
  123. package/src/types/policy.ts +27 -0
  124. package/src/types/reasoning-graph.ts +58 -0
  125. package/src/types/release.ts +115 -0
  126. package/src/types/reliability.ts +43 -0
  127. package/src/types/runtime-budget.ts +85 -0
  128. package/src/types/scheduler.ts +47 -0
  129. package/src/types/secret-vault.ts +62 -0
  130. package/src/types/skill-packages.ts +81 -0
  131. package/src/types/sqlite-vec.d.ts +5 -0
  132. package/src/types/websocket.ts +122 -0
@@ -0,0 +1,78 @@
1
+ import { getDmPairingService, isPairingChannel, } from '../services/dm-pairing.js';
2
+ function printPairingUsage() {
3
+ console.log(`Pairing commands:
4
+ pairing list <channel>
5
+ pairing approve <channel> <CODE>
6
+
7
+ Supported channels: telegram, whatsapp`);
8
+ }
9
+ function parseChannel(input) {
10
+ if (!input) {
11
+ throw new Error('Missing channel. Use: pairing <list|approve> <channel>.');
12
+ }
13
+ const normalized = input.trim().toLowerCase();
14
+ if (!isPairingChannel(normalized)) {
15
+ throw new Error(`Unsupported channel '${input}'. Supported channels: telegram, whatsapp.`);
16
+ }
17
+ return normalized;
18
+ }
19
+ function runPairingList(service, channel) {
20
+ const requests = service.listPending(channel);
21
+ if (requests.length === 0) {
22
+ console.log(`No pending pairing requests for channel '${channel}'.`);
23
+ return;
24
+ }
25
+ console.log(`Pending pairing requests for '${channel}' (${requests.length}):`);
26
+ for (const request of requests) {
27
+ console.log(` code=${request.code}\tsender=${request.senderId}\texpires=${request.expiresAt}`);
28
+ }
29
+ }
30
+ function runPairingApprove(service, channel, code) {
31
+ if (!code) {
32
+ throw new Error('Missing pairing code. Use: pairing approve <channel> <CODE>.');
33
+ }
34
+ const result = service.approve(channel, code);
35
+ switch (result.status) {
36
+ case 'approved':
37
+ console.log(`Approved sender '${result.senderId}' for channel '${channel}' using code '${code.toUpperCase()}'.`);
38
+ return;
39
+ case 'expired':
40
+ throw new Error(`Pairing code '${code.toUpperCase()}' is expired for channel '${channel}'.`);
41
+ case 'not_found':
42
+ throw new Error(`Pairing code '${code.toUpperCase()}' not found for channel '${channel}'.`);
43
+ }
44
+ }
45
+ /**
46
+ * Handle one-shot pairing commands.
47
+ * Returns true when invocation was recognized (handled or failed), false otherwise.
48
+ */
49
+ export function handlePairingCli(argv, service = getDmPairingService()) {
50
+ if (argv[0] !== 'pairing') {
51
+ return false;
52
+ }
53
+ const command = argv[1];
54
+ try {
55
+ switch (command) {
56
+ case 'list': {
57
+ const channel = parseChannel(argv[2]);
58
+ runPairingList(service, channel);
59
+ return true;
60
+ }
61
+ case 'approve': {
62
+ const channel = parseChannel(argv[2]);
63
+ runPairingApprove(service, channel, argv[3]);
64
+ return true;
65
+ }
66
+ default:
67
+ printPairingUsage();
68
+ process.exitCode = 1;
69
+ return true;
70
+ }
71
+ }
72
+ catch (error) {
73
+ const message = error instanceof Error ? error.message : String(error);
74
+ console.error(`Pairing command failed: ${message}`);
75
+ process.exitCode = 1;
76
+ return true;
77
+ }
78
+ }
@@ -0,0 +1,204 @@
1
+ import { getSecretVaultService } from '../services/secret-vault.js';
2
+ import { validateRuntimeConfig } from '../config/env-validator.js';
3
+ function readOption(args, flag) {
4
+ const index = args.indexOf(flag);
5
+ if (index === -1) {
6
+ return undefined;
7
+ }
8
+ const value = args[index + 1];
9
+ if (!value || value.startsWith('--')) {
10
+ throw new Error(`Missing value for ${flag}.`);
11
+ }
12
+ return value;
13
+ }
14
+ function hasFlag(args, flag) {
15
+ return args.includes(flag);
16
+ }
17
+ function parseOptionalNumber(args, flag) {
18
+ const value = readOption(args, flag);
19
+ if (value === undefined) {
20
+ return undefined;
21
+ }
22
+ const parsed = Number(value);
23
+ if (!Number.isFinite(parsed) || parsed <= 0) {
24
+ throw new Error(`${flag} must be a positive number.`);
25
+ }
26
+ return parsed;
27
+ }
28
+ function printUsage() {
29
+ console.log(`Secret vault commands:
30
+ secret list
31
+ secret doctor
32
+ secret set <NAME> <VALUE> [--scope <scope>] [--source <source>] [--required] [--rotation-hours <n>] [--warning-hours <n>] [--expires-at <ISO8601>]
33
+ secret rotate <NAME> <NEXT_VALUE> [--reason <text>] [--rotation-hours <n>] [--warning-hours <n>] [--expires-at <ISO8601>]
34
+ secret revoke <NAME> [--reason <text>]
35
+ config doctor`);
36
+ }
37
+ function runList(service) {
38
+ const secrets = service.listSecrets();
39
+ if (secrets.length === 0) {
40
+ console.log('No secrets registered.');
41
+ return;
42
+ }
43
+ for (const secret of secrets) {
44
+ console.log(`${secret.name}\tstatus=${secret.status}\tscope=${secret.scope}\tsource=${secret.source}\t` +
45
+ `required=${secret.required}\tversion=${secret.version}\t` +
46
+ `lastRotated=${secret.lastRotatedAt ?? 'n/a'}\texpires=${secret.expiresAt ?? 'none'}`);
47
+ }
48
+ }
49
+ function runDoctor(service) {
50
+ const diagnostics = service.getDiagnostics();
51
+ const status = diagnostics.health.hasIssues ? 'degraded' : 'ok';
52
+ console.log(`Secret vault status: ${status}`);
53
+ console.log(`Totals: total=${diagnostics.total}, active=${diagnostics.active}, expired=${diagnostics.expired}, revoked=${diagnostics.revoked}`);
54
+ if (diagnostics.health.missingRequired.length > 0) {
55
+ console.log(`Missing required: ${diagnostics.health.missingRequired.join(', ')}`);
56
+ }
57
+ if (diagnostics.health.expired.length > 0) {
58
+ console.log(`Expired: ${diagnostics.health.expired.join(', ')}`);
59
+ }
60
+ if (diagnostics.health.warnings.length > 0) {
61
+ console.log(`Warnings: ${diagnostics.health.warnings.join(' | ')}`);
62
+ }
63
+ if (diagnostics.dueForRotation.length > 0) {
64
+ console.log(`Due for rotation: ${diagnostics.dueForRotation.join(', ')}`);
65
+ }
66
+ }
67
+ function runSet(service, args) {
68
+ const name = args[0];
69
+ const value = args[1];
70
+ if (!name || !value) {
71
+ throw new Error('Usage: secret set <NAME> <VALUE> [...options]');
72
+ }
73
+ const scope = readOption(args, '--scope');
74
+ const source = readOption(args, '--source');
75
+ const metadata = service.setSecret({
76
+ name,
77
+ value,
78
+ scope,
79
+ source,
80
+ required: hasFlag(args, '--required'),
81
+ rotationWindowHours: parseOptionalNumber(args, '--rotation-hours'),
82
+ warningWindowHours: parseOptionalNumber(args, '--warning-hours'),
83
+ expiresAt: readOption(args, '--expires-at'),
84
+ });
85
+ console.log(`Secret '${metadata.name}' saved. version=${metadata.version}, status=${metadata.status}, expires=${metadata.expiresAt ?? 'none'}.`);
86
+ }
87
+ function runRotate(service, args) {
88
+ const name = args[0];
89
+ const nextValue = args[1];
90
+ if (!name || !nextValue) {
91
+ throw new Error('Usage: secret rotate <NAME> <NEXT_VALUE> [...options]');
92
+ }
93
+ const metadata = service.rotateSecret({
94
+ name,
95
+ nextValue,
96
+ reason: readOption(args, '--reason'),
97
+ rotationWindowHours: parseOptionalNumber(args, '--rotation-hours'),
98
+ warningWindowHours: parseOptionalNumber(args, '--warning-hours'),
99
+ expiresAt: readOption(args, '--expires-at'),
100
+ });
101
+ console.log(`Secret '${metadata.name}' rotated. version=${metadata.version}, status=${metadata.status}, expires=${metadata.expiresAt ?? 'none'}.`);
102
+ }
103
+ function runRevoke(service, args) {
104
+ const name = args[0];
105
+ if (!name) {
106
+ throw new Error('Usage: secret revoke <NAME> [--reason <text>]');
107
+ }
108
+ const metadata = service.revokeSecret({
109
+ name,
110
+ reason: readOption(args, '--reason'),
111
+ });
112
+ console.log(`Secret '${metadata.name}' revoked.`);
113
+ }
114
+ function runConfigDoctor() {
115
+ const result = validateRuntimeConfig();
116
+ const overallStatus = result.ok ? 'ok' : 'degraded';
117
+ console.log(`Config validation status: ${overallStatus}`);
118
+ console.log(`Validated at: ${result.validatedAt}`);
119
+ console.log(`Present keys (${result.presentKeys.length}): ${result.presentKeys.join(', ') || 'none'}`);
120
+ if (result.activeFeatures.length > 0) {
121
+ console.log(`Active features: ${result.activeFeatures.join(', ')}`);
122
+ }
123
+ if (result.issues.length === 0) {
124
+ console.log('No config issues detected.');
125
+ return;
126
+ }
127
+ if (result.fatalIssues.length > 0) {
128
+ console.log(`\nFatal issues (${result.fatalIssues.length}):`);
129
+ for (const issue of result.fatalIssues) {
130
+ console.log(` [${issue.key}] ${issue.message}`);
131
+ console.log(` Remediation: ${issue.remediation}`);
132
+ }
133
+ }
134
+ const nonFatal = result.issues.filter((i) => i.class !== 'missing_required');
135
+ if (nonFatal.length > 0) {
136
+ console.log(`\nWarnings (${nonFatal.length}):`);
137
+ for (const issue of nonFatal) {
138
+ console.log(` [${issue.class}] [${issue.key}] ${issue.message}`);
139
+ console.log(` Remediation: ${issue.remediation}`);
140
+ }
141
+ }
142
+ }
143
+ /**
144
+ * Handles one-shot secret and config CLI workflows.
145
+ * Returns true when invocation was recognized (handled or failed), false otherwise.
146
+ */
147
+ export function handleSecretVaultCli(argv, service = getSecretVaultService()) {
148
+ const topCommand = argv[0];
149
+ // ── config subcommands ─────────────────────────────────────────────────────
150
+ if (topCommand === 'config') {
151
+ const subcommand = argv[1];
152
+ try {
153
+ switch (subcommand) {
154
+ case 'doctor':
155
+ runConfigDoctor();
156
+ return true;
157
+ default:
158
+ console.log('Config commands:\n config doctor');
159
+ process.exitCode = 1;
160
+ return true;
161
+ }
162
+ }
163
+ catch (error) {
164
+ const message = error instanceof Error ? error.message : String(error);
165
+ console.error(`Config command failed: ${message}`);
166
+ process.exitCode = 1;
167
+ return true;
168
+ }
169
+ }
170
+ if (topCommand !== 'secret') {
171
+ return false;
172
+ }
173
+ const command = argv[1];
174
+ const commandArgs = argv.slice(2);
175
+ try {
176
+ switch (command) {
177
+ case 'list':
178
+ runList(service);
179
+ return true;
180
+ case 'doctor':
181
+ runDoctor(service);
182
+ return true;
183
+ case 'set':
184
+ runSet(service, commandArgs);
185
+ return true;
186
+ case 'rotate':
187
+ runRotate(service, commandArgs);
188
+ return true;
189
+ case 'revoke':
190
+ runRevoke(service, commandArgs);
191
+ return true;
192
+ default:
193
+ printUsage();
194
+ process.exitCode = 1;
195
+ return true;
196
+ }
197
+ }
198
+ catch (error) {
199
+ const message = error instanceof Error ? error.message : String(error);
200
+ console.error(`Secret vault command failed: ${message}`);
201
+ process.exitCode = 1;
202
+ return true;
203
+ }
204
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,404 @@
1
+ import { createRequire } from 'node:module';
2
+ const require = createRequire(import.meta.url);
3
+ import { handleOnboardCli, runOnboarding, runSetupWizard, startBasicREPL } from './core/onboarding.js';
4
+ import { Gateway } from './core/gateway.js';
5
+ import { handleDoctorCli, handleHelpCli, handleUnknownCommand } from './core/cli.js';
6
+ import { handleInstallCli } from './core/install-cli.js';
7
+ import { HeartbeatService } from './core/heartbeat.js';
8
+ import { Dispatcher } from './interfaces/dispatcher.js';
9
+ import { TelegramHandler } from './interfaces/telegram_handler.js';
10
+ import { WhatsAppHandler } from './interfaces/whatsapp_handler.js';
11
+ import { FileWatcherService } from './services/file-watcher.js';
12
+ import { ProactiveNotifier } from './services/proactive-notifier.js';
13
+ import { SkillRegistry } from './services/skill-registry.js';
14
+ import { McpServerManager } from './services/mcp-server-manager.js';
15
+ import { createBuiltinSkills } from './skills/builtin.js';
16
+ import { SttService } from './services/stt-service.js';
17
+ import { TtsService } from './services/tts-service.js';
18
+ import { QueueService } from './services/queue-service.js';
19
+ import { ModelRouter } from './services/model-router.js';
20
+ import { IncidentManager } from './services/incident-manager.js';
21
+ import { RuntimeBudgetGovernor } from './services/runtime-budget-governor.js';
22
+ import { LocalStateBackupService } from './services/local-state-backup.js';
23
+ import { logThought } from './utils/logger.js';
24
+ import { PolicyEngine } from './services/policy-engine.js';
25
+ import { savePolicyAuditLog } from './services/db.js';
26
+ import { getSecretVaultService } from './services/secret-vault.js';
27
+ import { getConfigValue, checkAndMigrateWorkspace } from './config/config-loader.js';
28
+ import { getIdentityDir, getWorkspaceSubdir } from './config/workspace.js';
29
+ import { handleSecretVaultCli } from './core/secret-vault-cli.js';
30
+ import { handlePairingCli } from './core/pairing-cli.js';
31
+ import { handleChannelsCli } from './core/channels-cli.js';
32
+ import { handleGatewayCli } from './core/gateway-cli.js';
33
+ import { handleLogsCli } from './core/logs-cli.js';
34
+ import { getDmPairingService } from './services/dm-pairing.js';
35
+ import { randomUUID } from 'node:crypto';
36
+ const secretVault = getSecretVaultService();
37
+ const pairingService = getDmPairingService();
38
+ function assertWindowsOnlyRuntime() {
39
+ if (process.platform === 'win32') {
40
+ return;
41
+ }
42
+ console.error(`[TwinBot] Windows-only runtime: detected unsupported platform '${process.platform}'.`);
43
+ process.exit(1);
44
+ }
45
+ assertWindowsOnlyRuntime();
46
+ // ── Early one-shot CLI commands (bypass service startup) ─────────────────────
47
+ if (handleHelpCli(process.argv.slice(2))) {
48
+ process.exit(process.exitCode ?? 0);
49
+ }
50
+ if (handleDoctorCli(process.argv.slice(2))) {
51
+ process.exit(process.exitCode ?? 0);
52
+ }
53
+ if (await handleInstallCli(process.argv.slice(2))) {
54
+ process.exit(process.exitCode ?? 0);
55
+ }
56
+ if (handleSecretVaultCli(process.argv.slice(2), secretVault)) {
57
+ process.exit(process.exitCode ?? 0);
58
+ }
59
+ if (handlePairingCli(process.argv.slice(2), pairingService)) {
60
+ process.exit(process.exitCode ?? 0);
61
+ }
62
+ const onboardCliHandled = await handleOnboardCli(process.argv.slice(2));
63
+ if (onboardCliHandled) {
64
+ process.exit(process.exitCode ?? 0);
65
+ }
66
+ const logsCliHandled = await handleLogsCli(process.argv.slice(2));
67
+ if (logsCliHandled) {
68
+ if (process.argv.includes('--follow') || process.argv.includes('-f')) {
69
+ await new Promise(() => { }); // Block event loop for watcher
70
+ }
71
+ else {
72
+ process.exit(process.exitCode ?? 0);
73
+ }
74
+ }
75
+ const gatewayCliHandled = await handleGatewayCli(process.argv.slice(2));
76
+ if (gatewayCliHandled) {
77
+ process.exit(process.exitCode ?? 0);
78
+ }
79
+ const channelsCliHandled = await handleChannelsCli(process.argv.slice(2));
80
+ if (channelsCliHandled) {
81
+ if (process.exitCode !== undefined) {
82
+ process.exit(process.exitCode);
83
+ }
84
+ // Hang the main thread so async tasks (like WhatsApp login) can complete
85
+ await new Promise(() => { });
86
+ }
87
+ if (handleUnknownCommand(process.argv.slice(2))) {
88
+ process.exit(process.exitCode ?? 1);
89
+ }
90
+ // Check for setup mode or missing critical configuration
91
+ const isSetupMode = process.argv[2] === 'setup';
92
+ const onboardFlag = process.argv.includes('--onboard');
93
+ if (onboardFlag) {
94
+ await runOnboarding();
95
+ process.exit(0);
96
+ }
97
+ checkAndMigrateWorkspace();
98
+ // ── Auto-Setup Trigger (If critical config missing) ──────────────────────────
99
+ async function tryAutoSetup() {
100
+ try {
101
+ secretVault.assertStartupPreflight(['API_SECRET']);
102
+ }
103
+ catch (error) {
104
+ console.log("\n[TwinBot] Welcome! Critical configuration missing.");
105
+ console.log("Starting the interactive setup wizard to configure your agent.\n");
106
+ const result = await runSetupWizard();
107
+ if (result.status !== 'success') {
108
+ const exitCode = result.status === 'cancelled' ? 130 : 1;
109
+ process.exit(exitCode);
110
+ }
111
+ console.log("\n[TwinBot] Setup complete. Initializing Gateway...\n");
112
+ }
113
+ }
114
+ if (!isSetupMode) {
115
+ await tryAutoSetup();
116
+ }
117
+ else {
118
+ const result = await runSetupWizard();
119
+ if (result.status === 'success') {
120
+ process.exit(0);
121
+ }
122
+ if (result.status === 'cancelled') {
123
+ process.exit(130);
124
+ }
125
+ process.exit(1);
126
+ }
127
+ console.log("TwinBot Gateway Initialized.");
128
+ // ── Heartbeat & Job Scheduler ────────────────────────────────────────────────
129
+ const heartbeat = new HeartbeatService(async (message) => {
130
+ console.log(`\n[TwinBot Heartbeat] ${message}\n`);
131
+ });
132
+ heartbeat.start();
133
+ void logThought('TwinBot process started and heartbeat initialized.');
134
+ // ── File Watcher ─────────────────────────────────────────────────────────────
135
+ const fileWatcher = new FileWatcherService();
136
+ fileWatcher.addTarget({
137
+ id: 'identity',
138
+ directory: getIdentityDir(),
139
+ exclude: [],
140
+ });
141
+ fileWatcher.addTarget({
142
+ id: 'memory-logs',
143
+ directory: getWorkspaceSubdir('memory'),
144
+ exclude: ['**/*.db', '**/*.db-journal', '**/*.sqlite', '**/*.sqlite-journal'],
145
+ });
146
+ // ── MCP Skill Registry & Server Manager ──────────────────────────────────────
147
+ const skillRegistry = new SkillRegistry();
148
+ skillRegistry.registerMany(createBuiltinSkills());
149
+ const mcpManager = new McpServerManager(skillRegistry);
150
+ void (async () => {
151
+ try {
152
+ await mcpManager.loadConfig();
153
+ await mcpManager.connectAll();
154
+ const summary = skillRegistry.summary();
155
+ const servers = mcpManager.listServers();
156
+ const connectedCount = servers.filter((s) => s.state === 'connected').length;
157
+ console.log(`[TwinBot MCP] ${connectedCount}/${servers.length} servers connected | ` +
158
+ `${summary.builtin ?? 0} builtin + ${summary.mcp ?? 0} MCP skills registered.`);
159
+ await logThought(`[MCP] Initialized: ${connectedCount} servers, ${summary.mcp ?? 0} MCP tools, ${summary.builtin ?? 0} builtins.`);
160
+ }
161
+ catch (err) {
162
+ console.error('[TwinBot MCP] Initialization failed:', err);
163
+ }
164
+ })();
165
+ // ── Gateway & Interface Dispatcher ────────────────────────────────────────────
166
+ const policyEngine = new PolicyEngine();
167
+ policyEngine.onDecision = (sessionId, decision) => {
168
+ savePolicyAuditLog(randomUUID(), sessionId, decision.skillName, decision.action, decision.reason, decision.profileId);
169
+ };
170
+ function parseToolSelectors(rawValue) {
171
+ if (!rawValue) {
172
+ return [];
173
+ }
174
+ return rawValue
175
+ .split(',')
176
+ .map((value) => value.trim())
177
+ .filter((value, index, all) => value.length > 0 && all.indexOf(value) === index);
178
+ }
179
+ const runtimeBudgetGovernor = new RuntimeBudgetGovernor();
180
+ const modelRouter = new ModelRouter({ budgetGovernor: runtimeBudgetGovernor });
181
+ const gateway = new Gateway(skillRegistry, {
182
+ policyEngine,
183
+ router: modelRouter,
184
+ toolPolicy: {
185
+ allow: parseToolSelectors(getConfigValue('TOOLS_ALLOW')),
186
+ deny: parseToolSelectors(getConfigValue('TOOLS_DENY')),
187
+ },
188
+ });
189
+ const telegramBotToken = secretVault.readSecret('TELEGRAM_BOT_TOKEN');
190
+ const telegramUserId = getConfigValue('TELEGRAM_USER_ID')?.trim();
191
+ const whatsappPhoneNumber = secretVault.readSecret('WHATSAPP_PHONE_NUMBER') ?? getConfigValue('WHATSAPP_PHONE_NUMBER');
192
+ const groqApiKey = secretVault.readSecret('GROQ_API_KEY');
193
+ let dispatcher = null;
194
+ if (telegramBotToken || whatsappPhoneNumber) {
195
+ if (groqApiKey) {
196
+ let telegramHandler;
197
+ let whatsappHandler;
198
+ const telegramAllowFrom = [];
199
+ const whatsappAllowFrom = [];
200
+ if (telegramBotToken) {
201
+ telegramHandler = new TelegramHandler(telegramBotToken);
202
+ if (telegramUserId) {
203
+ const parsedTelegramUserId = Number(telegramUserId);
204
+ if (!Number.isInteger(parsedTelegramUserId) || parsedTelegramUserId <= 0) {
205
+ console.error('[TwinBot] TELEGRAM_USER_ID must be a positive integer when provided.');
206
+ }
207
+ else {
208
+ telegramAllowFrom.push(String(parsedTelegramUserId));
209
+ }
210
+ }
211
+ }
212
+ if (whatsappPhoneNumber) {
213
+ whatsappHandler = new WhatsAppHandler();
214
+ whatsappAllowFrom.push(whatsappPhoneNumber);
215
+ }
216
+ const sttService = new SttService(groqApiKey);
217
+ const ttsService = new TtsService(groqApiKey);
218
+ // Initialize persistent delivery queue
219
+ const queueService = new QueueService(async (platform, chatId, text) => {
220
+ switch (platform) {
221
+ case 'telegram':
222
+ if (!telegramHandler)
223
+ throw new Error('Telegram handler not configured');
224
+ await telegramHandler.sendText(Number(chatId), text);
225
+ break;
226
+ case 'whatsapp':
227
+ if (!whatsappHandler)
228
+ throw new Error('WhatsApp handler not configured');
229
+ await whatsappHandler.sendText(String(chatId), text);
230
+ break;
231
+ default:
232
+ throw new Error(`Unsupported platform in queue: ${platform}`);
233
+ }
234
+ }, heartbeat.scheduler);
235
+ queueService.start();
236
+ dispatcher = new Dispatcher(telegramHandler, whatsappHandler, sttService, ttsService, gateway, queueService, {
237
+ pairingService,
238
+ telegram: {
239
+ dmPolicy: 'pairing',
240
+ allowFrom: telegramAllowFrom,
241
+ },
242
+ whatsapp: {
243
+ dmPolicy: 'pairing',
244
+ allowFrom: whatsappAllowFrom,
245
+ },
246
+ });
247
+ void logThought('[TwinBot] Messaging dispatcher and persistent queue initialized.');
248
+ }
249
+ else {
250
+ console.warn('[TwinBot] Dispatcher requires GROQ_API_KEY.');
251
+ }
252
+ }
253
+ else {
254
+ console.log('[TwinBot] Messaging dispatcher not initialized (missing Telegram AND WhatsApp configs).');
255
+ }
256
+ const incidentManager = new IncidentManager({
257
+ gateway,
258
+ router: modelRouter,
259
+ queue: dispatcher?.queue,
260
+ scheduler: heartbeat.scheduler,
261
+ });
262
+ incidentManager.start();
263
+ const localStateBackup = new LocalStateBackupService({
264
+ scheduler: heartbeat.scheduler,
265
+ });
266
+ localStateBackup.start();
267
+ void logThought('[TwinBot] Incident self-healing manager initialized.');
268
+ void logThought('[TwinBot] Local-state backup automation initialized.');
269
+ void logThought('[TwinBot] Runtime budget governor initialized.');
270
+ // ── Proactive Notifier ─────────────────────────────────────────────────────
271
+ const proactiveEnabled = !!telegramUserId;
272
+ const notifier = new ProactiveNotifier(async (target, text) => {
273
+ if (dispatcher) {
274
+ await dispatcher.sendProactive(target.platform, target.chatId, text);
275
+ await logThought(`[Proactive] Delivered message to ${target.platform}:${target.chatId}`);
276
+ return;
277
+ }
278
+ console.log(`[Proactive → ${target.platform}:${target.chatId}] ${text}`);
279
+ await logThought(`[Proactive] Dispatcher unavailable; message logged for ${target.platform}:${target.chatId}`);
280
+ }, {
281
+ platform: 'telegram',
282
+ chatId: telegramUserId ?? 'unknown',
283
+ }, proactiveEnabled);
284
+ // Wire scheduler events to the notifier
285
+ heartbeat.scheduler.on('job:error', (event) => {
286
+ void notifier.onSchedulerEvent(event);
287
+ });
288
+ heartbeat.scheduler.on('job:done', (event) => {
289
+ void notifier.onSchedulerEvent(event);
290
+ });
291
+ // Wire file watcher events to the notifier
292
+ fileWatcher.onEvent((event) => {
293
+ void notifier.onFileEvent(event);
294
+ });
295
+ // Start file watchers
296
+ void fileWatcher.startAll().catch((err) => {
297
+ console.error('[TwinBot] Failed to start file watchers:', err);
298
+ });
299
+ // ── Control Plane HTTP API ───────────────────────────────────────────────────
300
+ import { startApiServer } from './api/router.js';
301
+ import { WsHub } from './api/websocket-hub.js';
302
+ import { RuntimeEventProducer } from './api/runtime-event-producer.js';
303
+ const wsHub = new WsHub();
304
+ const runtimeEventProducer = new RuntimeEventProducer({
305
+ hub: wsHub,
306
+ incidentManager,
307
+ budgetGovernor: runtimeBudgetGovernor,
308
+ dispatcher: dispatcher ?? undefined,
309
+ modelRouter,
310
+ });
311
+ const apiPort = Number(getConfigValue('API_PORT')) || 18789;
312
+ startApiServer({
313
+ heartbeat,
314
+ skillRegistry,
315
+ mcpManager,
316
+ gateway,
317
+ dispatcher: dispatcher ?? undefined,
318
+ incidentManager,
319
+ budgetGovernor: runtimeBudgetGovernor,
320
+ localStateBackup,
321
+ modelRouter,
322
+ wsHub,
323
+ });
324
+ runtimeEventProducer.start();
325
+ async function waitForStartupHealthProbe(port, timeoutMs = 30_000, intervalMs = 500) {
326
+ const healthUrl = `http://localhost:${port}/health`;
327
+ const startTime = Date.now();
328
+ while (Date.now() - startTime < timeoutMs) {
329
+ try {
330
+ const response = await fetch(healthUrl, {
331
+ method: 'GET',
332
+ headers: { accept: 'application/json' },
333
+ signal: AbortSignal.timeout(2_000),
334
+ });
335
+ if (response.ok) {
336
+ const body = await response.json();
337
+ if (body.data?.status === 'ok' || body.data?.status === 'degraded') {
338
+ return true;
339
+ }
340
+ }
341
+ }
342
+ catch {
343
+ // Server not ready yet, continue polling
344
+ }
345
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
346
+ }
347
+ return false;
348
+ }
349
+ void (async () => {
350
+ const healthOk = await waitForStartupHealthProbe(apiPort);
351
+ if (healthOk) {
352
+ console.log('[TwinBot] Startup health probe passed. Gateway is ready.');
353
+ void logThought('[TwinBot] Startup health probe passed.');
354
+ }
355
+ else {
356
+ console.error('[TwinBot] Startup health probe failed within timeout. Gateway may not be healthy.');
357
+ void logThought('[TwinBot] Startup health probe FAILED.');
358
+ }
359
+ })();
360
+ // ── Signal Handlers ──────────────────────────────────────────────────────────
361
+ process.on('SIGINT', () => {
362
+ heartbeat.stop();
363
+ incidentManager.stop();
364
+ localStateBackup.stop();
365
+ runtimeEventProducer.stop();
366
+ wsHub.stop();
367
+ if (dispatcher) {
368
+ dispatcher.queue.stop();
369
+ dispatcher.shutdown();
370
+ }
371
+ void fileWatcher.stopAll();
372
+ void mcpManager.disconnectAll();
373
+ void logThought('TwinBot process received SIGINT; services stopped.');
374
+ process.exit(0);
375
+ });
376
+ process.on('SIGTERM', () => {
377
+ heartbeat.stop();
378
+ incidentManager.stop();
379
+ localStateBackup.stop();
380
+ runtimeEventProducer.stop();
381
+ wsHub.stop();
382
+ if (dispatcher) {
383
+ dispatcher.queue.stop();
384
+ dispatcher.shutdown();
385
+ }
386
+ void fileWatcher.stopAll();
387
+ void mcpManager.disconnectAll();
388
+ void logThought('TwinBot process received SIGTERM; services stopped.');
389
+ process.exit(0);
390
+ });
391
+ // ── Entry Point ──────────────────────────────────────────────────────────────
392
+ if (isSetupMode) {
393
+ runSetupWizard().catch((err) => {
394
+ const message = err instanceof Error ? err.message : String(err);
395
+ console.error(`[TwinBot] Setup wizard failed: ${message}`);
396
+ process.exit(1);
397
+ });
398
+ }
399
+ else if (process.argv.includes('--onboard')) {
400
+ runOnboarding().catch(console.error);
401
+ }
402
+ else {
403
+ startBasicREPL(gateway);
404
+ }