@jossuealcala/madre 0.4.1 → 0.5.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 +79 -0
- package/docs/REFERENCE.md +3 -2
- package/docs/SDK.md +19 -5
- package/docs/sdk/hello-module.mjs +8 -0
- package/package.json +1 -1
- package/public/app.js +256 -44
- package/public/es.js +36 -3
- package/public/index.html +2 -0
- package/public/styles.css +102 -11
- package/public/syntax.js +121 -0
- package/public/troubleshooting.js +13 -0
- package/src/adapters/codex.mjs +4 -1
- package/src/modules/index.mjs +23 -5
- package/src/modules/sdk.mjs +15 -0
- package/src/outbound.mjs +1 -1
- package/src/room.mjs +12 -0
- package/src/runtime-detection.mjs +50 -0
- package/src/server.mjs +9 -6
- package/src/updates.mjs +9 -1
|
@@ -38,6 +38,55 @@ const definitions = [
|
|
|
38
38
|
},
|
|
39
39
|
];
|
|
40
40
|
|
|
41
|
+
const OFFICIAL_DESTINATIONS = {
|
|
42
|
+
codex: { label: 'OpenAI', hosts: ['api.openai.com'], variable: 'OPENAI_BASE_URL' },
|
|
43
|
+
claude: { label: 'Anthropic', hosts: ['api.anthropic.com'], variable: 'ANTHROPIC_BASE_URL' },
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const enabled = (value) => /^(1|true|yes)$/i.test(String(value ?? '').trim());
|
|
47
|
+
|
|
48
|
+
function customRoutesAllowed(agentId, env) {
|
|
49
|
+
const raw = String(env.PULSE_ALLOW_CUSTOM_AGENT_ENDPOINTS ?? '').trim().toLowerCase();
|
|
50
|
+
if (/^(1|true|all)$/.test(raw)) return true;
|
|
51
|
+
return raw.split(',').map((item) => item.trim()).includes(agentId);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function routeHost(value) {
|
|
55
|
+
try { return new URL(value).hostname.toLowerCase(); } catch { return null; }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// A CLI owns its network connection, but MADRE owns the document handed to that CLI. Name the
|
|
59
|
+
// actual destination without ever returning a credential, path or query string. Unknown base URLs
|
|
60
|
+
// are held at the door until the human explicitly trusts that agent's endpoint.
|
|
61
|
+
export function agentRouteFromEnvironment(agentId, env = process.env) {
|
|
62
|
+
if (agentId === 'claude' && enabled(env.CLAUDE_CODE_USE_BEDROCK)) {
|
|
63
|
+
return { kind: 'provider', destination: 'Amazon Bedrock', source: 'CLAUDE_CODE_USE_BEDROCK', host: null, custom: false, allowed: true };
|
|
64
|
+
}
|
|
65
|
+
if (agentId === 'claude' && enabled(env.CLAUDE_CODE_USE_VERTEX)) {
|
|
66
|
+
return { kind: 'provider', destination: 'Google Vertex AI', source: 'CLAUDE_CODE_USE_VERTEX', host: null, custom: false, allowed: true };
|
|
67
|
+
}
|
|
68
|
+
if (agentId === 'claude' && enabled(env.CLAUDE_CODE_USE_FOUNDRY)) {
|
|
69
|
+
return { kind: 'provider', destination: 'Microsoft Foundry', source: 'CLAUDE_CODE_USE_FOUNDRY', host: null, custom: false, allowed: true };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const official = OFFICIAL_DESTINATIONS[agentId];
|
|
73
|
+
if (!official) return null;
|
|
74
|
+
const raw = String(env[official.variable] ?? '').trim();
|
|
75
|
+
if (!raw) return { kind: 'official', destination: official.label, source: null, host: null, custom: false, allowed: true };
|
|
76
|
+
const host = routeHost(raw);
|
|
77
|
+
const isOfficial = Boolean(host && official.hosts.some((candidate) => host === candidate || host.endsWith(`.${candidate}`)));
|
|
78
|
+
if (isOfficial) return { kind: 'official', destination: official.label, source: official.variable, host, custom: false, allowed: true };
|
|
79
|
+
const allowed = customRoutesAllowed(agentId, env);
|
|
80
|
+
return {
|
|
81
|
+
kind: 'custom',
|
|
82
|
+
destination: host ? `custom endpoint · ${host}` : 'custom endpoint',
|
|
83
|
+
source: official.variable,
|
|
84
|
+
host,
|
|
85
|
+
custom: true,
|
|
86
|
+
allowed,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
41
90
|
async function executable(path) {
|
|
42
91
|
try {
|
|
43
92
|
await access(path, constants.X_OK);
|
|
@@ -99,6 +148,7 @@ export async function detectAgents({ env = process.env } = {}) {
|
|
|
99
148
|
adapter: definition.adapter ?? null,
|
|
100
149
|
path,
|
|
101
150
|
version: path ? await readVersion(path, definition.versionArgs) : null,
|
|
151
|
+
route: agentRouteFromEnvironment(definition.id, env),
|
|
102
152
|
};
|
|
103
153
|
}));
|
|
104
154
|
}
|
package/src/server.mjs
CHANGED
|
@@ -47,6 +47,9 @@ import { applyKey, keyPlanFor } from './credentials.mjs';
|
|
|
47
47
|
import { loadConfig as readConfig, updateConfig } from './config.mjs';
|
|
48
48
|
import { Privacy, normalizeTerms, privacySettings } from './privacy.mjs';
|
|
49
49
|
import { checkForUpdate, detectInstall, updateCommand, releaseUrl, applyCommand } from './updates.mjs';
|
|
50
|
+
|
|
51
|
+
// Where a person reads the module contract, in the language the room is speaking.
|
|
52
|
+
const SDK_PAGE = { es: 'https://madre.run/sdk/', en: 'https://madre.run/en/sdk/' };
|
|
50
53
|
import { moduleUpdate } from './modules/updates.mjs';
|
|
51
54
|
import { adoptStrays, chatIndex, chatLedger, createChat, deleteChat, isChatId, listChats, openChat as openChatIndex, projectFloor, renameChat, touchChat, MAIN_CHAT } from './chats.mjs';
|
|
52
55
|
import { spawn } from 'node:child_process';
|
|
@@ -193,7 +196,7 @@ export async function createPulseServer({
|
|
|
193
196
|
const install = detectInstall({ projectRoot: canonicalProjectRoot });
|
|
194
197
|
async function versionView({ force = false } = {}) {
|
|
195
198
|
const check = await checkForUpdate({ name: PACKAGE.name, current: PACKAGE.version, cacheFile: join(root, 'updates.json'), fetchImpl: reportFetch, enabled: updatesEnabled(), force });
|
|
196
|
-
return { project: canonicalProjectRoot, ...check, name: PACKAGE.name, install, command: updateCommand(install, PACKAGE.name, check.latest ?? 'latest'), release: check.latest ? releaseUrl(PACKAGE.repository, check.latest) : null, envWins: process.env.PULSE_UPDATE_CHECK !== undefined };
|
|
199
|
+
return { project: canonicalProjectRoot, ...check, name: PACKAGE.name, install, command: updateCommand(install, PACKAGE.name, check.latest ?? 'latest'), release: check.latest ? releaseUrl(PACKAGE.repository, check.latest, roomLanguage()) : null, envWins: process.env.PULSE_UPDATE_CHECK !== undefined };
|
|
197
200
|
}
|
|
198
201
|
// Keeping the day's cache warm. MODULES is served from what is already on disk, so the looking
|
|
199
202
|
// happens after the screen is answered, never in front of it: at most once an hour per room,
|
|
@@ -250,7 +253,7 @@ export async function createPulseServer({
|
|
|
250
253
|
else if (!fifth.ready && index >= 0) { agents.splice(index, 1); changed = true; }
|
|
251
254
|
if (room) {
|
|
252
255
|
room.setInvoker(MADRE_ADAPTER, fifth.ready ? madreInvoker({ memory, ollama: () => ({ ...ollama, chatModel: ollama.madreModel ?? ollama.chatModel }), fetchImpl: reportFetch }) : null);
|
|
253
|
-
if (changed) await room.record('agents.updated', { agents: agents.map((agent) => ({ id: agent.id, label: agent.label, detected: agent.detected, ready: agent.ready, version: agent.version, local: Boolean(agent.local) })), removed: fifth.ready ? [] : [MADRE_AGENT_ID], reason: fifth.ready ? `@madre is in the room · ${fifth.version}` : '@madre left the room: Ollama has no chat model running' });
|
|
256
|
+
if (changed) await room.record('agents.updated', { agents: agents.map((agent) => ({ id: agent.id, label: agent.label, detected: agent.detected, ready: agent.ready, version: agent.version, local: Boolean(agent.local), route: agent.route ?? null })), removed: fifth.ready ? [] : [MADRE_AGENT_ID], reason: fifth.ready ? `@madre is in the room · ${fifth.version}` : '@madre left the room: Ollama has no chat model running' });
|
|
254
257
|
// That a local model is running is not the same as it being of use on this project, and
|
|
255
258
|
// the second thing is the one a person needs told. Said once per model, in the room, with
|
|
256
259
|
// the one button that settles it — the crew line above fires only when the crew CHANGES,
|
|
@@ -465,7 +468,7 @@ export async function createPulseServer({
|
|
|
465
468
|
const index = agents.findIndex((item) => item.id === next.id);
|
|
466
469
|
if (index === -1) { agents.push(next); changed = true; continue; }
|
|
467
470
|
const before = agents[index];
|
|
468
|
-
if (before.detected !== next.detected || before.ready !== next.ready || before.version !== next.version || before.path !== next.path) {
|
|
471
|
+
if (before.detected !== next.detected || before.ready !== next.ready || before.version !== next.version || before.path !== next.path || JSON.stringify(before.route ?? null) !== JSON.stringify(next.route ?? null)) {
|
|
469
472
|
agents[index] = { ...before, ...next };
|
|
470
473
|
changed = true;
|
|
471
474
|
}
|
|
@@ -473,7 +476,7 @@ export async function createPulseServer({
|
|
|
473
476
|
await refreshSessions();
|
|
474
477
|
if (changed && announce) {
|
|
475
478
|
await room.record('agents.updated', {
|
|
476
|
-
agents: agents.map((agent) => ({ id: agent.id, label: agent.label, detected: agent.detected, ready: agent.ready, version: agent.version, local: Boolean(agent.local) })),
|
|
479
|
+
agents: agents.map((agent) => ({ id: agent.id, label: agent.label, detected: agent.detected, ready: agent.ready, version: agent.version, local: Boolean(agent.local), route: agent.route ?? null })),
|
|
477
480
|
removed: [],
|
|
478
481
|
reason: 'the room looked again for the agents on this computer',
|
|
479
482
|
});
|
|
@@ -846,7 +849,7 @@ export async function createPulseServer({
|
|
|
846
849
|
response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
|
|
847
850
|
return response.end(html);
|
|
848
851
|
}
|
|
849
|
-
if (request.method === 'GET' && ['/app.js', '/brands.js', '/troubleshooting.js', '/inquiry.js', '/i18n.js', '/es.js', '/resay.js'].includes(url.pathname)) {
|
|
852
|
+
if (request.method === 'GET' && ['/app.js', '/brands.js', '/troubleshooting.js', '/inquiry.js', '/i18n.js', '/es.js', '/resay.js', '/syntax.js'].includes(url.pathname)) {
|
|
850
853
|
const js = await readFile(join(publicDirectory, url.pathname.slice(1)));
|
|
851
854
|
response.writeHead(200, { 'content-type': 'text/javascript; charset=utf-8' });
|
|
852
855
|
return response.end(js);
|
|
@@ -1474,7 +1477,7 @@ export async function createPulseServer({
|
|
|
1474
1477
|
if (request.method === 'GET' && url.pathname === '/api/extensions') {
|
|
1475
1478
|
const extensions = await describeModules(await moduleContext());
|
|
1476
1479
|
void warmModuleUpdates(extensions);
|
|
1477
|
-
return sendJson(response, 200, { installing, extensions, failures: loadFailures, folders: moduleFolders({ stateRoot: root, projectRoot: canonicalProjectRoot }), sdk:
|
|
1480
|
+
return sendJson(response, 200, { installing, extensions, failures: loadFailures, folders: moduleFolders({ stateRoot: root, projectRoot: canonicalProjectRoot }), sdk: SDK_PAGE[roomLanguage()] ?? SDK_PAGE.es });
|
|
1478
1481
|
}
|
|
1479
1482
|
// The human installs a module file an agent wrote (or they did): checked first, then copied into the chosen folder.
|
|
1480
1483
|
if (request.method === 'POST' && url.pathname === '/api/extensions/install-file') {
|
package/src/updates.mjs
CHANGED
|
@@ -51,7 +51,15 @@ export function applyCommand({ install, name, version, port, projectRoot }) {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
// Where a person reads what a version brought. The product's own house, in the language the room
|
|
55
|
+
// is speaking — a release page on a code host is written for people who already work there, and
|
|
56
|
+
// whoever pressed this button only wanted to know what changed. The repository stays the fallback
|
|
57
|
+
// for a build with no house to point at.
|
|
58
|
+
export const CHANGES_PAGE = { es: 'https://madre.run/cambios/', en: 'https://madre.run/en/changelog/' };
|
|
59
|
+
|
|
60
|
+
export function releaseUrl(repository, version, language = 'es') {
|
|
61
|
+
const page = CHANGES_PAGE[language] ?? CHANGES_PAGE.es;
|
|
62
|
+
if (page) return version ? `${page}#v${version}` : page;
|
|
55
63
|
const url = String(repository?.url ?? repository ?? '').replace(/^git\+/, '').replace(/\.git$/, '');
|
|
56
64
|
return url ? `${url}/releases/tag/v${version}` : null;
|
|
57
65
|
}
|