@0xmaxma/claude-gateway 1.1.9 → 1.2.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 (67) hide show
  1. package/README.md +61 -1
  2. package/dist/agent/builtin-commands.d.ts +10 -0
  3. package/dist/agent/builtin-commands.d.ts.map +1 -0
  4. package/dist/agent/builtin-commands.js +38 -0
  5. package/dist/agent/builtin-commands.js.map +1 -0
  6. package/dist/agent/runner.d.ts +7 -5
  7. package/dist/agent/runner.d.ts.map +1 -1
  8. package/dist/agent/runner.js +56 -30
  9. package/dist/agent/runner.js.map +1 -1
  10. package/dist/agent/workspace-loader.d.ts.map +1 -1
  11. package/dist/agent/workspace-loader.js +9 -4
  12. package/dist/agent/workspace-loader.js.map +1 -1
  13. package/dist/api/apps-router.d.ts +7 -0
  14. package/dist/api/apps-router.d.ts.map +1 -0
  15. package/dist/api/apps-router.js +242 -0
  16. package/dist/api/apps-router.js.map +1 -0
  17. package/dist/api/gateway-router.d.ts +17 -1
  18. package/dist/api/gateway-router.d.ts.map +1 -1
  19. package/dist/api/gateway-router.js +171 -2
  20. package/dist/api/gateway-router.js.map +1 -1
  21. package/dist/api/router.d.ts.map +1 -1
  22. package/dist/api/router.js +385 -4
  23. package/dist/api/router.js.map +1 -1
  24. package/dist/apps/agent-manager.d.ts +76 -0
  25. package/dist/apps/agent-manager.d.ts.map +1 -0
  26. package/dist/apps/agent-manager.js +311 -0
  27. package/dist/apps/agent-manager.js.map +1 -0
  28. package/dist/apps/compose-generator.d.ts +108 -0
  29. package/dist/apps/compose-generator.d.ts.map +1 -0
  30. package/dist/apps/compose-generator.js +687 -0
  31. package/dist/apps/compose-generator.js.map +1 -0
  32. package/dist/apps/installer.d.ts +101 -0
  33. package/dist/apps/installer.d.ts.map +1 -0
  34. package/dist/apps/installer.js +898 -0
  35. package/dist/apps/installer.js.map +1 -0
  36. package/dist/apps/registry-client.d.ts +37 -0
  37. package/dist/apps/registry-client.d.ts.map +1 -0
  38. package/dist/apps/registry-client.js +106 -0
  39. package/dist/apps/registry-client.js.map +1 -0
  40. package/dist/apps/registry.d.ts +54 -0
  41. package/dist/apps/registry.d.ts.map +1 -0
  42. package/dist/apps/registry.js +182 -0
  43. package/dist/apps/registry.js.map +1 -0
  44. package/dist/apps/socket-server.d.ts +46 -0
  45. package/dist/apps/socket-server.d.ts.map +1 -0
  46. package/dist/apps/socket-server.js +297 -0
  47. package/dist/apps/socket-server.js.map +1 -0
  48. package/dist/config/watcher.d.ts +1 -0
  49. package/dist/config/watcher.d.ts.map +1 -1
  50. package/dist/config/watcher.js +55 -2
  51. package/dist/config/watcher.js.map +1 -1
  52. package/dist/index.js +170 -3
  53. package/dist/index.js.map +1 -1
  54. package/dist/session/process.d.ts.map +1 -1
  55. package/dist/session/process.js +47 -6
  56. package/dist/session/process.js.map +1 -1
  57. package/dist/types.d.ts +6 -0
  58. package/dist/types.d.ts.map +1 -1
  59. package/mcp/server.ts +2 -0
  60. package/mcp/tools/apps/client.ts +78 -0
  61. package/mcp/tools/apps/module.ts +211 -0
  62. package/mcp/tools/apps/skills/app-status/SKILL.md +48 -0
  63. package/mcp/tools/apps/skills/create-app-yaml/SKILL.md +94 -0
  64. package/mcp/tools/apps/skills/install-app/SKILL.md +102 -0
  65. package/mcp/tools/apps/skills/list-apps/SKILL.md +34 -0
  66. package/mcp/tools/telegram/receiver-server.ts +36 -2
  67. package/package.json +1 -1
@@ -0,0 +1,78 @@
1
+ /**
2
+ * HTTP client for the gateway apps REST API.
3
+ */
4
+
5
+ export class AppsClient {
6
+ private readonly baseUrl: string;
7
+ private readonly apiKey: string;
8
+
9
+ constructor(apiUrl: string, apiKey?: string) {
10
+ this.baseUrl = apiUrl.replace(/\/$/, '');
11
+ this.apiKey = apiKey ?? '';
12
+ }
13
+
14
+ private url(p: string): string {
15
+ return `${this.baseUrl}/api/v1/apps${p}`;
16
+ }
17
+
18
+ private async request(method: string, p: string, body?: unknown): Promise<unknown> {
19
+ const headers: Record<string, string> = {};
20
+ if (body) headers['Content-Type'] = 'application/json';
21
+ if (this.apiKey) headers['Authorization'] = `Bearer ${this.apiKey}`;
22
+
23
+ const res = await fetch(this.url(p), {
24
+ method,
25
+ headers,
26
+ body: body ? JSON.stringify(body) : undefined,
27
+ });
28
+
29
+ if (!res.ok) {
30
+ const text = await res.text().catch(() => '');
31
+ throw new Error(`Apps API ${method} ${p} failed: HTTP ${res.status} ${text}`);
32
+ }
33
+
34
+ const ct = res.headers.get('content-type') ?? '';
35
+ if (ct.includes('application/json')) return res.json();
36
+ return res.text();
37
+ }
38
+
39
+ async listRegistry(): Promise<unknown> {
40
+ return this.request('GET', '/registry');
41
+ }
42
+
43
+ async getRegistry(name: string): Promise<unknown> {
44
+ return this.request('GET', `/registry/${encodeURIComponent(name)}`);
45
+ }
46
+
47
+ async listApps(): Promise<unknown> {
48
+ return this.request('GET', '');
49
+ }
50
+
51
+ async getApp(name: string): Promise<unknown> {
52
+ return this.request('GET', `/${encodeURIComponent(name)}`);
53
+ }
54
+
55
+ async getVersion(name: string): Promise<unknown> {
56
+ return this.request('GET', `/${encodeURIComponent(name)}/version`);
57
+ }
58
+
59
+ async install(params: Record<string, unknown>): Promise<unknown> {
60
+ return this.request('POST', '/install', params);
61
+ }
62
+
63
+ async pollJob(jobId: string): Promise<unknown> {
64
+ return this.request('GET', `/jobs/${encodeURIComponent(jobId)}`);
65
+ }
66
+
67
+ async uninstall(name: string): Promise<unknown> {
68
+ return this.request('DELETE', `/${encodeURIComponent(name)}`);
69
+ }
70
+
71
+ async update(name: string): Promise<unknown> {
72
+ return this.request('POST', `/${encodeURIComponent(name)}/update`);
73
+ }
74
+
75
+ async startStop(name: string, action: 'start' | 'stop' | 'restart'): Promise<unknown> {
76
+ return this.request('POST', `/${encodeURIComponent(name)}/${action}`);
77
+ }
78
+ }
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Apps tool module — install, list, and manage app store apps via gateway REST API.
3
+ */
4
+
5
+ import * as path from 'path';
6
+ import type {
7
+ ToolModule,
8
+ McpToolDefinition,
9
+ McpToolResult,
10
+ ToolVisibility,
11
+ } from '../../types';
12
+ import { AppsClient } from './client';
13
+
14
+ export class AppsModule implements ToolModule {
15
+ id = 'apps';
16
+ toolVisibility: ToolVisibility = 'all-configured';
17
+ skillsDir = path.join(__dirname, 'skills');
18
+
19
+ private client: AppsClient | null = null;
20
+
21
+ isEnabled(): boolean {
22
+ return Boolean(process.env.GATEWAY_API_URL);
23
+ }
24
+
25
+ private getClient(): AppsClient {
26
+ if (!this.client) {
27
+ const apiUrl = process.env.GATEWAY_API_URL!;
28
+ const apiKey = process.env.GATEWAY_API_KEY;
29
+ this.client = new AppsClient(apiUrl, apiKey);
30
+ }
31
+ return this.client;
32
+ }
33
+
34
+ getTools(): McpToolDefinition[] {
35
+ return [
36
+ {
37
+ name: 'browse_registry',
38
+ description: 'Browse the community app registry. Omit name to list all apps; provide name to get versions for a specific app.',
39
+ inputSchema: {
40
+ type: 'object',
41
+ properties: {
42
+ name: { type: 'string', description: 'App name to look up. Omit to list all.' },
43
+ },
44
+ additionalProperties: false,
45
+ },
46
+ },
47
+ {
48
+ name: 'install_app',
49
+ description: 'Install an app from the registry or a GitHub URL. Returns a jobId to poll with poll_install_job.',
50
+ inputSchema: {
51
+ type: 'object',
52
+ properties: {
53
+ registry_app: { type: 'string', description: 'Registry app name (e.g. "getpod-manager")' },
54
+ version: { type: 'string', description: 'Registry version to install (default: latest)' },
55
+ github_url: { type: 'string', description: 'GitHub repo URL (requires commit)' },
56
+ commit: { type: 'string', description: '40-char hex commit hash (required with github_url)' },
57
+ local_path: { type: 'string', description: 'Local app path within ~/.claude-gateway/apps/' },
58
+ env_vars: {
59
+ type: 'object',
60
+ description: 'Environment variables / secrets to inject',
61
+ additionalProperties: { type: 'string' },
62
+ },
63
+ },
64
+ additionalProperties: false,
65
+ },
66
+ },
67
+ {
68
+ name: 'poll_install_job',
69
+ description: 'Poll the status of an app install job. Returns status (pending/running/completed/failed), logs, and result on completion.',
70
+ inputSchema: {
71
+ type: 'object',
72
+ properties: {
73
+ job_id: { type: 'string', description: 'Job ID returned by install_app' },
74
+ },
75
+ required: ['job_id'],
76
+ additionalProperties: false,
77
+ },
78
+ },
79
+ {
80
+ name: 'list_apps',
81
+ description: 'List all installed apps with their status and proxy URLs.',
82
+ inputSchema: {
83
+ type: 'object',
84
+ properties: {},
85
+ additionalProperties: false,
86
+ },
87
+ },
88
+ {
89
+ name: 'app_status',
90
+ description: 'Get detailed status and version info for an installed app.',
91
+ inputSchema: {
92
+ type: 'object',
93
+ properties: {
94
+ name: { type: 'string', description: 'App name' },
95
+ },
96
+ required: ['name'],
97
+ additionalProperties: false,
98
+ },
99
+ },
100
+ {
101
+ name: 'update_app',
102
+ description: 'Update a registry-installed app to its latest version. Returns a jobId.',
103
+ inputSchema: {
104
+ type: 'object',
105
+ properties: {
106
+ name: { type: 'string', description: 'App name to update' },
107
+ },
108
+ required: ['name'],
109
+ additionalProperties: false,
110
+ },
111
+ },
112
+ {
113
+ name: 'uninstall_app',
114
+ description: 'Uninstall an app — stops containers, removes images, deletes files.',
115
+ inputSchema: {
116
+ type: 'object',
117
+ properties: {
118
+ name: { type: 'string', description: 'App name to uninstall' },
119
+ },
120
+ required: ['name'],
121
+ additionalProperties: false,
122
+ },
123
+ },
124
+ {
125
+ name: 'start_stop_app',
126
+ description: 'Start, stop, or restart an installed app.',
127
+ inputSchema: {
128
+ type: 'object',
129
+ properties: {
130
+ name: { type: 'string', description: 'App name' },
131
+ action: { type: 'string', enum: ['start', 'stop', 'restart'], description: 'Action to perform' },
132
+ },
133
+ required: ['name', 'action'],
134
+ additionalProperties: false,
135
+ },
136
+ },
137
+ ];
138
+ }
139
+
140
+ async handleTool(name: string, args: Record<string, unknown>): Promise<McpToolResult> {
141
+ const client = this.getClient();
142
+
143
+ switch (name) {
144
+ case 'browse_registry': {
145
+ const appName = args['name'] as string | undefined;
146
+ const data = appName
147
+ ? await client.getRegistry(appName)
148
+ : await client.listRegistry();
149
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
150
+ }
151
+
152
+ case 'install_app': {
153
+ const params: Record<string, unknown> = {};
154
+ if (args['registry_app']) params['registry_app'] = args['registry_app'];
155
+ if (args['version']) params['version'] = args['version'];
156
+ if (args['github_url']) params['github_url'] = args['github_url'];
157
+ if (args['commit']) params['commit'] = args['commit'];
158
+ if (args['local_path']) params['local_path'] = args['local_path'];
159
+ if (args['env_vars']) params['env_vars'] = args['env_vars'];
160
+ const data = await client.install(params);
161
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
162
+ }
163
+
164
+ case 'poll_install_job': {
165
+ const jobId = args['job_id'] as string;
166
+ const data = await client.pollJob(jobId);
167
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
168
+ }
169
+
170
+ case 'list_apps': {
171
+ const data = await client.listApps();
172
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
173
+ }
174
+
175
+ case 'app_status': {
176
+ const appName = args['name'] as string;
177
+ const [entry, version] = await Promise.all([
178
+ client.getApp(appName),
179
+ client.getVersion(appName).catch(() => null),
180
+ ]);
181
+ const result = { ...(entry as object), version_info: version };
182
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
183
+ }
184
+
185
+ case 'update_app': {
186
+ const appName = args['name'] as string;
187
+ const data = await client.update(appName);
188
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
189
+ }
190
+
191
+ case 'uninstall_app': {
192
+ const appName = args['name'] as string;
193
+ const data = await client.uninstall(appName);
194
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
195
+ }
196
+
197
+ case 'start_stop_app': {
198
+ const appName = args['name'] as string;
199
+ const action = args['action'] as 'start' | 'stop' | 'restart';
200
+ const data = await client.startStop(appName, action);
201
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
202
+ }
203
+
204
+ default:
205
+ return {
206
+ content: [{ type: 'text', text: `Unknown tool: ${name}` }],
207
+ isError: true,
208
+ };
209
+ }
210
+ }
211
+ }
@@ -0,0 +1,48 @@
1
+ ---
2
+ name: app-status
3
+ description: Show detailed status, version info, and update availability for an installed app.
4
+ user-invocable: true
5
+ allowed-tools:
6
+ - mcp__gateway__app_status
7
+ ---
8
+
9
+ # /app-status — App Status
10
+
11
+ Arguments passed: `$ARGUMENTS`
12
+
13
+ ---
14
+
15
+ ## Usage
16
+
17
+ `/app-status <app-name>`
18
+
19
+ ---
20
+
21
+ ## Steps
22
+
23
+ 1. Call `app_status` with the app name.
24
+
25
+ 2. Format and present:
26
+
27
+ ```
28
+ App: <name>
29
+ Status: running ✓ (or stopped / error / building)
30
+ Version: 1.2.0
31
+ Commit: abc123de
32
+ Source: registry (or custom / local)
33
+ Installed: 2026-05-01T10:00:00Z
34
+
35
+ Proxy routes:
36
+ api → /app/<name>/api/
37
+ web → /app/<name>/web/
38
+
39
+ Version info:
40
+ Installed: 1.2.0
41
+ Latest: 1.3.0 ← update available
42
+ Behind: yes
43
+ ```
44
+
45
+ 3. If an update is available and source is `registry`, offer:
46
+ > Run `/install-app <name>` after uninstalling, or use the update API.
47
+
48
+ If the app is not found (404), say so clearly.
@@ -0,0 +1,94 @@
1
+ ---
2
+ name: create-app-yaml
3
+ description: Scan Dockerfile(s) in the current directory and generate a draft app.yaml for the gateway app store.
4
+ user-invocable: true
5
+ allowed-tools:
6
+ - Read
7
+ - Write
8
+ - Bash(ls *)
9
+ - Bash(find * -name Dockerfile*)
10
+ ---
11
+
12
+ # /create-app-yaml — Generate Draft app.yaml
13
+
14
+ Arguments passed: `$ARGUMENTS`
15
+
16
+ Reads Dockerfile(s) in the current working directory and generates a draft `app.yaml`.
17
+
18
+ ---
19
+
20
+ ## Step 1 — Discover Dockerfiles
21
+
22
+ ```bash
23
+ find . -name "Dockerfile*" -not -path "*/node_modules/*" -not -path "*/.git/*"
24
+ ```
25
+
26
+ List what you found. If none, tell the user and stop.
27
+
28
+ ---
29
+
30
+ ## Step 2 — Infer services
31
+
32
+ For each Dockerfile, infer:
33
+ - **Service name** from the directory name or `Dockerfile.<name>` suffix
34
+ - **Exposed ports** from `EXPOSE` instructions — pick the first as the main port
35
+ - **Environment variables** from `ENV` instructions — these become `env:` entries
36
+ - Whether it looks like a web app (serves HTML/static files) vs an API
37
+
38
+ ---
39
+
40
+ ## Step 3 — Generate app.yaml
41
+
42
+ Write a draft `app.yaml` to the current directory:
43
+
44
+ ```yaml
45
+ apiVersion: apps.getpod.ai/v1
46
+ name: <inferred-from-dirname> # lowercase, hyphens only
47
+ version: 1.0.0
48
+ commit: "" # fill in the 40-char commit hash before release
49
+
50
+ services:
51
+ <service-name>:
52
+ build: . # or ./subdir if not root
53
+ ports:
54
+ - name: <api|web>
55
+ container: <port>
56
+ type: <api|web> # api = REST/backend, web = serves HTML
57
+ rate_limit: 60 # requests per second
58
+ environment:
59
+ # Secrets (no default value) — users will be prompted at install time
60
+ - MY_SECRET_KEY
61
+ # Variables with defaults
62
+ - LOG_LEVEL=info
63
+ healthcheck:
64
+ test: wget -qO- http://localhost:<port>/health || exit 1
65
+ interval: 30s
66
+ timeout: 10s
67
+ retries: 3
68
+ ```
69
+
70
+ Rules for port type:
71
+ - `type: web` if Dockerfile serves static HTML, runs Next.js/React/Vite, or uses nginx/caddy
72
+ - `type: api` otherwise
73
+
74
+ Rules for environment variables:
75
+ - If the Dockerfile has `ENV FOO=bar` → `FOO=bar` (has default)
76
+ - If you see `ARG FOO` or `ENV FOO` with no value → `FOO` (secret, no default)
77
+
78
+ ---
79
+
80
+ ## Step 4 — Show and explain
81
+
82
+ Show the generated `app.yaml` to the user and explain:
83
+ 1. They must fill in `commit:` with a 40-char hex commit hash before publishing
84
+ 2. They can add a `healthcheck:` if the service has a health endpoint
85
+ 3. If the app has an agent, they can add an `agent:` service block
86
+ 4. The `rate_limit` is requests per second — adjust to match expected load
87
+
88
+ ---
89
+
90
+ ## Notes
91
+
92
+ - Never write `network_mode`, `privileged`, or `cap_add` — these are blocked by the gateway
93
+ - Do not use floating `:latest` image tags — warn the user to pin versions
94
+ - The `commit:` field is intentionally left empty as a placeholder
@@ -0,0 +1,102 @@
1
+ ---
2
+ name: install-app
3
+ description: Install an app from the registry or a GitHub URL. Interactive — shows permissions summary, prompts for env vars, polls to completion, and reports proxy URLs.
4
+ user-invocable: true
5
+ allowed-tools:
6
+ - mcp__gateway__install_app
7
+ - mcp__gateway__poll_install_job
8
+ - mcp__gateway__browse_registry
9
+ ---
10
+
11
+ # /install-app — Install an App Store App
12
+
13
+ Arguments passed: `$ARGUMENTS`
14
+
15
+ ---
16
+
17
+ ## Argument formats
18
+
19
+ - `/install-app <registry-name>` — install latest version from registry
20
+ - `/install-app <registry-name> <version>` — install specific version
21
+ - `/install-app <github-url> <40-hex-commit>` — custom GitHub install (pinned commit)
22
+
23
+ ---
24
+
25
+ ## Step 1 — Resolve the app
26
+
27
+ **Registry install:** call `browse_registry` with the app name to get its versions list.
28
+ Show the user:
29
+ - App name and description
30
+ - Repo URL
31
+ - Version you will install (and whether it is latest)
32
+
33
+ **GitHub install:** validate that the commit is a 40-char hex string. If not, tell the user
34
+ and stop.
35
+
36
+ If the app is not found in the registry and no GitHub URL given, stop with a helpful message.
37
+
38
+ ---
39
+
40
+ ## Step 2 — Check for required env vars
41
+
42
+ Call `browse_registry` to get the app definition. If the app has `secretKeys` listed, those
43
+ env vars must be supplied. Prompt the user for each missing secret before proceeding:
44
+
45
+ ```
46
+ This app requires the following environment variables:
47
+ MY_API_KEY — (no default)
48
+ SOME_TOKEN — (no default)
49
+
50
+ Please provide values, e.g.:
51
+ MY_API_KEY=xxx
52
+ SOME_TOKEN=yyy
53
+ ```
54
+
55
+ Wait for the user's reply. Parse key=value pairs.
56
+
57
+ If no secrets are needed, proceed immediately.
58
+
59
+ ---
60
+
61
+ ## Step 3 — Show permissions summary
62
+
63
+ Before installing, show a brief summary:
64
+
65
+ ```
66
+ Installing: <app-name> v<version>
67
+ Source: <registry|github>
68
+ Repo: <url>
69
+ Commit: <first 8 chars>
70
+ Proxy routes: (from registry metadata if available)
71
+ Secrets to inject: <list or "none">
72
+
73
+ Proceed? (yes/no)
74
+ ```
75
+
76
+ Wait for confirmation.
77
+
78
+ ---
79
+
80
+ ## Step 4 — Install
81
+
82
+ Call `install_app` with the resolved parameters and any collected env vars.
83
+
84
+ Store the returned `jobId`.
85
+
86
+ ---
87
+
88
+ ## Step 5 — Poll to completion
89
+
90
+ Poll `poll_install_job` every 3 seconds. Show a brief progress line after each poll
91
+ (use the last log entry from the job). Stop when status is `completed` or `failed`.
92
+
93
+ On **completed**: show proxy URLs from `result.proxyUrls`.
94
+ On **failed**: show the error message and last 5 log entries.
95
+
96
+ ---
97
+
98
+ ## Notes
99
+
100
+ - Never pass branch names as `commit` — only 40-char hex strings are valid.
101
+ - `env_vars` values should not be echoed back to the user after collection.
102
+ - If the user cancels at any confirmation step, say so and stop.
@@ -0,0 +1,34 @@
1
+ ---
2
+ name: list-apps
3
+ description: List all installed apps with their status, version, and proxy URLs.
4
+ user-invocable: true
5
+ allowed-tools:
6
+ - mcp__gateway__list_apps
7
+ ---
8
+
9
+ # /list-apps — List Installed Apps
10
+
11
+ Arguments passed: `$ARGUMENTS`
12
+
13
+ ---
14
+
15
+ Call `list_apps` and format the result as a readable table:
16
+
17
+ ```
18
+ Installed Apps
19
+ ──────────────────────────────────────────────
20
+ NAME VERSION STATUS SOURCE
21
+ my-app 1.2.0 running registry
22
+ another-app 0.3.1 stopped custom
23
+ ──────────────────────────────────────────────
24
+ Total: 2 app(s)
25
+ ```
26
+
27
+ For each app also show its proxy URLs if it has ports:
28
+ ```
29
+ Proxy routes:
30
+ api → /app/my-app/api/
31
+ web → /app/my-app/web/
32
+ ```
33
+
34
+ If no apps are installed, say so clearly.
@@ -35,6 +35,7 @@ import { hasMarkdown, toTelegramHtml } from './pure'
35
35
  const STATE_DIR = process.env.TELEGRAM_STATE_DIR ?? join(homedir(), '.claude', 'channels', 'telegram')
36
36
  const ACCESS_FILE = join(STATE_DIR, 'access.json')
37
37
  const APPROVED_DIR = join(STATE_DIR, 'approved')
38
+ const AWAITING_OWNER_FILE = join(STATE_DIR, 'awaiting-owner')
38
39
  const ENV_FILE = join(STATE_DIR, '.env')
39
40
 
40
41
  // Load .env fallback when token not injected via env block (standalone mode).
@@ -114,7 +115,7 @@ type GroupPolicy = {
114
115
  }
115
116
 
116
117
  type Access = {
117
- dmPolicy: 'pairing' | 'allowlist' | 'disabled'
118
+ dmPolicy: 'open' | 'pairing' | 'allowlist' | 'disabled'
118
119
  allowFrom: string[]
119
120
  groups: Record<string, GroupPolicy>
120
121
  pending: Record<string, PendingEntry>
@@ -232,7 +233,40 @@ function gate(ctx: Context): GateResult {
232
233
  const senderId = String(from.id)
233
234
  const chatType = ctx.chat?.type
234
235
 
236
+ // Owner init-pairing sentinel: first private message auto-approves sender as owner.
235
237
  if (chatType === 'private') {
238
+ try {
239
+ const stat = statSync(AWAITING_OWNER_FILE)
240
+ const age = Date.now() - stat.mtimeMs
241
+ if (age < 10 * 60 * 1000) {
242
+ // Sentinel is valid — approve this sender as owner
243
+ if (!access.allowFrom.includes(senderId)) access.allowFrom.push(senderId)
244
+ saveAccess(access)
245
+ rmSync(AWAITING_OWNER_FILE, { force: true })
246
+ // Write approved file so receiver sends confirmation message
247
+ mkdirSync(APPROVED_DIR, { recursive: true })
248
+ writeFileSync(join(APPROVED_DIR, senderId), String(ctx.chat!.id))
249
+ return { action: 'deliver', access }
250
+ } else {
251
+ rmSync(AWAITING_OWNER_FILE, { force: true })
252
+ }
253
+ } catch (err) {
254
+ if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
255
+ // Unexpected I/O error — deny to be safe rather than silently allowing
256
+ return { action: 'drop' }
257
+ }
258
+ // ENOENT — no sentinel, continue normal gate logic
259
+ }
260
+ }
261
+
262
+ if (chatType === 'private') {
263
+ if (access.dmPolicy === 'open') {
264
+ if (!access.allowFrom.includes(senderId)) {
265
+ access.allowFrom.push(senderId)
266
+ saveAccess(access)
267
+ }
268
+ return { action: 'deliver', access }
269
+ }
236
270
  if (access.allowFrom.includes(senderId)) return { action: 'deliver', access }
237
271
  if (access.dmPolicy === 'allowlist') return { action: 'drop' }
238
272
 
@@ -1592,7 +1626,7 @@ if (RECEIVER_MODE) {
1592
1626
  }
1593
1627
  if (err instanceof Error && err.message === 'Aborted delay') return
1594
1628
  process.stderr.write(`telegram channel (receiver): polling failed: ${err}\n`)
1595
- return
1629
+ process.exit(1)
1596
1630
  }
1597
1631
  }
1598
1632
  })()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0xmaxma/claude-gateway",
3
- "version": "1.1.9",
3
+ "version": "1.2.0",
4
4
  "description": "Multi-agent gateway for Claude",
5
5
  "repository": {
6
6
  "type": "git",