iframer-toolkit 0.8.0 → 0.9.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/dist/cli.js +4307 -3920
- package/dist/local-server.cjs +5660 -0
- package/dist/mcp-server.cjs +253 -169
- package/package.json +4 -4
package/dist/mcp-server.cjs
CHANGED
|
@@ -35,8 +35,8 @@ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
|
35
35
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
36
36
|
|
|
37
37
|
// src/mcp/helpers.ts
|
|
38
|
-
var
|
|
39
|
-
var
|
|
38
|
+
var import_path5 = __toESM(require("path"));
|
|
39
|
+
var import_os3 = __toESM(require("os"));
|
|
40
40
|
|
|
41
41
|
// src/lib/logger.ts
|
|
42
42
|
var LEVELS = { debug: 0, info: 1, warn: 2, error: 3, silent: 4 };
|
|
@@ -66,6 +66,7 @@ function createLogger(tag) {
|
|
|
66
66
|
// src/lib/auth/crypto.ts
|
|
67
67
|
var import_crypto = __toESM(require("crypto"));
|
|
68
68
|
var import_fs = __toESM(require("fs"));
|
|
69
|
+
var import_os2 = __toESM(require("os"));
|
|
69
70
|
var import_path2 = __toESM(require("path"));
|
|
70
71
|
|
|
71
72
|
// src/lib/paths.ts
|
|
@@ -79,42 +80,76 @@ function getDataDir() {
|
|
|
79
80
|
function getLocalToken() {
|
|
80
81
|
if (process.env.IFRAMER_SECRET)
|
|
81
82
|
return process.env.IFRAMER_SECRET;
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
import_fs.default.writeFileSync(file, secret, { mode: 384 });
|
|
93
|
-
return secret;
|
|
94
|
-
} catch {
|
|
95
|
-
return "iframer-local-default-token";
|
|
83
|
+
const candidates = [
|
|
84
|
+
import_path2.default.join(getDataDir(), "secret"),
|
|
85
|
+
import_path2.default.join(process.env.XDG_RUNTIME_DIR || import_os2.default.tmpdir(), "iframer-secret")
|
|
86
|
+
];
|
|
87
|
+
for (const file of candidates) {
|
|
88
|
+
try {
|
|
89
|
+
const existing = import_fs.default.readFileSync(file, "utf8").trim();
|
|
90
|
+
if (existing)
|
|
91
|
+
return existing;
|
|
92
|
+
} catch {}
|
|
96
93
|
}
|
|
94
|
+
for (const file of candidates) {
|
|
95
|
+
try {
|
|
96
|
+
import_fs.default.mkdirSync(import_path2.default.dirname(file), { recursive: true });
|
|
97
|
+
const secret = import_crypto.default.randomBytes(32).toString("hex");
|
|
98
|
+
import_fs.default.writeFileSync(file, secret, { mode: 384 });
|
|
99
|
+
return secret;
|
|
100
|
+
} catch {}
|
|
101
|
+
}
|
|
102
|
+
throw new Error("iframer: could not read or create a persistent encryption secret in any " + `writable location (${candidates.join(", ")}). Set IFRAMER_SECRET to a ` + "stable value shared between the MCP server and CLI (openssl rand -hex 32).");
|
|
97
103
|
}
|
|
98
104
|
|
|
99
105
|
// src/mcp/local-server.ts
|
|
100
106
|
var import_child_process = require("child_process");
|
|
101
107
|
var import_net = __toESM(require("net"));
|
|
108
|
+
var import_fs3 = __toESM(require("fs"));
|
|
109
|
+
var import_path4 = __toESM(require("path"));
|
|
110
|
+
|
|
111
|
+
// src/lib/browser/registry.ts
|
|
102
112
|
var import_fs2 = __toESM(require("fs"));
|
|
103
113
|
var import_path3 = __toESM(require("path"));
|
|
114
|
+
var log = createLogger("registry");
|
|
115
|
+
function serverInfoPath() {
|
|
116
|
+
return import_path3.default.join(getDataDir(), "server.json");
|
|
117
|
+
}
|
|
118
|
+
function isPidAlive(pid) {
|
|
119
|
+
if (!Number.isInteger(pid) || pid <= 1)
|
|
120
|
+
return false;
|
|
121
|
+
try {
|
|
122
|
+
process.kill(pid, 0);
|
|
123
|
+
return true;
|
|
124
|
+
} catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function readServerInfo() {
|
|
129
|
+
try {
|
|
130
|
+
const info = JSON.parse(import_fs2.default.readFileSync(serverInfoPath(), "utf8"));
|
|
131
|
+
if (!Number.isInteger(info.pid) || !Number.isInteger(info.port))
|
|
132
|
+
return null;
|
|
133
|
+
return info;
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/mcp/local-server.ts
|
|
104
140
|
var __dirname = "/Users/eduardoverona/tools/iframer-toolkit/src/mcp";
|
|
105
141
|
var BASE_PORT = parseInt(process.env.IFRAMER_LOCAL_PORT || "3022", 10);
|
|
106
142
|
var PORT_SCAN_ATTEMPTS = 200;
|
|
107
143
|
var STARTUP_TIMEOUT_MS = 15000;
|
|
108
144
|
var HEALTH_POLL_MS = 300;
|
|
145
|
+
var SPAWN_LOCK_STALE_MS = 20000;
|
|
109
146
|
|
|
110
147
|
class LocalServerManager {
|
|
111
|
-
child = null;
|
|
112
148
|
startingPromise = null;
|
|
113
|
-
port = null;
|
|
114
149
|
baseUrl = "";
|
|
115
150
|
logPath;
|
|
116
151
|
constructor() {
|
|
117
|
-
this.logPath =
|
|
152
|
+
this.logPath = import_path4.default.join(getDataDir(), "local-server.log");
|
|
118
153
|
}
|
|
119
154
|
getBaseUrl() {
|
|
120
155
|
if (!this.baseUrl) {
|
|
@@ -123,113 +158,148 @@ class LocalServerManager {
|
|
|
123
158
|
return this.baseUrl;
|
|
124
159
|
}
|
|
125
160
|
async ensureRunning() {
|
|
126
|
-
if (this.child && !this.child.killed && await this.healthCheck())
|
|
127
|
-
return;
|
|
128
161
|
if (this.startingPromise)
|
|
129
162
|
return this.startingPromise;
|
|
130
|
-
|
|
163
|
+
if (await this.adoptExisting())
|
|
164
|
+
return;
|
|
165
|
+
this.startingPromise = this.startShared().finally(() => {
|
|
131
166
|
this.startingPromise = null;
|
|
132
167
|
});
|
|
133
168
|
return this.startingPromise;
|
|
134
169
|
}
|
|
135
|
-
async
|
|
136
|
-
|
|
137
|
-
|
|
170
|
+
async adoptExisting() {
|
|
171
|
+
const info = readServerInfo();
|
|
172
|
+
if (!info || !isPidAlive(info.pid))
|
|
173
|
+
return false;
|
|
174
|
+
const url = `http://127.0.0.1:${info.port}`;
|
|
175
|
+
if (await healthCheck(url)) {
|
|
176
|
+
this.baseUrl = url;
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
async startShared() {
|
|
138
182
|
const dataDir = getDataDir();
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
183
|
+
import_fs3.default.mkdirSync(dataDir, { recursive: true });
|
|
184
|
+
const lockDir = import_path4.default.join(dataDir, "server.spawn-lock");
|
|
185
|
+
let holdingLock = false;
|
|
186
|
+
try {
|
|
187
|
+
import_fs3.default.mkdirSync(lockDir);
|
|
188
|
+
holdingLock = true;
|
|
189
|
+
} catch {
|
|
190
|
+
const stale = (() => {
|
|
191
|
+
try {
|
|
192
|
+
return Date.now() - import_fs3.default.statSync(lockDir).mtimeMs > SPAWN_LOCK_STALE_MS;
|
|
193
|
+
} catch {
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
})();
|
|
197
|
+
if (stale) {
|
|
198
|
+
try {
|
|
199
|
+
import_fs3.default.rmdirSync(lockDir);
|
|
200
|
+
import_fs3.default.mkdirSync(lockDir);
|
|
201
|
+
holdingLock = true;
|
|
202
|
+
} catch {}
|
|
203
|
+
}
|
|
157
204
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (await this.healthCheck())
|
|
205
|
+
try {
|
|
206
|
+
if (!holdingLock) {
|
|
207
|
+
const deadline2 = Date.now() + STARTUP_TIMEOUT_MS;
|
|
208
|
+
while (Date.now() < deadline2) {
|
|
209
|
+
if (await this.adoptExisting())
|
|
210
|
+
return;
|
|
211
|
+
await sleep(HEALTH_POLL_MS);
|
|
212
|
+
}
|
|
213
|
+
throw new Error("Timed out waiting for another session to start the shared iframer server.");
|
|
214
|
+
}
|
|
215
|
+
if (await this.adoptExisting())
|
|
170
216
|
return;
|
|
171
|
-
await
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
217
|
+
const port = await findFreePort(BASE_PORT, PORT_SCAN_ATTEMPTS);
|
|
218
|
+
const url = `http://127.0.0.1:${port}`;
|
|
219
|
+
const { command, args } = this.resolveRuntime();
|
|
220
|
+
const logFd = import_fs3.default.openSync(this.logPath, "a");
|
|
221
|
+
const env = {
|
|
222
|
+
...process.env,
|
|
223
|
+
PORT: String(port),
|
|
224
|
+
IFRAMER_MODE: "local",
|
|
225
|
+
IFRAMER_DATA_DIR: dataDir
|
|
226
|
+
};
|
|
227
|
+
if (process.env.IFRAMER_SECRET) {
|
|
228
|
+
env.IFRAMER_SECRET = process.env.IFRAMER_SECRET;
|
|
229
|
+
} else {
|
|
230
|
+
try {
|
|
231
|
+
const secret = import_fs3.default.readFileSync(import_path4.default.join(dataDir, "secret"), "utf8").trim();
|
|
232
|
+
if (secret)
|
|
233
|
+
env.IFRAMER_SECRET = secret;
|
|
234
|
+
} catch {}
|
|
235
|
+
}
|
|
236
|
+
const child = import_child_process.spawn(command, args, {
|
|
237
|
+
env,
|
|
238
|
+
stdio: ["ignore", logFd, logFd],
|
|
239
|
+
detached: true
|
|
240
|
+
});
|
|
241
|
+
child.unref();
|
|
242
|
+
import_fs3.default.closeSync(logFd);
|
|
243
|
+
const deadline = Date.now() + STARTUP_TIMEOUT_MS;
|
|
244
|
+
while (Date.now() < deadline) {
|
|
245
|
+
if (await healthCheck(url)) {
|
|
246
|
+
this.baseUrl = url;
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
await sleep(HEALTH_POLL_MS);
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
child.kill("SIGKILL");
|
|
253
|
+
} catch {}
|
|
254
|
+
throw new Error(`Local iframer server failed to start on port ${port} within ${STARTUP_TIMEOUT_MS}ms.
|
|
176
255
|
` + `Last log lines:
|
|
177
|
-
${
|
|
256
|
+
${this.readLogTail()}`);
|
|
257
|
+
} finally {
|
|
258
|
+
if (holdingLock) {
|
|
259
|
+
try {
|
|
260
|
+
import_fs3.default.rmdirSync(lockDir);
|
|
261
|
+
} catch {}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
178
264
|
}
|
|
179
265
|
resolveRuntime() {
|
|
180
266
|
try {
|
|
181
267
|
const bunPath = require("child_process").execSync("which bun", { encoding: "utf8" }).trim();
|
|
182
|
-
const serverTs2 =
|
|
183
|
-
if (
|
|
268
|
+
const serverTs2 = import_path4.default.join(__dirname, "..", "..", "index.ts");
|
|
269
|
+
if (import_fs3.default.existsSync(serverTs2)) {
|
|
184
270
|
return { command: bunPath, args: ["run", serverTs2] };
|
|
185
271
|
}
|
|
186
272
|
} catch {}
|
|
187
|
-
const serverCjs =
|
|
188
|
-
if (
|
|
273
|
+
const serverCjs = import_path4.default.join(__dirname, "..", "..", "dist", "local-server.cjs");
|
|
274
|
+
if (import_fs3.default.existsSync(serverCjs)) {
|
|
189
275
|
return { command: "node", args: [serverCjs] };
|
|
190
276
|
}
|
|
191
|
-
const serverTs =
|
|
277
|
+
const serverTs = import_path4.default.join(__dirname, "..", "..", "index.ts");
|
|
192
278
|
return { command: "node", args: ["--import", "tsx", serverTs] };
|
|
193
279
|
}
|
|
194
280
|
async restart() {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
await this.ensureRunning();
|
|
198
|
-
}
|
|
199
|
-
shutdown() {
|
|
200
|
-
this.kill();
|
|
201
|
-
}
|
|
202
|
-
kill() {
|
|
203
|
-
if (this.child && !this.child.killed) {
|
|
281
|
+
const info = readServerInfo();
|
|
282
|
+
if (info && this.baseUrl) {
|
|
204
283
|
try {
|
|
205
|
-
this.
|
|
284
|
+
await fetch(`${this.baseUrl}/shutdown`, { method: "POST", signal: AbortSignal.timeout(3000) });
|
|
206
285
|
} catch {}
|
|
207
|
-
const
|
|
208
|
-
|
|
286
|
+
const deadline = Date.now() + 12000;
|
|
287
|
+
while (Date.now() < deadline && isPidAlive(info.pid)) {
|
|
288
|
+
await sleep(200);
|
|
289
|
+
}
|
|
290
|
+
if (isPidAlive(info.pid)) {
|
|
209
291
|
try {
|
|
210
|
-
|
|
211
|
-
c.kill("SIGKILL");
|
|
292
|
+
process.kill(info.pid, "SIGKILL");
|
|
212
293
|
} catch {}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
this.child = null;
|
|
216
|
-
}
|
|
217
|
-
async healthCheck() {
|
|
218
|
-
if (!this.baseUrl)
|
|
219
|
-
return false;
|
|
220
|
-
try {
|
|
221
|
-
const res = await fetch(`${this.baseUrl}/health`, {
|
|
222
|
-
signal: AbortSignal.timeout(2000)
|
|
223
|
-
});
|
|
224
|
-
const data = await res.json();
|
|
225
|
-
return data.ok === true;
|
|
226
|
-
} catch {
|
|
227
|
-
return false;
|
|
294
|
+
}
|
|
228
295
|
}
|
|
296
|
+
this.baseUrl = "";
|
|
297
|
+
await this.ensureRunning();
|
|
229
298
|
}
|
|
299
|
+
shutdown() {}
|
|
230
300
|
readLogTail() {
|
|
231
301
|
try {
|
|
232
|
-
const content =
|
|
302
|
+
const content = import_fs3.default.readFileSync(this.logPath, "utf8");
|
|
233
303
|
const lines = content.trim().split(`
|
|
234
304
|
`);
|
|
235
305
|
return lines.slice(-10).join(`
|
|
@@ -242,6 +312,15 @@ ${logTail}`);
|
|
|
242
312
|
function sleep(ms) {
|
|
243
313
|
return new Promise((r) => setTimeout(r, ms));
|
|
244
314
|
}
|
|
315
|
+
async function healthCheck(baseUrl) {
|
|
316
|
+
try {
|
|
317
|
+
const res = await fetch(`${baseUrl}/health`, { signal: AbortSignal.timeout(2000) });
|
|
318
|
+
const data = await res.json();
|
|
319
|
+
return data.ok === true;
|
|
320
|
+
} catch {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
245
324
|
function findFreePort(start, attempts) {
|
|
246
325
|
return new Promise((resolve, reject) => {
|
|
247
326
|
const tryPort = (p) => {
|
|
@@ -263,8 +342,13 @@ function findFreePort(start, attempts) {
|
|
|
263
342
|
});
|
|
264
343
|
}
|
|
265
344
|
|
|
345
|
+
// src/lib/errors.ts
|
|
346
|
+
function getErrorMessage(err) {
|
|
347
|
+
return err instanceof Error ? err.message : String(err);
|
|
348
|
+
}
|
|
349
|
+
|
|
266
350
|
// src/mcp/helpers.ts
|
|
267
|
-
var
|
|
351
|
+
var log2 = createLogger("mcp");
|
|
268
352
|
var BASE_URL = process.env.IFRAMER_URL || "http://localhost:3021";
|
|
269
353
|
var IFRAMER_SECRET = process.env.IFRAMER_SECRET;
|
|
270
354
|
var IFRAMER_MODE = process.env.IFRAMER_MODE;
|
|
@@ -330,8 +414,8 @@ async function resolveScreenshotPath(url) {
|
|
|
330
414
|
if (url.startsWith("file://")) {
|
|
331
415
|
const { fileURLToPath } = await import("url");
|
|
332
416
|
const filePath2 = fileURLToPath(url);
|
|
333
|
-
const
|
|
334
|
-
if (
|
|
417
|
+
const fs5 = await import("fs");
|
|
418
|
+
if (fs5.existsSync(filePath2))
|
|
335
419
|
return filePath2;
|
|
336
420
|
return null;
|
|
337
421
|
}
|
|
@@ -339,11 +423,11 @@ async function resolveScreenshotPath(url) {
|
|
|
339
423
|
if (!res.ok)
|
|
340
424
|
return null;
|
|
341
425
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
342
|
-
const
|
|
343
|
-
const dir =
|
|
344
|
-
|
|
345
|
-
const filePath =
|
|
346
|
-
|
|
426
|
+
const fs4 = await import("fs");
|
|
427
|
+
const dir = import_path5.default.join(import_os3.default.tmpdir(), "iframer-screenshots", "screenshots");
|
|
428
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
429
|
+
const filePath = import_path5.default.join(dir, `docker-${Date.now()}.jpg`);
|
|
430
|
+
fs4.writeFileSync(filePath, buf);
|
|
347
431
|
return filePath;
|
|
348
432
|
} catch {
|
|
349
433
|
return null;
|
|
@@ -352,16 +436,13 @@ async function resolveScreenshotPath(url) {
|
|
|
352
436
|
function err(message) {
|
|
353
437
|
return { content: [{ type: "text", text: message }], isError: true };
|
|
354
438
|
}
|
|
355
|
-
function getErrorMessage(err2) {
|
|
356
|
-
return err2 instanceof Error ? err2.message : String(err2);
|
|
357
|
-
}
|
|
358
439
|
|
|
359
440
|
// src/lib/domain-modes.ts
|
|
360
|
-
var
|
|
361
|
-
var
|
|
362
|
-
var
|
|
441
|
+
var import_fs4 = __toESM(require("fs"));
|
|
442
|
+
var import_path6 = __toESM(require("path"));
|
|
443
|
+
var log3 = createLogger("domain-modes");
|
|
363
444
|
function defaultFile() {
|
|
364
|
-
return
|
|
445
|
+
return import_path6.default.join(getDataDir(), "domain-modes.json");
|
|
365
446
|
}
|
|
366
447
|
var TTL_DAYS = 14;
|
|
367
448
|
var ESCALATION_LADDER = ["headless", "docker-headful", "binary-headful"];
|
|
@@ -442,8 +523,8 @@ class DomainModeStore {
|
|
|
442
523
|
}
|
|
443
524
|
load() {
|
|
444
525
|
try {
|
|
445
|
-
if (
|
|
446
|
-
this.data = JSON.parse(
|
|
526
|
+
if (import_fs4.default.existsSync(this.filePath)) {
|
|
527
|
+
this.data = JSON.parse(import_fs4.default.readFileSync(this.filePath, "utf-8"));
|
|
447
528
|
}
|
|
448
529
|
} catch {
|
|
449
530
|
this.data = {};
|
|
@@ -451,10 +532,10 @@ class DomainModeStore {
|
|
|
451
532
|
}
|
|
452
533
|
save() {
|
|
453
534
|
try {
|
|
454
|
-
|
|
455
|
-
|
|
535
|
+
import_fs4.default.mkdirSync(import_path6.default.dirname(this.filePath), { recursive: true });
|
|
536
|
+
import_fs4.default.writeFileSync(this.filePath, JSON.stringify(this.data, null, 2));
|
|
456
537
|
} catch (err2) {
|
|
457
|
-
|
|
538
|
+
log3.error("Failed to save:", err2);
|
|
458
539
|
}
|
|
459
540
|
}
|
|
460
541
|
}
|
|
@@ -567,6 +648,9 @@ var stepSchema = import_zod2.z.discriminatedUnion("type", [
|
|
|
567
648
|
]);
|
|
568
649
|
|
|
569
650
|
// src/mcp/tools/execute.ts
|
|
651
|
+
function resultOf(r, _type) {
|
|
652
|
+
return r.result;
|
|
653
|
+
}
|
|
570
654
|
function registerExecuteTool(server) {
|
|
571
655
|
server.tool("execute", `Execute a pipeline of browser steps. Auto-starts a session if needed. Handles obstacles (captcha, cookie banners) automatically.
|
|
572
656
|
|
|
@@ -638,9 +722,9 @@ Returns: ok, completedSteps, results, obstacles, capturedApi, and on failure: er
|
|
|
638
722
|
execResult = await runWithMode(params.options?.mode);
|
|
639
723
|
} catch (execErr) {
|
|
640
724
|
const msg = execErr instanceof Error ? execErr.message : String(execErr);
|
|
641
|
-
const isCrash = /
|
|
725
|
+
const isCrash = /ECONNREFUSED|ECONNRESET|EPIPE|socket hang up|fetch failed/i.test(msg);
|
|
642
726
|
if (isCrash) {
|
|
643
|
-
|
|
727
|
+
log2.info(`Execute crashed (${msg.slice(0, 80)}), restarting local server and retrying...`);
|
|
644
728
|
try {
|
|
645
729
|
await localServer.restart();
|
|
646
730
|
} catch {}
|
|
@@ -663,7 +747,7 @@ Retry: ${retryErr instanceof Error ? retryErr.message : String(retryErr)}
|
|
|
663
747
|
for (const nextMode of escalation) {
|
|
664
748
|
if (nextMode === "docker-headful" && !dockerRunning)
|
|
665
749
|
continue;
|
|
666
|
-
|
|
750
|
+
log2.info(`Auto-escalating to ${nextMode}`);
|
|
667
751
|
execResult = await runWithMode(nextMode);
|
|
668
752
|
if (execResult.ok)
|
|
669
753
|
break;
|
|
@@ -718,17 +802,26 @@ Knowledge cache updated for ${domain}. Before the next browser run on this domai
|
|
|
718
802
|
}
|
|
719
803
|
const meaningful = (data.results || []).filter((r) => r.ok && r.result !== undefined && r.result !== null);
|
|
720
804
|
for (const r of meaningful) {
|
|
721
|
-
if (r.step
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
} else if (r.step
|
|
729
|
-
|
|
805
|
+
if (r.step.type === "snapshot") {
|
|
806
|
+
const res = resultOf(r, "snapshot");
|
|
807
|
+
if (res?.snapshot) {
|
|
808
|
+
lines.push(`
|
|
809
|
+
--- Snapshot (${res.elementCount} elements) ---`);
|
|
810
|
+
lines.push(res.snapshot);
|
|
811
|
+
}
|
|
812
|
+
} else if (r.step.type === "find") {
|
|
813
|
+
const res = resultOf(r, "find");
|
|
814
|
+
if (res?.ref) {
|
|
815
|
+
lines.push(`
|
|
816
|
+
Found: ${res.ref} ${res.role} "${res.name}" (${res.matchCount} match${res.matchCount > 1 ? "es" : ""})`);
|
|
817
|
+
}
|
|
818
|
+
} else if (r.step.type === "screenshot") {
|
|
819
|
+
const res = resultOf(r, "screenshot");
|
|
820
|
+
if (res?.refs) {
|
|
821
|
+
lines.push(`
|
|
730
822
|
--- Annotated screenshot refs ---`);
|
|
731
|
-
|
|
823
|
+
lines.push(res.refs);
|
|
824
|
+
}
|
|
732
825
|
} else if (r.result !== undefined && r.result !== null) {
|
|
733
826
|
lines.push(`
|
|
734
827
|
step ${r.stepIndex}: ${JSON.stringify(r.result)}`);
|
|
@@ -774,8 +867,8 @@ ${api.domain} (${api.baseUrl})`);
|
|
|
774
867
|
|
|
775
868
|
// src/mcp/tools/session.ts
|
|
776
869
|
var import_zod5 = require("zod");
|
|
777
|
-
var
|
|
778
|
-
var
|
|
870
|
+
var import_path7 = __toESM(require("path"));
|
|
871
|
+
var import_fs5 = __toESM(require("fs"));
|
|
779
872
|
|
|
780
873
|
// src/mcp/tools/reverse-engineer.ts
|
|
781
874
|
var import_zod4 = require("zod");
|
|
@@ -883,11 +976,11 @@ Screenshot saved: ${filePath}`);
|
|
|
883
976
|
const mainDomain = captureResult.capturedApi[0]?.domain || "api";
|
|
884
977
|
const outDir = params.outputDir || `./${mainDomain}`;
|
|
885
978
|
try {
|
|
886
|
-
const
|
|
887
|
-
const
|
|
888
|
-
|
|
889
|
-
const jsonPath =
|
|
890
|
-
|
|
979
|
+
const fs5 = await import("fs");
|
|
980
|
+
const path7 = await import("path");
|
|
981
|
+
fs5.mkdirSync(outDir, { recursive: true });
|
|
982
|
+
const jsonPath = path7.join(outDir, "captured-api.json");
|
|
983
|
+
fs5.writeFileSync(jsonPath, JSON.stringify(captureResult.capturedApi, null, 2));
|
|
891
984
|
lines.push(`
|
|
892
985
|
Full captured data saved to: ${jsonPath}`);
|
|
893
986
|
lines.push("Read this file for complete curl commands, request/response bodies, and auth data.");
|
|
@@ -1099,11 +1192,11 @@ Sessions live in the single local SQLite database (~/.iframer/iframer.db), share
|
|
|
1099
1192
|
const lines = [result.message];
|
|
1100
1193
|
if (result.capturedApi && result.capturedApi.length > 0) {
|
|
1101
1194
|
formatCapturedApi(lines, result.capturedApi, { outputDir, typed: false });
|
|
1102
|
-
const outDir = outputDir ||
|
|
1195
|
+
const outDir = outputDir || import_path7.default.join(getDataDir(), "capture");
|
|
1103
1196
|
try {
|
|
1104
|
-
|
|
1105
|
-
const jsonPath =
|
|
1106
|
-
|
|
1197
|
+
import_fs5.default.mkdirSync(outDir, { recursive: true });
|
|
1198
|
+
const jsonPath = import_path7.default.join(outDir, "captured-api.json");
|
|
1199
|
+
import_fs5.default.writeFileSync(jsonPath, JSON.stringify(result.capturedApi, null, 2));
|
|
1107
1200
|
lines.push(`
|
|
1108
1201
|
Full data saved to: ${jsonPath}`);
|
|
1109
1202
|
} catch (writeErr) {
|
|
@@ -1173,19 +1266,19 @@ Full data saved to: ${jsonPath}`);
|
|
|
1173
1266
|
|
|
1174
1267
|
// src/mcp/tools/credentials.ts
|
|
1175
1268
|
var import_zod6 = require("zod");
|
|
1176
|
-
var
|
|
1177
|
-
var
|
|
1269
|
+
var import_fs7 = __toESM(require("fs"));
|
|
1270
|
+
var import_path9 = __toESM(require("path"));
|
|
1178
1271
|
|
|
1179
1272
|
// src/lib/knowledge.ts
|
|
1180
|
-
var
|
|
1181
|
-
var
|
|
1182
|
-
var
|
|
1273
|
+
var import_fs6 = __toESM(require("fs"));
|
|
1274
|
+
var import_path8 = __toESM(require("path"));
|
|
1275
|
+
var log4 = createLogger("knowledge");
|
|
1183
1276
|
function getKnowledgeDir() {
|
|
1184
|
-
return
|
|
1277
|
+
return import_path8.default.join(getDataDir(), "knowledge");
|
|
1185
1278
|
}
|
|
1186
1279
|
function getKnowledgePath(domain) {
|
|
1187
1280
|
const safe = sanitizeDomain(domain);
|
|
1188
|
-
return
|
|
1281
|
+
return import_path8.default.join(getKnowledgeDir(), `${safe}.md`);
|
|
1189
1282
|
}
|
|
1190
1283
|
function normalizeDomain(input) {
|
|
1191
1284
|
let d = (input || "").trim().toLowerCase();
|
|
@@ -1209,7 +1302,7 @@ function sanitizeDomain(input) {
|
|
|
1209
1302
|
function readKnowledge(domain) {
|
|
1210
1303
|
const p = getKnowledgePath(domain);
|
|
1211
1304
|
try {
|
|
1212
|
-
return
|
|
1305
|
+
return import_fs6.default.readFileSync(p, "utf8");
|
|
1213
1306
|
} catch {
|
|
1214
1307
|
return null;
|
|
1215
1308
|
}
|
|
@@ -1218,7 +1311,7 @@ function listKnowledge() {
|
|
|
1218
1311
|
const dir = getKnowledgeDir();
|
|
1219
1312
|
let entries = [];
|
|
1220
1313
|
try {
|
|
1221
|
-
entries =
|
|
1314
|
+
entries = import_fs6.default.readdirSync(dir);
|
|
1222
1315
|
} catch {
|
|
1223
1316
|
return [];
|
|
1224
1317
|
}
|
|
@@ -1226,10 +1319,10 @@ function listKnowledge() {
|
|
|
1226
1319
|
for (const file of entries) {
|
|
1227
1320
|
if (!file.endsWith(".md"))
|
|
1228
1321
|
continue;
|
|
1229
|
-
const full =
|
|
1322
|
+
const full = import_path8.default.join(dir, file);
|
|
1230
1323
|
try {
|
|
1231
|
-
const stat =
|
|
1232
|
-
const raw =
|
|
1324
|
+
const stat = import_fs6.default.statSync(full);
|
|
1325
|
+
const raw = import_fs6.default.readFileSync(full, "utf8");
|
|
1233
1326
|
const parsed = parseMarkdown(raw);
|
|
1234
1327
|
results.push({
|
|
1235
1328
|
domain: parsed?.domain ?? file.replace(/\.md$/, ""),
|
|
@@ -1247,7 +1340,7 @@ function clearKnowledge(domain) {
|
|
|
1247
1340
|
if (domain) {
|
|
1248
1341
|
const p = getKnowledgePath(domain);
|
|
1249
1342
|
try {
|
|
1250
|
-
|
|
1343
|
+
import_fs6.default.unlinkSync(p);
|
|
1251
1344
|
return { removed: 1 };
|
|
1252
1345
|
} catch {
|
|
1253
1346
|
return { removed: 0 };
|
|
@@ -1255,11 +1348,11 @@ function clearKnowledge(domain) {
|
|
|
1255
1348
|
}
|
|
1256
1349
|
let removed = 0;
|
|
1257
1350
|
try {
|
|
1258
|
-
const entries =
|
|
1351
|
+
const entries = import_fs6.default.readdirSync(dir);
|
|
1259
1352
|
for (const f of entries) {
|
|
1260
1353
|
if (f.endsWith(".md")) {
|
|
1261
1354
|
try {
|
|
1262
|
-
|
|
1355
|
+
import_fs6.default.unlinkSync(import_path8.default.join(dir, f));
|
|
1263
1356
|
removed++;
|
|
1264
1357
|
} catch {}
|
|
1265
1358
|
}
|
|
@@ -1339,8 +1432,8 @@ var ELICIT_TIMEOUT_MS = 45000;
|
|
|
1339
1432
|
function mcpLog(event, data) {
|
|
1340
1433
|
try {
|
|
1341
1434
|
const dir = getDataDir();
|
|
1342
|
-
|
|
1343
|
-
|
|
1435
|
+
import_fs7.default.mkdirSync(dir, { recursive: true });
|
|
1436
|
+
import_fs7.default.appendFileSync(import_path9.default.join(dir, "mcp.log"), JSON.stringify({ ts: new Date().toISOString(), event, ...data }) + `
|
|
1344
1437
|
`);
|
|
1345
1438
|
} catch {}
|
|
1346
1439
|
}
|
|
@@ -1562,6 +1655,7 @@ CRITICAL RULES:
|
|
|
1562
1655
|
5. ALWAYS check "knowledge get <domain>" before launching a browser. Skip the browser if the cache has what you need.
|
|
1563
1656
|
6. If execute fails, read the FULL error. It tells you the step, error type, and suggestion.
|
|
1564
1657
|
7. If the browser crashes, call "session restart" and retry. Don't panic.
|
|
1658
|
+
8. ALWAYS call "session" action=stop when you are done with browser work. It saves session state and frees the browser. Idle browsers are auto-reclaimed, but don't rely on that.
|
|
1565
1659
|
|
|
1566
1660
|
BROWSER MODES: Don't specify options.mode — iframer auto-selects and auto-escalates (headless → binary-headful). Only set a mode if the user explicitly asks.
|
|
1567
1661
|
|
|
@@ -1576,18 +1670,8 @@ registerSessionTool(server);
|
|
|
1576
1670
|
registerCredentialsTool(server);
|
|
1577
1671
|
registerReverseEngineerTool(server);
|
|
1578
1672
|
registerKnowledgeTool(server);
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
}
|
|
1582
|
-
process.on("exit", cleanup);
|
|
1583
|
-
process.on("SIGTERM", () => {
|
|
1584
|
-
cleanup();
|
|
1585
|
-
process.exit(0);
|
|
1586
|
-
});
|
|
1587
|
-
process.on("SIGINT", () => {
|
|
1588
|
-
cleanup();
|
|
1589
|
-
process.exit(0);
|
|
1590
|
-
});
|
|
1673
|
+
process.on("SIGTERM", () => process.exit(0));
|
|
1674
|
+
process.on("SIGINT", () => process.exit(0));
|
|
1591
1675
|
process.on("uncaughtException", (err2) => {
|
|
1592
1676
|
try {
|
|
1593
1677
|
console.error(`[mcp] uncaughtException: ${err2?.message}`);
|