@jossuealcala/madre 0.7.2 → 1.0.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 +773 -1
- package/README.md +92 -28
- package/bin/madre.mjs +30 -10
- package/docs/REFERENCE.md +157 -81
- package/docs/SDK.md +23 -19
- package/package.json +1 -1
- package/public/app.js +11207 -4196
- package/public/assets/brand/Geist-latin-ext.woff2 +0 -0
- package/public/assets/brand/Geist-latin.woff2 +0 -0
- package/public/assets/brand/GeistMono-latin-ext.woff2 +0 -0
- package/public/assets/brand/GeistMono-latin.woff2 +0 -0
- package/public/assets/brand/JetBrainsMono-latin-ext.woff2 +0 -0
- package/public/assets/brand/JetBrainsMono-latin.woff2 +0 -0
- package/public/assets/brand/OFL-Geist.txt +93 -0
- package/public/assets/brand/OFL-GeistMono.txt +93 -0
- package/public/assets/brand/OFL-JetBrainsMono.txt +93 -0
- package/public/assets/brand/OFL-Sora.txt +93 -0
- package/public/assets/brand/Sora-latin-ext.woff2 +0 -0
- package/public/assets/brand/Sora-latin.woff2 +0 -0
- package/public/assets/brand/madre-app-180.png +0 -0
- package/public/assets/brand/madre-app-192.png +0 -0
- package/public/assets/brand/madre-app-512.png +0 -0
- package/public/assets/brand/madre-app.svg +1 -0
- package/public/assets/brand/madre-favicon.svg +1 -0
- package/public/assets/news/1.0/alerts.webp +0 -0
- package/public/assets/news/1.0/bar.webp +0 -0
- package/public/assets/news/1.0/finder.webp +0 -0
- package/public/assets/news/1.0/github.webp +0 -0
- package/public/assets/news/1.0/intents.webp +0 -0
- package/public/assets/news/1.0/looks.webp +0 -0
- package/public/assets/news/1.0/modules.webp +0 -0
- package/public/assets/news/1.0/mother.webp +0 -0
- package/public/assets/news/1.0/nostromo.webp +0 -0
- package/public/assets/news/1.0/permissions.webp +0 -0
- package/public/assets/news/1.0/room.webp +0 -0
- package/public/assets/news/1.0/settings.webp +0 -0
- package/public/assets/wallpapers/biznaga.webp +0 -0
- package/public/assets/wallpapers/canela.webp +0 -0
- package/public/assets/wallpapers/jarrito.webp +0 -0
- package/public/assets/wallpapers/magna.webp +0 -0
- package/public/assets/wallpapers/oceano.webp +0 -0
- package/public/assets/wallpapers/paraiso.webp +0 -0
- package/public/assets/wallpapers/quila.webp +0 -0
- package/public/assets/wallpapers/tradicion.webp +0 -0
- package/public/assets/wallpapers/tropical.webp +0 -0
- package/public/assets/wallpapers/urbania.webp +0 -0
- package/public/assets/wallpapers/ventura.webp +0 -0
- package/public/assets/wallpapers/xico.webp +0 -0
- package/public/brands.js +28 -2
- package/public/editor.js +744 -0
- package/public/es.js +2784 -1026
- package/public/i18n.js +1 -1
- package/public/index.html +201 -175
- package/public/inquiry.js +11 -11
- package/public/loader.js +79 -0
- package/public/macroverso.js +1497 -0
- package/public/manifest.webmanifest +14 -0
- package/public/nostromo-engine.js +2222 -0
- package/public/resay.js +24 -0
- package/public/review-ask.js +85 -0
- package/public/salas-mac.css +787 -0
- package/public/salas-mac.html +154 -0
- package/public/salas-mac.js +3264 -0
- package/public/salas.css +85 -0
- package/public/salas.html +47 -0
- package/public/salas.js +200 -0
- package/public/styles.css +6207 -425
- package/public/touch.js +83 -0
- package/public/tour-chapters.js +342 -0
- package/public/tour.js +510 -0
- package/public/troubleshooting.js +159 -140
- package/src/adapters/codex.mjs +46 -6
- package/src/adapters/gemini.mjs +30 -10
- package/src/adapters/madre.mjs +110 -18
- package/src/adapters/process.mjs +42 -2
- package/src/auth-probe.mjs +2 -2
- package/src/bridge/devices.mjs +202 -0
- package/src/bridge/handoff.mjs +45 -0
- package/src/bridge/host.mjs +82 -0
- package/src/bridge/network.mjs +133 -0
- package/src/bridge/policy.mjs +89 -0
- package/src/bridge/qr.mjs +287 -0
- package/src/bridge/server.mjs +269 -0
- package/src/capabilities.mjs +5 -9
- package/src/chats.mjs +59 -22
- package/src/checkpoint.mjs +137 -35
- package/src/choices.mjs +18 -3
- package/src/cli-args.mjs +12 -1
- package/src/commands.mjs +15 -15
- package/src/config.mjs +73 -9
- package/src/conversation-context.mjs +5 -2
- package/src/dataset.mjs +3 -1
- package/src/directives.mjs +0 -5
- package/src/enlaces.mjs +451 -0
- package/src/event-store.mjs +55 -10
- package/src/exam.mjs +5 -5
- package/src/extensions.mjs +0 -2
- package/src/file-ops.mjs +23 -0
- package/src/files.mjs +52 -2
- package/src/github.mjs +407 -0
- package/src/json-file.mjs +63 -0
- package/src/lease.mjs +2 -2
- package/src/lobby-work.mjs +103 -0
- package/src/looks.mjs +55 -0
- package/src/maturity.mjs +3 -3
- package/src/mcp/github-server.mjs +164 -0
- package/src/mcp/image-server.mjs +1 -1
- package/src/mcp/memory-server.mjs +11 -3
- package/src/memory.mjs +38 -12
- package/src/models.mjs +89 -5
- package/src/modules/ahp.mjs +4 -4
- package/src/modules/ash.mjs +1 -1
- package/src/modules/bishop-plan.mjs +207 -0
- package/src/modules/bishop.mjs +645 -0
- package/src/modules/choices.mjs +2 -2
- package/src/modules/correo.mjs +1 -1
- package/src/modules/enlaces.mjs +255 -0
- package/src/modules/git-pulse.mjs +2 -2
- package/src/modules/github.mjs +322 -0
- package/src/modules/image-studio.mjs +3 -3
- package/src/modules/index.mjs +18 -4
- package/src/modules/intents-calibration.mjs +338 -0
- package/src/modules/intents-desk.mjs +481 -0
- package/src/modules/intents-observe.mjs +266 -0
- package/src/modules/intents-policy.mjs +414 -0
- package/src/modules/intents-try.mjs +91 -0
- package/src/modules/intents.mjs +145 -0
- package/src/modules/ollama.mjs +4 -4
- package/src/modules/playwright.mjs +121 -11
- package/src/modules/ripley.mjs +1 -1
- package/src/modules/runs.mjs +2 -2
- package/src/modules/sdk.mjs +10 -4
- package/src/ollama.mjs +6 -1
- package/src/outbound.mjs +19 -17
- package/src/override.mjs +100 -0
- package/src/ports.mjs +23 -1
- package/src/prices.mjs +72 -0
- package/src/privacy.mjs +30 -11
- package/src/quota-sources.mjs +0 -8
- package/src/request-guard.mjs +1 -1
- package/src/review-ask.mjs +111 -0
- package/src/room/archivist.mjs +55 -37
- package/src/room/budget.mjs +17 -5
- package/src/room/control.mjs +70 -7
- package/src/room/economy.mjs +35 -7
- package/src/room/escalation.mjs +2 -0
- package/src/room/guard.mjs +26 -2
- package/src/room/prompt.mjs +22 -15
- package/src/room/runs.mjs +84 -21
- package/src/room/vectors.mjs +19 -4
- package/src/room.mjs +351 -65
- package/src/router.mjs +3 -1
- package/src/runs.mjs +23 -5
- package/src/salas.mjs +313 -0
- package/src/sentinel-errors.mjs +1 -1
- package/src/server-sentinel.mjs +124 -0
- package/src/server.mjs +1526 -163
- package/src/setup.mjs +1 -0
- package/src/tour.mjs +159 -0
- package/src/updates.mjs +48 -8
- package/src/usage-sentinel.mjs +1 -1
- package/src/verdict.mjs +7 -7
- package/src/whats-new.mjs +160 -0
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,159 @@
|
|
|
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
|
+
export const TOUR_VERSION = '1.0';
|
|
24
|
+
// The chapters, in their order; «moved» is only for whoever comes from 0.7.2.
|
|
25
|
+
export const CHAPTERS = ['moved', 'home', 'project', 'talk', 'modes', 'coordinator', 'links', 'maps', 'settings', 'modules', 'phone', 'gestures'];
|
|
26
|
+
// The places that confuse most, each told once in one line (from option C).
|
|
27
|
+
export const HINTS = ['switch', 'links', 'key4'];
|
|
28
|
+
const PATHS = ['new', 'back'];
|
|
29
|
+
|
|
30
|
+
const fresh = () => ({ version: TOUR_VERSION, path: null, bonjourAt: null, done: [], at: null, closed: false, finished: null, hints: {}, held: null });
|
|
31
|
+
// A tab that shows the bonjour or the chapters says so every few seconds; another tab that finds it
|
|
32
|
+
// said within this long tells the person it is open there, instead of a second bonjour.
|
|
33
|
+
export const HELD_MS = 45 * 1000;
|
|
34
|
+
const TAB = /^[\w-]{6,64}$/;
|
|
35
|
+
const tourFile = (stateRoot) => join(stateRoot, 'tour.json');
|
|
36
|
+
|
|
37
|
+
// What is kept, checked field by field: a file written by hand or by an older MADRE is read for
|
|
38
|
+
// what it says correctly and nothing else. A file for another version starts the tour again.
|
|
39
|
+
export async function readTour(stateRoot) {
|
|
40
|
+
const raw = await readFile(tourFile(stateRoot), 'utf8').then((text) => JSON.parse(text)).catch(() => null);
|
|
41
|
+
if (!raw || typeof raw !== 'object' || raw.version !== TOUR_VERSION) return fresh();
|
|
42
|
+
const iso = (value) => (typeof value === 'string' && !Number.isNaN(Date.parse(value)) ? value : null);
|
|
43
|
+
return {
|
|
44
|
+
version: TOUR_VERSION,
|
|
45
|
+
path: PATHS.includes(raw.path) ? raw.path : null,
|
|
46
|
+
bonjourAt: iso(raw.bonjourAt),
|
|
47
|
+
done: Array.isArray(raw.done) ? CHAPTERS.filter((id) => raw.done.includes(id)) : [],
|
|
48
|
+
at: CHAPTERS.includes(raw.at) ? raw.at : null,
|
|
49
|
+
closed: raw.closed === true,
|
|
50
|
+
finished: iso(raw.finished),
|
|
51
|
+
hints: Object.fromEntries(HINTS.filter((id) => iso(raw.hints?.[id])).map((id) => [id, raw.hints[id]])),
|
|
52
|
+
held: raw.held && TAB.test(String(raw.held.by ?? '')) && iso(raw.held.at) ? { by: raw.held.by, at: raw.held.at } : null,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function writeTour(stateRoot, state) {
|
|
57
|
+
await writeWhole(tourFile(stateRoot), `${JSON.stringify(state, null, 2)}\n`);
|
|
58
|
+
return state;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// One change at a time, from the page: the path taken at the bonjour, a chapter read, where the
|
|
62
|
+
// person is, closed to go on later, finished, a hint seen, or everything again from the start.
|
|
63
|
+
// One change at a time (src/json-file.mjs): every tab says it holds the tour every few seconds,
|
|
64
|
+
// through whichever project it is on, and a chapter read in between was lost to it.
|
|
65
|
+
export function changeTour(stateRoot, change, now = new Date()) {
|
|
66
|
+
return oneAtATime(tourFile(stateRoot), () => applyChange(stateRoot, change, now));
|
|
67
|
+
}
|
|
68
|
+
async function applyChange(stateRoot, change, now) {
|
|
69
|
+
if (!change || typeof change !== 'object') return { error: 'change' };
|
|
70
|
+
if (change.reset === true) return writeTour(stateRoot, fresh());
|
|
71
|
+
const state = await readTour(stateRoot);
|
|
72
|
+
const at = new Date(now).toISOString();
|
|
73
|
+
if (change.path !== undefined) {
|
|
74
|
+
if (!PATHS.includes(change.path)) return { error: 'path' };
|
|
75
|
+
state.path = change.path;
|
|
76
|
+
}
|
|
77
|
+
if (change.bonjour === true) state.bonjourAt ??= at;
|
|
78
|
+
if (change.chapter !== undefined) {
|
|
79
|
+
if (!CHAPTERS.includes(change.chapter)) return { error: 'chapter' };
|
|
80
|
+
if (!state.done.includes(change.chapter)) state.done = CHAPTERS.filter((id) => id === change.chapter || state.done.includes(id));
|
|
81
|
+
}
|
|
82
|
+
if (change.at !== undefined) {
|
|
83
|
+
if (change.at !== null && !CHAPTERS.includes(change.at)) return { error: 'chapter' };
|
|
84
|
+
state.at = change.at;
|
|
85
|
+
}
|
|
86
|
+
if (change.closed !== undefined) state.closed = change.closed === true;
|
|
87
|
+
if (change.finished === true) { state.finished ??= at; state.closed = false; }
|
|
88
|
+
// Which tab has the tour open now (one says it every few seconds while it does), and that it
|
|
89
|
+
// let go — only the tab that holds it lets go.
|
|
90
|
+
if (change.hold !== undefined) {
|
|
91
|
+
if (!TAB.test(String(change.hold))) return { error: 'tab' };
|
|
92
|
+
state.held = { by: change.hold, at };
|
|
93
|
+
}
|
|
94
|
+
if (change.release !== undefined && state.held?.by === change.release) state.held = null;
|
|
95
|
+
if (change.hint !== undefined) {
|
|
96
|
+
if (!HINTS.includes(change.hint)) return { error: 'hint' };
|
|
97
|
+
state.hints[change.hint] ??= at;
|
|
98
|
+
}
|
|
99
|
+
return writeTour(stateRoot, state);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Whoever already used MADRE wrote in it: a message of theirs in the record of a room. Having a room
|
|
103
|
+
// proves nothing — a room is made with its record and its memory the moment it opens, a new
|
|
104
|
+
// person's first `madre start` too (2026-10-11) — and a project added in Home and never opened has
|
|
105
|
+
// not even that. The Coordinator's own room is not counted either.
|
|
106
|
+
const PERSON_SPOKE = /"type":"message\.created".*"role":"user"/;
|
|
107
|
+
async function spoke(file) {
|
|
108
|
+
const lines = createInterface({ input: createReadStream(file, { encoding: 'utf8' }), crlfDelay: Infinity });
|
|
109
|
+
try {
|
|
110
|
+
for await (const line of lines) if (PERSON_SPOKE.test(line)) return true;
|
|
111
|
+
} catch { /* unreadable: not proof of anything */ } finally {
|
|
112
|
+
lines.close();
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
export async function cameBefore(stateRoot) {
|
|
117
|
+
const dir = join(stateRoot, 'rooms');
|
|
118
|
+
const salas = salasRoomId(stateRoot);
|
|
119
|
+
for (const id of await readdir(dir).catch(() => [])) {
|
|
120
|
+
if (id === salas) continue;
|
|
121
|
+
const chats = await readdir(join(dir, id, 'chats')).catch(() => []);
|
|
122
|
+
for (const file of [join(dir, id, 'events.jsonl'), ...chats.map((chat) => join(dir, id, 'chats', chat, 'events.jsonl'))]) {
|
|
123
|
+
if (await stat(file).then((info) => info.isFile(), () => false) && await spoke(file)) return true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// The rooms Home cannot list: no record of where their project is, and not set aside on purpose.
|
|
130
|
+
export async function roomsWithoutProject(stateRoot) {
|
|
131
|
+
const dir = join(stateRoot, 'rooms');
|
|
132
|
+
const salas = salasRoomId(stateRoot);
|
|
133
|
+
const out = [];
|
|
134
|
+
for (const id of (await readdir(dir).catch(() => [])).sort()) {
|
|
135
|
+
if (id === salas || !/-[0-9a-f]{16}$/.test(id)) continue;
|
|
136
|
+
const has = (name) => stat(join(dir, id, name)).then(() => true, () => false);
|
|
137
|
+
if (await has('project.json') || await has('project.forgotten.json')) continue;
|
|
138
|
+
if (!(await has('events.jsonl')) && !(await has('memory.sqlite'))) continue;
|
|
139
|
+
out.push({ id, name: id.replace(/-[0-9a-f]{16}$/, '') });
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// One room: its folder looked for by name where people keep their projects, proved by its hash,
|
|
145
|
+
// and written beside its record as adding it in Home would. Answers where it was, or that it was
|
|
146
|
+
// not found (a folder that was deleted, moved, or lived somewhere else).
|
|
147
|
+
export async function recoverProject({ stateRoot, home, id }) {
|
|
148
|
+
const room = (await roomsWithoutProject(stateRoot)).find((one) => one.id === id);
|
|
149
|
+
if (!room) return { error: 'unknown' };
|
|
150
|
+
for (const where of PROJECT_HOMES) {
|
|
151
|
+
const candidate = join(home, where, room.name);
|
|
152
|
+
const real = await realpath(candidate).catch(() => null);
|
|
153
|
+
if (!real || projectRoomId(real) !== id) continue;
|
|
154
|
+
const added = await associateProject({ root: real, home, stateRoot });
|
|
155
|
+
if (added.error) continue;
|
|
156
|
+
return { id, name: added.name, root: added.root, found: true };
|
|
157
|
+
}
|
|
158
|
+
return { id, name: room.name, found: false };
|
|
159
|
+
}
|
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
|
-
|
|
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
|
|
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 ${
|
|
48
|
-
case 'project': return `npm install ${
|
|
49
|
-
case 'global': return `npm install -g ${
|
|
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
|
|
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 (
|
|
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';
|
package/src/usage-sentinel.mjs
CHANGED
|
@@ -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
|
|
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: '
|
|
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
|
|
43
|
-
: { text: t('Keep working in the
|
|
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
|
|
58
|
-
next: { text: t('Run the third test: it puts real questions from this
|
|
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
|
|
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
|
|
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 el remedio', en: 'Diagnostics gives you the remedy' },
|
|
55
|
+
body: {
|
|
56
|
+
es: 'Escribe lo que pasa con tus palabras y ves 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, a partir de tus propios turnos 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 qué abre #3 y #4: el nombre del proyecto, una frase tuya o, en #3, solo confirmar. Una llave aceptada puede valer unos minutos, y el modo 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, CI e issues con /gh, usando la sesión de gh que ya tienes. Antes de publicar algo te enseña qué saldría y te pregunta, y 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 cerebro de la memoria tiene su propio botón en la barra. Las conexiones 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 viven dentro del proyecto y el visor 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 turnos', 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 acentos 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 y las llaves 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.
|