@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
|
@@ -0,0 +1,114 @@
|
|
|
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
|
+
|
|
4
|
+
// Who made MADRE (2026-10-15). This file is what term 1 of ADDITIONAL-TERMS.md asks every copy and
|
|
5
|
+
// every work based on MADRE to keep showing: the corner of every project and of Home, «MADRE
|
|
6
|
+
// <version> · © 2026 Jossue Alcalá» linked to its author, and the same attribution in the
|
|
7
|
+
// Appropriate Legal Notices (Settings › Updates and errors › License and authorship). One source
|
|
8
|
+
// for both pages, so the words cannot drift from NOTICE and the terms (test/attribution.test.mjs).
|
|
9
|
+
|
|
10
|
+
import { language, t } from './i18n.js';
|
|
11
|
+
|
|
12
|
+
export const AUTHOR = 'Jossue Alcalá';
|
|
13
|
+
export const AUTHOR_URL = 'https://jossuealcala.com';
|
|
14
|
+
export const YEAR = 2026;
|
|
15
|
+
export const LICENSE_NAME = 'GNU AGPL v3';
|
|
16
|
+
const REPO = 'https://github.com/jossuealcacao-exe/madre';
|
|
17
|
+
export const LEGAL_LINKS = {
|
|
18
|
+
license: `${REPO}/blob/main/LICENSE`,
|
|
19
|
+
terms: `${REPO}/blob/main/ADDITIONAL-TERMS.md`,
|
|
20
|
+
source: REPO,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function drawAttribution(host, version) {
|
|
24
|
+
if (!host || !version) return null;
|
|
25
|
+
const link = document.createElement('a');
|
|
26
|
+
link.className = 'colophon-link';
|
|
27
|
+
link.href = AUTHOR_URL;
|
|
28
|
+
link.target = '_blank';
|
|
29
|
+
link.rel = 'noopener';
|
|
30
|
+
const name = document.createElement('b');
|
|
31
|
+
name.textContent = `MADRE ${version}`;
|
|
32
|
+
link.append(name, document.createTextNode(` · © ${YEAR} ${AUTHOR}`));
|
|
33
|
+
host.replaceChildren(link);
|
|
34
|
+
host.hidden = false;
|
|
35
|
+
return link;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The keys modules put beside the version (src/modules/sdk.mjs, `footer`, 2026-10-18): drawn here,
|
|
39
|
+
// with MADRE's own icons, so a module never reaches into the page. Each one opens its address in a
|
|
40
|
+
// new tab; hovered or focused, it shows the module's words and the key that goes there. Asked again
|
|
41
|
+
// whenever the modules change.
|
|
42
|
+
const FOOTER_GLYPHS = {
|
|
43
|
+
heart: '<path d="M10 16.4s-6-3.5-6-7.9A3.3 3.3 0 0 1 10 6.6a3.3 3.3 0 0 1 6 1.9c0 4.4-6 7.9-6 7.9z" fill="currentColor"/>',
|
|
44
|
+
star: '<path d="M10 3.4l2 4.2 4.6.6-3.4 3.2.9 4.6L10 13.8 5.9 16l.9-4.6-3.4-3.2 4.6-.6z" fill="currentColor"/>',
|
|
45
|
+
link: '<path d="M8.6 11.4l2.8-2.8M9 5.8l1.5-1.5a3 3 0 0 1 4.2 4.2L13.2 10M11 14.2l-1.5 1.5a3 3 0 0 1-4.2-4.2L6.8 10" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>',
|
|
46
|
+
};
|
|
47
|
+
const footerWords = (value) => (value == null ? '' : typeof value === 'string' ? value : value[language()] ?? value.en ?? value.es ?? '');
|
|
48
|
+
export async function drawColophonKeys(host) {
|
|
49
|
+
if (!host) return null;
|
|
50
|
+
const answer = await fetch('/api/modules/footer', { cache: 'no-store' }).then((response) => (response.ok ? response.json() : null)).catch(() => null);
|
|
51
|
+
host.querySelector('.colophon-keys')?.remove();
|
|
52
|
+
const keys = (answer?.keys ?? []).flatMap((module) => (module.footer ?? []).map((key) => ({ ...key, module: module.id })));
|
|
53
|
+
if (!keys.length) return null;
|
|
54
|
+
const make = (tag, className, text) => { const node = document.createElement(tag); if (className) node.className = className; if (text != null) node.textContent = text; return node; };
|
|
55
|
+
const box = make('span', 'colophon-keys');
|
|
56
|
+
for (const key of keys) {
|
|
57
|
+
const label = footerWords(key.label);
|
|
58
|
+
const one = make('span', `colophon-key ${key.icon}`);
|
|
59
|
+
const press = make('a', 'colophon-key-mark');
|
|
60
|
+
press.href = key.url;
|
|
61
|
+
press.target = '_blank';
|
|
62
|
+
press.rel = 'noopener';
|
|
63
|
+
press.setAttribute('aria-label', label);
|
|
64
|
+
press.innerHTML = `<svg viewBox="0 0 20 20" aria-hidden="true">${FOOTER_GLYPHS[key.icon] ?? FOOTER_GLYPHS.link}</svg>`; // trusted: our own SVG strings
|
|
65
|
+
const card = make('span', 'colophon-card');
|
|
66
|
+
card.setAttribute('role', 'tooltip');
|
|
67
|
+
card.append(make('b', null, label));
|
|
68
|
+
if (footerWords(key.text)) card.append(make('span', null, footerWords(key.text)));
|
|
69
|
+
const go = make('a', 'colophon-card-go', footerWords(key.action) || label);
|
|
70
|
+
go.href = key.url;
|
|
71
|
+
go.target = '_blank';
|
|
72
|
+
go.rel = 'noopener';
|
|
73
|
+
card.append(go);
|
|
74
|
+
one.append(press, card);
|
|
75
|
+
box.append(one);
|
|
76
|
+
}
|
|
77
|
+
host.append(box);
|
|
78
|
+
return box;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// The Appropriate Legal Notices (AGPL v3, section 0 and 5(d)): the copyright, that there is no
|
|
82
|
+
// warranty, that it may be shared under the License, how to read it — and, by term 1(b) of the
|
|
83
|
+
// additional terms, the author's attribution. Built with the Settings blocks both pages share.
|
|
84
|
+
export function drawLegal(host) {
|
|
85
|
+
if (!host) return null;
|
|
86
|
+
const make = (tag, className, text) => { const node = document.createElement(tag); if (className) node.className = className; if (text != null) node.textContent = text; return node; };
|
|
87
|
+
const block = make('section', 'sx-block sx-legal');
|
|
88
|
+
const head = make('header', 'sx-head');
|
|
89
|
+
const words = make('div', 'sx-words');
|
|
90
|
+
words.append(make('h3', null, t('License and authorship')), make('p', null, t('© {year} {author}. MADRE is free software: you can share it and change it under the {license}, with additional terms that keep its author\'s attribution visible. It comes with no warranty of any kind.', { year: YEAR, author: AUTHOR, license: LICENSE_NAME })));
|
|
91
|
+
head.append(words);
|
|
92
|
+
const rows = make('div', 'sx-rows');
|
|
93
|
+
const row = (label, hint, url, words) => {
|
|
94
|
+
const line = make('div', 'sx-row');
|
|
95
|
+
const text = make('div', 'sx-words');
|
|
96
|
+
text.append(make('b', null, label));
|
|
97
|
+
if (hint) text.append(make('span', 'sx-hint', hint));
|
|
98
|
+
const box = make('div', 'sx-control');
|
|
99
|
+
const link = make('a', 'sx-btn', words);
|
|
100
|
+
link.href = url;
|
|
101
|
+
link.target = '_blank';
|
|
102
|
+
link.rel = 'noopener';
|
|
103
|
+
box.append(link);
|
|
104
|
+
line.append(text, box);
|
|
105
|
+
rows.append(line);
|
|
106
|
+
};
|
|
107
|
+
row(t('Made by {author}', { author: AUTHOR }), t('Every copy and every version based on MADRE keeps this attribution visible.'), AUTHOR_URL, 'jossuealcala.com');
|
|
108
|
+
row(t('The license'), LICENSE_NAME, LEGAL_LINKS.license, t('Read'));
|
|
109
|
+
row(t('The additional terms'), t('Author attribution, origin and trademarks (Section 7).'), LEGAL_LINKS.terms, t('Read'));
|
|
110
|
+
row(t('The source code'), t('Whoever offers a modified MADRE must offer its source code too.'), LEGAL_LINKS.source, 'GitHub');
|
|
111
|
+
block.append(head, rows);
|
|
112
|
+
host.append(block);
|
|
113
|
+
return block;
|
|
114
|
+
}
|
package/public/brands.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
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
|
// Visual identity per agent.
|
|
2
4
|
//
|
|
3
5
|
// Colors follow each vendor's public palette so a Codex bubble reads as Codex
|
|
4
|
-
// and a Claude bubble as Claude. The marks are
|
|
6
|
+
// and a Claude bubble as Claude. The marks are drawn by MADRE: free takes that
|
|
7
|
+
// recall each vendor's mark without reproducing it (2026-10-17), because the
|
|
5
8
|
// vendors' logos are registered trademarks and their brand guidelines do not
|
|
6
9
|
// allow embedding them in third-party products without permission. To use an
|
|
7
10
|
// official asset under a license you hold, set `mark` to that inline SVG.
|
|
@@ -18,7 +21,8 @@ export const BRANDS = {
|
|
|
18
21
|
color: '#10A37F',
|
|
19
22
|
colorLight: '#0B7A5F',
|
|
20
23
|
ink: '#FFFFFF',
|
|
21
|
-
|
|
24
|
+
// A free take on OpenAI's blossom (2026-10-17): three rounded links turned 60° and knotted.
|
|
25
|
+
mark: () => glyph('<g fill="none" stroke="currentColor" stroke-width="2.4"><rect x="11.2" y="4.4" width="9.6" height="23.2" rx="4.8"/><rect x="11.2" y="4.4" width="9.6" height="23.2" rx="4.8" transform="rotate(60 16 16)"/><rect x="11.2" y="4.4" width="9.6" height="23.2" rx="4.8" transform="rotate(120 16 16)"/></g>'),
|
|
22
26
|
},
|
|
23
27
|
claude: {
|
|
24
28
|
label: 'Claude',
|
|
@@ -26,7 +30,8 @@ export const BRANDS = {
|
|
|
26
30
|
color: '#D97757',
|
|
27
31
|
colorLight: '#B85C3C',
|
|
28
32
|
ink: '#FFFFFF',
|
|
29
|
-
|
|
33
|
+
// A free take on Claude's spark (2026-10-17): ten tapered rays of uneven length round a heart.
|
|
34
|
+
mark: () => glyph('<circle cx="16" cy="16" r="3" fill="currentColor"/><path fill="currentColor" d="M15.5 14.48L16.4 2.61L17.4 2.07L18.27 2.79L16.8 14.61ZM16.49 14.48L22.85 7.14L23.85 7.19L24.02 8.18L17.46 15.34ZM17.29 15.05L28.29 12.41L29.09 13.15L28.67 14.16L17.57 16.33ZM17.6 15.99L25.98 19.58L26.25 20.51L25.38 20.93L17.07 17.19ZM17.3 16.93L23.66 27.24L23.16 28.26L22.02 28.2L16.17 17.59ZM16.5 17.52L15.65 27.59L14.78 28.14L14.04 27.43L15.2 17.39ZM15.51 17.52L8.41 25.81L7.35 25.7L7.12 24.65L14.54 16.66ZM14.71 16.95L6.02 18.92L5.25 18.34L5.71 17.49L14.43 15.67ZM14.4 16.01L3.57 11.55L3.37 10.45L4.32 9.86L14.93 14.81ZM14.7 15.07L9.8 6.91L10.15 5.98L11.13 6.14L15.83 14.41Z"/>'),
|
|
30
35
|
},
|
|
31
36
|
gemini: {
|
|
32
37
|
label: 'Gemini',
|
|
@@ -35,7 +40,8 @@ export const BRANDS = {
|
|
|
35
40
|
colorLight: '#5B45B8',
|
|
36
41
|
ink: '#FFFFFF',
|
|
37
42
|
gradient: 'linear-gradient(135deg, #4796E3 0%, #8E75E8 55%, #D96570 100%)',
|
|
38
|
-
|
|
43
|
+
// A free take on Gemini's sparkle (2026-10-17): four points in long curves, taller than wide.
|
|
44
|
+
mark: () => glyph('<path fill="currentColor" d="M16 2.4C16.8 10.2 21.4 15 29 16 21.4 17 16.8 21.8 16 29.6 15.2 21.8 10.6 17 3 16 10.6 15 15.2 10.2 16 2.4z"/>'),
|
|
39
45
|
},
|
|
40
46
|
opencode: {
|
|
41
47
|
label: 'OpenCode',
|
|
@@ -43,7 +49,18 @@ export const BRANDS = {
|
|
|
43
49
|
color: '#F2C94C',
|
|
44
50
|
colorLight: '#9A7A12',
|
|
45
51
|
ink: '#111111',
|
|
46
|
-
|
|
52
|
+
// A free take on OpenCode's block letter (2026-10-17): a square ring with a lighter block inside.
|
|
53
|
+
mark: () => glyph('<path fill="currentColor" fill-rule="evenodd" d="M6 6h20v20H6zM10.4 10.4v11.2h11.2V10.4z"/><path fill="currentColor" opacity=".5" d="M10.4 15.2h11.2v6.4H10.4z"/>'),
|
|
54
|
+
},
|
|
55
|
+
// MADRE's own drawing for Cursor (2026-10-17), a free take on its mark rather than the mark: a
|
|
56
|
+
// cube seen from above, its three faces in three weights, and a pointer cut out of the right face.
|
|
57
|
+
cursor: {
|
|
58
|
+
label: 'Cursor',
|
|
59
|
+
vendor: 'Cursor',
|
|
60
|
+
color: '#D9DEE5',
|
|
61
|
+
colorLight: '#3A3F47',
|
|
62
|
+
ink: '#111111',
|
|
63
|
+
mark: () => glyph('<path fill="currentColor" d="M16 3.2 27.4 9.6 16 16 4.6 9.6z"/><path fill="currentColor" opacity=".42" d="M4.6 9.6 16 16v12.8L4.6 22.4z"/><path fill="currentColor" fill-rule="evenodd" opacity=".7" d="M27.4 9.6v12.8L16 28.8V16zM16 16l11.4-6.4-6.2 12-1.9-3.9z"/>'),
|
|
47
64
|
},
|
|
48
65
|
};
|
|
49
66
|
|
|
@@ -72,7 +89,7 @@ BRANDS.ollama = {
|
|
|
72
89
|
// the same hue lifted; on a light ground the plum itself reads.
|
|
73
90
|
BRANDS.bishop = {
|
|
74
91
|
label: 'Coordinator',
|
|
75
|
-
vendor: '
|
|
92
|
+
vendor: 'Home',
|
|
76
93
|
color: '#D598D7',
|
|
77
94
|
colorLight: '#5C255E',
|
|
78
95
|
solid: '#5C255E',
|
|
@@ -0,0 +1,143 @@
|
|
|
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 code workspace's arithmetic (1.0.3), kept apart from the page so it can be tested on its own:
|
|
4
|
+
// what an agent changed in a file line by line, where each of those changes stands in the file as
|
|
5
|
+
// it is now, what the file reads like with one of them taken back, and which function a line is in.
|
|
6
|
+
// Nothing here touches the page, the network or the disk.
|
|
7
|
+
|
|
8
|
+
const lines = (text) => String(text ?? '').replace(/\r\n/g, '\n').split('\n');
|
|
9
|
+
|
|
10
|
+
// The changes between two texts as runs of lines: each says where it starts on each side and what
|
|
11
|
+
// it removed and put. The common start and end are set aside first; what is left is compared line
|
|
12
|
+
// by line (longest common subsequence). Past `maxCells` comparisons the middle is one change: a
|
|
13
|
+
// file rewritten that much is read as rewritten, not taken apart.
|
|
14
|
+
export function lineDiff(beforeText, afterText, { maxCells = 2_000_000 } = {}) {
|
|
15
|
+
const a = lines(beforeText);
|
|
16
|
+
const b = lines(afterText);
|
|
17
|
+
let start = 0;
|
|
18
|
+
while (start < a.length && start < b.length && a[start] === b[start]) start += 1;
|
|
19
|
+
let endA = a.length;
|
|
20
|
+
let endB = b.length;
|
|
21
|
+
while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) { endA -= 1; endB -= 1; }
|
|
22
|
+
const A = a.slice(start, endA);
|
|
23
|
+
const B = b.slice(start, endB);
|
|
24
|
+
if (!A.length && !B.length) return [];
|
|
25
|
+
if (A.length * B.length > maxCells) return [{ beforeStart: start, before: A, afterStart: start, after: B }];
|
|
26
|
+
const n = A.length;
|
|
27
|
+
const m = B.length;
|
|
28
|
+
const width = m + 1;
|
|
29
|
+
const table = new Uint32Array((n + 1) * width);
|
|
30
|
+
for (let i = n - 1; i >= 0; i -= 1) {
|
|
31
|
+
for (let j = m - 1; j >= 0; j -= 1) {
|
|
32
|
+
table[i * width + j] = A[i] === B[j] ? table[(i + 1) * width + j + 1] + 1 : Math.max(table[(i + 1) * width + j], table[i * width + j + 1]);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const hunks = [];
|
|
36
|
+
let open = null;
|
|
37
|
+
let i = 0;
|
|
38
|
+
let j = 0;
|
|
39
|
+
while (i < n || j < m) {
|
|
40
|
+
if (i < n && j < m && A[i] === B[j]) { if (open) { hunks.push(open); open = null; } i += 1; j += 1; continue; }
|
|
41
|
+
if (!open) open = { beforeStart: start + i, before: [], afterStart: start + j, after: [] };
|
|
42
|
+
if (j < m && (i === n || table[i * width + j + 1] >= table[(i + 1) * width + j])) { open.after.push(B[j]); j += 1; }
|
|
43
|
+
else { open.before.push(A[i]); i += 1; }
|
|
44
|
+
}
|
|
45
|
+
if (open) hunks.push(open);
|
|
46
|
+
return hunks;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Where each change of an agent stands in the file as it is now. Untouched since the turn, a change
|
|
50
|
+
// is where the turn left it. Edited since, it is found by its own lines, nearest to where it was; a
|
|
51
|
+
// change whose lines are no longer there (the human rewrote them) is no longer the agent's, and is
|
|
52
|
+
// left out. A change that only removed lines is found by the line the turn left just before it.
|
|
53
|
+
export function placeHunks(currentText, afterText, hunks) {
|
|
54
|
+
const now = lines(currentText);
|
|
55
|
+
if (afterText === null || afterText === undefined || String(afterText).replace(/\r\n/g, '\n') === now.join('\n')) return hunks.map((hunk) => ({ ...hunk, at: hunk.afterStart }));
|
|
56
|
+
const after = lines(afterText);
|
|
57
|
+
const out = [];
|
|
58
|
+
for (const hunk of hunks) {
|
|
59
|
+
let at = -1;
|
|
60
|
+
if (hunk.after.length) at = nearest(now, hunk.after, hunk.afterStart);
|
|
61
|
+
else {
|
|
62
|
+
const before = hunk.afterStart > 0 ? after[hunk.afterStart - 1] : null;
|
|
63
|
+
if (before === null) at = now.length && now[0] === after[0] ? 0 : -1;
|
|
64
|
+
else { const found = nearest(now, [before], hunk.afterStart - 1); at = found < 0 ? -1 : found + 1; }
|
|
65
|
+
}
|
|
66
|
+
if (at >= 0) out.push({ ...hunk, at });
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
function nearest(haystack, block, hint) {
|
|
71
|
+
let best = -1;
|
|
72
|
+
for (let k = 0; k + block.length <= haystack.length; k += 1) {
|
|
73
|
+
let same = true;
|
|
74
|
+
for (let x = 0; x < block.length; x += 1) if (haystack[k + x] !== block[x]) { same = false; break; }
|
|
75
|
+
if (same && (best < 0 || Math.abs(k - hint) < Math.abs(best - hint))) best = k;
|
|
76
|
+
}
|
|
77
|
+
return best;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// The file with one change taken back: the lines the agent put, where they stand now, become the
|
|
81
|
+
// lines it removed. Everything else stays byte for byte, line endings included.
|
|
82
|
+
export function revertHunk(currentText, hunk) {
|
|
83
|
+
const text = String(currentText ?? '');
|
|
84
|
+
const crlf = /\r\n/.test(text);
|
|
85
|
+
const now = lines(text);
|
|
86
|
+
now.splice(hunk.at, hunk.after.length, ...hunk.before);
|
|
87
|
+
return now.join(crlf ? '\r\n' : '\n');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// A change named by what it says, so «kept» survives a reload and never sticks to another change.
|
|
91
|
+
export function hunkKey(hunk) {
|
|
92
|
+
let hash = 0x811c9dc5;
|
|
93
|
+
const said = `${hunk.before.join('\n')}\u0000${hunk.after.join('\n')}`;
|
|
94
|
+
for (let i = 0; i < said.length; i += 1) {
|
|
95
|
+
hash ^= said.charCodeAt(i);
|
|
96
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
97
|
+
}
|
|
98
|
+
return hash.toString(16).padStart(8, '0');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// The function, class or heading a line belongs to, for the path above the code: the nearest line
|
|
102
|
+
// above it that opens one and is indented no deeper. `null` when nothing encloses it.
|
|
103
|
+
const OPENS = {
|
|
104
|
+
js: [
|
|
105
|
+
/^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s*\*?\s*([A-Za-z_$][\w$]*)/,
|
|
106
|
+
/^\s*(?:export\s+)?(?:default\s+)?class\s+([A-Za-z_$][\w$]*)/,
|
|
107
|
+
/^\s*(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?(?:function\b|\([^)]*\)\s*=>|[A-Za-z_$][\w$]*\s*=>)/,
|
|
108
|
+
/^\s*(?:static\s+)?(?:async\s+)?(?:get\s+|set\s+)?(#?[A-Za-z_$][\w$]*)\s*\([^)]*\)\s*\{\s*$/,
|
|
109
|
+
],
|
|
110
|
+
py: [/^\s*(?:async\s+)?def\s+([A-Za-z_]\w*)/, /^\s*class\s+([A-Za-z_]\w*)/],
|
|
111
|
+
css: [/^\s*([^{}@\s][^{}]*?)\s*\{\s*$/, /^\s*(@media[^{]*?)\s*\{/],
|
|
112
|
+
sh: [/^\s*(?:function\s+)?([A-Za-z_][\w-]*)\s*\(\)\s*\{?/],
|
|
113
|
+
};
|
|
114
|
+
const NOT_NAMES = new Set(['if', 'for', 'while', 'switch', 'catch', 'function', 'return', 'else', 'do', 'try']);
|
|
115
|
+
export function symbolAt(textOrLines, index, familyId) {
|
|
116
|
+
const all = Array.isArray(textOrLines) ? textOrLines : lines(textOrLines);
|
|
117
|
+
if (index < 0 || index >= all.length) return null;
|
|
118
|
+
if (familyId === 'md') {
|
|
119
|
+
for (let k = index; k >= 0; k -= 1) { const heading = all[k].match(/^#{1,6}\s+(.+?)\s*#*\s*$/); if (heading) return heading[1]; }
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
const patterns = OPENS[familyId];
|
|
123
|
+
if (!patterns) return null;
|
|
124
|
+
const depth = (line) => line.match(/^\s*/)[0].replace(/\t/g, ' ').length;
|
|
125
|
+
let limit = all[index].trim() ? depth(all[index]) : Infinity;
|
|
126
|
+
// A block closed above the line (a sibling function that ended) is skipped whole: its closing
|
|
127
|
+
// line is counted at its depth, and the opening line at that depth that matches it is passed over.
|
|
128
|
+
const closed = new Map();
|
|
129
|
+
for (let k = index; k >= 0 && index - k < 4000; k -= 1) {
|
|
130
|
+
const line = all[k];
|
|
131
|
+
const said = line.trim();
|
|
132
|
+
if (!said) continue;
|
|
133
|
+
const indent = depth(line);
|
|
134
|
+
if (k !== index && indent > limit) continue;
|
|
135
|
+
if (k !== index && /^[}\])]/.test(said) && !/[{([]\s*$/.test(said)) { closed.set(indent, (closed.get(indent) ?? 0) + 1); continue; }
|
|
136
|
+
const name = patterns.map((pattern) => said && line.match(pattern)?.[1]).find(Boolean) ?? null;
|
|
137
|
+
const opens = Boolean(name) || /[{([]\s*$/.test(said) || (familyId === 'py' && /:\s*$/.test(said));
|
|
138
|
+
if (k !== index && opens && (closed.get(indent) ?? 0) > 0) { closed.set(indent, closed.get(indent) - 1); continue; }
|
|
139
|
+
if (name && !NOT_NAMES.has(name)) return name.trim().slice(0, 80);
|
|
140
|
+
if (k !== index) limit = Math.min(limit, indent);
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
package/public/editor.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 editor: a file of the project, written by the human's own hand.
|
|
2
4
|
//
|
|
3
5
|
// A textarea does the typing — the caret, the selection, IME, spellcheck off, the browser's own
|
|
@@ -33,6 +35,8 @@ const ICONS = {
|
|
|
33
35
|
wrap: svg('M3.5 5.5h13M3.5 10h10a2.6 2.6 0 0 1 0 5.2H10M11.8 13.3 10 15.2l1.8 1.8M3.5 15h3.5'),
|
|
34
36
|
ask: svg('M4 5.6A2.6 2.6 0 0 1 6.6 3h6.8A2.6 2.6 0 0 1 16 5.6v4.8A2.6 2.6 0 0 1 13.4 13H9.2L5.6 16v-3A2.6 2.6 0 0 1 4 10.4z'),
|
|
35
37
|
link: svg('M8.3 11.7l3.4-3.4M7.2 13.2 6 14.4a2.6 2.6 0 0 1-3.7-3.7l2.6-2.6a2.6 2.6 0 0 1 3.7 0M12.8 6.8 14 5.6a2.6 2.6 0 0 1 3.7 3.7l-2.6 2.6a2.6 2.6 0 0 1-3.7 0'),
|
|
38
|
+
// The room's chooser chevron (app.js, ICON_CHEVRON), drawn on this grid: a menu opens under it.
|
|
39
|
+
chevron: svg('M5.25 7.75 10 12.5l4.75-4.75'),
|
|
36
40
|
};
|
|
37
41
|
|
|
38
42
|
// Spaces or tabs, and how many: what the file already does, not what the editor prefers.
|
|
@@ -102,7 +106,11 @@ export function findAll(text, query, { caseSensitive = false, regex = false, wor
|
|
|
102
106
|
return found;
|
|
103
107
|
}
|
|
104
108
|
|
|
105
|
-
|
|
109
|
+
// `draft`: what the human had written and not saved, kept by the room while another file was on
|
|
110
|
+
// screen (the code workspace's tabs); the file on disk stays `text`, so the editor knows it is unsaved.
|
|
111
|
+
// `onCaret(line)`: the line the caret is on, whenever it moves. `setMarks` paints lines in the margin
|
|
112
|
+
// (an agent's changes, in its colour) without touching the text.
|
|
113
|
+
export function createEditor({ text = '', draft = null, name = '', family = null, tokenize = null, t = (s) => s, fill = null, onSave = null, onAsk = null, onDirty = null, onCaret = null }) {
|
|
106
114
|
const say = (key, vars) => (vars && fill ? fill(t(key), vars) : t(key));
|
|
107
115
|
const crlf = /\r\n/.test(text);
|
|
108
116
|
const indent = detectIndent(text);
|
|
@@ -153,7 +161,7 @@ export function createEditor({ text = '', name = '', family = null, tokenize = n
|
|
|
153
161
|
<div class="ed-code">
|
|
154
162
|
<div class="ed-active" aria-hidden="true"></div>
|
|
155
163
|
<div class="ed-float" hidden>
|
|
156
|
-
<button type="button" class="ed-chip ask" data-float="ask"><i class="ed-ico">${ICONS.ask}</i><span></span><
|
|
164
|
+
<button type="button" class="ed-chip ask" data-float="ask"><i class="ed-ico">${ICONS.ask}</i><span></span><i class="ed-chev">${ICONS.chevron}</i></button>
|
|
157
165
|
<button type="button" class="ed-chip" data-float="comment"><i class="ed-ico">${ICONS.comment}</i><span></span></button>
|
|
158
166
|
<button type="button" class="ed-chip" data-float="copy"><i class="ed-ico">${ICONS.link}</i><span></span></button>
|
|
159
167
|
</div>
|
|
@@ -200,7 +208,7 @@ export function createEditor({ text = '', name = '', family = null, tokenize = n
|
|
|
200
208
|
$('.ed-indent').textContent = indent.tabs ? t('Tabs') : say('Spaces: {n}', { n: indent.size });
|
|
201
209
|
$('.ed-eol').textContent = crlf ? 'CRLF' : 'LF';
|
|
202
210
|
$('.ed-lang').textContent = family?.label ?? t('Plain text');
|
|
203
|
-
input.value = saved;
|
|
211
|
+
input.value = draft === null || draft === undefined ? saved : String(draft).replace(/\r\n/g, '\n');
|
|
204
212
|
// A textarea given its text holds the caret after the last character; the first focus would jump
|
|
205
213
|
// there. Editing starts where reading did: at the top, or at the line asked for.
|
|
206
214
|
input.setSelectionRange?.(0, 0);
|
|
@@ -240,6 +248,8 @@ export function createEditor({ text = '', name = '', family = null, tokenize = n
|
|
|
240
248
|
return html;
|
|
241
249
|
}
|
|
242
250
|
let lineCount = 1;
|
|
251
|
+
let lastCaret = 0;
|
|
252
|
+
let marks = new Map();
|
|
243
253
|
function countLines() { return lineCount; }
|
|
244
254
|
let frame = 0;
|
|
245
255
|
function draw() {
|
|
@@ -262,7 +272,8 @@ export function createEditor({ text = '', name = '', family = null, tokenize = n
|
|
|
262
272
|
const here = [];
|
|
263
273
|
for (let j = markAt; j < matches.length && matches[j].start < end; j += 1) here.push({ ...matches[j], cur: j === current });
|
|
264
274
|
html.push(`<div class="l">${drawLine(line, carry, offset, here) || ' '}</div>`);
|
|
265
|
-
|
|
275
|
+
const mark = marks.get(i + 1);
|
|
276
|
+
numbers.push(mark ? `<div class="mk" style="--mk:${mark.color}" title="${escape(mark.title ?? '')}">${i + 1}</div>` : `<div>${i + 1}</div>`);
|
|
266
277
|
offset = end + 1;
|
|
267
278
|
}
|
|
268
279
|
hl.innerHTML = html.join('');
|
|
@@ -322,6 +333,7 @@ export function createEditor({ text = '', name = '', family = null, tokenize = n
|
|
|
322
333
|
const line = caretLine(at);
|
|
323
334
|
const col = at - (value.lastIndexOf('\n', at - 1) + 1) + 1;
|
|
324
335
|
$('.ed-pos').textContent = say('Ln {line}, Col {col}', { line, col });
|
|
336
|
+
if (line !== lastCaret) { lastCaret = line; onCaret?.(line); }
|
|
325
337
|
const chosen = input.selectionEnd - input.selectionStart;
|
|
326
338
|
$('.ed-sel').textContent = chosen ? say('{n} selected', { n: chosen }) : '';
|
|
327
339
|
const dirty = isDirty();
|
|
@@ -738,6 +750,12 @@ export function createEditor({ text = '', name = '', family = null, tokenize = n
|
|
|
738
750
|
save,
|
|
739
751
|
// The file on disk changed under the editor and the human chose the disk's version.
|
|
740
752
|
reset(next) { saved = String(next).replace(/\r\n/g, '\n'); input.value = saved; changed(); },
|
|
753
|
+
// Lines to mark in the margin: [{ from, to, color, title }], one-based. An empty list clears them.
|
|
754
|
+
setMarks(list = []) {
|
|
755
|
+
marks = new Map();
|
|
756
|
+
for (const one of list) for (let n = one.from; n <= one.to; n += 1) marks.set(n, { color: /^[#\w(),.%\s-]{1,40}$/.test(one.color ?? '') ? one.color : 'currentColor', title: one.title ?? '' });
|
|
757
|
+
draw();
|
|
758
|
+
},
|
|
741
759
|
// Taken off the page: what it listened to outside itself goes with it.
|
|
742
760
|
destroy() { document.removeEventListener('selectionchange', followSelection); resized?.disconnect(); cancelAnimationFrame(frame); },
|
|
743
761
|
};
|