@jossuealcala/madre 0.3.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +412 -3
  2. package/CONTRIBUTING.md +3 -1
  3. package/README.md +67 -185
  4. package/SECURITY.md +2 -1
  5. package/bin/madre.mjs +56 -13
  6. package/docs/INTERNALS.md +16 -0
  7. package/docs/REFERENCE.md +249 -0
  8. package/docs/SDK.md +121 -0
  9. package/docs/room.png +0 -0
  10. package/docs/sdk/hello-module.mjs +51 -0
  11. package/package.json +9 -1
  12. package/public/app.js +3825 -851
  13. package/public/es.js +2050 -0
  14. package/public/i18n.js +66 -0
  15. package/public/index.html +94 -13
  16. package/public/inquiry.js +220 -0
  17. package/public/resay.js +77 -0
  18. package/public/styles.css +602 -62
  19. package/public/troubleshooting.js +168 -46
  20. package/src/adapters/claude.mjs +2 -1
  21. package/src/adapters/codex.mjs +2 -1
  22. package/src/adapters/gemini.mjs +6 -5
  23. package/src/adapters/opencode.mjs +2 -1
  24. package/src/adapters/process.mjs +17 -5
  25. package/src/asking.mjs +128 -0
  26. package/src/auth-probe.mjs +58 -1
  27. package/src/chats.mjs +193 -0
  28. package/src/checkpoint.mjs +1 -1
  29. package/src/cold.mjs +56 -0
  30. package/src/commands.mjs +6 -0
  31. package/src/conversation-context.mjs +35 -3
  32. package/src/credentials.mjs +145 -0
  33. package/src/dataset.mjs +56 -4
  34. package/src/distiller.mjs +12 -5
  35. package/src/event-store.mjs +14 -8
  36. package/src/exam.mjs +240 -0
  37. package/src/extensions.mjs +3 -2
  38. package/src/eyecat-watch.mjs +100 -0
  39. package/src/eyecat.mjs +169 -0
  40. package/src/i18n.mjs +47 -0
  41. package/src/image-studio.mjs +2 -0
  42. package/src/launch.mjs +61 -0
  43. package/src/maturity.mjs +94 -0
  44. package/src/mcp/image-server.mjs +12 -1
  45. package/src/mcp/memory-server.mjs +1 -1
  46. package/src/memory.mjs +325 -17
  47. package/src/modules/ahp.mjs +9 -7
  48. package/src/modules/ash.mjs +36 -0
  49. package/src/modules/git-pulse.mjs +5 -3
  50. package/src/modules/helpers.mjs +31 -0
  51. package/src/modules/image-studio.mjs +9 -4
  52. package/src/modules/index.mjs +141 -9
  53. package/src/modules/ollama.mjs +66 -10
  54. package/src/modules/playwright.mjs +36 -19
  55. package/src/modules/ripley.mjs +5 -3
  56. package/src/modules/sdk.mjs +93 -2
  57. package/src/modules/updates.mjs +81 -0
  58. package/src/ollama.mjs +5 -2
  59. package/src/outbound.mjs +292 -0
  60. package/src/privacy.mjs +54 -7
  61. package/src/room/context.mjs +4 -4
  62. package/src/room/economy.mjs +161 -0
  63. package/src/room/prompt.mjs +118 -46
  64. package/src/room.mjs +443 -44
  65. package/src/runtime-detection.mjs +27 -8
  66. package/src/server.mjs +699 -69
  67. package/src/setup.mjs +1 -1
  68. package/src/updates.mjs +4 -2
  69. package/src/usage-sentinel.mjs +13 -8
  70. package/src/verdict.mjs +74 -0
  71. package/src/ashcode.mjs +0 -64
  72. 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. CONFIGURE ONE TO OPEN THE ROOM.`;
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
@@ -61,14 +61,16 @@ async function readCache(file) {
61
61
  }
62
62
 
63
63
  // The check. `enabled: false` answers from the cache only and never touches the network.
64
- export async function checkForUpdate({ name, current, cacheFile, fetchImpl = globalThis.fetch, now = Date.now(), ttlMs = UPDATE_TTL_MS, enabled = true, force = false, timeoutMs = 4000 } = {}) {
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 } = {}) {
65
67
  const cached = cacheFile ? await readCache(cacheFile) : null;
66
68
  const fresh = cached && cached.name === name && Number.isFinite(cached.checkedAt) && now - cached.checkedAt < ttlMs;
67
69
  let latest = cached?.name === name ? cached.latest ?? null : null;
68
70
  let checkedAt = cached?.name === name ? cached.checkedAt ?? null : null;
69
71
  let source = latest ? 'cache' : 'none';
70
72
  let error = null;
71
- if (enabled && (force || !fresh)) {
73
+ if (enabled && !cacheOnly && (force || !fresh)) {
72
74
  try {
73
75
  const response = await fetchImpl(`${REGISTRY}/${encodeURIComponent(name).replace('%40', '@')}/latest`, { headers: { accept: 'application/json' }, signal: AbortSignal.timeout(timeoutMs) });
74
76
  if (!response.ok) throw new Error(`registry HTTP ${response.status}`);
@@ -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
- ? ` Continue with ${alternatives.map((item) => `@${item}`).join(' or ')}.`
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
- ? 'local room token budget (MADRE\'s own soft limit, not the provider\'s quota; cache reads count a tenth)'
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
- ? `has used ${Math.round(percent)}% of its ${label} and another turn like the last one would reach ${Math.round(projected)}%.`
53
- : `has used ${Math.round(percent)}% of its ${label}.`;
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
- message: `MADRE ${level}: @${agent} ${usage}${destination}`,
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
  }
@@ -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.';
@@ -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
- });