@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
package/public/syntax.js
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 names below are the names of formats — JavaScript, JSON, CSS, Markdown — and a format is
|
|
2
4
|
// called the same in every language, so they never pass through the catalogue.
|
|
3
5
|
//
|
package/public/touch.js
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
|
// A tap is not a hover (2026-10-11). On a screen with no pointer (a phone, a tablet without a
|
|
2
4
|
// trackpad), the browser gives :hover to whatever was tapped and keeps it until the next tap lands
|
|
3
5
|
// somewhere else: a button looked pressed after it was let go. So, where nothing can hover, the
|
package/public/tour-chapters.js
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 the welcome of MADRE 1.0 says (2026-10-11): twelve chapters and the sheet of gestures, in
|
|
2
4
|
// both languages side by side, as the cards of What's new are (src/whats-new.mjs). Each chapter
|
|
3
5
|
// fits on one view: a short scene, a line, three sentences, its gestures and «Take me there».
|
package/public/tour.js
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 welcome of MADRE 1.0 (2026-10-11), in Home and in every project: option B of «Recorrido de
|
|
2
4
|
// MADRE 1.0» — a bonjour, then a window of chapters read at the person's own pace — with two
|
|
3
5
|
// pieces of option C: a one-line hint the first time someone reaches a place that confuses, and
|
|
@@ -22,6 +24,8 @@ import { CHAPTERS, GESTURES, ORDER } from './tour-chapters.js';
|
|
|
22
24
|
const say = (pair) => (typeof pair === 'string' ? pair : pair?.[language()] ?? pair?.es ?? '');
|
|
23
25
|
const fill = (template, vars) => template.replace(/\{(\w+)\}/g, (whole, name) => (Object.hasOwn(vars, name) ? String(vars[name]) : whole));
|
|
24
26
|
const still = () => typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
27
|
+
// Up, turned for down: the way to a hint whose place is out of view.
|
|
28
|
+
const ARROW_UP = '<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 15.5V4.5M5.5 9 10 4.5 14.5 9" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
|
25
29
|
const CLOSE_ICON = '<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M5.5 5.5l9 9M14.5 5.5l-9 9" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>';
|
|
26
30
|
const FOLDER = '<svg viewBox="0 0 32 26" aria-hidden="true"><path d="M2 5.5A2.5 2.5 0 0 1 4.5 3h7.2l3 3.2h12.8A2.5 2.5 0 0 1 30 8.7v12.8a2.5 2.5 0 0 1-2.5 2.5h-23A2.5 2.5 0 0 1 2 21.5z" fill="currentColor"/></svg>';
|
|
27
31
|
const ROBOT = '<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="5" y="8" width="14" height="11" rx="3.5" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M12 4.5V8M3 13v2M21 13v2" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><circle cx="9.5" cy="13.5" r="1.3" fill="currentColor"/><circle cx="14.5" cy="13.5" r="1.3" fill="currentColor"/></svg>';
|
|
@@ -75,6 +79,36 @@ const SCENES = {
|
|
|
75
79
|
gestures: () => `<div class="ts-caps"><span class="ts-cap c0">⌘ ,</span><span class="ts-cap c1">Esc</span><span class="ts-cap c2">?</span></div>`,
|
|
76
80
|
};
|
|
77
81
|
|
|
82
|
+
// What a first-time card speaks of, lit, and the rest of the page a shade quieter: the card needs
|
|
83
|
+
// no beak to say where it points (2026-10-15). A ring laid over the place, never the place itself
|
|
84
|
+
// lifted (a key inside the bar cannot leave the bar's layer). `place()` follows it when it moves;
|
|
85
|
+
// `remove()` takes it away with the card.
|
|
86
|
+
export function spotlight(node) {
|
|
87
|
+
const none = { place() {}, remove() {} };
|
|
88
|
+
if (!node?.getBoundingClientRect || typeof document === 'undefined') return none;
|
|
89
|
+
const spot = document.createElement('div');
|
|
90
|
+
spot.className = 'hint-spot';
|
|
91
|
+
spot.setAttribute('aria-hidden', 'true');
|
|
92
|
+
document.body.append(spot);
|
|
93
|
+
const place = () => {
|
|
94
|
+
if (!node.isConnected) { remove(); return; }
|
|
95
|
+
const box = node.getBoundingClientRect();
|
|
96
|
+
spot.hidden = !box.width && !box.height;
|
|
97
|
+
if (spot.hidden) return;
|
|
98
|
+
const radius = Number.parseFloat(getComputedStyle(node).borderTopLeftRadius) || 10;
|
|
99
|
+
Object.assign(spot.style, { left: `${box.left - 4}px`, top: `${box.top - 4}px`, width: `${box.width + 8}px`, height: `${box.height + 8}px`, borderRadius: `${Math.min(radius + 4, (box.height + 8) / 2)}px` });
|
|
100
|
+
};
|
|
101
|
+
function remove() {
|
|
102
|
+
window.removeEventListener('resize', place);
|
|
103
|
+
window.removeEventListener('scroll', place, true);
|
|
104
|
+
spot.remove();
|
|
105
|
+
}
|
|
106
|
+
window.addEventListener('resize', place);
|
|
107
|
+
window.addEventListener('scroll', place, true);
|
|
108
|
+
place();
|
|
109
|
+
return { place, remove, node: spot };
|
|
110
|
+
}
|
|
111
|
+
|
|
78
112
|
// `onChange` hears the kept state each time it is known or changes (null if it could not be read).
|
|
79
113
|
// `phone`: none of it here (a phone, or the right half of a split window); `device`: this page is a
|
|
80
114
|
// phone, which gets one card the first time instead of the bonjour.
|
|
@@ -180,6 +214,8 @@ export function startTour({ page, go = {}, ready = () => true, phone = false, de
|
|
|
180
214
|
const done = list.filter((chapter) => state.done.includes(chapter.id)).length;
|
|
181
215
|
const pill = el('span', 'tour-pill');
|
|
182
216
|
const open = key('tour-pill-go', fill(t('Continue the tour · {n} of {total}'), { n: done, total: list.length }), () => openChapters(state.at));
|
|
217
|
+
// On a narrow bar it says only where it goes and how far (styles.css, .tour-pill-go[data-short]).
|
|
218
|
+
open.dataset.short = fill(t('Tour · {n}/{total}'), { n: done, total: list.length });
|
|
183
219
|
const away = key('tour-pill-x', null, () => { void keep({ finished: true }); });
|
|
184
220
|
away.innerHTML = CLOSE_ICON; // trusted: our own SVG string
|
|
185
221
|
away.setAttribute('aria-label', t('Hide the tour'));
|
|
@@ -482,14 +518,23 @@ export function startTour({ page, go = {}, ready = () => true, phone = false, de
|
|
|
482
518
|
const card = el('section', `tour-hint${inside ? ' inline' : ''}`);
|
|
483
519
|
card.setAttribute('role', 'note');
|
|
484
520
|
card.append(el('span', 'tour-hint-eyebrow', t('The first time')), el('b', null, title), el('p', null, text));
|
|
521
|
+
let spot = null;
|
|
522
|
+
let away = null;
|
|
523
|
+
let follow = null;
|
|
485
524
|
const done = () => {
|
|
486
525
|
if (!hintNode) return;
|
|
487
526
|
void keep({ hint: id });
|
|
527
|
+
spot?.remove();
|
|
488
528
|
card.remove();
|
|
529
|
+
away?.remove();
|
|
489
530
|
hintNode = null;
|
|
490
531
|
document.removeEventListener('keydown', onKey, true);
|
|
532
|
+
if (follow) { window.removeEventListener('scroll', follow, true); window.removeEventListener('resize', follow); }
|
|
491
533
|
};
|
|
492
|
-
|
|
534
|
+
// Esc puts away the hint in sight, or the one its way-there key speaks of; one hidden under a
|
|
535
|
+
// layer (the Project map) is not the one meant.
|
|
536
|
+
const shown = (node) => Boolean(node?.isConnected) && getComputedStyle(node).display !== 'none';
|
|
537
|
+
const onKey = (event) => { if (event.key === 'Escape' && hintNode && !inside && (shown(card) || shown(away))) { event.stopPropagation(); done(); } };
|
|
493
538
|
card.append(key('tour-btn small primary', t('Got it'), done));
|
|
494
539
|
hintNode = card;
|
|
495
540
|
if (inside) {
|
|
@@ -505,10 +550,44 @@ export function startTour({ page, go = {}, ready = () => true, phone = false, de
|
|
|
505
550
|
const width = Math.min(300, window.innerWidth - 24);
|
|
506
551
|
card.style.width = `${width}px`;
|
|
507
552
|
const height = card.getBoundingClientRect().height;
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
553
|
+
// When the place is scrolled out of view (or was never in it: a widget below the fold), the
|
|
554
|
+
// bubble does not stay behind on its own, out of reach (2026-10-17): it steps aside, and a key at
|
|
555
|
+
// the edge the place went past says where it is and takes the page there.
|
|
556
|
+
away = key('tour-hint-away', '', () => anchor.scrollIntoView?.({ block: 'center', behavior: still() ? 'auto' : 'smooth' }));
|
|
557
|
+
away.hidden = true;
|
|
558
|
+
if (typeof away.showPopover === 'function') away.popover = 'manual';
|
|
559
|
+
document.body.append(away);
|
|
560
|
+
const sayAway = (up) => {
|
|
561
|
+
away.dataset.side = up ? 'up' : 'down';
|
|
562
|
+
away.innerHTML = ARROW_UP; // trusted: our own SVG string
|
|
563
|
+
away.append(el('b', null, title), el('span', null, up ? t('Scroll up to see it') : t('Scroll down to see it')));
|
|
564
|
+
away.setAttribute('aria-label', `${title} · ${up ? t('Scroll up to see it') : t('Scroll down to see it')}`);
|
|
565
|
+
};
|
|
566
|
+
const place = () => {
|
|
567
|
+
if (!anchor.isConnected) { done(); return; }
|
|
568
|
+
const at = anchor.getBoundingClientRect();
|
|
569
|
+
const out = at.bottom < 8 ? 'up' : at.top > window.innerHeight - 8 ? 'down' : null;
|
|
570
|
+
card.hidden = Boolean(out);
|
|
571
|
+
if (out && away.dataset.side !== out) sayAway(out === 'up');
|
|
572
|
+
if (out && away.hidden) { away.hidden = false; try { away.showPopover?.(); } catch { /* on the page */ } }
|
|
573
|
+
if (!out && !away.hidden) { away.hidden = true; delete away.dataset.side; try { away.hidePopover?.(); } catch { /* on the page */ } }
|
|
574
|
+
if (out) return;
|
|
575
|
+
const onTheRight = at.left + at.width / 2 > window.innerWidth / 2;
|
|
576
|
+
const left = Math.max(12, Math.min(window.innerWidth - width - 12, onTheRight ? at.right - width : at.left));
|
|
577
|
+
const below = at.bottom + 10 + height < window.innerHeight;
|
|
578
|
+
Object.assign(card.style, { left: `${left}px`, top: `${below ? at.bottom + 10 : Math.max(12, at.top - height - 10)}px` });
|
|
579
|
+
};
|
|
580
|
+
spot = spotlight(anchor);
|
|
581
|
+
follow = () => place();
|
|
582
|
+
window.addEventListener('scroll', follow, true);
|
|
583
|
+
window.addEventListener('resize', follow);
|
|
584
|
+
place();
|
|
585
|
+
// Where it points: on the page, or on a layer over it (Home's Project map). A layer opened over
|
|
586
|
+
// the page hides the hints that point under it, and they come back when it closes (salas-mac.css).
|
|
587
|
+
const layer = anchor.closest?.('.mv') ? 'map' : 'page';
|
|
588
|
+
card.dataset.under = layer;
|
|
589
|
+
away.dataset.under = layer;
|
|
590
|
+
if (spot.node) spot.node.dataset.under = layer;
|
|
512
591
|
document.addEventListener('keydown', onKey, true);
|
|
513
592
|
return true;
|
|
514
593
|
}
|
|
@@ -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 { t } from './i18n.js';
|
|
2
4
|
|
|
3
5
|
// MU/TH/UR knowledge base: known conditions, how to recognise them in the
|
|
@@ -34,6 +36,10 @@ const installAgent = {
|
|
|
34
36
|
darwin: ['brew install opencode', '# or: npm install -g opencode-ai'],
|
|
35
37
|
linux: ['curl -fsSL https://opencode.ai/install | bash', '# or: npm install -g opencode-ai']
|
|
36
38
|
},
|
|
39
|
+
cursor: {
|
|
40
|
+
darwin: ['curl https://cursor.com/install -fsS | bash'],
|
|
41
|
+
linux: ['curl https://cursor.com/install -fsS | bash']
|
|
42
|
+
},
|
|
37
43
|
};
|
|
38
44
|
|
|
39
45
|
const loginAgent = {
|
|
@@ -41,6 +47,7 @@ const loginAgent = {
|
|
|
41
47
|
claude: { darwin: ['claude auth login', 'claude auth status --text'], linux: ['claude auth login', 'claude auth status --text']},
|
|
42
48
|
gemini: { darwin: ['gemini', '# inside gemini: /auth → "Use Gemini API key" or Google login'], linux: ['gemini', '# inside gemini: /auth']},
|
|
43
49
|
opencode: { darwin: ['opencode auth login', 'opencode auth list'], linux: ['opencode auth login', 'opencode auth list']},
|
|
50
|
+
cursor: { darwin: ['cursor-agent login', 'cursor-agent status'], linux: ['cursor-agent login', 'cursor-agent status']},
|
|
44
51
|
};
|
|
45
52
|
|
|
46
53
|
const same = (commands) => ({ darwin: commands, linux: commands });
|
|
@@ -244,7 +251,7 @@ export const CONDITIONS = [
|
|
|
244
251
|
severity: 'informational',
|
|
245
252
|
title: 'Where the usage numbers come from',
|
|
246
253
|
match: /ring|quota|limit window|resets|rollout|oauth usage|window reset|limits-real/i,
|
|
247
|
-
diagnosis: 'Each sphere\'s usage shows the provider\'s real limit when the CLI publishes it: Codex writes its 5-hour and weekly windows (used %, reset time) into every session rollout under ~/.codex/sessions; Claude Code\'s /usage comes from Anthropic\'s OAuth usage endpoint; MADRE can ask it with the token Claude Code keeps in the keychain when you start with PULSE_CLAUDE_USAGE=1 (macOS may ask once to allow the keychain read). Gemini and OpenCode publish nothing locally, so their usage shows MADRE\'s own rolling 5-hour window of budget tokens. A window whose reset time has passed counts as empty until the CLI reports again, and the
|
|
254
|
+
diagnosis: 'Each sphere\'s usage shows the provider\'s real limit when the CLI publishes it: Codex writes its 5-hour and weekly windows (used %, reset time) into every session rollout under ~/.codex/sessions; Claude Code\'s /usage comes from Anthropic\'s OAuth usage endpoint; MADRE can ask it with the token Claude Code keeps in the keychain when you start with PULSE_CLAUDE_USAGE=1 (macOS may ask once to allow the keychain read). Gemini and OpenCode publish nothing locally, so their usage shows MADRE\'s own rolling 5-hour window of budget tokens. A window whose reset time has passed counts as empty until the CLI reports again, and the failure monitor says "clear" when a full window resets.',
|
|
248
255
|
remedy: 'Click a sphere to see both windows and when they reset. If a sphere\'s usage looks stale, run one turn with that agent: Codex only rewrites its limits when it runs. Set PULSE_OFFICIAL_QUOTA=0 to stop reading provider limits altogether.',
|
|
249
256
|
fixes: same(['# click the sphere → 5h / 7d windows and reset times', 'PULSE_CLAUDE_USAGE=1 madre start # also read Claude Code\'s usage windows', 'PULSE_OFFICIAL_QUOTA=0 madre start # local window only']),
|
|
250
257
|
},
|
|
@@ -313,7 +320,7 @@ export const CONDITIONS = [
|
|
|
313
320
|
severity: 'informational',
|
|
314
321
|
title: 'Gemini: generating images',
|
|
315
322
|
match: /scope-gemini-imageGen/,
|
|
316
|
-
diagnosis: 'Gemini CLI 0.60 has no image-generation tool in headless mode, even though Google offers image models. MADRE fills the gap with the Images module: its own MCP server exposing generate_image on the Gemini API image models, attached to Gemini only inside a
|
|
323
|
+
diagnosis: 'Gemini CLI 0.60 has no image-generation tool in headless mode, even though Google offers image models. MADRE fills the gap with the Images module: its own MCP server exposing generate_image on the Gemini API image models, attached to Gemini only inside a permission to create with images on, billing your Gemini key.',
|
|
317
324
|
remedy: 'Turn Images on in Modules (it needs a Gemini API key with credits), then switch on Generate images for Gemini in Settings → Agents. Without the module, ask @codex or let the orchestrator route the image step to it.',
|
|
318
325
|
fixes: same(['# Modules → Images → Active', '# Settings → Agents → Gemini → Generate images', '# then: CREATE + "generate … as name.png"']),
|
|
319
326
|
},
|
|
@@ -324,7 +331,7 @@ export const CONDITIONS = [
|
|
|
324
331
|
severity: 'informational',
|
|
325
332
|
title: 'Claude Code: generating images',
|
|
326
333
|
match: /scope-claude-imageGen/,
|
|
327
|
-
diagnosis: 'Claude Code has no image-generation tool; Claude models describe and reason about images, they do not render them. The Images module gives Claude the MCP tool generate_image (MADRE\'s own server on the Gemini API), attached with --mcp-config only inside a
|
|
334
|
+
diagnosis: 'Claude Code has no image-generation tool; Claude models describe and reason about images, they do not render them. The Images module gives Claude the MCP tool generate_image (MADRE\'s own server on the Gemini API), attached with --mcp-config only inside a permission to create.',
|
|
328
335
|
remedy: 'Turn Images on in Modules, switch on Generate images for Claude in Settings → Agents, then use CREATE. Or route the image step to @codex.',
|
|
329
336
|
fixes: same(['# Modules → Images → Active', '# Settings → Agents → Claude → Generate images']),
|
|
330
337
|
},
|
|
@@ -335,7 +342,7 @@ export const CONDITIONS = [
|
|
|
335
342
|
severity: 'informational',
|
|
336
343
|
title: 'OpenCode: generating images',
|
|
337
344
|
match: /scope-opencode-imageGen/,
|
|
338
|
-
diagnosis: 'OpenCode exposes no image-generation tool of its own. The Images module adds MADRE\'s MCP image server to OpenCode\'s ephemeral config inside a
|
|
345
|
+
diagnosis: 'OpenCode exposes no image-generation tool of its own. The Images module adds MADRE\'s MCP image server to OpenCode\'s ephemeral config inside a permission to create.',
|
|
339
346
|
remedy: 'Turn Images on in Modules, switch on Generate images for OpenCode in Settings → Agents, then use CREATE. Or route the image step to @codex.',
|
|
340
347
|
fixes: same(['# Modules → Images → Active', '# Settings → Agents → OpenCode → Generate images']),
|
|
341
348
|
},
|
|
@@ -353,11 +360,11 @@ export const CONDITIONS = [
|
|
|
353
360
|
id: 'slash-commands',
|
|
354
361
|
code: 'MU-022',
|
|
355
362
|
severity: 'informational',
|
|
356
|
-
title: 'Commands and mentions in the
|
|
363
|
+
title: 'Commands and mentions in the Write box',
|
|
357
364
|
match: /slash|command|\/git|\/ahp|mention|@agent/i,
|
|
358
|
-
diagnosis: 'Type "/" for the project\'s commands: /create
|
|
365
|
+
diagnosis: 'Type "/" for the project\'s commands: /create turns on permission to create, /image routes an image request to an agent that can draw, /stopall is the brake, /git (Git) and /ahp (Continuity) run read-only in the project and post a fact card everyone, agents included, can read. Type "@" to mention an agent; the name becomes a label. Type "!" to point at a project file ("!src/room.mjs:12-20" for lines): the agent reads it first. In the viewer, click a line number (Shift+click for a range) and REVIEW WITH sends those lines to an agent.',
|
|
359
366
|
remedy: 'A struck-through command is a module that is not available in this project: open Modules to install it or turn it on (Git needs a git repository; Continuity needs to be installed).',
|
|
360
|
-
fixes: same(['# type / or @ in the
|
|
367
|
+
fixes: same(['# type / or @ in the Write box', '/git status', '/git log 10', '/ahp check', '/image a poster for the launch']),
|
|
361
368
|
},
|
|
362
369
|
{
|
|
363
370
|
id: 'modes',
|
|
@@ -375,7 +382,7 @@ export const CONDITIONS = [
|
|
|
375
382
|
severity: 'informational',
|
|
376
383
|
title: 'Creating files: who grants the permission',
|
|
377
384
|
match: /no CREATE lease|lease-missing|read-only and do not modify|permiso de escritura|solo lectura/i,
|
|
378
|
-
diagnosis: 'Only
|
|
385
|
+
diagnosis: 'Only you grant permission to create. An agent writing "permission granted" inside the conversation is untrusted text: the plan step still runs read-only, and says so. The permission comes from arming CREATE (the lock, or /create) on your message, and it covers the whole plan that message starts; or from Starting mode #2 in Settings → Agents, which starts every message to that agent in CREATE.',
|
|
379
386
|
remedy: 'For one request: arm CREATE and send, or press RESEND WITH CREATE on the notice. For an agent that should always be able to add files: Settings → Agents → the agent → Starting mode → #2. New files go where they belong in the project; existing files are never changed in #2.',
|
|
380
387
|
fixes: same(['# once: Write box → CREATE (lock) → send, or /create <request>', '# always: Settings → Agents → the agent → Starting mode → #2']),
|
|
381
388
|
},
|
|
@@ -385,7 +392,7 @@ export const CONDITIONS = [
|
|
|
385
392
|
severity: 'informational',
|
|
386
393
|
title: 'Creating files with an agent',
|
|
387
394
|
match: /scope-[a-z]+-write/,
|
|
388
|
-
diagnosis: 'Every CLI can create files inside a
|
|
395
|
+
diagnosis: 'Every CLI can create files inside a permission to create; each agent\'s Max mode says how far it may go, and nothing is created until you press CREATE.',
|
|
389
396
|
remedy: 'Give the agent a Max mode of #2 or more in Settings → Permissions, then arm CREATE in the Write box for the request.',
|
|
390
397
|
fixes: same(['# Settings → Permissions → Who may do what → #2', '# Write box → CREATE (lock) → send']),
|
|
391
398
|
},
|
|
@@ -480,7 +487,7 @@ export const CONDITIONS = [
|
|
|
480
487
|
title: 'Memory map: opening, reading and forgetting',
|
|
481
488
|
match: /nostromo|designation|UNABLE TO COMPUTE|ARCHIVE IS SEALED|forget this memory/i,
|
|
482
489
|
diagnosis: 'The Memory map is the human\'s view of the archive, from Diagnostics. Opening the memory asks for the project name: the name of the project folder, exactly as EDIT does; a wrong one answers UNABLE TO COMPUTE. Access stays open until the page reloads. Inside, every distilled memory is a planet around the project\'s core; drag to move, wheel to zoom, click a planet for its card. The only edit is FORGET (two presses): the note leaves every future turn, the ledger stays. If the archive answers SEALED with a number of minutes, wait them out: repeated strikes at the core close it for a while and the Write box reads INTRUDER until it opens again.',
|
|
483
|
-
remedy: 'Type the folder name shown in the project header as the project name. To find a memory quickly, ask an agent to search instead.
|
|
490
|
+
remedy: 'Type the folder name shown in the project header as the project name. To find a memory quickly, ask an agent to search instead. Avoid guessing names: several wrong ones in a row lock the Memory map for a while.',
|
|
484
491
|
fixes: same(['# Diagnostics → Memory map → project name = folder name', 'basename "$PWD"']),
|
|
485
492
|
},
|
|
486
493
|
{
|
|
@@ -500,7 +507,7 @@ export const CONDITIONS = [
|
|
|
500
507
|
severity: 'informational',
|
|
501
508
|
title: '@madre: the project\'s own memory, as an agent',
|
|
502
509
|
// Older rooms recorded @madre leaving as "madre is not installed": it is this condition, not MU-009.
|
|
503
|
-
match: /@madre|madre-local|needs Ollama running with a chat model|\bmadre is not installed on this computer|@madre left the room/i,
|
|
510
|
+
match: /@madre|madre-local|needs Ollama running with a chat model|\bmadre is not installed on this computer|@madre left the (room|project)/i,
|
|
504
511
|
diagnosis: 'When Ollama runs with a chat model, a fifth agent joins the project: @madre. It answers from the whole archive (distilled notes and exact quotes, cited as [#n]) and from what the message carries, locally, for free. It never writes, draws, browses or makes plans; when the project never discussed something it says so. Other agents may give it a plan step to check what was decided. It leaves the project when Ollama stops, and Diagnostics says so.',
|
|
505
512
|
remedy: 'Start Ollama and download a chat model from Local AI in Modules; its @madre in the project switch turns the agent off if you do not want it. Ask it "what did we decide about …" or "did we ever discuss …".',
|
|
506
513
|
fixes: same(['ollama serve', 'ollama pull qwen2.5:3b', '@madre what did we decide about the webhook?']),
|
|
@@ -513,7 +520,7 @@ export const CONDITIONS = [
|
|
|
513
520
|
title: 'Ollama: local smart search and a local archivist',
|
|
514
521
|
match: /ollama|11434|local model|nomic-embed|qwen|llama3/i,
|
|
515
522
|
diagnosis: 'When Ollama runs on this machine with an embedding model and a chat model, MADRE embeds the memory locally (recall by meaning without any key) and distils memories with the local model first, before Gemini and the others. Nothing leaves the machine for remembering. Off or absent, everything falls back to the providers. The Local AI module, in Modules, shows what Ollama has, downloads the recommended models, and switches each role.',
|
|
516
|
-
remedy: 'Install Ollama (ollama.com), start it, then open Local AI in Modules, press
|
|
523
|
+
remedy: 'Install Ollama (ollama.com), start it, then open Local AI in Modules, press RECHECK and download the models it suggests. Set PULSE_OLLAMA_MODEL or PULSE_OLLAMA_EMBED_MODEL to prefer others; PULSE_EMBED_PROVIDER=gemini keeps embeddings on Gemini even with Ollama running.',
|
|
517
524
|
fixes: {
|
|
518
525
|
darwin: ['brew install ollama', 'ollama serve', 'ollama pull nomic-embed-text', 'ollama pull qwen2.5:3b'],
|
|
519
526
|
linux: ['curl -fsSL https://ollama.com/install.sh | sh', 'ollama serve', 'ollama pull nomic-embed-text', 'ollama pull qwen2.5:3b']
|
|
@@ -566,11 +573,11 @@ export const CONDITIONS = [
|
|
|
566
573
|
id: 'memory-aberrations',
|
|
567
574
|
code: 'MU-041',
|
|
568
575
|
severity: 'informational',
|
|
569
|
-
title: '
|
|
576
|
+
title: 'Doubtful memories: what the project established is false',
|
|
570
577
|
match: /aberration|aberraci|eyecat|contradict|refut|false claim|hallucinat|alucina/i,
|
|
571
|
-
diagnosis: '
|
|
578
|
+
diagnosis: 'A doubtful memory is a claim the project established is false, kept on purpose rather than deleted: it is what the local model learns not to repeat. It never travels into a turn, and the note it refutes is taken out of circulation with it — a refuted memory is never recalled again, by words or by meaning. The memory reviewer watches for two agents saying opposite things about the same subject and raises the pair for a third agent to judge, one that took no part in either side. Confirming files the doubtful memory and quarantines what it refutes; dismissing says the project was right and the pair is never raised again. Forgetting a doubtful memory puts the note it refuted back in use.',
|
|
572
579
|
remedy: 'The Memory map shows them as collapsed bodies: open one to read what it took down, or clear it if the project was wrong about being wrong. The second test of the Memory review, in Settings → Memory, reports open contradictions.',
|
|
573
|
-
fixes: same(['# Diagnostics → Memory map → the dark bodies are
|
|
580
|
+
fixes: same(['# Diagnostics → Memory map → the dark bodies are doubtful memories', '# Settings → Memory → Analysis and tests → the second test']),
|
|
574
581
|
},
|
|
575
582
|
{
|
|
576
583
|
id: 'memory-cascade',
|
|
@@ -598,7 +605,7 @@ export const CONDITIONS = [
|
|
|
598
605
|
severity: 'informational',
|
|
599
606
|
title: 'Memory review: whether the archive works',
|
|
600
607
|
match: /three tests|coverage|consistency|held-out|match test|maturity|madurez|ready to be worked in/i,
|
|
601
|
-
diagnosis: 'The six readings count what the archive is made of; they cannot say whether it works, because nothing about a pile of notes says whether the right one comes back when it is needed. COVERAGE takes real questions from this project, runs recall at the exact point each was asked, and scores what came back against the reply that was actually given. CONSISTENCY reads the contradictions
|
|
608
|
+
diagnosis: 'The six readings count what the archive is made of; they cannot say whether it works, because nothing about a pile of notes says whether the right one comes back when it is needed. COVERAGE takes real questions from this project, runs recall at the exact point each was asked, and scores what came back against the reply that was actually given. CONSISTENCY reads the contradictions the memory reviewer still holds and whether doubtful memories are being filed more often lately. MATCH asks the local model, with this archive behind it, real questions that one of your agents (Codex, Claude, Gemini or OpenCode) already answered. All three measure against a control, because everything in one project is about the same handful of subjects and any two pieces of it look alike to search by meaning: to count, what the archive handed over has to beat what it would have handed over for a different question.',
|
|
602
609
|
remedy: 'Read the one sentence and the one instruction at the top of Settings → Memory; the readings and the tests are in Analysis and tests, below it. The free test is read as the panel opens, the cheap one keeps itself fresh once a day while the embeddings are local, and the slow one is yours to start and to stop. None of them spends a provider turn.',
|
|
603
610
|
fixes: same(['# Settings → Memory → Analysis and tests → Memory review', '# MATCH needs Ollama running with a chat model, and smart search on']),
|
|
604
611
|
},
|
|
@@ -704,7 +711,7 @@ export const allConditions = () => CONDITIONS.map(said);
|
|
|
704
711
|
|
|
705
712
|
// Lowercase and without accents: «limite» finds «límite», and a person types what they type.
|
|
706
713
|
const fold = (text) => String(text ?? '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
|
|
707
|
-
const AGENT_WORDS = new Set(['codex', 'claude', 'gemini', 'opencode', 'madre', 'ollama']);
|
|
714
|
+
const AGENT_WORDS = new Set(['codex', 'claude', 'gemini', 'opencode', 'cursor', 'madre', 'ollama']);
|
|
708
715
|
|
|
709
716
|
export function searchConditions(query) {
|
|
710
717
|
const words = fold(query).split(/[^a-z0-9#.@_-]+/).filter(Boolean);
|
|
@@ -0,0 +1,275 @@
|
|
|
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
|
+
// Your own wallpaper (1.0.3): a picture of yours behind MADRE, fitted to the screen like the ones
|
|
4
|
+
// that ship. The page does here what was done by hand for each of those (wallpapers/ →
|
|
5
|
+
// public/assets/wallpapers, and their colours in styles.css): it reads the picture, keeps its
|
|
6
|
+
// longest side at SIDE, encodes it, and takes its colours for the veil, the mesh and the tint. The
|
|
7
|
+
// server keeps it once for every page (src/looks.mjs); each page still chooses it or not. Shared by
|
|
8
|
+
// the room (app.js) and Home (salas-mac.js), which each draw it with their own keys.
|
|
9
|
+
import { t } from './i18n.js';
|
|
10
|
+
|
|
11
|
+
export const CUSTOM_WALL = 'custom';
|
|
12
|
+
const SIDE = 2880;
|
|
13
|
+
const READ_MAX = 40 * 1024 * 1024;
|
|
14
|
+
const SEND_MAX = 8 * 1024 * 1024;
|
|
15
|
+
const META_KEY = 'pulse.wallpaper.custom';
|
|
16
|
+
const PLUS_ICON = '<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 4.5v11M4.5 10h11" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>';
|
|
17
|
+
|
|
18
|
+
const node = (tag, className, text) => {
|
|
19
|
+
const made = document.createElement(tag);
|
|
20
|
+
if (className) made.className = className;
|
|
21
|
+
if (text !== undefined && text !== null) made.textContent = text;
|
|
22
|
+
return made;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
let current = null;
|
|
26
|
+
export const customWall = () => current;
|
|
27
|
+
// The last picture this browser knew, so a page paints with it before the server answers.
|
|
28
|
+
export function rememberedWall() {
|
|
29
|
+
try { return JSON.parse(localStorage.getItem(META_KEY) ?? 'null'); } catch { return null; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// The picture and its colours, as variables on the root (styles.css, --wpc-*): every wallpaper rule
|
|
33
|
+
// reads them. `null` takes them away; a page that had chosen it then wears no picture.
|
|
34
|
+
const VARIABLES = ['--wpc-url', '--wpc-pos', '--wpc-veil-d', '--wpc-veil-l', '--wpc-tint-d', '--wpc-tint-l', '--wpc-m1-d', '--wpc-m2-d', '--wpc-m3-d', '--wpc-m1-l', '--wpc-m2-l', '--wpc-m3-l'];
|
|
35
|
+
export function paintCustomWall(meta, root = globalThis.document?.documentElement) {
|
|
36
|
+
current = meta ?? null;
|
|
37
|
+
try { if (current) localStorage.setItem(META_KEY, JSON.stringify(current)); else localStorage.removeItem(META_KEY); } catch { /* this page only */ }
|
|
38
|
+
const style = root?.style;
|
|
39
|
+
if (!style?.setProperty) return;
|
|
40
|
+
if (!current) { for (const name of VARIABLES) style.removeProperty(name); return; }
|
|
41
|
+
const rgb = (colour) => colour.join(' ');
|
|
42
|
+
const { dark, light } = current.palette;
|
|
43
|
+
style.setProperty('--wpc-url', `url("/api/looks/wallpaper?v=${current.v}")`);
|
|
44
|
+
style.setProperty('--wpc-pos', `${current.focus.x}% ${current.focus.y}%`);
|
|
45
|
+
style.setProperty('--wpc-veil-d', rgb(dark.veil));
|
|
46
|
+
style.setProperty('--wpc-veil-l', rgb(light.veil));
|
|
47
|
+
style.setProperty('--wpc-tint-d', rgb(dark.tint));
|
|
48
|
+
style.setProperty('--wpc-tint-l', rgb(light.tint));
|
|
49
|
+
dark.mesh.forEach((colour, index) => style.setProperty(`--wpc-m${index + 1}-d`, rgb(colour)));
|
|
50
|
+
light.mesh.forEach((colour, index) => style.setProperty(`--wpc-m${index + 1}-l`, rgb(colour)));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Its colours, the way the shipped ones were taken: the whole for the veil and the tint, and three
|
|
54
|
+
// corners for the mesh (top left, right, bottom), each pressed towards the theme's ground.
|
|
55
|
+
function paletteOf(source) {
|
|
56
|
+
const canvas = node('canvas');
|
|
57
|
+
canvas.width = 48;
|
|
58
|
+
canvas.height = 32;
|
|
59
|
+
const ink = canvas.getContext('2d', { willReadFrequently: true });
|
|
60
|
+
ink.drawImage(source, 0, 0, 48, 32);
|
|
61
|
+
const { data } = ink.getImageData(0, 0, 48, 32);
|
|
62
|
+
const average = (x0, y0, x1, y1) => {
|
|
63
|
+
const sum = [0, 0, 0];
|
|
64
|
+
let count = 0;
|
|
65
|
+
for (let y = y0; y < y1; y += 1) {
|
|
66
|
+
for (let x = x0; x < x1; x += 1) {
|
|
67
|
+
const at = (y * 48 + x) * 4;
|
|
68
|
+
if (data[at + 3] < 16) continue;
|
|
69
|
+
sum[0] += data[at]; sum[1] += data[at + 1]; sum[2] += data[at + 2];
|
|
70
|
+
count += 1;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return count ? sum.map((value) => value / count) : [128, 128, 128];
|
|
74
|
+
};
|
|
75
|
+
const toward = (colour, ground, k) => colour.map((value, index) => Math.max(0, Math.min(255, Math.round(value * (1 - k) + ground[index] * k))));
|
|
76
|
+
const black = [0, 0, 0];
|
|
77
|
+
const white = [255, 255, 255];
|
|
78
|
+
const whole = average(0, 0, 48, 32);
|
|
79
|
+
const corners = [average(0, 0, 20, 13), average(28, 0, 48, 19), average(8, 20, 40, 32)];
|
|
80
|
+
return {
|
|
81
|
+
dark: { veil: toward(whole, black, 0.93), tint: toward(whole, black, 0.86), mesh: [toward(corners[0], black, 0.76), toward(corners[1], black, 0.66), toward(corners[2], black, 0.88)] },
|
|
82
|
+
light: { veil: toward(whole, white, 0.94), tint: toward(whole, white, 0.95), mesh: [toward(corners[0], white, 0.88), toward(corners[1], white, 0.92), toward(corners[2], white, 0.86)] },
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const encode = (canvas, type, quality) => new Promise((resolve) => canvas.toBlob(resolve, type, quality));
|
|
87
|
+
// The picture fitted and encoded, with its size and its colours. WebP where the browser writes it,
|
|
88
|
+
// JPEG where it does not; smaller again if it still weighs more than the server keeps.
|
|
89
|
+
export async function prepareWallpaper(file) {
|
|
90
|
+
if (!file) throw new Error(t('Choose a picture: a PNG, JPEG or WebP file.'));
|
|
91
|
+
if (file.size > READ_MAX) throw new Error(t('That picture is too large to read (more than 40 MB).'));
|
|
92
|
+
let bitmap;
|
|
93
|
+
try { bitmap = await createImageBitmap(file); } catch { throw new Error(t('That picture could not be read. Try a PNG, JPEG or WebP.')); }
|
|
94
|
+
const scale = Math.min(1, SIDE / Math.max(bitmap.width, bitmap.height));
|
|
95
|
+
const w = Math.max(1, Math.round(bitmap.width * scale));
|
|
96
|
+
const h = Math.max(1, Math.round(bitmap.height * scale));
|
|
97
|
+
const canvas = node('canvas');
|
|
98
|
+
canvas.width = w;
|
|
99
|
+
canvas.height = h;
|
|
100
|
+
const ink = canvas.getContext('2d');
|
|
101
|
+
ink.imageSmoothingQuality = 'high';
|
|
102
|
+
ink.drawImage(bitmap, 0, 0, w, h);
|
|
103
|
+
const palette = paletteOf(bitmap);
|
|
104
|
+
bitmap.close?.();
|
|
105
|
+
let blob = await encode(canvas, 'image/webp', 0.86);
|
|
106
|
+
if (!blob || blob.type !== 'image/webp') blob = await encode(canvas, 'image/jpeg', 0.88);
|
|
107
|
+
for (const quality of [0.74, 0.6]) if (blob && blob.size > SEND_MAX) blob = await encode(canvas, blob.type, quality);
|
|
108
|
+
if (!blob || blob.size > SEND_MAX) throw new Error(t('That picture could not be prepared.'));
|
|
109
|
+
return { blob, w, h, palette };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function uploadWallpaper(file, { focus = { x: 50, y: 50 } } = {}) {
|
|
113
|
+
const { blob, w, h, palette } = await prepareWallpaper(file);
|
|
114
|
+
const response = await fetch('/api/looks/wallpaper', {
|
|
115
|
+
method: 'PUT',
|
|
116
|
+
headers: { 'content-type': blob.type, 'x-madre-wallpaper': encodeURIComponent(JSON.stringify({ w, h, focus, palette })) },
|
|
117
|
+
body: blob,
|
|
118
|
+
});
|
|
119
|
+
const answer = await response.json().catch(() => ({}));
|
|
120
|
+
if (!response.ok || !answer.custom) throw new Error(answer.error ?? t('That picture could not be kept.'));
|
|
121
|
+
return answer.custom;
|
|
122
|
+
}
|
|
123
|
+
async function sendFrame(focus) {
|
|
124
|
+
const response = await fetch('/api/looks/wallpaper/frame', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(focus) });
|
|
125
|
+
const answer = await response.json().catch(() => ({}));
|
|
126
|
+
return response.ok ? answer.custom ?? null : null;
|
|
127
|
+
}
|
|
128
|
+
async function dropWallpaper() {
|
|
129
|
+
await fetch('/api/looks/wallpaper', { method: 'DELETE' }).catch(() => null);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// The browser's own chooser, opened from the press that asked for it.
|
|
133
|
+
export function pickPicture() {
|
|
134
|
+
return new Promise((resolve) => {
|
|
135
|
+
const input = node('input');
|
|
136
|
+
input.type = 'file';
|
|
137
|
+
input.accept = 'image/png,image/jpeg,image/webp,image/avif,image/gif';
|
|
138
|
+
input.addEventListener('change', () => resolve(input.files?.[0] ?? null), { once: true });
|
|
139
|
+
input.addEventListener('cancel', () => resolve(null), { once: true });
|
|
140
|
+
input.click();
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// The tile among the wallpapers: the picture when there is one; a «+» to add it when there is not.
|
|
145
|
+
export function customTile(paper, meta) {
|
|
146
|
+
paper.dataset.wp = CUSTOM_WALL;
|
|
147
|
+
paper.classList.toggle('paper-add', !meta);
|
|
148
|
+
const swatch = node('span', 'paper-swatch');
|
|
149
|
+
if (!meta) swatch.innerHTML = PLUS_ICON; // trusted: our own SVG string
|
|
150
|
+
paper.append(swatch, node('span', 'paper-name', t('Your picture')));
|
|
151
|
+
if (!meta) paper.title = t('Choose a picture of yours as the wallpaper');
|
|
152
|
+
return paper;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Adding it: the chooser, the picture prepared and kept, and then it is this page's wallpaper.
|
|
156
|
+
// `busy` marks the tile while that happens; `chosen` hands the kept picture back.
|
|
157
|
+
export async function addPicture({ busy = null, chosen, failed }) {
|
|
158
|
+
const file = await pickPicture();
|
|
159
|
+
if (!file) return;
|
|
160
|
+
busy?.classList.add('busy');
|
|
161
|
+
busy?.setAttribute('aria-busy', 'true');
|
|
162
|
+
try {
|
|
163
|
+
const meta = await uploadWallpaper(file);
|
|
164
|
+
paintCustomWall(meta);
|
|
165
|
+
chosen(meta);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
failed(error.message);
|
|
168
|
+
} finally {
|
|
169
|
+
busy?.classList.remove('busy');
|
|
170
|
+
busy?.removeAttribute('aria-busy');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// The frame drawn last follows the window's proportions; one listener for the page, not one per drawing.
|
|
175
|
+
let placeFrame = null;
|
|
176
|
+
globalThis.addEventListener?.('resize', () => placeFrame?.());
|
|
177
|
+
|
|
178
|
+
// Under the tiles while it is chosen: where the screen falls on the picture, and the picture
|
|
179
|
+
// changed or taken away. The whole picture, and over it the part the screen shows, in the screen's
|
|
180
|
+
// own proportions; carried by hand (or by the arrows) to the part that should stay in view.
|
|
181
|
+
export function customTools(meta, { changed, removed, failed, button }) {
|
|
182
|
+
const box = node('div', 'wall-tools');
|
|
183
|
+
const head = node('div', 'wall-tools-head');
|
|
184
|
+
const words = node('div', 'wall-tools-words');
|
|
185
|
+
words.append(node('b', null, t('Framing')));
|
|
186
|
+
const hint = node('span', 'wall-tools-hint');
|
|
187
|
+
words.append(hint);
|
|
188
|
+
head.append(words);
|
|
189
|
+
const frame = node('div', 'wall-frame');
|
|
190
|
+
const fit = node('span', 'wall-frame-fit');
|
|
191
|
+
const picture = node('img', 'wall-frame-pic');
|
|
192
|
+
picture.alt = '';
|
|
193
|
+
picture.src = `/api/looks/wallpaper?v=${meta.v}`;
|
|
194
|
+
picture.draggable = false;
|
|
195
|
+
const view = node('span', 'wall-frame-view');
|
|
196
|
+
view.tabIndex = 0;
|
|
197
|
+
view.setAttribute('role', 'slider');
|
|
198
|
+
view.setAttribute('aria-label', t('Framing of your picture'));
|
|
199
|
+
fit.append(picture, view);
|
|
200
|
+
frame.append(fit);
|
|
201
|
+
let focus = { ...meta.focus };
|
|
202
|
+
const sides = () => {
|
|
203
|
+
const pictureRatio = meta.w / meta.h;
|
|
204
|
+
const screen = (globalThis.innerWidth || 16) / (globalThis.innerHeight || 9);
|
|
205
|
+
return { fw: Math.min(1, screen / pictureRatio), fh: Math.min(1, pictureRatio / screen) };
|
|
206
|
+
};
|
|
207
|
+
const free = () => { const { fw, fh } = sides(); return fw < 0.995 ? 'x' : fh < 0.995 ? 'y' : null; };
|
|
208
|
+
const place = () => {
|
|
209
|
+
const { fw, fh } = sides();
|
|
210
|
+
view.style.width = `${fw * 100}%`;
|
|
211
|
+
view.style.height = `${fh * 100}%`;
|
|
212
|
+
view.style.left = `${(1 - fw) * focus.x}%`;
|
|
213
|
+
view.style.top = `${(1 - fh) * focus.y}%`;
|
|
214
|
+
const axis = free();
|
|
215
|
+
frame.dataset.free = axis ?? 'none';
|
|
216
|
+
hint.textContent = axis ? t('Drag the frame to choose which part of your picture stays in view.') : t('Your picture fills the screen as it is.');
|
|
217
|
+
view.setAttribute('aria-valuetext', `${Math.round(axis === 'y' ? focus.y : focus.x)}%`);
|
|
218
|
+
};
|
|
219
|
+
const live = () => globalThis.document?.documentElement?.style?.setProperty('--wpc-pos', `${focus.x}% ${focus.y}%`);
|
|
220
|
+
const settle = async () => {
|
|
221
|
+
const kept = await sendFrame(focus);
|
|
222
|
+
if (kept) { paintCustomWall(kept); changed(kept, { reframed: true }); } else failed(t('The framing could not be kept.'));
|
|
223
|
+
};
|
|
224
|
+
const clamp = (value) => Math.max(0, Math.min(100, Math.round(value * 10) / 10));
|
|
225
|
+
const follow = (event) => {
|
|
226
|
+
const area = fit.getBoundingClientRect();
|
|
227
|
+
const { fw, fh } = sides();
|
|
228
|
+
const u = (event.clientX - area.left) / area.width;
|
|
229
|
+
const v = (event.clientY - area.top) / area.height;
|
|
230
|
+
if (fw < 0.995) focus.x = clamp(((u - fw / 2) / (1 - fw)) * 100);
|
|
231
|
+
if (fh < 0.995) focus.y = clamp(((v - fh / 2) / (1 - fh)) * 100);
|
|
232
|
+
place();
|
|
233
|
+
live();
|
|
234
|
+
};
|
|
235
|
+
fit.addEventListener('pointerdown', (event) => {
|
|
236
|
+
if (!free()) return;
|
|
237
|
+
event.preventDefault();
|
|
238
|
+
fit.setPointerCapture?.(event.pointerId);
|
|
239
|
+
frame.classList.add('moving');
|
|
240
|
+
follow(event);
|
|
241
|
+
const move = (next) => follow(next);
|
|
242
|
+
const up = () => {
|
|
243
|
+
fit.removeEventListener('pointermove', move);
|
|
244
|
+
frame.classList.remove('moving');
|
|
245
|
+
void settle();
|
|
246
|
+
};
|
|
247
|
+
fit.addEventListener('pointermove', move);
|
|
248
|
+
fit.addEventListener('pointerup', up, { once: true });
|
|
249
|
+
fit.addEventListener('pointercancel', up, { once: true });
|
|
250
|
+
});
|
|
251
|
+
view.addEventListener('keydown', (event) => {
|
|
252
|
+
const axis = free();
|
|
253
|
+
const step = { ArrowLeft: -5, ArrowUp: -5, ArrowRight: 5, ArrowDown: 5 }[event.key];
|
|
254
|
+
if (!axis || step === undefined) return;
|
|
255
|
+
event.preventDefault();
|
|
256
|
+
focus[axis] = clamp(focus[axis] + step);
|
|
257
|
+
place();
|
|
258
|
+
live();
|
|
259
|
+
void settle();
|
|
260
|
+
});
|
|
261
|
+
picture.addEventListener('load', place, { once: true });
|
|
262
|
+
placeFrame = place;
|
|
263
|
+
place();
|
|
264
|
+
const acts = node('div', 'wall-tools-acts');
|
|
265
|
+
const change = button(t('Change picture'), () => void addPicture({ busy: change, chosen: (kept) => changed(kept, { reframed: false }), failed }));
|
|
266
|
+
const remove = button(t('Remove'), async () => {
|
|
267
|
+
remove.disabled = true;
|
|
268
|
+
await dropWallpaper();
|
|
269
|
+
paintCustomWall(null);
|
|
270
|
+
removed();
|
|
271
|
+
});
|
|
272
|
+
acts.append(change, remove);
|
|
273
|
+
box.append(head, frame, acts);
|
|
274
|
+
return box;
|
|
275
|
+
}
|
package/src/adapters/claude.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 { runReadonlyProcess } from './process.mjs';
|
|
2
4
|
|
|
3
5
|
// dontAsk denies any tool use that is not pre-approved, so under a lease the
|
package/src/adapters/codex.mjs
CHANGED