@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.
- package/README.md +791 -0
- package/config.template.json +87 -0
- package/dist/agent/context-isolation.d.ts +28 -0
- package/dist/agent/context-isolation.d.ts.map +1 -0
- package/dist/agent/context-isolation.js +80 -0
- package/dist/agent/context-isolation.js.map +1 -0
- package/dist/agent/runner.d.ts +211 -0
- package/dist/agent/runner.d.ts.map +1 -0
- package/dist/agent/runner.js +1802 -0
- package/dist/agent/runner.js.map +1 -0
- package/dist/agent/workspace-loader.d.ts +31 -0
- package/dist/agent/workspace-loader.d.ts.map +1 -0
- package/dist/agent/workspace-loader.js +207 -0
- package/dist/agent/workspace-loader.js.map +1 -0
- package/dist/api/auth.d.ts +25 -0
- package/dist/api/auth.d.ts.map +1 -0
- package/dist/api/auth.js +77 -0
- package/dist/api/auth.js.map +1 -0
- package/dist/api/cron-router.d.ts +22 -0
- package/dist/api/cron-router.d.ts.map +1 -0
- package/dist/api/cron-router.js +197 -0
- package/dist/api/cron-router.js.map +1 -0
- package/dist/api/gateway-router.d.ts +52 -0
- package/dist/api/gateway-router.d.ts.map +1 -0
- package/dist/api/gateway-router.js +215 -0
- package/dist/api/gateway-router.js.map +1 -0
- package/dist/api/router.d.ts +5 -0
- package/dist/api/router.d.ts.map +1 -0
- package/dist/api/router.js +1098 -0
- package/dist/api/router.js.map +1 -0
- package/dist/api/skills-router.d.ts +5 -0
- package/dist/api/skills-router.d.ts.map +1 -0
- package/dist/api/skills-router.js +424 -0
- package/dist/api/skills-router.js.map +1 -0
- package/dist/api/workspace-router.d.ts +4 -0
- package/dist/api/workspace-router.d.ts.map +1 -0
- package/dist/api/workspace-router.js +149 -0
- package/dist/api/workspace-router.js.map +1 -0
- package/dist/config/loader.d.ts +16 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +235 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/migrator.d.ts +70 -0
- package/dist/config/migrator.d.ts.map +1 -0
- package/dist/config/migrator.js +343 -0
- package/dist/config/migrator.js.map +1 -0
- package/dist/config/watcher.d.ts +30 -0
- package/dist/config/watcher.d.ts.map +1 -0
- package/dist/config/watcher.js +170 -0
- package/dist/config/watcher.js.map +1 -0
- package/dist/cron/manager.d.ts +48 -0
- package/dist/cron/manager.d.ts.map +1 -0
- package/dist/cron/manager.js +580 -0
- package/dist/cron/manager.js.map +1 -0
- package/dist/cron/scheduler.d.ts +50 -0
- package/dist/cron/scheduler.d.ts.map +1 -0
- package/dist/cron/scheduler.js +231 -0
- package/dist/cron/scheduler.js.map +1 -0
- package/dist/discord/receiver.d.ts +17 -0
- package/dist/discord/receiver.d.ts.map +1 -0
- package/dist/discord/receiver.js +108 -0
- package/dist/discord/receiver.js.map +1 -0
- package/dist/heartbeat/history.d.ts +24 -0
- package/dist/heartbeat/history.d.ts.map +1 -0
- package/dist/heartbeat/history.js +50 -0
- package/dist/heartbeat/history.js.map +1 -0
- package/dist/heartbeat/parser.d.ts +26 -0
- package/dist/heartbeat/parser.d.ts.map +1 -0
- package/dist/heartbeat/parser.js +283 -0
- package/dist/heartbeat/parser.js.map +1 -0
- package/dist/history/cleanup.d.ts +21 -0
- package/dist/history/cleanup.d.ts.map +1 -0
- package/dist/history/cleanup.js +171 -0
- package/dist/history/cleanup.js.map +1 -0
- package/dist/history/db.d.ts +27 -0
- package/dist/history/db.d.ts.map +1 -0
- package/dist/history/db.js +323 -0
- package/dist/history/db.js.map +1 -0
- package/dist/history/media-store.d.ts +27 -0
- package/dist/history/media-store.d.ts.map +1 -0
- package/dist/history/media-store.js +158 -0
- package/dist/history/media-store.js.map +1 -0
- package/dist/history/types.d.ts +62 -0
- package/dist/history/types.d.ts.map +1 -0
- package/dist/history/types.js +3 -0
- package/dist/history/types.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +483 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +3 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +81 -0
- package/dist/logger.js.map +1 -0
- package/dist/memory/manager.d.ts +45 -0
- package/dist/memory/manager.d.ts.map +1 -0
- package/dist/memory/manager.js +178 -0
- package/dist/memory/manager.js.map +1 -0
- package/dist/session/compactor.d.ts +22 -0
- package/dist/session/compactor.d.ts.map +1 -0
- package/dist/session/compactor.js +151 -0
- package/dist/session/compactor.js.map +1 -0
- package/dist/session/process.d.ts +76 -0
- package/dist/session/process.d.ts.map +1 -0
- package/dist/session/process.js +729 -0
- package/dist/session/process.js.map +1 -0
- package/dist/session/store.d.ts +102 -0
- package/dist/session/store.d.ts.map +1 -0
- package/dist/session/store.js +533 -0
- package/dist/session/store.js.map +1 -0
- package/dist/skills/index.d.ts +9 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +15 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/skills/invoker.d.ts +16 -0
- package/dist/skills/invoker.d.ts.map +1 -0
- package/dist/skills/invoker.js +36 -0
- package/dist/skills/invoker.js.map +1 -0
- package/dist/skills/loader.d.ts +23 -0
- package/dist/skills/loader.d.ts.map +1 -0
- package/dist/skills/loader.js +183 -0
- package/dist/skills/loader.js.map +1 -0
- package/dist/skills/parser.d.ts +55 -0
- package/dist/skills/parser.d.ts.map +1 -0
- package/dist/skills/parser.js +178 -0
- package/dist/skills/parser.js.map +1 -0
- package/dist/skills/sync.d.ts +10 -0
- package/dist/skills/sync.d.ts.map +1 -0
- package/dist/skills/sync.js +130 -0
- package/dist/skills/sync.js.map +1 -0
- package/dist/skills/watcher.d.ts +16 -0
- package/dist/skills/watcher.d.ts.map +1 -0
- package/dist/skills/watcher.js +25 -0
- package/dist/skills/watcher.js.map +1 -0
- package/dist/telegram/markdown.d.ts +25 -0
- package/dist/telegram/markdown.d.ts.map +1 -0
- package/dist/telegram/markdown.js +237 -0
- package/dist/telegram/markdown.js.map +1 -0
- package/dist/telegram/receiver.d.ts +17 -0
- package/dist/telegram/receiver.d.ts.map +1 -0
- package/dist/telegram/receiver.js +104 -0
- package/dist/telegram/receiver.js.map +1 -0
- package/dist/types.d.ts +224 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/web-ui.d.ts +6 -0
- package/dist/ui/web-ui.d.ts.map +1 -0
- package/dist/ui/web-ui.js +227 -0
- package/dist/ui/web-ui.js.map +1 -0
- package/dist/watch/factory.d.ts +26 -0
- package/dist/watch/factory.d.ts.map +1 -0
- package/dist/watch/factory.js +43 -0
- package/dist/watch/factory.js.map +1 -0
- package/dist/webhook/manager.d.ts +34 -0
- package/dist/webhook/manager.d.ts.map +1 -0
- package/dist/webhook/manager.js +120 -0
- package/dist/webhook/manager.js.map +1 -0
- package/mcp/bun.lock +264 -0
- package/mcp/channel-manager.ts +115 -0
- package/mcp/package.json +15 -0
- package/mcp/router.ts +69 -0
- package/mcp/server.ts +129 -0
- package/mcp/tools/agent/handlers.ts +578 -0
- package/mcp/tools/agent/module.ts +163 -0
- package/mcp/tools/browser/module.ts +321 -0
- package/mcp/tools/browser/skills/open-browser/SKILL.md +89 -0
- package/mcp/tools/cron/client.ts +65 -0
- package/mcp/tools/cron/module.ts +158 -0
- package/mcp/tools/cron/skills/cron/SKILL.md +46 -0
- package/mcp/tools/discord/access.ts +197 -0
- package/mcp/tools/discord/client.ts +31 -0
- package/mcp/tools/discord/commands.ts +48 -0
- package/mcp/tools/discord/inbound.ts +53 -0
- package/mcp/tools/discord/module.ts +442 -0
- package/mcp/tools/discord/outbound.ts +90 -0
- package/mcp/tools/discord/receiver-server.ts +58 -0
- package/mcp/tools/discord/skills/access/SKILL.md +86 -0
- package/mcp/tools/discord/skills/configure/SKILL.md +53 -0
- package/mcp/tools/discord/threading.ts +37 -0
- package/mcp/tools/discord/types.ts +106 -0
- package/mcp/tools/skills/handlers.ts +196 -0
- package/mcp/tools/skills/module.ts +163 -0
- package/mcp/tools/telegram/module.ts +454 -0
- package/mcp/tools/telegram/pure.ts +224 -0
- package/mcp/tools/telegram/receiver-server.ts +1656 -0
- package/mcp/tools/telegram/skills/access/SKILL.md +169 -0
- package/mcp/tools/telegram/skills/configure/SKILL.md +118 -0
- package/mcp/tools/telegram/typing.ts +327 -0
- package/mcp/types.ts +128 -0
- package/package.json +62 -0
- package/resource/claude_gateway.svg +112 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cron tool module — implements ToolModule interface.
|
|
3
|
+
* Provides cron job management tools via the gateway REST API.
|
|
4
|
+
* Not a chat channel — tool-only module with "all-configured" visibility.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import type {
|
|
9
|
+
ToolModule,
|
|
10
|
+
McpToolDefinition,
|
|
11
|
+
McpToolResult,
|
|
12
|
+
ToolVisibility,
|
|
13
|
+
} from '../../types';
|
|
14
|
+
import { CronClient } from './client';
|
|
15
|
+
|
|
16
|
+
export class CronModule implements ToolModule {
|
|
17
|
+
id = 'cron';
|
|
18
|
+
toolVisibility: ToolVisibility = 'all-configured';
|
|
19
|
+
skillsDir = path.join(__dirname, 'skills');
|
|
20
|
+
|
|
21
|
+
private client: CronClient | null = null;
|
|
22
|
+
|
|
23
|
+
isEnabled(): boolean {
|
|
24
|
+
return Boolean(process.env.GATEWAY_API_URL && process.env.GATEWAY_AGENT_ID);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private getClient(): CronClient {
|
|
28
|
+
if (!this.client) {
|
|
29
|
+
const apiUrl = process.env.GATEWAY_API_URL!;
|
|
30
|
+
const agentId = process.env.GATEWAY_AGENT_ID!;
|
|
31
|
+
const apiKey = process.env.GATEWAY_API_KEY;
|
|
32
|
+
this.client = new CronClient(apiUrl, agentId, apiKey);
|
|
33
|
+
}
|
|
34
|
+
return this.client;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
getTools(): McpToolDefinition[] {
|
|
38
|
+
return [
|
|
39
|
+
{
|
|
40
|
+
name: 'cron_list',
|
|
41
|
+
description: 'List scheduled cron jobs for this agent',
|
|
42
|
+
inputSchema: {
|
|
43
|
+
type: 'object',
|
|
44
|
+
properties: {},
|
|
45
|
+
additionalProperties: false,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'cron_create',
|
|
50
|
+
description: 'Create a new cron job',
|
|
51
|
+
inputSchema: {
|
|
52
|
+
type: 'object',
|
|
53
|
+
properties: {
|
|
54
|
+
name: { type: 'string', description: 'Job name' },
|
|
55
|
+
schedule: { type: 'string', description: '5-field cron expression' },
|
|
56
|
+
type: { type: 'string', enum: ['command', 'agent'], description: 'Job type' },
|
|
57
|
+
command: { type: 'string', description: 'Shell command (type=command)' },
|
|
58
|
+
prompt: { type: 'string', description: 'Agent prompt (type=agent)' },
|
|
59
|
+
telegram: { type: 'string', description: 'Telegram chat_id for response' },
|
|
60
|
+
discord: { type: 'string', description: 'Discord channel_id for agent response delivery' },
|
|
61
|
+
timeout_ms: { type: 'number', description: 'Timeout in milliseconds' },
|
|
62
|
+
scheduleKind: {
|
|
63
|
+
type: 'string',
|
|
64
|
+
enum: ['cron', 'at'],
|
|
65
|
+
description: 'Schedule type: "cron" for recurring (default), "at" for one-shot at a specific ISO datetime',
|
|
66
|
+
},
|
|
67
|
+
scheduleAt: {
|
|
68
|
+
type: 'string',
|
|
69
|
+
description: 'ISO 8601 datetime for one-shot execution (required when scheduleKind=at)',
|
|
70
|
+
},
|
|
71
|
+
deleteAfterRun: {
|
|
72
|
+
type: 'boolean',
|
|
73
|
+
description: 'Delete the job after it runs once. Defaults to true for scheduleKind=at, false for cron',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
required: ['name', 'type'],
|
|
77
|
+
additionalProperties: false,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'cron_delete',
|
|
82
|
+
description: 'Delete a cron job by ID',
|
|
83
|
+
inputSchema: {
|
|
84
|
+
type: 'object',
|
|
85
|
+
properties: {
|
|
86
|
+
job_id: { type: 'string', description: 'Job ID to delete' },
|
|
87
|
+
},
|
|
88
|
+
required: ['job_id'],
|
|
89
|
+
additionalProperties: false,
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'cron_run',
|
|
94
|
+
description: 'Run a cron job immediately',
|
|
95
|
+
inputSchema: {
|
|
96
|
+
type: 'object',
|
|
97
|
+
properties: {
|
|
98
|
+
job_id: { type: 'string', description: 'Job ID to run' },
|
|
99
|
+
},
|
|
100
|
+
required: ['job_id'],
|
|
101
|
+
additionalProperties: false,
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'cron_get_runs',
|
|
106
|
+
description: 'Get run history for a cron job',
|
|
107
|
+
inputSchema: {
|
|
108
|
+
type: 'object',
|
|
109
|
+
properties: {
|
|
110
|
+
job_id: { type: 'string', description: 'Job ID' },
|
|
111
|
+
},
|
|
112
|
+
required: ['job_id'],
|
|
113
|
+
additionalProperties: false,
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async handleTool(name: string, args: Record<string, unknown>): Promise<McpToolResult> {
|
|
120
|
+
const client = this.getClient();
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
switch (name) {
|
|
124
|
+
case 'cron_list': {
|
|
125
|
+
const jobs = await client.list();
|
|
126
|
+
return { content: [{ type: 'text', text: JSON.stringify(jobs, null, 2) }] };
|
|
127
|
+
}
|
|
128
|
+
case 'cron_create': {
|
|
129
|
+
// Fix 3: Default deleteAfterRun=true for at-type one-shot jobs so they are
|
|
130
|
+
// automatically cleaned up after firing, preventing re-fire on gateway restart.
|
|
131
|
+
const scheduleKind = (args.scheduleKind as string | undefined) ?? 'cron';
|
|
132
|
+
const deleteAfterRun = args.deleteAfterRun !== undefined
|
|
133
|
+
? args.deleteAfterRun
|
|
134
|
+
: scheduleKind === 'at';
|
|
135
|
+
const job = await client.create({ ...args, scheduleKind, deleteAfterRun });
|
|
136
|
+
return { content: [{ type: 'text', text: JSON.stringify(job, null, 2) }] };
|
|
137
|
+
}
|
|
138
|
+
case 'cron_delete': {
|
|
139
|
+
await client.delete(args.job_id as string);
|
|
140
|
+
return { content: [{ type: 'text', text: `deleted job ${args.job_id}` }] };
|
|
141
|
+
}
|
|
142
|
+
case 'cron_run': {
|
|
143
|
+
const run = await client.run(args.job_id as string);
|
|
144
|
+
return { content: [{ type: 'text', text: JSON.stringify(run, null, 2) }] };
|
|
145
|
+
}
|
|
146
|
+
case 'cron_get_runs': {
|
|
147
|
+
const runs = await client.getRuns(args.job_id as string);
|
|
148
|
+
return { content: [{ type: 'text', text: JSON.stringify(runs, null, 2) }] };
|
|
149
|
+
}
|
|
150
|
+
default:
|
|
151
|
+
return { content: [{ type: 'text', text: `unknown tool: ${name}` }], isError: true };
|
|
152
|
+
}
|
|
153
|
+
} catch (err) {
|
|
154
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
155
|
+
return { content: [{ type: 'text', text: `${name} failed: ${msg}` }], isError: true };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cron
|
|
3
|
+
description: Manage scheduled cron jobs for this agent — list, create, delete, run, and view run history. Use when the user asks to schedule tasks, set up recurring jobs, or check cron status.
|
|
4
|
+
user-invocable: true
|
|
5
|
+
allowed-tools:
|
|
6
|
+
- cron_list
|
|
7
|
+
- cron_create
|
|
8
|
+
- cron_delete
|
|
9
|
+
- cron_run
|
|
10
|
+
- cron_get_runs
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# /cron — Cron Job Management
|
|
14
|
+
|
|
15
|
+
Manage scheduled jobs for this agent via the gateway cron API.
|
|
16
|
+
|
|
17
|
+
Arguments passed: `$ARGUMENTS`
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Available commands
|
|
22
|
+
|
|
23
|
+
### No args or "list" — List all cron jobs
|
|
24
|
+
|
|
25
|
+
Use `cron_list` to show all scheduled jobs. Format the output as a table.
|
|
26
|
+
|
|
27
|
+
### "create" — Create a new cron job
|
|
28
|
+
|
|
29
|
+
Use `cron_create` with the required parameters:
|
|
30
|
+
- `name`: descriptive job name
|
|
31
|
+
- `schedule`: 5-field cron expression (e.g. `0 9 * * *` for daily at 9am)
|
|
32
|
+
- `type`: `command` (shell) or `agent` (Claude prompt)
|
|
33
|
+
- For `command` type: provide `command` parameter
|
|
34
|
+
- For `agent` type: provide `prompt` parameter, optionally `telegram` (chat_id for response)
|
|
35
|
+
|
|
36
|
+
### "delete <job_id>" — Delete a cron job
|
|
37
|
+
|
|
38
|
+
Use `cron_delete` with `job_id`.
|
|
39
|
+
|
|
40
|
+
### "run <job_id>" — Run a job immediately
|
|
41
|
+
|
|
42
|
+
Use `cron_run` with `job_id`.
|
|
43
|
+
|
|
44
|
+
### "runs <job_id>" — View run history
|
|
45
|
+
|
|
46
|
+
Use `cron_get_runs` with `job_id`. Format as a table showing status, duration, and timestamps.
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord access control — openclaw pattern from allow-list.ts + dm-command-auth.ts.
|
|
3
|
+
* Pure logic, no discord.js dependency.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import { randomBytes } from 'crypto';
|
|
9
|
+
import type { DiscordAccessConfig, DiscordMessageContext, DiscordAccess, DiscordGateResult } from './types';
|
|
10
|
+
export type { DiscordAccessConfig, DiscordAccess } from './types';
|
|
11
|
+
|
|
12
|
+
export type AccessResult = { allowed: boolean; reason?: string };
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// File-based access (new — pairing flow)
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
export function defaultAccess(): DiscordAccess {
|
|
19
|
+
return {
|
|
20
|
+
dmPolicy: 'pairing',
|
|
21
|
+
allowFrom: [],
|
|
22
|
+
guildAllowlist: [],
|
|
23
|
+
channelAllowlist: [],
|
|
24
|
+
roleAllowlist: [],
|
|
25
|
+
pending: {},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function loadAccess(stateDir: string): DiscordAccess {
|
|
30
|
+
const accessFile = path.join(stateDir, 'access.json');
|
|
31
|
+
try {
|
|
32
|
+
const raw = fs.readFileSync(accessFile, 'utf8');
|
|
33
|
+
const parsed = JSON.parse(raw) as Partial<DiscordAccess>;
|
|
34
|
+
return {
|
|
35
|
+
dmPolicy: parsed.dmPolicy ?? 'pairing',
|
|
36
|
+
allowFrom: parsed.allowFrom ?? [],
|
|
37
|
+
guildAllowlist: parsed.guildAllowlist ?? [],
|
|
38
|
+
channelAllowlist: parsed.channelAllowlist ?? [],
|
|
39
|
+
roleAllowlist: parsed.roleAllowlist ?? [],
|
|
40
|
+
pending: parsed.pending ?? {},
|
|
41
|
+
};
|
|
42
|
+
} catch (err) {
|
|
43
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
44
|
+
// Seed from env vars for backward compat
|
|
45
|
+
return {
|
|
46
|
+
dmPolicy: (process.env.DISCORD_DM_POLICY as DiscordAccess['dmPolicy']) ?? 'pairing',
|
|
47
|
+
allowFrom: process.env.DISCORD_DM_ALLOWLIST
|
|
48
|
+
? process.env.DISCORD_DM_ALLOWLIST.split(',').filter(Boolean)
|
|
49
|
+
: [],
|
|
50
|
+
guildAllowlist: process.env.DISCORD_GUILD_ALLOWLIST
|
|
51
|
+
? process.env.DISCORD_GUILD_ALLOWLIST.split(',').filter(Boolean)
|
|
52
|
+
: [],
|
|
53
|
+
channelAllowlist: process.env.DISCORD_CHANNEL_ALLOWLIST
|
|
54
|
+
? process.env.DISCORD_CHANNEL_ALLOWLIST.split(',').filter(Boolean)
|
|
55
|
+
: [],
|
|
56
|
+
roleAllowlist: process.env.DISCORD_ROLE_ALLOWLIST
|
|
57
|
+
? process.env.DISCORD_ROLE_ALLOWLIST.split(',').filter(Boolean)
|
|
58
|
+
: [],
|
|
59
|
+
pending: {},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
fs.renameSync(accessFile, `${accessFile}.corrupt-${Date.now()}`);
|
|
64
|
+
} catch {}
|
|
65
|
+
return defaultAccess();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function saveAccess(stateDir: string, access: DiscordAccess): void {
|
|
70
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
71
|
+
const accessFile = path.join(stateDir, 'access.json');
|
|
72
|
+
const tmp = accessFile + '.tmp';
|
|
73
|
+
fs.writeFileSync(tmp, JSON.stringify(access, null, 2) + '\n', { mode: 0o600 });
|
|
74
|
+
fs.renameSync(tmp, accessFile);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function pruneExpired(access: DiscordAccess, now?: number): boolean {
|
|
78
|
+
const ts = now ?? Date.now();
|
|
79
|
+
let changed = false;
|
|
80
|
+
for (const [code, p] of Object.entries(access.pending)) {
|
|
81
|
+
if (p.expiresAt < ts) {
|
|
82
|
+
delete access.pending[code];
|
|
83
|
+
changed = true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return changed;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function gate(
|
|
90
|
+
access: DiscordAccess,
|
|
91
|
+
context: DiscordMessageContext,
|
|
92
|
+
saveAccessFn: (a: DiscordAccess) => void,
|
|
93
|
+
generateCode: () => string = () => randomBytes(3).toString('hex'),
|
|
94
|
+
now?: number,
|
|
95
|
+
): DiscordGateResult {
|
|
96
|
+
const ts = now ?? Date.now();
|
|
97
|
+
const pruned = pruneExpired(access, ts);
|
|
98
|
+
if (pruned) saveAccessFn(access);
|
|
99
|
+
|
|
100
|
+
const { isDM, userId, guildId, channelId } = context;
|
|
101
|
+
|
|
102
|
+
if (!isDM) {
|
|
103
|
+
if (access.guildAllowlist.length > 0 && guildId) {
|
|
104
|
+
if (!access.guildAllowlist.includes(guildId)) return { action: 'drop' };
|
|
105
|
+
}
|
|
106
|
+
if (access.channelAllowlist.length > 0) {
|
|
107
|
+
if (!access.channelAllowlist.includes(channelId)) return { action: 'drop' };
|
|
108
|
+
}
|
|
109
|
+
return { action: 'deliver' };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// DM message
|
|
113
|
+
if (access.dmPolicy === 'disabled') return { action: 'drop' };
|
|
114
|
+
|
|
115
|
+
if (access.allowFrom.includes(userId)) return { action: 'deliver' };
|
|
116
|
+
if (access.dmPolicy === 'allowlist') return { action: 'drop' };
|
|
117
|
+
|
|
118
|
+
// pairing mode
|
|
119
|
+
for (const [code, p] of Object.entries(access.pending)) {
|
|
120
|
+
if (p.senderId === userId) {
|
|
121
|
+
if (p.replies >= 2) return { action: 'drop' };
|
|
122
|
+
p.replies++;
|
|
123
|
+
saveAccessFn(access);
|
|
124
|
+
return { action: 'pair', code, isResend: true };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (Object.keys(access.pending).length >= 3) return { action: 'drop' };
|
|
128
|
+
|
|
129
|
+
const code = generateCode();
|
|
130
|
+
access.pending[code] = {
|
|
131
|
+
senderId: userId,
|
|
132
|
+
channelId,
|
|
133
|
+
createdAt: ts,
|
|
134
|
+
expiresAt: ts + 60 * 60 * 1000,
|
|
135
|
+
replies: 1,
|
|
136
|
+
};
|
|
137
|
+
saveAccessFn(access);
|
|
138
|
+
return { action: 'pair', code, isResend: false };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// Env-var based access (backward compat)
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
export function checkAccess(
|
|
146
|
+
config: DiscordAccessConfig,
|
|
147
|
+
context: DiscordMessageContext,
|
|
148
|
+
memberRoles?: string[],
|
|
149
|
+
): AccessResult {
|
|
150
|
+
if (context.isDM) {
|
|
151
|
+
if (config.dmPolicy === 'disabled') return { allowed: false, reason: 'DM disabled' };
|
|
152
|
+
if (config.dmPolicy === 'allowlist') {
|
|
153
|
+
if (
|
|
154
|
+
!config.dmAllowlist.includes(context.userId) &&
|
|
155
|
+
!config.dmAllowlist.includes('*')
|
|
156
|
+
) {
|
|
157
|
+
return { allowed: false, reason: 'user not in DM allowlist' };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return { allowed: true };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (config.guildAllowlist.length && context.guildId) {
|
|
164
|
+
if (!config.guildAllowlist.includes(context.guildId)) {
|
|
165
|
+
return { allowed: false, reason: 'guild not allowed' };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (config.channelAllowlist.length) {
|
|
170
|
+
if (!config.channelAllowlist.includes(context.channelId)) {
|
|
171
|
+
return { allowed: false, reason: 'channel not allowed' };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (config.roleAllowlist.length && memberRoles) {
|
|
176
|
+
const hasRole = memberRoles.some(r => config.roleAllowlist.includes(r));
|
|
177
|
+
if (!hasRole) return { allowed: false, reason: 'missing required role' };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return { allowed: true };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function buildAccessConfig(env: NodeJS.ProcessEnv = process.env): DiscordAccessConfig {
|
|
184
|
+
return {
|
|
185
|
+
dmPolicy: (env.DISCORD_DM_POLICY as DiscordAccessConfig['dmPolicy']) ?? 'disabled',
|
|
186
|
+
dmAllowlist: env.DISCORD_DM_ALLOWLIST ? env.DISCORD_DM_ALLOWLIST.split(',').filter(Boolean) : [],
|
|
187
|
+
guildAllowlist: env.DISCORD_GUILD_ALLOWLIST
|
|
188
|
+
? env.DISCORD_GUILD_ALLOWLIST.split(',').filter(Boolean)
|
|
189
|
+
: [],
|
|
190
|
+
channelAllowlist: env.DISCORD_CHANNEL_ALLOWLIST
|
|
191
|
+
? env.DISCORD_CHANNEL_ALLOWLIST.split(',').filter(Boolean)
|
|
192
|
+
: [],
|
|
193
|
+
roleAllowlist: env.DISCORD_ROLE_ALLOWLIST
|
|
194
|
+
? env.DISCORD_ROLE_ALLOWLIST.split(',').filter(Boolean)
|
|
195
|
+
: [],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord.js client factory.
|
|
3
|
+
* Dynamic import keeps discord.js out of the module graph at load time.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export async function createDiscordClient(token: string): Promise<any> {
|
|
7
|
+
// @ts-ignore — discord.js in mcp/node_modules
|
|
8
|
+
const { Client, GatewayIntentBits, Partials } = await import('discord.js');
|
|
9
|
+
|
|
10
|
+
const client = new Client({
|
|
11
|
+
intents: [
|
|
12
|
+
GatewayIntentBits.Guilds,
|
|
13
|
+
GatewayIntentBits.GuildMessages,
|
|
14
|
+
GatewayIntentBits.DirectMessages,
|
|
15
|
+
GatewayIntentBits.MessageContent,
|
|
16
|
+
GatewayIntentBits.GuildMessageReactions,
|
|
17
|
+
],
|
|
18
|
+
partials: [
|
|
19
|
+
Partials.Channel,
|
|
20
|
+
Partials.Message,
|
|
21
|
+
],
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
await new Promise<void>((resolve, reject) => {
|
|
25
|
+
client.once('ready', () => resolve());
|
|
26
|
+
client.once('error', reject);
|
|
27
|
+
client.login(token).catch(reject);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return client;
|
|
31
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord slash command definitions — openclaw pattern from native-command.ts.
|
|
3
|
+
* SLASH_COMMANDS is plain JSON; discord.js builders only used in registerCommands().
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { SlashCommandDef } from './types';
|
|
7
|
+
|
|
8
|
+
export const SLASH_COMMANDS: SlashCommandDef[] = [
|
|
9
|
+
{
|
|
10
|
+
name: 'ask',
|
|
11
|
+
description: 'Ask the agent a question',
|
|
12
|
+
options: [{ name: 'question', description: 'Your question', required: true, type: 'STRING' }],
|
|
13
|
+
},
|
|
14
|
+
{ name: 'session', description: 'Show current session info' },
|
|
15
|
+
{
|
|
16
|
+
name: 'new',
|
|
17
|
+
description: 'Start a new conversation',
|
|
18
|
+
options: [{ name: 'name', description: 'Session name', required: false, type: 'STRING' }],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'model',
|
|
22
|
+
description: 'Show or switch AI model',
|
|
23
|
+
options: [{ name: 'name', description: 'Model name', required: false, type: 'STRING' }],
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export async function registerCommands(
|
|
28
|
+
client: any,
|
|
29
|
+
token: string,
|
|
30
|
+
): Promise<void> {
|
|
31
|
+
// @ts-ignore — discord.js in mcp/node_modules
|
|
32
|
+
const { REST, Routes, SlashCommandBuilder } = await import('discord.js');
|
|
33
|
+
const rest = new REST().setToken(token);
|
|
34
|
+
|
|
35
|
+
const bodies = SLASH_COMMANDS.map(cmd => {
|
|
36
|
+
const builder = new SlashCommandBuilder()
|
|
37
|
+
.setName(cmd.name)
|
|
38
|
+
.setDescription(cmd.description);
|
|
39
|
+
for (const opt of cmd.options ?? []) {
|
|
40
|
+
builder.addStringOption((o: any) =>
|
|
41
|
+
o.setName(opt.name).setDescription(opt.description).setRequired(opt.required),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return builder.toJSON();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
await rest.put(Routes.applicationCommands(client.user!.id), { body: bodies });
|
|
48
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord inbound message handler — openclaw pattern from message-handler.ts.
|
|
3
|
+
* Preflight validation → context building → handler dispatch.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { InboundMessage, InboundMessageHandler } from '../../types';
|
|
7
|
+
import type { DiscordConfig, DiscordMessage, DiscordMessageContext } from './types';
|
|
8
|
+
import type { DiscordAccessConfig } from './access';
|
|
9
|
+
import { checkAccess } from './access';
|
|
10
|
+
|
|
11
|
+
export function createMessageHandler(
|
|
12
|
+
agentId: string,
|
|
13
|
+
handler: InboundMessageHandler,
|
|
14
|
+
config: DiscordConfig,
|
|
15
|
+
accessConfig: DiscordAccessConfig,
|
|
16
|
+
) {
|
|
17
|
+
return async (message: DiscordMessage): Promise<void> => {
|
|
18
|
+
if (message.author.bot) return;
|
|
19
|
+
if (message.system) return;
|
|
20
|
+
|
|
21
|
+
const isDM = !message.guild;
|
|
22
|
+
const isThread = message.channel.isThread();
|
|
23
|
+
|
|
24
|
+
const context: DiscordMessageContext = {
|
|
25
|
+
guildId: message.guildId,
|
|
26
|
+
channelId: message.channelId,
|
|
27
|
+
threadId: isThread ? message.channelId : null,
|
|
28
|
+
userId: message.author.id,
|
|
29
|
+
username: message.author.username,
|
|
30
|
+
messageId: message.id,
|
|
31
|
+
isDM,
|
|
32
|
+
isThread,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const result = checkAccess(accessConfig, context);
|
|
36
|
+
if (!result.allowed) return;
|
|
37
|
+
|
|
38
|
+
const inbound: InboundMessage = {
|
|
39
|
+
channel: 'discord',
|
|
40
|
+
accountId: message.client.user?.id ?? 'discord',
|
|
41
|
+
senderId: message.author.id,
|
|
42
|
+
chatId: isThread ? message.channelId : message.channelId,
|
|
43
|
+
chatType: isDM ? 'direct' : 'group',
|
|
44
|
+
text: message.content,
|
|
45
|
+
messageId: message.id,
|
|
46
|
+
threadId: isThread ? message.channelId : undefined,
|
|
47
|
+
attachmentFileId: message.attachments.first()?.url,
|
|
48
|
+
ts: message.createdTimestamp,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
await handler(inbound);
|
|
52
|
+
};
|
|
53
|
+
}
|