@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
|
@@ -1,51 +1,49 @@
|
|
|
1
|
+
import { t } from './i18n.js';
|
|
2
|
+
|
|
1
3
|
// MU/TH/UR knowledge base: known conditions, how to recognise them in the
|
|
2
4
|
// room's own failure records, and the remedy as terminal commands per OS.
|
|
3
5
|
//
|
|
4
6
|
// Pure module (no DOM) so the catalog can be unit-tested and reused by the CLI.
|
|
5
7
|
|
|
8
|
+
// The two MADRE is tested on. Every remedy below is a command written for one of these two
|
|
9
|
+
// shells; a third set that nobody has ever run would be worse than none.
|
|
6
10
|
export const PLATFORMS = {
|
|
7
11
|
darwin: { label: 'macOS', shell: 'zsh' },
|
|
8
12
|
linux: { label: 'Linux', shell: 'bash' },
|
|
9
|
-
win32: { label: 'Windows', shell: 'PowerShell' },
|
|
10
13
|
};
|
|
11
14
|
|
|
12
15
|
const envExport = (name, value) => ({
|
|
13
16
|
darwin: [`export ${name}="${value}"`, `# persist: echo 'export ${name}="${value}"' >> ~/.zshrc`],
|
|
14
|
-
linux: [`export ${name}="${value}"`, `# persist: echo 'export ${name}="${value}"' >> ~/.bashrc`]
|
|
15
|
-
win32: [`$env:${name}="${value}"`, `# persist: setx ${name} "${value}"`],
|
|
17
|
+
linux: [`export ${name}="${value}"`, `# persist: echo 'export ${name}="${value}"' >> ~/.bashrc`]
|
|
16
18
|
});
|
|
17
19
|
|
|
18
20
|
const installAgent = {
|
|
19
21
|
codex: {
|
|
20
22
|
darwin: ['npm install -g @openai/codex', '# or install the ChatGPT desktop app, which bundles codex'],
|
|
21
|
-
linux: ['npm install -g @openai/codex']
|
|
22
|
-
win32: ['npm install -g @openai/codex'],
|
|
23
|
+
linux: ['npm install -g @openai/codex']
|
|
23
24
|
},
|
|
24
25
|
claude: {
|
|
25
26
|
darwin: ['brew install --cask claude-code', '# or: npm install -g @anthropic-ai/claude-code'],
|
|
26
|
-
linux: ['npm install -g @anthropic-ai/claude-code']
|
|
27
|
-
win32: ['npm install -g @anthropic-ai/claude-code'],
|
|
27
|
+
linux: ['npm install -g @anthropic-ai/claude-code']
|
|
28
28
|
},
|
|
29
29
|
gemini: {
|
|
30
30
|
darwin: ['npm install -g @google/gemini-cli'],
|
|
31
|
-
linux: ['npm install -g @google/gemini-cli']
|
|
32
|
-
win32: ['npm install -g @google/gemini-cli'],
|
|
31
|
+
linux: ['npm install -g @google/gemini-cli']
|
|
33
32
|
},
|
|
34
33
|
opencode: {
|
|
35
34
|
darwin: ['brew install opencode', '# or: npm install -g opencode-ai'],
|
|
36
|
-
linux: ['curl -fsSL https://opencode.ai/install | bash', '# or: npm install -g opencode-ai']
|
|
37
|
-
win32: ['npm install -g opencode-ai'],
|
|
35
|
+
linux: ['curl -fsSL https://opencode.ai/install | bash', '# or: npm install -g opencode-ai']
|
|
38
36
|
},
|
|
39
37
|
};
|
|
40
38
|
|
|
41
39
|
const loginAgent = {
|
|
42
|
-
codex: { darwin: ['codex login', 'codex login status'], linux: ['codex login', 'codex login status']
|
|
43
|
-
claude: { darwin: ['claude auth login', 'claude auth status --text'], linux: ['claude auth login', 'claude auth status --text']
|
|
44
|
-
gemini: { darwin: ['gemini', '# inside gemini: /auth → "Use Gemini API key" or Google login'], linux: ['gemini', '# inside gemini: /auth']
|
|
45
|
-
opencode: { darwin: ['opencode auth login', 'opencode auth list'], linux: ['opencode auth login', 'opencode auth list']
|
|
40
|
+
codex: { darwin: ['codex login', 'codex login status'], linux: ['codex login', 'codex login status']},
|
|
41
|
+
claude: { darwin: ['claude auth login', 'claude auth status --text'], linux: ['claude auth login', 'claude auth status --text']},
|
|
42
|
+
gemini: { darwin: ['gemini', '# inside gemini: /auth → "Use Gemini API key" or Google login'], linux: ['gemini', '# inside gemini: /auth']},
|
|
43
|
+
opencode: { darwin: ['opencode auth login', 'opencode auth list'], linux: ['opencode auth login', 'opencode auth list']},
|
|
46
44
|
};
|
|
47
45
|
|
|
48
|
-
const same = (commands) => ({ darwin: commands, linux: commands
|
|
46
|
+
const same = (commands) => ({ darwin: commands, linux: commands });
|
|
49
47
|
|
|
50
48
|
export const CONDITIONS = [
|
|
51
49
|
{
|
|
@@ -58,8 +56,7 @@ export const CONDITIONS = [
|
|
|
58
56
|
remedy: 'Switch Gemini to an API key from Google AI Studio. MADRE copies only the auth selection into its isolated home, so the key can stay in the keychain or in ~/.gemini/.env.',
|
|
59
57
|
fixes: {
|
|
60
58
|
darwin: ['gemini', '# inside gemini: /auth → "Use Gemini API key" and paste the key from https://aistudio.google.com/app/apikey', '# or, without the prompt:', ...envExport('GEMINI_API_KEY', 'YOUR_KEY').darwin],
|
|
61
|
-
linux: ['gemini', '# inside gemini: /auth → "Use Gemini API key"', ...envExport('GEMINI_API_KEY', 'YOUR_KEY').linux]
|
|
62
|
-
win32: ['gemini', '# inside gemini: /auth → "Use Gemini API key"', ...envExport('GEMINI_API_KEY', 'YOUR_KEY').win32],
|
|
59
|
+
linux: ['gemini', '# inside gemini: /auth → "Use Gemini API key"', ...envExport('GEMINI_API_KEY', 'YOUR_KEY').linux]
|
|
63
60
|
},
|
|
64
61
|
},
|
|
65
62
|
{
|
|
@@ -82,8 +79,7 @@ export const CONDITIONS = [
|
|
|
82
79
|
remedy: 'Wait a minute and retry, pick an explicit model in the composer (gemini-3-flash-preview skips the router), or raise the key\'s quota in Google AI Studio.',
|
|
83
80
|
fixes: {
|
|
84
81
|
darwin: ['# in the room: click the Gemini sphere twice → choose gemini-3-flash-preview', '# check quota: https://aistudio.google.com/app/apikey', 'gemini --model gemini-3-flash-preview -p "ping"'],
|
|
85
|
-
linux: ['gemini --model gemini-3-flash-preview -p "ping"', '# quota: https://aistudio.google.com/app/apikey']
|
|
86
|
-
win32: ['gemini --model gemini-3-flash-preview -p "ping"', '# quota: https://aistudio.google.com/app/apikey'],
|
|
82
|
+
linux: ['gemini --model gemini-3-flash-preview -p "ping"', '# quota: https://aistudio.google.com/app/apikey']
|
|
87
83
|
},
|
|
88
84
|
},
|
|
89
85
|
{
|
|
@@ -106,8 +102,7 @@ export const CONDITIONS = [
|
|
|
106
102
|
remedy: 'Tell MADRE which provider/model OpenCode should use in the room, or remove the stale credential so the default changes.',
|
|
107
103
|
fixes: {
|
|
108
104
|
darwin: ['npx @jossuealcala/madre setup', '# press [m] and pick a provider/model that has a session, e.g. openai/gpt-5.6-sol', '# one-off alternative:', 'PULSE_OPENCODE_MODEL=openai/gpt-5.6-sol npx @jossuealcala/madre start', '# or drop the stale key:', 'opencode auth logout anthropic'],
|
|
109
|
-
linux: ['npx @jossuealcala/madre setup', '# press [m] and pick a provider/model that has a session', 'PULSE_OPENCODE_MODEL=openai/gpt-5.6-sol npx @jossuealcala/madre start', 'opencode auth logout anthropic']
|
|
110
|
-
win32: ['npx @jossuealcala/madre setup', '# press [m] and pick a provider/model that has a session', '$env:PULSE_OPENCODE_MODEL="openai/gpt-5.6-sol"; npx @jossuealcala/madre start', 'opencode auth logout anthropic'],
|
|
105
|
+
linux: ['npx @jossuealcala/madre setup', '# press [m] and pick a provider/model that has a session', 'PULSE_OPENCODE_MODEL=openai/gpt-5.6-sol npx @jossuealcala/madre start', 'opencode auth logout anthropic']
|
|
111
106
|
},
|
|
112
107
|
},
|
|
113
108
|
{
|
|
@@ -116,7 +111,7 @@ export const CONDITIONS = [
|
|
|
116
111
|
title: 'Agent installed but signed out',
|
|
117
112
|
match: /not logged in|sign in required|Error authenticating|unauthorized|please (?:log|sign) ?in|no credentials/i,
|
|
118
113
|
diagnosis: 'The CLI is on this computer but has no session for its provider. MADRE never stores credentials; each agent keeps its own.',
|
|
119
|
-
remedy: '
|
|
114
|
+
remedy: 'Press SIGN IN on the agent\'s card: on first contact the room opens on the bridge, and later the same button lives in ⚙ CONNECTIONS. Codex and Claude sign in with a browser flow MADRE runs for you; Gemini and OpenCode hand you their command. `madre setup` does the same from a terminal.',
|
|
120
115
|
fixes: same(['npx @jossuealcala/madre setup', '# or directly:', 'codex login', 'claude auth login', 'opencode auth login', 'gemini # then /auth']),
|
|
121
116
|
perAgent: loginAgent,
|
|
122
117
|
},
|
|
@@ -126,7 +121,7 @@ export const CONDITIONS = [
|
|
|
126
121
|
title: 'Agent not found on this computer',
|
|
127
122
|
match: /is not installed on this computer|not found on this computer|ENOENT.*(codex|claude|gemini|opencode)/i,
|
|
128
123
|
diagnosis: 'MADRE looks for `codex`, `claude`, `gemini` and `opencode` on PATH plus a few known locations. Nothing answered.',
|
|
129
|
-
remedy: '
|
|
124
|
+
remedy: 'Press INSTALL on the agent\'s card: MADRE shows the exact command, runs it here and looks again when it finishes, no restart. `madre setup` does the same from a terminal.',
|
|
130
125
|
fixes: same(['# pick the agent you want; see per-agent commands below']),
|
|
131
126
|
perAgent: installAgent,
|
|
132
127
|
},
|
|
@@ -158,8 +153,7 @@ export const CONDITIONS = [
|
|
|
158
153
|
remedy: 'Press the RAISE button on this card, or type a new number in ⚙ CONNECTIONS (DEFAULT TIMEOUT · SECONDS, or the field on the agent\'s card): fields save the moment you leave them. It applies to the next turn and persists in ~/.pulse/config.json. Environment variables work too, but only for a server started after exporting them; a running room never sees a later export.',
|
|
159
154
|
fixes: {
|
|
160
155
|
darwin: ['# live, no restart: ⚙ CONNECTIONS → DEFAULT TIMEOUT · SECONDS → SAVE', '# at launch only (env wins over config.json):', 'PULSE_AGENT_TIMEOUT_MS=300000 PULSE_CLAUDE_TIMEOUT_MS=600000 madre start', '# or ~/.pulse/config.json → {"timeouts":{"default":300000,"claude":600000}}'],
|
|
161
|
-
linux: ['# live, no restart: ⚙ CONNECTIONS → DEFAULT TIMEOUT · SECONDS → SAVE', 'PULSE_AGENT_TIMEOUT_MS=300000 PULSE_CLAUDE_TIMEOUT_MS=600000 madre start', '# or ~/.pulse/config.json → {"timeouts":{"default":300000,"claude":600000}}']
|
|
162
|
-
win32: ['# live, no restart: ⚙ CONNECTIONS → DEFAULT TIMEOUT · SECONDS → SAVE', '$env:PULSE_AGENT_TIMEOUT_MS="300000"; $env:PULSE_CLAUDE_TIMEOUT_MS="600000"; madre start', '# or %USERPROFILE%\\.pulse\\config.json → {"timeouts":{"default":300000,"claude":600000}}'],
|
|
156
|
+
linux: ['# live, no restart: ⚙ CONNECTIONS → DEFAULT TIMEOUT · SECONDS → SAVE', 'PULSE_AGENT_TIMEOUT_MS=300000 PULSE_CLAUDE_TIMEOUT_MS=600000 madre start', '# or ~/.pulse/config.json → {"timeouts":{"default":300000,"claude":600000}}']
|
|
163
157
|
},
|
|
164
158
|
},
|
|
165
159
|
{
|
|
@@ -180,8 +174,7 @@ export const CONDITIONS = [
|
|
|
180
174
|
remedy: 'Split the message, or raise the cap if your CLIs cope.',
|
|
181
175
|
fixes: {
|
|
182
176
|
darwin: ['export PULSE_MAX_MESSAGE_CHARS="40000"'],
|
|
183
|
-
linux: ['export PULSE_MAX_MESSAGE_CHARS="40000"']
|
|
184
|
-
win32: ['$env:PULSE_MAX_MESSAGE_CHARS="40000"'],
|
|
177
|
+
linux: ['export PULSE_MAX_MESSAGE_CHARS="40000"']
|
|
185
178
|
},
|
|
186
179
|
},
|
|
187
180
|
{
|
|
@@ -202,8 +195,7 @@ export const CONDITIONS = [
|
|
|
202
195
|
remedy: 'Continue with another agent, press RAISE LOCAL BUDGET on this card, or type a number in ⚙ CONNECTIONS → LOCAL TOKEN BUDGET PER AGENT (saves on leaving the field). The provider\'s real limits show in each sphere\'s popover when the CLI reports them.',
|
|
203
196
|
fixes: {
|
|
204
197
|
darwin: ['# live: ⚙ CONNECTIONS → LOCAL TOKEN BUDGET PER AGENT → SAVE', '# at launch: PULSE_SOFT_TOKEN_BUDGET=1000000 madre start', '# or ~/.pulse/config.json → {"room":{"softTokenBudget":1000000}}'],
|
|
205
|
-
linux: ['# live: ⚙ CONNECTIONS → LOCAL TOKEN BUDGET PER AGENT → SAVE', 'PULSE_SOFT_TOKEN_BUDGET=1000000 madre start']
|
|
206
|
-
win32: ['# live: ⚙ CONNECTIONS → LOCAL TOKEN BUDGET PER AGENT → SAVE', '$env:PULSE_SOFT_TOKEN_BUDGET="1000000"; madre start'],
|
|
198
|
+
linux: ['# live: ⚙ CONNECTIONS → LOCAL TOKEN BUDGET PER AGENT → SAVE', 'PULSE_SOFT_TOKEN_BUDGET=1000000 madre start']
|
|
207
199
|
},
|
|
208
200
|
},
|
|
209
201
|
{
|
|
@@ -215,8 +207,7 @@ export const CONDITIONS = [
|
|
|
215
207
|
remedy: 'Use a different port, or find and stop the process holding it.',
|
|
216
208
|
fixes: {
|
|
217
209
|
darwin: ['npx @jossuealcala/madre start --port 4318', '# who holds 4317?', 'lsof -nP -iTCP:4317 -sTCP:LISTEN'],
|
|
218
|
-
linux: ['npx @jossuealcala/madre start --port 4318', 'ss -ltnp | grep 4317']
|
|
219
|
-
win32: ['npx @jossuealcala/madre start --port 4318', 'netstat -ano | findstr :4317', '# then: taskkill /PID <pid> /F'],
|
|
210
|
+
linux: ['npx @jossuealcala/madre start --port 4318', 'ss -ltnp | grep 4317']
|
|
220
211
|
},
|
|
221
212
|
},
|
|
222
213
|
{
|
|
@@ -299,11 +290,11 @@ export const CONDITIONS = [
|
|
|
299
290
|
{
|
|
300
291
|
id: 'modes',
|
|
301
292
|
severity: 'informational',
|
|
302
|
-
title: 'Permission modes: #0 GHOST · #1 EXCHANGE · #2 CREATE · #3 CONTROL',
|
|
303
|
-
match: /mode|ghost|exchange|control|override|designation|max mode|#[0-
|
|
304
|
-
diagnosis: 'Every message goes out at a mode, chosen in the chip after TO @agent (or typed as #2 in the text). #0 GHOST is off the record: nothing is saved, no other agent remembers it, gone on reload, no delegation. #1 EXCHANGE is the default: read and coordinate. #2 CREATE lets the agent add new files and folders anywhere in the project, where they belong; whatever existed before is put back after the turn and the room says so. #3 CONTROL puts one agent in command of the project itself: a git checkpoint is taken first, every change is listed afterwards, writes into .git, .pulse or .env files are reverted on the spot, and UNDO restores the checkpoint. One holder at a time; it needs MAX MODE 3 and a git repository. Your mode is the ceiling of any plan the message starts, and #3 is never delegated. Each agent has a MAX MODE in CONNECTIONS; above it, #2 is answered read-only and #3 is refused. When a #1 plan reaches a step that wants to create something, the room pauses and asks you: GRANT ONCE, GRANT FOR PLAN or DENY, with a 3-minute clock; silence denies.',
|
|
305
|
-
remedy: 'Pick the mode in the chip, or type #0..#
|
|
306
|
-
fixes: same(['# chip: TO @codex #1 EXCHANGE ▾ → choose', '@codex #2 create the poster', '# ⚙ CONNECTIONS → agent card → MAX MODE']),
|
|
293
|
+
title: 'Permission modes: #0 GHOST · #1 EXCHANGE · #2 CREATE · #3 CONTROL · #4 AIRLOCK',
|
|
294
|
+
match: /mode|ghost|exchange|control|airlock|deploy|despleg|producci|override|designation|max mode|#[0-4]\b/i,
|
|
295
|
+
diagnosis: 'Every message goes out at a mode, chosen in the chip after TO @agent (or typed as #2 in the text). #0 GHOST is off the record: nothing is saved, no other agent remembers it, gone on reload, no delegation. #1 EXCHANGE is the default: read and coordinate. #2 CREATE lets the agent add new files and folders anywhere in the project, where they belong; whatever existed before is put back after the turn and the room says so. #3 CONTROL puts one agent in command of the project itself: a git checkpoint is taken first, every change is listed afterwards, writes into .git, .pulse or .env files are reverted on the spot, and UNDO restores the checkpoint. One holder at a time; it needs MAX MODE 3 and a git repository. Your mode is the ceiling of any plan the message starts, and #3 is never delegated. Each agent has a MAX MODE in CONNECTIONS; above it, #2 is answered read-only and #3 is refused. When a #1 plan reaches a step that wants to create something, the room pauses and asks you: GRANT ONCE, GRANT FOR PLAN or DENY, with a 3-minute clock; silence denies. #4 AIRLOCK is CONTROL plus commands: tests, builds, git push, deploys with the CLIs and sessions already on this machine. Files still come back with UNDO; what leaves the machine does not, so the override asks twice: the designation, then the word AIRLOCK. An agent that says it needs a "production mode" or "permission to run commands" is asking for #4.',
|
|
296
|
+
remedy: 'Pick the mode in the chip, or type #0..#4 in the message. Raise an agent\'s MAX MODE in ⚙ CONNECTIONS. CONTROL asks for the project designation (the folder name) in the override; AIRLOCK asks for the designation and then the word AIRLOCK.',
|
|
297
|
+
fixes: same(['# chip: TO @codex #1 EXCHANGE ▾ → choose', '@codex #2 create the poster', '@opencode #4 run the tests and deploy to preview', '# ⚙ CONNECTIONS → agent card → MAX MODE']),
|
|
307
298
|
},
|
|
308
299
|
{
|
|
309
300
|
id: 'lease-missing',
|
|
@@ -332,8 +323,7 @@ export const CONDITIONS = [
|
|
|
332
323
|
remedy: 'Update Node to the current LTS.',
|
|
333
324
|
fixes: {
|
|
334
325
|
darwin: ['node --version', 'brew install node', '# or: nvm install --lts'],
|
|
335
|
-
linux: ['node --version', 'nvm install --lts', '# or your distro package for Node ≥ 22.5']
|
|
336
|
-
win32: ['node --version', 'winget install OpenJS.NodeJS.LTS'],
|
|
326
|
+
linux: ['node --version', 'nvm install --lts', '# or your distro package for Node ≥ 22.5']
|
|
337
327
|
},
|
|
338
328
|
},
|
|
339
329
|
{
|
|
@@ -345,8 +335,7 @@ export const CONDITIONS = [
|
|
|
345
335
|
remedy: 'The index is derived from the ledger: delete memory.sqlite (and its -wal/-shm siblings) and restart; MADRE rebuilds it. Distilled notes live in the same file, so export them from NOSTROMO first if they matter.',
|
|
346
336
|
fixes: {
|
|
347
337
|
darwin: ['node --version # must be ≥ 22.5', 'ls ~/.pulse/rooms/*/memory.sqlite*', 'rm ~/.pulse/rooms/<room>/memory.sqlite* # rebuilt on next start'],
|
|
348
|
-
linux: ['node --version # must be ≥ 22.5', 'ls ~/.pulse/rooms/*/memory.sqlite*', 'rm ~/.pulse/rooms/<room>/memory.sqlite* # rebuilt on next start']
|
|
349
|
-
win32: ['node --version # must be ≥ 22.5', 'dir $env:USERPROFILE\\.pulse\\rooms', 'Remove-Item $env:USERPROFILE\\.pulse\\rooms\\<room>\\memory.sqlite*'],
|
|
338
|
+
linux: ['node --version # must be ≥ 22.5', 'ls ~/.pulse/rooms/*/memory.sqlite*', 'rm ~/.pulse/rooms/<room>/memory.sqlite* # rebuilt on next start']
|
|
350
339
|
},
|
|
351
340
|
},
|
|
352
341
|
{
|
|
@@ -376,8 +365,7 @@ export const CONDITIONS = [
|
|
|
376
365
|
remedy: 'Sign the Gemini CLI in with an API key (/auth) or export GEMINI_API_KEY, then restart. If you do not want embeddings at all, set PULSE_EMBED=0 and the pause message stops.',
|
|
377
366
|
fixes: {
|
|
378
367
|
darwin: ['gemini # /auth → "Use Gemini API key"', ...envExport('GEMINI_API_KEY', 'YOUR_KEY').darwin, 'PULSE_EMBED=0 madre start # lexical only, no messages'],
|
|
379
|
-
linux: ['gemini # /auth → "Use Gemini API key"', ...envExport('GEMINI_API_KEY', 'YOUR_KEY').linux, 'PULSE_EMBED=0 madre start']
|
|
380
|
-
win32: ['gemini # /auth → "Use Gemini API key"', ...envExport('GEMINI_API_KEY', 'YOUR_KEY').win32, '$env:PULSE_EMBED="0"; madre start'],
|
|
368
|
+
linux: ['gemini # /auth → "Use Gemini API key"', ...envExport('GEMINI_API_KEY', 'YOUR_KEY').linux, 'PULSE_EMBED=0 madre start']
|
|
381
369
|
},
|
|
382
370
|
},
|
|
383
371
|
{
|
|
@@ -389,8 +377,7 @@ export const CONDITIONS = [
|
|
|
389
377
|
remedy: 'Update the CLI, then check that it sees the server. Codex can list servers from a config override; Claude accepts an inline --mcp-config. If a CLI keeps failing, PULSE_MEMORY_TOOLS=0 removes the tools for everyone and the room continues on automatic recall.',
|
|
390
378
|
fixes: {
|
|
391
379
|
darwin: ['codex mcp list', 'claude --version', 'gemini --version', 'PULSE_MEMORY_TOOLS=0 madre start # tools off, recall stays'],
|
|
392
|
-
linux: ['codex mcp list', 'claude --version', 'gemini --version', 'PULSE_MEMORY_TOOLS=0 madre start']
|
|
393
|
-
win32: ['codex mcp list', 'claude --version', 'gemini --version', '$env:PULSE_MEMORY_TOOLS="0"; madre start'],
|
|
380
|
+
linux: ['codex mcp list', 'claude --version', 'gemini --version', 'PULSE_MEMORY_TOOLS=0 madre start']
|
|
394
381
|
},
|
|
395
382
|
},
|
|
396
383
|
{
|
|
@@ -429,8 +416,7 @@ export const CONDITIONS = [
|
|
|
429
416
|
remedy: 'Install Ollama (ollama.com), start it, then in MODULES press RECHECK and PULL the models it suggests. Set PULSE_OLLAMA_MODEL or PULSE_OLLAMA_EMBED_MODEL to prefer others; PULSE_EMBED_PROVIDER=gemini keeps embeddings on Gemini even with Ollama running.',
|
|
430
417
|
fixes: {
|
|
431
418
|
darwin: ['brew install ollama', 'ollama serve', 'ollama pull nomic-embed-text', 'ollama pull qwen2.5:3b'],
|
|
432
|
-
linux: ['curl -fsSL https://ollama.com/install.sh | sh', 'ollama serve', 'ollama pull nomic-embed-text', 'ollama pull qwen2.5:3b']
|
|
433
|
-
win32: ['winget install Ollama.Ollama', 'ollama serve', 'ollama pull nomic-embed-text', 'ollama pull qwen2.5:3b'],
|
|
419
|
+
linux: ['curl -fsSL https://ollama.com/install.sh | sh', 'ollama serve', 'ollama pull nomic-embed-text', 'ollama pull qwen2.5:3b']
|
|
434
420
|
},
|
|
435
421
|
},
|
|
436
422
|
{
|
|
@@ -442,6 +428,18 @@ export const CONDITIONS = [
|
|
|
442
428
|
remedy: 'Start Ollama and PULL a chat model in MODULES → OLLAMA; the switch @MADRE IN THE ROOM turns the agent off if you do not want it. Ask it "what did we decide about …" or "did we ever discuss …".',
|
|
443
429
|
fixes: same(['ollama serve', 'ollama pull qwen2.5:3b', '@madre what did we decide about the webhook?']),
|
|
444
430
|
},
|
|
431
|
+
{
|
|
432
|
+
id: 'install-needs-admin',
|
|
433
|
+
severity: 'informational',
|
|
434
|
+
title: 'Installing a CLI: npm cannot write to the system folder',
|
|
435
|
+
match: /EACCES|EPERM|permission denied|Missing write access|npm ERR!.*sudo|administrator/i,
|
|
436
|
+
diagnosis: 'A global npm install writes into a folder that belongs to the system. With Node installed from its own installer that folder needs an administrator, so npm stops with a permission error. MADRE does not ask for your password: when it sees that wall it installs the CLI into a folder of its own, ~/.pulse/tools, and looks there as well as along PATH. The agent works the same; only the file lives somewhere else.',
|
|
437
|
+
remedy: 'Nothing to do: press INSTALL again and MADRE takes the second way by itself. If you would rather have the CLI everywhere in your terminal, install it yourself with your package manager, or give npm a folder of your own.',
|
|
438
|
+
fixes: {
|
|
439
|
+
darwin: ['# MADRE does this for you; these are the alternatives:', 'brew install --cask claude-code', 'npm config set prefix ~/.npm-global # then add ~/.npm-global/bin to your PATH'],
|
|
440
|
+
linux: ['# MADRE does this for you; these are the alternatives:', 'npm config set prefix ~/.npm-global # then add ~/.npm-global/bin to your PATH']
|
|
441
|
+
},
|
|
442
|
+
},
|
|
445
443
|
{
|
|
446
444
|
id: 'privacy-leak',
|
|
447
445
|
severity: 'warning',
|
|
@@ -460,12 +458,124 @@ export const CONDITIONS = [
|
|
|
460
458
|
remedy: 'Read the list under the reply before moving on. If the change is wrong press UNDO; if the agent should not have had it, lower its MAX MODE in ⚙ CONNECTIONS.',
|
|
461
459
|
fixes: same(['git log --oneline -5 # checkpoints are ordinary commits on a side ref', 'git stash list', '# room → UNDO under the CONTROL notice']),
|
|
462
460
|
},
|
|
461
|
+
{
|
|
462
|
+
id: 'conversations',
|
|
463
|
+
severity: 'informational',
|
|
464
|
+
title: 'Conversations: many per project, one memory, one at a time',
|
|
465
|
+
match: /conversation|already open for this project|ROOM_IN_USE|a turn is running in this conversation|new conversation|chats\.json/i,
|
|
466
|
+
diagnosis: 'A project has one memory and many conversations. The archive, the crew, the modules and the privacy list belong to the project and do not start over when you open another thread; a conversation is only the record of one line of work, and everything said in any of them feeds the same archive. The first conversation is the ledger that was always there, so a room that existed before opens exactly as it did. The numbering is the project\'s, not the thread\'s: a new conversation starts where the project got to, which is why a citation like #1411 means the same exchange in all of them. One conversation drives the crew at a time — two threads editing the same working tree is a way to lose work — so opening another while a turn runs is refused until it finishes. And one server per project: a second `madre start` on the same project is refused with the address of the room that is already open, because two servers would hand out the same sequence twice and the archive would quietly keep one of them.',
|
|
467
|
+
remedy: 'The panel on the left of the canvas lists them: open one, or start another at the bottom. Deleting one takes its transcript and nothing else — what the archivist distilled from it is the project\'s memory and stays. If MADRE says the project is already open, use that room and open a conversation inside it instead of starting a second server.',
|
|
468
|
+
fixes: same(['# room → the handle under the bar, on the left → NEW CONVERSATION', '# the address of the open room is in the refusal itself', 'ls ~/.pulse/rooms/*/chats.json # their names and which one is open']),
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
id: 'memory-aberrations',
|
|
472
|
+
severity: 'informational',
|
|
473
|
+
title: 'Aberrations: what the room established is false',
|
|
474
|
+
match: /aberration|aberraci|eyecat|contradict|refut|false claim|hallucinat|alucina/i,
|
|
475
|
+
diagnosis: 'An aberration is a claim the room established is false, kept on purpose rather than deleted: it is what the local model learns not to repeat. It never travels into a turn, and the note it refutes is taken out of circulation with it — a refuted memory is never recalled again, by words or by meaning. EYECAT watches for two agents saying opposite things about the same subject and raises the pair for a third agent to judge, one that took no part in either side. Confirming files the aberration and quarantines what it refutes; dismissing says the room was right and the pair is never raised again. Forgetting an aberration gives its note back.',
|
|
476
|
+
remedy: 'NOSTROMO shows them as collapsed bodies: open one to read what it took down, or clear it if the room was wrong about being wrong. The three tests report open contradictions under CONSISTENCY.',
|
|
477
|
+
fixes: same(['# room → ◉ NOSTROMO → the dark bodies are aberrations', '# ⚙ CONNECTIONS → MEMORY → THE THREE TESTS → CONSISTENCY']),
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
id: 'memory-cascade',
|
|
481
|
+
severity: 'informational',
|
|
482
|
+
title: 'Memory used · N by association',
|
|
483
|
+
match: /by association|cascade|spreading activation|keeps arriving with|memory used/i,
|
|
484
|
+
diagnosis: 'Under a reply, MADRE says what the archive handed that turn. Most of it was matched to what you asked, by words and by meaning. A note marked "by association" came along for a different reason: this room keeps carrying it in the same turn as one of the others, so the archive brings it too, however differently it reads. The strength is a ratio over the turns where each was found on its own merits — a memory the room reaches for constantly does not end up attached to everything, and a pair that stops meeting fades on its own. Association adds and never displaces: recall keeps two slots for it and hands back the ones it does not use.',
|
|
485
|
+
remedy: 'Click a note to see it in NOSTROMO, where the wires it travels on are drawn. It is the one part of recall that owes nothing to how a memory reads, so it can be switched off in MEMORY.',
|
|
486
|
+
fixes: same(['# ⚙ CONNECTIONS → MEMORY → CARRY WHAT A MEMORY KEEPS ARRIVING WITH', 'PULSE_RECALL_CASCADE=0 npx @jossuealcala/madre start']),
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
id: 'memory-cold',
|
|
490
|
+
severity: 'informational',
|
|
491
|
+
title: 'Cold memories: what the archive has had its chances with',
|
|
492
|
+
match: /cold memor|cold zone|never once carried|had its chances/i,
|
|
493
|
+
diagnosis: 'A memory is cold when three things are true at once: no turn has ever carried it, it shares a subject with nothing so nothing can reach it sideways either, and the archive has been opened at least a dozen times since it was written. The third is what makes the label worth acting on — never-recalled is what every memory is on the day it is written. Chances are counted in turns that actually reached into the archive, and only from the day the room started keeping that trail, so what was never written down is not held against the note.',
|
|
494
|
+
remedy: 'NOSTROMO shows COLD · N in its header when there are any; pressing it rings each one on the map. Open one and its card says the number plainly, with FORGET one press away. Asking about it is the other way out: the room writes that question for you under ASK.',
|
|
495
|
+
fixes: same(['# room → ◉ NOSTROMO → COLD', '# room → ◉ NOSTROMO → ASK → PUT IN THE COMPOSER']),
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
id: 'maturity-tests',
|
|
499
|
+
severity: 'informational',
|
|
500
|
+
title: 'The three tests: whether the archive works',
|
|
501
|
+
match: /three tests|coverage|consistency|held-out|match test|maturity|madurez|ready to be worked in/i,
|
|
502
|
+
diagnosis: 'The six readings count what the archive is made of; they cannot say whether it works, because nothing about a pile of notes says whether the right one comes back when it is needed. COVERAGE takes real questions from this room, runs recall at the exact point each was asked, and scores what came back against the reply that was actually given. CONSISTENCY reads the contradictions EYECAT still holds and whether aberrations are being filed more often lately. MATCH puts real questions a frontier CLI answered back to the local model with this archive behind it. All three measure against a control, because everything in one room is about the same handful of subjects and any two pieces of it read as close to an embedder: to count, what the archive handed over has to beat what it would have handed over for a different question.',
|
|
503
|
+
remedy: 'Read the one sentence and the one instruction at the top of MEMORY; the readings and the tests are a fold below it. The free test is read as the panel opens, the cheap one keeps itself fresh once a day while the embeddings are local, and the slow one is yours to start and to stop. None of them spends a provider turn.',
|
|
504
|
+
fixes: same(['# ⚙ CONNECTIONS → MEMORY → THE THREE TESTS', '# MATCH needs Ollama running with a chat model, and embeddings on']),
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
id: 'module-update',
|
|
508
|
+
severity: 'common',
|
|
509
|
+
title: 'Updating what a module drives',
|
|
510
|
+
match: /EACCES|permission denied.*npm|npm ERR!.*EACCES|update to \d|newest is|brew upgrade|install -g @playwright/i,
|
|
511
|
+
diagnosis: 'When something newer exists and the module knows how to fetch it, the button is beside the version on its card. It asks twice on purpose: the first press answers with the command, the second runs exactly that, and the output lands in the room line by line. Nothing installs by itself — putting software on your computer unannounced would break the one thing MADRE promises, that a command is seen before it runs. The usual failure is npm refusing a global install with EACCES, which means the global prefix belongs to root; sudo works and leaves you with root-owned files, so a user-owned prefix or a version manager is the better answer.',
|
|
512
|
+
remedy: 'Press the button beside the version, read the command, run it. If npm refuses with EACCES, point npm at a prefix you own or use a version manager, then press it again.',
|
|
513
|
+
fixes: {
|
|
514
|
+
darwin: ['npm config get prefix', 'npm config set prefix ~/.npm-global', 'echo \'export PATH="$HOME/.npm-global/bin:$PATH"\' >> ~/.zshrc', '# or: brew install node # a Homebrew node owns its own prefix'],
|
|
515
|
+
linux: ['npm config get prefix', 'npm config set prefix ~/.npm-global', 'echo \'export PATH="$HOME/.npm-global/bin:$PATH"\' >> ~/.bashrc', '# or use nvm, which owns the prefix it installs into'],
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
id: 'module-add',
|
|
520
|
+
severity: 'common',
|
|
521
|
+
title: 'Installing a module somebody else wrote',
|
|
522
|
+
match: /already taken|must live under \/api\/x\/|not a module|default export must be a module|module file|\.module\.mjs|add a module/i,
|
|
523
|
+
diagnosis: 'MODULES has + ADD A MODULE for a .mjs somebody wrote. It comes through the same door as everything else: written to a scratch copy, imported there, checked against the house rules, and installed only if it passes. Three things get a file refused, each with the reason: an id that belongs to a module MADRE ships with, a route outside its own corner of the API (/api/x/<id>/), and a file that will not load at all. Said plainly, because it is what it is: a module runs inside MADRE, with your permissions, on this computer. MADRE checks that it loads and stays in its corral; what the code intends is the one thing nobody can check for you.',
|
|
524
|
+
remedy: 'Install it only if you trust where it came from. A module you wrote can also be updated from where you publish it: declare updates: { url } in the file, or install it from a file and MADRE remembers which, and its card carries GET A NEWER FILE.',
|
|
525
|
+
fixes: same(['# MODULES → + ADD A MODULE', 'ls ~/.pulse/modules # every module you installed, and where it came from', '# docs/SDK.md is the whole contract, and docs/sdk/hello-module.mjs is one that works']),
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
id: 'local-ready',
|
|
529
|
+
severity: 'common',
|
|
530
|
+
title: 'Is the local model ready to be worked in?',
|
|
531
|
+
match: /is .*(local|madre).*(ready|good|any good)|ready to be worked in|measured against this (room|project)|check it against this room|local\.present|not measured/i,
|
|
532
|
+
diagnosis: 'Running is not the same as being of use. @madre being in the room means Ollama has a chat model; whether that model is worth your work on THIS project is a different question, and the room measures it: twelve real exchanges from this room are put back to the local model, and each answer is scored against what the crew actually answered that day — against a control, so that talking about the same project does not count. It takes minutes and spends nothing, because all of it runs on your machine.',
|
|
533
|
+
remedy: 'The room says which of the two it has, once per model, in the line that begins "local · @madre". If nobody has measured it, that line carries CHECK IT AGAINST THIS ROOM; the answer lands in the room when it finishes, with the one thing to do about it. A model that does not pass is not broken — the archive is still thin, and it fills where the work happens. The @madre sphere keeps the reading and its date after the line has scrolled away, and the same test lives in NOSTROMO → THE THREE TESTS.',
|
|
534
|
+
fixes: same([
|
|
535
|
+
'# the line in the room: local · @madre → CHECK IT AGAINST THIS ROOM',
|
|
536
|
+
'# or: NOSTROMO → THE THREE TESTS → the third one',
|
|
537
|
+
'# it needs embeddings on and some turns answered by a CLI agent, which is what it compares against',
|
|
538
|
+
]),
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
id: 'code000',
|
|
542
|
+
severity: 'blocking',
|
|
543
|
+
title: 'CODE000: the archive is sealed',
|
|
544
|
+
match: /CODE000|THE ARCHIVE IS SEALED|UNABLE TO COMPUTE\. UNABLE TO CLARIFY|designation|intruder/i,
|
|
545
|
+
diagnosis: 'Two doors ask for the project designation before doing something that cannot be undone: boarding NOSTROMO, and purging every private term already recorded. Getting that name wrong is never part of doing the work — you either know the project you are standing in or you are trying names — so MU/TH/UR counts it. Eight wrong names inside five minutes and CODE000 comes down: the archive is sealed for ten minutes, the crew is told in code, and NOSTROMO asks for the designation again.',
|
|
546
|
+
remedy: 'The designation is the name of the project folder this room was opened in, lowercase or not. Nothing was lost: the seal is a refusal to change the archive for ten minutes, not a deletion, and it lifts on its own. Reading the core is not one of those doors and never counts — it is the way in.',
|
|
547
|
+
fixes: same(['basename "$PWD" # the designation this room asks for', '# the seal lifts on its own; MU/TH/UR › NOSTROMO asks for the name again after it does']),
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
id: 'core-console',
|
|
551
|
+
severity: 'common',
|
|
552
|
+
title: 'The core closed while I was typing in it',
|
|
553
|
+
match: /unable to compute|interface closed|core clos|three strikes|inquiry|order 937|mu\/th\/ur 6000/i,
|
|
554
|
+
diagnosis: 'Inside the core there is a console: MU/TH/UR answers about this room from what the page already has — the blocks of the next briefing and their words, the command that would run it, what has left this machine, how many terms privacy is protecting, where the archive stands. Nothing is asked of the crew and nothing is sent. She answers or she says she cannot, and three inquiries she cannot parse close the frame. The count is on screen from the first one and every refusal names what she would have taken.',
|
|
555
|
+
remedy: 'Nothing happened to the archive: reading what the room says in your name is not an attempt on it. Open the core again — click the star at the centre of NOSTROMO — and the count starts over. HELP lists every inquiry; READ <BLOCK> prints one block of the briefing word for word.',
|
|
556
|
+
fixes: same(['# NOSTROMO → click the core → type HELP', '# READ MEMORIES prints the block the next turn would carry', '# SPECIAL ORDER 937 is answered too']),
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
id: 'outbound',
|
|
560
|
+
severity: 'common',
|
|
561
|
+
title: 'What leaves this computer, and how to stop each thing that does',
|
|
562
|
+
match: /what left this machine|outbound|leaves? (this )?(computer|machine)|sends? (my )?(code|data)|telemetry|phone home|privacy/i,
|
|
563
|
+
diagnosis: 'MADRE runs here and keeps what it knows in a file you own, and the list that qualifies that sentence is inside the core, under WHAT LEFT THIS MACHINE. Seven addresses, each saying what it carries and where it is switched off: the briefing, which goes to whoever runs the agent you sent the turn to; Gemini embeddings, which carry the text of your memories; the image model, which carries the prompt an agent wrote; the npm registry and GitHub, which get a package name and nothing else; the error collector, which gets a redacted condition and only with your press; and Anthropic, for how much of your Claude plan is left. Ollama is on the list to be seen staying here.',
|
|
564
|
+
remedy: 'Under the list is the log itself: every request this process made, MADRE\'s own and any a module made, because a module runs inside MADRE and cannot opt out of the wrapper. No body, no header and no query value is ever written there — only which parameters were set. An address nothing declares is reported as exactly that.',
|
|
565
|
+
fixes: same([
|
|
566
|
+
'# NOSTROMO → click the core → WHAT LEFT THIS MACHINE',
|
|
567
|
+
'cat ~/.pulse/rooms/*/outbound.jsonl | tail -20 # the same log, on disk',
|
|
568
|
+
'# embeddings, to keep memories on this computer: MU/TH/UR → MEMORY → EMBEDDINGS → OLLAMA',
|
|
569
|
+
'PULSE_UPDATE_CHECK=0 madre start # no version checks at all',
|
|
570
|
+
]),
|
|
571
|
+
},
|
|
463
572
|
];
|
|
464
573
|
|
|
574
|
+
// Which of the two sets of commands to show first. Anything else gets macOS, and the panel says
|
|
575
|
+
// which shells these are written for rather than guessing on the reader's behalf.
|
|
465
576
|
export function detectPlatform(nav = globalThis.navigator) {
|
|
466
577
|
const hint = `${nav?.userAgentData?.platform ?? ''} ${nav?.platform ?? ''} ${nav?.userAgent ?? ''}`.toLowerCase();
|
|
467
578
|
if (/mac|iphone|ipad|darwin/.test(hint)) return 'darwin';
|
|
468
|
-
if (/win/.test(hint)) return 'win32';
|
|
469
579
|
if (/linux|android|x11/.test(hint)) return 'linux';
|
|
470
580
|
return 'darwin';
|
|
471
581
|
}
|
|
@@ -474,13 +584,25 @@ export function detectPlatform(nav = globalThis.navigator) {
|
|
|
474
584
|
// only match their own agent; generic ones match anyone.
|
|
475
585
|
export function diagnose(errorText, agent = null) {
|
|
476
586
|
const text = String(errorText ?? '');
|
|
477
|
-
return CONDITIONS.filter((condition) => (!condition.agent || !agent || condition.agent === agent) && condition.match.test(text));
|
|
587
|
+
return CONDITIONS.filter((condition) => (!condition.agent || !agent || condition.agent === agent) && condition.match.test(text)).map(said);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// A condition reads in the room's language; what it MATCHES does not. The `match` patterns are
|
|
591
|
+
// tested against what a CLI printed, and a CLI prints English whatever the room speaks — so the
|
|
592
|
+
// patterns stay exactly as they are, and only the words a person reads go through the catalogue.
|
|
593
|
+
// Said at call time, because the table is built when this file is imported and the room learns
|
|
594
|
+
// its language after.
|
|
595
|
+
export function said(condition) {
|
|
596
|
+
return { ...condition, title: t(condition.title), diagnosis: t(condition.diagnosis), remedy: t(condition.remedy) };
|
|
478
597
|
}
|
|
598
|
+
export const allConditions = () => CONDITIONS.map(said);
|
|
479
599
|
|
|
480
600
|
export function searchConditions(query) {
|
|
481
601
|
const needle = String(query ?? '').trim().toLowerCase();
|
|
482
|
-
|
|
483
|
-
|
|
602
|
+
const all = allConditions();
|
|
603
|
+
if (!needle) return all;
|
|
604
|
+
// Searched in the language it is read in, plus the id and the agent, which are never translated.
|
|
605
|
+
return all.filter((condition) => [condition.id, condition.title, condition.diagnosis, condition.remedy, condition.agent ?? '']
|
|
484
606
|
.join(' ').toLowerCase().includes(needle));
|
|
485
607
|
}
|
|
486
608
|
|
package/src/adapters/claude.mjs
CHANGED
|
@@ -6,22 +6,27 @@ import { runReadonlyProcess } from './process.mjs';
|
|
|
6
6
|
export function claudeTools({ lease = null, scopes = null } = {}) {
|
|
7
7
|
const tools = ['Read', 'Glob', 'Grep'];
|
|
8
8
|
if (lease) tools.push('Write', 'Edit');
|
|
9
|
+
if (lease?.airlock) tools.push('Bash'); // AIRLOCK (#4): commands, git, deploy CLIs
|
|
9
10
|
if (scopes?.web) tools.push('WebFetch', 'WebSearch');
|
|
10
11
|
return tools;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
export function mcpServersFor({ imageStudio = null, memoryServer = null } = {}) {
|
|
14
|
+
export function mcpServersFor({ imageStudio = null, memoryServer = null, mcpServers = [] } = {}) {
|
|
14
15
|
const servers = {};
|
|
15
16
|
if (memoryServer) servers[memoryServer.name] = { command: memoryServer.command, args: memoryServer.args, env: memoryServer.env };
|
|
16
17
|
if (imageStudio) servers[imageStudio.name] = { command: imageStudio.command, args: imageStudio.args, env: imageStudio.env };
|
|
18
|
+
for (const server of mcpServers) servers[server.name] = { command: server.command, args: server.args ?? [], env: server.env ?? {} };
|
|
17
19
|
return servers;
|
|
18
20
|
}
|
|
21
|
+
// Claude's permission names for a module's server: each tool, or the whole server when the list is open.
|
|
22
|
+
export const claudeMcpTools = (server) => (server.tools?.length ? server.tools.map((tool) => `mcp__${server.name}__${tool}`) : [`mcp__${server.name}`]);
|
|
19
23
|
|
|
20
|
-
export function buildClaudeArgs({ prompt, model = null, attachmentsDir = null, lease = null, scopes = null, imageStudio = null, memoryServer = null }) {
|
|
24
|
+
export function buildClaudeArgs({ prompt, model = null, attachmentsDir = null, lease = null, scopes = null, imageStudio = null, memoryServer = null, mcpServers = [] }) {
|
|
21
25
|
const tools = claudeTools({ lease, scopes });
|
|
22
26
|
const mcpTools = [
|
|
23
27
|
...(memoryServer ? memoryServer.tools.map((tool) => `mcp__${memoryServer.name}__${tool}`) : []),
|
|
24
28
|
...(imageStudio ? [`mcp__${imageStudio.name}__${imageStudio.tool}`] : []),
|
|
29
|
+
...mcpServers.flatMap(claudeMcpTools),
|
|
25
30
|
];
|
|
26
31
|
const allowed = [
|
|
27
32
|
'Read', 'Glob', 'Grep',
|
|
@@ -30,13 +35,14 @@ export function buildClaudeArgs({ prompt, model = null, attachmentsDir = null, l
|
|
|
30
35
|
// CLI: a lone Write rule is denied under dontAsk), so both come together; in CREATE the room
|
|
31
36
|
// restores existing files after the turn.
|
|
32
37
|
...(lease ? [`Write(//${lease.outDir}/**)`, `Edit(//${lease.outDir}/**)`] : []),
|
|
38
|
+
...(lease?.airlock ? ['Bash'] : []),
|
|
33
39
|
...(scopes?.web ? ['WebFetch', 'WebSearch'] : []),
|
|
34
40
|
...mcpTools,
|
|
35
41
|
];
|
|
36
42
|
// Only MADRE's own MCP servers ever reach Claude here; --strict-mcp-config
|
|
37
43
|
// keeps the user's servers out of the isolated run.
|
|
38
|
-
const anyMcp = Boolean(imageStudio || memoryServer);
|
|
39
|
-
const mcpConfig = JSON.stringify({ mcpServers: mcpServersFor({ imageStudio, memoryServer }) });
|
|
44
|
+
const anyMcp = Boolean(imageStudio || memoryServer || mcpServers.length);
|
|
45
|
+
const mcpConfig = JSON.stringify({ mcpServers: mcpServersFor({ imageStudio, memoryServer, mcpServers }) });
|
|
40
46
|
return [
|
|
41
47
|
'-p',
|
|
42
48
|
...(model ? ['--model', model] : []),
|
|
@@ -95,10 +101,11 @@ export function parseClaudeOutput(output) {
|
|
|
95
101
|
}
|
|
96
102
|
}
|
|
97
103
|
|
|
98
|
-
export function invokeClaude({ executable, projectRoot, prompt, timeoutMs = 120000, signal, model = null, attachments = [], lease = null, scopes = null, imageStudio = null, memoryServer = null }) {
|
|
104
|
+
export function invokeClaude({ executable, projectRoot, prompt, timeoutMs = 120000, signal, model = null, attachments = [], lease = null, scopes = null, imageStudio = null, memoryServer = null, mcpServers = [], onProgress = null }) {
|
|
99
105
|
return runReadonlyProcess({
|
|
106
|
+
onProgress: onProgress ? (out) => onProgress({ chars: (() => { try { return String(JSON.parse(out)?.result ?? '').length; } catch { return 0; } })() }) : null,
|
|
100
107
|
executable,
|
|
101
|
-
args: buildClaudeArgs({ prompt, model, attachmentsDir: attachments[0]?.dir ?? null, lease, scopes, imageStudio, memoryServer }),
|
|
108
|
+
args: buildClaudeArgs({ prompt, model, attachmentsDir: attachments[0]?.dir ?? null, lease, scopes, imageStudio, memoryServer, mcpServers }),
|
|
102
109
|
cwd: projectRoot,
|
|
103
110
|
env: process.env,
|
|
104
111
|
timeoutMs,
|
package/src/adapters/codex.mjs
CHANGED
|
@@ -12,25 +12,28 @@ export function tomlValue(value) {
|
|
|
12
12
|
// The room's memory as an MCP server for this run only: Codex reads
|
|
13
13
|
// mcp_servers.<name> from its config, and -c overrides it on the command line
|
|
14
14
|
// without touching the user's config.toml.
|
|
15
|
-
export function codexMcpOverrides(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
export function codexMcpOverrides(servers) {
|
|
16
|
+
const list = (Array.isArray(servers) ? servers : [servers]).filter(Boolean);
|
|
17
|
+
return list.flatMap((server) => {
|
|
18
|
+
const key = `mcp_servers.${server.name}`;
|
|
19
|
+
return [
|
|
20
|
+
'-c', `${key}.command=${tomlValue(server.command)}`,
|
|
21
|
+
'-c', `${key}.args=${tomlValue(server.args ?? [])}`,
|
|
22
|
+
'-c', `${key}.env=${tomlValue(server.env ?? {})}`,
|
|
23
|
+
];
|
|
24
|
+
});
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
export function buildCodexArgs({ projectRoot, prompt, model = null, attachments = [], lease = null, scopes = null, memoryServer = null }) {
|
|
27
|
+
export function buildCodexArgs({ projectRoot, prompt, model = null, attachments = [], lease = null, scopes = null, memoryServer = null, mcpServers = [] }) {
|
|
26
28
|
const images = attachments.filter((file) => /^image\//.test(file.contentType ?? ''));
|
|
27
29
|
return [
|
|
28
|
-
|
|
30
|
+
// AIRLOCK (#4): commands with network, so pushes and deploys can leave; otherwise the sandbox.
|
|
31
|
+
'--sandbox', lease?.airlock ? 'danger-full-access' : lease ? 'workspace-write' : 'read-only',
|
|
29
32
|
'--ask-for-approval', 'never',
|
|
30
33
|
// Live web search is a global Codex flag; the human's web scope decides.
|
|
31
34
|
...(scopes?.web ? ['--search'] : []),
|
|
32
35
|
'-C', lease ? lease.outDir : projectRoot,
|
|
33
|
-
...codexMcpOverrides(memoryServer),
|
|
36
|
+
...codexMcpOverrides([memoryServer, ...mcpServers]),
|
|
34
37
|
// Image generation is a Codex feature; the human's scope decides per turn.
|
|
35
38
|
...(lease && (scopes?.imageGen === false || lease.scopes?.imageGen === false) ? ['-c', 'features.image_generation=false'] : []),
|
|
36
39
|
'exec',
|
|
@@ -70,10 +73,11 @@ export function parseCodexOutput(output) {
|
|
|
70
73
|
return { text: text.trim(), usage };
|
|
71
74
|
}
|
|
72
75
|
|
|
73
|
-
export function invokeCodex({ executable, projectRoot, prompt, timeoutMs = 120000, signal, model = null, attachments = [], lease = null, scopes = null, memoryServer = null }) {
|
|
76
|
+
export function invokeCodex({ executable, projectRoot, prompt, timeoutMs = 120000, signal, model = null, attachments = [], lease = null, scopes = null, memoryServer = null, mcpServers = [], onProgress = null }) {
|
|
74
77
|
return runReadonlyProcess({
|
|
78
|
+
onProgress: onProgress ? (out) => onProgress({ chars: parseCodexOutput(out).text.length }) : null,
|
|
75
79
|
executable,
|
|
76
|
-
args: buildCodexArgs({ projectRoot, prompt, model, attachments, lease, scopes, memoryServer }),
|
|
80
|
+
args: buildCodexArgs({ projectRoot, prompt, model, attachments, lease, scopes, memoryServer, mcpServers }),
|
|
77
81
|
cwd: lease ? lease.outDir : projectRoot,
|
|
78
82
|
env: process.env,
|
|
79
83
|
timeoutMs,
|