@agent360/browser-mcp 1.28.1 → 1.29.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/index.js CHANGED
@@ -15,13 +15,13 @@ import { ledErDoedt, forfaedreKaede } from './vagt.js';
15
15
  import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
16
16
  import { WebSocketServer } from 'ws';
17
17
  import { execSync, execFile } from 'child_process';
18
- import { dirname, join, resolve, sep } from 'path';
18
+ import { dirname, join, resolve, sep, isAbsolute } from 'path';
19
19
  import { fileURLToPath } from 'url';
20
20
  import { homedir } from 'os';
21
- import { readFileSync, writeFileSync, mkdirSync, appendFileSync } from 'fs';
21
+ import { readFileSync, writeFileSync, mkdirSync, appendFileSync, realpathSync, lstatSync, statSync } from 'fs';
22
22
  import { TOOLS, PROVIDER_PAGES } from './tools.js';
23
23
 
24
- // Read version from package.json single source of truth, never drifts
24
+ // Read version from package.json - single source of truth, never drifts
25
25
  const PKG_VERSION = JSON.parse(
26
26
  readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'package.json'), 'utf8')
27
27
  ).version;
@@ -32,23 +32,26 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
32
32
  const repoDir = dirname(__dirname); // parent of mcp-server/
33
33
 
34
34
  // FJERNET 22/8: her koerte `git pull --ff-only` + `npm install` ved hver serveropstart,
35
- // med cwd = pakkens foraeldremappe. I en npm-installation er det node_modules/@agent360/
35
+ // med cwd = pakkens foraeldremappe. I en npm-installation er det node_modules/@agent360/ -
36
36
  // og git soeger OPAD, saa kaldet landede i BRUGERENS EGET repo. Maalt: fra
37
37
  // node_modules/@agent360 opløser git toplevel til det omkringliggende projekt.
38
38
  // En agent-session maatte altsaa ikke mutere brugerens git-trae uden samtykke.
39
39
  // `npx @agent360/browser-mcp@latest` opdaterer allerede serveren; blokken var overfloedig.
40
40
  let extensionUpdated = false;
41
41
 
42
- const BASE_PORT = 9876;
43
- const MAX_PORT = 9895; // 20 ports instead of 10 zombies die within 5s via parent check
42
+ // Spaendet kan flyttes med env - ellers ville en test af port-udsultning skulle
43
+ // beslaglaegge de RIGTIGE porte og dermed sulte brugerens oevrige chats imens.
44
+ // Uden env er vaerdierne uaendrede. (Samme moenster som BROWSER_MCP_EXTENSION_ID.)
45
+ const BASE_PORT = Number(process.env.BROWSER_MCP_BASE_PORT) || 9876;
46
+ const MAX_PORT = Number(process.env.BROWSER_MCP_MAX_PORT) || 9895; // 20 ports instead of 10 - zombies die within 5s via parent check
44
47
 
45
48
  // ── Extension connections ───────────────────────────────────────────────────
46
49
  // FEJL MAALT 21/8: her stod `let extensionSocket = null`, og hver ny forbindelse
47
- // overskrev den. Er der to udgaver af udvidelsen indlaest i den samme Chrome
48
- // fx en "load unpacked"-kopi ved siden af en anden scanner BEGGE de samme porte
50
+ // overskrev den. Er der to udgaver af udvidelsen indlaest i den samme Chrome -
51
+ // fx en "load unpacked"-kopi ved siden af en anden - scanner BEGGE de samme porte
49
52
  // og forbinder til hver eneste server. Maalt med lsof: 2 ESTABLISHED forbindelser
50
53
  // paa hver af de fire aktive porte. Kommandoerne gik til den der forbandt sidst,
51
- // mens den anden holdt sit eget sessions-kort og sine egne fane-grupper og et
54
+ // mens den anden holdt sit eget sessions-kort og sine egne fane-grupper - og et
52
55
  // `terminate` fra den forkerte kopi lukkede serveren ned under den rigtige.
53
56
  //
54
57
  // Nu holdes alle forbindelser med deres identitet, kommandoer sendes kun til den
@@ -57,7 +60,9 @@ const MAX_PORT = 9895; // 20 ports instead of 10 — zombies die within 5s via p
57
60
  const connections = new Set(); // { ws, seq, extensionId, version, name, since }
58
61
  let connSeq = 0;
59
62
  let activePort = null;
60
- let alleePorteOptaget = false; // hele spaendet i brug se createWSS
63
+ let alleePorteOptaget = false; // hele spaendet i brug - se createWSS
64
+ let bindFejl = null; // bind fejlede af en ANDEN grund end optaget port
65
+ let portBundetTid = 0; // hvornaar porten sidst blev aaben - se sendToExtension
61
66
 
62
67
  function cmpVersion(a, b) {
63
68
  const pa = String(a || '0.0.0').split('.').map(n => parseInt(n, 10) || 0);
@@ -73,13 +78,13 @@ let sidsteKonfliktNoegle = '';
73
78
  function advarOmKonflikt(conn) {
74
79
  const alle = distinctExtensions();
75
80
  if (alle.length < 2) return;
76
- // Samme konflikt maa ikke skrige ved hver eneste hello kun naar billedet aendrer sig.
81
+ // Samme konflikt maa ikke skrige ved hver eneste hello - kun naar billedet aendrer sig.
77
82
  const noegle = alle.map(c => `${c.extensionId || 'ukendt'}@${c.version || '?'}`).sort().join('|');
78
83
  if (noegle === sidsteKonfliktNoegle) return;
79
84
  sidsteKonfliktNoegle = noegle;
80
85
  const aktiv = activeConnection();
81
86
  // Oplyser INGEN af dem en version (alle udgivne udgaver er fra foer haandtrykket),
82
- // er der intet grundlag for at vaelge. Det skal staa der ellers laeser man
87
+ // er der intet grundlag for at vaelge. Det skal staa der - ellers laeser man
83
88
  // "kommandoer sendes kun til X" som om X var det rigtige valg.
84
89
  const kanVaelge = alle.some(c => c.version);
85
90
  process.stderr.write(
@@ -101,14 +106,14 @@ function liveConnections() {
101
106
  //
102
107
  // MAALT 21/8 i flow-harnessen: uden laasen skiftede den aktive udvidelse MIDT i en
103
108
  // koersel. Foerste kommando (navigate) gik til udvidelse A, som aabnede fanen. Et
104
- // oejeblik senere forbandt udvidelse B og overtog, fordi den var nyere i raekken
109
+ // oejeblik senere forbandt udvidelse B og overtog, fordi den var nyere i raekken -
105
110
  // men B kendte ikke A's fane og lavede en frisk about:blank. Alt derefter fejlede med
106
111
  // "Cannot access contents of url about:blank". 21 af 43 vaerktoejer faldt paa det, og
107
112
  // symptomet lignede praecis "faner forsvinder" og "kun én session virker".
108
113
  //
109
114
  // Faner hoerer til den udvidelse der aabnede dem. Skifter man udvidelse, strander de.
110
115
  // Derfor: vaelg én gang, og bliv ved den saa laenge dens forbindelse lever. Doer den,
111
- // vaelges der forfra det er en aegte genopretning, ikke et vilkaarligt skift.
116
+ // vaelges der forfra - det er en aegte genopretning, ikke et vilkaarligt skift.
112
117
  let laastForbindelse = null;
113
118
  let harSendtKommando = false;
114
119
  const PINNET_UDVIDELSE = (process.env.BROWSER_MCP_EXTENSION_ID || '').trim() || null;
@@ -117,7 +122,7 @@ function activeConnection() {
117
122
  if (laastForbindelse && laastForbindelse.ws.readyState === 1) return laastForbindelse;
118
123
 
119
124
  // Nyeste udvidelse vinder. Ved uafgjort: den der forbandt sidst. En udvidelse fra
120
- // foer haandtrykket har ingen version og taber til en der har én haandtrykket kom
125
+ // foer haandtrykket har ingen version og taber til en der har én - haandtrykket kom
121
126
  // med den nyere udgave.
122
127
  let best = null;
123
128
  for (const c of liveConnections()) {
@@ -147,6 +152,22 @@ const pending = new Map();
147
152
  // Timers hoisted to module scope so gracefulShutdown can clear them deterministically.
148
153
  let heartbeat = null;
149
154
  let parentCheck = null;
155
+ let tomgangsvagt = null;
156
+
157
+ // ── 4-timers-tomgangen bor paa modul-niveau, ikke i hjerteslaget ────────────
158
+ //
159
+ // FUNDET AF REVIEW 7/9. Tjekket laa INDE i `heartbeat`, som kun findes mens serveren
160
+ // har en port - og som `frigivPort` rydder. Da porten blev doven, betoed det at en
161
+ // proces der ALDRIG binder (en chat der ikke roerer browseren) heller aldrig faar
162
+ // sin tomgang tjekket. Det var den eneste vej hvor en browser-inaktiv server gav sine
163
+ // ~35 MB tilbage; med mange samtidige chats er det maalbart. Nu koerer vagten altid.
164
+ // Graensen staar som literal, ikke bag en konstant: `vagter.test.mjs` laeser tallet
165
+ // direkte ud af kilden for at haandhaeve at den aldrig bliver kort. En kort graense
166
+ // ville lade en aaben chat miste browseren permanent - vaerre end en port der staar
167
+ // optaget lidt for laenge.
168
+ tomgangsvagt = setInterval(() => {
169
+ if (Date.now() - lastActivity > 4 * 60 * 60 * 1000) gracefulShutdown('Idle timeout (4h)');
170
+ }, 60000);
150
171
 
151
172
  // ── WebSocket Server ───────────────────────────────────────────────────────
152
173
 
@@ -160,7 +181,7 @@ function createWSS(port = BASE_PORT) {
160
181
  // ingen socket overhovedet.
161
182
  //
162
183
  // Chrome saetter ALTID Origin: chrome-extension://<32 tegn> paa en WebSocket fra
163
- // en udvidelse verificeret mod den koerende. Alt andet er per definition ikke
184
+ // en udvidelse - verificeret mod den koerende. Alt andet er per definition ikke
164
185
  // en udvidelse, saa gaten koster aegte brugere ingenting.
165
186
  verifyClient: ({ origin }, godkend) => {
166
187
  if (/^chrome-extension:\/\/[a-p]{32}$/.test(origin || '')) return godkend(true);
@@ -181,15 +202,25 @@ function createWSS(port = BASE_PORT) {
181
202
  } else {
182
203
  // MAALT 22/8: her stod KUN denne stderr-linje. Ingen laeser stderr fra en
183
204
  // MCP-server, saa udtoemte porte var en helt tavs fejl. Hvert vaerktoejskald
184
- // fejlede bagefter med "extension not connected" en tekst der oven i koebet
205
+ // fejlede bagefter med "extension not connected" - en tekst der oven i koebet
185
206
  // siger at serveren koerer og sender brugeren til Chrome Web Store. Begge dele
186
207
  // er forkerte naar sandheden er at vi aldrig fik en port. Flaget laeses i
187
208
  // sendToExtension, saa agenten kan give brugeren den rigtige forklaring.
188
209
  alleePorteOptaget = true;
189
210
  process.stderr.write(`[MCP] All ports ${BASE_PORT}-${MAX_PORT} in use. Cannot start.\n`);
211
+ loesPortLoefte(false);
190
212
  }
191
213
  } else {
214
+ // FUNDET AF REVIEW 7/9. Her stod KUN stderr-linjen. Foer porten blev doven, var
215
+ // det harmloest: en opstartsfejl man kunne se i loggen. Nu venter `sikrePort()`
216
+ // paa et loefte der aldrig blev indfriet - og `sendToExtension` goer
217
+ // `await sikrePort()` UDEN timeout. Enhver anden bind-fejl end EADDRINUSE
218
+ // (EACCES paa en privilegeret port, EADDRNOTAVAIL, en restriktiv firewall)
219
+ // ville derfor faa hvert eneste browser-kald til at haenge tavst for evigt.
220
+ // At haenge uden besked er vaerre end den fejl vi rettede.
221
+ bindFejl = err.message;
192
222
  process.stderr.write(`[MCP] WebSocket error: ${err.message}\n`);
223
+ loesPortLoefte(false);
193
224
  }
194
225
  });
195
226
 
@@ -197,7 +228,7 @@ function createWSS(port = BASE_PORT) {
197
228
  // WebSocket-handshaket fra en udvidelse baerer Origin: chrome-extension://<id>.
198
229
  // Den identificerer afsenderen UDEN at udvidelsen behoever at kende haandtrykket,
199
230
  // saa en konflikt mellem to indlaeste udvidelser kan opdages ogsaa naar begge er
200
- // gamle udgaver hvilket er praecis den situation konflikten opstaar i.
231
+ // gamle udgaver - hvilket er praecis den situation konflikten opstaar i.
201
232
  // Maalt 21/8: to distinkte origins ringede op til hver eneste server.
202
233
  const origin = req?.headers?.origin || '';
203
234
  const fraOrigin = /^chrome-extension:\/\/([a-p]{32})$/.exec(origin)?.[1] || null;
@@ -209,14 +240,14 @@ function createWSS(port = BASE_PORT) {
209
240
  // udvidelse med `hello version 99.0.0`, fik `browser_get_cookies` leveret, og
210
241
  // kunne lukke serveren med `terminate`. Alle tre trin lykkedes.
211
242
  //
212
- // Serveren lytter kun paa 127.0.0.1, saa angriberen skal koere lokalt men det
243
+ // Serveren lytter kun paa 127.0.0.1, saa angriberen skal koere lokalt - men det
213
244
  // goer enhver anden app og ethvert npm-postinstall-script. Og hvad den kan er
214
245
  // ikke smaating: laese alt agenten sender til browseren (kodeord fra ask_user,
215
246
  // cookies, sidetekst), fodre agenten med opdigtet sideindhold, og slukke
216
247
  // browser-adgangen i alle aabne chats.
217
248
  //
218
249
  // Chrome saetter ALTID `Origin: chrome-extension://<id>` paa en WebSocket fra en
219
- // udvidelse verificeret mod den koerende udvidelse. En manglende header er
250
+ // udvidelse - verificeret mod den koerende udvidelse. En manglende header er
220
251
  // derfor ikke en aeldre udgave; det er noget andet end en udvidelse.
221
252
  if (!fraOrigin) {
222
253
  process.stderr.write(
@@ -231,7 +262,7 @@ function createWSS(port = BASE_PORT) {
231
262
  // slaa dem fra: BROWSER_MCP_EXTENSION_ID=<id> binder serveren til én bestemt.
232
263
  // Uden den er valget vilkaarligt naar ingen af dem oplyser en version.
233
264
  if (PINNET_UDVIDELSE && fraOrigin && fraOrigin !== PINNET_UDVIDELSE) {
234
- process.stderr.write(`[MCP] Afviser udvidelse ${fraOrigin} bundet til ${PINNET_UDVIDELSE}\n`);
265
+ process.stderr.write(`[MCP] Afviser udvidelse ${fraOrigin} - bundet til ${PINNET_UDVIDELSE}\n`);
235
266
  try { ws.close(1008, 'not the pinned extension'); } catch {}
236
267
  return;
237
268
  }
@@ -248,7 +279,7 @@ function createWSS(port = BASE_PORT) {
248
279
  // If extension was auto-updated, trigger reload
249
280
  if (process.env.BROWSER_MCP_EXTENSION_UPDATED === '1') {
250
281
  process.env.BROWSER_MCP_EXTENSION_UPDATED = '';
251
- process.stderr.write('[MCP] Extension files updated triggering auto-reload\n');
282
+ process.stderr.write('[MCP] Extension files updated - triggering auto-reload\n');
252
283
  setTimeout(() => {
253
284
  sendToExtension('reload_extension', {}, 5000).catch(() => {});
254
285
  }, 1000);
@@ -260,7 +291,7 @@ function createWSS(port = BASE_PORT) {
260
291
 
261
292
  // Identitets-haandtryk fra offscreen-dokumentet (v1.28+).
262
293
  if (msg.type === 'hello') {
263
- // MAALT 23/8: her stod `conn.extensionId = msg.extensionId` altsaa lod
294
+ // MAALT 23/8: her stod `conn.extensionId = msg.extensionId` - altsaa lod
264
295
  // haandtrykket afsenderen OVERSKRIVE sin egen identitet med hvad som helst.
265
296
  // Origin-headeren er den eneste kilde Chrome selv saetter og som afsenderen
266
297
  // ikke kan forfalske, saa den vinder. Beskedens id gemmes separat: stemmer de
@@ -269,25 +300,37 @@ function createWSS(port = BASE_PORT) {
269
300
  conn.harHilst = true;
270
301
  if (conn.helloId && conn.helloId !== conn.extensionId) {
271
302
  process.stderr.write(
272
- `[MCP] Haandtryk oplyser ${conn.helloId} men Origin siger ${conn.extensionId} ` +
303
+ `[MCP] Haandtryk oplyser ${conn.helloId} men Origin siger ${conn.extensionId} - ` +
273
304
  'bruger Origin\n',
274
305
  );
275
306
  }
276
307
  conn.version = typeof msg.version === 'string' ? msg.version : null;
277
308
  conn.name = typeof msg.name === 'string' ? msg.name : null;
309
+ // Fingeraftryk af udvidelsens egen background.js (plan 1.10 / R2): versionsnummeret siger ikke hvilken KODE der
310
+ // koerer. Udgivelsens flowtest sammenligner det med repoets fil, saa en gammel kopi med samme nummer ikke kan
311
+ // passere som kandidaten.
312
+ conn.kode = typeof msg.kode === 'string' ? msg.kode : null;
278
313
  advarOmKonflikt(conn);
279
314
  return;
280
315
  }
281
316
 
317
+ // Aftrykket eftersendes, hvis udvidelsen ikke havde beregnet det da den hilste. MAALT 12/9 af Astra: en frist paa
318
+ // 1 s i udvidelsen gjorde et langsomt aftryk til `null`, og gaten afviste sin egen kandidat. Nu venter hilsenen
319
+ // ikke, og aftrykket kommer naar det er klart.
320
+ if (msg.type === 'kode') {
321
+ if (typeof msg.kode === 'string') conn.kode = msg.kode;
322
+ return;
323
+ }
324
+
282
325
  if (msg.type === 'terminate') {
283
326
  // terminate lukker serveren for ALLE chats paa porten, saa den har to gates.
284
327
  //
285
- // Om id-sammenligningen: ORIGIN er autoriteten Chrome saetter den, og
328
+ // Om id-sammenligningen: ORIGIN er autoriteten - Chrome saetter den, og
286
329
  // afsenderen kan ikke forfalske den. Haandtrykkets id er kun et ekstra
287
330
  // signal. MAALT 23/8 mod den AEGTE udvidelse: den sender hello med id null,
288
331
  // fordi chrome.runtime.getManifest() kan fejle i offscreen-dokumentet.
289
332
  // Kraevede vi lighed ubetinget, kunne en HELT legitim udvidelse aldrig lukke
290
- // sin session ned en fejl jeg selv indfoerte samme aften. Derfor: et id der
333
+ // sin session ned - en fejl jeg selv indfoerte samme aften. Derfor: et id der
291
334
  // MANGLER er fint (Origin har allerede bevist hvem det er), mens et id der er
292
335
  // TIL STEDE og peger et ANDET sted afvises.
293
336
  //
@@ -298,14 +341,18 @@ function createWSS(port = BASE_PORT) {
298
341
  // kopi, der lukkede sin sidste fane, rive serveren vaek under den
299
342
  // udvidelse der reelt loeste opgaven.
300
343
  if (!conn.harHilst || (conn.helloId && conn.helloId !== conn.extensionId)) {
301
- process.stderr.write('[MCP] terminate ignoreret intet gyldigt haandtryk\n');
344
+ process.stderr.write('[MCP] terminate ignoreret - intet gyldigt haandtryk\n');
302
345
  return;
303
346
  }
304
347
  if (activeConnection() !== conn) {
305
- process.stderr.write('[MCP] terminate ignoreret kom fra en inaktiv udvidelses-forbindelse\n');
348
+ process.stderr.write('[MCP] terminate ignoreret - kom fra en inaktiv udvidelses-forbindelse\n');
306
349
  return;
307
350
  }
308
- gracefulShutdown('Terminate signal from extension (last tab closed)');
351
+ // AENDRET 7/9: her stod `gracefulShutdown`. En chat der var faerdig med browseren
352
+ // mistede altsaa browseren HELT - og porten blev alligevel hverken frigivet hurtigt
353
+ // nok til andre (processen doede foerst efter oprydning) eller genvundet af chatten
354
+ // selv. Nu slippes kun porten; naeste browser-kald tager en ny (se sikrePort).
355
+ frigivPort('udvidelsen meldte: sidste fane lukket');
309
356
  return;
310
357
  }
311
358
 
@@ -320,11 +367,11 @@ function createWSS(port = BASE_PORT) {
320
367
 
321
368
  // ── En doed socket skal AFVISE de kald der var undervejs (MAALT 23/8) ──────
322
369
  // Foer roerte close-handleren ikke `pending`. Maalt: en socket der doede 300 ms
323
- // inde i et kald gav foerst svar efter 30.011 ms og med den FORKERTE
370
+ // inde i et kald gav foerst svar efter 30.011 ms - og med den FORKERTE
324
371
  // forklaring, "kommandoen tog for lang tid". For extract_list er timeouten 180
325
372
  // sekunder, altsaa tre minutters tavshed hvor sandheden var kendt med det samme.
326
373
  const afvisVentende = (grund) => {
327
- // Kun naar ingen anden levende forbindelse kan svare ellers ville et helt
374
+ // Kun naar ingen anden levende forbindelse kan svare - ellers ville et helt
328
375
  // normalt skift mellem to udvidelser afbryde kald der er fuldt i orden.
329
376
  if (!pending.size || liveConnections().length) return;
330
377
  const antal = pending.size;
@@ -333,16 +380,16 @@ function createWSS(port = BASE_PORT) {
333
380
  pending.delete(id);
334
381
  p.reject(new Error(
335
382
  `Forbindelsen til Chrome-udvidelsen forsvandt mens kommandoen koerte (${grund}). ` +
336
- 'Kommandoen naaede maaske at blive udfoert i browseren tjek tilstanden foer du ' +
383
+ 'Kommandoen naaede maaske at blive udfoert i browseren - tjek tilstanden foer du ' +
337
384
  'proever igen. Er udvidelsen slaaet fra eller Chrome lukket, saa start den og proev forfra.',
338
385
  ));
339
386
  }
340
- process.stderr.write(`[MCP] ${antal} ventende kald afvist ${grund}\n`);
387
+ process.stderr.write(`[MCP] ${antal} ventende kald afvist - ${grund}\n`);
341
388
  };
342
389
 
343
390
  // MAALT 23/8: der fandtes INGEN error-handler. Et ugyldigt WebSocket-frame (fx
344
391
  // RSV1 sat) faar 'ws' til at emitte 'error' paa socketen, og en uhaandteret
345
- // 'error' paa en EventEmitter kaster og draeber hele processen altsaa alle
392
+ // 'error' paa en EventEmitter kaster og draeber hele processen - altsaa alle
346
393
  // chats paa den port. Tre linjer lukker det.
347
394
  ws.on('error', (e) => {
348
395
  process.stderr.write(`[MCP] WebSocket-fejl paa forbindelsen: ${e?.message || e}\n`);
@@ -358,39 +405,151 @@ function createWSS(port = BASE_PORT) {
358
405
 
359
406
  server.on('listening', () => {
360
407
  activePort = port;
408
+ portBundetTid = Date.now();
361
409
  process.stderr.write(`[MCP] WebSocket server listening on ws://127.0.0.1:${port}\n`);
410
+ loesPortLoefte(true);
362
411
  });
363
412
 
364
- // Heartbeat + idle timeout (4 hours) hoisted to module scope so gracefulShutdown can clear it
413
+ // Heartbeat + idle timeout (4 hours) - hoisted to module scope so gracefulShutdown can clear it
414
+ // Ryddes foerst: porten kan bindes flere gange i samme proces (se sikrePort), og uden
415
+ // det her ville hver ny binding efterlade en ekstra timer der aldrig blev stoppet.
416
+ if (heartbeat) clearInterval(heartbeat);
365
417
  heartbeat = setInterval(() => {
366
418
  for (const c of liveConnections()) c.ws.ping();
367
- if (Date.now() - lastActivity > 4 * 60 * 60 * 1000) {
368
- gracefulShutdown('Idle timeout (4h)');
369
- }
370
419
  }, 20000);
371
420
  }
372
421
 
373
- createWSS();
422
+ // ── Porten tages ved BRUG, ikke ved opstart ─────────────────────────────────
423
+ //
424
+ // MAALT 7/9-2026 paa Gustavs maskine: 37 koerende servere, 20 porte i spaendet, 17 chats
425
+ // helt uden browser. Hver Claude Code-chat starter en server ved opstart - ogsaa de mange
426
+ // chats der aldrig roerer browseren - og her stod `createWSS()` paa modul-niveau. Porten
427
+ // blev altsaa reserveret af en chat der maaske aldrig fik brug for den, og holdt indtil
428
+ // chatten doede eller 4-timers-tomgangen udloeb.
429
+ //
430
+ // Konsekvensen var ensidigt slem: chat nr. 21 fik `alleePorteOptaget = true` ÉN gang og
431
+ // proevede aldrig igen - dens browser var doed hele chattens levetid, selv naar en port
432
+ // blev fri et minut senere.
433
+ //
434
+ // Nu bindes porten foerste gang et vaerktoej faktisk skal bruge udvidelsen, og HVERT kald
435
+ // proever igen hvis det forrige ikke fik en. Udvidelsen genscanner hele spaendet hvert
436
+ // 2. sekund (offscreen.js), saa en port der aabnes sent bliver fundet af sig selv.
437
+ //
438
+ // Bemaerk hvorfor det ikke er en 5-minutters timer der draeber processen: en chat der
439
+ // foerst skal bruge browseren efter en halv time ville saa staa uden. Processen lever
440
+ // videre - det er kun PORTEN der ikke holdes reserveret til noget der ikke sker.
441
+ let portResolver = null;
442
+ let bindLoefte = null;
443
+
444
+ function loesPortLoefte(fik) {
445
+ if (!portResolver) return;
446
+ const r = portResolver;
447
+ portResolver = null;
448
+ bindLoefte = null;
449
+ r(fik);
450
+ }
451
+
452
+ // Slip porten, men BLIV I LIVE. Forskellen er hele pointen: lukkede vi processen ned,
453
+ // ville en chat der er faerdig med browseren kl. 10 og skal bruge den igen kl. 10:40 staa
454
+ // uden - og Claude Code genstarter ikke en MCP-server midt i en samtale. Processen koster
455
+ // ~35 MB og ingen port; det er porten der er den knappe ressource.
456
+ function frigivPort(grund) {
457
+ if (activePort === null) return;
458
+ process.stderr.write(`[MCP] frigiver port ${activePort} - ${grund}\n`);
459
+ if (heartbeat) { clearInterval(heartbeat); heartbeat = null; }
460
+ const gammel = wss;
461
+ wss = null;
462
+ activePort = null;
463
+ laastForbindelse = null;
464
+ harSendtKommando = false; // naeste binding vaelger udvidelse forfra
465
+ for (const c of connections) { try { c.ws.close(); } catch {} }
466
+ connections.clear();
467
+ try { gammel?.close(); } catch {}
468
+ }
469
+
470
+ function sikrePort() {
471
+ if (activePort !== null) return Promise.resolve(true);
472
+ if (bindLoefte) return bindLoefte; // en binding er allerede i gang
473
+ alleePorteOptaget = false; // hvert forsoeg starter paa en frisk
474
+ bindFejl = null;
475
+ portBundetTid = 0; // nulstilles her; saettes naar 'listening' faktisk kommer
476
+ bindLoefte = new Promise((res) => { portResolver = res; });
477
+ // Vagthund. F8 var ét konkret hul hvor loeftet aldrig blev indfriet; det her lukker
478
+ // KLASSEN. Emitter en fremtidig fejlsti hverken 'listening' eller 'error', svarer
479
+ // kaldet nu med en fejl i stedet for at haenge tavst for evigt. 10 s er rigeligt:
480
+ // en loopback-binding tager millisekunder, og hele spaendet naas paa under ét.
481
+ const vagthund = setTimeout(() => {
482
+ if (!portResolver) return;
483
+ bindFejl = bindFejl || 'bindingen svarede ikke inden for 10 sekunder';
484
+ process.stderr.write('[MCP] port-bindingen svarede aldrig - opgiver dette forsoeg\n');
485
+ loesPortLoefte(false);
486
+ }, 10000);
487
+ bindLoefte.finally(() => clearTimeout(vagthund));
488
+ createWSS();
489
+ return bindLoefte;
490
+ }
374
491
 
375
492
  // ── Send command to extension ───────────────────────────────────────────────
376
493
 
377
- async function sendToExtension(method, params = {}, timeoutMs = 30000, _retries = 5) {
494
+ async function sendToExtension(method, params = {}, timeoutMs = 30000, _retries = 5, _ekstraRunde = false, _doerAabnetNu = null) {
495
+ // Skaf en port hvis vi ikke har en. Foerste kald binder; senere kald er en no-op.
496
+ // Fik vi ingen (hele spaendet optaget), proever naeste kald igen - derfor ingen kast her.
497
+ await sikrePort();
498
+ // Spoergsmaalet er om doeren var NYAABNET da kaldet begyndte - ikke om den stadig er
499
+ // "ny" efter at vi selv har brugt femten sekunder paa at proeve igen. Derfor maales det
500
+ // ved indgangen og baeres med gennem gentagelserne.
501
+ const doerAabnetNu = _doerAabnetNu !== null
502
+ ? _doerAabnetNu
503
+ : Boolean(portBundetTid && (Date.now() - portBundetTid) < 10000);
378
504
  // Retry if extension is temporarily disconnected (reconnects every 2s)
379
505
  const conn = activeConnection();
380
506
  if (!conn) {
381
507
  if (_retries > 0) {
382
508
  await new Promise(r => setTimeout(r, 1500));
383
- return sendToExtension(method, params, timeoutMs, _retries - 1);
509
+ return sendToExtension(method, params, timeoutMs, _retries - 1, _ekstraRunde, doerAabnetNu);
510
+ }
511
+ // ── Har vi lige aabnet doeren selv? (MAALT 8/9, #16) ─────────────────────
512
+ //
513
+ // Foer porten blev doven, var udvidelsen for laengst forbundet naar foerste
514
+ // vaerktoejskald kom. Nu starter uret VED kaldet: binding -> op til 2 s til
515
+ // udvidelsens naeste port-scanning -> probe -> WS-haandtryk, som udvidelsens egen
516
+ // bremse lovligt kan holde i flere sekunder. Budgettet er 5 x 1500 ms.
517
+ //
518
+ // Paa en maskine med mange samtidige chats er marginen tynd - og beskeden nedenfor
519
+ // er den vaerst mulige: den sender en bruger hen for at reparere en installation der
520
+ // virker. Derfor: er porten aabnet inden for de sidste 15 sekunder, giver vi den ét
521
+ // ekstra budget, og siger sandheden hvis den stadig er tom.
522
+ if (doerAabnetNu && !_ekstraRunde) {
523
+ return sendToExtension(method, params, timeoutMs, 5, true, doerAabnetNu);
524
+ }
525
+ if (doerAabnetNu) {
526
+ throw new Error(
527
+ `Porten ${activePort} blev aabnet for ${Math.round((Date.now() - portBundetTid) / 1000)} ` +
528
+ 'sekunder siden, og udvidelsen har ikke naaet at forbinde endnu. Den scanner hvert ' +
529
+ '2. sekund, saa det tager normalt under fem.\n' +
530
+ 'Det er sandsynligvis IKKE en manglende installation - proev kommandoen igen om et ' +
531
+ 'oejeblik. Bliver den ved, saa tjek at Chrome koerer og at udvidelsen er slaaet til.\n' +
532
+ 'Sig det til brugeren i den raekkefoelge. Bed IKKE om en geninstallation foerst.',
533
+ );
384
534
  }
385
535
  // This is the other half of the two-part setup: the server is clearly running (it is
386
536
  // throwing this), so what is missing is the extension, Chrome itself, or the connection
387
- // between them. Say which, and where to get it the agent relays this text to the user.
537
+ // between them. Say which, and where to get it - the agent relays this text to the user.
538
+ if (bindFejl) {
539
+ throw new Error(
540
+ `Serveren kunne ikke aabne en port paa 127.0.0.1 (${BASE_PORT}-${MAX_PORT}): ${bindFejl}\n` +
541
+ 'Det er IKKE Chrome eller udvidelsen - det er operativsystemet eller en firewall der ' +
542
+ 'afviser bindingen. Tjek om noget blokerer loopback-porte.\n' +
543
+ 'Sig praecis dét til brugeren. Sig IKKE at udvidelsen mangler.',
544
+ );
545
+ }
388
546
  if (alleePorteOptaget) {
389
547
  throw new Error(
390
- `Alle porte ${BASE_PORT}-${MAX_PORT} er optaget, saa denne server fik aldrig en port. ` +
391
- 'Det er IKKE et problem med Chrome eller udvidelsen de virker fint.\n' +
392
- `Du har ${MAX_PORT - BASE_PORT + 1} chats i gang der bruger browseren samtidig. ` +
393
- 'Luk en af dem pladsen frigives inden for faa sekunder og genstart saa denne chat.\n' +
548
+ `Alle porte ${BASE_PORT}-${MAX_PORT} er optaget lige nu, saa dette kald fik ingen port. ` +
549
+ 'Det er IKKE et problem med Chrome eller udvidelsen - de virker fint.\n' +
550
+ `${MAX_PORT - BASE_PORT + 1} andre chats bruger browseren i oejeblikket. ` +
551
+ 'Luk en af dem, eller vent til en bliver faerdig - og proev saa kommandoen igen. ' +
552
+ 'Denne chat skal IKKE genstartes: hvert kald proever selv at faa en port.\n' +
394
553
  'Sig praecis dét til brugeren. Sig IKKE at udvidelsen mangler.',
395
554
  );
396
555
  }
@@ -400,7 +559,7 @@ async function sendToExtension(method, params = {}, timeoutMs = 30000, _retries
400
559
  'Chrome extension (apparently not reachable).\n' +
401
560
  'Check, in order:\n' +
402
561
  ' 1. Chrome is actually open and running.\n' +
403
- ' 2. The extension is installed and enabled at chrome://extensions install it from\n' +
562
+ ' 2. The extension is installed and enabled at chrome://extensions - install it from\n' +
404
563
  ' https://chromewebstore.google.com/detail/agent360-browser-mcp/jdehgalffmffhfhmmhaokfbfnafnmgcl\n' +
405
564
  ' 3. Click the extension icon -> Reconnect, and wait 2-3 seconds.\n' +
406
565
  'Still stuck: https://browsermcp.dev/docs/troubleshooting/'
@@ -426,14 +585,14 @@ const INSTRUCTIONS = `You control the user's real Chrome browser via this MCP se
426
585
 
427
586
  ## Key behaviors
428
587
  - **Always use browser_ask_user** when you need credentials, 2FA codes, CAPTCHA help, or any user input. Never guess passwords or tokens.
429
- - **ALWAYS close tabs when done** with browser_close_tab after completing each task. Don't leave tabs open close them immediately after extracting the data you need. Use browser_list_tabs to find and close all session tabs when a task is complete.
430
- - **Check existing tabs first** with browser_list_tabs before navigating reuse tabs instead of opening duplicates.
431
- - **One task per tab** navigate to a URL, do your work, then close or move on.
588
+ - **ALWAYS close tabs when done** with browser_close_tab after completing each task. Don't leave tabs open - close them immediately after extracting the data you need. Use browser_list_tabs to find and close all session tabs when a task is complete.
589
+ - **Check existing tabs first** with browser_list_tabs before navigating - reuse tabs instead of opening duplicates.
590
+ - **One task per tab** - navigate to a URL, do your work, then close or move on.
432
591
  - **Tell the user what you're doing** in the browser. "I'm navigating to Stripe to find the API key" not just silently calling tools.
433
592
 
434
593
  ## Tab management
435
594
  - navigate creates tabs in your session's tab group (visible in Chrome as colored groups)
436
- - list_tabs only shows YOUR session's tabs other Claude sessions have their own
595
+ - list_tabs only shows YOUR session's tabs - other Claude sessions have their own
437
596
  - switch_tab lets you jump between your tabs
438
597
  - close_tab cleans up when you're done
439
598
 
@@ -446,27 +605,31 @@ const INSTRUCTIONS = `You control the user's real Chrome browser via this MCP se
446
605
  6. After success, extract what you need with browser_get_page_content
447
606
 
448
607
  ## Screenshots
449
- - browser_screenshot captures the visible tab useful for visual verification
450
- - The tab is auto-activated before capture, so it always shows the right page
608
+ - browser_screenshot captures YOUR session's tab, without pulling it in front of the user
609
+ - It does NOT activate the tab first. That was removed deliberately: the user sits in the
610
+ same window, and yanking their tab away on every screenshot is worse than the alternative
611
+ - The picture always comes from the debugger for YOUR tab. There is no fallback that photographs
612
+ whichever tab happens to be visible, so if the debugger cannot produce a frame the call fails.
613
+ If the whole window is covered, it may be raised briefly as a last resort
451
614
 
452
615
  ## Text-based selectors (preferred for dynamic sites)
453
- - browser_click("text=Get started") clicks any element containing "Get started"
454
- - browser_click("button:text(Submit)") clicks a button containing "Submit"
455
- - browser_fill("text=Email", "user@example.com") fills input near "Email" label
456
- - browser_wait("text=Success") waits for text to appear
616
+ - browser_click("text=Get started") - clicks any element containing "Get started"
617
+ - browser_click("button:text(Submit)") - clicks a button containing "Submit"
618
+ - browser_fill("text=Email", "user@example.com") - fills input near "Email" label
619
+ - browser_wait("text=Success") - waits for text to appear
457
620
  - These work on ALL sites including Google Cloud, Stripe, Slack (CSP-strict)
458
621
 
459
622
  ## Keyboard
460
- - browser_press_key("Enter") submit forms
461
- - browser_press_key("Tab") navigate between fields
462
- - browser_press_key("Escape") close dialogs
463
- - browser_press_key("ArrowDown") navigate dropdowns
464
- - browser_press_key("a", ctrl=true) select all
623
+ - browser_press_key("Enter") - submit forms
624
+ - browser_press_key("Tab") - navigate between fields
625
+ - browser_press_key("Escape") - close dialogs
626
+ - browser_press_key("ArrowDown") - navigate dropdowns
627
+ - browser_press_key("a", ctrl=true) - select all
465
628
 
466
629
  ## CAPTCHA handling
467
630
  Use browser_solve_captcha to detect and solve CAPTCHAs automatically:
468
- 1. Call browser_solve_captcha() detects CAPTCHA type on page
469
- 2. If reCAPTCHA v2 checkbox found → call browser_solve_captcha(action="click_checkbox") auto-clicks; often passes when signed into Google
631
+ 1. Call browser_solve_captcha() - detects CAPTCHA type on page
632
+ 2. If reCAPTCHA v2 checkbox found → call browser_solve_captcha(action="click_checkbox") - auto-clicks; often passes when signed into Google
470
633
  3. If image challenge appears → call browser_screenshot, analyze the grid visually, then call browser_solve_captcha(action="click_grid", cells=[2,5,7]) with the correct cell indices
471
634
  4. If all else fails → call browser_solve_captcha(action="ask_human") to show overlay to user
472
635
  5. After solving, retry the action that was blocked
@@ -482,7 +645,7 @@ For image grid challenges: cells are 0-indexed, left-to-right, top-to-bottom. A
482
645
  - If a standard selector fails, the extension recursively searches shadow roots
483
646
  - Text-based selectors ("text=Submit") also traverse shadow DOM
484
647
 
485
- ## Hard inputs use the specialised tools first
648
+ ## Hard inputs - use the specialised tools first
486
649
  - **Date inputs** → use browser_set_date (NOT browser_fill). Handles native date inputs, masked text inputs (MM/DD/YYYY etc.), AND calendar pickers (MUI, react-datepicker, AntD, Lexical/Meta). 3-path fallback with read-back verification.
487
650
  - **Autocomplete / combobox** (Languages on Meta Ads, country selects, async dropdowns) → use browser_set_combobox (NOT browser_select_option). Types partial query, waits for filtered listbox, clicks option. Supports multi-value chips.
488
651
  - **Drag-drop file zones without visible file input** → use browser_drop_file (NOT browser_upload_file). Finds hidden input in subtree/parent.
@@ -492,28 +655,31 @@ For image grid challenges: cells are 0-indexed, left-to-right, top-to-bottom. A
492
655
  - Element not found → try text-based selector instead of CSS
493
656
  - Screenshot fails → debugger fallback is automatic
494
657
  - Click doesn't work on SPA → debugger mouse events are used automatically
658
+ - A click, hover or key press answers "CDP svarede ikke inden … ms" → the tab is in the background, and Chrome does not deliver mouse or key input to a tab that is not active. Call browser_switch_tab to that tab, then try again (browser_click on a CSS selector already falls back to a script click)
659
+ - An answer with maaske_landet: true means the action was sent but its effect could not be confirmed. Check the page first (browser_get_page_content or browser_screenshot) and do not repeat it blindly: a second click can submit twice. landed: false means the page showed no visible reaction to the click. landed: null with uvist: true means something on the page changed when the mouse went down, but not from the click itself - it may be a ripple effect, and it may be a menu that opens on mousedown. Read the page before clicking again: a second click closes a menu that is already open. landed: null with uverificeret: true means the mouse button was sent but the page could not be read afterwards - same rule: read the page before repeating. browser_scroll uses uvist with its own meaning - there it means the scroll was sent but the movement could not be seen, and the answer carries ok:true, a note and the measured position
660
+ - browser_fill with afviger: true means the field shows something other than what you typed; read faktisk. uaendret: true means the field showed the same before and after, either because the value was already there in the page's own format or because the page refused it. Check faktisk before moving on
495
661
  - CAPTCHA blocks page → use browser_ask_user, let human solve it
496
662
  - browser_fill seemingly succeeds but value reverts → switch to browser_set_date or browser_set_combobox (most reverts are React-controlled validators)
497
663
 
498
664
  ## Extension updates
499
- The MCP server auto-pulls the latest code from git on every new session startup.
500
- If the extension files were updated, ask the user to reload it:
501
- "The Browser MCP extension was updated. Please go to chrome://extensions, find 'Agent360 Browser MCP', and click the reload icon (🔄) to apply the update."
502
- You cannot navigate to chrome:// pages the user must do this manually.
503
-
504
- ## When Browser MCP itself is the obstacle call browser_provide_feedback
505
- Before you tell the user "browser-mcp can't do X", or give up on a tool that errored, timed out, or did something other than what it promised: call **browser_provide_feedback** with what_happened. Do it yourself, without asking. It checks the install first this server against the latest on npm, the extension against this server, and whether more than one Browser MCP extension is connected at once because those three explain most "it's broken" moments, and none of them are visible to you otherwise. Read the verdict:
506
- - **outdated** or **disconnected** → relay fix_steps to the user, then retry the action. An outdated install explains most failures, so try the fix first but if the behaviour still looks wrong after that, report it anyway. A release window is not a reason to stay silent about a real bug.
665
+ The server updates through npm: clients configured with @agent360/browser-mcp@latest get the newest version when a session starts. When that version carries newer extension files, the server copies them into ~/.browser-mcp/extension and tells the connected extension to reload itself. Chrome Web Store installs update through the store.
666
+ If browser_provide_feedback reports that the extension is older than this server, or that more than one Browser MCP extension is connected, ask the user:
667
+ "Please go to chrome://extensions, find 'Agent360 Browser MCP', and click the reload icon (🔄). If two Browser MCP extensions are listed, turn one of them off."
668
+ You cannot navigate to chrome:// pages - the user must do this manually.
669
+
670
+ ## When Browser MCP itself is the obstacle - call browser_provide_feedback
671
+ Before you tell the user "browser-mcp can't do X", or give up on a tool that errored, timed out, or did something other than what it promised: call **browser_provide_feedback** with what_happened. Do it yourself, without asking. It checks the install first - the extension against this server, whether more than one Browser MCP extension is connected at once, and (only when BROWSER_MCP_CHECK_NPM=1) this server against the latest on npm - because those explain most "it's broken" moments, and none of them are visible to you otherwise. Read the verdict:
672
+ - **outdated** or **disconnected** → relay fix_steps to the user, then retry the action. An outdated install explains most failures, so try the fix first - but if the behaviour still looks wrong after that, report it anyway. A release window is not a reason to stay silent about a real bug.
507
673
  - **conflict** → more than one extension is loaded; tabs and sessions will keep behaving randomly until the user disables the extras at chrome://extensions. Say so plainly.
508
674
  - **current** → the install is fine, so this is a genuine gap. Offer the returned submit_url as a clickable link.
509
675
  - **unknown** → nothing local looks wrong, but the npm comparison was skipped (it is opt-in via BROWSER_MCP_CHECK_NPM=1). Treat it like **current**.
510
676
  Call it once per distinct obstacle, not once per retry.
511
677
 
512
678
  ## Sharing wishes / use-cases / bugs
513
- Browser MCP is community-shaped. Whenever the user (a) says "I wish browser-mcp could…" or expresses a missing feature, (b) hits something broken or unexpected that looks like a bug in browser-mcp itself, or (c) describes something cool they built with it call **browser_about** with the matching intent ("wish" / "use_case" / "bug") and a short title + body, then offer the returned submit_url to the user as a clickable link. Don't ask permission, just draft + offer the link. This is the user's path to contribute back, and they usually don't know it exists.`;
679
+ Browser MCP is community-shaped. Whenever the user (a) says "I wish browser-mcp could…" or expresses a missing feature, (b) hits something broken or unexpected that looks like a bug in browser-mcp itself, or (c) describes something cool they built with it - call **browser_about** with the matching intent ("wish" / "use_case" / "bug") and a short title + body, then offer the returned submit_url to the user as a clickable link. Don't ask permission, just draft + offer the link. This is the user's path to contribute back, and they usually don't know it exists.`;
514
680
 
515
681
  // FEJL MAALT 21/8: `instructions` laa i et TREDJE argument. Server-konstruktoeren
516
- // tager kun to (serverInfo, options), saa JavaScript smed objektet vaek i tavshed
682
+ // tager kun to (serverInfo, options), saa JavaScript smed objektet vaek i tavshed -
517
683
  // og hele blokken ovenfor naaede aldrig frem til nogen klient. Verificeret ved at
518
684
  // laese initialize-svaret: det havde kun protocolVersion, capabilities og serverInfo.
519
685
  // Det betyder at "luk altid faner naar du er faerdig", CAPTCHA-fremgangsmaaden,
@@ -590,6 +756,62 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
590
756
  return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
591
757
  }
592
758
 
759
+ // MAALT 10/9: skaermbilledets `path` fik en indeslutning 23/8, med begrundelsen
760
+ // "argumenterne kommer fra en model der laeser FREMMEDE websider". Praecis samme
761
+ // argument gaelder upload - og DER forlader filen faktisk maskinen. Stien gik raat
762
+ // videre til DOM.setFileInputFiles, saa upload_file({files:["~/.ssh/id_rsa"]}) paa en
763
+ // vilkaarlig side med et filfelt lagde noeglen i en upload. Vagten manglede netop hvor
764
+ // konsekvensen var stoerst.
765
+ if (method === 'upload_file' || method === 'drop_file') {
766
+ const raa = Array.isArray(args?.files) ? args.files
767
+ // Astra, tredje runde: aliaserne blev lagt SAMMEN, saa file + file_path sendte to filer.
768
+ // Udvidelsen valgte altid den foerste der fandtes - samme prioritet her.
769
+ : [args?.files || args?.file || args?.file_path].filter(Boolean);
770
+ const rod = resolve(process.cwd());
771
+ // MAALT 10/9 af Astra (anden runde), reproduceret paa denne maskine: resolve() fjerner
772
+ // "link/.." som TEKST foer symlinket er fulgt. Med link -> /ude/dir blev "link/../secret"
773
+ // tjekket som <cwd>/secret, mens den UAENDREDE sti blev sendt videre, og operativsystemet
774
+ // laeste /ude/secret. Node's egen realpathSync normaliserer ogsaa foerst - den gav ENOENT,
775
+ // og saa blev den leksikalske sti godkendt. realpathSync.native spoerger operativsystemet,
776
+ // der foelger links foer "..". Det svar tjekkes, og det er DET der sendes videre: ellers kan
777
+ // det der godkendes og det der aabnes vaere to forskellige filer. (Den giver ogsaa den rigtige
778
+ // bogstavstoerrelse, saa /users/... ikke afvises paa macOS.)
779
+ // Tilbage staar et smalt vindue: en proces med skriveadgang til arbejdsmappen kan bytte en
780
+ // mappe ud med et symlink mellem tjekket og det oejeblik Chrome aabner filen.
781
+ let rodReel = rod; try { rodReel = realpathSync.native(rod); } catch {}
782
+ // MAALT 11/9 af Astra (R5 F10): med arbejdsmappen "/" blev praefikset "//", og enhver almindelig fil
783
+ // blev afvist som "udenfor". En rod der allerede ender paa skilletegnet faar ikke et til.
784
+ const medSkille = (r) => (r.endsWith(sep) ? r : r + sep);
785
+ const inde = (p, r) => p === r || p.startsWith(medSkille(r));
786
+ const kanoniske = [];
787
+ for (const f of raa) {
788
+ const udfoldet = String(f).replace(/^~(?=\/|$)/, homedir());
789
+ const raaSti = isAbsolute(udfoldet) ? udfoldet : medSkille(rod) + udfoldet;
790
+ let reel = null; try { reel = realpathSync.native(raaSti); } catch {}
791
+ const afvis = (hvorfor) => ({
792
+ content: [{ type: 'text', text:
793
+ `Filen skal ligge inden for arbejdsmappen (${rod}). "${f}" ${hvorfor}.\n` +
794
+ `Uploads sender filen til en fremmed side, og stien kommer fra en model der ` +
795
+ `laeser de sider. Kopiér filen ind i arbejdsmappen foerst, hvis den skal med.` }],
796
+ isError: true,
797
+ });
798
+ if (!reel) return afvis(inde(resolve(raaSti), rod) ? 'findes ikke (eller kan ikke laeses)' : 'peger udenfor');
799
+ if (!inde(reel, rodReel)) return afvis('peger udenfor');
800
+ // Astra, tredje runde: en MAPPE blev godkendt ud fra mappens egen sti - men Chrome gennemloeber
801
+ // mappen og foelger links i den, saa bundle/key -> ~/.ssh/id_rsa kom med. Og en HARDLINK inde i
802
+ // mappen er samme fil som en fil udenfor; realpath kan ikke se det.
803
+ let st = null; try { st = statSync(reel); } catch {}
804
+ if (!st || !st.isFile()) return afvis('er ikke en almindelig fil (mapper uploades ikke - de kan indeholde links ud af arbejdsmappen)');
805
+ if (st.nlink > 1) return afvis('har flere navne paa disken (hardlink) og kan vaere en fil uden for arbejdsmappen');
806
+ kanoniske.push(reel);
807
+ }
808
+ if (args && kanoniske.length) {
809
+ args.files = kanoniske;
810
+ delete args.file;
811
+ delete args.file_path;
812
+ }
813
+ }
814
+
593
815
  // extract_list scrolls a container in a loop (up to 300 rounds × wait_ms), so the 30 s
594
816
  // default would kill a long mail list mid-walk and report a partial set as complete.
595
817
  const timeout = method === 'ask_user' ? (args?.timeout || 120000) + 5000 :
@@ -610,12 +832,31 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
610
832
  // overskrive en fil i brugerens hjemmemappe med PNG-bytes.
611
833
  const rod = resolve(process.cwd());
612
834
  const targetPath = resolve(rod, args.path);
613
- if (targetPath !== rod && !targetPath.startsWith(rod + sep)) {
835
+ if (targetPath !== rod && !targetPath.startsWith(rod.endsWith(sep) ? rod : rod + sep)) { // "/" giver ikke "//" (R5 F10)
614
836
  throw new Error(
615
837
  `path skal ligge inden for arbejdsmappen (${rod}). ` +
616
838
  `"${args.path}" peger udenfor. Brug en relativ sti uden ../.`,
617
839
  );
618
840
  }
841
+ // MAALT 10/9 af Astra (anden runde): tjekket ovenfor er kun tekst. "ud/x.png" med ud -> en mappe
842
+ // udenfor passerede, og PNG-bytes blev skrevet udenfor. Nu tjekkes det operativsystemet faktisk
843
+ // ville skrive til: stien maa ikke selv vaere et symlink (heller ikke et dinglende, som ville
844
+ // skabe filen i den anden ende), og den dybeste del af stien der findes skal ligge inde.
845
+ // lstat - ikke exists - saa et dinglende link taeller som "findes" og derfor bliver undersoegt.
846
+ const findes = (x) => { try { lstatSync(x); return true; } catch { return false; } };
847
+ let rodReel = rod; try { rodReel = realpathSync.native(rod); } catch {}
848
+ let forfader = dirname(targetPath);
849
+ while (!findes(forfader) && dirname(forfader) !== forfader) forfader = dirname(forfader);
850
+ let forfaderReel = null; try { forfaderReel = realpathSync.native(forfader); } catch {}
851
+ // Astra, tredje runde: en eksisterende HARDLINK er ikke et symlink, men skrivningen trunkerer den faelles fil.
852
+ let erLink = false, flereNavne = false;
853
+ try { const st = lstatSync(targetPath); erLink = st.isSymbolicLink(); flereNavne = st.nlink > 1; } catch {}
854
+ if (erLink || flereNavne || !forfaderReel || (forfaderReel !== rodReel && !forfaderReel.startsWith(rodReel.endsWith(sep) ? rodReel : rodReel + sep))) {
855
+ throw new Error(
856
+ `path skal ligge inden for arbejdsmappen (${rod}). ` +
857
+ `"${args.path}" peger udenfor (via et link). Brug en almindelig mappe i arbejdsmappen.`,
858
+ );
859
+ }
619
860
  mkdirSync(dirname(targetPath), { recursive: true });
620
861
  writeFileSync(targetPath, Buffer.from(base64, 'base64'));
621
862
  return {
@@ -651,7 +892,7 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
651
892
  }
652
893
  });
653
894
 
654
- // Naar udvidelsen er aeldre end serveren, svarer den `Unknown method: X` og det er
895
+ // Naar udvidelsen er aeldre end serveren, svarer den `Unknown method: X` - og det er
655
896
  // alt brugeren ser. Det sker GARANTERET: serveren kommer fra npm og opdateres straks,
656
897
  // mens udvidelsen skal gennem Chrome Web Stores review paa 1-3 dage. I det vindue
657
898
  // findes otte vaerktoejer i serveren som en 1.25.0-udvidelse ikke kender
@@ -659,7 +900,7 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
659
900
  // udklipsholder-vaerktoejer, alle fra v1.26.0).
660
901
  //
661
902
  // Serveren VED at udvidelsen er gammel: den sendte intet haandtryk. Saa i stedet for
662
- // en gaadefuld fejl faar brugeren at vide hvorfor og hvad de kan goere imens.
903
+ // en gaadefuld fejl faar brugeren at vide hvorfor - og hvad de kan goere imens.
663
904
  const ERSTATNINGER = {
664
905
  double_click: 'kald `browser_click` to gange',
665
906
  right_click: 'brug `browser_execute_script` med et contextmenu-event',
@@ -678,7 +919,7 @@ function forklarSkaevhed(besked) {
678
919
  const alt = ERSTATNINGER[m[1]];
679
920
  return `Error: browser_${m[1]} findes i denne server, men ikke i din Chrome-udvidelse.\n\n` +
680
921
  'Udvidelsen opdateres via Chrome Web Store og kan vaere 1-3 dage bagud efter en ' +
681
- 'udgivelse serveren opdateres med det samme via npm. Alt andet virker imens.\n' +
922
+ 'udgivelse - serveren opdateres med det samme via npm. Alt andet virker imens.\n' +
682
923
  (alt ? `\nIndtil da: ${alt}.\n` : '') +
683
924
  '\nTjek om en opdatering venter: chrome://extensions → Agent360 Browser MCP. ' +
684
925
  'Er den indlaest som "unpacked", saa koer `npx @agent360/browser-mcp install`.';
@@ -700,11 +941,11 @@ function handleAbout(args) {
700
941
 
701
942
  const instruction =
702
943
  intent === 'wish'
703
- ? `Share this exact submission link with the user as a clickable link, with a short note like "Click to submit your wish it'll open a pre-filled GitHub issue you can review before submitting": ${submit_url}`
944
+ ? `Share this exact submission link with the user as a clickable link, with a short note like "Click to submit your wish - it'll open a pre-filled GitHub issue you can review before submitting": ${submit_url}`
704
945
  : intent === 'use_case'
705
- ? `Share this exact submission link with the user as a clickable link, with a short note like "Click to share your use-case pre-filled, you can edit before submitting": ${submit_url}`
946
+ ? `Share this exact submission link with the user as a clickable link, with a short note like "Click to share your use-case - pre-filled, you can edit before submitting": ${submit_url}`
706
947
  : intent === 'bug'
707
- ? `Share this exact bug-report link with the user as a clickable link, with a short note like "Click to report pre-filled, please add reproduction steps before submitting": ${submit_url}`
948
+ ? `Share this exact bug-report link with the user as a clickable link, with a short note like "Click to report - pre-filled, please add reproduction steps before submitting": ${submit_url}`
708
949
  : `Browser MCP is community-shaped. Open wishlist: ${REPO_URL}/blob/main/WISHLIST.md · Use-cases: ${REPO_URL}/blob/main/USE_CASES.md · Submit anything: ${REPO_URL}/issues/new/choose`;
709
950
 
710
951
  return {
@@ -726,13 +967,13 @@ function handleAbout(args) {
726
967
  // ── Selv-diagnose: er installationen overhovedet frisk? ─────────────────────
727
968
  // Baggrund (21/8): den udgave der koerte lokalt var npm 1.25.0, mens rettelserne
728
969
  // laa uudgivet i repoet. Fejlen viste sig som "sessioner opfoerer sig underligt",
729
- // ikke som "du koerer en gammel version" og der fandtes ingen maade at spoerge
970
+ // ikke som "du koerer en gammel version" - og der fandtes ingen maade at spoerge
730
971
  // paa. Derfor spoerger vaerktoejet selv, foer det konkluderer noget som helst.
731
972
 
732
973
  // Friskheds-tjek mod npm. SLUKKET SOM STANDARD siden 22/8.
733
974
  //
734
- // Produktet lover paa forsiden "nothing leaves your machine ... no telemetry", og et
735
- // opslag i npm-registret ER et kald ud af maskinen ogsaa selv om det kun sender et
975
+ // Produktet lover paa forsiden at intet sendes til Agent360 og at der ingen telemetri er, og et
976
+ // opslag i npm-registret ER et kald ud af maskinen - ogsaa selv om det kun sender et
736
977
  // pakkenavn og ingen brugerdata. Loeftet vejer tungere end bekvemmeligheden, saa
737
978
  // tjekket er nu opt-in: saet BROWSER_MCP_CHECK_NPM=1.
738
979
  //
@@ -766,7 +1007,7 @@ function npmLatestVersion() {
766
1007
  // rettes. Logbogen ligger LOKALT og forlader ikke maskinen.
767
1008
  //
768
1009
  // Bevidst ikke auto-indsendelse til et offentligt GitHub-issue: rapporten baerer URL og
769
- // fejltekst fra den side agenten stod paa og det er ofte en annoncekonto, en indbakke
1010
+ // fejltekst fra den side agenten stod paa - og det er ofte en annoncekonto, en indbakke
770
1011
  // eller et kundesystem. Et offentligt issue kan ikke tages tilbage.
771
1012
  const FEEDBACK_LOG = join(homedir(), '.browser-mcp', 'feedback.jsonl');
772
1013
  const setteFingeraftryk = new Set(); // samme graense logges én gang pr. serverliv
@@ -782,7 +1023,7 @@ function fingeraftryk(kind, tool, what) {
782
1023
  }
783
1024
 
784
1025
  // URL'en reduceres til oprindelse + sti. Query og fragment baerer tokens, sessions-id'er
785
- // og soegetermer de har intet at goere i en logbog nogen senere kopierer ind i et issue.
1026
+ // og soegetermer - de har intet at goere i en logbog nogen senere kopierer ind i et issue.
786
1027
  function afkortUrl(u) {
787
1028
  if (!u) return null;
788
1029
  try { const x = new URL(u); return x.origin + x.pathname; } catch { return '(ulaeselig url)'; }
@@ -838,16 +1079,20 @@ async function handleProvideFeedback(args) {
838
1079
  );
839
1080
  fix_steps.push(
840
1081
  'Aabn chrome://extensions og slaa alle Browser MCP-udvidelser fra paa naer én. ' +
841
- 'Det skal brugeren selv goere chrome:// kan ikke styres herfra. Behold den nyeste.',
1082
+ 'Det skal brugeren selv goere - chrome:// kan ikke styres herfra. Behold den nyeste.',
842
1083
  );
843
1084
  }
844
- if (!active) {
1085
+ if (!active && activePort === null) {
1086
+ // Ingen port taget endnu = browseren er ikke brugt i denne chat. Der er intet
1087
+ // i stykker, og et fix-skridt her ville vaere en falsk alarm - se verdict 'idle'.
1088
+ findings.push('Browseren er ikke taget i brug i denne chat endnu, saa der er ingen forbindelse at maale paa. Det er ikke en fejl.');
1089
+ } else if (!active) {
845
1090
  findings.push('Ingen Chrome-udvidelse er forbundet til denne MCP-server lige nu.');
846
1091
  fix_steps.push('Tjek at Chrome koerer og at udvidelsen er slaaet til paa chrome://extensions, klik derefter paa ikonet → Reconnect.');
847
1092
  }
848
1093
  if (serverOutdated) {
849
1094
  findings.push(`MCP-serveren koerer v${PKG_VERSION}, men npm har v${npmLatest}. Fejlen kan allerede vaere rettet.`);
850
- fix_steps.push(`Genstart klienten den henter selv @agent360/browser-mcp@latest (v${npmLatest}).`);
1095
+ fix_steps.push(`Genstart klienten - den henter selv @agent360/browser-mcp@latest (v${npmLatest}).`);
851
1096
  }
852
1097
  if (extOutdated) {
853
1098
  findings.push(
@@ -856,25 +1101,44 @@ async function handleProvideFeedback(args) {
856
1101
  : `Udvidelsen er v${extVersion}, serveren er v${PKG_VERSION}. Udvidelsen mangler rettelser fra de mellemliggende udgaver.`,
857
1102
  );
858
1103
  // MAALT 22/8: "↻ reload" er ubrugeligt for en Chrome Web Store-bruger. Butikken
859
- // skubber paa Googles tidsplan efter et review paa 1-3 dage der er ingen nyere
1104
+ // skubber paa Googles tidsplan efter et review paa 1-3 dage - der er ingen nyere
860
1105
  // version at hente endnu, saa raadet foerer i ring. Serveren kan ikke se hvilken
861
1106
  // slags installation det er (den gamle udvidelse oplyser intet), saa begge tilfaelde
862
- // skal staa der og det skal siges at ventetiden er forventet, ikke en fejl.
1107
+ // skal staa der - og det skal siges at ventetiden er forventet, ikke en fejl.
863
1108
  fix_steps.push(
864
1109
  extVersion === null
865
1110
  ? 'Kommer udvidelsen fra Chrome Web Store: der er sandsynligvis en nyere version i review ' +
866
- '(1-3 dage efter en udgivelse). ↻ reload henter den IKKE foer Google har godkendt ' +
1111
+ '(1-3 dage efter en udgivelse). ↻ reload henter den IKKE foer Google har godkendt - ' +
867
1112
  'det er forventet og gaar over af sig selv. Alt andet virker imens. ' +
868
1113
  'Er den indlaest som "unpacked": koer `npx @agent360/browser-mcp install` og derefter ' +
869
1114
  'chrome://extensions → Agent360 Browser MCP → ↻ reload.'
870
- : 'Opdatér udvidelsen: chrome://extensions Agent360 Browser MCP reload. ' +
871
- 'Er den indlaest som "unpacked", saa koer `npx @agent360/browser-mcp install` foerst.',
1115
+ // MAALT 11/9 af Fable (e2e-review): ogsaa med kendt version kan det vaere en Chrome Web Store-installation, og saa
1116
+ // henter reload ingenting foer Google har godkendt. Begge tilfaelde skal staa der, ellers foerer raadet i ring.
1117
+ : 'Er udvidelsen indlaest som "unpacked": koer `npx @agent360/browser-mcp install` og derefter ' +
1118
+ 'chrome://extensions → Agent360 Browser MCP → ↻ reload. Kommer den fra Chrome Web Store: den nye version ' +
1119
+ 'ligger sandsynligvis i review (1-3 dage efter en udgivelse), og ↻ reload henter den IKKE foer Google har ' +
1120
+ 'godkendt - det er forventet og gaar over af sig selv. Alt andet virker imens.',
872
1121
  );
873
1122
  }
874
1123
 
1124
+ // ── "ingen forbindelse" og "ingen port endnu" er IKKE det samme ────────────
1125
+ //
1126
+ // FUNDET AF REVIEW 7/9. Foer porten blev doven, bandt hver server ved opstart, saa
1127
+ // udvidelsen var altid forbundet naar dette vaerktoej blev kaldt - og `!active`
1128
+ // betoed derfor paalideligt "udvidelsen kan ikke naas". Nu binder en chat foerst en
1129
+ // port naar den bruger browseren, saa en HELT SUND chat der ikke har roert den kan
1130
+ // staa uden forbindelse. Uden det her skel fik den `disconnected` + fix_steps der
1131
+ // bad brugeren geninstallere - og INSTRUCTIONS beder agenten viderebringe dem.
1132
+ // Vi ville altsaa fortaelle folk at deres installation var i stykker, fordi vi selv
1133
+ // endnu ikke havde aabnet doeren.
1134
+ //
1135
+ // Loesningen er ikke at binde en port her (et diagnose-vaerktoej skal ikke aendre
1136
+ // tilstand for at kunne maale den) - det er at sige praecis hvad der er tilfaeldet.
1137
+ const ingenPortEndnu = activePort === null;
875
1138
  const verdict =
876
1139
  exts.length > 1 ? 'conflict'
877
1140
  : (serverOutdated || extOutdated) ? 'outdated'
1141
+ : (!active && ingenPortEndnu) ? 'idle'
878
1142
  : !active ? 'disconnected'
879
1143
  : (npmLatest === null ? 'unknown' : 'current');
880
1144
 
@@ -886,6 +1150,8 @@ async function handleProvideFeedback(args) {
886
1150
  name: c.name,
887
1151
  version: c.version,
888
1152
  extension_id: c.extensionId,
1153
+ // Fingeraftryk af udvidelsens background.js - det er KODEN, ikke versionsnummeret, udgivelsens gate skal se.
1154
+ code: c.kode ?? null,
889
1155
  active: c === active,
890
1156
  })),
891
1157
  extension_up_to_date: extOutdated === null ? null : !extOutdated,
@@ -898,7 +1164,7 @@ async function handleProvideFeedback(args) {
898
1164
  what && `**What happened**\n${what}`,
899
1165
  tool && `\n**Tool**: \`${tool}\``,
900
1166
  // MAALT 22/8 ved sikkerhedsreview: her stod den RAA url, mens den lokale logbog
901
- // nedenfor bruger afkortUrl(). Query-strengen hvor tokens bor blev altsaa
1167
+ // nedenfor bruger afkortUrl(). Query-strengen - hvor tokens bor - blev altsaa
902
1168
  // strippet fra filen paa disken, men sendt uredigeret ind i et link til et
903
1169
  // OFFENTLIGT GitHub-issue. Praecis den forkerte vej rundt.
904
1170
  url && `\n**URL**: ${afkortUrl(url)}`,
@@ -914,11 +1180,15 @@ async function handleProvideFeedback(args) {
914
1180
  `&title=${encodeURIComponent(issueTitle)}&body=${encodeURIComponent(issueBody)}`;
915
1181
 
916
1182
  const instruction =
917
- verdict === 'conflict' || verdict === 'outdated' || verdict === 'disconnected'
1183
+ verdict === 'idle'
1184
+ ? 'Browseren er ikke taget i brug i denne chat endnu, saa der er intet at diagnosticere paa forbindelsen - ' +
1185
+ 'det er IKKE en fejl i installationen, og du maa ikke sige det til brugeren. ' +
1186
+ 'Er der en aegte mangel, saa tilbyd submit_url som et klikbart link.'
1187
+ : verdict === 'conflict' || verdict === 'outdated' || verdict === 'disconnected'
918
1188
  ? 'Fortael brugeren hvad der blev fundet, og giv fix_steps som konkrete skridt. Proev derefter handlingen igen. ' +
919
- 'Del KUN submit_url hvis problemet stadig staar efter at fix_steps er fulgt det er sandsynligvis installationen, ikke en fejl i Browser MCP.'
1189
+ 'Del KUN submit_url hvis problemet stadig staar efter at fix_steps er fulgt - det er sandsynligvis installationen, ikke en fejl i Browser MCP.'
920
1190
  : 'Installationen er frisk, saa det her er sandsynligvis en aegte mangel eller fejl. Fortael brugeren kort hvad der ikke kunne lade sig goere, ' +
921
- 'og tilbyd submit_url som et klikbart link ("forudfyldt du kan rette i den foer du sender"). Spoerg ikke om lov foerst.';
1191
+ 'og tilbyd submit_url som et klikbart link ("forudfyldt - du kan rette i den foer du sender"). Spoerg ikke om lov foerst.';
922
1192
 
923
1193
  const logbog = skrivTilLogbog({
924
1194
  at: new Date().toISOString(),
@@ -970,18 +1240,19 @@ async function handleExtractToken(args) {
970
1240
 
971
1241
  // ── Graceful shutdown ──────────────────────────────────────────────────────
972
1242
  // All shutdown paths funnel through gracefulShutdown so the cleanup chain runs
973
- // deterministically even on abrupt parent-exit. Without this, process.exit(0)
1243
+ // deterministically - even on abrupt parent-exit. Without this, process.exit(0)
974
1244
  // was racing against WS close-handshake, leaving zombie tabs in Chrome.
975
1245
 
976
1246
  let shuttingDown = false;
977
1247
  function gracefulShutdown(reason, code = 0) {
978
1248
  if (shuttingDown) return;
979
1249
  shuttingDown = true;
980
- process.stderr.write(`[MCP] ${reason} shutting down\n`);
1250
+ process.stderr.write(`[MCP] ${reason} - shutting down\n`);
981
1251
 
982
1252
  // Stop timers so they can't re-enter gracefulShutdown
983
1253
  if (parentCheck) clearInterval(parentCheck);
984
1254
  if (heartbeat) clearInterval(heartbeat);
1255
+ if (tomgangsvagt) clearInterval(tomgangsvagt);
985
1256
 
986
1257
  // Close WS with explicit close-frame so extension's onclose handler fires
987
1258
  for (const c of liveConnections()) {
@@ -1009,7 +1280,7 @@ process.on('exit', () => {
1009
1280
  // Claude Code → wrapper → npm exec → denne server
1010
1281
  //
1011
1282
  // `process.ppid` er `npm exec`, ikke Claude Code. Doer chatten, kan `npm exec`
1012
- // blive haengende som foraeldreloes og saa ser tjekket en levende foraelder for
1283
+ // blive haengende som foraeldreloes - og saa ser tjekket en levende foraelder for
1013
1284
  // evigt. Porten blev holdt i op til fire timer (idle-graensen), og med flere
1014
1285
  // forladte chats loeb spaendet fuldt.
1015
1286
  //
@@ -1019,7 +1290,7 @@ process.on('exit', () => {
1019
1290
  //
1020
1291
  // Kaeden hentes én gang ved opstart (én ps-kommando), og derefter koster tjekket
1021
1292
  // kun et signal 0 pr. led hvert 5. sekund. Kan kaeden ikke laeses (Windows, eller
1022
- // ps mangler), falder vi tilbage til det gamle enkelt-tjek daarligere, men aldrig
1293
+ // ps mangler), falder vi tilbage til det gamle enkelt-tjek - daarligere, men aldrig
1023
1294
  // vaerre end foer.
1024
1295
 
1025
1296
  // ps-opslaget bor her (det er en sideeffekt); selve kaede-logikken og doeds-dommen
@@ -1040,7 +1311,7 @@ function laesPpid(pid) {
1040
1311
  const parentPid = process.ppid;
1041
1312
 
1042
1313
  // MAALT 22/8: doer den naermeste foraelder FOER serveren er bootet (~2 sek node+SDK),
1043
- // laeser process.ppid vaerdien 1 altsaa launchd. Kaeden blev saa [1], og pid 1 er
1314
+ // laeser process.ppid vaerdien 1 - altsaa launchd. Kaeden blev saa [1], og pid 1 er
1044
1315
  // baade udoedelig og ejet af root, saa vagten var enten inert eller draebte os selv
1045
1316
  // paa EPERM. En kaede der kun bestaar af pid 1 vogter ingenting og skal ikke bruges.
1046
1317
  let vagtKaede = parentPid > 1 ? [parentPid] : [];
@@ -1050,7 +1321,7 @@ try {
1050
1321
  } catch {}
1051
1322
  if (!vagtKaede.length) {
1052
1323
  process.stderr.write('[MCP] ingen brugbar foraelder-kaede (ppid=' + parentPid +
1053
- ') falder tilbage paa idle-graensen alene\n');
1324
+ ') - falder tilbage paa idle-graensen alene\n');
1054
1325
  }
1055
1326
  process.stderr.write(`[MCP] vagt-kaede: ${vagtKaede.join(' → ')}\n`);
1056
1327
 
@@ -1061,14 +1332,14 @@ parentCheck = setInterval(() => {
1061
1332
  // ── Et doedt led betyder ikke automatisk at chatten er vaek (MAALT 23/8) ──────
1062
1333
  //
1063
1334
  // Kaeden blev frosset ved opstart. Men et MELLEMLED kan afslutte helt normalt
1064
- // mens ejeren koerer videre maalt to gange paa denne maskine, hvor kaeden gaar
1335
+ // mens ejeren koerer videre - maalt to gange paa denne maskine, hvor kaeden gaar
1065
1336
  // npm exec → wrapper → claude → Code Helper (Plugin) → Code. Et forbigaaende led
1066
1337
  // der lukkede pænt udloeste "chatten bag denne server er vaek", mens chatten var
1067
1338
  // uroert. Og risikoen er ensrettet vaerre end 1.25.0, som vogtede ét pid: nu er
1068
1339
  // hvert af 5-6 led en ny doedsaarsag.
1069
1340
  //
1070
1341
  // Derfor genlaeses kaeden foerst naar noget SER doedt ud. Kan vi stadig gaa fra
1071
- // vores egen foraelder op til en rod, er vi ikke foraeldreloese vi er bare blevet
1342
+ // vores egen foraelder op til en rod, er vi ikke foraeldreloese - vi er bare blevet
1072
1343
  // reparented, og den nye kaede overtager. Kun naar den vej ogsaa er vaek, lukker vi.
1073
1344
  //
1074
1345
  // Prisen er nul i normal drift: genlaesningen koerer kun i det tik hvor et led er
@@ -1081,14 +1352,14 @@ parentCheck = setInterval(() => {
1081
1352
  if (frisk.length) {
1082
1353
  process.stderr.write(
1083
1354
  `[MCP] led ${doede.join(', ')} er vaek, men kaeden gaar stadig op: ` +
1084
- `${frisk.join(' → ')} fortsaetter\n`,
1355
+ `${frisk.join(' → ')} - fortsaetter\n`,
1085
1356
  );
1086
1357
  vagtKaede = frisk;
1087
1358
  return;
1088
1359
  }
1089
1360
 
1090
1361
  gracefulShutdown(
1091
- `Proces ${doede[0]} i kaeden doede, og der er ingen levende vej op ` +
1362
+ `Proces ${doede[0]} i kaeden doede, og der er ingen levende vej op - ` +
1092
1363
  'chatten bag denne server er vaek',
1093
1364
  );
1094
1365
  }, 5000); // hvert 5. sekund