@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,163 @@
1
+ import type { ToolModule, McpToolDefinition, McpToolResult, ToolVisibility } from '../../types';
2
+ import { createAgent, updateAgent } from './handlers';
3
+ import type { CreateAgentArgs, UpdateAgentArgs } from './handlers';
4
+
5
+ export class AgentModule implements ToolModule {
6
+ id = 'agent';
7
+ toolVisibility: ToolVisibility = 'all-configured';
8
+
9
+ isEnabled(): boolean {
10
+ return true;
11
+ }
12
+
13
+ getTools(): McpToolDefinition[] {
14
+ return [
15
+ {
16
+ name: 'agent_create',
17
+ description:
18
+ 'Create a new Claude Gateway agent with a workspace and channel configuration. The gateway hot-adds the agent without requiring a restart.',
19
+ inputSchema: {
20
+ type: 'object',
21
+ properties: {
22
+ id: {
23
+ type: 'string',
24
+ description:
25
+ 'Agent ID: starts with a letter, 2-32 chars, letters/numbers/underscores/hyphens. Stored lowercase.',
26
+ },
27
+ description: {
28
+ type: 'string',
29
+ description:
30
+ "One-paragraph description of the agent's role, personality, and capabilities.",
31
+ },
32
+ channel: {
33
+ type: 'string',
34
+ enum: ['telegram', 'discord'],
35
+ description: 'Primary channel for this agent.',
36
+ },
37
+ bot_token: {
38
+ type: 'string',
39
+ description:
40
+ 'Bot token. Telegram: 123456789:AAHfiq... Discord: MTAxMTk....AAAA.xxx.',
41
+ },
42
+ telegram_user_id: {
43
+ type: 'string',
44
+ description:
45
+ 'Telegram numeric chat_id (6–15 digits, e.g. "99000000"). ONLY pass here when channel=telegram. IMPORTANT: If you are responding to a Telegram message, use the sender chat_id from the inbound message context directly — do NOT ask the user for it. NEVER put a Discord Snowflake (17–19 digits) here.',
46
+ },
47
+ discord_user_id: {
48
+ type: 'string',
49
+ description:
50
+ 'Discord Snowflake user ID (17–19 digits, e.g. "932651528878194749"). ONLY pass here when channel=discord. IMPORTANT: If you are responding to a Discord message, use the sender user ID from the inbound message context directly — do NOT ask the user for it. NEVER put a Telegram chat_id (6–15 digits) here.',
51
+ },
52
+ dm_policy: {
53
+ type: 'string',
54
+ enum: ['open', 'allowlist', 'pairing'],
55
+ description: 'DM access policy. Default: allowlist.',
56
+ },
57
+ model: {
58
+ type: 'string',
59
+ description: 'Claude model to use. Default: claude-sonnet-4-6.',
60
+ },
61
+ signature_emoji: {
62
+ type: 'string',
63
+ description: 'Optional emoji the agent uses as a signature.',
64
+ },
65
+ agents_md: {
66
+ type: 'string',
67
+ description:
68
+ 'Full AGENTS.md content (AI-generated by caller). If omitted, a stub is created from description.',
69
+ },
70
+ soul_md: {
71
+ type: 'string',
72
+ description: 'Full SOUL.md content. If omitted, an empty stub is created.',
73
+ },
74
+ user_md: {
75
+ type: 'string',
76
+ description: 'Full USER.md content. If omitted, an empty stub is created.',
77
+ },
78
+ },
79
+ required: ['id', 'description', 'channel', 'bot_token'],
80
+ additionalProperties: false,
81
+ },
82
+ },
83
+ {
84
+ name: 'agent_update',
85
+ description:
86
+ 'Update an existing agent: add a new channel, remove a channel, or update a workspace file.',
87
+ inputSchema: {
88
+ type: 'object',
89
+ properties: {
90
+ id: {
91
+ type: 'string',
92
+ description: 'Agent ID to update.',
93
+ },
94
+ action: {
95
+ type: 'string',
96
+ enum: ['add_channel', 'remove_channel', 'update_workspace_file'],
97
+ description: 'Operation to perform.',
98
+ },
99
+ channel: {
100
+ type: 'string',
101
+ enum: ['telegram', 'discord'],
102
+ description: 'Channel (required for add_channel and remove_channel).',
103
+ },
104
+ bot_token: {
105
+ type: 'string',
106
+ description: 'Bot token (required for add_channel).',
107
+ },
108
+ telegram_user_id: {
109
+ type: 'string',
110
+ description:
111
+ 'Telegram numeric chat_id (6–15 digits). add_channel only, when channel=telegram. IMPORTANT: If responding to a Telegram message, use the sender chat_id directly — do NOT ask the user for it. NEVER put a Discord Snowflake (17–19 digits) here.',
112
+ },
113
+ discord_user_id: {
114
+ type: 'string',
115
+ description:
116
+ 'Discord Snowflake user ID (17–19 digits). add_channel only, when channel=discord. IMPORTANT: If responding to a Discord message, use the sender user ID directly — do NOT ask the user for it. NEVER put a Telegram chat_id (6–15 digits) here.',
117
+ },
118
+ dm_policy: {
119
+ type: 'string',
120
+ enum: ['open', 'allowlist', 'pairing'],
121
+ description: 'DM policy for the new channel (add_channel only). Default: allowlist.',
122
+ },
123
+ filename: {
124
+ type: 'string',
125
+ description:
126
+ 'Workspace file to write (update_workspace_file only). E.g. "SOUL.md". No path traversal allowed.',
127
+ },
128
+ content: {
129
+ type: 'string',
130
+ description: 'New file content (update_workspace_file only).',
131
+ },
132
+ },
133
+ required: ['id', 'action'],
134
+ additionalProperties: false,
135
+ },
136
+ },
137
+ ];
138
+ }
139
+
140
+ async handleTool(name: string, args: Record<string, unknown>): Promise<McpToolResult> {
141
+ try {
142
+ let result: string;
143
+
144
+ if (name === 'agent_create') {
145
+ result = await createAgent(args as unknown as CreateAgentArgs);
146
+ } else if (name === 'agent_update') {
147
+ result = await updateAgent(args as unknown as UpdateAgentArgs);
148
+ } else {
149
+ return {
150
+ content: [{ type: 'text', text: `Unknown tool: ${name}` }],
151
+ isError: true,
152
+ };
153
+ }
154
+
155
+ return { content: [{ type: 'text', text: result }] };
156
+ } catch (err) {
157
+ return {
158
+ content: [{ type: 'text', text: (err as Error).message }],
159
+ isError: true,
160
+ };
161
+ }
162
+ }
163
+ }
@@ -0,0 +1,321 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import type { ToolModule, McpToolDefinition, McpToolResult, ToolVisibility } from '../../types';
4
+
5
+ let _reqId = 1;
6
+
7
+ export class BrowserModule implements ToolModule {
8
+ id = 'browser';
9
+ toolVisibility: ToolVisibility = 'all-configured';
10
+
11
+ isEnabled(): boolean {
12
+ return process.env.GETPOD_BROWSER_DISABLED !== 'true';
13
+ }
14
+
15
+ getTools(): McpToolDefinition[] {
16
+ return browserToolDefs;
17
+ }
18
+
19
+ async handleTool(name: string, args: Record<string, unknown>): Promise<McpToolResult> {
20
+ const sessionId = process.env.GATEWAY_SESSION_ID;
21
+ const agentId = process.env.GATEWAY_AGENT_ID;
22
+ if (sessionId) args = { ...args, session_id: sessionId };
23
+ if (agentId) args = { ...args, agent_id: agentId };
24
+
25
+ const result = await callGetpodBrowser(name, args);
26
+
27
+ if (name === 'browser_screenshot' && !result.isError) {
28
+ // getpod-browser returns {type:"image", data: base64, mimeType:"image/jpeg"}.
29
+ // Decode and save to /tmp so callers can attach the file path directly.
30
+ const block = result.content[0] as Record<string, string> | undefined;
31
+ const b64 = block?.['data'] ?? '';
32
+ const mime = block?.['mimeType'] ?? 'image/jpeg';
33
+ if (b64) {
34
+ const ext = mime.includes('png') ? 'png' : 'jpg';
35
+ const sid = (args.session_id as string | undefined) ?? 'default';
36
+ const filePath = path.join('/tmp', `browser_shot_${sid}.${ext}`);
37
+ fs.writeFileSync(filePath, Buffer.from(b64, 'base64'));
38
+ return { content: [{ type: 'text', text: filePath }] };
39
+ }
40
+ }
41
+
42
+ return result;
43
+ }
44
+ }
45
+
46
+ async function readFirstDataLine(res: Response): Promise<string | undefined> {
47
+ if (!res.body) return undefined;
48
+ const reader = res.body.getReader();
49
+ const decoder = new TextDecoder();
50
+ let buf = '';
51
+ try {
52
+ while (true) {
53
+ const { done, value } = await reader.read();
54
+ if (done) break;
55
+ buf += decoder.decode(value, { stream: true });
56
+ // Scan all complete \n-terminated lines; return the first data: line found
57
+ let newlineIdx: number;
58
+ while ((newlineIdx = buf.indexOf('\n')) !== -1) {
59
+ const line = buf.slice(0, newlineIdx);
60
+ buf = buf.slice(newlineIdx + 1);
61
+ if (line.startsWith('data: ')) return line;
62
+ }
63
+ }
64
+ } finally {
65
+ reader.cancel();
66
+ }
67
+ return buf.startsWith('data: ') ? buf : undefined;
68
+ }
69
+
70
+ async function callGetpodBrowser(
71
+ toolName: string,
72
+ args: Record<string, unknown>,
73
+ ): Promise<McpToolResult> {
74
+ const id = _reqId++;
75
+ const body = JSON.stringify({
76
+ jsonrpc: '2.0',
77
+ id,
78
+ method: 'tools/call',
79
+ params: { name: toolName, arguments: args },
80
+ });
81
+
82
+ const baseUrl = process.env.GETPOD_BROWSER_URL ?? 'http://127.0.0.1:10880';
83
+ const apiKey = process.env.GETPOD_BROWSER_API_KEY ?? '';
84
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' };
85
+ if (apiKey) {
86
+ headers['Authorization'] = `Bearer ${apiKey}`;
87
+ }
88
+ let res: Response;
89
+ try {
90
+ res = await fetch(`${baseUrl}/mcp`, {
91
+ method: 'POST',
92
+ headers,
93
+ body,
94
+ });
95
+ } catch (err) {
96
+ return {
97
+ content: [{ type: 'text', text: `getpod-browser unavailable: ${(err as Error).message}` }],
98
+ isError: true,
99
+ };
100
+ }
101
+
102
+ const dataLine = await readFirstDataLine(res);
103
+ if (!dataLine) {
104
+ return {
105
+ content: [{ type: 'text', text: 'empty response from getpod-browser' }],
106
+ isError: true,
107
+ };
108
+ }
109
+
110
+ let rpc: {
111
+ result?: { content: Array<Record<string, string>>; isError?: boolean };
112
+ error?: { message: string };
113
+ };
114
+ try {
115
+ rpc = JSON.parse(dataLine.slice('data: '.length));
116
+ } catch {
117
+ return {
118
+ content: [{ type: 'text', text: 'invalid JSON from getpod-browser' }],
119
+ isError: true,
120
+ };
121
+ }
122
+
123
+ if (rpc.error) {
124
+ return { content: [{ type: 'text', text: rpc.error.message }], isError: true };
125
+ }
126
+ if (!rpc.result) {
127
+ return { content: [{ type: 'text', text: 'no result in response' }], isError: true };
128
+ }
129
+
130
+ return {
131
+ content: rpc.result.content as Array<{ type: 'text'; text: string }>,
132
+ isError: rpc.result.isError,
133
+ };
134
+ }
135
+
136
+ const browserToolDefs: McpToolDefinition[] = [
137
+ {
138
+ name: 'browser_create_session',
139
+ description:
140
+ 'Create or resume a browser session. Returns stream_url and status. ' +
141
+ 'IMPORTANT: After creating a session, always share the stream_url with the user so they can open the browser in their client.',
142
+ inputSchema: {
143
+ type: 'object',
144
+ properties: {
145
+ session_id: { type: 'string', description: 'Unique session identifier' },
146
+ idle_timeout_seconds: {
147
+ type: 'integer',
148
+ description: 'Idle timeout in seconds (0 = no timeout)',
149
+ },
150
+ },
151
+ },
152
+ },
153
+ {
154
+ name: 'browser_close_session',
155
+ description: 'Close a browser session, killing the process and removing session data.',
156
+ inputSchema: {
157
+ type: 'object',
158
+ properties: { session_id: { type: 'string' } },
159
+ },
160
+ },
161
+ {
162
+ name: 'browser_get_stream_url',
163
+ description: 'Get the WebSocket stream URL for an active session (for frontend live view).',
164
+ inputSchema: {
165
+ type: 'object',
166
+ properties: { session_id: { type: 'string' } },
167
+ },
168
+ },
169
+ {
170
+ name: 'browser_navigate',
171
+ description: 'Navigate to a URL. If tab_id is provided, navigates that specific tab; otherwise navigates the active tab.',
172
+ inputSchema: {
173
+ type: 'object',
174
+ properties: {
175
+ session_id: { type: 'string' },
176
+ url: { type: 'string' },
177
+ tab_id: {
178
+ type: 'string',
179
+ description: 'Tab ID to navigate (optional, from browser_tabs); omit to use the active tab',
180
+ },
181
+ wait: {
182
+ type: 'string',
183
+ description: 'Wait condition: load, domcontentloaded, networkidle',
184
+ },
185
+ },
186
+ required: ['url'],
187
+ },
188
+ },
189
+ {
190
+ name: 'browser_snapshot',
191
+ description: 'Return the accessibility tree of the current page.',
192
+ inputSchema: {
193
+ type: 'object',
194
+ properties: {
195
+ session_id: { type: 'string' },
196
+ interactive_only: { type: 'boolean' },
197
+ },
198
+ },
199
+ },
200
+ {
201
+ name: 'browser_click',
202
+ description: 'Click an element by accessibility ref or CSS selector.',
203
+ inputSchema: {
204
+ type: 'object',
205
+ properties: { session_id: { type: 'string' }, ref: { type: 'string' } },
206
+ required: ['ref'],
207
+ },
208
+ },
209
+ {
210
+ name: 'browser_fill',
211
+ description: 'Fill an input element with a value.',
212
+ inputSchema: {
213
+ type: 'object',
214
+ properties: {
215
+ session_id: { type: 'string' },
216
+ ref: { type: 'string' },
217
+ value: { type: 'string' },
218
+ },
219
+ required: ['ref', 'value'],
220
+ },
221
+ },
222
+ {
223
+ name: 'browser_type',
224
+ description: 'Type text into the currently focused element.',
225
+ inputSchema: {
226
+ type: 'object',
227
+ properties: { session_id: { type: 'string' }, text: { type: 'string' } },
228
+ required: ['text'],
229
+ },
230
+ },
231
+ {
232
+ name: 'browser_evaluate',
233
+ description: 'Evaluate JavaScript in the browser and return the result.',
234
+ inputSchema: {
235
+ type: 'object',
236
+ properties: { session_id: { type: 'string' }, script: { type: 'string' } },
237
+ required: ['script'],
238
+ },
239
+ },
240
+ {
241
+ name: 'browser_scroll',
242
+ description: 'Scroll the page at (x, y) by (deltaX, deltaY).',
243
+ inputSchema: {
244
+ type: 'object',
245
+ properties: {
246
+ session_id: { type: 'string' },
247
+ x: { type: 'integer' },
248
+ y: { type: 'integer' },
249
+ deltaX: { type: 'integer' },
250
+ deltaY: { type: 'integer' },
251
+ },
252
+ required: ['x', 'y', 'deltaX', 'deltaY'],
253
+ },
254
+ },
255
+ {
256
+ name: 'browser_wait',
257
+ description: 'Wait for a condition: element selector, networkidle, or URL pattern.',
258
+ inputSchema: {
259
+ type: 'object',
260
+ properties: { session_id: { type: 'string' }, condition: { type: 'string' } },
261
+ required: ['condition'],
262
+ },
263
+ },
264
+ {
265
+ name: 'browser_get_text',
266
+ description: 'Get text content of an element matching the selector.',
267
+ inputSchema: {
268
+ type: 'object',
269
+ properties: { session_id: { type: 'string' }, selector: { type: 'string' } },
270
+ required: ['selector'],
271
+ },
272
+ },
273
+ {
274
+ name: 'browser_new_tab',
275
+ description: 'Open a new browser tab, optionally navigating to a URL. Returns tab_id.',
276
+ inputSchema: {
277
+ type: 'object',
278
+ properties: { session_id: { type: 'string' }, url: { type: 'string' } },
279
+ },
280
+ },
281
+ {
282
+ name: 'browser_close_tab',
283
+ description: 'Close a browser tab by tab_id.',
284
+ inputSchema: {
285
+ type: 'object',
286
+ properties: { session_id: { type: 'string' }, tab_id: { type: 'string' } },
287
+ required: ['tab_id'],
288
+ },
289
+ },
290
+ {
291
+ name: 'browser_tabs',
292
+ description: 'List all open browser tabs.',
293
+ inputSchema: {
294
+ type: 'object',
295
+ properties: { session_id: { type: 'string' } },
296
+ },
297
+ },
298
+ {
299
+ name: 'browser_screenshot',
300
+ description:
301
+ 'Capture the current viewport as JPEG. Returns the absolute file path of the saved image (ready to attach to Telegram).',
302
+ inputSchema: {
303
+ type: 'object',
304
+ properties: { session_id: { type: 'string' } },
305
+ },
306
+ },
307
+ {
308
+ name: 'browser_navigate_tab',
309
+ description: 'Navigate a specific tab (by tab_id) to a URL.',
310
+ inputSchema: {
311
+ type: 'object',
312
+ properties: {
313
+ session_id: { type: 'string' },
314
+ tab_id: { type: 'string', description: 'Tab ID returned by browser_tabs or browser_new_tab' },
315
+ url: { type: 'string' },
316
+ wait: { type: 'string', description: 'Wait condition: load, domcontentloaded, networkidle' },
317
+ },
318
+ required: ['tab_id', 'url'],
319
+ },
320
+ },
321
+ ];
@@ -0,0 +1,89 @@
1
+ ---
2
+ name: open-browser
3
+ description: "Open a URL in the agent's persistent browser session via MCP browser tools"
4
+ user-invocable: true
5
+ ---
6
+
7
+ # open-browser
8
+
9
+ When user says "เปิด X", "navigate to X", "open X in browser", "switch to tab X", etc.
10
+
11
+ ## Rules
12
+
13
+ - Use ONLY `mcp__gateway__browser_*` tools — never Python, never CDP direct, never filesystem
14
+ - NEVER use `wait: "networkidle"` — it hangs on SPAs. Omit `wait` entirely (fire-and-forget)
15
+ - Always check tab state before navigating — tab IDs change between sessions
16
+ - Do NOT pass `session_id` — it is injected automatically from agent context
17
+
18
+ ## Fast Open Flow
19
+
20
+ ### Step 1 — Create/resume session
21
+
22
+ Call `mcp__gateway__browser_create_session` with NO arguments (session_id is auto-injected).
23
+
24
+ Result contains `stream_url` — use it to build the browser URL for the user.
25
+
26
+ ### Step 2 — Send browser URL to user IMMEDIATELY (no waiting)
27
+
28
+ Reply on Telegram right away with the stream_url so the user can open the browser.
29
+
30
+ ### Step 3 — Check current tabs
31
+
32
+ Call `mcp__gateway__browser_tabs` — returns list of `{tab_id, url, title}`.
33
+
34
+ Use this to:
35
+ - Know which tab is active / which tab to navigate
36
+ - Decide whether to open a new tab or reuse existing one
37
+
38
+ ### Step 4 — Navigate (fire-and-forget)
39
+
40
+ **Option A — Navigate active tab** (no tab_id):
41
+ ```
42
+ browser_navigate(url)
43
+ ```
44
+
45
+ **Option B — Navigate specific tab**:
46
+ ```
47
+ browser_navigate(url, tab_id="t1")
48
+ ```
49
+
50
+ **Option C — Open in new tab**:
51
+ ```
52
+ browser_new_tab() → returns tab_id
53
+ browser_navigate(url, tab_id=<new_tab_id>)
54
+ ```
55
+
56
+ If `browser_navigate` or `browser_navigate_tab` returns error like `tab "t99" not found (available: [t1, t2])` → call `browser_tabs` again and pick correct tab_id.
57
+
58
+ ### Step 5 — Screenshot and confirm
59
+
60
+ Call `mcp__gateway__browser_screenshot` — result is absolute file path.
61
+
62
+ Send to Telegram with the screenshot attached.
63
+
64
+ ## Multi-tab Management
65
+
66
+ - `browser_tabs` — list all tabs with tab_id, url, title
67
+ - `browser_new_tab()` — open new blank tab, returns tab_id
68
+ - `browser_navigate(url, tab_id)` — navigate specific tab (optional tab_id)
69
+ - `browser_close_tab(tab_id)` — close tab
70
+
71
+ ## Available MCP tools (complete list)
72
+
73
+ - `browser_create_session` — create/resume session, returns stream_url
74
+ - `browser_close_session` — close session
75
+ - `browser_get_stream_url` — get stream URL for active session
76
+ - `browser_navigate` — navigate to URL; optional tab_id to target specific tab
77
+ - `browser_navigate_tab` — navigate specific tab by tab_id (legacy, prefer browser_navigate with tab_id)
78
+ - `browser_snapshot` — accessibility tree of current page
79
+ - `browser_click` — click element by ref or CSS selector
80
+ - `browser_fill` — fill input element
81
+ - `browser_type` — type text into focused element
82
+ - `browser_evaluate` — run JavaScript in browser
83
+ - `browser_scroll` — scroll page
84
+ - `browser_wait` — wait for element/networkidle/URL
85
+ - `browser_get_text` — get text content of element
86
+ - `browser_new_tab` — open new tab, returns tab_id
87
+ - `browser_close_tab` — close tab by tab_id
88
+ - `browser_tabs` — list all open tabs with tab_id + url + title
89
+ - `browser_screenshot` — capture viewport as JPEG, returns absolute file path
@@ -0,0 +1,65 @@
1
+ /**
2
+ * HTTP client for gateway cron REST API.
3
+ */
4
+
5
+ export class CronClient {
6
+ private readonly baseUrl: string;
7
+ private readonly agentId: string;
8
+ private readonly apiKey: string;
9
+
10
+ constructor(apiUrl: string, agentId: string, apiKey?: string) {
11
+ this.baseUrl = apiUrl.replace(/\/$/, '');
12
+ this.agentId = agentId;
13
+ this.apiKey = apiKey ?? '';
14
+ }
15
+
16
+ private url(path: string, query?: Record<string, string>): string {
17
+ const base = `${this.baseUrl}/api/v1/crons${path}`;
18
+ if (!query) return base;
19
+ const qs = new URLSearchParams(query).toString();
20
+ return `${base}?${qs}`;
21
+ }
22
+
23
+ private async request(method: string, path: string, body?: unknown, query?: Record<string, string>): Promise<unknown> {
24
+ const headers: Record<string, string> = {};
25
+ if (body) headers['Content-Type'] = 'application/json';
26
+ if (this.apiKey) headers['X-Api-Key'] = this.apiKey;
27
+
28
+ const res = await fetch(this.url(path, query), {
29
+ method,
30
+ headers,
31
+ body: body ? JSON.stringify(body) : undefined,
32
+ });
33
+
34
+ if (!res.ok) {
35
+ const text = await res.text().catch(() => '');
36
+ throw new Error(`API ${method} ${path} failed: HTTP ${res.status} ${text}`);
37
+ }
38
+
39
+ const contentType = res.headers.get('content-type') ?? '';
40
+ if (contentType.includes('application/json')) {
41
+ return res.json();
42
+ }
43
+ return res.text();
44
+ }
45
+
46
+ async list(): Promise<unknown> {
47
+ return this.request('GET', '', undefined, { agent: this.agentId });
48
+ }
49
+
50
+ async create(params: Record<string, unknown>): Promise<unknown> {
51
+ return this.request('POST', '', { ...params, agentId: this.agentId });
52
+ }
53
+
54
+ async delete(jobId: string): Promise<void> {
55
+ await this.request('DELETE', `/${jobId}`);
56
+ }
57
+
58
+ async run(jobId: string): Promise<unknown> {
59
+ return this.request('POST', `/${jobId}/run`);
60
+ }
61
+
62
+ async getRuns(jobId: string): Promise<unknown> {
63
+ return this.request('GET', `/${jobId}/runs`);
64
+ }
65
+ }