@0xmaxma/claude-gateway 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 (192) hide show
  1. package/README.md +791 -0
  2. package/config.template.json +87 -0
  3. package/dist/agent/context-isolation.d.ts +28 -0
  4. package/dist/agent/context-isolation.d.ts.map +1 -0
  5. package/dist/agent/context-isolation.js +80 -0
  6. package/dist/agent/context-isolation.js.map +1 -0
  7. package/dist/agent/runner.d.ts +211 -0
  8. package/dist/agent/runner.d.ts.map +1 -0
  9. package/dist/agent/runner.js +1802 -0
  10. package/dist/agent/runner.js.map +1 -0
  11. package/dist/agent/workspace-loader.d.ts +31 -0
  12. package/dist/agent/workspace-loader.d.ts.map +1 -0
  13. package/dist/agent/workspace-loader.js +207 -0
  14. package/dist/agent/workspace-loader.js.map +1 -0
  15. package/dist/api/auth.d.ts +25 -0
  16. package/dist/api/auth.d.ts.map +1 -0
  17. package/dist/api/auth.js +77 -0
  18. package/dist/api/auth.js.map +1 -0
  19. package/dist/api/cron-router.d.ts +22 -0
  20. package/dist/api/cron-router.d.ts.map +1 -0
  21. package/dist/api/cron-router.js +197 -0
  22. package/dist/api/cron-router.js.map +1 -0
  23. package/dist/api/gateway-router.d.ts +52 -0
  24. package/dist/api/gateway-router.d.ts.map +1 -0
  25. package/dist/api/gateway-router.js +215 -0
  26. package/dist/api/gateway-router.js.map +1 -0
  27. package/dist/api/router.d.ts +5 -0
  28. package/dist/api/router.d.ts.map +1 -0
  29. package/dist/api/router.js +1098 -0
  30. package/dist/api/router.js.map +1 -0
  31. package/dist/api/skills-router.d.ts +5 -0
  32. package/dist/api/skills-router.d.ts.map +1 -0
  33. package/dist/api/skills-router.js +424 -0
  34. package/dist/api/skills-router.js.map +1 -0
  35. package/dist/api/workspace-router.d.ts +4 -0
  36. package/dist/api/workspace-router.d.ts.map +1 -0
  37. package/dist/api/workspace-router.js +149 -0
  38. package/dist/api/workspace-router.js.map +1 -0
  39. package/dist/config/loader.d.ts +16 -0
  40. package/dist/config/loader.d.ts.map +1 -0
  41. package/dist/config/loader.js +235 -0
  42. package/dist/config/loader.js.map +1 -0
  43. package/dist/config/migrator.d.ts +70 -0
  44. package/dist/config/migrator.d.ts.map +1 -0
  45. package/dist/config/migrator.js +343 -0
  46. package/dist/config/migrator.js.map +1 -0
  47. package/dist/config/watcher.d.ts +30 -0
  48. package/dist/config/watcher.d.ts.map +1 -0
  49. package/dist/config/watcher.js +170 -0
  50. package/dist/config/watcher.js.map +1 -0
  51. package/dist/cron/manager.d.ts +48 -0
  52. package/dist/cron/manager.d.ts.map +1 -0
  53. package/dist/cron/manager.js +580 -0
  54. package/dist/cron/manager.js.map +1 -0
  55. package/dist/cron/scheduler.d.ts +50 -0
  56. package/dist/cron/scheduler.d.ts.map +1 -0
  57. package/dist/cron/scheduler.js +231 -0
  58. package/dist/cron/scheduler.js.map +1 -0
  59. package/dist/discord/receiver.d.ts +17 -0
  60. package/dist/discord/receiver.d.ts.map +1 -0
  61. package/dist/discord/receiver.js +108 -0
  62. package/dist/discord/receiver.js.map +1 -0
  63. package/dist/heartbeat/history.d.ts +24 -0
  64. package/dist/heartbeat/history.d.ts.map +1 -0
  65. package/dist/heartbeat/history.js +50 -0
  66. package/dist/heartbeat/history.js.map +1 -0
  67. package/dist/heartbeat/parser.d.ts +26 -0
  68. package/dist/heartbeat/parser.d.ts.map +1 -0
  69. package/dist/heartbeat/parser.js +283 -0
  70. package/dist/heartbeat/parser.js.map +1 -0
  71. package/dist/history/cleanup.d.ts +21 -0
  72. package/dist/history/cleanup.d.ts.map +1 -0
  73. package/dist/history/cleanup.js +171 -0
  74. package/dist/history/cleanup.js.map +1 -0
  75. package/dist/history/db.d.ts +27 -0
  76. package/dist/history/db.d.ts.map +1 -0
  77. package/dist/history/db.js +323 -0
  78. package/dist/history/db.js.map +1 -0
  79. package/dist/history/media-store.d.ts +27 -0
  80. package/dist/history/media-store.d.ts.map +1 -0
  81. package/dist/history/media-store.js +158 -0
  82. package/dist/history/media-store.js.map +1 -0
  83. package/dist/history/types.d.ts +62 -0
  84. package/dist/history/types.d.ts.map +1 -0
  85. package/dist/history/types.js +3 -0
  86. package/dist/history/types.js.map +1 -0
  87. package/dist/index.d.ts +3 -0
  88. package/dist/index.d.ts.map +1 -0
  89. package/dist/index.js +483 -0
  90. package/dist/index.js.map +1 -0
  91. package/dist/logger.d.ts +3 -0
  92. package/dist/logger.d.ts.map +1 -0
  93. package/dist/logger.js +81 -0
  94. package/dist/logger.js.map +1 -0
  95. package/dist/memory/manager.d.ts +45 -0
  96. package/dist/memory/manager.d.ts.map +1 -0
  97. package/dist/memory/manager.js +178 -0
  98. package/dist/memory/manager.js.map +1 -0
  99. package/dist/session/compactor.d.ts +22 -0
  100. package/dist/session/compactor.d.ts.map +1 -0
  101. package/dist/session/compactor.js +151 -0
  102. package/dist/session/compactor.js.map +1 -0
  103. package/dist/session/process.d.ts +76 -0
  104. package/dist/session/process.d.ts.map +1 -0
  105. package/dist/session/process.js +729 -0
  106. package/dist/session/process.js.map +1 -0
  107. package/dist/session/store.d.ts +102 -0
  108. package/dist/session/store.d.ts.map +1 -0
  109. package/dist/session/store.js +533 -0
  110. package/dist/session/store.js.map +1 -0
  111. package/dist/skills/index.d.ts +9 -0
  112. package/dist/skills/index.d.ts.map +1 -0
  113. package/dist/skills/index.js +15 -0
  114. package/dist/skills/index.js.map +1 -0
  115. package/dist/skills/invoker.d.ts +16 -0
  116. package/dist/skills/invoker.d.ts.map +1 -0
  117. package/dist/skills/invoker.js +36 -0
  118. package/dist/skills/invoker.js.map +1 -0
  119. package/dist/skills/loader.d.ts +23 -0
  120. package/dist/skills/loader.d.ts.map +1 -0
  121. package/dist/skills/loader.js +183 -0
  122. package/dist/skills/loader.js.map +1 -0
  123. package/dist/skills/parser.d.ts +55 -0
  124. package/dist/skills/parser.d.ts.map +1 -0
  125. package/dist/skills/parser.js +178 -0
  126. package/dist/skills/parser.js.map +1 -0
  127. package/dist/skills/sync.d.ts +10 -0
  128. package/dist/skills/sync.d.ts.map +1 -0
  129. package/dist/skills/sync.js +130 -0
  130. package/dist/skills/sync.js.map +1 -0
  131. package/dist/skills/watcher.d.ts +16 -0
  132. package/dist/skills/watcher.d.ts.map +1 -0
  133. package/dist/skills/watcher.js +25 -0
  134. package/dist/skills/watcher.js.map +1 -0
  135. package/dist/telegram/markdown.d.ts +25 -0
  136. package/dist/telegram/markdown.d.ts.map +1 -0
  137. package/dist/telegram/markdown.js +237 -0
  138. package/dist/telegram/markdown.js.map +1 -0
  139. package/dist/telegram/receiver.d.ts +17 -0
  140. package/dist/telegram/receiver.d.ts.map +1 -0
  141. package/dist/telegram/receiver.js +104 -0
  142. package/dist/telegram/receiver.js.map +1 -0
  143. package/dist/types.d.ts +224 -0
  144. package/dist/types.d.ts.map +1 -0
  145. package/dist/types.js +3 -0
  146. package/dist/types.js.map +1 -0
  147. package/dist/ui/web-ui.d.ts +6 -0
  148. package/dist/ui/web-ui.d.ts.map +1 -0
  149. package/dist/ui/web-ui.js +227 -0
  150. package/dist/ui/web-ui.js.map +1 -0
  151. package/dist/watch/factory.d.ts +26 -0
  152. package/dist/watch/factory.d.ts.map +1 -0
  153. package/dist/watch/factory.js +43 -0
  154. package/dist/watch/factory.js.map +1 -0
  155. package/dist/webhook/manager.d.ts +34 -0
  156. package/dist/webhook/manager.d.ts.map +1 -0
  157. package/dist/webhook/manager.js +120 -0
  158. package/dist/webhook/manager.js.map +1 -0
  159. package/mcp/bun.lock +264 -0
  160. package/mcp/channel-manager.ts +115 -0
  161. package/mcp/package.json +15 -0
  162. package/mcp/router.ts +69 -0
  163. package/mcp/server.ts +129 -0
  164. package/mcp/tools/agent/handlers.ts +578 -0
  165. package/mcp/tools/agent/module.ts +163 -0
  166. package/mcp/tools/browser/module.ts +321 -0
  167. package/mcp/tools/browser/skills/open-browser/SKILL.md +89 -0
  168. package/mcp/tools/cron/client.ts +65 -0
  169. package/mcp/tools/cron/module.ts +158 -0
  170. package/mcp/tools/cron/skills/cron/SKILL.md +46 -0
  171. package/mcp/tools/discord/access.ts +197 -0
  172. package/mcp/tools/discord/client.ts +31 -0
  173. package/mcp/tools/discord/commands.ts +48 -0
  174. package/mcp/tools/discord/inbound.ts +53 -0
  175. package/mcp/tools/discord/module.ts +442 -0
  176. package/mcp/tools/discord/outbound.ts +90 -0
  177. package/mcp/tools/discord/receiver-server.ts +58 -0
  178. package/mcp/tools/discord/skills/access/SKILL.md +86 -0
  179. package/mcp/tools/discord/skills/configure/SKILL.md +53 -0
  180. package/mcp/tools/discord/threading.ts +37 -0
  181. package/mcp/tools/discord/types.ts +106 -0
  182. package/mcp/tools/skills/handlers.ts +196 -0
  183. package/mcp/tools/skills/module.ts +163 -0
  184. package/mcp/tools/telegram/module.ts +454 -0
  185. package/mcp/tools/telegram/pure.ts +224 -0
  186. package/mcp/tools/telegram/receiver-server.ts +1656 -0
  187. package/mcp/tools/telegram/skills/access/SKILL.md +169 -0
  188. package/mcp/tools/telegram/skills/configure/SKILL.md +118 -0
  189. package/mcp/tools/telegram/typing.ts +327 -0
  190. package/mcp/types.ts +128 -0
  191. package/package.json +62 -0
  192. package/resource/claude_gateway.svg +112 -0
@@ -0,0 +1,729 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.SessionProcess = void 0;
40
+ const child_process_1 = require("child_process");
41
+ const events_1 = require("events");
42
+ const fs = __importStar(require("fs"));
43
+ const os = __importStar(require("os"));
44
+ const path = __importStar(require("path"));
45
+ const chokidar_1 = __importDefault(require("chokidar"));
46
+ const logger_1 = require("../logger");
47
+ const MAX_HISTORY_MESSAGES = 50;
48
+ const AUTO_RESTART_DELAY_MS = 5000;
49
+ const MAX_RESTARTS = 3;
50
+ const CHANNELS_ACTIVATION_PROMPT = 'Channels mode is active. Wait for incoming messages from your channels and respond to them.';
51
+ class SessionProcess extends events_1.EventEmitter {
52
+ constructor(sessionId, source, agentConfig, gatewayConfig, sessionStore, chatId) {
53
+ super();
54
+ this.lastActivityAt = Date.now(); // accessible by AgentRunner for eviction sort
55
+ this.spawnContext = null;
56
+ this.process = null;
57
+ this.stopping = false;
58
+ this.restartCount = 0;
59
+ this.restartRequested = false;
60
+ this._processing = false;
61
+ this._pendingRestart = false;
62
+ this.restartWatcher = null;
63
+ this.queryMode = false;
64
+ this._queryBuffer = '';
65
+ this._querySettled = false;
66
+ this.sessionId = sessionId;
67
+ this.source = source;
68
+ this.chatId = chatId ?? sessionId;
69
+ this.sessionChannel = source === 'discord' ? 'discord' : 'telegram';
70
+ this.agentConfig = agentConfig;
71
+ this.gatewayConfig = gatewayConfig;
72
+ this.sessionStore = sessionStore;
73
+ this.logger = (0, logger_1.createLogger)(`${agentConfig.id}:session:${sessionId}`, gatewayConfig.gateway.logDir);
74
+ // config.json lives 3 levels above workspace: <base>/<agentId>/workspace → <base>/config.json
75
+ this.configPath = path.resolve(agentConfig.workspace, '..', '..', '..', 'config.json');
76
+ const stateSubDir = source === 'discord' ? '.discord-state' : '.telegram-state';
77
+ this.restartSignalPath = path.join(agentConfig.workspace, stateSubDir, `restart-${sessionId}`);
78
+ }
79
+ /**
80
+ * Read model from config.json on disk so restarts pick up changes made after startup.
81
+ * Falls back to the cached agentConfig.claude.model if config can't be read.
82
+ */
83
+ readFreshModel() {
84
+ try {
85
+ const raw = fs.readFileSync(this.configPath, 'utf-8');
86
+ const config = JSON.parse(raw);
87
+ const found = config.agents?.find(a => a.id === this.agentConfig.id);
88
+ if (found?.claude?.model)
89
+ return found.claude.model;
90
+ }
91
+ catch {
92
+ // fallback to cached value
93
+ }
94
+ return this.agentConfig.claude.model;
95
+ }
96
+ async start() {
97
+ this.stopping = false;
98
+ this.restartCount = 0;
99
+ this.setupRestartWatcher();
100
+ await this.spawnProcess();
101
+ }
102
+ /**
103
+ * Watch for a restart signal file written by the agent.
104
+ * When detected, kill the current Claude process — scheduleRestart() will
105
+ * re-spawn it with the latest model from config.json.
106
+ */
107
+ setupRestartWatcher() {
108
+ if (this.restartWatcher)
109
+ return;
110
+ this.restartWatcher = chokidar_1.default.watch(this.restartSignalPath, { ignoreInitial: true });
111
+ this.restartWatcher.on('add', () => {
112
+ // Read signal file content before deleting — may contain a notify payload
113
+ let notifyPayload = null;
114
+ try {
115
+ const content = fs.readFileSync(this.restartSignalPath, 'utf-8').trim();
116
+ if (content) {
117
+ const parsed = JSON.parse(content);
118
+ notifyPayload = parsed.notify ?? null;
119
+ }
120
+ }
121
+ catch { /* empty or unparseable — no notify */ }
122
+ try {
123
+ fs.rmSync(this.restartSignalPath, { force: true });
124
+ }
125
+ catch { }
126
+ this.restartRequested = true;
127
+ this.logger.info('Graceful restart requested', { sessionId: this.sessionId, hasNotify: !!notifyPayload });
128
+ // Inject a marker into session history so the next spawned session
129
+ // knows the restart is complete and does not repeat it.
130
+ // If notify payload is present, include instruction for the agent to send a message after restart.
131
+ const marker = notifyPayload
132
+ ? `[System: Graceful restart completed successfully. Do not restart again. IMPORTANT: Send a Telegram reply to chat_id "${notifyPayload.chat_id}" with the message: "${notifyPayload.text}"]`
133
+ : '[System: Graceful restart completed successfully. Do not restart again.]';
134
+ const restartMsg = { role: 'assistant', content: marker, ts: Date.now() };
135
+ const appendRestartMarker = this.source !== 'api'
136
+ ? this.sessionStore.appendTelegramMessage(this.agentConfig.id, this.chatId, this.sessionId, restartMsg, this.sessionChannel)
137
+ : this.sessionStore.appendMessage(this.agentConfig.id, this.sessionId, restartMsg);
138
+ appendRestartMarker.catch(err => this.logger.warn('Failed to write restart marker', { error: err.message }));
139
+ if (this.process) {
140
+ this.process.kill('SIGTERM');
141
+ }
142
+ });
143
+ }
144
+ async buildInitialPrompt() {
145
+ const history = this.source !== 'api'
146
+ ? await this.sessionStore.loadTelegramSession(this.agentConfig.id, this.chatId, this.sessionId, this.sessionChannel)
147
+ : await this.sessionStore.loadSession(this.agentConfig.id, this.sessionId);
148
+ // If history exceeds the limit and history[0] is a compaction summary, rescue it
149
+ // so the model retains context from before the truncation window.
150
+ const SUMMARY_MARKER = '[Conversation Summary]';
151
+ const firstMsg = history[0];
152
+ const hasSummary = history.length > MAX_HISTORY_MESSAGES &&
153
+ firstMsg?.role === 'system' &&
154
+ typeof firstMsg.content === 'string' &&
155
+ firstMsg.content.trimStart().startsWith(SUMMARY_MARKER);
156
+ const recent = hasSummary
157
+ ? [firstMsg, ...history.slice(-(MAX_HISTORY_MESSAGES - 1))]
158
+ : history.slice(-MAX_HISTORY_MESSAGES);
159
+ const loadedAtSpawn = recent.length;
160
+ const archivedCount = history.length - recent.length;
161
+ const messageCountAtSpawn = history.length;
162
+ if (recent.length === 0) {
163
+ return { prompt: CHANNELS_ACTIVATION_PROMPT, loadedAtSpawn, archivedCount, messageCountAtSpawn };
164
+ }
165
+ const historyText = recent
166
+ .map(m => {
167
+ // system role carries injected summaries (e.g. [Image Context Summary]) from the runner
168
+ if (m.role === 'system')
169
+ return `System: ${m.content}`;
170
+ return `${m.role === 'user' ? 'User' : 'Assistant'}: ${m.content}`;
171
+ })
172
+ .join('\n');
173
+ return {
174
+ prompt: `[Conversation history with this user:\n${historyText}]\n\n${CHANNELS_ACTIVATION_PROMPT}`,
175
+ loadedAtSpawn,
176
+ archivedCount,
177
+ messageCountAtSpawn,
178
+ };
179
+ }
180
+ /**
181
+ * Read stdio MCP servers from Claude Code's user-scoped config (~/.claude/settings.json).
182
+ * Returns empty object if file doesn't exist, can't be parsed, or has no mcpServers.
183
+ */
184
+ readUserScopedMcp() {
185
+ try {
186
+ const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
187
+ const parsed = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
188
+ return parsed?.mcpServers ?? {};
189
+ }
190
+ catch {
191
+ return {};
192
+ }
193
+ }
194
+ /**
195
+ * Read stdio MCP servers from Claude Code's project-scoped config (~/.claude.json).
196
+ * Looks up projects[workspace].mcpServers for the agent's workspace path.
197
+ * Returns empty object if not found or on any error.
198
+ */
199
+ readProjectScopedMcp() {
200
+ try {
201
+ const claudeJsonPath = path.join(os.homedir(), '.claude.json');
202
+ const parsed = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf-8'));
203
+ const projectServers = parsed?.projects?.[this.agentConfig.workspace]?.mcpServers;
204
+ return projectServers ?? {};
205
+ }
206
+ catch {
207
+ return {};
208
+ }
209
+ }
210
+ writeMcpConfig() {
211
+ if (this.source === 'api' && !this.agentConfig.allow_tools)
212
+ return null;
213
+ const stateDir = path.join(this.agentConfig.workspace, '.telegram-state');
214
+ const sessionDir = path.join(this.agentConfig.workspace, '.sessions', this.sessionId);
215
+ fs.mkdirSync(sessionDir, { recursive: true, mode: 0o700 });
216
+ const mcpServerPath = path.resolve(__dirname, '..', '..', 'mcp', 'server.ts');
217
+ // Merge stdio servers from Claude Code user + project configs (project overrides user).
218
+ // Skip "telegram" and "gateway" from both — gateway always generates its own config below.
219
+ const userServers = this.readUserScopedMcp();
220
+ const projectServers = this.readProjectScopedMcp();
221
+ const extraServers = {};
222
+ for (const [name, server] of Object.entries({ ...userServers, ...projectServers })) {
223
+ if (name !== 'telegram' && name !== 'gateway')
224
+ extraServers[name] = server;
225
+ }
226
+ const mcpConfig = {
227
+ mcpServers: {
228
+ ...extraServers,
229
+ // Gateway always wins — must stay last to override any accidental collision
230
+ gateway: {
231
+ command: 'bun',
232
+ args: [mcpServerPath],
233
+ env: {
234
+ TELEGRAM_BOT_TOKEN: this.agentConfig.telegram?.botToken ?? '',
235
+ TELEGRAM_STATE_DIR: stateDir,
236
+ TELEGRAM_SEND_ONLY: 'true', // ALWAYS — session subprocesses never poll
237
+ DISCORD_BOT_TOKEN: this.agentConfig.discord?.botToken ?? '',
238
+ DISCORD_STATE_DIR: path.join(this.agentConfig.workspace, '.discord-state'),
239
+ DISCORD_GUILD_ALLOWLIST: (this.agentConfig.discord?.guildAllowlist ?? []).join(','),
240
+ DISCORD_CHANNEL_ALLOWLIST: (this.agentConfig.discord?.channelAllowlist ?? []).join(','),
241
+ DISCORD_DM_POLICY: this.agentConfig.discord?.dmPolicy ?? 'disabled',
242
+ DISCORD_DM_ALLOWLIST: (this.agentConfig.discord?.dmAllowlist ?? []).join(','),
243
+ GATEWAY_AGENT_ID: this.agentConfig.id,
244
+ GATEWAY_API_URL: process.env.GATEWAY_API_URL ?? `http://127.0.0.1:${process.env.PORT ?? '3000'}`,
245
+ GATEWAY_API_KEY: this.findApiKeyForAgent(this.agentConfig.id),
246
+ GATEWAY_ORIGIN_CHANNEL: this.source,
247
+ GATEWAY_WORKSPACE_DIR: this.agentConfig.workspace,
248
+ GATEWAY_SHARED_SKILLS_DIR: path.join(os.homedir(), '.claude-gateway', 'shared-skills'),
249
+ GATEWAY_SESSION_ID: this.sessionId,
250
+ },
251
+ },
252
+ },
253
+ };
254
+ const configPath = path.join(sessionDir, 'mcp-config.json');
255
+ fs.writeFileSync(configPath, JSON.stringify(mcpConfig, null, 2), { mode: 0o600 });
256
+ const serverNames = Object.keys(mcpConfig.mcpServers);
257
+ this.logger.debug('MCP config written', { sessionId: this.sessionId, servers: serverNames });
258
+ return configPath;
259
+ }
260
+ /** Find the first API key that has access to this agent (agents: '*' or includes agentId). */
261
+ findApiKeyForAgent(agentId) {
262
+ const keys = this.gatewayConfig.gateway.api?.keys;
263
+ if (!keys?.length)
264
+ return '';
265
+ const match = keys.find(k => k.agents === '*' || (Array.isArray(k.agents) && k.agents.includes(agentId)));
266
+ return match?.key ?? '';
267
+ }
268
+ buildArgs(mcpConfigPath, model) {
269
+ const args = [
270
+ '--model', model,
271
+ '--input-format', 'stream-json',
272
+ '--output-format', 'stream-json',
273
+ '--include-partial-messages',
274
+ '--print',
275
+ '--verbose',
276
+ ];
277
+ if (mcpConfigPath) {
278
+ // NOTE: --strict-mcp-config is intentionally omitted.
279
+ // With --strict-mcp-config, Claude Code blocks all plugin MCP servers (e.g. figma).
280
+ // Without it, enabled plugins (figma, etc.) load automatically alongside --mcp-config.
281
+ args.unshift('--mcp-config', mcpConfigPath);
282
+ }
283
+ if (this.agentConfig.claude.dangerouslySkipPermissions) {
284
+ args.push('--dangerously-skip-permissions');
285
+ }
286
+ for (const flag of this.agentConfig.claude.extraFlags ?? []) {
287
+ args.push(flag);
288
+ }
289
+ return args;
290
+ // NOTE: NO --channels flag — messages arrive via stdin injection, not Telegram channels
291
+ }
292
+ static toStreamJsonTurn(text) {
293
+ return JSON.stringify({
294
+ type: 'user',
295
+ message: { role: 'user', content: [{ type: 'text', text }] },
296
+ });
297
+ }
298
+ async spawnProcess() {
299
+ const { prompt: initialPrompt, loadedAtSpawn, archivedCount, messageCountAtSpawn } = await this.buildInitialPrompt();
300
+ this.spawnContext = { loadedAtSpawn, archivedCount, messageCountAtSpawn };
301
+ const mcpConfigPath = this.writeMcpConfig();
302
+ const freshModel = this.readFreshModel();
303
+ const args = this.buildArgs(mcpConfigPath, freshModel);
304
+ const claudeBinRaw = process.env.CLAUDE_BIN ?? 'claude';
305
+ const claudeBinParts = claudeBinRaw.split(' ');
306
+ const claudeBin = claudeBinParts[0];
307
+ const allArgs = [...claudeBinParts.slice(1), ...args];
308
+ this.logger.info('Spawning session subprocess', {
309
+ sessionId: this.sessionId,
310
+ source: this.source,
311
+ });
312
+ const proc = (0, child_process_1.spawn)(claudeBin, allArgs, {
313
+ env: {
314
+ ...process.env,
315
+ CLAUDE_WORKSPACE: this.agentConfig.workspace,
316
+ TELEGRAM_BOT_TOKEN: this.agentConfig.telegram?.botToken ?? '',
317
+ GATEWAY_RESTART_SIGNAL_PATH: this.restartSignalPath,
318
+ },
319
+ cwd: this.agentConfig.workspace,
320
+ stdio: ['pipe', 'pipe', 'pipe'],
321
+ });
322
+ this.process = proc;
323
+ // Send initial prompt only for Telegram sessions.
324
+ // API sessions receive the first message directly via sendApiMessage(),
325
+ // so no activation prompt is needed and sending one would race with
326
+ // the first API turn, causing sendApiMessage to resolve with the wrong result.
327
+ if (this.source !== 'api') {
328
+ proc.stdin?.write(SessionProcess.toStreamJsonTurn(initialPrompt) + '\n');
329
+ }
330
+ // Capture stdout — emit output events + persist assistant replies
331
+ const stateDir = this.source === 'discord'
332
+ ? path.join(this.agentConfig.workspace, '.discord-state')
333
+ : path.join(this.agentConfig.workspace, '.telegram-state');
334
+ const typingDir = path.join(stateDir, 'typing');
335
+ const heartbeatPath = this.source !== 'api'
336
+ ? path.join(typingDir, `${this.chatId}.heartbeat`)
337
+ : null;
338
+ const statusPath = this.source !== 'api'
339
+ ? path.join(typingDir, `${this.chatId}.status`)
340
+ : null;
341
+ const writeStatus = (status, detail) => {
342
+ if (statusPath) {
343
+ const payload = detail
344
+ ? JSON.stringify({ status, detail })
345
+ : status;
346
+ try {
347
+ fs.writeFileSync(statusPath, payload);
348
+ }
349
+ catch { }
350
+ }
351
+ };
352
+ const CODING_TOOLS = new Set(['Write', 'Edit', 'NotebookEdit', 'MultiEdit']);
353
+ const TOOL_LABELS = {
354
+ Read: { emoji: '📖', verb: 'Reading' },
355
+ Edit: { emoji: '✏️', verb: 'Editing' },
356
+ Write: { emoji: '📝', verb: 'Writing' },
357
+ MultiEdit: { emoji: '❤️‍🔥', verb: 'Editing' },
358
+ NotebookEdit: { emoji: '📕', verb: 'Editing notebook' },
359
+ Grep: { emoji: '🔦', verb: 'Searching for' },
360
+ Glob: { emoji: '📂', verb: 'Finding files' },
361
+ Bash: { emoji: '💻', verb: 'Running' },
362
+ WebFetch: { emoji: '🌐', verb: 'Fetching' },
363
+ WebSearch: { emoji: '🔎', verb: 'Searching' },
364
+ Agent: { emoji: '🤖', verb: 'Running agent' },
365
+ Task: { emoji: '👉', verb: 'Running task' },
366
+ TodoWrite: { emoji: '📋', verb: 'Updating tasks' },
367
+ };
368
+ function shortenPath(p) {
369
+ // Keep last 2 segments for context (e.g. "src/api-router.ts" not just "api-router.ts")
370
+ const parts = p.split('/');
371
+ return parts.length > 2 ? parts.slice(-2).join('/') : parts[parts.length - 1] || p;
372
+ }
373
+ function truncateDetail(s, maxLines = 5, maxChars = 300) {
374
+ const lines = s.split('\n').filter(l => l.trim());
375
+ const trimmedByLines = lines.length > maxLines;
376
+ const kept = lines.slice(0, maxLines);
377
+ let result = kept.join('\n');
378
+ if (result.length > maxChars) {
379
+ result = result.slice(0, maxChars) + '...';
380
+ }
381
+ else if (trimmedByLines) {
382
+ result += '\n...';
383
+ }
384
+ return result;
385
+ }
386
+ function extractToolDetail(name, input) {
387
+ const label = TOOL_LABELS[name] ?? { emoji: '🔧', verb: name };
388
+ const { emoji, verb } = label;
389
+ // Build context parts based on tool type
390
+ switch (name) {
391
+ case 'Read':
392
+ case 'Edit':
393
+ case 'Write':
394
+ case 'MultiEdit':
395
+ case 'NotebookEdit': {
396
+ const file = typeof input.file_path === 'string' ? shortenPath(input.file_path) : '';
397
+ const desc = typeof input.description === 'string' ? ` — ${input.description}` : '';
398
+ return truncateDetail(`${emoji} ${verb}: ${file}${desc}`);
399
+ }
400
+ case 'Grep': {
401
+ const pattern = typeof input.pattern === 'string' ? `"${input.pattern}"` : '';
402
+ const path = typeof input.path === 'string' ? ` in ${shortenPath(input.path)}` : '';
403
+ return truncateDetail(`${emoji} ${verb}: ${pattern}${path}`);
404
+ }
405
+ case 'Glob': {
406
+ const pattern = typeof input.pattern === 'string' ? `"${input.pattern}"` : '';
407
+ return truncateDetail(`${emoji} ${verb}: ${pattern}`);
408
+ }
409
+ case 'Bash': {
410
+ const desc = typeof input.description === 'string' ? input.description : '';
411
+ const cmd = typeof input.command === 'string' ? input.command : '';
412
+ return truncateDetail(`${emoji} ${verb}: ${desc || cmd}`);
413
+ }
414
+ case 'WebFetch': {
415
+ const url = typeof input.url === 'string' ? input.url : '';
416
+ return truncateDetail(`${emoji} ${verb}: ${url}`);
417
+ }
418
+ case 'WebSearch': {
419
+ const query = typeof input.query === 'string' ? input.query : '';
420
+ return truncateDetail(`${emoji} ${verb}: "${query}"`);
421
+ }
422
+ case 'Agent':
423
+ case 'Task': {
424
+ const desc = typeof input.description === 'string' ? input.description : '';
425
+ const prompt = typeof input.prompt === 'string' ? input.prompt : '';
426
+ return truncateDetail(`${emoji} ${verb}: ${desc || prompt}`);
427
+ }
428
+ case 'TodoWrite': {
429
+ const todos = Array.isArray(input.todos) ? input.todos : [];
430
+ const active = todos.find(t => t.status === 'in_progress');
431
+ const detail = active?.content ?? `${todos.length} items`;
432
+ return truncateDetail(`${emoji} ${verb}: ${detail}`);
433
+ }
434
+ default: {
435
+ // Generic fallback: try description, then name
436
+ const desc = typeof input.description === 'string' ? input.description : '';
437
+ return truncateDetail(`${emoji} ${verb}: ${desc || '...'}`);
438
+ }
439
+ }
440
+ }
441
+ let assistantBuffer = '';
442
+ // Track partial message text to avoid double-counting when --include-partial-messages is active.
443
+ // Each partial `type: 'assistant'` event contains the FULL text so far, not a delta.
444
+ let lastPartialText = '';
445
+ // Track context from message_start events (first sub-call of each turn) for accurate context % display.
446
+ // result.usage is cumulative across all sub-calls; message_start.usage reflects a single API call's context.
447
+ let lastMessageStartContext = 0;
448
+ proc.stdout?.on('data', (data) => {
449
+ // Update heartbeat so the receiver's stalled detector knows Claude is active
450
+ if (heartbeatPath) {
451
+ try {
452
+ fs.writeFileSync(heartbeatPath, String(Date.now()));
453
+ }
454
+ catch { }
455
+ }
456
+ const lines = data.toString().split('\n');
457
+ for (const line of lines) {
458
+ if (!line.trim())
459
+ continue;
460
+ this.emit('output', line);
461
+ this.logger.debug('session output', { line });
462
+ // Try to capture assistant text for SessionStore + update status file
463
+ try {
464
+ const obj = JSON.parse(line);
465
+ // stream-json assistant message (partial or final)
466
+ if (obj.type === 'assistant' && Array.isArray(obj.message?.content)) {
467
+ // Extract full text from all text blocks in this message
468
+ let fullText = '';
469
+ for (const block of obj.message.content) {
470
+ if (block.type === 'text')
471
+ fullText += block.text;
472
+ }
473
+ const isPartial = obj.stop_reason === null || obj.stop_reason === undefined;
474
+ if (isPartial) {
475
+ // Partial message: update buffer with only the delta
476
+ // (fullText is cumulative, so delta = new portion)
477
+ if (fullText.length > lastPartialText.length) {
478
+ const delta = fullText.slice(lastPartialText.length);
479
+ if (this.queryMode) {
480
+ this._queryBuffer += delta;
481
+ }
482
+ else {
483
+ assistantBuffer += delta;
484
+ }
485
+ }
486
+ lastPartialText = fullText;
487
+ }
488
+ else {
489
+ // Final message: use full text, reset partial tracking
490
+ if (fullText.length > lastPartialText.length) {
491
+ const delta = fullText.slice(lastPartialText.length);
492
+ if (this.queryMode) {
493
+ this._queryBuffer += delta;
494
+ }
495
+ else {
496
+ assistantBuffer += delta;
497
+ }
498
+ }
499
+ lastPartialText = '';
500
+ }
501
+ if (!this.queryMode) {
502
+ // Detect tool use to write status (same as before)
503
+ const toolBlock = obj.message.content.find((b) => b.type === 'tool_use');
504
+ if (toolBlock) {
505
+ const detail = extractToolDetail(toolBlock.name ?? '', toolBlock.input ?? {});
506
+ writeStatus(CODING_TOOLS.has(toolBlock.name ?? '') ? 'coding' : 'tool', detail);
507
+ }
508
+ else if (!isPartial && obj.message.content.some((b) => b.type === 'text')) {
509
+ // Only update thinking status on final messages, not every partial
510
+ const textBlock = obj.message.content.find((b) => b.type === 'text');
511
+ const textSnippet = textBlock?.text ? truncateDetail(`🧠 ${textBlock.text}`) : undefined;
512
+ writeStatus('thinking', textSnippet);
513
+ }
514
+ }
515
+ }
516
+ // task_started / task_progress
517
+ if (obj.type === 'system' && (obj.subtype === 'task_started' || obj.subtype === 'task_progress')) {
518
+ const taskDesc = typeof obj.description === 'string' ? obj.description : '';
519
+ if (obj.subtype === 'task_started') {
520
+ writeStatus('tool', truncateDetail(`🤖 ${taskDesc}`));
521
+ }
522
+ else {
523
+ const toolName = typeof obj.last_tool_name === 'string' ? obj.last_tool_name : '';
524
+ const toolLabel = TOOL_LABELS[toolName] ?? { emoji: '🔧', verb: toolName };
525
+ writeStatus('tool', truncateDetail(`${toolLabel.emoji} ${taskDesc}`));
526
+ }
527
+ }
528
+ // rate_limit_event
529
+ if (obj.type === 'rate_limit_event') {
530
+ writeStatus('waiting', '⏳ Rate limited, retrying...');
531
+ }
532
+ // text delta (standalone, not from assistant messages)
533
+ if (obj.type === 'text') {
534
+ if (this.queryMode) {
535
+ this._queryBuffer += obj.text ?? '';
536
+ }
537
+ else {
538
+ assistantBuffer += obj.text ?? '';
539
+ }
540
+ }
541
+ // Capture context size from message_start (first sub-call of each turn)
542
+ if (obj.type === 'stream_event' && obj.event?.type === 'message_start') {
543
+ const msUsage = obj.event.message?.usage;
544
+ if (msUsage) {
545
+ lastMessageStartContext = (msUsage.input_tokens ?? 0) + (msUsage.cache_read_input_tokens ?? 0) + (msUsage.cache_creation_input_tokens ?? 0);
546
+ }
547
+ }
548
+ // result = end of turn
549
+ if (obj.type === 'result') {
550
+ lastPartialText = ''; // reset for next turn
551
+ writeStatus(obj.is_error ? 'error' : 'done');
552
+ if (this.queryMode) {
553
+ if (this._queryTimer)
554
+ clearTimeout(this._queryTimer);
555
+ if (!this._querySettled) {
556
+ this._querySettled = true;
557
+ const resolve = this._queryResolve;
558
+ this.queryMode = false;
559
+ this._queryResolve = undefined;
560
+ resolve?.(this._queryBuffer.trim());
561
+ }
562
+ this._queryBuffer = '';
563
+ assistantBuffer = '';
564
+ }
565
+ else {
566
+ if (assistantBuffer.trim()) {
567
+ const assistantMsg = { role: 'assistant', content: assistantBuffer.trim(), ts: Date.now() };
568
+ const appendAssistant = this.source !== 'api'
569
+ ? this.sessionStore.appendTelegramMessage(this.agentConfig.id, this.chatId, this.sessionId, assistantMsg, this.sessionChannel)
570
+ : this.sessionStore.appendMessage(this.agentConfig.id, this.sessionId, assistantMsg);
571
+ appendAssistant.catch(() => { });
572
+ assistantBuffer = '';
573
+ }
574
+ // Emit tokenUsage using message_start context (accurate per-call context window usage)
575
+ // rather than result.usage which is cumulative across all sub-calls in the turn.
576
+ const usage = obj.usage;
577
+ const outputTokens = usage?.output_tokens ?? 0;
578
+ const totalTokens = lastMessageStartContext + outputTokens;
579
+ if (lastMessageStartContext > 0) {
580
+ this.emit('tokenUsage', { inputTokens: lastMessageStartContext, outputTokens, totalTokens });
581
+ }
582
+ lastMessageStartContext = 0;
583
+ }
584
+ }
585
+ }
586
+ catch {
587
+ /* not JSON */
588
+ }
589
+ }
590
+ });
591
+ proc.stderr?.on('data', (data) => {
592
+ this.logger.warn('session stderr', { stderr: data.toString() });
593
+ });
594
+ proc.on('exit', (code, signal) => {
595
+ this.logger.info('session subprocess exited', {
596
+ code,
597
+ signal,
598
+ sessionId: this.sessionId,
599
+ });
600
+ this.process = null;
601
+ if (!this.stopping)
602
+ this.scheduleRestart();
603
+ });
604
+ proc.on('error', (err) => {
605
+ this.logger.error('session subprocess error', { error: err.message });
606
+ });
607
+ }
608
+ scheduleRestart() {
609
+ // Graceful self-restart requested by agent — reset counter so it doesn't
610
+ // count against MAX_RESTARTS (this is an intentional restart, not a crash).
611
+ if (this.restartRequested) {
612
+ this.restartRequested = false;
613
+ this.restartCount = 0;
614
+ }
615
+ if (this.restartCount >= MAX_RESTARTS) {
616
+ this.logger.error('Session max restarts reached', { sessionId: this.sessionId });
617
+ this.emit('failed');
618
+ return;
619
+ }
620
+ this.restartCount++;
621
+ this.logger.warn(`Scheduling session restart in ${AUTO_RESTART_DELAY_MS}ms`, {
622
+ attempt: this.restartCount,
623
+ });
624
+ setTimeout(() => {
625
+ if (!this.stopping) {
626
+ this.spawnProcess().catch(err => this.logger.error('restart failed', { error: err.message }));
627
+ }
628
+ }, AUTO_RESTART_DELAY_MS);
629
+ }
630
+ sendMessage(text) {
631
+ if (!this.process?.stdin?.writable) {
632
+ this.logger.warn('Cannot send message: subprocess not running', {
633
+ sessionId: this.sessionId,
634
+ });
635
+ return;
636
+ }
637
+ // Signal queued state so the typing loop can update the reaction immediately
638
+ if (this.source !== 'api') {
639
+ const stateSubDir = this.source === 'discord' ? '.discord-state' : '.telegram-state';
640
+ const statusPath = path.join(this.agentConfig.workspace, stateSubDir, 'typing', `${this.sessionId}.status`);
641
+ try {
642
+ fs.writeFileSync(statusPath, 'queued');
643
+ }
644
+ catch { }
645
+ }
646
+ this.process.stdin.write(SessionProcess.toStreamJsonTurn(text) + '\n');
647
+ }
648
+ query(prompt, timeoutMs = 60000) {
649
+ return new Promise((resolve, reject) => {
650
+ if (!this.process?.stdin?.writable) {
651
+ reject(new Error('Cannot query: subprocess not running'));
652
+ return;
653
+ }
654
+ this._querySettled = false;
655
+ this._queryResolve = resolve;
656
+ this._queryBuffer = '';
657
+ this.queryMode = true;
658
+ this._queryTimer = setTimeout(() => {
659
+ if (this._querySettled)
660
+ return;
661
+ this._querySettled = true;
662
+ this.queryMode = false;
663
+ this._queryResolve = undefined;
664
+ reject(new Error('query timeout'));
665
+ }, timeoutMs);
666
+ this.sendMessage(prompt);
667
+ });
668
+ }
669
+ get isProcessing() { return this._processing; }
670
+ setProcessing(active) {
671
+ if (this._processing !== active) {
672
+ this._processing = active;
673
+ this.emit('processingChange', active);
674
+ if (!active && this._pendingRestart) {
675
+ this.emit('deferredRestartReady');
676
+ }
677
+ }
678
+ }
679
+ interrupt() {
680
+ if (!this.process || this.process.killed)
681
+ return false;
682
+ if (!this._processing)
683
+ return false;
684
+ this.process.kill('SIGINT');
685
+ return true;
686
+ }
687
+ markPendingRestart() {
688
+ if (!this._processing) {
689
+ this.emit('deferredRestartReady');
690
+ }
691
+ else {
692
+ this._pendingRestart = true;
693
+ }
694
+ }
695
+ touch() {
696
+ this.lastActivityAt = Date.now();
697
+ }
698
+ isIdle(idleMs) {
699
+ return Date.now() - this.lastActivityAt > idleMs;
700
+ }
701
+ isRunning() {
702
+ return this.process !== null && !this.process.killed;
703
+ }
704
+ async stop() {
705
+ this.stopping = true;
706
+ await this.restartWatcher?.close();
707
+ this.restartWatcher = null;
708
+ try {
709
+ fs.rmSync(this.restartSignalPath, { force: true });
710
+ }
711
+ catch { }
712
+ if (!this.process)
713
+ return;
714
+ return new Promise((resolve) => {
715
+ const proc = this.process;
716
+ proc.once('exit', () => {
717
+ this.process = null;
718
+ resolve();
719
+ });
720
+ proc.kill('SIGTERM');
721
+ setTimeout(() => {
722
+ if (this.process)
723
+ proc.kill('SIGKILL');
724
+ }, 10000);
725
+ });
726
+ }
727
+ }
728
+ exports.SessionProcess = SessionProcess;
729
+ //# sourceMappingURL=process.js.map