@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,53 @@
1
+ ---
2
+ name: discord-configure
3
+ description: Configure the Discord channel — save bot token, set auto-thread, embed options.
4
+ user-invocable: true
5
+ ---
6
+
7
+ # /gateway:discord-configure — Configure Discord
8
+
9
+ Use this skill to set up the Discord channel for an agent.
10
+
11
+ ## Required setup
12
+
13
+ ### 1. Save the bot token
14
+ ```
15
+ /gateway:discord-configure token <BOT_TOKEN>
16
+ ```
17
+ Writes `DISCORD_BOT_TOKEN=<token>` to `$DISCORD_STATE_DIR/.env`.
18
+
19
+ ### 2. Set required env vars in agent config
20
+ Add to the agent's `.env` file:
21
+ ```
22
+ DISCORD_BOT_TOKEN=<your-bot-token>
23
+ DISCORD_STATE_DIR=~/.claude-gateway/agents/<id>/.discord-state
24
+ DISCORD_DM_POLICY=disabled # open | allowlist | disabled
25
+ DISCORD_GUILD_ALLOWLIST= # comma-separated guild IDs (empty = all)
26
+ DISCORD_CHANNEL_ALLOWLIST= # comma-separated channel IDs (empty = all)
27
+ DISCORD_AUTO_THREAD=false # create thread per conversation
28
+ DISCORD_USE_EMBEDS=false # use embeds for long responses
29
+ ```
30
+
31
+ ## Discord Developer Portal checklist
32
+
33
+ 1. Go to https://discord.com/developers/applications
34
+ 2. Select your application → Bot settings
35
+ 3. Enable **MESSAGE CONTENT INTENT** (privileged intent)
36
+ 4. Required bot permissions:
37
+ - Send Messages
38
+ - Read Message History
39
+ - Create Public Threads
40
+ - Send Messages in Threads
41
+ - Attach Files
42
+ - Embed Links
43
+ - Add Reactions
44
+ - Use Application Commands
45
+
46
+ ## Implementation
47
+
48
+ Parse the subcommand and act accordingly:
49
+ - `token <value>` — write `.env` file to DISCORD_STATE_DIR
50
+ - `show` — display current env vars (mask the token)
51
+ - `check` — verify the bot can connect (call discord_reply to a test channel if provided)
52
+
53
+ Confirm each action and display what was changed.
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Discord thread management — openclaw pattern from threading.ts.
3
+ * Uses local DiscordMessage interface; no top-level discord.js import.
4
+ */
5
+
6
+ import type { DiscordMessage } from './types';
7
+
8
+ export async function maybeCreateThread(
9
+ message: DiscordMessage,
10
+ autoThread: boolean,
11
+ autoArchiveDuration: number,
12
+ ): Promise<{ id: string } | null> {
13
+ if (!autoThread) return null;
14
+ if (message.channel.isThread()) return null;
15
+ if (!message.guild) return null;
16
+
17
+ const name = sanitizeThreadName(message.content);
18
+ return message.startThread({ name, autoArchiveDuration });
19
+ }
20
+
21
+ export function sanitizeThreadName(text: string): string {
22
+ return text
23
+ .replace(/<@!?\d+>/g, '')
24
+ .replace(/\s+/g, ' ')
25
+ .trim()
26
+ .slice(0, 100) || 'conversation';
27
+ }
28
+
29
+ export function buildSessionKey(
30
+ agentId: string,
31
+ channelId: string,
32
+ userId: string,
33
+ threadId?: string | null,
34
+ ): string {
35
+ const chatId = threadId ?? channelId;
36
+ return `${agentId}:discord:${userId}:${chatId}`;
37
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Discord-specific types. No discord.js imports — pure TypeScript.
3
+ */
4
+
5
+ export interface DiscordPending {
6
+ senderId: string;
7
+ channelId: string;
8
+ createdAt: number;
9
+ expiresAt: number;
10
+ replies: number;
11
+ }
12
+
13
+ export interface DiscordAccess {
14
+ dmPolicy: 'pairing' | 'allowlist' | 'disabled';
15
+ allowFrom: string[];
16
+ guildAllowlist: string[];
17
+ channelAllowlist: string[];
18
+ roleAllowlist: string[];
19
+ pending: Record<string, DiscordPending>;
20
+ }
21
+
22
+ export type DiscordGateResult =
23
+ | { action: 'deliver' }
24
+ | { action: 'drop' }
25
+ | { action: 'pair'; code: string; isResend: boolean };
26
+
27
+ export type DiscordConfig = {
28
+ botToken: string;
29
+ guildAllowlist?: string[];
30
+ channelAllowlist?: string[];
31
+ dmPolicy: 'open' | 'allowlist' | 'disabled';
32
+ dmAllowlist?: string[];
33
+ autoThread: boolean;
34
+ autoThreadArchiveMinutes: 60 | 1440 | 4320 | 10080;
35
+ maxMessageLength: number;
36
+ useEmbeds: boolean;
37
+ };
38
+
39
+ export type DiscordMessageContext = {
40
+ guildId: string | null;
41
+ channelId: string;
42
+ threadId: string | null;
43
+ userId: string;
44
+ username: string;
45
+ messageId: string;
46
+ isDM: boolean;
47
+ isThread: boolean;
48
+ };
49
+
50
+ export type DiscordAccessConfig = {
51
+ dmPolicy: 'open' | 'allowlist' | 'disabled';
52
+ dmAllowlist: string[];
53
+ guildAllowlist: string[];
54
+ channelAllowlist: string[];
55
+ roleAllowlist: string[];
56
+ };
57
+
58
+ /** Minimal interface for a Discord text-based channel send operation. */
59
+ export interface SendableChannel {
60
+ send(options: SendOptions): Promise<SentMessage>;
61
+ }
62
+
63
+ export type SendOptions = {
64
+ content?: string;
65
+ embeds?: EmbedData[];
66
+ files?: FileAttachment[];
67
+ reply?: { messageReference: string };
68
+ };
69
+
70
+ export type SentMessage = { id: string };
71
+
72
+ export type EmbedData = { description: string };
73
+
74
+ export type FileAttachment = { attachment: string; name?: string };
75
+
76
+ /** Minimal interface for a Discord message (for inbound handler). */
77
+ export interface DiscordMessage {
78
+ id: string;
79
+ content: string;
80
+ author: { id: string; username: string; bot: boolean };
81
+ system: boolean;
82
+ guild: { id: string } | null;
83
+ guildId: string | null;
84
+ channelId: string;
85
+ channel: {
86
+ isThread(): boolean;
87
+ parentId?: string | null;
88
+ };
89
+ createdTimestamp: number;
90
+ attachments: { first(): { url: string } | undefined };
91
+ client: { user: { id: string } | null };
92
+ startThread(options: { name: string; autoArchiveDuration: number }): Promise<{ id: string }>;
93
+ }
94
+
95
+ export type SlashCommandDef = {
96
+ name: string;
97
+ description: string;
98
+ options?: SlashCommandOption[];
99
+ };
100
+
101
+ export type SlashCommandOption = {
102
+ name: string;
103
+ description: string;
104
+ required: boolean;
105
+ type: 'STRING';
106
+ };
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Skill management handlers: create, delete, install.
3
+ * Pure functions operating on the filesystem — no MCP dependency.
4
+ */
5
+
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+
9
+ // Skill name validation: lowercase alphanumeric + hyphens, 1-64 chars
10
+ const VALID_NAME_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
11
+
12
+ // Reserved names that conflict with system commands
13
+ const RESERVED_NAMES = new Set([
14
+ 'help', 'sessions', 'session', 'new', 'clear', 'compact',
15
+ 'rename', 'model', 'restart', 'access', 'configure',
16
+ ]);
17
+
18
+ const MAX_SKILL_SIZE = 100 * 1024; // 100KB
19
+
20
+ function validateName(name: string): string | null {
21
+ if (!VALID_NAME_RE.test(name)) {
22
+ return `Invalid skill name "${name}". Must be lowercase alphanumeric with hyphens, 1-64 chars.`;
23
+ }
24
+ if (RESERVED_NAMES.has(name)) {
25
+ return `Skill name "${name}" is reserved. Choose a different name.`;
26
+ }
27
+ return null;
28
+ }
29
+
30
+ function getSkillDir(scope: 'workspace' | 'shared', name: string, workspaceDir: string, sharedSkillsDir: string): string {
31
+ if (scope === 'workspace') {
32
+ return path.join(workspaceDir, 'skills', name);
33
+ }
34
+ return path.join(sharedSkillsDir, name);
35
+ }
36
+
37
+ export interface CreateSkillParams {
38
+ name: string;
39
+ description: string;
40
+ content: string;
41
+ scope: 'workspace' | 'shared';
42
+ workspaceDir: string;
43
+ sharedSkillsDir: string;
44
+ }
45
+
46
+ /**
47
+ * Create a new skill by writing a SKILL.md file with proper frontmatter.
48
+ */
49
+ export async function createSkill(params: CreateSkillParams): Promise<string> {
50
+ const { name, description, content, scope, workspaceDir, sharedSkillsDir } = params;
51
+
52
+ const nameError = validateName(name);
53
+ if (nameError) throw new Error(nameError);
54
+
55
+ const skillDir = getSkillDir(scope, name, workspaceDir, sharedSkillsDir);
56
+ const skillFile = path.join(skillDir, 'SKILL.md');
57
+
58
+ if (fs.existsSync(skillFile)) {
59
+ throw new Error(`Skill "${name}" already exists at ${skillDir}. Use skill_delete first or choose a different name.`);
60
+ }
61
+
62
+ // Build SKILL.md with frontmatter
63
+ const skillMd = [
64
+ '---',
65
+ `name: ${name}`,
66
+ `description: "${description.replace(/"/g, '\\"')}"`,
67
+ '---',
68
+ '',
69
+ content,
70
+ ].join('\n');
71
+
72
+ // Atomic write: create dir, write to .tmp, rename
73
+ fs.mkdirSync(skillDir, { recursive: true });
74
+ const tmpFile = skillFile + '.tmp';
75
+ fs.writeFileSync(tmpFile, skillMd, 'utf-8');
76
+ fs.renameSync(tmpFile, skillFile);
77
+
78
+ return `Skill "${name}" created at ${skillFile} (scope: ${scope})`;
79
+ }
80
+
81
+ export interface DeleteSkillParams {
82
+ name: string;
83
+ scope: 'workspace' | 'shared';
84
+ workspaceDir: string;
85
+ sharedSkillsDir: string;
86
+ }
87
+
88
+ /**
89
+ * Delete a skill by removing its directory.
90
+ */
91
+ export async function deleteSkill(params: DeleteSkillParams): Promise<string> {
92
+ const { name, scope, workspaceDir, sharedSkillsDir } = params;
93
+
94
+ const skillDir = getSkillDir(scope, name, workspaceDir, sharedSkillsDir);
95
+ const skillFile = path.join(skillDir, 'SKILL.md');
96
+
97
+ if (!fs.existsSync(skillFile)) {
98
+ throw new Error(`Skill "${name}" not found in ${scope} (looked at ${skillDir})`);
99
+ }
100
+
101
+ fs.rmSync(skillDir, { recursive: true, force: true });
102
+
103
+ return `Skill "${name}" deleted from ${scope}`;
104
+ }
105
+
106
+ export interface InstallSkillParams {
107
+ url: string;
108
+ scope: 'workspace' | 'shared';
109
+ name?: string;
110
+ force?: boolean;
111
+ workspaceDir: string;
112
+ sharedSkillsDir: string;
113
+ }
114
+
115
+ /**
116
+ * Convert a github.com URL to raw.githubusercontent.com.
117
+ * e.g. https://github.com/user/repo/tree/main/skills/foo/SKILL.md
118
+ * -> https://raw.githubusercontent.com/user/repo/main/skills/foo/SKILL.md
119
+ */
120
+ export function toRawGitHubUrl(url: string): string {
121
+ // Already raw
122
+ if (url.includes('raw.githubusercontent.com')) return url;
123
+
124
+ // GitHub blob/tree URL pattern
125
+ const match = url.match(
126
+ /^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/(tree|blob)\/([^/]+)\/(.+)$/
127
+ );
128
+ if (match) {
129
+ const [, owner, repo, , branch, filePath] = match;
130
+ // If the URL points to a directory, append /SKILL.md
131
+ const finalPath = filePath.endsWith('SKILL.md') ? filePath : `${filePath}/SKILL.md`;
132
+ return `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${finalPath}`;
133
+ }
134
+
135
+ return url;
136
+ }
137
+
138
+ /**
139
+ * Install a skill from a URL by fetching SKILL.md and writing it locally.
140
+ */
141
+ export async function installSkill(params: InstallSkillParams): Promise<string> {
142
+ const { url, scope, force, workspaceDir, sharedSkillsDir } = params;
143
+
144
+ // Security: only allow HTTPS
145
+ if (!url.startsWith('https://')) {
146
+ throw new Error('Only HTTPS URLs are supported for security.');
147
+ }
148
+
149
+ const rawUrl = toRawGitHubUrl(url);
150
+
151
+ // Fetch content
152
+ const response = await fetch(rawUrl);
153
+ if (!response.ok) {
154
+ throw new Error(`Failed to fetch ${rawUrl}: ${response.status} ${response.statusText}`);
155
+ }
156
+
157
+ const content = await response.text();
158
+
159
+ // Size check
160
+ if (content.length > MAX_SKILL_SIZE) {
161
+ throw new Error(`SKILL.md exceeds ${MAX_SKILL_SIZE / 1024}KB limit (${(content.length / 1024).toFixed(1)}KB)`);
162
+ }
163
+
164
+ // Parse frontmatter to extract name
165
+ const { extractFrontmatter } = await import('../../../src/skills/parser');
166
+ const extracted = extractFrontmatter(content);
167
+ if (!extracted) {
168
+ throw new Error('Invalid SKILL.md: no valid YAML frontmatter found.');
169
+ }
170
+
171
+ const fm = extracted.frontmatter;
172
+ const skillName = params.name || (typeof fm.name === 'string' ? fm.name : '');
173
+ if (!skillName) {
174
+ throw new Error('Invalid SKILL.md: missing "name" in frontmatter. Use the name parameter to override.');
175
+ }
176
+
177
+ const nameError = validateName(skillName);
178
+ if (nameError) throw new Error(nameError);
179
+
180
+ const skillDir = getSkillDir(scope, skillName, workspaceDir, sharedSkillsDir);
181
+ const skillFile = path.join(skillDir, 'SKILL.md');
182
+
183
+ if (fs.existsSync(skillFile) && !force) {
184
+ throw new Error(`Skill "${skillName}" already exists. Use force: true to overwrite.`);
185
+ }
186
+
187
+ // Write skill file (atomic)
188
+ fs.mkdirSync(skillDir, { recursive: true });
189
+ const tmpFile = skillFile + '.tmp';
190
+ fs.writeFileSync(tmpFile, content, 'utf-8');
191
+ fs.renameSync(tmpFile, skillFile);
192
+
193
+ const description = typeof fm.description === 'string' ? fm.description : '(no description)';
194
+
195
+ return `Skill "${skillName}" installed from ${rawUrl} (scope: ${scope})\nDescription: ${description}`;
196
+ }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Skills tool module — implements ToolModule interface.
3
+ * Provides skill management tools: skill_create, skill_delete, skill_install.
4
+ * Tool-only module with "all-configured" visibility.
5
+ */
6
+
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import type {
10
+ ToolModule,
11
+ McpToolDefinition,
12
+ McpToolResult,
13
+ ToolVisibility,
14
+ } from '../../types';
15
+ import {
16
+ createSkill,
17
+ deleteSkill,
18
+ installSkill,
19
+ type CreateSkillParams,
20
+ type DeleteSkillParams,
21
+ type InstallSkillParams,
22
+ } from './handlers';
23
+
24
+ export class SkillsModule implements ToolModule {
25
+ id = 'skills';
26
+ toolVisibility: ToolVisibility = 'all-configured';
27
+
28
+ isEnabled(): boolean {
29
+ // Skills tools are available when workspace dir is known
30
+ return Boolean(process.env.GATEWAY_WORKSPACE_DIR);
31
+ }
32
+
33
+ getTools(): McpToolDefinition[] {
34
+ return [
35
+ {
36
+ name: 'skill_create',
37
+ description: 'Create a new skill from content. The skill will be available immediately via hot-reload.',
38
+ inputSchema: {
39
+ type: 'object',
40
+ properties: {
41
+ name: {
42
+ type: 'string',
43
+ description: 'Skill slug name (lowercase, hyphens allowed, e.g. "my-helper")',
44
+ },
45
+ description: {
46
+ type: 'string',
47
+ description: 'One-line description of what the skill does',
48
+ },
49
+ content: {
50
+ type: 'string',
51
+ description: 'Full SKILL.md body content (instructions for the agent)',
52
+ },
53
+ scope: {
54
+ type: 'string',
55
+ enum: ['workspace', 'shared'],
56
+ description: 'Where to save: "workspace" (this agent only) or "shared" (all agents). Default: workspace',
57
+ },
58
+ },
59
+ required: ['name', 'description', 'content'],
60
+ additionalProperties: false,
61
+ },
62
+ },
63
+ {
64
+ name: 'skill_delete',
65
+ description: 'Delete a skill by name.',
66
+ inputSchema: {
67
+ type: 'object',
68
+ properties: {
69
+ name: {
70
+ type: 'string',
71
+ description: 'Skill name to delete',
72
+ },
73
+ scope: {
74
+ type: 'string',
75
+ enum: ['workspace', 'shared'],
76
+ description: 'Where to look: "workspace" or "shared". Default: workspace',
77
+ },
78
+ },
79
+ required: ['name'],
80
+ additionalProperties: false,
81
+ },
82
+ },
83
+ {
84
+ name: 'skill_install',
85
+ description: 'Install a skill from a GitHub URL or raw URL pointing to a SKILL.md file.',
86
+ inputSchema: {
87
+ type: 'object',
88
+ properties: {
89
+ url: {
90
+ type: 'string',
91
+ description: 'URL to SKILL.md (GitHub URLs auto-converted to raw)',
92
+ },
93
+ scope: {
94
+ type: 'string',
95
+ enum: ['workspace', 'shared'],
96
+ description: 'Where to install: "workspace" or "shared". Default: workspace',
97
+ },
98
+ name: {
99
+ type: 'string',
100
+ description: 'Override skill name (default: parsed from frontmatter)',
101
+ },
102
+ force: {
103
+ type: 'boolean',
104
+ description: 'Overwrite if skill already exists. Default: false',
105
+ },
106
+ },
107
+ required: ['url'],
108
+ additionalProperties: false,
109
+ },
110
+ },
111
+ ];
112
+ }
113
+
114
+ async handleTool(name: string, args: Record<string, unknown>): Promise<McpToolResult> {
115
+ const workspaceDir = process.env.GATEWAY_WORKSPACE_DIR!;
116
+ const sharedSkillsDir = process.env.GATEWAY_SHARED_SKILLS_DIR ||
117
+ path.join(process.env.HOME || '/tmp', '.claude-gateway', 'shared-skills');
118
+
119
+ try {
120
+ switch (name) {
121
+ case 'skill_create': {
122
+ const params: CreateSkillParams = {
123
+ name: args.name as string,
124
+ description: args.description as string,
125
+ content: args.content as string,
126
+ scope: (args.scope as 'workspace' | 'shared') || 'workspace',
127
+ workspaceDir,
128
+ sharedSkillsDir,
129
+ };
130
+ const result = await createSkill(params);
131
+ return { content: [{ type: 'text', text: result }] };
132
+ }
133
+ case 'skill_delete': {
134
+ const params: DeleteSkillParams = {
135
+ name: args.name as string,
136
+ scope: (args.scope as 'workspace' | 'shared') || 'workspace',
137
+ workspaceDir,
138
+ sharedSkillsDir,
139
+ };
140
+ const result = await deleteSkill(params);
141
+ return { content: [{ type: 'text', text: result }] };
142
+ }
143
+ case 'skill_install': {
144
+ const params: InstallSkillParams = {
145
+ url: args.url as string,
146
+ scope: (args.scope as 'workspace' | 'shared') || 'workspace',
147
+ name: args.name as string | undefined,
148
+ force: args.force as boolean | undefined,
149
+ workspaceDir,
150
+ sharedSkillsDir,
151
+ };
152
+ const result = await installSkill(params);
153
+ return { content: [{ type: 'text', text: result }] };
154
+ }
155
+ default:
156
+ return { content: [{ type: 'text', text: `unknown tool: ${name}` }], isError: true };
157
+ }
158
+ } catch (err) {
159
+ const msg = err instanceof Error ? err.message : String(err);
160
+ return { content: [{ type: 'text', text: `${name} failed: ${msg}` }], isError: true };
161
+ }
162
+ }
163
+ }