@jossuealcala/madre 0.7.2 → 1.0.1

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.
Files changed (172) hide show
  1. package/CHANGELOG.md +817 -1
  2. package/CONTRIBUTING.md +5 -1
  3. package/NOTICE +20 -1
  4. package/README.md +99 -34
  5. package/TRADEMARKS.md +65 -0
  6. package/bin/madre.mjs +30 -10
  7. package/docs/INTERNALS.md +2 -2
  8. package/docs/REFERENCE.md +159 -82
  9. package/docs/SDK.md +23 -19
  10. package/docs/madre-banner.png +0 -0
  11. package/package.json +4 -3
  12. package/public/app.js +11668 -4219
  13. package/public/assets/brand/Geist-latin-ext.woff2 +0 -0
  14. package/public/assets/brand/Geist-latin.woff2 +0 -0
  15. package/public/assets/brand/GeistMono-latin-ext.woff2 +0 -0
  16. package/public/assets/brand/GeistMono-latin.woff2 +0 -0
  17. package/public/assets/brand/JetBrainsMono-latin-ext.woff2 +0 -0
  18. package/public/assets/brand/JetBrainsMono-latin.woff2 +0 -0
  19. package/public/assets/brand/OFL-Geist.txt +93 -0
  20. package/public/assets/brand/OFL-GeistMono.txt +93 -0
  21. package/public/assets/brand/OFL-JetBrainsMono.txt +93 -0
  22. package/public/assets/brand/OFL-Sora.txt +93 -0
  23. package/public/assets/brand/Sora-latin-ext.woff2 +0 -0
  24. package/public/assets/brand/Sora-latin.woff2 +0 -0
  25. package/public/assets/brand/madre-app-180.png +0 -0
  26. package/public/assets/brand/madre-app-192.png +0 -0
  27. package/public/assets/brand/madre-app-512.png +0 -0
  28. package/public/assets/brand/madre-app.svg +1 -0
  29. package/public/assets/brand/madre-favicon.svg +1 -0
  30. package/public/assets/news/1.0/alerts.webp +0 -0
  31. package/public/assets/news/1.0/bar.webp +0 -0
  32. package/public/assets/news/1.0/finder.webp +0 -0
  33. package/public/assets/news/1.0/github.webp +0 -0
  34. package/public/assets/news/1.0/intents.webp +0 -0
  35. package/public/assets/news/1.0/looks.webp +0 -0
  36. package/public/assets/news/1.0/modules.webp +0 -0
  37. package/public/assets/news/1.0/mother.webp +0 -0
  38. package/public/assets/news/1.0/nostromo.webp +0 -0
  39. package/public/assets/news/1.0/permissions.webp +0 -0
  40. package/public/assets/news/1.0/room.webp +0 -0
  41. package/public/assets/news/1.0/settings.webp +0 -0
  42. package/public/assets/wallpapers/biznaga.webp +0 -0
  43. package/public/assets/wallpapers/canela.webp +0 -0
  44. package/public/assets/wallpapers/jarrito.webp +0 -0
  45. package/public/assets/wallpapers/magna.webp +0 -0
  46. package/public/assets/wallpapers/oceano.webp +0 -0
  47. package/public/assets/wallpapers/paraiso.webp +0 -0
  48. package/public/assets/wallpapers/quila.webp +0 -0
  49. package/public/assets/wallpapers/tradicion.webp +0 -0
  50. package/public/assets/wallpapers/tropical.webp +0 -0
  51. package/public/assets/wallpapers/urbania.webp +0 -0
  52. package/public/assets/wallpapers/ventura.webp +0 -0
  53. package/public/assets/wallpapers/xico.webp +0 -0
  54. package/public/brands.js +28 -2
  55. package/public/editor.js +744 -0
  56. package/public/es.js +3443 -1425
  57. package/public/i18n.js +1 -1
  58. package/public/index.html +201 -175
  59. package/public/inquiry.js +11 -11
  60. package/public/loader.js +79 -0
  61. package/public/macroverso.js +1517 -0
  62. package/public/manifest.webmanifest +14 -0
  63. package/public/nostromo-engine.js +2222 -0
  64. package/public/resay.js +24 -0
  65. package/public/review-ask.js +85 -0
  66. package/public/salas-mac.css +1281 -0
  67. package/public/salas-mac.html +158 -0
  68. package/public/salas-mac.js +4593 -0
  69. package/public/salas.css +85 -0
  70. package/public/salas.html +47 -0
  71. package/public/salas.js +200 -0
  72. package/public/styles.css +6372 -426
  73. package/public/touch.js +83 -0
  74. package/public/tour-chapters.js +452 -0
  75. package/public/tour.js +546 -0
  76. package/public/troubleshooting.js +160 -141
  77. package/src/adapters/codex.mjs +46 -6
  78. package/src/adapters/gemini.mjs +30 -10
  79. package/src/adapters/madre.mjs +110 -18
  80. package/src/adapters/process.mjs +42 -2
  81. package/src/auth-probe.mjs +31 -8
  82. package/src/bridge/devices.mjs +202 -0
  83. package/src/bridge/handoff.mjs +45 -0
  84. package/src/bridge/host.mjs +82 -0
  85. package/src/bridge/network.mjs +133 -0
  86. package/src/bridge/policy.mjs +89 -0
  87. package/src/bridge/qr.mjs +287 -0
  88. package/src/bridge/server.mjs +269 -0
  89. package/src/capabilities.mjs +5 -9
  90. package/src/chats.mjs +59 -22
  91. package/src/checkpoint.mjs +137 -35
  92. package/src/choices.mjs +18 -3
  93. package/src/cli-args.mjs +12 -1
  94. package/src/commands.mjs +15 -15
  95. package/src/config.mjs +73 -9
  96. package/src/conversation-context.mjs +5 -2
  97. package/src/credentials.mjs +80 -0
  98. package/src/dataset.mjs +3 -1
  99. package/src/directives.mjs +6 -11
  100. package/src/enlaces.mjs +451 -0
  101. package/src/event-store.mjs +55 -10
  102. package/src/exam.mjs +5 -5
  103. package/src/extensions.mjs +31 -12
  104. package/src/file-ops.mjs +23 -0
  105. package/src/files.mjs +52 -2
  106. package/src/github.mjs +407 -0
  107. package/src/heartbeat.mjs +55 -0
  108. package/src/json-file.mjs +63 -0
  109. package/src/lease.mjs +2 -2
  110. package/src/lobby-work.mjs +103 -0
  111. package/src/local-ai.mjs +48 -0
  112. package/src/looks.mjs +88 -0
  113. package/src/maturity.mjs +3 -3
  114. package/src/mcp/github-server.mjs +164 -0
  115. package/src/mcp/image-server.mjs +1 -1
  116. package/src/mcp/memory-server.mjs +11 -3
  117. package/src/memory.mjs +38 -12
  118. package/src/models.mjs +127 -5
  119. package/src/modules/ahp.mjs +4 -4
  120. package/src/modules/ash.mjs +1 -1
  121. package/src/modules/bishop-plan.mjs +207 -0
  122. package/src/modules/bishop.mjs +645 -0
  123. package/src/modules/choices.mjs +2 -2
  124. package/src/modules/correo.mjs +1 -1
  125. package/src/modules/enlaces.mjs +255 -0
  126. package/src/modules/git-pulse.mjs +2 -2
  127. package/src/modules/github.mjs +322 -0
  128. package/src/modules/image-studio.mjs +3 -3
  129. package/src/modules/index.mjs +18 -4
  130. package/src/modules/intents-calibration.mjs +338 -0
  131. package/src/modules/intents-desk.mjs +481 -0
  132. package/src/modules/intents-observe.mjs +266 -0
  133. package/src/modules/intents-policy.mjs +414 -0
  134. package/src/modules/intents-try.mjs +91 -0
  135. package/src/modules/intents.mjs +145 -0
  136. package/src/modules/ollama.mjs +18 -6
  137. package/src/modules/playwright.mjs +121 -11
  138. package/src/modules/ripley.mjs +1 -1
  139. package/src/modules/runs.mjs +2 -2
  140. package/src/modules/sdk.mjs +10 -4
  141. package/src/ollama.mjs +6 -1
  142. package/src/outbound.mjs +36 -17
  143. package/src/override.mjs +100 -0
  144. package/src/ports.mjs +23 -1
  145. package/src/prices.mjs +72 -0
  146. package/src/privacy.mjs +30 -11
  147. package/src/quota-sources.mjs +0 -8
  148. package/src/request-guard.mjs +1 -1
  149. package/src/review-ask.mjs +111 -0
  150. package/src/room/archivist.mjs +55 -37
  151. package/src/room/budget.mjs +17 -5
  152. package/src/room/control.mjs +70 -7
  153. package/src/room/economy.mjs +35 -7
  154. package/src/room/escalation.mjs +3 -0
  155. package/src/room/guard.mjs +26 -2
  156. package/src/room/prompt.mjs +22 -15
  157. package/src/room/runs.mjs +84 -21
  158. package/src/room/vectors.mjs +19 -4
  159. package/src/room.mjs +411 -84
  160. package/src/router.mjs +3 -1
  161. package/src/runs.mjs +23 -5
  162. package/src/salas.mjs +313 -0
  163. package/src/sentinel-errors.mjs +1 -1
  164. package/src/server-sentinel.mjs +124 -0
  165. package/src/server.mjs +1864 -178
  166. package/src/setup.mjs +1 -0
  167. package/src/tour.mjs +174 -0
  168. package/src/updates.mjs +48 -8
  169. package/src/usage-sentinel.mjs +1 -1
  170. package/src/verdict.mjs +7 -7
  171. package/src/whats-new.mjs +160 -0
  172. package/docs/madre-banner.svg +0 -25
package/src/setup.mjs CHANGED
@@ -123,6 +123,7 @@ async function listOpenCodeModels(agent) {
123
123
  return new Promise((resolve) => {
124
124
  const child = spawn(agent.path, ['models'], { stdio: ['ignore', 'pipe', 'ignore'] });
125
125
  let out = '';
126
+ child.stdout.setEncoding('utf8');
126
127
  child.stdout.on('data', (chunk) => { out += chunk; });
127
128
  const timer = setTimeout(() => child.kill('SIGKILL'), 15000);
128
129
  child.on('close', () => { clearTimeout(timer); resolve(out.split('\n').map((line) => line.trim()).filter((line) => /^[\w.-]+\/[\w.:-]+$/.test(line))); });
package/src/tour.mjs ADDED
@@ -0,0 +1,174 @@
1
+ // The welcome of MADRE 1.0 (2026-10-11): one bonjour per person and per major version, then the
2
+ // chapters that tell everything new, at the person's own pace (docs: the «Recorrido de MADRE 1.0»
3
+ // decision, option B with two pieces of C — the one-line hints and the sheet of gestures).
4
+ //
5
+ // Where it was, what was read, which hints were seen: kept in ~/.pulse/tour.json, never in a
6
+ // browser. Every project is its own origin and a project may open on another port next time, so a
7
+ // flag in the browser would be forgotten or said twice; this file is one per person, as
8
+ // whats-new.json and review-ask.json are.
9
+ //
10
+ // It also brings the projects of 0.7.2 to Home. 0.7.2 never wrote where a project is
11
+ // (project.json), so Home cannot list them; but a room's id is its folder's name and a hash of its
12
+ // path (src/salas.mjs), so the folder is found by looking for that name where people keep their
13
+ // projects and is proved by the hash. Nothing is guessed: a folder whose hash does not match is
14
+ // not that project, and a project MADRE was told to forget is never brought back.
15
+
16
+ import { createReadStream } from 'node:fs';
17
+ import { readdir, readFile, realpath, stat } from 'node:fs/promises';
18
+ import { join } from 'node:path';
19
+ import { createInterface } from 'node:readline';
20
+ import { oneAtATime, writeWhole } from './json-file.mjs';
21
+ import { PROJECT_HOMES, associateProject, projectRoomId, salasRoomId } from './salas.mjs';
22
+
23
+ // 1.1 (2026-10-14): the same welcome, a third way through it. Whoever read the tour of 1.0 is told
24
+ // what changed since (path «since»): the clearer names, Home as a board, its widgets and the local
25
+ // AI chosen project by project. A tour.json of 1.0 is that person; it is started again for 1.1 and
26
+ // remembers where it came from (`before`).
27
+ export const TOUR_VERSION = '1.1';
28
+ // The chapters, in their order; «moved» is only for whoever comes from 0.7.2, «renamed» only for
29
+ // whoever comes from 1.0.
30
+ export const CHAPTERS = ['moved', 'home', 'renamed', 'board', 'widgets', 'project', 'talk', 'modes', 'coordinator', 'links', 'maps', 'settings', 'modules', 'local', 'phone', 'gestures'];
31
+ // The places that confuse most, each told once in one line (from option C); then, from 1.1, what
32
+ // each key of Home's bar opens, the first time it opens, and what each widget of Home shows. (A
33
+ // project's bar says it with its own first-touch cards, public/app.js, FIRST_TOUCH.)
34
+ export const HINTS = ['switch', 'links', 'key4',
35
+ 'home-links', 'home-map', 'home-settings', 'home-add',
36
+ 'w-system', 'w-servers', 'w-memory', 'w-coordinator', 'w-models', 'w-usage'];
37
+ const PATHS = ['new', 'back', 'since'];
38
+ const BEFORE = ['1.0'];
39
+
40
+ const fresh = (before = null) => ({ version: TOUR_VERSION, path: null, bonjourAt: null, done: [], at: null, closed: false, finished: null, hints: {}, held: null, before });
41
+ // A tab that shows the bonjour or the chapters says so every few seconds; another tab that finds it
42
+ // said within this long tells the person it is open there, instead of a second bonjour.
43
+ export const HELD_MS = 45 * 1000;
44
+ const TAB = /^[\w-]{6,64}$/;
45
+ const tourFile = (stateRoot) => join(stateRoot, 'tour.json');
46
+
47
+ // What is kept, checked field by field: a file written by hand or by an older MADRE is read for
48
+ // what it says correctly and nothing else. A file for another version starts the tour again.
49
+ export async function readTour(stateRoot) {
50
+ const raw = await readFile(tourFile(stateRoot), 'utf8').then((text) => JSON.parse(text)).catch(() => null);
51
+ if (!raw || typeof raw !== 'object') return fresh();
52
+ // The tour of 1.0, read or not: this person had 1.0, and 1.1 starts again for them.
53
+ if (raw.version === '1.0') return fresh('1.0');
54
+ if (raw.version !== TOUR_VERSION) return fresh();
55
+ const iso = (value) => (typeof value === 'string' && !Number.isNaN(Date.parse(value)) ? value : null);
56
+ return {
57
+ version: TOUR_VERSION,
58
+ path: PATHS.includes(raw.path) ? raw.path : null,
59
+ bonjourAt: iso(raw.bonjourAt),
60
+ done: Array.isArray(raw.done) ? CHAPTERS.filter((id) => raw.done.includes(id)) : [],
61
+ at: CHAPTERS.includes(raw.at) ? raw.at : null,
62
+ closed: raw.closed === true,
63
+ finished: iso(raw.finished),
64
+ hints: Object.fromEntries(HINTS.filter((id) => iso(raw.hints?.[id])).map((id) => [id, raw.hints[id]])),
65
+ held: raw.held && TAB.test(String(raw.held.by ?? '')) && iso(raw.held.at) ? { by: raw.held.by, at: raw.held.at } : null,
66
+ before: BEFORE.includes(raw.before) ? raw.before : null,
67
+ };
68
+ }
69
+
70
+ async function writeTour(stateRoot, state) {
71
+ await writeWhole(tourFile(stateRoot), `${JSON.stringify(state, null, 2)}\n`);
72
+ return state;
73
+ }
74
+
75
+ // One change at a time, from the page: the path taken at the bonjour, a chapter read, where the
76
+ // person is, closed to go on later, finished, a hint seen, or everything again from the start.
77
+ // One change at a time (src/json-file.mjs): every tab says it holds the tour every few seconds,
78
+ // through whichever project it is on, and a chapter read in between was lost to it.
79
+ export function changeTour(stateRoot, change, now = new Date()) {
80
+ return oneAtATime(tourFile(stateRoot), () => applyChange(stateRoot, change, now));
81
+ }
82
+ async function applyChange(stateRoot, change, now) {
83
+ if (!change || typeof change !== 'object') return { error: 'change' };
84
+ // Again from the start, as the same person: whoever came from 1.0 still did.
85
+ if (change.reset === true) return writeTour(stateRoot, fresh((await readTour(stateRoot)).before));
86
+ const state = await readTour(stateRoot);
87
+ const at = new Date(now).toISOString();
88
+ if (change.path !== undefined) {
89
+ if (!PATHS.includes(change.path)) return { error: 'path' };
90
+ state.path = change.path;
91
+ }
92
+ if (change.bonjour === true) state.bonjourAt ??= at;
93
+ if (change.chapter !== undefined) {
94
+ if (!CHAPTERS.includes(change.chapter)) return { error: 'chapter' };
95
+ if (!state.done.includes(change.chapter)) state.done = CHAPTERS.filter((id) => id === change.chapter || state.done.includes(id));
96
+ }
97
+ if (change.at !== undefined) {
98
+ if (change.at !== null && !CHAPTERS.includes(change.at)) return { error: 'chapter' };
99
+ state.at = change.at;
100
+ }
101
+ if (change.closed !== undefined) state.closed = change.closed === true;
102
+ if (change.finished === true) { state.finished ??= at; state.closed = false; }
103
+ // Which tab has the tour open now (one says it every few seconds while it does), and that it
104
+ // let go — only the tab that holds it lets go.
105
+ if (change.hold !== undefined) {
106
+ if (!TAB.test(String(change.hold))) return { error: 'tab' };
107
+ state.held = { by: change.hold, at };
108
+ }
109
+ if (change.release !== undefined && state.held?.by === change.release) state.held = null;
110
+ if (change.hint !== undefined) {
111
+ if (!HINTS.includes(change.hint)) return { error: 'hint' };
112
+ state.hints[change.hint] ??= at;
113
+ }
114
+ return writeTour(stateRoot, state);
115
+ }
116
+
117
+ // Whoever already used MADRE wrote in it: a message of theirs in the record of a room. Having a room
118
+ // proves nothing — a room is made with its record and its memory the moment it opens, a new
119
+ // person's first `madre start` too (2026-10-11) — and a project added in Home and never opened has
120
+ // not even that. The Coordinator's own room is not counted either.
121
+ const PERSON_SPOKE = /"type":"message\.created".*"role":"user"/;
122
+ async function spoke(file) {
123
+ const lines = createInterface({ input: createReadStream(file, { encoding: 'utf8' }), crlfDelay: Infinity });
124
+ try {
125
+ for await (const line of lines) if (PERSON_SPOKE.test(line)) return true;
126
+ } catch { /* unreadable: not proof of anything */ } finally {
127
+ lines.close();
128
+ }
129
+ return false;
130
+ }
131
+ export async function cameBefore(stateRoot) {
132
+ const dir = join(stateRoot, 'rooms');
133
+ const salas = salasRoomId(stateRoot);
134
+ for (const id of await readdir(dir).catch(() => [])) {
135
+ if (id === salas) continue;
136
+ const chats = await readdir(join(dir, id, 'chats')).catch(() => []);
137
+ for (const file of [join(dir, id, 'events.jsonl'), ...chats.map((chat) => join(dir, id, 'chats', chat, 'events.jsonl'))]) {
138
+ if (await stat(file).then((info) => info.isFile(), () => false) && await spoke(file)) return true;
139
+ }
140
+ }
141
+ return false;
142
+ }
143
+
144
+ // The rooms Home cannot list: no record of where their project is, and not set aside on purpose.
145
+ export async function roomsWithoutProject(stateRoot) {
146
+ const dir = join(stateRoot, 'rooms');
147
+ const salas = salasRoomId(stateRoot);
148
+ const out = [];
149
+ for (const id of (await readdir(dir).catch(() => [])).sort()) {
150
+ if (id === salas || !/-[0-9a-f]{16}$/.test(id)) continue;
151
+ const has = (name) => stat(join(dir, id, name)).then(() => true, () => false);
152
+ if (await has('project.json') || await has('project.forgotten.json')) continue;
153
+ if (!(await has('events.jsonl')) && !(await has('memory.sqlite'))) continue;
154
+ out.push({ id, name: id.replace(/-[0-9a-f]{16}$/, '') });
155
+ }
156
+ return out;
157
+ }
158
+
159
+ // One room: its folder looked for by name where people keep their projects, proved by its hash,
160
+ // and written beside its record as adding it in Home would. Answers where it was, or that it was
161
+ // not found (a folder that was deleted, moved, or lived somewhere else).
162
+ export async function recoverProject({ stateRoot, home, id }) {
163
+ const room = (await roomsWithoutProject(stateRoot)).find((one) => one.id === id);
164
+ if (!room) return { error: 'unknown' };
165
+ for (const where of PROJECT_HOMES) {
166
+ const candidate = join(home, where, room.name);
167
+ const real = await realpath(candidate).catch(() => null);
168
+ if (!real || projectRoomId(real) !== id) continue;
169
+ const added = await associateProject({ root: real, home, stateRoot });
170
+ if (added.error) continue;
171
+ return { id, name: added.name, root: added.root, found: true };
172
+ }
173
+ return { id, name: room.name, found: false };
174
+ }
package/src/updates.mjs CHANGED
@@ -10,12 +10,33 @@ import { dirname } from 'node:path';
10
10
  export const REGISTRY = 'https://registry.npmjs.org';
11
11
  export const UPDATE_TTL_MS = 24 * 60 * 60 * 1000;
12
12
 
13
- const parse = (version) => String(version ?? '').trim().replace(/^v/, '').split(/[.+-]/).slice(0, 3).map((part) => Number.parseInt(part, 10) || 0);
13
+ // A version read as semver reads it: three numbers, then the pre-release, if any; build metadata
14
+ // (after +) never counts.
15
+ const parse = (version) => {
16
+ const text = String(version ?? '').trim().replace(/^v/, '').split('+')[0];
17
+ const dash = text.indexOf('-');
18
+ const core = (dash >= 0 ? text.slice(0, dash) : text).split('.').slice(0, 3).map((part) => Number.parseInt(part, 10) || 0);
19
+ while (core.length < 3) core.push(0);
20
+ return { core, pre: dash >= 0 ? text.slice(dash + 1).split('.').filter(Boolean) : [] };
21
+ };
14
22
 
15
- // -1 when a < b, 0 when equal, 1 when a > b. Pre-release tags are ignored: a room compares releases.
23
+ // -1 when a < b, 0 when equal, 1 when a > b, with semver's precedence for pre-releases (2026-10-12,
24
+ // LOG-16): `1.1.0-alpha.1` comes before `1.1.0`, and `alpha.2` after `alpha.1`. They were read as
25
+ // equal, so whoever ran an alpha was never told the release was out — and the Windows port (1.1)
26
+ // ships its alphas and betas this way.
16
27
  export function compareVersions(a, b) {
17
28
  const [x, y] = [parse(a), parse(b)];
18
- for (let i = 0; i < 3; i += 1) if (x[i] !== y[i]) return x[i] < y[i] ? -1 : 1;
29
+ for (let i = 0; i < 3; i += 1) if (x.core[i] !== y.core[i]) return x.core[i] < y.core[i] ? -1 : 1;
30
+ if (!x.pre.length || !y.pre.length) return x.pre.length === y.pre.length ? 0 : x.pre.length ? -1 : 1;
31
+ for (let i = 0; i < Math.max(x.pre.length, y.pre.length); i += 1) {
32
+ const [p, q] = [x.pre[i], y.pre[i]];
33
+ if (p === undefined || q === undefined) return p === undefined ? -1 : 1;
34
+ if (p === q) continue;
35
+ const [pn, qn] = [/^\d+$/.test(p), /^\d+$/.test(q)];
36
+ if (pn && qn) return Number(p) < Number(q) ? -1 : 1;
37
+ if (pn !== qn) return pn ? -1 : 1;
38
+ return p < q ? -1 : 1;
39
+ }
19
40
  return 0;
20
41
  }
21
42
 
@@ -29,6 +50,13 @@ export function detectInstall({ script = process.argv[1] ?? '', projectRoot = pr
29
50
  return 'source';
30
51
  }
31
52
 
53
+ // A version as semver writes it, and nothing else (2026-10-12, SRV-07 of the 1.0 review). What the
54
+ // registry or the cache in ~/.pulse says is read as data: a «version» with a `;` in it went, whole,
55
+ // into the shell line that restarts the room.
56
+ const VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
57
+ const PACKAGE_NAME = /^(?:@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*$/;
58
+ export const isVersion = (value) => typeof value === 'string' && VERSION.test(value);
59
+
32
60
  export function updateCommand(install, name, version = 'latest') {
33
61
  switch (install) {
34
62
  case 'npx': return `npx ${name}@${version} start`;
@@ -38,15 +66,27 @@ export function updateCommand(install, name, version = 'latest') {
38
66
  }
39
67
  }
40
68
 
69
+ // The version, downloaded into npm's own cache while the room is still open. A release is listed
70
+ // by the registry a little before its tarball can be fetched, and a room that closed first and
71
+ // then found nothing to start was simply gone: the restart below runs detached and nobody is left
72
+ // to say why. Fetched first, the restart starts from the cache; refused, the room stays.
73
+ export function fetchCommand({ name, version }) {
74
+ if (!PACKAGE_NAME.test(String(name)) || !isVersion(version)) return null;
75
+ return { command: 'npm', args: ['cache', 'add', `${name}@${version}`, '--no-fund', '--no-audit'] };
76
+ }
77
+
41
78
  // One shell line that installs the version and starts the same room on the same port.
42
79
  // The room's own process runs it detached after closing its listener, then exits.
43
80
  const shq = (value) => `'${String(value).replace(/'/g, `'\\''`)}'`;
44
81
  export function applyCommand({ install, name, version, port, projectRoot }) {
82
+ if (!PACKAGE_NAME.test(String(name)) || !isVersion(version)) return null;
45
83
  const start = `start --no-open --port ${Number(port) || 4317} --project ${shq(projectRoot)}`;
84
+ const spec = shq(`${name}@${version}`);
85
+ const bin = shq(name.split('/').pop());
46
86
  switch (install) {
47
- case 'npx': return `exec npx -y ${name}@${version} ${start}`;
48
- case 'project': return `npm install ${name}@${version} --no-fund --no-audit && exec npx --no ${name.split('/').pop()} ${start}`;
49
- case 'global': return `npm install -g ${name}@${version} --no-fund --no-audit && exec ${name.split('/').pop()} ${start}`;
87
+ case 'npx': return `exec npx -y ${spec} ${start}`;
88
+ case 'project': return `npm install ${spec} --no-fund --no-audit && exec npx --no ${bin} ${start}`;
89
+ case 'global': return `npm install -g ${spec} --no-fund --no-audit && exec ${bin} ${start}`;
50
90
  default: return null; // source: the human pulls
51
91
  }
52
92
  }
@@ -74,7 +114,7 @@ async function readCache(file) {
74
114
  export async function checkForUpdate({ name, current, cacheFile, fetchImpl = globalThis.fetch, now = Date.now(), ttlMs = UPDATE_TTL_MS, enabled = true, force = false, cacheOnly = false, timeoutMs = 4000 } = {}) {
75
115
  const cached = cacheFile ? await readCache(cacheFile) : null;
76
116
  const fresh = cached && cached.name === name && Number.isFinite(cached.checkedAt) && now - cached.checkedAt < ttlMs;
77
- let latest = cached?.name === name ? cached.latest ?? null : null;
117
+ let latest = cached?.name === name && isVersion(cached.latest) ? cached.latest : null;
78
118
  let checkedAt = cached?.name === name ? cached.checkedAt ?? null : null;
79
119
  let source = latest ? 'cache' : 'none';
80
120
  let error = null;
@@ -83,7 +123,7 @@ export async function checkForUpdate({ name, current, cacheFile, fetchImpl = glo
83
123
  const response = await fetchImpl(`${REGISTRY}/${encodeURIComponent(name).replace('%40', '@')}/latest`, { headers: { accept: 'application/json' }, signal: AbortSignal.timeout(timeoutMs) });
84
124
  if (!response.ok) throw new Error(`registry HTTP ${response.status}`);
85
125
  const doc = await response.json();
86
- if (typeof doc?.version !== 'string') throw new Error('registry answered without a version');
126
+ if (!isVersion(doc?.version)) throw new Error('registry answered without a version');
87
127
  latest = doc.version;
88
128
  checkedAt = now;
89
129
  source = 'registry';
@@ -48,7 +48,7 @@ export class UsageSentinel {
48
48
  ? t(' Continue with {who}.', { who: alternatives.map((item) => `@${item}`).join(t(' or ')) })
49
49
  : t(' Prepare a handoff before the current agent becomes unavailable.');
50
50
  const label = source === 'room-soft-budget'
51
- ? t("local room token budget (MADRE's own soft limit, not the provider's quota; cache reads count a tenth)")
51
+ ? t("local project token budget (MADRE's own soft limit, not the provider's quota; cache reads count a tenth)")
52
52
  : source === 'test-simulation'
53
53
  ? t('simulated provider usage window')
54
54
  : t('provider usage window');
package/src/verdict.mjs CHANGED
@@ -13,7 +13,7 @@
13
13
  // language and read at call time, not when this file is imported.
14
14
  import { t } from './i18n.mjs';
15
15
 
16
- export const WHERE = { nostromo: 'NOSTROMO', ask: 'NOSTROMO · ASK', room: 'THE ROOM', tests: 'THE THREE TESTS' };
16
+ export const WHERE = { nostromo: 'MEMORY MAP', ask: 'MEMORY MAP · ASK', room: 'THE PROJECT', tests: 'MEMORY REVIEW' };
17
17
  const where = (key) => t(WHERE[key]);
18
18
 
19
19
  export function verdictFor({ maturity = null, exams = {}, cold = null, asks = 0 } = {}) {
@@ -39,8 +39,8 @@ export function verdictFor({ maturity = null, exams = {}, cold = null, asks = 0
39
39
  headline: stage?.label ?? null,
40
40
  says: coverage.says,
41
41
  next: asks
42
- ? { text: t('Answer the {n} questions the room wrote for itself. They are the holes this test is finding.', { n: asks }), where: where('ask') }
43
- : { text: t('Keep working in the room. The archive fills where the work happens, and this test measures exactly that.'), where: where('room') },
42
+ ? { text: t('Answer the {n} questions the project wrote for itself. They are the holes this test is finding.', { n: asks }), where: where('ask') }
43
+ : { text: t('Keep working in the project. The archive fills where the work happens, and this test measures exactly that.'), where: where('room') },
44
44
  };
45
45
  }
46
46
 
@@ -54,8 +54,8 @@ export function verdictFor({ maturity = null, exams = {}, cold = null, asks = 0
54
54
  if (!match?.ran) {
55
55
  return {
56
56
  headline: stage?.label ?? null,
57
- says: coverage?.ran ? coverage.says : t('Nothing contradicts anything and the archive answers what this room asks.'),
58
- next: { text: t('Run the third test: it puts real questions from this room back to the local model and says whether it lands where the agents landed.'), where: where('tests') },
57
+ says: coverage?.ran ? coverage.says : t('Nothing contradicts anything and the archive answers what this project asks.'),
58
+ next: { text: t('Run the third test: it puts real questions from this project back to the local model and says whether it lands where the agents landed.'), where: where('tests') },
59
59
  };
60
60
  }
61
61
 
@@ -63,12 +63,12 @@ export function verdictFor({ maturity = null, exams = {}, cold = null, asks = 0
63
63
  return {
64
64
  headline: stage?.label ?? null,
65
65
  says: match.passed
66
- ? `${match.says} ${t('This room is ready to be worked in with the local model.')}`
66
+ ? `${match.says} ${t('This project is ready to be worked on with the local model.')}`
67
67
  : match.says,
68
68
  next: match.passed
69
69
  ? (dead
70
70
  ? { text: t('Nothing is wrong. {n} memories have had every chance and were never the answer: look at them and decide.', { n: dead }), where: where('nostromo') }
71
- : { text: t('Nothing to fix. Keep using the room; the archive grows where the work is.'), where: where('room') })
71
+ : { text: t('Nothing to fix. Keep using the project; the archive grows where the work is.'), where: where('room') })
72
72
  : { text: t('The local model does not land where the agents land yet. Keep the archive growing and run this test again in a week.'), where: where('tests') },
73
73
  };
74
74
  }
package/src/whats-new.mjs CHANGED
@@ -22,7 +22,165 @@ const FILE = 'whats-new.json';
22
22
 
23
23
  // `icon` names one of the room's 8-bit icons. `where` says where it lives, in the words the
24
24
  // screens use. `more` is the one thing nobody is told how to find.
25
+ //
26
+ // From 1.0 a sheet is a board: each item also has an `id`, a picture (`image`, under
27
+ // /assets/news/<major.minor>/, whitelisted in server.mjs) and an `action`, the place «Take me
28
+ // there» opens, or null when there is nowhere to go. The page knows exactly these places.
29
+ export const NEWS_ACTIONS = ['settings', 'mother', 'intents', 'permissions', 'github', 'bar', 'modules', 'nostromo', 'room', 'looks', 'finder'];
30
+
25
31
  export const NOTES = {
32
+ '1.0.0': {
33
+ intro: {
34
+ es: 'MADRE 1.0 se ve y se usa como un producto: todo lo que configuras en un solo lugar, ayudas que aprenden de cómo trabajas y un proyecto que acomodas a tu gusto.',
35
+ en: 'MADRE 1.0 looks and works like a product: everything you configure in one place, help that learns from how you work, and a project you arrange your way.',
36
+ },
37
+ items: [
38
+ {
39
+ id: 'settings',
40
+ icon: 'gear',
41
+ image: '/assets/news/1.0/settings.webp',
42
+ title: { es: 'Ajustes en una ventana', en: 'Settings in one window' },
43
+ body: {
44
+ es: 'Todo lo que configuras está junto: apariencia, proyecto, agentes, memoria, permisos, privacidad y versión. Cada tarjeta dice cómo está antes de abrirla, y lo que más usas se cambia ahí mismo. Un buscador encuentra cualquier ajuste.',
45
+ en: 'Everything you configure is together: appearance, project, agents, memory, permissions, privacy and version. Each card tells you how things stand before you open it, and what you use most changes right there. A search finds any setting.',
46
+ },
47
+ where: { es: 'El botón de ajustes de la barra · también ⌘,', en: 'The settings button in the bar · also ⌘,' },
48
+ action: 'settings',
49
+ },
50
+ {
51
+ id: 'mother',
52
+ icon: 'check',
53
+ image: '/assets/news/1.0/mother.webp',
54
+ title: { es: 'Diagnóstico te da la solución', en: 'Diagnostics gives you the remedy' },
55
+ body: {
56
+ es: 'Describe con tus palabras lo que pasa y te dice qué hacer, con la tecla que lo resuelve sin salir del proyecto. Lo que falló en este proyecto aparece primero y marcado, para que no tengas que adivinar.',
57
+ en: 'Describe what is happening in your own words and you see what to do, with the key that fixes it without leaving the project. What failed in this project shows first and marked, so you do not have to guess.',
58
+ },
59
+ where: { es: 'El botón DIAGNÓSTICO de la barra', en: 'The DIAGNOSTICS button in the bar' },
60
+ action: 'mother',
61
+ },
62
+ {
63
+ id: 'intents',
64
+ icon: 'chip',
65
+ image: '/assets/news/1.0/intents.webp',
66
+ title: { es: 'Intenciones aprende de tu uso', en: 'Intenciones learns from how you work' },
67
+ body: {
68
+ es: 'Mientras escribes, sugiere cuánto debe pensar cada agente para lo que pides, según cómo has trabajado y sin gastar tokens. Puede proponerte un nivel más bajo para un solo mensaje y, si se quedó corto, vuelve a subir.',
69
+ en: 'As you write, it suggests how hard each agent should think for what you ask, learned from your own turns at no token cost. It can offer a lower level for a single message, and if that fell short it goes back up.',
70
+ },
71
+ where: { es: 'Bajo la caja de texto · Módulos › Intenciones', en: 'Under the message box · Modules › Intenciones' },
72
+ action: 'intents',
73
+ },
74
+ {
75
+ id: 'permissions',
76
+ icon: 'lock',
77
+ image: '/assets/news/1.0/permissions.webp',
78
+ title: { es: 'Permisos a tu manera', en: 'Permissions your way' },
79
+ body: {
80
+ es: 'Elige cómo confirmas #3 y #4: con el nombre del proyecto, con una frase tuya o, en #3, solo confirmando. Una confirmación puede valer unos minutos, y el permiso máximo de cada agente cabe en una sola tabla.',
81
+ en: 'Choose what unlocks #3 and #4: the project name, a phrase of your own or, for #3, just a confirmation. An accepted key can hold for a few minutes, and every agent’s highest mode fits in one table.',
82
+ },
83
+ where: { es: 'Ajustes › Permisos', en: 'Settings › Permissions' },
84
+ action: 'permissions',
85
+ },
86
+ {
87
+ id: 'github',
88
+ icon: 'branch',
89
+ image: '/assets/news/1.0/github.webp',
90
+ title: { es: 'GitHub en el proyecto', en: 'GitHub in the project' },
91
+ body: {
92
+ es: 'Revisa pull requests, pruebas automáticas (CI) e issues con /gh, usando la sesión de gh que ya tienes. Antes de publicar algo te muestra qué saldría y te pregunta; los agentes solo publican en #4.',
93
+ en: 'Check pull requests, CI and issues with /gh, using the gh session you already have. Before anything is posted it shows you what would go out and asks, and agents only post in #4.',
94
+ },
95
+ where: { es: 'Módulos › GitHub · luego /gh en la caja', en: 'Modules › GitHub · then /gh in the box' },
96
+ action: 'github',
97
+ },
98
+ {
99
+ id: 'bar',
100
+ icon: 'bars',
101
+ image: '/assets/news/1.0/bar.webp',
102
+ title: { es: 'Tu barra, a tu gusto', en: 'Your bar, your way' },
103
+ body: {
104
+ es: 'Clic derecho en la barra y los íconos tiemblan: quita con − lo que no usas y agrega Memoria, Ahorro de tokens, Intenciones, IA local o cualquier módulo con ventana.',
105
+ en: 'Right-click the bar and the icons jiggle: take off what you do not use with − and add Memory, Token saver, Intenciones, Local AI or any module with a window.',
106
+ },
107
+ where: { es: 'Clic derecho en la barra · o déjala presionada', en: 'Right-click the bar · or press and hold it' },
108
+ action: 'bar',
109
+ },
110
+ {
111
+ id: 'modules',
112
+ icon: 'grid',
113
+ image: '/assets/news/1.0/modules.webp',
114
+ title: { es: 'Módulos como apps', en: 'Modules as apps' },
115
+ body: {
116
+ es: 'Los módulos son un tablero de mosaicos, cada uno con su interruptor y su ficha. Ahorro de tokens, Intenciones e IA local se abren en su propia ventana, y las descargas e instalaciones muestran su avance real.',
117
+ en: 'Modules are a board of tiles, each with its switch and its sheet. Token saver, Intenciones and Local AI open in a window of their own, and downloads and installs show their real progress.',
118
+ },
119
+ where: { es: 'El botón de módulos de la barra', en: 'The modules button in the bar' },
120
+ action: 'modules',
121
+ },
122
+ {
123
+ id: 'nostromo',
124
+ icon: 'orbit',
125
+ image: '/assets/news/1.0/nostromo.webp',
126
+ title: { es: 'El Mapa de memoria a un clic', en: 'The Memory map one click away' },
127
+ body: {
128
+ es: 'El Mapa de memoria tiene su propio botón en la barra. Los vínculos entre memorias se ven aunque Ollama esté apagado.',
129
+ en: 'The memory brain has its own button in the bar. The links between memories show even with Ollama off.',
130
+ },
131
+ where: { es: 'El botón Mapa de memoria de la barra', en: 'The Memory map button in the bar' },
132
+ action: 'nostromo',
133
+ },
134
+ {
135
+ id: 'room',
136
+ icon: 'home',
137
+ image: '/assets/news/1.0/room.webp',
138
+ title: { es: 'El proyecto, acomodado a tu gusto', en: 'The project, arranged your way' },
139
+ body: {
140
+ es: 'Cada panel se abre al lado o en una ventana que mueves. Varias conversaciones trabajan a la vez, los servidores de tu proyecto corren dentro del proyecto y el visor también edita código.',
141
+ en: 'Every panel opens beside you or in a window you move. Several conversations work at once, your project’s servers live inside the project, and the viewer edits code.',
142
+ },
143
+ where: { es: 'El encabezado de cada panel · Ajustes › Proyecto y respuestas', en: 'The header of every panel · Settings › Project and turns' },
144
+ action: 'room',
145
+ },
146
+ {
147
+ id: 'looks',
148
+ icon: 'star',
149
+ image: '/assets/news/1.0/looks.webp',
150
+ title: { es: '17 colores de acento y 12 fondos', en: '17 accents and 12 wallpapers' },
151
+ body: {
152
+ es: 'Cada uno pensado para tema claro y oscuro. Las respuestas se leen sobre vidrio esmerilado encima de tu fondo, y las tarjetas tienen un relieve suave.',
153
+ en: 'Each one made for light and dark themes. Replies read on frosted glass over your wallpaper, and cards stand out in soft relief.',
154
+ },
155
+ where: { es: 'Ajustes › Apariencia', en: 'Settings › Appearance' },
156
+ action: 'looks',
157
+ },
158
+ {
159
+ id: 'alerts',
160
+ icon: 'bell',
161
+ image: '/assets/news/1.0/alerts.webp',
162
+ title: { es: 'Alertas más claras', en: 'Clearer alerts' },
163
+ body: {
164
+ es: 'Las confirmaciones de #3 y #4 llegan en una sola hoja clara, con lo que vas a aprobar a la vista. El menú «revisarlo con» ya no se corta.',
165
+ en: 'Confirmations and the keys for #3 and #4 arrive as one clear sheet, with what you are approving in view. The “review it with” menu is no longer cut off.',
166
+ },
167
+ where: { es: 'Aparecen solas cuando el proyecto necesita tu palabra', en: 'They appear on their own when the project needs your word' },
168
+ action: null,
169
+ },
170
+ {
171
+ id: 'finder',
172
+ icon: 'list',
173
+ image: '/assets/news/1.0/finder.webp',
174
+ title: { es: 'Archivos nuevos desde Finder', en: 'New files from the Finder' },
175
+ body: {
176
+ es: 'Una tecla junto al buscador crea un archivo y lo abre en el visor, listo para escribir. Las carpetas que falten se crean solas.',
177
+ en: 'A key beside the search creates a file and opens it in the viewer, ready to write. Any missing folders are made for you.',
178
+ },
179
+ where: { es: 'Finder › junto al buscador', en: 'Finder › next to the search' },
180
+ action: 'finder',
181
+ },
182
+ ],
183
+ },
26
184
  '0.7.0': {
27
185
  intro: {
28
186
  es: 'Esta versión le da forma a la memoria de la sala: ahora la ves en tres dimensiones, la lees en un tablero y cada memoria te dice cómo está.',
@@ -96,6 +254,8 @@ export const NOTES = {
96
254
  };
97
255
 
98
256
  export const notesFor = (version) => NOTES[version] ?? null;
257
+ // The newest version that has a sheet: what a preview opens on a copy whose own version has none.
258
+ export const newestNotes = () => Object.keys(NOTES).sort(compareVersions).at(-1) ?? null;
99
259
 
100
260
  // Whether this person should see the sheet for this version. Someone who has seen an older one,
101
261
  // or who used the room before the sheet existed, sees it once; a first install never does.
@@ -1,25 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="1200" height="300" viewBox="0 0 1200 300" role="img" aria-label="MADRE · MU/TH/UR 6000 · INTERFACE 2037">
2
- <defs>
3
- <linearGradient id="bg" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#050605"/><stop offset="1" stop-color="#0b0d0b"/></linearGradient>
4
- <pattern id="scan" width="4" height="4" patternUnits="userSpaceOnUse"><rect width="4" height="1" fill="#9bff66" fill-opacity="0.06"/></pattern>
5
- <filter id="glow" x="-20%" y="-50%" width="140%" height="200%"><feGaussianBlur stdDeviation="4" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
6
- <filter id="glowSoft" x="-20%" y="-50%" width="140%" height="200%"><feGaussianBlur stdDeviation="10"/></filter>
7
- </defs>
8
- <rect width="1200" height="300" fill="url(#bg)"/>
9
- <rect width="1200" height="300" fill="url(#scan)"/>
10
- <rect x="0.5" y="0.5" width="1199" height="299" fill="none" stroke="#9bff66" stroke-opacity="0.18"/>
11
- <g transform="translate(90 150)" stroke="#9bff66" fill="none" stroke-width="5" stroke-linecap="round" stroke-linejoin="round">
12
- <path d="M0 0 H140 L176 0 L208 -70 L248 70 L284 0 L312 0 L330 -36 L348 0 H460" stroke-opacity="0.25" filter="url(#glowSoft)" stroke-width="14"/>
13
- <path d="M0 0 H140 L176 0 L208 -70 L248 70 L284 0 L312 0 L330 -36 L348 0 H460" filter="url(#glow)"/>
14
- <circle cx="520" cy="0" r="12" fill="#9bff66" stroke="none" filter="url(#glow)"/>
15
- </g>
16
- <g font-family="SF Mono, Menlo, Consolas, monospace" fill="#9bff66">
17
- <text x="690" y="150" font-size="84" font-weight="700" letter-spacing="14" filter="url(#glow)">MADRE</text>
18
- <text x="692" y="194" font-size="15" letter-spacing="6" fill-opacity="0.75">MU/TH/UR 6000 · INTERFACE 2037</text>
19
- <text x="692" y="224" font-size="13" letter-spacing="3" fill-opacity="0.55">ONE ROOM · FOUR AGENTS · ONE MEMORY</text>
20
- </g>
21
- <g font-family="SF Mono, Menlo, Consolas, monospace" font-size="11" letter-spacing="4" fill="#9bff66" fill-opacity="0.45">
22
- <text x="90" y="262">CODEX · CLAUDE CODE · GEMINI CLI · OPENCODE</text>
23
- <text x="1110" y="262" text-anchor="end">PULSE CHANNEL · LOCAL FIRST</text>
24
- </g>
25
- </svg>