@jossuealcala/madre 1.0.2 → 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/CHANGELOG.en.md +69 -1
- package/CHANGELOG.md +69 -1
- package/README.en.md +11 -15
- package/README.md +3 -3
- package/bin/madre.cjs +40 -0
- package/bin/madre.mjs +5 -0
- package/docs/REFERENCE.md +29 -3
- package/docs/SDK.md +4 -2
- package/docs/en/REFERENCE.md +29 -3
- package/docs/en/SDK.md +4 -2
- package/docs/training/README.md +28 -1
- package/docs/training/train.sh +5 -1
- package/package.json +3 -3
- package/public/app.js +1626 -151
- package/public/attribution.js +44 -1
- package/public/brands.js +20 -5
- package/public/code-tools.js +143 -0
- package/public/editor.js +20 -4
- package/public/es.js +393 -25
- package/public/hardware.js +50 -0
- package/public/index.html +5 -1
- package/public/macroverso.js +187 -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 +412 -47
- package/public/power.js +100 -0
- package/public/resay.js +5 -0
- package/public/salas-mac.css +98 -14
- package/public/salas-mac.html +9 -1
- package/public/salas-mac.js +307 -30
- package/public/styles.css +838 -61
- package/public/tour.js +48 -6
- package/public/troubleshooting.js +6 -1
- package/public/wallpaper.js +275 -0
- package/src/adapters/cursor.mjs +96 -0
- package/src/adapters/gemini.mjs +10 -3
- package/src/adapters/madre.mjs +2 -1
- package/src/auth-probe.mjs +143 -12
- package/src/bridge/policy.mjs +1 -1
- package/src/capabilities.mjs +9 -0
- package/src/checkpoint.mjs +16 -1
- package/src/credentials.mjs +3 -0
- package/src/dataset.mjs +76 -4
- package/src/enlaces.mjs +208 -2
- package/src/extensions.mjs +11 -3
- package/src/i18n.mjs +16 -0
- package/src/looks.mjs +125 -2
- package/src/market-key.mjs +8 -0
- package/src/market-remote.mjs +239 -0
- package/src/models.mjs +4 -0
- package/src/module-scope.mjs +68 -0
- package/src/modules/ahp.mjs +102 -7
- package/src/modules/bishop-exam.mjs +122 -0
- package/src/modules/bishop.mjs +71 -12
- package/src/modules/enlaces.mjs +87 -6
- package/src/modules/image-studio.mjs +10 -1
- package/src/modules/intents.mjs +3 -1
- package/src/modules/local-models.mjs +124 -0
- package/src/modules/ollama.mjs +12 -3
- package/src/modules/playwright.mjs +73 -15
- package/src/modules/sdk.mjs +61 -9
- package/src/npm-launch.mjs +18 -0
- package/src/ollama.mjs +6 -5
- package/src/outbound.mjs +9 -1
- package/src/ports.mjs +4 -2
- package/src/room/control.mjs +1 -1
- package/src/room/prompt.mjs +13 -2
- package/src/room.mjs +14 -4
- package/src/router.mjs +1 -1
- package/src/runs.mjs +4 -1
- package/src/runtime-detection.mjs +9 -0
- package/src/salas.mjs +26 -0
- package/src/server.mjs +617 -48
- package/src/setup.mjs +1 -1
package/public/market.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
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
|
+
// The Market (2026-10-18): every module MADRE can turn on or install, shown as a shop in Home and
|
|
4
|
+
// in each project (market-view.js), and offered in the conversation the moment one would help
|
|
5
|
+
// (app.js, offerModule). What a module looks like — its line and its drawing — lives here once, for the
|
|
6
|
+
// project's board of MODULES and for the Market alike. Everything is free for now; `price` is where a
|
|
7
|
+
// paid module will say what it costs.
|
|
8
|
+
//
|
|
9
|
+
// What is written here is the Market MADRE ships with. The catalog the author publishes at
|
|
10
|
+
// madre.run/mercado/ (src/market-remote.mjs, signed) is read over it when there is one: its shelves,
|
|
11
|
+
// its featured order, its words, and the modules that came after this version of MADRE.
|
|
12
|
+
|
|
13
|
+
import { language, t } from './i18n.js';
|
|
14
|
+
|
|
15
|
+
// `icon` is drawn on a 20-unit grid in `currentColor`, with `ic-f` marking the part a tile tints;
|
|
16
|
+
// `hue` is the two colors of the module's own app icon, top to bottom (2026-10-18, the Market).
|
|
17
|
+
export const MODULE_FACES = {
|
|
18
|
+
ash: { line: 'Compact replies from every agent', wide: true, window: true, hue: ['#38bdf8', '#0369a1'], icon: '<path d="M4 5.2h12M4 9.4h8.4M4 13.6h4.6" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/><path class="ic-f" d="M12.6 12.2l2.6 3 2.6-3" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/><path d="M15.2 8v7" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/>' },
|
|
19
|
+
intents: { line: 'How hard each agent should think', wide: true, window: true, hue: ['#a78bfa', '#6d28d9'], icon: '<path d="M3.2 14.2a6.8 6.8 0 0 1 13.6 0" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/><path class="ic-f" d="M3.2 14.2a6.8 6.8 0 0 1 4.4-6.4L10 14.2z" fill="currentColor" fill-opacity=".28" stroke="none"/><path d="M10 14.2l3.4-4.8" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/><circle cx="10" cy="14.2" r="1.5" fill="currentColor"/>' },
|
|
20
|
+
ollama: { line: 'Memory by meaning, on this machine', wide: true, window: true, hue: ['#4ade80', '#15803d'], icon: '<rect x="5" y="5" width="10" height="10" rx="2.6" fill="none" stroke="currentColor" stroke-width="1.7"/><path d="M8 2.6v2.4M12 2.6v2.4M8 15v2.4M12 15v2.4M2.6 8h2.4M2.6 12h2.4M15 8h2.4M15 12h2.4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle class="ic-f" cx="10" cy="10" r="2.1" fill="currentColor" fill-opacity=".28" stroke="currentColor" stroke-width="1.4"/>' },
|
|
21
|
+
'image-studio': { line: 'Images for the agents, with your key', hue: ['#fb7185', '#c026d3'], icon: '<rect x="2.8" y="3.8" width="14.4" height="12.4" rx="3" fill="none" stroke="currentColor" stroke-width="1.7"/><path class="ic-f" d="M3.4 14.6l4-4 2.8 2.6 2.1-1.8 4.3 3.4z" fill="currentColor" fill-opacity=".28" stroke="none"/><path d="M3.4 14.6l4-4 2.8 2.6 2.1-1.8 4.3 3.4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round"/><path d="M13.4 5.6l.55 1.25 1.25.55-1.25.55-.55 1.25-.55-1.25-1.25-.55 1.25-.55z" fill="currentColor"/>' },
|
|
22
|
+
playwright: { line: 'A browser the agents can look through', hue: ['#60a5fa', '#1d4ed8'], icon: '<rect x="2.6" y="3.6" width="14.8" height="12.8" rx="3" fill="none" stroke="currentColor" stroke-width="1.7"/><path d="M2.6 7.4h14.8" fill="none" stroke="currentColor" stroke-width="1.5"/><circle cx="5.1" cy="5.5" r=".8" fill="currentColor"/><circle cx="7.4" cy="5.5" r=".8" fill="currentColor"/><path class="ic-f" d="M9.2 9.4l5.4 2-2.3.9-.9 2.3z" fill="currentColor" fill-opacity=".28" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/>' },
|
|
23
|
+
ripley: { line: 'Preview of HTML, SVG and Markdown', hue: ['#fbbf24', '#ea580c'], icon: '<rect x="2.6" y="3.6" width="14.8" height="12.8" rx="3" fill="none" stroke="currentColor" stroke-width="1.7"/><path class="ic-f" d="M5.4 10s1.8-3 4.6-3 4.6 3 4.6 3-1.8 3-4.6 3-4.6-3-4.6-3z" fill="currentColor" fill-opacity=".28" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><circle cx="10" cy="10" r="1.3" fill="currentColor"/>' },
|
|
24
|
+
'git-pulse': { line: 'The repository, posted to the project', hue: ['#94a3b8', '#475569'], icon: '<circle cx="5.5" cy="5" r="1.8" fill="none" stroke="currentColor" stroke-width="1.6"/><circle cx="5.5" cy="15" r="1.8" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M5.5 6.8v6.4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/><path class="ic-f" d="M8.8 11h2l1.2-3.4 1.6 6 1.2-2.6h1.7" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/>' },
|
|
25
|
+
runs: { line: 'Commands an agent asks you to run', hue: ['#6b7280', '#1f2937'], icon: '<rect x="2.6" y="3.6" width="14.8" height="12.8" rx="3" fill="none" stroke="currentColor" stroke-width="1.7"/><path class="ic-f" d="M6.2 7.8l2.6 2.2-2.6 2.2" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/><path d="M10.6 12.4h3.4" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/>' },
|
|
26
|
+
choices: { line: 'Decisions as buttons, not paragraphs', hue: ['#2dd4bf', '#0f766e'], icon: '<rect x="3.2" y="3.4" width="13.6" height="3.6" rx="1.8" fill="none" stroke="currentColor" stroke-width="1.5"/><rect class="ic-f" x="3.2" y="8.2" width="13.6" height="3.6" rx="1.8" fill="currentColor" fill-opacity=".28" stroke="currentColor" stroke-width="1.5"/><path d="M12.2 10l1 1 1.9-1.9" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/><rect x="3.2" y="13" width="13.6" height="3.6" rx="1.8" fill="none" stroke="currentColor" stroke-width="1.5"/>' },
|
|
27
|
+
ahp: { line: 'Project state and handoffs in .ahp/', hue: ['#34d399', '#047857'], icon: '<path d="M4.4 8.4a5.9 5.9 0 0 1 10.4-2.7" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/><path d="M15.3 2.9v3.2h-3.2" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/><path d="M15.6 11.6a5.9 5.9 0 0 1-10.4 2.7" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/><path d="M4.7 17.1v-3.2h3.2" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/><circle class="ic-f" cx="10" cy="10" r="1.9" fill="currentColor" fill-opacity=".28" stroke="currentColor" stroke-width="1.4"/>' },
|
|
28
|
+
correo: { line: 'Send one email from your account', hue: ['#fb923c', '#c2410c'], icon: '<rect x="2.6" y="4.6" width="14.8" height="10.8" rx="2.6" fill="none" stroke="currentColor" stroke-width="1.7"/><path class="ic-f" d="M3.4 6l6.6 4.8L16.6 6" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round"/>' },
|
|
29
|
+
github: { line: 'Pull requests, issues and CI, in the project', hue: ['#64748b', '#1e293b'], icon: '<circle cx="5.5" cy="4.8" r="1.9" fill="none" stroke="currentColor" stroke-width="1.6"/><circle cx="5.5" cy="15.2" r="1.9" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M5.5 6.7v6.6" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/><path d="M14.5 13.3V8.2a2.6 2.6 0 0 0-2.6-2.6H9.2" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/><path d="M11 3.6l-2 2 2 2" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/><circle class="ic-f" cx="14.5" cy="15.2" r="1.9" fill="currentColor" fill-opacity=".28" stroke="currentColor" stroke-width="1.6"/>' },
|
|
30
|
+
// Faces for the modules MODULES lists without one (2026-10-18, for the Market): their words and drawings.
|
|
31
|
+
enlaces: { line: 'What other projects know, through a signed connection', hue: ['#f472b6', '#be123c'], icon: '<path d="M8.1 11.9l3.8-3.8" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/><path class="ic-f" d="M9 5.5l1.6-1.6a3.3 3.3 0 0 1 4.7 4.7L13.7 10.2" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/><path d="M11 14.5l-1.6 1.6a3.3 3.3 0 0 1-4.7-4.7l1.6-1.6" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/>' },
|
|
32
|
+
mcp: { line: 'Other apps and services, for the agents', hue: ['#e879f9', '#7e22ce'], icon: '<path d="M7.6 2.8v3.4M12.4 2.8v3.4" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/><path class="ic-f" d="M5.2 6.2h9.6v3.2a4.8 4.8 0 0 1-9.6 0z" fill="currentColor" fill-opacity=".28" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/><path d="M10 14.2v3" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/>' },
|
|
33
|
+
};
|
|
34
|
+
export const MODULE_ANY = '<path d="M7.5 4.5h2.2a1.6 1.6 0 1 1 3.1 0H15v3.2a1.6 1.6 0 1 1 0 3.1V15h-3.2a1.6 1.6 0 1 0-3.1 0H5.5v-3.6a1.6 1.6 0 1 0 0-3.1V4.5z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>';
|
|
35
|
+
const HUE_ANY = ['#94a3b8', '#334155'];
|
|
36
|
+
|
|
37
|
+
export const moduleFace = (item) => {
|
|
38
|
+
if (MODULE_FACES[item?.id]) return MODULE_FACES[item.id];
|
|
39
|
+
// A module that came after this MADRE wears the face its catalog entry gives it.
|
|
40
|
+
const entry = remoteEntry(item?.id);
|
|
41
|
+
if (entry) return { line: pick(entry.line), icon: cleanSvg(entry.icon, { inner: true }) ?? MODULE_ANY, hue: entry.hue ?? HUE_ANY, name: pick(entry.name), summary: pick(entry.summary) };
|
|
42
|
+
return { line: null, icon: MODULE_ANY, hue: HUE_ANY };
|
|
43
|
+
};
|
|
44
|
+
// A module's app icon: its drawing in white on its own two colors (the Market, Home's MODULES).
|
|
45
|
+
export function appIcon(item, size = 56) {
|
|
46
|
+
const face = moduleFace(item);
|
|
47
|
+
const [top, bottom] = face.hue ?? HUE_ANY;
|
|
48
|
+
const mark = document.createElement('span');
|
|
49
|
+
mark.className = 'mk-icon';
|
|
50
|
+
mark.style.setProperty('--mk-size', `${size}px`);
|
|
51
|
+
mark.style.setProperty('--mk-a', top);
|
|
52
|
+
mark.style.setProperty('--mk-b', bottom);
|
|
53
|
+
mark.innerHTML = `<svg viewBox="0 0 20 20" aria-hidden="true">${face.icon}</svg>`; // trusted: our own SVG strings
|
|
54
|
+
return mark;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// The shelves of the Market, in the order they are shown. Each module sits on one.
|
|
58
|
+
export const MARKET_SHELVES = [
|
|
59
|
+
{ id: 'no-terminal', title: 'Without the terminal', about: 'What used to take a command, in one press.' },
|
|
60
|
+
{ id: 'create', title: 'To create', about: 'Images, previews and decisions as buttons.' },
|
|
61
|
+
{ id: 'local-ai', title: 'AI on your computer', about: 'Free, private, and lighter on your plans.' },
|
|
62
|
+
{ id: 'connect', title: 'Connect it', about: 'Your other projects and the apps you already use.' },
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
// Where each module sits, whether it is turned on for every project (`everywhere`) or installed into
|
|
66
|
+
// one (`project`), and what it costs (0: free). A module MADRE has no entry for still shows, on the
|
|
67
|
+
// last shelf.
|
|
68
|
+
export const MARKET = {
|
|
69
|
+
playwright: { shelf: 'no-terminal', scope: 'everywhere', price: 0, featured: 1, pitch: 'Let your agents see your page: they open it, click through it and bring back screenshots.' },
|
|
70
|
+
ahp: { shelf: 'no-terminal', scope: 'project', price: 0, featured: 2, pitch: 'Hand work from one agent to another without losing the thread: the project keeps its state beside the code.' },
|
|
71
|
+
runs: { shelf: 'no-terminal', scope: 'everywhere', price: 0 },
|
|
72
|
+
github: { shelf: 'no-terminal', scope: 'everywhere', price: 0 },
|
|
73
|
+
'image-studio': { shelf: 'create', scope: 'everywhere', price: 0, featured: 3, pitch: 'Ask for a logo, a banner or an illustration, and the agents make it with your own key.' },
|
|
74
|
+
ripley: { shelf: 'create', scope: 'everywhere', price: 0 },
|
|
75
|
+
choices: { shelf: 'create', scope: 'everywhere', price: 0 },
|
|
76
|
+
ollama: { shelf: 'local-ai', scope: 'everywhere', price: 0, featured: 4, pitch: 'A memory that searches by meaning, and @madre, on your own computer: free and private.' },
|
|
77
|
+
intents: { shelf: 'local-ai', scope: 'everywhere', price: 0 },
|
|
78
|
+
ash: { shelf: 'local-ai', scope: 'everywhere', price: 0 },
|
|
79
|
+
enlaces: { shelf: 'connect', scope: 'everywhere', price: 0 },
|
|
80
|
+
mcp: { shelf: 'connect', scope: 'everywhere', price: 0 },
|
|
81
|
+
};
|
|
82
|
+
export const marketEntry = (id) => {
|
|
83
|
+
const base = MARKET[id] ?? { shelf: 'connect', scope: 'everywhere', price: 0 };
|
|
84
|
+
const entry = remoteEntry(id);
|
|
85
|
+
if (!entry && !remote) return base;
|
|
86
|
+
const featured = remote?.featured?.length ? remote.featured.indexOf(id) + 1 : base.featured;
|
|
87
|
+
return { ...base, ...(entry ? { shelf: entry.shelf ?? base.shelf, scope: entry.scope ?? base.scope, price: entry.price ?? base.price, pitch: pick(entry.pitch) ?? base.pitch, minMadre: entry.minMadre ?? null } : {}), featured: featured || 0 };
|
|
88
|
+
};
|
|
89
|
+
// The shelves, the catalog's when it names them.
|
|
90
|
+
export const marketShelves = () => (remote?.shelves?.length ? remote.shelves.map((shelf) => ({ id: shelf.id, title: pick(shelf.title) ?? shelf.id, about: pick(shelf.about) ?? '' })) : MARKET_SHELVES);
|
|
91
|
+
// What the shop shows: every module that can be turned on or installed. One that is always there
|
|
92
|
+
// (Git) is part of MADRE, not something to get, and so is the Coordinator, which is Home itself.
|
|
93
|
+
export const inMarket = (item) => Boolean(item) && !item.fixed && item.kind !== undefined && item.id !== 'bishop';
|
|
94
|
+
// The ones the carousel shows, in their order.
|
|
95
|
+
export const marketFeatured = (items = []) => items.filter((item) => marketEntry(item.id).featured).sort((a, b) => marketEntry(a.id).featured - marketEntry(b.id).featured);
|
|
96
|
+
|
|
97
|
+
// THE OFFER IN THE CONVERSATION. A message that asks for what a module does, while that module is
|
|
98
|
+
// off, gets one card under it: what would help and the key to turn it on. Read from the person's
|
|
99
|
+
// own words only; one offer per message; never one that was declined, and never the same one twice
|
|
100
|
+
// in a page.
|
|
101
|
+
export const MARKET_OFFERS = [
|
|
102
|
+
{ id: 'playwright', when: /navegador|browser|playwright|captura|screenshot|pantallazo|\bclic\b|\bclick\b|consola|console|responsive|m[oó]vil|mobile|e2e|end.to.end|pru[eé]ba(lo|la|las|los)? en|abre (la|el) (p[aá]gina|app|sitio)|open the (page|app|site)/i, says: 'To open your page in a browser, click through it and take screenshots, the agents need Test browser.' },
|
|
103
|
+
{ id: 'ripley', when: /vista previa|\bpreview\b|c[oó]mo se ve|ver (la|mi) p[aá]gina|see (the|my) page/i, says: 'To see your pages as they look, beside the conversation, turn on Preview.' },
|
|
104
|
+
{ id: 'image-studio', when: /\b(genera|gener[aá]|crea|haz|dibuja|make|generate|draw)\w*\s+(me\s+)?(una?\s+|an?\s+)?(imagen|ilustraci[oó]n|logo|foto|banner|icono|image|illustration|picture|icon)/i, says: 'To make images, the agents need Images.' },
|
|
105
|
+
{ id: 'github', when: /\bgithub\b|pull request|\bPRs?\b|\bissues?\b|\bCI\b/i, says: 'To read your pull requests, issues and CI, the agents need GitHub.' },
|
|
106
|
+
{ id: 'ahp', when: /traspaso|handoff|continuidad|continuity|retom(a|ar) donde|pick up where|estado del proyecto|project state/i, says: 'To keep the project\'s state and hand work between agents and editors, install Continuity.' },
|
|
107
|
+
{ id: 'ollama', when: /@madre\b|sin internet|offline|ia local|local ai/i, says: 'For @madre and a memory that searches by meaning, turn on Local AI.' },
|
|
108
|
+
{ id: 'ash', when: /gast(ar|e|o) menos|ahorr(a|ar|o) tokens|demasiados tokens|too many tokens|save tokens/i, says: 'To have every agent answer shorter and spend less, turn on Token saver.' },
|
|
109
|
+
];
|
|
110
|
+
export function marketOffer(text, items = [], { declined = [], shown = [] } = {}) {
|
|
111
|
+
const words = String(text ?? '');
|
|
112
|
+
if (!words.trim()) return null;
|
|
113
|
+
for (const offer of MARKET_OFFERS) {
|
|
114
|
+
if (declined.includes(offer.id) || shown.includes(offer.id)) continue;
|
|
115
|
+
const item = items.find((one) => one.id === offer.id);
|
|
116
|
+
if (!item || item.status?.installed || !inMarket(item)) continue;
|
|
117
|
+
if (offer.when.test(words)) return { item, says: offer.says };
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// THE CATALOG from madre.run (src/market-remote.mjs). The page is handed it by /api/market, already
|
|
123
|
+
// checked against the author's signature; its words come in both languages, and its drawings are
|
|
124
|
+
// kept to the shapes a drawing needs before they reach the page (cleanSvg), signed or not.
|
|
125
|
+
let remote = null;
|
|
126
|
+
const pick = (value) => (value == null ? null : typeof value === 'string' ? value : value[language()] ?? value.en ?? value.es ?? null);
|
|
127
|
+
export function setMarketCatalog(catalog) {
|
|
128
|
+
remote = catalog && Array.isArray(catalog.modules) ? { ...catalog, byId: new Map(catalog.modules.map((entry) => [entry.id, entry])) } : null;
|
|
129
|
+
}
|
|
130
|
+
export const remoteEntry = (id) => remote?.byId.get(id) ?? null;
|
|
131
|
+
// The catalog's modules this MADRE does not have yet, as the Market lists them: to get.
|
|
132
|
+
export function marketExtras(items = []) {
|
|
133
|
+
if (!remote) return [];
|
|
134
|
+
return remote.modules.filter((entry) => entry.file && !items.some((item) => item.id === entry.id)).map((entry) => ({
|
|
135
|
+
id: entry.id, kind: 'remote', remote: entry, name: pick(entry.name) ?? entry.id, summary: pick(entry.summary) ?? '', version: entry.version,
|
|
136
|
+
creates: [], requires: [], status: { installed: false }, preflight: { ok: true, problems: [] },
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
// A drawing from the catalog: only the elements and attributes a drawing is made of. No script,
|
|
140
|
+
// no link, no style, no outside address.
|
|
141
|
+
const SVG_TAGS = new Set(['svg', 'g', 'path', 'rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon', 'defs', 'lineargradient', 'radialgradient', 'stop', 'clippath', 'filter', 'fedropshadow', 'fegaussianblur', 'text', 'tspan']);
|
|
142
|
+
const SVG_ATTRS = /^(d|x|y|x1|x2|y1|y2|cx|cy|r|fx|fy|rx|ry|dx|dy|width|height|points|viewbox|fill|fill-opacity|fill-rule|clip-rule|stroke|stroke-width|stroke-linecap|stroke-linejoin|stroke-opacity|stroke-dasharray|opacity|transform|offset|stop-color|stop-opacity|id|class|clip-path|filter|gradientunits|gradienttransform|stddeviation|flood-color|flood-opacity|font-family|font-size|font-weight|text-anchor|xmlns|aria-hidden|focusable|preserveaspectratio)$/;
|
|
143
|
+
export function cleanSvg(markup, { inner = false } = {}) {
|
|
144
|
+
if (typeof markup !== 'string' || !markup.trim() || typeof DOMParser === 'undefined') return null;
|
|
145
|
+
const doc = new DOMParser().parseFromString(inner ? `<svg xmlns="http://www.w3.org/2000/svg">${markup}</svg>` : markup, 'image/svg+xml');
|
|
146
|
+
const root = doc.documentElement;
|
|
147
|
+
if (root?.nodeName?.toLowerCase() !== 'svg' || doc.getElementsByTagName('parsererror').length) return null;
|
|
148
|
+
const clean = (node) => {
|
|
149
|
+
for (const attr of [...node.attributes]) {
|
|
150
|
+
if (!SVG_ATTRS.test(attr.name.toLowerCase()) || /url\(\s*(?!#)/i.test(attr.value) || /javascript:|data:/i.test(attr.value)) node.removeAttribute(attr.name);
|
|
151
|
+
}
|
|
152
|
+
for (const child of [...node.children]) {
|
|
153
|
+
if (!SVG_TAGS.has(child.nodeName.toLowerCase())) child.remove();
|
|
154
|
+
else clean(child);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
clean(root);
|
|
158
|
+
return inner ? root.innerHTML : root.outerHTML;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Asked once when a page opens: the catalog for the faces and the Market, and two kinds of news,
|
|
162
|
+
// each said once, as the page's own notice. What the Market updated on its own; and what is new in
|
|
163
|
+
// it — the author's lines and the modules that arrived — with the key that opens it there.
|
|
164
|
+
export async function loadMarket({ toast = null, open = null } = {}) {
|
|
165
|
+
const answer = await fetch('/api/market', { cache: 'no-store' }).then((response) => (response.ok ? response.json() : null)).catch(() => null);
|
|
166
|
+
if (!answer) return null;
|
|
167
|
+
setMarketCatalog(answer.catalog);
|
|
168
|
+
if (!toast) return answer;
|
|
169
|
+
const updated = answer.news ?? [];
|
|
170
|
+
if (updated.length) {
|
|
171
|
+
toast(updated.map((one) => [t('{name} updated itself to version {version}.', { name: t(one.name ?? one.id), version: one.to }), pick(one.changes)].filter(Boolean).join(' ')).join(' '));
|
|
172
|
+
void fetch('/api/market/seen', { method: 'POST' }).catch(() => null);
|
|
173
|
+
}
|
|
174
|
+
const fresh = answer.announce ?? { news: [], modules: [] };
|
|
175
|
+
const arrived = fresh.modules ?? [];
|
|
176
|
+
const lines = [
|
|
177
|
+
...(fresh.news ?? []).map((one) => pick(one.text)),
|
|
178
|
+
...(arrived.length === 1 ? [t('New in the Market: {name}.', { name: pick(arrived[0].name) ?? arrived[0].id })] : arrived.length > 1 ? [t('{n} new modules in the Market.', { n: arrived.length })] : []),
|
|
179
|
+
].filter(Boolean);
|
|
180
|
+
if (lines.length) {
|
|
181
|
+
const about = (fresh.news ?? []).find((one) => one.module)?.module ?? (arrived.length === 1 ? arrived[0].id : null);
|
|
182
|
+
const tell = () => toast(lines.join(' '), open ? { label: t('See it'), run: () => open(about) } : null);
|
|
183
|
+
// After the update notice, never over it.
|
|
184
|
+
if (updated.length) setTimeout(tell, 6500); else tell();
|
|
185
|
+
const keys = [...(fresh.news ?? []).map((one) => `news:${one.id}`), ...arrived.map((one) => `module:${one.id}`)];
|
|
186
|
+
void fetch('/api/market/announced', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ keys }) }).catch(() => null);
|
|
187
|
+
}
|
|
188
|
+
return answer;
|
|
189
|
+
}
|
package/public/neural.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
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
|
+
// Neural mode (1.0.3): the Memory map and the Projects map drawn as the neurons of a brain
|
|
4
|
+
// (nostromo-engine.js, drawNeural; macroverso.js). What both maps share lives here: the key's
|
|
5
|
+
// drawing, the watch over whether this machine is giving the map the frames it needs, and the
|
|
6
|
+
// invitation to try the mode when it is not — an invitation to a way of seeing, never a warning:
|
|
7
|
+
// the person chooses when to use each.
|
|
8
|
+
import { t } from './i18n.js';
|
|
9
|
+
|
|
10
|
+
// A neuron: its body, four dendrites, a bead at the end of each. Open, the body fills.
|
|
11
|
+
export const NEURAL_ICON = '<svg viewBox="0 0 20 20" aria-hidden="true"><circle class="ic-f" cx="10" cy="10" r="2.7" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M10 7.3V4M12.4 11.3l3 2.4M7.6 11.3l-3 2.4M12.5 8.7l2.9-1.8" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="10" cy="3.4" r="1.15" fill="currentColor"/><circle cx="15.9" cy="14.1" r="1.15" fill="currentColor"/><circle cx="4.1" cy="14.1" r="1.15" fill="currentColor"/><circle cx="15.9" cy="6.5" r="1.15" fill="currentColor"/></svg>';
|
|
12
|
+
|
|
13
|
+
const KEY = 'pulse.neural';
|
|
14
|
+
export function neuralChosen() {
|
|
15
|
+
try { return localStorage.getItem(KEY) === 'on'; } catch { return false; }
|
|
16
|
+
}
|
|
17
|
+
export function rememberNeural(on) {
|
|
18
|
+
try { if (on) localStorage.setItem(KEY, 'on'); else localStorage.removeItem(KEY); } catch { /* this page only */ }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// The key in a map's head: its drawing and its word, pressed while the mode is on.
|
|
22
|
+
export function neuralKey(button) {
|
|
23
|
+
button.innerHTML = `${NEURAL_ICON}<span>${t('Neural')}</span>`; // trusted: our own SVG string and our own word
|
|
24
|
+
button.title = t('Neural mode · your memories as the neurons of a brain: only what travels lights up');
|
|
25
|
+
return button;
|
|
26
|
+
}
|
|
27
|
+
export function paintNeuralKey(button, on) {
|
|
28
|
+
button?.setAttribute('aria-pressed', String(Boolean(on)));
|
|
29
|
+
button?.classList.remove('invite');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Whether the map is getting its frames: the work one frame takes and the time between frames,
|
|
33
|
+
// read as a median over a couple of seconds once the map has settled. A page in the background is
|
|
34
|
+
// not counted. Told once; `reset` starts the watch again (the map was opened again).
|
|
35
|
+
export function createStrain({ onStrain, settle = 2500, sample = 120, work = 12, gap = 30 } = {}) {
|
|
36
|
+
let since = 0;
|
|
37
|
+
let last = 0;
|
|
38
|
+
let told = false;
|
|
39
|
+
const works = [];
|
|
40
|
+
const gaps = [];
|
|
41
|
+
const median = (list) => [...list].sort((a, b) => a - b)[Math.floor(list.length / 2)];
|
|
42
|
+
const watch = {
|
|
43
|
+
frame(spent, now) {
|
|
44
|
+
if (told || globalThis.document?.hidden) { last = 0; return; }
|
|
45
|
+
if (!since) since = now;
|
|
46
|
+
if (last) gaps.push(now - last);
|
|
47
|
+
last = now;
|
|
48
|
+
works.push(spent);
|
|
49
|
+
if (now - since < settle) { works.length = 0; gaps.length = 0; return; }
|
|
50
|
+
while (works.length > sample) works.shift();
|
|
51
|
+
while (gaps.length > sample) gaps.shift();
|
|
52
|
+
if (works.length < sample) return;
|
|
53
|
+
const heap = globalThis.performance?.memory;
|
|
54
|
+
const full = heap ? heap.usedJSHeapSize / heap.jsHeapSizeLimit > 0.85 : false;
|
|
55
|
+
if (median(works) > work || median(gaps) > gap || full) { told = true; onStrain(); }
|
|
56
|
+
},
|
|
57
|
+
reset() { since = 0; last = 0; told = false; works.length = 0; gaps.length = 0; },
|
|
58
|
+
};
|
|
59
|
+
return watch;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The invitation: the key breathes in the accent and a small card under it says what the mode is,
|
|
63
|
+
// with the way to try it and the way to leave it be. Once a session for each map.
|
|
64
|
+
export function inviteNeural({ key, host, map, onTry }) {
|
|
65
|
+
if (!key || !host || key.getAttribute('aria-pressed') === 'true') return null;
|
|
66
|
+
const seen = `pulse.neural.invited.${map}`;
|
|
67
|
+
try { if (sessionStorage.getItem(seen)) return null; sessionStorage.setItem(seen, '1'); } catch { /* shown once on this page */ }
|
|
68
|
+
key.classList.add('invite');
|
|
69
|
+
const card = document.createElement('div');
|
|
70
|
+
card.className = 'neural-invite';
|
|
71
|
+
card.setAttribute('role', 'status');
|
|
72
|
+
const mark = document.createElement('span');
|
|
73
|
+
mark.className = 'neural-invite-mark';
|
|
74
|
+
mark.innerHTML = NEURAL_ICON; // trusted: our own SVG string
|
|
75
|
+
const words = document.createElement('div');
|
|
76
|
+
words.className = 'neural-invite-words';
|
|
77
|
+
const title = document.createElement('b');
|
|
78
|
+
title.textContent = t('Try neural mode');
|
|
79
|
+
const line = document.createElement('span');
|
|
80
|
+
line.textContent = t('The same network, as neurons: only the paths something travels light up.');
|
|
81
|
+
words.append(title, line);
|
|
82
|
+
const acts = document.createElement('div');
|
|
83
|
+
acts.className = 'neural-invite-acts';
|
|
84
|
+
const go = document.createElement('button');
|
|
85
|
+
go.type = 'button';
|
|
86
|
+
go.className = 'neural-invite-go';
|
|
87
|
+
go.textContent = t('Try it');
|
|
88
|
+
const later = document.createElement('button');
|
|
89
|
+
later.type = 'button';
|
|
90
|
+
later.className = 'neural-invite-later';
|
|
91
|
+
later.textContent = t('Not now');
|
|
92
|
+
acts.append(go, later);
|
|
93
|
+
card.append(mark, words, acts);
|
|
94
|
+
const close = () => { card.remove(); key.classList.remove('invite'); };
|
|
95
|
+
go.addEventListener('click', () => { close(); onTry(); });
|
|
96
|
+
later.addEventListener('click', close);
|
|
97
|
+
host.append(card);
|
|
98
|
+
// Under the key, never past the edge of what holds it.
|
|
99
|
+
const place = () => {
|
|
100
|
+
const box = host.getBoundingClientRect();
|
|
101
|
+
const at = key.getBoundingClientRect();
|
|
102
|
+
const width = card.offsetWidth || 320;
|
|
103
|
+
card.style.top = `${Math.round(at.bottom - box.top + 10)}px`;
|
|
104
|
+
card.style.left = `${Math.round(Math.max(12, Math.min(box.width - width - 12, at.right - box.left - width)))}px`;
|
|
105
|
+
};
|
|
106
|
+
place();
|
|
107
|
+
requestAnimationFrame?.(place);
|
|
108
|
+
setTimeout(() => { if (card.isConnected) close(); }, 14000);
|
|
109
|
+
return card;
|
|
110
|
+
}
|