agentgate-mcp 0.2.0 → 0.3.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/ARCHITECTURE.md +18 -34
- package/MCP_TOOLS.md +50 -26
- package/README.md +54 -75
- package/package.json +1 -3
- package/src/browser-session.js +230 -0
- package/src/cli.js +14 -45
- package/src/config.js +1 -9
- package/src/mcp-server.js +136 -67
- 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,12 @@
|
|
|
1
1
|
#!/usr/bin/env -S node --disable-warning=ExperimentalWarning
|
|
2
2
|
|
|
3
3
|
import process from 'node:process';
|
|
4
|
+
import { BrowserSession } from './browser-session.js';
|
|
4
5
|
import { config, ensureDirs, fileExists } from './config.js';
|
|
5
6
|
import { KeyDatabase } from './db.js';
|
|
6
7
|
import { enableFileLogging, createLogger } from './logger.js';
|
|
7
8
|
import { McpServer } from './mcp-server.js';
|
|
8
9
|
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
10
|
|
|
14
11
|
const log = createLogger('cli');
|
|
15
12
|
|
|
@@ -17,32 +14,21 @@ function bootstrap() {
|
|
|
17
14
|
ensureDirs();
|
|
18
15
|
enableFileLogging(config.LOG_DIR);
|
|
19
16
|
|
|
20
|
-
const vault = new Vault({
|
|
21
|
-
vaultFile: config.VAULT_FILE,
|
|
22
|
-
keyringFile: config.KEYRING_FILE
|
|
23
|
-
});
|
|
24
|
-
vault.initialize();
|
|
25
|
-
|
|
26
17
|
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 };
|
|
18
|
+
const session = new BrowserSession({ browserProfileDir: config.BROWSER_PROFILE_DIR });
|
|
19
|
+
const orchestrator = new Orchestrator({ db, browserSession: session });
|
|
20
|
+
|
|
21
|
+
return { db, session, orchestrator };
|
|
36
22
|
}
|
|
37
23
|
|
|
38
24
|
async function main() {
|
|
39
25
|
const command = process.argv[2] ?? 'serve';
|
|
40
|
-
const {
|
|
26
|
+
const { db, session, orchestrator } = bootstrap();
|
|
41
27
|
|
|
42
28
|
switch (command) {
|
|
43
29
|
case 'login':
|
|
44
|
-
await
|
|
45
|
-
process.stdout.write('\nGoogle session saved. AgentGate can now
|
|
30
|
+
await session.login();
|
|
31
|
+
process.stdout.write('\nGoogle session saved. AgentGate can now open browsers with your Google account.\n');
|
|
46
32
|
process.stdout.write('Start the MCP server with: agentgate serve\n\n');
|
|
47
33
|
break;
|
|
48
34
|
|
|
@@ -52,42 +38,25 @@ async function main() {
|
|
|
52
38
|
break;
|
|
53
39
|
|
|
54
40
|
case 'doctor':
|
|
55
|
-
await runDoctor({
|
|
56
|
-
break;
|
|
57
|
-
|
|
58
|
-
case 'scaffold': {
|
|
59
|
-
const code = runScaffold({
|
|
60
|
-
registryDir: config.REGISTRY_DIR,
|
|
61
|
-
argv: process.argv.slice(3),
|
|
62
|
-
stdout: process.stdout,
|
|
63
|
-
stderr: process.stderr
|
|
64
|
-
});
|
|
65
|
-
process.exitCode = code;
|
|
41
|
+
await runDoctor({ db });
|
|
66
42
|
break;
|
|
67
|
-
}
|
|
68
43
|
|
|
69
44
|
default:
|
|
70
|
-
process.stderr.write(`Unknown command: ${command}\n`);
|
|
71
|
-
process.stderr.write('
|
|
45
|
+
process.stderr.write(`Unknown command: ${command}\n\n`);
|
|
46
|
+
process.stderr.write('Usage:\n');
|
|
72
47
|
process.stderr.write(' agentgate login Sign in with Google (opens browser)\n');
|
|
73
48
|
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');
|
|
49
|
+
process.stderr.write(' agentgate doctor Health check\n\n');
|
|
76
50
|
process.exitCode = 1;
|
|
77
51
|
}
|
|
78
52
|
}
|
|
79
53
|
|
|
80
|
-
async function runDoctor({
|
|
54
|
+
async function runDoctor({ db }) {
|
|
81
55
|
const checks = {};
|
|
82
56
|
|
|
83
57
|
checks.googleLoggedIn = fileExists(config.BROWSER_PROFILE_DIR);
|
|
84
|
-
checks.vaultExists = fileExists(config.VAULT_FILE);
|
|
85
|
-
checks.keyringExists = fileExists(config.KEYRING_FILE);
|
|
86
|
-
|
|
87
|
-
const recipes = registry.listServices();
|
|
88
|
-
checks.serviceRecipes = recipes.length;
|
|
89
|
-
|
|
90
58
|
checks.dbFile = config.DB_FILE;
|
|
59
|
+
|
|
91
60
|
checks.dbAccessible = true;
|
|
92
61
|
try { db.getAllKeys(); } catch { checks.dbAccessible = false; }
|
|
93
62
|
|
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,60 +20,103 @@ 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
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'open_browser',
|
|
35
|
+
description:
|
|
36
|
+
'Open a browser with the saved Google session and navigate to a URL. ' +
|
|
37
|
+
'Returns a screenshot of the page so you can see what is on screen and decide what to do next. ' +
|
|
38
|
+
'The user must have run `agentgate login` first.',
|
|
39
|
+
inputSchema: {
|
|
40
|
+
type: 'object',
|
|
41
|
+
properties: {
|
|
42
|
+
url: { type: 'string', description: 'URL to navigate to' }
|
|
43
|
+
},
|
|
44
|
+
required: ['url']
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'browser_action',
|
|
49
|
+
description:
|
|
50
|
+
'Perform an action in the open browser and get a screenshot back. ' +
|
|
51
|
+
'Actions: click, fill, select, press, scroll, goto, wait, screenshot, extract_text, extract_all_text. ' +
|
|
52
|
+
'Use the screenshot to see the result and decide your next step.',
|
|
53
|
+
inputSchema: {
|
|
54
|
+
type: 'object',
|
|
55
|
+
properties: {
|
|
56
|
+
action: {
|
|
31
57
|
type: 'string',
|
|
32
|
-
description: '
|
|
58
|
+
description: 'Action to perform',
|
|
59
|
+
enum: ['click', 'fill', 'select', 'press', 'scroll', 'goto', 'wait', 'screenshot', 'extract_text', 'extract_all_text']
|
|
33
60
|
},
|
|
34
|
-
|
|
61
|
+
selector: {
|
|
35
62
|
type: 'string',
|
|
36
|
-
description: '
|
|
63
|
+
description: 'CSS selector or Playwright selector (e.g. "text=Sign in with Google", "#submit-btn", "input[name=email]")'
|
|
37
64
|
},
|
|
38
|
-
|
|
65
|
+
value: {
|
|
39
66
|
type: 'string',
|
|
40
|
-
description: '
|
|
67
|
+
description: 'Value for fill/select actions, or scroll distance in pixels'
|
|
68
|
+
},
|
|
69
|
+
key: {
|
|
70
|
+
type: 'string',
|
|
71
|
+
description: 'Key to press (e.g. "Enter", "Tab")'
|
|
72
|
+
},
|
|
73
|
+
url: {
|
|
74
|
+
type: 'string',
|
|
75
|
+
description: 'URL for goto action'
|
|
76
|
+
},
|
|
77
|
+
ms: {
|
|
78
|
+
type: 'number',
|
|
79
|
+
description: 'Milliseconds for wait action (default 5000)'
|
|
41
80
|
}
|
|
42
81
|
},
|
|
43
|
-
required: ['
|
|
82
|
+
required: ['action']
|
|
44
83
|
}
|
|
45
84
|
},
|
|
46
85
|
{
|
|
47
|
-
name: '
|
|
48
|
-
description: '
|
|
86
|
+
name: 'save_key',
|
|
87
|
+
description: 'Save an API key you found on the page to AgentGate\'s local database for future use.',
|
|
88
|
+
inputSchema: {
|
|
89
|
+
type: 'object',
|
|
90
|
+
properties: {
|
|
91
|
+
service: { type: 'string', description: 'Service name to store the key under' },
|
|
92
|
+
api_key: { type: 'string', description: 'The API key value to store' }
|
|
93
|
+
},
|
|
94
|
+
required: ['service', 'api_key']
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: 'close_browser',
|
|
99
|
+
description: 'Close the browser session when you are done.',
|
|
49
100
|
inputSchema: {
|
|
50
101
|
type: 'object',
|
|
51
102
|
properties: {}
|
|
52
103
|
}
|
|
53
104
|
},
|
|
54
105
|
{
|
|
55
|
-
name: '
|
|
56
|
-
description: '
|
|
106
|
+
name: 'list_my_keys',
|
|
107
|
+
description: 'List all API keys stored by AgentGate.',
|
|
57
108
|
inputSchema: {
|
|
58
109
|
type: 'object',
|
|
59
|
-
properties: {
|
|
60
|
-
service: {
|
|
61
|
-
type: 'string',
|
|
62
|
-
description: 'Service name to revoke the key for'
|
|
63
|
-
}
|
|
64
|
-
},
|
|
65
|
-
required: ['service']
|
|
110
|
+
properties: {}
|
|
66
111
|
}
|
|
67
112
|
},
|
|
68
113
|
{
|
|
69
|
-
name: '
|
|
70
|
-
description: '
|
|
114
|
+
name: 'revoke_key',
|
|
115
|
+
description: 'Delete a stored API key from AgentGate.',
|
|
71
116
|
inputSchema: {
|
|
72
117
|
type: 'object',
|
|
73
118
|
properties: {
|
|
74
|
-
service: {
|
|
75
|
-
type: 'string',
|
|
76
|
-
description: 'Service name to check'
|
|
77
|
-
}
|
|
119
|
+
service: { type: 'string', description: 'Service name to revoke' }
|
|
78
120
|
},
|
|
79
121
|
required: ['service']
|
|
80
122
|
}
|
|
@@ -85,14 +127,10 @@ export class McpServer {
|
|
|
85
127
|
constructor({ orchestrator }) {
|
|
86
128
|
this.orchestrator = orchestrator;
|
|
87
129
|
this.queue = Promise.resolve();
|
|
88
|
-
this.initialized = false;
|
|
89
130
|
}
|
|
90
131
|
|
|
91
132
|
start() {
|
|
92
|
-
const rl = readline.createInterface({
|
|
93
|
-
input: process.stdin,
|
|
94
|
-
crlfDelay: Infinity
|
|
95
|
-
});
|
|
133
|
+
const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
96
134
|
|
|
97
135
|
rl.on('line', (line) => {
|
|
98
136
|
this.queue = this.queue.then(() => this.processLine(line));
|
|
@@ -100,6 +138,7 @@ export class McpServer {
|
|
|
100
138
|
|
|
101
139
|
rl.on('close', () => {
|
|
102
140
|
log.info('stdin closed, shutting down');
|
|
141
|
+
this.orchestrator.closeBrowser().catch(() => {});
|
|
103
142
|
});
|
|
104
143
|
|
|
105
144
|
log.info('MCP server started', { version: VERSION });
|
|
@@ -113,11 +152,7 @@ export class McpServer {
|
|
|
113
152
|
try {
|
|
114
153
|
request = JSON.parse(trimmed);
|
|
115
154
|
} catch {
|
|
116
|
-
this.write({
|
|
117
|
-
jsonrpc: '2.0',
|
|
118
|
-
error: { code: -32700, message: 'Parse error' },
|
|
119
|
-
id: null
|
|
120
|
-
});
|
|
155
|
+
this.write({ jsonrpc: '2.0', error: { code: -32700, message: 'Parse error' }, id: null });
|
|
121
156
|
return;
|
|
122
157
|
}
|
|
123
158
|
|
|
@@ -132,23 +167,13 @@ export class McpServer {
|
|
|
132
167
|
} catch (error) {
|
|
133
168
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
134
169
|
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
|
-
});
|
|
170
|
+
this.write({ jsonrpc: '2.0', id: request.id, error: { code: -32000, message } });
|
|
140
171
|
}
|
|
141
172
|
}
|
|
142
173
|
|
|
143
174
|
handleNotification(request) {
|
|
144
|
-
|
|
145
|
-
if (method === 'notifications/initialized') {
|
|
146
|
-
this.initialized = true;
|
|
175
|
+
if (request.method === 'notifications/initialized') {
|
|
147
176
|
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
177
|
}
|
|
153
178
|
}
|
|
154
179
|
|
|
@@ -163,21 +188,14 @@ export class McpServer {
|
|
|
163
188
|
};
|
|
164
189
|
}
|
|
165
190
|
|
|
166
|
-
if (method === 'ping') {
|
|
167
|
-
return {};
|
|
168
|
-
}
|
|
191
|
+
if (method === 'ping') return {};
|
|
169
192
|
|
|
170
|
-
if (method === 'tools/list') {
|
|
171
|
-
return { tools: TOOL_DEFS };
|
|
172
|
-
}
|
|
193
|
+
if (method === 'tools/list') return { tools: TOOL_DEFS };
|
|
173
194
|
|
|
174
195
|
if (method === 'tools/call') {
|
|
175
196
|
const { name, arguments: args } = params || {};
|
|
176
|
-
log.info(`
|
|
177
|
-
|
|
178
|
-
return {
|
|
179
|
-
content: [{ type: 'text', text: JSON.stringify(output) }]
|
|
180
|
-
};
|
|
197
|
+
log.info(`Tool call: ${name}`);
|
|
198
|
+
return this.callTool(name, args || {});
|
|
181
199
|
}
|
|
182
200
|
|
|
183
201
|
throw new Error(`Unsupported method: ${method}`);
|
|
@@ -186,18 +204,69 @@ export class McpServer {
|
|
|
186
204
|
async callTool(name, args) {
|
|
187
205
|
switch (name) {
|
|
188
206
|
case 'get_or_create_key':
|
|
189
|
-
return this.orchestrator.
|
|
207
|
+
return this.formatText(this.orchestrator.getKey(args.service));
|
|
208
|
+
|
|
209
|
+
case 'open_browser': {
|
|
210
|
+
const result = await this.orchestrator.openBrowser(args.url);
|
|
211
|
+
return this.formatBrowserResult(result);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
case 'browser_action': {
|
|
215
|
+
const result = await this.orchestrator.browserAction(args);
|
|
216
|
+
return this.formatBrowserResult(result);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
case 'save_key':
|
|
220
|
+
return this.formatText(this.orchestrator.saveKey(args.service, args.api_key));
|
|
221
|
+
|
|
222
|
+
case 'close_browser':
|
|
223
|
+
await this.orchestrator.closeBrowser();
|
|
224
|
+
return this.formatText({ status: 'browser closed' });
|
|
225
|
+
|
|
190
226
|
case 'list_my_keys':
|
|
191
|
-
return this.orchestrator.getAllMyKeys();
|
|
227
|
+
return this.formatText(this.orchestrator.getAllMyKeys());
|
|
228
|
+
|
|
192
229
|
case 'revoke_key':
|
|
193
|
-
return this.orchestrator.revokeKey(args.service);
|
|
194
|
-
|
|
195
|
-
return this.orchestrator.checkKeyStatus(args.service);
|
|
230
|
+
return this.formatText(this.orchestrator.revokeKey(args.service));
|
|
231
|
+
|
|
196
232
|
default:
|
|
197
233
|
throw new Error(`Unknown tool: ${name}`);
|
|
198
234
|
}
|
|
199
235
|
}
|
|
200
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Format a browser result with screenshot as image content.
|
|
239
|
+
*/
|
|
240
|
+
formatBrowserResult(result) {
|
|
241
|
+
const content = [];
|
|
242
|
+
|
|
243
|
+
if (result.screenshot) {
|
|
244
|
+
content.push({
|
|
245
|
+
type: 'image',
|
|
246
|
+
data: result.screenshot,
|
|
247
|
+
mimeType: 'image/png'
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Include text metadata
|
|
252
|
+
const meta = { url: result.url, title: result.title };
|
|
253
|
+
if (result.extracted_text) {
|
|
254
|
+
meta.extracted_text = result.extracted_text;
|
|
255
|
+
}
|
|
256
|
+
content.push({ type: 'text', text: JSON.stringify(meta) });
|
|
257
|
+
|
|
258
|
+
return { content };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Format a plain JSON result as text content.
|
|
263
|
+
*/
|
|
264
|
+
formatText(data) {
|
|
265
|
+
return {
|
|
266
|
+
content: [{ type: 'text', text: JSON.stringify(data) }]
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
201
270
|
write(payload) {
|
|
202
271
|
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
203
272
|
}
|
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
|
-
}
|