@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.
- package/README.md +61 -1
- package/dist/agent/builtin-commands.d.ts +10 -0
- package/dist/agent/builtin-commands.d.ts.map +1 -0
- package/dist/agent/builtin-commands.js +38 -0
- package/dist/agent/builtin-commands.js.map +1 -0
- package/dist/agent/runner.d.ts +7 -5
- package/dist/agent/runner.d.ts.map +1 -1
- package/dist/agent/runner.js +56 -30
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/workspace-loader.d.ts.map +1 -1
- package/dist/agent/workspace-loader.js +9 -4
- package/dist/agent/workspace-loader.js.map +1 -1
- package/dist/api/apps-router.d.ts +7 -0
- package/dist/api/apps-router.d.ts.map +1 -0
- package/dist/api/apps-router.js +242 -0
- package/dist/api/apps-router.js.map +1 -0
- package/dist/api/gateway-router.d.ts +17 -1
- package/dist/api/gateway-router.d.ts.map +1 -1
- package/dist/api/gateway-router.js +171 -2
- package/dist/api/gateway-router.js.map +1 -1
- package/dist/api/router.d.ts.map +1 -1
- package/dist/api/router.js +385 -4
- package/dist/api/router.js.map +1 -1
- package/dist/apps/agent-manager.d.ts +76 -0
- package/dist/apps/agent-manager.d.ts.map +1 -0
- package/dist/apps/agent-manager.js +311 -0
- package/dist/apps/agent-manager.js.map +1 -0
- package/dist/apps/compose-generator.d.ts +108 -0
- package/dist/apps/compose-generator.d.ts.map +1 -0
- package/dist/apps/compose-generator.js +687 -0
- package/dist/apps/compose-generator.js.map +1 -0
- package/dist/apps/installer.d.ts +101 -0
- package/dist/apps/installer.d.ts.map +1 -0
- package/dist/apps/installer.js +898 -0
- package/dist/apps/installer.js.map +1 -0
- package/dist/apps/registry-client.d.ts +37 -0
- package/dist/apps/registry-client.d.ts.map +1 -0
- package/dist/apps/registry-client.js +106 -0
- package/dist/apps/registry-client.js.map +1 -0
- package/dist/apps/registry.d.ts +54 -0
- package/dist/apps/registry.d.ts.map +1 -0
- package/dist/apps/registry.js +182 -0
- package/dist/apps/registry.js.map +1 -0
- package/dist/apps/socket-server.d.ts +46 -0
- package/dist/apps/socket-server.d.ts.map +1 -0
- package/dist/apps/socket-server.js +297 -0
- package/dist/apps/socket-server.js.map +1 -0
- package/dist/config/watcher.d.ts +1 -0
- package/dist/config/watcher.d.ts.map +1 -1
- package/dist/config/watcher.js +55 -2
- package/dist/config/watcher.js.map +1 -1
- package/dist/index.js +170 -3
- package/dist/index.js.map +1 -1
- package/dist/session/process.d.ts.map +1 -1
- package/dist/session/process.js +47 -6
- package/dist/session/process.js.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/mcp/server.ts +2 -0
- package/mcp/tools/apps/client.ts +78 -0
- package/mcp/tools/apps/module.ts +211 -0
- package/mcp/tools/apps/skills/app-status/SKILL.md +48 -0
- package/mcp/tools/apps/skills/create-app-yaml/SKILL.md +94 -0
- package/mcp/tools/apps/skills/install-app/SKILL.md +102 -0
- package/mcp/tools/apps/skills/list-apps/SKILL.md +34 -0
- package/mcp/tools/telegram/receiver-server.ts +36 -2
- 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
|
-
|
|
1629
|
+
process.exit(1)
|
|
1596
1630
|
}
|
|
1597
1631
|
}
|
|
1598
1632
|
})()
|