@jossuealcala/madre 0.3.2 → 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 +425 -0
- package/CONTRIBUTING.md +6 -1
- package/README.md +67 -183
- 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 +3880 -849
- package/public/es.js +2050 -0
- package/public/i18n.js +66 -0
- package/public/index.html +95 -14
- package/public/inquiry.js +220 -0
- package/public/resay.js +77 -0
- package/public/styles.css +626 -68
- package/public/troubleshooting.js +173 -51
- package/src/adapters/claude.mjs +13 -6
- package/src/adapters/codex.mjs +17 -13
- package/src/adapters/gemini.mjs +27 -16
- package/src/adapters/opencode.mjs +16 -12
- package/src/adapters/process.mjs +17 -5
- package/src/asking.mjs +128 -0
- package/src/auth-probe.mjs +58 -1
- package/src/capabilities.mjs +4 -3
- package/src/chats.mjs +193 -0
- package/src/checkpoint.mjs +1 -1
- package/src/cold.mjs +56 -0
- package/src/commands.mjs +31 -3
- 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/lease.mjs +5 -3
- 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 +6 -4
- package/src/modules/helpers.mjs +31 -0
- package/src/modules/image-studio.mjs +9 -4
- package/src/modules/index.mjs +143 -5
- package/src/modules/ollama.mjs +66 -10
- package/src/modules/playwright.mjs +90 -0
- package/src/modules/ripley.mjs +5 -3
- package/src/modules/sdk.mjs +104 -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/control.mjs +4 -4
- package/src/room/economy.mjs +161 -0
- package/src/room/prompt.mjs +118 -43
- package/src/room.mjs +465 -58
- package/src/runtime-detection.mjs +27 -8
- package/src/server.mjs +724 -68
- package/src/setup.mjs +1 -1
- package/src/updates.mjs +17 -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/src/setup.mjs
CHANGED
|
@@ -103,7 +103,7 @@ export function renderReport({ agents, probes, projectRoot, config = {}, palette
|
|
|
103
103
|
const verdict = online.length
|
|
104
104
|
? `${online.length} OF ${agents.length} AGENTS ONLINE${local ? ' · @MADRE LOCAL' : ''}. ROOM CAN OPEN.`
|
|
105
105
|
: local ? 'NO CLI ONLINE · @MADRE (LOCAL) IS. THE ROOM CAN OPEN WITH MEMORY ALONE; CONNECT A CLI TO WORK ON FILES.'
|
|
106
|
-
: `NO AGENT ONLINE.
|
|
106
|
+
: `NO AGENT ONLINE. THE ROOM OPENS ANYWAY AND WALKS YOU THROUGH CONNECTING ONE.`;
|
|
107
107
|
lines.push('');
|
|
108
108
|
lines.push(` ${p.phosphor('MOTHER')}${p.dim(' ›')} ${p.bold(verdict)}`);
|
|
109
109
|
return lines.join('\n');
|
package/src/updates.mjs
CHANGED
|
@@ -38,6 +38,19 @@ export function updateCommand(install, name, version = 'latest') {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
// One shell line that installs the version and starts the same room on the same port.
|
|
42
|
+
// The room's own process runs it detached after closing its listener, then exits.
|
|
43
|
+
const shq = (value) => `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
44
|
+
export function applyCommand({ install, name, version, port, projectRoot }) {
|
|
45
|
+
const start = `start --no-open --port ${Number(port) || 4317} --project ${shq(projectRoot)}`;
|
|
46
|
+
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}`;
|
|
50
|
+
default: return null; // source: the human pulls
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
41
54
|
export function releaseUrl(repository, version) {
|
|
42
55
|
const url = String(repository?.url ?? repository ?? '').replace(/^git\+/, '').replace(/\.git$/, '');
|
|
43
56
|
return url ? `${url}/releases/tag/v${version}` : null;
|
|
@@ -48,14 +61,16 @@ async function readCache(file) {
|
|
|
48
61
|
}
|
|
49
62
|
|
|
50
63
|
// The check. `enabled: false` answers from the cache only and never touches the network.
|
|
51
|
-
|
|
64
|
+
// `cacheOnly` answers from what is already on disk and never touches the network: it is what a
|
|
65
|
+
// screen asks for, so opening MODULES never waits on a registry.
|
|
66
|
+
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 } = {}) {
|
|
52
67
|
const cached = cacheFile ? await readCache(cacheFile) : null;
|
|
53
68
|
const fresh = cached && cached.name === name && Number.isFinite(cached.checkedAt) && now - cached.checkedAt < ttlMs;
|
|
54
69
|
let latest = cached?.name === name ? cached.latest ?? null : null;
|
|
55
70
|
let checkedAt = cached?.name === name ? cached.checkedAt ?? null : null;
|
|
56
71
|
let source = latest ? 'cache' : 'none';
|
|
57
72
|
let error = null;
|
|
58
|
-
if (enabled && (force || !fresh)) {
|
|
73
|
+
if (enabled && !cacheOnly && (force || !fresh)) {
|
|
59
74
|
try {
|
|
60
75
|
const response = await fetchImpl(`${REGISTRY}/${encodeURIComponent(name).replace('%40', '@')}/latest`, { headers: { accept: 'application/json' }, signal: AbortSignal.timeout(timeoutMs) });
|
|
61
76
|
if (!response.ok) throw new Error(`registry HTTP ${response.status}`);
|
package/src/usage-sentinel.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { t } from './i18n.mjs';
|
|
2
|
+
|
|
1
3
|
const severity = { normal: 0, warning: 1, critical: 2, exhausted: 3 };
|
|
2
4
|
|
|
3
5
|
export function classifyUsagePercent(value) {
|
|
@@ -40,17 +42,19 @@ export class UsageSentinel {
|
|
|
40
42
|
}
|
|
41
43
|
if (level === 'normal' || severity[level] <= severity[previous]) return null;
|
|
42
44
|
|
|
45
|
+
// Said in the room's language: this is MADRE's own voice, not a CLI's output. What is
|
|
46
|
+
// already in the ledger keeps the words it was written with.
|
|
43
47
|
const destination = alternatives.length
|
|
44
|
-
?
|
|
45
|
-
: ' Prepare a handoff before the current agent becomes unavailable.';
|
|
48
|
+
? t(' Continue with {who}.', { who: alternatives.map((item) => `@${item}`).join(t(' or ')) })
|
|
49
|
+
: t(' Prepare a handoff before the current agent becomes unavailable.');
|
|
46
50
|
const label = source === 'room-soft-budget'
|
|
47
|
-
?
|
|
51
|
+
? t("local room token budget (MADRE's own soft limit, not the provider's quota; cache reads count a tenth)")
|
|
48
52
|
: source === 'test-simulation'
|
|
49
|
-
? 'simulated provider usage window'
|
|
50
|
-
: 'provider usage window';
|
|
53
|
+
? t('simulated provider usage window')
|
|
54
|
+
: t('provider usage window');
|
|
51
55
|
const usage = byProjection
|
|
52
|
-
?
|
|
53
|
-
:
|
|
56
|
+
? t('has used {pct}% of its {label} and another turn like the last one would reach {projected}%.', { pct: Math.round(percent), label, projected: Math.round(projected) })
|
|
57
|
+
: t('has used {pct}% of its {label}.', { pct: Math.round(percent), label });
|
|
54
58
|
return {
|
|
55
59
|
agent,
|
|
56
60
|
usedPercent: percent,
|
|
@@ -59,7 +63,8 @@ export class UsageSentinel {
|
|
|
59
63
|
source,
|
|
60
64
|
resetAt,
|
|
61
65
|
alternatives,
|
|
62
|
-
|
|
66
|
+
// The level is a word on a screen, so it is said in the room's language too.
|
|
67
|
+
message: `MADRE ${t(level)}: @${agent} ${usage}${destination}`,
|
|
63
68
|
};
|
|
64
69
|
}
|
|
65
70
|
}
|
package/src/verdict.mjs
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// One sentence about where a room stands, and one thing to do about it.
|
|
2
|
+
//
|
|
3
|
+
// Everything needed to say it was already being measured: six readings of what the archive is
|
|
4
|
+
// made of, three tests of whether it works, the memories nothing has ever reached for, and the
|
|
5
|
+
// questions the room cannot answer. What was missing was somebody deciding which of those matters
|
|
6
|
+
// today. Nine numbers on a screen is not a verdict — it is homework.
|
|
7
|
+
//
|
|
8
|
+
// The order is the order a person should act in, and it is not negotiable by score: something the
|
|
9
|
+
// room believes that is false outranks something the room has not learned yet, and both outrank
|
|
10
|
+
// anything about size. Whatever is chosen, it is one thing, and it says where to go and do it.
|
|
11
|
+
|
|
12
|
+
// Where to go and do it. These name places on the screen, so they are said in the room's
|
|
13
|
+
// language and read at call time, not when this file is imported.
|
|
14
|
+
import { t } from './i18n.mjs';
|
|
15
|
+
|
|
16
|
+
export const WHERE = { nostromo: 'NOSTROMO', ask: 'NOSTROMO · ASK', room: 'THE ROOM', tests: 'THE THREE TESTS' };
|
|
17
|
+
const where = (key) => t(WHERE[key]);
|
|
18
|
+
|
|
19
|
+
export function verdictFor({ maturity = null, exams = {}, cold = null, asks = 0 } = {}) {
|
|
20
|
+
const consistency = exams.consistency ?? null;
|
|
21
|
+
const coverage = exams.coverage ?? null;
|
|
22
|
+
const match = exams.match ?? null;
|
|
23
|
+
const stage = maturity?.stage ?? null;
|
|
24
|
+
const weakest = maturity?.signals?.find((signal) => signal.id === maturity.weakest) ?? null;
|
|
25
|
+
|
|
26
|
+
// 1 · Something false, first. An archive that contradicts itself teaches the contradiction.
|
|
27
|
+
if (consistency?.ran && consistency.open > 0) {
|
|
28
|
+
return {
|
|
29
|
+
headline: stage?.label ?? null,
|
|
30
|
+
says: consistency.says,
|
|
31
|
+
next: { text: t('Settle the {n} contradictions EYECAT is holding. Until they are settled, everything built on this archive inherits them.', { n: consistency.open }), where: where('nostromo') },
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 2 · Then what it cannot answer. The room already wrote the questions; pointing at them is
|
|
36
|
+
// more use than repeating the number.
|
|
37
|
+
if (coverage?.ran && !coverage.passed) {
|
|
38
|
+
return {
|
|
39
|
+
headline: stage?.label ?? null,
|
|
40
|
+
says: coverage.says,
|
|
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') },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 3 · Then whatever the readings say is thinnest. That advice is already written where the
|
|
48
|
+
// reading is taken, so it is quoted rather than invented here.
|
|
49
|
+
if (weakest && weakest.value < 0.6) {
|
|
50
|
+
return { headline: stage?.label ?? null, says: `${weakest.label.toLowerCase()}: ${weakest.detail}.`, next: { text: weakest.next, where: where('room') } };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 4 · And when nothing is wrong, the only honest thing left is the test nobody has run.
|
|
54
|
+
if (!match?.ran) {
|
|
55
|
+
return {
|
|
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') },
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const dead = cold?.count ?? 0;
|
|
63
|
+
return {
|
|
64
|
+
headline: stage?.label ?? null,
|
|
65
|
+
says: match.passed
|
|
66
|
+
? `${match.says} ${t('This room is ready to be worked in with the local model.')}`
|
|
67
|
+
: match.says,
|
|
68
|
+
next: match.passed
|
|
69
|
+
? (dead
|
|
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') })
|
|
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
|
+
};
|
|
74
|
+
}
|
package/src/ashcode.mjs
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
// AshCode / ORDER 937 is a deliberately conservative, local text transform.
|
|
2
|
-
// It is lossy, not encryption or a model tokenizer. Keep the original in the
|
|
3
|
-
// event log so the human can inspect what was actually abbreviated.
|
|
4
|
-
|
|
5
|
-
const SPANISH = /\b(?:el|la|los|las|un|una|unos|unas|para|proyecto|página|pagina|imagen|crear|genera|dentro)\b/gi;
|
|
6
|
-
const ENGLISH = /\b(?:the|a|an|for|project|page|image|create|generate|inside|please)\b/gi;
|
|
7
|
-
const PROTECTED = /"[^"\n]*"|'[^'\n]*'|@[\p{L}\p{N}_-]+|![\p{L}\p{N}_./:-]+/gu;
|
|
8
|
-
const RISKY = /```|`|https?:\/\/|\n|[{}<>]|\b(?:no|nunca|sin|excepto|not|never|without|except)\b|\d|(?:^|\s)(?:\.{1,2}\/|\/|[\w.-]+\/)[\w./-]+/iu;
|
|
9
|
-
|
|
10
|
-
export function ashLanguage(input) {
|
|
11
|
-
const text = String(input ?? '');
|
|
12
|
-
const es = (text.match(SPANISH) ?? []).length + (/[áéíóúñ¿¡]/i.test(text) ? 2 : 0);
|
|
13
|
-
const en = (text.match(ENGLISH) ?? []).length;
|
|
14
|
-
if (es === en || Math.max(es, en) < 2) return null;
|
|
15
|
-
return es > en ? 'es' : 'en';
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function compressAshCode(input) {
|
|
19
|
-
const original = String(input ?? '').trim();
|
|
20
|
-
const unchanged = (reason) => ({ text: original, original, applied: false, reason, originalChars: original.length, encodedChars: original.length, language: null });
|
|
21
|
-
if (!original) return unchanged('empty');
|
|
22
|
-
if (RISKY.test(original) || /(?:^|\s)\/[A-Za-z_.]/.test(original)) return unchanged('protected-structure');
|
|
23
|
-
const language = ashLanguage(original);
|
|
24
|
-
if (!language) return unchanged('language-uncertain');
|
|
25
|
-
|
|
26
|
-
const protectedSpans = [];
|
|
27
|
-
let text = original.replace(PROTECTED, (span) => {
|
|
28
|
-
const index = protectedSpans.push(span) - 1;
|
|
29
|
-
return `\uE000${index}\uE001`;
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
if (language === 'es') {
|
|
33
|
-
text = text
|
|
34
|
-
.replace(/\b(?:yo\s+)?(?:quiero|necesito|quisiera)\s+que\s+/gi, '')
|
|
35
|
-
.replace(/\bme gustaría que\s+/gi, '')
|
|
36
|
-
.replace(/\bpor favor\b[,.]?\s*/gi, '')
|
|
37
|
-
.replace(/\bdentro del?\b/gi, 'en')
|
|
38
|
-
.replace(/\b(?:el|la|los|las|un|una|unos|unas)\s+/g, '')
|
|
39
|
-
.replace(/\bhome de (?:la )?(?:página|pagina) principal\b/gi, 'home');
|
|
40
|
-
} else {
|
|
41
|
-
text = text
|
|
42
|
-
.replace(/\bI (?:would like|want|need) you to\s+/gi, '')
|
|
43
|
-
.replace(/\b(?:could|can) you\s+/gi, '')
|
|
44
|
-
.replace(/\bplease\b[,.]?\s*/gi, '')
|
|
45
|
-
.replace(/\binside of\b/gi, 'in')
|
|
46
|
-
.replace(/\b(?:the|a|an)\s+/g, '')
|
|
47
|
-
.replace(/\bhome page of (?:the )?main (?:web)?site\b/gi, 'home page');
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
text = text
|
|
51
|
-
.replace(/\b([\p{L}]+)(?:\s+\1)+\b/giu, '$1')
|
|
52
|
-
.replace(/\s+([,.;:!?])/g, '$1')
|
|
53
|
-
.replace(/\s{2,}/g, ' ')
|
|
54
|
-
.trim()
|
|
55
|
-
.replace(/[.。]$/, '');
|
|
56
|
-
text = text.replace(/\uE000(\d+)\uE001/g, (_, index) => protectedSpans[Number(index)]);
|
|
57
|
-
const encoded = `ASH937/${language}: ${text}`;
|
|
58
|
-
// A shorter character string is not proof of fewer model tokens. Refuse
|
|
59
|
-
// marginal rewrites because the prefix itself may erase any gain.
|
|
60
|
-
if (encoded.length > original.length - 8) return unchanged('no-safe-gain');
|
|
61
|
-
return { text: encoded, original, applied: true, reason: null, originalChars: original.length, encodedChars: encoded.length, language };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export const ASHCODE_BETA_WARNING = 'AshCode beta: abbreviation can change meaning or cause errors. Review the original before relying on a result; character reduction is not verified token savings.';
|
package/src/modules/ashcode.mjs
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
// AshCode: opt-in abbreviation of prompts and replies. Beta, so the switch asks
|
|
2
|
-
// for confirmation and the room says so every time it turns on.
|
|
3
|
-
|
|
4
|
-
import { defineModule } from './sdk.mjs';
|
|
5
|
-
|
|
6
|
-
const WARNING = 'Beta: abbreviation may alter meaning or introduce errors. Review the original. Character reduction is not verified token savings.';
|
|
7
|
-
|
|
8
|
-
export default defineModule({
|
|
9
|
-
id: 'ashcode',
|
|
10
|
-
configKey: 'ashCode',
|
|
11
|
-
name: 'AshCode',
|
|
12
|
-
vendor: 'MADRE · ORDER 937',
|
|
13
|
-
version: '0.1.0-beta',
|
|
14
|
-
summary: 'Opt-in Spanish/English prompt and reply abbreviation. Keeps the original visible; may change meaning. Shorter characters do not guarantee fewer provider tokens.',
|
|
15
|
-
creates: ['nothing in the project', 'an ashCode switch in ~/.pulse/config.json', 'original and abbreviated text in the room event log when applied'],
|
|
16
|
-
card: 'ashcode',
|
|
17
|
-
confirm: 'AshCode is beta and can change meaning. Send { "confirm": true } to enable it.',
|
|
18
|
-
toggledEvent: { beta: true },
|
|
19
|
-
toggledBody: () => ({ beta: true, warning: 'AshCode beta may alter meaning; review the original. Character reduction is not verified token savings.' }),
|
|
20
|
-
async status(ctx) {
|
|
21
|
-
return {
|
|
22
|
-
status: { installed: Boolean(ctx.settings.enabled), detail: ctx.settings.enabled ? 'on · beta · ORDER 937 available' : 'off · beta' },
|
|
23
|
-
install: { display: ctx.settings.enabled ? 'disable AshCode' : 'enable AshCode (config.json)', platforms: [] },
|
|
24
|
-
warning: WARNING,
|
|
25
|
-
};
|
|
26
|
-
},
|
|
27
|
-
async onToggle(ctx, enabled) { ctx.room.setAshCode(enabled); return null; },
|
|
28
|
-
});
|