@jossuealcala/madre 1.0.1 → 1.0.3
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/ADDITIONAL-TERMS.md +93 -0
- package/CHANGELOG.en.md +2013 -0
- package/CHANGELOG.md +102 -1
- package/CONTRIBUTING.md +1 -1
- package/LICENSE +661 -202
- package/NOTICE +26 -14
- package/README.en.md +241 -0
- package/README.md +21 -12
- package/TRADEMARKS.en.md +50 -0
- package/TRADEMARKS.md +33 -16
- package/bin/madre.cjs +40 -0
- package/bin/madre.mjs +7 -0
- package/docs/INTERNALS.md +6 -0
- package/docs/REFERENCE.md +40 -5
- package/docs/SDK.md +4 -2
- package/docs/en/INTERNALS.md +103 -0
- package/docs/en/REFERENCE.md +365 -0
- package/docs/en/SDK.md +308 -0
- package/docs/en/training/README.md +59 -0
- package/docs/training/README.md +48 -9
- package/docs/training/train.sh +7 -2
- package/package.json +13 -5
- package/public/abduction.js +2 -0
- package/public/app.js +3154 -539
- package/public/attribution.js +114 -0
- package/public/brands.js +23 -6
- package/public/code-tools.js +143 -0
- package/public/editor.js +22 -4
- package/public/es.js +1004 -416
- package/public/hardware.js +50 -0
- package/public/i18n.js +25 -1
- package/public/index.html +18 -11
- package/public/inquiry.js +2 -0
- package/public/loader.js +2 -0
- package/public/macroverso.js +189 -96
- package/public/market-art.js +161 -0
- package/public/market-view.js +529 -0
- package/public/market.js +189 -0
- package/public/neural.js +110 -0
- package/public/nostromo-engine.js +414 -47
- package/public/power.js +100 -0
- package/public/resay.js +11 -0
- package/public/review-ask.js +2 -0
- package/public/routing.js +23 -1
- package/public/salas-mac.css +119 -18
- package/public/salas-mac.html +14 -2
- package/public/salas-mac.js +358 -56
- package/public/salas.css +2 -0
- package/public/salas.html +1 -0
- package/public/salas.js +4 -2
- package/public/styles.css +1793 -77
- package/public/syntax.js +2 -0
- package/public/touch.js +2 -0
- package/public/tour-chapters.js +2 -0
- package/public/tour.js +84 -5
- package/public/troubleshooting.js +24 -17
- package/public/wallpaper.js +275 -0
- package/src/adapters/claude.mjs +2 -0
- package/src/adapters/codex.mjs +2 -0
- package/src/adapters/cursor.mjs +96 -0
- package/src/adapters/gemini.mjs +13 -4
- package/src/adapters/madre.mjs +5 -2
- package/src/adapters/opencode.mjs +7 -3
- package/src/adapters/process.mjs +2 -0
- package/src/asking.mjs +2 -0
- package/src/auth-probe.mjs +145 -12
- package/src/bridge/devices.mjs +2 -0
- package/src/bridge/handoff.mjs +2 -0
- package/src/bridge/host.mjs +2 -0
- package/src/bridge/network.mjs +2 -0
- package/src/bridge/policy.mjs +3 -1
- package/src/bridge/qr.mjs +2 -0
- package/src/bridge/server.mjs +2 -0
- package/src/capabilities.mjs +11 -0
- package/src/chats.mjs +2 -0
- package/src/checkpoint.mjs +18 -1
- package/src/choices.mjs +2 -0
- package/src/cli-args.mjs +2 -0
- package/src/cold.mjs +2 -0
- package/src/commands.mjs +2 -0
- package/src/config.mjs +2 -0
- package/src/conversation-context.mjs +15 -1
- package/src/credentials.mjs +35 -3
- package/src/dataset.mjs +78 -4
- package/src/directives.mjs +2 -0
- package/src/distiller.mjs +2 -0
- package/src/embeddings.mjs +2 -0
- package/src/enlaces.mjs +214 -6
- package/src/event-store.mjs +2 -0
- package/src/exam.mjs +6 -4
- package/src/extensions.mjs +13 -3
- package/src/eyecat-watch.mjs +2 -0
- package/src/eyecat.mjs +2 -0
- package/src/file-ops.mjs +2 -0
- package/src/files.mjs +32 -0
- package/src/github.mjs +2 -0
- package/src/heartbeat.mjs +2 -0
- package/src/i18n.mjs +31 -1
- package/src/image-studio.mjs +2 -0
- package/src/json-file.mjs +2 -0
- package/src/launch.mjs +2 -0
- package/src/lease.mjs +5 -1
- package/src/lobby-work.mjs +2 -0
- package/src/local-ai.mjs +2 -0
- package/src/looks.mjs +127 -2
- package/src/market-key.mjs +8 -0
- package/src/market-remote.mjs +239 -0
- package/src/maturity.mjs +3 -1
- package/src/mcp/connect.mjs +345 -0
- package/src/mcp/github-server.mjs +2 -0
- package/src/mcp/image-server.mjs +2 -0
- package/src/mcp/memory-server.mjs +2 -0
- package/src/mcp/proxy.mjs +153 -0
- package/src/mcp/smtp-server.mjs +2 -0
- package/src/memory-dashboard.mjs +2 -0
- package/src/memory-tools.mjs +2 -0
- package/src/memory.mjs +2 -0
- package/src/models.mjs +49 -3
- package/src/module-scope.mjs +68 -0
- package/src/modules/ahp.mjs +104 -7
- package/src/modules/ash-policy.mjs +2 -0
- package/src/modules/ash.mjs +3 -1
- package/src/modules/bishop-exam.mjs +122 -0
- package/src/modules/bishop-plan.mjs +5 -3
- package/src/modules/bishop.mjs +84 -23
- package/src/modules/choices.mjs +2 -0
- package/src/modules/correo.mjs +2 -0
- package/src/modules/enlaces.mjs +89 -6
- package/src/modules/git-pulse.mjs +2 -0
- package/src/modules/github.mjs +2 -0
- package/src/modules/helpers.mjs +2 -0
- package/src/modules/image-studio.mjs +13 -2
- package/src/modules/index.mjs +4 -1
- package/src/modules/intents-calibration.mjs +2 -0
- package/src/modules/intents-desk.mjs +4 -2
- package/src/modules/intents-observe.mjs +2 -0
- package/src/modules/intents-policy.mjs +4 -1
- package/src/modules/intents-try.mjs +2 -0
- package/src/modules/intents.mjs +11 -7
- package/src/modules/local-models.mjs +124 -0
- package/src/modules/mcp-connect.mjs +293 -0
- package/src/modules/ollama.mjs +15 -4
- package/src/modules/playwright.mjs +88 -15
- package/src/modules/ripley.mjs +2 -0
- package/src/modules/runs.mjs +5 -3
- package/src/modules/sdk.mjs +63 -9
- package/src/modules/updates.mjs +2 -0
- package/src/mother.mjs +2 -0
- package/src/npm-launch.mjs +18 -0
- package/src/ollama.mjs +8 -5
- package/src/outbound.mjs +16 -6
- package/src/override.mjs +2 -0
- package/src/ports.mjs +6 -2
- package/src/prices.mjs +2 -0
- package/src/privacy.mjs +2 -0
- package/src/quota-monitor.mjs +2 -0
- package/src/quota-sources.mjs +2 -0
- package/src/request-guard.mjs +2 -0
- package/src/review-ask.mjs +2 -0
- package/src/room/archivist.mjs +2 -0
- package/src/room/attachments.mjs +2 -0
- package/src/room/budget.mjs +2 -0
- package/src/room/context.mjs +2 -0
- package/src/room/control.mjs +7 -1
- package/src/room/copilot.mjs +76 -0
- package/src/room/economy.mjs +2 -0
- package/src/room/escalation.mjs +2 -0
- package/src/room/ghost.mjs +2 -0
- package/src/room/guard.mjs +2 -0
- package/src/room/prompt.mjs +25 -5
- package/src/room/returns.mjs +82 -0
- package/src/room/runs.mjs +4 -0
- package/src/room/vectors.mjs +2 -0
- package/src/room.mjs +223 -35
- package/src/router.mjs +3 -1
- package/src/runs.mjs +6 -1
- package/src/runtime-detection.mjs +11 -0
- package/src/salas.mjs +28 -0
- package/src/sentinel-errors.mjs +2 -0
- package/src/server-sentinel.mjs +2 -0
- package/src/server.mjs +757 -81
- package/src/setup.mjs +3 -1
- package/src/smtp.mjs +2 -0
- package/src/tour.mjs +2 -0
- package/src/updates.mjs +2 -0
- package/src/usage-sentinel.mjs +2 -0
- package/src/vault.mjs +2 -0
- package/src/verdict.mjs +3 -1
- package/src/whats-new.mjs +2 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
|
|
3
|
+
// Cursor's agent CLI (`cursor-agent`, 2026.05), added 2026-10-17. It reads only: every turn runs in
|
|
4
|
+
// Cursor's own ask mode (`--mode ask`, «Q&A style, read-only») under a configuration made for the
|
|
5
|
+
// turn and removed after it (CURSOR_CONFIG_DIR), which denies every command and every write and
|
|
6
|
+
// keeps the sandbox off. Print mode alone would give it every tool, write and shell included, so
|
|
7
|
+
// neither is ever left out. Its sign-in lives in the system keychain, not in that folder, so the
|
|
8
|
+
// turn keeps the person's session and nothing else of their settings.
|
|
9
|
+
//
|
|
10
|
+
// Tried against the real CLI on 2026-10-17: it read and answered; asked to create a file and to run
|
|
11
|
+
// `touch`, it wrote nothing and said it was in Ask mode. `--sandbox enabled` was dropped: on a Mac
|
|
12
|
+
// where Cursor's sandbox is not available the run ends at once («Sandbox mode is enabled but not
|
|
13
|
+
// available on this system»). Writing (#2 and up) waits for its own trial (capabilities.mjs).
|
|
14
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
15
|
+
import { tmpdir } from 'node:os';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { runReadonlyProcess } from './process.mjs';
|
|
18
|
+
|
|
19
|
+
// What the turn's own configuration allows: reading, and nothing else.
|
|
20
|
+
export const READONLY_CONFIG = {
|
|
21
|
+
version: 1,
|
|
22
|
+
permissions: { allow: [], deny: ['Shell(*)', 'Write(**)'] },
|
|
23
|
+
approvalMode: 'allowlist',
|
|
24
|
+
sandbox: { mode: 'disabled' },
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function buildCursorArgs({ projectRoot, prompt, model = process.env.PULSE_CURSOR_MODEL || null }) {
|
|
28
|
+
return [
|
|
29
|
+
'--print',
|
|
30
|
+
'--output-format', 'stream-json',
|
|
31
|
+
'--mode', 'ask',
|
|
32
|
+
// A headless run cannot answer the «trust this folder?» question; the project is the human's.
|
|
33
|
+
'--trust',
|
|
34
|
+
'--workspace', projectRoot,
|
|
35
|
+
...(model && model !== 'auto' ? ['--model', model] : []),
|
|
36
|
+
prompt,
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function cursorEnvironment(environment = process.env, { configDir = null } = {}) {
|
|
41
|
+
// A turn never opens a browser: a sign-in is the room's to start, from Settings › Agents.
|
|
42
|
+
return { ...environment, NO_OPEN_BROWSER: '1', ...(configDir ? { CURSOR_CONFIG_DIR: configDir } : {}) };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// `--output-format stream-json` writes one JSON object per line and closes with a `result` line
|
|
46
|
+
// that carries the whole answer and, when the CLI has it, the turn's tokens. The assistant lines
|
|
47
|
+
// before it are the same text in pieces: read when the result never came (the run was cut short).
|
|
48
|
+
export function parseCursorOutput(output) {
|
|
49
|
+
const pieces = [];
|
|
50
|
+
let result = null;
|
|
51
|
+
let usage = null;
|
|
52
|
+
let error = null;
|
|
53
|
+
for (const line of String(output ?? '').split('\n').filter(Boolean)) {
|
|
54
|
+
let event;
|
|
55
|
+
try { event = JSON.parse(line); } catch { continue; }
|
|
56
|
+
if (event?.type === 'assistant') {
|
|
57
|
+
for (const part of event.message?.content ?? []) if (part?.type === 'text' && typeof part.text === 'string') pieces.push(part.text);
|
|
58
|
+
}
|
|
59
|
+
if (event?.type === 'result') {
|
|
60
|
+
if (event.is_error) error = typeof event.result === 'string' && event.result.trim() ? event.result.trim() : 'Cursor returned an error.';
|
|
61
|
+
else if (typeof event.result === 'string') result = event.result;
|
|
62
|
+
const tokens = event.usage;
|
|
63
|
+
if (tokens && typeof tokens === 'object') {
|
|
64
|
+
const input = Number(tokens.inputTokens ?? tokens.input_tokens ?? 0);
|
|
65
|
+
const output = Number(tokens.outputTokens ?? tokens.output_tokens ?? 0);
|
|
66
|
+
const cached = Number(tokens.cacheReadTokens ?? tokens.cache_read_tokens ?? 0);
|
|
67
|
+
usage = { inputTokens: input, cachedInputTokens: cached, outputTokens: output, reasoningTokens: 0, totalTokens: input + output, costUsd: null, source: 'cursor-json' };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (event?.type === 'error' && !error) error = String(event.error?.message ?? event.message ?? 'Cursor returned an error.');
|
|
71
|
+
}
|
|
72
|
+
// A run that ended in an error answers nothing, so the room reports the error, not a fragment.
|
|
73
|
+
const text = error ? '' : (result ?? pieces.join('')).trim();
|
|
74
|
+
return { text, usage, ...(error ? { error } : {}) };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function invokeCursor({ executable, projectRoot, prompt, timeoutMs = 120000, signal, model = null, onProgress = null }) {
|
|
78
|
+
const configDir = await mkdtemp(join(tmpdir(), 'madre-cursor-'));
|
|
79
|
+
try {
|
|
80
|
+
await writeFile(join(configDir, 'cli-config.json'), `${JSON.stringify(READONLY_CONFIG, null, 2)}\n`);
|
|
81
|
+
return await runReadonlyProcess({
|
|
82
|
+
onProgress: onProgress ? (out) => onProgress({ chars: out.length }) : null,
|
|
83
|
+
executable,
|
|
84
|
+
args: buildCursorArgs({ projectRoot, prompt, ...(model ? { model } : {}) }),
|
|
85
|
+
cwd: projectRoot,
|
|
86
|
+
env: cursorEnvironment(process.env, { configDir }),
|
|
87
|
+
timeoutMs,
|
|
88
|
+
signal,
|
|
89
|
+
label: 'Cursor',
|
|
90
|
+
parse: parseCursorOutput,
|
|
91
|
+
});
|
|
92
|
+
} finally {
|
|
93
|
+
// The chats and caches the CLI wrote there go with it: a turn keeps no session of its own.
|
|
94
|
+
await rm(configDir, { recursive: true, force: true, maxRetries: 6, retryDelay: 60 }).catch(() => {});
|
|
95
|
+
}
|
|
96
|
+
}
|
package/src/adapters/gemini.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
|
|
1
3
|
import { priceTokens, PRICES_CHECKED } from '../prices.mjs';
|
|
2
4
|
import { copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
5
|
import { t } from '../i18n.mjs';
|
|
@@ -276,7 +278,14 @@ export function diagnoseGeminiStderr(stderr) {
|
|
|
276
278
|
if (/status:?\s*503|UNAVAILABLE|high demand/i.test(text)) {
|
|
277
279
|
return { code: 'UNAVAILABLE', message: t('Google reported the model as unavailable (HTTP 503) and the CLI kept retrying.'), hint: t('Try again shortly or choose another model.') };
|
|
278
280
|
}
|
|
279
|
-
|
|
281
|
+
// Google closed Gemini Code Assist for individuals to this CLI (seen 2026-10-17): the personal
|
|
282
|
+
// login still holds a valid token and every request is refused before it reaches a model. Named
|
|
283
|
+
// apart from a bad key so the room says what to do, and with Google's own word in the message so
|
|
284
|
+
// Diagnóstico finds its entry (gemini-ineligible-tier) in whatever language the room speaks.
|
|
285
|
+
if (/IneligibleTierError|no longer supported for Gemini Code Assist|migrate to the Antigravity/i.test(text)) {
|
|
286
|
+
return { code: 'INELIGIBLE', message: t('Google no longer accepts this Gemini sign-in (IneligibleTierError): the free personal Google account is refused before it reaches a model.'), hint: t('Paste a Gemini API key from Google AI Studio in Settings › Agents › Gemini, or run `gemini`, type /auth and pick "Use Gemini API key".') };
|
|
287
|
+
}
|
|
288
|
+
if (/status:\s*40[13]|PERMISSION_DENIED|API key not valid/i.test(text)) {
|
|
280
289
|
return { code: 'AUTH', message: t('Google rejected the Gemini credentials.'), hint: t('Run `gemini` and use /auth, or check GEMINI_API_KEY.') };
|
|
281
290
|
}
|
|
282
291
|
// A subagent, refused. The policy is right to refuse it — a subagent answers outside the lease
|
|
@@ -288,7 +297,7 @@ export function diagnoseGeminiStderr(stderr) {
|
|
|
288
297
|
if (/Unauthorized tool call:\s*'?invoke_agent'?|Blocked call:[^\n]*invoke_agent/i.test(text)) {
|
|
289
298
|
return {
|
|
290
299
|
code: 'SUBAGENT_BLOCKED',
|
|
291
|
-
message: t('Gemini tried to hand this
|
|
300
|
+
message: t('Gemini tried to hand this task to one of its own subagents. MADRE does not allow that: a subagent answers outside the permission MADRE controls, so nothing it wrote would be checked, shown or undoable.'),
|
|
292
301
|
hint: t('Ask again saying it should do the work itself, without subagents.'),
|
|
293
302
|
};
|
|
294
303
|
}
|
|
@@ -335,7 +344,7 @@ export async function invokeGemini({
|
|
|
335
344
|
const diagnosis = diagnoseGeminiStderr(stderr);
|
|
336
345
|
if (!diagnosis) return null;
|
|
337
346
|
firstSeenAt ??= Date.now();
|
|
338
|
-
const terminal = diagnosis.code === 'CREDITS_DEPLETED' || diagnosis.code === 'AUTH';
|
|
347
|
+
const terminal = diagnosis.code === 'CREDITS_DEPLETED' || diagnosis.code === 'AUTH' || diagnosis.code === 'INELIGIBLE';
|
|
339
348
|
if (!terminal && Date.now() - firstSeenAt < failFastMs) return null;
|
|
340
349
|
return Object.assign(new Error(`${diagnosis.message} ${diagnosis.hint}`), { code: diagnosis.code, diagnosis });
|
|
341
350
|
};
|
|
@@ -355,7 +364,7 @@ export async function invokeGemini({
|
|
|
355
364
|
} catch (error) {
|
|
356
365
|
const produced = parseGeminiOutput(error.partialOutput ?? '').text;
|
|
357
366
|
const diagnosis = error.diagnosis ?? diagnoseGeminiStderr(error.partialStderr ?? error.stderr ?? '');
|
|
358
|
-
if (diagnosis?.code === 'AUTH' || diagnosis?.code === 'CREDITS_DEPLETED') {
|
|
367
|
+
if (diagnosis?.code === 'AUTH' || diagnosis?.code === 'INELIGIBLE' || diagnosis?.code === 'CREDITS_DEPLETED') {
|
|
359
368
|
error.message = `${diagnosis.message} ${diagnosis.hint}`;
|
|
360
369
|
error.code = diagnosis.code;
|
|
361
370
|
throw error;
|
package/src/adapters/madre.mjs
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
|
|
1
3
|
// @madre: the room's own intelligence. A local model (Ollama) that answers
|
|
2
4
|
// from what the room remembers: the whole archive, not only the recent window,
|
|
3
5
|
// plus the files the human points at. It never writes, never delegates, never
|
|
4
6
|
// leaves the machine. It grows as the memory grows.
|
|
5
7
|
|
|
6
8
|
import { ollamaGenerate } from '../ollama.mjs';
|
|
9
|
+
import { inLanguage } from '../i18n.mjs';
|
|
7
10
|
|
|
8
11
|
export const MADRE_AGENT_ID = 'madre';
|
|
9
12
|
export const MADRE_ADAPTER = 'madre-local';
|
|
@@ -230,7 +233,7 @@ export function gist(text, max = 240) {
|
|
|
230
233
|
export function roundBullets(round, { es = true } = {}) {
|
|
231
234
|
return round.answers.map((answer) => (answer.sequence
|
|
232
235
|
? `- @${answer.agent} [#${answer.sequence}]: ${gist(answer.text)}`
|
|
233
|
-
: `- @${answer.agent}: ${es ? 'no respondió' : 'did not answer'}${answer.error ? ` (${
|
|
236
|
+
: `- @${answer.agent}: ${es ? 'no respondió' : 'did not answer'}${answer.error ? ` (${gist(inLanguage(answer.error, es ? 'es' : 'en'), 200).replace(/[.\s]+$/, '')})` : ''}.`)).join('\n');
|
|
234
237
|
}
|
|
235
238
|
export function comparisonHolds(line, round) {
|
|
236
239
|
const spoke = new Set(round.answers.filter((answer) => answer.sequence).map((answer) => answer.agent));
|
|
@@ -264,7 +267,7 @@ export async function gather(memory, text, { maxChars = 7000 } = {}) {
|
|
|
264
267
|
export function madreInvoker({ memory, ollama, fetchImpl = globalThis.fetch }) {
|
|
265
268
|
return async ({ prompt, text = '', timeoutMs = 180000, model = null, requester = 'you', crew = [], delegation = false, maxSteps = 4, round = null, bishop = null }) => {
|
|
266
269
|
const state = typeof ollama === 'function' ? ollama() : ollama;
|
|
267
|
-
if (!state?.running || !state.chatModel) throw new Error('@madre left the
|
|
270
|
+
if (!state?.running || !state.chatModel) throw new Error('@madre left the project: Ollama is not running. Start Ollama, or open Local AI in Modules.');
|
|
268
271
|
const question = text || prompt.match(/User message: ([\s\S]*)$/)?.[1] || prompt.slice(-2000);
|
|
269
272
|
const local = localReply(question, { requester, crew, delegation, maxSteps, model: model ?? state.chatModel });
|
|
270
273
|
if (local) return { text: local.text, usage: { ...NO_USAGE }, grounded: { notes: 0, quotes: 0 }, synthetic: local.kind, declined: local.kind === 'declined' ? 'action' : undefined };
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
|
|
1
3
|
import { runReadonlyProcess } from './process.mjs';
|
|
2
4
|
|
|
3
5
|
const readonlyConfig = {
|
|
@@ -21,13 +23,15 @@ const readonlyConfig = {
|
|
|
21
23
|
// Optional provider/model override (e.g. "openai/gpt-5.6-sol"). Without it,
|
|
22
24
|
// OpenCode picks its own default provider, which may not be the one the user
|
|
23
25
|
// actually signed in to.
|
|
24
|
-
export function buildOpenCodeArgs({ projectRoot, prompt, model = process.env.PULSE_OPENCODE_MODEL, attachments = [] }) {
|
|
26
|
+
export function buildOpenCodeArgs({ projectRoot, prompt, model = process.env.PULSE_OPENCODE_MODEL, effort = null, attachments = [] }) {
|
|
25
27
|
return [
|
|
26
28
|
'--pure',
|
|
27
29
|
'run',
|
|
28
30
|
'--format', 'json',
|
|
29
31
|
'--agent', 'pulse-readonly',
|
|
30
32
|
...(model ? ['--model', model] : []),
|
|
33
|
+
// The model's reasoning rung, as OpenCode names it (`--variant`, 1.18).
|
|
34
|
+
...(effort ? ['--variant', effort] : []),
|
|
31
35
|
...attachments.flatMap((file) => ['--file', file.path]),
|
|
32
36
|
'--dir', projectRoot,
|
|
33
37
|
prompt,
|
|
@@ -154,11 +158,11 @@ export function parseOpenCodeOutput(output) {
|
|
|
154
158
|
return { text: text.join('').trim(), usage, ...(error ? { error } : {}) };
|
|
155
159
|
}
|
|
156
160
|
|
|
157
|
-
export function invokeOpenCode({ executable, projectRoot, prompt, timeoutMs = 120000, signal, model = null, attachments = [], lease = null, scopes = null, imageStudio = null, memoryServer = null, mcpServers = [], onProgress = null }) {
|
|
161
|
+
export function invokeOpenCode({ executable, projectRoot, prompt, timeoutMs = 120000, signal, model = null, effort = null, attachments = [], lease = null, scopes = null, imageStudio = null, memoryServer = null, mcpServers = [], onProgress = null }) {
|
|
158
162
|
return runReadonlyProcess({
|
|
159
163
|
onProgress: onProgress ? (out) => onProgress({ chars: out.length }) : null,
|
|
160
164
|
executable,
|
|
161
|
-
args: buildOpenCodeArgs({ projectRoot, prompt, attachments, ...(model ? { model } : {}) }),
|
|
165
|
+
args: buildOpenCodeArgs({ projectRoot, prompt, attachments, effort, ...(model ? { model } : {}) }),
|
|
162
166
|
cwd: projectRoot,
|
|
163
167
|
env: openCodeEnvironment(process.env, { lease, scopes, imageStudio, memoryServer, mcpServers }),
|
|
164
168
|
timeoutMs,
|
package/src/adapters/process.mjs
CHANGED
package/src/asking.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
|
|
1
3
|
// What the room should ask next.
|
|
2
4
|
//
|
|
3
5
|
// The maturity reading says where the archive is thin; it does not say what to do about it on a
|
package/src/auth-probe.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
|
|
1
3
|
import { execFile } from 'node:child_process';
|
|
2
4
|
import { t } from './i18n.mjs';
|
|
3
|
-
import { access, readFile } from 'node:fs/promises';
|
|
5
|
+
import { access, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
6
|
import { homedir } from 'node:os';
|
|
5
7
|
import { join } from 'node:path';
|
|
6
8
|
import { promisify } from 'node:util';
|
|
@@ -43,6 +45,18 @@ export function parseOpenCodeAuthList({ stdout = '' }) {
|
|
|
43
45
|
return { state: 'signed-out', detail: 'no providers configured', providers: [] };
|
|
44
46
|
}
|
|
45
47
|
|
|
48
|
+
// `cursor-agent status` says «Not logged in», or who is signed in («Logged in as …»). A key in the
|
|
49
|
+
// environment (CURSOR_API_KEY) signs every run in on its own, so it counts too.
|
|
50
|
+
export function parseCursorStatus({ stdout = '', stderr = '' }, env = process.env) {
|
|
51
|
+
const said = stripAnsi(`${stdout}\n${stderr}`);
|
|
52
|
+
if (/not\s+logged\s+in|not\s+authenticated|log\s*in\s+first/i.test(said)) {
|
|
53
|
+
return env.CURSOR_API_KEY ? { state: 'signed-in', detail: 'via CURSOR_API_KEY' } : { state: 'signed-out', detail: 'not logged in' };
|
|
54
|
+
}
|
|
55
|
+
const who = said.match(/logged\s+in\s+(?:as\s+)?([^\s]+@[^\s]+|\S+)/i)?.[1];
|
|
56
|
+
if (who) return { state: 'signed-in', detail: `as ${who}` };
|
|
57
|
+
return env.CURSOR_API_KEY ? { state: 'signed-in', detail: 'via CURSOR_API_KEY' } : { state: 'unknown', detail: said.trim().split('\n')[0]?.slice(0, 120) || 'no answer from cursor-agent status' };
|
|
58
|
+
}
|
|
59
|
+
|
|
46
60
|
export function geminiAuthState({ settings, hasOauth, hasApiKey }) {
|
|
47
61
|
const type = settings?.security?.auth?.selectedType;
|
|
48
62
|
if (type === 'gemini-api-key') return hasApiKey ? { state: 'signed-in', detail: 'via API key' } : { state: 'signed-out', detail: 'API key selected but not found' };
|
|
@@ -85,8 +99,9 @@ export async function probeGemini({ env = process.env } = {}) {
|
|
|
85
99
|
return geminiAuthState({ settings, hasOauth, hasApiKey });
|
|
86
100
|
}
|
|
87
101
|
|
|
88
|
-
// What each CLI needs from the user.
|
|
89
|
-
//
|
|
102
|
+
// What each CLI needs from the user. The room runs the sign-in itself; the same command typed in a
|
|
103
|
+
// terminal stays as the other way in (`terminal`, `terminalNote`), offered and never required.
|
|
104
|
+
// The setup wizard runs them in the user's terminal; install hints are only printed.
|
|
90
105
|
export const AGENT_SETUP = {
|
|
91
106
|
codex: {
|
|
92
107
|
install: ['npm install -g @openai/codex', 'or install the ChatGPT desktop app, which bundles codex'],
|
|
@@ -114,27 +129,86 @@ export const AGENT_SETUP = {
|
|
|
114
129
|
// Its browser flow has no local server to come back to: the page gives a code and the CLI
|
|
115
130
|
// waits for it on its own input. The room asks for it and types it in.
|
|
116
131
|
asksForCode: true,
|
|
132
|
+
terminalNote: 'Add --console to bill the work to the Anthropic Console instead of your subscription.',
|
|
117
133
|
vendor: 'Anthropic',
|
|
118
134
|
account: 'Signs in with a Claude account or with the Anthropic Console. An Anthropic API key works too.',
|
|
119
135
|
paid: true,
|
|
120
136
|
},
|
|
121
137
|
gemini: {
|
|
122
138
|
install: ['npm install -g @google/gemini-cli'],
|
|
123
|
-
|
|
124
|
-
|
|
139
|
+
// Gemini has no sign-in command: it signs in the first time it is asked something with
|
|
140
|
+
// "Login with Google" selected. Without a terminal it asks one yes/no, opens the browser by
|
|
141
|
+
// itself and waits on 127.0.0.1 for Google to come back, so the room needs no code and no
|
|
142
|
+
// address. The one-word question it is launched with is let through once the credentials are
|
|
143
|
+
// written: Google refuses free personal accounts only there, and a refused sign-in is undone
|
|
144
|
+
// (see geminiSignIn and loginAgent in server.mjs).
|
|
145
|
+
login: ['--prompt', 'ok'],
|
|
146
|
+
loginNote: 'Opens your browser to sign in with your Google account.',
|
|
147
|
+
browser: true,
|
|
148
|
+
methods: [
|
|
149
|
+
// The free personal account is refused by Google on this CLI (IneligibleTierError, 2026-10-17);
|
|
150
|
+
// the door stays for the accounts Google still serves, and says so.
|
|
151
|
+
{ id: 'google', label: 'Google account', args: ['--prompt', 'ok'], note: 'Opens your browser to sign in with your Google account. Google refuses free personal accounts on this CLI; if it refuses yours, paste a Gemini API key from Google AI Studio instead.' },
|
|
152
|
+
],
|
|
153
|
+
room: {
|
|
154
|
+
display: 'gemini · Login with Google',
|
|
155
|
+
flow: 'gemini-google',
|
|
156
|
+
// It asks "Do you want to continue? [Y/n]" first; the click on the button was the yes.
|
|
157
|
+
consent: true,
|
|
158
|
+
// With CI set the CLI refuses to open a browser and gives up without a terminal.
|
|
159
|
+
ci: false,
|
|
160
|
+
// A key in the environment would sign it in with the key instead of the account chosen.
|
|
161
|
+
dropEnv: ['GEMINI_API_KEY', 'GOOGLE_API_KEY'],
|
|
162
|
+
// One process, so cancelling stops the one that holds the browser's way back.
|
|
163
|
+
env: { GEMINI_CLI_NO_RELAUNCH: 'true' },
|
|
164
|
+
},
|
|
165
|
+
// In a terminal it is its own prompt that signs in (the setup wizard hands it over whole).
|
|
125
166
|
interactive: true,
|
|
167
|
+
terminal: 'gemini',
|
|
168
|
+
terminalNote: 'Then type /auth and pick "Use Gemini API key" with a key from aistudio.google.com/app/apikey. "Login with Google" is refused for free personal accounts.',
|
|
126
169
|
vendor: 'Google',
|
|
127
|
-
account: '
|
|
128
|
-
|
|
170
|
+
account: 'Connects with a Gemini API key from Google AI Studio. A Google account works only where Google still accepts it: free personal accounts are refused.',
|
|
171
|
+
// Whether it costs depends on the key's project, not on the CLI: no «free option» promised.
|
|
172
|
+
paid: null,
|
|
129
173
|
},
|
|
130
174
|
opencode: {
|
|
131
175
|
install: ['brew install opencode', 'or: npm install -g opencode-ai'],
|
|
132
176
|
login: ['auth', 'login'],
|
|
133
177
|
loginNote: 'Pick a provider and paste its key or complete its OAuth flow.',
|
|
178
|
+
// Its keys are pasted from the room (see openCodeProviders); these are the providers it signs
|
|
179
|
+
// in to with an account. `-p` skips its provider list, so the room never draws a menu. Only
|
|
180
|
+
// the ones whose owner says a third party may sign in this way: GitHub supports Copilot in
|
|
181
|
+
// OpenCode since 2026-01. Left out on purpose: a Claude subscription (Anthropic does not allow
|
|
182
|
+
// claude.ai sign-ins in other tools), and ChatGPT and GitLab, until OpenAI or GitLab say so in
|
|
183
|
+
// writing. They stay one `opencode auth login` away, in the terminal, for whoever chooses them.
|
|
184
|
+
browser: true,
|
|
185
|
+
methods: [
|
|
186
|
+
{
|
|
187
|
+
id: 'github-copilot',
|
|
188
|
+
label: 'GitHub Copilot',
|
|
189
|
+
args: ['auth', 'login', '-p', 'github-copilot'],
|
|
190
|
+
note: 'Gives you a code to type at github.com/login/device: the work goes against your Copilot subscription (Pro, Pro+, Business or Enterprise).',
|
|
191
|
+
// It asks which GitHub first; the room takes github.com. Enterprise is in the terminal.
|
|
192
|
+
room: { deviceCode: true, answers: [{ prompt: 'Select GitHub deployment type', send: '\r' }] },
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
terminalNote: 'Pick a provider, then paste its key or sign in with its account. GitHub Enterprise and the other providers are in its list.',
|
|
134
196
|
vendor: 'OpenCode',
|
|
135
197
|
account: 'Brings no model of its own: you point it at a provider you already use, in the cloud or on this computer.',
|
|
136
198
|
paid: null,
|
|
137
199
|
},
|
|
200
|
+
// Cursor's agent CLI (2026-10-17): `login` opens the browser and waits for Cursor to come back.
|
|
201
|
+
// It has no npm package; its own installer is the way in, typed in a terminal.
|
|
202
|
+
cursor: {
|
|
203
|
+
install: ['curl https://cursor.com/install -fsS | bash'],
|
|
204
|
+
login: ['login'],
|
|
205
|
+
loginNote: 'Opens your browser to sign in with your Cursor account.',
|
|
206
|
+
browser: true,
|
|
207
|
+
terminal: 'cursor-agent login',
|
|
208
|
+
vendor: 'Cursor',
|
|
209
|
+
account: 'Signs in with your Cursor account and uses its plan. For now it reads and answers; it does not write in your project.',
|
|
210
|
+
paid: true,
|
|
211
|
+
},
|
|
138
212
|
};
|
|
139
213
|
|
|
140
214
|
// Installing a CLI from the room itself. npm is the common denominator: every one of these
|
|
@@ -155,6 +229,10 @@ export const TAKES_KEY = new Set(['claude', 'gemini', 'opencode']);
|
|
|
155
229
|
export const SIGN_IN_URL = /https?:\/\/\S+/;
|
|
156
230
|
export const CODE_PROMPT = /paste\s+(?:the\s+)?code|enter\s+(?:the\s+)?code|c[oó]digo\s*[:>]/i;
|
|
157
231
|
export const looksLikeCodePrompt = (line) => CODE_PROMPT.test(String(line ?? ''));
|
|
232
|
+
// The other way round: a device sign-in shows a code to type on the provider's page
|
|
233
|
+
// ("Enter code: 3475-C193"), and the room has to show it, not ask for one.
|
|
234
|
+
export const DEVICE_CODE = /enter\s+(?:the\s+|this\s+)?code:?\s+([A-Z0-9]{4,}(?:-[A-Z0-9]{4,})+)/i;
|
|
235
|
+
export const deviceCodeIn = (line) => String(line ?? '').match(DEVICE_CODE)?.[1] ?? null;
|
|
158
236
|
|
|
159
237
|
export function accountNoteFor(id) {
|
|
160
238
|
// Said in the room's language when it is asked for, not when this file is imported.
|
|
@@ -183,10 +261,10 @@ export function installPlanFor(agent, { prefix = null } = {}) {
|
|
|
183
261
|
};
|
|
184
262
|
}
|
|
185
263
|
|
|
186
|
-
// How the room can (re)connect an agent. Codex and
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
264
|
+
// How the room can (re)connect an agent. Codex, Claude and Gemini sign in with a browser flow
|
|
265
|
+
// their own CLI drives, so the server can run them and stream what they say; OpenCode too, for
|
|
266
|
+
// the providers in its `methods`, and its keys are pasted. Every plan also carries `terminal`:
|
|
267
|
+
// the same sign-in typed by hand, for whoever prefers it.
|
|
190
268
|
export function loginPlanFor(agent, { method = null } = {}) {
|
|
191
269
|
const setup = AGENT_SETUP[agent.id];
|
|
192
270
|
if (!setup) return null;
|
|
@@ -194,17 +272,71 @@ export function loginPlanFor(agent, { method = null } = {}) {
|
|
|
194
272
|
// Which door: the one asked for, the first one the CLI offers, or the single flow it has.
|
|
195
273
|
const chosen = setup.methods?.find((one) => one.id === method) ?? setup.methods?.[0] ?? null;
|
|
196
274
|
const args = chosen?.args ?? setup.login;
|
|
275
|
+
const room = headless ? { ...(setup.room ?? {}), ...(chosen?.room ?? {}) } : {};
|
|
197
276
|
return {
|
|
198
277
|
headless,
|
|
199
278
|
command: agent.path ?? agent.id,
|
|
200
279
|
args: headless ? args : [],
|
|
201
|
-
display:
|
|
280
|
+
display: room.display ?? `${agent.id} ${(headless ? args : setup.login).join(' ')}`,
|
|
202
281
|
note: chosen?.note ?? setup.loginNote,
|
|
282
|
+
terminal: { command: setup.terminal ?? `${agent.id} ${setup.login.join(' ')}`, note: setup.terminalNote ?? setup.loginNote },
|
|
203
283
|
install: setup.install,
|
|
204
284
|
// What the room offers as a choice, and what it must be ready to be asked for.
|
|
205
285
|
methods: setup.methods ? setup.methods.map(({ id, label, note }) => ({ id, label, note })) : null,
|
|
206
286
|
method: chosen?.id ?? null,
|
|
207
287
|
asksForCode: Boolean(headless && setup.asksForCode),
|
|
288
|
+
// How the room runs it, when the CLI needs more than its arguments (see AGENT_SETUP.gemini.room).
|
|
289
|
+
...(room.flow ? { flow: room.flow } : {}),
|
|
290
|
+
...(room.consent ? { consent: true } : {}),
|
|
291
|
+
...(room.ci === false ? { ci: false } : {}),
|
|
292
|
+
...(room.env ? { env: room.env } : {}),
|
|
293
|
+
...(room.dropEnv ? { dropEnv: room.dropEnv } : {}),
|
|
294
|
+
// A code to show rather than one to paste, and the menu questions the click already answered.
|
|
295
|
+
...(room.deviceCode ? { deviceCode: true } : {}),
|
|
296
|
+
...(room.answers ? { answers: room.answers } : {}),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// The yes/no Gemini asks before opening the browser, when it has no terminal to draw a dialog in.
|
|
301
|
+
export const CONSENT_PROMPT = /do you want to continue\?\s*\[y\/n\]/i;
|
|
302
|
+
export const looksLikeConsentPrompt = (line) => CONSENT_PROMPT.test(String(line ?? ''));
|
|
303
|
+
|
|
304
|
+
const GEMINI_REFUSED = /IneligibleTierError|no longer supported for Gemini Code Assist|migrate to the Antigravity/i;
|
|
305
|
+
|
|
306
|
+
// Gemini's Google sign-in, run from the room. Before: "Login with Google" selected in its own
|
|
307
|
+
// settings, and the account it had put aside, so the run signs in anew instead of answering with
|
|
308
|
+
// the old one. During: `signedIn` says when the credentials are written, which is when the room
|
|
309
|
+
// stops the run. After a sign-in that did not finish: `undo` puts both files back as they were.
|
|
310
|
+
// MADRE never reads the token: it moves the file it is in and checks that it exists.
|
|
311
|
+
export function geminiSignIn({ env = process.env, verifyMs = 30000 } = {}) {
|
|
312
|
+
const dir = join(env.GEMINI_CLI_HOME ?? homedir(), '.gemini');
|
|
313
|
+
const settingsFile = join(dir, 'settings.json');
|
|
314
|
+
const credsFile = join(dir, 'oauth_creds.json');
|
|
315
|
+
let before = null;
|
|
316
|
+
const readIfThere = (path) => readFile(path, 'utf8').catch(() => null);
|
|
317
|
+
const putBack = (path, previous) => (previous === null ? rm(path, { force: true }) : writeFile(path, previous, { mode: 0o600 }));
|
|
318
|
+
return {
|
|
319
|
+
async prepare() {
|
|
320
|
+
await mkdir(dir, { recursive: true });
|
|
321
|
+
before = { settings: await readIfThere(settingsFile), creds: await readIfThere(credsFile) };
|
|
322
|
+
let settings = {};
|
|
323
|
+
try { settings = JSON.parse(before.settings ?? '{}') ?? {}; } catch { settings = {}; }
|
|
324
|
+
settings.security = { ...(settings.security ?? {}), auth: { ...(settings.security?.auth ?? {}), selectedType: 'oauth-personal' } };
|
|
325
|
+
await writeFile(settingsFile, `${JSON.stringify(settings, null, 2)}\n`);
|
|
326
|
+
if (before.creds !== null) await rm(credsFile, { force: true });
|
|
327
|
+
},
|
|
328
|
+
signedIn: () => exists(credsFile),
|
|
329
|
+
// Google lets a free personal account sign in and refuses it at the first question
|
|
330
|
+
// (IneligibleTierError, 2026-10-17): a sign-in is only good once that question was not refused.
|
|
331
|
+
refused: (line) => GEMINI_REFUSED.test(String(line ?? '')),
|
|
332
|
+
// How long that first question is waited for before the run is stopped and the sign-in kept.
|
|
333
|
+
verifyMs,
|
|
334
|
+
async undo() {
|
|
335
|
+
if (!before) return;
|
|
336
|
+
await putBack(settingsFile, before.settings);
|
|
337
|
+
await putBack(credsFile, before.creds);
|
|
338
|
+
before = null;
|
|
339
|
+
},
|
|
208
340
|
};
|
|
209
341
|
}
|
|
210
342
|
|
|
@@ -215,6 +347,7 @@ export async function probeAgentAuth(agent) {
|
|
|
215
347
|
case 'codex': return parseCodexLoginStatus(await run(agent.path, ['login', 'status']));
|
|
216
348
|
case 'claude': return parseClaudeAuthStatus(await run(agent.path, ['auth', 'status', '--json']));
|
|
217
349
|
case 'opencode': return parseOpenCodeAuthList(await run(agent.path, ['auth', 'list']));
|
|
350
|
+
case 'cursor': return parseCursorStatus(await run(agent.path, ['status']));
|
|
218
351
|
case 'gemini': return probeGemini();
|
|
219
352
|
default: return { state: 'unknown', detail: 'no probe for this agent' };
|
|
220
353
|
}
|
package/src/bridge/devices.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
|
|
1
3
|
// The phones that may reach this Mac, and the networks where they may.
|
|
2
4
|
//
|
|
3
5
|
// One file, `~/.pulse/devices.json`, owner-only, shared by every room on this Mac: any of them can
|
package/src/bridge/handoff.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
|
|
1
3
|
// Where the person was, on the Mac and on the phone (docs/MOBILE.md, MOV-005).
|
|
2
4
|
//
|
|
3
5
|
// One small file for the whole Mac, `~/.pulse/handoff.json`, owner-only: the last place each side
|
package/src/bridge/host.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
|
|
1
3
|
// Who keeps the bridge up. Every room on this Mac offers to; the port decides.
|
|
2
4
|
//
|
|
3
5
|
// No process of its own and nothing to start: each room, every few seconds, asks whether a phone
|
package/src/bridge/network.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
|
|
1
3
|
// Which network this Mac is on, and how a phone on it reaches the Mac.
|
|
2
4
|
//
|
|
3
5
|
// A network is known by its router: the hardware address of the default gateway, hashed. Two
|
package/src/bridge/policy.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
|
|
1
3
|
// What a phone may do in a room (docs/MOBILE.md, section 4).
|
|
2
4
|
//
|
|
3
5
|
// A list of what passes, not of what does not: a route the room grows tomorrow stays on the Mac
|
|
@@ -15,7 +17,7 @@ import { t } from '../i18n.mjs';
|
|
|
15
17
|
|
|
16
18
|
// Every file the room's page loads: one missing here and app.js cannot import it, so the whole page
|
|
17
19
|
// stops on the phone (test/phone-bridge.test.mjs reads app.js's imports to keep this whole).
|
|
18
|
-
const STATIC = new Set(['/', '/app.js', '/nostromo-engine.js', '/brands.js', '/troubleshooting.js', '/inquiry.js', '/i18n.js', '/es.js', '/resay.js', '/syntax.js', '/editor.js', '/routing.js', '/abduction.js', '/styles.css', '/review-ask.js', '/loader.js', '/tour.js', '/tour-chapters.js', '/touch.js']);
|
|
20
|
+
const STATIC = new Set(['/', '/app.js', '/nostromo-engine.js', '/brands.js', '/troubleshooting.js', '/inquiry.js', '/i18n.js', '/es.js', '/resay.js', '/syntax.js', '/editor.js', '/routing.js', '/abduction.js', '/styles.css', '/review-ask.js', '/loader.js', '/tour.js', '/tour-chapters.js', '/touch.js', '/attribution.js', '/power.js', '/wallpaper.js', '/neural.js', '/hardware.js', '/code-tools.js', '/market.js', '/market-view.js', '/market-art.js']);
|
|
19
21
|
|
|
20
22
|
const READ = new Set([
|
|
21
23
|
'/api/state', '/api/events', '/api/chats', '/api/version', '/api/whats-new', '/api/glance', '/api/handoff',
|
package/src/bridge/qr.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
|
|
1
3
|
// The QR a phone scans to pair with this Mac: text in, a grid of dark and light modules out, and
|
|
2
4
|
// an SVG of that grid the room sizes and colours with CSS.
|
|
3
5
|
//
|
package/src/bridge/server.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
|
|
1
3
|
// The bridge: the one thing on this Mac a phone can reach (docs/MOBILE.md, MOV-001).
|
|
2
4
|
//
|
|
3
5
|
// Rooms keep listening on 127.0.0.1 and keep refusing anything that does not call them by that
|
package/src/capabilities.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
|
|
1
3
|
// What each CLI can do from MADRE, verified against the installed versions on
|
|
2
4
|
// 2026-09-16 (Codex 0.153, Claude Code 2.1, Gemini CLI 0.60, OpenCode 1.18).
|
|
3
5
|
// The room shows this so a human knows whom to ask for what, and an
|
|
@@ -33,6 +35,15 @@ export const CAPABILITIES = {
|
|
|
33
35
|
imageGen: false,
|
|
34
36
|
web: { how: 'policy: allow google_web_search, web_fetch', note: 'Off until turned on in Settings › Agents › Web access.' },
|
|
35
37
|
},
|
|
38
|
+
// Cursor (2026-10-17) reads only, in its own ask mode: writing waits until its confinement has
|
|
39
|
+
// been tried against the real CLI, as OpenCode's was. Attachments are named in the briefing.
|
|
40
|
+
cursor: {
|
|
41
|
+
read: true,
|
|
42
|
+
imageIn: false,
|
|
43
|
+
write: false,
|
|
44
|
+
imageGen: false,
|
|
45
|
+
web: false,
|
|
46
|
+
},
|
|
36
47
|
opencode: {
|
|
37
48
|
read: true,
|
|
38
49
|
imageIn: { how: '-f <file>', note: 'Files attach to the message.' },
|
package/src/chats.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
|
|
1
3
|
// A project has one memory and many conversations.
|
|
2
4
|
//
|
|
3
5
|
// The archive, the crew, the modules and the privacy list belong to the project: they are what
|
package/src/checkpoint.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
|
|
1
3
|
import { execFile } from 'node:child_process';
|
|
2
4
|
import { access, mkdir, mkdtemp, readFile, readdir, rm, rmdir, stat, unlink, writeFile } from 'node:fs/promises';
|
|
3
5
|
import { tmpdir } from 'node:os';
|
|
@@ -224,7 +226,9 @@ async function diffOne(root, before, prefix = '', exclude = []) {
|
|
|
224
226
|
if (!files.length) return { files: [], stat: '' };
|
|
225
227
|
const only = files.length < listed.length ? ['--', ...files.flatMap((file) => [file.path.slice(prefix.length), ...(file.from ? [file.from.slice(prefix.length)] : [])])] : [];
|
|
226
228
|
const stat = (await git(root, ['diff', '--no-ext-diff', '--no-textconv', '--stat=100', before.tree, after, ...only], { via })).trim();
|
|
227
|
-
|
|
229
|
+
// The tree the turn left is kept with what it changed (the code workspace reads it back): the
|
|
230
|
+
// agent's own lines stay its own even after the human edits the file again.
|
|
231
|
+
return { files, afterTree: after, stat: prefix && stat ? stat.split('\n').map((line) => (line.startsWith(' ') ? ` ${prefix}${line.trimStart()}` : line)).join('\n') : stat };
|
|
228
232
|
}
|
|
229
233
|
|
|
230
234
|
// What changed since the checkpoint, in the project and in every nested repository, paths
|
|
@@ -241,6 +245,19 @@ export async function diffCheckpoint(root, checkpoint) {
|
|
|
241
245
|
return { afterTree: outer.afterTree ?? null, files, stat, forbidden: files.filter((file) => forbiddenIn(file.path)).map((file) => file.path) };
|
|
242
246
|
}
|
|
243
247
|
|
|
248
|
+
// What a file said at a photograph, or in the tree a turn left behind, read from git and never from
|
|
249
|
+
// the disk. `null` when git no longer has it (a shadow repository gone with a restart, a tree git
|
|
250
|
+
// pruned), when the file was not there, or when the path is not a path inside the project.
|
|
251
|
+
const CHECKPOINT_ID = /^[0-9A-Za-z][\w-]{0,79}$/;
|
|
252
|
+
const TREE_ID = /^[0-9a-f]{40,64}$/;
|
|
253
|
+
export async function readAt(root, { checkpointId = null, tree = null, path = '' } = {}) {
|
|
254
|
+
const inner = String(path ?? '');
|
|
255
|
+
if (!inner || inner.startsWith('/') || inner.startsWith('-') || inner.split('/').includes('..')) return null;
|
|
256
|
+
const rev = checkpointId ? (CHECKPOINT_ID.test(String(checkpointId)) ? `${CHECKPOINT_REF}/${checkpointId}` : null) : (TREE_ID.test(String(tree ?? '')) ? tree : null);
|
|
257
|
+
if (!rev) return null;
|
|
258
|
+
try { return await git(root, ['show', `${rev}:${inner}`]); } catch { return null; }
|
|
259
|
+
}
|
|
260
|
+
|
|
244
261
|
// Which repository a project-relative path belongs to, and the path inside it.
|
|
245
262
|
function repoFor(root, checkpoint, path) {
|
|
246
263
|
for (const part of checkpoint.nested ?? []) {
|
package/src/choices.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
|
|
1
3
|
// An agent asking the human to choose.
|
|
2
4
|
//
|
|
3
5
|
// The room already has one fenced channel, ```pulse, and it puts other agents to work. This is
|
package/src/cli-args.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
|
|
1
3
|
// Tiny argv reader for the MADRE CLI. Accepts `--name value` and
|
|
2
4
|
// `--name=value`, and remembers which options the user set explicitly so the
|
|
3
5
|
// room can choose sensible defaults (like the next free port) only when they
|
package/src/cold.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
|
|
1
3
|
// Cold zones: the part of the archive nothing has ever reached for.
|
|
2
4
|
//
|
|
3
5
|
// A memory is cold when three things are true at once, and the three together are what make the
|
package/src/commands.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
|
|
1
3
|
import { execFile } from 'node:child_process';
|
|
2
4
|
import { access } from 'node:fs/promises';
|
|
3
5
|
import { join } from 'node:path';
|