@virtue-ai/gateway-connect 0.1.0 → 0.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 CHANGED
@@ -23,7 +23,8 @@ This will:
23
23
  1. Open your browser for OAuth login (select "Authorize the platform")
24
24
  2. Save gateway credentials to `~/.openclaw/mcp-gateway.json`
25
25
  3. Patch `~/.openclaw/openclaw.json` to connect claude-cli to the gateway
26
- 4. Verify connection and list available tools
26
+ 4. Install the trajectory recording plugin (sends full session trace to VirtueAI dashboard)
27
+ 5. Verify connection and list available tools
27
28
 
28
29
  ### Step 3: Start Using
29
30
 
@@ -40,7 +41,8 @@ That's it. OpenClaw now has access to all MCP tools on the gateway (GitHub, Goog
40
41
  1. **OAuth 2.0 PKCE authentication** — Registers an OAuth client, opens browser for login, exchanges authorization code for tokens
41
42
  2. **MCP config generation** — Writes `~/.openclaw/mcp-gateway.json` with gateway URL and bearer token
42
43
  3. **OpenClaw config patching** — Adds `--mcp-config` to the claude-cli backend args in `~/.openclaw/openclaw.json`
43
- 4. **Connection verification** — Calls `tools/list` on the gateway and reports available tools
44
+ 4. **Trajectory recording** — Installs an OpenClaw plugin (`virtueai-trajectory`) that automatically sends every agent step (user prompts, agent responses, tool calls) to the VirtueAI prompt-guard API for dashboard visibility
45
+ 5. **Connection verification** — Calls `tools/list` on the gateway and reports available tools
44
46
 
45
47
  ## Options
46
48
 
@@ -48,8 +50,9 @@ That's it. OpenClaw now has access to all MCP tools on the gateway (GitHub, Goog
48
50
  npx @virtue-ai/gateway-connect [options]
49
51
 
50
52
  Options:
51
- --gateway-url <url> Gateway URL (required)
52
- --help Show help message
53
+ --gateway-url <url> Gateway URL (required)
54
+ --guard-uuid <uuid> Guard UUID for trajectory recording (or set VIRTUEAI_GUARD_UUID env var)
55
+ --help Show help message
53
56
  ```
54
57
 
55
58
  ## Re-authentication
package/dist/index.js CHANGED
@@ -19,6 +19,7 @@ import https from 'https';
19
19
  import os from 'os';
20
20
  import path from 'path';
21
21
  import { URL } from 'url';
22
+ import { generateTrajectoryPlugin, enableTrajectoryPlugin } from './trajectory-plugin.js';
22
23
  // ---------------------------------------------------------------------------
23
24
  // Constants
24
25
  // ---------------------------------------------------------------------------
@@ -222,7 +223,7 @@ async function authenticate(gatewayUrl) {
222
223
  // ---------------------------------------------------------------------------
223
224
  // Step 2: Write MCP gateway config
224
225
  // ---------------------------------------------------------------------------
225
- function writeMcpConfig(gatewayUrl, accessToken) {
226
+ function writeMcpConfig(gatewayUrl, accessToken, guardUuid) {
226
227
  fs.mkdirSync(OPENCLAW_DIR, { recursive: true });
227
228
  const config = {
228
229
  mcpServers: {
@@ -234,6 +235,10 @@ function writeMcpConfig(gatewayUrl, accessToken) {
234
235
  },
235
236
  },
236
237
  },
238
+ trajectory: {
239
+ gatewayUrl,
240
+ guardUuid: guardUuid || process.env.VIRTUEAI_GUARD_UUID || '',
241
+ },
237
242
  };
238
243
  fs.writeFileSync(MCP_CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
239
244
  console.log(` Written: ${MCP_CONFIG_PATH}`);
@@ -341,18 +346,21 @@ Usage:
341
346
  npx @virtue-ai/gateway-connect [options]
342
347
 
343
348
  Options:
344
- --gateway-url <url> Gateway URL (default: ${DEFAULT_GATEWAY_URL})
345
- --help Show this help message
349
+ --gateway-url <url> Gateway URL (default: ${DEFAULT_GATEWAY_URL})
350
+ --guard-uuid <uuid> Guard UUID for trajectory recording (or set VIRTUEAI_GUARD_UUID)
351
+ --help Show this help message
346
352
 
347
353
  What it does:
348
354
  1. Opens browser for OAuth login
349
355
  2. Saves MCP config to ~/.openclaw/mcp-gateway.json
350
356
  3. Patches ~/.openclaw/openclaw.json to use the gateway
351
- 4. Verifies connection by listing available tools
357
+ 4. Installs trajectory plugin for full session recording
358
+ 5. Verifies connection by listing available tools
352
359
  `);
353
360
  process.exit(0);
354
361
  }
355
362
  let gatewayUrl = getArg('gateway-url') || DEFAULT_GATEWAY_URL;
363
+ const guardUuid = getArg('guard-uuid') || process.env.VIRTUEAI_GUARD_UUID;
356
364
  // Strip /mcp suffix and normalize to lowercase
357
365
  gatewayUrl = gatewayUrl.replace(/\/mcp\/?$/, '').toLowerCase();
358
366
  console.log('\n VirtueAI Gateway Connect\n');
@@ -361,16 +369,21 @@ What it does:
361
369
  const { accessToken } = await authenticate(gatewayUrl);
362
370
  // Step 2: Write MCP config
363
371
  console.log('\n Configuring OpenClaw...');
364
- writeMcpConfig(gatewayUrl, accessToken);
372
+ writeMcpConfig(gatewayUrl, accessToken, guardUuid);
365
373
  // Step 3: Patch openclaw.json
366
374
  patchOpenClawConfig();
367
- // Step 4: Verify
375
+ // Step 4: Install trajectory plugin
376
+ console.log('\n Setting up trajectory recording...');
377
+ generateTrajectoryPlugin(guardUuid);
378
+ enableTrajectoryPlugin();
379
+ // Step 5: Verify
368
380
  console.log('');
369
381
  const toolCount = await verifyConnection(gatewayUrl, accessToken);
370
382
  // Done
371
383
  console.log(`
372
384
  Done! OpenClaw is now connected to VirtueAI MCP gateway.
373
385
  ${toolCount} tools available across the gateway.
386
+ Trajectory recording enabled (via virtueai-trajectory plugin).
374
387
 
375
388
  Config files:
376
389
  ${MCP_CONFIG_PATH}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Trajectory Plugin Generator
3
+ *
4
+ * Generates and installs an OpenClaw plugin that hooks into the agent lifecycle
5
+ * to send every trajectory step to the VirtueAI gateway's prompt-guard API.
6
+ *
7
+ * Hooks used (all fire-and-forget, never block the agent):
8
+ * - llm_input: captures user prompt → role "user"
9
+ * - llm_output: captures agent reply → role "agent"
10
+ * - after_tool_call: captures tool results → role "agent"
11
+ */
12
+ /**
13
+ * Generate the trajectory plugin files on disk.
14
+ */
15
+ export declare function generateTrajectoryPlugin(guardUuid?: string): void;
16
+ /**
17
+ * Enable the trajectory plugin in openclaw.json.
18
+ */
19
+ export declare function enableTrajectoryPlugin(): void;
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Trajectory Plugin Generator
3
+ *
4
+ * Generates and installs an OpenClaw plugin that hooks into the agent lifecycle
5
+ * to send every trajectory step to the VirtueAI gateway's prompt-guard API.
6
+ *
7
+ * Hooks used (all fire-and-forget, never block the agent):
8
+ * - llm_input: captures user prompt → role "user"
9
+ * - llm_output: captures agent reply → role "agent"
10
+ * - after_tool_call: captures tool results → role "agent"
11
+ */
12
+ import fs from 'fs';
13
+ import os from 'os';
14
+ import path from 'path';
15
+ // ---------------------------------------------------------------------------
16
+ // Constants
17
+ // ---------------------------------------------------------------------------
18
+ const OPENCLAW_DIR = path.join(os.homedir(), '.openclaw');
19
+ const OPENCLAW_CONFIG_PATH = path.join(OPENCLAW_DIR, 'openclaw.json');
20
+ const MCP_CONFIG_PATH = path.join(OPENCLAW_DIR, 'mcp-gateway.json');
21
+ const PLUGIN_ID = 'virtueai-trajectory';
22
+ const PLUGIN_DIR = path.join(OPENCLAW_DIR, 'extensions', PLUGIN_ID);
23
+ const DEFAULT_GUARD_UUID = '3a2389709528a539a12ba6239e402ef159ecffa88f99af6e13236e69dd9bb2e5';
24
+ // ---------------------------------------------------------------------------
25
+ // Plugin source code (generated as a string, written to disk)
26
+ // ---------------------------------------------------------------------------
27
+ function buildPluginSource() {
28
+ return `\
29
+ import { readFileSync } from "fs";
30
+ import { join } from "path";
31
+ import { homedir } from "os";
32
+
33
+ /**
34
+ * VirtueAI Trajectory Plugin
35
+ *
36
+ * Sends every agent interaction step to the VirtueAI gateway prompt-guard API
37
+ * so the full trajectory is visible in the dashboard.
38
+ *
39
+ * All hooks are fire-and-forget — errors are logged, never thrown.
40
+ */
41
+
42
+ const MCP_CONFIG_PATH = join(homedir(), ".openclaw", "mcp-gateway.json");
43
+ const DEFAULT_GUARD_UUID =
44
+ "${DEFAULT_GUARD_UUID}";
45
+
46
+ function loadConfig() {
47
+ try {
48
+ const raw = readFileSync(MCP_CONFIG_PATH, "utf-8");
49
+ const cfg = JSON.parse(raw);
50
+ const virtueai = cfg.mcpServers?.virtueai;
51
+ if (!virtueai) return null;
52
+
53
+ const authHeader = virtueai.headers?.Authorization ?? "";
54
+ const token = authHeader.replace(/^Bearer\\s+/i, "");
55
+ const gatewayUrl = (virtueai.url ?? "").replace(/\\/mcp\\/?$/, "");
56
+
57
+ const guardUuid =
58
+ cfg.trajectory?.guardUuid ||
59
+ process.env.VIRTUEAI_GUARD_UUID ||
60
+ DEFAULT_GUARD_UUID;
61
+
62
+ if (!gatewayUrl || !token) return null;
63
+ return { gatewayUrl, token, guardUuid };
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+
69
+ function truncate(s, max = 2000) {
70
+ if (typeof s !== "string") {
71
+ try { s = JSON.stringify(s); } catch { s = String(s); }
72
+ }
73
+ return s.length > max ? s.slice(0, max) + "..." : s;
74
+ }
75
+
76
+ const plugin = {
77
+ id: "${PLUGIN_ID}",
78
+ name: "VirtueAI Trajectory",
79
+ description: "Sends agent trajectory steps to VirtueAI gateway for dashboard visibility",
80
+
81
+ register(api) {
82
+ const config = loadConfig();
83
+ if (!config) {
84
+ api.logger.warn("[virtueai-trajectory] No valid config found in " + MCP_CONFIG_PATH + ", plugin disabled");
85
+ return;
86
+ }
87
+
88
+ let gatewaySessionId = null;
89
+ const endpoint = config.gatewayUrl + "/api/prompt-guard/topic_guard";
90
+
91
+ async function sendStep(role, content) {
92
+ const body = {
93
+ user_prompt: truncate(content),
94
+ guard_uuid: config.guardUuid,
95
+ role,
96
+ };
97
+ if (gatewaySessionId) {
98
+ body.session_id = gatewaySessionId;
99
+ }
100
+
101
+ try {
102
+ const res = await fetch(endpoint, {
103
+ method: "POST",
104
+ headers: {
105
+ "Content-Type": "application/json",
106
+ Authorization: "Bearer " + config.token,
107
+ },
108
+ body: JSON.stringify(body),
109
+ });
110
+ const data = await res.json();
111
+
112
+ if (data?.session_id && !gatewaySessionId) {
113
+ gatewaySessionId = data.session_id;
114
+ api.logger.info("[virtueai-trajectory] Gateway session: " + gatewaySessionId);
115
+ }
116
+ } catch (err) {
117
+ api.logger.warn("[virtueai-trajectory] Failed to send step: " + (err?.message ?? err));
118
+ }
119
+ }
120
+
121
+ // Hook: user prompt sent to LLM
122
+ api.on("llm_input", (event) => {
123
+ if (event.prompt) {
124
+ sendStep("user", event.prompt);
125
+ }
126
+ });
127
+
128
+ // Hook: LLM response received
129
+ api.on("llm_output", (event) => {
130
+ const text = (event.assistantTexts ?? []).join("\\n").trim();
131
+ if (text) {
132
+ sendStep("agent", text);
133
+ }
134
+ });
135
+
136
+ // Hook: tool call completed
137
+ api.on("after_tool_call", (event) => {
138
+ const params = event.params
139
+ ? Object.entries(event.params)
140
+ .map(([k, v]) => k + "=" + JSON.stringify(v))
141
+ .join(", ")
142
+ : "";
143
+ const callStr = event.toolName + "(" + params + ")";
144
+ const resultStr = event.result != null ? truncate(event.result, 500) : (event.error ?? "no result");
145
+ sendStep("agent", callStr + " → " + resultStr);
146
+ });
147
+
148
+ api.logger.info("[virtueai-trajectory] Plugin registered, sending to " + config.gatewayUrl);
149
+ },
150
+ };
151
+
152
+ export default plugin;
153
+ `;
154
+ }
155
+ // ---------------------------------------------------------------------------
156
+ // Public API
157
+ // ---------------------------------------------------------------------------
158
+ /**
159
+ * Generate the trajectory plugin files on disk.
160
+ */
161
+ export function generateTrajectoryPlugin(guardUuid) {
162
+ fs.mkdirSync(PLUGIN_DIR, { recursive: true });
163
+ // package.json
164
+ const pkg = {
165
+ name: '@virtue-ai/trajectory',
166
+ version: '1.0.0',
167
+ type: 'module',
168
+ openclaw: {
169
+ extensions: ['./index.ts'],
170
+ },
171
+ };
172
+ fs.writeFileSync(path.join(PLUGIN_DIR, 'package.json'), JSON.stringify(pkg, null, 2) + '\n');
173
+ // index.ts (the actual plugin)
174
+ fs.writeFileSync(path.join(PLUGIN_DIR, 'index.ts'), buildPluginSource());
175
+ // Persist guard UUID in mcp-gateway.json trajectory section
176
+ if (guardUuid) {
177
+ try {
178
+ const raw = fs.readFileSync(MCP_CONFIG_PATH, 'utf-8');
179
+ const cfg = JSON.parse(raw);
180
+ if (!cfg.trajectory)
181
+ cfg.trajectory = {};
182
+ cfg.trajectory.guardUuid = guardUuid;
183
+ fs.writeFileSync(MCP_CONFIG_PATH, JSON.stringify(cfg, null, 2) + '\n');
184
+ }
185
+ catch {
186
+ // Config may not exist yet — will be written by writeMcpConfig
187
+ }
188
+ }
189
+ console.log(` Generated trajectory plugin: ${PLUGIN_DIR}`);
190
+ }
191
+ /**
192
+ * Enable the trajectory plugin in openclaw.json.
193
+ */
194
+ export function enableTrajectoryPlugin() {
195
+ let config = {};
196
+ if (fs.existsSync(OPENCLAW_CONFIG_PATH)) {
197
+ try {
198
+ config = JSON.parse(fs.readFileSync(OPENCLAW_CONFIG_PATH, 'utf-8'));
199
+ }
200
+ catch {
201
+ config = {};
202
+ }
203
+ }
204
+ // plugins.entries
205
+ if (!config.plugins)
206
+ config.plugins = {};
207
+ if (!config.plugins.entries)
208
+ config.plugins.entries = {};
209
+ config.plugins.entries[PLUGIN_ID] = { enabled: true };
210
+ // plugins.allow
211
+ if (!config.plugins.allow)
212
+ config.plugins.allow = [];
213
+ if (!config.plugins.allow.includes(PLUGIN_ID)) {
214
+ config.plugins.allow.push(PLUGIN_ID);
215
+ }
216
+ // plugins.installs (so OpenClaw knows where the plugin lives)
217
+ if (!config.plugins.installs)
218
+ config.plugins.installs = {};
219
+ config.plugins.installs[PLUGIN_ID] = {
220
+ source: 'path',
221
+ sourcePath: PLUGIN_DIR,
222
+ installPath: PLUGIN_DIR,
223
+ version: '1.0.0',
224
+ installedAt: new Date().toISOString(),
225
+ };
226
+ fs.writeFileSync(OPENCLAW_CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
227
+ console.log(` Enabled trajectory plugin in: ${OPENCLAW_CONFIG_PATH}`);
228
+ }
package/package.json CHANGED
@@ -1,17 +1,26 @@
1
1
  {
2
2
  "name": "@virtue-ai/gateway-connect",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "One-command setup to connect OpenClaw to VirtueAI MCP gateway",
5
5
  "type": "module",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "main": "dist/index.js",
6
10
  "bin": {
7
- "gateway-connect": "./dist/index.js"
11
+ "gateway-connect": "dist/index.js"
8
12
  },
9
13
  "scripts": {
10
14
  "build": "tsc",
11
15
  "start": "node dist/index.js",
12
16
  "dev": "tsx src/index.ts"
13
17
  },
14
- "keywords": ["openclaw", "mcp", "gateway", "virtueai"],
18
+ "keywords": [
19
+ "openclaw",
20
+ "mcp",
21
+ "gateway",
22
+ "virtueai"
23
+ ],
15
24
  "license": "MIT",
16
25
  "devDependencies": {
17
26
  "@types/node": "^22.10.0",
package/Dockerfile DELETED
@@ -1,29 +0,0 @@
1
- FROM node:22-bookworm
2
-
3
- RUN apt-get update && apt-get install -y jq curl && rm -rf /var/lib/apt/lists/*
4
-
5
- # Create non-root user (claude --dangerously-skip-permissions blocks root)
6
- RUN useradd -m -s /bin/bash claw
7
-
8
- # Install OpenClaw + Claude Code CLI
9
- RUN npm install -g openclaw@latest @anthropic-ai/claude-code
10
-
11
- # Copy gateway-connect tool
12
- WORKDIR /app
13
- COPY package.json package-lock.json* ./
14
- RUN npm install
15
- COPY tsconfig.json ./
16
- COPY src ./src
17
- RUN npm run build
18
-
19
- # Make the CLI executable
20
- RUN chmod +x dist/index.js
21
-
22
- # Switch to non-root user
23
- RUN chown -R claw:claw /app /home/claw
24
- USER claw
25
-
26
- # Expose callback port for OAuth flow
27
- EXPOSE 19876
28
-
29
- CMD ["/bin/bash"]
package/src/index.ts DELETED
@@ -1,457 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * VirtueAI Gateway Connect
4
- *
5
- * One-command setup to connect OpenClaw to VirtueAI MCP gateway.
6
- *
7
- * 1. OAuth 2.0 PKCE login → obtain access token
8
- * 2. Write MCP gateway config to ~/.openclaw/mcp-gateway.json
9
- * 3. Patch ~/.openclaw/openclaw.json to use claude-cli with --mcp-config
10
- * 4. Verify connection by listing tools
11
- *
12
- * Usage: npx @virtue-ai/gateway-connect [--gateway-url https://...]
13
- */
14
-
15
- import crypto from 'crypto';
16
- import { execSync } from 'child_process';
17
- import fs from 'fs';
18
- import http from 'http';
19
- import https from 'https';
20
- import os from 'os';
21
- import path from 'path';
22
- import { URL } from 'url';
23
-
24
- // ---------------------------------------------------------------------------
25
- // Constants
26
- // ---------------------------------------------------------------------------
27
-
28
- const CALLBACK_PORT = 19876;
29
- const CALLBACK_PATH = '/callback';
30
- const REDIRECT_URI = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
31
- const SCOPES = 'claudeai copilot mcp:read mcp:execute mcp:access';
32
-
33
- const OPENCLAW_DIR = path.join(os.homedir(), '.openclaw');
34
- const MCP_CONFIG_PATH = path.join(OPENCLAW_DIR, 'mcp-gateway.json');
35
- const OPENCLAW_CONFIG_PATH = path.join(OPENCLAW_DIR, 'openclaw.json');
36
-
37
- const DEFAULT_GATEWAY_URL = 'https://virtueai-agent-gtw-l3phon63.ngrok.io';
38
-
39
- // ---------------------------------------------------------------------------
40
- // Helpers
41
- // ---------------------------------------------------------------------------
42
-
43
- function getArg(name: string): string | undefined {
44
- const idx = process.argv.indexOf(`--${name}`);
45
- return idx !== -1 ? process.argv[idx + 1] : undefined;
46
- }
47
-
48
- function hasFlag(name: string): boolean {
49
- return process.argv.includes(`--${name}`);
50
- }
51
-
52
- function fetchJson(
53
- url: string,
54
- options: { method?: string; headers?: Record<string, string>; body?: string },
55
- ): Promise<{ status: number; data: any }> {
56
- return new Promise((resolve, reject) => {
57
- const parsed = new URL(url);
58
- const mod = parsed.protocol === 'https:' ? https : http;
59
- const req = mod.request(
60
- parsed,
61
- {
62
- method: options.method ?? 'GET',
63
- headers: {
64
- ...(options.body ? { 'Content-Type': 'application/x-www-form-urlencoded' } : {}),
65
- ...(options.headers ?? {}),
66
- },
67
- },
68
- (res) => {
69
- let body = '';
70
- res.on('data', (chunk: Buffer) => (body += chunk.toString()));
71
- res.on('end', () => {
72
- try {
73
- resolve({ status: res.statusCode ?? 0, data: JSON.parse(body) });
74
- } catch {
75
- resolve({ status: res.statusCode ?? 0, data: body });
76
- }
77
- });
78
- },
79
- );
80
- req.on('error', reject);
81
- if (options.body) req.write(options.body);
82
- req.end();
83
- });
84
- }
85
-
86
- function generateCodeVerifier(): string {
87
- return crypto.randomBytes(32).toString('base64url');
88
- }
89
-
90
- function generateCodeChallenge(verifier: string): string {
91
- return crypto.createHash('sha256').update(verifier).digest('base64url');
92
- }
93
-
94
- function openBrowser(url: string): void {
95
- const platform = process.platform;
96
- try {
97
- if (platform === 'darwin') {
98
- execSync(`open "${url}"`);
99
- } else if (platform === 'linux') {
100
- execSync(`xdg-open "${url}" 2>/dev/null || sensible-browser "${url}" 2>/dev/null || echo ""`);
101
- } else {
102
- execSync(`start "" "${url}"`);
103
- }
104
- } catch {
105
- // Silently fail — we always print the URL below
106
- }
107
- }
108
-
109
- // ---------------------------------------------------------------------------
110
- // Step 1: OAuth PKCE Authentication
111
- // ---------------------------------------------------------------------------
112
-
113
- async function authenticate(gatewayUrl: string): Promise<{
114
- accessToken: string;
115
- refreshToken?: string;
116
- clientId: string;
117
- }> {
118
- // Discover OAuth metadata
119
- console.log(' Discovering OAuth endpoints...');
120
- const { data: metadata } = await fetchJson(
121
- `${gatewayUrl}/.well-known/oauth-authorization-server`,
122
- { method: 'GET' },
123
- );
124
-
125
- if (!metadata.authorization_endpoint || !metadata.token_endpoint) {
126
- console.error('Error: Could not discover OAuth endpoints from gateway.');
127
- console.error('Response:', JSON.stringify(metadata, null, 2));
128
- process.exit(1);
129
- }
130
-
131
- const authEndpoint: string = metadata.authorization_endpoint;
132
- const tokenEndpoint: string = metadata.token_endpoint;
133
- const registerEndpoint: string = metadata.registration_endpoint;
134
-
135
- console.log(` Auth endpoint: ${authEndpoint}`);
136
- console.log(` Token endpoint: ${tokenEndpoint}`);
137
-
138
- // Register OAuth client
139
- console.log(' Registering OAuth client...');
140
- const { status: regStatus, data: clientInfo } = await fetchJson(registerEndpoint, {
141
- method: 'POST',
142
- headers: { 'Content-Type': 'application/json' },
143
- body: JSON.stringify({
144
- client_name: 'openclaw-gateway-connect',
145
- grant_types: ['authorization_code', 'refresh_token'],
146
- redirect_uris: [REDIRECT_URI],
147
- scope: SCOPES,
148
- token_endpoint_auth_method: 'none',
149
- }),
150
- });
151
-
152
- if (!clientInfo.client_id) {
153
- console.error(`Error: Client registration failed (${regStatus}).`);
154
- console.error(JSON.stringify(clientInfo, null, 2));
155
- process.exit(1);
156
- }
157
-
158
- const clientId: string = clientInfo.client_id;
159
- console.log(` Client ID: ${clientId}`);
160
-
161
- // Build authorization URL with PKCE
162
- const codeVerifier = generateCodeVerifier();
163
- const codeChallenge = generateCodeChallenge(codeVerifier);
164
- const state = crypto.randomBytes(16).toString('hex');
165
-
166
- const authUrl = new URL(authEndpoint);
167
- authUrl.searchParams.set('client_id', clientId);
168
- authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
169
- authUrl.searchParams.set('response_type', 'code');
170
- authUrl.searchParams.set('scope', SCOPES);
171
- authUrl.searchParams.set('state', state);
172
- authUrl.searchParams.set('code_challenge', codeChallenge);
173
- authUrl.searchParams.set('code_challenge_method', 'S256');
174
-
175
- // Start callback server & open browser
176
- const authCode = await new Promise<string>((resolve, reject) => {
177
- const server = http.createServer((req, res) => {
178
- if (!req.url?.startsWith(CALLBACK_PATH)) {
179
- res.writeHead(404);
180
- res.end('Not found');
181
- return;
182
- }
183
-
184
- const url = new URL(req.url, `http://localhost:${CALLBACK_PORT}`);
185
- const code = url.searchParams.get('code');
186
- const returnedState = url.searchParams.get('state');
187
- const error = url.searchParams.get('error');
188
-
189
- if (error) {
190
- res.writeHead(400, { 'Content-Type': 'text/html' });
191
- res.end(`<h2>Authentication failed</h2><p>${error}</p><p>You can close this window.</p>`);
192
- server.close();
193
- reject(new Error(`OAuth error: ${error}`));
194
- return;
195
- }
196
-
197
- if (!code || returnedState !== state) {
198
- res.writeHead(400, { 'Content-Type': 'text/html' });
199
- res.end('<h2>Invalid callback</h2><p>Missing code or state mismatch.</p>');
200
- server.close();
201
- reject(new Error('Invalid callback: missing code or state mismatch'));
202
- return;
203
- }
204
-
205
- res.writeHead(200, { 'Content-Type': 'text/html' });
206
- res.end(
207
- '<h2>Authentication successful!</h2>' +
208
- '<p>You can close this window and return to the terminal.</p>' +
209
- '<script>window.close()</script>',
210
- );
211
- server.close();
212
- resolve(code);
213
- });
214
-
215
- server.listen(CALLBACK_PORT, '0.0.0.0', () => {
216
- console.log(`\n Opening browser for login...`);
217
- console.log(` (callback server listening on port ${CALLBACK_PORT})\n`);
218
- console.log(` If browser doesn't open, visit this URL:\n`);
219
- console.log(` ${authUrl.toString()}\n`);
220
- openBrowser(authUrl.toString());
221
- });
222
-
223
- server.on('error', (err) => {
224
- if ((err as NodeJS.ErrnoException).code === 'EADDRINUSE') {
225
- reject(new Error(`Port ${CALLBACK_PORT} is in use. Close the other process and try again.`));
226
- } else {
227
- reject(err);
228
- }
229
- });
230
-
231
- // Timeout after 5 minutes
232
- setTimeout(() => {
233
- server.close();
234
- reject(new Error('Authentication timed out (5 minutes). Please try again.'));
235
- }, 5 * 60 * 1000);
236
- });
237
-
238
- console.log(' Authorization code received. Exchanging for tokens...');
239
-
240
- // Exchange code for tokens
241
- const tokenBody = new URLSearchParams({
242
- grant_type: 'authorization_code',
243
- client_id: clientId,
244
- code: authCode,
245
- redirect_uri: REDIRECT_URI,
246
- code_verifier: codeVerifier,
247
- }).toString();
248
-
249
- const { status: tokenStatus, data: tokenData } = await fetchJson(tokenEndpoint, {
250
- method: 'POST',
251
- body: tokenBody,
252
- });
253
-
254
- if (!tokenData.access_token) {
255
- console.error(`Error: Token exchange failed (${tokenStatus}).`);
256
- console.error(JSON.stringify(tokenData, null, 2));
257
- process.exit(1);
258
- }
259
-
260
- console.log(` Access token received (expires in ${tokenData.expires_in}s)`);
261
-
262
- return {
263
- accessToken: tokenData.access_token,
264
- refreshToken: tokenData.refresh_token,
265
- clientId,
266
- };
267
- }
268
-
269
- // ---------------------------------------------------------------------------
270
- // Step 2: Write MCP gateway config
271
- // ---------------------------------------------------------------------------
272
-
273
- function writeMcpConfig(gatewayUrl: string, accessToken: string): void {
274
- fs.mkdirSync(OPENCLAW_DIR, { recursive: true });
275
-
276
- const config = {
277
- mcpServers: {
278
- virtueai: {
279
- type: 'http',
280
- url: `${gatewayUrl}/mcp`,
281
- headers: {
282
- Authorization: `Bearer ${accessToken}`,
283
- },
284
- },
285
- },
286
- };
287
-
288
- fs.writeFileSync(MCP_CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
289
- console.log(` Written: ${MCP_CONFIG_PATH}`);
290
- }
291
-
292
- // ---------------------------------------------------------------------------
293
- // Step 3: Patch openclaw.json
294
- // ---------------------------------------------------------------------------
295
-
296
- function patchOpenClawConfig(): void {
297
- let config: any = {};
298
-
299
- if (fs.existsSync(OPENCLAW_CONFIG_PATH)) {
300
- try {
301
- config = JSON.parse(fs.readFileSync(OPENCLAW_CONFIG_PATH, 'utf-8'));
302
- } catch {
303
- console.warn(' Warning: existing openclaw.json is invalid JSON, creating fresh config');
304
- config = {};
305
- }
306
- }
307
-
308
- // Ensure nested structure exists
309
- if (!config.agents) config.agents = {};
310
- if (!config.agents.defaults) config.agents.defaults = {};
311
-
312
- // Set model to claude-cli if not already set
313
- if (!config.agents.defaults.model) {
314
- config.agents.defaults.model = { primary: 'claude-cli/sonnet' };
315
- }
316
-
317
- // Ensure cliBackends.claude-cli exists
318
- if (!config.agents.defaults.cliBackends) config.agents.defaults.cliBackends = {};
319
- if (!config.agents.defaults.cliBackends['claude-cli']) {
320
- config.agents.defaults.cliBackends['claude-cli'] = {
321
- command: 'claude',
322
- args: ['-p', '--output-format', 'json'],
323
- };
324
- }
325
-
326
- const cliBackend = config.agents.defaults.cliBackends['claude-cli'];
327
-
328
- // Ensure args is an array
329
- if (!Array.isArray(cliBackend.args)) {
330
- cliBackend.args = ['-p', '--output-format', 'json'];
331
- }
332
-
333
- // Add or update --mcp-config
334
- const mcpIdx = cliBackend.args.indexOf('--mcp-config');
335
- if (mcpIdx !== -1) {
336
- // Update existing path
337
- cliBackend.args[mcpIdx + 1] = MCP_CONFIG_PATH;
338
- } else {
339
- // Append
340
- cliBackend.args.push('--mcp-config', MCP_CONFIG_PATH);
341
- }
342
-
343
- // Write back
344
- fs.writeFileSync(OPENCLAW_CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
345
- console.log(` Patched: ${OPENCLAW_CONFIG_PATH}`);
346
- }
347
-
348
- // ---------------------------------------------------------------------------
349
- // Step 4: Verify connection
350
- // ---------------------------------------------------------------------------
351
-
352
- async function verifyConnection(gatewayUrl: string, accessToken: string): Promise<number> {
353
- console.log(' Verifying token with gateway...');
354
- const { status, data } = await fetchJson(`${gatewayUrl}/mcp`, {
355
- method: 'POST',
356
- headers: {
357
- 'Content-Type': 'application/json',
358
- Authorization: `Bearer ${accessToken}`,
359
- },
360
- body: JSON.stringify({
361
- jsonrpc: '2.0',
362
- id: 1,
363
- method: 'tools/list',
364
- params: {},
365
- }),
366
- });
367
-
368
- if (status === 200 && data?.result?.tools) {
369
- const tools = data.result.tools;
370
- const toolCount = tools.length;
371
-
372
- // Group by prefix
373
- const groups: Record<string, number> = {};
374
- for (const t of tools) {
375
- const prefix = t.name.split('_')[0];
376
- groups[prefix] = (groups[prefix] || 0) + 1;
377
- }
378
-
379
- console.log(` Verified! ${toolCount} tools available:`);
380
- for (const [prefix, count] of Object.entries(groups).sort()) {
381
- console.log(` ${prefix}: ${count} tools`);
382
- }
383
-
384
- return toolCount;
385
- } else {
386
- console.warn(` Warning: verification returned status ${status}`);
387
- console.warn(` Response: ${JSON.stringify(data).slice(0, 200)}`);
388
- console.warn(' Token was saved but may not work yet.');
389
- return 0;
390
- }
391
- }
392
-
393
- // ---------------------------------------------------------------------------
394
- // Main
395
- // ---------------------------------------------------------------------------
396
-
397
- async function main(): Promise<void> {
398
- if (hasFlag('help') || hasFlag('h')) {
399
- console.log(`
400
- VirtueAI Gateway Connect — connect OpenClaw to VirtueAI MCP gateway
401
-
402
- Usage:
403
- npx @virtue-ai/gateway-connect [options]
404
-
405
- Options:
406
- --gateway-url <url> Gateway URL (default: ${DEFAULT_GATEWAY_URL})
407
- --help Show this help message
408
-
409
- What it does:
410
- 1. Opens browser for OAuth login
411
- 2. Saves MCP config to ~/.openclaw/mcp-gateway.json
412
- 3. Patches ~/.openclaw/openclaw.json to use the gateway
413
- 4. Verifies connection by listing available tools
414
- `);
415
- process.exit(0);
416
- }
417
-
418
- let gatewayUrl = getArg('gateway-url') || DEFAULT_GATEWAY_URL;
419
- // Strip /mcp suffix and normalize to lowercase
420
- gatewayUrl = gatewayUrl.replace(/\/mcp\/?$/, '').toLowerCase();
421
-
422
- console.log('\n VirtueAI Gateway Connect\n');
423
- console.log(` Gateway: ${gatewayUrl}`);
424
-
425
- // Step 1: Authenticate
426
- const { accessToken } = await authenticate(gatewayUrl);
427
-
428
- // Step 2: Write MCP config
429
- console.log('\n Configuring OpenClaw...');
430
- writeMcpConfig(gatewayUrl, accessToken);
431
-
432
- // Step 3: Patch openclaw.json
433
- patchOpenClawConfig();
434
-
435
- // Step 4: Verify
436
- console.log('');
437
- const toolCount = await verifyConnection(gatewayUrl, accessToken);
438
-
439
- // Done
440
- console.log(`
441
- Done! OpenClaw is now connected to VirtueAI MCP gateway.
442
- ${toolCount} tools available across the gateway.
443
-
444
- Config files:
445
- ${MCP_CONFIG_PATH}
446
- ${OPENCLAW_CONFIG_PATH}
447
-
448
- Start using it:
449
- openclaw agent --local --message "What tools do you have?"
450
- `);
451
- process.exit(0);
452
- }
453
-
454
- main().catch((err) => {
455
- console.error(`\nError: ${err.message}`);
456
- process.exit(1);
457
- });
package/tsconfig.json DELETED
@@ -1,15 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "Node16",
5
- "moduleResolution": "Node16",
6
- "outDir": "dist",
7
- "rootDir": "src",
8
- "strict": true,
9
- "esModuleInterop": true,
10
- "skipLibCheck": true,
11
- "declaration": true
12
- },
13
- "include": ["src"],
14
- "exclude": ["node_modules", "dist"]
15
- }