mach6-core 1.1.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 (245) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +454 -0
  3. package/dist/agent/context-monitor.d.ts +40 -0
  4. package/dist/agent/context-monitor.d.ts.map +1 -0
  5. package/dist/agent/context-monitor.js +138 -0
  6. package/dist/agent/context-monitor.js.map +1 -0
  7. package/dist/agent/context.d.ts +8 -0
  8. package/dist/agent/context.d.ts.map +1 -0
  9. package/dist/agent/context.js +197 -0
  10. package/dist/agent/context.js.map +1 -0
  11. package/dist/agent/runner.d.ts +42 -0
  12. package/dist/agent/runner.d.ts.map +1 -0
  13. package/dist/agent/runner.js +168 -0
  14. package/dist/agent/runner.js.map +1 -0
  15. package/dist/agent/system-prompt.d.ts +10 -0
  16. package/dist/agent/system-prompt.d.ts.map +1 -0
  17. package/dist/agent/system-prompt.js +120 -0
  18. package/dist/agent/system-prompt.js.map +1 -0
  19. package/dist/boot/sequence.d.ts +34 -0
  20. package/dist/boot/sequence.d.ts.map +1 -0
  21. package/dist/boot/sequence.js +103 -0
  22. package/dist/boot/sequence.js.map +1 -0
  23. package/dist/channels/__test__/integration.test.d.ts +8 -0
  24. package/dist/channels/__test__/integration.test.d.ts.map +1 -0
  25. package/dist/channels/__test__/integration.test.js +221 -0
  26. package/dist/channels/__test__/integration.test.js.map +1 -0
  27. package/dist/channels/adapter.d.ts +53 -0
  28. package/dist/channels/adapter.d.ts.map +1 -0
  29. package/dist/channels/adapter.js +164 -0
  30. package/dist/channels/adapter.js.map +1 -0
  31. package/dist/channels/adapters/discord.d.ts +49 -0
  32. package/dist/channels/adapters/discord.d.ts.map +1 -0
  33. package/dist/channels/adapters/discord.js +382 -0
  34. package/dist/channels/adapters/discord.js.map +1 -0
  35. package/dist/channels/adapters/whatsapp.d.ts +59 -0
  36. package/dist/channels/adapters/whatsapp.d.ts.map +1 -0
  37. package/dist/channels/adapters/whatsapp.js +503 -0
  38. package/dist/channels/adapters/whatsapp.js.map +1 -0
  39. package/dist/channels/bus.d.ts +55 -0
  40. package/dist/channels/bus.d.ts.map +1 -0
  41. package/dist/channels/bus.js +307 -0
  42. package/dist/channels/bus.js.map +1 -0
  43. package/dist/channels/formatter.d.ts +16 -0
  44. package/dist/channels/formatter.d.ts.map +1 -0
  45. package/dist/channels/formatter.js +226 -0
  46. package/dist/channels/formatter.js.map +1 -0
  47. package/dist/channels/presence.d.ts +67 -0
  48. package/dist/channels/presence.d.ts.map +1 -0
  49. package/dist/channels/presence.js +209 -0
  50. package/dist/channels/presence.js.map +1 -0
  51. package/dist/channels/registry.d.ts +57 -0
  52. package/dist/channels/registry.d.ts.map +1 -0
  53. package/dist/channels/registry.js +159 -0
  54. package/dist/channels/registry.js.map +1 -0
  55. package/dist/channels/router.d.ts +49 -0
  56. package/dist/channels/router.d.ts.map +1 -0
  57. package/dist/channels/router.js +244 -0
  58. package/dist/channels/router.js.map +1 -0
  59. package/dist/channels/types.d.ts +279 -0
  60. package/dist/channels/types.d.ts.map +1 -0
  61. package/dist/channels/types.js +67 -0
  62. package/dist/channels/types.js.map +1 -0
  63. package/dist/cli/brand.d.ts +71 -0
  64. package/dist/cli/brand.d.ts.map +1 -0
  65. package/dist/cli/brand.js +194 -0
  66. package/dist/cli/brand.js.map +1 -0
  67. package/dist/cli/wizard.d.ts +8 -0
  68. package/dist/cli/wizard.d.ts.map +1 -0
  69. package/dist/cli/wizard.js +520 -0
  70. package/dist/cli/wizard.js.map +1 -0
  71. package/dist/config/config.d.ts +47 -0
  72. package/dist/config/config.d.ts.map +1 -0
  73. package/dist/config/config.js +63 -0
  74. package/dist/config/config.js.map +1 -0
  75. package/dist/config/validator.d.ts +18 -0
  76. package/dist/config/validator.d.ts.map +1 -0
  77. package/dist/config/validator.js +92 -0
  78. package/dist/config/validator.js.map +1 -0
  79. package/dist/cron/budget.d.ts +39 -0
  80. package/dist/cron/budget.d.ts.map +1 -0
  81. package/dist/cron/budget.js +98 -0
  82. package/dist/cron/budget.js.map +1 -0
  83. package/dist/formatters/markdown.d.ts +6 -0
  84. package/dist/formatters/markdown.d.ts.map +1 -0
  85. package/dist/formatters/markdown.js +85 -0
  86. package/dist/formatters/markdown.js.map +1 -0
  87. package/dist/gateway/daemon.d.ts +97 -0
  88. package/dist/gateway/daemon.d.ts.map +1 -0
  89. package/dist/gateway/daemon.js +772 -0
  90. package/dist/gateway/daemon.js.map +1 -0
  91. package/dist/heartbeat/scheduler.d.ts +45 -0
  92. package/dist/heartbeat/scheduler.d.ts.map +1 -0
  93. package/dist/heartbeat/scheduler.js +102 -0
  94. package/dist/heartbeat/scheduler.js.map +1 -0
  95. package/dist/index.d.ts +3 -0
  96. package/dist/index.d.ts.map +1 -0
  97. package/dist/index.js +305 -0
  98. package/dist/index.js.map +1 -0
  99. package/dist/memory/integrity.d.ts +37 -0
  100. package/dist/memory/integrity.d.ts.map +1 -0
  101. package/dist/memory/integrity.js +108 -0
  102. package/dist/memory/integrity.js.map +1 -0
  103. package/dist/providers/anthropic.d.ts +3 -0
  104. package/dist/providers/anthropic.d.ts.map +1 -0
  105. package/dist/providers/anthropic.js +232 -0
  106. package/dist/providers/anthropic.js.map +1 -0
  107. package/dist/providers/diagnostics.d.ts +18 -0
  108. package/dist/providers/diagnostics.d.ts.map +1 -0
  109. package/dist/providers/diagnostics.js +87 -0
  110. package/dist/providers/diagnostics.js.map +1 -0
  111. package/dist/providers/github-copilot.d.ts +3 -0
  112. package/dist/providers/github-copilot.d.ts.map +1 -0
  113. package/dist/providers/github-copilot.js +145 -0
  114. package/dist/providers/github-copilot.js.map +1 -0
  115. package/dist/providers/gladius.d.ts +3 -0
  116. package/dist/providers/gladius.d.ts.map +1 -0
  117. package/dist/providers/gladius.js +16 -0
  118. package/dist/providers/gladius.js.map +1 -0
  119. package/dist/providers/openai.d.ts +3 -0
  120. package/dist/providers/openai.d.ts.map +1 -0
  121. package/dist/providers/openai.js +161 -0
  122. package/dist/providers/openai.js.map +1 -0
  123. package/dist/providers/retry.d.ts +2 -0
  124. package/dist/providers/retry.d.ts.map +1 -0
  125. package/dist/providers/retry.js +50 -0
  126. package/dist/providers/retry.js.map +1 -0
  127. package/dist/providers/types.d.ts +78 -0
  128. package/dist/providers/types.d.ts.map +1 -0
  129. package/dist/providers/types.js +3 -0
  130. package/dist/providers/types.js.map +1 -0
  131. package/dist/security/sanitizer.d.ts +26 -0
  132. package/dist/security/sanitizer.d.ts.map +1 -0
  133. package/dist/security/sanitizer.js +115 -0
  134. package/dist/security/sanitizer.js.map +1 -0
  135. package/dist/sessions/manager.d.ts +40 -0
  136. package/dist/sessions/manager.d.ts.map +1 -0
  137. package/dist/sessions/manager.js +255 -0
  138. package/dist/sessions/manager.js.map +1 -0
  139. package/dist/sessions/queue.d.ts +41 -0
  140. package/dist/sessions/queue.d.ts.map +1 -0
  141. package/dist/sessions/queue.js +93 -0
  142. package/dist/sessions/queue.js.map +1 -0
  143. package/dist/sessions/store.d.ts +12 -0
  144. package/dist/sessions/store.d.ts.map +1 -0
  145. package/dist/sessions/store.js +74 -0
  146. package/dist/sessions/store.js.map +1 -0
  147. package/dist/sessions/sub-agent.d.ts +18 -0
  148. package/dist/sessions/sub-agent.d.ts.map +1 -0
  149. package/dist/sessions/sub-agent.js +117 -0
  150. package/dist/sessions/sub-agent.js.map +1 -0
  151. package/dist/sessions/types.d.ts +48 -0
  152. package/dist/sessions/types.d.ts.map +1 -0
  153. package/dist/sessions/types.js +3 -0
  154. package/dist/sessions/types.js.map +1 -0
  155. package/dist/test/channel-integration.d.ts +10 -0
  156. package/dist/test/channel-integration.d.ts.map +1 -0
  157. package/dist/test/channel-integration.js +226 -0
  158. package/dist/test/channel-integration.js.map +1 -0
  159. package/dist/test/prompt-test.d.ts +2 -0
  160. package/dist/test/prompt-test.d.ts.map +1 -0
  161. package/dist/test/prompt-test.js +33 -0
  162. package/dist/test/prompt-test.js.map +1 -0
  163. package/dist/test/smoke.d.ts +8 -0
  164. package/dist/test/smoke.d.ts.map +1 -0
  165. package/dist/test/smoke.js +134 -0
  166. package/dist/test/smoke.js.map +1 -0
  167. package/dist/tools/builtin/comb.d.ts +4 -0
  168. package/dist/tools/builtin/comb.d.ts.map +1 -0
  169. package/dist/tools/builtin/comb.js +50 -0
  170. package/dist/tools/builtin/comb.js.map +1 -0
  171. package/dist/tools/builtin/edit.d.ts +3 -0
  172. package/dist/tools/builtin/edit.d.ts.map +1 -0
  173. package/dist/tools/builtin/edit.js +42 -0
  174. package/dist/tools/builtin/edit.js.map +1 -0
  175. package/dist/tools/builtin/exec.d.ts +3 -0
  176. package/dist/tools/builtin/exec.d.ts.map +1 -0
  177. package/dist/tools/builtin/exec.js +75 -0
  178. package/dist/tools/builtin/exec.js.map +1 -0
  179. package/dist/tools/builtin/image.d.ts +3 -0
  180. package/dist/tools/builtin/image.d.ts.map +1 -0
  181. package/dist/tools/builtin/image.js +208 -0
  182. package/dist/tools/builtin/image.js.map +1 -0
  183. package/dist/tools/builtin/memory.d.ts +3 -0
  184. package/dist/tools/builtin/memory.d.ts.map +1 -0
  185. package/dist/tools/builtin/memory.js +36 -0
  186. package/dist/tools/builtin/memory.js.map +1 -0
  187. package/dist/tools/builtin/message.d.ts +13 -0
  188. package/dist/tools/builtin/message.d.ts.map +1 -0
  189. package/dist/tools/builtin/message.js +330 -0
  190. package/dist/tools/builtin/message.js.map +1 -0
  191. package/dist/tools/builtin/process.d.ts +43 -0
  192. package/dist/tools/builtin/process.d.ts.map +1 -0
  193. package/dist/tools/builtin/process.js +178 -0
  194. package/dist/tools/builtin/process.js.map +1 -0
  195. package/dist/tools/builtin/read.d.ts +3 -0
  196. package/dist/tools/builtin/read.d.ts.map +1 -0
  197. package/dist/tools/builtin/read.js +36 -0
  198. package/dist/tools/builtin/read.js.map +1 -0
  199. package/dist/tools/builtin/spawn.d.ts +8 -0
  200. package/dist/tools/builtin/spawn.d.ts.map +1 -0
  201. package/dist/tools/builtin/spawn.js +90 -0
  202. package/dist/tools/builtin/spawn.js.map +1 -0
  203. package/dist/tools/builtin/tts.d.ts +3 -0
  204. package/dist/tools/builtin/tts.d.ts.map +1 -0
  205. package/dist/tools/builtin/tts.js +77 -0
  206. package/dist/tools/builtin/tts.js.map +1 -0
  207. package/dist/tools/builtin/web-fetch.d.ts +3 -0
  208. package/dist/tools/builtin/web-fetch.d.ts.map +1 -0
  209. package/dist/tools/builtin/web-fetch.js +46 -0
  210. package/dist/tools/builtin/web-fetch.js.map +1 -0
  211. package/dist/tools/builtin/write.d.ts +3 -0
  212. package/dist/tools/builtin/write.d.ts.map +1 -0
  213. package/dist/tools/builtin/write.js +31 -0
  214. package/dist/tools/builtin/write.js.map +1 -0
  215. package/dist/tools/mcp-bridge.d.ts +42 -0
  216. package/dist/tools/mcp-bridge.d.ts.map +1 -0
  217. package/dist/tools/mcp-bridge.js +198 -0
  218. package/dist/tools/mcp-bridge.js.map +1 -0
  219. package/dist/tools/policy.d.ts +57 -0
  220. package/dist/tools/policy.d.ts.map +1 -0
  221. package/dist/tools/policy.js +106 -0
  222. package/dist/tools/policy.js.map +1 -0
  223. package/dist/tools/registry.d.ts +15 -0
  224. package/dist/tools/registry.d.ts.map +1 -0
  225. package/dist/tools/registry.js +41 -0
  226. package/dist/tools/registry.js.map +1 -0
  227. package/dist/tools/sandbox.d.ts +91 -0
  228. package/dist/tools/sandbox.d.ts.map +1 -0
  229. package/dist/tools/sandbox.js +279 -0
  230. package/dist/tools/sandbox.js.map +1 -0
  231. package/dist/tools/types.d.ts +23 -0
  232. package/dist/tools/types.d.ts.map +1 -0
  233. package/dist/tools/types.js +3 -0
  234. package/dist/tools/types.js.map +1 -0
  235. package/dist/web/http-api.d.ts +61 -0
  236. package/dist/web/http-api.d.ts.map +1 -0
  237. package/dist/web/http-api.js +200 -0
  238. package/dist/web/http-api.js.map +1 -0
  239. package/dist/web/server.d.ts +7 -0
  240. package/dist/web/server.d.ts.map +1 -0
  241. package/dist/web/server.js +396 -0
  242. package/dist/web/server.js.map +1 -0
  243. package/mach6.example.json +50 -0
  244. package/package.json +58 -0
  245. package/web/index.html +1370 -0
@@ -0,0 +1,772 @@
1
+ /**
2
+ * Mach6 — Gateway Daemon
3
+ *
4
+ * The persistent process. Manages channel lifecycle, agent sessions,
5
+ * signal handling, graceful shutdown, and hot-reload.
6
+ *
7
+ * Mach6 AI Gateway — sovereign engine.
8
+ */
9
+ import * as os from 'node:os';
10
+ import * as path from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+ // Use global process (don't import — it shadows signal handlers)
13
+ import { palette, gradient, versionBanner, kvLine, ok, warn, info, divider, } from '../cli/brand.js';
14
+ import { ChannelRegistry } from '../channels/registry.js';
15
+ import { DiscordAdapter } from '../channels/adapters/discord.js';
16
+ import { WhatsAppAdapter } from '../channels/adapters/whatsapp.js';
17
+ import { ToolRegistry } from '../tools/registry.js';
18
+ import { presenceManager } from '../channels/presence.js';
19
+ import { HeartbeatScheduler } from '../heartbeat/scheduler.js';
20
+ import { readTool } from '../tools/builtin/read.js';
21
+ import { writeTool } from '../tools/builtin/write.js';
22
+ import { execTool } from '../tools/builtin/exec.js';
23
+ import { editTool } from '../tools/builtin/edit.js';
24
+ import { imageTool } from '../tools/builtin/image.js';
25
+ import { processStartTool, processPollTool, processKillTool, processListTool, } from '../tools/builtin/process.js';
26
+ import { ttsTool } from '../tools/builtin/tts.js';
27
+ import { webFetchTool } from '../tools/builtin/web-fetch.js';
28
+ import { memorySearchTool } from '../tools/builtin/memory.js';
29
+ import { combRecallTool, combStageTool } from '../tools/builtin/comb.js';
30
+ import { createSpawnTool, createSubAgentStatusTool } from '../tools/builtin/spawn.js';
31
+ import { SubAgentManager } from '../sessions/sub-agent.js';
32
+ import { createMessageTool, createTypingTool, createPresenceTool, createDeleteMessageTool, createMarkReadTool } from '../tools/builtin/message.js';
33
+ import { SessionManager } from '../sessions/manager.js';
34
+ import { buildSystemPrompt } from '../agent/system-prompt.js';
35
+ import { runAgent } from '../agent/runner.js';
36
+ import { loadConfig } from '../config/config.js';
37
+ import { anthropicProvider } from '../providers/anthropic.js';
38
+ import { openaiProvider } from '../providers/openai.js';
39
+ import { githubCopilotProvider } from '../providers/github-copilot.js';
40
+ import { gladiusProvider } from '../providers/gladius.js';
41
+ import { createSandboxedRegistry } from '../tools/sandbox.js';
42
+ import { HttpApiServer } from '../web/http-api.js';
43
+ import { McpBridge } from '../tools/mcp-bridge.js';
44
+ // ─── Provider Registry ─────────────────────────────────────────────────────
45
+ const PROVIDERS = new Map([
46
+ ['anthropic', anthropicProvider],
47
+ ['openai', openaiProvider],
48
+ ['github-copilot', githubCopilotProvider],
49
+ ['gladius', gladiusProvider],
50
+ ]);
51
+ // ─── Gateway ───────────────────────────────────────────────────────────────
52
+ export class Mach6Gateway {
53
+ config;
54
+ gatewayConfig;
55
+ channelRegistry;
56
+ toolRegistry;
57
+ sessionManager;
58
+ activeTurns = new Map();
59
+ provider;
60
+ providerName;
61
+ model;
62
+ systemPrompt;
63
+ shutdownRequested = false;
64
+ heartbeat;
65
+ subAgentManager;
66
+ startTime = Date.now();
67
+ httpApi = null;
68
+ mcpBridges = [];
69
+ constructor(gatewayConfig) {
70
+ this.gatewayConfig = gatewayConfig;
71
+ this.config = loadConfig(gatewayConfig.configPath);
72
+ // Set cwd to workspace so tools resolve relative paths correctly
73
+ if (this.config.workspace) {
74
+ process.chdir(this.config.workspace);
75
+ process.env.MACH6_WORKSPACE = this.config.workspace;
76
+ console.log(`${palette.dim} [gateway]${palette.reset} Working directory: ${palette.cyan}${this.config.workspace}${palette.reset}`);
77
+ }
78
+ // Provider
79
+ this.providerName = this.config.defaultProvider;
80
+ this.provider = PROVIDERS.get(this.providerName);
81
+ if (!this.provider) {
82
+ throw new Error(`Unknown provider: ${this.providerName}`);
83
+ }
84
+ this.model = this.config.defaultModel;
85
+ // Tools
86
+ this.toolRegistry = new ToolRegistry();
87
+ for (const tool of [
88
+ readTool, writeTool, editTool, execTool, imageTool,
89
+ processStartTool, processPollTool, processKillTool, processListTool,
90
+ ttsTool, webFetchTool, memorySearchTool, combRecallTool, combStageTool,
91
+ ]) {
92
+ this.toolRegistry.register(tool);
93
+ }
94
+ // Heartbeat scheduler
95
+ const hbConfig = this.gatewayConfig.heartbeat ?? {};
96
+ this.heartbeat = new HeartbeatScheduler({
97
+ activeIntervalMin: hbConfig.activeIntervalMin ?? 30,
98
+ idleIntervalMin: hbConfig.idleIntervalMin ?? 120,
99
+ sleepingIntervalMin: hbConfig.sleepingIntervalMin ?? 360,
100
+ quietHoursStart: hbConfig.quietHoursStart ?? 23,
101
+ quietHoursEnd: hbConfig.quietHoursEnd ?? 8,
102
+ });
103
+ // Sessions
104
+ this.sessionManager = new SessionManager(this.config.sessionsDir);
105
+ // System prompt (base — rebuilt per-message with channel context)
106
+ this.systemPrompt = buildSystemPrompt({
107
+ workspace: this.config.workspace,
108
+ tools: this.toolRegistry.list().map(t => t.name),
109
+ });
110
+ console.log(`${palette.dim} [gateway]${palette.reset} System prompt: ${palette.silver}${this.systemPrompt.length} chars${palette.reset}`);
111
+ // Channel registry
112
+ this.channelRegistry = new ChannelRegistry({
113
+ globalOwnerIds: gatewayConfig.ownerIds,
114
+ onAdapterHealthChange: (id, health) => {
115
+ console.log(`${palette.dim} [gateway]${palette.reset} Adapter ${palette.cyan}${id}${palette.reset}: ${palette.silver}${health.state}${palette.reset}${health.lastError ? ` ${palette.dim}(${health.lastError})${palette.reset}` : ''}`);
116
+ },
117
+ });
118
+ // Register message tool (needs channelRegistry — must come after registry creation)
119
+ this.toolRegistry.register(createMessageTool(this.channelRegistry));
120
+ this.toolRegistry.register(createTypingTool(this.channelRegistry));
121
+ this.toolRegistry.register(createPresenceTool(this.channelRegistry));
122
+ this.toolRegistry.register(createDeleteMessageTool(this.channelRegistry));
123
+ this.toolRegistry.register(createMarkReadTool(this.channelRegistry));
124
+ // Sub-agent manager + spawn tools
125
+ this.subAgentManager = new SubAgentManager(this.sessionManager);
126
+ const provCfg = this.config.providers[this.providerName] ?? {};
127
+ const spawnProvConfig = {
128
+ model: this.model,
129
+ maxTokens: this.config.maxTokens,
130
+ temperature: this.config.temperature,
131
+ ...provCfg,
132
+ };
133
+ this.toolRegistry.register(createSpawnTool(this.subAgentManager, this.provider, spawnProvConfig, this.toolRegistry, this.config.workspace));
134
+ this.toolRegistry.register(createSubAgentStatusTool(this.subAgentManager));
135
+ // Rebuild system prompt now that all tools are registered
136
+ this.systemPrompt = buildSystemPrompt({
137
+ workspace: this.config.workspace,
138
+ tools: this.toolRegistry.list().map(t => t.name),
139
+ });
140
+ }
141
+ // ── Start ──────────────────────────────────────────────────────────────
142
+ async start() {
143
+ console.log(versionBanner('1.0.0'));
144
+ const gatewayTitle = gradient('GATEWAY', [138, 43, 226], [0, 229, 255]);
145
+ console.log(` ${palette.bold}${gatewayTitle}${palette.reset}`);
146
+ console.log();
147
+ console.log(kvLine('Provider', `${palette.cyan}${this.providerName}${palette.reset}${palette.dim}/${palette.reset}${palette.white}${this.model}${palette.reset}`));
148
+ console.log(kvLine('Tools', `${palette.gold}${this.toolRegistry.list().length}${palette.reset} ${palette.dim}registered${palette.reset}`));
149
+ console.log(kvLine('Workspace', `${palette.cyan}${this.config.workspace}${palette.reset}`));
150
+ console.log(kvLine('PID', `${palette.dim}${process.pid}${palette.reset}`));
151
+ console.log();
152
+ console.log(divider());
153
+ // Connect MCP servers (external tool sources)
154
+ await this.connectMcpServers();
155
+ // Register signal handlers
156
+ this.setupSignals();
157
+ // Subscribe to bus messages
158
+ const bus = this.channelRegistry.getBus();
159
+ this.startMessageLoop();
160
+ // Start channels
161
+ await this.startChannels();
162
+ // Start HTTP API server
163
+ await this.startHttpApi();
164
+ const elapsed = Date.now() - this.startTime;
165
+ console.log();
166
+ console.log(divider());
167
+ const readyMsg = gradient('GATEWAY READY', [0, 230, 118], [0, 188, 212]);
168
+ console.log(` ${palette.bold}${palette.green}⚡${palette.reset} ${palette.bold}${readyMsg}${palette.reset} ${palette.dim}— ${elapsed}ms${palette.reset}`);
169
+ console.log();
170
+ }
171
+ // ── MCP Servers ────────────────────────────────────────────────────────
172
+ async connectMcpServers() {
173
+ const mcpConfig = this.gatewayConfig.mcpServers;
174
+ if (!mcpConfig || typeof mcpConfig !== 'object') {
175
+ console.log(info(`MCP: no servers configured`));
176
+ return;
177
+ }
178
+ const entries = Object.entries(mcpConfig);
179
+ const enabled = entries.filter(([_, cfg]) => cfg.enabled !== false);
180
+ if (enabled.length === 0) {
181
+ console.log(info(`MCP: no enabled servers`));
182
+ return;
183
+ }
184
+ console.log(info(`MCP: connecting to ${palette.white}${enabled.length}${palette.reset} server(s)...`));
185
+ for (const [name, cfg] of enabled) {
186
+ try {
187
+ const command = [...(cfg.command ?? []), ...(cfg.args ?? [])];
188
+ const bridge = new McpBridge({
189
+ command,
190
+ cwd: cfg.cwd ?? this.config.workspace,
191
+ env: cfg.env,
192
+ timeout: 30000,
193
+ });
194
+ await bridge.connect();
195
+ // Register all discovered tools into Mach6's registry
196
+ const tools = bridge.getTools();
197
+ for (const tool of tools) {
198
+ this.toolRegistry.register(tool);
199
+ }
200
+ this.mcpBridges.push(bridge);
201
+ console.log(ok(`MCP: ${palette.cyan}${name}${palette.reset} — ${tools.length} tools`));
202
+ }
203
+ catch (err) {
204
+ console.log(warn(`MCP: ${name} — ${err instanceof Error ? err.message : err}`));
205
+ // Non-fatal — other servers + builtins still work
206
+ }
207
+ }
208
+ // Rebuild system prompt with new tools
209
+ if (this.mcpBridges.length > 0) {
210
+ this.systemPrompt = buildSystemPrompt({
211
+ workspace: this.config.workspace,
212
+ tools: this.toolRegistry.list().map(t => t.name),
213
+ });
214
+ console.log(ok(`MCP: system prompt rebuilt (${palette.gold}${this.toolRegistry.list().length}${palette.reset} total tools)`));
215
+ }
216
+ }
217
+ // ── Channel Setup ──────────────────────────────────────────────────────
218
+ async startChannels() {
219
+ const channels = this.gatewayConfig.channels;
220
+ if (!channels)
221
+ return;
222
+ // Discord (non-fatal — if Discord fails, other adapters still start)
223
+ if (channels.discord?.enabled) {
224
+ try {
225
+ console.log(info('Starting Discord adapter...'));
226
+ const adapter = new DiscordAdapter('discord-main');
227
+ const policy = {
228
+ dmPolicy: 'open',
229
+ groupPolicy: 'mention-only',
230
+ ownerIds: this.gatewayConfig.ownerIds ?? [],
231
+ requireMention: true,
232
+ selfId: channels.discord.botId, // Required for mention detection
233
+ siblingBotIds: channels.discord.siblingBotIds ?? [],
234
+ ...channels.discord.policy,
235
+ };
236
+ await this.channelRegistry.register(adapter, { token: channels.discord.token, botId: channels.discord.botId }, policy);
237
+ console.log(ok(`Discord ${palette.green}connected${palette.reset}`));
238
+ presenceManager.registerAdapter('discord-main', (chatId) => adapter.typing(chatId));
239
+ // Register Discord client for rich activity presence
240
+ const discordClient = adapter.getClient();
241
+ if (discordClient)
242
+ presenceManager.registerDiscordClient('discord-main', discordClient);
243
+ }
244
+ catch (err) {
245
+ console.log(warn(`Discord failed — ${err.message}`));
246
+ }
247
+ }
248
+ // WhatsApp (non-fatal — log and continue if it fails)
249
+ if (channels.whatsapp?.enabled) {
250
+ try {
251
+ console.log(info('Starting WhatsApp adapter...'));
252
+ const adapter = new WhatsAppAdapter('whatsapp-main');
253
+ const policy = {
254
+ dmPolicy: 'allowlist',
255
+ groupPolicy: 'mention-only',
256
+ ownerIds: this.gatewayConfig.ownerIds ?? [],
257
+ allowedSenders: this.gatewayConfig.ownerIds ?? [],
258
+ ...channels.whatsapp.policy,
259
+ };
260
+ await this.channelRegistry.register(adapter, {
261
+ authDir: channels.whatsapp.authDir,
262
+ phoneNumber: channels.whatsapp.phoneNumber,
263
+ autoRead: channels.whatsapp.autoRead ?? true,
264
+ onQR: (qr) => {
265
+ console.log(`\n ${palette.gold}📱 WhatsApp QR Code — scan to link:${palette.reset}\n${qr}\n`);
266
+ },
267
+ }, policy);
268
+ console.log(ok(`WhatsApp ${palette.green}connected${palette.reset}`));
269
+ presenceManager.registerAdapter('whatsapp-main', (chatId) => adapter.typing(chatId));
270
+ }
271
+ catch (err) {
272
+ console.log(warn(`WhatsApp failed — ${err.message}`));
273
+ }
274
+ }
275
+ }
276
+ // ── HTTP API ───────────────────────────────────────────────────────────
277
+ async startHttpApi() {
278
+ const port = this.gatewayConfig.apiPort ?? 3006;
279
+ const apiKey = process.env.MACH6_API_KEY || process.env.API_KEY || '';
280
+ if (!apiKey) {
281
+ console.log(warn('No MACH6_API_KEY — HTTP API disabled'));
282
+ return;
283
+ }
284
+ this.httpApi = new HttpApiServer({
285
+ port,
286
+ apiKey,
287
+ allowedOrigins: this.config.allowedOrigins ?? ['*'],
288
+ onChat: async (request) => {
289
+ return this.handleHttpChat(request);
290
+ },
291
+ onRelay: async (target, text) => {
292
+ // Relay to WhatsApp
293
+ try {
294
+ const result = await this.channelRegistry.sendToChannel('whatsapp', target, {
295
+ content: text,
296
+ });
297
+ return { success: result.success, error: result.error };
298
+ }
299
+ catch (err) {
300
+ return { success: false, error: err instanceof Error ? err.message : String(err) };
301
+ }
302
+ },
303
+ onHealth: () => this.status(),
304
+ });
305
+ await this.httpApi.start();
306
+ }
307
+ /**
308
+ * Handle an HTTP API chat request by running it through the full agent pipeline.
309
+ * Creates a synthetic BusEnvelope and processes it like any other channel message.
310
+ */
311
+ handleHttpChat(request) {
312
+ return new Promise(async (resolve, reject) => {
313
+ const sessionId = request.sessionId ?? `http-${request.source ?? 'web'}-${request.senderId ?? 'anon'}`;
314
+ const controller = new AbortController();
315
+ try {
316
+ // Load or create session
317
+ let session = this.sessionManager.load(sessionId) ?? this.sessionManager.create(sessionId, {
318
+ provider: this.providerName,
319
+ model: this.model,
320
+ });
321
+ // Build system prompt
322
+ const turnPrompt = buildSystemPrompt({
323
+ workspace: this.config.workspace,
324
+ tools: this.toolRegistry.list().map(t => t.name),
325
+ channel: 'http',
326
+ chatType: 'direct',
327
+ senderId: request.senderId ?? 'http-user',
328
+ });
329
+ if (session.messages.length > 0 && session.messages[0].role === 'system') {
330
+ session.messages[0].content = turnPrompt;
331
+ }
332
+ else {
333
+ session.messages.unshift({ role: 'system', content: turnPrompt });
334
+ }
335
+ // Build user message content
336
+ const userContent = request.senderName
337
+ ? `[${request.senderName}] ${request.text}`
338
+ : request.text;
339
+ session.messages.push({ role: 'user', content: userContent });
340
+ // Sandbox context — HTTP API users get 'standard' tier (not admin)
341
+ const ownerIds = this.gatewayConfig.ownerIds ?? [];
342
+ const isOwner = request.senderId ? ownerIds.includes(request.senderId) : false;
343
+ const sandboxCtx = {
344
+ sessionId,
345
+ adapterId: 'http-api',
346
+ channelType: 'http',
347
+ chatType: 'direct',
348
+ senderId: request.senderId ?? 'http-user',
349
+ isOwner,
350
+ };
351
+ const sandboxedTools = createSandboxedRegistry(this.toolRegistry, sandboxCtx);
352
+ // Provider config
353
+ const providerCfg = this.config.providers[this.providerName] ?? {};
354
+ const provConfig = {
355
+ model: this.model,
356
+ maxTokens: this.config.maxTokens,
357
+ temperature: this.config.temperature,
358
+ systemPrompt: this.systemPrompt,
359
+ ...providerCfg,
360
+ };
361
+ // Run agent
362
+ console.log(`${palette.dim} [http]${palette.reset} Agent turn for ${palette.violet}${sessionId}${palette.reset}`);
363
+ const startMs = Date.now();
364
+ const result = await runAgent(session.messages, {
365
+ provider: this.provider,
366
+ providerConfig: provConfig,
367
+ toolRegistry: sandboxedTools,
368
+ sessionId,
369
+ maxIterations: this.config.maxIterations ?? 50,
370
+ abortSignal: controller.signal,
371
+ onEvent: (ev) => {
372
+ if (ev.type === 'usage') {
373
+ this.sessionManager.trackUsage(session, ev.usage.inputTokens, ev.usage.outputTokens);
374
+ }
375
+ },
376
+ onToolStart: (name) => console.log(` ${palette.violet}⚡ ${name}${palette.reset}`),
377
+ onToolEnd: (name) => console.log(` ${palette.green}✓ ${name}${palette.reset}`),
378
+ });
379
+ // Save session
380
+ session.messages = result.messages;
381
+ if (result.text) {
382
+ session.messages.push({ role: 'assistant', content: result.text });
383
+ }
384
+ this.sessionManager.save(session);
385
+ resolve({
386
+ text: result.text ?? '',
387
+ sessionId,
388
+ durationMs: Date.now() - startMs,
389
+ });
390
+ }
391
+ catch (err) {
392
+ reject(err);
393
+ }
394
+ });
395
+ }
396
+ // ── Message Loop ───────────────────────────────────────────────────────
397
+ /**
398
+ * The core message loop. Polls the bus for new messages and dispatches
399
+ * agent turns. Each session gets at most one concurrent agent turn.
400
+ *
401
+ * When a new message arrives during an active turn, the bus handles it:
402
+ * - interrupt priority → cancels current turn
403
+ * - high priority → queued, injected at next iteration
404
+ * - normal/low → queued for after current turn
405
+ */
406
+ startMessageLoop() {
407
+ const bus = this.channelRegistry.getBus();
408
+ const knownSessions = new Set();
409
+ // Check for new sessions periodically
410
+ setInterval(() => {
411
+ if (this.shutdownRequested)
412
+ return;
413
+ const routes = this.channelRegistry.getRouter().getRoutes();
414
+ for (const route of routes) {
415
+ if (knownSessions.has(route.sessionId))
416
+ continue;
417
+ knownSessions.add(route.sessionId);
418
+ // Subscribe to this session
419
+ bus.subscribe(route.sessionId, (envelope) => {
420
+ this.handleEnvelope(envelope);
421
+ });
422
+ // Subscribe to interrupts
423
+ bus.onInterrupt(route.sessionId, (envelope) => {
424
+ this.handleInterrupt(envelope);
425
+ });
426
+ }
427
+ }, 100);
428
+ }
429
+ pendingEnvelopes = new Map();
430
+ async handleEnvelope(envelope) {
431
+ const sessionId = envelope.sessionId;
432
+ // Check if there's already an active turn for this session
433
+ if (this.activeTurns.has(sessionId)) {
434
+ // Queue the envelope for when the current turn finishes
435
+ const pending = this.pendingEnvelopes.get(sessionId) ?? [];
436
+ pending.push(envelope);
437
+ this.pendingEnvelopes.set(sessionId, pending);
438
+ console.log(`${palette.dim} [gateway]${palette.reset} Queued for active session ${palette.violet}${sessionId}${palette.reset} ${palette.dim}(${pending.length} pending)${palette.reset}`);
439
+ return;
440
+ }
441
+ // Start a new agent turn
442
+ await this.runAgentTurn(envelope);
443
+ }
444
+ handleInterrupt(envelope) {
445
+ const sessionId = envelope.sessionId;
446
+ const active = this.activeTurns.get(sessionId);
447
+ if (!active)
448
+ return;
449
+ console.log(`${palette.dim} [gateway]${palette.reset} ${palette.yellow}Interrupting${palette.reset} session ${palette.violet}${sessionId}${palette.reset}`);
450
+ active.abortController.abort('new_message');
451
+ }
452
+ // ── Agent Turn ─────────────────────────────────────────────────────────
453
+ async runAgentTurn(envelope) {
454
+ const sessionId = envelope.sessionId;
455
+ const controller = new AbortController();
456
+ const turn = {
457
+ sessionId,
458
+ abortController: controller,
459
+ startedAt: Date.now(),
460
+ channelType: envelope.source.channelType,
461
+ chatId: envelope.source.chatId,
462
+ adapterId: envelope.source.adapterId,
463
+ };
464
+ this.activeTurns.set(sessionId, turn);
465
+ // Build sandbox context for this session
466
+ const ownerIds = this.gatewayConfig.ownerIds ?? [];
467
+ const isOwner = ownerIds.includes(envelope.source.senderId);
468
+ const chatType = (envelope.source.chatId.includes('@g.') || envelope.metadata.guildId) ? 'group' : 'direct';
469
+ const sandboxCtx = {
470
+ sessionId,
471
+ adapterId: envelope.source.adapterId,
472
+ channelType: envelope.source.channelType,
473
+ chatType,
474
+ senderId: envelope.source.senderId,
475
+ isOwner,
476
+ };
477
+ const sandboxedTools = createSandboxedRegistry(this.toolRegistry, sandboxCtx);
478
+ // Record user activity for heartbeat scheduling
479
+ if (envelope.source.adapterId !== 'heartbeat') {
480
+ this.heartbeat.recordUserActivity();
481
+ }
482
+ this.channelRegistry.setSessionActive(sessionId, true);
483
+ // Start sustained typing (refreshes every 4s until response sent)
484
+ const typingTarget = { adapterId: envelope.source.adapterId, chatId: envelope.source.chatId };
485
+ presenceManager.startTyping(typingTarget);
486
+ try {
487
+ // Load or create session
488
+ let session = this.sessionManager.load(sessionId) ?? this.sessionManager.create(sessionId, {
489
+ provider: this.providerName,
490
+ model: this.model,
491
+ });
492
+ // Build channel-aware system prompt (refreshes workspace files each turn)
493
+ const turnPrompt = buildSystemPrompt({
494
+ workspace: this.config.workspace,
495
+ tools: this.toolRegistry.list().map(t => t.name),
496
+ channel: envelope.source.channelType,
497
+ chatType: envelope.source.chatId.includes('@g.') ? 'group' : 'direct',
498
+ senderId: envelope.source.senderId,
499
+ });
500
+ // Replace or insert system prompt (always fresh — workspace files may have changed)
501
+ if (session.messages.length > 0 && session.messages[0].role === 'system') {
502
+ session.messages[0].content = turnPrompt;
503
+ }
504
+ else {
505
+ session.messages.unshift({ role: 'system', content: turnPrompt });
506
+ }
507
+ // Add user message
508
+ const userContent = this.buildUserContent(envelope);
509
+ session.messages.push({ role: 'user', content: userContent });
510
+ // Pre-flight context trim: estimate token count and archive if approaching limit
511
+ // Rough estimate: 1 token ≈ 4 chars. Model limit 128K, leave 20K headroom.
512
+ const TOKEN_LIMIT = 128_000;
513
+ const HEADROOM = 20_000;
514
+ const estimatedTokens = session.messages.reduce((sum, m) => {
515
+ const content = typeof m.content === 'string' ? m.content : JSON.stringify(m.content ?? '');
516
+ return sum + Math.ceil(content.length / 4);
517
+ }, 0);
518
+ if (estimatedTokens > TOKEN_LIMIT - HEADROOM) {
519
+ console.log(`${palette.dim} [gateway]${palette.reset} ${palette.yellow}⚠${palette.reset} Pre-flight trim: ~${estimatedTokens} tokens ${palette.dim}(limit ${TOKEN_LIMIT})${palette.reset}`);
520
+ const archived = this.sessionManager.archive(sessionId, 30);
521
+ console.log(`${palette.dim} [gateway]${palette.reset} Archived ${archived} messages → ${session.messages.length} remaining`);
522
+ // Reload session after archive
523
+ const trimmed = this.sessionManager.load(sessionId);
524
+ if (trimmed) {
525
+ session.messages = trimmed.messages;
526
+ }
527
+ }
528
+ // Provider config
529
+ const providerCfg = this.config.providers[this.providerName] ?? {};
530
+ const provConfig = {
531
+ model: this.model,
532
+ maxTokens: this.config.maxTokens,
533
+ temperature: this.config.temperature,
534
+ systemPrompt: this.systemPrompt,
535
+ ...providerCfg,
536
+ };
537
+ // Run agent
538
+ console.log(`\n${palette.dim} [turn]${palette.reset} ${palette.violet}${sessionId}${palette.reset} ${palette.dim}via${palette.reset} ${envelope.source.channelType}${palette.dim}/${palette.reset}${envelope.source.chatId}`);
539
+ const turnStartTime = Date.now();
540
+ const result = await runAgent(session.messages, {
541
+ provider: this.provider,
542
+ providerConfig: provConfig,
543
+ toolRegistry: sandboxedTools,
544
+ sessionId,
545
+ maxIterations: this.config.maxIterations ?? 50,
546
+ abortSignal: controller.signal,
547
+ onEvent: (ev) => {
548
+ if (ev.type === 'usage') {
549
+ this.sessionManager.trackUsage(session, ev.usage.inputTokens, ev.usage.outputTokens);
550
+ console.log(` ${palette.dim}📊 +${ev.usage.inputTokens}in / +${ev.usage.outputTokens}out${palette.reset}`);
551
+ }
552
+ // Update presence when LLM starts streaming
553
+ if (ev.type === 'text_delta' || ev.type === 'done') {
554
+ presenceManager.llmStreaming();
555
+ }
556
+ },
557
+ onToolStart: (name) => {
558
+ console.log(` ${palette.violet}⚡ ${name}${palette.reset}`);
559
+ presenceManager.toolStart(name);
560
+ },
561
+ onToolEnd: (name, res) => {
562
+ const preview = res.length > 100 ? res.slice(0, 100) + '...' : res;
563
+ console.log(` ${palette.green}✓ ${name}${palette.reset} ${palette.dim}${preview.split('\n')[0]}${palette.reset}`);
564
+ presenceManager.toolEnd(name);
565
+ },
566
+ });
567
+ const turnElapsed = Date.now() - turnStartTime;
568
+ console.log(`${palette.dim} [turn]${palette.reset} Complete ${palette.dim}— ${turnElapsed}ms, ${result.iterations} iter, ${result.toolCalls.length} tools${palette.reset}`);
569
+ // Save session
570
+ session.messages = result.messages;
571
+ if (result.text) {
572
+ session.messages.push({ role: 'assistant', content: result.text });
573
+ }
574
+ this.sessionManager.save(session);
575
+ // Auto-archive bloated sessions (>200KB → keep last 30 messages)
576
+ this.sessionManager.autoArchive();
577
+ // Send response back through the channel
578
+ if (result.text && result.text !== 'NO_REPLY' && result.text !== 'HEARTBEAT_OK') {
579
+ console.log(`${palette.dim} [send]${palette.reset} → ${envelope.source.adapterId}/${envelope.source.chatId} ${palette.dim}(${result.text.length} chars)${palette.reset}`);
580
+ try {
581
+ const sendResult = await this.channelRegistry.send(envelope.source.adapterId, envelope.source.chatId, {
582
+ content: result.text,
583
+ replyToId: envelope.metadata.platformMessageId,
584
+ });
585
+ console.log(`${palette.dim} [send]${palette.reset} ${palette.green}delivered${palette.reset}`);
586
+ }
587
+ catch (sendErr) {
588
+ console.error(` ${palette.red}✗ [send]${palette.reset} ${sendErr}`);
589
+ }
590
+ }
591
+ else {
592
+ console.log(`${palette.dim} [turn]${palette.reset} No response ${palette.dim}(${result.text ? result.text.slice(0, 50) : 'null'})${palette.reset}`);
593
+ }
594
+ }
595
+ catch (err) {
596
+ if (controller.signal.aborted) {
597
+ console.log(`${palette.dim} [turn]${palette.reset} ${palette.yellow}Interrupted${palette.reset} ${palette.violet}${sessionId}${palette.reset}`);
598
+ // Re-process with accumulated messages
599
+ // Check our pending queue + bus drain
600
+ const pending = this.pendingEnvelopes.get(sessionId) ?? [];
601
+ const busPending = this.channelRegistry.getBus().drain(sessionId);
602
+ const allPending = [...pending, ...busPending];
603
+ this.pendingEnvelopes.delete(sessionId);
604
+ if (allPending.length > 0) {
605
+ // Recursion with new context
606
+ this.activeTurns.delete(sessionId);
607
+ this.channelRegistry.setSessionActive(sessionId, false);
608
+ await this.runAgentTurn(allPending[allPending.length - 1]); // most recent message
609
+ return;
610
+ }
611
+ }
612
+ else {
613
+ console.error(` ${palette.red}✗ [turn]${palette.reset} ${palette.violet}${sessionId}${palette.reset}: ${err}`);
614
+ // Send error message
615
+ try {
616
+ await this.channelRegistry.send(envelope.source.adapterId, envelope.source.chatId, { content: `⚠️ Error: ${err instanceof Error ? err.message : String(err)}` });
617
+ }
618
+ catch { /* ignore send errors */ }
619
+ }
620
+ }
621
+ finally {
622
+ this.activeTurns.delete(sessionId);
623
+ this.channelRegistry.setSessionActive(sessionId, false);
624
+ presenceManager.stopTyping(typingTarget);
625
+ // Process any pending messages that arrived during this turn
626
+ const pending = this.pendingEnvelopes.get(sessionId);
627
+ if (pending && pending.length > 0) {
628
+ this.pendingEnvelopes.delete(sessionId);
629
+ console.log(`${palette.dim} [gateway]${palette.reset} Processing ${pending.length} pending for ${palette.violet}${sessionId}${palette.reset}`);
630
+ // Process the most recent pending message (others are stale context)
631
+ await this.runAgentTurn(pending[pending.length - 1]);
632
+ }
633
+ }
634
+ }
635
+ buildUserContent(envelope) {
636
+ const parts = [];
637
+ // Sender context
638
+ if (envelope.source.senderName) {
639
+ parts.push(`[${envelope.source.senderName}]`);
640
+ }
641
+ // Reply context
642
+ if (envelope.source.replyToId) {
643
+ parts.push(`(replying to message ${envelope.source.replyToId})`);
644
+ }
645
+ // Text
646
+ if (envelope.payload.text) {
647
+ parts.push(envelope.payload.text);
648
+ }
649
+ // Media descriptions — include local path if downloaded
650
+ if (envelope.payload.media?.length) {
651
+ for (const m of envelope.payload.media) {
652
+ const desc = [m.type];
653
+ if (m.filename)
654
+ desc.push(m.filename);
655
+ else if (m.mimeType)
656
+ desc.push(m.mimeType);
657
+ if (m.path)
658
+ desc.push(`path=${m.path}`);
659
+ if (m.caption)
660
+ desc.push(`caption="${m.caption}"`);
661
+ if (m.width && m.height)
662
+ desc.push(`${m.width}x${m.height}`);
663
+ parts.push(`[${desc.join(', ')}]`);
664
+ }
665
+ }
666
+ return parts.join(' ');
667
+ }
668
+ // ── Signals ────────────────────────────────────────────────────────────
669
+ setupSignals() {
670
+ const shutdown = async (signal) => {
671
+ if (this.shutdownRequested)
672
+ return;
673
+ this.shutdownRequested = true;
674
+ console.log(`\n${palette.dim} [gateway]${palette.reset} ${palette.yellow}${signal}${palette.reset} — shutting down...`);
675
+ // Cancel all active turns
676
+ for (const [, turn] of this.activeTurns) {
677
+ turn.abortController.abort('shutdown');
678
+ }
679
+ // Disconnect all channels
680
+ await this.channelRegistry.destroy();
681
+ // Stop HTTP API
682
+ if (this.httpApi)
683
+ await this.httpApi.stop();
684
+ // Disconnect MCP bridges
685
+ for (const bridge of this.mcpBridges) {
686
+ try {
687
+ bridge.disconnect();
688
+ }
689
+ catch { /* ignore */ }
690
+ }
691
+ presenceManager.stopAll();
692
+ this.heartbeat.stop();
693
+ console.log(`${palette.dim} [gateway]${palette.reset} Shutdown complete.`);
694
+ process.exit(0);
695
+ };
696
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
697
+ process.on('SIGINT', () => shutdown('SIGINT'));
698
+ // SIGUSR1 = hot-reload config (Linux/macOS only — not supported on Windows)
699
+ // On Windows: restart the process, or POST /api/v1/health to verify state
700
+ if (process.platform !== 'win32') {
701
+ process.on('SIGUSR1', () => {
702
+ console.log(`${palette.dim} [gateway]${palette.reset} ${palette.cyan}SIGUSR1${palette.reset} — reloading config...`);
703
+ try {
704
+ this.config = loadConfig(this.gatewayConfig.configPath);
705
+ this.providerName = this.config.defaultProvider;
706
+ this.provider = PROVIDERS.get(this.providerName);
707
+ this.model = this.config.defaultModel;
708
+ this.systemPrompt = buildSystemPrompt({
709
+ workspace: this.config.workspace,
710
+ tools: this.toolRegistry.list().map(t => t.name),
711
+ });
712
+ console.log(`${palette.dim} [gateway]${palette.reset} System prompt refreshed ${palette.dim}(${this.systemPrompt.length} chars)${palette.reset}`);
713
+ console.log(ok('Config reloaded successfully'));
714
+ }
715
+ catch (err) {
716
+ console.error(` ${palette.red}✗ [gateway]${palette.reset} Config reload failed: ${err}`);
717
+ }
718
+ });
719
+ }
720
+ }
721
+ // ── Status ─────────────────────────────────────────────────────────────
722
+ status() {
723
+ return {
724
+ uptime: Date.now() - this.startTime,
725
+ provider: `${this.providerName}/${this.model}`,
726
+ channels: this.channelRegistry.list(),
727
+ activeTurns: this.activeTurns.size,
728
+ sessions: this.sessionManager.list().length,
729
+ tools: this.toolRegistry.list().length,
730
+ };
731
+ }
732
+ }
733
+ // ─── CLI Entry ─────────────────────────────────────────────────────────────
734
+ export async function startGateway(configPath) {
735
+ // Load gateway config from mach6.json or env
736
+ const config = loadConfig(configPath);
737
+ // Build gateway config from environment + mach6.json
738
+ const gatewayConfig = {
739
+ configPath,
740
+ ownerIds: config.ownerIds ?? [],
741
+ channels: {
742
+ discord: {
743
+ enabled: !!process.env.DISCORD_BOT_TOKEN || !!config.discord?.token,
744
+ token: process.env.DISCORD_BOT_TOKEN ?? config.discord?.token ?? '',
745
+ botId: config.discord?.botId,
746
+ siblingBotIds: config.discord?.siblingBotIds,
747
+ policy: config.discord?.policy,
748
+ },
749
+ whatsapp: {
750
+ enabled: !!config.whatsapp?.enabled,
751
+ authDir: config.whatsapp?.authDir ?? path.join(os.homedir(), '.mach6', 'whatsapp-auth'),
752
+ phoneNumber: config.whatsapp?.phoneNumber,
753
+ autoRead: config.whatsapp?.autoRead ?? true,
754
+ policy: config.whatsapp?.policy,
755
+ },
756
+ },
757
+ apiPort: config.apiPort ?? 3006,
758
+ };
759
+ const gateway = new Mach6Gateway(gatewayConfig);
760
+ await gateway.start();
761
+ return gateway;
762
+ }
763
+ // Run directly
764
+ const __filename = fileURLToPath(import.meta.url);
765
+ if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(__filename)) {
766
+ const configPath = process.argv.find(a => a.startsWith('--config='))?.split('=')[1];
767
+ startGateway(configPath).catch(err => {
768
+ console.error(` ${palette.red}✗${palette.reset} Gateway startup failed: ${err}`);
769
+ process.exit(1);
770
+ });
771
+ }
772
+ //# sourceMappingURL=daemon.js.map