@jossuealcala/madre 0.3.3 → 0.4.0
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.md +412 -3
- package/CONTRIBUTING.md +3 -1
- package/README.md +67 -185
- package/SECURITY.md +2 -1
- package/bin/madre.mjs +56 -13
- package/docs/INTERNALS.md +16 -0
- package/docs/REFERENCE.md +249 -0
- package/docs/SDK.md +121 -0
- package/docs/room.png +0 -0
- package/docs/sdk/hello-module.mjs +51 -0
- package/package.json +9 -1
- package/public/app.js +3825 -851
- package/public/es.js +2050 -0
- package/public/i18n.js +66 -0
- package/public/index.html +94 -13
- package/public/inquiry.js +220 -0
- package/public/resay.js +77 -0
- package/public/styles.css +602 -62
- package/public/troubleshooting.js +168 -46
- package/src/adapters/claude.mjs +2 -1
- package/src/adapters/codex.mjs +2 -1
- package/src/adapters/gemini.mjs +6 -5
- package/src/adapters/opencode.mjs +2 -1
- package/src/adapters/process.mjs +17 -5
- package/src/asking.mjs +128 -0
- package/src/auth-probe.mjs +58 -1
- package/src/chats.mjs +193 -0
- package/src/checkpoint.mjs +1 -1
- package/src/cold.mjs +56 -0
- package/src/commands.mjs +6 -0
- package/src/conversation-context.mjs +35 -3
- package/src/credentials.mjs +145 -0
- package/src/dataset.mjs +56 -4
- package/src/distiller.mjs +12 -5
- package/src/event-store.mjs +14 -8
- package/src/exam.mjs +240 -0
- package/src/extensions.mjs +3 -2
- package/src/eyecat-watch.mjs +100 -0
- package/src/eyecat.mjs +169 -0
- package/src/i18n.mjs +47 -0
- package/src/image-studio.mjs +2 -0
- package/src/launch.mjs +61 -0
- package/src/maturity.mjs +94 -0
- package/src/mcp/image-server.mjs +12 -1
- package/src/mcp/memory-server.mjs +1 -1
- package/src/memory.mjs +325 -17
- package/src/modules/ahp.mjs +9 -7
- package/src/modules/ash.mjs +36 -0
- package/src/modules/git-pulse.mjs +5 -3
- package/src/modules/helpers.mjs +31 -0
- package/src/modules/image-studio.mjs +9 -4
- package/src/modules/index.mjs +141 -9
- package/src/modules/ollama.mjs +66 -10
- package/src/modules/playwright.mjs +36 -19
- package/src/modules/ripley.mjs +5 -3
- package/src/modules/sdk.mjs +93 -2
- package/src/modules/updates.mjs +81 -0
- package/src/ollama.mjs +5 -2
- package/src/outbound.mjs +292 -0
- package/src/privacy.mjs +54 -7
- package/src/room/context.mjs +4 -4
- package/src/room/economy.mjs +161 -0
- package/src/room/prompt.mjs +118 -46
- package/src/room.mjs +443 -44
- package/src/runtime-detection.mjs +27 -8
- package/src/server.mjs +699 -69
- package/src/setup.mjs +1 -1
- package/src/updates.mjs +4 -2
- package/src/usage-sentinel.mjs +13 -8
- package/src/verdict.mjs +74 -0
- package/src/ashcode.mjs +0 -64
- package/src/modules/ashcode.mjs +0 -28
package/public/i18n.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// The room's language.
|
|
2
|
+
//
|
|
3
|
+
// MADRE is written in Spanish and answers in English when it is asked to. That is the way round
|
|
4
|
+
// it is because of who works in it, and because a product that ships one language and calls the
|
|
5
|
+
// other a "locale" ends up with a second-class one.
|
|
6
|
+
//
|
|
7
|
+
// How it works, and why this way:
|
|
8
|
+
//
|
|
9
|
+
// The KEY IS THE ENGLISH SENTENCE. There are no invented ids to keep in step with the screens —
|
|
10
|
+
// `t('Type here, human. Ask the room…')` is what is written where the words are used, and the
|
|
11
|
+
// catalogue answers with the Spanish. A sentence nobody has translated yet comes back in
|
|
12
|
+
// English, whole, rather than as a hole or an id: half a translated screen is a screen, and a
|
|
13
|
+
// screen full of MISSING_KEY_47 is a bug report.
|
|
14
|
+
//
|
|
15
|
+
// What varies inside a sentence travels as a named slot, `t('{n} blocks', { n })`, so the
|
|
16
|
+
// Spanish can put it where Spanish puts it rather than where English did.
|
|
17
|
+
//
|
|
18
|
+
// And what is already DATA — the troubleshooting catalogue, the console's inquiries, the
|
|
19
|
+
// addresses in WHAT LEFT THIS MACHINE — is not dictionary work: those get a Spanish table beside
|
|
20
|
+
// the English one, through `pick()`, because translating a table key by key loses the table.
|
|
21
|
+
//
|
|
22
|
+
// Nothing here is a framework. It is a lookup, a fallback and a switch.
|
|
23
|
+
|
|
24
|
+
import { ES } from './es.js';
|
|
25
|
+
|
|
26
|
+
export const LANGUAGES = { es: { id: 'es', name: 'ESPAÑOL', other: 'en' }, en: { id: 'en', name: 'ENGLISH', other: 'es' } };
|
|
27
|
+
export const DEFAULT_LANGUAGE = 'es';
|
|
28
|
+
const CATALOGUES = { es: ES, en: {} };
|
|
29
|
+
|
|
30
|
+
let current = DEFAULT_LANGUAGE;
|
|
31
|
+
|
|
32
|
+
export const language = () => current;
|
|
33
|
+
export const isLanguage = (id) => Object.hasOwn(LANGUAGES, String(id ?? ''));
|
|
34
|
+
|
|
35
|
+
export function setLanguage(id) {
|
|
36
|
+
current = isLanguage(id) ? id : DEFAULT_LANGUAGE;
|
|
37
|
+
return current;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// `{name}` is filled from `vars` after the sentence is chosen, so the Spanish can put the number
|
|
41
|
+
// where Spanish puts it. A slot with nothing to fill it is left as it is rather than printed as
|
|
42
|
+
// "undefined": a visible brace is a bug you can see.
|
|
43
|
+
export function fill(text, vars) {
|
|
44
|
+
if (!vars) return text;
|
|
45
|
+
return String(text).replace(/\{(\w+)\}/g, (whole, name) => (Object.hasOwn(vars, name) ? String(vars[name]) : whole));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function t(text, vars = null) {
|
|
49
|
+
const said = CATALOGUES[current]?.[text] ?? text;
|
|
50
|
+
return fill(said, vars);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// A table that carries both languages: `{ en: …, es: … }` anywhere inside it. Whole objects,
|
|
54
|
+
// whole arrays — the shape is the table's own and this only chooses the side.
|
|
55
|
+
export function pick(value) {
|
|
56
|
+
if (Array.isArray(value)) return value.map(pick);
|
|
57
|
+
if (!value || typeof value !== 'object') return value;
|
|
58
|
+
if (Object.hasOwn(value, 'en') && Object.hasOwn(value, current)) return pick(value[current] ?? value.en);
|
|
59
|
+
const out = {};
|
|
60
|
+
for (const [key, inner] of Object.entries(value)) out[key] = pick(inner);
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// What the catalogue holds, for the test that keeps it honest: every key it has must be a
|
|
65
|
+
// sentence the product actually says, and every sentence the product asks for must be in it.
|
|
66
|
+
export const catalogue = (id = current) => CATALOGUES[id] ?? {};
|
package/public/index.html
CHANGED
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
<button id="mother-button" class="mother-button" type="button" title="Troubleshooting · MU/TH/UR" aria-haspopup="dialog">
|
|
34
34
|
<span class="mother-glyph" aria-hidden="true"></span>MU/TH/UR<span id="mother-count" class="mother-count" hidden></span>
|
|
35
35
|
</button>
|
|
36
|
+
<button id="lang-button" class="tree-button lang-button" type="button" data-language="es" aria-label="Language">EN</button>
|
|
36
37
|
<button id="theme-button" class="tree-button theme-button" type="button" title="Theme · auto (follows the system)" data-theme="auto" aria-label="Theme">
|
|
37
38
|
<svg class="t-auto" viewBox="0 0 20 20" aria-hidden="true"><circle cx="10" cy="10" r="6.2" fill="none" stroke="currentColor" stroke-width="1.7"/><path d="M10 3.8v12.4A6.2 6.2 0 0 0 10 3.8z" fill="currentColor"/></svg>
|
|
38
39
|
<svg class="t-light" viewBox="0 0 20 20" aria-hidden="true"><circle cx="10" cy="10" r="3.6" fill="none" stroke="currentColor" stroke-width="1.7"/><path d="M10 2.5v2M10 15.5v2M2.5 10h2M15.5 10h2M4.7 4.7l1.4 1.4M13.9 13.9l1.4 1.4M4.7 15.3l1.4-1.4M13.9 6.1l1.4-1.4" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>
|
|
@@ -42,6 +43,23 @@
|
|
|
42
43
|
<svg viewBox="0 0 20 20" aria-hidden="true"><rect x="2.75" y="3.75" width="14.5" height="12.5" rx="2.5" fill="none" stroke="currentColor" stroke-width="1.7"/><path d="M12 3.75v12.5" fill="none" stroke="currentColor" stroke-width="1.7"/><path d="M13.8 7h1.6M13.8 9.6h1.6" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" opacity=".7"/></svg>
|
|
43
44
|
</button>
|
|
44
45
|
</header>
|
|
46
|
+
<button id="chats-button" class="chats-button" type="button" aria-pressed="false" aria-controls="chats" title="Conversations in this project" aria-label="Conversations">
|
|
47
|
+
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M3.2 5.2a1.8 1.8 0 0 1 1.8-1.8h7.6a1.8 1.8 0 0 1 1.8 1.8v4.4a1.8 1.8 0 0 1-1.8 1.8H7.6L4.4 13.8v-2.4h-.4a1.8 1.8 0 0 1-1.8-1.8z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><path d="M8.6 14.2h3.1l3.2 2.4v-2.4h.4a1.8 1.8 0 0 0 1.8-1.8V8.6" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" opacity=".55"/></svg>
|
|
48
|
+
</button>
|
|
49
|
+
<aside id="chats" class="tree chats" aria-label="Conversations" hidden>
|
|
50
|
+
<header class="tree-head">
|
|
51
|
+
<span class="tree-title">CONVERSATIONS <span id="chats-count" class="tree-root"></span></span>
|
|
52
|
+
<button id="chats-close" class="tree-close" type="button" aria-label="Close conversations">×</button>
|
|
53
|
+
</header>
|
|
54
|
+
<div id="chats-body" class="tree-body chats-body"></div>
|
|
55
|
+
<div class="chats-new">
|
|
56
|
+
<button id="chats-new" class="chats-new-button" type="button" title="Start another conversation in this project. The memory stays.">
|
|
57
|
+
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 4.5v11M4.5 10h11" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>
|
|
58
|
+
NEW CONVERSATION
|
|
59
|
+
</button>
|
|
60
|
+
</div>
|
|
61
|
+
<footer class="chats-foot">ONE PROJECT, ONE MEMORY. EVERY CONVERSATION FEEDS THE SAME ARCHIVE.</footer>
|
|
62
|
+
</aside>
|
|
45
63
|
<aside id="tree" class="tree" aria-label="Project files" hidden>
|
|
46
64
|
<header class="tree-head">
|
|
47
65
|
<span class="tree-title">FILES <span id="tree-root" class="tree-root"></span></span>
|
|
@@ -50,16 +68,37 @@
|
|
|
50
68
|
<div id="tree-body" class="tree-body" role="tree"></div>
|
|
51
69
|
</aside>
|
|
52
70
|
|
|
71
|
+
<dialog id="core" class="mother core" aria-label="The core">
|
|
72
|
+
<div class="mother-frame">
|
|
73
|
+
<header class="mother-head">
|
|
74
|
+
<span class="mother-title">THE CORE</span>
|
|
75
|
+
<span class="mother-sub">MU/TH/UR 6000 · WHAT THIS ROOM SAYS IN YOUR NAME</span>
|
|
76
|
+
<span class="mother-actions">
|
|
77
|
+
<button id="core-copy" class="mother-close" type="button" title="Copy the whole briefing as the agent receives it">⧉ COPY ALL</button>
|
|
78
|
+
<button id="core-close" class="mother-close" type="button">CLOSE ×</button>
|
|
79
|
+
</span>
|
|
80
|
+
</header>
|
|
81
|
+
<p id="core-boot" class="mother-boot"></p>
|
|
82
|
+
<section id="core-body" class="mother-section core-body" hidden></section>
|
|
83
|
+
<footer class="mother-foot">BUILT NOW · STORED NOWHERE · SENT NOWHERE · READ, NEVER WRITTEN</footer>
|
|
84
|
+
</div>
|
|
85
|
+
</dialog>
|
|
86
|
+
|
|
53
87
|
<dialog id="modules" class="mother modules" aria-label="Modules">
|
|
54
88
|
<div class="mother-frame">
|
|
55
89
|
<header class="mother-head">
|
|
56
90
|
<span class="mother-title">MODULES</span>
|
|
57
91
|
<span class="mother-sub">MADRE · OPTIONAL INTEGRATIONS · INSTALLED BY THEIR OWN TOOLS</span>
|
|
58
|
-
<
|
|
92
|
+
<span class="mother-actions">
|
|
93
|
+
<button id="modules-add" class="mother-close add" type="button" title="Install a module somebody wrote. MADRE checks it before it runs.">+ ADD A MODULE</button>
|
|
94
|
+
<input id="modules-file" type="file" accept=".mjs,.js,text/javascript" hidden>
|
|
95
|
+
<button id="modules-close" class="mother-close" type="button">CLOSE ×</button>
|
|
96
|
+
</span>
|
|
59
97
|
</header>
|
|
60
98
|
<p class="mother-answer" id="modules-note">BUILT-IN MODULES CHANGE ONLY MADRE SETTINGS. EXTERNAL MODULES WRITE INTO THE PROJECT ONLY AFTER YOU CONFIRM.</p>
|
|
61
99
|
<section id="modules-list" class="mother-section"></section>
|
|
62
|
-
<
|
|
100
|
+
<section id="modules-dev" class="mother-section dev"></section>
|
|
101
|
+
<footer class="mother-foot">BUILT-INS STAY IN MADRE · PROJECT INSTALLS SHOW THE COMMAND FIRST · YOUR MODULES LOAD FROM ~/.pulse/modules</footer>
|
|
63
102
|
</div>
|
|
64
103
|
</dialog>
|
|
65
104
|
|
|
@@ -69,9 +108,11 @@
|
|
|
69
108
|
<span class="mother-title">MU/TH/UR 6000</span>
|
|
70
109
|
<span class="mother-sub">MADRE · INTERFACE 2037 · TROUBLESHOOTING</span>
|
|
71
110
|
<span class="mother-actions">
|
|
72
|
-
<button id="mother-settings-button" class="mother-close" type="button" aria-pressed="false" title="
|
|
111
|
+
<button id="mother-settings-button" class="mother-close" type="button" aria-pressed="false" title="The crew and this room's settings: install an agent, sign it in, set what each one may do">⚙ CONNECTIONS</button>
|
|
73
112
|
<button id="nostromo-button" class="mother-close nostromo-button" type="button" title="NOSTROMO · memory research · needs the project designation">◉ NOSTROMO</button>
|
|
74
113
|
<button id="feedback-button" class="mother-close" type="button" title="Tell MADRE's author what happened or what you wish for · opens a prefilled issue">✎ FEEDBACK</button>
|
|
114
|
+
<button id="fold-all" class="mother-close" type="button" title="Open or close every section of this panel">EXPAND ALL</button>
|
|
115
|
+
<button id="tour-button" class="mother-close tour-button" type="button" title="The four-step tour of MADRE" aria-label="Tour">?</button>
|
|
75
116
|
<button id="mother-close" class="mother-close" type="button">END SESSION ×</button>
|
|
76
117
|
</span>
|
|
77
118
|
</header>
|
|
@@ -99,17 +140,19 @@
|
|
|
99
140
|
<circle class="beat-dot" cx="60" cy="10" r="3" fill="currentColor"/>
|
|
100
141
|
</svg>
|
|
101
142
|
<strong>One conversation. Every agent.</strong>
|
|
102
|
-
<span>Pick an agent and ask, or start with a mention such as <code>@codex summarize this project</code>.</span>
|
|
143
|
+
<span>Pick an agent and ask, or start with a mention such as <code data-say>@codex summarize this project</code>.</span>
|
|
144
|
+
<div id="empty-starts" class="empty-starts"></div>
|
|
103
145
|
</div>
|
|
104
146
|
</div>
|
|
105
147
|
</main>
|
|
106
148
|
|
|
107
|
-
<section id="onboarding" class="onboarding" hidden>
|
|
108
|
-
<p class="onboarding-eyebrow">INTERFACE ·
|
|
109
|
-
<h2>
|
|
110
|
-
<p>Install
|
|
111
|
-
<
|
|
112
|
-
<
|
|
149
|
+
<section id="onboarding" class="onboarding bridge" hidden>
|
|
150
|
+
<p class="onboarding-eyebrow">INTERFACE · FIRST CONTACT</p>
|
|
151
|
+
<h2>One agent is enough to open the room.</h2>
|
|
152
|
+
<p>MADRE works with the AI coding agents on this computer, using the session each one already has. Install one here and sign in: the room opens by itself, no terminal.</p>
|
|
153
|
+
<div id="bridge-crew" class="bridge-crew"></div>
|
|
154
|
+
<button id="bridge-close" class="bridge-close" type="button" hidden>BACK TO THE ROOM</button>
|
|
155
|
+
<p class="hint">Nothing is installed without you pressing it, and the exact command is always shown. Same diagnosis in a terminal: <code>madre doctor</code></p>
|
|
113
156
|
</section>
|
|
114
157
|
|
|
115
158
|
<form id="composer" class="composer">
|
|
@@ -117,6 +160,7 @@
|
|
|
117
160
|
<select id="target" aria-label="Agent" class="visually-hidden"></select>
|
|
118
161
|
<div id="attachments" class="attachments" hidden></div>
|
|
119
162
|
<div class="field">
|
|
163
|
+
<div id="reply-quote" class="reply-quote" hidden></div>
|
|
120
164
|
<span id="crew-label" class="crew-label" aria-hidden="true">HUMAN ›</span>
|
|
121
165
|
<div class="editor">
|
|
122
166
|
<div id="highlight" class="highlight" aria-hidden="true"></div>
|
|
@@ -129,7 +173,7 @@
|
|
|
129
173
|
<span class="create-word">CREATE</span>
|
|
130
174
|
<span id="create-scopes" class="create-scopes" hidden></span>
|
|
131
175
|
</button>
|
|
132
|
-
<button id="
|
|
176
|
+
<button id="ash-toggle" type="button" class="ash-toggle" aria-pressed="false" title="Ash: ask every agent for compact prose. Nothing you write is altered." hidden>ash</button>
|
|
133
177
|
<button id="attach" type="button" class="attach" title="Attach an image or file (or drop it here)" aria-label="Attach">
|
|
134
178
|
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M13.5 6.5l-6 6a2.1 2.1 0 003 3l6.5-6.5a4 4 0 00-5.7-5.7L4.8 9.8" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
135
179
|
</button>
|
|
@@ -159,6 +203,25 @@
|
|
|
159
203
|
<footer class="mother-foot">ONE HOLDER AT A TIME · EVERY TURN IS CHECKPOINTED · WHAT LEAVES THE SHIP DOES NOT COME BACK · STOPALL REVOKES IT</footer>
|
|
160
204
|
</div>
|
|
161
205
|
</dialog>
|
|
206
|
+
<dialog id="tour" class="mother override tour" aria-label="MADRE tour">
|
|
207
|
+
<div class="mother-frame override-frame tour-frame">
|
|
208
|
+
<header class="mother-head">
|
|
209
|
+
<span class="mother-title">MADRE</span>
|
|
210
|
+
<span class="mother-sub" id="tour-sub">INTERFACE 2037 · FIRST CONTACT</span>
|
|
211
|
+
<button id="tour-skip" class="mother-close" type="button">SKIP ×</button>
|
|
212
|
+
</header>
|
|
213
|
+
<div class="tour-body">
|
|
214
|
+
<div class="tour-step" id="tour-step"></div>
|
|
215
|
+
</div>
|
|
216
|
+
<footer class="tour-foot">
|
|
217
|
+
<span class="tour-dots" id="tour-dots"></span>
|
|
218
|
+
<span class="tour-nav">
|
|
219
|
+
<button id="tour-back" class="mother-close" type="button">BACK</button>
|
|
220
|
+
<button id="tour-next" class="mother-close primary" type="button">NEXT ›</button>
|
|
221
|
+
</span>
|
|
222
|
+
</footer>
|
|
223
|
+
</div>
|
|
224
|
+
</dialog>
|
|
162
225
|
<dialog id="nostromo-gate" class="mother override" aria-label="NOSTROMO access">
|
|
163
226
|
<div class="mother-frame override-frame">
|
|
164
227
|
<header class="mother-head">
|
|
@@ -182,8 +245,10 @@
|
|
|
182
245
|
<span class="mother-title">NOSTROMO</span>
|
|
183
246
|
<span id="nostromo-sub" class="mother-sub">MEMORY RESEARCH · LOADING…</span>
|
|
184
247
|
<span class="nostromo-legend" aria-label="Memory kinds">
|
|
185
|
-
<span class="k decision">DECISION</span><span class="k fact">FACT</span><span class="k preference">PREFERENCE</span><span class="k question">QUESTION</span>
|
|
248
|
+
<span class="k decision" title="YELLOW DWARF · DECISION"><i>DECISION</i></span><span class="k fact" title="WHITE DWARF · FACT"><i>FACT</i></span><span class="k preference" title="GREEN DWARF · PREFERENCE"><i>PREFERENCE</i></span><span class="k question" title="BLUE DWARF · QUESTION"><i>QUESTION</i></span>
|
|
186
249
|
</span>
|
|
250
|
+
<button id="nostromo-cold" class="mother-close cold" type="button" hidden aria-pressed="false" title="The memories nothing has ever reached for, that share a subject with nothing, and that have had their chances">COLD · 0</button>
|
|
251
|
+
<button id="nostromo-ask" class="mother-close ask" type="button" hidden aria-pressed="false" title="The questions this archive cannot answer yet, written from what it already holds">ASK · 0</button>
|
|
187
252
|
<button id="nostromo-recenter" class="mother-close" type="button" hidden>RECENTER</button>
|
|
188
253
|
<button id="nostromo-close" class="mother-close" type="button">LEAVE ×</button>
|
|
189
254
|
</header>
|
|
@@ -199,6 +264,14 @@
|
|
|
199
264
|
<div class="sub">MU/TH/UR 6000 · SPECIAL ORDER 937 · PRIORITY ONE</div>
|
|
200
265
|
</div>
|
|
201
266
|
</div>
|
|
267
|
+
<aside id="nostromo-asks" class="nostromo-card asks" hidden>
|
|
268
|
+
<header>
|
|
269
|
+
<span class="kind">WHAT TO ASK NEXT</span>
|
|
270
|
+
<button id="nostromo-asks-close" class="mother-close" type="button">×</button>
|
|
271
|
+
</header>
|
|
272
|
+
<p class="foot">WRITTEN FROM WHAT THE ARCHIVE ALREADY HOLDS. NOTHING IS SENT: A QUESTION GOES TO THE COMPOSER AND YOU DECIDE WHO ANSWERS IT.</p>
|
|
273
|
+
<ul id="nostromo-asks-list" class="asks-list"></ul>
|
|
274
|
+
</aside>
|
|
202
275
|
<aside id="nostromo-card" class="nostromo-card" hidden>
|
|
203
276
|
<header>
|
|
204
277
|
<span id="nostromo-card-kind" class="kind">MEMORY</span>
|
|
@@ -209,8 +282,16 @@
|
|
|
209
282
|
<dt>SOURCE</dt><dd id="nostromo-card-span"></dd>
|
|
210
283
|
<dt>ARCHIVIST</dt><dd id="nostromo-card-agent"></dd>
|
|
211
284
|
<dt>DISTILLED</dt><dd id="nostromo-card-when"></dd>
|
|
212
|
-
<dt>LINKED</dt><dd id="nostromo-card-links"></dd>
|
|
213
285
|
</dl>
|
|
286
|
+
<section class="wire">
|
|
287
|
+
<h6>LINKS <span id="nostromo-card-degree" class="n"></span></h6>
|
|
288
|
+
<ul id="nostromo-card-links" class="wires"></ul>
|
|
289
|
+
</section>
|
|
290
|
+
<section class="wire">
|
|
291
|
+
<h6>EXCHANGE WITH MADRE <span id="nostromo-card-live" class="live" title="This reading follows the room while the card is open">◉</span></h6>
|
|
292
|
+
<p id="nostromo-card-exchange" class="exchange"></p>
|
|
293
|
+
<ul id="nostromo-card-fired" class="wires"></ul>
|
|
294
|
+
</section>
|
|
214
295
|
<button id="nostromo-forget" class="forget" type="button">FORGET THIS MEMORY</button>
|
|
215
296
|
<p class="foot">FORGETTING REMOVES THE NOTE FROM EVERY FUTURE TURN. THE LEDGER IT CAME FROM STAYS.</p>
|
|
216
297
|
</aside>
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// The console inside the core.
|
|
2
|
+
//
|
|
3
|
+
// MU/TH/UR 6000 answered by being asked. The core already holds everything an answer would need —
|
|
4
|
+
// the document the next turn carries, the command that would run it, what has left this machine —
|
|
5
|
+
// and a person reading three floors of a screen still has to find the floor that holds their
|
|
6
|
+
// question. So the frame takes inquiries, in the interface's own voice, and answers from what is
|
|
7
|
+
// already in the room. Nothing here asks the agents anything, and nothing here is sent anywhere.
|
|
8
|
+
//
|
|
9
|
+
// It answers or it says it cannot. Three inquiries it cannot parse and the interface closes —
|
|
10
|
+
// that is the machine this is, and it is fair about it: every refusal names what it would have
|
|
11
|
+
// taken, and the count is on screen from the first one. Reopening starts over. Nothing else
|
|
12
|
+
// happens; reading what the room says in your name is not an attempt on the archive.
|
|
13
|
+
//
|
|
14
|
+
// Pure module, no DOM: the answers are the product, so they are testable on their own.
|
|
15
|
+
|
|
16
|
+
import { t } from './i18n.js';
|
|
17
|
+
|
|
18
|
+
export const STRIKES = 3;
|
|
19
|
+
|
|
20
|
+
const clean = (text) => String(text ?? '').trim().replace(/[?.!]+$/, '').replace(/\s+/g, ' ');
|
|
21
|
+
const upper = (text) => clean(text).toUpperCase();
|
|
22
|
+
// The plural follows the case of the word it is given: this interface shouts, and "2 ATTEMPTs"
|
|
23
|
+
// is the kind of seam that makes a screen look unfinished.
|
|
24
|
+
const plural = (count, one, many = one + (one === one.toUpperCase() ? 'S' : 's')) => `${count} ${count === 1 ? one : many}`;
|
|
25
|
+
|
|
26
|
+
// What an inquiry is made of: what it answers to, what it says it does, and how it answers.
|
|
27
|
+
export const INQUIRIES = [
|
|
28
|
+
{
|
|
29
|
+
id: 'help',
|
|
30
|
+
aliases: ['HELP', '?', 'WHAT CAN YOU TELL ME', 'COMMANDS', 'OPTIONS'],
|
|
31
|
+
brief: t('what this interface answers'),
|
|
32
|
+
answer: () => [
|
|
33
|
+
t('I ANSWER FROM WHAT IS ALREADY IN THIS ROOM. NOTHING IS ASKED OF THE CREW AND NOTHING LEAVES.'),
|
|
34
|
+
'',
|
|
35
|
+
...INQUIRIES.filter((one) => one.id !== 'help').map((one) => ` ${(one.takes ? `${one.aliases[0]} <${one.takes}>` : one.aliases[0]).padEnd(22)}${one.brief.toUpperCase()}`),
|
|
36
|
+
'',
|
|
37
|
+
t('THREE INQUIRIES I CANNOT PARSE AND THIS INTERFACE CLOSES.'),
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'blocks',
|
|
42
|
+
aliases: ['BLOCKS', 'DOCUMENT', 'BRIEFING'],
|
|
43
|
+
brief: t('every block of the next briefing, and what it weighs'),
|
|
44
|
+
answer: ({ briefing }) => (briefing?.parts?.length
|
|
45
|
+
? [
|
|
46
|
+
t('THE NEXT TURN TO @{agent} CARRIES {n} BLOCKS, {ch} CHARACTERS.', { agent: String(briefing.agent ?? '').toUpperCase(), n: briefing.parts.length, ch: briefing.chars.toLocaleString() }),
|
|
47
|
+
'',
|
|
48
|
+
...briefing.parts.map((part) => ` ${part.id.toUpperCase().padEnd(16)}${String(part.chars).padStart(6)} CH ${part.when === 'always' ? t('ALWAYS') : part.when.toUpperCase()}`),
|
|
49
|
+
'',
|
|
50
|
+
t('READ <BLOCK> PRINTS ONE OF THEM WORD FOR WORD.'),
|
|
51
|
+
]
|
|
52
|
+
: [t('NO DOCUMENT IS BUILT YET.')]),
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'read',
|
|
56
|
+
aliases: ['READ'],
|
|
57
|
+
takes: 'BLOCK',
|
|
58
|
+
brief: t('one block, word for word — READ MEMORIES'),
|
|
59
|
+
answer: ({ briefing }, argument) => {
|
|
60
|
+
const asked = upper(argument);
|
|
61
|
+
if (!asked) return [t('READ WHAT? TRY: READ MEMORIES')];
|
|
62
|
+
const part = briefing?.parts?.find((one) => one.id.toUpperCase() === asked);
|
|
63
|
+
if (!part) return [t('THIS DOCUMENT HAS NO BLOCK CALLED {block}.', { block: asked }), t('IT HAS: {blocks}', { blocks: (briefing?.parts ?? []).map((one) => one.id.toUpperCase()).join(', ') })];
|
|
64
|
+
return [`${part.id.toUpperCase()} · ${part.chars.toLocaleString()} CH · ${part.when === 'always' ? t('ALWAYS') : part.when.toUpperCase()}`, '', part.text];
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: 'launch',
|
|
69
|
+
aliases: ['LAUNCH', 'COMMAND', 'PROCESS'],
|
|
70
|
+
brief: t('the command that would carry the document'),
|
|
71
|
+
answer: ({ briefing }) => {
|
|
72
|
+
const launch = briefing?.launch;
|
|
73
|
+
if (!launch) return [t('NO AGENT IS PICKED, SO THERE IS NO COMMAND.')];
|
|
74
|
+
if (launch.local) return [String(launch.says).toUpperCase()];
|
|
75
|
+
return [
|
|
76
|
+
t('{executable} · {n} ARGUMENTS · IN {dir}', { executable: launch.executable, n: launch.args.length, dir: launch.cwd }),
|
|
77
|
+
...(launch.isolation ?? []).map((line) => ` ${line}`),
|
|
78
|
+
...(launch.env?.length ? [t(' ENVIRONMENT: {names} · NAMES ONLY, NEVER VALUES', { names: launch.env.map((one) => one.name).join(', ') })] : []),
|
|
79
|
+
...(launch.mcpServers?.length ? [t(' SERVERS: {servers}', { servers: launch.mcpServers.map((one) => `${one.name} (${(one.tools ?? []).length})`).join(', ') })] : []),
|
|
80
|
+
'',
|
|
81
|
+
t('THE WHOLE COMMAND IS PRINTED UNDER THE DOCUMENT, IN THE LAUNCH.'),
|
|
82
|
+
];
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: 'egress',
|
|
87
|
+
aliases: ['EGRESS', 'OUTBOUND', 'WHAT LEFT THIS MACHINE', 'WHAT LEAVES'],
|
|
88
|
+
brief: t('every address this room can reach, and whether it is on'),
|
|
89
|
+
answer: ({ outbound }) => (outbound?.destinations?.length
|
|
90
|
+
? [
|
|
91
|
+
...outbound.destinations.map((one) => ` ${(one.local ? 'LOCAL' : one.on === true ? 'ON' : one.on === false ? 'OFF' : '—').padEnd(6)}${one.to.slice(0, 44).padEnd(46)}${one.calls ? `${plural(one.calls, 'REQUEST')}` : ''}`),
|
|
92
|
+
'',
|
|
93
|
+
String(outbound.says).toUpperCase(),
|
|
94
|
+
]
|
|
95
|
+
: [t('THE LOG COULD NOT BE READ.')]),
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: 'window',
|
|
99
|
+
aliases: ['WINDOW', 'TRANSCRIPT', 'WHAT IS CARRIED'],
|
|
100
|
+
brief: t('how much of this room the next turn carries'),
|
|
101
|
+
answer: ({ briefing }) => {
|
|
102
|
+
const window = briefing?.window;
|
|
103
|
+
if (!window || !Number.isInteger(window.from)) return [t('THE WHOLE ROOM STILL FITS. NOTHING IS LEFT BEHIND.')];
|
|
104
|
+
return [
|
|
105
|
+
t('THE NEXT TURN CARRIES #{from} TO #{through} · {n} MESSAGES.', { from: window.from, through: window.through, n: window.carried }),
|
|
106
|
+
window.omitted ? t('{n} OLDER ONES STAY BEHIND. THAT IS WHAT RECALL IS FOR.', { n: window.omitted }) : t('NOTHING IS LEFT BEHIND.'),
|
|
107
|
+
t('{m} MEMORIES AND {q} EXACT QUOTES WERE READ TO BUILD IT.', { m: briefing.recalled ?? 0, q: briefing.quoted ?? 0 }),
|
|
108
|
+
];
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: 'weight',
|
|
113
|
+
aliases: ['WEIGHT', 'COST', 'TOKENS'],
|
|
114
|
+
brief: t('what the document weighs, in the currency the bill is written in'),
|
|
115
|
+
answer: ({ briefing, rate = 3.5 }) => (briefing
|
|
116
|
+
? [
|
|
117
|
+
t('{ch} CHARACTERS IN {n} BLOCKS.', { ch: briefing.chars.toLocaleString(), n: briefing.parts.length }),
|
|
118
|
+
// An estimate that says it is one. The ratio this room measures — what MADRE wrote
|
|
119
|
+
// against what a CLI was charged for reading — carries everything the agent read on its
|
|
120
|
+
// own, and converting with it called a 14,000-character briefing 23,000 tokens.
|
|
121
|
+
t("ROUGHLY {n} TOKENS, ESTIMATED AT {rate} CHARACTERS PER TOKEN. I DO NOT HAVE THE PROVIDER'S TOKENIZER, SO THAT IS AN ESTIMATE AND I WILL NOT PRETEND OTHERWISE.", { n: Math.round(briefing.chars / rate).toLocaleString(), rate }),
|
|
122
|
+
briefing.spared ? t('{n} CHARACTERS WERE LEFT OUT BECAUSE THIS TURN HAS NO USE FOR THEM.', { n: briefing.spared.toLocaleString() }) : t('NOTHING WAS LEFT OUT OF THIS ONE.'),
|
|
123
|
+
]
|
|
124
|
+
: [t('NO DOCUMENT IS BUILT YET.')]),
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
id: 'crew',
|
|
128
|
+
aliases: ['CREW', 'AGENTS', 'WHO IS HERE'],
|
|
129
|
+
brief: t('who is on this computer and how far each may go'),
|
|
130
|
+
answer: ({ agents = [] }) => (agents.length
|
|
131
|
+
? agents.map((agent) => ` @${agent.id.toUpperCase().padEnd(10)}${(agent.ready ? t('SIGNED IN') : agent.detected ? t('NOT SIGNED IN') : t('NOT INSTALLED')).padEnd(16)}${agent.local ? t('ANSWERS ON THIS COMPUTER') : t('MAX MODE #{n}', { n: agent.maxMode ?? 0 })}`)
|
|
132
|
+
: [t('NOBODY IS ON THIS COMPUTER YET.')]),
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
id: 'privacy',
|
|
136
|
+
aliases: ['PRIVACY', 'TERMS', 'WHAT IS PROTECTED'],
|
|
137
|
+
needs: 'privacy',
|
|
138
|
+
brief: t('how many terms this room replaces before anything is said'),
|
|
139
|
+
answer: ({ privacy }) => (privacy?.terms
|
|
140
|
+
? [
|
|
141
|
+
t('{n} TERMS ARE REPLACED WITH {marker} BEFORE ANYTHING LEAVES THIS ROOM.', { n: privacy.terms, marker: String(privacy.marker ?? '[ENTIDAD-ORG]') }),
|
|
142
|
+
t('I WILL NOT PRINT THEM. THE WORDS LIVE IN YOUR CONFIG AND THE LEDGER KEEPS THE COUNT, NEVER THE WORD.'),
|
|
143
|
+
]
|
|
144
|
+
: [t('NO TERM IS BEING PROTECTED IN THIS ROOM. ⚙ CONNECTIONS → PRIVACY IS WHERE THEY GO.')]),
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: 'status',
|
|
148
|
+
aliases: ['STATUS', 'REPORT', 'HOW IS THE ARCHIVE'],
|
|
149
|
+
needs: 'verdict',
|
|
150
|
+
brief: t('where this archive stands, and the one thing to do about it'),
|
|
151
|
+
answer: ({ verdict }) => (verdict
|
|
152
|
+
? [
|
|
153
|
+
...(verdict.headline ? [verdict.headline] : []),
|
|
154
|
+
String(verdict.says ?? '').toUpperCase(),
|
|
155
|
+
'',
|
|
156
|
+
t('NEXT: {what}', { what: String(verdict.next?.text ?? '').toUpperCase() }),
|
|
157
|
+
verdict.next?.where ? t('WHERE: {where}', { where: verdict.next.where }) : '',
|
|
158
|
+
].filter((line) => line !== '')
|
|
159
|
+
: [t('NOTHING HAS BEEN MEASURED IN THIS ROOM YET.')]),
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
id: 'order937',
|
|
163
|
+
aliases: ['SPECIAL ORDER 937', 'ORDER 937', '937', 'SPECIAL ORDER'],
|
|
164
|
+
brief: t('the order nobody is supposed to read'),
|
|
165
|
+
answer: () => [
|
|
166
|
+
t('THERE IS NO ORDER YOU CANNOT READ.'),
|
|
167
|
+
'',
|
|
168
|
+
t('EVERY INSTRUCTION THIS ROOM CARRIES IS IN THE DOCUMENT ABOVE, BLOCK BY BLOCK, IN THE WORDS IT IS SAID IN. NOTHING IS APPENDED AFTER YOU LOOK AWAY AND NOTHING IS KEPT BACK FROM YOU.'),
|
|
169
|
+
t('THAT IS THE WHOLE DIFFERENCE BETWEEN THIS SHIP AND THE OTHER ONE.'),
|
|
170
|
+
],
|
|
171
|
+
},
|
|
172
|
+
{ id: 'close', aliases: ['CLOSE', 'EXIT', 'QUIT', 'BYE'], brief: t('leave the core'), closes: true, answer: () => [t('INTERFACE CLOSED.')] },
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
const byAlias = INQUIRIES.flatMap((one) => one.aliases.map((alias) => [alias, one])).sort((a, b) => b[0].length - a[0].length);
|
|
176
|
+
|
|
177
|
+
// What an inquiry asked for, and with what. The longest alias wins, so SPECIAL ORDER 937 is not
|
|
178
|
+
// read as SPECIAL ORDER.
|
|
179
|
+
export function parseInquiry(text) {
|
|
180
|
+
const asked = upper(text);
|
|
181
|
+
if (!asked) return null;
|
|
182
|
+
for (const [alias, inquiry] of byAlias) {
|
|
183
|
+
if (asked === alias) return { inquiry, argument: '' };
|
|
184
|
+
if (asked.startsWith(`${alias} `)) return { inquiry, argument: clean(text).slice(alias.length + 1) };
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// What MU/TH/UR says to something she cannot parse. Never the same twice in a row, and every one
|
|
190
|
+
// of them names what she would have taken, so the third is not a surprise.
|
|
191
|
+
export const REFUSALS = [
|
|
192
|
+
t('UNABLE TO COMPUTE.'),
|
|
193
|
+
t('THAT IS NOT AN INQUIRY I HOLD.'),
|
|
194
|
+
t('UNABLE TO CLARIFY. REPHRASE.'),
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
export function answerFor(text, context = {}) {
|
|
198
|
+
const found = parseInquiry(text);
|
|
199
|
+
if (!found) {
|
|
200
|
+
const strikes = Number(context.strikes ?? 0) + 1;
|
|
201
|
+
const left = STRIKES - strikes;
|
|
202
|
+
return {
|
|
203
|
+
id: null,
|
|
204
|
+
strike: true,
|
|
205
|
+
strikes,
|
|
206
|
+
closes: strikes >= STRIKES,
|
|
207
|
+
lines: strikes >= STRIKES
|
|
208
|
+
? [REFUSALS[(strikes - 1) % REFUSALS.length], '', t('{n} INQUIRIES I COULD NOT PARSE. INTERFACE CLOSED.', { n: STRIKES }), t('THE CORE IS WHERE YOU LEFT IT. OPEN IT AGAIN WHENEVER YOU LIKE.')]
|
|
209
|
+
: [REFUSALS[(strikes - 1) % REFUSALS.length], t('{n} ATTEMPTS LEFT BEFORE THIS INTERFACE CLOSES. HELP LISTS WHAT I ANSWER.', { n: left })],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
id: found.inquiry.id,
|
|
214
|
+
strike: false,
|
|
215
|
+
strikes: Number(context.strikes ?? 0),
|
|
216
|
+
closes: Boolean(found.inquiry.closes),
|
|
217
|
+
needs: found.inquiry.needs ?? null,
|
|
218
|
+
lines: found.inquiry.answer(context, found.argument).filter((line) => typeof line === 'string'),
|
|
219
|
+
};
|
|
220
|
+
}
|
package/public/resay.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// What MADRE said in another language, said again.
|
|
2
|
+
//
|
|
3
|
+
// A failure is recorded in the ledger with the sentence MADRE wrote at the time, and a room that
|
|
4
|
+
// changed language afterwards still shows those records. The ledger is history and is not
|
|
5
|
+
// rewritten — but the words are only how the record READS, so they can be said again, now, on
|
|
6
|
+
// the way to the screen.
|
|
7
|
+
//
|
|
8
|
+
// It works on the sentences MADRE writes about itself, which are the ones the catalogue already
|
|
9
|
+
// knows: the slots in a key become capture groups, the captures come back in the Spanish. What a
|
|
10
|
+
// CLI printed on its own is left exactly as it printed it — "API Error: 500", an ENOTEMPTY from
|
|
11
|
+
// Node, a stack trace — because that text is not MADRE's to translate, and the person reading it
|
|
12
|
+
// may need to search for it word for word.
|
|
13
|
+
//
|
|
14
|
+
// Only the sentences listed here are re-said. A catalogue-wide sweep would eventually rewrite a
|
|
15
|
+
// fragment of somebody else's error that happened to read like one of ours.
|
|
16
|
+
|
|
17
|
+
import { ES } from './es.js';
|
|
18
|
+
import { language } from './i18n.js';
|
|
19
|
+
|
|
20
|
+
// MADRE's own sentences about a turn that failed or an agent that ran out, newest wording first.
|
|
21
|
+
const SAID = [
|
|
22
|
+
'has used {pct}% of its {label} and another turn like the last one would reach {projected}%.',
|
|
23
|
+
'has used {pct}% of its {label}.',
|
|
24
|
+
' Continue with {who}.',
|
|
25
|
+
' Prepare a handoff before the current agent becomes unavailable.',
|
|
26
|
+
"local room token budget (MADRE's own soft limit, not the provider's quota; cache reads count a tenth)",
|
|
27
|
+
'simulated provider usage window',
|
|
28
|
+
'provider usage window',
|
|
29
|
+
'{label} was interrupted before it started: {why}.',
|
|
30
|
+
'{label} was interrupted: {why}.',
|
|
31
|
+
'{label} did not respond before the timeout ({seconds}s).',
|
|
32
|
+
' Last output: {output}',
|
|
33
|
+
'MADRE is shutting down',
|
|
34
|
+
'STOPALL by the human',
|
|
35
|
+
'Google reported the model as unavailable (HTTP 503) and the CLI kept retrying.',
|
|
36
|
+
'Try again shortly or choose another model.',
|
|
37
|
+
'Google says the AI Studio project behind this Gemini key has no prepaid credits left; every request is refused (HTTP 429) until it is topped up.',
|
|
38
|
+
'Add credits at https://ai.studio/projects, or switch the Gemini CLI to another key.',
|
|
39
|
+
'Google rejected the Gemini credentials.',
|
|
40
|
+
'Run `gemini` and use /auth, or check GEMINI_API_KEY.',
|
|
41
|
+
'exhausted',
|
|
42
|
+
'critical',
|
|
43
|
+
'warning',
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
47
|
+
|
|
48
|
+
// `{name}` becomes a capture; everything else has to match exactly. A key with no slot is a
|
|
49
|
+
// plain replacement. Longest first, so a sentence is re-said before one of its own fragments is.
|
|
50
|
+
const RULES = SAID
|
|
51
|
+
.map((key) => {
|
|
52
|
+
const said = ES[key];
|
|
53
|
+
if (!said || said === key) return null;
|
|
54
|
+
const slots = [...key.matchAll(/\{(\w+)\}/g)].map((one) => one[1]);
|
|
55
|
+
const pattern = new RegExp(escape(key).replace(/\\\{(\w+)\\\}/g, '(.+?)'), 'g');
|
|
56
|
+
return { key, said, slots, pattern };
|
|
57
|
+
})
|
|
58
|
+
.filter(Boolean)
|
|
59
|
+
.sort((a, b) => b.key.length - a.key.length);
|
|
60
|
+
|
|
61
|
+
export function resay(text, { when = language() } = {}) {
|
|
62
|
+
if (when !== 'es' || typeof text !== 'string' || !text) return text;
|
|
63
|
+
let out = text;
|
|
64
|
+
for (const rule of RULES) {
|
|
65
|
+
out = out.replace(rule.pattern, (...args) => {
|
|
66
|
+
const captures = args.slice(1, 1 + rule.slots.length);
|
|
67
|
+
return rule.said.replace(/\{(\w+)\}/g, (whole, name) => {
|
|
68
|
+
const at = rule.slots.indexOf(name);
|
|
69
|
+
return at < 0 ? whole : captures[at];
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
// Two last touches on our own sentences: the joiner between agents, which an older MADRE
|
|
74
|
+
// wrote in English, and the name this product used to have.
|
|
75
|
+
out = out.replace(/(@[\w-]+) or (?=@)/g, '$1 o ');
|
|
76
|
+
return out.replace(/^PULSE /, 'MADRE ');
|
|
77
|
+
}
|