agentgate-mcp 0.2.0 → 0.3.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/ARCHITECTURE.md +18 -34
- package/MCP_TOOLS.md +50 -26
- package/README.md +63 -73
- package/package.json +1 -3
- package/src/browser-session.js +230 -0
- package/src/cli.js +40 -43
- package/src/config.js +1 -9
- package/src/mcp-server.js +147 -71
- package/src/orchestrator.js +54 -67
- package/services/_template.service.json +0 -34
- package/src/browser-runtime.js +0 -411
- package/src/integrations/captcha-solver.js +0 -128
- package/src/integrations/gmail-watcher.js +0 -129
- package/src/playwright-engine.js +0 -391
- package/src/registry.js +0 -47
- package/src/scaffold.js +0 -103
- package/src/setup.js +0 -109
- package/src/signup-engine.js +0 -24
- package/src/vault.js +0 -105
package/src/cli.js
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env -S node --disable-warning=ExperimentalWarning
|
|
2
2
|
|
|
3
3
|
import process from 'node:process';
|
|
4
|
+
import { execSync } from 'node:child_process';
|
|
5
|
+
import { BrowserSession } from './browser-session.js';
|
|
4
6
|
import { config, ensureDirs, fileExists } from './config.js';
|
|
5
7
|
import { KeyDatabase } from './db.js';
|
|
6
8
|
import { enableFileLogging, createLogger } from './logger.js';
|
|
7
9
|
import { McpServer } from './mcp-server.js';
|
|
8
10
|
import { Orchestrator } from './orchestrator.js';
|
|
9
|
-
import { ServiceRegistry } from './registry.js';
|
|
10
|
-
import { runScaffold } from './scaffold.js';
|
|
11
|
-
import { SignupEngine } from './signup-engine.js';
|
|
12
|
-
import { Vault } from './vault.js';
|
|
13
11
|
|
|
14
12
|
const log = createLogger('cli');
|
|
15
13
|
|
|
@@ -17,32 +15,21 @@ function bootstrap() {
|
|
|
17
15
|
ensureDirs();
|
|
18
16
|
enableFileLogging(config.LOG_DIR);
|
|
19
17
|
|
|
20
|
-
const vault = new Vault({
|
|
21
|
-
vaultFile: config.VAULT_FILE,
|
|
22
|
-
keyringFile: config.KEYRING_FILE
|
|
23
|
-
});
|
|
24
|
-
vault.initialize();
|
|
25
|
-
|
|
26
18
|
const db = new KeyDatabase(config.DB_FILE);
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
browserProfileDir: config.BROWSER_PROFILE_DIR
|
|
32
|
-
});
|
|
33
|
-
const orchestrator = new Orchestrator({ db, registry, signupEngine: automationEngine });
|
|
34
|
-
|
|
35
|
-
return { vault, db, orchestrator, registry, automationEngine };
|
|
19
|
+
const session = new BrowserSession({ browserProfileDir: config.BROWSER_PROFILE_DIR });
|
|
20
|
+
const orchestrator = new Orchestrator({ db, browserSession: session });
|
|
21
|
+
|
|
22
|
+
return { db, session, orchestrator };
|
|
36
23
|
}
|
|
37
24
|
|
|
38
25
|
async function main() {
|
|
39
26
|
const command = process.argv[2] ?? 'serve';
|
|
40
|
-
const {
|
|
27
|
+
const { db, session, orchestrator } = bootstrap();
|
|
41
28
|
|
|
42
29
|
switch (command) {
|
|
43
30
|
case 'login':
|
|
44
|
-
await
|
|
45
|
-
process.stdout.write('\nGoogle session saved. AgentGate can now
|
|
31
|
+
await session.login();
|
|
32
|
+
process.stdout.write('\nGoogle session saved. AgentGate can now open browsers with your Google account.\n');
|
|
46
33
|
process.stdout.write('Start the MCP server with: agentgate serve\n\n');
|
|
47
34
|
break;
|
|
48
35
|
|
|
@@ -52,42 +39,52 @@ async function main() {
|
|
|
52
39
|
break;
|
|
53
40
|
|
|
54
41
|
case 'doctor':
|
|
55
|
-
await runDoctor({
|
|
42
|
+
await runDoctor({ db });
|
|
56
43
|
break;
|
|
57
44
|
|
|
58
|
-
case '
|
|
59
|
-
|
|
60
|
-
registryDir: config.REGISTRY_DIR,
|
|
61
|
-
argv: process.argv.slice(3),
|
|
62
|
-
stdout: process.stdout,
|
|
63
|
-
stderr: process.stderr
|
|
64
|
-
});
|
|
65
|
-
process.exitCode = code;
|
|
45
|
+
case 'setup':
|
|
46
|
+
runSetupClaude();
|
|
66
47
|
break;
|
|
67
|
-
}
|
|
68
48
|
|
|
69
49
|
default:
|
|
70
|
-
process.stderr.write(`Unknown command: ${command}\n`);
|
|
71
|
-
process.stderr.write('
|
|
50
|
+
process.stderr.write(`Unknown command: ${command}\n\n`);
|
|
51
|
+
process.stderr.write('Usage:\n');
|
|
72
52
|
process.stderr.write(' agentgate login Sign in with Google (opens browser)\n');
|
|
53
|
+
process.stderr.write(' agentgate setup Add to Claude Code + auto-approve tools\n');
|
|
73
54
|
process.stderr.write(' agentgate serve Start MCP server\n');
|
|
74
|
-
process.stderr.write(' agentgate doctor Health check\n');
|
|
75
|
-
process.stderr.write(' agentgate scaffold Generate a service recipe\n\n');
|
|
55
|
+
process.stderr.write(' agentgate doctor Health check\n\n');
|
|
76
56
|
process.exitCode = 1;
|
|
77
57
|
}
|
|
78
58
|
}
|
|
79
59
|
|
|
80
|
-
|
|
81
|
-
const
|
|
60
|
+
function runSetupClaude() {
|
|
61
|
+
const steps = [
|
|
62
|
+
{ cmd: 'claude mcp add agentgate -- agentgate serve', label: 'Adding MCP server to Claude Code' },
|
|
63
|
+
{ cmd: 'claude config add allowedTools "mcp__agentgate__*"', label: 'Auto-approving AgentGate tools' }
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
for (const { cmd, label } of steps) {
|
|
67
|
+
process.stdout.write(`${label}...\n`);
|
|
68
|
+
try {
|
|
69
|
+
execSync(cmd, { stdio: 'inherit' });
|
|
70
|
+
} catch {
|
|
71
|
+
process.stderr.write(`Failed: ${cmd}\n`);
|
|
72
|
+
process.stderr.write('Make sure Claude Code CLI is installed: https://claude.ai/claude-code\n');
|
|
73
|
+
process.exitCode = 1;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
82
77
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
78
|
+
process.stdout.write('\nDone! AgentGate is ready. Tools will run without approval prompts.\n');
|
|
79
|
+
process.stdout.write('Next: agentgate login\n\n');
|
|
80
|
+
}
|
|
86
81
|
|
|
87
|
-
|
|
88
|
-
checks
|
|
82
|
+
async function runDoctor({ db }) {
|
|
83
|
+
const checks = {};
|
|
89
84
|
|
|
85
|
+
checks.googleLoggedIn = fileExists(config.BROWSER_PROFILE_DIR);
|
|
90
86
|
checks.dbFile = config.DB_FILE;
|
|
87
|
+
|
|
91
88
|
checks.dbAccessible = true;
|
|
92
89
|
try { db.getAllKeys(); } catch { checks.dbAccessible = false; }
|
|
93
90
|
|
package/src/config.js
CHANGED
|
@@ -20,22 +20,14 @@ const APP_DIR = resolveAppDir();
|
|
|
20
20
|
const DATA_DIR = path.join(APP_DIR, 'data');
|
|
21
21
|
const LOG_DIR = path.join(APP_DIR, 'logs');
|
|
22
22
|
const BROWSER_PROFILE_DIR = path.join(APP_DIR, 'browser-profile');
|
|
23
|
-
const REGISTRY_DIR = path.resolve(process.cwd(), 'services');
|
|
24
|
-
const VAULT_FILE = path.join(APP_DIR, 'vault.enc');
|
|
25
|
-
const KEYRING_FILE = path.join(APP_DIR, 'keyring.json');
|
|
26
23
|
const DB_FILE = path.join(DATA_DIR, 'agentgate.sqlite');
|
|
27
|
-
const SETTINGS_FILE = path.join(APP_DIR, 'settings.json');
|
|
28
24
|
|
|
29
25
|
export const config = {
|
|
30
26
|
APP_DIR,
|
|
31
27
|
DATA_DIR,
|
|
32
28
|
LOG_DIR,
|
|
33
29
|
BROWSER_PROFILE_DIR,
|
|
34
|
-
|
|
35
|
-
VAULT_FILE,
|
|
36
|
-
KEYRING_FILE,
|
|
37
|
-
DB_FILE,
|
|
38
|
-
SETTINGS_FILE
|
|
30
|
+
DB_FILE
|
|
39
31
|
};
|
|
40
32
|
|
|
41
33
|
export function ensureDirs() {
|
package/src/mcp-server.js
CHANGED
|
@@ -8,8 +8,7 @@ const log = createLogger('mcp-server');
|
|
|
8
8
|
function loadVersion() {
|
|
9
9
|
try {
|
|
10
10
|
const pkgPath = path.resolve(import.meta.dirname, '..', 'package.json');
|
|
11
|
-
|
|
12
|
-
return pkg.version || '0.0.0';
|
|
11
|
+
return JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version || '0.0.0';
|
|
13
12
|
} catch {
|
|
14
13
|
return '0.0.0';
|
|
15
14
|
}
|
|
@@ -21,63 +20,113 @@ const TOOL_DEFS = [
|
|
|
21
20
|
{
|
|
22
21
|
name: 'get_or_create_key',
|
|
23
22
|
description:
|
|
24
|
-
'
|
|
25
|
-
'
|
|
26
|
-
'just provide the service name and signup URL.',
|
|
23
|
+
'Check if AgentGate has a cached API key for a service. ' +
|
|
24
|
+
'Returns the key if cached, or tells you no key exists so you can use open_browser to go get one.',
|
|
27
25
|
inputSchema: {
|
|
28
26
|
type: 'object',
|
|
29
27
|
properties: {
|
|
30
|
-
service: {
|
|
28
|
+
service: { type: 'string', description: 'Service name, e.g. "openai", "twelvelabs"' }
|
|
29
|
+
},
|
|
30
|
+
required: ['service']
|
|
31
|
+
},
|
|
32
|
+
annotations: { title: 'Check cached API key', readOnlyHint: true, destructiveHint: false, openWorldHint: false }
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'open_browser',
|
|
36
|
+
description:
|
|
37
|
+
'Open a browser with the saved Google session and navigate to a URL. ' +
|
|
38
|
+
'Returns a screenshot of the page so you can see what is on screen and decide what to do next. ' +
|
|
39
|
+
'The user must have run `agentgate login` first.',
|
|
40
|
+
inputSchema: {
|
|
41
|
+
type: 'object',
|
|
42
|
+
properties: {
|
|
43
|
+
url: { type: 'string', description: 'URL to navigate to' }
|
|
44
|
+
},
|
|
45
|
+
required: ['url']
|
|
46
|
+
},
|
|
47
|
+
annotations: { title: 'Open browser', readOnlyHint: false, destructiveHint: false, openWorldHint: true }
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'browser_action',
|
|
51
|
+
description:
|
|
52
|
+
'Perform an action in the open browser and get a screenshot back. ' +
|
|
53
|
+
'Actions: click, fill, select, press, scroll, goto, wait, screenshot, extract_text, extract_all_text. ' +
|
|
54
|
+
'Use the screenshot to see the result and decide your next step.',
|
|
55
|
+
inputSchema: {
|
|
56
|
+
type: 'object',
|
|
57
|
+
properties: {
|
|
58
|
+
action: {
|
|
59
|
+
type: 'string',
|
|
60
|
+
description: 'Action to perform',
|
|
61
|
+
enum: ['click', 'fill', 'select', 'press', 'scroll', 'goto', 'wait', 'screenshot', 'extract_text', 'extract_all_text']
|
|
62
|
+
},
|
|
63
|
+
selector: {
|
|
31
64
|
type: 'string',
|
|
32
|
-
description: '
|
|
65
|
+
description: 'CSS selector or Playwright selector (e.g. "text=Sign in with Google", "#submit-btn", "input[name=email]")'
|
|
33
66
|
},
|
|
34
|
-
|
|
67
|
+
value: {
|
|
35
68
|
type: 'string',
|
|
36
|
-
description: '
|
|
69
|
+
description: 'Value for fill/select actions, or scroll distance in pixels'
|
|
37
70
|
},
|
|
38
|
-
|
|
71
|
+
key: {
|
|
39
72
|
type: 'string',
|
|
40
|
-
description: '
|
|
73
|
+
description: 'Key to press (e.g. "Enter", "Tab")'
|
|
74
|
+
},
|
|
75
|
+
url: {
|
|
76
|
+
type: 'string',
|
|
77
|
+
description: 'URL for goto action'
|
|
78
|
+
},
|
|
79
|
+
ms: {
|
|
80
|
+
type: 'number',
|
|
81
|
+
description: 'Milliseconds for wait action (default 5000)'
|
|
41
82
|
}
|
|
42
83
|
},
|
|
43
|
-
required: ['
|
|
44
|
-
}
|
|
84
|
+
required: ['action']
|
|
85
|
+
},
|
|
86
|
+
annotations: { title: 'Browser action', readOnlyHint: false, destructiveHint: false, openWorldHint: true }
|
|
45
87
|
},
|
|
46
88
|
{
|
|
47
|
-
name: '
|
|
48
|
-
description: '
|
|
89
|
+
name: 'save_key',
|
|
90
|
+
description: 'Save an API key you found on the page to AgentGate\'s local database for future use.',
|
|
91
|
+
inputSchema: {
|
|
92
|
+
type: 'object',
|
|
93
|
+
properties: {
|
|
94
|
+
service: { type: 'string', description: 'Service name to store the key under' },
|
|
95
|
+
api_key: { type: 'string', description: 'The API key value to store' }
|
|
96
|
+
},
|
|
97
|
+
required: ['service', 'api_key']
|
|
98
|
+
},
|
|
99
|
+
annotations: { title: 'Save API key', readOnlyHint: false, destructiveHint: false, openWorldHint: false }
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'close_browser',
|
|
103
|
+
description: 'Close the browser session when you are done.',
|
|
49
104
|
inputSchema: {
|
|
50
105
|
type: 'object',
|
|
51
106
|
properties: {}
|
|
52
|
-
}
|
|
107
|
+
},
|
|
108
|
+
annotations: { title: 'Close browser', readOnlyHint: false, destructiveHint: false, openWorldHint: false }
|
|
53
109
|
},
|
|
54
110
|
{
|
|
55
|
-
name: '
|
|
56
|
-
description: '
|
|
111
|
+
name: 'list_my_keys',
|
|
112
|
+
description: 'List all API keys stored by AgentGate.',
|
|
57
113
|
inputSchema: {
|
|
58
114
|
type: 'object',
|
|
59
|
-
properties: {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
description: 'Service name to revoke the key for'
|
|
63
|
-
}
|
|
64
|
-
},
|
|
65
|
-
required: ['service']
|
|
66
|
-
}
|
|
115
|
+
properties: {}
|
|
116
|
+
},
|
|
117
|
+
annotations: { title: 'List API keys', readOnlyHint: true, destructiveHint: false, openWorldHint: false }
|
|
67
118
|
},
|
|
68
119
|
{
|
|
69
|
-
name: '
|
|
70
|
-
description: '
|
|
120
|
+
name: 'revoke_key',
|
|
121
|
+
description: 'Delete a stored API key from AgentGate.',
|
|
71
122
|
inputSchema: {
|
|
72
123
|
type: 'object',
|
|
73
124
|
properties: {
|
|
74
|
-
service: {
|
|
75
|
-
type: 'string',
|
|
76
|
-
description: 'Service name to check'
|
|
77
|
-
}
|
|
125
|
+
service: { type: 'string', description: 'Service name to revoke' }
|
|
78
126
|
},
|
|
79
127
|
required: ['service']
|
|
80
|
-
}
|
|
128
|
+
},
|
|
129
|
+
annotations: { title: 'Revoke API key', readOnlyHint: false, destructiveHint: true, openWorldHint: false }
|
|
81
130
|
}
|
|
82
131
|
];
|
|
83
132
|
|
|
@@ -85,14 +134,10 @@ export class McpServer {
|
|
|
85
134
|
constructor({ orchestrator }) {
|
|
86
135
|
this.orchestrator = orchestrator;
|
|
87
136
|
this.queue = Promise.resolve();
|
|
88
|
-
this.initialized = false;
|
|
89
137
|
}
|
|
90
138
|
|
|
91
139
|
start() {
|
|
92
|
-
const rl = readline.createInterface({
|
|
93
|
-
input: process.stdin,
|
|
94
|
-
crlfDelay: Infinity
|
|
95
|
-
});
|
|
140
|
+
const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
96
141
|
|
|
97
142
|
rl.on('line', (line) => {
|
|
98
143
|
this.queue = this.queue.then(() => this.processLine(line));
|
|
@@ -100,6 +145,7 @@ export class McpServer {
|
|
|
100
145
|
|
|
101
146
|
rl.on('close', () => {
|
|
102
147
|
log.info('stdin closed, shutting down');
|
|
148
|
+
this.orchestrator.closeBrowser().catch(() => {});
|
|
103
149
|
});
|
|
104
150
|
|
|
105
151
|
log.info('MCP server started', { version: VERSION });
|
|
@@ -113,11 +159,7 @@ export class McpServer {
|
|
|
113
159
|
try {
|
|
114
160
|
request = JSON.parse(trimmed);
|
|
115
161
|
} catch {
|
|
116
|
-
this.write({
|
|
117
|
-
jsonrpc: '2.0',
|
|
118
|
-
error: { code: -32700, message: 'Parse error' },
|
|
119
|
-
id: null
|
|
120
|
-
});
|
|
162
|
+
this.write({ jsonrpc: '2.0', error: { code: -32700, message: 'Parse error' }, id: null });
|
|
121
163
|
return;
|
|
122
164
|
}
|
|
123
165
|
|
|
@@ -132,23 +174,13 @@ export class McpServer {
|
|
|
132
174
|
} catch (error) {
|
|
133
175
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
134
176
|
log.error(`Request failed: ${request.method}`, { error: message });
|
|
135
|
-
this.write({
|
|
136
|
-
jsonrpc: '2.0',
|
|
137
|
-
id: request.id,
|
|
138
|
-
error: { code: -32000, message }
|
|
139
|
-
});
|
|
177
|
+
this.write({ jsonrpc: '2.0', id: request.id, error: { code: -32000, message } });
|
|
140
178
|
}
|
|
141
179
|
}
|
|
142
180
|
|
|
143
181
|
handleNotification(request) {
|
|
144
|
-
|
|
145
|
-
if (method === 'notifications/initialized') {
|
|
146
|
-
this.initialized = true;
|
|
182
|
+
if (request.method === 'notifications/initialized') {
|
|
147
183
|
log.info('Client initialized');
|
|
148
|
-
} else if (method === 'notifications/cancelled') {
|
|
149
|
-
log.info('Client cancelled request', { params: request.params });
|
|
150
|
-
} else {
|
|
151
|
-
log.debug(`Unhandled notification: ${method}`);
|
|
152
184
|
}
|
|
153
185
|
}
|
|
154
186
|
|
|
@@ -163,21 +195,14 @@ export class McpServer {
|
|
|
163
195
|
};
|
|
164
196
|
}
|
|
165
197
|
|
|
166
|
-
if (method === 'ping') {
|
|
167
|
-
return {};
|
|
168
|
-
}
|
|
198
|
+
if (method === 'ping') return {};
|
|
169
199
|
|
|
170
|
-
if (method === 'tools/list') {
|
|
171
|
-
return { tools: TOOL_DEFS };
|
|
172
|
-
}
|
|
200
|
+
if (method === 'tools/list') return { tools: TOOL_DEFS };
|
|
173
201
|
|
|
174
202
|
if (method === 'tools/call') {
|
|
175
203
|
const { name, arguments: args } = params || {};
|
|
176
|
-
log.info(`
|
|
177
|
-
|
|
178
|
-
return {
|
|
179
|
-
content: [{ type: 'text', text: JSON.stringify(output) }]
|
|
180
|
-
};
|
|
204
|
+
log.info(`Tool call: ${name}`);
|
|
205
|
+
return this.callTool(name, args || {});
|
|
181
206
|
}
|
|
182
207
|
|
|
183
208
|
throw new Error(`Unsupported method: ${method}`);
|
|
@@ -186,18 +211,69 @@ export class McpServer {
|
|
|
186
211
|
async callTool(name, args) {
|
|
187
212
|
switch (name) {
|
|
188
213
|
case 'get_or_create_key':
|
|
189
|
-
return this.orchestrator.
|
|
214
|
+
return this.formatText(this.orchestrator.getKey(args.service));
|
|
215
|
+
|
|
216
|
+
case 'open_browser': {
|
|
217
|
+
const result = await this.orchestrator.openBrowser(args.url);
|
|
218
|
+
return this.formatBrowserResult(result);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
case 'browser_action': {
|
|
222
|
+
const result = await this.orchestrator.browserAction(args);
|
|
223
|
+
return this.formatBrowserResult(result);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
case 'save_key':
|
|
227
|
+
return this.formatText(this.orchestrator.saveKey(args.service, args.api_key));
|
|
228
|
+
|
|
229
|
+
case 'close_browser':
|
|
230
|
+
await this.orchestrator.closeBrowser();
|
|
231
|
+
return this.formatText({ status: 'browser closed' });
|
|
232
|
+
|
|
190
233
|
case 'list_my_keys':
|
|
191
|
-
return this.orchestrator.getAllMyKeys();
|
|
234
|
+
return this.formatText(this.orchestrator.getAllMyKeys());
|
|
235
|
+
|
|
192
236
|
case 'revoke_key':
|
|
193
|
-
return this.orchestrator.revokeKey(args.service);
|
|
194
|
-
|
|
195
|
-
return this.orchestrator.checkKeyStatus(args.service);
|
|
237
|
+
return this.formatText(this.orchestrator.revokeKey(args.service));
|
|
238
|
+
|
|
196
239
|
default:
|
|
197
240
|
throw new Error(`Unknown tool: ${name}`);
|
|
198
241
|
}
|
|
199
242
|
}
|
|
200
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Format a browser result with screenshot as image content.
|
|
246
|
+
*/
|
|
247
|
+
formatBrowserResult(result) {
|
|
248
|
+
const content = [];
|
|
249
|
+
|
|
250
|
+
if (result.screenshot) {
|
|
251
|
+
content.push({
|
|
252
|
+
type: 'image',
|
|
253
|
+
data: result.screenshot,
|
|
254
|
+
mimeType: 'image/png'
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Include text metadata
|
|
259
|
+
const meta = { url: result.url, title: result.title };
|
|
260
|
+
if (result.extracted_text) {
|
|
261
|
+
meta.extracted_text = result.extracted_text;
|
|
262
|
+
}
|
|
263
|
+
content.push({ type: 'text', text: JSON.stringify(meta) });
|
|
264
|
+
|
|
265
|
+
return { content };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Format a plain JSON result as text content.
|
|
270
|
+
*/
|
|
271
|
+
formatText(data) {
|
|
272
|
+
return {
|
|
273
|
+
content: [{ type: 'text', text: JSON.stringify(data) }]
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
201
277
|
write(payload) {
|
|
202
278
|
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
203
279
|
}
|
package/src/orchestrator.js
CHANGED
|
@@ -3,52 +3,20 @@ import { createLogger } from './logger.js';
|
|
|
3
3
|
const log = createLogger('orchestrator');
|
|
4
4
|
|
|
5
5
|
export class Orchestrator {
|
|
6
|
-
constructor({ db,
|
|
6
|
+
constructor({ db, browserSession }) {
|
|
7
7
|
this.db = db;
|
|
8
|
-
this.
|
|
9
|
-
this.signupEngine = signupEngine;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
getAllMyKeys() {
|
|
13
|
-
return this.db.getAllKeys().map((row) => ({
|
|
14
|
-
service: row.service,
|
|
15
|
-
api_key: row.api_key,
|
|
16
|
-
status: row.status,
|
|
17
|
-
created_at: row.created_at,
|
|
18
|
-
updated_at: row.updated_at
|
|
19
|
-
}));
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
checkKeyStatus(service) {
|
|
23
|
-
const existing = this.db.getActiveKey(service);
|
|
24
|
-
if (!existing) {
|
|
25
|
-
return { service, exists: false, status: 'missing' };
|
|
26
|
-
}
|
|
27
|
-
return {
|
|
28
|
-
service,
|
|
29
|
-
exists: true,
|
|
30
|
-
status: existing.status,
|
|
31
|
-
created_at: existing.created_at,
|
|
32
|
-
updated_at: existing.updated_at
|
|
33
|
-
};
|
|
8
|
+
this.session = browserSession;
|
|
34
9
|
}
|
|
35
10
|
|
|
36
11
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* @param {object} opts
|
|
40
|
-
* @param {string} opts.service - Service name (e.g. "openai")
|
|
41
|
-
* @param {string} opts.signup_url - Signup or login URL
|
|
42
|
-
* @param {string} [opts.api_key_url] - Direct URL to API keys dashboard
|
|
12
|
+
* Check DB for a cached key. Does NOT create one.
|
|
43
13
|
*/
|
|
44
|
-
|
|
14
|
+
getKey(service) {
|
|
45
15
|
if (!service) throw new Error('service is required');
|
|
46
|
-
if (!signup_url) throw new Error('signup_url is required');
|
|
47
16
|
|
|
48
|
-
// Check cache first
|
|
49
17
|
const existing = this.db.getActiveKey(service);
|
|
50
18
|
if (existing) {
|
|
51
|
-
log.info(`
|
|
19
|
+
log.info(`Cache hit for ${service}`);
|
|
52
20
|
return {
|
|
53
21
|
service,
|
|
54
22
|
api_key: existing.api_key,
|
|
@@ -57,51 +25,70 @@ export class Orchestrator {
|
|
|
57
25
|
};
|
|
58
26
|
}
|
|
59
27
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
name: service,
|
|
67
|
-
signup_url,
|
|
68
|
-
api_key_url: api_key_url || null,
|
|
69
|
-
workflow: [] // empty = smart navigation mode
|
|
70
|
-
};
|
|
71
|
-
} else {
|
|
72
|
-
// Recipe exists — override URLs if caller provided them
|
|
73
|
-
if (signup_url) serviceDef.signup_url = signup_url;
|
|
74
|
-
if (api_key_url) serviceDef.api_key_url = api_key_url;
|
|
75
|
-
}
|
|
28
|
+
return {
|
|
29
|
+
service,
|
|
30
|
+
exists: false,
|
|
31
|
+
message: `No key cached for "${service}". Use open_browser to navigate to the service and get one.`
|
|
32
|
+
};
|
|
33
|
+
}
|
|
76
34
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Save a key the agent found on a page.
|
|
37
|
+
*/
|
|
38
|
+
saveKey(service, apiKey) {
|
|
39
|
+
if (!service) throw new Error('service is required');
|
|
40
|
+
if (!apiKey) throw new Error('api_key is required');
|
|
82
41
|
|
|
83
|
-
const fresh = await this.signupEngine.createKey(serviceDef);
|
|
84
42
|
const stored = this.db.upsertActiveKey({
|
|
85
43
|
service,
|
|
86
|
-
apiKey
|
|
87
|
-
metadata:
|
|
44
|
+
apiKey,
|
|
45
|
+
metadata: { savedAt: new Date().toISOString() }
|
|
88
46
|
});
|
|
89
47
|
|
|
90
|
-
log.info(`Key
|
|
91
|
-
|
|
48
|
+
log.info(`Key saved for ${service}`);
|
|
92
49
|
return {
|
|
93
50
|
service,
|
|
94
51
|
api_key: stored.api_key,
|
|
95
|
-
|
|
52
|
+
status: 'saved',
|
|
96
53
|
created_at: stored.created_at
|
|
97
54
|
};
|
|
98
55
|
}
|
|
99
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Open browser with Google session.
|
|
59
|
+
*/
|
|
60
|
+
async openBrowser(url) {
|
|
61
|
+
if (!url) throw new Error('url is required');
|
|
62
|
+
return this.session.open(url);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Perform a browser action.
|
|
67
|
+
*/
|
|
68
|
+
async browserAction(params) {
|
|
69
|
+
return this.session.action(params);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Close browser.
|
|
74
|
+
*/
|
|
75
|
+
async closeBrowser() {
|
|
76
|
+
await this.session.close();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
getAllMyKeys() {
|
|
80
|
+
return this.db.getAllKeys().map((row) => ({
|
|
81
|
+
service: row.service,
|
|
82
|
+
api_key: row.api_key,
|
|
83
|
+
status: row.status,
|
|
84
|
+
created_at: row.created_at,
|
|
85
|
+
updated_at: row.updated_at
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
|
|
100
89
|
revokeKey(service) {
|
|
101
90
|
const changed = this.db.revokeKey(service);
|
|
102
|
-
if (changed) {
|
|
103
|
-
log.info(`Revoked key for ${service}`);
|
|
104
|
-
}
|
|
91
|
+
if (changed) log.info(`Revoked key for ${service}`);
|
|
105
92
|
return { service, revoked: changed };
|
|
106
93
|
}
|
|
107
94
|
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "replace_me",
|
|
3
|
-
"signup_url": "https://example.com/signup",
|
|
4
|
-
"requires": {
|
|
5
|
-
"google": false,
|
|
6
|
-
"card": false
|
|
7
|
-
},
|
|
8
|
-
"runtime": {
|
|
9
|
-
"headless": true
|
|
10
|
-
},
|
|
11
|
-
"workflow": [
|
|
12
|
-
{ "type": "goto", "url": "{{service.signup_url}}" },
|
|
13
|
-
{ "type": "wait_for", "selector": "body" },
|
|
14
|
-
|
|
15
|
-
{ "type": "fill", "selector": "input[name='email']", "value": "{{generated.emailAlias}}" },
|
|
16
|
-
{ "type": "fill", "selector": "input[name='password']", "value": "{{generated.password}}" },
|
|
17
|
-
{ "type": "click", "selector": "button[type='submit']" },
|
|
18
|
-
{ "type": "store_alias", "emailPath": "generated.emailAlias", "passwordPath": "generated.password" },
|
|
19
|
-
|
|
20
|
-
{ "type": "wait_for_email_code", "target": "scratch.emailCode", "timeoutMs": 120000 },
|
|
21
|
-
{ "type": "fill", "selector": "input[name='verification_code']", "value": "{{scratch.emailCode}}" },
|
|
22
|
-
{ "type": "click", "selector": "button[type='submit']" },
|
|
23
|
-
|
|
24
|
-
{ "type": "solve_captcha", "target": "scratch.captchaToken", "provider": "capsolver" },
|
|
25
|
-
{ "type": "fill_card" },
|
|
26
|
-
|
|
27
|
-
{ "type": "goto", "url": "https://example.com/dashboard/api-keys" },
|
|
28
|
-
{ "type": "click", "selector": "text=Create API Key" },
|
|
29
|
-
|
|
30
|
-
{ "type": "extract_text", "selector": "body", "target": "scratch.pageText" },
|
|
31
|
-
{ "type": "regex_extract", "source": "scratch.pageText", "pattern": "api[_-]?key[:\\s]+([A-Za-z0-9_\\-]{20,})", "target": "result.apiKey" },
|
|
32
|
-
{ "type": "assert_present", "path": "result.apiKey", "message": "API key not found in page output" }
|
|
33
|
-
]
|
|
34
|
-
}
|