@jossuealcala/madre 1.0.3 → 1.0.4

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/CHANGELOG.en.md CHANGED
@@ -4,7 +4,14 @@ Every published version of `@jossuealcala/madre`. Dates in ISO format.
4
4
 
5
5
  A version closes when it is on npm: until then its section is called **Unreleased** and it can keep growing. Each published version has exactly one `vX.Y.Z` tag, one GitHub release and one section here; the patch number can reach two digits (`0.2.10`) before the minor goes up. See `docs/ROADMAP.md` for the criteria on what bumps each number. This is the English translation of [CHANGELOG.md](CHANGELOG.md); the Spanish file is the original.
6
6
 
7
- ## 1.0.4 · Unreleased
7
+ ## 1.0.5 · Unreleased
8
+
9
+ ## 1.0.4 · 2026-10-19
10
+
11
+ - **Cursor in the welcome.** The «Connect your agents» animation showed four agents around MADRE; now Cursor sits above her, and lights up when connected like the rest.
12
+ - **Your project's servers open even when they listen on the whole network.** Node, Express and Vite with `--host` listen on every interface of your computer, and the preview named them («open to your whole network · start it on 127.0.0.1») without letting you open them. Now they open the same way, inside your computer, and the list says «your network can reach it too». Servers from other folders are still not shown.
13
+ - **The project's state dot always shows.** Beside the project's name on the bar, the dot that says whether it is live got cut when the bar was tight. Now the name gives way, with «…», and the dot always stays whole.
14
+ - **Home's bar fits again.** With the Market and Modules keys, Home's bar ran under the buttons on the right and the pinned projects shrank to «+5», with no room to move them or drop a new one. Now, when it does not fit, the computer's name steps out first, then the keys keep only their icon (their name shows on hover), and last the agents step out. The projects always keep their room, also while you arrange the bar.
8
15
 
9
16
  ## 1.0.3 · 2026-10-17
10
17
 
package/CHANGELOG.md CHANGED
@@ -4,7 +4,14 @@ Todas las versiones publicadas de `@jossuealcala/madre`. Fechas en ISO.
4
4
 
5
5
  Una versión se cierra cuando está en npm: hasta entonces su sección se llama **Sin publicar** y puede crecer. Cada versión publicada tiene exactamente una etiqueta `vX.Y.Z`, una release en GitHub y una sección aquí; el parche puede llegar a dos dígitos (`0.2.10`) antes de subir el menor. Ver `docs/ROADMAP.md` para el criterio de qué sube cada número.
6
6
 
7
- ## 1.0.4 · Sin publicar
7
+ ## 1.0.5 · Sin publicar
8
+
9
+ ## 1.0.4 · 2026-10-19
10
+
11
+ - **Cursor en la bienvenida.** La animación de «Conecta tus agentes» mostraba cuatro agentes alrededor de MADRE; ahora Cursor está arriba, y se enciende al conectarse como los demás.
12
+ - **Los servidores de tu proyecto se abren aunque escuchen en toda la red.** Node, Express y Vite con `--host` escuchan en todas las interfaces de tu computadora, y la vista previa los nombraba («está abierto a toda la red · inícialo en 127.0.0.1») sin dejar abrirlos. Ahora se abren igual, dentro de tu computadora, y la lista avisa «tu red también lo alcanza». Los servidores de otras carpetas siguen sin mostrarse.
13
+ - **El punto de estado del proyecto siempre se ve.** Junto al nombre del proyecto, en la barra, el punto que dice si está en vivo se cortaba cuando la barra iba justa. Ahora el que cede es el nombre, con «…», y el punto queda siempre entero.
14
+ - **La barra de Inicio vuelve a caber.** Con las teclas Mercado y Módulos, la barra de Inicio se metía debajo de los botones de la derecha y los proyectos fijados quedaban reducidos a «+5», sin espacio para moverlos ni soltar uno nuevo. Ahora, cuando no cabe, primero se oculta el nombre de la computadora, luego las teclas se quedan con su ícono (su nombre aparece al pasar el ratón) y al final se ocultan los agentes. Los proyectos conservan siempre su lugar, también mientras acomodas la barra.
8
15
 
9
16
  ## 1.0.3 · 2026-10-17
10
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jossuealcala/madre",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "MADRE · one local room where the AI coding agents already on your machine (Codex, Claude Code, Gemini CLI, OpenCode) work on your project together, share one memory you can see and explore, and answer to you. Read-only by default, with per-message permission modes and checkpoints you can undo. Nothing leaves your machine on its own.",
5
5
  "keywords": [
6
6
  "ai-agents",
package/public/app.js CHANGED
@@ -2513,7 +2513,8 @@ function paintPorts(read) {
2513
2513
  row.type = 'button';
2514
2514
  // A server that sends no Content-Type used to leave the separator hanging: «:5173 node · 200 · ».
2515
2515
  const kind = found.html ? '' : (found.contentType ?? '').trim();
2516
- const what = `${found.command} · ${found.status}${kind ? ` · ${kind}` : ''}`;
2516
+ // Open to the whole network too: it opens here on 127.0.0.1, and the row says the rest.
2517
+ const what = `${found.command} · ${found.status}${kind ? ` · ${kind}` : ''}${found.network ? ` · ${t('your network can reach it too')}` : ''}`;
2517
2518
  row.append(el('i', 'ports-lamp'), el('b', null, `:${found.port}`), el('span', null, found.framable === false ? t('{what} · cannot be shown in here', { what }) : what));
2518
2519
  row.addEventListener('click', () => openPort(found));
2519
2520
  menu.append(row);
@@ -6383,15 +6384,15 @@ function markRun({ responseMessageId, index }, status, where = null, startedAt =
6383
6384
  updateRunClock(button);
6384
6385
  }
6385
6386
  if (status === 'serving' && where?.port) {
6387
+ // A server open to the whole network opens here too, on 127.0.0.1 (2026-10-18); the line says
6388
+ // that the network can reach it as well.
6386
6389
  live.append(el('span', 'where', where.network
6387
- ? t('serving on :{port} · open to your whole network, so the project will not open it', { port: where.port })
6390
+ ? t('serving on :{port} · your network can reach it too', { port: where.port })
6388
6391
  : t('serving on :{port}', { port: where.port })));
6389
- if (!where.network) {
6390
- const open = el('button', 'run-open', t('OPEN IN YOUR SERVERS'));
6391
- open.type = 'button';
6392
- open.addEventListener('click', () => { browserShow(); void browserOpen(`http://127.0.0.1:${where.port}/`); });
6393
- live.append(open);
6394
- }
6392
+ const open = el('button', 'run-open', t('OPEN IN YOUR SERVERS'));
6393
+ open.type = 'button';
6394
+ open.addEventListener('click', () => { browserShow(); void browserOpen(`http://127.0.0.1:${where.port}/`); });
6395
+ live.append(open);
6395
6396
  }
6396
6397
  const stop = el('button', 'run-stop', t('STOP'));
6397
6398
  stop.type = 'button';
package/public/es.js CHANGED
@@ -1407,7 +1407,8 @@ export const ES = {
1407
1407
  '@{agent} waits for the turn writing in the project · one turn writes at a time': '@{agent} espera a la respuesta que está escribiendo en el proyecto · solo una respuesta escribe a la vez',
1408
1408
  'SERVING': 'EN MARCHA',
1409
1409
  'serving on :{port}': 'en marcha en :{port}',
1410
- 'serving on :{port} · open to your whole network, so the project will not open it': 'en marcha en :{port} · está abierto a toda tu red, por eso el proyecto no lo abre',
1410
+ 'serving on :{port} · your network can reach it too': 'en marcha en :{port} · tu red también lo alcanza',
1411
+ 'your network can reach it too': 'tu red también lo alcanza',
1411
1412
  'OPEN IN YOUR SERVERS': 'ABRIR EN TUS SERVIDORES',
1412
1413
  'That command is not running.': 'Ese comando no está corriendo.',
1413
1414
  'what it left could not be read ({reason})': 'no se pudo leer lo que dejó ({reason})',
@@ -118,6 +118,13 @@ body.salas-mac {
118
118
  @media (max-width: 560px) { #sm-reload { display: none; } }
119
119
  .sm-brand:focus-visible { outline: 2px solid var(--s-acc); outline-offset: 3px; }
120
120
  .sm-spacer { flex: 1; }
121
+ /* A bar that does not fit (salas-mac.js, fitBar): the computer's name steps out, then the keys keep
122
+ their icons, then the agents step out. */
123
+ .sm-topbar.is-tight { gap: 10px; }
124
+ .sm-topbar.is-tight .sm-live > span { display: none; }
125
+ .sm-topbar.is-tighter .sm-barkey > span { display: none; }
126
+ .sm-topbar.is-tighter .sm-barkey { padding: 0 8px; }
127
+ .sm-topbar.is-tightest .sm-tb-agents { display: none; }
121
128
  /* The bar's keys are the room's bar keys (styles.css, .mother-button, .tree-button, .stop-all). */
122
129
  .sm-barkey, .sm-stop { display: inline-flex; align-items: center; gap: 7px; height: 26px; padding: 0 10px; border: 0; border-radius: 6px; font-family: var(--mono); font-size: 10px; font-weight: 400; letter-spacing: .18em; text-transform: uppercase; white-space: nowrap; cursor: pointer; }
123
130
  .sm-barkey svg, .sm-stop svg { width: var(--ico-sm); height: var(--ico-sm); }
@@ -3167,6 +3167,47 @@ function pinsIndexAt(x) {
3167
3167
  window.addEventListener('storage', (event) => { if (event.key === PIN_KEY) { pins.saved = null; if (data) drawPins(); } });
3168
3168
  }
3169
3169
 
3170
+ // THE BAR FITS ITS ROW (2026-10-18). Everything on it keeps its width but the projects, so with the
3171
+ // Market and Modules keys it ran under the right group and left the projects a sliver («+5»), with
3172
+ // no room to drop or move one. Now, when it does not fit, the computer's name steps out first; then
3173
+ // the keys keep only their icons (their name stays in the tooltip); then the agents, as on a narrow
3174
+ // screen. The
3175
+ // projects keep at least BAR_ROOMS of the row, and all of it while the bar is being arranged.
3176
+ const BAR_ROOMS = 220;
3177
+ let barWatch = null;
3178
+ function fitBar() {
3179
+ const bar = document.querySelector('.sm-top > .sm-topbar:first-child');
3180
+ const rooms = ui.bar.open;
3181
+ if (!bar) return;
3182
+ for (const key of bar.querySelectorAll('.sm-barkey')) { const words = key.querySelector('span')?.textContent?.trim(); if (words) { key.title = words; key.setAttribute('aria-label', words); } }
3183
+ bar.classList.remove('is-tight', 'is-tighter', 'is-tightest');
3184
+ // The projects fold the ones that do not fit behind «+n» on their own (pinsFit): asked again at
3185
+ // each step, they are short while some are folded and they have less than BAR_ROOMS.
3186
+ const roomsShort = () => {
3187
+ if (!data || !rooms || rooms.hidden) return false;
3188
+ if (pins.editing) return rooms.scrollWidth > rooms.clientWidth + 1;
3189
+ pinsFit();
3190
+ return pins.spilled.length > 0 && rooms.clientWidth < BAR_ROOMS;
3191
+ };
3192
+ const short = () => bar.scrollWidth > bar.clientWidth + 1 || roomsShort();
3193
+ for (const step of ['is-tight', 'is-tighter', 'is-tightest']) { if (!short()) break; bar.classList.add(step); }
3194
+ if (data && !pins.editing) pinsFit();
3195
+ // What fitting changed on the bar is its own doing, not a change to fit again.
3196
+ barWatch?.takeRecords();
3197
+ }
3198
+ {
3199
+ const bar = document.querySelector('.sm-top > .sm-topbar:first-child');
3200
+ let queued = false;
3201
+ const later = () => { if (queued) return; queued = true; requestAnimationFrame(() => { queued = false; fitBar(); }); };
3202
+ if (bar) {
3203
+ new ResizeObserver(later).observe(bar);
3204
+ // What sits on the bar changes it: rooms pinned or opened, agents, who waits, a newer MADRE.
3205
+ barWatch = new MutationObserver((changes) => { if (changes.some((change) => change.target !== bar)) later(); });
3206
+ barWatch.observe(bar, { childList: true, subtree: true, attributes: true, attributeFilter: ['hidden', 'class'] });
3207
+ }
3208
+ later();
3209
+ }
3210
+
3170
3211
  ui.bar.links.addEventListener('click', () => { if ((data?.enlaces ?? []).length) { if (!ui.linksSheet.open) ui.linksSheet.showModal(); } else openLinkSheet(); });
3171
3212
  ui.bar.update.addEventListener('click', () => openSettings('release'));
3172
3213
  // RELOAD, as a room reloads (app.js, reloadWithGrace): the curtain of Home's own ground with MADRE's
@@ -3963,7 +4004,8 @@ function agentsPanel({ onPaint = null, compact = false } = {}) {
3963
4004
  // scene of what it is about, moving: the agents come to MADRE and light up as they are connected;
3964
4005
  // the folders drop into the lobby; the rooms light up one after another as the beat runs through
3965
4006
  // them. Drawn here, from MADRE's own marks; still for whoever asked for less motion.
3966
- const SCENE_AGENTS = [['codex', 17, 30], ['claude', 27, 78], ['gemini', 83, 30], ['opencode', 73, 78]];
4007
+ // Five since Cursor joined (1.0.3): the four around MADRE, and Cursor above her.
4008
+ const SCENE_AGENTS = [['codex', 17, 30], ['claude', 27, 78], ['cursor', 50, 17], ['gemini', 83, 30], ['opencode', 73, 78]];
3967
4009
  const FOLDER_GLYPH = '<svg viewBox="0 0 32 26" aria-hidden="true"><path d="M2 5.5A2.5 2.5 0 0 1 4.5 3h7.2l3 3.2h12.8A2.5 2.5 0 0 1 30 8.7v12.8a2.5 2.5 0 0 1-2.5 2.5h-23A2.5 2.5 0 0 1 2 21.5z" fill="currentColor"/><path d="M2 10h28" stroke="rgba(255,255,255,.35)" stroke-width="1.4"/></svg>';
3968
4010
  const BEAT_PATH = 'M0 22H70L78 22L86 6L96 38L104 22H190L198 22L206 6L216 38L224 22H320L328 22L336 6L346 38L354 22H420';
3969
4011
  function wizardScene(kind) {
package/public/styles.css CHANGED
@@ -10218,3 +10218,9 @@ html.mk-open .toast { z-index: 2147482995 !important; left: 50% !important; righ
10218
10218
  .colophon-card-go { justify-self: start; margin-top: 2px; padding: 6px 12px; border-radius: 99px; background: var(--madre-green); color: var(--madre-green-ink); font-weight: 650; text-decoration: none; }
10219
10219
  .colophon-card-go:hover { filter: brightness(1.06); }
10220
10220
  @media (prefers-reduced-motion: reduce) { .colophon-key.heart:is(:hover, :focus-within) .colophon-key-mark svg { animation: none; } }
10221
+
10222
+ /* THE STATE DOT ALWAYS SHOWS (2026-10-18). The bar's left group shrinks with its overflow hidden, and
10223
+ the project's name kept its whole width, so what got cut was the last thing in the group: the dot
10224
+ that says whether the project is live. Now the name gives way, with its «…», and the dot never does. */
10225
+ .bar > .bar-left > #project:not(.chat-here) { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
10226
+ .bar > .bar-left > .connection { flex: none; }
package/src/ports.mjs CHANGED
@@ -22,12 +22,10 @@ const SYSTEM_PORTS = new Set([22, 25, 53, 88, 139, 445, 548, 631, 5353, 7000]);
22
22
  const NOISE = /^(rapportd|sharingd|identityservicesd|controlce|remoted|launchd|mDNSResponder|cupsd|Spotify|Dropbox|Docker|com\.docker|ollama|llama-ser)/i;
23
23
 
24
24
  // One `lsof -nP -iTCP -sTCP:LISTEN` table into the ports a human might want to look at.
25
- // Loopback only: a line bound to * or 0.0.0.0 is reachable from the network, and this room does
26
- // not help anyone frame that by accident.
27
- // Everything listening, split by whether RIPLEY may open it. The wider half is NOT a list of
28
- // things to show as openable — it is the answer to «there is nothing here», which was a lie:
29
- // `node server.js` and `python -m http.server` bind every interface by default, so the most
30
- // common case in the world was being discarded in silence and reported as an empty computer.
25
+ // Everything listening, split by where it listens: loopback, or every interface (* or 0.0.0.0),
26
+ // which is reachable from the network too. `node server.js` and `python -m http.server` bind every
27
+ // interface by default. Since 2026-10-18 the servers sentinel offers a project's own server from
28
+ // either half, opened on 127.0.0.1, and says which ones the network can also reach.
31
29
  export function classifyListening(output = '', { self = null } = {}) {
32
30
  const found = new Map();
33
31
  const wider = new Map();
@@ -76,8 +76,12 @@ export class ServerSentinel {
76
76
  const pids = [...new Set(everyone.map((one) => one.pid))];
77
77
  const cwds = pids.length ? parseCwds(await this.#cwdsOf(pids).catch(() => '')) : new Map();
78
78
  const mine = (one) => inProject(cwds.get(one.pid), this.#projectRoot);
79
- const own = listening.ports.filter(mine).slice(0, MAX_PROBED);
80
- const wider = listening.wider.filter(mine).map(({ port, command }) => ({ port, command }));
79
+ // A server bound to every interface answers on 127.0.0.1 too (2026-10-18): Node, Express and
80
+ // Vite with --host do it by default, and leaving them out left a project's own servers off its
81
+ // list. Opening one here reaches it on this computer; that it can also be reached from the
82
+ // network is the human's choice, and the list says so.
83
+ const own = [...listening.ports.filter(mine), ...listening.wider.filter(mine).map((one) => ({ ...one, network: true }))].sort(byPort).slice(0, MAX_PROBED);
84
+ const wider = [];
81
85
 
82
86
  const next = new Map();
83
87
  for (const found of own) {
@@ -88,7 +92,7 @@ export class ServerSentinel {
88
92
  if (this.#notHttp.has(key)) continue;
89
93
  const probe = await this.#probe(found.port).catch(() => ({ ok: false }));
90
94
  if (!probe.ok || await this.#isRoom(found.port).catch(() => false)) { this.#notHttp.add(key); continue; }
91
- next.set(found.port, { ...found, status: probe.status, html: probe.html, contentType: probe.contentType, framable: probe.framable, here: true });
95
+ next.set(found.port, { ...found, status: probe.status, html: probe.html, contentType: probe.contentType, framable: probe.framable, here: true, ...(found.network ? { network: true } : {}) });
92
96
  }
93
97
 
94
98
  const started = [...next.values()].filter((one) => this.#servers.get(one.port)?.pid !== one.pid);