@agent360/browser-mcp 1.29.0 → 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,26 +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
- // Spaendet kan flyttes med env ellers ville en test af port-udsultning skulle
42
+ // Spaendet kan flyttes med env - ellers ville en test af port-udsultning skulle
43
43
  // beslaglaegge de RIGTIGE porte og dermed sulte brugerens oevrige chats imens.
44
44
  // Uden env er vaerdierne uaendrede. (Samme moenster som BROWSER_MCP_EXTENSION_ID.)
45
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
46
+ const MAX_PORT = Number(process.env.BROWSER_MCP_MAX_PORT) || 9895; // 20 ports instead of 10 - zombies die within 5s via parent check
47
47
 
48
48
  // ── Extension connections ───────────────────────────────────────────────────
49
49
  // FEJL MAALT 21/8: her stod `let extensionSocket = null`, og hver ny forbindelse
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
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
52
52
  // og forbinder til hver eneste server. Maalt med lsof: 2 ESTABLISHED forbindelser
53
53
  // paa hver af de fire aktive porte. Kommandoerne gik til den der forbandt sidst,
54
- // 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
55
55
  // `terminate` fra den forkerte kopi lukkede serveren ned under den rigtige.
56
56
  //
57
57
  // Nu holdes alle forbindelser med deres identitet, kommandoer sendes kun til den
@@ -60,8 +60,9 @@ const MAX_PORT = Number(process.env.BROWSER_MCP_MAX_PORT) || 9895; // 20 ports i
60
60
  const connections = new Set(); // { ws, seq, extensionId, version, name, since }
61
61
  let connSeq = 0;
62
62
  let activePort = null;
63
- let alleePorteOptaget = false; // hele spaendet i brug se createWSS
63
+ let alleePorteOptaget = false; // hele spaendet i brug - se createWSS
64
64
  let bindFejl = null; // bind fejlede af en ANDEN grund end optaget port
65
+ let portBundetTid = 0; // hvornaar porten sidst blev aaben - se sendToExtension
65
66
 
66
67
  function cmpVersion(a, b) {
67
68
  const pa = String(a || '0.0.0').split('.').map(n => parseInt(n, 10) || 0);
@@ -77,13 +78,13 @@ let sidsteKonfliktNoegle = '';
77
78
  function advarOmKonflikt(conn) {
78
79
  const alle = distinctExtensions();
79
80
  if (alle.length < 2) return;
80
- // 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.
81
82
  const noegle = alle.map(c => `${c.extensionId || 'ukendt'}@${c.version || '?'}`).sort().join('|');
82
83
  if (noegle === sidsteKonfliktNoegle) return;
83
84
  sidsteKonfliktNoegle = noegle;
84
85
  const aktiv = activeConnection();
85
86
  // Oplyser INGEN af dem en version (alle udgivne udgaver er fra foer haandtrykket),
86
- // 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
87
88
  // "kommandoer sendes kun til X" som om X var det rigtige valg.
88
89
  const kanVaelge = alle.some(c => c.version);
89
90
  process.stderr.write(
@@ -105,14 +106,14 @@ function liveConnections() {
105
106
  //
106
107
  // MAALT 21/8 i flow-harnessen: uden laasen skiftede den aktive udvidelse MIDT i en
107
108
  // koersel. Foerste kommando (navigate) gik til udvidelse A, som aabnede fanen. Et
108
- // 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 -
109
110
  // men B kendte ikke A's fane og lavede en frisk about:blank. Alt derefter fejlede med
110
111
  // "Cannot access contents of url about:blank". 21 af 43 vaerktoejer faldt paa det, og
111
112
  // symptomet lignede praecis "faner forsvinder" og "kun én session virker".
112
113
  //
113
114
  // Faner hoerer til den udvidelse der aabnede dem. Skifter man udvidelse, strander de.
114
115
  // Derfor: vaelg én gang, og bliv ved den saa laenge dens forbindelse lever. Doer den,
115
- // 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.
116
117
  let laastForbindelse = null;
117
118
  let harSendtKommando = false;
118
119
  const PINNET_UDVIDELSE = (process.env.BROWSER_MCP_EXTENSION_ID || '').trim() || null;
@@ -121,7 +122,7 @@ function activeConnection() {
121
122
  if (laastForbindelse && laastForbindelse.ws.readyState === 1) return laastForbindelse;
122
123
 
123
124
  // Nyeste udvidelse vinder. Ved uafgjort: den der forbandt sidst. En udvidelse fra
124
- // 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
125
126
  // med den nyere udgave.
126
127
  let best = null;
127
128
  for (const c of liveConnections()) {
@@ -156,13 +157,13 @@ let tomgangsvagt = null;
156
157
  // ── 4-timers-tomgangen bor paa modul-niveau, ikke i hjerteslaget ────────────
157
158
  //
158
159
  // FUNDET AF REVIEW 7/9. Tjekket laa INDE i `heartbeat`, som kun findes mens serveren
159
- // har en port og som `frigivPort` rydder. Da porten blev doven, betoed det at en
160
+ // har en port - og som `frigivPort` rydder. Da porten blev doven, betoed det at en
160
161
  // proces der ALDRIG binder (en chat der ikke roerer browseren) heller aldrig faar
161
162
  // sin tomgang tjekket. Det var den eneste vej hvor en browser-inaktiv server gav sine
162
163
  // ~35 MB tilbage; med mange samtidige chats er det maalbart. Nu koerer vagten altid.
163
164
  // Graensen staar som literal, ikke bag en konstant: `vagter.test.mjs` laeser tallet
164
165
  // direkte ud af kilden for at haandhaeve at den aldrig bliver kort. En kort graense
165
- // ville lade en aaben chat miste browseren permanent vaerre end en port der staar
166
+ // ville lade en aaben chat miste browseren permanent - vaerre end en port der staar
166
167
  // optaget lidt for laenge.
167
168
  tomgangsvagt = setInterval(() => {
168
169
  if (Date.now() - lastActivity > 4 * 60 * 60 * 1000) gracefulShutdown('Idle timeout (4h)');
@@ -180,7 +181,7 @@ function createWSS(port = BASE_PORT) {
180
181
  // ingen socket overhovedet.
181
182
  //
182
183
  // Chrome saetter ALTID Origin: chrome-extension://<32 tegn> paa en WebSocket fra
183
- // 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
184
185
  // en udvidelse, saa gaten koster aegte brugere ingenting.
185
186
  verifyClient: ({ origin }, godkend) => {
186
187
  if (/^chrome-extension:\/\/[a-p]{32}$/.test(origin || '')) return godkend(true);
@@ -201,7 +202,7 @@ function createWSS(port = BASE_PORT) {
201
202
  } else {
202
203
  // MAALT 22/8: her stod KUN denne stderr-linje. Ingen laeser stderr fra en
203
204
  // MCP-server, saa udtoemte porte var en helt tavs fejl. Hvert vaerktoejskald
204
- // 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
205
206
  // siger at serveren koerer og sender brugeren til Chrome Web Store. Begge dele
206
207
  // er forkerte naar sandheden er at vi aldrig fik en port. Flaget laeses i
207
208
  // sendToExtension, saa agenten kan give brugeren den rigtige forklaring.
@@ -212,7 +213,7 @@ function createWSS(port = BASE_PORT) {
212
213
  } else {
213
214
  // FUNDET AF REVIEW 7/9. Her stod KUN stderr-linjen. Foer porten blev doven, var
214
215
  // det harmloest: en opstartsfejl man kunne se i loggen. Nu venter `sikrePort()`
215
- // paa et loefte der aldrig blev indfriet og `sendToExtension` goer
216
+ // paa et loefte der aldrig blev indfriet - og `sendToExtension` goer
216
217
  // `await sikrePort()` UDEN timeout. Enhver anden bind-fejl end EADDRINUSE
217
218
  // (EACCES paa en privilegeret port, EADDRNOTAVAIL, en restriktiv firewall)
218
219
  // ville derfor faa hvert eneste browser-kald til at haenge tavst for evigt.
@@ -227,7 +228,7 @@ function createWSS(port = BASE_PORT) {
227
228
  // WebSocket-handshaket fra en udvidelse baerer Origin: chrome-extension://<id>.
228
229
  // Den identificerer afsenderen UDEN at udvidelsen behoever at kende haandtrykket,
229
230
  // saa en konflikt mellem to indlaeste udvidelser kan opdages ogsaa naar begge er
230
- // gamle udgaver hvilket er praecis den situation konflikten opstaar i.
231
+ // gamle udgaver - hvilket er praecis den situation konflikten opstaar i.
231
232
  // Maalt 21/8: to distinkte origins ringede op til hver eneste server.
232
233
  const origin = req?.headers?.origin || '';
233
234
  const fraOrigin = /^chrome-extension:\/\/([a-p]{32})$/.exec(origin)?.[1] || null;
@@ -239,14 +240,14 @@ function createWSS(port = BASE_PORT) {
239
240
  // udvidelse med `hello version 99.0.0`, fik `browser_get_cookies` leveret, og
240
241
  // kunne lukke serveren med `terminate`. Alle tre trin lykkedes.
241
242
  //
242
- // 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
243
244
  // goer enhver anden app og ethvert npm-postinstall-script. Og hvad den kan er
244
245
  // ikke smaating: laese alt agenten sender til browseren (kodeord fra ask_user,
245
246
  // cookies, sidetekst), fodre agenten med opdigtet sideindhold, og slukke
246
247
  // browser-adgangen i alle aabne chats.
247
248
  //
248
249
  // Chrome saetter ALTID `Origin: chrome-extension://<id>` paa en WebSocket fra en
249
- // udvidelse verificeret mod den koerende udvidelse. En manglende header er
250
+ // udvidelse - verificeret mod den koerende udvidelse. En manglende header er
250
251
  // derfor ikke en aeldre udgave; det er noget andet end en udvidelse.
251
252
  if (!fraOrigin) {
252
253
  process.stderr.write(
@@ -261,7 +262,7 @@ function createWSS(port = BASE_PORT) {
261
262
  // slaa dem fra: BROWSER_MCP_EXTENSION_ID=<id> binder serveren til én bestemt.
262
263
  // Uden den er valget vilkaarligt naar ingen af dem oplyser en version.
263
264
  if (PINNET_UDVIDELSE && fraOrigin && fraOrigin !== PINNET_UDVIDELSE) {
264
- 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`);
265
266
  try { ws.close(1008, 'not the pinned extension'); } catch {}
266
267
  return;
267
268
  }
@@ -278,7 +279,7 @@ function createWSS(port = BASE_PORT) {
278
279
  // If extension was auto-updated, trigger reload
279
280
  if (process.env.BROWSER_MCP_EXTENSION_UPDATED === '1') {
280
281
  process.env.BROWSER_MCP_EXTENSION_UPDATED = '';
281
- process.stderr.write('[MCP] Extension files updated triggering auto-reload\n');
282
+ process.stderr.write('[MCP] Extension files updated - triggering auto-reload\n');
282
283
  setTimeout(() => {
283
284
  sendToExtension('reload_extension', {}, 5000).catch(() => {});
284
285
  }, 1000);
@@ -290,7 +291,7 @@ function createWSS(port = BASE_PORT) {
290
291
 
291
292
  // Identitets-haandtryk fra offscreen-dokumentet (v1.28+).
292
293
  if (msg.type === 'hello') {
293
- // MAALT 23/8: her stod `conn.extensionId = msg.extensionId` altsaa lod
294
+ // MAALT 23/8: her stod `conn.extensionId = msg.extensionId` - altsaa lod
294
295
  // haandtrykket afsenderen OVERSKRIVE sin egen identitet med hvad som helst.
295
296
  // Origin-headeren er den eneste kilde Chrome selv saetter og som afsenderen
296
297
  // ikke kan forfalske, saa den vinder. Beskedens id gemmes separat: stemmer de
@@ -299,25 +300,37 @@ function createWSS(port = BASE_PORT) {
299
300
  conn.harHilst = true;
300
301
  if (conn.helloId && conn.helloId !== conn.extensionId) {
301
302
  process.stderr.write(
302
- `[MCP] Haandtryk oplyser ${conn.helloId} men Origin siger ${conn.extensionId} ` +
303
+ `[MCP] Haandtryk oplyser ${conn.helloId} men Origin siger ${conn.extensionId} - ` +
303
304
  'bruger Origin\n',
304
305
  );
305
306
  }
306
307
  conn.version = typeof msg.version === 'string' ? msg.version : null;
307
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;
308
313
  advarOmKonflikt(conn);
309
314
  return;
310
315
  }
311
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
+
312
325
  if (msg.type === 'terminate') {
313
326
  // terminate lukker serveren for ALLE chats paa porten, saa den har to gates.
314
327
  //
315
- // Om id-sammenligningen: ORIGIN er autoriteten Chrome saetter den, og
328
+ // Om id-sammenligningen: ORIGIN er autoriteten - Chrome saetter den, og
316
329
  // afsenderen kan ikke forfalske den. Haandtrykkets id er kun et ekstra
317
330
  // signal. MAALT 23/8 mod den AEGTE udvidelse: den sender hello med id null,
318
331
  // fordi chrome.runtime.getManifest() kan fejle i offscreen-dokumentet.
319
332
  // Kraevede vi lighed ubetinget, kunne en HELT legitim udvidelse aldrig lukke
320
- // 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
321
334
  // MANGLER er fint (Origin har allerede bevist hvem det er), mens et id der er
322
335
  // TIL STEDE og peger et ANDET sted afvises.
323
336
  //
@@ -328,15 +341,15 @@ function createWSS(port = BASE_PORT) {
328
341
  // kopi, der lukkede sin sidste fane, rive serveren vaek under den
329
342
  // udvidelse der reelt loeste opgaven.
330
343
  if (!conn.harHilst || (conn.helloId && conn.helloId !== conn.extensionId)) {
331
- process.stderr.write('[MCP] terminate ignoreret intet gyldigt haandtryk\n');
344
+ process.stderr.write('[MCP] terminate ignoreret - intet gyldigt haandtryk\n');
332
345
  return;
333
346
  }
334
347
  if (activeConnection() !== conn) {
335
- 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');
336
349
  return;
337
350
  }
338
351
  // AENDRET 7/9: her stod `gracefulShutdown`. En chat der var faerdig med browseren
339
- // mistede altsaa browseren HELT og porten blev alligevel hverken frigivet hurtigt
352
+ // mistede altsaa browseren HELT - og porten blev alligevel hverken frigivet hurtigt
340
353
  // nok til andre (processen doede foerst efter oprydning) eller genvundet af chatten
341
354
  // selv. Nu slippes kun porten; naeste browser-kald tager en ny (se sikrePort).
342
355
  frigivPort('udvidelsen meldte: sidste fane lukket');
@@ -354,11 +367,11 @@ function createWSS(port = BASE_PORT) {
354
367
 
355
368
  // ── En doed socket skal AFVISE de kald der var undervejs (MAALT 23/8) ──────
356
369
  // Foer roerte close-handleren ikke `pending`. Maalt: en socket der doede 300 ms
357
- // 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
358
371
  // forklaring, "kommandoen tog for lang tid". For extract_list er timeouten 180
359
372
  // sekunder, altsaa tre minutters tavshed hvor sandheden var kendt med det samme.
360
373
  const afvisVentende = (grund) => {
361
- // 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
362
375
  // normalt skift mellem to udvidelser afbryde kald der er fuldt i orden.
363
376
  if (!pending.size || liveConnections().length) return;
364
377
  const antal = pending.size;
@@ -367,16 +380,16 @@ function createWSS(port = BASE_PORT) {
367
380
  pending.delete(id);
368
381
  p.reject(new Error(
369
382
  `Forbindelsen til Chrome-udvidelsen forsvandt mens kommandoen koerte (${grund}). ` +
370
- '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 ' +
371
384
  'proever igen. Er udvidelsen slaaet fra eller Chrome lukket, saa start den og proev forfra.',
372
385
  ));
373
386
  }
374
- process.stderr.write(`[MCP] ${antal} ventende kald afvist ${grund}\n`);
387
+ process.stderr.write(`[MCP] ${antal} ventende kald afvist - ${grund}\n`);
375
388
  };
376
389
 
377
390
  // MAALT 23/8: der fandtes INGEN error-handler. Et ugyldigt WebSocket-frame (fx
378
391
  // RSV1 sat) faar 'ws' til at emitte 'error' paa socketen, og en uhaandteret
379
- // 'error' paa en EventEmitter kaster og draeber hele processen altsaa alle
392
+ // 'error' paa en EventEmitter kaster og draeber hele processen - altsaa alle
380
393
  // chats paa den port. Tre linjer lukker det.
381
394
  ws.on('error', (e) => {
382
395
  process.stderr.write(`[MCP] WebSocket-fejl paa forbindelsen: ${e?.message || e}\n`);
@@ -392,11 +405,12 @@ function createWSS(port = BASE_PORT) {
392
405
 
393
406
  server.on('listening', () => {
394
407
  activePort = port;
408
+ portBundetTid = Date.now();
395
409
  process.stderr.write(`[MCP] WebSocket server listening on ws://127.0.0.1:${port}\n`);
396
410
  loesPortLoefte(true);
397
411
  });
398
412
 
399
- // 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
400
414
  // Ryddes foerst: porten kan bindes flere gange i samme proces (se sikrePort), og uden
401
415
  // det her ville hver ny binding efterlade en ekstra timer der aldrig blev stoppet.
402
416
  if (heartbeat) clearInterval(heartbeat);
@@ -408,13 +422,13 @@ function createWSS(port = BASE_PORT) {
408
422
  // ── Porten tages ved BRUG, ikke ved opstart ─────────────────────────────────
409
423
  //
410
424
  // MAALT 7/9-2026 paa Gustavs maskine: 37 koerende servere, 20 porte i spaendet, 17 chats
411
- // helt uden browser. Hver Claude Code-chat starter en server ved opstart ogsaa de mange
412
- // chats der aldrig roerer browseren og her stod `createWSS()` paa modul-niveau. Porten
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
413
427
  // blev altsaa reserveret af en chat der maaske aldrig fik brug for den, og holdt indtil
414
428
  // chatten doede eller 4-timers-tomgangen udloeb.
415
429
  //
416
430
  // Konsekvensen var ensidigt slem: chat nr. 21 fik `alleePorteOptaget = true` ÉN gang og
417
- // proevede aldrig igen dens browser var doed hele chattens levetid, selv naar en port
431
+ // proevede aldrig igen - dens browser var doed hele chattens levetid, selv naar en port
418
432
  // blev fri et minut senere.
419
433
  //
420
434
  // Nu bindes porten foerste gang et vaerktoej faktisk skal bruge udvidelsen, og HVERT kald
@@ -423,7 +437,7 @@ function createWSS(port = BASE_PORT) {
423
437
  //
424
438
  // Bemaerk hvorfor det ikke er en 5-minutters timer der draeber processen: en chat der
425
439
  // foerst skal bruge browseren efter en halv time ville saa staa uden. Processen lever
426
- // videre det er kun PORTEN der ikke holdes reserveret til noget der ikke sker.
440
+ // videre - det er kun PORTEN der ikke holdes reserveret til noget der ikke sker.
427
441
  let portResolver = null;
428
442
  let bindLoefte = null;
429
443
 
@@ -437,11 +451,11 @@ function loesPortLoefte(fik) {
437
451
 
438
452
  // Slip porten, men BLIV I LIVE. Forskellen er hele pointen: lukkede vi processen ned,
439
453
  // ville en chat der er faerdig med browseren kl. 10 og skal bruge den igen kl. 10:40 staa
440
- // uden og Claude Code genstarter ikke en MCP-server midt i en samtale. Processen koster
454
+ // uden - og Claude Code genstarter ikke en MCP-server midt i en samtale. Processen koster
441
455
  // ~35 MB og ingen port; det er porten der er den knappe ressource.
442
456
  function frigivPort(grund) {
443
457
  if (activePort === null) return;
444
- process.stderr.write(`[MCP] frigiver port ${activePort} ${grund}\n`);
458
+ process.stderr.write(`[MCP] frigiver port ${activePort} - ${grund}\n`);
445
459
  if (heartbeat) { clearInterval(heartbeat); heartbeat = null; }
446
460
  const gammel = wss;
447
461
  wss = null;
@@ -458,6 +472,7 @@ function sikrePort() {
458
472
  if (bindLoefte) return bindLoefte; // en binding er allerede i gang
459
473
  alleePorteOptaget = false; // hvert forsoeg starter paa en frisk
460
474
  bindFejl = null;
475
+ portBundetTid = 0; // nulstilles her; saettes naar 'listening' faktisk kommer
461
476
  bindLoefte = new Promise((res) => { portResolver = res; });
462
477
  // Vagthund. F8 var ét konkret hul hvor loeftet aldrig blev indfriet; det her lukker
463
478
  // KLASSEN. Emitter en fremtidig fejlsti hverken 'listening' eller 'error', svarer
@@ -466,7 +481,7 @@ function sikrePort() {
466
481
  const vagthund = setTimeout(() => {
467
482
  if (!portResolver) return;
468
483
  bindFejl = bindFejl || 'bindingen svarede ikke inden for 10 sekunder';
469
- process.stderr.write('[MCP] port-bindingen svarede aldrig opgiver dette forsoeg\n');
484
+ process.stderr.write('[MCP] port-bindingen svarede aldrig - opgiver dette forsoeg\n');
470
485
  loesPortLoefte(false);
471
486
  }, 10000);
472
487
  bindLoefte.finally(() => clearTimeout(vagthund));
@@ -476,24 +491,54 @@ function sikrePort() {
476
491
 
477
492
  // ── Send command to extension ───────────────────────────────────────────────
478
493
 
479
- async function sendToExtension(method, params = {}, timeoutMs = 30000, _retries = 5) {
494
+ async function sendToExtension(method, params = {}, timeoutMs = 30000, _retries = 5, _ekstraRunde = false, _doerAabnetNu = null) {
480
495
  // Skaf en port hvis vi ikke har en. Foerste kald binder; senere kald er en no-op.
481
- // Fik vi ingen (hele spaendet optaget), proever naeste kald igen derfor ingen kast her.
496
+ // Fik vi ingen (hele spaendet optaget), proever naeste kald igen - derfor ingen kast her.
482
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);
483
504
  // Retry if extension is temporarily disconnected (reconnects every 2s)
484
505
  const conn = activeConnection();
485
506
  if (!conn) {
486
507
  if (_retries > 0) {
487
508
  await new Promise(r => setTimeout(r, 1500));
488
- 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
+ );
489
534
  }
490
535
  // This is the other half of the two-part setup: the server is clearly running (it is
491
536
  // throwing this), so what is missing is the extension, Chrome itself, or the connection
492
- // 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.
493
538
  if (bindFejl) {
494
539
  throw new Error(
495
540
  `Serveren kunne ikke aabne en port paa 127.0.0.1 (${BASE_PORT}-${MAX_PORT}): ${bindFejl}\n` +
496
- 'Det er IKKE Chrome eller udvidelsen det er operativsystemet eller en firewall der ' +
541
+ 'Det er IKKE Chrome eller udvidelsen - det er operativsystemet eller en firewall der ' +
497
542
  'afviser bindingen. Tjek om noget blokerer loopback-porte.\n' +
498
543
  'Sig praecis dét til brugeren. Sig IKKE at udvidelsen mangler.',
499
544
  );
@@ -501,9 +546,9 @@ async function sendToExtension(method, params = {}, timeoutMs = 30000, _retries
501
546
  if (alleePorteOptaget) {
502
547
  throw new Error(
503
548
  `Alle porte ${BASE_PORT}-${MAX_PORT} er optaget lige nu, saa dette kald fik ingen port. ` +
504
- 'Det er IKKE et problem med Chrome eller udvidelsen de virker fint.\n' +
549
+ 'Det er IKKE et problem med Chrome eller udvidelsen - de virker fint.\n' +
505
550
  `${MAX_PORT - BASE_PORT + 1} andre chats bruger browseren i oejeblikket. ` +
506
- 'Luk en af dem, eller vent til en bliver faerdig og proev saa kommandoen igen. ' +
551
+ 'Luk en af dem, eller vent til en bliver faerdig - og proev saa kommandoen igen. ' +
507
552
  'Denne chat skal IKKE genstartes: hvert kald proever selv at faa en port.\n' +
508
553
  'Sig praecis dét til brugeren. Sig IKKE at udvidelsen mangler.',
509
554
  );
@@ -514,7 +559,7 @@ async function sendToExtension(method, params = {}, timeoutMs = 30000, _retries
514
559
  'Chrome extension (apparently not reachable).\n' +
515
560
  'Check, in order:\n' +
516
561
  ' 1. Chrome is actually open and running.\n' +
517
- ' 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' +
518
563
  ' https://chromewebstore.google.com/detail/agent360-browser-mcp/jdehgalffmffhfhmmhaokfbfnafnmgcl\n' +
519
564
  ' 3. Click the extension icon -> Reconnect, and wait 2-3 seconds.\n' +
520
565
  'Still stuck: https://browsermcp.dev/docs/troubleshooting/'
@@ -540,14 +585,14 @@ const INSTRUCTIONS = `You control the user's real Chrome browser via this MCP se
540
585
 
541
586
  ## Key behaviors
542
587
  - **Always use browser_ask_user** when you need credentials, 2FA codes, CAPTCHA help, or any user input. Never guess passwords or tokens.
543
- - **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.
544
- - **Check existing tabs first** with browser_list_tabs before navigating reuse tabs instead of opening duplicates.
545
- - **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.
546
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.
547
592
 
548
593
  ## Tab management
549
594
  - navigate creates tabs in your session's tab group (visible in Chrome as colored groups)
550
- - 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
551
596
  - switch_tab lets you jump between your tabs
552
597
  - close_tab cleans up when you're done
553
598
 
@@ -560,27 +605,31 @@ const INSTRUCTIONS = `You control the user's real Chrome browser via this MCP se
560
605
  6. After success, extract what you need with browser_get_page_content
561
606
 
562
607
  ## Screenshots
563
- - browser_screenshot captures the visible tab useful for visual verification
564
- - 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
565
614
 
566
615
  ## Text-based selectors (preferred for dynamic sites)
567
- - browser_click("text=Get started") clicks any element containing "Get started"
568
- - browser_click("button:text(Submit)") clicks a button containing "Submit"
569
- - browser_fill("text=Email", "user@example.com") fills input near "Email" label
570
- - 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
571
620
  - These work on ALL sites including Google Cloud, Stripe, Slack (CSP-strict)
572
621
 
573
622
  ## Keyboard
574
- - browser_press_key("Enter") submit forms
575
- - browser_press_key("Tab") navigate between fields
576
- - browser_press_key("Escape") close dialogs
577
- - browser_press_key("ArrowDown") navigate dropdowns
578
- - 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
579
628
 
580
629
  ## CAPTCHA handling
581
630
  Use browser_solve_captcha to detect and solve CAPTCHAs automatically:
582
- 1. Call browser_solve_captcha() detects CAPTCHA type on page
583
- 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
584
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
585
634
  4. If all else fails → call browser_solve_captcha(action="ask_human") to show overlay to user
586
635
  5. After solving, retry the action that was blocked
@@ -596,7 +645,7 @@ For image grid challenges: cells are 0-indexed, left-to-right, top-to-bottom. A
596
645
  - If a standard selector fails, the extension recursively searches shadow roots
597
646
  - Text-based selectors ("text=Submit") also traverse shadow DOM
598
647
 
599
- ## Hard inputs use the specialised tools first
648
+ ## Hard inputs - use the specialised tools first
600
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.
601
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.
602
651
  - **Drag-drop file zones without visible file input** → use browser_drop_file (NOT browser_upload_file). Finds hidden input in subtree/parent.
@@ -606,28 +655,31 @@ For image grid challenges: cells are 0-indexed, left-to-right, top-to-bottom. A
606
655
  - Element not found → try text-based selector instead of CSS
607
656
  - Screenshot fails → debugger fallback is automatic
608
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
609
661
  - CAPTCHA blocks page → use browser_ask_user, let human solve it
610
662
  - browser_fill seemingly succeeds but value reverts → switch to browser_set_date or browser_set_combobox (most reverts are React-controlled validators)
611
663
 
612
664
  ## Extension updates
613
- The MCP server auto-pulls the latest code from git on every new session startup.
614
- If the extension files were updated, ask the user to reload it:
615
- "The Browser MCP extension was updated. Please go to chrome://extensions, find 'Agent360 Browser MCP', and click the reload icon (🔄) to apply the update."
616
- You cannot navigate to chrome:// pages the user must do this manually.
617
-
618
- ## When Browser MCP itself is the obstacle call browser_provide_feedback
619
- 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:
620
- - **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.
621
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.
622
674
  - **current** → the install is fine, so this is a genuine gap. Offer the returned submit_url as a clickable link.
623
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**.
624
676
  Call it once per distinct obstacle, not once per retry.
625
677
 
626
678
  ## Sharing wishes / use-cases / bugs
627
- 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.`;
628
680
 
629
681
  // FEJL MAALT 21/8: `instructions` laa i et TREDJE argument. Server-konstruktoeren
630
- // 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 -
631
683
  // og hele blokken ovenfor naaede aldrig frem til nogen klient. Verificeret ved at
632
684
  // laese initialize-svaret: det havde kun protocolVersion, capabilities og serverInfo.
633
685
  // Det betyder at "luk altid faner naar du er faerdig", CAPTCHA-fremgangsmaaden,
@@ -704,6 +756,62 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
704
756
  return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
705
757
  }
706
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
+
707
815
  // extract_list scrolls a container in a loop (up to 300 rounds × wait_ms), so the 30 s
708
816
  // default would kill a long mail list mid-walk and report a partial set as complete.
709
817
  const timeout = method === 'ask_user' ? (args?.timeout || 120000) + 5000 :
@@ -724,12 +832,31 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
724
832
  // overskrive en fil i brugerens hjemmemappe med PNG-bytes.
725
833
  const rod = resolve(process.cwd());
726
834
  const targetPath = resolve(rod, args.path);
727
- if (targetPath !== rod && !targetPath.startsWith(rod + sep)) {
835
+ if (targetPath !== rod && !targetPath.startsWith(rod.endsWith(sep) ? rod : rod + sep)) { // "/" giver ikke "//" (R5 F10)
728
836
  throw new Error(
729
837
  `path skal ligge inden for arbejdsmappen (${rod}). ` +
730
838
  `"${args.path}" peger udenfor. Brug en relativ sti uden ../.`,
731
839
  );
732
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
+ }
733
860
  mkdirSync(dirname(targetPath), { recursive: true });
734
861
  writeFileSync(targetPath, Buffer.from(base64, 'base64'));
735
862
  return {
@@ -765,7 +892,7 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
765
892
  }
766
893
  });
767
894
 
768
- // 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
769
896
  // alt brugeren ser. Det sker GARANTERET: serveren kommer fra npm og opdateres straks,
770
897
  // mens udvidelsen skal gennem Chrome Web Stores review paa 1-3 dage. I det vindue
771
898
  // findes otte vaerktoejer i serveren som en 1.25.0-udvidelse ikke kender
@@ -773,7 +900,7 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
773
900
  // udklipsholder-vaerktoejer, alle fra v1.26.0).
774
901
  //
775
902
  // Serveren VED at udvidelsen er gammel: den sendte intet haandtryk. Saa i stedet for
776
- // 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.
777
904
  const ERSTATNINGER = {
778
905
  double_click: 'kald `browser_click` to gange',
779
906
  right_click: 'brug `browser_execute_script` med et contextmenu-event',
@@ -792,7 +919,7 @@ function forklarSkaevhed(besked) {
792
919
  const alt = ERSTATNINGER[m[1]];
793
920
  return `Error: browser_${m[1]} findes i denne server, men ikke i din Chrome-udvidelse.\n\n` +
794
921
  'Udvidelsen opdateres via Chrome Web Store og kan vaere 1-3 dage bagud efter en ' +
795
- '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' +
796
923
  (alt ? `\nIndtil da: ${alt}.\n` : '') +
797
924
  '\nTjek om en opdatering venter: chrome://extensions → Agent360 Browser MCP. ' +
798
925
  'Er den indlaest som "unpacked", saa koer `npx @agent360/browser-mcp install`.';
@@ -814,11 +941,11 @@ function handleAbout(args) {
814
941
 
815
942
  const instruction =
816
943
  intent === 'wish'
817
- ? `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}`
818
945
  : intent === 'use_case'
819
- ? `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}`
820
947
  : intent === 'bug'
821
- ? `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}`
822
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`;
823
950
 
824
951
  return {
@@ -840,13 +967,13 @@ function handleAbout(args) {
840
967
  // ── Selv-diagnose: er installationen overhovedet frisk? ─────────────────────
841
968
  // Baggrund (21/8): den udgave der koerte lokalt var npm 1.25.0, mens rettelserne
842
969
  // laa uudgivet i repoet. Fejlen viste sig som "sessioner opfoerer sig underligt",
843
- // 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
844
971
  // paa. Derfor spoerger vaerktoejet selv, foer det konkluderer noget som helst.
845
972
 
846
973
  // Friskheds-tjek mod npm. SLUKKET SOM STANDARD siden 22/8.
847
974
  //
848
- // Produktet lover paa forsiden "nothing leaves your machine ... no telemetry", og et
849
- // 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
850
977
  // pakkenavn og ingen brugerdata. Loeftet vejer tungere end bekvemmeligheden, saa
851
978
  // tjekket er nu opt-in: saet BROWSER_MCP_CHECK_NPM=1.
852
979
  //
@@ -880,7 +1007,7 @@ function npmLatestVersion() {
880
1007
  // rettes. Logbogen ligger LOKALT og forlader ikke maskinen.
881
1008
  //
882
1009
  // Bevidst ikke auto-indsendelse til et offentligt GitHub-issue: rapporten baerer URL og
883
- // 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
884
1011
  // eller et kundesystem. Et offentligt issue kan ikke tages tilbage.
885
1012
  const FEEDBACK_LOG = join(homedir(), '.browser-mcp', 'feedback.jsonl');
886
1013
  const setteFingeraftryk = new Set(); // samme graense logges én gang pr. serverliv
@@ -896,7 +1023,7 @@ function fingeraftryk(kind, tool, what) {
896
1023
  }
897
1024
 
898
1025
  // URL'en reduceres til oprindelse + sti. Query og fragment baerer tokens, sessions-id'er
899
- // 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.
900
1027
  function afkortUrl(u) {
901
1028
  if (!u) return null;
902
1029
  try { const x = new URL(u); return x.origin + x.pathname; } catch { return '(ulaeselig url)'; }
@@ -952,12 +1079,12 @@ async function handleProvideFeedback(args) {
952
1079
  );
953
1080
  fix_steps.push(
954
1081
  'Aabn chrome://extensions og slaa alle Browser MCP-udvidelser fra paa naer én. ' +
955
- '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.',
956
1083
  );
957
1084
  }
958
1085
  if (!active && activePort === null) {
959
1086
  // Ingen port taget endnu = browseren er ikke brugt i denne chat. Der er intet
960
- // i stykker, og et fix-skridt her ville vaere en falsk alarm se verdict 'idle'.
1087
+ // i stykker, og et fix-skridt her ville vaere en falsk alarm - se verdict 'idle'.
961
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.');
962
1089
  } else if (!active) {
963
1090
  findings.push('Ingen Chrome-udvidelse er forbundet til denne MCP-server lige nu.');
@@ -965,7 +1092,7 @@ async function handleProvideFeedback(args) {
965
1092
  }
966
1093
  if (serverOutdated) {
967
1094
  findings.push(`MCP-serveren koerer v${PKG_VERSION}, men npm har v${npmLatest}. Fejlen kan allerede vaere rettet.`);
968
- 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}).`);
969
1096
  }
970
1097
  if (extOutdated) {
971
1098
  findings.push(
@@ -974,35 +1101,39 @@ async function handleProvideFeedback(args) {
974
1101
  : `Udvidelsen er v${extVersion}, serveren er v${PKG_VERSION}. Udvidelsen mangler rettelser fra de mellemliggende udgaver.`,
975
1102
  );
976
1103
  // MAALT 22/8: "↻ reload" er ubrugeligt for en Chrome Web Store-bruger. Butikken
977
- // 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
978
1105
  // version at hente endnu, saa raadet foerer i ring. Serveren kan ikke se hvilken
979
1106
  // slags installation det er (den gamle udvidelse oplyser intet), saa begge tilfaelde
980
- // 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.
981
1108
  fix_steps.push(
982
1109
  extVersion === null
983
1110
  ? 'Kommer udvidelsen fra Chrome Web Store: der er sandsynligvis en nyere version i review ' +
984
- '(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 - ' +
985
1112
  'det er forventet og gaar over af sig selv. Alt andet virker imens. ' +
986
1113
  'Er den indlaest som "unpacked": koer `npx @agent360/browser-mcp install` og derefter ' +
987
1114
  'chrome://extensions → Agent360 Browser MCP → ↻ reload.'
988
- : 'Opdatér udvidelsen: chrome://extensions Agent360 Browser MCP reload. ' +
989
- '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.',
990
1121
  );
991
1122
  }
992
1123
 
993
1124
  // ── "ingen forbindelse" og "ingen port endnu" er IKKE det samme ────────────
994
1125
  //
995
1126
  // FUNDET AF REVIEW 7/9. Foer porten blev doven, bandt hver server ved opstart, saa
996
- // udvidelsen var altid forbundet naar dette vaerktoej blev kaldt og `!active`
1127
+ // udvidelsen var altid forbundet naar dette vaerktoej blev kaldt - og `!active`
997
1128
  // betoed derfor paalideligt "udvidelsen kan ikke naas". Nu binder en chat foerst en
998
1129
  // port naar den bruger browseren, saa en HELT SUND chat der ikke har roert den kan
999
1130
  // staa uden forbindelse. Uden det her skel fik den `disconnected` + fix_steps der
1000
- // bad brugeren geninstallere og INSTRUCTIONS beder agenten viderebringe dem.
1131
+ // bad brugeren geninstallere - og INSTRUCTIONS beder agenten viderebringe dem.
1001
1132
  // Vi ville altsaa fortaelle folk at deres installation var i stykker, fordi vi selv
1002
1133
  // endnu ikke havde aabnet doeren.
1003
1134
  //
1004
1135
  // Loesningen er ikke at binde en port her (et diagnose-vaerktoej skal ikke aendre
1005
- // tilstand for at kunne maale den) det er at sige praecis hvad der er tilfaeldet.
1136
+ // tilstand for at kunne maale den) - det er at sige praecis hvad der er tilfaeldet.
1006
1137
  const ingenPortEndnu = activePort === null;
1007
1138
  const verdict =
1008
1139
  exts.length > 1 ? 'conflict'
@@ -1019,6 +1150,8 @@ async function handleProvideFeedback(args) {
1019
1150
  name: c.name,
1020
1151
  version: c.version,
1021
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,
1022
1155
  active: c === active,
1023
1156
  })),
1024
1157
  extension_up_to_date: extOutdated === null ? null : !extOutdated,
@@ -1031,7 +1164,7 @@ async function handleProvideFeedback(args) {
1031
1164
  what && `**What happened**\n${what}`,
1032
1165
  tool && `\n**Tool**: \`${tool}\``,
1033
1166
  // MAALT 22/8 ved sikkerhedsreview: her stod den RAA url, mens den lokale logbog
1034
- // nedenfor bruger afkortUrl(). Query-strengen hvor tokens bor blev altsaa
1167
+ // nedenfor bruger afkortUrl(). Query-strengen - hvor tokens bor - blev altsaa
1035
1168
  // strippet fra filen paa disken, men sendt uredigeret ind i et link til et
1036
1169
  // OFFENTLIGT GitHub-issue. Praecis den forkerte vej rundt.
1037
1170
  url && `\n**URL**: ${afkortUrl(url)}`,
@@ -1048,14 +1181,14 @@ async function handleProvideFeedback(args) {
1048
1181
 
1049
1182
  const instruction =
1050
1183
  verdict === 'idle'
1051
- ? 'Browseren er ikke taget i brug i denne chat endnu, saa der er intet at diagnosticere paa forbindelsen ' +
1184
+ ? 'Browseren er ikke taget i brug i denne chat endnu, saa der er intet at diagnosticere paa forbindelsen - ' +
1052
1185
  'det er IKKE en fejl i installationen, og du maa ikke sige det til brugeren. ' +
1053
1186
  'Er der en aegte mangel, saa tilbyd submit_url som et klikbart link.'
1054
1187
  : verdict === 'conflict' || verdict === 'outdated' || verdict === 'disconnected'
1055
1188
  ? 'Fortael brugeren hvad der blev fundet, og giv fix_steps som konkrete skridt. Proev derefter handlingen igen. ' +
1056
- '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.'
1057
1190
  : 'Installationen er frisk, saa det her er sandsynligvis en aegte mangel eller fejl. Fortael brugeren kort hvad der ikke kunne lade sig goere, ' +
1058
- '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.';
1059
1192
 
1060
1193
  const logbog = skrivTilLogbog({
1061
1194
  at: new Date().toISOString(),
@@ -1107,14 +1240,14 @@ async function handleExtractToken(args) {
1107
1240
 
1108
1241
  // ── Graceful shutdown ──────────────────────────────────────────────────────
1109
1242
  // All shutdown paths funnel through gracefulShutdown so the cleanup chain runs
1110
- // deterministically even on abrupt parent-exit. Without this, process.exit(0)
1243
+ // deterministically - even on abrupt parent-exit. Without this, process.exit(0)
1111
1244
  // was racing against WS close-handshake, leaving zombie tabs in Chrome.
1112
1245
 
1113
1246
  let shuttingDown = false;
1114
1247
  function gracefulShutdown(reason, code = 0) {
1115
1248
  if (shuttingDown) return;
1116
1249
  shuttingDown = true;
1117
- process.stderr.write(`[MCP] ${reason} shutting down\n`);
1250
+ process.stderr.write(`[MCP] ${reason} - shutting down\n`);
1118
1251
 
1119
1252
  // Stop timers so they can't re-enter gracefulShutdown
1120
1253
  if (parentCheck) clearInterval(parentCheck);
@@ -1147,7 +1280,7 @@ process.on('exit', () => {
1147
1280
  // Claude Code → wrapper → npm exec → denne server
1148
1281
  //
1149
1282
  // `process.ppid` er `npm exec`, ikke Claude Code. Doer chatten, kan `npm exec`
1150
- // 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
1151
1284
  // evigt. Porten blev holdt i op til fire timer (idle-graensen), og med flere
1152
1285
  // forladte chats loeb spaendet fuldt.
1153
1286
  //
@@ -1157,7 +1290,7 @@ process.on('exit', () => {
1157
1290
  //
1158
1291
  // Kaeden hentes én gang ved opstart (én ps-kommando), og derefter koster tjekket
1159
1292
  // kun et signal 0 pr. led hvert 5. sekund. Kan kaeden ikke laeses (Windows, eller
1160
- // 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
1161
1294
  // vaerre end foer.
1162
1295
 
1163
1296
  // ps-opslaget bor her (det er en sideeffekt); selve kaede-logikken og doeds-dommen
@@ -1178,7 +1311,7 @@ function laesPpid(pid) {
1178
1311
  const parentPid = process.ppid;
1179
1312
 
1180
1313
  // MAALT 22/8: doer den naermeste foraelder FOER serveren er bootet (~2 sek node+SDK),
1181
- // 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
1182
1315
  // baade udoedelig og ejet af root, saa vagten var enten inert eller draebte os selv
1183
1316
  // paa EPERM. En kaede der kun bestaar af pid 1 vogter ingenting og skal ikke bruges.
1184
1317
  let vagtKaede = parentPid > 1 ? [parentPid] : [];
@@ -1188,7 +1321,7 @@ try {
1188
1321
  } catch {}
1189
1322
  if (!vagtKaede.length) {
1190
1323
  process.stderr.write('[MCP] ingen brugbar foraelder-kaede (ppid=' + parentPid +
1191
- ') falder tilbage paa idle-graensen alene\n');
1324
+ ') - falder tilbage paa idle-graensen alene\n');
1192
1325
  }
1193
1326
  process.stderr.write(`[MCP] vagt-kaede: ${vagtKaede.join(' → ')}\n`);
1194
1327
 
@@ -1199,14 +1332,14 @@ parentCheck = setInterval(() => {
1199
1332
  // ── Et doedt led betyder ikke automatisk at chatten er vaek (MAALT 23/8) ──────
1200
1333
  //
1201
1334
  // Kaeden blev frosset ved opstart. Men et MELLEMLED kan afslutte helt normalt
1202
- // 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
1203
1336
  // npm exec → wrapper → claude → Code Helper (Plugin) → Code. Et forbigaaende led
1204
1337
  // der lukkede pænt udloeste "chatten bag denne server er vaek", mens chatten var
1205
1338
  // uroert. Og risikoen er ensrettet vaerre end 1.25.0, som vogtede ét pid: nu er
1206
1339
  // hvert af 5-6 led en ny doedsaarsag.
1207
1340
  //
1208
1341
  // Derfor genlaeses kaeden foerst naar noget SER doedt ud. Kan vi stadig gaa fra
1209
- // 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
1210
1343
  // reparented, og den nye kaede overtager. Kun naar den vej ogsaa er vaek, lukker vi.
1211
1344
  //
1212
1345
  // Prisen er nul i normal drift: genlaesningen koerer kun i det tik hvor et led er
@@ -1219,14 +1352,14 @@ parentCheck = setInterval(() => {
1219
1352
  if (frisk.length) {
1220
1353
  process.stderr.write(
1221
1354
  `[MCP] led ${doede.join(', ')} er vaek, men kaeden gaar stadig op: ` +
1222
- `${frisk.join(' → ')} fortsaetter\n`,
1355
+ `${frisk.join(' → ')} - fortsaetter\n`,
1223
1356
  );
1224
1357
  vagtKaede = frisk;
1225
1358
  return;
1226
1359
  }
1227
1360
 
1228
1361
  gracefulShutdown(
1229
- `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 - ` +
1230
1363
  'chatten bag denne server er vaek',
1231
1364
  );
1232
1365
  }, 5000); // hvert 5. sekund