@jossuealcala/madre 1.0.1 → 1.0.3

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.
Files changed (189) hide show
  1. package/ADDITIONAL-TERMS.md +93 -0
  2. package/CHANGELOG.en.md +2013 -0
  3. package/CHANGELOG.md +102 -1
  4. package/CONTRIBUTING.md +1 -1
  5. package/LICENSE +661 -202
  6. package/NOTICE +26 -14
  7. package/README.en.md +241 -0
  8. package/README.md +21 -12
  9. package/TRADEMARKS.en.md +50 -0
  10. package/TRADEMARKS.md +33 -16
  11. package/bin/madre.cjs +40 -0
  12. package/bin/madre.mjs +7 -0
  13. package/docs/INTERNALS.md +6 -0
  14. package/docs/REFERENCE.md +40 -5
  15. package/docs/SDK.md +4 -2
  16. package/docs/en/INTERNALS.md +103 -0
  17. package/docs/en/REFERENCE.md +365 -0
  18. package/docs/en/SDK.md +308 -0
  19. package/docs/en/training/README.md +59 -0
  20. package/docs/training/README.md +48 -9
  21. package/docs/training/train.sh +7 -2
  22. package/package.json +13 -5
  23. package/public/abduction.js +2 -0
  24. package/public/app.js +3154 -539
  25. package/public/attribution.js +114 -0
  26. package/public/brands.js +23 -6
  27. package/public/code-tools.js +143 -0
  28. package/public/editor.js +22 -4
  29. package/public/es.js +1004 -416
  30. package/public/hardware.js +50 -0
  31. package/public/i18n.js +25 -1
  32. package/public/index.html +18 -11
  33. package/public/inquiry.js +2 -0
  34. package/public/loader.js +2 -0
  35. package/public/macroverso.js +189 -96
  36. package/public/market-art.js +161 -0
  37. package/public/market-view.js +529 -0
  38. package/public/market.js +189 -0
  39. package/public/neural.js +110 -0
  40. package/public/nostromo-engine.js +414 -47
  41. package/public/power.js +100 -0
  42. package/public/resay.js +11 -0
  43. package/public/review-ask.js +2 -0
  44. package/public/routing.js +23 -1
  45. package/public/salas-mac.css +119 -18
  46. package/public/salas-mac.html +14 -2
  47. package/public/salas-mac.js +358 -56
  48. package/public/salas.css +2 -0
  49. package/public/salas.html +1 -0
  50. package/public/salas.js +4 -2
  51. package/public/styles.css +1793 -77
  52. package/public/syntax.js +2 -0
  53. package/public/touch.js +2 -0
  54. package/public/tour-chapters.js +2 -0
  55. package/public/tour.js +84 -5
  56. package/public/troubleshooting.js +24 -17
  57. package/public/wallpaper.js +275 -0
  58. package/src/adapters/claude.mjs +2 -0
  59. package/src/adapters/codex.mjs +2 -0
  60. package/src/adapters/cursor.mjs +96 -0
  61. package/src/adapters/gemini.mjs +13 -4
  62. package/src/adapters/madre.mjs +5 -2
  63. package/src/adapters/opencode.mjs +7 -3
  64. package/src/adapters/process.mjs +2 -0
  65. package/src/asking.mjs +2 -0
  66. package/src/auth-probe.mjs +145 -12
  67. package/src/bridge/devices.mjs +2 -0
  68. package/src/bridge/handoff.mjs +2 -0
  69. package/src/bridge/host.mjs +2 -0
  70. package/src/bridge/network.mjs +2 -0
  71. package/src/bridge/policy.mjs +3 -1
  72. package/src/bridge/qr.mjs +2 -0
  73. package/src/bridge/server.mjs +2 -0
  74. package/src/capabilities.mjs +11 -0
  75. package/src/chats.mjs +2 -0
  76. package/src/checkpoint.mjs +18 -1
  77. package/src/choices.mjs +2 -0
  78. package/src/cli-args.mjs +2 -0
  79. package/src/cold.mjs +2 -0
  80. package/src/commands.mjs +2 -0
  81. package/src/config.mjs +2 -0
  82. package/src/conversation-context.mjs +15 -1
  83. package/src/credentials.mjs +35 -3
  84. package/src/dataset.mjs +78 -4
  85. package/src/directives.mjs +2 -0
  86. package/src/distiller.mjs +2 -0
  87. package/src/embeddings.mjs +2 -0
  88. package/src/enlaces.mjs +214 -6
  89. package/src/event-store.mjs +2 -0
  90. package/src/exam.mjs +6 -4
  91. package/src/extensions.mjs +13 -3
  92. package/src/eyecat-watch.mjs +2 -0
  93. package/src/eyecat.mjs +2 -0
  94. package/src/file-ops.mjs +2 -0
  95. package/src/files.mjs +32 -0
  96. package/src/github.mjs +2 -0
  97. package/src/heartbeat.mjs +2 -0
  98. package/src/i18n.mjs +31 -1
  99. package/src/image-studio.mjs +2 -0
  100. package/src/json-file.mjs +2 -0
  101. package/src/launch.mjs +2 -0
  102. package/src/lease.mjs +5 -1
  103. package/src/lobby-work.mjs +2 -0
  104. package/src/local-ai.mjs +2 -0
  105. package/src/looks.mjs +127 -2
  106. package/src/market-key.mjs +8 -0
  107. package/src/market-remote.mjs +239 -0
  108. package/src/maturity.mjs +3 -1
  109. package/src/mcp/connect.mjs +345 -0
  110. package/src/mcp/github-server.mjs +2 -0
  111. package/src/mcp/image-server.mjs +2 -0
  112. package/src/mcp/memory-server.mjs +2 -0
  113. package/src/mcp/proxy.mjs +153 -0
  114. package/src/mcp/smtp-server.mjs +2 -0
  115. package/src/memory-dashboard.mjs +2 -0
  116. package/src/memory-tools.mjs +2 -0
  117. package/src/memory.mjs +2 -0
  118. package/src/models.mjs +49 -3
  119. package/src/module-scope.mjs +68 -0
  120. package/src/modules/ahp.mjs +104 -7
  121. package/src/modules/ash-policy.mjs +2 -0
  122. package/src/modules/ash.mjs +3 -1
  123. package/src/modules/bishop-exam.mjs +122 -0
  124. package/src/modules/bishop-plan.mjs +5 -3
  125. package/src/modules/bishop.mjs +84 -23
  126. package/src/modules/choices.mjs +2 -0
  127. package/src/modules/correo.mjs +2 -0
  128. package/src/modules/enlaces.mjs +89 -6
  129. package/src/modules/git-pulse.mjs +2 -0
  130. package/src/modules/github.mjs +2 -0
  131. package/src/modules/helpers.mjs +2 -0
  132. package/src/modules/image-studio.mjs +13 -2
  133. package/src/modules/index.mjs +4 -1
  134. package/src/modules/intents-calibration.mjs +2 -0
  135. package/src/modules/intents-desk.mjs +4 -2
  136. package/src/modules/intents-observe.mjs +2 -0
  137. package/src/modules/intents-policy.mjs +4 -1
  138. package/src/modules/intents-try.mjs +2 -0
  139. package/src/modules/intents.mjs +11 -7
  140. package/src/modules/local-models.mjs +124 -0
  141. package/src/modules/mcp-connect.mjs +293 -0
  142. package/src/modules/ollama.mjs +15 -4
  143. package/src/modules/playwright.mjs +88 -15
  144. package/src/modules/ripley.mjs +2 -0
  145. package/src/modules/runs.mjs +5 -3
  146. package/src/modules/sdk.mjs +63 -9
  147. package/src/modules/updates.mjs +2 -0
  148. package/src/mother.mjs +2 -0
  149. package/src/npm-launch.mjs +18 -0
  150. package/src/ollama.mjs +8 -5
  151. package/src/outbound.mjs +16 -6
  152. package/src/override.mjs +2 -0
  153. package/src/ports.mjs +6 -2
  154. package/src/prices.mjs +2 -0
  155. package/src/privacy.mjs +2 -0
  156. package/src/quota-monitor.mjs +2 -0
  157. package/src/quota-sources.mjs +2 -0
  158. package/src/request-guard.mjs +2 -0
  159. package/src/review-ask.mjs +2 -0
  160. package/src/room/archivist.mjs +2 -0
  161. package/src/room/attachments.mjs +2 -0
  162. package/src/room/budget.mjs +2 -0
  163. package/src/room/context.mjs +2 -0
  164. package/src/room/control.mjs +7 -1
  165. package/src/room/copilot.mjs +76 -0
  166. package/src/room/economy.mjs +2 -0
  167. package/src/room/escalation.mjs +2 -0
  168. package/src/room/ghost.mjs +2 -0
  169. package/src/room/guard.mjs +2 -0
  170. package/src/room/prompt.mjs +25 -5
  171. package/src/room/returns.mjs +82 -0
  172. package/src/room/runs.mjs +4 -0
  173. package/src/room/vectors.mjs +2 -0
  174. package/src/room.mjs +223 -35
  175. package/src/router.mjs +3 -1
  176. package/src/runs.mjs +6 -1
  177. package/src/runtime-detection.mjs +11 -0
  178. package/src/salas.mjs +28 -0
  179. package/src/sentinel-errors.mjs +2 -0
  180. package/src/server-sentinel.mjs +2 -0
  181. package/src/server.mjs +757 -81
  182. package/src/setup.mjs +3 -1
  183. package/src/smtp.mjs +2 -0
  184. package/src/tour.mjs +2 -0
  185. package/src/updates.mjs +2 -0
  186. package/src/usage-sentinel.mjs +2 -0
  187. package/src/vault.mjs +2 -0
  188. package/src/verdict.mjs +3 -1
  189. package/src/whats-new.mjs +2 -0
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
1
3
  // PLAYWRIGHT: a headless browser for the crew, pointed only at this MADRE. Agents open the
2
4
  // RIPLEY preview of project files, click, read the console and take screenshots into the turn's
3
5
  // scratch folder. Runs @playwright/mcp per turn, isolated, with allowed origins limited to the
@@ -9,11 +11,16 @@ import { tmpdir } from 'node:os';
9
11
  import { join } from 'node:path';
10
12
  import { t } from '../i18n.mjs';
11
13
  import { defineModule } from './sdk.mjs';
14
+ import { switchIn } from '../module-scope.mjs';
12
15
  import { packageVersion } from './helpers.mjs';
13
16
  import { terminateProcessTree, lastWords } from '../adapters/process.mjs';
14
17
 
15
18
  export const PLAYWRIGHT_PACKAGE = '@playwright/mcp';
16
19
  export const PLAYWRIGHT_BROWSERS = ['chromium', 'firefox', 'webkit'];
20
+ // The browser the server downloads for each engine: `install-browser` is `playwright install <name>`
21
+ // underneath, so the name is the engine's own. The remedy used to say chrome-for-testing whatever
22
+ // was chosen, and a person on webkit installed a browser that could never fix it (2026-10-14).
23
+ export const installBrowserLine = (browser = 'chromium') => `npx ${PLAYWRIGHT_PACKAGE} install-browser ${PLAYWRIGHT_BROWSERS.includes(browser) ? browser : 'chromium'}`;
17
24
  export const PLAYWRIGHT_SERVER_NAME = 'pulse-playwright';
18
25
  // The tools @playwright/mcp exposes that a room turn may use. Screenshots and files land in scratch.
19
26
  export const PLAYWRIGHT_TOOLS = ['browser_navigate', 'browser_navigate_back', 'browser_snapshot', 'browser_click', 'browser_type', 'browser_fill_form', 'browser_hover', 'browser_press_key', 'browser_select_option', 'browser_wait_for', 'browser_console_messages', 'browser_network_requests', 'browser_take_screenshot', 'browser_resize', 'browser_tabs', 'browser_close'];
@@ -29,6 +36,51 @@ export async function playwrightVersion({ env = process.env, projectRoot = proce
29
36
  return version;
30
37
  }
31
38
 
39
+ // Why npm stopped, in its own words: the line that says it, not the last one, which only points at
40
+ // a log file («npm error A complete log of this run can be found in …», 2026-10-18).
41
+ export function npmReason(output) {
42
+ const lines = String(output ?? '').split('\n').map((line) => line.trim()).filter(Boolean);
43
+ const said = lines.filter((line) => /^npm (error|ERR!)/i.test(line)).map((line) => line.replace(/^npm (error|ERR!)\s*/i, '')).filter((line) => line && !/complete log of this run|^(code|errno|syscall)\b/i.test(line));
44
+ return said.find((line) => !/^path\b/i.test(line)) ?? said[0] ?? lastWords(output);
45
+ }
46
+
47
+ // One command of the module's own, run to its end with no terminal: getting the server or its
48
+ // browser onto this computer. What it printed last is kept for the case it fails.
49
+ function runToEnd(command, args, { cwd, env, timeoutMs = 900000 } = {}) {
50
+ return new Promise((resolve) => {
51
+ let err = '';
52
+ let child;
53
+ try { child = spawn(command, args, { cwd, env: { ...env, CI: '1', NO_COLOR: '1' }, stdio: ['ignore', 'ignore', 'pipe'], shell: false, detached: true, windowsHide: true }); }
54
+ catch (error) { resolve({ code: 1, said: error.message }); return; }
55
+ const timer = setTimeout(() => terminateProcessTree(child, { graceMs: 1000 }), timeoutMs);
56
+ timer.unref?.();
57
+ child.stderr.setEncoding('utf8');
58
+ child.stderr.on('data', (chunk) => { err = (err + chunk).slice(-4000); });
59
+ child.on('error', (error) => { clearTimeout(timer); resolve({ code: 1, said: error.message }); });
60
+ child.on('close', (code) => { clearTimeout(timer); resolve({ code, said: npmReason(err) }); });
61
+ });
62
+ }
63
+
64
+ // What the switch puts in place by itself before it gives up (2026-10-18): the person turned it
65
+ // on, and a browser to download is MADRE's to fetch, not a line to copy into a terminal. The
66
+ // server, globally (where `npx --no` finds it at turn time), when it is missing; then the browser
67
+ // the server itself downloads for the engine chosen. Asked again after.
68
+ export async function preparePlaywright({ browser = 'chromium', env = process.env, cwd = process.cwd(), run = runToEnd, starts = playwrightStarts, version = playwrightVersion } = {}) {
69
+ const engine = PLAYWRIGHT_BROWSERS.includes(browser) ? browser : 'chromium';
70
+ let installed = await version({ env, projectRoot: cwd, fresh: true });
71
+ const steps = [];
72
+ if (!installed) {
73
+ const server = await run('npm', ['install', '-g', PLAYWRIGHT_PACKAGE], { cwd, env });
74
+ steps.push({ display: `npm install -g ${PLAYWRIGHT_PACKAGE}`, code: server.code });
75
+ installed = await version({ env, projectRoot: cwd, fresh: true });
76
+ if (!installed) return { ok: false, steps, problem: server.said ?? t('the browser server could not be installed') };
77
+ }
78
+ const fetched = await run('npx', ['--no', PLAYWRIGHT_PACKAGE, 'install-browser', engine], { cwd, env });
79
+ steps.push({ display: installBrowserLine(engine), code: fetched.code });
80
+ const check = await starts({ browser, env, cwd });
81
+ return { ...check, steps, ...(check.ok ? {} : { problem: check.problem ?? fetched.said }) };
82
+ }
83
+
32
84
  // Does the browser server start, with its browser? Asked when the switch is turned on, with the
33
85
  // same server a turn gets: the card said «on · chromium» while every turn's server closed before it
34
86
  // spoke, and the agents saw only CONNECTION_CLOSED. It is asked to open about:blank, because a
@@ -87,7 +139,7 @@ export async function playwrightStarts({ browser = 'chromium', env = process.env
87
139
  });
88
140
  child.stderr.on('data', (chunk) => { err += chunk; });
89
141
  child.on('error', (error) => finish({ ok: false, problem: error.message }));
90
- child.on('close', (code) => finish({ ok: false, problem: lastWords(err) ?? t('it exited with code {code} before it spoke', { code }) }));
142
+ child.on('close', (code) => finish({ ok: false, problem: npmReason(err) ?? t('it exited with code {code} before it spoke', { code }) }));
91
143
  send({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2025-03-26', capabilities: {}, clientInfo: { name: 'madre', version: '1' } } });
92
144
  });
93
145
  }
@@ -101,15 +153,18 @@ export const wantsBrowser = (text) => BROWSER_INTENT.test(String(text ?? ''));
101
153
  // The last answer to «does it start?», for a switch that was already on: asked again in the
102
154
  // background at most every ten minutes, so the card can say the browser is missing and no turn
103
155
  // carries a server that would only fail.
104
- let started = { at: 0, result: null, asking: null };
156
+ // The answer belongs to the browser it was asked about: chosen another, it is asked again, or a
157
+ // browser that does not start would ride on the answer of one that does.
158
+ let started = { at: 0, result: null, asking: null, browser: null };
105
159
  export function playwrightHealth({ browser = 'chromium', env = process.env, cwd = process.cwd(), now = Date.now(), check = playwrightStarts } = {}) {
106
160
  if (env.PULSE_PLAYWRIGHT_CHECK === '0') return started.result;
161
+ if (started.browser !== browser) started = { at: 0, result: null, asking: null, browser };
107
162
  if (!started.asking && now - started.at > 10 * 60 * 1000) {
108
- started.asking = check({ browser, env, cwd }).then((result) => { started = { at: Date.now(), result, asking: null }; return result; }, () => { started.asking = null; return null; });
163
+ started.asking = check({ browser, env, cwd }).then((result) => { if (started.browser === browser) started = { at: Date.now(), result, asking: null, browser }; return result; }, () => { started.asking = null; return null; });
109
164
  }
110
165
  return started.result;
111
166
  }
112
- export function forgetPlaywrightHealth(result = null) { started = { at: result ? Date.now() : 0, result, asking: null }; }
167
+ export function forgetPlaywrightHealth(result = null, browser = null) { started = { at: result ? Date.now() : 0, result, asking: null, browser }; }
113
168
 
114
169
  // What the browser may open: this room, and the servers the human started from RUNS in this project
115
170
  // while they listen on 127.0.0.1. RIPLEY's sealed frame keeps no data, so an app that stores its
@@ -134,6 +189,24 @@ export function playwrightServerFor({ port, outputDir, browser = 'chromium', hea
134
189
  };
135
190
  }
136
191
 
192
+ async function playwrightReady(ctx) {
193
+ const browser = ctx.settings.browser ?? 'chromium';
194
+ let check = await playwrightStarts({ browser, env: ctx.env, cwd: ctx.projectRoot });
195
+ // Something missing: MADRE fetches it, then asks again, before saying anything to the person.
196
+ if (!check.ok && process.platform !== 'win32') {
197
+ await ctx.record('extension.preparing', { id: 'playwright', name: 'PLAYWRIGHT', browser });
198
+ check = await preparePlaywright({ browser, env: ctx.env, cwd: ctx.projectRoot });
199
+ }
200
+ forgetPlaywrightHealth(check, browser);
201
+ if (check.ok) return null;
202
+ // The server is there and only the browser is missing: say which one, and the one line that
203
+ // installs it. Without the server, both lines.
204
+ const version = await playwrightVersion({ env: ctx.env, projectRoot: ctx.projectRoot });
205
+ return { status: 412, body: { error: version
206
+ ? t('TEST BROWSER stays off: MADRE downloaded the {browser} browser and it still does not start ({problem}). Check your internet connection and turn it on again, or choose another browser in its settings.', { problem: String(check.problem).replace(/[.\s]+$/, ''), browser })
207
+ : t('TEST BROWSER stays off: MADRE could not install its browser server ({problem}). Check your internet connection and turn it on again.', { problem: String(check.problem).replace(/[.\s]+$/, '') }), code: 'MU-054' } };
208
+ }
209
+
137
210
  export default defineModule({
138
211
  id: 'playwright',
139
212
  name: 'Test browser',
@@ -141,7 +214,7 @@ export default defineModule({
141
214
  tracks: { name: PLAYWRIGHT_PACKAGE, npm: PLAYWRIGHT_PACKAGE },
142
215
  summary: 'Hands an agent a headless browser that reaches only this project and the servers you start from COMMANDS, on the turns that ask to look at a page, click through it, read its console or take screenshots.',
143
216
  creates: ['nothing in the project \u00b7 screenshots land in .pulse/out/<turn>/', 'a switch in ~/.pulse/config.json', 'a browser per turn, started and stopped by the CLI', 'no origin but this project is reachable through it'],
144
- requires: ['@playwright/mcp and a chromium browser on this machine', 'PREVIEW on, to have pages to open'],
217
+ requires: ['@playwright/mcp and the browser you choose for it (chromium by default) on this machine', 'PREVIEW on, to have pages to open'],
145
218
  settings: { enabled: false, browser: 'chromium', headless: true },
146
219
  card: 'switch',
147
220
  controls: [
@@ -154,8 +227,8 @@ export default defineModule({
154
227
  return {
155
228
  runs: [{ name: PLAYWRIGHT_PACKAGE, version }],
156
229
  settings: { browser: ctx.settings.browser ?? 'chromium', headless: ctx.settings.headless !== false },
157
- status: { installed: Boolean(ctx.settings.enabled), detail: ctx.settings.enabled ? (!version ? t('on · the browser server is not installed') : health?.ok === false ? t('on · the browser does not start: {problem}', { problem: String(health.problem).slice(0, 160) }) : `${t('on')} · ${ctx.settings.browser}`) : version ? t('off') : t('off · the browser server is not installed') },
158
- preflight: version ? { ok: true, problems: [] } : { ok: false, problems: ['Install the browser server first: npm install -g @playwright/mcp, then its browser: npx @playwright/mcp install-browser chrome-for-testing'] },
230
+ status: { installed: Boolean(ctx.settings.enabled), detail: ctx.settings.enabled ? (!version ? t('on · the browser server is not installed') : health?.ok === false ? t('on · the browser does not start: {problem}. Turn it off and on again: MADRE downloads what is missing.', { problem: String(health.problem).replace(/[.\s]+$/, '').slice(0, 160) }) : `${t('on')} · ${ctx.settings.browser}`) : version ? t('off') : t('off · the browser server is not installed') },
231
+ preflight: version ? { ok: true, problems: [] } : { ok: false, problems: [`Install the browser server first: npm install -g @playwright/mcp, then its browser: ${installBrowserLine(ctx.settings.browser)}`] },
159
232
  install: { display: ctx.settings.enabled ? 'disable PLAYWRIGHT' : 'enable PLAYWRIGHT (config.json)', platforms: ['codex', 'claude', 'gemini', 'opencode'] },
160
233
  };
161
234
  },
@@ -183,15 +256,15 @@ export default defineModule({
183
256
  const outputDir = turn.scratchDir ?? join(turn.roomDir ?? ctx.stateRoot, 'playwright');
184
257
  return [playwrightServerFor({ port: turn.port, outputDir, browser: ctx.settings.browser ?? 'chromium', headless: ctx.settings.headless !== false, servers: turn.servers ?? [] })];
185
258
  },
186
- // The switch, guarded: on only if the server starts and has its browser. Off is always allowed.
259
+ // Before it turns on, in one project or in all: the server starts and has its browser, or MADRE
260
+ // fetches what is missing and asks again. `null` is ready; anything else is the refusal.
261
+ ready: playwrightReady,
262
+ // The switch, guarded: on only if it is ready. Off is always allowed. A project's switch is that
263
+ // project's (module-scope.mjs).
187
264
  async toggle(ctx) {
188
265
  const enabled = !ctx.settings.enabled;
189
- if (enabled) {
190
- const check = await playwrightStarts({ browser: ctx.settings.browser ?? 'chromium', env: ctx.env, cwd: ctx.projectRoot });
191
- forgetPlaywrightHealth(check);
192
- if (!check.ok) return { status: 412, body: { error: t('TEST BROWSER did not start, so it stays off: {problem}. Install the server and its browser (npm install -g @playwright/mcp, then npx @playwright/mcp install-browser chrome-for-testing) and turn it on again.', { problem: String(check.problem).replace(/[.\s]+$/, '') }), code: 'MU-054' } };
193
- }
194
- await ctx.updateConfig({ modules: { playwright: { ...(ctx.config.modules?.playwright ?? {}), enabled } } });
266
+ if (enabled) { const refused = await playwrightReady(ctx); if (refused) return refused; }
267
+ await ctx.updateConfig({ modules: { playwright: switchIn(ctx.config.modules?.playwright ?? {}, ctx.roomId ?? null, enabled) } });
195
268
  await ctx.record('extension.toggled', { id: 'playwright', name: 'PLAYWRIGHT', enabled });
196
269
  return { status: 200, body: { enabled } };
197
270
  },
@@ -203,6 +276,6 @@ export default defineModule({
203
276
  match: /@playwright\/mcp|playwright.*not (found|installed)|browser server/i,
204
277
  diagnosis: 'The TEST BROWSER module runs @playwright/mcp per turn. Two different things have to be on this computer and only one of them is the package: the server, and the browser build that this version of it expects. A server that starts without its browser answers every tool call with "Browser … is not installed", and one that cannot start at all reaches the agent as CONNECTION_CLOSED.',
205
278
  remedy: 'Install the server once, globally, then its browser — the browser is downloaded by the server itself, not by the playwright CLI — and RECHECK in MODULES.',
206
- fixes: { darwin: ['npm install -g @playwright/mcp', 'npx @playwright/mcp install-browser chrome-for-testing'], linux: ['npm install -g @playwright/mcp', 'npx @playwright/mcp install-browser chrome-for-testing'] },
279
+ fixes: { darwin: ['npm install -g @playwright/mcp', 'npx @playwright/mcp install-browser chromium'], linux: ['npm install -g @playwright/mcp', 'npx @playwright/mcp install-browser chromium'] },
207
280
  }],
208
281
  });
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
1
3
  // RIPLEY: the file viewer renders HTML, SVG and Markdown inside a sealed frame.
2
4
  // A switch in config.json, read live by the preview route.
3
5
 
@@ -1,9 +1,11 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
1
3
  // Runs: letting an agent ask the human to run a command instead of asking them to copy it.
2
4
  //
3
5
  // Below #4 AIRLOCK an agent cannot run anything, and some answers turn on a command anyway: the
4
6
  // tests after a fix, a build, a linter. This gives that request a shape — a fenced block the room
5
- // turns into one button per command — and brings the output back to the room as a card every
6
- // agent reads on its next turn.
7
+ // turns into one button per command — and brings the output back to the room as a card, and to
8
+ // the agent that asked in a turn of its own once every line has run (src/room/returns.mjs).
7
9
  //
8
10
  // Nothing an agent writes runs by itself. Each line is pressed by the human, runs alone from the
9
11
  // project root with no shell, and is photographed first so UNDO can put the files back. What a
@@ -22,7 +24,7 @@ export default defineModule({
22
24
  name: 'Commands',
23
25
  vendor: 'MADRE',
24
26
  version: '1.0.0',
25
- summary: 'Lets an agent ask you to run a command it cannot run in its mode (tests, a build, a linter). Each command is a button: nothing runs until you press it, it runs from the project root with no shell, a restore point is taken first, and the output comes back to the project. Costs about 300 tokens of instruction per turn below FULL ACCESS while it is on.',
27
+ summary: 'Lets an agent ask you to run a command it cannot run in its mode (tests, a build, a linter). Each command is a button: nothing runs until you press it, it runs from the project root with no shell, a restore point is taken first, and when it has run the output goes back to the agent that asked, which goes on. Costs about 300 tokens of instruction per turn below FULL ACCESS while it is on.',
26
28
  creates: ['nothing in the project', 'a switch in ~/.pulse/config.json'],
27
29
  async status(ctx) {
28
30
  return {
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
1
3
  // MADRE's module SDK. A module is one file: what it is, the settings it keeps
2
4
  // in ~/.pulse/config.json, how it describes itself to MODULES, what its switch
3
5
  // does, the routes it serves and the events it listens to. The server builds a
@@ -17,6 +19,7 @@
17
19
 
18
20
  import { readFile, stat } from 'node:fs/promises';
19
21
  import { forgetSecret, readSecret, summary as vaultSummary, writeSecret } from '../vault.mjs';
22
+ import { moduleOnIn, roomsOf, scopeIn, settingsIn, switchIn } from '../module-scope.mjs';
20
23
 
21
24
  const camel = (id) => id.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
22
25
 
@@ -65,7 +68,7 @@ export function dependencies(runs) {
65
68
  return list.filter((dep) => dep && dep.name).map((dep) => ({ name: String(dep.name), version: dep.version ?? null, target: dep.target ?? null }));
66
69
  }
67
70
 
68
- // Every field this contract answers to. Thirty-four of them and all but two optional, which is
71
+ // Every field this contract answers to. Thirty-eight of them and all but two optional, which is
69
72
  // generous until you misspell one: `sumary` used to install without a word and leave a card with
70
73
  // an empty summary. A key MADRE does not know is not fatal — a module written for a newer MADRE
71
74
  // may carry fields this one has not learned yet — so it is said out loud instead of refused.
@@ -74,10 +77,31 @@ export const MODULE_FIELDS = new Set([
74
77
  'requires', 'models', 'commands', 'card', 'configKey', 'settings', 'controls', 'routes', 'reaches',
75
78
  'conditions', 'slash', 'status', 'preflight', 'detect', 'confirm', 'installCommand', 'secrets',
76
79
  'updatePlan', 'toolsForTurn', 'toggle', 'onToggle', 'onSettings', 'onEvent', 'toggledEvent',
77
- 'toggledBody', 'describe',
80
+ 'toggledBody', 'describe', 'ready', 'footer',
78
81
  ]);
79
82
  export const unknownFields = (spec) => Object.keys(spec ?? {}).filter((key) => !MODULE_FIELDS.has(key));
80
83
 
84
+ // A key beside MADRE's version (2026-10-18): what a module may put on the page, and nothing more.
85
+ // MADRE draws it — one of its own icons, the module's words, an https address that opens in a new
86
+ // tab — so a module never reaches into the page itself.
87
+ export const FOOTER_ICONS = ['heart', 'star', 'link'];
88
+ const footerWords = (value) => {
89
+ if (typeof value === 'string') return value.slice(0, 160);
90
+ if (!value || typeof value !== 'object') return null;
91
+ const out = {};
92
+ for (const lang of ['en', 'es']) if (typeof value[lang] === 'string') out[lang] = value[lang].slice(0, 160);
93
+ return Object.keys(out).length ? out : null;
94
+ };
95
+ function footerKeys(spec) {
96
+ return (Array.isArray(spec.footer) ? spec.footer : []).slice(0, 2).map((key) => {
97
+ let url = null;
98
+ try { url = new URL(String(key?.url ?? '')); } catch { /* said below */ }
99
+ if (url?.protocol !== 'https:') throw new Error(`Module ${spec.id}: a key beside the version needs an https address.`);
100
+ if (!footerWords(key.label)) throw new Error(`Module ${spec.id}: a key beside the version needs a label.`);
101
+ return { icon: FOOTER_ICONS.includes(key.icon) ? key.icon : 'link', label: footerWords(key.label), text: footerWords(key.text), action: footerWords(key.action), url: url.href };
102
+ });
103
+ }
104
+
81
105
  export function defineModule(spec) {
82
106
  if (!spec?.id || !/^[a-z][a-z0-9-]*$/.test(spec.id)) throw new Error(`Module id must be kebab-case: ${spec?.id}`);
83
107
  if (!spec.name) throw new Error(`Module ${spec.id} needs a name.`);
@@ -95,6 +119,8 @@ export function defineModule(spec) {
95
119
  // lives and MADRE can go and get it: the file is fetched, checked the same way an upload is,
96
120
  // and only replaces the one installed if it passes and says it is newer.
97
121
  updates: spec.updates?.url && /^https:\/\//.test(String(spec.updates.url)) ? { url: String(spec.updates.url) } : null,
122
+ // Keys beside MADRE's version, drawn by MADRE while the module is on (footerKeys).
123
+ footer: footerKeys(spec),
98
124
  summary: spec.summary ?? '', creates: spec.creates ?? [], requires: spec.requires ?? [], models: spec.models ?? [], commands: spec.commands ?? (spec.slash?.length ? spec.slash.map((command) => command.usage ?? `/${command.name}`) : undefined),
99
125
  card: spec.card ?? (kind === 'builtin' ? 'switch' : 'installer'),
100
126
  // The settings floor of the card, declared instead of drawn: MADRE renders these and saves
@@ -131,8 +157,14 @@ export function defineModule(spec) {
131
157
  return { name: secret.name, label: secret.label ?? secret.name.toUpperCase(), note: secret.note ?? '', where: secret.where ?? '' };
132
158
  }),
133
159
  };
134
- const settingsFrom = (config) => ({ ...defaults, ...(config?.modules?.[configKey] ?? {}) });
160
+ // A project reads its own word on `enabled` when it said one (module-scope.mjs).
161
+ const settingsFrom = (config, roomId = null) => ({ ...defaults, ...settingsIn(config?.modules?.[configKey] ?? {}, roomId) });
135
162
  const mine = (ctx, settings) => ({ ...ctx, settings, vault: vaultFor(spec.id, ctx.stateRoot) });
163
+ const scopeHere = (ctx) => {
164
+ const block = ctx.config?.modules?.[configKey] ?? {};
165
+ const own = ctx.roomId ? roomsOf(block)[ctx.roomId] : undefined;
166
+ return { all: typeof block.enabled === 'boolean' ? block.enabled : Boolean(defaults.enabled), here: typeof own === 'boolean' ? own : null };
167
+ };
136
168
  const module = {
137
169
  ...base,
138
170
  configKey,
@@ -150,14 +182,14 @@ export function defineModule(spec) {
150
182
  }),
151
183
  // Tools for a turn, only while the module is on. Failures never break a turn.
152
184
  toolsForTurn: spec.toolsForTurn ? async (ctx, turn) => {
153
- const settings = settingsFrom(ctx.config);
185
+ const settings = settingsFrom(ctx.config, ctx.roomId);
154
186
  if (kind === 'builtin' && !settings.enabled) return [];
155
187
  try { return (await spec.toolsForTurn(mine(ctx, settings), turn)) ?? []; } catch (error) { console.error(`MADRE module ${spec.id}: toolsForTurn failed: ${error.message}`); return []; }
156
188
  } : null,
157
189
  // How the outside thing this module drives gets a newer version onto this computer. MADRE
158
190
  // shows the command before it runs and never runs one the human has not read. A module that
159
191
  // cannot update what it drives returns the note that says where to get it instead.
160
- updatePlan: spec.updatePlan ? async (ctx, what) => spec.updatePlan(mine(ctx, settingsFrom(ctx.config)), what) : null,
192
+ updatePlan: spec.updatePlan ? async (ctx, what) => spec.updatePlan(mine(ctx, settingsFrom(ctx.config, ctx.roomId)), what) : null,
161
193
  // Legacy installer hooks, kept on the object so the confirm-and-run path can use them.
162
194
  detect: spec.detect ?? null,
163
195
  preflight: spec.preflight ?? null,
@@ -165,7 +197,7 @@ export function defineModule(spec) {
165
197
 
166
198
  // What MODULES shows: base fields plus status, preflight, install line and whatever the module adds.
167
199
  async describe(ctx) {
168
- const settings = settingsFrom(ctx.config);
200
+ const settings = settingsFrom(ctx.config, ctx.roomId);
169
201
  const own = spec.status ? await spec.status(mine(ctx, settings)) : {};
170
202
  const installed = own.installed ?? (kind === 'builtin' ? Boolean(settings.enabled) : false);
171
203
  const runs = dependencies(own.runs);
@@ -186,6 +218,8 @@ export function defineModule(spec) {
186
218
  status: own.status ?? { installed, detail: own.detail ?? (installed ? 'on' : 'off') },
187
219
  preflight: own.preflight ?? { ok: true, problems: [] },
188
220
  install: own.install ?? (kind === 'builtin' ? { display: installed ? `disable ${base.name}` : `enable ${base.name} (config.json)`, platforms: [] } : { display: '', platforms: [] }),
221
+ // Whether it is on for every project, and this project's own word when it said one.
222
+ ...(kind === 'builtin' ? { scope: scopeHere(ctx) } : {}),
189
223
  };
190
224
  },
191
225
 
@@ -202,7 +236,7 @@ export function defineModule(spec) {
202
236
  } else {
203
237
  next = String(value ?? '').slice(0, 500);
204
238
  }
205
- const settings = { ...settingsFrom(ctx.config), [key]: next };
239
+ const settings = { ...settingsFrom(ctx.config, ctx.roomId), [key]: next };
206
240
  await ctx.updateConfig({ modules: { [configKey]: { ...(ctx.config.modules?.[configKey] ?? {}), [key]: next } } });
207
241
  if (spec.onSettings) await spec.onSettings(mine(ctx, settings), settings);
208
242
  return { status: 200, body: { settings: Object.fromEntries(base.controls.map((known) => [known.key, settings[known.key] ?? null])) } };
@@ -211,15 +245,35 @@ export function defineModule(spec) {
211
245
  // The switch. Default for builtins: flip `enabled`, persist, tell the room.
212
246
  // A module may guard it (`confirm`) or replace it (`toggle`).
213
247
  toggle: kind === 'builtin' || spec.toggle ? async (ctx, payload = {}) => {
214
- const settings = settingsFrom(ctx.config);
248
+ const settings = settingsFrom(ctx.config, ctx.roomId);
215
249
  if (spec.toggle) return spec.toggle(mine(ctx, settings), payload);
216
250
  const enabled = !settings.enabled;
217
251
  if (enabled && spec.confirm && payload.confirm !== true) return { status: 400, body: { error: spec.confirm } };
218
- await ctx.updateConfig({ modules: { [configKey]: { ...(ctx.config.modules?.[configKey] ?? {}), enabled } } });
252
+ if (enabled && spec.ready) { const refused = await spec.ready(mine(ctx, settings)); if (refused) return refused; }
253
+ // A project's switch is that project's (module-scope.mjs); Home's is every project's.
254
+ await ctx.updateConfig({ modules: { [configKey]: switchIn(ctx.config.modules?.[configKey] ?? {}, ctx.roomId ?? null, enabled) } });
219
255
  const after = spec.onToggle ? await spec.onToggle(mine(ctx, { ...settings, enabled }), enabled) : null;
220
256
  await ctx.record('extension.toggled', { id: spec.id, name: base.name, enabled, ...(spec.toggledEvent ?? {}) });
221
257
  return { status: 200, body: { enabled, ...(after ?? {}), ...(spec.toggledBody?.(enabled) ?? {}) } };
222
258
  } : null,
259
+
260
+ // Home's word on which projects (2026-10-18): `{ all }` for every project, `{ rooms: { id: on } }`
261
+ // for some. Turning it on anywhere asks what turning it on asks (`confirm`, `ready`); the
262
+ // project rooms read the new word on their next turn.
263
+ scope: kind === 'builtin' || spec.toggle ? async (ctx, change = {}) => {
264
+ const block = ctx.config.modules?.[configKey] ?? {};
265
+ const next = scopeIn(block, change);
266
+ const fallback = Boolean(defaults.enabled);
267
+ const turningOn = change.all === true || Object.values(change.rooms ?? {}).some((on) => on === true);
268
+ const settings = settingsFrom(ctx.config, ctx.roomId);
269
+ if (turningOn && spec.confirm && change.confirm !== true) return { status: 400, body: { error: spec.confirm } };
270
+ if (turningOn && spec.ready) { const refused = await spec.ready(mine(ctx, settings)); if (refused) return refused; }
271
+ await ctx.updateConfig({ modules: { [configKey]: next } });
272
+ const here = moduleOnIn(next, ctx.roomId ?? null, fallback);
273
+ if (spec.onToggle && here !== Boolean(settings.enabled)) await spec.onToggle(mine(ctx, { ...settings, enabled: here }), here);
274
+ await ctx.record('extension.toggled', { id: spec.id, name: base.name, enabled: here, scope: { all: typeof next.enabled === 'boolean' ? next.enabled : fallback, rooms: roomsOf(next) } });
275
+ return { status: 200, body: { all: typeof next.enabled === 'boolean' ? next.enabled : fallback, rooms: roomsOf(next) } };
276
+ } : null,
223
277
  };
224
278
  return Object.freeze(module);
225
279
  }
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
1
3
  // Is there a newer version of what a module is? Two kinds of answer, because there are two kinds
2
4
  // of module.
3
5
  //
package/src/mother.mjs CHANGED
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
1
3
  // MU/TH/UR's private channel. When someone strikes at her memory core, MOTHER
2
4
  // tells her children (the agents) in a code the console cannot read; the room
3
5
  // shows the code, the agents get the words. The channel lives in
@@ -0,0 +1,18 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
3
+ // What `npx` leaves in MADRE's environment about the call that started it, taken away before MADRE
4
+ // runs anything (2026-10-18).
5
+ //
6
+ // npm hands every process it starts the details of its own run: which package it was asked for
7
+ // (npm_config_package), that it may say yes for you (npm_config_yes), the folder it was typed in
8
+ // (npm_config_local_prefix, INIT_CWD), its own command and script. They are about that one call,
9
+ // and every npm or npx MADRE ran after inherited them: Continuity's installer, started in a
10
+ // project, went to install the package MADRE had been launched from, in the project's folder,
11
+ // and failed. The person's npm configuration (cache, registry, prefix, their npmrc) stays.
12
+ const LAUNCH = /^(npm_config_(package|yes|call|local_prefix|npm_version|user_agent)|npm_command|npm_execpath|npm_node_execpath|npm_lifecycle_event|npm_lifecycle_script|npm_package_json|INIT_CWD)$/i;
13
+
14
+ export function forgetNpxLaunch(env = process.env) {
15
+ const gone = [];
16
+ for (const name of Object.keys(env)) if (LAUNCH.test(name)) { delete env[name]; gone.push(name); }
17
+ return gone;
18
+ }
package/src/ollama.mjs CHANGED
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
1
3
  // Ollama: the local intelligence MADRE leans on when it is there. Embeddings
2
4
  // for recall by meaning and a model to distil memories, both on this machine,
3
5
  // so remembering costs no tokens elsewhere and nothing leaves. Nothing here is
@@ -8,10 +10,11 @@ import { t } from './i18n.mjs';
8
10
  export const DEFAULT_OLLAMA_HOST = 'http://127.0.0.1:11434';
9
11
  // Embedding models we know how to talk to, best first.
10
12
  export const EMBED_MODELS = ['nomic-embed-text', 'mxbai-embed-large', 'snowflake-arctic-embed', 'all-minilm', 'bge-m3'];
11
- // Chat models that follow instructions well enough to distil, best first for a 16 GB machine.
12
- // General chat models first: @madre speaks for the room in the human's language, and coder
13
- // models drift into other voices. A coder is taken only when nothing else is there.
14
- export const CHAT_MODELS = ['qwen2.5:7b', 'llama3.1:8b', 'gemma3:4b', 'qwen2.5:3b', 'llama3.2:3b', 'qwen2.5:1.5b', 'llama3.2:1b', 'qwen2.5-coder:7b', 'qwen2.5-coder:1.5b'];
13
+ // Chat models that follow instructions well enough to distil, best first. The larger Qwen2.5
14
+ // sizes lead: the room only offers one to a computer that runs it (src/modules/local-models.mjs),
15
+ // so when it is there it was pulled for this machine. General chat models before coders: @madre
16
+ // speaks for the room in the human's language, and coder models drift into other voices.
17
+ export const CHAT_MODELS = ['qwen2.5:72b', 'qwen2.5:32b', 'qwen2.5:14b', 'qwen2.5:7b', 'llama3.1:8b', 'gemma3:4b', 'qwen2.5:3b', 'llama3.2:3b', 'qwen2.5:1.5b', 'llama3.2:1b', 'qwen2.5-coder:7b', 'qwen2.5-coder:1.5b'];
15
18
  // Ollama's default window is 4k tokens and it drops the OLDEST text when a prompt overflows:
16
19
  // the system prompt goes first. Every MADRE call asks for a wider window.
17
20
  export const DEFAULT_NUM_CTX = 8192;
@@ -58,7 +61,7 @@ export async function probeOllama({ host = ollamaHost(), fetchImpl = globalThis.
58
61
  // What Ollama itself is running. It answers or it does not; either way the probe goes on.
59
62
  const version = await fetchImpl(`${host}/api/version`, { signal: AbortSignal.timeout(timeoutMs) })
60
63
  .then((answer) => (answer.ok ? answer.json() : null)).then((body) => body?.version ?? null).catch(() => null);
61
- const models = (payload.models ?? []).map((model) => ({ name: model.name, size: model.size ?? 0, family: model.details?.family ?? '', details: model.details ?? {} }));
64
+ const models = (payload.models ?? []).map((model) => ({ name: model.name, size: model.size ?? 0, digest: model.digest ?? null, family: model.details?.family ?? '', details: model.details ?? {} }));
62
65
  const embeds = models.filter(isEmbedModel);
63
66
  const chats = models.filter((model) => !isEmbedModel(model));
64
67
  return {
package/src/outbound.mjs CHANGED
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
1
3
  // What left this machine.
2
4
  //
3
5
  // MADRE runs on your computer and keeps everything it knows in a file you own, and that sentence
@@ -48,7 +50,7 @@ export const DESTINATIONS = [
48
50
  inside: false,
49
51
  },
50
52
  {
51
- id: 'embeddings', host: 'generativelanguage.googleapis.com', match: (url) => /batchEmbedContents/.test(url.pathname), to: 'Google · Gemini embeddings',
53
+ id: 'embeddings', host: 'generativelanguage.googleapis.com', match: (url) => /batchEmbedContents/.test(url.pathname), to: 'Google · Gemini search by meaning',
52
54
  what: 'The text of what is being embedded: the first 2000 characters of each memory, and the question a recall is made of.',
53
55
  when: 'a memory is written or recalled, while embeddings are set to Gemini',
54
56
  where: 'Settings › Memory › Smart search · Ollama keeps it on this computer, off drops back to words',
@@ -57,7 +59,7 @@ export const DESTINATIONS = [
57
59
  {
58
60
  id: 'image', host: 'generativelanguage.googleapis.com', match: (url) => /generateContent/.test(url.pathname), to: 'Google · Gemini image model',
59
61
  what: 'The prompt an agent wrote for an image.',
60
- when: 'an agent calls the image tool during a lease',
62
+ when: 'an agent uses the image tool during a permission to create',
61
63
  where: 'Modules › Images',
62
64
  // It runs in a process of its own and writes its own line into this same log, so it is here.
63
65
  inside: true,
@@ -65,7 +67,7 @@ export const DESTINATIONS = [
65
67
  {
66
68
  id: 'npm', host: 'registry.npmjs.org', to: 'the npm registry',
67
69
  what: 'The name of a package, to read the version of its latest release. Nothing about you or your project.',
68
- when: 'at most once a day per package, while the release channel is on',
70
+ when: 'at most once a day per package, while Look for new versions is on',
69
71
  where: 'Settings › Version and reports › Look for new versions · or PULSE_UPDATE_CHECK=0',
70
72
  inside: true,
71
73
  },
@@ -73,7 +75,7 @@ export const DESTINATIONS = [
73
75
  // Only the release read: the rest of api.github.com is the GitHub module's, and says so itself.
74
76
  id: 'github', host: 'api.github.com', match: (url) => /\/releases\/latest$/.test(url.pathname), to: 'GitHub',
75
77
  what: 'The name of a repository, to read the tag of its latest release. Nothing about you or your project.',
76
- when: 'at most once a day per repository, while the release channel is on',
78
+ when: 'at most once a day per repository, while Look for new versions is on',
77
79
  where: 'Settings › Version and reports › Look for new versions · or PULSE_UPDATE_CHECK=0',
78
80
  inside: true,
79
81
  },
@@ -85,6 +87,14 @@ export const DESTINATIONS = [
85
87
  where: 'Always on: it is how the author knows how many people use MADRE',
86
88
  inside: true,
87
89
  },
90
+ {
91
+ // The Market (src/market-remote.mjs): a signed catalog, and the file of a module you get from it.
92
+ id: 'market', host: 'madre.run', match: (url) => url.pathname.startsWith('/mercado/'), to: "madre.run · the Market",
93
+ what: 'Nothing about you: it asks for the catalog of modules, and for the file of a module you got or that has a newer version. Every file is checked against the author\'s signature before it is used.',
94
+ when: 'when MADRE opens, at most every ten minutes, and when you get a module',
95
+ where: 'Settings › Version and reports › Look for new versions · or PULSE_MARKET=0',
96
+ inside: true,
97
+ },
88
98
  {
89
99
  id: 'usage', host: 'madre.run', match: (url) => url.pathname === '/api/usage', to: "madre.run · the author's usage figures",
90
100
  what: "The author's token, to read how many copies are on. Only in the author's own copy, run from source.",
@@ -321,7 +331,7 @@ export class OutboundLog {
321
331
  export function outboundView({ log = null, state = {}, agents = [] } = {}) {
322
332
  const counts = log?.counts() ?? {};
323
333
  // Said in the room's language (2026-10-12): the list read in English inside a Spanish room.
324
- const vendors = { codex: 'OpenAI', claude: 'Anthropic', gemini: 'Google', opencode: t('the provider OpenCode is signed in to') };
334
+ const vendors = { codex: 'OpenAI', claude: 'Anthropic', gemini: 'Google', opencode: t('the provider OpenCode is signed in to'), cursor: 'Cursor' };
325
335
  const crew = agents.filter((agent) => agent.detected && !agent.local).map((agent) => `@${agent.id} → ${agent.route?.destination ?? vendors[agent.id] ?? t('its own provider')}`);
326
336
  const destinations = DESTINATIONS.map((one) => ({
327
337
  id: one.id,
@@ -344,7 +354,7 @@ export function outboundView({ log = null, state = {}, agents = [] } = {}) {
344
354
  recent: log?.recent({ limit: 40 }) ?? [],
345
355
  undeclared,
346
356
  says: undeclared
347
- ? (undeclared === 1 ? t('1 request went to an address nothing here declares. A module can do that; nothing can do it unlogged.') : t('{n} requests went to an address nothing here declares. A module can do that; nothing can do it unlogged.', { n: undeclared }))
357
+ ? (undeclared === 1 ? t('1 request went to an address not on this list. A module can do that, but it is always logged.') : t('{n} requests went to addresses not on this list. A module can do that, but it is always logged.', { n: undeclared }))
348
358
  : t('Every request this process made went to an address declared above.'),
349
359
  };
350
360
  }
package/src/override.mjs CHANGED
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
1
3
  import { createHash, timingSafeEqual } from 'node:crypto';
2
4
 
3
5
  // How #3 CONTROL and #4 AIRLOCK are unlocked, as the person chose it in AJUSTES › Permissions.
package/src/ports.mjs CHANGED
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
1
3
  // What is listening on this computer, so RIPLEY can look at it.
2
4
  //
3
5
  // A room that renders a project's files but cannot open the project's own dev server is half a
@@ -14,8 +16,10 @@
14
16
  const SYSTEM_PORTS = new Set([22, 25, 53, 88, 139, 445, 548, 631, 5353, 7000]);
15
17
  // Processes that always listen and never serve a page worth previewing.
16
18
  // Ollama is here because MADRE itself uses it (the local model, @madre): started from a project
17
- // folder it would pass for that project's server.
18
- const NOISE = /^(rapportd|sharingd|identityservicesd|controlce|remoted|launchd|mDNSResponder|cupsd|Spotify|Dropbox|Docker|com\.docker|ollama)/i;
19
+ // folder it would pass for that project's server. So is its runner, `llama-server` («llama-ser» in
20
+ // lsof's nine letters): each model it loads listens on a port of its own and closes it when the
21
+ // model is unloaded after a few idle minutes, which read as «your server stopped» (2026-10-17).
22
+ const NOISE = /^(rapportd|sharingd|identityservicesd|controlce|remoted|launchd|mDNSResponder|cupsd|Spotify|Dropbox|Docker|com\.docker|ollama|llama-ser)/i;
19
23
 
20
24
  // One `lsof -nP -iTCP -sTCP:LISTEN` table into the ports a human might want to look at.
21
25
  // Loopback only: a line bound to * or 0.0.0.0 is reachable from the network, and this room does
package/src/prices.mjs CHANGED
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
1
3
  // What a turn would cost at the provider's API list price, for the CLIs that do not say it
2
4
  // themselves. Claude Code reports its own figure (`total_cost_usd`); Codex and Gemini report only
3
5
  // tokens, and OpenCode reports 0 when the model runs on a subscription. So the room prices those
package/src/privacy.mjs CHANGED
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
1
3
  // Privacy: terms that must never travel through a room. ERROR-001 in a real room
2
4
  // showed the channel: an agent's own configuration (organisation instructions,
3
5
  // the account it runs under) leaks into its reply, the reply enters the shared
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
1
3
  export class QuotaMonitor {
2
4
  #sources;
3
5
  #onReport;
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
1
3
  import { execFile } from 'node:child_process';
2
4
  import { readdir, readFile, stat } from 'node:fs/promises';
3
5
  import { homedir } from 'node:os';
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
1
3
  // The room listens on 127.0.0.1, which keeps other computers out but not other web pages: any
2
4
  // site open in this browser can send a request to 127.0.0.1:4317, and an uploaded module is code
3
5
  // MADRE runs with your permissions. Two checks, before any route, close that door.
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
1
3
  // Asking for a star or a review (2026-10-10): three times at most, and never again once answered.
2
4
  //
3
5
  // After twenty minutes of MADRE in use, and on the second and the seventh day after it was first
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
1
3
  // The archivist: every so often the cheapest allowed agent reads what nobody
2
4
  // has distilled and keeps the few notes worth remembering. Scheduling by count
3
5
  // or by quiet, one batch per run, a bench for whoever fails, a report to the
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
1
3
  // Human uploads, stored under the room (never in the project). Registered
2
4
  // here so a message can reference them by id; the ledger lets a restarted
3
5
  // room rebuild the registry.
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
1
3
  // The local token budget: a rolling window per agent (five hours, like the
2
4
  // providers' short windows), raw totals for the record, and the seeding of the
3
5
  // usage sentinel from the ledger on start.