@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.
- package/ADDITIONAL-TERMS.md +93 -0
- package/CHANGELOG.en.md +2013 -0
- package/CHANGELOG.md +102 -1
- package/CONTRIBUTING.md +1 -1
- package/LICENSE +661 -202
- package/NOTICE +26 -14
- package/README.en.md +241 -0
- package/README.md +21 -12
- package/TRADEMARKS.en.md +50 -0
- package/TRADEMARKS.md +33 -16
- package/bin/madre.cjs +40 -0
- package/bin/madre.mjs +7 -0
- package/docs/INTERNALS.md +6 -0
- package/docs/REFERENCE.md +40 -5
- package/docs/SDK.md +4 -2
- package/docs/en/INTERNALS.md +103 -0
- package/docs/en/REFERENCE.md +365 -0
- package/docs/en/SDK.md +308 -0
- package/docs/en/training/README.md +59 -0
- package/docs/training/README.md +48 -9
- package/docs/training/train.sh +7 -2
- package/package.json +13 -5
- package/public/abduction.js +2 -0
- package/public/app.js +3154 -539
- package/public/attribution.js +114 -0
- package/public/brands.js +23 -6
- package/public/code-tools.js +143 -0
- package/public/editor.js +22 -4
- package/public/es.js +1004 -416
- package/public/hardware.js +50 -0
- package/public/i18n.js +25 -1
- package/public/index.html +18 -11
- package/public/inquiry.js +2 -0
- package/public/loader.js +2 -0
- package/public/macroverso.js +189 -96
- package/public/market-art.js +161 -0
- package/public/market-view.js +529 -0
- package/public/market.js +189 -0
- package/public/neural.js +110 -0
- package/public/nostromo-engine.js +414 -47
- package/public/power.js +100 -0
- package/public/resay.js +11 -0
- package/public/review-ask.js +2 -0
- package/public/routing.js +23 -1
- package/public/salas-mac.css +119 -18
- package/public/salas-mac.html +14 -2
- package/public/salas-mac.js +358 -56
- package/public/salas.css +2 -0
- package/public/salas.html +1 -0
- package/public/salas.js +4 -2
- package/public/styles.css +1793 -77
- package/public/syntax.js +2 -0
- package/public/touch.js +2 -0
- package/public/tour-chapters.js +2 -0
- package/public/tour.js +84 -5
- package/public/troubleshooting.js +24 -17
- package/public/wallpaper.js +275 -0
- package/src/adapters/claude.mjs +2 -0
- package/src/adapters/codex.mjs +2 -0
- package/src/adapters/cursor.mjs +96 -0
- package/src/adapters/gemini.mjs +13 -4
- package/src/adapters/madre.mjs +5 -2
- package/src/adapters/opencode.mjs +7 -3
- package/src/adapters/process.mjs +2 -0
- package/src/asking.mjs +2 -0
- package/src/auth-probe.mjs +145 -12
- package/src/bridge/devices.mjs +2 -0
- package/src/bridge/handoff.mjs +2 -0
- package/src/bridge/host.mjs +2 -0
- package/src/bridge/network.mjs +2 -0
- package/src/bridge/policy.mjs +3 -1
- package/src/bridge/qr.mjs +2 -0
- package/src/bridge/server.mjs +2 -0
- package/src/capabilities.mjs +11 -0
- package/src/chats.mjs +2 -0
- package/src/checkpoint.mjs +18 -1
- package/src/choices.mjs +2 -0
- package/src/cli-args.mjs +2 -0
- package/src/cold.mjs +2 -0
- package/src/commands.mjs +2 -0
- package/src/config.mjs +2 -0
- package/src/conversation-context.mjs +15 -1
- package/src/credentials.mjs +35 -3
- package/src/dataset.mjs +78 -4
- package/src/directives.mjs +2 -0
- package/src/distiller.mjs +2 -0
- package/src/embeddings.mjs +2 -0
- package/src/enlaces.mjs +214 -6
- package/src/event-store.mjs +2 -0
- package/src/exam.mjs +6 -4
- package/src/extensions.mjs +13 -3
- package/src/eyecat-watch.mjs +2 -0
- package/src/eyecat.mjs +2 -0
- package/src/file-ops.mjs +2 -0
- package/src/files.mjs +32 -0
- package/src/github.mjs +2 -0
- package/src/heartbeat.mjs +2 -0
- package/src/i18n.mjs +31 -1
- package/src/image-studio.mjs +2 -0
- package/src/json-file.mjs +2 -0
- package/src/launch.mjs +2 -0
- package/src/lease.mjs +5 -1
- package/src/lobby-work.mjs +2 -0
- package/src/local-ai.mjs +2 -0
- package/src/looks.mjs +127 -2
- package/src/market-key.mjs +8 -0
- package/src/market-remote.mjs +239 -0
- package/src/maturity.mjs +3 -1
- package/src/mcp/connect.mjs +345 -0
- package/src/mcp/github-server.mjs +2 -0
- package/src/mcp/image-server.mjs +2 -0
- package/src/mcp/memory-server.mjs +2 -0
- package/src/mcp/proxy.mjs +153 -0
- package/src/mcp/smtp-server.mjs +2 -0
- package/src/memory-dashboard.mjs +2 -0
- package/src/memory-tools.mjs +2 -0
- package/src/memory.mjs +2 -0
- package/src/models.mjs +49 -3
- package/src/module-scope.mjs +68 -0
- package/src/modules/ahp.mjs +104 -7
- package/src/modules/ash-policy.mjs +2 -0
- package/src/modules/ash.mjs +3 -1
- package/src/modules/bishop-exam.mjs +122 -0
- package/src/modules/bishop-plan.mjs +5 -3
- package/src/modules/bishop.mjs +84 -23
- package/src/modules/choices.mjs +2 -0
- package/src/modules/correo.mjs +2 -0
- package/src/modules/enlaces.mjs +89 -6
- package/src/modules/git-pulse.mjs +2 -0
- package/src/modules/github.mjs +2 -0
- package/src/modules/helpers.mjs +2 -0
- package/src/modules/image-studio.mjs +13 -2
- package/src/modules/index.mjs +4 -1
- package/src/modules/intents-calibration.mjs +2 -0
- package/src/modules/intents-desk.mjs +4 -2
- package/src/modules/intents-observe.mjs +2 -0
- package/src/modules/intents-policy.mjs +4 -1
- package/src/modules/intents-try.mjs +2 -0
- package/src/modules/intents.mjs +11 -7
- package/src/modules/local-models.mjs +124 -0
- package/src/modules/mcp-connect.mjs +293 -0
- package/src/modules/ollama.mjs +15 -4
- package/src/modules/playwright.mjs +88 -15
- package/src/modules/ripley.mjs +2 -0
- package/src/modules/runs.mjs +5 -3
- package/src/modules/sdk.mjs +63 -9
- package/src/modules/updates.mjs +2 -0
- package/src/mother.mjs +2 -0
- package/src/npm-launch.mjs +18 -0
- package/src/ollama.mjs +8 -5
- package/src/outbound.mjs +16 -6
- package/src/override.mjs +2 -0
- package/src/ports.mjs +6 -2
- package/src/prices.mjs +2 -0
- package/src/privacy.mjs +2 -0
- package/src/quota-monitor.mjs +2 -0
- package/src/quota-sources.mjs +2 -0
- package/src/request-guard.mjs +2 -0
- package/src/review-ask.mjs +2 -0
- package/src/room/archivist.mjs +2 -0
- package/src/room/attachments.mjs +2 -0
- package/src/room/budget.mjs +2 -0
- package/src/room/context.mjs +2 -0
- package/src/room/control.mjs +7 -1
- package/src/room/copilot.mjs +76 -0
- package/src/room/economy.mjs +2 -0
- package/src/room/escalation.mjs +2 -0
- package/src/room/ghost.mjs +2 -0
- package/src/room/guard.mjs +2 -0
- package/src/room/prompt.mjs +25 -5
- package/src/room/returns.mjs +82 -0
- package/src/room/runs.mjs +4 -0
- package/src/room/vectors.mjs +2 -0
- package/src/room.mjs +223 -35
- package/src/router.mjs +3 -1
- package/src/runs.mjs +6 -1
- package/src/runtime-detection.mjs +11 -0
- package/src/salas.mjs +28 -0
- package/src/sentinel-errors.mjs +2 -0
- package/src/server-sentinel.mjs +2 -0
- package/src/server.mjs +757 -81
- package/src/setup.mjs +3 -1
- package/src/smtp.mjs +2 -0
- package/src/tour.mjs +2 -0
- package/src/updates.mjs +2 -0
- package/src/usage-sentinel.mjs +2 -0
- package/src/vault.mjs +2 -0
- package/src/verdict.mjs +3 -1
- 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:
|
|
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
|
-
|
|
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
|
|
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: [
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
});
|
package/src/modules/ripley.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
|
// 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
|
|
package/src/modules/runs.mjs
CHANGED
|
@@ -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
|
|
6
|
-
// agent
|
|
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
|
|
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 {
|
package/src/modules/sdk.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
|
// 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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/modules/updates.mjs
CHANGED
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
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
package/src/quota-monitor.mjs
CHANGED
package/src/quota-sources.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 { execFile } from 'node:child_process';
|
|
2
4
|
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
3
5
|
import { homedir } from 'node:os';
|
package/src/request-guard.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
|
// 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.
|
package/src/review-ask.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
|
// 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
|
package/src/room/archivist.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
|
// 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
|
package/src/room/attachments.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
|
// 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.
|
package/src/room/budget.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
|
// 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.
|