@virtue-ai/gateway-connect 0.2.0 → 0.2.1

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
@@ -28,11 +28,23 @@ This will:
28
28
 
29
29
  ### Step 3: Start Using
30
30
 
31
+ One-shot mode:
32
+
31
33
  ```bash
32
34
  openclaw agent --local --session-id demo --message "What tools do you have?"
33
35
  ```
34
36
 
35
- That's it. OpenClaw now has access to all MCP tools on the gateway (GitHub, Google Workspace, Gmail, Calendar, Slack, PayPal, HR, Firebase, BigQuery, Brave Search, Chrome DevTools, and more).
37
+ Interactive TUI (two terminals):
38
+
39
+ ```bash
40
+ # Terminal 1: start the gateway
41
+ openclaw gateway
42
+
43
+ # Terminal 2: open TUI
44
+ openclaw tui
45
+ ```
46
+
47
+ OpenClaw now has access to all MCP tools on the gateway (GitHub, Google Workspace, Gmail, Calendar, Slack, PayPal, HR, Firebase, BigQuery, Brave Search, Chrome DevTools, and more).
36
48
 
37
49
  ## What It Does
38
50
 
package/dist/index.js CHANGED
@@ -30,6 +30,7 @@ const SCOPES = 'claudeai copilot mcp:read mcp:execute mcp:access';
30
30
  const OPENCLAW_DIR = path.join(os.homedir(), '.openclaw');
31
31
  const MCP_CONFIG_PATH = path.join(OPENCLAW_DIR, 'mcp-gateway.json');
32
32
  const OPENCLAW_CONFIG_PATH = path.join(OPENCLAW_DIR, 'openclaw.json');
33
+ const MCP_PROXY_PATH = path.join(OPENCLAW_DIR, 'virtueai-mcp-proxy.mjs');
33
34
  const DEFAULT_GATEWAY_URL = 'https://virtueai-agent-gtw-l3phon63.ngrok.io';
34
35
  // ---------------------------------------------------------------------------
35
36
  // Helpers
@@ -223,22 +224,87 @@ async function authenticate(gatewayUrl) {
223
224
  // ---------------------------------------------------------------------------
224
225
  // Step 2: Write MCP gateway config
225
226
  // ---------------------------------------------------------------------------
227
+ function generateMcpProxy(gatewayUrl, accessToken) {
228
+ const source = `#!/usr/bin/env node
229
+ // Stdio-to-HTTP MCP proxy for VirtueAI gateway.
230
+ // Claude Code checks OAuth discovery on remote HTTP MCP servers, which
231
+ // conflicts with the gateway's OAuth endpoint. This proxy runs as a local
232
+ // stdio server so Claude Code skips OAuth and uses the pre-obtained token.
233
+
234
+ import { createInterface } from "readline";
235
+ import https from "https";
236
+ import http from "http";
237
+ import { URL } from "url";
238
+
239
+ const GATEWAY_MCP_URL = ${JSON.stringify(gatewayUrl + '/mcp')};
240
+ const TOKEN = ${JSON.stringify(accessToken)};
241
+
242
+ const rl = createInterface({ input: process.stdin, terminal: false });
243
+ let buf = "";
244
+
245
+ rl.on("line", (line) => {
246
+ buf += line;
247
+ let msg;
248
+ try { msg = JSON.parse(buf); } catch { return; }
249
+ buf = "";
250
+ forward(msg);
251
+ });
252
+
253
+ function forward(msg) {
254
+ const parsed = new URL(GATEWAY_MCP_URL);
255
+ const mod = parsed.protocol === "https:" ? https : http;
256
+ const body = JSON.stringify(msg);
257
+ const req = mod.request(parsed, {
258
+ method: "POST",
259
+ headers: {
260
+ "Content-Type": "application/json",
261
+ "Authorization": "Bearer " + TOKEN,
262
+ "Accept": "application/json, text/event-stream",
263
+ "Content-Length": Buffer.byteLength(body),
264
+ },
265
+ }, (res) => {
266
+ let data = "";
267
+ res.on("data", (c) => data += c);
268
+ res.on("end", () => {
269
+ if (data.trim()) {
270
+ process.stdout.write(data.trim() + "\\n");
271
+ }
272
+ });
273
+ });
274
+ req.on("error", (e) => {
275
+ if (msg.id != null) {
276
+ process.stdout.write(JSON.stringify({
277
+ jsonrpc: "2.0",
278
+ id: msg.id,
279
+ error: { code: -32000, message: "Proxy error: " + e.message },
280
+ }) + "\\n");
281
+ }
282
+ });
283
+ req.end(body);
284
+ }
285
+ `;
286
+ fs.writeFileSync(MCP_PROXY_PATH, source, { mode: 0o755 });
287
+ console.log(` Generated proxy: ${MCP_PROXY_PATH}`);
288
+ }
226
289
  function writeMcpConfig(gatewayUrl, accessToken, guardUuid) {
227
290
  fs.mkdirSync(OPENCLAW_DIR, { recursive: true });
291
+ generateMcpProxy(gatewayUrl, accessToken);
228
292
  const config = {
229
293
  mcpServers: {
230
294
  virtueai: {
231
- type: 'http',
232
- url: `${gatewayUrl}/mcp`,
233
- headers: {
234
- Authorization: `Bearer ${accessToken}`,
235
- },
295
+ type: 'stdio',
296
+ command: 'node',
297
+ args: [MCP_PROXY_PATH],
236
298
  },
237
299
  },
238
300
  trajectory: {
239
301
  gatewayUrl,
240
302
  guardUuid: guardUuid || process.env.VIRTUEAI_GUARD_UUID || '',
241
303
  },
304
+ _auth: {
305
+ gatewayUrl,
306
+ accessToken,
307
+ },
242
308
  };
243
309
  fs.writeFileSync(MCP_CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
244
310
  console.log(` Written: ${MCP_CONFIG_PATH}`);
@@ -266,28 +332,24 @@ function patchOpenClawConfig() {
266
332
  if (!config.agents.defaults.model) {
267
333
  config.agents.defaults.model = { primary: 'claude-cli/sonnet' };
268
334
  }
269
- // Ensure cliBackends.claude-cli exists
335
+ // Ensure cliBackends.claude-cli exists (don't override user's existing config)
270
336
  if (!config.agents.defaults.cliBackends)
271
337
  config.agents.defaults.cliBackends = {};
272
338
  if (!config.agents.defaults.cliBackends['claude-cli']) {
273
339
  config.agents.defaults.cliBackends['claude-cli'] = {
274
340
  command: 'claude',
275
- args: ['-p', '--output-format', 'json'],
276
341
  };
277
342
  }
278
343
  const cliBackend = config.agents.defaults.cliBackends['claude-cli'];
279
- // Ensure args is an array
344
+ // OpenClaw mergeBackendConfig: override.args ?? base.args (full replacement, not merge)
280
345
  if (!Array.isArray(cliBackend.args)) {
281
- cliBackend.args = ['-p', '--output-format', 'json'];
346
+ cliBackend.args = ['-p', '--output-format', 'json', '--permission-mode', 'bypassPermissions'];
282
347
  }
283
- // Add or update --mcp-config
284
348
  const mcpIdx = cliBackend.args.indexOf('--mcp-config');
285
349
  if (mcpIdx !== -1) {
286
- // Update existing path
287
350
  cliBackend.args[mcpIdx + 1] = MCP_CONFIG_PATH;
288
351
  }
289
352
  else {
290
- // Append
291
353
  cliBackend.args.push('--mcp-config', MCP_CONFIG_PATH);
292
354
  }
293
355
  // Write back
@@ -47,12 +47,9 @@ function loadConfig() {
47
47
  try {
48
48
  const raw = readFileSync(MCP_CONFIG_PATH, "utf-8");
49
49
  const cfg = JSON.parse(raw);
50
- const virtueai = cfg.mcpServers?.virtueai;
51
- if (!virtueai) return null;
52
50
 
53
- const authHeader = virtueai.headers?.Authorization ?? "";
54
- const token = authHeader.replace(/^Bearer\\s+/i, "");
55
- const gatewayUrl = (virtueai.url ?? "").replace(/\\/mcp\\/?$/, "");
51
+ const gatewayUrl = cfg._auth?.gatewayUrl ?? cfg.trajectory?.gatewayUrl ?? "";
52
+ const token = cfg._auth?.accessToken ?? "";
56
53
 
57
54
  const guardUuid =
58
55
  cfg.trajectory?.guardUuid ||
@@ -86,9 +83,14 @@ const plugin = {
86
83
  }
87
84
 
88
85
  let gatewaySessionId = null;
86
+ let endpointDisabled = false;
89
87
  const endpoint = config.gatewayUrl + "/api/prompt-guard/topic_guard";
90
88
 
89
+ api.logger.info("[virtueai-trajectory] Plugin registered, sending to " + config.gatewayUrl);
90
+
91
91
  async function sendStep(role, content) {
92
+ if (endpointDisabled) return;
93
+
92
94
  const body = {
93
95
  user_prompt: truncate(content),
94
96
  guard_uuid: config.guardUuid,
@@ -107,8 +109,18 @@ const plugin = {
107
109
  },
108
110
  body: JSON.stringify(body),
109
111
  });
110
- const data = await res.json();
111
112
 
113
+ if (res.status === 404) {
114
+ endpointDisabled = true;
115
+ api.logger.warn("[virtueai-trajectory] Endpoint returned 404 — trajectory recording disabled. Is the prompt-guard API deployed on the gateway?");
116
+ return;
117
+ }
118
+ if (!res.ok) {
119
+ api.logger.warn("[virtueai-trajectory] HTTP " + res.status + " from gateway");
120
+ return;
121
+ }
122
+
123
+ const data = await res.json();
112
124
  if (data?.session_id && !gatewaySessionId) {
113
125
  gatewaySessionId = data.session_id;
114
126
  api.logger.info("[virtueai-trajectory] Gateway session: " + gatewaySessionId);
@@ -162,7 +174,7 @@ export function generateTrajectoryPlugin(guardUuid) {
162
174
  fs.mkdirSync(PLUGIN_DIR, { recursive: true });
163
175
  // package.json
164
176
  const pkg = {
165
- name: '@virtue-ai/trajectory',
177
+ name: '@virtue-ai/virtueai-trajectory',
166
178
  version: '1.0.0',
167
179
  type: 'module',
168
180
  openclaw: {
@@ -170,6 +182,15 @@ export function generateTrajectoryPlugin(guardUuid) {
170
182
  },
171
183
  };
172
184
  fs.writeFileSync(path.join(PLUGIN_DIR, 'package.json'), JSON.stringify(pkg, null, 2) + '\n');
185
+ // openclaw.plugin.json (manifest required by OpenClaw)
186
+ const manifest = {
187
+ id: PLUGIN_ID,
188
+ name: 'VirtueAI Trajectory',
189
+ description: 'Sends agent trajectory steps to VirtueAI gateway for dashboard visibility',
190
+ version: '1.0.0',
191
+ configSchema: {},
192
+ };
193
+ fs.writeFileSync(path.join(PLUGIN_DIR, 'openclaw.plugin.json'), JSON.stringify(manifest, null, 2) + '\n');
173
194
  // index.ts (the actual plugin)
174
195
  fs.writeFileSync(path.join(PLUGIN_DIR, 'index.ts'), buildPluginSource());
175
196
  // Persist guard UUID in mcp-gateway.json trajectory section
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@virtue-ai/gateway-connect",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "One-command setup to connect OpenClaw to VirtueAI MCP gateway",
5
5
  "type": "module",
6
6
  "files": [