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.
@@ -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 import_path4 = __toESM(require("path"));
39
- var import_os2 = __toESM(require("os"));
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 dir = getDataDir();
83
- const file = import_path2.default.join(dir, "secret");
84
- try {
85
- const existing = import_fs.default.readFileSync(file, "utf8").trim();
86
- if (existing)
87
- return existing;
88
- } catch {}
89
- try {
90
- import_fs.default.mkdirSync(dir, { recursive: true });
91
- const secret = import_crypto.default.randomBytes(32).toString("hex");
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 = import_path3.default.join(getDataDir(), "local-server.log");
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
- this.startingPromise = this.doStart().finally(() => {
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 doStart() {
136
- this.port = await findFreePort(BASE_PORT, PORT_SCAN_ATTEMPTS);
137
- this.baseUrl = `http://127.0.0.1:${this.port}`;
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
- import_fs2.default.mkdirSync(dataDir, { recursive: true });
140
- const { command, args } = this.resolveRuntime();
141
- const logFd = import_fs2.default.openSync(this.logPath, "a");
142
- const env = {
143
- ...process.env,
144
- PORT: String(this.port),
145
- IFRAMER_MODE: "local",
146
- IFRAMER_DATA_DIR: dataDir,
147
- IFRAMER_PARENT_PID: String(process.pid)
148
- };
149
- if (process.env.IFRAMER_SECRET) {
150
- env.IFRAMER_SECRET = process.env.IFRAMER_SECRET;
151
- } else {
152
- try {
153
- const secret = import_fs2.default.readFileSync(import_path3.default.join(dataDir, "secret"), "utf8").trim();
154
- if (secret)
155
- env.IFRAMER_SECRET = secret;
156
- } catch {}
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
- this.child = import_child_process.spawn(command, args, {
159
- env,
160
- stdio: ["ignore", logFd, logFd],
161
- detached: false
162
- });
163
- import_fs2.default.closeSync(logFd);
164
- this.child.on("exit", (code) => {
165
- this.child = null;
166
- });
167
- const deadline = Date.now() + STARTUP_TIMEOUT_MS;
168
- while (Date.now() < deadline) {
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 sleep(HEALTH_POLL_MS);
172
- }
173
- this.kill();
174
- const logTail = this.readLogTail();
175
- throw new Error(`Local iframer server failed to start on port ${this.port} within ${STARTUP_TIMEOUT_MS}ms.
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
- ${logTail}`);
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 = import_path3.default.join(__dirname, "..", "..", "index.ts");
183
- if (import_fs2.default.existsSync(serverTs2)) {
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 = import_path3.default.join(__dirname, "..", "..", "dist", "local-server.cjs");
188
- if (import_fs2.default.existsSync(serverCjs)) {
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 = import_path3.default.join(__dirname, "..", "..", "index.ts");
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
- this.kill();
196
- await sleep(500);
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.child.kill("SIGTERM");
284
+ await fetch(`${this.baseUrl}/shutdown`, { method: "POST", signal: AbortSignal.timeout(3000) });
206
285
  } catch {}
207
- const c = this.child;
208
- setTimeout(() => {
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
- if (!c.killed)
211
- c.kill("SIGKILL");
292
+ process.kill(info.pid, "SIGKILL");
212
293
  } catch {}
213
- }, 2000);
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 = import_fs2.default.readFileSync(this.logPath, "utf8");
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 log = createLogger("mcp");
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 fs4 = await import("fs");
334
- if (fs4.existsSync(filePath2))
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 fs3 = await import("fs");
343
- const dir = import_path4.default.join(import_os2.default.tmpdir(), "iframer-screenshots", "screenshots");
344
- fs3.mkdirSync(dir, { recursive: true });
345
- const filePath = import_path4.default.join(dir, `docker-${Date.now()}.jpg`);
346
- fs3.writeFileSync(filePath, buf);
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 import_fs3 = __toESM(require("fs"));
361
- var import_path5 = __toESM(require("path"));
362
- var log2 = createLogger("domain-modes");
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 import_path5.default.join(getDataDir(), "domain-modes.json");
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 (import_fs3.default.existsSync(this.filePath)) {
446
- this.data = JSON.parse(import_fs3.default.readFileSync(this.filePath, "utf-8"));
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
- import_fs3.default.mkdirSync(import_path5.default.dirname(this.filePath), { recursive: true });
455
- import_fs3.default.writeFileSync(this.filePath, JSON.stringify(this.data, null, 2));
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
- log2.error("Failed to save:", err2);
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 = /connect|ECONNREFUSED|EPIPE|closed|timed out|abort|fetch failed/i.test(msg);
725
+ const isCrash = /ECONNREFUSED|ECONNRESET|EPIPE|socket hang up|fetch failed/i.test(msg);
642
726
  if (isCrash) {
643
- log.info(`Execute crashed (${msg.slice(0, 80)}), restarting local server and retrying...`);
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
- log.info(`Auto-escalating to ${nextMode}`);
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?.type === "snapshot" && r.result?.snapshot) {
722
- lines.push(`
723
- --- Snapshot (${r.result.elementCount} elements) ---`);
724
- lines.push(r.result.snapshot);
725
- } else if (r.step?.type === "find" && r.result?.ref) {
726
- lines.push(`
727
- Found: ${r.result.ref} ${r.result.role} "${r.result.name}" (${r.result.matchCount} match${r.result.matchCount > 1 ? "es" : ""})`);
728
- } else if (r.step?.type === "screenshot" && r.result?.refs) {
729
- lines.push(`
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
- lines.push(r.result.refs);
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 import_path6 = __toESM(require("path"));
778
- var import_fs4 = __toESM(require("fs"));
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 fs4 = await import("fs");
887
- const path6 = await import("path");
888
- fs4.mkdirSync(outDir, { recursive: true });
889
- const jsonPath = path6.join(outDir, "captured-api.json");
890
- fs4.writeFileSync(jsonPath, JSON.stringify(captureResult.capturedApi, null, 2));
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 || import_path6.default.join(getDataDir(), "capture");
1195
+ const outDir = outputDir || import_path7.default.join(getDataDir(), "capture");
1103
1196
  try {
1104
- import_fs4.default.mkdirSync(outDir, { recursive: true });
1105
- const jsonPath = import_path6.default.join(outDir, "captured-api.json");
1106
- import_fs4.default.writeFileSync(jsonPath, JSON.stringify(result.capturedApi, null, 2));
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 import_fs6 = __toESM(require("fs"));
1177
- var import_path8 = __toESM(require("path"));
1269
+ var import_fs7 = __toESM(require("fs"));
1270
+ var import_path9 = __toESM(require("path"));
1178
1271
 
1179
1272
  // src/lib/knowledge.ts
1180
- var import_fs5 = __toESM(require("fs"));
1181
- var import_path7 = __toESM(require("path"));
1182
- var log3 = createLogger("knowledge");
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 import_path7.default.join(getDataDir(), "knowledge");
1277
+ return import_path8.default.join(getDataDir(), "knowledge");
1185
1278
  }
1186
1279
  function getKnowledgePath(domain) {
1187
1280
  const safe = sanitizeDomain(domain);
1188
- return import_path7.default.join(getKnowledgeDir(), `${safe}.md`);
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 import_fs5.default.readFileSync(p, "utf8");
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 = import_fs5.default.readdirSync(dir);
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 = import_path7.default.join(dir, file);
1322
+ const full = import_path8.default.join(dir, file);
1230
1323
  try {
1231
- const stat = import_fs5.default.statSync(full);
1232
- const raw = import_fs5.default.readFileSync(full, "utf8");
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
- import_fs5.default.unlinkSync(p);
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 = import_fs5.default.readdirSync(dir);
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
- import_fs5.default.unlinkSync(import_path7.default.join(dir, f));
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
- import_fs6.default.mkdirSync(dir, { recursive: true });
1343
- import_fs6.default.appendFileSync(import_path8.default.join(dir, "mcp.log"), JSON.stringify({ ts: new Date().toISOString(), event, ...data }) + `
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
- function cleanup() {
1580
- localServer.shutdown();
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}`);